brackt/plans/completed/bracket-expansion.md
Chris Parsons 35fe84a1dd
Update claude.md to push more tests. (#84)
* Update claude.md to push more tests.

* Archiving old plans.

* fix: run DB migrations programmatically on server startup

Replace unreliable drizzle-kit CLI migration with drizzle-orm's built-in
migrator running in the server process before accepting connections. This
ensures migrations are always applied on deploy and fails fast if they error.

- Add runMigrations() to server.ts using drizzle-orm/postgres-js/migrator
- Skip migrations in development (handled manually via db:migrate)
- Add DATABASE_URL guard with a clear error message
- Remove start:production script (now identical to start)
- Update Dockerfile CMD to use npm run start

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 22:31:04 -08:00

12 KiB

Bracket Expansion Plan

Problem Statement

Current bracket system only supports 4 and 8 team single-elimination brackets. We need to support:

  • Standard brackets: 4, 8, 16, 32 teams
  • March Madness: 68 teams with unique structure
    • First Four: 4 play-in games (8 teams)
    • Round of 64: 32 games (64 teams including First Four winners)
    • Round of 32, Sweet Sixteen, Elite Eight, Final Four, Championship
    • Only Elite Eight and beyond score fantasy points (per Q18)
    • Play-in teams can be any seeds (e.g., 11a vs 11b, 16a vs 16b), not just bottom seeds

Current Implementation

What we have:

  • generateSingleEliminationBracket(eventId, participantIds[])
  • Supports 4 or 8 teams only
  • Creates matches for all rounds automatically
  • Simple sequential seeding (1 vs 2, 3 vs 4, etc.)

Limitations:

  • Fixed bracket sizes (4, 8)
  • No play-in game support
  • No concept of "scoring rounds" vs "non-scoring rounds"
  • No flexible seeding/matchup configuration

Proposed Solution: Hybrid Approach

Create pre-defined bracket templates for common structures, with manual override capability.

Bracket Templates

interface BracketTemplate {
  id: string;
  name: string;
  totalTeams: number;
  rounds: BracketRound[];
  scoringStartsAtRound?: string; // e.g., "Elite Eight" for March Madness
}

interface BracketRound {
  name: string; // "First Four", "Round of 64", "Elite Eight", etc.
  matchCount: number;
  feedsInto?: string; // Which round winners advance to
  isScoring: boolean; // Does this round affect fantasy points?
}

// Example templates:
const BRACKET_TEMPLATES = {
  NCAA_68: {
    id: 'ncaa_68',
    name: 'NCAA March Madness (68 teams)',
    totalTeams: 68,
    scoringStartsAtRound: 'Elite Eight',
    rounds: [
      { name: 'First Four', matchCount: 4, feedsInto: 'Round of 64', isScoring: false },
      { name: 'Round of 64', matchCount: 32, feedsInto: 'Round of 32', isScoring: false },
      { name: 'Round of 32', matchCount: 16, feedsInto: 'Sweet Sixteen', isScoring: false },
      { name: 'Sweet Sixteen', matchCount: 8, feedsInto: 'Elite Eight', isScoring: false },
      { name: 'Elite Eight', matchCount: 4, feedsInto: 'Final Four', isScoring: true }, // 8 teams, share 5-8th
      { name: 'Final Four', matchCount: 2, feedsInto: 'Championship', isScoring: true }, // Losers share 3-4th
      { name: 'Championship', matchCount: 1, feedsInto: null, isScoring: true }, // Winner 1st, Loser 2nd
    ],
  },

  NFL_14: {
    id: 'nfl_14',
    name: 'NFL Playoffs (14 teams)',
    totalTeams: 14,
    scoringStartsAtRound: 'Wild Card',
    rounds: [
      { name: 'Wild Card', matchCount: 6, feedsInto: 'Divisional', isScoring: false }, // 2 teams get bye
      { name: 'Divisional', matchCount: 4, feedsInto: 'Conference Championship', isScoring: true }, // QF, share 5-8th
      { name: 'Conference Championship', matchCount: 2, feedsInto: 'Super Bowl', isScoring: true }, // SF, share 3-4th
      { name: 'Super Bowl', matchCount: 1, feedsInto: null, isScoring: true }, // Finals, 1st/2nd
    ],
  },

  NBA_16: {
    id: 'nba_16',
    name: 'NBA Playoffs (16 teams)',
    totalTeams: 16,
    scoringStartsAtRound: 'Conference Quarterfinals',
    rounds: [
      { name: 'First Round', matchCount: 8, feedsInto: 'Conference Semifinals', isScoring: false },
      { name: 'Conference Semifinals', matchCount: 4, feedsInto: 'Conference Finals', isScoring: true }, // QF, share 5-8th
      { name: 'Conference Finals', matchCount: 2, feedsInto: 'NBA Finals', isScoring: true }, // SF, share 3-4th
      { name: 'NBA Finals', matchCount: 1, feedsInto: null, isScoring: true }, // Finals, 1st/2nd
    ],
  },

  SIMPLE_16: {
    id: 'simple_16',
    name: 'Simple 16-Team Bracket',
    totalTeams: 16,
    scoringStartsAtRound: 'Quarterfinals',
    rounds: [
      { name: 'Round of 16', matchCount: 8, feedsInto: 'Quarterfinals', isScoring: false },
      { name: 'Quarterfinals', matchCount: 4, feedsInto: 'Semifinals', isScoring: true }, // Share 5-8th
      { name: 'Semifinals', matchCount: 2, feedsInto: 'Finals', isScoring: true }, // Share 3-4th
      { name: 'Finals', matchCount: 1, feedsInto: null, isScoring: true }, // 1st/2nd
    ],
  },

  SIMPLE_32: {
    id: 'simple_32',
    name: 'Simple 32-Team Bracket',
    totalTeams: 32,
    scoringStartsAtRound: 'Quarterfinals',
    rounds: [
      { name: 'Round of 32', matchCount: 16, feedsInto: 'Round of 16', isScoring: false },
      { name: 'Round of 16', matchCount: 8, feedsInto: 'Quarterfinals', isScoring: false },
      { name: 'Quarterfinals', matchCount: 4, feedsInto: 'Semifinals', isScoring: true },
      { name: 'Semifinals', matchCount: 2, feedsInto: 'Finals', isScoring: true },
      { name: 'Finals', matchCount: 1, feedsInto: null, isScoring: true },
    ],
  },
};

UI Flow

Step 1: Select Template

When creating a bracket event:
1. Admin selects bracket template from dropdown
2. Shows preview of rounds and scoring structure
3. Option to customize (add/remove rounds, change scoring start point)

Step 2: Assign Participants

For March Madness (68 teams):
- Show all 68 slots grouped by round
- First Four: 8 slots (4 matchups) - manual seeding (e.g., "11a vs 11b", "16a vs 16b")
- Round of 64: 60 slots (remaining teams) + 4 TBD (from First Four winners)
- Admin can assign any participant to any slot

For simpler brackets (4, 8, 16):
- Linear list of slots
- Drag-and-drop or dropdown selection
- Traditional seeding (1 vs 16, 2 vs 15, etc.)

Step 3: Generate Bracket

System creates:
- All playoff_matches records for all rounds
- Properly linked feedsInto relationships
- Marks which rounds contribute to scoring
- Sets initial participant assignments

Database Schema Additions

// Add to playoff_matches table
playoff_matches {
  // ... existing fields

  // NEW FIELDS:
  isScoring: boolean (default: true) // Does this match affect fantasy points?
  templateRound: varchar(50) // "First Four", "Round of 64", etc.
  seedInfo: varchar(50) // "1 vs 16", "11a vs 11b", etc. (for display)
}

// Add to scoring_events table
scoring_events {
  // ... existing fields

  // NEW FIELDS:
  bracketTemplateId: varchar(50) // "ncaa_68", "nfl_14", etc.
  scoringStartsAtRound: varchar(50) // Which round starts fantasy scoring
}

Scoring Logic Updates

/**
 * Process playoff event completion - updated for template system
 */
async function processPlayoffEvent(eventId: string) {
  const event = await getScoringEventById(eventId);
  const matches = await findPlayoffMatchesByEventId(eventId);

  // Filter to only scoring matches
  const scoringMatches = matches.filter(m => m.isScoring);

  // Determine which placements to assign based on templateRound
  // Elite Eight losers (4 teams) -> share 5-8th
  // Final Four losers (2 teams) -> share 3-4th
  // Championship loser (1 team) -> 2nd
  // Championship winner (1 team) -> 1st

  // For non-scoring rounds: assign 0 points (per Q20)
  const nonScoringLosers = matches
    .filter(m => !m.isScoring && m.loserId)
    .map(m => m.loserId);

  for (const loserId of nonScoringLosers) {
    await setParticipantResult(loserId, event.sportsSeasonId, null, 0);
  }
}

Admin UI Changes

Bracket Generation Page:

<Card>
  <CardHeader>
    <CardTitle>Select Bracket Template</CardTitle>
  </CardHeader>
  <CardContent>
    <Select name="templateId">
      <SelectItem value="simple_4">Simple 4-Team</SelectItem>
      <SelectItem value="simple_8">Simple 8-Team</SelectItem>
      <SelectItem value="simple_16">Simple 16-Team</SelectItem>
      <SelectItem value="nba_16">NBA Playoffs (16 teams)</SelectItem>
      <SelectItem value="nfl_14">NFL Playoffs (14 teams)</SelectItem>
      <SelectItem value="simple_32">Simple 32-Team</SelectItem>
      <SelectItem value="ncaa_68">March Madness (68 teams)</SelectItem>
    </Select>

    {/* Show template details when selected */}
    <div className="mt-4">
      <h4>Rounds:</h4>
      {selectedTemplate.rounds.map(round => (
        <div key={round.name}>
          {round.name} - {round.matchCount} matches
          {round.isScoring && <Badge>Scoring Round</Badge>}
        </div>
      ))}
    </div>
  </CardContent>
</Card>

<Card>
  <CardHeader>
    <CardTitle>Assign Participants ({templateTeamCount} slots)</CardTitle>
  </CardHeader>
  <CardContent>
    {/* For March Madness: group by round */}
    {template.id === 'ncaa_68' ? (
      <>
        <h4>First Four (Play-in Games)</h4>
        {/* 4 matchups, 8 slots with seed labels */}
        <MatchupSelector
          matchup={1}
          label1="11a vs"
          label2="11b"
          participants={participants}
        />

        <h4>Round of 64</h4>
        {/* 28 direct assignments + 4 TBD from First Four */}
        <SlotSelector
          slots={60}
          tbdSlots={4}
          participants={participants}
        />
      </>
    ) : (
      /* Simple linear assignment for other brackets */
      <LinearSlotSelector
        slots={templateTeamCount}
        participants={participants}
      />
    )}
  </CardContent>
</Card>

Implementation Phases

Phase 2.6: Template System Foundation

  • Define bracket template type and constants
  • Add isScoring and templateRound to playoff_matches schema
  • Add bracketTemplateId and scoringStartsAtRound to scoring_events
  • Create database migration
  • Update playoff-match model to support templates

Phase 2.7: Simple Template Expansion (4, 8, 16, 32)

  • Create template definitions for 4, 8, 16, 32 team brackets
  • Update generateBracketFromTemplate() function
  • Update bracket generation UI to use template selector
  • Test with 16-team and 32-team brackets
  • Update scoring calculator for non-scoring rounds (0 points)

Phase 2.8: March Madness Template (68 teams)

  • Create NCAA_68 template definition
  • Build First Four matchup assignment UI
  • Build Round of 64 with TBD slot handling
  • Implement winner advancement from First Four to Round of 64
  • Test complete 68-team bracket flow
  • Verify Elite Eight scoring starts correctly

Phase 2.9: NFL/NBA Templates

  • Create NFL_14 template (with bye weeks)
  • Create NBA_16 template
  • Handle bye week logic (teams skip first round)
  • Test both templates

Alternative Considered: Manual Match Creation

Pros:

  • Maximum flexibility
  • Works for any structure

Cons:

  • Too complex for admins
  • Error-prone (easy to miss connections)
  • No validation of bracket structure
  • Time-consuming for large brackets

Verdict: Not recommended. Templates provide better UX with flexibility where needed.

Open Questions

  1. Custom Templates: Should admins be able to save custom bracket templates for reuse?

    • Recommendation: Not in initial implementation. Add if requested.
  2. Seed Validation: Should system validate proper bracket structure (e.g., 1 seed plays 16 seed)?

    • Recommendation: No. Allow flexible seeding for play-ins and special cases.
  3. Series Support: Should we support best-of-7 series (NBA/NHL)?

    • Recommendation: Not initially. Just track series winner for now.
  4. Bracket Visualization: Show traditional bracket tree diagram?

    • Recommendation: Phase 3 enhancement. Current table view sufficient for Phase 2.

Success Criteria

Admin can create 4, 8, 16, 32 team brackets Admin can create March Madness 68-team bracket with First Four Admin can assign any participant to any slot (flexible seeding) System correctly identifies which rounds score fantasy points Participants eliminated in non-scoring rounds get 0 points Elite Eight and beyond calculate placements correctly Winner advancement works across all bracket sizes