Replace granular change-level specs (001–036) with living feature specs: - 001-combatant-management (CRUD, persistence, clear, confirm buttons) - 002-turn-tracking (rounds, turn order, advance/retreat, top bar) - 003-combatant-state (HP, AC, conditions, concentration, initiative) - 004-bestiary (search, stat blocks, source management, panel UX) Workflow changes: - Add /integrate-issue command (replaces /issue-to-spec) for routing issues to existing specs or handing off to /speckit.specify - Update /sync-issue to list specs instead of requiring feature branch - Update /write-issue to reference /integrate-issue - Add RPI skills (research, plan, implement) to .claude/skills/ - Create docs/agents/ for RPI artifacts (research reports, plans) - Remove update-agent-context.sh call from /speckit.plan - Update CLAUDE.md with proportional scope-based workflow table - Bump constitution to 3.0.0 (specs describe features, not changes) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
24 KiB
Feature Specification: Turn Tracking
Feature Branch: 002-turn-tracking
Created: 2026-03-03
Status: Implemented
Overview
Turn Tracking covers all aspects of managing the flow of combat: advancing and retreating through combatants in initiative order, incrementing and decrementing the round counter at round boundaries, sorting combatants by initiative value, and presenting the top bar UI with navigation controls, round display, and active combatant display.
Domain Model
Key Entities
- Combatant — An identified participant in the encounter. Carries an optional integer
initiativevalue that determines position in turn order. - Encounter — The aggregate root. Contains an ordered list of combatants (sorted by initiative descending, unset last), an
activeIndexpointing to the current combatant, and aroundNumber(positive integer, starting at 1).
Domain Events
- TurnAdvanced — Emitted on every successful AdvanceTurn. Carries:
previousCombatantId,newCombatantId,roundNumber. - RoundAdvanced — Emitted when advancing crosses the end of the combatant list. Carries:
newRoundNumber. - TurnRetreated — Emitted on every successful RetreatTurn. Carries:
previousCombatantId,newCombatantId,roundNumber. - RoundRetreated — Emitted when retreating crosses a round boundary backward. Carries:
newRoundNumber. - InitiativeSet — Emitted when a combatant's initiative value is set or changed.
When a round boundary is crossed, the corresponding turn event (TurnAdvanced or TurnRetreated) MUST be emitted first, followed by the round event (RoundAdvanced or RoundRetreated). This emission order is part of the observable domain contract.
Invariants
- INV-1: An encounter MAY have zero combatants (empty encounter is valid aggregate state). AdvanceTurn and RetreatTurn on an empty encounter MUST return a DomainError with no state change and no events.
- INV-2: If
combatants.length > 0,activeIndexMUST satisfy0 <= activeIndex < combatants.length. Ifcombatants.length == 0,activeIndexMUST be 0. - INV-3:
roundNumberMUST be a positive integer (>= 1). It MUST only increase on AdvanceTurn and only decrease on RetreatTurn; it MUST never drop below 1. - INV-4: AdvanceTurn and RetreatTurn MUST be pure functions of the current encounter state. Given identical input, output MUST be identical.
- INV-5: Every successful AdvanceTurn or RetreatTurn MUST emit at least one domain event (TurnAdvanced or TurnRetreated respectively). No silent state changes.
- INV-6: The initiative sort MUST be stable — combatants with equal initiative (or multiple combatants with no initiative) retain their relative insertion order.
- INV-7: The active combatant's identity MUST be preserved through any initiative-driven reorder — the active turn tracks the combatant by identity, not by index position.
User Scenarios & Testing (mandatory)
Advancing Turns
Story A1 — Advance to the Next Combatant (Priority: P1)
As a game master running an encounter, I want to advance the turn to the next combatant in initiative order so that play moves forward through the encounter.
Acceptance Scenarios:
- 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.
- 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.
- 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.
- Given an encounter with combatants [A, B, C], activeIndex 0, roundNumber 1, When AdvanceTurn is applied three times, Then activeIndex returns to 0 and roundNumber is 2.
Story A2 — Round Increment on Wrap (Priority: P1)
As a game master, I want the round number to increment automatically when the last combatant's turn ends so that I always know which round of combat I am in.
Acceptance Scenarios:
- 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).
- 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).
- 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).
Story A3 — AdvanceTurn on Empty Encounter (Priority: P1)
As a developer, I want AdvanceTurn to fail safely on an empty encounter so that no invalid state is ever produced.
Acceptance Scenarios:
- 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.
Retreating Turns
Story R1 — Go Back to the Previous Turn (Priority: P1)
As a game master running a combat encounter, I want to go back to the previous combatant's turn so that I can correct mistakes (e.g., a forgotten action, an incorrectly skipped combatant) without restarting the encounter.
Acceptance Scenarios:
- Given an encounter with combatants [A, B, C], activeIndex 1, roundNumber 1, When RetreatTurn, Then activeIndex is 0, roundNumber is 1, and a TurnRetreated event is emitted with previousCombatantId B, newCombatantId A, roundNumber 1.
- Given an encounter with a single combatant [A], activeIndex 0, roundNumber 3, When RetreatTurn, Then activeIndex is 0, roundNumber is 2, and events are emitted in order: TurnRetreated then RoundRetreated (newRoundNumber 2).
Story R2 — Round Decrement on Wrap Backward (Priority: P1)
As a game master, I want the round number to decrement when retreating past the first combatant so that the encounter state accurately reflects where I am in the timeline.
Acceptance Scenarios:
- Given an encounter with combatants [A, B, C], activeIndex 0, roundNumber 2, When RetreatTurn, Then activeIndex is 2, roundNumber is 1, and events are emitted in order: TurnRetreated (previousCombatantId A, newCombatantId C, roundNumber 1) then RoundRetreated (newRoundNumber 1).
Story R3 — Retreat Blocked at Encounter Start (Priority: P1)
As a game master, I want the Previous Turn action to fail when I am at the very beginning of the encounter so that round 1 / first combatant is the earliest reachable state.
Acceptance Scenarios:
- Given an encounter with combatants [A, B, C], activeIndex 0, roundNumber 1, When RetreatTurn, Then the operation MUST fail with an error. No events are emitted. State is unchanged.
- Given an encounter with a single combatant [A], activeIndex 0, roundNumber 1, When RetreatTurn, Then the operation MUST fail with an error. No events are emitted. State is unchanged.
Story R4 — RetreatTurn on Empty Encounter (Priority: P1)
As a developer, I want RetreatTurn to fail safely on an empty encounter so that no invalid state is ever produced.
Acceptance Scenarios:
- Given an encounter with an empty combatant list, When RetreatTurn, Then the operation MUST fail with an invalid-encounter error. No events are emitted. State is unchanged.
Round Tracking
Story RD1 — Round Number Display (Priority: P1)
As a game master, I want the current round number to always be visible at the top of the tracker so that I never lose track of which round of combat I am in.
Acceptance Scenarios:
- Given an active encounter in Round 2, When the user views the top bar, Then the round badge shows "R2" (or equivalent compact format) as a visually distinct element.
- Given the user advances the turn and the round increments from 3 to 4, Then the round badge updates to the new round number immediately without layout shift.
- Given an encounter with no combatants, When viewing the top bar, Then the round badge still shows the current round number.
Turn Order (Initiative Sorting)
Story TO1 — Automatic Ordering by Initiative (Priority: P1)
As a game master, I want the encounter to automatically sort combatants from highest to lowest initiative whenever initiative values are set or changed so that I do not have to manually reorder them.
Acceptance Scenarios:
- Given combatants A (initiative 20), B (initiative 5), C (initiative 15), When all initiatives are set, Then the combatant order is A (20), C (15), B (5).
- Given combatants in order A (20), C (15), B (5), When B's initiative is changed to 25, Then the order becomes B (25), A (20), C (15).
- Given combatants A (initiative 10) and B (initiative 10) with the same value, Then their relative order is preserved (stable sort — the combatant who was added or set first stays ahead).
Story TO2 — Combatants Without Initiative (Priority: P2)
As a game master, I want combatants who have not had their initiative set to appear at the end of the turn order so that the encounter remains usable while I am still entering initiative values.
Acceptance Scenarios:
- Given combatants A (initiative 15), B (no initiative), C (initiative 10), Then the order is A (15), C (10), B (no initiative).
- Given combatants A (no initiative) and B (no initiative), Then their relative order is preserved from when they were added.
- Given combatant A (no initiative), When initiative is set to 12, Then A moves to its correct sorted position among combatants that have initiative values.
- Given combatant A (initiative 15), When the user clears A's initiative, Then A moves to the end of the turn order.
Story TO3 — Active Turn Preserved During Reorder (Priority: P2)
As a game master mid-encounter, I want the active combatant's turn to be preserved when initiative changes cause a reorder so that I do not lose track of whose turn it is.
Acceptance Scenarios:
- Given it is combatant B's turn (activeIndex points to B), When combatant A's initiative is changed causing a reorder, Then the active turn still points to combatant B.
- Given it is combatant A's turn, When combatant A's own initiative is changed causing a reorder, Then the active turn still points to combatant A.
Top Bar Display
Story TB1 — Scanning Round and Combatant at a Glance (Priority: P1)
As a game master running an encounter, I want the round number and current combatant displayed as distinct, visually separated elements so I can instantly identify both without parsing a combined string.
Acceptance Scenarios:
- Given an active encounter in Round 2 with "Goblin" as the active combatant, When the user views the top bar, Then "R2" appears as a muted badge/pill near the left side and "Goblin" appears as a prominent centered label, with no dash or combined string.
- Given an active encounter in Round 1 at the first combatant, When the encounter starts, Then the round badge shows the round number and the center displays the first combatant's name as separate visual elements.
- Given the user advances the turn, When the round increments from 3 to 4, Then the round badge updates without layout shift.
Story TB2 — Fixed Top Bar (Priority: P1)
As a game master managing a large encounter with many combatants, I want the turn navigation bar pinned to the top of the screen so that I can always navigate turns without scrolling away from the controls.
Acceptance Scenarios:
- Given an encounter with enough combatants to overflow the viewport, When the user scrolls through the combatant list, Then the turn navigation bar (Previous / round badge / combatant name / Next) remains fixed at the top of the encounter area and never scrolls out of view.
- Given any viewport width, When the encounter tracker is displayed, Then the top navigation bar remains fixed and the combatant list scrolls independently.
Story TB3 — Left-Center-Right Layout (Priority: P1)
As a game master, I want the top bar to follow a clear left-center-right structure so that controls are always in predictable positions regardless of combatant name length.
Acceptance Scenarios:
- Given an encounter with a short combatant name like "Orc", When viewing the bar, Then the layout maintains the left-center-right structure with the name centered.
- Given an encounter with a long combatant name like "Ancient Red Dragon Wyrm of the Northern Wastes", When viewing the bar, Then the name truncates gracefully without pushing action buttons off-screen.
- Given a narrow viewport, When viewing the bar, Then all three zones (round badge, combatant name, action buttons) remain visible and accessible.
Story TB4 — Turn Navigation Controls Accessible and Correctly Disabled (Priority: P1)
As a game master, I want the Previous Turn and Next Turn buttons placed prominently in the fixed top bar, with the Previous button disabled when no retreat is possible, so that I can quickly navigate turns from any scroll position.
Acceptance Scenarios:
- Given the encounter tracker is displayed, When the user looks at the screen, Then the Previous Turn and Next Turn buttons are visible in the fixed top bar, above the combatant list.
- Given the encounter is at round 1 with the first combatant active, When the user views the turn controls, Then the Previous Turn button is disabled (visually indicating it cannot be used).
- Given the encounter has no combatants, When the user views the tracker, Then both turn navigation buttons are disabled.
- Given the tracker has many combatants requiring scrolling, When the user scrolls down, Then the turn navigation controls remain accessible at the top (no scrolling needed to reach them).
- Given the Previous and Next buttons are displayed, When the user looks at the controls, Then the buttons are visually distinct with clear directional indicators (icons, labels, or both).
Story TB5 — No Combatants State (Priority: P2)
As a game master with an empty encounter, I want the top bar to handle the no-combatants state gracefully so that it does not appear broken.
Acceptance Scenarios:
- Given an encounter with no combatants, When viewing the top bar, Then the round badge still shows the round number and the center area displays a placeholder message indicating no active combatant.
Story TB6 — Active Combatant Scrolled into View on Turn Change (Priority: P2)
As a game master, I want the active combatant's row to automatically scroll into view when the turn changes so that the active row is always visible after navigation.
Acceptance Scenarios:
- Given the active combatant's row is scrolled off-screen, When the turn changes via Next or Previous, Then the combatant list automatically scrolls to bring the newly active combatant's row into view.
Requirements (mandatory)
Functional Requirements
Advancing Turns
- FR-001: The domain MUST expose an AdvanceTurn operation that accepts an Encounter and returns the resulting Encounter state plus emitted domain events.
- FR-002: AdvanceTurn MUST increment
activeIndexby 1, wrapping to 0 when advancing past the last combatant. - FR-003: When
activeIndexwraps to 0,roundNumberMUST increment by 1. - FR-004: AdvanceTurn on an empty encounter MUST return a DomainError without modifying state or emitting events.
- FR-005: Domain events MUST be returned as values from AdvanceTurn, not dispatched via side effects.
Retreating Turns
- FR-006: The domain MUST expose a RetreatTurn operation that moves the active turn to the previous combatant in initiative order.
- FR-007: RetreatTurn MUST decrement
activeIndexby 1. WhenactiveIndexwould go below 0, it MUST wrap to the last combatant and decrementroundNumberby 1. - FR-008: RetreatTurn at round 1 with
activeIndex0 MUST fail with a DomainError. This is the earliest possible encounter state. - FR-009: RetreatTurn on an empty encounter MUST fail with a DomainError without modifying state or emitting events.
- FR-010: RetreatTurn MUST emit a TurnRetreated event on success. When crossing a round boundary, a RoundRetreated event MUST also be emitted: TurnRetreated first, then RoundRetreated.
- FR-011: RetreatTurn MUST be a pure function of the current encounter state. Given identical input, output MUST be identical.
Turn Order (Initiative Sorting)
- FR-012: The system MUST automatically reorder combatants from highest to lowest initiative whenever an initiative value is set, changed, or cleared.
- FR-013: Combatants without an initiative value MUST be placed after all combatants that have initiative values.
- FR-014: The sort MUST be stable: combatants with equal initiative (or multiple combatants without initiative) retain their relative order.
- FR-015: The system MUST preserve the active combatant's turn identity when reordering occurs — the active turn tracks the combatant by identity, not by
activeIndexposition. - FR-016: Zero and negative integers MUST be accepted as valid initiative values.
- FR-017: Non-integer initiative values MUST be rejected with an error.
- FR-018: The system MUST emit an InitiativeSet domain event when a combatant's initiative is set or changed.
Top Bar Display
- FR-019: The top bar MUST remain fixed at the top of the encounter tracker area and MUST NOT scroll out of view.
- FR-020: The top bar MUST follow a left-center-right layout: [prev button] [round badge] — [combatant name] — [action buttons] [next button].
- FR-021: The round number MUST be displayed as a compact, visually muted badge or pill element (format: "R{n}", e.g., "R1", "R2") positioned to the left of the combatant name.
- FR-022: The current combatant's name MUST be displayed as a prominent, centered label and MUST be the visual focal point of the bar.
- FR-023: The round number and combatant name MUST be visually distinct elements — not joined by a dash or rendered as a single string.
- FR-024: The combatant name MUST truncate with an ellipsis when it exceeds available space rather than causing layout overflow.
- FR-025: When no combatants exist, the center area MUST display a placeholder message; the round badge MUST still show the current round number.
- FR-026: The Previous Turn button MUST be disabled when the encounter is at its earliest state (round 1, first combatant active).
- FR-027: Both turn navigation buttons MUST be disabled when the encounter has no combatants.
- FR-028: The turn navigation buttons MUST have clear directional indicators (icons, labels, or both) so the user can distinguish Previous from Next at a glance.
- FR-029: When the active turn changes via Next or Previous, the active combatant's row MUST automatically scroll into view if it is not currently visible in the scrollable list area.
- FR-030: The combatant list MUST be the only scrollable region — positioned between the fixed top bar and the fixed bottom bar.
Success Criteria (mandatory)
- SC-001: All AdvanceTurn 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 UI layers (layer boundary compliance).
- SC-004: A user can reverse a turn advancement using a single click on the Previous Turn button.
- SC-005: The Previous Turn button is correctly disabled at the earliest encounter state (round 1, first combatant), preventing invalid operations 100% of the time.
- SC-006: All RetreatTurn acceptance scenarios pass as deterministic, pure-function tests with no I/O dependencies.
- SC-007: After setting or changing any initiative value, the encounter's combatant order immediately reflects the correct descending initiative sort.
- SC-008: The active combatant's turn is never lost or shifted to a different combatant due to an initiative-driven reorder.
- SC-009: Combatants without initiative are always displayed after combatants with initiative values.
- SC-010: Users can identify the current round number and active combatant in under 1 second of looking at the top bar, without needing to parse a combined string.
- SC-011: The top bar layout remains visually balanced and functional across viewport widths from 320px to 1920px.
- SC-012: All existing top bar functionality (turn navigation, roll initiative, manage sources, clear encounter) remains fully operational.
- SC-013: Combatant names up to 40 characters display without layout breakage; longer names truncate gracefully.
- SC-014: With 20+ combatants in an encounter, the turn navigation bar remains visible at all scroll positions without any user action beyond normal scrolling.
Edge Cases
- Empty combatant list: Valid aggregate state. AdvanceTurn and RetreatTurn both return a DomainError (no state change, no events). The top bar shows the round badge and a placeholder for the combatant name.
- Single combatant, advancing: Every advance wraps and increments the round. Both TurnAdvanced and RoundAdvanced are emitted.
- Single combatant, retreating at round 1: RetreatTurn fails because there is no previous turn.
- Single combatant, retreating at round > 1: RetreatTurn succeeds, decrementing the round; both TurnRetreated and RoundRetreated are emitted.
- Large round numbers: No overflow or special-case behavior; round increments and decrements uniformly.
- Retreating at round 1, activeIndex 0: The earliest possible state — RetreatTurn MUST fail. Round number can never drop below 1.
- All combatants have the same initiative: Relative order is preserved (stable sort preserves insertion order).
- Initiative cleared mid-encounter: The combatant moves to the end of the turn order. The active combatant identity is preserved.
- Initiative changed for the active combatant: Reorder occurs; the active turn still points to that combatant at its new position.
- Initiative set to zero or a negative value: Treated as a normal integer — sorted accordingly.
- Combatant name extremely long (50+ characters): Name truncates with an ellipsis; layout does not break.
- Very narrow viewport: Round badge and navigation buttons remain visible; combatant name truncates.
- Very short viewport (e.g., 400px tall): Combatant list area is still scrollable, even if only a small portion is visible.
- Active combatant scrolled off-screen: On turn change, the list auto-scrolls to bring the newly active combatant into view.
Assumptions
- Initiative values are integers (no decimals). There is no dice-rolling or randomization in the domain — the user provides the final value.
- Tiebreaking for equal initiative values uses stable sort (preserves existing relative order). Secondary tiebreakers (e.g., Dexterity modifier) are not included in the MVP baseline.
- RetreatTurn is the inverse of AdvanceTurn for position and round tracking only. It does not restore any other state (e.g., HP changes made during a combatant's turn are not undone). It is not a full undo/redo stack.
- Keyboard shortcuts for Previous/Next Turn navigation are not included in the MVP baseline.
- The round badge uses the compact format "R{number}" (e.g., "R1", "R2").
- No new domain entities or persistence changes are required for the top bar display — it is a presentational layer over existing encounter state.