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.
175 lines
6.0 KiB
Plaintext
175 lines
6.0 KiB
Plaintext
// 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: i64, row: i64) -> Cell {
|
|
Cell.{ col = col, row = row }
|
|
}
|
|
|
|
main :: () -> i32 {
|
|
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;
|
|
}
|