Mechanical sweep of all .sx sources, plan docs, and tests/expected snapshots for the sx language rename (s8/s16/s32/s64 -> i8/i16/i32/i64). Verified: tools/run_tests.sh 23/23. Note: the ios-sim build has 2 pre-existing 'restart' dot-call errors from the sx opt-in UFCS change (sx a47ea14) — independent of this rename (present pre-sweep); migrated in the follow-up commit.
146 lines
5.3 KiB
Plaintext
146 lines
5.3 KiB
Plaintext
// 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) -> i64 {
|
|
n : i64 = 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 :: () -> i32 {
|
|
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]i64 = ---;
|
|
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;
|
|
}
|