Stop the think-clock claiming things it can't know

The clock beside the acting player told two lies, in opposite directions.

Refresh mid-hand and it restarts at 0:00. The clock lives in memory, so a reload wipes
it, and the bar then says the player has just started thinking when they might have
been in the tank for two minutes. It is the one number in the bar whose whole job is to
tell a dealer when to nudge somebody, and a refresh reset it to "no need".

It cannot know that time -- the action moved before this session existed -- so it does
not guess. It shows a dash until the action next moves, and says why if you hover:

  "The action was already on this seat when the page loaded, so there's nothing to
   time from yet. The clock starts when the action next moves."

The moment the action moves it is measuring something it saw, and it counts from zero
again.

And past an hour it read "65:03". A table left open on a hand really does get there --
the state survives a refresh by design -- and sixty-five minutes written as though it
were minutes and seconds is a number the eye gets wrong. It reads 1:05:03 now.

Checked in the browser rather than reasoned about: dealt in this session it counts 0:00
then 0:02; refreshed mid-hand it shows the dash and keeps showing it rather than
drifting up from an invented zero; one action later it is back to a real 0:00; and with
the clock wound forward it reads 1:05:04 with the slow-tank styling still applied.

5 tests (283 total) on the formatting, including that minutes are padded once there are
hours and not before, and that a day reads 25:00:00 rather than wrapping.

Also checked and correct already: a backgrounded tab does not lose time -- four minutes
away reads 4:01 on return, because the clock is a timestamp difference rather than a
count of ticks -- and the timer disappears entirely once the hand is over.

Co-Authored-By: Claude Opus 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_015bnp7xKUPFFSpwkYNFaPgb
This commit is contained in:
2026-09-01 20:06:42 -06:00
co-authored by Claude Opus 5
parent ab92259e3e
commit cb057b5159
2 changed files with 74 additions and 2 deletions
@@ -11,6 +11,11 @@ const ActionBar = {
mount(state, hooks) {
this.state = state;
this.hooks = hooks;
// Whoever was already on the clock when this page loaded — see tankUnknownFor.
this.tankUnknownFor = state.bettingOpen &&
state.actingSeatIndex !== null && state.actingSeatIndex !== undefined
? state.actingSeatIndex
: null;
this.watchHeight();
this.render();
},
@@ -207,6 +212,14 @@ const ActionBar = {
tankStartedAt: null,
tankInterval: null,
// The seat that was already being asked to act when this page loaded, if there was
// one. Its clock is unknowable: the action moved to them before this session
// started — maybe two minutes ago, maybe last night — and starting from zero would
// have the bar say they had just begun thinking. Which it did, every time a dealer
// refreshed mid-hand. Cleared the moment the action moves, because from then on the
// clock is measuring something it actually saw.
tankUnknownFor: null,
// Restarts only when the action actually moves — a re-render for any other reason
// (a chip edit, a name change) must not put a thinking player back to zero.
syncTankTimer() {
@@ -220,6 +233,8 @@ const ActionBar = {
}
if (this.tankSeatIndex !== index) {
// Once the action has moved under our own eyes, nothing is unknown any more.
if (this.tankSeatIndex !== null) this.tankUnknownFor = null;
this.tankSeatIndex = index;
this.tankStartedAt = Date.now();
}
@@ -243,13 +258,33 @@ const ActionBar = {
const el = document.getElementById("actionTank");
if (!el || this.tankStartedAt === null) return;
if (this.tankUnknownFor !== null && this.tankSeatIndex === this.tankUnknownFor) {
el.textContent = "—";
el.title = "The action was already on this seat when the page loaded, so there's nothing to time from yet. The clock starts when the action next moves.";
el.classList.remove("tankNudge", "tankSlow");
return;
}
const seconds = Math.floor((Date.now() - this.tankStartedAt) / 1000);
const mins = Math.floor(seconds / 60);
el.textContent = `${mins}:${String(seconds % 60).padStart(2, "0")}`;
el.title = "How long this player has been thinking";
el.textContent = this.formatTank(seconds);
el.classList.toggle("tankNudge", seconds >= this.NUDGE_AFTER_S && seconds < this.SLOW_AFTER_S);
el.classList.toggle("tankSlow", seconds >= this.SLOW_AFTER_S);
},
// "0:07", "4:01", and past the hour "1:05:03" rather than "65:03" — a table left
// open on a hand does reach that, and sixty-five minutes written as though it were
// a minutes-and-seconds reading is a number the eye gets wrong.
formatTank(seconds) {
const pad = value => String(value).padStart(2, "0");
const hours = Math.floor(seconds / 3600);
const minutes = Math.floor((seconds % 3600) / 60);
const rest = seconds % 60;
return hours > 0
? `${hours}:${pad(minutes)}:${pad(rest)}`
: `${minutes}:${pad(rest)}`;
},
// Shown only once someone is all-in and the money has actually split — a single
// undivided pot needs no announcement, and a line that's always there stops being
// read.
+37
View File
@@ -0,0 +1,37 @@
// How the think-clock reads.
const test = require("node:test");
const assert = require("node:assert");
const path = require("path");
const ActionBar = require(path.join(
__dirname, "..", "..", "JoshHeaps.Net", "wwwroot", "js", "casino", "holdem", "actionBar.js"));
test("seconds under a minute", () => {
assert.equal(ActionBar.formatTank(0), "0:00");
assert.equal(ActionBar.formatTank(7), "0:07");
assert.equal(ActionBar.formatTank(59), "0:59");
});
test("minutes and seconds", () => {
assert.equal(ActionBar.formatTank(60), "1:00");
assert.equal(ActionBar.formatTank(241), "4:01");
assert.equal(ActionBar.formatTank(3599), "59:59");
});
test("past the hour it says the hour", () => {
// A table left open on a hand reaches this, and "65:03" is a number the eye reads
// as minutes and seconds when it isn't.
assert.equal(ActionBar.formatTank(3600), "1:00:00");
assert.equal(ActionBar.formatTank(3903), "1:05:03");
assert.equal(ActionBar.formatTank(45296), "12:34:56");
});
test("the minutes are padded once there are hours, and not before", () => {
// "1:5:03" would be wrong; "05:03" on its own would be odd.
assert.equal(ActionBar.formatTank(3903), "1:05:03");
assert.equal(ActionBar.formatTank(303), "5:03");
});
test("a day is still a readable number rather than a wrapped one", () => {
assert.equal(ActionBar.formatTank(90000), "25:00:00");
});