versionCode 8. public_html/ changed, so the service worker cache goes to
bgclock-v23.
Confirmed on device from a debug APK: the tally reads as a tally in the bar at
arm's length, and from the far side of the board.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The ruled card it had been reads as a document, a table or a form at the size
it actually renders — six strokes inside a 16x18 box at clamp(23px,6.4vw,32px)
— and nothing in it said score. A tally does, with no box around it to explain
itself, and it is the one score mark that survives being seen from the far side
of the board: upside down it is still a tally.
Three uprights and the slash, not four and the slash. At 23px a fourth upright
closes the gaps and the whole glyph turns into a picket fence; the slash is kept
inside the uprights' span for the same reason, since running it past them made
the icon read wider than the gear and speaker beside it.
Drawn against six alternatives in two passes, judged rendered in the real bar
at both ends of the size clamp rather than described. The passes that lost are
worth a line each, because the reasons were not visible in the geometry: two
stacks of checkers flips to say the other player is ahead when the far player
looks at it, which is worse than merely asymmetric; a bar chart is the stock
analytics glyph and says statistics rather than this match; and a tally inside a
card is smothered by the card, whatever the count.
Screenshots 1 and 5 are redone. Not 2 and 3, where the open sheet covers the bar
and the icon is not in shot at all.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
versionCode 7. public_html/ changed, so the service worker cache goes to
bgclock-v22.
Confirmed on device, from a debug APK over the top of 1.5: the settings kept
from 1.5 came back on the You row with the switch off; the gear stayed
reachable with the two bases set apart, which is the failure that would have
had no way out; each side ran on its own delay, including a player set to zero
starting on their reserve immediately; and a turn left running well past the
delay, force-quit and reopened, came back paused with the split still in force.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A handicap match had no way to happen: one time control, one delay, both
players. The settings sheet now carries a switch between Match and Time —
"Different time per player" — and turning it on gives Time and Delay a second
row each, labelled You for the bottom half and Them for the far end, the same
way round as the score sheet. Them starts as a copy of You. Turning it off puts
both players back on You's value, so the row that survives is the one still on
screen; turning it on again re-copies rather than remembering what Them used to
say. The presets go while it is on, because a preset is one pair of numbers and
there are two players to give it to.
Underneath, cfg.base and cfg.delay are arrays indexed like st.reserve and the
panels, and they are arrays whether or not the switch is on. Split is a fact
about the sheet, not about the clock: render() and tap() read their own side's
number and never ask which mode they are in, and shared is simply the state
where the two entries are equal, kept that way by one line in the setter. The
line that mattered most was matchPristine(), which compares both sides now —
left alone it would have made the gear unreachable the moment the two bases
differed, silently and with no way back but clearing storage.
bg.base and bg.delay still mean what they always did, which is side 1, so an
install from before this lands symmetric and an older build reading the same
storage still finds its own settings. bg.split, bg.base.them and bg.delay.them
are the new keys, and all four are clamped on the way in — points was the only
setting ever validated at load, which was survivable while nothing but our own
steppers could write these.
The sheet loses two rows that earned nothing. Sound duplicated the speaker
button that is in the bar at all times, including mid-match when the sheet
cannot be opened at all; the hint under Done repeated what the sheet's own
behaviour teaches. The score sheet's hint now names the match it would restart
within, and the Time copy ends in a colon and puts its two sums on their own
line, where they fit without breaking mid-sum.
Two bugs worth recording, both invisible in the diff and both caught by
rendering rather than by asserting. The Them rows are .side elements and
.side{display:flex} beat the hidden attribute, so both stepper rows showed in
shared mode — the probe asked for the hidden property, which was set correctly,
and passed. And data-them was already the panels' opponent-score span, so
hiding the sheet's Them rows blanked the score on both halves and collapsed the
head grid; it is data-them-row now.
Screenshots 2 and 3 are redone. The other three are unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
versionCode 6. public_html/ changed, so the service worker cache goes to
bgclock-v21.
Not confirmed on device: the force-quit-and-reopen check from RELEASING.md
step 4 has not been run for this build. Nothing in it touches storage, timing
or the state machine — the change is a label, an inherited colour and a lit
button, all written from render() — but the check is still owed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A stopped clock looked exactly like a running one. The panel keeps its accent
while paused, and isLive() counts a paused side as live, so the delay bar just
freezes mid-drain — nothing about either half says the clocks aren't moving.
The only tell was the play glyph in the bar, which is small, grey, and upside
down for one of the two players. It matters most in the state the app is
designed to land in: a match restored from localStorage comes back paused on
purpose, and nothing said so.
So both halves say it in words, under the delay bar, sized to sit between the
bar and the clock: wider than the bar at every screen size, narrower than the
clock at m:ss. The band is tighter than it sounds, since "3:00" is only about
2.2x the bar, so part of the width comes from the tracking rather than the
glyph size. Measured from 320 to 1024px wide it lands at 1.3-1.4x the delay
row and 74-82% of the clock. It is wider than the clock in the last ten
seconds of a reserve, where the readout drops to three narrow characters —
unavoidable inside these bounds, and the two are never confusable at four
times the height.
The word hangs off the delay row rather than joining .stack, so the big clock
does not move by a pixel when the clock stops. It cannot live inside .delay
either: that is opacity:0 whenever no delay is running, and a child can never
be more opaque than its parent, which would lose the word on the waiting half,
on a spent delay, and at a delay of 0. It takes no colour of its own, so it
wears whatever ink the clock above it is wearing, in every theme.
The play button lights up while paused, the same --icon-hot a held Reset uses.
Both toggles live in render() rather than in paintPlay(), for the reason the
comment there already gives: state written from the render loop cannot go
stale.
Screenshot 2 is redone — the score sheet pauses, so the word shows behind the
scrim. The other four are unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
versionCode 5. public_html/ changed, so the service worker cache goes to
bgclock-v20.
Confirmed on device: a turn left running well past the delay, force-quit and
reopened, now comes back with the time it had actually spent.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
versionCode 4. public_html/ changed, so the service worker cache goes to
bgclock-v19 — without it an installed web user would keep serving the old page
from cache indefinitely.
The five store screenshots are untouched: the ring only exists while a finger
is on the button, and none of them shows that.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A test user found the reset button, tapped it once and gave up. That is what
the old feedback invited: a first tap brightened the icon and started a ring
draining over 2s, and a draining ring reads as "wait" or "loading", not "press
me again". The one thing that would have said otherwise — a word — is not
available here, because the bar is shared by two players sitting opposite each
other and every word in this app lives inside a panel that rotates to face one
of them.
"Tap this twice" has no wordless vocabulary. "Keep holding" has a very
well-worn one, so reset is now a 975ms hold: a dim track ring appears whole the
moment the finger lands, a bright arc fills over it from 12 o'clock, and
letting go early makes the arc retreat. The retreat is the instruction. A stray
hold is also far less likely than two stray taps inside 2s, so the live game
this was guarding is guarded better than before.
Three things that look incidental and are not:
- Hiding the ring is an opacity that waits out the retreat, not display:none,
which cut the retreat off at the instant of release — invisible, and it was
the whole point. The fade has a real duration because a 0s transition with a
delay may be treated as no transition at all, taking the delay with it.
- width:auto on the ring: .btn svg sets a width for the bar icons, and
inheriting it against the ring's new height draws an ellipse.
- The ring's transition is exempted from the blanket prefers-reduced-motion
rule. Collapsing it would fill the ring the instant you touched the button
and claim the reset was done 975ms before it was. It reports state.
Keyboard, switch and assistive activation cannot hold, and hold-only would have
left those users unable to reset at all, one release after an accessibility
pass. Clicks arriving with detail 0 — no pointer behind them — keep the old
two-presses-within-2s and the draining ring.
Success now sounds: the flag's falling triad, rising instead. Deliberately not
another rising fifth, which sndSwap owns and plays on every handover.
Also corrects a claim in RELEASING.md: the screenshots reproduce near-exactly,
not exactly. Two runs of identical code differ by a 4x13 pixel sliver where the
delay bar's fill edge lands mid-pixel, so a non-empty git diff after re-running
proves nothing on its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 14:51:06 +02:00
16 changed files with 513 additions and 119 deletions
They are rendered, not photographed, so they reproduce exactly and can be
redone whenever the UI changes. Output is 1170x2532 — 390x844 CSS pixels at
device scale 3, an iPhone-sized frame that F-Droid is happy with.
They are rendered rather than photographed, so they can be redone whenever the
UI changes. Output is 1170x2532 — 390x844 CSS pixels at device scale 3, an
iPhone-sized frame that F-Droid is happy with.
Near-exact, not exact: two runs of identical code differ by a 4x13 pixel sliver
at the delay bar's fill edge, where the scaleX boundary lands mid-pixel and the
rasteriser rounds it differently. Invisible, but it means a non-empty git diff
after re-running proves nothing on its own.
Three things bite, and all three fail quietly rather than loudly:
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.