Skip to content

Commit

Permalink
feat: multiple features
Browse files Browse the repository at this point in the history
- detect game over and show dialog
- improve responsive design
- end game dialog for win or lose.
- fix issues with websockets.
- improve performance
  • Loading branch information
LucSPI committed May 22, 2023
1 parent 1d42ad5 commit b0c3bb3
Show file tree
Hide file tree
Showing 18 changed files with 587 additions and 500 deletions.
80 changes: 77 additions & 3 deletions src/components/Board/Elements.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { forwardRef } from 'react';
import cn from 'classnames';
import { forwardRef, useMemo } from 'react';
import { CustomPieces, CustomSquareRenderer } from 'react-chessboard/dist/chessboard/types';
import { BsFillStopwatchFill } from 'react-icons/bs';
import { FiUser } from 'react-icons/fi';
import { Bishop, King, Knight, Pawn, Queen, Rook } from '~/assets/icons';

import { getFormatedTime } from './utils';
export type PiecesTypes = keyof CustomPieces;
export const customPieces: CustomPieces = {
wB: ({ squareWidth }) => <Bishop width={squareWidth - 10} color="white" />,
wN: ({ squareWidth }) => <Knight width={squareWidth - 10} color="white" />,
Expand All @@ -17,7 +21,77 @@ export const customPieces: CustomPieces = {
bR: ({ squareWidth }) => <Rook width={squareWidth - 10} />,
};

export const Square: CustomSquareRenderer = forwardRef(({ children, style }, ref) => {
export const customPiecesMap = new Map(Object.entries(customPieces));
function getPieceKey(piece: string, isWhite: boolean) {
return isWhite ? `w${piece.toUpperCase()}` : `b${piece.toUpperCase()}`;
}
const PlayerInfo = (props: { picture: string; username: string; rating: number }) => {
return (
<div className="flex items-center">
{props.picture ? (
<img src={props.picture} alt="" className="w-6 rounded-full mr-2" />
) : (
<div className="w-6 h-6 rounded-full mr-2 bg-gray-200 flex items-center justify-center">
<FiUser className="h-4 w-4" />
</div>
)}
<p>
{props.username} ({props.rating})
</p>
</div>
);
};

type PlayerStatusProps = {
side: 'top' | 'bottom';
picture: string;
username: string;
rating: number;
timeLeft: number;
isWhite?: boolean;
takenPieces: {
[key: string]: number;
};
};

export const PlayerStatus = (props: PlayerStatusProps) => {
const takenPieces = useMemo(
() =>
props.takenPieces
? Object.entries(props.takenPieces).map(([piece, count]) => {
const Piece = customPiecesMap.get(getPieceKey(piece, !props.isWhite));
if (!Piece) return null;
return new Array(count)
.fill(null)
.map((_, index) => (
<Piece key={`${piece}-${index}`} squareWidth={14} isDragging={false} />
));
})
: [],
[props.takenPieces],
);
return (
<div
style={{ /* maxWidth: '70vh', width: '70vw', */ margin: '0 auto' }}
className={cn(
'board_container px-4 py-2 flex justify-between items-stretch border border-gray-200',
{
'rounded-t-md': props.side === 'top',
'rounded-b-md': props.side === 'bottom',
},
)}
>
<PlayerInfo picture={props.picture} rating={props.rating} username={props.username} />
<div className="flex">{takenPieces}</div>
<div className="flex items-center border border-gray-200 rounded-md px-2 py-1">
<BsFillStopwatchFill className="text-blue-100 mr-2" />
<p>{getFormatedTime(props.timeLeft)}</p>
</div>
</div>
);
};

export const CustomSquare: CustomSquareRenderer = forwardRef(({ children, style }, ref) => {
return (
<div
ref={ref}
Expand Down
203 changes: 128 additions & 75 deletions src/components/Board/index.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,60 @@
import { Chess } from 'chess.js';
import { CSSProperties, useRef, useState } from 'react';
import { Square } from 'chess.js';
import { Chessboard } from 'react-chessboard';
import { Square } from 'react-chessboard/dist/chessboard/types';
import { START_POSITION } from '~/assets/CONSTANTS';

import { Square as CustomSquare, customPieces } from './Elements';
import { createStore } from '@udecode/zustood';
import { Chess } from 'chess.js';
import { CSSProperties, useEffect, useRef } from 'react';
import { CustomSquare, PlayerStatus, customPieces } from './Elements';
import { BoardProps, BoardState, SanitizedChessboardProps } from './interfaces';
import { customDarkSquareStyle, customLightSquareStyle } from './styles';
import './styles.css';
import { Piece } from 'react-chessboard/dist/chessboard/types';

const DEFAULT_BOARD_PROPS: SanitizedChessboardProps = {
id: 'board',
animationDuration: 300,

position: START_POSITION,
boardOrientation: 'white',
arePremovesAllowed: false,

customSquare: CustomSquare,
customPieces: customPieces,
customDarkSquareStyle: customDarkSquareStyle,
customLightSquareStyle: customLightSquareStyle,
};

export const boardStore = createStore('board')<BoardState>({
moveFrom: '',
possibleMoves: {},
});

export default function Board(props: BoardProps) {
const chess = useRef(new Chess(props.fen || START_POSITION)).current;
const board = boardStore.useTrackedStore();

useEffect(() => {
chess.load(props.fen || START_POSITION);
}, [props.fen]);
const isTurn = () => (props.isWhite ? chess.turn() === 'w' : chess.turn() === 'b');

export default function Board(_props: {
width?: number;
disabled?: boolean;
onChange?: (fen: string) => void;
}) {
const game = useRef(new Chess()).current;

const [moveFrom, setMoveFrom] = useState<Square | ''>('');
const [options, setOptions] = useState<{ [key in Square]?: CSSProperties }>({});
// const [, setNewFen] = useState(game.fen());
function getMoveOptions(square: Square) {
const moves = game.moves({
const isDisabled = () => props.disabled || !isTurn() || chess.isGameOver();

function move(from: BoardState['moveFrom'], to: Square) {
const move = chess.move({
from,
to,
promotion: 'q',
});
if (!move) return;
boardStore.set.moveFrom('');
boardStore.set.possibleMoves({});
props.onChange?.(chess.fen(), move, chess.isGameOver());
}

function setPossibleMoves(square: Square) {
const moves = chess.moves({
square,
verbose: true,
});
Expand All @@ -29,7 +66,7 @@ export default function Board(_props: {
moves.map((move) => {
newSquares[move.to] = {
background:
game.get(move.to) && game.get(move.to).color !== game.get(square).color
chess.get(move.to) && chess.get(move.to).color !== chess.get(square).color
? 'radial-gradient(circle, rgba(0,0,0,.1) 85%, transparent 85%)'
: 'radial-gradient(circle, rgba(0,0,0,.1) 25%, transparent 25%)',
borderRadius: '50%',
Expand All @@ -39,69 +76,85 @@ export default function Board(_props: {
newSquares[square] = {
background: 'rgba(255, 255, 0, 0.4)',
};
return newSquares;

boardStore.set.possibleMoves(newSquares);
}

function deselect() {
boardStore.set.moveFrom('');
boardStore.set.possibleMoves({});
}

function select(square: Square) {
boardStore.set.moveFrom(square);
setPossibleMoves(square);
}

function onSquareClick(square: Square) {
if (_props.disabled) return;
if (moveFrom) {
if (moveFrom === square) {
setMoveFrom('');
setOptions({});
return;
}

const hasOption = options[square];

if (hasOption) {
game.move({
from: moveFrom,
to: square,
});
setMoveFrom('');
setOptions({});
_props.onChange?.(game.fen());
return;
} else if (game.get(square)) {
setMoveFrom(square);
setOptions(getMoveOptions(square));
} else {
setMoveFrom('');
setOptions({});
}
}

if (game.get(square)) {
setMoveFrom(square);
setOptions(getMoveOptions(square));
}
if (isDisabled()) return;
const piece = chess.get(square);
const isMove = board.moveFrom && board.moveFrom !== square && board.possibleMoves[square];
const isSelect =
!isMove &&
((piece.color === 'w' && props.isWhite) || (piece.color === 'b' && !props.isWhite));
const isDeselect = !isMove && !isSelect && board.moveFrom;

if (isMove) move(board.moveFrom, square);
else if (isDeselect) deselect();
else if (isSelect) select(square);
}

function onDragOverSquare(_square: Square) {
return !isDisabled();
}

function onDrop(_sSquare: Square, _tSquare: Square, _piece: Piece) {
return !isDisabled();
}

function onDragStart(_piece: Piece, _sourceSquare: Square) {
return !isDisabled();
}

function isDraggablePiece() {
return !isDisabled();
}

const lostPieces = {};
const capturedPieces = {};

return (
<div
className="board_container"
style={{
maxWidth: '80vh',
}}
>
<Chessboard
id={'board'}
animationDuration={300}
isDraggablePiece={(args) => {
if (_props.disabled) return false;
if (game.turn() === 'w' && args.piece.startsWith('w')) return true;
else if (game.turn() === 'b' && args.piece.startsWith('b')) return true;
return false;
}}
arePremovesAllowed={false}
customPieces={customPieces}
customSquare={CustomSquare}
customDarkSquareStyle={customDarkSquareStyle}
customLightSquareStyle={customLightSquareStyle}
position={game.fen()}
onSquareClick={onSquareClick}
customSquareStyles={{
...options,
}}
<div>
<PlayerStatus
side="top"
isWhite={!props.isWhite}
rating={props.opponent?.rating || 0}
picture={props.opponent?.picture || ''}
timeLeft={props.opponent?.clockms || 0}
takenPieces={lostPieces}
username={props.opponent?.username || 'Opponent'}
/>
<div className="board-container">
<Chessboard
{...DEFAULT_BOARD_PROPS}
onPieceDrop={onDrop}
onSquareClick={onSquareClick}
onPieceDragBegin={onDragStart}
onDragOverSquare={onDragOverSquare}
isDraggablePiece={isDraggablePiece}
customSquareStyles={board.possibleMoves}
position={props.fen || START_POSITION}
boardOrientation={props.isWhite ? 'white' : 'black'}
/>
</div>
<PlayerStatus
side="bottom"
rating={props.player?.rating || 0}
picture={props.player?.picture || ''}
timeLeft={props.player?.clockms || 0}
isWhite={props.isWhite}
takenPieces={capturedPieces}
username={props.player?.username || 'You'}
/>
</div>
);
Expand Down
44 changes: 44 additions & 0 deletions src/components/Board/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Move, Square } from 'chess.js';
import { CSSProperties } from 'react';
import { ChessboardProps } from 'react-chessboard/dist/chessboard/types';

export interface SanitizedChessboardProps
extends Partial<
Pick<
ChessboardProps,
| 'id'
| 'position'
| 'customSquare'
| 'customPieces'
| 'isDraggablePiece'
| 'boardOrientation'
| 'areArrowsAllowed'
| 'animationDuration'
| 'arePremovesAllowed'
| 'arePiecesDraggable'
| 'customSquareStyles'
| 'customDarkSquareStyle'
| 'customLightSquareStyle'
>
> {}

export type BoardProps = {
fen?: string;
isWhite?: boolean;
player?: Player;
opponent?: Player;
disabled?: boolean;
onChange?: (fen: string, move: Move, isGameOver: boolean) => void;
};

export type Player = Partial<{
rating: number;
picture: string;
clockms: number;
username: string;
}>;

export type BoardState = {
possibleMoves: { [key in Square]?: CSSProperties };
moveFrom: Square | '';
};
14 changes: 14 additions & 0 deletions src/components/Board/styles.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
.board-container {
width: 90vw;
max-width: calc(80vh);
margin: 0 auto;
user-select: none;
}

@media screen and (min-width: 768px) {
.board-container {
max-width: calc(70vh);
width: 70vw;
margin: 0 auto;
}
}
13 changes: 13 additions & 0 deletions src/components/Board/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
export function getFormatedTime(time: number) {
const formated = new Date(0);
formated.setMilliseconds(time);
return formated.toISOString().substring(14, 19);
}
export const PiecesCount = {
k: 1,
q: 1,
b: 2,
n: 2,
r: 2,
p: 8,
};
Loading

0 comments on commit b0c3bb3

Please sign in to comment.