Add the first resolution-pipeline step to the headless board model.
- Introduce an `empty` hole sentinel on the Gem enum (ordinal 6, outside
GEM_COUNT so the RNG/pick_gem never draw it). board_dump renders holes as
EMPTY_CHAR ('.') via a single branch in gem_char, leaving boards without
holes byte-identical to before (existing goldens unchanged).
- clear_cells(board, mask): set every matched cell to `.empty`, leave all
others untouched, return the count cleared.
- clear_matches(board): detect+clear in one call; returns 0 (board unchanged)
when there are no matches.
No gravity or refill yet (P2.2 / P2.3).
tests/clear.sx applies detect->clear to hand-crafted boards (single
horizontal/vertical runs, disjoint runs, an overlapping L/T whose shared cell
clears once, and a no-match checkerboard), snapshots before/after, and asserts
matched cells became holes, non-matched cells are unchanged, and the cleared
count is exact. Locked as tests/expected/clear.{stdout,exit}.
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.
Add a pure-sx match detector to the board model: `find_matches` walks each
row and column once in maximal same-type spans and marks every cell in a run
of length >= 3 into a `MatchMask` (a per-cell membership set mirroring
Board.cells). Overlapping shapes (L / T where a horizontal and vertical run
share a cell) collapse to the union automatically. `dump_matches` renders the
set deterministically: matched cells show their gem char, others '.'.
Detection only — no clear/collapse/refill (that is P2.1).
tests/match_detect.sx exercises hand-crafted boards (built explicitly on a
run-free checkerboard, no seeded init): a horizontal 3-run, a vertical 3-run,
multiple disjoint runs, length-4 and length-5 runs, intersecting L and T
shapes (shared cell counted once), and a no-match board. Output is locked as
tests/expected/match_detect.stdout (+ .exit) and asserts matched-cell counts.
Add board.sx, the headless Phase-1 match-3 core:
- Gem enum (6 types, ordinal 0..5) + single-char dump alphabet.
- Rng: a 32-bit LCG carried in s64, masked to 32 bits each step, so the
stream is host-width independent and valid for any seed.
- Board (8x8, row-major) with idx/at/set accessors and a seeded init that
fills row-major, excluding any gem that would complete a 3-in-a-row with
the two cells to the left or above — so the result has zero pre-existing
matches. Single RNG draw per cell, always terminates.
- board_dump: deterministic one-row-per-line textual snapshot.
tests/board_init.sx seeds with a fixed seed, dumps the board, and asserts
zero horizontal/vertical 3-in-a-row runs via an independent scan. Output and
exit code are locked as goldens. App ios-sim build is unaffected (main.sx
does not import the model yet).