v0.8.5 — housekeeping from the audit, and the playtest line retired
The third release from the audit; nothing a player sees changes. CHANGELOG has the detail. The 0.4.9 playtest line is no longer maintained (Jesse, 2026-09-29): the deploy rule that existed for it is gone and #85 is moot. The table test (#39 #35 #42a #40) is closed — every line of the checklist was met at a table. #46 is done and cannot regrow: the 36 unused declarations are removed and `noUnusedLocals`/`noUnusedParameters` are on; two of them were dead bot functions from rejected candidates the round said it had deleted. The documents no longer teach `trainCapSlack` (a knob that throws), point at `as-built.md` (deleted in 0.8.2), model `officeType` (the engine says `tier`) or describe `collisionOccurred` (never emitted); the README's account of bot flags now matches the bot's. Five playtest saves committed in `docs/` against the repository's own rule are in the ignored `playtests/`. What the audit found and did not fix is written down as TODO #112-#117, each with its reason. #112 is `docs/plans/structure.md`, the proposal for `http.ts`, `main.ts` and `check`. #117 — `/api/save` hands a seat the seed mid-game — waits on a conversation. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FrCWubm9GAftYCm2hWdKwK
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
e47cd3d400
commit
04ca74c365
@@ -730,7 +730,6 @@ function entryConflict(
|
||||
|
||||
/** Puts a train onto a Mainline card with its crossing time already computed. */
|
||||
function enterMainline(
|
||||
s: GameState,
|
||||
node: Extract<DivisionNode, { kind: 'mainline' }>,
|
||||
id: TrayId,
|
||||
tray: CrewTray,
|
||||
@@ -862,7 +861,7 @@ function moveTrain(s: GameState, id: TrayId, tray: CrewTray, events: GameEvent[]
|
||||
if (conflict === 'held') return 'held';
|
||||
if (conflict === 'collided') return 'moved';
|
||||
|
||||
enterMainline(s, node, id, tray, target);
|
||||
enterMainline(node, id, tray, target);
|
||||
const dp = s.division.nodes[dpIndex];
|
||||
if (dp?.kind === 'divisionPoint') dp.holding = dp.holding.filter((t) => t !== id);
|
||||
events.push({
|
||||
@@ -926,7 +925,7 @@ function moveTrain(s: GameState, id: TrayId, tray: CrewTray, events: GameEvent[]
|
||||
// The wreck's A/D track is released by `collide` itself, which is why it has to be.
|
||||
if (conflict === 'collided') return 'moved';
|
||||
|
||||
enterMainline(s, node, id, tray, target);
|
||||
enterMainline(node, id, tray, target);
|
||||
area.adOccupancy = area.adOccupancy.filter((t) => t !== id);
|
||||
events.push({
|
||||
type: 'trainHighballed',
|
||||
@@ -998,7 +997,7 @@ function moveTrain(s: GameState, id: TrayId, tray: CrewTray, events: GameEvent[]
|
||||
if (conflict === 'collided') return 'moved';
|
||||
|
||||
node.holding = node.holding.filter((t) => t !== id);
|
||||
enterMainline(s, node, id, tray, index, true);
|
||||
enterMainline(node, id, tray, index, true);
|
||||
events.push({
|
||||
type: 'trainHighballed',
|
||||
trainNumber: tray.trainNumber ?? 0,
|
||||
@@ -1400,7 +1399,7 @@ function redFlagStop(
|
||||
return c?.kind.kind === 'maneuver' && c.kind.key === 'redFlags';
|
||||
});
|
||||
if (!holdsFlag) return 'proceed';
|
||||
if (!arrivalWouldCollide(s, id, tray, dest.seat)) return 'proceed';
|
||||
if (!arrivalWouldCollide(s, dest.seat)) return 'proceed';
|
||||
|
||||
s.clock.pendingDecision = { kind: 'redFlag', train: id, seat: dest.seat, from };
|
||||
return 'ask';
|
||||
@@ -1413,7 +1412,7 @@ function redFlagStop(
|
||||
* if these two ever diverge, the prompt offers a flag against a collision that will not happen, or
|
||||
* stays silent before one that will.
|
||||
*/
|
||||
function arrivalWouldCollide(s: GameState, id: TrayId, tray: CrewTray, seat: SeatIndex): boolean {
|
||||
function arrivalWouldCollide(s: GameState, seat: SeatIndex): boolean {
|
||||
const area = areaAtSeat(s, seat);
|
||||
const hasInterlocking = [...area.grid.values()].some((c) => c.enhancements.includes('interlocking'));
|
||||
const full = area.adOccupancy.length >= officeProfile(area.tier).adTracks;
|
||||
|
||||
+6
-10
@@ -16,7 +16,6 @@
|
||||
|
||||
import {
|
||||
FREIGHT_PROFILES,
|
||||
LABORER_ACTIONS_PER_LOAD,
|
||||
MAX_CONSIST,
|
||||
REALIGNMENTS,
|
||||
consistSize,
|
||||
@@ -65,7 +64,6 @@ import {
|
||||
pooled,
|
||||
railFacingOf,
|
||||
seatOf,
|
||||
spaceOn,
|
||||
standingSides,
|
||||
trackOrder,
|
||||
turnOf,
|
||||
@@ -647,7 +645,7 @@ function passengerWork(
|
||||
* crew is unaffected by all of them.
|
||||
*/
|
||||
/** Which district a tray is standing in; 0 when it is out on the Division. */
|
||||
function trayySeat(tray: CrewTray): SeatIndex {
|
||||
function traySeat(tray: CrewTray): SeatIndex {
|
||||
return tray.position.at === 'grid' ? tray.position.seat : 0;
|
||||
}
|
||||
|
||||
@@ -1578,7 +1576,7 @@ function checkPlay(
|
||||
return target && target.kind === 'mainline' ? null : 'NOT_CONNECTED';
|
||||
}
|
||||
if (!placement) return 'NO_PLACEMENT';
|
||||
return checkEnhancementPlacement(s, area, card.kind.key, placement);
|
||||
return checkEnhancementPlacement(area, card.kind.key, placement);
|
||||
}
|
||||
|
||||
case 'mainlineModifier':
|
||||
@@ -1587,7 +1585,7 @@ function checkPlay(
|
||||
// copy uses the enhancement's grid placement rather than going onto a Mainline card.
|
||||
if (card.kind.key === 'facingPointLocksMainline') {
|
||||
if (!placement) return 'NO_PLACEMENT';
|
||||
return checkEnhancementPlacement(s, area, 'facingPointLocks', placement);
|
||||
return checkEnhancementPlacement(area, 'facingPointLocks', placement);
|
||||
}
|
||||
// The rest are laid on a Mainline card, which is not a grid coordinate — see
|
||||
// `mainline.modify`.
|
||||
@@ -1830,7 +1828,6 @@ function execute(s: GameState, player: PlayerIndex, i: Intent): GameEvent[] {
|
||||
const from = trayCoord(s, i.trayId)!;
|
||||
const dests = destinationsFor(s, player, i.trayId, from, i.reverse);
|
||||
const dest = selectDestination(dests, i.to, i.via)!;
|
||||
const tray = s.trays.get(i.trayId)!;
|
||||
// The port the crew pulls out THROUGH — the same one `destinationsFor` explored from, so the
|
||||
// cut it recouples on the way out is the cut the walk counted.
|
||||
const facingNow = facingPort(s, i.trayId);
|
||||
@@ -2454,7 +2451,7 @@ export function reduce(s: GameState, e: GameEvent): void {
|
||||
* front of `stock` is a drop being undone, so it is refunded on the square it was left on
|
||||
* instead of being charged again at the far end.
|
||||
*/
|
||||
const owner = playerAtSeat(s, trayySeat(tray));
|
||||
const owner = playerAtSeat(s, traySeat(tray));
|
||||
const taken = e.recoupled ? e.stock.slice(e.recoupled.stock.length) : e.stock;
|
||||
spendFreightBudget(s, owner, tray, e.at, taken);
|
||||
if (e.recoupled) refundFreightBudget(s, owner, tray, e.recoupled.at, e.recoupled.stock);
|
||||
@@ -2477,7 +2474,7 @@ export function reduce(s: GameState, e: GameEvent): void {
|
||||
*/
|
||||
tray.engineAt = e.engineAt;
|
||||
// "Spends one move in the yard" — the sort costs a Move.
|
||||
const sorter = turnOf(s, playerAtSeat(s, trayySeat(tray)));
|
||||
const sorter = turnOf(s, playerAtSeat(s, traySeat(tray)));
|
||||
sorter.movesRemaining = Math.max(0, sorter.movesRemaining - 1);
|
||||
break;
|
||||
}
|
||||
@@ -2524,7 +2521,7 @@ export function reduce(s: GameState, e: GameEvent): void {
|
||||
// cars set out to the east leave it where it was.
|
||||
card.standingWest = onWestSide ? k + cut.length : k;
|
||||
}
|
||||
spendFreightBudget(s, playerAtSeat(s, trayySeat(tray)), tray, e.at, e.stock);
|
||||
spendFreightBudget(s, playerAtSeat(s, traySeat(tray)), tray, e.at, e.stock);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -3184,7 +3181,6 @@ export function watertowersRemovable(area: OfficeArea): GridCoord[] {
|
||||
* - ABS Signals → a Mainline card, not the Office Area
|
||||
*/
|
||||
export function checkEnhancementPlacement(
|
||||
s: GameState,
|
||||
area: OfficeArea,
|
||||
key: string,
|
||||
placement: GridCoord,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* An intent is a PROPOSAL. It may be rejected. Contrast with an event (events.ts), which is a fact.
|
||||
*/
|
||||
|
||||
import type { CarType, Direction, Hand, OfficeTier, TrackGeometry } from './content.ts';
|
||||
import type { CarType, Direction, OfficeTier } from './content.ts';
|
||||
import type { CardId, GridCoord, PlayerIndex, SeatIndex, TrayId } from './state.ts';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
+1
-4
@@ -12,7 +12,7 @@
|
||||
* If you find yourself writing a rule here, it belongs in apply.ts.
|
||||
*/
|
||||
|
||||
import type { CarType, Hand, TrackGeometry } from './content.ts';
|
||||
import type { CarType } from './content.ts';
|
||||
import { enhancementRule, mainlineProfile } from './content.ts';
|
||||
import { check, areaOf, destinationsFor, withRouteCache } from './apply.ts';
|
||||
import type { Intent } from './intents.ts';
|
||||
@@ -191,9 +191,6 @@ export function legalSwitchingActions(s: GameState, player: PlayerIndex): Intent
|
||||
return withRouteCache(s, () => switchCandidates(s, player).filter((i) => check(s, player, i) === null));
|
||||
}
|
||||
|
||||
export function isLegal(s: GameState, player: PlayerIndex, i: Intent): boolean {
|
||||
return check(s, player, i) === null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Candidate generation. Over-generates freely — `check` is the authority, so a candidate that
|
||||
|
||||
@@ -67,9 +67,3 @@ export function createRng(seed: number): Rng {
|
||||
};
|
||||
}
|
||||
|
||||
/** Restore an RNG mid-stream, for rebuilding a game from a snapshot. */
|
||||
export function restoreRng(state: number): Rng {
|
||||
// mulberry32 advances from its state before drawing, and createRng seeds state directly,
|
||||
// so restoring is just seeding with the saved state.
|
||||
return createRng(state);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
OPENING_DEALS,
|
||||
MAINLINE_DECK,
|
||||
houseRules,
|
||||
mainlineProfile,
|
||||
TRACK_CARDS,
|
||||
ROLLING_STOCK_SUPPLY,
|
||||
STAGES_PER_DAY,
|
||||
|
||||
Reference in New Issue
Block a user