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:
@@ -0,0 +1,39 @@
|
||||
defmodule Chesstrainer.CacheTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Chesstrainer.Cache
|
||||
|
||||
setup do
|
||||
table = :"test_#{System.unique_integer([:positive])}"
|
||||
{:ok, pid} = Cache.start_link(table)
|
||||
on_exit(fn -> if Process.alive?(pid), do: GenServer.stop(pid) end)
|
||||
{:ok, %{pid: pid, table: table}}
|
||||
end
|
||||
|
||||
describe "ChessTrainer.Cache function tests" do
|
||||
test "cache is started", %{pid: pid, table: table} do
|
||||
assert is_pid(pid)
|
||||
assert 0 = Cache.size(table)
|
||||
end
|
||||
|
||||
test "put/3", %{table: table} do
|
||||
assert :ok = Cache.put(table, "key1", "value1")
|
||||
assert {:ok, "value1"} = Cache.get(table, "key1")
|
||||
assert 1 = Cache.size(table)
|
||||
|
||||
assert :ok = Cache.put(table, "key2", "value2")
|
||||
assert {:ok, "value2"} = Cache.get(table, "key2")
|
||||
assert 2 = Cache.size(table)
|
||||
end
|
||||
|
||||
test "delete/2", %{table: table} do
|
||||
assert :ok = Cache.put(table, "key1", "value1")
|
||||
assert :ok = Cache.put(table, "key2", "value2")
|
||||
assert 2 = Cache.size(table)
|
||||
|
||||
assert :ok = Cache.delete(table, "key1")
|
||||
assert 1 = Cache.size(table)
|
||||
assert :error = Cache.get(table, "key1")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
defmodule Chesstrainer.EndgamesTest do
|
||||
use Chesstrainer.DataCase
|
||||
|
||||
alias Chesstrainer.Endgames
|
||||
alias Chesstrainer.Endgames.Endgame
|
||||
|
||||
describe "create_endgame/1 FEN validation" do
|
||||
test "accepts a standard valid FEN" do
|
||||
attrs = %{
|
||||
fen: "8/8/3k4/8/8/3K1R2/8/8 w - - 0 1",
|
||||
color: :white,
|
||||
key: "KR v K",
|
||||
result: :win,
|
||||
rating: 1500
|
||||
}
|
||||
|
||||
assert {:ok, %Endgame{}} = Endgames.create_endgame(attrs)
|
||||
end
|
||||
|
||||
test "normalizes fullmove 0 to 1 before inserting" do
|
||||
attrs = %{
|
||||
fen: "6k1/5p2/6p1/8/7p/8/6PP/6K1 b - - 0 0",
|
||||
color: :black,
|
||||
key: "KPPP v KPP",
|
||||
result: :win,
|
||||
rating: 1500
|
||||
}
|
||||
|
||||
assert {:ok, endgame} = Endgames.create_endgame(attrs)
|
||||
assert endgame.fen == "6k1/5p2/6p1/8/7p/8/6PP/6K1 b - - 0 1"
|
||||
end
|
||||
|
||||
test "trims whitespace around the FEN before inserting" do
|
||||
attrs = %{
|
||||
fen: " 8/8/3k4/8/8/3K1R2/8/8 w - - 0 1\t\n",
|
||||
color: :white,
|
||||
key: "KR v K",
|
||||
result: :win,
|
||||
rating: 1500
|
||||
}
|
||||
|
||||
assert {:ok, endgame} = Endgames.create_endgame(attrs)
|
||||
assert endgame.fen == "8/8/3k4/8/8/3K1R2/8/8 w - - 0 1"
|
||||
end
|
||||
|
||||
test "rejects a FEN with a zero digit in a rank" do
|
||||
attrs = %{
|
||||
fen: "4k3/8/8/8/8/8/8/0K7 w - - 0 1",
|
||||
color: :white,
|
||||
key: "KQ v K",
|
||||
result: :win,
|
||||
rating: 1500
|
||||
}
|
||||
|
||||
assert {:error, changeset} = Endgames.create_endgame(attrs)
|
||||
assert [msg] = errors_on(changeset).fen
|
||||
assert msg =~ "board must be 8 ranks"
|
||||
end
|
||||
|
||||
test "rejects a FEN with the wrong number of fields" do
|
||||
attrs = %{
|
||||
fen: "8/8/3k4/8/8/3K1R2/8/8",
|
||||
color: :white,
|
||||
key: "KQ v K",
|
||||
result: :win,
|
||||
rating: 1500
|
||||
}
|
||||
|
||||
assert {:error, changeset} = Endgames.create_endgame(attrs)
|
||||
assert [msg] = errors_on(changeset).fen
|
||||
assert msg =~ "6 space-separated"
|
||||
end
|
||||
|
||||
test "rejects a FEN with an unknown active color" do
|
||||
attrs = %{
|
||||
fen: "8/8/3k4/8/8/3K1R2/8/8 x - - 0 1",
|
||||
color: :white,
|
||||
key: "KQ v K",
|
||||
result: :win,
|
||||
rating: 1500
|
||||
}
|
||||
|
||||
assert {:error, changeset} = Endgames.create_endgame(attrs)
|
||||
assert [msg] = errors_on(changeset).fen
|
||||
assert msg =~ "active color"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,62 @@
|
||||
defmodule Chesstrainer.FENTest do
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Chesstrainer.FEN
|
||||
|
||||
describe "color_from_fen/1" do
|
||||
test "extracts white from the active-color field" do
|
||||
assert FEN.color_from_fen("8/8/3k4/8/8/3K1R2/8/8 w - - 0 1") == :white
|
||||
end
|
||||
|
||||
test "extracts black from the active-color field" do
|
||||
assert FEN.color_from_fen("6k1/5p2/6p1/8/7p/8/6PP/6K1 b - - 0 1") == :black
|
||||
end
|
||||
|
||||
test "returns nil when the active-color field is missing" do
|
||||
assert FEN.color_from_fen("8/8/8/8/8/8/8/8") == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "normalize/1" do
|
||||
test "trims surrounding whitespace" do
|
||||
fen = "8/8/3k4/8/8/3K1R2/8/8 w - - 0 1"
|
||||
assert FEN.normalize(" \n" <> fen <> "\t\n") == fen
|
||||
end
|
||||
|
||||
test "rewrites fullmove 0 to 1" do
|
||||
fen = "6k1/5p2/6p1/8/7p/8/6PP/6K1 b - - 0 0"
|
||||
assert FEN.normalize(fen) == "6k1/5p2/6p1/8/7p/8/6PP/6K1 b - - 0 1"
|
||||
end
|
||||
|
||||
test "rewrites fullmove 0 even when the FEN has surrounding whitespace" do
|
||||
fen = "\n 6k1/5p2/6p1/8/7p/8/6PP/6K1 b - - 0 0 \t"
|
||||
assert FEN.normalize(fen) == "6k1/5p2/6p1/8/7p/8/6PP/6K1 b - - 0 1"
|
||||
end
|
||||
|
||||
test "leaves a non-zero fullmove untouched" do
|
||||
fen = "8/8/3k4/8/8/3K1R2/8/8 w - - 0 5"
|
||||
assert FEN.normalize(fen) == fen
|
||||
end
|
||||
|
||||
test "leaves a standard starting position untouched" do
|
||||
fen = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"
|
||||
assert FEN.normalize(fen) == fen
|
||||
end
|
||||
|
||||
test "passes through a non-FEN string unchanged" do
|
||||
assert FEN.normalize("not a fen") == "not a fen"
|
||||
end
|
||||
|
||||
test "passes through a partial FEN (fewer than 6 fields) unchanged" do
|
||||
fen = "8/8/3k4/8/8/3K1R2/8/8 w - -"
|
||||
assert FEN.normalize(fen) == fen
|
||||
end
|
||||
|
||||
test "passes through a FEN with a leading-zero fullmove unchanged" do
|
||||
# Fullmove 0 only matches the literal "0"; leading zeros stay so the
|
||||
# validator can surface them rather than silently masking the typo.
|
||||
fen = "8/8/3k4/8/8/3K1R2/8/8 w - - 0 00"
|
||||
assert FEN.normalize(fen) == fen
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,115 @@
|
||||
defmodule ChesstrainerWeb.EndgameLive.PlayTest do
|
||||
use ChesstrainerWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Chesstrainer.EndgamesFixtures
|
||||
|
||||
defp assert_init_board(view, expected_fen, expected_orientation, expected_player_color) do
|
||||
%{proxy: {ref, _topic, _}} = view
|
||||
|
||||
assert_receive {^ref, {:push_event, "init_board", payload}}, 100
|
||||
|
||||
assert payload.fen == expected_fen
|
||||
assert payload.orientation == expected_orientation
|
||||
assert payload.player_color == expected_player_color
|
||||
end
|
||||
|
||||
describe "mount" do
|
||||
test "renders the chessboard hook div and pushes init_board", %{conn: conn} do
|
||||
endgame = endgame_fixture(color: :white)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/endgames/#{endgame}/play")
|
||||
|
||||
assert has_element?(view, "#endgame-board")
|
||||
assert_init_board(view, endgame.fen, "white", "white")
|
||||
end
|
||||
|
||||
test "pushes black orientation when the endgame color is black", %{conn: conn} do
|
||||
endgame = endgame_fixture(color: :black)
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/endgames/#{endgame}/play")
|
||||
|
||||
assert_init_board(view, endgame.fen, "black", "black")
|
||||
end
|
||||
|
||||
test "shows endgame metadata in the header", %{conn: conn} do
|
||||
endgame = endgame_fixture(key: "KQ v K", color: :white, result: :win, rating: 1800)
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/endgames/#{endgame}/play")
|
||||
|
||||
assert html =~ "KQ v K"
|
||||
assert html =~ "white to move"
|
||||
assert html =~ "win"
|
||||
assert html =~ "1800"
|
||||
end
|
||||
|
||||
test "raises when the endgame does not exist", %{conn: conn} do
|
||||
assert_raise Ecto.NoResultsError, fn ->
|
||||
live(conn, ~p"/endgames/00000000-0000-0000-0000-000000000000/play")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "chess_fen_invalid" do
|
||||
test "shows a flash when the chessground hook reports an unparseable FEN", %{conn: conn} do
|
||||
endgame = endgame_fixture()
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/endgames/#{endgame}/play")
|
||||
|
||||
view
|
||||
|> render_hook("chess_fen_invalid", %{
|
||||
"fen" => endgame.fen,
|
||||
"message" => "Invalid FEN: move number must be a positive integer"
|
||||
})
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "Endgame FEN is invalid"
|
||||
assert html =~ "move number must be a positive integer"
|
||||
end
|
||||
end
|
||||
|
||||
describe "endgame_move_played" do
|
||||
test "appends the SAN move and updates last_san / last_from_to", %{conn: conn} do
|
||||
endgame = endgame_fixture()
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/endgames/#{endgame}/play")
|
||||
|
||||
view
|
||||
|> render_hook("endgame_move_played", %{
|
||||
"from" => "d1",
|
||||
"to" => "d7",
|
||||
"san" => "Qd7+",
|
||||
"fen" => endgame.fen,
|
||||
"endgame_id" => endgame.id
|
||||
})
|
||||
|
||||
html = render(view)
|
||||
assert html =~ "Qd7+"
|
||||
assert html =~ "d1 ➔ d7"
|
||||
end
|
||||
end
|
||||
|
||||
describe "reset" do
|
||||
test "clears move list and re-pushes init_board", %{conn: conn} do
|
||||
endgame = endgame_fixture()
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/endgames/#{endgame}/play")
|
||||
|
||||
view
|
||||
|> render_hook("endgame_move_played", %{
|
||||
"from" => "d1",
|
||||
"to" => "d7",
|
||||
"san" => "Qd7+",
|
||||
"fen" => endgame.fen,
|
||||
"endgame_id" => endgame.id
|
||||
})
|
||||
|
||||
assert render(view) =~ "Qd7+"
|
||||
|
||||
view |> element("button", "Reset") |> render_click()
|
||||
|
||||
assert render(view) =~ "No Moves"
|
||||
assert_init_board(view, endgame.fen, "white", "white")
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,28 @@
|
||||
defmodule ChesstrainerWeb.EndgameLive.PlayerIndexTest do
|
||||
use ChesstrainerWeb.ConnCase
|
||||
|
||||
import Phoenix.LiveViewTest
|
||||
import Chesstrainer.EndgamesFixtures
|
||||
|
||||
test "lists endgames with a play link per row", %{conn: conn} do
|
||||
endgame = endgame_fixture()
|
||||
other = endgame_fixture()
|
||||
|
||||
{:ok, _view, html} = live(conn, ~p"/endgames")
|
||||
|
||||
assert html =~ "Endgames"
|
||||
assert html =~ endgame.key
|
||||
assert html =~ other.key
|
||||
assert html =~ ~p"/endgames/#{endgame}/play"
|
||||
assert html =~ ~p"/endgames/#{other}/play"
|
||||
end
|
||||
|
||||
test "navigating to a play page renders the board", %{conn: conn} do
|
||||
endgame = endgame_fixture()
|
||||
|
||||
{:ok, view, _html} = live(conn, ~p"/endgames/#{endgame}/play")
|
||||
|
||||
assert has_element?(view, "#endgame-board")
|
||||
assert has_element?(view, ~s{a[href="/endgames"]})
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
defmodule Chesstrainer.EndgamesFixtures do
|
||||
@moduledoc """
|
||||
This module defines test helpers for creating
|
||||
entities via the `Chesstrainer.Endgames` context.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Generate a unique endgame FEN.
|
||||
|
||||
Endgames have a unique constraint on :fen, so tests need
|
||||
distinct FENs when inserting more than one. Each call
|
||||
shifts the white king's file to avoid FEN collisions.
|
||||
"""
|
||||
def unique_endgame_fen do
|
||||
# White king on rank 1 in columns b..g, leaving 1-6 empty squares
|
||||
# before and 6-1 after. Both before_king and after_king are valid
|
||||
# single digits (1-8), and the rank sums to 8.
|
||||
offset = System.unique_integer([:positive]) |> rem(6)
|
||||
before_king = offset + 1
|
||||
after_king = 7 - before_king
|
||||
"4k3/8/8/8/8/8/8/#{before_king}K#{after_king} w - - 0 1"
|
||||
end
|
||||
|
||||
@doc """
|
||||
Generate an endgame.
|
||||
|
||||
A simple K v K position with a unique FEN, color,
|
||||
key, rating, and result.
|
||||
"""
|
||||
def endgame_fixture(attrs \\ %{}) do
|
||||
{:ok, endgame} =
|
||||
attrs
|
||||
|> Enum.into(%{
|
||||
fen: unique_endgame_fen(),
|
||||
key: "KQ v K",
|
||||
color: :white,
|
||||
result: :win,
|
||||
rating: 1500
|
||||
})
|
||||
|> Chesstrainer.Endgames.create_endgame()
|
||||
|
||||
endgame
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user