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>
176 lines
6 KiB
TypeScript
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",
|
|
};
|
|
});
|
|
}
|
|
}
|