Skip to content

Release 1.4.6 — stability sprint (drag, predictive back, audio) #6

Merged
yet300 merged 6 commits into
mainfrom
1.4.6
May 16, 2026
Merged

Release 1.4.6 — stability sprint (drag, predictive back, audio) #6
yet300 merged 6 commits into
mainfrom
1.4.6

Conversation

@yet300

@yet300 yet300 commented May 16, 2026

Copy link
Copy Markdown
Owner

Summary

Bug-fix / hardening release. Six commits, all targeted at real user-visible
issues found while playing on iOS and Android.

User-visible fixes

  • Piece won't drop on a valid cell after a line clear. The drag pipeline
    captured a stale model.grid in a pointerInput closure, so freshly cleared
    cells still read as occupied until the user went back to Home → Continue.
    (186a5e4)
  • Crash on cancelled iOS predictive back gesture. Decompose 3.5.0's default
    predictiveBackAnimation spring-overshoots its corner radius below zero,
    which RoundedCornerShape.createOutline rejects. Replaced with a
    Cupertino-style translation-only animation that has no animated Shape.
    (a19ef49)
  • High score visibly drops after Continue. GameEngine.restore overwrote
    the seeded lifetime best with whatever was in the save blob, which could be
    older than the persisted setting. Now merged with max. Unit-tested.
    (6f6e040)
  • iOS music could leak past stopMusic. A race between the playlist
    coroutine's dispatcher hop and stopMusic() let a new player start after
    cancel. Guarded with a monotonically-incremented generation. (6f6e040)

Hardening / under the hood

  • Save file gets a versioned envelope (SavedGame(version, state)) and
    corrupt / wrong-version blobs are now cleared from disk instead of silently
    failing on every cold start. (6f6e040)
  • iOS SFX now lazy-load on cache miss, matching the Android behaviour.
    The knownSfx list is no longer a hard requirement; missing files are
    remembered in a miss-set so they don't re-pay the I/O cost on every call.
    (41e1ff3)

Performance

  • Drag + grid recompositions cut sharply. Two passes:
    • Hoisted state reads on the drag overlay into the layout phase so the
      whole GameContent stops recomposing 60×/s during a drag. (f21aaa3)
    • Game-over saturation ColorFilter is now applied once on a parent
      graphicsLayer instead of 64×; cells extracted into a GridCell taking
      stable primitives (cellId: Int, clearedNonce: Int, …) so Compose can
      actually skip them. (f21aaa3)
    • Animated hover/predict pulses pushed through () -> Float lambdas — only
      cells in the hover or predicted-line set read the per-frame state. (41e1ff3)

Versioning

  • appVersionName 1.4.0 → 1.4.6
  • appVersionCode 9 → 10
  • MARKETING_VERSION 1.4.0 → 1.4.6, CURRENT_PROJECT_VERSION 9 → 10
  • Play Store changelog: fastlane/.../changelogs/10.txt

Known / not in scope

  • SFX assets block_place.mp3 and line_clear_1..4.mp3 are referenced by
    playPlacement() / playClear() but not present in
    composeResources/files/audio/ — placement and clear sounds are silent
    on both platforms. iOS lazy-load is in place so dropping the files in will
    Just Work, but the asset files themselves need to be added separately.
  • iOS AVAudioSession.setActive(true) was investigated and skipped — the K/N
    binding in this Compose Multiplatform version doesn't expose it directly,
    and setCategory(Playback) alone is sufficient for current playback needs.

Test plan

  • Drag a piece through a sequence of placements that trigger line clears;
    verify pieces always drop on valid cells, including immediately after a
    multi-line clear animation finishes.
  • On iOS, start a back-swipe from the edge on the Game screen, release
    it before crossing the commit threshold. Verify no crash; previous
    screen slides back into place.
  • Set a high score, kill the app via app switcher mid-round, relaunch and
    tap Continue. Verify HUD shows the higher of (settings best, save best).
  • Toggle background music repeatedly across screen transitions / fg/bg
    cycles. Verify only one player is ever audible.
  • Force-write a corrupt save (manual edit of the settings JSON), cold
    launch. Verify the app starts a new game and the corrupt blob is gone
    from the settings store.
  • Smoke: combo flashes, particle bursts, danger vignette, settings sheet
    open/close — no visual regressions.

yet3a and others added 6 commits May 16, 2026 18:45
The drag pipeline captured `model.grid` once at drag start (via
pointerInput's awaitPointerEventScope closure). When lines cleared
mid-drag, `canPlacePiece` kept checking against the stale grid and
rejected drops on cells that had just been freed — the user had to
exit to Home and Continue to recover.

Wrap the drag callbacks in rememberUpdatedState so they always see
the current grid. Also:
- Restore pieceIdCounter on engine restore to prevent ID collisions
  between persisted pieces and newly refilled ones.
- Track clear-animation nonce per cell so concurrent clears don't
  skip the animation when the parent's prevNonce updates first.
- Center the dragged ghost on the finger horizontally for a more
  predictable snap target regardless of pickup point.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GameContent no longer recomposes on every drag tick. DraggedPieceOverlay
now reads dragDropState.dragPosition inside its Modifier.offset lambda,
so the State subscription stays in the layout phase and only the overlay
relayouts as the finger moves.

GameGrid applies the game-over saturation ColorFilter once on a parent
graphicsLayer instead of per-cell, cutting 64 ColorMatrix applications
to one. Cells are extracted into a private GridCell composable that
receives stable primitive parameters (cellId: Int, clearedNonce: Int,
isInClearedEvent: Boolean, tapEnabled: Boolean) instead of the full
Grid/ClearEvent/Piece references, so Compose can actually skip cells
whose inputs didn't change.

DragDropState.updateDrag always recomputes isValidPlacement — the grid
can change under a stationary finger when a line-clear finishes
mid-drag, and gating the recompute on anchor-change reintroduced the
"valid spot but won't place" bug from the previous commit. Structural
equality on mutableStateOf already elides redundant emits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
H1 — Best score wiped on restore (GameEngine.kt:92): restore() did a
wholesale `_state.value = state`, dropping the bestScore previously
seeded from settings. If autosave hadn't flushed before the prior
process death, the HUD would silently downgrade for the rest of the
round and bestAtRoundStart (used by the review-prompt qualifier)
would be wrong. Merge max(seeded, restored) inside restore() so all
callers are protected. Covered by two new tests in
GameEngineTest.kt (restore_preserves_seeded_best_when_save_has_lower_best,
restore_uses_save_best_when_higher_than_seeded).

H2 — Save robustness (SettingsBackedGameSaveRepository.kt:34-66):
wrap persisted JSON in a versioned envelope (SavedGame { version,
state }, CURRENT_SAVE_VERSION = 1). On load, drop saves whose
version doesn't match — and, critically, settings.remove(KEY_SAVE)
when the blob is unparseable so we don't pay the parse cost every
cold start and don't leave a permanent one-way trap if a future
GameState rename invalidates existing saves.

H3 — iOS music loop race (NativePlatformSoundPlayer.kt:46-118):
after loadPlayer (which hops dispatchers) the loop could publish a
new musicPlayer and call play() after stopMusic() had already
cleared the field — leaking audio with no way to stop it. Add a
monotonic musicGeneration captured at launch; check ensureActive()
+ generation before assigning/playing. stopMusic() increments the
generation so any in-flight load on the previous generation is
dropped on the floor.

Nits:
- systemVersionMoreOrEqualThan.kt:17 — remove stray println.
- GameContent.kt:186-187 — replace magic `8` with Grid.SIZE.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
GameGrid read predictAlpha and hoverPulseAlpha (both InfiniteTransition
floats) as State directly in the BoxWithConstraints scope and then passed
the unwrapped Float to every GridCell. Two consequences:

1. The parent's content lambda re-ran ~60×/s, re-evaluating all 64 cell
   call-sites every frame.
2. Cells received a Float parameter that changed every frame, so even
   cells with stable inputs lost their skippability and recomposed.

Pass each pulse as a `() -> Float` instead. The lambda identity is
remembered, so it doesn't churn between recompositions, and the State
read only happens inside the branches that actually use it — i.e. only
in cells where isHoverGhost && hoverValid (hover pulse) or in cells
where inPredictedLine (predict pulse). Cells outside those sets never
read the State and stay skippable.

iOS SFX: switch safePlayReturning to lazy-load on cache miss, matching
the Android resolver. The knownSfx list was an enumeration requirement
— if a key wasn't preloaded, it was permanently silent. Failed lookups
are cached in sfxMisses so missing assets don't keep paying the I/O
cost on every call.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Decompose 3.5.0's default predictiveBackAnimation crashes with
IllegalArgumentException: "Corner size in Px can't be negative" when
the user starts a back swipe and releases it before crossing the
commit threshold. Its built-in animatable runs the screen's
RoundedCornerShape radius back to 0 via a spring, which overshoots
below zero; RoundedCornerShape.createOutline then rejects the
negative value and there's no catch in the layer pipeline.

Replace it with a Cupertino-style animation that only translates
the layers (no animated Shape anywhere). Any spring overshoot now
shows up as a few pixels of extra horizontal drift, which is
invisible — and the dim overlay's alpha is explicitly coerced to
[0, 1]. Visually closer to native iOS swipe-back too.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yet300 yet300 merged commit 9e19b7d into main May 16, 2026
3 checks passed
@yet300 yet300 deleted the 1.4.6 branch May 16, 2026 16:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants