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:
96
board.sx
96
board.sx
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user