brackt/plans/scoring-system.md

38 KiB
Raw Blame History

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

// 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

/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

// 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

  • 1.1 Update database schema

    • Add 8 point fields to seasons table
    • Add scoringPattern enum to replace/augment scoringType
    • Add totalMajors, majorsCompleted, qualifyingPointsFinalized to sportsSeasons
    • Create scoring_events table
    • Create event_results table
    • Create playoff_matches table
    • Create participant_qualifying_totals table
    • Create team_sport_scores table
    • Create team_standings table
    • Create team_standings_snapshots table (for daily tracking)
    • Create participant_expected_values table
    • Create participant_season_results table (for F1 current points)
    • Run migration
  • 1.2 Create model functions (basic structure)

    • app/models/scoring-rules.ts - Get/update season scoring config
    • app/models/scoring-event.ts - CRUD for scoring events
    • app/models/event-result.ts - Record event results
    • app/models/scoring-calculator.ts - Calculate points and standings
    • app/models/standings.ts - Get standings and team breakdowns
  • 1.3 Update season settings UI

    • Add scoring rules editor component
    • Add 8 input fields for placement points
    • Update season create/edit forms
    • Add validation for point values
  • 1.4 Basic admin result entry UI

    • Create admin route structure for sports seasons
    • List scoring events for a sports season
    • Create new event form
    • Basic result entry form (placement only)

Phase 2: Playoff Scoring (Single Elimination)

Goal: Implement playoff bracket tracking and scoring for NFL/NBA/MLB

  • 2.1 Playoff event creation

    • UI for creating playoff rounds (QF, SF, Finals)
    • Bracket structure definition
    • Match creation (automatic from bracket structure)
  • 2.2 Bracket match tracking

    • UI for entering match results (winner/loser)
    • Display bracket structure
    • Show matchup participants
  • 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)
    • Update participantResults with final placements
    • Implement 0-point recording for early eliminations (Q20)
  • 2.4 Basic standings display

    • Create StandingsTable component
    • Show total points, rank, movement
    • Show participants remaining count
    • League home page standings integration
  • 2.5 Testing with real data

    • Create test NFL playoff bracket
    • Enter sample results
    • Verify point calculations
    • Test tie scenarios

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.