P4.4: selection highlight + score/moves HUD (sx, iOS sim)

Tap a gem to select it: BoardView hit-tests the touch to a grid cell and
draws a bright rim + translucent fill over it; tapping the same cell clears
the selection, tapping another moves it, tapping off-board clears it.
Selection only — no swap (that's P5). The HUD renders the live score and
remaining moves (out of the move limit) in the Lato font on a translucent
card above the grid.

The touch→cell geometry is factored into a pure BoardLayout (no GL/stb
imports) that BoardView composes and P5 will reuse for swap endpoints.
tests/hit_test.sx locks point_to_cell as the exact inverse of cell_frame
(every cell center round-trips; off-board taps reject) — headless because
BoardLayout pulls no C imports. goldens/p4_hud.png captures the scene after
a real idb tap at (201,437)pt: the HUD plus a yellow selection rim on the
red gem at cell (col 4, row 3).
This commit is contained in:
swipelab
2026-06-05 00:00:48 +03:00
parent 3cd1ef1585
commit 9ed98c73d2
7 changed files with 237 additions and 25 deletions

View File

@@ -0,0 +1 @@
0

View File

@@ -0,0 +1,5 @@
grid origin (100,0) cell 75
ok: 64/64 cell centers round-trip
corner maps to (3,5)
ok: 0 off-board taps resolved to a cell
ok: hit-test mapping is the inverse of the layout

68
tests/hit_test.sx Normal file
View File

@@ -0,0 +1,68 @@
// Hit-test golden (P4.4): lock the touch→cell mapping `BoardLayout.point_to_cell`
// as the exact inverse of `cell_frame`. The two are written independently — one
// multiplies a cell index by cell_size, the other divides a point by cell_size
// and truncates — so round-tripping every cell center back to its own cell is a
// real check, not a tautology. BoardView and P5's swap input both reuse this
// mapping, so a drift here would silently send taps/swaps to the wrong cell.
//
// Imports BoardLayout (no GL/stb), not BoardView, so it links headless. It also
// avoids tests/test.sx, whose modules/process.sx → modules/trace.sx pulls in a
// second `Frame` struct that collides with the UI `Frame`. 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";
main :: () -> s32 {
// 800×600 with no safe inset → a 600px square grid, cell 75, centered: the
// grid origin lands at (100, 0). Integer math keeps the dump deterministic.
lay : BoardLayout = ---;
lay.compute(Frame.make(0.0, 0.0, 800.0, 600.0), EdgeInsets.zero());
print("grid origin ({},{}) cell {}\n",
cast(s64) lay.origin.x, cast(s64) lay.origin.y, cast(s64) lay.cell_size);
fails : s64 = 0;
// Every cell center must map back to its own cell.
hits : s64 = 0;
for 0..BOARD_ROWS: (row) {
for 0..BOARD_COLS: (col) {
cf := lay.cell_frame(col, row);
center := Point.{ x = cf.mid_x(), y = cf.mid_y() };
if h := lay.point_to_cell(center) {
if h.col == col and h.row == row { hits += 1; }
}
}
}
if hits != BOARD_CELLS { fails += 1; }
print("ok: {}/{} cell centers round-trip\n", hits, BOARD_CELLS);
// A cell's top-left corner belongs to that cell (the leading edge is
// inclusive), so corner-of-(3,5) resolves to (3,5).
corner := Point.{ x = lay.origin.x + 3.0 * lay.cell_size, y = lay.origin.y + 5.0 * lay.cell_size };
corner_col : s64 = -1;
corner_row : s64 = -1;
if h := lay.point_to_cell(corner) { corner_col = h.col; corner_row = h.row; }
if corner_col != 3 or corner_row != 5 { fails += 1; }
print("corner maps to ({},{})\n", corner_col, corner_row);
// Off-board taps reject (null): left of, above, and right of the grid. None
// should resolve to a cell, so the on-board count must stay 0.
off_left := Point.{ x = lay.origin.x - 5.0, y = lay.origin.y + 10.0 };
off_above := Point.{ x = lay.origin.x + 10.0, y = lay.origin.y - 5.0 };
off_right := Point.{ x = lay.origin.x + 8.0 * lay.cell_size + 1.0, y = lay.origin.y + 10.0 };
on_board : s64 = 0;
if h := lay.point_to_cell(off_left) { on_board += 1; print("off_left hit ({},{})\n", h.col, h.row); }
if h := lay.point_to_cell(off_above) { on_board += 1; print("off_above hit ({},{})\n", h.col, h.row); }
if h := lay.point_to_cell(off_right) { on_board += 1; print("off_right hit ({},{})\n", h.col, h.row); }
if on_board != 0 { fails += 1; }
print("ok: {} off-board taps resolved to a cell\n", on_board);
if fails == 0 {
print("ok: hit-test mapping is the inverse of the layout\n");
return 0;
}
print("FAIL: {} hit-test checks failed\n", fails);
return 1;
}