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>
252 lines
13 KiB
Markdown
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
|