mirror of
https://github.com/danbee/chess
synced 2025-03-04 08:39:06 +00:00
Show game state on the board
This commit is contained in:
parent
6b31491ad4
commit
5556de00b0
@ -49,6 +49,35 @@
|
|||||||
&.player-is-black.white-to-move::before {
|
&.player-is-black.white-to-move::before {
|
||||||
top: 2%;
|
top: 2%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.board-rank-labels,
|
||||||
|
.board-file-labels {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.player-is-white {
|
||||||
|
.board-rank-labels {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-file-labels {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.player-is-black {
|
||||||
|
.board-rank-labels {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.board-file-labels {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row-reverse;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.board-rank-labels {
|
.board-rank-labels {
|
||||||
@ -59,26 +88,6 @@
|
|||||||
width: 5%;
|
width: 5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.board.player-is-white {
|
|
||||||
.board-rank-labels {
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
}
|
|
||||||
|
|
||||||
.board-file-labels {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.board.player-is-black {
|
|
||||||
.board-rank-labels {
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
|
|
||||||
.board-file-labels {
|
|
||||||
flex-direction: row-reverse;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.board-file-labels {
|
.board-file-labels {
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 5%;
|
height: 5%;
|
||||||
@ -87,6 +96,14 @@
|
|||||||
width: 90%;
|
width: 90%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.board-game-state {
|
||||||
|
bottom: 0;
|
||||||
|
color: white;
|
||||||
|
height: 5%;
|
||||||
|
position: absolute;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
|
||||||
.board-label {
|
.board-label {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import { connect } from "react-redux";
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
|
|
||||||
import ChessBoardSquare from "./chess-board-square";
|
import ChessBoardSquare from "./chess-board-square";
|
||||||
|
import RankLabels from "./rank-labels";
|
||||||
|
import FileLabels from "./file-labels";
|
||||||
|
|
||||||
class ChessBoard extends React.Component {
|
class ChessBoard extends React.Component {
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
@ -72,6 +74,12 @@ class ChessBoard extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get gameState() {
|
||||||
|
const { store } = this.props;
|
||||||
|
console.log(store.getState().state);
|
||||||
|
return store.getState().state;
|
||||||
|
}
|
||||||
|
|
||||||
get boardClass() {
|
get boardClass() {
|
||||||
const turn = this.getTurn();
|
const turn = this.getTurn();
|
||||||
const player = this.getPlayer();
|
const player = this.getPlayer();
|
||||||
@ -79,43 +87,19 @@ class ChessBoard extends React.Component {
|
|||||||
return classNames("board", turn + "-to-move", "player-is-" + player);
|
return classNames("board", turn + "-to-move", "player-is-" + player);
|
||||||
}
|
}
|
||||||
|
|
||||||
get rankLabels() {
|
|
||||||
return [1, 2, 3, 4, 5, 6, 7, 8];
|
|
||||||
}
|
|
||||||
|
|
||||||
get fileLabels() {
|
|
||||||
return ["a", "b", "c", "d", "e", "f", "g", "h"];
|
|
||||||
}
|
|
||||||
|
|
||||||
renderRankLabels() {
|
|
||||||
return _.map(this.rankLabels, (rankLabel) => {
|
|
||||||
return (
|
|
||||||
<div key={rankLabel} className="board-label">{rankLabel}</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
renderFileLabels() {
|
|
||||||
return _.map(this.fileLabels, (fileLabel) => {
|
|
||||||
return (
|
|
||||||
<div key={fileLabel} className="board-label">{fileLabel}</div>
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div className={this.boardClass}>
|
<div className={this.boardClass}>
|
||||||
<div className="board-rank-labels">
|
<RankLabels />
|
||||||
{this.renderRankLabels()}
|
<FileLabels />
|
||||||
</div>
|
|
||||||
<div className="board-file-labels">
|
|
||||||
{this.renderFileLabels()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="board-body">
|
<div className="board-body">
|
||||||
{this.renderSquares()}
|
{this.renderSquares()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="board-game-state">
|
||||||
|
{this.gameState}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
31
assets/js/components/file-labels.js
Normal file
31
assets/js/components/file-labels.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
import _ from "lodash";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
class FileLabels extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
get fileLabels() {
|
||||||
|
return ["a", "b", "c", "d", "e", "f", "g", "h"];
|
||||||
|
}
|
||||||
|
|
||||||
|
renderFileLabels() {
|
||||||
|
return _.map(this.fileLabels, (fileLabel) => {
|
||||||
|
return (
|
||||||
|
<div key={fileLabel} className="board-label">{fileLabel}</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="board-file-labels">
|
||||||
|
{this.renderFileLabels()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FileLabels;
|
||||||
31
assets/js/components/rank-labels.js
Normal file
31
assets/js/components/rank-labels.js
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from "react";
|
||||||
|
import _ from "lodash";
|
||||||
|
import classNames from "classnames";
|
||||||
|
|
||||||
|
class RankLabels extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
get rankLabels() {
|
||||||
|
return [1, 2, 3, 4, 5, 6, 7, 8];
|
||||||
|
}
|
||||||
|
|
||||||
|
renderRankLabels() {
|
||||||
|
return _.map(this.rankLabels, (rankLabel) => {
|
||||||
|
return (
|
||||||
|
<div key={rankLabel} className="board-label">{rankLabel}</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<div className="board-rank-labels">
|
||||||
|
{this.renderRankLabels()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default RankLabels;
|
||||||
@ -13,6 +13,7 @@ const chessBoardReducer = (state = defaultState, action) => {
|
|||||||
return Immutable.fromJS(state)
|
return Immutable.fromJS(state)
|
||||||
.set("board", action.board)
|
.set("board", action.board)
|
||||||
.set("turn", action.turn)
|
.set("turn", action.turn)
|
||||||
|
.set("state", action.state)
|
||||||
.set("selectedSquare", null)
|
.set("selectedSquare", null)
|
||||||
.set("moves", [])
|
.set("moves", [])
|
||||||
.toJS();
|
.toJS();
|
||||||
|
|||||||
@ -16,6 +16,7 @@ export const setGame = (data) => {
|
|||||||
type: SET_GAME,
|
type: SET_GAME,
|
||||||
board: data.board,
|
board: data.board,
|
||||||
turn: data.turn,
|
turn: data.turn,
|
||||||
|
state: data.state,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ const defaultState = {
|
|||||||
|
|
||||||
player: null,
|
player: null,
|
||||||
turn: null,
|
turn: null,
|
||||||
|
state: null,
|
||||||
|
|
||||||
moves: [],
|
moves: [],
|
||||||
|
|
||||||
|
|||||||
@ -5,16 +5,48 @@ defmodule Chess.GameState do
|
|||||||
alias Chess.Moves
|
alias Chess.Moves
|
||||||
alias Chess.Moves.Piece
|
alias Chess.Moves.Piece
|
||||||
|
|
||||||
|
def state(board, colour) do
|
||||||
|
cond do
|
||||||
|
player_checkmated?(board, colour) ->
|
||||||
|
"checkmate"
|
||||||
|
player_stalemated?(board, colour) ->
|
||||||
|
"stalemate"
|
||||||
|
king_in_check?(board, colour) ->
|
||||||
|
"check"
|
||||||
|
true -> nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
def player_checkmated?(board, colour) do
|
def player_checkmated?(board, colour) do
|
||||||
|
king_in_check?(board, colour) &&
|
||||||
|
player_cannot_move?(board, colour)
|
||||||
|
end
|
||||||
|
|
||||||
|
def player_stalemated?(board, colour) do
|
||||||
|
!king_in_check?(board, colour) &&
|
||||||
|
player_cannot_move?(board, colour)
|
||||||
|
end
|
||||||
|
|
||||||
|
def king_in_check?(board, colour) do
|
||||||
|
king =
|
||||||
|
board
|
||||||
|
|> Board.search(%{"type" => "king", "colour" => colour})
|
||||||
|
|> List.first
|
||||||
|
|
||||||
|
board
|
||||||
|
|> Piece.attacked?(king)
|
||||||
|
end
|
||||||
|
|
||||||
|
def player_cannot_move?(board, colour) do
|
||||||
board
|
board
|
||||||
|> Board.search(%{"colour" => colour})
|
|> Board.search(%{"colour" => colour})
|
||||||
|> Enum.all?(fn({file, rank}) ->
|
|> Enum.all?(fn({file, rank}) ->
|
||||||
board
|
board
|
||||||
|> cannot_escape_check?({file, rank})
|
|> piece_cannot_move?({file, rank})
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
def cannot_escape_check?(board, {file, rank}) do
|
def piece_cannot_move?(board, {file, rank}) do
|
||||||
piece =
|
piece =
|
||||||
board
|
board
|
||||||
|> Board.piece({file, rank})
|
|> Board.piece({file, rank})
|
||||||
@ -27,14 +59,4 @@ defmodule Chess.GameState do
|
|||||||
|> king_in_check?(piece["colour"])
|
|> king_in_check?(piece["colour"])
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
|
|
||||||
def king_in_check?(board, colour) do
|
|
||||||
king =
|
|
||||||
board
|
|
||||||
|> Board.search(%{"type" => "king", "colour" => colour})
|
|
||||||
|> List.first
|
|
||||||
|
|
||||||
board
|
|
||||||
|> Piece.attacked?(king)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|||||||
@ -15,6 +15,7 @@ defmodule Chess.Store.Game do
|
|||||||
schema "games" do
|
schema "games" do
|
||||||
field :board, :map, default: Board.default()
|
field :board, :map, default: Board.default()
|
||||||
field :turn, :string, default: "white"
|
field :turn, :string, default: "white"
|
||||||
|
field :state, :string
|
||||||
|
|
||||||
belongs_to :user, Chess.Store.User
|
belongs_to :user, Chess.Store.User
|
||||||
belongs_to :opponent, Chess.Store.User, references: :id
|
belongs_to :opponent, Chess.Store.User, references: :id
|
||||||
@ -34,6 +35,7 @@ defmodule Chess.Store.Game do
|
|||||||
struct
|
struct
|
||||||
|> cast(params, required_attrs())
|
|> cast(params, required_attrs())
|
||||||
|> validate_king_in_check(struct, params)
|
|> validate_king_in_check(struct, params)
|
||||||
|
|> check_game_state(struct, params)
|
||||||
end
|
end
|
||||||
|
|
||||||
def change_turn("black"), do: "white"
|
def change_turn("black"), do: "white"
|
||||||
@ -49,6 +51,11 @@ defmodule Chess.Store.Game do
|
|||||||
or_where: game.opponent_id == ^user_id
|
or_where: game.opponent_id == ^user_id
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def check_game_state(changeset, _struct, params) do
|
||||||
|
changeset
|
||||||
|
|> put_change(:state, GameState.state(params.board, params.turn))
|
||||||
|
end
|
||||||
|
|
||||||
def validate_king_in_check(changeset, %Game{turn: turn}, %{board: board}) do
|
def validate_king_in_check(changeset, %Game{turn: turn}, %{board: board}) do
|
||||||
if GameState.king_in_check?(board, turn) do
|
if GameState.king_in_check?(board, turn) do
|
||||||
changeset
|
changeset
|
||||||
|
|||||||
@ -22,7 +22,8 @@ defmodule ChessWeb.GameChannel do
|
|||||||
payload = %{
|
payload = %{
|
||||||
player: player(socket, game),
|
player: player(socket, game),
|
||||||
board: Board.transform(game.board),
|
board: Board.transform(game.board),
|
||||||
turn: game.turn
|
turn: game.turn,
|
||||||
|
state: game.state
|
||||||
}
|
}
|
||||||
|
|
||||||
socket
|
socket
|
||||||
@ -81,7 +82,8 @@ defmodule ChessWeb.GameChannel do
|
|||||||
def send_update(game) do
|
def send_update(game) do
|
||||||
payload = %{
|
payload = %{
|
||||||
board: Board.transform(game.board),
|
board: Board.transform(game.board),
|
||||||
turn: game.turn
|
turn: game.turn,
|
||||||
|
state: game.state
|
||||||
}
|
}
|
||||||
ChessWeb.Endpoint.broadcast("game:#{game.id}", "game:update", payload)
|
ChessWeb.Endpoint.broadcast("game:#{game.id}", "game:update", payload)
|
||||||
end
|
end
|
||||||
|
|||||||
@ -0,0 +1,9 @@
|
|||||||
|
defmodule Chess.Repo.Migrations.AddStateToGame do
|
||||||
|
use Ecto.Migration
|
||||||
|
|
||||||
|
def change do
|
||||||
|
alter table("games") do
|
||||||
|
add :state, :string
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -91,4 +91,14 @@ defmodule Chess.GameStateTest do
|
|||||||
|
|
||||||
refute GameState.player_checkmated?(board, "white")
|
refute GameState.player_checkmated?(board, "white")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
test "game can be stalemated" do
|
||||||
|
board = %{
|
||||||
|
"0,0" => %{"type" => "king", "colour" => "white"},
|
||||||
|
"1,2" => %{"type" => "rook", "colour" => "black"},
|
||||||
|
"2,1" => %{"type" => "rook", "colour" => "black"},
|
||||||
|
}
|
||||||
|
|
||||||
|
assert GameState.player_stalemated?(board, "white")
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user