mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-04 11:22:40 -05:00
307 lines
8.5 KiB
TypeScript
307 lines
8.5 KiB
TypeScript
import { useState } from "react";
|
|
import { Outlet } from "react-router";
|
|
import { SendouButton } from "~/components/elements/Button";
|
|
import { SendouSwitch } from "~/components/elements/Switch";
|
|
import { Input } from "~/components/Input";
|
|
import { Label } from "~/components/Label";
|
|
import { Main } from "~/components/Main";
|
|
import type { Tables } from "~/db/tables";
|
|
import type { Bracket as BracketType } from "~/features/tournament-bracket/core/Bracket";
|
|
import * as Engine from "~/features/tournament-bracket/core/engine";
|
|
import type { TournamentManagerDataSet } from "~/features/tournament-bracket/core/engine/types";
|
|
import styles from "../bracket-test.module.css";
|
|
|
|
type FormatType = Tables["TournamentStage"]["type"];
|
|
|
|
const FORMAT_OPTIONS: { value: FormatType; label: string }[] = [
|
|
{ value: "single_elimination", label: "Single Elim" },
|
|
{ value: "double_elimination", label: "Double Elim" },
|
|
{ value: "round_robin", label: "Round Robin" },
|
|
{ value: "swiss", label: "Swiss" },
|
|
];
|
|
|
|
export default function BracketTestLayout() {
|
|
const [format, setFormat] = useState<FormatType>("double_elimination");
|
|
const [teamCount, setTeamCount] = useState(8);
|
|
const [bracketExpanded, setBracketExpanded] = useState(true);
|
|
const [completedRounds, setCompletedRounds] = useState(0);
|
|
const [completedWbRounds, setCompletedWbRounds] = useState(0);
|
|
const [completedLbRounds, setCompletedLbRounds] = useState(0);
|
|
|
|
const clampedTeamCount = Math.max(2, teamCount);
|
|
const teams = generateTeams(clampedTeamCount);
|
|
const teamIds = teams.map((t) => t.id);
|
|
|
|
const data = generateBracketData(format, teamIds);
|
|
const isDoubleElim = format === "double_elimination";
|
|
const { totalRounds, wbRounds, lbRounds } = countRounds(data, isDoubleElim);
|
|
|
|
if (isDoubleElim) {
|
|
simulateCompletedRoundsByGroup(data, completedWbRounds, completedLbRounds);
|
|
} else {
|
|
simulateCompletedRounds(data, Math.min(completedRounds, totalRounds));
|
|
}
|
|
|
|
const mockTournament = {
|
|
ctx: {
|
|
id: 1,
|
|
name: "Bracket Test",
|
|
isFinalized: 0,
|
|
castedMatchesInfo: null,
|
|
teams,
|
|
settings: {
|
|
bracketProgression: [
|
|
{
|
|
name: "Test Bracket",
|
|
type: format,
|
|
requiresCheckIn: false,
|
|
settings: {},
|
|
},
|
|
],
|
|
},
|
|
bracketProgressionOverrides: [],
|
|
participatedUsers: teamIds,
|
|
},
|
|
brackets: [] as unknown[],
|
|
teamById: (id: number) => teams.find((t) => t.id === id) ?? null,
|
|
teamMemberOfByUser: () => null,
|
|
isOrganizer: () => false,
|
|
tournamentTeamLogoSrc: () => null,
|
|
streamingParticipantIds: [] as number[],
|
|
streams: [] as unknown[],
|
|
isLeagueDivision: false,
|
|
};
|
|
|
|
const mockBracket = {
|
|
id: 1,
|
|
idx: 0,
|
|
preview: false,
|
|
data,
|
|
type: format,
|
|
name: "Test Bracket",
|
|
canBeStarted: false,
|
|
tournament: mockTournament,
|
|
settings: format === "swiss" ? { roundCount: 5 } : null,
|
|
sources: undefined,
|
|
seeding: undefined,
|
|
createdAt: null,
|
|
requiresCheckIn: false,
|
|
startTime: null,
|
|
simulatedMatch: () => undefined,
|
|
currentStandings: () => [],
|
|
participantTournamentTeamIds: teamIds,
|
|
everyMatchOver: false,
|
|
isUnderground: false,
|
|
} as unknown as BracketType;
|
|
|
|
mockTournament.brackets = [mockBracket];
|
|
|
|
return (
|
|
<Main bigger>
|
|
<h1 className="text-lg">Bracket Test</h1>
|
|
<div className={styles.settings}>
|
|
<div className={styles.settingGroup}>
|
|
<Label>Format</Label>
|
|
<div className="stack horizontal sm">
|
|
{FORMAT_OPTIONS.map((opt) => (
|
|
<SendouButton
|
|
key={opt.value}
|
|
variant={format === opt.value ? undefined : "outlined"}
|
|
size="small"
|
|
onPress={() => setFormat(opt.value)}
|
|
>
|
|
{opt.label}
|
|
</SendouButton>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className={styles.settingGroup}>
|
|
<Label htmlFor="team-count">Teams</Label>
|
|
<Input
|
|
id="team-count"
|
|
type="number"
|
|
min={2}
|
|
max={128}
|
|
value={String(teamCount)}
|
|
onChange={(e) => setTeamCount(Number(e.target.value))}
|
|
className={styles.teamCountInput}
|
|
/>
|
|
</div>
|
|
{isDoubleElim ? (
|
|
<>
|
|
<div className={styles.settingGroup}>
|
|
<Label htmlFor="completed-wb">Completed WB rounds</Label>
|
|
<Input
|
|
id="completed-wb"
|
|
type="number"
|
|
min={0}
|
|
max={wbRounds}
|
|
value={String(Math.min(completedWbRounds, wbRounds))}
|
|
onChange={(e) => setCompletedWbRounds(Number(e.target.value))}
|
|
className={styles.teamCountInput}
|
|
/>
|
|
</div>
|
|
<div className={styles.settingGroup}>
|
|
<Label htmlFor="completed-lb">Completed LB rounds</Label>
|
|
<Input
|
|
id="completed-lb"
|
|
type="number"
|
|
min={0}
|
|
max={lbRounds}
|
|
value={String(Math.min(completedLbRounds, lbRounds))}
|
|
onChange={(e) => setCompletedLbRounds(Number(e.target.value))}
|
|
className={styles.teamCountInput}
|
|
/>
|
|
</div>
|
|
</>
|
|
) : (
|
|
<div className={styles.settingGroup}>
|
|
<Label htmlFor="completed-rounds">Completed rounds</Label>
|
|
<Input
|
|
id="completed-rounds"
|
|
type="number"
|
|
min={0}
|
|
max={totalRounds}
|
|
value={String(Math.min(completedRounds, totalRounds))}
|
|
onChange={(e) => setCompletedRounds(Number(e.target.value))}
|
|
className={styles.teamCountInput}
|
|
/>
|
|
</div>
|
|
)}
|
|
<div className={styles.settingGroup}>
|
|
<SendouSwitch
|
|
id="expanded"
|
|
isSelected={bracketExpanded}
|
|
onChange={setBracketExpanded}
|
|
>
|
|
Expanded
|
|
</SendouSwitch>
|
|
</div>
|
|
</div>
|
|
<Outlet
|
|
context={{
|
|
tournament: mockTournament,
|
|
bracketExpanded,
|
|
setBracketExpanded,
|
|
hasChildTournaments: false,
|
|
preparedMaps: null,
|
|
bracket: mockBracket,
|
|
}}
|
|
/>
|
|
</Main>
|
|
);
|
|
}
|
|
|
|
function generateTeams(count: number) {
|
|
return Array.from({ length: count }, (_, i) => ({
|
|
id: i + 1,
|
|
name: `Team ${i + 1}`,
|
|
seed: i + 1,
|
|
members: [{ userId: i + 1, username: `Player${i + 1}` }],
|
|
droppedOut: 0,
|
|
}));
|
|
}
|
|
|
|
function countRounds(data: TournamentManagerDataSet, isDoubleElim: boolean) {
|
|
const totalRounds = Math.max(...data.round.map((r) => r.number));
|
|
|
|
if (!isDoubleElim) return { totalRounds, wbRounds: 0, lbRounds: 0 };
|
|
|
|
const wbGroupId = data.group.find((g) => g.number === 1)?.id;
|
|
const lbGroupId = data.group.find((g) => g.number === 2)?.id;
|
|
|
|
const wbRounds = data.round.filter((r) => r.group_id === wbGroupId).length;
|
|
const lbRounds = data.round.filter((r) => r.group_id === lbGroupId).length;
|
|
|
|
return { totalRounds, wbRounds, lbRounds };
|
|
}
|
|
|
|
function simulateCompletedRoundsByGroup(
|
|
data: TournamentManagerDataSet,
|
|
wbCompleted: number,
|
|
lbCompleted: number,
|
|
) {
|
|
const wbGroupId = data.group.find((g) => g.number === 1)?.id;
|
|
const lbGroupId = data.group.find((g) => g.number === 2)?.id;
|
|
|
|
const completedRoundIds = new Set<number>();
|
|
for (const round of data.round) {
|
|
if (round.group_id === wbGroupId && round.number <= wbCompleted) {
|
|
completedRoundIds.add(round.id);
|
|
}
|
|
if (round.group_id === lbGroupId && round.number <= lbCompleted) {
|
|
completedRoundIds.add(round.id);
|
|
}
|
|
}
|
|
|
|
markMatchesCompleted(data, completedRoundIds);
|
|
}
|
|
|
|
function simulateCompletedRounds(
|
|
data: TournamentManagerDataSet,
|
|
completedRounds: number,
|
|
) {
|
|
if (completedRounds <= 0) return;
|
|
|
|
const roundsByNumber = new Map<number, number[]>();
|
|
for (const round of data.round) {
|
|
const existing = roundsByNumber.get(round.number) ?? [];
|
|
existing.push(round.id);
|
|
roundsByNumber.set(round.number, existing);
|
|
}
|
|
|
|
const completedRoundIds = new Set<number>();
|
|
for (let n = 1; n <= completedRounds; n++) {
|
|
for (const id of roundsByNumber.get(n) ?? []) {
|
|
completedRoundIds.add(id);
|
|
}
|
|
}
|
|
|
|
markMatchesCompleted(data, completedRoundIds);
|
|
}
|
|
|
|
function markMatchesCompleted(
|
|
data: TournamentManagerDataSet,
|
|
completedRoundIds: Set<number>,
|
|
) {
|
|
for (const match of data.match) {
|
|
if (!completedRoundIds.has(match.round_id)) continue;
|
|
// skip BYE matches (opponent slot is null entirely)
|
|
if (match.opponent1 === null || match.opponent2 === null) continue;
|
|
|
|
match.opponent1 = { ...match.opponent1, score: 2, result: "win" };
|
|
match.opponent2 = { ...match.opponent2, score: 0, result: "loss" };
|
|
match.status = 4;
|
|
}
|
|
}
|
|
|
|
function generateBracketData(
|
|
format: FormatType,
|
|
teamIds: number[],
|
|
): TournamentManagerDataSet {
|
|
if (format === "swiss") {
|
|
return Engine.create({
|
|
tournamentId: 1,
|
|
name: "Test Bracket",
|
|
type: "swiss",
|
|
seeding: teamIds,
|
|
settings: { groupCount: 1, roundCount: 5 },
|
|
});
|
|
}
|
|
|
|
const settings =
|
|
format === "single_elimination"
|
|
? { thirdPlaceMatch: false }
|
|
: format === "double_elimination"
|
|
? null
|
|
: { teamsPerGroup: 4 };
|
|
|
|
return Engine.create({
|
|
tournamentId: 1,
|
|
name: "Test Bracket",
|
|
type: format,
|
|
seeding: teamIds,
|
|
settings,
|
|
});
|
|
}
|