Match length is a new first row in the settings, 1 to 29 points, stepping by
2 so it can only ever be odd. The Time setting now means time *per point* and
each clock starts at time × points, which the settings sheet spells out.
The middle bar icon opens a score sheet. Edits are staged there and only
reach the state on Done, which then does one of three things:
nothing changed turn, delay position and moves all untouched
score changed displayed times become the reserves, then back to the
first tap with moves zeroed — the clocks keep their value
clock changed the spent delay is added back on so the panel shows
exactly what was typed, and the turn carries on
Tapping outside the sheet discards the staged edits, so a stray tap can't
restart a game.
reset() was called both by the Reset button and by every settings change, so
clearing the score in it would have wiped a live match whenever the delay was
nudged. It splits into resetClocks() and resetAll(); only the button clears
the score. Shortening a match clamps a score that no longer fits.
The whole game now persists, not just the settings: score, both reserves,
moves, whose turn and how much delay was left, saved on the discrete
transitions and on pagehide/visibilitychange. Time passing while the app is
dead can't be charged to anyone, so a running clock is stored as paused and
comes back on the play button. A malformed saved game is ignored rather than
half-applied.
Each panel shows its own score first, stacked above its move count at the
top left.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
34 lines
980 B
JavaScript
34 lines
980 B
JavaScript
/* Cache-first: once installed the clock never touches the network again. */
|
|
const CACHE = "bgclock-v4";
|
|
const FILES = [
|
|
"./",
|
|
"./index.html",
|
|
"./manifest.webmanifest",
|
|
"./icon-192.png",
|
|
"./icon-512.png",
|
|
"./icon-maskable-512.png"
|
|
];
|
|
|
|
self.addEventListener("install", (e) => {
|
|
e.waitUntil(caches.open(CACHE).then((c) => c.addAll(FILES)).then(() => self.skipWaiting()));
|
|
});
|
|
|
|
self.addEventListener("activate", (e) => {
|
|
e.waitUntil(
|
|
caches.keys()
|
|
.then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k))))
|
|
.then(() => self.clients.claim())
|
|
);
|
|
});
|
|
|
|
self.addEventListener("fetch", (e) => {
|
|
if (e.request.method !== "GET") return;
|
|
e.respondWith(
|
|
caches.match(e.request).then((hit) => hit || fetch(e.request).then((res) => {
|
|
const copy = res.clone();
|
|
caches.open(CACHE).then((c) => c.put(e.request, copy)).catch(() => {});
|
|
return res;
|
|
}).catch(() => caches.match("./index.html")))
|
|
);
|
|
});
|