Files
m3te/tests/collapse.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

147 lines
5.1 KiB
Plaintext

// Collapse golden: run gravity over several HAND-CRAFTED boards and snapshot the
// post-collapse board. Unlike the clear/match goldens these boards need not be
// run-free — `collapse` never inspects matches, it only moves gems past holes —
// so each column is painted to exercise a distinct case (holes in the middle, at
// the bottom, a full column of holes, a column with none, a lone gem, an
// alternating stack). Distinct gem letters are stacked vertically so the
// top-to-bottom order is observable in the dump.
//
// For each scene the before/after boards are printed, and two facts are asserted
// independently of the dump: every column ends with its original gems (same
// top-to-bottom order) packed at the BOTTOM and all holes contiguous above, and
// the returned `moved` flag is exact.
#import "modules/std.sx";
#import "board.sx";
t :: #import "test.sx";
// Inverse of `gem_char`: map a board character back to its Gem. The hole glyph
// maps to `.empty`, so a board can be hand-written with holes in any position.
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).
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
}
// The collapse invariant, checked column by column against the original board:
// each column's gems (its non-hole cells, in top-to-bottom order) must reappear
// packed contiguously at the BOTTOM in that same order, with every cell above
// them a hole. This single check covers holes-bubble-to-top, gems-settle-to-
// bottom, order-preservation, and the all-holes / no-holes edge columns at once.
check_collapsed :: (orig: *Board, b: *Board) -> bool {
for 0..BOARD_COLS (col) {
gems : [BOARD_ROWS]Gem = ---;
n := 0;
for 0..BOARD_ROWS (row) {
g := orig.at(col, row);
if g != .empty { gems[n] = g; n += 1; }
}
boundary := BOARD_ROWS - n; // first row that must hold a gem
for 0..BOARD_ROWS (row) {
if row < boundary {
if b.at(col, row) != .empty { return false; }
} else {
if b.at(col, row) != gems[row - boundary] { return false; }
}
}
}
true
}
// Collapse one scene, snapshot before/after, and assert the collapse invariant
// plus the exact `moved` flag.
scene :: (name: string, rows: []string, want_moved: bool) {
b := load_board(rows);
orig := load_board(rows); // pristine copy for the invariant check
moved := collapse(@b);
print("== {} ==\n", name);
out("before:\n");
out(board_dump(@orig));
out("after:\n");
out(board_dump(@b));
t.expect(check_collapsed(@orig, @b), concat(name, ": gems packed bottom, holes top, order preserved"));
t.expect(moved == want_moved, concat(name, ": moved flag exact"));
}
main :: () -> i32 {
print("== collapse (gravity) ==\n");
// Eight independent columns, one case each (top-to-bottom):
// col0 holes in the MIDDLE: R O Y G B straddle three holes -> all fall.
// col1 holes at the BOTTOM: R O Y sit on top -> fall to the floor.
// col2 a FULL column of holes -> stays all holes.
// col3 NO holes (eight gems) -> unchanged.
// col4 already settled (holes already at the top) -> unchanged.
// col5 a LONE gem at the very top -> drops to the floor.
// col6 an ALTERNATING gem/hole stack -> gems pack, order preserved.
// col7 three gems already resting on the floor -> unchanged.
scene("varied", .[
"RR.R.RR.",
".O.O....",
".Y.Y..O.",
"O..GR...",
"Y..BO.Y.",
"...RY..R",
"G..OG.GO",
"B..YB..Y",
], true);
// No holes anywhere: gravity has nothing to do, board is left byte-identical.
scene("no-holes", .[
"ROYGBPRO",
"YGBPROYG",
"BPROYGBP",
"ROYGBPRO",
"YGBPROYG",
"BPROYGBP",
"ROYGBPRO",
"YGBPROYG",
], false);
// Every cell a hole: an empty board collapses to itself, nothing moves.
scene("all-holes", .[
"........",
"........",
"........",
"........",
"........",
"........",
"........",
"........",
], false);
// Already settled: every column has its holes contiguous at the top and its
// gems contiguous at the bottom (this IS the post-collapse form of "varied").
// Re-collapsing must move nothing and leave the board unchanged — the
// idempotency the P2.4 cascade relies on to detect that gravity has stopped.
scene("settled", .[
"...R....",
"...O....",
"...Y....",
"R..GR...",
"O..BO.R.",
"YR.RY.OR",
"GO.OG.YO",
"BY.YBRGY",
], false);
print("ok: collapse over hand-crafted boards\n");
return 0;
}