v0.7.9 — solitaire setup screen feedback, and the dead settings it exposed

Four pieces of feedback from Jesse on the solitaire setup screen.

THE COLLISION LIMITS DID NOTHING IN SOLITAIRE. Asked to reword those
entries to "the game ends immediately and results in a loss", which was
unwriteable: advance.ts gated the §3.4 check on competitive/coop, and a
solitaire game's mode is 'solitaire'. Both limits were offered as live
settings, rode into the config, and never fired — the existing text was
already false. The exclusion was never a stated rule and nothing
recorded a reason for it. Jesse's ruling: the settings do what they say,
so the gate is gone rather than the controls.

Measured, not asserted — 200 standard developer-bot games:
loss/collisionFloor 1 in 200, Days played 5.00 -> 4.98 mean with a
minimum of 1, collisions per game unchanged at 0.14. Recorded in
TODO.md under Play Balance, since full-length figures predate it.

Extra start defaults to ownOffice: at one seat it is the same rule as
anyOffice (apply.ts only rejects another seat's start), so this is a
label fix with no gameplay effect.

Also: collision wording on all three screens, Employee Rotation reads
"not applicable for solitaire", and the save warning is legible at 14px
on an amber panel with buttons that say Continue Existing Saved Game and
Deal New Game.

869 tests pass, two new; one asserted the opposite of the ruling and
says so where it was reversed.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX
This commit is contained in:
Jesse.Markowitz
2026-08-30 01:01:11 -04:00
co-authored by Claude Sonnet 5
parent 193800a649
commit f2c87b6871
9 changed files with 148 additions and 27 deletions
+19 -5
View File
@@ -1636,11 +1636,25 @@ function shiftChange(s: GameState, events: GameEvent[]): AdvanceResult {
events.push({ type: 'stageBegan', day: s.clock.day, stage: s.clock.stage });
}
// §3.4 — every mode but competitive-and-coop-only: a Day's collisions against `maxCollisionsPerDay`
// and the game's running total against `maxCollisionsTotal`. `0` disables either check. Flat, not
// scaled by player count — Jesse's call, 2026-08-20: more players is more independent chances to
// collide, not a bigger shared budget.
if (s.config.mode === 'competitive' || s.config.mode === 'coop') {
/**
* §3.4 — EVERY MODE, SOLITAIRE INCLUDED: a Day's collisions against `maxCollisionsPerDay` and the
* game's running total against `maxCollisionsTotal`. `0` disables either check. Flat, not scaled
* by player count — Jesse's call, 2026-08-20: more players is more independent chances to collide,
* not a bigger shared budget.
*
* SOLITAIRE WAS EXCLUDED UNTIL 2026-08-30 and nothing said so. The gate here read `mode ===
* 'competitive' || mode === 'coop'`, while `SOLO_CONFIG` carried both limits and the New Game
* dialog offered them as live settings — so a solitaire player could set a collision limit, read
* "the game ends in a loss" beside it, and crash as often as they liked. Found reviewing that
* screen's wording (Jesse, 2026-08-30); his ruling is that the settings should do what they say,
* so the gate is gone rather than the controls.
*
* A SOLITAIRE GAME CAN THEREFORE NOW END EARLY, which no measurement in `TODO.md` was taken
* under. At the shipped defaults (3 a Day, 5 total) it is a rare ending rather than a common one —
* the bot averages 0.06 collisions a game — but any figure quoted from a full-length run predates
* it.
*/
{
const perDayBreach =
s.config.maxCollisionsPerDay > 0 && s.collisionsToday >= s.config.maxCollisionsPerDay;
const totalBreach =
+23 -12
View File
@@ -76,6 +76,17 @@ dialog input:focus{outline:none;border-color:#4d6fa8}
padding:5px 14px;cursor:pointer;font:inherit;font-size:13px}
.ng-buttons button:hover{border-color:#4d6fa8}
#ng-deal{background:#31527f;border-color:#4d6fa8}
/* The save warning is the one thing on this screen that describes something IRREVERSIBLE, and it
sat in `.ng-note` — the same dim 11px grey as the twenty explanatory notes above it, which is
where the eye has already learned there is nothing to act on. Sized and coloured to be read
(Jesse, 2026-08-30). Amber rather than red: losing a saved game is a real cost, not a danger, and
red here would outrank the actual rules of the game sitting above it. */
#ss-saved-note{font-size:14px;line-height:1.5;color:#f0c674;background:#2e2718;
border:1px solid #7a6320;border-left-width:4px;border-radius:5px;padding:10px 12px;margin:16px 0 0}
/* Two live choices, so neither is the quiet one: `Deal New Game` keeps the primary blue it has when
it is the only button, and `Continue` is given the same weight rather than reading as a cancel. */
#ss-deal{background:#31527f;border-color:#4d6fa8}
#ss-resume{background:#2f5340;border-color:#4f8a68}
main{display:grid;grid-template-columns:minmax(0,1fr) 400px;gap:14px;padding:14px;align-items:start}
@media(max-width:1100px){main{grid-template-columns:1fr}}
section{background:var(--panel);border:1px solid var(--line);border-radius:7px;
@@ -505,13 +516,13 @@ ul.blocked li{padding:2px 0}
</div>
<div class="set-row" id="lb-colday-row">
<label class="ng-gate"><input type="checkbox" id="lb-colday-on" checked>
<span>The game ends and everyone loses if collisions in one Day reach</span>
<span>The game ends immediately and results in a loss if collisions in one Day reach</span>
<input id="lb-colday" type="number" min="0" step="1" class="gate-num"></label>
<span class="set-hint" id="lb-colday-hint"></span>
</div>
<div class="set-row" id="lb-coltotal-row">
<label class="ng-gate"><input type="checkbox" id="lb-coltotal-on" checked>
<span>The game ends and everyone loses after this many collisions in the whole game</span>
<span>The game ends immediately and results in a loss after this many collisions in the whole game</span>
<input id="lb-coltotal" type="number" min="0" step="1" class="gate-num"></label>
<span class="set-hint" id="lb-coltotal-hint"></span>
</div>
@@ -717,13 +728,13 @@ ul.blocked li{padding:2px 0}
</div>
<div class="set-row" id="ss-colday-row">
<label class="ng-gate"><input type="checkbox" id="ss-colday-on" checked>
<span>The game ends in a loss if collisions in one Day reach</span>
<span>The game ends immediately and results in a loss if collisions in one Day reach</span>
<input id="ss-colday" type="number" min="0" step="1" class="gate-num"></label>
<span class="set-hint" id="ss-colday-hint"></span>
</div>
<div class="set-row" id="ss-coltotal-row">
<label class="ng-gate"><input type="checkbox" id="ss-coltotal-on" checked>
<span>The game ends in a loss after this many collisions in the whole game</span>
<span>The game ends immediately and results in a loss after this many collisions in the whole game</span>
<input id="ss-coltotal" type="number" min="0" step="1" class="gate-num"></label>
<span class="set-hint" id="ss-coltotal-hint"></span>
</div>
@@ -742,8 +753,7 @@ ul.blocked li{padding:2px 0}
<span class="set-hint" id="ss-visibility-hint"></span>
</div>
<div class="set-row" id="ss-rotation-row">
<label class="ng-num"><span>Employee Rotation — meaningless at a table of one, shown here so
this screen and the lobby read as one list</span>
<label class="ng-num"><span>Employee Rotation — not applicable for solitaire</span>
<input id="ss-rotation" type="checkbox" disabled></label>
<span class="set-hint" id="ss-rotation-hint"></span>
</div>
@@ -769,12 +779,13 @@ ul.blocked li{padding:2px 0}
save (`commitNewGame` calls `clearSave`), so without a way back the door would be a way to
lose a game in progress — and the door is reached by clicking "Play solitaire", which nobody
reads as "discard what I was playing". -->
<p class="ng-note" id="ss-saved-note" hidden>You have a solitaire game in progress. Dealing a new
one below replaces it &mdash; there is no undo for that.</p>
<p id="ss-saved-note" hidden><b>You have a solitaire game in progress.</b> Dealing a new game
replaces it permanently &mdash; there is no undo. Choose <b>Continue Existing Saved Game</b>
to pick it up where you left off.</p>
<menu class="ng-buttons">
<button id="ss-resume" type="button" hidden>Continue saved game</button>
<button id="ss-deal" type="button">Deal</button>
<button id="ss-resume" type="button" hidden>Continue Existing Saved Game</button>
<button id="ss-deal" type="button">Deal New Game</button>
</menu>
</section>
</div>
@@ -1015,13 +1026,13 @@ ul.blocked li{padding:2px 0}
</div>
<div class="set-row" id="ng-colday-row">
<label class="ng-gate"><input type="checkbox" id="ng-colday-on" checked>
<span>The game ends and everyone loses if collisions in one Day reach</span>
<span>The game ends immediately and results in a loss if collisions in one Day reach</span>
<input id="ng-colday" type="number" min="0" step="1" class="gate-num"></label>
<span class="set-hint" id="ng-colday-hint"></span>
</div>
<div class="set-row" id="ng-coltotal-row">
<label class="ng-gate"><input type="checkbox" id="ng-coltotal-on" checked>
<span>The game ends and everyone loses after this many collisions in the whole game</span>
<span>The game ends immediately and results in a loss after this many collisions in the whole game</span>
<input id="ng-coltotal" type="number" min="0" step="1" class="gate-num"></label>
<span class="set-hint" id="ng-coltotal-hint"></span>
</div>
+8 -2
View File
@@ -119,8 +119,14 @@ export const PRESETS: readonly Preset[] = [
revenueFloor: (players, days) => collectiveRevenueFloor(players, days),
rules: {
startingHand: SIX,
// Nobody else's district exists, so "any Control Point" and "your own" are the same rule.
extraStart: 'anyOffice',
/**
* Nobody else's district exists, so "any Control Point" and "your own" are the same rule —
* `apply.ts` only ever rejects `ownOffice` when `start.seat !== seatOf(s, player)`, which
* cannot happen at one seat. It said `anyOffice` until 2026-08-30, which was true and read
* wrong: a solitaire player has no "any player" to contrast themselves with, so the permissive
* label described a permission nobody was being granted. Jesse's call; no gameplay effect.
*/
extraStart: 'ownOffice',
passengerPerCoach: 1,
freightPerLoad: 1,
trainPerTransit: 0,