import { describe, expect, it } from "vitest"; import { advanceTurn, isDomainError } from "../advance-turn.js"; import type { DomainEvent } from "../events.js"; import { type Combatant, combatantId, createEncounter, type Encounter, } from "../types.js"; // --- Helpers --- function makeCombatant(name: string): Combatant { return { id: combatantId(name), name }; } const A = makeCombatant("A"); const B = makeCombatant("B"); const C = makeCombatant("C"); function encounter( combatants: Combatant[], activeIndex: number, roundNumber: number, ): Encounter { const result = createEncounter(combatants, activeIndex, roundNumber); if (isDomainError(result)) { throw new Error(`Test setup failed: ${result.message}`); } return result; } function successResult(enc: Encounter) { const result = advanceTurn(enc); if (isDomainError(result)) { throw new Error(`Expected success, got error: ${result.message}`); } return result; } // --- Acceptance Scenarios --- describe("advanceTurn", () => { describe("acceptance scenarios", () => { it("scenario 1: advances from first to second combatant", () => { const enc = encounter([A, B, C], 0, 1); const { encounter: next, events } = successResult(enc); expect(next.activeIndex).toBe(1); expect(next.roundNumber).toBe(1); expect(events).toEqual([ { type: "TurnAdvanced", previousCombatantId: combatantId("A"), newCombatantId: combatantId("B"), roundNumber: 1, }, ]); }); it("scenario 2: advances from second to third combatant", () => { const enc = encounter([A, B, C], 1, 1); const { encounter: next, events } = successResult(enc); expect(next.activeIndex).toBe(2); expect(next.roundNumber).toBe(1); expect(events).toEqual([ { type: "TurnAdvanced", previousCombatantId: combatantId("B"), newCombatantId: combatantId("C"), roundNumber: 1, }, ]); }); it("scenario 3: wraps from last combatant to first, increments round", () => { const enc = encounter([A, B, C], 2, 1); const { encounter: next, events } = successResult(enc); expect(next.activeIndex).toBe(0); expect(next.roundNumber).toBe(2); expect(events).toEqual([ { type: "TurnAdvanced", previousCombatantId: combatantId("C"), newCombatantId: combatantId("A"), roundNumber: 2, }, { type: "RoundAdvanced", newRoundNumber: 2, }, ]); }); it("scenario 4: wraps at round 5 to round 6 (not hardcoded)", () => { const enc = encounter([A, B, C], 2, 5); const { encounter: next, events } = successResult(enc); expect(next.activeIndex).toBe(0); expect(next.roundNumber).toBe(6); expect(events[0]).toMatchObject({ type: "TurnAdvanced", roundNumber: 6, }); expect(events[1]).toEqual({ type: "RoundAdvanced", newRoundNumber: 6, }); }); it("scenario 5: single combatant always wraps", () => { const enc = encounter([A], 0, 1); const { encounter: next, events } = successResult(enc); expect(next.activeIndex).toBe(0); expect(next.roundNumber).toBe(2); expect(events).toEqual([ { type: "TurnAdvanced", previousCombatantId: combatantId("A"), newCombatantId: combatantId("A"), roundNumber: 2, }, { type: "RoundAdvanced", newRoundNumber: 2, }, ]); }); it("scenario 6: two advances on a 2-combatant encounter completes a round", () => { const enc = encounter([A, B], 0, 1); const first = successResult(enc); expect(first.encounter.activeIndex).toBe(1); expect(first.encounter.roundNumber).toBe(1); const second = successResult(first.encounter); expect(second.encounter.activeIndex).toBe(0); expect(second.encounter.roundNumber).toBe(2); }); it("scenario 7: empty combatant list returns error", () => { const enc: Encounter = { combatants: [], activeIndex: 0, roundNumber: 1, }; const result = advanceTurn(enc); expect(isDomainError(result)).toBe(true); if (isDomainError(result)) { expect(result.code).toBe("invalid-encounter"); } }); it("scenario 8: three advances on [A,B,C] completes a full round cycle", () => { let enc = encounter([A, B, C], 0, 1); enc = successResult(enc).encounter; enc = successResult(enc).encounter; enc = successResult(enc).encounter; expect(enc.activeIndex).toBe(0); expect(enc.roundNumber).toBe(2); }); }); describe("invariants", () => { it("INV-1: createEncounter accepts empty combatant list", () => { const result = createEncounter([]); expect(isDomainError(result)).toBe(false); }); it("INV-2: activeIndex always in bounds across all scenarios", () => { const combatants = [A, B, C]; let enc = encounter(combatants, 0, 1); for (let i = 0; i < 10; i++) { const result = successResult(enc); expect(result.encounter.activeIndex).toBeGreaterThanOrEqual(0); expect(result.encounter.activeIndex).toBeLessThan(combatants.length); enc = result.encounter; } }); it("INV-3: roundNumber never decreases", () => { let enc = encounter([A, B, C], 0, 1); let prevRound = enc.roundNumber; for (let i = 0; i < 10; i++) { const result = successResult(enc); expect(result.encounter.roundNumber).toBeGreaterThanOrEqual(prevRound); prevRound = result.encounter.roundNumber; enc = result.encounter; } }); it("INV-4: determinism — same input produces same output", () => { const enc = encounter([A, B, C], 1, 3); const result1 = advanceTurn(enc); const result2 = advanceTurn(enc); expect(result1).toEqual(result2); }); it("INV-5: every success emits at least TurnAdvanced", () => { const scenarios: Encounter[] = [ encounter([A, B, C], 0, 1), encounter([A, B, C], 2, 1), encounter([A], 0, 1), ]; for (const enc of scenarios) { const result = successResult(enc); const hasTurnAdvanced = result.events.some( (e: DomainEvent) => e.type === "TurnAdvanced", ); expect(hasTurnAdvanced).toBe(true); } }); it("event ordering: on wrap, events are [TurnAdvanced, RoundAdvanced]", () => { const enc = encounter([A, B, C], 2, 1); const { events } = successResult(enc); expect(events).toHaveLength(2); expect(events[0].type).toBe("TurnAdvanced"); expect(events[1].type).toBe("RoundAdvanced"); }); }); });