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
|
||||
}
|
||||
|
||||
1
tests/expected/refill.exit
Normal file
1
tests/expected/refill.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
39
tests/expected/refill.stdout
Normal file
39
tests/expected/refill.stdout
Normal file
@@ -0,0 +1,39 @@
|
||||
== refill (seeded) ==
|
||||
start:
|
||||
OGOGOGOG
|
||||
RRROGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGYG
|
||||
GOPPPOYO
|
||||
OGOGOGYG
|
||||
GOGOGOGO
|
||||
after clear:
|
||||
OGOGOGOG
|
||||
...OGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOG.G
|
||||
GO...O.O
|
||||
OGOGOG.G
|
||||
GOGOGOGO
|
||||
after collapse:
|
||||
.....G.G
|
||||
OG.GOO.O
|
||||
OGOOGG.G
|
||||
GOOGOOOO
|
||||
OGGOGGGG
|
||||
GOOGOOOO
|
||||
OGOGOGGG
|
||||
GOGOGOGO
|
||||
after refill:
|
||||
RROPYGGG
|
||||
OGRGOOGO
|
||||
OGOOGGPG
|
||||
GOOGOOOO
|
||||
OGGOGGGG
|
||||
GOOGOOOO
|
||||
OGOGOGGG
|
||||
GOGOGOGO
|
||||
filled 9 holes
|
||||
ok: refill fills every hole from the seeded stream
|
||||
145
tests/refill.sx
Normal file
145
tests/refill.sx
Normal file
@@ -0,0 +1,145 @@
|
||||
// Refill golden: run the full resolution pipeline clear -> collapse -> refill
|
||||
// over a HAND-CRAFTED seeded board and snapshot every stage. The starting board
|
||||
// is the run-free O/G checkerboard from match_detect carrying three disjoint
|
||||
// matches (RRR row 1, PPP row 5, YYY col 6); clearing punches 9 holes, gravity
|
||||
// floats them to the top of their columns, and refill drops fresh gems in.
|
||||
//
|
||||
// Determinism is shown three independent ways, all locked by the snapshot and by
|
||||
// asserts:
|
||||
// * stream-continuation — each refilled hole holds exactly the NEXT draw of the
|
||||
// board's own RNG, replayed from the pre-refill state. A reseed-from-scratch
|
||||
// or a constant fill both fail this.
|
||||
// * reproducibility — the same start + seed refills to a byte-identical board.
|
||||
// * threading across refills — re-opening the just-filled holes and refilling
|
||||
// again yields DIFFERENT gems, proving the RNG advances rather than reseeding.
|
||||
#import "modules/std.sx";
|
||||
#import "board.sx";
|
||||
t :: #import "test.sx";
|
||||
|
||||
SEED :: 1337;
|
||||
|
||||
// Inverse of `gem_char`: map a board character back to its Gem so the starting
|
||||
// board can be written as a human-readable grid. The hole glyph maps to `.empty`.
|
||||
char_to_gem :: (c: u8) -> Gem {
|
||||
if c == EMPTY_CHAR { return .empty; }
|
||||
for 0..GEM_COUNT: (i) {
|
||||
if GEM_CHARS[i] == c { return cast(Gem) i; }
|
||||
}
|
||||
.red
|
||||
}
|
||||
|
||||
// Load an 8x8 board from `rows` (top row first, each exactly BOARD_COLS chars).
|
||||
// The RNG is left unseeded — callers seed it before drawing.
|
||||
load_board :: (rows: []string) -> Board {
|
||||
b : Board = ---;
|
||||
for 0..BOARD_ROWS: (row) {
|
||||
line := rows[row];
|
||||
for 0..BOARD_COLS: (col) {
|
||||
b.set(col, row, char_to_gem(line[col]));
|
||||
}
|
||||
}
|
||||
b
|
||||
}
|
||||
|
||||
count_empties :: (b: *Board) -> s64 {
|
||||
n : s64 = 0;
|
||||
for 0..BOARD_CELLS: (i) { if b.cells[i] == .empty { n += 1; } }
|
||||
n
|
||||
}
|
||||
|
||||
boards_equal :: (a: *Board, b: *Board) -> bool {
|
||||
for 0..BOARD_CELLS: (i) { if a.cells[i] != b.cells[i] { return false; } }
|
||||
true
|
||||
}
|
||||
|
||||
// A fresh starting board with its RNG seeded from SEED. Because the RNG lives on
|
||||
// the board, the refill it later performs is reproducible for SEED. The board is
|
||||
// a run-free checkerboard carrying three disjoint matches — a horizontal RRR run
|
||||
// (row 1, cols 0-2), a horizontal PPP run (row 5, cols 2-4) and a vertical YYY
|
||||
// run (col 6, rows 4-6) — so clearing them punches 9 holes across six columns.
|
||||
fresh_board :: () -> Board {
|
||||
b := load_board(.[
|
||||
"OGOGOGOG",
|
||||
"RRROGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGYG",
|
||||
"GOPPPOYO",
|
||||
"OGOGOGYG",
|
||||
"GOGOGOGO",
|
||||
]);
|
||||
b.rng = rng_seeded(SEED);
|
||||
b
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
print("== refill (seeded) ==\n");
|
||||
|
||||
// Pipeline, snapshotting each stage.
|
||||
b := fresh_board();
|
||||
out("start:\n"); out(board_dump(@b));
|
||||
clear_matches(@b);
|
||||
out("after clear:\n"); out(board_dump(@b));
|
||||
collapse(@b);
|
||||
out("after collapse:\n"); out(board_dump(@b));
|
||||
|
||||
// Snapshot the holed board (cells + RNG state) BEFORE refill, so we can both
|
||||
// check which cells refill touched and replay the stream from the same state.
|
||||
pre := b;
|
||||
filled := refill(@b);
|
||||
out("after refill:\n"); out(board_dump(@b));
|
||||
print("filled {} holes\n", filled);
|
||||
|
||||
// (1) No empty cells remain.
|
||||
t.expect(count_empties(@b) == 0, "refill: board has zero empty cells");
|
||||
|
||||
// (2) Stream continuation + not-a-constant: every refilled cell holds exactly
|
||||
// the next draw of the board's RNG, taken row-major from the pre-refill state,
|
||||
// and the drawn gems are not all identical.
|
||||
v := Rng.{ state = pre.rng.state };
|
||||
stream_ok := true;
|
||||
distinct := false;
|
||||
have_first := false;
|
||||
first : Gem = .empty;
|
||||
for 0..BOARD_CELLS: (i) {
|
||||
if pre.cells[i] == .empty {
|
||||
want := cast(Gem) v.next_range(GEM_COUNT);
|
||||
if b.cells[i] != want { stream_ok = false; }
|
||||
if !have_first { first = b.cells[i]; have_first = true; }
|
||||
else if b.cells[i] != first { distinct = true; }
|
||||
}
|
||||
}
|
||||
t.expect(stream_ok, "refill: each hole holds the next seeded-stream gem");
|
||||
t.expect(distinct, "refill: drawn gems vary (not a fixed constant)");
|
||||
|
||||
// (3) Reproducibility: same start + seed refills to a byte-identical board.
|
||||
b2 := fresh_board();
|
||||
clear_matches(@b2);
|
||||
collapse(@b2);
|
||||
refill(@b2);
|
||||
t.expect(boards_equal(@b, @b2), "refill: same start + seed -> identical board");
|
||||
|
||||
// (4) Threading across refills: re-open exactly the cells the first refill
|
||||
// filled, then refill again. The board's RNG has advanced past the first
|
||||
// fill, so the second fill draws new gems — proof it does NOT reseed per call.
|
||||
holes_n := 0;
|
||||
hole_idx : [BOARD_CELLS]s64 = ---;
|
||||
fill1 : [BOARD_CELLS]Gem = ---;
|
||||
for 0..BOARD_CELLS: (i) {
|
||||
if pre.cells[i] == .empty {
|
||||
hole_idx[holes_n] = i;
|
||||
fill1[holes_n] = b.cells[i];
|
||||
holes_n += 1;
|
||||
}
|
||||
}
|
||||
for 0..holes_n: (k) { b.cells[hole_idx[k]] = .empty; }
|
||||
refill(@b);
|
||||
differs := false;
|
||||
for 0..holes_n: (k) {
|
||||
if b.cells[hole_idx[k]] != fill1[k] { differs = true; }
|
||||
}
|
||||
t.expect(differs, "refill: a second refill of the same holes draws new gems (RNG threads, no reseed)");
|
||||
|
||||
print("ok: refill fills every hole from the seeded stream\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user