Add CS2 Major Qualifying Points simulator and stage management (#260)
* Add CS2 Major qualifying points simulator
Implements a full CS2 Major tournament simulator with:
- 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3)
+ Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5)
- Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season
- Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank
- Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3)
- Stage assignments stored per-event so actual field composition drives simulation
- Admin CS Elo form for entering team Elo + HLTV world rankings
- Admin CS2 stage setup page for assigning teams to stages and tracking advancement
- Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table
- 24 unit tests covering all exported pure functions
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate Elo + ranking input into generic elo-ratings page
The darts-elo and cs-elo pages were unreachable from the admin nav,
which always links to the generic elo-ratings page. Extended elo-ratings
to conditionally show world ranking fields for simulator types that need
it (darts_bracket, cs2_major_qualifying_points), then deleted the
redundant sport-specific pages.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate server postgres connections into one shared pool
Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).
Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix and() bug and add Swiss loop safety guard
- cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements
were using JS && instead of Drizzle and(), causing WHERE to filter only
by participantId (not scoringEventId), which would update rows across
all events instead of just the target event
- cs-major-simulator.ts: add break guard in simulateSwiss while loop to
prevent infinite loop if pairGroups returns no pairs
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix all remaining code review issues
- cs2-major-stage.ts: use schema column reference for stageEliminated
in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
results when a stage is complete (8 recorded eliminations), only
simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
React state (eliminatedChecked map) for checkbox show/hide logic;
remove unused stageMap and unassignedParticipants variables
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix oxlint errors: non-null assertions, sort→toSorted, unused vars
- cs-major-simulator.ts: replace 5 non-null assertions (!) with safe
optional chaining / if-guards; replace 6 .sort() with .toSorted()
- cs2-major-stage.ts: remove unused `inArray` import
- cs2-setup.tsx: remove unused `assignedIds` variable
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix flaky Champions Stage stochastic test
The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730).
With the Champions Stage bracket math this gives team-0 a ~19.6% win
rate — right at the 0.2 threshold, causing the test to fail ~63% of
the time in CI despite 1000 iterations.
Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate
and raising the assertion threshold to 0.25 for a clear safety margin.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 13:40:05 -07:00
|
|
|
|
import { describe, it, expect } from "vitest";
|
|
|
|
|
|
import {
|
|
|
|
|
|
gameWinProb,
|
|
|
|
|
|
seriesWinProb,
|
|
|
|
|
|
sampleField,
|
|
|
|
|
|
simulateSwiss,
|
|
|
|
|
|
simulateChampionsStage,
|
2026-04-07 17:28:24 -04:00
|
|
|
|
calcStage3ExitQP,
|
|
|
|
|
|
simulateOneMajor,
|
Add CS2 Major Qualifying Points simulator and stage management (#260)
* Add CS2 Major qualifying points simulator
Implements a full CS2 Major tournament simulator with:
- 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3)
+ Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5)
- Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season
- Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank
- Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3)
- Stage assignments stored per-event so actual field composition drives simulation
- Admin CS Elo form for entering team Elo + HLTV world rankings
- Admin CS2 stage setup page for assigning teams to stages and tracking advancement
- Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table
- 24 unit tests covering all exported pure functions
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate Elo + ranking input into generic elo-ratings page
The darts-elo and cs-elo pages were unreachable from the admin nav,
which always links to the generic elo-ratings page. Extended elo-ratings
to conditionally show world ranking fields for simulator types that need
it (darts_bracket, cs2_major_qualifying_points), then deleted the
redundant sport-specific pages.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate server postgres connections into one shared pool
Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).
Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix and() bug and add Swiss loop safety guard
- cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements
were using JS && instead of Drizzle and(), causing WHERE to filter only
by participantId (not scoringEventId), which would update rows across
all events instead of just the target event
- cs-major-simulator.ts: add break guard in simulateSwiss while loop to
prevent infinite loop if pairGroups returns no pairs
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix all remaining code review issues
- cs2-major-stage.ts: use schema column reference for stageEliminated
in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
results when a stage is complete (8 recorded eliminations), only
simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
React state (eliminatedChecked map) for checkbox show/hide logic;
remove unused stageMap and unassignedParticipants variables
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix oxlint errors: non-null assertions, sort→toSorted, unused vars
- cs-major-simulator.ts: replace 5 non-null assertions (!) with safe
optional chaining / if-guards; replace 6 .sort() with .toSorted()
- cs2-major-stage.ts: remove unused `inArray` import
- cs2-setup.tsx: remove unused `assignedIds` variable
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix flaky Champions Stage stochastic test
The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730).
With the Champions Stage bracket math this gives team-0 a ~19.6% win
rate — right at the 0.2 threshold, causing the test to fail ~63% of
the time in CI despite 1000 iterations.
Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate
and raising the assertion threshold to 0.25 for a clear safety margin.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 13:40:05 -07:00
|
|
|
|
} from "../cs-major-simulator";
|
2026-04-07 17:28:24 -04:00
|
|
|
|
import type { AdvancedTeam } from "../cs-major-simulator";
|
|
|
|
|
|
|
|
|
|
|
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
Add CS2 Major Qualifying Points simulator and stage management (#260)
* Add CS2 Major qualifying points simulator
Implements a full CS2 Major tournament simulator with:
- 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3)
+ Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5)
- Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season
- Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank
- Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3)
- Stage assignments stored per-event so actual field composition drives simulation
- Admin CS Elo form for entering team Elo + HLTV world rankings
- Admin CS2 stage setup page for assigning teams to stages and tracking advancement
- Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table
- 24 unit tests covering all exported pure functions
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate Elo + ranking input into generic elo-ratings page
The darts-elo and cs-elo pages were unreachable from the admin nav,
which always links to the generic elo-ratings page. Extended elo-ratings
to conditionally show world ranking fields for simulator types that need
it (darts_bracket, cs2_major_qualifying_points), then deleted the
redundant sport-specific pages.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate server postgres connections into one shared pool
Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).
Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix and() bug and add Swiss loop safety guard
- cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements
were using JS && instead of Drizzle and(), causing WHERE to filter only
by participantId (not scoringEventId), which would update rows across
all events instead of just the target event
- cs-major-simulator.ts: add break guard in simulateSwiss while loop to
prevent infinite loop if pairGroups returns no pairs
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix all remaining code review issues
- cs2-major-stage.ts: use schema column reference for stageEliminated
in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
results when a stage is complete (8 recorded eliminations), only
simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
React state (eliminatedChecked map) for checkbox show/hide logic;
remove unused stageMap and unassignedParticipants variables
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix oxlint errors: non-null assertions, sort→toSorted, unused vars
- cs-major-simulator.ts: replace 5 non-null assertions (!) with safe
optional chaining / if-guards; replace 6 .sort() with .toSorted()
- cs2-major-stage.ts: remove unused `inArray` import
- cs2-setup.tsx: remove unused `assignedIds` variable
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix flaky Champions Stage stochastic test
The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730).
With the Champions Stage bracket math this gives team-0 a ~19.6% win
rate — right at the 0.2 threshold, causing the test to fail ~63% of
the time in CI despite 1000 iterations.
Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate
and raising the assertion threshold to 0.25 for a clear safety margin.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 13:40:05 -07:00
|
|
|
|
|
|
|
|
|
|
function makeTeams(n: number, baseElo = 1800, baseRank = 1) {
|
|
|
|
|
|
return Array.from({ length: n }, (_, i) => ({
|
|
|
|
|
|
id: `team-${i}`,
|
|
|
|
|
|
elo: baseElo - i * 10,
|
|
|
|
|
|
rank: baseRank + i,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-07 17:28:24 -04:00
|
|
|
|
function makeAdvancedTeams(n: number, losses = 0, baseElo = 1800, baseRank = 1): AdvancedTeam[] {
|
|
|
|
|
|
return Array.from({ length: n }, (_, i) => ({
|
|
|
|
|
|
id: `team-${i}`,
|
|
|
|
|
|
elo: baseElo - i * 10,
|
|
|
|
|
|
rank: baseRank + i,
|
|
|
|
|
|
losses,
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function makeQPConfig(): Map<number, number> {
|
|
|
|
|
|
const config = new Map<number, number>();
|
|
|
|
|
|
config.set(1, 500);
|
|
|
|
|
|
config.set(2, 400);
|
|
|
|
|
|
config.set(3, 325);
|
|
|
|
|
|
config.set(4, 250);
|
|
|
|
|
|
config.set(5, 175);
|
|
|
|
|
|
config.set(6, 125);
|
|
|
|
|
|
config.set(7, 75);
|
|
|
|
|
|
config.set(8, 50);
|
|
|
|
|
|
for (let i = 9; i <= 16; i++) config.set(i, (17 - i) * 10);
|
|
|
|
|
|
return config;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
Add CS2 Major Qualifying Points simulator and stage management (#260)
* Add CS2 Major qualifying points simulator
Implements a full CS2 Major tournament simulator with:
- 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3)
+ Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5)
- Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season
- Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank
- Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3)
- Stage assignments stored per-event so actual field composition drives simulation
- Admin CS Elo form for entering team Elo + HLTV world rankings
- Admin CS2 stage setup page for assigning teams to stages and tracking advancement
- Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table
- 24 unit tests covering all exported pure functions
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate Elo + ranking input into generic elo-ratings page
The darts-elo and cs-elo pages were unreachable from the admin nav,
which always links to the generic elo-ratings page. Extended elo-ratings
to conditionally show world ranking fields for simulator types that need
it (darts_bracket, cs2_major_qualifying_points), then deleted the
redundant sport-specific pages.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate server postgres connections into one shared pool
Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).
Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix and() bug and add Swiss loop safety guard
- cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements
were using JS && instead of Drizzle and(), causing WHERE to filter only
by participantId (not scoringEventId), which would update rows across
all events instead of just the target event
- cs-major-simulator.ts: add break guard in simulateSwiss while loop to
prevent infinite loop if pairGroups returns no pairs
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix all remaining code review issues
- cs2-major-stage.ts: use schema column reference for stageEliminated
in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
results when a stage is complete (8 recorded eliminations), only
simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
React state (eliminatedChecked map) for checkbox show/hide logic;
remove unused stageMap and unassignedParticipants variables
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix oxlint errors: non-null assertions, sort→toSorted, unused vars
- cs-major-simulator.ts: replace 5 non-null assertions (!) with safe
optional chaining / if-guards; replace 6 .sort() with .toSorted()
- cs2-major-stage.ts: remove unused `inArray` import
- cs2-setup.tsx: remove unused `assignedIds` variable
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix flaky Champions Stage stochastic test
The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730).
With the Champions Stage bracket math this gives team-0 a ~19.6% win
rate — right at the 0.2 threshold, causing the test to fail ~63% of
the time in CI despite 1000 iterations.
Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate
and raising the assertion threshold to 0.25 for a clear safety margin.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 13:40:05 -07:00
|
|
|
|
// ─── gameWinProb ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("gameWinProb", () => {
|
|
|
|
|
|
it("returns 0.5 for equal Elo", () => {
|
|
|
|
|
|
expect(gameWinProb(1800, 1800)).toBeCloseTo(0.5, 5);
|
|
|
|
|
|
expect(gameWinProb(2000, 2000)).toBeCloseTo(0.5, 5);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("returns > 0.5 for positive advantage, < 0.5 for negative", () => {
|
|
|
|
|
|
expect(gameWinProb(2000, 1700)).toBeGreaterThan(0.5);
|
|
|
|
|
|
expect(gameWinProb(1700, 2000)).toBeLessThan(0.5);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("is symmetric: p(a,b) + p(b,a) = 1", () => {
|
|
|
|
|
|
expect(gameWinProb(2000, 1700) + gameWinProb(1700, 2000)).toBeCloseTo(1.0, 5);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("stays strictly in (0, 1)", () => {
|
|
|
|
|
|
const p = gameWinProb(3000, 1000);
|
|
|
|
|
|
expect(p).toBeLessThan(1);
|
|
|
|
|
|
expect(p).toBeGreaterThan(0);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── seriesWinProb ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("seriesWinProb", () => {
|
|
|
|
|
|
it("returns 0.5 for equal players (p=0.5) in any format", () => {
|
|
|
|
|
|
expect(seriesWinProb(0.5, 2)).toBeCloseTo(0.5, 5); // Bo3
|
|
|
|
|
|
expect(seriesWinProb(0.5, 3)).toBeCloseTo(0.5, 5); // Bo5
|
|
|
|
|
|
expect(seriesWinProb(0.5, 4)).toBeCloseTo(0.5, 5); // Bo7
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("amplifies better team advantage in longer formats", () => {
|
|
|
|
|
|
const bo3 = seriesWinProb(0.6, 2);
|
|
|
|
|
|
const bo5 = seriesWinProb(0.6, 3);
|
|
|
|
|
|
expect(bo5).toBeGreaterThan(bo3);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("approaches 1.0 as p → 1.0", () => {
|
|
|
|
|
|
expect(seriesWinProb(0.9999, 2)).toBeCloseTo(1.0, 3);
|
|
|
|
|
|
expect(seriesWinProb(0.9999, 3)).toBeCloseTo(1.0, 3);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("sums to 1 with complement", () => {
|
|
|
|
|
|
const p = 0.63;
|
|
|
|
|
|
expect(seriesWinProb(p, 2) + seriesWinProb(1 - p, 2)).toBeCloseTo(1.0, 5);
|
|
|
|
|
|
expect(seriesWinProb(p, 3) + seriesWinProb(1 - p, 3)).toBeCloseTo(1.0, 5);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("Bo3 win prob with p=0.6 is in expected range", () => {
|
|
|
|
|
|
// P(2-0) + P(2-1) = 0.36 + 2*0.36*0.4 = 0.36 + 0.288 = 0.648
|
|
|
|
|
|
expect(seriesWinProb(0.6, 2)).toBeCloseTo(0.648, 3);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── sampleField ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("sampleField", () => {
|
|
|
|
|
|
it("returns all teams when pool <= fieldSize", () => {
|
|
|
|
|
|
const pool = makeTeams(20);
|
|
|
|
|
|
const field = sampleField(pool, 32);
|
|
|
|
|
|
expect(field).toHaveLength(20);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("returns fieldSize teams when pool > fieldSize", () => {
|
|
|
|
|
|
const pool = makeTeams(50);
|
|
|
|
|
|
const field = sampleField(pool, 32);
|
|
|
|
|
|
expect(field).toHaveLength(32);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("always includes the top 12 ranked teams", () => {
|
|
|
|
|
|
const pool = makeTeams(50);
|
|
|
|
|
|
const top12Ids = new Set(pool.slice(0, 12).map((t) => t.id));
|
|
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
|
|
|
|
const field = sampleField(pool, 32);
|
|
|
|
|
|
const fieldIds = new Set(field.map((t) => t.id));
|
|
|
|
|
|
for (const id of top12Ids) {
|
|
|
|
|
|
expect(fieldIds.has(id)).toBe(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("never includes more teams than fieldSize", () => {
|
|
|
|
|
|
const pool = makeTeams(100);
|
|
|
|
|
|
for (let i = 0; i < 10; i++) {
|
|
|
|
|
|
expect(sampleField(pool, 32)).toHaveLength(32);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("returns unique teams", () => {
|
|
|
|
|
|
const pool = makeTeams(50);
|
|
|
|
|
|
const field = sampleField(pool, 32);
|
|
|
|
|
|
const ids = field.map((t) => t.id);
|
|
|
|
|
|
expect(new Set(ids).size).toBe(ids.length);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── simulateSwiss ────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("simulateSwiss", () => {
|
|
|
|
|
|
it("returns exactly 8 advanced and 8 eliminated from 16 teams", () => {
|
|
|
|
|
|
const teams = makeTeams(16);
|
|
|
|
|
|
const result = simulateSwiss(teams, false);
|
|
|
|
|
|
expect(result.advanced).toHaveLength(8);
|
|
|
|
|
|
expect(result.eliminated).toHaveLength(8);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("all advanced teams have 3 wins (implicit by advancement threshold)", () => {
|
|
|
|
|
|
const teams = makeTeams(16);
|
|
|
|
|
|
const result = simulateSwiss(teams, false);
|
|
|
|
|
|
// Advanced teams reached 3 wins — we can't directly check wins here,
|
|
|
|
|
|
// but we verify each participant appears in exactly one group
|
|
|
|
|
|
const allIds = new Set([
|
|
|
|
|
|
...result.advanced.map((t) => t.id),
|
|
|
|
|
|
...result.eliminated.map((t) => t.id),
|
|
|
|
|
|
]);
|
|
|
|
|
|
expect(allIds.size).toBe(16);
|
|
|
|
|
|
for (const team of teams) {
|
|
|
|
|
|
expect(allIds.has(team.id)).toBe(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("eliminated teams have wins 0, 1, or 2", () => {
|
|
|
|
|
|
const teams = makeTeams(16);
|
|
|
|
|
|
const result = simulateSwiss(teams, false);
|
|
|
|
|
|
for (const t of result.eliminated) {
|
|
|
|
|
|
expect(t.wins).toBeGreaterThanOrEqual(0);
|
|
|
|
|
|
expect(t.wins).toBeLessThanOrEqual(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-07 17:28:24 -04:00
|
|
|
|
it("advanced teams have a losses field with value 0, 1, or 2", () => {
|
|
|
|
|
|
const teams = makeTeams(16);
|
|
|
|
|
|
const result = simulateSwiss(teams, false);
|
|
|
|
|
|
for (const t of result.advanced) {
|
|
|
|
|
|
expect(typeof t.losses).toBe("number");
|
|
|
|
|
|
expect(t.losses).toBeGreaterThanOrEqual(0);
|
|
|
|
|
|
expect(t.losses).toBeLessThanOrEqual(2);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
Add CS2 Major Qualifying Points simulator and stage management (#260)
* Add CS2 Major qualifying points simulator
Implements a full CS2 Major tournament simulator with:
- 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3)
+ Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5)
- Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season
- Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank
- Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3)
- Stage assignments stored per-event so actual field composition drives simulation
- Admin CS Elo form for entering team Elo + HLTV world rankings
- Admin CS2 stage setup page for assigning teams to stages and tracking advancement
- Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table
- 24 unit tests covering all exported pure functions
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate Elo + ranking input into generic elo-ratings page
The darts-elo and cs-elo pages were unreachable from the admin nav,
which always links to the generic elo-ratings page. Extended elo-ratings
to conditionally show world ranking fields for simulator types that need
it (darts_bracket, cs2_major_qualifying_points), then deleted the
redundant sport-specific pages.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate server postgres connections into one shared pool
Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).
Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix and() bug and add Swiss loop safety guard
- cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements
were using JS && instead of Drizzle and(), causing WHERE to filter only
by participantId (not scoringEventId), which would update rows across
all events instead of just the target event
- cs-major-simulator.ts: add break guard in simulateSwiss while loop to
prevent infinite loop if pairGroups returns no pairs
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix all remaining code review issues
- cs2-major-stage.ts: use schema column reference for stageEliminated
in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
results when a stage is complete (8 recorded eliminations), only
simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
React state (eliminatedChecked map) for checkbox show/hide logic;
remove unused stageMap and unassignedParticipants variables
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix oxlint errors: non-null assertions, sort→toSorted, unused vars
- cs-major-simulator.ts: replace 5 non-null assertions (!) with safe
optional chaining / if-guards; replace 6 .sort() with .toSorted()
- cs2-major-stage.ts: remove unused `inArray` import
- cs2-setup.tsx: remove unused `assignedIds` variable
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix flaky Champions Stage stochastic test
The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730).
With the Champions Stage bracket math this gives team-0 a ~19.6% win
rate — right at the 0.2 threshold, causing the test to fail ~63% of
the time in CI despite 1000 iterations.
Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate
and raising the assertion threshold to 0.25 for a clear safety margin.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 13:40:05 -07:00
|
|
|
|
it("total wins + losses = total matches played (conservation check)", () => {
|
|
|
|
|
|
const teams = makeTeams(16);
|
|
|
|
|
|
const result = simulateSwiss(teams, false);
|
|
|
|
|
|
// Eliminated teams have exactly 3 losses
|
|
|
|
|
|
// Total losses = 8 * 3 = 24
|
|
|
|
|
|
// Total wins = sum of wins for all eliminated + 8 * 3 for advanced = ?
|
|
|
|
|
|
// Each win by advanced team = 1 loss for an eliminated team
|
|
|
|
|
|
const totalLossesEliminated = result.eliminated.length * 3; // 24
|
|
|
|
|
|
expect(totalLossesEliminated).toBe(24);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("works with Bo3 format (Swiss all Bo3)", () => {
|
|
|
|
|
|
const teams = makeTeams(16);
|
|
|
|
|
|
const result = simulateSwiss(teams, true);
|
|
|
|
|
|
expect(result.advanced).toHaveLength(8);
|
|
|
|
|
|
expect(result.eliminated).toHaveLength(8);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-07 17:28:24 -04:00
|
|
|
|
it("works with decisiveMatchesBo3 enabled", () => {
|
|
|
|
|
|
const teams = makeTeams(16);
|
|
|
|
|
|
const result = simulateSwiss(teams, false, true);
|
|
|
|
|
|
expect(result.advanced).toHaveLength(8);
|
|
|
|
|
|
expect(result.eliminated).toHaveLength(8);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("returns empty arrays for 0 teams", () => {
|
|
|
|
|
|
const result = simulateSwiss([], false);
|
|
|
|
|
|
expect(result.advanced).toHaveLength(0);
|
|
|
|
|
|
expect(result.eliminated).toHaveLength(0);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("throws for odd team count", () => {
|
|
|
|
|
|
expect(() => simulateSwiss(makeTeams(15), false)).toThrow(/even number of teams/);
|
|
|
|
|
|
expect(() => simulateSwiss(makeTeams(1), false)).toThrow(/even number of teams/);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
Add CS2 Major Qualifying Points simulator and stage management (#260)
* Add CS2 Major qualifying points simulator
Implements a full CS2 Major tournament simulator with:
- 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3)
+ Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5)
- Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season
- Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank
- Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3)
- Stage assignments stored per-event so actual field composition drives simulation
- Admin CS Elo form for entering team Elo + HLTV world rankings
- Admin CS2 stage setup page for assigning teams to stages and tracking advancement
- Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table
- 24 unit tests covering all exported pure functions
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate Elo + ranking input into generic elo-ratings page
The darts-elo and cs-elo pages were unreachable from the admin nav,
which always links to the generic elo-ratings page. Extended elo-ratings
to conditionally show world ranking fields for simulator types that need
it (darts_bracket, cs2_major_qualifying_points), then deleted the
redundant sport-specific pages.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate server postgres connections into one shared pool
Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).
Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix and() bug and add Swiss loop safety guard
- cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements
were using JS && instead of Drizzle and(), causing WHERE to filter only
by participantId (not scoringEventId), which would update rows across
all events instead of just the target event
- cs-major-simulator.ts: add break guard in simulateSwiss while loop to
prevent infinite loop if pairGroups returns no pairs
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix all remaining code review issues
- cs2-major-stage.ts: use schema column reference for stageEliminated
in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
results when a stage is complete (8 recorded eliminations), only
simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
React state (eliminatedChecked map) for checkbox show/hide logic;
remove unused stageMap and unassignedParticipants variables
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix oxlint errors: non-null assertions, sort→toSorted, unused vars
- cs-major-simulator.ts: replace 5 non-null assertions (!) with safe
optional chaining / if-guards; replace 6 .sort() with .toSorted()
- cs2-major-stage.ts: remove unused `inArray` import
- cs2-setup.tsx: remove unused `assignedIds` variable
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix flaky Champions Stage stochastic test
The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730).
With the Champions Stage bracket math this gives team-0 a ~19.6% win
rate — right at the 0.2 threshold, causing the test to fail ~63% of
the time in CI despite 1000 iterations.
Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate
and raising the assertion threshold to 0.25 for a clear safety margin.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 13:40:05 -07:00
|
|
|
|
it("teams with much higher Elo advance more often (stochastic check)", () => {
|
|
|
|
|
|
// Top 8 teams have Elo 2000+, bottom 8 have Elo 1000
|
|
|
|
|
|
const teams = [
|
|
|
|
|
|
...Array.from({ length: 8 }, (_, i) => ({ id: `top-${i}`, elo: 2000, rank: i + 1 })),
|
|
|
|
|
|
...Array.from({ length: 8 }, (_, i) => ({ id: `bot-${i}`, elo: 1000, rank: i + 9 })),
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
let topAdvances = 0;
|
|
|
|
|
|
const runs = 100;
|
|
|
|
|
|
for (let i = 0; i < runs; i++) {
|
|
|
|
|
|
const result = simulateSwiss(teams, false);
|
|
|
|
|
|
for (const t of result.advanced) {
|
|
|
|
|
|
if (t.id.startsWith("top-")) topAdvances++;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Top teams should advance much more than half the time across runs
|
|
|
|
|
|
// Expected: ~7-8 top teams advance per run → topAdvances should be >> 400 (50%)
|
|
|
|
|
|
expect(topAdvances / runs).toBeGreaterThan(6);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── simulateChampionsStage ───────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("simulateChampionsStage", () => {
|
|
|
|
|
|
it("throws if not exactly 8 teams", () => {
|
2026-04-07 17:28:24 -04:00
|
|
|
|
expect(() => simulateChampionsStage(makeAdvancedTeams(7))).toThrow();
|
|
|
|
|
|
expect(() => simulateChampionsStage(makeAdvancedTeams(9))).toThrow();
|
Add CS2 Major Qualifying Points simulator and stage management (#260)
* Add CS2 Major qualifying points simulator
Implements a full CS2 Major tournament simulator with:
- 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3)
+ Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5)
- Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season
- Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank
- Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3)
- Stage assignments stored per-event so actual field composition drives simulation
- Admin CS Elo form for entering team Elo + HLTV world rankings
- Admin CS2 stage setup page for assigning teams to stages and tracking advancement
- Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table
- 24 unit tests covering all exported pure functions
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate Elo + ranking input into generic elo-ratings page
The darts-elo and cs-elo pages were unreachable from the admin nav,
which always links to the generic elo-ratings page. Extended elo-ratings
to conditionally show world ranking fields for simulator types that need
it (darts_bracket, cs2_major_qualifying_points), then deleted the
redundant sport-specific pages.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate server postgres connections into one shared pool
Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).
Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix and() bug and add Swiss loop safety guard
- cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements
were using JS && instead of Drizzle and(), causing WHERE to filter only
by participantId (not scoringEventId), which would update rows across
all events instead of just the target event
- cs-major-simulator.ts: add break guard in simulateSwiss while loop to
prevent infinite loop if pairGroups returns no pairs
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix all remaining code review issues
- cs2-major-stage.ts: use schema column reference for stageEliminated
in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
results when a stage is complete (8 recorded eliminations), only
simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
React state (eliminatedChecked map) for checkbox show/hide logic;
remove unused stageMap and unassignedParticipants variables
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix oxlint errors: non-null assertions, sort→toSorted, unused vars
- cs-major-simulator.ts: replace 5 non-null assertions (!) with safe
optional chaining / if-guards; replace 6 .sort() with .toSorted()
- cs2-major-stage.ts: remove unused `inArray` import
- cs2-setup.tsx: remove unused `assignedIds` variable
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix flaky Champions Stage stochastic test
The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730).
With the Champions Stage bracket math this gives team-0 a ~19.6% win
rate — right at the 0.2 threshold, causing the test to fail ~63% of
the time in CI despite 1000 iterations.
Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate
and raising the assertion threshold to 0.25 for a clear safety margin.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 13:40:05 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("assigns placements to all 8 teams", () => {
|
2026-04-07 17:28:24 -04:00
|
|
|
|
const teams = makeAdvancedTeams(8);
|
Add CS2 Major Qualifying Points simulator and stage management (#260)
* Add CS2 Major qualifying points simulator
Implements a full CS2 Major tournament simulator with:
- 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3)
+ Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5)
- Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season
- Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank
- Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3)
- Stage assignments stored per-event so actual field composition drives simulation
- Admin CS Elo form for entering team Elo + HLTV world rankings
- Admin CS2 stage setup page for assigning teams to stages and tracking advancement
- Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table
- 24 unit tests covering all exported pure functions
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate Elo + ranking input into generic elo-ratings page
The darts-elo and cs-elo pages were unreachable from the admin nav,
which always links to the generic elo-ratings page. Extended elo-ratings
to conditionally show world ranking fields for simulator types that need
it (darts_bracket, cs2_major_qualifying_points), then deleted the
redundant sport-specific pages.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate server postgres connections into one shared pool
Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).
Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix and() bug and add Swiss loop safety guard
- cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements
were using JS && instead of Drizzle and(), causing WHERE to filter only
by participantId (not scoringEventId), which would update rows across
all events instead of just the target event
- cs-major-simulator.ts: add break guard in simulateSwiss while loop to
prevent infinite loop if pairGroups returns no pairs
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix all remaining code review issues
- cs2-major-stage.ts: use schema column reference for stageEliminated
in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
results when a stage is complete (8 recorded eliminations), only
simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
React state (eliminatedChecked map) for checkbox show/hide logic;
remove unused stageMap and unassignedParticipants variables
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix oxlint errors: non-null assertions, sort→toSorted, unused vars
- cs-major-simulator.ts: replace 5 non-null assertions (!) with safe
optional chaining / if-guards; replace 6 .sort() with .toSorted()
- cs2-major-stage.ts: remove unused `inArray` import
- cs2-setup.tsx: remove unused `assignedIds` variable
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix flaky Champions Stage stochastic test
The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730).
With the Champions Stage bracket math this gives team-0 a ~19.6% win
rate — right at the 0.2 threshold, causing the test to fail ~63% of
the time in CI despite 1000 iterations.
Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate
and raising the assertion threshold to 0.25 for a clear safety margin.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 13:40:05 -07:00
|
|
|
|
const result = simulateChampionsStage(teams);
|
|
|
|
|
|
expect(result.placements.size).toBe(8);
|
|
|
|
|
|
for (const team of teams) {
|
|
|
|
|
|
expect(result.placements.has(team.id)).toBe(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-07 17:28:24 -04:00
|
|
|
|
it("has exactly one 1st, one 2nd, two placement-3s, four placement-5s", () => {
|
|
|
|
|
|
const teams = makeAdvancedTeams(8);
|
Add CS2 Major Qualifying Points simulator and stage management (#260)
* Add CS2 Major qualifying points simulator
Implements a full CS2 Major tournament simulator with:
- 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3)
+ Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5)
- Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season
- Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank
- Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3)
- Stage assignments stored per-event so actual field composition drives simulation
- Admin CS Elo form for entering team Elo + HLTV world rankings
- Admin CS2 stage setup page for assigning teams to stages and tracking advancement
- Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table
- 24 unit tests covering all exported pure functions
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate Elo + ranking input into generic elo-ratings page
The darts-elo and cs-elo pages were unreachable from the admin nav,
which always links to the generic elo-ratings page. Extended elo-ratings
to conditionally show world ranking fields for simulator types that need
it (darts_bracket, cs2_major_qualifying_points), then deleted the
redundant sport-specific pages.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate server postgres connections into one shared pool
Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).
Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix and() bug and add Swiss loop safety guard
- cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements
were using JS && instead of Drizzle and(), causing WHERE to filter only
by participantId (not scoringEventId), which would update rows across
all events instead of just the target event
- cs-major-simulator.ts: add break guard in simulateSwiss while loop to
prevent infinite loop if pairGroups returns no pairs
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix all remaining code review issues
- cs2-major-stage.ts: use schema column reference for stageEliminated
in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
results when a stage is complete (8 recorded eliminations), only
simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
React state (eliminatedChecked map) for checkbox show/hide logic;
remove unused stageMap and unassignedParticipants variables
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix oxlint errors: non-null assertions, sort→toSorted, unused vars
- cs-major-simulator.ts: replace 5 non-null assertions (!) with safe
optional chaining / if-guards; replace 6 .sort() with .toSorted()
- cs2-major-stage.ts: remove unused `inArray` import
- cs2-setup.tsx: remove unused `assignedIds` variable
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix flaky Champions Stage stochastic test
The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730).
With the Champions Stage bracket math this gives team-0 a ~19.6% win
rate — right at the 0.2 threshold, causing the test to fail ~63% of
the time in CI despite 1000 iterations.
Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate
and raising the assertion threshold to 0.25 for a clear safety margin.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 13:40:05 -07:00
|
|
|
|
const result = simulateChampionsStage(teams);
|
|
|
|
|
|
const placements = [...result.placements.values()];
|
2026-04-07 17:28:24 -04:00
|
|
|
|
expect(placements.filter((p) => p === 1)).toHaveLength(1);
|
|
|
|
|
|
expect(placements.filter((p) => p === 2)).toHaveLength(1);
|
|
|
|
|
|
expect(placements.filter((p) => p === 3)).toHaveLength(2); // both SF losers
|
|
|
|
|
|
expect(placements.filter((p) => p === 5)).toHaveLength(4); // all QF losers
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("seeds by losses ascending, then rank as tiebreaker", () => {
|
|
|
|
|
|
// Team with 0 losses gets seeded 1st regardless of world rank.
|
|
|
|
|
|
// Make all teams equal Elo so seeding determines matchups purely.
|
|
|
|
|
|
// team-0 has 0 losses (seed 1) vs team-7 has 2 losses (seed 8).
|
|
|
|
|
|
const teams: AdvancedTeam[] = [
|
|
|
|
|
|
{ id: "a", elo: 1800, rank: 1, losses: 0 },
|
|
|
|
|
|
{ id: "b", elo: 1800, rank: 2, losses: 0 },
|
|
|
|
|
|
{ id: "c", elo: 1800, rank: 3, losses: 1 },
|
|
|
|
|
|
{ id: "d", elo: 1800, rank: 4, losses: 1 },
|
|
|
|
|
|
{ id: "e", elo: 1800, rank: 5, losses: 2 },
|
|
|
|
|
|
{ id: "f", elo: 1800, rank: 6, losses: 2 },
|
|
|
|
|
|
{ id: "g", elo: 1800, rank: 7, losses: 2 },
|
|
|
|
|
|
{ id: "h", elo: 1800, rank: 8, losses: 2 },
|
|
|
|
|
|
];
|
|
|
|
|
|
// With equal Elo, seeding by losses produces consistent bracket structure.
|
|
|
|
|
|
// Just verify it runs correctly and produces valid placements.
|
|
|
|
|
|
const result = simulateChampionsStage(teams);
|
|
|
|
|
|
expect(result.placements.size).toBe(8);
|
|
|
|
|
|
const values = [...result.placements.values()];
|
|
|
|
|
|
expect(values).toContain(1);
|
|
|
|
|
|
expect(values).toContain(2);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("rank used as tiebreaker when losses are equal", () => {
|
|
|
|
|
|
const teams = makeAdvancedTeams(8, 1); // all have losses = 1
|
|
|
|
|
|
const result = simulateChampionsStage(teams);
|
|
|
|
|
|
expect(result.placements.size).toBe(8);
|
Add CS2 Major Qualifying Points simulator and stage management (#260)
* Add CS2 Major qualifying points simulator
Implements a full CS2 Major tournament simulator with:
- 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3)
+ Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5)
- Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season
- Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank
- Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3)
- Stage assignments stored per-event so actual field composition drives simulation
- Admin CS Elo form for entering team Elo + HLTV world rankings
- Admin CS2 stage setup page for assigning teams to stages and tracking advancement
- Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table
- 24 unit tests covering all exported pure functions
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate Elo + ranking input into generic elo-ratings page
The darts-elo and cs-elo pages were unreachable from the admin nav,
which always links to the generic elo-ratings page. Extended elo-ratings
to conditionally show world ranking fields for simulator types that need
it (darts_bracket, cs2_major_qualifying_points), then deleted the
redundant sport-specific pages.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate server postgres connections into one shared pool
Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).
Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix and() bug and add Swiss loop safety guard
- cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements
were using JS && instead of Drizzle and(), causing WHERE to filter only
by participantId (not scoringEventId), which would update rows across
all events instead of just the target event
- cs-major-simulator.ts: add break guard in simulateSwiss while loop to
prevent infinite loop if pairGroups returns no pairs
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix all remaining code review issues
- cs2-major-stage.ts: use schema column reference for stageEliminated
in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
results when a stage is complete (8 recorded eliminations), only
simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
React state (eliminatedChecked map) for checkbox show/hide logic;
remove unused stageMap and unassignedParticipants variables
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix oxlint errors: non-null assertions, sort→toSorted, unused vars
- cs-major-simulator.ts: replace 5 non-null assertions (!) with safe
optional chaining / if-guards; replace 6 .sort() with .toSorted()
- cs2-major-stage.ts: remove unused `inArray` import
- cs2-setup.tsx: remove unused `assignedIds` variable
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix flaky Champions Stage stochastic test
The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730).
With the Champions Stage bracket math this gives team-0 a ~19.6% win
rate — right at the 0.2 threshold, causing the test to fail ~63% of
the time in CI despite 1000 iterations.
Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate
and raising the assertion threshold to 0.25 for a clear safety margin.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 13:40:05 -07:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("top Elo team wins more often than last (stochastic)", () => {
|
2026-04-07 17:28:24 -04:00
|
|
|
|
const teams = Array.from({ length: 8 }, (_, i): AdvancedTeam => ({
|
Add CS2 Major Qualifying Points simulator and stage management (#260)
* Add CS2 Major qualifying points simulator
Implements a full CS2 Major tournament simulator with:
- 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3)
+ Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5)
- Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season
- Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank
- Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3)
- Stage assignments stored per-event so actual field composition drives simulation
- Admin CS Elo form for entering team Elo + HLTV world rankings
- Admin CS2 stage setup page for assigning teams to stages and tracking advancement
- Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table
- 24 unit tests covering all exported pure functions
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate Elo + ranking input into generic elo-ratings page
The darts-elo and cs-elo pages were unreachable from the admin nav,
which always links to the generic elo-ratings page. Extended elo-ratings
to conditionally show world ranking fields for simulator types that need
it (darts_bracket, cs2_major_qualifying_points), then deleted the
redundant sport-specific pages.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate server postgres connections into one shared pool
Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).
Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix and() bug and add Swiss loop safety guard
- cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements
were using JS && instead of Drizzle and(), causing WHERE to filter only
by participantId (not scoringEventId), which would update rows across
all events instead of just the target event
- cs-major-simulator.ts: add break guard in simulateSwiss while loop to
prevent infinite loop if pairGroups returns no pairs
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix all remaining code review issues
- cs2-major-stage.ts: use schema column reference for stageEliminated
in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
results when a stage is complete (8 recorded eliminations), only
simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
React state (eliminatedChecked map) for checkbox show/hide logic;
remove unused stageMap and unassignedParticipants variables
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix oxlint errors: non-null assertions, sort→toSorted, unused vars
- cs-major-simulator.ts: replace 5 non-null assertions (!) with safe
optional chaining / if-guards; replace 6 .sort() with .toSorted()
- cs2-major-stage.ts: remove unused `inArray` import
- cs2-setup.tsx: remove unused `assignedIds` variable
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix flaky Champions Stage stochastic test
The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730).
With the Champions Stage bracket math this gives team-0 a ~19.6% win
rate — right at the 0.2 threshold, causing the test to fail ~63% of
the time in CI despite 1000 iterations.
Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate
and raising the assertion threshold to 0.25 for a clear safety margin.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 13:40:05 -07:00
|
|
|
|
id: `team-${i}`,
|
|
|
|
|
|
elo: 1800 - i * 100,
|
|
|
|
|
|
rank: i + 1,
|
2026-04-07 17:28:24 -04:00
|
|
|
|
losses: 0,
|
Add CS2 Major Qualifying Points simulator and stage management (#260)
* Add CS2 Major qualifying points simulator
Implements a full CS2 Major tournament simulator with:
- 3-stage Swiss format (Opening Bo1, Elimination Bo1/Bo3, Decider all Bo3)
+ Champions Stage 8-team single-elimination (QF Bo3, SF Bo3, GF Bo5)
- Monte Carlo simulation (10,000 iterations) accumulating QP across 2 majors/season
- Sampled 24-team field per iteration: top 12 guaranteed, remaining weighted by 1/rank
- Stage 3 exits (placements 9-16) sub-ranked by W-L record (2-3 > 1-3 > 0-3)
- Stage assignments stored per-event so actual field composition drives simulation
- Admin CS Elo form for entering team Elo + HLTV world rankings
- Admin CS2 stage setup page for assigning teams to stages and tracking advancement
- Database migration: cs2_major_qualifying_points enum value + cs2_major_stage_results table
- 24 unit tests covering all exported pure functions
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate Elo + ranking input into generic elo-ratings page
The darts-elo and cs-elo pages were unreachable from the admin nav,
which always links to the generic elo-ratings page. Extended elo-ratings
to conditionally show world ranking fields for simulator types that need
it (darts_bracket, cs2_major_qualifying_points), then deleted the
redundant sport-specific pages.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Consolidate server postgres connections into one shared pool
Four separate postgres() clients were open simultaneously (app, timer,
snapshots, socket), each defaulting to 10 connections, exhausting the
database's max_connections limit. Replaced with a single shared lazy-
initialized client in server/db.ts using a Proxy to defer the
DATABASE_URL check until first use (preserving test compatibility).
Also bumps the CS2 Champions Stage stochastic test from 200 → 1000
iterations to eliminate flakiness.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix and() bug and add Swiss loop safety guard
- cs2-major-stage.ts: markCs2StageEliminations and setCs2FinalPlacements
were using JS && instead of Drizzle and(), causing WHERE to filter only
by participantId (not scoringEventId), which would update rows across
all events instead of just the target event
- cs-major-simulator.ts: add break guard in simulateSwiss while loop to
prevent infinite loop if pairGroups returns no pairs
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix all remaining code review issues
- cs2-major-stage.ts: use schema column reference for stageEliminated
in markCs2StageEliminations instead of raw SQL string
- cs-major-simulator.ts: simulateOneMajor now locks in known stage
results when a stage is complete (8 recorded eliminations), only
simulating the remaining stages during live events
- admin event page: add CS2 Stage Setup button for cs2_major_qualifying_points
simulator types; expose simulatorType in server loader type cast
- cs2-setup.tsx: replace document.getElementById DOM manipulation with
React state (eliminatedChecked map) for checkbox show/hide logic;
remove unused stageMap and unassignedParticipants variables
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix oxlint errors: non-null assertions, sort→toSorted, unused vars
- cs-major-simulator.ts: replace 5 non-null assertions (!) with safe
optional chaining / if-guards; replace 6 .sort() with .toSorted()
- cs2-major-stage.ts: remove unused `inArray` import
- cs2-setup.tsx: remove unused `assignedIds` variable
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
* Fix flaky Champions Stage stochastic test
The makeTeams(8) helper creates only a 70-pt Elo spread (1800→1730).
With the Champions Stage bracket math this gives team-0 a ~19.6% win
rate — right at the 0.2 threshold, causing the test to fail ~63% of
the time in CI despite 1000 iterations.
Use 100-pt steps (1800→1100) instead, giving team-0 a ~40% win rate
and raising the assertion threshold to 0.25 for a clear safety margin.
https://claude.ai/code/session_019w21Nkf5TvTZHH6oVHaQXR
---------
Co-authored-by: Claude <noreply@anthropic.com>
2026-04-05 13:40:05 -07:00
|
|
|
|
}));
|
|
|
|
|
|
let wins = 0;
|
|
|
|
|
|
for (let i = 0; i < 1000; i++) {
|
|
|
|
|
|
const result = simulateChampionsStage(teams);
|
|
|
|
|
|
if (result.placements.get("team-0") === 1) wins++;
|
|
|
|
|
|
}
|
|
|
|
|
|
// Should win well above 12.5% (1/8 random baseline)
|
|
|
|
|
|
expect(wins / 1000).toBeGreaterThan(0.25);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-04-07 17:28:24 -04:00
|
|
|
|
|
|
|
|
|
|
// ─── calcStage3ExitQP ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
// Placements 9–16 with descending QP: 9→80, 10→70, ..., 16→10
|
|
|
|
|
|
function makeStage3QPConfig(): Map<number, number> {
|
|
|
|
|
|
const config = new Map<number, number>();
|
|
|
|
|
|
for (let i = 9; i <= 16; i++) {
|
|
|
|
|
|
config.set(i, (17 - i) * 10);
|
|
|
|
|
|
}
|
|
|
|
|
|
return config;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
describe("calcStage3ExitQP", () => {
|
|
|
|
|
|
it("returns empty map for empty input", () => {
|
|
|
|
|
|
const result = calcStage3ExitQP([], new Map());
|
|
|
|
|
|
expect(result.size).toBe(0);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("tie-splits QP within each wins group", () => {
|
|
|
|
|
|
// 2 teams at 2 wins, 2 teams at 1 win, 4 teams at 0 wins
|
|
|
|
|
|
const elim = [
|
|
|
|
|
|
{ id: "a", wins: 2 }, { id: "b", wins: 2 }, // slots 9-10: avg (80+70)/2 = 75
|
|
|
|
|
|
{ id: "c", wins: 1 }, { id: "d", wins: 1 }, // slots 11-12: avg (60+50)/2 = 55
|
|
|
|
|
|
{ id: "e", wins: 0 }, { id: "f", wins: 0 },
|
|
|
|
|
|
{ id: "g", wins: 0 }, { id: "h", wins: 0 }, // slots 13-16: avg (40+30+20+10)/4 = 25
|
|
|
|
|
|
];
|
|
|
|
|
|
const config = makeStage3QPConfig();
|
|
|
|
|
|
const result = calcStage3ExitQP(elim, config);
|
|
|
|
|
|
|
|
|
|
|
|
expect(result.get("a")).toBeCloseTo(75);
|
|
|
|
|
|
expect(result.get("b")).toBeCloseTo(75);
|
|
|
|
|
|
expect(result.get("c")).toBeCloseTo(55);
|
|
|
|
|
|
expect(result.get("d")).toBeCloseTo(55);
|
|
|
|
|
|
expect(result.get("e")).toBeCloseTo(25);
|
|
|
|
|
|
expect(result.get("h")).toBeCloseTo(25);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("all teams with the same wins get identical averaged QP", () => {
|
|
|
|
|
|
const elim = Array.from({ length: 8 }, (_, i) => ({ id: `t-${i}`, wins: 1 }));
|
|
|
|
|
|
const config = makeStage3QPConfig();
|
|
|
|
|
|
const result = calcStage3ExitQP(elim, config);
|
|
|
|
|
|
|
|
|
|
|
|
// Average of slots 9–16: (80+70+60+50+40+30+20+10) / 8 = 45
|
|
|
|
|
|
const expected = (80 + 70 + 60 + 50 + 40 + 30 + 20 + 10) / 8;
|
|
|
|
|
|
for (const [, qp] of result) {
|
|
|
|
|
|
expect(qp).toBeCloseTo(expected);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("higher wins groups get higher QP", () => {
|
|
|
|
|
|
const elim = [
|
|
|
|
|
|
{ id: "high", wins: 2 },
|
|
|
|
|
|
{ id: "mid", wins: 1 },
|
|
|
|
|
|
{ id: "low", wins: 0 },
|
|
|
|
|
|
];
|
|
|
|
|
|
// Pad to 8 to use real slots: add 5 more at wins=0
|
|
|
|
|
|
for (let i = 0; i < 5; i++) elim.push({ id: `pad-${i}`, wins: 0 });
|
|
|
|
|
|
const config = makeStage3QPConfig();
|
|
|
|
|
|
const result = calcStage3ExitQP(elim, config);
|
|
|
|
|
|
|
|
|
|
|
|
const highQP = result.get("high") ?? 0;
|
|
|
|
|
|
const midQP = result.get("mid") ?? 0;
|
|
|
|
|
|
const lowQP = result.get("low") ?? 0;
|
|
|
|
|
|
expect(highQP).toBeGreaterThan(midQP);
|
|
|
|
|
|
expect(midQP).toBeGreaterThan(lowQP);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ─── simulateOneMajor ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
describe("simulateOneMajor", () => {
|
|
|
|
|
|
it("returns a QP entry for all pool teams (no stage data)", () => {
|
|
|
|
|
|
const pool = makeTeams(32);
|
|
|
|
|
|
const result = simulateOneMajor(pool, undefined, makeQPConfig());
|
|
|
|
|
|
expect(result.size).toBe(32);
|
|
|
|
|
|
for (const [, qp] of result) {
|
|
|
|
|
|
expect(qp).toBeGreaterThanOrEqual(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("at least one team earns positive QP", () => {
|
|
|
|
|
|
const pool = makeTeams(32);
|
|
|
|
|
|
const result = simulateOneMajor(pool, undefined, makeQPConfig());
|
|
|
|
|
|
const nonZero = [...result.values()].filter((q) => q > 0);
|
|
|
|
|
|
expect(nonZero.length).toBeGreaterThan(0);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("exactly 16 teams earn 0 QP (stage 1 and 2 exits)", () => {
|
|
|
|
|
|
// Run multiple times to get a stable count — should always be 16 with 32-team pool
|
|
|
|
|
|
const pool = makeTeams(32);
|
|
|
|
|
|
const qpConfig = makeQPConfig();
|
|
|
|
|
|
for (let run = 0; run < 5; run++) {
|
|
|
|
|
|
const result = simulateOneMajor(pool, undefined, qpConfig);
|
|
|
|
|
|
const zeroCount = [...result.values()].filter((q) => q === 0).length;
|
|
|
|
|
|
expect(zeroCount).toBe(16);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("stage 1 eliminated teams earn 0 QP when stage is complete", () => {
|
|
|
|
|
|
const pool = makeTeams(32);
|
|
|
|
|
|
const qpConfig = makeQPConfig();
|
|
|
|
|
|
|
|
|
|
|
|
// Build stage results: 16 teams in stage 1, 8 in stage 2, 8 in stage 3
|
|
|
|
|
|
// Stage 1 eliminations: first 8 pool teams
|
|
|
|
|
|
const stageResults = new Map<string, {
|
|
|
|
|
|
stageEntry: number;
|
|
|
|
|
|
stageEliminated: number | null;
|
|
|
|
|
|
stageEliminatedWins: number | null;
|
|
|
|
|
|
}>();
|
|
|
|
|
|
for (let i = 0; i < 16; i++) {
|
|
|
|
|
|
stageResults.set(`team-${i}`, {
|
|
|
|
|
|
stageEntry: 1,
|
|
|
|
|
|
stageEliminated: i < 8 ? 1 : null, // first 8 eliminated at stage 1
|
|
|
|
|
|
stageEliminatedWins: i < 8 ? (i % 3) : null,
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
for (let i = 16; i < 24; i++) {
|
|
|
|
|
|
stageResults.set(`team-${i}`, { stageEntry: 2, stageEliminated: null, stageEliminatedWins: null });
|
|
|
|
|
|
}
|
|
|
|
|
|
for (let i = 24; i < 32; i++) {
|
|
|
|
|
|
stageResults.set(`team-${i}`, { stageEntry: 3, stageEliminated: null, stageEliminatedWins: null });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const result = simulateOneMajor(pool, stageResults, qpConfig);
|
|
|
|
|
|
|
|
|
|
|
|
// Stage 1 eliminated teams (team-0 through team-7) must get 0 QP
|
|
|
|
|
|
for (let i = 0; i < 8; i++) {
|
|
|
|
|
|
expect(result.get(`team-${i}`)).toBe(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("works with a pool exactly equal to FIELD_SIZE (32 teams)", () => {
|
|
|
|
|
|
// sampleField returns all teams when pool.length <= fieldSize
|
|
|
|
|
|
const pool = makeTeams(32);
|
|
|
|
|
|
expect(() => simulateOneMajor(pool, undefined, makeQPConfig())).not.toThrow();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
it("QF and SF losers receive averaged QP (not individual slot QP)", () => {
|
|
|
|
|
|
// With a uniform qpConfig we can verify tie-splitting: if slots 5-8 all have
|
|
|
|
|
|
// distinct values, QF losers should all get the average, not random individual values.
|
|
|
|
|
|
const qpConfig = new Map([
|
|
|
|
|
|
[1, 1000], [2, 800],
|
|
|
|
|
|
[3, 600], [4, 400],
|
|
|
|
|
|
[5, 300], [6, 200], [7, 100], [8, 50],
|
|
|
|
|
|
...Array.from({ length: 8 }, (_, i): [number, number] => [i + 9, 0]),
|
|
|
|
|
|
]);
|
|
|
|
|
|
const expectedQFAvg = (300 + 200 + 100 + 50) / 4; // 162.5
|
|
|
|
|
|
const expectedSFAvg = (600 + 400) / 2; // 500
|
|
|
|
|
|
|
|
|
|
|
|
const pool = makeTeams(32);
|
|
|
|
|
|
|
|
|
|
|
|
// Run a few times; every QF and SF loser should get the averaged values
|
|
|
|
|
|
for (let run = 0; run < 5; run++) {
|
|
|
|
|
|
const result = simulateOneMajor(pool, undefined, qpConfig);
|
|
|
|
|
|
const qpValues = [...result.values()];
|
|
|
|
|
|
// The averaged values should appear in the results
|
|
|
|
|
|
const hasQFAvg = qpValues.some((q) => Math.abs(q - expectedQFAvg) < 0.01);
|
|
|
|
|
|
const hasSFAvg = qpValues.some((q) => Math.abs(q - expectedSFAvg) < 0.01);
|
|
|
|
|
|
expect(hasQFAvg).toBe(true);
|
|
|
|
|
|
expect(hasSFAvg).toBe(true);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|