Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
import { getSimulatorInfo, SIMULATOR_TYPES, type SimulatorType } from "./registry";
|
|
|
|
|
|
|
|
|
|
export type SimulatorInputKey =
|
|
|
|
|
| "sourceOdds"
|
|
|
|
|
| "sourceElo"
|
|
|
|
|
| "worldRanking"
|
|
|
|
|
| "rating"
|
|
|
|
|
| "projectedWins"
|
|
|
|
|
| "projectedTablePoints"
|
|
|
|
|
| "seed"
|
|
|
|
|
| "region"
|
|
|
|
|
| "metadata";
|
|
|
|
|
|
|
|
|
|
export type SimulatorSetupSection =
|
|
|
|
|
| "futuresOdds"
|
|
|
|
|
| "eloRatings"
|
|
|
|
|
| "rankings"
|
|
|
|
|
| "ratings"
|
|
|
|
|
| "regularStandings"
|
|
|
|
|
| "bracket"
|
|
|
|
|
| "events"
|
|
|
|
|
| "golfSkills"
|
|
|
|
|
| "surfaceElo"
|
|
|
|
|
| "cs2Setup"
|
|
|
|
|
| "participants";
|
|
|
|
|
|
|
|
|
|
export interface SimulatorManifestProfile {
|
|
|
|
|
simulatorType: SimulatorType;
|
|
|
|
|
displayName: string;
|
|
|
|
|
description: string;
|
|
|
|
|
defaultConfig: Record<string, unknown>;
|
|
|
|
|
requiredInputs: SimulatorInputKey[];
|
|
|
|
|
optionalInputs: SimulatorInputKey[];
|
|
|
|
|
derivableInputs?: Partial<Record<SimulatorInputKey, SimulatorInputKey[]>>;
|
|
|
|
|
setupSections: SimulatorSetupSection[];
|
|
|
|
|
minParticipantInputs?: number;
|
Make the AFL simulator read the bracket that was actually drawn
An AFL club seeded into an Elimination Final is awarded 15 fantasy points the
moment the bracket is generated — afl_10's entryFloor: 7 — and cannot finish
worse than the 7th-8th tier. Its EV still read 13.
AFLSimulator was stateless with respect to the live bracket. It read only
participants, sourceElo and the regular-season standings, then re-projected the
whole ladder and re-seeded 1-10 from Elo on every one of its 10,000 iterations.
So a team with a locked Elimination Final berth was re-drawn into the Wildcard
Round, or out of the finals entirely, in a slice of them — and there it scores 0.
Even with the ladder complete the Math.random() tiebreaker reshuffled every club
tied on ladder points, which in the AFL is most of the middle of the table.
Games already played were re-played the same way, so a completed Wildcard win was
worth 50% of nothing. Mixing zeroes into a distribution whose floor is 15 is what
produced 13.
afl_10 is the only template that defines entryFloor at all, which is why this
surfaced here and not on LLWS, whose floors only exist once a team has won
something.
The simulator now mirrors llws-simulator's bracket-aware mode:
- readAflBracketSeeds reads the 10 seeds from the slots generateAFL10Bracket
writes them into. The two Elimination Final participant2 slots are TBD by
design and are never read as seeds, leaving exactly 10 named slots. No draw
at all falls back to the ladder projection; a partially seeded, duplicated or
unknown draw throws rather than silently discarding the draw and every
recorded result with it.
- makePlayGame replays a completed match from its recorded result whenever both
recorded teams are the two the simulation routed into that game, so an
already-played result sticks across all iterations.
- simAFLFinals labels each game with the round and match number
generateAFL10Bracket and advanceAFLWinner use, so a result is looked up
against the game it was played in. Its routing was already correct.
EV >= the banked floor now holds by construction, with no clamping: a team seeded
into an Elimination Final is in that game in every iteration. Column sums stay
exactly 1.0 and the 340-point total-EV invariant is unchanged, since teams
outside the bracket simply score nothing.
Fixing the simulator alone would not have held. processMatchResult calls
updateProbabilitiesAfterResult on every result, and its ICM branch re-derives
each still-alive participant's whole distribution from P(1st) alone, knowing
nothing about the bracket — so the next finals result would have put the EV
straight back under the floor. That branch was built for futures-odds seasons.
It now runs only when the season's EVs did not come from a bracket-aware
simulator; when they did, that simulator is re-run instead, since it already
knows the completed matches. Both conditions matter: re-running a bracket-blind
simulator would re-draw the field and hand equity back to knocked-out teams, so
a new manifest `bracketAware` flag limits this to AFL and LLWS. A failed re-run
leaves probabilities untouched rather than falling back to the ICM path that is
being replaced.
The finalized-participant pinning loop now runs after that recalculation rather
than before. A simulation run rewrites every participant in the season, the
finalized ones included; a finalized placement is a fact, not a projection, so it
is written last and wins.
Tests: seeds clear the entry floors their seeding banked, a Qualifying Final
entrant is structurally absent from the 7th-8th tier, the bracket's draw beats
Elo (the weakest club seeded 1, the strongest seeded 10), completed Wildcard and
Qualifying Finals are replayed with the winner banking its floor, teams outside
the bracket are zeroed, and the column sums and 340 total survive. The bracket
fixtures deliberately seed the ten weakest clubs, because seeding the strongest
ten lets the ladder projection reproduce much the same field by accident. Six of
the seven were confirmed to fail against the previous behavior. Plus the
probability-updater branch in each direction, its failure path, and the pin
ordering.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPxDnSEKVoKx9HFcQ6gmcS
2026-08-29 02:42:31 +00:00
|
|
|
/**
|
|
|
|
|
* The simulator reads the season's generated bracket: it uses the real draw as its seeding
|
|
|
|
|
* and replays completed matches from their recorded result, instead of re-drawing the field
|
|
|
|
|
* every iteration.
|
|
|
|
|
*
|
|
|
|
|
* Only such a simulator can be safely re-run once results are in — a bracket-blind one puts
|
|
|
|
|
* knocked-out teams back in contention. updateProbabilitiesAfterResult reads this to decide
|
|
|
|
|
* whether to re-run the simulator or fall back to the generic ICM recalculation.
|
|
|
|
|
*/
|
|
|
|
|
bracketAware?: boolean;
|
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const BASE_CONFIG = {
|
|
|
|
|
iterations: 50_000,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const PROFILES: Record<SimulatorType, Omit<SimulatorManifestProfile, "simulatorType" | "displayName" | "description">> = {
|
|
|
|
|
f1_standings: {
|
|
|
|
|
defaultConfig: { iterations: 10_000, raceNoise: 0.5, participantVolatility: 1.5 },
|
|
|
|
|
requiredInputs: ["sourceOdds"],
|
|
|
|
|
optionalInputs: [],
|
|
|
|
|
setupSections: ["participants", "futuresOdds", "events"],
|
|
|
|
|
},
|
|
|
|
|
indycar_standings: {
|
|
|
|
|
defaultConfig: { iterations: 10_000, raceNoise: 0.5, participantVolatility: 1.5 },
|
|
|
|
|
requiredInputs: ["sourceOdds"],
|
|
|
|
|
optionalInputs: [],
|
|
|
|
|
setupSections: ["participants", "futuresOdds", "events"],
|
|
|
|
|
},
|
|
|
|
|
golf_qualifying_points: {
|
|
|
|
|
defaultConfig: { iterations: 10_000, fieldSize: 156, plBeta: 1.5 },
|
|
|
|
|
requiredInputs: [],
|
|
|
|
|
optionalInputs: ["sourceOdds", "rating"],
|
|
|
|
|
setupSections: ["participants", "events", "golfSkills"],
|
|
|
|
|
},
|
|
|
|
|
playoff_bracket: {
|
|
|
|
|
defaultConfig: { ...BASE_CONFIG },
|
|
|
|
|
requiredInputs: ["sourceOdds"],
|
|
|
|
|
optionalInputs: ["sourceElo"],
|
|
|
|
|
setupSections: ["participants", "futuresOdds", "bracket"],
|
|
|
|
|
},
|
|
|
|
|
ucl_bracket: {
|
2026-06-30 23:24:48 +00:00
|
|
|
defaultConfig: { ...BASE_CONFIG, parityFactor: 400, inputPolicy: { oddsWeight: 0.3 } },
|
2026-06-26 05:16:54 +00:00
|
|
|
requiredInputs: ["sourceElo"],
|
|
|
|
|
optionalInputs: ["sourceOdds"],
|
|
|
|
|
derivableInputs: { sourceElo: ["sourceOdds"] },
|
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
setupSections: ["participants", "futuresOdds", "bracket"],
|
|
|
|
|
},
|
|
|
|
|
ncaam_bracket: {
|
2026-05-12 22:26:25 -07:00
|
|
|
defaultConfig: { ...BASE_CONFIG, ratingScaleFactor: 7.5, inputPolicy: { ratingMin: -10, ratingMax: 35, fallbackRatingDelta: 5 } },
|
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
requiredInputs: ["rating"],
|
|
|
|
|
optionalInputs: ["sourceOdds", "sourceElo", "seed", "region"],
|
|
|
|
|
derivableInputs: { rating: ["sourceOdds"] },
|
|
|
|
|
setupSections: ["participants", "ratings", "futuresOdds", "bracket"],
|
|
|
|
|
},
|
|
|
|
|
ncaaw_bracket: {
|
2026-05-13 01:06:16 -07:00
|
|
|
defaultConfig: { ...BASE_CONFIG, inputPolicy: { ratingMin: 0.70, ratingMax: 0.97, missingRatingStrategy: "worstKnownMinus", fallbackRatingDelta: 0.01 } },
|
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
requiredInputs: ["rating"],
|
|
|
|
|
optionalInputs: ["sourceOdds", "seed", "region"],
|
|
|
|
|
derivableInputs: { rating: ["sourceOdds"] },
|
|
|
|
|
setupSections: ["participants", "ratings", "futuresOdds", "bracket"],
|
|
|
|
|
},
|
|
|
|
|
nba_bracket: {
|
|
|
|
|
defaultConfig: { ...BASE_CONFIG, parityFactor: 400, seasonGames: 82 },
|
|
|
|
|
requiredInputs: ["sourceElo"],
|
|
|
|
|
optionalInputs: ["sourceOdds", "projectedWins"],
|
|
|
|
|
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
|
|
|
|
|
setupSections: ["participants", "eloRatings", "regularStandings", "bracket"],
|
|
|
|
|
},
|
|
|
|
|
nhl_bracket: {
|
|
|
|
|
defaultConfig: { ...BASE_CONFIG, parityFactor: 1000, seasonGames: 82, overtimeRate: 0.23 },
|
|
|
|
|
requiredInputs: ["sourceElo"],
|
|
|
|
|
optionalInputs: ["sourceOdds", "projectedWins"],
|
|
|
|
|
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
|
|
|
|
|
setupSections: ["participants", "eloRatings", "regularStandings", "bracket"],
|
|
|
|
|
},
|
|
|
|
|
nfl_bracket: {
|
|
|
|
|
defaultConfig: { ...BASE_CONFIG, parityFactor: 400, seasonGames: 17, homeFieldElo: 48 },
|
|
|
|
|
requiredInputs: ["sourceElo"],
|
|
|
|
|
optionalInputs: ["sourceOdds", "projectedWins"],
|
|
|
|
|
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
|
|
|
|
|
setupSections: ["participants", "eloRatings", "regularStandings"],
|
|
|
|
|
},
|
|
|
|
|
afl_bracket: {
|
|
|
|
|
defaultConfig: { ...BASE_CONFIG, parityFactor: 450, seasonGames: 23 },
|
|
|
|
|
requiredInputs: ["sourceElo"],
|
|
|
|
|
optionalInputs: ["projectedWins"],
|
|
|
|
|
derivableInputs: { sourceElo: ["projectedWins"] },
|
Make the AFL simulator read the bracket that was actually drawn
An AFL club seeded into an Elimination Final is awarded 15 fantasy points the
moment the bracket is generated — afl_10's entryFloor: 7 — and cannot finish
worse than the 7th-8th tier. Its EV still read 13.
AFLSimulator was stateless with respect to the live bracket. It read only
participants, sourceElo and the regular-season standings, then re-projected the
whole ladder and re-seeded 1-10 from Elo on every one of its 10,000 iterations.
So a team with a locked Elimination Final berth was re-drawn into the Wildcard
Round, or out of the finals entirely, in a slice of them — and there it scores 0.
Even with the ladder complete the Math.random() tiebreaker reshuffled every club
tied on ladder points, which in the AFL is most of the middle of the table.
Games already played were re-played the same way, so a completed Wildcard win was
worth 50% of nothing. Mixing zeroes into a distribution whose floor is 15 is what
produced 13.
afl_10 is the only template that defines entryFloor at all, which is why this
surfaced here and not on LLWS, whose floors only exist once a team has won
something.
The simulator now mirrors llws-simulator's bracket-aware mode:
- readAflBracketSeeds reads the 10 seeds from the slots generateAFL10Bracket
writes them into. The two Elimination Final participant2 slots are TBD by
design and are never read as seeds, leaving exactly 10 named slots. No draw
at all falls back to the ladder projection; a partially seeded, duplicated or
unknown draw throws rather than silently discarding the draw and every
recorded result with it.
- makePlayGame replays a completed match from its recorded result whenever both
recorded teams are the two the simulation routed into that game, so an
already-played result sticks across all iterations.
- simAFLFinals labels each game with the round and match number
generateAFL10Bracket and advanceAFLWinner use, so a result is looked up
against the game it was played in. Its routing was already correct.
EV >= the banked floor now holds by construction, with no clamping: a team seeded
into an Elimination Final is in that game in every iteration. Column sums stay
exactly 1.0 and the 340-point total-EV invariant is unchanged, since teams
outside the bracket simply score nothing.
Fixing the simulator alone would not have held. processMatchResult calls
updateProbabilitiesAfterResult on every result, and its ICM branch re-derives
each still-alive participant's whole distribution from P(1st) alone, knowing
nothing about the bracket — so the next finals result would have put the EV
straight back under the floor. That branch was built for futures-odds seasons.
It now runs only when the season's EVs did not come from a bracket-aware
simulator; when they did, that simulator is re-run instead, since it already
knows the completed matches. Both conditions matter: re-running a bracket-blind
simulator would re-draw the field and hand equity back to knocked-out teams, so
a new manifest `bracketAware` flag limits this to AFL and LLWS. A failed re-run
leaves probabilities untouched rather than falling back to the ICM path that is
being replaced.
The finalized-participant pinning loop now runs after that recalculation rather
than before. A simulation run rewrites every participant in the season, the
finalized ones included; a finalized placement is a fact, not a projection, so it
is written last and wins.
Tests: seeds clear the entry floors their seeding banked, a Qualifying Final
entrant is structurally absent from the 7th-8th tier, the bracket's draw beats
Elo (the weakest club seeded 1, the strongest seeded 10), completed Wildcard and
Qualifying Finals are replayed with the winner banking its floor, teams outside
the bracket are zeroed, and the column sums and 340 total survive. The bracket
fixtures deliberately seed the ten weakest clubs, because seeding the strongest
ten lets the ladder projection reproduce much the same field by accident. Six of
the seven were confirmed to fail against the previous behavior. Plus the
probability-updater branch in each direction, its failure path, and the pin
ordering.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPxDnSEKVoKx9HFcQ6gmcS
2026-08-29 02:42:31 +00:00
|
|
|
// The bracket is optional — before one exists the ladder is projected from Elo — but once
|
|
|
|
|
// it is drawn the simulator seeds from it and honors completed results.
|
|
|
|
|
setupSections: ["participants", "eloRatings", "regularStandings", "bracket"],
|
|
|
|
|
bracketAware: true,
|
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
},
|
|
|
|
|
epl_standings: {
|
2026-05-13 15:02:23 -07:00
|
|
|
defaultConfig: {
|
|
|
|
|
...BASE_CONFIG,
|
2026-05-31 17:39:43 +00:00
|
|
|
iterations: 10_000,
|
2026-05-13 15:02:23 -07:00
|
|
|
seasonGames: 38,
|
|
|
|
|
parityFactor: 400,
|
|
|
|
|
matchParityFactor: 400,
|
|
|
|
|
averageOpponentElo: 1500,
|
|
|
|
|
baseDrawRate: 0.26,
|
|
|
|
|
drawDecay: 0.002,
|
|
|
|
|
},
|
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
requiredInputs: ["sourceElo"],
|
|
|
|
|
optionalInputs: ["sourceOdds", "projectedTablePoints"],
|
|
|
|
|
derivableInputs: { sourceElo: ["projectedTablePoints", "sourceOdds"] },
|
|
|
|
|
setupSections: ["participants", "eloRatings", "regularStandings"],
|
|
|
|
|
},
|
|
|
|
|
snooker_bracket: {
|
|
|
|
|
defaultConfig: { ...BASE_CONFIG, eloDivisor: 400 },
|
|
|
|
|
requiredInputs: ["sourceElo"],
|
|
|
|
|
optionalInputs: ["worldRanking", "seed"],
|
|
|
|
|
setupSections: ["participants", "eloRatings", "rankings", "bracket"],
|
|
|
|
|
},
|
|
|
|
|
tennis_qualifying_points: {
|
|
|
|
|
defaultConfig: { iterations: 10_000, eloDivisor: 400, fallbackElo: 1500 },
|
|
|
|
|
requiredInputs: [],
|
|
|
|
|
optionalInputs: ["worldRanking"],
|
|
|
|
|
setupSections: ["participants", "surfaceElo", "events"],
|
|
|
|
|
},
|
|
|
|
|
mlb_bracket: {
|
2026-06-26 05:16:54 +00:00
|
|
|
defaultConfig: { ...BASE_CONFIG, seasonGames: 162, inputPolicy: { oddsWeight: 0.3 } },
|
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
requiredInputs: ["sourceElo"],
|
|
|
|
|
optionalInputs: ["sourceOdds", "projectedWins"],
|
|
|
|
|
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
|
|
|
|
|
setupSections: ["participants", "eloRatings", "regularStandings"],
|
|
|
|
|
},
|
|
|
|
|
wnba_bracket: {
|
2026-06-30 23:24:48 +00:00
|
|
|
defaultConfig: { ...BASE_CONFIG, parityFactor: 400, seasonGames: 44, srsEloScale: 30 },
|
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
requiredInputs: ["sourceElo"],
|
|
|
|
|
optionalInputs: ["sourceOdds", "projectedWins"],
|
|
|
|
|
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
|
|
|
|
|
setupSections: ["participants", "eloRatings", "regularStandings"],
|
|
|
|
|
},
|
|
|
|
|
world_cup: {
|
2026-06-26 05:16:54 +00:00
|
|
|
defaultConfig: { ...BASE_CONFIG, iterations: 10_000, inputPolicy: { oddsWeight: 0.3 } },
|
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
requiredInputs: ["sourceElo"],
|
|
|
|
|
optionalInputs: ["sourceOdds", "worldRanking"],
|
|
|
|
|
derivableInputs: { sourceElo: ["sourceOdds"] },
|
|
|
|
|
setupSections: ["participants", "eloRatings", "futuresOdds", "events"],
|
|
|
|
|
},
|
|
|
|
|
darts_bracket: {
|
2026-05-31 17:39:43 +00:00
|
|
|
defaultConfig: { ...BASE_CONFIG, iterations: 10_000, eloDivisor: 400 },
|
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
requiredInputs: ["sourceElo", "worldRanking"],
|
|
|
|
|
optionalInputs: ["seed"],
|
|
|
|
|
setupSections: ["participants", "eloRatings", "rankings"],
|
|
|
|
|
},
|
|
|
|
|
cs2_major_qualifying_points: {
|
|
|
|
|
defaultConfig: { iterations: 10_000, fieldSize: 32, guaranteedCount: 12 },
|
|
|
|
|
requiredInputs: ["sourceElo"],
|
|
|
|
|
optionalInputs: ["worldRanking", "metadata"],
|
|
|
|
|
setupSections: ["participants", "eloRatings", "rankings", "cs2Setup", "events"],
|
|
|
|
|
},
|
|
|
|
|
ncaa_football_bracket: {
|
2026-06-30 23:24:48 +00:00
|
|
|
defaultConfig: { ...BASE_CONFIG, parityFactor: 400, bracketSize: 12, inputPolicy: { oddsWeight: 0.4 } },
|
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
requiredInputs: ["sourceElo"],
|
|
|
|
|
optionalInputs: ["sourceOdds", "worldRanking"],
|
|
|
|
|
derivableInputs: { sourceElo: ["sourceOdds"] },
|
|
|
|
|
setupSections: ["participants", "eloRatings", "futuresOdds", "bracket"],
|
|
|
|
|
},
|
|
|
|
|
llws_bracket: {
|
Address review findings in LLWS simulator
Four fixes from code review of the previous commit.
1. Preserve the futures board's dispersion (llws-simulator.ts).
convertFuturesToElo finishes by rescaling any field onto a fixed
1250-1750 Elo span, discarding how spread out the board actually is: a
board with a 22%-priced favorite and one with a 6%-priced favorite both
came out 500 Elo wide. On a tight board that inflated the favorite from
6% to 13% -- worse than the raw-futures model it replaced (RMSE 0.025 vs
0.005), so the previous commit was a regression in that regime.
buildLLWSElos now maps decompressed strengths by their log-ratio to the
field's geometric mean, so Elo span tracks real dispersion. Re-swept the
parity factor across three board shapes rather than one: 550 minimizes
total error. Simulated vs priced favorite, with RMSE:
wide 21.8% -> 20.7% (0.0055), elo span 1355-1682
top-heavy 28.2% -> 25.6% (0.0085), elo span 1354-1733
tight 6.0% -> 5.8% (0.0026), elo span 1484-1518
2. Rate an unpriced team at the field's median, not 1500.
DEFAULT_ELO is the midpoint of the Elo output range, not of the field;
on a typical board it ranked an unpriced team ~6th of 20, so blanking a
team's odds promoted it. buildLLWSElos now returns the priced field's
median alongside the ratings (ranks 11th of 20 on the same board).
3. Pick the bracket event deterministically.
scoringEvents.findFirst with no ordering returned an arbitrary row when a
season had more than one llws_20 playoff event; landing on a stale one
silently reverted to a randomized draw that ignored all recorded results.
Now takes the most recent, matching world-cup-simulator.ts.
4. Fail on a partially seeded bracket instead of discarding it.
readBracketSlots returned null on any single missing participant, throwing
away the draw and every completed result with no warning. Since
playoff_matches participant columns are ON DELETE SET NULL, removing and
re-adding one participant mid-tournament was enough to put eliminated
teams back in contention. It now distinguishes "generated but not seeded"
(0 slots filled -> randomized draw) from "partially seeded" (throws).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5vQZMPeokzfMqHQjq1RDZ
2026-08-21 22:08:04 +00:00
|
|
|
defaultConfig: { ...BASE_CONFIG, parityFactor: 550, usTeamCount: 10, internationalTeamCount: 10 },
|
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
requiredInputs: ["sourceOdds"],
|
|
|
|
|
optionalInputs: ["metadata"],
|
Make LLWS simulator bracket-aware and calibrate its futures model
The LLWS simulator was overestimating favorites and ignoring games that had
already been played. Two separate causes:
1. Championship futures were used directly as single-game strength
(p1 / (p1 + p2)). A future already compounds the ~6 wins needed to take
the title, so this made every individual game as lopsided as the whole
tournament and re-compounded that edge round after round. Against a
representative 20-team board the favorite priced at 21.8% simulated at
44.9%, and the longest shot fell to ~0%.
Futures are now decompressed to single-game Elo via convertFuturesToElo,
the same pipeline the other bracket simulators use, and games are played
with eloWinProbabilityWithParity. The parity factor was calibrated by
sweeping it until a randomized-draw simulation reproduces the board it
was fed: at 1000 the favorite simulates at 21.8% and field-wide RMSE
drops from 0.062 to 0.003. It is overridable per season via config.
2. The simulator never read playoff_matches, so it re-ran the tournament
from an empty bracket every time and shuffled the draw at random each
iteration. A recorded loss changed nothing.
It now loads the seeded llws_20 bracket, places teams in their real
slots, and replays completed games from their recorded result instead of
re-simulating them, so an eliminated team correctly drops to zero. When
no bracket exists (or it has no participants seeded) it falls back to the
previous randomized-draw behavior, and a seeded bracket is authoritative
about which side a team is on, so externalId is only required on the
pre-bracket path.
Guards: a recorded result is only honored when its two participants are the
ones the simulation routed into that game, so a corrupt or out-of-order row
cannot desynchronize the rest of the bracket; brackets seeding an unknown or
duplicated participant now fail loudly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01X5vQZMPeokzfMqHQjq1RDZ
2026-08-21 21:09:47 +00:00
|
|
|
// The bracket is optional — without one the draw is randomized — but once it
|
|
|
|
|
// exists the simulator reads the real draw and honors completed results from it.
|
|
|
|
|
setupSections: ["participants", "futuresOdds", "bracket"],
|
Make the AFL simulator read the bracket that was actually drawn
An AFL club seeded into an Elimination Final is awarded 15 fantasy points the
moment the bracket is generated — afl_10's entryFloor: 7 — and cannot finish
worse than the 7th-8th tier. Its EV still read 13.
AFLSimulator was stateless with respect to the live bracket. It read only
participants, sourceElo and the regular-season standings, then re-projected the
whole ladder and re-seeded 1-10 from Elo on every one of its 10,000 iterations.
So a team with a locked Elimination Final berth was re-drawn into the Wildcard
Round, or out of the finals entirely, in a slice of them — and there it scores 0.
Even with the ladder complete the Math.random() tiebreaker reshuffled every club
tied on ladder points, which in the AFL is most of the middle of the table.
Games already played were re-played the same way, so a completed Wildcard win was
worth 50% of nothing. Mixing zeroes into a distribution whose floor is 15 is what
produced 13.
afl_10 is the only template that defines entryFloor at all, which is why this
surfaced here and not on LLWS, whose floors only exist once a team has won
something.
The simulator now mirrors llws-simulator's bracket-aware mode:
- readAflBracketSeeds reads the 10 seeds from the slots generateAFL10Bracket
writes them into. The two Elimination Final participant2 slots are TBD by
design and are never read as seeds, leaving exactly 10 named slots. No draw
at all falls back to the ladder projection; a partially seeded, duplicated or
unknown draw throws rather than silently discarding the draw and every
recorded result with it.
- makePlayGame replays a completed match from its recorded result whenever both
recorded teams are the two the simulation routed into that game, so an
already-played result sticks across all iterations.
- simAFLFinals labels each game with the round and match number
generateAFL10Bracket and advanceAFLWinner use, so a result is looked up
against the game it was played in. Its routing was already correct.
EV >= the banked floor now holds by construction, with no clamping: a team seeded
into an Elimination Final is in that game in every iteration. Column sums stay
exactly 1.0 and the 340-point total-EV invariant is unchanged, since teams
outside the bracket simply score nothing.
Fixing the simulator alone would not have held. processMatchResult calls
updateProbabilitiesAfterResult on every result, and its ICM branch re-derives
each still-alive participant's whole distribution from P(1st) alone, knowing
nothing about the bracket — so the next finals result would have put the EV
straight back under the floor. That branch was built for futures-odds seasons.
It now runs only when the season's EVs did not come from a bracket-aware
simulator; when they did, that simulator is re-run instead, since it already
knows the completed matches. Both conditions matter: re-running a bracket-blind
simulator would re-draw the field and hand equity back to knocked-out teams, so
a new manifest `bracketAware` flag limits this to AFL and LLWS. A failed re-run
leaves probabilities untouched rather than falling back to the ICM path that is
being replaced.
The finalized-participant pinning loop now runs after that recalculation rather
than before. A simulation run rewrites every participant in the season, the
finalized ones included; a finalized placement is a fact, not a projection, so it
is written last and wins.
Tests: seeds clear the entry floors their seeding banked, a Qualifying Final
entrant is structurally absent from the 7th-8th tier, the bracket's draw beats
Elo (the weakest club seeded 1, the strongest seeded 10), completed Wildcard and
Qualifying Finals are replayed with the winner banking its floor, teams outside
the bracket are zeroed, and the column sums and 340 total survive. The bracket
fixtures deliberately seed the ten weakest clubs, because seeding the strongest
ten lets the ladder projection reproduce much the same field by accident. Six of
the seven were confirmed to fail against the previous behavior. Plus the
probability-updater branch in each direction, its failure path, and the pin
ordering.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VPxDnSEKVoKx9HFcQ6gmcS
2026-08-29 02:42:31 +00:00
|
|
|
bracketAware: true,
|
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
},
|
|
|
|
|
college_hockey_bracket: {
|
2026-06-26 05:16:54 +00:00
|
|
|
// College hockey blends odds into Elo internally (and also uses NPI rank,
|
|
|
|
|
// which the central resolver can't represent). oddsWeight 0 makes the central
|
|
|
|
|
// resolver leave a present base Elo untouched (no double-count), while still
|
|
|
|
|
// letting odds resolve a participant's Elo when they are the only source — so
|
|
|
|
|
// readiness and the sim's own internal odds blend both keep working.
|
|
|
|
|
defaultConfig: { ...BASE_CONFIG, bracketSize: 16, parityFactor: 850, inputPolicy: { oddsWeight: 0 } },
|
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
requiredInputs: ["sourceElo"],
|
|
|
|
|
optionalInputs: ["sourceOdds", "worldRanking"],
|
|
|
|
|
derivableInputs: { sourceElo: ["sourceOdds"] },
|
|
|
|
|
setupSections: ["participants", "eloRatings", "rankings", "futuresOdds", "bracket"],
|
|
|
|
|
},
|
|
|
|
|
brackt: {
|
|
|
|
|
defaultConfig: { iterations: 20_000 },
|
|
|
|
|
requiredInputs: [],
|
|
|
|
|
optionalInputs: [],
|
|
|
|
|
setupSections: ["participants"],
|
|
|
|
|
},
|
2026-05-12 14:22:34 -07:00
|
|
|
nll_bracket: {
|
|
|
|
|
defaultConfig: {
|
|
|
|
|
...BASE_CONFIG,
|
|
|
|
|
seasonGames: 18,
|
|
|
|
|
parityFactor: 400,
|
|
|
|
|
bracketSize: 8,
|
|
|
|
|
playoffTeams: 8,
|
|
|
|
|
regularSeasonTeamCount: 14,
|
|
|
|
|
homeFieldElo: 0,
|
|
|
|
|
regularSeasonMode: "project_remaining_games",
|
|
|
|
|
regularSeasonNoise: 0.9,
|
|
|
|
|
},
|
|
|
|
|
requiredInputs: ["sourceElo"],
|
|
|
|
|
optionalInputs: ["projectedWins", "sourceOdds", "seed"],
|
|
|
|
|
derivableInputs: { sourceElo: ["projectedWins", "sourceOdds"] },
|
|
|
|
|
setupSections: ["participants", "eloRatings", "regularStandings", "bracket"],
|
|
|
|
|
},
|
Add MLS sport simulator (mls_bracket) (#422)
* Add MLS sport simulator (mls_bracket)
Adds Major League Soccer as a draftable sport with a full-season Monte Carlo
simulator. Models both preseason regular-season projection (34 games across
Eastern and Western conferences) and the MLS Cup Playoffs bracket, including
the Wild Card (single game + PKs), Round 1 best-of-3 series, Conference
Semis/Finals (single game), and MLS Cup.
P1–P8 mapping: MLS Cup winner, finalist, Conference Finals losers, Conference
Semifinals losers. Conference assignment reads from regularSeasonStandings.conference
or falls back to the region simulator input ("Eastern"/"Western").
Admin inputs: projectedTablePoints (primary, max 102 for 34×3), with derivation
chain to sourceElo via existing input-policy; sourceOdds as alternative.
No hardcoded team data — all inputs are admin-managed per season.
- database/schema.ts: add mls_bracket to simulatorTypeEnum
- drizzle/0104_chief_boom_boom.sql: migration for the new enum value
- mls-simulator.ts: MLSSimulator + exported pure helpers for testability
- registry.ts / manifest.ts / simulator-config.ts: register mls_bracket
- mls-simulator.test.ts: 38 unit tests covering all helpers and sync checks
https://claude.ai/code/session_015wkBJ3SYGcMGjsddKKGkwa
* Fix MLS simulator: config loading, sourceOdds fallback, normalization
Three issues found in code review comparing against EPL and NFL simulators:
1. Load simulator config from DB via getSportsSeasonSimulatorConfig so
admins can override iterations, seasonGames, parityFactor, drawRates
per season without code changes. Previously all constants were hardcoded.
2. Add sourceOdds → convertFuturesToElo fallback when no sourceElo is
present. EPL and NFL both do this; MLS was throwing immediately instead
of attempting the odds conversion that the manifest declares as optional.
3. Call normalizeSimulationResultColumns before returning results, matching
EPL's local normalization pattern for consistency (runner also normalizes
globally, but EPL calls it locally too).
https://claude.ai/code/session_015wkBJ3SYGcMGjsddKKGkwa
* Polish MLS simulator: configNumber zero, logger warning, Map lookup
Three small fixes from secondary code review:
1. configNumber: allow value >= 0 (not just > 0) so admins can
legitimately set baseDrawRate or drawDecay to 0 without the value
being silently discarded and replaced with the default.
2. Add logger.warn when a participant is excluded from simulation due
to a missing Elo rating, matching the NLL bracket-aware pattern.
Gives admins a visible signal instead of a silent exclusion.
3. getBySeeds: build a Map once instead of calling Array.find() per
seed. Eliminates 4M linear scans across 50k iterations for a 9-
element array — trivially fast either way, but Map is the right tool.
https://claude.ai/code/session_015wkBJ3SYGcMGjsddKKGkwa
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-05-14 12:45:53 -07:00
|
|
|
mls_bracket: {
|
|
|
|
|
defaultConfig: {
|
|
|
|
|
...BASE_CONFIG,
|
|
|
|
|
seasonGames: 34,
|
|
|
|
|
parityFactor: 400,
|
|
|
|
|
matchParityFactor: 400,
|
|
|
|
|
averageOpponentElo: 1500,
|
|
|
|
|
baseDrawRate: 0.26,
|
|
|
|
|
drawDecay: 0.002,
|
|
|
|
|
playoffTeamsPerConference: 9,
|
|
|
|
|
},
|
|
|
|
|
requiredInputs: ["sourceElo"],
|
|
|
|
|
optionalInputs: ["sourceOdds", "projectedTablePoints", "seed", "region"],
|
|
|
|
|
derivableInputs: { sourceElo: ["projectedTablePoints", "sourceOdds"] },
|
|
|
|
|
setupSections: ["participants", "eloRatings", "regularStandings"],
|
|
|
|
|
},
|
Formalize simulator system with manifest, input-policy, runner, and admin UI (#409)
Introduces three new schema tables (simulator_profiles,
sports_season_simulator_configs, season_participant_simulator_inputs),
a central model layer (app/models/simulator.ts), and a single runner
entry point so every simulator run follows the same prepare → simulate
→ persist → snapshot → recalculate flow.
Key additions:
- manifest.ts: per-simulator display names, default configs, required/
optional inputs, derivable-input declarations, and setup sections
- input-policy.ts: resolves sourceElo from projectedWins,
projectedTablePoints, or sourceOdds; resolves ratings from sourceOdds;
supports block / fallbackElo / averageKnown / worstKnownMinus strategies
- runner.ts: single entry point for admin simulation runs; materialises
derived inputs, normalises result columns, zeroes omitted participants,
snapshots EVs, and recalculates linked fantasy standings
- /admin/simulators: inventory page with per-season readiness and bulk run
- /admin/sports-seasons/:id/simulator: per-season setup page with readiness
summary, input-policy editor, raw JSON config override, and CSV bulk input
- NCAAM/NCAAW simulators now read ratings from season_participant_simulator_inputs,
falling back to the hardcoded name-keyed maps while DB data is being populated
- Clone flow copies simulator config by default; volatile inputs (odds, Elo)
only copied when explicitly requested
Code-review fixes included in this commit:
- source field in compatibility bridge checked with !== null instead of !== undefined
- sourceEloRequirementLabel no longer appends "configured fallback" when the
participant is already excluded from all resolved sources
- Duplicate inline label maps in input-policy.ts replaced with simulatorInputLabel
- save-config preserves existing inputPolicy when the submitted JSON omits it
- Input table truncation label added (Showing 20 of N)
- CSV description notes values must not contain commas
- N+1 comment added to listSportsSeasonSimulatorSummaries
- assertRegistrySchemaDriftFree called in manifest tests
- Runner test suite added covering happy path, already-running guard,
readiness failure, empty results, and error recovery with status reset
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-11 21:09:53 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export const SIMULATOR_MANIFEST: Record<SimulatorType, SimulatorManifestProfile> =
|
|
|
|
|
Object.fromEntries(
|
|
|
|
|
SIMULATOR_TYPES.map((simulatorType) => {
|
|
|
|
|
const info = getSimulatorInfo(simulatorType);
|
|
|
|
|
const profile = PROFILES[simulatorType];
|
|
|
|
|
return [
|
|
|
|
|
simulatorType,
|
|
|
|
|
{
|
|
|
|
|
simulatorType,
|
|
|
|
|
displayName: info?.name ?? simulatorType,
|
|
|
|
|
description: info?.description ?? "",
|
|
|
|
|
...profile,
|
|
|
|
|
minParticipantInputs: profile.minParticipantInputs ?? 1,
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
})
|
|
|
|
|
) as Record<SimulatorType, SimulatorManifestProfile>;
|
|
|
|
|
|
|
|
|
|
export function getManifestSimulatorProfile(
|
|
|
|
|
simulatorType: SimulatorType
|
|
|
|
|
): SimulatorManifestProfile {
|
|
|
|
|
return SIMULATOR_MANIFEST[simulatorType];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function simulatorInputLabel(key: SimulatorInputKey): string {
|
|
|
|
|
const labels: Record<SimulatorInputKey, string> = {
|
|
|
|
|
sourceOdds: "futures odds",
|
|
|
|
|
sourceElo: "Elo rating",
|
|
|
|
|
worldRanking: "ranking",
|
|
|
|
|
rating: "rating",
|
|
|
|
|
projectedWins: "projected wins",
|
|
|
|
|
projectedTablePoints: "projected table points",
|
|
|
|
|
seed: "seed",
|
|
|
|
|
region: "region",
|
|
|
|
|
metadata: "metadata",
|
|
|
|
|
};
|
|
|
|
|
return labels[key];
|
|
|
|
|
}
|