Files
initiative/specs/002-add-combatant/spec.md

162 lines
6.2 KiB
Markdown

# Feature Specification: Add Combatant
**Feature Branch**: `002-add-combatant`
**Created**: 2026-03-03
**Status**: Draft
**Input**: User description: "let us add a spec for the option to add a combatant to the encounter. a new combatant is added to the end of the list."
## User Scenarios & Testing *(mandatory)*
### User Story 1 - Add Combatant to Encounter (Priority: P1)
A game master adds a new combatant to an existing encounter. The new
combatant is appended to the end of the initiative order. This allows
late-joining participants or newly discovered enemies to enter combat.
**Why this priority**: Adding combatants is the foundational mutation
for populating an encounter. Without it, the encounter has no
participants and no other feature (turn advancement, removal) is useful.
**Independent Test**: Can be fully tested as a pure state transition
with no I/O, persistence, or UI. Given an Encounter value and an
AddCombatant action with a name, assert the resulting Encounter value
and emitted domain events.
**Acceptance Scenarios**:
1. **Given** an empty encounter (no combatants, activeIndex 0,
roundNumber 1),
**When** AddCombatant with name "Gandalf",
**Then** combatants is [Gandalf], activeIndex is 0,
roundNumber is 1,
and a CombatantAdded event is emitted with the new combatant's
id and name "Gandalf" and position 0.
2. **Given** an encounter with combatants [A, B], activeIndex 0,
roundNumber 1,
**When** AddCombatant with name "C",
**Then** combatants is [A, B, C], activeIndex is 0,
roundNumber is 1,
and a CombatantAdded event is emitted with position 2.
3. **Given** an encounter with combatants [A, B, C], activeIndex 2,
roundNumber 3,
**When** AddCombatant with name "D",
**Then** combatants is [A, B, C, D], activeIndex is 2,
roundNumber is 3,
and a CombatantAdded event is emitted with position 3.
The active combatant does not change.
4. **Given** an encounter with combatants [A],
**When** AddCombatant is applied twice with names "B" then "C",
**Then** combatants is [A, B, C] in that order.
Each operation emits its own CombatantAdded event.
5. **Given** an encounter with combatants [A, B],
**When** AddCombatant with an empty name "",
**Then** the operation MUST fail with a validation error.
No events are emitted. State is unchanged.
6. **Given** an encounter with combatants [A, B],
**When** AddCombatant with a whitespace-only name " ",
**Then** the operation MUST fail with a validation error.
No events are emitted. State is unchanged.
---
### Edge Cases
- Empty name or whitespace-only name: AddCombatant MUST return a
DomainError (no state change, no events).
- Adding to an empty encounter: the new combatant becomes the first
and only participant; activeIndex remains 0.
- Adding during mid-round: the activeIndex must not shift; the
currently active combatant stays active.
- Duplicate names: allowed. Combatants are distinguished by their
unique id, not by name.
## Domain Model *(mandatory)*
### Key Entities
- **Combatant**: An identified participant in the encounter with a
unique CombatantId (branded string) and a name (non-empty string).
- **Encounter**: The aggregate root. Contains an ordered list of
combatants, an activeIndex pointing to the current combatant, and
a roundNumber (positive integer, starting at 1).
### Domain Events
- **CombatantAdded**: Emitted on every successful AddCombatant.
Carries: combatantId, name, position (zero-based index where the
combatant was inserted).
### Invariants
- **INV-1** (preserved): An encounter MAY have zero combatants.
- **INV-2** (preserved): If combatants.length > 0, activeIndex MUST
satisfy 0 <= activeIndex < combatants.length. If
combatants.length == 0, activeIndex MUST be 0.
- **INV-3** (preserved): roundNumber MUST be a positive integer
(>= 1) and MUST only increase.
- **INV-4**: AddCombatant MUST be a pure function of the current
encounter state and the input name. Given identical input, output
MUST be identical (except for id generation — see Assumptions).
- **INV-5**: Every successful AddCombatant MUST emit exactly one
CombatantAdded event. No silent state changes.
- **INV-6**: AddCombatant MUST NOT change the activeIndex or
roundNumber of the encounter.
- **INV-7**: The new combatant MUST be appended to the end of the
combatants list (last position).
## Requirements *(mandatory)*
### Functional Requirements
- **FR-001**: The domain MUST expose an AddCombatant operation that
accepts an Encounter and a combatant name, and returns the updated
Encounter state plus emitted domain events.
- **FR-002**: AddCombatant MUST append the new combatant to the end
of the combatants list.
- **FR-003**: AddCombatant MUST assign a unique CombatantId to the
new combatant.
- **FR-004**: AddCombatant MUST reject empty or whitespace-only names
by returning a DomainError without modifying state or emitting
events.
- **FR-005**: AddCombatant MUST NOT alter the activeIndex or
roundNumber of the encounter.
- **FR-006**: Domain events MUST be returned as values from the
operation, not dispatched via side effects.
### Out of Scope (MVP baseline does not include)
- Removing combatants from an encounter
- Reordering combatants after adding
- Initiative score or automatic sorting
- Combatant attributes beyond name (HP, conditions, stats)
- Maximum combatant count limits
- Persistence, serialization, or storage
- UI or any adapter layer
## Assumptions
- CombatantId generation is the caller's responsibility (passed in or
generated by the application layer), keeping the domain function
pure and deterministic. The domain function will accept a
CombatantId as part of its input rather than generating one
internally.
- Name validation trims whitespace; a name that is empty after
trimming is invalid.
## Success Criteria *(mandatory)*
### Measurable Outcomes
- **SC-001**: All 6 acceptance scenarios pass as deterministic,
pure-function tests with no I/O dependencies.
- **SC-002**: Invariants INV-1 through INV-7 are verified by tests.
- **SC-003**: The domain module has zero imports from application,
adapter, or agent layers (layer boundary compliance).
- **SC-004**: Adding a combatant to an encounter preserves all
existing combatants and their order unchanged.