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