From b39e4923e17388d502e4af8dee53061527159ff2 Mon Sep 17 00:00:00 2001 From: Lukas Date: Thu, 12 Mar 2026 10:24:26 +0100 Subject: [PATCH] Remove demo combatants and allow empty encounters Empty encounters are now valid (INV-1 updated). New sessions start with zero combatants instead of pre-populated Aria/Brak/Cael. Co-Authored-By: Claude Opus 4.6 --- apps/web/src/hooks/use-encounter.ts | 21 ++++++------------- .../domain/src/__tests__/advance-turn.test.ts | 4 ++-- packages/domain/src/types.ts | 15 ++++++------- 3 files changed, 14 insertions(+), 26 deletions(-) diff --git a/apps/web/src/hooks/use-encounter.ts b/apps/web/src/hooks/use-encounter.ts index 20ebab2..185a4e8 100644 --- a/apps/web/src/hooks/use-encounter.ts +++ b/apps/web/src/hooks/use-encounter.ts @@ -22,7 +22,6 @@ import type { } from "@initiative/domain"; import { combatantId, - createEncounter, isDomainError, creatureId as makeCreatureId, resolveCreatureName, @@ -33,24 +32,16 @@ import { saveEncounter, } from "../persistence/encounter-storage.js"; -function createDemoEncounter(): Encounter { - const result = createEncounter([ - { id: combatantId("1"), name: "Aria" }, - { id: combatantId("2"), name: "Brak" }, - { id: combatantId("3"), name: "Cael" }, - ]); - - if (isDomainError(result)) { - throw new Error(`Failed to create demo encounter: ${result.message}`); - } - - return result; -} +const EMPTY_ENCOUNTER: Encounter = { + combatants: [], + activeIndex: 0, + roundNumber: 1, +}; function initializeEncounter(): Encounter { const stored = loadEncounter(); if (stored !== null) return stored; - return createDemoEncounter(); + return EMPTY_ENCOUNTER; } function deriveNextId(encounter: Encounter): number { diff --git a/packages/domain/src/__tests__/advance-turn.test.ts b/packages/domain/src/__tests__/advance-turn.test.ts index 944cead..e38fa95 100644 --- a/packages/domain/src/__tests__/advance-turn.test.ts +++ b/packages/domain/src/__tests__/advance-turn.test.ts @@ -169,9 +169,9 @@ describe("advanceTurn", () => { }); describe("invariants", () => { - it("INV-1: createEncounter rejects empty combatant list", () => { + it("INV-1: createEncounter accepts empty combatant list", () => { const result = createEncounter([]); - expect(isDomainError(result)).toBe(true); + expect(isDomainError(result)).toBe(false); }); it("INV-2: activeIndex always in bounds across all scenarios", () => { diff --git a/packages/domain/src/types.ts b/packages/domain/src/types.ts index e9dd9d3..e0075a1 100644 --- a/packages/domain/src/types.ts +++ b/packages/domain/src/types.ts @@ -38,8 +38,8 @@ function domainError(code: string, message: string): DomainError { /** * Creates a valid Encounter, enforcing INV-1, INV-2, INV-3. - * - INV-1: At least one combatant required. - * - INV-2: activeIndex defaults to 0 (always in bounds). + * - INV-1: An encounter MAY have zero combatants. + * - INV-2: activeIndex defaults to 0 (always in bounds when combatants exist). * - INV-3: roundNumber defaults to 1 (positive integer). */ export function createEncounter( @@ -47,13 +47,10 @@ export function createEncounter( activeIndex = 0, roundNumber = 1, ): Encounter | DomainError { - if (combatants.length === 0) { - return domainError( - "invalid-encounter", - "An encounter must have at least one combatant", - ); - } - if (activeIndex < 0 || activeIndex >= combatants.length) { + if ( + combatants.length > 0 && + (activeIndex < 0 || activeIndex >= combatants.length) + ) { return domainError( "invalid-encounter", `activeIndex ${activeIndex} out of bounds for ${combatants.length} combatants`,