P5.2: swipe commits legal swap / reverts illegal (sx, iOS sim)

Wire touch input into the model in BoardView.handle_event. A press records
the drag start (new DragInput, heap-allocated so it survives the per-frame
BoardView rebuild between mouse_down and mouse_up); the release resolves the
gesture against the same layout it was drawn with. A swipe — start→end mapped
by swipe_intent to an adjacent-swap intent — is fed straight into
commit_swap: a legal swap applies, cascades (clear→collapse→refill), accrues
score and spends a move; an illegal one reverts, no move. A sub-threshold /
off-board drag carries no intent and falls back to the tap behaviour
(toggle/clear selection). The next frame re-renders board + HUD from the model.

Reuses swipe.sx + board_layout.sx + commit_swap unchanged — this is wiring,
not new legality/cascade logic.

tests/swipe_commit.sx (new golden) drives the full path on the seeded board
(SEED 1337): a rightward swipe (0,0)->(1,0) is illegal (two reds) and reverts
byte-for-byte with no score/move; (5,4)->(6,4) is legal, completes R,R,R on
row 4, awards 30, spends one move.

Sim evidence (iPhone 17, iOS 26.0): goldens/p5_swap_before.png (SCORE 0,
MOVES 30/30) and goldens/p5_swap_after.png (SCORE 30, MOVES 29/30) bracket a
real idb-injected swipe at (276,475)->(327,475) pt = cell (5,4)->(6,4); the
three reds clear and the board matches the model's resolved state.
This commit is contained in:
swipelab
2026-06-05 00:32:40 +03:00
parent ea0ba59a23
commit e5df37523f
7 changed files with 188 additions and 5 deletions

104
tests/swipe_commit.sx Normal file
View File

@@ -0,0 +1,104 @@
// Swipe→commit wiring golden (P5.2): prove the full input-to-model path P5.2
// adds — a touch drag resolved by `swipe_intent` and fed straight into
// `commit_swap` — on the SAME seeded board the iOS app renders (SEED 1337).
// Feeds SYNTHETIC down/up screen positions built from a BoardLayout, resolves
// the swap intent, then commits it, asserting:
// - an ILLEGAL swipe ((0,0)->(1,0): two reds → no match) reverts: the board is
// byte-for-byte unchanged, score stays 0, and no move is spent;
// - a known LEGAL swipe ((5,4)->(6,4): brings R into (5,4), completing R,R,R
// across cols 3-5 of row 4) commits: the board changes, score accrues, and
// exactly one move is spent.
// No rendering, no model reach-around — it calls exactly what BoardView.handle_event
// calls. Links headless like tests/swipe_intent.sx; avoids tests/test.sx because
// its trace.sx pulls in a second `Frame` that collides with the UI one. Failure
// is signalled via a non-zero exit code (the runner checks exit code AND stdout).
#import "modules/std.sx";
#import "board.sx";
#import "board_layout.sx";
#import "swipe.sx";
SEED :: 1337;
cell_center :: (lay: *BoardLayout, col: s64, row: s64) -> Point {
cf := lay.cell_frame(col, row);
Point.{ x = cf.mid_x(), y = cf.mid_y() }
}
boards_equal :: (x: *Board, y: *Board) -> bool {
for 0..BOARD_CELLS: (i) {
if !(x.cells[i] == y.cells[i]) { return false; }
}
true
}
main :: () -> s32 {
// 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.
lay : BoardLayout = ---;
lay.compute(Frame.make(0.0, 0.0, 800.0, 600.0), EdgeInsets.zero());
D : f32 = 60.0;
fails : s64 = 0;
// ── ILLEGAL swipe reverts ──────────────────────────────────────────────
// (0,0) and (1,0) are both red on the seed board, so swapping them forms no
// match. The rightward drag must resolve to exactly this pair, and
// commit_swap must reject it — board untouched, score 0, no move spent.
print("== illegal swipe reverts ==\n");
bi : Board = ---;
bi.init(SEED);
before : Board = bi;
out(board_dump(@bi));
a_il := cell_center(@lay, 0, 0);
if s := swipe_intent(@lay, a_il, Point.{ x = a_il.x + D, y = a_il.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 := commit_swap(@bi, s.a, s.b);
print("legal {} awarded {} score {} moves_made {} moves_remaining {}\n",
mv.legal, mv.cascade.awarded, bi.score, bi.moves_made, mv.moves_remaining);
if mv.legal { fails += 1; }
if !boards_equal(@before, @bi) { fails += 1; }
if bi.score != 0 { fails += 1; }
if bi.moves_made != 0 { fails += 1; }
if mv.moves_remaining != bi.move_limit { fails += 1; }
} else {
print("intent none\n");
fails += 1;
}
// ── LEGAL swipe commits ────────────────────────────────────────────────
// (5,4)->(6,4): the rightward swipe brings R into (5,4), completing R,R,R
// across cols 3-5 of row 4. commit_swap applies it, resolves the cascade
// (score accrues into Board.score) and spends one move; the board changes.
print("== legal swipe commits ==\n");
bl : Board = ---;
bl.init(SEED);
pre : Board = bl;
a_le := cell_center(@lay, 5, 4);
if s := swipe_intent(@lay, a_le, Point.{ x = a_le.x + D, y = a_le.y }) {
print("intent ({},{})->({},{})\n", s.a.col, s.a.row, s.b.col, s.b.row);
if !(s.a.col == 5 and s.a.row == 4 and s.b.col == 6 and s.b.row == 4) { fails += 1; }
mv := commit_swap(@bl, s.a, s.b);
print("legal {} depth {} awarded {} score {} moves_made {} moves_remaining {}\n",
mv.legal, mv.cascade.depth, mv.cascade.awarded, bl.score, bl.moves_made, mv.moves_remaining);
if !mv.legal { fails += 1; }
if boards_equal(@pre, @bl) { fails += 1; }
if !(bl.score > 0) { fails += 1; }
if bl.moves_made != 1 { fails += 1; }
if mv.moves_remaining != bl.move_limit - 1 { fails += 1; }
out("after:\n");
out(board_dump(@bl));
} else {
print("intent none\n");
fails += 1;
}
if fails == 0 {
print("ok: swipe reverts illegal, commits legal\n");
return 0;
}
print("FAIL: {} swipe-commit checks failed\n", fails);
return 1;
}