Better UX when trying to pick a tiebreaker map + misc things

This commit is contained in:
Kalle 2024-02-01 20:49:51 +02:00
parent e506a22cb8
commit f969e86ea0
12 changed files with 118 additions and 60 deletions

View File

@ -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;

View File

@ -992,6 +992,7 @@ function StatChart({
options={chartOptions as any}
headerSuffix={t("analyzer:abilityPoints.short")}
valueSuffix={valueSuffix}
xAxis="linear"
/>
);
}

View File

@ -47,6 +47,10 @@ export default function SendouqRules() {
<div>
Match can be canceled if both group owners agree. If the groups
don&apos;t agree then the match should be played out.
<br />
<br /> Only exception is if a group asks to play without Splattercolor
Screen and the other group in the match doesn&apos;t want to. In this
situation either group is free to cancel the match.
</div>
<h2 className="text-lg mt-4">Room hosting</h2>

View File

@ -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"] {

View File

@ -426,7 +426,7 @@ export default function TournamentBracketsPage() {
) : null}
</div>
{tournament.ctx.isFinalized || tournament.canFinalize(user) ? (
<FinalStandings standings={tournament.standings} />
<FinalStandings />
) : null}
<BracketNav bracketIdx={bracketIdx} setBracketIdx={setBracketIdx} />
<div
@ -571,11 +571,13 @@ function AddSubsPopOver() {
);
}
function FinalStandings({ standings }: { standings: Standing[] }) {
function FinalStandings() {
const tournament = useTournament();
const { t } = useTranslation(["tournament"]);
const [viewAll, setViewAll] = React.useState(false);
const standings = tournament.standings;
if (standings.length < 2) {
console.error("Unexpectedly few standings");
return null;
@ -625,6 +627,7 @@ function FinalStandings({ standings }: { standings: Standing[] }) {
to={userPage(player)}
key={player.userId}
className="stack items-center text-xs"
data-testid="standing-player"
>
<Avatar user={player} size="xxs" />
</Link>

View File

@ -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"]);

View File

@ -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({
<div>
<div className="text-lighter text-sm stack horizontal sm items-center">
<input
id={id}
id="no-host"
type="checkbox"
name="prefersNotToHost"
defaultChecked={Boolean(prefersNotToHost)}
/>
<label htmlFor={id} className="mb-0">
<label htmlFor="no-host" className="mb-0">
{t("tournament:pre.info.noHost")}
</label>
</div>
@ -909,21 +912,26 @@ function CounterPickMapPoolPicker() {
}`}
>
<option value=""></option>
{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 (
<option key={stageId} value={stageId}>
{t(`game-misc:STAGE_${stageId}`)}{" "}
{isBanned
const isTiebreaker =
stageId === tiebreakerStageId;
return (
<option key={stageId} value={stageId}>
{t(`game-misc:STAGE_${stageId}`)}{" "}
{isTiebreaker
? `(${t(
"tournament:pre.pool.tiebreaker.short",
)})`
: isBanned
? `(${t("tournament:pre.pool.banned")})`
: ""}
</option>
);
})}
</option>
);
})}
</select>
</div>
);
@ -934,6 +942,7 @@ function CounterPickMapPoolPicker() {
{validateCounterPickMapPool(
counterPickMapPool,
isOneModeTournamentOf,
tournament.ctx.tieBreakerMapPool,
) === "VALID" ? (
<SubmitButton
_action="UPDATE_MAP_POOL"
@ -948,6 +957,7 @@ function CounterPickMapPoolPicker() {
status={validateCounterPickMapPool(
counterPickMapPool,
isOneModeTournamentOf,
tournament.ctx.tieBreakerMapPool,
)}
/>
)}
@ -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<StageId, number>();
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 ||

View File

@ -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<string | null>`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),

View File

@ -44,7 +44,7 @@ export function UserResultsTable({
</tr>
</thead>
<tbody>
{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}
</Link>
@ -116,7 +117,10 @@ export function UserResultsTable({
)}
</td>
<td>
<ul className="u__results-players">
<ul
className="u__results-players"
data-testid={`mates-cell-placement-${i}`}
>
{result.mates.map((player) => (
<li
key={player.name ? player.name : player.id}

View File

@ -366,7 +366,7 @@ function PowerChart() {
];
}, [data]);
return <Chart options={chartOptions as any} />;
return <Chart options={chartOptions as any} xAxis="localTime" />;
}
const MIN_DEGREE = 5;

View File

@ -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);
});
});

View File

@ -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.",