T007–T011: implement AdvanceTurn domain logic (pure function, events, invariants, 8 acceptance tests)
This commit is contained in:
233
packages/domain/src/__tests__/advance-turn.test.ts
Normal file
233
packages/domain/src/__tests__/advance-turn.test.ts
Normal file
@@ -0,0 +1,233 @@
|
||||
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 rejects empty combatant list", () => {
|
||||
const result = createEncounter([]);
|
||||
expect(isDomainError(result)).toBe(true);
|
||||
});
|
||||
|
||||
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");
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user