/**
* 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: '',
},
{
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: '',
},
{
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: '',
},
{
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: '',
},
{
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: '',
},
];
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 (
`
` +
`` +
`${esc(p.label)}
`
);
}).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
? ` ${esc(f.awaiting.asks)} · ${esc(f.awaiting.train)}`
: '';
const fedora =
superName === null
? ''
: `
` +
`🎩 Superintendent ${esc(superName)}
`;
return (
`
Day ${f.day}Stage ${f.stage} of 12` +
`${esc(f.clock)}
` +
`
phase ${esc(f.phase)}
` +
`
waiting on ${esc(who)}${asked}
` +
// THE FEDORA RIDES AT THE END OF THE PHASE ROW (`TODO.md` #29, Jesse). It sat on its own line
// between the phases and everything above them, which put a thing that changes every third
// Stage in the middle of the things that change every Stage. The row it belongs beside is the
// one whose last chip is Supervisor Shift — the phase that passes it.
`
${chips}${fedora}
`
);
}
/**
* 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}
`;