T001–T006: Phase 1 setup (workspace, Biome, TS, Vitest, layer boundary enforcement)
This commit is contained in:
160
specs/001-advance-turn/spec.md
Normal file
160
specs/001-advance-turn/spec.md
Normal file
@@ -0,0 +1,160 @@
|
||||
# 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: AdvanceTurn MUST reject with an error.
|
||||
- Single combatant: every advance wraps and increments the round.
|
||||
- Large round numbers: no overflow or special-case behavior; round
|
||||
increments uniformly.
|
||||
|
||||
## 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 MUST have at least one combatant.
|
||||
Operations on an empty encounter MUST fail.
|
||||
- **INV-2**: activeIndex MUST always satisfy
|
||||
0 <= activeIndex < len(combatants).
|
||||
- **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).
|
||||
Reference in New Issue
Block a user