// Swap-legality golden: exercise swap / adjacency / swap_legal and the // legal_swaps enumeration. The predicate cases run over HAND-CRAFTED boards and // are asserted directly; the move enumeration runs over the seeded board and is // dumped deterministically and locked as a snapshot. // // Every hand-crafted 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 cells under test overridden — so any match observed is // purely the trial swap's doing, never a pre-existing run. #import "modules/std.sx"; #import "board.sx"; t :: #import "test.sx"; SEED :: 1337; // Inverse of `gem_char`: map a gem character back to its Gem so each board can // be written as a human-readable grid of GEM_CHARS. char_to_gem :: (c: u8) -> Gem { 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 } // Whole-board equality, cell by cell — used to prove a trial swap leaves the // board untouched. boards_equal :: (x: *Board, y: *Board) -> bool { for 0..BOARD_CELLS (i) { if !(x.cells[i] == y.cells[i]) { return false; } } true } cell :: (col: s64, row: s64) -> Cell { Cell.{ col = col, row = row } } main :: () -> s32 { print("== swap & legality ==\n"); // Board whose ONLY swap-formable match is the adjacent (2,3)<->(3,3) // exchange: it slides an R into row 3 to complete R,R,R across cols 0-2. The // B at (2,3) keeps the pre-swap row run-free. Reused by the revert check. scene_3run : []string = .[ "OGOGOGOG", "GOGOGOGO", "OGOGOGOG", "RRBRGOGO", "OGOGOGOG", "GOGOGOGO", "OGOGOGOG", "GOGOGOGO", ]; // Legal: an adjacent swap that forms a 3-run. Swapping (3,3)<->(2,3) brings // R to (2,3), completing R,R,R on row 3 — the match lands on the SECOND // swapped cell, so a one-sided check would miss it. legal_3run := load_board(scene_3run); t.expect(swap_legal(@legal_3run, cell(3, 3), cell(2, 3)) == true, "legal: adjacent swap forms a 3-run"); // Illegal: an adjacent swap that forms NO match. The R guards at (3,2) and // (4,4) break the runs the displaced checkerboard gems would otherwise make. no_match := load_board(.[ "OGOGOGOG", "GOGOGOGO", "OGOROGOG", "GOGOGOGO", "OGOGRGOG", "GOGOGOGO", "OGOGOGOG", "GOGOGOGO", ]); t.expect(swap_legal(@no_match, cell(3, 3), cell(4, 3)) == false, "illegal: adjacent swap forms no match"); // Illegal: a NON-adjacent pair, rejected before any match check. The board // is rigged so that the (0,3)<->(2,3) exchange WOULD complete a B column run // if it were allowed — proving the rejection is by adjacency, not by an // absent match. non_adjacent := load_board(.[ "OGOGOGOG", "GOGOGOGO", "BGOGOGOG", "YOBOGOGO", "BGOGOGOG", "GOGOGOGO", "OGOGOGOG", "GOGOGOGO", ]); t.expect(adjacent(cell(0, 3), cell(2, 3)) == false, "non-adjacent pair is not adjacent"); t.expect(swap_legal(@non_adjacent, cell(0, 3), cell(2, 3)) == false, "illegal: non-adjacent pair rejected without a match check"); // Illegal: a DIAGONAL pair, likewise rejected before any match check. The // (3,3)<->(4,4) exchange WOULD complete a P column run if it were allowed. diagonal := load_board(.[ "OGOGOGOG", "GOGOGOGO", "OGOPOGOG", "GOGYGOGO", "OGOPPGOG", "GOGOGOGO", "OGOGOGOG", "GOGOGOGO", ]); t.expect(adjacent(cell(3, 3), cell(4, 4)) == false, "diagonal pair is not adjacent"); t.expect(swap_legal(@diagonal, cell(3, 3), cell(4, 4)) == false, "illegal: diagonal pair rejected without a match check"); // Legal because only the OTHER swapped gem matches. Player moves the gem at // (4,3) to (5,3); the gem that comes back from (5,3) lands at (4,3) and // completes a B column run there, while the moved gem at (5,3) matches // nothing. Either swapped cell participating is enough. other_gem := load_board(.[ "OGOGOGOG", "GOGOGOGO", "OGOGBGOG", "GOGOYBGO", "OGOGBGOG", "GOGOGOGO", "OGOGOGOG", "GOGOGOGO", ]); t.expect(swap_legal(@other_gem, cell(4, 3), cell(5, 3)) == true, "legal: only the other swapped gem matches"); // Non-mutating: a legality probe and a manual test-then-revert both leave // the board byte-for-byte identical. `swap` is its own inverse. print("== swap revert (non-mutating) ==\n"); revert := load_board(scene_3run); fresh := load_board(scene_3run); out("before:\n"); out(board_dump(@revert)); probe := swap_legal(@revert, cell(3, 3), cell(2, 3)); // trials + reverts swap(@revert, cell(3, 3), cell(2, 3)); // mutate swap(@revert, cell(3, 3), cell(2, 3)); // revert (self-inverse) out("after:\n"); out(board_dump(@revert)); t.expect(probe == true, "probe swap was indeed legal"); t.expect(boards_equal(@revert, @fresh), "board unchanged after probe + test-then-revert"); // Enumeration over the seeded board: a fixed, deterministic list locked as a // snapshot. Order is row-major over the top-left cell, right neighbour // before down neighbour. print("== legal_swaps: seeded {} ==\n", SEED); seeded : Board = ---; seeded.init(SEED); out(board_dump(@seeded)); out("--\n"); moves := legal_swaps(@seeded); out(dump_swaps(@moves)); print("ok: swap legality over hand-crafted boards\n"); return 0; }