brackt/app/services/simulations/soccer-helpers.ts
2026-05-07 11:48:57 -07:00

49 lines
1.6 KiB
TypeScript

import { eloWinProbabilityWithParity } from "~/services/probability-engine";
export type SoccerMatchResult = "win" | "draw" | "loss";
export interface EloSoccerMatchOptions {
baseDrawRate: number;
drawDecay: number;
parityFactor?: number;
}
export function soccerDrawProbability(
eloA: number,
eloB: number,
baseDrawRate: number,
drawDecay: number
): number {
return baseDrawRate * Math.exp(-drawDecay * Math.abs(eloA - eloB));
}
/**
* Simulate a soccer-style Elo match with an explicit draw probability.
* Returns the result from team A's perspective.
*/
export function simulateEloSoccerMatch(
eloA: number,
eloB: number,
{ baseDrawRate, drawDecay, parityFactor = 400 }: EloSoccerMatchOptions
): SoccerMatchResult {
const pDraw = soccerDrawProbability(eloA, eloB, baseDrawRate, drawDecay);
const pWinNoDraw = eloWinProbabilityWithParity(eloA, eloB, parityFactor);
const pWin = (1 - pDraw) * pWinNoDraw;
const rand = Math.random();
if (rand < pWin) return "win";
if (rand < pWin + pDraw) return "draw";
return "loss";
}
/** Generate simple low-scoring goals for table tiebreakers after a result is known. */
export function simulateSimpleSoccerGoals(result: SoccerMatchResult): { gf: number; ga: number } {
if (result === "draw") {
const goals = Math.random() < 0.55 ? 1 : Math.random() < 0.75 ? 0 : 2;
return { gf: goals, ga: goals };
}
const winnerGoals = Math.random() < 0.55 ? 2 : Math.random() < 0.75 ? 1 : 3;
const loserGoals = winnerGoals === 1 ? 0 : Math.random() < 0.35 ? 1 : 0;
if (result === "win") return { gf: winnerGoals, ga: loserGoals };
return { gf: loserGoals, ga: winnerGoals };
}