Fix QP standings: filter to drafted/points-earning participants with correct global ranks #76
7 changed files with 55 additions and 32 deletions
|
|
@ -23,6 +23,7 @@ describe("QualifyingPointsStandings", () => {
|
|||
totalQualifyingPoints: "14.67",
|
||||
eventsScored: 1,
|
||||
finalRanking: null,
|
||||
globalRank: 1,
|
||||
participant: { id: "participant-1", name: "Player One" },
|
||||
},
|
||||
{
|
||||
|
|
@ -30,6 +31,7 @@ describe("QualifyingPointsStandings", () => {
|
|||
totalQualifyingPoints: "0.50",
|
||||
eventsScored: 1,
|
||||
finalRanking: null,
|
||||
globalRank: 2,
|
||||
participant: { id: "participant-2", name: "Player Two" },
|
||||
},
|
||||
{
|
||||
|
|
@ -37,6 +39,7 @@ describe("QualifyingPointsStandings", () => {
|
|||
totalQualifyingPoints: "0.43",
|
||||
eventsScored: 1,
|
||||
finalRanking: null,
|
||||
globalRank: 3,
|
||||
participant: { id: "participant-3", name: "Player Three" },
|
||||
},
|
||||
]}
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ describe("SportsSection", () => {
|
|||
await user.click(screen.getByText("2025 MLB Season"));
|
||||
|
||||
expect(mlbCheckbox).toBeChecked();
|
||||
});
|
||||
}, 15000);
|
||||
|
||||
it("submits the currently selected sports seasons after toggling", async () => {
|
||||
const user = userEvent.setup();
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ interface QPStanding {
|
|||
totalQualifyingPoints: string;
|
||||
eventsScored: number;
|
||||
finalRanking: number | null;
|
||||
globalRank: number;
|
||||
participant: {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
@ -70,26 +71,11 @@ export function QualifyingPointsStandings({
|
|||
const ownershipMap = new Map(teamOwnerships.map((o) => [o.participantId, o]));
|
||||
const userParticipantSet = new Set(userParticipantIds);
|
||||
|
||||
// Assign current ranks with tie handling
|
||||
const rankedStandings: Array<QPStanding & { currentRank: number }> = [];
|
||||
let previousQP = -1;
|
||||
let previousRankStart = -1;
|
||||
|
||||
for (let index = 0; index < standings.length; index++) {
|
||||
const standing = standings[index];
|
||||
const currentQP = parseFloat(standing.totalQualifyingPoints);
|
||||
|
||||
let currentRank: number;
|
||||
if (index > 0 && Math.abs(currentQP - previousQP) < 0.001) {
|
||||
currentRank = previousRankStart;
|
||||
} else {
|
||||
currentRank = index + 1;
|
||||
}
|
||||
|
||||
rankedStandings.push({ ...standing, currentRank });
|
||||
previousRankStart = currentRank;
|
||||
previousQP = currentQP;
|
||||
}
|
||||
// Use the pre-computed global rank from the loader so displayed ranks reflect the full
|
||||
// participant field even when the array has been filtered down to drafted/points-earning rows.
|
||||
const rankedStandings: Array<QPStanding & { currentRank: number }> = standings.map(
|
||||
(standing) => ({ ...standing, currentRank: standing.globalRank })
|
||||
);
|
||||
|
||||
const tiedCountByRank = new Map<number, number>();
|
||||
for (const s of rankedStandings) {
|
||||
|
|
|
|||
|
|
@ -64,6 +64,7 @@ interface QPStanding {
|
|||
totalQualifyingPoints: string;
|
||||
eventsScored: number;
|
||||
finalRanking: number | null;
|
||||
globalRank: number;
|
||||
participant: {
|
||||
id: string;
|
||||
name: string;
|
||||
|
|
|
|||
|
|
@ -25,11 +25,25 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||
|
||||
const events = await getScoringEventsForSportsSeason(params.id);
|
||||
|
||||
// For qualifying sports seasons, get QP standings
|
||||
// For qualifying sports seasons, get QP standings with global ranks attached
|
||||
let qpStandings = null;
|
||||
const scoringRules = null;
|
||||
if (sportsSeason.scoringPattern === "qualifying_points") {
|
||||
qpStandings = await getQPStandings(params.id);
|
||||
const standings = await getQPStandings(params.id);
|
||||
let prevQP = -1;
|
||||
let prevRankStart = 1;
|
||||
qpStandings = standings.map((s, index) => {
|
||||
const qp = parseFloat(s.totalQualifyingPoints);
|
||||
let rank: number;
|
||||
if (index > 0 && Math.abs(qp - prevQP) < 0.001) {
|
||||
rank = prevRankStart;
|
||||
} else {
|
||||
rank = index + 1;
|
||||
prevRankStart = rank;
|
||||
}
|
||||
prevQP = qp;
|
||||
return { ...s, globalRank: rank };
|
||||
});
|
||||
}
|
||||
|
||||
// Tournaments for this sport that don't already have a scoring event in this season.
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
let participantPoints: { participantId: string; points: number }[] = [];
|
||||
let partialScoreParticipantIds: string[] = [];
|
||||
let seasonStandings: SeasonStanding[] = [];
|
||||
type QPStanding = Awaited<ReturnType<typeof getQPStandings>>[number];
|
||||
type QPStanding = Awaited<ReturnType<typeof getQPStandings>>[number] & { globalRank: number };
|
||||
let qpStandings: QPStanding[] = [];
|
||||
|
||||
// Group standings for group-stage events (e.g. FIFA World Cup)
|
||||
|
|
@ -293,9 +293,28 @@ export async function loader(args: Route.LoaderArgs) {
|
|||
},
|
||||
}));
|
||||
} else if (scoringPattern === "qualifying_points") {
|
||||
// Fetch qualifying points standings
|
||||
const standings = await getQPStandings(sportsSeasonId);
|
||||
qpStandings = standings;
|
||||
// Compute global ranks with tie handling across the full field before filtering,
|
||||
// so the displayed rank numbers remain correct after undrafted/zero-QP rows are removed.
|
||||
let prevQP = -1;
|
||||
let prevRankStart = 1;
|
||||
const withGlobalRank = standings.map((s, index) => {
|
||||
const qp = parseFloat(s.totalQualifyingPoints);
|
||||
let rank: number;
|
||||
if (index > 0 && Math.abs(qp - prevQP) < 0.001) {
|
||||
rank = prevRankStart;
|
||||
} else {
|
||||
rank = index + 1;
|
||||
prevRankStart = rank;
|
||||
}
|
||||
prevQP = qp;
|
||||
return { ...s, globalRank: rank };
|
||||
});
|
||||
qpStandings = withGlobalRank.filter(
|
||||
(s) =>
|
||||
parseFloat(s.totalQualifyingPoints) > 0 ||
|
||||
ownershipMap.has(s.participant.id)
|
||||
);
|
||||
}
|
||||
|
||||
// Fetch event schedule for all patterns (upcoming + recent)
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ describe("WorldCupSimulator", () => {
|
|||
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
|
||||
mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
|
||||
|
||||
const sim = new WorldCupSimulator(100);
|
||||
const sim = new WorldCupSimulator(20);
|
||||
await expect(sim.simulate("season-1")).rejects.toThrow("No participants found");
|
||||
});
|
||||
|
||||
|
|
@ -125,7 +125,7 @@ describe("WorldCupSimulator", () => {
|
|||
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
|
||||
mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
|
||||
|
||||
const sim = new WorldCupSimulator(100);
|
||||
const sim = new WorldCupSimulator(20);
|
||||
const results = await sim.simulate("season-1");
|
||||
expect(results).toHaveLength(48);
|
||||
const ids = new Set(results.map((r) => r.participantId));
|
||||
|
|
@ -141,7 +141,7 @@ describe("WorldCupSimulator", () => {
|
|||
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
|
||||
mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
|
||||
|
||||
const sim = new WorldCupSimulator(100);
|
||||
const sim = new WorldCupSimulator(20);
|
||||
const results = await sim.simulate("season-1");
|
||||
|
||||
const sumFirst = results.reduce((s, r) => s + r.probabilities.probFirst, 0);
|
||||
|
|
@ -162,7 +162,7 @@ describe("WorldCupSimulator", () => {
|
|||
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
|
||||
mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
|
||||
|
||||
const sim = new WorldCupSimulator(100);
|
||||
const sim = new WorldCupSimulator(50);
|
||||
const results = await sim.simulate("season-1");
|
||||
|
||||
// probFirst + probSecond + probThird + probFourth should cover all probability mass
|
||||
|
|
@ -218,7 +218,7 @@ describe("WorldCupSimulator", () => {
|
|||
mockDb.query.tournamentGroups.findMany.mockResolvedValue([group, ...remainingGroups]);
|
||||
mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
|
||||
|
||||
const sim = new WorldCupSimulator(100);
|
||||
const sim = new WorldCupSimulator(20);
|
||||
const results = await sim.simulate("season-1");
|
||||
|
||||
const p3Result = results.find((r) => r.participantId === "p3");
|
||||
|
|
@ -238,7 +238,7 @@ describe("WorldCupSimulator", () => {
|
|||
mockDb.query.tournamentGroups.findMany.mockResolvedValue([]);
|
||||
mockDb.query.playoffMatches.findMany.mockResolvedValue([]);
|
||||
|
||||
const sim = new WorldCupSimulator(100);
|
||||
const sim = new WorldCupSimulator(20);
|
||||
const results = await sim.simulate("season-1");
|
||||
|
||||
for (const r of results) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue