Split into multiple files, add eslint, prettier

This commit is contained in:
Aode (lion) 2021-12-30 10:30:37 -06:00
parent 2dcf6da9e6
commit f417229526
11 changed files with 9135 additions and 367 deletions

4
.eslintignore Normal file
View file

@ -0,0 +1,4 @@
# don't ever lint node_modules
node_modules
# don't lint build output (make sure it's set to your correct build folder name)
dist

11
.eslintrc.js Normal file
View file

@ -0,0 +1,11 @@
module.exports = {
root: true,
parser: '@typescript-eslint/parser',
plugins: [
'@typescript-eslint',
],
extends: [
'eslint:recommended',
'plugin:@typescript-eslint/recommended',
],
};

3
.prettierignore Normal file
View file

@ -0,0 +1,3 @@
# Ignore artifacts:
build
coverage

1
.prettierrc.json Normal file
View file

@ -0,0 +1 @@
{}

View file

@ -39,5 +39,11 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.8.1",
"@typescript-eslint/parser": "^5.8.1",
"eslint": "^8.5.0",
"prettier": "2.5.1"
}
}

View file

@ -1,100 +1,5 @@
import { validateBoardState } from './App';
test('it works', () => {
expect(2 + 2).toEqual(4);
})
test('validates valid states', () => {
const inputs = [
{
"a": {
3: {
kind: "king",
color: "black",
}
}
},
{
"b": {
5: {
kind: "rook",
color: "white",
}
}
},
{
"c": {
8: {
kind: "bishop",
color: "black",
}
},
"e": {
4: {
kind: "pawn",
color: "white",
},
5: {
kind: "knight",
color: "white",
},
7: {
kind: "queen",
color: "black",
}
}
}
];
inputs.map((input) => {
const result = validateBoardState(input);
expect(result).not.toBeNull();
});
});
test('does not validate invalid states', () => {
const inputs = [
{
"j": {
3: {
kind: "king",
color: "black",
}
}
},
{
"b": {
9: {
kind: "rook",
color: "white",
}
}
},
{
"c": {
8: {
kind: "bishop",
color: "black",
}
},
"e": {
4: {
kind: "pawn",
color: "white",
},
5: {
type: "knight",
color: "white",
},
7: {
kind: "queen",
color: "black",
}
}
}
];
inputs.map((input) => {
const result = validateBoardState(input);
expect(result).toBeNull();
});
});
export { }

View file

@ -1,272 +1,6 @@
import React from "react";
import Board, { BoardState } from "./Board";
import './App.css';
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 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;
}
type PieceState = {
kind: PIECE;
color: COLOR;
}
type Selected = {
piece: PieceState,
location: Coordinates,
}
type FileState = {
[key in RANK]?: PieceState;
}
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 COLORS_AS_STRINGS: string[] = COLORS.map((color) => color as string);
const PIECES_AS_STRINGS: string[] = PIECES.map((piece) => piece as string);
const FILES_AS_STRINGS: string[] = FILES.map((file) => file as string);
const RANKS_AS_NUMBERS: number[] = RANKS.map((rank) => rank as number);
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 validateBoardState = (input: unknown): BoardState | null => {
if (typeof (input) !== "object" || Array.isArray(input) || input === null) {
console.error("Input is not keyed object");
return null;
}
const hasInvalidShape = Object.entries(input).some(([file, ranks]) => {
if (typeof (file) !== "string") {
console.error("file is not string");
return true;
}
if (!FILES_AS_STRINGS.includes(file)) {
console.error("file is not included in valid files")
return true;
}
if (typeof (ranks) !== "object" || Array.isArray(ranks) || ranks === null) {
console.error("ranks is not a keyed object");
return true;
}
return Object.entries(ranks).some(([rank, pieces]) => {
if (typeof (rank) !== "string") {
console.error("rank is not a string");
return true;
}
if (!RANKS_AS_NUMBERS.includes(Number(rank))) {
console.error("rank is not included in valid ranks");
return true;
}
if (typeof (pieces) !== "object" || Array.isArray(pieces) || pieces === null) {
console.error("piece state is not a keyed object");
return true;
}
return Object.entries(pieces).some(([key, value]) => {
if (typeof (key) !== "string") {
console.error("piece key is not a string");
return true;
}
if (!["kind", "color"].includes(key)) {
console.error("piece key is not included in valid keys")
return true;
}
if (key === "kind" && (typeof (value) !== "string" || !PIECES_AS_STRINGS.includes(value))) {
console.error("kind's value is not a string, or not a valid piece");
return true;
} else if (key === "color" && (typeof (value) !== "string" || !COLORS_AS_STRINGS.includes(value))) {
console.error("colors' value is not a string, or not a valid color");
return true;
}
return false;
});
});
});
if (hasInvalidShape) {
return null;
}
return input as BoardState;
}
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>
);
}
const App = (): JSX.Element => {
const boardState: BoardState = {
"a": {
@ -311,4 +45,3 @@ const App = (): JSX.Element => {
}
export default App;
export { validateBoardState };

198
src/Board.tsx Normal file
View file

@ -0,0 +1,198 @@
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;
}
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,
};

99
src/validate.test.ts Normal file
View file

@ -0,0 +1,99 @@
import { validateBoardState } from './validate';
test('validates valid states', () => {
const inputs = [
{
"a": {
3: {
kind: "king",
color: "black",
}
}
},
{
"b": {
5: {
kind: "rook",
color: "white",
}
}
},
{
"c": {
8: {
kind: "bishop",
color: "black",
}
},
"e": {
4: {
kind: "pawn",
color: "white",
},
5: {
kind: "knight",
color: "white",
},
7: {
kind: "queen",
color: "black",
}
}
}
];
inputs.map((input) => {
const result = validateBoardState(input);
expect(result).not.toBeNull();
});
});
test('does not validate invalid states', () => {
const inputs = [
{
"j": {
3: {
kind: "king",
color: "black",
}
}
},
{
"b": {
9: {
kind: "rook",
color: "white",
}
}
},
{
"c": {
8: {
kind: "bishop",
color: "black",
}
},
"e": {
4: {
kind: "pawn",
color: "white",
},
5: {
type: "knight",
color: "white",
},
7: {
kind: "queen",
color: "black",
}
}
}
];
inputs.map((input) => {
const result = validateBoardState(input);
expect(result).toBeNull();
});
});

79
src/validate.ts Normal file
View file

@ -0,0 +1,79 @@
import { COLORS, PIECES, FILES, RANKS, BoardState } from "./Board";
const COLORS_AS_STRINGS: string[] = COLORS.map((color) => color as string);
const PIECES_AS_STRINGS: string[] = PIECES.map((piece) => piece as string);
const FILES_AS_STRINGS: string[] = FILES.map((file) => file as string);
const RANKS_AS_NUMBERS: number[] = RANKS.map((rank) => rank as number);
const validateBoardState = (input: unknown): BoardState | null => {
if (typeof (input) !== "object" || Array.isArray(input) || input === null) {
console.error("Input is not keyed object");
return null;
}
const hasInvalidShape = Object.entries(input).some(([file, ranks]) => {
if (typeof (file) !== "string") {
console.error("file is not string");
return true;
}
if (!FILES_AS_STRINGS.includes(file)) {
console.error("file is not included in valid files")
return true;
}
if (typeof (ranks) !== "object" || Array.isArray(ranks) || ranks === null) {
console.error("ranks is not a keyed object");
return true;
}
return Object.entries(ranks).some(([rank, pieces]) => {
if (typeof (rank) !== "string") {
console.error("rank is not a string");
return true;
}
if (!RANKS_AS_NUMBERS.includes(Number(rank))) {
console.error("rank is not included in valid ranks");
return true;
}
if (typeof (pieces) !== "object" || Array.isArray(pieces) || pieces === null) {
console.error("piece state is not a keyed object");
return true;
}
return Object.entries(pieces).some(([key, value]) => {
if (typeof (key) !== "string") {
console.error("piece key is not a string");
return true;
}
if (!["kind", "color"].includes(key)) {
console.error("piece key is not included in valid keys")
return true;
}
if (key === "kind" && (typeof (value) !== "string" || !PIECES_AS_STRINGS.includes(value))) {
console.error("kind's value is not a string, or not a valid piece");
return true;
} else if (key === "color" && (typeof (value) !== "string" || !COLORS_AS_STRINGS.includes(value))) {
console.error("colors' value is not a string, or not a valid color");
return true;
}
return false;
});
});
});
if (hasInvalidShape) {
return null;
}
return input as BoardState;
};
export {
validateBoardState,
};

8729
yarn.lock Normal file

File diff suppressed because it is too large Load diff