felt/.planning/phases/01-tournament-engine/01-PLAN-E.md
Mikkel Georgsen 21ff95068e docs(01): create Phase 1 plans (A-N) with research and feedback
14 plans in 6 waves covering all 68 requirements for the Tournament
Engine phase. Includes research (go-libsql, NATS JetStream, Svelte 5
runes, ICM complexity), plan verification (2 iterations), and user
feedback (hand-for-hand UX, SEAT-06 reword, re-entry semantics,
integration test, DKK defaults, JWT 7-day expiry, clock tap safety).

Wave structure:
  1: A (scaffold), B (schema)
  2: C (auth/audit), D (clock), E (templates), J (frontend scaffold)
  3: F (financial), H (seating), M (layout shell)
  4: G (player management)
  5: I (tournament lifecycle)
  6: K (overview/financials), L (players), N (tables/more)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 02:58:22 +01:00

252 lines
13 KiB
Markdown

# Plan E: Blind Structure + Chip Sets + Templates
---
wave: 2
depends_on: [01-PLAN-A, 01-PLAN-B]
files_modified:
- internal/blind/structure.go
- internal/blind/wizard.go
- internal/blind/templates.go
- internal/template/chipset.go
- internal/template/payout.go
- internal/template/buyin.go
- internal/template/tournament.go
- internal/server/routes/templates.go
- internal/blind/wizard_test.go
- internal/template/tournament_test.go
autonomous: true
requirements: [BLIND-01, BLIND-02, BLIND-03, BLIND-04, BLIND-05, BLIND-06, CHIP-01, CHIP-02, CHIP-03, CHIP-04, FIN-01, FIN-02, FIN-05, FIN-06, FIN-10]
---
## Goal
All reusable building blocks — chip sets, blind structures, payout structures, buy-in configs — have full CRUD with API endpoints. Built-in templates (Turbo, Standard, Deep Stack, WSOP-style) ship as seed data. The structure wizard generates a blind structure from inputs (player count, starting chips, duration, denominations). Tournament templates compose these building blocks. Template management is a dedicated area.
## Context
- **Templates are compositions of building blocks** — not monolithic configs (CONTEXT.md locked decision)
- **Building blocks are venue-level** — shared across tournaments
- **Local changes by default** — tournament gets a copy, edits don't affect the template
- **Structure wizard** lives in template management (CONTEXT.md locked decision)
- **Built-in templates** ship with the app (BLIND-05)
- **Big Blind Ante** support alongside standard ante (BLIND-02)
- **Mixed game rotation** (HORSE, 8-Game) via game type per level (BLIND-03)
- See 01-RESEARCH.md for blind wizard algorithm description
## User Decisions (from CONTEXT.md)
- **Template-first creation** — TD picks a template, everything pre-fills, tweak for tonight, Start
- **Building blocks feel like LEGO** — pick chip set, pick blind structure, pick payout table, name it, done
- **Dedicated template management area** — create from scratch, duplicate/edit existing, save tournament config as new template
- **Entry count = unique entries only** — for payout bracket selection
- **Prize rounding** — round down to nearest venue-configured denomination
## Tasks
<task id="E1" title="Implement building block CRUD: chip sets, blind structures, payout structures, buy-in configs">
**1. Chip Set Service** (`internal/template/chipset.go`):
- `ChipSet` struct: ID, Name, IsBuiltin, CreatedAt, UpdatedAt
- `ChipDenomination` struct: ID, ChipSetID, Value (int64 cents), ColorHex, Label, SortOrder
- CRUD operations:
- `CreateChipSet(ctx, name string, denominations []ChipDenomination) (*ChipSet, error)`
- `GetChipSet(ctx, id string) (*ChipSet, error)` — includes denominations
- `ListChipSets(ctx) ([]ChipSet, error)`
- `UpdateChipSet(ctx, id, name string, denominations []ChipDenomination) error`
- `DeleteChipSet(ctx, id string) error` — fail if referenced by active tournament
- `DuplicateChipSet(ctx, id string, newName string) (*ChipSet, error)`
- Built-in chip sets cannot be deleted (is_builtin = true), but can be duplicated
**2. Blind Structure Service** (`internal/blind/structure.go`):
- `BlindStructure` struct: ID, Name, IsBuiltin, GameTypeDefault, Notes, CreatedAt, UpdatedAt
- `BlindLevel` struct per BLIND-01:
- Position, LevelType (round/break), GameType
- SmallBlind, BigBlind, Ante, BBAnte (all int64) — BLIND-02
- DurationSeconds, ChipUpDenominationValue (*int64 nullable) — CHIP-02
- Notes
- CRUD operations:
- `CreateStructure(ctx, name string, levels []BlindLevel) (*BlindStructure, error)`
- `GetStructure(ctx, id string) (*BlindStructure, error)` — includes levels ordered by position
- `ListStructures(ctx) ([]BlindStructure, error)`
- `UpdateStructure(ctx, id string, name string, levels []BlindLevel) error`
- `DeleteStructure(ctx, id string) error`
- `DuplicateStructure(ctx, id string, newName string) (*BlindStructure, error)`
- Validation:
- At least one round level required
- Small blind < big blind
- Duration > 0 for all levels
- Positions must be contiguous starting from 0
**3. Payout Structure Service** (`internal/template/payout.go`):
- `PayoutStructure` struct: ID, Name, IsBuiltin, CreatedAt, UpdatedAt
- `PayoutBracket` struct: ID, StructureID, MinEntries, MaxEntries
- `PayoutTier` struct: ID, BracketID, Position, PercentageBasisPoints (int64, 5000 = 50.00%)
- CRUD with brackets and tiers as nested entities
- Validation:
- Brackets must cover contiguous ranges (no gaps)
- Tier percentages per bracket must sum to exactly 10000 (100.00%)
- At least one bracket required
**4. Buy-in Config Service** (`internal/template/buyin.go`):
- `BuyinConfig` struct with all fields from schema: buyin amount, starting chips, rake, bounty, rebuy config, addon config, reentry config, late reg config
- `RakeSplit` struct: Category (house/staff/league/season_reserve), Amount (int64 cents)
- CRUD operations
- Validation:
- Rake splits sum must equal rake_total
- All amounts must be non-negative
- Rebuy/addon limits must be non-negative
- If bounty_amount > 0, bounty_chip must be > 0
**5. API Routes** (`internal/server/routes/templates.go`):
All routes require auth. Create/update/delete require admin role. List/get are floor+ accessible.
Chip Sets:
- `GET /api/v1/chip-sets` — list all
- `GET /api/v1/chip-sets/{id}` — get with denominations
- `POST /api/v1/chip-sets` — create
- `PUT /api/v1/chip-sets/{id}` — update
- `DELETE /api/v1/chip-sets/{id}` — delete
- `POST /api/v1/chip-sets/{id}/duplicate` — duplicate
Blind Structures:
- `GET /api/v1/blind-structures` — list all
- `GET /api/v1/blind-structures/{id}` — get with levels
- `POST /api/v1/blind-structures` — create
- `PUT /api/v1/blind-structures/{id}` — update
- `DELETE /api/v1/blind-structures/{id}` — delete
- `POST /api/v1/blind-structures/{id}/duplicate` — duplicate
Payout Structures:
- `GET /api/v1/payout-structures` — list all
- `GET /api/v1/payout-structures/{id}` — get with brackets and tiers
- `POST /api/v1/payout-structures` — create
- `PUT /api/v1/payout-structures/{id}` — update
- `DELETE /api/v1/payout-structures/{id}` — delete
- `POST /api/v1/payout-structures/{id}/duplicate` — duplicate
Buy-in Configs:
- `GET /api/v1/buyin-configs` — list all
- `GET /api/v1/buyin-configs/{id}` — get
- `POST /api/v1/buyin-configs` — create
- `PUT /api/v1/buyin-configs/{id}` — update
- `DELETE /api/v1/buyin-configs/{id}` — delete
- `POST /api/v1/buyin-configs/{id}/duplicate` — duplicate
All mutations record audit entries.
**Verification:**
- Full CRUD cycle for each building block type via curl
- Validation rejects invalid inputs (e.g., payout tiers not summing to 100%)
- Built-in items cannot be deleted
- Duplicate creates independent copy
</task>
<task id="E2" title="Implement tournament templates, built-in seed data, and structure wizard">
**1. Tournament Template Service** (`internal/template/tournament.go`):
- `TournamentTemplate` struct: ID, Name, Description, ChipSetID, BlindStructureID, PayoutStructureID, BuyinConfigID, PointsFormulaID (nullable), MinPlayers, MaxPlayers, EarlySignupBonusChips, EarlySignupCutoff, PunctualityBonusChips, IsPKO, IsBuiltin, CreatedAt, UpdatedAt
- CRUD operations:
- `CreateTemplate(ctx, template TournamentTemplate) (*TournamentTemplate, error)` — validates all FK references exist
- `GetTemplate(ctx, id string) (*TournamentTemplate, error)` — returns template with populated building block summaries (names, not full data)
- `GetTemplateExpanded(ctx, id string) (*ExpandedTemplate, error)` — returns template with ALL building block data (for tournament creation)
- `ListTemplates(ctx) ([]TournamentTemplate, error)`
- `UpdateTemplate(ctx, template TournamentTemplate) error`
- `DeleteTemplate(ctx, id string) error`
- `DuplicateTemplate(ctx, id string, newName string) (*TournamentTemplate, error)`
- `SaveAsTemplate(ctx, tournamentID string, name string) (*TournamentTemplate, error)` — creates a new template from a tournament's current config
- API Routes:
- `GET /api/v1/tournament-templates` — list all
- `GET /api/v1/tournament-templates/{id}` — get with building block summaries
- `GET /api/v1/tournament-templates/{id}/expanded` — get with full building block data
- `POST /api/v1/tournament-templates` — create
- `PUT /api/v1/tournament-templates/{id}` — update
- `DELETE /api/v1/tournament-templates/{id}` — delete
- `POST /api/v1/tournament-templates/{id}/duplicate` — duplicate
**2. Built-in Seed Data** (`internal/blind/templates.go`):
Create built-in templates that ship with the app. Add to seed migration or boot logic (skip if already exist).
Built-in Blind Structures:
- **Turbo** (~2hr for 20 players): 15-minute levels, aggressive blind jumps, 1 break
- Levels: 25/50, 50/100, 75/150, 100/200, break, 150/300, 200/400, 300/600, 400/800, break, 600/1200, 800/1600, 1000/2000, 1500/3000, 2000/4000
- Starting chips: 10,000
- **Standard** (~3-4hr for 20 players): 20-minute levels, moderate progression, 2 breaks
- Levels: 25/50, 50/100, 75/150, 100/200, 150/300, break, 200/400, 300/600, 400/800, 500/1000, break, 600/1200, 800/1600, 1000/2000, 1500/3000, 2000/4000, 3000/6000
- Starting chips: 15,000
- **Deep Stack** (~5-6hr for 20 players): 30-minute levels, slow progression, 3 breaks
- Starting chips: 25,000, wider level range
- **WSOP-style**: 60-minute levels, with antes starting at level 4, BB ante option
- Starting chips: 50,000, slow progression
Built-in Payout Structures:
- **Standard**: 8-20 entries (3 prizes: 50/30/20), 21-30 (4 prizes: 45/26/17/12), 31-40 (5 prizes), 41+ (6 prizes)
Built-in Tournament Templates (compose the above):
- Turbo template (Turbo blinds + Standard payout + default chip set + basic buy-in)
- Standard template
- Deep Stack template
- WSOP-style template
Each built-in has `is_builtin = true` — cannot be deleted, but can be duplicated.
**3. Structure Wizard** (`internal/blind/wizard.go`):
Algorithm to generate a blind structure from inputs:
- Inputs: `playerCount int`, `startingChips int64`, `targetDurationMinutes int`, `chipSetID string` (for denomination alignment)
- Algorithm (from 01-RESEARCH.md):
1. Calculate target number of levels: `targetDuration / levelDuration` (default 20-minute levels)
2. Calculate target final big blind: `startingChips * playerCount / 10` (roughly — at the end, average stack = 10 BB)
3. Calculate geometric progression ratio: `(finalBB / initialBB)^(1/numLevels)`
4. Generate levels with geometric blind progression
5. Snap each blind to nearest chip denomination from the chip set
6. Ensure SB = BB/2 (or closest denomination)
7. Add antes starting at ~level 4-5 (standard is ante = BB at higher levels)
8. Insert breaks every 4-5 levels (10-minute breaks)
9. Mark chip-up breaks when lower denominations are no longer needed
- Output: `[]BlindLevel` ready to save as a blind structure
- API Route:
- `POST /api/v1/blind-structures/wizard` — body: `{"player_count": 20, "starting_chips": 15000, "target_duration_minutes": 240, "chip_set_id": "..."}`
- Response: generated `[]BlindLevel` (NOT saved — preview only, TD can then save)
**4. Tests:**
- `internal/blind/wizard_test.go`:
- Test wizard generates sensible structure for various inputs (10, 20, 40, 80 players)
- Test blind values align with chip denominations
- Test breaks are inserted at reasonable intervals
- Test generated structure has increasing blinds
- Test edge case: very short tournament (1 hour), very long tournament (8 hours)
- `internal/template/tournament_test.go`:
- Test template creation with valid FK references
- Test template creation with invalid FK reference returns error
- Test SaveAsTemplate from running tournament
- Test GetTemplateExpanded returns all building block data
**Verification:**
- All 4 built-in templates exist after first startup
- Wizard generates a blind structure from sample inputs
- Generated blind values align with chip denominations
- Full CRUD for tournament templates works
- Template expanded endpoint returns complete building block data
</task>
## Verification Criteria
1. Unlimited configurable levels with all fields (round/break, game type, SB/BB, ante, BB ante, duration, chip-up, notes)
2. Big Blind Ante field exists alongside standard ante
3. Mixed game rotation via game_type per level
4. Blind structures can be saved/loaded as reusable templates
5. 4 built-in blind structures + 4 built-in tournament templates exist on first boot
6. Structure wizard produces a playable structure from inputs
7. Chip sets with denominations, colors, and values fully manageable
8. Chip-up tracking via chip_up_denomination field per level
9. Payout structures with entry-count brackets work correctly
10. All building blocks compose into tournament templates
## Must-Haves (Goal-Backward)
- [ ] Building blocks are independent reusable entities (not embedded in templates)
- [ ] Templates compose building blocks by reference (LEGO pattern)
- [ ] Built-in templates ship with the app and cannot be deleted
- [ ] Structure wizard generates playable blind structures from inputs
- [ ] Big Blind Ante and mixed game rotation are supported in the level definition
- [ ] Payout structure tiers always sum to exactly 100% per bracket
- [ ] Chip denominations have colors for display rendering