v0.7.1 — a caboose is not a load, a Day that says it ended, and a train you may throw away
Four issues off the Gitea tracker, all of them things a player saw at the board. Reasoning for every item, and what was verified how: CHANGELOG.md. - Gitea#8: X22 Pee-Dee refused every caboose, including the one it was made up with, so setting it out stranded the train. All six cabooses are minted loaded because §2.2's "coloured is loaded, white is empty" doubles as a piece count in the supply table; one read of the flag took that literally. A caboose carries the crew, not freight, so it is never a load. - Gitea#10: a Day turns over inside the phases that run themselves, so it passes between one click and the next — and both transient signals fade before a player reading the board notices. A modal stops and waits, carrying the standings, the Days left and the combined target. Suppressed on the first frame, on Undo stepping back across a rollover, and on the Day the game ends. - Gitea#9, which SUPERSEDES Gitea#6 from three days ago: a Timetabled train may be tossed face-up to a Department slot, where a rival may pick it up — the second half of the ruling needed no code, since that is where every discard already goes. An Extra still may not. A New Game setting on this line (discardTimetabled, on by default), the plain rule on the 0.4.9 line. - Gitea#2 is not an engine bug: the rules are implemented exactly, and running the coach pool dry is Jesse's ruling to keep — "part of the strategy". What was wrong is that the game said nothing. A blocked platform now gives its reason, from the engine's own predicate, including how many coaches are stranded in Classification and what brings them back. The same four ship as v0.4.9g on the playtest line. Closes #2 Closes #8 Closes #9 Closes #10 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FLnYR4XtXQNamYJXGYT8oC
This commit is contained in:
co-authored by
Claude Opus 5
parent
603d38602c
commit
441447648d
+61
-11
@@ -10,7 +10,7 @@ import { TURNCHART_CSS, turnChartHtml } from '../sim/turnchart.ts';
|
||||
import type { Frame } from '../sim/view.ts';
|
||||
import { seatLabel } from '../sim/view.ts';
|
||||
import type { Menu, Save } from './game.ts';
|
||||
import { PANEL_CSS, blockedHtml, facilitiesHtml, pilesHtml, timetableHtml, yardHtml } from './panels.ts';
|
||||
import { PANEL_CSS, blockedHtml, dayEndHtml, facilitiesHtml, pilesHtml, timetableHtml, yardHtml } from './panels.ts';
|
||||
import { TOOLTIP_CSS, installTooltips } from './tooltip.ts';
|
||||
import { playCue } from './sound.ts';
|
||||
import {
|
||||
@@ -158,6 +158,13 @@ let zoom = settings.zoom;
|
||||
* ended or what happened in between. Reported exactly that way.
|
||||
*/
|
||||
let lastPhase: string | null = null;
|
||||
/**
|
||||
* The Day the page last drew, so a Day rolling over can be shown as a dialog (Gitea#10).
|
||||
*
|
||||
* Null until the first frame: arriving in a game already on Day 3 is not Day 2 ending, and a page
|
||||
* reloaded mid-game would otherwise announce a rollover that happened before it was watching.
|
||||
*/
|
||||
let lastDay: number | null = null;
|
||||
/**
|
||||
* WHICH CREW THE BOARD IS DRAWING, when the district holds more than one.
|
||||
*
|
||||
@@ -302,6 +309,10 @@ function gameOptionsFromUrl(params: URLSearchParams): NewGameOptions {
|
||||
if (STARTING_HAND_LABELS.some((o) => o.value === hand)) rules.startingHand = hand as StartingHand;
|
||||
const extra = params.get('extra');
|
||||
if (EXTRA_START_LABELS.some((o) => o.value === extra)) rules.extraStart = extra as ExtraStartRule;
|
||||
// §6.2 (Gitea#9). This one defaults ON, so the URL only ever has to carry the OFF case — `?toss=0`.
|
||||
// Read the same way the optional rules are: present and not "0" means on.
|
||||
const toss = params.get('toss');
|
||||
if (toss !== null) rules.discardTimetabled = toss !== '0';
|
||||
|
||||
const revenue: Partial<RevenueRules> = {};
|
||||
for (const [param, key] of Object.entries(RULE_PARAMS)) {
|
||||
@@ -358,6 +369,7 @@ function solitaireDefaults(options: NewGameOptions): NewGameOptions {
|
||||
houseRules: {
|
||||
startingHand: options.houseRules?.startingHand ?? p.startingHand,
|
||||
extraStart: options.houseRules?.extraStart ?? p.extraStart,
|
||||
discardTimetabled: options.houseRules?.discardTimetabled ?? p.discardTimetabled,
|
||||
revenue: {
|
||||
passengerPerCoach: revenue.passengerPerCoach ?? p.passengerPerCoach,
|
||||
freightPerLoad: revenue.freightPerLoad ?? p.freightPerLoad,
|
||||
@@ -374,6 +386,9 @@ function rulesToUrl(rules: HouseRules, options: NewGameOptions, seed: string): s
|
||||
// The dialog answers reach `start()` through the URL and nowhere else, so a setting missing from
|
||||
// here is a setting the dialog silently discards.
|
||||
params.set('extra', rules.extraStart);
|
||||
// Written only when OFF, for the reason the optional rules are written only when on: this one
|
||||
// defaults to on, so `toss=1` on every link would say nothing and cost a parameter.
|
||||
if (!rules.discardTimetabled) params.set('toss', '0');
|
||||
for (const [param, key] of Object.entries(RULE_PARAMS)) params.set(param, String(rules.revenue[key]));
|
||||
for (const [param, key] of Object.entries(VICTORY_PARAMS)) {
|
||||
const value = options[key];
|
||||
@@ -561,6 +576,34 @@ function flashAnnounce(text: string): void {
|
||||
}, 4200);
|
||||
}
|
||||
|
||||
/**
|
||||
* THE DAY ROLLING OVER, as a dialog that has to be dismissed (Gitea#10).
|
||||
*
|
||||
* "As the game rolls off the end of the day, you get a dialog saying such. Hard to keep track of
|
||||
* time." Nothing on screen was wrong — the clock, the turn chart and the timetable all said which
|
||||
* Day it was — but a Day turns over inside the phases that run themselves, so it happens while the
|
||||
* player is looking at the board waiting for their next turn. The two transient signals the page
|
||||
* already had are both gone in under five seconds.
|
||||
*
|
||||
* Not shown when:
|
||||
* - this is the first frame (`lastDay === null`) — arriving on Day 3 is not Day 2 ending;
|
||||
* - the Day went DOWN, which is Undo stepping back across the rollover, not a Day passing;
|
||||
* - the game finished on that rollover, when the outcome panel is the thing to read instead.
|
||||
*/
|
||||
function noteDayEnd(f: Frame): void {
|
||||
const previous = lastDay;
|
||||
lastDay = f.day;
|
||||
if (previous === null || f.day <= previous) return;
|
||||
if (f.status !== 'active') return;
|
||||
const dlg = document.getElementById('dayenddlg') as HTMLDialogElement | null;
|
||||
const body = document.getElementById('dayendbody');
|
||||
if (!dlg || !body) return;
|
||||
body.innerHTML = dayEndHtml(f);
|
||||
// A second rollover cannot happen while this one is open, but a redraw can — `showModal` throws
|
||||
// on an already-open dialog rather than doing nothing.
|
||||
if (!dlg.open) dlg.showModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* THE FIRST FRAME OF A MULTIPLAYER GAME — the one moment nobody had ever seen drawn.
|
||||
*
|
||||
@@ -1186,6 +1229,8 @@ function render(): void {
|
||||
}
|
||||
lastPhase = f.phaseKey;
|
||||
|
||||
noteDayEnd(f);
|
||||
|
||||
/**
|
||||
* A train completing its run pays every player and nobody took a turn to cause it, so it is said
|
||||
* out loud rather than left in the log. Drained, so it shows once and does not re-fire on a redraw.
|
||||
@@ -1236,6 +1281,9 @@ function renderUndo(): void {
|
||||
// A phase change is announced by comparing against the last frame drawn. Stepping BACK into a
|
||||
// different phase is not that event, so the banner is suppressed rather than fired backwards.
|
||||
lastPhase = null;
|
||||
// Same for the Day: `noteDayEnd` already ignores a Day going down, but undoing back across a
|
||||
// rollover and then replaying forward through it would announce the same Day ending twice.
|
||||
lastDay = null;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1577,20 +1625,21 @@ function renderActions(
|
||||
/**
|
||||
* WHEN NOTHING IN HAND MAY BE DISCARDED, SAY SO AND SAY WHAT TO DO INSTEAD.
|
||||
*
|
||||
* §6.2 as ruled by Jesse (Gitea#6): a train card is never discarded, so a player holding four
|
||||
* trains has exactly one way forward — play one onto the timetable. The rule creates that corner
|
||||
* deliberately and needs no machinery, but it must not be a corner the player has to infer from
|
||||
* a discard button that has quietly stopped appearing.
|
||||
* §6.2 (Gitea#9) leaves two kinds of undiscardable card — an Extra always, and a Timetabled
|
||||
* train in a game whose `discardTimetabled` rule is off. Either way a player holding nothing but
|
||||
* those has exactly one way forward: play one. The rule creates that corner deliberately and
|
||||
* needs no machinery, but it must not be a corner the player has to infer from a discard button
|
||||
* that has quietly stopped appearing. The reason is the card's own (`handKeepWhy`), so this says
|
||||
* what actually refused rather than assuming which of the two rules is in force.
|
||||
*/
|
||||
const stuck = f.handDiscardable.length > 0 && f.handDiscardable.every((d) => !d);
|
||||
const why = f.handKeepWhy.find((w) => w !== null) ?? '';
|
||||
const tip = stuck
|
||||
? 'You may not end a turn holding more than three cards, and a TRAIN CARD IS NEVER ' +
|
||||
'DISCARDED. Every card you hold is a train, so the only way on is to play one onto the ' +
|
||||
'timetable. A train may be held for as many Stages and Days as you like; it just cannot be ' +
|
||||
'thrown away.'
|
||||
? 'You may not end a turn holding more than three cards, and every card you hold is one that ' +
|
||||
`cannot be thrown away. ${why} The only way on is to play one.`
|
||||
: 'You may not end a turn holding more than three cards (four with a Red Flag). Play ' +
|
||||
'one onto the board, or discard one face-up to a Department slot. A train card is never ' +
|
||||
'discardable and can only be played.';
|
||||
'one onto the board, or discard one face-up to a Department slot — where a rival may pick ' +
|
||||
'it up.';
|
||||
html +=
|
||||
`<div class="grp"><button class="act blocked" disabled data-tip="${tip.replace(/"/g, '"')}">` +
|
||||
(stuck
|
||||
@@ -1888,6 +1937,7 @@ if (newBtn && dlg) {
|
||||
houseRules: {
|
||||
startingHand: settings.startingHand,
|
||||
extraStart: settings.extraStart,
|
||||
discardTimetabled: settings.discardTimetabled,
|
||||
revenue: {
|
||||
passengerPerCoach: settings.passengerPerCoach,
|
||||
freightPerLoad: settings.freightPerLoad,
|
||||
|
||||
Reference in New Issue
Block a user