1283 lines
44 KiB
Markdown
1283 lines
44 KiB
Markdown
# 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)
|
||
- **Structure**: Final season standings determine placements
|
||
- **Placement Logic**: Use the sport's official standings (1st through 8th)
|
||
- **Points**: Apply league scoring rules to final positions
|
||
|
||
#### 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):
|
||
1. Number of 1st place finishes
|
||
2. Number of 2nd place finishes
|
||
3. Number of 3rd place finishes
|
||
4. (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**:
|
||
1. Admin enters result in admin section (e.g., "Tiger Woods finished 3rd at Masters")
|
||
2. System determines event type (qualifying event vs final scoring event)
|
||
3. If qualifying: Update participant's QP total, recalculate QP standings
|
||
4. If final scoring: Update participant final placement, recalculate team scores and standings
|
||
5. Changes propagate to all leagues that include affected participants
|
||
|
||
### Implementation Order
|
||
|
||
1. Scoring rules configuration (admin/commissioner UI)
|
||
2. Result entry system (admin UI)
|
||
3. Scoring calculation engine (models/services)
|
||
4. Standings display (league member UI)
|
||
5. Expected value calculation (background job)
|
||
6. 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
```typescript
|
||
// 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
|
||
|
||
/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/seasons/$seasonId/sports/$sportsSeasonId
|
||
- 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)
|
||
|
||
/leagues/$leagueId/seasons/$seasonId/teams/$teamId
|
||
- Detailed team breakdown
|
||
- All picks with their placements and points
|
||
- Breakdown by sport season
|
||
```
|
||
|
||
### Components
|
||
|
||
```typescript
|
||
// 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/components/scoring/EventResultEntry.tsx
|
||
- Admin form to enter event results
|
||
- Different UI for each scoring pattern
|
||
- Playoff bracket input
|
||
- Tournament placement input
|
||
- Season standings input
|
||
|
||
// 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
|
||
1. Database schema updates (add tables and fields)
|
||
2. Scoring configuration UI (season settings page)
|
||
3. Basic scoring calculator functions
|
||
4. Admin result entry UI (simple version)
|
||
|
||
### Phase 2: Playoff Scoring (Single Elimination)
|
||
1. Playoff event creation
|
||
2. Bracket match tracking
|
||
3. Playoff scoring calculation
|
||
4. Basic standings display
|
||
5. Test with NFL/NBA data
|
||
|
||
### Phase 3: Other Scoring Patterns
|
||
1. Season standings (F1)
|
||
2. Qualifying points (Golf/Tennis)
|
||
3. Page playoffs (AFL)
|
||
4. Pattern-specific UI components
|
||
|
||
### Phase 4: Standings & Display
|
||
1. Full standings table with tiebreakers
|
||
2. Movement tracking
|
||
3. Team breakdown pages
|
||
4. Sport season pages with ownership
|
||
5. Bracket visualization
|
||
|
||
### Phase 5: Expected Value
|
||
1. Probability distribution storage
|
||
2. EV calculation engine
|
||
3. Daily update job
|
||
4. Autodraft ranking integration
|
||
5. Projected totals display
|
||
|
||
### Phase 6: Polish & Optimization
|
||
1. Caching strategy
|
||
2. Performance optimization
|
||
3. Historical views
|
||
4. Admin recalculation tools
|
||
5. 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
|
||
|
||
- [x] **1.1** Update database schema ✅ *Completed*
|
||
- [x] Add 8 point fields to `seasons` table
|
||
- [x] Add `scoringPattern` enum to replace/augment `scoringType`
|
||
- [x] Add `totalMajors`, `majorsCompleted`, `qualifyingPointsFinalized` to `sportsSeasons`
|
||
- [x] Create `scoring_events` table
|
||
- [x] Create `event_results` table
|
||
- [x] Create `playoff_matches` table
|
||
- [x] Create `participant_qualifying_totals` table
|
||
- [x] Create `team_sport_scores` table
|
||
- [x] Create `team_standings` table
|
||
- [x] Create `team_standings_snapshots` table (for daily tracking)
|
||
- [x] Create `participant_expected_values` table
|
||
- [x] Create `participant_season_results` table (for F1 current points)
|
||
- [x] Create `qualifying_point_config` table (configurable QP values)
|
||
- [x] Add all relations for new tables
|
||
- [x] Run migration
|
||
- [x] Update test fixtures with new scoring fields
|
||
|
||
- [x] **1.2** Create model functions (basic structure) ✅ *Completed*
|
||
- [x] `app/models/scoring-rules.ts` - Get/update season scoring config
|
||
- [x] `app/models/scoring-event.ts` - CRUD for scoring events
|
||
- [x] `app/models/event-result.ts` - Record event results
|
||
- [x] `app/models/scoring-calculator.ts` - Calculate points and standings (core logic placeholder)
|
||
- [x] `app/models/standings.ts` - Get standings and team breakdowns
|
||
- [x] Add draftPicks relations to schema
|
||
- [x] All TypeScript compilation passes
|
||
|
||
- [x] **1.3** Update season settings UI ✅ *Completed*
|
||
- [x] Add scoring rules editor component (ScoringRulesEditor.tsx)
|
||
- [x] Add 8 input fields for placement points with emojis
|
||
- [x] Update league settings form to include scoring rules
|
||
- [x] Update league creation form to include scoring rules
|
||
- [x] Add validation for point values (0-1000 range)
|
||
- [x] Disable editing after draft starts (settings page only)
|
||
- [x] Show helpful tips and preview
|
||
|
||
- [x] **1.4** Basic admin result entry UI ✅ *Completed*
|
||
- [x] Create admin route structure for sports seasons
|
||
- [x] List scoring events for a sports season
|
||
- [x] Create new event form (playoff_game, major_tournament, race, final_standings)
|
||
- [x] Basic result entry form (placement only)
|
||
- [x] Update existing results (inline editing with Enter key support)
|
||
- [x] Delete results with confirmation
|
||
- [x] Delete scoring events with confirmation
|
||
- [x] Proper .server.ts file separation for client/server code
|
||
- [x] 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
|
||
|
||
- [x] **2.1** Playoff event creation ✅
|
||
- [x] UI for creating playoff rounds (QF, SF, Finals)
|
||
- [x] Bracket structure definition (4-team and 8-team brackets)
|
||
- [x] Match creation (automatic from bracket structure)
|
||
- [x] Created playoff-match model with bracket generation
|
||
- [x] Created bracket UI route at `/admin/sports-seasons/:id/events/:eventId/bracket`
|
||
|
||
- [x] **2.2** Bracket match tracking ✅
|
||
- [x] UI for entering match results (winner/loser)
|
||
- [x] Display bracket structure grouped by round
|
||
- [x] Show matchup participants with TBD placeholders
|
||
- [x] Automatic winner advancement to next round
|
||
- [x] Added "Manage Bracket" button to event details page
|
||
|
||
- [x] **2.3** Playoff scoring calculation ✅
|
||
- [x] Implement placement sharing logic (Q19)
|
||
- [x] Calculate averaged points for shared placements
|
||
- [x] Handle multi-way ties (4-way for QF losers, 2-way for SF losers)
|
||
- [x] Update `participantResults` with final placements
|
||
- [x] Implement 0-point recording for early eliminations (Q20)
|
||
- [x] Created `processPlayoffEvent` function in scoring-calculator
|
||
- [x] Support for Quarterfinals (5th-8th shared), Semifinals (3rd-4th shared), Finals (1st, 2nd)
|
||
|
||
- [x] **2.4** Basic standings display ✅
|
||
- [x] Create `StandingsTable` component
|
||
- [x] Show total points, rank, movement indicators
|
||
- [x] Show participants remaining count
|
||
- [x] Display placement breakdown (1st-8th counts) for tiebreakers
|
||
- [x] Rank badges with trophy/medal icons for top 3
|
||
- [x] League home page standings integration (component ready, route integration pending)
|
||
|
||
- [x] **2.5** Testing with real data ✅
|
||
- [x] Created comprehensive test suite (18 tests)
|
||
- [x] Test NFL playoff scenarios (8-team bracket)
|
||
- [x] Test NCAA March Madness Elite Eight scenarios
|
||
- [x] Verify point calculations for all rounds
|
||
- [x] Test tie scenarios (2-way, 4-way, edge cases)
|
||
- [x] 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
|
||
|
||
```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?
|
||
}
|
||
```
|
||
|
||
#### 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
|
||
|
||
```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", "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
|
||
|
||
- [x] **2.6** Template System Foundation ✅
|
||
- [x] Define bracket template TypeScript types and constants
|
||
- [x] Add `isScoring`, `templateRound`, `seedInfo` to playoff_matches schema
|
||
- [x] Add `bracketTemplateId`, `scoringStartsAtRound` to scoring_events schema
|
||
- [x] Create database migration (drizzle/0023)
|
||
- [x] Update playoff-match model to support templates
|
||
- [x] Create template definitions file with all bracket templates (app/lib/bracket-templates.ts)
|
||
- [x] Add `generateBracketFromTemplate()` function
|
||
- [x] Add `advanceWinnerTemplate()` function for template-based advancement
|
||
- [x] Test suite for template structures (16 tests passing)
|
||
|
||
- [ ] **2.7** Simple Template Expansion (16, 32 teams) ⏭️ **NEXT**
|
||
- [x] Create template definitions for 16 and 32 team brackets (already done in 2.6)
|
||
- [ ] Update bracket generation UI to use template selector
|
||
- [ ] Test with 16-team bracket (Round of 16 → QF → SF → Finals)
|
||
- [ ] Test with 32-team bracket (Round of 32 → Round of 16 → QF → SF → Finals)
|
||
- [ ] Update scoring calculator for non-scoring rounds (0 points per Q20)
|
||
- [ ] Verify Elite Eight scoring starts correctly
|
||
|
||
- [ ] **2.8** March Madness Template (68 teams)
|
||
- [ ] Create NCAA_68 template definition
|
||
- [ ] Build First Four matchup assignment UI (4 play-in games)
|
||
- [ ] Build Round of 64 with TBD slot handling (winners from First Four)
|
||
- [ ] Implement winner advancement from First Four to Round of 64
|
||
- [ ] Test complete 68-team bracket flow
|
||
- [ ] Verify only Elite Eight and beyond score points
|
||
- [ ] Test early elimination = 0 points (per Q20)
|
||
|
||
- [ ] **2.9** NFL/NBA Templates
|
||
- [ ] Create NFL_14 template (with bye weeks)
|
||
- [ ] Create NBA_16 template (with conference structure)
|
||
- [ ] Handle bye week logic (teams skip Wild Card round)
|
||
- [ ] Test NFL playoff bracket
|
||
- [ ] Test NBA playoff bracket
|
||
- [ ] Verify scoring starts at correct round for each template
|
||
|
||
---
|
||
|
||
### Phase 3: Other Scoring Patterns
|
||
**Goal**: Implement season standings (F1) and qualifying points (Golf/Tennis)
|
||
|
||
- [ ] **3.1** Season standings (F1)
|
||
- [ ] UI for entering current season points
|
||
- [ ] Store running totals in `participant_season_results`
|
||
- [ ] Display current standings on sport season page
|
||
- [ ] Final standings entry (end of season)
|
||
- [ ] Convert final standings to fantasy placements
|
||
|
||
- [ ] **3.2** Qualifying points (Golf/Tennis)
|
||
- [ ] QP value configuration per sports season (Q21)
|
||
- [ ] UI for entering major tournament results
|
||
- [ ] Calculate and store QP totals
|
||
- [ ] Display QP standings
|
||
- [ ] Show projected fantasy points based on current QP (Q6)
|
||
- [ ] Manual finalization button (Q4)
|
||
- [ ] Handle QP ties with shared placements (Q5)
|
||
|
||
- [ ] **3.3** Page playoffs (AFL)
|
||
- [ ] Research AFL page playoff structure
|
||
- [ ] Implement bracket logic for all 8 placements
|
||
- [ ] UI for entering page playoff results
|
||
- [ ] Test with sample data
|
||
|
||
- [ ] **3.4** Pattern-specific UI components
|
||
- [ ] `PlayoffBracket` component with ownership hints (Q15)
|
||
- [ ] `QualifyingPointsStandings` component
|
||
- [ ] `SeasonStandings` component for F1
|
||
- [ ] Pattern detection and appropriate display
|
||
|
||
### Phase 4: Standings & Display
|
||
**Goal**: Full-featured standings with breakdowns and historical views
|
||
|
||
- [ ] **4.1** Enhanced standings table
|
||
- [ ] Implement tiebreaker logic (placement counts)
|
||
- [ ] Add sorting by tiebreakers
|
||
- [ ] Display placement breakdown per team
|
||
|
||
- [ ] **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
|
||
- [ ] `TeamScoreBreakdown` component
|
||
- [ ] 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
|
||
- [ ] Create sport season detail route
|
||
- [ ] Display all participants for that sport
|
||
- [ ] Show current results/standings
|
||
- [ ] Ownership indicators with avatars + tooltips (Q11, Q15)
|
||
- [ ] Bracket display for playoff sports (Q3)
|
||
|
||
- [ ] **4.5** 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 `calculateExpectedValue` function
|
||
- [ ] League-specific calculation based on scoring rules
|
||
- [ ] Batch calculation for all participants in a season
|
||
|
||
- [ ] **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
|
||
|
||
---
|
||
|
||
**STATUS: READY TO BEGIN IMPLEMENTATION** ✅
|
||
|
||
All planning complete. Starting with Phase 1: Core Scoring Infrastructure.
|