Files
initiative/specs/001-advance-turn/spec.md
Lukas 187f98fc52 Relax INV-1/INV-2 in 001-advance-turn spec to allow empty encounters
Prepares for 002-add-combatant by treating an empty combatant list as
a valid aggregate state (activeIndex must be 0). AdvanceTurn behavior
on non-empty encounters is unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 18:06:13 +01:00

6.5 KiB

Feature Specification: Advance Turn

Feature Branch: 001-advance-turn Created: 2026-03-03 Status: Draft Input: Walking-skeleton domain feature — deterministic turn advancement

User Scenarios & Testing (mandatory)

User Story 1 - Advance Turn (Priority: P1)

A game master running an encounter advances the turn to the next combatant in initiative order. When the last combatant in the round finishes, the round number increments and play wraps to the first combatant.

Why this priority: This is the irreducible core of an initiative tracker. Without turn advancement, no other feature has meaning.

Independent Test: Can be fully tested as a pure state transition with no I/O, persistence, or UI. Given an Encounter value and an AdvanceTurn action, assert the resulting Encounter value and emitted domain events.

Acceptance Scenarios:

  1. Given an encounter with combatants [A, B, C], activeIndex 0, roundNumber 1, When AdvanceTurn, Then activeIndex is 1, roundNumber is 1, and a TurnAdvanced event is emitted with previousCombatantId A, newCombatantId B, roundNumber 1.

  2. Given an encounter with combatants [A, B, C], activeIndex 1, roundNumber 1, When AdvanceTurn, Then activeIndex is 2, roundNumber is 1, and a TurnAdvanced event is emitted with previousCombatantId B, newCombatantId C, roundNumber 1.

  3. Given an encounter with combatants [A, B, C], activeIndex 2, roundNumber 1, When AdvanceTurn, Then activeIndex is 0, roundNumber is 2, and events are emitted in order: TurnAdvanced (previousCombatantId C, newCombatantId A, roundNumber 2) then RoundAdvanced (newRoundNumber 2).

  4. Given an encounter with combatants [A, B, C], activeIndex 2, roundNumber 5, When AdvanceTurn, Then activeIndex is 0, roundNumber is 6, and events are emitted in order: TurnAdvanced then RoundAdvanced (verifies round increment is not hardcoded to 2).

  5. Given an encounter with a single combatant [A], activeIndex 0, roundNumber 1, When AdvanceTurn, Then activeIndex is 0, roundNumber is 2, and events are emitted in order: TurnAdvanced (previousCombatantId A, newCombatantId A, roundNumber 2) then RoundAdvanced (newRoundNumber 2).

  6. Given an encounter with combatants [A, B], activeIndex 0, roundNumber 1, When AdvanceTurn is applied twice in sequence, Then after the first: activeIndex 1, roundNumber 1; after the second: activeIndex 0, roundNumber 2.

  7. Given an encounter with an empty combatant list, When AdvanceTurn, Then the operation MUST fail with an invalid-encounter error. No events are emitted. State is unchanged.

  8. Given an encounter with combatants [A, B, C], activeIndex 0, roundNumber 1, When AdvanceTurn is applied three times, Then the encounter completes a full round cycle: activeIndex returns to 0 and roundNumber is 2.


Edge Cases

  • Empty combatant list: valid aggregate state, but AdvanceTurn MUST return a DomainError (no state change, no events).
  • Single combatant: every advance wraps and increments the round.
  • Large round numbers: no overflow or special-case behavior; round increments uniformly.

Clarifications

Session 2026-03-03

  • Q: Should an encounter with zero combatants be a valid aggregate state? → A: Yes. Empty encounter is valid; AdvanceTurn returns DomainError.
  • Q: What is activeIndex when combatants list is empty? → A: activeIndex MUST be 0.
  • Q: Does this change any non-empty encounter behavior? → A: No. All existing acceptance scenarios and event contracts remain unchanged.

Domain Model (mandatory)

Key Entities

  • Combatant: An identified participant in the encounter. For this feature, a combatant is an opaque identity (e.g., a name or id). The MVP baseline does not include HP, conditions, or stats.
  • Encounter: The aggregate root. Contains an ordered list of combatants (pre-sorted by initiative), an activeIndex pointing to the current combatant, and a roundNumber (positive integer, starting at 1).

Domain Events

  • TurnAdvanced: Emitted on every successful AdvanceTurn. Carries: previousCombatantId, newCombatantId, roundNumber.
  • RoundAdvanced: Emitted when activeIndex wraps past the last combatant. Carries: newRoundNumber.

When a round boundary is crossed, both TurnAdvanced and RoundAdvanced MUST be emitted in that order (TurnAdvanced first). This emission order is part of the observable domain contract and MUST be verified by tests.

Invariants

  • INV-1: An encounter MAY have zero combatants (an empty encounter is a valid aggregate state). AdvanceTurn on an empty encounter MUST return a DomainError with no state change and no events.
  • INV-2: If combatants.length > 0, activeIndex MUST satisfy 0 <= activeIndex < combatants.length. If combatants.length == 0, activeIndex MUST be 0.
  • INV-3: roundNumber MUST be a positive integer (>= 1) and MUST only increase (never decrease or reset).
  • INV-4: AdvanceTurn MUST be a pure function of the current encounter state. Given identical input, output MUST be identical.
  • INV-5: Every successful AdvanceTurn MUST emit at least one domain event (TurnAdvanced). No silent state changes.

Requirements (mandatory)

Functional Requirements

  • FR-001: The domain MUST expose an AdvanceTurn operation that accepts an Encounter and returns the next Encounter state plus emitted domain events.
  • FR-002: AdvanceTurn MUST increment activeIndex by 1, wrapping to 0 when past the last combatant.
  • FR-003: When activeIndex wraps to 0, roundNumber MUST increment by 1.
  • FR-004: AdvanceTurn on an empty encounter MUST return an error without modifying state or emitting events.
  • FR-005: Domain events MUST be returned as values from the operation, not dispatched via side effects.

Out of Scope (MVP baseline does not include)

  • Initiative rolling or combatant ordering logic
  • Hit points, damage, conditions, or status effects
  • Adding or removing combatants mid-encounter
  • Persistence, serialization, or storage
  • UI, CLI, or any adapter layer
  • Agent behavior or suggestions

Success Criteria (mandatory)

Measurable Outcomes

  • SC-001: All 8 acceptance scenarios pass as deterministic, pure-function tests with no I/O dependencies.
  • SC-002: Invariants INV-1 through INV-5 are verified by tests.
  • SC-003: The domain module has zero imports from application, adapter, or agent layers (layer boundary compliance).