1
0
mirror of https://github.com/danbee/chess synced 2025-03-04 08:39:06 +00:00

Game now works using LiveView

This commit is contained in:
Daniel Barber 2023-10-25 16:21:02 -05:00
parent 1e411e2e73
commit 2314dc6a19
12 changed files with 119 additions and 80 deletions

View File

@ -8,16 +8,13 @@
width: 1.5%; width: 1.5%;
} }
.board__container {
grid-area: board;
}
.board { .board {
background: $background-color; background: $background-color;
border-collapse: unset; border-collapse: unset;
border-radius: 2.8%; border-radius: 2.8%;
border-spacing: 1px; border-spacing: 1px;
color: $foreground-color; color: $foreground-color;
grid-area: board;
height: var(--board-size); height: var(--board-size);
padding: calc(var(--board-size) / 20); padding: calc(var(--board-size) / 20);
position: relative; position: relative;
@ -61,14 +58,6 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
.board__body {
flex-direction: column-reverse;
}
.board__row {
flex-direction: row;
}
} }
.board--player-is-black { .board--player-is-black {
@ -81,45 +70,20 @@
display: flex; display: flex;
flex-direction: row-reverse; flex-direction: row-reverse;
} }
.board__body {
flex-direction: column;
}
.board__row {
flex-direction: row-reverse;
}
} }
.board__body { .board__body {
background: $background-color; background: $background-color;
border: 0.25rem solid $foreground-color; border: 0.25rem solid $foreground-color;
border-radius: calc(var(--board-size) / 100); border-radius: calc(var(--board-size) / 100);
display: flex; display: grid;
flex-direction: column; grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(8, 1fr);
height: 100%; height: 100%;
padding: 1%; padding: 1%;
width: 100%; 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 { .board__rank-labels {
display: none; display: none;
height: 90%; height: 90%;

View File

@ -1,6 +1,8 @@
defmodule Chess.Board do defmodule Chess.Board do
@moduledoc false @moduledoc false
require Logger
def search(board, %{"type" => type, "colour" => colour}) do def search(board, %{"type" => type, "colour" => colour}) do
board board
|> Enum.filter(fn {_index, piece} -> |> Enum.filter(fn {_index, piece} ->
@ -17,9 +19,13 @@ defmodule Chess.Board do
|> indexes_to_tuples |> indexes_to_tuples
end end
def piece(board, {file, rank}) do
board["#{file},#{rank}"]
end
def move_piece(board, %{ def move_piece(board, %{
"from" => [from_file, from_rank], from: {from_file, from_rank},
"to" => [to_file, to_rank] to: {to_file, to_rank}
}) do }) do
{piece, board} = Map.pop(board, to_index({from_file, from_rank})) {piece, board} = Map.pop(board, to_index({from_file, from_rank}))
{piece_captured, board} = Map.pop(board, to_index({to_file, to_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 def castling_move(board, %{from: {4, rank}, to: {2, _rank}}) do
move_piece(board, %{ move_piece(board, %{
"from" => [0, rank], from: {0, rank},
"to" => [3, rank] to: {3, rank}
}) })
end 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, %{ move_piece(board, %{
"from" => [7, rank], from: {7, rank},
"to" => [5, rank] to: {5, rank}
}) })
end end

View File

@ -14,6 +14,8 @@ defmodule Chess.Moves do
alias Chess.Moves.Pieces.Queen alias Chess.Moves.Pieces.Queen
alias Chess.Moves.Pieces.King alias Chess.Moves.Pieces.King
require Logger
def make_move(game, move_params) do def make_move(game, move_params) do
params = params =
game.board game.board

View File

@ -39,7 +39,8 @@ defmodule ChessWeb do
# Import convenience functions from controllers # Import convenience functions from controllers
import Phoenix.Controller, import Phoenix.Controller,
only: [get_csrf_token: 0, get_flash: 2, view_module: 1] 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 all HTML functionality (forms, tags, etc)
use Phoenix.HTML use Phoenix.HTML
@ -52,11 +53,10 @@ defmodule ChessWeb do
end end
end end
def live_view do def live_view do
quote do quote do
use Phoenix.LiveView, use Phoenix.LiveView,
layout: {ChessWeb.LayoutView, "live.html"} layout: {ChessWeb.LayoutView, :live}
unquote(view_helpers()) unquote(view_helpers())
end end
@ -90,6 +90,7 @@ defmodule ChessWeb do
def router do def router do
quote do quote do
use Phoenix.Router use Phoenix.Router
import Phoenix.LiveView.Router import Phoenix.LiveView.Router
end end
end end

View File

@ -5,7 +5,6 @@ defmodule ChessWeb.GameChannel do
import ChessWeb.GameView, only: [player: 2, opponent: 2] import ChessWeb.GameView, only: [player: 2, opponent: 2]
alias Chess.Board
alias Chess.Emails alias Chess.Emails
alias Chess.Mailer alias Chess.Mailer
alias Chess.MoveList alias Chess.MoveList
@ -29,7 +28,7 @@ defmodule ChessWeb.GameChannel do
opponent_id: opponent(game, socket.assigns.user_id).id, opponent_id: opponent(game, socket.assigns.user_id).id,
player: player(game, socket.assigns.user_id), player: player(game, socket.assigns.user_id),
opponent: opponent(game, socket.assigns.user_id).name, opponent: opponent(game, socket.assigns.user_id).name,
board: Board.transform(game.board), board: game.board,
turn: game.turn, turn: game.turn,
state: game.state, state: game.state,
moves: MoveList.transform(game.moves) moves: MoveList.transform(game.moves)
@ -135,7 +134,7 @@ defmodule ChessWeb.GameChannel do
|> Queries.game_with_moves(socket.assigns.game_id) |> Queries.game_with_moves(socket.assigns.game_id)
payload = %{ payload = %{
board: Board.transform(game.board), board: game.board,
turn: game.turn, turn: game.turn,
state: game.state, state: game.state,
moves: MoveList.transform(game.moves) moves: MoveList.transform(game.moves)

View File

@ -4,15 +4,14 @@ defmodule ChessWeb.Endpoint do
@session_options [ @session_options [
store: :cookie, store: :cookie,
key: "_chess_key", key: "_chess_key",
signing_salt: "9LqUhZTU" signing_salt: "9LqUhZTU",
same_site: "Lax"
] ]
if sandbox = Application.compile_env(:chess, :sandbox) do if sandbox = Application.compile_env(:chess, :sandbox) do
plug(Phoenix.Ecto.SQL.Sandbox, sandbox: sandbox) plug(Phoenix.Ecto.SQL.Sandbox, sandbox: sandbox)
end end
socket("/socket", ChessWeb.UserSocket)
socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]]) socket("/live", Phoenix.LiveView.Socket, websocket: [connect_info: [session: @session_options]])
# Serve at "/" the static files from "priv/static" directory. # Serve at "/" the static files from "priv/static" directory.

View File

@ -42,13 +42,6 @@ defmodule ChessWeb.Router do
resources("/password", PasswordController, only: [:edit, :update], singleton: true) resources("/password", PasswordController, only: [:edit, :update], singleton: true)
end 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 if Mix.env() == :dev do
forward("/sent_emails", Bamboo.SentEmailViewerPlug) forward("/sent_emails", Bamboo.SentEmailViewerPlug)
end end

View File

@ -21,9 +21,12 @@
<div class="board__label">h</div> <div class="board__label">h</div>
</div> </div>
<% rank_range = if white?(@user, @game), do: 7..0, else: 0..7 %>
<% file_range = if black?(@user, @game), do: 7..0, else: 0..7 %>
<div class="board__body"> <div class="board__body">
<%= for rank <- 0..7 do %> <%= for rank <- rank_range do %>
<%= for file <- 0..7 do %> <%= for file <- file_range do %>
<%= render ChessWeb.SquareView, <%= render ChessWeb.SquareView,
"square.html", "square.html",
rank: rank, rank: rank,
@ -36,6 +39,6 @@
</div> </div>
<div class="game-state game-state--<%= @game.state %>"> <div class="game-state game-state--<%= @game.state %>">
<%= states(@game.state) %> <%= state_text(@game.state) %>
</div> </div>
</div> </div>

View File

@ -15,10 +15,23 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <%= for {move_pair, i} <- @game.moves |>
<th scope="row" class="move-list__line-number">1.</th> Enum.chunk_every(2) |> Enum.with_index do %>
<td class="move-list__move move-list__move--white">e4</td> <tr>
</tr> <th scope="row" class="move-list__line-number"><%= i + 1 %>.</th>
<%= case move_pair do %>
<% [white_move, black_move] -> %>
<td class="move-list__move move-list__move--<%=
white_move.piece["colour"] %>"><%= move_text(white_move) %></td>
<td class="move-list__move move-list__move--<%=
black_move.piece["colour"] %>"><%= move_text(black_move) %></td>
<% [white_move] -> %>
<td class="move-list__move move-list__move--<%=
white_move.piece["colour"] %>"><%= move_text(white_move) %></td>
<td></td>
<% end %>
</tr>
<% end %>
</tbody> </tbody>
</table> </table>
</div> </div>

View File

@ -6,6 +6,15 @@ defmodule ChessWeb.GameView do
import Phoenix.Component import Phoenix.Component
import Chess.Auth, only: [current_user: 1] 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 def won_lost(user, game) do
if game_over?(game) && game.state == "checkmate" do if game_over?(game) && game.state == "checkmate" do
(your_turn?(user, game) && (your_turn?(user, game) &&
@ -21,7 +30,7 @@ defmodule ChessWeb.GameView do
def state(user, game) do def state(user, game) do
cond do cond do
GameState.game_over?(game) -> GameState.game_over?(game) ->
states(game.state) state_text(game.state)
your_turn?(user, game) -> your_turn?(user, game) ->
gettext("Your turn") gettext("Your turn")
@ -37,6 +46,14 @@ defmodule ChessWeb.GameView do
end end
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 def your_turn?(user, game) do
user user
|> player_colour(game) == game.turn |> player_colour(game) == game.turn
@ -47,7 +64,7 @@ defmodule ChessWeb.GameView do
end end
def piece(board, {file, rank}) do def piece(board, {file, rank}) do
board["#{file},#{rank}"] Chess.Board.piece(board, {file, rank})
end end
def files(user, game) do def files(user, game) do
@ -79,7 +96,19 @@ defmodule ChessWeb.GameView do
end end
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( Map.get(
%{ %{
"checkmate" => gettext("Checkmate!"), "checkmate" => gettext("Checkmate!"),

View File

@ -3,12 +3,18 @@ defmodule ChessWeb.BoardLive do
alias Chess.Store.User alias Chess.Store.User
alias Chess.Store.Game alias Chess.Store.Game
alias Chess.Store.Move
alias Chess.Repo alias Chess.Repo
alias Chess.Board
alias Chess.Moves alias Chess.Moves
alias ChessWeb.GameView
import Ecto.Query
require Logger
def render(assigns) do def render(assigns) do
Phoenix.View.render(ChessWeb.GameView, "board.html", assigns) Phoenix.View.render(GameView, "board.html", assigns)
end end
def mount(_params, %{"user_id" => user_id, "game_id" => game_id}, socket) do def mount(_params, %{"user_id" => user_id, "game_id" => game_id}, socket) do
@ -41,6 +47,7 @@ defmodule ChessWeb.BoardLive do
end end
def handle_info(%{event: "move", payload: state}, socket) do def handle_info(%{event: "move", payload: state}, socket) do
Logger.info("Handling move from board")
{:noreply, assign(socket, state)} {:noreply, assign(socket, state)}
end end
@ -49,7 +56,7 @@ defmodule ChessWeb.BoardLive do
board = game.board board = game.board
user = socket.assigns[:user] user = socket.assigns[:user]
colour = ChessWeb.GameView.player_colour(user, game) colour = GameView.player_colour(user, game)
assigns = assigns =
if colour == game.turn do if colour == game.turn do
@ -66,7 +73,7 @@ defmodule ChessWeb.BoardLive do
end end
defp handle_selection(board, colour, file, rank) do defp handle_selection(board, colour, file, rank) do
case Board.piece(board, {file, rank}) do case GameView.piece(board, {file, rank}) do
%{"colour" => ^colour} -> %{"colour" => ^colour} ->
[ [
{:selected, {file, rank}}, {:selected, {file, rank}},
@ -88,14 +95,21 @@ defmodule ChessWeb.BoardLive do
|> Moves.make_move(%{from: selected, to: {file, rank}}) |> Moves.make_move(%{from: selected, to: {file, rank}})
|> case do |> case do
{:ok, %{game: game}} -> {:ok, %{game: game}} ->
board = Board.transform(game.board) game
|> Repo.preload([:user, :opponent])
broadcast_move(game, board) |> Repo.preload(
moves:
from(
move in Move,
order_by: [asc: move.inserted_at]
)
)
|> broadcast_move(game.board)
[ [
{:selected, nil}, {:selected, nil},
{:available, []}, {:available, []},
{:board, board}, {:board, game.board},
{:game, game} {:game, game}
] ]
end end

View File

@ -3,23 +3,39 @@ defmodule ChessWeb.GameInfoLive do
alias Chess.Store.User alias Chess.Store.User
alias Chess.Store.Game alias Chess.Store.Game
alias Chess.Store.Move
alias Chess.Repo alias Chess.Repo
import Ecto.Query import Ecto.Query
require Logger
def render(assigns) do def render(assigns) do
Phoenix.View.render(ChessWeb.GameView, "game_info.html", assigns) Phoenix.View.render(ChessWeb.GameView, "game_info.html", assigns)
end end
def mount(_params, %{"game_id" => game_id, "user_id" => user_id}, socket) do 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) user = Repo.get!(User, user_id)
game = game =
Game.for_user(user) Game.for_user(user)
|> preload(:user) |> preload(:user)
|> preload(:opponent) |> preload(:opponent)
|> preload(
moves:
^from(
move in Move,
order_by: [asc: move.inserted_at]
)
)
|> Repo.get!(game_id) |> Repo.get!(game_id)
{:ok, assign(socket, game: game, user: user)} {:ok, assign(socket, game: game, user: user)}
end end
def handle_info(%{event: "move", payload: state}, socket) do
{:noreply, assign(socket, state)}
end
end end