Fix caching, Limits placement, and explain the board better

This commit is contained in:
Jesse
2026-07-31 23:34:18 -04:00
parent 2eca9de09f
commit dd300ac154
10 changed files with 466 additions and 26 deletions
+31 -2
View File
@@ -32,7 +32,14 @@ import type { GameConfig, GameState, PlayerIndex } from '../engine/state.ts';
import { narrate } from '../sim/narrate.ts';
// Import from the view module, NOT replay.ts — replay.ts writes files and reads process.argv,
// which would pull node:fs into a browser bundle.
import { cardName, describeIntent, geometryLabel, snapshot, variantLabel } from '../sim/view.ts';
import {
cardName,
describeIntent,
geometryLabel,
snapshot,
trainName,
variantLabel,
} from '../sim/view.ts';
import type { TrackGeometry } from '../engine/content.ts';
import { variantsFor } from '../engine/track.ts';
import type { Frame } from '../sim/view.ts';
@@ -258,6 +265,25 @@ export function submit(game: Game, intent: Intent): boolean {
return true;
}
/**
* Which cards in hand can be played RIGHT NOW, in hand order.
*
* A hand where some cards are simply unplayable — a Station upgrade before the Office is a Depot, a
* Modifier with no Facility to sit beside — looks identical to one where everything is available.
* Derived from `legalActions`, so it cannot disagree with what the buttons offer.
*/
export function handPlayable(game: Game): boolean[] {
const hand = game.state.decks.hands.get(0) ?? [];
const actor = currentActor(game);
if (actor === null) return hand.map(() => false);
const playable = new Set(
legalActions(game.state, actor)
.filter((i) => i.type === 'card.play')
.map((i) => (i as Extract<Intent, { type: 'card.play' }>).cardId),
);
return hand.map((id) => playable.has(id));
}
/** The board as the replay draws it, so the live game and the replay agree. */
export function view(game: Game): Frame {
return snapshot(game.state, [], null);
@@ -267,7 +293,10 @@ function record(game: Game, events: GameEvent[]): void {
for (const e of events) {
// The same filter the replay uses: actor changes and phase bookkeeping are noise on screen.
if (e.type === 'actorChanged') continue;
const n = narrate(e, { cardName: (id) => cardName(game.state, id) });
const n = narrate(e, {
cardName: (id) => cardName(game.state, id),
trainName: (id) => trainName(game.state, id),
});
game.log.push({ text: n.text, tone: n.tone });
}
// Keep the log bounded; the full history lives in `history` and can be replayed.