Files
m3te/tests/cascade.sx
swipelab 6f7d2f4db2 lang migration: rename signed integer types sN -> iN
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.
2026-06-12 09:36:51 +03:00

142 lines
4.9 KiB
Plaintext

// Cascade golden: resolve a HAND-CRAFTED seeded board to a stable state and
// snapshot the whole settle loop. The board carries a single horizontal match
// (BBB at row 3, cols 0-2) sitting on the run-free O/G checkerboard, painted so
// that clearing it is NOT the end: col 0 holds R . R R straddling that match's
// cell, so once the B is cleared and gravity packs the column, the three reds
// fall adjacent into a fresh vertical RRR — a SECOND match the first clear set
// up. So the loop runs at least two rounds before it stabilises. The exact
// sequence (per-round boards + final depth) is locked by the snapshot.
//
// Two things are asserted independently of the dump: the final board is stable
// (find_matches empty), and the public `resolve` reproduces the manual loop's
// depth, per-round cleared counts, and final board byte-for-byte. A control
// checkerboard with no initial match resolves at depth 0, untouched.
#import "modules/std.sx";
#import "board.sx";
t :: #import "test.sx";
SEED :: 7;
// Number of rounds the crafted cascade runs. Locked alongside the golden.
EXPECTED_DEPTH :: 2;
// 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 resolving.
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
}
boards_equal :: (a: *Board, b: *Board) -> bool {
for 0..BOARD_CELLS (i) { if a.cells[i] != b.cells[i] { return false; } }
true
}
// The crafted cascade board: checkerboard everywhere except a horizontal BBB at
// row 3 (cols 0-2) and reds salted down col 0 (rows 2,4,5) around that match's
// cell. Clearing BBB punches the col-0 hole between the reds; gravity then packs
// R,R,R adjacent → a vertical match for round 2. Its RNG is seeded from SEED so
// the refill that follows each clear is reproducible.
cascade_board :: () -> Board {
b := load_board(.[
"OGOGOGOG",
"GOGOGOGO",
"RGOGOGOG",
"BBBOGOGO",
"RGOGOGOG",
"ROGOGOGO",
"OGOGOGOG",
"GOGOGOGO",
]);
b.rng = rng_seeded(SEED);
b
}
// A run-free checkerboard with no initial match — the depth-0 control.
checker_board :: () -> Board {
b := load_board(.[
"OGOGOGOG",
"GOGOGOGO",
"OGOGOGOG",
"GOGOGOGO",
"OGOGOGOG",
"GOGOGOGO",
"OGOGOGOG",
"GOGOGOGO",
]);
b.rng = rng_seeded(SEED);
b
}
main :: () -> i32 {
print("== cascade (resolution loop) ==\n");
// Drive the loop one round at a time so each post-round board is visible in
// the snapshot, recording the per-round cleared counts and the depth.
b := cascade_board();
out("start:\n");
out(board_dump(@b));
depth := 0;
counts := List(i64).{};
while true {
n := resolve_step(@b);
if n == 0 { break; }
depth += 1;
counts.append(n);
print("round {}: cleared {} cells\n", depth, n);
out(board_dump(@b));
}
print("cascade depth {}\n", depth);
// The loop reached a stable board.
fm := find_matches(@b);
t.expect(fm.count() == 0, "cascade: final board has no matches");
// A genuine multi-round chain at the expected depth.
t.expect(depth == EXPECTED_DEPTH, "cascade: depth equals expected");
t.expect(depth >= 2, "cascade: chained at least two rounds");
// The public `resolve` on a fresh identical board reproduces the manual
// loop exactly: same depth, same per-round cleared counts, same final board.
b2 := cascade_board();
c := resolve(@b2);
t.expect(c.depth == depth, "cascade: resolve depth matches manual loop");
same_counts := c.cleared.len == counts.len;
if same_counts {
for 0..counts.len (i) {
if c.cleared.items[i] != counts.items[i] { same_counts = false; }
}
}
t.expect(same_counts, "cascade: resolve per-round counts match manual loop");
t.expect(boards_equal(@b, @b2), "cascade: resolve final board matches manual loop");
// Control: a checkerboard with no initial match resolves at depth 0 and is
// left untouched (no clear, no collapse, no refill draw).
ctrl := checker_board();
before := ctrl;
cc := resolve(@ctrl);
t.expect(cc.depth == 0, "control: stable board resolves at depth 0");
t.expect(cc.cleared.len == 0, "control: depth 0 yields an empty per-round list");
t.expect(boards_equal(@before, @ctrl), "control: stable board left unchanged");
print("ok: cascade resolves to a stable board\n");
return 0;
}