Merge branch 'flow/m3te/P5.2' into m3te-plan
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
BIN
goldens/p5_swap_after.png
Normal file
BIN
goldens/p5_swap_after.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 MiB |
BIN
goldens/p5_swap_before.png
Normal file
BIN
goldens/p5_swap_before.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 MiB |
10
main.sx
10
main.sx
@@ -44,10 +44,15 @@ g_assets : *BoardAssets = null;
|
||||
// per-frame rebuild; a tap hit-tests a cell and toggles this.
|
||||
g_sel : *BoardSelection = null;
|
||||
|
||||
// In-progress touch drag (P5.2). Heap-allocated for the same reason: the press
|
||||
// and release that bracket a swipe land on different per-frame BoardView values,
|
||||
// so the drag start must persist between them.
|
||||
g_drag : *DragInput = null;
|
||||
|
||||
// Rebuilt each frame inside the pipeline's arena; carries the current safe-area
|
||||
// insets so the grid stays inside the notch / home-indicator region.
|
||||
build_ui :: () -> View {
|
||||
BoardView.{ board = g_board, assets = g_assets, sel = g_sel, safe = g_safe_insets }
|
||||
BoardView.{ board = g_board, assets = g_assets, sel = g_sel, drag = g_drag, safe = g_safe_insets }
|
||||
}
|
||||
|
||||
frame :: () {
|
||||
@@ -144,6 +149,9 @@ main :: () -> void {
|
||||
g_sel = xx context.allocator.alloc(size_of(BoardSelection));
|
||||
g_sel.init();
|
||||
|
||||
g_drag = xx context.allocator.alloc(size_of(DragInput));
|
||||
g_drag.init();
|
||||
|
||||
g_pipeline.set_body(closure(build_ui));
|
||||
|
||||
g_plat.run_frame_loop(closure(frame));
|
||||
|
||||
1
tests/expected/swipe_commit.exit
Normal file
1
tests/expected/swipe_commit.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
24
tests/expected/swipe_commit.stdout
Normal file
24
tests/expected/swipe_commit.stdout
Normal file
@@ -0,0 +1,24 @@
|
||||
== illegal swipe reverts ==
|
||||
RRPPOGRG
|
||||
PGPOPRRO
|
||||
YYBBYRYB
|
||||
GBYYRGGP
|
||||
OGBRRORY
|
||||
BYRRPRBG
|
||||
YOYYROBB
|
||||
OROBPPRB
|
||||
intent (0,0)->(1,0)
|
||||
legal false awarded 0 score 0 moves_made 0 moves_remaining 30
|
||||
== legal swipe commits ==
|
||||
intent (5,4)->(6,4)
|
||||
legal true depth 1 awarded 30 score 30 moves_made 1 moves_remaining 29
|
||||
after:
|
||||
RRPBORRG
|
||||
PGPPOGRO
|
||||
YYBOPRYB
|
||||
GBYBYRGP
|
||||
OGBYRGOY
|
||||
BYRRPRBG
|
||||
YOYYROBB
|
||||
OROBPPRB
|
||||
ok: swipe reverts illegal, commits legal
|
||||
104
tests/swipe_commit.sx
Normal file
104
tests/swipe_commit.sx
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user