mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-11 05:36:10 -05:00
Refactor
This commit is contained in:
@@ -103,9 +103,7 @@ export async function findByTournamentId(
|
||||
"TournamentMatch.number",
|
||||
"TournamentMatch.startedAt",
|
||||
"TournamentMatch.winnerSide",
|
||||
// totalKos is re-aggregated fresh from the game results; the
|
||||
// totalKos/totalPoints that old write paths persisted into the
|
||||
// opponent JSON is stale residue and gets stripped/overwritten
|
||||
// totalKos is never persisted, it is aggregated fresh from the game results
|
||||
serializedOpponentWithKos("opponentOne").as("opponent1"),
|
||||
serializedOpponentWithKos("opponentTwo").as("opponent2"),
|
||||
])
|
||||
@@ -121,15 +119,15 @@ export async function findByTournamentId(
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the opponent JSON with the freshly aggregated KO count: strips the
|
||||
* legacy `totalPoints` and overwrites `totalKos` with the SQL sum over the
|
||||
* match's game results. Resolves to `null` for BYEs (the column is `null`).
|
||||
* Builds the opponent JSON with the freshly aggregated KO count: sets
|
||||
* `totalKos` to the SQL sum over the match's game results. Resolves to `null`
|
||||
* for BYEs (the column is `null`).
|
||||
*/
|
||||
function serializedOpponentWithKos(
|
||||
column: "opponentOne" | "opponentTwo",
|
||||
): RawBuilder<ParticipantResult | null> {
|
||||
return kyselySql<ParticipantResult | null>`json_set(
|
||||
json_remove(${kyselySql.ref(`TournamentMatch.${column}`)}, '$.totalPoints'),
|
||||
${kyselySql.ref(`TournamentMatch.${column}`)},
|
||||
'$.totalKos',
|
||||
sum(
|
||||
case
|
||||
@@ -376,9 +374,6 @@ export function resetBracket(tournamentStageId: number) {
|
||||
function serializeOpponent(opponent: ParticipantResult | null): string | null {
|
||||
if (!opponent) return null;
|
||||
|
||||
const { totalKos, totalPoints, ...persisted } =
|
||||
opponent as ParticipantResult & {
|
||||
totalPoints?: number;
|
||||
};
|
||||
const { totalKos, ...persisted } = opponent;
|
||||
return JSON.stringify(persisted);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
tournamentTeamPage,
|
||||
} from "../../../../utils/urls";
|
||||
import { useUser } from "../../../auth/core/user";
|
||||
import { TOURNAMENT } from "../../../tournament/tournament-constants";
|
||||
import type { Bracket, Standing } from "../../core/Bracket";
|
||||
import * as Swiss from "../../core/engine/swiss/team-status";
|
||||
import * as Progression from "../../core/Progression";
|
||||
@@ -87,8 +86,7 @@ export function PlacementsTable({
|
||||
advanceThreshold: bracket.settings.advanceThreshold,
|
||||
losses: stats.setLosses,
|
||||
wins: stats.setWins,
|
||||
roundCount:
|
||||
bracket.settings.roundCount ?? TOURNAMENT.SWISS_DEFAULT_ROUND_COUNT,
|
||||
roundCount: bracket.swissRoundCount,
|
||||
}) === "advanced"
|
||||
? bracket.tournament.brackets.find((otherBracket) =>
|
||||
otherBracket.sources?.some(
|
||||
@@ -288,9 +286,7 @@ function StandingsTable({
|
||||
advanceThreshold: bracket.settings.advanceThreshold,
|
||||
losses: s.stats.setLosses,
|
||||
wins: s.stats.setWins,
|
||||
roundCount:
|
||||
bracket.settings.roundCount ??
|
||||
TOURNAMENT.SWISS_DEFAULT_ROUND_COUNT,
|
||||
roundCount: bracket.swissRoundCount,
|
||||
}) === "eliminated";
|
||||
|
||||
if (renderQualifiedRow) qualifiedRowRendered = true;
|
||||
|
||||
@@ -126,7 +126,7 @@ export abstract class Bracket {
|
||||
return;
|
||||
|
||||
try {
|
||||
let data = this.data as Engine.BracketData;
|
||||
let data = this.data;
|
||||
|
||||
const teamOrder = this.teamOrderForSimulation();
|
||||
|
||||
@@ -141,7 +141,6 @@ export abstract class Bracket {
|
||||
loopCount++;
|
||||
|
||||
for (const match of data.match) {
|
||||
if (!match) continue;
|
||||
// we have a result already
|
||||
if (match.winnerSide) {
|
||||
continue;
|
||||
@@ -225,9 +224,7 @@ export abstract class Bracket {
|
||||
simulatedMatch(matchId: number) {
|
||||
if (!this.simulatedData) return;
|
||||
|
||||
return this.simulatedData.match
|
||||
.filter(Boolean)
|
||||
.find((match) => match.id === matchId);
|
||||
return this.simulatedData.match.find((match) => match.id === matchId);
|
||||
}
|
||||
|
||||
/** Whether reporting a game in this bracket also records if the game was a KO win. */
|
||||
@@ -235,12 +232,17 @@ export abstract class Bracket {
|
||||
return false;
|
||||
}
|
||||
|
||||
get type(): Tables["TournamentStage"]["type"] {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
abstract get type(): Tables["TournamentStage"]["type"];
|
||||
|
||||
get standings(): Standing[] {
|
||||
throw new Error("not implemented");
|
||||
abstract get standings(): Standing[];
|
||||
|
||||
/**
|
||||
* How many rounds a swiss bracket has. Comes from the bracket's own stage
|
||||
* settings rather than `settings` (the progression's, editable at any time),
|
||||
* so it can't drift from the bracket that actually exists.
|
||||
*/
|
||||
get swissRoundCount() {
|
||||
return Engine.swissRoundCount(this.data);
|
||||
}
|
||||
|
||||
get participantTournamentTeamIds() {
|
||||
@@ -397,16 +399,14 @@ export abstract class Bracket {
|
||||
return this.teamsPendingCheckIn.includes(team.id);
|
||||
}
|
||||
|
||||
source(_options: {
|
||||
abstract source(options: {
|
||||
placements: number[];
|
||||
advanceThreshold?: number;
|
||||
rest?: boolean;
|
||||
}): {
|
||||
relevantMatchesFinished: boolean;
|
||||
teams: number[];
|
||||
} {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
};
|
||||
|
||||
teamsWithNames(teams: { id: number }[]) {
|
||||
return teams.map((team) => {
|
||||
@@ -474,7 +474,5 @@ export abstract class Bracket {
|
||||
return ongoingMatchIds;
|
||||
}
|
||||
|
||||
defaultRoundBestOfs(_data: BracketData): BracketMapCounts {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
abstract defaultRoundBestOfs(data: BracketData): BracketMapCounts;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,11 @@ export class SingleEliminationBracket extends Bracket {
|
||||
return "single_elimination";
|
||||
}
|
||||
|
||||
/** Unreachable: bracket progression validation rejects single elimination as a source. */
|
||||
source(): never {
|
||||
throw new Error("Single elimination bracket can't be a source");
|
||||
}
|
||||
|
||||
defaultRoundBestOfs(data: BracketData) {
|
||||
const result: BracketMapCounts = new Map();
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import * as R from "remeda";
|
||||
import type { Tables } from "~/db/tables";
|
||||
import * as Standings from "~/features/tournament/core/Standings";
|
||||
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
|
||||
import type { BracketData } from "~/features/tournament-bracket/core/engine/types";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { logger } from "~/utils/logger";
|
||||
@@ -59,9 +58,7 @@ export class SwissBracket extends Bracket {
|
||||
advanceThreshold,
|
||||
wins: standing.stats?.setWins ?? 0,
|
||||
losses: standing.stats?.setLosses ?? 0,
|
||||
roundCount:
|
||||
this.settings?.roundCount ??
|
||||
TOURNAMENT.SWISS_DEFAULT_ROUND_COUNT,
|
||||
roundCount: this.swissRoundCount,
|
||||
}),
|
||||
}))
|
||||
.filter((t) => t.status === "advanced")
|
||||
|
||||
@@ -1088,8 +1088,7 @@ export class Tournament {
|
||||
match.opponent1?.id === team.id || match.opponent2?.id === team.id,
|
||||
).length;
|
||||
const notAllRoundsGenerated =
|
||||
bracket.settings?.roundCount &&
|
||||
setsGeneratedCount !== bracket.settings.roundCount;
|
||||
setsGeneratedCount !== bracket.swissRoundCount;
|
||||
|
||||
if (isParticipant && notAllRoundsGenerated) {
|
||||
return { type: "WAITING_FOR_ROUND" } as const;
|
||||
|
||||
@@ -329,13 +329,6 @@ export class StageCreator {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ordering method for the first round of the upper bracket of an elimination stage.
|
||||
*/
|
||||
getStandardBracketFirstRoundOrdering(): SeedOrdering {
|
||||
return "space_between";
|
||||
}
|
||||
|
||||
/**
|
||||
* The only major ordering for the lower bracket.
|
||||
*/
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Duel, ParticipantSlot } from "../types";
|
||||
import type { StageCreator } from "./builder";
|
||||
import * as helpers from "./helpers";
|
||||
import { ordering } from "./seeding";
|
||||
import { ordering, STANDARD_BRACKET_FIRST_ROUND_ORDERING } from "./seeding";
|
||||
|
||||
/**
|
||||
* Creates a double elimination stage.
|
||||
@@ -12,8 +12,7 @@ import { ordering } from "./seeding";
|
||||
export function createDoubleElimination(creator: StageCreator): void {
|
||||
const slots = creator.getSlots();
|
||||
const stage = creator.createStage();
|
||||
const method = creator.getStandardBracketFirstRoundOrdering();
|
||||
const ordered = ordering[method](slots);
|
||||
const ordered = ordering[STANDARD_BRACKET_FIRST_ROUND_ORDERING](slots);
|
||||
|
||||
const { losers: losersWb, winner: winnerWb } = creator.createStandardBracket(
|
||||
stage.id,
|
||||
|
||||
@@ -65,6 +65,10 @@ export const ordering: OrderingMap = {
|
||||
},
|
||||
};
|
||||
|
||||
/** The ordering method for the first round of the upper bracket of an elimination stage. */
|
||||
export const STANDARD_BRACKET_FIRST_ROUND_ORDERING: SeedOrdering =
|
||||
"space_between";
|
||||
|
||||
export const defaultMinorOrdering: { [key: number]: SeedOrdering[] } = {
|
||||
// 1 or 2: Not possible.
|
||||
4: ["natural", "reverse"],
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { TournamentStageSettings } from "~/db/tables";
|
||||
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import type { CreateBracketInput, StageSettings, StageType } from "../types";
|
||||
import type {
|
||||
BracketData,
|
||||
CreateBracketInput,
|
||||
StageSettings,
|
||||
StageType,
|
||||
} from "../types";
|
||||
|
||||
/**
|
||||
* Resolves the user-selected settings into the engine's internal stage
|
||||
@@ -44,6 +49,18 @@ export function resolveStageSettings(input: CreateBracketInput): StageSettings {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How many rounds a swiss bracket was created with. Read off the stage's own
|
||||
* settings (resolved and persisted when the bracket was created) so that later
|
||||
* edits to the tournament's bracket progression can't change the advance and
|
||||
* elimination math of a bracket that already exists.
|
||||
*/
|
||||
export function swissRoundCount(data: BracketData): number {
|
||||
return (
|
||||
data.stage[0]?.settings.roundCount ?? TOURNAMENT.SWISS_DEFAULT_ROUND_COUNT
|
||||
);
|
||||
}
|
||||
|
||||
/** Whether the bracket will include a third place match. Only possible for single elimination with at least 4 participants. */
|
||||
export function hasThirdPlaceMatch(args: {
|
||||
type: StageType;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Duel, ParticipantSlot } from "../types";
|
||||
import type { StageCreator } from "./builder";
|
||||
import { ordering } from "./seeding";
|
||||
import { ordering, STANDARD_BRACKET_FIRST_ROUND_ORDERING } from "./seeding";
|
||||
|
||||
/**
|
||||
* Creates a single elimination stage.
|
||||
@@ -10,8 +10,7 @@ import { ordering } from "./seeding";
|
||||
export function createSingleElimination(creator: StageCreator): void {
|
||||
const slots = creator.getSlots();
|
||||
const stage = creator.createStage();
|
||||
const method = creator.getStandardBracketFirstRoundOrdering();
|
||||
const ordered = ordering[method](slots);
|
||||
const ordered = ordering[STANDARD_BRACKET_FIRST_ROUND_ORDERING](slots);
|
||||
|
||||
const { losers } = creator.createStandardBracket(stage.id, 1, ordered);
|
||||
createConsolationFinal(creator, stage.id, losers);
|
||||
|
||||
@@ -5,7 +5,11 @@
|
||||
*/
|
||||
|
||||
export { create } from "./create";
|
||||
export { hasThirdPlaceMatch, roundRobinGroupCount } from "./create/settings";
|
||||
export {
|
||||
hasThirdPlaceMatch,
|
||||
roundRobinGroupCount,
|
||||
swissRoundCount,
|
||||
} from "./create/settings";
|
||||
export { endDroppedTeamMatches } from "./propagation/dropped-teams";
|
||||
export { reportResult } from "./propagation/report-result";
|
||||
export { resetMatchResults } from "./propagation/reset-result";
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import blossom from "edmonds-blossom-fixed";
|
||||
import { err, ok, type Result } from "neverthrow";
|
||||
import * as R from "remeda";
|
||||
import { TOURNAMENT } from "~/features/tournament/tournament-constants";
|
||||
import invariant from "~/utils/invariant";
|
||||
import { swissRoundCount } from "../create/settings";
|
||||
import type {
|
||||
BracketData,
|
||||
GeneratedRound,
|
||||
@@ -23,7 +23,7 @@ export function generateRound(
|
||||
args: {
|
||||
groupId: number;
|
||||
standings: SwissStanding[];
|
||||
settings: { advanceThreshold?: number; roundCount?: number } | null;
|
||||
settings: { advanceThreshold?: number } | null;
|
||||
},
|
||||
): Result<GeneratedRound, string> {
|
||||
// lets consider only this groups matches
|
||||
@@ -52,8 +52,7 @@ export function generateRound(
|
||||
|
||||
// filter out teams that have advanced or been eliminated if early advance/elimination is enabled
|
||||
if (typeof args.settings?.advanceThreshold === "number") {
|
||||
const roundCount =
|
||||
args.settings.roundCount ?? TOURNAMENT.SWISS_DEFAULT_ROUND_COUNT;
|
||||
const roundCount = swissRoundCount(data);
|
||||
const advanceThreshold = args.settings.advanceThreshold;
|
||||
|
||||
standingsWithoutDropouts = standingsWithoutDropouts.filter((standing) => {
|
||||
|
||||
@@ -32,8 +32,8 @@ export function up(db) {
|
||||
"stageId",
|
||||
"groupId",
|
||||
"number",
|
||||
case when "opponentOne" = 'null' then null else json_remove("opponentOne", '$.result', '$.forfeit') end,
|
||||
case when "opponentTwo" = 'null' then null else json_remove("opponentTwo", '$.result', '$.forfeit') end,
|
||||
case when "opponentOne" = 'null' then null else json_remove("opponentOne", '$.result', '$.forfeit', '$.totalPoints', '$.totalKos') end,
|
||||
case when "opponentTwo" = 'null' then null else json_remove("opponentTwo", '$.result', '$.forfeit', '$.totalPoints', '$.totalKos') end,
|
||||
case
|
||||
when "opponentOne" ->> '$.result' = 'win' then 'opponent1'
|
||||
when "opponentTwo" ->> '$.result' = 'win' then 'opponent2'
|
||||
|
||||
Reference in New Issue
Block a user