diff --git a/client/index.html b/client/index.html index bc91636..fcfae82 100644 --- a/client/index.html +++ b/client/index.html @@ -2,7 +2,7 @@ - + Chess Game diff --git a/client/package.json b/client/package.json index f74e718..73b9a48 100644 --- a/client/package.json +++ b/client/package.json @@ -14,6 +14,7 @@ "react": "^18.2.0", "react-dom": "^18.2.0", "react-router-dom": "^6.14.2", + "socket.io-client": "^4.7.1", "sort-by": "^1.2.0" }, "devDependencies": { diff --git a/client/pnpm-lock.yaml b/client/pnpm-lock.yaml index b5f473b..dadc7cd 100644 --- a/client/pnpm-lock.yaml +++ b/client/pnpm-lock.yaml @@ -20,6 +20,9 @@ dependencies: react-router-dom: specifier: ^6.14.2 version: 6.14.2(react-dom@18.2.0)(react@18.2.0) + socket.io-client: + specifier: ^4.7.1 + version: 4.7.1 sort-by: specifier: ^1.2.0 version: 1.2.0 @@ -322,6 +325,10 @@ packages: engines: {node: '>=14'} dev: false + /@socket.io/component-emitter@3.1.0: + resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + dev: false + /@swc/core-darwin-arm64@1.3.44: resolution: {integrity: sha512-Y+oVsCjXUPvr3D9YLuB1gjP84TseM/CRkbPNrf+3JXQhsPEkgxdIdFP1cl/obeqMQrRgPpvSfK+TOvGuOuV22g==} engines: {node: '>=10'} @@ -569,6 +576,18 @@ packages: resolution: {integrity: sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==} dev: true + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: false + /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: true @@ -581,6 +600,25 @@ packages: resolution: {integrity: sha512-8rY8HdCxuSVY8wku3i/eDac4g1b4cSbruzocenrqBlzqruAZYHjQCHIjC66dLR9DXhEHTojsC4EjhZ8KmzwXqA==} dev: true + /engine.io-client@6.5.1: + resolution: {integrity: sha512-hE5wKXH8Ru4L19MbM1GgYV/2Qo54JSMh1rlJbfpa40bEWkCKNo3ol2eOtGmowcr+ysgbI7+SGL+by42Q3pt/Ng==} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + engine.io-parser: 5.1.0 + ws: 8.11.0 + xmlhttprequest-ssl: 2.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /engine.io-parser@5.1.0: + resolution: {integrity: sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==} + engines: {node: '>=10.0.0'} + dev: false + /esbuild@0.17.14: resolution: {integrity: sha512-vOO5XhmVj/1XQR9NQ1UPq6qvMYL7QFJU57J5fKBKBKxp17uDt5PgxFDb4A2nEiXhr1qQs4x0F5+66hVVw4ruNw==} engines: {node: '>=12'} @@ -800,6 +838,10 @@ packages: brace-expansion: 1.1.11 dev: true + /ms@2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: false + /mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} dependencies: @@ -1055,6 +1097,30 @@ packages: loose-envify: 1.4.0 dev: false + /socket.io-client@4.7.1: + resolution: {integrity: sha512-Qk3Xj8ekbnzKu3faejo4wk2MzXA029XppiXtTF/PkbTg+fcwaTw1PlDrTrrrU4mKoYC4dvlApOnSeyLCKwek2w==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + engine.io-client: 6.5.1 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /sort-by@1.2.0: resolution: {integrity: sha512-aRyW65r3xMnf4nxJRluCg0H/woJpksU1dQxRtXYzau30sNBOmf5HACpDd9MZDhKh7ALQ5FgSOfMPwZEtUmMqcg==} dependencies: @@ -1199,6 +1265,24 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true + /ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + + /xmlhttprequest-ssl@2.0.0: + resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} + engines: {node: '>=0.4.0'} + dev: false + /yaml@2.3.1: resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} engines: {node: '>= 14'} diff --git a/client/public/knight.svg b/client/public/knight.svg new file mode 100644 index 0000000..95bb641 --- /dev/null +++ b/client/public/knight.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/client/public/vite.svg b/client/public/vite.svg deleted file mode 100644 index e7b8dfb..0000000 --- a/client/public/vite.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/components/Chessboard.tsx b/client/src/components/Chessboard.tsx index 2d5d858..2e1f4ec 100644 --- a/client/src/components/Chessboard.tsx +++ b/client/src/components/Chessboard.tsx @@ -27,7 +27,7 @@ import Chess, { MOVE_FLAGS, PIECE_PROMOTION, PiecePromotionType, -} from "../../../server/src/chess/engine"; +} from "../../../server/src/engine"; import { MouseEventHandler, useState } from "react"; import Show from "../utils/Show"; import useWindowSize from "../utils/useWindowSize"; @@ -38,10 +38,9 @@ const PIECES: Record> = { }; const ChessBoard: React.FC<{ chess: Chess; - sendMove: (move: Move) => void; - undo: () => void; + makeMove: (move: Move) => void; blackPerspective?: boolean; -}> = ({ chess, sendMove, undo, blackPerspective }) => { +}> = ({ chess, makeMove, blackPerspective }) => { const { width, height } = useWindowSize(); const [activeTile, setActiveTile] = useState(-1); const [promotionMove, setPromotionMove] = useState< @@ -90,7 +89,7 @@ const ChessBoard: React.FC<{ if (tileProps[tile].isPromotion) return setPromotionMove(moveObj); - sendMove(moveObj); + makeMove(moveObj); setActiveTile(-1); } @@ -109,22 +108,16 @@ const ChessBoard: React.FC<{ promotion, }; - sendMove(moveObj); + makeMove(moveObj); setPromotionMove(null); setActiveTile(-1); }; - const handleUndo = () => { - setPromotionMove(null); - setActiveTile(-1); - undo(); - }; - return ( <>
@@ -148,7 +141,6 @@ const ChessBoard: React.FC<{ )}
- ); }; diff --git a/client/src/index.css b/client/src/index.css index 5448da9..88a948a 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -6,3 +6,27 @@ img { max-width: 100%; max-height: 100%; } + +.loading:after { + display: inline-block; + animation: dotty steps(1, end) 2s infinite; + content: ""; +} + +@keyframes dotty { + 0% { + content: ""; + } + 25% { + content: "."; + } + 50% { + content: ".."; + } + 75% { + content: "..."; + } + 100% { + content: ""; + } +} diff --git a/client/src/main.tsx b/client/src/main.tsx index 5d6137b..9a7a398 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -1,16 +1,21 @@ -import React from "react"; import ReactDOM from "react-dom/client"; import "./index.css"; import { createBrowserRouter, RouterProvider } from "react-router-dom"; +import Home from "./routes/Home"; import Game from "./routes/Game"; -const router = createBrowserRouter([{ - path:'/', - element: -}]); +const router = createBrowserRouter([ + { + path: "/", + element: , + }, + { + path: "/game", + element: , + }, +]); +// React's StricMode doesn't play well with how I implemented the socket connection ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( - - - + ); diff --git a/client/src/routes/Game.tsx b/client/src/routes/Game.tsx index 4959f6a..da7afda 100644 --- a/client/src/routes/Game.tsx +++ b/client/src/routes/Game.tsx @@ -1,26 +1,104 @@ -import Chess, { Move } from "../../../server/src/chess/engine"; -import { useState } from "react"; +import Chess, { COLOR, Move } from "../../../server/src/engine"; +import { useState, useEffect } from "react"; import ChessBoard from "../components/Chessboard"; +import { useLocation, useNavigate } from "react-router-dom"; +import useCopyToClipboard from "../utils/useCopyToClipboard"; + +import { socket } from "../sockets/socket"; +import { CopyIcon } from "../utils/icons"; +import Show from "../utils/Show"; + +const Game = () => { + const navigate = useNavigate(); + const { state } = useLocation(); + + const { id, color } = + state != null ? state : { id: undefined, color: undefined }; -const App = () => { const [chess] = useState(Chess.load()); - const [, setUpdate] = useState(false); + const [game, setGame] = useState(false); - const sendMove = (move: Move) => { + const makeMove = (move: Move) => { + socket.emit("make move", id, move); chess.makeMove(move); }; + useEffect(() => { + console.log({ id, color }); + + if (id == undefined || color == undefined) + return navigate("/", { + state: { error: "Did not receive a game ID or color." }, + }); + + socket.connect(); + + socket.on("connect", () => { + console.log("Connected to socket", socket.id); + socket.emit("join game", id, color); + }); + + socket.on("disconnect", () => { + console.log("Disconnected"); + }); + + socket.on("join error", () => { + socket.disconnect(); + navigate("/", { + state: { error: "Could not join, please try again." }, + }); + }); + + socket.on("start game", () => setGame(true)); + + // TODO: board doesn't rerender + socket.on("receive move", (move: Move) => { + chess.makeMove(move); + }); + + return () => { + socket.disconnect(); + }; + }, []); + return ( - { - chess.undo(); - setUpdate((prev) => !prev); - }} - /> + }> + + ); }; -export default App; +const Waiting: React.FC<{ + id: string; +}> = ({ id }) => { + const [, copyToClipboard] = useCopyToClipboard(); + const [hasCopied, setHasCopied] = useState(false); + + return ( +
+
Waiting for opponent to join
+
+ Share this ID with your friend: +
+ {id} + +
+
+
+ ); +}; + +export default Game; diff --git a/client/src/routes/Home.tsx b/client/src/routes/Home.tsx new file mode 100644 index 0000000..131eb28 --- /dev/null +++ b/client/src/routes/Home.tsx @@ -0,0 +1,133 @@ +import { useState } from "react"; +import { useLocation, useNavigate } from "react-router-dom"; +import { Color, COLOR } from "../../../server/src/engine"; +import Show from "../utils/Show"; + +const Home = () => { + const navigate = useNavigate(); + const [id, setID] = useState(""); + const [join, setJoin] = useState(false); + const [color, setColor] = useState(COLOR.WHITE); + const [error, setError] = useState(); + + const createGame = () => { + fetch("/api/create-game") + .then((res) => { + if (res.ok) return res.text(); + throw res; + }) + .then((id) => { + setID(id); + }) + .catch((err) => { + console.error(err); + setError(error); + }); + }; + + const joinGame = () => { + fetch(`/api/other-color/${id}`) + .then((res) => { + if (res.ok) return res.text(); + throw res; + }) + .then((text) => { + if (text == "invalid id") throw new Error("Invalid game ID"); + if (text == "full") throw new Error("Game already had 2 players"); + + if (text == COLOR.BLACK) + navigate("/game", { state: { id, color: text } }); + }) + .catch((err) => { + console.error(err); + setError(error); + }); + }; + + return ( + <> + +
+
+ + +
+ +
+
+

Select color:

+
+ setColor(COLOR.WHITE)} + /> + + setColor(COLOR.BLACK)} + /> + +
+ +
+
+
+ +
+
+ + setID(e.target.value)} + /> + +
+
+
+
+ + ); +}; + +// FIX: currently not working +const Err = () => { + const { state } = useLocation(); + const error = state?.state; + + return ( + +
{error}
+
+ ); +}; + +export default Home; diff --git a/client/src/sockets/socket.ts b/client/src/sockets/socket.ts new file mode 100644 index 0000000..454d942 --- /dev/null +++ b/client/src/sockets/socket.ts @@ -0,0 +1,9 @@ +import { io } from "socket.io-client"; + +// "undefined" means the URL will be computed from the `window.location` object +const URL = + process.env.NODE_ENV === "production" ? undefined : "http://localhost:5000"; + +export const socket = io(URL as string, { + autoConnect: false, +}); diff --git a/client/src/utils/Show.tsx b/client/src/utils/Show.tsx index 8865c90..7bae33d 100644 --- a/client/src/utils/Show.tsx +++ b/client/src/utils/Show.tsx @@ -3,7 +3,7 @@ const Show: React.FC<{ children: React.ReactNode; fallback?: React.ReactNode; }> = ({ when, children, fallback }) => { - return <> {when ? children : fallback}; + return <> {when === true ? children : fallback ?? <>}; }; export default Show; diff --git a/client/src/utils/icons.tsx b/client/src/utils/icons.tsx new file mode 100644 index 0000000..318cdd5 --- /dev/null +++ b/client/src/utils/icons.tsx @@ -0,0 +1,14 @@ +export const CopyIcon = () => ( + + + + +); diff --git a/client/src/utils/useCopyToClipboard.tsx b/client/src/utils/useCopyToClipboard.tsx new file mode 100644 index 0000000..97e803e --- /dev/null +++ b/client/src/utils/useCopyToClipboard.tsx @@ -0,0 +1,43 @@ +import { useState, useCallback } from "react"; + +type State = { + error: Error | null; + text: string | null; +}; + +export default function useCopyToClipboard(): [ + State, + (value: any) => Promise +] { + const [state, setState] = useState({ + error: null, + text: null, + }); + + const copyToClipboard = useCallback(async (value: string) => { + if (!navigator?.clipboard) { + return setState({ + error: new Error("Clipboard not supported"), + text: null, + }); + } + + const handleSuccess = () => { + setState({ + error: null, + text: value, + }); + }; + + const handleFailure = (e: any) => { + setState({ + error: e, + text: null, + }); + }; + + navigator.clipboard.writeText(value).then(handleSuccess, handleFailure); + }, []); + + return [state, copyToClipboard]; +} diff --git a/client/vite.config.ts b/client/vite.config.ts index 830dc01..051f42b 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -12,5 +12,6 @@ export default defineConfig({ changeOrigin: true, }, }, + hmr: false, }, }); diff --git a/package.json b/package.json index d85664d..8af9531 100644 --- a/package.json +++ b/package.json @@ -5,11 +5,11 @@ "main": "index.js", "scripts": { "start": "node server/dist/server.js", - "server": "nodemon server/src/server.ts", - "client": "npm run dev --prefix client", - "dev": "concurrently \"npm run server\" \"npm run client\"", + "dev:server": "nodemon server/src/server.ts", + "dev:client": "npm run dev --prefix client", + "dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"", "build:server": "tsc -p .", - "build:client":"npm run build --prefix client", + "build:client": "npm run build --prefix client", "build": "concurrently \"npm run build:server\" \"npm run build:client\"", "test": "vitest run" }, @@ -18,7 +18,8 @@ "license": "ISC", "dependencies": { "dotenv": "^16.3.1", - "express": "^4.18.2" + "express": "^4.18.2", + "socket.io": "^4.7.1" }, "devDependencies": { "@types/express": "^4.17.17", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d411bd9..1efcac0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,6 +11,9 @@ dependencies: express: specifier: ^4.18.2 version: 4.18.2 + socket.io: + specifier: ^4.7.1 + version: 4.7.1 devDependencies: '@types/express': @@ -265,6 +268,10 @@ packages: '@jridgewell/sourcemap-codec': 1.4.14 dev: true + /@socket.io/component-emitter@3.1.0: + resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + dev: false + /@tsconfig/node10@1.0.9: resolution: {integrity: sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==} dev: true @@ -304,6 +311,16 @@ packages: '@types/node': 18.15.11 dev: true + /@types/cookie@0.4.1: + resolution: {integrity: sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==} + dev: false + + /@types/cors@2.8.13: + resolution: {integrity: sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==} + dependencies: + '@types/node': 18.15.11 + dev: false + /@types/express-serve-static-core@4.17.33: resolution: {integrity: sha512-TPBqmR/HRYI3eC2E5hmiivIzv+bidAfXofM+sbonAGvyDhySGw9/PQZFt2BLOrjUUR++4eJVpx6KnLQK1Fk9tA==} dependencies: @@ -327,7 +344,6 @@ packages: /@types/node@18.15.11: resolution: {integrity: sha512-E5Kwq2n4SbMzQOn6wnmBjuK9ouqlURrcZDVfbo9ftDDTFt3nk7ZKK4GMOzoYgnpQJKcxwQw+lGaBvvlMo0qN/Q==} - dev: true /@types/qs@6.9.7: resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==} @@ -449,6 +465,11 @@ packages: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} dev: true + /base64id@2.0.0: + resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} + engines: {node: ^4.5.0 || >= 5.9} + dev: false + /binary-extensions@2.2.0: resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} engines: {node: '>=8'} @@ -609,11 +630,24 @@ packages: resolution: {integrity: sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==} dev: false + /cookie@0.4.2: + resolution: {integrity: sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==} + engines: {node: '>= 0.6'} + dev: false + /cookie@0.5.0: resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} engines: {node: '>= 0.6'} dev: false + /cors@2.8.5: + resolution: {integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==} + engines: {node: '>= 0.10'} + dependencies: + object-assign: 4.1.1 + vary: 1.1.2 + dev: false + /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} dev: true @@ -658,7 +692,6 @@ packages: optional: true dependencies: ms: 2.1.2 - dev: true /deep-eql@4.1.3: resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} @@ -713,6 +746,31 @@ packages: engines: {node: '>= 0.8'} dev: false + /engine.io-parser@5.1.0: + resolution: {integrity: sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==} + engines: {node: '>=10.0.0'} + dev: false + + /engine.io@6.5.1: + resolution: {integrity: sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==} + engines: {node: '>=10.0.0'} + dependencies: + '@types/cookie': 0.4.1 + '@types/cors': 2.8.13 + '@types/node': 18.15.11 + accepts: 1.3.8 + base64id: 2.0.0 + cookie: 0.4.2 + cors: 2.8.5 + debug: 4.3.4 + engine.io-parser: 5.1.0 + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /esbuild@0.17.15: resolution: {integrity: sha512-LBUV2VsUIc/iD9ME75qhT4aJj0r75abCVS0jakhFzOtR7TQsqQA5w0tZ+KTKnwl3kXE0MhskNdHDh/I5aCR1Zw==} engines: {node: '>=12'} @@ -1031,7 +1089,6 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -1076,6 +1133,11 @@ packages: engines: {node: '>=0.10.0'} dev: true + /object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + dev: false + /object-inspect@1.12.3: resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==} dev: false @@ -1313,6 +1375,42 @@ packages: is-fullwidth-code-point: 4.0.0 dev: true + /socket.io-adapter@2.5.2: + resolution: {integrity: sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==} + dependencies: + ws: 8.11.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dev: false + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + + /socket.io@4.7.1: + resolution: {integrity: sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw==} + engines: {node: '>=10.0.0'} + dependencies: + accepts: 1.3.8 + base64id: 2.0.0 + cors: 2.8.5 + debug: 4.3.4 + engine.io: 6.5.1 + socket.io-adapter: 2.5.2 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + /source-map-js@1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} @@ -1660,6 +1758,19 @@ packages: strip-ansi: 6.0.1 dev: true + /ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: ^5.0.2 + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + dev: false + /y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} diff --git a/server/src/__test__/getFEN.test.ts b/server/src/__test__/getFEN.test.ts index c26b108..d208e42 100644 --- a/server/src/__test__/getFEN.test.ts +++ b/server/src/__test__/getFEN.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import Chess, { DEFAULT_POSITION } from "../chess/engine"; +import Chess, { DEFAULT_POSITION } from "../engine"; test("starting position", () => { expect(Chess.load().getFEN()).toEqual(DEFAULT_POSITION); diff --git a/server/src/__test__/getMovesForSquare.test.ts b/server/src/__test__/getMovesForSquare.test.ts index 22eaff7..f572e41 100644 --- a/server/src/__test__/getMovesForSquare.test.ts +++ b/server/src/__test__/getMovesForSquare.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import Chess, { InternalMove, MOVE_FLAGS, Square } from "../chess/engine"; +import Chess, { InternalMove, MOVE_FLAGS, Square } from "../engine"; type ExpectedMoves = Record< string, diff --git a/server/src/__test__/insufficientMaterial.test.ts b/server/src/__test__/insufficientMaterial.test.ts index 7943b6b..0b29bc7 100644 --- a/server/src/__test__/insufficientMaterial.test.ts +++ b/server/src/__test__/insufficientMaterial.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import Chess from "../chess/engine"; +import Chess from "../engine"; describe("insufficient material", () => { test("2 kings", () => { diff --git a/server/src/__test__/isSquareAttacked.test.ts b/server/src/__test__/isSquareAttacked.test.ts index 1f9fc97..0046e50 100644 --- a/server/src/__test__/isSquareAttacked.test.ts +++ b/server/src/__test__/isSquareAttacked.test.ts @@ -1,5 +1,5 @@ import { expect, test } from "vitest"; -import Chess from "../chess/engine"; +import Chess from "../engine"; test("", () => { const chess = Chess.load(); diff --git a/server/src/__test__/loadFEN.test.ts b/server/src/__test__/loadFEN.test.ts index 55aa203..9056199 100644 --- a/server/src/__test__/loadFEN.test.ts +++ b/server/src/__test__/loadFEN.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import Chess, { COLOR, PIECE } from "../chess/engine"; +import Chess, { COLOR, PIECE } from "../engine"; describe("valid FEN strings", () => { test("default", () => { diff --git a/server/src/__test__/validateFEN.test.ts b/server/src/__test__/validateFEN.test.ts index e57bd2f..8e13ffb 100644 --- a/server/src/__test__/validateFEN.test.ts +++ b/server/src/__test__/validateFEN.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { DEFAULT_POSITION, validateFEN } from "../chess/engine"; +import { DEFAULT_POSITION, validateFEN } from "../engine"; describe("valid FEN strings", () => { test("starting position", () => { diff --git a/server/src/chess/engine.ts b/server/src/engine.ts similarity index 99% rename from server/src/chess/engine.ts rename to server/src/engine.ts index 4a1223d..8f67486 100644 --- a/server/src/chess/engine.ts +++ b/server/src/engine.ts @@ -985,6 +985,10 @@ export default class Chess { // TODO: implement history() { } + toString() { + return this.getFEN(); + } + static Builder = class { private _board: Board = new Array(64).fill(null); private _turn: Color = COLOR.WHITE; diff --git a/server/src/server.ts b/server/src/server.ts index 0a67082..7748665 100644 --- a/server/src/server.ts +++ b/server/src/server.ts @@ -1,12 +1,115 @@ -import express, { Application, Request, Response } from "express"; +import express, { Request, Response } from "express"; import path from "path"; import dotenv from "dotenv"; +import http from "http"; +import { Server } from "socket.io"; +import { randomUUID } from "crypto"; +import Chess, { COLOR, Color, Move } from "./engine"; dotenv.config(); const PORT = process.env.PORT || 5000; -const app: Application = express(); +const app = express(); +const server = http.createServer(app); +const io = new Server( + server, + process.env.NODE_ENV === "development" + ? { + cors: { + origin: "http://localhost:3000", + }, + } + : {} +); + +type Room = { + [color in Color]: string | null; +} & { + game: Chess; +}; + +const rooms: Map = new Map(); + +io.on("connection", (socket) => { + console.log("A user connected", socket.id); + + socket.on("disconnect", () => { + console.log("A user disconnected", socket.id); + }); + + socket.on("join game", (id: string, color: Color) => { + const room = rooms.get(id); + + console.log({ + isRoom: room != undefined, + w: room?.w, + b: room?.b, + sock: socket.id, + color, + cond: + room == undefined || (room[color] != null && room[color] != socket.id), + }); + + if ( + room == undefined || + (room[color] != null && room[color] != socket.id) + ) { + socket.emit("join error"); + return; + } + + socket.join(id); + + room[color] = socket.id; + + if (room[COLOR.WHITE] != null && room[COLOR.BLACK] != null) + io.to(id).emit("start game"); + }); + + socket.on("make move", (id: string, move: Move) => { + socket.to(id).emit("receive move", move); + }); +}); + +if (process.env.NODE_ENV === "development") + app.get("/games", (_req, res) => { + const m = new Map(); + rooms.forEach((room, id) => + m.set(id, { + w: room.w, + b: room.b, + game: room.game.getFEN(), + }) + ); + return res.json(Object.fromEntries(m)); + }); + +app.get("/api/create-game", (_req, res) => { + let id = randomUUID(); + + while (rooms.has(id)) id = randomUUID(); + + rooms.set(id, { + b: null, + w: null, + game: Chess.load(), + }); + + res.send(id); + console.log("Created room:", id); +}); + +app.get("/api/other-color/:id", (req, res) => { + const id = req.params.id; + + const room = rooms.get(id); + + if (room == undefined) res.send("invalid id"); + else if (room[COLOR.WHITE] == null) res.send(COLOR.WHITE); + else if (room[COLOR.BLACK] == null) res.send(COLOR.BLACK); + else res.send("full"); +}); if (process.env.NODE_ENV === "production") { const __dirname = path.resolve(); @@ -33,6 +136,6 @@ app.use((err: Error, _req: Request, res: Response) => { }); }); -app.listen(PORT, () => +server.listen(PORT, () => console.log("Server listening on http://localhost:" + PORT) );