diff --git a/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js b/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js index 1998e6e..0e5d969 100644 --- a/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js +++ b/JoshHeaps.Net/wwwroot/js/casino/holdem/actionBar.js @@ -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. diff --git a/tests/holdem/tankClock.test.js b/tests/holdem/tankClock.test.js new file mode 100644 index 0000000..1dccacf --- /dev/null +++ b/tests/holdem/tankClock.test.js @@ -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"); +});