mirror of
https://github.com/TheRealOwenRees/chess-endgame-trainer.git
synced 2026-09-19 11:03:52 +00:00
normalise FENs - trim and fuill move to 1 on addition
This commit is contained in:
@@ -28,6 +28,9 @@ chesstrainer-*.tar
|
|||||||
# Ignore assets that are produced by build tools.
|
# Ignore assets that are produced by build tools.
|
||||||
/priv/static/assets/
|
/priv/static/assets/
|
||||||
|
|
||||||
|
# Ignore ETS table backups
|
||||||
|
/priv/static/backups/
|
||||||
|
|
||||||
# Ignore digested assets cache.
|
# Ignore digested assets cache.
|
||||||
/priv/static/cache_manifest.json
|
/priv/static/cache_manifest.json
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ defmodule Chesstrainer.Endgames.Endgame do
|
|||||||
@doc false
|
@doc false
|
||||||
def changeset(endgame, attrs) do
|
def changeset(endgame, attrs) do
|
||||||
endgame
|
endgame
|
||||||
|> cast(attrs, [:fen, :key, :message, :notes, :result, :rating, :color])
|
|> cast(normalize_attrs(attrs), [:fen, :key, :message, :notes, :result, :rating, :color])
|
||||||
|> validate_required([:fen, :color], message: "Invalid FEN")
|
|> validate_required([:fen, :color], message: "Invalid FEN")
|
||||||
|> validate_required([:key, :result, :rating])
|
|> validate_required([:key, :result, :rating])
|
||||||
|> validate_format(:key, ~r/^(?=.{5,10}$)KQ*R*[NB]*P*\sv\sKQ*R*[NB]*P*$/,
|
|> validate_format(:key, ~r/^(?=.{5,10}$)KQ*R*[NB]*P*\sv\sKQ*R*[NB]*P*$/,
|
||||||
@@ -45,6 +45,14 @@ defmodule Chesstrainer.Endgames.Endgame do
|
|||||||
|> unique_constraint(:fen, message: "FEN already exists")
|
|> unique_constraint(:fen, message: "FEN already exists")
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp normalize_attrs(%{"fen" => fen} = attrs) when is_binary(fen),
|
||||||
|
do: Map.put(attrs, "fen", Chesstrainer.FEN.normalize(fen))
|
||||||
|
|
||||||
|
defp normalize_attrs(%{fen: fen} = attrs) when is_binary(fen),
|
||||||
|
do: Map.put(attrs, :fen, Chesstrainer.FEN.normalize(fen))
|
||||||
|
|
||||||
|
defp normalize_attrs(attrs), do: attrs
|
||||||
|
|
||||||
# Strict structural validation. Mirrors chess.js rules so the FEN survives
|
# Strict structural validation. Mirrors chess.js rules so the FEN survives
|
||||||
# both the server-side parser (Chex, permissive) and the client-side parser
|
# both the server-side parser (Chex, permissive) and the client-side parser
|
||||||
# (chess.js, strict).
|
# (chess.js, strict).
|
||||||
|
|||||||
@@ -7,6 +7,39 @@ defmodule Chesstrainer.FEN do
|
|||||||
|> color_initial_to_color_atom()
|
|> color_initial_to_color_atom()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Normalizes a FEN string for storage.
|
||||||
|
|
||||||
|
Trims surrounding whitespace and rewrites a fullmove number of `"0"` to
|
||||||
|
`"1"`. The fullmove rewrite is a chess.js-compatibility fix — chess.js
|
||||||
|
rejects fullmove `0` as invalid even though many board editors default
|
||||||
|
to it. Other FEN fields are left untouched; structural errors are
|
||||||
|
surfaced by the Endgame changeset, not silently masked here.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
iex> Chesstrainer.FEN.normalize(" 8/8/3k4/8/8/3K1R2/8/8 w - - 0 0\\n ")
|
||||||
|
"8/8/3k4/8/8/3K1R2/8/8 w - - 0 1"
|
||||||
|
|
||||||
|
iex> Chesstrainer.FEN.normalize("8/8/3k4/8/8/3K1R2/8/8 w - - 0 5")
|
||||||
|
"8/8/3k4/8/8/3K1R2/8/8 w - - 0 5"
|
||||||
|
|
||||||
|
iex> Chesstrainer.FEN.normalize("not a fen")
|
||||||
|
"not a fen"
|
||||||
|
"""
|
||||||
|
@spec normalize(String.t()) :: String.t()
|
||||||
|
def normalize(fen) when is_binary(fen) do
|
||||||
|
trimmed = String.trim(fen)
|
||||||
|
|
||||||
|
case String.split(trimmed, " ") do
|
||||||
|
[board, active, castling, en_passant, halfmove, "0"] ->
|
||||||
|
Enum.join([board, active, castling, en_passant, halfmove, "1"], " ")
|
||||||
|
|
||||||
|
_ ->
|
||||||
|
trimmed
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
defp color_initial_to_color_atom("b"), do: :black
|
defp color_initial_to_color_atom("b"), do: :black
|
||||||
defp color_initial_to_color_atom("w"), do: :white
|
defp color_initial_to_color_atom("w"), do: :white
|
||||||
defp color_initial_to_color_atom(_), do: nil
|
defp color_initial_to_color_atom(_), do: nil
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ defmodule Chesstrainer.EndgamesTest do
|
|||||||
assert {:ok, %Endgame{}} = Endgames.create_endgame(attrs)
|
assert {:ok, %Endgame{}} = Endgames.create_endgame(attrs)
|
||||||
end
|
end
|
||||||
|
|
||||||
test "rejects a FEN with fullmove number zero" do
|
test "normalizes fullmove 0 to 1 before inserting" do
|
||||||
attrs = %{
|
attrs = %{
|
||||||
fen: "6k1/5p2/6p1/8/7p/8/6PP/6K1 b - - 0 0",
|
fen: "6k1/5p2/6p1/8/7p/8/6PP/6K1 b - - 0 0",
|
||||||
color: :black,
|
color: :black,
|
||||||
@@ -26,9 +26,21 @@ defmodule Chesstrainer.EndgamesTest do
|
|||||||
rating: 1500
|
rating: 1500
|
||||||
}
|
}
|
||||||
|
|
||||||
assert {:error, changeset} = Endgames.create_endgame(attrs)
|
assert {:ok, endgame} = Endgames.create_endgame(attrs)
|
||||||
assert [msg] = errors_on(changeset).fen
|
assert endgame.fen == "6k1/5p2/6p1/8/7p/8/6PP/6K1 b - - 0 1"
|
||||||
assert msg =~ "fullmove number"
|
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
|
end
|
||||||
|
|
||||||
test "rejects a FEN with a zero digit in a rank" do
|
test "rejects a FEN with a zero digit in a rank" do
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user