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:
swipelab
2026-06-04 20:48:20 +03:00
parent 3903add44b
commit 3e180a121a
4 changed files with 217 additions and 0 deletions

View File

@@ -455,3 +455,48 @@ refill :: (board: *Board) -> s64 {
}
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
}