From d469cdff564c43617ff71ed55cf0e646ce75c2f6 Mon Sep 17 00:00:00 2001 From: Nathaniel Tampus Date: Sat, 25 Mar 2023 19:18:01 +0800 Subject: [PATCH] allow player to claim win/draw for abandoned games --- client/src/components/game/GamePage.tsx | 100 +++++++++++++++++++-- client/src/components/game/socketEvents.ts | 14 ++- server/src/socket/game.socket.ts | 59 +++++++++++- server/src/socket/index.ts | 4 +- 4 files changed, 166 insertions(+), 11 deletions(-) diff --git a/client/src/components/game/GamePage.tsx b/client/src/components/game/GamePage.tsx index 31b41e0..9352c30 100644 --- a/client/src/components/game/GamePage.tsx +++ b/client/src/components/game/GamePage.tsx @@ -65,6 +65,36 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { const chatListRef = useRef(null); const moveListRef = useRef(null); + const [abandonSeconds, setAbandonSeconds] = useState(60); + useEffect(() => { + if ( + lobby.side === "s" || + lobby.reason || + lobby.winner || + !lobby.pgn || + !lobby.white || + !lobby.black || + (lobby.white.id !== session?.user?.id && lobby.black.id !== session?.user?.id) + ) + return; + + let interval: number; + if (!lobby.white?.connected || !lobby.black?.connected) { + setAbandonSeconds(60); + interval = Number( + setInterval(() => { + if (abandonSeconds === 0 || (lobby.white?.connected && lobby.black?.connected)) { + clearInterval(interval); + return; + } + setAbandonSeconds((s) => s - 1); + }, 1000) + ); + } + return () => clearInterval(interval); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [lobby.black, lobby.white, lobby.black?.disconnectedOn, lobby.white?.disconnectedOn]); + useEffect(() => { if (!session?.user || !session.user?.id) return; socket.connect(); @@ -216,11 +246,11 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { } function isDraggablePiece({ piece }: { piece: string }) { - return piece.startsWith(lobby.side); + return piece.startsWith(lobby.side) && !lobby.reason && !lobby.winner; } function onDrop(sourceSquare: Square, targetSquare: Square) { - if (lobby.side === "s" || navFen) return false; + if (lobby.side === "s" || navFen || lobby.reason || lobby.winner) return false; // premove if (lobby.side !== lobby.actualGame.turn()) return true; @@ -267,7 +297,7 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { } function onPieceDragBegin(_piece: string, sourceSquare: Square) { - if (lobby.side !== lobby.actualGame.turn() || navFen) return; + if (lobby.side !== lobby.actualGame.turn() || navFen || lobby.reason || lobby.winner) return; getMoveOptions(sourceSquare); } @@ -278,7 +308,7 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { function onSquareClick(square: Square) { updateCustomSquares({ rightClicked: {} }); - if (lobby.side !== lobby.actualGame.turn() || navFen) return; + if (lobby.side !== lobby.actualGame.turn() || navFen || lobby.reason || lobby.winner) return; function resetFirstMove(square: Square) { setMoveFrom(square); @@ -461,6 +491,20 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) { }; } + function claimAbandoned(type: "win" | "draw") { + if ( + lobby.side === "s" || + lobby.reason || + lobby.winner || + !lobby.pgn || + abandonSeconds > 0 || + (lobby.black?.connected && lobby.white?.connected) + ) { + return; + } + socket.emit("claimAbandoned", type); + } + return (
@@ -577,7 +621,53 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
-
+
+ {(lobby.reason || + (lobby.pgn && + lobby.white && + session?.user?.id === lobby.white?.id && + lobby.black && + !lobby.black?.connected) || + (lobby.pgn && + lobby.black && + session?.user?.id === lobby.black?.id && + lobby.white && + !lobby.white?.connected)) && ( +
+ {lobby.reason ? ( + lobby.reason === "abandoned" ? ( + lobby.winner === "draw" ? ( + "The game ended in a draw due to abandonment." + ) : ( + `The game was won by ${lobby.winner} due to abandonment.` + ) + ) : lobby.winner === "draw" ? ( + "The game ended in a draw." + ) : ( + `The game was won by checkmate (${lobby.winner}).` + ) + ) : abandonSeconds > 0 ? ( + `Your opponent has disconnected. You can claim the win or draw in ${abandonSeconds} second${ + abandonSeconds > 1 ? "s" : "" + }.` + ) : ( +
+ Your opponent has disconnected. +
+ + +
+
+ )} +
+ )}
    { const m = { author: { name: "server" } } as Message; - if (reason === "checkmate") { + if (reason === "abandoned") { + if (!winnerSide) { + m.message = `${winnerName} has claimed a draw due to abandonment.`; + } else { + m.message = `${winnerName} (${winnerSide}) has claimed the win due to abandonment.`; + } + } else if (reason === "checkmate") { m.message = `${winnerName} (${winnerSide}) has won by checkmate.`; } else { let message = "The game has ended in a draw"; @@ -81,6 +87,10 @@ export function initSocket( } m.message = message.concat("."); } + actions.updateLobby({ + type: "updateLobby", + payload: { reason, winner: winnerSide || "draw" } + }); actions.addMessage(m); } ); diff --git a/server/src/socket/game.socket.ts b/server/src/socket/game.socket.ts index b3eed82..6dfe59d 100644 --- a/server/src/socket/game.socket.ts +++ b/server/src/socket/game.socket.ts @@ -18,11 +18,13 @@ export async function joinLobby(this: Socket, gameCode: string) { } if (game.white && game.white?.id === this.request.session.user.id) { game.white.connected = true; + game.white.disconnectedOn = undefined; if (game.white.name !== this.request.session.user.name) { game.white.name = this.request.session.user.name; } } else if (game.black && game.black?.id === this.request.session.user.id) { game.black.connected = true; + game.black.disconnectedOn = undefined; if (game.black.name !== this.request.session.user.name) { game.black.name = this.request.session.user.name; } @@ -50,7 +52,7 @@ export async function joinLobby(this: Socket, gameCode: string) { export async function leaveLobby(this: Socket, reason?: DisconnectReason, code?: string) { if (this.rooms.size >= 3 && !code) { - console.log(`[WARNING] leaveLobby: room size is ${this.rooms.size}, aborting...`); + console.log(`leaveLobby: room size is ${this.rooms.size}, aborting...`); return; } const game = activeGames.find( @@ -68,8 +70,10 @@ export async function leaveLobby(this: Socket, reason?: DisconnectReason, code?: } if (game.black && game.black?.id === this.request.session.user.id) { game.black.connected = false; + game.black.disconnectedOn = Date.now(); } else if (game.white && game.white?.id === this.request.session.user.id) { game.white.connected = false; + game.white.disconnectedOn = Date.now(); } // count sockets @@ -94,6 +98,54 @@ export async function leaveLobby(this: Socket, reason?: DisconnectReason, code?: await this.leave(code || Array.from(this.rooms)[1]); } +export async function claimAbandoned(this: Socket, type: "win" | "draw") { + const game = activeGames.find((g) => g.code === Array.from(this.rooms)[1]); + if ( + !game || + !game.pgn || + !game.white || + !game.black || + (game.white.id !== this.request.session.user.id && + game.black.id !== this.request.session.user.id) + ) { + console.log(`claimAbandoned: Game not found or user is not a player.`); + return; + } + + if ( + (game.white && + game.white.id === this.request.session.user.id && + (game.black?.connected || + Date.now() - (game.black?.disconnectedOn as number) < 50000)) || + (game.black && + game.black.id === this.request.session.user.id && + (game.white?.connected || Date.now() - (game.white?.disconnectedOn as number) < 50000)) + ) { + console.log( + `claimAbandoned: Invalid claim by ${this.request.session.user.name}. Opponent is still connected or disconnected less than 50 seconds ago.` + ); + return; + } + + game.reason = "abandoned"; + + if (type === "draw") { + game.winner = "draw"; + } else if (game.white && game.white?.id === this.request.session.user.id) { + game.winner = "white"; + } else if (game.black && game.black?.id === this.request.session.user.id) { + game.winner = "black"; + } + + const gameOver = { + reason: game.reason, + winnerName: this.request.session.user.name, + winnerSide: game.winner === "draw" ? undefined : game.winner + }; + + io.to(game.code as string).emit("gameOver", gameOver); +} + export async function getLatestGame(this: Socket) { const game = activeGames.find((g) => g.code === Array.from(this.rooms)[1]); if (game) this.emit("receivedLatestGame", game); @@ -101,7 +153,7 @@ export async function getLatestGame(this: Socket) { export async function sendMove(this: Socket, m: { from: string; to: string; promotion?: string }) { const game = activeGames.find((g) => g.code === Array.from(this.rooms)[1]); - if (!game) return; + if (!game || game.reason || game.winner) return; const chess = new Chess(); if (game.pgn) { chess.loadPgn(game.pgn); @@ -139,6 +191,7 @@ export async function sendMove(this: Socket, m: { from: string; to: string; prom } else { game.winner = "draw"; } + game.reason = reason; io.to(game.code as string).emit("gameOver", { reason, winnerName, winnerSide }); } if (newMove) { @@ -182,7 +235,7 @@ export async function joinAsPlayer(this: Socket) { side: "black" }); } else { - console.log("[WARNING] attempted to join a game with already 2 players"); + console.log("joinAsPlayer: attempted to join a game with already 2 players"); } io.to(game.code as string).emit("receivedLatestGame", game); } diff --git a/server/src/socket/index.ts b/server/src/socket/index.ts index 7c0c7ca..d14d032 100644 --- a/server/src/socket/index.ts +++ b/server/src/socket/index.ts @@ -6,7 +6,8 @@ import { getLatestGame, sendMove, joinAsPlayer, - chat + chat, + claimAbandoned } from "./game.socket.js"; const socketConnect = (socket: Socket) => { @@ -34,6 +35,7 @@ const socketConnect = (socket: Socket) => { socket.on("sendMove", sendMove); socket.on("joinAsPlayer", joinAsPlayer); socket.on("chat", chat); + socket.on("claimAbandoned", claimAbandoned); }; export const init = () => {