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.
145 lines
6.1 KiB
Plaintext
145 lines
6.1 KiB
Plaintext
// UI deadlock-recovery golden (final-review F1): prove the RENDERED swipe-commit
|
||
// path reshuffles a deadlocked board, just like the headless turn loop's no-moves
|
||
// rule (tests/level.sx). The iOS/macOS view commits a swipe through
|
||
// `plan_and_commit` (NOT `play_turn`), so the reshuffle must live on THAT shared
|
||
// path or a stuck board stays stuck on screen. This test drives exactly what
|
||
// BoardView.handle_event does — resolve a drag with `swipe_intent`, feed the intent
|
||
// into `plan_and_commit` — on the provably deadlocked diagonal-Latin-square board
|
||
// from tests/level.sx, asserting:
|
||
// - BEFORE: no immediate match, zero legal swaps, status in_progress (stuck);
|
||
// - AFTER the UI commit path resolves: the board RESHUFFLED — `has_legal_swap`
|
||
// is true and >=1 legal swap exists, still with no immediate match — and the
|
||
// reshuffle itself spent no move and no score (turn accounting unchanged).
|
||
// Pre-fix `plan_and_commit` skipped the reshuffle, so `has_legal_swap` stayed false
|
||
// after and this FAILS; with the shared `reshuffle_if_deadlocked` wired in it PASSES.
|
||
// No rendering, no model reach-around. Links headless like tests/anim_plan.sx;
|
||
// avoids tests/test.sx (its trace.sx pulls in a second `Frame` that collides with
|
||
// the UI one). Failure is a non-zero exit code (the runner checks exit + stdout).
|
||
#import "modules/std.sx";
|
||
#import "modules/ui/types.sx";
|
||
#import "board.sx";
|
||
#import "board_anim.sx";
|
||
#import "board_layout.sx";
|
||
#import "swipe.sx";
|
||
|
||
SEED :: 1337;
|
||
|
||
// Inverse of `gem_char`: map a board character back to its Gem so the deadlocked
|
||
// board can be written as a human-readable grid (mirrors tests/level.sx).
|
||
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), seeded RNG, score zeroed, turn
|
||
// counters reset to a fresh game, and the per-level goal set.
|
||
load_board :: (rows: []string, seed: i64, move_limit: i64, target_score: 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.target_score = target_score;
|
||
b
|
||
}
|
||
|
||
cell_center :: (lay: *BoardLayout, col: i64, row: i64) -> Point {
|
||
cf := lay.cell_frame(col, row);
|
||
Point.{ x = cf.mid_x(), y = cf.mid_y() }
|
||
}
|
||
|
||
main :: () -> i32 {
|
||
fails : i64 = 0;
|
||
|
||
// 800×600, no safe inset → 600px square grid, cell 75, origin (100, 0). A 60px
|
||
// drag clears the cell*0.5 = 37.5px swipe threshold on the dominant axis — the
|
||
// SAME layout/threshold tests/swipe_commit.sx drives the UI path through.
|
||
lay : BoardLayout = ---;
|
||
lay.compute(Frame.make(0.0, 0.0, 800.0, 600.0), EdgeInsets.zero());
|
||
D : f32 = 60.0;
|
||
|
||
// The provably deadlocked board from tests/level.sx: gem(col,row)=(col-row) mod
|
||
// 6, a diagonal Latin square. Equal gems lie only on slope-1 diagonals, so no
|
||
// row/column holds two of a kind within reach — no immediate match and no
|
||
// orthogonal swap can line up three.
|
||
print("== deadlocked board: UI swipe-commit path must reshuffle ==\n");
|
||
b := load_board(.[
|
||
"ROYGBPRO",
|
||
"PROYGBPR",
|
||
"BPROYGBP",
|
||
"GBPROYGB",
|
||
"YGBPROYG",
|
||
"OYGBPROY",
|
||
"ROYGBPRO",
|
||
"PROYGBPR",
|
||
], SEED, 10, 1500);
|
||
out(board_dump(@b));
|
||
|
||
pre_m := find_matches(@b);
|
||
pre_sw := legal_swaps(@b);
|
||
print("before: matches {} legal_swaps {} has_legal_swap {} status {}\n",
|
||
pre_m.count(), pre_sw.len, has_legal_swap(@b), status_name(level_status(@b)));
|
||
if pre_m.count() != 0 { fails += 1; }
|
||
if pre_sw.len != 0 { fails += 1; }
|
||
if has_legal_swap(@b) { fails += 1; }
|
||
if level_status(@b) != .in_progress { fails += 1; }
|
||
|
||
score_before := b.score;
|
||
made_before := b.moves_made;
|
||
remain_before := b.moves_remaining();
|
||
|
||
// Drive the EXACT UI path: a rightward drag on (0,0) resolves to the (0,0)->(1,0)
|
||
// swap intent, which BoardView.handle_event feeds straight into plan_and_commit.
|
||
// On this deadlocked board every swap is illegal (no match), so the swipe itself
|
||
// commits nothing — the recovery must come from the post-settle reshuffle.
|
||
a0 := cell_center(@lay, 0, 0);
|
||
if s := swipe_intent(@lay, a0, Point.{ x = a0.x + D, y = a0.y }) {
|
||
print("intent ({},{})->({},{})\n", s.a.col, s.a.row, s.b.col, s.b.row);
|
||
if !(s.a.col == 0 and s.a.row == 0 and s.b.col == 1 and s.b.row == 0) { fails += 1; }
|
||
mv := plan_and_commit(@b, s.a, s.b);
|
||
print("commit: legal {} rounds {} awarded {}\n", mv.legal, mv.rounds.len, mv.awarded);
|
||
// The illegal swipe spends nothing; its timeline is the bare ping-back.
|
||
if mv.legal { fails += 1; }
|
||
if mv.rounds.len != 0 { fails += 1; }
|
||
} else {
|
||
print("intent none\n");
|
||
fails += 1;
|
||
}
|
||
|
||
post_m := find_matches(@b);
|
||
post_sw := legal_swaps(@b);
|
||
print("after: matches {} legal_swaps {} has_legal_swap {} status {}\n",
|
||
post_m.count(), post_sw.len, has_legal_swap(@b), status_name(level_status(@b)));
|
||
print("after: score {} moves_made {} moves_remaining {}\n",
|
||
b.score, b.moves_made, b.moves_remaining());
|
||
out(board_dump(@b));
|
||
|
||
// The fix: the UI commit path reshuffled the deadlock away. has_legal_swap flips
|
||
// false -> true; >=1 legal swap exists; still no immediate match.
|
||
if !has_legal_swap(@b) { fails += 1; }
|
||
if post_sw.len <= 0 { fails += 1; }
|
||
if post_m.count() != 0 { fails += 1; }
|
||
|
||
// The reshuffle is NOT a move: score, moves spent, and budget are untouched.
|
||
if b.score != score_before { fails += 1; }
|
||
if b.moves_made != made_before { fails += 1; }
|
||
if b.moves_remaining() != remain_before { fails += 1; }
|
||
if level_status(@b) != .in_progress { fails += 1; }
|
||
|
||
if fails == 0 {
|
||
print("ok: UI swipe-commit path reshuffles a deadlocked board\n");
|
||
return 0;
|
||
}
|
||
print("FAIL: {} UI-reshuffle checks failed\n", fails);
|
||
return 1;
|
||
}
|