mirror of
https://github.com/openfrontio/OpenFrontIO.git
synced 2026-06-22 01:57:44 +00:00
02e35c3fca
## Description: Have at least 2 clients and majority vote to decide a winner ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced --------- Co-authored-by: Scott Anderson <662325+scottanderson@users.noreply.github.com>
124 lines
3.2 KiB
TypeScript
124 lines
3.2 KiB
TypeScript
import { Logger } from "winston";
|
|
import { z } from "zod";
|
|
import {
|
|
ClientMessageSchema,
|
|
ClientSendWinnerMessage,
|
|
ServerErrorMessage,
|
|
} from "../../../../../core/Schemas";
|
|
import { Client } from "../../../../Client";
|
|
import { GameServer } from "../../../../GameServer";
|
|
|
|
export async function postJoinMessageHandler(
|
|
gs: GameServer,
|
|
log: Logger,
|
|
client: Client,
|
|
message: string,
|
|
) {
|
|
try {
|
|
const parsed = ClientMessageSchema.safeParse(JSON.parse(message));
|
|
if (!parsed.success) {
|
|
const error = z.prettifyError(parsed.error);
|
|
log.error("Failed to parse client message", error, {
|
|
clientID: client.clientID,
|
|
});
|
|
client.ws.send(
|
|
JSON.stringify({
|
|
error,
|
|
message,
|
|
type: "error",
|
|
} satisfies ServerErrorMessage),
|
|
);
|
|
client.ws.close(1002, "ClientMessageSchema");
|
|
return;
|
|
}
|
|
const clientMsg = parsed.data;
|
|
switch (clientMsg.type) {
|
|
case "intent": {
|
|
if (clientMsg.intent.clientID !== client.clientID) {
|
|
log.warn(
|
|
`client id mismatch, client: ${client.clientID}, intent: ${clientMsg.intent.clientID}`,
|
|
);
|
|
return;
|
|
}
|
|
if (clientMsg.intent.type === "mark_disconnected") {
|
|
log.warn(
|
|
`Should not receive mark_disconnected intent from client`,
|
|
);
|
|
return;
|
|
}
|
|
gs.addIntent(clientMsg.intent);
|
|
break;
|
|
}
|
|
case "ping": {
|
|
gs.lastPingUpdate = Date.now();
|
|
client.lastPing = Date.now();
|
|
break;
|
|
}
|
|
case "hash": {
|
|
client.hashes.set(clientMsg.turnNumber, clientMsg.hash);
|
|
break;
|
|
}
|
|
case "winner": {
|
|
handleWinner(gs, log, client, clientMsg);
|
|
break;
|
|
}
|
|
default: {
|
|
log.warn(`Unknown message type: ${(clientMsg as any).type}`, {
|
|
clientID: client.clientID,
|
|
});
|
|
break;
|
|
}
|
|
}
|
|
} catch (error) {
|
|
log.info(`error handline websocket request in game server: ${error}`, {
|
|
clientID: client.clientID,
|
|
});
|
|
}
|
|
}
|
|
|
|
function handleWinner(
|
|
gs: GameServer,
|
|
log: Logger,
|
|
client: Client, clientMsg: ClientSendWinnerMessage) {
|
|
if (
|
|
gs.outOfSyncClients.has(client.clientID) ||
|
|
gs.kickedClients.has(client.clientID) ||
|
|
gs.winner !== null ||
|
|
client.reportedWinner !== null
|
|
) {
|
|
return;
|
|
}
|
|
client.reportedWinner = clientMsg.winner;
|
|
|
|
// Add client vote
|
|
const winnerKey = JSON.stringify(clientMsg.winner);
|
|
if (!gs.winnerVotes.has(winnerKey)) {
|
|
gs.winnerVotes.set(winnerKey, { ips: new Set(), winner: clientMsg });
|
|
}
|
|
const potentialWinner = gs.winnerVotes.get(winnerKey)!;
|
|
potentialWinner.ips.add(client.ip);
|
|
|
|
const activeUniqueIPs = new Set(gs.activeClients.map((c) => c.ip));
|
|
|
|
// Require at least two unique IPs to agree
|
|
if (activeUniqueIPs.size < 2) {
|
|
return;
|
|
}
|
|
|
|
// Check if winner has majority
|
|
if (potentialWinner.ips.size * 2 < activeUniqueIPs.size) {
|
|
return;
|
|
}
|
|
|
|
// Vote succeeded
|
|
gs.winner = potentialWinner.winner;
|
|
log.info(
|
|
`Winner determined by ${potentialWinner.ips.size}/${activeUniqueIPs.size} active IPs`,
|
|
{
|
|
gameID: gs.id,
|
|
winnerKey: winnerKey,
|
|
},
|
|
);
|
|
gs.archiveGame();
|
|
}
|