From f969e86ea0ef79159d82c1fb8fd0d01e97f8242b Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Thu, 1 Feb 2024 20:49:51 +0200 Subject: [PATCH] Better UX when trying to pick a tiebreaker map + misc things --- app/db/seed/index.ts | 47 ++++++++------ .../build-analyzer/routes/analyzer.tsx | 1 + app/features/sendouq/routes/q.rules.tsx | 4 ++ .../tournament-bracket/core/Bracket.ts | 24 ++++---- .../routes/to.$id.brackets.tsx | 7 ++- .../tournament/routes/to.$id.admin.tsx | 1 - .../tournament/routes/to.$id.register.tsx | 61 +++++++++++++------ .../user-page/UserRepository.server.ts | 10 +-- .../user-page/components/UserResultsTable.tsx | 8 ++- .../routes/u.$identifier.seasons.tsx | 2 +- e2e/tournament-bracket.spec.ts | 12 ++++ public/locales/en/tournament.json | 1 + 12 files changed, 118 insertions(+), 60 deletions(-) diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts index b22a8f06f..ebe1bc5e0 100644 --- a/app/db/seed/index.ts +++ b/app/db/seed/index.ts @@ -68,6 +68,7 @@ import { NZAP_TEST_ID, } from "./constants"; import placements from "./placements.json"; +import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps"; const calendarEventWithToToolsRegOpen = () => calendarEventWithToTools("PICNIC", true); @@ -961,26 +962,31 @@ const tiebreakerPicks = new MapPool([ { mode: "CB", stageId: 4 }, ]); function calendarEventWithToToolsTieBreakerMapPool() { - for (const { mode, stageId } of tiebreakerPicks.stageModePairs) { - sql - .prepare( - ` - insert into "MapPoolMap" ( - "tieBreakerCalendarEventId", - "stageId", - "mode" - ) values ( - $tieBreakerCalendarEventId, - $stageId, - $mode + for (const tieBreakerCalendarEventId of [ + TO_TOOLS_CALENDAR_EVENT_ID, // PICNIC + TO_TOOLS_CALENDAR_EVENT_ID + 2, // Paddling Pool + ]) { + for (const { mode, stageId } of tiebreakerPicks.stageModePairs) { + sql + .prepare( + ` + insert into "MapPoolMap" ( + "tieBreakerCalendarEventId", + "stageId", + "mode" + ) values ( + $tieBreakerCalendarEventId, + $stageId, + $mode + ) + `, ) - `, - ) - .run({ - tieBreakerCalendarEventId: TO_TOOLS_CALENDAR_EVENT_ID, - stageId, - mode, - }); + .run({ + tieBreakerCalendarEventId, + stageId, + mode, + }); + } } } @@ -1112,6 +1118,9 @@ function calendarEventWithToToolsTeams( for (const pair of shuffledPairs) { if (event === "ITZ" && pair.mode !== "SZ") continue; + if (BANNED_MAPS[pair.mode].includes(pair.stageId)) { + continue; + } if (pair.mode === "SZ" && SZ >= (event === "ITZ" ? 6 : 2)) continue; if (pair.mode === "TC" && TC >= 2) continue; diff --git a/app/features/build-analyzer/routes/analyzer.tsx b/app/features/build-analyzer/routes/analyzer.tsx index f29eaa344..b5823c71a 100644 --- a/app/features/build-analyzer/routes/analyzer.tsx +++ b/app/features/build-analyzer/routes/analyzer.tsx @@ -992,6 +992,7 @@ function StatChart({ options={chartOptions as any} headerSuffix={t("analyzer:abilityPoints.short")} valueSuffix={valueSuffix} + xAxis="linear" /> ); } diff --git a/app/features/sendouq/routes/q.rules.tsx b/app/features/sendouq/routes/q.rules.tsx index d40bacf3b..a891822dc 100644 --- a/app/features/sendouq/routes/q.rules.tsx +++ b/app/features/sendouq/routes/q.rules.tsx @@ -47,6 +47,10 @@ export default function SendouqRules() {
Match can be canceled if both group owners agree. If the groups don't agree then the match should be played out. +
+
Only exception is if a group asks to play without Splattercolor + Screen and the other group in the match doesn't want to. In this + situation either group is free to cancel the match.

Room hosting

diff --git a/app/features/tournament-bracket/core/Bracket.ts b/app/features/tournament-bracket/core/Bracket.ts index 5b1225924..31ca02ba0 100644 --- a/app/features/tournament-bracket/core/Bracket.ts +++ b/app/features/tournament-bracket/core/Bracket.ts @@ -688,17 +688,19 @@ class RoundRobinBracket extends Bracket { let lastPlacement = 0; let currentPlacement = 1; let teamsEncountered = 0; - return sorted.map((team) => { - if (team.placement !== lastPlacement) { - lastPlacement = team.placement; - currentPlacement = teamsEncountered + 1; - } - teamsEncountered++; - return { - ...team, - placement: currentPlacement, - }; - }); + return this.standingsWithoutNonParticipants( + sorted.map((team) => { + if (team.placement !== lastPlacement) { + lastPlacement = team.placement; + currentPlacement = teamsEncountered + 1; + } + teamsEncountered++; + return { + ...team, + placement: currentPlacement, + }; + }), + ); } get type(): TournamentBracketProgression[number]["type"] { diff --git a/app/features/tournament-bracket/routes/to.$id.brackets.tsx b/app/features/tournament-bracket/routes/to.$id.brackets.tsx index 24a35856b..bf8d917da 100644 --- a/app/features/tournament-bracket/routes/to.$id.brackets.tsx +++ b/app/features/tournament-bracket/routes/to.$id.brackets.tsx @@ -426,7 +426,7 @@ export default function TournamentBracketsPage() { ) : null} {tournament.ctx.isFinalized || tournament.canFinalize(user) ? ( - + ) : null}
diff --git a/app/features/tournament/routes/to.$id.admin.tsx b/app/features/tournament/routes/to.$id.admin.tsx index ec41a637b..69505b940 100644 --- a/app/features/tournament/routes/to.$id.admin.tsx +++ b/app/features/tournament/routes/to.$id.admin.tsx @@ -230,7 +230,6 @@ export const action: ActionFunction = async ({ request, params }) => { return null; }; -// xxx: download participants of certain bracket (not checked in probably most relevant) // TODO: translations export default function TournamentAdminPage() { const { t } = useTranslation(["calendar"]); diff --git a/app/features/tournament/routes/to.$id.register.tsx b/app/features/tournament/routes/to.$id.register.tsx index e04747537..cd15f6145 100644 --- a/app/features/tournament/routes/to.$id.register.tsx +++ b/app/features/tournament/routes/to.$id.register.tsx @@ -29,6 +29,7 @@ import { MapPool } from "~/features/map-list-generator/core/map-pool"; import { BANNED_MAPS } from "~/features/sendouq-settings/banned-maps"; import * as TeamRepository from "~/features/team/TeamRepository.server"; import { findMapPoolByTeamId } from "~/features/tournament-bracket"; +import type { TournamentData } from "~/features/tournament-bracket/core/Tournament.server"; import { tournamentFromDB, type TournamentDataTeam, @@ -155,8 +156,11 @@ export const action: ActionFunction = async ({ request, params }) => { const mapPool = new MapPool(data.mapPool); validate(ownTeam); validate( - validateCounterPickMapPool(mapPool, isOneModeTournamentOf(event)) === - "VALID", + validateCounterPickMapPool( + mapPool, + isOneModeTournamentOf(event), + tournament.ctx.tieBreakerMapPool, + ) === "VALID", ); upsertCounterpickMaps({ @@ -557,7 +561,6 @@ function TeamInfo({ canUnregister: boolean; }) { const { t } = useTranslation(["tournament", "common"]); - const id = React.useId(); const fetcher = useFetcher(); return ( @@ -599,12 +602,12 @@ function TeamInfo({
-
@@ -909,21 +912,26 @@ function CounterPickMapPoolPicker() { }`} > - {stageIds - .filter((id) => id !== tiebreakerStageId) - .map((stageId) => { - const isBanned = - BANNED_MAPS[mode].includes(stageId); + {stageIds.map((stageId) => { + const isBanned = + BANNED_MAPS[mode].includes(stageId); - return ( - - ); - })} + + ); + })}
); @@ -934,6 +942,7 @@ function CounterPickMapPoolPicker() { {validateCounterPickMapPool( counterPickMapPool, isOneModeTournamentOf, + tournament.ctx.tieBreakerMapPool, ) === "VALID" ? ( )} @@ -967,7 +977,8 @@ function MapPoolValidationStatusMessage({ if ( status !== "TOO_MUCH_STAGE_REPEAT" && status !== "STAGE_REPEAT_IN_SAME_MODE" && - status !== "INCLUDES_BANNED" + status !== "INCLUDES_BANNED" && + status !== "INCLUDES_TIEBREAKER" ) return null; @@ -987,11 +998,13 @@ type CounterPickValidationStatus = | "VALID" | "TOO_MUCH_STAGE_REPEAT" | "STAGE_REPEAT_IN_SAME_MODE" - | "INCLUDES_BANNED"; + | "INCLUDES_BANNED" + | "INCLUDES_TIEBREAKER"; function validateCounterPickMapPool( mapPool: MapPool, isOneModeOnlyTournamentFor: ModeShort | null, + tieBreakerMapPool: TournamentData["ctx"]["tieBreakerMapPool"], ): CounterPickValidationStatus { const stageCounts = new Map(); for (const stageId of mapPool.stages) { @@ -1024,6 +1037,16 @@ function validateCounterPickMapPool( return "INCLUDES_BANNED"; } + if ( + mapPool.stageModePairs.some((pair) => + tieBreakerMapPool.some( + (stage) => stage.mode === pair.mode && stage.stageId === pair.stageId, + ), + ) + ) { + return "INCLUDES_TIEBREAKER"; + } + if ( !isOneModeOnlyTournamentFor && (mapPool.parsed.SZ.length !== TOURNAMENT.COUNTERPICK_MAPS_PER_MODE || diff --git a/app/features/user-page/UserRepository.server.ts b/app/features/user-page/UserRepository.server.ts index 097de066d..28044b658 100644 --- a/app/features/user-page/UserRepository.server.ts +++ b/app/features/user-page/UserRepository.server.ts @@ -211,18 +211,18 @@ export function findResultsByUserId(userId: number) { "TournamentResult.isHighlight", jsonArrayFrom( eb - .selectFrom("TournamentTeamMember") - .innerJoin("User", "User.id", "TournamentTeamMember.userId") + .selectFrom("TournamentResult as TournamentResult2") + .innerJoin("User", "User.id", "TournamentResult2.userId") .select([ ...COMMON_USER_FIELDS, sql`null`.as("name"), ]) .whereRef( - "TournamentTeamMember.tournamentTeamId", + "TournamentResult2.tournamentTeamId", "=", - "TournamentTeam.id", + "TournamentResult.tournamentTeamId", ) - .where("TournamentTeamMember.userId", "!=", userId), + .where("TournamentResult2.userId", "!=", userId), ).as("mates"), ]) .where("TournamentResult.userId", "=", userId), diff --git a/app/features/user-page/components/UserResultsTable.tsx b/app/features/user-page/components/UserResultsTable.tsx index f502c918d..3187ac708 100644 --- a/app/features/user-page/components/UserResultsTable.tsx +++ b/app/features/user-page/components/UserResultsTable.tsx @@ -44,7 +44,7 @@ export function UserResultsTable({ - {results.map((result) => { + {results.map((result, i) => { // We are trying to construct a reasonable label for the checkbox // which shouldn't contain the whole information of the table row as // that can be also accessed when needed. @@ -98,6 +98,7 @@ export function UserResultsTable({ to={tournamentBracketsPage({ tournamentId: result.tournamentId, })} + data-testid="tournament-name-cell" > {result.eventName} @@ -116,7 +117,10 @@ export function UserResultsTable({ )} -
    +
      {result.mates.map((player) => (
    • ; + return ; } const MIN_DEGREE = 5; diff --git a/e2e/tournament-bracket.spec.ts b/e2e/tournament-bracket.spec.ts index f90ed49ef..9bfe2a8d5 100644 --- a/e2e/tournament-bracket.spec.ts +++ b/e2e/tournament-bracket.spec.ts @@ -363,7 +363,19 @@ test.describe("Tournament bracket", () => { await expect(page.getByTestId("standing-1")).toBeVisible(); await isNotVisible(page.getByTestId("standing-3")); + // not possible to reopen finals match anymore await navigateToMatch(page, 14); await isNotVisible(page.getByTestId("reopen-match-button")); + await backToBracket(page); + + // added result to user profile + await page.getByTestId("standing-player").first().click(); + await page.getByText("Results").click(); + await expect( + page.getByTestId("tournament-name-cell").first(), + ).toContainText("Paddling Pool 253"); + await expect( + page.locator('[data-testid="mates-cell-placement-0"] li'), + ).toHaveCount(3); }); }); diff --git a/public/locales/en/tournament.json b/public/locales/en/tournament.json index 9a7e1c1e6..1af77532a 100644 --- a/public/locales/en/tournament.json +++ b/public/locales/en/tournament.json @@ -39,6 +39,7 @@ "pre.pool.tiebreaker": "Tiebreaker: {{stage}}", "pre.pool.pick": "Pick {{number}}", "pre.pool.banned": "Banned", + "pre.pool.tiebreaker.short": "Tiebreaker", "pre.sub.prompt": "No team in mind for this event? You can also join the list of subs.",