brackt/app/models/__tests__/season-standings.test.ts
Chris Parsons e2b178221a
Add oxlint linting setup with zero errors (#194)
* Add oxlint and fix all lint errors

- Install oxlint, add .oxlintrc.json with rules for TypeScript/React
- Add npm run lint / lint:fix scripts
- Add Claude PostToolUse hook to run oxlint on every edited file
- Fix 101 errors: unused vars/imports, eqeqeq, prefer-const, no-new-array
- Fix no-array-index-key (use stable keys or suppress positional cases)
- Fix exhaustive-deps missing dependency in useEffect
- Promote exhaustive-deps and no-array-index-key to errors
- Fix Map.get() !== null bug in $leagueId.server.ts (should be !== undefined)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix no-explicit-any warnings and upgrade tsconfig to ES2023

- Replace all `any` types with proper types or `unknown` across ~20 files
- Add typed socket payload interfaces in draft route and useDraftSocket
- Use any[] with eslint-disable for socket.io callbacks (legitimate escape hatch)
- Bump all tsconfigs from ES2022 → ES2023 to support toSorted/toReversed
- Fix cascading type errors uncovered by removing any: Map.get narrowing,
  participant relation types, ChartDataPoint, Partial<NewSeason> indexing
- Add ParticipantResultWithParticipant type to participant-result model
- Fix test fixtures to match updated interfaces (DraftCell, ParticipantResult)
- Fix duplicate getQPStandings import in sportsSeasonId.server.ts

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Promote no-explicit-any to error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-21 09:44:05 -07:00

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
expect(points).toBeCloseTo(21.67, 2);
});
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).toBeCloseTo(38.33, 2); // (50 + 40 + 25) / 3
// 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(67.5); // (75 + 60) / 2
});
});
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
expect(allTied).toBe(42.5);
});
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);
});
});
});