From 2314dc6a19a4886cef19b22db1d09d5abba502b3 Mon Sep 17 00:00:00 2001 From: Dan Barber Date: Wed, 25 Oct 2023 16:21:02 -0500 Subject: [PATCH] Game now works using LiveView --- assets/css/components/_board.scss | 44 ++----------------- lib/chess/board.ex | 18 +++++--- lib/chess/moves.ex | 2 + lib/chess_web.ex | 7 +-- lib/chess_web/channels/game_channel.ex | 5 +-- lib/chess_web/endpoint.ex | 5 +-- lib/chess_web/router.ex | 7 --- lib/chess_web/templates/game/board.html.leex | 9 ++-- .../templates/game/game_info.html.leex | 21 +++++++-- lib/chess_web/views/game_view.ex | 35 +++++++++++++-- lib/chess_web/views/live/board_live.ex | 30 +++++++++---- lib/chess_web/views/live/game_info_live.ex | 16 +++++++ 12 files changed, 119 insertions(+), 80 deletions(-) diff --git a/assets/css/components/_board.scss b/assets/css/components/_board.scss index c9fac3c..5d0ef5f 100644 --- a/assets/css/components/_board.scss +++ b/assets/css/components/_board.scss @@ -8,16 +8,13 @@ width: 1.5%; } -.board__container { - grid-area: board; -} - .board { background: $background-color; border-collapse: unset; border-radius: 2.8%; border-spacing: 1px; color: $foreground-color; + grid-area: board; height: var(--board-size); padding: calc(var(--board-size) / 20); position: relative; @@ -61,14 +58,6 @@ display: flex; flex-direction: row; } - - .board__body { - flex-direction: column-reverse; - } - - .board__row { - flex-direction: row; - } } .board--player-is-black { @@ -81,45 +70,20 @@ display: flex; flex-direction: row-reverse; } - - .board__body { - flex-direction: column; - } - - .board__row { - flex-direction: row-reverse; - } } .board__body { background: $background-color; border: 0.25rem solid $foreground-color; border-radius: calc(var(--board-size) / 100); - display: flex; - flex-direction: column; + display: grid; + grid-template-columns: repeat(8, 1fr); + grid-template-rows: repeat(8, 1fr); height: 100%; padding: 1%; width: 100%; } -.board__row { - display: flex; - flex-grow: 1; - - @include odd-between(1, 8) { - .square { - @include odd-between(1, 8) { @extend %square--black; } - @include even-between(1, 8) { @extend %square--white; } - } - } - @include even-between(1, 8) { - .square { - @include odd-between(1, 8) { @extend %square--white; } - @include even-between(1, 8) { @extend %square--black; } - } - } -} - .board__rank-labels { display: none; height: 90%; diff --git a/lib/chess/board.ex b/lib/chess/board.ex index e533e99..63f18fd 100644 --- a/lib/chess/board.ex +++ b/lib/chess/board.ex @@ -1,6 +1,8 @@ defmodule Chess.Board do @moduledoc false + require Logger + def search(board, %{"type" => type, "colour" => colour}) do board |> Enum.filter(fn {_index, piece} -> @@ -17,9 +19,13 @@ defmodule Chess.Board do |> indexes_to_tuples end + def piece(board, {file, rank}) do + board["#{file},#{rank}"] + end + def move_piece(board, %{ - "from" => [from_file, from_rank], - "to" => [to_file, to_rank] + 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})) @@ -54,15 +60,15 @@ defmodule Chess.Board 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 move_piece(board, %{ - "from" => [7, rank], - "to" => [5, rank] + from: {7, rank}, + to: {5, rank} }) end diff --git a/lib/chess/moves.ex b/lib/chess/moves.ex index 3fc1c00..73f729a 100644 --- a/lib/chess/moves.ex +++ b/lib/chess/moves.ex @@ -14,6 +14,8 @@ defmodule Chess.Moves do alias Chess.Moves.Pieces.Queen alias Chess.Moves.Pieces.King + require Logger + def make_move(game, move_params) do params = game.board diff --git a/lib/chess_web.ex b/lib/chess_web.ex index aa29f97..db3c672 100644 --- a/lib/chess_web.ex +++ b/lib/chess_web.ex @@ -39,7 +39,8 @@ defmodule ChessWeb do # Import convenience functions from controllers import Phoenix.Controller, only: [get_csrf_token: 0, get_flash: 2, view_module: 1] - import Phoenix.LiveView.Helpers + + import Phoenix.Component # Use all HTML functionality (forms, tags, etc) use Phoenix.HTML @@ -52,11 +53,10 @@ defmodule ChessWeb do end end - def live_view do quote do use Phoenix.LiveView, - layout: {ChessWeb.LayoutView, "live.html"} + layout: {ChessWeb.LayoutView, :live} unquote(view_helpers()) end @@ -90,6 +90,7 @@ defmodule ChessWeb do def router do quote do use Phoenix.Router + import Phoenix.LiveView.Router end end diff --git a/lib/chess_web/channels/game_channel.ex b/lib/chess_web/channels/game_channel.ex index e3707c6..9f85bd9 100644 --- a/lib/chess_web/channels/game_channel.ex +++ b/lib/chess_web/channels/game_channel.ex @@ -5,7 +5,6 @@ defmodule ChessWeb.GameChannel do import ChessWeb.GameView, only: [player: 2, opponent: 2] - alias Chess.Board alias Chess.Emails alias Chess.Mailer alias Chess.MoveList @@ -29,7 +28,7 @@ defmodule ChessWeb.GameChannel do opponent_id: opponent(game, socket.assigns.user_id).id, player: player(game, socket.assigns.user_id), opponent: opponent(game, socket.assigns.user_id).name, - board: Board.transform(game.board), + board: game.board, turn: game.turn, state: game.state, moves: MoveList.transform(game.moves) @@ -135,7 +134,7 @@ defmodule ChessWeb.GameChannel do |> Queries.game_with_moves(socket.assigns.game_id) payload = %{ - board: Board.transform(game.board), + board: game.board, turn: game.turn, state: game.state, moves: MoveList.transform(game.moves) diff --git a/lib/chess_web/endpoint.ex b/lib/chess_web/endpoint.ex index 9a5b77a..fd404cd 100644 --- a/lib/chess_web/endpoint.ex +++ b/lib/chess_web/endpoint.ex @@ -4,15 +4,14 @@ defmodule ChessWeb.Endpoint do @session_options [ store: :cookie, key: "_chess_key", - signing_salt: "9LqUhZTU" + signing_salt: "9LqUhZTU", + same_site: "Lax" ] if sandbox = Application.compile_env(:chess, :sandbox) do plug(Phoenix.Ecto.SQL.Sandbox, sandbox: sandbox) end - socket("/socket", ChessWeb.UserSocket) - socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]) # Serve at "/" the static files from "priv/static" directory. diff --git a/lib/chess_web/router.ex b/lib/chess_web/router.ex index d640d43..bb56960 100644 --- a/lib/chess_web/router.ex +++ b/lib/chess_web/router.ex @@ -42,13 +42,6 @@ defmodule ChessWeb.Router do resources("/password", PasswordController, only: [:edit, :update], singleton: true) end - # Other scopes may use custom stacks. - scope "/api", as: :api do - pipe_through([:api, :auth, :ensure_auth]) - - resources("/opponents", ChessWeb.Api.OpponentsController, only: [:index]) - end - if Mix.env() == :dev do forward("/sent_emails", Bamboo.SentEmailViewerPlug) end diff --git a/lib/chess_web/templates/game/board.html.leex b/lib/chess_web/templates/game/board.html.leex index d11be6e..f243169 100644 --- a/lib/chess_web/templates/game/board.html.leex +++ b/lib/chess_web/templates/game/board.html.leex @@ -21,9 +21,12 @@
h
+ <% rank_range = if white?(@user, @game), do: 7..0, else: 0..7 %> + <% file_range = if black?(@user, @game), do: 7..0, else: 0..7 %> +
- <%= for rank <- 0..7 do %> - <%= for file <- 0..7 do %> + <%= for rank <- rank_range do %> + <%= for file <- file_range do %> <%= render ChessWeb.SquareView, "square.html", rank: rank, @@ -36,6 +39,6 @@
- <%= states(@game.state) %> + <%= state_text(@game.state) %>
diff --git a/lib/chess_web/templates/game/game_info.html.leex b/lib/chess_web/templates/game/game_info.html.leex index 8c2c50a..317cab7 100644 --- a/lib/chess_web/templates/game/game_info.html.leex +++ b/lib/chess_web/templates/game/game_info.html.leex @@ -15,10 +15,23 @@ - - 1. - e4 - + <%= for {move_pair, i} <- @game.moves |> + Enum.chunk_every(2) |> Enum.with_index do %> + + <%= i + 1 %>. + <%= case move_pair do %> + <% [white_move, black_move] -> %> + "><%= move_text(white_move) %> + "><%= move_text(black_move) %> + <% [white_move] -> %> + "><%= move_text(white_move) %> + + <% end %> + + <% end %> diff --git a/lib/chess_web/views/game_view.ex b/lib/chess_web/views/game_view.ex index 2be10d8..a76ecc7 100644 --- a/lib/chess_web/views/game_view.ex +++ b/lib/chess_web/views/game_view.ex @@ -6,6 +6,15 @@ defmodule ChessWeb.GameView do import Phoenix.Component import Chess.Auth, only: [current_user: 1] + @pieces %{ + pawn: "", + knight: "N", + bishop: "B", + rook: "R", + queen: "Q", + king: "K" + } + def won_lost(user, game) do if game_over?(game) && game.state == "checkmate" do (your_turn?(user, game) && @@ -21,7 +30,7 @@ defmodule ChessWeb.GameView do def state(user, game) do cond do GameState.game_over?(game) -> - states(game.state) + state_text(game.state) your_turn?(user, game) -> gettext("Your turn") @@ -37,6 +46,14 @@ defmodule ChessWeb.GameView do end end + def white?(user, game) do + player_colour(user, game) == "white" + end + + def black?(user, game) do + player_colour(user, game) == "black" + end + def your_turn?(user, game) do user |> player_colour(game) == game.turn @@ -47,7 +64,7 @@ defmodule ChessWeb.GameView do end def piece(board, {file, rank}) do - board["#{file},#{rank}"] + Chess.Board.piece(board, {file, rank}) end def files(user, game) do @@ -79,7 +96,19 @@ defmodule ChessWeb.GameView do end end - def states(state) do + def move_text(move) do + move = Chess.Store.Move.transform(move) + + piece_type = move.piece["type"] |> String.to_atom() + + [ + @pieces[piece_type], + move.to + ] + |> Enum.join() + end + + def state_text(state) do Map.get( %{ "checkmate" => gettext("Checkmate!"), diff --git a/lib/chess_web/views/live/board_live.ex b/lib/chess_web/views/live/board_live.ex index e58d732..ac062fb 100644 --- a/lib/chess_web/views/live/board_live.ex +++ b/lib/chess_web/views/live/board_live.ex @@ -3,12 +3,18 @@ defmodule ChessWeb.BoardLive do alias Chess.Store.User alias Chess.Store.Game + alias Chess.Store.Move alias Chess.Repo - alias Chess.Board alias Chess.Moves + alias ChessWeb.GameView + + import Ecto.Query + + require Logger + def render(assigns) do - Phoenix.View.render(ChessWeb.GameView, "board.html", assigns) + Phoenix.View.render(GameView, "board.html", assigns) end def mount(_params, %{"user_id" => user_id, "game_id" => game_id}, socket) do @@ -41,6 +47,7 @@ defmodule ChessWeb.BoardLive do end def handle_info(%{event: "move", payload: state}, socket) do + Logger.info("Handling move from board") {:noreply, assign(socket, state)} end @@ -49,7 +56,7 @@ defmodule ChessWeb.BoardLive do board = game.board user = socket.assigns[:user] - colour = ChessWeb.GameView.player_colour(user, game) + colour = GameView.player_colour(user, game) assigns = if colour == game.turn do @@ -66,7 +73,7 @@ defmodule ChessWeb.BoardLive do end defp handle_selection(board, colour, file, rank) do - case Board.piece(board, {file, rank}) do + case GameView.piece(board, {file, rank}) do %{"colour" => ^colour} -> [ {:selected, {file, rank}}, @@ -88,14 +95,21 @@ defmodule ChessWeb.BoardLive do |> Moves.make_move(%{from: selected, to: {file, rank}}) |> case do {:ok, %{game: game}} -> - board = Board.transform(game.board) - - broadcast_move(game, board) + game + |> Repo.preload([:user, :opponent]) + |> Repo.preload( + moves: + from( + move in Move, + order_by: [asc: move.inserted_at] + ) + ) + |> broadcast_move(game.board) [ {:selected, nil}, {:available, []}, - {:board, board}, + {:board, game.board}, {:game, game} ] end diff --git a/lib/chess_web/views/live/game_info_live.ex b/lib/chess_web/views/live/game_info_live.ex index f4447db..63e873e 100644 --- a/lib/chess_web/views/live/game_info_live.ex +++ b/lib/chess_web/views/live/game_info_live.ex @@ -3,23 +3,39 @@ defmodule ChessWeb.GameInfoLive do alias Chess.Store.User alias Chess.Store.Game + alias Chess.Store.Move alias Chess.Repo import Ecto.Query + require Logger + def render(assigns) do Phoenix.View.render(ChessWeb.GameView, "game_info.html", assigns) end def mount(_params, %{"game_id" => game_id, "user_id" => user_id}, socket) do + ChessWeb.Endpoint.subscribe("game:#{game_id}") + user = Repo.get!(User, user_id) game = Game.for_user(user) |> preload(:user) |> preload(:opponent) + |> preload( + moves: + ^from( + move in Move, + order_by: [asc: move.inserted_at] + ) + ) |> Repo.get!(game_id) {:ok, assign(socket, game: game, user: user)} end + + def handle_info(%{event: "move", payload: state}, socket) do + {:noreply, assign(socket, state)} + end end