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 { 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 { 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 { 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; async removePlayer(matchId: MatchId, participantId: PlayerId): Promise { 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); await 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; } }