Commit Graph

19 Commits

Author SHA1 Message Date
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
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
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
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
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
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
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
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
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
d35fa8a5a6 P6.3: per-gem idle/select/land/clear animations (sx, iOS sim)
New gem_anim.sx adds a purely-visual per-gem pose set driven by a single
animation clock: a calm always-on idle breath (scale-pulse + bob, per-gem
phase, ramped in from rest), a selection pop, a landing squash-bounce, and
a clear pop. BoardView draws every settled gem through gem_pose_at /
gem_pose_frame; the move timeline (P6.1) and FX (P6.2) are untouched and the
input-lock semantics are unchanged (idle never locks input).

Determinism: the idle is always-on, so main reads M3TE_ANIM_TIME=<seconds>
to freeze the clock at a chosen phase (t==0 == the resting board, so the
pre-P6.3 goldens reproduce) and M3TE_SELECT=<cellIndex> to force a selection
for capture. tests/gem_pose.sx locks the t==0-rest invariant and the reaction
envelopes headlessly (fails if the idle ramp is dropped).

Goldens (deterministic capture): p6_idle_t0 (resting), p6_idle_mid (pinned
mid-breath), p6_select (selection pop on cell 3,3). Purely visual: no change
to model/score/moves/hit-testing.
2026-06-05 07:59:16 +03:00
swipelab
c2548aa854 P6.2: score popups & match FX (sx, iOS sim)
Add a purely-visual, transient juice layer over a committed move — score
popups + a tinted particle/flash burst at the clears — with no change to the
model, score, moves, or settled board.

- assets/fx/particle.png: key the painted transparency checkerboard out of the
  provided particle art to real alpha (8-connected border flood fill +
  smooth luminance falloff that preserves the soft glow), downscaled to a
  256x256 RGBA white sparkle. tools/key_particle.sx is the reproducible tool.
- board_fx.sx: BoardFx (live particle bursts + one "+points" popup) and
  BoardFxAssets. The engine image path samples texture*white (no draw-time
  tint), so the white sprite is tinted per gem colour at LOAD time into one
  texture per colour; a burst animates by scale (grow -> shrink) and the soft
  texture edges carry the fade. Combos (cascade depth > 1) burst bigger and the
  popup is larger + gold. All driven by delta_time and self-pruning.
- board_anim.sx: AnimMove carries the model's cascade.awarded so the popup
  shows the real payout without re-deriving any scoring in the view.
- board_view.sx / main.sx: wire BoardFx + the tinted assets, tick each frame,
  spawn on a legal commit, and render bursts (clipped to the grid) under the
  popups (drawn on top). Input-lock (BoardAnim.active) is untouched; FX never
  gate input and may outlast the move slightly before vanishing.

Goldens (iPhone-17-class sim, iOS 26): p6_fx.png (combo: gold "+480" + bursts
mid-cascade), p6_fx_match.png (single match: "+30" + red burst), p6_fx_after.png
(settled board, FX fully gone). Gate: ios-sim build links, 15/15 logic tests
green (scoring/cascade goldens unchanged).
2026-06-05 02:18:55 +03:00
swipelab
5ec7247001 P6.1: lock input for the full in-flight animation window
A swipe that began while a move animation was playing could still commit:
mouse_down latched the drag unconditionally and the animation-active check
sat at mouse_up, so a press made mid-animation committed once the timeline
finished before release — against a board mid-transition.

Gate input at gesture START instead. Add a pure `accepts_input(anim)`
predicate (false while a timeline is active) and check it at mouse_down: a
press begun mid-animation is dropped and never latches a drag, so it cannot
commit when the animation later settles. The now-dead mouse_up gate is
removed. Animation visuals and the logical model are unchanged.

Extend tests/anim_plan.sx to assert accepts_input rejects for the whole
window (idle accept / busy reject / settled accept) and that press-gating
drops the exact failure gesture a release-gate would let through.
2026-06-05 01:23:12 +03:00
swipelab
0b858f7724 P6.1: swap/clear/fall move tweens (sx, iOS sim)
Add a purely-visual animation timeline so the board no longer snaps on a
move. board_anim.sx records, on a value-copy of the pre-move board, the
swap and each cascade round's matched cells + per-column fall provenance,
then BoardView plays it over delta_time: the two swapped gems SLIDE between
cells (and ping out-and-back on an illegal swap), matched gems SCALE OUT,
and survivors FALL into place while refills drop in from above the grid.

The model stays authoritative: plan_and_commit still calls commit_swap on
the real board exactly as before, and the recording replays the identical
primitives from the identical cells + RNG state, so the timeline ends ON
the model's settled board. tests/anim_plan.sx is the determinism guard —
it asserts the committed board, score, moves, and the timeline's final
state all equal an independent commit_swap of the same move, that the
rounds are contiguous, and that an illegal swap records nothing and leaves
the board untouched. All pre-existing logic/cascade goldens stay green.

Evidence (sx-test-metal, iOS 26.0, time-sampled with temporarily-lengthened
durations; committed durations are the short production values):
goldens/p6_anim_swap.png  gems sliding between (5,4)/(6,4)
goldens/p6_anim_clear.png matched reds scaling out in row 4
goldens/p6_anim_fall.png  gems mid-fall with gaps + refill dropping in
goldens/p6_anim_after.png settled board == model (SCORE 30, MOVES 29/30)
2026-06-05 01:06:02 +03:00
swipelab
e5df37523f P5.2: swipe commits legal swap / reverts illegal (sx, iOS sim)
Wire touch input into the model in BoardView.handle_event. A press records
the drag start (new DragInput, heap-allocated so it survives the per-frame
BoardView rebuild between mouse_down and mouse_up); the release resolves the
gesture against the same layout it was drawn with. A swipe — start→end mapped
by swipe_intent to an adjacent-swap intent — is fed straight into
commit_swap: a legal swap applies, cascades (clear→collapse→refill), accrues
score and spends a move; an illegal one reverts, no move. A sub-threshold /
off-board drag carries no intent and falls back to the tap behaviour
(toggle/clear selection). The next frame re-renders board + HUD from the model.

Reuses swipe.sx + board_layout.sx + commit_swap unchanged — this is wiring,
not new legality/cascade logic.

tests/swipe_commit.sx (new golden) drives the full path on the seeded board
(SEED 1337): a rightward swipe (0,0)->(1,0) is illegal (two reds) and reverts
byte-for-byte with no score/move; (5,4)->(6,4) is legal, completes R,R,R on
row 4, awards 30, spends one move.

Sim evidence (iPhone 17, iOS 26.0): goldens/p5_swap_before.png (SCORE 0,
MOVES 30/30) and goldens/p5_swap_after.png (SCORE 30, MOVES 29/30) bracket a
real idb-injected swipe at (276,475)->(327,475) pt = cell (5,4)->(6,4); the
three reds clear and the board matches the model's resolved state.
2026-06-05 00:32:40 +03:00
swipelab
9ed98c73d2 P4.4: selection highlight + score/moves HUD (sx, iOS sim)
Tap a gem to select it: BoardView hit-tests the touch to a grid cell and
draws a bright rim + translucent fill over it; tapping the same cell clears
the selection, tapping another moves it, tapping off-board clears it.
Selection only — no swap (that's P5). The HUD renders the live score and
remaining moves (out of the move limit) in the Lato font on a translucent
card above the grid.

The touch→cell geometry is factored into a pure BoardLayout (no GL/stb
imports) that BoardView composes and P5 will reuse for swap endpoints.
tests/hit_test.sx locks point_to_cell as the exact inverse of cell_frame
(every cell center round-trips; off-board taps reject) — headless because
BoardLayout pulls no C imports. goldens/p4_hud.png captures the scene after
a real idb tap at (201,437)pt: the HUD plus a yellow selection rim on the
red gem at cell (col 4, row 3).
2026-06-05 00:00:48 +03:00
swipelab
c5ed5cc4f7 P4.3: render seeded board with real gem sprites (sx, iOS sim)
Adopt the modules/ui UIPipeline framework (as the chess reference app does)
and replace the P0 placeholder quad with a BoardView (View protocol, modeled
on chess/board_view.sx):
- background.png fills the screen; an 8x8 cell.png grid is centered in the
  safe area; each cell's gem is sampled from gems.png by UV column = gem index
  (0=red .. 5=purple).
- Drive it from board.sx seeded with 1337 (the board_init golden's seed), so
  the on-screen layout matches that snapshot gem-for-gem.

main.sx now hosts the view via UIPipeline (Metal on iOS, GL on desktop) and
heap-allocates the board/asset state behind pointers (UFCS method calls on a
value-typed global mutate a copy, so mutable state must live behind a pointer
as the reference app does).

Vendor the C deps the UI module's image/font path needs (stb_image,
stb_truetype, kb_text_shape, file_utils); their #include "vendors/..." paths
resolve relative to the project root.

Evidence: ios-sim build links clean; tools/run_tests.sh 11/11 pass; running
app captured at goldens/p4_board.png.
2026-06-04 23:34:05 +03:00
swipelab
bb07bd9cae P0.3: flip the quad color on a real iOS-Simulator tap, driven by sx
Prove the input path end-to-end. Poll platform events in the frame loop
and, on a mouse_down (UIKit touchesBegan, mapped to an Event in uikit.sx),
toggle the centered quad between orange and green. The toggle marks the
vertex buffer dirty; the next frame re-uploads the active color palette
before drawing. Flip on the press only — a tap also delivers mouse_up
(touchesEnded), so toggling on both would net to no visible change.

goldens/p0_input_before.png (orange quad) and goldens/p0_input_after.png
(green quad) bracket a real tap injected at the device-screen center
(201,437 pt) via `idb ui tap` on the booted iOS 26.0 simulator.
2026-06-04 18:40:49 +03:00
swipelab
df6cb2161d P0.2: draw a colored quad on the iOS Simulator, driven by sx
Prove the render path end-to-end: bring up UIKit + Metal (already
scaffolded), clear to a solid blue, and draw one centered orange quad via
the GPU protocol's clear+quad path — an MSL pass-through pipeline plus a
6-vertex (2-triangle) NDC buffer, created lazily once the MTLDevice exists.

Geometry is NDC [-0.5, 0.5]² so the quad is the central 50%x50% of the
drawable regardless of device resolution, keeping the screenshot golden
unambiguous. Background (0.10, 0.20, 0.55), quad (1.0, 0.6, 0.0).

goldens/p0_quad.png is the first screenshot golden, captured from the
booted iOS 26.0 simulator after install + launch.
2026-06-04 18:21:35 +03:00
swipelab
05fae4d78f P0.1: scaffold m3te sx project from the proven game structure
Stand up build.sx (macos + ios/ios-sim targets, bundle id
co.swipelab.m3te, output sx-out/ios/M3te.app, assets dir) and a minimal
main.sx that brings up the platform (UIKit+Metal on iOS, SDL3+GL on
macOS) and renders a solid-clear frame. Add assets/ and goldens/
directories and a .gitignore for build artifacts.

Modeled on game/build.sx and game/main.sx; modules resolve from the
compiler binary with no -L flag.
2026-06-04 18:08:54 +03:00