import { describe, expect, it } from "vitest"; import type { DomainEvent } from "../events.js"; import { retreatTurn } from "../retreat-turn.js"; import { type Combatant, combatantId, createEncounter, type Encounter, isDomainError, } from "../types.js"; import { expectDomainError } from "./test-helpers.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 = retreatTurn(enc); if (isDomainError(result)) { throw new Error(`Expected success, got error: ${result.message}`); } return result; } // --- Acceptance Scenarios --- describe("retreatTurn", () => { describe("acceptance scenarios", () => { it("scenario 1: mid-round retreat — retreats from second to first combatant", () => { const enc = encounter([A, B, C], 1, 1); const { encounter: next, events } = successResult(enc); expect(next.activeIndex).toBe(0); expect(next.roundNumber).toBe(1); expect(events).toEqual([ { type: "TurnRetreated", previousCombatantId: combatantId("B"), newCombatantId: combatantId("A"), roundNumber: 1, }, ]); }); it("scenario 2: round-boundary retreat — wraps from first combatant to last, decrements round", () => { const enc = encounter([A, B, C], 0, 2); const { encounter: next, events } = successResult(enc); expect(next.activeIndex).toBe(2); expect(next.roundNumber).toBe(1); expect(events).toEqual([ { type: "TurnRetreated", previousCombatantId: combatantId("A"), newCombatantId: combatantId("C"), roundNumber: 1, }, { type: "RoundRetreated", newRoundNumber: 1, }, ]); }); it("scenario 3: start-of-encounter error — cannot retreat at round 1 index 0", () => { const enc = encounter([A, B, C], 0, 1); const result = retreatTurn(enc); expectDomainError(result, "no-previous-turn"); }); it("scenario 4: single-combatant retreat — wraps to same combatant, decrements round", () => { const enc = encounter([A], 0, 2); const { encounter: next, events } = successResult(enc); expect(next.activeIndex).toBe(0); expect(next.roundNumber).toBe(1); expect(events).toEqual([ { type: "TurnRetreated", previousCombatantId: combatantId("A"), newCombatantId: combatantId("A"), roundNumber: 1, }, { type: "RoundRetreated", newRoundNumber: 1, }, ]); }); it("scenario 5: empty-encounter error", () => { const enc: Encounter = { combatants: [], activeIndex: 0, roundNumber: 1, }; const result = retreatTurn(enc); expectDomainError(result, "invalid-encounter"); }); }); describe("invariants", () => { it("determinism — same input produces same output", () => { const enc = encounter([A, B, C], 1, 3); const result1 = retreatTurn(enc); const result2 = retreatTurn(enc); expect(result1).toEqual(result2); }); it("activeIndex always in bounds after retreat", () => { const combatants = [A, B, C]; // Start at round 4 so we can retreat many times let enc = encounter(combatants, 2, 4); for (let i = 0; i < 9; i++) { const result = successResult(enc); expect(result.encounter.activeIndex).toBeGreaterThanOrEqual(0); expect(result.encounter.activeIndex).toBeLessThan(combatants.length); enc = result.encounter; } }); it("roundNumber never goes below 1", () => { let enc = encounter([A, B, C], 2, 2); // Retreat through rounds — should stop at round 1 index 0 while (!(enc.roundNumber === 1 && enc.activeIndex === 0)) { const result = successResult(enc); expect(result.encounter.roundNumber).toBeGreaterThanOrEqual(1); enc = result.encounter; } }); it("every success emits at least TurnRetreated", () => { const scenarios: Encounter[] = [ encounter([A, B, C], 1, 1), encounter([A, B, C], 0, 2), encounter([A], 0, 2), ]; for (const enc of scenarios) { const result = successResult(enc); const hasTurnRetreated = result.events.some( (e: DomainEvent) => e.type === "TurnRetreated", ); expect(hasTurnRetreated).toBe(true); } }); it("event ordering: on wrap, events are [TurnRetreated, RoundRetreated]", () => { const enc = encounter([A, B, C], 0, 2); const { events } = successResult(enc); expect(events).toHaveLength(2); expect(events[0].type).toBe("TurnRetreated"); expect(events[1].type).toBe("RoundRetreated"); }); }); });