1874 lines
87 KiB
Markdown
1874 lines
87 KiB
Markdown
# Piece Modifier Profiles
|
||
|
||
## TL;DR
|
||
|
||
> **Quick Summary**: Users can author Modifier Profiles — reusable sets of rules that attach to pieces by type ("all knights +1 HP") OR by layout slot ("the b1 knight has +2 range"). Profiles are a first-class entity saved to their own library, selectable at game start, hot-swappable mid-game at turn boundaries. Six T1 modifier categories ship end-to-end: HP bonus, movement range, movement directions, capture behavior, promotion override, damage resistance.
|
||
>
|
||
> **Deliverables**:
|
||
> - 2 new preset hooks (`transformMoveGenerator`, `modifyMoveAttrs`) wrapping existing generators (not replacing)
|
||
> - 6 new `ChessAttrMap` entries + self-registering modifier catalog
|
||
> - `ModifierProfile` type, Zod schema, library persistence (`houserules:modifier-profiles:v1`)
|
||
> - WS message `modifier-profile.update` for hot-swap with server-side legality validator + rollback
|
||
> - Dedicated "Modifier Profiles" editor screen (reuses layout-editor primitives) accessible from rules drawer
|
||
> - Hover tooltip + click-to-pin side panel for in-play inspection
|
||
> - Full Playwright e2e vertical slice
|
||
> - ADR + user docs
|
||
>
|
||
> **Estimated Effort**: Large
|
||
> **Parallel Execution**: YES — 7 waves
|
||
> **Critical Path**: T1 (ADR) → T3 (hooks) → T5 (registry) → T6 (descriptors) → T11 (apply) → T12 (hot-swap) → T20 (e2e)
|
||
|
||
---
|
||
|
||
## Context
|
||
|
||
### Original Request
|
||
> "plan out the feature of being able to add presets that apply to certain pieces? So like, a particular piece can have more HP, or higher movement, or move backwards, etc"
|
||
|
||
### Interview Summary
|
||
|
||
**Key Decisions (user-confirmed)**:
|
||
- **Scope**: BOTH layered — per-type (all knights) AND per-instance (the b1 knight). Per-instance overrides per-type.
|
||
- **T1 modifier catalog** (all 6): HP bonus, movement range, movement directions, capture behavior, promotion override, invulnerability/damage resistance.
|
||
- **UX**: Dedicated new editor screen, accessible from rules drawer. Visual (layout-editor style). In-play inspection: hover tooltip + click-to-pin panel.
|
||
- **Timing**: Hot-swap mid-game (applied at turn boundary for determinism).
|
||
- **Storage**: Separate "modifier profiles" as first-class reusable entities — profile + layout combine at game start.
|
||
- **Depth**: Full vertical slice (engine + data + serialization + UI + e2e) for T1. T2/T3 planned as future tiers.
|
||
|
||
### Research Findings (bg_6129dc58)
|
||
- Preset hook system is rich (13 hooks) but **missing a move-generator override** — we add one.
|
||
- Pieces have per-instance `EntityId`s already. All per-piece state stored via declared `pieceAttributes` → `ChessAttrMap` union → seeded in `onActivate`/`onPieceSpawn`, auto-retracted via `effectivePieceAttrs` on death.
|
||
- `piece-hp.ts` is the reference implementation — follow its pattern.
|
||
- Facts serialize automatically via `GameStatePayload.facts` — **no wire changes needed for modifier VALUES**, only for modifier DEFINITIONS (new WS message + room-create field).
|
||
- Move generation keyed by piece TYPE but passes `pieceId` — per-instance state readable during generation.
|
||
|
||
### Metis Review (`ses_25c313fd2ffeljiMUzgKRTyMM0`)
|
||
- Identified 3 architecture decisions needing resolution before task-level planning.
|
||
- All 3 have been resolved in this plan (see "Architecture Decisions (ADR)" below) with sensible defaults aligned to user's interview answers. User may override.
|
||
- Recommended registry pattern for modifier catalog (adopted — T5).
|
||
- Recommended wrap semantics over replace for move-generator hook (adopted — T3).
|
||
- Recommended layout-bound keying for per-instance (adopted — Option B).
|
||
|
||
---
|
||
|
||
## Architecture Decisions (ADR)
|
||
|
||
> These are baked in as defaults. Each has a sensible rationale aligned with user's interview answers. User may override any before execution begins.
|
||
|
||
### ADR-1: Move Generator Hook Contract — **WRAP (not REPLACE)**
|
||
- **Decision**: New hook signature: `transformMoveGenerator?(engine, pieceId, prevGenerator) => MoveGenerator`
|
||
- **Rationale**: Composition — multiple presets + profile can stack. Each receives the previous generator and returns a new one. Enables "profile adds backward move" + "preset forbids captures" to layer cleanly.
|
||
- **Contract**: Returned generator emits **pseudo-legal** moves. Engine's legality layer (self-check filter, turn validation, repetition) ALWAYS runs downstream. Profile-aware generators MUST NOT bypass self-check.
|
||
|
||
### ADR-2: Per-Instance Identity Keying — **Layout-Slot Bound (Option B)**
|
||
- **Decision**: Per-instance modifiers stored keyed by layout-slot identifier (`square` from the layout, e.g. `"b1"`). Profile specifies a target `layoutId` it's bound to. At game start, when `applyLayout()` assigns `EntityId`s to pieces, engine maps `square → EntityId` and seeds facts accordingly.
|
||
- **Rationale**: Aligned with user's "profile + layout combine at game start". Portable within a single layout. Cross-layout reuse allowed for per-type; per-instance portion dropped with warning if applied to a non-matching layout.
|
||
- **Orphan handling**: If profile's per-instance entry targets a `square` with no piece in the current layout, show warning in editor; skip entry at game start (non-fatal).
|
||
|
||
### ADR-3: Hot-Swap Reconciliation (per modifier kind, at turn boundary only)
|
||
|
||
| Modifier | On swap-apply | On swap-remove |
|
||
|----------|---------------|----------------|
|
||
| HpBonus | `maxHp` recomputed; `currentHp = min(currentHp, newMax)` (never grows) | Same clamp rule |
|
||
| RangeBonus | Next legal-move query reflects new range | Next legal-move query reflects baseline |
|
||
| DirectionAdditions | Next legal-move query includes new directions | Next legal-move query excludes removed directions |
|
||
| CaptureFlags | Applies to next capture event | Baseline capture rules from next event |
|
||
| PromotionOverride | Applies to future promotions only; past promotions untouched | Future promotions use default |
|
||
| DamageResistance | Applies to next damage event | Baseline damage from next event |
|
||
|
||
- **Timing**: Hot-swap applies at **turn boundary only** (after `turnEnd`, before next player's `turnStart`).
|
||
- **Mid-turn edit attempts**: Queued server-side; applied at next turn boundary.
|
||
- **Rollback**: Server runs legality validator BEFORE broadcast. If invalid → NACK to sender with error code, no broadcast. No client optimistic state to rewind.
|
||
|
||
### ADR-4: Stacking Rules per Modifier Kind
|
||
|
||
| Modifier | Stacking rule |
|
||
|----------|---------------|
|
||
| HpBonus | **Additive**: base + perType + perInstance + sumOf(activePreset.hpBonus) |
|
||
| RangeBonus | **Additive**: clamped to `[0, 7]` |
|
||
| DirectionAdditions | **Union** of direction sets |
|
||
| CaptureFlags | **Union** (bitflags OR'd) |
|
||
| PromotionOverride | **Precedence wins**: perInstance > perType > presetDefault |
|
||
| DamageResistance | **Multiplicative**: `finalDamage = amount * ∏(1 - resistance_i)`, clamped to `≥ 0` |
|
||
|
||
### ADR-5: Precedence (Source Priority)
|
||
`per-instance (profile) > per-type (profile) > preset > engine base`
|
||
- For additive/union stacking: all sources contribute.
|
||
- For override-style (PromotionOverride): higher-priority source wins.
|
||
|
||
### ADR-6: Legality Validator Checklist
|
||
On each profile apply / swap, validate:
|
||
1. Every side has ≥1 king on board AFTER modifiers applied.
|
||
2. King pieces MUST NOT carry `Invulnerability` modifier (hard rejection).
|
||
3. No per-instance entries reference non-existent squares (warn, skip).
|
||
4. Each side has ≥1 legal move on next turn (run fast legal-move query).
|
||
5. Total facts per piece ≤ 16 attrs (prevent DoS via attr bloat).
|
||
- Error codes: `E_PROFILE_NO_KING`, `E_PROFILE_INVULN_KING`, `E_PROFILE_ORPHAN_INSTANCE`, `E_PROFILE_DEADLOCK`, `E_PROFILE_ATTR_LIMIT`.
|
||
|
||
### ADR-7: Single Active Profile Per Game (T1)
|
||
- One profile active at a time. Changing profile = full swap via `modifier-profile.update`.
|
||
- Profile composition/stacking → T2.
|
||
|
||
### ADR-8: Registry Pattern for Modifier Catalog
|
||
- Each T1 modifier lives in ONE file under `packages/chess/src/modifiers/descriptors/{kind}.ts`.
|
||
- Registers into `MODIFIER_REGISTRY` at module load (mirror `PRESET_REGISTRY`, `LAYOUT_REGISTRY`).
|
||
- Descriptor shape: `{ id, attrName, valueSchema, stackingRule, apply(session, pieceId, value), describe(value), uiForm }`.
|
||
- T3 custom modifiers will plug into this same registry.
|
||
|
||
---
|
||
|
||
## Work Objectives
|
||
|
||
### Core Objective
|
||
Ship a first-class "Modifier Profile" system that lets users attach rule modifiers to pieces by type OR by layout slot, save/share profiles in a library, select them at game start, hot-swap them mid-game, and inspect active modifiers during play.
|
||
|
||
### Concrete Deliverables
|
||
|
||
**Engine package** (`packages/chess/src/`):
|
||
- `modifiers/types.ts` — `ModifierProfile`, `TypeModifier`, `InstanceModifier`, `ModifierKindId`, `ModifierDescriptor`
|
||
- `modifiers/registry.ts` — `MODIFIER_REGISTRY` singleton, registration API
|
||
- `modifiers/descriptors/{hp-bonus,range-bonus,direction-additions,capture-flags,promotion-override,damage-resistance}.ts` — 6 descriptor impls
|
||
- `modifiers/apply.ts` — `applyProfileToSession(session, profile, layout)` — seed facts at game start
|
||
- `modifiers/reconcile.ts` — `reconcileProfileSwap(session, oldProfile, newProfile)` — hot-swap diff + clamp
|
||
- `modifiers/validate.ts` — legality checklist
|
||
- `modifiers/schema.ts` — Zod schema for profile serialization
|
||
- `modifiers/library.ts` — localStorage persistence (`houserules:modifier-profiles:v1`)
|
||
- `modifiers/index.ts` — barrel + side-effect registration
|
||
- Extend `presets/registry.ts`: add `transformMoveGenerator?`, `modifyMoveAttrs?` hooks
|
||
- Extend `schema.ts`: 6 new attrs in `ChessAttrMap`
|
||
- Extend `engine.ts`: call `transformMoveGenerator` chain before type's default generator; integrate profile application in lifecycle
|
||
|
||
**UI package** (`packages/chess/src/ui/`):
|
||
- `ModifierProfileEditor.tsx` — dedicated editor modal/screen (reuses layout board primitives)
|
||
- `ModifierPalettePanel.tsx` — modifier catalog (schema-driven forms)
|
||
- `PerTypePanel.tsx` — per-type modifier list
|
||
- `PerInstancePanel.tsx` — board view + click-to-select + per-piece modifier list
|
||
- `ModifierTooltip.tsx` — hover tooltip showing active modifiers with source attribution
|
||
- `ModifierPinnedPanel.tsx` — click-pinned side panel
|
||
- `RulesDrawer.tsx` — add "Modifier Profiles" entry
|
||
- Extend `Lobby.tsx` — profile picker alongside layout picker
|
||
- Extend `GameView.tsx` — wire tooltip + pinned panel
|
||
|
||
**Server package** (`packages/server/src/`):
|
||
- Extend `protocol.ts`: `ModifierProfileSchema`, `ProfileRequestSchema`, `modifier-profile.update` message, `ModifierProfileUpdatedPayload`, new error codes
|
||
- Extend `rooms.ts`: `Room.profile?: ModifierProfile` field
|
||
- Extend `game-session.ts`: accept optional profile
|
||
- Extend `broadcast.ts`: resolve + validate profile on room-create; handle `modifier-profile.update` message (validate + broadcast or NACK)
|
||
- New `profile-validator.ts` — runs legality checklist
|
||
|
||
**Net package** (`packages/chess/src/net/`):
|
||
- Extend `types.ts`: `ModifierProfileWire`, `TypeModifierWire`, `InstanceModifierWire`
|
||
|
||
**Docs**:
|
||
- `docs/adr/modifier-profiles.md` — ADR (this plan's architecture section formalized)
|
||
- `docs/user/modifier-profiles.md` — user-facing guide
|
||
|
||
**E2E** (`packages/chess/e2e/`):
|
||
- `modifier-profiles.spec.ts` — ~18 scenarios
|
||
|
||
### Definition of Done
|
||
|
||
- [ ] `bun run check` green (typecheck + lint + vitest)
|
||
- [ ] All 6 modifier descriptors pass unit tests including stacking + precedence
|
||
- [ ] `modifier-profile.update` WS round-trip tested server-side (happy + illegal + rollback)
|
||
- [ ] Hot-swap reconciliation idempotent (same inputs → same state)
|
||
- [ ] Editor accessible from rules drawer, reachable in ≤2 clicks
|
||
- [ ] Hover tooltip shows modifiers within 200ms; pinned panel updates within 500ms on hot-swap
|
||
- [ ] Full e2e vertical slice passes in Playwright
|
||
- [ ] ADR + user docs published
|
||
- [ ] Profile JSON size ≤ 8KB (fits URL share)
|
||
|
||
### Must Have
|
||
- All 6 T1 modifier categories working end-to-end.
|
||
- Per-type AND per-instance (Option B layout-slot keying) both functional.
|
||
- Hot-swap at turn boundary with server-authoritative validation.
|
||
- In-play inspection with source attribution (per-instance/per-type/preset/base).
|
||
- Reuse of layout-editor board primitives — NO duplicate board component.
|
||
- Shared engine code path for client move preview + server validation (no drift).
|
||
|
||
### Must NOT Have (Guardrails — from Metis)
|
||
|
||
- ❌ Custom DSL / scripting for user-authored modifiers (T3)
|
||
- ❌ Cross-piece aura effects (T3)
|
||
- ❌ Multiple profiles stacking (post-T3)
|
||
- ❌ Copy/paste modifiers between pieces in editor (T2)
|
||
- ❌ Visual diff of "modified" vs base pieces (T2)
|
||
- ❌ Conflict resolution UI (T2 — show inline validator error only in T1)
|
||
- ❌ Undo/redo inside editor beyond native text inputs (T2)
|
||
- ❌ Mid-turn hot-swap (T1: turn-boundary-only)
|
||
- ❌ New piece TYPES via modifiers (modifiers modify, they don't invent)
|
||
- ❌ Breaking changes to existing WS messages (all additions are NEW messages)
|
||
- ❌ Invulnerability on kings (hard-rejected by validator)
|
||
- ❌ Client computing effective modifiers differently from server (shared code path mandatory)
|
||
- ❌ Duplicating the board editor component (reuse layout-editor primitives)
|
||
- ❌ Any acceptance criterion requiring human visual confirmation
|
||
- ❌ Monster commits combining engine + UI + tests
|
||
|
||
### Must NOT Have (AI Slop Patterns)
|
||
|
||
- ❌ `as any`, `@ts-ignore`, or bypassing strict TS
|
||
- ❌ Generic names like `data`, `result`, `item`, `temp`
|
||
- ❌ Empty catch blocks
|
||
- ❌ `console.log` in production code
|
||
- ❌ Over-commenting (don't narrate obvious code)
|
||
- ❌ Premature abstraction (keep descriptors concrete until 3+ share shape)
|
||
- ❌ Duplicate validation logic across client/server (server is authority; client mirrors)
|
||
|
||
---
|
||
|
||
## Verification Strategy (MANDATORY)
|
||
|
||
> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed.
|
||
|
||
### Test Decision
|
||
- **Infrastructure exists**: YES (vitest + Playwright, established patterns)
|
||
- **Automated tests**: TDD — RED failing test first, GREEN minimal impl, REFACTOR
|
||
- **Framework**: `bun run test` (vitest) for unit/integration, `bunx playwright test` for e2e
|
||
- **Pattern**: Mirror `piece-hp.test.ts`, `layout-library.test.ts`, `layouts.spec.ts`
|
||
|
||
### QA Policy
|
||
Every task MUST include agent-executed QA scenarios. Evidence saved to `.sisyphus/evidence/piece-modifiers/task-{N}-{slug}.{ext}`.
|
||
|
||
- **Engine/data**: `bun run test {path}` — assertion-based
|
||
- **Server**: `bun run test {server-path}` + WebSocket harness
|
||
- **Frontend/UI**: Playwright from repo root (`bunx playwright test e2e/modifier-profiles.spec.ts`)
|
||
- **Integration**: E2E full-flow scenarios
|
||
|
||
---
|
||
|
||
## Execution Strategy
|
||
|
||
### Parallel Execution Waves
|
||
|
||
```
|
||
Wave 1 (Foundation - start immediately):
|
||
└── T1: Architecture ADR document (single task — blocks everything)
|
||
|
||
Wave 2 (Schema scaffolding - PARALLEL after T1):
|
||
├── T2: Add 6 ChessAttrMap entries
|
||
├── T3: Add transformMoveGenerator + modifyMoveAttrs preset hooks
|
||
├── T4: Modifier types (ModifierProfile, descriptor types)
|
||
└── T5: Modifier registry + barrel
|
||
|
||
Wave 3 (Descriptors + schema - PARALLEL after Wave 2):
|
||
├── T6: HP-bonus descriptor + tests
|
||
├── T7: Range-bonus descriptor + tests
|
||
├── T8: Direction-additions descriptor + tests
|
||
├── T9: Capture-flags descriptor + tests
|
||
├── T10: Promotion-override descriptor + tests
|
||
├── T11: Damage-resistance descriptor + tests
|
||
├── T12: Zod schema + roundtrip tests
|
||
└── T13: Library persistence
|
||
|
||
Wave 4 (Integration - PARALLEL after Wave 3):
|
||
├── T14: applyProfileToSession (game start)
|
||
├── T15: reconcileProfileSwap (hot-swap)
|
||
├── T16: Legality validator
|
||
├── T17: Server protocol extensions (schemas + error codes)
|
||
└── T18: UI editor shell + rules drawer entry
|
||
|
||
Wave 5 (Server + UI core - PARALLEL after Wave 4):
|
||
├── T19: Server room-create accepts profile
|
||
├── T20: Server modifier-profile.update WS handler
|
||
├── T21: UI per-type panel
|
||
├── T22: UI per-instance panel (board picker)
|
||
└── T23: UI save/load library + URL share
|
||
|
||
Wave 6 (In-play integration - PARALLEL after Wave 5):
|
||
├── T24: Hover tooltip inspection
|
||
├── T25: Pinned side panel
|
||
└── T26: Lobby profile picker integration
|
||
|
||
Wave 7 (End-to-end verification - PARALLEL):
|
||
├── T27: Full e2e Playwright suite
|
||
└── T28: ADR + user docs
|
||
|
||
Wave FINAL (after ALL tasks — 4 parallel reviews, then user okay):
|
||
├── F1: Plan compliance audit (oracle)
|
||
├── F2: Code quality review (unspecified-high)
|
||
├── F3: Real manual QA (unspecified-high)
|
||
└── F4: Scope fidelity check (deep)
|
||
```
|
||
|
||
### Dependency Matrix
|
||
|
||
| Task | Depends On | Blocks |
|
||
|------|------------|--------|
|
||
| 1 | — | 2-28 |
|
||
| 2 | 1 | 6-11, 14 |
|
||
| 3 | 1 | 6-11, 14 |
|
||
| 4 | 1 | 5, 6-11, 12 |
|
||
| 5 | 4 | 6-11, 14 |
|
||
| 6 | 2, 3, 5 | 14, 21, 22, 24 |
|
||
| 7 | 2, 3, 5 | 14, 21, 22, 24 |
|
||
| 8 | 2, 3, 5 | 14, 21, 22, 24 |
|
||
| 9 | 2, 3, 5 | 14, 21, 22, 24 |
|
||
| 10 | 2, 3, 5 | 14, 21, 22, 24 |
|
||
| 11 | 2, 3, 5 | 14, 21, 22, 24 |
|
||
| 12 | 4 | 13, 17, 19 |
|
||
| 13 | 12 | 23 |
|
||
| 14 | 6-11, 16 | 15, 19 |
|
||
| 15 | 14 | 20 |
|
||
| 16 | 12 | 14, 19, 20 |
|
||
| 17 | 12, 16 | 19, 20 |
|
||
| 18 | 5 | 21, 22, 23 |
|
||
| 19 | 14, 17 | 26, 27 |
|
||
| 20 | 15, 17 | 24, 25, 27 |
|
||
| 21 | 6-11, 18 | 26, 27 |
|
||
| 22 | 6-11, 18 | 26, 27 |
|
||
| 23 | 13, 18 | 26, 27 |
|
||
| 24 | 6-11, 20 | 25, 27 |
|
||
| 25 | 24 | 27 |
|
||
| 26 | 19, 21, 22, 23 | 27 |
|
||
| 27 | 19-26 | 28 |
|
||
| 28 | 1, 27 | F1 |
|
||
|
||
### Agent Dispatch Summary
|
||
|
||
| Wave | Tasks | Categories |
|
||
|------|-------|------------|
|
||
| 1 | T1 (ADR) | `ultrabrain` |
|
||
| 2 | T2-T5 | T2: `quick`, T3: `deep`, T4: `unspecified-low`, T5: `deep` |
|
||
| 3 | T6-T13 | T6-T11: `unspecified-high`, T12: `unspecified-low`, T13: `unspecified-low` |
|
||
| 4 | T14-T18 | T14-T16: `deep`, T17: `unspecified-low`, T18: `visual-engineering` |
|
||
| 5 | T19-T23 | T19-T20: `deep`, T21-T22: `visual-engineering`, T23: `unspecified-low` |
|
||
| 6 | T24-T26 | T24-T25: `visual-engineering`, T26: `unspecified-low` |
|
||
| 7 | T27-T28 | T27: `unspecified-high` (+ playwright skill), T28: `writing` |
|
||
| FINAL | F1-F4 | F1: `oracle`, F2: `unspecified-high`, F3: `unspecified-high`, F4: `deep` |
|
||
|
||
---
|
||
|
||
## TODOs
|
||
|
||
- [x] 1. **Architecture ADR document**
|
||
|
||
**What to do**:
|
||
- Create `docs/adr/modifier-profiles.md` formalizing ADR-1 through ADR-8 from the plan's Architecture Decisions section.
|
||
- Include: hook contract (WRAP, pseudo-legal), per-instance keying (Option B layout-slot bound), hot-swap semantics (turn-boundary), stacking rules table, precedence chain, legality validator checklist, single-profile constraint, registry pattern choice.
|
||
- Add a "Rejected Alternatives" section (REPLACE semantics, Option A/C keying, mid-turn swap) with rationale.
|
||
|
||
**Must NOT do**: Don't write code in this task. Don't design UI. ADR is a pure decision log.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `ultrabrain` — foundational architectural decisions
|
||
- **Skills**: [`repo-analysis`] — read existing ADR patterns if any
|
||
|
||
**Parallelization**: Wave 1 (solo). Blocks: ALL other tasks. Blocked By: None.
|
||
|
||
**References**:
|
||
- `.sisyphus/plans/piece-modifiers.md` § "Architecture Decisions (ADR)" — source of truth
|
||
- `.sisyphus/plans/starting-layouts.md` — prior plan format reference
|
||
- `packages/chess/src/presets/piece-hp.ts` — reference preset pattern
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] File `docs/adr/modifier-profiles.md` exists
|
||
- [ ] `ls docs/adr/modifier-profiles.md` returns file (verify via Bash)
|
||
- [ ] Contains all 8 ADR decisions, each with: Decision statement, Rationale, Rejected Alternatives
|
||
- [ ] Stacking rules table covers all 6 T1 modifier kinds
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: ADR file readable and contains all sections
|
||
Tool: Bash
|
||
Steps:
|
||
1. Run: grep -c "^## ADR-" docs/adr/modifier-profiles.md
|
||
2. Assert output: 8
|
||
3. Run: grep -c "^| HpBonus\|^| RangeBonus\|^| DirectionAdditions\|^| CaptureFlags\|^| PromotionOverride\|^| DamageResistance" docs/adr/modifier-profiles.md
|
||
4. Assert output: ≥6 (stacking table rows)
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-1-adr-structure.txt
|
||
```
|
||
|
||
**Commit**: YES (solo) — `docs(adr): modifier-profiles architecture decisions`
|
||
- Files: `docs/adr/modifier-profiles.md`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 2. **Add 6 modifier attrs to ChessAttrMap**
|
||
|
||
**What to do**:
|
||
- Edit `packages/chess/src/schema.ts`
|
||
- Add 6 new optional attribute types to `ChessAttrMap`:
|
||
- `HpBonus: number`
|
||
- `RangeBonus: number`
|
||
- `DirectionAdditions: readonly Direction[]` (use existing `Direction` type)
|
||
- `CaptureFlags: number` (bitflags: `CAN_CAPTURE_OWN=1`, `CANNOT_BE_CAPTURED=2`, `EN_PASSANT=4`)
|
||
- `PromotionOverride: PieceType | "disabled"`
|
||
- `DamageResistance: number` (0-1 range, multiplicative)
|
||
- Export capture-flag constants from `schema.ts`.
|
||
|
||
**Must NOT do**: Don't modify core attrs (PieceType, Color, Position, HasMoved). Don't touch engine.ts — types only.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `quick` — mechanical type additions
|
||
- **Skills**: []
|
||
|
||
**Parallelization**: Wave 2. Blocks: 6-11, 14. Blocked By: 1.
|
||
|
||
**References**:
|
||
- `packages/chess/src/schema.ts` — current ChessAttrMap shape
|
||
- `packages/chess/src/presets/piece-hp.ts:13-15` — how Hp attr was added (same pattern)
|
||
- `docs/adr/modifier-profiles.md` § ADR-4 — stacking rules dictate value shapes
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bun run check` passes (typecheck green)
|
||
- [ ] `grep "HpBonus\|RangeBonus\|DirectionAdditions\|CaptureFlags\|PromotionOverride\|DamageResistance" packages/chess/src/schema.ts` → 6 matches
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Types added without breaking existing build
|
||
Tool: Bash
|
||
Steps:
|
||
1. Run: bun run check
|
||
2. Assert: exit code 0, output contains "Tests" and no TS errors
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-2-typecheck.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(engine): add 6 modifier attrs to ChessAttrMap`
|
||
- Files: `packages/chess/src/schema.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 3. **Add transformMoveGenerator + modifyMoveAttrs preset hooks**
|
||
|
||
**What to do**:
|
||
- Extend `PresetDef` in `packages/chess/src/presets/registry.ts`:
|
||
```ts
|
||
transformMoveGenerator?: (
|
||
engine: ChessEngine,
|
||
pieceId: EntityId,
|
||
prev: MoveGenerator
|
||
) => MoveGenerator;
|
||
modifyMoveAttrs?: (
|
||
engine: ChessEngine,
|
||
pieceId: EntityId
|
||
) => { rangeBonus?: number; directionAdditions?: readonly Direction[] };
|
||
```
|
||
- Update `ChessEngine.getAllLegalMoves()` in `packages/chess/src/engine.ts`:
|
||
- After resolving piece type's base generator, fold through all active presets' `transformMoveGenerator` (reduce: `acc, preset => preset.transformMoveGenerator?.(engine, pieceId, acc) ?? acc`).
|
||
- Existing `getExtraMoves` / `filterMoves` continue to run downstream.
|
||
- Self-check filter MUST still run after all transformations.
|
||
- Add unit tests in `packages/chess/src/presets/transform-hook.test.ts`:
|
||
- Noop preset: legal moves unchanged.
|
||
- Wrapping preset: adds backward move to pawn; base forward moves still present.
|
||
- Two wrapping presets compose: second receives output of first.
|
||
- Self-check filter still runs: king-in-check blocks non-escaping moves even if transform added them.
|
||
|
||
**Must NOT do**: Don't add REPLACE semantics. Don't bypass self-check filter. Don't change existing hook signatures.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `deep` — contract design, subtle composition semantics
|
||
- **Skills**: [`code-search`]
|
||
|
||
**Parallelization**: Wave 2. Blocks: 6-11, 14. Blocked By: 1.
|
||
|
||
**References**:
|
||
- `packages/chess/src/presets/registry.ts` — current PresetDef shape
|
||
- `packages/chess/src/engine.ts:getAllLegalMoves` — where to integrate
|
||
- `docs/adr/modifier-profiles.md` § ADR-1 — contract spec
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bun run test packages/chess/src/presets/transform-hook.test.ts` → all tests pass
|
||
- [ ] `bun run check` green
|
||
- [ ] Existing preset tests unchanged (no regressions)
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Noop transform preserves legal moves
|
||
Tool: Bash
|
||
Steps:
|
||
1. Run: bun run test packages/chess/src/presets/transform-hook.test.ts
|
||
2. Assert: exit 0, "noop preset preserves legal moves" test passes
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-3-noop.txt
|
||
|
||
Scenario: Self-check filter still applies after transform
|
||
Tool: Bash
|
||
Steps:
|
||
1. Same test file — scenario "transform cannot bypass self-check"
|
||
2. Assert: king-in-check with transformed pawn → pawn move blocked
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-3-selfcheck.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(engine): add transformMoveGenerator + modifyMoveAttrs preset hooks`
|
||
- Files: `packages/chess/src/presets/registry.ts`, `packages/chess/src/engine.ts`, `packages/chess/src/presets/transform-hook.test.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 4. **Modifier profile types**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/modifiers/types.ts`:
|
||
```ts
|
||
export type ModifierKindId = "hp-bonus" | "range-bonus" | "direction-additions"
|
||
| "capture-flags" | "promotion-override" | "damage-resistance";
|
||
export interface TypeModifier { readonly kind: ModifierKindId; readonly pieceType: PieceType; readonly color: PieceColor; readonly value: unknown; }
|
||
export interface InstanceModifier { readonly kind: ModifierKindId; readonly square: Square; readonly value: unknown; }
|
||
export interface ModifierProfile {
|
||
readonly id: string;
|
||
readonly name: string;
|
||
readonly description: string;
|
||
readonly layoutId?: string; // Optional - only needed if per-instance used
|
||
readonly perType: readonly TypeModifier[];
|
||
readonly perInstance: readonly InstanceModifier[];
|
||
readonly version: 1;
|
||
readonly source: "premade" | "custom";
|
||
}
|
||
export interface ModifierDescriptor<V = unknown> {
|
||
readonly id: ModifierKindId;
|
||
readonly attrName: keyof ChessAttrMap;
|
||
readonly label: string;
|
||
readonly valueSchema: ZodSchema<V>;
|
||
readonly stackingRule: "additive" | "union" | "multiplicative" | "priority-wins";
|
||
readonly apply: (session: Session, pieceId: EntityId, value: V, sumOfOthers: V | null) => void;
|
||
readonly describe: (value: V) => string;
|
||
readonly uiForm: "number" | "direction-set" | "capture-flags" | "promotion-target" | "percentage";
|
||
}
|
||
```
|
||
|
||
**Must NOT do**: Don't implement descriptors yet (T6-T11). Don't write schema here (T12). Types only.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-low` — type definitions
|
||
- **Skills**: []
|
||
|
||
**Parallelization**: Wave 2. Blocks: 5, 6-11, 12. Blocked By: 1.
|
||
|
||
**References**:
|
||
- `packages/chess/src/layouts/types.ts` — parallel structure for reference
|
||
- `packages/chess/src/schema.ts` — ChessAttrMap import target
|
||
- `docs/adr/modifier-profiles.md` § ADR-2, ADR-8
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] File `packages/chess/src/modifiers/types.ts` exists
|
||
- [ ] `bun run check` green
|
||
- [ ] Types export: `ModifierKindId`, `TypeModifier`, `InstanceModifier`, `ModifierProfile`, `ModifierDescriptor`
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Types compile and export correctly
|
||
Tool: Bash
|
||
Steps:
|
||
1. Run: bun run check
|
||
2. Assert: exit 0
|
||
3. Run: grep "^export " packages/chess/src/modifiers/types.ts | wc -l
|
||
4. Assert: ≥5 exports
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-4-types.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(engine): modifier profile types`
|
||
- Files: `packages/chess/src/modifiers/types.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 5. **Modifier registry + barrel**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/modifiers/registry.ts`:
|
||
```ts
|
||
class ModifierRegistry {
|
||
private byId = new Map<ModifierKindId, ModifierDescriptor>();
|
||
register(d: ModifierDescriptor): void { /* throw if dup */ }
|
||
get(id: ModifierKindId): ModifierDescriptor | undefined { ... }
|
||
list(): readonly ModifierDescriptor[] { ... }
|
||
has(id: ModifierKindId): boolean { ... }
|
||
}
|
||
export const MODIFIER_REGISTRY = new ModifierRegistry();
|
||
```
|
||
- Create `packages/chess/src/modifiers/index.ts` — barrel + side-effect imports for all 6 descriptors (T6-T11 will add imports as they're written).
|
||
- Unit tests `packages/chess/src/modifiers/registry.test.ts`: register/get/list/duplicate-throws.
|
||
|
||
**Must NOT do**: Don't register any descriptors here. Don't implement descriptor logic.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `deep` — mirror existing registry patterns
|
||
- **Skills**: [`code-search`]
|
||
|
||
**Parallelization**: Wave 2. Blocks: 6-11, 14. Blocked By: 4.
|
||
|
||
**References**:
|
||
- `packages/chess/src/presets/registry.ts` — PresetRegistry pattern
|
||
- `packages/chess/src/layouts/registry.ts` — LayoutRegistry pattern
|
||
- `packages/chess/src/presets/piece-type-registry.ts` — another mirror
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bun run test packages/chess/src/modifiers/registry.test.ts` passes
|
||
- [ ] `MODIFIER_REGISTRY.list().length === 0` initially (before T6-T11 register)
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Registry accepts registrations and prevents duplicates
|
||
Tool: Bash
|
||
Steps:
|
||
1. Run: bun run test packages/chess/src/modifiers/registry.test.ts
|
||
2. Assert: exit 0, all tests pass
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-5-registry.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(engine): modifier registry pattern`
|
||
- Files: `packages/chess/src/modifiers/registry.ts`, `packages/chess/src/modifiers/index.ts`, `packages/chess/src/modifiers/registry.test.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 6. **HP-bonus descriptor + tests**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/modifiers/descriptors/hp-bonus.ts`:
|
||
- `id: "hp-bonus"`, `attrName: "HpBonus"`, `valueSchema: z.number().int().min(-10).max(10)`, `stackingRule: "additive"`, `uiForm: "number"`.
|
||
- `apply(session, pieceId, value, sumOfOthers)` → `session.insert(pieceId, "HpBonus", (sumOfOthers ?? 0) + value)`.
|
||
- `describe(v)` → `"HP ${v >= 0 ? '+' : ''}${v}"`.
|
||
- Side-effect register in `modifiers/index.ts`.
|
||
- Tests in `packages/chess/src/modifiers/descriptors/hp-bonus.test.ts`:
|
||
- Additive stacking (2 per-type + 1 per-instance → sum all 3).
|
||
- Precedence: no override behavior (additive means all contribute).
|
||
- Integration with piece-hp preset: total maxHp = base + HpBonus.
|
||
|
||
**Must NOT do**: Don't modify piece-hp preset itself. Don't handle damage here — that's orthogonal (DamageResistance).
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high` — descriptor + tests
|
||
- **Skills**: [`code-search`]
|
||
|
||
**Parallelization**: Wave 3 (PARALLEL with 7-11). Blocks: 14, 21, 22, 24. Blocked By: 2, 3, 5.
|
||
|
||
**References**:
|
||
- `packages/chess/src/presets/piece-hp.ts` — HP system integration
|
||
- `packages/chess/src/modifiers/types.ts` — descriptor shape
|
||
- `docs/adr/modifier-profiles.md` § ADR-4
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bun run test packages/chess/src/modifiers/descriptors/hp-bonus.test.ts` → all pass
|
||
- [ ] `MODIFIER_REGISTRY.get("hp-bonus")` returns descriptor
|
||
- [ ] Stacking test: 3 entries of +2 → piece gets HpBonus=6
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: HP bonus stacks additively across sources
|
||
Tool: Bash
|
||
Steps:
|
||
1. Run: bun run test packages/chess/src/modifiers/descriptors/hp-bonus.test.ts -t "stacks additively"
|
||
2. Assert: test passes, final HpBonus = sum of all sources
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-6-hp-stacking.txt
|
||
|
||
Scenario: HP bonus integrates with existing HP preset
|
||
Tool: Bash
|
||
Steps:
|
||
1. Same test file — "integrates with piece-hp preset"
|
||
2. Assert: maxHp === baseHp + HpBonus
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-6-hp-integration.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(engine): hp-bonus modifier descriptor`
|
||
- Files: `packages/chess/src/modifiers/descriptors/hp-bonus.ts`, `packages/chess/src/modifiers/descriptors/hp-bonus.test.ts`, `packages/chess/src/modifiers/index.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 7. **Range-bonus descriptor + tests**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/modifiers/descriptors/range-bonus.ts`:
|
||
- `id: "range-bonus"`, `attrName: "RangeBonus"`, `valueSchema: z.number().int().min(-7).max(7)`, `stackingRule: "additive"` (clamped to `[0, 7]`), `uiForm: "number"`.
|
||
- `apply` seeds `RangeBonus` fact as clamped sum.
|
||
- Integration: modify `modifyMoveAttrs` return so sliding pieces (rook/bishop/queen) see extended range.
|
||
- Unit behavior: update existing sliding-move generators to consume `session.get(pieceId, "RangeBonus") ?? 0` if present.
|
||
|
||
**Must NOT do**: Don't affect knight/king/pawn (non-sliding). Those get separate modifier (DirectionAdditions) if needed.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high`
|
||
- **Skills**: [`code-search`]
|
||
|
||
**Parallelization**: Wave 3 (PARALLEL with 6, 8-11). Blocks: 14, 21, 22, 24. Blocked By: 2, 3, 5.
|
||
|
||
**References**:
|
||
- `packages/chess/src/presets/piece-type-registry.ts` — sliding-move generators (rook, bishop, queen)
|
||
- `packages/chess/src/modifiers/types.ts`
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bun run test packages/chess/src/modifiers/descriptors/range-bonus.test.ts` → all pass
|
||
- [ ] Rook with +1 range: legal-move list includes one extra square per ray direction (when not blocked)
|
||
- [ ] Clamping: +10 + +5 on same piece → effective 7 (max)
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Range bonus extends sliding pieces
|
||
Tool: Bash
|
||
Steps:
|
||
1. Set up empty board + rook on a1 with RangeBonus=1 (test fixture, board size effectively lets us observe clamping)
|
||
2. Run test: verify rook legal moves extend by 1 square per direction
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-7-range-extension.txt
|
||
|
||
Scenario: Range bonus clamped to [0, 7]
|
||
Tool: Bash
|
||
Steps:
|
||
1. Apply +10 stacked bonus
|
||
2. Assert: effective value === 7
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-7-clamp.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(engine): range-bonus modifier descriptor`
|
||
- Files: `packages/chess/src/modifiers/descriptors/range-bonus.ts`, `.test.ts`, `modifiers/index.ts`, sliding-move generator updates
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 8. **Direction-additions descriptor + tests**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/modifiers/descriptors/direction-additions.ts`:
|
||
- `id: "direction-additions"`, `attrName: "DirectionAdditions"`, `valueSchema: z.array(directionEnum)`, `stackingRule: "union"`, `uiForm: "direction-set"`.
|
||
- `apply`: set fact to union of arrays.
|
||
- Integration: use `transformMoveGenerator` to wrap piece's base generator. Added directions generate 1-square moves (for step pieces) OR extend sliding pieces (for rook/bishop/queen) per the added direction.
|
||
- Test: pawn with `["backward"]` gets a backward-1 move; rook with `["diagonal-ne"]` gets a diagonal ray added.
|
||
|
||
**Must NOT do**: Don't auto-add capture semantics for new directions — that stays governed by CaptureFlags. Don't change existing generators except via the transform hook.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high` — non-trivial integration through transform hook
|
||
- **Skills**: [`code-search`]
|
||
|
||
**Parallelization**: Wave 3. Blocks: 14, 21, 22, 24. Blocked By: 2, 3, 5.
|
||
|
||
**References**:
|
||
- `packages/chess/src/presets/pawns-move-backward.ts` (if exists — search for backward pawn preset; else model from piece-type-registry)
|
||
- `packages/chess/src/modifiers/descriptors/range-bonus.ts` (T7 — similar transform hook pattern)
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bun run test packages/chess/src/modifiers/descriptors/direction-additions.test.ts` passes
|
||
- [ ] Pawn + backward direction: 1-square backward move appears in legal moves
|
||
- [ ] Union stacking: two entries each adding different directions → both present
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Backward-pawn modifier grants backward move
|
||
Tool: Bash
|
||
Steps:
|
||
1. Fixture: pawn on e4 white, no blockers
|
||
2. Apply DirectionAdditions=["backward"]
|
||
3. Query legal moves → assert e3 is in the set
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-8-backward-pawn.txt
|
||
|
||
Scenario: Direction union stacks without duplication
|
||
Tool: Bash
|
||
Steps:
|
||
1. Two sources: ["backward"] and ["sideways-left"] and ["backward"]
|
||
2. Assert final set = {backward, sideways-left} (no dup backward)
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-8-union.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(engine): direction-additions modifier descriptor`
|
||
- Files: `packages/chess/src/modifiers/descriptors/direction-additions.ts`, `.test.ts`, `modifiers/index.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 9. **Capture-flags descriptor + tests**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/modifiers/descriptors/capture-flags.ts`:
|
||
- `id: "capture-flags"`, `attrName: "CaptureFlags"`, `valueSchema: z.number().int().min(0).max(7)`, `stackingRule: "union"` (bitwise OR), `uiForm: "capture-flags"`.
|
||
- Flags: `CAN_CAPTURE_OWN = 1`, `CANNOT_BE_CAPTURED = 2`, `EN_PASSANT = 4`.
|
||
- Integration via `filterMoves` (if piece has `CANNOT_BE_CAPTURED`, filter out moves that would capture it from ALL enemy pieces) — this requires engine-wide scan.
|
||
- Integration via `filterMoves` for `CAN_CAPTURE_OWN`: include moves to own-color squares.
|
||
- Tests: 3 flag scenarios × happy path, flag union stacking, toggling behavior on live board.
|
||
|
||
**Must NOT do**: Don't conflate with `CaptureHook` (which governs damage events). CaptureFlags govern move-generation legality only.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high` — touches multiple hook points
|
||
- **Skills**: [`code-search`]
|
||
|
||
**Parallelization**: Wave 3. Blocks: 14, 21, 22, 24. Blocked By: 2, 3, 5.
|
||
|
||
**References**:
|
||
- `packages/chess/src/rules/capture.ts` — existing capture logic
|
||
- `packages/chess/src/engine.ts:filterMoves invocation`
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] Piece with `CANNOT_BE_CAPTURED`: no enemy legal move targets it
|
||
- [ ] Piece with `CAN_CAPTURE_OWN`: legal moves include own-color squares
|
||
- [ ] Union stacking: flags OR'd correctly
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: CANNOT_BE_CAPTURED shields piece from enemies
|
||
Tool: Bash
|
||
Steps:
|
||
1. Fixture: white knight b1 with flag=2; black queen in check range
|
||
2. Query black's legal moves → assert queen cannot target b1
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-9-shielded.txt
|
||
|
||
Scenario: CAN_CAPTURE_OWN allows friendly fire
|
||
Tool: Bash
|
||
Steps:
|
||
1. White knight with flag=1, friendly pawn in move range
|
||
2. Assert knight's legal moves include the friendly square
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-9-friendly-fire.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(engine): capture-flags modifier descriptor`
|
||
- Files: `packages/chess/src/modifiers/descriptors/capture-flags.ts`, `.test.ts`, `modifiers/index.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 10. **Promotion-override descriptor + tests**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/modifiers/descriptors/promotion-override.ts`:
|
||
- `id: "promotion-override"`, `attrName: "PromotionOverride"`, `valueSchema: z.union([pieceTypeSchema, z.literal("disabled")])`, `stackingRule: "priority-wins"` (per-instance > per-type), `uiForm: "promotion-target"`.
|
||
- Integration: hook into existing promotion logic in move-resolver. If piece has `PromotionOverride="disabled"`, remove the promotion step from the move. If set to a piece type, force that type.
|
||
- Tests: pawn with override="queen" always promotes to queen; override="disabled" → pawn stays pawn; per-instance beats per-type.
|
||
|
||
**Must NOT do**: Don't allow promotion to "king" (validator rejects — kings are singular-ish). Don't change default promotion prompt for pieces without override.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high`
|
||
- **Skills**: [`code-search`]
|
||
|
||
**Parallelization**: Wave 3. Blocks: 14, 21, 22, 24. Blocked By: 2, 3, 5.
|
||
|
||
**References**:
|
||
- `packages/chess/src/rules/` — search for "promotion" to find resolver
|
||
- `packages/chess/src/modifiers/types.ts`
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] Pawn reaches rank 8 with override="bishop" → becomes bishop (no prompt)
|
||
- [ ] Pawn with override="disabled" → stays pawn on rank 8
|
||
- [ ] Per-instance override beats per-type override
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Promotion forced to specific type
|
||
Tool: Bash
|
||
Steps:
|
||
1. Fixture: pawn on e7 with PromotionOverride="bishop"
|
||
2. Execute move e7→e8
|
||
3. Assert piece on e8 is bishop
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-10-forced.txt
|
||
|
||
Scenario: Promotion disabled keeps piece as pawn
|
||
Tool: Bash
|
||
Steps:
|
||
1. Fixture: pawn on e7 with PromotionOverride="disabled"
|
||
2. Move to e8 — assert piece remains pawn
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-10-disabled.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(engine): promotion-override modifier descriptor`
|
||
- Files: `packages/chess/src/modifiers/descriptors/promotion-override.ts`, `.test.ts`, `modifiers/index.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 11. **Damage-resistance descriptor + tests**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/modifiers/descriptors/damage-resistance.ts`:
|
||
- `id: "damage-resistance"`, `attrName: "DamageResistance"`, `valueSchema: z.number().min(0).max(1)`, `stackingRule: "multiplicative"` (as `1 - ∏(1 - r_i)`), `uiForm: "percentage"`.
|
||
- Hook into `onDamage`: reduce incoming damage by `amount * (1 - effectiveResistance)`. Clamp final damage to `≥ 0`.
|
||
- Tests:
|
||
- 0.5 resistance → half damage.
|
||
- Stacking: 0.5 + 0.5 → `1 - (0.5 * 0.5) = 0.75` → 25% damage taken.
|
||
- Integration with HP preset: piece with HP=2 takes 1 damage (instead of 2) with 0.5 resistance.
|
||
|
||
**Must NOT do**: Don't reduce damage below 0 (no healing via damage). Don't make resistance value negative.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high` — integrates with existing damage pipeline
|
||
- **Skills**: [`code-search`]
|
||
|
||
**Parallelization**: Wave 3. Blocks: 14, 21, 22, 24. Blocked By: 2, 3, 5.
|
||
|
||
**References**:
|
||
- `packages/chess/src/presets/piece-hp.ts` — existing `onDamage` hook
|
||
- `packages/chess/src/engine.ts:dealDamage`
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bun run test packages/chess/src/modifiers/descriptors/damage-resistance.test.ts` passes
|
||
- [ ] Multiplicative stacking formula verified
|
||
- [ ] Damage clamped to ≥ 0
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Resistance reduces damage multiplicatively
|
||
Tool: Bash
|
||
Steps:
|
||
1. Piece HP=10 with resistance=0.5
|
||
2. Deal 4 damage
|
||
3. Assert HP after = 8 (damage halved)
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-11-reduction.txt
|
||
|
||
Scenario: Stacked resistances compound
|
||
Tool: Bash
|
||
Steps:
|
||
1. Two resistances 0.5 + 0.5 → effective 0.75
|
||
2. Deal 4 damage → assert HP reduced by 1 (4 * 0.25)
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-11-stack.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(engine): damage-resistance modifier descriptor`
|
||
- Files: `packages/chess/src/modifiers/descriptors/damage-resistance.ts`, `.test.ts`, `modifiers/index.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 12. **Zod schema + roundtrip tests**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/modifiers/schema.ts` with Zod schemas for:
|
||
- `TypeModifierSchema` — validates `kind` ∈ registry, `pieceType`, `color`, `value` dispatched via `MODIFIER_REGISTRY.get(kind).valueSchema`.
|
||
- `InstanceModifierSchema` — same but with `square` instead of `pieceType`.
|
||
- `ModifierProfileSchema` — full profile with `version: z.literal(1)`.
|
||
- `parse(profile)` → `ModifierProfile`; `stringify(profile)` → canonical JSON.
|
||
- Tests: roundtrip (parse-stringify-parse deep-equal); 10+ invalid fixtures rejected with specific errors; unknown `kind` rejected.
|
||
|
||
**Must NOT do**: Don't hardcode value schemas — delegate to registry. Don't version-skip (v1 only in T1; v0 migration stub only if needed for future).
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-low` — Zod schema work
|
||
- **Skills**: [`context7`] — latest Zod API
|
||
|
||
**Parallelization**: Wave 3. Blocks: 13, 17, 19. Blocked By: 4.
|
||
|
||
**References**:
|
||
- `packages/chess/src/layouts/schema.ts` or equivalent — mirror layouts pattern
|
||
- `packages/server/src/protocol.ts` — Zod usage patterns in project
|
||
- Zod v3 docs: `z.discriminatedUnion`, `z.record`
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bun run test packages/chess/src/modifiers/schema.test.ts` passes
|
||
- [ ] 10+ invalid fixtures rejected with specific error messages
|
||
- [ ] Roundtrip test: `parse(JSON.parse(JSON.stringify(profile)))` deep-equal to input
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Valid profile roundtrips losslessly
|
||
Tool: Bash
|
||
Steps:
|
||
1. Run: bun run test packages/chess/src/modifiers/schema.test.ts -t "roundtrip"
|
||
2. Assert: exit 0, no output diff
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-12-roundtrip.txt
|
||
|
||
Scenario: Invalid kind rejected
|
||
Tool: Bash
|
||
Steps:
|
||
1. Test fixture with kind="nonexistent"
|
||
2. Run schema test — assert rejection with `ZodError` path
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-12-invalid.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(engine): ModifierProfile Zod schema + roundtrip tests`
|
||
- Files: `packages/chess/src/modifiers/schema.ts`, `packages/chess/src/modifiers/schema.test.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 13. **Library persistence**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/modifiers/library.ts`:
|
||
- Key: `houserules:modifier-profiles:v1`
|
||
- `SavedModifierProfile` shape: `{ profile: ModifierProfile, starred: boolean, createdAt, updatedAt }`
|
||
- API: `saveToLibrary`, `loadLibrary`, `deleteFromLibrary`, `setStarred`, `duplicateEntry`, `makeId`.
|
||
- Max 20 entries; oldest-non-starred eviction (mirror layout-library).
|
||
- localStorage shim for tests (mirror `layout-library.test.ts`).
|
||
- Tests: `packages/chess/src/modifiers/library.test.ts` — save/load/delete/star/evict/max-starred-full (14+ tests mirroring layout-library).
|
||
|
||
**Must NOT do**: Don't add cloud sync. Don't support multiple library namespaces.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-low` — direct mirror of layout-library
|
||
- **Skills**: []
|
||
|
||
**Parallelization**: Wave 3. Blocks: 23. Blocked By: 12.
|
||
|
||
**References**:
|
||
- `packages/chess/src/persist/layout-library.ts` — direct mirror
|
||
- `packages/chess/src/persist/layout-library.test.ts` — mirror tests
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bun run test packages/chess/src/modifiers/library.test.ts` passes (14+ tests)
|
||
- [ ] Save → load roundtrip returns identical entries
|
||
- [ ] Eviction works (21st save removes oldest-non-starred)
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Save and reload library entry
|
||
Tool: Bash
|
||
Steps:
|
||
1. Run: bun run test packages/chess/src/modifiers/library.test.ts -t "save and load"
|
||
2. Assert: entry saved, loaded identically
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-13-save.txt
|
||
|
||
Scenario: Eviction preserves starred entries
|
||
Tool: Bash
|
||
Steps:
|
||
1. Test "evict oldest non-starred"
|
||
2. Assert: starred entries survive, oldest unstarred evicted
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-13-evict.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(engine): modifier-profile library persistence (v1)`
|
||
- Files: `packages/chess/src/modifiers/library.ts`, `packages/chess/src/modifiers/library.test.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 14. **applyProfileToSession (game start)**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/modifiers/apply.ts`:
|
||
- `applyProfileToSession(session, profile, layout)`:
|
||
1. For each `perType` entry: iterate pieces matching `(pieceType, color)`, seed modifier facts via `descriptor.apply(session, pieceId, value, sumOfOthers)`.
|
||
2. For each `perInstance` entry: look up pieceId at `square` in layout's piece mapping; if found, seed fact; if not found, log warning (orphan handling per ADR-2).
|
||
3. Stacking: collect all values for each `(pieceId, kind)` pair BEFORE applying, compute effective per ADR-4 stacking rules, then apply once.
|
||
- Integrate with `ChessEngine` constructor: if `options.profile` supplied, call after layout applied but before presets activate.
|
||
- Tests `apply.test.ts`: seed facts correctly for per-type + per-instance; stacking verified; orphan instances logged + skipped.
|
||
|
||
**Must NOT do**: Don't apply profile mid-game (that's T15). Don't modify engine's preset activation order beyond the single insertion point.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `deep` — critical lifecycle integration
|
||
- **Skills**: []
|
||
|
||
**Parallelization**: Wave 4. Blocks: 15, 19. Blocked By: 6-11, 16.
|
||
|
||
**References**:
|
||
- `packages/chess/src/starting-position.ts:applyLayout` — similar function, same invocation site
|
||
- `packages/chess/src/engine.ts` — constructor + lifecycle
|
||
- `docs/adr/modifier-profiles.md` § ADR-4, ADR-5
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bun run test packages/chess/src/modifiers/apply.test.ts` passes
|
||
- [ ] `effectivePieceAttrs(pieceId)` on a modified piece matches expected table
|
||
- [ ] Orphan `perInstance` entry logged as warning, skipped
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Per-type modifiers seed all matching pieces
|
||
Tool: Bash
|
||
Steps:
|
||
1. Profile: perType=[{kind:"hp-bonus", pieceType:"knight", color:"white", value:3}]
|
||
2. Apply to session with classic layout
|
||
3. Assert: both white knights have HpBonus=3
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-14-per-type.txt
|
||
|
||
Scenario: Per-instance modifier targets specific square
|
||
Tool: Bash
|
||
Steps:
|
||
1. Profile: perInstance=[{kind:"range-bonus", square:"b1", value:2}]
|
||
2. Apply
|
||
3. Assert: b1 knight has RangeBonus=2; g1 knight does not
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-14-per-instance.txt
|
||
|
||
Scenario: Orphan instance entry handled gracefully
|
||
Tool: Bash
|
||
Steps:
|
||
1. Profile with perInstance square="z9" (invalid)
|
||
2. Apply — assert no throw, warning logged, other entries still applied
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-14-orphan.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(engine): apply profile at game start`
|
||
- Files: `packages/chess/src/modifiers/apply.ts`, `.test.ts`, `packages/chess/src/engine.ts` (integration point)
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 15. **reconcileProfileSwap (hot-swap)**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/modifiers/reconcile.ts`:
|
||
- `reconcileProfileSwap(session, oldProfile, newProfile, layout)`:
|
||
1. Compute old effective modifiers per piece.
|
||
2. Compute new effective modifiers per piece.
|
||
3. Diff → for each (pieceId, attr) pair, apply per-kind reconciliation rule (ADR-3):
|
||
- HpBonus change: recompute maxHp, clamp currentHp to `min(currentHp, newMax)`.
|
||
- RangeBonus / DirectionAdditions change: overwrite fact (next move query will reflect).
|
||
- CaptureFlags change: overwrite (applies to next move).
|
||
- PromotionOverride change: overwrite (applies to future promotions).
|
||
- DamageResistance change: overwrite (applies to next damage event).
|
||
4. Idempotent: `reconcile(a, b, layout); reconcile(a, b, layout)` → same state as single call.
|
||
- Tests: 5+ scenarios including HP clamp on maxReduction, idempotency, no-op when profiles equal, full swap (all modifiers different), partial swap.
|
||
|
||
**Must NOT do**: Don't apply mid-turn. Don't retroactively resolve past events (in-flight attacks resolve against pre-swap state).
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `deep` — determinism-critical
|
||
- **Skills**: []
|
||
|
||
**Parallelization**: Wave 4. Blocks: 20. Blocked By: 14.
|
||
|
||
**References**:
|
||
- `packages/chess/src/modifiers/apply.ts` (T14) — source of effective computation
|
||
- `docs/adr/modifier-profiles.md` § ADR-3
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bun run test packages/chess/src/modifiers/reconcile.test.ts` passes (5+ scenarios)
|
||
- [ ] HP clamp rule: piece at 7/10 HP, modifier removes +3 → maxHp=7, currentHp=7 (clamped)
|
||
- [ ] Idempotency test passes
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: HP clamp on maxHp reduction
|
||
Tool: Bash
|
||
Steps:
|
||
1. Piece at currentHp=7, maxHp=10 (HpBonus=+3)
|
||
2. Reconcile to profile with HpBonus=0 → maxHp=7, currentHp=min(7,7)=7
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-15-hp-clamp.txt
|
||
|
||
Scenario: Reconcile is idempotent
|
||
Tool: Bash
|
||
Steps:
|
||
1. Reconcile(old, new, layout) twice
|
||
2. Assert session state identical after 1 call and after 2 calls
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-15-idempotent.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(engine): hot-swap reconciliation`
|
||
- Files: `packages/chess/src/modifiers/reconcile.ts`, `.test.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 16. **Legality validator**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/modifiers/validate.ts`:
|
||
- `validateProfile(profile, layout, session?): ValidationResult { errors: [...], warnings: [...] }`
|
||
- Checklist (ADR-6):
|
||
1. Both sides have ≥1 king (after applying all kingless-impact modifiers — e.g., CANNOT_BE_CAPTURED on king is fine; we only check presence).
|
||
2. No king has Invulnerability / CANNOT_BE_CAPTURED (HARD — treated as error).
|
||
3. No per-instance orphan refs (WARN, not error).
|
||
4. Each side has ≥1 legal move on starting position (fast query — skip if session not provided).
|
||
5. Attrs per piece ≤ 16 (DoS prevention).
|
||
- Error codes: `E_PROFILE_NO_KING`, `E_PROFILE_INVULN_KING`, `E_PROFILE_ORPHAN_INSTANCE`, `E_PROFILE_DEADLOCK`, `E_PROFILE_ATTR_LIMIT`.
|
||
- Tests: each error code has a positive + negative fixture; warning-level orphan doesn't block save.
|
||
|
||
**Must NOT do**: Don't run full legal-game simulation (too slow for hot-swap). Don't over-validate — T1 catches the critical-safety subset only.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `deep` — checklist correctness is critical
|
||
- **Skills**: []
|
||
|
||
**Parallelization**: Wave 4. Blocks: 14, 19, 20. Blocked By: 12.
|
||
|
||
**References**:
|
||
- `packages/chess/src/layouts/validate.ts` — sibling validator pattern
|
||
- `docs/adr/modifier-profiles.md` § ADR-6
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bun run test packages/chess/src/modifiers/validate.test.ts` passes
|
||
- [ ] 5 error codes each tested positive + negative (10+ tests)
|
||
- [ ] Invuln on king → error; invuln on queen → allowed
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Invuln on king rejected
|
||
Tool: Bash
|
||
Steps:
|
||
1. Profile: perInstance=[{kind:"capture-flags", square:"e1", value: CANNOT_BE_CAPTURED}]
|
||
2. Validate against classic layout
|
||
3. Assert: errors contains E_PROFILE_INVULN_KING
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-16-invuln-king.txt
|
||
|
||
Scenario: Orphan instance triggers warning not error
|
||
Tool: Bash
|
||
Steps:
|
||
1. Profile with perInstance square="z9"
|
||
2. Validate
|
||
3. Assert: warnings has entry; errors empty
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-16-orphan.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(engine): profile legality validator`
|
||
- Files: `packages/chess/src/modifiers/validate.ts`, `.test.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 17. **Server protocol extensions**
|
||
|
||
**What to do**:
|
||
- Extend `packages/server/src/protocol.ts`:
|
||
- Import `ModifierProfileSchema` from chess package (re-export).
|
||
- Add to `RoomCreatePayloadSchema`: `profile?: z.union([ProfileByIdSchema, ModifierProfileSchema]).optional()` (by ID reference to library OR inline profile).
|
||
- New message: `ModifierProfileUpdatePayloadSchema` — `{ roomCode, newProfile: ModifierProfile, version: number }`.
|
||
- New broadcast: `ModifierProfileUpdatedPayload` — `{ profile: ModifierProfile, version: number, appliedAt: "turn-boundary" }`.
|
||
- Error codes: `MODIFIER_PROFILE_INVALID`, `MODIFIER_PROFILE_NO_KING`, `MODIFIER_PROFILE_INVULN_KING`, `MODIFIER_PROFILE_DEADLOCK`.
|
||
- Wire types in `packages/chess/src/net/types.ts`: mirror server.
|
||
- Update `packages/server/PROTOCOL.md` with full docs + examples.
|
||
- Tests in `protocol.test.ts`: all new schemas validate valid payloads, reject invalid.
|
||
|
||
**Must NOT do**: Don't modify existing messages. Don't remove or rename existing fields.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-low` — additive schema work
|
||
- **Skills**: []
|
||
|
||
**Parallelization**: Wave 4. Blocks: 19, 20. Blocked By: 12, 16.
|
||
|
||
**References**:
|
||
- `packages/server/src/protocol.ts` — current shape (look for `RoomCreatePayloadSchema`, `LAYOUT_INVALID` precedent)
|
||
- `packages/server/PROTOCOL.md` — docs pattern
|
||
- `packages/chess/src/net/types.ts` — client mirror
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bun run test packages/server/src/protocol.test.ts` passes (existing + new)
|
||
- [ ] `bun run check` green
|
||
- [ ] PROTOCOL.md has sections for all new messages with example JSON
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: room-create accepts inline profile
|
||
Tool: Bash
|
||
Steps:
|
||
1. Fixture: valid RoomCreatePayload with profile inline
|
||
2. Run schema.parse → assert no throw
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-17-inline.txt
|
||
|
||
Scenario: modifier-profile.update schema validates
|
||
Tool: Bash
|
||
Steps:
|
||
1. Fixture: valid update payload with version + profile
|
||
2. Assert parse succeeds
|
||
3. Fixture: missing version → assert rejection
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-17-update.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(server): modifier profile protocol schemas + error codes`
|
||
- Files: `packages/server/src/protocol.ts`, `packages/server/src/protocol.test.ts`, `packages/server/PROTOCOL.md`, `packages/chess/src/net/types.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 18. **UI editor shell + rules drawer entry**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/ui/ModifierProfileEditor.tsx`:
|
||
- Modal-style editor (mirror `LayoutEditor.tsx` layout).
|
||
- Layout: left=palette (modifier catalog), center=board preview (reuse `LayoutEditor`'s board primitives), right=panel switcher (per-type / per-instance / library).
|
||
- State: working profile draft + isDirty flag.
|
||
- Header: name input, description input, save/cancel buttons, library drawer toggle.
|
||
- Accessible from rules drawer: add "Modifier Profiles" entry in `RulesDrawer.tsx` (or equivalent) that opens the editor.
|
||
- Add e2e fixture: `data-testid="open-modifier-editor"`, `data-testid="modifier-editor-modal"`.
|
||
|
||
**Must NOT do**: Don't implement per-type or per-instance panels yet (T21, T22). Don't write save/load (T23). Shell only.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `visual-engineering`
|
||
- **Skills**: [`interface-design`, `frontend-ui-ux`]
|
||
|
||
**Parallelization**: Wave 4. Blocks: 21, 22, 23. Blocked By: 5.
|
||
|
||
**References**:
|
||
- `packages/chess/src/ui/LayoutEditor.tsx` — shell pattern to mirror
|
||
- Find rules drawer: `grep -r "rules" packages/chess/src/ui --include="*.tsx"` (probably in GameView or settings drawer)
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bunx playwright test e2e/modifier-profiles.spec.ts -g "editor opens"` passes
|
||
- [ ] Editor visible within 1s of click
|
||
- [ ] Esc closes editor (reuse layout-editor pattern)
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Editor opens from rules drawer
|
||
Tool: Playwright
|
||
Preconditions: Game running solo mode, rules drawer available
|
||
Steps:
|
||
1. Click [data-testid=open-rules-drawer]
|
||
2. Click [data-testid=open-modifier-editor]
|
||
3. Wait for [data-testid=modifier-editor-modal] visible (timeout 1s)
|
||
4. Screenshot evidence
|
||
Expected Result: modal visible within 1s
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-18-open.png
|
||
|
||
Scenario: Esc closes editor
|
||
Tool: Playwright
|
||
Steps:
|
||
1. Open editor
|
||
2. Press Escape
|
||
3. Assert modal hidden within 300ms
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-18-esc.png
|
||
```
|
||
|
||
**Commit**: YES — `feat(ui): modifier profile editor shell + rules drawer entry`
|
||
- Files: `packages/chess/src/ui/ModifierProfileEditor.tsx`, rules-drawer update, `e2e/modifier-profiles.spec.ts` (first tests)
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 19. **Server: room-create accepts profile**
|
||
|
||
**What to do**:
|
||
- Update `packages/server/src/rooms.ts`: add `Room.profile?: ModifierProfile` field.
|
||
- Update `packages/server/src/game-session.ts`: accept optional profile, pass to ChessEngine options bag.
|
||
- Update `packages/server/src/broadcast.ts`: in `handleRoomCreate`:
|
||
1. If payload has `profile`, resolve it (inline OR by-id lookup stub — for T1 inline only).
|
||
2. Validate via chess `validateProfile(profile, layout)`.
|
||
3. If invalid → respond with error code; don't create room.
|
||
4. If valid → create room with profile; echo in `room.created` payload.
|
||
- Tests `packages/server/src/room.create-profile.test.ts`: valid profile creates room; invalid king-invuln rejected with `MODIFIER_PROFILE_INVULN_KING`.
|
||
|
||
**Must NOT do**: Don't support profile-by-id lookup (T1: inline only). Don't break existing no-profile room-create.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `deep` — integration + validation
|
||
- **Skills**: []
|
||
|
||
**Parallelization**: Wave 5. Blocks: 26, 27. Blocked By: 14, 17.
|
||
|
||
**References**:
|
||
- `packages/server/src/broadcast.ts` — existing layout resolution pattern (in `handleRoomCreate`)
|
||
- `packages/server/src/rooms.ts` — Room shape (see layout field added in prior plan)
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bun run test packages/server/src/room.create-profile.test.ts` passes
|
||
- [ ] Existing room-create tests without profile still pass
|
||
- [ ] Invalid profile rejected with specific error code
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Room created with valid profile
|
||
Tool: Bash
|
||
Steps:
|
||
1. Test creates room with profile in payload
|
||
2. Assert: room.profile === submitted profile; broadcast echoes
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-19-create.txt
|
||
|
||
Scenario: Invalid profile rejects room creation
|
||
Tool: Bash
|
||
Steps:
|
||
1. Test with profile granting CANNOT_BE_CAPTURED to king
|
||
2. Assert: response error code MODIFIER_PROFILE_INVULN_KING
|
||
3. Assert: no room created
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-19-invuln.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(server): room-create accepts profile`
|
||
- Files: `packages/server/src/rooms.ts`, `packages/server/src/game-session.ts`, `packages/server/src/broadcast.ts`, `packages/server/src/room.create-profile.test.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 20. **Server: modifier-profile.update WS handler**
|
||
|
||
**What to do**:
|
||
- Add handler in `packages/server/src/broadcast.ts` for `modifier-profile.update` message:
|
||
1. Parse payload via `ModifierProfileUpdatePayloadSchema`.
|
||
2. Authenticate: only room host OR both-player consent (T1: host-only for simplicity).
|
||
3. Queue until turn-boundary (ADR-3): if mid-turn, set `room.pendingProfile`; apply on next `turnEnd`.
|
||
4. On apply: validate via `validateProfile(profile, layout, session)`. If invalid → NACK to sender with error code; don't broadcast.
|
||
5. If valid: call `reconcileProfileSwap(session, oldProfile, newProfile, layout)` (T15); update `room.profile`; increment version; broadcast `modifier-profile.updated` to all clients with new profile + facts delta.
|
||
- Tests `packages/server/src/ws.modifier-profile-update.test.ts`: happy path, illegal-rejected-with-rollback, turn-boundary queueing, simultaneous-swap (last-write-wins).
|
||
|
||
**Must NOT do**: Don't broadcast invalid swaps. Don't allow clients to bypass validator. Don't support mid-turn application (queue instead).
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `deep` — concurrency + state consistency
|
||
- **Skills**: [`code-search`]
|
||
|
||
**Parallelization**: Wave 5. Blocks: 24, 25, 27. Blocked By: 15, 17.
|
||
|
||
**References**:
|
||
- `packages/server/src/broadcast.ts` — existing WS message dispatcher
|
||
- `docs/adr/modifier-profiles.md` § ADR-3, ADR-6
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] `bun run test packages/server/src/ws.modifier-profile-update.test.ts` passes (4+ scenarios)
|
||
- [ ] Happy path: broadcast to all clients with new version number
|
||
- [ ] Illegal: NACK to sender only; no broadcast
|
||
- [ ] Turn-boundary queueing: mid-turn update doesn't apply immediately
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Happy-path hot-swap broadcasts
|
||
Tool: Bash
|
||
Steps:
|
||
1. Test sends valid modifier-profile.update
|
||
2. Trigger turnEnd
|
||
3. Assert: all connected clients receive modifier-profile.updated with new version
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-20-happy.txt
|
||
|
||
Scenario: Illegal swap rollback
|
||
Tool: Bash
|
||
Steps:
|
||
1. Send update granting invuln to king
|
||
2. Trigger turnEnd
|
||
3. Assert: sender receives NACK with error code; no broadcast; room.profile unchanged
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-20-illegal.txt
|
||
```
|
||
|
||
**Commit**: YES — `feat(server): modifier-profile.update WS handler`
|
||
- Files: `packages/server/src/broadcast.ts`, `packages/server/src/ws.modifier-profile-update.test.ts`
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 21. **UI per-type panel**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/ui/PerTypePanel.tsx`:
|
||
- Layout: list of `TypeModifier` rows; "Add Type Modifier" button opens sub-form.
|
||
- Sub-form: piece type dropdown (from PIECE_TYPE_REGISTRY), color toggle (white/black/both), modifier kind dropdown (from MODIFIER_REGISTRY), value input (schema-driven from `descriptor.uiForm`).
|
||
- Validation: Zod schemas per modifier kind validate value input live.
|
||
- On save: append to working profile's `perType` array.
|
||
- On delete: remove entry.
|
||
- Unit tests via Playwright: add per-type modifier → panel shows row with descriptor.describe output.
|
||
|
||
**Must NOT do**: Don't implement per-instance here (T22). Don't bake hardcoded modifier kinds — iterate `MODIFIER_REGISTRY.list()`.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `visual-engineering`
|
||
- **Skills**: [`interface-design`]
|
||
|
||
**Parallelization**: Wave 5. Blocks: 26, 27. Blocked By: 6-11, 18.
|
||
|
||
**References**:
|
||
- `packages/chess/src/ui/ModifierProfileEditor.tsx` (T18) — parent shell
|
||
- `packages/chess/src/ui/LayoutEditor.tsx` — form patterns
|
||
- `packages/chess/src/modifiers/registry.ts` — iteration source
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] Adding a per-type modifier shows in row list
|
||
- [ ] All 6 modifier kinds selectable in dropdown
|
||
- [ ] Invalid value input disables Save button
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Add per-type HP modifier
|
||
Tool: Playwright
|
||
Steps:
|
||
1. Open modifier editor → Per-Type tab
|
||
2. Click [data-testid=add-type-modifier]
|
||
3. Select pieceType=knight, color=white, kind=hp-bonus, value=2
|
||
4. Click [data-testid=save-type-modifier]
|
||
5. Assert: row shows "Knight (white): HP +2"
|
||
6. Screenshot evidence
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-21-add.png
|
||
|
||
Scenario: Invalid value disables save
|
||
Tool: Playwright
|
||
Steps:
|
||
1. Open add-modifier form, set range-bonus value=100 (out of range)
|
||
2. Assert [data-testid=save-type-modifier] is disabled
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-21-invalid.png
|
||
```
|
||
|
||
**Commit**: YES — `feat(ui): per-type modifier panel`
|
||
- Files: `packages/chess/src/ui/PerTypePanel.tsx`, editor wiring, e2e additions
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 22. **UI per-instance panel (board picker)**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/ui/PerInstancePanel.tsx`:
|
||
- Layout: board preview (reuse LayoutEditor's board primitive) + click-to-select piece.
|
||
- Selected piece panel: list current per-instance modifiers for that square; "Add Modifier" form (modifier kind + value input — schema-driven).
|
||
- Per ADR-2: profile must be bound to a layout. If no layout selected yet → prompt user to pick layout first.
|
||
- On selection, show modifier list; on add, append to working profile's `perInstance` array keyed by `square`.
|
||
|
||
**Must NOT do**: Don't allow per-instance without a layout. Don't permit editing to produce orphans silently (show warning).
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `visual-engineering`
|
||
- **Skills**: [`interface-design`]
|
||
|
||
**Parallelization**: Wave 5. Blocks: 26, 27. Blocked By: 6-11, 18.
|
||
|
||
**References**:
|
||
- `packages/chess/src/ui/LayoutEditor.tsx:BoardPanel` — board primitive
|
||
- `packages/chess/src/ui/PerTypePanel.tsx` (T21) — parallel form pattern
|
||
- `docs/adr/modifier-profiles.md` § ADR-2
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] Click piece on board → selection highlight + modifier list appears
|
||
- [ ] Adding modifier keys it by `square` in `perInstance` array
|
||
- [ ] Changing bound layout re-maps selection appropriately
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Per-instance modifier attached to specific piece
|
||
Tool: Playwright
|
||
Steps:
|
||
1. Open modifier editor → Per-Instance tab → select layout "classic"
|
||
2. Click b1 knight on preview board
|
||
3. Add modifier: kind=range-bonus, value=1
|
||
4. Assert: profile's perInstance contains {square:"b1", kind:"range-bonus", value:1}
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-22-attach.png
|
||
|
||
Scenario: No-layout state prompts selection
|
||
Tool: Playwright
|
||
Steps:
|
||
1. Open editor, no layout bound
|
||
2. Assert Per-Instance tab shows "Select a layout first"
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-22-no-layout.png
|
||
```
|
||
|
||
**Commit**: YES — `feat(ui): per-instance modifier panel`
|
||
- Files: `packages/chess/src/ui/PerInstancePanel.tsx`, e2e additions
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 23. **UI save/load library + URL share**
|
||
|
||
**What to do**:
|
||
- Add to `ModifierProfileEditor.tsx`:
|
||
- Save button: writes to library via `saveToLibrary()`.
|
||
- Library drawer: list saved profiles, load/rename/delete/star actions.
|
||
- Share button: encode profile as URL param `?modifierProfileId=<id>` OR `?modifierProfile=<base64-json>` for portability.
|
||
- URL reader: `Lobby.tsx` reads `modifierProfile*` params and seeds editor/creates room.
|
||
- Size check: enforce ≤ 8KB for URL-encoded profile; fall back to library-id share if too large.
|
||
|
||
**Must NOT do**: Don't auto-save (explicit Save click only). Don't share as plain URL if > 8KB.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-low` — mirrors layout save/share
|
||
- **Skills**: []
|
||
|
||
**Parallelization**: Wave 5. Blocks: 26, 27. Blocked By: 13, 18.
|
||
|
||
**References**:
|
||
- `packages/chess/src/ui/Lobby.tsx` — layout URL-param pattern (same approach)
|
||
- `packages/chess/src/ui/LayoutEditor.tsx` — library drawer pattern
|
||
- `packages/chess/src/modifiers/library.ts` (T13)
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] Save → refresh page → reload from library → profile identical
|
||
- [ ] URL share encode/decode roundtrips
|
||
- [ ] Profile > 8KB falls back to library-id path
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Save and reload profile from library
|
||
Tool: Playwright
|
||
Steps:
|
||
1. Create profile "My HP" with a modifier, click Save
|
||
2. Close editor, reopen, open library drawer
|
||
3. Click "My HP" entry → assert editor loads with matching state
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-23-save-reload.png
|
||
|
||
Scenario: URL share roundtrips
|
||
Tool: Playwright
|
||
Steps:
|
||
1. Save profile, click Share → copy URL
|
||
2. Open URL in new tab
|
||
3. Assert profile pre-loaded in editor
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-23-share.png
|
||
```
|
||
|
||
**Commit**: YES — `feat(ui): profile library save/load + URL share`
|
||
- Files: `ModifierProfileEditor.tsx` updates, Lobby.tsx URL reader, e2e additions
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 24. **Hover tooltip inspection**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/ui/ModifierTooltip.tsx`:
|
||
- Listens to hover events on piece squares in `GameView.tsx`.
|
||
- Reads effective modifiers from session facts via `engine.session.get(pieceId, attr)` for each registered attr.
|
||
- Cross-references with active profile to determine SOURCE of each modifier (per-type vs per-instance vs preset vs base).
|
||
- Tooltip layout: piece name + color, list of active modifiers with source badge each (e.g. "HP +2 (per-type)", "Range +1 (per-instance)").
|
||
- Appear within 200ms of hover.
|
||
- Reuse existing floating-ui lib if in project (grep first).
|
||
- Wire into GameView.
|
||
|
||
**Must NOT do**: Don't make tooltip modal / blocking. Don't require click (hover only for this task). Don't show pre-T1 modifiers (unregistered attrs).
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `visual-engineering`
|
||
- **Skills**: [`interface-design`, `frontend-ui-ux`]
|
||
|
||
**Parallelization**: Wave 6. Blocks: 25, 27. Blocked By: 6-11, 20.
|
||
|
||
**References**:
|
||
- `packages/chess/src/ui/GameView.tsx` — hover hook target
|
||
- `packages/chess/src/modifiers/registry.ts` — iterate to enumerate visible attrs
|
||
- Existing tooltip libs in project: grep for `floating-ui|Tooltip|@radix-ui`
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] Hover any modified piece → tooltip appears within 200ms
|
||
- [ ] Tooltip shows all active modifiers with correct source badges
|
||
- [ ] Unmodified piece → tooltip shows no modifier rows
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Hover modified piece shows tooltip
|
||
Tool: Playwright
|
||
Preconditions: Solo game started with profile granting HP +2 per-type on knights
|
||
Steps:
|
||
1. Hover white knight on b1
|
||
2. Wait for tooltip within 200ms timeout
|
||
3. Assert tooltip text contains "HP +2" and "(per-type)" source
|
||
4. Screenshot
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-24-hover.png
|
||
|
||
Scenario: Unmodified piece has empty modifier section
|
||
Tool: Playwright
|
||
Steps:
|
||
1. Hover pawn (no modifiers in profile)
|
||
2. Assert tooltip shows piece name but no modifier rows
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-24-empty.png
|
||
```
|
||
|
||
**Commit**: YES — `feat(ui): hover modifier tooltip`
|
||
- Files: `packages/chess/src/ui/ModifierTooltip.tsx`, `GameView.tsx` integration
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 25. **Pinned side panel**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/src/ui/ModifierPinnedPanel.tsx`:
|
||
- Click a piece (not hover) → panel pins on right side.
|
||
- Content: same as tooltip but more detailed (full descriptions, source chain for stacked modifiers).
|
||
- Reactive: subscribes to session fact changes — updates on hot-swap within 500ms.
|
||
- Pinned state persists across turns until user clicks X or clicks another piece (re-pins to new piece).
|
||
- Wire into GameView.
|
||
|
||
**Must NOT do**: Don't include change history log (T2). Don't support multiple pinned panels simultaneously.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `visual-engineering`
|
||
- **Skills**: [`interface-design`]
|
||
|
||
**Parallelization**: Wave 6. Blocks: 27. Blocked By: 24.
|
||
|
||
**References**:
|
||
- `packages/chess/src/ui/ModifierTooltip.tsx` (T24) — content source
|
||
- `packages/chess/src/ui/GameView.tsx` — panel mounting point
|
||
- Existing side panels in GameView for layout reference
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] Click piece → panel pins within 200ms
|
||
- [ ] Hot-swap updates pinned panel within 500ms
|
||
- [ ] Clicking X closes panel
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Click pins panel with matching content
|
||
Tool: Playwright
|
||
Steps:
|
||
1. Click b1 knight
|
||
2. Wait for [data-testid=modifier-pinned-panel] visible
|
||
3. Assert content matches what tooltip would show
|
||
4. Screenshot
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-25-pin.png
|
||
|
||
Scenario: Hot-swap updates pinned panel reactively
|
||
Tool: Playwright
|
||
Steps:
|
||
1. Pin panel on a modified piece (current: HP +2)
|
||
2. Trigger hot-swap via modifier-profile.update (new: HP +5)
|
||
3. Wait for panel text to update — assert within 500ms
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-25-hotswap.png
|
||
```
|
||
|
||
**Commit**: YES — `feat(ui): pinned modifier inspection panel`
|
||
- Files: `packages/chess/src/ui/ModifierPinnedPanel.tsx`, `GameView.tsx` integration
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 26. **Lobby profile picker integration**
|
||
|
||
**What to do**:
|
||
- Update `packages/chess/src/ui/Lobby.tsx`:
|
||
- Add Profile Picker next to Layout Picker.
|
||
- Dropdown of saved profiles from library + "None" + "Custom…" (opens editor).
|
||
- On Create Room: if profile selected, include in RoomCreatePayload.
|
||
- Reads `?modifierProfile*` URL param and pre-selects.
|
||
- Show active profile on GameView header similar to LayoutBadge (new `ModifierProfileBadge`).
|
||
|
||
**Must NOT do**: Don't auto-load "last used" profile unless user explicitly enables (T2 feature). Don't duplicate editor entry point — reuse Custom… flow.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-low` — mirror layout picker
|
||
- **Skills**: []
|
||
|
||
**Parallelization**: Wave 6. Blocks: 27. Blocked By: 19, 21, 22, 23.
|
||
|
||
**References**:
|
||
- `packages/chess/src/ui/Lobby.tsx` — LayoutPicker wiring pattern
|
||
- `packages/chess/src/ui/GameView.tsx:LayoutBadge` — badge pattern
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] Profile picker visible in lobby
|
||
- [ ] Creating room with profile: room state includes profile; GameView shows badge
|
||
- [ ] URL param pre-selects profile
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Create room with profile
|
||
Tool: Playwright
|
||
Steps:
|
||
1. In lobby, select layout=classic, profile="My HP Profile"
|
||
2. Click Create Room
|
||
3. Assert GameView shows ModifierProfileBadge with profile name
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-26-create.png
|
||
|
||
Scenario: URL pre-select
|
||
Tool: Playwright
|
||
Steps:
|
||
1. Visit /lobby?modifierProfileId=<id>
|
||
2. Assert picker shows selected profile
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-26-url.png
|
||
```
|
||
|
||
**Commit**: YES — `feat(ui): lobby profile picker integration`
|
||
- Files: `packages/chess/src/ui/Lobby.tsx`, `GameView.tsx` (badge addition)
|
||
- Pre-commit: `bun run check`
|
||
|
||
- [x] 27. **Full e2e Playwright suite**
|
||
|
||
**What to do**:
|
||
- Create `packages/chess/e2e/modifier-profiles.spec.ts` with 18 scenarios:
|
||
1. Editor opens from rules drawer within 1s
|
||
2. Esc closes editor
|
||
3. Create per-type HP bonus → save → reload from library
|
||
4. Create per-instance range bonus on b1 → save → reload
|
||
5. URL share roundtrip (with `?modifierProfile*` param)
|
||
6. Invalid profile (invuln on king) save blocked in editor with inline error
|
||
7. Solo game creates with profile; modifier visible in hover tooltip
|
||
8. Multiplayer: host creates room with profile, joiner sees same profile on join
|
||
9. Hot-swap mid-game: host updates profile at turn boundary, both clients see new values
|
||
10. Illegal hot-swap rejected by server with NACK; profile unchanged
|
||
11. Pinned panel click + content verification
|
||
12. Pinned panel updates reactively on hot-swap
|
||
13. Pawn with DirectionAdditions=["backward"] can move backward in game
|
||
14. Rook with RangeBonus=+1 can move 1 square further
|
||
15. Knight with CaptureFlags=CANNOT_BE_CAPTURED — enemy cannot target it
|
||
16. Pawn with PromotionOverride="bishop" auto-promotes to bishop (no prompt)
|
||
17. Piece with DamageResistance=0.5 takes half damage (test via HP preset + attack flow)
|
||
18. Profile library max-20 eviction test
|
||
- Run from repo root: `bunx playwright test e2e/modifier-profiles.spec.ts`.
|
||
|
||
**Must NOT do**: Don't add tests for T2/T3 features. Don't modify existing layout/multiplayer specs.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `unspecified-high` — thorough e2e authoring
|
||
- **Skills**: [`playwright`]
|
||
|
||
**Parallelization**: Wave 7. Blocks: 28. Blocked By: 19-26.
|
||
|
||
**References**:
|
||
- `packages/chess/e2e/layouts.spec.ts` — 24-scenario exemplar
|
||
- `packages/chess/e2e/multiplayer.spec.ts` — two-client test pattern
|
||
- `playwright.config.ts` (repo root)
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] All 18 scenarios pass: `bunx playwright test e2e/modifier-profiles.spec.ts` → 18/18 green
|
||
- [ ] Runs in < 90s total
|
||
- [ ] Evidence files for each scenario saved to `.sisyphus/evidence/piece-modifiers/e2e/`
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Full e2e suite passes
|
||
Tool: Bash (invoking Playwright)
|
||
Steps:
|
||
1. Run: bunx playwright test e2e/modifier-profiles.spec.ts --reporter=list
|
||
2. Assert: exit 0
|
||
3. Assert: 18 passed, 0 failed
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-27-e2e-results.txt
|
||
|
||
Scenario: Cross-browser spot check (chromium baseline)
|
||
Tool: Bash
|
||
Steps:
|
||
1. Run: bunx playwright test e2e/modifier-profiles.spec.ts --project=chromium
|
||
2. Assert exit 0
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-27-chromium.txt
|
||
```
|
||
|
||
**Commit**: YES — `test(e2e): modifier profiles full vertical slice`
|
||
- Files: `packages/chess/e2e/modifier-profiles.spec.ts`
|
||
- Pre-commit: `bun run check` + `bunx playwright test e2e/modifier-profiles.spec.ts`
|
||
|
||
- [x] 28. **ADR finalization + user docs**
|
||
|
||
**What to do**:
|
||
- Finalize `docs/adr/modifier-profiles.md` (T1): add "Implementation Retrospective" section noting any deviations or discoveries.
|
||
- Create `docs/user/modifier-profiles.md`:
|
||
- What are modifier profiles?
|
||
- How to open the editor
|
||
- Per-type vs per-instance (when to use each)
|
||
- Catalog of 6 T1 modifiers (one subsection each, with 1 example)
|
||
- Hot-swap semantics (turn-boundary, host-only in T1)
|
||
- Library save/load/share workflow
|
||
- In-play inspection (hover + pin)
|
||
- Known limitations (T1 scope; roadmap to T2/T3)
|
||
- Include screenshots from e2e evidence.
|
||
|
||
**Must NOT do**: Don't describe T2/T3 as if shipped. Don't promise features not in plan.
|
||
|
||
**Recommended Agent Profile**:
|
||
- **Category**: `writing`
|
||
- **Skills**: []
|
||
|
||
**Parallelization**: Wave 7. Blocks: F1. Blocked By: 1, 27.
|
||
|
||
**References**:
|
||
- `.sisyphus/evidence/piece-modifiers/` — screenshots
|
||
- `docs/adr/modifier-profiles.md` (T1)
|
||
- `docs/user/` — existing user docs pattern if present
|
||
|
||
**Acceptance Criteria**:
|
||
- [ ] Both files exist
|
||
- [ ] User doc references all 6 modifiers with at least 1 example each
|
||
- [ ] Screenshots embedded (verify files referenced exist)
|
||
|
||
**QA Scenarios**:
|
||
```
|
||
Scenario: Docs exist and reference all modifiers
|
||
Tool: Bash
|
||
Steps:
|
||
1. Assert: ls docs/adr/modifier-profiles.md docs/user/modifier-profiles.md (both exist)
|
||
2. Run: grep -c "hp-bonus\|range-bonus\|direction-additions\|capture-flags\|promotion-override\|damage-resistance" docs/user/modifier-profiles.md
|
||
3. Assert output ≥ 6
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-28-docs.txt
|
||
|
||
Scenario: Referenced screenshots exist
|
||
Tool: Bash
|
||
Steps:
|
||
1. Extract image refs from docs/user/modifier-profiles.md
|
||
2. For each, assert the file exists
|
||
Evidence: .sisyphus/evidence/piece-modifiers/task-28-images.txt
|
||
```
|
||
|
||
**Commit**: YES — `docs(user): modifier profiles user guide`
|
||
- Files: `docs/adr/modifier-profiles.md` (finalization), `docs/user/modifier-profiles.md`
|
||
- Pre-commit: none (docs only)
|
||
|
||
---
|
||
|
||
## Final Verification Wave (MANDATORY — after ALL implementation tasks)
|
||
|
||
- [x] F1. **Plan Compliance Audit** — `oracle`
|
||
Read this plan end-to-end. For each "Must Have": verify implementation exists (read file, run tests). For each "Must NOT Have": search codebase for forbidden patterns — reject with file:line if found. Check every ADR decision is reflected in code (WRAP semantics in transformMoveGenerator, layout-slot keying in per-instance, turn-boundary-only hot-swap, registry pattern, etc). Verify evidence files exist in `.sisyphus/evidence/piece-modifiers/`. Compare deliverables against plan.
|
||
Output: `Must Have [N/N] | Must NOT Have [N/N] | ADR Decisions [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT`
|
||
|
||
- [x] F2. **Code Quality Review** — `unspecified-high`
|
||
Run `bun run check` (typecheck + lint + vitest). Review all changed files for: `as any`/`@ts-ignore`, empty catches, `console.log` in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction, generic names (data/result/item/temp). Verify descriptors follow registry pattern (no hardcoded switch statements across 6+ files). Verify shared engine code between client/server (no drift).
|
||
Output: `Build [PASS/FAIL] | Lint [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | Pattern compliance [PASS/FAIL] | VERDICT`
|
||
|
||
- [x] F3. **Real Manual QA** — `unspecified-high` (+ `playwright` skill)
|
||
Start from clean state. Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence. Test cross-task integration: create profile with all 6 modifier kinds → save to library → URL-share → load in new tab → start room with profile + layout → play 3 turns using modified moves → hot-swap to different profile → verify reconciliation → hover/pin inspection throughout → game ends correctly. Test edge cases: empty profile, profile with orphan instance entries, illegal profile (king invuln), simultaneous swap, reconnect during swap.
|
||
Output: `Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT`
|
||
|
||
- [x] F4. **Scope Fidelity Check** — `deep`
|
||
For each task: read "What to do", read actual diff (git log/diff). Verify 1:1 — everything in spec was built (no missing), nothing beyond spec was built (no creep). Check "Must NOT do" compliance per task. Detect cross-task contamination: Task N touching Task M's files. Flag unaccounted changes. Verify T2/T3 features NOT smuggled into T1: no copy-paste, no diff UI, no DSL, no auras, no multi-profile stacking, no mid-turn swap.
|
||
Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | Scope creep [CLEAN/N issues] | VERDICT`
|
||
|
||
---
|
||
|
||
## Commit Strategy
|
||
|
||
Atomic commits per task. Each commit compiles + tests green. Use conventional commits.
|
||
|
||
1. `docs(adr): modifier-profiles architecture decisions`
|
||
2. `feat(engine): add 6 modifier attrs to ChessAttrMap`
|
||
3. `feat(engine): add transformMoveGenerator + modifyMoveAttrs preset hooks`
|
||
4. `feat(engine): modifier profile types`
|
||
5. `feat(engine): modifier registry pattern`
|
||
6-11. `feat(engine): {kind}-modifier descriptor with unit tests` (×6)
|
||
12. `feat(engine): ModifierProfile Zod schema + roundtrip tests`
|
||
13. `feat(engine): modifier-profile library persistence (v1)`
|
||
14. `feat(engine): apply profile at game start`
|
||
15. `feat(engine): hot-swap reconciliation`
|
||
16. `feat(engine): profile legality validator`
|
||
17. `feat(server): modifier profile protocol schemas + error codes`
|
||
18. `feat(ui): modifier profile editor shell + rules drawer entry`
|
||
19. `feat(server): room-create accepts profile`
|
||
20. `feat(server): modifier-profile.update WS handler`
|
||
21. `feat(ui): per-type modifier panel`
|
||
22. `feat(ui): per-instance modifier panel`
|
||
23. `feat(ui): profile library save/load + URL share`
|
||
24. `feat(ui): hover modifier tooltip`
|
||
25. `feat(ui): pinned modifier inspection panel`
|
||
26. `feat(ui): lobby profile picker integration`
|
||
27. `test(e2e): modifier profiles full vertical slice`
|
||
28. `docs(user): modifier profiles user guide`
|
||
|
||
## Success Criteria
|
||
|
||
### Verification Commands
|
||
```bash
|
||
bun run check # Expected: all green
|
||
bun run test packages/chess/src/modifiers/ # Expected: N pass, 0 fail
|
||
bun run test packages/server/src/profile-validator.test.ts # Expected: all pass
|
||
bunx playwright test e2e/modifier-profiles.spec.ts # Expected: 18/18 pass
|
||
```
|
||
|
||
### Final Checklist
|
||
- [ ] All 6 modifier descriptors registered and functional
|
||
- [ ] Per-type + per-instance both work end-to-end
|
||
- [ ] Hot-swap at turn boundary, validated server-side
|
||
- [ ] Editor reachable in ≤2 clicks from game
|
||
- [ ] Hover tooltip < 200ms, pinned panel < 500ms on update
|
||
- [ ] All QA scenario evidence files present
|
||
- [ ] ADR + user docs published
|
||
- [ ] `bun run check` green
|
||
- [ ] Playwright e2e 18/18 green
|
||
- [ ] No `as any`, no `@ts-ignore`, no pattern violations
|
||
- [ ] F1-F4 final-wave all APPROVE
|
||
- [ ] User explicit "okay"
|