P3.1: base match scoring by run length (pure sx)
Add a run-enumeration path and base scoring to the pure-sx model, scoring one resolution round's clears purely by maximal-run length. - find_runs(board) enumerates each maximal H/V run (len >= 3) with its length, parallel to find_matches/MatchMask (left untouched so every clear/cascade caller is unaffected). - run_score(len): length 3 -> 30, 4 -> 60, 5+ -> 100 (named constants). - score_round(board): sum of run_score over the current runs (read-only, must be called before the round's clear). L/T rule: each maximal run scores independently by its own length, so an overlapping L/T scores horizontal + vertical (shared corner counts toward both runs). - Board gains a running `score` field (init zeroes it) and add_round_score accumulates a round's base points into it; the cross-round combo multiplier off Cascade.depth is left for P3.2. - tests/score.sx golden over hand-crafted single-round boards asserts exact points for len-3/4/5 runs, disjoint runs (sum), an overlapping L/T, and a no-match board (0).
This commit is contained in:
1
tests/expected/score.exit
Normal file
1
tests/expected/score.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
84
tests/expected/score.stdout
Normal file
84
tests/expected/score.stdout
Normal file
@@ -0,0 +1,84 @@
|
||||
== score (base match scoring) ==
|
||||
== len3-run-30 ==
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GORRROGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
--
|
||||
1 runs
|
||||
H len 3 at fixed 3 start 2
|
||||
points 30
|
||||
== len4-run-60 ==
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GORRRRGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
--
|
||||
1 runs
|
||||
H len 4 at fixed 3 start 2
|
||||
points 60
|
||||
== len5-run-100 ==
|
||||
OGOGOGOG
|
||||
GRRRRRGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
--
|
||||
1 runs
|
||||
H len 5 at fixed 1 start 1
|
||||
points 100
|
||||
== disjoint-30+60+100 ==
|
||||
RRRGOGOG
|
||||
GOGOGOGO
|
||||
GOBBBBOG
|
||||
OGOGOGOG
|
||||
GPPPPPGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
--
|
||||
3 runs
|
||||
H len 3 at fixed 0 start 0
|
||||
H len 4 at fixed 2 start 2
|
||||
H len 5 at fixed 4 start 1
|
||||
points 190
|
||||
== overlap-LT-per-run-120 ==
|
||||
OGOGOGOG
|
||||
GRRRGOGO
|
||||
OROGOGOG
|
||||
GRGOGOGO
|
||||
OGOGOGOG
|
||||
GOGYYYGO
|
||||
OGOGYGOG
|
||||
GOGOYOGO
|
||||
--
|
||||
4 runs
|
||||
H len 3 at fixed 1 start 1
|
||||
H len 3 at fixed 5 start 3
|
||||
V len 3 at fixed 1 start 1
|
||||
V len 3 at fixed 4 start 5
|
||||
points 120
|
||||
== no-match-0 ==
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
OGOGOGOG
|
||||
GOGOGOGO
|
||||
--
|
||||
0 runs
|
||||
points 0
|
||||
ok: base scoring over hand-crafted boards
|
||||
144
tests/score.sx
Normal file
144
tests/score.sx
Normal file
@@ -0,0 +1,144 @@
|
||||
// Base-scoring golden (P3.1): score several HAND-CRAFTED single-round boards by
|
||||
// RUN LENGTH and snapshot the runs + points. Every board sits on the run-free
|
||||
// O/G checkerboard (adjacent cells always differ, so zero stray runs) with only
|
||||
// the runs under test painted in, so the score is purely the painted runs'.
|
||||
//
|
||||
// Scheme (named constants in board.sx): run length 3 -> 30, length 4 -> 60,
|
||||
// length 5+ -> 100. L/T rule: each maximal run scores INDEPENDENTLY by its own
|
||||
// length, so an overlapping L/T scores horizontal + vertical (the shared corner
|
||||
// counts toward both runs). The scene names encode the expected points.
|
||||
//
|
||||
// For each scene the board, its enumerated runs, and the round's points are
|
||||
// printed, and two facts are asserted independently of the dump: `score_round`
|
||||
// equals the documented value, and `add_round_score` adds exactly that into the
|
||||
// board's running `score` total.
|
||||
#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),
|
||||
// with the running score zeroed so the accumulation check starts from a known base.
|
||||
load_board :: (rows: []string) -> 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.score = 0;
|
||||
b
|
||||
}
|
||||
|
||||
// Score one scene: snapshot board + enumerated runs + points, then assert
|
||||
// `score_round` is exact and `add_round_score` accumulates it into `board.score`.
|
||||
scene :: (name: string, rows: []string, want_points: s64) {
|
||||
b := load_board(rows);
|
||||
runs := find_runs(@b);
|
||||
|
||||
print("== {} ==\n", name);
|
||||
out(board_dump(@b));
|
||||
out("--\n");
|
||||
out(dump_runs(@runs));
|
||||
print("points {}\n", score_round(@b));
|
||||
|
||||
t.expect(score_round(@b) == want_points, concat(name, ": score_round exact"));
|
||||
added := add_round_score(@b);
|
||||
t.expect(added == want_points and b.score == want_points,
|
||||
concat(name, ": add_round_score accumulates into board.score"));
|
||||
}
|
||||
|
||||
main :: () -> s32 {
|
||||
print("== score (base match scoring) ==\n");
|
||||
|
||||
// Single length-3 horizontal run (row 3, cols 2-4) -> SCORE_RUN_3 = 30.
|
||||
scene("len3-run-30", .[
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GORRROGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
], 30);
|
||||
|
||||
// Single length-4 horizontal run (row 3, cols 2-5) -> SCORE_RUN_4 = 60.
|
||||
scene("len4-run-60", .[
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GORRRRGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
], 60);
|
||||
|
||||
// Single length-5 horizontal run (row 1, cols 1-5) -> SCORE_RUN_5_PLUS = 100.
|
||||
scene("len5-run-100", .[
|
||||
"OGOGOGOG",
|
||||
"GRRRRRGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
], 100);
|
||||
|
||||
// Multiple disjoint runs, points = sum of per-run values: horizontal R len3
|
||||
// (row 0, cols 0-2 -> 30), horizontal B len4 (row 2, cols 2-5 -> 60),
|
||||
// horizontal P len5 (row 4, cols 1-5 -> 100) = 190.
|
||||
scene("disjoint-30+60+100", .[
|
||||
"RRRGOGOG",
|
||||
"GOGOGOGO",
|
||||
"GOBBBBOG",
|
||||
"OGOGOGOG",
|
||||
"GPPPPPGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
], 190);
|
||||
|
||||
// Overlapping L and T, scored per-run independently: the L is a horizontal R
|
||||
// len3 (row 1, cols 1-3) meeting a vertical R len3 (col 1, rows 1-3) at corner
|
||||
// (1,1) -> 30 + 30; the T is a horizontal Y len3 (row 5, cols 3-5) meeting a
|
||||
// vertical Y len3 (col 4, rows 5-7) at (4,5) -> 30 + 30. Total 120 (the shared
|
||||
// corners count toward both runs, unlike the unioned clear set).
|
||||
scene("overlap-LT-per-run-120", .[
|
||||
"OGOGOGOG",
|
||||
"GRRRGOGO",
|
||||
"OROGOGOG",
|
||||
"GRGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGYYYGO",
|
||||
"OGOGYGOG",
|
||||
"GOGOYOGO",
|
||||
], 120);
|
||||
|
||||
// No match: the bare checkerboard has no run -> 0 points.
|
||||
scene("no-match-0", .[
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
"OGOGOGOG",
|
||||
"GOGOGOGO",
|
||||
], 0);
|
||||
|
||||
print("ok: base scoring over hand-crafted boards\n");
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user