Files
Michess/server/src/socket/game.socket.ts
2023-04-09 02:41:46 +08:00

266 lines
9.6 KiB
TypeScript

import GameModel, { activeGames } from "../db/models/game.model.js";
import type { Game } from "@chessu/types";
import type { DisconnectReason, Socket } from "socket.io";
import { Chess } from "chess.js";
import { io } from "../server.js";
export async function joinLobby(this: Socket, gameCode: string) {
const game = activeGames.find((g) => g.code === gameCode);
if (!game) {
console.log(`joinLobby: Game code ${gameCode} not found.`);
return;
}
if (game.host && game.host?.id === this.request.session.user.id) {
game.host.connected = true;
if (game.host.name !== this.request.session.user.name) {
game.host.name = this.request.session.user.name;
}
}
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;
}
} else {
if (game.observers === undefined) game.observers = [];
const user = {
id: this.request.session.user.id,
name: this.request.session.user.name
};
game.observers?.push(user);
}
if (this.rooms.size >= 2) {
await leaveLobby.call(this);
}
if (game.timeout) {
clearTimeout(game.timeout);
game.timeout = undefined;
}
await this.join(gameCode);
io.to(game.code as string).emit("receivedLatestGame", game);
}
export async function leaveLobby(this: Socket, reason?: DisconnectReason, code?: string) {
if (this.rooms.size >= 3 && !code) {
console.log(`leaveLobby: room size is ${this.rooms.size}, aborting...`);
return;
}
const game = activeGames.find(
(g) =>
g.code === (code || this.rooms.size === 2 ? Array.from(this.rooms)[1] : 0) ||
(g.black?.connected && g.black?.id === this.request.session.user.id) ||
(g.white?.connected && g.white?.id === this.request.session.user.id) ||
g.observers?.find((o) => this.request.session.user.id === o.id)
);
if (game) {
const user = game.observers?.find((o) => o.id === this.request.session.user.id);
if (user) {
game.observers?.splice(game.observers?.indexOf(user), 1);
}
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
const sockets = await io.in(game.code as string).fetchSockets();
if (sockets.length <= 0 || (reason === undefined && sockets.length <= 1)) {
if (game.timeout) clearTimeout(game.timeout);
let timeout = 1000 * 60; // 1 minute
if (game.pgn) {
timeout *= 20; // 20 minutes if game has started
}
game.timeout = Number(
setTimeout(() => {
activeGames.splice(activeGames.indexOf(game), 1);
}, timeout)
);
} else {
this.to(game.code as string).emit("receivedLatestGame", game);
}
}
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: Invalid game 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.endReason = "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 { id } = (await GameModel.save(game)) as Game;
game.id = id;
const gameOver = {
reason: game.endReason,
winnerName: this.request.session.user.name,
winnerSide: game.winner === "draw" ? undefined : game.winner,
id
};
io.to(game.code as string).emit("gameOver", gameOver);
if (game.timeout) clearTimeout(game.timeout);
activeGames.splice(activeGames.indexOf(game), 1);
}
export async function getLatestGame(this: Socket) {
const game = activeGames.find((g) => g.code === Array.from(this.rooms)[1]);
if (game) this.emit("receivedLatestGame", game);
}
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 || game.endReason || game.winner) return;
const chess = new Chess();
if (game.pgn) {
chess.loadPgn(game.pgn);
}
try {
const prevTurn = chess.turn();
if (
(prevTurn === "b" && this.request.session.user.id !== game.black?.id) ||
(prevTurn === "w" && this.request.session.user.id !== game.white?.id)
) {
throw new Error("not turn to move");
}
const newMove = chess.move(m);
if (newMove) {
game.pgn = chess.pgn();
this.to(game.code as string).emit("receivedMove", m);
if (chess.isGameOver()) {
let reason: Game["endReason"];
if (chess.isCheckmate()) reason = "checkmate";
else if (chess.isStalemate()) reason = "stalemate";
else if (chess.isThreefoldRepetition()) reason = "repetition";
else if (chess.isInsufficientMaterial()) reason = "insufficient";
else if (chess.isDraw()) reason = "draw";
const winnerSide =
reason === "checkmate" ? (prevTurn === "w" ? "white" : "black") : undefined;
const winnerName =
reason === "checkmate"
? winnerSide === "white"
? game.white?.name
: game.black?.name
: undefined;
if (reason === "checkmate") {
game.winner = winnerSide;
} else {
game.winner = "draw";
}
game.endReason = reason;
const { id } = (await GameModel.save(game)) as Game; // save game to db
game.id = id;
io.to(game.code as string).emit("gameOver", { reason, winnerName, winnerSide, id });
if (game.timeout) clearTimeout(game.timeout);
activeGames.splice(activeGames.indexOf(game), 1);
}
} else {
throw new Error("invalid move");
}
} catch (e) {
console.log("sendMove error: " + e);
this.emit("receivedLatestGame", game);
}
}
export async function joinAsPlayer(this: Socket) {
const game = activeGames.find((g) => g.code === Array.from(this.rooms)[1]);
if (!game) return;
const user = game.observers?.find((o) => o.id === this.request.session.user.id);
if (!game.white) {
const sessionUser = {
id: this.request.session.user.id,
name: this.request.session.user.name,
connected: true
};
game.white = sessionUser;
if (user) game.observers?.splice(game.observers?.indexOf(user), 1);
io.to(game.code as string).emit("userJoinedAsPlayer", {
name: this.request.session.user.name,
side: "white"
});
game.startedAt = Date.now();
} else if (!game.black) {
const sessionUser = {
id: this.request.session.user.id,
name: this.request.session.user.name,
connected: true
};
game.black = sessionUser;
if (user) game.observers?.splice(game.observers?.indexOf(user), 1);
io.to(game.code as string).emit("userJoinedAsPlayer", {
name: this.request.session.user.name,
side: "black"
});
game.startedAt = Date.now();
} else {
console.log("joinAsPlayer: attempted to join a game with already 2 players");
}
io.to(game.code as string).emit("receivedLatestGame", game);
}
export async function chat(this: Socket, message: string) {
this.to(Array.from(this.rooms)[1]).emit("chat", {
author: this.request.session.user,
message
});
}