// 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; }