mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-05-10 04:40:46 -05:00
* Got something going * Style overwrites * width != height * More playing with lines * Migrations * Start bracket initial * Unhardcode stage generation params * Link to match page * Matches page initial * Support directly adding seed to map list generator * Add docs * Maps in matches page * Add invariant about tie breaker map pool * Fix PICNIC lacking tie breaker maps * Only link in bracket when tournament has started * Styled tournament roster inputs * Prefer IGN in tournament match page * ModeProgressIndicator * Some conditional rendering * Match action initial + better error display * Persist bestOf in DB * Resolve best of ahead of time * Move brackets-manager to core * Score reporting works * Clear winner on score report * ModeProgressIndicator: highlight winners * Fix inconsistent input * Better text when submitting match * mapCountPlayedInSetWithCertainty that works * UNDO_REPORT_SCORE implemented * Permission check when starting tournament * Remove IGN from upsert * View match results page * Source in DB * Match page waiting for teams * Move tournament bracket to feature folder * REOPEN_MATCH initial * Handle proper resetting of match * Inline bracket-manager * Syncify * Transactions * Handle match is locked gracefully * Match page auto refresh * Fix match refresh called "globally" * Bracket autoupdate * Move fillWithNullTillPowerOfTwo to utils with testing * Fix map lists not visible after tournament started * Optimize match events * Show UI while in progress to members * Fix start tournament alert not being responsive * Teams can check in * Fix map list 400 * xxx -> TODO * Seeds page * Remove map icons for team page * Don't display link to seeds after tournament has started * Admin actions initial * Change captain admin action * Make all hooks ts * Admin actions functioning * Fix validate error not displaying in CatchBoundary * Adjust validate args order * Remove admin loader * Make delete team button menancing * Only include checked in teams to bracket * Optimize to.id route loads * Working show map list generator toggle * Update full tournaments flow * Make full tournaments work with many start times * Handle undefined in crud * Dynamic stage banner * Handle default strat if map list generation fails * Fix crash on brackets if less than 2 teams * Add commented out test for reference * Add TODO * Add players from team during register * TrustRelationship * Prefers not to host feature * Last before merge * Rename some vars * More renames
670 lines
15 KiB
TypeScript
670 lines
15 KiB
TypeScript
import { suite } from "uvu";
|
|
import * as assert from "uvu/assert";
|
|
import { createTournamentMapList } from ".";
|
|
import type { RankedModeShort } from "../in-game-lists";
|
|
import { rankedModesShort } from "../in-game-lists/modes";
|
|
import { MapPool } from "../map-pool-serializer";
|
|
import type { TournamentMaplistInput } from "./types";
|
|
|
|
const TournamentMapListGenerator = suite("Tournament map list generator");
|
|
const TournamentMapListGeneratorOneMode = suite(
|
|
"Tournament map list generator (one mode)"
|
|
);
|
|
|
|
const team1Picks = new MapPool([
|
|
{ mode: "SZ", stageId: 4 },
|
|
{ mode: "SZ", stageId: 5 },
|
|
{ mode: "TC", stageId: 5 },
|
|
{ mode: "TC", stageId: 6 },
|
|
{ mode: "RM", stageId: 7 },
|
|
{ mode: "RM", stageId: 8 },
|
|
{ mode: "CB", stageId: 9 },
|
|
{ mode: "CB", stageId: 10 },
|
|
]);
|
|
const team2Picks = new MapPool([
|
|
{ mode: "SZ", stageId: 11 },
|
|
{ mode: "SZ", stageId: 9 },
|
|
{ mode: "TC", stageId: 2 },
|
|
{ mode: "TC", stageId: 8 },
|
|
{ mode: "RM", stageId: 7 },
|
|
{ mode: "RM", stageId: 1 },
|
|
{ mode: "CB", stageId: 2 },
|
|
{ mode: "CB", stageId: 3 },
|
|
]);
|
|
const tiebreakerPicks = new MapPool([
|
|
{ mode: "SZ", stageId: 1 },
|
|
{ mode: "TC", stageId: 11 },
|
|
{ mode: "RM", stageId: 3 },
|
|
{ mode: "CB", stageId: 4 },
|
|
]);
|
|
|
|
const generateMaps = ({
|
|
bestOf = 5,
|
|
seed = "test",
|
|
teams = [
|
|
{
|
|
id: 1,
|
|
maps: team1Picks,
|
|
},
|
|
{
|
|
id: 2,
|
|
maps: team2Picks,
|
|
},
|
|
],
|
|
tiebreakerMaps = tiebreakerPicks,
|
|
modesIncluded = [...rankedModesShort],
|
|
}: Partial<TournamentMaplistInput> = {}) => {
|
|
return createTournamentMapList({
|
|
bestOf,
|
|
seed,
|
|
teams,
|
|
tiebreakerMaps,
|
|
modesIncluded,
|
|
});
|
|
};
|
|
|
|
TournamentMapListGenerator("Modes are spread evenly", () => {
|
|
const mapList = generateMaps();
|
|
const modes = new Set(rankedModesShort);
|
|
|
|
assert.equal(mapList.length, 5);
|
|
|
|
for (const [i, { mode }] of mapList.entries()) {
|
|
const rankedMode = mode as RankedModeShort;
|
|
if (!modes.has(rankedMode)) {
|
|
assert.equal(i, 4, "Repeated mode early");
|
|
assert.equal(mode, mapList[0]!.mode, "1st and 5th mode are not the same");
|
|
}
|
|
|
|
modes.delete(rankedMode);
|
|
}
|
|
});
|
|
|
|
TournamentMapListGenerator("Equal picks", () => {
|
|
let our = 0;
|
|
let their = 0;
|
|
let tiebreaker = 0;
|
|
|
|
const mapList = generateMaps();
|
|
|
|
for (const { stageId, mode } of mapList) {
|
|
if (team1Picks.has({ stageId, mode })) {
|
|
our++;
|
|
}
|
|
|
|
if (team2Picks.has({ stageId, mode })) {
|
|
their++;
|
|
}
|
|
|
|
if (tiebreakerPicks.has({ stageId, mode })) {
|
|
tiebreaker++;
|
|
}
|
|
}
|
|
|
|
assert.equal(our, their);
|
|
assert.equal(tiebreaker, 1);
|
|
});
|
|
|
|
TournamentMapListGenerator("No stage repeats in optimal case", () => {
|
|
const mapList = generateMaps();
|
|
|
|
const stages = new Set(mapList.map(({ stageId }) => stageId));
|
|
|
|
assert.equal(stages.size, 5);
|
|
});
|
|
|
|
TournamentMapListGenerator(
|
|
"Always generates same maplist given same input",
|
|
() => {
|
|
const mapList1 = generateMaps();
|
|
const mapList2 = generateMaps();
|
|
|
|
assert.equal(mapList1.length, 5);
|
|
|
|
for (let i = 0; i < mapList1.length; i++) {
|
|
assert.equal(mapList1[i]!.stageId, mapList2[i]!.stageId);
|
|
assert.equal(mapList1[i]!.mode, mapList2[i]!.mode);
|
|
}
|
|
}
|
|
);
|
|
|
|
TournamentMapListGenerator(
|
|
"Order of team doesn't matter regarding what maplist gets created",
|
|
() => {
|
|
const mapList1 = generateMaps();
|
|
const mapList2 = generateMaps({
|
|
teams: [
|
|
{
|
|
id: 2,
|
|
maps: team2Picks,
|
|
},
|
|
{
|
|
id: 1,
|
|
maps: team1Picks,
|
|
},
|
|
],
|
|
});
|
|
|
|
assert.equal(mapList1.length, 5);
|
|
|
|
for (let i = 0; i < mapList1.length; i++) {
|
|
assert.equal(mapList1[i]!.stageId, mapList2[i]!.stageId);
|
|
assert.equal(mapList1[i]!.mode, mapList2[i]!.mode);
|
|
}
|
|
}
|
|
);
|
|
|
|
TournamentMapListGenerator(
|
|
"Order of maps in the list doesn't matter regarding what maplist gets created",
|
|
() => {
|
|
const mapList1 = generateMaps({
|
|
teams: [
|
|
{
|
|
id: 1,
|
|
maps: team1Picks,
|
|
},
|
|
{
|
|
id: 2,
|
|
maps: team2Picks,
|
|
},
|
|
],
|
|
});
|
|
const mapList2 = generateMaps({
|
|
teams: [
|
|
{
|
|
id: 1,
|
|
maps: team1Picks,
|
|
},
|
|
{
|
|
id: 2,
|
|
maps: new MapPool(team2Picks.stageModePairs.slice().reverse()),
|
|
},
|
|
],
|
|
});
|
|
|
|
assert.equal(mapList1.length, 5);
|
|
|
|
for (let i = 0; i < mapList1.length; i++) {
|
|
assert.equal(mapList1[i]!.stageId, mapList2[i]!.stageId);
|
|
assert.equal(mapList1[i]!.mode, mapList2[i]!.mode);
|
|
}
|
|
}
|
|
);
|
|
|
|
const duplicationPicks = new MapPool([
|
|
{ mode: "SZ", stageId: 4 },
|
|
{ mode: "SZ", stageId: 5 },
|
|
{ mode: "TC", stageId: 4 },
|
|
{ mode: "TC", stageId: 5 },
|
|
{ mode: "RM", stageId: 6 },
|
|
{ mode: "RM", stageId: 7 },
|
|
{ mode: "CB", stageId: 6 },
|
|
{ mode: "CB", stageId: 7 },
|
|
]);
|
|
const duplicationTiebreaker = new MapPool([
|
|
{ mode: "SZ", stageId: 7 },
|
|
{ mode: "TC", stageId: 6 },
|
|
{ mode: "RM", stageId: 5 },
|
|
{ mode: "CB", stageId: 4 },
|
|
]);
|
|
|
|
TournamentMapListGenerator(
|
|
"Uses other teams maps if one didn't submit maplist",
|
|
() => {
|
|
const mapList = generateMaps({
|
|
teams: [
|
|
{
|
|
id: 1,
|
|
maps: new MapPool([]),
|
|
},
|
|
{
|
|
id: 2,
|
|
maps: team2Picks,
|
|
},
|
|
],
|
|
});
|
|
|
|
assert.equal(mapList.length, 5);
|
|
|
|
for (let i = 0; i < mapList.length - 1; i++) {
|
|
// map belongs to team 2 map list
|
|
const map = mapList[i];
|
|
assert.ok(map);
|
|
|
|
team2Picks.has({ mode: map.mode, stageId: map.stageId });
|
|
}
|
|
}
|
|
);
|
|
|
|
TournamentMapListGenerator(
|
|
"Creates map list even if neither team submitted maps",
|
|
() => {
|
|
const mapList = generateMaps({
|
|
teams: [
|
|
{
|
|
id: 1,
|
|
maps: new MapPool([]),
|
|
},
|
|
{
|
|
id: 2,
|
|
maps: new MapPool([]),
|
|
},
|
|
],
|
|
});
|
|
|
|
assert.equal(mapList.length, 5);
|
|
}
|
|
);
|
|
|
|
TournamentMapListGenerator("Handles worst case with duplication", () => {
|
|
const maplist = generateMaps({
|
|
teams: [
|
|
{
|
|
id: 1,
|
|
maps: duplicationPicks,
|
|
},
|
|
{
|
|
id: 2,
|
|
maps: duplicationPicks,
|
|
},
|
|
],
|
|
bestOf: 7,
|
|
tiebreakerMaps: duplicationTiebreaker,
|
|
});
|
|
|
|
assert.equal(maplist.length, 7);
|
|
|
|
// all stages appear
|
|
const stages = new Set(maplist.map(({ stageId }) => stageId));
|
|
assert.equal(stages.size, 4);
|
|
|
|
// no consecutive stage replays
|
|
for (let i = 0; i < maplist.length - 1; i++) {
|
|
assert.not.equal(maplist[i]!.stageId, maplist[i + 1]!.stageId);
|
|
}
|
|
});
|
|
|
|
const team2PicksWithSomeDuplication = new MapPool([
|
|
{ mode: "SZ", stageId: 4 },
|
|
{ mode: "SZ", stageId: 11 },
|
|
{ mode: "TC", stageId: 5 },
|
|
{ mode: "TC", stageId: 6 },
|
|
{ mode: "RM", stageId: 7 },
|
|
{ mode: "RM", stageId: 2 },
|
|
{ mode: "CB", stageId: 9 },
|
|
{ mode: "CB", stageId: 10 },
|
|
]);
|
|
|
|
TournamentMapListGenerator("Keeps things fair when overlap", () => {
|
|
const mapList = generateMaps({
|
|
teams: [
|
|
{
|
|
id: 1,
|
|
maps: team1Picks,
|
|
},
|
|
{
|
|
id: 2,
|
|
maps: team2PicksWithSomeDuplication,
|
|
},
|
|
],
|
|
bestOf: 7,
|
|
});
|
|
|
|
assert.equal(mapList.length, 7);
|
|
|
|
let team1PicksAppeared = 0;
|
|
let team2PicksAppeared = 0;
|
|
|
|
for (const { stageId, mode } of mapList) {
|
|
if (team1Picks.has({ stageId, mode })) {
|
|
team1PicksAppeared++;
|
|
}
|
|
|
|
if (team2PicksWithSomeDuplication.has({ stageId, mode })) {
|
|
team2PicksAppeared++;
|
|
}
|
|
}
|
|
|
|
assert.equal(team1PicksAppeared, team2PicksAppeared);
|
|
});
|
|
|
|
TournamentMapListGenerator("No map picked by same team twice in row", () => {
|
|
for (let i = 1; i <= 10; i++) {
|
|
const mapList = generateMaps({
|
|
teams: [
|
|
{
|
|
id: 1,
|
|
maps: team1Picks,
|
|
},
|
|
{
|
|
id: 2,
|
|
maps: team2Picks,
|
|
},
|
|
],
|
|
seed: String(i),
|
|
});
|
|
|
|
for (let j = 0; j < mapList.length - 1; j++) {
|
|
if (typeof mapList[j]!.source !== "number") continue;
|
|
assert.not.equal(mapList[j]!.source, mapList[j + 1]!.source);
|
|
}
|
|
}
|
|
});
|
|
|
|
// TODO: figure out how to handle this
|
|
// checks for case were there is complete overlap in one mode but not others
|
|
// which means with forced tiebreaker the map list would become unbalanced
|
|
// TournamentMapListGenerator.only(
|
|
// "Handles impossible duplication situation by using BOTH as tiebreaker",
|
|
// () => {
|
|
// const maps = generateMaps({
|
|
// teams: [
|
|
// {
|
|
// id: 11,
|
|
// maps: new MapPool([
|
|
// // dupe
|
|
// {
|
|
// stageId: 11,
|
|
// mode: "RM",
|
|
// },
|
|
// {
|
|
// stageId: 11,
|
|
// mode: "TC",
|
|
// },
|
|
// {
|
|
// stageId: 3,
|
|
// mode: "SZ",
|
|
// },
|
|
// // dupe
|
|
// {
|
|
// stageId: 1,
|
|
// mode: "RM",
|
|
// },
|
|
// {
|
|
// stageId: 4,
|
|
// mode: "SZ",
|
|
// },
|
|
// {
|
|
// stageId: 10,
|
|
// mode: "CB",
|
|
// },
|
|
// {
|
|
// stageId: 3,
|
|
// mode: "TC",
|
|
// },
|
|
// {
|
|
// stageId: 2,
|
|
// mode: "CB",
|
|
// },
|
|
// ]),
|
|
// },
|
|
// {
|
|
// id: 4,
|
|
// maps: new MapPool([
|
|
// {
|
|
// stageId: 2,
|
|
// mode: "SZ",
|
|
// },
|
|
// {
|
|
// stageId: 10,
|
|
// mode: "TC",
|
|
// },
|
|
// {
|
|
// stageId: 8,
|
|
// mode: "SZ",
|
|
// },
|
|
// {
|
|
// stageId: 11,
|
|
// mode: "RM",
|
|
// },
|
|
// {
|
|
// stageId: 6,
|
|
// mode: "TC",
|
|
// },
|
|
// {
|
|
// stageId: 1,
|
|
// mode: "RM",
|
|
// },
|
|
// {
|
|
// stageId: 11,
|
|
// mode: "CB",
|
|
// },
|
|
// {
|
|
// stageId: 6,
|
|
// mode: "CB",
|
|
// },
|
|
// ]),
|
|
// },
|
|
// ],
|
|
// seed: String(1),
|
|
// bestOf: 5,
|
|
// modesIncluded: ["SZ", "TC", "RM", "CB"],
|
|
// tiebreakerMaps: new MapPool([
|
|
// {
|
|
// stageId: 1,
|
|
// mode: "SZ",
|
|
// },
|
|
// {
|
|
// stageId: 2,
|
|
// mode: "TC",
|
|
// },
|
|
// {
|
|
// stageId: 3,
|
|
// mode: "RM",
|
|
// },
|
|
// {
|
|
// stageId: 4,
|
|
// mode: "CB",
|
|
// },
|
|
// ]),
|
|
// });
|
|
|
|
// assert.equal(maps[maps.length - 1].source, "BOTH");
|
|
// }
|
|
// );
|
|
|
|
const team1SZPicks = new MapPool([
|
|
{ mode: "SZ", stageId: 4 },
|
|
{ mode: "SZ", stageId: 5 },
|
|
{ mode: "SZ", stageId: 6 },
|
|
{ mode: "SZ", stageId: 7 },
|
|
{ mode: "SZ", stageId: 8 },
|
|
{ mode: "SZ", stageId: 9 },
|
|
]);
|
|
const team2SZPicks = new MapPool([
|
|
{ mode: "SZ", stageId: 1 },
|
|
{ mode: "SZ", stageId: 2 },
|
|
{ mode: "SZ", stageId: 3 },
|
|
{ mode: "SZ", stageId: 9 },
|
|
{ mode: "SZ", stageId: 10 },
|
|
{ mode: "SZ", stageId: 11 },
|
|
]);
|
|
const team2SZPicksNoOverlap = new MapPool([
|
|
{ mode: "SZ", stageId: 1 },
|
|
{ mode: "SZ", stageId: 2 },
|
|
{ mode: "SZ", stageId: 3 },
|
|
{ mode: "SZ", stageId: 14 },
|
|
{ mode: "SZ", stageId: 10 },
|
|
{ mode: "SZ", stageId: 11 },
|
|
]);
|
|
|
|
TournamentMapListGeneratorOneMode(
|
|
"Creates map list for one mode inferring from the team picks",
|
|
() => {
|
|
const mapList = generateMaps({
|
|
teams: [
|
|
{
|
|
id: 1,
|
|
maps: team1SZPicks,
|
|
},
|
|
{
|
|
id: 2,
|
|
maps: team2SZPicks,
|
|
},
|
|
],
|
|
modesIncluded: ["SZ"],
|
|
tiebreakerMaps: new MapPool([]),
|
|
});
|
|
for (let i = 0; i < mapList.length - 1; i++) {
|
|
assert.equal(mapList[i]!.mode, "SZ");
|
|
}
|
|
}
|
|
);
|
|
|
|
TournamentMapListGeneratorOneMode(
|
|
"Creates one mode map list from empty map lists",
|
|
() => {
|
|
const mapList = generateMaps({
|
|
teams: [
|
|
{
|
|
id: 1,
|
|
maps: new MapPool([]),
|
|
},
|
|
{
|
|
id: 2,
|
|
maps: new MapPool([]),
|
|
},
|
|
],
|
|
modesIncluded: ["SZ"],
|
|
tiebreakerMaps: new MapPool([]),
|
|
});
|
|
for (let i = 0; i < mapList.length - 1; i++) {
|
|
assert.equal(mapList[i]!.mode, "SZ");
|
|
}
|
|
}
|
|
);
|
|
|
|
TournamentMapListGeneratorOneMode(
|
|
"Creates all different maps from empty map lists",
|
|
() => {
|
|
const mapList = generateMaps({
|
|
teams: [
|
|
{
|
|
id: 1,
|
|
maps: new MapPool([]),
|
|
},
|
|
{
|
|
id: 2,
|
|
maps: new MapPool([]),
|
|
},
|
|
],
|
|
modesIncluded: ["SZ"],
|
|
tiebreakerMaps: new MapPool([]),
|
|
});
|
|
|
|
const stages = new Set(mapList.map(({ stageId }) => stageId));
|
|
assert.equal(stages.size, 5);
|
|
}
|
|
);
|
|
|
|
TournamentMapListGeneratorOneMode(
|
|
"Tiebreaker is always from the maps of the teams when possible",
|
|
() => {
|
|
for (let i = 1; i <= 10; i++) {
|
|
const mapList = generateMaps({
|
|
teams: [
|
|
{
|
|
id: 1,
|
|
maps: team1SZPicks,
|
|
},
|
|
{
|
|
id: 2,
|
|
maps: team2SZPicks,
|
|
},
|
|
],
|
|
modesIncluded: ["SZ"],
|
|
seed: String(i),
|
|
tiebreakerMaps: new MapPool([]),
|
|
});
|
|
|
|
const last = mapList[mapList.length - 1];
|
|
|
|
assert.equal(last?.mode, "SZ");
|
|
assert.equal(last?.stageId, 9);
|
|
}
|
|
}
|
|
);
|
|
|
|
TournamentMapListGeneratorOneMode(
|
|
"Tiebreaker is from neither team's pool if no overlap",
|
|
() => {
|
|
const mapList = generateMaps({
|
|
teams: [
|
|
{
|
|
id: 1,
|
|
maps: team1SZPicks,
|
|
},
|
|
{
|
|
id: 2,
|
|
maps: team2SZPicksNoOverlap,
|
|
},
|
|
],
|
|
modesIncluded: ["SZ"],
|
|
tiebreakerMaps: new MapPool([]),
|
|
});
|
|
|
|
const last = mapList[mapList.length - 1];
|
|
|
|
assert.not.ok(
|
|
team1SZPicks.stageModePairs.some(
|
|
({ stageId }) => stageId === last?.stageId
|
|
)
|
|
);
|
|
assert.not.ok(
|
|
team2SZPicksNoOverlap.stageModePairs.some(
|
|
({ stageId }) => stageId === last?.stageId
|
|
)
|
|
);
|
|
}
|
|
);
|
|
|
|
TournamentMapListGeneratorOneMode("Handles worst case duplication", () => {
|
|
const mapList = generateMaps({
|
|
teams: [
|
|
{
|
|
id: 1,
|
|
maps: team1SZPicks,
|
|
},
|
|
{
|
|
id: 2,
|
|
maps: team1SZPicks,
|
|
},
|
|
],
|
|
modesIncluded: ["SZ"],
|
|
tiebreakerMaps: new MapPool([]),
|
|
bestOf: 7,
|
|
});
|
|
|
|
for (const [i, stage] of mapList.entries()) {
|
|
if (i === 6) {
|
|
assert.equal(stage?.source, "TIEBREAKER");
|
|
} else {
|
|
assert.equal(stage?.source, "BOTH");
|
|
}
|
|
}
|
|
});
|
|
|
|
TournamentMapListGeneratorOneMode("Handles one team submitted no maps", () => {
|
|
const mapList = generateMaps({
|
|
teams: [
|
|
{
|
|
id: 1,
|
|
maps: team1SZPicks,
|
|
},
|
|
{
|
|
id: 2,
|
|
maps: new MapPool([]),
|
|
},
|
|
],
|
|
modesIncluded: ["SZ"],
|
|
tiebreakerMaps: new MapPool([]),
|
|
});
|
|
|
|
for (const stage of mapList) {
|
|
assert.equal(stage.source, 1);
|
|
}
|
|
});
|
|
|
|
TournamentMapListGenerator.run();
|
|
TournamentMapListGeneratorOneMode.run();
|