# 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 **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 **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 ## 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