Skip to content

Commit

Permalink
Add notice for failed events
Browse files Browse the repository at this point in the history
  • Loading branch information
claabs committed Jan 17, 2024
1 parent 752918a commit f23303d
Show file tree
Hide file tree
Showing 4 changed files with 79 additions and 23 deletions.
49 changes: 49 additions & 0 deletions RULES-FLAW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# The 2024 Copenhagen Major Swiss Flaw

## The Problem

With the introduction of the new [Initial Swiss Matchups rule](https://github.com/ValveSoftware/counter-strike/blob/main/major-supplemental-rulebook.md?plain=1#L324-L335) added for the 2024 Copenhagen Major cycle, an unintended consequence was created.

With all matchups 50/50, there's about 0.6% chance that 1-1 record teams in round 3 cannot find valid matchups. It can cause the round 3 matchups selection to force a rematch, which is not allowed by the [current ruleset](https://github.com/ValveSoftware/counter-strike/blob/main/major-supplemental-rulebook.md?plain=1#L303):

>In round 2 and 3, the highest seeded team faces the lowest seeded team available that does not result in a rematch within the stage.
There's no clarity in the ruleset how this matchup problem should be resolved, so those simulated event failures are excluded from the results.

## Example Matchups

Here's an example round 3 1-1 matchup pool, ordered by difficulty then initial seed:

| Team | Seed | Diff | Previous Opponents |
|---------------|------|------|-------------------------------------|
| Cloud9 | 3 | 2 | OG (1-1), Nexus (2-0) |
| SAW | 5 | 2 | Zero Tenacity (1-1), Permitta (2-0) |
| BetBoom | 6 | 2 | JANO (1-1), SINNERS (2-0) |
| fnatic | 16 | 2 | 9 Pandas (1-1), Natus Vincere (2-0) |
| 9 Pandas | 8 | -2 | fnatic (1-1), ECSTATIC (0-2) |
| OG | 11 | -2 | Cloud9 (1-1), 3DMAX (0-2) |
| Zero Tenacity | 13 | -2 | SAW (1-1), BIG (0-2) |
| JANO | 14 | -2 | BetBoom (1-1), Virtus.pro (0-2) |

The matchups are created from highest seed first, picking the lowest seed that doesn't result in a rematch:

- *Cloud9* is top seed, so they pair with the bottom seed *JANO* first
- *SAW* is next highest seed, but already played *Zero Tenacity*, so they pair with the next lowest seed, which is *OG*
- *BetBoom* is next highest seed, and pair with the lowest seed, which is *Zero Tenacity*
- ***fnatic* and *9 Pandas* remain, but they already played round 1. Here lies the issue.**

| Team A | Team B |
|------------|---------------|
| Cloud9 | JANO |
| SAW | OG |
| BetBoom | Zero Tenacity |
| **fnatic** | **9 Pandas** |

## Author's Opinion

I'm not sure why the new Initial Swiss Matchups were added. My guess is it's due to not disadvantage the open qualifier teams in initial seeding. However, it causes this issue in subsequent rounds when the seed matchup approach changes.

Valve should either:

1. Seed the teams initially to meet their initial matchup goals (invite team, closed team, invite team, closed team, etc.) and revert the Initial Swiss Matchups rule
1. Seed the teams according to ranking points and revert the Initial Swiss Matchups rule
6 changes: 6 additions & 0 deletions src/simulation-result-viewer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ export class SimulationResultViewer extends LitElement {
}}"
></vaadin-icon>
</vaadin-horizontal-layout>
<h4 class="sim-error-header">
${this.simulationResults.failedSimulations.toLocaleString()} simulations failed due to
<a href="https://github.com/claabs/cs-buchholz-simulator/blob/master/RULES-FLAW.md"
>rules issues</a
>
</h4>
<vaadin-form-layout .responsiveSteps=${this.responsiveSteps}>
<vaadin-accordion>
<h3 class="sim-header">${this.simulationResults.qualWins}-0 Teams</h3>
Expand Down
20 changes: 13 additions & 7 deletions src/simulator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ export interface SimulationResults {
qualified: TeamResults[];
allWins: TeamResults[];
allLosses: TeamResults[];
failedSimulations: number;
}

export interface SimulationEventMessage {
Expand All @@ -83,6 +84,7 @@ export interface SimulationEventMessage {

export interface MessageFromWorkerFinish {
data: Map<string, TeamResultCounts>;
errors: number;
type: 'finish';
}

Expand Down Expand Up @@ -122,7 +124,8 @@ export const generateEasyProbabilities = (
export const formatResultsCounts = (
categorizedResults: Map<string, TeamResultCounts>,
simSettings: SimulationSettings,
iterations: number
iterations: number,
failedSimulations: number
): SimulationResults => {
const { qualified, allWins, allLosses } = Array.from(categorizedResults.entries()).reduce(
(acc, [teamName, resultCounts]) => {
Expand Down Expand Up @@ -173,6 +176,7 @@ export const formatResultsCounts = (
allLosses: allLosses.sort((a, b) => b.rate - a.rate),
iterations,
...simSettings,
failedSimulations,
};
};

Expand All @@ -185,17 +189,17 @@ export const simulateEvents = async (
): Promise<SimulationResults> => {
const workerCount = window.navigator.hardwareConcurrency;
const iterationsPerWorker = Math.floor(iterations / workerCount);
const runningWorkers: Promise<Map<string, TeamResultCounts>>[] = [];
const runningWorkers: Promise<MessageFromWorkerFinish>[] = [];
let progressTotal = 0;
for (let i = 0; i < workerCount; i += 1) {
// eslint-disable-next-line @typescript-eslint/no-loop-func
const promise = new Promise<Map<string, TeamResultCounts>>((resolve, reject) => {
const promise = new Promise<MessageFromWorkerFinish>((resolve, reject) => {
const worker = new Worker(new URL('./worker/simulation-worker.js', import.meta.url), {
type: 'module',
});
worker.addEventListener('message', (evt: MessageEvent<MessageFromWorker>) => {
if (evt.data.type === 'finish') {
resolve(evt.data.data);
resolve(evt.data);
} else {
progressTotal += evt.data.data;
progress(progressTotal / iterations);
Expand All @@ -215,9 +219,11 @@ export const simulateEvents = async (
runningWorkers.push(promise);
}
const allTeamResults = new Map<string, TeamResultCounts>();
let failedSimulations = 0;
const workerResults = await Promise.all(runningWorkers);
workerResults.forEach((teamResults) => {
teamResults.forEach((teamCounts, teamName) => {
workerResults.forEach((workerResult) => {
failedSimulations += workerResult.errors;
workerResult.data.forEach((teamCounts, teamName) => {
const prevTotal = allTeamResults.get(teamName);
if (!prevTotal) {
allTeamResults.set(teamName, teamCounts);
Expand Down Expand Up @@ -250,5 +256,5 @@ export const simulateEvents = async (
});
});

return formatResultsCounts(allTeamResults, simSettings, iterations);
return formatResultsCounts(allTeamResults, simSettings, iterations, failedSimulations);
};
27 changes: 11 additions & 16 deletions src/worker/simulation-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,8 +226,9 @@ const matchRecordGroup = (recordGroup: TeamStandingWithDifficulty[]): Matchup[]
while (!validLowTeam) {
lowTeam = sortedGroup.pop();
if (!lowTeam) {
console.log(sortedGroupCopy);
throw new Error('Missing low seed team');
// eslint-disable-next-line no-console
console.warn('Simulation failed, failed group:', sortedGroupCopy);
throw new Error('No valid matchups for seeding found');
}
const lowTeamName = lowTeam.name;
if (!highTeam.pastOpponents.some((opp) => opp.teamName === lowTeamName)) {
Expand Down Expand Up @@ -342,18 +343,13 @@ export const simulateEvent = (
const eliminated: TeamStanding[] = [];
const archivedMatchups = [];
while (competitors.length) {
try {
const matchups = calculateMatchups(competitors);
archivedMatchups.push(matchups);
const standings = simulateMatchups(matchups, probabilities, simSettings);
const qualElimResult = extractQualElims(standings, simSettings);
competitors = qualElimResult.competitors;
qualified.push(...qualElimResult.qualified);
eliminated.push(...qualElimResult.eliminated);
} catch (err) {
console.log(archivedMatchups);
throw new Error('bad event');
}
const matchups = calculateMatchups(competitors);
archivedMatchups.push(matchups);
const standings = simulateMatchups(matchups, probabilities, simSettings);
const qualElimResult = extractQualElims(standings, simSettings);
competitors = qualElimResult.competitors;
qualified.push(...qualElimResult.qualified);
eliminated.push(...qualElimResult.eliminated);
}
return {
qualified,
Expand Down Expand Up @@ -389,10 +385,9 @@ const onMessage = (evt: MessageEvent<SimulationEventMessage>) => {
}
}

console.log('badEvents:', badEvents);

const finishMessage: MessageFromWorkerFinish = {
data: allTeamResults,
errors: badEvents,
type: 'finish',
};
self.postMessage(finishMessage);
Expand Down

0 comments on commit f23303d

Please sign in to comment.