Three items folded into the unshipped v0.7.9. WAITING ON (reported by Jesse from play). The status line said "nobody — the Division is running itself" while the game was stopped on the Superintendent. Frame.actor carried clock.currentActor, which is null for the whole Mainline Phase, so all three interruptions — §8.1's clearance ruling, Gitea#5's Yard Office offer, Gitea#19's Red Flag prompt — reported that nobody was holding it up. actingPlayer had the answer since the Gitea#5 refactor; the Frame threw it away. It carries actingPlayer now, plus a new `awaiting` field naming the question and the train: "waiting on Bob · a clearance ruling · Train 4". Naming the person alone is not enough when three different things can be pending. THE FEDORA (TODO #29) rides at the right-hand end of the phase row instead of a line of its own, and wraps under rather than squeezing the chips. THE DEVELOPER REPLAY (TODO #34) printed "loss — revenueFloor", the same defect Gitea#16 was filed about, still alive because nothing player-facing pointed at it. panels.ts's reasonSentence is exported and shared rather than reimplemented, fed the last recorded frame and stripped of markup. The drift test maps win/loss to won/lost so it still checks the two AGREE rather than that they are spelled alike. Also carries the previous, unsigned commit's work: the two setup screens worded the same section by section. 877 tests pass, ten new. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdG46Ja2PEDBkpqiDazMoX
176 lines
10 KiB
TypeScript
176 lines
10 KiB
TypeScript
/**
|
|
* The turn chart — where in the Day we are, and which of the five phases is running.
|
|
*
|
|
* SELF-CONTAINED ON PURPOSE, exactly like the two board renderers in `board-svg.ts`. The playable
|
|
* app and the site's replay viewer import it normally; the standalone replay is a single HTML file
|
|
* with an inline script and cannot import anything, so it embeds this via `Function.toString()`.
|
|
* That keeps ONE implementation across all three screens.
|
|
*
|
|
* It lived only in `main.ts` before, so a replay showed the Day and the phase as plain text while
|
|
* the live game showed the chart — and the violet "you are here" highlight, which is the single
|
|
* most-glanced-at thing on the page, appeared on one screen out of three.
|
|
*/
|
|
|
|
/** What a chart needs from a frame. A `Frame` satisfies this; so does a replay's rehydrated one. */
|
|
export type TurnChartFrame = {
|
|
day: number;
|
|
stage: number;
|
|
clock: string;
|
|
phase: string;
|
|
phaseKey: string;
|
|
actor: number | null;
|
|
/** What the game has stopped to ask, when it has. Null while a phase is simply running. */
|
|
awaiting?: { asks: string; train: string } | null;
|
|
};
|
|
|
|
/**
|
|
* The five phases of a Stage, in order, each with what actually happens in it.
|
|
*
|
|
* The first three are the rules text; Cargo and Supervisor Shift are written from what the engine
|
|
* does, since the recovered sheet does not spell them out.
|
|
*/
|
|
/**
|
|
* `superName` is the player holding the Fedora, or null for a table where saying so is noise — one
|
|
* seat, where it is always you.
|
|
*
|
|
* REPORTED BY JESSE 2026-08-23, from a two-player game on StartOS: seat 1 played a train card and
|
|
* seat 2 was asked to build the train, "what's the rule for WHICH player builds a train?" The engine
|
|
* was right — §7 makes up a consist "starting with the Superintendent and working left" — but the
|
|
* board did not name the Superintendent ANYWHERE, so the question could not be answered from the
|
|
* screen. `Frame` has carried `superintendent` all along and the standalone replay printed it; the
|
|
* live game never did.
|
|
*/
|
|
export function turnChartHtml(f: TurnChartFrame, actorName: string | null, superName: string | null = null): string {
|
|
const PHASES: { key: string; label: string; tip: string; icon: string }[] = [
|
|
{
|
|
key: 'localOps',
|
|
label: 'Local Operations',
|
|
tip: 'Switch with a train, Freight Agent operations, or draw a card. One of the three, once per Stage — this is the whole of your turn.',
|
|
// a turnout: the choice of three roads
|
|
icon: '<path class="ic" d="M1 11h6l6-6h4M7 11h10"/>',
|
|
},
|
|
{
|
|
key: 'newTrain',
|
|
label: 'New Train',
|
|
// Says WHO builds, which is the question this phase actually raises at a table: the round
|
|
// starts with the Superintendent and works left, one car each, repeating (§7, Gap 9) — not
|
|
// with whoever played the card. An Extra is the exception: its player loads it as they choose.
|
|
tip: 'Timetabled trains for this Stage are built: starting with the Superintendent and working left, each player adds ONE car, going round again until the consist is full or the Division Yard has nothing suitable. New timetabled trains are rolled onto the timetable. Held trains are built. An Extra is loaded by the player who played it.',
|
|
// a locomotive being made up
|
|
icon: '<rect class="ic" x="2" y="6" width="9" height="7" rx="1"/><path class="ic" d="M11 9h4v4h-4"/><circle class="icf" cx="5" cy="15" r="1.5"/><circle class="icf" cx="13" cy="15" r="1.5"/>',
|
|
},
|
|
{
|
|
key: 'mainline',
|
|
label: 'Mainline',
|
|
tip: 'All trains holding at an Office or Division Point must attempt to move. This phase is automatic — you make no decisions in it, except a clearance ruling if one is called for.',
|
|
// open road
|
|
icon: '<path class="ic" d="M2 7h16M2 13h16"/><path class="ic" d="M6 7v6M12 7v6"/>',
|
|
},
|
|
{
|
|
key: 'loadUnload',
|
|
label: 'Cargo',
|
|
tip: 'Your workers act, once each per Stage. A Laborer moves a load one square across MEN | AT | WORK, or starts one, or begins an unload. A Porter boards or detrains passengers, which earns Revenue in a single action.',
|
|
// a loaded box on a flat
|
|
icon: '<rect class="ic" x="3" y="7" width="8" height="6"/><path class="ic" d="M3 10h8M7 7v6"/><path class="ic" d="M13 13h4"/>',
|
|
},
|
|
{
|
|
key: 'shiftChange',
|
|
label: 'Supervisor Shift',
|
|
tip: "End of the Stage. Every Laborer and Porter is refreshed for the next one, and every third Stage — 3, 6, 9 and 12 — the Superintendent's hat passes to the next player.",
|
|
// a pocket watch
|
|
icon: '<circle class="ic" cx="10" cy="11" r="6"/><path class="ic" d="M10 8v3l2 2M8 2h4"/>',
|
|
},
|
|
];
|
|
|
|
const esc = (t: string): string =>
|
|
String(t).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c] ?? c);
|
|
|
|
const at = PHASES.findIndex((p) => p.key === f.phaseKey);
|
|
const chips = PHASES.map((p, i) => {
|
|
const state = p.key === f.phaseKey ? 'now' : i < at ? 'done' : '';
|
|
return (
|
|
`<li class="tc-phase ${state}" data-tip="${esc(p.label)} — ${esc(p.tip)}" tabindex="0">` +
|
|
`<svg width="20" height="20" viewBox="0 0 20 20" aria-hidden="true">${p.icon}</svg>` +
|
|
`<span>${esc(p.label)}</span></li>`
|
|
);
|
|
}).join('');
|
|
|
|
/**
|
|
* WAITING ON WHOM, AND FOR WHAT.
|
|
*
|
|
* "nobody — the Division is running itself" is true of an automatic phase and was being printed
|
|
* over the top of three interruptions that are emphatically waiting on a person: §8.1's clearance
|
|
* ruling, the Yard Office offer and the Red Flag prompt. The Frame carried the phase's actor,
|
|
* which is null throughout the Mainline Phase, so a game stopped on a named player's decision
|
|
* reported that nobody was holding it up (Jesse, 2026-08-30). `Frame.actor` is `actingPlayer` now
|
|
* and answers who; `awaiting` says what, because "waiting on Bob" with no more than that is a
|
|
* game that looks stuck to everyone except Bob.
|
|
*/
|
|
const who = actorName ?? 'nobody — the Division is running itself';
|
|
const asked = f.awaiting
|
|
? ` <span class="tc-asks">${esc(f.awaiting.asks)} · ${esc(f.awaiting.train)}</span>`
|
|
: '';
|
|
const fedora =
|
|
superName === null
|
|
? ''
|
|
: `<div class="tc-super" data-tip="The Superintendent holds the Fedora. Every third Stage — 3, 6, 9 and 12 — it passes one seat to the left. The office rules on whether a following train may depart, and every round that goes round the table starts here: the New Train make-up round, and the acting order within a phase." tabindex="0">` +
|
|
`<span aria-hidden="true">🎩</span> Superintendent <b>${esc(superName)}</b></div>`;
|
|
|
|
return (
|
|
`<div class="tc-when"><b>Day ${f.day}</b><span>Stage ${f.stage} of 12</span>` +
|
|
`<span class="dim">${esc(f.clock)}</span></div>` +
|
|
`<div class="tc-now">phase <b>${esc(f.phase)}</b></div>` +
|
|
`<div class="tc-who">waiting on <b>${esc(who)}</b>${asked}</div>` +
|
|
// THE FEDORA RIDES AT THE END OF THE PHASE ROW (`TODO.md` #29, Jesse). It sat on its own line
|
|
// between the phases and everything above them, which put a thing that changes every third
|
|
// Stage in the middle of the things that change every Stage. The row it belongs beside is the
|
|
// one whose last chip is Supervisor Shift — the phase that passes it.
|
|
`<div class="tc-row"><ol class="tc-phases">${chips}</ol>${fedora}</div>`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Styling, shared so the three screens cannot drift.
|
|
*
|
|
* VIOLET, not the amber used for actions. The palette already spends amber on "you can click this",
|
|
* blue on "this is a legal square or a link", green on good and red on bad. The turn chart is none
|
|
* of those — it reports WHERE YOU ARE, and a player should never read it as something to press.
|
|
* Violet is the one hue not already carrying a meaning, so it cannot be confused with any of them.
|
|
*/
|
|
export const TURNCHART_CSS = `
|
|
#turnchart{display:flex;align-items:center;gap:16px;flex-wrap:wrap;
|
|
background:#161b22;border-bottom:1px solid #2c333d;padding:7px 14px}
|
|
.tc-when{display:flex;align-items:baseline;gap:10px}
|
|
.tc-when b{font-size:15px;color:#cfe0f5}
|
|
.tc-now{font-size:12px;color:#8b94a3}
|
|
.tc-now b{color:#b98cf0;font-size:13px}
|
|
/* WHO EVERYONE IS WAITING ON. Solitaire has one player, but the seat still has a name — the log
|
|
says "Player Solitaire", so the chart should agree. In multiplayer this is the thing a table
|
|
glances at most often, so it gets its own chip rather than hiding in the phase text. */
|
|
.tc-who{display:flex;align-items:center;gap:6px;font-size:12px;color:#8b94a3}
|
|
.tc-asks{color:#a99ac4;font-style:italic}
|
|
.tc-who b{color:#b98cf0;background:rgba(150,110,230,.16);border:1px solid #8b6ad0;
|
|
border-radius:11px;padding:1px 9px;font-size:12px}
|
|
/* WHO HOLDS THE FEDORA. Violet like the rest of the chart — this is "where you are" news, not
|
|
something to press — but unfilled, so the eye still lands on "waiting on" first: that is the one
|
|
that changes every turn, while this changes four times a Day. */
|
|
/* The phase row and the Fedora on one line, the hat pushed to the far end (TODO.md #29): the row
|
|
is the Stage, and the Superintendent is who holds it. Wraps under the phases on a narrow screen
|
|
rather than squeezing the chips. */
|
|
.tc-row{display:flex;align-items:center;gap:12px;flex-wrap:wrap}
|
|
.tc-row ol.tc-phases{flex:1 1 auto}
|
|
.tc-super{margin-left:auto;display:flex;align-items:center;gap:6px;font-size:12px;color:#8b94a3;cursor:help}
|
|
.tc-super b{color:#cbb6f2;border:1px solid #6b5a94;border-radius:11px;padding:1px 9px;font-size:12px}
|
|
ol.tc-phases{display:flex;gap:6px;list-style:none;margin:0;padding:0;flex-wrap:wrap}
|
|
.tc-phase{display:flex;align-items:center;gap:6px;border:1px solid #2c333d;border-radius:14px;
|
|
padding:3px 10px 3px 7px;font-size:11px;color:#8b94a3;background:#1a1f26;cursor:help}
|
|
.tc-phase svg{display:block;flex:0 0 auto}
|
|
.tc-phase .ic{stroke:#5f6b7a;fill:none;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
|
|
.tc-phase .icf{fill:#5f6b7a;stroke:none}
|
|
.tc-phase.done{opacity:.55}
|
|
.tc-phase.now{border-color:#b98cf0;background:rgba(150,110,230,.15);color:#e7dcfb;font-weight:700;
|
|
box-shadow:0 0 0 2px rgba(150,110,230,.18)}
|
|
.tc-phase.now .ic{stroke:#b98cf0}
|
|
.tc-phase.now .icf{fill:#b98cf0}
|
|
`;
|