Resume the clock on leaving the score sheet, and split the readout in three

Closing the score sheet left the clock paused, so glancing at the score or
nudging a clock cost a tap on play. It now picks the turn back up on the way
out. openScore() records whether it was the thing that paused the clock, so a
pause the players set themselves survives. After a score change there is
nothing to resume — newGame() has already moved to IDLE — and resumeNow()
only acts on PAUSE, so that case needs no special handling. The scrim behaves
the same, since dismissing discards the staged edits and leaves the turn live.

togglePause() had the resume arithmetic inline; it splits out as resumeNow()
to match pauseNow(), leaving the play button and the sheet on one path.

The score line becomes three labelled lines:

  You:   2 out of 5
  Them:  1
  Moves: 7

.head is a two-column grid with a max-content label column, so the values
line up whatever the system font renders — no hand-tuned em width to drift
between iOS and Android. Each line is a display:contents wrapper, which keeps
one element per line to hide when a one-point match drops the score. The gap
above Moves rides on that row's two cells, since a display:contents row can't
carry a margin, and a :not([hidden]) guard drops it when there is no score
line above to separate from.

Each half reads from its own side of the board, so both players see their own
score on the You line. st.score is already indexed by side, so the mirroring
costs nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 15:11:31 +02:00
co-authored by Claude Opus 5
parent 9834296722
commit 0f69c78b74
3 changed files with 81 additions and 44 deletions
+26 -13
View File
@@ -56,8 +56,10 @@ except installation and the service worker.
unwinds around it — and a second tap within those two seconds does it. One unwinds around it — and a second tap within those two seconds does it. One
stray thumb can't wipe a live match. stray thumb can't wipe a live match.
- **Pause** freezes mid-turn and resumes exactly where it stopped, delay - **Pause** freezes mid-turn and resumes exactly where it stopped, delay
included. Opening either sheet pauses automatically, and leaves it paused so included. Opening either sheet pauses automatically. The settings sheet leaves
the clock only restarts when both players are ready. it paused — everything in there resets the clocks anyway — while the score
sheet picks the turn back up when you close it, so glancing at the score
doesn't cost you a tap on play.
Backgammon clocks pause between games, and after a cocked die the delay is Backgammon clocks pause between games, and after a cocked die the delay is
normally restarted — use pause and reset for those. normally restarted — use pause and reset for those.
@@ -68,26 +70,37 @@ Set **Match** in the settings to the number of points you're playing to — 1, 3
5 and up to 29. The time you set is *per point*, so a 5-point match at 2:00 a 5 and up to 29. The time you set is *per point*, so a 5-point match at 2:00 a
point puts 10:00 on each clock, and the settings sheet shows you that sum. point puts 10:00 on each clock, and the settings sheet shows you that sum.
At **1 point** there's no match to keep score of, so the score line and the Each half reads the state from that player's own side of the board:
score-sheet icon both disappear and you get the plain single-game clock. Set
the match to 3 or more and they come back.
The score sheet is the middle icon in the bar. It reads **You** for whoever is ```
holding the phone — the bottom half — and **Them** for the far end, and each You: 1 out of 5
panel shows its own score first, so both players read their own score on the Them: 3
left: `Score: 1 - 3, out of 5`. Moves: 7
```
So "You" always means whoever is sitting at that end — both players read their
own score on the top line, and the two halves show mirrored numbers.
At **1 point** there's no match to keep score of, so the score lines and the
score-sheet icon all disappear and you get the plain single-game clock with just
its move count. Set the match to 3 or more and they come back.
The score sheet is the middle icon in the bar. It's filled in from the phone
holder's side: **You** is the bottom half, **Them** the far end.
Nothing in that sheet takes effect until you press **Done**: Nothing in that sheet takes effect until you press **Done**:
- **Done with nothing changed** leaves everything exactly as it was — same turn, - **Done with nothing changed** leaves everything exactly as it was — same turn,
same delay part-spent, same move counts. Tapping outside the sheet is the same same delay part-spent, same move counts — and the clock picks up where it left
as changing nothing, so a stray tap can't restart a game. off. Tapping outside the sheet is the same as changing nothing, so a stray tap
can't restart a game. A pause you set yourself before opening the sheet
survives; only the sheet's own pause is undone.
- **Change a score** and the game restarts when you press Done: move counters - **Change a score** and the game restarts when you press Done: move counters
back to zero and the turn back to the first tap, waiting on whoever rolls the back to zero and the turn back to the first tap, waiting on whoever rolls the
lower die. The clocks keep whatever they were showing. lower die. The clocks keep whatever they were showing.
- **Change only a clock** — the sheet also lets you correct either player's time - **Change only a clock** — the sheet also lets you correct either player's time
to the second — and the game carries on undisturbed: same turn, same delay to the second — and the game carries on undisturbed and running: same turn,
position, same moves. same delay position, same moves.
Shortening the match to fewer points than someone has already won pulls their Shortening the match to fewer points than someone has already won pulls their
score down to fit. score down to fit.
+54 -30
View File
@@ -75,25 +75,30 @@
.panel--top .inner{padding:14px 20px max(14px,env(safe-area-inset-top))} .panel--top .inner{padding:14px 20px max(14px,env(safe-area-inset-top))}
.panel--bottom .inner{padding:14px 20px max(14px,env(safe-area-inset-bottom))} .panel--bottom .inner{padding:14px 20px max(14px,env(safe-area-inset-bottom))}
/* the label column sizes itself to the widest label, so the three values
line up whatever the system font turns out to be */
.head{ .head{
display:flex;flex-direction:column;align-items:flex-start; display:grid;
gap:3px; grid-template-columns:max-content max-content;
justify-content:start;
gap:3px 8px;
order:-1; order:-1;
}
.score[hidden]{display:none}
.score,.moves{
font-size:clamp(13px,3.7vw,20px); font-size:clamp(13px,3.7vw,20px);
font-weight:700; font-weight:700;
letter-spacing:.01em; letter-spacing:.01em;
color:var(--idle-mute); color:var(--idle-mute);
white-space:nowrap; white-space:nowrap;
} }
.panel[data-state="active"] .score, .line{display:contents}
.panel[data-state="active"] .moves, .line[hidden]{display:none}
.panel[data-state="flagged"] .score, /* set the moves line apart from the score — a display:contents row can't take
.panel[data-state="flagged"] .moves{color:var(--accent-mute)} a margin itself, so both its cells carry it and the row shifts down evenly.
.panel[data-state="flagged"] .score, The guard drops the space when there's no score line above to separate from. */
.panel[data-state="flagged"] .moves{color:rgba(255,255,255,.55)} .line:not([hidden]) + .line--moves > *{margin-top:7px}
.panel[data-state="active"] .head,
.panel[data-state="flagged"] .head{color:var(--accent-mute)}
.panel[data-state="flagged"] .head{color:rgba(255,255,255,.55)}
.stack{ .stack{
flex:1; flex:1;
@@ -313,8 +318,9 @@
<section class="panel panel--top" data-side="0" data-state="idle"> <section class="panel panel--top" data-side="0" data-state="idle">
<div class="inner"> <div class="inner">
<div class="head"> <div class="head">
<div class="score" hidden>Score: <span data-score>0 - 0</span>, out of <span data-target>1</span></div> <span class="line" hidden><span>You:</span><span><span data-you>0</span> out of <span data-target>1</span></span></span>
<div class="moves">Moves: <span data-moves>0</span></div> <span class="line" hidden><span>Them:</span><span data-them>0</span></span>
<span class="line line--moves"><span>Moves:</span><span data-moves>0</span></span>
</div> </div>
<div class="stack"> <div class="stack">
<div class="time" data-time>3:00</div> <div class="time" data-time>3:00</div>
@@ -373,8 +379,9 @@
<section class="panel panel--bottom" data-side="1" data-state="idle"> <section class="panel panel--bottom" data-side="1" data-state="idle">
<div class="inner"> <div class="inner">
<div class="head"> <div class="head">
<div class="score" hidden>Score: <span data-score>0 - 0</span>, out of <span data-target>1</span></div> <span class="line" hidden><span>You:</span><span><span data-you>0</span> out of <span data-target>1</span></span></span>
<div class="moves">Moves: <span data-moves>0</span></div> <span class="line" hidden><span>Them:</span><span data-them>0</span></span>
<span class="line line--moves"><span>Moves:</span><span data-moves>0</span></span>
</div> </div>
<div class="stack"> <div class="stack">
<div class="time" data-time>3:00</div> <div class="time" data-time>3:00</div>
@@ -571,9 +578,10 @@
root: p, root: p,
time: p.querySelector("[data-time]"), time: p.querySelector("[data-time]"),
moves: p.querySelector("[data-moves]"), moves: p.querySelector("[data-moves]"),
score: p.querySelector("[data-score]"), you: p.querySelector("[data-you]"),
them: p.querySelector("[data-them]"),
target: p.querySelector("[data-target]"), target: p.querySelector("[data-target]"),
scoreLine: p.querySelector(".score"), scoreLines: [].slice.call(p.querySelectorAll(".line:not(.line--moves)")),
delay: p.querySelector("[data-delay]"), delay: p.querySelector("[data-delay]"),
fill: p.querySelector("[data-fill]"), fill: p.querySelector("[data-fill]"),
dnum: p.querySelector("[data-dnum]") dnum: p.querySelector("[data-dnum]")
@@ -691,7 +699,8 @@
el[i].time.textContent = fmt(main); el[i].time.textContent = fmt(main);
el[i].moves.textContent = st.moves[i]; el[i].moves.textContent = st.moves[i];
el[i].score.textContent = st.score[i] + " - " + st.score[1 - i]; el[i].you.textContent = st.score[i]; // each panel is "You" to its own player
el[i].them.textContent = st.score[1 - i];
el[i].target.textContent = cfg.points; el[i].target.textContent = cfg.points;
var showDelay = live && cfg.delay > 0 && dLeft > 0; var showDelay = live && cfg.delay > 0 && dLeft > 0;
@@ -753,18 +762,19 @@
saveGame(); saveGame();
} }
function togglePause(){ function resumeNow(){
if(st.phase === RUN){ if(st.phase !== PAUSE) return;
pauseNow(); st.turnAt = performance.now() - st.held; // pick the turn up where it stopped
return; st.phase = RUN;
}else if(st.phase === PAUSE){
st.turnAt = performance.now() - st.held;
st.phase = RUN;
}else return;
paintPlay(); paintPlay();
saveGame(); saveGame();
} }
function togglePause(){
if(st.phase === RUN) pauseNow();
else resumeNow();
}
/* SVG elements aren't HTMLElements, so they have no .hidden property — /* SVG elements aren't HTMLElements, so they have no .hidden property —
setting it only makes a JS expando. The attribute has to be set by hand. */ setting it only makes a JS expando. The attribute has to be set by hand. */
function showIcon(btn, name, on){ function showIcon(btn, name, on){
@@ -902,7 +912,7 @@
if(many) node.removeAttribute("hidden"); else node.setAttribute("hidden", ""); if(many) node.removeAttribute("hidden"); else node.setAttribute("hidden", "");
} }
show(btnScore); show(btnScore);
el.forEach(function(e){ show(e.scoreLine); }); el.forEach(function(e){ e.scoreLines.forEach(show); });
} }
function clock(ms){ // m:ss, for copy rather than the big readout function clock(ms){ // m:ss, for copy rather than the big readout
@@ -1003,12 +1013,23 @@
document.getElementById("done").addEventListener("click", closeSheet); document.getElementById("done").addEventListener("click", closeSheet);
// tapping outside the score sheet discards the staged edits rather than // tapping outside the score sheet discards the staged edits rather than
// committing them — a stray tap must never restart a game // committing them — a stray tap must never restart a game
scrim.addEventListener("click", closeSheet); scrim.addEventListener("click", function(){
if(openNode === scoreSheet) closeScore(); else closeSheet();
});
/* ============ score sheet ============ */ /* ============ score sheet ============ */
// Edits are staged here and only reach `st` on Done, so opening the sheet, // Edits are staged here and only reach `st` on Done, so opening the sheet,
// fiddling and tapping away changes nothing. // fiddling and tapping away changes nothing.
var edit = null, before = null; var edit = null, before = null, resumeOnClose = false;
// leaving the sheet with the turn still live carries straight on. After a score
// change there is no turn to resume — newGame() has already moved us to IDLE,
// which the PAUSE check below quietly skips.
function closeScore(){
if(resumeOnClose) resumeNow();
resumeOnClose = false;
closeSheet();
}
function paintScore(){ function paintScore(){
document.getElementById("v-you").textContent = edit.score[1]; document.getElementById("v-you").textContent = edit.score[1];
@@ -1021,6 +1042,9 @@
} }
function openScore(){ function openScore(){
// only the sheet's own pause gets undone on the way out; a pause the players
// set themselves survives
resumeOnClose = (st.phase === RUN);
pauseNow(); pauseNow();
disarm(); disarm();
// seed from what the panels are showing, not from the banked reserve — for // seed from what the panels are showing, not from the banked reserve — for
@@ -1074,7 +1098,7 @@
document.getElementById("score").addEventListener("click", openScore); document.getElementById("score").addEventListener("click", openScore);
document.getElementById("score-done").addEventListener("click", function(){ document.getElementById("score-done").addEventListener("click", function(){
applyScore(); applyScore();
closeSheet(); closeScore();
}); });
btnReset.addEventListener("click", armReset); btnReset.addEventListener("click", armReset);
+1 -1
View File
@@ -1,5 +1,5 @@
/* Cache-first: once installed the clock never touches the network again. */ /* Cache-first: once installed the clock never touches the network again. */
const CACHE = "bgclock-v5"; const CACHE = "bgclock-v6";
const FILES = [ const FILES = [
"./", "./",
"./index.html", "./index.html",