mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-08-28 06:08:13 -05:00
KO refactor
This commit is contained in:
@@ -21,7 +21,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
.withPoints {
|
||||
.withKo {
|
||||
grid-template-areas:
|
||||
"header header header"
|
||||
"actions actions actions"
|
||||
|
||||
@@ -39,8 +39,8 @@ interface MatchActionTabProps {
|
||||
ownTeamId: number | null;
|
||||
stageId: StageId;
|
||||
mode: ModeShort;
|
||||
withPoints: boolean;
|
||||
onSubmit?: (data: { winnerId: number; points?: [number, number] }) => void;
|
||||
withKo: boolean;
|
||||
onSubmit?: (data: { winnerId: number; ko?: boolean }) => void;
|
||||
isSubmitting?: boolean;
|
||||
setEnding?: SetEndingData;
|
||||
actionButtons?: React.ReactNode;
|
||||
@@ -52,7 +52,7 @@ export function MatchActionTab({
|
||||
ownTeamId,
|
||||
stageId,
|
||||
mode,
|
||||
withPoints,
|
||||
withKo,
|
||||
onSubmit,
|
||||
isSubmitting,
|
||||
setEnding,
|
||||
@@ -73,14 +73,7 @@ export function MatchActionTab({
|
||||
|
||||
const submit = () => {
|
||||
if (winnerId === null) return;
|
||||
const submitPoints: [number, number] | undefined = withPoints
|
||||
? isKo
|
||||
? winnerId === teams[0].id
|
||||
? [100, 0]
|
||||
: [0, 100]
|
||||
: [0, 0]
|
||||
: undefined;
|
||||
onSubmit?.({ winnerId, points: submitPoints });
|
||||
onSubmit?.({ winnerId, ko: withKo ? isKo : undefined });
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -92,14 +85,14 @@ export function MatchActionTab({
|
||||
mode={mode}
|
||||
winnerId={winnerId}
|
||||
teams={teams}
|
||||
withPoints={withPoints}
|
||||
withKo={withKo}
|
||||
isKo={isKo}
|
||||
isSubmitting={isSubmitting}
|
||||
onBack={() => setConfirming(false)}
|
||||
onConfirm={submit}
|
||||
/>
|
||||
) : (
|
||||
<div className={clsx(styles.root, { [styles.withPoints]: withPoints })}>
|
||||
<div className={clsx(styles.root, { [styles.withKo]: withKo })}>
|
||||
<div className={styles.title}>{t("q:match.action.selectWinner")}</div>
|
||||
{actionButtons ? (
|
||||
<div className={styles.actionButtons}>{actionButtons}</div>
|
||||
@@ -156,7 +149,7 @@ export function MatchActionTab({
|
||||
/>
|
||||
</RadioGroup>
|
||||
|
||||
{withPoints ? (
|
||||
{withKo ? (
|
||||
<div className={styles.ko}>
|
||||
<label className={styles.koLabel}>
|
||||
<input
|
||||
@@ -199,7 +192,7 @@ function SetEndingConfirmation({
|
||||
mode,
|
||||
winnerId,
|
||||
teams,
|
||||
withPoints,
|
||||
withKo,
|
||||
isKo,
|
||||
isSubmitting,
|
||||
onBack,
|
||||
@@ -210,7 +203,7 @@ function SetEndingConfirmation({
|
||||
mode: ModeShort;
|
||||
winnerId: number;
|
||||
teams: [ActionTabTeam, ActionTabTeam];
|
||||
withPoints: boolean;
|
||||
withKo: boolean;
|
||||
isKo: boolean;
|
||||
isSubmitting?: boolean;
|
||||
onBack: () => void;
|
||||
@@ -226,7 +219,7 @@ function SetEndingConfirmation({
|
||||
timestamp: Date.now(),
|
||||
winner: winnerSide,
|
||||
rosters: setEnding.currentRosters,
|
||||
points: withPoints
|
||||
points: withKo
|
||||
? isKo
|
||||
? [winnerSide === "ALPHA" ? 100 : 0, winnerSide === "BRAVO" ? 100 : 0]
|
||||
: [0, 0]
|
||||
|
||||
@@ -290,7 +290,7 @@ export default function MatchPageTestRoute() {
|
||||
ownTeamId={1}
|
||||
stageId={4}
|
||||
mode="SZ"
|
||||
withPoints={true}
|
||||
withKo={true}
|
||||
actionButtons={
|
||||
<SendouButton
|
||||
variant="minimal-destructive"
|
||||
|
||||
@@ -74,7 +74,7 @@ function ReportMapSection({ viewerSide }: { viewerSide: ScrimSide }) {
|
||||
ownTeamId={ownTeamId}
|
||||
stageId={map.stageId}
|
||||
mode={map.mode}
|
||||
withPoints={false}
|
||||
withKo={false}
|
||||
isSubmitting={fetcher.state !== "idle"}
|
||||
onSubmit={({ winnerId }) => {
|
||||
fetcher.submit(
|
||||
|
||||
@@ -422,7 +422,7 @@ function InProgressTab({
|
||||
ownTeamId={ownTeamId}
|
||||
stageId={currentMap.stageId}
|
||||
mode={currentMap.mode}
|
||||
withPoints={false}
|
||||
withKo={false}
|
||||
isSubmitting={fetcher.state !== "idle"}
|
||||
setEnding={setEnding}
|
||||
onSubmit={({ winnerId }) => {
|
||||
|
||||
@@ -37,7 +37,7 @@ export type SeedOrdering =
|
||||
/** The seeding for a stage. Each element is a participant id or a BYE: `null`. */
|
||||
export type Seeding = (number | null)[];
|
||||
|
||||
// xxx: are all of these really in use?
|
||||
// xxx: are all of these really in use? particularly what is the difference between Locked and Waiting for us. Waiting sems redundant. Also difference between Ready and Running, surely Running is enough?
|
||||
/** Same values as the old brackets-model Status — persisted in TournamentMatch.status. */
|
||||
export const MatchStatus = {
|
||||
/** The two matches leading to this one are not completed yet. */
|
||||
|
||||
@@ -31,36 +31,14 @@ const reportedMatchPosition = z.preprocess(
|
||||
.max(Math.max(...TOURNAMENT.AVAILABLE_BEST_OF) - 1),
|
||||
);
|
||||
|
||||
const point = z.number().int().min(0).max(100);
|
||||
const points = z.preprocess(
|
||||
safeJSONParse,
|
||||
z
|
||||
.tuple([point, point])
|
||||
.nullish()
|
||||
.refine(
|
||||
(val) => {
|
||||
if (!val) return true;
|
||||
const [p1, p2] = val;
|
||||
|
||||
// KO
|
||||
if (p1 === 100 && p2 === 0) return true;
|
||||
if (p2 === 100 && p1 === 0) return true;
|
||||
// ...or no points sent at all (TODO: if we decide that this KO only approach is solid then we can do a proper data model migration)
|
||||
if (p1 === 0 && p2 === 0) return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
{
|
||||
message: "Invalid points. Valid: 100-0, 0-100 or 0-0.",
|
||||
},
|
||||
),
|
||||
);
|
||||
// TODO: KO is stored as points (100-0, 0-100 or 0-0). If we decide that this KO only approach is solid then we can do a proper data model migration
|
||||
const ko = z.preprocess(safeJSONParse, z.boolean().nullish());
|
||||
export const matchSchema = z.union([
|
||||
z.object({
|
||||
_action: _action("REPORT_SCORE"),
|
||||
winnerTeamId: id,
|
||||
position: reportedMatchPosition,
|
||||
points,
|
||||
ko,
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("SET_ACTIVE_ROSTER"),
|
||||
@@ -80,7 +58,7 @@ export const matchSchema = z.union([
|
||||
_action: _action("UPDATE_REPORTED_SCORE"),
|
||||
rosters: bothTeamPlayerIds,
|
||||
resultId: id,
|
||||
points,
|
||||
ko,
|
||||
}),
|
||||
z.object({
|
||||
_action: _action("REOPEN_MATCH"),
|
||||
|
||||
@@ -160,23 +160,18 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||
|
||||
const winnerSide = data.winnerTeamId === match.opponentOne.id ? 0 : 1;
|
||||
|
||||
errorToastIfFalsy(
|
||||
!data.points ||
|
||||
data.points[0] === data.points[1] ||
|
||||
(winnerSide === 0 && data.points[0] > data.points[1]) ||
|
||||
(winnerSide === 1 && data.points[1] > data.points[0]),
|
||||
"Points are invalid (winner must have more points than loser)",
|
||||
);
|
||||
|
||||
const bracket = tournament.bracketByIdx(
|
||||
tournament.matchIdToBracketIdx(match.id)!,
|
||||
)!;
|
||||
errorToastIfFalsy(
|
||||
// xxx: just have data.ko from now on? and update error
|
||||
!bracket.collectsKos || data.points,
|
||||
"Points are required for this bracket",
|
||||
!bracket.collectsKos || typeof data.ko === "boolean",
|
||||
"KO status is required for this bracket",
|
||||
);
|
||||
|
||||
const points = bracket.collectsKos
|
||||
? koToPoints({ ko: Boolean(data.ko), winnerSide })
|
||||
: null;
|
||||
|
||||
const teamOneRoster = tournamentTeamToActiveRosterUserIds(
|
||||
tournament.teamById(match.opponentOne.id!)!,
|
||||
tournament.minMembersPerTeam,
|
||||
@@ -215,8 +210,8 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||
winnerTeamId: data.winnerTeamId,
|
||||
number: data.position + 1,
|
||||
source: String(currentMap.source),
|
||||
opponentOnePoints: data.points?.[0] ?? null,
|
||||
opponentTwoPoints: data.points?.[1] ?? null,
|
||||
opponentOnePoints: points?.[0] ?? null,
|
||||
opponentTwoPoints: points?.[1] ?? null,
|
||||
});
|
||||
|
||||
for (const userId of teamOneRoster) {
|
||||
@@ -395,41 +390,31 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||
"Invalid roster",
|
||||
);
|
||||
|
||||
const hadPoints = typeof result.opponentOnePoints === "number";
|
||||
const willHavePoints = typeof data.points?.[0] === "number";
|
||||
errorToastIfFalsy(
|
||||
(hadPoints && willHavePoints) || (!hadPoints && !willHavePoints),
|
||||
"Points mismatch",
|
||||
);
|
||||
const hadKoRecorded = typeof result.opponentOnePoints === "number";
|
||||
const hasKoSubmitted = typeof data.ko === "boolean";
|
||||
errorToastIfFalsy(hadKoRecorded === hasKoSubmitted, "KO status mismatch");
|
||||
|
||||
if (data.points) {
|
||||
if (data.points[0] !== result.opponentOnePoints) {
|
||||
// changing points at this point could retroactively change who advanced from the group
|
||||
errorToastIfFalsy(
|
||||
tournament.matchCanBeReopened(match.id),
|
||||
"Bracket has progressed",
|
||||
);
|
||||
}
|
||||
|
||||
if (data.points[0] === 100) {
|
||||
errorToastIfFalsy(
|
||||
result.winnerTeamId === match.opponentOne!.id,
|
||||
"KO winner must match the result winner",
|
||||
);
|
||||
} else if (data.points[1] === 100) {
|
||||
errorToastIfFalsy(
|
||||
result.winnerTeamId === match.opponentTwo!.id,
|
||||
"KO winner must match the result winner",
|
||||
);
|
||||
}
|
||||
const wasKo =
|
||||
result.opponentOnePoints === 100 || result.opponentTwoPoints === 100;
|
||||
if (typeof data.ko === "boolean" && data.ko !== wasKo) {
|
||||
// changing the KO status at this point could retroactively change who advanced from the group
|
||||
errorToastIfFalsy(
|
||||
tournament.matchCanBeReopened(match.id),
|
||||
"Bracket has progressed",
|
||||
);
|
||||
}
|
||||
|
||||
sql.transaction(() => {
|
||||
if (data.points) {
|
||||
if (typeof data.ko === "boolean") {
|
||||
const points = koToPoints({
|
||||
ko: data.ko,
|
||||
winnerSide: result.winnerTeamId === match.opponentOne!.id ? 0 : 1,
|
||||
});
|
||||
|
||||
updateMatchGameResultPoints({
|
||||
matchGameResultId: result.id,
|
||||
opponentOnePoints: data.points[0],
|
||||
opponentTwoPoints: data.points[1],
|
||||
opponentOnePoints: points[0],
|
||||
opponentTwoPoints: points[1],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -868,6 +853,19 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||
return null;
|
||||
};
|
||||
|
||||
/** KO status is stored as points: the KO winner gets 100 and the loser 0, a game that was not a KO is stored as 0-0. */
|
||||
function koToPoints({
|
||||
ko,
|
||||
winnerSide,
|
||||
}: {
|
||||
ko: boolean;
|
||||
winnerSide: 0 | 1;
|
||||
}): [number, number] {
|
||||
if (!ko) return [0, 0];
|
||||
|
||||
return winnerSide === 0 ? [100, 0] : [0, 100];
|
||||
}
|
||||
|
||||
function canReportTournamentScore({
|
||||
match,
|
||||
isMemberOfATeamInTheMatch,
|
||||
|
||||
@@ -54,7 +54,7 @@ export function TournamentMatchActionTab({
|
||||
|
||||
if (!teamOne || !teamTwo) return null;
|
||||
|
||||
const withPoints = tournament.bracketByIdxOrDefault(
|
||||
const withKo = tournament.bracketByIdxOrDefault(
|
||||
tournament.matchIdToBracketIdx(data.match.id) ?? 0,
|
||||
).collectsKos;
|
||||
|
||||
@@ -113,16 +113,16 @@ export function TournamentMatchActionTab({
|
||||
ownTeamId={ownTeamId}
|
||||
stageId={currentMap.stageId}
|
||||
mode={currentMap.mode}
|
||||
withPoints={withPoints}
|
||||
withKo={withKo}
|
||||
setEnding={setEnding}
|
||||
isSubmitting={reportFetcher.state !== "idle"}
|
||||
onSubmit={({ winnerId, points }) => {
|
||||
onSubmit={({ winnerId, ko }) => {
|
||||
reportFetcher.submit(
|
||||
{
|
||||
_action: "REPORT_SCORE",
|
||||
winnerTeamId: String(winnerId),
|
||||
position: String(scoreSum),
|
||||
...(points ? { points: JSON.stringify(points) } : {}),
|
||||
...(typeof ko === "boolean" ? { ko: String(ko) } : {}),
|
||||
},
|
||||
{ method: "post" },
|
||||
);
|
||||
|
||||
@@ -349,7 +349,7 @@ function EditReportedScoresSection({
|
||||
const { t } = useTranslation(["tournament"]);
|
||||
const tournament = useTournament();
|
||||
|
||||
const withPoints = tournament.bracketByIdxOrDefault(
|
||||
const withKo = tournament.bracketByIdxOrDefault(
|
||||
tournament.matchIdToBracketIdx(data.match.id) ?? 0,
|
||||
).collectsKos;
|
||||
|
||||
@@ -363,7 +363,7 @@ function EditReportedScoresSection({
|
||||
index={index}
|
||||
result={result}
|
||||
teams={teams}
|
||||
withPoints={withPoints}
|
||||
withKo={withKo}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -375,12 +375,12 @@ function EditReportedScoreRow({
|
||||
index,
|
||||
result,
|
||||
teams,
|
||||
withPoints,
|
||||
withKo,
|
||||
}: {
|
||||
index: number;
|
||||
result: TournamentMatchLoaderData["results"][number];
|
||||
teams: [TournamentDataTeam, TournamentDataTeam];
|
||||
withPoints: boolean;
|
||||
withKo: boolean;
|
||||
}) {
|
||||
const { t } = useTranslation(["common", "game-misc", "tournament"]);
|
||||
const tournament = useTournament();
|
||||
@@ -433,7 +433,7 @@ function EditReportedScoreRow({
|
||||
fetcher={fetcher}
|
||||
result={result}
|
||||
teams={teams}
|
||||
withPoints={withPoints}
|
||||
withKo={withKo}
|
||||
minMembersPerTeam={tournament.minMembersPerTeam}
|
||||
onCancel={() => setEditing(false)}
|
||||
index={index}
|
||||
@@ -445,7 +445,7 @@ function EditReportedScoreForm({
|
||||
fetcher,
|
||||
result,
|
||||
teams,
|
||||
withPoints,
|
||||
withKo,
|
||||
minMembersPerTeam,
|
||||
onCancel,
|
||||
index,
|
||||
@@ -453,7 +453,7 @@ function EditReportedScoreForm({
|
||||
fetcher: ReturnType<typeof useFetcher>;
|
||||
result: TournamentMatchLoaderData["results"][number];
|
||||
teams: [TournamentDataTeam, TournamentDataTeam];
|
||||
withPoints: boolean;
|
||||
withKo: boolean;
|
||||
minMembersPerTeam: number;
|
||||
onCancel: () => void;
|
||||
index: number;
|
||||
@@ -475,13 +475,6 @@ function EditReportedScoreForm({
|
||||
result.opponentOnePoints === 100 || result.opponentTwoPoints === 100,
|
||||
);
|
||||
|
||||
const team0Won = result.winnerTeamId === teams[0].id;
|
||||
const points: [number, number] = isKO
|
||||
? team0Won
|
||||
? [100, 0]
|
||||
: [0, 100]
|
||||
: [0, 0];
|
||||
|
||||
const formValid = checkedPlayers.every(
|
||||
(team) => team.length === minMembersPerTeam,
|
||||
);
|
||||
@@ -536,9 +529,9 @@ function EditReportedScoreForm({
|
||||
name="rosters"
|
||||
value={JSON.stringify(checkedPlayers)}
|
||||
/>
|
||||
{withPoints ? (
|
||||
{withKo ? (
|
||||
<>
|
||||
<input type="hidden" name="points" value={JSON.stringify(points)} />
|
||||
<input type="hidden" name="ko" value={String(isKO)} />
|
||||
<label className="stack horizontal sm items-center mx-auto">
|
||||
<input
|
||||
type="checkbox"
|
||||
|
||||
Reference in New Issue
Block a user