diff --git a/backend/src/server.ts b/backend/src/server.ts index b90157a..565bd16 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -2,7 +2,7 @@ import { createServer } from "http"; import express from "express"; import morgan from "morgan"; import cors from "cors"; -import { generateRoomCode } from "./utils/calc.js"; +import { generateRoomCode, processGameover } from "./utils/calc.js"; import dotenv from "dotenv"; import { Server } from "socket.io"; import { Chess } from "chess.js"; @@ -17,11 +17,12 @@ globalThis.roomFen = new Map(); const ENVIRONMENT = process.env.ENV || "dev"; -const START_FEN = new Chess().fen(); +const START_FEN = + "rnbqkbnr/ppppp2p/5p2/6p1/8/4P1P1/PPPP1P1P/RNBQKBNR w KQkq g6 0 3"; //new Chess().fen(); const engine = stockfish(); engine.onmessage = function (msg) { - console.log(msg); + console.log(msg); }; engine.postMessage("uci"); @@ -29,157 +30,171 @@ let expressApp = express(); expressApp.use(express.json()); expressApp.use(express.urlencoded({ extended: true })); if (ENVIRONMENT === "dev") { - expressApp.use(morgan("dev")); - expressApp.use(cors()); + expressApp.use(morgan("dev")); + expressApp.use(cors()); } let httpServer = createServer(expressApp); const io = new Server(httpServer, { - serveClient: false, - cors: - ENVIRONMENT === "dev" - ? { origin: "*", methods: ["GET", "POST"] } - : undefined, + serveClient: false, + cors: + ENVIRONMENT === "dev" + ? { origin: "*", methods: ["GET", "POST"] } + : undefined, }); // Add GET /health-check express route expressApp.get("/health-check", (req, res) => { - res.status(200).send("OK"); + res.status(200).send("OK"); }); expressApp.post("/stockfish", async (req, res) => { - console.log(req.body); - console.log("QUERY", req.body.fen); - // if chess engine replies - engine.onmessage = function (msg) { - console.log(msg); - // in case the response has already been sent? - if (res.headersSent) { - return; - } - // only send response when it is a recommendation - if (typeof (msg == "string") && msg.match("bestmove")) { - res.send(msg.split(" ")[1]); - } - }; - - // run chess engine - engine.postMessage("ucinewgame"); - engine.postMessage("position fen " + req.body.fen); - engine.postMessage("go depth 20"); + console.log("QUERY", req.body.fen); + // if chess engine replies + engine.onmessage = function (msg) { + console.log(msg); + // in case the response has already been sent? + if (res.headersSent) { + return; + } + // only send response when it is a recommendation + if (typeof (msg == "string") && msg.match("bestmove")) { + res.send(msg.split(" ")[1]); + } + }; + + // run chess engine + engine.postMessage("ucinewgame"); + engine.postMessage("position fen " + req.body.fen); + engine.postMessage("go depth 20"); }); io.on("connection", (socket) => { - console.log(`${socket.id} connected`); - - socket.on("ping", (data, ack) => { - const startTime = Date.now(); - ack({ data: "pong", timestamp: data.timestamp }); - }); - - socket.on("create", (callback) => { - const roomId = generateRoomCode(); - console.log(socket.id, "create:", roomId); - socket.join(roomId); - callback(roomId); - }); - - socket.on("join", (roomId, callback) => { - console.log(socket.id, "join:", roomId); - if (roomId !== "ai" && !io.sockets.adapter.rooms.has(roomId)) { - callback(false); - return; - } - socket.join(roomId); - - globalThis.roomFen.set(roomId === "ai" ? socket.id : roomId, START_FEN); - - if (roomId === "ai" || io.sockets.adapter.rooms.get(roomId).size === 2) - io.to(roomId === "ai" ? socket.id : roomId).emit( - "start", - START_FEN, - socket.id - ); - - callback(true); - }); - - socket.on("leave", () => { - socket.rooms.forEach((roomId) => { - if (roomId !== socket.id) socket.leave(roomId); + console.log(`${socket.id} connected`); + + socket.on("ping", (data, ack) => { + const startTime = Date.now(); + ack({ data: "pong", timestamp: data.timestamp }); + }); + + socket.on("create", (callback) => { + const roomId = generateRoomCode(); + console.log(socket.id, "create:", roomId); + socket.join(roomId); + callback(roomId); + }); + + socket.on("join", (roomId, callback) => { + console.log(socket.id, "join:", roomId); + if (roomId !== "ai" && !io.sockets.adapter.rooms.has(roomId)) { + callback(false); + return; + } + socket.join(roomId); + + globalThis.roomFen.set(roomId === "ai" ? socket.id : roomId, START_FEN); + + if (roomId === "ai" || io.sockets.adapter.rooms.get(roomId).size === 2) + io.to(roomId === "ai" ? socket.id : roomId).emit( + "start", + START_FEN, + socket.id + ); + + callback(true); + }); + + socket.on("leave", () => { + socket.rooms.forEach((roomId) => { + if (roomId !== socket.id) socket.leave(roomId); + }); + }); + + socket.on("move", async (move, callback) => { + console.log(socket.id, move); + + const roomIter = socket.rooms.values(); + let roomId = ""; + for (let room of roomIter) { + if (room !== socket.id) { + roomId = room; + if (room === "ai") break; + } + } + + // const allowed = Math.random() < 0.7; + // if (allowed) { + // const chess = new Chess( + // globalThis.roomFen.get(roomId === "ai" ? socket.id : roomId) + // ); + // const availableMoves = chess.moves(); + // chess.move( + // availableMoves[Math.floor(Math.random() * availableMoves.length)] + // ); + // io.to(roomId === "ai" ? socket.id : roomId).emit( + // "update", + // chess.fen(), + // socket.id, + // move + // ); + // globalThis.roomFen.set(roomId === "ai" ? socket.id : roomId, chess.fen()); + // console.log(globalThis.roomFen); + // callback("Allowed " + move); + // } else callback("Denied"); + + const res = await interpretMove(move, globalThis.roomFen.get(roomId)); + if (res instanceof FailedMove) { + callback(res.error); + } else if (res instanceof SuccessfulMove) { + io.to(roomId === "ai" ? socket.id : roomId).emit( + "update", + res, + socket.id, + res.move.toString() + ); + globalThis.roomFen.set(roomId, res.fen); + callback(res.move.toString()); + + processGameover( + new Chess(res.fen), + io, + roomId === "ai" ? socket.id : roomId + ); + + if (roomId === "ai") { + fetch("http://localhost:8080/stockfish", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + fen: globalThis.roomFen.get(socket.id), + }), + }).then((response) => + response.text().then((res) => { + const chess = new Chess( + globalThis.roomFen.get(socket.id) + ); + console.log(globalThis.roomFen.get(socket.id)); + chess.move(res); + io.to(socket.id).emit("update", chess.fen(), "ai", res); + globalThis.roomFen.set(socket.id, chess.fen()); + + processGameover(chess, io, socket.id); + }) + ); + } + } else { + assertUnreachable(res); + } + }); + + socket.on("disconnecting", () => { + console.log(`${socket.id} disconnecting`); + }); + + socket.on("disconnected", () => { + console.log(`${socket.id} disconnected`); }); - }); - - socket.on("move", async (move, callback) => { - console.log(socket.id, move); - - const roomIter = socket.rooms.values(); - let roomId = ""; - for (let room of roomIter) { - if (room !== socket.id) { - roomId = room; - if (room === "ai") break; - } - } - - // const allowed = Math.random() < 0.7; - // if (allowed) { - // const chess = new Chess( - // globalThis.roomFen.get(roomId === "ai" ? socket.id : roomId) - // ); - // const availableMoves = chess.moves(); - // chess.move( - // availableMoves[Math.floor(Math.random() * availableMoves.length)] - // ); - // io.to(roomId === "ai" ? socket.id : roomId).emit( - // "update", - // chess.fen(), - // socket.id, - // move - // ); - // globalThis.roomFen.set(roomId === "ai" ? socket.id : roomId, chess.fen()); - // console.log(globalThis.roomFen); - // callback("Allowed " + move); - // } else callback("Denied"); - - const res = await interpretMove(move, globalThis.roomFen.get(roomId)); - if (res instanceof FailedMove) { - callback(res.error); - } else if (res instanceof SuccessfulMove) { - io.to(roomId).emit("update", res.fen, socket.id, move); - globalThis.roomFen.set(roomId, res.fen); - callback(res.move.toString()); - - if (roomId === "ai") { - fetch("http://localhost:8080/stockfish", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ - fen: globalThis.roomFen.get(socket.id), - }), - }).then((response) => - response.text().then((res) => { - const chess = new Chess(globalThis.roomFen.get(socket.id)); - console.log(globalThis.roomFen.get(socket.id)); - chess.move(res); - io.to(socket.id).emit("update", chess.fen(), "ai", res); - globalThis.roomFen.set(socket.id, chess.fen()); - }) - ); - } - } else { - assertUnreachable(res); - } - }); - - socket.on("disconnecting", () => { - console.log(`${socket.id} disconnecting`); - }); - - socket.on("disconnected", () => { - console.log(`${socket.id} disconnected`); - }); }); httpServer.listen(8080); diff --git a/backend/src/utils/calc.ts b/backend/src/utils/calc.ts index 80abde6..68c09a5 100644 --- a/backend/src/utils/calc.ts +++ b/backend/src/utils/calc.ts @@ -1,3 +1,6 @@ +import { Chess } from "chess.js"; +import { Server } from "socket.io"; + // Generate a random 6-letter room code export function generateRoomCode() { const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; @@ -9,3 +12,13 @@ export function generateRoomCode() { } return roomCode; } + +export function processGameover(chess: Chess, io: Server, roomId: string) { + if (chess.isGameOver()) io.to(roomId).emit("gameover", getWinner(chess)); +} + +function getWinner(chess: Chess) { + if (chess.isCheckmate()) + return chess.turn() === "b" ? "White wins!" : "Black wins!"; + else return "Draw!"; +} diff --git a/frontend/src/Game.tsx b/frontend/src/Game.tsx index 1e67ab8..5279d77 100644 --- a/frontend/src/Game.tsx +++ b/frontend/src/Game.tsx @@ -1,16 +1,9 @@ -import { - Button, - Container, - Grid, - Icon, - Input, - Loader, -} from "semantic-ui-react"; +import { Button, Container, Grid, Icon, Input } from "semantic-ui-react"; import { Chessboard } from "react-chessboard"; import "./Game.css"; import { Message } from "./types/message"; import { useCallback, useEffect, useRef, useState } from "react"; -import { move, onUpdate } from "./utils/socket"; +import { move, onGameover, onUpdate } from "./utils/socket"; export default function Game({ fen, @@ -25,6 +18,7 @@ export default function Game({ const [text, setText] = useState(""); const [messages, setMessages] = useState([]); const [isLoading, setLoading] = useState(false); + const [gameover, setGameover] = useState(false); // const messages: Message[] = [ // { outgoing: true, text: "Knight to E5" }, @@ -51,6 +45,10 @@ export default function Game({ msgs.concat({ outgoing: false, text: "Your turn!" }) ); }); + onGameover((text) => { + setMessages((msgs) => msgs.concat({ outgoing: false, text })); + setGameover(true); + }); }, []); const sendMessage = useCallback(() => { @@ -127,7 +125,7 @@ export default function Game({ ? "Enter move" : "Waiting for opponent..." } - disabled={!isTurn || isLoading} + disabled={!isTurn || isLoading || gameover} /> diff --git a/frontend/src/utils/socket.ts b/frontend/src/utils/socket.ts index 84c67d6..d5270ac 100644 --- a/frontend/src/utils/socket.ts +++ b/frontend/src/utils/socket.ts @@ -41,3 +41,7 @@ export function onUpdate( callback(fen, lastMovedUser !== socket.id, lastMove); }); } + +export function onGameover(callback: (message: string) => void) { + socket.on("gameover", callback); +}