v0.4.4 — New Game dialog for the opening hand and the three revenue rates, Yard renamed Interchange, and two movement bugs behind a mirrored consist
This commit is contained in:
+43
-37
@@ -16,9 +16,9 @@ import {
|
||||
MOVES_PER_LOCAL_OPS,
|
||||
MODIFIER_PROFILES,
|
||||
OFFICE_PROFILES,
|
||||
OPENING_OTHER,
|
||||
OPENING_TRACK,
|
||||
OPENING_DEALS,
|
||||
MAINLINE_PROFILES,
|
||||
houseRules,
|
||||
mainlineProfile,
|
||||
TRACK_CARDS,
|
||||
ROLLING_STOCK_SUPPLY,
|
||||
@@ -287,53 +287,59 @@ export function createGame(opts: SetupOptions): GameState {
|
||||
.sort((a, b) => divisionRolls[a]! - divisionRolls[b]! || b - a);
|
||||
|
||||
/**
|
||||
* §4.6-4.7 — THE OPENING DEAL, dealt from two piles rather than one.
|
||||
* §4.6-4.7 — THE OPENING DEAL, in whichever of the three shapes this game was dealt with.
|
||||
*
|
||||
* 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
|
||||
* `threeRandom` and `sixRandom` deal off ONE shuffled deck; `threeTrackThreeOther` shuffles track
|
||||
* separately and deals three of each, then shuffles the remainder back together so the game runs
|
||||
* off one deck from the first draw onward either way. `content.ts` says what each option is for.
|
||||
*
|
||||
* A player dealt six holds six against a limit of three, on purpose: 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).
|
||||
* `draw.end`, which needs no special case for this). A player dealt three is already at the limit.
|
||||
*
|
||||
* 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`.
|
||||
* §4.7 — "starting from the Superintendent, deal each player…", which proceeds round the table and
|
||||
* is therefore SEAT order, not player order. That is the same in all three shapes.
|
||||
*/
|
||||
const rules = houseRules(config);
|
||||
const deal = OPENING_DEALS[rules.startingHand];
|
||||
const deck = buildDeck(config.mode);
|
||||
const cards = new Map<CardId, Card>();
|
||||
for (const c of deck) cards.set(c.id, c);
|
||||
|
||||
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 trackCursor = 0;
|
||||
let otherCursor = 0;
|
||||
// §4.7 — "starting from the Superintendent, deal each player…", which proceeds round the table
|
||||
// and is therefore seat order, not player order.
|
||||
const superSeat = seating.indexOf(superintendent);
|
||||
for (let i = 0; i < playerCount; i++) {
|
||||
const p = seating[(superSeat + i) % playerCount]!;
|
||||
hands.set(p, [
|
||||
...trackPile.slice(trackCursor, trackCursor + OPENING_TRACK),
|
||||
...otherPile.slice(otherCursor, otherCursor + OPENING_OTHER),
|
||||
]);
|
||||
trackCursor += OPENING_TRACK;
|
||||
otherCursor += OPENING_OTHER;
|
||||
}
|
||||
const hands = new Map<PlayerIndex, CardId[]>();
|
||||
const dealtTo = (i: number): PlayerIndex => seating[(superSeat + i) % playerCount]!;
|
||||
|
||||
// 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)]);
|
||||
let shuffled: CardId[];
|
||||
if (deal.any > 0) {
|
||||
// One deck, one shuffle, the top cards off it — the prototype's own deal.
|
||||
const single = rng.shuffle(ids);
|
||||
let cut = 0;
|
||||
for (let i = 0; i < playerCount; i++) {
|
||||
hands.set(dealtTo(i), single.slice(cut, cut + deal.any));
|
||||
cut += deal.any;
|
||||
}
|
||||
shuffled = single.slice(cut);
|
||||
} else {
|
||||
// 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)));
|
||||
let trackCursor = 0;
|
||||
let otherCursor = 0;
|
||||
for (let i = 0; i < playerCount; i++) {
|
||||
hands.set(dealtTo(i), [
|
||||
...trackPile.slice(trackCursor, trackCursor + deal.track),
|
||||
...otherPile.slice(otherCursor, otherCursor + deal.other),
|
||||
]);
|
||||
trackCursor += deal.track;
|
||||
otherCursor += deal.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.
|
||||
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. Taken after the recombination, so a Department slot can
|
||||
|
||||
Reference in New Issue
Block a user