Merge branch 'flow/m3te/P3.2' into m3te-plan
This commit is contained in:
57
board.sx
57
board.sx
@@ -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
|
||||
|
||||
140
tests/combo.sx
Normal file
140
tests/combo.sx
Normal file
@@ -0,0 +1,140 @@
|
||||
// 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;
|
||||
}
|
||||
1
tests/expected/combo.exit
Normal file
1
tests/expected/combo.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
41
tests/expected/combo.stdout
Normal file
41
tests/expected/combo.stdout
Normal file
@@ -0,0 +1,41 @@
|
||||
== combo (cascade multiplier) ==
|
||||
== single-depth1 ==
|
||||
RRRGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
round 1: cleared 3 base 30 x1 = 30
|
||||
flat sum 30
|
||||
multiplied total 30
|
||||
== cascade-depth2 ==
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
RGOGOGOG
|
||||
BBBOGOGO
|
||||
RGOGOGOG
|
||||
ROGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
round 1: cleared 3 base 30 x1 = 30
|
||||
round 2: cleared 3 base 30 x2 = 60
|
||||
flat sum 60
|
||||
multiplied total 90
|
||||
== chain-depth3 ==
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
RGOGOGOG
|
||||
BBBOGOGO
|
||||
RGOGOGOG
|
||||
ROGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
round 1: cleared 3 base 30 x1 = 30
|
||||
round 2: cleared 3 base 30 x2 = 60
|
||||
round 3: cleared 3 base 30 x3 = 90
|
||||
flat sum 90
|
||||
multiplied total 180
|
||||
ok: combo multiplier scales cascade rounds
|
||||
Reference in New Issue
Block a user