basic admin endgame view and add (#3)

Reviewed-on: #3
This commit was merged in pull request #3.
This commit is contained in:
2026-06-22 18:44:52 +00:00
parent e955e40930
commit b47fcdffcb
82 changed files with 2031 additions and 2 deletions
@@ -0,0 +1,17 @@
defmodule Chesstrainer.Endgames.Tablebase.Cache do
@moduledoc """
A cache of tablebases by FEN.
"""
alias Chesstrainer.Cache
@table :tablebase
def put(fen, tablebase), do: Cache.put(@table, fen, tablebase)
def get(fen), do: Cache.get(@table, fen)
def delete(fen), do: Cache.delete(@table, fen)
def show, do: Cache.show(@table)
def size, do: Cache.size(@table)
end
@@ -0,0 +1,41 @@
defmodule Chesstrainer.Endgames.Tablebase.Move do
@moduledoc """
Moves struct for the tablebase
"""
@type result :: :win | :draw | :loss
@type t :: %__MODULE__{
category: result,
checkmate: boolean(),
conversion: boolean(),
dtc: integer() | nil,
dtm: integer() | nil,
dtw: integer() | nil,
dtz: integer() | nil,
precise_dtz: integer() | nil,
san: String.t() | nil,
stalemate: boolean(),
uci: String.t() | nil,
variant_loss: boolean(),
variant_win: boolean(),
zeroing: boolean(),
insufficient_material: boolean()
}
defstruct category: nil,
checkmate: false,
conversion: false,
dtc: nil,
dtm: nil,
dtw: nil,
dtz: nil,
precise_dtz: nil,
san: nil,
stalemate: false,
uci: nil,
variant_loss: false,
variant_win: false,
zeroing: false,
insufficient_material: false
end
@@ -0,0 +1,106 @@
defmodule Chesstrainer.Endgames.Tablebase.Parser do
@moduledoc """
Fetch and parse tablebase data to fit the tablebase struct
Lichess tablebase response from FEN string.
https://lichess.org/api#tag/tablebase
FEN must have spaces replaced by underscores to satisfy the Lichess API.
"""
import Chesstrainer.Ratelimiters
alias Chesstrainer.Endgames.Tablebase
@lichess_tablebase_url "http://tablebase.lichess.ovh/standard?fen="
@spec from_fen(String.t()) ::
{:ok, Tablebase.t()} | {:cooldown, non_neg_integer()} | {:error, term()}
def from_fen(fen) do
case lichess_check_cooldown() do
{:ok, 0} -> tablebase_response(fen)
{:cooldown, remaining_ms} -> {:cooldown, remaining_ms}
end
end
@spec tablebase_response(String.t()) :: {:ok, Tablebase.t()} | {:error, term()}
defp tablebase_response(fen) do
response =
fen
|> sanitise_fen()
|> build_request_string()
|> Req.get()
|> parse_response()
case response do
{:ok, body} -> from_map(body)
{:error, reason} -> {:error, reason}
end
end
# exposed for testing
def from_map(map) do
{:ok,
%Tablebase{
category: map["category"] |> String.to_atom(),
checkmate: map["checkmate"],
dtc: map["dtc"],
dtm: map["dtm"],
dtw: map["dtw"],
dtz: map["dtz"],
insufficient_material: map["insufficient_material"],
moves: Enum.map(map["moves"], &moves_from_map/1)
}}
end
defp moves_from_map(map) do
%Tablebase.Move{
category: map["category"] |> String.to_atom(),
checkmate: map["checkmate"],
conversion: map["conversion"],
dtc: map["dtc"],
dtm: map["dtm"],
dtw: map["dtw"],
dtz: map["dtz"],
insufficient_material: map["insufficient_material"],
precise_dtz: map["precise_dtz"],
san: map["san"],
stalemate: map["stalemate"],
uci: map["uci"],
variant_loss: map["variant_loss"],
variant_win: map["variant_win"],
zeroing: map["zeroing"]
}
end
defp parse_response(response) do
case response do
# Success
{:ok, %Req.Response{status: 200, body: body}} ->
{:ok, body}
# Client errors
{:ok, %Req.Response{status: status, body: body}} when status in 400..499 ->
{:error, {:client_error, status, body}}
# Rate limiting
{:ok, %Req.Response{status: 429, body: body}} ->
lichess_add_cooldown()
{:error, {:too_many_requests, 429, body}}
# Server errors
{:ok, %Req.Response{status: status, body: body}} when status in 500..599 ->
{:error, {:server_error, status, body}}
# Catchall for other status codes
{:ok, %Req.Response{status: status, body: body}} ->
{:error, {:http_error, status, body}}
# Network / clientside failure
{:error, reason} ->
{:error, reason}
end
end
defp sanitise_fen(fen), do: String.replace(fen, " ", "_")
defp build_request_string(fen), do: "#{@lichess_tablebase_url}#{fen}"
end