mirror of
https://github.com/TheRealOwenRees/chess-endgame-trainer.git
synced 2026-09-19 11:03:52 +00:00
Feature/endgame play (#1)
* create endgame play section * add cache tests * normalise FENs - trim and fuill move to 1 on addition * remove svg on root and allow navbar to exist across all pages * update nav items
This commit is contained in:
@@ -34,6 +34,20 @@ defmodule ChesstrainerWeb.Layouts do
|
||||
slot :inner_block, required: true
|
||||
|
||||
def app(assigns) do
|
||||
~H"""
|
||||
<main class="px-4 py-20 sm:px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-2xl space-y-4">
|
||||
{render_slot(@inner_block)}
|
||||
</div>
|
||||
</main>
|
||||
<.flash_group flash={@flash} />
|
||||
"""
|
||||
end
|
||||
|
||||
@doc """
|
||||
Renders the application navbar.
|
||||
"""
|
||||
def navbar(assigns) do
|
||||
~H"""
|
||||
<header class="navbar px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex-1">
|
||||
@@ -45,30 +59,22 @@ defmodule ChesstrainerWeb.Layouts do
|
||||
<div class="flex-none">
|
||||
<ul class="flex flex-column px-1 space-x-4 items-center">
|
||||
<li>
|
||||
<a href="https://phoenixframework.org/" class="btn btn-ghost">Website</a>
|
||||
<a href="/endgames" class="btn btn-ghost">Endgames</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://github.com/phoenixframework/phoenix" class="btn btn-ghost">GitHub</a>
|
||||
<a href="/admin/endgames" class="btn btn-ghost">Admin</a>
|
||||
</li>
|
||||
<li>
|
||||
<.theme_toggle />
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://phoenix.hexdocs.pm/overview.html" class="btn btn-primary">
|
||||
Get Started <span aria-hidden="true">→</span>
|
||||
<a href="/" class="btn btn-primary">
|
||||
Login / Signup
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="px-4 py-20 sm:px-6 lg:px-8">
|
||||
<div class="mx-auto max-w-2xl space-y-4">
|
||||
{render_slot(@inner_block)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<.flash_group flash={@flash} />
|
||||
"""
|
||||
end
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<Layouts.navbar />
|
||||
{@inner_content}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,43 +1,4 @@
|
||||
<Layouts.flash_group flash={@flash} />
|
||||
<div class="left-[40rem] fixed inset-y-0 right-0 z-0 hidden lg:block xl:left-[50rem]">
|
||||
<svg
|
||||
viewBox="0 0 1480 957"
|
||||
fill="none"
|
||||
aria-hidden="true"
|
||||
class="absolute inset-0 h-full w-full"
|
||||
preserveAspectRatio="xMinYMid slice"
|
||||
>
|
||||
<path fill="#EE7868" d="M0 0h1480v957H0z" />
|
||||
<path
|
||||
d="M137.542 466.27c-582.851-48.41-988.806-82.127-1608.412 658.2l67.39 810 3083.15-256.51L1535.94-49.622l-98.36 8.183C1269.29 281.468 734.115 515.799 146.47 467.012l-8.928-.742Z"
|
||||
fill="#FF9F92"
|
||||
/>
|
||||
<path
|
||||
d="M371.028 528.664C-169.369 304.988-545.754 149.198-1361.45 665.565l-182.58 792.025 3014.73 694.98 389.42-1689.25-96.18-22.171C1505.28 697.438 924.153 757.586 379.305 532.09l-8.277-3.426Z"
|
||||
fill="#FA8372"
|
||||
/>
|
||||
<path
|
||||
d="M359.326 571.714C-104.765 215.795-428.003-32.102-1349.55 255.554l-282.3 1224.596 3047.04 722.01 312.24-1354.467C1411.25 1028.3 834.355 935.995 366.435 577.166l-7.109-5.452Z"
|
||||
fill="#E96856"
|
||||
fill-opacity=".6"
|
||||
/>
|
||||
<path
|
||||
d="M1593.87 1236.88c-352.15 92.63-885.498-145.85-1244.602-613.557l-5.455-7.105C-12.347 152.31-260.41-170.8-1225-131.458l-368.63 1599.048 3057.19 704.76 130.31-935.47Z"
|
||||
fill="#C42652"
|
||||
fill-opacity=".2"
|
||||
/>
|
||||
<path
|
||||
d="M1411.91 1526.93c-363.79 15.71-834.312-330.6-1085.883-863.909l-3.822-8.102C72.704 125.95-101.074-242.476-1052.01-408.907l-699.85 1484.267 2837.75 1338.01 326.02-886.44Z"
|
||||
fill="#A41C42"
|
||||
fill-opacity=".2"
|
||||
/>
|
||||
<path
|
||||
d="M1116.26 1863.69c-355.457-78.98-720.318-535.27-825.287-1115.521l-1.594-8.816C185.286 163.833 112.786-237.016-762.678-643.898L-1822.83 608.665 571.922 2635.55l544.338-771.86Z"
|
||||
fill="#A41C42"
|
||||
fill-opacity=".2"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="px-4 py-10 sm:px-6 sm:py-28 lg:px-8 xl:px-28 xl:py-32">
|
||||
<div class="mx-auto max-w-xl lg:mx-0">
|
||||
<svg viewBox="0 0 71 48" class="h-12" aria-hidden="true">
|
||||
|
||||
@@ -34,6 +34,7 @@ defmodule ChesstrainerWeb.EndgameLive.Index do
|
||||
<div class="sr-only">
|
||||
<.link navigate={~p"/admin/endgames/#{endgame}"}>Show</.link>
|
||||
</div>
|
||||
<.link navigate={~p"/endgames/#{endgame}/play"}>Play</.link>
|
||||
<.link navigate={~p"/admin/endgames/#{endgame}/edit"}>Edit</.link>
|
||||
</:action>
|
||||
<:action :let={{id, endgame}}>
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
defmodule ChesstrainerWeb.EndgameLive.Play do
|
||||
use ChesstrainerWeb, :live_view
|
||||
|
||||
alias Chesstrainer.Endgames
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
Play endgame {@endgame.key}
|
||||
<:subtitle>
|
||||
{@endgame.color} to move · Result: {@endgame.result} · Rating: {@endgame.rating}
|
||||
</:subtitle>
|
||||
<:actions>
|
||||
<.button navigate={~p"/endgames"}>
|
||||
<.icon name="hero-arrow-left" /> Back to endgames
|
||||
</.button>
|
||||
</:actions>
|
||||
</.header>
|
||||
|
||||
<div class="bg-gray-100 p-4 rounded mb-4 flex justify-between items-center shadow-sm">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600 font-semibold">Last Played Move:</p>
|
||||
<p class="text-lg font-mono text-blue-600">{@last_from_to}</p>
|
||||
<p class="text-lg font-mono text-blue-600">{@last_san}</p>
|
||||
<p class="text-lg font-mono text-emerald-600">
|
||||
{if @move_list == [] do
|
||||
"No Moves"
|
||||
else
|
||||
Enum.reverse(@move_list) |> Enum.join(", ")
|
||||
end}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<.button phx-click="reset" variant="primary">
|
||||
<.icon name="hero-arrow-path" /> Reset
|
||||
</.button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center bg-gray-200 p-4 rounded-xl shadow-inner">
|
||||
<div
|
||||
id="endgame-board"
|
||||
phx-hook="ChessBoard"
|
||||
phx-update="ignore"
|
||||
data-endgame-id={@endgame.id}
|
||||
class="w-[400px] h-[400px]"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<.list>
|
||||
<:item title="Fen">{@endgame.fen}</:item>
|
||||
<:item title="Key">{@endgame.key}</:item>
|
||||
<:item title="Color">{@endgame.color}</:item>
|
||||
<:item title="Result">{@endgame.result}</:item>
|
||||
<:item title="Rating">{@endgame.rating}</:item>
|
||||
</.list>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(%{"id" => id}, _session, socket) do
|
||||
endgame = Endgames.get_endgame!(id)
|
||||
|
||||
socket =
|
||||
socket
|
||||
|> assign(:page_title, "Play endgame")
|
||||
|> assign(:endgame, endgame)
|
||||
|> assign(:move_list, [])
|
||||
|> assign(:last_san, "None yet")
|
||||
|> assign(:last_from_to, "None yet")
|
||||
|> push_event("init_board", %{
|
||||
fen: endgame.fen,
|
||||
orientation: Atom.to_string(endgame.color),
|
||||
player_color: Atom.to_string(endgame.color)
|
||||
})
|
||||
|
||||
{:ok, socket}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_event(
|
||||
"endgame_move_played",
|
||||
%{"from" => from, "to" => to, "san" => san},
|
||||
socket
|
||||
) do
|
||||
# TODO: server-side legality check + tablebase evaluation when wired
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:last_from_to, "#{from} ➔ #{to}")
|
||||
|> assign(:last_san, san)
|
||||
|> assign(:move_list, [san | socket.assigns.move_list])}
|
||||
end
|
||||
|
||||
def handle_event("chess_fen_invalid", %{"fen" => fen, "message" => message}, socket) do
|
||||
{:noreply,
|
||||
put_flash(
|
||||
socket,
|
||||
:error,
|
||||
"Endgame FEN is invalid and the board cannot be loaded: #{message} (#{fen})"
|
||||
)}
|
||||
end
|
||||
|
||||
def handle_event("reset", _params, socket) do
|
||||
{:noreply,
|
||||
socket
|
||||
|> assign(:move_list, [])
|
||||
|> assign(:last_san, "None yet")
|
||||
|> assign(:last_from_to, "None yet")
|
||||
|> push_event("init_board", %{
|
||||
fen: socket.assigns.endgame.fen,
|
||||
orientation: Atom.to_string(socket.assigns.endgame.color),
|
||||
player_color: Atom.to_string(socket.assigns.endgame.color)
|
||||
})}
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,38 @@
|
||||
defmodule ChesstrainerWeb.EndgameLive.PlayerIndex do
|
||||
use ChesstrainerWeb, :live_view
|
||||
|
||||
alias Chesstrainer.Endgames
|
||||
|
||||
@impl true
|
||||
def render(assigns) do
|
||||
~H"""
|
||||
<Layouts.app flash={@flash}>
|
||||
<.header>
|
||||
Endgames
|
||||
<:subtitle>Pick an endgame to play.</:subtitle>
|
||||
</.header>
|
||||
|
||||
<.table
|
||||
id="player-endgames"
|
||||
rows={@streams.endgames}
|
||||
>
|
||||
<:col :let={{_id, endgame}} label="Key">{endgame.key}</:col>
|
||||
<:col :let={{_id, endgame}} label="Color">{endgame.color}</:col>
|
||||
<:col :let={{_id, endgame}} label="Result">{endgame.result}</:col>
|
||||
<:col :let={{_id, endgame}} label="Rating">{endgame.rating}</:col>
|
||||
<:action :let={{_id, endgame}}>
|
||||
<.link navigate={~p"/endgames/#{endgame}/play"}>Play</.link>
|
||||
</:action>
|
||||
</.table>
|
||||
</Layouts.app>
|
||||
"""
|
||||
end
|
||||
|
||||
@impl true
|
||||
def mount(_params, _session, socket) do
|
||||
{:ok,
|
||||
socket
|
||||
|> assign(:page_title, "Endgames")
|
||||
|> stream(:endgames, Endgames.list_endgames())}
|
||||
end
|
||||
end
|
||||
@@ -31,6 +31,9 @@ defmodule ChesstrainerWeb.EndgameLive.Show do
|
||||
<.button navigate={~p"/admin/endgames"}>
|
||||
<.icon name="hero-arrow-left" />
|
||||
</.button>
|
||||
<.button navigate={~p"/endgames/#{@endgame}/play"}>
|
||||
<.icon name="hero-play" /> Play (opens trainer)
|
||||
</.button>
|
||||
<.button variant="primary" navigate={~p"/admin/endgames/#{@endgame}/edit?return_to=show"}>
|
||||
<.icon name="hero-pencil-square" /> Edit endgame
|
||||
</.button>
|
||||
|
||||
@@ -20,6 +20,9 @@ defmodule ChesstrainerWeb.Router do
|
||||
get "/", PageController, :home
|
||||
|
||||
live "/chess-test", ChessTestLive
|
||||
|
||||
live "/endgames", EndgameLive.PlayerIndex, :index
|
||||
live "/endgames/:id/play", EndgameLive.Play, :play
|
||||
end
|
||||
|
||||
scope "/admin", ChesstrainerWeb do
|
||||
|
||||
Reference in New Issue
Block a user