From 9e36945f8ddc7f5da1e06f3876a926b0157cb93b Mon Sep 17 00:00:00 2001 From: Dan Barber Date: Fri, 18 May 2018 11:44:08 -0400 Subject: [PATCH] =?UTF-8?q?=20Castling=20moves!=20=E2=99=94=E2=86=92?= =?UTF-8?q?=E2=86=90=E2=99=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/chess/board.ex | 47 +++++- lib/chess/moves.ex | 8 +- lib/chess/moves/pieces/king.ex | 6 +- lib/chess/moves/pieces/king/castling.ex | 102 ++++++++++++ lib/chess/repo/queries.ex | 6 - lib/chess_web/channels/game_channel.ex | 4 +- test/chess/board_test.exs | 32 +++- .../chess/moves/pieces/king/castling_test.exs | 151 ++++++++++++++++++ 8 files changed, 336 insertions(+), 20 deletions(-) create mode 100644 lib/chess/moves/pieces/king/castling.ex create mode 100644 test/chess/moves/pieces/king/castling_test.exs diff --git a/lib/chess/board.ex b/lib/chess/board.ex index 1acdacd..3c575c5 100644 --- a/lib/chess/board.ex +++ b/lib/chess/board.ex @@ -30,22 +30,53 @@ defmodule Chess.Board do board["#{file},#{rank}"] end - def move_piece(board, %{"from" => from, "to" => to}) do - [from_file, from_rank] = from - [to_file, to_rank] = to + def move_piece(board, %{ + "from" => [from_file, from_rank], + "to" => [to_file, to_rank] + }) do + {piece, board} = Map.pop(board, to_index({from_file, from_rank})) + {piece_captured, board} = Map.pop(board, to_index({to_file, to_rank})) + board = Map.put(board, to_index({to_file, to_rank}), piece) - {piece, board} = Map.pop(board, "#{from_file},#{from_rank}") - {piece_captured, board} = Map.pop(board, "#{to_file},#{to_rank}") + board = + if castling_move?(piece, from_file, to_file) do + board + |> castling_move(%{ + "from" => [from_file, from_rank], + "to" => [to_file, to_rank] + }) + |> Map.get(:board) + else + board + end %{ from: %{"file" => from_file, "rank" => from_rank}, to: %{"file" => to_file, "rank" => to_rank}, - board: Map.put(board, "#{to_file},#{to_rank}", piece), + board: board, piece: piece, piece_captured: piece_captured, } end + def castling_move?(%{"type" => "king"}, 4, to_file) do + to_file == 2 || to_file == 6 + end + def castling_move?(_, _, _), do: false + + def castling_move(board, %{"from" => [4, rank], "to" => [2, _rank]}) do + move_piece(board, %{ + "from" => [0, rank], + "to" => [3, rank], + }) + end + def castling_move(board, %{"from" => [4, rank], "to" => [6, _rank]}) do + move_piece(board, %{ + "from" => [7, rank], + "to" => [5, rank], + }) + end + def default do %{ "0,7" => %{"type" => "rook", "colour" => "black"}, @@ -86,6 +117,10 @@ defmodule Chess.Board do } end + defp to_index({file, rank}) do + "#{file},#{rank}" + end + defp indexes_to_tuples(list) do list |> Enum.map(fn({index, _piece}) -> index_to_tuple(index) end) diff --git a/lib/chess/moves.ex b/lib/chess/moves.ex index 2e5a573..bd0d9b9 100644 --- a/lib/chess/moves.ex +++ b/lib/chess/moves.ex @@ -15,7 +15,9 @@ defmodule Chess.Moves do alias Chess.Moves.Pieces.King def make_move(game, move_params) do - params = Board.move_piece(game.board, move_params) + params = + game.board + |> Board.move_piece(move_params) Multi.new |> Multi.update(:game, Game.move_changeset(game, params)) @@ -23,7 +25,7 @@ defmodule Chess.Moves do |> Repo.transaction end - def available(board, {file, rank}) do + def available(board, {file, rank}, move_list \\ []) do piece = board |> Board.piece({file, rank}) @@ -38,7 +40,7 @@ defmodule Chess.Moves do %{"type" => "knight"} -> Knight.moves(board, {file, rank}) %{"type" => "king"} -> - King.moves(board, {file, rank}) + King.moves(board, {file, rank}, move_list) %{"type" => "queen"} -> Queen.moves(board, {file, rank}) end diff --git a/lib/chess/moves/pieces/king.ex b/lib/chess/moves/pieces/king.ex index 8aeb92d..1374dfe 100644 --- a/lib/chess/moves/pieces/king.ex +++ b/lib/chess/moves/pieces/king.ex @@ -2,9 +2,11 @@ defmodule Chess.Moves.Pieces.King do @moduledoc false alias Chess.Moves.Generator + alias Chess.Moves.Pieces.King.Castling - def moves(board, {file, rank}) do - Generator.moves(board, {file, rank}, pattern()) + def moves(board, {file, rank}, move_list) do + Generator.moves(board, {file, rank}, pattern()) ++ + Castling.moves(board, {file, rank}, move_list) end defp pattern do diff --git a/lib/chess/moves/pieces/king/castling.ex b/lib/chess/moves/pieces/king/castling.ex new file mode 100644 index 0000000..c5cc7e3 --- /dev/null +++ b/lib/chess/moves/pieces/king/castling.ex @@ -0,0 +1,102 @@ +defmodule Chess.Moves.Pieces.King.Castling do + @moduledoc false + + alias Chess.Board + alias Chess.GameState + alias Chess.Store.Move + + def moves(board, {4, rank}, move_list) when rank == 0 or rank == 7 do + colour = + board + |> Board.piece({4, rank}) + |> Map.get("colour") + + if not king_has_moved?(move_list, colour) do + board + |> _moves(rank, colour, move_list) + else + [] + end + end + def moves(_board, _piece, _move_list), do: [] + + def _moves(board, _rank, colour, move_list) do + board + |> Board.search(%{"type" => "rook", "colour" => colour}) + |> Enum.map(fn ({file, rank}) -> + case file do + 0 -> queen_side_move(board, rank, colour, move_list) + 7 -> king_side_move(board, rank, colour, move_list) + _ -> nil + end + end) + |> Enum.reject(&is_nil(&1)) + end + + defp king_has_moved?(move_list, colour) do + move_list + |> Enum.any?(fn (move) -> + match?(%Move{ + piece: %{"type" => "king", "colour" => ^colour} + }, move) + end) + end + + defp queen_side_move(board, rank, colour, move_list) do + if queen_side_squares_empty?(board, rank) && + !queen_side_in_check?(board, rank, colour) && + !rook_has_moved?(0, move_list, colour) do + {2, rank} + end + end + + defp king_side_move(board, rank, colour, move_list) do + if king_side_squares_empty?(board, rank) && + !king_side_in_check?(board, rank, colour) && + !rook_has_moved?(7, move_list, colour) do + {6, rank} + end + end + + defp rook_has_moved?(file, move_list, colour) do + move_list + |> Enum.any?(fn (move) -> + match?(%Move{ + piece: %{"type" => "rook", "colour" => ^colour}, + from: %{"file" => ^file}, + }, move) + end) + end + + defp queen_side_in_check?(board, rank, colour) do + [{2, rank}, {3, rank}] + |> Enum.any?(fn ({to_file, to_rank}) -> + board + |> Board.move_piece(%{"from" => [4, rank], "to" => [to_file, to_rank]}) + |> Map.get(:board) + |> GameState.king_in_check?(colour) + end) + end + + defp king_side_in_check?(board, rank, colour) do + [{5, rank}, {6, rank}] + |> Enum.any?(fn ({to_file, to_rank}) -> + board + |> Board.move_piece(%{"from" => [4, rank], "to" => [to_file, to_rank]}) + |> Map.get(:board) + |> GameState.king_in_check?(colour) + end) + end + + defp queen_side_squares_empty?(board, rank) do + [{1, rank}, {2, rank}, {3, rank}] + |> Enum.map(&Board.piece(board, &1)) + |> Enum.all?(&is_nil(&1)) + end + + defp king_side_squares_empty?(board, rank) do + [{5, rank}, {6, rank}] + |> Enum.map(&Board.piece(board, &1)) + |> Enum.all?(&is_nil(&1)) + end +end diff --git a/lib/chess/repo/queries.ex b/lib/chess/repo/queries.ex index aa4ecde..3266231 100644 --- a/lib/chess/repo/queries.ex +++ b/lib/chess/repo/queries.ex @@ -19,10 +19,4 @@ defmodule Chess.Repo.Queries do |> preload(:moves) |> Repo.get!(game_id) end - - def game_for_user(user_id, game_id) do - user_id - |> Game.for_user_id() - |> Repo.get!(game_id) - end end diff --git a/lib/chess_web/channels/game_channel.ex b/lib/chess_web/channels/game_channel.ex index a40388b..85edd1f 100644 --- a/lib/chess_web/channels/game_channel.ex +++ b/lib/chess_web/channels/game_channel.ex @@ -61,12 +61,12 @@ defmodule ChessWeb.GameChannel do ) do game = socket.assigns.current_user_id - |> Queries.game_for_user(socket.assigns.game_id) + |> Queries.game_with_moves(socket.assigns.game_id) moves = Moves.available(game.board, { String.to_integer(file), String.to_integer(rank) - }) + }, game.moves) reply = %{ moves: Enum.map(moves, &(Tuple.to_list(&1))) diff --git a/test/chess/board_test.exs b/test/chess/board_test.exs index 0183e8f..b6d829d 100644 --- a/test/chess/board_test.exs +++ b/test/chess/board_test.exs @@ -46,10 +46,40 @@ defmodule Chess.BoardTest do } %{board: new_board} = - Board.move_piece(board, %{"from" => ["3", "0"], "to" => ["5", "2"]}) + Board.move_piece(board, %{"from" => [3, 0], "to" => [5, 2]}) assert new_board == %{ "5,2" => %{"type" => "queen", "colour" => "white"}, } end + + test "can perform a castling move on the kings side" do + board = %{ + "4,0" => %{"type" => "king", "colour" => "white"}, + "7,0" => %{"type" => "rook", "colour" => "white"}, + } + + %{board: new_board} = + Board.move_piece(board, %{"from" => [4, 0], "to" => [6, 0]}) + + assert new_board == %{ + "6,0" => %{"type" => "king", "colour" => "white"}, + "5,0" => %{"type" => "rook", "colour" => "white"}, + } + end + + test "can perform a castling move on the queens side" do + board = %{ + "4,0" => %{"type" => "king", "colour" => "white"}, + "0,0" => %{"type" => "rook", "colour" => "white"}, + } + + %{board: new_board} = + Board.move_piece(board, %{"from" => [4, 0], "to" => [2, 0]}) + + assert new_board == %{ + "2,0" => %{"type" => "king", "colour" => "white"}, + "3,0" => %{"type" => "rook", "colour" => "white"}, + } + end end diff --git a/test/chess/moves/pieces/king/castling_test.exs b/test/chess/moves/pieces/king/castling_test.exs new file mode 100644 index 0000000..998282e --- /dev/null +++ b/test/chess/moves/pieces/king/castling_test.exs @@ -0,0 +1,151 @@ +defmodule Chess.Moves.Pieces.King.CastlingTest do + use Chess.DataCase + + alias Chess.Moves + alias Chess.Store.Move + + test "king can move two spaces to castle with the king side rook" do + board = %{ + "4,0" => %{"type" => "king", "colour" => "white"}, + "7,0" => %{"type" => "rook", "colour" => "white"}, + } + moves = Moves.available(board, {4, 0}) + + expected_moves = Enum.sort([ + {3, 0}, {5, 0}, {3, 1}, {4, 1}, {5, 1}, + {6, 0}, + ]) + assert Enum.sort(moves) == expected_moves + end + + test "king can move two spaces to castle with the queen side rook" do + board = %{ + "4,0" => %{"type" => "king", "colour" => "white"}, + "0,0" => %{"type" => "rook", "colour" => "white"}, + } + moves = Moves.available(board, {4, 0}) + + expected_moves = Enum.sort([ + {3, 0}, {5, 0}, {3, 1}, {4, 1}, {5, 1}, + {2, 0}, + ]) + assert Enum.sort(moves) == expected_moves + end + + test "cannot castle if a piece is in the way" do + board = %{ + "4,0" => %{"type" => "king", "colour" => "white"}, + "3,0" => %{"type" => "queen", "colour" => "white"}, + "0,0" => %{"type" => "rook", "colour" => "white"}, + } + moves = Moves.available(board, {4, 0}) + + expected_moves = Enum.sort([ + {5, 0}, {3, 1}, {4, 1}, {5, 1}, + ]) + assert Enum.sort(moves) == expected_moves + end + + test "cannot castle if it would result in the king being in check" do + board = %{ + "4,0" => %{"type" => "king", "colour" => "white"}, + "0,0" => %{"type" => "rook", "colour" => "white"}, + "2,7" => %{"type" => "queen", "colour" => "black"}, + } + moves = Moves.available(board, {4, 0}) + + expected_moves = Enum.sort([ + {3, 0}, {5, 0}, {3, 1}, {4, 1}, {5, 1}, + ]) + assert Enum.sort(moves) == expected_moves + end + + test "cannot castle if the king moves through a space that is attacked" do + board = %{ + "4,0" => %{"type" => "king", "colour" => "white"}, + "0,0" => %{"type" => "rook", "colour" => "white"}, + "3,7" => %{"type" => "queen", "colour" => "black"}, + } + moves = Moves.available(board, {4, 0}) + + expected_moves = Enum.sort([ + {3, 0}, {5, 0}, {3, 1}, {4, 1}, {5, 1}, + ]) + assert Enum.sort(moves) == expected_moves + end + + test "cannot castle if the king has moved" do + board = %{ + "4,0" => %{"type" => "king", "colour" => "white"}, + "0,0" => %{"type" => "rook", "colour" => "white"}, + } + move_list = [ + %Move{ + from: %{"file" => 4, "rank" => 0}, + to: %{"file" => 4, "rank" => 1}, + piece: %{"type" => "king", "colour" => "white"} + }, + %Move{ + from: %{"file" => 4, "rank" => 1}, + to: %{"file" => 4, "rank" => 0}, + piece: %{"type" => "king", "colour" => "white"} + }, + ] + moves = Moves.available(board, {4, 0}, move_list) + + expected_moves = Enum.sort([ + {3, 0}, {5, 0}, {3, 1}, {4, 1}, {5, 1}, + ]) + assert Enum.sort(moves) == expected_moves + end + + test "cannot castle if the queen side rook has moved" do + board = %{ + "4,0" => %{"type" => "king", "colour" => "white"}, + "0,0" => %{"type" => "rook", "colour" => "white"}, + } + move_list = [ + %Move{ + from: %{"file" => 0, "rank" => 0}, + to: %{"file" => 0, "rank" => 1}, + piece: %{"type" => "rook", "colour" => "white"} + }, + %Move{ + from: %{"file" => 0, "rank" => 1}, + to: %{"file" => 0, "rank" => 0}, + piece: %{"type" => "rook", "colour" => "white"} + }, + ] + moves = Moves.available(board, {4, 0}, move_list) + + expected_moves = Enum.sort([ + {3, 0}, {5, 0}, {3, 1}, {4, 1}, {5, 1}, + ]) + assert Enum.sort(moves) == expected_moves + end + + test "cannot castle if the king side rook has moved" do + board = %{ + "4,0" => %{"type" => "king", "colour" => "white"}, + "7,0" => %{"type" => "rook", "colour" => "white"}, + } + move_list = [ + %Move{ + from: %{"file" => 7, "rank" => 0}, + to: %{"file" => 7, "rank" => 1}, + piece: %{"type" => "rook", "colour" => "white"} + }, + %Move{ + from: %{"file" => 7, "rank" => 1}, + to: %{"file" => 7, "rank" => 0}, + piece: %{"type" => "rook", "colour" => "white"} + }, + ] + moves = Moves.available(board, {4, 0}, move_list) + + expected_moves = Enum.sort([ + {3, 0}, {5, 0}, {3, 1}, {4, 1}, {5, 1}, + ]) + assert Enum.sort(moves) == expected_moves + end +end