49 lines
1.6 KiB
TypeScript
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 };
|
|
}
|