Skip to content

Fix rare duplicate cards caused by stale selections - #102

Merged
austin-smith merged 1 commit into
mainfrom
fix-stale-selection-card-duplication
Aug 31, 2026
Merged

Fix rare duplicate cards caused by stale selections#102
austin-smith merged 1 commit into
mainfrom
fix-stale-selection-card-duplication

Conversation

@austin-smith

Copy link
Copy Markdown
Owner

What Changed

  • tryMoveSelection — the single funnel every move goes through (taps, drags, auto-moves, auto-finish) — now re-derives the selection from the live board via a new liveSelection(matching:) helper before applying anything. If the source no longer holds exactly the selected cards, the move is refused and the selection cleared. Valid moves use the freshly derived cards, so a stale isFaceUp can't leak in either.
  • processPendingAutoMoveIfPossible validates a queued auto-move the same way before animating, so a stale request is dropped instead of flying a card to a destination the session would then refuse.
  • allCards moved from the persistence file's private extension into GameState, alongside a new hasNoDuplicateCardIDs invariant that refreshAutoFinishAvailability (the checkpoint every mutating flow ends with) now asserts in debug builds — any residual duplication path crashes at the exact mutation instead of corrupting silently.
  • New regression tests in StaleSelectionMoveTests covering the confirmed corruption sequence, the waste-draw variant, and a sanity check that fresh selections still move.

Why

Rarely, the same card could appear twice on the board (seen in Klondike and FreeCell). Root cause: moves are applied ~0.2 s after they're decided, when the drop-flight animation lands. During that window the board is still tappable, and the moving card is invisible but still in the model — so a second tap on the same pile queues an auto-move whose selection snapshot includes the in-flight card. When that stale snapshot was applied after the first move landed, removeSelection removed cards by position while the destination received the snapshot's copies, leaving one card on the board twice (and, in the waste variant, vanishing another). The corruption self-destructed on relaunch because restore sanitization rejects invalid decks and deals a fresh game, which is why it was so hard to pin down.

The fix validates at the model funnel (covers every entry point and all variants) plus at auto-move processing, with a debug tripwire as defense in depth.

Validation

  • Reproduced the corruption deterministically first: driving the view model in the exact order ContentView does produced the same card on a foundation and a tableau pile simultaneously.
  • After the fix, the reproduction sequence is refused with the board left intact; the scenarios are now regression tests.
  • Full unit suite passes: 605 tests, 0 failures.

moves apply after an animated drop flight, so a selection queued during
that window snapshots the pre-move board, including the in-flight card.
applying that stale snapshot removed cards by position while appending
the snapshot's copies, leaving the same card on the board twice. the
move funnel now re-derives every selection from the live state and
refuses on mismatch, pending auto-moves validate before animating, and
a debug assertion checks the no-duplicate-card invariant after every
mutating flow.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Aug 31, 2026

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review Completed 2026-08-31T18:07:02.377977Z 8bde3db PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8bde3db7ba

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread README.md
@austin-smith
austin-smith merged commit cf089df into main Aug 31, 2026
3 checks passed
@austin-smith
austin-smith deleted the fix-stale-selection-card-duplication branch August 31, 2026 18:08
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.

1 participant