Track becomes a deck card, Departments become decks, and trains must be made up to leave, industry restrictions enforced, update display and add a new game button
This commit is contained in:
+54
-20
@@ -42,14 +42,15 @@ export type TrackGeometry = 'straight' | 'curved' | 'sharpCurved' | 'turnout';
|
||||
export type Hand = 'left' | 'right' | 'none';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Track — a PER-PLAYER supply, not deck cards
|
||||
// Track — deck cards, drawn and played like any other
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export type TrackProfile = {
|
||||
geometry: TrackGeometry;
|
||||
hand: Hand;
|
||||
name: string;
|
||||
perPlayer: number;
|
||||
/** Column B of the sheet, "Number in Deck". */
|
||||
copiesInDeck: number;
|
||||
/** Column J of the sheet, "Train can stop here". Turnouts are blank. */
|
||||
isOperationalRail: boolean;
|
||||
/** Sharp curves: "Any move across this track is doubled (two moves)". */
|
||||
@@ -57,30 +58,42 @@ export type TrackProfile = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 26 pieces per player, 104 for four players. Track is NOT in the Home Office deck — this is the
|
||||
* single biggest structural change from the placeholder, and it means the draw is entirely about
|
||||
* industry, trains and enhancements.
|
||||
* TRACK IS IN THE HOME OFFICE DECK, and is drawn and played like any other card.
|
||||
*
|
||||
* Handedness is PRINTED, not chosen on placement. That supersedes Gap 11.
|
||||
* 104 cards, straight from column B of `docs/Deck cards2.xlsx`. An earlier reading made track a
|
||||
* separate per-player supply of 26 pieces, sitting outside the deck and laid one a turn. That came
|
||||
* from misreading the sheet's LAST column, headed "Track Per Player" — 8/4/4/1/1/4/4 = 26, which is
|
||||
* a note about each player's likely share of 104 across four players, not a second stack of cards.
|
||||
* The sheet's own totals settle it: "Sum other 115", "Total track 104", and a grand total of 231
|
||||
* once the 12 start cards are counted. 115 + 104 + 12 = 231.
|
||||
*
|
||||
* It matters well beyond bookkeeping. Track competes for the draw with industry, trains and
|
||||
* enhancements, so building a district is paid for in cards you did not draw instead — and the
|
||||
* hand-of-three is the real constraint on how fast a railroad grows.
|
||||
*
|
||||
* Handedness is PRINTED, not chosen on placement: it is the diagonal the 45° leg lies on.
|
||||
*/
|
||||
export const TRACK_SUPPLY: readonly TrackProfile[] = [
|
||||
{ geometry: 'straight', hand: 'none', name: 'Straight track', perPlayer: 8, isOperationalRail: true, moveCost: 1 },
|
||||
{ geometry: 'curved', hand: 'left', name: 'Curved track (left)', perPlayer: 4, isOperationalRail: true, moveCost: 1 },
|
||||
{ geometry: 'curved', hand: 'right', name: 'Curved track (right)', perPlayer: 4, isOperationalRail: true, moveCost: 1 },
|
||||
{ geometry: 'sharpCurved', hand: 'left', name: 'Sharp Curved Track (left)', perPlayer: 1, isOperationalRail: true, moveCost: 2 },
|
||||
{ geometry: 'sharpCurved', hand: 'right', name: 'Sharp Curved Track (right)', perPlayer: 1, isOperationalRail: true, moveCost: 2 },
|
||||
{ geometry: 'turnout', hand: 'left', name: 'Turnout (left)', perPlayer: 4, isOperationalRail: false, moveCost: 1 },
|
||||
{ geometry: 'turnout', hand: 'right', name: 'Turnout (right)', perPlayer: 4, isOperationalRail: false, moveCost: 1 },
|
||||
export const TRACK_CARDS: readonly TrackProfile[] = [
|
||||
{ geometry: 'straight', hand: 'none', name: 'Straight track', copiesInDeck: 32, isOperationalRail: true, moveCost: 1 },
|
||||
{ geometry: 'curved', hand: 'left', name: 'Curved track (left)', copiesInDeck: 16, isOperationalRail: true, moveCost: 1 },
|
||||
{ geometry: 'curved', hand: 'right', name: 'Curved track (right)', copiesInDeck: 16, isOperationalRail: true, moveCost: 1 },
|
||||
{ geometry: 'sharpCurved', hand: 'left', name: 'Sharp Curved Track (left)', copiesInDeck: 4, isOperationalRail: true, moveCost: 2 },
|
||||
{ geometry: 'sharpCurved', hand: 'right', name: 'Sharp Curved Track (right)', copiesInDeck: 4, isOperationalRail: true, moveCost: 2 },
|
||||
{ geometry: 'turnout', hand: 'left', name: 'Turnout (left)', copiesInDeck: 16, isOperationalRail: false, moveCost: 1 },
|
||||
{ geometry: 'turnout', hand: 'right', name: 'Turnout (right)', copiesInDeck: 16, isOperationalRail: false, moveCost: 1 },
|
||||
];
|
||||
|
||||
export const TRACK_PER_PLAYER = TRACK_SUPPLY.reduce((n, t) => n + t.perPlayer, 0); // 26
|
||||
/** 104 — the sheet's "Total track". */
|
||||
export const TRACK_IN_DECK = TRACK_CARDS.reduce((n, t) => n + t.copiesInDeck, 0);
|
||||
|
||||
/** Start cards, placed at setup and never shuffled: 4 Whistle Posts, 8 Limits. */
|
||||
export const WHISTLE_POST_SUPPLY = 4;
|
||||
export const LIMITS_SUPPLY = 8;
|
||||
|
||||
/** Legacy alias kept so existing call sites resolve; prefer TRACK_SUPPLY. */
|
||||
export const TRACK_PROFILES = TRACK_SUPPLY;
|
||||
/** Look up a track card's profile by the two things that identify it. */
|
||||
export function trackProfile(geometry: TrackGeometry, hand: Hand): TrackProfile | undefined {
|
||||
return TRACK_CARDS.find((t) => t.geometry === geometry && t.hand === hand);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Offices — Depot 4, Station 2, Terminal 1
|
||||
@@ -174,6 +187,26 @@ export type IndustryProfile = {
|
||||
* ratio while preserving the sheet's proportions exactly: the outbound/inbound balance and the
|
||||
* lockout structure are unchanged, because every kind scales by the same factor.
|
||||
*/
|
||||
/**
|
||||
* LOCKOUTS, from the sheet's "Lockouts" column verbatim:
|
||||
*
|
||||
* Freight house -> Freight house, Grocer's warehouse
|
||||
* Mine Tipple -> Power Plant
|
||||
* Refinery -> Power plant
|
||||
* Power Plant -> Mine Tipple, Refinery
|
||||
* Packing Sheds -> Grocer's Warehouse
|
||||
* Grocer's Warehouse -> Packing Sheds, Freight House
|
||||
*
|
||||
* Every pair is a PRODUCER and the CONSUMER of the same commodity: Mine Tipple makes coal and the
|
||||
* Power Plant burns it, the Refinery makes oil and the Power Plant burns that too, Packing Sheds
|
||||
* fill reefers and the Grocer's Warehouse empties them. You may build one end of a chain or the
|
||||
* other, never both — which is what forces the traffic to run BETWEEN districts rather than in
|
||||
* circles inside one.
|
||||
*
|
||||
* Freight house listing ITSELF is the sheet stating the general rule in the one row where it would
|
||||
* otherwise look like an omission: no two of the same industry in one Office Area. That rule is
|
||||
* enforced for every kind in `isLockedOut`, not repeated in each row here.
|
||||
*/
|
||||
export const INDUSTRY_PROFILES: readonly IndustryProfile[] = [
|
||||
{ kind: 'freightHouse', name: 'Freight House', carTypes: ['boxcar'], flow: 'both', baseOut: 1, baseIn: 1, baseLoaders: 1, lockouts: ['grocersWarehouse'], copies: 6 },
|
||||
{ kind: 'mineTipple', name: 'Mine Tipple', carTypes: ['hopper'], flow: 'outbound', baseOut: 1, baseIn: 0, baseLoaders: 1, lockouts: ['powerPlant'], copies: 6 },
|
||||
@@ -672,7 +705,7 @@ export function lengthProfile(length: GameLength): LengthProfile {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Deck composition — 140 cards
|
||||
// Deck composition
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
@@ -688,6 +721,7 @@ export function isOpponentOnly(category: string): boolean {
|
||||
export function deckComposition(): { category: string; count: number }[] {
|
||||
const sum = (xs: readonly { copies: number }[]): number => xs.reduce((n, x) => n + x.copies, 0);
|
||||
return [
|
||||
{ category: 'track', count: TRACK_IN_DECK },
|
||||
{ category: 'office', count: OFFICE_PROFILES.reduce((n, o) => n + o.copiesInDeck, 0) },
|
||||
{ category: 'industry', count: sum(INDUSTRY_PROFILES) },
|
||||
{ category: 'modifier', count: sum(MODIFIER_PROFILES) },
|
||||
@@ -700,9 +734,9 @@ export function deckComposition(): { category: string; count: number }[] {
|
||||
];
|
||||
}
|
||||
|
||||
export const DECK_SIZE = deckComposition().reduce((n, c) => n + c.count, 0); // 140
|
||||
export const DECK_SIZE = deckComposition().reduce((n, c) => n + c.count, 0);
|
||||
|
||||
/** The solitaire deck drops the 22 opponent-directed cards, leaving 118. */
|
||||
/** The solitaire deck drops the 22 opponent-directed cards. */
|
||||
export const SOLITAIRE_DECK_SIZE = deckComposition()
|
||||
.filter((c) => !isOpponentOnly(c.category))
|
||||
.reduce((n, c) => n + c.count, 0);
|
||||
|
||||
Reference in New Issue
Block a user