v0.4.0 — multiplayer Phases 0 and 1: seat and player split apart, turn state per player, the page behind a Session, and eight seat/player mix-ups fixed with tests that fail without them

This commit is contained in:
Jesse
2026-08-13 14:06:02 -04:00
parent 216006b091
commit 49f8504b05
34 changed files with 1743 additions and 526 deletions
+12 -3
View File
@@ -64,7 +64,14 @@ export const SOLO_CONFIG: GameConfig = {
export type ActionGroup = {
kind: string;
title: string;
actions: { index: number; label: string }[];
/**
* `tip` is the card's own description, resolved HERE rather than by the page.
*
* The button label is short; the hover text used to be looked up with `cardDescription(state, id)`
* from the browser, which needs the whole `GameState`. A remote client has no state, so the menu
* carries it. See `docs/architecture/multiplayer.md` §5.
*/
actions: { index: number; label: string; tip?: string }[];
};
export type Game = {
@@ -175,13 +182,15 @@ export function actionGroups(game: Game): { options: Intent[]; groups: ActionGro
if (actor === null) return { options: [], groups: [] };
const options = legalActions(game.state, actor);
const byKind = new Map<string, { index: number; label: string }[]>();
const byKind = new Map<string, { index: number; label: string; tip?: string }[]>();
options.forEach((intent, index) => {
const label = describeIntent(game.state, intent);
const list = byKind.get(intent.type) ?? [];
const cardId = 'cardId' in intent ? (intent as { cardId: string }).cardId : null;
const tip = cardId ? cardDescription(game.state, cardId) : undefined;
// Orientation variants and duplicate copies describe identically; showing one is enough, and a
// list of forty identical rows hides the real choice rather than presenting it.
if (!list.some((a) => a.label === label)) list.push({ index, label });
if (!list.some((a) => a.label === label)) list.push({ index, label, ...(tip ? { tip } : {}) });
byKind.set(intent.type, list);
});