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

13 KiB

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