From 5556de00b0d3cd751c560e7fd8cce06dc415e5f6 Mon Sep 17 00:00:00 2001 From: Dan Barber Date: Sat, 7 Apr 2018 17:17:35 -0400 Subject: [PATCH] Show game state on the board --- assets/css/_board.scss | 57 ++++++++++++------- assets/js/components/chess-board.js | 44 +++++--------- assets/js/components/file-labels.js | 31 ++++++++++ assets/js/components/rank-labels.js | 31 ++++++++++ assets/js/reducers/chess-board.js | 1 + assets/js/store/actions.js | 1 + assets/js/store/default-state.js | 1 + lib/chess/game_state.ex | 46 +++++++++++---- lib/chess/store/game.ex | 7 +++ lib/chess_web/channels/game_channel.ex | 6 +- .../20180407190333_add_state_to_game.exs | 9 +++ test/chess/game_state_test.exs | 10 ++++ 12 files changed, 180 insertions(+), 64 deletions(-) create mode 100644 assets/js/components/file-labels.js create mode 100644 assets/js/components/rank-labels.js create mode 100644 priv/repo/migrations/20180407190333_add_state_to_game.exs diff --git a/assets/css/_board.scss b/assets/css/_board.scss index 2aef1aa..a9f05dc 100644 --- a/assets/css/_board.scss +++ b/assets/css/_board.scss @@ -49,6 +49,35 @@ &.player-is-black.white-to-move::before { 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 { @@ -59,26 +88,6 @@ 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 { display: flex; height: 5%; @@ -87,6 +96,14 @@ width: 90%; } +.board-game-state { + bottom: 0; + color: white; + height: 5%; + position: absolute; + width: 90%; +} + .board-label { align-items: center; display: flex; diff --git a/assets/js/components/chess-board.js b/assets/js/components/chess-board.js index ac417e9..e8af715 100644 --- a/assets/js/components/chess-board.js +++ b/assets/js/components/chess-board.js @@ -4,6 +4,8 @@ import { connect } from "react-redux"; import classNames from "classnames"; import ChessBoardSquare from "./chess-board-square"; +import RankLabels from "./rank-labels"; +import FileLabels from "./file-labels"; class ChessBoard extends React.Component { 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() { const turn = this.getTurn(); const player = this.getPlayer(); @@ -79,43 +87,19 @@ class ChessBoard extends React.Component { 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 ( -
{rankLabel}
- ); - }); - } - - renderFileLabels() { - return _.map(this.fileLabels, (fileLabel) => { - return ( -
{fileLabel}
- ); - }); - } - render() { return (
-
- {this.renderRankLabels()} -
-
- {this.renderFileLabels()} -
+ +
{this.renderSquares()}
+ +
+ {this.gameState} +
); } diff --git a/assets/js/components/file-labels.js b/assets/js/components/file-labels.js new file mode 100644 index 0000000..81cc3da --- /dev/null +++ b/assets/js/components/file-labels.js @@ -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 ( +
{fileLabel}
+ ); + }); + } + + render() { + return ( +
+ {this.renderFileLabels()} +
+ ); + } +} + +export default FileLabels; diff --git a/assets/js/components/rank-labels.js b/assets/js/components/rank-labels.js new file mode 100644 index 0000000..00781f7 --- /dev/null +++ b/assets/js/components/rank-labels.js @@ -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 ( +
{rankLabel}
+ ); + }); + } + + render() { + return ( +
+ {this.renderRankLabels()} +
+ ); + } +} + +export default RankLabels; diff --git a/assets/js/reducers/chess-board.js b/assets/js/reducers/chess-board.js index d838d7c..3a9d3b6 100644 --- a/assets/js/reducers/chess-board.js +++ b/assets/js/reducers/chess-board.js @@ -13,6 +13,7 @@ const chessBoardReducer = (state = defaultState, action) => { return Immutable.fromJS(state) .set("board", action.board) .set("turn", action.turn) + .set("state", action.state) .set("selectedSquare", null) .set("moves", []) .toJS(); diff --git a/assets/js/store/actions.js b/assets/js/store/actions.js index f0dd769..96ea577 100644 --- a/assets/js/store/actions.js +++ b/assets/js/store/actions.js @@ -16,6 +16,7 @@ export const setGame = (data) => { type: SET_GAME, board: data.board, turn: data.turn, + state: data.state, }; }; diff --git a/assets/js/store/default-state.js b/assets/js/store/default-state.js index 2613f47..1a3102e 100644 --- a/assets/js/store/default-state.js +++ b/assets/js/store/default-state.js @@ -3,6 +3,7 @@ const defaultState = { player: null, turn: null, + state: null, moves: [], diff --git a/lib/chess/game_state.ex b/lib/chess/game_state.ex index 7b22fd7..79b78ce 100644 --- a/lib/chess/game_state.ex +++ b/lib/chess/game_state.ex @@ -5,16 +5,48 @@ defmodule Chess.GameState do alias Chess.Moves 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 + 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.search(%{"colour" => colour}) |> Enum.all?(fn({file, rank}) -> board - |> cannot_escape_check?({file, rank}) + |> piece_cannot_move?({file, rank}) end) end - def cannot_escape_check?(board, {file, rank}) do + def piece_cannot_move?(board, {file, rank}) do piece = board |> Board.piece({file, rank}) @@ -27,14 +59,4 @@ defmodule Chess.GameState do |> king_in_check?(piece["colour"]) 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 diff --git a/lib/chess/store/game.ex b/lib/chess/store/game.ex index 48c4dc6..cfa04ce 100644 --- a/lib/chess/store/game.ex +++ b/lib/chess/store/game.ex @@ -15,6 +15,7 @@ defmodule Chess.Store.Game do schema "games" do field :board, :map, default: Board.default() field :turn, :string, default: "white" + field :state, :string belongs_to :user, Chess.Store.User belongs_to :opponent, Chess.Store.User, references: :id @@ -34,6 +35,7 @@ defmodule Chess.Store.Game do struct |> cast(params, required_attrs()) |> validate_king_in_check(struct, params) + |> check_game_state(struct, params) end def change_turn("black"), do: "white" @@ -49,6 +51,11 @@ defmodule Chess.Store.Game do or_where: game.opponent_id == ^user_id 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 if GameState.king_in_check?(board, turn) do changeset diff --git a/lib/chess_web/channels/game_channel.ex b/lib/chess_web/channels/game_channel.ex index 4fe6d4b..0297259 100644 --- a/lib/chess_web/channels/game_channel.ex +++ b/lib/chess_web/channels/game_channel.ex @@ -22,7 +22,8 @@ defmodule ChessWeb.GameChannel do payload = %{ player: player(socket, game), board: Board.transform(game.board), - turn: game.turn + turn: game.turn, + state: game.state } socket @@ -81,7 +82,8 @@ defmodule ChessWeb.GameChannel do def send_update(game) do payload = %{ board: Board.transform(game.board), - turn: game.turn + turn: game.turn, + state: game.state } ChessWeb.Endpoint.broadcast("game:#{game.id}", "game:update", payload) end diff --git a/priv/repo/migrations/20180407190333_add_state_to_game.exs b/priv/repo/migrations/20180407190333_add_state_to_game.exs new file mode 100644 index 0000000..e6d7e2d --- /dev/null +++ b/priv/repo/migrations/20180407190333_add_state_to_game.exs @@ -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 diff --git a/test/chess/game_state_test.exs b/test/chess/game_state_test.exs index d04b251..8453293 100644 --- a/test/chess/game_state_test.exs +++ b/test/chess/game_state_test.exs @@ -91,4 +91,14 @@ defmodule Chess.GameStateTest do refute GameState.player_checkmated?(board, "white") 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