felt/.planning/phases/01-tournament-engine/01-PLAN-I.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

16 KiB

Plan I: Tournament Lifecycle + Multi-Tournament + Chop/Deal


wave: 5 depends_on: [01-PLAN-D, 01-PLAN-F, 01-PLAN-G, 01-PLAN-H] files_modified:

  • internal/tournament/tournament.go
  • internal/tournament/state.go
  • internal/tournament/multi.go
  • internal/financial/icm.go
  • internal/financial/chop.go
  • internal/server/routes/tournaments.go
  • internal/financial/icm_test.go
  • internal/financial/chop_test.go
  • internal/tournament/tournament_test.go
  • internal/tournament/integration_test.go autonomous: true requirements: [MULTI-01, MULTI-02, FIN-11, SEAT-06]

Goal

The tournament lifecycle — create from template, configure, start, run, and close — works end-to-end. Multiple simultaneous tournaments run with fully independent state (clocks, financials, players, tables). A tournament lobby shows all active tournaments. Chop/deal support (ICM, chip-chop, even-chop, custom) handles end-game scenarios. The tap-tap seat move flow from the seating engine is wired into the full tournament context.

Context

  • Clock engine — Plan D (running, independent per tournament)
  • Financial engine — Plan F (transactions, prize pool, payouts)
  • Player management — Plan G (buy-in, bust-out, rankings)
  • Seating engine — Plan H (tables, balancing, break table)
  • Template-first creation — CONTEXT.md: TD picks template, everything pre-fills
  • Tournament auto-closes when one player remains (CONTEXT.md)
  • ICM calculator: Malmuth-Harville exact for <=10 players, Monte Carlo for 11+ (01-RESEARCH.md Pitfall 3)

User Decisions (from CONTEXT.md)

  • Template-first creation — pick template → pre-fill → tweak → Start
  • Local changes by default — tournament gets a copy of building blocks
  • Tournament auto-closes when one player remains — no manual "end tournament" button
  • Multi-tournament switching — tabs at top (phone) or split view (tablet landscape)
  • Flexible chop/deal support — ICM, custom split, partial chop, any number of players
  • Prize money and league positions are independent — money can be chopped but positions determined by play
  • Minimum player threshold — Start button unavailable until met

Tasks

**1. Tournament Service** (`internal/tournament/tournament.go`): - `TournamentService` struct with db, clock registry, financial engine, player service, seating service, audit trail, hub
  • CreateFromTemplate(ctx, templateID string, overrides TournamentOverrides) (*Tournament, error):

    • Load expanded template (all building blocks)
    • Create tournament record with status "created"
    • Copy all building block references (chip_set_id, blind_structure_id, etc.) — local copy semantics
    • Apply overrides (name, min/max players, bonuses, PKO flag, etc.)
    • Create tables from blueprint if specified, or empty table set
    • Record audit entry
    • Broadcast tournament.created event
    • Return tournament
  • CreateManual(ctx, config TournamentConfig) (*Tournament, error):

    • Create without a template — manually specify all config
    • Same flow but no template reference
  • GetTournament(ctx, id string) (*TournamentDetail, error):

    • Return full tournament state:
      type TournamentDetail struct {
          Tournament      Tournament
          ClockSnapshot   *ClockSnapshot
          Tables          []TableDetail
          Players         PlayerSummary  // counts: registered, active, busted
          PrizePool       PrizePoolSummary
          Rankings        []PlayerRanking
          RecentActivity  []AuditEntry   // last 20 audit entries
          BalanceStatus   *BalanceStatus
      }
      
  • StartTournament(ctx, id string) error:

    • Validate minimum players met
    • Validate at least one table exists with seats
    • Set status to "registering" → "running"
    • Start the clock engine (loads blind structure, starts countdown)
    • Set started_at timestamp
    • Record audit entry
    • Broadcast tournament.started event
  • PauseTournament(ctx, id string) error:

    • Pause the clock
    • Set status to "paused"
    • Record audit entry, broadcast
  • ResumeTournament(ctx, id string) error:

    • Resume the clock
    • Set status to "running"
    • Record audit entry, broadcast
  • EndTournament(ctx, id string) error:

    • Called automatically when 1 player remains, or manually for deals
    • Stop the clock
    • Assign finishing positions to remaining active players
    • Calculate and apply final payouts
    • Set status to "completed", ended_at timestamp
    • Record audit entry
    • Broadcast tournament.ended event
  • CancelTournament(ctx, id string) error:

    • Set status to "cancelled"
    • Stop clock if running
    • Void all pending transactions (mark as cancelled, not deleted)
    • Record audit entry, broadcast
  • CheckAutoClose(ctx, id string) error:

    • Called after every bust-out (from player management)
    • Count remaining active players
    • If 1 remaining: auto-close tournament
    • If 0 remaining (edge case): cancel tournament

2. Tournament State Aggregation (internal/tournament/state.go):

  • GetTournamentState(ctx, id string) (*TournamentState, error):

    • Aggregates all state for WebSocket snapshot:
      • Clock state (from clock engine)
      • Player counts (registered, active, busted)
      • Table states (all tables with seats)
      • Financial summary (prize pool, entries, rebuys)
      • Rankings (current)
      • Balance status
    • This is what new WebSocket connections receive (replaces the stub from Plan A)
    • Sent as a single JSON message on connect
  • BuildActivityFeed(ctx, tournamentID string, limit int) ([]ActivityEntry, error):

    • Convert recent audit entries into human-readable activity items:
      type ActivityEntry struct {
          Timestamp   int64  `json:"timestamp"`
          Type        string `json:"type"`      // "bust", "buyin", "rebuy", "clock", "seat", etc.
          Title       string `json:"title"`      // "John Smith busted by Jane Doe"
          Description string `json:"description"` // "Table 1, Seat 4 → 12th place"
          Icon        string `json:"icon"`       // For frontend rendering
      }
      

3. Multi-Tournament Manager (internal/tournament/multi.go) — MULTI-01:

  • MultiManager struct — manages multiple tournament services (or uses tournament-scoped queries)

  • ListActiveTournaments(ctx) ([]TournamentSummary, error) — MULTI-02:

    • Return all tournaments with status in (registering, running, paused, final_table)
    • Summary: ID, name, status, player count, current level, blind, remaining time
    • This powers the tournament lobby view
  • GetTournamentSummary(ctx, id string) (*TournamentSummary, error):

    • Lightweight summary for lobby display
  • Independence guarantee: Every piece of state (clock, players, tables, financials) is scoped by tournament_id. No global singletons. Multiple tournaments run simultaneously with zero interference.

4. API Routes (internal/server/routes/tournaments.go):

  • POST /api/v1/tournaments — create from template: body: {"template_id": "...", "name": "Friday Night Turbo", "overrides": {...}}
  • POST /api/v1/tournaments/manual — create without template
  • GET /api/v1/tournaments — list all (active and recent)
  • GET /api/v1/tournaments/active — active only (lobby, MULTI-02)
  • GET /api/v1/tournaments/{id} — full tournament state
  • GET /api/v1/tournaments/{id}/state — WebSocket-compatible full state snapshot
  • POST /api/v1/tournaments/{id}/start — start tournament
  • POST /api/v1/tournaments/{id}/pause — pause
  • POST /api/v1/tournaments/{id}/resume — resume
  • POST /api/v1/tournaments/{id}/cancel — cancel
  • GET /api/v1/tournaments/{id}/activity — recent activity feed

5. WebSocket Integration:

  • Update the WebSocket hub (Plan A) to send full tournament state on connect
  • Messages are typed: {type: "clock.tick", data: {...}}, {type: "player.bust", data: {...}}, etc.
  • Client subscribes to a specific tournament by sending {type: "subscribe", tournament_id: "..."}
  • Lobby clients subscribe without tournament_id to receive all tournament summaries

Verification:

  • Create tournament from template with all config pre-filled
  • Start tournament after minimum players met
  • Multiple tournaments run simultaneously with independent state
  • Tournament auto-closes when 1 player remains
  • Lobby shows all active tournaments
  • WebSocket sends full state on connect
**1. ICM Calculator** (`internal/financial/icm.go`) — FIN-11: - `Malmuth-Harville Algorithm` (exact, for <= 10 players): - Calculate each player's equity in the prize pool based on chip stacks - Recursively calculate probability of each player finishing in each position - `CalculateICMExact(stacks []int64, payouts []int64) ([]int64, error)`: - Input: chip stacks (int64) and payout amounts (int64 cents) - Output: ICM value for each player (int64 cents) - Algorithm: for each player, calculate P(finish in each position) using chip-proportion recursion - Sum P(position) * payout(position) for all positions
  • Monte Carlo ICM (approximate, for 11+ players):

    • CalculateICMMonteCarlo(stacks []int64, payouts []int64, iterations int) ([]int64, error):
      • Default iterations: 100,000 (converges to <0.1% error per 01-RESEARCH.md)
      • Simulate tournament outcomes based on chip probabilities
      • For each iteration: randomly determine finishing order weighted by chip stacks
      • Average results across all iterations
      • Return ICM values (int64 cents)
  • CalculateICM(stacks []int64, payouts []int64) ([]int64, error):

    • Dispatcher: use exact if <= 10 players, Monte Carlo otherwise
    • Validate inputs: stacks and payouts must be non-empty, all positive
    • ICM values must sum to total prize pool (validate before returning)

2. Chop/Deal Engine (internal/financial/chop.go):

  • ChopEngine struct with financial engine, audit trail

  • ProposeDeal(ctx, tournamentID string, dealType string, params DealParams) (*DealProposal, error):

    • Deal types:
      • ICM: TD inputs chip stacks → system calculates ICM values → shows proposed payouts
      • Chip Chop: Divide pool proportionally by chip count (simpler than ICM)
      • Even Chop: Equal split among all remaining players
      • Custom: TD enters specific amounts per player
      • Partial Chop: Split some money, keep remainder + points in play (CONTEXT.md: "split some money, play on for remaining + league points")
    • DealParams:
      type DealParams struct {
          PlayerStacks   map[string]int64 // playerID -> chip count (for ICM/chip chop)
          CustomAmounts  map[string]int64 // playerID -> custom payout (for custom)
          PartialPool    int64            // Amount to split (for partial chop)
          RemainingPool  int64            // Amount left in play (for partial chop)
      }
      
    • Returns proposal for all remaining players with calculated payouts:
      type DealProposal struct {
          ID            string
          TournamentID  string
          DealType      string
          Payouts       []DealPayout // {PlayerID, PlayerName, Amount, ChipStack, ICMValue}
          TotalAmount   int64        // Must equal prize pool (or partial pool for partial chop)
          IsPartial     bool
          RemainingPool int64        // If partial, what's still in play
          CreatedAt     int64
      }
      
    • Validate: sum of proposed payouts == total pool (or partial pool)
    • Does NOT apply yet — returns proposal for TD approval
  • ConfirmDeal(ctx, tournamentID, proposalID string) error:

    • Apply all payouts as transactions
    • If full chop: set all players' status to "deal", assign finishing positions
    • If partial chop: apply partial payouts, tournament continues with remaining pool
    • Prize money and league positions are independent (CONTEXT.md): positions for league points are based on chip counts at deal time (or play on for partial chop)
    • Record audit entry with full deal details
    • If full chop: end tournament (set status "completed")
    • Broadcast
  • CancelDeal(ctx, tournamentID, proposalID string) error

3. API Routes (add to tournaments.go):

  • POST /api/v1/tournaments/{id}/deal/propose — body: {"type": "icm", "stacks": {"player1": 50000, "player2": 30000}}
  • GET /api/v1/tournaments/{id}/deal/proposals — list proposals
  • POST /api/v1/tournaments/{id}/deal/proposals/{proposalId}/confirm — confirm deal
  • POST /api/v1/tournaments/{id}/deal/proposals/{proposalId}/cancel — cancel

4. Tests:

  • internal/financial/icm_test.go:

    • Test ICM exact with 2 players: known expected values
    • Test ICM exact with 3 players: verify against known ICM tables
    • Test ICM exact with 5 players: verify sum equals prize pool
    • Test ICM Monte Carlo with 15 players: verify result is within 1% of expected (statistical test)
    • Test ICM with equal stacks: all players should get approximately equal ICM values
    • Test ICM edge case: one player has 99% of chips
    • Test performance: ICM for 10 players completes in < 1 second
    • Test performance: Monte Carlo ICM for 20 players completes in < 2 seconds
  • internal/financial/chop_test.go:

    • Test chip chop: payouts proportional to chip stacks
    • Test even chop: all players get equal amounts
    • Test custom split: arbitrary amounts summing to pool
    • Test partial chop: partial payouts + remaining pool for continuation
    • Test deal sets finishing positions correctly
    • Test full chop ends tournament
  • internal/tournament/tournament_test.go:

    • Test create from template pre-fills all config
    • Test start requires minimum players
    • Test auto-close when 1 player remains
    • Test multiple tournaments run independently
    • Test tournament state aggregation includes all components
  • internal/tournament/integration_test.goEnd-to-end tournament lifecycle test:

    • Create tournament from template (standard 10-player, PKO)
    • Register 10 players with buy-ins (verify prize pool, rake, seat assignments)
    • Start tournament, advance clock through 3 levels
    • Process 2 rebuys, 1 add-on (verify prize pool update, transaction log)
    • Bust 5 players with bounty transfers (verify rankings, hitman chain, balance check)
    • Undo 1 bust → verify re-ranking of all subsequent positions
    • Run table balancing (verify suggestion → accept → move)
    • Break a table (verify even redistribution)
    • Propose and confirm ICM deal with remaining 5 players
    • Final assertions:
      • Sum of all payouts == prize pool (int64, zero deviation)
      • All 10 players have correct finishing positions
      • Audit trail contains every state-changing action
      • All transactions are accounted for (buyin + rebuy + addon = contributions, payouts + rake = disbursements)
      • Tournament status is "completed"

Verification:

  • ICM calculation produces correct values for known test cases
  • All chop types produce valid payouts summing to prize pool
  • Partial chop allows tournament to continue
  • Full deal ends the tournament and assigns positions
  • Multiple tournaments run simultaneously without interference
  • Tournament lobby shows all active tournaments

Verification Criteria

  1. Tournament creation from template pre-fills all configuration
  2. Tournament start requires minimum players met
  3. Tournament auto-closes when one player remains
  4. Multiple simultaneous tournaments with independent clocks, financials, and players
  5. Tournament lobby shows all active tournaments (MULTI-02)
  6. ICM calculator works for 2-20+ players (exact for <=10, Monte Carlo for 11+)
  7. All chop types (ICM, chip-chop, even-chop, custom, partial) work correctly
  8. Prize money and league positions are independent in deal scenarios
  9. Full tournament state sent on WebSocket connect
  10. Activity feed shows recent actions in human-readable form

Must-Haves (Goal-Backward)

  • Template-first tournament creation with local copy semantics
  • Tournament auto-closes when one player remains
  • Multiple simultaneous tournaments with fully independent state
  • Tournament lobby view for multi-tournament overview
  • ICM calculator (exact <=10, Monte Carlo 11+) produces correct values
  • Chop/deal support (ICM, chip-chop, even-chop, custom, partial)
  • Prize money and league positions independent
  • Full tournament state available on WebSocket connect