Files
m3te/README.md
swipelab 5be379f180 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.
2026-06-05 14:57:27 +03:00

118 lines
5.1 KiB
Markdown

# m3te
A match-3 game written entirely in the **sx** language, targeting **iOS** first.
- Game logic, rendering, input, and UI are all authored in sx.
- Art (palettes, sprite sheets) is produced as real assets.
- Verification gate: sx logic tests pass AND the iOS app builds & launches in the Simulator.
Development is driven by the multi-agent `flow` (Product Owner → Worker → Reviewer → Observer).
## Verification gate
The gate has two halves. Both must pass. The sx compiler used below lives at
`/Users/agra/projects/sx/zig-out/bin/sx` (override the runner's binary with the
`SX` env var). Run everything from the repo root.
### 1. Logic tests
Pure-sx logic tests run under `sx` and have their stdout + exit code diffed
against committed snapshots in `tests/expected/`. A failed assertion exits the
process non-zero, so it fails the runner (and the gate).
```bash
bash tools/run_tests.sh
```
- A test is any `tests/<name>.sx` that has a `tests/expected/<name>.exit`
marker; `tests/test.sx` (the `expect` assert helper) has no marker, so it is
not itself run.
- Regenerate snapshots after an intentional change: `bash tools/run_tests.sh --update`.
### 2. iOS Simulator build + launch
Build the app for the simulator, then install/launch it on an available device
and screenshot the rendered scene (blue background + a centered orange quad).
```bash
# Build the .app bundle (sx-out/ios/M3te.app):
/Users/agra/projects/sx/zig-out/bin/sx build --target ios-sim main.sx
# Discover an available simulator — do NOT hardcode a udid:
xcrun simctl list devices available
# e.g. capture the first available device's UDID into $udid:
udid=$(xcrun simctl list devices available | grep -Eo '[0-9A-Fa-f-]{36}' | head -1)
# Boot it (skip if already "Booted") and bring the Simulator window up:
xcrun simctl boot "$udid" || true
open -a Simulator
# Install, launch (bundle id co.swipelab.m3te), and screenshot:
xcrun simctl install booted sx-out/ios/M3te.app
xcrun simctl launch booted co.swipelab.m3te
xcrun simctl io booted screenshot /tmp/m3te.png
```
The screenshot should match `goldens/p0_quad.png` (a centered orange quad over a
blue clear), modulo the status-bar clock — pixel-exact equality is not required.
A tap on the quad flips its color (orange ↔ green); see
`goldens/p0_input_before.png` / `goldens/p0_input_after.png`.
### Deterministic animation capture (P6.3)
The per-gem idle loop (`gem_anim.sx`) is always-on, so a plain screenshot is
time-dependent. Two environment variables pin the visual state so the board can
be captured reproducibly. The simulator forwards any `SIMCTL_CHILD_*` variable to
the launched app, so prefix them on the `simctl launch`:
- `M3TE_ANIM_TIME=<seconds>` freezes the animation clock at that phase. **`t=0`
is the resting board** — every gem sits at its static pose, so the pre-P6.3
goldens reproduce unchanged. A larger `t` (e.g. `1.0`) shows the mid-breath
idle deformation. The select/land reactions read this same pinned phase.
- `M3TE_SELECT=<cellIndex 0..63>` (= `row*8 + col`) force-selects a cell at
startup, so the selection highlight + pop can be captured without a tap.
```bash
# Resting board (idle at rest): goldens/p6_idle_t0.png
SIMCTL_CHILD_M3TE_ANIM_TIME=0 xcrun simctl launch booted co.swipelab.m3te
# Mid-breath idle: goldens/p6_idle_mid.png
SIMCTL_CHILD_M3TE_ANIM_TIME=1.0 xcrun simctl launch booted co.swipelab.m3te
# Selection pop on cell (3,3): goldens/p6_select.png
env SIMCTL_CHILD_M3TE_ANIM_TIME=0.17 SIMCTL_CHILD_M3TE_SELECT=27 \
xcrun simctl launch booted co.swipelab.m3te
```
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.