Implement the 002-add-combatant feature that adds the possibility to add new combatants to an encounter
This commit is contained in:
161
specs/002-add-combatant/spec.md
Normal file
161
specs/002-add-combatant/spec.md
Normal file
@@ -0,0 +1,161 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user