// Combo-multiplier golden (P3.2): drive seeded boards through the full cascade // settle and snapshot the per-round scoring. Each round's base points // (`score_round`) are scaled by `combo_multiplier(round)` = the 1-based round // index, so round 1 ×1, round 2 ×2, round 3 ×3, …; `resolve` accumulates the // multiplied sum into `Board.score` and reports it as `Cascade.awarded`. // // Three scenes prove the rule end to end: // - single-depth1: one clear, depth 1 → base ×1 = base exactly (NO bonus). // - cascade-depth2: the P2.4 cascade board (seed 7) → a real 2-round chain // whose multiplied total (90) strictly beats the flat sum (60). // - chain-depth3: the same crafted board at seed 10 → a 3-round chain, // 30×1 + 30×2 + 30×3 = 180, well above the flat 90. // // For each scene the starting board and every round's (cleared, base, multiplier, // round points) are printed so the golden is self-explanatory, the flat and // multiplied totals are printed side by side, and `resolve` on a fresh identical // board is asserted to award EXACTLY the multiplied total into `Board.score`. #import "modules/std.sx"; #import "board.sx"; t :: #import "test.sx"; // Inverse of `gem_char`: map a board character back to its Gem so each 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), // seeded RNG, running score zeroed so `board.score` ends equal to the payout. load_board :: (rows: []string, seed: s64) -> 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.rng = rng_seeded(seed); b.score = 0; b } // One scene: drive the settle one round at a time so each round is visible in the // snapshot — score_round BEFORE the clear, multiplier off the 1-based round index, // mirroring `resolve` exactly. Print per-round (cleared, base, multiplier, round // points) and the flat-vs-multiplied totals, then assert `resolve` on a fresh // identical board awards `want_mult` into `Board.score` and reports it as // `Cascade.awarded` at the same depth. A depth-1 settle must equal the flat sum // (no bonus); a deeper chain must strictly exceed it. scene :: (name: string, rows: []string, seed: s64, want_flat: s64, want_mult: s64) { print("== {} ==\n", name); b := load_board(rows, seed); out(board_dump(@b)); flat : s64 = 0; mult : s64 = 0; depth : s64 = 0; while true { base := score_round(@b); n := resolve_step(@b); if n == 0 { break; } depth += 1; m := combo_multiplier(depth); print("round {}: cleared {} base {} x{} = {}\n", depth, n, base, m, base * m); flat += base; mult += base * m; } print("flat sum {}\n", flat); print("multiplied total {}\n", mult); t.expect(flat == want_flat, concat(name, ": flat sum exact")); t.expect(mult == want_mult, concat(name, ": multiplied total exact")); if depth >= 2 { t.expect(mult > flat, concat(name, ": multi-round chain beats flat sum")); } else { t.expect(mult == flat, concat(name, ": single round scores flat (no bonus)")); } // The public `resolve` on a fresh identical board reproduces the payout: // accumulates the multiplied total into `Board.score` and reports it as // `Cascade.awarded`, at the same depth. b2 := load_board(rows, seed); c := resolve(@b2); t.expect(c.depth == depth, concat(name, ": resolve depth matches manual loop")); t.expect(c.awarded == want_mult, concat(name, ": resolve awarded equals multiplied total")); t.expect(b2.score == want_mult, concat(name, ": resolve accumulates into board.score")); } main :: () -> s32 { print("== combo (cascade multiplier) ==\n"); // Single-round clear (seed 0): one RRR clears and the refill makes no new // match, so the settle stops at depth 1 → base 30 ×1 = 30, exactly the flat // value. Proves there is no combo bonus on a single round. scene("single-depth1", .[ "RRRGOGOG", "GOGOGOGO", "OGOGOGOG", "GOGOGOGO", "OGOGOGOG", "GOGOGOGO", "OGOGOGOG", "GOGOGOGO", ], 0, 30, 30); // The P2.4 cascade board (seed 7): round 1 clears the horizontal BBB (base 30 // ×1), round 2 the gravity-formed vertical RRR (base 30 ×2) → 30 + 60 = 90, // strictly above the flat 30 + 30 = 60. scene("cascade-depth2", .[ "OGOGOGOG", "GOGOGOGO", "RGOGOGOG", "BBBOGOGO", "RGOGOGOG", "ROGOGOGO", "OGOGOGOG", "GOGOGOGO", ], 7, 60, 90); // The same crafted board at seed 10: the refill after round 2 sets up a third // len-3 clear, a controlled 3-round chain → 30×1 + 30×2 + 30×3 = 180, well // above the flat 90. scene("chain-depth3", .[ "OGOGOGOG", "GOGOGOGO", "RGOGOGOG", "BBBOGOGO", "RGOGOGOG", "ROGOGOGO", "OGOGOGOG", "GOGOGOGO", ], 10, 90, 180); print("ok: combo multiplier scales cascade rounds\n"); return 0; }