- 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.
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
Option 1: Template-Based Bracket System ✅ RECOMMENDED
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
isScoringandtemplateRoundto playoff_matches schema - Add
bracketTemplateIdandscoringStartsAtRoundto 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
-
Custom Templates: Should admins be able to save custom bracket templates for reuse?
- Recommendation: Not in initial implementation. Add if requested.
-
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.
-
Series Support: Should we support best-of-7 series (NBA/NHL)?
- Recommendation: Not initially. Just track series winner for now.
-
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