Files
m3te/tests/swipe_reshuffle.sx
swipelab 6f7d2f4db2 lang migration: rename signed integer types sN -> iN
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.
2026-06-12 09:36:51 +03:00

145 lines
6.1 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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;
}