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.
204 lines
6.7 KiB
Plaintext
204 lines
6.7 KiB
Plaintext
// 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;
|
|
}
|