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
|
||||
}
|
||||
|
||||
1
tests/expected/match_detect.exit
Normal file
1
tests/expected/match_detect.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
109
tests/expected/match_detect.stdout
Normal file
109
tests/expected/match_detect.stdout
Normal file
@@ -0,0 +1,109 @@
|
||||
== horizontal-3 ==
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GORRROGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
--
|
||||
........
|
||||
........
|
||||
........
|
||||
..RRR...
|
||||
........
|
||||
........
|
||||
........
|
||||
........
|
||||
== vertical-3 ==
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOBOG
|
||||
GOGOGBGO
|
||||
OGOGOBOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
--
|
||||
........
|
||||
........
|
||||
.....B..
|
||||
.....B..
|
||||
.....B..
|
||||
........
|
||||
........
|
||||
........
|
||||
== disjoint-runs ==
|
||||
OGOGOGOG
|
||||
RRROGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGYG
|
||||
GOPPPOYO
|
||||
OGOGOGYG
|
||||
GOGOGOGO
|
||||
--
|
||||
........
|
||||
RRR.....
|
||||
........
|
||||
........
|
||||
......Y.
|
||||
..PPP.Y.
|
||||
......Y.
|
||||
........
|
||||
== len4-and-len5 ==
|
||||
OGOGOGOG
|
||||
GRRRRRGO
|
||||
OGOGOGOB
|
||||
GOGOGOGB
|
||||
OGOGOGOB
|
||||
GOGOGOGB
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
--
|
||||
........
|
||||
.RRRRR..
|
||||
.......B
|
||||
.......B
|
||||
.......B
|
||||
.......B
|
||||
........
|
||||
........
|
||||
== L-and-T ==
|
||||
OGOGOGOG
|
||||
GRRRGOGO
|
||||
OROGOGOG
|
||||
GRGOGOGO
|
||||
OGOGOGOG
|
||||
GOGYYYGO
|
||||
OGOGYGOG
|
||||
GOGOYOGO
|
||||
--
|
||||
........
|
||||
.RRR....
|
||||
.R......
|
||||
.R......
|
||||
........
|
||||
...YYY..
|
||||
....Y...
|
||||
....Y...
|
||||
== no-matches ==
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
--
|
||||
........
|
||||
........
|
||||
........
|
||||
........
|
||||
........
|
||||
........
|
||||
........
|
||||
........
|
||||
ok: match detection over hand-crafted boards
|
||||
121
tests/match_detect.sx
Normal file
121
tests/match_detect.sx
Normal file
@@ -0,0 +1,121 @@
|
||||
// Match-detection golden: run `find_matches` over several HAND-CRAFTED boards
|
||||
// and snapshot the matched-cell set for each. Every board is built explicitly
|
||||
// (no seeded init) on a checkerboard O/G background — which is itself run-free,
|
||||
// since adjacent cells always differ — with the runs under test painted in the
|
||||
// other gem colours. For each scene the board and its matched-cell dump are
|
||||
// printed, and the matched-cell count is asserted independently of the dump.
|
||||
#import "modules/std.sx";
|
||||
#import "board.sx";
|
||||
t :: #import "test.sx";
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Build a scene: load an 8x8 board from `rows` (top row first, each exactly
|
||||
// BOARD_COLS gem characters), detect matches, print board + matched dump, and
|
||||
// assert the matched-cell count.
|
||||
scene :: (name: string, rows: []string, want_count: s64) {
|
||||
b : Board = ---;
|
||||
for 0..BOARD_ROWS: (row) {
|
||||
line := rows[row];
|
||||
for 0..BOARD_COLS: (col) {
|
||||
b.set(col, row, char_to_gem(line[col]));
|
||||
}
|
||||
}
|
||||
|
||||
m := find_matches(@b);
|
||||
print("== {} ==\n", name);
|
||||
out(board_dump(@b));
|
||||
out("--\n");
|
||||
out(dump_matches(@b, @m));
|
||||
|
||||
t.expect(m.count() == want_count, name);
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
// Single horizontal 3-run (row 3, cols 2-4).
|
||||
scene("horizontal-3", .[
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GORRROGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
], 3);
|
||||
|
||||
// Single vertical 3-run (col 5, rows 2-4).
|
||||
scene("vertical-3", .[
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOBOG",
|
||||
"GOGOGBGO",
|
||||
"OGOGOBOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
], 3);
|
||||
|
||||
// Multiple disjoint runs: horizontal R (row 1, cols 0-2), horizontal P
|
||||
// (row 5, cols 2-4), vertical Y (col 6, rows 4-6).
|
||||
scene("disjoint-runs", .[
|
||||
"OGOGOGOG",
|
||||
"RRROGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGYG",
|
||||
"GOPPPOYO",
|
||||
"OGOGOGYG",
|
||||
"GOGOGOGO",
|
||||
], 9);
|
||||
|
||||
// A length-5 horizontal run (row 1, cols 1-5) and a length-4 vertical run
|
||||
// (col 7, rows 2-5).
|
||||
scene("len4-and-len5", .[
|
||||
"OGOGOGOG",
|
||||
"GRRRRRGO",
|
||||
"OGOGOGOB",
|
||||
"GOGOGOGB",
|
||||
"OGOGOGOB",
|
||||
"GOGOGOGB",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
], 9);
|
||||
|
||||
// Intersecting shapes, shared cell counted once: an L (R horizontal row 1
|
||||
// cols 1-3 meeting R vertical col 1 rows 1-3 at the corner (1,1)) and a T
|
||||
// (Y horizontal row 5 cols 3-5 meeting Y vertical col 4 rows 5-7 at the
|
||||
// mid/top cell (4,5)).
|
||||
scene("L-and-T", .[
|
||||
"OGOGOGOG",
|
||||
"GRRRGOGO",
|
||||
"OROGOGOG",
|
||||
"GRGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGYYYGO",
|
||||
"OGOGYGOG",
|
||||
"GOGOYOGO",
|
||||
], 10);
|
||||
|
||||
// No matches: the bare checkerboard, every adjacent pair differs.
|
||||
scene("no-matches", .[
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
], 0);
|
||||
|
||||
print("ok: match detection over hand-crafted boards\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user