Files
initiative/specs/012-turn-navigation/data-model.md

59 lines
1.9 KiB
Markdown

# Data Model: Turn Navigation
## Existing Entities (unchanged)
### Encounter
- `combatants`: readonly array of Combatant
- `activeIndex`: number (0-based index into combatants)
- `roundNumber`: positive integer (>= 1)
### Combatant
- `id`: CombatantId (branded string)
- `name`: string
- `initiative?`: number
- `maxHp?`: number
- `currentHp?`: number
## New Domain Events
### TurnRetreated
Emitted on every successful RetreatTurn operation.
| Field | Type | Description |
|-------|------|-------------|
| type | `"TurnRetreated"` (literal) | Discriminant for event union |
| previousCombatantId | CombatantId | The combatant whose turn was active before retreat |
| newCombatantId | CombatantId | The combatant who is now active after retreat |
| roundNumber | number | The round number after the retreat |
### RoundRetreated
Emitted when RetreatTurn crosses a round boundary (activeIndex wraps from 0 to last combatant).
| Field | Type | Description |
|-------|------|-------------|
| type | `"RoundRetreated"` (literal) | Discriminant for event union |
| newRoundNumber | number | The round number after decrementing |
## State Transitions
### RetreatTurn
**Input**: Encounter
**Output**: `{ encounter: Encounter, events: DomainEvent[] }` | `DomainError`
**Rules**:
1. If `combatants.length === 0` -> DomainError("invalid-encounter")
2. If `roundNumber === 1 && activeIndex === 0` -> DomainError("no-previous-turn")
3. If `activeIndex > 0`: newIndex = activeIndex - 1, roundNumber unchanged
4. If `activeIndex === 0`: newIndex = combatants.length - 1, roundNumber - 1
**Events emitted**:
- Always: TurnRetreated
- On round boundary crossing (rule 4): TurnRetreated then RoundRetreated (order matters)
## Validation Rules
- RetreatTurn MUST NOT produce roundNumber < 1
- RetreatTurn MUST NOT produce activeIndex < 0 or >= combatants.length
- RetreatTurn is a pure function: identical input produces identical output