mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Map list generating algorithm modes
This commit is contained in:
parent
1398dadc31
commit
30d82e92d3
|
|
@ -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" }));
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
|
|
|||
35
app/core/tournament/mapList.test.ts
Normal file
35
app/core/tournament/mapList.test.ts
Normal 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();
|
||||
117
app/core/tournament/mapList.ts
Normal file
117
app/core/tournament/mapList.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
11
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@
|
|||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"types": ["cypress"],
|
||||
"paths": {
|
||||
"~/*": ["./app/*"]
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user