export function shuffle(arr, rng = Math.random) { const a = [...arr]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(rng() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a; } export class Bag { constructor(ids, { rng = Math.random, state = null } = {}) { this.rng = rng; if (state && Array.isArray(state.bag) && state.bag.length) { this.ids = [...ids]; this.bag = state.bag; this.cursor = state.cursor ?? 0; this.round = state.round ?? 1; this.history = []; this.histPos = -1; this.lastShown = null; } else { this.setIds(ids); } } setIds(ids) { this.ids = [...ids]; this.bag = shuffle(this.ids, this.rng); this.cursor = 0; this.round = 1; this.history = []; this.histPos = -1; this.lastShown = null; } _pop() { if (this.cursor >= this.bag.length) { // new round: reshuffle, avoid immediate repeat of lastShown this.round += 1; this.cursor = 0; if (this.ids.length > 1) { do { this.bag = shuffle(this.ids, this.rng); } while (this.bag[0] === this.lastShown); } else { this.bag = shuffle(this.ids, this.rng); } } return this.bag[this.cursor++]; } next() { // if we stepped back, going next replays forward through history first if (this.histPos < this.history.length - 1) { this.histPos += 1; this.lastShown = this.history[this.histPos]; return this.lastShown; } const id = this._pop(); this.history.push(id); this.histPos = this.history.length - 1; this.lastShown = id; return id; } prev() { if (this.histPos <= 0) return null; this.histPos -= 1; this.lastShown = this.history[this.histPos]; return this.lastShown; } progress() { return { seen: Math.min(this.cursor, this.bag.length), total: this.bag.length, round: this.round }; } serialize() { return { bag: this.bag, cursor: this.cursor, round: this.round }; } }