New tournament seeding page (#2701)

This commit is contained in:
Kalle 2026-01-06 18:23:52 +02:00 committed by GitHub
parent e9149edcde
commit 503101a451
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 1784 additions and 221 deletions

5
.claude/settings.json Normal file
View File

@ -0,0 +1,5 @@
{
"enabledPlugins": {
"code-review@claude-plugins-official": true
}
}

View File

@ -1,38 +0,0 @@
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import type * as React from "react";
export function Draggable({
id,
disabled,
liClassName,
children,
testId,
}: {
id: number;
disabled: boolean;
liClassName: string;
children: React.ReactNode;
testId?: string;
}) {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id, disabled });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<li
className={liClassName}
style={style}
ref={setNodeRef}
data-testid={testId}
{...listeners}
{...attributes}
>
{children}
</li>
);
}

View File

@ -180,6 +180,7 @@ const basicSeeds = (variation?: SeedVariation | null) => [
userProfiles, userProfiles,
userMapModePreferences, userMapModePreferences,
userQWeaponPool, userQWeaponPool,
seedingSkills,
lastMonthsVoting, lastMonthsVoting,
syncPlusTiers, syncPlusTiers,
lastMonthSuggestions, lastMonthSuggestions,
@ -298,6 +299,7 @@ function wipeDB() {
"TournamentBadgeOwner", "TournamentBadgeOwner",
"BadgeManager", "BadgeManager",
"TournamentOrganization", "TournamentOrganization",
"SeedingSkill",
]; ];
for (const table of tablesToDelete) { for (const table of tablesToDelete) {
@ -545,6 +547,38 @@ async function userQWeaponPool() {
} }
} }
function seedingSkills() {
const users = sql.prepare('SELECT id FROM "User" LIMIT 500').all() as {
id: number;
}[];
for (const { id: userId } of users) {
if (faker.number.float() < 0.7) {
const mu = faker.number.float({ min: 22, max: 45 });
const sigma = faker.number.float({ min: 4, max: 8 });
const ordinal = mu - 3 * sigma;
sql
.prepare(
`INSERT INTO "SeedingSkill" ("userId", "type", "mu", "sigma", "ordinal") VALUES (?, 'RANKED', ?, ?, ?)`,
)
.run(userId, mu, sigma, ordinal);
}
if (faker.number.float() < 0.5) {
const mu = faker.number.float({ min: 22, max: 42 });
const sigma = faker.number.float({ min: 4, max: 8 });
const ordinal = mu - 3 * sigma;
sql
.prepare(
`INSERT INTO "SeedingSkill" ("userId", "type", "mu", "sigma", "ordinal") VALUES (?, 'UNRANKED', ?, ?, ?)`,
)
.run(userId, mu, sigma, ordinal);
}
}
}
function fakeUser(usedNames: Set<string>) { function fakeUser(usedNames: Set<string>) {
return () => ({ return () => ({
discordAvatar: null, discordAvatar: null,
@ -1349,13 +1383,15 @@ function calendarEventWithToToolsTeams(
"name", "name",
"createdAt", "createdAt",
"tournamentId", "tournamentId",
"inviteCode" "inviteCode",
"seed"
) values ( ) values (
$id, $id,
$name, $name,
$createdAt, $createdAt,
$tournamentId, $tournamentId,
$inviteCode $inviteCode,
$seed
) )
`, `,
) )
@ -1365,6 +1401,7 @@ function calendarEventWithToToolsTeams(
createdAt: dateToDatabaseTimestamp(new Date()), createdAt: dateToDatabaseTimestamp(new Date()),
tournamentId, tournamentId,
inviteCode: shortNanoid(), inviteCode: shortNanoid(),
seed: id,
}); });
// in PICNIC & PP Chimera is not checked in + in LUTI no check-ins at all // in PICNIC & PP Chimera is not checked in + in LUTI no check-ins at all

View File

@ -493,6 +493,16 @@ export interface Tournament {
parentTournamentId: number | null; parentTournamentId: number | null;
/** Is the tournament finalized meaning all the matches are played and TO has locked it making it read-only */ /** Is the tournament finalized meaning all the matches are played and TO has locked it making it read-only */
isFinalized: Generated<DBBoolean>; isFinalized: Generated<DBBoolean>;
/** Snapshot of teams and rosters when seeds were last saved. Used to detect NEW teams/players. */
seedingSnapshot: JSONColumnTypeNullable<SeedingSnapshot>;
}
export interface SeedingSnapshot {
savedAt: number;
teams: Array<{
teamId: number;
members: Array<{ userId: number; username: string }>;
}>;
} }
export interface PreparedMaps { export interface PreparedMaps {

View File

@ -3,6 +3,7 @@ import {
DndContext, DndContext,
DragOverlay, DragOverlay,
PointerSensor, PointerSensor,
TouchSensor,
useDraggable, useDraggable,
useSensor, useSensor,
useSensors, useSensors,
@ -62,7 +63,15 @@ export default function Planner() {
previewPath: string; previewPath: string;
} | null>(null); } | null>(null);
const sensors = useSensors(useSensor(PointerSensor)); const sensors = useSensors(
useSensor(PointerSensor),
useSensor(TouchSensor, {
activationConstraint: {
delay: 200,
tolerance: 5,
},
}),
);
const handleMount = React.useCallback( const handleMount = React.useCallback(
(mountedEditor: Editor) => { (mountedEditor: Editor) => {

View File

@ -189,6 +189,8 @@ img[src$="?outline=red"] {
background: transparent; background: transparent;
cursor: grab; cursor: grab;
touch-action: none; touch-action: none;
user-select: none;
-webkit-user-select: none;
} }
.plans__draggable-button:hover { .plans__draggable-button:hover {

View File

@ -1,4 +1,6 @@
.item { .item {
cursor: move; cursor: move;
touch-action: none; touch-action: none;
user-select: none;
-webkit-user-select: none;
} }

View File

@ -4,6 +4,7 @@ import {
KeyboardSensor, KeyboardSensor,
PointerSensor, PointerSensor,
pointerWithin, pointerWithin,
TouchSensor,
useSensor, useSensor,
useSensors, useSensors,
} from "@dnd-kit/core"; } from "@dnd-kit/core";
@ -114,6 +115,12 @@ function TierListMakerContent() {
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor), useSensor(PointerSensor),
useSensor(TouchSensor, {
activationConstraint: {
delay: 200,
tolerance: 5,
},
}),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}), }),

View File

@ -18,7 +18,6 @@ import {
import { assertUnreachable } from "~/utils/types"; import { assertUnreachable } from "~/utils/types";
import { idObject } from "~/utils/zod"; import { idObject } from "~/utils/zod";
import type { PreparedMaps } from "../../../db/tables"; import type { PreparedMaps } from "../../../db/tables";
import { updateTeamSeeds } from "../../tournament/queries/updateTeamSeeds.server";
import { getServerTournamentManager } from "../core/brackets-manager/manager.server"; import { getServerTournamentManager } from "../core/brackets-manager/manager.server";
import { roundMapsFromInput } from "../core/mapList.server"; import { roundMapsFromInput } from "../core/mapList.server";
import * as Swiss from "../core/Swiss"; import * as Swiss from "../core/Swiss";
@ -113,19 +112,26 @@ export const action: ActionFunction = async ({ params, request }) => {
bracket, bracket,
}), }),
); );
// ensures autoseeding is disabled
const isAllSeedsPersisted = tournament.ctx.teams.every(
(team) => typeof team.seed === "number",
);
if (!isAllSeedsPersisted) {
updateTeamSeeds({
tournamentId: tournament.ctx.id,
teamIds: tournament.ctx.teams.map((team) => team.id),
});
}
})(); })();
// ensures autoseeding is disabled
const isAllSeedsPersisted = tournament.ctx.teams.every(
(team) => typeof team.seed === "number",
);
if (!isAllSeedsPersisted) {
await TournamentRepository.updateTeamSeeds({
tournamentId: tournament.ctx.id,
teamIds: tournament.ctx.teams.map((team) => team.id),
teamsWithMembers: tournament.ctx.teams.map((team) => ({
teamId: team.id,
members: team.members.map((m) => ({
userId: m.userId,
username: m.username,
})),
})),
});
}
if (!tournament.isTest) { if (!tournament.isTest) {
notify({ notify({
userIds: seeding.flatMap((tournamentTeamId) => userIds: seeding.flatMap((tournamentTeamId) =>

View File

@ -9,6 +9,7 @@ import {
} from "~/features/tournament/tournament-constants"; } from "~/features/tournament/tournament-constants";
import { import {
modesIncluded, modesIncluded,
sortTeamsBySeeding,
tournamentIsRanked, tournamentIsRanked,
} from "~/features/tournament/tournament-utils"; } from "~/features/tournament/tournament-utils";
import type * as Progression from "~/features/tournament-bracket/core/Progression"; import type * as Progression from "~/features/tournament-bracket/core/Progression";
@ -53,30 +54,10 @@ export class Tournament {
simulateBrackets?: boolean; simulateBrackets?: boolean;
}) { }) {
const hasStarted = data.stage.length > 0; const hasStarted = data.stage.length > 0;
const minMembersPerTeam = ctx.settings.minMembersPerTeam ?? 4;
const teamsInSeedOrder = ctx.teams.sort((a, b) => { const teamsInSeedOrder = sortTeamsBySeeding(ctx.teams, minMembersPerTeam);
if (a.startingBracketIdx !== b.startingBracketIdx) {
return (a.startingBracketIdx ?? 0) - (b.startingBracketIdx ?? 0);
}
if (a.seed && b.seed) {
return a.seed - b.seed;
}
if (a.seed && !b.seed) {
return -1;
}
if (!a.seed && b.seed) {
return 1;
}
return this.compareUnseededTeams(
a,
b,
ctx.settings.minMembersPerTeam ?? 4,
);
});
this.simulateBrackets = simulateBrackets; this.simulateBrackets = simulateBrackets;
this.ctx = { this.ctx = {
...ctx, ...ctx,
@ -90,37 +71,6 @@ export class Tournament {
this.initBrackets(data); this.initBrackets(data);
} }
private compareUnseededTeams(
a: TournamentData["ctx"]["teams"][number],
b: TournamentData["ctx"]["teams"][number],
minMembersPerTeam: number,
) {
const aIsFull = a.members.length >= minMembersPerTeam;
const bIsFull = b.members.length >= minMembersPerTeam;
if (aIsFull && !bIsFull) {
return -1;
}
if (!aIsFull && bIsFull) {
return 1;
}
if (a.avgSeedingSkillOrdinal && b.avgSeedingSkillOrdinal) {
return b.avgSeedingSkillOrdinal - a.avgSeedingSkillOrdinal;
}
if (a.avgSeedingSkillOrdinal && !b.avgSeedingSkillOrdinal) {
return -1;
}
if (!a.avgSeedingSkillOrdinal && b.avgSeedingSkillOrdinal) {
return 1;
}
return a.createdAt - b.createdAt;
}
private initBrackets(data: TournamentManagerDataSet) { private initBrackets(data: TournamentManagerDataSet) {
for (const [ for (const [
bracketIdx, bracketIdx,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -317,6 +317,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
}, },
castTwitchAccounts: null, castTwitchAccounts: null,
castedMatchesInfo: null, castedMatchesInfo: null,
seedingSnapshot: null,
mapPickingStyle: "TO", mapPickingStyle: "TO",
rules: rules:
"For the complete and up to date rules see #rules and #announcements in the discord.\n\n**Tournament Proceedings**\nContact your opponent through tournament match page. If issues occur, a TO may direct you to a captains chat in the discord.\n\n**Map Counterpicks**\nThe loser of each match chooses the next map in the round. A team may not choose a map that has already been played in the set.\n\n**Disconnections**\nEach team can replay once per set when a disconnection occurs on their side if both of the following apply: \n- the disconnection occurs before 2:30 on the match timer.\n- the objective counter of the team without the disconnect is above 40.\nIf a disconnection occurs before 30 seconds into the match then a free replay is given. Please avoid replaying when these conditions arent met (i.e. gentlemens replay) so to keep the tournament running on time.\n\n**Other Rules**\n- Use of the private battle quit feature for malicious purposes will result in disqualification.\n- Penalties may be issued to teams that are not in the match lobby within 10 minutes of round start.\n\n**Player Restrictions**\nEach team is allowed up to 6 players. Players of the following group are not allowed to participate without specific exemption from Puma\n- Non-OCE players\n- Oceanink banned players\n\n-- Tournament Organisers reserve the right to make last minute changes to the rules —", "For the complete and up to date rules see #rules and #announcements in the discord.\n\n**Tournament Proceedings**\nContact your opponent through tournament match page. If issues occur, a TO may direct you to a captains chat in the discord.\n\n**Map Counterpicks**\nThe loser of each match chooses the next map in the round. A team may not choose a map that has already been played in the set.\n\n**Disconnections**\nEach team can replay once per set when a disconnection occurs on their side if both of the following apply: \n- the disconnection occurs before 2:30 on the match timer.\n- the objective counter of the team without the disconnect is above 40.\nIf a disconnection occurs before 30 seconds into the match then a free replay is given. Please avoid replaying when these conditions arent met (i.e. gentlemens replay) so to keep the tournament running on time.\n\n**Other Rules**\n- Use of the private battle quit feature for malicious purposes will result in disqualification.\n- Penalties may be issued to teams that are not in the match lobby within 10 minutes of round start.\n\n**Player Restrictions**\nEach team is allowed up to 6 players. Players of the following group are not allowed to participate without specific exemption from Puma\n- Non-OCE players\n- Oceanink banned players\n\n-- Tournament Organisers reserve the right to make last minute changes to the rules —",
@ -410,6 +411,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 1, isOwner: 1,
createdAt: 1734656039, createdAt: 1734656039,
inGameName: "Plussy#1291", inGameName: "Plussy#1291",
plusTier: null,
}, },
{ {
userId: 2899, userId: 2899,
@ -422,6 +424,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734656044, createdAt: 1734656044,
inGameName: "CHIMERA#1263", inGameName: "CHIMERA#1263",
plusTier: null,
}, },
{ {
userId: 6114, userId: 6114,
@ -434,6 +437,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734656047, createdAt: 1734656047,
inGameName: "CountMeOut#1985", inGameName: "CountMeOut#1985",
plusTier: null,
}, },
{ {
userId: 33963, userId: 33963,
@ -446,6 +450,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734664082, createdAt: 1734664082,
inGameName: "BIDOOFGMAX#8251", inGameName: "BIDOOFGMAX#8251",
plusTier: null,
}, },
{ {
userId: 30176, userId: 30176,
@ -458,6 +463,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734674285, createdAt: 1734674285,
inGameName: "Bugha 33#1316", inGameName: "Bugha 33#1316",
plusTier: null,
}, },
], ],
checkIns: [ checkIns: [
@ -499,6 +505,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 1, isOwner: 1,
createdAt: 1734423187, createdAt: 1734423187,
inGameName: "☆ SD-J ☆#2947", inGameName: "☆ SD-J ☆#2947",
plusTier: null,
}, },
{ {
userId: 21689, userId: 21689,
@ -511,6 +518,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734424893, createdAt: 1734424893,
inGameName: "parasyka#2169", inGameName: "parasyka#2169",
plusTier: null,
}, },
{ {
userId: 3147, userId: 3147,
@ -523,6 +531,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734426984, createdAt: 1734426984,
inGameName: "cookie♪#1006", inGameName: "cookie♪#1006",
plusTier: null,
}, },
{ {
userId: 2072, userId: 2072,
@ -535,6 +544,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734426986, createdAt: 1734426986,
inGameName: null, inGameName: null,
plusTier: null,
}, },
], ],
checkIns: [ checkIns: [
@ -571,6 +581,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 1, isOwner: 1,
createdAt: 1734660846, createdAt: 1734660846,
inGameName: "Telethia#6611", inGameName: "Telethia#6611",
plusTier: null,
}, },
{ {
userId: 13370, userId: 13370,
@ -583,6 +594,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734660856, createdAt: 1734660856,
inGameName: "Puma#2209", inGameName: "Puma#2209",
plusTier: null,
}, },
{ {
userId: 45, userId: 45,
@ -595,6 +607,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734660882, createdAt: 1734660882,
inGameName: "ShockWavee#3003", inGameName: "ShockWavee#3003",
plusTier: null,
}, },
{ {
userId: 1843, userId: 1843,
@ -607,6 +620,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734663143, createdAt: 1734663143,
inGameName: null, inGameName: null,
plusTier: null,
}, },
], ],
checkIns: [ checkIns: [
@ -643,6 +657,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 1, isOwner: 1,
createdAt: 1734683349, createdAt: 1734683349,
inGameName: "mitsi#2589", inGameName: "mitsi#2589",
plusTier: null,
}, },
{ {
userId: 13590, userId: 13590,
@ -655,6 +670,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734683352, createdAt: 1734683352,
inGameName: "☆ SD-N ☆#2936", inGameName: "☆ SD-N ☆#2936",
plusTier: null,
}, },
{ {
userId: 10757, userId: 10757,
@ -667,6 +683,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734683356, createdAt: 1734683356,
inGameName: "Wilds ♪#6274", inGameName: "Wilds ♪#6274",
plusTier: null,
}, },
{ {
userId: 33047, userId: 33047,
@ -679,6 +696,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734683966, createdAt: 1734683966,
inGameName: "2F Law#1355", inGameName: "2F Law#1355",
plusTier: null,
}, },
{ {
userId: 41024, userId: 41024,
@ -691,6 +709,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734685180, createdAt: 1734685180,
inGameName: "His Silly#2385", inGameName: "His Silly#2385",
plusTier: null,
}, },
], ],
checkIns: [ checkIns: [
@ -732,6 +751,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 1, isOwner: 1,
createdAt: 1734608907, createdAt: 1734608907,
inGameName: "H! Veems#3106", inGameName: "H! Veems#3106",
plusTier: null,
}, },
{ {
userId: 29665, userId: 29665,
@ -744,6 +764,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734608923, createdAt: 1734608923,
inGameName: "H!PwPwPew#2889", inGameName: "H!PwPwPew#2889",
plusTier: null,
}, },
{ {
userId: 46006, userId: 46006,
@ -756,6 +777,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734608925, createdAt: 1734608925,
inGameName: "H!Ozzysqid#2558", inGameName: "H!Ozzysqid#2558",
plusTier: null,
}, },
{ {
userId: 33483, userId: 33483,
@ -768,6 +790,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734608931, createdAt: 1734608931,
inGameName: "DrkXWolf17#3326", inGameName: "DrkXWolf17#3326",
plusTier: null,
}, },
{ {
userId: 11780, userId: 11780,
@ -780,6 +803,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734659216, createdAt: 1734659216,
inGameName: "Slanted#1646", inGameName: "Slanted#1646",
plusTier: null,
}, },
{ {
userId: 37901, userId: 37901,
@ -792,6 +816,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734684084, createdAt: 1734684084,
inGameName: null, inGameName: null,
plusTier: null,
}, },
], ],
checkIns: [ checkIns: [
@ -828,6 +853,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 1, isOwner: 1,
createdAt: 1734397954, createdAt: 1734397954,
inGameName: "Albonchap#9998", inGameName: "Albonchap#9998",
plusTier: null,
}, },
{ {
userId: 43662, userId: 43662,
@ -840,6 +866,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734397970, createdAt: 1734397970,
inGameName: "FoolLime#1864", inGameName: "FoolLime#1864",
plusTier: null,
}, },
{ {
userId: 33491, userId: 33491,
@ -852,6 +879,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734397973, createdAt: 1734397973,
inGameName: "snowy#2709", inGameName: "snowy#2709",
plusTier: null,
}, },
{ {
userId: 46467, userId: 46467,
@ -864,6 +892,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734398287, createdAt: 1734398287,
inGameName: "Veryneggy#1494", inGameName: "Veryneggy#1494",
plusTier: null,
}, },
{ {
userId: 46813, userId: 46813,
@ -876,6 +905,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734398628, createdAt: 1734398628,
inGameName: "Mikil#2961", inGameName: "Mikil#2961",
plusTier: null,
}, },
], ],
checkIns: [ checkIns: [
@ -917,6 +947,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 1, isOwner: 1,
createdAt: 1734598652, createdAt: 1734598652,
inGameName: "ЯR Dit-toe#3315", inGameName: "ЯR Dit-toe#3315",
plusTier: null,
}, },
{ {
userId: 33611, userId: 33611,
@ -929,6 +960,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734598655, createdAt: 1734598655,
inGameName: "ЯR Samkat #3138", inGameName: "ЯR Samkat #3138",
plusTier: null,
}, },
{ {
userId: 31148, userId: 31148,
@ -941,6 +973,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734598656, createdAt: 1734598656,
inGameName: "ЯR smart!!#1424", inGameName: "ЯR smart!!#1424",
plusTier: null,
}, },
{ {
userId: 33578, userId: 33578,
@ -953,6 +986,7 @@ export const ZONES_WEEKLY_38 = (): TournamentData => ({
isOwner: 0, isOwner: 0,
createdAt: 1734612388, createdAt: 1734612388,
inGameName: "Mat#1561", inGameName: "Mat#1561",
plusTier: null,
}, },
], ],
checkIns: [ checkIns: [

View File

@ -1451,6 +1451,7 @@ export const PADDLING_POOL_257 = () =>
], ],
lockedMatches: [], lockedMatches: [],
}, },
seedingSnapshot: null,
mapPickingStyle: "AUTO_ALL", mapPickingStyle: "AUTO_ALL",
name: "Paddling Pool 257", name: "Paddling Pool 257",
description: description:
@ -7429,6 +7430,7 @@ export const PADDLING_POOL_255 = () =>
], ],
lockedMatches: [], lockedMatches: [],
}, },
seedingSnapshot: null,
mapPickingStyle: "AUTO_ALL", mapPickingStyle: "AUTO_ALL",
name: "Paddling Pool 255", name: "Paddling Pool 255",
description: null, description: null,
@ -13715,6 +13717,7 @@ export const IN_THE_ZONE_32 = ({
discordUrl: null, discordUrl: null,
castTwitchAccounts: ["dappleproductions", "kyochandxd"], castTwitchAccounts: ["dappleproductions", "kyochandxd"],
castedMatchesInfo: null, castedMatchesInfo: null,
seedingSnapshot: null,
mapPickingStyle: "AUTO_SZ", mapPickingStyle: "AUTO_SZ",
name: "In The Zone 32", name: "In The Zone 32",
description: "Part of sendou.ink ranked season 2", description: "Part of sendou.ink ranked season 2",

View File

@ -89,6 +89,7 @@ export const testTournament = ({
], ],
}, },
castedMatchesInfo: null, castedMatchesInfo: null,
seedingSnapshot: null,
teams: nTeams(participant.length, Math.min(...participant)), teams: nTeams(participant.length, Math.min(...participant)),
author: { author: {
chatNameColor: null, chatNameColor: null,

View File

@ -55,6 +55,7 @@ export async function findById(id: number) {
"CalendarEvent.description", "CalendarEvent.description",
"CalendarEventDate.startTime", "CalendarEventDate.startTime",
"Tournament.isFinalized", "Tournament.isFinalized",
"Tournament.seedingSnapshot",
jsonObjectFrom( jsonObjectFrom(
eb eb
.selectFrom("TournamentOrganization") .selectFrom("TournamentOrganization")
@ -175,6 +176,7 @@ export async function findById(id: number) {
isSetAsRanked ? "RANKED" : "UNRANKED", isSetAsRanked ? "RANKED" : "UNRANKED",
), ),
) )
.leftJoin("PlusTier", "PlusTier.userId", "User.id")
.select([ .select([
"User.id as userId", "User.id as userId",
"User.username", "User.username",
@ -184,6 +186,7 @@ export async function findById(id: number) {
"User.country", "User.country",
"User.twitch", "User.twitch",
"SeedingSkill.ordinal", "SeedingSkill.ordinal",
"PlusTier.tier as plusTier",
"TournamentTeamMember.isOwner", "TournamentTeamMember.isOwner",
"TournamentTeamMember.createdAt", "TournamentTeamMember.createdAt",
sql<string | null> /*sql*/`coalesce( sql<string | null> /*sql*/`coalesce(
@ -1175,3 +1178,42 @@ export async function searchByName({
return sqlQuery.execute(); return sqlQuery.execute();
} }
export function updateTeamSeeds({
tournamentId,
teamIds,
teamsWithMembers,
}: {
tournamentId: number;
teamIds: number[];
teamsWithMembers: Array<{
teamId: number;
members: Array<{ userId: number; username: string }>;
}>;
}) {
return db.transaction().execute(async (trx) => {
await trx
.updateTable("TournamentTeam")
.set({ seed: null })
.where("tournamentId", "=", tournamentId)
.execute();
for (const [i, teamId] of teamIds.entries()) {
await trx
.updateTable("TournamentTeam")
.set({ seed: i + 1 })
.where("id", "=", teamId)
.execute();
}
const snapshot = JSON.stringify({
savedAt: databaseTimestampNow(),
teams: teamsWithMembers,
});
await trx
.updateTable("Tournament")
.set({ seedingSnapshot: snapshot })
.where("id", "=", tournamentId)
.execute();
});
}

View File

@ -11,7 +11,7 @@ import {
successToast, successToast,
} from "~/utils/remix.server"; } from "~/utils/remix.server";
import { idObject } from "~/utils/zod"; import { idObject } from "~/utils/zod";
import { updateTeamSeeds } from "../queries/updateTeamSeeds.server"; import * as TournamentRepository from "../TournamentRepository.server";
import * as TournamentTeamRepository from "../TournamentTeamRepository.server"; import * as TournamentTeamRepository from "../TournamentTeamRepository.server";
import { seedsActionSchema } from "../tournament-schemas.server"; import { seedsActionSchema } from "../tournament-schemas.server";
@ -32,7 +32,21 @@ export const action: ActionFunction = async ({ request, params }) => {
switch (data._action) { switch (data._action) {
case "UPDATE_SEEDS": { case "UPDATE_SEEDS": {
updateTeamSeeds({ tournamentId, teamIds: data.seeds }); const teamsWithMembers = tournament.ctx.teams
.filter((t) => data.seeds.includes(t.id))
.map((team) => ({
teamId: team.id,
members: team.members.map((m) => ({
userId: m.userId,
username: m.username,
})),
}));
await TournamentRepository.updateTeamSeeds({
tournamentId,
teamIds: data.seeds,
teamsWithMembers,
});
clearTournamentDataCache(tournamentId); clearTournamentDataCache(tournamentId);
return successToast("Seeds saved successfully"); return successToast("Seeds saved successfully");
} }

View File

@ -1,26 +0,0 @@
import { sql } from "~/db/sql";
const resetSeeds = sql.prepare(/*sql*/ `
update "TournamentTeam"
set "seed" = null
where "tournamentId" = @tournamentId
`);
const updateSeedStm = sql.prepare(/*sql*/ `
update "TournamentTeam"
set "seed" = @seed
where "id" = @teamId
`);
export const updateTeamSeeds = sql.transaction(
({ tournamentId, teamIds }: { tournamentId: number; teamIds: number[] }) => {
resetSeeds.run({ tournamentId });
for (const [i, teamId] of teamIds.entries()) {
updateSeedStm.run({
teamId,
seed: i + 1,
});
}
},
);

View File

@ -0,0 +1,216 @@
.teamsList {
display: flex;
flex-direction: column;
gap: var(--s-2);
padding-left: 0;
}
.headerRow {
display: none;
width: 100%;
align-items: center;
padding: var(--s-1-5) var(--s-3);
column-gap: var(--s-2);
font-size: var(--fonts-xs);
font-weight: var(--bold);
grid-template-columns: 2.5rem 2.25rem 2rem 1fr 3rem 1fr;
}
.teamCard {
display: grid;
width: 100%;
align-items: center;
padding: var(--s-2) var(--s-3);
border-radius: var(--rounded);
border: 1px solid var(--border);
background-color: var(--bg-lighter);
column-gap: var(--s-2);
row-gap: var(--s-2);
font-size: var(--fonts-xs);
grid-template-columns: 2rem 2.25rem 2rem 1fr 3rem;
grid-template-areas:
"handle seed logo name sp"
"players players players players players";
list-style: none;
}
.handleArea {
grid-area: handle;
}
.seedArea {
grid-area: seed;
}
.logoArea {
grid-area: logo;
}
.nameArea {
grid-area: name;
}
.spArea {
grid-area: sp;
}
.playersArea {
grid-area: players;
}
@media screen and (min-width: 640px) {
.headerRow {
display: grid;
}
.teamCard {
grid-template-columns: 2.5rem 2.25rem 2rem 1fr 3rem 1fr;
grid-template-areas: "handle seed logo name sp players";
row-gap: 0;
}
}
.teamCardDragging {
opacity: 0.5;
}
.dragHandle {
display: flex;
align-items: center;
justify-content: center;
cursor: grab;
background: none;
border: none;
font-size: var(--fonts-lg);
color: var(--text-lighter);
padding: var(--s-1);
touch-action: none;
user-select: none;
-webkit-user-select: none;
}
.dragHandle:hover {
color: var(--text);
}
.dragHandle:active:not(:disabled) {
cursor: grabbing;
}
.dragHandle:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.seedInput {
--input-width: 3rem;
text-align: center;
padding: var(--s-0-5);
border-radius: var(--rounded-sm);
border: 1px solid var(--border);
background-color: var(--bg-input);
font-size: var(--fonts-xs);
}
.seedInput:focus {
outline: 2px solid var(--theme);
outline-offset: 1px;
}
.teamNameContainer {
position: relative;
display: flex;
align-items: center;
gap: var(--s-1);
}
.teamName {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.newBadge {
flex-shrink: 0;
background-color: var(--theme-secondary);
color: var(--button-text);
font-size: var(--fonts-xxxxs);
font-weight: var(--bold);
padding: 1px 4px;
border-radius: var(--rounded-xs);
text-transform: uppercase;
}
.playersList {
display: flex;
flex-wrap: wrap;
gap: var(--s-1);
}
.playerBadge {
display: flex;
align-items: center;
gap: var(--s-1);
background-color: var(--bg-darker);
border-radius: var(--rounded);
padding: var(--s-0-5) var(--s-2);
font-weight: var(--semi-bold);
}
.playerNew {
position: relative;
}
.playerNewBadge {
background-color: var(--theme-info);
color: white;
font-size: var(--fonts-xxxxs);
font-weight: var(--bold);
padding: 1px 3px;
border-radius: var(--rounded-xs);
text-transform: uppercase;
margin-left: var(--s-1);
}
.plusTier {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 2px;
font-size: var(--fonts-xxxs);
color: var(--text-lighter);
}
.playerRemoved {
text-decoration: line-through;
color: var(--text-lighter);
opacity: 0.6;
}
.spValue {
font-variant-numeric: tabular-nums;
}
.outOfOrder {
color: var(--theme-warning);
}
.form {
width: 100%;
display: flex;
align-items: center;
}
.orderButton {
margin-block-start: var(--s-2);
margin-inline-end: auto;
}
.overlay {
background-color: var(--bg-lighter-solid);
border: 1px solid var(--border);
border-radius: var(--rounded);
padding: var(--s-2) var(--s-3);
box-shadow: var(--shadow-md);
opacity: 0.8;
}

View File

@ -4,6 +4,7 @@ import {
DragOverlay, DragOverlay,
KeyboardSensor, KeyboardSensor,
PointerSensor, PointerSensor,
TouchSensor,
useSensor, useSensor,
useSensors, useSensors,
} from "@dnd-kit/core"; } from "@dnd-kit/core";
@ -11,27 +12,31 @@ import {
arrayMove, arrayMove,
SortableContext, SortableContext,
sortableKeyboardCoordinates, sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy, verticalListSortingStrategy,
} from "@dnd-kit/sortable"; } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import clsx from "clsx"; import clsx from "clsx";
import * as React from "react"; import * as React from "react";
import { Link, useFetcher, useNavigation } from "react-router"; import { Link, useFetcher, useNavigation } from "react-router";
import { Alert } from "~/components/Alert"; import { Alert } from "~/components/Alert";
import { Avatar } from "~/components/Avatar";
import { Catcher } from "~/components/Catcher"; import { Catcher } from "~/components/Catcher";
import { Draggable } from "~/components/Draggable";
import { SendouButton } from "~/components/elements/Button"; import { SendouButton } from "~/components/elements/Button";
import { SendouDialog } from "~/components/elements/Dialog"; import { SendouDialog } from "~/components/elements/Dialog";
import { Image } from "~/components/Image";
import { InfoPopover } from "~/components/InfoPopover";
import { SubmitButton } from "~/components/SubmitButton"; import { SubmitButton } from "~/components/SubmitButton";
import { Table } from "~/components/Table"; import { Table } from "~/components/Table";
import type { SeedingSnapshot } from "~/db/tables";
import type { TournamentDataTeam } from "~/features/tournament-bracket/core/Tournament.server"; import type { TournamentDataTeam } from "~/features/tournament-bracket/core/Tournament.server";
import invariant from "~/utils/invariant"; import invariant from "~/utils/invariant";
import { userResultsPage } from "~/utils/urls"; import { navIconUrl, userResultsPage } from "~/utils/urls";
import { Avatar } from "../../../components/Avatar";
import { InfoPopover } from "../../../components/InfoPopover";
import { ordinalToRoundedSp } from "../../mmr/mmr-utils"; import { ordinalToRoundedSp } from "../../mmr/mmr-utils";
import { action } from "../actions/to.$id.seeds.server"; import { action } from "../actions/to.$id.seeds.server";
import { loader } from "../loaders/to.$id.seeds.server"; import { loader } from "../loaders/to.$id.seeds.server";
import { useTournament } from "./to.$id"; import { useTournament } from "./to.$id";
import styles from "./to.$id.seeds.module.css";
export { loader, action }; export { loader, action };
export default function TournamentSeedsPage() { export default function TournamentSeedsPage() {
@ -49,11 +54,28 @@ export default function TournamentSeedsPage() {
distance: 8, distance: 8,
}, },
}), }),
useSensor(TouchSensor, {
activationConstraint: {
delay: 200,
tolerance: 5,
},
}),
useSensor(KeyboardSensor, { useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates, coordinateGetter: sortableKeyboardCoordinates,
}), }),
); );
const seedingSnapshot = tournament.ctx.seedingSnapshot;
const newTeamIds = computeNewTeamIds(tournament.ctx.teams, seedingSnapshot);
const newPlayersByTeam = computeNewPlayers(
tournament.ctx.teams,
seedingSnapshot,
);
const removedPlayersByTeam = computeRemovedPlayers(
tournament.ctx.teams,
seedingSnapshot,
);
const teamsSorted = [...tournament.ctx.teams].sort( const teamsSorted = [...tournament.ctx.teams].sort(
(a, b) => teamOrder.indexOf(a.id) - teamOrder.indexOf(b.id), (a, b) => teamOrder.indexOf(a.id) - teamOrder.indexOf(b.id),
); );
@ -78,6 +100,37 @@ export default function TournamentSeedsPage() {
(team) => !team.seed, (team) => !team.seed,
); );
const handleSeedChange = (teamId: number, newSeed: number) => {
if (newSeed < 1) return;
const clampedSeed = Math.min(newSeed, teamOrder.length);
const currentIndex = teamOrder.indexOf(teamId);
const targetIndex = clampedSeed - 1;
if (currentIndex === targetIndex) return;
const newOrder = [...teamOrder];
newOrder.splice(currentIndex, 1);
newOrder.splice(targetIndex, 0, teamId);
setTeamOrder(newOrder);
};
const sortAllBySp = () => {
const sortedTeams = [...tournament.ctx.teams].sort((a, b) => {
if (
a.avgSeedingSkillOrdinal !== null &&
b.avgSeedingSkillOrdinal !== null
) {
return b.avgSeedingSkillOrdinal - a.avgSeedingSkillOrdinal;
}
if (a.avgSeedingSkillOrdinal !== null) return -1;
if (b.avgSeedingSkillOrdinal !== null) return 1;
return 0;
});
setTeamOrder(sortedTeams.map((t) => t.id));
};
return ( return (
<div className="stack lg"> <div className="stack lg">
<SeedAlert teamOrder={teamOrder} /> <SeedAlert teamOrder={teamOrder} />
@ -90,23 +143,13 @@ export default function TournamentSeedsPage() {
</div> </div>
) : ( ) : (
<SendouButton <SendouButton
className="tournament__seeds__order-button" className={styles.orderButton}
variant="minimal" variant="minimal"
size="small" size="small"
type="button" type="button"
onPress={() => { onPress={sortAllBySp}
setTeamOrder(
structuredClone(tournament.ctx.teams)
.sort(
(a, b) =>
(b.avgSeedingSkillOrdinal ?? Number.NEGATIVE_INFINITY) -
(a.avgSeedingSkillOrdinal ?? Number.NEGATIVE_INFINITY),
)
.map((t) => t.id),
);
}}
> >
Sort automatically Sort all by SP
</SendouButton> </SendouButton>
)} )}
</div> </div>
@ -117,12 +160,13 @@ export default function TournamentSeedsPage() {
.join()} .join()}
/> />
) : null} ) : null}
<ul> <ul className={styles.teamsList}>
<li className="tournament__seeds__teams-list-row"> <li className={styles.headerRow}>
<div className="tournament__seeds__teams-container__header" /> <div />
<div className="tournament__seeds__teams-container__header" /> <div>Seed</div>
<div className="tournament__seeds__teams-container__header">Name</div> <div />
<div className="tournament__seeds__teams-container__header stack horizontal xxs"> <div>Name</div>
<div className="stack horizontal xxs">
SP SP
<InfoPopover tiny> <InfoPopover tiny>
Seeding point is a value that tracks players' head-to-head Seeding point is a value that tracks players' head-to-head
@ -130,9 +174,7 @@ export default function TournamentSeedsPage() {
different points. different points.
</InfoPopover> </InfoPopover>
</div> </div>
<div className="tournament__seeds__teams-container__header"> <div>Players</div>
Players
</div>
</li> </li>
<DndContext <DndContext
id="team-seed-sorter" id="team-seed-sorter"
@ -165,19 +207,12 @@ export default function TournamentSeedsPage() {
strategy={verticalListSortingStrategy} strategy={verticalListSortingStrategy}
> >
{teamsSorted.map((team, i) => ( {teamsSorted.map((team, i) => (
<Draggable <SeedingDraggable
key={team.id} key={team.id}
id={team.id} id={team.id}
testId={`seed-team-${team.id}`} testId={`seed-team-${team.id}`}
disabled={navigation.state !== "idle"} disabled={navigation.state !== "idle"}
liClassName={clsx( isActive={activeTeam?.id === team.id}
"tournament__seeds__teams-list-row",
"sortable",
{
disabled: navigation.state !== "idle",
invisible: activeTeam?.id === team.id,
},
)}
> >
<RowContents <RowContents
team={team} team={team}
@ -188,25 +223,36 @@ export default function TournamentSeedsPage() {
: null, : null,
outOfOrder: isOutOfOrder(team, teamsSorted[i - 1]), outOfOrder: isOutOfOrder(team, teamsSorted[i - 1]),
}} }}
isNewTeam={newTeamIds.has(team.id)}
newPlayerIds={newPlayersByTeam.get(team.id)}
removedPlayers={removedPlayersByTeam.get(team.id)}
onSeedChange={(newSeed) => handleSeedChange(team.id, newSeed)}
/> />
</Draggable> </SeedingDraggable>
))} ))}
</SortableContext> </SortableContext>
<DragOverlay> <DragOverlay>
{activeTeam && ( {activeTeam ? (
<li className="tournament__seeds__teams-list-row active"> <li className={clsx(styles.teamCard, styles.overlay)}>
<div className={styles.handleArea}>
<button className={styles.dragHandle} type="button">
</button>
</div>
<RowContents <RowContents
team={activeTeam} team={activeTeam}
seed={teamOrder.indexOf(activeTeam.id) + 1}
teamSeedingSkill={{ teamSeedingSkill={{
sp: activeTeam.avgSeedingSkillOrdinal sp: activeTeam.avgSeedingSkillOrdinal
? ordinalToRoundedSp(activeTeam.avgSeedingSkillOrdinal) ? ordinalToRoundedSp(activeTeam.avgSeedingSkillOrdinal)
: null, : null,
outOfOrder: false, outOfOrder: false,
}} }}
onSeedChange={() => {}}
/> />
</li> </li>
)} ) : null}
</DragOverlay> </DragOverlay>
</DndContext> </DndContext>
</ul> </ul>
@ -214,6 +260,60 @@ export default function TournamentSeedsPage() {
); );
} }
function SeedingDraggable({
id,
disabled,
children,
testId,
isActive,
}: {
id: number;
disabled: boolean;
children: React.ReactNode;
testId?: string;
isActive: boolean;
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id, disabled });
const style = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<li
className={clsx(styles.teamCard, {
[styles.teamCardDragging]: isDragging,
invisible: isActive,
})}
style={style}
ref={setNodeRef}
data-testid={testId}
{...attributes}
>
<div className={styles.handleArea}>
<button
className={styles.dragHandle}
{...listeners}
disabled={disabled}
type="button"
data-testid={`${testId}-handle`}
>
</button>
</div>
{children}
</li>
);
}
function StartingBracketDialog() { function StartingBracketDialog() {
const fetcher = useFetcher(); const fetcher = useFetcher();
const tournament = useTournament(); const tournament = useTournament();
@ -338,7 +438,7 @@ function SeedAlert({ teamOrder }: { teamOrder: number[] }) {
const teamOrderChanged = teamOrder.some((id, i) => id !== teamOrderInDb[i]); const teamOrderChanged = teamOrder.some((id, i) => id !== teamOrderInDb[i]);
return ( return (
<fetcher.Form method="post" className="tournament__seeds__form"> <fetcher.Form method="post" className={styles.form}>
<input type="hidden" name="tournamentId" value={tournament.ctx.id} /> <input type="hidden" name="tournamentId" value={tournament.ctx.id} />
<input type="hidden" name="seeds" value={JSON.stringify(teamOrder)} /> <input type="hidden" name="seeds" value={JSON.stringify(teamOrder)} />
<input type="hidden" name="_action" value="UPDATE_SEEDS" /> <input type="hidden" name="_action" value="UPDATE_SEEDS" />
@ -366,6 +466,10 @@ function RowContents({
team, team,
seed, seed,
teamSeedingSkill, teamSeedingSkill,
isNewTeam,
newPlayerIds,
removedPlayers,
onSeedChange,
}: { }: {
team: TournamentDataTeam; team: TournamentDataTeam;
seed?: number; seed?: number;
@ -373,37 +477,176 @@ function RowContents({
sp: number | null; sp: number | null;
outOfOrder: boolean; outOfOrder: boolean;
}; };
isNewTeam?: boolean;
newPlayerIds?: Set<number>;
removedPlayers?: Array<{ userId: number; username: string }>;
onSeedChange?: (newSeed: number) => void;
}) { }) {
const tournament = useTournament(); const tournament = useTournament();
const [inputValue, setInputValue] = React.useState(String(seed ?? ""));
React.useEffect(() => {
setInputValue(String(seed ?? ""));
}, [seed]);
const handleInputBlur = () => {
const newSeed = Number.parseInt(inputValue, 10);
if (!Number.isNaN(newSeed) && onSeedChange) {
onSeedChange(newSeed);
} else {
setInputValue(String(seed ?? ""));
}
};
const logoUrl = tournament.tournamentTeamLogoSrc(team); const logoUrl = tournament.tournamentTeamLogoSrc(team);
return ( return (
<> <>
<div>{seed}</div> <div className={styles.seedArea}>
<div>{logoUrl ? <Avatar url={logoUrl} size="xxs" /> : null}</div> {seed !== undefined && onSeedChange ? (
<div className="tournament__seeds__team-name"> <input
{team.checkIns.length > 0 ? "✅ " : "❌ "} {team.name} type="text"
className={styles.seedInput}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onBlur={handleInputBlur}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.currentTarget.blur();
}
}}
/>
) : (
<div>{seed}</div>
)}
</div> </div>
<div className={clsx({ "text-warning": teamSeedingSkill.outOfOrder })}> <div className={styles.logoArea}>
{teamSeedingSkill.sp} {logoUrl ? <Avatar url={logoUrl} size="xxs" /> : null}
</div> </div>
<div className="stack horizontal sm"> <div className={styles.nameArea}>
{team.members.map((member) => { <div className={styles.teamNameContainer}>
return ( <span className={styles.teamName}>
<div key={member.userId} className="tournament__seeds__team-member"> {team.checkIns.length > 0 ? "✅ " : "❌ "} {team.name}
<Link </span>
to={userResultsPage(member, true)} {isNewTeam ? <span className={styles.newBadge}>NEW</span> : null}
className="tournament__seeds__team-member__name" </div>
> </div>
{member.username} <div className={styles.spArea}>
</Link> <div
className={clsx(styles.spValue, {
[styles.outOfOrder]: teamSeedingSkill.outOfOrder,
})}
>
{teamSeedingSkill.sp}
</div>
</div>
<div className={styles.playersArea}>
<div className={styles.playersList}>
{removedPlayers?.map((player) => (
<div
key={`removed-${player.userId}`}
className={clsx(styles.playerBadge, styles.playerRemoved)}
>
{player.username}
</div> </div>
); ))}
})} {team.members.map((member) => {
const isNew = newPlayerIds?.has(member.userId);
return (
<div
key={member.userId}
className={clsx(styles.playerBadge, {
[styles.playerNew]: isNew,
})}
>
<Link to={userResultsPage(member, true)}>
{member.username}
</Link>
{member.plusTier ? (
<span className={styles.plusTier}>
<Image
path={navIconUrl("plus")}
width={14}
height={14}
alt=""
/>
{member.plusTier}
</span>
) : null}
{isNew ? (
<span className={styles.playerNewBadge}>NEW</span>
) : null}
</div>
);
})}
</div>
</div> </div>
</> </>
); );
} }
function computeNewTeamIds(
teams: TournamentDataTeam[],
snapshot: SeedingSnapshot | null,
): Set<number> {
if (!snapshot) return new Set();
const savedTeamIds = new Set(snapshot.teams.map((t) => t.teamId));
return new Set(teams.filter((t) => !savedTeamIds.has(t.id)).map((t) => t.id));
}
function computeNewPlayers(
teams: TournamentDataTeam[],
snapshot: SeedingSnapshot | null,
): Map<number, Set<number>> {
const result = new Map<number, Set<number>>();
if (!snapshot) return result;
const savedTeamMap = new Map(
snapshot.teams.map((t) => [
t.teamId,
new Set(t.members.map((m) => m.userId)),
]),
);
for (const team of teams) {
const savedMembers = savedTeamMap.get(team.id);
if (!savedMembers) continue;
const newPlayerIds = new Set(
team.members
.filter((m) => !savedMembers.has(m.userId))
.map((m) => m.userId),
);
if (newPlayerIds.size > 0) {
result.set(team.id, newPlayerIds);
}
}
return result;
}
function computeRemovedPlayers(
teams: TournamentDataTeam[],
snapshot: SeedingSnapshot | null,
): Map<number, Array<{ userId: number; username: string }>> {
const result = new Map<number, Array<{ userId: number; username: string }>>();
if (!snapshot) return result;
const currentTeamMap = new Map(
teams.map((t) => [t.id, new Set(t.members.map((m) => m.userId))]),
);
for (const savedTeam of snapshot.teams) {
const currentMembers = currentTeamMap.get(savedTeam.teamId);
if (!currentMembers) continue;
const removedMembers = savedTeam.members.filter(
(member) => !currentMembers.has(member.userId),
);
if (removedMembers.length > 0) {
result.set(savedTeam.teamId, removedMembers);
}
}
return result;
}
export const ErrorBoundary = Catcher; export const ErrorBoundary = Catcher;

View File

@ -1,6 +1,330 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import type { ParsedBracket } from "../tournament-bracket/core/Progression"; import type { ParsedBracket } from "../tournament-bracket/core/Progression";
import { getBracketProgressionLabel } from "./tournament-utils"; import {
compareTeamsForOrdering,
findTeamInsertPosition,
getBracketProgressionLabel,
sortTeamsBySeeding,
type TeamForOrdering,
} from "./tournament-utils";
const createTeam = (
id: number,
options: {
seed?: number | null;
members?: number;
avgSeedingSkillOrdinal?: number | null;
createdAt?: number;
startingBracketIdx?: number | null;
} = {},
): TeamForOrdering => ({
id,
seed: options.seed ?? null,
members: { length: options.members ?? 4 },
avgSeedingSkillOrdinal: options.avgSeedingSkillOrdinal ?? 100,
createdAt: options.createdAt ?? id,
startingBracketIdx: options.startingBracketIdx ?? null,
});
const MIN_MEMBERS = 4;
describe("compareTeamsForOrdering", () => {
describe("full teams priority", () => {
it("places full teams before not-full teams", () => {
const fullTeam = createTeam(1, { members: 4 });
const notFullTeam = createTeam(2, { members: 3 });
const result = compareTeamsForOrdering(
fullTeam,
notFullTeam,
MIN_MEMBERS,
);
expect(result).toBeLessThan(0);
});
it("places not-full teams after full teams", () => {
const notFullTeam = createTeam(1, { members: 3 });
const fullTeam = createTeam(2, { members: 4 });
const result = compareTeamsForOrdering(
notFullTeam,
fullTeam,
MIN_MEMBERS,
);
expect(result).toBeGreaterThan(0);
});
});
describe("seed priority", () => {
it("orders by seed when both have seeds", () => {
const team1 = createTeam(1, { seed: 1 });
const team2 = createTeam(2, { seed: 2 });
const result = compareTeamsForOrdering(team1, team2, MIN_MEMBERS);
expect(result).toBeLessThan(0);
});
it("places seeded team before unseeded team when unseeded has no skill", () => {
const seededTeam = createTeam(1, { seed: 5 });
const unseededTeam = createTeam(2);
const result = compareTeamsForOrdering(
seededTeam,
unseededTeam,
MIN_MEMBERS,
);
expect(result).toBeLessThan(0);
});
it("compares by skill ordinal when both full teams have skill but only one has seed", () => {
const seededLowSkill = createTeam(1, {
seed: 5,
avgSeedingSkillOrdinal: 100,
});
const unseededHighSkill = createTeam(2, { avgSeedingSkillOrdinal: 300 });
const result = compareTeamsForOrdering(
seededLowSkill,
unseededHighSkill,
MIN_MEMBERS,
);
expect(result).toBeGreaterThan(0);
});
it("places seeded team first when only seeded team has skill ordinal", () => {
const seededWithSkill = createTeam(1, {
seed: 5,
avgSeedingSkillOrdinal: 100,
});
const unseededNoSkill = createTeam(2);
const result = compareTeamsForOrdering(
seededWithSkill,
unseededNoSkill,
MIN_MEMBERS,
);
expect(result).toBeLessThan(0);
});
it("places seeded team first when not-full team has higher skill", () => {
const seededFull = createTeam(1, {
seed: 5,
avgSeedingSkillOrdinal: 100,
});
const unseededNotFull = createTeam(2, {
members: 3,
avgSeedingSkillOrdinal: 500,
});
const result = compareTeamsForOrdering(
seededFull,
unseededNotFull,
MIN_MEMBERS,
);
expect(result).toBeLessThan(0);
});
});
describe("skill ordinal priority", () => {
it("orders by skill ordinal when no seeds (higher skill first)", () => {
const highSkill = createTeam(1, { avgSeedingSkillOrdinal: 300 });
const lowSkill = createTeam(2, { avgSeedingSkillOrdinal: 100 });
const result = compareTeamsForOrdering(highSkill, lowSkill, MIN_MEMBERS);
expect(result).toBeLessThan(0);
});
it("places team with skill before team without skill", () => {
const withSkill = createTeam(1, { avgSeedingSkillOrdinal: 100 });
const withoutSkill = createTeam(2);
const result = compareTeamsForOrdering(
withSkill,
withoutSkill,
MIN_MEMBERS,
);
expect(result).toBeLessThan(0);
});
});
describe("createdAt tiebreaker", () => {
it("orders by createdAt when all else is equal", () => {
const olderTeam = createTeam(1, { createdAt: 100 });
const newerTeam = createTeam(2, { createdAt: 200 });
const result = compareTeamsForOrdering(olderTeam, newerTeam, MIN_MEMBERS);
expect(result).toBeLessThan(0);
});
});
});
describe("sortTeamsBySeeding", () => {
it("sorts teams correctly with mixed properties", () => {
const teams = [
createTeam(1, { members: 3, avgSeedingSkillOrdinal: 500 }),
createTeam(2, { seed: 2 }),
createTeam(3, { avgSeedingSkillOrdinal: 300 }),
createTeam(4, { seed: 1 }),
createTeam(5, { avgSeedingSkillOrdinal: 400 }),
createTeam(6, { members: 3 }),
];
const sorted = sortTeamsBySeeding(teams, MIN_MEMBERS);
expect(sorted.map((t) => t.id)).toEqual([5, 3, 4, 2, 1, 6]);
});
it("does not mutate original array", () => {
const teams = [
createTeam(2, { avgSeedingSkillOrdinal: 100 }),
createTeam(1, { avgSeedingSkillOrdinal: 200 }),
];
sortTeamsBySeeding(teams, MIN_MEMBERS);
expect(teams[0].id).toBe(2);
});
});
describe("findTeamInsertPosition", () => {
it("inserts at beginning when new team should be first", () => {
const team1 = createTeam(1, { avgSeedingSkillOrdinal: 100 });
const team2 = createTeam(2, { avgSeedingSkillOrdinal: 200 });
const teamMap = new Map([
[1, team1],
[2, team2],
]);
const existingOrder = [2, 1];
const newTeam = createTeam(3, { avgSeedingSkillOrdinal: 300 });
const position = findTeamInsertPosition(
existingOrder,
newTeam,
teamMap,
MIN_MEMBERS,
);
expect(position).toBe(0);
});
it("inserts at end when new team should be last", () => {
const team1 = createTeam(1, { avgSeedingSkillOrdinal: 100 });
const team2 = createTeam(2, { avgSeedingSkillOrdinal: 200 });
const teamMap = new Map([
[1, team1],
[2, team2],
]);
const existingOrder = [2, 1];
const newTeam = createTeam(3, { avgSeedingSkillOrdinal: 50 });
const position = findTeamInsertPosition(
existingOrder,
newTeam,
teamMap,
MIN_MEMBERS,
);
expect(position).toBe(2);
});
it("inserts in middle based on comparison", () => {
const team1 = createTeam(1, { avgSeedingSkillOrdinal: 100 });
const team2 = createTeam(2, { avgSeedingSkillOrdinal: 300 });
const team3 = createTeam(3, { avgSeedingSkillOrdinal: 200 });
const teamMap = new Map([
[1, team1],
[2, team2],
[3, team3],
]);
const existingOrder = [2, 3, 1];
const newTeam = createTeam(4, { avgSeedingSkillOrdinal: 150 });
const position = findTeamInsertPosition(
existingOrder,
newTeam,
teamMap,
MIN_MEMBERS,
);
expect(position).toBe(2);
});
it("handles empty existing order", () => {
const teamMap = new Map<number, TeamForOrdering>();
const existingOrder: number[] = [];
const newTeam = createTeam(1, { avgSeedingSkillOrdinal: 100 });
const position = findTeamInsertPosition(
existingOrder,
newTeam,
teamMap,
MIN_MEMBERS,
);
expect(position).toBe(0);
});
it("skips missing teams in map", () => {
const team1 = createTeam(1, { avgSeedingSkillOrdinal: 100 });
const teamMap = new Map([[1, team1]]);
const existingOrder = [2, 1];
const newTeam = createTeam(3, { avgSeedingSkillOrdinal: 150 });
const position = findTeamInsertPosition(
existingOrder,
newTeam,
teamMap,
MIN_MEMBERS,
);
expect(position).toBe(1);
});
});
describe("sortTeamsBySeeding with startingBracketIdx", () => {
it("orders by startingBracketIdx first", () => {
const teams = [
createTeam(1, {
startingBracketIdx: 1,
avgSeedingSkillOrdinal: 500,
}),
createTeam(2, {
startingBracketIdx: 0,
avgSeedingSkillOrdinal: 100,
}),
createTeam(3, {
startingBracketIdx: 0,
avgSeedingSkillOrdinal: 200,
}),
];
const sorted = sortTeamsBySeeding(teams, MIN_MEMBERS);
expect(sorted.map((t) => t.id)).toEqual([3, 2, 1]);
});
it("uses seeds within same bracket", () => {
const teams = [
createTeam(1, { seed: 2 }),
createTeam(2, { seed: 1 }),
createTeam(3, { avgSeedingSkillOrdinal: 500 }),
];
const sorted = sortTeamsBySeeding(teams, MIN_MEMBERS);
expect(sorted.map((t) => t.id)).toEqual([3, 2, 1]);
});
});
const createBracket = (name: string): ParsedBracket => ({ const createBracket = (name: string): ParsedBracket => ({
name, name,

View File

@ -262,6 +262,75 @@ export function normalizedTeamCount({
return teamsCount * minMembersPerTeam; return teamsCount * minMembersPerTeam;
} }
export type TeamForOrdering = {
id: number;
seed: number | null;
members: { length: number };
avgSeedingSkillOrdinal: number | null;
createdAt: number;
startingBracketIdx: number | null;
};
export function compareTeamsForOrdering(
a: TeamForOrdering,
b: TeamForOrdering,
minMembersPerTeam: number,
): number {
if (a.startingBracketIdx !== b.startingBracketIdx) {
return (a.startingBracketIdx ?? 0) - (b.startingBracketIdx ?? 0);
}
const aIsFull = a.members.length >= minMembersPerTeam;
const bIsFull = b.members.length >= minMembersPerTeam;
if (aIsFull && !bIsFull) {
return -1;
}
if (!aIsFull && bIsFull) {
return 1;
}
if (a.seed !== null && b.seed !== null) {
return a.seed - b.seed;
}
if (
a.avgSeedingSkillOrdinal !== b.avgSeedingSkillOrdinal &&
a.avgSeedingSkillOrdinal !== null &&
b.avgSeedingSkillOrdinal !== null
) {
return b.avgSeedingSkillOrdinal - a.avgSeedingSkillOrdinal;
}
return a.createdAt - b.createdAt;
}
export function sortTeamsBySeeding<T extends TeamForOrdering>(
teams: T[],
minMembersPerTeam: number,
): T[] {
return [...teams].sort((a, b) =>
compareTeamsForOrdering(a, b, minMembersPerTeam),
);
}
export function findTeamInsertPosition<T extends TeamForOrdering>(
existingOrder: number[],
newTeam: T,
teamMap: Map<number, T>,
minMembersPerTeam: number,
): number {
for (let i = 0; i < existingOrder.length; i++) {
const existingTeam = teamMap.get(existingOrder[i]);
if (!existingTeam) continue;
if (compareTeamsForOrdering(newTeam, existingTeam, minMembersPerTeam) < 0) {
return i;
}
}
return existingOrder.length;
}
export function getBracketProgressionLabel( export function getBracketProgressionLabel(
startingBracketIdx: number, startingBracketIdx: number,
progression: ParsedBracket[], progression: ParsedBracket[],

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -33,13 +33,11 @@ const navigateToMatch = async (page: Page, matchId: number) => {
const reportResult = async ({ const reportResult = async ({
page, page,
amountOfMapsToReport, amountOfMapsToReport,
sidesWithMoreThanFourPlayers = ["last"],
winner = 1, winner = 1,
points, points,
}: { }: {
page: Page; page: Page;
amountOfMapsToReport: 1 | 2 | 3 | 4; amountOfMapsToReport: 1 | 2 | 3 | 4;
sidesWithMoreThanFourPlayers?: ("first" | "last")[];
winner?: 1 | 2; winner?: 1 | 2;
points?: [number, number]; points?: [number, number];
}) => { }) => {
@ -53,9 +51,12 @@ const reportResult = async ({
await page.getByTestId("actions-tab").click(); await page.getByTestId("actions-tab").click();
// Auto-detect and set rosters for teams with 5+ players
// Check if first team needs roster selection (checkbox exists and is not disabled)
const firstTeamCheckbox = page.getByTestId("player-checkbox-0").first();
if ( if (
sidesWithMoreThanFourPlayers.includes("first") && (await firstTeamCheckbox.count()) > 0 &&
!(await page.getByTestId("player-checkbox-0").first().isDisabled()) !(await firstTeamCheckbox.isDisabled())
) { ) {
await page.getByTestId("player-checkbox-0").first().click(); await page.getByTestId("player-checkbox-0").first().click();
await page.getByTestId("player-checkbox-1").first().click(); await page.getByTestId("player-checkbox-1").first().click();
@ -67,9 +68,12 @@ const reportResult = async ({
// update went through // update went through
await expect(page.getByTestId("player-checkbox-0").first()).toBeDisabled(); await expect(page.getByTestId("player-checkbox-0").first()).toBeDisabled();
} }
// Check if second team needs roster selection
const lastTeamCheckbox = page.getByTestId("player-checkbox-0").last();
if ( if (
sidesWithMoreThanFourPlayers.includes("last") && (await lastTeamCheckbox.count()) > 0 &&
!(await page.getByTestId("player-checkbox-0").last().isDisabled()) !(await lastTeamCheckbox.isDisabled())
) { ) {
await page.getByTestId("player-checkbox-0").last().click(); await page.getByTestId("player-checkbox-0").last().click();
await page.getByTestId("player-checkbox-1").last().click(); await page.getByTestId("player-checkbox-1").last().click();
@ -139,6 +143,8 @@ const expectScore = (page: Page, score: [number, number]) =>
test.describe("Tournament bracket", () => { test.describe("Tournament bracket", () => {
test("sets active roster as regular member", async ({ page }) => { test("sets active roster as regular member", async ({ page }) => {
const tournamentId = 1; const tournamentId = 1;
// User 37 is owner of team 10 (seed 10) which has 5 players
// Team 10 vs Team 9 (seed 9) is match 2 in WB Round 1
const matchId = 2; const matchId = 2;
await startBracket(page, tournamentId); await startBracket(page, tournamentId);
@ -147,10 +153,13 @@ test.describe("Tournament bracket", () => {
page, page,
url: tournamentMatchPage({ tournamentId, matchId }), url: tournamentMatchPage({ tournamentId, matchId }),
}); });
await expect(page.getByTestId("active-roster-needed-text")).toBeVisible(); await expect(page.getByTestId("active-roster-needed-text")).toBeVisible();
await page.getByTestId("actions-tab").click(); await page.getByTestId("actions-tab").click();
// Team 10 has 5 players; select first 4 for active roster
// Team 10 is team 2 (second team in the match), so use last()
await page.getByTestId("player-checkbox-0").last().click(); await page.getByTestId("player-checkbox-0").last().click();
await page.getByTestId("player-checkbox-1").last().click(); await page.getByTestId("player-checkbox-1").last().click();
await page.getByTestId("player-checkbox-2").last().click(); await page.getByTestId("player-checkbox-2").last().click();
@ -163,6 +172,7 @@ test.describe("Tournament bracket", () => {
page, page,
url: tournamentMatchPage({ tournamentId, matchId }), url: tournamentMatchPage({ tournamentId, matchId }),
}); });
// Only team 10 needed to set roster (team 9 has 4 players)
await isNotVisible(page.getByTestId("active-roster-needed-text")); await isNotVisible(page.getByTestId("active-roster-needed-text"));
await page.getByTestId("actions-tab").click(); await page.getByTestId("actions-tab").click();
@ -214,7 +224,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 1, amountOfMapsToReport: 1,
sidesWithMoreThanFourPlayers: ["first", "last"],
}); });
await backToBracket(page); await backToBracket(page);
@ -246,7 +255,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: ["last"],
winner: 2, winner: 2,
}); });
await backToBracket(page); await backToBracket(page);
@ -323,7 +331,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: [],
}); });
await backToBracket(page); await backToBracket(page);
@ -394,7 +401,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: ["first", "last"],
points: [100, 0], points: [100, 0],
}); });
await backToBracket(page); await backToBracket(page);
@ -434,7 +440,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 3, amountOfMapsToReport: 3,
sidesWithMoreThanFourPlayers: ["first", "last"],
}); });
await navigate({ await navigate({
@ -448,7 +453,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 3, amountOfMapsToReport: 3,
sidesWithMoreThanFourPlayers: ["first", "last"],
}); });
await backToBracket(page); await backToBracket(page);
@ -511,7 +515,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: [],
points: [100, 0], points: [100, 0],
}); });
await backToBracket(page); await backToBracket(page);
@ -635,7 +638,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: ["first"],
points: [100, 0], points: [100, 0],
}); });
@ -699,7 +701,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: ["last"],
points: [100, 0], points: [100, 0],
}); });
await backToBracket(page); await backToBracket(page);
@ -710,7 +711,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: ["first"],
points: [100, 0], points: [100, 0],
}); });
await backToBracket(page); await backToBracket(page);
@ -719,7 +719,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: ["last"],
points: [100, 0], points: [100, 0],
}); });
await backToBracket(page); await backToBracket(page);
@ -728,7 +727,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: ["last"],
points: [100, 0], points: [100, 0],
}); });
await backToBracket(page); await backToBracket(page);
@ -739,7 +737,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: ["first"],
points: [0, 100], points: [0, 100],
winner: 2, winner: 2,
}); });
@ -770,7 +767,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: ["last"],
points: [100, 0], points: [100, 0],
}); });
await backToBracket(page); await backToBracket(page);
@ -779,7 +775,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: ["first", "last"],
points: [100, 0], points: [100, 0],
}); });
await backToBracket(page); await backToBracket(page);
@ -790,7 +785,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 1, amountOfMapsToReport: 1,
sidesWithMoreThanFourPlayers: ["last"],
points: [100, 0], points: [100, 0],
}); });
await backToBracket(page); await backToBracket(page);
@ -843,7 +837,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: ["last"],
}); });
await backToBracket(page); await backToBracket(page);
@ -855,7 +848,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: ["last"],
}); });
await backToBracket(page); await backToBracket(page);
@ -890,7 +882,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: ["last"],
}); });
await page.getByTestId("admin-tab").click(); await page.getByTestId("admin-tab").click();
@ -962,7 +953,6 @@ test.describe("Tournament bracket", () => {
page, page,
amountOfMapsToReport: 3, amountOfMapsToReport: 3,
points: [100, 0], points: [100, 0],
sidesWithMoreThanFourPlayers: ["last"],
winner: 1, winner: 1,
}); });
}); });
@ -989,7 +979,6 @@ test.describe("Tournament bracket", () => {
await reportResult({ await reportResult({
page, page,
amountOfMapsToReport: 2, amountOfMapsToReport: 2,
sidesWithMoreThanFourPlayers: id === 1 ? [] : ["last"],
}); });
await backToBracket(page); await backToBracket(page);
} }

View File

@ -255,7 +255,7 @@ test.describe("Tournament", () => {
url: `${tournamentPage(1)}/seeds`, url: `${tournamentPage(1)}/seeds`,
}); });
await page.getByTestId("seed-team-1").hover(); await page.getByTestId("seed-team-1-handle").hover();
await page.mouse.down(); await page.mouse.down();
// i think the drag & drop library might actually be a bit buggy // i think the drag & drop library might actually be a bit buggy
// so we have to do it in steps like this to allow for testing // so we have to do it in steps like this to allow for testing

View File

@ -0,0 +1,7 @@
export function up(db) {
db.transaction(() => {
db.prepare(
/* sql */ `alter table "Tournament" add "seedingSnapshot" text default null`,
).run();
})();
}