import InfoBox from '../components/InfoBox.astro';

When a player starts a daily challenge on their phone and finishes it on their laptop, they expect to see the same game state: the same score, the same recap, the same leaderboard submission status across both devices. What actually happened was simpler and worse — whichever device spoke to the server last would overwrite what came before it, and a player signing into a second device could find the same day's game marked finished on the server but un-submitted locally, duplicating their leaderboard score.

The fix was straightforward on paper: remember both copies and merge them rather than overwrite. On the ground, the path to the fix exposed three separate bugs — timing races, sync gaps, and a leaderboard dedup missing its intended column — all of which had to be resolved together.

## The sync gap that silently succeeded

A player reached game-over on their phone, saw the final score, and submitted it. The submission succeeded. The server recorded the leaderboard row, acknowledged the success to the client, and the phone's local storage marked the day as submitted. But unknown to both client and server, the account's synced record of today's score — the copy that shows up when the player signs in on a different device — was still marked un-submitted, stuck in the state it had been when the game ended but before leaderboard submission finished.

The root cause was timing. The flow was:

1. Player reaches game-over (score known, submission status `false`)
2. `account.syncDailyStats()` runs, stores the game state with `submitted: false`
3. `submitScore()` posts the leaderboard row and succeeds
4. Server acknowledges the success, client's local storage is updated

The sync fired before submission could complete, and no re-sync happened afterward. The gap was introduced in the cross-device accounts feature that brought `syncDailyStats()` into the game loop in the first place; adding a second sync call after submission succeeded closed it. The fix was a one-line addition to the game-over flow, tested immediately with a regression test that reached game-over, submitted a score while signed in, and asserted the account's synced copy flipped its submitted flag to match.

## The overwrite that said no

The account sync brought a second problem with it. When a player signed in on a second device and that device fetched the server's copy of today's game — the score synced from the first device — it merged that score with whatever was already local on the second device by taking whichever had a higher score and calling it done. That worked when scores were genuinely comparable, but the server did not do the same merge. It simply overwrote.

`AccountService.setDailyStats()` wrote the device's local game state unconditionally, comparing it to nothing. A device that had played and reached a lower score would fetch the server's (correct) higher score, but then the game page would load the local copy instead and keep showing the worse one — even after sign-out and sign-in, since the local bad copy was still there.

The solution was to make both sides merge identically. A shared `mergeDailyResult()` function took both the local and synced copies and produced one true copy: the higher score wins, and the `submitted` flag stays true once either side marked it so. Both client and server now use the same function when updating the synced result. The strategy, plus the column added to leaderboard rows to link them to accounts (see below), was written up as a decision record to keep the logic centralized and the intent explicit.

## The race that left the game waiting

The game page was built to show a fallback: if the local device had nothing for today yet, show the synced version from the account instead. That worked fine as a fallback, but loading the synced version and starting a fresh live game happened in the same moment, and if account-refresh data arrived after the live game had already begun, it was discarded. A device with a zero-word game pending (in the initial state) could bounce between the live game and a stale recap depending on whether the user navigated away and back, refreshing the account data in the meantime.

The fix was to detect that race explicitly: if the game page started a fresh live game while account data was still being fetched, and that account data arrives before any actual play, undo the live start (reset to zero words, keeping it in the initial state) so the fallback recap can be shown instead. A replay with zero words, once added, is kept as a separate code path, so this automatic reset never interferes with legitimate restarts.

## The missing column

All of these fixes worked around a gap in the leaderboard design. Scores were already scoped to a day and deduplicated per device to keep only the best score per player per day. But when a player signed in on a second device, that second device had its own identifier and would create what looked like a "new" entry the dedup logic did not recognize as a duplicate, letting a player submit the same score twice.

The fix was to add account linkage to the dedup logic: when a player submits a score while signed in, the server now records their account ID alongside the score and prioritizes account linkage in dedup. If a row has an account link, it groups by that; otherwise it falls back to the original device-scoped dedup.

On account deletion, the link is severed (set to null) rather than deleting the row outright, keeping the player's historical leaderboard presence visible as anonymous. The privacy policy updated to disclose both the new link and the anonymization-on-delete behavior.

This reversed the earlier decision that accounts would hold no link back to any leaderboard row. The rationale was sound when applied to preventing cross-device dedup — there was no cross-device state to dedup then. Once cross-device play became real, the need became equally clear: a player who has played on three devices and signs in on a fourth needs to know which of those three old scores were theirs, so that fourth device's recap can show the real best-of-three, not a fresh game or the first device's score.

## Plans

The dedup is now persistent. Whether re-merging strategies will be needed for any future cross-device features (duel game state, for instance) remains an open question. The next several days of play will test whether the account-linkage merge has side effects the automated tests could not predict.