P2.3: seeded refill (pure sx)
Add refill to the board model: every .empty hole is filled with a fresh gem drawn from the board's OWN seeded RNG, so refills are fully reproducible for a seed and continue the stream rather than reseeding. - Board now owns its RNG state (rng: Rng); init seeds and draws from it, so draws after init/clears thread deterministically. The init draw sequence is unchanged, so board_init's golden is byte-identical. - refill(board) fills all holes in row-major order wherever they sit (does not assume collapse ran) and makes no attempt to avoid matches — a refill may create new runs, which drives the P2.4 cascade. - tests/refill.sx (fixed seed) runs clear -> collapse -> refill, locks the staged dump as a golden, and asserts: zero empties after refill; each hole holds the next seeded-stream gem (replayed from the pre-refill state); drawn gems vary (not a constant); same start+seed -> identical board; a second refill of the same holes draws new gems (RNG threads, no reseed).
This commit is contained in:
38
board.sx
38
board.sx
@@ -81,6 +81,13 @@ Board :: struct {
|
||||
// Row-major: cell (col, row) lives at row*BOARD_COLS + col.
|
||||
cells: [BOARD_CELLS]Gem;
|
||||
|
||||
// The board's own deterministic RNG. `init` seeds it, then every later draw
|
||||
// — refill (P2.3) and the cascade beyond — advances THIS state, so the whole
|
||||
// gem stream for a seed is reproducible and successive refills continue the
|
||||
// sequence instead of reseeding. A hand-built board (one made without `init`)
|
||||
// must seed this before any draw.
|
||||
rng: Rng;
|
||||
|
||||
idx :: (col: s64, row: s64) -> s64 {
|
||||
row * BOARD_COLS + col
|
||||
}
|
||||
@@ -100,10 +107,10 @@ Board :: struct {
|
||||
// drawn from the remaining allowed types. At most two types are ever
|
||||
// excluded, so a choice always remains.
|
||||
init :: (self: *Board, seed: s64) {
|
||||
rng := rng_seeded(seed);
|
||||
self.rng = rng_seeded(seed);
|
||||
for 0..BOARD_ROWS: (row) {
|
||||
for 0..BOARD_COLS: (col) {
|
||||
self.set(col, row, pick_gem(self, @rng, col, row));
|
||||
self.set(col, row, pick_gem(self, @self.rng, col, row));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -421,3 +428,30 @@ collapse :: (board: *Board) -> bool {
|
||||
}
|
||||
moved
|
||||
}
|
||||
|
||||
// ── Refill (P2.3) ──────────────────────────────────────────────────────────────
|
||||
// Final step of the resolution pipeline: drop a fresh gem into every hole. Each
|
||||
// `.empty` cell is replaced by a gem drawn from the board's OWN seeded RNG, so a
|
||||
// given seed always produces the same refill and successive refills continue the
|
||||
// stream rather than repeating — the state threads through `init`, clears and
|
||||
// prior refills, never reseeding. Holes are filled wherever they sit, in
|
||||
// row-major order, so refill does not assume `collapse` ran first.
|
||||
//
|
||||
// Unlike `init`, refill makes NO attempt to avoid matches: a refilled gem may
|
||||
// complete a new run, which is exactly what drives the P2.4 cascade. `next_range`
|
||||
// only ever yields ordinals 0..GEM_COUNT, so a hole is never refilled with
|
||||
// `.empty`; afterwards the board has no holes left. Returns the number of cells
|
||||
// filled (0 on a board that had none).
|
||||
refill :: (board: *Board) -> s64 {
|
||||
rng := @board.rng;
|
||||
filled : s64 = 0;
|
||||
for 0..BOARD_ROWS: (row) {
|
||||
for 0..BOARD_COLS: (col) {
|
||||
if board.at(col, row) == .empty {
|
||||
board.set(col, row, cast(Gem) rng.next_range(GEM_COUNT));
|
||||
filled += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
filled
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user