Migrate Prettier/Eslint/Stylelint setup to Biome (#1772)

* Initial

* CSS lint

* Test CI

* Add 1v1, 2v2, and 3v3 Tags (#1771)

* Initial

* CSS lint

* Test CI

* Rename step

---------

Co-authored-by: xi <104683822+ximk@users.noreply.github.com>
This commit is contained in:
Kalle
2024-06-24 13:07:17 +03:00
committed by GitHub
parent 73e9fca492
commit fd48bced91
1070 changed files with 219485 additions and 224310 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -6,251 +6,251 @@ import type { DeepPartial, Side } from "../types";
import { BaseGetter } from "./getter";
export class BaseUpdater extends BaseGetter {
/**
* Updates the matches related (previous and next) to a match.
*
* @param match A match.
* @param updatePrevious Whether to update the previous matches.
* @param updateNext Whether to update the next matches.
*/
protected updateRelatedMatches(
match: Match,
updatePrevious: boolean,
updateNext: boolean,
): void {
const { roundNumber, roundCount } = this.getRoundPositionalInfo(
match.round_id,
);
/**
* Updates the matches related (previous and next) to a match.
*
* @param match A match.
* @param updatePrevious Whether to update the previous matches.
* @param updateNext Whether to update the next matches.
*/
protected updateRelatedMatches(
match: Match,
updatePrevious: boolean,
updateNext: boolean,
): void {
const { roundNumber, roundCount } = this.getRoundPositionalInfo(
match.round_id,
);
const stage = this.storage.select("stage", match.stage_id);
if (!stage) throw Error("Stage not found.");
const stage = this.storage.select("stage", match.stage_id);
if (!stage) throw Error("Stage not found.");
const group = this.storage.select("group", match.group_id);
if (!group) throw Error("Group not found.");
const group = this.storage.select("group", match.group_id);
if (!group) throw Error("Group not found.");
const matchLocation = helpers.getMatchLocation(stage.type, group.number);
const matchLocation = helpers.getMatchLocation(stage.type, group.number);
updatePrevious &&
this.updatePrevious(match, matchLocation, stage, roundNumber);
updateNext &&
this.updateNext(match, matchLocation, stage, roundNumber, roundCount);
}
updatePrevious &&
this.updatePrevious(match, matchLocation, stage, roundNumber);
updateNext &&
this.updateNext(match, matchLocation, stage, roundNumber, roundCount);
}
/**
* Updates a match based on a partial match.
*
* @param stored A reference to what will be updated in the storage.
* @param match Input of the update.
* @param force Whether to force update locked matches.
*/
protected updateMatch(
stored: Match,
match: DeepPartial<Match>,
force?: boolean,
): void {
if (!force && helpers.isMatchUpdateLocked(stored))
throw Error("The match is locked.");
/**
* Updates a match based on a partial match.
*
* @param stored A reference to what will be updated in the storage.
* @param match Input of the update.
* @param force Whether to force update locked matches.
*/
protected updateMatch(
stored: Match,
match: DeepPartial<Match>,
force?: boolean,
): void {
if (!force && helpers.isMatchUpdateLocked(stored))
throw Error("The match is locked.");
const stage = this.storage.select("stage", stored.stage_id);
if (!stage) throw Error("Stage not found.");
const stage = this.storage.select("stage", stored.stage_id);
if (!stage) throw Error("Stage not found.");
const inRoundRobin = helpers.isRoundRobin(stage);
const inRoundRobin = helpers.isRoundRobin(stage);
const { statusChanged, resultChanged } = helpers.setMatchResults(
stored,
match,
inRoundRobin,
);
this.applyMatchUpdate(stored);
const { statusChanged, resultChanged } = helpers.setMatchResults(
stored,
match,
inRoundRobin,
);
this.applyMatchUpdate(stored);
// Don't update related matches if it's a simple score update.
if (!statusChanged && !resultChanged) return;
// Don't update related matches if it's a simple score update.
if (!statusChanged && !resultChanged) return;
if (!helpers.isRoundRobin(stage) && !helpers.isSwiss(stage))
this.updateRelatedMatches(stored, statusChanged, resultChanged);
}
if (!helpers.isRoundRobin(stage) && !helpers.isSwiss(stage))
this.updateRelatedMatches(stored, statusChanged, resultChanged);
}
/**
* Updates the opponents and status of a match and its child games.
*
* @param match A match.
*/
protected applyMatchUpdate(match: Match): void {
if (!this.storage.update("match", match.id, match))
throw Error("Could not update the match.");
}
/**
* Updates the opponents and status of a match and its child games.
*
* @param match A match.
*/
protected applyMatchUpdate(match: Match): void {
if (!this.storage.update("match", match.id, match))
throw Error("Could not update the match.");
}
/**
* Updates the match(es) leading to the current match based on this match results.
*
* @param match Input of the update.
* @param matchLocation Location of the current match.
* @param stage The parent stage.
* @param roundNumber Number of the round.
*/
protected updatePrevious(
match: Match,
matchLocation: GroupType,
stage: Stage,
roundNumber: number,
): void {
const previousMatches = this.getPreviousMatches(
match,
matchLocation,
stage,
roundNumber,
);
if (previousMatches.length === 0) return;
/**
* Updates the match(es) leading to the current match based on this match results.
*
* @param match Input of the update.
* @param matchLocation Location of the current match.
* @param stage The parent stage.
* @param roundNumber Number of the round.
*/
protected updatePrevious(
match: Match,
matchLocation: GroupType,
stage: Stage,
roundNumber: number,
): void {
const previousMatches = this.getPreviousMatches(
match,
matchLocation,
stage,
roundNumber,
);
if (previousMatches.length === 0) return;
if (match.status < Status.Running) {
this.resetMatchesStatus(previousMatches);
}
}
if (match.status < Status.Running) {
this.resetMatchesStatus(previousMatches);
}
}
/**
* Resets the status of a list of matches to what it should currently be.
*
* @param matches The matches to update.
*/
protected resetMatchesStatus(matches: Match[]): void {
for (const match of matches) {
match.status = helpers.getMatchStatus(match);
this.applyMatchUpdate(match);
}
}
/**
* Resets the status of a list of matches to what it should currently be.
*
* @param matches The matches to update.
*/
protected resetMatchesStatus(matches: Match[]): void {
for (const match of matches) {
match.status = helpers.getMatchStatus(match);
this.applyMatchUpdate(match);
}
}
/**
* Updates the match(es) following the current match based on this match results.
*
* @param match Input of the update.
* @param matchLocation Location of the current match.
* @param stage The parent stage.
* @param roundNumber Number of the round.
* @param roundCount Count of rounds.
*/
protected updateNext(
match: Match,
matchLocation: GroupType,
stage: Stage,
roundNumber: number,
roundCount: number,
): void {
const nextMatches = this.getNextMatches(
match,
matchLocation,
stage,
roundNumber,
roundCount,
);
if (nextMatches.length === 0) {
return;
}
/**
* Updates the match(es) following the current match based on this match results.
*
* @param match Input of the update.
* @param matchLocation Location of the current match.
* @param stage The parent stage.
* @param roundNumber Number of the round.
* @param roundCount Count of rounds.
*/
protected updateNext(
match: Match,
matchLocation: GroupType,
stage: Stage,
roundNumber: number,
roundCount: number,
): void {
const nextMatches = this.getNextMatches(
match,
matchLocation,
stage,
roundNumber,
roundCount,
);
if (nextMatches.length === 0) {
return;
}
const winnerSide = helpers.getMatchResult(match);
const actualRoundNumber =
stage.settings.skipFirstRound && matchLocation === "winner_bracket"
? roundNumber + 1
: roundNumber;
const winnerSide = helpers.getMatchResult(match);
const actualRoundNumber =
stage.settings.skipFirstRound && matchLocation === "winner_bracket"
? roundNumber + 1
: roundNumber;
if (winnerSide)
this.applyToNextMatches(
helpers.setNextOpponent,
match,
matchLocation,
actualRoundNumber,
roundCount,
nextMatches,
winnerSide,
);
else
this.applyToNextMatches(
helpers.resetNextOpponent,
match,
matchLocation,
actualRoundNumber,
roundCount,
nextMatches,
);
}
if (winnerSide)
this.applyToNextMatches(
helpers.setNextOpponent,
match,
matchLocation,
actualRoundNumber,
roundCount,
nextMatches,
winnerSide,
);
else
this.applyToNextMatches(
helpers.resetNextOpponent,
match,
matchLocation,
actualRoundNumber,
roundCount,
nextMatches,
);
}
/**
* Applies a SetNextOpponent function to matches following the current match.
*
* @param setNextOpponent The SetNextOpponent function.
* @param match The current match.
* @param matchLocation Location of the current match.
* @param roundNumber Number of the current round.
* @param roundCount Count of rounds.
* @param nextMatches The matches following the current match.
* @param winnerSide Side of the winner in the current match.
*/
protected applyToNextMatches(
setNextOpponent: SetNextOpponent,
match: Match,
matchLocation: GroupType,
roundNumber: number,
roundCount: number,
nextMatches: (Match | null)[],
winnerSide?: Side,
): void {
if (matchLocation === "final_group") {
if (!nextMatches[0]) throw Error("First next match is null.");
setNextOpponent(nextMatches[0], "opponent1", match, "opponent1");
setNextOpponent(nextMatches[0], "opponent2", match, "opponent2");
this.applyMatchUpdate(nextMatches[0]);
return;
}
/**
* Applies a SetNextOpponent function to matches following the current match.
*
* @param setNextOpponent The SetNextOpponent function.
* @param match The current match.
* @param matchLocation Location of the current match.
* @param roundNumber Number of the current round.
* @param roundCount Count of rounds.
* @param nextMatches The matches following the current match.
* @param winnerSide Side of the winner in the current match.
*/
protected applyToNextMatches(
setNextOpponent: SetNextOpponent,
match: Match,
matchLocation: GroupType,
roundNumber: number,
roundCount: number,
nextMatches: (Match | null)[],
winnerSide?: Side,
): void {
if (matchLocation === "final_group") {
if (!nextMatches[0]) throw Error("First next match is null.");
setNextOpponent(nextMatches[0], "opponent1", match, "opponent1");
setNextOpponent(nextMatches[0], "opponent2", match, "opponent2");
this.applyMatchUpdate(nextMatches[0]);
return;
}
const nextSide = helpers.getNextSide(
match.number,
roundNumber,
roundCount,
matchLocation,
);
const nextSide = helpers.getNextSide(
match.number,
roundNumber,
roundCount,
matchLocation,
);
if (nextMatches[0]) {
setNextOpponent(nextMatches[0], nextSide, match, winnerSide);
this.propagateByeWinners(nextMatches[0]);
}
if (nextMatches[0]) {
setNextOpponent(nextMatches[0], nextSide, match, winnerSide);
this.propagateByeWinners(nextMatches[0]);
}
if (nextMatches.length !== 2) return;
if (!nextMatches[1]) throw Error("Second next match is null.");
if (nextMatches.length !== 2) return;
if (!nextMatches[1]) throw Error("Second next match is null.");
// The second match is either the consolation final (single elimination) or a loser bracket match (double elimination).
// The second match is either the consolation final (single elimination) or a loser bracket match (double elimination).
if (matchLocation === "single_bracket") {
setNextOpponent(
nextMatches[1],
nextSide,
match,
winnerSide && helpers.getOtherSide(winnerSide),
);
this.applyMatchUpdate(nextMatches[1]);
} else {
const nextSideLB = helpers.getNextSideLoserBracket(
match.number,
nextMatches[1],
roundNumber,
);
setNextOpponent(
nextMatches[1],
nextSideLB,
match,
winnerSide && helpers.getOtherSide(winnerSide),
);
this.propagateByeWinners(nextMatches[1]);
}
}
if (matchLocation === "single_bracket") {
setNextOpponent(
nextMatches[1],
nextSide,
match,
winnerSide && helpers.getOtherSide(winnerSide),
);
this.applyMatchUpdate(nextMatches[1]);
} else {
const nextSideLB = helpers.getNextSideLoserBracket(
match.number,
nextMatches[1],
roundNumber,
);
setNextOpponent(
nextMatches[1],
nextSideLB,
match,
winnerSide && helpers.getOtherSide(winnerSide),
);
this.propagateByeWinners(nextMatches[1]);
}
}
/**
* Propagates winner against BYEs in related matches.
*
* @param match The current match.
*/
protected propagateByeWinners(match: Match): void {
helpers.setMatchResults(match, match, false); // BYE propagation is only in non round-robin stages.
this.applyMatchUpdate(match);
/**
* Propagates winner against BYEs in related matches.
*
* @param match The current match.
*/
protected propagateByeWinners(match: Match): void {
helpers.setMatchResults(match, match, false); // BYE propagation is only in non round-robin stages.
this.applyMatchUpdate(match);
if (helpers.hasBye(match)) this.updateRelatedMatches(match, true, true);
}
if (helpers.hasBye(match)) this.updateRelatedMatches(match, true, true);
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,56 +1,56 @@
import type { Storage } from "./types";
export class Delete {
private readonly storage: Storage;
private readonly storage: Storage;
/**
* Creates an instance of Delete, which will handle cleanly deleting data in the storage.
*
* @param storage The implementation of Storage.
*/
constructor(storage: Storage) {
this.storage = storage;
}
/**
* Creates an instance of Delete, which will handle cleanly deleting data in the storage.
*
* @param storage The implementation of Storage.
*/
constructor(storage: Storage) {
this.storage = storage;
}
/**
* Deletes a stage, and all its components:
*
* - Groups
* - Rounds
* - Matches
* - Match games
*
* @param stageId ID of the stage.
*/
public stage(stageId: number): void {
// The order is important here, because the abstract storage can possibly have foreign key checks (e.g. SQL).
/**
* Deletes a stage, and all its components:
*
* - Groups
* - Rounds
* - Matches
* - Match games
*
* @param stageId ID of the stage.
*/
public stage(stageId: number): void {
// The order is important here, because the abstract storage can possibly have foreign key checks (e.g. SQL).
if (!this.storage.delete("match", { stage_id: stageId }))
throw Error("Could not delete matches.");
if (!this.storage.delete("match", { stage_id: stageId }))
throw Error("Could not delete matches.");
if (!this.storage.delete("round", { stage_id: stageId }))
throw Error("Could not delete rounds.");
if (!this.storage.delete("round", { stage_id: stageId }))
throw Error("Could not delete rounds.");
if (!this.storage.delete("group", { stage_id: stageId }))
throw Error("Could not delete groups.");
if (!this.storage.delete("group", { stage_id: stageId }))
throw Error("Could not delete groups.");
if (!this.storage.delete("stage", { id: stageId }))
throw Error("Could not delete the stage.");
}
if (!this.storage.delete("stage", { id: stageId }))
throw Error("Could not delete the stage.");
}
/**
* Deletes **the stages** of a tournament (and all their components, see {@link stage | delete.stage()}).
*
* You are responsible for deleting the tournament itself.
*
* @param tournamentId ID of the tournament.
*/
public tournament(tournamentId: number): void {
const stages = this.storage.select("stage", {
tournament_id: tournamentId,
});
if (!stages) throw Error("Error getting the stages.");
/**
* Deletes **the stages** of a tournament (and all their components, see {@link stage | delete.stage()}).
*
* You are responsible for deleting the tournament itself.
*
* @param tournamentId ID of the tournament.
*/
public tournament(tournamentId: number): void {
const stages = this.storage.select("stage", {
tournament_id: tournamentId,
});
if (!stages) throw Error("Error getting the stages.");
for (const stage of stages) this.stage(stage.id);
}
for (const stage of stages) this.stage(stage.id);
}
}

View File

@@ -3,153 +3,153 @@ import { BaseGetter } from "./base/getter";
import * as helpers from "./helpers";
export class Find extends BaseGetter {
/**
* Gets the upper bracket (the only bracket if single elimination or the winner bracket in double elimination).
*
* @param stageId ID of the stage.
*/
public upperBracket(stageId: number): Group {
const stage = this.storage.select("stage", stageId);
if (!stage) throw Error("Stage not found.");
/**
* Gets the upper bracket (the only bracket if single elimination or the winner bracket in double elimination).
*
* @param stageId ID of the stage.
*/
public upperBracket(stageId: number): Group {
const stage = this.storage.select("stage", stageId);
if (!stage) throw Error("Stage not found.");
switch (stage.type) {
case "round_robin":
throw Error("Round-robin stages do not have an upper bracket.");
case "single_elimination":
case "double_elimination":
return this.getUpperBracket(stageId);
default:
throw Error("Unknown stage type.");
}
}
switch (stage.type) {
case "round_robin":
throw Error("Round-robin stages do not have an upper bracket.");
case "single_elimination":
case "double_elimination":
return this.getUpperBracket(stageId);
default:
throw Error("Unknown stage type.");
}
}
/**
* Gets the loser bracket.
*
* @param stageId ID of the stage.
*/
public loserBracket(stageId: number): Group {
const stage = this.storage.select("stage", stageId);
if (!stage) throw Error("Stage not found.");
/**
* Gets the loser bracket.
*
* @param stageId ID of the stage.
*/
public loserBracket(stageId: number): Group {
const stage = this.storage.select("stage", stageId);
if (!stage) throw Error("Stage not found.");
switch (stage.type) {
case "round_robin":
throw Error("Round-robin stages do not have a loser bracket.");
case "single_elimination":
throw Error("Single elimination stages do not have a loser bracket.");
case "double_elimination":
// eslint-disable-next-line no-case-declarations
const group = this.getLoserBracket(stageId);
if (!group) throw Error("Loser bracket not found.");
return group;
default:
throw Error("Unknown stage type.");
}
}
switch (stage.type) {
case "round_robin":
throw Error("Round-robin stages do not have a loser bracket.");
case "single_elimination":
throw Error("Single elimination stages do not have a loser bracket.");
case "double_elimination": {
const group = this.getLoserBracket(stageId);
if (!group) throw Error("Loser bracket not found.");
return group;
}
default:
throw Error("Unknown stage type.");
}
}
/**
* Returns the matches leading to the given match.
*
* If a `participantId` is given, the previous match _from their point of view_ is returned.
*
* @param matchId ID of the target match.
* @param participantId Optional ID of the participant.
*/
public previousMatches(matchId: number, participantId?: number): Match[] {
const match = this.storage.select("match", matchId);
if (!match) throw Error("Match not found.");
/**
* Returns the matches leading to the given match.
*
* If a `participantId` is given, the previous match _from their point of view_ is returned.
*
* @param matchId ID of the target match.
* @param participantId Optional ID of the participant.
*/
public previousMatches(matchId: number, participantId?: number): Match[] {
const match = this.storage.select("match", matchId);
if (!match) throw Error("Match not found.");
const stage = this.storage.select("stage", match.stage_id);
if (!stage) throw Error("Stage not found.");
const stage = this.storage.select("stage", match.stage_id);
if (!stage) throw Error("Stage not found.");
const group = this.storage.select("group", match.group_id);
if (!group) throw Error("Group not found.");
const group = this.storage.select("group", match.group_id);
if (!group) throw Error("Group not found.");
const round = this.storage.select("round", match.round_id);
if (!round) throw Error("Round not found.");
const round = this.storage.select("round", match.round_id);
if (!round) throw Error("Round not found.");
const matchLocation = helpers.getMatchLocation(stage.type, group.number);
const previousMatches = this.getPreviousMatches(
match,
matchLocation,
stage,
round.number,
);
const matchLocation = helpers.getMatchLocation(stage.type, group.number);
const previousMatches = this.getPreviousMatches(
match,
matchLocation,
stage,
round.number,
);
if (participantId !== undefined)
return previousMatches.filter((m) =>
helpers.isParticipantInMatch(m, participantId),
);
if (participantId !== undefined)
return previousMatches.filter((m) =>
helpers.isParticipantInMatch(m, participantId),
);
return previousMatches;
}
return previousMatches;
}
/**
* Returns the matches following the given match.
*
* If a `participantId` is given:
* - If the participant won, the next match _from their point of view_ is returned.
* - If the participant is eliminated, no match is returned.
*
* @param matchId ID of the target match.
* @param participantId Optional ID of the participant.
*/
public nextMatches(matchId: number, participantId?: number): Match[] {
const match = this.storage.select("match", matchId);
if (!match) throw Error("Match not found.");
/**
* Returns the matches following the given match.
*
* If a `participantId` is given:
* - If the participant won, the next match _from their point of view_ is returned.
* - If the participant is eliminated, no match is returned.
*
* @param matchId ID of the target match.
* @param participantId Optional ID of the participant.
*/
public nextMatches(matchId: number, participantId?: number): Match[] {
const match = this.storage.select("match", matchId);
if (!match) throw Error("Match not found.");
const stage = this.storage.select("stage", match.stage_id);
if (!stage) throw Error("Stage not found.");
const stage = this.storage.select("stage", match.stage_id);
if (!stage) throw Error("Stage not found.");
const group = this.storage.select("group", match.group_id);
if (!group) throw Error("Group not found.");
const group = this.storage.select("group", match.group_id);
if (!group) throw Error("Group not found.");
const { roundNumber, roundCount } = this.getRoundPositionalInfo(
match.round_id,
);
const matchLocation = helpers.getMatchLocation(stage.type, group.number);
const { roundNumber, roundCount } = this.getRoundPositionalInfo(
match.round_id,
);
const matchLocation = helpers.getMatchLocation(stage.type, group.number);
const nextMatches = helpers.getNonNull(
this.getNextMatches(match, matchLocation, stage, roundNumber, roundCount),
);
const nextMatches = helpers.getNonNull(
this.getNextMatches(match, matchLocation, stage, roundNumber, roundCount),
);
if (participantId !== undefined) {
const loser = helpers.getLoser(match);
if (stage.type === "single_elimination" && loser?.id === participantId)
return []; // Eliminated.
if (participantId !== undefined) {
const loser = helpers.getLoser(match);
if (stage.type === "single_elimination" && loser?.id === participantId)
return []; // Eliminated.
if (stage.type === "double_elimination") {
const [upperBracketMatch, lowerBracketMatch] = nextMatches;
if (stage.type === "double_elimination") {
const [upperBracketMatch, lowerBracketMatch] = nextMatches;
if (loser?.id === participantId) {
if (lowerBracketMatch) return [lowerBracketMatch];
else return []; // Eliminated from lower bracket.
}
if (loser?.id === participantId) {
if (lowerBracketMatch) return [lowerBracketMatch];
return []; // Eliminated from lower bracket.
}
const winner = helpers.getWinner(match);
if (winner?.id === participantId) return [upperBracketMatch];
const winner = helpers.getWinner(match);
if (winner?.id === participantId) return [upperBracketMatch];
throw Error("The participant does not belong to this match.");
}
}
throw Error("The participant does not belong to this match.");
}
}
return nextMatches;
}
return nextMatches;
}
/**
* Finds a match in a given group. The match must have the given number in a round of which the number in group is given.
*
* **Example:** In group of id 1, give me the 4th match in the 3rd round.
*
* @param groupId ID of the group.
* @param roundNumber Number of the round in its parent group.
* @param matchNumber Number of the match in its parent round.
*/
public match(
groupId: number,
roundNumber: number,
matchNumber: number,
): Match {
return this.findMatch(groupId, roundNumber, matchNumber);
}
/**
* Finds a match in a given group. The match must have the given number in a round of which the number in group is given.
*
* **Example:** In group of id 1, give me the 4th match in the 3rd round.
*
* @param groupId ID of the group.
* @param roundNumber Number of the round in its parent group.
* @param matchNumber Number of the match in its parent round.
*/
public match(
groupId: number,
roundNumber: number,
matchNumber: number,
): Match {
return this.findMatch(groupId, roundNumber, matchNumber);
}
}

View File

@@ -1,273 +1,264 @@
import type { Stage, Group, Round, Match } from "~/modules/brackets-model";
import type { Group, Match, Round, Stage } from "~/modules/brackets-model";
import { Status } from "~/modules/brackets-model";
import type { Database, ParticipantSlot } from "./types";
import { BaseGetter } from "./base/getter";
import * as helpers from "./helpers";
import type { Database, ParticipantSlot } from "./types";
export class Get extends BaseGetter {
/**
* Returns the data needed to display a stage.
*
* @param stageId ID of the stage.
*/
public stageData(stageId: number): Database {
const stageData = this.getStageSpecificData(stageId);
/**
* Returns the data needed to display a stage.
*
* @param stageId ID of the stage.
*/
public stageData(stageId: number): Database {
const stageData = this.getStageSpecificData(stageId);
return {
stage: [stageData.stage],
group: stageData.groups,
round: stageData.rounds,
match: stageData.matches,
};
}
return {
stage: [stageData.stage],
group: stageData.groups,
round: stageData.rounds,
match: stageData.matches,
};
}
/**
* Returns the data needed to display a whole tournament with all its stages.
*
* @param tournamentId ID of the tournament.
*/
public tournamentData(tournamentId: number): Database {
const stages = this.storage.select("stage", {
tournament_id: tournamentId,
});
if (!stages) throw Error("Error getting stages.");
/**
* Returns the data needed to display a whole tournament with all its stages.
*
* @param tournamentId ID of the tournament.
*/
public tournamentData(tournamentId: number): Database {
const stages = this.storage.select("stage", {
tournament_id: tournamentId,
});
if (!stages) throw Error("Error getting stages.");
const stagesData = stages.map((stage) =>
this.getStageSpecificData(stage.id),
);
const stagesData = stages.map((stage) =>
this.getStageSpecificData(stage.id),
);
return {
stage: stages,
group: stagesData.reduce(
(acc, data) => [...acc, ...data.groups],
[] as Group[],
),
round: stagesData.reduce(
(acc, data) => [...acc, ...data.rounds],
[] as Round[],
),
match: stagesData.reduce(
(acc, data) => [...acc, ...data.matches],
[] as Match[],
),
};
}
return {
stage: stages,
group: stagesData.flatMap((data) => data.groups),
round: stagesData.flatMap((data) => data.rounds),
match: stagesData.flatMap((data) => data.matches),
};
}
/**
* Returns the stage that is not completed yet, because of uncompleted matches.
* If all matches are completed in this tournament, there is no "current stage", so `null` is returned.
*
* @param tournamentId ID of the tournament.
*/
public currentStage(tournamentId: number): Stage | null {
const stages = this.storage.select("stage", {
tournament_id: tournamentId,
});
if (!stages) throw Error("Error getting stages.");
/**
* Returns the stage that is not completed yet, because of uncompleted matches.
* If all matches are completed in this tournament, there is no "current stage", so `null` is returned.
*
* @param tournamentId ID of the tournament.
*/
public currentStage(tournamentId: number): Stage | null {
const stages = this.storage.select("stage", {
tournament_id: tournamentId,
});
if (!stages) throw Error("Error getting stages.");
for (const stage of stages) {
const matches = this.storage.select("match", {
stage_id: stage.id,
});
if (!matches) throw Error("Error getting matches.");
for (const stage of stages) {
const matches = this.storage.select("match", {
stage_id: stage.id,
});
if (!matches) throw Error("Error getting matches.");
if (matches.every((match) => match.status >= Status.Completed)) continue;
if (matches.every((match) => match.status >= Status.Completed)) continue;
return stage;
}
return stage;
}
return null;
}
return null;
}
/**
* Returns the round that is not completed yet, because of uncompleted matches.
* If all matches are completed in this stage of a tournament, there is no "current round", so `null` is returned.
*
* Note: The consolation final of single elimination and the grand final of double elimination will be in a different `Group`.
*
* @param stageId ID of the stage.
* @example
* If you don't know the stage id, you can first get the current stage.
* ```js
* const tournamentId = 3;
* const currentStage = manager.get.currentStage(tournamentId);
* const currentRound = manager.get.currentRound(currentStage.id);
* ```
*/
public currentRound(stageId: number): Round | null {
const matches = this.storage.select("match", { stage_id: stageId });
if (!matches) throw Error("Error getting matches.");
/**
* Returns the round that is not completed yet, because of uncompleted matches.
* If all matches are completed in this stage of a tournament, there is no "current round", so `null` is returned.
*
* Note: The consolation final of single elimination and the grand final of double elimination will be in a different `Group`.
*
* @param stageId ID of the stage.
* @example
* If you don't know the stage id, you can first get the current stage.
* ```js
* const tournamentId = 3;
* const currentStage = manager.get.currentStage(tournamentId);
* const currentRound = manager.get.currentRound(currentStage.id);
* ```
*/
public currentRound(stageId: number): Round | null {
const matches = this.storage.select("match", { stage_id: stageId });
if (!matches) throw Error("Error getting matches.");
const matchesByRound = helpers.splitBy(matches, "round_id");
const matchesByRound = helpers.splitBy(matches, "round_id");
for (const roundMatches of matchesByRound) {
if (roundMatches.every((match) => match.status >= Status.Completed))
continue;
for (const roundMatches of matchesByRound) {
if (roundMatches.every((match) => match.status >= Status.Completed))
continue;
const round = this.storage.select("round", roundMatches[0].round_id);
if (!round) throw Error("Round not found.");
return round;
}
const round = this.storage.select("round", roundMatches[0].round_id);
if (!round) throw Error("Round not found.");
return round;
}
return null;
}
return null;
}
/**
* Returns the matches that can currently be played in parallel.
* If all matches are completed in this stage of a tournament, an empty array is returned.
*
* Note: Completed matches are also returned.
*
* @param stageId ID of the stage.
* @example
* If you don't know the stage id, you can first get the current stage.
* ```js
* const tournamentId = 3;
* const currentStage = manager.get.currentStage(tournamentId);
* const currentMatches = manager.get.currentMatches(currentStage.id);
* ```
*/
public currentMatches(stageId: number): Match[] {
const stage = this.storage.select("stage", stageId);
if (!stage) throw Error("Stage not found.");
/**
* Returns the matches that can currently be played in parallel.
* If all matches are completed in this stage of a tournament, an empty array is returned.
*
* Note: Completed matches are also returned.
*
* @param stageId ID of the stage.
* @example
* If you don't know the stage id, you can first get the current stage.
* ```js
* const tournamentId = 3;
* const currentStage = manager.get.currentStage(tournamentId);
* const currentMatches = manager.get.currentMatches(currentStage.id);
* ```
*/
public currentMatches(stageId: number): Match[] {
const stage = this.storage.select("stage", stageId);
if (!stage) throw Error("Stage not found.");
// TODO: Implement this for all stage types.
// - For round robin, 1 round per group can be played in parallel at their own pace.
// - For double elimination, 1 round per bracket (upper and lower) can be played in parallel at their own pace.
if (stage.type !== "single_elimination")
throw Error(
"Not implemented for round robin and double elimination. Ask if needed.",
);
// TODO: Implement this for all stage types.
// - For round robin, 1 round per group can be played in parallel at their own pace.
// - For double elimination, 1 round per bracket (upper and lower) can be played in parallel at their own pace.
if (stage.type !== "single_elimination")
throw Error(
"Not implemented for round robin and double elimination. Ask if needed.",
);
const matches = this.storage.select("match", { stage_id: stageId });
if (!matches) throw Error("Error getting matches.");
const matches = this.storage.select("match", { stage_id: stageId });
if (!matches) throw Error("Error getting matches.");
const matchesByRound = helpers.splitBy(matches, "round_id");
const roundCount = helpers.getUpperBracketRoundCount(stage.settings.size!);
const matchesByRound = helpers.splitBy(matches, "round_id");
const roundCount = helpers.getUpperBracketRoundCount(stage.settings.size!);
// Save multiple queries for `round`.
let currentRoundIndex = -1;
// Save multiple queries for `round`.
let currentRoundIndex = -1;
for (const roundMatches of matchesByRound) {
currentRoundIndex++;
for (const roundMatches of matchesByRound) {
currentRoundIndex++;
if (
stage.settings.consolationFinal &&
currentRoundIndex === roundCount - 1
) {
// We are on the final of the single elimination.
const [final] = roundMatches;
const [consolationFinal] = matchesByRound[currentRoundIndex + 1];
if (
stage.settings.consolationFinal &&
currentRoundIndex === roundCount - 1
) {
// We are on the final of the single elimination.
const [final] = roundMatches;
const [consolationFinal] = matchesByRound[currentRoundIndex + 1];
const finals = [final, consolationFinal];
if (finals.every((match) => match.status >= Status.Completed))
return [];
const finals = [final, consolationFinal];
if (finals.every((match) => match.status >= Status.Completed))
return [];
return finals;
}
return finals;
}
if (roundMatches.every((match) => match.status >= Status.Completed))
continue;
if (roundMatches.every((match) => match.status >= Status.Completed))
continue;
return roundMatches;
}
return roundMatches;
}
return [];
}
return [];
}
/**
* Returns the seeding of a stage.
*
* @param stageId ID of the stage.
*/
public seeding(stageId: number): ParticipantSlot[] {
const stage = this.storage.select("stage", stageId);
if (!stage) throw Error("Stage not found.");
/**
* Returns the seeding of a stage.
*
* @param stageId ID of the stage.
*/
public seeding(stageId: number): ParticipantSlot[] {
const stage = this.storage.select("stage", stageId);
if (!stage) throw Error("Stage not found.");
const pickRelevantProps = (slot: ParticipantSlot): ParticipantSlot => {
if (slot === null) return null;
const { id, position } = slot;
return { id, position };
};
const pickRelevantProps = (slot: ParticipantSlot): ParticipantSlot => {
if (slot === null) return null;
const { id, position } = slot;
return { id, position };
};
if (stage.type === "round_robin")
return this.roundRobinSeeding(stage).map(pickRelevantProps);
if (stage.type === "round_robin")
return this.roundRobinSeeding(stage).map(pickRelevantProps);
return this.eliminationSeeding(stage).map(pickRelevantProps);
}
return this.eliminationSeeding(stage).map(pickRelevantProps);
}
/**
* Returns the seeding of a round-robin stage.
*
* @param stage The stage.
*/
private roundRobinSeeding(stage: Stage): ParticipantSlot[] {
if (stage.settings.size === undefined)
throw Error("The size of the seeding is undefined.");
/**
* Returns the seeding of a round-robin stage.
*
* @param stage The stage.
*/
private roundRobinSeeding(stage: Stage): ParticipantSlot[] {
if (stage.settings.size === undefined)
throw Error("The size of the seeding is undefined.");
const matches = this.storage.select("match", { stage_id: stage.id });
if (!matches) throw Error("Error getting matches.");
const matches = this.storage.select("match", { stage_id: stage.id });
if (!matches) throw Error("Error getting matches.");
const slots = helpers.convertMatchesToSeeding(matches);
const slots = helpers.convertMatchesToSeeding(matches);
// BYE vs. BYE matches of a round-robin stage are removed
// when the stage is created. We need to add them back temporarily.
if (slots.length < stage.settings.size) {
const diff = stage.settings.size - slots.length;
for (let i = 0; i < diff; i++) slots.push(null);
}
// BYE vs. BYE matches of a round-robin stage are removed
// when the stage is created. We need to add them back temporarily.
if (slots.length < stage.settings.size) {
const diff = stage.settings.size - slots.length;
for (let i = 0; i < diff; i++) slots.push(null);
}
const unique = helpers.uniqueBy(slots, (item) => item && item.position);
const seeding = helpers.setArraySize(unique, stage.settings.size, null);
return seeding;
}
const unique = helpers.uniqueBy(slots, (item) => item?.position);
const seeding = helpers.setArraySize(unique, stage.settings.size, null);
return seeding;
}
/**
* Returns the seeding of an elimination stage.
*
* @param stage The stage.
*/
private eliminationSeeding(stage: Stage): ParticipantSlot[] {
const round = this.storage.selectFirst("round", {
stage_id: stage.id,
number: 1,
});
if (!round) throw Error("Error getting the first round.");
/**
* Returns the seeding of an elimination stage.
*
* @param stage The stage.
*/
private eliminationSeeding(stage: Stage): ParticipantSlot[] {
const round = this.storage.selectFirst("round", {
stage_id: stage.id,
number: 1,
});
if (!round) throw Error("Error getting the first round.");
const matches = this.storage.select("match", { round_id: round.id });
if (!matches) throw Error("Error getting matches.");
const matches = this.storage.select("match", { round_id: round.id });
if (!matches) throw Error("Error getting matches.");
return helpers.convertMatchesToSeeding(matches);
}
return helpers.convertMatchesToSeeding(matches);
}
/**
* Returns only the data specific to the given stage (without the participants).
*
* @param stageId ID of the stage.
*/
private getStageSpecificData(stageId: number): {
stage: Stage;
groups: Group[];
rounds: Round[];
matches: Match[];
} {
const stage = this.storage.select("stage", stageId);
if (!stage) throw Error("Stage not found.");
/**
* Returns only the data specific to the given stage (without the participants).
*
* @param stageId ID of the stage.
*/
private getStageSpecificData(stageId: number): {
stage: Stage;
groups: Group[];
rounds: Round[];
matches: Match[];
} {
const stage = this.storage.select("stage", stageId);
if (!stage) throw Error("Stage not found.");
const groups = this.storage.select("group", { stage_id: stageId });
if (!groups) throw Error("Error getting groups.");
const groups = this.storage.select("group", { stage_id: stageId });
if (!groups) throw Error("Error getting groups.");
const rounds = this.storage.select("round", { stage_id: stageId });
if (!rounds) throw Error("Error getting rounds.");
const rounds = this.storage.select("round", { stage_id: stageId });
if (!rounds) throw Error("Error getting rounds.");
const matches = this.storage.select("match", { stage_id: stageId });
if (!matches) throw Error("Error getting matches.");
const matches = this.storage.select("match", { stage_id: stageId });
if (!matches) throw Error("Error getting matches.");
return {
stage,
groups,
rounds,
matches,
};
}
return {
stage,
groups,
rounds,
matches,
};
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,32 +1,32 @@
import { BracketsManager } from "./manager";
import * as helpers from "./helpers";
import { BracketsManager } from "./manager";
import type {
CrudInterface,
Database,
Duel,
OmitId,
OrderingMap,
ParticipantSlot,
Scores,
Side,
StandardBracketResults,
Storage,
Table,
CrudInterface,
Database,
Duel,
OmitId,
OrderingMap,
ParticipantSlot,
Scores,
Side,
StandardBracketResults,
Storage,
Table,
} from "./types";
export {
BracketsManager,
CrudInterface,
Database,
Duel,
OmitId,
OrderingMap,
ParticipantSlot,
Scores,
Side,
StandardBracketResults,
Storage,
Table,
helpers,
BracketsManager,
type CrudInterface,
type Database,
type Duel,
type OmitId,
type OrderingMap,
type ParticipantSlot,
type Scores,
type Side,
type StandardBracketResults,
type Storage,
type Table,
helpers,
};

View File

@@ -1,125 +1,125 @@
import type {
CrudInterface,
Database,
DataTypes,
Storage,
Table,
} from "./types";
import type { InputStage, Stage } from "~/modules/brackets-model";
import { create } from "./create";
import { Get } from "./get";
import { Update } from "./update";
import { Delete } from "./delete";
import { Find } from "./find";
import { Reset } from "./reset";
import { Get } from "./get";
import * as helpers from "./helpers";
import { Reset } from "./reset";
import type {
CrudInterface,
DataTypes,
Database,
Storage,
Table,
} from "./types";
import { Update } from "./update";
/**
* A class to handle tournament management at those levels: `stage`, `group`, `round` and `match`.
*/
export class BracketsManager {
public storage: Storage;
public storage: Storage;
public get: Get;
public update: Update;
public delete: Delete;
public find: Find;
public reset: Reset;
public get: Get;
public update: Update;
public delete: Delete;
public find: Find;
public reset: Reset;
/**
* Creates an instance of BracketsManager, which will handle all the stuff from the library.
*
* @param storageInterface An implementation of CrudInterface.
*/
constructor(storageInterface: CrudInterface) {
const storage = storageInterface as Storage;
/**
* Creates an instance of BracketsManager, which will handle all the stuff from the library.
*
* @param storageInterface An implementation of CrudInterface.
*/
constructor(storageInterface: CrudInterface) {
const storage = storageInterface as Storage;
storage.selectFirst = <T extends Table>(
table: T,
filter: Partial<DataTypes[T]>,
): DataTypes[T] | null => {
const results = this.storage.select<T>(table, filter);
if (!results || results.length === 0) return null;
return results[0];
};
storage.selectFirst = <T extends Table>(
table: T,
filter: Partial<DataTypes[T]>,
): DataTypes[T] | null => {
const results = this.storage.select<T>(table, filter);
if (!results || results.length === 0) return null;
return results[0];
};
storage.selectLast = <T extends Table>(
table: T,
filter: Partial<DataTypes[T]>,
): DataTypes[T] | null => {
const results = this.storage.select<T>(table, filter);
if (!results || results.length === 0) return null;
return results[results.length - 1];
};
storage.selectLast = <T extends Table>(
table: T,
filter: Partial<DataTypes[T]>,
): DataTypes[T] | null => {
const results = this.storage.select<T>(table, filter);
if (!results || results.length === 0) return null;
return results[results.length - 1];
};
this.storage = storage;
this.get = new Get(this.storage);
this.update = new Update(this.storage);
this.delete = new Delete(this.storage);
this.find = new Find(this.storage);
this.reset = new Reset(this.storage);
}
this.storage = storage;
this.get = new Get(this.storage);
this.update = new Update(this.storage);
this.delete = new Delete(this.storage);
this.find = new Find(this.storage);
this.reset = new Reset(this.storage);
}
/**
* Creates a stage for an existing tournament. The tournament won't be created.
*
* @param stage A stage to create.
*/
public create(stage: InputStage): Stage {
return create.call(this, stage);
}
/**
* Creates a stage for an existing tournament. The tournament won't be created.
*
* @param stage A stage to create.
*/
public create(stage: InputStage): Stage {
return create.call(this, stage);
}
/**
* Imports data in the database.
*
* @param data Data to import.
* @param normalizeIds Enable ID normalization: all IDs (and references to them) are remapped to consecutive IDs starting from 0.
*/
public import(data: Database, normalizeIds = false): void {
if (normalizeIds) data = helpers.normalizeIds(data);
/**
* Imports data in the database.
*
* @param data Data to import.
* @param normalizeIds Enable ID normalization: all IDs (and references to them) are remapped to consecutive IDs starting from 0.
*/
public import(rawData: Database, normalizeIds = false): void {
const data = normalizeIds ? helpers.normalizeIds(rawData) : rawData;
if (!this.storage.delete("stage"))
throw Error("Could not empty the stage table.");
if (!this.storage.insert("stage", data.stage))
throw Error("Could not import stages.");
if (!this.storage.delete("stage"))
throw Error("Could not empty the stage table.");
if (!this.storage.insert("stage", data.stage))
throw Error("Could not import stages.");
if (!this.storage.delete("group"))
throw Error("Could not empty the group table.");
if (!this.storage.insert("group", data.group))
throw Error("Could not import groups.");
if (!this.storage.delete("group"))
throw Error("Could not empty the group table.");
if (!this.storage.insert("group", data.group))
throw Error("Could not import groups.");
if (!this.storage.delete("round"))
throw Error("Could not empty the round table.");
if (!this.storage.insert("round", data.round))
throw Error("Could not import rounds.");
if (!this.storage.delete("round"))
throw Error("Could not empty the round table.");
if (!this.storage.insert("round", data.round))
throw Error("Could not import rounds.");
if (!this.storage.delete("match"))
throw Error("Could not empty the match table.");
if (!this.storage.insert("match", data.match))
throw Error("Could not import matches.");
}
if (!this.storage.delete("match"))
throw Error("Could not empty the match table.");
if (!this.storage.insert("match", data.match))
throw Error("Could not import matches.");
}
/**
* Exports data from the database.
*/
public export(): Database {
const stages = this.storage.select("stage");
if (!stages) throw Error("Error getting stages.");
/**
* Exports data from the database.
*/
public export(): Database {
const stages = this.storage.select("stage");
if (!stages) throw Error("Error getting stages.");
const groups = this.storage.select("group");
if (!groups) throw Error("Error getting groups.");
const groups = this.storage.select("group");
if (!groups) throw Error("Error getting groups.");
const rounds = this.storage.select("round");
if (!rounds) throw Error("Error getting rounds.");
const rounds = this.storage.select("round");
if (!rounds) throw Error("Error getting rounds.");
const matches = this.storage.select("match");
if (!matches) throw Error("Error getting matches.");
const matches = this.storage.select("match");
if (!matches) throw Error("Error getting matches.");
return {
stage: stages,
group: groups,
round: rounds,
match: matches,
};
}
return {
stage: stages,
group: groups,
round: rounds,
match: matches,
};
}
}

View File

@@ -1,143 +1,143 @@
// https://web.archive.org/web/20200601102344/https://tl.net/forum/sc2-tournaments/202139-superior-double-elimination-losers-bracket-seeding
import type { SeedOrdering } from "~/modules/brackets-model";
import type { OrderingMap } from "./types";
import invariant from "~/utils/invariant";
import type { OrderingMap } from "./types";
export const ordering: OrderingMap = {
natural: <T>(array: T[]) => [...array],
reverse: <T>(array: T[]) => [...array].reverse(),
half_shift: <T>(array: T[]) => [
...array.slice(array.length / 2),
...array.slice(0, array.length / 2),
],
reverse_half_shift: <T>(array: T[]) => [
...array.slice(0, array.length / 2).reverse(),
...array.slice(array.length / 2).reverse(),
],
pair_flip: <T>(array: T[]) => {
const result: T[] = [];
for (let i = 0; i < array.length; i += 2)
result.push(array[i + 1], array[i]);
return result;
},
// https://stackoverflow.com/a/11631472
space_between: <T>(array: T[]) => {
const numPlayers = array.length;
natural: <T>(array: T[]) => [...array],
reverse: <T>(array: T[]) => [...array].reverse(),
half_shift: <T>(array: T[]) => [
...array.slice(array.length / 2),
...array.slice(0, array.length / 2),
],
reverse_half_shift: <T>(array: T[]) => [
...array.slice(0, array.length / 2).reverse(),
...array.slice(array.length / 2).reverse(),
],
pair_flip: <T>(array: T[]) => {
const result: T[] = [];
for (let i = 0; i < array.length; i += 2)
result.push(array[i + 1], array[i]);
return result;
},
// https://stackoverflow.com/a/11631472
space_between: <T>(array: T[]) => {
const numPlayers = array.length;
const rounds = Math.log(numPlayers) / Math.log(2) - 1;
let pls = [1, 2];
for (let i = 0; i < rounds; i++) {
pls = nextLayer(pls);
}
return seedsToOrderedArray(pls);
function nextLayer(pls: number[]) {
const out: number[] = [];
const length = pls.length * 2 + 1;
pls.forEach(function (d) {
out.push(d);
out.push(length - d);
});
return out;
}
// this part added to the SO answer
function seedsToOrderedArray(seeds: number[]) {
return seeds.map((seed) => {
const participant = array[seed - 1];
invariant(participant !== undefined, `No participant for seed ${seed}`);
return participant;
});
}
},
inner_outer: <T>(array: T[]) => {
if (array.length === 2) return array;
const rounds = Math.log(numPlayers) / Math.log(2) - 1;
let pls = [1, 2];
for (let i = 0; i < rounds; i++) {
pls = nextLayer(pls);
}
return seedsToOrderedArray(pls);
function nextLayer(pls: number[]) {
const out: number[] = [];
const length = pls.length * 2 + 1;
for (const d of pls) {
out.push(d);
out.push(length - d);
}
return out;
}
// this part added to the SO answer
function seedsToOrderedArray(seeds: number[]) {
return seeds.map((seed) => {
const participant = array[seed - 1];
invariant(participant !== undefined, `No participant for seed ${seed}`);
return participant;
});
}
},
inner_outer: <T>(array: T[]) => {
if (array.length === 2) return array;
const size = array.length / 4;
const size = array.length / 4;
const innerPart = [
array.slice(size, 2 * size),
array.slice(2 * size, 3 * size),
]; // [_, X, X, _]
const outerPart = [array.slice(0, size), array.slice(3 * size, 4 * size)]; // [X, _, _, X]
const innerPart = [
array.slice(size, 2 * size),
array.slice(2 * size, 3 * size),
]; // [_, X, X, _]
const outerPart = [array.slice(0, size), array.slice(3 * size, 4 * size)]; // [X, _, _, X]
const methods = {
inner(part: T[][]): T[] {
return [part[0].pop()!, part[1].shift()!];
},
outer(part: T[][]): T[] {
return [part[0].shift()!, part[1].pop()!];
},
};
const methods = {
inner(part: T[][]): T[] {
return [part[0].pop()!, part[1].shift()!];
},
outer(part: T[][]): T[] {
return [part[0].shift()!, part[1].pop()!];
},
};
const result: T[] = [];
const result: T[] = [];
/**
* Adds a part (inner or outer) of a part.
*
* @param part The part to process.
* @param method The method to use.
*/
function add(part: T[][], method: "inner" | "outer"): void {
if (part[0].length > 0 && part[1].length > 0)
result.push(...methods[method](part));
}
/**
* Adds a part (inner or outer) of a part.
*
* @param part The part to process.
* @param method The method to use.
*/
function add(part: T[][], method: "inner" | "outer"): void {
if (part[0].length > 0 && part[1].length > 0)
result.push(...methods[method](part));
}
for (let i = 0; i < size / 2; i++) {
add(outerPart, "outer"); // Outer part's outer
add(innerPart, "inner"); // Inner part's inner
add(outerPart, "inner"); // Outer part's inner
add(innerPart, "outer"); // Inner part's outer
}
for (let i = 0; i < size / 2; i++) {
add(outerPart, "outer"); // Outer part's outer
add(innerPart, "inner"); // Inner part's inner
add(outerPart, "inner"); // Outer part's inner
add(innerPart, "outer"); // Inner part's outer
}
return result;
},
"groups.effort_balanced": <T>(array: T[], groupCount: number) => {
const result: T[] = [];
let i = 0,
j = 0;
return result;
},
"groups.effort_balanced": <T>(array: T[], groupCount: number) => {
const result: T[] = [];
let i = 0;
let j = 0;
while (result.length < array.length) {
result.push(array[i]);
i += groupCount;
if (i >= array.length) i = ++j;
}
while (result.length < array.length) {
result.push(array[i]);
i += groupCount;
if (i >= array.length) i = ++j;
}
return result;
},
"groups.seed_optimized": <T>(array: T[], groupCount: number) => {
const groups = Array.from(Array(groupCount), (_): T[] => []);
return result;
},
"groups.seed_optimized": <T>(array: T[], groupCount: number) => {
const groups = Array.from(Array(groupCount), (_): T[] => []);
for (let run = 0; run < array.length / groupCount; run++) {
if (run % 2 === 0) {
for (let group = 0; group < groupCount; group++)
groups[group].push(array[run * groupCount + group]);
} else {
for (let group = 0; group < groupCount; group++)
groups[groupCount - group - 1].push(array[run * groupCount + group]);
}
}
for (let run = 0; run < array.length / groupCount; run++) {
if (run % 2 === 0) {
for (let group = 0; group < groupCount; group++)
groups[group].push(array[run * groupCount + group]);
} else {
for (let group = 0; group < groupCount; group++)
groups[groupCount - group - 1].push(array[run * groupCount + group]);
}
}
return groups.flat();
},
"groups.bracket_optimized": () => {
throw Error("Not implemented.");
},
return groups.flat();
},
"groups.bracket_optimized": () => {
throw Error("Not implemented.");
},
};
export const defaultMinorOrdering: { [key: number]: SeedOrdering[] } = {
// 1 or 2: Not possible.
4: ["natural", "reverse"],
8: ["natural", "reverse", "natural"],
16: ["natural", "reverse_half_shift", "reverse", "natural"],
32: ["natural", "reverse", "half_shift", "natural", "natural"],
64: ["natural", "reverse", "half_shift", "reverse", "natural", "natural"],
128: [
"natural",
"reverse",
"half_shift",
"pair_flip",
"pair_flip",
"pair_flip",
"natural",
],
// 1 or 2: Not possible.
4: ["natural", "reverse"],
8: ["natural", "reverse", "natural"],
16: ["natural", "reverse_half_shift", "reverse", "natural"],
32: ["natural", "reverse", "half_shift", "natural", "natural"],
64: ["natural", "reverse", "half_shift", "reverse", "natural", "natural"],
128: [
"natural",
"reverse",
"half_shift",
"pair_flip",
"pair_flip",
"pair_flip",
"natural",
],
};

View File

@@ -3,52 +3,52 @@ import { BaseUpdater } from "./base/updater";
import * as helpers from "./helpers";
export class Reset extends BaseUpdater {
/**
* Resets the results of a match.
*
* This will update related matches accordingly.
*
* @param matchId ID of the match.
*/
public matchResults(matchId: number): void {
const stored = this.storage.select("match", matchId);
if (!stored) throw Error("Match not found.");
/**
* Resets the results of a match.
*
* This will update related matches accordingly.
*
* @param matchId ID of the match.
*/
public matchResults(matchId: number): void {
const stored = this.storage.select("match", matchId);
if (!stored) throw Error("Match not found.");
const stage = this.storage.select("stage", stored.stage_id);
if (!stage) throw Error("Stage not found.");
const stage = this.storage.select("stage", stored.stage_id);
if (!stage) throw Error("Stage not found.");
const group = this.storage.select("group", stored.group_id);
if (!group) throw Error("Group not found.");
const group = this.storage.select("group", stored.group_id);
if (!group) throw Error("Group not found.");
const { roundNumber, roundCount } = this.getRoundPositionalInfo(
stored.round_id,
);
const matchLocation = helpers.getMatchLocation(stage.type, group.number);
const nextMatches =
stage.type !== "round_robin" && stage.type !== "swiss"
? this.getNextMatches(
stored,
matchLocation,
stage,
roundNumber,
roundCount,
)
: [];
const { roundNumber, roundCount } = this.getRoundPositionalInfo(
stored.round_id,
);
const matchLocation = helpers.getMatchLocation(stage.type, group.number);
const nextMatches =
stage.type !== "round_robin" && stage.type !== "swiss"
? this.getNextMatches(
stored,
matchLocation,
stage,
roundNumber,
roundCount,
)
: [];
if (
nextMatches.some(
(match) =>
match &&
match.status >= Status.Running &&
!helpers.isMatchByeCompleted(match),
)
)
throw Error("The match is locked.");
if (
nextMatches.some(
(match) =>
match &&
match.status >= Status.Running &&
!helpers.isMatchByeCompleted(match),
)
)
throw Error("The match is locked.");
helpers.resetMatchResults(stored);
this.applyMatchUpdate(stored);
helpers.resetMatchResults(stored);
this.applyMatchUpdate(stored);
if (!helpers.isRoundRobin(stage) && !helpers.isSwiss(stage))
this.updateRelatedMatches(stored, true, true);
}
if (!helpers.isRoundRobin(stage) && !helpers.isSwiss(stage))
this.updateRelatedMatches(stored, true, true);
}
}

View File

@@ -1,7 +1,7 @@
import * as assert from "uvu/assert";
import { BracketsManager } from "../manager";
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { suite } from "uvu";
import * as assert from "uvu/assert";
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
const storage = new InMemoryDatabase();
const manager = new BracketsManager(storage);
@@ -9,64 +9,64 @@ const manager = new BracketsManager(storage);
const ExtraFields = suite("Update results with extra fields");
ExtraFields.before.each(() => {
storage.reset();
storage.reset();
});
ExtraFields("Extra fields when updating a match", () => {
manager.create({
name: "Amateur",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
});
manager.create({
name: "Amateur",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
});
manager.update.match({
id: 0,
// @ts-expect-error incomplete types
weather: "rainy", // Extra field.
opponent1: {
score: 3,
result: "win",
},
opponent2: {
score: 1,
result: "loss",
},
});
manager.update.match({
id: 0,
// @ts-expect-error incomplete types
weather: "rainy", // Extra field.
opponent1: {
score: 3,
result: "win",
},
opponent2: {
score: 1,
result: "loss",
},
});
manager.update.match({
id: 1,
opponent1: {
score: 3,
result: "win",
// @ts-expect-error incomplete types
foo: 42, // Extra field.
},
opponent2: {
score: 1,
result: "loss",
},
});
manager.update.match({
id: 1,
opponent1: {
score: 3,
result: "win",
// @ts-expect-error incomplete types
foo: 42, // Extra field.
},
opponent2: {
score: 1,
result: "loss",
},
});
manager.update.match({
id: 2,
opponent1: {
score: 3,
result: "win",
},
opponent2: {
score: 1,
result: "loss",
// @ts-expect-error incomplete types
info: { replacements: [1, 2] }, // Extra field.
},
});
manager.update.match({
id: 2,
opponent1: {
score: 3,
result: "win",
},
opponent2: {
score: 1,
result: "loss",
// @ts-expect-error incomplete types
info: { replacements: [1, 2] }, // Extra field.
},
});
assert.equal(storage.select<any>("match", 0).weather, "rainy");
assert.equal(storage.select<any>("match", 1).opponent1.foo, 42);
assert.equal(storage.select<any>("match", 2).opponent2.info, {
replacements: [1, 2],
});
assert.equal(storage.select<any>("match", 0).weather, "rainy");
assert.equal(storage.select<any>("match", 1).opponent1.foo, 42);
assert.equal(storage.select<any>("match", 2).opponent2.info, {
replacements: [1, 2],
});
});
ExtraFields.run();

View File

@@ -1,7 +1,7 @@
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
import { suite } from "uvu";
import * as assert from "uvu/assert";
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
const storage = new InMemoryDatabase();
const manager = new BracketsManager(storage);
@@ -9,90 +9,90 @@ const manager = new BracketsManager(storage);
const DeleteStage = suite("Delete stage");
DeleteStage.before.each(() => {
storage.reset();
storage.reset();
});
DeleteStage("should delete a stage and all its linked data", () => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
});
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
});
manager.delete.stage(0);
manager.delete.stage(0);
const stages = storage.select("stage")!;
const groups = storage.select("group")!;
const rounds = storage.select("round")!;
const matches = storage.select<any>("match")!;
const stages = storage.select("stage")!;
const groups = storage.select("group")!;
const rounds = storage.select("round")!;
const matches = storage.select<any>("match")!;
assert.equal(stages.length, 0);
assert.equal(groups.length, 0);
assert.equal(rounds.length, 0);
assert.equal(matches.length, 0);
assert.equal(stages.length, 0);
assert.equal(groups.length, 0);
assert.equal(rounds.length, 0);
assert.equal(matches.length, 0);
});
DeleteStage("should delete one stage and only its linked data", () => {
manager.create({
name: "Example 1",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
});
manager.create({
name: "Example 1",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
});
manager.create({
name: "Example 2",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
});
manager.create({
name: "Example 2",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
});
manager.delete.stage(0);
manager.delete.stage(0);
const stages = storage.select<any>("stage")!;
const groups = storage.select<any>("group")!;
const rounds = storage.select<any>("round")!;
const matches = storage.select<any>("match")!;
const stages = storage.select<any>("stage")!;
const groups = storage.select<any>("group")!;
const rounds = storage.select<any>("round")!;
const matches = storage.select<any>("match")!;
assert.equal(stages.length, 1);
assert.equal(groups.length, 1);
assert.equal(rounds.length, 2);
assert.equal(matches.length, 3);
assert.equal(stages.length, 1);
assert.equal(groups.length, 1);
assert.equal(rounds.length, 2);
assert.equal(matches.length, 3);
// Remaining data
assert.equal(stages[0].id, 1);
assert.equal(groups[0].id, 1);
assert.equal(rounds[0].id, 2);
assert.equal(matches[0].id, 3);
// Remaining data
assert.equal(stages[0].id, 1);
assert.equal(groups[0].id, 1);
assert.equal(rounds[0].id, 2);
assert.equal(matches[0].id, 3);
});
DeleteStage("should delete all stages of the tournament", () => {
manager.create({
name: "Example 1",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
});
manager.create({
name: "Example 1",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
});
manager.create({
name: "Example 2",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
});
manager.create({
name: "Example 2",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
});
manager.delete.tournament(0);
manager.delete.tournament(0);
const stages = storage.select("stage")!;
const groups = storage.select("group")!;
const rounds = storage.select("round")!;
const matches = storage.select<any>("match")!;
const stages = storage.select("stage")!;
const groups = storage.select("group")!;
const rounds = storage.select("round")!;
const matches = storage.select<any>("match")!;
assert.equal(stages.length, 0);
assert.equal(groups.length, 0);
assert.equal(rounds.length, 0);
assert.equal(matches.length, 0);
assert.equal(stages.length, 0);
assert.equal(groups.length, 0);
assert.equal(rounds.length, 0);
assert.equal(matches.length, 0);
});
DeleteStage.run();

View File

@@ -1,8 +1,8 @@
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
import { suite } from "uvu";
import * as assert from "uvu/assert";
import { Status } from "~/db/types";
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
const storage = new InMemoryDatabase();
const manager = new BracketsManager(storage);
@@ -10,421 +10,421 @@ const manager = new BracketsManager(storage);
const CreateDoubleElimination = suite("Delete stage");
CreateDoubleElimination.before.each(() => {
storage.reset();
storage.reset();
});
CreateDoubleElimination("should create a double elimination stage", () => {
manager.create({
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: { seedOrdering: ["natural"], grandFinal: "simple" },
});
manager.create({
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: { seedOrdering: ["natural"], grandFinal: "simple" },
});
const stage = storage.select<any>("stage", 0);
assert.equal(stage.name, "Amateur");
assert.equal(stage.type, "double_elimination");
const stage = storage.select<any>("stage", 0);
assert.equal(stage.name, "Amateur");
assert.equal(stage.type, "double_elimination");
assert.equal(storage.select<any>("group")!.length, 3);
assert.equal(storage.select<any>("round")!.length, 4 + 6 + 1);
assert.equal(storage.select<any>("match")!.length, 30);
assert.equal(storage.select<any>("group")!.length, 3);
assert.equal(storage.select<any>("round")!.length, 4 + 6 + 1);
assert.equal(storage.select<any>("match")!.length, 30);
});
CreateDoubleElimination(
"should create a tournament with 256+ tournaments",
() => {
manager.create({
name: "Example with 256 participants",
tournamentId: 0,
type: "double_elimination",
settings: { size: 256 },
});
},
"should create a tournament with 256+ tournaments",
() => {
manager.create({
name: "Example with 256 participants",
tournamentId: 0,
type: "double_elimination",
settings: { size: 256 },
});
},
);
CreateDoubleElimination(
"should create a tournament with a double grand final",
() => {
manager.create({
name: "Example with double grand final",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: { grandFinal: "double", seedOrdering: ["natural"] },
});
"should create a tournament with a double grand final",
() => {
manager.create({
name: "Example with double grand final",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: { grandFinal: "double", seedOrdering: ["natural"] },
});
assert.equal(storage.select<any>("group")!.length, 3);
assert.equal(storage.select<any>("round")!.length, 3 + 4 + 2);
assert.equal(storage.select<any>("match")!.length, 15);
},
assert.equal(storage.select<any>("group")!.length, 3);
assert.equal(storage.select<any>("round")!.length, 3 + 4 + 2);
assert.equal(storage.select<any>("match")!.length, 15);
},
);
const MatchUpdateDoubleElimination = suite(
"Previous and next match update in double elimination stage",
"Previous and next match update in double elimination stage",
);
MatchUpdateDoubleElimination.before.each(() => {
storage.reset();
storage.reset();
});
MatchUpdateDoubleElimination(
"should end a match and determine next matches",
() => {
manager.create({
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: { seedOrdering: ["natural"], grandFinal: "simple" },
});
"should end a match and determine next matches",
() => {
manager.create({
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: { seedOrdering: ["natural"], grandFinal: "simple" },
});
const before = storage.select<any>("match", 8); // First match of WB round 2
assert.equal(before.opponent2.id, null);
const before = storage.select<any>("match", 8); // First match of WB round 2
assert.equal(before.opponent2.id, null);
manager.update.match({
id: 0, // First match of WB round 1
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 0, // First match of WB round 1
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 1, // Second match of WB round 1
opponent1: { score: 13 },
opponent2: { score: 16, result: "win" },
});
manager.update.match({
id: 1, // Second match of WB round 1
opponent1: { score: 13 },
opponent2: { score: 16, result: "win" },
});
manager.update.match({
id: 15, // First match of LB round 1
opponent1: { score: 16, result: "win" },
opponent2: { score: 10 },
});
manager.update.match({
id: 15, // First match of LB round 1
opponent1: { score: 16, result: "win" },
opponent2: { score: 10 },
});
assert.equal(
storage.select<any>("match", 8).opponent1.id, // Determined opponent for WB round 2
storage.select<any>("match", 0).opponent1.id, // Winner of first match round 1
);
assert.equal(
storage.select<any>("match", 8).opponent1.id, // Determined opponent for WB round 2
storage.select<any>("match", 0).opponent1.id, // Winner of first match round 1
);
assert.equal(
storage.select<any>("match", 8).opponent2.id, // Determined opponent for WB round 2
storage.select<any>("match", 1).opponent2.id, // Winner of second match round 1
);
assert.equal(
storage.select<any>("match", 8).opponent2.id, // Determined opponent for WB round 2
storage.select<any>("match", 1).opponent2.id, // Winner of second match round 1
);
assert.equal(
storage.select<any>("match", 15).opponent2.id, // Determined opponent for LB round 1
storage.select<any>("match", 1).opponent1.id, // Loser of second match round 1
);
assert.equal(
storage.select<any>("match", 15).opponent2.id, // Determined opponent for LB round 1
storage.select<any>("match", 1).opponent1.id, // Loser of second match round 1
);
assert.equal(
storage.select<any>("match", 19).opponent2.id, // Determined opponent for LB round 2
storage.select<any>("match", 0).opponent2.id, // Loser of first match round 1
);
},
assert.equal(
storage.select<any>("match", 19).opponent2.id, // Determined opponent for LB round 2
storage.select<any>("match", 0).opponent2.id, // Loser of first match round 1
);
},
);
MatchUpdateDoubleElimination(
"should propagate winner when BYE is already in next match in loser bracket",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, null],
settings: { grandFinal: "simple" },
});
"should propagate winner when BYE is already in next match in loser bracket",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, null],
settings: { grandFinal: "simple" },
});
manager.update.match({
id: 1, // Second match of WB round 1
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 1, // Second match of WB round 1
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
const loserId = storage.select<any>("match", 1).opponent2.id;
let matchSemiLB = storage.select<any>("match", 3);
const loserId = storage.select<any>("match", 1).opponent2.id;
let matchSemiLB = storage.select<any>("match", 3);
assert.equal(matchSemiLB.opponent2.id, loserId);
assert.equal(matchSemiLB.opponent2.result, "win");
assert.equal(matchSemiLB.status, Status.Completed);
assert.equal(matchSemiLB.opponent2.id, loserId);
assert.equal(matchSemiLB.opponent2.result, "win");
assert.equal(matchSemiLB.status, Status.Completed);
assert.equal(
storage.select<any>("match", 4).opponent2.id, // Propagated winner in LB Final because of the BYE.
loserId,
);
assert.equal(
storage.select<any>("match", 4).opponent2.id, // Propagated winner in LB Final because of the BYE.
loserId,
);
manager.reset.matchResults(1); // Second match of WB round 1
manager.reset.matchResults(1); // Second match of WB round 1
matchSemiLB = storage.select<any>("match", 3);
assert.equal(matchSemiLB.opponent2.id, null);
assert.equal(matchSemiLB.opponent2.result, undefined);
assert.equal(matchSemiLB.status, Status.Locked);
matchSemiLB = storage.select<any>("match", 3);
assert.equal(matchSemiLB.opponent2.id, null);
assert.equal(matchSemiLB.opponent2.result, undefined);
assert.equal(matchSemiLB.status, Status.Locked);
assert.equal(storage.select<any>("match", 4).opponent2.id, null); // Propagated winner is removed.
},
assert.equal(storage.select<any>("match", 4).opponent2.id, null); // Propagated winner is removed.
},
);
MatchUpdateDoubleElimination("should determine matches in grand final", () => {
manager.create({
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4],
settings: { grandFinal: "double" },
});
manager.create({
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4],
settings: { grandFinal: "double" },
});
manager.update.match({
id: 0, // First match of WB round 1
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 0, // First match of WB round 1
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 1, // Second match of WB round 1
opponent1: { score: 13 },
opponent2: { score: 16, result: "win" },
});
manager.update.match({
id: 1, // Second match of WB round 1
opponent1: { score: 13 },
opponent2: { score: 16, result: "win" },
});
manager.update.match({
id: 2, // WB Final
opponent1: { score: 16, result: "win" },
opponent2: { score: 9 },
});
manager.update.match({
id: 2, // WB Final
opponent1: { score: 16, result: "win" },
opponent2: { score: 9 },
});
assert.equal(
storage.select<any>("match", 5).opponent1.id, // Determined opponent for the grand final (round 1)
storage.select<any>("match", 0).opponent1.id, // Winner of WB Final
);
assert.equal(
storage.select<any>("match", 5).opponent1.id, // Determined opponent for the grand final (round 1)
storage.select<any>("match", 0).opponent1.id, // Winner of WB Final
);
manager.update.match({
id: 3, // Only match of LB round 1
opponent1: { score: 12, result: "win" }, // Team 4
opponent2: { score: 8 },
});
manager.update.match({
id: 3, // Only match of LB round 1
opponent1: { score: 12, result: "win" }, // Team 4
opponent2: { score: 8 },
});
manager.update.match({
id: 4, // LB Final
opponent1: { score: 14, result: "win" }, // Team 3
opponent2: { score: 7 },
});
manager.update.match({
id: 4, // LB Final
opponent1: { score: 14, result: "win" }, // Team 3
opponent2: { score: 7 },
});
assert.equal(
storage.select<any>("match", 5).opponent2.id, // Determined opponent for the grand final (round 1)
storage.select<any>("match", 1).opponent2.id, // Winner of LB Final
);
assert.equal(
storage.select<any>("match", 5).opponent2.id, // Determined opponent for the grand final (round 1)
storage.select<any>("match", 1).opponent2.id, // Winner of LB Final
);
manager.update.match({
id: 5, // Grand Final round 1
opponent1: { score: 10 },
opponent2: { score: 16, result: "win" }, // Team 3
});
manager.update.match({
id: 5, // Grand Final round 1
opponent1: { score: 10 },
opponent2: { score: 16, result: "win" }, // Team 3
});
assert.equal(
storage.select<any>("match", 6).opponent2.id, // Determined opponent for the grand final (round 2)
storage.select<any>("match", 1).opponent2.id, // Winner of LB Final
);
assert.equal(
storage.select<any>("match", 6).opponent2.id, // Determined opponent for the grand final (round 2)
storage.select<any>("match", 1).opponent2.id, // Winner of LB Final
);
assert.equal(storage.select<any>("match", 5).status, Status.Completed); // Grand final (round 1)
assert.equal(storage.select<any>("match", 6).status, Status.Ready); // Grand final (round 2)
assert.equal(storage.select<any>("match", 5).status, Status.Completed); // Grand final (round 1)
assert.equal(storage.select<any>("match", 6).status, Status.Ready); // Grand final (round 2)
manager.update.match({
id: 6, // Grand Final round 2
opponent1: { score: 16, result: "win" },
opponent2: { score: 10 },
});
manager.update.match({
id: 6, // Grand Final round 2
opponent1: { score: 16, result: "win" },
opponent2: { score: 10 },
});
});
MatchUpdateDoubleElimination(
"should determine next matches and reset them",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4],
settings: { grandFinal: "double" },
});
"should determine next matches and reset them",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4],
settings: { grandFinal: "double" },
});
manager.update.match({
id: 0, // First match of WB round 1
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 0, // First match of WB round 1
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
const beforeReset = storage.select<any>("match", 3); // Determined opponent for LB round 1
assert.equal(
beforeReset.opponent1.id,
storage.select<any>("match", 0).opponent2.id,
);
assert.equal(beforeReset.opponent1.position, 1); // Must be set.
const beforeReset = storage.select<any>("match", 3); // Determined opponent for LB round 1
assert.equal(
beforeReset.opponent1.id,
storage.select<any>("match", 0).opponent2.id,
);
assert.equal(beforeReset.opponent1.position, 1); // Must be set.
manager.reset.matchResults(0); // First match of WB round 1
manager.reset.matchResults(0); // First match of WB round 1
const afterReset = storage.select<any>("match", 3); // Determined opponent for LB round 1
assert.equal(afterReset.opponent1.id, null);
assert.equal(afterReset.opponent1.position, 1); // It must stay.
},
const afterReset = storage.select<any>("match", 3); // Determined opponent for LB round 1
assert.equal(afterReset.opponent1.id, null);
assert.equal(afterReset.opponent1.position, 1); // It must stay.
},
);
MatchUpdateDoubleElimination(
"should choose the correct previous and next matches based on losers ordering",
() => {
manager.create({
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: {
seedOrdering: ["natural", "reverse", "reverse"],
grandFinal: "simple",
},
});
"should choose the correct previous and next matches based on losers ordering",
() => {
manager.create({
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: {
seedOrdering: ["natural", "reverse", "reverse"],
grandFinal: "simple",
},
});
manager.update.match({ id: 0, opponent1: { result: "win" } }); // WB 1.1
assert.equal(
storage.select<any>("match", 18).opponent2.id, // Determined opponent for last match of LB round 1 (reverse ordering for losers)
storage.select<any>("match", 0).opponent2.id, // Loser of first match round 1
);
manager.update.match({ id: 0, opponent1: { result: "win" } }); // WB 1.1
assert.equal(
storage.select<any>("match", 18).opponent2.id, // Determined opponent for last match of LB round 1 (reverse ordering for losers)
storage.select<any>("match", 0).opponent2.id, // Loser of first match round 1
);
manager.update.match({ id: 1, opponent1: { result: "win" } }); // WB 1.2
assert.equal(
storage.select<any>("match", 18).opponent1.id, // Determined opponent for last match of LB round 1 (reverse ordering for losers)
storage.select<any>("match", 1).opponent2.id, // Loser of second match round 1
);
manager.update.match({ id: 1, opponent1: { result: "win" } }); // WB 1.2
assert.equal(
storage.select<any>("match", 18).opponent1.id, // Determined opponent for last match of LB round 1 (reverse ordering for losers)
storage.select<any>("match", 1).opponent2.id, // Loser of second match round 1
);
manager.update.match({ id: 8, opponent1: { result: "win" } }); // WB 2.1
assert.equal(
storage.select<any>("match", 22).opponent1.id, // Determined opponent for last match of LB round 2 (reverse ordering for losers)
storage.select<any>("match", 8).opponent2.id, // Loser of first match round 2
);
manager.update.match({ id: 8, opponent1: { result: "win" } }); // WB 2.1
assert.equal(
storage.select<any>("match", 22).opponent1.id, // Determined opponent for last match of LB round 2 (reverse ordering for losers)
storage.select<any>("match", 8).opponent2.id, // Loser of first match round 2
);
manager.update.match({ id: 6, opponent1: { result: "win" } }); // WB 1.7
manager.update.match({ id: 7, opponent1: { result: "win" } }); // WB 1.8
manager.update.match({ id: 11, opponent1: { result: "win" } }); // WB 2.4
manager.update.match({ id: 15, opponent1: { result: "win" } }); // LB 1.1
manager.update.match({ id: 19, opponent1: { result: "win" } }); // LB 2.1
manager.update.match({ id: 6, opponent1: { result: "win" } }); // WB 1.7
manager.update.match({ id: 7, opponent1: { result: "win" } }); // WB 1.8
manager.update.match({ id: 11, opponent1: { result: "win" } }); // WB 2.4
manager.update.match({ id: 15, opponent1: { result: "win" } }); // LB 1.1
manager.update.match({ id: 19, opponent1: { result: "win" } }); // LB 2.1
assert.equal(storage.select<any>("match", 8).status, Status.Completed); // WB 2.1
},
assert.equal(storage.select<any>("match", 8).status, Status.Completed); // WB 2.1
},
);
MatchUpdateDoubleElimination(
"should send the losers to the right LB matches in round 1",
() => {
manager.create({
name: "Example with inner_outer loser ordering",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
seedOrdering: ["inner_outer", "inner_outer"],
},
});
"should send the losers to the right LB matches in round 1",
() => {
manager.create({
name: "Example with inner_outer loser ordering",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
seedOrdering: ["inner_outer", "inner_outer"],
},
});
assert.equal(storage.select<any>("match", 7).opponent1.position, 1);
assert.equal(storage.select<any>("match", 7).opponent2.position, 4);
assert.equal(storage.select<any>("match", 8).opponent1.position, 2);
assert.equal(storage.select<any>("match", 8).opponent2.position, 3);
assert.equal(storage.select<any>("match", 7).opponent1.position, 1);
assert.equal(storage.select<any>("match", 7).opponent2.position, 4);
assert.equal(storage.select<any>("match", 8).opponent1.position, 2);
assert.equal(storage.select<any>("match", 8).opponent2.position, 3);
// Match of position 1.
manager.update.match({
id: 0,
opponent1: { result: "win" }, // Loser id: 7.
});
// Match of position 1.
manager.update.match({
id: 0,
opponent1: { result: "win" }, // Loser id: 7.
});
assert.equal(storage.select<any>("match", 7).opponent1.id, 8);
assert.equal(storage.select<any>("match", 7).opponent1.id, 8);
// Match of position 2.
manager.update.match({
id: 1,
opponent1: { result: "win" }, // Loser id: 4.
});
// Match of position 2.
manager.update.match({
id: 1,
opponent1: { result: "win" }, // Loser id: 4.
});
assert.equal(storage.select<any>("match", 8).opponent1.id, 5);
assert.equal(storage.select<any>("match", 8).opponent1.id, 5);
// Match of position 3.
manager.update.match({
id: 2,
opponent1: { result: "win" }, // Loser id: 6.
});
// Match of position 3.
manager.update.match({
id: 2,
opponent1: { result: "win" }, // Loser id: 6.
});
assert.equal(storage.select<any>("match", 8).opponent2.id, 7);
assert.equal(storage.select<any>("match", 8).opponent2.id, 7);
// Match of position 4.
manager.update.match({
id: 3,
opponent1: { result: "win" }, // Loser id: 5.
});
// Match of position 4.
manager.update.match({
id: 3,
opponent1: { result: "win" }, // Loser id: 5.
});
assert.equal(storage.select<any>("match", 7).opponent2.id, 6);
},
assert.equal(storage.select<any>("match", 7).opponent2.id, 6);
},
);
const SkipFirstRoundDoubleElimination = suite("Skip first round");
SkipFirstRoundDoubleElimination.before.each(() => {
storage.reset();
storage.reset();
manager.create({
name: "Example with double grand final",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: {
seedOrdering: ["natural"],
skipFirstRound: true,
grandFinal: "double",
},
});
manager.create({
name: "Example with double grand final",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: {
seedOrdering: ["natural"],
skipFirstRound: true,
grandFinal: "double",
},
});
});
SkipFirstRoundDoubleElimination(
"should create a double elimination stage with skip first round option",
() => {
assert.equal(storage.select<any>("group")!.length, 3);
assert.equal(storage.select<any>("round")!.length, 3 + 6 + 2); // One round less in WB.
assert.equal(
storage.select<any>("match")!.length,
4 + 2 + 1 + (4 + 4 + 2 + 2 + 1 + 1) + (1 + 1),
);
"should create a double elimination stage with skip first round option",
() => {
assert.equal(storage.select<any>("group")!.length, 3);
assert.equal(storage.select<any>("round")!.length, 3 + 6 + 2); // One round less in WB.
assert.equal(
storage.select<any>("match")!.length,
4 + 2 + 1 + (4 + 4 + 2 + 2 + 1 + 1) + (1 + 1),
);
assert.equal(storage.select<any>("round", 0).number, 1); // Even though the "real" first round is skipped, the stored first round's number should be 1.
assert.equal(storage.select<any>("round", 0).number, 1); // Even though the "real" first round is skipped, the stored first round's number should be 1.
assert.equal(storage.select<any>("match", 0).opponent1.id, 1); // First match of WB.
assert.equal(storage.select<any>("match", 7).opponent1.id, 2); // First match of LB.
},
assert.equal(storage.select<any>("match", 0).opponent1.id, 1); // First match of WB.
assert.equal(storage.select<any>("match", 7).opponent1.id, 2); // First match of LB.
},
);
SkipFirstRoundDoubleElimination(
"should choose the correct previous and next matches",
() => {
manager.update.match({ id: 0, opponent1: { result: "win" } });
assert.equal(storage.select<any>("match", 7).opponent1.id, 2); // First match of LB Round 1 (must stay).
assert.equal(storage.select<any>("match", 12).opponent1.id, 3); // First match of LB Round 2 (must be updated).
"should choose the correct previous and next matches",
() => {
manager.update.match({ id: 0, opponent1: { result: "win" } });
assert.equal(storage.select<any>("match", 7).opponent1.id, 2); // First match of LB Round 1 (must stay).
assert.equal(storage.select<any>("match", 12).opponent1.id, 3); // First match of LB Round 2 (must be updated).
manager.update.match({ id: 1, opponent1: { result: "win" } });
assert.equal(storage.select<any>("match", 7).opponent2.id, 4); // First match of LB Round 1 (must stay).
assert.equal(storage.select<any>("match", 11).opponent1.id, 7); // Second match of LB Round 2 (must be updated).
manager.update.match({ id: 1, opponent1: { result: "win" } });
assert.equal(storage.select<any>("match", 7).opponent2.id, 4); // First match of LB Round 1 (must stay).
assert.equal(storage.select<any>("match", 11).opponent1.id, 7); // Second match of LB Round 2 (must be updated).
manager.update.match({ id: 4, opponent1: { result: "win" } }); // First match of WB Round 2.
assert.equal(storage.select<any>("match", 18).opponent1.id, 5); // First match of LB Round 4.
manager.update.match({ id: 4, opponent1: { result: "win" } }); // First match of WB Round 2.
assert.equal(storage.select<any>("match", 18).opponent1.id, 5); // First match of LB Round 4.
manager.update.match({ id: 7, opponent1: { result: "win" } }); // First match of LB Round 1.
assert.equal(storage.select<any>("match", 11).opponent2.id, 2); // First match of LB Round 2.
manager.update.match({ id: 7, opponent1: { result: "win" } }); // First match of LB Round 1.
assert.equal(storage.select<any>("match", 11).opponent2.id, 2); // First match of LB Round 2.
for (let i = 2; i < 21; i++)
manager.update.match({ id: i, opponent1: { result: "win" } });
for (let i = 2; i < 21; i++)
manager.update.match({ id: i, opponent1: { result: "win" } });
assert.equal(storage.select<any>("match", 15).opponent1.id, 7); // First match of LB Round 3.
assert.equal(storage.select<any>("match", 15).opponent1.id, 7); // First match of LB Round 3.
assert.equal(storage.select<any>("match", 21).opponent1.id, 1); // GF Round 1.
assert.equal(storage.select<any>("match", 21).opponent2.id, 9); // GF Round 1.
assert.equal(storage.select<any>("match", 21).opponent1.id, 1); // GF Round 1.
assert.equal(storage.select<any>("match", 21).opponent2.id, 9); // GF Round 1.
manager.update.match({ id: 21, opponent2: { result: "win" } });
manager.update.match({ id: 21, opponent2: { result: "win" } });
assert.equal(storage.select<any>("match", 21).opponent1.id, 1); // GF Round 2.
assert.equal(storage.select<any>("match", 22).opponent2.id, 9); // GF Round 2.
assert.equal(storage.select<any>("match", 21).opponent1.id, 1); // GF Round 2.
assert.equal(storage.select<any>("match", 22).opponent2.id, 9); // GF Round 2.
manager.update.match({ id: 22, opponent2: { result: "win" } });
},
manager.update.match({ id: 22, opponent2: { result: "win" } });
},
);
CreateDoubleElimination.run();

View File

@@ -1,231 +1,231 @@
import { suite } from "uvu";
import * as assert from "uvu/assert";
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
import * as assert from "uvu/assert";
import { suite } from "uvu";
const storage = new InMemoryDatabase();
const manager = new BracketsManager(storage);
const FindSingleElimination = suite(
"Find previous and next matches in single elimination",
"Find previous and next matches in single elimination",
);
FindSingleElimination.before.each(() => {
storage.reset();
storage.reset();
});
FindSingleElimination("should find previous matches", () => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
});
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
});
const beforeFirst = manager.find.previousMatches(0);
assert.equal(beforeFirst.length, 0);
const beforeFirst = manager.find.previousMatches(0);
assert.equal(beforeFirst.length, 0);
const beforeSemi1 = manager.find.previousMatches(4);
assert.equal(beforeSemi1.length, 2);
assert.equal(beforeSemi1[0].id, 0);
assert.equal(beforeSemi1[1].id, 1);
const beforeSemi1 = manager.find.previousMatches(4);
assert.equal(beforeSemi1.length, 2);
assert.equal(beforeSemi1[0].id, 0);
assert.equal(beforeSemi1[1].id, 1);
const beforeSemi2 = manager.find.previousMatches(5);
assert.equal(beforeSemi2.length, 2);
assert.equal(beforeSemi2[0].id, 2);
assert.equal(beforeSemi2[1].id, 3);
const beforeSemi2 = manager.find.previousMatches(5);
assert.equal(beforeSemi2.length, 2);
assert.equal(beforeSemi2[0].id, 2);
assert.equal(beforeSemi2[1].id, 3);
const beforeFinal = manager.find.previousMatches(6);
assert.equal(beforeFinal.length, 2);
assert.equal(beforeFinal[0].id, 4);
assert.equal(beforeFinal[1].id, 5);
const beforeFinal = manager.find.previousMatches(6);
assert.equal(beforeFinal.length, 2);
assert.equal(beforeFinal[0].id, 4);
assert.equal(beforeFinal[1].id, 5);
});
FindSingleElimination("should find next matches", () => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
});
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
});
const afterFirst = manager.find.nextMatches(0);
assert.equal(afterFirst.length, 1);
assert.equal(afterFirst[0].id, 4);
const afterFirst = manager.find.nextMatches(0);
assert.equal(afterFirst.length, 1);
assert.equal(afterFirst[0].id, 4);
const afterSemi1 = manager.find.nextMatches(4);
assert.equal(afterSemi1.length, 1);
assert.equal(afterSemi1[0].id, 6);
const afterSemi1 = manager.find.nextMatches(4);
assert.equal(afterSemi1.length, 1);
assert.equal(afterSemi1[0].id, 6);
const afterFinal = manager.find.nextMatches(6);
assert.equal(afterFinal.length, 0);
const afterFinal = manager.find.nextMatches(6);
assert.equal(afterFinal.length, 0);
});
FindSingleElimination(
"should return matches from the point of view of a participant",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
seedOrdering: ["natural"],
},
});
"should return matches from the point of view of a participant",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
seedOrdering: ["natural"],
},
});
manager.update.match({ id: 0, opponent1: { result: "loss" } });
const afterFirstEliminated = manager.find.nextMatches(0, 1);
assert.equal(afterFirstEliminated.length, 0);
const afterFirstContinued = manager.find.nextMatches(0, 2);
assert.equal(afterFirstContinued.length, 1);
manager.update.match({ id: 0, opponent1: { result: "loss" } });
const afterFirstEliminated = manager.find.nextMatches(0, 1);
assert.equal(afterFirstEliminated.length, 0);
const afterFirstContinued = manager.find.nextMatches(0, 2);
assert.equal(afterFirstContinued.length, 1);
manager.update.match({ id: 1, opponent1: { result: "win" } });
const beforeSemi1Up = manager.find.previousMatches(4, 2);
assert.equal(beforeSemi1Up.length, 1);
assert.equal(beforeSemi1Up[0].id, 0);
manager.update.match({ id: 1, opponent1: { result: "win" } });
const beforeSemi1Up = manager.find.previousMatches(4, 2);
assert.equal(beforeSemi1Up.length, 1);
assert.equal(beforeSemi1Up[0].id, 0);
const beforeSemi1Down = manager.find.previousMatches(4, 3);
assert.equal(beforeSemi1Down.length, 1);
assert.equal(beforeSemi1Down[0].id, 1);
},
const beforeSemi1Down = manager.find.previousMatches(4, 3);
assert.equal(beforeSemi1Down.length, 1);
assert.equal(beforeSemi1Down[0].id, 1);
},
);
const FindDoubleElimination = suite(
"Find previous and next matches in double elimination",
"Find previous and next matches in double elimination",
);
FindDoubleElimination.before.each(() => {
storage.reset();
storage.reset();
});
FindDoubleElimination("should find previous matches", () => {
manager.create({
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
});
manager.create({
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
});
const beforeFirstWB = manager.find.previousMatches(0);
assert.equal(beforeFirstWB.length, 0);
const beforeFirstWB = manager.find.previousMatches(0);
assert.equal(beforeFirstWB.length, 0);
const beforeSemi1WB = manager.find.previousMatches(4);
assert.equal(beforeSemi1WB.length, 2);
assert.equal(beforeSemi1WB[0].id, 0);
assert.equal(beforeSemi1WB[1].id, 1);
const beforeSemi1WB = manager.find.previousMatches(4);
assert.equal(beforeSemi1WB.length, 2);
assert.equal(beforeSemi1WB[0].id, 0);
assert.equal(beforeSemi1WB[1].id, 1);
const beforeSemi2WB = manager.find.previousMatches(5);
assert.equal(beforeSemi2WB.length, 2);
assert.equal(beforeSemi2WB[0].id, 2);
assert.equal(beforeSemi2WB[1].id, 3);
const beforeSemi2WB = manager.find.previousMatches(5);
assert.equal(beforeSemi2WB.length, 2);
assert.equal(beforeSemi2WB[0].id, 2);
assert.equal(beforeSemi2WB[1].id, 3);
const beforeFinalWB = manager.find.previousMatches(6);
assert.equal(beforeFinalWB.length, 2);
assert.equal(beforeFinalWB[0].id, 4);
assert.equal(beforeFinalWB[1].id, 5);
const beforeFinalWB = manager.find.previousMatches(6);
assert.equal(beforeFinalWB.length, 2);
assert.equal(beforeFinalWB[0].id, 4);
assert.equal(beforeFinalWB[1].id, 5);
const beforeFirstRound1LB = manager.find.previousMatches(7);
assert.equal(beforeFirstRound1LB.length, 2);
assert.equal(beforeFirstRound1LB[0].id, 0);
assert.equal(beforeFirstRound1LB[1].id, 1);
const beforeFirstRound1LB = manager.find.previousMatches(7);
assert.equal(beforeFirstRound1LB.length, 2);
assert.equal(beforeFirstRound1LB[0].id, 0);
assert.equal(beforeFirstRound1LB[1].id, 1);
const beforeFirstRound2LB = manager.find.previousMatches(9);
assert.equal(beforeFirstRound2LB.length, 2);
assert.equal(beforeFirstRound2LB[0].id, 5);
assert.equal(beforeFirstRound2LB[1].id, 7);
const beforeFirstRound2LB = manager.find.previousMatches(9);
assert.equal(beforeFirstRound2LB.length, 2);
assert.equal(beforeFirstRound2LB[0].id, 5);
assert.equal(beforeFirstRound2LB[1].id, 7);
const beforeSemi1LB = manager.find.previousMatches(11);
assert.equal(beforeSemi1LB.length, 2);
assert.equal(beforeSemi1LB[0].id, 9);
assert.equal(beforeSemi1LB[1].id, 10);
const beforeSemi1LB = manager.find.previousMatches(11);
assert.equal(beforeSemi1LB.length, 2);
assert.equal(beforeSemi1LB[0].id, 9);
assert.equal(beforeSemi1LB[1].id, 10);
const beforeFinalLB = manager.find.previousMatches(12);
assert.equal(beforeFinalLB.length, 2);
assert.equal(beforeFinalLB[0].id, 6);
assert.equal(beforeFinalLB[1].id, 11);
const beforeFinalLB = manager.find.previousMatches(12);
assert.equal(beforeFinalLB.length, 2);
assert.equal(beforeFinalLB[0].id, 6);
assert.equal(beforeFinalLB[1].id, 11);
});
FindDoubleElimination("should find next matches", () => {
manager.create({
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
});
manager.create({
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
});
const afterFirstWB = manager.find.nextMatches(0);
assert.equal(afterFirstWB.length, 2);
assert.equal(afterFirstWB[0].id, 4);
assert.equal(afterFirstWB[1].id, 7);
const afterFirstWB = manager.find.nextMatches(0);
assert.equal(afterFirstWB.length, 2);
assert.equal(afterFirstWB[0].id, 4);
assert.equal(afterFirstWB[1].id, 7);
const afterSemi1WB = manager.find.nextMatches(4);
assert.equal(afterSemi1WB.length, 2);
assert.equal(afterSemi1WB[0].id, 6);
assert.equal(afterSemi1WB[1].id, 10);
const afterSemi1WB = manager.find.nextMatches(4);
assert.equal(afterSemi1WB.length, 2);
assert.equal(afterSemi1WB[0].id, 6);
assert.equal(afterSemi1WB[1].id, 10);
const afterFinalWB = manager.find.nextMatches(6);
assert.equal(afterFinalWB.length, 1);
assert.equal(afterFinalWB[0].id, 12);
const afterFinalWB = manager.find.nextMatches(6);
assert.equal(afterFinalWB.length, 1);
assert.equal(afterFinalWB[0].id, 12);
const afterFirstRound1LB = manager.find.nextMatches(7);
assert.equal(afterFirstRound1LB.length, 1);
assert.equal(afterFirstRound1LB[0].id, 9);
const afterFirstRound1LB = manager.find.nextMatches(7);
assert.equal(afterFirstRound1LB.length, 1);
assert.equal(afterFirstRound1LB[0].id, 9);
const afterFirstRound2LB = manager.find.nextMatches(9);
assert.equal(afterFirstRound2LB.length, 1);
assert.equal(afterFirstRound2LB[0].id, 11);
const afterFirstRound2LB = manager.find.nextMatches(9);
assert.equal(afterFirstRound2LB.length, 1);
assert.equal(afterFirstRound2LB[0].id, 11);
const afterSemi1LB = manager.find.nextMatches(11);
assert.equal(afterSemi1LB.length, 1);
assert.equal(afterSemi1LB[0].id, 12);
const afterSemi1LB = manager.find.nextMatches(11);
assert.equal(afterSemi1LB.length, 1);
assert.equal(afterSemi1LB[0].id, 12);
const afterFinalLB = manager.find.nextMatches(12);
assert.equal(afterFinalLB.length, 0);
const afterFinalLB = manager.find.nextMatches(12);
assert.equal(afterFinalLB.length, 0);
});
FindDoubleElimination(
"should return matches from the point of view of a participant",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4],
settings: {
seedOrdering: ["natural"],
},
});
"should return matches from the point of view of a participant",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4],
settings: {
seedOrdering: ["natural"],
},
});
manager.update.match({ id: 0, opponent1: { result: "loss" } });
const afterFirstEliminated = manager.find.nextMatches(0, 1);
assert.equal(afterFirstEliminated.length, 1);
assert.equal(afterFirstEliminated[0].id, 3);
const afterFirstContinued = manager.find.nextMatches(0, 2);
assert.equal(afterFirstContinued.length, 1);
assert.equal(afterFirstContinued[0].id, 2);
manager.update.match({ id: 0, opponent1: { result: "loss" } });
const afterFirstEliminated = manager.find.nextMatches(0, 1);
assert.equal(afterFirstEliminated.length, 1);
assert.equal(afterFirstEliminated[0].id, 3);
const afterFirstContinued = manager.find.nextMatches(0, 2);
assert.equal(afterFirstContinued.length, 1);
assert.equal(afterFirstContinued[0].id, 2);
manager.update.match({ id: 1, opponent1: { result: "win" } });
const beforeSemi1Up = manager.find.previousMatches(2, 2);
assert.equal(beforeSemi1Up.length, 1);
assert.equal(beforeSemi1Up[0].id, 0);
manager.update.match({ id: 1, opponent1: { result: "win" } });
const beforeSemi1Up = manager.find.previousMatches(2, 2);
assert.equal(beforeSemi1Up.length, 1);
assert.equal(beforeSemi1Up[0].id, 0);
const beforeSemi1Down = manager.find.previousMatches(2, 3);
assert.equal(beforeSemi1Down.length, 1);
assert.equal(beforeSemi1Down[0].id, 1);
const beforeSemi1Down = manager.find.previousMatches(2, 3);
assert.equal(beforeSemi1Down.length, 1);
assert.equal(beforeSemi1Down[0].id, 1);
manager.update.match({ id: 3, opponent1: { result: "loss" } });
const afterLowerBracketEliminated = manager.find.nextMatches(3, 1);
assert.equal(afterLowerBracketEliminated.length, 0);
const afterLowerBracketContinued = manager.find.nextMatches(3, 4);
assert.equal(afterLowerBracketContinued.length, 1);
assert.equal(afterLowerBracketContinued[0].id, 4);
manager.update.match({ id: 3, opponent1: { result: "loss" } });
const afterLowerBracketEliminated = manager.find.nextMatches(3, 1);
assert.equal(afterLowerBracketEliminated.length, 0);
const afterLowerBracketContinued = manager.find.nextMatches(3, 4);
assert.equal(afterLowerBracketContinued.length, 1);
assert.equal(afterLowerBracketContinued[0].id, 4);
assert.throws(
() => manager.find.nextMatches(3, 42),
"The participant does not belong to this match.",
);
},
assert.throws(
() => manager.find.nextMatches(3, 42),
"The participant does not belong to this match.",
);
},
);
FindSingleElimination.run();

View File

@@ -1,7 +1,7 @@
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
import { suite } from "uvu";
import * as assert from "uvu/assert";
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
const storage = new InMemoryDatabase();
const manager = new BracketsManager(storage);
@@ -9,509 +9,509 @@ const manager = new BracketsManager(storage);
const BYEHandling = suite("BYE handling");
BYEHandling.before.each(() => {
storage.reset();
storage.reset();
});
BYEHandling("should propagate BYEs through the brackets", () => {
manager.create({
name: "Example with BYEs",
tournamentId: 0,
type: "double_elimination",
seeding: [1, null, null, null],
settings: { seedOrdering: ["natural"], grandFinal: "simple" },
});
manager.create({
name: "Example with BYEs",
tournamentId: 0,
type: "double_elimination",
seeding: [1, null, null, null],
settings: { seedOrdering: ["natural"], grandFinal: "simple" },
});
assert.equal(storage.select<any>("match", 2).opponent1.id, 1);
assert.equal(storage.select<any>("match", 2).opponent2, null);
assert.equal(storage.select<any>("match", 2).opponent1.id, 1);
assert.equal(storage.select<any>("match", 2).opponent2, null);
assert.equal(storage.select<any>("match", 3).opponent1, null);
assert.equal(storage.select<any>("match", 3).opponent2, null);
assert.equal(storage.select<any>("match", 3).opponent1, null);
assert.equal(storage.select<any>("match", 3).opponent2, null);
assert.equal(storage.select<any>("match", 4).opponent1, null);
assert.equal(storage.select<any>("match", 4).opponent2, null);
assert.equal(storage.select<any>("match", 4).opponent1, null);
assert.equal(storage.select<any>("match", 4).opponent2, null);
assert.equal(storage.select<any>("match", 5).opponent1.id, 1);
assert.equal(storage.select<any>("match", 5).opponent2, null);
assert.equal(storage.select<any>("match", 5).opponent1.id, 1);
assert.equal(storage.select<any>("match", 5).opponent2, null);
});
BYEHandling("should handle incomplete seeding during creation", () => {
manager.create({
name: "Example with BYEs",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2],
settings: {
seedOrdering: ["natural"],
balanceByes: false, // Default value.
size: 4,
},
});
manager.create({
name: "Example with BYEs",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2],
settings: {
seedOrdering: ["natural"],
balanceByes: false, // Default value.
size: 4,
},
});
assert.equal(storage.select<any>("match", 0).opponent1.id, 1);
assert.equal(storage.select<any>("match", 0).opponent2.id, 2);
assert.equal(storage.select<any>("match", 0).opponent1.id, 1);
assert.equal(storage.select<any>("match", 0).opponent2.id, 2);
assert.equal(storage.select<any>("match", 1).opponent1, null);
assert.equal(storage.select<any>("match", 1).opponent2, null);
assert.equal(storage.select<any>("match", 1).opponent1, null);
assert.equal(storage.select<any>("match", 1).opponent2, null);
});
BYEHandling("should balance BYEs in the seeding", () => {
manager.create({
name: "Example with BYEs",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2],
settings: {
seedOrdering: ["natural"],
balanceByes: true,
size: 4,
},
});
manager.create({
name: "Example with BYEs",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2],
settings: {
seedOrdering: ["natural"],
balanceByes: true,
size: 4,
},
});
assert.equal(storage.select<any>("match", 0).opponent1.id, 1);
assert.equal(storage.select<any>("match", 0).opponent2, null);
assert.equal(storage.select<any>("match", 0).opponent1.id, 1);
assert.equal(storage.select<any>("match", 0).opponent2, null);
assert.equal(storage.select<any>("match", 1).opponent1.id, 2);
assert.equal(storage.select<any>("match", 1).opponent2, null);
assert.equal(storage.select<any>("match", 1).opponent1.id, 2);
assert.equal(storage.select<any>("match", 1).opponent2, null);
});
const PositionChecks = suite("Position checks");
PositionChecks.before.each(() => {
storage.reset();
storage.reset();
manager.create({
name: "Example with double grand final",
tournamentId: 0,
type: "double_elimination",
settings: {
size: 8,
grandFinal: "simple",
seedOrdering: ["natural"],
},
});
manager.create({
name: "Example with double grand final",
tournamentId: 0,
type: "double_elimination",
settings: {
size: 8,
grandFinal: "simple",
seedOrdering: ["natural"],
},
});
});
PositionChecks(
"should not have a position when we don't need the origin of a participant",
() => {
const matchFromWbRound2 = storage.select<any>("match", 4);
assert.equal(matchFromWbRound2.opponent1.position, undefined);
assert.equal(matchFromWbRound2.opponent2.position, undefined);
"should not have a position when we don't need the origin of a participant",
() => {
const matchFromWbRound2 = storage.select<any>("match", 4);
assert.equal(matchFromWbRound2.opponent1.position, undefined);
assert.equal(matchFromWbRound2.opponent2.position, undefined);
const matchFromLbRound2 = storage.select<any>("match", 9);
assert.equal(matchFromLbRound2.opponent2.position, undefined);
const matchFromLbRound2 = storage.select<any>("match", 9);
assert.equal(matchFromLbRound2.opponent2.position, undefined);
const matchFromGrandFinal = storage.select<any>("match", 13);
assert.equal(matchFromGrandFinal.opponent1.position, undefined);
},
const matchFromGrandFinal = storage.select<any>("match", 13);
assert.equal(matchFromGrandFinal.opponent1.position, undefined);
},
);
PositionChecks(
"should have a position where we need the origin of a participant",
() => {
const matchFromWbRound1 = storage.select<any>("match", 0);
assert.equal(matchFromWbRound1.opponent1.position, 1);
assert.equal(matchFromWbRound1.opponent2.position, 2);
"should have a position where we need the origin of a participant",
() => {
const matchFromWbRound1 = storage.select<any>("match", 0);
assert.equal(matchFromWbRound1.opponent1.position, 1);
assert.equal(matchFromWbRound1.opponent2.position, 2);
const matchFromLbRound1 = storage.select<any>("match", 7);
assert.equal(matchFromLbRound1.opponent1.position, 1);
assert.equal(matchFromLbRound1.opponent2.position, 2);
const matchFromLbRound1 = storage.select<any>("match", 7);
assert.equal(matchFromLbRound1.opponent1.position, 1);
assert.equal(matchFromLbRound1.opponent2.position, 2);
const matchFromLbRound2 = storage.select<any>("match", 9);
assert.equal(matchFromLbRound2.opponent1.position, 2);
const matchFromLbRound2 = storage.select<any>("match", 9);
assert.equal(matchFromLbRound2.opponent1.position, 2);
const matchFromGrandFinal = storage.select<any>("match", 13);
assert.equal(matchFromGrandFinal.opponent2.position, 1);
},
const matchFromGrandFinal = storage.select<any>("match", 13);
assert.equal(matchFromGrandFinal.opponent2.position, 1);
},
);
const SpecialCases = suite("Special cases");
SpecialCases.before.each(() => {
storage.reset();
storage.reset();
});
SpecialCases("should throw if the name of the stage is not provided", () => {
assert.throws(
() =>
// @ts-expect-error testing throwing
manager.create({
tournamentId: 0,
type: "single_elimination",
}),
"You must provide a name for the stage.",
);
assert.throws(
() =>
// @ts-expect-error testing throwing
manager.create({
tournamentId: 0,
type: "single_elimination",
}),
"You must provide a name for the stage.",
);
});
SpecialCases(
"should throw if the tournament id of the stage is not provided",
() => {
assert.throws(
() =>
// @ts-expect-error testing throwing
manager.create({
name: "Example",
type: "single_elimination",
}),
"You must provide a tournament id for the stage.",
);
},
"should throw if the tournament id of the stage is not provided",
() => {
assert.throws(
() =>
// @ts-expect-error testing throwing
manager.create({
name: "Example",
type: "single_elimination",
}),
"You must provide a tournament id for the stage.",
);
},
);
SpecialCases(
"should throw if the participant count of a stage is not a power of two",
() => {
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7],
}),
"The library only supports a participant count which is a power of two.",
);
"should throw if the participant count of a stage is not a power of two",
() => {
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7],
}),
"The library only supports a participant count which is a power of two.",
);
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
settings: { size: 3 },
}),
"The library only supports a participant count which is a power of two.",
);
},
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
settings: { size: 3 },
}),
"The library only supports a participant count which is a power of two.",
);
},
);
SpecialCases(
"should throw if the participant count of a stage is less than two",
() => {
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
settings: { size: 0 },
}),
"Impossible to create an empty stage. If you want an empty seeding, just set the size of the stage.",
);
"should throw if the participant count of a stage is less than two",
() => {
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
settings: { size: 0 },
}),
"Impossible to create an empty stage. If you want an empty seeding, just set the size of the stage.",
);
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
settings: { size: 1 },
}),
"Impossible to create a stage with less than 2 participants.",
);
},
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
settings: { size: 1 },
}),
"Impossible to create a stage with less than 2 participants.",
);
},
);
const SeedingAndOrderingInElimination = suite(
"Seeding and ordering in elimination",
"Seeding and ordering in elimination",
);
SeedingAndOrderingInElimination.before.each(() => {
storage.reset();
storage.reset();
manager.create({
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: {
seedOrdering: [
"inner_outer",
"reverse",
"pair_flip",
"half_shift",
"reverse",
],
},
});
manager.create({
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: {
seedOrdering: [
"inner_outer",
"reverse",
"pair_flip",
"half_shift",
"reverse",
],
},
});
});
SeedingAndOrderingInElimination(
"should have the good orderings everywhere",
() => {
const firstRoundMatchWB = storage.select<any>("match", 0);
assert.equal(firstRoundMatchWB.opponent1.position, 1);
assert.equal(firstRoundMatchWB.opponent2.position, 16);
"should have the good orderings everywhere",
() => {
const firstRoundMatchWB = storage.select<any>("match", 0);
assert.equal(firstRoundMatchWB.opponent1.position, 1);
assert.equal(firstRoundMatchWB.opponent2.position, 16);
const firstRoundMatchLB = storage.select<any>("match", 15);
assert.equal(firstRoundMatchLB.opponent1.position, 8);
assert.equal(firstRoundMatchLB.opponent2.position, 7);
const firstRoundMatchLB = storage.select<any>("match", 15);
assert.equal(firstRoundMatchLB.opponent1.position, 8);
assert.equal(firstRoundMatchLB.opponent2.position, 7);
const secondRoundMatchLB = storage.select<any>("match", 19);
assert.equal(secondRoundMatchLB.opponent1.position, 2);
const secondRoundMatchLB = storage.select<any>("match", 19);
assert.equal(secondRoundMatchLB.opponent1.position, 2);
const secondRoundSecondMatchLB = storage.select<any>("match", 20);
assert.equal(secondRoundSecondMatchLB.opponent1.position, 1);
const secondRoundSecondMatchLB = storage.select<any>("match", 20);
assert.equal(secondRoundSecondMatchLB.opponent1.position, 1);
const fourthRoundMatchLB = storage.select<any>("match", 25);
assert.equal(fourthRoundMatchLB.opponent1.position, 2);
const fourthRoundMatchLB = storage.select<any>("match", 25);
assert.equal(fourthRoundMatchLB.opponent1.position, 2);
const finalRoundMatchLB = storage.select<any>("match", 28);
assert.equal(finalRoundMatchLB.opponent1.position, 1);
},
const finalRoundMatchLB = storage.select<any>("match", 28);
assert.equal(finalRoundMatchLB.opponent1.position, 1);
},
);
SeedingAndOrderingInElimination("should update the orderings in rounds", () => {
let firstRoundMatchWB = storage.select<any>("match", 0);
let firstRoundMatchWB = storage.select<any>("match", 0);
// Inner outer before changing.
assert.equal(firstRoundMatchWB.opponent1.position, 1);
assert.equal(firstRoundMatchWB.opponent2.position, 16);
// Inner outer before changing.
assert.equal(firstRoundMatchWB.opponent1.position, 1);
assert.equal(firstRoundMatchWB.opponent2.position, 16);
manager.update.roundOrdering(0, "pair_flip");
manager.update.roundOrdering(0, "pair_flip");
firstRoundMatchWB = storage.select<any>("match", 0);
firstRoundMatchWB = storage.select<any>("match", 0);
// Should now be pair_flip.
assert.equal(firstRoundMatchWB.opponent1.position, 2);
assert.equal(firstRoundMatchWB.opponent2.position, 1);
// Should now be pair_flip.
assert.equal(firstRoundMatchWB.opponent1.position, 2);
assert.equal(firstRoundMatchWB.opponent2.position, 1);
manager.update.roundOrdering(5, "reverse");
manager.update.roundOrdering(5, "reverse");
const secondRoundMatchLB = storage.select<any>("match", 19);
assert.equal(secondRoundMatchLB.opponent1.position, 4);
const secondRoundMatchLB = storage.select<any>("match", 19);
assert.equal(secondRoundMatchLB.opponent1.position, 4);
const secondRoundSecondMatchLB = storage.select<any>("match", 20);
assert.equal(secondRoundSecondMatchLB.opponent1.position, 3);
const secondRoundSecondMatchLB = storage.select<any>("match", 20);
assert.equal(secondRoundSecondMatchLB.opponent1.position, 3);
});
SeedingAndOrderingInElimination(
"should throw if round does not support ordering",
() => {
assert.throws(
() => manager.update.roundOrdering(6, "natural"), // LB Round 2
"This round does not support ordering.",
);
"should throw if round does not support ordering",
() => {
assert.throws(
() => manager.update.roundOrdering(6, "natural"), // LB Round 2
"This round does not support ordering.",
);
assert.throws(
() => manager.update.roundOrdering(9, "natural"), // LB Round 6 (last minor round)
"This round does not support ordering.",
);
},
assert.throws(
() => manager.update.roundOrdering(9, "natural"), // LB Round 6 (last minor round)
"This round does not support ordering.",
);
},
);
SeedingAndOrderingInElimination(
"should throw if at least one match is running or completed",
() => {
manager.update.match({
id: 0,
opponent1: { score: 1 },
});
"should throw if at least one match is running or completed",
() => {
manager.update.match({
id: 0,
opponent1: { score: 1 },
});
assert.throws(
() => manager.update.roundOrdering(0, "natural"),
"At least one match has started or is completed.",
);
assert.throws(
() => manager.update.roundOrdering(0, "natural"),
"At least one match has started or is completed.",
);
manager.update.match({
id: 0,
opponent1: { result: "win" },
});
manager.update.match({
id: 0,
opponent1: { result: "win" },
});
assert.throws(
() => manager.update.roundOrdering(0, "natural"),
"At least one match has started or is completed.",
);
},
assert.throws(
() => manager.update.roundOrdering(0, "natural"),
"At least one match has started or is completed.",
);
},
);
SeedingAndOrderingInElimination(
"should update all the ordering of a stage at once",
() => {
manager.update.ordering(0, [
"pair_flip",
"half_shift",
"reverse",
"natural",
]);
"should update all the ordering of a stage at once",
() => {
manager.update.ordering(0, [
"pair_flip",
"half_shift",
"reverse",
"natural",
]);
const firstRoundMatchWB = storage.select<any>("match", 0);
assert.equal(firstRoundMatchWB.opponent1.position, 2);
assert.equal(firstRoundMatchWB.opponent2.position, 1);
const firstRoundMatchWB = storage.select<any>("match", 0);
assert.equal(firstRoundMatchWB.opponent1.position, 2);
assert.equal(firstRoundMatchWB.opponent2.position, 1);
const firstRoundMatchLB = storage.select<any>("match", 15);
assert.equal(firstRoundMatchLB.opponent1.position, 5);
assert.equal(firstRoundMatchLB.opponent2.position, 6);
const firstRoundMatchLB = storage.select<any>("match", 15);
assert.equal(firstRoundMatchLB.opponent1.position, 5);
assert.equal(firstRoundMatchLB.opponent2.position, 6);
const secondRoundMatchLB = storage.select<any>("match", 19);
assert.equal(secondRoundMatchLB.opponent1.position, 4);
const secondRoundMatchLB = storage.select<any>("match", 19);
assert.equal(secondRoundMatchLB.opponent1.position, 4);
const secondRoundSecondMatchLB = storage.select<any>("match", 20);
assert.equal(secondRoundSecondMatchLB.opponent1.position, 3);
const secondRoundSecondMatchLB = storage.select<any>("match", 20);
assert.equal(secondRoundSecondMatchLB.opponent1.position, 3);
const fourthRoundMatchLB = storage.select<any>("match", 25);
assert.equal(fourthRoundMatchLB.opponent1.position, 1);
const fourthRoundMatchLB = storage.select<any>("match", 25);
assert.equal(fourthRoundMatchLB.opponent1.position, 1);
const finalRoundMatchLB = storage.select<any>("match", 28);
assert.equal(finalRoundMatchLB.opponent1.position, 1);
},
const finalRoundMatchLB = storage.select<any>("match", 28);
assert.equal(finalRoundMatchLB.opponent1.position, 1);
},
);
const ResetMatchAndMatchGames = suite("Reset match and match games");
ResetMatchAndMatchGames.before.each(() => {
storage.reset();
storage.reset();
});
ResetMatchAndMatchGames("should reset results of a match", () => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2],
settings: {
seedOrdering: ["natural"],
size: 8,
},
});
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2],
settings: {
seedOrdering: ["natural"],
size: 8,
},
});
manager.update.match({
id: 0,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 0,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
let match = storage.select<any>("match", 0);
assert.equal(match.opponent1.score, 16);
assert.equal(match.opponent2.score, 12);
assert.equal(match.opponent1.result, "win");
let match = storage.select<any>("match", 0);
assert.equal(match.opponent1.score, 16);
assert.equal(match.opponent2.score, 12);
assert.equal(match.opponent1.result, "win");
let semi1 = storage.select<any>("match", 4);
assert.equal(semi1.opponent1.result, "win");
assert.equal(semi1.opponent2, null);
let semi1 = storage.select<any>("match", 4);
assert.equal(semi1.opponent1.result, "win");
assert.equal(semi1.opponent2, null);
let final = storage.select<any>("match", 6);
assert.equal(final.opponent1.result, "win");
assert.equal(final.opponent2, null);
let final = storage.select<any>("match", 6);
assert.equal(final.opponent1.result, "win");
assert.equal(final.opponent2, null);
manager.reset.matchResults(0); // Score stays as is.
manager.reset.matchResults(0); // Score stays as is.
match = storage.select<any>("match", 0);
assert.equal(match.opponent1.score, 16);
assert.equal(match.opponent2.score, 12);
assert.equal(match.opponent1.result, undefined);
match = storage.select<any>("match", 0);
assert.equal(match.opponent1.score, 16);
assert.equal(match.opponent2.score, 12);
assert.equal(match.opponent1.result, undefined);
semi1 = storage.select<any>("match", 4);
assert.equal(semi1.opponent1.result, undefined);
assert.equal(semi1.opponent2, null);
semi1 = storage.select<any>("match", 4);
assert.equal(semi1.opponent1.result, undefined);
assert.equal(semi1.opponent2, null);
final = storage.select<any>("match", 6);
assert.equal(final.opponent1.result, undefined);
assert.equal(final.opponent2, null);
final = storage.select<any>("match", 6);
assert.equal(final.opponent1.result, undefined);
assert.equal(final.opponent2, null);
});
ResetMatchAndMatchGames(
"should throw when at least one of the following match is locked",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
settings: {
seedOrdering: ["natural"],
},
});
"should throw when at least one of the following match is locked",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
settings: {
seedOrdering: ["natural"],
},
});
manager.update.match({
id: 0,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 0,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 1,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 1,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 2,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 2,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
assert.throws(() => manager.reset.matchResults(0), "The match is locked.");
},
assert.throws(() => manager.reset.matchResults(0), "The match is locked.");
},
);
const ImportExport = suite("Import / export");
ImportExport.before.each(() => {
storage.reset();
storage.reset();
});
ImportExport("should import data in the storage", () => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
settings: {
seedOrdering: ["natural"],
},
});
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
settings: {
seedOrdering: ["natural"],
},
});
const initialData = manager.get.stageData(0);
const initialData = manager.get.stageData(0);
manager.update.match({
id: 0,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 0,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 1,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 1,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 2,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 2,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
assert.equal(storage.select<any>("match", 0).opponent1.result, "win");
assert.equal(storage.select<any>("match", 1).opponent1.result, "win");
assert.equal(storage.select<any>("match", 0).opponent1.result, "win");
assert.equal(storage.select<any>("match", 1).opponent1.result, "win");
manager.import(initialData);
manager.import(initialData);
assert.equal(storage.select<any>("match", 0).opponent1.result, undefined);
assert.equal(storage.select<any>("match", 1).opponent1.result, undefined);
assert.equal(storage.select<any>("match", 0).opponent1.result, undefined);
assert.equal(storage.select<any>("match", 1).opponent1.result, undefined);
});
ImportExport("should export data from the storage", () => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
settings: {
seedOrdering: ["natural"],
},
});
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
settings: {
seedOrdering: ["natural"],
},
});
const data = manager.export();
const data = manager.export();
for (const key of ["stage", "group", "round", "match"]) {
assert.ok(Object.keys(data).includes(key));
}
for (const key of ["stage", "group", "round", "match"]) {
assert.ok(Object.keys(data).includes(key));
}
assert.equal(storage.select<any>("stage"), data.stage);
assert.equal(storage.select<any>("group"), data.group);
assert.equal(storage.select<any>("round"), data.round);
assert.equal(storage.select<any>("match"), data.match);
assert.equal(storage.select<any>("stage"), data.stage);
assert.equal(storage.select<any>("group"), data.group);
assert.equal(storage.select<any>("round"), data.round);
assert.equal(storage.select<any>("match"), data.match);
});
BYEHandling.run();

View File

@@ -1,7 +1,7 @@
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
import { suite } from "uvu";
import * as assert from "uvu/assert";
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
const storage = new InMemoryDatabase();
const manager = new BracketsManager(storage);
@@ -9,84 +9,84 @@ const manager = new BracketsManager(storage);
const GetSeeding = suite("Get seeding");
GetSeeding("should get the seeding of a round-robin stage", () => {
storage.reset();
storage.reset();
manager.create({
name: "Example",
tournamentId: 0,
type: "round_robin",
settings: {
groupCount: 8,
size: 32,
seedOrdering: ["groups.seed_optimized"],
},
});
manager.create({
name: "Example",
tournamentId: 0,
type: "round_robin",
settings: {
groupCount: 8,
size: 32,
seedOrdering: ["groups.seed_optimized"],
},
});
const seeding = manager.get.seeding(0);
assert.equal(seeding.length, 32);
assert.equal(seeding[0]!.position, 1);
assert.equal(seeding[1]!.position, 2);
const seeding = manager.get.seeding(0);
assert.equal(seeding.length, 32);
assert.equal(seeding[0]!.position, 1);
assert.equal(seeding[1]!.position, 2);
});
GetSeeding("should get the seeding of a round-robin stage with BYEs", () => {
storage.reset();
storage.reset();
manager.create({
name: "Example",
tournamentId: 0,
type: "round_robin",
settings: {
groupCount: 2,
size: 8,
},
seeding: [1, null, null, null, null, null, null, null],
});
manager.create({
name: "Example",
tournamentId: 0,
type: "round_robin",
settings: {
groupCount: 2,
size: 8,
},
seeding: [1, null, null, null, null, null, null, null],
});
const seeding = manager.get.seeding(0);
assert.equal(seeding.length, 8);
const seeding = manager.get.seeding(0);
assert.equal(seeding.length, 8);
});
GetSeeding("should get the seeding of a single elimination stage", () => {
storage.reset();
storage.reset();
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
settings: { size: 16 },
});
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
settings: { size: 16 },
});
const seeding = manager.get.seeding(0);
assert.equal(seeding.length, 16);
assert.equal(seeding[0]!.position, 1);
assert.equal(seeding[1]!.position, 2);
const seeding = manager.get.seeding(0);
assert.equal(seeding.length, 16);
assert.equal(seeding[0]!.position, 1);
assert.equal(seeding[1]!.position, 2);
});
GetSeeding("should get the seeding with BYEs", () => {
storage.reset();
storage.reset();
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, null, 2, 3, 4, null, null, 5],
settings: {
seedOrdering: ["inner_outer"],
},
});
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, null, 2, 3, 4, null, null, 5],
settings: {
seedOrdering: ["inner_outer"],
},
});
const seeding = manager.get.seeding(0);
assert.equal(seeding.length, 8);
assert.equal(seeding, [
{ id: 1, position: 1 },
null,
{ id: 2, position: 3 },
{ id: 3, position: 4 },
{ id: 4, position: 5 },
null,
null,
{ id: 5, position: 8 },
]);
const seeding = manager.get.seeding(0);
assert.equal(seeding.length, 8);
assert.equal(seeding, [
{ id: 1, position: 1 },
null,
{ id: 2, position: 3 },
{ id: 3, position: 4 },
{ id: 4, position: 5 },
null,
null,
{ id: 5, position: 8 },
]);
});
GetSeeding.run();

View File

@@ -1,309 +1,309 @@
import { suite } from "uvu";
import * as assert from "uvu/assert";
import {
assertRoundRobin,
balanceByes,
makeGroups,
makeRoundRobinMatches,
assertRoundRobin,
balanceByes,
makeGroups,
makeRoundRobinMatches,
} from "../helpers";
import { ordering } from "../ordering";
const RoundRobinGroups = suite("Round-robin groups");
RoundRobinGroups("should place participants in groups", () => {
assert.equal(makeGroups([1, 2, 3, 4, 5], 2), [
[1, 2, 3],
[4, 5],
]);
assert.equal(makeGroups([1, 2, 3, 4, 5, 6, 7, 8], 2), [
[1, 2, 3, 4],
[5, 6, 7, 8],
]);
assert.equal(makeGroups([1, 2, 3, 4, 5, 6, 7, 8], 3), [
[1, 2, 3],
[4, 5, 6],
[7, 8],
]);
assert.equal(makeGroups([1, 2, 3, 4, 5], 2), [
[1, 2, 3],
[4, 5],
]);
assert.equal(makeGroups([1, 2, 3, 4, 5, 6, 7, 8], 2), [
[1, 2, 3, 4],
[5, 6, 7, 8],
]);
assert.equal(makeGroups([1, 2, 3, 4, 5, 6, 7, 8], 3), [
[1, 2, 3],
[4, 5, 6],
[7, 8],
]);
});
RoundRobinGroups("should make the rounds for a round-robin group", () => {
assertRoundRobin([1, 2, 3], makeRoundRobinMatches([1, 2, 3]));
assertRoundRobin([1, 2, 3, 4], makeRoundRobinMatches([1, 2, 3, 4]));
assertRoundRobin([1, 2, 3, 4, 5], makeRoundRobinMatches([1, 2, 3, 4, 5]));
assertRoundRobin(
[1, 2, 3, 4, 5, 6],
makeRoundRobinMatches([1, 2, 3, 4, 5, 6]),
);
assertRoundRobin([1, 2, 3], makeRoundRobinMatches([1, 2, 3]));
assertRoundRobin([1, 2, 3, 4], makeRoundRobinMatches([1, 2, 3, 4]));
assertRoundRobin([1, 2, 3, 4, 5], makeRoundRobinMatches([1, 2, 3, 4, 5]));
assertRoundRobin(
[1, 2, 3, 4, 5, 6],
makeRoundRobinMatches([1, 2, 3, 4, 5, 6]),
);
});
const SeedOrderingMethods = suite("Seed ordering methods");
SeedOrderingMethods(
"should place 2 participants with inner-outer method",
() => {
const teams = [1, 2]; // This is the minimum participant count supported by the library.
const placement = ordering["inner_outer"](teams);
assert.equal(placement, [1, 2]);
},
"should place 2 participants with inner-outer method",
() => {
const teams = [1, 2]; // This is the minimum participant count supported by the library.
const placement = ordering.inner_outer(teams);
assert.equal(placement, [1, 2]);
},
);
SeedOrderingMethods(
"should place 4 participants with inner-outer method",
() => {
const teams = [1, 2, 3, 4];
const placement = ordering["inner_outer"](teams);
assert.equal(placement, [1, 4, 2, 3]);
},
"should place 4 participants with inner-outer method",
() => {
const teams = [1, 2, 3, 4];
const placement = ordering.inner_outer(teams);
assert.equal(placement, [1, 4, 2, 3]);
},
);
SeedOrderingMethods(
"should place 8 participants with inner-outer method",
() => {
const teams = [1, 2, 3, 4, 5, 6, 7, 8];
const placement = ordering["inner_outer"](teams);
assert.equal(placement, [1, 8, 4, 5, 2, 7, 3, 6]);
},
"should place 8 participants with inner-outer method",
() => {
const teams = [1, 2, 3, 4, 5, 6, 7, 8];
const placement = ordering.inner_outer(teams);
assert.equal(placement, [1, 8, 4, 5, 2, 7, 3, 6]);
},
);
SeedOrderingMethods(
"should place 16 participants with inner-outer method",
() => {
const teams = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
const placement = ordering["inner_outer"](teams);
assert.equal(
placement,
[1, 16, 8, 9, 4, 13, 5, 12, 2, 15, 7, 10, 3, 14, 6, 11],
);
},
"should place 16 participants with inner-outer method",
() => {
const teams = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16];
const placement = ordering.inner_outer(teams);
assert.equal(
placement,
[1, 16, 8, 9, 4, 13, 5, 12, 2, 15, 7, 10, 3, 14, 6, 11],
);
},
);
SeedOrderingMethods("should make a natural ordering", () => {
assert.equal(
ordering["natural"]([1, 2, 3, 4, 5, 6, 7, 8]),
[1, 2, 3, 4, 5, 6, 7, 8],
);
assert.equal(
ordering.natural([1, 2, 3, 4, 5, 6, 7, 8]),
[1, 2, 3, 4, 5, 6, 7, 8],
);
});
SeedOrderingMethods("should make a reverse ordering", () => {
assert.equal(
ordering["reverse"]([1, 2, 3, 4, 5, 6, 7, 8]),
[8, 7, 6, 5, 4, 3, 2, 1],
);
assert.equal(
ordering.reverse([1, 2, 3, 4, 5, 6, 7, 8]),
[8, 7, 6, 5, 4, 3, 2, 1],
);
});
SeedOrderingMethods("should make a half shift ordering", () => {
assert.equal(
ordering["half_shift"]([1, 2, 3, 4, 5, 6, 7, 8]),
[5, 6, 7, 8, 1, 2, 3, 4],
);
assert.equal(
ordering.half_shift([1, 2, 3, 4, 5, 6, 7, 8]),
[5, 6, 7, 8, 1, 2, 3, 4],
);
});
SeedOrderingMethods("should make a reverse half shift ordering", () => {
assert.equal(
ordering["reverse_half_shift"]([1, 2, 3, 4, 5, 6, 7, 8]),
[4, 3, 2, 1, 8, 7, 6, 5],
);
assert.equal(
ordering.reverse_half_shift([1, 2, 3, 4, 5, 6, 7, 8]),
[4, 3, 2, 1, 8, 7, 6, 5],
);
});
SeedOrderingMethods("should make a pair flip ordering", () => {
assert.equal(
ordering["pair_flip"]([1, 2, 3, 4, 5, 6, 7, 8]),
[2, 1, 4, 3, 6, 5, 8, 7],
);
assert.equal(
ordering.pair_flip([1, 2, 3, 4, 5, 6, 7, 8]),
[2, 1, 4, 3, 6, 5, 8, 7],
);
});
SeedOrderingMethods(
"should make an effort balanced ordering for groups",
() => {
assert.equal(
ordering["groups.effort_balanced"]([1, 2, 3, 4, 5, 6, 7, 8], 4),
[
1,
5, // 1st group
2,
6, // 2nd group
3,
7, // 3rd group
4,
8, // 4th group
],
);
"should make an effort balanced ordering for groups",
() => {
assert.equal(
ordering["groups.effort_balanced"]([1, 2, 3, 4, 5, 6, 7, 8], 4),
[
1,
5, // 1st group
2,
6, // 2nd group
3,
7, // 3rd group
4,
8, // 4th group
],
);
assert.equal(
ordering["groups.effort_balanced"](
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
4,
),
[
1,
5,
9,
13, // 1st group
2,
6,
10,
14, // 2nd group
3,
7,
11,
15, // 3rd group
4,
8,
12,
16, // 4th group
],
);
assert.equal(
ordering["groups.effort_balanced"](
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
4,
),
[
1,
5,
9,
13, // 1st group
2,
6,
10,
14, // 2nd group
3,
7,
11,
15, // 3rd group
4,
8,
12,
16, // 4th group
],
);
assert.equal(
ordering["groups.effort_balanced"]([1, 2, 3, 4, 5, 6, 7, 8], 2),
[
1,
3,
5,
7, // 1st group
2,
4,
6,
8, // 2nd group
],
);
},
assert.equal(
ordering["groups.effort_balanced"]([1, 2, 3, 4, 5, 6, 7, 8], 2),
[
1,
3,
5,
7, // 1st group
2,
4,
6,
8, // 2nd group
],
);
},
);
SeedOrderingMethods("should make a snake ordering for groups", () => {
assert.equal(ordering["groups.seed_optimized"]([1, 2, 3, 4, 5, 6, 7, 8], 4), [
1,
8, // 1st group
2,
7, // 2nd group
3,
6, // 3rd group
4,
5, // 4th group
]);
assert.equal(ordering["groups.seed_optimized"]([1, 2, 3, 4, 5, 6, 7, 8], 4), [
1,
8, // 1st group
2,
7, // 2nd group
3,
6, // 3rd group
4,
5, // 4th group
]);
assert.equal(
ordering["groups.seed_optimized"](
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
4,
),
[
1,
8,
9,
16, // 1st group
2,
7,
10,
15, // 2nd group
3,
6,
11,
14, // 3rd group
4,
5,
12,
13, // 4th group
],
);
assert.equal(
ordering["groups.seed_optimized"](
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
4,
),
[
1,
8,
9,
16, // 1st group
2,
7,
10,
15, // 2nd group
3,
6,
11,
14, // 3rd group
4,
5,
12,
13, // 4th group
],
);
assert.equal(ordering["groups.seed_optimized"]([1, 2, 3, 4, 5, 6, 7, 8], 2), [
1,
4,
5,
8, // 1st group
2,
3,
6,
7, // 2nd group
]);
assert.equal(ordering["groups.seed_optimized"]([1, 2, 3, 4, 5, 6, 7, 8], 2), [
1,
4,
5,
8, // 1st group
2,
3,
6,
7, // 2nd group
]);
});
const BalanceByes = suite("Balance BYEs");
BalanceByes("should ignore input BYEs in the seeding", () => {
assert.equal(
balanceByes(
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, null, null, null, null],
16,
),
balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 16),
);
assert.equal(
balanceByes(
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, null, null, null, null],
16,
),
balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 16),
);
assert.equal(
balanceByes(
[1, 2, 3, null, 4, 5, 6, 7, 8, null, 9, 10, null, 11, null, 12, null],
16,
),
balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 16),
);
assert.equal(
balanceByes(
[1, 2, 3, null, 4, 5, 6, 7, 8, null, 9, 10, null, 11, null, 12, null],
16,
),
balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 16),
);
});
BalanceByes(
"should take the target size as an argument or calculate it",
() => {
assert.equal(
balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 16),
balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
);
},
"should take the target size as an argument or calculate it",
() => {
assert.equal(
balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], 16),
balanceByes([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]),
);
},
);
BalanceByes("should prefer matches with only one BYE", () => {
assert.equal(
balanceByes([
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
null,
null,
null,
null,
]),
[1, 2, 3, 4, 5, 6, 7, 8, 9, null, 10, null, 11, null, 12, null],
);
assert.equal(
balanceByes([
1,
2,
3,
4,
5,
6,
7,
8,
9,
10,
11,
12,
null,
null,
null,
null,
]),
[1, 2, 3, 4, 5, 6, 7, 8, 9, null, 10, null, 11, null, 12, null],
);
assert.equal(
balanceByes(
[1, 2, 3, 4, 5, 6, 7, 8, null, null, null, null, null, null, null, null],
16,
),
[1, null, 2, null, 3, null, 4, null, 5, null, 6, null, 7, null, 8, null],
);
assert.equal(
balanceByes(
[1, 2, 3, 4, 5, 6, 7, 8, null, null, null, null, null, null, null, null],
16,
),
[1, null, 2, null, 3, null, 4, null, 5, null, 6, null, 7, null, 8, null],
);
assert.equal(
balanceByes(
[
1,
2,
3,
4,
5,
6,
7,
null,
null,
null,
null,
null,
null,
null,
null,
null,
],
16,
),
[1, null, 2, null, 3, null, 4, null, 5, null, 6, null, 7, null, null, null],
);
assert.equal(
balanceByes(
[
1,
2,
3,
4,
5,
6,
7,
null,
null,
null,
null,
null,
null,
null,
null,
null,
],
16,
),
[1, null, 2, null, 3, null, 4, null, 5, null, 6, null, 7, null, null, null],
);
});
RoundRobinGroups.run();

View File

@@ -1,7 +1,7 @@
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
import { suite } from "uvu";
import * as assert from "uvu/assert";
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
const storage = new InMemoryDatabase();
const manager = new BracketsManager(storage);
@@ -9,199 +9,199 @@ const manager = new BracketsManager(storage);
const CreateRoundRobinStage = suite("Create a round-robin stage");
CreateRoundRobinStage.before.each(() => {
storage.reset();
storage.reset();
});
CreateRoundRobinStage("should create a round-robin stage", () => {
const example = {
name: "Example",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: { groupCount: 2 },
} as any;
const example = {
name: "Example",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: { groupCount: 2 },
} as any;
manager.create(example);
manager.create(example);
const stage = storage.select<any>("stage", 0)!;
assert.equal(stage.name, example.name);
assert.equal(stage.type, example.type);
const stage = storage.select<any>("stage", 0)!;
assert.equal(stage.name, example.name);
assert.equal(stage.type, example.type);
assert.equal(storage.select("group")!.length, 2);
assert.equal(storage.select("round")!.length, 6);
assert.equal(storage.select("match")!.length, 12);
assert.equal(storage.select("group")!.length, 2);
assert.equal(storage.select("round")!.length, 6);
assert.equal(storage.select("match")!.length, 12);
});
CreateRoundRobinStage(
"should create a round-robin stage with a manual seeding",
() => {
const example = {
name: "Example",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
groupCount: 2,
manualOrdering: [
[1, 4, 6, 7],
[2, 3, 5, 8],
],
},
} as any;
"should create a round-robin stage with a manual seeding",
() => {
const example = {
name: "Example",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
groupCount: 2,
manualOrdering: [
[1, 4, 6, 7],
[2, 3, 5, 8],
],
},
} as any;
manager.create(example);
manager.create(example);
for (let groupIndex = 0; groupIndex < 2; groupIndex++) {
const matches = storage.select<any>("match", { group_id: groupIndex })!;
const participants = [
matches[0].opponent1.position,
matches[1].opponent2.position,
matches[1].opponent1.position,
matches[0].opponent2.position,
];
for (let groupIndex = 0; groupIndex < 2; groupIndex++) {
const matches = storage.select<any>("match", { group_id: groupIndex })!;
const participants = [
matches[0].opponent1.position,
matches[1].opponent2.position,
matches[1].opponent1.position,
matches[0].opponent2.position,
];
assert.equal(participants, example.settings.manualOrdering[groupIndex]);
}
},
assert.equal(participants, example.settings.manualOrdering[groupIndex]);
}
},
);
CreateRoundRobinStage(
"should throw if manual ordering has invalid counts",
() => {
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
groupCount: 2,
manualOrdering: [[1, 4, 6, 7]],
},
}),
"Group count in the manual ordering does not correspond to the given group count.",
);
"should throw if manual ordering has invalid counts",
() => {
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
groupCount: 2,
manualOrdering: [[1, 4, 6, 7]],
},
}),
"Group count in the manual ordering does not correspond to the given group count.",
);
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
groupCount: 2,
manualOrdering: [
[1, 4],
[2, 3],
],
},
}),
"Not enough seeds in at least one group of the manual ordering.",
);
},
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
groupCount: 2,
manualOrdering: [
[1, 4],
[2, 3],
],
},
}),
"Not enough seeds in at least one group of the manual ordering.",
);
},
);
CreateRoundRobinStage(
"should create a round-robin stage without BYE vs. BYE matches",
() => {
const example = {
name: "Example",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4, 5, null, null, null],
settings: { groupCount: 2 },
} as any;
"should create a round-robin stage without BYE vs. BYE matches",
() => {
const example = {
name: "Example",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4, 5, null, null, null],
settings: { groupCount: 2 },
} as any;
manager.create(example);
manager.create(example);
// One match must be missing.
assert.equal(storage.select("match")!.length, 11);
},
// One match must be missing.
assert.equal(storage.select("match")!.length, 11);
},
);
CreateRoundRobinStage(
"should create a round-robin stage with to be determined participants",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "round_robin",
settings: {
groupCount: 4,
size: 16,
},
});
"should create a round-robin stage with to be determined participants",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "round_robin",
settings: {
groupCount: 4,
size: 16,
},
});
assert.equal(storage.select("group")!.length, 4);
assert.equal(storage.select("round")!.length, 4 * 3);
assert.equal(storage.select("match")!.length, 4 * 3 * 2);
},
assert.equal(storage.select("group")!.length, 4);
assert.equal(storage.select("round")!.length, 4 * 3);
assert.equal(storage.select("match")!.length, 4 * 3 * 2);
},
);
CreateRoundRobinStage(
"should create a round-robin stage with effort balanced",
() => {
manager.create({
name: "Example with effort balanced",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
groupCount: 2,
seedOrdering: ["groups.seed_optimized"],
},
});
"should create a round-robin stage with effort balanced",
() => {
manager.create({
name: "Example with effort balanced",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: {
groupCount: 2,
seedOrdering: ["groups.seed_optimized"],
},
});
assert.equal(storage.select<any>("match", 0).opponent1.id, 1);
assert.equal(storage.select<any>("match", 0).opponent2.id, 8);
},
assert.equal(storage.select<any>("match", 0).opponent1.id, 1);
assert.equal(storage.select<any>("match", 0).opponent2.id, 8);
},
);
CreateRoundRobinStage("should throw if no group count given", () => {
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "round_robin",
}),
"You must specify a group count for round-robin stages.",
);
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "round_robin",
}),
"You must specify a group count for round-robin stages.",
);
});
CreateRoundRobinStage(
"should throw if the group count is not strictly positive",
() => {
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "round_robin",
settings: {
groupCount: 0,
size: 4,
seedOrdering: ["groups.seed_optimized"],
},
}),
"You must provide a strictly positive group count.",
);
},
"should throw if the group count is not strictly positive",
() => {
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "round_robin",
settings: {
groupCount: 0,
size: 4,
seedOrdering: ["groups.seed_optimized"],
},
}),
"You must provide a strictly positive group count.",
);
},
);
const UpdateRoundRobinScores = suite("Update scores in a round-robin stage");
UpdateRoundRobinScores.before.each(() => {
storage.reset();
manager.create({
name: "Example scores",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4],
settings: { groupCount: 1 },
});
storage.reset();
manager.create({
name: "Example scores",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4],
settings: { groupCount: 1 },
});
});
const ExampleUseCase = suite("Example use-case");
@@ -210,52 +210,52 @@ const ExampleUseCase = suite("Example use-case");
// https://organizer.toornament.com/tournaments/3359823657332629504/stages/3359826493568360448/groups/3359826494507884609/result
ExampleUseCase.before.each(() => {
storage.reset();
manager.create({
name: "Example scores",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4],
settings: { groupCount: 1 },
});
storage.reset();
manager.create({
name: "Example scores",
tournamentId: 0,
type: "round_robin",
seeding: [1, 2, 3, 4],
settings: { groupCount: 1 },
});
});
ExampleUseCase("should set all the scores", () => {
manager.update.match({
id: 0,
opponent1: { score: 16, result: "win" }, // POCEBLO
opponent2: { score: 9 }, // AQUELLEHEURE?!
});
manager.update.match({
id: 0,
opponent1: { score: 16, result: "win" }, // POCEBLO
opponent2: { score: 9 }, // AQUELLEHEURE?!
});
manager.update.match({
id: 1,
opponent1: { score: 3 }, // Ballec Squad
opponent2: { score: 16, result: "win" }, // twitch.tv/mrs_fly
});
manager.update.match({
id: 1,
opponent1: { score: 3 }, // Ballec Squad
opponent2: { score: 16, result: "win" }, // twitch.tv/mrs_fly
});
manager.update.match({
id: 2,
opponent1: { score: 16, result: "win" }, // twitch.tv/mrs_fly
opponent2: { score: 0 }, // AQUELLEHEURE?!
});
manager.update.match({
id: 2,
opponent1: { score: 16, result: "win" }, // twitch.tv/mrs_fly
opponent2: { score: 0 }, // AQUELLEHEURE?!
});
manager.update.match({
id: 3,
opponent1: { score: 16, result: "win" }, // POCEBLO
opponent2: { score: 2 }, // Ballec Squad
});
manager.update.match({
id: 3,
opponent1: { score: 16, result: "win" }, // POCEBLO
opponent2: { score: 2 }, // Ballec Squad
});
manager.update.match({
id: 4,
opponent1: { score: 16, result: "win" }, // Ballec Squad
opponent2: { score: 12 }, // AQUELLEHEURE?!
});
manager.update.match({
id: 4,
opponent1: { score: 16, result: "win" }, // Ballec Squad
opponent2: { score: 12 }, // AQUELLEHEURE?!
});
manager.update.match({
id: 5,
opponent1: { score: 4 }, // twitch.tv/mrs_fly
opponent2: { score: 16, result: "win" }, // POCEBLO
});
manager.update.match({
id: 5,
opponent1: { score: 4 }, // twitch.tv/mrs_fly
opponent2: { score: 16, result: "win" }, // POCEBLO
});
});
CreateRoundRobinStage.run();

View File

@@ -1,8 +1,8 @@
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
import { suite } from "uvu";
import * as assert from "uvu/assert";
import { Status } from "~/db/types";
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
const storage = new InMemoryDatabase();
const manager = new BracketsManager(storage);
@@ -10,332 +10,332 @@ const manager = new BracketsManager(storage);
const CreateSingleEliminationStage = suite("Create single elimination stage");
CreateSingleEliminationStage.before.each(() => {
storage.reset();
storage.reset();
});
CreateSingleEliminationStage("should create a single elimination stage", () => {
const example = {
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: { seedOrdering: ["natural"] },
} as any;
const example = {
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: { seedOrdering: ["natural"] },
} as any;
manager.create(example);
manager.create(example);
const stage = storage.select<any>("stage", 0);
assert.equal(stage.name, example.name);
assert.equal(stage.type, example.type);
const stage = storage.select<any>("stage", 0);
assert.equal(stage.name, example.name);
assert.equal(stage.type, example.type);
assert.equal(storage.select<any>("group")!.length, 1);
assert.equal(storage.select<any>("round")!.length, 4);
assert.equal(storage.select<any>("match")!.length, 15);
assert.equal(storage.select<any>("group")!.length, 1);
assert.equal(storage.select<any>("round")!.length, 4);
assert.equal(storage.select<any>("match")!.length, 15);
});
CreateSingleEliminationStage(
"should create a single elimination stage with BYEs",
() => {
manager.create({
name: "Example with BYEs",
tournamentId: 0,
type: "single_elimination",
seeding: [1, null, 3, 4, null, null, 7, 8],
settings: { seedOrdering: ["natural"] },
});
"should create a single elimination stage with BYEs",
() => {
manager.create({
name: "Example with BYEs",
tournamentId: 0,
type: "single_elimination",
seeding: [1, null, 3, 4, null, null, 7, 8],
settings: { seedOrdering: ["natural"] },
});
assert.equal(storage.select<any>("match", 4).opponent1.id, 1); // Determined because of opponent's BYE.
assert.equal(storage.select<any>("match", 4).opponent2.id, null); // To be determined.
assert.equal(storage.select<any>("match", 5).opponent1, null); // BYE propagated.
assert.equal(storage.select<any>("match", 5).opponent2.id, null); // To be determined.
},
assert.equal(storage.select<any>("match", 4).opponent1.id, 1); // Determined because of opponent's BYE.
assert.equal(storage.select<any>("match", 4).opponent2.id, null); // To be determined.
assert.equal(storage.select<any>("match", 5).opponent1, null); // BYE propagated.
assert.equal(storage.select<any>("match", 5).opponent2.id, null); // To be determined.
},
);
CreateSingleEliminationStage(
"should create a single elimination stage with consolation final",
() => {
manager.create({
name: "Example with consolation final",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: { consolationFinal: true, seedOrdering: ["natural"] },
});
"should create a single elimination stage with consolation final",
() => {
manager.create({
name: "Example with consolation final",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: { consolationFinal: true, seedOrdering: ["natural"] },
});
assert.equal(storage.select<any>("group")!.length, 2);
assert.equal(storage.select<any>("round")!.length, 4);
assert.equal(storage.select<any>("match")!.length, 8);
},
assert.equal(storage.select<any>("group")!.length, 2);
assert.equal(storage.select<any>("round")!.length, 4);
assert.equal(storage.select<any>("match")!.length, 8);
},
);
CreateSingleEliminationStage(
"should create a single elimination stage with consolation final and BYEs",
() => {
manager.create({
name: "Example with consolation final and BYEs",
tournamentId: 0,
type: "single_elimination",
seeding: [null, null, null, 4, 5, 6, 7, 8],
settings: { consolationFinal: true, seedOrdering: ["natural"] },
});
"should create a single elimination stage with consolation final and BYEs",
() => {
manager.create({
name: "Example with consolation final and BYEs",
tournamentId: 0,
type: "single_elimination",
seeding: [null, null, null, 4, 5, 6, 7, 8],
settings: { consolationFinal: true, seedOrdering: ["natural"] },
});
assert.equal(storage.select<any>("match", 4).opponent1, null);
assert.equal(storage.select<any>("match", 4).opponent2.id, 4);
assert.equal(storage.select<any>("match", 4).opponent1, null);
assert.equal(storage.select<any>("match", 4).opponent2.id, 4);
// Consolation final
assert.equal(storage.select<any>("match", 7).opponent1, null);
assert.equal(storage.select<any>("match", 7).opponent2.id, null);
},
// Consolation final
assert.equal(storage.select<any>("match", 7).opponent1, null);
assert.equal(storage.select<any>("match", 7).opponent2.id, null);
},
);
CreateSingleEliminationStage(
"should create a single elimination stage with Bo3 matches",
() => {
manager.create({
name: "Example with Bo3 matches",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: { seedOrdering: ["natural"] },
});
"should create a single elimination stage with Bo3 matches",
() => {
manager.create({
name: "Example with Bo3 matches",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8],
settings: { seedOrdering: ["natural"] },
});
assert.equal(storage.select<any>("group")!.length, 1);
assert.equal(storage.select<any>("round")!.length, 3);
assert.equal(storage.select<any>("match")!.length, 7);
},
assert.equal(storage.select<any>("group")!.length, 1);
assert.equal(storage.select<any>("round")!.length, 3);
assert.equal(storage.select<any>("match")!.length, 7);
},
);
CreateSingleEliminationStage(
"should determine the number property of created stages",
() => {
manager.create({
name: "Stage 1",
tournamentId: 0,
type: "single_elimination",
settings: { size: 2 },
});
"should determine the number property of created stages",
() => {
manager.create({
name: "Stage 1",
tournamentId: 0,
type: "single_elimination",
settings: { size: 2 },
});
assert.equal(storage.select<any>("stage", 0).number, 1);
assert.equal(storage.select<any>("stage", 0).number, 1);
manager.create({
name: "Stage 2",
tournamentId: 0,
type: "single_elimination",
settings: { size: 2 },
});
manager.create({
name: "Stage 2",
tournamentId: 0,
type: "single_elimination",
settings: { size: 2 },
});
assert.equal(storage.select<any>("stage", 1).number, 2);
assert.equal(storage.select<any>("stage", 1).number, 2);
manager.delete.stage(0);
manager.delete.stage(0);
manager.create({
name: "Stage 3",
tournamentId: 0,
type: "single_elimination",
settings: { size: 2 },
});
manager.create({
name: "Stage 3",
tournamentId: 0,
type: "single_elimination",
settings: { size: 2 },
});
assert.equal(storage.select<any>("stage", 2).number, 3);
},
assert.equal(storage.select<any>("stage", 2).number, 3);
},
);
CreateSingleEliminationStage(
"should create a stage with the given number property",
() => {
manager.create({
name: "Stage 1",
tournamentId: 0,
type: "single_elimination",
settings: { size: 2 },
});
"should create a stage with the given number property",
() => {
manager.create({
name: "Stage 1",
tournamentId: 0,
type: "single_elimination",
settings: { size: 2 },
});
manager.create({
name: "Stage 2",
tournamentId: 0,
type: "single_elimination",
settings: { size: 2 },
});
manager.create({
name: "Stage 2",
tournamentId: 0,
type: "single_elimination",
settings: { size: 2 },
});
manager.delete.stage(0);
manager.delete.stage(0);
manager.create({
name: "Stage 1 (new)",
tournamentId: 0,
type: "single_elimination",
number: 1,
settings: { size: 2 },
});
manager.create({
name: "Stage 1 (new)",
tournamentId: 0,
type: "single_elimination",
number: 1,
settings: { size: 2 },
});
assert.equal(storage.select<any>("stage", 2).number, 1);
},
assert.equal(storage.select<any>("stage", 2).number, 1);
},
);
CreateSingleEliminationStage(
"should throw if the given number property already exists",
() => {
manager.create({
name: "Stage 1",
tournamentId: 0,
type: "single_elimination",
number: 1,
settings: { size: 2 },
});
"should throw if the given number property already exists",
() => {
manager.create({
name: "Stage 1",
tournamentId: 0,
type: "single_elimination",
number: 1,
settings: { size: 2 },
});
assert.throws(
() =>
manager.create({
name: "Stage 1",
tournamentId: 0,
type: "single_elimination",
number: 1, // Duplicate
settings: { size: 2 },
}),
"The given stage number already exists.",
);
},
assert.throws(
() =>
manager.create({
name: "Stage 1",
tournamentId: 0,
type: "single_elimination",
number: 1, // Duplicate
settings: { size: 2 },
}),
"The given stage number already exists.",
);
},
);
CreateSingleEliminationStage(
"should throw if the seeding has duplicate participants",
() => {
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [
1,
1, // Duplicate
3,
4,
],
}),
"The seeding has a duplicate participant.",
);
},
"should throw if the seeding has duplicate participants",
() => {
assert.throws(
() =>
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [
1,
1, // Duplicate
3,
4,
],
}),
"The seeding has a duplicate participant.",
);
},
);
CreateSingleEliminationStage(
"should throw if trying to set a draw as a result",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
});
"should throw if trying to set a draw as a result",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
});
assert.throws(
() =>
manager.update.match({
id: 0,
opponent1: { result: "draw" },
}),
"Having a draw is forbidden in an elimination tournament.",
);
},
assert.throws(
() =>
manager.update.match({
id: 0,
opponent1: { result: "draw" },
}),
"Having a draw is forbidden in an elimination tournament.",
);
},
);
const PreviousAndNextMatchUpdate = suite("Previous and next match update");
PreviousAndNextMatchUpdate.before.each(() => {
storage.reset();
storage.reset();
});
PreviousAndNextMatchUpdate(
"should determine matches in consolation final",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
settings: { consolationFinal: true },
});
"should determine matches in consolation final",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
settings: { consolationFinal: true },
});
manager.update.match({
id: 0, // First match of round 1
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 0, // First match of round 1
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 1, // Second match of round 1
opponent1: { score: 13 },
opponent2: { score: 16, result: "win" },
});
manager.update.match({
id: 1, // Second match of round 1
opponent1: { score: 13 },
opponent2: { score: 16, result: "win" },
});
assert.equal(
storage.select<any>("match", 3).opponent1.id, // Determined opponent for the consolation final
storage.select<any>("match", 0).opponent2.id, // Loser of Semi 1
);
assert.equal(
storage.select<any>("match", 3).opponent1.id, // Determined opponent for the consolation final
storage.select<any>("match", 0).opponent2.id, // Loser of Semi 1
);
assert.equal(
storage.select<any>("match", 3).opponent2.id, // Determined opponent for the consolation final
storage.select<any>("match", 1).opponent1.id, // Loser of Semi 2
);
assert.equal(
storage.select<any>("match", 3).opponent2.id, // Determined opponent for the consolation final
storage.select<any>("match", 1).opponent1.id, // Loser of Semi 2
);
assert.equal(storage.select<any>("match", 2).status, Status.Ready);
assert.equal(storage.select<any>("match", 3).status, Status.Ready);
},
assert.equal(storage.select<any>("match", 2).status, Status.Ready);
assert.equal(storage.select<any>("match", 3).status, Status.Ready);
},
);
PreviousAndNextMatchUpdate(
"should play both the final and consolation final in parallel",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
settings: { consolationFinal: true },
});
"should play both the final and consolation final in parallel",
() => {
manager.create({
name: "Example",
tournamentId: 0,
type: "single_elimination",
seeding: [1, 2, 3, 4],
settings: { consolationFinal: true },
});
manager.update.match({
id: 0, // First match of round 1
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 0, // First match of round 1
opponent1: { score: 16, result: "win" },
opponent2: { score: 12 },
});
manager.update.match({
id: 1, // Second match of round 1
opponent1: { score: 13 },
opponent2: { score: 16, result: "win" },
});
manager.update.match({
id: 1, // Second match of round 1
opponent1: { score: 13 },
opponent2: { score: 16, result: "win" },
});
manager.update.match({
id: 2, // Final
opponent1: { score: 12 },
opponent2: { score: 9 },
});
manager.update.match({
id: 2, // Final
opponent1: { score: 12 },
opponent2: { score: 9 },
});
assert.equal(storage.select<any>("match", 2).status, Status.Running);
assert.equal(storage.select<any>("match", 3).status, Status.Ready);
assert.equal(storage.select<any>("match", 2).status, Status.Running);
assert.equal(storage.select<any>("match", 3).status, Status.Ready);
manager.update.match({
id: 3, // Consolation final
opponent1: { score: 12 },
opponent2: { score: 9 },
});
manager.update.match({
id: 3, // Consolation final
opponent1: { score: 12 },
opponent2: { score: 9 },
});
assert.equal(storage.select<any>("match", 2).status, Status.Running);
assert.equal(storage.select<any>("match", 3).status, Status.Running);
assert.equal(storage.select<any>("match", 2).status, Status.Running);
assert.equal(storage.select<any>("match", 3).status, Status.Running);
manager.update.match({
id: 3, // Consolation final
opponent1: { score: 16, result: "win" },
opponent2: { score: 9 },
});
manager.update.match({
id: 3, // Consolation final
opponent1: { score: 16, result: "win" },
opponent2: { score: 9 },
});
assert.equal(storage.select<any>("match", 2).status, Status.Running);
assert.equal(storage.select<any>("match", 2).status, Status.Running);
manager.update.match({
id: 2, // Final
opponent1: { score: 16, result: "win" },
opponent2: { score: 9 },
});
},
manager.update.match({
id: 2, // Final
opponent1: { score: 16, result: "win" },
opponent2: { score: 9 },
});
},
);
CreateSingleEliminationStage.run();

View File

@@ -1,316 +1,316 @@
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
import { suite } from "uvu";
import * as assert from "uvu/assert";
import { Status } from "~/db/types";
import { InMemoryDatabase } from "~/modules/brackets-memory-db";
import { BracketsManager } from "../manager";
const storage = new InMemoryDatabase();
const manager = new BracketsManager(storage);
const example = {
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: { seedOrdering: ["natural"] },
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
settings: { seedOrdering: ["natural"] },
} as any;
const UpdateMatches = suite("Update matches");
UpdateMatches.before.each(() => {
storage.reset();
manager.create(example);
storage.reset();
manager.create(example);
});
UpdateMatches("should start a match", () => {
const before = storage.select<any>("match", 0);
assert.equal(before.status, Status.Ready);
const before = storage.select<any>("match", 0);
assert.equal(before.status, Status.Ready);
manager.update.match({
id: 0,
opponent1: { score: 0 },
opponent2: { score: 0 },
});
manager.update.match({
id: 0,
opponent1: { score: 0 },
opponent2: { score: 0 },
});
const after = storage.select<any>("match", 0);
assert.equal(after.status, Status.Running);
const after = storage.select<any>("match", 0);
assert.equal(after.status, Status.Running);
});
UpdateMatches(
"should update the scores for a match and set it to running",
() => {
manager.update.match({
id: 0,
opponent1: { score: 2 },
opponent2: { score: 1 },
});
"should update the scores for a match and set it to running",
() => {
manager.update.match({
id: 0,
opponent1: { score: 2 },
opponent2: { score: 1 },
});
const after = storage.select<any>("match", 0);
assert.equal(after.status, Status.Running);
assert.equal(after.opponent1.score, 2);
const after = storage.select<any>("match", 0);
assert.equal(after.status, Status.Running);
assert.equal(after.opponent1.score, 2);
// Name should stay. It shouldn't be overwritten.
assert.equal(after.opponent1.id, 1);
},
// Name should stay. It shouldn't be overwritten.
assert.equal(after.opponent1.id, 1);
},
);
UpdateMatches("should end the match by only setting the winner", () => {
const before = storage.select<any>("match", 0);
assert.not.ok(before.opponent1.result);
const before = storage.select<any>("match", 0);
assert.not.ok(before.opponent1.result);
manager.update.match({
id: 0,
opponent1: { result: "win" },
});
manager.update.match({
id: 0,
opponent1: { result: "win" },
});
const after = storage.select<any>("match", 0);
assert.equal(after.status, Status.Completed);
assert.equal(after.opponent1.result, "win");
assert.equal(after.opponent2.result, "loss");
const after = storage.select<any>("match", 0);
assert.equal(after.status, Status.Completed);
assert.equal(after.opponent1.result, "win");
assert.equal(after.opponent2.result, "loss");
});
UpdateMatches(
"should change the winner of the match and update in the next match",
() => {
manager.update.match({
id: 0,
opponent1: { result: "win" },
});
"should change the winner of the match and update in the next match",
() => {
manager.update.match({
id: 0,
opponent1: { result: "win" },
});
assert.equal(storage.select<any>("match", 8).opponent1.id, 1);
assert.equal(storage.select<any>("match", 8).opponent1.id, 1);
manager.update.match({
id: 0,
opponent2: { result: "win" },
});
manager.update.match({
id: 0,
opponent2: { result: "win" },
});
const after = storage.select<any>("match", 0);
assert.equal(after.status, Status.Completed);
assert.equal(after.opponent1.result, "loss");
assert.equal(after.opponent2.result, "win");
const after = storage.select<any>("match", 0);
assert.equal(after.status, Status.Completed);
assert.equal(after.opponent1.result, "loss");
assert.equal(after.opponent2.result, "win");
const nextMatch = storage.select<any>("match", 8);
assert.equal(nextMatch.status, Status.Waiting);
assert.equal(nextMatch.opponent1.id, 2);
},
const nextMatch = storage.select<any>("match", 8);
assert.equal(nextMatch.status, Status.Waiting);
assert.equal(nextMatch.opponent1.id, 2);
},
);
UpdateMatches("should update the status of the next match", () => {
manager.update.match({
id: 0,
opponent1: { result: "win" },
});
manager.update.match({
id: 0,
opponent1: { result: "win" },
});
assert.equal(storage.select<any>("match", 8).status, Status.Waiting);
assert.equal(storage.select<any>("match", 8).status, Status.Waiting);
manager.update.match({
id: 1,
opponent1: { result: "win" },
});
manager.update.match({
id: 1,
opponent1: { result: "win" },
});
assert.equal(storage.select<any>("match", 8).status, Status.Ready);
assert.equal(storage.select<any>("match", 8).status, Status.Ready);
});
UpdateMatches("should end the match by setting winner and loser", () => {
manager.update.match({
id: 0,
status: Status.Running,
});
manager.update.match({
id: 0,
status: Status.Running,
});
manager.update.match({
id: 0,
opponent1: { result: "win" },
opponent2: { result: "loss" },
});
manager.update.match({
id: 0,
opponent1: { result: "win" },
opponent2: { result: "loss" },
});
const after = storage.select<any>("match", 0);
assert.equal(after.status, Status.Completed);
assert.equal(after.opponent1.result, "win");
assert.equal(after.opponent2.result, "loss");
const after = storage.select<any>("match", 0);
assert.equal(after.status, Status.Completed);
assert.equal(after.opponent1.result, "win");
assert.equal(after.opponent2.result, "loss");
});
UpdateMatches("should remove results from a match without score", () => {
manager.update.match({
id: 0,
opponent1: { result: "win" },
opponent2: { result: "loss" },
});
manager.update.match({
id: 0,
opponent1: { result: "win" },
opponent2: { result: "loss" },
});
manager.reset.matchResults(0);
manager.reset.matchResults(0);
const after = storage.select<any>("match", 0);
assert.equal(after.status, Status.Ready);
assert.not.ok(after.opponent1.result);
assert.not.ok(after.opponent2.result);
const after = storage.select<any>("match", 0);
assert.equal(after.status, Status.Ready);
assert.not.ok(after.opponent1.result);
assert.not.ok(after.opponent2.result);
});
UpdateMatches("should remove results from a match with score", () => {
manager.update.match({
id: 0,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12, result: "loss" },
});
manager.update.match({
id: 0,
opponent1: { score: 16, result: "win" },
opponent2: { score: 12, result: "loss" },
});
manager.reset.matchResults(0);
manager.reset.matchResults(0);
const after = storage.select<any>("match", 0);
assert.equal(after.status, Status.Running);
assert.not.ok(after.opponent1.result);
assert.not.ok(after.opponent2.result);
const after = storage.select<any>("match", 0);
assert.equal(after.status, Status.Running);
assert.not.ok(after.opponent1.result);
assert.not.ok(after.opponent2.result);
});
UpdateMatches("should not set the other score to 0 if only one given", () => {
// It shouldn't be our decision to set the other score to 0.
// It shouldn't be our decision to set the other score to 0.
manager.update.match({
id: 1,
opponent1: { score: 1 },
});
manager.update.match({
id: 1,
opponent1: { score: 1 },
});
const after = storage.select<any>("match", 1);
assert.equal(after.status, Status.Running);
assert.equal(after.opponent1.score, 1);
assert.not.ok(after.opponent2.score);
const after = storage.select<any>("match", 1);
assert.equal(after.status, Status.Running);
assert.equal(after.opponent1.score, 1);
assert.not.ok(after.opponent2.score);
});
UpdateMatches(
"should end the match by setting the winner and the scores",
() => {
manager.update.match({
id: 1,
opponent1: { score: 6 },
opponent2: { result: "win", score: 3 },
});
"should end the match by setting the winner and the scores",
() => {
manager.update.match({
id: 1,
opponent1: { score: 6 },
opponent2: { result: "win", score: 3 },
});
const after = storage.select<any>("match", 1);
assert.equal(after.status, Status.Completed);
const after = storage.select<any>("match", 1);
assert.equal(after.status, Status.Completed);
assert.equal(after.opponent1.result, "loss");
assert.equal(after.opponent1.score, 6);
assert.equal(after.opponent1.result, "loss");
assert.equal(after.opponent1.score, 6);
assert.equal(after.opponent2.result, "win");
assert.equal(after.opponent2.score, 3);
},
assert.equal(after.opponent2.result, "win");
assert.equal(after.opponent2.score, 3);
},
);
UpdateMatches("should throw if two winners", () => {
assert.throws(
() =>
manager.update.match({
id: 3,
opponent1: { result: "win" },
opponent2: { result: "win" },
}),
"There are two winners.",
);
assert.throws(
() =>
manager.update.match({
id: 3,
opponent1: { result: "win" },
opponent2: { result: "win" },
}),
"There are two winners.",
);
assert.throws(
() =>
manager.update.match({
id: 3,
opponent1: { result: "loss" },
opponent2: { result: "loss" },
}),
"There are two losers.",
);
assert.throws(
() =>
manager.update.match({
id: 3,
opponent1: { result: "loss" },
opponent2: { result: "loss" },
}),
"There are two losers.",
);
});
const GiveOpponentIds = suite("Give opponent IDs when updating");
GiveOpponentIds.before.each(() => {
storage.reset();
storage.reset();
manager.create({
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4],
settings: { seedOrdering: ["natural"] },
});
manager.create({
name: "Amateur",
tournamentId: 0,
type: "double_elimination",
seeding: [1, 2, 3, 4],
settings: { seedOrdering: ["natural"] },
});
});
GiveOpponentIds("should update the right opponents based on their IDs", () => {
manager.update.match({
id: 0,
opponent1: {
id: 2,
score: 10,
},
opponent2: {
id: 1,
score: 5,
},
});
manager.update.match({
id: 0,
opponent1: {
id: 2,
score: 10,
},
opponent2: {
id: 1,
score: 5,
},
});
// Actual results must be inverted.
const after = storage.select<any>("match", 0);
assert.equal(after.opponent1.score, 5);
assert.equal(after.opponent2.score, 10);
// Actual results must be inverted.
const after = storage.select<any>("match", 0);
assert.equal(after.opponent1.score, 5);
assert.equal(after.opponent2.score, 10);
});
GiveOpponentIds(
"should update the right opponent based on its ID, the other one is the remaining one",
() => {
manager.update.match({
id: 0,
opponent1: {
id: 2,
score: 10,
},
});
"should update the right opponent based on its ID, the other one is the remaining one",
() => {
manager.update.match({
id: 0,
opponent1: {
id: 2,
score: 10,
},
});
// Actual results must be inverted.
const after = storage.select<any>("match", 0);
assert.not.ok(after.opponent1.score);
assert.equal(after.opponent2.score, 10);
},
// Actual results must be inverted.
const after = storage.select<any>("match", 0);
assert.not.ok(after.opponent1.score);
assert.equal(after.opponent2.score, 10);
},
);
GiveOpponentIds(
"should throw when the given opponent ID does not exist in the match",
() => {
assert.throws(
() =>
manager.update.match({
id: 0,
opponent1: {
id: 3, // Belongs to match id 1.
score: 10,
},
}),
/The given opponent[12] ID does not exist in this match./,
);
},
"should throw when the given opponent ID does not exist in the match",
() => {
assert.throws(
() =>
manager.update.match({
id: 0,
opponent1: {
id: 3, // Belongs to match id 1.
score: 10,
},
}),
/The given opponent[12] ID does not exist in this match./,
);
},
);
const LockedMatches = suite("Locked matches");
LockedMatches.before.each(() => {
storage.reset();
manager.create(example);
storage.reset();
manager.create(example);
});
LockedMatches(
"should throw when the matches leading to the match have not been completed yet",
() => {
manager.update.match({ id: 0 }); // No problem when no previous match.
assert.throws(
() => manager.update.match({ id: 8 }),
"The match is locked.",
); // First match of WB Round 2.
assert.throws(
() => manager.update.match({ id: 15 }),
"The match is locked.",
); // First match of LB Round 1.
assert.throws(
() => manager.update.match({ id: 19 }),
"The match is locked.",
); // First match of LB Round 1.
assert.throws(
() => manager.update.match({ id: 23 }),
"The match is locked.",
); // First match of LB Round 3.
},
"should throw when the matches leading to the match have not been completed yet",
() => {
manager.update.match({ id: 0 }); // No problem when no previous match.
assert.throws(
() => manager.update.match({ id: 8 }),
"The match is locked.",
); // First match of WB Round 2.
assert.throws(
() => manager.update.match({ id: 15 }),
"The match is locked.",
); // First match of LB Round 1.
assert.throws(
() => manager.update.match({ id: 19 }),
"The match is locked.",
); // First match of LB Round 1.
assert.throws(
() => manager.update.match({ id: 23 }),
"The match is locked.",
); // First match of LB Round 3.
},
);
UpdateMatches.run();

View File

@@ -1,17 +1,17 @@
import type {
Group,
Match,
Round,
SeedOrdering,
Stage,
Group,
Match,
Round,
SeedOrdering,
Stage,
} from "~/modules/brackets-model";
/**
* Type of an object implementing every ordering method.
*/
export type OrderingMap = Record<
SeedOrdering,
<T>(array: T[], ...args: number[]) => T[]
SeedOrdering,
<T>(array: T[], ...args: number[]) => T[]
>;
/**
@@ -53,42 +53,42 @@ export type Scores = { opponent1: number; opponent2: number };
* Positional information about a round.
*/
export type RoundPositionalInfo = {
roundNumber: number;
roundCount: number;
roundNumber: number;
roundCount: number;
};
/**
* The result of an array which was split by parity.
*/
export interface ParitySplit<T> {
even: T[];
odd: T[];
even: T[];
odd: T[];
}
/**
* Makes an object type deeply partial.
*/
export type DeepPartial<T> = T extends object
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
? {
[P in keyof T]?: DeepPartial<T[P]>;
}
: T;
/**
* Converts all value types to array types.
*/
export type ValueToArray<T> = {
[K in keyof T]: Array<T[K]>;
[K in keyof T]: Array<T[K]>;
};
/**
* Data type associated to each database table.
*/
export interface DataTypes {
stage: Stage;
group: Group;
round: Round;
match: Match;
stage: Stage;
group: Group;
round: Round;
match: Match;
}
/**
@@ -107,117 +107,117 @@ export type TournamentManagerDataSet = Database;
* An item in the final standings of an elimination stage.
*/
export interface FinalStandingsItem {
id: number;
name: string;
rank: number;
id: number;
name: string;
rank: number;
}
/**
* Contains the losers and the winner of the bracket.
*/
export interface StandardBracketResults {
/**
* The list of losers for each round of the bracket.
*/
losers: ParticipantSlot[][];
/**
* The list of losers for each round of the bracket.
*/
losers: ParticipantSlot[][];
/**
* The winner of the bracket.
*/
winner: ParticipantSlot;
/**
* The winner of the bracket.
*/
winner: ParticipantSlot;
}
/**
* This CRUD interface is used by the manager to abstract storage.
*/
export interface CrudInterface {
/**
* Inserts a value in the database and returns its id.
*
* @param table Where to insert.
* @param value What to insert.
*/
insert<T extends Table>(table: T, value: OmitId<DataTypes[T]>): number;
/**
* Inserts a value in the database and returns its id.
*
* @param table Where to insert.
* @param value What to insert.
*/
insert<T extends Table>(table: T, value: OmitId<DataTypes[T]>): number;
/**
* Inserts multiple values in the database.
*
* @param table Where to insert.
* @param values What to insert.
*/
insert<T extends Table>(table: T, values: OmitId<DataTypes[T]>[]): boolean;
/**
* Inserts multiple values in the database.
*
* @param table Where to insert.
* @param values What to insert.
*/
insert<T extends Table>(table: T, values: OmitId<DataTypes[T]>[]): boolean;
/**
* Gets all data from a table in the database.
*
* @param table Where to get from.
*/
select<T extends Table>(table: T): Array<DataTypes[T]> | null;
/**
* Gets all data from a table in the database.
*
* @param table Where to get from.
*/
select<T extends Table>(table: T): Array<DataTypes[T]> | null;
/**
* Gets specific data from a table in the database.
*
* @param table Where to get from.
* @param id What to get.
*/
select<T extends Table>(table: T, id: number): DataTypes[T] | null;
/**
* Gets specific data from a table in the database.
*
* @param table Where to get from.
* @param id What to get.
*/
select<T extends Table>(table: T, id: number): DataTypes[T] | null;
/**
* Gets data from a table in the database with a filter.
*
* @param table Where to get from.
* @param filter An object to filter data.
*/
select<T extends Table>(
table: T,
filter: Partial<DataTypes[T]>,
): Array<DataTypes[T]> | null;
/**
* Gets data from a table in the database with a filter.
*
* @param table Where to get from.
* @param filter An object to filter data.
*/
select<T extends Table>(
table: T,
filter: Partial<DataTypes[T]>,
): Array<DataTypes[T]> | null;
/**
* Updates data in a table.
*
* @param table Where to update.
* @param id What to update.
* @param value How to update.
*/
update<T extends Table>(table: T, id: number, value: DataTypes[T]): boolean;
/**
* Updates data in a table.
*
* @param table Where to update.
* @param id What to update.
* @param value How to update.
*/
update<T extends Table>(table: T, id: number, value: DataTypes[T]): boolean;
/**
* Updates data in a table.
*
* @param table Where to update.
* @param filter An object to filter data.
* @param value How to update.
*/
update<T extends Table>(
table: T,
filter: Partial<DataTypes[T]>,
value: Partial<DataTypes[T]>,
): boolean;
/**
* Updates data in a table.
*
* @param table Where to update.
* @param filter An object to filter data.
* @param value How to update.
*/
update<T extends Table>(
table: T,
filter: Partial<DataTypes[T]>,
value: Partial<DataTypes[T]>,
): boolean;
/**
* Empties a table completely.
*
* @param table Where to delete everything.
*/
delete<T extends Table>(table: T): boolean;
/**
* Empties a table completely.
*
* @param table Where to delete everything.
*/
delete<T extends Table>(table: T): boolean;
/**
* Delete data in a table, based on a filter.
*
* @param table Where to delete in.
* @param filter An object to filter data.
*/
delete<T extends Table>(table: T, filter: Partial<DataTypes[T]>): boolean;
/**
* Delete data in a table, based on a filter.
*
* @param table Where to delete in.
* @param filter An object to filter data.
*/
delete<T extends Table>(table: T, filter: Partial<DataTypes[T]>): boolean;
}
export interface Storage extends CrudInterface {
selectFirst<T extends Table>(
table: T,
filter: Partial<DataTypes[T]>,
): DataTypes[T] | null;
selectLast<T extends Table>(
table: T,
filter: Partial<DataTypes[T]>,
): DataTypes[T] | null;
selectFirst<T extends Table>(
table: T,
filter: Partial<DataTypes[T]>,
): DataTypes[T] | null;
selectLast<T extends Table>(
table: T,
filter: Partial<DataTypes[T]>,
): DataTypes[T] | null;
}

View File

@@ -1,120 +1,120 @@
import type { Match, Round, SeedOrdering } from "~/modules/brackets-model";
import { Status } from "~/modules/brackets-model";
import { ordering } from "./ordering";
import { BaseUpdater } from "./base/updater";
import type { DeepPartial } from "./types";
import * as helpers from "./helpers";
import { ordering } from "./ordering";
import type { DeepPartial } from "./types";
export class Update extends BaseUpdater {
/**
* Updates partial information of a match. Its id must be given.
*
* This will update related matches accordingly.
*
* @param match Values to change in a match.
*/
public match<M extends Match = Match>(match: DeepPartial<M>): void {
if (match.id === undefined) throw Error("No match id given.");
/**
* Updates partial information of a match. Its id must be given.
*
* This will update related matches accordingly.
*
* @param match Values to change in a match.
*/
public match<M extends Match = Match>(match: DeepPartial<M>): void {
if (match.id === undefined) throw Error("No match id given.");
const stored = this.storage.select("match", match.id);
if (!stored) throw Error("Match not found.");
const stored = this.storage.select("match", match.id);
if (!stored) throw Error("Match not found.");
this.updateMatch(stored, match);
}
this.updateMatch(stored, match);
}
/**
* Updates the seed ordering of every ordered round in a stage.
*
* @param stageId ID of the stage.
* @param seedOrdering A list of ordering methods.
*/
public ordering(stageId: number, seedOrdering: SeedOrdering[]): void {
const stage = this.storage.select("stage", stageId);
if (!stage) throw Error("Stage not found.");
/**
* Updates the seed ordering of every ordered round in a stage.
*
* @param stageId ID of the stage.
* @param seedOrdering A list of ordering methods.
*/
public ordering(stageId: number, seedOrdering: SeedOrdering[]): void {
const stage = this.storage.select("stage", stageId);
if (!stage) throw Error("Stage not found.");
helpers.ensureNotRoundRobin(stage);
helpers.ensureNotRoundRobin(stage);
const roundsToOrder = this.getOrderedRounds(stage);
if (seedOrdering.length !== roundsToOrder.length)
throw Error("The count of seed orderings is incorrect.");
const roundsToOrder = this.getOrderedRounds(stage);
if (seedOrdering.length !== roundsToOrder.length)
throw Error("The count of seed orderings is incorrect.");
for (let i = 0; i < roundsToOrder.length; i++)
this.updateRoundOrdering(roundsToOrder[i], seedOrdering[i]);
}
for (let i = 0; i < roundsToOrder.length; i++)
this.updateRoundOrdering(roundsToOrder[i], seedOrdering[i]);
}
/**
* Updates the seed ordering of a round.
*
* @param roundId ID of the round.
* @param method Seed ordering method.
*/
public roundOrdering(roundId: number, method: SeedOrdering): void {
const round = this.storage.select("round", roundId);
if (!round) throw Error("This round does not exist.");
/**
* Updates the seed ordering of a round.
*
* @param roundId ID of the round.
* @param method Seed ordering method.
*/
public roundOrdering(roundId: number, method: SeedOrdering): void {
const round = this.storage.select("round", roundId);
if (!round) throw Error("This round does not exist.");
const stage = this.storage.select("stage", round.stage_id);
if (!stage) throw Error("Stage not found.");
const stage = this.storage.select("stage", round.stage_id);
if (!stage) throw Error("Stage not found.");
helpers.ensureNotRoundRobin(stage);
helpers.ensureNotRoundRobin(stage);
this.updateRoundOrdering(round, method);
}
this.updateRoundOrdering(round, method);
}
/**
* Update the seed ordering of a round.
*
* @param round The round of which to update the ordering.
* @param method The new ordering method.
*/
private updateRoundOrdering(round: Round, method: SeedOrdering): void {
const matches = this.storage.select("match", { round_id: round.id });
if (!matches) throw Error("This round has no match.");
/**
* Update the seed ordering of a round.
*
* @param round The round of which to update the ordering.
* @param method The new ordering method.
*/
private updateRoundOrdering(round: Round, method: SeedOrdering): void {
const matches = this.storage.select("match", { round_id: round.id });
if (!matches) throw Error("This round has no match.");
if (matches.some((match) => match.status > Status.Ready))
throw Error("At least one match has started or is completed.");
if (matches.some((match) => match.status > Status.Ready))
throw Error("At least one match has started or is completed.");
const stage = this.storage.select("stage", round.stage_id);
if (!stage) throw Error("Stage not found.");
if (stage.settings.size === undefined) throw Error("Undefined stage size.");
const stage = this.storage.select("stage", round.stage_id);
if (!stage) throw Error("Stage not found.");
if (stage.settings.size === undefined) throw Error("Undefined stage size.");
const group = this.storage.select("group", round.group_id);
if (!group) throw Error("Group not found.");
const group = this.storage.select("group", round.group_id);
if (!group) throw Error("Group not found.");
const inLoserBracket = helpers.isLoserBracket(stage.type, group.number);
const roundCountLB = helpers.getLowerBracketRoundCount(stage.settings.size);
const seeds = helpers.getSeeds(
inLoserBracket,
round.number,
roundCountLB,
matches.length,
);
const positions = ordering[method](seeds);
const inLoserBracket = helpers.isLoserBracket(stage.type, group.number);
const roundCountLB = helpers.getLowerBracketRoundCount(stage.settings.size);
const seeds = helpers.getSeeds(
inLoserBracket,
round.number,
roundCountLB,
matches.length,
);
const positions = ordering[method](seeds);
this.applyRoundOrdering(round.number, matches, positions);
}
this.applyRoundOrdering(round.number, matches, positions);
}
/**
* Updates the ordering of participants in a round's matches.
*
* @param roundNumber The number of the round.
* @param matches The matches of the round.
* @param positions The new positions.
*/
private applyRoundOrdering(
roundNumber: number,
matches: Match[],
positions: number[],
): void {
for (const match of matches) {
const updated = { ...match };
updated.opponent1 = helpers.findPosition(matches, positions.shift()!);
/**
* Updates the ordering of participants in a round's matches.
*
* @param roundNumber The number of the round.
* @param matches The matches of the round.
* @param positions The new positions.
*/
private applyRoundOrdering(
roundNumber: number,
matches: Match[],
positions: number[],
): void {
for (const match of matches) {
const updated = { ...match };
updated.opponent1 = helpers.findPosition(matches, positions.shift()!);
// The only rounds where we have a second ordered participant are first rounds of brackets (upper and lower).
if (roundNumber === 1)
updated.opponent2 = helpers.findPosition(matches, positions.shift()!);
// The only rounds where we have a second ordered participant are first rounds of brackets (upper and lower).
if (roundNumber === 1)
updated.opponent2 = helpers.findPosition(matches, positions.shift()!);
if (!this.storage.update("match", updated.id, updated))
throw Error("Could not update the match.");
}
}
if (!this.storage.update("match", updated.id, updated))
throw Error("Could not update the match.");
}
}
}

View File

@@ -1,263 +1,263 @@
import clone from "just-clone";
import type {
CrudInterface,
OmitId,
Table,
Database,
CrudInterface,
Database,
OmitId,
Table,
} from "~/modules/brackets-manager";
export class InMemoryDatabase implements CrudInterface {
protected data: Database = {
stage: [],
group: [],
round: [],
match: [],
};
protected data: Database = {
stage: [],
group: [],
round: [],
match: [],
};
/**
* @param data "import" data from external
*/
setData(data: Database): void {
this.data = data;
}
/**
* @param data "import" data from external
*/
setData(data: Database): void {
this.data = data;
}
/**
* @param partial Filter
*/
makeFilter(partial: any): (entry: any) => boolean {
return (entry: any): boolean => {
let result = true;
for (const key of Object.keys(partial))
result = result && entry[key] === partial[key];
/**
* @param partial Filter
*/
makeFilter(partial: any): (entry: any) => boolean {
return (entry: any): boolean => {
let result = true;
for (const key of Object.keys(partial))
result = result && entry[key] === partial[key];
return result;
};
}
return result;
};
}
/**
* Clearing all of the data
*/
reset(): void {
this.data = {
stage: [],
group: [],
round: [],
match: [],
};
}
/**
* Clearing all of the data
*/
reset(): void {
this.data = {
stage: [],
group: [],
round: [],
match: [],
};
}
insert<T>(table: Table, value: OmitId<T>): number;
/**
* Inserts multiple values in the database.
*
* @param table Where to insert.
* @param values What to insert.
*/
insert<T>(table: Table, values: OmitId<T>[]): boolean;
insert<T>(table: Table, value: OmitId<T>): number;
/**
* Inserts multiple values in the database.
*
* @param table Where to insert.
* @param values What to insert.
*/
insert<T>(table: Table, values: OmitId<T>[]): boolean;
/**
* Implementation of insert
*
* @param table Where to insert.
* @param values What to insert.
*/
insert<T>(table: Table, values: OmitId<T> | OmitId<T>[]): number | boolean {
let id =
this.data[table].length > 0
? Math.max(...this.data[table].map((d) => d.id)) + 1
: 0;
/**
* Implementation of insert
*
* @param table Where to insert.
* @param values What to insert.
*/
insert<T>(table: Table, values: OmitId<T> | OmitId<T>[]): number | boolean {
let id =
this.data[table].length > 0
? Math.max(...this.data[table].map((d) => d.id)) + 1
: 0;
if (!Array.isArray(values)) {
try {
// @ts-expect-error idc
if (values.id) {
// @ts-expect-error idc
this.data[table][values.id] = values;
} else {
// @ts-expect-error imported
this.data[table].push({ id, ...values });
}
} catch (error) {
return -1;
}
return id;
}
if (!Array.isArray(values)) {
try {
// @ts-expect-error idc
if (values.id) {
// @ts-expect-error idc
this.data[table][values.id] = values;
} else {
// @ts-expect-error imported
this.data[table].push({ id, ...values });
}
} catch (error) {
return -1;
}
return id;
}
try {
values.forEach((object) => {
// @ts-expect-error idc
if (object.id) {
// @ts-expect-error idc
this.data[table][object.id] = object;
} else {
// @ts-expect-error imported
this.data[table].push({ id: id++, ...object });
}
});
} catch (error) {
return false;
}
try {
for (const object of values) {
// @ts-expect-error idc
if (object.id) {
// @ts-expect-error idc
this.data[table][object.id] = object;
} else {
// @ts-expect-error imported
this.data[table].push({ id: id++, ...object });
}
}
} catch (error) {
return false;
}
return true;
}
return true;
}
/**
* Gets all data from a table in the database.
*
* @param table Where to get from.
*/
select<T>(table: Table): T[] | null;
/**
* Gets specific data from a table in the database.
*
* @param table Where to get from.
* @param id What to get.
*/
select<T>(table: Table, id: number): T | null;
/**
* Gets data from a table in the database with a filter.
*
* @param table Where to get from.
* @param filter An object to filter data.
*/
select<T>(table: Table, filter: Partial<T>): T[] | null;
/**
* Gets all data from a table in the database.
*
* @param table Where to get from.
*/
select<T>(table: Table): T[] | null;
/**
* Gets specific data from a table in the database.
*
* @param table Where to get from.
* @param id What to get.
*/
select<T>(table: Table, id: number): T | null;
/**
* Gets data from a table in the database with a filter.
*
* @param table Where to get from.
* @param filter An object to filter data.
*/
select<T>(table: Table, filter: Partial<T>): T[] | null;
/**
* @param table Where to get from.
* @param arg Arg.
*/
select<T>(table: Table, arg?: number | Partial<T>): T[] | null {
try {
if (arg === undefined) {
// @ts-expect-error imported
return this.data[table].map(clone);
}
/**
* @param table Where to get from.
* @param arg Arg.
*/
select<T>(table: Table, arg?: number | Partial<T>): T[] | null {
try {
if (arg === undefined) {
// @ts-expect-error imported
return this.data[table].map(clone);
}
if (typeof arg === "number") {
// @ts-expect-error imported
return clone(this.data[table].find((d) => d?.id === arg));
}
if (typeof arg === "number") {
// @ts-expect-error imported
return clone(this.data[table].find((d) => d?.id === arg));
}
// @ts-expect-error imported
return this.data[table].filter(this.makeFilter(arg)).map(clone);
} catch (error) {
return null;
}
}
// @ts-expect-error imported
return this.data[table].filter(this.makeFilter(arg)).map(clone);
} catch (error) {
return null;
}
}
/**
* Updates data in a table.
*
* @param table Where to update.
* @param id What to update.
* @param value How to update.
*/
/**
* Updates data in a table.
*
* @param table Where to update.
* @param id What to update.
* @param value How to update.
*/
update<T>(table: Table, id: number, value: T): boolean;
update<T>(table: Table, id: number, value: T): boolean;
/**
* Updates data in a table.
*
* @param table Where to update.
* @param filter An object to filter data.
* @param value How to update.
*/
update<T>(table: Table, filter: Partial<T>, value: Partial<T>): boolean;
/**
* Updates data in a table.
*
* @param table Where to update.
* @param filter An object to filter data.
* @param value How to update.
*/
update<T>(table: Table, filter: Partial<T>, value: Partial<T>): boolean;
/**
* Updates data in a table.
*
* @param table Where to update.
* @param arg
* @param value How to update.
*/
update<T>(
table: Table,
arg: number | Partial<T>,
value?: Partial<T>,
): boolean {
if (typeof arg === "number") {
if (
this.data[table][arg] &&
value &&
// @ts-expect-error idc
this.data[table][arg].id !== value.id
) {
throw new Error("ID mismatch.");
}
try {
// @ts-expect-error imported
this.data[table][arg] = value;
return true;
} catch (error) {
return false;
}
}
/**
* Updates data in a table.
*
* @param table Where to update.
* @param arg
* @param value How to update.
*/
update<T>(
table: Table,
arg: number | Partial<T>,
value?: Partial<T>,
): boolean {
if (typeof arg === "number") {
if (
this.data[table][arg] &&
value &&
// @ts-expect-error idc
this.data[table][arg].id !== value.id
) {
throw new Error("ID mismatch.");
}
try {
// @ts-expect-error imported
this.data[table][arg] = value;
return true;
} catch (error) {
return false;
}
}
const values = this.data[table].filter(this.makeFilter(arg));
if (!values) {
return false;
}
const values = this.data[table].filter(this.makeFilter(arg));
if (!values) {
return false;
}
values.forEach((v: { id: any }) => {
const existing = this.data[table][v.id];
for (const key in value) {
if (
// @ts-expect-error imported
existing[key] &&
typeof existing[key] === "object" &&
typeof value[key] === "object"
) {
Object.assign(existing[key], value[key]); // For opponent objects, this does a deep merge of level 2.
} else {
// @ts-expect-error imported
existing[key] = value[key]; // Otherwise, do a simple value assignment.
}
}
this.data[table][v.id] = existing;
});
for (const v of values) {
const existing = this.data[table][v.id];
for (const key in value) {
if (
// @ts-expect-error imported
existing[key] &&
typeof existing[key] === "object" &&
typeof value[key] === "object"
) {
Object.assign(existing[key], value[key]); // For opponent objects, this does a deep merge of level 2.
} else {
// @ts-expect-error imported
existing[key] = value[key]; // Otherwise, do a simple value assignment.
}
}
this.data[table][v.id] = existing;
}
return true;
}
return true;
}
/**
* Empties a table completely.
*
* @param table Where to delete everything.
*/
delete(table: Table): boolean;
/**
* Delete data in a table, based on a filter.
*
* @param table Where to delete in.
* @param filter An object to filter data.
*/
delete<T>(table: Table, filter: Partial<T>): boolean;
/**
* Empties a table completely.
*
* @param table Where to delete everything.
*/
delete(table: Table): boolean;
/**
* Delete data in a table, based on a filter.
*
* @param table Where to delete in.
* @param filter An object to filter data.
*/
delete<T>(table: Table, filter: Partial<T>): boolean;
/**
* Delete data in a table, based on a filter.
*
* @param table Where to delete in.
* @param filter An object to filter data.
*/
delete<T>(table: Table, filter?: Partial<T>): boolean {
const values = this.data[table];
if (!values) {
return false;
}
/**
* Delete data in a table, based on a filter.
*
* @param table Where to delete in.
* @param filter An object to filter data.
*/
delete<T>(table: Table, filter?: Partial<T>): boolean {
const values = this.data[table];
if (!values) {
return false;
}
if (!filter) {
this.data[table] = [];
if (!filter) {
this.data[table] = [];
return true;
}
return true;
}
const predicate = this.makeFilter(filter);
const negativeFilter = (value: any): boolean => !predicate(value);
const predicate = this.makeFilter(filter);
const negativeFilter = (value: any): boolean => !predicate(value);
// @ts-expect-error imported
this.data[table] = values.filter(negativeFilter);
// @ts-expect-error imported
this.data[table] = values.filter(negativeFilter);
return true;
}
return true;
}
}

View File

@@ -3,10 +3,10 @@
*-----------------------------------------------------------*/
import type {
GrandFinalType,
RoundRobinMode,
SeedOrdering,
StageType,
GrandFinalType,
RoundRobinMode,
SeedOrdering,
StageType,
} from "./unions";
/**
@@ -24,101 +24,101 @@ export type Seeding = (number | null)[];
* Used to create a stage.
*/
export interface InputStage {
/**
* ID of the parent tournament.
*
* Used to determine the `number` property of a stage related to a tournament.
*/
tournamentId: number;
/**
* ID of the parent tournament.
*
* Used to determine the `number` property of a stage related to a tournament.
*/
tournamentId: number;
/** Name of the stage. */
name: string;
/** Name of the stage. */
name: string;
/** Type of stage. */
type: StageType;
/** Type of stage. */
type: StageType;
/** The number of the stage in its tournament. Is determined if not given. */
number?: number;
/** The number of the stage in its tournament. Is determined if not given. */
number?: number;
/** Contains participants or `null` for BYEs. */
seeding?: Seeding;
/** Contains participants or `null` for BYEs. */
seeding?: Seeding;
/** Contains optional settings specific to each stage type. */
settings?: StageSettings;
/** Contains optional settings specific to each stage type. */
settings?: StageSettings;
}
/**
* The possible settings for a stage.
*/
export interface StageSettings {
/**
* The number of participants.
*/
size?: number;
/**
* The number of participants.
*/
size?: number;
/**
* A list of ordering methods to apply to the seeding.
*
* - For a round-robin stage: 1 item required (**with** `"groups."` prefix).
* - Used to distribute in groups.
* - For a simple elimination stage, 1 item required (**without** `"groups."` prefix).
* - Used to distribute in round 1.
* - For a double elimination stage, 1 item required, 3+ items supported (**without** `"groups."` prefix).
* - Item 1 (required) - Used to distribute in WB round 1.
* - Item 2 - Used to distribute WB losers in LB round 1.
* - Items 3+ - Used to distribute WB losers in LB minor rounds (1 per round).
*/
seedOrdering?: SeedOrdering[];
/**
* A list of ordering methods to apply to the seeding.
*
* - For a round-robin stage: 1 item required (**with** `"groups."` prefix).
* - Used to distribute in groups.
* - For a simple elimination stage, 1 item required (**without** `"groups."` prefix).
* - Used to distribute in round 1.
* - For a double elimination stage, 1 item required, 3+ items supported (**without** `"groups."` prefix).
* - Item 1 (required) - Used to distribute in WB round 1.
* - Item 2 - Used to distribute WB losers in LB round 1.
* - Items 3+ - Used to distribute WB losers in LB minor rounds (1 per round).
*/
seedOrdering?: SeedOrdering[];
/**
* Whether to balance BYEs in the seeding of an elimination stage.
*
* This prevents having BYE against BYE in matches.
*/
balanceByes?: boolean;
/**
* Whether to balance BYEs in the seeding of an elimination stage.
*
* This prevents having BYE against BYE in matches.
*/
balanceByes?: boolean;
/**
* Number of groups in a round-robin stage.
*/
groupCount?: number;
/**
* Number of groups in a round-robin stage.
*/
groupCount?: number;
/**
* The mode for the round-robin stage.
*
* - If `simple`, each participant plays each opponent once.
* - If `double`, each participant plays each opponent twice, once at home and once away.
*/
roundRobinMode?: RoundRobinMode;
/**
* The mode for the round-robin stage.
*
* - If `simple`, each participant plays each opponent once.
* - If `double`, each participant plays each opponent twice, once at home and once away.
*/
roundRobinMode?: RoundRobinMode;
/**
* A list of seeds per group for a round-robin stage to be manually ordered.
*
* Seed ordering is ignored if this property is given.
*/
manualOrdering?: number[][];
/**
* A list of seeds per group for a round-robin stage to be manually ordered.
*
* Seed ordering is ignored if this property is given.
*/
manualOrdering?: number[][];
/**
* Optional final between semi-final losers.
*/
consolationFinal?: boolean;
/**
* Optional final between semi-final losers.
*/
consolationFinal?: boolean;
/**
* Whether to skip the first round of the WB of a double elimination stage.
*/
skipFirstRound?: boolean;
/**
* Whether to skip the first round of the WB of a double elimination stage.
*/
skipFirstRound?: boolean;
/**
* Optional grand final between WB and LB winners.
*
* - If `none`, there is no grand final.
* - If `simple`, the final is a single match. The winner is the winner of the stage.
* - If `double`, if the WB winner wins, he's the winner of the stage. But if he loses, the final is reset and there is a very last match.
* It might be fairer since it gives the WB winner the right to lose once during the stage...
*/
grandFinal?: GrandFinalType;
/**
* Optional grand final between WB and LB winners.
*
* - If `none`, there is no grand final.
* - If `simple`, the final is a single match. The winner is the winner of the stage.
* - If `double`, if the WB winner wins, he's the winner of the stage. But if he loses, the final is reset and there is a very last match.
* It might be fairer since it gives the WB winner the right to lose once during the stage...
*/
grandFinal?: GrandFinalType;
swiss?: {
groupCount: number;
roundCount: number;
};
swiss?: {
groupCount: number;
roundCount: number;
};
}

View File

@@ -8,55 +8,55 @@ import type { Result } from "./unions";
* The possible status for a match.
*/
export enum Status {
/** The two matches leading to this one are not completed yet. */
Locked = 0,
/** The two matches leading to this one are not completed yet. */
Locked = 0,
/** One participant is ready and waiting for the other one. */
Waiting = 1,
/** One participant is ready and waiting for the other one. */
Waiting = 1,
/** Both participants are ready to start. */
Ready = 2,
/** Both participants are ready to start. */
Ready = 2,
/** The match is running. */
Running = 3,
/** The match is running. */
Running = 3,
/** The match is completed. */
Completed = 4,
/** The match is completed. */
Completed = 4,
}
/**
* The results of a participant in a match.
*/
export interface ParticipantResult {
/** If `null`, the participant is to be determined. */
id: number | null;
/** If `null`, the participant is to be determined. */
id: number | null;
/** Indicates where the participant comes from. */
position?: number;
/** Indicates where the participant comes from. */
position?: number;
/** If this participant forfeits, the other automatically wins. */
forfeit?: boolean;
/** If this participant forfeits, the other automatically wins. */
forfeit?: boolean;
/** The current score of the participant. */
score?: number;
/** The current score of the participant. */
score?: number;
/** How many points in total participant scored in total this set. KO = 100 points. Getting KO'd = 0 points. */
totalPoints?: number;
/** How many points in total participant scored in total this set. KO = 100 points. Getting KO'd = 0 points. */
totalPoints?: number;
/** Tells what is the result of a duel for this participant. */
result?: Result;
/** Tells what is the result of a duel for this participant. */
result?: Result;
}
/**
* Only contains information about match status and results.
*/
export interface MatchResults {
/** Status of the match. */
status: Status;
/** Status of the match. */
status: Status;
/** First opponent of the match. */
opponent1: ParticipantResult | null;
/** First opponent of the match. */
opponent1: ParticipantResult | null;
/** Second opponent of the match. */
opponent2: ParticipantResult | null;
/** Second opponent of the match. */
opponent2: ParticipantResult | null;
}

View File

@@ -15,39 +15,39 @@ import type { StageType } from "./unions";
* A stage, which can be a round-robin stage or a single/double elimination stage.
*/
export interface Stage {
/** ID of the stage. */
id: number;
/** ID of the stage. */
id: number;
/** ID of the tournament this stage belongs to. */
tournament_id: number;
/** ID of the tournament this stage belongs to. */
tournament_id: number;
/** Name of the stage. */
name: string;
/** Name of the stage. */
name: string;
/** Type of the stage. */
type: StageType;
/** Type of the stage. */
type: StageType;
/** Settings of the stage. */
settings: StageSettings;
/** Settings of the stage. */
settings: StageSettings;
/** The number of the stage in its tournament. */
number: number;
/** The number of the stage in its tournament. */
number: number;
createdAt?: number | null;
createdAt?: number | null;
}
/**
* A group of a stage.
*/
export interface Group {
/** ID of the group. */
id: number;
/** ID of the group. */
id: number;
/** ID of the parent stage. */
stage_id: number;
/** ID of the parent stage. */
stage_id: number;
/** The number of the group in its stage. */
number: number;
/** The number of the group in its stage. */
number: number;
}
// The next levels don't have a `name` property. They are automatically named with their `number` and their context (parent levels).
@@ -56,42 +56,42 @@ export interface Group {
* A round of a group.
*/
export interface Round {
/** ID of the round. */
id: number;
/** ID of the round. */
id: number;
/** ID of the parent stage. */
stage_id: number;
/** ID of the parent stage. */
stage_id: number;
/** ID of the parent group. */
group_id: number;
/** ID of the parent group. */
group_id: number;
/** The number of the round in its group. */
number: number;
/** The number of the round in its group. */
number: number;
/** Info about the maps count */
maps?: Pick<TournamentRoundMaps, "count" | "type" | "pickBan"> | null;
/** Info about the maps count */
maps?: Pick<TournamentRoundMaps, "count" | "type" | "pickBan"> | null;
}
/**
* A match of a round.
*/
export interface Match extends MatchResults {
/** ID of the match. */
id: number;
/** ID of the match. */
id: number;
/** ID of the parent stage. */
stage_id: number;
/** ID of the parent stage. */
stage_id: number;
/** ID of the parent group. */
group_id: number;
/** ID of the parent group. */
group_id: number;
/** ID of the parent round. */
round_id: number;
/** ID of the parent round. */
round_id: number;
/** The number of the match in its round. */
number: number;
/** The number of the match in its round. */
number: number;
lastGameFinishedAt?: number | null;
lastGameFinishedAt?: number | null;
createdAt?: number | null;
createdAt?: number | null;
}

View File

@@ -6,10 +6,10 @@
* The only supported types of stage.
*/
export type StageType =
| "round_robin"
| "single_elimination"
| "double_elimination"
| "swiss";
| "round_robin"
| "single_elimination"
| "double_elimination"
| "swiss";
/**
* All the possible types of group in an elimination stage.
@@ -19,10 +19,10 @@ export type StageType =
* - `final_group` for both single and double elimination.
*/
export type GroupType =
| "single_bracket"
| "winner_bracket"
| "loser_bracket"
| "final_group";
| "single_bracket"
| "winner_bracket"
| "loser_bracket"
| "final_group";
/**
* The possible types for a double elimination stage's grand final.
@@ -43,16 +43,16 @@ export type RoundRobinMode = "simple" | "double";
* Used to order seeds.
*/
export type SeedOrdering =
| "natural"
| "reverse"
| "half_shift"
| "reverse_half_shift"
| "pair_flip"
| "inner_outer"
| "space_between"
| "groups.effort_balanced"
| "groups.seed_optimized"
| "groups.bracket_optimized";
| "natural"
| "reverse"
| "half_shift"
| "reverse_half_shift"
| "pair_flip"
| "inner_outer"
| "space_between"
| "groups.effort_balanced"
| "groups.seed_optimized"
| "groups.bracket_optimized";
/**
* The possible results of a duel for a participant.

View File

@@ -1,139 +1,139 @@
export const DEFAULT_LANGUAGE = "en";
export const languages = [
{
code: "da",
name: "Dansk",
},
{
code: "de",
name: "Deutsch",
},
{
code: "en",
name: "English",
},
{
code: "es-ES",
name: "Español (España)",
},
{
code: "es-US",
name: "Español (Latino)",
},
{
code: "fr-CA",
name: "Français (NoA)",
},
{
code: "fr-EU",
name: "Français (NoE)",
},
{
code: "he",
name: "עברית",
},
{
code: "it",
name: "Italiano",
},
{
code: "ja",
name: "日本語",
},
{
code: "ko",
name: "한국어",
},
{
code: "pl",
name: "Polski",
},
{
code: "pt-BR",
name: "Português do Brasil",
},
{
code: "nl",
name: "Nederlands",
},
{
code: "ru",
name: "Русский",
},
{
code: "zh",
name: "中文",
},
{
code: "da",
name: "Dansk",
},
{
code: "de",
name: "Deutsch",
},
{
code: "en",
name: "English",
},
{
code: "es-ES",
name: "Español (España)",
},
{
code: "es-US",
name: "Español (Latino)",
},
{
code: "fr-CA",
name: "Français (NoA)",
},
{
code: "fr-EU",
name: "Français (NoE)",
},
{
code: "he",
name: "עברית",
},
{
code: "it",
name: "Italiano",
},
{
code: "ja",
name: "日本語",
},
{
code: "ko",
name: "한국어",
},
{
code: "pl",
name: "Polski",
},
{
code: "pt-BR",
name: "Português do Brasil",
},
{
code: "nl",
name: "Nederlands",
},
{
code: "ru",
name: "Русский",
},
{
code: "zh",
name: "中文",
},
] as const;
export const languagesUnified = [
{
code: "da",
name: "Dansk",
},
{
code: "de",
name: "Deutsch",
},
{
code: "en",
name: "English",
},
{
code: "es",
name: "Español",
},
{
code: "fr",
name: "Français",
},
{
code: "he",
name: "עברית",
},
{
code: "it",
name: "Italiano",
},
{
code: "ja",
name: "日本語",
},
{
code: "ko",
name: "한국어",
},
{
code: "pl",
name: "Polski",
},
{
code: "pt",
name: "Português",
},
{
code: "nl",
name: "Nederlands",
},
{
code: "ru",
name: "Русский",
},
{
code: "zh",
name: "中文",
},
{
code: "da",
name: "Dansk",
},
{
code: "de",
name: "Deutsch",
},
{
code: "en",
name: "English",
},
{
code: "es",
name: "Español",
},
{
code: "fr",
name: "Français",
},
{
code: "he",
name: "עברית",
},
{
code: "it",
name: "Italiano",
},
{
code: "ja",
name: "日本語",
},
{
code: "ko",
name: "한국어",
},
{
code: "pl",
name: "Polski",
},
{
code: "pt",
name: "Português",
},
{
code: "nl",
name: "Nederlands",
},
{
code: "ru",
name: "Русский",
},
{
code: "zh",
name: "中文",
},
] as const;
export type LanguageCode = (typeof languages)[number]["code"];
export const config = {
supportedLngs: languages.map((lang) => lang.code),
fallbackLng: DEFAULT_LANGUAGE,
defaultNS: "common",
react: { useSuspense: false },
interpolation: {
escapeValue: false,
},
supportedLngs: languages.map((lang) => lang.code),
fallbackLng: DEFAULT_LANGUAGE,
defaultNS: "common",
react: { useSuspense: false },
interpolation: {
escapeValue: false,
},
};

View File

@@ -1,30 +1,30 @@
import { resolve } from "node:path";
import { createCookie } from "@remix-run/node";
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { RemixI18Next } from "remix-i18next/server";
import { config } from "./config";
const TEN_YEARS_IN_SECONDS = 31_536_000 * 10;
export const i18nCookie = createCookie("i18n", {
sameSite: "lax",
path: "/",
maxAge: TEN_YEARS_IN_SECONDS,
sameSite: "lax",
path: "/",
maxAge: TEN_YEARS_IN_SECONDS,
});
export const i18next = new RemixI18Next({
detection: {
cookie: i18nCookie,
supportedLanguages: config.supportedLngs,
fallbackLanguage: config.fallbackLng,
},
i18next: {
...config,
backend: {
loadPath: resolve("./locales/{{lng}}/{{ns}}.json"),
},
},
backend: Backend,
detection: {
cookie: i18nCookie,
supportedLanguages: config.supportedLngs,
fallbackLanguage: config.fallbackLng,
},
i18next: {
...config,
backend: {
loadPath: resolve("./locales/{{lng}}/{{ns}}.json"),
},
},
backend: Backend,
});
export default i18next;

View File

@@ -1,29 +1,29 @@
import { resolve } from "node:path";
import type { EntryContext } from "@remix-run/server-runtime";
import { createInstance } from "i18next";
import type { i18n as i18nType } from "i18next";
import Backend from "i18next-fs-backend";
import { resolve } from "node:path";
import { initReactI18next } from "react-i18next";
import { config } from "./config";
import i18next from "./i18next.server";
import type { i18n as i18nType } from "i18next";
export async function i18Instance(request: Request, context: EntryContext) {
const instance = createInstance() as i18nType;
const instance = createInstance() as i18nType;
const lng = await i18next.getLocale(request);
const ns = i18next.getRouteNamespaces(context);
const lng = await i18next.getLocale(request);
const ns = i18next.getRouteNamespaces(context);
await instance
.use(initReactI18next)
.use(Backend)
.init({
...config,
lng,
ns,
backend: {
loadPath: resolve("./locales/{{lng}}/{{ns}}.json"),
},
});
await instance
.use(initReactI18next)
.use(Backend)
.init({
...config,
lng,
ns,
backend: {
loadPath: resolve("./locales/{{lng}}/{{ns}}.json"),
},
});
return instance;
return instance;
}

View File

@@ -6,27 +6,27 @@ import { getInitialNamespaces } from "remix-i18next/client";
import { config } from "./config";
export function i18nLoader() {
return i18next
.use(initReactI18next)
.use(LanguageDetector)
.use(Backend)
.init({
...config,
ns: getInitialNamespaces(),
backend: {
loadPath: (lng: any, ns: any) => {
// use vite static asset fingerprinting
return new URL(
`../../../locales/${lng[0]}/${ns[0]}.json`,
import.meta.url,
).href;
},
},
detection: {
order: ["htmlTag"],
caches: [],
},
// without this hydration fails in E2E tests
initImmediate: false,
});
return i18next
.use(initReactI18next)
.use(LanguageDetector)
.use(Backend)
.init({
...config,
ns: getInitialNamespaces(),
backend: {
loadPath: (lng: any, ns: any) => {
// use vite static asset fingerprinting
return new URL(
`../../../locales/${lng[0]}/${ns[0]}.json`,
import.meta.url,
).href;
},
},
detection: {
order: ["htmlTag"],
caches: [],
},
// without this hydration fails in E2E tests
initImmediate: false,
});
}

View File

@@ -1,83 +1,83 @@
// Reference for Ability Chunks of Primary Slot-Only abilities: https://splatoonwiki.org/wiki/Ability_chunk#Splatoon_3
export const abilities = [
{ name: "ISM", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "ISS", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "IRU", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "RSU", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "SSU", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "SCU", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "SS", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "SPU", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "QR", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "QSJ", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "BRU", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "RES", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "SRU", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "IA", type: "STACKABLE", abilityChunkTypesRequired: [] },
{
name: "OG",
type: "HEAD_MAIN_ONLY",
abilityChunkTypesRequired: ["RSU", "SSU", "RES"],
},
{
name: "LDE",
type: "HEAD_MAIN_ONLY",
abilityChunkTypesRequired: ["ISM", "ISS", "IRU"],
},
{
name: "T",
type: "HEAD_MAIN_ONLY",
abilityChunkTypesRequired: ["SCU", "SS", "SPU"],
},
{
name: "CB",
type: "HEAD_MAIN_ONLY",
abilityChunkTypesRequired: ["RSU", "SSU", "SCU"],
},
{
name: "NS",
type: "CLOTHES_MAIN_ONLY",
abilityChunkTypesRequired: ["IRU", "RSU", "SSU"],
},
{
name: "H",
type: "CLOTHES_MAIN_ONLY",
abilityChunkTypesRequired: ["QR", "BRU", "RES"],
},
{
name: "TI",
type: "CLOTHES_MAIN_ONLY",
abilityChunkTypesRequired: ["ISM", "ISS", "IA"],
},
{
name: "RP",
type: "CLOTHES_MAIN_ONLY",
abilityChunkTypesRequired: ["SS", "QR", "SRU"],
},
{ name: "AD", type: "CLOTHES_MAIN_ONLY", abilityChunkTypesRequired: [] },
{
name: "SJ",
type: "SHOES_MAIN_ONLY",
abilityChunkTypesRequired: ["QSJ", "SRU", "IA"],
},
{
name: "OS",
type: "SHOES_MAIN_ONLY",
abilityChunkTypesRequired: ["IRU", "SPU", "BRU"],
},
{
name: "DR",
type: "SHOES_MAIN_ONLY",
abilityChunkTypesRequired: ["QSJ", "RES", "IA"],
},
{ name: "ISM", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "ISS", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "IRU", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "RSU", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "SSU", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "SCU", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "SS", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "SPU", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "QR", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "QSJ", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "BRU", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "RES", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "SRU", type: "STACKABLE", abilityChunkTypesRequired: [] },
{ name: "IA", type: "STACKABLE", abilityChunkTypesRequired: [] },
{
name: "OG",
type: "HEAD_MAIN_ONLY",
abilityChunkTypesRequired: ["RSU", "SSU", "RES"],
},
{
name: "LDE",
type: "HEAD_MAIN_ONLY",
abilityChunkTypesRequired: ["ISM", "ISS", "IRU"],
},
{
name: "T",
type: "HEAD_MAIN_ONLY",
abilityChunkTypesRequired: ["SCU", "SS", "SPU"],
},
{
name: "CB",
type: "HEAD_MAIN_ONLY",
abilityChunkTypesRequired: ["RSU", "SSU", "SCU"],
},
{
name: "NS",
type: "CLOTHES_MAIN_ONLY",
abilityChunkTypesRequired: ["IRU", "RSU", "SSU"],
},
{
name: "H",
type: "CLOTHES_MAIN_ONLY",
abilityChunkTypesRequired: ["QR", "BRU", "RES"],
},
{
name: "TI",
type: "CLOTHES_MAIN_ONLY",
abilityChunkTypesRequired: ["ISM", "ISS", "IA"],
},
{
name: "RP",
type: "CLOTHES_MAIN_ONLY",
abilityChunkTypesRequired: ["SS", "QR", "SRU"],
},
{ name: "AD", type: "CLOTHES_MAIN_ONLY", abilityChunkTypesRequired: [] },
{
name: "SJ",
type: "SHOES_MAIN_ONLY",
abilityChunkTypesRequired: ["QSJ", "SRU", "IA"],
},
{
name: "OS",
type: "SHOES_MAIN_ONLY",
abilityChunkTypesRequired: ["IRU", "SPU", "BRU"],
},
{
name: "DR",
type: "SHOES_MAIN_ONLY",
abilityChunkTypesRequired: ["QSJ", "RES", "IA"],
},
] as const;
export const abilitiesShort = abilities.map((ability) => ability.name);
export const stackableAbilitiesShort = abilities
.filter((ability) => ability.type === "STACKABLE")
.map((ability) => ability.name);
.filter((ability) => ability.type === "STACKABLE")
.map((ability) => ability.name);
export const mainOnlyAbilitiesShort = abilities
.filter((ability) => ability.type !== "STACKABLE")
.map((ability) => ability.name);
.filter((ability) => ability.type !== "STACKABLE")
.map((ability) => ability.name);

View File

@@ -1,75 +1,75 @@
export const headGearIds = [
1, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011,
1012, 1014, 1018, 1019, 1020, 1021, 1023, 1024, 1025, 1026, 1027, 1028, 1029,
1030, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 2000, 2001, 2002,
2003, 2004, 2005, 2008, 2009, 2010, 2011, 2012, 2013, 3000, 3001, 3002, 3003,
3004, 3005, 3006, 3007, 3008, 3009, 3010, 3011, 3012, 3013, 3014, 3015, 3016,
3017, 3018, 3021, 3022, 3023, 3024, 3025, 3026, 3027, 3028, 3029, 3030, 3031,
3032, 4000, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 4009, 4010, 4011,
4012, 4013, 4014, 4015, 4016, 4017, 4018, 4019, 4020, 4021, 4022, 4023, 5000,
5001, 5002, 5003, 5004, 5007, 5008, 5009, 5010, 5011, 5012, 6000, 6001, 6002,
6003, 6004, 6005, 6006, 7000, 7002, 7004, 7005, 7006, 7007, 7008, 7009, 7011,
7012, 7013, 7014, 7015, 7016, 7017, 7018, 7019, 7020, 7021, 8000, 8001, 8002,
8003, 8004, 8005, 8006, 8007, 8008, 8009, 8010, 8011, 8012, 8014, 8015, 8016,
8017, 8018, 8019, 9001, 9002, 9003, 9004, 9005, 9007, 9008, 9009, 10003,
21000, 21001, 21002, 21003, 21004, 21005, 21007, 21008, 21009, 21010, 21011,
21012, 21013, 21014, 21015, 21016, 21017, 21018, 21019, 21020, 21021, 24016,
24017, 24018, 24019, 24020, 24021, 24022, 24023, 24024, 24025, 24026, 24027,
24028, 24029, 24030, 24031, 24032, 24033, 25000, 25001, 25002, 25003, 25004,
25005, 25006, 25007, 25008, 25009, 25010, 25011, 25012, 25013, 25016, 25017,
25018, 25019, 25020, 27000, 27004, 27109, 27306, 28000, 28001, 29200, 29201,
29202, 29203, 29204,
1, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011,
1012, 1014, 1018, 1019, 1020, 1021, 1023, 1024, 1025, 1026, 1027, 1028, 1029,
1030, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 2000, 2001, 2002,
2003, 2004, 2005, 2008, 2009, 2010, 2011, 2012, 2013, 3000, 3001, 3002, 3003,
3004, 3005, 3006, 3007, 3008, 3009, 3010, 3011, 3012, 3013, 3014, 3015, 3016,
3017, 3018, 3021, 3022, 3023, 3024, 3025, 3026, 3027, 3028, 3029, 3030, 3031,
3032, 4000, 4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 4009, 4010, 4011,
4012, 4013, 4014, 4015, 4016, 4017, 4018, 4019, 4020, 4021, 4022, 4023, 5000,
5001, 5002, 5003, 5004, 5007, 5008, 5009, 5010, 5011, 5012, 6000, 6001, 6002,
6003, 6004, 6005, 6006, 7000, 7002, 7004, 7005, 7006, 7007, 7008, 7009, 7011,
7012, 7013, 7014, 7015, 7016, 7017, 7018, 7019, 7020, 7021, 8000, 8001, 8002,
8003, 8004, 8005, 8006, 8007, 8008, 8009, 8010, 8011, 8012, 8014, 8015, 8016,
8017, 8018, 8019, 9001, 9002, 9003, 9004, 9005, 9007, 9008, 9009, 10003,
21000, 21001, 21002, 21003, 21004, 21005, 21007, 21008, 21009, 21010, 21011,
21012, 21013, 21014, 21015, 21016, 21017, 21018, 21019, 21020, 21021, 24016,
24017, 24018, 24019, 24020, 24021, 24022, 24023, 24024, 24025, 24026, 24027,
24028, 24029, 24030, 24031, 24032, 24033, 25000, 25001, 25002, 25003, 25004,
25005, 25006, 25007, 25008, 25009, 25010, 25011, 25012, 25013, 25016, 25017,
25018, 25019, 25020, 27000, 27004, 27109, 27306, 28000, 28001, 29200, 29201,
29202, 29203, 29204,
] as const;
export const clothesGearIds = [
2, 3, 1000, 1001, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012,
1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1026, 1027,
1028, 1029, 1030, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 1040,
1041, 1042, 1043, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1053, 1054, 1055,
1056, 1057, 1058, 1059, 1060, 1062, 1063, 1066, 1067, 1068, 1069, 1070, 1071,
1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, 1081, 1082, 1083, 1084, 1085,
1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095, 2000, 2001, 2002,
2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015,
2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2026, 2027, 2028, 3000,
3001, 3002, 3003, 3004, 3005, 3006, 3007, 3008, 3009, 3010, 3011, 3013, 4000,
4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 4009, 4010, 5000, 5001, 5002,
5003, 5004, 5005, 5006, 5007, 5008, 5009, 5010, 5011, 5012, 5013, 5014, 5015,
5016, 5017, 5018, 5019, 5020, 5021, 5022, 5023, 5024, 5025, 5026, 5027, 5028,
5029, 5033, 5034, 5035, 5036, 5038, 5039, 5040, 5041, 5042, 5043, 5045, 5046,
5047, 5048, 5049, 5050, 5051, 5052, 5053, 5054, 5055, 5056, 5057, 5058, 5059,
5060, 5061, 5062, 5063, 5064, 5065, 6000, 6001, 6003, 6004, 6005, 6006, 6007,
6008, 7000, 7001, 7002, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010, 7011,
7012, 7013, 7014, 7015, 7016, 7017, 7018, 7019, 7020, 7021, 7022, 7023, 7024,
7025, 8000, 8001, 8002, 8003, 8004, 8005, 8006, 8007, 8008, 8009, 8010, 8011,
8012, 8013, 8014, 8015, 8017, 8018, 8019, 8020, 8021, 8022, 8023, 8024, 8025,
8026, 8027, 8028, 8030, 8031, 8032, 8033, 8034, 8035, 8036, 8037, 8038, 8039,
8040, 8041, 8042, 8043, 9000, 9001, 9002, 9003, 9004, 9005, 9007, 9008, 9009,
9010, 9011, 9012, 9013, 9014, 9015, 9016, 9017, 10000, 10001, 10002, 10004,
10005, 10006, 10007, 10008, 10009, 10010, 10012, 10013, 10014, 10015, 21000,
21001, 21002, 21003, 21004, 21005, 21006, 21008, 21009, 21010, 21011, 21012,
21013, 21014, 21015, 21016, 21017, 25000, 25001, 25002, 25003, 25004, 25005,
25006, 25007, 25008, 25009, 25010, 25011, 25012, 25013, 25014, 25015, 25017,
25018, 25019, 25020, 26000, 27000, 27004, 27306, 28000, 28001, 29200,
2, 3, 1000, 1001, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011, 1012,
1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1026, 1027,
1028, 1029, 1030, 1031, 1032, 1033, 1034, 1035, 1036, 1037, 1038, 1039, 1040,
1041, 1042, 1043, 1046, 1047, 1048, 1049, 1050, 1051, 1052, 1053, 1054, 1055,
1056, 1057, 1058, 1059, 1060, 1062, 1063, 1066, 1067, 1068, 1069, 1070, 1071,
1072, 1073, 1074, 1075, 1076, 1077, 1078, 1079, 1081, 1082, 1083, 1084, 1085,
1086, 1087, 1088, 1089, 1090, 1091, 1092, 1093, 1094, 1095, 2000, 2001, 2002,
2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015,
2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023, 2024, 2026, 2027, 2028, 3000,
3001, 3002, 3003, 3004, 3005, 3006, 3007, 3008, 3009, 3010, 3011, 3013, 4000,
4001, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 4009, 4010, 5000, 5001, 5002,
5003, 5004, 5005, 5006, 5007, 5008, 5009, 5010, 5011, 5012, 5013, 5014, 5015,
5016, 5017, 5018, 5019, 5020, 5021, 5022, 5023, 5024, 5025, 5026, 5027, 5028,
5029, 5033, 5034, 5035, 5036, 5038, 5039, 5040, 5041, 5042, 5043, 5045, 5046,
5047, 5048, 5049, 5050, 5051, 5052, 5053, 5054, 5055, 5056, 5057, 5058, 5059,
5060, 5061, 5062, 5063, 5064, 5065, 6000, 6001, 6003, 6004, 6005, 6006, 6007,
6008, 7000, 7001, 7002, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010, 7011,
7012, 7013, 7014, 7015, 7016, 7017, 7018, 7019, 7020, 7021, 7022, 7023, 7024,
7025, 8000, 8001, 8002, 8003, 8004, 8005, 8006, 8007, 8008, 8009, 8010, 8011,
8012, 8013, 8014, 8015, 8017, 8018, 8019, 8020, 8021, 8022, 8023, 8024, 8025,
8026, 8027, 8028, 8030, 8031, 8032, 8033, 8034, 8035, 8036, 8037, 8038, 8039,
8040, 8041, 8042, 8043, 9000, 9001, 9002, 9003, 9004, 9005, 9007, 9008, 9009,
9010, 9011, 9012, 9013, 9014, 9015, 9016, 9017, 10000, 10001, 10002, 10004,
10005, 10006, 10007, 10008, 10009, 10010, 10012, 10013, 10014, 10015, 21000,
21001, 21002, 21003, 21004, 21005, 21006, 21008, 21009, 21010, 21011, 21012,
21013, 21014, 21015, 21016, 21017, 25000, 25001, 25002, 25003, 25004, 25005,
25006, 25007, 25008, 25009, 25010, 25011, 25012, 25013, 25014, 25015, 25017,
25018, 25019, 25020, 26000, 27000, 27004, 27306, 28000, 28001, 29200,
] as const;
export const shoesGearIds = [
1, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011,
1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024,
1025, 1026, 1027, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2008, 2009, 2010,
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2022, 2023, 2024,
2025, 2026, 2027, 2028, 2029, 2030, 2031, 2032, 2033, 2034, 2035, 2036, 2037,
2038, 2039, 2040, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, 2050, 2051,
2052, 2053, 3000, 3001, 3002, 3003, 3004, 3005, 3006, 3007, 3008, 3009, 3010,
3011, 3012, 3013, 3014, 3015, 3016, 3017, 3018, 3019, 3020, 3021, 3022, 3023,
3024, 3025, 3026, 3027, 3028, 3029, 3030, 3031, 4000, 4001, 4002, 4003, 4007,
4008, 4009, 4010, 4011, 4012, 4013, 4014, 4015, 4016, 4017, 4018, 4019, 4020,
4021, 4022, 4023, 4024, 4025, 4026, 4027, 4028, 5000, 5001, 5002, 6000, 6001,
6002, 6003, 6004, 6005, 6006, 6007, 6008, 6009, 6010, 6011, 6012, 6013, 6014,
6016, 6017, 6018, 6019, 6020, 6021, 6022, 6023, 6024, 6025, 6026, 6027, 6028,
6029, 6030, 7000, 7001, 7002, 7003, 7004, 8000, 8001, 8002, 8003, 8004, 8005,
8006, 8007, 8008, 8009, 8010, 8011, 8013, 8014, 8015, 8016, 21001, 21002,
21003, 21004, 21005, 21006, 21007, 21008, 21009, 23008, 23009, 23010, 23011,
23012, 23013, 23014, 23015, 23016, 25000, 25001, 25002, 25003, 25004, 25005,
25006, 25007, 25008, 25009, 25010, 25011, 25012, 25013, 25014, 25015, 25018,
25019, 25020, 27000, 27004, 27306, 28000, 28001, 29200,
1, 1000, 1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008, 1009, 1010, 1011,
1012, 1013, 1014, 1015, 1016, 1017, 1018, 1019, 1020, 1021, 1022, 1023, 1024,
1025, 1026, 1027, 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2008, 2009, 2010,
2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2022, 2023, 2024,
2025, 2026, 2027, 2028, 2029, 2030, 2031, 2032, 2033, 2034, 2035, 2036, 2037,
2038, 2039, 2040, 2042, 2043, 2044, 2045, 2046, 2047, 2048, 2049, 2050, 2051,
2052, 2053, 3000, 3001, 3002, 3003, 3004, 3005, 3006, 3007, 3008, 3009, 3010,
3011, 3012, 3013, 3014, 3015, 3016, 3017, 3018, 3019, 3020, 3021, 3022, 3023,
3024, 3025, 3026, 3027, 3028, 3029, 3030, 3031, 4000, 4001, 4002, 4003, 4007,
4008, 4009, 4010, 4011, 4012, 4013, 4014, 4015, 4016, 4017, 4018, 4019, 4020,
4021, 4022, 4023, 4024, 4025, 4026, 4027, 4028, 5000, 5001, 5002, 6000, 6001,
6002, 6003, 6004, 6005, 6006, 6007, 6008, 6009, 6010, 6011, 6012, 6013, 6014,
6016, 6017, 6018, 6019, 6020, 6021, 6022, 6023, 6024, 6025, 6026, 6027, 6028,
6029, 6030, 7000, 7001, 7002, 7003, 7004, 8000, 8001, 8002, 8003, 8004, 8005,
8006, 8007, 8008, 8009, 8010, 8011, 8013, 8014, 8015, 8016, 21001, 21002,
21003, 21004, 21005, 21006, 21007, 21008, 21009, 23008, 23009, 23010, 23011,
23012, 23013, 23014, 23015, 23016, 25000, 25001, 25002, 25003, 25004, 25005,
25006, 25007, 25008, 25009, 25010, 25011, 25012, 25013, 25014, 25015, 25018,
25019, 25020, 27000, 27004, 27306, 28000, 28001, 29200,
] as const;

View File

@@ -1,59 +1,59 @@
export { modes, modesShort } from "./modes";
export {
weaponCategories,
mainWeaponIds,
weaponIdToAltId,
weaponIdIsNotAlt,
subWeaponIds,
specialWeaponIds,
exampleMainWeaponIdWithSpecialWeaponId,
nonBombSubWeaponIds,
nonDamagingSpecialWeaponIds,
SPLAT_BOMB_ID,
SUCTION_BOMB_ID,
BURST_BOMB_ID,
SPRINKLER_ID,
SPLASH_WALL_ID,
FIZZY_BOMB_ID,
CURLING_BOMB_ID,
AUTO_BOMB_ID,
SQUID_BEAKON_ID,
POINT_SENSOR_ID,
INK_MINE_ID,
TOXIC_MIST_ID,
ANGLE_SHOOTER_ID,
TORPEDO_ID,
TRIZOOKA_ID,
BIG_BUBBLER_ID,
ZIPCASTER_ID,
TENTA_MISSILES_ID,
INK_STORM_ID,
BOOYAH_BOMB_ID,
WAVE_BREAKER_ID,
INK_VAC_ID,
KILLER_WAIL_ID,
INKJET_ID,
ULTRA_STAMP_ID,
CRAB_TANK_ID,
REEF_SLIDER_ID,
TRIPLE_INKSTRIKE_ID,
TACTICOOLER_ID,
weaponCategories,
mainWeaponIds,
weaponIdToAltId,
weaponIdIsNotAlt,
subWeaponIds,
specialWeaponIds,
exampleMainWeaponIdWithSpecialWeaponId,
nonBombSubWeaponIds,
nonDamagingSpecialWeaponIds,
SPLAT_BOMB_ID,
SUCTION_BOMB_ID,
BURST_BOMB_ID,
SPRINKLER_ID,
SPLASH_WALL_ID,
FIZZY_BOMB_ID,
CURLING_BOMB_ID,
AUTO_BOMB_ID,
SQUID_BEAKON_ID,
POINT_SENSOR_ID,
INK_MINE_ID,
TOXIC_MIST_ID,
ANGLE_SHOOTER_ID,
TORPEDO_ID,
TRIZOOKA_ID,
BIG_BUBBLER_ID,
ZIPCASTER_ID,
TENTA_MISSILES_ID,
INK_STORM_ID,
BOOYAH_BOMB_ID,
WAVE_BREAKER_ID,
INK_VAC_ID,
KILLER_WAIL_ID,
INKJET_ID,
ULTRA_STAMP_ID,
CRAB_TANK_ID,
REEF_SLIDER_ID,
TRIPLE_INKSTRIKE_ID,
TACTICOOLER_ID,
} from "./weapon-ids";
export { headGearIds, clothesGearIds, shoesGearIds } from "./gear-ids";
export { abilitiesShort, abilities } from "./abilities";
export type {
Ability,
AbilityType,
AbilityWithUnknown,
ModeShort,
ModeWithStage,
StageId,
BuildAbilitiesTuple,
BuildAbilitiesTupleWithUnknown,
MainWeaponId,
SubWeaponId,
SpecialWeaponId,
RankedModeShort,
Ability,
AbilityType,
AbilityWithUnknown,
ModeShort,
ModeWithStage,
StageId,
BuildAbilitiesTuple,
BuildAbilitiesTupleWithUnknown,
MainWeaponId,
SubWeaponId,
SpecialWeaponId,
RankedModeShort,
} from "./types";
export { isAbility } from "./utils";
export { stageIds } from "./stage-ids";

View File

@@ -1,11 +1,11 @@
import type { RankedModeShort } from "./types";
export const modes = [
{ short: "TW" },
{ short: "SZ" },
{ short: "TC" },
{ short: "RM" },
{ short: "CB" },
{ short: "TW" },
{ short: "SZ" },
{ short: "TC" },
{ short: "RM" },
{ short: "CB" },
] as const;
export const modesShort = modes.map((mode) => mode.short);

View File

@@ -1,54 +1,54 @@
// when adding new stage id also update the array in create-misc-json.ts and regenerate the jsons
export const stageIds = [
0, // Scorch Gorge
1, // Eeltail Alley
2, // Hagglefish Market
3, // Undertow Spillway
4, // Mincemeat Metalworks
5, // Hammerhead Bridge
6, // Museum d'Alfonsino
7, // Mahi-Mahi Resort
8, // Inkblot Art Academy
9, // Sturgeon Shipyard
10, // MakoMart
11, // Wahoo World
12, // Flounder Heights
13, // Brinewater Springs
14, // Manta Maria
15, // Um'ami Ruins
16, // Humpback Pump Track
17, // Barnacle & Dime
18, // Crableg Capital
19, // Shipshape Cargo Co.
20, // Bluefin Depot
21, // Robo ROM-en
22, // Marlin Airport
23, // Lemuria Hub
0, // Scorch Gorge
1, // Eeltail Alley
2, // Hagglefish Market
3, // Undertow Spillway
4, // Mincemeat Metalworks
5, // Hammerhead Bridge
6, // Museum d'Alfonsino
7, // Mahi-Mahi Resort
8, // Inkblot Art Academy
9, // Sturgeon Shipyard
10, // MakoMart
11, // Wahoo World
12, // Flounder Heights
13, // Brinewater Springs
14, // Manta Maria
15, // Um'ami Ruins
16, // Humpback Pump Track
17, // Barnacle & Dime
18, // Crableg Capital
19, // Shipshape Cargo Co.
20, // Bluefin Depot
21, // Robo ROM-en
22, // Marlin Airport
23, // Lemuria Hub
] as const;
export const stagesObj = {
SCORCH_GORGE: 0,
EELTAIL_ALLEY: 1,
HAGGLEFISH_MARKET: 2,
UNDERTOW_SPILLWAY: 3,
MINCEMEAT_METALWORKS: 4,
HAMMERHEAD_BRIDGE: 5,
MUSEUM_D_ALFONSINO: 6,
MAHI_MAHI_RESORT: 7,
INKBLOT_ART_ACADEMY: 8,
STURGEON_SHIPYARD: 9,
MAKOMART: 10,
WAHOO_WORLD: 11,
FLOUNDER_HEIGHTS: 12,
BRINEWATER_SPRINGS: 13,
MANTA_MARIA: 14,
UM_AMI_RUINS: 15,
HUMPBACK_PUMP_TRACK: 16,
BARNACLE_AND_DIME: 17,
CRABLEG_CAPITAL: 18,
SHIPSHAPE_CARGO_CO: 19,
BLUEFIN_DEPOT: 20,
ROBO_ROM_EN: 21,
MARLIN_AIRPORT: 22,
LEMURIA_HUB: 23,
SCORCH_GORGE: 0,
EELTAIL_ALLEY: 1,
HAGGLEFISH_MARKET: 2,
UNDERTOW_SPILLWAY: 3,
MINCEMEAT_METALWORKS: 4,
HAMMERHEAD_BRIDGE: 5,
MUSEUM_D_ALFONSINO: 6,
MAHI_MAHI_RESORT: 7,
INKBLOT_ART_ACADEMY: 8,
STURGEON_SHIPYARD: 9,
MAKOMART: 10,
WAHOO_WORLD: 11,
FLOUNDER_HEIGHTS: 12,
BRINEWATER_SPRINGS: 13,
MANTA_MARIA: 14,
UM_AMI_RUINS: 15,
HUMPBACK_PUMP_TRACK: 16,
BARNACLE_AND_DIME: 17,
CRABLEG_CAPITAL: 18,
SHIPSHAPE_CARGO_CO: 19,
BLUEFIN_DEPOT: 20,
ROBO_ROM_EN: 21,
MARLIN_AIRPORT: 22,
LEMURIA_HUB: 23,
} as const;

View File

@@ -2,9 +2,9 @@ import type { abilities } from "./abilities";
import type { modes } from "./modes";
import type { stageIds } from "./stage-ids";
import type {
subWeaponIds,
mainWeaponIds,
specialWeaponIds,
mainWeaponIds,
specialWeaponIds,
subWeaponIds,
} from "./weapon-ids";
export type ModeShort = (typeof modes)[number]["short"];
@@ -23,27 +23,27 @@ export type SubWeaponId = (typeof subWeaponIds)[number];
export type SpecialWeaponId = (typeof specialWeaponIds)[number];
export type BuildAbilitiesTuple = [
head: [main: Ability, s1: Ability, s2: Ability, s3: Ability],
clothes: [main: Ability, s1: Ability, s2: Ability, s3: Ability],
shoes: [main: Ability, s1: Ability, s2: Ability, s3: Ability],
head: [main: Ability, s1: Ability, s2: Ability, s3: Ability],
clothes: [main: Ability, s1: Ability, s2: Ability, s3: Ability],
shoes: [main: Ability, s1: Ability, s2: Ability, s3: Ability],
];
export type BuildAbilitiesTupleWithUnknown = [
head: [
main: AbilityWithUnknown,
s1: AbilityWithUnknown,
s2: AbilityWithUnknown,
s3: AbilityWithUnknown,
],
clothes: [
main: AbilityWithUnknown,
s1: AbilityWithUnknown,
s2: AbilityWithUnknown,
s3: AbilityWithUnknown,
],
shoes: [
main: AbilityWithUnknown,
s1: AbilityWithUnknown,
s2: AbilityWithUnknown,
s3: AbilityWithUnknown,
],
head: [
main: AbilityWithUnknown,
s1: AbilityWithUnknown,
s2: AbilityWithUnknown,
s3: AbilityWithUnknown,
],
clothes: [
main: AbilityWithUnknown,
s1: AbilityWithUnknown,
s2: AbilityWithUnknown,
s3: AbilityWithUnknown,
],
shoes: [
main: AbilityWithUnknown,
s1: AbilityWithUnknown,
s2: AbilityWithUnknown,
s3: AbilityWithUnknown,
],
];

View File

@@ -2,5 +2,5 @@ import { abilities } from "./abilities";
import type { Ability } from "./types";
export function isAbility(value: string): value is Ability {
return Boolean(abilities.some((a) => a.name === value));
return Boolean(abilities.some((a) => a.name === value));
}

View File

@@ -1,48 +1,48 @@
import type { MainWeaponId } from "./types";
export const weaponAltNames = new Map<MainWeaponId, string[] | string>()
.set(10, "vjr")
.set(11, "cjr")
.set(21, "nsplash")
.set(41, "ttek")
.set(46, "ttek")
.set(60, "nzap")
.set(61, "nzap")
.set(81, "96d")
.set(91, ["cjs", "cjet"])
.set(220, "rb")
.set(221, ["rb", "crb"])
.set(251, "rpd")
.set(300, "l3")
.set(301, "l3d")
.set(310, "h3")
.set(311, "h3d")
.set(1041, "swex")
.set(1120, "vincent")
.set(1121, "vincent")
.set(2011, "zf")
.set(2021, "zf")
.set(2070, "pencil")
.set(2071, "pencil")
.set(3000, "bucket")
.set(3001, "bucket")
.set(3005, "bucket")
.set(4001, "zimi")
.set(4030, "bp")
.set(4031, "bpn")
.set(4050, "hedit")
.set(4051, "hedit")
.set(5001, "napples")
.set(5030, "vds")
.set(5031, "cds")
.set(5040, "detras")
.set(5041, "letras")
.set(6010, "tent")
.set(6011, "tent")
.set(7010, "bow")
.set(7015, "bow")
.set(8000, ["sword", "chainsaw", "stamp"])
.set(8001, ["sword", "chainsaw", "stamp"])
.set(8005, ["sword", "chainsaw", "stamp"])
.set(8010, ["sword", "vwiper"])
.set(8011, ["sword", "diper", "dwiper"]);
.set(10, "vjr")
.set(11, "cjr")
.set(21, "nsplash")
.set(41, "ttek")
.set(46, "ttek")
.set(60, "nzap")
.set(61, "nzap")
.set(81, "96d")
.set(91, ["cjs", "cjet"])
.set(220, "rb")
.set(221, ["rb", "crb"])
.set(251, "rpd")
.set(300, "l3")
.set(301, "l3d")
.set(310, "h3")
.set(311, "h3d")
.set(1041, "swex")
.set(1120, "vincent")
.set(1121, "vincent")
.set(2011, "zf")
.set(2021, "zf")
.set(2070, "pencil")
.set(2071, "pencil")
.set(3000, "bucket")
.set(3001, "bucket")
.set(3005, "bucket")
.set(4001, "zimi")
.set(4030, "bp")
.set(4031, "bpn")
.set(4050, "hedit")
.set(4051, "hedit")
.set(5001, "napples")
.set(5030, "vds")
.set(5031, "cds")
.set(5040, "detras")
.set(5041, "letras")
.set(6010, "tent")
.set(6011, "tent")
.set(7010, "bow")
.set(7015, "bow")
.set(8000, ["sword", "chainsaw", "stamp"])
.set(8001, ["sword", "chainsaw", "stamp"])
.set(8005, ["sword", "chainsaw", "stamp"])
.set(8010, ["sword", "vwiper"])
.set(8011, ["sword", "diper", "dwiper"]);

View File

@@ -2,111 +2,111 @@ import { assertUnreachable } from "~/utils/types";
import type { MainWeaponId, SpecialWeaponId, SubWeaponId } from "./types";
export const weaponCategories = [
{
name: "SHOOTERS",
weaponIds: [
0, 1, 10, 11, 20, 21, 30, 31, 40, 41, 45, 46, 47, 50, 51, 60, 61, 70, 71,
80, 81, 90, 91, 100, 101, 300, 301, 310, 311, 400, 401,
],
},
{
name: "BLASTERS",
weaponIds: [
200, 201, 205, 210, 211, 220, 221, 230, 231, 240, 241, 250, 251, 260, 261,
],
},
{
name: "ROLLERS",
weaponIds: [
1000, 1001, 1010, 1011, 1015, 1020, 1021, 1030, 1031, 1040, 1041,
],
},
{
name: "BRUSHES",
weaponIds: [1100, 1101, 1110, 1111, 1115, 1121, 1120],
},
{
name: "CHARGERS",
weaponIds: [
2000, 2001, 2010, 2011, 2015, 2020, 2021, 2030, 2031, 2040, 2041, 2050,
2051, 2060, 2061, 2070, 2071,
],
},
{
name: "SLOSHERS",
weaponIds: [
3000, 3001, 3005, 3010, 3011, 3020, 3021, 3030, 3031, 3040, 3041, 3050,
3051,
],
},
{
name: "SPLATLINGS",
weaponIds: [
4000, 4001, 4010, 4011, 4015, 4020, 4021, 4030, 4031, 4040, 4041, 4050,
4051,
],
},
{
name: "DUALIES",
weaponIds: [
5000, 5001, 5010, 5015, 5011, 5020, 5021, 5030, 5031, 5040, 5041, 5050,
5051,
],
},
{
name: "BRELLAS",
weaponIds: [6000, 6001, 6005, 6010, 6011, 6020, 6021, 6030, 6031],
},
{
name: "STRINGERS",
weaponIds: [7010, 7011, 7015, 7020, 7021, 7030, 7031],
},
{
name: "SPLATANAS",
weaponIds: [8000, 8001, 8005, 8010, 8011, 8020, 8021],
},
{
name: "SHOOTERS",
weaponIds: [
0, 1, 10, 11, 20, 21, 30, 31, 40, 41, 45, 46, 47, 50, 51, 60, 61, 70, 71,
80, 81, 90, 91, 100, 101, 300, 301, 310, 311, 400, 401,
],
},
{
name: "BLASTERS",
weaponIds: [
200, 201, 205, 210, 211, 220, 221, 230, 231, 240, 241, 250, 251, 260, 261,
],
},
{
name: "ROLLERS",
weaponIds: [
1000, 1001, 1010, 1011, 1015, 1020, 1021, 1030, 1031, 1040, 1041,
],
},
{
name: "BRUSHES",
weaponIds: [1100, 1101, 1110, 1111, 1115, 1121, 1120],
},
{
name: "CHARGERS",
weaponIds: [
2000, 2001, 2010, 2011, 2015, 2020, 2021, 2030, 2031, 2040, 2041, 2050,
2051, 2060, 2061, 2070, 2071,
],
},
{
name: "SLOSHERS",
weaponIds: [
3000, 3001, 3005, 3010, 3011, 3020, 3021, 3030, 3031, 3040, 3041, 3050,
3051,
],
},
{
name: "SPLATLINGS",
weaponIds: [
4000, 4001, 4010, 4011, 4015, 4020, 4021, 4030, 4031, 4040, 4041, 4050,
4051,
],
},
{
name: "DUALIES",
weaponIds: [
5000, 5001, 5010, 5015, 5011, 5020, 5021, 5030, 5031, 5040, 5041, 5050,
5051,
],
},
{
name: "BRELLAS",
weaponIds: [6000, 6001, 6005, 6010, 6011, 6020, 6021, 6030, 6031],
},
{
name: "STRINGERS",
weaponIds: [7010, 7011, 7015, 7020, 7021, 7030, 7031],
},
{
name: "SPLATANAS",
weaponIds: [8000, 8001, 8005, 8010, 8011, 8020, 8021],
},
] as const;
export const mainWeaponIds = weaponCategories
.flatMap((category) => category.weaponIds)
.sort((a, b) => a - b);
.flatMap((category) => category.weaponIds)
.sort((a, b) => a - b);
export const weaponIdToAltId = new Map<
MainWeaponId,
MainWeaponId | MainWeaponId[]
MainWeaponId,
MainWeaponId | MainWeaponId[]
>([
[40, [45, 47]],
[41, 46],
[200, 205],
[1010, 1015],
[1110, 1115],
[2010, 2015],
[3000, 3005],
[4010, 4015],
[5010, 5015],
[6000, 6005],
[7010, 7015],
[8000, 8005],
[40, [45, 47]],
[41, 46],
[200, 205],
[1010, 1015],
[1110, 1115],
[2010, 2015],
[3000, 3005],
[4010, 4015],
[5010, 5015],
[6000, 6005],
[7010, 7015],
[8000, 8005],
]);
export const altWeaponIdToId = new Map<MainWeaponId, MainWeaponId>([
[45, 40],
[47, 40],
[46, 41],
[205, 200],
[1015, 1010],
[1115, 1110],
[2015, 2010],
[3005, 3000],
[4015, 4010],
[5015, 5010],
[6005, 6000],
[7015, 7010],
[8005, 8000],
[45, 40],
[47, 40],
[46, 41],
[205, 200],
[1015, 1010],
[1115, 1110],
[2015, 2010],
[3005, 3000],
[4015, 4010],
[5015, 5010],
[6005, 6000],
[7015, 7010],
[8005, 8000],
]);
const altWeaponIds = new Set(altWeaponIdToId.keys());
export const weaponIdIsNotAlt = (weaponId: MainWeaponId) =>
!altWeaponIds.has(weaponId);
!altWeaponIds.has(weaponId);
export const SPLAT_BOMB_ID = 0;
export const SUCTION_BOMB_ID = 1;
@@ -124,28 +124,28 @@ export const ANGLE_SHOOTER_ID = 12;
export const TORPEDO_ID = 13;
export const subWeaponIds = [
SPLAT_BOMB_ID,
SUCTION_BOMB_ID,
BURST_BOMB_ID,
SPRINKLER_ID,
SPLASH_WALL_ID,
FIZZY_BOMB_ID,
CURLING_BOMB_ID,
AUTO_BOMB_ID,
SQUID_BEAKON_ID,
POINT_SENSOR_ID,
INK_MINE_ID,
TOXIC_MIST_ID,
ANGLE_SHOOTER_ID,
TORPEDO_ID,
SPLAT_BOMB_ID,
SUCTION_BOMB_ID,
BURST_BOMB_ID,
SPRINKLER_ID,
SPLASH_WALL_ID,
FIZZY_BOMB_ID,
CURLING_BOMB_ID,
AUTO_BOMB_ID,
SQUID_BEAKON_ID,
POINT_SENSOR_ID,
INK_MINE_ID,
TOXIC_MIST_ID,
ANGLE_SHOOTER_ID,
TORPEDO_ID,
] as const;
export const nonBombSubWeaponIds = [
SPRINKLER_ID,
SPLASH_WALL_ID,
SQUID_BEAKON_ID,
POINT_SENSOR_ID,
TOXIC_MIST_ID,
SPRINKLER_ID,
SPLASH_WALL_ID,
SQUID_BEAKON_ID,
POINT_SENSOR_ID,
TOXIC_MIST_ID,
] as SubWeaponId[];
export const TRIZOOKA_ID = 1;
@@ -169,73 +169,73 @@ export const TRIPLE_SPLASHDOWN_ID = 18;
export const SPLATTERCOLOR_SCREEN_ID = 19;
export const specialWeaponIds = [
TRIZOOKA_ID,
BIG_BUBBLER_ID,
ZIPCASTER_ID,
TENTA_MISSILES_ID,
INK_STORM_ID,
BOOYAH_BOMB_ID,
WAVE_BREAKER_ID,
INK_VAC_ID,
KILLER_WAIL_ID,
INKJET_ID,
ULTRA_STAMP_ID,
CRAB_TANK_ID,
REEF_SLIDER_ID,
TRIPLE_INKSTRIKE_ID,
TACTICOOLER_ID,
SUPER_CHUMP_ID,
KRAKEN_ROYALE_ID,
TRIPLE_SPLASHDOWN_ID,
SPLATTERCOLOR_SCREEN_ID,
TRIZOOKA_ID,
BIG_BUBBLER_ID,
ZIPCASTER_ID,
TENTA_MISSILES_ID,
INK_STORM_ID,
BOOYAH_BOMB_ID,
WAVE_BREAKER_ID,
INK_VAC_ID,
KILLER_WAIL_ID,
INKJET_ID,
ULTRA_STAMP_ID,
CRAB_TANK_ID,
REEF_SLIDER_ID,
TRIPLE_INKSTRIKE_ID,
TACTICOOLER_ID,
SUPER_CHUMP_ID,
KRAKEN_ROYALE_ID,
TRIPLE_SPLASHDOWN_ID,
SPLATTERCOLOR_SCREEN_ID,
] as const;
export const nonDamagingSpecialWeaponIds = [BIG_BUBBLER_ID, TACTICOOLER_ID];
export const exampleMainWeaponIdWithSpecialWeaponId = (
specialWeaponId: SpecialWeaponId,
specialWeaponId: SpecialWeaponId,
): MainWeaponId => {
switch (specialWeaponId) {
case TRIZOOKA_ID:
return 40;
case BIG_BUBBLER_ID:
return 10;
case ZIPCASTER_ID:
return 8000;
case TENTA_MISSILES_ID:
return 1030;
case INK_STORM_ID:
return 3040;
case BOOYAH_BOMB_ID:
return 3020;
case WAVE_BREAKER_ID:
return 220;
case INK_VAC_ID:
return 2010;
case KILLER_WAIL_ID:
return 50;
case INKJET_ID:
return 3010;
case ULTRA_STAMP_ID:
return 4000;
case CRAB_TANK_ID:
return 20;
case REEF_SLIDER_ID:
return 5040;
case TRIPLE_INKSTRIKE_ID:
return 41;
case TACTICOOLER_ID:
return 60;
case SUPER_CHUMP_ID:
return 61;
case KRAKEN_ROYALE_ID:
return 4011;
case TRIPLE_SPLASHDOWN_ID:
return 211;
case SPLATTERCOLOR_SCREEN_ID:
return 401;
default: {
assertUnreachable(specialWeaponId);
}
}
switch (specialWeaponId) {
case TRIZOOKA_ID:
return 40;
case BIG_BUBBLER_ID:
return 10;
case ZIPCASTER_ID:
return 8000;
case TENTA_MISSILES_ID:
return 1030;
case INK_STORM_ID:
return 3040;
case BOOYAH_BOMB_ID:
return 3020;
case WAVE_BREAKER_ID:
return 220;
case INK_VAC_ID:
return 2010;
case KILLER_WAIL_ID:
return 50;
case INKJET_ID:
return 3010;
case ULTRA_STAMP_ID:
return 4000;
case CRAB_TANK_ID:
return 20;
case REEF_SLIDER_ID:
return 5040;
case TRIPLE_INKSTRIKE_ID:
return 41;
case TACTICOOLER_ID:
return 60;
case SUPER_CHUMP_ID:
return 61;
case KRAKEN_ROYALE_ID:
return 4011;
case TRIPLE_SPLASHDOWN_ID:
return 211;
case SPLATTERCOLOR_SCREEN_ID:
return 401;
default: {
assertUnreachable(specialWeaponId);
}
}
};

View File

@@ -1,5 +1,5 @@
export const PATREON_INITIAL_URL =
"https://www.patreon.com/api/oauth2/v2/campaigns/2744004/members?include=currently_entitled_tiers,user&fields%5Buser%5D=social_connections&fields%5Btier%5D=created_at";
"https://www.patreon.com/api/oauth2/v2/campaigns/2744004/members?include=currently_entitled_tiers,user&fields%5Buser%5D=social_connections&fields%5Btier%5D=created_at";
// tier 1 lowest, tier 4 highest
export const TIER_1_ID = "6959473";

View File

@@ -1,69 +1,69 @@
import { z } from "zod";
import {
TIER_1_ID,
TIER_2_ID,
TIER_3_ID,
TIER_4_ID,
UNKNOWN_TIER_ID,
TIER_1_ID,
TIER_2_ID,
TIER_3_ID,
TIER_4_ID,
UNKNOWN_TIER_ID,
} from "./constants";
export const patronResponseSchema = z.object({
data: z.array(
z.object({
attributes: z.object({}),
id: z.string(),
relationships: z.object({
currently_entitled_tiers: z.object({
data: z.array(
z.object({
id: z.enum([
TIER_1_ID,
TIER_2_ID,
TIER_3_ID,
TIER_4_ID,
UNKNOWN_TIER_ID,
]),
type: z.string(),
attributes: z
.object({
created_at: z.string(),
})
.nullish(),
}),
),
}),
user: z.object({
data: z.object({ id: z.string(), type: z.string() }),
links: z.object({ related: z.string() }),
}),
}),
type: z.string(),
}),
),
included: z
.array(
z.object({
attributes: z.object({
social_connections: z
.object({
discord: z
.object({
user_id: z.string(),
})
.nullish(),
})
.nullish(),
}),
id: z.string(),
type: z.string(),
}),
)
.nullish(),
links: z.object({ next: z.string() }).nullish(),
meta: z.object({
pagination: z.object({
cursors: z.object({ next: z.string().nullish() }),
total: z.number(),
}),
}),
data: z.array(
z.object({
attributes: z.object({}),
id: z.string(),
relationships: z.object({
currently_entitled_tiers: z.object({
data: z.array(
z.object({
id: z.enum([
TIER_1_ID,
TIER_2_ID,
TIER_3_ID,
TIER_4_ID,
UNKNOWN_TIER_ID,
]),
type: z.string(),
attributes: z
.object({
created_at: z.string(),
})
.nullish(),
}),
),
}),
user: z.object({
data: z.object({ id: z.string(), type: z.string() }),
links: z.object({ related: z.string() }),
}),
}),
type: z.string(),
}),
),
included: z
.array(
z.object({
attributes: z.object({
social_connections: z
.object({
discord: z
.object({
user_id: z.string(),
})
.nullish(),
})
.nullish(),
}),
id: z.string(),
type: z.string(),
}),
)
.nullish(),
links: z.object({ next: z.string() }).nullish(),
meta: z.object({
pagination: z.object({
cursors: z.object({ next: z.string().nullish() }),
total: z.number(),
}),
}),
});

View File

@@ -1,163 +1,154 @@
import type { z } from "zod";
import { MOD_DISCORD_IDS } from "~/constants";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { dateToDatabaseTimestamp } from "~/utils/dates";
import { fetchWithTimeout } from "~/utils/fetch";
import { logger } from "~/utils/logger";
import type { Unpacked } from "~/utils/types";
import {
PATREON_INITIAL_URL,
TIER_1_ID,
TIER_2_ID,
TIER_3_ID,
TIER_4_ID,
UNKNOWN_TIER_ID,
PATREON_INITIAL_URL,
TIER_1_ID,
TIER_2_ID,
TIER_3_ID,
TIER_4_ID,
UNKNOWN_TIER_ID,
} from "./constants";
import { patronResponseSchema } from "./schema";
import * as UserRepository from "~/features/user-page/UserRepository.server";
import { logger } from "~/utils/logger";
import { MOD_DISCORD_IDS } from "~/constants";
export async function updatePatreonData(): Promise<void> {
const patrons: UserRepository.UpdatePatronDataArgs = [];
const noDiscordConnected: Array<string> = [];
const noDataIds: Array<string> = [];
let nextUrlToFetchWith = PATREON_INITIAL_URL;
const patrons: UserRepository.UpdatePatronDataArgs = [];
const noDiscordConnected: Array<string> = [];
const noDataIds: Array<string> = [];
let nextUrlToFetchWith = PATREON_INITIAL_URL;
while (nextUrlToFetchWith) {
const patronData = await fetchPatronData(nextUrlToFetchWith);
while (nextUrlToFetchWith) {
const patronData = await fetchPatronData(nextUrlToFetchWith);
const parsed = parsePatronData(patronData);
patrons.push(...parsed.patrons);
noDiscordConnected.push(...parsed.noDiscordConnection);
noDataIds.push(...parsed.noDataIds);
const parsed = parsePatronData(patronData);
patrons.push(...parsed.patrons);
noDiscordConnected.push(...parsed.noDiscordConnection);
noDataIds.push(...parsed.noDataIds);
// TS freaks out if we don't keep nextUrlToFetchWith string so that's why this weird thing here
nextUrlToFetchWith = patronData.links?.next ?? "";
}
// TS freaks out if we don't keep nextUrlToFetchWith string so that's why this weird thing here
nextUrlToFetchWith = patronData.links?.next ?? "";
}
const patronsWithMods: UserRepository.UpdatePatronDataArgs = [
...patrons,
...MOD_DISCORD_IDS.filter((discordId) =>
patrons.every((p) => p.discordId !== discordId),
).map((discordId) => ({
discordId,
patronTier: 4,
patronSince: dateToDatabaseTimestamp(new Date()),
})),
];
const patronsWithMods: UserRepository.UpdatePatronDataArgs = [
...patrons,
...MOD_DISCORD_IDS.filter((discordId) =>
patrons.every((p) => p.discordId !== discordId),
).map((discordId) => ({
discordId,
patronTier: 4,
patronSince: dateToDatabaseTimestamp(new Date()),
})),
];
await UserRepository.updatePatronData(patronsWithMods);
// eslint-disable-next-line no-console
console.log(
`Added ${patrons.length} patrons. ${
noDiscordConnected.length
} patrons had no Discord connected. No full data for following Patreon ID's: ${noDataIds.join(
", ",
)}`,
);
await UserRepository.updatePatronData(patronsWithMods);
}
async function fetchPatronData(urlToFetch: string) {
if (!process.env["PATREON_ACCESS_TOKEN"]) {
throw new Error("Missing Patreon access token");
}
if (!process.env.PATREON_ACCESS_TOKEN) {
throw new Error("Missing Patreon access token");
}
const response = await fetchWithTimeout(
urlToFetch,
{
headers: {
Authorization: `Bearer ${process.env["PATREON_ACCESS_TOKEN"]}`,
},
},
30_000,
);
const response = await fetchWithTimeout(
urlToFetch,
{
headers: {
Authorization: `Bearer ${process.env.PATREON_ACCESS_TOKEN}`,
},
},
30_000,
);
if (!response.ok) {
throw new Error(
`Patreon response not succesful. Status code was: ${response.status}`,
);
}
if (!response.ok) {
throw new Error(
`Patreon response not succesful. Status code was: ${response.status}`,
);
}
return patronResponseSchema.parse(await response.json());
return patronResponseSchema.parse(await response.json());
}
function parsePatronData({
data,
included,
data,
included,
}: z.infer<typeof patronResponseSchema>) {
const patronsWithIds: Array<
{
patreonId: string;
} & Omit<Unpacked<UserRepository.UpdatePatronDataArgs>, "discordId">
> = [];
const patronsWithIds: Array<
{
patreonId: string;
} & Omit<Unpacked<UserRepository.UpdatePatronDataArgs>, "discordId">
> = [];
for (const patron of data) {
if (patron.relationships.currently_entitled_tiers.data.length === 0) {
continue;
}
for (const patron of data) {
if (patron.relationships.currently_entitled_tiers.data.length === 0) {
continue;
}
const tier = [TIER_4_ID, TIER_3_ID, TIER_2_ID, TIER_1_ID].find((id) =>
patron.relationships.currently_entitled_tiers.data.some(
(tier) => tier.id === id,
),
);
const tier = [TIER_4_ID, TIER_3_ID, TIER_2_ID, TIER_1_ID].find((id) =>
patron.relationships.currently_entitled_tiers.data.some(
(tier) => tier.id === id,
),
);
patronsWithIds.push({
patreonId: patron.relationships.user.data.id,
patronSince: dateToDatabaseTimestamp(
new Date(
patron.relationships.currently_entitled_tiers.data[0].attributes
?.created_at ?? Date.now(),
),
),
patronTier: idToTierNumber(tier),
});
}
patronsWithIds.push({
patreonId: patron.relationships.user.data.id,
patronSince: dateToDatabaseTimestamp(
new Date(
patron.relationships.currently_entitled_tiers.data[0].attributes
?.created_at ?? Date.now(),
),
),
patronTier: idToTierNumber(tier),
});
}
const result: {
patrons: UserRepository.UpdatePatronDataArgs;
noDiscordConnection: Array<string>;
noDataIds: string[];
} = {
patrons: [],
noDiscordConnection: [],
noDataIds: [],
};
for (const extraData of included ?? []) {
if (extraData.type !== "user") continue;
const result: {
patrons: UserRepository.UpdatePatronDataArgs;
noDiscordConnection: Array<string>;
noDataIds: string[];
} = {
patrons: [],
noDiscordConnection: [],
noDataIds: [],
};
for (const extraData of included ?? []) {
if (extraData.type !== "user") continue;
const patronData = patronsWithIds.find((p) => p.patreonId === extraData.id);
if (!patronData) {
result.noDataIds.push(extraData.id);
continue;
}
const patronData = patronsWithIds.find((p) => p.patreonId === extraData.id);
if (!patronData) {
result.noDataIds.push(extraData.id);
continue;
}
const discordId = extraData.attributes.social_connections?.discord?.user_id;
if (!discordId) {
result.noDiscordConnection.push(extraData.id);
continue;
}
const discordId = extraData.attributes.social_connections?.discord?.user_id;
if (!discordId) {
result.noDiscordConnection.push(extraData.id);
continue;
}
result.patrons.push({
patronSince: patronData.patronSince,
discordId,
patronTier: patronData.patronTier,
});
}
result.patrons.push({
patronSince: patronData.patronSince,
discordId,
patronTier: patronData.patronTier,
});
}
return result;
return result;
}
function idToTierNumber(id: string | undefined) {
if (!id || id === UNKNOWN_TIER_ID) {
return null;
}
if (!id || id === UNKNOWN_TIER_ID) {
return null;
}
const tier = [null, TIER_1_ID, TIER_2_ID, TIER_3_ID, TIER_4_ID].indexOf(id);
const tier = [null, TIER_1_ID, TIER_2_ID, TIER_3_ID, TIER_4_ID].indexOf(id);
if (tier === -1) {
logger.warn("Unknown tier for patron", id);
return null;
}
if (tier === -1) {
logger.warn("Unknown tier for patron", id);
return null;
}
return tier;
return tier;
}

View File

@@ -1,107 +1,107 @@
import invariant from "~/utils/invariant";
import { MapPool } from "~/features/map-list-generator/core/map-pool";
import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps";
import invariant from "~/utils/invariant";
import type { ModeShort, StageId } from "../in-game-lists";
import { modesShort } from "../in-game-lists";
import { stagesObj as s } from "../in-game-lists/stage-ids";
export const SENDOUQ_DEFAULT_MAPS: Record<
ModeShort,
[StageId, StageId, StageId, StageId, StageId, StageId, StageId]
ModeShort,
[StageId, StageId, StageId, StageId, StageId, StageId, StageId]
> = {
TW: [
s.EELTAIL_ALLEY,
s.HAGGLEFISH_MARKET,
s.UNDERTOW_SPILLWAY,
s.WAHOO_WORLD,
s.UM_AMI_RUINS,
s.HUMPBACK_PUMP_TRACK,
s.ROBO_ROM_EN,
],
SZ: [
s.HAGGLEFISH_MARKET,
s.MAHI_MAHI_RESORT,
s.INKBLOT_ART_ACADEMY,
s.MAKOMART,
s.HUMPBACK_PUMP_TRACK,
s.CRABLEG_CAPITAL,
s.ROBO_ROM_EN,
],
TC: [
s.ROBO_ROM_EN,
s.EELTAIL_ALLEY,
s.UNDERTOW_SPILLWAY,
s.MUSEUM_D_ALFONSINO,
s.MAKOMART,
s.MANTA_MARIA,
s.SHIPSHAPE_CARGO_CO,
],
RM: [
s.SCORCH_GORGE,
s.HAGGLEFISH_MARKET,
s.UNDERTOW_SPILLWAY,
s.MUSEUM_D_ALFONSINO,
s.FLOUNDER_HEIGHTS,
s.CRABLEG_CAPITAL,
s.MINCEMEAT_METALWORKS,
],
CB: [
s.SCORCH_GORGE,
s.INKBLOT_ART_ACADEMY,
s.BRINEWATER_SPRINGS,
s.MANTA_MARIA,
s.HUMPBACK_PUMP_TRACK,
s.UM_AMI_RUINS,
s.ROBO_ROM_EN,
],
TW: [
s.EELTAIL_ALLEY,
s.HAGGLEFISH_MARKET,
s.UNDERTOW_SPILLWAY,
s.WAHOO_WORLD,
s.UM_AMI_RUINS,
s.HUMPBACK_PUMP_TRACK,
s.ROBO_ROM_EN,
],
SZ: [
s.HAGGLEFISH_MARKET,
s.MAHI_MAHI_RESORT,
s.INKBLOT_ART_ACADEMY,
s.MAKOMART,
s.HUMPBACK_PUMP_TRACK,
s.CRABLEG_CAPITAL,
s.ROBO_ROM_EN,
],
TC: [
s.ROBO_ROM_EN,
s.EELTAIL_ALLEY,
s.UNDERTOW_SPILLWAY,
s.MUSEUM_D_ALFONSINO,
s.MAKOMART,
s.MANTA_MARIA,
s.SHIPSHAPE_CARGO_CO,
],
RM: [
s.SCORCH_GORGE,
s.HAGGLEFISH_MARKET,
s.UNDERTOW_SPILLWAY,
s.MUSEUM_D_ALFONSINO,
s.FLOUNDER_HEIGHTS,
s.CRABLEG_CAPITAL,
s.MINCEMEAT_METALWORKS,
],
CB: [
s.SCORCH_GORGE,
s.INKBLOT_ART_ACADEMY,
s.BRINEWATER_SPRINGS,
s.MANTA_MARIA,
s.HUMPBACK_PUMP_TRACK,
s.UM_AMI_RUINS,
s.ROBO_ROM_EN,
],
};
for (const mode of modesShort) {
invariant(
SENDOUQ_DEFAULT_MAPS[mode].length ===
new Set(SENDOUQ_DEFAULT_MAPS[mode]).size,
"Duplicate maps in SENDOUQ_DEFAULT_MAPS",
);
invariant(
SENDOUQ_DEFAULT_MAPS[mode].length ===
new Set(SENDOUQ_DEFAULT_MAPS[mode]).size,
"Duplicate maps in SENDOUQ_DEFAULT_MAPS",
);
invariant(
BANNED_MAPS[mode].every(
(stageId) => !SENDOUQ_DEFAULT_MAPS[mode].includes(stageId),
),
`Banned maps in the default map pool of ${mode}`,
);
invariant(
BANNED_MAPS[mode].every(
(stageId) => !SENDOUQ_DEFAULT_MAPS[mode].includes(stageId),
),
`Banned maps in the default map pool of ${mode}`,
);
}
export const sourceTypes = [
"DEFAULT",
"TIEBREAKER",
"BOTH",
"TO",
"COUNTERPICK",
"DEFAULT",
"TIEBREAKER",
"BOTH",
"TO",
"COUNTERPICK",
] as const;
// this is only used as a fallback, in the case that map list generation has a bug
export const DEFAULT_MAP_POOL = new MapPool([
{ mode: "SZ", stageId: 6 },
{ mode: "SZ", stageId: 8 },
{ mode: "SZ", stageId: 9 },
{ mode: "SZ", stageId: 15 },
{ mode: "SZ", stageId: 17 },
{ mode: "SZ", stageId: 6 },
{ mode: "SZ", stageId: 8 },
{ mode: "SZ", stageId: 9 },
{ mode: "SZ", stageId: 15 },
{ mode: "SZ", stageId: 17 },
{ mode: "TC", stageId: 1 },
{ mode: "TC", stageId: 2 },
{ mode: "TC", stageId: 10 },
{ mode: "TC", stageId: 14 },
{ mode: "TC", stageId: 16 },
{ mode: "TC", stageId: 1 },
{ mode: "TC", stageId: 2 },
{ mode: "TC", stageId: 10 },
{ mode: "TC", stageId: 14 },
{ mode: "TC", stageId: 16 },
{ mode: "RM", stageId: 0 },
{ mode: "RM", stageId: 3 },
{ mode: "RM", stageId: 9 },
{ mode: "RM", stageId: 10 },
{ mode: "RM", stageId: 17 },
{ mode: "RM", stageId: 0 },
{ mode: "RM", stageId: 3 },
{ mode: "RM", stageId: 9 },
{ mode: "RM", stageId: 10 },
{ mode: "RM", stageId: 17 },
{ mode: "CB", stageId: 0 },
{ mode: "CB", stageId: 1 },
{ mode: "CB", stageId: 8 },
{ mode: "CB", stageId: 14 },
{ mode: "CB", stageId: 16 },
{ mode: "CB", stageId: 0 },
{ mode: "CB", stageId: 1 },
{ mode: "CB", stageId: 8 },
{ mode: "CB", stageId: 14 },
{ mode: "CB", stageId: 16 },
]);

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
export { createTournamentMapList } from "./tournament-map-list";
export type {
BracketType,
TournamentMaplistInput,
TournamentMaplistSource,
TournamentMapListMap,
BracketType,
TournamentMaplistInput,
TournamentMaplistSource,
TournamentMapListMap,
} from "./types";
export { sourceTypes } from "./constants";
export { seededRandom } from "./utils";

View File

@@ -4,77 +4,77 @@
import { logger } from "~/utils/logger";
import {
seededRandom,
type TournamentMapListMap,
type TournamentMaplistInput,
type TournamentMapListMap,
type TournamentMaplistInput,
seededRandom,
} from ".";
import { type ModeWithStage, modesShort, stageIds } from "../in-game-lists";
type StarterMapArgs = Pick<
TournamentMaplistInput,
"modesIncluded" | "tiebreakerMaps" | "seed" | "teams"
TournamentMaplistInput,
"modesIncluded" | "tiebreakerMaps" | "seed" | "teams"
>;
export function starterMap(args: StarterMapArgs): Array<TournamentMapListMap> {
const { shuffle } = seededRandom(args.seed);
const { shuffle } = seededRandom(args.seed);
const commonMap = resolveRandomCommonMap(args.teams, shuffle);
if (commonMap) {
return [{ ...commonMap, source: "BOTH" }];
}
const commonMap = resolveRandomCommonMap(args.teams, shuffle);
if (commonMap) {
return [{ ...commonMap, source: "BOTH" }];
}
if (!args.tiebreakerMaps.isEmpty()) {
const randomTiebreaker = shuffle(args.tiebreakerMaps.stageModePairs)[0];
if (!args.tiebreakerMaps.isEmpty()) {
const randomTiebreaker = shuffle(args.tiebreakerMaps.stageModePairs)[0];
return [
{
mode: randomTiebreaker.mode,
stageId: randomTiebreaker.stageId,
source: "TIEBREAKER",
},
];
}
return [
{
mode: randomTiebreaker.mode,
stageId: randomTiebreaker.stageId,
source: "TIEBREAKER",
},
];
}
// should be only one mode here always but just in case
// making it capable of handling many modes too
const allAvailableMaps = shuffle(
args.modesIncluded
.sort((a, b) => modesShort.indexOf(a) - modesShort.indexOf(b))
.flatMap((mode) => stageIds.map((stageId) => ({ mode, stageId }))),
);
// should be only one mode here always but just in case
// making it capable of handling many modes too
const allAvailableMaps = shuffle(
args.modesIncluded
.sort((a, b) => modesShort.indexOf(a) - modesShort.indexOf(b))
.flatMap((mode) => stageIds.map((stageId) => ({ mode, stageId }))),
);
for (const map of allAvailableMaps) {
if (
!args.teams.some((team) =>
team.maps.stageModePairs.some(
(teamMap) =>
teamMap.mode === map.mode && teamMap.stageId === map.stageId,
),
)
) {
return [{ ...map, source: "DEFAULT" }];
}
}
for (const map of allAvailableMaps) {
if (
!args.teams.some((team) =>
team.maps.stageModePairs.some(
(teamMap) =>
teamMap.mode === map.mode && teamMap.stageId === map.stageId,
),
)
) {
return [{ ...map, source: "DEFAULT" }];
}
}
logger.warn("startedMap: fallback choice");
logger.warn("startedMap: fallback choice");
return [{ ...allAvailableMaps[0], source: "DEFAULT" }];
return [{ ...allAvailableMaps[0], source: "DEFAULT" }];
}
function resolveRandomCommonMap(
teams: StarterMapArgs["teams"],
shuffle: <T>(o: T[]) => T[],
teams: StarterMapArgs["teams"],
shuffle: <T>(o: T[]) => T[],
): ModeWithStage | null {
const teamOnePicks = shuffle(teams[0].maps.stageModePairs);
const teamTwoPicks = shuffle(teams[1].maps.stageModePairs);
const teamOnePicks = shuffle(teams[0].maps.stageModePairs);
const teamTwoPicks = shuffle(teams[1].maps.stageModePairs);
for (const map of teamOnePicks) {
for (const map2 of teamTwoPicks) {
if (map.mode === map2.mode && map.stageId === map2.stageId) {
return map;
}
}
}
for (const map of teamOnePicks) {
for (const map2 of teamTwoPicks) {
if (map.mode === map2.mode && map.stageId === map2.stageId) {
return map;
}
}
}
return null;
return null;
}

View File

@@ -2,9 +2,9 @@ import invariant from "~/utils/invariant";
import { type ModeShort, type StageId, stageIds } from "../in-game-lists";
import { DEFAULT_MAP_POOL } from "./constants";
import type {
TournamentMaplistInput,
TournamentMapListMap,
TournamentMaplistSource,
TournamentMapListMap,
TournamentMaplistInput,
TournamentMaplistSource,
} from "./types";
import { seededRandom } from "./utils";
@@ -13,404 +13,403 @@ type ModeWithStageAndScore = TournamentMapListMap & { score: number };
const OPTIMAL_MAPLIST_SCORE = 0;
export function createTournamentMapList(
input: TournamentMaplistInput,
input: TournamentMaplistInput,
): Array<TournamentMapListMap> {
validateInput(input);
const { shuffle } = seededRandom(input.seed);
const stages = shuffle(resolveCommonStages());
const mapList: Array<ModeWithStageAndScore & { score: number }> = [];
const bestMapList: { maps?: Array<ModeWithStageAndScore>; score: number } = {
score: Infinity,
};
const usedStages = new Set<number>();
const backtrack = () => {
invariant(mapList.length <= input.count, "mapList.length > input.count");
const mapListScore = rateMapList();
if (typeof mapListScore === "number" && mapListScore < bestMapList.score) {
bestMapList.maps = [...mapList];
bestMapList.score = mapListScore;
}
// There can't be better map list than this
if (bestMapList.score === OPTIMAL_MAPLIST_SCORE) {
return;
}
const stageList =
mapList.length < input.count - 1 || input.tiebreakerMaps.length === 0
? resolveStages()
: input.tiebreakerMaps.stageModePairs.map((p) => ({
...p,
score: 0,
source: "TIEBREAKER" as const,
}));
for (const [i, stage] of stageList.entries()) {
if (!stageIsOk(stage, i)) continue;
mapList.push(stage);
usedStages.add(i);
backtrack();
usedStages.delete(i);
mapList.pop();
}
};
backtrack();
if (bestMapList.maps) return bestMapList.maps;
throw new Error("couldn't generate maplist");
function resolveCommonStages() {
const sorted = input.teams
.slice()
.sort((a, b) => a.id - b.id) as TournamentMaplistInput["teams"];
const result = sorted[0].maps.stageModePairs.map((pair) => ({
...pair,
score: 1,
source: sorted[0].id as TournamentMaplistSource,
}));
for (const stage of sorted[1].maps.stageModePairs) {
const alreadyIncludedStage = result.find(
(alreadyIncludedStage) =>
alreadyIncludedStage.stageId === stage.stageId &&
alreadyIncludedStage.mode === stage.mode,
);
if (alreadyIncludedStage) {
alreadyIncludedStage.score = 0;
alreadyIncludedStage.source = "BOTH";
} else {
result.push({ ...stage, score: -1, source: sorted[1].id });
}
}
if (
input.teams[0].maps.stages.length === 0 &&
input.teams[1].maps.stages.length === 0
) {
// neither team submitted map, we go default
result.push(
...getDefaultMapPool().map((pair) => ({
...pair,
score: 0,
source: "DEFAULT" as const,
})),
);
} else if (
input.teams[0].maps.stages.length === 0 ||
input.teams[1].maps.stages.length === 0
) {
// let's set it up for later that if one team doesn't have stages set
// we can make a maplist consisting of only stages from the team that did submit
for (const stageObj of result) {
stageObj.score = 0;
}
}
return result.sort((a, b) =>
`${a.stageId}-${a.mode}`.localeCompare(`${b.stageId}-${b.mode}`),
);
}
function resolveStages() {
if (utilizeOtherStageIdsWhenNoTiebreaker()) {
// no overlap so we need to use a random map for tiebreaker
if (tournamentIsOneModeOnly()) {
return shuffle([...stageIds])
.filter(
(stageId) =>
!input.teams[0].maps.hasStage(stageId) &&
!input.teams[1].maps.hasStage(stageId),
)
.map((stageId) => ({
stageId,
mode: input.modesIncluded[0],
score: 0,
source: "TIEBREAKER" as const,
}));
} else {
return DEFAULT_MAP_POOL.stageModePairs
.filter(
(pair) =>
!input.teams[0].maps.has(pair) && !input.teams[1].maps.has(pair),
)
.map((pair) => ({
stageId: pair.stageId,
mode: pair.mode,
score: 0,
source: "TIEBREAKER" as const,
}));
}
}
return stages;
}
function validateInput(input: TournamentMaplistInput) {
invariant(
input.teams.every((t) =>
t.maps.stageModePairs.every((pair) =>
input.modesIncluded.includes(pair.mode),
),
),
"Maps submitted for modes not included in the tournament",
);
for (const team of input.teams) {
const stringified = team.maps.stageModePairs.map(
(p) => `${p.stageId}-${p.mode}`,
);
const unique = new Set(stringified);
invariant(
unique.size === stringified.length,
`Duplicate maps in map pool (team ${team.id})`,
);
}
}
function utilizeOtherStageIdsWhenNoTiebreaker() {
if (mapList.length < input.count - 1) return false;
if (
input.teams.every((team) => !team.maps.isEmpty()) &&
!input.teams[0].maps.overlaps(input.teams[1].maps)
) {
return true;
}
const teamsMapsLeftNotPicked =
[...input.teams[0].maps, ...input.teams[1].maps].filter(
(stage) =>
!mapList.some(
(map) => map.stageId === stage.stageId && map.mode === stage.mode,
),
).length > 0;
if (!teamsMapsLeftNotPicked) return true;
return false;
}
function getDefaultMapPool() {
if (tournamentIsOneModeOnly()) {
const mode = input.modesIncluded[0];
return stageIds.map((id) => ({ mode, stageId: id }));
}
return DEFAULT_MAP_POOL.stageModePairs.filter(({ mode }) =>
input.modesIncluded.includes(mode),
);
}
type StageValidatorInput = Pick<
ModeWithStageAndScore,
"score" | "stageId" | "mode" | "source"
>;
// adding rules here can achieve to things
// 1) adjust what kind of map list is generated
// 2) optimize the algorithm my eliminating subtrees from consideration
function stageIsOk(stage: StageValidatorInput, index: number) {
if (usedStages.has(index)) return false;
if (mapListAlreadyFull()) return false;
if (isEarlyModeRepeat(stage)) return false;
if (isNotFollowingModePattern(stage)) return false;
if (isMakingThingsUnfair(stage)) return false;
if (isStageRepeatWithoutBreak(stage)) return false;
if (isSecondPickBySameTeamInRow(stage)) return false;
if (wouldPreventTiebreaker(stage)) return false;
return true;
}
function tournamentIsOneModeOnly() {
return input.modesIncluded.length === 1;
}
function mapListAlreadyFull() {
return mapList.length === input.count;
}
function isEarlyModeRepeat(stage: StageValidatorInput) {
if (tournamentIsOneModeOnly()) return false;
// all modes already appeared
if (mapList.length >= input.modesIncluded.length) return false;
if (
mapList.some(
(alreadyIncludedStage) => alreadyIncludedStage.mode === stage.mode,
)
) {
return true;
}
return false;
}
function isNotFollowingModePattern(stage: StageValidatorInput) {
if (tournamentIsOneModeOnly()) return false;
if (input.followModeOrder) {
return isNotFollowingModeOrder(stage);
}
// not all modes appeared yet
if (mapList.length < input.modesIncluded.length) return false;
let previousModeShouldBe: ModeShort | undefined;
for (let i = 0; i < mapList.length; i++) {
if (mapList[i].mode === stage.mode) {
if (i === 0) {
previousModeShouldBe = mapList[mapList.length - 1].mode;
} else {
previousModeShouldBe = mapList[i - 1].mode;
}
}
}
if (!previousModeShouldBe) return false;
return mapList[mapList.length - 1].mode !== previousModeShouldBe;
}
function isNotFollowingModeOrder(stage: StageValidatorInput) {
let currentIndex = 0;
for (let i = 0; i < mapList.length; i++) {
currentIndex++;
if (currentIndex === input.modesIncluded.length) currentIndex = 0;
}
return stage.mode !== input.modesIncluded[currentIndex];
}
// don't allow making two picks from one team in row
function isMakingThingsUnfair(stage: StageValidatorInput) {
// allow to handle edge case where both teams have 100% overlap in one mode
// but not others in Bo5 for example could make impossible to make map because e.g.
// 1) overlap
// 2) team 1 pick
// 3) team 2 pick
// 4) team 1 pick
// 5) TIEBREAKER <- system has to allow
// ---
// but later we will with score make sure that we exhaust better options too
if (stage.source === "TIEBREAKER") return false;
const score = mapList.reduce((acc, cur) => acc + cur.score, 0);
const newScore = score + stage.score;
if (score !== 0 && newScore !== 0) return true;
if (newScore !== 0 && mapList.length + 1 === input.count) return true;
return false;
}
function isStageRepeatWithoutBreak(stage: StageValidatorInput) {
const lastStage = mapList[mapList.length - 1];
if (!lastStage) return false;
return lastStage.stageId === stage.stageId;
}
function isSecondPickBySameTeamInRow(stage: StageValidatorInput) {
const lastStage = mapList[mapList.length - 1];
if (!lastStage) return false;
if (stage.score === 0) return false;
return lastStage.score === stage.score;
}
function wouldPreventTiebreaker(stage: StageValidatorInput) {
// tiebreaker always guaranteed if maps are explicitly set
if (input.tiebreakerMaps.length > 0) return false;
const commonMaps = input.teams[0].maps.stageModePairs.filter(
({ stageId, mode }) =>
input.teams[1].maps.stageModePairs.some(
(pair) => pair.stageId === stageId && pair.mode === mode,
),
);
const newMapList = [...mapList, stage];
const newCommonMaps = commonMaps.filter(
({ stageId, mode }) =>
!newMapList.some(
(pair) => pair.stageId === stageId && pair.mode === mode,
),
);
// there was at least one possible common map
// to pick as tiebreaker but it (or they) got picked too early
return (
commonMaps.length > 0 &&
// handles special case where both teams have the same maps in their pool
commonMaps.length !== input.teams[0].maps.stageModePairs.length &&
newCommonMaps.length === 0 &&
newMapList.length !== input.count
);
}
function rateMapList() {
// not a full map list
if (mapList.length !== input.count) return;
let score = OPTIMAL_MAPLIST_SCORE;
const appearedMaps = new Map<StageId, number>();
for (const stage of mapList) {
const timesAppeared = appearedMaps.get(stage.stageId) ?? 0;
if (timesAppeared > 0) {
score += timesAppeared;
}
appearedMaps.set(stage.stageId, timesAppeared + 1);
}
if (!lastMapIsAGoodTieBreaker()) {
score += 1;
}
const scoreSum = mapList.reduce((acc, cur) => acc + cur.score, 0);
if (scoreSum !== 0) {
score += 100;
}
return score;
}
function lastMapIsAGoodTieBreaker() {
// guaranteed to be good if more than one mode
if (!tournamentIsOneModeOnly()) return true;
// specifically made tiebreaker map is considered good
const last = mapList[mapList.length - 1];
if (last.source === "TIEBREAKER") return true;
// we can't have a map from pools of both teams if both didn't submit maps
if (input.teams.some((team) => team.maps.stageModePairs.length === 0)) {
return true;
}
const tieBreakerMap = mapList[mapList.length - 1];
validateInput(input);
const { shuffle } = seededRandom(input.seed);
const stages = shuffle(resolveCommonStages());
const mapList: Array<ModeWithStageAndScore & { score: number }> = [];
const bestMapList: { maps?: Array<ModeWithStageAndScore>; score: number } = {
score: Number.POSITIVE_INFINITY,
};
const usedStages = new Set<number>();
const backtrack = () => {
invariant(mapList.length <= input.count, "mapList.length > input.count");
const mapListScore = rateMapList();
if (typeof mapListScore === "number" && mapListScore < bestMapList.score) {
bestMapList.maps = [...mapList];
bestMapList.score = mapListScore;
}
// There can't be better map list than this
if (bestMapList.score === OPTIMAL_MAPLIST_SCORE) {
return;
}
const stageList =
mapList.length < input.count - 1 || input.tiebreakerMaps.length === 0
? resolveStages()
: input.tiebreakerMaps.stageModePairs.map((p) => ({
...p,
score: 0,
source: "TIEBREAKER" as const,
}));
for (const [i, stage] of stageList.entries()) {
if (!stageIsOk(stage, i)) continue;
mapList.push(stage);
usedStages.add(i);
backtrack();
usedStages.delete(i);
mapList.pop();
}
};
backtrack();
if (bestMapList.maps) return bestMapList.maps;
throw new Error("couldn't generate maplist");
function resolveCommonStages() {
const sorted = input.teams
.slice()
.sort((a, b) => a.id - b.id) as TournamentMaplistInput["teams"];
const result = sorted[0].maps.stageModePairs.map((pair) => ({
...pair,
score: 1,
source: sorted[0].id as TournamentMaplistSource,
}));
for (const stage of sorted[1].maps.stageModePairs) {
const alreadyIncludedStage = result.find(
(alreadyIncludedStage) =>
alreadyIncludedStage.stageId === stage.stageId &&
alreadyIncludedStage.mode === stage.mode,
);
if (alreadyIncludedStage) {
alreadyIncludedStage.score = 0;
alreadyIncludedStage.source = "BOTH";
} else {
result.push({ ...stage, score: -1, source: sorted[1].id });
}
}
if (
input.teams[0].maps.stages.length === 0 &&
input.teams[1].maps.stages.length === 0
) {
// neither team submitted map, we go default
result.push(
...getDefaultMapPool().map((pair) => ({
...pair,
score: 0,
source: "DEFAULT" as const,
})),
);
} else if (
input.teams[0].maps.stages.length === 0 ||
input.teams[1].maps.stages.length === 0
) {
// let's set it up for later that if one team doesn't have stages set
// we can make a maplist consisting of only stages from the team that did submit
for (const stageObj of result) {
stageObj.score = 0;
}
}
return result.sort((a, b) =>
`${a.stageId}-${a.mode}`.localeCompare(`${b.stageId}-${b.mode}`),
);
}
function resolveStages() {
if (utilizeOtherStageIdsWhenNoTiebreaker()) {
// no overlap so we need to use a random map for tiebreaker
if (tournamentIsOneModeOnly()) {
return shuffle([...stageIds])
.filter(
(stageId) =>
!input.teams[0].maps.hasStage(stageId) &&
!input.teams[1].maps.hasStage(stageId),
)
.map((stageId) => ({
stageId,
mode: input.modesIncluded[0],
score: 0,
source: "TIEBREAKER" as const,
}));
}
return DEFAULT_MAP_POOL.stageModePairs
.filter(
(pair) =>
!input.teams[0].maps.has(pair) && !input.teams[1].maps.has(pair),
)
.map((pair) => ({
stageId: pair.stageId,
mode: pair.mode,
score: 0,
source: "TIEBREAKER" as const,
}));
}
return stages;
}
function validateInput(input: TournamentMaplistInput) {
invariant(
input.teams.every((t) =>
t.maps.stageModePairs.every((pair) =>
input.modesIncluded.includes(pair.mode),
),
),
"Maps submitted for modes not included in the tournament",
);
for (const team of input.teams) {
const stringified = team.maps.stageModePairs.map(
(p) => `${p.stageId}-${p.mode}`,
);
const unique = new Set(stringified);
invariant(
unique.size === stringified.length,
`Duplicate maps in map pool (team ${team.id})`,
);
}
}
function utilizeOtherStageIdsWhenNoTiebreaker() {
if (mapList.length < input.count - 1) return false;
if (
input.teams.every((team) => !team.maps.isEmpty()) &&
!input.teams[0].maps.overlaps(input.teams[1].maps)
) {
return true;
}
const teamsMapsLeftNotPicked =
[...input.teams[0].maps, ...input.teams[1].maps].filter(
(stage) =>
!mapList.some(
(map) => map.stageId === stage.stageId && map.mode === stage.mode,
),
).length > 0;
if (!teamsMapsLeftNotPicked) return true;
return false;
}
function getDefaultMapPool() {
if (tournamentIsOneModeOnly()) {
const mode = input.modesIncluded[0];
return stageIds.map((id) => ({ mode, stageId: id }));
}
return DEFAULT_MAP_POOL.stageModePairs.filter(({ mode }) =>
input.modesIncluded.includes(mode),
);
}
type StageValidatorInput = Pick<
ModeWithStageAndScore,
"score" | "stageId" | "mode" | "source"
>;
// adding rules here can achieve to things
// 1) adjust what kind of map list is generated
// 2) optimize the algorithm my eliminating subtrees from consideration
function stageIsOk(stage: StageValidatorInput, index: number) {
if (usedStages.has(index)) return false;
if (mapListAlreadyFull()) return false;
if (isEarlyModeRepeat(stage)) return false;
if (isNotFollowingModePattern(stage)) return false;
if (isMakingThingsUnfair(stage)) return false;
if (isStageRepeatWithoutBreak(stage)) return false;
if (isSecondPickBySameTeamInRow(stage)) return false;
if (wouldPreventTiebreaker(stage)) return false;
return true;
}
function tournamentIsOneModeOnly() {
return input.modesIncluded.length === 1;
}
function mapListAlreadyFull() {
return mapList.length === input.count;
}
function isEarlyModeRepeat(stage: StageValidatorInput) {
if (tournamentIsOneModeOnly()) return false;
// all modes already appeared
if (mapList.length >= input.modesIncluded.length) return false;
if (
mapList.some(
(alreadyIncludedStage) => alreadyIncludedStage.mode === stage.mode,
)
) {
return true;
}
return false;
}
function isNotFollowingModePattern(stage: StageValidatorInput) {
if (tournamentIsOneModeOnly()) return false;
if (input.followModeOrder) {
return isNotFollowingModeOrder(stage);
}
// not all modes appeared yet
if (mapList.length < input.modesIncluded.length) return false;
let previousModeShouldBe: ModeShort | undefined;
for (let i = 0; i < mapList.length; i++) {
if (mapList[i].mode === stage.mode) {
if (i === 0) {
previousModeShouldBe = mapList[mapList.length - 1].mode;
} else {
previousModeShouldBe = mapList[i - 1].mode;
}
}
}
if (!previousModeShouldBe) return false;
return mapList[mapList.length - 1].mode !== previousModeShouldBe;
}
function isNotFollowingModeOrder(stage: StageValidatorInput) {
let currentIndex = 0;
for (let i = 0; i < mapList.length; i++) {
currentIndex++;
if (currentIndex === input.modesIncluded.length) currentIndex = 0;
}
return stage.mode !== input.modesIncluded[currentIndex];
}
// don't allow making two picks from one team in row
function isMakingThingsUnfair(stage: StageValidatorInput) {
// allow to handle edge case where both teams have 100% overlap in one mode
// but not others in Bo5 for example could make impossible to make map because e.g.
// 1) overlap
// 2) team 1 pick
// 3) team 2 pick
// 4) team 1 pick
// 5) TIEBREAKER <- system has to allow
// ---
// but later we will with score make sure that we exhaust better options too
if (stage.source === "TIEBREAKER") return false;
const score = mapList.reduce((acc, cur) => acc + cur.score, 0);
const newScore = score + stage.score;
if (score !== 0 && newScore !== 0) return true;
if (newScore !== 0 && mapList.length + 1 === input.count) return true;
return false;
}
function isStageRepeatWithoutBreak(stage: StageValidatorInput) {
const lastStage = mapList[mapList.length - 1];
if (!lastStage) return false;
return lastStage.stageId === stage.stageId;
}
function isSecondPickBySameTeamInRow(stage: StageValidatorInput) {
const lastStage = mapList[mapList.length - 1];
if (!lastStage) return false;
if (stage.score === 0) return false;
return lastStage.score === stage.score;
}
function wouldPreventTiebreaker(stage: StageValidatorInput) {
// tiebreaker always guaranteed if maps are explicitly set
if (input.tiebreakerMaps.length > 0) return false;
const commonMaps = input.teams[0].maps.stageModePairs.filter(
({ stageId, mode }) =>
input.teams[1].maps.stageModePairs.some(
(pair) => pair.stageId === stageId && pair.mode === mode,
),
);
const newMapList = [...mapList, stage];
const newCommonMaps = commonMaps.filter(
({ stageId, mode }) =>
!newMapList.some(
(pair) => pair.stageId === stageId && pair.mode === mode,
),
);
// there was at least one possible common map
// to pick as tiebreaker but it (or they) got picked too early
return (
commonMaps.length > 0 &&
// handles special case where both teams have the same maps in their pool
commonMaps.length !== input.teams[0].maps.stageModePairs.length &&
newCommonMaps.length === 0 &&
newMapList.length !== input.count
);
}
function rateMapList() {
// not a full map list
if (mapList.length !== input.count) return;
let score = OPTIMAL_MAPLIST_SCORE;
const appearedMaps = new Map<StageId, number>();
for (const stage of mapList) {
const timesAppeared = appearedMaps.get(stage.stageId) ?? 0;
if (timesAppeared > 0) {
score += timesAppeared;
}
appearedMaps.set(stage.stageId, timesAppeared + 1);
}
if (!lastMapIsAGoodTieBreaker()) {
score += 1;
}
const scoreSum = mapList.reduce((acc, cur) => acc + cur.score, 0);
if (scoreSum !== 0) {
score += 100;
}
return score;
}
function lastMapIsAGoodTieBreaker() {
// guaranteed to be good if more than one mode
if (!tournamentIsOneModeOnly()) return true;
// specifically made tiebreaker map is considered good
const last = mapList[mapList.length - 1];
if (last.source === "TIEBREAKER") return true;
// we can't have a map from pools of both teams if both didn't submit maps
if (input.teams.some((team) => team.maps.stageModePairs.length === 0)) {
return true;
}
const tieBreakerMap = mapList[mapList.length - 1];
let appearanceCount = 0;
for (const team of input.teams) {
for (const stage of team.maps.stages) {
if (stage === tieBreakerMap.stageId) appearanceCount++;
}
}
return appearanceCount === 2;
}
let appearanceCount = 0;
for (const team of input.teams) {
for (const stage of team.maps.stages) {
if (stage === tieBreakerMap.stageId) appearanceCount++;
}
}
return appearanceCount === 2;
}
}

View File

@@ -3,33 +3,33 @@ import type { ModeShort, ModeWithStage } from "../in-game-lists";
import type { sourceTypes } from "./constants";
export type BracketType =
| "GROUPS"
| "SE"
| "DE_WINNERS"
| "DE_LOSERS"
| "SWISS";
| "GROUPS"
| "SE"
| "DE_WINNERS"
| "DE_LOSERS"
| "SWISS";
export interface TournamentMaplistInput {
count: number;
seed: string;
teams: [
{
id: number;
maps: MapPool;
},
{
id: number;
maps: MapPool;
},
];
tiebreakerMaps: MapPool;
modesIncluded: ModeShort[];
followModeOrder?: boolean;
count: number;
seed: string;
teams: [
{
id: number;
maps: MapPool;
},
{
id: number;
maps: MapPool;
},
];
tiebreakerMaps: MapPool;
modesIncluded: ModeShort[];
followModeOrder?: boolean;
}
export type TournamentMaplistSource = number | (typeof sourceTypes)[number];
export type TournamentMapListMap = ModeWithStage & {
source: TournamentMaplistSource;
bannedByTournamentTeamId?: number;
source: TournamentMaplistSource;
bannedByTournamentTeamId?: number;
};

View File

@@ -1,64 +1,69 @@
// https://stackoverflow.com/a/68523152
function cyrb128(str: string) {
let h1 = 1779033703,
h2 = 3144134277,
h3 = 1013904242,
h4 = 2773480762;
for (let i = 0, k; i < str.length; i++) {
k = str.charCodeAt(i);
h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
}
h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
return [
(h1 ^ h2 ^ h3 ^ h4) >>> 0,
(h2 ^ h1) >>> 0,
(h3 ^ h1) >>> 0,
(h4 ^ h1) >>> 0,
];
let h1 = 1779033703;
let h2 = 3144134277;
let h3 = 1013904242;
let h4 = 2773480762;
// biome-ignore lint/suspicious/noImplicitAnyLet: biome migration
for (let i = 0, k; i < str.length; i++) {
k = str.charCodeAt(i);
h1 = h2 ^ Math.imul(h1 ^ k, 597399067);
h2 = h3 ^ Math.imul(h2 ^ k, 2869860233);
h3 = h4 ^ Math.imul(h3 ^ k, 951274213);
h4 = h1 ^ Math.imul(h4 ^ k, 2716044179);
}
h1 = Math.imul(h3 ^ (h1 >>> 18), 597399067);
h2 = Math.imul(h4 ^ (h2 >>> 22), 2869860233);
h3 = Math.imul(h1 ^ (h3 >>> 17), 951274213);
h4 = Math.imul(h2 ^ (h4 >>> 19), 2716044179);
return [
(h1 ^ h2 ^ h3 ^ h4) >>> 0,
(h2 ^ h1) >>> 0,
(h3 ^ h1) >>> 0,
(h4 ^ h1) >>> 0,
];
}
function mulberry32(a: number) {
return function () {
var t = (a += 0x6d2b79f5);
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
return () => {
// biome-ignore lint/suspicious/noAssignInExpressions: biome migration
// biome-ignore lint/style/noParameterAssign: biome migration
let t = (a += 0x6d2b79f5);
t = Math.imul(t ^ (t >>> 15), t | 1);
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
};
}
export const seededRandom = (seed: string) => {
const rng = mulberry32(cyrb128(seed)[0]);
const rng = mulberry32(cyrb128(seed)[0]);
const rnd = (lo: number, hi?: number, defaultHi = 1) => {
if (hi === undefined) {
hi = lo === undefined ? defaultHi : lo;
lo = 0;
}
const rnd = (lo: number, hi?: number, defaultHi = 1) => {
if (hi === undefined) {
// biome-ignore lint/style/noParameterAssign: biome migration
hi = lo === undefined ? defaultHi : lo;
// biome-ignore lint/style/noParameterAssign: biome migration
lo = 0;
}
return rng() * (hi - lo) + lo;
};
return rng() * (hi - lo) + lo;
};
const rndInt = (lo: number, hi?: number) => Math.floor(rnd(lo, hi, 2));
const rndInt = (lo: number, hi?: number) => Math.floor(rnd(lo, hi, 2));
const shuffle = <T>(o: T[]) => {
const a = o.slice();
const shuffle = <T>(o: T[]) => {
const a = o.slice();
for (let i = a.length - 1; i > 0; i--) {
const j = rndInt(i + 1);
const x = a[i];
a[i] = a[j]!;
a[j] = x!;
}
for (let i = a.length - 1; i > 0; i--) {
const j = rndInt(i + 1);
const x = a[i];
a[i] = a[j]!;
a[j] = x!;
}
return a;
};
return a;
};
return { rnd, rndInt, shuffle };
return { rnd, rndInt, shuffle };
};

View File

@@ -2,32 +2,32 @@ import { z } from "zod";
import type { Unpacked } from "~/utils/types";
export const streamsSchema = z.object({
data: z.array(
z.object({
id: z.string(),
user_id: z.string(),
user_login: z.string(),
user_name: z.string(),
game_id: z.string(),
game_name: z.string(),
type: z.string(),
title: z.string(),
viewer_count: z.number(),
started_at: z.string(),
language: z.string(),
thumbnail_url: z.string(),
tag_ids: z.array(z.unknown()),
tags: z.array(z.string()).nullish(),
is_mature: z.boolean(),
}),
),
pagination: z.object({ cursor: z.string().nullish() }),
data: z.array(
z.object({
id: z.string(),
user_id: z.string(),
user_login: z.string(),
user_name: z.string(),
game_id: z.string(),
game_name: z.string(),
type: z.string(),
title: z.string(),
viewer_count: z.number(),
started_at: z.string(),
language: z.string(),
thumbnail_url: z.string(),
tag_ids: z.array(z.unknown()),
tags: z.array(z.string()).nullish(),
is_mature: z.boolean(),
}),
),
pagination: z.object({ cursor: z.string().nullish() }),
});
export const tokenResponseSchema = z.object({
access_token: z.string(),
expires_in: z.number(),
token_type: z.string(),
access_token: z.string(),
expires_in: z.number(),
token_type: z.string(),
});
export type StreamsResponse = z.infer<typeof streamsSchema>;

View File

@@ -1,9 +1,9 @@
import { cachified } from "@epic-web/cachified";
import { type StreamsResponse, type RawStream, streamsSchema } from "./schemas";
import { getToken, purgeCachedToken } from "./token";
import { getTwitchEnvVars } from "./utils";
import { cache } from "~/utils/cache.server";
import type { Unpacked } from "~/utils/types";
import { type RawStream, type StreamsResponse, streamsSchema } from "./schemas";
import { getToken, purgeCachedToken } from "./token";
import { getTwitchEnvVars } from "./utils";
// const STREAMS_MOCK = [
// {
@@ -69,110 +69,107 @@ import type { Unpacked } from "~/utils/types";
// ];
export async function getStreams() {
try {
const result = await cachified({
key: `twitch-streams`,
cache,
// 2 minutes
ttl: 1000 * 60 * 2,
// 10 minutes
staleWhileRevalidate: 1000 * 60 * 5 * 2,
async getFreshValue() {
// eslint-disable-next-line no-console
console.log("getting fresh twitch streams");
try {
const result = await cachified({
key: "twitch-streams",
cache,
// 2 minutes
ttl: 1000 * 60 * 2,
// 10 minutes
staleWhileRevalidate: 1000 * 60 * 5 * 2,
async getFreshValue() {
return (await getAllStreams())
.map(mapRawStream)
.sort((a, b) => b.viewerCount - a.viewerCount);
},
});
return (await getAllStreams())
.map(mapRawStream)
.sort((a, b) => b.viewerCount - a.viewerCount);
},
});
return result;
} catch (e) {
console.error(e);
return [];
}
return result;
} catch (e) {
console.error(e);
return [];
}
}
export type MappedStream = Unpacked<ReturnType<typeof mapRawStream>>;
function mapRawStream(stream: RawStream) {
return {
thumbnailUrl: stream.thumbnail_url,
twitchUserName: stream.user_login.toLowerCase(),
viewerCount: stream.viewer_count,
};
return {
thumbnailUrl: stream.thumbnail_url,
twitchUserName: stream.user_login.toLowerCase(),
viewerCount: stream.viewer_count,
};
}
const SPLATOON_3_TWITCH_GAME_ID = "1158884259";
async function getAllStreams() {
if (process.env.NODE_ENV === "test") return [];
if (process.env.NODE_ENV === "test") return [];
const result: RawStream[] = [];
const result: RawStream[] = [];
let cursor: string | undefined = undefined;
let count = 0;
while (true) {
if (count === 50) {
throw new Error("Stuck getting streams");
}
const { data, pagination } = await getStreamsChunk({ cursor });
let cursor: string | undefined = undefined;
let count = 0;
while (true) {
if (count === 50) {
throw new Error("Stuck getting streams");
}
const { data, pagination } = await getStreamsChunk({ cursor });
result.push(
// filter to ensure each streamer appears only once
...data.filter(
(stream) =>
!result.some(
(existingStream) => existingStream.user_id === stream.user_id,
),
),
);
if (!pagination.cursor) {
return result;
}
result.push(
// filter to ensure each streamer appears only once
...data.filter(
(stream) =>
!result.some(
(existingStream) => existingStream.user_id === stream.user_id,
),
),
);
if (!pagination.cursor) {
return result;
}
cursor = pagination.cursor;
count++;
}
cursor = pagination.cursor;
count++;
}
}
export async function getStreamsChunk({
isRetry = false,
cursor,
isRetry = false,
cursor,
}: {
isRetry?: boolean;
cursor?: string;
isRetry?: boolean;
cursor?: string;
}): Promise<StreamsResponse> {
const { TWITCH_CLIENT_ID } = getTwitchEnvVars();
const token = await getToken();
const { TWITCH_CLIENT_ID } = getTwitchEnvVars();
const token = await getToken();
const res = await fetch(
`https://api.twitch.tv/helix/streams?game_id=${SPLATOON_3_TWITCH_GAME_ID}&first=100&after=${
cursor ?? ""
}`,
{
headers: [
["Authorization", `Bearer ${token}`],
["Client-Id", TWITCH_CLIENT_ID],
],
},
);
const res = await fetch(
`https://api.twitch.tv/helix/streams?game_id=${SPLATOON_3_TWITCH_GAME_ID}&first=100&after=${
cursor ?? ""
}`,
{
headers: [
["Authorization", `Bearer ${token}`],
["Client-Id", TWITCH_CLIENT_ID],
],
},
);
if (res.status === 401 && !isRetry) {
purgeCachedToken();
return getStreamsChunk({ isRetry: true, cursor });
}
if (res.status === 401 && !isRetry) {
purgeCachedToken();
return getStreamsChunk({ isRetry: true, cursor });
}
if (!res.ok) {
throw new Error(
`Getting Twitch token failed with status code: ${res.status}`,
);
}
if (!res.ok) {
throw new Error(
`Getting Twitch token failed with status code: ${res.status}`,
);
}
const parsed = streamsSchema.safeParse(await res.json());
if (!parsed.success) {
throw new Error(parsed.error.message);
}
const parsed = streamsSchema.safeParse(await res.json());
if (!parsed.success) {
throw new Error(parsed.error.message);
}
return parsed.data;
return parsed.data;
}

View File

@@ -4,41 +4,36 @@ import { tokenResponseSchema } from "./schemas";
import { getTwitchEnvVars } from "./utils";
async function getFreshToken() {
const { TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET } = getTwitchEnvVars();
const { TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET } = getTwitchEnvVars();
const res = await fetch(
"https://id.twitch.tv/oauth2/token" +
`?client_id=${TWITCH_CLIENT_ID}` +
`&client_secret=${TWITCH_CLIENT_SECRET}` +
"&grant_type=client_credentials",
{ method: "POST" },
);
if (!res.ok) {
throw new Error(
`Getting Twitch token failed with status code: ${res.status}`,
);
}
const res = await fetch(
`https://id.twitch.tv/oauth2/token?client_id=${TWITCH_CLIENT_ID}&client_secret=${TWITCH_CLIENT_SECRET}&grant_type=client_credentials`,
{ method: "POST" },
);
if (!res.ok) {
throw new Error(
`Getting Twitch token failed with status code: ${res.status}`,
);
}
const parsed = tokenResponseSchema.safeParse(await res.json());
if (!parsed.success) {
throw new Error("Token response schema validation failed");
}
const parsed = tokenResponseSchema.safeParse(await res.json());
if (!parsed.success) {
throw new Error("Token response schema validation failed");
}
return parsed.data.access_token;
return parsed.data.access_token;
}
export function getToken() {
return cachified({
key: `twitch-token`,
cache,
getFreshValue() {
// eslint-disable-next-line no-console
console.log("getting fresh twitch token");
return getFreshToken();
},
});
return cachified({
key: "twitch-token",
cache,
getFreshValue() {
return getFreshToken();
},
});
}
export function purgeCachedToken() {
cache.delete("twitch-token");
cache.delete("twitch-token");
}

View File

@@ -1,18 +1,18 @@
import invariant from "~/utils/invariant";
export const getTwitchEnvVars = () => {
const { TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET } = process.env;
invariant(
TWITCH_CLIENT_ID,
"Missing TWITCH_CLIENT_ID env var, showing no streams",
);
invariant(
TWITCH_CLIENT_SECRET,
"Missing TWITCH_CLIENT_SECRET env var, showing no streams",
);
const { TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET } = process.env;
invariant(
TWITCH_CLIENT_ID,
"Missing TWITCH_CLIENT_ID env var, showing no streams",
);
invariant(
TWITCH_CLIENT_SECRET,
"Missing TWITCH_CLIENT_SECRET env var, showing no streams",
);
return { TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET };
return { TWITCH_CLIENT_ID, TWITCH_CLIENT_SECRET };
};
export const twitchThumbnailUrlToSrc = (url: string) =>
url.replace("{width}", "640").replace("{height}", "360");
url.replace("{width}", "640").replace("{height}", "360");