From 29629aac678a1d11a050ccd44ff85d52ecdfa56a Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Tue, 18 Oct 2022 23:47:57 +0200 Subject: [PATCH 01/19] Configure versions, add changes to .gitignore --- .gitignore | 5 +++++ .tool-versions | 3 +++ assets/package-lock.json | 6 ++++++ mix.lock | 25 +++++++++++++++++++++++++ 4 files changed, 39 insertions(+) create mode 100644 .tool-versions create mode 100644 assets/package-lock.json create mode 100644 mix.lock diff --git a/.gitignore b/.gitignore index 3dbb8bf..2c792c9 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,8 @@ chess-*.tar 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/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/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"}, +} From 047d8cd1bfd0743ecc8cf370299a5972c4fbbc5c Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 01:02:08 +0200 Subject: [PATCH 02/19] Add live view, POC >> the gameplay is_valid? func --- lib/chess/gameplay/position.ex | 9 ++ lib/chess_web/channels/user_socket.ex | 35 ++++++ lib/chess_web/endpoint.ex | 1 + lib/chess_web/live/game_live.ex | 39 +++++++ lib/chess_web/live/game_live.html.heex | 153 +++++++++++++++++++++++++ lib/chess_web/router.ex | 2 +- 6 files changed, 238 insertions(+), 1 deletion(-) create mode 100644 lib/chess/gameplay/position.ex create mode 100644 lib/chess_web/channels/user_socket.ex create mode 100644 lib/chess_web/live/game_live.ex create mode 100644 lib/chess_web/live/game_live.html.heex diff --git a/lib/chess/gameplay/position.ex b/lib/chess/gameplay/position.ex new file mode 100644 index 0000000..adc8e64 --- /dev/null +++ b/lib/chess/gameplay/position.ex @@ -0,0 +1,9 @@ +defmodule Chess.Gameplay.Position do + @moduledoc """ + Gameplay position validation + """ + + def is_valid?({row, column}) when column > 7 or column < 0, do: :false + def is_valid?({row, column}) when row > 7 or row < 0, do: :false + def is_valid?({row, column}), do: :true +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..663efcc --- /dev/null +++ b/lib/chess_web/live/game_live.ex @@ -0,0 +1,39 @@ +defmodule ChessWeb.GameLive do + use ChessWeb, :live_view + + @impl true + def mount(_params, _session, socket) do + {:ok, assign(socket, query: "", results: %{})} + end + + @impl true + def handle_event("suggest", %{"q" => query}, socket) do + {:noreply, assign(socket, results: search(query), query: query)} + end + + @impl true + def handle_event("search", %{"q" => query}, socket) do + case search(query) do + %{^query => vsn} -> + {:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")} + + _ -> + {:noreply, + socket + |> put_flash(:error, "No dependencies found matching \"#{query}\"") + |> assign(results: %{}, query: query)} + end + end + + defp search(query) do + if not ChessWeb.Endpoint.config(:code_reloader) do + raise "action disabled when not in development" + end + + for {app, desc, vsn} <- Application.started_applications(), + app = to_string(app), + String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"), + into: %{}, + do: {app, vsn} + end +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..6e2f7e5 --- /dev/null +++ b/lib/chess_web/live/game_live.html.heex @@ -0,0 +1,153 @@ +
+

Peace of mind from prototype to production

+ +
+ + + <%= for {app, _vsn} <- @results do %> + + <% end %> + + +
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/lib/chess_web/router.ex b/lib/chess_web/router.ex index 6545232..38593c1 100644 --- a/lib/chess_web/router.ex +++ b/lib/chess_web/router.ex @@ -17,7 +17,7 @@ defmodule ChessWeb.Router do scope "/", ChessWeb do pipe_through :browser - get "/", PageController, :index + live "/", GameLive, :index end # Other scopes may use custom stacks. From 8bcb3085cdec1ffe1882a20898f0b771678964cf Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 01:14:54 +0200 Subject: [PATCH 03/19] POC >> try some things... --- .gitignore | 2 ++ lib/chess/gameplay/position.ex | 10 +++++++--- lib/chess_web/live/game_live.ex | 7 ++++++- lib/chess_web/live/game_live.html.heex | 6 ++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 2c792c9..ead1914 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,8 @@ 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/ diff --git a/lib/chess/gameplay/position.ex b/lib/chess/gameplay/position.ex index adc8e64..315a323 100644 --- a/lib/chess/gameplay/position.ex +++ b/lib/chess/gameplay/position.ex @@ -3,7 +3,11 @@ defmodule Chess.Gameplay.Position do Gameplay position validation """ - def is_valid?({row, column}) when column > 7 or column < 0, do: :false - def is_valid?({row, column}) when row > 7 or row < 0, do: :false - def is_valid?({row, column}), do: :true + def is_valid?({row, _}) when not is_integer(row), do: false + def is_valid?({_, column}) when not is_integer(column), do: false + + def is_valid?({_, column}) when column > 7 or column < 0, do: false + def is_valid?({row, _}) when row > 7 or row < 0, do: false + + def is_valid?({_, _}), do: true end diff --git a/lib/chess_web/live/game_live.ex b/lib/chess_web/live/game_live.ex index 663efcc..a5b640e 100644 --- a/lib/chess_web/live/game_live.ex +++ b/lib/chess_web/live/game_live.ex @@ -3,7 +3,7 @@ defmodule ChessWeb.GameLive do @impl true def mount(_params, _session, socket) do - {:ok, assign(socket, query: "", results: %{})} + {:ok, assign(socket, query: "", results: %{}, counter: 0)} end @impl true @@ -25,6 +25,11 @@ defmodule ChessWeb.GameLive do end end + @impl true + def handle_event("incr", _, socket) do + {:noreply, socket |> assign(counter: socket.assigns[:counter] + 1)} + end + defp search(query) do if not ChessWeb.Endpoint.config(:code_reloader) do raise "action disabled when not in development" diff --git a/lib/chess_web/live/game_live.html.heex b/lib/chess_web/live/game_live.html.heex index 6e2f7e5..74540b9 100644 --- a/lib/chess_web/live/game_live.html.heex +++ b/lib/chess_web/live/game_live.html.heex @@ -12,6 +12,12 @@ +
+
+ +
+
+
From a9e069df1164a7e7ef94fd66c50983876f0674f7 Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 02:18:16 +0200 Subject: [PATCH 04/19] Add pieces and moves --- lib/chess/gameplay/bishop.ex | 23 ++++++ lib/chess/gameplay/board.ex | 128 +++++++++++++++++++++++++++++++++ lib/chess/gameplay/knight.ex | 23 ++++++ lib/chess/gameplay/pawn.ex | 78 ++++++++++++++++++++ lib/chess/gameplay/position.ex | 16 +++++ lib/chess/gameplay/queen.ex | 12 ++++ lib/chess/gameplay/rook.ex | 23 ++++++ 7 files changed, 303 insertions(+) create mode 100644 lib/chess/gameplay/bishop.ex create mode 100644 lib/chess/gameplay/board.ex create mode 100644 lib/chess/gameplay/knight.ex create mode 100644 lib/chess/gameplay/pawn.ex create mode 100644 lib/chess/gameplay/queen.ex create mode 100644 lib/chess/gameplay/rook.ex 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..7ef0ef4 --- /dev/null +++ b/lib/chess/gameplay/board.ex @@ -0,0 +1,128 @@ +defmodule Chess.Gameplay.Board do + @moduledoc """ + Chess Play Board + """ + + alias Chess.Gameplay.Bishop + 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, :white), + {1, 7} => make_piece(:knight, :white), + {2, 7} => make_piece(:bishop, :white), + {3, 7} => make_piece(:queen, :white), + {4, 7} => make_piece(:king, :white), + {5, 7} => make_piece(:bishop, :white), + {6, 7} => make_piece(:knight, :white), + {7, 7} => make_piece(:rook, :white) + } + + 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) + :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 move(board, start_pos, end_pos) do + {piece, board} = Map.pop(board, start_pos) + {captured, board} = Map.pop(board, end_pos) + + %{ + start_p: start_pos, + end_p: end_pos, + board: Map.put(board, end_pos, piece), + piece: piece, + captured: captured + } + end + + def piece(board, pos) do + board[pos] + 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/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..695a125 --- /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}, List.last(previous_moves)) + 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 index 315a323..314a64b 100644 --- a/lib/chess/gameplay/position.ex +++ b/lib/chess/gameplay/position.ex @@ -10,4 +10,20 @@ defmodule Chess.Gameplay.Position do def is_valid?({row, _}) when row > 7 or row < 0, do: false def is_valid?({_, _}), do: true + + def parse(str) do + with [rs, cs] <- String.split(str, ","), + {r, _} <- Integer.parse(rs), + {c, _} <- Integer.parse(cs), + true <- is_valid?({r, c}) do + {:ok, {r, c}} + else + _ -> + {:error, "Invalid position"} + end + end + + def to_string({r, c}) do + Integer.to_string(r) <> "," <> Integer.to_string(c) + end 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..67eab7a --- /dev/null +++ b/lib/chess/gameplay/rook.ex @@ -0,0 +1,23 @@ +defmodule Chess.Gameplay.Rook do + @moduledoc """ + Rook + """ + + 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 + [{0, 1}, {0, -1}, {1, 0}, {-1, 0}] + end +end From dcec8a3ca177ee909c16ab3dc52d09501a20b834 Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 02:57:35 +0200 Subject: [PATCH 05/19] Add king and moves --- lib/chess/gameplay/board.ex | 55 ++++++++++++++++- lib/chess/gameplay/game.ex | 3 + lib/chess/gameplay/king.ex | 84 ++++++++++++++++++++++++++ lib/chess/gameplay/rook.ex | 6 +- lib/chess_web/live/game_live.ex | 23 ++++++- lib/chess_web/live/game_live.html.heex | 1 + 6 files changed, 165 insertions(+), 7 deletions(-) create mode 100644 lib/chess/gameplay/game.ex create mode 100644 lib/chess/gameplay/king.ex diff --git a/lib/chess/gameplay/board.ex b/lib/chess/gameplay/board.ex index 7ef0ef4..958144b 100644 --- a/lib/chess/gameplay/board.ex +++ b/lib/chess/gameplay/board.ex @@ -4,6 +4,7 @@ defmodule Chess.Gameplay.Board do """ alias Chess.Gameplay.Bishop + alias Chess.Gameplay.King alias Chess.Gameplay.Knight alias Chess.Gameplay.Pawn alias Chess.Gameplay.Position @@ -40,6 +41,8 @@ defmodule Chess.Gameplay.Board do case type do :bishop -> Bishop.moves(board, pos) + :king -> + King.moves(board, pos, previous_moves) :knight -> Knight.moves(board, pos) :pawn -> @@ -51,14 +54,50 @@ defmodule Chess.Gameplay.Board do end end + def find(board, %{type: type, colour: colour}) do + board + |> Enum.filter(fn {_, piece} -> match?(%{type: ^type, 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 = + cond do + is_castle?(piece, start_pos, end_pos) -> + board |> castle_rook(end_pos) |> Map.get(:board) + + should_promote_pawn?(piece, end_pos) -> + promote_pawn(board, piece, end_pos) + + true -> + board + end + %{ start_p: start_pos, end_p: end_pos, - board: Map.put(board, end_pos, piece), + board: board, piece: piece, captured: captured } @@ -68,6 +107,20 @@ defmodule Chess.Gameplay.Board do board[pos] end + def should_promote_pawn?(%{type: :pawn, colour: :white}, {_, 7}), do: true + def should_promote_pawn?(%{type: :pawn, colour: :black}, {_, 0}), do: true + def should_promote_pawn?(_, _), do: false + + def promote_pawn(board, %{type: :pawn, colour: colour}, pos) do + Map.put(board, pos, %{type: :queen, colour: colour}) + end + + def is_castle?(%{type: :king}, {4, _}, {end_c, _}), do: end_c == 6 or end_c == 2 + def is_castle?(_, _, _), do: false + + def castle_rook(board, {c, r}) when c == 2, do: board |> move({0, r}, {3, r}) + def castle_rook(board, {c, r}) when c == 6, do: board |> move({7, r}, {5, r}) + def generate_moves(board, {c, r}, move_rule) do colour = piece(board, {c, r}).colour generate_moves(board, colour, {c, r}, move_rule) diff --git a/lib/chess/gameplay/game.ex b/lib/chess/gameplay/game.ex new file mode 100644 index 0000000..ad7e749 --- /dev/null +++ b/lib/chess/gameplay/game.ex @@ -0,0 +1,3 @@ +defmodule Chess.Gameplay.Game do + alias Chess.Gameplay.Board +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/rook.ex b/lib/chess/gameplay/rook.ex index 67eab7a..80efd8d 100644 --- a/lib/chess/gameplay/rook.ex +++ b/lib/chess/gameplay/rook.ex @@ -6,11 +6,7 @@ defmodule Chess.Gameplay.Rook do alias Chess.Gameplay.Board def moves(board, pos) do - Enum.reduce( - directions(), - [], - fn dir, acc -> acc ++ Board.generate_moves(board, pos, dir) end - ) + Enum.reduce(directions(), [], &(&2 ++ Board.generate_moves(board, pos, &1))) end ########### diff --git a/lib/chess_web/live/game_live.ex b/lib/chess_web/live/game_live.ex index a5b640e..9c3678e 100644 --- a/lib/chess_web/live/game_live.ex +++ b/lib/chess_web/live/game_live.ex @@ -3,7 +3,11 @@ defmodule ChessWeb.GameLive do @impl true def mount(_params, _session, socket) do - {:ok, assign(socket, query: "", results: %{}, counter: 0)} + if connected?(socket) do + :timer.send_interval(1000, self(), :tick) + end + + {:ok, assign(socket, query: "", results: %{}, counter: 0) |> assign_current_time()} end @impl true @@ -30,6 +34,23 @@ defmodule ChessWeb.GameLive do {:noreply, socket |> assign(counter: socket.assigns[:counter] + 1)} end + @impl true + def handle_info(:tick, socket) do + socket = assign_current_time(socket) + + {:noreply, socket} + end + + def assign_current_time(socket) do + now = + Time.utc_now() + |> Time.to_string() + |> String.split(".") + |> hd + + assign(socket, now: now) + end + defp search(query) do if not ChessWeb.Endpoint.config(:code_reloader) do raise "action disabled when not in development" diff --git a/lib/chess_web/live/game_live.html.heex b/lib/chess_web/live/game_live.html.heex index 74540b9..9e1adf1 100644 --- a/lib/chess_web/live/game_live.html.heex +++ b/lib/chess_web/live/game_live.html.heex @@ -15,6 +15,7 @@
+

<%= @now %>

From 08fe35b2756ed156b77c1911305f015b856c1629 Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 03:13:07 +0200 Subject: [PATCH 06/19] Board fixes... --- lib/chess/gameplay/board.ex | 84 ++++++++++++++++++++++++++++--------- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/lib/chess/gameplay/board.ex b/lib/chess/gameplay/board.ex index 958144b..ccb3aa7 100644 --- a/lib/chess/gameplay/board.ex +++ b/lib/chess/gameplay/board.ex @@ -82,16 +82,12 @@ defmodule Chess.Gameplay.Board do board = Map.put(board, end_pos, piece) - board = - cond do - is_castle?(piece, start_pos, end_pos) -> - board |> castle_rook(end_pos) |> Map.get(:board) - - should_promote_pawn?(piece, end_pos) -> - promote_pawn(board, piece, end_pos) - - true -> - board + %{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 %{ @@ -103,23 +99,71 @@ defmodule Chess.Gameplay.Board do } 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 - def should_promote_pawn?(%{type: :pawn, colour: :white}, {_, 7}), do: true - def should_promote_pawn?(%{type: :pawn, colour: :black}, {_, 0}), do: true - def should_promote_pawn?(_, _), do: false - - def promote_pawn(board, %{type: :pawn, colour: colour}, pos) do - Map.put(board, pos, %{type: :queen, colour: colour}) + defp promote_pawn(board, %{type: :pawn, colour: colour}, pos) do + %{board: Map.put(board, pos, %{type: :queen, colour: colour})} end - def is_castle?(%{type: :king}, {4, _}, {end_c, _}), do: end_c == 6 or end_c == 2 - def is_castle?(_, _, _), do: false + 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 - def castle_rook(board, {c, r}) when c == 2, do: board |> move({0, r}, {3, r}) - def castle_rook(board, {c, r}) when c == 6, do: board |> move({7, r}, {5, r}) + 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 From 0beddaab1e13732f87a8fbcf76661dd1a432056b Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 05:21:55 +0200 Subject: [PATCH 07/19] Fix initial state; add state logic --- assets/css/app.css | 4 + lib/chess/gameplay/board.ex | 16 +-- lib/chess/gameplay/game.ex | 71 ++++++++++ lib/chess/gameplay/pawn.ex | 2 +- lib/chess_web/live/game_live.ex | 91 +++++++++---- lib/chess_web/live/game_live.html.heex | 176 +++---------------------- 6 files changed, 173 insertions(+), 187 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index 2c2cd8b..5f00fed 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -135,6 +135,10 @@ grid-template-rows: repeat(8, var(--square-size)); } +.board .selected { + border: 3px dotted #1b1851; +} + .square { display: flex; justify-content: center; diff --git a/lib/chess/gameplay/board.ex b/lib/chess/gameplay/board.ex index ccb3aa7..8e03e3a 100644 --- a/lib/chess/gameplay/board.ex +++ b/lib/chess/gameplay/board.ex @@ -21,14 +21,14 @@ defmodule Chess.Gameplay.Board do {5, 0} => make_piece(:bishop, :white), {6, 0} => make_piece(:knight, :white), {7, 0} => make_piece(:rook, :white), - {0, 7} => make_piece(:rook, :white), - {1, 7} => make_piece(:knight, :white), - {2, 7} => make_piece(:bishop, :white), - {3, 7} => make_piece(:queen, :white), - {4, 7} => make_piece(:king, :white), - {5, 7} => make_piece(:bishop, :white), - {6, 7} => make_piece(:knight, :white), - {7, 7} => 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) diff --git a/lib/chess/gameplay/game.ex b/lib/chess/gameplay/game.ex index ad7e749..0bf2560 100644 --- a/lib/chess/gameplay/game.ex +++ b/lib/chess/gameplay/game.ex @@ -1,3 +1,74 @@ 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: []} + + def move(game_state, from_pos, to_pos) do + %{board: board, turn: turn} = game_state + + 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 + + ########### + # PRIVATE # + ########### + + 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}, + game_state + ) do + %{ + board: board, + moves: + (game_state + |> Map.get(:moves)) ++ + [%{start_p: start_p, end_p: end_p, piece: piece}], + turn: game_state |> Map.get(:turn) |> flip_turn(), + captured_pieces: game_state |> Map.get(:captured_pieces) |> append_if_not_nil(captured) + } + 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, "It's not available move for selected piece"} + + board |> Board.move(from_p, to_p) |> Map.get(:board) |> Board.is_king_checked?(turn) -> + {:error, "The king would be in check"} + + true -> + {:ok, nil} + end + end + + defp negate(b), do: !b end diff --git a/lib/chess/gameplay/pawn.ex b/lib/chess/gameplay/pawn.ex index 695a125..4fa5272 100644 --- a/lib/chess/gameplay/pawn.ex +++ b/lib/chess/gameplay/pawn.ex @@ -9,7 +9,7 @@ defmodule Chess.Gameplay.Pawn do colour = (board |> Board.piece({c, r})).colour regular_moves(board, colour, {c, r}) ++ - attacks(board, colour, {c, r}, List.last(previous_moves)) + attacks(board, colour, {c, r}, previous_moves |> List.last()) end ########### diff --git a/lib/chess_web/live/game_live.ex b/lib/chess_web/live/game_live.ex index 9c3678e..13c9bb8 100644 --- a/lib/chess_web/live/game_live.ex +++ b/lib/chess_web/live/game_live.ex @@ -1,31 +1,46 @@ defmodule ChessWeb.GameLive do use ChessWeb, :live_view + alias Chess.Gameplay.Game + alias Chess.Gameplay.Position + @impl true def mount(_params, _session, socket) do if connected?(socket) do :timer.send_interval(1000, self(), :tick) end - {:ok, assign(socket, query: "", results: %{}, counter: 0) |> assign_current_time()} + {:ok, assign(socket, state: Game.initial(), selected_pos: nil) |> assign_current_time()} end @impl true - def handle_event("suggest", %{"q" => query}, socket) do - {:noreply, assign(socket, results: search(query), query: query)} - end @impl true - def handle_event("search", %{"q" => query}, socket) do - case search(query) do - %{^query => vsn} -> - {:noreply, redirect(socket, external: "https://hexdocs.pm/#{query}/#{vsn}")} - - _ -> - {:noreply, - socket - |> put_flash(:error, "No dependencies found matching \"#{query}\"") - |> assign(results: %{}, query: query)} + def handle_event("select_sq", %{"pos" => pos}, socket) do + with {:ok, pos} <- Position.parse(pos) do + case socket.assigns.selected_pos do + ^pos -> + {:noreply, + socket + |> assign(selected_pos: nil)} + + _ -> + case socket.assigns.selected_pos do + nil -> + {:noreply, socket |> assign(selected_pos: pos)} + + from_p -> + with {:ok, state} <- + Game.move(socket.assigns.state, from_p, pos) do + {:noreply, assign(socket, state: state, selected_pos: nil)} + else + {:error, msg} -> + {:noreply, socket |> put_flash(:error, msg)} + end + end + end + else + {:error, msg} -> {:noreply, socket |> put_flash(:error, msg)} end end @@ -51,15 +66,47 @@ defmodule ChessWeb.GameLive do assign(socket, now: now) end - defp search(query) do - if not ChessWeb.Endpoint.config(:code_reloader) do - raise "action disabled when not in development" + 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 + Integer.to_string(c) <> "," <> Integer.to_string(r) + end + + def pos_to_s(nil), do: nil + + def piece_render(nil), do: nil + + def piece_render(%{type: piece, colour: :white}) do + case piece do + :pawn -> "figure white pawn" + :knight -> "figure white knight" + :bishop -> "figure white bishop" + :rook -> "figure white rook" + :queen -> "figure white queen" + :king -> "figure white king" end + end - for {app, desc, vsn} <- Application.started_applications(), - app = to_string(app), - String.starts_with?(app, query) and not List.starts_with?(desc, ~c"ERTS"), - into: %{}, - do: {app, vsn} + def piece_render(%{type: piece, colour: :black}) do + case piece do + :pawn -> "figure black pawn" + :knight -> "figure black knight" + :bishop -> "figure black bishop" + :rook -> "figure black rook" + :queen -> "figure black queen" + :king -> "figure black king" + end end end diff --git a/lib/chess_web/live/game_live.html.heex b/lib/chess_web/live/game_live.html.heex index 9e1adf1..934eeb4 100644 --- a/lib/chess_web/live/game_live.html.heex +++ b/lib/chess_web/live/game_live.html.heex @@ -1,160 +1,24 @@
-

Peace of mind from prototype to production

- -
- - - <%= for {app, _vsn} <- @results do %> - - <% end %> - - -
-
- -
-
- -

<%= @now %>

-
-
- -
+

Chess

+

<%= @now %>

-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ <%= for row <- 7..0 do %> + <%= for col <- 0..7 do %> +
+ +
+
+ <% end %> + <% end %>
+ +

Turn: <%= @state.turn %>

+

Captures:

+ <%= for c <- @state.captured_pieces do %> +

<%= piece_render(c) %>

+ <% end %>
From 32aa40e2d56412bc45cab6b3fdfbb186320d87f8 Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 06:15:43 +0200 Subject: [PATCH 08/19] Add checkmate validation, other fixes --- lib/chess/gameplay/board.ex | 5 ++ lib/chess/gameplay/game.ex | 79 ++++++++++++++++++++------ lib/chess/gameplay/position.ex | 20 +++---- lib/chess_web/live/game_live.ex | 9 +-- lib/chess_web/live/game_live.html.heex | 17 +++--- 5 files changed, 92 insertions(+), 38 deletions(-) diff --git a/lib/chess/gameplay/board.ex b/lib/chess/gameplay/board.ex index 8e03e3a..7625630 100644 --- a/lib/chess/gameplay/board.ex +++ b/lib/chess/gameplay/board.ex @@ -59,6 +59,11 @@ defmodule Chess.Gameplay.Board do |> 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}) diff --git a/lib/chess/gameplay/game.ex b/lib/chess/gameplay/game.ex index 0bf2560..a7807ce 100644 --- a/lib/chess/gameplay/game.ex +++ b/lib/chess/gameplay/game.ex @@ -6,18 +6,47 @@ defmodule Chess.Gameplay.Game do alias Chess.Gameplay.Board - def initial(), do: %{turn: :white, board: Board.initial_state(), moves: [], captured_pieces: []} + 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} = game_state + %{board: board, turn: turn, outcome: outcome} = game_state - 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"} + 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) + + + def 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 + ########### # PRIVATE # ########### @@ -38,17 +67,29 @@ defmodule Chess.Gameplay.Game do defp update_game_state( %{board: board, captured: captured, piece: piece, start_p: start_p, end_p: end_p}, - game_state + %{moves: moves, turn: turn, captured_pieces: captured_pieces} ) do %{ board: board, - moves: - (game_state - |> Map.get(:moves)) ++ - [%{start_p: start_p, end_p: end_p, piece: piece}], - turn: game_state |> Map.get(:turn) |> flip_turn(), - captured_pieces: game_state |> Map.get(:captured_pieces) |> append_if_not_nil(captured) + 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 @@ -59,10 +100,16 @@ defmodule Chess.Gameplay.Game do 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 -> + board + |> Board.moves(from_p, prev_moves) + |> Enum.any?(&(&1 == to_p)) + |> negate -> {:error, "It's not available move for selected piece"} - board |> Board.move(from_p, to_p) |> Map.get(:board) |> Board.is_king_checked?(turn) -> + board + |> Board.move(from_p, to_p) + |> Map.get(:board) + |> Board.is_king_checked?(turn) -> {:error, "The king would be in check"} true -> @@ -70,5 +117,5 @@ defmodule Chess.Gameplay.Game do end end - defp negate(b), do: !b + defp negate(val), do: !val end diff --git a/lib/chess/gameplay/position.ex b/lib/chess/gameplay/position.ex index 314a64b..45ab9a3 100644 --- a/lib/chess/gameplay/position.ex +++ b/lib/chess/gameplay/position.ex @@ -3,27 +3,25 @@ defmodule Chess.Gameplay.Position do Gameplay position validation """ - def is_valid?({row, _}) when not is_integer(row), do: false - def is_valid?({_, column}) when not is_integer(column), do: false + 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?({_, column}) when column > 7 or column < 0, do: false - def is_valid?({row, _}) when row > 7 or row < 0, 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 [rs, cs] <- String.split(str, ","), - {r, _} <- Integer.parse(rs), + with [cs, rs] <- String.split(str, ","), {c, _} <- Integer.parse(cs), - true <- is_valid?({r, c}) do - {:ok, {r, c}} + {r, _} <- Integer.parse(rs), + true <- is_valid?({c, r}) do + {:ok, {c, r}} else _ -> {:error, "Invalid position"} end end - def to_string({r, c}) do - Integer.to_string(r) <> "," <> Integer.to_string(c) - end + def to_string({c, r}), do: "#{c},#{r}" end diff --git a/lib/chess_web/live/game_live.ex b/lib/chess_web/live/game_live.ex index 13c9bb8..b0225a9 100644 --- a/lib/chess_web/live/game_live.ex +++ b/lib/chess_web/live/game_live.ex @@ -80,10 +80,7 @@ defmodule ChessWeb.GameLive do "square #{colour} #{selected}" end - def pos_to_s({c, r}) do - Integer.to_string(c) <> "," <> Integer.to_string(r) - end - + def pos_to_s({c, r}), do: "#{c},#{r}" def pos_to_s(nil), do: nil def piece_render(nil), do: nil @@ -109,4 +106,8 @@ defmodule ChessWeb.GameLive do :king -> "figure black king" end 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 index 934eeb4..545a9d9 100644 --- a/lib/chess_web/live/game_live.html.heex +++ b/lib/chess_web/live/game_live.html.heex @@ -1,5 +1,5 @@
-

Chess

+

Game

<%= @now %>

<%= for row <- 7..0 do %> @@ -9,16 +9,19 @@ phx-click="select_sq" phx-value-pos={pos_to_s({col, row})} > -
<% end %> <% end %>
-

Turn: <%= @state.turn %>

-

Captures:

- <%= for c <- @state.captured_pieces do %> -

<%= piece_render(c) %>

- <% end %> +
+

Turn: <%= @state.turn %>

+
<%= outcome(@state) %>
+

Captures:

+ <%= for captured <- @state.captured_pieces do %> +

+ <% end %> +
+
From 63aacd62e7262b4dd2d5185e904a5bad6d5d863a Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 06:52:20 +0200 Subject: [PATCH 09/19] Add genserver for saving state --- lib/chess/gameplay/game.ex | 11 +++---- lib/chess/utils/converter.ex | 18 +++++++++++ lib/chess_web/live/game_live.ex | 44 ++++++++++++++++++-------- lib/chess_web/live/game_live.html.heex | 5 +++ lib/chess_web/live/gameplay_server.ex | 43 +++++++++++++++++++++++++ 5 files changed, 101 insertions(+), 20 deletions(-) create mode 100644 lib/chess/utils/converter.ex create mode 100644 lib/chess_web/live/gameplay_server.ex diff --git a/lib/chess/gameplay/game.ex b/lib/chess/gameplay/game.ex index a7807ce..fd3cbb5 100644 --- a/lib/chess/gameplay/game.ex +++ b/lib/chess/gameplay/game.ex @@ -35,10 +35,13 @@ defmodule Chess.Gameplay.Game do def checkmate?(state), do: checked?(state) and no_moves_available?(state) - def stalemate?(state), do: no_moves_available?(state) + def stalemate?(state), do: no_moves_available?(state) and !checked?(state) + ########### + # PRIVATE # + ########### - def no_moves_available?(state) do + defp no_moves_available?(state) do state.board |> Board.find(%{colour: state.turn}) |> Enum.any?(fn {pos, _} -> @@ -47,10 +50,6 @@ defmodule Chess.Gameplay.Game do |> negate end - ########### - # PRIVATE # - ########### - defp exec_move(game_state, from_pos, to_pos) do with {:ok, _} <- game_state |> valid_move?(from_pos, to_pos) do { diff --git a/lib/chess/utils/converter.ex b/lib/chess/utils/converter.ex new file mode 100644 index 0000000..a799ce8 --- /dev/null +++ b/lib/chess/utils/converter.ex @@ -0,0 +1,18 @@ +defmodule Chess.Utils.Converter do + @minute 60 + @hour @minute * 60 + @day @hour * 24 + @week @day * 7 + @divisor [@week, @day, @hour, @minute, 1] + + def sec_to_str(sec) do + {_, [s, m, h, d, w]} = + Enum.reduce(@divisor, {sec, []}, fn divisor, {n, acc} -> + {rem(n, divisor), [div(n, divisor) | acc]} + end) + + ["#{w} wk", "#{d} d", "#{h} hr", "#{m} min", "#{s} sec"] + |> Enum.reject(fn str -> String.starts_with?(str, "0") end) + |> Enum.join(", ") + end +end diff --git a/lib/chess_web/live/game_live.ex b/lib/chess_web/live/game_live.ex index b0225a9..fa6e382 100644 --- a/lib/chess_web/live/game_live.ex +++ b/lib/chess_web/live/game_live.ex @@ -3,18 +3,28 @@ defmodule ChessWeb.GameLive do alias Chess.Gameplay.Game alias Chess.Gameplay.Position + alias Chess.Utils.Converter + alias ChessWeb.GameplayServer @impl true def mount(_params, _session, socket) do - if connected?(socket) do - :timer.send_interval(1000, self(), :tick) - end - - {:ok, assign(socket, state: Game.initial(), selected_pos: nil) |> assign_current_time()} + 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 - @impl true def handle_event("select_sq", %{"pos" => pos}, socket) do with {:ok, pos} <- Position.parse(pos) do @@ -30,8 +40,7 @@ defmodule ChessWeb.GameLive do {:noreply, socket |> assign(selected_pos: pos)} from_p -> - with {:ok, state} <- - Game.move(socket.assigns.state, from_p, pos) do + with {:ok, state} <- GenServer.call(socket.assigns.pid, {:move, from_p, pos}) do {:noreply, assign(socket, state: state, selected_pos: nil)} else {:error, msg} -> @@ -45,15 +54,17 @@ defmodule ChessWeb.GameLive do end @impl true - def handle_event("incr", _, socket) do - {:noreply, socket |> assign(counter: socket.assigns[:counter] + 1)} + def handle_cast({:new_state, state}, socket) do + {:noreply, socket |> assign(state: state)} end @impl true - def handle_info(:tick, socket) do - socket = assign_current_time(socket) + def handle_cast({:new_time, time}, socket) do + {:noreply, socket |> assign_current_time |> assign_player_time(time)} + end - {:noreply, socket} + def assign_player_time(socket, %{white: white_time, black: black_time}) do + socket |> assign(white_time: white_time) |> assign(black_time: black_time) end def assign_current_time(socket) do @@ -66,6 +77,10 @@ defmodule ChessWeb.GameLive do 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 @@ -107,6 +122,7 @@ defmodule ChessWeb.GameLive do end end + def outcome(%{outcome: :out_of_time, turn: turn}), do: "#{turn} lost, ran out of time!" def outcome(%{outcome: :checkmate, turn: turn}), do: "#{turn} lost, checkmate!" def outcome(%{outcome: :stalemate}), do: "Tie by stalemate!" def outcome(_), do: nil diff --git a/lib/chess_web/live/game_live.html.heex b/lib/chess_web/live/game_live.html.heex index 545a9d9..6b5952e 100644 --- a/lib/chess_web/live/game_live.html.heex +++ b/lib/chess_web/live/game_live.html.heex @@ -1,6 +1,11 @@

Game

<%= @now %>

+

White: + <%= if @white_time != 0 do seconds_to_str(@white_time) else "out of time" end %> + | Black: + <%= if @black_time != 0 do seconds_to_str(@black_time) else "out of time" end %> +

<%= for row <- 7..0 do %> <%= for col <- 0..7 do %> diff --git a/lib/chess_web/live/gameplay_server.ex b/lib/chess_web/live/gameplay_server.ex new file mode 100644 index 0000000..50a7c67 --- /dev/null +++ b/lib/chess_web/live/gameplay_server.ex @@ -0,0 +1,43 @@ +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 |> Map.put(:outcome, :out_of_time)}) + {:noreply, server_state |> Map.put(:time, Map.put(time, turn, 0))} + 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 From 4efe6860a8222f2f31c66901f712b7e9f7a56fac Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 06:59:51 +0200 Subject: [PATCH 10/19] handle game... --- config/dev.exs | 2 +- lib/chess_web/live/gameplay_server.ex | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) 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_web/live/gameplay_server.ex b/lib/chess_web/live/gameplay_server.ex index 50a7c67..ad5f72b 100644 --- a/lib/chess_web/live/gameplay_server.ex +++ b/lib/chess_web/live/gameplay_server.ex @@ -21,9 +21,15 @@ defmodule ChessWeb.GameplayServer do with 0 <- time[turn] - 1 do %{state: game_state} = server_state + game_state = game_state |> Map.put(:outcome, :out_of_time) + GenServer.cast(ppid, {:new_time, time |> Map.put(turn, 0)}) - GenServer.cast(ppid, {:new_state, game_state |> Map.put(:outcome, :out_of_time)}) - {:noreply, server_state |> Map.put(:time, Map.put(time, 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} From 8e469f59589d5ec27825eb10875fca6d89b9befb Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 09:40:00 +0200 Subject: [PATCH 11/19] Fix css... etc --- assets/css/app.css | 12 ++++++ lib/chess/utils/converter.ex | 51 +++++++++++++++++++------- lib/chess_web/live/game_live.html.heex | 28 ++++++++------ 3 files changed, 67 insertions(+), 24 deletions(-) diff --git a/assets/css/app.css b/assets/css/app.css index 5f00fed..ec6f23d 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; @@ -197,3 +205,7 @@ .figure.rook { background-position-x: -182.5px; } + +.btm-section { + margin-top: 2rem; +} diff --git a/lib/chess/utils/converter.ex b/lib/chess/utils/converter.ex index a799ce8..db52ebf 100644 --- a/lib/chess/utils/converter.ex +++ b/lib/chess/utils/converter.ex @@ -1,18 +1,43 @@ defmodule Chess.Utils.Converter do @minute 60 @hour @minute * 60 - @day @hour * 24 - @week @day * 7 - @divisor [@week, @day, @hour, @minute, 1] - - def sec_to_str(sec) do - {_, [s, m, h, d, w]} = - Enum.reduce(@divisor, {sec, []}, fn divisor, {n, acc} -> - {rem(n, divisor), [div(n, divisor) | acc]} - end) - - ["#{w} wk", "#{d} d", "#{h} hr", "#{m} min", "#{s} sec"] - |> Enum.reject(fn str -> String.starts_with?(str, "0") end) - |> Enum.join(", ") + + 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/live/game_live.html.heex b/lib/chess_web/live/game_live.html.heex index 6b5952e..7f1bade 100644 --- a/lib/chess_web/live/game_live.html.heex +++ b/lib/chess_web/live/game_live.html.heex @@ -1,11 +1,19 @@ -
-

Game

-

<%= @now %>

-

White: - <%= if @white_time != 0 do seconds_to_str(@white_time) else "out of time" end %> - | Black: - <%= if @black_time != 0 do seconds_to_str(@black_time) else "out of time" end %> -

+
+
+

Game

+

+ White: + <%= if @white_time != 0 do seconds_to_str(@white_time) else "out of time" end %> + || Black: + <%= if @black_time != 0 do seconds_to_str(@black_time) else "out of time" end %> +

+
+
+

Turn: <%= @state.turn %>

+
<%= outcome(@state) %>
+
+ +
<%= for row <- 7..0 do %> <%= for col <- 0..7 do %> @@ -20,9 +28,7 @@ <% end %>
-
-

Turn: <%= @state.turn %>

-
<%= outcome(@state) %>
+

Captures:

<%= for captured <- @state.captured_pieces do %>

From 1618d5c7bc5b5efc55e02147c238b215ce3ac366 Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 09:50:07 +0200 Subject: [PATCH 12/19] formatting the code --- lib/chess_web/live/game_live.ex | 25 +++++++++++++++---------- lib/chess_web/live/game_live.html.heex | 2 +- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/lib/chess_web/live/game_live.ex b/lib/chess_web/live/game_live.ex index fa6e382..34f9f26 100644 --- a/lib/chess_web/live/game_live.ex +++ b/lib/chess_web/live/game_live.ex @@ -55,12 +55,17 @@ defmodule ChessWeb.GameLive do @impl true def handle_cast({:new_state, state}, socket) do - {:noreply, socket |> assign(state: state)} + {:noreply, assign(socket, state: state)} end @impl true def handle_cast({:new_time, time}, socket) do - {:noreply, socket |> assign_current_time |> assign_player_time(time)} + { + :noreply, + socket + |> assign_current_time + |> assign_player_time(time) + } end def assign_player_time(socket, %{white: white_time, black: black_time}) do @@ -102,23 +107,23 @@ defmodule ChessWeb.GameLive do def piece_render(%{type: piece, colour: :white}) do case piece do - :pawn -> "figure white pawn" - :knight -> "figure white knight" :bishop -> "figure white bishop" - :rook -> "figure white rook" - :queen -> "figure white queen" :king -> "figure white king" + :knight -> "figure white knight" + :pawn -> "figure white pawn" + :queen -> "figure white queen" + :rook -> "figure white rook" end end def piece_render(%{type: piece, colour: :black}) do case piece do - :pawn -> "figure black pawn" - :knight -> "figure black knight" :bishop -> "figure black bishop" - :rook -> "figure black rook" - :queen -> "figure black queen" :king -> "figure black king" + :knight -> "figure black knight" + :pawn -> "figure black pawn" + :queen -> "figure black queen" + :rook -> "figure black rook" end end diff --git a/lib/chess_web/live/game_live.html.heex b/lib/chess_web/live/game_live.html.heex index 7f1bade..44501a7 100644 --- a/lib/chess_web/live/game_live.html.heex +++ b/lib/chess_web/live/game_live.html.heex @@ -31,7 +31,7 @@

Captures:

<%= for captured <- @state.captured_pieces do %> -

+
<% end %>
From 10309cd22cdbc947a7a6e576a26f047b70be0c02 Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 10:13:02 +0200 Subject: [PATCH 13/19] DRY... code --- lib/chess_web/live/game_live.ex | 36 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/lib/chess_web/live/game_live.ex b/lib/chess_web/live/game_live.ex index 34f9f26..b11289e 100644 --- a/lib/chess_web/live/game_live.ex +++ b/lib/chess_web/live/game_live.ex @@ -69,7 +69,9 @@ defmodule ChessWeb.GameLive do end def assign_player_time(socket, %{white: white_time, black: black_time}) do - socket |> assign(white_time: white_time) |> assign(black_time: black_time) + socket + |> assign(white_time: white_time) + |> assign(black_time: black_time) end def assign_current_time(socket) do @@ -106,25 +108,29 @@ defmodule ChessWeb.GameLive do def piece_render(nil), do: nil def piece_render(%{type: piece, colour: :white}) do - case piece do - :bishop -> "figure white bishop" - :king -> "figure white king" - :knight -> "figure white knight" - :pawn -> "figure white pawn" - :queen -> "figure white queen" - :rook -> "figure white rook" + figure = + case piece do + :bishop -> "bishop" + :king -> "king" + :knight -> "knight" + :pawn -> "pawn" + :queen -> "queen" + :rook -> "rook" end + "figure white #{figure}" end def piece_render(%{type: piece, colour: :black}) do - case piece do - :bishop -> "figure black bishop" - :king -> "figure black king" - :knight -> "figure black knight" - :pawn -> "figure black pawn" - :queen -> "figure black queen" - :rook -> "figure black rook" + figure = + case piece do + :bishop -> "bishop" + :king -> "king" + :knight -> "knight" + :pawn -> "pawn" + :queen -> "queen" + :rook -> "rook" end + "figure black #{figure}" end def outcome(%{outcome: :out_of_time, turn: turn}), do: "#{turn} lost, ran out of time!" From 9b2609f965af9314f96451dfb188146af1243c61 Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 10:26:21 +0200 Subject: [PATCH 14/19] code format --- lib/chess_web/live/game_live.ex | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/lib/chess_web/live/game_live.ex b/lib/chess_web/live/game_live.ex index b11289e..eb9523f 100644 --- a/lib/chess_web/live/game_live.ex +++ b/lib/chess_web/live/game_live.ex @@ -30,9 +30,7 @@ defmodule ChessWeb.GameLive do with {:ok, pos} <- Position.parse(pos) do case socket.assigns.selected_pos do ^pos -> - {:noreply, - socket - |> assign(selected_pos: nil)} + {:noreply, assign(socket, selected_pos: nil)} _ -> case socket.assigns.selected_pos do @@ -44,12 +42,12 @@ defmodule ChessWeb.GameLive do {:noreply, assign(socket, state: state, selected_pos: nil)} else {:error, msg} -> - {:noreply, socket |> put_flash(:error, msg)} + {:noreply, put_flash(socket, :error, msg)} end end end else - {:error, msg} -> {:noreply, socket |> put_flash(:error, msg)} + {:error, msg} -> {:noreply, put_flash(socket, :error, msg)} end end @@ -62,9 +60,7 @@ defmodule ChessWeb.GameLive do def handle_cast({:new_time, time}, socket) do { :noreply, - socket - |> assign_current_time - |> assign_player_time(time) + assign_player_time(socket, time) } end From 864fbca79613150d80c5067501707b1faccf6863 Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 10:50:19 +0200 Subject: [PATCH 15/19] Remove timing --- lib/chess_web/live/game_live.ex | 14 ++------------ lib/chess_web/live/game_live.html.heex | 6 ------ lib/chess_web/live/gameplay_server.ex | 5 +++-- 3 files changed, 5 insertions(+), 20 deletions(-) diff --git a/lib/chess_web/live/game_live.ex b/lib/chess_web/live/game_live.ex index eb9523f..10a9c8d 100644 --- a/lib/chess_web/live/game_live.ex +++ b/lib/chess_web/live/game_live.ex @@ -57,17 +57,8 @@ defmodule ChessWeb.GameLive do end @impl true - def handle_cast({:new_time, time}, socket) do - { - :noreply, - assign_player_time(socket, time) - } - end - - def assign_player_time(socket, %{white: white_time, black: black_time}) do - socket - |> assign(white_time: white_time) - |> assign(black_time: black_time) + def handle_cast(_, socket) do + {:noreply, socket} end def assign_current_time(socket) do @@ -129,7 +120,6 @@ defmodule ChessWeb.GameLive do "figure black #{figure}" end - def outcome(%{outcome: :out_of_time, turn: turn}), do: "#{turn} lost, ran out of time!" def outcome(%{outcome: :checkmate, turn: turn}), do: "#{turn} lost, checkmate!" def outcome(%{outcome: :stalemate}), do: "Tie by stalemate!" def outcome(_), do: nil diff --git a/lib/chess_web/live/game_live.html.heex b/lib/chess_web/live/game_live.html.heex index 44501a7..abf0ced 100644 --- a/lib/chess_web/live/game_live.html.heex +++ b/lib/chess_web/live/game_live.html.heex @@ -1,12 +1,6 @@

Game

-

- White: - <%= if @white_time != 0 do seconds_to_str(@white_time) else "out of time" end %> - || Black: - <%= if @black_time != 0 do seconds_to_str(@black_time) else "out of time" end %> -

Turn: <%= @state.turn %>

diff --git a/lib/chess_web/live/gameplay_server.ex b/lib/chess_web/live/gameplay_server.ex index ad5f72b..4ebe09f 100644 --- a/lib/chess_web/live/gameplay_server.ex +++ b/lib/chess_web/live/gameplay_server.ex @@ -21,14 +21,15 @@ defmodule ChessWeb.GameplayServer do with 0 <- time[turn] - 1 do %{state: game_state} = server_state - game_state = game_state |> Map.put(:outcome, :out_of_time) 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) + server_state + |> Map.put(:time, Map.put(time, turn, 0)) + |> Map.put(:state, game_state) } else t when t < 0 -> From f49e3559df887c9d7a39e83731608048c5069f04 Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 10:58:18 +0200 Subject: [PATCH 16/19] Inline captured pieces --- assets/css/app.css | 8 ++++++++ lib/chess_web/live/game_live.html.heex | 4 +++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/assets/css/app.css b/assets/css/app.css index ec6f23d..24e84ce 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -209,3 +209,11 @@ .btm-section { margin-top: 2rem; } + +.captured { + margin: 0 auto; +} + +.captured-piece { + float: left; +} diff --git a/lib/chess_web/live/game_live.html.heex b/lib/chess_web/live/game_live.html.heex index abf0ced..788f200 100644 --- a/lib/chess_web/live/game_live.html.heex +++ b/lib/chess_web/live/game_live.html.heex @@ -25,7 +25,9 @@

Captures:

<%= for captured <- @state.captured_pieces do %> -
+
+
+
<% end %>
From 7be5d83d64863f2ff5d76b9fcc0ca9b9238482ba Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 11:00:36 +0200 Subject: [PATCH 17/19] Dry code --- lib/chess_web/templates/page/index.html.heex | 139 ------------------- 1 file changed, 139 deletions(-) delete mode 100644 lib/chess_web/templates/page/index.html.heex 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 @@ -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
From 16d8518a3bc1ee0597d0a9fa0bcf93d08ccc654d Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 11:13:10 +0200 Subject: [PATCH 18/19] Refactor render_piece function; fix warnings --- lib/chess/gameplay/game.ex | 4 ++-- lib/chess_web/live/game_live.ex | 17 ++--------------- 2 files changed, 4 insertions(+), 17 deletions(-) diff --git a/lib/chess/gameplay/game.ex b/lib/chess/gameplay/game.ex index fd3cbb5..49efcdf 100644 --- a/lib/chess/gameplay/game.ex +++ b/lib/chess/gameplay/game.ex @@ -103,13 +103,13 @@ defmodule Chess.Gameplay.Game do |> Board.moves(from_p, prev_moves) |> Enum.any?(&(&1 == to_p)) |> negate -> - {:error, "It's not available move for selected piece"} + {: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 would be in check"} + {:error, "The king will be in check!"} true -> {:ok, nil} diff --git a/lib/chess_web/live/game_live.ex b/lib/chess_web/live/game_live.ex index 10a9c8d..a58a3e2 100644 --- a/lib/chess_web/live/game_live.ex +++ b/lib/chess_web/live/game_live.ex @@ -94,7 +94,7 @@ defmodule ChessWeb.GameLive do def piece_render(nil), do: nil - def piece_render(%{type: piece, colour: :white}) do + def piece_render(%{type: piece, colour: colour}) do figure = case piece do :bishop -> "bishop" @@ -104,20 +104,7 @@ defmodule ChessWeb.GameLive do :queen -> "queen" :rook -> "rook" end - "figure white #{figure}" - end - - def piece_render(%{type: piece, colour: :black}) do - figure = - case piece do - :bishop -> "bishop" - :king -> "king" - :knight -> "knight" - :pawn -> "pawn" - :queen -> "queen" - :rook -> "rook" - end - "figure black #{figure}" + "figure #{colour} #{figure}" end def outcome(%{outcome: :checkmate, turn: turn}), do: "#{turn} lost, checkmate!" From 964e12132126fc74bc9b2184dd51412f72ff36a4 Mon Sep 17 00:00:00 2001 From: Maksym Kosenko Date: Wed, 19 Oct 2022 11:17:37 +0200 Subject: [PATCH 19/19] Remove commented code from router --- lib/chess_web/router.ex | 5 ----- 1 file changed, 5 deletions(-) diff --git a/lib/chess_web/router.ex b/lib/chess_web/router.ex index 38593c1..6692e86 100644 --- a/lib/chess_web/router.ex +++ b/lib/chess_web/router.ex @@ -19,9 +19,4 @@ defmodule ChessWeb.Router do live "/", GameLive, :index end - - # Other scopes may use custom stacks. - # scope "/api", ChessWeb do - # pipe_through :api - # end end