allow player to claim win/draw for abandoned games
This commit is contained in:
@@ -65,6 +65,36 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
const chatListRef = useRef<HTMLUListElement>(null);
|
const chatListRef = useRef<HTMLUListElement>(null);
|
||||||
const moveListRef = useRef<HTMLDivElement>(null);
|
const moveListRef = useRef<HTMLDivElement>(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(() => {
|
useEffect(() => {
|
||||||
if (!session?.user || !session.user?.id) return;
|
if (!session?.user || !session.user?.id) return;
|
||||||
socket.connect();
|
socket.connect();
|
||||||
@@ -216,11 +246,11 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isDraggablePiece({ piece }: { piece: string }) {
|
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) {
|
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
|
// premove
|
||||||
if (lobby.side !== lobby.actualGame.turn()) return true;
|
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) {
|
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);
|
getMoveOptions(sourceSquare);
|
||||||
}
|
}
|
||||||
@@ -278,7 +308,7 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
|
|
||||||
function onSquareClick(square: Square) {
|
function onSquareClick(square: Square) {
|
||||||
updateCustomSquares({ rightClicked: {} });
|
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) {
|
function resetFirstMove(square: Square) {
|
||||||
setMoveFrom(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 (
|
return (
|
||||||
<div className="flex w-full flex-wrap justify-center gap-6 px-4 py-4 lg:gap-10 2xl:gap-16">
|
<div className="flex w-full flex-wrap justify-center gap-6 px-4 py-4 lg:gap-10 2xl:gap-16">
|
||||||
<div className="relative h-min">
|
<div className="relative h-min">
|
||||||
@@ -577,7 +621,53 @@ export default function GamePage({ initialLobby }: { initialLobby: Game }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="h-60 w-full min-w-fit">
|
<div className="relative h-60 w-full min-w-fit">
|
||||||
|
{(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)) && (
|
||||||
|
<div className="bg-neutral absolute w-full rounded-t-lg bg-opacity-95 p-2">
|
||||||
|
{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" : ""
|
||||||
|
}.`
|
||||||
|
) : (
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<span>Your opponent has disconnected.</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={() => claimAbandoned("win")}
|
||||||
|
className="btn btn-sm btn-primary"
|
||||||
|
>
|
||||||
|
Claim win
|
||||||
|
</button>
|
||||||
|
<button onClick={() => claimAbandoned("draw")} className="btn btn-sm btn-ghost">
|
||||||
|
Draw
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div className="bg-base-300 flex h-full w-full min-w-[64px] flex-col rounded-lg p-4 shadow-sm">
|
<div className="bg-base-300 flex h-full w-full min-w-[64px] flex-col rounded-lg p-4 shadow-sm">
|
||||||
<ul
|
<ul
|
||||||
className="mb-4 flex h-full flex-col gap-1 overflow-y-scroll break-words"
|
className="mb-4 flex h-full flex-col gap-1 overflow-y-scroll break-words"
|
||||||
|
|||||||
@@ -62,13 +62,19 @@ export function initSocket(
|
|||||||
}: {
|
}: {
|
||||||
reason: string;
|
reason: string;
|
||||||
winnerName?: string;
|
winnerName?: string;
|
||||||
winnerSide?: string;
|
winnerSide?: "white" | "black" | "draw";
|
||||||
}) => {
|
}) => {
|
||||||
const m = {
|
const m = {
|
||||||
author: { name: "server" }
|
author: { name: "server" }
|
||||||
} as Message;
|
} 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.`;
|
m.message = `${winnerName} (${winnerSide}) has won by checkmate.`;
|
||||||
} else {
|
} else {
|
||||||
let message = "The game has ended in a draw";
|
let message = "The game has ended in a draw";
|
||||||
@@ -81,6 +87,10 @@ export function initSocket(
|
|||||||
}
|
}
|
||||||
m.message = message.concat(".");
|
m.message = message.concat(".");
|
||||||
}
|
}
|
||||||
|
actions.updateLobby({
|
||||||
|
type: "updateLobby",
|
||||||
|
payload: { reason, winner: winnerSide || "draw" }
|
||||||
|
});
|
||||||
actions.addMessage(m);
|
actions.addMessage(m);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,11 +18,13 @@ export async function joinLobby(this: Socket, gameCode: string) {
|
|||||||
}
|
}
|
||||||
if (game.white && game.white?.id === this.request.session.user.id) {
|
if (game.white && game.white?.id === this.request.session.user.id) {
|
||||||
game.white.connected = true;
|
game.white.connected = true;
|
||||||
|
game.white.disconnectedOn = undefined;
|
||||||
if (game.white.name !== this.request.session.user.name) {
|
if (game.white.name !== this.request.session.user.name) {
|
||||||
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) {
|
} else if (game.black && game.black?.id === this.request.session.user.id) {
|
||||||
game.black.connected = true;
|
game.black.connected = true;
|
||||||
|
game.black.disconnectedOn = undefined;
|
||||||
if (game.black.name !== this.request.session.user.name) {
|
if (game.black.name !== this.request.session.user.name) {
|
||||||
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) {
|
export async function leaveLobby(this: Socket, reason?: DisconnectReason, code?: string) {
|
||||||
if (this.rooms.size >= 3 && !code) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
const game = activeGames.find(
|
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) {
|
if (game.black && game.black?.id === this.request.session.user.id) {
|
||||||
game.black.connected = false;
|
game.black.connected = false;
|
||||||
|
game.black.disconnectedOn = Date.now();
|
||||||
} else if (game.white && game.white?.id === this.request.session.user.id) {
|
} else if (game.white && game.white?.id === this.request.session.user.id) {
|
||||||
game.white.connected = false;
|
game.white.connected = false;
|
||||||
|
game.white.disconnectedOn = Date.now();
|
||||||
}
|
}
|
||||||
|
|
||||||
// count sockets
|
// count sockets
|
||||||
@@ -94,6 +98,54 @@ export async function leaveLobby(this: Socket, reason?: DisconnectReason, code?:
|
|||||||
await this.leave(code || Array.from(this.rooms)[1]);
|
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) {
|
export async function getLatestGame(this: Socket) {
|
||||||
const game = activeGames.find((g) => g.code === Array.from(this.rooms)[1]);
|
const game = activeGames.find((g) => g.code === Array.from(this.rooms)[1]);
|
||||||
if (game) this.emit("receivedLatestGame", game);
|
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 }) {
|
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]);
|
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();
|
const chess = new Chess();
|
||||||
if (game.pgn) {
|
if (game.pgn) {
|
||||||
chess.loadPgn(game.pgn);
|
chess.loadPgn(game.pgn);
|
||||||
@@ -139,6 +191,7 @@ export async function sendMove(this: Socket, m: { from: string; to: string; prom
|
|||||||
} else {
|
} else {
|
||||||
game.winner = "draw";
|
game.winner = "draw";
|
||||||
}
|
}
|
||||||
|
game.reason = reason;
|
||||||
io.to(game.code as string).emit("gameOver", { reason, winnerName, winnerSide });
|
io.to(game.code as string).emit("gameOver", { reason, winnerName, winnerSide });
|
||||||
}
|
}
|
||||||
if (newMove) {
|
if (newMove) {
|
||||||
@@ -182,7 +235,7 @@ export async function joinAsPlayer(this: Socket) {
|
|||||||
side: "black"
|
side: "black"
|
||||||
});
|
});
|
||||||
} else {
|
} 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);
|
io.to(game.code as string).emit("receivedLatestGame", game);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import {
|
|||||||
getLatestGame,
|
getLatestGame,
|
||||||
sendMove,
|
sendMove,
|
||||||
joinAsPlayer,
|
joinAsPlayer,
|
||||||
chat
|
chat,
|
||||||
|
claimAbandoned
|
||||||
} from "./game.socket.js";
|
} from "./game.socket.js";
|
||||||
|
|
||||||
const socketConnect = (socket: Socket) => {
|
const socketConnect = (socket: Socket) => {
|
||||||
@@ -34,6 +35,7 @@ const socketConnect = (socket: Socket) => {
|
|||||||
socket.on("sendMove", sendMove);
|
socket.on("sendMove", sendMove);
|
||||||
socket.on("joinAsPlayer", joinAsPlayer);
|
socket.on("joinAsPlayer", joinAsPlayer);
|
||||||
socket.on("chat", chat);
|
socket.on("chat", chat);
|
||||||
|
socket.on("claimAbandoned", claimAbandoned);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const init = () => {
|
export const init = () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user