P1.2: match detection (3+ horizontal/vertical runs)

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.
This commit is contained in:
swipelab
2026-06-04 19:27:15 +03:00
parent 169f592951
commit b0c081e397
4 changed files with 327 additions and 0 deletions

View File

@@ -145,3 +145,99 @@ board_dump :: (self: *Board) -> string {
}
buf
}
// ── Match detection ────────────────────────────────────────────────────────
// Per-cell membership over the board: cell (col, row) is `true` iff it takes
// part in some horizontal or vertical run of three or more same-type gems.
// This mask IS the matched-cell SET — overlapping shapes (an L or a T where a
// horizontal and a vertical run share a cell) collapse to a single `true`, so
// the union is automatic. The layout mirrors Board.cells exactly so the
// clear/cascade phase can consume it without translation.
MatchMask :: struct {
cells: [BOARD_CELLS]bool;
at :: (self: *MatchMask, col: s64, row: s64) -> bool {
self.cells[Board.idx(col, row)]
}
count :: (self: *MatchMask) -> s64 {
n : s64 = 0;
for 0..BOARD_CELLS: (i) { if self.cells[i] { n += 1; } }
n
}
}
// Mark a closed span of cells along one axis. `vertical` picks the axis; `fixed`
// is the constant coordinate (the row for a horizontal span, the column for a
// vertical one) and the span covers `start..end` of the moving coordinate.
mark_run :: (m: *MatchMask, vertical: bool, fixed: s64, start: s64, end: s64) {
for start..end: (i) {
if vertical {
m.cells[Board.idx(fixed, i)] = true;
} else {
m.cells[Board.idx(i, fixed)] = true;
}
}
}
// Detect every maximal horizontal and vertical run of length >= 3 and mark all
// participating cells. Each row and column is scanned once, extending a run
// while the gem type holds; a maximal run of length >= 3 marks its whole span,
// so length-4 / length-5 runs are simply longer spans of the same walk. A cell
// shared by an intersecting horizontal and vertical run is marked once per
// axis into the same slot — idempotent, so the union counts it once.
find_matches :: (b: *Board) -> MatchMask {
m : MatchMask = ---;
for 0..BOARD_CELLS: (i) { m.cells[i] = false; }
// Horizontal: walk each row left-to-right in maximal same-type spans.
for 0..BOARD_ROWS: (row) {
col := 0;
while col < BOARD_COLS {
g := b.at(col, row);
run_end := col + 1;
while run_end < BOARD_COLS and b.at(run_end, row) == g {
run_end += 1;
}
if run_end - col >= 3 { mark_run(@m, false, row, col, run_end); }
col = run_end;
}
}
// Vertical: walk each column top-to-bottom in maximal same-type spans.
for 0..BOARD_COLS: (col) {
row := 0;
while row < BOARD_ROWS {
g := b.at(col, row);
run_end := row + 1;
while run_end < BOARD_ROWS and b.at(col, run_end) == g {
run_end += 1;
}
if run_end - row >= 3 { mark_run(@m, true, col, row, run_end); }
row = run_end;
}
}
m
}
// Deterministic textual dump of a matched-cell SET, in the same row-major grid
// shape as `board_dump`: a matched cell shows its gem character, an unmatched
// cell shows '.'. A board with no matches dumps as an all-'.' grid, which reads
// unambiguously as the empty set. Suitable for snapshotting.
dump_matches :: (b: *Board, m: *MatchMask) -> string {
line_w := BOARD_COLS + 1; // 8 cells + newline
buf := cstring(BOARD_ROWS * line_w);
for 0..BOARD_ROWS: (row) {
base := row * line_w;
for 0..BOARD_COLS: (col) {
if m.at(col, row) {
buf[base + col] = gem_char(b.at(col, row));
} else {
buf[base + col] = 46; // '.'
}
}
buf[base + BOARD_COLS] = 10; // '\n'
}
buf
}