Add collapse(board): per-column gravity that packs gems contiguously at
the bottom (preserving top-to-bottom order) and bubbles holes to the top,
with no horizontal movement. Returns whether any gem fell, for the P2.4
cascade. Does not refill (that is P2.3).
tests/collapse.sx snapshots gravity over hand-crafted boards exercising
holes in the middle / at the bottom, a full column of holes, a column
with none, a lone gem, an alternating stack, and an already-settled board
(idempotency). Asserts, independently of the dump, that each column's gems
end packed at the bottom in original order with holes above, plus the
exact moved flag. Golden locked in tests/expected/collapse.{stdout,exit}.
147 lines
5.1 KiB
Plaintext
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 :: () -> s32 {
|
|
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;
|
|
}
|