Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Bug-fix / hardening release. Six commits, all targeted at real user-visible
issues found while playing on iOS and Android.
User-visible fixes
captured a stale
model.gridin apointerInputclosure, so freshly clearedcells still read as occupied until the user went back to Home → Continue.
(186a5e4)
predictiveBackAnimationspring-overshoots its corner radius below zero,which
RoundedCornerShape.createOutlinerejects. Replaced with aCupertino-style translation-only animation that has no animated
Shape.(a19ef49)
GameEngine.restoreoverwrotethe 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)
stopMusic. A race between the playlistcoroutine's dispatcher hop and
stopMusic()let a new player start aftercancel. Guarded with a monotonically-incremented generation. (6f6e040)
Hardening / under the hood
SavedGame(version, state)) andcorrupt / wrong-version blobs are now cleared from disk instead of silently
failing on every cold start. (6f6e040)
The
knownSfxlist is no longer a hard requirement; missing files areremembered in a miss-set so they don't re-pay the I/O cost on every call.
(41e1ff3)
Performance
whole
GameContentstops recomposing 60×/s during a drag. (f21aaa3)ColorFilteris now applied once on a parentgraphicsLayerinstead of 64×; cells extracted into aGridCelltakingstable primitives (
cellId: Int,clearedNonce: Int, …) so Compose canactually skip them. (f21aaa3)
() -> Floatlambdas — onlycells in the hover or predicted-line set read the per-frame state. (41e1ff3)
Versioning
appVersionName1.4.0 → 1.4.6appVersionCode9 → 10MARKETING_VERSION1.4.0 → 1.4.6,CURRENT_PROJECT_VERSION9 → 10fastlane/.../changelogs/10.txtKnown / not in scope
block_place.mp3andline_clear_1..4.mp3are referenced byplayPlacement()/playClear()but not present incomposeResources/files/audio/— placement and clear sounds are silenton 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.
AVAudioSession.setActive(true)was investigated and skipped — the K/Nbinding in this Compose Multiplatform version doesn't expose it directly,
and
setCategory(Playback)alone is sufficient for current playback needs.Test plan
verify pieces always drop on valid cells, including immediately after a
multi-line clear animation finishes.
it before crossing the commit threshold. Verify no crash; previous
screen slides back into place.
tap Continue. Verify HUD shows the higher of (settings best, save best).
cycles. Verify only one player is ever audible.
launch. Verify the app starts a new game and the corrupt blob is gone
from the settings store.
open/close — no visual regressions.