v0.7.5 — solitaire asks before it deals, the same way multiplayer already does

A new #solitairesetup screen in play.html asks the full shared game-options
block — type, starting hand, Extra start, revenue, victory conditions,
optional rules — before a genuinely fresh visit deals a game. A saved game,
an explicit ?seed=, or a URL a Deal already wrote all skip past it, same as
?lobby already skips the front doors on an invite link.

The in-game dialog, the lobby and this screen now share one
wireGameTypeBlock()/commitNewGame() pair instead of the dialog carrying its
own copy of the questions.

859 tests pass. Not yet played in a browser.

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-29 19:14:37 -04:00
co-authored by Claude Sonnet 5
parent a02d1fcffe
commit 3e961496b0
7 changed files with 465 additions and 126 deletions
+182 -3
View File
@@ -80,7 +80,7 @@ main{display:grid;grid-template-columns:minmax(0,1fr) 400px;gap:14px;padding:14p
@media(max-width:1100px){main{grid-template-columns:1fr}}
section{background:var(--panel);border:1px solid var(--line);border-radius:7px;
padding:10px 12px;margin-bottom:12px}
#lobby{max-width:1040px;margin:0 auto;padding:14px}
#lobby,#solitairesetup{max-width:1040px;margin:0 auto;padding:14px}
/* The create form is two short lists, not one long one: what game this is on the left, what its
rules are on the right. Collapses to one column where there is no room for two. */
.lb-two{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.1fr);gap:22px;align-items:start}
@@ -96,8 +96,8 @@ section{background:var(--panel);border:1px solid var(--line);border-radius:7px;
.ng-radio.disabled{opacity:.45;cursor:not-allowed}
.ng-radio.disabled:hover{background:none}
.lb-why{color:#e0b060;font-size:11px}
#lobby h2{margin-top:0}
#lobby h3{margin-bottom:2px}
#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)}
.lb-seat:last-child{border-bottom:none}
.lb-seat .who{flex:1}
@@ -592,6 +592,185 @@ ul.blocked li{padding:2px 0}
</section>
</div>
<!-- ===================================================================
SOLITAIRE SETUP — the same question multiplayer already asks first,
now asked here too (Jesse, 2026-08-29): a genuinely fresh visit deals
nothing until this screen's own Deal button is pressed. A saved game,
an explicit `?seed=`, or a URL already carrying a Deal's answers (any
of the shared block's fields — `hand` names the one always written)
all skip straight past this screen, exactly as `?lobby` already skips
past it into the lobby: those are not "no plan yet", they are a
choice already made, elsewhere.
THE SAME BLOCK THE DIALOG AND THE LOBBY USE, same shared module
(`settings-form.ts`), same order — three screens are one design now
instead of two. Only Solitaire can be dealt from here, so the other
four types are shown exactly as the in-game dialog shows them: present,
disabled, with a note pointing at the Multiplayer door instead.
==================================================================== -->
<div id="solitairesetup" hidden>
<header><b><a href="./index.html" class="home">Station Master</a></b> — <span class="dim">Solitaire</span></header>
<section>
<h2>New solitaire game</h2>
<p class="ng-note">One railroad, one player, five full days by default — everything below is
yours to change before you deal. Clearing the Revenue floor wins; falling short loses.</p>
<div class="lb-params">
<label class="ng-num"><span>Seed</span>
<input id="ss-seed" type="text" inputmode="numeric" autocomplete="off" placeholder="blank for a random seed"></label>
<label class="ng-num"><span>Days</span>
<input id="ss-days" type="number" min="1" max="20" step="1" value="5"></label>
</div>
<p class="ng-note">The same seed and the same settings always deal the same railroad, so a game
can be shared, compared or replayed. Leave it blank for a random one.</p>
<h3>Game type</h3>
<div class="set-row" id="ss-type-row">
<label class="ng-radio"><input type="radio" name="ss-type" value="solitaire" checked>
<span><b>Solitaire</b><br><span class="dim">One railroad, one player. The whole Division is yours to run.</span></span></label>
<label class="ng-radio"><input type="radio" name="ss-type" value="coop">
<span><b>Co-op</b><br><span class="dim">Everyone&#8217;s Revenue is one table score. You win together or lose together.</span></span></label>
<label class="ng-radio"><input type="radio" name="ss-type" value="competitive">
<span><b>Competitive</b><br><span class="dim">Highest Revenue wins — unless the table misses its combined minimum, and then everyone loses.</span></span></label>
<label class="ng-radio"><input type="radio" name="ss-type" value="cutthroat">
<span><b>Cutthroat</b><br><span class="dim">Highest Revenue wins, and nothing is shared — the only way everyone loses is three collisions in one Day.</span></span></label>
<label class="ng-radio"><input type="radio" name="ss-type" value="custom">
<span><b>Custom</b><br><span class="dim">Whatever you set below. Selected for you the moment you change a rule; it is scored as the type you started from.</span></span></label>
</div>
<p class="ng-note" id="ss-type-note"></p>
<details id="ss-settings" open>
<summary>Game settings</summary>
<p class="ng-note">Every rule the game type sets, and every one of them yours to change.
Changing any of them selects <b>Custom</b>; clicking a type again resets all of them back
to it.</p>
<div class="set-groups">
<div class="set-group">
<h3>Starting hand</h3>
<p class="ng-note">What you are dealt before the first turn. The hand limit is three either
way — deal six and the first turn is spent choosing which of them to keep.</p>
<div class="set-row" id="ss-hand-row">
<label class="ng-radio"><input type="radio" name="ss-hand" value="threeRandom">
<span><b>Three random cards</b><br><span class="dim">The original rule. At the hand limit already, and no guarantee of track.</span></span></label>
<label class="ng-radio"><input type="radio" name="ss-hand" value="sixRandom" checked>
<span><b>Six random cards</b><br><span class="dim">Twice the choice, still no guaranteed track — the first turn is a discard.</span></span></label>
<label class="ng-radio"><input type="radio" name="ss-hand" value="threeTrackThreeOther">
<span><b>Three random track and three random non-track cards</b><br><span class="dim">Dealt from two piles, so the district you can build is dealt rather than waited for.</span></span></label>
<span class="set-hint" id="ss-hand-hint"></span>
</div>
</div>
<div class="set-group">
<h3>Where an Extra may start</h3>
<p class="ng-note">The player who plays an Extra Train card chooses where its Crew Tray goes,
and the place decides which way it runs — a Division Point sends it away from itself; in the
middle of the railroad the player picks east or west. The Division Points and the Interchange
belong to nobody and are always available. Starting one inside a district is the part that
favours a seat, so it is set here. An Office must be a Control Point whatever this says: a
Whistle Post never qualifies.</p>
<div class="set-row" id="ss-extra-row">
<label class="ng-radio"><input type="radio" name="ss-extra" value="divisionPointsOnly">
<span><b>Division Points and the Interchange only</b><br><span class="dim">The strictest reading. Every Extra begins on shared ground.</span></span></label>
<label class="ng-radio"><input type="radio" name="ss-extra" value="ownOffice">
<span><b>Also the playing player&#8217;s own Control Point</b><br><span class="dim">You may start one at home, but not in somebody else&#8217;s district.</span></span></label>
<label class="ng-radio"><input type="radio" name="ss-extra" value="anyOffice">
<span><b>Also any player&#8217;s Control Point</b><br><span class="dim">The most permissive — an Extra may be planted in another player&#8217;s district.</span></span></label>
<span class="set-hint" id="ss-extra-hint"></span>
</div>
</div>
<div class="set-group">
<h3>Revenue</h3>
<p class="ng-note">What each piece of work pays, 0 to 5. A coach pays when it is boarded and
again when it is detrained; a load pays when it is made up and again when it is broken. Zero
switches an economy off so the others can be read.</p>
<div class="set-row" id="ss-passenger-row">
<label class="ng-num"><span>Passenger revenue per coach</span>
<input id="ss-passenger" type="number" min="0" max="5" step="1" value="1"></label>
<span class="set-hint" id="ss-passenger-hint"></span>
</div>
<div class="set-row" id="ss-freight-row">
<label class="ng-num"><span>Freight revenue per load</span>
<input id="ss-freight" type="number" min="0" max="5" step="1" value="1"></label>
<span class="set-hint" id="ss-freight-hint"></span>
</div>
<div class="set-row" id="ss-transit-row">
<label class="ng-num"><span>Train revenue per transit</span>
<input id="ss-transit" type="number" min="0" max="5" step="1" value="0"></label>
<span class="set-hint" id="ss-transit-hint"></span>
</div>
<p class="ng-note">A transit pays every player, once, when a train runs off the end of the
Division — the one thing nobody has to work for.</p>
</div>
<div class="set-group">
<h3>Victory conditions</h3>
<p class="ng-note">The ways this game can end badly. How long it runs is set above, in Days.</p>
<div class="set-row" id="ss-minrev-row">
<label class="ng-gate"><input type="checkbox" id="ss-minrev-on" checked>
<span>You lose if Revenue at the end is under</span>
<input id="ss-minrev" type="number" min="0" step="1" class="gate-num"></label>
<span class="set-hint" id="ss-minrev-hint"></span>
</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>
<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>
<input id="ss-coltotal" type="number" min="0" step="1" class="gate-num"></label>
<span class="set-hint" id="ss-coltotal-hint"></span>
</div>
<p class="ng-note">The opponent-directed cards — Derail, Watertower, Hobo Jungle and the
nineteen others, along with the seven that answer them — are not implemented yet, so no game
type deals them whatever else is set here.</p>
</div>
<div class="set-group">
<h3>Optional rules</h3>
<p class="ng-note">Off in every game type; each one changes how the game plays.</p>
<div class="set-row" id="ss-visibility-row">
<label class="ng-num"><span>Reduced Visibility — five switching Moves instead of six in the
night Stages (1&ndash;3 and 11&ndash;12)</span>
<input id="ss-visibility" type="checkbox"></label>
<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>
<input id="ss-rotation" type="checkbox" disabled></label>
<span class="set-hint" id="ss-rotation-hint"></span>
</div>
<div class="set-row" id="ss-toolbox-row">
<label class="ng-num"><span>Emergency Toolbox — start holding a Red Flag, so a hand of four;
play or discard down to three on the first turn</span>
<input id="ss-toolbox" type="checkbox"></label>
<span class="set-hint" id="ss-toolbox-hint"></span>
</div>
<div class="set-row" id="ss-tossloco-row">
<label class="ng-num"><span>A Timetabled train may be discarded — toss it face-up to a
Department slot. Turn this off and a train card can only ever be played onto the timetable.
An Extra is never discardable either way</span>
<input id="ss-tossloco" type="checkbox"></label>
<span class="set-hint" id="ss-tossloco-hint"></span>
</div>
</div>
</div>
</details>
<menu class="ng-buttons">
<button id="ss-deal" type="button">Deal</button>
</menu>
</section>
</div>
<div id="gameui" hidden>
<div class="topbar">
<header>