diff --git a/assets/js/services/channel.js b/assets/js/services/channel.js index 8b29412..2330de2 100644 --- a/assets/js/services/channel.js +++ b/assets/js/services/channel.js @@ -34,7 +34,10 @@ class Channel { } sendMove(move) { - this.channel.push("game:move", move); + this.channel.push("game:move", move) + .receive("error", resp => { + alert(resp.message); + }); } } diff --git a/lib/chess/moves/piece.ex b/lib/chess/moves/piece.ex index 3c75381..10f8a3a 100644 --- a/lib/chess/moves/piece.ex +++ b/lib/chess/moves/piece.ex @@ -54,14 +54,17 @@ defmodule Chess.Moves.Piece do defp _attacked?(_board, {7, _rank}, {1, _}, _), do: false defp _attacked?(_board, {_file, 7}, {_, 1}, _), do: false defp _attacked?(board, {file, rank}, {fv, rv}, pieces) do - {file, rank} = - board - |> Generator.moves({file, rank}, {fv, rv}) - |> List.last + board + |> Generator.moves({file, rank}, {fv, rv}) + |> List.last + |> case do + {file, rank} -> + piece = board["#{file},#{rank}"] - piece = board["#{file},#{rank}"] - - Enum.any?(pieces, &(match?(%{"type" => ^&1}, piece))) + Enum.any?(pieces, &(match?(%{"type" => ^&1}, piece))) + nil -> + false + end end defp _attacked?(board, {file, rank}, pattern, piece_type) do diff --git a/lib/chess/store/game.ex b/lib/chess/store/game.ex index 2f5387d..14dfcf0 100644 --- a/lib/chess/store/game.ex +++ b/lib/chess/store/game.ex @@ -28,6 +28,12 @@ defmodule Chess.Store.Game do |> foreign_key_constraint(:opponent_id) end + def move_changeset(struct, params \\ %{}) do + struct + |> cast(params, required_attrs()) + |> validate_king_in_check(struct, params) + end + def change_turn("black"), do: "white" def change_turn("white"), do: "black" @@ -41,6 +47,20 @@ defmodule Chess.Store.Game do or_where: game.opponent_id == ^user_id end + def validate_king_in_check(changeset, %Game{turn: turn}, %{board: board}) do + case Board.king_in_check?(board, turn) do + true -> + changeset + |> add_error( + :board, + "That move would result in your king being in check" + ) + _ -> + changeset + end + end + def validate_king_in_check(changeset, _, _), do: changeset + def ordered(query) do query |> order_by([game], desc: game.inserted_at) diff --git a/lib/chess_web/channels/game_channel.ex b/lib/chess_web/channels/game_channel.ex index 50e01be..4fe6d4b 100644 --- a/lib/chess_web/channels/game_channel.ex +++ b/lib/chess_web/channels/game_channel.ex @@ -24,7 +24,9 @@ defmodule ChessWeb.GameChannel do board: Board.transform(game.board), turn: game.turn } - push(socket, "game:update", payload) + + socket + |> push("game:update", payload) {:noreply, socket} end @@ -35,7 +37,7 @@ defmodule ChessWeb.GameChannel do |> Game.for_user_id() |> Repo.get!(socket.assigns.game_id) - changeset = Game.changeset( + changeset = Game.move_changeset( game, %{ board: Board.move_piece(game.board, move_params), turn: Game.change_turn(game.turn) @@ -46,7 +48,11 @@ defmodule ChessWeb.GameChannel do {:ok, game} -> send_update(game) - {:noreply, socket} + {:noreply, socket} + {:error, changeset} -> + {message, _} = changeset.errors[:board] + + {:reply, {:error, %{message: message}}, socket} end end diff --git a/test/chess/moves/piece_test.exs b/test/chess/moves/piece_test.exs index 9fa6376..a95e79b 100644 --- a/test/chess/moves/piece_test.exs +++ b/test/chess/moves/piece_test.exs @@ -21,6 +21,16 @@ defmodule Chess.Moves.PieceTest do refute Piece.attacked?(board, {4, 0}) end + test "piece next to another piece is being attacked" do + board = %{ + "4,0" => %{"type" => "king", "colour" => "white"}, + "4,1" => %{"type" => "pawn", "colour" => "white"}, + "7,3" => %{"type" => "bishop", "colour" => "black"}, + } + + assert Piece.attacked?(board, {4, 0}) + end + test "piece is not being attacked by piece of its own colour" do board = %{ "4,5" => %{"type" => "king", "colour" => "white"}, diff --git a/test/features/moves_test.exs b/test/features/moves_test.exs index e3cb08b..7ed1e94 100644 --- a/test/features/moves_test.exs +++ b/test/features/moves_test.exs @@ -115,6 +115,75 @@ defmodule Chess.MovesTest do |> assert_has(square_containing("f4-r3", "white.pawn")) end + test "cannot move the king into a position that would result in check", + %{session: session} do + user = insert(:user, %{ + name: "Link", + email: "link@hyrule.com", + password: "ilovezelda" + }) + opponent = insert(:user, %{ + name: "Zelda", + email: "zelda@hyrule.com", + password: "ganonsucks" + }) + insert(:game, %{ + board: %{ + "4,0" => %{"type" => "king", "colour" => "white"}, + "3,7" => %{"type" => "queen", "colour" => "black"}, + }, + user_id: user.id, + opponent_id: opponent.id, + turn: "white", + }) + + session + |> login("link@hyrule.com", "ilovezelda") + |> visit("/games") + |> click(link("Game with Zelda")) + + session + |> click(css("#f4-r0")) + |> click(css("#f3-r0")) + |> refute_has(square_containing("f3-r0", "white.king")) + |> assert_has(square_containing("f4-r0", "white.king")) + end + + test "cannot make a move that would place the king in check", + %{session: session} do + user = insert(:user, %{ + name: "Link", + email: "link@hyrule.com", + password: "ilovezelda" + }) + opponent = insert(:user, %{ + name: "Zelda", + email: "zelda@hyrule.com", + password: "ganonsucks" + }) + insert(:game, %{ + board: %{ + "4,0" => %{"type" => "king", "colour" => "white"}, + "4,1" => %{"type" => "rook", "colour" => "white"}, + "4,7" => %{"type" => "queen", "colour" => "black"}, + }, + user_id: user.id, + opponent_id: opponent.id, + turn: "white", + }) + + session + |> login("link@hyrule.com", "ilovezelda") + |> visit("/games") + |> click(link("Game with Zelda")) + + session + |> click(css("#f4-r1")) + |> click(css("#f2-r1")) + |> refute_has(square_containing("f2-r1", "white.rook")) + |> assert_has(square_containing("f4-r1", "white.rook")) + end + defp square_selected(square) do css("##{square}.selected") end