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:
+58
-15
@@ -41,8 +41,10 @@ import {
|
||||
variantLabel,
|
||||
} from '../sim/view.ts';
|
||||
import { HAND_LIMIT, trainProfile } from '../engine/content.ts';
|
||||
import type { TrackGeometry } from '../engine/content.ts';
|
||||
import { variantsFor } from '../engine/track.ts';
|
||||
import type { Hand, TrackGeometry } from '../engine/content.ts';
|
||||
import type { Port } from '../engine/track.ts';
|
||||
import { joins, neighbour, variantsFor } from '../engine/track.ts';
|
||||
import { areaOf } from '../engine/apply.ts';
|
||||
import type { Frame } from '../sim/view.ts';
|
||||
|
||||
export const SOLO_CONFIG: GameConfig = {
|
||||
@@ -103,7 +105,6 @@ const GROUP_ORDER: readonly { prefix: string; title: string }[] = [
|
||||
{ prefix: 'card.play', title: 'Play a card from my hand' },
|
||||
{ prefix: 'card.discard', title: 'Discard a card from my hand' },
|
||||
{ prefix: 'draw.end', title: 'Finish' },
|
||||
{ prefix: 'track.lay', title: 'Lay track from your supply' },
|
||||
{ prefix: 'mainline.modify', title: 'Mainline modifiers' },
|
||||
// American spelling throughout, to match MANEUVER_CARDS and the source deck.
|
||||
{ prefix: 'maneuver.', title: 'Maneuvers' },
|
||||
@@ -262,19 +263,18 @@ function subjectOf(
|
||||
): { subjectKey: string; subject: string; spot: string; coord: { row: number; col: number } } | null {
|
||||
const at = (c: { row: number; col: number }): string => `(${c.row}, ${c.col})`;
|
||||
if (i.type === 'card.play' && i.placement) {
|
||||
// A track card is an ordinary card play; its rotation and what it would meet are the whole of
|
||||
// the decision, so they ride on the spot rather than being left for the player to work out.
|
||||
const kind = game.state.cards.get(i.cardId)?.kind;
|
||||
const track =
|
||||
kind?.kind === 'track'
|
||||
? rotationNote(kind.geometry, i.variant, kind.hand) +
|
||||
joinsNote(game, kind.geometry, kind.hand, i.variant, i.placement)
|
||||
: rotationNote(null, i.variant);
|
||||
return {
|
||||
subjectKey: `card:${i.cardId}`,
|
||||
subject: cardName(game.state, i.cardId),
|
||||
spot: `${at(i.placement)}${rotationNote(null, i.variant)}`,
|
||||
coord: i.placement,
|
||||
};
|
||||
}
|
||||
if (i.type === 'track.lay') {
|
||||
const hand = i.hand === 'none' ? '' : `${i.hand}-hand `;
|
||||
return {
|
||||
subjectKey: `track:${i.geometry}:${i.hand}`,
|
||||
subject: `${hand}${geometryLabel(i.geometry)}`,
|
||||
spot: `${at(i.placement)}${rotationNote(i.geometry, i.variant)}`,
|
||||
spot: `${at(i.placement)}${track}`,
|
||||
coord: i.placement,
|
||||
};
|
||||
}
|
||||
@@ -285,10 +285,53 @@ function subjectOf(
|
||||
* Rotations share a square, so the square alone does not identify the choice — and "rotation 2"
|
||||
* does not tell a player which way the rail will run, which for a curve or turnout is the entire
|
||||
* decision. Only shown when there is more than one way to lay the piece.
|
||||
*
|
||||
* HAND IS NOT OPTIONAL HERE. It decides which diagonal the 45° leg lies on, so `variantsFor` without
|
||||
* it answers for the left-hand card whatever you are actually holding: every right-hand turnout was
|
||||
* offered as "stem west, through east, diverges south" — the mirror of the card it would lay — and
|
||||
* every right-hand curve named the wrong edge. The placement was right and the description was
|
||||
* backwards, which is worse than no description at all.
|
||||
*/
|
||||
function rotationNote(geometry: TrackGeometry | null, variant: number | undefined): string {
|
||||
function rotationNote(
|
||||
geometry: TrackGeometry | null,
|
||||
variant: number | undefined,
|
||||
hand: Hand = 'none',
|
||||
): string {
|
||||
if (geometry === null) return variant === undefined || variant === 0 ? '' : ` — option ${variant + 1}`;
|
||||
return variantsFor(geometry).length > 1 ? variantLabel(geometry, variant) : '';
|
||||
return variantsFor(geometry, hand).length > 1 ? variantLabel(geometry, variant, hand) : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* WHAT THIS PLACEMENT WOULD ACTUALLY CONNECT TO.
|
||||
*
|
||||
* Two cards meeting at an edge is not a rail — on a north or south edge their 45° legs must also lie
|
||||
* on the same diagonal — so "is this square legal" and "does this piece join the one I am aiming at"
|
||||
* are different questions, and only the first was on screen. A player building a crossover down onto
|
||||
* a siding had to pick a hand and a rotation and find out afterwards.
|
||||
*/
|
||||
function joinsNote(
|
||||
game: Game,
|
||||
geometry: TrackGeometry,
|
||||
hand: Hand,
|
||||
variant: number | undefined,
|
||||
placement: { row: number; col: number },
|
||||
): string {
|
||||
const v = variantsFor(geometry, hand)[variant ?? 0];
|
||||
if (!v) return '';
|
||||
const area = areaOf(game.state, 0);
|
||||
const probe = {
|
||||
geometry: { kind: 'track', geometry, ...v, ...(hand !== 'none' ? { hand } : {}) },
|
||||
baseOperationalRail: true, standing: [], facility: null, modifiers: [], enhancements: [],
|
||||
} as never;
|
||||
|
||||
const where: Record<Port, string> = { n: 'above', s: 'below', e: 'to the east', w: 'to the west' };
|
||||
const met: string[] = [];
|
||||
for (const p of ['n', 's', 'w', 'e'] as Port[]) {
|
||||
const n = neighbour(placement, p);
|
||||
const nb = area.grid.get(`${n.row},${n.col}`);
|
||||
if (nb && joins(probe, p, nb)) met.push(where[p]);
|
||||
}
|
||||
return met.length === 0 ? '' : ` · joins the track ${met.join(' and ')}`;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user