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.
217 lines
8.7 KiB
Plaintext
217 lines
8.7 KiB
Plaintext
// Turn accounting + special-match flagging golden (P3.3).
|
|
//
|
|
// Two things are proven over crafted, deterministic boards:
|
|
//
|
|
// 1. Special-match FLAGGING (pure, no RNG): `count_specials` tallies a single
|
|
// detection round's maximal runs by special length — runs of length exactly
|
|
// 4 (`len4`) and of length 5 or more (`len5_plus`). A round with only
|
|
// length-3 runs tallies neither; the tiers are independent and length-3 runs
|
|
// never count. This is a HOOK for future special gems — detection only.
|
|
//
|
|
// 2. Turn ACCOUNTING via `commit_swap`, the single player-move entry point P5
|
|
// (input) and P7 (turn loop) will call. A legal swap (one that forms a
|
|
// match) is committed — applied, resolved (scoring accrues), and it spends
|
|
// exactly one move; the returned `PlayerMove` reports the settle's payout
|
|
// and special flags. An illegal swap is rejected — board reverted, no move
|
|
// spent, no score change.
|
|
//
|
|
// Every counter/flag is asserted independently of the dump AND printed so the
|
|
// golden is self-explanatory and deterministic.
|
|
#import "modules/std.sx";
|
|
#import "board.sx";
|
|
t :: #import "test.sx";
|
|
|
|
SEED :: 7;
|
|
LIMIT :: 5;
|
|
|
|
// 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, and the turn counters reset to a fresh game
|
|
// (no moves made, the given move budget).
|
|
load_board :: (rows: []string, seed: i64, move_limit: 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.moves_made = 0;
|
|
b.move_limit = move_limit;
|
|
b
|
|
}
|
|
|
|
boards_equal :: (a: *Board, b: *Board) -> bool {
|
|
for 0..BOARD_CELLS (i) { if a.cells[i] != b.cells[i] { return false; } }
|
|
true
|
|
}
|
|
|
|
// One flag scene: snapshot the board, then count its single round's special
|
|
// runs and assert the tallies (and the boolean flags derived from them) are
|
|
// exactly the documented values. No RNG, no clear — pure detection.
|
|
flag_scene :: (name: string, rows: []string, want_len4: i64, want_len5_plus: i64) {
|
|
print("== {} ==\n", name);
|
|
b := load_board(rows, 0, LIMIT);
|
|
out(board_dump(@b));
|
|
sp := count_specials(@b);
|
|
print("len4 {} len5_plus {}\n", sp.len4, sp.len5_plus);
|
|
print("had_len4 {} had_len5_plus {}\n", sp.len4 > 0, sp.len5_plus > 0);
|
|
t.expect(sp.len4 == want_len4, concat(name, ": len4 count exact"));
|
|
t.expect(sp.len5_plus == want_len5_plus, concat(name, ": len5_plus count exact"));
|
|
}
|
|
|
|
main :: () -> i32 {
|
|
print("== turn (accounting + special-match flagging) ==\n");
|
|
|
|
// ── Special-match flagging (single round, no RNG) ──────────────────────
|
|
// Only length-3 runs: a lone horizontal RRR -> neither tier flags.
|
|
flag_scene("flag-len3-none", .[
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
"OGOGOGOG",
|
|
"GORRROGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
], 0, 0);
|
|
|
|
// A length-4 run -> len4 flags, len5_plus does not.
|
|
flag_scene("flag-len4", .[
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
"OGOGOGOG",
|
|
"GORRRRGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
], 1, 0);
|
|
|
|
// A length-5 run -> len5_plus flags, len4 does not.
|
|
flag_scene("flag-len5", .[
|
|
"OGOGOGOG",
|
|
"GRRRRRGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
], 0, 1);
|
|
|
|
// A length-3, a length-4 and a length-5 run together: the length-3 is ignored
|
|
// and the two special tiers each count their own run -> len4 1, len5_plus 1.
|
|
flag_scene("flag-len4-and-len5", .[
|
|
"RRRGOGOG",
|
|
"GOGOGOGO",
|
|
"GOBBBBOG",
|
|
"OGOGOGOG",
|
|
"GPPPPPGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
"OGOGOGOG",
|
|
], 1, 1);
|
|
|
|
// ── Turn accounting via commit_swap ────────────────────────────────────
|
|
// A legal swap that forms a length-3 run. Swapping (2,0)<->(2,1) drops a red
|
|
// into row 0 to complete RRR at cols 0-2 (the donor row 1 is shaped so the
|
|
// displaced gem makes no second run). The swap is committed: applied,
|
|
// resolved (score accrues), and it spends exactly one move.
|
|
print("== commit-legal-len3 ==\n");
|
|
bl := load_board(.[
|
|
"RROGOGOG",
|
|
"GGROGOGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
], SEED, LIMIT);
|
|
out(board_dump(@bl));
|
|
score_before := bl.score;
|
|
made_before := bl.moves_made;
|
|
rem_before := bl.moves_remaining();
|
|
print("before: score {} moves_made {} moves_remaining {}\n", score_before, made_before, rem_before);
|
|
mv := commit_swap(@bl, Cell.{ col = 2, row = 0 }, Cell.{ col = 2, row = 1 });
|
|
print("after: legal {} depth {} awarded {} len4 {} len5_plus {}\n",
|
|
mv.legal, mv.cascade.depth, mv.cascade.awarded, mv.cascade.len4, mv.cascade.len5_plus);
|
|
print("after: score {} moves_made {} moves_remaining {}\n", bl.score, bl.moves_made, mv.moves_remaining);
|
|
out(board_dump(@bl));
|
|
t.expect(mv.legal, "legal-len3: swap committed");
|
|
t.expect(bl.moves_made == made_before + 1, "legal-len3: moves_made +1");
|
|
t.expect(mv.moves_remaining == rem_before - 1, "legal-len3: reported moves_remaining -1");
|
|
t.expect(bl.moves_remaining() == rem_before - 1, "legal-len3: board moves_remaining -1");
|
|
t.expect(bl.score > score_before, "legal-len3: score accrued");
|
|
t.expect(mv.cascade.awarded == bl.score - score_before, "legal-len3: awarded equals score delta");
|
|
t.expect(mv.cascade.depth >= 1, "legal-len3: settled at least one round");
|
|
|
|
// An illegal swap on a run-free checkerboard: adjacent but forms no match.
|
|
// It must be rejected outright — board reverted, no move spent, no score.
|
|
print("== commit-illegal ==\n");
|
|
bi := load_board(.[
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
], SEED, LIMIT);
|
|
before := bi;
|
|
out(board_dump(@bi));
|
|
print("before: score {} moves_made {} moves_remaining {}\n", bi.score, bi.moves_made, bi.moves_remaining());
|
|
miv := commit_swap(@bi, Cell.{ col = 0, row = 0 }, Cell.{ col = 1, row = 0 });
|
|
print("after: legal {} depth {} awarded {} len4 {} len5_plus {}\n",
|
|
miv.legal, miv.cascade.depth, miv.cascade.awarded, miv.cascade.len4, miv.cascade.len5_plus);
|
|
print("after: score {} moves_made {} moves_remaining {}\n", bi.score, bi.moves_made, miv.moves_remaining);
|
|
t.expect(!miv.legal, "illegal: swap rejected");
|
|
t.expect(miv.cascade.depth == 0, "illegal: no cascade");
|
|
t.expect(miv.cascade.awarded == 0, "illegal: no points");
|
|
t.expect(bi.moves_made == 0, "illegal: no move spent");
|
|
t.expect(miv.moves_remaining == LIMIT, "illegal: full budget remains");
|
|
t.expect(bi.score == 0, "illegal: score unchanged");
|
|
t.expect(boards_equal(@before, @bi), "illegal: board reverted");
|
|
|
|
// A legal swap that forms a length-4 run (RRRR at cols 0-3): proves the
|
|
// special-match flag rides through the whole settle onto the PlayerMove.
|
|
print("== commit-legal-len4 ==\n");
|
|
b4 := load_board(.[
|
|
"RROROGOG",
|
|
"GGROGOGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
"OGOGOGOG",
|
|
"GOGOGOGO",
|
|
], SEED, LIMIT);
|
|
out(board_dump(@b4));
|
|
m4 := commit_swap(@b4, Cell.{ col = 2, row = 0 }, Cell.{ col = 2, row = 1 });
|
|
print("after: legal {} depth {} awarded {} len4 {} len5_plus {}\n",
|
|
m4.legal, m4.cascade.depth, m4.cascade.awarded, m4.cascade.len4, m4.cascade.len5_plus);
|
|
print("after: had_len4 {} had_len5_plus {}\n", m4.cascade.had_len4(), m4.cascade.had_len5_plus());
|
|
print("after: score {} moves_made {} moves_remaining {}\n", b4.score, b4.moves_made, m4.moves_remaining);
|
|
out(board_dump(@b4));
|
|
t.expect(m4.legal, "legal-len4: swap committed");
|
|
t.expect(m4.cascade.had_len4(), "legal-len4: settle flags a length-4 run");
|
|
t.expect(b4.moves_made == 1, "legal-len4: one move spent");
|
|
t.expect(b4.score > 0, "legal-len4: score accrued");
|
|
|
|
print("ok: turn accounting + special-match flagging\n");
|
|
return 0;
|
|
}
|