sendou.ink/app/modules/tournament-map-list-generator/generation.test.ts

864 lines
17 KiB
TypeScript

import { describe, expect, test } from "vitest";
import { MapPool } from "~/features/map-list-generator/core/map-pool";
import { rankedModesShort } from "../in-game-lists/modes";
import type { RankedModeShort } from "../in-game-lists/types";
import { generateBalancedMapList } from "./balanced-map-list";
import { DEFAULT_MAP_POOL } from "./constants";
import type { TournamentMaplistInput } from "./types";
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 team2PicksNoOverlap = new MapPool([
{ mode: "SZ", stageId: 11 },
{ mode: "SZ", stageId: 9 },
{ mode: "TC", stageId: 2 },
{ mode: "TC", stageId: 8 },
{ mode: "RM", stageId: 17 },
{ 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 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 },
]);
const generateMaps = ({
count = 5,
seed = "test",
teams = [
{
id: 1,
maps: team1Picks,
},
{
id: 2,
maps: team2Picks,
},
],
tiebreakerMaps = tiebreakerPicks,
modesIncluded = [...rankedModesShort],
followModeOrder = false,
}: Partial<TournamentMaplistInput> = {}) => {
return generateBalancedMapList({
count,
seed,
teams,
tiebreakerMaps,
modesIncluded,
followModeOrder,
});
};
describe("Tournament map list generator", () => {
test("Modes are spread evenly", () => {
const mapList = generateMaps();
const modes = new Set(rankedModesShort);
expect(mapList.length).toBe(5);
for (const [i, { mode }] of mapList.entries()) {
const rankedMode = mode as RankedModeShort;
if (!modes.has(rankedMode)) {
expect(i).toBe(4);
expect(mode).toBe(mapList[0].mode);
}
modes.delete(rankedMode);
}
});
test("Follow mode order option", () => {
const mapList = generateMaps({ followModeOrder: true });
expect(mapList[0].mode).toBe("SZ");
expect(mapList[1].mode).toBe("TC");
expect(mapList[2].mode).toBe("RM");
expect(mapList[3].mode).toBe("CB");
expect(mapList[4].mode).toBe("SZ");
});
test("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++;
}
}
expect(our).toBe(their);
expect(tiebreaker).toBe(1);
});
test("No stage repeats in optimal case", () => {
const mapList = generateMaps();
const stages = new Set(mapList.map(({ stageId }) => stageId));
expect(stages.size).toBe(5);
});
test("Always generates same maplist given same input", () => {
const mapList1 = generateMaps();
const mapList2 = generateMaps();
expect(mapList1.length).toBe(5);
for (let i = 0; i < mapList1.length; i++) {
expect(mapList1[i].stageId).toBe(mapList2[i].stageId);
expect(mapList1[i].mode).toBe(mapList2[i].mode);
}
});
test("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,
},
],
});
expect(mapList1.length).toBe(5);
for (let i = 0; i < mapList1.length; i++) {
expect(mapList1[i].stageId).toBe(mapList2[i].stageId);
expect(mapList1[i].mode).toBe(mapList2[i].mode);
}
});
test("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()),
},
],
});
expect(mapList1.length).toBe(5);
for (let i = 0; i < mapList1.length; i++) {
expect(mapList1[i].stageId).toBe(mapList2[i].stageId);
expect(mapList1[i].mode).toBe(mapList2[i].mode);
}
});
test("Uses other teams maps if one didn't submit maplist", () => {
const mapList = generateMaps({
teams: [
{
id: 1,
maps: new MapPool([]),
},
{
id: 2,
maps: team2Picks,
},
],
});
expect(mapList.length).toBe(5);
for (let i = 0; i < mapList.length - 1; i++) {
// map belongs to team 2 map list
const map = mapList[i];
expect(map).toBeTruthy();
expect(team2Picks.has({ mode: map.mode, stageId: map.stageId })).toBe(
true,
);
}
});
test("Creates map list even if neither team submitted maps", () => {
const mapList = generateMaps({
teams: [
{
id: 1,
maps: new MapPool([]),
},
{
id: 2,
maps: new MapPool([]),
},
],
});
expect(mapList.length).toBe(5);
});
test("Handles worst case with duplication", () => {
const maplist = generateMaps({
teams: [
{
id: 1,
maps: duplicationPicks,
},
{
id: 2,
maps: duplicationPicks,
},
],
count: 7,
tiebreakerMaps: duplicationTiebreaker,
});
expect(maplist.length).toBe(7);
// all stages appear
const stages = new Set(maplist.map(({ stageId }) => stageId));
expect(stages.size).toBe(4);
// no consecutive stage replays
for (let i = 0; i < maplist.length - 1; i++) {
expect(maplist[i].stageId).not.toBe(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 },
]);
test("Keeps things fair when overlap", () => {
const mapList = generateMaps({
teams: [
{
id: 1,
maps: team1Picks,
},
{
id: 2,
maps: team2PicksWithSomeDuplication,
},
],
count: 7,
});
expect(mapList.length).toBe(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++;
}
}
expect(team1PicksAppeared).toBe(team2PicksAppeared);
});
test("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;
expect(mapList[j].source).not.toBe(mapList[j + 1].source);
}
}
});
test("Calculates all mode maps without tiebreaker", () => {
const mapList = generateMaps({
teams: [
{
id: 1,
maps: team1Picks,
},
{
id: 2,
maps: team2Picks,
},
],
count: 7,
tiebreakerMaps: new MapPool([]),
});
// the one map both of them picked
expect(mapList[6].stageId).toBe(7);
expect(mapList[6].mode).toBe("RM");
});
test("Calculates all mode maps without tiebreaker (no overlap)", () => {
const mapList = generateMaps({
teams: [
{
id: 1,
maps: team1Picks,
},
{
id: 2,
maps: team2PicksNoOverlap,
},
],
count: 7,
tiebreakerMaps: new MapPool([]),
});
// default map pool contains the tiebreaker
expect(
DEFAULT_MAP_POOL.stageModePairs.some(
(pair) =>
pair.stageId === mapList[6].stageId && pair.mode === mapList[6].mode,
),
).toBe(true);
// neither teams map pool contains the tiebreaker
expect(
team1Picks.stageModePairs.some(
(pair) =>
pair.stageId === mapList[6].stageId && pair.mode === mapList[6].mode,
),
).toBe(false);
expect(
team2PicksNoOverlap.stageModePairs.some(
(pair) =>
pair.stageId === mapList[6].stageId && pair.mode === mapList[6].mode,
),
).toBe(false);
});
const threeModesArgs: TournamentMaplistInput = {
count: 7,
seed: "1002",
modesIncluded: ["TC", "TW", "RM"],
tiebreakerMaps: new MapPool({
TW: [],
SZ: [],
TC: [],
RM: [],
CB: [],
}),
teams: [
{
id: 1002,
maps: new MapPool({
TW: [9, 7, 6, 5, 3, 2, 0],
SZ: [],
TC: [9, 8, 7, 4, 1, 6, 2],
RM: [9, 7, 6, 5, 3, 1, 0],
CB: [],
}),
},
{
id: 1001,
maps: new MapPool({
TW: [8, 7, 5, 2, 9, 4, 3],
SZ: [],
TC: [7, 6, 5, 3, 2, 0, 9],
RM: [9, 8, 6, 5, 3, 2, 7],
CB: [],
}),
},
],
};
test("generates list of modes included length > 1 && < 4", () => {
const maps = generateMaps(threeModesArgs);
expect(maps.length).toBe(7);
});
// paddling pool 264
test("handles 100% overlap in one mode and none in others", () => {
// should not throw
generateMaps({
count: 5,
followModeOrder: false,
modesIncluded: ["SZ", "TC", "RM", "CB"],
seed: "4866",
teams: [
{
id: 2317,
maps: new MapPool([
{
stageId: 2,
mode: "SZ",
},
{
stageId: 17,
mode: "SZ",
},
{
stageId: 2,
mode: "TC",
},
{
stageId: 10,
mode: "TC",
},
{
stageId: 0,
mode: "RM",
},
{
stageId: 3,
mode: "RM",
},
{
stageId: 6,
mode: "CB",
},
{
stageId: 21,
mode: "CB",
},
]),
},
{
id: 2322,
maps: new MapPool([
{
stageId: 7,
mode: "SZ",
},
{
stageId: 18,
mode: "SZ",
},
{
stageId: 2,
mode: "TC",
},
{
stageId: 10,
mode: "TC",
},
{
stageId: 2,
mode: "RM",
},
{
stageId: 19,
mode: "RM",
},
{
stageId: 7,
mode: "CB",
},
{
stageId: 18,
mode: "CB",
},
]),
},
],
tiebreakerMaps: new MapPool([
{
stageId: 15,
mode: "SZ",
},
{
stageId: 0,
mode: "CB",
},
{
stageId: 16,
mode: "RM",
},
{
stageId: 8,
mode: "TC",
},
]),
});
});
});
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 },
]);
describe("TournamentMapListGeneratorOneMode", () => {
test("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++) {
expect(mapList[i].mode).toBe("SZ");
}
});
test("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++) {
expect(mapList[i].mode).toBe("SZ");
}
});
test("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));
expect(stages.size).toBe(5);
});
test("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];
expect(last?.mode).toBe("SZ");
expect(last?.stageId).toBe(9);
}
});
test("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];
expect(
team1SZPicks.stageModePairs.some(
({ stageId }) => stageId === last?.stageId,
),
).toBe(false);
expect(
team2SZPicksNoOverlap.stageModePairs.some(
({ stageId }) => stageId === last?.stageId,
),
).toBe(false);
});
test("Handles worst case duplication", () => {
const mapList = generateMaps({
teams: [
{
id: 1,
maps: team1SZPicks,
},
{
id: 2,
maps: team1SZPicks,
},
],
modesIncluded: ["SZ"],
tiebreakerMaps: new MapPool([]),
count: 7,
});
for (const [i, stage] of mapList.entries()) {
if (i === 6) {
expect(stage?.source).toBe("TIEBREAKER");
} else {
expect(stage?.source).toBe("BOTH");
}
}
});
test("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) {
expect(stage.source).toBe(1);
}
});
test('Throws if including modes not specified in "modesIncluded"', () => {
expect(() =>
generateMaps({
teams: [
{
id: 1,
maps: team1Picks,
},
{
id: 2,
maps: new MapPool([]),
},
],
modesIncluded: ["SZ"],
}),
).toThrow();
});
test("Throws if duplicate maps in the pool", () => {
expect(() =>
generateMaps({
teams: [
{
id: 1,
maps: new MapPool([
{ mode: "SZ", stageId: 1 },
{ mode: "SZ", stageId: 1 },
]),
},
{
id: 2,
maps: new MapPool([]),
},
],
modesIncluded: ["SZ"],
}),
).toThrowError("Duplicate map");
});
});
describe("Recently played maps", () => {
test("Avoids recently played maps when possible", () => {
const recentlyPlayedMaps = [
{ mode: "SZ" as const, stageId: 4 as const },
{ mode: "TC" as const, stageId: 5 as const },
];
const mapList = generateMaps({
seed: "recent-test",
recentlyPlayedMaps,
});
const hasRecentMap = mapList.some((map) =>
recentlyPlayedMaps.some(
(recent) => recent.mode === map.mode && recent.stageId === map.stageId,
),
);
expect(hasRecentMap).toBe(false);
});
test("Works correctly with no recently played maps", () => {
const mapList = generateMaps({
recentlyPlayedMaps: [],
});
expect(mapList.length).toBe(5);
});
test("Penalties decrease for maps further back in history", () => {
const recentlyPlayedMaps = [
{ mode: "SZ" as const, stageId: 4 as const },
{ mode: "SZ" as const, stageId: 5 as const },
{ mode: "TC" as const, stageId: 5 as const },
{ mode: "TC" as const, stageId: 6 as const },
{ mode: "RM" as const, stageId: 7 as const },
{ mode: "RM" as const, stageId: 8 as const },
];
const mapListWithRecent = generateMaps({
seed: "history-test",
recentlyPlayedMaps,
});
const hasVeryRecentMap = mapListWithRecent.some((map) =>
recentlyPlayedMaps
.slice(0, 2)
.some(
(recent) =>
recent.mode === map.mode && recent.stageId === map.stageId,
),
);
expect(hasVeryRecentMap).toBe(false);
});
test("Still generates valid maplist even with many recently played maps", () => {
const recentlyPlayedMaps = [
...team1Picks.stageModePairs,
...team2Picks.stageModePairs,
];
const mapList = generateMaps({
seed: "many-recent",
recentlyPlayedMaps,
});
expect(mapList.length).toBe(5);
});
});