mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-26 09:20:24 -05:00
226 lines
5.2 KiB
TypeScript
226 lines
5.2 KiB
TypeScript
import * as R from "remeda";
|
|
import type { TournamentRoundMaps } from "~/db/tables";
|
|
import type {
|
|
ModeShort,
|
|
ModeWithStage,
|
|
StageId,
|
|
} from "~/modules/in-game-lists/types";
|
|
import type { TournamentMapListMap } from "~/modules/tournament-map-list-generator";
|
|
import invariant from "~/utils/invariant";
|
|
import { logger } from "~/utils/logger";
|
|
import { assertUnreachable } from "~/utils/types";
|
|
import { isSetOverByResults } from "../tournament-bracket-utils";
|
|
import type { TournamentDataTeam } from "./Tournament.server";
|
|
|
|
export const types = [
|
|
"COUNTERPICK",
|
|
"COUNTERPICK_MODE_REPEAT_OK",
|
|
"BAN_2",
|
|
] as const;
|
|
export type Type = (typeof types)[number];
|
|
|
|
export function turnOf({
|
|
results,
|
|
maps,
|
|
teams,
|
|
mapList,
|
|
}: {
|
|
results: Array<{ winnerTeamId: number }>;
|
|
maps: TournamentRoundMaps;
|
|
teams: [number, number];
|
|
mapList?: TournamentMapListMap[] | null;
|
|
}) {
|
|
if (!maps.pickBan) return null;
|
|
if (!mapList) return null;
|
|
|
|
switch (maps.pickBan) {
|
|
case "BAN_2": {
|
|
// typically lower seed is the "bottom team" and they pick first
|
|
const [secondPicker, firstPicker] = teams;
|
|
|
|
if (
|
|
!mapList.some((map) => map.bannedByTournamentTeamId === firstPicker)
|
|
) {
|
|
return firstPicker;
|
|
}
|
|
|
|
if (
|
|
!mapList.some((map) => map.bannedByTournamentTeamId === secondPicker)
|
|
) {
|
|
return secondPicker;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
case "COUNTERPICK_MODE_REPEAT_OK":
|
|
case "COUNTERPICK": {
|
|
// there exists an unplayed map
|
|
if (mapList.length > results.length) return null;
|
|
|
|
if (
|
|
isSetOverByResults({ count: maps.count, results, countType: maps.type })
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
const latestWinner = results[results.length - 1]?.winnerTeamId;
|
|
invariant(latestWinner, "turnOf: No winner found");
|
|
|
|
const result = teams.find(
|
|
(tournamentTeamId) => latestWinner !== tournamentTeamId,
|
|
);
|
|
invariant(result, "turnOf: No result found");
|
|
|
|
return result;
|
|
}
|
|
default: {
|
|
assertUnreachable(maps.pickBan);
|
|
}
|
|
}
|
|
}
|
|
|
|
export function isLegal({
|
|
map,
|
|
...rest
|
|
}: MapListWithStatusesArgs & { map: ModeWithStage }) {
|
|
const pool = mapsListWithLegality(rest);
|
|
|
|
return pool.some(
|
|
(m) => m.mode === map.mode && m.stageId === map.stageId && m.isLegal,
|
|
);
|
|
}
|
|
|
|
interface MapListWithStatusesArgs {
|
|
results: Array<{ mode: ModeShort; stageId: StageId; winnerTeamId: number }>;
|
|
maps: TournamentRoundMaps | null;
|
|
mapList: TournamentMapListMap[] | null;
|
|
teams: [TournamentDataTeam, TournamentDataTeam];
|
|
pickerTeamId: number;
|
|
tieBreakerMapPool: ModeWithStage[];
|
|
toSetMapPool: Array<{ mode: ModeShort; stageId: StageId }>;
|
|
}
|
|
export function mapsListWithLegality(args: MapListWithStatusesArgs) {
|
|
const mapPool = (() => {
|
|
if (!args.maps?.pickBan) return [];
|
|
switch (args.maps.pickBan) {
|
|
case "BAN_2": {
|
|
if (!args.mapList) {
|
|
logger.warn("mapsListWithLegality: mapList is empty");
|
|
return [];
|
|
}
|
|
return args.mapList;
|
|
}
|
|
case "COUNTERPICK_MODE_REPEAT_OK":
|
|
case "COUNTERPICK": {
|
|
if (args.toSetMapPool.length === 0) {
|
|
const combinedPools = [
|
|
...(args.teams[0].mapPool ?? []),
|
|
...(args.teams[1].mapPool ?? []),
|
|
...args.tieBreakerMapPool,
|
|
];
|
|
|
|
const result: ModeWithStage[] = [];
|
|
for (const map of combinedPools) {
|
|
if (
|
|
!result.some(
|
|
(m) => m.mode === map.mode && m.stageId === map.stageId,
|
|
)
|
|
) {
|
|
result.push(map);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
return args.toSetMapPool;
|
|
}
|
|
default: {
|
|
assertUnreachable(args.maps.pickBan);
|
|
}
|
|
}
|
|
})();
|
|
|
|
const modesIncluded = R.unique(mapPool.map((m) => m.mode));
|
|
|
|
const unavailableStagesSet = unavailableStages(args);
|
|
const unavailableModesSetAll = unavailableModes(args);
|
|
const unavailableModesSet =
|
|
// one mode tournament
|
|
unavailableModesSetAll.size < modesIncluded.length
|
|
? unavailableModesSetAll
|
|
: new Set();
|
|
|
|
const result = mapPool.map((map) => {
|
|
const isLegal =
|
|
!unavailableStagesSet.has(map.stageId) &&
|
|
!unavailableModesSet.has(map.mode);
|
|
|
|
return { ...map, isLegal };
|
|
});
|
|
|
|
const everythingBanned = result.every((map) => !map.isLegal);
|
|
if (everythingBanned) {
|
|
return result.map((map) => ({ ...map, isLegal: true }));
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function unavailableStages({
|
|
results,
|
|
mapList,
|
|
maps,
|
|
}: {
|
|
results: Array<{ mode: ModeShort; stageId: StageId }>;
|
|
mapList?: TournamentMapListMap[] | null;
|
|
maps: TournamentRoundMaps | null;
|
|
}): Set<StageId> {
|
|
if (!maps?.pickBan) return new Set();
|
|
|
|
switch (maps.pickBan) {
|
|
case "BAN_2": {
|
|
return new Set(
|
|
mapList
|
|
?.filter((m) => m.bannedByTournamentTeamId)
|
|
.map((map) => map.stageId) ?? [],
|
|
);
|
|
}
|
|
case "COUNTERPICK_MODE_REPEAT_OK":
|
|
case "COUNTERPICK": {
|
|
return new Set(results.map((result) => result.stageId));
|
|
}
|
|
default: {
|
|
assertUnreachable(maps.pickBan);
|
|
}
|
|
}
|
|
}
|
|
|
|
function unavailableModes({
|
|
results,
|
|
pickerTeamId,
|
|
maps,
|
|
}: {
|
|
results: Array<{ mode: ModeShort; winnerTeamId: number }>;
|
|
pickerTeamId: number;
|
|
maps: TournamentRoundMaps | null;
|
|
}): Set<ModeShort> {
|
|
if (
|
|
!maps?.pickBan ||
|
|
maps.pickBan === "BAN_2" ||
|
|
maps.pickBan === "COUNTERPICK_MODE_REPEAT_OK"
|
|
) {
|
|
return new Set();
|
|
}
|
|
|
|
// can't pick the same mode last won on
|
|
const result = new Set(
|
|
results
|
|
.filter((result) => result.winnerTeamId === pickerTeamId)
|
|
.slice(-1)
|
|
.map((result) => result.mode),
|
|
);
|
|
|
|
return result;
|
|
}
|