diff --git a/lib/chess/board.ex b/lib/chess/board.ex index 3c575c5..78bf947 100644 --- a/lib/chess/board.ex +++ b/lib/chess/board.ex @@ -2,17 +2,19 @@ defmodule Chess.Board do @moduledoc false def transform(board) do - Enum.map(0..7, fn (rank) -> - Enum.map(0..7, fn (file) -> - board - |> piece({file, rank}) - end) + Enum.map(0..7, fn rank -> + {rank, + Enum.map(0..7, fn file -> + {file, + board + |> piece({file, rank})} + end)} end) end def search(board, %{"type" => type, "colour" => colour}) do board - |> Enum.filter(fn({_index, piece}) -> + |> Enum.filter(fn {_index, piece} -> match?(%{"type" => ^type, "colour" => ^colour}, piece) end) |> indexes_to_tuples @@ -20,7 +22,7 @@ defmodule Chess.Board do def search(board, %{"colour" => colour}) do board - |> Enum.filter(fn({_index, piece}) -> + |> Enum.filter(fn {_index, piece} -> match?(%{"colour" => ^colour}, piece) end) |> indexes_to_tuples @@ -31,9 +33,9 @@ defmodule Chess.Board do end def move_piece(board, %{ - "from" => [from_file, from_rank], - "to" => [to_file, to_rank] - }) do + 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) @@ -42,8 +44,8 @@ defmodule Chess.Board do if castling_move?(piece, from_file, to_file) do board |> castling_move(%{ - "from" => [from_file, from_rank], - "to" => [to_file, to_rank] + from: {from_file, from_rank}, + to: {to_file, to_rank} }) |> Map.get(:board) else @@ -55,39 +57,40 @@ defmodule Chess.Board do to: %{"file" => to_file, "rank" => to_rank}, board: board, piece: piece, - piece_captured: piece_captured, + 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 + def castling_move(board, %{from: {4, rank}, to: {2, _rank}}) do move_piece(board, %{ - "from" => [0, rank], - "to" => [3, rank], + from: {0, rank}, + to: {3, rank} }) end - def castling_move(board, %{"from" => [4, rank], "to" => [6, _rank]}) do + + def castling_move(board, %{from: {4, rank}, to: {6, _rank}}) do move_piece(board, %{ - "from" => [7, rank], - "to" => [5, rank], + from: {7, rank}, + to: {5, rank} }) end def default do %{ - "0,7" => %{"type" => "rook", "colour" => "black"}, + "0,7" => %{"type" => "rook", "colour" => "black"}, "1,7" => %{"type" => "knight", "colour" => "black"}, "2,7" => %{"type" => "bishop", "colour" => "black"}, - "3,7" => %{"type" => "queen", "colour" => "black"}, - "4,7" => %{"type" => "king", "colour" => "black"}, + "3,7" => %{"type" => "queen", "colour" => "black"}, + "4,7" => %{"type" => "king", "colour" => "black"}, "5,7" => %{"type" => "bishop", "colour" => "black"}, "6,7" => %{"type" => "knight", "colour" => "black"}, - "7,7" => %{"type" => "rook", "colour" => "black"}, - + "7,7" => %{"type" => "rook", "colour" => "black"}, "0,6" => %{"type" => "pawn", "colour" => "black"}, "1,6" => %{"type" => "pawn", "colour" => "black"}, "2,6" => %{"type" => "pawn", "colour" => "black"}, @@ -96,7 +99,6 @@ defmodule Chess.Board do "5,6" => %{"type" => "pawn", "colour" => "black"}, "6,6" => %{"type" => "pawn", "colour" => "black"}, "7,6" => %{"type" => "pawn", "colour" => "black"}, - "0,1" => %{"type" => "pawn", "colour" => "white"}, "1,1" => %{"type" => "pawn", "colour" => "white"}, "2,1" => %{"type" => "pawn", "colour" => "white"}, @@ -105,15 +107,14 @@ defmodule Chess.Board do "5,1" => %{"type" => "pawn", "colour" => "white"}, "6,1" => %{"type" => "pawn", "colour" => "white"}, "7,1" => %{"type" => "pawn", "colour" => "white"}, - - "0,0" => %{"type" => "rook", "colour" => "white"}, + "0,0" => %{"type" => "rook", "colour" => "white"}, "1,0" => %{"type" => "knight", "colour" => "white"}, "2,0" => %{"type" => "bishop", "colour" => "white"}, - "3,0" => %{"type" => "queen", "colour" => "white"}, - "4,0" => %{"type" => "king", "colour" => "white"}, + "3,0" => %{"type" => "queen", "colour" => "white"}, + "4,0" => %{"type" => "king", "colour" => "white"}, "5,0" => %{"type" => "bishop", "colour" => "white"}, "6,0" => %{"type" => "knight", "colour" => "white"}, - "7,0" => %{"type" => "rook", "colour" => "white"} + "7,0" => %{"type" => "rook", "colour" => "white"} } end @@ -123,13 +124,13 @@ defmodule Chess.Board do defp indexes_to_tuples(list) do list - |> Enum.map(fn({index, _piece}) -> index_to_tuple(index) end) + |> Enum.map(fn {index, _piece} -> index_to_tuple(index) end) end defp index_to_tuple(index) do index |> String.split(",") - |> Enum.map(&(String.to_integer(&1))) - |> List.to_tuple + |> Enum.map(&String.to_integer(&1)) + |> List.to_tuple() end end diff --git a/lib/chess/game_state.ex b/lib/chess/game_state.ex index 610128e..76fa64f 100644 --- a/lib/chess/game_state.ex +++ b/lib/chess/game_state.ex @@ -14,11 +14,15 @@ defmodule Chess.GameState do cond do player_checkmated?(board, colour) -> "checkmate" + player_stalemated?(board, colour) -> "stalemate" + king_in_check?(board, colour) -> "check" - true -> nil + + true -> + nil end end @@ -36,7 +40,7 @@ defmodule Chess.GameState do king = board |> Board.search(%{"type" => "king", "colour" => colour}) - |> List.first + |> List.first() if is_nil(king) do raise "There is no #{colour} king!" @@ -49,7 +53,7 @@ defmodule Chess.GameState do def player_cannot_move?(board, colour) do board |> Board.search(%{"colour" => colour}) - |> Enum.all?(fn({file, rank}) -> + |> Enum.all?(fn {file, rank} -> board |> piece_cannot_move?({file, rank}) end) @@ -62,9 +66,9 @@ defmodule Chess.GameState do board |> Moves.available({file, rank}) - |> Enum.all?(fn({to_file, to_rank}) -> + |> Enum.all?(fn {to_file, to_rank} -> board - |> Board.move_piece(%{"from" => [file, rank], "to" => [to_file, to_rank]}) + |> Board.move_piece(%{from: {file, rank}, to: {to_file, to_rank}}) |> Map.get(:board) |> king_in_check?(piece["colour"]) end) diff --git a/lib/chess/moves.ex b/lib/chess/moves.ex index bd0d9b9..3fc1c00 100644 --- a/lib/chess/moves.ex +++ b/lib/chess/moves.ex @@ -19,10 +19,10 @@ defmodule Chess.Moves do game.board |> Board.move_piece(move_params) - Multi.new + Multi.new() |> Multi.update(:game, Game.move_changeset(game, params)) |> Multi.insert(:move, Ecto.build_assoc(game, :moves, params)) - |> Repo.transaction + |> Repo.transaction() end def available(board, {file, rank}, move_list \\ []) do @@ -33,14 +33,19 @@ defmodule Chess.Moves do case piece do %{"type" => "pawn"} -> Pawn.moves(board, {file, rank}) + %{"type" => "rook"} -> Rook.moves(board, {file, rank}) + %{"type" => "bishop"} -> Bishop.moves(board, {file, rank}) + %{"type" => "knight"} -> Knight.moves(board, {file, rank}) + %{"type" => "king"} -> King.moves(board, {file, rank}, move_list) + %{"type" => "queen"} -> Queen.moves(board, {file, rank}) end diff --git a/lib/chess/moves/pieces/king/castling.ex b/lib/chess/moves/pieces/king/castling.ex index c5cc7e3..4aafa3d 100644 --- a/lib/chess/moves/pieces/king/castling.ex +++ b/lib/chess/moves/pieces/king/castling.ex @@ -18,12 +18,13 @@ defmodule Chess.Moves.Pieces.King.Castling do [] 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}) -> + |> 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) @@ -35,44 +36,50 @@ defmodule Chess.Moves.Pieces.King.Castling do defp king_has_moved?(move_list, colour) do move_list - |> Enum.any?(fn (move) -> - match?(%Move{ - piece: %{"type" => "king", "colour" => ^colour} - }, move) + |> 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 + !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 + !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) + |> 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}) -> + |> Enum.any?(fn {to_file, to_rank} -> board - |> Board.move_piece(%{"from" => [4, rank], "to" => [to_file, to_rank]}) + |> Board.move_piece(%{from: {4, rank}, to: {to_file, to_rank}}) |> Map.get(:board) |> GameState.king_in_check?(colour) end) @@ -80,9 +87,9 @@ defmodule Chess.Moves.Pieces.King.Castling do defp king_side_in_check?(board, rank, colour) do [{5, rank}, {6, rank}] - |> Enum.any?(fn ({to_file, to_rank}) -> + |> Enum.any?(fn {to_file, to_rank} -> board - |> Board.move_piece(%{"from" => [4, rank], "to" => [to_file, to_rank]}) + |> Board.move_piece(%{from: {4, rank}, to: {to_file, to_rank}}) |> Map.get(:board) |> GameState.king_in_check?(colour) end) diff --git a/lib/chess/store/game.ex b/lib/chess/store/game.ex index 9610821..d5335f1 100644 --- a/lib/chess/store/game.ex +++ b/lib/chess/store/game.ex @@ -14,14 +14,14 @@ defmodule Chess.Store.Game do alias Chess.Store.User schema "games" do - field :board, :map, default: Board.default() - field :turn, :string, default: "white" - field :state, :string + field(:board, :map, default: Board.default()) + field(:turn, :string, default: "white") + field(:state, :string) - belongs_to :user, User - belongs_to :opponent, User, references: :id + belongs_to(:user, User) + belongs_to(:opponent, User, references: :id) - has_many :moves, Move + has_many(:moves, Move) timestamps() end @@ -55,15 +55,17 @@ defmodule Chess.Store.Game do end def for_user_id(user_id) do - from game in Game, + from(game in Game, where: game.user_id == ^user_id, or_where: game.opponent_id == ^user_id + ) end def check_game_state(changeset) do changeset |> put_change( - :state, GameState.state(changeset.changes.board, changeset.changes.turn) + :state, + GameState.state(changeset.changes.board, changeset.changes.turn) ) end @@ -78,6 +80,7 @@ defmodule Chess.Store.Game do changeset end end + def validate_king_in_check(changeset, _, _), do: changeset def ordered(query) do diff --git a/lib/chess_web/channels/game_channel.ex b/lib/chess_web/channels/game_channel.ex index 113fd1d..e3707c6 100644 --- a/lib/chess_web/channels/game_channel.ex +++ b/lib/chess_web/channels/game_channel.ex @@ -32,7 +32,7 @@ defmodule ChessWeb.GameChannel do board: Board.transform(game.board), turn: game.turn, state: game.state, - moves: MoveList.transform(game.moves), + moves: MoveList.transform(game.moves) } socket @@ -57,6 +57,7 @@ defmodule ChessWeb.GameChannel do update_opponent(socket, game) {:noreply, socket} + {:error, :game, changeset, _} -> {message, _} = changeset.errors[:board] @@ -65,21 +66,26 @@ defmodule ChessWeb.GameChannel do end def handle_in( - "game:get_available_moves", - %{"square" => [file, rank]}, - socket - ) do + "game:get_available_moves", + %{"square" => [file, rank]}, + socket + ) do game = socket.assigns.user_id |> Queries.game_with_moves(socket.assigns.game_id) - moves = Moves.available(game.board, { - String.to_integer(file), - String.to_integer(rank) - }, game.moves) + moves = + Moves.available( + game.board, + { + String.to_integer(file), + String.to_integer(rank) + }, + game.moves + ) reply = %{ - moves: Enum.map(moves, &(Tuple.to_list(&1))) + moves: Enum.map(moves, &Tuple.to_list(&1)) } {:reply, {:ok, reply}, socket} @@ -88,27 +94,29 @@ defmodule ChessWeb.GameChannel do def update_opponent(socket, game) do opponent_id = opponent(game, socket.assigns.user_id).id - |> Integer.to_string + |> Integer.to_string() send_update(socket) "game:#{game.id}" - |> Presence.list + |> Presence.list() |> case do %{^opponent_id => _} -> nil + _ -> socket |> Emails.opponent_moved_email(game) - |> Mailer.deliver_later + |> Mailer.deliver_later() end end def track_presence(socket) do - {:ok, _} = Presence.track(socket, socket.assigns.user_id, %{ - user_id: socket.assigns.user_id, - online_at: inspect(System.system_time(:second)) - }) + {:ok, _} = + Presence.track(socket, socket.assigns.user_id, %{ + user_id: socket.assigns.user_id, + online_at: inspect(System.system_time(:second)) + }) socket |> push("presence_state", Presence.list(socket)) @@ -116,8 +124,8 @@ defmodule ChessWeb.GameChannel do def convert_params(%{"from" => from, "to" => to}) do %{ - "from" => Enum.map(from, &(String.to_integer(&1))), - "to" => Enum.map(to, &(String.to_integer(&1))), + "from" => Enum.map(from, &String.to_integer(&1)), + "to" => Enum.map(to, &String.to_integer(&1)) } end @@ -130,7 +138,7 @@ defmodule ChessWeb.GameChannel do board: Board.transform(game.board), turn: game.turn, state: game.state, - moves: MoveList.transform(game.moves), + moves: MoveList.transform(game.moves) } ChessWeb.Endpoint.broadcast("game:#{game.id}", "game:update", payload) diff --git a/lib/chess_web/templates/game/board.html.leex b/lib/chess_web/templates/game/board.html.leex index 437d2e6..c5d3ede 100644 --- a/lib/chess_web/templates/game/board.html.leex +++ b/lib/chess_web/templates/game/board.html.leex @@ -1,4 +1,4 @@ -
+
1
2
@@ -22,14 +22,14 @@
- <%= for rank <- 0..7 do %> + <%= for {rank, row} <- @board do %>
- <%= for file <- 0..7 do %> + <%= for {file, piece} <- row do %> <%= render ChessWeb.SquareView, "square.html", rank: rank, file: file, - piece: @game.board["#{file},#{rank}"], + piece: piece, selected: {file, rank} == @selected, available: {file, rank} in @available %> <% end %> diff --git a/lib/chess_web/views/live/board_live.ex b/lib/chess_web/views/live/board_live.ex index ff7612d..c834bb2 100644 --- a/lib/chess_web/views/live/board_live.ex +++ b/lib/chess_web/views/live/board_live.ex @@ -5,6 +5,7 @@ defmodule ChessWeb.BoardLive do alias Chess.Store.Game alias Chess.Repo alias Chess.Board + alias Chess.Moves import Chess.Auth, only: [get_user!: 1] @@ -19,11 +20,24 @@ defmodule ChessWeb.BoardLive do Game.for_user(user) |> Repo.get!(game_id) - {:ok, assign(socket, game: game, user: user, selected: nil, available: [])} + {:ok, assign(socket, default_assigns(game, user))} end def handle_event("click", %{"rank" => rank, "file" => file}, socket) do - {:noreply, socket |> handle_click(file, rank)} + { + :noreply, + socket |> handle_click(String.to_integer(file), String.to_integer(rank)) + } + end + + defp default_assigns(game, user) do + %{ + board: Board.transform(game.board), + game: game, + user: user, + selected: nil, + available: [] + } end defp handle_click(socket, file, rank) do @@ -34,27 +48,56 @@ defmodule ChessWeb.BoardLive do colour = ChessWeb.GameView.player_colour(user, game) assigns = - case socket.assigns do - %{:selected => nil} -> - case Board.piece(board, {file, rank}) do - %{"colour" => ^colour} -> - [{:selected, selected(file, rank)}] + if colour == game.turn do + case socket.assigns do + %{selected: nil} -> + handle_selection(board, colour, file, rank) - _ -> - [] - end - - _ -> - [{:selected, nil}] + _ -> + handle_move(socket.assigns, file, rank) + end end assign(socket, assigns) end - defp selected(file, rank) do - { - String.to_integer(file), - String.to_integer(rank) - } + defp handle_selection(board, colour, file, rank) do + case Board.piece(board, {file, rank}) do + %{"colour" => ^colour} -> + [ + {:selected, {file, rank}}, + {:available, Moves.available(board, {file, rank})} + ] + + _ -> + [] + end + end + + defp handle_move( + %{game: game, available: available, selected: selected}, + file, + rank + ) do + if {file, rank} in available do + new_game = + game + |> Moves.make_move(%{from: selected, to: {file, rank}}) + |> case do + {:ok, %{game: new_game}} -> + new_game + end + + new_board = Board.transform(new_game.board) + + [ + {:selected, nil}, + {:available, []}, + {:board, new_board}, + {:game, new_game} + ] + else + [{:selected, nil}, {:available, []}] + end end end diff --git a/test/chess/board_test.exs b/test/chess/board_test.exs index b6d829d..c2ec727 100644 --- a/test/chess/board_test.exs +++ b/test/chess/board_test.exs @@ -6,35 +6,62 @@ defmodule Chess.BoardTest do alias Chess.Board test "returns a piece from the board" do - board = Board.default + board = Board.default() expected_piece = %{"type" => "pawn", "colour" => "white"} assert Board.piece(board, {4, 1}) == expected_piece end test "finds pieces on the board" do - board = Board.default + board = Board.default() piece = %{"type" => "pawn", "colour" => "white"} + expected_result = [ - {0, 1}, {1, 1}, {2, 1}, {3, 1}, {4, 1}, {5, 1}, {6, 1}, {7, 1}, + {0, 1}, + {1, 1}, + {2, 1}, + {3, 1}, + {4, 1}, + {5, 1}, + {6, 1}, + {7, 1} ] + assert Board.search(board, piece) == expected_result end test "finds pieces on the board with a partial search" do - board = Board.default + board = Board.default() piece = %{"colour" => "white"} - expected_result = [ - {0, 1}, {1, 1}, {2, 1}, {3, 1}, {4, 1}, {5, 1}, {6, 1}, {7, 1}, - {0, 0}, {1, 0}, {2, 0}, {3, 0}, {4, 0}, {5, 0}, {6, 0}, {7, 0}, - ] |> Enum.sort + + expected_result = + [ + {0, 1}, + {1, 1}, + {2, 1}, + {3, 1}, + {4, 1}, + {5, 1}, + {6, 1}, + {7, 1}, + {0, 0}, + {1, 0}, + {2, 0}, + {3, 0}, + {4, 0}, + {5, 0}, + {6, 0}, + {7, 0} + ] + |> Enum.sort() + assert Board.search(board, piece) == expected_result end test "finds a single piece on the board" do - board = Board.default + board = Board.default() piece = %{"type" => "king", "colour" => "black"} assert Board.search(board, piece) == [{4, 7}] @@ -42,44 +69,41 @@ defmodule Chess.BoardTest do test "moves a piece" do board = %{ - "3,0" => %{"type" => "queen", "colour" => "white"}, + "3,0" => %{"type" => "queen", "colour" => "white"} } - %{board: new_board} = - Board.move_piece(board, %{"from" => [3, 0], "to" => [5, 2]}) + %{board: new_board} = Board.move_piece(board, %{from: {3, 0}, to: {5, 2}}) assert new_board == %{ - "5,2" => %{"type" => "queen", "colour" => "white"}, - } + "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"}, + "7,0" => %{"type" => "rook", "colour" => "white"} } - %{board: new_board} = - Board.move_piece(board, %{"from" => [4, 0], "to" => [6, 0]}) + %{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"}, - } + "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"}, + "0,0" => %{"type" => "rook", "colour" => "white"} } - %{board: new_board} = - Board.move_piece(board, %{"from" => [4, 0], "to" => [2, 0]}) + %{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"}, - } + "2,0" => %{"type" => "king", "colour" => "white"}, + "3,0" => %{"type" => "rook", "colour" => "white"} + } end end diff --git a/test/chess/store/game_test.exs b/test/chess/store/game_test.exs index cc972b5..a841b37 100644 --- a/test/chess/store/game_test.exs +++ b/test/chess/store/game_test.exs @@ -20,6 +20,7 @@ defmodule Chess.Store.GameTest do opponent_id: opponent.id, turn: "white" } + changeset = Game.changeset(%Game{}, attrs) assert changeset.valid? @@ -33,6 +34,7 @@ defmodule Chess.Store.GameTest do opponent_id: 2, turn: "white" } + changeset = Game.changeset(%Game{}, attrs) assert changeset.valid? @@ -81,18 +83,20 @@ defmodule Chess.Store.GameTest do user = insert(:user) opponent = insert(:opponent) - game = insert(:game, %{ - board: Board.default, - user_id: user.id, - opponent_id: opponent.id, - }) + game = + insert(:game, %{ + board: Board.default(), + user_id: user.id, + opponent_id: opponent.id + }) - move_params = %{"from" => [4, 1], "to" => [4, 3]} + move_params = %{from: {4, 1}, to: {4, 3}} - changeset = Game.move_changeset( - game, - Board.move_piece(game.board, move_params) - ) + changeset = + Game.move_changeset( + game, + Board.move_piece(game.board, move_params) + ) assert {:ok, new_game} = Repo.update(changeset) assert new_game.turn == "black"