From a70b7f88f3433ec4a8fc27f955865a632763c4f4 Mon Sep 17 00:00:00 2001 From: "Jesse.Markowitz" Date: Sun, 30 Aug 2026 09:01:19 -0400 Subject: [PATCH] Say who the game is waiting on, and move the Fedora; replay in words MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three items folded into the unshipped v0.7.9. WAITING ON (reported by Jesse from play). The status line said "nobody — the Division is running itself" while the game was stopped on the Superintendent. Frame.actor carried clock.currentActor, which is null for the whole Mainline Phase, so all three interruptions — §8.1's clearance ruling, Gitea#5's Yard Office offer, Gitea#19's Red Flag prompt — reported that nobody was holding it up. actingPlayer had the answer since the Gitea#5 refactor; the Frame threw it away. It carries actingPlayer now, plus a new `awaiting` field naming the question and the train: "waiting on Bob · a clearance ruling · Train 4". Naming the person alone is not enough when three different things can be pending. THE FEDORA (TODO #29) rides at the right-hand end of the phase row instead of a line of its own, and wraps under rather than squeezing the chips. THE DEVELOPER REPLAY (TODO #34) printed "loss — revenueFloor", the same defect Gitea#16 was filed about, still alive because nothing player-facing pointed at it. panels.ts's reasonSentence is exported and shared rather than reimplemented, fed the last recorded frame and stripped of markup. The drift test maps win/loss to won/lost so it still checks the two AGREE rather than that they are spelled alike. Also carries the previous, unsigned commit's work: the two setup screens worded the same section by section. 877 tests pass, ten new. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX --- CHANGELOG.md | 66 +++++++++++++++++++++++++++++++++++++++++- TODO.md | 23 +++++++++++++-- src/sim/replay.ts | 17 ++++++++++- src/sim/turnchart.ts | 34 ++++++++++++++++++---- src/sim/view.ts | 30 +++++++++++++++++-- src/web/lobby.ts | 43 ++++++++++++--------------- src/web/main.ts | 40 ++++++++++++++----------- src/web/panels.ts | 7 ++++- src/web/play.html | 33 +++++++++++---------- test/replay.test.ts | 23 ++++++++++++++- test/web.test.ts | 69 +++++++++++++++++++++++++++++++++++++------- 11 files changed, 304 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a8d59e7..a0c3cd8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -154,6 +154,70 @@ missing one just made it a different form that happened to share a rules block. The drift guard that had been checking three prefixes now checks two, and a new assertion fails if any `ng-` id ever reappears. +### The two screens are worded the same, section by section + +Jesse, 2026-08-30: "let's get the text between the Multiplayer and solitaire as close as possible to +the same." Walked section by section; where the two said the same thing differently, the lobby's +wording won, because it was written for the harder case. + +- **The chair note is one paragraph on both**, replacing two that each described only their own + case: "Every chair must be filled before the game can start. For solitaire, there's only one + player. For multiplayer, that must be filled by a person or a bot. The number of players cannot be + changed once the game is created." +- **The Game type heading names which game the screen deals** — "Game type (multi-player)" and + "Game type (solitaire)". That heading is now the whole explanation for the dimmed rows. +- **No reason is printed beside a dimmed type any more.** The lobby appended "— dealt with the New + game button, not here" to Solitaire, and the solitaire screen appended "— use the Multiplayer + button; a table needs a server" to each of three. Jesse: "grayed out with no additional + explanation. The explanation above… is sufficient." One heading says it once; three rows were + saying it three times. `markUnavailable` is down to dimming, and `.lb-why` is gone with them. +- **The sentence under the radios is deleted on both.** It restated the type just chosen to the + person who had just chosen it. Its one non-obvious case — how many settings a Custom game differs + by — is not lost: `form.mark` already puts "Co-op default: …" on each row that differs, which is + where a reader can act on it rather than a count they would have to go and find. The tests that + read that sentence now read the row hints instead. +- **The Game settings, Starting hand and Victory conditions notes are the lobby's on both**, with + one edit Jesse asked for: "clicking a type again resets" → "changing the game type resets", which + names the control that does it rather than a gesture. Victory conditions was not on his list — + applied under the same rule, and the lobby's wording fits now that solitaire also shows the table + size. + +What is deliberately still different: each screen's own opening paragraph, and the heading above. + +### The status line said nobody was holding the game up + +Jesse, 2026-08-30: "waiting on shows 'nobody — the Division is running itself' BUT the system is +actually waiting on the Superintendent." + +`Frame.actor` carried `clock.currentActor`, which is null for the whole Mainline Phase — so the three +interruptions that stop the game and put a question to a named person (§8.1's clearance ruling, +Gitea#5's Yard Office offer, Gitea#19's Red Flag prompt) all reported that the Division was running +itself. `actingPlayer` has had the answer since the Gitea#5 refactor; the Frame threw it away. + +It carries `actingPlayer` now, **and what the question is**: "waiting on **Bob** · a clearance ruling +· Train 4". Naming the person is not enough on its own — three different things can be pending, and +"waiting on Bob" with nothing after it is a game that looks stuck to everyone except Bob. + +### The Fedora moved to the end of the phase row + +`TODO.md` #29. It sat on a line of its own between the phase chips and everything above them, which +put a thing that moves every third Stage in among the things that move every Stage. It rides at the +right-hand end of the phase row now — the row whose last chip is Supervisor Shift, the phase that +passes it — and wraps underneath rather than squeezing the chips on a narrow screen. + +### The developer replay stopped printing a raw enum + +`TODO.md` #34. Its heading read `loss — revenueFloor`: the exact defect Gitea#16 was filed about on +the playable page, still alive here a release after that was fixed, because nothing player-facing +pointed at it. It reads "lost — The Division closed short: 3 Revenue between everyone, against a +floor of 15…" now. + +`panels.ts`'s `reasonSentence` is exported and shared rather than reimplemented, so the replay and +the results screen cannot explain one ending two different ways. It is fed the last recorded frame — +the state the outcome was decided in — and stripped of markup, since it lands in an `

` and a +console line. The drift test that compared this string against the engine now maps `win`/`loss` to +`won`/`lost`, so it still checks the two agree rather than that they are spelled alike. + ### Three wording and layout fixes - **The collision entries** on all three screens now read "The game ends immediately and results in @@ -168,7 +232,7 @@ any `ng-` id ever reappears. cost, not a danger, and red would outrank the actual rules above it. The buttons say **Continue Existing Saved Game** and **Deal New Game** rather than "Continue saved game" and "Deal". -874 tests pass, seven of them new; one existing test asserted the opposite of the collision ruling +877 tests pass, ten of them new; one existing test asserted the opposite of the collision ruling above and says so where it was reversed. --- diff --git a/TODO.md b/TODO.md index a0c06c9..4b06fd2 100644 --- a/TODO.md +++ b/TODO.md @@ -168,8 +168,10 @@ pass as 19-21 and 24 above, and all three are answered by Gitea#18 rather than f 28. **INVESTIGATE: move the game's settings off the top line and into a card of their own** — and show ALL of them, not the four that fit. Reasoning in Display below. -29. **Put the Fedora at the right-hand end of the phase row**, with (or in) the Supervisor Shift - pill. Reasoning in Display below. +29. ~~**Put the Fedora at the right-hand end of the phase row**~~ — done 2026-08-30 in v0.7.9. It + rides at the end of the phase row now, and wraps under it rather than squeezing the chips on a + narrow screen. Went in beside the "waiting on nobody" fix below, since both are the top few rows + of status that Jesse asked to have re-looked at as a whole. Queued 2026-08-25, from releasing 0.7.1 / 0.4.9g. **Both are blocked on the two commits being made and pushed** — they were still uncommitted when the session ended. @@ -218,7 +220,12 @@ Queued 2026-08-29, from building Gitea#11 and #16 (both shipped in v0.7.3, main rather than recorded, a second pass can add any of them retroactively to games already played and saved. Needs Jesse and a conversation, not code, to start. -34. **`replay.ts` prints a raw outcome enum, exactly as the results screen used to.** Its summary +34. ~~**`replay.ts` prints a raw outcome enum, exactly as the results screen used to.**~~ — done + 2026-08-30 in v0.7.9. `panels.ts`'s `reasonSentence` is exported and shared, fed the last + recorded frame and stripped of markup; the drift test now maps `win`/`loss` to `won`/`lost` so + it still checks agreement rather than spelling. Original note below. + + **`replay.ts` printed a raw outcome enum, exactly as the results screen used to.** Its summary line is `` `${o.result} — ${o.reason}` ``, which renders "loss — revenueFloor" — the same defect Gitea#16 was filed about, in the dev-side replay viewer rather than the playable page. The sentences now exist (`panels.ts`'s `reasonSentence`), but they are written against a `Frame` and @@ -359,6 +366,16 @@ Queued 2026-08-29, from building Gitea#11 and #16 (both shipped in v0.7.3, main fields and confirmed the dealt game matches what was chosen. Worth being an early item in the next play session, alongside #39's four unplayed v0.7.4 features. +43. ~~**"Waiting on" said nobody while the game was stopped on the Superintendent.**~~ — done + 2026-08-30 in v0.7.9. Reported by Jesse from play. `Frame.actor` carried `clock.currentActor`, + null for the whole Mainline Phase, so all three interruptions (clearance, Yard Office, Red Flag) + reported that the Division was running itself. It carries `actingPlayer` now and a new + `awaiting` field says what the question is and which train it is about. + + **Worth knowing for the next Frame field:** the engine had the right answer in `actingPlayer` + since the Gitea#5 refactor, and the Frame simply did not carry it. A view that reads one field + of `clock` directly is a place this can happen again. + --- ## Replay / Save Games diff --git a/src/sim/replay.ts b/src/sim/replay.ts index 748d913..32225d6 100644 --- a/src/sim/replay.ts +++ b/src/sim/replay.ts @@ -35,6 +35,7 @@ import { legalActions } from '../engine/legal.ts'; import { createGame } from '../engine/setup.ts'; import type { Facility, GameConfig, GameState } from '../engine/state.ts'; import { actingPlayer } from '../engine/state.ts'; +import { reasonSentence } from '../web/panels.ts'; import { developerBot, lastChoiceReason } from './bot.ts'; import { carLabel, cuesFor, idleNote, isVisible, narrate } from './narrate.ts'; // The view-model lives in its own module so the browser build can import it without dragging in @@ -148,12 +149,26 @@ export function record(seed: number, length: GameLength, maxSteps = 100_000): Re push(applied.events); } + /** + * IN WORDS, NOT AS AN ENUM (`TODO.md` #34). This heading read `loss — revenueFloor`, which is + * exactly the defect Gitea#16 was filed about on the playable page — it just outlived the fix + * here, because nothing player-facing pointed at it. `reasonSentence` is shared rather than + * reimplemented, so the replay and the results screen cannot end up explaining the same ending + * two different ways. + * + * Fed the LAST frame, which is the state the outcome was decided in and is already recorded. + * Tags are stripped: this lands in an `

` and in a console line, neither of which wants markup. + */ const o = s.outcome; + const last = frames[frames.length - 1]; + const why = o && last ? reasonSentence(last, o, last.day).replace(/<[^>]+>/g, '') : ''; return { seed, length, frames, - outcome: o ? `${o.result} — ${o.reason} · final Revenue ${s.players[0]?.revenue ?? 0}` : 'unfinished', + outcome: o + ? `${o.result === 'win' ? 'won' : 'lost'} — ${why} Final Revenue ${s.players[0]?.revenue ?? 0}.` + : 'unfinished', }; } diff --git a/src/sim/turnchart.ts b/src/sim/turnchart.ts index 5c637c7..e7e1cd7 100644 --- a/src/sim/turnchart.ts +++ b/src/sim/turnchart.ts @@ -19,6 +19,8 @@ export type TurnChartFrame = { phase: string; phaseKey: string; actor: number | null; + /** What the game has stopped to ask, when it has. Null while a phase is simply running. */ + awaiting?: { asks: string; train: string } | null; }; /** @@ -93,8 +95,21 @@ export function turnChartHtml(f: TurnChartFrame, actorName: string | null, super ); }).join(''); - // An automatic phase is waiting on nobody, and saying so is more use than a blank. + /** + * WAITING ON WHOM, AND FOR WHAT. + * + * "nobody — the Division is running itself" is true of an automatic phase and was being printed + * over the top of three interruptions that are emphatically waiting on a person: §8.1's clearance + * ruling, the Yard Office offer and the Red Flag prompt. The Frame carried the phase's actor, + * which is null throughout the Mainline Phase, so a game stopped on a named player's decision + * reported that nobody was holding it up (Jesse, 2026-08-30). `Frame.actor` is `actingPlayer` now + * and answers who; `awaiting` says what, because "waiting on Bob" with no more than that is a + * game that looks stuck to everyone except Bob. + */ const who = actorName ?? 'nobody — the Division is running itself'; + const asked = f.awaiting + ? ` ${esc(f.awaiting.asks)} · ${esc(f.awaiting.train)}` + : ''; const fedora = superName === null ? '' @@ -105,9 +120,12 @@ export function turnChartHtml(f: TurnChartFrame, actorName: string | null, super `
Day ${f.day}Stage ${f.stage} of 12` + `${esc(f.clock)}
` + `
phase ${esc(f.phase)}
` + - `
waiting on ${esc(who)}
` + - fedora + - `
    ${chips}
` + `
waiting on ${esc(who)}${asked}
` + + // THE FEDORA RIDES AT THE END OF THE PHASE ROW (`TODO.md` #29, Jesse). It sat on its own line + // between the phases and everything above them, which put a thing that changes every third + // Stage in the middle of the things that change every Stage. The row it belongs beside is the + // one whose last chip is Supervisor Shift — the phase that passes it. + `
    ${chips}
${fedora}
` ); } @@ -130,12 +148,18 @@ export const TURNCHART_CSS = ` says "Player Solitaire", so the chart should agree. In multiplayer this is the thing a table glances at most often, so it gets its own chip rather than hiding in the phase text. */ .tc-who{display:flex;align-items:center;gap:6px;font-size:12px;color:#8b94a3} +.tc-asks{color:#a99ac4;font-style:italic} .tc-who b{color:#b98cf0;background:rgba(150,110,230,.16);border:1px solid #8b6ad0; border-radius:11px;padding:1px 9px;font-size:12px} /* WHO HOLDS THE FEDORA. Violet like the rest of the chart — this is "where you are" news, not something to press — but unfilled, so the eye still lands on "waiting on" first: that is the one that changes every turn, while this changes four times a Day. */ -.tc-super{display:flex;align-items:center;gap:6px;font-size:12px;color:#8b94a3;cursor:help} +/* The phase row and the Fedora on one line, the hat pushed to the far end (TODO.md #29): the row + is the Stage, and the Superintendent is who holds it. Wraps under the phases on a narrow screen + rather than squeezing the chips. */ +.tc-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap} +.tc-row ol.tc-phases{flex:1 1 auto} +.tc-super{margin-left:auto;display:flex;align-items:center;gap:6px;font-size:12px;color:#8b94a3;cursor:help} .tc-super b{color:#cbb6f2;border:1px solid #6b5a94;border-radius:11px;padding:1px 9px;font-size:12px} ol.tc-phases{display:flex;gap:6px;list-style:none;margin:0;padding:0;flex-wrap:wrap} .tc-phase{display:flex;align-items:center;gap:6px;border:1px solid #2c333d;border-radius:14px; diff --git a/src/sim/view.ts b/src/sim/view.ts index e2cffb1..755ce68 100644 --- a/src/sim/view.ts +++ b/src/sim/view.ts @@ -47,7 +47,7 @@ import { } from '../engine/content.ts'; import type { Intent } from '../engine/intents.ts'; import type { Facility, GameConfig, GameState, PlayerIndex, SeatIndex, TrackCard, TurnoutOrientation } from '../engine/state.ts'; -import { carsOn, playerAtSeat, railFacingOf, seatOf, turnOf } from '../engine/state.ts'; +import { actingPlayer, carsOn, playerAtSeat, railFacingOf, seatOf, turnOf } from '../engine/state.ts'; import type { Hand, HouseRules, TrackGeometry } from '../engine/content.ts'; import type { Port } from '../engine/track.ts'; import { connectionsFor, slopeOfPair, variantsFor } from '../engine/track.ts'; @@ -354,7 +354,21 @@ export type Frame = { * replay recorder, which sees the events; the live game keeps its own on the Game object. */ cues?: string[]; + /** + * WHO THE GAME IS WAITING ON — the phase's actor, or the owner of a pending interruption when + * there is one. It carried `clock.currentActor` alone until 2026-08-30, which is null during the + * Mainline Phase, so a game stopped dead on a Superintendent's clearance ruling reported "waiting + * on nobody — the Division is running itself" while it waited on a named person to click + * (reported by Jesse). The engine had the answer the whole time in `actingPlayer`. + */ actor: number | null; + /** + * WHAT that player is being asked, when the game is stopped on a question rather than a turn. + * Null whenever the phase is simply running. Naming the person is not enough on its own: three + * different interruptions can be waiting, and "waiting on Bob" with no more than that is a game + * that looks stuck to everyone except Bob. + */ + awaiting: { asks: string; train: string } | null; superintendent: number; revenue: number; /** @@ -1342,7 +1356,19 @@ export function snapshot( clock: clockTime(s.clock.stage), phase: phaseLabel(s.clock.phase), phaseKey: s.clock.phase, - actor: s.clock.currentActor, + actor: actingPlayer(s), + /** + * The three interruptions §8.1 and Gitea#5/#19 can raise, said in the words the prompt itself + * uses. `decisionActor` above decides WHO; this is only what they are looking at. + */ + awaiting: (() => { + const d = s.clock.pendingDecision; + if (!d) return null; + const train = trainName(s, d.train); + if (d.kind === 'clearance') return { asks: 'a clearance ruling', train }; + if (d.kind === 'yardOffice') return { asks: 'the Yard Office offer', train }; + return { asks: 'a Red Flag', train }; + })(), superintendent: s.clock.superintendent, revenue: s.players[viewer]?.revenue ?? 0, lines, diff --git a/src/web/lobby.ts b/src/web/lobby.ts index d9f0661..5536548 100644 --- a/src/web/lobby.ts +++ b/src/web/lobby.ts @@ -256,25 +256,21 @@ export function runLobby(handlers: LobbyHandlers, resume?: { token: string; game else if (type === 'custom') type = base; for (const r of typeRadios()) r.checked = r.value === type; - const scoring = preset(base).scoring; - const note = $('lb-type-note'); - if (type === 'custom') { - note.textContent = - `${gameTypeLabel('custom', scoring)} · ${differing.length} ` + - `${differing.length === 1 ? 'setting differs' : 'settings differ'} from ${preset(base).label}.`; - note.className = 'ng-note changed-note'; - // A Custom game is nobody's default: open the block that says how it differs. - $('lb-settings').open = true; - } else { - note.textContent = preset(type as PresetName).blurb; - note.className = 'ng-note'; - } + /** + * NO SENTENCE UNDER THE RADIOS since 2026-08-30 — it restated the type just chosen to the person + * who had just chosen it, and the row is already labelled and already carries its own + * description (Jesse: "There's no need to repeat it below"). `form.mark` still puts a hint on + * each row that actually differs, which is where a Custom game's differences can be acted on. + */ + // A Custom game is nobody's default: open the block that says how it differs. + if (type === 'custom') $('lb-settings').open = true; } for (const r of typeRadios()) { // Solitaire is on this screen so the two screens read as one list, but there is nothing here to - // deal it with — the New Game dialog is where a solitaire game comes from. - if (r.value === 'solitaire') markUnavailable(r, 'dealt with the New game button, not here'); + // deal it with. Dimmed and left to speak for itself: the heading says "Game type + // (multi-player)", which is the explanation (Jesse, 2026-08-30). + if (r.value === 'solitaire') markUnavailable(r); r.onchange = () => { if (!r.checked) return; if (r.value === 'custom') { @@ -615,18 +611,15 @@ export function prefillCode(code: string): void { * * Reported by Jesse 2026-08-23: "solitaire is disabled, but really hard to tell." A bare `disabled` * on a radio leaves the whole row at full strength — the dot simply refuses the click, which reads - * as a broken control rather than an unavailable one. Dims the row and says why, once. + * as a broken control rather than an unavailable one. + * + * The dimming is the whole signal now. It used to append a reason to the row as well, and dropped + * that in 2026-08-30 along with the same text on the solitaire screen: one heading naming which + * game the screen deals says it once, where three dimmed rows each said it again. */ -function markUnavailable(radio: HTMLInputElement, why: string): void { +function markUnavailable(radio: HTMLInputElement): void { radio.disabled = true; - const row = radio.closest('label'); - if (!row) return; - row.classList.add('disabled'); - if (row.querySelector('.lb-why')) return; - const note = document.createElement('span'); - note.className = 'lb-why'; - note.textContent = ` — ${why}`; - row.querySelector('span')?.appendChild(note); + radio.closest('label')?.classList.add('disabled'); } function escapeHtml(s: string): string { diff --git a/src/web/main.ts b/src/web/main.ts index d3d361e..66a0234 100644 --- a/src/web/main.ts +++ b/src/web/main.ts @@ -2019,12 +2019,16 @@ function wireGameTypeBlock(prefix: string, root: ParentNode): WiredGameType { if (differing.length > 0) type = 'custom'; else if (type === 'custom') type = base; for (const r of typeRadios()) r.checked = r.value === type; - const note = field('type-note'); - note.textContent = - type === 'custom' - ? `${gameTypeLabel('custom', preset(base).scoring)} · ${differing.length} ` + - `${differing.length === 1 ? 'setting differs' : 'settings differ'} from ${preset(base).label}.` - : preset(type as PresetName).blurb; + /** + * NO SENTENCE UNDER THE RADIOS. It restated the type just chosen — the row is already labelled + * and already carries its own one-line description — so it was the choice read back to the + * person who had just made it (Jesse, 2026-08-30: "It's obvious from what they selected above + * what they're playing. There's no need to repeat it below."). + * + * The Custom case said something the radios do NOT — how many settings differ, and from which + * type — and that is not lost: `form.mark` puts a hint on each row that actually differs, which + * is where a reader can act on it rather than a count they would then have to go and find. + */ } function selectPreset(name: PresetName): void { @@ -2043,19 +2047,21 @@ function wireGameTypeBlock(prefix: string, root: ParentNode): WiredGameType { } for (const r of typeRadios()) { - // Nothing here can deal a multiplayer game: a `LocalSession` runs the engine in this browser and - // a table needs a server. The lobby is the door, and the row says so rather than just refusing - // the click (Jesse, 2026-08-23 — a disabled radio that looks enabled reads as a broken one). + /** + * Nothing here can deal a multiplayer game: a `LocalSession` runs the engine in this browser and + * a table needs a server. Dimmed rather than hidden, so what this screen offers and what the + * lobby offers read as one list (Jesse, 2026-08-23 — a disabled radio that looks enabled reads + * as a broken one). + * + * NO REASON PRINTED BESIDE THEM since 2026-08-30. Each row used to gain "— use the Multiplayer + * button; a table needs a server", which is three unreachable types each explaining the same + * thing on a screen whose heading already says "Game type (solitaire)". Jesse: "grayed out with + * no additional explanation. The explanation above… is sufficient." The lobby dims Solitaire the + * same way and says nothing either, which is what lets one list serve both screens. + */ if (r.value !== 'solitaire' && r.value !== 'custom') { r.disabled = true; - const row = r.closest('label'); - if (row && !row.querySelector('.lb-why')) { - row.classList.add('disabled'); - const note = document.createElement('span'); - note.className = 'lb-why'; - note.textContent = ' — use the Multiplayer button; a table needs a server'; - row.querySelector('span')?.appendChild(note); - } + r.closest('label')?.classList.add('disabled'); } r.onchange = () => { if (!r.checked) return; diff --git a/src/web/panels.ts b/src/web/panels.ts index 753489d..aeb6680 100644 --- a/src/web/panels.ts +++ b/src/web/panels.ts @@ -254,8 +254,13 @@ function collisionsHtml(f: Frame): string { * game read the words `GAME OVER — revenueFloor`: an internal enum value, printed at the one moment * the game has the player's whole attention. Each reason gets a sentence that says what actually * happened, with this game's own numbers in it. + * + * EXPORTED for the developer replay recorder (`sim/replay.ts`), which was still printing + * `loss — revenueFloor` into its own heading a release after this was written — the same defect the + * issue was filed about, surviving in the one place nobody had looked (`TODO.md` #34). One + * implementation, so the two cannot say the game ended for different reasons. */ -function reasonSentence(f: Frame, o: NonNullable, day: number): string { +export function reasonSentence(f: Frame, o: NonNullable, day: number): string { const combined = f.players.reduce((n, p) => n + p.revenue, 0); switch (o.reason) { case 'daysElapsed': diff --git a/src/web/play.html b/src/web/play.html index 21589ce..d4281e2 100644 --- a/src/web/play.html +++ b/src/web/play.html @@ -106,7 +106,6 @@ section{background:var(--panel);border:1px solid var(--line);border-radius:7px; lobby and nothing on it said so — a radio that silently refuses reads as a broken radio. */ .ng-radio.disabled{opacity:.45;cursor:not-allowed} .ng-radio.disabled:hover{background:none} -.lb-why{color:#e0b060;font-size:11px} #lobby h2,#solitairesetup h2{margin-top:0} #lobby h3,#solitairesetup h3{margin-bottom:2px} .lb-seat{display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid var(--line)} @@ -414,12 +413,13 @@ ul.blocked li{padding:2px 0} can be shared, compared or replayed. Leave it blank for a random one.

-

Every chair has to be taken before the game can start — by a person or by a - bot. Pick the size of the table now; it cannot change once the game is created.

+

Every chair must be filled before the game can start. For solitaire, + there’s only one player. For multiplayer, that must be filled by a person or a + bot. The number of players cannot be changed once the game is created.

-

Game type

+

Game type (multi-player)

@@ -433,7 +433,7 @@ ul.blocked li{padding:2px 0} Custom
Whatever you set below. Selected for you the moment you change a rule; it is scored as the type you started from.
-

+