BIG feat: add sockets, create and join games

This commit is contained in:
Cozma Rares
2023-07-29 22:41:08 +03:00
parent 6874bba431
commit 156073b6a0
25 changed files with 664 additions and 57 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<link rel="icon" type="image/svg+xml" href="/knight.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Chess Game</title>
</head>
+1
View File
@@ -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": {
+84
View File
@@ -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'}
+5
View File
@@ -0,0 +1,5 @@
<svg fill="#000000" height="800px" width="800px" version="1.1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 279.519 279.519" xmlns:xlink="http://www.w3.org/1999/xlink" enable-background="new 0 0 279.519 279.519">
<g>
<path d="m231.426,239.916h-13.5l-20.569-27.505 39.203-121.231c0.192-0.597 0.291-1.219 0.291-1.846 0-49.259-40.075-89.334-89.334-89.334-22.35,0-43.74,8.295-60.231,23.358-1.264,1.155-1.975,2.794-1.953,4.506 0.022,1.712 0.774,3.333 2.067,4.456l23.776,20.633-44.972,44.449c-6.649,6.574-6.915,17.127-0.604,24.025l14.575,15.93c1.606,1.757 4.101,2.395 6.357,1.631l49.715-16.894c1.061,0.677 2.151,1.305 3.274,1.873l-59.256,62.332c-13.653,14.363-19.268,34.451-15.305,53.617h-16.868c-3.313,0-6,2.687-6,6v27.603c0,3.313 2.687,6 6,6h183.334c3.313,0 6-2.687 6-6v-27.603c0-3.313-2.686-6-6-6zm-142.464-45.35l63.484-66.779c1.307,0.14 2.625,0.231 3.954,0.231 20.659,0 37.467-16.808 37.467-37.467 0-10.007-3.897-19.417-10.975-26.493-2.342-2.343-6.143-2.343-8.484,0-2.344,2.343-2.344,6.142 0,8.485 4.81,4.81 7.459,11.206 7.459,18.008 0,14.042-11.425,25.467-25.467,25.467-9.036,0-17.478-4.858-22.029-12.679-1.667-2.864-5.341-3.836-8.204-2.167-2.863,1.667-3.834,5.34-2.167,8.204 0.696,1.196 1.468,2.332 2.282,3.43l-39.899,13.559-11.93-13.039c-1.941-2.122-1.859-5.369 0.186-7.39l49.58-49.002c1.188-1.175 1.833-2.792 1.779-4.462-0.055-1.67-0.803-3.242-2.064-4.337l-23.354-20.267c13.434-10.275 29.863-15.868 46.937-15.868 42.332,0 76.83,34.188 77.328,76.404l-39.86,123.268c-0.599,1.853-0.262,3.88 0.904,5.439l17.054,22.804h-125.641c-4.2-16.056 0.153-33.242 11.66-45.349zm136.464,72.953h-171.334v-15.603h171.334v15.603z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

Before

Width:  |  Height:  |  Size: 1.5 KiB

+6 -14
View File
@@ -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<Color, Record<PieceType, string>> = {
};
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<number>(-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 (
<>
<div className="relative w-fit h-fit isolate border-[6px] border-black rounded-lg peer">
<div
className="grid grid-rows-8 grid-cols-8 aspect-square"
className="grid grid-rows-8 grid-cols-8 aspect-square select-none"
onClick={handleClick}
style={{ width: `${gridSize}px` }}
>
@@ -148,7 +141,6 @@ const ChessBoard: React.FC<{
</>
)}
</div>
<button onClick={handleUndo}>undo</button>
</>
);
};
+24
View File
@@ -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: "";
}
}
+13 -8
View File
@@ -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:<Game />
}]);
const router = createBrowserRouter([
{
path: "/",
element: <Home />,
},
{
path: "/game",
element: <Game />,
},
]);
// React's StricMode doesn't play well with how I implemented the socket connection
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
<RouterProvider router={router} />
);
+93 -15
View File
@@ -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 (
<ChessBoard
key={chess.getFEN()}
chess={chess}
sendMove={sendMove}
undo={() => {
chess.undo();
setUpdate((prev) => !prev);
}}
/>
<Show when={game} fallback={<Waiting id={id} />}>
<ChessBoard
key={chess.getFEN()}
chess={chess}
makeMove={makeMove}
blackPerspective={color === COLOR.BLACK}
/>
</Show>
);
};
export default App;
const Waiting: React.FC<{
id: string;
}> = ({ id }) => {
const [, copyToClipboard] = useCopyToClipboard();
const [hasCopied, setHasCopied] = useState(false);
return (
<div className="ml-2">
<div className="loading text-lg">Waiting for opponent to join</div>
<div>
Share this ID with your friend:
<div className="bg-gray-800 text-white p-2 rounded-md w-fit">
<code className="mr-4">{id}</code>
<button
className="inline-flex flex-row gap-1 justify-center items-center border border-white p-1 text-xs rounded-md"
onClick={() => {
copyToClipboard(id);
setHasCopied(true);
}}
>
<CopyIcon /> {hasCopied ? "Copied!" : "Copy"}
</button>
</div>
</div>
</div>
);
};
export default Game;
+133
View File
@@ -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<string>("");
const [join, setJoin] = useState(false);
const [color, setColor] = useState<Color>(COLOR.WHITE);
const [error, setError] = useState<Error>();
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 (
<>
<Err />
<div className="text-xl">
<div className="absolute top-0 left-0 right-0 bottom-0 m-auto p-6 bg-gray-800 text-white w-fit h-fit rounded-[25px]">
<button
className="block cursor-pointer border-2 rounded-md p-2 w-full hover:bg-white hover:text-gray-800 transition-colors mb-3"
onClick={createGame}
>
Create Game
</button>
<button
className="block cursor-pointer border-2 rounded-md p-2 w-full hover:bg-white hover:text-gray-800 transition-colors"
onClick={() => setJoin(true)}
>
Join Game
</button>
</div>
<Show when={join == false && id != ""}>
<div className="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex justify-center items-center bg-zinc-800 bg-opacity-70">
<div className="bg-gray-900 text-white p-4 rounded-lg">
<p>Select color:</p>
<div className="grid grid-cols-[auto,minmax(0,1fr)] text-center ">
<input
type="radio"
name="color"
id="white"
checked
onChange={() => setColor(COLOR.WHITE)}
/>
<label htmlFor="white">White</label>
<input
type="radio"
name="color"
id="black"
onChange={() => setColor(COLOR.BLACK)}
/>
<label htmlFor="black">Black</label>
</div>
<button
className="block cursor-pointer border-2 rounded-md p-2 w-full hover:bg-white hover:text-gray-800 transition-colors mt-3"
onClick={() => navigate("/game", { state: { id, color } })}
>
Start Game
</button>
</div>
</div>
</Show>
<Show when={join}>
<div className="absolute top-0 left-0 right-0 bottom-0 w-full h-full flex justify-center items-center bg-zinc-800 bg-opacity-70">
<div className="bg-gray-900 text-white p-4 rounded-lg">
<label htmlFor="game-id">Insert game ID: </label>
<input
className="block bg-zinc-700 p-1 mt-2"
type="text"
name="game-id"
id="game-id"
value={id}
onChange={(e) => setID(e.target.value)}
/>
<button
className="block cursor-pointer border-2 rounded-md p-2 w-full hover:bg-white hover:text-gray-800 transition-colors mt-3"
onClick={joinGame}
>
Join
</button>
</div>
</div>
</Show>
</div>
</>
);
};
// FIX: currently not working
const Err = () => {
const { state } = useLocation();
const error = state?.state;
return (
<Show when={error != undefined}>
<div>{error}</div>
</Show>
);
};
export default Home;
+9
View File
@@ -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,
});
+1 -1
View File
@@ -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;
+14
View File
@@ -0,0 +1,14 @@
export const CopyIcon = () => (
<svg
stroke="currentColor"
fill="currentColor"
strokeWidth="0"
viewBox="0 0 24 24"
height="1em"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path fill="none" d="M0 0h24v24H0V0z"></path>
<path d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"></path>
</svg>
);
+43
View File
@@ -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<void>
] {
const [state, setState] = useState<State>({
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];
}
+1
View File
@@ -12,5 +12,6 @@ export default defineConfig({
changeOrigin: true,
},
},
hmr: false,
},
});
+6 -5
View File
@@ -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",
+114 -3
View File
@@ -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'}
+1 -1
View File
@@ -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);
@@ -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,
@@ -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", () => {
+1 -1
View File
@@ -1,5 +1,5 @@
import { expect, test } from "vitest";
import Chess from "../chess/engine";
import Chess from "../engine";
test("", () => {
const chess = Chess.load();
+1 -1
View File
@@ -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", () => {
+1 -1
View File
@@ -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", () => {
@@ -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;
+106 -3
View File
@@ -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<string, Room> = 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)
);