Extend the HUD to show the per-level goal (SCORE x / target) alongside
moves. When the model's level_status (P7.1) is won/lost, draw a centered
overlay banner ("YOU WIN!" / "OUT OF MOVES") with a "PLAY AGAIN" restart
button over the dimmed board; the banner appears once any winning/losing
cascade animation settles. Status is read from the model, never recomputed
in the view.
A finished level freezes board-cell input; only the restart button is live.
Its rect is derived from the shared BoardLayout grid (new BannerLayout), so
the hit-test lands exactly on the drawn button. A tap reseeds the same
starting level through board.restart and clears the transient view layers,
returning to a clean in_progress board.
Banner is text + rects only (honours colour/alpha; no draw-time image tint,
issue 0002). New env capture hooks (M3TE_TARGET / M3TE_MOVE_LIMIT /
M3TE_RESTART) force a terminal status / restart for deterministic goldens.
Tests: tests/banner_layout.sx locks the restart button rect <-> hit-test
round-trip headlessly. Goldens p7_win / p7_lose / p7_restart captured on the
iOS simulator.
82 lines
3.7 KiB
Plaintext
82 lines
3.7 KiB
Plaintext
// Banner / restart hit-test golden (P7.2): lock the win/lose banner geometry and
|
||
// the restart-button hit-test that `BoardView.handle_event` relies on. The button
|
||
// rect is derived from the SAME grid layout the gems use, and a finished level
|
||
// freezes every board-cell tap, so the button is the only live target — if its
|
||
// rect drifted from what `render_banner` draws, a tap on the visible button would
|
||
// miss and the level could never be restarted. This test checks, headlessly:
|
||
//
|
||
// * the button is centered over the grid and sits fully inside the panel,
|
||
// * the button centre hit-tests INTO the button (tap → restart),
|
||
// * an off-button tap (a board corner) hit-tests OUT (frozen, no restart).
|
||
//
|
||
// Imports BoardLayout (no GL/stb), not BoardView, so it links headless — same
|
||
// shape and rationale as tests/hit_test.sx. 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";
|
||
|
||
irect :: (f: Frame) -> string {
|
||
format("({},{},{},{})",
|
||
cast(s64) f.origin.x, cast(s64) f.origin.y,
|
||
cast(s64) f.size.width, cast(s64) f.size.height)
|
||
}
|
||
|
||
main :: () -> s32 {
|
||
// 800×600, no safe inset → 600px square grid, cell 75, origin (100,0): the
|
||
// same layout tests/hit_test.sx pins, so the numbers are checkable by hand.
|
||
lay : BoardLayout = ---;
|
||
lay.compute(Frame.make(0.0, 0.0, 800.0, 600.0), EdgeInsets.zero());
|
||
|
||
grid := lay.grid_frame();
|
||
bl := lay.banner();
|
||
print("grid {}\n", irect(grid));
|
||
print("panel {}\n", irect(bl.panel));
|
||
print("title {}\n", irect(bl.title));
|
||
print("button {}\n", irect(bl.button));
|
||
|
||
fails : s64 = 0;
|
||
|
||
// The button is horizontally centered on the grid (centred banner).
|
||
bcx := bl.button.mid_x();
|
||
if cast(s64) bcx != cast(s64) grid.mid_x() { fails += 1; }
|
||
print("button mid_x {} grid mid_x {}\n", cast(s64) bcx, cast(s64) grid.mid_x());
|
||
|
||
// The whole button sits inside the panel — its four corners are contained,
|
||
// so it can never spill outside the drawn card.
|
||
bx0 := bl.button.origin.x; by0 := bl.button.origin.y;
|
||
bx1 := bl.button.max_x(); by1 := bl.button.max_y();
|
||
corners_in : s64 = 0;
|
||
if bl.panel.contains(Point.{ x = bx0, y = by0 }) { corners_in += 1; }
|
||
if bl.panel.contains(Point.{ x = bx1, y = by0 }) { corners_in += 1; }
|
||
if bl.panel.contains(Point.{ x = bx0, y = by1 }) { corners_in += 1; }
|
||
if bl.panel.contains(Point.{ x = bx1, y = by1 }) { corners_in += 1; }
|
||
if corners_in != 4 { fails += 1; }
|
||
print("button corners inside panel: {}/4\n", corners_in);
|
||
|
||
// A tap on the button centre restarts (hit-test true); the panel itself is
|
||
// also contained, so the centre is unambiguously on the button.
|
||
center := Point.{ x = bl.button.mid_x(), y = bl.button.mid_y() };
|
||
hit_center := bl.button.contains(center);
|
||
if !hit_center { fails += 1; }
|
||
print("button center hit: {}\n", hit_center);
|
||
|
||
// Off-button taps that a finished level must NOT treat as restart: the grid's
|
||
// top-left corner cell centre, and a point just outside the panel. Neither is
|
||
// in the button, so each leaves the level frozen.
|
||
corner_cell := Point.{ x = grid.origin.x + lay.cell_size * 0.5, y = grid.origin.y + lay.cell_size * 0.5 };
|
||
outside := Point.{ x = bl.panel.origin.x - 5.0, y = bl.panel.mid_y() };
|
||
off_hits : s64 = 0;
|
||
if bl.button.contains(corner_cell) { off_hits += 1; }
|
||
if bl.button.contains(outside) { off_hits += 1; }
|
||
if off_hits != 0 { fails += 1; }
|
||
print("off-button taps that hit the button: {}\n", off_hits);
|
||
|
||
if fails == 0 {
|
||
print("ok: restart button hit-test matches the drawn banner\n");
|
||
return 0;
|
||
}
|
||
print("FAIL: {} banner hit-test checks failed\n", fails);
|
||
return 1;
|
||
}
|