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: