// Clear golden: run detect→clear over several HAND-CRAFTED boards and snapshot // the post-clear board. Each board sits on the run-free O/G checkerboard from // match_detect (adjacent cells always differ, so it has zero pre-existing // matches) with only the runs under test painted in — so any hole in the result // is purely the cleared match's doing. For each scene the before/after boards // are printed, and three facts are asserted independently of the dump: matched // cells became holes, non-matched cells are byte-identical, and the cleared // count is exact. The boards (and their match counts) mirror match_detect.sx. #import "modules/std.sx"; #import "board.sx"; t :: #import "test.sx"; // Inverse of `gem_char`: map a gem character back to its Gem so each board can // be written as a human-readable grid. The hole glyph maps to `.empty`, so a // board can be hand-written with pre-existing holes (cells left by a prior // clear) for the holes-never-match regression. 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 gem // characters). 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 } // Detect→clear one scene, snapshot before/after, and assert the three clear // invariants against the matched-cell set: every flagged cell is now a hole, // every unflagged cell is unchanged, and the returned count is exact. scene :: (name: string, rows: []string, want_cleared: i64) { b := load_board(rows); orig := load_board(rows); // pristine copy for the unchanged check m := find_matches(@b); cleared := clear_cells(@b, @m); print("== {} ==\n", name); out("before:\n"); out(board_dump(@orig)); out("after:\n"); out(board_dump(@b)); cleared_holes := true; // every matched cell is now a hole others_intact := true; // every other cell is byte-identical for 0..BOARD_CELLS (i) { if m.cells[i] { if !(b.cells[i] == .empty) { cleared_holes = false; } } else { if !(b.cells[i] == orig.cells[i]) { others_intact = false; } } } t.expect(cleared_holes, concat(name, ": cleared cells are holes")); t.expect(others_intact, concat(name, ": non-matched cells unchanged")); t.expect(cleared == want_cleared, concat(name, ": cleared count exact")); } main :: () -> i32 { print("== clear (detect -> clear) ==\n"); // Single horizontal 3-run (row 3, cols 2-4) → three holes there only. scene("horizontal-3", .[ "OGOGOGOG", "GOGOGOGO", "OGOGOGOG", "GORRROGO", "OGOGOGOG", "GOGOGOGO", "OGOGOGOG", "GOGOGOGO", ], 3); // Single vertical 3-run (col 5, rows 2-4). scene("vertical-3", .[ "OGOGOGOG", "GOGOGOGO", "OGOGOBOG", "GOGOGBGO", "OGOGOBOG", "GOGOGOGO", "OGOGOGOG", "GOGOGOGO", ], 3); // Disjoint runs: horizontal R (row 1), horizontal P (row 5), vertical Y // (col 6) — three separate hole clusters, 9 cells total. scene("disjoint-runs", .[ "OGOGOGOG", "RRROGOGO", "OGOGOGOG", "GOGOGOGO", "OGOGOGYG", "GOPPPOYO", "OGOGOGYG", "GOGOGOGO", ], 9); // Overlapping L and T: a horizontal run and a vertical run share a cell (the // L's corner (1,1), the T's stem-top (4,5)). The mask already unions the // shared cell, so clear removes the whole union as one set — 10 holes, not // 11 — exercising the overlapping-clear acceptance case. scene("L-and-T", .[ "OGOGOGOG", "GRRRGOGO", "OROGOGOG", "GRGOGOGO", "OGOGOGOG", "GOGYYYGO", "OGOGYGOG", "GOGOYOGO", ], 10); // No matches: the bare checkerboard is left completely unchanged (0 holes), // so its before/after dumps are identical. scene("no-matches", .[ "OGOGOGOG", "GOGOGOGO", "OGOGOGOG", "GOGOGOGO", "OGOGOGOG", "GOGOGOGO", "OGOGOGOG", "GOGOGOGO", ], 0); // Holes never match: a checkerboard carrying a horizontal 3-run of holes // (row 3, cols 2-4) and a vertical 3-run of holes (col 1, rows 5-7), left by // earlier clears. A line of 3+ holes is NOT a match, so detect finds nothing, // clear removes nothing, and before/after are identical. Without this, a // post-clear board would keep re-"matching" its own holes and the P2.4 // cascade would never stabilise. scene("holes-no-match", .[ "OGOGOGOG", "GOGOGOGO", "OGOGOGOG", "GO...OGO", "OGOGOGOG", "G.GOGOGO", "O.OGOGOG", "G.GOGOGO", ], 0); // clear_matches: the one-call detect+clear returns the same cleared count // and punches the holes itself. cm := load_board(.[ "OGOGOGOG", "GOGOGOGO", "OGOGOGOG", "GORRROGO", "OGOGOGOG", "GOGOGOGO", "OGOGOGOG", "GOGOGOGO", ]); t.expect(clear_matches(@cm) == 3, "clear_matches: detect+clear returns count"); t.expect(cm.at(2, 3) == .empty and cm.at(3, 3) == .empty and cm.at(4, 3) == .empty, "clear_matches: matched run is now holes"); // Holes are never matchable: a board whose only equal-adjacent runs are // holes yields an empty match set, and clear_matches reports 0 (no change). holes := load_board(.[ "OGOGOGOG", "GOGOGOGO", "OGOGOGOG", "GO...OGO", "OGOGOGOG", "G.GOGOGO", "O.OGOGOG", "G.GOGOGO", ]); hm := find_matches(@holes); t.expect(hm.count() == 0, "holes: a line of 3+ holes is not a match"); t.expect(clear_matches(@holes) == 0, "holes: clear_matches returns 0 on a holes-only board"); // Cascade base case: after a real clear punches a 3-in-a-line into holes, // re-detecting on the cleared board must find nothing — otherwise the P2.4 // cascade loop would re-match its own holes and never terminate. casc := load_board(.[ "OGOGOGOG", "GOGOGOGO", "OGOGOBOG", "GOGOGBGO", "OGOGOBOG", "GOGOGOGO", "OGOGOGOG", "GOGOGOGO", ]); t.expect(clear_matches(@casc) == 3, "cascade: first clear removes the vertical 3-run"); t.expect(clear_matches(@casc) == 0, "cascade: re-clear on the holed board returns 0"); print("ok: clear over hand-crafted boards\n"); return 0; }