brackt/plans/bracket-expansion.md

346 lines
12 KiB
Markdown
Raw Normal View History

# 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
### Option 1: Template-Based Bracket System ✅ **RECOMMENDED**
Create pre-defined bracket templates for common structures, with manual override capability.
#### Bracket Templates
```typescript
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
```typescript
// 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
```typescript
/**
* 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:**
```tsx
<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