QuickChess/src/index.js

689 lines
17 KiB
JavaScript
Raw Normal View History

2020-12-27 20:11:50 -05:00
import React from 'react';
import ReactDOM from 'react-dom';
import Popup from './components/Popup';
2020-12-27 20:11:50 -05:00
import './index.css';
2020-12-29 16:38:52 -05:00
const BLACK = 0;
const WHITE = 1;
const PAWN = 0;
const ROOK = 1;
const KNIGHT = 2;
const BISHOP = 3;
const QUEEN = 4;
const KING = 5;
2020-12-27 23:58:51 -05:00
const Images = [
'./white_pawn.svg',
'./white_rook.svg',
'./white_knight.svg',
'./white_bishop.svg',
'./white_queen.svg',
'./white_king.svg',
'./black_pawn.svg',
'./black_rook.svg',
'./black_knight.svg',
'./black_bishop.svg',
'./black_queen.svg',
'./black_king.svg',
];
function range(n) {
return Array.from(Array(n).keys());
}
function isBlack(piece) {
2020-12-29 16:38:52 -05:00
return piece != null && piece.color === BLACK;
}
function isWhite(piece) {
2020-12-29 16:38:52 -05:00
return piece != null && piece.color === WHITE;
}
function isPawn(piece) {
2020-12-29 16:38:52 -05:00
return piece != null && piece.type === PAWN;
}
function isRook(piece) {
2020-12-29 16:38:52 -05:00
return piece != null && piece.type === ROOK;
}
function isBishop(piece) {
2020-12-29 16:38:52 -05:00
return piece != null && piece.type === BISHOP;
}
function isQueen(piece) {
2020-12-29 16:38:52 -05:00
return piece != null && piece.type === QUEEN;
}
function isKnight(piece) {
2020-12-29 16:38:52 -05:00
return piece != null && piece.type === KNIGHT;
}
function isKing(piece) {
2020-12-29 16:38:52 -05:00
return piece != null && piece.type === KING;
}
function imageFromPiece(piece) {
if (piece) {
2020-12-29 16:55:35 -05:00
const image = piece.color === WHITE ? piece.type : piece.type + 6;
2020-12-29 16:38:52 -05:00
return Images[image];
}
return null;
}
2020-12-27 23:58:51 -05:00
function Square(props) {
2020-12-28 22:51:01 -05:00
return (
<button
className="square"
onClick={props.onClick}
style={{
2020-12-29 16:38:52 -05:00
backgroundImage: `url(${imageFromPiece(props.piece)})`,
2020-12-28 22:51:01 -05:00
backgroundSize: `100%`,
backgroundColor: props.bgColor,
}}
title={props.piece == null ? "" : props.piece.getInfoText()}
2020-12-28 22:51:01 -05:00
>
</button>
);
2020-12-27 20:11:50 -05:00
}
2020-12-29 16:38:52 -05:00
class Piece {
constructor(color, type) {
this.color = color;
this.type = type;
this.passantable = false;
this.moves = 0;
2020-12-29 16:38:52 -05:00
}
setType(type) {
this.type = type;
}
getInfoText() {
if(this.moves === 1) {
return "Has made 1 move"
} else {
return "Has made " + this.moves + " moves"
}
}
2020-12-29 16:38:52 -05:00
}
2020-12-27 20:11:50 -05:00
class Board extends React.Component {
2020-12-27 23:58:51 -05:00
constructor(props) {
super(props);
this.state = (props && props.text) ?
{
squares: this.stateFromText(props.text),
blackIsNext: true,
hand: {
heldPiece: null,
},
} : this.originalState();
2020-12-27 23:58:51 -05:00
}
setHand(hand) {
this.setState({
squares: this.state.squares,
blackIsNext: this.state.blackIsNext,
hand: hand,
});
}
clone() {
2020-12-30 13:48:53 -05:00
let board = new Board();
board.state.squares = this.state.squares.slice();
board.state.blackIsNext = this.state.blackIsNext;
board.state.hand = {
heldPiece: this.state.hand.heldPiece,
};
return board;
}
stateFromText(text) {
text = text.replace(/[\n]+/g, '');
const squares = text.substring(1);
return {
hand: null,
blackIsNext: text[0].toUpperCase() === 'B',
squares: squares.split('').map(c => {
const type = c.toLowerCase();
const color = c === type ? WHITE : BLACK;
switch (type) {
case 'r':
return new Piece(color, ROOK);
case 'n':
return new Piece(color, KNIGHT);
case 'b':
return new Piece(color, BISHOP);
case 'q':
return new Piece(color, QUEEN);
case 'k':
return new Piece(color, KING);
case 'p':
return new Piece(color, PAWN);
default:
return null;
}
}),
};
}
shuffledBackRow() {
return "rnbqkbnr".split('').sort(() => Math.random() - 0.5).join('');
}
resetWithShuffledBackRow() {
const backRow = this.shuffledBackRow();
const text = ["B", backRow, "pppppppp",
"________", "________", "________", "________",
"PPPPPPPP", backRow.toUpperCase()].join('');
this.setState(this.stateFromText(text));
}
textFromState() {
const turn = (this.state.blackIsNext? 'B' : 'W');
return turn + this.state.squares.map(square => {
if (!square) {
return '_';
}
let color = (c) => {
return square.color === BLACK ? c.toUpperCase() : c;
};
switch (square.type) {
case ROOK:
return color('r');
case KNIGHT:
return color('n');
case BISHOP:
return color('b');
case QUEEN:
return color('q');
case KING:
return color('k');
case PAWN:
return color('p');
default:
return '_';
}
}).join('');;
}
doReset() {
this.setState(this.originalState());
}
originalState() {
2020-12-30 13:48:53 -05:00
let squares = [];
const mainRow = [ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK];
2020-12-30 10:08:18 -05:00
function add(num, color, type) {
2020-12-30 13:48:53 -05:00
for(let i = 0; i < num; i++) {
if(color != null && type != null) {
squares.push(new Piece(color, type));
} else {
squares.push(null);
}
2020-12-30 10:08:18 -05:00
}
}
2020-12-30 13:48:53 -05:00
mainRow.forEach(type => add(1, WHITE, type));
2020-12-30 10:08:18 -05:00
add(8, WHITE, PAWN);
2020-12-30 13:48:53 -05:00
add(32, null, null);
2020-12-30 10:08:18 -05:00
add(8, BLACK, PAWN);
2020-12-30 13:48:53 -05:00
mainRow.forEach(type => add(1, BLACK, type));
2020-12-29 16:55:35 -05:00
return ({
squares,
blackIsNext: true,
hand: {
heldPiece: null,
},
showPopup: false,
});
2020-12-27 23:58:51 -05:00
}
getXandY(i) {
2020-12-30 13:48:53 -05:00
const x = i % 8;
const y = Math.floor(i / 8);
return [x, y];
}
2020-12-30 10:08:18 -05:00
getIndex(x, y) {
return x + (y * 8);
}
isValidXY(x, y) {
return x < 8 && x >=0 && y < 8 && y >= 0;
}
2020-12-30 13:48:53 -05:00
squareCount() {
return this.state.squares.length;
}
squareAt(i) {
return i >= 0 && i < 64 ? this.state.squares[i] : null;
}
2020-12-28 22:51:01 -05:00
pieceAt(x, y) {
2020-12-30 13:48:53 -05:00
if (this.isValidXY(x, y)) {
2020-12-30 10:08:18 -05:00
return this.state.squares[this.getIndex(x, y)];
} else {
return null;
}
}
whiteAt(x, y) {
2020-12-30 13:48:53 -05:00
const square = this.pieceAt(x, y);
if (square == null) {
return false;
}
2020-12-28 22:51:01 -05:00
return isWhite(square);
}
blackAt(x, y) {
2020-12-30 13:48:53 -05:00
const square = this.pieceAt(x, y);
if (square == null) {
return false;
}
2020-12-28 22:51:01 -05:00
return isBlack(square);
}
isEnemyOf(piece, x, y) {
2020-12-30 13:48:53 -05:00
return isBlack(piece) ? this.whiteAt(x, y) : this.blackAt(x, y);
}
getValidMovesAt(piece, x, y) {
2020-12-30 13:48:53 -05:00
let moves = [];
const tryAddMove = (x, y) => {
2020-12-30 10:08:18 -05:00
if (this.isValidXY(x, y)) {
if(this.pieceAt(x, y) == null) {
2020-12-30 13:48:53 -05:00
moves.push({x, y});
2020-12-30 10:08:18 -05:00
// Keep searching
return 0;
} else if (this.isEnemyOf(piece, x, y)) {
2020-12-30 13:48:53 -05:00
moves.push({x, y});
2020-12-30 10:08:18 -05:00
}
// Stop searching
return 1;
}
};
2020-12-30 13:48:53 -05:00
function addBunch(xFunc, yFunc, isUp) {
for (let i = 1; i < 8; i++) {
if(tryAddMove(xFunc(i), yFunc(i)) !== 0) {
break;
}
}
}
if (isPawn(piece)) {
const pieceIsBlack = isBlack(piece);
const shift = pieceIsBlack ? -1 : 1;
const startLine = pieceIsBlack ? 6 : 1;
// Check for en passant
const left = this.pieceAt(x - 1, y);
const right = this.pieceAt(x + 1, y);
if (left != null && left.passantable && left.color !== piece.color) {
2020-12-30 13:48:53 -05:00
moves.push({x: x - 1, y: y + shift, passant: {x: x - 1, y}})
}
if (right != null && right.passantable && right.color !== piece.color) {
2020-12-30 13:48:53 -05:00
moves.push({x: x + 1, y: y + shift, passant: {x: x + 1, y}})
}
2020-12-28 22:51:01 -05:00
if (this.pieceAt(x, y + shift) == null) {
2020-12-30 13:48:53 -05:00
moves.push({x, y: y + shift});
// Pawn moving two spaces becomes en-passantable
2020-12-28 22:51:01 -05:00
if (y === startLine && this.pieceAt(x, y + (shift * 2)) == null) {
2020-12-30 13:48:53 -05:00
moves.push({x, y: y + (shift * 2), passantable: true});
2020-12-28 18:01:05 -05:00
}
}
2020-12-29 16:38:52 -05:00
[x + 1, x - 1].forEach(x => {
2020-12-30 10:08:18 -05:00
if (this.isValidXY(x, y + shift) && this.isEnemyOf(piece, x, y + shift)) {
2020-12-30 13:48:53 -05:00
moves.push({x, y: y + shift});
2020-12-28 18:01:05 -05:00
}
});
} else if (isRook(piece)) {
2020-12-30 13:48:53 -05:00
addBunch(n => {return x;}, n => {return y + n;});
addBunch(n => {return x;}, n => {return y - n;});
addBunch(n => {return x + n;}, n => {return y;});
addBunch(n => {return x - n;}, n => {return y;});
} else if (isBishop(piece)) {
2020-12-30 13:48:53 -05:00
addBunch(n => {return x + n;}, n => {return y + n;});
addBunch(n => {return x - n;}, n => {return y + n;});
addBunch(n => {return x + n;}, n => {return y - n;});
addBunch(n => {return x - n;}, n => {return y - n;});
} else if (isQueen(piece)) {
const [rook, bishop] =
2020-12-30 13:48:53 -05:00
[new Piece(piece.color, ROOK), new Piece(piece.color, BISHOP)];
moves = moves.concat(this.getValidMovesAt(rook, x, y));
moves = moves.concat(this.getValidMovesAt(bishop, x, y));
} else if (isKnight(piece)) {
[
[2, 1], [2, -1], [-2, 1], [-2, -1],
[1, 2], [1, -2], [-1, 2], [-1, -2],
].forEach(delta => tryAddMove(x + delta[0], y + delta[1]));
} else if (isKing(piece)) {
[[1, 1], [1, -1], [-1, 1], [-1, -1], [0, 1], [0, -1], [1, 0], [-1, 0]]
.forEach(delta => tryAddMove(x + delta[0], y + delta[1]));
if (piece.moves === 0) {
2020-12-30 22:50:07 -05:00
const kingIndex = this.findIndex(piece);
const [x, y] = this.getXandY(kingIndex);
let leftRook = this.pieceAt(0, y);
if(isRook(leftRook) && leftRook.moves === 0) {
// Check if spaces between rook and king are empty
2020-12-30 22:50:07 -05:00
if(this.pieceAt(1, y) == null &&
this.pieceAt(2, y) == null &&
this.pieceAt(3, y) == null) {
// Check if between space puts king in check
let board = this.clone();
board.state.squares[board.getIndex(x - 1, y)] = piece;
board.state.squares[kingIndex] = null;
if(board.inCheck(piece) == null) {
moves.push({x: x - 2, y, castle: [x - 1, y]});
}
}
}
2020-12-30 22:50:07 -05:00
let rightRook = this.pieceAt(7, y);
if(isRook(rightRook) && rightRook.moves === 0) {
// Check if spaces between rook and king are empty
2020-12-30 22:50:07 -05:00
if(this.pieceAt(5, y) == null &&
this.pieceAt(6, y) == null) {
// Check if between space puts king in check
let board = this.clone();
board.state.squares[board.getIndex(x + 1, y)] = piece;
board.state.squares[kingIndex] = null;
if(board.inCheck(piece) == null) {
moves.push({x: x + 2, y, castle: [x + 1, y]});
}
}
}
}
}
return moves;
}
2020-12-29 16:38:52 -05:00
findIndex(piece) {
2020-12-30 13:48:53 -05:00
for(let i = 0; i < this.squareCount(); i++) {
const check = this.state.squares[i];
2020-12-29 16:38:52 -05:00
if(check && check.type === piece.type && check.color === piece.color) {
return i;
2020-12-28 19:21:13 -05:00
}
}
2020-12-29 16:38:52 -05:00
return null;
}
distanceBetween(i1, i2) {
const [pos1X, pos1Y] = this.getXandY(i1);
const [pos2X, pos2Y] = this.getXandY(i2);
2020-12-29 16:38:52 -05:00
2020-12-30 13:48:53 -05:00
let a = pos1X - pos2X;
2020-12-29 16:38:52 -05:00
a = a * a;
2020-12-30 13:48:53 -05:00
let b = pos1Y - pos2Y;
2020-12-29 16:38:52 -05:00
b = b * b;
return Math.sqrt(a + b);
}
inCheck(piece) {
const kingPos = this.getXandY(this.findIndex(piece));
2020-12-28 19:21:13 -05:00
2020-12-30 13:48:53 -05:00
for(let i = 0; i < this.squareCount(); i++) {
const [x, y] = this.getXandY(i);
2020-12-29 08:25:17 -05:00
if(this.isEnemyOf(piece, x, y)) {
const moves = this.getValidMoves(i);
2020-12-30 13:48:53 -05:00
for(let j = 0; j < moves.length; j++) {
if(moves[j].x === kingPos[0] && moves[j].y === kingPos[1]) {
2020-12-29 08:25:17 -05:00
return piece;
}
2020-12-28 19:21:13 -05:00
}
}
}
2020-12-28 22:51:01 -05:00
return null;
2020-12-28 19:21:13 -05:00
}
2020-12-29 08:25:17 -05:00
whoInCheck() {
const blackKing = this.inCheck(new Piece(BLACK, KING));
2020-12-29 16:38:52 -05:00
return blackKing ? blackKing : this.inCheck(new Piece(WHITE, KING));
2020-12-29 08:25:17 -05:00
}
2020-12-28 19:21:13 -05:00
checkmate() {
const checkedKing = this.whoInCheck();
2020-12-29 08:25:17 -05:00
if (checkedKing != null) {
// For each square
2020-12-30 13:48:53 -05:00
for(let i = 0; i < this.squareCount(); i++) {
const piece = this.squareAt(i);
2020-12-29 08:25:17 -05:00
// If that piece is on the checked team
if (piece != null && isBlack(piece) === isBlack(checkedKing)) {
// For each move of the above piece
const moves = this.getValidMoves(i)
for(const move of moves) {
// Copy the board
const board = this.clone();
board.state.squares[board.getIndex(move.x, move.y)] = board.state.squares[i];
board.state.squares[i] = null;
const check = board.inCheck(checkedKing);
if (check == null || check.color !== checkedKing.color) {
2020-12-29 08:25:17 -05:00
return false;
}
}
}
}
return true;
}
2020-12-28 19:21:13 -05:00
return false;
}
getValidMoves(source) {
const [x, y] = this.getXandY(source);
const piece = this.squareAt(source);
return this.getValidMovesAt(piece, x, y);
}
isValidMove(source, dest) {
const [destX, destY] = this.getXandY(dest);
for (const move of this.getValidMoves(source)) {
2020-12-30 10:08:18 -05:00
if (destX === move.x && destY === move.y) {
return move;
}
}
return null;
}
isHoldingPiece() {
return this.heldPiece() != null;
}
heldPiece() {
return (this.state && this.state.hand) ? this.state.hand.heldPiece : null;
}
2020-12-29 08:25:17 -05:00
makeMove(from, to) {
2020-12-27 23:58:51 -05:00
const squares = this.state.squares.slice();
const move = this.isValidMove(from, to)
if (move) {
if (move.passant) {
squares[this.getIndex(move.passant.x, move.passant.y)] = null;
}
2020-12-30 22:50:07 -05:00
if (move.castle) {
// .castle holds the position where the rook should end up
// King moved left
const rookX = move.castle[0] > move.x ? 0 : 7;
console.log("Replace ");
console.log(move.castle);
console.log("With ");
console.log([rookX, move.castle[1]]);
squares[this.getIndex(move.castle[0], move.castle[1])] =
squares[this.getIndex(rookX, move.castle[1])];
squares[this.getIndex(rookX, move.castle[1])] = null;
}
// Remove existing passantable states
squares.forEach(square => {
if (square) {
square.passantable = false;
}
});
if (move.passantable) {
squares[from].passantable = true;
}
const y = this.getXandY(to)[1];
2020-12-29 08:25:17 -05:00
squares[to] = squares[from];
squares[from] = null;
if (squares[to].type === PAWN && (y === 0 || y === 7)) {
squares[to].setType(QUEEN);
}
squares[to].moves++;
2020-12-29 08:25:17 -05:00
this.setState({
squares: squares,
blackIsNext: !this.state.blackIsNext,
hand: {
heldPiece: null,
}
2020-12-29 08:25:17 -05:00
});
return 0;
}
return 1;
}
handleClick(i) {
if (this.checkmate()) {
return;
}
if (this.isHoldingPiece()) {
// Copy the board
2020-12-30 13:48:53 -05:00
let board = this.clone();
board.state.squares[i] = board.state.squares[board.heldPiece()];
board.state.squares[this.heldPiece()] = null;
const moversKing = this.state.blackIsNext ?
new Piece(BLACK, KING) : new Piece(WHITE, KING);
if (board.inCheck(moversKing) != null) {
return;
}
2020-12-29 08:25:17 -05:00
if (this.makeMove(this.heldPiece(), i) !== 0) {
this.setHand({
heldPiece: null,
});
}
} else if (this.state.squares[i] != null) {
const isSquareBlack = isBlack(this.state.squares[i]);
if(isSquareBlack === this.state.blackIsNext) {
this.setHand({
heldPiece: i,
});
}
2020-12-27 23:58:51 -05:00
}
}
2020-12-27 20:11:50 -05:00
renderSquare(i) {
const plainBg = (i + (Math.floor(i / 8))) % 2 === 0 ? "white" : "#666";
const bgColor = this.heldPiece() === i ? "#5D98E6" : plainBg;
2020-12-27 23:58:51 -05:00
return (
<Square
2020-12-30 13:48:53 -05:00
key={"square-" + i}
2020-12-29 16:38:52 -05:00
piece={this.state.squares[i]}
2020-12-27 23:58:51 -05:00
onClick={() => this.handleClick(i)}
2020-12-28 22:51:01 -05:00
bgColor={bgColor}
2020-12-27 23:58:51 -05:00
/>
);
}
2020-12-30 13:48:53 -05:00
row(r) {
const i = r * 8;
2020-12-27 23:58:51 -05:00
return (
<div className="board-row" key={"row=" + r}>
{range(8).map(n => this.renderSquare(n + i))}
</div>
2020-12-27 23:58:51 -05:00
);
2020-12-27 20:11:50 -05:00
}
togglePopup() {
this.setState({
showPopup: !this.state.showPopup
});
}
renderPopup() {
return (this.state.showPopup ?
<Popup
header='QuickChess'
closePopup={this.togglePopup.bind(this)}
body={<div>
<p>This is a simple implementation of the classic board game. It
supports all possible moves, including castling, and <em>en
passant</em>.</p>
<button onClick={
this.resetWithShuffledBackRow.bind(this)
}>Shuffled Back Row</button>
<button onClick={this.doReset.bind(this)}>Reset Game</button>
</div>}
/>
: null
);
}
/* Board class can't (always) include a settings button if used as a demo. */
2020-12-27 20:11:50 -05:00
render() {
const checkMsg = this.whoInCheck() ? "Check! " : "";
const isCheckmate = this.checkmate();
const namedPlayer = isCheckmate ?
!this.state.blackIsNext : this.state.blackIsNext
const color = namedPlayer ? 'Black' : 'White';
const status = isCheckmate ? "Checkmate! " + color + " Wins!" :
checkMsg + color + "'s Turn";
2020-12-27 20:11:50 -05:00
return (
<div style={{textAlign: `center`,}}>
<div className="status">
<h1 style={{display: "inline-block"}}>{status}</h1>
<button
onClick={this.togglePopup.bind(this)}>
Settings
</button>
</div>
{range(8).map(n => this.row(n))}
{this.renderPopup()}
2020-12-27 20:11:50 -05:00
</div>
);
}
}
class Game extends React.Component {
constructor(props){
super(props);
this.state = { showPopup: false };
}
2020-12-27 20:11:50 -05:00
render() {
return (
<div className="game">
<div className="game-board">
<Board />
</div>
2020-12-27 20:11:50 -05:00
</div>
);
}
}
// ========================================
ReactDOM.render(
<Game />,
document.getElementById('root')
);