- Added a new route for league standings: `/leagues/:leagueId/standings/:seasonId` - Implemented the `StandingsTable` component to display team standings with ranking, points, and placement breakdown. - Integrated tiebreaker logic for ranking teams based on total points and placements. - Enhanced the league home page with a link to view standings. - Updated scoring system documentation to reflect the new standings features. - Added comprehensive tests for tiebreaker logic and `StandingsTable` component. - Created shared types for standings to be used in both server and client code.
55 KiB
Scoring System Implementation Plan
Overview
Build a comprehensive scoring system that:
- Tracks real-world sports results and converts them to fantasy points
- Supports season-specific scoring rules (customizable placement points)
- Calculates team standings with tiebreakers
- Handles 4 distinct scoring patterns across different sports
- Integrates with expected value calculations for draft rankings
- Displays brackets and standings to league members
Requirements Summary
Scoring Patterns (4 Types)
Based on the answers, we need to support these distinct patterns:
1. Single Elimination Playoffs (NFL, MLB, NBA)
- Structure: Quarterfinals → Semifinals → Finals → Champion
- Placement Logic: Multiple teams share placements based on round of elimination
- Winner: 1st place
- Finals loser: 2nd place
- Semifinal losers: Share 3rd/4th placement
- Quarterfinal losers: Share 5th-8th placement
- Points: Average the league scoring rules for shared placements
- QF losers get avg(5th, 6th, 7th, 8th) = avg(25, 25, 15, 15) = 20 points (with default scoring)
- SF losers get avg(3rd, 4th) = avg(50, 40) = 45 points
- Finals loser gets 70 points
- Winner gets 100 points
2. Page Playoffs (AFL)
- Structure: More complex bracket with specific placements for 1st through 8th
- Placement Logic: All 8 placements are determined individually (7th, 8th, 5th, 6th, 3rd, 4th, 2nd, 1st)
- Points: Apply league scoring rules directly (or average if needed)
3. Season Standings (F1) ✅ Implemented
- Structure: Final season standings determine placements
- Placement Logic:
- Championship points are entered throughout the season
- Positions automatically calculated from points (highest = 1st)
- Top 8 finishers by position get fantasy placements
- Ties handled automatically (same points = same position)
- Points: Apply league scoring rules to final positions
- Implementation: Managed via "Final Standings" event type in events system
4. Qualifying Points (Golf, Tennis)
- Structure: Two-phase scoring system
- Phase 1: Major tournaments award qualifying points (QP)
- Phase 2: After all majors complete, QP standings (1-8) convert to fantasy points
- Qualifying Point Values: Fixed system (not league-customizable)
- 1st: 20 QP, 2nd: 14 QP, 3rd: 10 QP, 4th: 8 QP
- 5th: 5 QP, 6th: 5 QP, 7th: 3 QP, 8th: 3 QP
- 9th-12th: 2 QP each
- 13th-16th: 1 QP each
- Points: Once all majors done, rank participants by total QP, then apply league scoring to ranks 1-8
League Scoring Configuration
Default Placement Points (customizable per season):
- 1st: 100 points
- 2nd: 70 points
- 3rd: 50 points
- 4th: 40 points
- 5th: 25 points
- 6th: 25 points
- 7th: 15 points
- 8th: 15 points
Customization:
- Configured on league create/edit pages
- Applied at season level (can vary between seasons of same league)
- Same point values apply to ALL sports in that season
- Only placement points are customizable (no bonus points or weighting)
Team Scoring
Calculation:
- Sum of all drafted participants' final points
- No "best N" counting - all picks count
Tiebreakers (in order):
- Number of 1st place finishes
- Number of 2nd place finishes
- Number of 3rd place finishes
- (continue through all placements)
Standings Display
Show on League Home:
- Total points per team
- Current rank
- Number of participants not yet finished
- Expected value / projected final total
- Movement indicators (up/down from previous calculation)
Additional Views:
- Historical view after season completion
- "Ownership hints" on each sport's page (per league)
Access Control:
- League members only (not public)
Expected Value System
Purpose: Provide pre-draft rankings and keep autodraft current
Calculation:
- League-specific (based on that league's scoring rules)
- Based on probability distribution of placements for each participant
- P(1st), P(2nd), P(3rd), ..., P(8th)
- EV = Σ [P(placement) × points(placement)]
- Updates daily during season
- Not shown on draft board (used internally for autodraft rankings)
Data Entry
Current: Manual entry by admins only Future: API integration for automatic updates
Flow:
- Admin enters result in admin section (e.g., "Tiger Woods finished 3rd at Masters")
- System determines event type (qualifying event vs final scoring event)
- If qualifying: Update participant's QP total, recalculate QP standings
- If final scoring: Update participant final placement, recalculate team scores and standings
- Changes propagate to all leagues that include affected participants
Implementation Order
- Scoring rules configuration (admin/commissioner UI)
- Result entry system (admin UI)
- Scoring calculation engine (models/services)
- Standings display (league member UI)
- Expected value calculation (background job)
- Draft integration (autodraft ranking updates)
Follow-up Clarification Questions
Playoff Mechanics
Q1: For single elimination playoffs, when you say "average out the points" - I want to confirm the flow:
- Two teams lose in semifinals
- Both get assigned placements 3rd AND 4th (share both placements)?
- OR both get assigned "tied for 3rd" (same placement)?
- Then we calculate:
(points_for_3rd + points_for_4th) / 2 = (50 + 40) / 2 = 45?
Is this correct? Or is there a different mechanism?
Answer: Solid question. I could see random ties happening in sports like F1 where like 4th and 5th tie, but 3rd is on its own. I think it makes the most sense to be able to assign 3rd to both teams, and then the system calculates that I should be splitting the points between 3rd and 4th? If you have a better idea on how to tackle this, I'm happy to listen.
Q2: For playoffs, do we need to track individual games/matches in the bracket?
- If yes: Need to store home/away, winner, score, etc.?
- Or just: Store final round of elimination per participant?
Answer: We should probably keep track of everything so that we can display it to the end user, as it's probably interesting?
Q3: Should we display the full bracket structure (showing all matchups) to users?
- Or just show: "Team X eliminated in Semifinals"?
Answer: Yeah, the bracket would be fun to show, we can show the manager who picked each team too to kind of promote "oh who's my real life rival in this matchup" to promote some friendly trash talking?
Qualifying Points
Q4: When all majors in a sports season complete, does the QP → fantasy points conversion happen:
- Automatically (when last major marked complete)?
- Or manually (admin clicks "Finalize Qualifying Points" button)?
Answer: I think manually is fine. We can automate that later.
Q5: How do we handle ties in qualifying points totals?
- Standard tiebreaker rules (e.g., best finish in a major)?
- Or share the placements (both get 3rd/4th)?
Answer: Share the placements.
Q6: For QP display, you mentioned "current qualification points totals and potential projected points" - what does projected mean here?
- Projected QP based on remaining majors?
- Or projected fantasy points based on current QP ranking?
Answer: Projected fantasy points based on current QP ranking.
Season Standings (F1)
Q7: For season standings sports (F1), do we need to:
- Track individual race results throughout the season?
- Or just: Enter the final season standings once (when season completes)?
Answer: We don't need to track the individual races, but we need to be able to show the current F1 points in the drivers championship during the season, and then we can use that to assign points at the very end.
Q8: If tracking races individually, do race results affect fantasy scoring or just final standings?
Answer: Just final standings.
Expected Value
Q9: Where does the probability distribution (P(1st), P(2nd), etc.) come from?
- Manual entry by admins?
- Statistical model based on historical data?
- External source/API?
- Start with equal probabilities and refine later?
Answer: I'd like to come up with a statistical model. We can likely get some futures to help predict, or we can pull in Elo ratings in some situations and then do some monte carlo simulations?
Q10: Does EV update based on:
- Real results as season progresses (e.g., Tiger Woods wins Masters → higher P(1st))?
- Or just: Set once pre-season and update daily based on external data?
Answer: I'd like to update daily based on real results but also model.
Display & UI
Q11: What does "ownership hints on each sport's page" mean exactly?
- Show which teams own which participants for that specific sport?
- E.g., on "2024 NFL Playoffs" page, show team logos next to each NFL team?
Answer: Like if we have the page for the NFL playoffs, along with showing the bracket (say Kansas City vs Buffalo), we'd also highlight which managers drafted Kansas City and Buffalo.
Q12: For standings movement indicators, what triggers a new "snapshot" for comparison?
- Each time scores are recalculated?
- Daily?
- After each scoring event completes?
Answer: I'd like a trailing 7 day change if possible?
Tie Handling & Edge Cases
Q13: When an admin enters results and there's a tie, which approach do you prefer?
- Option A: Admin assigns both participants "3rd place" → system automatically determines they share 3rd/4th and splits those points
- Option B: Admin explicitly selects "shared placement" and chooses which positions to share (3rd and 4th)
Which UX makes more sense?
Answer: I'm not sure? Whatever makes more sense to you is good with me.
Q14: For showing current F1 championship points during the season:
- Do admins manually update the points after each race?
- Or should we plan to pull from an API eventually?
- Should we store: (a) just the current running total, or (b) individual race results to show the progression?
Answer: We should just store the current running total, and we'll update manually but plan to pull from an API eventually.
Q15: When showing brackets with ownership hints, what should we display?
- Team name? (e.g., "Chris's Team drafted Kansas City")
- Team logo?
- Manager avatar?
- All of the above?
Answer: Um, manager avatar would be neat with a tooltip that has the manager's team name?
Q16: For the 7-day trailing change in standings:
- Should we create a standings snapshot every day automatically?
- Or only when recalculation happens (which might be less frequent)?
- How long should we keep old snapshots? Just 7 days, or keep them all for historical analysis?
Answer: Let's keep a standings snapshot every day automatically. I think we can keep them for historical analysis, because it might be cool to show a chart on how the points progressed through the season?
Q17: Can more than 2 participants tie for the same placement?
- Example: 3 participants all finish 3rd → they share 3rd/4th/5th positions?
- Or is it always max 2 sharing?
This matters especially for QP ties in golf/tennis where you might have several players with the same QP total.
Answer: More than two participants can tie for the same placement. It will happen a lot honestly, quarterfinalists will almost always tie 4-way.
Q18: Regarding participant limits and scoring:
- For playoffs: Only 8 teams qualify typically, so top 8 scoring is fine?
- For F1: ~20 drivers exist, but we only score top 8 in final standings?
- For golf/tennis: Potentially 100+ players in a major, but only top 16 get QP, and only top 8 in QP standings get fantasy points?
Is this the correct understanding?
Answer: This is correct. Playoffs may have more than 8 teams as well. March Madness in NCAA Men's Basketball has 68, but we only care about the teams that make the Elite Eight for scoring.
Final Edge Case Questions
Q19: For the tie entry UX (Q13), I recommend Option A:
- Admin enters the same placement number (e.g., "5") for all tied participants
- System automatically detects how many participants have that placement (e.g., 4 participants)
- System calculates they share positions 5, 6, 7, 8 and averages those point values
- This works for any number of ties (2-way, 4-way, even 8-way)
Is this acceptable?
Answer: Yes it is.
Q20: For large tournaments (March Madness with 68 teams), when a drafted participant is eliminated before the Elite Eight (scoring rounds):
- Should they be recorded as "finished" with 0 fantasy points?
- Or remain "unfinished" with no result record?
This affects the "participants remaining" count on standings. I recommend: Record as finished with 0 points, so they don't count as "remaining."
Answer: They are recorded as finished with 0 fantasy points.
Q21: For qualifying events (golf/tennis majors), should the QP placement range be:
- Fixed at top 16 for all qualifying sports (your listed QP values)?
- Or configurable per sports season?
I recommend: Make it configurable per sports season with a default of top 16 using your listed values.
Answer: I like your recommendation.
Refined Database Architecture
Schema Changes
Modify Existing Tables
// ADD to seasons table
seasons {
// ... existing fields
// Scoring configuration (8 values for 1st through 8th)
pointsFor1st: integer (default: 100)
pointsFor2nd: integer (default: 70)
pointsFor3rd: integer (default: 50)
pointsFor4th: integer (default: 40)
pointsFor5th: integer (default: 25)
pointsFor6th: integer (default: 25)
pointsFor7th: integer (default: 15)
pointsFor8th: integer (default: 15)
}
// ADD to sportsSeasons table
sportsSeasons {
// ... existing fields
// Replace scoringType enum with new patterns
scoringPattern: enum (
'single_elimination_playoff',
'page_playoff',
'season_standings',
'qualifying_points'
)
// For qualifying points sports
totalMajors: integer (nullable) // How many major tournaments
majorsCompleted: integer (default: 0)
qualifyingPointsFinalized: boolean (default: false)
}
// participantResults remains mostly the same but clarify usage
participantResults {
// ... existing fields
// finalPosition: Used for actual fantasy scoring (1-8)
// pointsAwarded: Calculated based on league scoring rules (NOT stored here - calculated on demand)
// qualifyingPoints: Used ONLY for qualifying_points pattern sports
}
New Tables
// Individual scoring events (games, tournaments, races)
scoring_events {
id: uuid PRIMARY KEY
sportsSeasonId: uuid -> sportsSeasons.id
name: varchar(255) // "2024 Masters", "Super Bowl LIX", "Monaco Grand Prix"
eventDate: date
eventType: enum('playoff_game', 'major_tournament', 'race', 'final_standings')
// For playoff events
playoffRound: varchar(50) // "Quarterfinals", "Semifinals", "Finals"
// For qualifying events
isQualifyingEvent: boolean (default: false)
isComplete: boolean (default: false)
completedAt: timestamp
createdAt: timestamp
updatedAt: timestamp
}
// Results for participants in specific events
event_results {
id: uuid PRIMARY KEY
scoringEventId: uuid -> scoring_events.id
participantId: uuid -> participants.id
// Placement in this specific event
placement: integer
// For qualifying events: QP awarded in this event
qualifyingPointsAwarded: decimal
// For playoff events: eliminated or advanced
eliminated: boolean
// Raw sport-specific data (optional)
rawScore: decimal // strokes, points, time, etc.
createdAt: timestamp
updatedAt: timestamp
}
// Playoff bracket matches (for display and tracking)
playoff_matches {
id: uuid PRIMARY KEY
scoringEventId: uuid -> scoring_events.id
round: varchar(50) // "Quarterfinals", "Semifinals", "Finals"
matchNumber: integer // 1, 2, 3, 4 (for QF); 1, 2 (for SF); 1 (for F)
participant1Id: uuid -> participants.id (nullable)
participant2Id: uuid -> participants.id (nullable)
winnerId: uuid -> participants.id (nullable)
loserId: uuid -> participants.id (nullable)
isComplete: boolean (default: false)
// Optional detailed data
participant1Score: decimal
participant2Score: decimal
createdAt: timestamp
updatedAt: timestamp
}
// Aggregated participant qualifying points (for qualifying_points sports)
participant_qualifying_totals {
id: uuid PRIMARY KEY
participantId: uuid -> participants.id
sportsSeasonId: uuid -> sportsSeasons.id
totalQualifyingPoints: decimal
eventsScored: integer
// After finalization
finalRanking: integer (nullable) // 1-8 based on QP totals
updatedAt: timestamp
}
// Team scores aggregated by sport season
team_sport_scores {
id: uuid PRIMARY KEY
teamId: uuid -> teams.id
sportsSeasonId: uuid -> sportsSeasons.id
totalPoints: decimal
participantsCompleted: integer // How many of this team's picks have finished
participantsTotal: integer // Total picks for this sport
calculatedAt: timestamp
}
// Overall team standings (snapshot-based for movement tracking)
team_standings {
id: uuid PRIMARY KEY
teamId: uuid -> teams.id
seasonId: uuid -> seasons.id
totalPoints: decimal
currentRank: integer
previousRank: integer (nullable) // For movement indicators
// Tiebreaker data
firstPlaceCount: integer (default: 0)
secondPlaceCount: integer (default: 0)
thirdPlaceCount: integer (default: 0)
fourthPlaceCount: integer (default: 0)
fifthPlaceCount: integer (default: 0)
sixthPlaceCount: integer (default: 0)
seventhPlaceCount: integer (default: 0)
eighthPlaceCount: integer (default: 0)
participantsRemaining: integer // Not yet finished
calculatedAt: timestamp
}
// Expected value tracking (league-specific)
participant_expected_values {
id: uuid PRIMARY KEY
participantId: uuid -> participants.id
seasonId: uuid -> seasons.id
// Probability distribution (stored as percentages)
probFirst: decimal(5,2) // e.g., 15.50 = 15.5%
probSecond: decimal(5,2)
probThird: decimal(5,2)
probFourth: decimal(5,2)
probFifth: decimal(5,2)
probSixth: decimal(5,2)
probSeventh: decimal(5,2)
probEighth: decimal(5,2)
// Calculated EV
expectedValue: decimal // Based on league scoring
calculatedAt: timestamp
updatedAt: timestamp
}
Enum Updates
// NEW
export const scoringPatternEnum = pgEnum("scoring_pattern", [
"single_elimination_playoff",
"page_playoff",
"season_standings",
"qualifying_points",
]);
// NEW
export const eventTypeEnum = pgEnum("event_type", [
"playoff_game",
"major_tournament",
"race",
"final_standings",
]);
// Keep existing scoringType for now, but may deprecate
export const scoringTypeEnum = pgEnum("scoring_type", [
"playoffs",
"regular_season",
"majors",
]);
Model Functions (Pseudocode)
Scoring Calculator
// app/models/scoring-calculator.ts
/**
* Calculate fantasy points for a participant based on league scoring rules
*/
function calculateFantasyPoints(
finalPlacement: number,
seasonScoringRules: { pointsFor1st, pointsFor2nd, ..., pointsFor8th }
): number {
const pointsMap = {
1: seasonScoringRules.pointsFor1st,
2: seasonScoringRules.pointsFor2nd,
3: seasonScoringRules.pointsFor3rd,
4: seasonScoringRules.pointsFor4th,
5: seasonScoringRules.pointsFor5th,
6: seasonScoringRules.pointsFor6th,
7: seasonScoringRules.pointsFor7th,
8: seasonScoringRules.pointsFor8th,
};
return pointsMap[finalPlacement] || 0;
}
/**
* Calculate averaged points for shared placements (playoffs)
*/
function calculateAveragedPoints(
placements: number[], // e.g., [5, 6, 7, 8] for quarterfinal losers
seasonScoringRules: ScoringRules
): number {
const total = placements.reduce((sum, placement) => {
return sum + calculateFantasyPoints(placement, seasonScoringRules);
}, 0);
return total / placements.length;
}
/**
* Process a playoff event completion
*/
async function processPlayoffEvent(eventId: uuid) {
const event = await getEventById(eventId);
const matches = await getMatchesForEvent(eventId);
// Determine which placements are finalized
if (event.playoffRound === "Finals") {
// Winner gets 1st, loser gets 2nd
const finalMatch = matches[0];
await setParticipantFinalPlacement(finalMatch.winnerId, 1);
await setParticipantFinalPlacement(finalMatch.loserId, 2);
} else if (event.playoffRound === "Semifinals") {
// Losers share 3rd/4th
const losers = matches.map(m => m.loserId);
for (const loserId of losers) {
await setParticipantSharedPlacement(loserId, [3, 4]);
}
} else if (event.playoffRound === "Quarterfinals") {
// Losers share 5th-8th
const losers = matches.map(m => m.loserId);
for (const loserId of losers) {
await setParticipantSharedPlacement(loserId, [5, 6, 7, 8]);
}
}
// Trigger recalculation for all affected leagues
await recalculateAffectedLeagues(event.sportsSeasonId);
}
/**
* Process a qualifying event completion (majors)
*/
async function processQualifyingEvent(eventId: uuid) {
const results = await getEventResults(eventId);
// Award qualifying points based on placement
for (const result of results) {
const qpAwarded = getQualifyingPointsForPlacement(result.placement);
// Update participant's total QP
await addQualifyingPoints(
result.participantId,
result.sportsSeasonId,
qpAwarded
);
}
// Check if all majors are complete
const sportsSeason = await getSportsSeason(event.sportsSeasonId);
sportsSeason.majorsCompleted += 1;
if (sportsSeason.majorsCompleted >= sportsSeason.totalMajors) {
// Finalize qualifying points
await finalizeQualifyingPoints(sportsSeason.id);
}
// Update QP standings displays (no fantasy points yet)
await recalculateQPStandings(sportsSeason.id);
}
/**
* Finalize qualifying points and convert to fantasy placements
*/
async function finalizeQualifyingPoints(sportsSeasonId: uuid) {
// Get all participants ranked by total QP
const qpStandings = await getQPStandings(sportsSeasonId);
// Assign final placements (1-8) based on QP ranking
for (let i = 0; i < Math.min(8, qpStandings.length); i++) {
const participant = qpStandings[i];
await setParticipantFinalPlacement(participant.id, i + 1);
}
// Mark as finalized
await markQPFinalized(sportsSeasonId);
// Trigger recalculation for all affected leagues
await recalculateAffectedLeagues(sportsSeasonId);
}
/**
* Recalculate all team scores and standings for a season
*/
async function recalculateStandings(seasonId: uuid) {
const season = await getSeason(seasonId);
const teams = await getTeamsForSeason(seasonId);
const sportsSeasons = await getSportsSeasonsForSeason(seasonId);
for (const team of teams) {
let totalPoints = 0;
let placementCounts = {1:0, 2:0, 3:0, 4:0, 5:0, 6:0, 7:0, 8:0};
// Get all drafted participants for this team
const picks = await getDraftPicksForTeam(team.id);
for (const pick of picks) {
const result = await getParticipantFinalResult(pick.participantId);
if (result && result.finalPosition) {
// Calculate points based on league scoring rules
const points = calculateFantasyPoints(result.finalPosition, season);
totalPoints += points;
// Track placement for tiebreakers
placementCounts[result.finalPosition]++;
}
}
// Update or create standings record
await upsertTeamStanding(team.id, seasonId, {
totalPoints,
...placementCounts,
participantsRemaining: calculateRemaining(picks),
});
}
// Rank teams and set previousRank for movement tracking
await rankTeamsWithTiebreakers(seasonId);
}
Expected Value Calculator
// app/models/expected-value-calculator.ts
/**
* Calculate expected value for a participant in a specific season
*/
async function calculateExpectedValue(
participantId: uuid,
seasonId: uuid
): Promise<number> {
const season = await getSeason(seasonId);
const probDist = await getProbabilityDistribution(participantId, seasonId);
let ev = 0;
ev += probDist.probFirst * season.pointsFor1st;
ev += probDist.probSecond * season.pointsFor2nd;
ev += probDist.probThird * season.pointsFor3rd;
ev += probDist.probFourth * season.pointsFor4th;
ev += probDist.probFifth * season.pointsFor5th;
ev += probDist.probSixth * season.pointsFor6th;
ev += probDist.probSeventh * season.pointsFor7th;
ev += probDist.probEighth * season.pointsFor8th;
return ev;
}
/**
* Update all expected values for a season (daily job)
*/
async function updateAllExpectedValues(seasonId: uuid) {
const participants = await getAllParticipantsForSeason(seasonId);
for (const participant of participants) {
// Get/update probability distribution (from external source or model)
const probDist = await fetchProbabilityDistribution(participant.id);
// Calculate EV
const ev = await calculateExpectedValue(participant.id, seasonId);
// Store
await upsertParticipantEV(participant.id, seasonId, {
...probDist,
expectedValue: ev,
});
}
// Update draft rankings for autodraft
await updateAutodraftRankings(seasonId);
}
UI Components & Routes
Admin Routes
/admin/sports-seasons/$sportsSeasonId/events
- List all scoring events
- Create new event (tournament, playoff round, etc.)
/admin/sports-seasons/$sportsSeasonId/events/$eventId
- Enter results for specific event
- Mark event complete
- For playoffs: Enter bracket matchups and winners
- For majors: Enter participant placements
- For final_standings (F1, etc.):
- Enter championship points for all participants
- Positions auto-calculated from points (highest = 1st)
- Update standings throughout season
- Finalize button to convert top 8 to fantasy placements
/admin/sports-seasons/$sportsSeasonId/finalize-qp
- View current qualifying points standings
- Finalize QP → fantasy points conversion
- (Only for qualifying_points sports after all majors complete)
/admin/recalculate-standings
- Manual recalculation trigger
- Select season or all seasons
Commissioner Routes
/leagues/$leagueId/seasons/$seasonId/settings
- Edit scoring rules (8 placement point values)
- Part of existing season settings page
Member Routes
/leagues/$leagueId/seasons/$seasonId
- Main league home: Standings table
- Total points, rank, movement indicators
- Participants remaining count
- Projected totals (based on EV)
/leagues/$leagueId/seasons/$seasonId/standings/history
- Historical standings view (after season completion)
- Show final results, placements, tiebreakers
/leagues/$leagueId/sports-seasons/$sportsSeasonId ✅ *Implemented*
- View for specific sport season
- Show all participants in this sport
- Ownership hints (which teams own which participants)
- Current results/QP standings
- Bracket display (for playoff sports)
- **Implementation note**: Route simplified to not require seasonId since it's determined from sports season relations
/leagues/$leagueId/seasons/$seasonId/teams/$teamId
- Detailed team breakdown
- All picks with their placements and points
- Breakdown by sport season
Components
// app/components/standings/StandingsTable.tsx
- Display team rankings
- Show total points, movement, participants remaining
- Clickable rows to team detail
// app/components/standings/TeamScoreBreakdown.tsx
- Detailed view of a team's score
- List all picks with placement and points
- Group by sport season
// app/components/scoring/ScoringRulesEditor.tsx
- Edit 8 placement point values
- Show defaults, allow customization
- Preview point distribution
// app/routes/admin.sports-seasons.$id.events.$eventId.tsx
- Admin page to enter event results (implemented inline, not separate component)
- Different UI for each event type:
- playoff_game: Link to bracket management
- major_tournament: Participant placement input
- final_standings: Championship points table with auto-calculated positions
// app/components/scoring/PlayoffBracket.tsx
- Visual bracket display
- Show matchups, winners, rounds
- Highlight participants owned in current league
// app/components/scoring/QualifyingPointsStandings.tsx
- Show current QP totals
- Projected placements based on current QP
- Projected fantasy points
// app/components/scoring/OwnershipIndicator.tsx
- Small component showing which team(s) own a participant
- Used on sport season pages
Implementation Phases
Phase 1: Core Scoring Infrastructure
- Database schema updates (add tables and fields)
- Scoring configuration UI (season settings page)
- Basic scoring calculator functions
- Admin result entry UI (simple version)
Phase 2: Playoff Scoring (Single Elimination)
- Playoff event creation
- Bracket match tracking
- Playoff scoring calculation
- Basic standings display
- Test with NFL/NBA data
Phase 3: Other Scoring Patterns
- Season standings (F1)
- Qualifying points (Golf/Tennis)
- Page playoffs (AFL)
- Pattern-specific UI components
Phase 4: Standings & Display
- Full standings table with tiebreakers
- Movement tracking
- Team breakdown pages
- Sport season pages with ownership
- Bracket visualization
Phase 5: Expected Value
- Probability distribution storage
- EV calculation engine
- Daily update job
- Autodraft ranking integration
- Projected totals display
Phase 6: Polish & Optimization
- Caching strategy
- Performance optimization
- Historical views
- Admin recalculation tools
- Error handling & edge cases
Implementation Decisions Summary
Based on all answers (Q1-Q18), here are the key decisions:
Scoring Mechanics
- ✅ Placement averaging: Admin enters same placement for ties → system auto-calculates shared positions (Q1, Q13)
- ✅ Multi-way ties: Supported for any number of participants (Q17)
- ✅ March Madness: Only Elite Eight (top 8) get scored, earlier rounds = 0 points (Q18)
- ✅ F1 during season: Store current running total, manual update with API future (Q14)
- ✅ Golf/Tennis QP: Top 16 get QP, top 8 in QP standings get fantasy points (Q18)
Display & UI
- ✅ Bracket ownership: Show manager avatars with team name tooltips (Q15)
- ✅ Bracket matchups: Track all matches for display (Q2, Q3)
- ✅ QP projected points: Show fantasy points based on current QP ranking (Q6)
Standings & Snapshots
- ✅ Movement tracking: Daily automatic snapshots, kept for historical analysis (Q16)
- ✅ 7-day change: Compare to snapshot from 7 days ago
- ✅ Historical charts: Display point progression over time
Expected Value
- ✅ Probability source: Statistical model (Elo, futures, Monte Carlo simulations) (Q9)
- ✅ EV updates: Daily, based on real results + model (Q10)
- ✅ League-specific: Different EV per league based on scoring rules (Q14 from original)
Process Flow
- ✅ QP finalization: Manual (admin button) after all majors complete (Q4)
- ✅ QP ties: Share placements (Q5)
- ✅ Playoff mechanics: Based on elimination round, all losers in same round tie (Q1, Q7)
- ✅ F1 scoring: Just final standings, not individual races (Q8)
All Questions Answered ✅
All clarification questions (Q1-Q21) have been answered and confirmed:
- Q19: ✅ Tie entry uses Option A (admin enters same placement, system auto-calculates)
- Q20: ✅ Early elimination = 0 points (recorded as finished)
- Q21: ✅ QP range configurable per sports season (default top 16)
Progress Tracking
Phase 1: Core Scoring Infrastructure
Goal: Set up database schema and basic scoring configuration
-
1.1 Update database schema ✅ Completed
- Add 8 point fields to
seasonstable - Add
scoringPatternenum to replace/augmentscoringType - Add
totalMajors,majorsCompleted,qualifyingPointsFinalizedtosportsSeasons - Create
scoring_eventstable - Create
event_resultstable - Create
playoff_matchestable - Create
participant_qualifying_totalstable - Create
team_sport_scorestable - Create
team_standingstable - Create
team_standings_snapshotstable (for daily tracking) - Create
participant_expected_valuestable - Create
participant_season_resultstable (for F1 current points) - Create
qualifying_point_configtable (configurable QP values) - Add all relations for new tables
- Run migration
- Update test fixtures with new scoring fields
- Add 8 point fields to
-
1.2 Create model functions (basic structure) ✅ Completed
app/models/scoring-rules.ts- Get/update season scoring configapp/models/scoring-event.ts- CRUD for scoring eventsapp/models/event-result.ts- Record event resultsapp/models/scoring-calculator.ts- Calculate points and standings (core logic placeholder)app/models/standings.ts- Get standings and team breakdowns- Add draftPicks relations to schema
- All TypeScript compilation passes
-
1.3 Update season settings UI ✅ Completed
- Add scoring rules editor component (ScoringRulesEditor.tsx)
- Add 8 input fields for placement points with emojis
- Update league settings form to include scoring rules
- Update league creation form to include scoring rules
- Add validation for point values (0-1000 range)
- Disable editing after draft starts (settings page only)
- Show helpful tips and preview
-
1.4 Basic admin result entry UI ✅ Completed
- Create admin route structure for sports seasons
- List scoring events for a sports season
- Create new event form (playoff_game, major_tournament, race, final_standings)
- Basic result entry form (placement only)
- Update existing results (inline editing with Enter key support)
- Delete results with confirmation
- Delete scoring events with confirmation
- Proper .server.ts file separation for client/server code
- Navigation from sports season → events list → event details
Phase 2: Playoff Scoring (Single Elimination)
Goal: Implement playoff bracket tracking and scoring for all tournament types
-
2.1 Playoff event creation ✅
- UI for creating playoff rounds (QF, SF, Finals)
- Bracket structure definition (4-team and 8-team brackets)
- Match creation (automatic from bracket structure)
- Created playoff-match model with bracket generation
- Created bracket UI route at
/admin/sports-seasons/:id/events/:eventId/bracket
-
2.2 Bracket match tracking ✅
- UI for entering match results (winner/loser)
- Display bracket structure grouped by round
- Show matchup participants with TBD placeholders
- Automatic winner advancement to next round
- Added "Manage Bracket" button to event details page
-
2.3 Playoff scoring calculation ✅
- Implement placement sharing logic (Q19)
- Calculate averaged points for shared placements
- Handle multi-way ties (4-way for QF losers, 2-way for SF losers)
- Update
participantResultswith final placements - Implement 0-point recording for early eliminations (Q20)
- Created
processPlayoffEventfunction in scoring-calculator - Support for Quarterfinals (5th-8th shared), Semifinals (3rd-4th shared), Finals (1st, 2nd)
-
2.4 Basic standings display ✅
- Create
StandingsTablecomponent - Show total points, rank, movement indicators
- Show participants remaining count
- Display placement breakdown (1st-8th counts) for tiebreakers
- Rank badges with trophy/medal icons for top 3
- League home page standings integration (component ready, route integration pending)
- Create
-
2.5 Testing with real data ✅
- Created comprehensive test suite (18 tests)
- Test NFL playoff scenarios (8-team bracket)
- Test NCAA March Madness Elite Eight scenarios
- Verify point calculations for all rounds
- Test tie scenarios (2-way, 4-way, edge cases)
- All tests passing ✅
LIMITATION: Phase 2.1-2.5 only supports 4 and 8 team brackets. Phases 2.6-2.9 expand to support all tournament sizes.
Bracket Expansion (Phases 2.6-2.9)
Current implementation only supports 4 and 8 team brackets. We need to support:
- Standard brackets: 4, 8, 16, 32 teams
- March Madness: 68 teams with First Four play-in games
- NFL Playoffs: 14 teams with bye weeks
- NBA Playoffs: 16 teams with conference structure
- Scoring rounds vs non-scoring rounds: March Madness only scores Elite Eight and beyond
Template-Based Bracket System
Create pre-defined bracket templates for common structures, with flexible participant assignment.
Bracket Template Structure
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?
}
Key Templates
NCAA March Madness (68 teams)
- First Four (4 games, 8 teams) - play-in games, NOT scoring
- Round of 64 (32 games) - NOT scoring
- Round of 32 (16 games) - NOT scoring
- Sweet Sixteen (8 games) - NOT scoring
- Elite Eight (4 games) - SCORING STARTS (losers share 5th-8th)
- Final Four (2 games) - scoring (losers share 3rd-4th)
- Championship (1 game) - scoring (1st/2nd)
NFL Playoffs (14 teams)
- Wild Card (6 games, 12 teams) - 2 teams get byes
- Divisional (4 games) - SCORING STARTS (QF, losers share 5th-8th)
- Conference Championship (2 games) - scoring (SF, losers share 3rd-4th)
- Super Bowl (1 game) - scoring (Finals, 1st/2nd)
NBA Playoffs (16 teams)
- First Round (8 games) - NOT scoring
- Conference Semifinals (4 games) - SCORING STARTS (QF, losers share 5th-8th)
- Conference Finals (2 games) - scoring (SF, losers share 3rd-4th)
- NBA Finals (1 game) - scoring (Finals, 1st/2nd)
Simple 16-team and 32-team brackets
- Start scoring at Quarterfinals (top 8)
- Earlier rounds award 0 points per Q20
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", "Elite Eight", 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", "simple_16", etc.
scoringStartsAtRound: varchar(50) // Which round starts fantasy scoring
}
Implementation Phases
-
2.6 Template System Foundation ✅
- Define bracket template TypeScript types and constants
- Add
isScoring,templateRound,seedInfoto playoff_matches schema - Add
bracketTemplateId,scoringStartsAtRoundto scoring_events schema - Create database migration (drizzle/0023)
- Update playoff-match model to support templates
- Create template definitions file with all bracket templates (app/lib/bracket-templates.ts)
- Add
generateBracketFromTemplate()function - Add
advanceWinnerTemplate()function for template-based advancement - Test suite for template structures (16 tests passing)
-
2.7 Simple Template Expansion (16, 32 teams) ✅ Completed
- Create template definitions for 16 and 32 team brackets (already done in 2.6)
- Update bracket generation UI to use template selector
- Update server action to use
generateBracketFromTemplateandadvanceWinnerTemplate - Update scoring calculator for non-scoring rounds (0 points per Q20)
- Create comprehensive integration tests (19 tests covering 16, 32, and NCAA 68 brackets)
- Verify Elite Eight scoring starts correctly
- All 207 tests passing including new bracket integration tests
-
2.8 March Madness Template (68 teams) ✅ Completed
- Create NCAA_68 template definition
- Implement NCAA tournament seeding with First Four play-in logic
- Build generateNCAA68Bracket() function with proper seeding
- Implement TBD slot handling for Round of 64 (winners from First Four)
- Implement advanceFirstFourWinner() for First Four → Round of 64 advancement
- Create comprehensive unit tests (27 tests covering NCAA 68 structure and logic)
- Verify only Elite Eight and beyond score points
- Verify early elimination = 0 points (per Q20)
- All 248 tests passing (including 27 NCAA 68 tests)
- Build First Four matchup assignment UI (admin interface)
- UI is part of general bracket generation at
/admin/sports-seasons/:id/events/:eventId/bracket - ParticipantSelector component handles all 68 participant assignments
- First Four round displays in bracket UI with winner selection
- Winners automatically advance to Round of 64 TBD slots
- UI is part of general bracket generation at
-
2.9 NFL Templates ✅ Completed
- Create NFL_14 template (with bye weeks)
- Basic structure tests in bracket-templates.test.ts
- Implement generateNFL14Bracket() with bye week logic
- Wild Card assigns 12 participants (seeds 3-14)
- Seeds 1-2 automatically advance to Divisional round with TBD opponents
- Add special case handling in generateBracketFromTemplate()
- Create comprehensive tests for NFL_14 bracket (20 tests)
- Verify scoring starts at correct round (Divisional)
- Test bye week math and tournament structure
- Note: NBA 16-team playoffs use SIMPLE_16 template (functionally identical, generic naming)
Phase 3: Other Scoring Patterns
Goal: Implement season standings (F1) and qualifying points (Golf/Tennis)
-
3.1 Season standings (F1) ✅ Completed
- Create
participant_season_resultsmodel functions (app/models/participant-season-result.ts) - Implement
processSeasonStandingsfunction in scoring-calculator - UI for entering championship points via "Final Standings" event type
- Auto-calculate positions from points (highest points = 1st place)
- Store running totals in
participant_season_resultstable - Bulk update standings throughout season
- Finalize season button converts top 8 to fantasy placements
- Handle ties automatically (Q13, Q19)
- Comprehensive test suite (13 tests covering all scenarios)
- All 281 tests passing ✅
Implementation Note: Season standings are managed through the events system using
eventType: "final_standings". When viewing a final_standings event, admins see a standings tracker table where they enter championship points after each race/event. Positions are automatically calculated from points. When the season ends, marking the event as complete triggersprocessSeasonStandings()which assigns fantasy placements to the top 8 finishers. - Create
-
3.2 Qualifying points (Golf/Tennis) ✅ Completed
- QP value configuration per sports season (Q21)
- Created
qualifying_point_configtable with configurable QP values - Default QP values: 1st=20, 2nd=14, 3rd=10, 4th=8, 5th-6th=5, 7th-8th=3, 9th-12th=2, 13th-16th=1
- Model functions:
initializeQPConfig,getQPConfig,updateQPConfig,getQPForPlacement
- Created
- UI for entering major tournament results
- Uses existing event results system with Major Tournament event type
- Automatic detection: Major tournaments in qualifying_points seasons are marked as qualifying events
- Manual override: "Mark as Qualifying Event" button for retroactive fixing
- Calculate and store QP totals
processQualifyingEventfunction in scoring-calculator- Proper tie handling: Groups tied participants, sums QP for occupied positions, divides equally
- Example: 7-way tie for 14th (positions 14-20) = (1+1+1+0+0+0+0)/7 = 0.43 QP each
participant_qualifying_totalstable tracks running totals and events scored
- Display QP standings
QualifyingPointsStandingscomponent shows current standings- Displays total QP, events scored, and current rank
- Shows majors progress (X of Y completed)
- Visual indicators for finalized vs in-progress state
- QP values display with 2 decimal precision (.toFixed(2))
- Show projected fantasy points based on current QP (Q6)
- Calculates projected points for top 8 based on league scoring rules
- Shows before finalization to preview what participants would earn
- Updates dynamically as QP totals change throughout season
- Manual finalization button (Q4)
- "Finalize Qualifying Points" button appears when all majors complete
- Confirmation dialog prevents accidental finalization
- Converts top 8 QP standings to fantasy placements (1-8)
- Awards fantasy points based on league scoring rules
- Locks QP totals (no further changes after finalization)
- Handle QP ties with shared placements (Q5)
- Tie handling during QP awards: Same placement entered → system calculates shared positions
- Tie handling during finalization: Participants with same QP share fantasy placements
- Both work for any number of ties (2-way, 4-way, 7-way, etc.)
- Sports season configuration
- Added "Scoring Pattern" dropdown to create/edit forms
- "Qualifying Points (Golf/Tennis)" option available
- Conditional "Total Majors" field (e.g., 4 for Golf, 4 for Tennis Grand Slams)
- Visual badge on events page showing "Qualifying Points Mode"
- Smart event creation
- Event type defaults to "Major Tournament" for qualifying_points seasons
- Helper text explains Major Tournaments automatically award QP
- Process/Reprocess QP button for manual recalculation
- Comprehensive test suite
- 17 unit tests in qualifying-points.test.ts
- 10 integration tests in scoring-calculator-qualifying.test.ts
- Tests cover: configuration, accumulation, tie handling, finalization, edge cases
- All 308 tests passing ✅
- QP value configuration per sports season (Q21)
-
3.3 Page playoffs (AFL) ✅ Completed
- Research AFL finals format (2026+ Wildcard Round system)
- Create AFL_10 bracket template with double-chance system
- Implement generateAFL10Bracket function for complex advancement
- Add AFL-specific advancement logic (advanceAFLWinner)
- Update scoring calculator for AFL placements (EF: 7-8, SF: 5-6, PF: 3-4, GF: 1-2)
- Comprehensive test suite (24 tests covering all rounds and placements)
- All 337 tests passing ✅
Implementation Notes:
- AFL Finals use a complex "double-chance" system where top 6 teams bypass Wildcard
- Qualifying Finals winners skip Semi-Finals, losers get second chance in Semi-Finals
- Wildcard Round (7v10, 8v9): Losers get 0 points (9th-10th place)
- Elimination Finals: Losers share 7th-8th
- Semi-Finals: Losers share 5th-6th
- Preliminary Finals: Losers share 3rd-4th
- Grand Final: Winner 1st, Loser 2nd
-
3.4 Pattern-specific UI components ✅ Completed
PlayoffBracketcomponent with ownership hints (Q15)QualifyingPointsStandingscomponent (completed in 3.2)SeasonStandingscomponent for F1SportSeasonDisplaycomponent for pattern detection and appropriate display- Member-facing route
/leagues/:leagueId/sports-seasons/:sportsSeasonId
Implementation Notes:
- PlayoffBracket: Displays bracket with matches grouped by round, shows ownership with team avatar badges
- SeasonStandings: Shows F1-style championship standings with positions auto-calculated from points
- SportSeasonDisplay: Pattern detection wrapper that renders appropriate component based on scoring pattern
- Route created for members to view individual sport seasons within their league
- Ownership hints show team avatars with hover tooltips (Q15: manager name in title attribute)
- All TypeScript compilation passes ✅
Phase 4: Standings & Display
Goal: Full-featured standings with breakdowns and historical views
-
4.1 Enhanced standings table ✅ Completed
- Implement tiebreaker logic (placement counts)
- Add sorting by tiebreakers
- Display placement breakdown per team
- Create standalone standings page route
- Integrate StandingsTable component into route
- Add navigation from league home page
- Fix client/server code separation issues
- Comprehensive test suite (13 tiebreaker tests + 21 component tests)
- All 371 tests passing ✅
Implementation Notes:
- Tiebreaker cascade: Total points → 1st place count → 2nd place count → ... → 8th place count
- Teams with identical scores and placements share the same rank
- Rank numbers skip after ties (e.g., two teams tied for 1st, next is 3rd)
StandingsTablecomponent displays:- Rank badges with trophy/medal icons for top 3
- Total points with one decimal place
- Placement breakdown showing counts for each placement (1st-8th)
- Movement indicators (up/down arrows)
- Participants remaining count
recalculateStandingsfunction now assigns ranks using tiebreaker logicpreviousRanktracked for movement indicators- Created standalone standings page at
/leagues/:leagueId/standings/:seasonId - Fixed module bundling issue by separating types into
app/types/standings.ts - Types can now be safely imported by both client and server code
- Added "View Standings" link in League Info section on league home page
- Files created/updated:
- app/models/scoring-calculator.ts (added compareTeamsForRanking, assignRanks)
- app/models/standings.ts (fixed sort order, re-exports types)
- app/types/standings.ts (new - shared type definitions)
- app/components/standings/StandingsTable.tsx (new component)
- app/routes/leagues/$leagueId.standings.$seasonId.tsx (new route)
- app/routes/leagues/$leagueId.tsx (added standings link)
- app/routes.ts (registered new route)
- tsconfig.node.json (added app/types/**/*.ts to include)
- app/models/tests/tiebreaker-logic.test.ts (new tests)
- app/components/standings/tests/StandingsTable.test.tsx (new tests)
-
4.2 Movement tracking & snapshots
- Daily snapshot creation (scheduled job)
- 7-day comparison logic (Q16, Q12)
- Movement indicators (up/down arrows)
- Store snapshots for historical analysis (Q16)
-
4.3 Team breakdown pages
TeamScoreBreakdowncomponent- List all drafted participants
- Show placement and points per participant
- Group by sport season
- Link to sport season pages
-
4.4 Sport season pages with ownership ✅ Completed in Phase 3.4
- Create sport season detail route (
/leagues/:leagueId/sports-seasons/:sportsSeasonId) - Display all participants for that sport
- Show current results/standings (pattern-specific via SportSeasonDisplay)
- Ownership indicators with avatars + tooltips (Q11, Q15)
- Bracket display for playoff sports (Q3)
Note: Route exists and displays correctly. Navigation cards on league home page pending (see 4.5)
- Create sport season detail route (
-
4.5 League home page integration (partially complete)
- Add navigation link to standings page from league home
- Display team standings table on league home page (or keep separate as currently implemented)
- Add "Sports Seasons" section showing all sports in current season
- Link each sport season card to detail page (Q11 - ownership hints on each sport's page)
- Show sport status badges (upcoming, active, completed)
- Quick access to brackets from league home
Implementation Notes:
- "View Standings" link added in League Info section (app/routes/leagues/$leagueId.tsx:433-440)
- Standalone standings page preferred over embedding in league home for cleaner UX
- Sport season detail pages already exist (Phase 3.4) - need navigation cards on league home
-
4.6 Historical views
- Season completion detection
- Historical standings page
- Point progression chart over time (Q16)
- Final results with tiebreaker details
Phase 5: Expected Value
Goal: Implement EV calculation and autodraft integration
-
5.1 Probability distribution storage
- Create data structure for probabilities (8 placements)
- Admin UI for manual entry (temporary)
- Validation (probabilities sum to 100%)
-
5.2 EV calculation engine
- Implement
calculateExpectedValuefunction - League-specific calculation based on scoring rules
- Batch calculation for all participants in a season
- Implement
-
5.3 Daily update job
- Create scheduled job/cron
- Fetch/update probability distributions
- Recalculate all EVs
- Log updates
-
5.4 Autodraft ranking integration
- Update draft queue ranking based on EV
- Sort available participants by EV
- Autodraft picks highest EV available
-
5.5 Projected totals display
- Calculate projected final points (completed + EV of remaining)
- Display on standings table
- Show in team breakdown
Phase 6: Polish & Optimization
Goal: Performance, caching, and production-ready features
-
6.1 Caching strategy
- Cache standings calculations
- Cache EV calculations
- Invalidation on score updates
- Redis or in-memory caching
-
6.2 Performance optimization
- Database query optimization
- Indexing strategy
- Pagination for large datasets
- Load testing
-
6.3 Admin recalculation tools
- Manual recalculation button per season
- Bulk recalculation for all seasons
- Recalculation audit log
- Progress indicators for long operations
-
6.4 Error handling
- Validation for result entry
- Handle missing data gracefully
- Error messages and recovery
- Rollback mechanisms
-
6.5 Edge cases & polish
- Handle participant withdrawals/injuries
- Scoring corrections/disputes flow
- Timezone handling for event dates
- Mobile responsive design
- Accessibility improvements
Future Enhancements (Not MVP)
- API integration for automatic result updates
- Socket.IO for live scoring updates
- Statistical model for EV (Elo, Monte Carlo) (Q9, Q10)
- Advanced bracket visualization library
- Commissioner result entry (not just admin)
- Notification system for scoring updates
- Social features (trash talk, reactions)
- Export standings to CSV/PDF
IMPLEMENTATION STATUS
- ✅ Phase 1: Core Scoring Infrastructure - COMPLETE
- ✅ Phase 2: Playoff Scoring (Single Elimination) - COMPLETE
- Supports 4, 8, 16, 32 team brackets
- NCAA 68-team March Madness with First Four
- NFL 14-team playoffs with bye weeks
- Template-based system for all bracket types
- ✅ Phase 3: Other Scoring Patterns - COMPLETE
- 3.1 Season Standings (F1) - Event-based workflow with auto-calculated positions
- 3.2 Qualifying Points (Golf/Tennis) - Two-phase scoring with configurable QP values
- 3.3 AFL Finals - Double-chance system with Wildcard Round (10 teams)
- 3.4 Pattern-specific UI Components - PlayoffBracket, SeasonStandings, SportSeasonDisplay components + member route
- ⏳ Phase 4: Standings & Display - IN PROGRESS
- ✅ 4.1 Enhanced Standings Table - COMPLETE (tiebreaker logic, placement breakdowns, standalone page, navigation)
- ⏳ 4.5 League Home Integration - PARTIAL (standings link added, sports season cards pending)
- ⏳ Phase 5: Expected Value - TODO
- ⏳ Phase 6: Polish & Optimization - TODO
Current Focus: Phase 4 - Standings & Display (4.1 complete with standalone page, 4.5 partially complete)
Total Test Count: 371 tests passing ✅