brackt/plans/completed/scoring-system.md
Chris Parsons 35fe84a1dd
Update claude.md to push more tests. (#84)
* Update claude.md to push more tests.

* Archiving old plans.

* fix: run DB migrations programmatically on server startup

Replace unreliable drizzle-kit CLI migration with drizzle-orm's built-in
migrator running in the server process before accepting connections. This
ensures migrations are always applied on deploy and fails fast if they error.

- Add runMigrations() to server.ts using drizzle-orm/postgres-js/migrator
- Skip migrations in development (handled manually via db:migrate)
- Add DATABASE_URL guard with a clear error message
- Remove start:production script (now identical to start)
- Update Dockerfile CMD to use npm run start

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-07 22:31:04 -08:00

60 KiB
Raw Permalink 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) 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):

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

  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 Completed

    • 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)
    • Create qualifying_point_config table (configurable QP values)
    • Add all relations for new tables
    • Run migration
    • Update test fixtures with new scoring fields
  • 1.2 Create model functions (basic structure) Completed

    • 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 (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 participantResults with final placements
    • Implement 0-point recording for early eliminations (Q20)
    • Created processPlayoffEvent function in scoring-calculator
    • Support for Quarterfinals (5th-8th shared), Semifinals (3rd-4th shared), Finals (1st, 2nd)
  • 2.4 Basic standings display

    • Create StandingsTable component
    • 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)
  • 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, seedInfo to playoff_matches schema
    • Add bracketTemplateId, scoringStartsAtRound to 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 generateBracketFromTemplate and advanceWinnerTemplate
    • 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
  • 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_results model functions (app/models/participant-season-result.ts)
    • Implement processSeasonStandings function 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_results table
    • 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 triggers processSeasonStandings() which assigns fantasy placements to the top 8 finishers.

  • 3.2 Qualifying points (Golf/Tennis) Completed

    • QP value configuration per sports season (Q21)
      • Created qualifying_point_config table 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
    • 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
      • processQualifyingEvent function 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_totals table tracks running totals and events scored
    • Display QP standings
      • QualifyingPointsStandings component 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
  • 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

    • PlayoffBracket component with ownership hints (Q15)
    • QualifyingPointsStandings component (completed in 3.2)
    • SeasonStandings component for F1
    • SportSeasonDisplay component 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)
    • StandingsTable component 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
    • recalculateStandings function now assigns ranks using tiebreaker logic
    • previousRank tracked 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 Completed

    • Daily snapshot creation (scheduled job)
    • 7-day comparison logic (Q16, Q12)
    • Movement indicators (up/down arrows)
    • Store snapshots for historical analysis (Q16)

    Implementation Notes:

    • Created server/snapshots.ts - automated daily snapshot system (runs once per day)
    • System starts automatically with server initialization in server/socket.ts
    • Updated app/routes/leagues/$leagueId.standings.$seasonId.tsx to use getSevenDayStandingsChange()
    • Created admin interface at /admin/standings-snapshots for manual snapshot management
    • Bulk actions available to create snapshots for all active seasons
    • Movement indicators show green arrows for rank improvements, red arrows for declines
    • Comprehensive test suite with 15 unit tests in app/models/__tests__/standings-snapshots.test.ts
    • All 386 tests passing
  • 4.3 Team breakdown pages Completed

    • TeamScoreBreakdown component
    • List all drafted participants
    • Show placement and points per participant
    • Group by sport season
    • Link to sport season pages

    Implementation Notes:

    • Created TeamScoreBreakdown component (app/components/standings/TeamScoreBreakdown.tsx)
    • Created team breakdown route at /leagues/:leagueId/standings/:seasonId/teams/:teamId
    • Updated getTeamScoreBreakdown model to include sportsSeasonId in grouping
    • Added clickable team name links in StandingsTable component
    • Each sport section includes "View Details →" link to sport season page
    • Displays summary stats: total points, rank badge, participant completion
    • Shows placement breakdown with top finishes highlighted
    • Groups participants by sport with per-sport totals
    • All participants show pick number, round, name, placement badge, and points
    • "Back to Standings" navigation link
    • Comprehensive test suite with 18 tests (all passing)
    • Files created/updated:
      • app/components/standings/TeamScoreBreakdown.tsx (new component)
      • app/routes/leagues/$leagueId.standings.$seasonId.teams.$teamId.tsx (new route)
      • app/components/standings/StandingsTable.tsx (added links)
      • app/models/standings.ts (enhanced bySport grouping)
      • app/routes.ts (added team breakdown route)
      • app/components/standings/tests/TeamScoreBreakdown.test.tsx (new tests)
      • app/components/standings/tests/StandingsTable.test.tsx (updated for router context)
  • 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)

  • 4.5 League home page integration Completed

    • Add navigation link to standings page from league home
    • Display team standings table on league home page (kept separate as standalone page)
    • 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 (via sport season cards)

    Implementation Notes:

    • "View Standings" link in League Info section (app/routes/leagues/$leagueId.tsx)
    • Standalone standings page for cleaner UX
    • Sport season detail pages exist from Phase 3.4
    • Created SportSeasonCard component with status badges (app/components/sports/SportSeasonCard.tsx)
    • Sports Seasons section displays 2-column grid on desktop, single column on mobile
    • Each card shows: sport name, season name, status badge, scoring pattern icon
    • Status badges use color coding: blue=upcoming, green=active, gray=completed
    • Cards link directly to sport season detail pages where brackets/standings are displayed
    • Updated loader to use findCurrentSeasonWithSports for efficient data fetching
  • 4.6 Historical views Completed

    • Season completion detection (isSeasonComplete, getSeasonCompletionPercentage)
    • Point progression chart over time (Q16) - PointProgressionChart component with recharts
    • Final results with tiebreaker details (season completion badge, "Final Standings" title)
    • Comprehensive test suite (15 tests for helpers, chart, and progression logic)

    Implementation Notes:

    • Created app/lib/season-helpers.ts with completion detection utilities
    • Installed recharts library for data visualization
    • Created PointProgressionChart component displaying multi-line chart of team points over time
    • Updated standings page to show:
      • Season completion status badge (✓ Season Complete or X% Complete)
      • Point progression chart when historical snapshot data exists
      • Title changes to "Final Standings" for completed seasons
    • Added getSeasonPointProgression function to format snapshot data for charting
    • Files created/updated:
      • app/lib/season-helpers.ts (new - completion detection)
      • app/components/standings/PointProgressionChart.tsx (new - chart component)
      • app/models/standings.ts (added getSeasonPointProgression)
      • app/routes/leagues/$leagueId.standings.$seasonId.tsx (added chart display)
      • app/models/season.ts (added scoringPattern to SeasonWithSportsSeasons type)
      • app/lib/tests/season-helpers.test.ts (new - 9 tests)
      • app/models/tests/point-progression.test.ts (new - 6 tests)
      • app/components/standings/tests/PointProgressionChart.test.tsx (new - 6 tests)

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

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 - COMPLETE
    • 4.1 Enhanced Standings Table - COMPLETE (tiebreaker logic, placement breakdowns, standalone page, navigation)
    • 4.2 Movement Tracking & Snapshots - COMPLETE (daily snapshots, 7-day comparison, admin interface)
    • 4.3 Team Breakdown Pages - COMPLETE (detailed score breakdown, sport grouping, navigation)
    • 4.4 Sport Season Pages with Ownership - COMPLETE (pattern-specific displays, ownership hints, brackets)
    • 4.5 League Home Integration - COMPLETE (sports season cards, status badges, direct navigation)
    • 4.6 Historical Views - COMPLETE (season completion detection, point progression charts, final standings display)
  • Phase 5: Expected Value - TODO
  • Phase 6: Polish & Optimization - TODO

Current Focus: Phase 4 COMPLETE! Ready to begin Phase 5 - Expected Value calculations

Total Test Count: 425 tests passing

Next Task: Phase 5 - Expected Value System (probability distribution storage, EV calculation engine, autodraft integration)