P2.1: clear matched cells (pure sx)
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}.
This commit is contained in:
46
board.sx
46
board.sx
@@ -8,8 +8,10 @@
|
|||||||
#import "modules/std.sx";
|
#import "modules/std.sx";
|
||||||
|
|
||||||
// ── Gem ──────────────────────────────────────────────────────────────────
|
// ── Gem ──────────────────────────────────────────────────────────────────
|
||||||
// Six distinct gem types. The enum's ordinal (0..5) IS the gem index, so it
|
// Six distinct gem types plus an `empty` hole sentinel. The ordinal of a real
|
||||||
// casts cleanly to/from the integers the RNG and the textual dump work in.
|
// gem (0..5) IS its gem index, so it casts cleanly to/from the integers the RNG
|
||||||
|
// and the textual dump work in; `empty` (ordinal 6) sits outside that range and
|
||||||
|
// is never drawn by the RNG.
|
||||||
GEM_COUNT :: 6;
|
GEM_COUNT :: 6;
|
||||||
|
|
||||||
Gem :: enum {
|
Gem :: enum {
|
||||||
@@ -19,13 +21,24 @@ Gem :: enum {
|
|||||||
green;
|
green;
|
||||||
blue;
|
blue;
|
||||||
purple;
|
purple;
|
||||||
|
// A hole: a cell with no gem, left behind when a match is cleared (P2.1).
|
||||||
|
// Distinct from all six gem types and never produced by init/pick_gem
|
||||||
|
// (which only draw ordinals 0..GEM_COUNT), so gravity/refill (P2.2/P2.3) can
|
||||||
|
// test a cell for `== .empty` to find holes. Outside GEM_CHARS, so it dumps
|
||||||
|
// via the dedicated EMPTY_CHAR rather than the gem alphabet.
|
||||||
|
empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
// One stable character per gem type, indexed by ordinal — the alphabet the
|
// One stable character per gem type, indexed by ordinal — the alphabet the
|
||||||
// board dump (and its golden) is written in.
|
// board dump (and its golden) is written in.
|
||||||
GEM_CHARS :: "ROYGBP";
|
GEM_CHARS :: "ROYGBP";
|
||||||
|
|
||||||
|
// Hole glyph for the board dump: an empty cell renders as this instead of a gem
|
||||||
|
// character. Distinct from every gem in GEM_CHARS.
|
||||||
|
EMPTY_CHAR :: 46; // '.'
|
||||||
|
|
||||||
gem_char :: (g: Gem) -> u8 {
|
gem_char :: (g: Gem) -> u8 {
|
||||||
|
if g == .empty { return EMPTY_CHAR; }
|
||||||
GEM_CHARS[cast(s64) g]
|
GEM_CHARS[cast(s64) g]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,3 +345,32 @@ dump_swaps :: (swaps: *List(Swap)) -> string {
|
|||||||
}
|
}
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Clear (P2.1) ─────────────────────────────────────────────────────────────
|
||||||
|
// First step of the resolution pipeline: turn matched cells into holes. No
|
||||||
|
// gravity or refill here (P2.2 / P2.3) — clearing only writes `.empty` into the
|
||||||
|
// matched cells and leaves every other cell exactly as it was.
|
||||||
|
|
||||||
|
// Set every cell flagged in `mask` to a hole, leaving all unflagged cells
|
||||||
|
// unchanged. Returns the number of cells cleared. `mask` is the matched-cell SET
|
||||||
|
// from find_matches, so overlapping L/T shapes (already unioned into a single
|
||||||
|
// `true` per shared cell) clear as one set with no double-counting.
|
||||||
|
clear_cells :: (board: *Board, mask: *MatchMask) -> s64 {
|
||||||
|
cleared : s64 = 0;
|
||||||
|
for 0..BOARD_CELLS: (i) {
|
||||||
|
if mask.cells[i] {
|
||||||
|
board.cells[i] = .empty;
|
||||||
|
cleared += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cleared
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect matches on `board` and clear them in one step. Returns the number of
|
||||||
|
// cells cleared — 0 when there are no matches, in which case the board is left
|
||||||
|
// unchanged. The count drives later cascade/scoring (P2.2+): a non-zero result
|
||||||
|
// means the board changed and the resolution loop should continue.
|
||||||
|
clear_matches :: (board: *Board) -> s64 {
|
||||||
|
m := find_matches(board);
|
||||||
|
clear_cells(board, @m)
|
||||||
|
}
|
||||||
|
|||||||
151
tests/clear.sx
Normal file
151
tests/clear.sx
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
// Clear golden: run detect→clear over several HAND-CRAFTED boards and snapshot
|
||||||
|
// the post-clear board. Each board sits on the run-free O/G checkerboard from
|
||||||
|
// match_detect (adjacent cells always differ, so it has zero pre-existing
|
||||||
|
// matches) with only the runs under test painted in — so any hole in the result
|
||||||
|
// is purely the cleared match's doing. For each scene the before/after boards
|
||||||
|
// are printed, and three facts are asserted independently of the dump: matched
|
||||||
|
// cells became holes, non-matched cells are byte-identical, and the cleared
|
||||||
|
// count is exact. The boards (and their match counts) mirror match_detect.sx.
|
||||||
|
#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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load an 8x8 board from `rows` (top row first, each exactly BOARD_COLS gem
|
||||||
|
// characters).
|
||||||
|
load_board :: (rows: []string) -> Board {
|
||||||
|
b : Board = ---;
|
||||||
|
for 0..BOARD_ROWS: (row) {
|
||||||
|
line := rows[row];
|
||||||
|
for 0..BOARD_COLS: (col) {
|
||||||
|
b.set(col, row, char_to_gem(line[col]));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect→clear one scene, snapshot before/after, and assert the three clear
|
||||||
|
// invariants against the matched-cell set: every flagged cell is now a hole,
|
||||||
|
// every unflagged cell is unchanged, and the returned count is exact.
|
||||||
|
scene :: (name: string, rows: []string, want_cleared: s64) {
|
||||||
|
b := load_board(rows);
|
||||||
|
orig := load_board(rows); // pristine copy for the unchanged check
|
||||||
|
|
||||||
|
m := find_matches(@b);
|
||||||
|
cleared := clear_cells(@b, @m);
|
||||||
|
|
||||||
|
print("== {} ==\n", name);
|
||||||
|
out("before:\n");
|
||||||
|
out(board_dump(@orig));
|
||||||
|
out("after:\n");
|
||||||
|
out(board_dump(@b));
|
||||||
|
|
||||||
|
cleared_holes := true; // every matched cell is now a hole
|
||||||
|
others_intact := true; // every other cell is byte-identical
|
||||||
|
for 0..BOARD_CELLS: (i) {
|
||||||
|
if m.cells[i] {
|
||||||
|
if !(b.cells[i] == .empty) { cleared_holes = false; }
|
||||||
|
} else {
|
||||||
|
if !(b.cells[i] == orig.cells[i]) { others_intact = false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.expect(cleared_holes, concat(name, ": cleared cells are holes"));
|
||||||
|
t.expect(others_intact, concat(name, ": non-matched cells unchanged"));
|
||||||
|
t.expect(cleared == want_cleared, concat(name, ": cleared count exact"));
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
print("== clear (detect -> clear) ==\n");
|
||||||
|
|
||||||
|
// Single horizontal 3-run (row 3, cols 2-4) → three holes there only.
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Disjoint runs: horizontal R (row 1), horizontal P (row 5), vertical Y
|
||||||
|
// (col 6) — three separate hole clusters, 9 cells total.
|
||||||
|
scene("disjoint-runs", .[
|
||||||
|
"OGOGOGOG",
|
||||||
|
"RRROGOGO",
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GOGOGOGO",
|
||||||
|
"OGOGOGYG",
|
||||||
|
"GOPPPOYO",
|
||||||
|
"OGOGOGYG",
|
||||||
|
"GOGOGOGO",
|
||||||
|
], 9);
|
||||||
|
|
||||||
|
// Overlapping L and T: a horizontal run and a vertical run share a cell (the
|
||||||
|
// L's corner (1,1), the T's stem-top (4,5)). The mask already unions the
|
||||||
|
// shared cell, so clear removes the whole union as one set — 10 holes, not
|
||||||
|
// 11 — exercising the overlapping-clear acceptance case.
|
||||||
|
scene("L-and-T", .[
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GRRRGOGO",
|
||||||
|
"OROGOGOG",
|
||||||
|
"GRGOGOGO",
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GOGYYYGO",
|
||||||
|
"OGOGYGOG",
|
||||||
|
"GOGOYOGO",
|
||||||
|
], 10);
|
||||||
|
|
||||||
|
// No matches: the bare checkerboard is left completely unchanged (0 holes),
|
||||||
|
// so its before/after dumps are identical.
|
||||||
|
scene("no-matches", .[
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GOGOGOGO",
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GOGOGOGO",
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GOGOGOGO",
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GOGOGOGO",
|
||||||
|
], 0);
|
||||||
|
|
||||||
|
// clear_matches: the one-call detect+clear returns the same cleared count
|
||||||
|
// and punches the holes itself.
|
||||||
|
cm := load_board(.[
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GOGOGOGO",
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GORRROGO",
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GOGOGOGO",
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GOGOGOGO",
|
||||||
|
]);
|
||||||
|
t.expect(clear_matches(@cm) == 3, "clear_matches: detect+clear returns count");
|
||||||
|
t.expect(cm.at(2, 3) == .empty and cm.at(3, 3) == .empty and cm.at(4, 3) == .empty,
|
||||||
|
"clear_matches: matched run is now holes");
|
||||||
|
|
||||||
|
print("ok: clear over hand-crafted boards\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
1
tests/expected/clear.exit
Normal file
1
tests/expected/clear.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
97
tests/expected/clear.stdout
Normal file
97
tests/expected/clear.stdout
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
== clear (detect -> clear) ==
|
||||||
|
== horizontal-3 ==
|
||||||
|
before:
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GORRROGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
after:
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GO...OGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
== vertical-3 ==
|
||||||
|
before:
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOBOG
|
||||||
|
GOGOGBGO
|
||||||
|
OGOGOBOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
after:
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGO.OG
|
||||||
|
GOGOG.GO
|
||||||
|
OGOGO.OG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
== disjoint-runs ==
|
||||||
|
before:
|
||||||
|
OGOGOGOG
|
||||||
|
RRROGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOGYG
|
||||||
|
GOPPPOYO
|
||||||
|
OGOGOGYG
|
||||||
|
GOGOGOGO
|
||||||
|
after:
|
||||||
|
OGOGOGOG
|
||||||
|
...OGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOG.G
|
||||||
|
GO...O.O
|
||||||
|
OGOGOG.G
|
||||||
|
GOGOGOGO
|
||||||
|
== L-and-T ==
|
||||||
|
before:
|
||||||
|
OGOGOGOG
|
||||||
|
GRRRGOGO
|
||||||
|
OROGOGOG
|
||||||
|
GRGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGYYYGO
|
||||||
|
OGOGYGOG
|
||||||
|
GOGOYOGO
|
||||||
|
after:
|
||||||
|
OGOGOGOG
|
||||||
|
G...GOGO
|
||||||
|
O.OGOGOG
|
||||||
|
G.GOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOG...GO
|
||||||
|
OGOG.GOG
|
||||||
|
GOGO.OGO
|
||||||
|
== no-matches ==
|
||||||
|
before:
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
after:
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
ok: clear over hand-crafted boards
|
||||||
Reference in New Issue
Block a user