// The offline tile bag — the shuffled draw pile for one local game. Structurally a port of // backend/internal/engine/bag.go (fill from the variant's counts + blanks, draw from the end, // return-and-reshuffle for an exchange), but shuffled with a small DETERMINISTIC in-house PRNG // rather than Go's math/rand: a local game only needs to be reproducible from its own seed and // sequence of operations (for replay from the stored journal), not bit-identical to a server // game (docs plan). Blanks ride as BLANK_INDEX, matching lib/alphabet.ts (and the engine's // blankTile = 0xff = 255). import { RULESETS } from './ruleset'; import { BLANK_INDEX } from '../alphabet'; import type { Variant } from '../model'; // mulberry32 is a compact deterministic PRNG returning a float in [0, 1). Seeded from the game // seed so the shuffle sequence — and thus the draws — replay identically for the same seed and // the same sequence of returns. function mulberry32(seed: number): () => number { let a = seed >>> 0; return () => { a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; } /** * Bag is one local game's draw pile. Construct it with the variant and a numeric seed; draw() * and return() mutate it in place. It is reproducible: the same seed and the same sequence of * operations yield the same draws. */ export class Bag { private tiles: number[]; private readonly rand: () => number; constructor(variant: Variant, seed: number) { const rs = RULESETS[variant]; const tiles: number[] = []; for (let i = 0; i < rs.counts.length; i++) { for (let n = 0; n < rs.counts[i]; n++) tiles.push(i); } for (let n = 0; n < rs.blanks; n++) tiles.push(BLANK_INDEX); this.tiles = tiles; this.rand = mulberry32(seed); this.shuffle(); } /** length is the number of tiles left in the bag. */ get length(): number { return this.tiles.length; } /** * draw removes up to n tiles from the end of the bag and returns them. Drawing more than * remain returns all of them; drawing from an empty bag returns an empty array. */ draw(n: number): number[] { const take = Math.min(n, this.tiles.length); return this.tiles.splice(this.tiles.length - take, take); } /** return puts tiles back into the bag and reshuffles, as when a player exchanges tiles. */ return(tiles: readonly number[]): void { for (const t of tiles) this.tiles.push(t); this.shuffle(); } // shuffle randomises the remaining tiles in place with the bag's own PRNG (Fisher–Yates). private shuffle(): void { for (let i = this.tiles.length - 1; i > 0; i--) { const j = Math.floor(this.rand() * (i + 1)); const tmp = this.tiles[i]; this.tiles[i] = this.tiles[j]; this.tiles[j] = tmp; } } }