Archive game when client sends win message Fixes #823 (#852)

## Description

Have the Game Server archive when it receives a win event instead of
when the game completes. It will still archive when the game completes
if there are no winners. It only uses the winner message from clients
that are in sync and are not kicked.

* Fixed a bug with the stats collection, the arrays were not expanded
enough causing NaN to be inserted into the array which caused Zod
validation to fail

* Fixed a bug with the win modal, was incorrectly showing player as
winner even if they lost.

## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced
- [x] I understand that submitting code with bugs that could have been
caught through manual testing blocks releases and new features for all
contributors

## Please put your Discord username so you can be contacted if a bug or
regression is found:

<DISCORD USERNAME>
This commit is contained in:
evanpelle
2025-05-23 13:04:51 -07:00
committed by GitHub
parent 90d7625a9f
commit 9563c189eb
4 changed files with 63 additions and 38 deletions
+3 -1
View File
@@ -187,7 +187,9 @@ export class ClientGameRunner {
}
private saveGame(update: WinUpdate) {
if (this.myPlayer === null) throw new Error("Not initialized");
if (this.myPlayer === null) {
return;
}
const players: PlayerRecord[] = [
{
playerID: this.myPlayer.id(),
+5 -2
View File
@@ -207,10 +207,13 @@ export class WinModal extends LitElement implements Layer {
new SendWinnerEvent(winnerClient, wu.allPlayersStats, "player"),
);
}
if (winner === this.game.myPlayer()) {
if (
winnerClient !== null &&
winnerClient === this.game.myPlayer()?.clientID()
) {
this._title = translateText("win_modal.you_won");
} else {
this._title = translateText("win_modal.you_won", {
this._title = translateText("win_modal.other_won", {
player: winner.name(),
});
}
+4 -4
View File
@@ -56,7 +56,7 @@ export class StatsImpl implements Stats {
const p = this._makePlayerStats(player);
if (p === undefined) return;
if (p.attacks === undefined) p.attacks = [0];
while (p.attacks.length < index) p.attacks.push(0);
while (p.attacks.length <= index) p.attacks.push(0);
p.attacks[index] += value;
}
@@ -80,7 +80,7 @@ export class StatsImpl implements Stats {
if (p === undefined) return;
if (p.boats === undefined) p.boats = { [type]: [0] };
if (p.boats[type] === undefined) p.boats[type] = [0];
while (p.boats[type].length < index) p.boats[type].push(0);
while (p.boats[type].length <= index) p.boats[type].push(0);
p.boats[type][index] += value;
}
@@ -95,7 +95,7 @@ export class StatsImpl implements Stats {
if (p === undefined) return;
if (p.bombs === undefined) p.bombs = { [type]: [0] };
if (p.bombs[type] === undefined) p.bombs[type] = [0];
while (p.bombs[type].length < index) p.bombs[type].push(0);
while (p.bombs[type].length <= index) p.bombs[type].push(0);
p.bombs[type][index] += value;
}
@@ -103,7 +103,7 @@ export class StatsImpl implements Stats {
const p = this._makePlayerStats(player);
if (p === undefined) return;
if (p.gold === undefined) p.gold = [0];
while (p.gold.length < index) p.gold.push(0);
while (p.gold.length <= index) p.gold.push(0);
p.gold[index] += value;
}
+51 -31
View File
@@ -56,6 +56,7 @@ export class GameServer {
private _hasPrestarted = false;
private kickedClients: Set<ClientID> = new Set();
private outOfSyncClients: Set<ClientID> = new Set();
constructor(
public readonly id: string,
@@ -200,7 +201,15 @@ export class GameServer {
client.hashes.set(clientMsg.turnNumber, clientMsg.hash);
}
if (clientMsg.type === "winner") {
if (
this.outOfSyncClients.has(client.clientID) ||
this.kickedClients.has(client.clientID) ||
this.winner !== null
) {
return;
}
this.winner = clientMsg;
this.archiveGame();
}
} catch (error) {
this.log.info(
@@ -382,40 +391,16 @@ export class GameServer {
}
this.log.info(`ending game with ${this.turns.length} turns`);
try {
if (this.allClients.size > 0) {
const playerRecords: PlayerRecord[] = Array.from(
this.allClients.values(),
).map((client) => {
const stats = this.winner?.allPlayersStats[client.clientID];
if (stats === undefined) {
this.log.warn(
`Unable to find stats for clientID ${client.clientID}`,
);
}
return {
playerID: client.playerID,
clientID: client.clientID,
username: client.username,
persistentID: client.persistentID,
stats,
} satisfies PlayerRecord;
});
archive(
createGameRecord(
this.id,
this.gameStartInfo.config,
playerRecords,
this.turns,
this._startTime ?? 0,
Date.now(),
this.winner?.winner ?? null,
this.winner?.winnerType ?? null,
),
);
} else {
if (this.allClients.size === 0) {
this.log.info("no clients joined, not archiving game", {
gameID: this.id,
});
} else if (this.winner !== null) {
this.log.info("game already archived", {
gameID: this.id,
});
} else {
this.archiveGame();
}
} catch (error) {
let errorDetails;
@@ -549,6 +534,40 @@ export class GameServer {
}
}
private archiveGame() {
this.log.info("archiving game", {
gameID: this.id,
winner: this.winner?.winner,
});
const playerRecords: PlayerRecord[] = Array.from(
this.allClients.values(),
).map((client) => {
const stats = this.winner?.allPlayersStats[client.clientID];
if (stats === undefined) {
this.log.warn(`Unable to find stats for clientID ${client.clientID}`);
}
return {
playerID: client.playerID,
clientID: client.clientID,
username: client.username,
persistentID: client.persistentID,
stats,
} satisfies PlayerRecord;
});
archive(
createGameRecord(
this.id,
this.gameStartInfo.config,
playerRecords,
this.turns,
this._startTime ?? 0,
Date.now(),
this.winner?.winner ?? null,
this.winner?.winnerType ?? null,
),
);
}
private handleSynchronization() {
if (this.activeClients.length <= 1) {
return;
@@ -586,6 +605,7 @@ export class GameServer {
const desyncMsg = JSON.stringify(serverDesync.data);
for (const c of outOfSyncClients) {
this.outOfSyncClients.add(c.clientID);
if (this.sentDesyncMessageClients.has(c.clientID)) {
continue;
}