100 lines
4 KiB
TypeScript
100 lines
4 KiB
TypeScript
|
|
/**
|
||
|
|
* 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;
|
||
|
|
}
|