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

View File

@@ -17,6 +17,7 @@
#import "modules/ui/font.sx";
#import "board.sx";
#import "board_layout.sx";
#import "swipe.sx";
// Fraction of a cell each gem occupies; the remainder is margin so a gem sits
// inside its cell tile rather than touching the tile's edges.
@@ -112,7 +113,8 @@ load_texture :: (path: [:0]u8, gpu: ?GPU) -> u32 {
// Which board cell the player has currently selected, if any. Lives behind a
// pointer (heap-allocated in main) because BoardView is a value rebuilt every
// frame from `build_ui`, so the view itself cannot carry state across frames.
// Selection only — P5 turns a selected→adjacent tap into a swap.
// A tap toggles this highlight; a swipe commits a swap (see DragInput) and
// clears it.
BoardSelection :: struct {
active: bool;
cell: Cell;
@@ -138,10 +140,35 @@ BoardSelection :: struct {
}
}
// Tracks an in-progress touch drag between its press and release so a swipe can
// be resolved on lift: the press records the start point, and release maps
// start→end through `swipe_intent` to an adjacent-swap intent. Heap-allocated
// (like BoardSelection) so it survives BoardView's per-frame rebuild between the
// down (touchesBegan → mouse_down) and up (touchesEnded → mouse_up) events.
DragInput :: struct {
active: bool;
start: Point;
init :: (self: *DragInput) {
self.active = false;
self.start = Point.{ x = 0.0, y = 0.0 };
}
begin :: (self: *DragInput, p: Point) {
self.active = true;
self.start = p;
}
clear :: (self: *DragInput) {
self.active = false;
}
}
BoardView :: struct {
board: *Board;
assets: *BoardAssets;
sel: *BoardSelection;
drag: *DragInput;
safe: EdgeInsets;
// Where the grid sits + the touch↔cell mapping. Recomputed each render /
@@ -209,14 +236,33 @@ impl View for BoardView {
render_hud(ctx, self.board, avail);
}
// Touch input. A press records the drag start; the release resolves the
// gesture against the SAME layout it was drawn with. A swipe (start→end maps
// to an adjacent-swap intent) is fed straight into `commit_swap`: a legal
// swap applies, cascades, scores and spends a move, an illegal one reverts —
// either way the next frame re-renders the board + HUD from the model. A
// sub-threshold / off-board drag carries no intent and falls back to the tap
// behaviour: toggle the selection on the pressed cell, or clear it off-board.
handle_event :: (self: *BoardView, event: *Event, frame: Frame) -> bool {
self.compute_layout(frame);
if event.* == {
case .mouse_down: (d) {
if hit := self.layout.point_to_cell(d.position) {
self.sel.toggle(hit);
} else {
self.drag.begin(d.position);
return true;
}
case .mouse_up: (d) {
if !self.drag.active { return false; }
start := self.drag.start;
self.drag.clear();
if intent := swipe_intent(@self.layout, start, d.position) {
commit_swap(self.board, intent.a, intent.b);
self.sel.clear();
} else {
if hit := self.layout.point_to_cell(start) {
self.sel.toggle(hit);
} else {
self.sel.clear();
}
}
return true;
}