chore: reorganize files and add some boilerplate

This commit is contained in:
Cozma Rares
2023-07-24 12:06:42 +03:00
parent 7d6dc492c3
commit 76032237d4
12 changed files with 591 additions and 244 deletions
+5 -1
View File
@@ -9,8 +9,12 @@
"preview": "vite preview"
},
"dependencies": {
"localforage": "^1.10.0",
"match-sorter": "^6.3.1",
"react": "^18.2.0",
"react-dom": "^18.2.0"
"react-dom": "^18.2.0",
"react-router-dom": "^6.14.2",
"sort-by": "^1.2.0"
},
"devDependencies": {
"@types/react": "^18.0.28",
+89
View File
@@ -5,12 +5,24 @@ settings:
excludeLinksFromLockfile: false
dependencies:
localforage:
specifier: ^1.10.0
version: 1.10.0
match-sorter:
specifier: ^6.3.1
version: 6.3.1
react:
specifier: ^18.2.0
version: 18.2.0
react-dom:
specifier: ^18.2.0
version: 18.2.0(react@18.2.0)
react-router-dom:
specifier: ^6.14.2
version: 6.14.2(react-dom@18.2.0)(react@18.2.0)
sort-by:
specifier: ^1.2.0
version: 1.2.0
devDependencies:
'@types/react':
@@ -45,6 +57,13 @@ packages:
engines: {node: '>=10'}
dev: true
/@babel/runtime@7.22.6:
resolution: {integrity: sha512-wDb5pWm4WDdF6LFUde3Jl8WzPA+3ZbxYqkC6xAXuD3irdEHN1k0NfTRrJD8ZD378SJ61miMLCqIOXYhd8x+AJQ==}
engines: {node: '>=6.9.0'}
dependencies:
regenerator-runtime: 0.13.11
dev: false
/@esbuild/android-arm64@0.17.14:
resolution: {integrity: sha512-eLOpPO1RvtsP71afiFTvS7tVFShJBCT0txiv/xjFBo5a7R7Gjw7X0IgIaFoLKhqXYAXhahoXm7qAmRXhY4guJg==}
engines: {node: '>=12'}
@@ -298,6 +317,11 @@ packages:
fastq: 1.15.0
dev: true
/@remix-run/router@1.7.2:
resolution: {integrity: sha512-7Lcn7IqGMV+vizMPoEl5F0XDshcdDYtMI6uJLQdQz5CfZAwy3vvGKYSUk789qndt5dEC4HfSjviSYlSoHGL2+A==}
engines: {node: '>=14'}
dev: false
/@swc/core-darwin-arm64@1.3.44:
resolution: {integrity: sha512-Y+oVsCjXUPvr3D9YLuB1gjP84TseM/CRkbPNrf+3JXQhsPEkgxdIdFP1cl/obeqMQrRgPpvSfK+TOvGuOuV22g==}
engines: {node: '>=10'}
@@ -668,6 +692,10 @@ packages:
function-bind: 1.1.1
dev: true
/immediate@3.0.6:
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
dev: false
/inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
dependencies:
@@ -718,6 +746,12 @@ packages:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
dev: false
/lie@3.1.1:
resolution: {integrity: sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==}
dependencies:
immediate: 3.0.6
dev: false
/lilconfig@2.1.0:
resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==}
engines: {node: '>=10'}
@@ -727,6 +761,12 @@ packages:
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
dev: true
/localforage@1.10.0:
resolution: {integrity: sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==}
dependencies:
lie: 3.1.1
dev: false
/loose-envify@1.4.0:
resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
hasBin: true
@@ -734,6 +774,13 @@ packages:
js-tokens: 4.0.0
dev: false
/match-sorter@6.3.1:
resolution: {integrity: sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==}
dependencies:
'@babel/runtime': 7.22.6
remove-accents: 0.4.2
dev: false
/merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -791,6 +838,11 @@ packages:
engines: {node: '>= 6'}
dev: true
/object-path@0.6.0:
resolution: {integrity: sha512-fxrwsCFi3/p+LeLOAwo/wyRMODZxdGBtUlWRzsEpsUVrisZbEfZ21arxLGfaWfcnqb8oHPNihIb4XPE8CQPN5A==}
engines: {node: '>=0.8.0'}
dev: false
/once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
dependencies:
@@ -909,6 +961,29 @@ packages:
scheduler: 0.23.0
dev: false
/react-router-dom@6.14.2(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-5pWX0jdKR48XFZBuJqHosX3AAHjRAzygouMTyimnBPOLdY3WjzUSKhus2FVMihUFWzeLebDgr4r8UeQFAct7Bg==}
engines: {node: '>=14'}
peerDependencies:
react: '>=16.8'
react-dom: '>=16.8'
dependencies:
'@remix-run/router': 1.7.2
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
react-router: 6.14.2(react@18.2.0)
dev: false
/react-router@6.14.2(react@18.2.0):
resolution: {integrity: sha512-09Zss2dE2z+T1D03IheqAFtK4UzQyX8nFPWx6jkwdYzGLXd5ie06A6ezS2fO6zJfEb/SpG6UocN2O1hfD+2urQ==}
engines: {node: '>=14'}
peerDependencies:
react: '>=16.8'
dependencies:
'@remix-run/router': 1.7.2
react: 18.2.0
dev: false
/react@18.2.0:
resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==}
engines: {node: '>=0.10.0'}
@@ -929,6 +1004,14 @@ packages:
picomatch: 2.3.1
dev: true
/regenerator-runtime@0.13.11:
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
dev: false
/remove-accents@0.4.2:
resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==}
dev: false
/resolve@1.22.1:
resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==}
hasBin: true
@@ -972,6 +1055,12 @@ packages:
loose-envify: 1.4.0
dev: false
/sort-by@1.2.0:
resolution: {integrity: sha512-aRyW65r3xMnf4nxJRluCg0H/woJpksU1dQxRtXYzau30sNBOmf5HACpDd9MZDhKh7ALQ5FgSOfMPwZEtUmMqcg==}
dependencies:
object-path: 0.6.0
dev: false
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
+5 -230
View File
@@ -1,43 +1,8 @@
import wp from "./assets/pieces/wp.png";
import wn from "./assets/pieces/wn.png";
import wb from "./assets/pieces/wb.png";
import wr from "./assets/pieces/wr.png";
import wq from "./assets/pieces/wq.png";
import wk from "./assets/pieces/wk.png";
import Chess, { Move } from "../../server/src/chess/engine";
import { useState } from "react";
import ChessBoard from "./components/Chessboard";
import bp from "./assets/pieces/bp.png";
import bn from "./assets/pieces/bn.png";
import bb from "./assets/pieces/bb.png";
import br from "./assets/pieces/br.png";
import bq from "./assets/pieces/bq.png";
import bk from "./assets/pieces/bk.png";
import Chess, {
squareColor,
Piece,
PieceType,
Color,
algebraic,
file,
rank,
FILE,
RANK,
squareIndex,
Move,
MOVE_FLAGS,
PIECE_PROMOTION,
COLOR,
PiecePromotionType,
} from "../../server/src/chess/engine";
import { MouseEventHandler, useLayoutEffect, useState } from "react";
import Show from "./utils/Show";
const PIECES: Record<Color, Record<PieceType, string>> = {
w: { p: wp, n: wn, b: wb, r: wr, q: wq, k: wk },
b: { p: bp, n: bn, b: bb, r: br, q: bq, k: bk },
};
export default function App() {
const App = () => {
const [chess] = useState(Chess.load());
const [, setUpdate] = useState(false);
@@ -58,196 +23,6 @@ export default function App() {
</button>
</>
);
}
// taken form https://github.com/uidotdev/usehooks
function useWindowSize() {
const [size, setSize] = useState({
width: -1,
height: -1,
});
useLayoutEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
handleResize();
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return size;
}
const ChessBoard: React.FC<{
chess: Chess;
sendMove: (move: Move) => void;
blackPerspective?: boolean;
}> = ({ chess, sendMove, blackPerspective }) => {
const { width, height } = useWindowSize();
const [activeTile, setActiveTile] = useState<number>(-1);
const [promotionMove, setPromotionMove] = useState<
(Pick<Move, "from" | "to"> & { color: Color }) | null
>(null);
const gridSize = Math.min(width, height, 800);
const tileProps = new Array(64).fill(null).map((_, i) => ({
tileNumber: i,
piece: chess.getPiece(i),
isAttacked: false,
isPromotion: false,
isActive: false,
}));
if (activeTile != -1) {
tileProps[activeTile].isActive = true;
chess.getMovesForSquare(algebraic(activeTile)).forEach(({ to, flags }) => {
const square = squareIndex(to);
tileProps[square].isAttacked = true;
tileProps[square].isPromotion =
flags & MOVE_FLAGS.PROMOTION ? true : false;
});
}
const tiles = tileProps.map((props, i) => (
<Tile key={i} {...props} blackPerspective={blackPerspective} />
));
const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
if (promotionMove != null) return;
// TODO: fix types
// @ts-ignore
const tile = parseInt(e.target.dataset.tile);
if (isNaN(tile)) return;
if (activeTile != -1 && tileProps[tile].isAttacked) {
const moveObj = {
from: algebraic(activeTile),
to: algebraic(tile),
color: (chess.getPiece(activeTile) as Piece).color,
};
if (tileProps[tile].isPromotion) return setPromotionMove(moveObj);
sendMove(moveObj);
setActiveTile(-1);
}
if (tileProps[tile].piece == null) return setActiveTile(-1);
if (tile == activeTile) setActiveTile(-1);
else setActiveTile(tile);
};
const sendPromotionMove = (promotion: PiecePromotionType) => {
if (promotionMove == null) return;
const moveObj: Move = {
from: promotionMove.from,
to: promotionMove.to,
promotion,
};
sendMove(moveObj);
setPromotionMove(null);
setActiveTile(-1);
};
return (
<div className="flex flex-row h-fit items-center">
<div
className="relative grid grid-rows-8 grid-cols-8 aspect-square border-8 border-black rounded-lg"
onClick={handleClick}
style={{ width: `${gridSize}px` }}
>
{blackPerspective == true ? tiles.reverse() : tiles}
</div>
{promotionMove == null ? (
<></>
) : (
<div className="w-fit flex flex-col bg-black rounded-r-2xl p-1">
{PIECE_PROMOTION.map((p) => (
<img
key={p}
src={PIECES[promotionMove.color][p]}
style={{ width: `${gridSize / 8}px`, aspectRatio: 1 }}
onClick={() => sendPromotionMove(p)}
/>
))}
</div>
)}
</div>
);
};
const TILE_COLORS = Object.freeze({
w: {
bg: "bg-white-tile",
text: "text-black-tile",
active: "bg-sky-400",
},
b: {
bg: "bg-black-tile",
text: "text-white-tile",
active: "bg-sky-700",
},
} as const);
const Tile: React.FC<{
tileNumber: number;
piece: Piece | null;
isActive: boolean;
isAttacked: boolean;
blackPerspective?: boolean;
}> = ({ tileNumber, piece, isActive, isAttacked, blackPerspective }) => {
const color = squareColor(tileNumber);
const { bg: bgColor, text: textColor } = TILE_COLORS[color];
const activeColor = isActive ? TILE_COLORS[color].active : "";
const tileFile = file(tileNumber);
const tileRank = rank(tileNumber);
const square = algebraic(tileNumber);
const isFirstColumn = tileFile == (blackPerspective ? FILE.H : FILE.A);
const isLastRow = tileRank == (blackPerspective ? RANK.EIGHTH : RANK.FIRST);
return (
<div
className={`relative aspect-square font-bold text-xl isolate group [&>*]:pointer-events-none ${bgColor} ${activeColor}`}
data-tile={tileNumber}
>
{piece == null ? <></> : <img src={PIECES[piece.color][piece.type]} />}
<Show when={isAttacked == true}>
<Show
when={piece == null}
fallback={
<div className="absolute -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 w-full aspect-square border-8 border-gray-900/40 rounded-full"></div>
}
>
<div className="absolute -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 w-[35%] aspect-square bg-gray-900/40 rounded-full group-hover:w-[45%]"></div>
</Show>
</Show>
<Show when={isFirstColumn}>
<div className={`absolute top-1 left-1 -z-10 ${textColor}`}>
{square[1]}
</div>
</Show>
<Show when={isLastRow}>
<div className={`absolute bottom-1 right-1 -z-10 ${textColor}`}>
{square[0]}
</div>
</Show>
</div>
);
};
export default App;
+206
View File
@@ -0,0 +1,206 @@
import wp from "../assets/pieces/wp.png";
import wn from "../assets/pieces/wn.png";
import wb from "../assets/pieces/wb.png";
import wr from "../assets/pieces/wr.png";
import wq from "../assets/pieces/wq.png";
import wk from "../assets/pieces/wk.png";
import bp from "../assets/pieces/bp.png";
import bn from "../assets/pieces/bn.png";
import bb from "../assets/pieces/bb.png";
import br from "../assets/pieces/br.png";
import bq from "../assets/pieces/bq.png";
import bk from "../assets/pieces/bk.png";
import Chess, {
squareColor,
Piece,
PieceType,
Color,
algebraic,
file,
rank,
FILE,
RANK,
squareIndex,
Move,
MOVE_FLAGS,
PIECE_PROMOTION,
PiecePromotionType,
} from "../../../server/src/chess/engine";
import { MouseEventHandler, useState } from "react";
import Show from "../utils/Show";
import useWindowSize from "../utils/useWindowSize";
const PIECES: Record<Color, Record<PieceType, string>> = {
w: { p: wp, n: wn, b: wb, r: wr, q: wq, k: wk },
b: { p: bp, n: bn, b: bb, r: br, q: bq, k: bk },
};
const ChessBoard: React.FC<{
chess: Chess;
sendMove: (move: Move) => void;
blackPerspective?: boolean;
}> = ({ chess, sendMove, blackPerspective }) => {
const { width, height } = useWindowSize();
const [activeTile, setActiveTile] = useState<number>(-1);
const [promotionMove, setPromotionMove] = useState<
(Pick<Move, "from" | "to"> & { color: Color }) | null
>(null);
const gridSize = Math.min(width, height, 800);
const tileProps = new Array(64).fill(null).map((_, i) => ({
tileNumber: i,
piece: chess.getPiece(i),
isAttacked: false,
isPromotion: false,
isActive: false,
}));
if (activeTile != -1) {
tileProps[activeTile].isActive = true;
chess.getMovesForSquare(algebraic(activeTile)).forEach(({ to, flags }) => {
const square = squareIndex(to);
tileProps[square].isAttacked = true;
tileProps[square].isPromotion =
flags & MOVE_FLAGS.PROMOTION ? true : false;
});
}
const tiles = tileProps.map((props, i) => (
<Tile key={i} {...props} blackPerspective={blackPerspective} />
));
const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
if (promotionMove != null) return;
// TODO: fix types
// @ts-ignore
const tile = parseInt(e.target.dataset.tile);
if (isNaN(tile)) return;
if (activeTile != -1 && tileProps[tile].isAttacked) {
const moveObj = {
from: algebraic(activeTile),
to: algebraic(tile),
color: (chess.getPiece(activeTile) as Piece).color,
};
if (tileProps[tile].isPromotion) return setPromotionMove(moveObj);
sendMove(moveObj);
setActiveTile(-1);
}
if (tileProps[tile].piece == null) return setActiveTile(-1);
if (tile == activeTile) setActiveTile(-1);
else setActiveTile(tile);
};
const sendPromotionMove = (promotion: PiecePromotionType) => {
if (promotionMove == null) return;
const moveObj: Move = {
from: promotionMove.from,
to: promotionMove.to,
promotion,
};
sendMove(moveObj);
setPromotionMove(null);
setActiveTile(-1);
};
return (
<div className="flex flex-row h-fit items-center">
<div
className="relative grid grid-rows-8 grid-cols-8 aspect-square border-8 border-black rounded-lg"
onClick={handleClick}
style={{ width: `${gridSize}px` }}
>
{blackPerspective == true ? tiles.reverse() : tiles}
</div>
{promotionMove == null ? (
<></>
) : (
<div className="w-fit flex flex-col bg-black rounded-r-2xl p-1">
{PIECE_PROMOTION.map((p) => (
<img
key={p}
src={PIECES[promotionMove.color][p]}
style={{ width: `${gridSize / 8}px`, aspectRatio: 1 }}
onClick={() => sendPromotionMove(p)}
/>
))}
</div>
)}
</div>
);
};
const TILE_COLORS = Object.freeze({
w: {
bg: "bg-white-tile",
text: "text-black-tile",
active: "bg-sky-400",
},
b: {
bg: "bg-black-tile",
text: "text-white-tile",
active: "bg-sky-700",
},
} as const);
const Tile: React.FC<{
tileNumber: number;
piece: Piece | null;
isActive: boolean;
isAttacked: boolean;
blackPerspective?: boolean;
}> = ({ tileNumber, piece, isActive, isAttacked, blackPerspective }) => {
const color = squareColor(tileNumber);
const { bg: bgColor, text: textColor } = TILE_COLORS[color];
const activeColor = isActive ? TILE_COLORS[color].active : "";
const tileFile = file(tileNumber);
const tileRank = rank(tileNumber);
const square = algebraic(tileNumber);
const isFirstColumn = tileFile == (blackPerspective ? FILE.H : FILE.A);
const isLastRow = tileRank == (blackPerspective ? RANK.EIGHTH : RANK.FIRST);
return (
<div
className={`relative aspect-square font-bold text-xl isolate group [&>*]:pointer-events-none ${bgColor} ${activeColor}`}
data-tile={tileNumber}
>
{piece == null ? <></> : <img src={PIECES[piece.color][piece.type]} />}
<Show when={isAttacked == true}>
<Show
when={piece == null}
fallback={
<div className="absolute -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 w-full aspect-square border-8 border-gray-900/40 rounded-full"></div>
}
>
<div className="absolute -translate-x-1/2 -translate-y-1/2 top-1/2 left-1/2 w-[35%] aspect-square bg-gray-900/40 rounded-full group-hover:w-[45%]"></div>
</Show>
</Show>
<Show when={isFirstColumn}>
<div className={`absolute top-1 left-1 -z-10 ${textColor}`}>
{square[1]}
</div>
</Show>
<Show when={isLastRow}>
<div className={`absolute bottom-1 right-1 -z-10 ${textColor}`}>
{square[0]}
</div>
</Show>
</div>
);
};
export default ChessBoard;
+27
View File
@@ -0,0 +1,27 @@
import { useLayoutEffect, useState } from "react";
// taken form https://github.com/uidotdev/usehooks
export default function useWindowSize() {
const [size, setSize] = useState({
width: -1,
height: -1,
});
useLayoutEffect(() => {
const handleResize = () => {
setSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
handleResize();
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};
}, []);
return size;
}
+12 -3
View File
@@ -1,7 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react-swc'
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react-swc";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
})
server: {
port: 3000,
proxy: {
"/api": {
target: "http://localhost:5000",
changeOrigin: true,
},
},
},
});