mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-09-12 06:06:28 -05:00
point -> ko DB migration
This commit is contained in:
@@ -219,11 +219,7 @@ function SetEndingConfirmation({
|
||||
timestamp: Date.now(),
|
||||
winner: winnerSide,
|
||||
rosters: setEnding.currentRosters,
|
||||
points: withKo
|
||||
? isKo
|
||||
? [winnerSide === "ALPHA" ? 100 : 0, winnerSide === "BRAVO" ? 100 : 0]
|
||||
: [0, 0]
|
||||
: undefined,
|
||||
ko: withKo ? isKo : undefined,
|
||||
};
|
||||
|
||||
const updatedScore = {
|
||||
|
||||
@@ -48,8 +48,8 @@ export interface TimelineMap {
|
||||
alpha: Array<MainWeaponId | null>;
|
||||
bravo: Array<MainWeaponId | null>;
|
||||
};
|
||||
/** Optional point values [alpha, bravo] */
|
||||
points?: [number, number];
|
||||
/** Whether the game ended in a knockout. Undefined if not collected. */
|
||||
ko?: boolean;
|
||||
/** Side that picked this map (counterpick / postGame map PICK). Renders a click indicator next to that side's WIN/LOSS label. */
|
||||
pickedBy?: MatchSide;
|
||||
}
|
||||
@@ -210,15 +210,12 @@ function TimelineHeader({
|
||||
function TimelineMapRow({ map }: { map: TimelineMap }) {
|
||||
const { t } = useTranslation(["game-misc"]);
|
||||
|
||||
const alphaPoints = map.points?.[0];
|
||||
const bravoPoints = map.points?.[1];
|
||||
|
||||
return (
|
||||
<div className={styles.mapEvent}>
|
||||
<div className={styles.mapSide}>
|
||||
<SideResult
|
||||
result={map.winner === "ALPHA" ? "WIN" : "LOSS"}
|
||||
points={alphaPoints}
|
||||
isKo={map.ko && map.winner === "ALPHA"}
|
||||
weapons={map.weapons?.alpha}
|
||||
isPicked={map.pickedBy === "ALPHA"}
|
||||
/>
|
||||
@@ -242,7 +239,7 @@ function TimelineMapRow({ map }: { map: TimelineMap }) {
|
||||
<div className={styles.mapSide}>
|
||||
<SideResult
|
||||
result={map.winner === "BRAVO" ? "WIN" : "LOSS"}
|
||||
points={bravoPoints}
|
||||
isKo={map.ko && map.winner === "BRAVO"}
|
||||
weapons={map.weapons?.bravo}
|
||||
isPicked={map.pickedBy === "BRAVO"}
|
||||
/>
|
||||
@@ -253,12 +250,12 @@ function TimelineMapRow({ map }: { map: TimelineMap }) {
|
||||
|
||||
function SideResult({
|
||||
result,
|
||||
points,
|
||||
isKo,
|
||||
weapons,
|
||||
isPicked,
|
||||
}: {
|
||||
result: "WIN" | "LOSS";
|
||||
points?: number;
|
||||
isKo?: boolean;
|
||||
weapons?: Array<MainWeaponId | null>;
|
||||
isPicked?: boolean;
|
||||
}) {
|
||||
@@ -288,7 +285,7 @@ function SideResult({
|
||||
? t("q:match.timeline.win")
|
||||
: t("q:match.timeline.loss")}
|
||||
</span>
|
||||
{points === 100 ? (
|
||||
{isKo ? (
|
||||
<span className={styles.resultPoints}>{t("q:match.action.ko")}</span>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
@@ -692,6 +692,8 @@ export interface TournamentMatchPickBanEvent {
|
||||
export interface TournamentMatchGameResult {
|
||||
createdAt: Generated<number>;
|
||||
id: GeneratedAlways<number>;
|
||||
/** Whether the game ended in a knockout. `null` if not collected for this bracket. */
|
||||
ko: DBBoolean | null;
|
||||
matchId: number;
|
||||
mode: ModeShort;
|
||||
number: number;
|
||||
@@ -699,8 +701,6 @@ export interface TournamentMatchGameResult {
|
||||
source: string;
|
||||
stageId: StageId;
|
||||
winnerTeamId: number;
|
||||
opponentOnePoints: number | null;
|
||||
opponentTwoPoints: number | null;
|
||||
}
|
||||
|
||||
export interface TournamentMatchGameResultParticipant {
|
||||
|
||||
@@ -58,7 +58,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||
? (map.source as MapListMap["source"])
|
||||
: Number(map.source),
|
||||
participatedUserIds: null,
|
||||
points: null,
|
||||
ko: null,
|
||||
})),
|
||||
teamAlpha: {
|
||||
id: match.groupAlpha.id,
|
||||
|
||||
@@ -52,8 +52,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||
"TournamentMatchGameResult.mode",
|
||||
"TournamentMatchGameResult.winnerTeamId",
|
||||
"TournamentMatchGameResult.source",
|
||||
"TournamentMatchGameResult.opponentOnePoints",
|
||||
"TournamentMatchGameResult.opponentTwoPoints",
|
||||
"TournamentMatchGameResult.ko",
|
||||
jsonArrayFrom(
|
||||
innerEb
|
||||
.selectFrom("TournamentMatchGameResultParticipant")
|
||||
@@ -106,10 +105,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||
participatedUserIds: playedMap.participants.map((p) => p.userId),
|
||||
winnerTeamId: playedMap.winnerTeamId,
|
||||
source: parseSource(playedMap.source),
|
||||
points:
|
||||
playedMap.opponentOnePoints && playedMap.opponentTwoPoints
|
||||
? [playedMap.opponentOnePoints, playedMap.opponentTwoPoints]
|
||||
: null,
|
||||
ko: playedMap.ko !== null ? Boolean(playedMap.ko) : null,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -147,7 +143,7 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
|
||||
participatedUserIds: null,
|
||||
winnerTeamId: null,
|
||||
source: mapListMap.source,
|
||||
points: null,
|
||||
ko: null,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
@@ -526,8 +526,8 @@ export type MapListMap = {
|
||||
| "ROLL";
|
||||
winnerTeamId: number | null;
|
||||
participatedUserIds: Array<number> | null;
|
||||
/** (round robin only) points of the match used for tiebreaker purposes. e.g. [100, 0] indicates a knockout. */
|
||||
points: [number, number] | null;
|
||||
/** (round robin only) whether the map ended in a knockout. `null` if not tracked. */
|
||||
ko: boolean | null;
|
||||
};
|
||||
|
||||
type TournamentMatchTeam = {
|
||||
|
||||
@@ -688,7 +688,7 @@ export default function MatchPageTestRoute() {
|
||||
mode: "RM",
|
||||
timestamp: 1712856200,
|
||||
winner: "ALPHA",
|
||||
points: [100, 42],
|
||||
ko: true,
|
||||
weapons: {
|
||||
alpha: [40, null, 1100, 3040],
|
||||
bravo: [null, 210, null, 4010],
|
||||
|
||||
@@ -128,18 +128,13 @@ export async function findByTournamentId(
|
||||
function serializedOpponentWithKos(
|
||||
column: "opponentOne" | "opponentTwo",
|
||||
): RawBuilder<ParticipantResult | null> {
|
||||
const [winnerPoints, loserPoints] =
|
||||
column === "opponentOne"
|
||||
? (["opponentOnePoints", "opponentTwoPoints"] as const)
|
||||
: (["opponentTwoPoints", "opponentOnePoints"] as const);
|
||||
|
||||
return kyselySql<ParticipantResult | null>`json_set(
|
||||
json_remove(${kyselySql.ref(`TournamentMatch.${column}`)}, '$.totalPoints'),
|
||||
'$.totalKos',
|
||||
sum(
|
||||
case
|
||||
when ${kyselySql.ref(`TournamentMatchGameResult.${winnerPoints}`)} = 100
|
||||
and ${kyselySql.ref(`TournamentMatchGameResult.${loserPoints}`)} = 0
|
||||
when "TournamentMatchGameResult"."ko" = 1
|
||||
and "TournamentMatchGameResult"."winnerTeamId" = ${kyselySql.ref(`TournamentMatch.${column}`)} ->> '$.id'
|
||||
then 1
|
||||
else 0
|
||||
end
|
||||
|
||||
@@ -31,7 +31,6 @@ const reportedMatchPosition = z.preprocess(
|
||||
.max(Math.max(...TOURNAMENT.AVAILABLE_BEST_OF) - 1),
|
||||
);
|
||||
|
||||
// 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({
|
||||
|
||||
@@ -105,8 +105,7 @@ export function findResultById(id: number) {
|
||||
.select([
|
||||
"TournamentMatchGameResult.id",
|
||||
"TournamentMatchGameResult.matchId",
|
||||
"TournamentMatchGameResult.opponentOnePoints",
|
||||
"TournamentMatchGameResult.opponentTwoPoints",
|
||||
"TournamentMatchGameResult.ko",
|
||||
"TournamentMatchGameResult.winnerTeamId",
|
||||
])
|
||||
.where("TournamentMatchGameResult.id", "=", id)
|
||||
@@ -123,8 +122,7 @@ export function findResultsByMatchId(matchId: number) {
|
||||
"TournamentMatchGameResult.mode",
|
||||
"TournamentMatchGameResult.source",
|
||||
"TournamentMatchGameResult.createdAt",
|
||||
"TournamentMatchGameResult.opponentOnePoints",
|
||||
"TournamentMatchGameResult.opponentTwoPoints",
|
||||
"TournamentMatchGameResult.ko",
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom("TournamentMatchGameResultParticipant")
|
||||
|
||||
@@ -42,7 +42,7 @@ import { resolveMapList } from "../core/mapList.server";
|
||||
import { deleteParticipantsByMatchGameResultId } from "../queries/deleteParticipantsByMatchGameResultId.server";
|
||||
import { insertTournamentMatchGameResult } from "../queries/insertTournamentMatchGameResult.server";
|
||||
import { insertTournamentMatchGameResultParticipant } from "../queries/insertTournamentMatchGameResultParticipant.server";
|
||||
import { updateMatchGameResultPoints } from "../queries/updateMatchGameResultPoints.server";
|
||||
import { updateMatchGameResultKo } from "../queries/updateMatchGameResultKo.server";
|
||||
import type { FindMatchById } from "../TournamentMatchRepository.server";
|
||||
import {
|
||||
matchIsLocked,
|
||||
@@ -158,8 +158,6 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||
];
|
||||
invariant(currentMap, "Can't resolve current map");
|
||||
|
||||
const winnerSide = data.winnerTeamId === match.opponentOne.id ? 0 : 1;
|
||||
|
||||
const bracket = tournament.bracketByIdx(
|
||||
tournament.matchIdToBracketIdx(match.id)!,
|
||||
)!;
|
||||
@@ -168,10 +166,6 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||
"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,
|
||||
@@ -210,8 +204,7 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||
winnerTeamId: data.winnerTeamId,
|
||||
number: data.position + 1,
|
||||
source: String(currentMap.source),
|
||||
opponentOnePoints: points?.[0] ?? null,
|
||||
opponentTwoPoints: points?.[1] ?? null,
|
||||
ko: bracket.collectsKos ? Number(Boolean(data.ko)) : null,
|
||||
});
|
||||
|
||||
for (const userId of teamOneRoster) {
|
||||
@@ -390,12 +383,15 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||
"Invalid roster",
|
||||
);
|
||||
|
||||
const hadKoRecorded = typeof result.opponentOnePoints === "number";
|
||||
const hasKoSubmitted = typeof data.ko === "boolean";
|
||||
errorToastIfFalsy(hadKoRecorded === hasKoSubmitted, "KO status mismatch");
|
||||
const bracket = tournament.bracketByIdx(
|
||||
tournament.matchIdToBracketIdx(match.id)!,
|
||||
)!;
|
||||
errorToastIfFalsy(
|
||||
!bracket.collectsKos || typeof data.ko === "boolean",
|
||||
"KO status is required for this bracket",
|
||||
);
|
||||
|
||||
const wasKo =
|
||||
result.opponentOnePoints === 100 || result.opponentTwoPoints === 100;
|
||||
const wasKo = Boolean(result.ko);
|
||||
if (typeof data.ko === "boolean" && data.ko !== wasKo) {
|
||||
// changing the KO status at this point could retroactively change who advanced from the group
|
||||
errorToastIfFalsy(
|
||||
@@ -406,15 +402,9 @@ export const action: ActionFunction = async ({ params, request }) => {
|
||||
|
||||
sql.transaction(() => {
|
||||
if (typeof data.ko === "boolean") {
|
||||
const points = koToPoints({
|
||||
ko: data.ko,
|
||||
winnerSide: result.winnerTeamId === match.opponentOne!.id ? 0 : 1,
|
||||
});
|
||||
|
||||
updateMatchGameResultPoints({
|
||||
updateMatchGameResultKo({
|
||||
matchGameResultId: result.id,
|
||||
opponentOnePoints: points[0],
|
||||
opponentTwoPoints: points[1],
|
||||
ko: data.ko,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -853,19 +843,6 @@ 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,
|
||||
|
||||
@@ -271,13 +271,7 @@ function buildSetEndingData({
|
||||
alpha: alphaParticipants,
|
||||
bravo: bravoParticipants,
|
||||
},
|
||||
points:
|
||||
result.opponentOnePoints != null && result.opponentTwoPoints != null
|
||||
? ([result.opponentOnePoints, result.opponentTwoPoints] as [
|
||||
number,
|
||||
number,
|
||||
])
|
||||
: undefined,
|
||||
ko: result.ko != null ? Boolean(result.ko) : undefined,
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -399,8 +399,7 @@ function EditReportedScoreRow({
|
||||
previousFetcherStateRef.current = fetcher.state;
|
||||
}, [fetcher.state, fetcher.data]);
|
||||
|
||||
const isKo =
|
||||
result.opponentOnePoints === 100 || result.opponentTwoPoints === 100;
|
||||
const isKo = Boolean(result.ko);
|
||||
|
||||
if (!editing) {
|
||||
return (
|
||||
@@ -471,9 +470,7 @@ function EditReportedScoreForm({
|
||||
.map((p) => p.userId),
|
||||
];
|
||||
});
|
||||
const [isKO, setIsKO] = React.useState(
|
||||
result.opponentOnePoints === 100 || result.opponentTwoPoints === 100,
|
||||
);
|
||||
const [isKO, setIsKO] = React.useState(Boolean(result.ko));
|
||||
|
||||
const formValid = checkedPlayers.every(
|
||||
(team) => team.length === minMembersPerTeam,
|
||||
|
||||
@@ -153,9 +153,6 @@ function resolveTimelineMaps(
|
||||
}));
|
||||
|
||||
return data.results.map((result, mapIndex) => {
|
||||
const hasPoints =
|
||||
result.opponentOnePoints !== null && result.opponentTwoPoints !== null;
|
||||
|
||||
const alphaRoster = resolveRoster(result.participants, opponentOneId);
|
||||
const bravoRoster = resolveRoster(result.participants, opponentTwoId);
|
||||
|
||||
@@ -185,12 +182,7 @@ function resolveTimelineMaps(
|
||||
weapons: hasAnyWeapon
|
||||
? { alpha: alphaWeapons, bravo: bravoWeapons }
|
||||
: undefined,
|
||||
points: hasPoints
|
||||
? ([result.opponentOnePoints, result.opponentTwoPoints] as [
|
||||
number,
|
||||
number,
|
||||
])
|
||||
: undefined,
|
||||
ko: result.ko != null ? Boolean(result.ko) : undefined,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3,9 +3,9 @@ import type { Tables } from "~/db/tables";
|
||||
|
||||
const stm = sql.prepare(/* sql */ `
|
||||
insert into "TournamentMatchGameResult"
|
||||
("matchId", "stageId", "mode", "winnerTeamId", "reporterId", "number", "source", "opponentOnePoints", "opponentTwoPoints")
|
||||
("matchId", "stageId", "mode", "winnerTeamId", "reporterId", "number", "source", "ko")
|
||||
values
|
||||
(@matchId, @stageId, @mode, @winnerTeamId, @reporterId, @number, @source, @opponentOnePoints, @opponentTwoPoints)
|
||||
(@matchId, @stageId, @mode, @winnerTeamId, @reporterId, @number, @source, @ko)
|
||||
returning *
|
||||
`);
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import { sql } from "~/db/sql";
|
||||
|
||||
const stm = sql.prepare(/* sql */ `
|
||||
update "TournamentMatchGameResult"
|
||||
set "ko" = @ko
|
||||
where "id" = @id
|
||||
`);
|
||||
|
||||
export function updateMatchGameResultKo({
|
||||
matchGameResultId,
|
||||
ko,
|
||||
}: {
|
||||
matchGameResultId: number;
|
||||
ko: boolean;
|
||||
}) {
|
||||
stm.run({ id: matchGameResultId, ko: Number(ko) });
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { sql } from "~/db/sql";
|
||||
|
||||
const stm = sql.prepare(/* sql */ `
|
||||
update "TournamentMatchGameResult"
|
||||
set "opponentOnePoints" = @opponentOnePoints,
|
||||
"opponentTwoPoints" = @opponentTwoPoints
|
||||
where "id" = @id
|
||||
`);
|
||||
|
||||
export function updateMatchGameResultPoints({
|
||||
matchGameResultId,
|
||||
opponentOnePoints,
|
||||
opponentTwoPoints,
|
||||
}: {
|
||||
matchGameResultId: number;
|
||||
opponentOnePoints: number;
|
||||
opponentTwoPoints: number;
|
||||
}) {
|
||||
stm.run({ id: matchGameResultId, opponentOnePoints, opponentTwoPoints });
|
||||
}
|
||||
@@ -130,8 +130,7 @@ describe("Tournament match page", () => {
|
||||
),
|
||||
"Result participants should only include active roster user ids",
|
||||
).toBeTruthy();
|
||||
expect(result.opponentOnePoints).toBe(null);
|
||||
expect(result.opponentTwoPoints).toBe(null);
|
||||
expect(result.ko).toBe(null);
|
||||
expect(result.winnerTeamId).toBe(1);
|
||||
});
|
||||
|
||||
|
||||
BIN
db-test.sqlite3
BIN
db-test.sqlite3
Binary file not shown.
@@ -110,6 +110,26 @@ export function up(db) {
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
/* sql */ `alter table "TournamentMatchGameResult" add "ko" integer`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
/* sql */ `
|
||||
update "TournamentMatchGameResult"
|
||||
set "ko" = 1
|
||||
where ("opponentOnePoints" = 100 and "opponentTwoPoints" = 0)
|
||||
or ("opponentOnePoints" = 0 and "opponentTwoPoints" = 100)
|
||||
`,
|
||||
).run();
|
||||
|
||||
db.prepare(
|
||||
/* sql */ `alter table "TournamentMatchGameResult" drop column "opponentOnePoints"`,
|
||||
).run();
|
||||
db.prepare(
|
||||
/* sql */ `alter table "TournamentMatchGameResult" drop column "opponentTwoPoints"`,
|
||||
).run();
|
||||
|
||||
db.pragma("foreign_key_check");
|
||||
})();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user