Map list generating algorithm modes

This commit is contained in:
Kalle (Sendou) 2021-12-16 09:00:10 +02:00
parent 1398dadc31
commit 30d82e92d3
8 changed files with 213 additions and 74 deletions

View File

@ -1,31 +0,0 @@
import { Mode, Stage } from ".prisma/client";
import { EliminationBracket } from "./tournament/bracket";
interface MapDesirability {
stageName: string;
value: number;
originalValue: number;
modes: Mode[];
originalModes: Mode[];
}
export function generateMapListMapForRounds({
mapPool,
rounds,
}: {
mapPool: Stage[];
rounds: EliminationBracket<number[]>;
}): EliminationBracket<Stage[][]> {
const modes = Array.from(new Set(mapPool.map((stage) => stage.mode)));
return {
winners: rounds.winners.map((round) => roundsMapList(round)),
losers: rounds.losers.map((round) => roundsMapList(round)),
};
function roundsMapList(bestOf: number): Stage[] {
return new Array(bestOf)
.fill(null)
.map(() => ({ id: -1, name: "The Reef", mode: "SZ" }));
}
}

View File

@ -1,6 +1,6 @@
import type { BracketType, Stage } from ".prisma/client";
import invariant from "tiny-invariant";
import { generateMapListMapForRounds } from "../mapList";
import { generateMapListForRounds } from "./mapList";
import type { Bracket } from "./algorithms";
export function participantCountToRoundsInfo({
@ -18,7 +18,7 @@ export function participantCountToRoundsInfo({
> {
const roundNames = getRoundNames(bracket);
const roundsDefaultBestOf = getRoundsDefaultBestOf(bracket);
const mapList = generateMapListMapForRounds({
const mapList = generateMapListForRounds({
mapPool,
rounds: roundsDefaultBestOf,
});
@ -29,6 +29,8 @@ export function participantCountToRoundsInfo({
winners: roundNames.winners.map((roundName, i) => {
const bestOf = roundsDefaultBestOf.winners[i];
const maps = mapList.winners[i];
invariant(bestOf, "bestOf undefined in winners");
invariant(maps, "maps undefined in winners");
return {
name: roundName,
bestOf,
@ -38,6 +40,8 @@ export function participantCountToRoundsInfo({
losers: roundNames.losers.map((roundName, i) => {
const bestOf = roundsDefaultBestOf.losers[i];
const maps = mapList.losers[i];
invariant(bestOf, "bestOf undefined in losers");
invariant(maps, "bestOf undefined in losers");
return {
name: roundName,
bestOf,
@ -53,9 +57,11 @@ const GRAND_FINALS_DEFAULT = 7;
const LOSERS_DEFAULT = 3;
const LOSERS_FINALS_DEFAULT = 5;
function getRoundsDefaultBestOf(
export type BestOf = 3 | 5 | 7 | 9;
export function getRoundsDefaultBestOf(
bracket: Bracket
): EliminationBracket<number[]> {
): EliminationBracket<BestOf[]> {
const { winners: winnersRoundCount, losers: losersRoundCount } =
countRounds(bracket);
@ -106,8 +112,8 @@ export function countRounds(bracket: Bracket): EliminationBracket<number> {
while (true) {
losers++;
const match1 = losersMatch.match1;
const match2 = losersMatch.match2;
const match1 = losersMatch?.match1;
const match2 = losersMatch?.match2;
if (match1 && losersMatchIds.has(match1.id)) {
losersMatch = match1;
continue;
@ -144,7 +150,7 @@ export function countRounds(bracket: Bracket): EliminationBracket<number> {
export function resolveTournamentFormatString(
brackets: { type: BracketType }[]
) {
invariant(brackets.length > 0, "Unexpected no brackets");
invariant(brackets[0], "no brackets");
return brackets[0].type === "DE"
? "Double Elimination"
: "Single Elimination";

View File

@ -0,0 +1,35 @@
import { Mode, Stage } from ".prisma/client";
import invariant from "tiny-invariant";
import { suite } from "uvu";
import * as assert from "uvu/assert";
import { eliminationBracket } from "./algorithms";
import { getRoundsDefaultBestOf } from "./bracket";
import { generateMapListForRounds } from "./mapList";
const MapListForRounds = suite("generateMapListMapForRounds()");
const modes: Mode[] = ["SZ", "TC", "RM", "CB"];
const mapPool: Stage[] = ["a", "b", "c", "d", "e", "f", "g"].map((name, i) => {
const mode = modes.shift();
invariant(mode, "Unexpected no mode");
modes.push(mode);
return { name, id: i, mode };
});
const bracket = eliminationBracket(100, "DE");
const rounds = getRoundsDefaultBestOf(bracket);
MapListForRounds("No mode is repeated in the same round", () => {
const bracket = generateMapListForRounds({ mapPool, rounds });
for (const side of [bracket.winners, bracket.losers]) {
for (const round of side) {
for (const [i, stage] of round.entries()) {
if (i === 0) continue;
assert.not.equal(stage.mode, round[i - 1]?.mode);
}
}
}
});
MapListForRounds.run();

View File

@ -0,0 +1,117 @@
import { Mode, Stage } from ".prisma/client";
import shuffle from "just-shuffle";
import invariant from "tiny-invariant";
import clone from "just-clone";
import { BestOf, EliminationBracket } from "./bracket";
export function generateMapListForRounds({
mapPool,
rounds,
}: {
mapPool: Stage[];
rounds: EliminationBracket<BestOf[]>;
}): EliminationBracket<Stage[][]> {
const modes = mapPool.reduce((acc: [Mode, number][], cur) => {
if (cur.mode === "SZ") return acc;
if (acc.some(([mode]) => mode === cur.mode)) {
return acc.map((modeTuple) =>
modeTuple[0] === cur.mode
? ([modeTuple[0], ++modeTuple[1]] as [Mode, number])
: modeTuple
);
}
acc.push([cur.mode, 1]);
return acc;
}, []);
let currentModes = clone(modes);
const hasSZ = mapPool.some((stage) => stage.mode === "SZ");
return {
winners: rounds.winners.map((round) => roundsMapList(round)),
losers: rounds.losers.map((round) => roundsMapList(round)),
};
function roundsMapList(bestOf: BestOf): Stage[] {
const modes = resolveModes(bestOf);
return new Array(bestOf).fill(null).map((_, i) => {
const mode = modes[i];
invariant(mode, "mode undefined");
return { id: -1, name: "The Reef", mode };
});
}
function resolveModes(bestOf: BestOf): Mode[] {
let result: (Mode | null)[] = [];
/** 0, 1, 2, 3, 4 */
const amountOfSZToAdd = !hasSZ ? 0 : Math.floor(bestOf / 2);
for (let _ = 0; _ < amountOfSZToAdd; _++) {
result.push("SZ");
}
while (result.length < bestOf) {
result.push(null);
}
result = shuffle(result);
while (SZInInvalidPosition(result)) {
result = shuffle(result);
}
let resultWithNoNull: Mode[] = [];
for (let i = 0; i < result.length; i++) {
const element = result[i];
// Is SZ
if (element) {
resultWithNoNull.push(element);
continue;
}
const previous = resultWithNoNull[i - 1];
invariant(previous !== null, "previous is null");
if (currentModes.every(([_mode, count]) => count === 0)) {
currentModes = clone(modes);
}
resetCurrentModesIfWouldHaveToRepeatMode(previous);
currentModes = shuffle(currentModes);
currentModes.sort((a, b) => {
if (previous && previous === a[0]) return 1;
if (previous && previous === b[0]) return -1;
return b[1] - a[1];
});
const nextModeTuple = currentModes[0];
invariant(nextModeTuple, "nextModeTuple undefined");
invariant(previous !== nextModeTuple[0], "Repeating mode");
resultWithNoNull.push(nextModeTuple[0]);
nextModeTuple[1]--;
}
return resultWithNoNull;
}
function SZInInvalidPosition(modes: (Mode | null)[]) {
for (const [i, mode] of modes.entries()) {
if (i === 0) continue;
if (!mode) continue;
if (mode === modes[i - 1]) return true;
}
return false;
}
function resetCurrentModesIfWouldHaveToRepeatMode(previous?: Mode | null) {
if (!previous) return;
const modesLeft = currentModes.flatMap(([mode, count]) =>
count === 0 ? [] : mode
);
if (modesLeft.length > 1) return;
invariant(modesLeft[0], "modesLeft[0] is undefined");
if (modesLeft[0][0] !== previous) return;
currentModes = clone(modes);
}
}

View File

@ -1,5 +1,6 @@
import { useMatches } from "remix";
import { useTournamentRounds } from "~/hooks/useTournamentRounds";
import type { UseTournamentRoundsState } from "~/hooks/useTournamentRounds/types";
import type { FindTournamentByNameForUrlI } from "~/services/tournament";
// TODO: handle warning if check-in has not concluded
@ -9,47 +10,45 @@ export default function StartBracketTab() {
const [rounds, dispatch] = useTournamentRounds({
mapPool,
teams,
// TODO: and also below don't show losers if SE
type: "DE",
});
return (
<div>
<h2>Winners</h2>
{rounds.winners.map((round) => {
return (
// TODO: key potentially unstable
<section key={round.name}>
<h3>{round.name}</h3>
<ol>
{round.mapList.map((stage) => {
return (
<li key={stage.id}>
{stage.name} {stage.mode}
</li>
);
})}
</ol>
</section>
);
})}
<h2>Losers</h2>
{rounds.losers.map((round) => {
return (
// TODO: key potentially unstable
<section key={round.name}>
<h3>{round.name}</h3>
<ol>
{round.mapList.map((stage) => {
return (
<li key={stage.id}>
{stage.name} {stage.mode}
</li>
);
})}
</ol>
</section>
);
})}
<RoundsCollection side="Winners" rounds={rounds.winners} />
<RoundsCollection side="Losers" rounds={rounds.losers} />
</div>
);
}
function RoundsCollection({
side,
rounds,
}: {
side: "Winners" | "Losers";
rounds: UseTournamentRoundsState["winners"];
}) {
return (
<>
<h2>{side}</h2>
{rounds.map((round) => {
return (
// TODO: key potentially unstable
<section key={round.name}>
<h3>{round.name}</h3>
<ol>
{round.mapList.map((stage) => {
return (
<li key={stage.id}>
{stage.mode} {stage.name}
</li>
);
})}
</ol>
</section>
);
})}
</>
);
}

11
package-lock.json generated
View File

@ -18,6 +18,7 @@
"cross-env": "^7.0.3",
"express": "^4.17.1",
"express-session": "^1.17.2",
"just-clone": "^5.0.1",
"just-shuffle": "^4.0.1",
"morgan": "^1.10.0",
"passport": "^0.5.0",
@ -4078,6 +4079,11 @@
"verror": "1.10.0"
}
},
"node_modules/just-clone": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/just-clone/-/just-clone-5.0.1.tgz",
"integrity": "sha512-y6hFQ8X4fhJTZzdNvrE9mnCQX33uN4xV0hZ+5WNgs6yCcrRzs9ieFXuYRKznWqQ/gOSZpJu0yQ6hRHLCU6ezyw=="
},
"node_modules/just-shuffle": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/just-shuffle/-/just-shuffle-4.0.1.tgz",
@ -11546,6 +11552,11 @@
"verror": "1.10.0"
}
},
"just-clone": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/just-clone/-/just-clone-5.0.1.tgz",
"integrity": "sha512-y6hFQ8X4fhJTZzdNvrE9mnCQX33uN4xV0hZ+5WNgs6yCcrRzs9ieFXuYRKznWqQ/gOSZpJu0yQ6hRHLCU6ezyw=="
},
"just-shuffle": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/just-shuffle/-/just-shuffle-4.0.1.tgz",

View File

@ -34,6 +34,7 @@
"cross-env": "^7.0.3",
"express": "^4.17.1",
"express-session": "^1.17.2",
"just-clone": "^5.0.1",
"just-shuffle": "^4.0.1",
"morgan": "^1.10.0",
"passport": "^0.5.0",

View File

@ -11,6 +11,7 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"types": ["cypress"],
"paths": {
"~/*": ["./app/*"]