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:
swipelab
2026-06-05 14:57:27 +03:00
parent bf38c7a100
commit 5be379f180
10 changed files with 284 additions and 7 deletions

View File

@@ -84,3 +84,34 @@ env SIMCTL_CHILD_M3TE_ANIM_TIME=0.17 SIMCTL_CHILD_M3TE_SELECT=27 \
With no variable set the game runs fully live (the clock advances by
`delta_time`). `tests/gem_pose.sx` locks the `t==0`-rest invariant headlessly.
### Level-state capture (P7.2)
The win/lose banner and restart button are driven by the model's `level_status`
(score vs. goal vs. move budget). Three more env hooks force a terminal status
(or a restart) so the banner / restart states can be screenshot deterministically
without scripting a winning swipe — combine them with `M3TE_ANIM_TIME` to pin the
idle clock:
- `M3TE_TARGET=<n>` overrides the per-level score goal. `0` makes the fresh board
read **won** immediately (`score 0 ≥ goal 0`).
- `M3TE_MOVE_LIMIT=<n>` overrides the move budget. `0` makes it read **lost**
(budget spent below the goal).
- `M3TE_RESTART=<non-zero>` runs `board.restart` after the overrides, capturing
the fresh `in_progress` board the restart button produces.
```bash
# Win banner + restart over the board: goldens/p7_win.png
env SIMCTL_CHILD_M3TE_TARGET=0 SIMCTL_CHILD_M3TE_ANIM_TIME=0 \
xcrun simctl launch booted co.swipelab.m3te
# Lose banner ("OUT OF MOVES") + restart: goldens/p7_lose.png
env SIMCTL_CHILD_M3TE_MOVE_LIMIT=0 SIMCTL_CHILD_M3TE_ANIM_TIME=0 \
xcrun simctl launch booted co.swipelab.m3te
# Fresh in_progress board after restart: goldens/p7_restart.png
env SIMCTL_CHILD_M3TE_TARGET=0 SIMCTL_CHILD_M3TE_RESTART=1 SIMCTL_CHILD_M3TE_ANIM_TIME=0 \
xcrun simctl launch booted co.swipelab.m3te
```
While a banner is up the board freezes (only the restart button is live, per
P7.1's finished-level rule); `tests/banner_layout.sx` locks the restart button's
rect ↔ hit-test round-trip headlessly.