An AFL top-4 seed has the double chance from the moment the bracket is
drawn: lose the Qualifying Final, lose the Semi-Final, and you still
finish in the 5th-6th tier. Nothing was awarding that. Seeds 1-4 sat on
0 fantasy points until their first game resolved, which understated
every roster holding them.
Add an `entryFloor` field to BracketRound for floors a seeding locks in
before anyone plays, plus `applyBracketEntryFloors` to bank them, wired
into both bracket generation and reprocess-bracket. For afl_10 that is 5
for the Qualifying Finals (seeds 1-4) and 7 for the Elimination Finals
(seeds 5-6). Every write is provisional, so a real result supersedes it,
and upsertParticipantResult's never-un-finalize guard leaves finalized
rows alone.
Two related floors were also wrong, both from the generic
"winning into a scoring round means top-8" default in
nonScoringWinnerFloorFor:
- Qualifying Finals winners banked 5 when the bye to a Preliminary
Final guarantees the 3rd-4th tier. progressive-floor-scoring.test.ts
already asserted 3 here, but via an isScoring=true call the runtime
never makes.
- Wildcard winners banked 5 when winning only buys an Elimination
Final, whose losers are the 7th-8th tier — an over-award of a full
tier until that game was played.
Both are now explicit nonScoringWinnerFloor values on the template.
reprocess-bracket now applies entry floors after wiping results and
before replaying matches, and no longer refuses a bracket with no
completed matches, so setting a bracket and reprocessing awards the
guaranteed points. It stays silent on Discord as before.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JkhdcbUCvramxJdVdoBsKd
275 lines
12 KiB
TypeScript
275 lines
12 KiB
TypeScript
/**
|
|
* AFL Finals System Tests
|
|
* Phase 3.3: AFL Finals (10 teams with Wildcard Round from 2026)
|
|
*
|
|
* Tests the complex double-chance finals system used by the AFL
|
|
*/
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
import { AFL_10, getScoringRoundType } from "~/lib/bracket-templates";
|
|
import { calculateFantasyPoints, calculateAveragedPoints, calculateBracketPoints, type ScoringRules } from "../scoring-rules";
|
|
import { getBracketEntryFloor } from "../scoring-calculator";
|
|
|
|
const DEFAULT_SCORING: ScoringRules = {
|
|
pointsFor1st: 100,
|
|
pointsFor2nd: 70,
|
|
pointsFor3rd: 50,
|
|
pointsFor4th: 40,
|
|
pointsFor5th: 25,
|
|
pointsFor6th: 25,
|
|
pointsFor7th: 15,
|
|
pointsFor8th: 15,
|
|
};
|
|
|
|
describe("AFL Finals System - Phase 3.3", () => {
|
|
describe("AFL_10 Template Structure", () => {
|
|
it("has correct template configuration", () => {
|
|
expect(AFL_10.id).toBe("afl_10");
|
|
expect(AFL_10.name).toBe("AFL Finals (10 teams with Wildcard)");
|
|
expect(AFL_10.totalTeams).toBe(10);
|
|
expect(AFL_10.rounds).toHaveLength(6);
|
|
expect(AFL_10.scoringStartsAtRound).toBe("Elimination Finals");
|
|
});
|
|
|
|
it("has Wildcard Round as first round (non-scoring)", () => {
|
|
const wildcardRound = AFL_10.rounds[0];
|
|
expect(wildcardRound.name).toBe("Wildcard Round");
|
|
expect(wildcardRound.matchCount).toBe(2);
|
|
expect(wildcardRound.feedsInto).toBe("Elimination Finals");
|
|
expect(wildcardRound.isScoring).toBe(false); // Losers get 0 points
|
|
});
|
|
|
|
it("has Qualifying Finals as second round (non-scoring - double chance)", () => {
|
|
const qualifyingRound = AFL_10.rounds[1];
|
|
expect(qualifyingRound.name).toBe("Qualifying Finals");
|
|
expect(qualifyingRound.matchCount).toBe(2);
|
|
expect(qualifyingRound.feedsInto).toBe("Preliminary Finals"); // Winners skip Semi-Finals
|
|
expect(qualifyingRound.isScoring).toBe(false); // Losers get second chance
|
|
});
|
|
|
|
it("has Elimination Finals as third round (scoring - share 7-8)", () => {
|
|
const eliminationRound = AFL_10.rounds[2];
|
|
expect(eliminationRound.name).toBe("Elimination Finals");
|
|
expect(eliminationRound.matchCount).toBe(2);
|
|
expect(eliminationRound.feedsInto).toBe("Semi-Finals");
|
|
expect(eliminationRound.isScoring).toBe(true); // Losers share 7th-8th
|
|
});
|
|
|
|
it("has Semi-Finals as fourth round (scoring - share 5-6)", () => {
|
|
const semiRound = AFL_10.rounds[3];
|
|
expect(semiRound.name).toBe("Semi-Finals");
|
|
expect(semiRound.matchCount).toBe(2);
|
|
expect(semiRound.feedsInto).toBe("Preliminary Finals");
|
|
expect(semiRound.isScoring).toBe(true); // Losers share 5th-6th
|
|
});
|
|
|
|
it("has Preliminary Finals as fifth round (scoring - share 3-4)", () => {
|
|
const prelimRound = AFL_10.rounds[4];
|
|
expect(prelimRound.name).toBe("Preliminary Finals");
|
|
expect(prelimRound.matchCount).toBe(2);
|
|
expect(prelimRound.feedsInto).toBe("Grand Final");
|
|
expect(prelimRound.isScoring).toBe(true); // Losers share 3rd-4th
|
|
});
|
|
|
|
it("has Grand Final as final round (scoring - 1st and 2nd)", () => {
|
|
const grandFinalRound = AFL_10.rounds[5];
|
|
expect(grandFinalRound.name).toBe("Grand Final");
|
|
expect(grandFinalRound.matchCount).toBe(1);
|
|
expect(grandFinalRound.feedsInto).toBeNull();
|
|
expect(grandFinalRound.isScoring).toBe(true); // Winner 1st, Loser 2nd
|
|
});
|
|
});
|
|
|
|
describe("AFL Scoring Round Types", () => {
|
|
it("identifies Grand Final as finals type", () => {
|
|
const type = getScoringRoundType("Grand Final", AFL_10);
|
|
expect(type).toBe("finals"); // 1st and 2nd
|
|
});
|
|
|
|
it("identifies Preliminary Finals as semifinals type", () => {
|
|
const type = getScoringRoundType("Preliminary Finals", AFL_10);
|
|
expect(type).toBe("semifinals"); // Share 3rd-4th
|
|
});
|
|
|
|
it("identifies Semi-Finals as quarterfinals type (custom AFL mapping)", () => {
|
|
const type = getScoringRoundType("Semi-Finals", AFL_10);
|
|
expect(type).toBe("quarterfinals"); // Share 5th-6th (custom)
|
|
});
|
|
|
|
it("identifies Elimination Finals as quarterfinals type", () => {
|
|
const type = getScoringRoundType("Elimination Finals", AFL_10);
|
|
expect(type).toBe("quarterfinals"); // Share 7th-8th
|
|
});
|
|
|
|
it("identifies Qualifying Finals as non-scoring", () => {
|
|
const type = getScoringRoundType("Qualifying Finals", AFL_10);
|
|
expect(type).toBeNull(); // Double chance - no scoring
|
|
});
|
|
|
|
it("identifies Wildcard Round as non-scoring", () => {
|
|
const type = getScoringRoundType("Wildcard Round", AFL_10);
|
|
expect(type).toBeNull(); // Early elimination
|
|
});
|
|
});
|
|
|
|
describe("AFL Placement Points Calculations", () => {
|
|
it("calculates correct points for 1st place (Grand Final winner)", () => {
|
|
const points = calculateFantasyPoints(1, DEFAULT_SCORING);
|
|
expect(points).toBe(100);
|
|
});
|
|
|
|
it("calculates correct points for 2nd place (Grand Final loser)", () => {
|
|
const points = calculateFantasyPoints(2, DEFAULT_SCORING);
|
|
expect(points).toBe(70);
|
|
});
|
|
|
|
it("calculates correct averaged points for 3rd-4th (Preliminary Finals losers)", () => {
|
|
// Two teams lose Prelim Finals, share 3rd and 4th
|
|
const points = calculateAveragedPoints([3, 4], DEFAULT_SCORING);
|
|
expect(points).toBe(45); // (50 + 40) / 2
|
|
});
|
|
|
|
it("calculates correct averaged points for 5th-6th (Semi-Finals losers)", () => {
|
|
// Two teams lose Semi-Finals, share 5th and 6th
|
|
const points = calculateAveragedPoints([5, 6], DEFAULT_SCORING);
|
|
expect(points).toBe(25); // (25 + 25) / 2
|
|
});
|
|
|
|
it("calculates correct averaged points for 7th-8th (Elimination Finals losers)", () => {
|
|
// Two teams lose Elimination Finals, share 7th and 8th
|
|
const points = calculateAveragedPoints([7, 8], DEFAULT_SCORING);
|
|
expect(points).toBe(15); // (15 + 15) / 2
|
|
});
|
|
|
|
it("returns 0 points for Wildcard Round losers (9th-10th)", () => {
|
|
// Two teams lose Wildcard Round, eliminated before scoring begins
|
|
const points = calculateFantasyPoints(0, DEFAULT_SCORING);
|
|
expect(points).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("AFL Finals Progression", () => {
|
|
it("has correct round progression for top 6 teams", () => {
|
|
// Top 6 teams (1-6) skip Wildcard Round
|
|
// QF winners (1-4) skip Semi-Finals and go straight to Preliminary Finals
|
|
// QF losers (from 1-4) get second chance in Semi-Finals
|
|
|
|
expect(AFL_10.rounds[1].name).toBe("Qualifying Finals"); // Top 4 play here
|
|
expect(AFL_10.rounds[1].feedsInto).toBe("Preliminary Finals"); // Winners bypass SF
|
|
});
|
|
|
|
it("has correct round progression for wildcard teams", () => {
|
|
// Teams 7-10 play Wildcard Round
|
|
// Winners advance to Elimination Finals
|
|
// Losers eliminated (0 points)
|
|
|
|
expect(AFL_10.rounds[0].name).toBe("Wildcard Round");
|
|
expect(AFL_10.rounds[0].feedsInto).toBe("Elimination Finals");
|
|
expect(AFL_10.rounds[0].isScoring).toBe(false);
|
|
});
|
|
|
|
it("has correct double-chance system structure", () => {
|
|
// Qualifying Finals losers go to Semi-Finals (second chance)
|
|
// Elimination Finals winners go to Semi-Finals
|
|
// This creates the "double chance" for top 4 teams
|
|
|
|
const qfRound = AFL_10.rounds.find(r => r.name === "Qualifying Finals");
|
|
const sfRound = AFL_10.rounds.find(r => r.name === "Semi-Finals");
|
|
|
|
expect(qfRound?.isScoring).toBe(false); // Losers not eliminated
|
|
expect(sfRound?.name).toBe("Semi-Finals");
|
|
});
|
|
});
|
|
|
|
describe("AFL Point Distribution", () => {
|
|
it("has all 8 scoring placements covered", () => {
|
|
// Count scoring rounds
|
|
const scoringRounds = AFL_10.rounds.filter(r => r.isScoring);
|
|
expect(scoringRounds).toHaveLength(4); // EF, SF, PF, GF
|
|
|
|
// Total teams eliminated in scoring rounds: 2 + 2 + 2 + 2 = 8 teams get points
|
|
const totalScoringTeams = scoringRounds.reduce((sum, round) => {
|
|
// Each match has 2 participants, losers get points (except GF winner also gets points)
|
|
return sum + round.matchCount * 2;
|
|
}, 0);
|
|
|
|
expect(totalScoringTeams).toBe(14); // But only 8 unique placements (some shared)
|
|
});
|
|
|
|
it("has 2 teams eliminated outside top 8 (Wildcard losers)", () => {
|
|
const wildcardRound = AFL_10.rounds[0];
|
|
expect(wildcardRound.isScoring).toBe(false);
|
|
expect(wildcardRound.matchCount).toBe(2);
|
|
|
|
// 2 matches = 4 teams, 2 winners advance, 2 losers eliminated (0 points)
|
|
const eliminatedTeams = wildcardRound.matchCount; // 2 losers
|
|
expect(eliminatedTeams).toBe(2);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("AFL guaranteed floors from seeding (afl_10)", () => {
|
|
const byName = (name: string) => AFL_10.rounds.find((r) => r.name === name);
|
|
|
|
describe("getBracketEntryFloor — banked the moment the bracket is set", () => {
|
|
it("gives seeds 1-4 the 5th-6th tier: the double chance is locked in at seeding", () => {
|
|
// Worst case for a top-4 seed is lose the Qualifying Final, then lose the
|
|
// Semi-Final — which is the 5th-6th tier. They can never finish below it.
|
|
expect(getBracketEntryFloor("Qualifying Finals", "afl_10")).toBe(5);
|
|
expect(calculateBracketPoints(5, DEFAULT_SCORING, "afl_10")).toBe(25);
|
|
});
|
|
|
|
it("gives seeds 5-6 the 7th-8th tier: they are seeded straight into a scoring round", () => {
|
|
expect(getBracketEntryFloor("Elimination Finals", "afl_10")).toBe(7);
|
|
expect(calculateBracketPoints(7, DEFAULT_SCORING, "afl_10")).toBe(15);
|
|
});
|
|
|
|
it("gives seeds 7-10 nothing: a Wildcard loss is worth 0", () => {
|
|
expect(getBracketEntryFloor("Wildcard Round", "afl_10")).toBeNull();
|
|
});
|
|
|
|
it("returns null when the template is unknown or missing", () => {
|
|
expect(getBracketEntryFloor("Qualifying Finals", null)).toBeNull();
|
|
expect(getBracketEntryFloor("Qualifying Finals", "not_a_template")).toBeNull();
|
|
});
|
|
|
|
it("does not hand out floors for TBD rounds nobody is seeded into yet", () => {
|
|
// These rounds do carry a loser tier, but every slot is empty at generation,
|
|
// so applyBracketEntryFloors has no participant to write against.
|
|
expect(byName("Semi-Finals")?.entryFloor).toBeUndefined();
|
|
expect(byName("Preliminary Finals")?.entryFloor).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("nonScoringWinnerFloor — the generic top-8 default is wrong for both AFL non-scoring rounds", () => {
|
|
it("Qualifying Finals winners bank 3, not 5 — the bye means a Prelim loss is 3rd-4th", () => {
|
|
expect(byName("Qualifying Finals")?.nonScoringWinnerFloor).toBe(3);
|
|
});
|
|
|
|
it("Wildcard winners bank 7, not 5 — winning only buys an Elimination Final", () => {
|
|
expect(byName("Wildcard Round")?.nonScoringWinnerFloor).toBe(7);
|
|
});
|
|
});
|
|
|
|
describe("floors only ever improve along every AFL path", () => {
|
|
const pts = (position: number) => calculateBracketPoints(position, DEFAULT_SCORING, "afl_10");
|
|
|
|
it("top-4 seed: entry 5 → QF win 3 → PF win 2 → GF win 1", () => {
|
|
expect(pts(5)).toBeLessThan(pts(3));
|
|
expect(pts(3)).toBeLessThan(pts(2));
|
|
expect(pts(2)).toBeLessThan(pts(1));
|
|
});
|
|
|
|
it("top-4 seed losing the QF holds the entry floor, then finalizes at 5th-6th", () => {
|
|
// QF losers advance to the Semi-Final, so nothing is written at the QF —
|
|
// the entry floor of 5 carries them until the Semi-Final resolves.
|
|
const entryFloor = getBracketEntryFloor("Qualifying Finals", "afl_10");
|
|
expect(entryFloor).toBe(5);
|
|
expect(pts(entryFloor ?? 0)).toBe(25); // unchanged by the loss
|
|
});
|
|
|
|
it("seeds 5-6 and Wildcard winners share a 7th-8th floor, below the top-4's", () => {
|
|
expect(pts(7)).toBeLessThan(pts(5));
|
|
});
|
|
});
|
|
});
|