6.2 KiB
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:
-
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.
-
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.
-
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.
-
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.
-
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.
-
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.