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([[0, 1]]); for (let place = 0; place < maxPlaces; place++) { const next = new Map(); 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; constructor(providedDb?: ReturnType) { this.db = providedDb ?? database(); } async simulate(sportsSeasonId: string): Promise { 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(); 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(); 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(); 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", }; }); } }