brackt/docs/agents/nll-simulator-implementation-plan.md
Chris Parsons 5b261bf258
Add NLL box lacrosse simulator implementation plan and preseason-draftability guidance (#417)
* docs: plan NLL preseason simulator

* Add NLL Season + Playoffs Monte Carlo simulator

14-team regular season (18 games) projects top-8 via Elo + decaying
projectedWins prior (adjusts for games already played). Playoff bracket:
QF single-game (1v8, 2v7, 3v6, 4v5), SF and Finals best-of-3. Three
runtime modes: bracket-aware → known-seed → regular-season projection.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12 14:22:34 -07:00

11 KiB

NLL Box Lacrosse Simulator Implementation Plan

Goal

Implement National Lacrosse League (NLL) box lacrosse as a preseason-draftable sport. Brackt leagues should be able to draft NLL teams before the regular season starts, while the simulator projects both:

  1. the 18-game regular season and top-eight playoff qualification; and
  2. the NLL playoff bracket: single-elimination Quarterfinals followed by best-of-three Semifinals and best-of-three NLL Finals.

This is not a playoff-only MVP. The simulator must support all NLL teams in preseason and in-season states, then converge naturally to bracket-only behavior once the playoff field is known.

Current NLL format assumptions

Use these assumptions until the league changes format or an admin overrides season config:

  • NLL uses unified single-table standings.
  • The league has 14 franchises.
  • Each team plays an 18-game regular season.
  • Top eight teams qualify for the playoffs.
  • Quarterfinal matchups are 1 vs 8, 2 vs 7, 3 vs 6, and 4 vs 5.
  • Quarterfinals are single elimination.
  • Semifinals and NLL Finals are best-of-three series.
  • Bracket arms should be fixed as (1 vs 8) winner vs (4 vs 5) winner and (2 vs 7) winner vs (3 vs 6) winner, matching the published NLL bracket layout.

References checked May 12, 2026:

Architecture fit

Follow the simulator guide:

  • Add a dedicated nll_bracket simulatorType.
  • Keep mutable season inputs in season_participant_simulator_inputs; do not hardcode current teams, current standings, odds, seeds, or ratings in the simulator.
  • Query simulator config and inputs through the simulator model/runner pathways already used by admin routes.
  • Use the existing input-policy system so direct Elo can be derived from projected wins or futures odds.
  • Return the standard top-eight probability columns used by expected values.

Data model and config changes

Simulator type

Add nll_bracket in all simulator type locations:

  • database/schema.ts simulatorTypeEnum
  • app/services/simulations/registry.ts SIMULATOR_TYPES
  • app/services/simulations/registry.ts registry entry
  • app/services/simulations/manifest.ts profile map
  • app/services/simulations/simulator-config.ts projected-wins config

Generate the enum migration with npm run db:generate; never hand-write migration SQL.

Manifest profile

Recommended profile:

nll_bracket: {
  defaultConfig: {
    iterations: 50_000,
    seasonGames: 18,
    parityFactor: 400,
    bracketSize: 8,
    playoffTeams: 8,
    regularSeasonTeamCount: 14,
    regularSeasonMode: "project_remaining_games",
    regularSeasonNoise: 0.9,
    homeFieldElo: 0,
  },
  requiredInputs: ["sourceElo"],
  optionalInputs: ["projectedWins", "sourceOdds", "seed"],
  derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
  setupSections: ["participants", "eloRatings", "regularStandings", "bracket"],
}

projectedWins is the preferred preseason admin input. Direct sourceElo should override it, and futures odds can remain a fallback/alternative.

Simulator config

Add nll_bracket to app/services/simulations/simulator-config.ts:

nll_bracket: {
  seasonGames: 18,
  parityFactor: 400,
  averageOpponentElo: 1500,
}

This enables generic projected-wins-to-Elo handling in admin setup.

Simulator algorithm

Create app/services/simulations/nll-simulator.ts with an NLLSimulator implements Simulator.

Inputs loaded per run

Load:

  • all seasonParticipants for the sports season;
  • resolved sourceElo values from simulator inputs / EV compatibility bridge;
  • optional projectedWins for preseason regular-season projection;
  • optional seed values for known playoff seeds;
  • regular-season standings from the existing regular standings table when available;
  • playoff bracket event/matches/games when available.

Fail readiness through the manifest/input policy if required Elo cannot be resolved for every participant that can affect the playoff field. Do not silently assign Elo unless the season config explicitly enables an input-policy fallback.

Mode selection

The simulator should choose the most complete available state:

  1. Bracket-aware mode: if a populated NLL playoff bracket exists, honor it.
    • Use completed match winners/losers.
    • Use completed game rows inside best-of-three matches when present.
    • Simulate unresolved games/matches.
  2. Known-seed mode: if no bracket exists but eight teams have seeds 1-8, simulate the playoff bracket directly from those seeds.
  3. Regular-season projection mode: otherwise, simulate the full regular season each iteration, derive top-eight seeds, then simulate playoffs.

Regular-season projection mode is required for preseason draftability.

Regular-season projection mode

For each Monte Carlo iteration:

  1. Start each team from current standings if present:
    • wins, losses, and gamesPlayed from the regular standings table;
    • if standings are absent, start all teams at 0-0.
  2. Determine remaining games:
    • remainingGames = max(0, seasonGames - gamesPlayed).
  3. Convert Elo to game win probability against an average opponent:
    • p = 1 / (1 + 10 ** ((averageOpponentElo - teamElo) / parityFactor)).
  4. Project additional wins:
    • preseason/default option: sample remainingGames Bernoulli trials at p;
    • if projectedWins exists, calibrate the team's mean final wins toward that projection by blending Elo-derived win rate with projected-win rate;
    • preserve current wins as a floor, so projected final wins cannot fall below already-earned wins.
  5. Rank all teams by simulated final wins, with random tiebreaker for teams tied on wins.
  6. Take the top eight as seeds 1-8.
  7. Simulate the playoff bracket for those seeds.

Recommended projection blend for phase one:

projectedRate = projectedWins / seasonGames
eloRate = eloGameWinProbability(teamElo, averageOpponentElo, parityFactor)
regularSeasonWinRate = projectedWins == null
  ? eloRate
  : 0.65 * projectedRate + 0.35 * eloRate

Keep 0.65 configurable as projectedWinsWeight if added to defaultConfig.

Playoff simulation

For playoff games, use team-vs-team Elo probability:

pA = 1 / (1 + 10 ** ((eloB - eloA) / parityFactor))

Then simulate:

  • Quarterfinals: one game.
  • Semifinals: best of three, first team to two wins.
  • Finals: best of three, first team to two wins.

Export small helpers for testability:

  • nllGameWinProbability(eloA, eloB, parityFactor)
  • simulateNllGame(teamA, teamB, parityFactor)
  • simulateBestOfThree(teamA, teamB, parityFactor, existingWins?)
  • buildNllBracketFromSeeds(seedEntries)
  • simulateNllPlayoffs(seedEntries, options)
  • simulateRegularSeasonSeeds(teams, options)

Completed results and partial series

When a playoff bracket exists:

  • A completed match with winnerId should be deterministic.
  • For best-of-three matches, completed playoff_match_games rows should set current series wins.
  • If a team already has two series wins, treat the series as complete even if playoffMatches.isComplete has not yet been toggled.
  • If only one game is complete, simulate the remaining game(s) from the current 1-0 state.

Probability mapping

Map each iteration to Brackt's standard top-eight shape:

  • champion: probFirst
  • finalist: probSecond
  • two semifinal losers: split evenly across probThird and probFourth
  • four quarterfinal losers: split evenly across probFifth, probSixth, probSeventh, probEighth
  • non-playoff teams in a given iteration receive no placement count for that iteration

Across all participants:

  • probFirst sums to 1;
  • probSecond sums to 1;
  • probThird and probFourth each sum to 1;
  • probFifth through probEighth each sum to 1.

Admin and data setup

Preseason setup workflow

  1. Create the NLL sport with simulatorType = "nll_bracket".
  2. Create the NLL sports season with all 14 teams as participants.
  3. Import preseason projectedWins for every team through the generic simulator CSV importer.
  4. Optionally import championship futures odds.
  5. Run the simulator before draft rooms open; all 14 teams should receive EV based on playoff qualification and bracket outcomes.

In-season workflow

  1. Sync or manually update regular standings.
  2. Re-run the simulator as standings change.
  3. projectedWins can remain a preseason prior, but current standings should increasingly dominate as games are played.

Playoff workflow

  1. Once seeds are final, enter seeds or create the bracket.
  2. If bracket exists, the simulator should stop projecting regular-season qualification and use bracket-aware mode.
  3. As playoff games complete, update match/game results and re-run the simulator.

Testing plan

Add app/services/simulations/__tests__/nll-simulator.test.ts.

Required tests:

  • projected-wins-to-Elo readiness works for nll_bracket;
  • regular-season projection can run from a preseason 0-0 state with 14 teams;
  • higher projected wins increase playoff qualification and title probability;
  • only eight teams qualify in each simulated iteration;
  • seed ordering builds fixed bracket arms correctly: (1,8)/(4,5) and (2,7)/(3,6);
  • Quarterfinals are single-game;
  • Semifinals and Finals are best-of-three;
  • partial best-of-three state is honored;
  • completed matches are deterministic;
  • all output probability columns sum correctly;
  • manifest, registry, and schema enum stay in sync.

Run at minimum:

npm run typecheck
npm run lint
npm run test:run

Implementation task breakdown

  1. Add schema enum and migration for nll_bracket.
  2. Add manifest/config/registry entries.
  3. Implement pure NLL simulator helpers.
  4. Implement regular-season projection mode.
  5. Implement known-seed playoff mode.
  6. Implement bracket-aware mode with completed match/game support.
  7. Add unit tests and readiness/manifest regression coverage.
  8. Seed/create admin data for the NLL sport and current season outside of production code.
  9. Run required checks.
  10. Verify /admin/simulators shows actionable setup state and can run preseason NLL seasons.

Explicit non-goals

  • Do not hardcode current NLL standings, team Elo values, futures odds, or playoff seeds into production simulator code.
  • Do not make NLL playoff-only; preseason draftability requires regular-season simulation.
  • Do not add route-level Drizzle queries for simulator setup; use model/service layers.
  • Do not create migration SQL manually.