- Added a comprehensive plan for bracket expansion, including support for 4, 8, 16, 32, and 68 team formats. - Introduced a template-based bracket system with predefined templates for NCAA March Madness, NFL Playoffs, NBA Playoffs, and simple brackets. - Updated UI flow for bracket creation, allowing admins to select templates and assign participants flexibly. - Enhanced database schema to accommodate new scoring rules and bracket templates. - Proposed updates to scoring logic to handle non-scoring rounds and participant placements correctly. - Documented implementation phases for gradual rollout of new features. - Addressed critical bugs in the playoff event processing and scoring logic, ensuring proper advancement and scoring rules across multiple leagues.
345 lines
12 KiB
Markdown
345 lines
12 KiB
Markdown
# 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
|