Skip to content

Commit

Permalink
CBI (#37)
Browse files Browse the repository at this point in the history
* CBI

* Tailwind

* fix tests
  • Loading branch information
enkoder authored Feb 2, 2025
1 parent d629896 commit 67f604e
Show file tree
Hide file tree
Showing 6 changed files with 447 additions and 3 deletions.
1 change: 1 addition & 0 deletions api/migrations/0011_multi_swiss.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE tournaments ADD COLUMN multi_swiss INTEGER NOT NULL DEFAULT 0;
21 changes: 18 additions & 3 deletions api/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
getTournamentsByType,
getTournamentsByUserId,
} from "./lib/abr.js";
import { CBI_2024, handleMultiPodTournament } from "./lib/cbi.js";
import * as NRDB from "./lib/nrdb.js";
import { calculatePointDistribution, getSeasonConfig } from "./lib/ranking.js";
import { trace } from "./lib/tracer.js";
Expand All @@ -34,6 +35,8 @@ import type {
const DISALLOW_TOURNAMENT_ID = [
// circuit opener selected as circuit breaker
3694,
// cbi Pods 2024
4678, 4676, 4677,
];

enum Queues {
Expand Down Expand Up @@ -353,9 +356,21 @@ async function handleTournamentIngest(
return;
}

const entries = await getEntries(abrTournament.id);
const cutTo = entries.filter((e) => e.rank_top !== null).length;
const tournamentBlob = abrToTournament(abrTournament, seasonId, cutTo);
let entries: ABREntryType[] = [];
let tournamentBlob: Tournament;
let cutTo: number;

// If we found the top cut for a multi-pod tournament - artisially hand craft the data
if (abrTournament.id === CBI_2024.top_cut.abr_tournament_id) {
({ entries, cutTo, tournamentBlob } = await handleMultiPodTournament(
seasonId,
abrTournament,
));
} else {
entries = await getEntries(abrTournament.id);
cutTo = entries.filter((e) => e.rank_top !== null).length;
tournamentBlob = abrToTournament(abrTournament, seasonId, cutTo);
}

const fingerprint = objectHash({
tournament: tournamentBlob,
Expand Down
2 changes: 2 additions & 0 deletions api/src/lib/abr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export const ABRTournament = z.object({
matchdata: z.boolean(),
winner_runner_identity: z.string().optional(),
winner_corp_identity: z.string().optional(),
multi_swiss: z.number().optional(),
});

export function abrToTournament(
Expand All @@ -80,6 +81,7 @@ export function abrToTournament(
date: abr.date.toString(),
fingerprint: "",
cutTo: cutTo,
multi_swiss: 0,
};
}

Expand Down
242 changes: 242 additions & 0 deletions api/src/lib/cbi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
import { g } from "../g.js";
import { Tournaments } from "../models/tournament.js";
import type { Tournament } from "../schema.js";
import {
type ABREntryType,
type ABRTournamentType,
abrToTournament,
getEntries,
} from "./abr.js";
import {} from "./nsg.js";
import {
type PlayerStanding,
type TournamentStandings,
getTournamentStandings,
} from "./nsg.js";

type PodType = "americas" | "apac" | "emea";

export interface PodConfig {
abr_tournament_id: number;
nsg_tournament_id: number;
rounds_played: number;
pod: PodType;
}

export interface CBIConfig {
name: string;
pods: Record<PodType, PodConfig>;
top_cut: {
abr_tournament_id: number;
};
}

export const CBI_2024 = {
name: "Circuit Breaker Invitational 2024",
pods: {
emea: {
abr_tournament_id: 4678,
nsg_tournament_id: 3736,
rounds_played: 8,
},
apac: {
// https://alwaysberunning.net/tournaments/4676
abr_tournament_id: 4676,
nsg_tournament_id: 3733,
rounds_played: 7,
},
// https://alwaysberunning.net/tournaments/4677
americas: {
abr_tournament_id: 4677,
nsg_tournament_id: 3734,
rounds_played: 8,
},
},
// https://alwaysberunning.net/tournaments/4689
top_cut: {
abr_tournament_id: 4689,
},
} as CBIConfig;

interface PodResult {
points: number;
sos: number;
extended_sos: number;
// Use either user_id or user_name to match with ABR entry
user_id?: number;
user_name?: string;
}

// Ordered list of pod results, matching the order of players in each pod

interface CBIEntry extends ABREntryType {
points?: number;
sos?: number;
extended_sos?: number;
rounds_played?: number;
}

const STANDINGS: Record<number, PlayerStanding> = {
31796: {
player: {
id: 41759,
name_with_pronouns: "The King (he/him)",
},
points: 20,
sos: "1.76339285714285625",
extended_sos: "1.7596540178571425",
},
47610: {
player: {
id: 41729,
name_with_pronouns: "Eden (it)",
},
points: 15,
sos: "1.375",
extended_sos: "1.627253401360544285714285714285714286",
},
};

/**
* This is a mega hack, but it works even though this will likely never happen again.
* We'll always remember the good times in 2024/5
*
* We need to:
* - Get all pod standings
* - Combine standings across all pods
* - Sort players by their best normalized score and top cut
* - Create a new tournament if it doesn't exist
* - Create results for each entry using new placement swiss & top cut
*/
export async function handleMultiPodTournament(
seasonId: number,
abrTournament: ABRTournamentType,
): Promise<{
entries: ABREntryType[];
cutTo: number;
tournamentBlob: Tournament;
}> {
// Get top cut entries
const topCutEntries = await getEntries(CBI_2024.top_cut.abr_tournament_id);
const cutTo = topCutEntries.filter((e) => e.rank_top !== null).length;

// Get all pod entries unsorted
const podEntries = {
emea: await getEntries(CBI_2024.pods.emea.abr_tournament_id),
apac: await getEntries(CBI_2024.pods.apac.abr_tournament_id),
americas: await getEntries(CBI_2024.pods.americas.abr_tournament_id),
};

const cbiEntries: CBIEntry[] = [];

// fetch the pod standings from tournaments.nullsignal.games
const podStandings: {
emea: TournamentStandings;
apac: TournamentStandings;
americas: TournamentStandings;
} = {
emea: await getTournamentStandings(CBI_2024.pods.emea.nsg_tournament_id),
apac: await getTournamentStandings(CBI_2024.pods.apac.nsg_tournament_id),
americas: await getTournamentStandings(
CBI_2024.pods.americas.nsg_tournament_id,
),
};

// Join up the pod results with the abr entries
for (const [pod, abrEntries] of Object.entries(podEntries)) {
for (const abrEntry of abrEntries) {
// Find this player's cobra results
const playerStanding =
podStandings[pod].stages[0].standings.find(
(s: PlayerStanding) =>
s.player?.id === abrEntry.user_id ||
s.player?.name_with_pronouns
.toLowerCase()
.includes(
abrEntry.user_name?.toLowerCase() ||
abrEntry.user_import_name?.toLowerCase(),
),
// Need to hard code some of these as they used different accounts for their swiss and top cut rounds
) || STANDINGS[abrEntry.user_id];
if (!playerStanding) {
if (!playerStanding) {
console.log(
abrEntry.user_name || abrEntry.user_import_name,
abrEntry.user_id,
);
throw new Error(
`Could not find player standing for ${abrEntry.user_name} (${abrEntry.user_id})`,
);
}
}

cbiEntries.push({
...abrEntry,
points: playerStanding.points,
sos: Number(playerStanding.sos),
extended_sos: Number(playerStanding.extended_sos),
rounds_played: CBI_2024.pods[pod].rounds_played,
});
}
}

// Sort players by their best normalized score
const sortedCbiEntries: CBIEntry[] = [
...cbiEntries.sort((a, b) => {
const aNormalized = a.points / (a.rounds_played * 3);
const bNormalized = b.points / (b.rounds_played * 3);
if (aNormalized === bNormalized) {
// sort by sos then esos
if (a.sos === b.sos) {
return b.extended_sos - a.extended_sos;
}
return b.sos - a.sos;
}
return bNormalized - aNormalized;
}),
];

// Now finally make sure that the swiss and top cut placements are correct
const finalEntries: CBIEntry[] = [];
for (let i = 0; i < sortedCbiEntries.length; i++) {
const entry = sortedCbiEntries[i];

// thanks for this NotAgain
if (entry.user_import_name === "dranked") {
entry.user_import_name = "NotAgain";
}

const topCutEntry = topCutEntries.find(
(p) =>
(p.user_id && p.user_id === entry.user_id) ||
(p.user_name && p.user_name === entry.user_name) ||
(p.user_import_name && p.user_import_name === entry.user_import_name),
);

finalEntries.push({
...entry,
rank_swiss: i + 1,
rank_top: topCutEntry ? topCutEntry.rank_top : null,
});
}

// Create the tournament blob
const baseAbrTournament = {
...abrTournament,
title: CBI_2024.name,
players_count: finalEntries.length,
};

const finalTournament = abrToTournament(baseAbrTournament, seasonId, cutTo);

return {
entries: finalEntries,
cutTo,
tournamentBlob: {
...finalTournament,
players_count: finalEntries.length,
name: CBI_2024.name,
multi_swiss: 1,
},
};
}
Loading

0 comments on commit 67f604e

Please sign in to comment.