Fixed some behaviour in Elo calculation. Began implement match logic.

This commit is contained in:
jd
2026-02-22 02:07:50 +00:00
parent 564ffe7c8c
commit 0fa00e6759
10 changed files with 284 additions and 36 deletions

179
src/orm/matches.ts Normal file
View File

@@ -0,0 +1,179 @@
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';
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();
}
if (!dbResult) {
throw new NotFoundError('No matching user exists');
}
return new Match(
MatchId.fromID(dbResult?.[0]?.match_id),
GameId.fromID(dbResult?.[0]?.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: UserId, claims?: Claims): Promise<void> {
// if (
// !(
// Claims.test(Claims.ADMIN, claims) ||
// Claims.test(Claims.USERS.OTHER.DELETE, claims) ||
// (Claims.test(Claims.USERS.SELF.DELETE, claims) && id === claims?.userId)
// )
// ) {
// throw new UnauthorizedError();
// }
//
// // Ensure user exists before attempting to delete
// await this.get(id);
// await sql.transaction(async (tx) => {
// await tx`DELETE
// FROM user_claims
// WHERE user_id = ${id.raw}`;
// await tx`DELETE
// FROM users
// WHERE id = ${id.raw}`;
// });
return;
}
}