Files
m3te/tests/combo.sx
swipelab 6f7d2f4db2 lang migration: rename signed integer types sN -> iN
Mechanical sweep of all .sx sources, plan docs, and tests/expected
snapshots for the sx language rename (s8/s16/s32/s64 -> i8/i16/i32/i64).
Verified: tools/run_tests.sh 23/23.

Note: the ios-sim build has 2 pre-existing 'restart' dot-call errors
from the sx opt-in UFCS change (sx a47ea14) — independent of this
rename (present pre-sweep); migrated in the follow-up commit.
2026-06-12 09:36:51 +03:00

141 lines
5.3 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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: i64) -> 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: i64, want_flat: i64, want_mult: i64) {
print("== {} ==\n", name);
b := load_board(rows, seed);
out(board_dump(@b));
flat : i64 = 0;
mult : i64 = 0;
depth : i64 = 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 :: () -> i32 {
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;
}