P3.2: cascade combo multiplier (pure sx)

Scale each cascade round's base points by combo_multiplier(round) = the
1-based round index (round 1 x1, round 2 x2, ...), so deeper chains pay
out more. resolve now reads score_round before each clear, accumulates
score_round * combo_multiplier(round) into Board.score, and reports the
settle's payout as the new Cascade.awarded field. A depth-1 settle scores
exactly its base (x1, no bonus); any multi-round chain strictly exceeds
the same clears scored flat.

resolve_step keeps its signature (no scoring), so cascade.sx and its
golden are unchanged; score_round/add_round_score are untouched, so
score.sx is unchanged. New tests/combo.sx golden locks exact cumulative
scores for a single-round clear (30), the P2.4 cascade board (flat 60 ->
mult 90), and a controlled 3-round chain (flat 90 -> mult 180), printing
per-round base/multiplier/points so the golden self-explains.
This commit is contained in:
swipelab
2026-06-04 21:17:08 +03:00
parent 359c6a70bd
commit c7368a82f1
4 changed files with 223 additions and 16 deletions

View File

@@ -88,10 +88,10 @@ Board :: struct {
// must seed this before any draw.
rng: Rng;
// Running score total (P3.1). `init` zeroes it; `add_round_score` accumulates
// a round's base points (see `score_round`). The cascade-wide combo MULTIPLIER
// off `Cascade.depth` lands in P3.2, and the HUD (P4.4) reads this field. A
// hand-built board must zero this before accumulating.
// Running score total. `init` zeroes it; `add_round_score` adds a single
// round's base points (see `score_round`), and `resolve` adds each cascade
// round's base scaled by `combo_multiplier` (P3.2). The HUD (P4.4) reads this
// field. A hand-built board must zero this before accumulating.
score: s64;
idx :: (col: s64, row: s64) -> s64 {
@@ -474,11 +474,14 @@ refill :: (board: *Board) -> s64 {
// 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.
// `cleared.len == depth`. `awarded` is the total points this settle added to
// `Board.score`: the sum over rounds of `score_round * combo_multiplier(round)`
// (P3.2), so the HUD (P4.4) and turn accounting (P3.3) can read a swap's payout
// without re-deriving it. A depth-0 (already-stable) board awards 0.
Cascade :: struct {
depth: s64;
cleared: List(s64);
awarded: s64;
}
// One resolution round: detect matches and, if any, clear them, collapse under
@@ -494,16 +497,24 @@ resolve_step :: (board: *Board) -> s64 {
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 the board to a stable state, running rounds until one finds no match,
// scoring each round with the cascade combo multiplier (P3.2). Returns the
// cascade: its depth, per-round cleared-cell counts, and total `awarded` points.
// Each round adds `score_round * combo_multiplier(round)` (round 1-based) to
// `Board.score`; an already-stable board returns depth 0, awards 0, untouched.
resolve :: (board: *Board) -> Cascade {
result := Cascade.{ depth = 0, cleared = List(s64).{} };
result := Cascade.{ depth = 0, cleared = List(s64).{}, awarded = 0 };
while true {
// Read the round's base points while its runs are still on the board:
// `resolve_step` clears them, so the score has to be taken first.
base := score_round(board);
n := resolve_step(board);
if n == 0 { break; }
result.cleared.append(n);
result.depth += 1;
points := base * combo_multiplier(result.depth);
board.score += points;
result.awarded += points;
result.cleared.append(n);
}
result
}
@@ -521,7 +532,8 @@ resolve :: (board: *Board) -> Cascade {
// corner) scores horizontal + vertical — the corner counts toward both runs'
// lengths, unlike the cleared-cell set which unions it once.
//
// One round only: the cross-round combo MULTIPLIER (off `Cascade.depth`) is P3.2.
// One round only: the cross-round combo MULTIPLIER is `combo_multiplier` (P3.2),
// applied by `resolve`; this base scheme is unscaled.
SCORE_RUN_3 :: 30;
SCORE_RUN_4 :: 60;
SCORE_RUN_5_PLUS :: 100;
@@ -602,16 +614,29 @@ score_round :: (board: *Board) -> s64 {
total
}
// Add this round's base points to the board's running `score` total and return
// them. The accumulation primitive the HUD (P4.4) reads and that P3.2 wraps with
// the cascade combo multiplier — P3.2 layers the per-round multiplier here / in
// the resolve loop without changing `score_round`.
// Add this round's base points (×1, no combo multiplier) to the board's running
// `score` total and return them. The single-round accumulation primitive; the
// cascade loop (`resolve`) instead scales each round by `combo_multiplier`
// (P3.2). Neither path changes `score_round`.
add_round_score :: (board: *Board) -> s64 {
points := score_round(board);
board.score += points;
points
}
// ── Combo multiplier (P3.2) ────────────────────────────────────────────────
// Across one swap's cascade, each resolution round's base points (`score_round`)
// are scaled by a multiplier that grows with chain depth, so deeper chains pay
// out more. The scheme: the 1-based round index IS the multiplier — round 1 ×1,
// round 2 ×2, round 3 ×3, … A single-round settle (depth 1) therefore scores
// exactly its base (×1, no bonus); every round past the first is amplified, so a
// multi-round chain strictly beats the same clears scored flat. `resolve`
// accumulates `score_round * combo_multiplier(round)` per round into `Board.score`
// and reports the sum as `Cascade.awarded`.
combo_multiplier :: (round: s64) -> s64 {
round
}
// Deterministic textual dump of an enumerated run list, in `find_runs` order: a
// count header, then one run per line as `<axis> len <n> at fixed <f> start <s>`
// where axis is H (horizontal) or V (vertical). An empty list dumps as just