P2.1: holes never match in find_matches

find_matches walked maximal same-type spans without excluding `.empty`, so a
line of 3+ holes (left by a prior clear) was reported as a match. After any
vertical 3-clear or L/T clear the board carries such a line, so find_matches /
clear_matches returned non-zero on a board with no real gem match — which would
prevent the P2.4 cascade from ever stabilising.

Fix at the source: a run is only a match if its gem type is not `.empty`. Holes
already break runs of real gems (a hole differs from every gem), so this is the
only change needed and every caller (P1.3 legality, P2.4 cascade) is now correct.

Regression in tests/clear.sx: a holes-only board yields zero matches and
clear_matches 0, and re-clearing a holed board returns 0. Other goldens are
unchanged (no board without holes is affected).
This commit is contained in:
swipelab
2026-06-04 20:08:26 +03:00
parent 2713a67b2b
commit 6e2a57f3a2
3 changed files with 78 additions and 3 deletions

View File

@@ -11,8 +11,11 @@
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.
// be written as a human-readable grid. The hole glyph maps to `.empty`, so a
// board can be hand-written with pre-existing holes (cells left by a prior
// clear) for the holes-never-match regression.
char_to_gem :: (c: u8) -> Gem {
if c == EMPTY_CHAR { return .empty; }
for 0..GEM_COUNT: (i) {
if GEM_CHARS[i] == c { return cast(Gem) i; }
}
@@ -130,6 +133,23 @@ main :: () -> s32 {
"GOGOGOGO",
], 0);
// Holes never match: a checkerboard carrying a horizontal 3-run of holes
// (row 3, cols 2-4) and a vertical 3-run of holes (col 1, rows 5-7), left by
// earlier clears. A line of 3+ holes is NOT a match, so detect finds nothing,
// clear removes nothing, and before/after are identical. Without this, a
// post-clear board would keep re-"matching" its own holes and the P2.4
// cascade would never stabilise.
scene("holes-no-match", .[
"OGOGOGOG",
"GOGOGOGO",
"OGOGOGOG",
"GO...OGO",
"OGOGOGOG",
"G.GOGOGO",
"O.OGOGOG",
"G.GOGOGO",
], 0);
// clear_matches: the one-call detect+clear returns the same cleared count
// and punches the holes itself.
cm := load_board(.[
@@ -146,6 +166,38 @@ main :: () -> s32 {
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");
// Holes are never matchable: a board whose only equal-adjacent runs are
// holes yields an empty match set, and clear_matches reports 0 (no change).
holes := load_board(.[
"OGOGOGOG",
"GOGOGOGO",
"OGOGOGOG",
"GO...OGO",
"OGOGOGOG",
"G.GOGOGO",
"O.OGOGOG",
"G.GOGOGO",
]);
hm := find_matches(@holes);
t.expect(hm.count() == 0, "holes: a line of 3+ holes is not a match");
t.expect(clear_matches(@holes) == 0, "holes: clear_matches returns 0 on a holes-only board");
// Cascade base case: after a real clear punches a 3-in-a-line into holes,
// re-detecting on the cleared board must find nothing — otherwise the P2.4
// cascade loop would re-match its own holes and never terminate.
casc := load_board(.[
"OGOGOGOG",
"GOGOGOGO",
"OGOGOBOG",
"GOGOGBGO",
"OGOGOBOG",
"GOGOGOGO",
"OGOGOGOG",
"GOGOGOGO",
]);
t.expect(clear_matches(@casc) == 3, "cascade: first clear removes the vertical 3-run");
t.expect(clear_matches(@casc) == 0, "cascade: re-clear on the holed board returns 0");
print("ok: clear over hand-crafted boards\n");
return 0;
}

View File

@@ -94,4 +94,23 @@ OGOGOGOG
GOGOGOGO
OGOGOGOG
GOGOGOGO
== holes-no-match ==
before:
OGOGOGOG
GOGOGOGO
OGOGOGOG
GO...OGO
OGOGOGOG
G.GOGOGO
O.OGOGOG
G.GOGOGO
after:
OGOGOGOG
GOGOGOGO
OGOGOGOG
GO...OGO
OGOGOGOG
G.GOGOGO
O.OGOGOG
G.GOGOGO
ok: clear over hand-crafted boards