brackt/app/services/simulations/brackt-simulator.ts
Chris Parsons 5b03b22267
Add Brackt as a league-private meta-sport (#200) (#377)
Commissioners can add Brackt to any league. Each team drafts one manager
from their own league; at season end, that manager's final overall fantasy
ranking earns placement points. Resolution fires automatically when all
other sports finalize.

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 20:31:44 -07:00

176 lines
6 KiB
TypeScript

import { and, eq, notInArray } from "drizzle-orm";
import { database } from "~/database/context";
import * as schema from "~/database/schema";
import type { Simulator, SimulationProbabilities, SimulationResult } from "./types";
const PROB_KEYS = [
"probFirst",
"probSecond",
"probThird",
"probFourth",
"probFifth",
"probSixth",
"probSeventh",
"probEighth",
] as const;
function emptyProbabilities(): SimulationProbabilities {
return {
probFirst: 0,
probSecond: 0,
probThird: 0,
probFourth: 0,
probFifth: 0,
probSixth: 0,
probSeventh: 0,
probEighth: 0,
};
}
export function harvillePlacementProbabilities(weights: number[]): number[][] {
const n = weights.length;
const maxPlaces = Math.min(8, n);
if (n === 0) return [];
if (weights.every((w) => w <= 0)) {
return weights.map(() =>
Array.from({ length: maxPlaces }, () => Number((1 / n).toFixed(6)))
);
}
const probs = weights.map(() => Array.from({ length: maxPlaces }, () => 0));
let states = new Map<number, number>([[0, 1]]);
for (let place = 0; place < maxPlaces; place++) {
const next = new Map<number, number>();
for (const [mask, stateProb] of states) {
const remaining = weights
.map((weight, index) => ({ weight: Math.max(0, weight), index }))
.filter(({ index }) => (mask & (1 << index)) === 0);
const total = remaining.reduce((sum, item) => sum + item.weight, 0);
for (const item of remaining) {
const pickProb = total > 0 ? item.weight / total : 1 / remaining.length;
const probability = stateProb * pickProb;
probs[item.index][place] += probability;
const nextMask = mask | (1 << item.index);
next.set(nextMask, (next.get(nextMask) ?? 0) + probability);
}
}
states = next;
}
return probs;
}
export class BracktSimulator implements Simulator {
private readonly db: ReturnType<typeof database>;
constructor(providedDb?: ReturnType<typeof database>) {
this.db = providedDb ?? database();
}
async simulate(sportsSeasonId: string): Promise<SimulationResult[]> {
const db = this.db;
const bracktSeason = await db.query.sportsSeasons.findFirst({
where: eq(schema.sportsSeasons.id, sportsSeasonId),
with: { sport: true },
});
if (!bracktSeason?.fantasySeasonId) {
throw new Error("Brackt simulation requires a private sports season linked to a fantasy season");
}
const fantasySeasonId = bracktSeason.fantasySeasonId;
const [teams, bracktParticipants, seasonSports, draftPicks] = await Promise.all([
db.query.teams.findMany({
where: eq(schema.teams.seasonId, fantasySeasonId),
orderBy: (teamsTable, { asc }) => [asc(teamsTable.name)],
}),
db.query.seasonParticipants.findMany({
where: eq(schema.seasonParticipants.sportsSeasonId, sportsSeasonId),
}),
db.query.seasonSports.findMany({
where: eq(schema.seasonSports.seasonId, fantasySeasonId),
with: { sportsSeason: { with: { sport: true } } },
}),
db.query.draftPicks.findMany({
where: eq(schema.draftPicks.seasonId, fantasySeasonId),
with: { participant: { with: { sportsSeason: { with: { sport: true } } } } },
}),
]);
if (teams.length < 6 || teams.length > 16) {
throw new Error("Brackt supports leagues with 6 to 16 teams");
}
const nonBracktSeasonIds = seasonSports
.filter((link) => link.sportsSeason.sport.slug !== "brackt")
.map((link) => link.sportsSeasonId);
const draftedIds = draftPicks.map((pick) => pick.participantId);
const availableBySportsSeason = new Map<string, number>();
if (nonBracktSeasonIds.length > 0) {
for (const nonBracktSeasonId of nonBracktSeasonIds) {
const whereClause = draftedIds.length > 0
? and(
eq(schema.seasonParticipants.sportsSeasonId, nonBracktSeasonId),
notInArray(schema.seasonParticipants.id, draftedIds)
)
: eq(schema.seasonParticipants.sportsSeasonId, nonBracktSeasonId);
const available = await db.query.seasonParticipants.findMany({ where: whereClause });
const average = available.length > 0
? available.reduce((sum, p) => sum + Number(p.expectedValue ?? 0), 0) / available.length
: 0;
availableBySportsSeason.set(nonBracktSeasonId, average);
}
}
const picksByTeam = new Map<string, typeof draftPicks>();
for (const pick of draftPicks) {
const existing = picksByTeam.get(pick.teamId) ?? [];
existing.push(pick);
picksByTeam.set(pick.teamId, existing);
}
const weights = teams.map((team) => {
const teamPicks = picksByTeam.get(team.id) ?? [];
const draftedBySportsSeason = new Set<string>();
let projection = 0;
for (const pick of teamPicks) {
if (pick.participant.sportsSeason.sport.slug === "brackt") continue;
draftedBySportsSeason.add(pick.participant.sportsSeasonId);
projection += Number(pick.participant.expectedValue ?? 0);
}
for (const nonBracktSeasonId of nonBracktSeasonIds) {
if (!draftedBySportsSeason.has(nonBracktSeasonId)) {
projection += availableBySportsSeason.get(nonBracktSeasonId) ?? 0;
}
}
return Math.max(0, projection);
});
const placementProbabilities = harvillePlacementProbabilities(weights);
const participantByTeamId = new Map(bracktParticipants.map((p) => [p.externalId, p]));
return teams.map((team, teamIndex) => {
const participant = participantByTeamId.get(team.id);
if (!participant) {
throw new Error(`Missing Brackt participant for team ${team.name}`);
}
const probabilities = emptyProbabilities();
for (let i = 0; i < Math.min(PROB_KEYS.length, placementProbabilities[teamIndex].length); i++) {
probabilities[PROB_KEYS[i]] = Number(placementProbabilities[teamIndex][i].toFixed(4));
}
return {
participantId: participant.id,
probabilities,
source: "brackt_harville",
};
});
}
}