Add a purely-visual animation timeline so the board no longer snaps on a move. board_anim.sx records, on a value-copy of the pre-move board, the swap and each cascade round's matched cells + per-column fall provenance, then BoardView plays it over delta_time: the two swapped gems SLIDE between cells (and ping out-and-back on an illegal swap), matched gems SCALE OUT, and survivors FALL into place while refills drop in from above the grid. The model stays authoritative: plan_and_commit still calls commit_swap on the real board exactly as before, and the recording replays the identical primitives from the identical cells + RNG state, so the timeline ends ON the model's settled board. tests/anim_plan.sx is the determinism guard — it asserts the committed board, score, moves, and the timeline's final state all equal an independent commit_swap of the same move, that the rounds are contiguous, and that an illegal swap records nothing and leaves the board untouched. All pre-existing logic/cascade goldens stay green. Evidence (sx-test-metal, iOS 26.0, time-sampled with temporarily-lengthened durations; committed durations are the short production values): goldens/p6_anim_swap.png gems sliding between (5,4)/(6,4) goldens/p6_anim_clear.png matched reds scaling out in row 4 goldens/p6_anim_fall.png gems mid-fall with gaps + refill dropping in goldens/p6_anim_after.png settled board == model (SCORE 30, MOVES 29/30)
118 lines
4.7 KiB
Plaintext
118 lines
4.7 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.
|
|
// 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 :: () -> s32 {
|
|
fails : s64 = 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; }
|
|
|
|
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;
|
|
}
|