Save the match every second instead of waiting to be told
A force-quit deep into a turn came back at the clock's last-turn-switch value, losing the whole interrupted turn. The tell was the delay counter returning at a full 12: the newest saved state was the one tap() writes, and tap() sets turnAt and calls saveGame() in the same breath, so it carries held ~ 0. Restoring it reproduces the moment of the turn switch exactly. The two triggers meant to save at quit time — visibilitychange and pagehide — do not fire in Android's WebView. MainActivity's onPause() forwards to web.onPause() under a comment claiming that is what makes visibilitychange fire. It isn't: document visibility follows the WebView's view and window visibility, not that call. The forwarding is still right for stopping timers and audio, but the saved match should never have depended on it. This is not new. It has almost certainly been broken since 1.0, and the release check could not catch it, because for the first 12 seconds of a turn the delay absorbs everything and render() never touches st.reserve. A clock that has lost its place is indistinguishable from one that is correctly sitting still, so every force-quit test that quit soon after a turn change passed. RELEASING.md now says to let the turn run 40 seconds first, which is the only version of that check worth running. So the page stops waiting to be told. A timer writes the match once a second while a clock is running — a timer rather than a hook in loop(), because requestAnimationFrame stops when the page is hidden, which is exactly when this matters. blur joins the listeners too: a different signal, and one that does fire when the task switcher opens. At most a second can now be lost, which is well inside the delay nobody is charged for anyway. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,14 @@ make some moves, force-quit from the task switcher and reopen — everything
|
|||||||
should come back, paused. That is the check that proves the app's storage is
|
should come back, paused. That is the check that proves the app's storage is
|
||||||
working, and it fails silently rather than loudly.
|
working, and it fails silently rather than loudly.
|
||||||
|
|
||||||
|
**Let the turn run at least 40 seconds before you quit.** This is the whole
|
||||||
|
check, and doing it any faster proves nothing: for the first 12 seconds of a
|
||||||
|
turn the delay absorbs everything and the reserve clock is *supposed* to sit
|
||||||
|
still, so a clock that has lost its place looks exactly like one that hasn't.
|
||||||
|
Quit well past the delay and the time must come back lower than it was at the
|
||||||
|
turn switch — and the delay counter must not be sitting at a full 12. That is
|
||||||
|
how a broken save hid from this check from 1.0 to 1.3.
|
||||||
|
|
||||||
## 5. Commit, tag, push
|
## 5. Commit, tag, push
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -121,8 +121,15 @@ public class MainActivity extends Activity {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// forwarded so the page's visibilitychange fires — that is what calls
|
// Forwarded so the WebView stops its timers, animations and audio while the
|
||||||
// saveGame() and nudges the audio context back after backgrounding
|
// app is in the background, and picks them up again on return.
|
||||||
|
//
|
||||||
|
// This does NOT drive the page's visibilitychange — an earlier comment here
|
||||||
|
// claimed it did, and the saved match quietly depended on that being true.
|
||||||
|
// Document visibility follows the WebView's view and window visibility, not
|
||||||
|
// this call, so the page can be killed without ever hearing about it. The
|
||||||
|
// page no longer relies on being told: it writes the match to localStorage
|
||||||
|
// once a second while a clock is running.
|
||||||
@Override
|
@Override
|
||||||
protected void onPause() {
|
protected void onPause() {
|
||||||
super.onPause();
|
super.onPause();
|
||||||
|
|||||||
+16
-1
@@ -1051,11 +1051,26 @@
|
|||||||
if(st.phase === PAUSE && st.active < 0) st.phase = IDLE; // can't be mid-turn with no turn
|
if(st.phase === PAUSE && st.active < 0) st.phase = IDLE; // can't be mid-turn with no turn
|
||||||
}
|
}
|
||||||
|
|
||||||
// the only hooks iOS reliably fires before killing an app
|
// The hooks iOS fires before killing an app. Android's WebView is the problem
|
||||||
|
// child: neither of these is guaranteed there, whatever MainActivity's
|
||||||
|
// onPause() forwarding suggests, and when they don't fire the newest save is
|
||||||
|
// the one tap() wrote — which carries held ≈ 0, because tap() sets turnAt and
|
||||||
|
// saves in the same breath. Reopening then hands back the entire turn, and
|
||||||
|
// the bug hides in plain sight: quit inside the delay window and a clock that
|
||||||
|
// correctly hasn't moved looks exactly like a clock that lost its place.
|
||||||
document.addEventListener("visibilitychange", function(){
|
document.addEventListener("visibilitychange", function(){
|
||||||
if(document.visibilityState === "hidden") saveGame();
|
if(document.visibilityState === "hidden") saveGame();
|
||||||
});
|
});
|
||||||
window.addEventListener("pagehide", saveGame);
|
window.addEventListener("pagehide", saveGame);
|
||||||
|
// a different signal from visibilitychange, and it does fire when Android's
|
||||||
|
// task switcher opens. saveGame() is idempotent, so an extra call is free
|
||||||
|
window.addEventListener("blur", saveGame);
|
||||||
|
|
||||||
|
// and the one that doesn't depend on being told anything: a second is the
|
||||||
|
// most a force-quit can now cost, which is well inside the delay nobody is
|
||||||
|
// charged for. A timer rather than a hook in loop(), because rAF stops when
|
||||||
|
// the page is hidden — exactly when this matters most.
|
||||||
|
setInterval(function(){ if(st.phase === RUN) saveGame(); }, 1000);
|
||||||
|
|
||||||
/* Reset is a hold, not a tap. It used to be two taps inside 2s, and a test
|
/* Reset is a hold, not a tap. It used to be two taps inside 2s, and a test
|
||||||
user found the button, tapped it once and gave up — which is exactly what
|
user found the button, tapped it once and gave up — which is exactly what
|
||||||
|
|||||||
Reference in New Issue
Block a user