@@ -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)
);