mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-30 23:27:55 -05:00
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:
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
];
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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"]);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
Reference in New Issue
Block a user