212 lines
4.6 KiB
TypeScript
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 };
|