Commit Graph

99 Commits

Author SHA1 Message Date
swipelab
1a8360ec1d P15.1: add extended easing toolkit + determinism snapshot (sx)
Pure, headless easing curves of t in [0,1] for the organic-animation pass
(swap/fall/combine juice), placed alongside the existing ease_out_cubic /
ease_in_quad in board_anim.sx: ease_in_cubic, ease_in_out_cubic, ease_out_back
(bounded overshoot, settles to exactly 1), spring (damped wobble to exactly 1),
and squash_envelope (signed squash-&-stretch landing shape). The math module has
no exp/pow, so the decaying curves use a (1-t)^n polynomial envelope that hits 0
at t==1, pinning f(1) precisely.

Additive only: no render code calls the new curves yet. tests/easing.sx locks,
per curve, the endpoints, overshoot/undershoot bounds, and monotonicity-where-
required (booleans only, so the snapshot is platform-stable), structured so P16.2
can append illegal-swap bounce-back assertions. Test count 21 -> 22.
2026-06-06 10:16:02 +03:00
swipelab
a62ddcf0b9 Merge branch 'flow/m3te/P14.1' into m3te-plan 2026-06-06 09:39:15 +03:00
swipelab
dbaebb779f P14.1: report sx std-library gaps surfaced by m3te 2026-06-06 09:34:42 +03:00
swipelab
f2a9579106 Merge branch 'flow/m3te/P13.1' into m3te-plan 2026-06-06 09:28:41 +03:00
swipelab
d043319d00 P13.1: fix F2 — retire stale procedural synth, doc the real Triple Treat DSP path
The README audio-regeneration section called tools/synth_audio.py the
'build-time DSP path', but that script is the original P10.1 procedural
synthesizer: its main() overwrites assets/audio/*.wav with note-frequency
synthetic audio. Following the documented path would clobber the curated
Triple Treat pack cues. Remove the script (cleanest resolution — kills the
clobber hazard) and rewrite the README to describe the actual production:
real pack clips (per-cue source in assets/audio/LICENSE.txt) down-mixed,
trimmed, faded, peak-normalized to ~-15 dBFS, re-wrapped via afconvert;
combo ladder = real Match FX ordered by brightness (P10.9); cascade one cue
per round (P10.10). measure_pitch.py kept (read-only verification, never
writes a WAV). WAVs/.sx/goldens byte-unchanged.
2026-06-06 09:23:20 +03:00
swipelab
5fa0a95cb4 P13.1: fix README cue-capture recipe — terminate before each env-pinned relaunch
The M3TE_* capture pins are read only at app startup, so a second `simctl
launch` against the still-running app reused its PID and silently ignored the
new pin (the win->lose cue recipe only ever produced `cue win`). Add
`--terminate-running-process` to every pinned launch across all capture recipes
and document the startup-only rule explicitly. Docs-only; no .sx change.
2026-06-06 09:12:10 +03:00
swipelab
2f5d60b9e1 P13.1: final vibe integration — playthrough validation, full golden sweep, finalized docs
Integration-only (no logic changes). Validated the full candy vibe in the booted
iPhone sim and brought every artifact in line with the shipped candy palette +
Triple Treat SFX bank.

Goldens — swept all 23:
- Refreshed 15 that predated the candy palette (P12): p6_idle_t0, p6_idle_mid,
  p6_select, p7_restart, p5_swap_before/after, p6_anim_swap/clear/fall/after,
  p6_fx, p6_fx_after, p6_fx_match, p6_inputlock_board, p11_combo_deep — re-captured
  via the documented M3TE_* hooks.
- Left 5 unchanged (board+HUD region byte-identical to the current build, verified
  by cropped-region hash): p4_board, p4_hud, p9_polish, p7_win, p7_lose.
- Removed 3 obsolete pre-board orange-quad goldens (app no longer renders them):
  p0_quad, p0_input_before, p0_input_after.

Docs — README.md:
- Section 2 now describes the candy board (not the old orange quad) and points at
  goldens/p6_idle_t0.png; dropped the removed p0_* references.
- Added the final audio model: Triple Treat SFX provenance + per-cue mapping, the
  per-round ascending cascade (one combo cue per round, clamped at combo5), the
  WAVE/mono/44100/Int16 @ -15 dBFS format spec, and the cue-log capture commands.
- Added image-art asset regeneration (codex imagegen via codex exec + sips
  normalize to exact per-asset dims/format).

Gate: ios-sim build links (exit 0); 21/21 pure-sx logic tests pass. Playthrough
evidence (cue NSLog ascending combo1..combo5 + win/lose stingers, screenshots)
captured in the worker report.
2026-06-06 08:57:26 +03:00
swipelab
211edbee76 Merge branch 'flow/m3te/P10.10' into m3te-plan 2026-06-06 08:41:27 +03:00
swipelab
51b3397ade P10.10: play one ascending combo cue per cascade round
For combos, play a sound for each match ascending (Candy-Crush cascade run):
as a chain resolves, EACH successive round plays the next higher cue
(combo1, combo2, … clamped at combo5) instead of a single combo cue keyed
to the final cascade depth at commit.

- board_anim.sx: add `BoardAnim.cascade_fired` (edge-trigger high-water mark,
  reset on init/begin) and the pure `cascade_rounds_started(elapsed, n)` helper
  — how many rounds have begun clearing on the swap→(clear,fall)* timeline.
- main.sx: in the frame loop, diff `cascade_rounds_started` against
  `cascade_fired` and play one ascending cue per newly-cleared round, once each,
  gated on a real multi-round chain (rounds >= 2). Additive; never touches
  board/score/move state.
- board_view.sx: drop the single `sfx_cascade(final depth)` at commit; keep
  `sfx_swap` / `sfx_match` (and win/lose) exactly as before.
- tests/cascade_rounds.sx: headless snapshot of the per-round timing + the
  ascending combo1..combo5 run with the combo5 clamp.

Sim (M3TE_FX=11, depth-5): log show shows combo1→combo2→combo3→combo4→combo5
at successive timestamps ~0.36s apart (= CLEAR+FALL per-round spacing).
2026-06-06 08:37:46 +03:00
swipelab
704ae08011 Merge branch 'flow/m3te/P10.9' into m3te-plan 2026-06-06 08:25:16 +03:00
swipelab
c3c5467723 P10.9: use real Match FX cues for the cascade combos
Replace the pitch-laddered single-pop combo ladder with FIVE distinct REAL
Match FX cues from the user's Triple Treat SFX pack (Match SFX/), ordered to
convey cascade escalation by spectral brightness / high-frequency energy
(combo1 dullest -> combo5 sparkliest):

  combo1 <- Match SFX/Match FX 2-RCM.wav  (dark/full,   centroid ~1.7 kHz)
  combo2 <- Match SFX/Match FX 4-RCM.wav  (warm mid,    centroid ~2.1 kHz)
  combo3 <- Match SFX/Match FX 6-RCM.wav  (bright mid,  centroid ~3.2 kHz)
  combo4 <- Match SFX/Match FX 7-RCM.wav  (rich+bright, centroid ~4.7 kHz)
  combo5 <- Match SFX/Match FX 3-RCM.wav  (sparkly,     centroid ~6.8 kHz)

The Match FX set does not cleanly pitch-ascend, so brightness is the ordering
signal; spectral centroid ascends monotonically 1.68 < 2.09 < 3.18 < 4.70 <
6.77 kHz. Each down-mixed to mono, trimmed to a 0.50 s onset window, eased in
(~6 ms) and rounded out with a 150 ms cosine fade-out, peak-normalized -15 dBFS.

Pure drop-in: audio.sx and all wiring are untouched; swap/match/win/lose are
byte-identical to P10.8. The depth->cue-index mapping (cascade_cue, fx_combo)
locks the integer mapping, not the audio content, so both tests stay valid.
LICENSE.txt combo provenance updated to the real Match FX sources.
2026-06-06 08:21:16 +03:00
swipelab
274d726002 Merge branch 'flow/m3te/P10.8' into m3te-plan 2026-06-06 07:55:00 +03:00
swipelab
28e32435f9 P10.8: rebuild the SFX bank from the user's Triple Treat SFX pack
Replace the 9-cue bank with best-fit selections from the user-provided
Triple_Treat_SFX.zip, converted to the engine format (mono / 44100 / Int16,
<= ~600 ms) and peak-normalized to a gentle, consistent -15 dBFS. Drop-in:
audio.sx and all wiring are untouched; only assets/audio/** changes.

Per-cue source within the pack:
  swap   <- Transition SFX/Swipe FX 1   (light swipe = the swap gesture)
  match  <- Pop:Bubble SFX/Pop FX 5     (juicy candy pop, first clear)
  combo1..5 <- Pop:Bubble SFX/Pop FX 3  (one pop pitch-laddered +0/+2/+4/+7/+9
               semitones; the pack's Match set does not ascend monotonically)
  win    <- Success:Power-Up SFX/Power Up FX 1 (short triumphant)
  lose   <- Fail SFX/Fail FX 2          (gentle tonal, not boomy)

combo1..5 ascend in fundamental: 687 < 771 < 865 < 1029 < 1155 Hz. The 30 MB
pack and its .meta/__MACOSX cruft are not committed; LICENSE.txt records the
exact per-cue source file within the pack.
2026-06-06 07:51:36 +03:00
swipelab
eca994f454 Merge branch 'flow/m3te/P10.7' into m3te-plan 2026-06-06 06:55:54 +03:00
swipelab
e4a502e922 P10.7: soften the SFX bank (gentler / less aggressive)
DSP-soften the existing P10.6 CC0 bank in place — same filenames, same
canonical WAVE/mono/44100/Int16 format, drop-in (engine untouched):

- Onset eased in: short qsin fade-in (8-14 ms) tames the attack transient
  so pops bloop instead of snap (onset peak in first 20 ms down 5.6-8.7 dB
  per cue; combo attack-to-peak 1.4-2.4 ms -> 15-26 ms).
- Highs rolled off: warm two-pole low-pass (2.6 kHz swap/lose, 3.0 kHz
  match/combo, 3.6 kHz win) for a rounded tone. Spectral centroid down
  ~40-60%; >4 kHz energy collapses (win 76%->28%, combos ~10%->0.5%).
- Quieter: re-normalized to -15.5 dBFS (swap/lose -17.5), down from ~-9/-12,
  lowering both peak and RMS on every cue.

Candy character retained; cascade ladder preserved (combo1..5 fundamentals
still ascend 1045<1173<1317<1566<1758 Hz). LICENSE provenance updated.
2026-06-06 06:52:51 +03:00
swipelab
d4de4f2b8e Merge branch 'flow/m3te/P10.6' into m3te-plan 2026-06-05 23:24:29 +03:00
swipelab
f3e3876574 P10.6: tune SFX bank to the candy character (real CC0 marimba/glock + pops)
Refine the same drop-in bank toward Candy-Crush candy/jelly character; engine
(audio.sx) untouched, same filenames/format.

- swap: freesound CC0 "Pop 01" (LilMati #506546) — soft light blip.
- match: freesound CC0 "Cartoon Pop" (Mish7913 #741368) — bright juicy candy pop.
- combo1..5: VCSL CC0 Marimba F5, real-resample pitch-laddered to a C-major
  pentatonic (C5 D5 E5 G5 A5) — warm/sweet mallet sparkle, not glassy; ascending.
- win: VCSL CC0 Glockenspiel C5/G5/C6 sequenced into a sugary ascending bell
  fanfare.
- lose: VCSL CC0 Marimba B4->G4 descending — soft gentle "aww".

All real free CC0 assets (NOT ripped from Candy Crush); provenance + license per
cue in LICENSE.txt. Mono/44100/Int16, <=580 ms, peak <=-9 dBFS (lose -12).
Combos ascend (1045<1173<1317<1566<1758 Hz). Gate green; all 9 cues load on sim.
2026-06-05 23:20:50 +03:00
swipelab
61fbfb5d79 Merge branch 'flow/m3te/P10.5' into m3te-plan 2026-06-05 22:47:06 +03:00
swipelab
ae44e5b7fb P10.5: replace synth SFX bank with real CC0 Kenney assets (quieter, cleaner)
QA: "the sfx is too loud and scratchy." Drop-in asset swap — no engine change.

Replace the 9 synthesized cues (swap, match, combo1..combo5, win, lose) with
real free-licensed (CC0 1.0) sound effects from Kenney's Interface Sounds /
Digital Audio packs:
  swap  <- pluck_002        match <- confirmation_002
  win   <- powerUp7         lose  <- minimize_006
  combo1..5 <- glass_001 pitch-laddered up a pentatonic run (0,+2,+4,+7,+9 st)
              via resample DSP -> ascending fundamentals 1918..3226 Hz.

Every cue: mono / 44100 Hz / Int16 PCM, <=0.54 s. Whole bank peak-normalized to
-9 dBFS, so peak AND RMS are well below the old synth (old peak -0.7..-2.9 dB,
mean -5.8..-9.3 dB; new peak -9.0 dB, mean -22..-30 dB). Audibly cleaner — no
synth harmonics.

LICENSE.txt rewritten with real per-file provenance (source URL + CC0). clear.wav
(already a CC0 Kenney clip, not loaded by the engine) left unchanged. Engine
(audio.sx/board_view.sx/main.sx) untouched; sim build loads all 9 cues, zero
"load failed".
2026-06-05 22:41:52 +03:00
swipelab
c5c18ef62e Merge branch 'flow/m3te/P12.3' into m3te-plan 2026-06-05 22:07:44 +03:00
swipelab
5e78d25d8b P12.3: candy clear colour for palette cohesion (sx / iOS)
Retune the GPU clear colour on both render paths (Metal on iOS, GL on
desktop) from the old dark navy (0.05,0.06,0.10) to a candy-lavender tone
sampled from the regenerated background's mid-gradient. The dark navy was
the one off-palette constant left after P12.1/P12.2; the background art
covers the full drawable in steady state, so it only ever surfaced as a
pre-load flash / potential dark seam. A single shared CLEAR_R/G/B keeps the
two paths from diverging.

Refresh the stale full-board goldens (p4_board, p9_polish), which still
showed the pre-P12.2 dark HUD, to the current cohesive candy palette.
2026-06-05 22:05:01 +03:00
swipelab
9a7a2003ef Merge branch 'flow/m3te/P12.2' into m3te-plan 2026-06-05 21:55:45 +03:00
swipelab
7d18ba7e4d P12.2: candy HUD & win/lose banner restyle (sx / iOS)
Restyle the code-drawn UI toward the candy look — colours, corner-rounding
and glossy feel only; no rect geometry moves.

- HUD: grape candy card with a glossy top sheen, a bright rounded rim and
  warm cream text on a soft purple shadow (was a flat dark translucent panel).
- Banner panel: grape candy fill under a sheen + bright rim, rounder corners.
- Titles: celebratory candy gold YOU WIN! / punchy coral OUT OF MOVES, each
  on a tinted drop shadow for pop.
- PLAY AGAIN: bubblegum candy button with a glossy sheen, bright rim and a
  darker bevel lip for a 3D candy edge.

BannerLayout rects (panel/title/button) and the restart hit-test are
untouched, so tests/banner_layout still passes. Refresh the p4_hud / p7_win /
p7_lose goldens.
2026-06-05 21:52:00 +03:00
swipelab
246dcfa224 Merge branch 'flow/m3te/P12.1' into m3te-plan 2026-06-05 21:44:48 +03:00
swipelab
5695974283 P12.1: brighter candy background & cell tile (sx / iOS)
Regenerate the board art in a Candy-Crush palette via real image
generation (codex imagegen tool), keeping each PNG's exact dims and
source format so no code/layout changes are needed:

- background.png: 863x1822 opaque RGB — bright bubblegum-pink ->
  lavender -> sky-blue candy gradient with soft bokeh sparkles
  (was the dark purple/indigo/teal gradient).
- cell.png: 128x128 RGBA — light glossy frosted candy tile with a
  warm pink/lavender tint, keeping it light so the 6 gems stay legible.

Generated at imagegen-native sizes, then center-cropped (background, to
preserve aspect) and resized with sips to the exact target dims/format.
Refresh the resting-board goldens (p9_polish, p4_board) captured at
M3TE_ANIM_TIME=0 to show the new palette with all 6 gems legible.
2026-06-05 21:34:45 +03:00
swipelab
8bbb060a67 Merge branch 'flow/m3te/P11.3' into m3te-plan 2026-06-05 21:19:51 +03:00
swipelab
cd8667d170 P11.3: glossier candy gem & selection feel (sx / iOS)
Make the selection highlight read as glossy candy without new art and
without disturbing the idle-rest invariant. board_view's render_selection
layers a soft outward glow (two concentric stroked rings — the renderer has
no blur), a warm wash, a bright rim doubled by a thin inner highlight for a
glassy edge, and a wet sheen that rides the selected gem's live pose. Every
layer is a rect/overlay (issue 0002 forbids a draw-time gem-texture tint).

The gloss is selection-only: render_selection runs solely when a cell is
selected, so the resting board (no selection) is byte-identical to before
and the t==0 idle pose stays exactly the static sprite (locked by
tests/gem_pose). The selection-pop motion still comes from gem_anim; no
board / score / move state changes and input stays gated by BoardAnim.active.

Updated goldens/p6_select.png; README documents the P11.3 selection gloss
and its reproduce commands (reusing the P6.3 M3TE_SELECT + M3TE_ANIM_TIME
hooks).
2026-06-05 21:15:44 +03:00
swipelab
a8629c378b Merge branch 'flow/m3te/P11.2' into m3te-plan 2026-06-05 20:55:37 +03:00
swipelab
0b293a2c48 P11.2: escalating combo emphasis tied to cascade depth (sx / iOS)
Scale the combo FX with cascade depth (mv.rounds.len) — the same depth the
cascade SFX (play_cascade) steps up on — so deeper cascades read as more
exciting and land in lockstep with the audio escalation. Purely visual and
self-pruning: no board / score / move state changes, and input stays gated by
BoardAnim.active alone.

- board_fx.sx: add fx_combo_level (mirrors audio's cascade_cue_index clamp:
  depth<=1 -> floor, depth>=5 -> ceiling). The +points popup now carries the
  cascade depth and grows one font step + lerps gold -> hot-gold per level
  (fx_popup_font / fx_popup_color). Every burst of a deep cascade gets a
  whole-move depth boost (FX_BURST_DEPTH) on top of the existing per-round bump.
- board_view.sx: render_fx_popups derives styling from depth and tops a combo
  with a "COMBO xN" label naming the true cascade depth.
- tests/fx_combo.sx: headless snapshot locking the depth->level/font table and
  asserting fx_combo_level matches the cascade-cue index column entry-for-entry.
- goldens/p11_combo_deep.png + README: deterministic depth-5 capture (M3TE_FX=11)
  vs the depth-1 single clear (M3TE_FX=3); FX gone after settle at a later phase.
2026-06-05 20:51:56 +03:00
swipelab
b68b60a537 Merge branch 'flow/m3te/P11.1' into m3te-plan 2026-06-05 20:40:50 +03:00
swipelab
b65e592a8c P11.1: juicier match pops & brighter bursts (sx / iOS)
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).
2026-06-05 20:36:25 +03:00
swipelab
f8268a6171 Merge branch 'flow/m3te/P10.4' into m3te-plan 2026-06-05 20:08:43 +03:00
swipelab
c35c63d8a9 P10.4: snapshot-test cascade-cue depth->index mapping (sx)
Add tests/cascade_cue.sx, a headless logic test that reuses audio.sx's
pure cascade_cue_index / cascade_cue_name (the exact functions
play_cascade calls) to lock the cascade-depth -> combo-cue clamp:
depth <= 1 -> combo1, depth >= COMBO_CLIPS -> combo5, monotonic between.
Covers the escalation logic the audio playback path can't gate-cover,
with no audio. Committed stdout/exit snapshot; suite is now 19 tests.
2026-06-05 20:06:21 +03:00
swipelab
672ce63628 Merge branch 'flow/m3te/P10.3' into m3te-plan 2026-06-05 20:03:19 +03:00
swipelab
51fdb75d35 P10.3: wire the SFX bank to game events (sx / iOS)
Drive event-appropriate cues from the existing System Sound Services bank,
purely additively — no board/score/move state is read or written and every
call stays inline-if-OS==.ios guarded.

board_view.sx (move-commit path): a committed gesture now plays the swap
slide cue for any swipe intent (legal or the reverted ping-back); a legal
move adds the match pop on its first clearing round; a multi-round chain
adds the escalating cascade cue keyed to the recorded AnimMove depth
(mv.rounds.len), kept distinct from the match pop so a single clear is never
doubled. An illegal swap plays only the swap cue.

main.sx (frame loop): the win/lose stinger fires EXACTLY ONCE, edge-triggered
on the frame the banner comes up — the level has settled won/lost and any
in-flight cascade has finished animating. Status is read-only from the model;
a restart re-arms the edge for a fresh win/lose.

audio.sx: each play_* method logs a per-cue NSLog line at play time so the
ordering is observable via `log show`; cascade_cue_name maps the clamped
combo index to a stable literal (literals only — the string→NSString bridge
needs NUL-terminated bytes).
2026-06-05 19:59:45 +03:00
swipelab
ee6073f8dd Merge branch 'flow/m3te/P10.2' into m3te-plan 2026-06-05 19:42:06 +03:00
swipelab
59218731f1 P10.2: SFX bank over System Sound Services (sx / iOS)
Refactor GameAudio from the single clear cue into a full bank: load swap,
match, combo1..combo5, win, lose once at startup, each into its own
SystemSoundID via load_system_sound. Expose per-event play methods
(play_swap/play_match/play_cascade/play_win/play_lose), each a single
AudioServicesPlaySystemSound. play_cascade selects the ascending clip by
clamping cascade depth through the pure, OS-agnostic cascade_cue_index
(depth<=1 -> combo1, depth>=5 -> combo5) so P10.4 can snapshot it headlessly.

Layer stays purely additive and inline-if-OS==.ios guarded; the clear trigger
is migrated to sfx_cascade(mv.rounds.len) at its board_view call site, with
sfx_swap/match/win/lose shims staged for P10.3 wiring. No framework added
beyond the already-linked AudioToolbox/CoreFoundation (build.sx untouched).
2026-06-05 19:38:26 +03:00
swipelab
45e3eec622 Merge branch 'flow/m3te/P10.1' into m3te-plan 2026-06-05 19:32:00 +03:00
swipelab
7f23bc8b19 P10.1: candy-vibe higher-pitched SFX bank (sx / iOS assets)
Produce the first deliverable of the Candy-Crush vibe pass: a bank of bright,
glossy, higher-pitched SFX WAVs under assets/audio/, all in the exact canonical
iOS System-Sound format clear.wav uses (mono, 44100 Hz, signed-16-bit PCM).

Bank: swap, match, combo1..combo5 (ascending pentatonic run C6 D6 E6 G6 A6),
win (ascending arpeggio), lose (descending stinger). Every cue sits above
clear.wav's ~784 Hz fundamental; combo1<..<combo5 step strictly upward
(1047<1175<1319<1568<1760 Hz). Each loads via AudioServicesCreateSystemSoundID
with status 0.

Synthesized by the build-time tools/synth_audio.py (pure-Python additive
synthesis; the app never runs it) and converted with afconvert. Pitch verified
with tools/measure_pitch.py. Provenance (CC0) recorded in LICENSE.txt. No sx
code changes — engine wiring is P10.2/P10.3.
2026-06-05 19:28:11 +03:00
swipelab
0d9ee13984 Merge branch 'flow/m3te/P9.1' into m3te-plan 2026-06-05 19:04:35 +03:00
swipelab
219dd127dd P9.1: visual polish — frame the board off the screen bezel (sx, iOS sim)
Polish pass before final acceptance. The 8x8 grid was rendering flush to the
left/right screen edges (gems ~4pt from the bezel on iPhone 17). Add a content
margin (BOARD_INSET_X = 16pt) layered on top of the platform safe-area insets so
the grid is framed by the background, while the HUD keeps using the bare safe
insets so it still hugs the top below the Dynamic Island. The grid is
width-constrained in portrait, so this inset is what sizes it; vertical
centering inside the safe area is unchanged, and the win/lose banner (derived
from the grid) stays centered over the framed board.

Safe-area verified on a current iPhone simulator (iPhone 17, iOS 26): HUD below
the Dynamic Island, board far above the home indicator, forced win/lose banners
centered and unclipped.

The headless geometry tests (hit_test, banner_layout) call compute() with a
zero inset directly, so they are unaffected; full logic gate stays green (18/18).

Goldens: add p9_polish.png (resting board, M3TE_ANIM_TIME=0) as the canonical
polished layout. Re-capture the README-documented deterministic goldens whose
board position shifts by the 16pt margin (p4_board, p4_hud, p6_idle_t0,
p6_idle_mid, p6_select, p7_win, p7_lose, p7_restart). The in-flight move-timeline
goldens (p5_swap_*, p6_anim_*, p6_fx_*, p6_inputlock_board) and the p0 quad
goldens are not reproducible via the documented env pins (which pin only the idle
clock + level state), so they are left as-is.
2026-06-05 19:01:09 +03:00
swipelab
20d7c2e7f8 Merge branch 'flow/m3te/P8.1' into m3te-plan 2026-06-05 18:31:27 +03:00
swipelab
f0a13293bb P8.1: minimal match/clear SFX via iOS System Sound Services (sx FFI)
Feasibility spike outcome: iOS audio from sx is feasible with no sx-library
change. System Sound Services is plain C, reached with the same `#foreign`
FFI uikit.sx already uses (UIApplicationMain / dlsym / CACurrentMediaTime);
AudioToolbox + CoreFoundation are linked per-target in build.sx.

Smallest viable SFX: one short CC0 clip (Kenney Interface Sounds, CC0 1.0)
played when a swap clears a match. Purely additive — audio.sx reads/writes
no score/board/move state; the wiring in board_view only adds a call.

- audio.sx: load clear.wav once, AudioServicesPlaySystemSound on clear
- board_view.sx: trigger sfx_clear() on a legal swap that clears (>=1 round)
- main.sx: allocate + init g_audio at boot
- build.sx: link AudioToolbox + CoreFoundation on iOS
- assets/audio/clear.wav (+ one-line CC0 credit in LICENSE.txt)

Verified: ios-sim build links; 18/18 tests pass; sim boot log shows
"[sx] audio: clear cue loaded" (AudioServicesCreateSystemSoundID succeeded,
asset shipped in the bundle and decoded).
2026-06-05 18:19:33 +03:00
swipelab
0a3cd1561b Merge branch 'flow/m3te/P7.2' into m3te-plan 2026-06-05 15:25:58 +03:00
swipelab
0f84b09f7b P7.2 fix: reset per-gem landing state on restart
The restart button (BoardView.do_restart) reseeded the model and dropped
selection/drag/anim/FX, but left GemMotion.land_at carrying the prior move's
landing stamps. A restart fired right after a terminal cascade therefore
replayed that move's squash-bounce on the freshly seeded board instead of
showing a clean resting pose.

Factor the landing reset into GemMotion.reset_landings (init now delegates to
it) and call it from do_restart, so a restart returns every cell to its
resting idle pose. The idle clock keeps running, so the always-on idle simply
resumes from rest.

Regression: tests/gem_pose.sx section 7 stamps a cell mid-squash, asserts it
is squashing, then asserts reset_landings returns every cell to rest while
leaving the clock untouched. Fails on the pre-fix (no-op reset) behaviour,
passes after. Gate green: ios-sim build + 18/18 logic tests.
2026-06-05 15:17:37 +03:00
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
swipelab
bf38c7a100 Merge branch 'flow/m3te/P7.1' into m3te-plan 2026-06-05 08:42:47 +03:00
swipelab
e77c470546 P7.1: freeze finished level — reject moves after won/lost
play_turn now checks level_status before committing: a won or lost
level rejects the swap (accepted=false) with no move spent and no
score change, until restart returns it to in_progress. Adds an
accepted flag to TurnResult so the renderer can show the move was
ignored. Regression in tests/level.sx asserts post-won and post-lost
play_turn leaves score/moves/status unchanged and that restart
re-enables play.
2026-06-05 08:37:28 +03:00
swipelab
a40a994ae1 P7.1: turn / goal state machine (pure sx)
Add a thin, deterministic level loop over the headless model in board.sx:

- target_score: per-level score goal (DEFAULT_TARGET_SCORE=1500), seeded by init.
- Status (in_progress/won/lost) derived purely from score/target/move budget via
  level_status; won is checked before lost so meeting the goal on the final move
  wins. status_name for the HUD/snapshots.
- has_legal_swap: allocation-free deadlock probe (first legal pair short-circuits).
- reshuffle: Fisher-Yates over existing gems via the board's seeded RNG until the
  arrangement has no immediate match and at least one legal move; consumes no
  move, terminates via MAX_RESHUFFLE_TRIES.
- restart: reseed a fresh, reproducible level (resets cells/score/moves/goal).
- play_turn / TurnResult: commit_swap then reshuffle on deadlock while in
  progress, reporting the resulting status.

tests/level.sx (golden tests/expected/level.{stdout,exit}) asserts: start
in_progress; one legal swap crossing a low goal -> won transition; budget
exhausted below an unreachable goal -> lost transition; a provably deadlocked
diagonal Latin-square board ((col-row) mod 6) reshuffles to >=1 legal move with
no immediate match and no move spent; restart resets progress and a fixed seed
reproduces the same starting board.

Gate: sx build --target ios-sim main.sx (exit 0); bash tools/run_tests.sh
(17 passed, 0 failed).
2026-06-05 08:25:08 +03:00
swipelab
7e82c34a1f Merge branch 'flow/m3te/P6.3' into m3te-plan 2026-06-05 08:11:33 +03:00