chess-board/src/Board.tsx

212 lines
4.6 KiB
TypeScript

import React from "react";
type COLOR = "black" | "white";
type PIECE = "pawn" | "rook" | "knight" | "bishop" | "queen" | "king";
type FILE = "a" | "b" | "c" | "d" | "e" | "f" | "g" | "h";
type RANK = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
type Coordinates = {
rank: RANK;
file: FILE;
};
type PieceState = {
kind: PIECE;
color: COLOR;
};
type Selected = {
piece: PieceState;
location: Coordinates;
};
type OnSelectPiece = (selected: Selected) => Promise<void>;
type OnMovePiece = (
selected: Selected,
destination: Coordinates
) => Promise<void>;
type OnSelectDestination = (destination: Coordinates) => Promise<void>;
interface BoardProps {
playerColor: COLOR;
boardState: BoardState;
onSelectPiece: OnSelectPiece;
onMovePiece: OnMovePiece;
}
interface CellProps {
boardState: BoardState;
playerColor: COLOR;
rank: RANK;
file: FILE;
selected: Selected | null;
onSelectPiece: OnSelectPiece;
onSelectDestination: OnSelectDestination;
}
export type FileState = {
[key in RANK]?: PieceState;
};
export type BoardState = {
[key in FILE]?: FileState;
};
const RANKS: RANK[] = [1, 2, 3, 4, 5, 6, 7, 8];
const FILES: FILE[] = ["a", "b", "c", "d", "e", "f", "g", "h"];
const PIECES: PIECE[] = ["pawn", "rook", "knight", "bishop", "queen", "king"];
const COLORS: COLOR[] = ["white", "black"];
const iconFor = ({ kind, color }: PieceState): string => {
switch (kind) {
case "pawn":
return color === "white" ? "♙" : "♟︎";
case "rook":
return color === "white" ? "♖" : "♜";
case "knight":
return color === "white" ? "♘" : "♞";
case "bishop":
return color === "white" ? "♗" : "♝";
case "queen":
return color === "white" ? "♕" : "♛";
case "king":
return color === "white" ? "♔" : "♚";
}
};
const Cell = ({
boardState,
playerColor,
rank,
file,
selected,
onSelectDestination,
onSelectPiece,
}: CellProps): JSX.Element => {
const filed = boardState[file];
const onEmptyClick = async () => {
return await onSelectDestination({ rank, file });
};
if (typeof filed === "undefined") {
return <td className="icon-cell" onClick={onEmptyClick}></td>;
}
const piece = filed[rank];
if (typeof piece === "undefined") {
return <td className="icon-cell" onClick={onEmptyClick}></td>;
}
const onClick = async () => {
const selected: Selected = {
piece: { ...piece },
location: {
file,
rank,
},
};
return await onSelectPiece(selected);
};
const maybeOnClick = piece.color === playerColor ? onClick : undefined;
const classNames =
selected !== null &&
selected.piece.kind === piece.kind &&
selected.piece.color === piece.color &&
selected.location.file === file &&
selected.location.rank === rank
? ["icon-cell", "selected"]
: ["icon-cell"];
return (
<td className={classNames.join(" ")} onClick={maybeOnClick}>
<span className="icon">{iconFor(piece)}</span>
</td>
);
};
const Board = ({
boardState,
onMovePiece,
onSelectPiece: superOnSelectPiece,
playerColor,
}: BoardProps): JSX.Element => {
const [selected, setSelected] = React.useState<Selected | null>(null);
const onSelectPiece: OnSelectPiece = async (selected) => {
setSelected(selected);
return await superOnSelectPiece(selected);
};
const onSelectDestination = async (
destination: Coordinates
): Promise<void> => {
if (selected === null) {
return;
}
const output = await onMovePiece(selected, destination);
setSelected(null);
return output;
};
const files = [...FILES];
const ranks = [...RANKS];
if (playerColor === "white") {
ranks.reverse();
} else {
files.reverse();
}
return (
<table className="chess-board">
<tr>
<td></td>
{files.map((file) => (
<td key={file}>{file.toUpperCase()}</td>
))}
<td></td>
</tr>
{ranks.map((rank) => (
<tr key={rank}>
<td>{rank}</td>
{files.map((file) => (
<Cell
key={file}
boardState={boardState}
playerColor={playerColor}
file={file}
rank={rank}
selected={selected}
onSelectDestination={onSelectDestination}
onSelectPiece={onSelectPiece}
/>
))}
<td>{rank}</td>
</tr>
))}
<tr>
<td></td>
{files.map((file) => (
<td key={file}>{file.toUpperCase()}</td>
))}
<td></td>
</tr>
</table>
);
};
export default Board;
export { PIECES, COLORS, FILES, RANKS };