brackt/app/lib/afl-wildcard-reseed.ts

100 lines
4 KiB
TypeScript
Raw Permalink Normal View History

/**
* AFL Wildcard Round Elimination Finals re-seeding.
*
* The Wildcard Round is drawn 7 v 10 and 8 v 9, and its two winners fill the open slots
* in the Elimination Finals opposite the 5th and 6th seeds. Those slots are NOT a fixed
* crossover: the winners are re-seeded by ladder position, exactly as the classic final
* eight pairs 5 v 8 and 6 v 7 the higher seed of the two hosts meets the lower-ranked
* winner. So 5th plays whichever winner finished further down the ladder and 6th plays
* the other, whichever Wildcard game each came out of.
*
* Worked example: 10th beats 7th and 9th beats 8th. A fixed crossover would send the
* 7v10 winner (10th) to 6th and the 8v9 winner (9th) to 5th handing the higher host
* the better opponent. Re-seeded, 5th plays 10th and 6th plays 9th.
*/
/** Seeds drawn into each Wildcard Round match, in [participant1, participant2] order. */
export const AFL_WILDCARD_DRAW: Readonly<Record<number, readonly [number, number]>> = {
1: [7, 10],
2: [8, 9],
};
/** Seed hosting each Elimination Finals match (its participant1 slot). */
export const AFL_ELIMINATION_HOSTS: Readonly<Record<number, number>> = {
1: 5,
2: 6,
};
export interface AflWildcardResult {
matchNumber: number;
/** Slot the winner occupied, or null while the match is still to be played. */
winnerSlot: 1 | 2 | null;
}
export interface AflWildcardPlacement {
wildcardMatchNumber: number;
/** Seed of the Wildcard winner being placed. */
seed: number;
eliminationMatchNumber: number;
}
/**
* Decide which Elimination Final each decided Wildcard winner belongs in.
*
* A winner is only placed once its destination is settled whichever way the other
* Wildcard game falls, so results can be entered in either order:
* - 7th winning match 1 outranks both possible match 2 winners always meets 6th.
* - 10th winning match 1 is outranked by both always meets 5th.
* - A match 2 winner (8th or 9th) sits between them, so it is held back until match 1
* is decided rather than being placed and then moved.
*
* Undecided winners are simply omitted; the caller fills the slots it is handed and
* leaves the rest TBD.
*/
export function resolveAflWildcardPlacements(
results: readonly AflWildcardResult[]
): AflWildcardPlacement[] {
const entries = results.map((result) => {
const draw = AFL_WILDCARD_DRAW[result.matchNumber];
if (!draw) {
throw new Error(`Unknown AFL Wildcard Round match number ${result.matchNumber}`);
}
return {
matchNumber: result.matchNumber,
seed: result.winnerSlot === null ? null : draw[result.winnerSlot - 1],
// Every seed the match could still send through — one entry once it is decided.
possibleSeeds: result.winnerSlot === null ? [...draw] : [draw[result.winnerSlot - 1]],
};
});
// Best-ranked winner takes the weakest host, so order the hosts worst seed first.
const hostsWorstFirst = Object.keys(AFL_ELIMINATION_HOSTS)
.map(Number)
.toSorted((a, b) => AFL_ELIMINATION_HOSTS[b] - AFL_ELIMINATION_HOSTS[a]);
const placements: AflWildcardPlacement[] = [];
for (const entry of entries) {
const seed = entry.seed;
if (seed === null) continue;
const others = entries.filter((other) => other !== entry);
const outranks = (other: (typeof entries)[number]) => other.possibleSeeds.every((s) => s < seed);
const outrankedBy = (other: (typeof entries)[number]) => other.possibleSeeds.every((s) => s > seed);
// This winner's rank is only knowable while every other one sits wholly above or
// wholly below it — an undecided game straddling this seed leaves it unplaceable.
if (!others.every((other) => outranks(other) || outrankedBy(other))) continue;
const rank = others.filter(outranks).length;
const eliminationMatchNumber = hostsWorstFirst[rank];
if (eliminationMatchNumber === undefined) {
throw new Error(`No Elimination Finals slot for AFL Wildcard winner ranked ${rank + 1}`);
}
placements.push({ wildcardMatchNumber: entry.matchNumber, seed, eliminationMatchNumber });
}
return placements;
}