A participant tied for a scoring placement splits the combined points of the tied positions. Four code paths computed a pick's points, each re-implementing the same bracket/qualifying_points/default cascade, and two of them omitted the qualifying_points arm entirely. A golfer tied for 8th was therefore worth the full 15 points on the team page and draft board but the split 7.5 in the standings, so a team read 225 on one screen and 218 on another. Collapse the cascade into a single calculatePickPoints helper and route all six call sites through it, backed by one shared getSharedPlacementCounts loader replacing the two separate tie-count queries. Tie counts span every participant in the sports season, not just drafted ones, since an undrafted tie partner still halves the award. Also round split awards to the nearest whole point in calculateAveragedPoints. The /rules page states ties are "combined and split equally among them, rounded to the nearest whole point", and its own worked example rounds 18.33 down to 18, so this is nearest rather than ceiling. Season point values are integer columns, making this averaging the only source of fractional points; rounding here means the standings' 218 is now correct by construction rather than a display artifact, and per-pick values visibly sum to the team total. Existing assertions encoding the unrounded results are updated, and the rules page's two published examples are asserted directly so the code and the published rule cannot drift apart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019LZLF1PAfeKdpYog3NKtyz
176 lines
6.6 KiB
TypeScript
176 lines
6.6 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import { calculateFantasyPoints, calculateAveragedPoints, type ScoringRules } from "../scoring-rules";
|
|
|
|
const DEFAULT_SCORING: ScoringRules = {
|
|
pointsFor1st: 100,
|
|
pointsFor2nd: 70,
|
|
pointsFor3rd: 50,
|
|
pointsFor4th: 40,
|
|
pointsFor5th: 25,
|
|
pointsFor6th: 25,
|
|
pointsFor7th: 15,
|
|
pointsFor8th: 15,
|
|
};
|
|
|
|
describe("Season Standings (F1 Pattern)", () => {
|
|
describe("Basic Placement Scoring", () => {
|
|
it("should calculate points for 1st place finisher", () => {
|
|
const points = calculateFantasyPoints(1, DEFAULT_SCORING);
|
|
expect(points).toBe(100);
|
|
});
|
|
|
|
it("should calculate points for 8th place finisher", () => {
|
|
const points = calculateFantasyPoints(8, DEFAULT_SCORING);
|
|
expect(points).toBe(15);
|
|
});
|
|
|
|
it("should return 0 points for finishers beyond 8th place", () => {
|
|
const points = calculateFantasyPoints(0, DEFAULT_SCORING);
|
|
expect(points).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("Tie Scenarios", () => {
|
|
it("should handle 2-way tie for 3rd place", () => {
|
|
// Two drivers tied for 3rd get average of 3rd and 4th place points
|
|
const points = calculateAveragedPoints([3, 4], DEFAULT_SCORING);
|
|
// (50 + 40) / 2 = 45
|
|
expect(points).toBe(45);
|
|
});
|
|
|
|
it("should handle 3-way tie for 5th place", () => {
|
|
// Three drivers tied for 5th share 5th, 6th, and 7th place points
|
|
const points = calculateAveragedPoints([5, 6, 7], DEFAULT_SCORING);
|
|
// (25 + 25 + 15) / 3 = 21.67 → 22 (nearest whole point)
|
|
expect(points).toBe(22);
|
|
});
|
|
|
|
it("should handle 4-way tie for 1st place", () => {
|
|
// Four drivers tied for 1st (unlikely but possible)
|
|
const points = calculateAveragedPoints([1, 2, 3, 4], DEFAULT_SCORING);
|
|
// (100 + 70 + 50 + 40) / 4 = 65
|
|
expect(points).toBe(65);
|
|
});
|
|
});
|
|
|
|
describe("Season Standings Simulation", () => {
|
|
it("should correctly score a complete F1-style season (no ties)", () => {
|
|
// Simulate final standings positions 1-10
|
|
const standings = [
|
|
{ driver: "Max Verstappen", position: 1, fantasyPoints: 100 },
|
|
{ driver: "Lewis Hamilton", position: 2, fantasyPoints: 70 },
|
|
{ driver: "Charles Leclerc", position: 3, fantasyPoints: 50 },
|
|
{ driver: "Lando Norris", position: 4, fantasyPoints: 40 },
|
|
{ driver: "Carlos Sainz", position: 5, fantasyPoints: 25 },
|
|
{ driver: "George Russell", position: 6, fantasyPoints: 25 },
|
|
{ driver: "Sergio Perez", position: 7, fantasyPoints: 15 },
|
|
{ driver: "Fernando Alonso", position: 8, fantasyPoints: 15 },
|
|
{ driver: "Oscar Piastri", position: 9, fantasyPoints: 0 }, // Beyond top 8
|
|
{ driver: "Pierre Gasly", position: 10, fantasyPoints: 0 }, // Beyond top 8
|
|
];
|
|
|
|
standings.forEach((entry) => {
|
|
const points = calculateFantasyPoints(entry.position, DEFAULT_SCORING);
|
|
expect(points).toBe(entry.fantasyPoints);
|
|
});
|
|
|
|
// Total points if one team drafted all top 8
|
|
const totalTop8 = 100 + 70 + 50 + 40 + 25 + 25 + 15 + 15;
|
|
expect(totalTop8).toBe(340);
|
|
});
|
|
|
|
it("should correctly score season with tied positions", () => {
|
|
// Simulate standings where 3rd, 4th, and 5th are tied
|
|
const _standings = [
|
|
{ position: 1, expected: 100 },
|
|
{ position: 2, expected: 70 },
|
|
// Three tied for 3rd place - each gets placement 3
|
|
// When scoring, these will share 3rd, 4th, 5th
|
|
{ position: 3, base: 50 }, // Will be averaged: (50+40+25)/3
|
|
{ position: 3, base: 50 },
|
|
{ position: 3, base: 50 },
|
|
{ position: 6, expected: 25 },
|
|
{ position: 7, expected: 15 },
|
|
{ position: 8, expected: 15 },
|
|
];
|
|
|
|
// Verify first two positions
|
|
expect(calculateFantasyPoints(1, DEFAULT_SCORING)).toBe(100);
|
|
expect(calculateFantasyPoints(2, DEFAULT_SCORING)).toBe(70);
|
|
|
|
// Verify tied positions get averaged
|
|
const tiedPoints = calculateAveragedPoints([3, 4, 5], DEFAULT_SCORING);
|
|
expect(tiedPoints).toBe(38); // (50 + 40 + 25) / 3 = 38.33 → 38
|
|
|
|
// Verify remaining positions
|
|
expect(calculateFantasyPoints(6, DEFAULT_SCORING)).toBe(25);
|
|
expect(calculateFantasyPoints(7, DEFAULT_SCORING)).toBe(15);
|
|
expect(calculateFantasyPoints(8, DEFAULT_SCORING)).toBe(15);
|
|
});
|
|
|
|
it("should handle ties that cross the 8th place boundary", () => {
|
|
// Simulate where 7th, 8th, and 9th are tied
|
|
// In practice, 7th and 8th would score, 9th would get 0
|
|
|
|
// First 7 and 8 get points
|
|
expect(calculateFantasyPoints(7, DEFAULT_SCORING)).toBe(15);
|
|
expect(calculateFantasyPoints(8, DEFAULT_SCORING)).toBe(15);
|
|
|
|
// If they share 7th and 8th place
|
|
const shared7and8 = calculateAveragedPoints([7, 8], DEFAULT_SCORING);
|
|
expect(shared7and8).toBe(15); // (15 + 15) / 2 = 15
|
|
|
|
// 9th gets 0
|
|
expect(calculateFantasyPoints(0, DEFAULT_SCORING)).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("Custom Scoring Rules", () => {
|
|
it("should work with custom F1 league scoring rules", () => {
|
|
const customScoring: ScoringRules = {
|
|
pointsFor1st: 150,
|
|
pointsFor2nd: 100,
|
|
pointsFor3rd: 75,
|
|
pointsFor4th: 60,
|
|
pointsFor5th: 50,
|
|
pointsFor6th: 40,
|
|
pointsFor7th: 30,
|
|
pointsFor8th: 20,
|
|
};
|
|
|
|
expect(calculateFantasyPoints(1, customScoring)).toBe(150);
|
|
expect(calculateFantasyPoints(8, customScoring)).toBe(20);
|
|
|
|
// Tied positions
|
|
const tied3rd = calculateAveragedPoints([3, 4], customScoring);
|
|
expect(tied3rd).toBe(68); // (75 + 60) / 2 = 67.5 → 68 (halves round up)
|
|
});
|
|
});
|
|
|
|
describe("Edge Cases", () => {
|
|
it("should handle all 8 positions tied", () => {
|
|
// Extremely unlikely but mathematically possible
|
|
const allTied = calculateAveragedPoints([1, 2, 3, 4, 5, 6, 7, 8], DEFAULT_SCORING);
|
|
// (100 + 70 + 50 + 40 + 25 + 25 + 15 + 15) / 8 = 42.5 → 43
|
|
expect(allTied).toBe(43);
|
|
});
|
|
|
|
it("should handle only top 4 finishing (others DNF/DQ)", () => {
|
|
// If only 4 participants finish the season
|
|
expect(calculateFantasyPoints(1, DEFAULT_SCORING)).toBe(100);
|
|
expect(calculateFantasyPoints(2, DEFAULT_SCORING)).toBe(70);
|
|
expect(calculateFantasyPoints(3, DEFAULT_SCORING)).toBe(50);
|
|
expect(calculateFantasyPoints(4, DEFAULT_SCORING)).toBe(40);
|
|
|
|
// Positions 5-8 would have no participants
|
|
// Any non-finishers get 0
|
|
expect(calculateFantasyPoints(0, DEFAULT_SCORING)).toBe(0);
|
|
});
|
|
|
|
it("should handle single participant season", () => {
|
|
// Only 1 participant completes the season
|
|
const points = calculateFantasyPoints(1, DEFAULT_SCORING);
|
|
expect(points).toBe(100);
|
|
});
|
|
});
|
|
});
|