273 lines
11 KiB
Markdown
273 lines
11 KiB
Markdown
|
|
# 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: <https://www.nll.com/playoff-bracket/>
|
||
|
|
- Official 2026 playoff schedule/format announcement: <https://www.nll.com/news/national-lacrosse-league-unveils-2026-playoff-schedule-and-quarterfinal-matchups/>
|
||
|
|
- Official unified standings/playoff structure announcement: <https://www.nll.com/news/nll-adopts-unified-standings-format-and-updated-playoff-structure-for-the-2023-24-season/>
|
||
|
|
- Official NLL standings page: <https://www.nll.com/standings/>
|
||
|
|
|
||
|
|
## 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.
|