diff --git a/app/core/tournament/mapList.test.ts b/app/core/tournament/mapList.test.ts index 9f087e259..309c1eede 100644 --- a/app/core/tournament/mapList.test.ts +++ b/app/core/tournament/mapList.test.ts @@ -1,4 +1,4 @@ -import { Stage } from ".prisma/client"; +import type { Mode, Stage } from ".prisma/client"; import clone from "just-clone"; import { suite } from "uvu"; import * as assert from "uvu/assert"; @@ -8,6 +8,7 @@ import { generateMapListForRounds } from "./mapList"; const MapListForRounds = suite("generateMapListMapForRounds()"); +const ALL_MODES_LENGTH = 4; const mapPool: Stage[] = JSON.parse( `[{"id":923,"mode":"TC","name":"The Reef"},{"id":925,"mode":"CB","name":"The Reef"},{"id":927,"mode":"SZ","name":"Musselforge Fitness"},{"id":929,"mode":"RM","name":"Musselforge Fitness"},{"id":934,"mode":"RM","name":"Starfish Mainstage"},{"id":942,"mode":"SZ","name":"Inkblot Art Academy"},{"id":943,"mode":"TC","name":"Inkblot Art Academy"},{"id":947,"mode":"SZ","name":"Sturgeon Shipyard"},{"id":948,"mode":"TC","name":"Sturgeon Shipyard"},{"id":953,"mode":"TC","name":"Moray Towers"},{"id":959,"mode":"RM","name":"Port Mackerel"},{"id":960,"mode":"CB","name":"Port Mackerel"},{"id":972,"mode":"SZ","name":"Snapper Canal"},{"id":978,"mode":"TC","name":"Blackbelly Skatepark"},{"id":980,"mode":"CB","name":"Blackbelly Skatepark"},{"id":985,"mode":"CB","name":"MakoMart"},{"id":987,"mode":"SZ","name":"Walleye Warehouse"},{"id":988,"mode":"TC","name":"Walleye Warehouse"},{"id":994,"mode":"RM","name":"Shellendorf Institute"},{"id":995,"mode":"CB","name":"Shellendorf Institute"},{"id":1007,"mode":"SZ","name":"Piranha Pit"},{"id":1012,"mode":"SZ","name":"Camp Triggerfish"},{"id":1019,"mode":"RM","name":"Wahoo World"},{"id":1020,"mode":"CB","name":"Wahoo World"},{"id":1027,"mode":"SZ","name":"Ancho-V Games"},{"id":1034,"mode":"RM","name":"Skipper Pavilion"}]` ); @@ -42,4 +43,25 @@ MapListForRounds("Should have all the map and mode combos", () => { assert.equal(mapPoolToEmpty.length, 0); }); +MapListForRounds( + "Should not repeat mode (except SZ) in a round before other modes have appeared", + () => { + for (const side of [mapList.winners, mapList.losers]) { + for (const round of side) { + const modes: Mode[] = []; + for (const stage of round) { + if ( + modes.includes(stage.mode) && + modes.length < ALL_MODES_LENGTH && + stage.mode !== "SZ" + ) { + throw new Error(`Repeated mode: ${JSON.stringify(round, null, 2)}`); + } + modes.push(stage.mode); + } + } + } + } +); + MapListForRounds.run(); diff --git a/app/core/tournament/mapList.ts b/app/core/tournament/mapList.ts index d674af5ef..e2b6dafd7 100644 --- a/app/core/tournament/mapList.ts +++ b/app/core/tournament/mapList.ts @@ -27,6 +27,7 @@ export function generateMapListForRounds({ }, []); let currentModes = clone(modes); const hasSZ = mapPool.some((stage) => stage.mode === "SZ"); + const allModesLength = modes.length + Number(hasSZ); return { winners: rounds.winners.map((round) => roundsMapList(round)), @@ -81,8 +82,18 @@ export function generateMapListForRounds({ resetCurrentModesIfWouldHaveToRepeatMode(previous); currentModes = shuffle(currentModes); currentModes.sort((a, b) => { - if (previous && previous === a[0]) return 1; - if (previous && previous === b[0]) return -1; + // Don't repeat a mode before all the modes have appeared in a round + const modesOfTheRound = Array.from(new Set(resultWithNoNull)); + if (modesOfTheRound.length < allModesLength) { + if ( + modesOfTheRound.includes(a[0]) && + modesOfTheRound.includes(b[0]) + ) { + return 0; + } + if (modesOfTheRound.includes(b[0])) return -1; + if (modesOfTheRound.includes(a[0])) return 1; + } return b[1] - a[1]; }); @@ -171,7 +182,7 @@ export function generateMapListForRounds({ !stagesAlreadyIncludedThisRound.includes(mapPoolMap.name) ); - // TODO: handle this smarter + // TODO: handle this fallback behavior smarter if (!stage) { stage = stages.find((mapPoolMap) => mapPoolMap.modes.includes(mode)); }