P7.2: goal HUD + win/lose banner + restart button (sx, iOS sim)
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.
This commit is contained in:
81
tests/banner_layout.sx
Normal file
81
tests/banner_layout.sx
Normal file
@@ -0,0 +1,81 @@
|
||||
// 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;
|
||||
}
|
||||
1
tests/expected/banner_layout.exit
Normal file
1
tests/expected/banner_layout.exit
Normal file
@@ -0,0 +1 @@
|
||||
0
|
||||
9
tests/expected/banner_layout.stdout
Normal file
9
tests/expected/banner_layout.stdout
Normal file
@@ -0,0 +1,9 @@
|
||||
grid (100,0,600,600)
|
||||
panel (148,168,503,264)
|
||||
title (148,215,503,79)
|
||||
button (248,326,302,63)
|
||||
button mid_x 400 grid mid_x 400
|
||||
button corners inside panel: 4/4
|
||||
button center hit: true
|
||||
off-button taps that hit the button: 0
|
||||
ok: restart button hit-test matches the drawn banner
|
||||
Reference in New Issue
Block a user