Third report of the same symptom, this time with the build confirmed current on screen, which ruled out v0.7.7's caching fault and left the real cause exposed. v0.7.5 skipped the setup screen whenever load() found a save, reasoned as "a saved game is a game to resume". A browser that has ever played solitaire always has one, so the door could never reach the screen again — and the fresh private window that appeared to vindicate v0.7.7 simply had no save. Two real faults were stacked; the caching one is fixed and had been masking this. The door outranks a saved game now: ?solitaire is a request to set one up, while a bare reload still resumes (pinned by its own test). Since Deal clears the save, the screen carries #ss-resume and says what Deal costs, so the door cannot destroy a game in progress. Also, per Jesse, riding along rather than taking its own release: the splash footer now names both ways to play. 868 tests pass, four new. The reproduction was a failing test written before the fix. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX
1115 lines
70 KiB
HTML
1115 lines
70 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Station Master — play</title>
|
||
<style>
|
||
:root{--bg:#12151a;--fg:#e6e9ee;--dim:#8b94a3;--line:#2c333d;--panel:#1a1f26;--run:#3a4250;--now:#b98cf0}
|
||
*{box-sizing:border-box}
|
||
body{margin:0;background:var(--bg);color:var(--fg);
|
||
font:14px/1.45 ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
|
||
h1{font-size:17px;margin:0}
|
||
h2{font-size:12px;text-transform:uppercase;letter-spacing:.09em;color:var(--dim);
|
||
margin:0 0 6px;font-weight:600}
|
||
h2.big{font-size:15px;text-transform:none;letter-spacing:.01em;color:#cfe0f5;
|
||
margin:0 0 9px;padding-bottom:5px;border-bottom:1px solid #2c333d}
|
||
h3{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:var(--dim);margin:9px 0 4px}
|
||
.dim{color:var(--dim)}
|
||
/* THE TOP TWO ROWS TRAVEL TOGETHER.
|
||
The title bar was sticky and the turn chart under it was not, so scrolling down took away the one
|
||
thing you most need while reading the board — which Day and Stage it is, which phase is running,
|
||
and who it is waiting on. They are one block now, and it sticks as one. */
|
||
.topbar{position:sticky;top:0;z-index:5}
|
||
/* ONE LINE, AND IT HAS TO STAY ONE LINE. The build stamp was pushed to the far right by
|
||
`margin-left:auto` and wrapped the whole row onto a second line the moment the content grew.
|
||
Tighter gaps, a smaller stamp, and nothing on here that is not worth the width. */
|
||
header{background:var(--panel);border-bottom:1px solid var(--line);
|
||
padding:8px 12px;display:flex;gap:13px;align-items:baseline;flex-wrap:wrap;font-size:13px}
|
||
header b{font-size:16px}
|
||
header button{background:#2a3038;color:inherit;border:1px solid #2c333d;border-radius:5px;padding:3px 9px;cursor:pointer;font:inherit;font-size:12px}
|
||
header button:hover{border-color:#4d6fa8}
|
||
/* Nothing to take back yet. It keeps its place — a button that disappears reads as a bug — but it
|
||
stops inviting the press. */
|
||
header button:disabled{opacity:.45;cursor:not-allowed;border-color:#2c333d}
|
||
header button:disabled:hover{border-color:#2c333d}
|
||
.zoom{display:inline-flex;align-items:center;gap:4px}
|
||
.zoom button{padding:3px 9px;line-height:1}
|
||
.lb-invite{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin:0 0 10px;
|
||
background:#1e242c;border:1px solid var(--line);border-radius:7px;padding:10px 12px}
|
||
.lb-invite-label{font-size:11px;color:var(--dim)}
|
||
.lb-invite-code{font-size:22px;font-weight:700;letter-spacing:.08em;color:#f2e6cf}
|
||
#lb-settings{margin:10px 0;border:1px solid var(--line);border-radius:7px;padding:8px 12px;background:#171c23}
|
||
#lb-settings summary{cursor:pointer;font-size:13px;color:#9fb6d8}
|
||
#lb-settings h3{font-size:12px;margin:12px 0 4px;color:#9fb6d8}
|
||
.zoom #zoomlabel{font-size:11px;color:var(--dim);min-width:32px;text-align:center;display:inline-block}
|
||
.build{margin-left:auto;font-size:10px;opacity:.55;white-space:nowrap}
|
||
.home{color:inherit;text-decoration:none;border-bottom:1px dotted #5f6b7a}
|
||
.home:hover{color:#5aa9e6}
|
||
.pace{font-size:12px;padding:1px 7px;border-radius:10px}
|
||
.pace.good{background:rgba(40,140,60,.32)}
|
||
.pace.behind{background:rgba(190,120,40,.28)}
|
||
.yard.bare{outline:1px dashed #e0a060;outline-offset:3px;border-radius:4px;padding:3px}
|
||
/* NEW GAME DIALOG. Native <dialog>, so Esc closes it and focus is trapped without any of that
|
||
being written here. Everything below is only colour and spacing: the browser's own white-on-white
|
||
default is unreadable against this page. */
|
||
dialog{background:var(--panel);color:var(--fg);border:1px solid var(--line);border-radius:9px;
|
||
padding:16px 18px;max-width:520px;width:calc(100% - 32px);font:inherit;max-height:86vh;overflow:auto}
|
||
dialog::backdrop{background:rgba(0,0,0,.62)}
|
||
dialog h3{margin-top:15px}
|
||
/* The rationale under each heading, not beside each control — the reasons are sentences, and a
|
||
sentence squeezed into a label column wraps into noise. */
|
||
.ng-note{color:var(--dim);font-size:11.5px;margin:0 0 8px;line-height:1.45}
|
||
dialog input[type=text],dialog input[type=number]{background:#0f1318;color:var(--fg);
|
||
border:1px solid var(--line);border-radius:5px;padding:4px 7px;font:inherit;font-size:13px}
|
||
dialog input[type=text]{width:100%}
|
||
dialog input[type=number]{width:64px;text-align:right}
|
||
dialog input:focus{outline:none;border-color:#4d6fa8}
|
||
/* Whole rows, so the click target is the sentence and not the 13px circle beside it. */
|
||
.ng-radio{display:flex;gap:9px;align-items:flex-start;padding:6px 7px;border-radius:5px;cursor:pointer}
|
||
.ng-radio:hover{background:#20262e}
|
||
.ng-radio input{margin-top:3px;flex:0 0 auto}
|
||
.ng-radio .dim{font-size:11.5px}
|
||
.ng-num{display:flex;justify-content:space-between;align-items:center;gap:12px;padding:4px 7px}
|
||
.ng-buttons{display:flex;justify-content:flex-end;gap:8px;margin:16px 0 0;padding:0}
|
||
.ng-buttons button{background:#2a3038;color:inherit;border:1px solid var(--line);border-radius:5px;
|
||
padding:5px 14px;cursor:pointer;font:inherit;font-size:13px}
|
||
.ng-buttons button:hover{border-color:#4d6fa8}
|
||
#ng-deal{background:#31527f;border-color:#4d6fa8}
|
||
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;
|
||
padding:10px 12px;margin-bottom:12px}
|
||
#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}
|
||
@media(max-width:860px){.lb-two{grid-template-columns:1fr;gap:0}}
|
||
.lb-col{min-width:0}
|
||
.lb-span{grid-column:1/-1;min-width:0}
|
||
/* The rules in as many columns as the width allows — one in the New Game dialog, which is narrow. */
|
||
.set-groups{display:grid;grid-template-columns:repeat(auto-fit,minmax(290px,1fr));gap:0 26px;align-items:start}
|
||
.set-group{min-width:0;break-inside:avoid}
|
||
.set-group h3:first-child{margin-top:4px}
|
||
/* A DISABLED CHOICE HAS TO LOOK DISABLED. Reported by Jesse: Solitaire is not selectable in the
|
||
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)}
|
||
.lb-seat:last-child{border-bottom:none}
|
||
.lb-seat .who{flex:1}
|
||
#presence{color:#e0b060;font-size:12px;padding:0 14px;empty-cells:hide}
|
||
#presence:empty{display:none}
|
||
/* division strip */
|
||
#division{display:flex;gap:7px;overflow-x:auto;padding-bottom:4px}
|
||
.node{border:1px solid var(--line);border-radius:6px;padding:6px 9px;min-width:112px;flex:0 0 auto}
|
||
.node.office{border-color:#4d6fa8;background:rgba(60,100,170,.13)}
|
||
.node .nm{font-weight:600;font-size:12px}
|
||
.chip{display:inline-block;background:#2f6b3d;border-radius:4px;padding:1px 6px;margin:3px 3px 0 0;font-size:11px}
|
||
.consist{font-size:10px;color:var(--dim)}
|
||
.grade{font-size:10px;background:rgba(200,90,60,.35);border-radius:3px;padding:1px 4px;font-weight:400}
|
||
.mlmods{font-size:10px;background:rgba(200,150,60,.28);border-radius:3px;padding:1px 4px;margin-top:2px}
|
||
/* board */
|
||
#grid{display:grid;gap:5px;overflow-x:auto}
|
||
.cell{border:1px solid var(--line);border-radius:5px;padding:5px 6px;min-height:64px;background:#161b21}
|
||
.cell.run{background:var(--run)}
|
||
.cell.office{border-color:#4d6fa8}
|
||
.cell.modifier{background:#2b2333;border-style:dashed}
|
||
.cell.legal{outline:2px solid #5aa9e6;outline-offset:-2px;cursor:pointer;
|
||
box-shadow:0 0 0 3px rgba(90,169,230,.18) inset}
|
||
.cell.legal:hover{background:#233246}
|
||
.cell.ghost{border-style:dashed;background:rgba(90,169,230,.07);
|
||
display:flex;flex-direction:column;justify-content:center;align-items:center;text-align:center}
|
||
.cell.ghost .nm{font-weight:400;color:#5aa9e6;font-size:11px}
|
||
.cell .nm{font-size:12px;font-weight:600}
|
||
.cell .what{font-size:10px;line-height:1.25;margin-top:2px}
|
||
.cell{cursor:help}
|
||
.coord{font-size:10px}
|
||
.enh{font-size:10px;background:rgba(90,140,220,.30);border-radius:3px;padding:1px 4px;margin-top:2px}
|
||
.crew{font-size:11px;margin-top:3px;color:#8fd6a0}
|
||
.cars{font-size:10px;color:var(--dim)}
|
||
/* THE HAND IS THE ACTION SURFACE. Each card carries its own verbs, so the twelve-button discard
|
||
block and the separate "play a card" subject list are both gone. The buttons are small and quiet
|
||
until the card is picked — the card is the thing being read, they are what you do with it. */
|
||
.handcard .cardacts{display:flex;gap:4px;margin-top:5px}
|
||
button.cardact{margin:0;padding:1px 7px;font-size:10px;background:#2f3a48;border:1px solid #46536440;
|
||
border-radius:9px;color:#cfd6e0;text-align:center}
|
||
button.cardact:hover{background:#3d4c60;border-color:#5aa9e6}
|
||
button.cardact.on{background:#2b3444;border-color:#c8912f;color:#f2e6cf;font-weight:700}
|
||
button.cardact.discard{color:#d6b48a}
|
||
.handcard.picked{border-color:#c8912f;box-shadow:0 0 0 2px rgba(200,145,47,.18)}
|
||
#phasenote{position:fixed;left:50%;top:14px;transform:translateX(-50%) translateY(-14px);z-index:40;
|
||
background:#241d33;border:1px solid var(--now);color:#e7dcfb;border-radius:14px;padding:5px 16px;
|
||
font-size:13px;font-weight:700;opacity:0;pointer-events:none;transition:opacity .18s,transform .18s;
|
||
box-shadow:0 6px 22px rgba(0,0,0,.5)}
|
||
#phasenote.shown{opacity:1;transform:translateX(-50%) translateY(0)}
|
||
@media(prefers-reduced-motion:reduce){#phasenote{transition:none}}
|
||
/* A train completing its run pays EVERY player, so it is announced rather than left to the log —
|
||
green like a revenue gain, and its own element so it cannot fight the phase note for the slot. */
|
||
#announce{position:fixed;left:50%;top:14px;transform:translateX(-50%) translateY(-14px);z-index:41;
|
||
background:#16301f;border:1px solid #4c9a6a;color:#c8f0d8;border-radius:14px;padding:7px 18px;
|
||
font-size:14px;font-weight:700;opacity:0;pointer-events:none;transition:opacity .18s,transform .18s;
|
||
box-shadow:0 6px 22px rgba(0,0,0,.5);max-width:min(680px,90vw);text-align:center}
|
||
#announce.shown{opacity:1;transform:translateX(-50%) translateY(0)}
|
||
@media(prefers-reduced-motion:reduce){#announce{transition:none}}
|
||
/* The die's answer, said where the player is looking. Green like a revenue gain — it is a good
|
||
thing that just happened — and it disappears with the next render, as a moment should. */
|
||
.scheduled{display:block;font-size:11.5px;margin:0 0 7px;padding:3px 9px;border-radius:5px;
|
||
background:rgba(40,140,60,.22);border:1px solid #2f6b47;color:#bfe8cd;font-weight:600}
|
||
.makeup-note{font-size:11px;margin:0 0 5px}
|
||
/* WHICH TRAIN AM I SWITCHING. A row of crews rather than a stacked list — they are alternatives,
|
||
and the chosen one is the crew whose squares the board is drawing, so it wears the same violet
|
||
"you are here" the rest of the page uses. */
|
||
.crewpick{display:flex;flex-wrap:wrap;gap:6px;align-items:baseline}
|
||
.crewpick h3{flex:0 0 100%;margin:0 0 2px}
|
||
button.act.crew{width:auto;display:inline-block;margin:0}
|
||
button.act.crew.on{border-color:var(--now);background:rgba(185,140,240,.18);color:#e9dcff}
|
||
/* THE ORDER TO ADD THE CARS IN. Only ever drawn for a train whose make-up order can lock it (7/8
|
||
Local), so it is worth a coloured rule down the side: a player who sees this once needs to read
|
||
it, and will not see it again on the next train. `warn` is the train already made up badly. */
|
||
.makeup-advice{font-size:11px;line-height:1.45;margin:0 0 6px;padding:5px 8px;border-radius:4px;
|
||
border-left:3px solid #4d6fa8;background:rgba(60,100,170,.13)}
|
||
.makeup-advice.warn{border-left-color:#e0a060;background:rgba(200,150,60,.15)}
|
||
/* Moves left, beside the moves. Amber like the actions, because it is a budget being spent by the
|
||
buttons underneath it; struck through in red once it is gone. */
|
||
.moves{display:inline-block;font-size:11px;margin:0 0 7px;padding:1px 8px;border-radius:10px;
|
||
background:rgba(200,145,47,.16);border:1px solid #8a6d1f;color:#f2e6cf}
|
||
.moves.spent{background:rgba(190,50,50,.20);border-color:#8a4040;color:#e58080}
|
||
/* actions */
|
||
#actions{max-height:none}
|
||
.grp{margin-bottom:6px}
|
||
/* THESE ARE THE THINGS YOU CAN DO. An action carried the same grey border as every other panel
|
||
on the page, so the one region that is clickable did not look it. Amber border and a lit face,
|
||
used nowhere else, so "this is a move" is answered before the label is read. */
|
||
button{background:#2a3038;color:var(--fg);border:1px solid var(--line);border-radius:5px;
|
||
padding:5px 9px;margin:2px 3px 2px 0;cursor:pointer;font:inherit;font-size:12px;text-align:left}
|
||
button:hover{background:#39424e;border-color:#4d6fa8}
|
||
/* GENERIC, and it was not. `header button:disabled` and `#actions button:disabled` were the only
|
||
disabled styles on the page, so a disabled button anywhere else — #lb-start being the one that
|
||
mattered — kept its normal face AND still lit up under the cursor from the rule above. It was
|
||
advertising a click it would refuse. */
|
||
button:disabled{opacity:.45;cursor:not-allowed}
|
||
button:disabled:hover{background:#2a3038;border-color:var(--line)}
|
||
#actions button{background:#2b3444;border:2px solid #c8912f;box-shadow:0 0 0 1px rgba(200,145,47,.18);
|
||
color:#f2e6cf;font-weight:600}
|
||
#actions button:hover{background:#3a4a63;border-color:#f0b64a;box-shadow:0 0 0 3px rgba(240,182,74,.20)}
|
||
#actions button:focus-visible{outline:2px solid #f0b64a;outline-offset:2px}
|
||
button.act{display:inline-block}
|
||
/* The district collapses when nothing in it is changing. A summary line stays, because a panel
|
||
that vanishes entirely reads as broken rather than as folded away. */
|
||
button.ghost{background:#222831;border:1px solid #4a5361;color:#c6ccd6;font-size:11px;
|
||
padding:2px 9px;margin-left:10px;text-transform:none;letter-spacing:0;vertical-align:middle}
|
||
button.ghost:hover{border-color:#4d6fa8;color:var(--fg)}
|
||
#district.folded #grid{display:none}
|
||
#district.folded .districtrule{display:none}
|
||
/* Said once, quietly, beside the thing it governs — a rule a player needs on their first district
|
||
and never again, so it must not shout. */
|
||
.districtrule{font-size:11px;line-height:1.4;color:#8b94a3;margin:0 0 7px;padding:5px 8px;
|
||
border-left:2px solid #39424e;background:#171c23;border-radius:0 4px 4px 0}
|
||
.districtrule b{color:#cfd6e0}
|
||
#district.folded #districtsummary{display:block;padding:2px 0 1px;font-size:12px}
|
||
#districtsummary{display:none}
|
||
/* An action you cannot take yet keeps its place but drops its light — the amber means "press me",
|
||
so a disabled button must not wear it. */
|
||
#actions button.blocked,#actions button:disabled{background:#232830;border:1px dashed #4a5361;
|
||
color:#7d8794;box-shadow:none;cursor:not-allowed;font-weight:400}
|
||
#actions button.blocked:hover{background:#232830;border-color:#4a5361;box-shadow:none}
|
||
h3.actions-hd{font-size:13px;text-transform:none;letter-spacing:.01em;color:#cfe0f5;
|
||
margin:0 0 7px;font-weight:700}
|
||
.over{padding:9px;border-radius:5px;font-weight:700;margin-bottom:8px}
|
||
.over.win{background:rgba(40,140,60,.35)}
|
||
.over.loss{background:rgba(160,60,60,.3)}
|
||
/* THE EXTENSION VOTE (Gitea#11) — unanimous, so who has not answered yet is the useful half. */
|
||
.vote-tally{display:flex;flex-wrap:wrap;gap:4px 12px;margin:0 0 9px;font-size:12px}
|
||
.vote.yes{color:#8fd6a0}
|
||
.vote.no{color:#d98f8f}
|
||
.vote.wait{color:var(--dim)}
|
||
/* Wider than the New Game dialog: the results carry a seven-column per-player table (Gitea#16). */
|
||
#resultsdlg{max-width:640px}
|
||
#resultsdlg table{max-width:100%}
|
||
/* cards, log, blocked */
|
||
.card.gone{opacity:.35;text-decoration:line-through}
|
||
.subj{display:block;width:100%;margin:2px 0}
|
||
.subj.open{border-color:#4d6fa8;background:#39424e}
|
||
.spots{margin:0 0 6px 12px;padding-left:8px;border-left:2px solid #4d6fa8}
|
||
.card{display:inline-block;background:#242c36;border:1px solid var(--line);border-radius:4px;
|
||
padding:2px 6px;margin:2px 2px 0 0;font-size:11px}
|
||
#log{max-height:230px;overflow:auto;font-size:12px}
|
||
.line{padding:1px 0}
|
||
.t-good{color:#8fd6a0}.t-bad{color:#e58080}.t-clock{color:#9fb6d8;font-weight:600}.t-quiet{color:var(--dim)}
|
||
/* A phase marker is a HEADING inside the log, and takes the same violet the turn chart uses for
|
||
"where you are" — the two are saying the same thing in different places. `act` is what the
|
||
player did, so it stays the brightest thing in the column. */
|
||
.t-phase{color:var(--now);font-weight:700;margin-top:4px;letter-spacing:.02em}
|
||
.t-act{color:#e6e9ee}
|
||
.t-start{color:#cfe0f5;font-weight:700;font-size:13px;border-bottom:1px solid var(--line);
|
||
padding-bottom:3px;margin-bottom:3px}
|
||
ul.blocked{list-style:none;margin:0;padding:0;font-size:12px}
|
||
ul.blocked li{padding:2px 0}
|
||
.sev-warn{color:#e0b060}.sev-stop{color:#e58080}
|
||
/* THE RULES BLOCK, shared by the lobby and the New Game dialog (`settings-form.ts`).
|
||
`.changed` is the one signal that says "this game is not the type it claims" — amber, the palette's
|
||
attention colour, never red: a changed rule is a choice, not an error. */
|
||
.set-row{padding:3px 0}
|
||
.set-row.changed{border-left:3px solid #e0b060;padding-left:9px;margin-left:-12px;
|
||
background:rgba(224,176,96,.08);border-radius:0 4px 4px 0}
|
||
.set-hint{display:block;font-size:11px;color:var(--dim);margin:2px 0 0}
|
||
/* The line under the type radios when a game is no longer the type it started as. */
|
||
.changed-note{color:#e0b060}
|
||
.set-row.changed .set-hint{color:#e0b060}
|
||
.set-row.unavailable{opacity:.5}
|
||
.ng-gate{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:3px 0}
|
||
.ng-gate input[type=checkbox]{flex:none}
|
||
.gate-num{width:82px}
|
||
.ng-gate input:disabled{opacity:.45}
|
||
.lb-params{display:flex;gap:14px;flex-wrap:wrap;align-items:end;margin:8px 0}
|
||
.lb-params .ng-num{flex:1 1 150px}
|
||
/* An error a player must not scroll past. The grey `.dim` line this replaced was routinely missed —
|
||
it looked like the note above it. Red is the palette's `.sev-stop`. */
|
||
.lb-error{color:#e58080;font-size:14px;font-weight:600;margin:8px 0;padding:8px 11px;
|
||
border-left:3px solid #e58080;background:rgba(229,128,128,.12);border-radius:0 5px 5px 0;
|
||
empty-cells:hide}
|
||
.lb-error:empty{display:none}
|
||
.lb-warn{color:#e0b060;font-size:13px;margin:8px 0}
|
||
.lb-doors{display:flex;gap:9px;margin:14px 0 12px}
|
||
.lb-door{flex:1;background:#222831;color:var(--fg);border:1px solid var(--line);border-radius:7px;
|
||
padding:9px 12px;cursor:pointer;font:inherit;font-size:14px}
|
||
.lb-door:hover{border-color:#4d6fa8}
|
||
.lb-door.active{background:#2f3a4b;border-color:#6f8fc8;color:#cfe0f5;font-weight:600}
|
||
.lb-saved{display:flex;align-items:center;gap:10px}
|
||
.lb-known-row{display:flex;align-items:center;gap:10px;padding:6px 0;border-bottom:1px solid var(--line)}
|
||
.lb-known-row:last-child{border-bottom:none}
|
||
.lb-known-row .code{font-weight:700;letter-spacing:.06em;color:#f2e6cf;flex:1}
|
||
.lb-type{font-size:15px;font-weight:600;color:#cfe0f5;margin:2px 0 8px}
|
||
.lb-preview-head{background:#1e242c;border:1px solid var(--line);border-radius:7px;padding:10px 12px;margin:0 0 10px}
|
||
/* The read-only rule set — what a joining player reads before sitting down, and what the seating
|
||
screen keeps showing afterwards. Generated, never a form: nobody but the host may change these. */
|
||
.rules-list{border:1px solid var(--line);border-radius:7px;background:#171c23;padding:4px 12px;margin:0 0 12px}
|
||
.rules-list dl{display:grid;grid-template-columns:minmax(140px,auto) 1fr;gap:2px 14px;margin:8px 0}
|
||
.rules-list dt{color:var(--dim);font-size:12px}
|
||
.rules-list dd{margin:0;font-size:13px}
|
||
.rules-list dd.changed{color:#e0b060}
|
||
.rules-list h4{font-size:11px;text-transform:uppercase;letter-spacing:.07em;color:var(--dim);margin:10px 0 0}
|
||
/* THE HANDOFF. Between the host pressing Start and the first Frame arriving there is nothing to
|
||
draw — and a blank board is what a broken game looks like too. */
|
||
#handoff{position:fixed;inset:0;z-index:20;display:none;align-items:center;justify-content:center;
|
||
background:rgba(18,21,26,.94);text-align:center;padding:20px}
|
||
#handoff.shown{display:flex}
|
||
#handoff .hand-inner{max-width:460px}
|
||
#handoff h2{font-size:15px;text-transform:none;letter-spacing:.01em;color:#cfe0f5}
|
||
#handoff .hand-note{color:var(--dim);font-size:13px;margin-top:8px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- THE LOBBY (Phase 4) — shown instead of the game UI whenever there is no game yet to play: no
|
||
stored session token, or a token whose game hasn't started. `lobby.ts` owns everything in here;
|
||
`main.ts` only decides whether THIS div or `#gameui` below is the one currently visible.
|
||
`#newgamedlg` at the very end of the body is solitaire-only, and asks the same questions through
|
||
the same shared module (`settings-form.ts`) — the two blocks are generated from one template. -->
|
||
<div id="lobby" hidden>
|
||
<header><b><a href="./index.html" class="home">Station Master</a></b> — <span class="dim">Multiplayer</span></header>
|
||
|
||
<!-- Whatever brought the player here when it was not their own click: a game that was ended under
|
||
them, or a lobby that closed. Never a field-level error — those sit with their fields. -->
|
||
<p class="lb-error" id="lb-notice" role="alert" hidden></p>
|
||
|
||
<section id="lb-secret-section">
|
||
<h2>Join secret</h2>
|
||
<!-- Saved: one line, not a password field asking to be filled in again. `lobby.ts` swaps these
|
||
two, and re-opens the ask automatically when the server answers 403. -->
|
||
<div id="lb-secret-saved" class="lb-saved" hidden>
|
||
<span class="dim">Join secret · saved in this browser</span>
|
||
<button id="lb-secret-change" class="ghost" type="button">Change</button>
|
||
</div>
|
||
<div id="lb-secret-ask">
|
||
<p class="ng-note">Whoever is running this server gave you a secret out of band (a chat message, not a public page). It is kept in this browser only, never shown back, and sent with every lobby request.</p>
|
||
<label class="ng-num"><span>Join secret</span><input id="lb-secret" type="password" autocomplete="off"></label>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- EVERY GAME THIS BROWSER IS IN. `localStorage` used to hold exactly ONE multiplayer session, so
|
||
joining a second silently overwrote the first and locked that seat out for good. Keyed by game
|
||
now, and this is where they are picked between — and forgotten deliberately rather than by
|
||
accident. -->
|
||
<section id="lb-known" hidden>
|
||
<h2>Games you are in</h2>
|
||
<p class="ng-note">This browser holds a seat in these. Rejoining needs the token kept here, so
|
||
<b>Forget</b> is the one thing that cannot be undone from this screen.</p>
|
||
<div id="lb-known-list"></div>
|
||
</section>
|
||
|
||
<section id="lb-choice-section">
|
||
<label class="ng-num"><span>Your display name</span><input id="lb-name" type="text" autocomplete="off" maxlength="40"></label>
|
||
|
||
<!-- TWO DOORS, one panel at a time. Joining used to be a heading at the BOTTOM of the create
|
||
form: someone sent a code had to scroll past fifteen fields they had no use for. -->
|
||
<div class="lb-doors">
|
||
<button id="lb-door-join" class="lb-door active" type="button">Join a game</button>
|
||
<button id="lb-door-create" class="lb-door" type="button">Create a game</button>
|
||
</div>
|
||
|
||
<section id="lb-join-panel">
|
||
<h2>Join a game</h2>
|
||
<p class="ng-note">Ask whoever created the game for its code. You will see the whole rule set
|
||
before you take a seat.</p>
|
||
<label class="ng-num"><span>Game code</span><input id="lb-code" type="text" autocomplete="off" placeholder="RAIL-1234"></label>
|
||
<button id="lb-look" type="button">Look up game</button>
|
||
<p class="lb-error" id="lb-join-err" role="alert"></p>
|
||
|
||
<!-- Filled by `/api/lobby/preview` — the config, who is seated, and nothing else. The SEED is
|
||
never in that response: it decides every shuffle in the game. -->
|
||
<div id="lb-preview" hidden>
|
||
<h3>Before you sit down</h3>
|
||
<div class="lb-preview-head">
|
||
<div class="lb-invite-code" id="lb-preview-code"></div>
|
||
<div id="lb-preview-type" class="lb-type"></div>
|
||
<div id="lb-preview-who" class="dim"></div>
|
||
</div>
|
||
<div id="lb-preview-rules" class="rules-list"></div>
|
||
<button id="lb-join" type="button">Join this game</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="lb-create-panel" hidden>
|
||
<h2>Create a new game</h2>
|
||
<p class="ng-note">You become the host — you choose the game type and, once everyone is seated, start the game. 2 to 4 players.</p>
|
||
|
||
<!-- TWO COLUMNS WHERE THERE IS ROOM. One 640px-wide column made this form a very long scroll
|
||
for what is really two short lists: what game this is, and what its rules are. -->
|
||
<div class="lb-two">
|
||
<div class="lb-col">
|
||
|
||
<!-- THE PARAMETERS, above the type. Changing one of these does NOT make the game Custom: the
|
||
types are formulas in the table size and the length, so the Revenue floor re-derives and
|
||
"Co-op, 3 players, 8 days" is still Co-op. -->
|
||
<div class="lb-params">
|
||
<label class="ng-num"><span>Seed</span>
|
||
<input id="lb-seed" type="text" inputmode="numeric" autocomplete="off" placeholder="blank for a random seed"></label>
|
||
<label class="ng-num"><span>Players at the table</span>
|
||
<select id="lb-players">
|
||
<option value="2">2</option>
|
||
<option value="3">3</option>
|
||
<option value="4" selected>4</option>
|
||
</select></label>
|
||
<label class="ng-num"><span>Days</span>
|
||
<input id="lb-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>
|
||
<!-- WITH THE TABLE SIZE IT IS ABOUT, not below the rules block — reported by Jesse, who found
|
||
it separated from the control it explains by fifteen settings. -->
|
||
<p class="ng-note">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.</p>
|
||
</div>
|
||
|
||
<div class="lb-col">
|
||
<h3>Game type</h3>
|
||
<div class="set-row" id="lb-type-row">
|
||
<label class="ng-radio"><input type="radio" name="lb-type" value="solitaire">
|
||
<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="lb-type" value="coop" checked>
|
||
<span><b>Co-op</b><br><span class="dim">Everyone’s Revenue is one table score. You win together or lose together.</span></span></label>
|
||
<label class="ng-radio"><input type="radio" name="lb-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="lb-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="lb-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="lb-type-note"></p>
|
||
</div>
|
||
|
||
<!-- FULL WIDTH WHEN IT OPENS. Reported by Jesse: opened inside the right-hand column it made a
|
||
very long scroll with the left column standing empty beside it. It is a grid child of its
|
||
own now, spanning both, and its five groups flow into as many columns as fit. -->
|
||
<details id="lb-settings" class="lb-span">
|
||
<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>, which keeps the scoring of the type you
|
||
started from; clicking a type again resets all of them back to it. They are fixed when the
|
||
game is created and cannot be changed once it starts.</p>
|
||
<div class="set-groups">
|
||
|
||
<div class="set-group">
|
||
<h3>Starting hand</h3>
|
||
<p class="ng-note">What each player is 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="lb-hand-row">
|
||
<label class="ng-radio"><input type="radio" name="lb-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="lb-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="lb-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="lb-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="lb-extra-row">
|
||
<label class="ng-radio"><input type="radio" name="lb-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="lb-extra" value="ownOffice">
|
||
<span><b>Also the playing player’s own Control Point</b><br><span class="dim">You may start one at home, but not in somebody else’s district.</span></span></label>
|
||
<label class="ng-radio"><input type="radio" name="lb-extra" value="anyOffice">
|
||
<span><b>Also any player’s Control Point</b><br><span class="dim">The most permissive — an Extra may be planted in another player’s district.</span></span></label>
|
||
<span class="set-hint" id="lb-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="lb-passenger-row">
|
||
<label class="ng-num"><span>Passenger revenue per coach</span>
|
||
<input id="lb-passenger" type="number" min="0" max="5" step="1" value="1"></label>
|
||
<span class="set-hint" id="lb-passenger-hint"></span>
|
||
</div>
|
||
<div class="set-row" id="lb-freight-row">
|
||
<label class="ng-num"><span>Freight revenue per load</span>
|
||
<input id="lb-freight" type="number" min="0" max="5" step="1" value="1"></label>
|
||
<span class="set-hint" id="lb-freight-hint"></span>
|
||
</div>
|
||
<div class="set-row" id="lb-transit-row">
|
||
<label class="ng-num"><span>Train revenue per transit</span>
|
||
<input id="lb-transit" type="number" min="0" max="5" step="1" value="0"></label>
|
||
<span class="set-hint" id="lb-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. Each one is switched on or off in its own
|
||
right; how long the game runs is set above, with the table size.</p>
|
||
<div class="set-row" id="lb-minrev-row">
|
||
<label class="ng-gate"><input type="checkbox" id="lb-minrev-on" checked>
|
||
<span>Everyone loses if combined Revenue at the end is under</span>
|
||
<input id="lb-minrev" type="number" min="0" step="1" class="gate-num"></label>
|
||
<span class="set-hint" id="lb-minrev-hint"></span>
|
||
</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>
|
||
<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>
|
||
<input id="lb-coltotal" type="number" min="0" step="1" class="gate-num"></label>
|
||
<span class="set-hint" id="lb-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="lb-visibility-row">
|
||
<label class="ng-num"><span>Reduced Visibility — five switching Moves instead of six in the
|
||
night Stages (1–3 and 11–12)</span>
|
||
<input id="lb-visibility" type="checkbox"></label>
|
||
<span class="set-hint" id="lb-visibility-hint"></span>
|
||
</div>
|
||
<div class="set-row" id="lb-rotation-row">
|
||
<label class="ng-num"><span>Employee Rotation — at the end of each Day everyone moves one
|
||
chair left and takes over the next station up the line. Your Revenue and the Fedora go with
|
||
you; the district stays where it is</span>
|
||
<input id="lb-rotation" type="checkbox"></label>
|
||
<span class="set-hint" id="lb-rotation-hint"></span>
|
||
</div>
|
||
<div class="set-row" id="lb-toolbox-row">
|
||
<label class="ng-num"><span>Emergency Toolbox — everyone starts holding a Red Flag, so a hand
|
||
of four; play or discard down to three on the first turn</span>
|
||
<input id="lb-toolbox" type="checkbox"></label>
|
||
<span class="set-hint" id="lb-toolbox-hint"></span>
|
||
</div>
|
||
<div class="set-row" id="lb-tossloco-row">
|
||
<label class="ng-num"><span>A Timetabled train may be discarded — toss it face-up to a
|
||
Department slot, where a rival may pick it up. 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="lb-tossloco" type="checkbox"></label>
|
||
<span class="set-hint" id="lb-tossloco-hint"></span>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</details>
|
||
|
||
<div class="lb-span">
|
||
<button id="lb-create" type="button">Create game</button>
|
||
<p class="lb-error" id="lb-create-err" role="alert"></p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
|
||
<!-- Shown once created or joined, in place of the choice above, until the host starts the game. -->
|
||
<section id="lb-seating-section" hidden>
|
||
<h2>Seating</h2>
|
||
<div class="lb-invite">
|
||
<div>
|
||
<div class="lb-invite-label">Send this to your players</div>
|
||
<div class="lb-invite-code" id="lb-gamecode"></div>
|
||
</div>
|
||
<button id="lb-copy" class="ghost" type="button">Copy code</button>
|
||
<button id="lb-copylink" class="ghost" type="button">Copy invite link</button>
|
||
<span class="ng-note" id="lb-copied"></span>
|
||
</div>
|
||
<p class="ng-note">They enter the code under <b>Join a game</b>, along with the same join secret
|
||
you used — the link carries the code, never the secret.</p>
|
||
<p class="ng-note">The host may fill an empty seat with a bot or clear an occupied one, and
|
||
starts the game once every seat is either a player or a bot. <b>These chairs are not the
|
||
running order</b> — who sits where along the Division is decided by a D12 roll when the game
|
||
starts, and the map shows the result.</p>
|
||
<div id="lb-seats"></div>
|
||
<p class="lb-warn" id="lb-stream-note" role="status" hidden></p>
|
||
<button id="lb-start" disabled>Start game</button>
|
||
<button id="lb-leave" class="ghost" type="button">Leave</button>
|
||
<p class="lb-error" id="lb-start-note" role="alert"></p>
|
||
|
||
<h3>The game you are in</h3>
|
||
<div id="lb-seating-type" class="lb-type"></div>
|
||
<div id="lb-seating-rules" class="rules-list"></div>
|
||
</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’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’s own Control Point</b><br><span class="dim">You may start one at home, but not in somebody else’s district.</span></span></label>
|
||
<label class="ng-radio"><input type="radio" name="ss-extra" value="anyOffice">
|
||
<span><b>Also any player’s Control Point</b><br><span class="dim">The most permissive — an Extra may be planted in another player’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–3 and 11–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>
|
||
|
||
<!-- Shown only when `station-master.save.v1` holds a game. Dealing from this screen CLEARS that
|
||
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 — there is no undo for that.</p>
|
||
|
||
<menu class="ng-buttons">
|
||
<button id="ss-resume" type="button" hidden>Continue saved game</button>
|
||
<button id="ss-deal" type="button">Deal</button>
|
||
</menu>
|
||
</section>
|
||
</div>
|
||
|
||
<div id="gameui" hidden>
|
||
<div class="topbar">
|
||
<header>
|
||
<b><a href="./index.html" class="home">Station Master</a></b>
|
||
<span>Revenue <b id="revenue">0</b></span>
|
||
<!-- The objective, and nothing else: score, target, Days left. The "behind the pace" chip and the
|
||
engine's guess at what your score ought to be were noise on the one line that must not wrap. -->
|
||
<span id="objective" class="pace">—</span>
|
||
<span class="dim">seed <span id="seed">—</span></span>
|
||
<!-- THE GAME CODE SURVIVES THE LOBBY. It used to end at `Lobby.Start` — the code was never carried
|
||
into `LobbyReady` — so a seated player could not say which game they were in, could not match
|
||
it against the administrator's Games in Progress list, and could not pass it to a latecomer.
|
||
Empty (and collapsed) in solitaire, where there is no code. -->
|
||
<span class="dim" id="gamecode" title="The code this game was created under. The administrator's Games in Progress list uses it, and it is how you say which game you mean."></span>
|
||
<!-- Co-op, Competitive, Cutthroat or Custom, derived from the config the Frame carries
|
||
(`presets.ts`). A Cutthroat game used to look exactly like a Co-op one from the board. -->
|
||
<span class="dim" id="gametype" title=""></span>
|
||
<!-- WHICH RULES THIS GAME IS BEING PLAYED UNDER. The settings are chosen when the game is dealt
|
||
and then never mentioned again, which makes a playtest note ("scored 4") unreadable a week
|
||
later: at 0 revenue per transit that is a different game from the same seed at 5. Short enough
|
||
to keep the header on one line; the tooltip spells it out. -->
|
||
<span class="dim" id="houserules" title="">—</span>
|
||
<button id="sound" title="Whistle at the end of each Stage, the crossing bell at the end of each Day, and the conductor when a train is built. Currently synthesised, not recorded.">🔇 muted</button>
|
||
<!-- BOARD ZOOM. Applies to the Division map and the Office Area grid alike — both already scroll
|
||
horizontally (`#division`, `#grid`) when they run wide, so this only ever needs to resize the
|
||
rendered SVG's own pixel dimensions; the existing scrollbar keeps doing the panning. -->
|
||
<span class="zoom" title="Zoom the Division map and your Office Area. Both already scroll — this only changes their size.">
|
||
<button id="zoomout" aria-label="Zoom out">−</button><span id="zoomlabel">100%</span><button id="zoomin" aria-label="Zoom in">+</button>
|
||
</span>
|
||
<button id="undo" title="Take the last action back. The save is the seed plus the moves made, so this replays the game without the last one — as far back as you like.">Undo</button>
|
||
<button id="savefile" title="Download this game as a save file you can replay or share">Save replay</button>
|
||
<button id="newgame" title="Deal a fresh game. You choose the seed, the opening hand and what the three economies pay. Undo steps back one action at a time; this throws the whole game away, so download the replay first if you want to keep it.">New game</button>
|
||
<button id="multiplayer" title="Create or join a Competitive or Co-op game on this server, with other players.">Multiplayer</button>
|
||
<!-- LEAVING A RUNNING GAME. Reported by Jesse 2026-08-23: "if I'm a player in the middle of the
|
||
game and I need to leave, how do I leave the game, clear the token from my browser so I can
|
||
play a different game later?" There was no way at all — the page rejoined the same game on
|
||
every load and nothing let go of it. This keeps your seat — the table waits for you — and
|
||
your token, and puts you back at the lobby, which lists every game this browser is in. -->
|
||
<button id="leavegame" hidden title="Go back to the lobby. Your seat is kept and the game waits for you — the lobby lists it under Games you are in, so you can come back or hand the browser to a different game.">Leave game</button>
|
||
<a class="home" href="./replays.html" style="font-size:12px">replays</a>
|
||
<span class="dim build" title="what is actually deployed">__BUILD__</span>
|
||
</header>
|
||
|
||
<!-- The turn chart: where in the Day we are, and which of the five phases is running. Lifted out
|
||
of the title bar, where "phase: Mainline" said nothing about what came before or after it. -->
|
||
<!-- Filled by `turnChartHtml`, shared with both replay viewers so all three screens report where
|
||
you are in the Day identically. -->
|
||
<div id="turnchart"></div>
|
||
</div><!-- /topbar -->
|
||
<!-- Announces a phase CHANGE. The turn chart says where you are; this says that it moved, which is
|
||
what you miss when the automatic phases run between two clicks. -->
|
||
<div id="phasenote"></div>
|
||
<div id="announce"></div>
|
||
<!-- `lobby-and-sessions.md` §5 — a disconnect keeps the seat and the game simply waits; this is
|
||
what says WHY, instead of the table watching nothing happen with no explanation. Empty and
|
||
collapsed whenever everyone connected is still connected — a `LocalSession` never fills it. -->
|
||
<div id="presence"></div>
|
||
|
||
<main>
|
||
<div>
|
||
<section><h2>The Division — west to east</h2><div id="division"></div>
|
||
<p class="ng-note" id="seating-chain"></p></section>
|
||
<section id="district">
|
||
<h2>Your Office Area
|
||
<span class="dim" style="text-transform:none;letter-spacing:0">— hover any card for the full explanation</span>
|
||
<button id="districttoggle" class="ghost" title="Auto-hide keeps the district open during Local Operations and Cargo — the phases that change it — and folds it otherwise. Click to pin it open or hidden instead.">auto-hide: on</button>
|
||
</h2>
|
||
<div id="districtsummary" class="dim"></div>
|
||
<!-- THE RULE THAT SHAPES EVERY DISTRICT, said once where the district is.
|
||
It is enforced everywhere and was stated nowhere: the Limits signs are the only growth
|
||
point on the Running Track and they move outward as you build, and no card anywhere can
|
||
be inserted between two cards already down. -->
|
||
<div class="districtrule">A district only ever grows <b>outwards</b>. On the Running Track the
|
||
Limits signs are the growth point — lay a card on a sign and the sign moves one further out;
|
||
nothing may be built beyond it. Everywhere else a card needs an empty square that its rails
|
||
reach. <b>No card is ever inserted between two cards already down</b>, so nothing you have
|
||
built is pushed along by something laid later.</div>
|
||
<div id="grid"></div>
|
||
</section>
|
||
<section><h2>History</h2><div id="log"></div></section>
|
||
</div>
|
||
|
||
<div>
|
||
<section><h2 class="big">Your Move</h2><div id="actions"></div></section>
|
||
<section><h2 class="big">Cards Available</h2>
|
||
<h3>Cards in My Hand</h3><div id="hand" class="cardrow">—</div>
|
||
<h3>Department Decks & Salvage Yard — top card face up, with depth</h3><div id="depts" class="cardrow ref">—</div>
|
||
</section>
|
||
<section><h2 class="big">The Yards</h2>
|
||
<h3>Division Yard <span class="dim" id="divtot"></span></h3>
|
||
<div id="divyard" class="yard"></div>
|
||
<h3>Classification Yard <span class="dim" id="clstot"></span></h3>
|
||
<div id="clsyard" class="yard"></div>
|
||
<div class="dim" id="yardnote"></div>
|
||
</section>
|
||
<section><h2>Timetable — who is due out, and when</h2><div id="timetable"></div>
|
||
<!-- WHAT THE COLOURS MEAN. Asked directly in playtesting — "what do green and purple mean?"
|
||
Violet is "where you are" everywhere on this page, and the green is a flash rather than a
|
||
state, which is exactly why it needed saying. -->
|
||
<div class="tt-key">
|
||
<span class="k-due">Train due</span>
|
||
<span class="k-now">this Stage</span>
|
||
<span class="k-past">already gone</span>
|
||
<span class="k-fresh">just rolled</span>
|
||
</div>
|
||
<div class="dim" style="font-size:11px;margin-top:5px">A train's number is its seniority and direction — odd runs west, even runs east. The Stage it departs is set by a 1D12 roll when its card is played, and it then runs at that time every Day.</div>
|
||
</section>
|
||
<section><h2>Blocked — why nothing is moving</h2><ul class="blocked" id="blocked"></ul></section>
|
||
<section><h2>Facilities</h2><div id="facs"></div></section>
|
||
</div>
|
||
</main>
|
||
|
||
<!-- ===================================================================
|
||
NEW GAME — the seed, the opening hand, and what the three economies pay.
|
||
|
||
It was a `prompt()` asking for a seed. Two of the three things that decide what kind of game
|
||
you are about to play had no way in at all: the opening hand had been changed twice with no
|
||
way back to the earlier rule, and the revenue rates were constants in the source. Balance is
|
||
the open question in this game (`TODO.md`), and the way to settle it is to deal several games
|
||
at different settings — which needs a dialog, not a rebuild.
|
||
|
||
Every control has a default that is the recommended answer, so DEAL with nothing touched is a
|
||
complete, sensible game. The settings ride in the URL alongside the seed, because a seed alone
|
||
no longer names a game: `?seed=430` with a different opening hand is a different railroad.
|
||
==================================================================== -->
|
||
</div><!-- /gameui -->
|
||
|
||
<dialog id="newgamedlg" aria-labelledby="ng-title">
|
||
<form method="dialog" id="newgameform">
|
||
<h2 class="big" id="ng-title">New game</h2>
|
||
|
||
<!-- THE SAME BLOCK THE LOBBY USES, same shared module, same order — the two screens are one
|
||
design. Only Solitaire can be dealt here; the multiplayer types are shown disabled rather
|
||
than hidden, so what this screen offers and what the lobby offers read as one list. -->
|
||
<div class="lb-params">
|
||
<label class="ng-num"><span>Seed</span>
|
||
<input id="ng-seed" type="text" inputmode="numeric" autocomplete="off" placeholder="blank for a random seed"></label>
|
||
<label class="ng-num"><span>Days</span>
|
||
<input id="ng-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="ng-type-row">
|
||
<label class="ng-radio"><input type="radio" name="ng-type" value="solitaire">
|
||
<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="ng-type" value="coop" checked>
|
||
<span><b>Co-op</b><br><span class="dim">Everyone’s Revenue is one table score. You win together or lose together.</span></span></label>
|
||
<label class="ng-radio"><input type="radio" name="ng-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="ng-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="ng-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="ng-type-note"></p>
|
||
|
||
<details id="ng-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 each player is 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="ng-hand-row">
|
||
<label class="ng-radio"><input type="radio" name="ng-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="ng-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="ng-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="ng-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="ng-extra-row">
|
||
<label class="ng-radio"><input type="radio" name="ng-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="ng-extra" value="ownOffice">
|
||
<span><b>Also the playing player’s own Control Point</b><br><span class="dim">You may start one at home, but not in somebody else’s district.</span></span></label>
|
||
<label class="ng-radio"><input type="radio" name="ng-extra" value="anyOffice">
|
||
<span><b>Also any player’s Control Point</b><br><span class="dim">The most permissive — an Extra may be planted in another player’s district.</span></span></label>
|
||
<span class="set-hint" id="ng-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="ng-passenger-row">
|
||
<label class="ng-num"><span>Passenger revenue per coach</span>
|
||
<input id="ng-passenger" type="number" min="0" max="5" step="1" value="1"></label>
|
||
<span class="set-hint" id="ng-passenger-hint"></span>
|
||
</div>
|
||
<div class="set-row" id="ng-freight-row">
|
||
<label class="ng-num"><span>Freight revenue per load</span>
|
||
<input id="ng-freight" type="number" min="0" max="5" step="1" value="1"></label>
|
||
<span class="set-hint" id="ng-freight-hint"></span>
|
||
</div>
|
||
<div class="set-row" id="ng-transit-row">
|
||
<label class="ng-num"><span>Train revenue per transit</span>
|
||
<input id="ng-transit" type="number" min="0" max="5" step="1" value="0"></label>
|
||
<span class="set-hint" id="ng-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. Each one is switched on or off in its own
|
||
right; how long the game runs is set above, with the table size.</p>
|
||
<div class="set-row" id="ng-minrev-row">
|
||
<label class="ng-gate"><input type="checkbox" id="ng-minrev-on" checked>
|
||
<span>Everyone loses if combined Revenue at the end is under</span>
|
||
<input id="ng-minrev" type="number" min="0" step="1" class="gate-num"></label>
|
||
<span class="set-hint" id="ng-minrev-hint"></span>
|
||
</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>
|
||
<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>
|
||
<input id="ng-coltotal" type="number" min="0" step="1" class="gate-num"></label>
|
||
<span class="set-hint" id="ng-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="ng-visibility-row">
|
||
<label class="ng-num"><span>Reduced Visibility — five switching Moves instead of six in the
|
||
night Stages (1–3 and 11–12)</span>
|
||
<input id="ng-visibility" type="checkbox"></label>
|
||
<span class="set-hint" id="ng-visibility-hint"></span>
|
||
</div>
|
||
<div class="set-row" id="ng-rotation-row">
|
||
<label class="ng-num"><span>Employee Rotation — at the end of each Day everyone moves one
|
||
chair left and takes over the next station up the line. Your Revenue and the Fedora go with
|
||
you; the district stays where it is</span>
|
||
<input id="ng-rotation" type="checkbox"></label>
|
||
<span class="set-hint" id="ng-rotation-hint"></span>
|
||
</div>
|
||
<div class="set-row" id="ng-toolbox-row">
|
||
<label class="ng-num"><span>Emergency Toolbox — everyone starts holding a Red Flag, so a hand
|
||
of four; play or discard down to three on the first turn</span>
|
||
<input id="ng-toolbox" type="checkbox"></label>
|
||
<span class="set-hint" id="ng-toolbox-hint"></span>
|
||
</div>
|
||
<div class="set-row" id="ng-tossloco-row">
|
||
<label class="ng-num"><span>A Timetabled train may be discarded — toss it face-up to a
|
||
Department slot, where a rival may pick it up. 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="ng-tossloco" type="checkbox"></label>
|
||
<span class="set-hint" id="ng-tossloco-hint"></span>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</details>
|
||
|
||
<menu class="ng-buttons">
|
||
<span class="ng-note" id="ng-multiplayer-note" style="margin:0 auto 0 0">Use the <b>Multiplayer</b> button instead — it creates or joins a game on this server.</span>
|
||
<button value="cancel" id="ng-cancel" type="submit" formnovalidate>Cancel</button>
|
||
<button value="deal" id="ng-deal" type="submit">Deal</button>
|
||
</menu>
|
||
</form>
|
||
</dialog>
|
||
|
||
<!-- THE DAY ROLLING OVER (Gitea#10). A Day turns inside the automatic phases, so it happens
|
||
between one click and the next; the phase banner and the announcement flash both fade before
|
||
someone reading the board notices them. A modal stops and waits, which is the whole request:
|
||
"hard to keep track of time". Filled by `dayEndHtml` and opened from `render()`. -->
|
||
<dialog id="dayenddlg" aria-labelledby="de-title">
|
||
<form method="dialog">
|
||
<div id="dayendbody"></div>
|
||
<menu class="ng-buttons">
|
||
<button value="ok" id="de-ok" type="submit">Carry on</button>
|
||
</menu>
|
||
</form>
|
||
</dialog>
|
||
|
||
<!-- THE END-OF-GAME RESULTS (Gitea#16). Filled by `resultsHtml` and opened from `renderEnding`,
|
||
which puts it up once per ending unasked and leaves a button to reopen it. Reopenable matters:
|
||
Gitea#11 lets a table play past the end, and continuing must not cost you the results screen. -->
|
||
<dialog id="resultsdlg" aria-labelledby="rs-title">
|
||
<form method="dialog">
|
||
<div id="resultsbody"></div>
|
||
<menu class="ng-buttons">
|
||
<button value="ok" id="rs-ok" type="submit">Close</button>
|
||
</menu>
|
||
</form>
|
||
</dialog>
|
||
|
||
<!-- THE HANDOFF, between `Lobby.Start` and the first Frame.
|
||
`beginRemote` used to write "… connecting to the game" into `#presence` — the DISCONNECT banner,
|
||
whose job is `⚠ waiting on Alice`. It worked only because the first render overwrote it, and it
|
||
had nothing to say when the first push never came. This is its own state: it opens on the way
|
||
into a game, holds a deliberate beat so the game visibly begins, and closes on the first Frame. -->
|
||
<div id="handoff">
|
||
<div class="hand-inner">
|
||
<h2 id="handoff-title">Dealing the railroad…</h2>
|
||
<div id="handoff-note" class="hand-note"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<script type="module" src="./web/main.js"></script>
|
||
</body>
|
||
</html>
|