From 1ce3bc124648c91472567e8782190d7e9be8935b Mon Sep 17 00:00:00 2001 From: Jonathan Loh Date: Sun, 21 Jan 2024 04:08:59 +0800 Subject: [PATCH] Add play against stockfish --- backend/package.json | 2 + backend/src/server.ts | 106 ++++++++++++++----- backend/yarn.lock | 82 ++++++++++++++ frontend/src/Landing.tsx | 15 ++- frontend/src/utils/{socket.tsx => socket.ts} | 0 5 files changed, 179 insertions(+), 26 deletions(-) rename frontend/src/utils/{socket.tsx => socket.ts} (100%) diff --git a/backend/package.json b/backend/package.json index ca32e79..0c67519 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,6 +23,7 @@ "socket.io": "^4.7.4", "socketcluster-client": "^19.1.0", "socketcluster-server": "^19.0.1", + "stockfish": "^10.0.2", "uuid": "^9.0.1" }, "keywords": [ @@ -43,6 +44,7 @@ "chess": "node test/illegalChess.js" }, "devDependencies": { + "@types/express": "^4.17.21", "@types/node": "^20.11.5", "socket.io-client": "^4.7.4", "typescript": "^5.3.3" diff --git a/backend/src/server.ts b/backend/src/server.ts index 77a92d1..b4206c9 100644 --- a/backend/src/server.ts +++ b/backend/src/server.ts @@ -9,6 +9,7 @@ import { Chess } from "chess.js"; import "./utils/globals.js"; import { FailedMove, interpretMove } from "./chess/engine.js"; import { assertUnreachable } from "./utils/assertions.js"; +import stockfish from "stockfish"; dotenv.config(); @@ -17,9 +18,16 @@ globalThis.roomFen = new Map(); const ENVIRONMENT = process.env.ENV || "dev"; const START_FEN = new Chess().fen(); -const chess = new Chess(); + +const engine = stockfish(); +engine.onmessage = function (msg) { + console.log(msg); +}; +engine.postMessage("uci"); let expressApp = express(); +expressApp.use(express.json()); +expressApp.use(express.urlencoded({ extended: true })); if (ENVIRONMENT === "dev") { expressApp.use(morgan("dev")); expressApp.use(cors()); @@ -40,6 +48,28 @@ expressApp.get("/health-check", (req, res) => { 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"); +}); + io.on("connection", (socket) => { console.log(`${socket.id} connected`); @@ -57,17 +87,22 @@ io.on("connection", (socket) => { socket.on("join", (roomId, callback) => { console.log(socket.id, "join:", roomId); - if (!io.sockets.adapter.rooms.has(roomId)) { - callback("denied"); + if (roomId !== "ai" && !io.sockets.adapter.rooms.has(roomId)) { + callback(false); return; } socket.join(roomId); - globalThis.roomFen.set(roomId, START_FEN); - if (io.sockets.adapter.rooms.get(roomId).size === 2) - io.to(roomId).emit("start", START_FEN, socket.id); + 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("answer"); + callback(true); }); socket.on("leave", () => { @@ -78,41 +113,64 @@ io.on("connection", (socket) => { socket.on("move", async (move, callback) => { console.log(socket.id, move); - const allowed = Math.random() < 0.7; //TODO LLM const roomIter = socket.rooms.values(); let roomId = ""; for (let room of roomIter) { if (room !== socket.id) { roomId = room; - break; + 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 (typeof res === "string") { io.to(roomId).emit("update", res, socket.id, move); + globalThis.roomFen.set(roomId, res); callback(res); + + 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); } - - // setTimeout(() => { - // if (allowed) { - // //TODO execute parsed move from LLM - // const availableMoves = chess.moves(); - // chess.move( - // availableMoves[Math.floor(Math.random() * availableMoves.length)] - // ); - // socket.rooms.forEach((roomId) => { - // if (roomId !== socket.id) - // io.to(roomId).emit("update", chess.fen(), socket.id, move); - // }); - // callback("Allowed " + move); - // } else - // }, 1000); }); socket.on("disconnecting", () => { diff --git a/backend/yarn.lock b/backend/yarn.lock index b2c8060..7d8d2f5 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -12,6 +12,21 @@ resolved "https://registry.yarnpkg.com/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz#96116f2a912e0c02817345b3c10751069920d553" integrity sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg== +"@types/body-parser@*": + version "1.19.5" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4" + integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg== + dependencies: + "@types/connect" "*" + "@types/node" "*" + +"@types/connect@*": + version "3.4.38" + resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858" + integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug== + dependencies: + "@types/node" "*" + "@types/cookie@^0.4.1": version "0.4.1" resolved "https://registry.yarnpkg.com/@types/cookie/-/cookie-0.4.1.tgz#bfd02c1f2224567676c1545199f87c3a861d878d" @@ -24,6 +39,41 @@ dependencies: "@types/node" "*" +"@types/express-serve-static-core@^4.17.33": + version "4.17.41" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz#5077defa630c2e8d28aa9ffc2c01c157c305bef6" + integrity sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA== + dependencies: + "@types/node" "*" + "@types/qs" "*" + "@types/range-parser" "*" + "@types/send" "*" + +"@types/express@^4.17.21": + version "4.17.21" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d" + integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ== + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "^4.17.33" + "@types/qs" "*" + "@types/serve-static" "*" + +"@types/http-errors@*": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f" + integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA== + +"@types/mime@*": + version "3.0.4" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45" + integrity sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw== + +"@types/mime@^1": + version "1.3.5" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690" + integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w== + "@types/node@*", "@types/node@>=10.0.0", "@types/node@^20.11.5": version "20.11.5" resolved "https://registry.yarnpkg.com/@types/node/-/node-20.11.5.tgz#be10c622ca7fcaa3cf226cf80166abc31389d86e" @@ -31,6 +81,33 @@ dependencies: undici-types "~5.26.4" +"@types/qs@*": + version "6.9.11" + resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.11.tgz#208d8a30bc507bd82e03ada29e4732ea46a6bbda" + integrity sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ== + +"@types/range-parser@*": + version "1.2.7" + resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb" + integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ== + +"@types/send@*": + version "0.17.4" + resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a" + integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA== + dependencies: + "@types/mime" "^1" + "@types/node" "*" + +"@types/serve-static@*": + version "1.15.5" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.5.tgz#15e67500ec40789a1e8c9defc2d32a896f05b033" + integrity sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ== + dependencies: + "@types/http-errors" "*" + "@types/mime" "*" + "@types/node" "*" + abbrev@1: version "1.1.1" resolved "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz" @@ -1047,6 +1124,11 @@ statuses@2.0.1: resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz" integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ== +stockfish@^10.0.2: + version "10.0.2" + resolved "https://registry.yarnpkg.com/stockfish/-/stockfish-10.0.2.tgz#f4ba642f27e323b3adee9396d0ccfaf32553af18" + integrity sha512-MuDhChwRwFe4nkn5OKuDj2byf4lTVlpJK47D99kNi7Qr1QlM8PIi940AVdF4SMmFQnPtyYeT2xzlX69d5v8WQg== + stream-demux@^10.0.0, stream-demux@^10.0.1: version "10.0.1" resolved "https://registry.npmjs.org/stream-demux/-/stream-demux-10.0.1.tgz" diff --git a/frontend/src/Landing.tsx b/frontend/src/Landing.tsx index 4a99f63..007b049 100644 --- a/frontend/src/Landing.tsx +++ b/frontend/src/Landing.tsx @@ -57,6 +57,17 @@ export default function Landing({ ) : ( <> + + + @@ -93,7 +104,7 @@ export default function Landing({ > ) : ( )} diff --git a/frontend/src/utils/socket.tsx b/frontend/src/utils/socket.ts similarity index 100% rename from frontend/src/utils/socket.tsx rename to frontend/src/utils/socket.ts