Break leaderboard ties with Melee's tiebreakers

After total of all sessions, ties fall to OMW%, GW% and OGW% over all the
league's matches (bye = 2-0 win, not an opponent; opponents floored at 1/3),
then name. Verified identical to Melee's order and percentages on a real
night. The leaderboard shows the three percentages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
derekandClaude Opus 5.5 committed 2026-10-28 17:13:24 +00:00
1 parent 0e14228b8f
commit a99997b96d
4 files changed
+66 -7

No files matched your search

+40 -3
View File
@@ -1,11 +1,43 @@
import type { Wdl } from "./tables.ts";
type M = { session_id: number; p1_id: number; p2_id: number | null; p1_deck: string | null; p2_deck: string | null; p1_wins: number; p2_wins: number };
type M = { session_id: number; p1_id: number; p2_id: number | null; p1_deck: string | null; p2_deck: string | null; p1_wins: number; p2_wins: number; draws?: number };
type R = { session_id: number; player_id: number; points: number; deck: string | null; wins: number | null; draws: number | null; losses: number | null };
export type Tiebreak = { omw: number; gw: number; ogw: number };
/** Melee's (MTR) tiebreakers over every match row: opponents' match-win %, game-win %, opponents' game-win %.
* A bye is a 2-0 win but not an opponent; opponents' percentages are floored at 1/3. Checked against Melee's
* own values in a real standings export. */
export function tiebreakers(ms: M[]): Map<number, Tiebreak> {
const st = new Map<number, { mp: number; rounds: number; gp: number; games: number; opps: number[] }>();
for (const m of ms) {
const d = m.draws ?? 0;
const sides: [number | null, number | null, number, number][] = [
[m.p1_id, m.p2_id, m.p1_wins, m.p2_wins],
[m.p2_id, m.p1_id, m.p2_wins, m.p1_wins],
];
for (const [pid, opp, me, them] of sides) {
if (pid == null) continue;
const s = st.get(pid) ?? { mp: 0, rounds: 0, gp: 0, games: 0, opps: [] };
s.mp += me > them ? 3 : me === them ? 1 : 0;
s.rounds++;
s.gp += 3 * me + d;
s.games += me + them + d;
if (opp != null) s.opps.push(opp);
st.set(pid, s);
}
}
const mwp = (p: number) => { const s = st.get(p)!; return Math.max(1 / 3, s.mp / (3 * s.rounds)); };
const gwp = (p: number) => { const s = st.get(p)!; return s.games ? s.gp / (3 * s.games) : 0; };
const avg = (ps: number[], f: (p: number) => number) => (ps.length ? ps.reduce((a, p) => a + f(p), 0) / ps.length : 0);
return new Map([...st].map(([p, s]) =>
[p, { omw: avg(s.opps, mwp), gw: gwp(p), ogw: avg(s.opps, (o) => Math.max(1 / 3, gwp(o))) }]));
}
/** 3 pts per match win, 1 per draw, 0 per loss (a bye is a win). A session's score is the sum of its match
* points; the leaderboard adds up each player's best `bestN` sessions. Sessions with match rows use those;
* otherwise the imported standings are used. */
* otherwise the imported standings are used. Ties: total of all sessions, then Melee's tiebreakers over all the
* league's matches, then name. */
export function computeStandings(ms: M[], rs: R[], names: Map<number, string>, bestN: number) {
const per = new Map<number, Map<number, number>>(); // player -> session -> points
const rec = new Map<number, Wdl>();
@@ -46,6 +78,7 @@ export function computeStandings(ms: M[], rs: R[], names: Map<number, string>, b
});
}
const tb = tiebreakers(ms);
const rows = [...per].map(([pid, sessions]) => {
const top = [...sessions.values()].sort((a, b) => b - a);
return {
@@ -54,8 +87,12 @@ export function computeStandings(ms: M[], rs: R[], names: Map<number, string>, b
total: top.reduce((a, b) => a + b, 0),
sessions: top.length,
rec: partial.has(pid) ? null : rec.get(pid) ?? null,
tb: tb.get(pid) ?? null, // null: no match rows (standings-only sessions)
};
}).sort((a, b) => b.score - a.score || b.total - a.total || a.name.localeCompare(b.name));
});
const desc = (x = 0, y = 0) => (Math.abs(x - y) < 1e-9 ? 0 : y - x); // float noise must not break a tie
rows.sort((a, b) => b.score - a.score || b.total - a.total || desc(a.tb?.omw, b.tb?.omw) || desc(a.tb?.gw, b.tb?.gw) ||
desc(a.tb?.ogw, b.tb?.ogw) || a.name.localeCompare(b.name));
const sum = (w: Wdl) => w[0] + w[1] + w[2];
return { rows, decks: [...decks].sort((a, b) => sum(b[1]) - sum(a[1])) };
}