Visual-juice vibe-pass, FX-only — no logic/state changes, input gating still owned by BoardAnim.active. - board_fx.sx: bigger, punchier match bursts — peak size 1.95->2.50 cells, combo bonus 0.55->0.72, and the per-gem fx tints saturated a touch (low channel trimmed, dominant/mid lifted) so every burst pops as a brighter, more vivid candy colour. The hot per-pixel tint loop's hoisted locals are preserved (issue 0001). - gem_anim.sx: snappier clear pop — faster rise (0.30->0.18 of the window) to a bigger overshoot (CLEAR_POP_A 0.22->0.34) so the matched-gem clear reads as a candy snap. gem_pose's clear-pop invariants still hold. - main.sx: M3TE_FX=<n> deterministic match-FX capture hook, mirroring the M3TE_SELECT pattern. Commits the n-th currently-legal swap at startup via the normal plan_and_commit path and begins the move timeline + burst/popup FX; M3TE_ANIM_TIME pins the phase and the frame loop holds the move/FX frozen while pinned, so the burst + "+points" screenshot identically every run. A larger M3TE_ANIM_TIME captures the settled, FX-gone board. Startup- only and guarded, so normal play is untouched. - README.md: document the new M3TE_FX pin alongside the other capture hooks. - goldens/p6_fx_match.png: updated deterministic golden (iOS 26 sim, SIMCTL_CHILD_M3TE_FX=3 SIMCTL_CHILD_M3TE_ANIM_TIME=0.22) — the vertical red 3-match, burst region +1.4% mean luminance / 3.2:1 brighter:dimmer vs the same scene on the pre-juice constants. Gate: ios-sim build links, 19/19 logic tests green (incl. gem_pose t=0 rest).
147 lines
6.6 KiB
Markdown
147 lines
6.6 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.
|
|
|
|
### Match-FX capture (P11.1)
|
|
|
|
The match bursts + score popup (`board_fx.sx`) only spawn off a *committed* move,
|
|
which the simulator can't script (there is no public touch injection). One more
|
|
env hook forces a representative match at startup so the FX can be screenshot
|
|
deterministically — combine it with `M3TE_ANIM_TIME` to freeze the phase:
|
|
|
|
- `M3TE_FX=<n>` commits the **n-th currently-legal swap** (1-based, clamped; `=1`
|
|
is the first) through the normal `plan_and_commit` path, then begins the move
|
|
timeline + its burst/popup FX. While `M3TE_ANIM_TIME` is set the move/FX
|
|
timelines are pinned at that phase (the frame loop holds them frozen), so the
|
|
burst and floating `+points` render identically every run. A larger
|
|
`M3TE_ANIM_TIME` lands past the timeline, capturing the settled board with the
|
|
FX fully pruned. Startup-only and guarded by the var, so normal play is
|
|
untouched.
|
|
|
|
The legal-swap order is the fixed enumeration in `tests/expected/swap_legality.stdout`
|
|
(row-major, right-before-down). For seed 1337, `M3TE_FX=3` is the vertical red
|
|
3-match used by the golden.
|
|
|
|
```bash
|
|
# Punchy match burst + "+30" popup, pinned mid-clear: goldens/p6_fx_match.png
|
|
env SIMCTL_CHILD_M3TE_FX=3 SIMCTL_CHILD_M3TE_ANIM_TIME=0.22 \
|
|
xcrun simctl launch booted co.swipelab.m3te
|
|
# Same match, later phase — FX fully gone over the settled board (no golden):
|
|
env SIMCTL_CHILD_M3TE_FX=3 SIMCTL_CHILD_M3TE_ANIM_TIME=2.0 \
|
|
xcrun simctl launch booted co.swipelab.m3te
|
|
```
|