From 45dc10164e872510683020ad682a5aeb9cb674d5 Mon Sep 17 00:00:00 2001 From: Chris Parsons <438676+chrisparsons83@users.noreply.github.com> Date: Tue, 12 May 2026 11:38:55 -0700 Subject: [PATCH] docs: plan NLL preseason simulator --- .../nll-simulator-implementation-plan.md | 272 ++++++++++++++++++ docs/agents/simulators.md | 12 + 2 files changed, 284 insertions(+) create mode 100644 docs/agents/nll-simulator-implementation-plan.md diff --git a/docs/agents/nll-simulator-implementation-plan.md b/docs/agents/nll-simulator-implementation-plan.md new file mode 100644 index 0000000..3dfa70a --- /dev/null +++ b/docs/agents/nll-simulator-implementation-plan.md @@ -0,0 +1,272 @@ +# 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: + +- Official NLL playoff bracket page: +- Official 2026 playoff schedule/format announcement: +- Official unified standings/playoff structure announcement: +- Official NLL standings page: + +## 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: + +```ts +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`: + +```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: + +```ts +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: + +```ts +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: + +```bash +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. diff --git a/docs/agents/simulators.md b/docs/agents/simulators.md index 2d6e456..2da6d03 100644 --- a/docs/agents/simulators.md +++ b/docs/agents/simulators.md @@ -23,6 +23,18 @@ Before running older Elo-based simulators, the runner materializes resolved Elo The runner also materializes derived `rating` values for rating-based simulators that declare that support, such as preseason NCAAM/NCAAW using futures odds before KenPom/Barttorvik-style ratings are available. +## Preseason Draftability + +Brackt sports are expected to be draftable before their real-world season starts. When adding or revising a team-sport simulator, design for preseason and in-season runs first: + +- Include all draftable teams/participants, not only the eventual playoff field. +- Simulate regular-season qualification or standings when playoff participation is not known yet. +- Let current standings override or blend with preseason projections as the season progresses. +- Use projected wins, projected table points, futures odds, direct ratings, or sport-specific ratings as season-scoped inputs that can drive preseason EVs. +- Switch to bracket-aware simulation only when a real bracket or finalized seeds exist. + +A playoff-only simulator is not sufficient for team sports that Brackt drafts in preseason. If a sport ever has a true exception, that exception must be explicit in the sport-specific implementation plan and product requirements. + ## Season Scoping Never store mutable simulator state on `sports`. A sport can have multiple active seasons at once, such as NHL playoffs for one season and preseason drafting for the next. Each sports season needs independent: