206 lines
7.3 KiB
TypeScript
206 lines
7.3 KiB
TypeScript
import { Claims } from './claims';
|
|
import { sql } from 'bun';
|
|
import { first, orderBy } from 'lodash';
|
|
import { BadRequestError, NotFoundError, UnauthorizedError } from '../utilities/errors';
|
|
import { GameId, MatchId, PlayerId, UserId } from '../utilities/secureIds';
|
|
import { calculateElos } from '../utilities/elo';
|
|
import { orm } from './orm';
|
|
|
|
export class MatchParticipant {
|
|
matchId?: MatchId;
|
|
playerId: PlayerId;
|
|
gamesPlayed?: number;
|
|
standing: number;
|
|
elo: number;
|
|
eloChange: number;
|
|
isRatingLocked?: boolean;
|
|
|
|
constructor(
|
|
playerId: PlayerId,
|
|
standing: number,
|
|
eloChange: number = 0,
|
|
gamesPlayed?: number,
|
|
elo: number = 1000,
|
|
matchId?: MatchId,
|
|
) {
|
|
this.matchId = matchId;
|
|
this.playerId = playerId;
|
|
this.standing = standing;
|
|
this.elo = elo;
|
|
this.gamesPlayed = gamesPlayed;
|
|
this.eloChange = eloChange;
|
|
}
|
|
}
|
|
|
|
export class Match {
|
|
id: MatchId;
|
|
gameId: GameId;
|
|
players: MatchParticipant[];
|
|
owner: UserId;
|
|
|
|
constructor(id: MatchId, gameId: GameId, players: MatchParticipant[], owner: UserId) {
|
|
this.id = id;
|
|
this.gameId = gameId;
|
|
this.players = players;
|
|
this.owner = owner;
|
|
}
|
|
}
|
|
|
|
export class MatchOrm {
|
|
async create({
|
|
gameId,
|
|
participants,
|
|
ownerId,
|
|
}: {
|
|
gameId: GameId;
|
|
participants: MatchParticipant[];
|
|
ownerId: UserId;
|
|
}): Promise<Match> {
|
|
await sql`INSERT INTO matches (game_id, owning_user_id) VALUES (${gameId.raw}, ${ownerId.raw})`;
|
|
const newMatchId = MatchId.fromID((first(await sql`SELECT lastval();`) as any)?.lastval as string);
|
|
|
|
const players = await sql`
|
|
SELECT p.id,
|
|
p.is_rating_locked,
|
|
(CASE WHEN p.is_rating_locked THEN 1000 ELSE 1000 + COALESCE(sum(mp.elo_change), 0) END) as elo,
|
|
(CASE WHEN p.is_rating_locked THEN 0 ELSE count(mp.*) END) as games_played
|
|
FROM players p
|
|
LEFT JOIN match_players mp ON mp.player_id = p.id
|
|
WHERE p.id IN ${sql(participants.map((x) => x.playerId.raw))}
|
|
GROUP BY p.id;`;
|
|
|
|
for (let i in participants) {
|
|
const player = players.find((x: any) => x.id === participants[i].playerId.raw);
|
|
participants[i].elo = parseInt(player.elo);
|
|
participants[i].gamesPlayed = parseInt(player.games_played);
|
|
participants[i].isRatingLocked = player.is_rating_locked;
|
|
}
|
|
|
|
const amendedPlayers = calculateElos(participants);
|
|
|
|
await sql.transaction(async (tx) => {
|
|
for (let i in amendedPlayers) {
|
|
await tx`
|
|
INSERT INTO match_players(match_id, player_id, standing, elo_change)
|
|
VALUES (${newMatchId.raw},
|
|
${amendedPlayers[i].playerId.raw},
|
|
${amendedPlayers[i].standing},
|
|
${amendedPlayers[i].isRatingLocked ? 0 : amendedPlayers[i].eloChange})`;
|
|
|
|
if (amendedPlayers[i].isRatingLocked) {
|
|
continue;
|
|
}
|
|
|
|
await tx`UPDATE players SET elo=${amendedPlayers[i].elo} WHERE id=${amendedPlayers[i].playerId.raw}`;
|
|
}
|
|
});
|
|
|
|
return await this.get(newMatchId);
|
|
}
|
|
|
|
async get(id: MatchId, claims?: Claims): Promise<Match> {
|
|
const dbResult = await sql`
|
|
SELECT m.id as match_id,
|
|
m.owning_user_id as owner_id,
|
|
g.id as game_id,
|
|
g.name as game_name,
|
|
p.id as player_id,
|
|
p.name as player_name,
|
|
p.elo as elo,
|
|
mp.standing as standing,
|
|
mp.elo_change as elo_change
|
|
FROM matches m
|
|
LEFT JOIN games g ON g.id = m.game_id
|
|
LEFT JOIN match_players mp ON mp.match_id = m.id
|
|
LEFT JOIN players p ON p.id = mp.player_id
|
|
WHERE m.id = ${id.raw}`;
|
|
|
|
if (
|
|
!(
|
|
Claims.test(Claims.ADMIN, claims) ||
|
|
(Claims.test(Claims.MATCHES.OWNED.READ, claims) && dbResult?.[0]?.owner_id === claims?.userId?.raw) ||
|
|
(Claims.test(Claims.MATCHES.PARTICIPANT.READ, claims) &&
|
|
dbResult?.some((x: any) => x.player_id === claims?.userId?.raw))
|
|
)
|
|
) {
|
|
throw new UnauthorizedError();
|
|
}
|
|
|
|
const matchData = dbResult?.find((x: any) => x.match_id);
|
|
if (!matchData?.match_id) {
|
|
throw new NotFoundError('No matching match exists');
|
|
}
|
|
|
|
return new Match(
|
|
MatchId.fromID(matchData?.match_id),
|
|
GameId.fromID(matchData?.game_id),
|
|
orderBy(
|
|
dbResult
|
|
.filter((x: any) => x.player_id)
|
|
.map(
|
|
(x: any) =>
|
|
new MatchParticipant(
|
|
PlayerId.fromID(x.player_id),
|
|
parseInt(x.standing),
|
|
parseInt(x.elo_change),
|
|
undefined,
|
|
parseInt(x.elo),
|
|
),
|
|
),
|
|
'standing',
|
|
'asc',
|
|
),
|
|
UserId.fromID(dbResult?.[0]?.owner_id),
|
|
);
|
|
}
|
|
|
|
async drop(id: MatchId, claims?: Claims): Promise<void> {
|
|
const match = await this.get(id);
|
|
if (
|
|
!(
|
|
Claims.test(Claims.ADMIN, claims) ||
|
|
(Claims.test(Claims.MATCHES.OWNED.DELETE, claims) && match.owner === claims?.userId)
|
|
)
|
|
) {
|
|
throw new UnauthorizedError();
|
|
}
|
|
|
|
await sql.transaction(async (tx) => {
|
|
await tx`DELETE
|
|
FROM match_players
|
|
WHERE match_id = ${id.raw}`;
|
|
await tx`DELETE
|
|
FROM matches
|
|
WHERE id = ${id.raw}`;
|
|
});
|
|
|
|
return;
|
|
}
|
|
|
|
async removePlayer(matchId: MatchId, participantId: UserId): Promise<void>;
|
|
async removePlayer(matchId: MatchId, participantId: PlayerId): Promise<void> {
|
|
let playerId: PlayerId = participantId;
|
|
if (participantId instanceof UserId) {
|
|
playerId = (await orm.users.get(participantId))?.playerId;
|
|
if (!playerId) {
|
|
throw new BadRequestError('User is not a participant');
|
|
}
|
|
}
|
|
|
|
const player = await orm.players.get(playerId);
|
|
|
|
sql.transaction(async (tx) => {
|
|
const eloRefund = parseInt(
|
|
(
|
|
await tx`SELECT elo_change FROM public.match_players WHERE match_id=${matchId.raw} AND player_id = ${playerId.raw}`
|
|
)?.[0]?.elo_change ?? 0,
|
|
);
|
|
await tx`DELETE FROM match_players WHERE match_id=${matchId.raw} AND player_id=${playerId.raw}`;
|
|
if (!player.isRatingLocked) {
|
|
await tx`UPDATE players SET elo=${player.elo - eloRefund} WHERE id=${playerId.raw}`;
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
}
|