P1.3: adjacent-swap legality (pure sx)
Add swap + legality to the board model: - swap(board, a, b): in-place, self-inverse cell exchange (trial then revert). - adjacent(a, b): orthogonal-adjacency predicate (diagonal/gap = false). - swap_legal(board, a, b): legal iff adjacent AND, after the trial swap, either swapped cell participates in a 3+ match (reuses find_matches); leaves the board unchanged. Non-adjacent/diagonal rejected before any match check. - Cell/Swap structs + legal_swaps(board): all currently-legal swaps in a stable row-major, right-before-down order; dump_swaps for deterministic snapshotting. tests/swap_legality.sx asserts the predicate over hand-crafted boards (legal 3-run, no-match, non-adjacent, diagonal, only-the-other-gem-matches) and the non-mutating revert; locks legal_swaps over the seeded board as a golden.
This commit is contained in:
91
board.sx
91
board.sx
@@ -241,3 +241,94 @@ dump_matches :: (b: *Board, m: *MatchMask) -> string {
|
||||
}
|
||||
buf
|
||||
}
|
||||
|
||||
// ── Swap & legality ──────────────────────────────────────────────────────────
|
||||
// A board cell address. Kept separate from the row-major index so swap callers
|
||||
// and the move enumeration speak in (col, row) like the rest of the model.
|
||||
Cell :: struct {
|
||||
col: s64;
|
||||
row: s64;
|
||||
}
|
||||
|
||||
// Exchange the gems of two cells, in place. `swap` is its own inverse: calling
|
||||
// it again with the same two cells restores the board, so a caller can trial a
|
||||
// swap, inspect the result, then swap back to revert.
|
||||
swap :: (board: *Board, a: Cell, b: Cell) {
|
||||
ai := Board.idx(a.col, a.row);
|
||||
bi := Board.idx(b.col, b.row);
|
||||
tmp := board.cells[ai];
|
||||
board.cells[ai] = board.cells[bi];
|
||||
board.cells[bi] = tmp;
|
||||
}
|
||||
|
||||
// Two cells are orthogonally adjacent iff they differ by exactly one step along
|
||||
// a single axis. The same cell, a diagonal, or any longer gap is not adjacent.
|
||||
adjacent :: (a: Cell, b: Cell) -> bool {
|
||||
if a.row == b.row { return a.col == b.col + 1 or a.col == b.col - 1; }
|
||||
if a.col == b.col { return a.row == b.row + 1 or a.row == b.row - 1; }
|
||||
false
|
||||
}
|
||||
|
||||
// Legality of swapping two cells: legal iff they are orthogonally adjacent AND,
|
||||
// after the swap, at least one of the two swapped cells takes part in a 3+ match
|
||||
// (via `find_matches`). A swap that only completes a run for the OTHER moved gem
|
||||
// still counts — either swapped position participating is enough. Non-adjacent
|
||||
// or diagonal pairs are rejected outright, before any match check. The board is
|
||||
// left UNCHANGED: the trial swap is reverted before returning.
|
||||
swap_legal :: (board: *Board, a: Cell, b: Cell) -> bool {
|
||||
if !adjacent(a, b) { return false; }
|
||||
swap(board, a, b);
|
||||
m := find_matches(board);
|
||||
legal := m.at(a.col, a.row) or m.at(b.col, b.row);
|
||||
swap(board, a, b); // revert the trial swap
|
||||
legal
|
||||
}
|
||||
|
||||
// One legal move: an unordered pair of adjacent cells. By construction `a` is
|
||||
// the top-left cell of the pair and `b` is its right (same row) or down (same
|
||||
// col) neighbour, so each adjacency is represented once — never as both (a, b)
|
||||
// and (b, a).
|
||||
Swap :: struct {
|
||||
a: Cell;
|
||||
b: Cell;
|
||||
}
|
||||
|
||||
// Enumerate every currently-legal swap in a stable order: row-major over the
|
||||
// top-left cell of each pair, and for each cell its right neighbour before its
|
||||
// down neighbour. This visits each orthogonal adjacency exactly once. The order
|
||||
// is fixed (independent of board contents), so later hint / no-moves logic and
|
||||
// the snapshot can depend on it.
|
||||
legal_swaps :: (board: *Board) -> List(Swap) {
|
||||
result := List(Swap).{};
|
||||
for 0..BOARD_ROWS: (row) {
|
||||
for 0..BOARD_COLS: (col) {
|
||||
here := Cell.{ col = col, row = row };
|
||||
if col + 1 < BOARD_COLS {
|
||||
right := Cell.{ col = col + 1, row = row };
|
||||
if swap_legal(board, here, right) {
|
||||
result.append(Swap.{ a = here, b = right });
|
||||
}
|
||||
}
|
||||
if row + 1 < BOARD_ROWS {
|
||||
down := Cell.{ col = col, row = row + 1 };
|
||||
if swap_legal(board, here, down) {
|
||||
result.append(Swap.{ a = here, b = down });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
// Deterministic textual dump of an enumerated swap list, in list order: a count
|
||||
// header, then one swap per line as its unordered cell pair `(col,row)-(col,row)`
|
||||
// with the canonical top-left cell first. An empty list (no legal moves) dumps
|
||||
// as just "0 legal swaps", which reads unambiguously. Suitable for snapshotting.
|
||||
dump_swaps :: (swaps: *List(Swap)) -> string {
|
||||
result := format("{} legal swaps\n", swaps.len);
|
||||
for 0..swaps.len: (i) {
|
||||
s := swaps.items[i];
|
||||
result = concat(result, format("({},{})-({},{})\n", s.a.col, s.a.row, s.b.col, s.b.row));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user