diff --git a/packages/engine/src/game.ts b/packages/engine/src/game.ts index d0ccc68..a77c528 100644 --- a/packages/engine/src/game.ts +++ b/packages/engine/src/game.ts @@ -200,6 +200,12 @@ export interface GameConfig { * standees: green, red, magenta, blue, light blue, yellow). Defaults to * seat order. */ colors?: number[]; + /** + * Deck revision. Absent = the original build, which stored games replay + * against forever. Revision 2 removes LIFESAVER from two-player decks + * ("Not applicable in a 2-player game." — the card face). + */ + deckRev?: number; } export interface GameState { @@ -2895,7 +2901,11 @@ export function createGame(config: GameConfig): { state: GameState; events: Game })), ); - const [deckShuffled, rng3] = shuffle(rng, buildDeck(config.sets)); + let fullDeck = buildDeck(config.sets); + if ((config.deckRev ?? 1) >= 2 && n === 2) { + fullDeck = fullDeck.filter((c) => c.cardId !== "lifesaver"); + } + const [deckShuffled, rng3] = shuffle(rng, fullDeck); rng = rng3; const deck = [...deckShuffled]; const discard: CardInstance[] = []; diff --git a/packages/engine/test/casting.test.ts b/packages/engine/test/casting.test.ts index 570beeb..6da3625 100644 --- a/packages/engine/test/casting.test.ts +++ b/packages/engine/test/casting.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { applyCommand, activePlayer, boardView } from "../src/game"; +import { applyCommand, activePlayer, boardView, createGame } from "../src/game"; import { cellKey, edgeKey, hasLineOfSight, neighbor, type Side } from "../src/board"; import type { CardInstance } from "../src/cards"; import { newGame, must, giveCard, toRound2, faceOff } from "./helpers"; @@ -314,3 +314,17 @@ describe("stored-log compatibility", () => { expect(state.players.find((p) => p.id === defender)!.life).toBe(15 - 5); // fireball: 2 + the 3 }); }); + +describe("deck revisions", () => { + it("revision 2 removes LIFESAVER at two players, and only there", () => { + const two = createGame({ playerIds: ["a", "b"], seed: 9, sets: ["basic", "expansion1"], deckRev: 2 }); + const inGame = (s: typeof two.state) => + [...s.deck, ...s.discard, ...s.players.flatMap((p) => p.hand)].some((c) => c.cardId === "lifesaver"); + expect(inGame(two.state)).toBe(false); + // Three players keep it; old two-player games (no deckRev) keep it too. + const three = createGame({ playerIds: ["a", "b", "c"], seed: 9, sets: ["basic", "expansion1"], deckRev: 2 }); + expect(inGame(three.state)).toBe(true); + const legacy = createGame({ playerIds: ["a", "b"], seed: 9, sets: ["basic", "expansion1"] }); + expect(inGame(legacy.state)).toBe(true); + }); +}); diff --git a/packages/server/src/rooms.ts b/packages/server/src/rooms.ts index 167647d..9c02a8f 100644 --- a/packages/server/src/rooms.ts +++ b/packages/server/src/rooms.ts @@ -157,7 +157,7 @@ export function pickColor(room: Room, playerId: PlayerId, color: number): string return null; } -function startInMemory(room: Room, expansion: boolean, colors?: number[]): { events: GameEvent[] } | { error: string } { +function startInMemory(room: Room, expansion: boolean, colors?: number[], deckRev?: number): { events: GameEvent[] } | { error: string } { const n = room.players.length; if (n < 2 || n > 6) return { error: "supported player counts: 2 to 6" }; const { state, events } = createGame({ @@ -165,6 +165,7 @@ function startInMemory(room: Room, expansion: boolean, colors?: number[]): { eve seed: room.seed, sets: expansion ? ["basic", "expansion1"] : ["basic"], ...(colors ? { colors } : {}), + ...(deckRev ? { deckRev } : {}), }); room.expansion = expansion; room.state = state; @@ -175,9 +176,9 @@ function startInMemory(room: Room, expansion: boolean, colors?: number[]): { eve export function startGame(room: Room, expansion: boolean): { events: GameEvent[] } | { error: string } { if (room.state) return { error: "already started" }; const colors = resolveColors(room); - const result = startInMemory(room, expansion, colors); + const result = startInMemory(room, expansion, colors, 2); if ("error" in result) return result; - appendLine(room.id, { kind: "start", expansion, colors }); + appendLine(room.id, { kind: "start", expansion, colors, deckRev: 2 }); recordRoom(room); return result; } @@ -396,7 +397,7 @@ export function loadPersistedRooms(): void { room.players.push(line.name); room.tokens.set(line.name, joinHash); } else if (line.kind === "start") { - const r = startInMemory(room, line.expansion, line.colors); + const r = startInMemory(room, line.expansion, line.colors, line.deckRev); if ("error" in r) throw new Error(`replay start failed: ${r.error}`); } else if (line.kind === "command") { if (!room.state) throw new Error("command before start in log"); diff --git a/packages/server/src/store.ts b/packages/server/src/store.ts index 6403220..7f6f518 100644 --- a/packages/server/src/store.ts +++ b/packages/server/src/store.ts @@ -31,6 +31,8 @@ export interface StartLine { expansion: boolean; /** Final wizard colors, in player join order. */ colors?: number[]; + /** Deck revision the game was dealt from; absent = original build. */ + deckRev?: number; } export interface CommandLine { diff --git a/packages/web/src/local.svelte.ts b/packages/web/src/local.svelte.ts index b329266..bf17d27 100644 --- a/packages/web/src/local.svelte.ts +++ b/packages/web/src/local.svelte.ts @@ -101,6 +101,7 @@ class LocalGame { seed, sets: expansion ? ["basic", "expansion1"] : ["basic"], ...(colors ? { colors } : {}), + deckRev: 2, }; const { state, events } = createGame(config); this.config = config;