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.
158 lines
7.3 KiB
Plaintext
158 lines
7.3 KiB
Plaintext
// Animation-layer determinism guard (P6.1): prove the swap/clear/fall animation
|
|
// timeline is PURELY VISUAL — it never changes the model's result. `plan_and_commit`
|
|
// commits the move on the real board (authoritative) AND records the visual
|
|
// timeline on a value-copy; this test asserts, on the SAME seed the app renders
|
|
// (SEED 1337):
|
|
// - the board `plan_and_commit` leaves is byte-for-byte identical to an
|
|
// independent `commit_swap` of the same move, with the same score + moves;
|
|
// - the recorded timeline ENDS on that exact state: `move.final` equals the
|
|
// model board, the rounds are contiguous (round 0 starts on the swapped board,
|
|
// each later round starts on the prior round's settled board), and the last
|
|
// round's `after` equals `final`;
|
|
// - an illegal swap records no rounds and leaves the board untouched.
|
|
// It also guards the P6.1 input-lock fix: `accepts_input` rejects a gesture for
|
|
// the FULL in-flight animation window (mouse_down → settle), so a swipe begun
|
|
// while a move animates never latches a drag and never commits — even if it is
|
|
// released after the timeline ends.
|
|
// No rendering — it calls exactly what BoardView.handle_event calls. Links headless
|
|
// like tests/swipe_commit.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.
|
|
#import "modules/std.sx";
|
|
#import "board.sx";
|
|
#import "board_anim.sx";
|
|
|
|
SEED :: 1337;
|
|
|
|
boards_equal :: (x: *Board, y: *Board) -> bool {
|
|
for 0..BOARD_CELLS (i) {
|
|
if !(x.cells[i] == y.cells[i]) { return false; }
|
|
}
|
|
true
|
|
}
|
|
|
|
main :: () -> i32 {
|
|
fails : i64 = 0;
|
|
|
|
// ── Legal swap: plan == model, timeline ends on the model ───────────────
|
|
// (5,4)->(6,4): brings R into (5,4), completing R,R,R across cols 3-5 of row
|
|
// 4 — the same legal swap tests/swipe_commit.sx commits.
|
|
print("== legal swap: plan matches model ==\n");
|
|
a := Cell.{ col = 5, row = 4 };
|
|
b := Cell.{ col = 6, row = 4 };
|
|
|
|
bm : Board = ---;
|
|
bm.init(SEED);
|
|
mvm := commit_swap(@bm, a, b);
|
|
|
|
ba : Board = ---;
|
|
ba.init(SEED);
|
|
move := plan_and_commit(@ba, a, b);
|
|
|
|
print("model: legal {} depth {} score {} moves {}\n",
|
|
mvm.legal, mvm.cascade.depth, bm.score, bm.moves_made);
|
|
print("plan: legal {} rounds {} score {} moves {}\n",
|
|
move.legal, move.rounds.len, ba.score, ba.moves_made);
|
|
|
|
if !move.legal { fails += 1; }
|
|
if !boards_equal(@ba, @bm) { fails += 1; } // committed board == model
|
|
if ba.score != bm.score { fails += 1; }
|
|
if ba.moves_made != bm.moves_made { fails += 1; }
|
|
if move.rounds.len != mvm.cascade.depth { fails += 1; }
|
|
|
|
// move.final equals the model board.
|
|
final_eq := true;
|
|
for 0..BOARD_CELLS (i) {
|
|
if !(move.final[i] == bm.cells[i]) { final_eq = false; }
|
|
}
|
|
if !final_eq { fails += 1; }
|
|
print("final==model {}\n", final_eq);
|
|
|
|
// Timeline contiguity: round 0 starts on the swapped pre board; each later
|
|
// round starts on the previous round's settled board; final == last after.
|
|
contiguous := true;
|
|
if move.rounds.len > 0 {
|
|
ai := Board.idx(a.col, a.row);
|
|
bi := Board.idx(b.col, b.row);
|
|
r0 := @move.rounds.items[0];
|
|
for 0..BOARD_CELLS (i) {
|
|
expect : Gem = move.pre[i];
|
|
if i == ai { expect = move.pre[bi]; }
|
|
else if i == bi { expect = move.pre[ai]; }
|
|
if !(r0.before[i] == expect) { contiguous = false; }
|
|
}
|
|
for 1..move.rounds.len (k) {
|
|
prev := @move.rounds.items[k - 1];
|
|
cur := @move.rounds.items[k];
|
|
for 0..BOARD_CELLS (i) {
|
|
if !(cur.before[i] == prev.after[i]) { contiguous = false; }
|
|
}
|
|
}
|
|
last := @move.rounds.items[move.rounds.len - 1];
|
|
for 0..BOARD_CELLS (i) {
|
|
if !(last.after[i] == move.final[i]) { contiguous = false; }
|
|
}
|
|
}
|
|
if !contiguous { fails += 1; }
|
|
print("contiguous {}\n", contiguous);
|
|
out("final board:\n");
|
|
out(board_dump(@bm));
|
|
|
|
// ── Illegal swap: no timeline, board untouched ──────────────────────────
|
|
// (0,0)->(1,0): two reds → no match. plan_and_commit must leave the board
|
|
// exactly as it was, spend no move, and record zero rounds.
|
|
print("== illegal swap: untouched ==\n");
|
|
bi2 : Board = ---;
|
|
bi2.init(SEED);
|
|
pre2 : Board = bi2;
|
|
mi := plan_and_commit(@bi2, Cell.{ col = 0, row = 0 }, Cell.{ col = 1, row = 0 });
|
|
print("legal {} rounds {} score {} moves {}\n", mi.legal, mi.rounds.len, bi2.score, bi2.moves_made);
|
|
if mi.legal { fails += 1; }
|
|
if mi.rounds.len != 0 { fails += 1; }
|
|
if !boards_equal(@pre2, @bi2) { fails += 1; }
|
|
if bi2.score != 0 { fails += 1; }
|
|
if bi2.moves_made != 0 { fails += 1; }
|
|
|
|
// ── Input gate: locked for the FULL in-flight animation ─────────────────
|
|
// The view begins a drag on mouse_down only when `accepts_input` is true,
|
|
// and commits on mouse_up only if a drag latched. So a gesture that BEGINS
|
|
// while a move animates must NEVER commit — even if it is released after the
|
|
// animation has fully settled. This guards the P6.1 input-lock fix.
|
|
print("== input gate: locked while animating ==\n");
|
|
gate : BoardAnim = ---;
|
|
gate.init();
|
|
idle_ok := accepts_input(@gate); // no move in flight → accept
|
|
gate.begin(move); // a legal move's timeline starts
|
|
busy_ok := accepts_input(@gate); // mouse_down DURING animation → reject
|
|
while gate.active { gate.tick(0.05); } // player holds; timeline plays out
|
|
settled_ok := accepts_input(@gate); // animation fully settled → accept
|
|
print("accepts idle {} busy {} settled {}\n", idle_ok, busy_ok, settled_ok);
|
|
if !idle_ok { fails += 1; }
|
|
if busy_ok { fails += 1; } // MUST be locked while animating
|
|
if !settled_ok { fails += 1; }
|
|
|
|
// The board MUST decide a gesture at PRESS, not at RELEASE. Over the exact
|
|
// failure scenario — a gesture PRESSED while animating and RELEASED after the
|
|
// timeline has settled — the two policies diverge:
|
|
// release-gate: commit unless animating AT RELEASE → COMMITS (the timeline
|
|
// finished first), letting input slip through mid-transition.
|
|
// press-gate: latch only if input accepted AT PRESS → DROPS, because
|
|
// input was locked for the whole window the timeline ran.
|
|
gate.init();
|
|
gate.begin(move);
|
|
accept_at_press := accepts_input(@gate); // mouse_down while animating
|
|
while gate.active { gate.tick(0.05); }
|
|
accept_at_release := accepts_input(@gate); // mouse_up after settle
|
|
release_gate_commits := accept_at_release;
|
|
press_gate_commits := accept_at_press;
|
|
print("release_gate_commits {} press_gate_commits {}\n", release_gate_commits, press_gate_commits);
|
|
if !release_gate_commits { fails += 1; } // the scenario release-gating lets through
|
|
if press_gate_commits { fails += 1; } // the board press-gates: MUST NOT commit
|
|
|
|
if fails == 0 {
|
|
print("ok: animation layer leaves the model result unchanged\n");
|
|
return 0;
|
|
}
|
|
print("FAIL: {} anim-determinism checks failed\n", fails);
|
|
return 1;
|
|
}
|