v0.3.0 — playtest fixes, two new rules, and §7 enforced

This commit is contained in:
Jesse
2026-08-12 21:08:28 -04:00
parent 6ead39c530
commit 1bf1e95058
38 changed files with 8514 additions and 5989 deletions
+47 -7
View File
@@ -16,6 +16,8 @@ import {
MOVES_PER_LOCAL_OPS,
MODIFIER_PROFILES,
OFFICE_PROFILES,
OPENING_OTHER,
OPENING_TRACK,
MAINLINE_PROFILES,
mainlineProfile,
TRACK_CARDS,
@@ -173,7 +175,10 @@ function buildPassengerFacility(tier: Parameters<typeof officeProfile>[0]): NonN
outboundBox: [],
inboundBox: [],
capacity: { outbound: p.passengerOut, inbound: p.passengerIn },
menAtWork: [null, null, null],
// No freight pipeline: a Depot, Station and Terminal work passengers only, and the MEN | AT |
// WORK sign is printed "For Freight Facilities" (rules-v0.2.md:452). This was a three-slot array
// of nulls, which read as "three empty boxes" to every renderer that looked.
menAtWork: null,
industryTrack: { length: 0, cars: [] },
laborers: 0,
porters: p.porters,
@@ -244,21 +249,56 @@ export function createGame(opts: SetupOptions): GameState {
const superintendent = argmax(superRolls);
void divisionRolls; // seating is fixed by array order; the roll is recorded for the event log
// §4.6-4.7 - shuffle, deal three each, then turn three face-up as the Department slots.
/**
* §4.6-4.7 — THE OPENING DEAL, dealt from two piles rather than one.
*
* Track is shuffled SEPARATELY from everything else and each player is dealt **3 track cards and 3
* other cards**; the track left over is then shuffled back in and the game runs off one deck as
* before. The player opens holding six against a limit of three, so the first turn is spent
* choosing what to keep — draw as usual, then play or discard down to three (§6.2, enforced on
* `draw.end`, which needs no special case for this).
*
* WHY. A run-around needs five specific pieces in a usable order, and the bot held a turnout and a
* matching-hand curve together on **0.2% of turns** — about once every eight games. Measured five
* ways, that is a SUPPLY problem and not a bot weakness: 91 run-arounds per 100 games when track
* was a private 26-piece supply the player chose from, 29 per 100 once it was drawn, 4 per 60 with
* track in the deck. Dealing the opening district as track restores something close to the
* prototype's private supply without reintroducing an unbounded one, and makes the opening a
* decision rather than a wait.
*
* PROVISIONAL — flagged for review once it has been played. See `TODO.md`.
*/
const deck = buildDeck(config.mode);
const cards = new Map<CardId, Card>();
for (const c of deck) cards.set(c.id, c);
const shuffled = rng.shuffle(deck.map((c) => c.id));
const isTrack = (id: CardId): boolean => cards.get(id)?.kind.kind === 'track';
const ids = deck.map((c) => c.id);
// Shuffled in a fixed order — track first — so the RNG stream is deterministic for a given seed.
const trackPile = rng.shuffle(ids.filter(isTrack));
const otherPile = rng.shuffle(ids.filter((id) => !isTrack(id)));
const hands = new Map<PlayerIndex, CardId[]>();
let cursor = 0;
let trackCursor = 0;
let otherCursor = 0;
for (let i = 0; i < playerCount; i++) {
const p = (superintendent + i) % playerCount;
hands.set(p, shuffled.slice(cursor, cursor + 3));
cursor += 3;
hands.set(p, [
...trackPile.slice(trackCursor, trackCursor + OPENING_TRACK),
...otherPile.slice(otherCursor, otherCursor + OPENING_OTHER),
]);
trackCursor += OPENING_TRACK;
otherCursor += OPENING_OTHER;
}
// The remainder goes back into ONE deck for the rest of the game — the split is an opening-deal
// device only, so track competes for the draw exactly as before from the first draw onward.
const shuffled = rng.shuffle([...trackPile.slice(trackCursor), ...otherPile.slice(otherCursor)]);
// §4.6-4.7 — three cards turned face up beside the deck. Each is the bottom of a Department pile
// that grows as players discard onto it.
// that grows as players discard onto it. Taken after the recombination, so a Department slot can
// show track like any other card.
let cursor = 0;
const departments: CardId[][] = [[], [], []];
for (const pile of departments) {
const card = shuffled[cursor++];