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