diff --git a/.gitignore b/.gitignore index 3dbb8bf..ead1914 100644 --- a/.gitignore +++ b/.gitignore @@ -28,7 +28,14 @@ chess-*.tar # Ignore digested assets cache. /priv/static/cache_manifest.json +.elixir_ls + # In case you use Node.js/npm, you want to ignore these. npm-debug.log /assets/node_modules/ +# Ignore log folder +/log/ + +.vscode/ +.idea diff --git a/.tool-versions b/.tool-versions new file mode 100644 index 0000000..d6b6975 --- /dev/null +++ b/.tool-versions @@ -0,0 +1,3 @@ +elixir 1.14.0-otp-25 +erlang 25.1 +nodejs 16.13.2 diff --git a/assets/css/app.css b/assets/css/app.css index 2c2cd8b..24e84ce 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -123,6 +123,14 @@ Chess CSS */ +.hd-section-left { + float: left; +} + +.hd-section-right { + float: right; +} + .board { --square-size: 80px; box-sizing: content-box; @@ -135,6 +143,10 @@ grid-template-rows: repeat(8, var(--square-size)); } +.board .selected { + border: 3px dotted #1b1851; +} + .square { display: flex; justify-content: center; @@ -193,3 +205,15 @@ .figure.rook { background-position-x: -182.5px; } + +.btm-section { + margin-top: 2rem; +} + +.captured { + margin: 0 auto; +} + +.captured-piece { + float: left; +} diff --git a/assets/package-lock.json b/assets/package-lock.json new file mode 100644 index 0000000..dad8277 --- /dev/null +++ b/assets/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "assets", + "lockfileVersion": 2, + "requires": true, + "packages": {} +} diff --git a/config/dev.exs b/config/dev.exs index 86ea363..d97c05c 100644 --- a/config/dev.exs +++ b/config/dev.exs @@ -9,7 +9,7 @@ import Config config :chess, ChessWeb.Endpoint, # Binding to loopback ipv4 address prevents access from other machines. # Change to `ip: {0, 0, 0, 0}` to allow access from other machines. - http: [ip: {127, 0, 0, 1}, port: 4000], + http: [ip: {0, 0, 0, 0}, port: 4000], check_origin: false, code_reloader: true, debug_errors: true, diff --git a/lib/chess/gameplay/bishop.ex b/lib/chess/gameplay/bishop.ex new file mode 100644 index 0000000..bb80aa7 --- /dev/null +++ b/lib/chess/gameplay/bishop.ex @@ -0,0 +1,23 @@ +defmodule Chess.Gameplay.Bishop do + @moduledoc """ + Bishop + """ + + alias Chess.Gameplay.Board + + def moves(board, pos) do + Enum.reduce( + directions(), + [], + fn dir, acc -> acc ++ Board.generate_moves(board, pos, dir) end + ) + end + + ########### + # PRIVATE # + ########### + + defp directions do + [{1, 1}, {1, -1}, {-1, 1}, {-1, -1}] + end +end diff --git a/lib/chess/gameplay/board.ex b/lib/chess/gameplay/board.ex new file mode 100644 index 0000000..7625630 --- /dev/null +++ b/lib/chess/gameplay/board.ex @@ -0,0 +1,230 @@ +defmodule Chess.Gameplay.Board do + @moduledoc """ + Chess Play Board + """ + + alias Chess.Gameplay.Bishop + alias Chess.Gameplay.King + alias Chess.Gameplay.Knight + alias Chess.Gameplay.Pawn + alias Chess.Gameplay.Position + alias Chess.Gameplay.Queen + alias Chess.Gameplay.Rook + + def initial_state do + s = %{ + {0, 0} => make_piece(:rook, :white), + {1, 0} => make_piece(:knight, :white), + {2, 0} => make_piece(:bishop, :white), + {3, 0} => make_piece(:queen, :white), + {4, 0} => make_piece(:king, :white), + {5, 0} => make_piece(:bishop, :white), + {6, 0} => make_piece(:knight, :white), + {7, 0} => make_piece(:rook, :white), + {0, 7} => make_piece(:rook, :black), + {1, 7} => make_piece(:knight, :black), + {2, 7} => make_piece(:bishop, :black), + {3, 7} => make_piece(:queen, :black), + {4, 7} => make_piece(:king, :black), + {5, 7} => make_piece(:bishop, :black), + {6, 7} => make_piece(:knight, :black), + {7, 7} => make_piece(:rook, :black) + } + + s = Enum.reduce(0..7, s, fn i, acc -> Map.put(acc, {i, 1}, make_piece(:pawn, :white)) end) + Enum.reduce(0..7, s, fn i, acc -> Map.put(acc, {i, 6}, make_piece(:pawn, :black)) end) + end + + def moves(board, pos, previous_moves \\ []) do + %{type: type} = piece(board, pos) + + case type do + :bishop -> + Bishop.moves(board, pos) + :king -> + King.moves(board, pos, previous_moves) + :knight -> + Knight.moves(board, pos) + :pawn -> + Pawn.moves(board, pos, previous_moves) + :queen -> + Queen.moves(board, pos) + :rook -> + Rook.moves(board, pos) + end + end + + def find(board, %{type: type, colour: colour}) do + board + |> Enum.filter(fn {_, piece} -> match?(%{type: ^type, colour: ^colour}, piece) end) + end + + def find(board, %{colour: colour}) do + board + |> Enum.filter(fn {_, piece} -> match?(%{colour: ^colour}, piece) end) + end + + def is_king_checked?(board, colour) do + board + |> find(%{type: :king, colour: colour}) + |> List.first() + |> (fn {p, _} -> p end).() + |> is_attacked?(board) + end + + def is_attacked?(pos, board) do + board + |> Enum.any?(fn + {^pos, _} -> false + + {p, _} -> moves(board, p) |> Enum.any?(&(pos == &1)) + end) + end + + def move(board, start_pos, end_pos) do + {piece, board} = Map.pop(board, start_pos) + {captured, board} = Map.pop(board, end_pos) + + board = Map.put(board, end_pos, piece) + + %{board: board, captured: captured} = + with %{board: board, spec_capt: spec_capt} <- + special_move_if_needed(board, piece, captured, start_pos, end_pos) do + %{board: board, captured: spec_capt} + else + %{board: board} -> %{board: board, captured: captured} + end + + %{ + start_p: start_pos, + end_p: end_pos, + board: board, + piece: piece, + captured: captured + } + end + + defp special_move_if_needed(board, piece, captured, start_p, end_p) do + cond do + is_castle?(piece, start_p, end_p) -> + board |> castle_rook(end_p) + + is_en_passant?(piece, captured, start_p, end_p) -> + board |> en_passant(piece, end_p) + + should_promote_pawn?(piece, end_p) -> + promote_pawn(board, piece, end_p) + + true -> + %{board: board} + end + end + + defp should_promote_pawn?(%{type: :pawn, colour: :white}, {_, 7}), do: true + defp should_promote_pawn?(%{type: :pawn, colour: :black}, {_, 0}), do: true + defp should_promote_pawn?(_, _), do: false + + def piece(board, pos) do + board[pos] + end + + defp promote_pawn(board, %{type: :pawn, colour: colour}, pos) do + %{board: Map.put(board, pos, %{type: :queen, colour: colour})} + end + + defp is_castle?(%{type: :king}, {4, _}, {end_c, _}), do: end_c == 6 or end_c == 2 + defp is_castle?(_, _, _), do: false + + defp castle_rook(board, {c, r}) when c == 2, do: board |> move({0, r}, {3, r}) + defp castle_rook(board, {c, r}) when c == 6, do: board |> move({7, r}, {5, r}) + + defp is_en_passant?( + %{type: :pawn, colour: :white}, + nil, + {start_c, 4}, + {end_c, 5} + ) + when end_c == start_c + 1 or end_c == start_c - 1, + do: true + + defp is_en_passant?( + %{type: :pawn, colour: :black}, + nil, + {start_c, 3}, + {end_c, 2} + ) + when end_c == start_c - 1 or end_c == start_c + 1, + do: true + + defp is_en_passant?(_, _, _, _), do: false + + defp en_passant(board, %{type: :pawn, colour: :white}, {end_c, _}) do + board + |> Map.delete({end_c, 4}) + |> (&Map.put(%{spec_capt: %{type: :pawn, colour: :black}}, :board, &1)).() + end + + defp en_passant(board, %{type: :pawn, colour: :black}, {end_c, _}) do + board + |> Map.delete({end_c, 3}) + |> (&Map.put(%{spec_capt: %{type: :pawn, colour: :white}}, :board, &1)).() + end + + def generate_moves(board, {c, r}, move_rule) do + colour = piece(board, {c, r}).colour + generate_moves(board, colour, {c, r}, move_rule) + end + + ########### + # PRIVATE # + ########### + + defp generate_moves(board, colour, {c, r}, {dc, dr}) do + pos = {c + dc, r + dr} + + cond do + !Position.is_valid?(pos) || taken(board, colour, pos) -> + [] + + can_capture?(board, colour, pos) -> + [pos] + + true -> + [pos | generate_moves(board, colour, pos, {dc, dr})] + end + end + + defp generate_moves(_board, _colour, _pos, []), do: [] + + defp generate_moves(board, colour, pos, [{c, r} | moves]) do + cond do + !Position.is_valid?({c, r}) || taken(board, colour, {c, r}) -> + generate_moves(board, colour, pos, moves) + + can_capture?(board, colour, {c, r}) -> + [{c, r} | generate_moves(board, colour, pos, moves)] + + true -> + [{c, r} | generate_moves(board, colour, pos, moves)] + end + end + + defp can_capture?(board, :white, {c, r}) do + piece = board[{c, r}] + piece && piece.colour == :black + end + + defp can_capture?(board, :black, {c, r}) do + piece = board[{c, r}] + piece && piece.colour == :white + end + + defp taken(board, colour, {c, r}) do + piece = piece(board, {c, r}) + piece && piece.colour == colour + end + + defp make_piece(type, colour) do + %{type: type, colour: colour} + end +end diff --git a/lib/chess/gameplay/game.ex b/lib/chess/gameplay/game.ex new file mode 100644 index 0000000..49efcdf --- /dev/null +++ b/lib/chess/gameplay/game.ex @@ -0,0 +1,120 @@ +defmodule Chess.Gameplay.Game do + @moduledoc """ + Initiate game process and moves + """ + + alias Chess.Gameplay.Board + + + def initial do + %{turn: :white, + board: Board.initial_state(), + moves: [], + captured_pieces: [], + checked: false, + outcome: nil} + end + + def move(game_state, from_pos, to_pos) do + %{board: board, turn: turn, outcome: outcome} = game_state + + if outcome do + {:error, "Game has finished"} + else + case Board.piece(board, from_pos) do + nil -> {:error, "No piece at position"} + %{colour: ^turn} -> exec_move(game_state, from_pos, to_pos) + _ -> {:error, "Not your turn"} + end + end + end + + def checked?(%{board: board, turn: turn}) do + Board.is_king_checked?(board, turn) + end + + def checkmate?(state), do: checked?(state) and no_moves_available?(state) + + def stalemate?(state), do: no_moves_available?(state) and !checked?(state) + + ########### + # PRIVATE # + ########### + + defp no_moves_available?(state) do + state.board + |> Board.find(%{colour: state.turn}) + |> Enum.any?(fn {pos, _} -> + state.board |> Board.moves(pos) |> Enum.any?(&match?({:ok, _}, valid_move?(state, pos, &1))) + end) + |> negate + end + + defp exec_move(game_state, from_pos, to_pos) do + with {:ok, _} <- game_state |> valid_move?(from_pos, to_pos) do + { + :ok, + game_state + |> Map.get(:board) + |> Board.move(from_pos, to_pos) + |> update_game_state(game_state) + } + else + {:error, message} -> {:error, message} + end + end + + defp update_game_state( + %{board: board, captured: captured, piece: piece, start_p: start_p, end_p: end_p}, + %{moves: moves, turn: turn, captured_pieces: captured_pieces} + ) do + %{ + board: board, + moves: moves ++ [%{start_p: start_p, end_p: end_p, piece: piece}], + turn: flip_turn(turn), + captured_pieces: captured_pieces |> append_if_not_nil(captured) + } + |> update_outcome() + end + + defp update_outcome(state) do + state + |> Map.put(:checked, checked?(state)) + |> Map.put(:outcome, outcome(state)) + end + + defp outcome(state) do + cond do + checkmate?(state) -> :checkmate + stalemate?(state) -> :stalemate + true -> nil + end + end + + defp append_if_not_nil(list, nil), do: list + defp append_if_not_nil(list, el), do: list ++ [el] + + defp flip_turn(:white), do: :black + defp flip_turn(:black), do: :white + + defp valid_move?(%{board: board, turn: turn, moves: prev_moves}, from_p, to_p) do + cond do + board + |> Board.moves(from_p, prev_moves) + |> Enum.any?(&(&1 == to_p)) + |> negate -> + {:error, "This move is not available for the selected piece!"} + + board + |> Board.move(from_p, to_p) + |> Map.get(:board) + |> Board.is_king_checked?(turn) -> + {:error, "The king will be in check!"} + + true -> + {:ok, nil} + end + end + + defp negate(val), do: !val +end diff --git a/lib/chess/gameplay/king.ex b/lib/chess/gameplay/king.ex new file mode 100644 index 0000000..aee105c --- /dev/null +++ b/lib/chess/gameplay/king.ex @@ -0,0 +1,84 @@ +defmodule Chess.Gameplay.King do + @moduledoc """ + King + """ + + alias Chess.Gameplay.Board + + def moves(board, {c, r}, previous_moves \\ []) do + moves = + Enum.reduce(-1..1, [], fn i, acc -> + Enum.reduce(-1..1, [], fn j, in_acc -> + [{c + i, r + j} | in_acc] + end) ++ acc + end) + |> Enum.filter(fn {cx, rx} -> cx != c or rx != r end) + + Board.generate_moves(board, {c, r}, moves) ++ castling(board, {c, r}, previous_moves) + end + + defp castling(board, {4, r}, previous_moves) when r == 0 or r == 7 do + colour = Board.piece(board, {4, r}).colour + + if not king_has_moved?(colour, previous_moves) do + [ + queenside_castling(board, colour, {4, r}, previous_moves), + kingside_castling(board, colour, {4, r}, previous_moves) + ] + |> Enum.filter(&(!!&1)) + else + [] + end + end + + defp castling(_board, _pos, _prev), do: [] + + defp queenside_castling(board, colour, {4, r}, previous_moves) do + with %{type: :rook, colour: ^colour} <- Board.piece(board, {0, r}), + false <- rook_has_moved?(colour, {0, r}, previous_moves), + false <- blocked_or_checked_on_queenside?(board, colour, r) do + {2, r} + else + _ -> nil + end + end + + defp blocked_or_checked_on_queenside?(board, colour, r) do + [{1, r}, {2, r}, {3, r}] |> Enum.any?(&(!!Board.piece(board, &1))) || + [{4, r}, {3, r}, {2, r}] + |> Enum.any?(fn pos -> + board |> Board.move({4, r}, pos) |> Map.get(:board) |> Board.is_king_checked?(colour) + end) + end + + defp kingside_castling(board, colour, {4, r}, previous_moves) do + with %{type: :rook, colour: ^colour} <- Board.piece(board, {7, r}), + false <- rook_has_moved?(colour, {7, r}, previous_moves), + false <- blocked_or_checked_on_kingside?(board, colour, r) do + {6, r} + else + _ -> nil + end + end + + defp blocked_or_checked_on_kingside?(board, colour, r) do + [{5, r}, {6, r}] |> Enum.any?(&(!!Board.piece(board, &1))) || + [{4, r}, {5, r}, {6, r}] + |> Enum.any?(fn pos -> + board + |> Board.move({4, r}, pos) + |> Map.get(:board) + |> Board.is_king_checked?(colour) + end) + end + + defp king_has_moved?(colour, previous_moves), + do: Enum.any?(previous_moves, &match?(%{piece: %{type: :king, colour: ^colour}}, &1)) + + defp rook_has_moved?(colour, start_p, previous_moves), + do: + Enum.any?( + previous_moves, + &match?(%{piece: %{type: :rook, colour: ^colour}, start_p: ^start_p}, &1) + ) +end diff --git a/lib/chess/gameplay/knight.ex b/lib/chess/gameplay/knight.ex new file mode 100644 index 0000000..1958b23 --- /dev/null +++ b/lib/chess/gameplay/knight.ex @@ -0,0 +1,23 @@ +defmodule Chess.Gameplay.Knight do + @moduledoc """ + Knight + """ + + alias Chess.Gameplay.Board + + def moves(board, {c, r}) do + Board.generate_moves( + board, + {c, r}, + Enum.map(possible_changes(), fn {dc, dr} -> {c + dc, r + dr} end) + ) + end + + ########### + # PRIVATE # + ########### + + defp possible_changes do + [{1, 2}, {2, 1}, {-1, 2}, {-2, 1}, {1, -2}, {2, -1}, {-1, -2}, {-2, -1}] + end +end diff --git a/lib/chess/gameplay/pawn.ex b/lib/chess/gameplay/pawn.ex new file mode 100644 index 0000000..4fa5272 --- /dev/null +++ b/lib/chess/gameplay/pawn.ex @@ -0,0 +1,78 @@ +defmodule Chess.Gameplay.Pawn do + @moduledoc """ + Pawn + """ + + alias Chess.Gameplay.Board + + def moves(board, {c, r}, previous_moves \\ []) do + colour = (board |> Board.piece({c, r})).colour + + regular_moves(board, colour, {c, r}) ++ + attacks(board, colour, {c, r}, previous_moves |> List.last()) + end + + ########### + # PRIVATE # + ########### + + defp regular_moves(board, :white, {c, r}) do + cond do + blocked?(board, {c, r + 1}) -> [] + r == 1 -> [{c, r + 1} | regular_moves(board, :white, {c, r + 1})] + true -> [{c, r + 1}] + end + end + + defp regular_moves(board, :black, {c, r}) do + cond do + blocked?(board, {c, r - 1}) -> [] + r == 6 -> [{c, r - 1} | regular_moves(board, :black, {c, r - 1})] + true -> [{c, r - 1}] + end + end + + defp blocked?(board, pos), do: !!Board.piece(board, pos) + + defp has_piece_of_colour?(board, pos, colour) do + Board.piece(board, pos) |> matches_colour?(colour) + end + + defp matches_colour?(piece, colour), do: piece && piece.colour == colour + + defp attacks( + board, + :white, + {c, 4}, + %{ + piece: %{type: :pawn, colour: :black}, + start_p: {sc, sr}, + end_p: {_ec, er} + }) when sr == 6 and (sc == c - 1 or sc == c + 1) and er == 4 do + [{sc, 5} | attacks(board, :white, {c, 4}, [])] + end + + defp attacks( + board, + :black, + {c, 3}, + %{ + piece: %{type: :pawn, colour: :white}, + start_p: {sc, sr}, + end_p: {_ec, er} + }) when sr == 1 and (sc == c - 1 or sc == c + 1) and er == 3 do + [{sc, 2} | attacks(board, :black, {c, 3}, [])] + end + + defp attacks(board, :white, {c, r}, _prev) do + Enum.filter([{c - 1, r + 1}, {c + 1, r + 1}], fn p -> + has_piece_of_colour?(board, p, :black) + end) + end + + defp attacks(board, :black, {c, r}, _prev) do + Enum.filter([{c - 1, r - 1}, {c + 1, r - 1}], fn p -> + has_piece_of_colour?(board, p, :white) + end) + end +end diff --git a/lib/chess/gameplay/position.ex b/lib/chess/gameplay/position.ex new file mode 100644 index 0000000..45ab9a3 --- /dev/null +++ b/lib/chess/gameplay/position.ex @@ -0,0 +1,27 @@ +defmodule Chess.Gameplay.Position do + @moduledoc """ + Gameplay position validation + """ + + def is_valid?({col, _}) when not is_integer(col), do: false + def is_valid?({_, row}) when not is_integer(row), do: false + + def is_valid?({_, row}) when row > 7 or row < 0, do: false + def is_valid?({col, _}) when col > 7 or col < 0, do: false + + def is_valid?({_, _}), do: true + + def parse(str) do + with [cs, rs] <- String.split(str, ","), + {c, _} <- Integer.parse(cs), + {r, _} <- Integer.parse(rs), + true <- is_valid?({c, r}) do + {:ok, {c, r}} + else + _ -> + {:error, "Invalid position"} + end + end + + def to_string({c, r}), do: "#{c},#{r}" +end diff --git a/lib/chess/gameplay/queen.ex b/lib/chess/gameplay/queen.ex new file mode 100644 index 0000000..2ad8e7c --- /dev/null +++ b/lib/chess/gameplay/queen.ex @@ -0,0 +1,12 @@ +defmodule Chess.Gameplay.Queen do + @moduledoc """ + Queen + """ + + alias Chess.Gameplay.Bishop + alias Chess.Gameplay.Rook + + def moves(board, pos) do + Bishop.moves(board, pos) ++ Rook.moves(board, pos) + end +end diff --git a/lib/chess/gameplay/rook.ex b/lib/chess/gameplay/rook.ex new file mode 100644 index 0000000..80efd8d --- /dev/null +++ b/lib/chess/gameplay/rook.ex @@ -0,0 +1,19 @@ +defmodule Chess.Gameplay.Rook do + @moduledoc """ + Rook + """ + + alias Chess.Gameplay.Board + + def moves(board, pos) do + Enum.reduce(directions(), [], &(&2 ++ Board.generate_moves(board, pos, &1))) + end + + ########### + # PRIVATE # + ########### + + defp directions do + [{0, 1}, {0, -1}, {1, 0}, {-1, 0}] + end +end diff --git a/lib/chess/utils/converter.ex b/lib/chess/utils/converter.ex new file mode 100644 index 0000000..db52ebf --- /dev/null +++ b/lib/chess/utils/converter.ex @@ -0,0 +1,43 @@ +defmodule Chess.Utils.Converter do + @minute 60 + @hour @minute * 60 + + def sec_to_str(seconds) when seconds >= @hour do + h = div(seconds, @hour) + + m = + seconds + |> rem(@hour) + |> div(@minute) + |> pad_int() + + s = + seconds + |> rem(@hour) + |> rem(@minute) + |> pad_int() + + "#{h}:#{m}:#{s}" + end + + def sec_to_str(seconds) do + m = div(seconds, @minute) + + s = + seconds + |> rem(@minute) + |> pad_int() + + "#{m}:#{s}" + end + + ########### + # PRIVATE # + ########### + + defp pad_int(int, padding \\ 2) do + int + |> Integer.to_string() + |> String.pad_leading(padding, "0") + end +end diff --git a/lib/chess_web/channels/user_socket.ex b/lib/chess_web/channels/user_socket.ex new file mode 100644 index 0000000..fc42c72 --- /dev/null +++ b/lib/chess_web/channels/user_socket.ex @@ -0,0 +1,35 @@ +defmodule ChessWeb.UserSocket do + use Phoenix.Socket + + ## Channels + # channel "room:*", MultichessWeb.RoomChannel + + # Socket params are passed from the client and can + # be used to verify and authenticate a user. After + # verification, you can put default assigns into + # the socket that will be set for all channels, ie + # + # {:ok, assign(socket, :user_id, verified_user_id)} + # + # To deny connection, return `:error`. + # + # See `Phoenix.Token` documentation for examples in + # performing token verification on connect. + @impl true + def connect(_params, socket, _connect_info) do + {:ok, socket} + end + + # Socket id's are topics that allow you to identify all sockets for a given user: + # + # def id(socket), do: "user_socket:#{socket.assigns.user_id}" + # + # Would allow you to broadcast a "disconnect" event and terminate + # all active sockets and channels for a given user: + # + # MultichessWeb.Endpoint.broadcast("user_socket:#{user.id}", "disconnect", %{}) + # + # Returning `nil` makes this socket anonymous. + @impl true + def id(_socket), do: nil +end diff --git a/lib/chess_web/endpoint.ex b/lib/chess_web/endpoint.ex index a86ad97..e0f80ff 100644 --- a/lib/chess_web/endpoint.ex +++ b/lib/chess_web/endpoint.ex @@ -10,6 +10,7 @@ defmodule ChessWeb.Endpoint do signing_salt: "y/33nBbH" ] + socket "/socket", ChessWeb.UserSocket, websocket: true, longpoll: false 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/live/game_live.ex b/lib/chess_web/live/game_live.ex new file mode 100644 index 0000000..a58a3e2 --- /dev/null +++ b/lib/chess_web/live/game_live.ex @@ -0,0 +1,113 @@ +defmodule ChessWeb.GameLive do + use ChessWeb, :live_view + + alias Chess.Gameplay.Game + alias Chess.Gameplay.Position + alias Chess.Utils.Converter + alias ChessWeb.GameplayServer + + @impl true + def mount(_params, _session, socket) do + socket = + assign(socket, state: Game.initial(), selected_pos: nil) + |> assign_current_time() + |> assign(black_time: 5 * 60) + |> assign(white_time: 5 * 60) + + {:ok, + if connected?(socket) do + {:ok, pid} = GenServer.start_link(GameplayServer, {self(), socket.assigns.state}) + + assign(socket, pid: pid) + else + socket + end + } + end + + @impl true + def handle_event("select_sq", %{"pos" => pos}, socket) do + with {:ok, pos} <- Position.parse(pos) do + case socket.assigns.selected_pos do + ^pos -> + {:noreply, assign(socket, selected_pos: nil)} + + _ -> + case socket.assigns.selected_pos do + nil -> + {:noreply, socket |> assign(selected_pos: pos)} + + from_p -> + with {:ok, state} <- GenServer.call(socket.assigns.pid, {:move, from_p, pos}) do + {:noreply, assign(socket, state: state, selected_pos: nil)} + else + {:error, msg} -> + {:noreply, put_flash(socket, :error, msg)} + end + end + end + else + {:error, msg} -> {:noreply, put_flash(socket, :error, msg)} + end + end + + @impl true + def handle_cast({:new_state, state}, socket) do + {:noreply, assign(socket, state: state)} + end + + @impl true + def handle_cast(_, socket) do + {:noreply, socket} + end + + def assign_current_time(socket) do + now = + Time.utc_now() + |> Time.to_string() + |> String.split(".") + |> hd + + assign(socket, now: now) + end + + def seconds_to_str(sec) do + Converter.sec_to_str(sec) + end + + def get_classes(selected_pos, col, row) do + colour = + case rem(row + col, 2) do + 0 -> :black + 1 -> :white + end + selected = + case selected_pos == {col, row} do + true -> "selected" + _ -> "" + end + "square #{colour} #{selected}" + end + + def pos_to_s({c, r}), do: "#{c},#{r}" + def pos_to_s(nil), do: nil + + def piece_render(nil), do: nil + + def piece_render(%{type: piece, colour: colour}) do + figure = + case piece do + :bishop -> "bishop" + :king -> "king" + :knight -> "knight" + :pawn -> "pawn" + :queen -> "queen" + :rook -> "rook" + end + "figure #{colour} #{figure}" + end + + def outcome(%{outcome: :checkmate, turn: turn}), do: "#{turn} lost, checkmate!" + def outcome(%{outcome: :stalemate}), do: "Tie by stalemate!" + def outcome(_), do: nil +end diff --git a/lib/chess_web/live/game_live.html.heex b/lib/chess_web/live/game_live.html.heex new file mode 100644 index 0000000..788f200 --- /dev/null +++ b/lib/chess_web/live/game_live.html.heex @@ -0,0 +1,34 @@ + + + Game + + + Turn: <%= @state.turn %> + <%= outcome(@state) %> + + + + + <%= for row <- 7..0 do %> + <%= for col <- 0..7 do %> + + + + <% end %> + <% end %> + + + + Captures: + <%= for captured <- @state.captured_pieces do %> + + + + <% end %> + + + diff --git a/lib/chess_web/live/gameplay_server.ex b/lib/chess_web/live/gameplay_server.ex new file mode 100644 index 0000000..4ebe09f --- /dev/null +++ b/lib/chess_web/live/gameplay_server.ex @@ -0,0 +1,50 @@ +defmodule ChessWeb.GameplayServer do + @moduledoc """ + Storing game state + """ + + use GenServer + alias Chess.Gameplay.Game + + @impl true + def handle_call({:move, from_p, to_p}, _from, server_state) do + with {:ok, state} <- Game.move(server_state |> Map.get(:state), from_p, to_p) do + {:reply, {:ok, state}, Map.put(server_state, :state, state)} + else + {:error, msg} -> {:reply, {:error, msg}, server_state} + end + end + + @impl true + def handle_info(:tick, server_state) do + %{ppid: ppid, state: %{turn: turn}, time: time} = server_state + + with 0 <- time[turn] - 1 do + %{state: game_state} = server_state + + GenServer.cast(ppid, {:new_time, time |> Map.put(turn, 0)}) + GenServer.cast(ppid, {:new_state, game_state}) + + { + :noreply, + server_state + |> Map.put(:time, Map.put(time, turn, 0)) + |> Map.put(:state, game_state) + } + else + t when t < 0 -> + {:noreply, server_state} + + p_time_left -> + new_time = time |> Map.put(turn, p_time_left) + GenServer.cast(ppid, {:new_time, new_time}) + {:noreply, server_state |> Map.put(:time, new_time)} + end + end + + @impl true + def init({pid, init_state}) do + :timer.send_interval(1000, self(), :tick) + {:ok, %{ppid: pid, state: init_state, time: %{white: 5 * 2, black: 5 * 2}}} + end +end diff --git a/lib/chess_web/router.ex b/lib/chess_web/router.ex index 6545232..6692e86 100644 --- a/lib/chess_web/router.ex +++ b/lib/chess_web/router.ex @@ -17,11 +17,6 @@ defmodule ChessWeb.Router do scope "/", ChessWeb do pipe_through :browser - get "/", PageController, :index + live "/", GameLive, :index end - - # Other scopes may use custom stacks. - # scope "/api", ChessWeb do - # pipe_through :api - # end end diff --git a/lib/chess_web/templates/page/index.html.heex b/lib/chess_web/templates/page/index.html.heex deleted file mode 100644 index 97fcea1..0000000 --- a/lib/chess_web/templates/page/index.html.heex +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/mix.lock b/mix.lock new file mode 100644 index 0000000..e827ca2 --- /dev/null +++ b/mix.lock @@ -0,0 +1,25 @@ +%{ + "castore": {:hex, :castore, "0.1.18", "deb5b9ab02400561b6f5708f3e7660fc35ca2d51bfc6a940d2f513f89c2975fc", [:mix], [], "hexpm", "61bbaf6452b782ef80b33cdb45701afbcf0a918a45ebe7e73f1130d661e66a06"}, + "cowboy": {:hex, :cowboy, "2.9.0", "865dd8b6607e14cf03282e10e934023a1bd8be6f6bacf921a7e2a96d800cd452", [:make, :rebar3], [{:cowlib, "2.11.0", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "2c729f934b4e1aa149aff882f57c6372c15399a20d54f65c8d67bef583021bde"}, + "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, + "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, + "esbuild": {:hex, :esbuild, "0.5.0", "d5bb08ff049d7880ee3609ed5c4b864bd2f46445ea40b16b4acead724fb4c4a3", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}], "hexpm", "f183a0b332d963c4cfaf585477695ea59eef9a6f2204fdd0efa00e099694ffe5"}, + "file_system": {:hex, :file_system, "0.2.10", "fb082005a9cd1711c05b5248710f8826b02d7d1784e7c3451f9c1231d4fc162d", [:mix], [], "hexpm", "41195edbfb562a593726eda3b3e8b103a309b733ad25f3d642ba49696bf715dc"}, + "floki": {:hex, :floki, "0.33.1", "f20f1eb471e726342b45ccb68edb9486729e7df94da403936ea94a794f072781", [:mix], [{:html_entities, "~> 0.5.0", [hex: :html_entities, repo: "hexpm", optional: false]}], "hexpm", "461035fd125f13fdf30f243c85a0b1e50afbec876cbf1ceefe6fddd2e6d712c6"}, + "html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"}, + "jason": {:hex, :jason, "1.4.0", "e855647bc964a44e2f67df589ccf49105ae039d4179db7f6271dfd3843dc27e6", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "79a3791085b2a0f743ca04cec0f7be26443738779d09302e01318f97bdb82121"}, + "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"}, + "phoenix": {:hex, :phoenix, "1.6.14", "57678366dc1d5bad49832a0fc7f12c2830c10d3eacfad681bfe9602cd4445f04", [:mix], [{:castore, ">= 0.0.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "d48c0da00b3d4cd1aad6055387917491af9f6e1f1e96cedf6c6b7998df9dba26"}, + "phoenix_html": {:hex, :phoenix_html, "3.2.0", "1c1219d4b6cb22ac72f12f73dc5fad6c7563104d083f711c3fcd8551a1f4ae11", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: true]}], "hexpm", "36ec97ba56d25c0136ef1992c37957e4246b649d620958a1f9fa86165f8bc54f"}, + "phoenix_live_reload": {:hex, :phoenix_live_reload, "1.3.3", "3a53772a6118d5679bf50fc1670505a290e32a1d195df9e069d8c53ab040c054", [:mix], [{:file_system, "~> 0.2.1 or ~> 0.3", [hex: :file_system, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.4", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "766796676e5f558dbae5d1bdb066849673e956005e3730dfd5affd7a6da4abac"}, + "phoenix_live_view": {:hex, :phoenix_live_view, "0.17.12", "74f4c0ad02d7deac2d04f50b52827a5efdc5c6e7fac5cede145f5f0e4183aedc", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix, "~> 1.6.0 or ~> 1.7.0", [hex: :phoenix, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 3.1", [hex: :phoenix_html, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.2 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "af6dd5e0aac16ff43571f527a8e0616d62cb80b10eb87aac82170243e50d99c8"}, + "phoenix_pubsub": {:hex, :phoenix_pubsub, "2.1.1", "ba04e489ef03763bf28a17eb2eaddc2c20c6d217e2150a61e3298b0f4c2012b5", [:mix], [], "hexpm", "81367c6d1eea5878ad726be80808eb5a787a23dee699f96e72b1109c57cdd8d9"}, + "phoenix_view": {:hex, :phoenix_view, "1.1.2", "1b82764a065fb41051637872c7bd07ed2fdb6f5c3bd89684d4dca6e10115c95a", [:mix], [{:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}], "hexpm", "7ae90ad27b09091266f6adbb61e1d2516a7c3d7062c6789d46a7554ec40f3a56"}, + "plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"}, + "plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"}, + "plug_crypto": {:hex, :plug_crypto, "1.2.3", "8f77d13aeb32bfd9e654cb68f0af517b371fb34c56c9f2b58fe3df1235c1251a", [:mix], [], "hexpm", "b5672099c6ad5c202c45f5a403f21a3411247f164e4a8fab056e5cd8a290f4a2"}, + "ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"}, + "telemetry": {:hex, :telemetry, "1.1.0", "a589817034a27eab11144ad24d5c0f9fab1f58173274b1e9bae7074af9cbee51", [:rebar3], [], "hexpm", "b727b2a1f75614774cff2d7565b64d0dfa5bd52ba517f16543e6fc7efcc0df48"}, + "telemetry_metrics": {:hex, :telemetry_metrics, "0.6.1", "315d9163a1d4660aedc3fee73f33f1d355dcc76c5c3ab3d59e76e3edf80eef1f", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7be9e0871c41732c233be71e4be11b96e56177bf15dde64a8ac9ce72ac9834c6"}, + "telemetry_poller": {:hex, :telemetry_poller, "1.0.0", "db91bb424e07f2bb6e73926fcafbfcbcb295f0193e0a00e825e589a0a47e8453", [:rebar3], [{:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3a24eafd66c3f42da30fc3ca7dda1e9d546c12250a2d60d7b81d264fbec4f6e"}, +}