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.
141 lines
5.3 KiB
Plaintext
141 lines
5.3 KiB
Plaintext
// 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;
|
||
}
|