- Shell 48.5%
- Python 22.6%
- HTML 14.7%
- C 14.2%
| .vscode | ||
| assets | ||
| goldens | ||
| src | ||
| store/appstore | ||
| tests | ||
| tools | ||
| .gitignore | ||
| build.sx | ||
| main.sx | ||
| README.md | ||
| shell.html | ||
m3te
A candy match-3 game written entirely in sx, running on iOS, Android, macOS, and web (wasm). Game logic, rendering, input, UI, audio wiring, and animation are all sx; the only non-sx surface is FFI to system frameworks (UIKit/Metal/AudioToolbox on iOS, AAudio on Android, SDL3 on macOS/wasm).
Play: swipe adjacent gems, clear matches, cascade for combos, beat the score goal before moves run out. Ten hand-tuned levels with unlock progression and a best-run high score.
Requires the sx compiler on PATH, or set SX to the binary
(e.g. export SX=/path/to/sx). Run everything from the repo root.
Gate
The gate commands verify cross-target compilation and pure-sx test suite:
export PATH="$HOME/projects/sx/zig-out/bin:$PATH"
sx build --target ios-sim main.sx
bash tools/run_tests.sh
sx build main.sx
sx build --target aarch64-linux-android26 main.sx
sx build --target wasm main.sx
Logic tests: any tests/<name>.sx with a tests/expected/<name>.exit marker.
Regenerate after intentional changes: bash tools/run_tests.sh --update.
Build & run
iOS Simulator
sx build --target ios-sim main.sx
udid=$(xcrun simctl list devices booted | grep -Eo '[0-9A-Fa-f-]{36}' | head -1)
xcrun simctl install "$udid" sx-out/ios/M3te.app
xcrun simctl launch --terminate-running-process "$udid" co.swipelab.m3te
sleep 2
xcrun simctl io "$udid" screenshot /tmp/m3te.png
If more than one simulator is Booted, pass $udid explicitly (booted is
ambiguous). Always wait ~2s after launch before screenshotting to bypass the white launch screen. Goldens compare the board + HUD region, not the status strip (which differs per grab).
Android (API 26+ — AAudio)
sx build --target aarch64-linux-android26 main.sx
adb install -r sx-out/android/m3te.apk
adb shell monkey -p co.swipelab.m3te -c android.intent.category.LAUNCHER 1
adb exec-out screencap -p > /tmp/m3te_android.png
The aarch64-linux-android26 target is required for libaaudio linking. Cue
logs: adb logcat -d -s sx.
macOS
sx build main.sx
./sx-out/macos/M3te # run from repo root so assets/ resolves
Web (wasm)
sx build --target wasm main.sx
(cd sx-out/wasm32 && python3 -m http.server 8000) # open localhost:8000
Audio starts suspended (browser autoplay) and resumes on the first tap.
Also builds for 64-bit web target via sx build --target wasm64-unknown-emscripten main.sx.
Architecture & View Layer
main.sx initializes UiRoot and UIRenderer runtimes, passing input events into the compose Router.
- Compose View Layer: All UI screens, panels, buttons, chips, and banners build as compose trees (
Viewhierarchy). - Candy Components (
src/views/candy.sx): Design system providing glossy candy fills, bevel lips, top sheens, bright rims, shadowed labels, and custom layout frames. - Compose Router: Input taps route through
tappablecompose components and event handlers. - App Compositor (
src/views/app_view.sx): Eased lock-step sliding page transition (AppSlide) betweenmenu.sxandboard_view.sxwith depth parallax background. - Custom Rendering: Custom rendering is reserved strictly for grid cell tiles/gems (
BoardGridView), transient match particle FX (FxOverlay), and background ambient decorations (orbs/sparkles).
Layout
| Path | Role |
|---|---|
main.sx |
Entry, frame loop, GPU setup, UiRoot + UIRenderer runtime, M3TE_* capture hooks |
build.sx |
Per-target build / bundle config |
src/game/ |
Headless model: board, layout, anim, FX, levels, session, swipe |
src/views/ |
Compose view layer: candy.sx, menu.sx, board_view.sx, app_view.sx |
src/audio/ |
SFX + BGM mixer; iOS / AAudio / SDL backends |
src/save/ |
{unlocked, best, sfx, music} — file / localStorage |
tests/ |
Pure-sx snapshots (tools/run_tests.sh) |
goldens/ |
Reference screenshots (not pixel-exact) |
tools/regen_goldens.sh |
Regenerate all goldens via capture hooks (needs a booted sim) |
Capture hooks
Startup-only env vars (normal play is untouched). On simctl launch, prefix
with SIMCTL_CHILD_ and pass --terminate-running-process so a fresh process
re-reads the pins.
| var | effect |
|---|---|
M3TE_ANIM_TIME=t |
Freeze animation clock at phase t (0 = rest) |
M3TE_TRANS=t |
Mid-slide at progress t (menu to game) |
M3TE_SELECT=i |
Force-select cell i (row*8+col), with ts=0 (pinned to clock) |
M3TE_SELECT_LIVE=i |
Force-select cell i, back-dating since so ts=1.0 (simulates live path) |
M3TE_FX=n |
Commit n-th legal swap + FX/audio (3 = single match, 11 = depth-5 cascade on seed 1337) |
M3TE_BADSWAP=n |
Commit n-th illegal adjacent pair → bounce-back (41 on seed 1337) |
M3TE_TARGET=0 |
Force level won |
M3TE_MOVE_LIMIT=0 |
Force level lost |
M3TE_RESTART=1 |
Restart board after overrides |
M3TE_FPS=1 |
Show corner FPS overlay |
Example (resting board golden):
SIMCTL_CHILD_M3TE_ANIM_TIME=0 xcrun simctl launch --terminate-running-process \
"$udid" co.swipelab.m3te
sleep 2 && xcrun simctl io "$udid" screenshot goldens/p6_idle_t0.png
Full scenario list: bash tools/regen_goldens.sh. Cue order on device:
xcrun simctl spawn "$udid" log show --last 30s \
--predicate 'eventMessage CONTAINS "[sx] audio"' --style compact
On macOS, pass hooks in the shell env (M3TE_FX=11 ./sx-out/macos/M3te).
Assets
- SFX — curated Triple Treat pack clips (not synthesized): mono WAV
44100/Int16. Provenance per cue in
assets/audio/LICENSE.txt. Read-only pitch check:tools/measure_pitch.py. Do not regenerate with a synth script. - Music —
assets/audio/music.wav(CC0 “Happy” by Alex McCulloch). Toggle on the main menu with SFX. - Art — candy gems / background under
assets/. Refresh goldens viatools/regen_goldens.shafter art changes.
Conventions
- Everything in sx; FFI only for system frameworks.
- Capture hooks are startup-only and env-guarded.
- Goldens are visual references — compare board + HUD, not the status strip.