P2.4: cascade resolution loop (pure sx)
Add the settle loop a swap triggers: resolve(board) runs rounds of
detect -> clear -> collapse -> refill until a round finds no match,
returning a Cascade { depth, cleared } so P3 can read per-round
cleared-cell counts and the combo-driving depth. resolve_step exposes
one round. Termination follows from eventually reaching no-match; no
artificial round cap.
tests/cascade.sx: a fixed-seed hand-crafted board where clearing the
initial BBB lets gravity pack col 0 into a fresh vertical RRR, so the
loop chains two rounds; snapshots each per-round board and the final
depth, asserts the final board is stable and that public resolve
reproduces the manual loop, plus a depth-0 control on an unchanged
checkerboard. Locked in tests/expected/cascade.{stdout,exit}.
This commit is contained in:
45
board.sx
45
board.sx
@@ -455,3 +455,48 @@ refill :: (board: *Board) -> s64 {
|
|||||||
}
|
}
|
||||||
filled
|
filled
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Cascade resolution (P2.4) ──────────────────────────────────────────────
|
||||||
|
// The settle loop a swap triggers: keep resolving matches until the board is
|
||||||
|
// stable. One round is detect → clear → collapse → refill; the loop repeats
|
||||||
|
// while a round still finds a match. Gravity can align falling survivors into a
|
||||||
|
// fresh run and a seeded refill can complete one, so a single clear chains into
|
||||||
|
// more — the cascade. Termination is reached the first round that detects no
|
||||||
|
// match; for a fixed seed the whole sequence is deterministic.
|
||||||
|
|
||||||
|
// Outcome of resolving a board to a stable state. `depth` is the number of
|
||||||
|
// rounds that found and cleared at least one match (0 for an already-stable
|
||||||
|
// board). `cleared` holds those rounds' cleared-cell counts in round order, so
|
||||||
|
// `cleared.len == depth`; P3 scores each round off this list and reads the
|
||||||
|
// combo multiplier from the depth.
|
||||||
|
Cascade :: struct {
|
||||||
|
depth: s64;
|
||||||
|
cleared: List(s64);
|
||||||
|
}
|
||||||
|
|
||||||
|
// One resolution round: detect matches and, if any, clear them, collapse under
|
||||||
|
// gravity, then refill the holes from the board's seeded RNG. Returns the
|
||||||
|
// number of cells cleared this round — 0 iff the board was already stable, in
|
||||||
|
// which case nothing moves and no gem is drawn. `resolve` repeats this until it
|
||||||
|
// returns 0.
|
||||||
|
resolve_step :: (board: *Board) -> s64 {
|
||||||
|
cleared := clear_matches(board);
|
||||||
|
if cleared == 0 { return 0; }
|
||||||
|
collapse(board);
|
||||||
|
refill(board);
|
||||||
|
cleared
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the board to a stable state, running rounds until one finds no match.
|
||||||
|
// Returns the cascade: its depth and per-round cleared-cell counts. An
|
||||||
|
// already-stable board returns depth 0 with an empty `cleared` list, untouched.
|
||||||
|
resolve :: (board: *Board) -> Cascade {
|
||||||
|
result := Cascade.{ depth = 0, cleared = List(s64).{} };
|
||||||
|
while true {
|
||||||
|
n := resolve_step(board);
|
||||||
|
if n == 0 { break; }
|
||||||
|
result.cleared.append(n);
|
||||||
|
result.depth += 1;
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|||||||
141
tests/cascade.sx
Normal file
141
tests/cascade.sx
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
// Cascade golden: resolve a HAND-CRAFTED seeded board to a stable state and
|
||||||
|
// snapshot the whole settle loop. The board carries a single horizontal match
|
||||||
|
// (BBB at row 3, cols 0-2) sitting on the run-free O/G checkerboard, painted so
|
||||||
|
// that clearing it is NOT the end: col 0 holds R . R R straddling that match's
|
||||||
|
// cell, so once the B is cleared and gravity packs the column, the three reds
|
||||||
|
// fall adjacent into a fresh vertical RRR — a SECOND match the first clear set
|
||||||
|
// up. So the loop runs at least two rounds before it stabilises. The exact
|
||||||
|
// sequence (per-round boards + final depth) is locked by the snapshot.
|
||||||
|
//
|
||||||
|
// Two things are asserted independently of the dump: the final board is stable
|
||||||
|
// (find_matches empty), and the public `resolve` reproduces the manual loop's
|
||||||
|
// depth, per-round cleared counts, and final board byte-for-byte. A control
|
||||||
|
// checkerboard with no initial match resolves at depth 0, untouched.
|
||||||
|
#import "modules/std.sx";
|
||||||
|
#import "board.sx";
|
||||||
|
t :: #import "test.sx";
|
||||||
|
|
||||||
|
SEED :: 7;
|
||||||
|
|
||||||
|
// Number of rounds the crafted cascade runs. Locked alongside the golden.
|
||||||
|
EXPECTED_DEPTH :: 2;
|
||||||
|
|
||||||
|
// Inverse of `gem_char`: map a board character back to its Gem so the starting
|
||||||
|
// board can be written as a human-readable grid. The hole glyph maps to `.empty`.
|
||||||
|
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; }
|
||||||
|
}
|
||||||
|
.red
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load an 8x8 board from `rows` (top row first, each exactly BOARD_COLS chars).
|
||||||
|
// The RNG is left unseeded — callers seed it before resolving.
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
boards_equal :: (a: *Board, b: *Board) -> bool {
|
||||||
|
for 0..BOARD_CELLS: (i) { if a.cells[i] != b.cells[i] { return false; } }
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
// The crafted cascade board: checkerboard everywhere except a horizontal BBB at
|
||||||
|
// row 3 (cols 0-2) and reds salted down col 0 (rows 2,4,5) around that match's
|
||||||
|
// cell. Clearing BBB punches the col-0 hole between the reds; gravity then packs
|
||||||
|
// R,R,R adjacent → a vertical match for round 2. Its RNG is seeded from SEED so
|
||||||
|
// the refill that follows each clear is reproducible.
|
||||||
|
cascade_board :: () -> Board {
|
||||||
|
b := load_board(.[
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GOGOGOGO",
|
||||||
|
"RGOGOGOG",
|
||||||
|
"BBBOGOGO",
|
||||||
|
"RGOGOGOG",
|
||||||
|
"ROGOGOGO",
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GOGOGOGO",
|
||||||
|
]);
|
||||||
|
b.rng = rng_seeded(SEED);
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
// A run-free checkerboard with no initial match — the depth-0 control.
|
||||||
|
checker_board :: () -> Board {
|
||||||
|
b := load_board(.[
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GOGOGOGO",
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GOGOGOGO",
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GOGOGOGO",
|
||||||
|
"OGOGOGOG",
|
||||||
|
"GOGOGOGO",
|
||||||
|
]);
|
||||||
|
b.rng = rng_seeded(SEED);
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
main :: () -> s32 {
|
||||||
|
print("== cascade (resolution loop) ==\n");
|
||||||
|
|
||||||
|
// Drive the loop one round at a time so each post-round board is visible in
|
||||||
|
// the snapshot, recording the per-round cleared counts and the depth.
|
||||||
|
b := cascade_board();
|
||||||
|
out("start:\n");
|
||||||
|
out(board_dump(@b));
|
||||||
|
|
||||||
|
depth := 0;
|
||||||
|
counts := List(s64).{};
|
||||||
|
while true {
|
||||||
|
n := resolve_step(@b);
|
||||||
|
if n == 0 { break; }
|
||||||
|
depth += 1;
|
||||||
|
counts.append(n);
|
||||||
|
print("round {}: cleared {} cells\n", depth, n);
|
||||||
|
out(board_dump(@b));
|
||||||
|
}
|
||||||
|
print("cascade depth {}\n", depth);
|
||||||
|
|
||||||
|
// The loop reached a stable board.
|
||||||
|
fm := find_matches(@b);
|
||||||
|
t.expect(fm.count() == 0, "cascade: final board has no matches");
|
||||||
|
|
||||||
|
// A genuine multi-round chain at the expected depth.
|
||||||
|
t.expect(depth == EXPECTED_DEPTH, "cascade: depth equals expected");
|
||||||
|
t.expect(depth >= 2, "cascade: chained at least two rounds");
|
||||||
|
|
||||||
|
// The public `resolve` on a fresh identical board reproduces the manual
|
||||||
|
// loop exactly: same depth, same per-round cleared counts, same final board.
|
||||||
|
b2 := cascade_board();
|
||||||
|
c := resolve(@b2);
|
||||||
|
t.expect(c.depth == depth, "cascade: resolve depth matches manual loop");
|
||||||
|
same_counts := c.cleared.len == counts.len;
|
||||||
|
if same_counts {
|
||||||
|
for 0..counts.len: (i) {
|
||||||
|
if c.cleared.items[i] != counts.items[i] { same_counts = false; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
t.expect(same_counts, "cascade: resolve per-round counts match manual loop");
|
||||||
|
t.expect(boards_equal(@b, @b2), "cascade: resolve final board matches manual loop");
|
||||||
|
|
||||||
|
// Control: a checkerboard with no initial match resolves at depth 0 and is
|
||||||
|
// left untouched (no clear, no collapse, no refill draw).
|
||||||
|
ctrl := checker_board();
|
||||||
|
before := ctrl;
|
||||||
|
cc := resolve(@ctrl);
|
||||||
|
t.expect(cc.depth == 0, "control: stable board resolves at depth 0");
|
||||||
|
t.expect(cc.cleared.len == 0, "control: depth 0 yields an empty per-round list");
|
||||||
|
t.expect(boards_equal(@before, @ctrl), "control: stable board left unchanged");
|
||||||
|
|
||||||
|
print("ok: cascade resolves to a stable board\n");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
1
tests/expected/cascade.exit
Normal file
1
tests/expected/cascade.exit
Normal file
@@ -0,0 +1 @@
|
|||||||
|
0
|
||||||
30
tests/expected/cascade.stdout
Normal file
30
tests/expected/cascade.stdout
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
== cascade (resolution loop) ==
|
||||||
|
start:
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
RGOGOGOG
|
||||||
|
BBBOGOGO
|
||||||
|
RGOGOGOG
|
||||||
|
ROGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
round 1: cleared 3 cells
|
||||||
|
RBRGOGOG
|
||||||
|
OGOOGOGO
|
||||||
|
GOGGOGOG
|
||||||
|
RGOOGOGO
|
||||||
|
RGOGOGOG
|
||||||
|
ROGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
round 2: cleared 3 cells
|
||||||
|
RBRGOGOG
|
||||||
|
PGOOGOGO
|
||||||
|
YOGGOGOG
|
||||||
|
RGOOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
OGOGOGOG
|
||||||
|
GOGOGOGO
|
||||||
|
cascade depth 2
|
||||||
|
ok: cascade resolves to a stable board
|
||||||
Reference in New Issue
Block a user