2025-11-11 10:08:25 -08:00
|
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
|
import { DEFAULT_QP_VALUES } from "../qualifying-points";
|
|
|
|
|
|
|
|
|
|
describe("Qualifying Points Configuration", () => {
|
|
|
|
|
describe("DEFAULT_QP_VALUES", () => {
|
|
|
|
|
it("should have 16 placement values", () => {
|
|
|
|
|
expect(DEFAULT_QP_VALUES).toHaveLength(16);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should have correct QP values for top placements", () => {
|
|
|
|
|
expect(DEFAULT_QP_VALUES[0]).toEqual({ placement: 1, points: 20 });
|
|
|
|
|
expect(DEFAULT_QP_VALUES[1]).toEqual({ placement: 2, points: 14 });
|
|
|
|
|
expect(DEFAULT_QP_VALUES[2]).toEqual({ placement: 3, points: 10 });
|
|
|
|
|
expect(DEFAULT_QP_VALUES[3]).toEqual({ placement: 4, points: 8 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should have 5 QP for 5th and 6th place", () => {
|
|
|
|
|
expect(DEFAULT_QP_VALUES[4]).toEqual({ placement: 5, points: 5 });
|
|
|
|
|
expect(DEFAULT_QP_VALUES[5]).toEqual({ placement: 6, points: 5 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should have 3 QP for 7th and 8th place", () => {
|
|
|
|
|
expect(DEFAULT_QP_VALUES[6]).toEqual({ placement: 7, points: 3 });
|
|
|
|
|
expect(DEFAULT_QP_VALUES[7]).toEqual({ placement: 8, points: 3 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should have 2 QP for 9th-12th place", () => {
|
|
|
|
|
expect(DEFAULT_QP_VALUES[8]).toEqual({ placement: 9, points: 2 });
|
|
|
|
|
expect(DEFAULT_QP_VALUES[9]).toEqual({ placement: 10, points: 2 });
|
|
|
|
|
expect(DEFAULT_QP_VALUES[10]).toEqual({ placement: 11, points: 2 });
|
|
|
|
|
expect(DEFAULT_QP_VALUES[11]).toEqual({ placement: 12, points: 2 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should have 1 QP for 13th-16th place", () => {
|
|
|
|
|
expect(DEFAULT_QP_VALUES[12]).toEqual({ placement: 13, points: 1 });
|
|
|
|
|
expect(DEFAULT_QP_VALUES[13]).toEqual({ placement: 14, points: 1 });
|
|
|
|
|
expect(DEFAULT_QP_VALUES[14]).toEqual({ placement: 15, points: 1 });
|
|
|
|
|
expect(DEFAULT_QP_VALUES[15]).toEqual({ placement: 16, points: 1 });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should be sorted by placement (ascending)", () => {
|
|
|
|
|
for (let i = 0; i < DEFAULT_QP_VALUES.length - 1; i++) {
|
|
|
|
|
expect(DEFAULT_QP_VALUES[i].placement).toBeLessThan(
|
|
|
|
|
DEFAULT_QP_VALUES[i + 1].placement
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should have descending point values for higher placements", () => {
|
|
|
|
|
expect(DEFAULT_QP_VALUES[0].points).toBeGreaterThan(DEFAULT_QP_VALUES[1].points);
|
|
|
|
|
expect(DEFAULT_QP_VALUES[1].points).toBeGreaterThan(DEFAULT_QP_VALUES[2].points);
|
|
|
|
|
expect(DEFAULT_QP_VALUES[2].points).toBeGreaterThan(DEFAULT_QP_VALUES[3].points);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("QP Accumulation Logic", () => {
|
|
|
|
|
it("should accumulate QP across multiple events", () => {
|
|
|
|
|
// Simulating participant earning QP from 4 majors
|
|
|
|
|
const event1QP = 20; // 1st place
|
|
|
|
|
const event2QP = 14; // 2nd place
|
|
|
|
|
const event3QP = 10; // 3rd place
|
|
|
|
|
const event4QP = 8; // 4th place
|
|
|
|
|
|
|
|
|
|
const totalQP = event1QP + event2QP + event3QP + event4QP;
|
|
|
|
|
|
|
|
|
|
expect(totalQP).toBe(52);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should handle decimal QP values", () => {
|
|
|
|
|
const qp1 = 20.5;
|
|
|
|
|
const qp2 = 14.25;
|
|
|
|
|
|
|
|
|
|
const total = qp1 + qp2;
|
|
|
|
|
|
|
|
|
|
expect(total).toBe(34.75);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should handle zero QP awards", () => {
|
|
|
|
|
const event1QP = 20;
|
|
|
|
|
const event2QP = 0; // Finished beyond 16th
|
|
|
|
|
|
|
|
|
|
const total = event1QP + event2QP;
|
|
|
|
|
|
|
|
|
|
expect(total).toBe(20);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("QP Ranking Logic", () => {
|
|
|
|
|
it("should rank participants by total QP (highest first)", () => {
|
|
|
|
|
const standings = [
|
|
|
|
|
{ name: "Player A", qp: 44 },
|
|
|
|
|
{ name: "Player B", qp: 50 },
|
|
|
|
|
{ name: "Player C", qp: 38 },
|
|
|
|
|
];
|
|
|
|
|
|
2026-03-21 09:44:05 -07:00
|
|
|
const sorted = standings.toSorted((a, b) => b.qp - a.qp);
|
2025-11-11 10:08:25 -08:00
|
|
|
|
|
|
|
|
expect(sorted[0].name).toBe("Player B"); // 50 QP
|
|
|
|
|
expect(sorted[1].name).toBe("Player A"); // 44 QP
|
|
|
|
|
expect(sorted[2].name).toBe("Player C"); // 38 QP
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should handle ties in QP totals", () => {
|
|
|
|
|
const standings = [
|
|
|
|
|
{ name: "Player A", qp: 30 },
|
|
|
|
|
{ name: "Player B", qp: 30 },
|
|
|
|
|
{ name: "Player C", qp: 20 },
|
|
|
|
|
];
|
|
|
|
|
|
2026-03-21 09:44:05 -07:00
|
|
|
const sorted = standings.toSorted((a, b) => b.qp - a.qp);
|
2025-11-11 10:08:25 -08:00
|
|
|
|
|
|
|
|
// Both players with 30 QP should be before Player C
|
|
|
|
|
expect(sorted[2].name).toBe("Player C");
|
|
|
|
|
expect(sorted[0].qp).toBe(30);
|
|
|
|
|
expect(sorted[1].qp).toBe(30);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("QP to Fantasy Placement Conversion", () => {
|
|
|
|
|
it("should assign top 8 QP leaders to fantasy placements 1-8", () => {
|
|
|
|
|
const qpRankings = [50, 44, 38, 30, 25, 20, 15, 10, 5, 2];
|
|
|
|
|
|
|
|
|
|
const top8 = qpRankings.slice(0, 8);
|
|
|
|
|
|
|
|
|
|
expect(top8).toHaveLength(8);
|
|
|
|
|
expect(top8[0]).toBe(50); // 1st place
|
|
|
|
|
expect(top8[7]).toBe(10); // 8th place
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should assign 0 fantasy points to participants beyond top 8", () => {
|
|
|
|
|
const qpRankings = [50, 44, 38, 30, 25, 20, 15, 10, 5, 2];
|
|
|
|
|
|
|
|
|
|
const beyond8 = qpRankings.slice(8);
|
|
|
|
|
|
|
|
|
|
expect(beyond8).toHaveLength(2);
|
|
|
|
|
// These would get 0 fantasy points
|
|
|
|
|
const fantasyPoints = beyond8.map(() => 0);
|
|
|
|
|
expect(fantasyPoints).toEqual([0, 0]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should handle ties at the cutoff (8th place)", () => {
|
|
|
|
|
// Scenario: Two players tied for 8th place QP
|
|
|
|
|
const qpRankings = [
|
|
|
|
|
{ name: "P1", qp: 50 }, // 1st
|
|
|
|
|
{ name: "P2", qp: 44 }, // 2nd
|
|
|
|
|
{ name: "P3", qp: 38 }, // 3rd
|
|
|
|
|
{ name: "P4", qp: 30 }, // 4th
|
|
|
|
|
{ name: "P5", qp: 25 }, // 5th
|
|
|
|
|
{ name: "P6", qp: 20 }, // 6th
|
|
|
|
|
{ name: "P7", qp: 15 }, // 7th
|
|
|
|
|
{ name: "P8", qp: 10 }, // Tied for 8th
|
|
|
|
|
{ name: "P9", qp: 10 }, // Tied for 8th
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Both should get placement 8
|
|
|
|
|
const tied8th = qpRankings.filter(p => p.qp === 10);
|
|
|
|
|
expect(tied8th).toHaveLength(2);
|
|
|
|
|
|
|
|
|
|
// In implementation, both would share 8th place fantasy placement
|
|
|
|
|
const placement = 8;
|
|
|
|
|
expect(placement).toBe(8);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("Tie Handling During QP Awards", () => {
|
|
|
|
|
it("should correctly split QP among tied participants", () => {
|
|
|
|
|
// Scenario: 7 players tied for 14th place
|
|
|
|
|
// They occupy positions 14-20
|
|
|
|
|
// Positions 14, 15, 16 get 1 QP each = 3 total QP
|
|
|
|
|
// Positions 17-20 get 0 QP
|
|
|
|
|
// Total: 3 QP divided by 7 players = 3/7 = 0.428571... per player
|
|
|
|
|
|
|
|
|
|
const tieCount = 7;
|
|
|
|
|
const startPosition = 14;
|
|
|
|
|
|
|
|
|
|
// Calculate total QP for positions 14-20
|
|
|
|
|
const qpMap: Record<number, number> = {
|
|
|
|
|
14: 1,
|
|
|
|
|
15: 1,
|
|
|
|
|
16: 1,
|
|
|
|
|
17: 0,
|
|
|
|
|
18: 0,
|
|
|
|
|
19: 0,
|
|
|
|
|
20: 0,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let totalQP = 0;
|
|
|
|
|
for (let pos = startPosition; pos < startPosition + tieCount; pos++) {
|
|
|
|
|
totalQP += qpMap[pos] || 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const qpPerPlayer = totalQP / tieCount;
|
|
|
|
|
|
|
|
|
|
expect(totalQP).toBe(3);
|
|
|
|
|
expect(qpPerPlayer).toBeCloseTo(0.428571, 5);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should handle 2-way tie for 5th place", () => {
|
|
|
|
|
// 2 players tied for 5th occupy positions 5 and 6
|
|
|
|
|
// Position 5: 5 QP, Position 6: 5 QP
|
|
|
|
|
// Total: 10 QP / 2 players = 5 QP each
|
|
|
|
|
|
|
|
|
|
const tieCount = 2;
|
|
|
|
|
const startPosition = 5;
|
|
|
|
|
const qpMap: Record<number, number> = { 5: 5, 6: 5 };
|
|
|
|
|
|
|
|
|
|
let totalQP = 0;
|
|
|
|
|
for (let pos = startPosition; pos < startPosition + tieCount; pos++) {
|
|
|
|
|
totalQP += qpMap[pos];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const qpPerPlayer = totalQP / tieCount;
|
|
|
|
|
|
|
|
|
|
expect(totalQP).toBe(10);
|
|
|
|
|
expect(qpPerPlayer).toBe(5);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should handle 3-way tie for 1st place", () => {
|
|
|
|
|
// 3 players tied for 1st occupy positions 1, 2, 3
|
|
|
|
|
// Position 1: 20 QP, Position 2: 14 QP, Position 3: 10 QP
|
|
|
|
|
// Total: 44 QP / 3 players = 14.666... QP each
|
|
|
|
|
|
|
|
|
|
const tieCount = 3;
|
|
|
|
|
const startPosition = 1;
|
|
|
|
|
const qpMap: Record<number, number> = { 1: 20, 2: 14, 3: 10 };
|
|
|
|
|
|
|
|
|
|
let totalQP = 0;
|
|
|
|
|
for (let pos = startPosition; pos < startPosition + tieCount; pos++) {
|
|
|
|
|
totalQP += qpMap[pos];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const qpPerPlayer = totalQP / tieCount;
|
|
|
|
|
|
|
|
|
|
expect(totalQP).toBe(44);
|
|
|
|
|
expect(qpPerPlayer).toBeCloseTo(14.666667, 5);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("Reprocessing Events", () => {
|
|
|
|
|
it("should handle reprocessing by removing old QP before adding new QP", () => {
|
|
|
|
|
// Scenario: Admin enters results, processes QP, then realizes there's an error
|
|
|
|
|
// They fix the placements and reprocess
|
|
|
|
|
|
|
|
|
|
// Initial processing
|
|
|
|
|
const participant1InitialQP = 20; // 1st place
|
|
|
|
|
const participant2InitialQP = 14; // 2nd place
|
|
|
|
|
|
|
|
|
|
let totalQP1 = participant1InitialQP;
|
|
|
|
|
let totalQP2 = participant2InitialQP;
|
|
|
|
|
|
|
|
|
|
// Admin realizes participant 1 should have been 3rd, not 1st
|
|
|
|
|
// Reprocessing: subtract old QP, add new QP
|
|
|
|
|
totalQP1 = totalQP1 - participant1InitialQP + 10; // Remove 20, add 10 (3rd place)
|
|
|
|
|
totalQP2 = totalQP2 - participant2InitialQP + 20; // Remove 14, add 20 (moved to 1st)
|
|
|
|
|
|
|
|
|
|
expect(totalQP1).toBe(10); // Should have 10 QP (3rd place)
|
|
|
|
|
expect(totalQP2).toBe(20); // Should have 20 QP (1st place)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it("should not increment majorsCompleted when reprocessing", () => {
|
|
|
|
|
// First processing
|
|
|
|
|
let majorsCompleted = 0;
|
|
|
|
|
const wasAlreadyProcessed = false;
|
|
|
|
|
|
|
|
|
|
if (!wasAlreadyProcessed) {
|
|
|
|
|
majorsCompleted += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expect(majorsCompleted).toBe(1);
|
|
|
|
|
|
|
|
|
|
// Reprocessing (wasAlreadyProcessed = true)
|
|
|
|
|
const reprocessing = true;
|
|
|
|
|
|
|
|
|
|
if (!reprocessing) {
|
|
|
|
|
majorsCompleted += 1;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
expect(majorsCompleted).toBe(1); // Should still be 1, not 2
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe("Scoring Workflow", () => {
|
|
|
|
|
it("should process 4 majors and calculate final standings", () => {
|
|
|
|
|
// Simulating a full season with 4 majors
|
|
|
|
|
const participants = [
|
|
|
|
|
{ name: "Tiger Woods", qp: 0, events: 0 },
|
|
|
|
|
{ name: "Rory McIlroy", qp: 0, events: 0 },
|
|
|
|
|
{ name: "Scottie Scheffler", qp: 0, events: 0 },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Event 1: The Masters
|
|
|
|
|
participants[0].qp += 20; // Tiger wins
|
|
|
|
|
participants[0].events++;
|
|
|
|
|
participants[1].qp += 14; // Rory 2nd
|
|
|
|
|
participants[1].events++;
|
|
|
|
|
participants[2].qp += 10; // Scottie 3rd
|
|
|
|
|
participants[2].events++;
|
|
|
|
|
|
|
|
|
|
// Event 2: PGA Championship
|
|
|
|
|
participants[1].qp += 20; // Rory wins
|
|
|
|
|
participants[1].events++;
|
|
|
|
|
participants[0].qp += 14; // Tiger 2nd
|
|
|
|
|
participants[0].events++;
|
|
|
|
|
participants[2].qp += 10; // Scottie 3rd
|
|
|
|
|
participants[2].events++;
|
|
|
|
|
|
|
|
|
|
// Event 3: U.S. Open
|
|
|
|
|
participants[2].qp += 20; // Scottie wins
|
|
|
|
|
participants[2].events++;
|
|
|
|
|
participants[0].qp += 14; // Tiger 2nd
|
|
|
|
|
participants[0].events++;
|
|
|
|
|
participants[1].qp += 10; // Rory 3rd
|
|
|
|
|
participants[1].events++;
|
|
|
|
|
|
|
|
|
|
// Event 4: The Open
|
|
|
|
|
participants[0].qp += 20; // Tiger wins
|
|
|
|
|
participants[0].events++;
|
|
|
|
|
participants[1].qp += 14; // Rory 2nd
|
|
|
|
|
participants[1].events++;
|
|
|
|
|
participants[2].qp += 10; // Scottie 3rd
|
|
|
|
|
participants[2].events++;
|
|
|
|
|
|
|
|
|
|
// Final standings
|
|
|
|
|
participants.sort((a, b) => b.qp - a.qp);
|
|
|
|
|
|
|
|
|
|
expect(participants[0].name).toBe("Tiger Woods");
|
|
|
|
|
expect(participants[0].qp).toBe(68); // 20+14+14+20
|
|
|
|
|
expect(participants[0].events).toBe(4);
|
|
|
|
|
|
|
|
|
|
expect(participants[1].name).toBe("Rory McIlroy");
|
|
|
|
|
expect(participants[1].qp).toBe(58); // 14+20+10+14
|
|
|
|
|
expect(participants[1].events).toBe(4);
|
|
|
|
|
|
|
|
|
|
expect(participants[2].name).toBe("Scottie Scheffler");
|
|
|
|
|
expect(participants[2].qp).toBe(50); // 10+10+20+10
|
|
|
|
|
expect(participants[2].events).toBe(4);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|