Packages

PMN (Portable Move Notation) implementation for Elixir. Provides parsing and validation of protocol-level Moves as ordered sequences of Actions in abstract strategy board games.

Current section

Files

Jump to
sashite_pmn lib sashite pmn formatter.ex
Raw

lib/sashite/pmn/formatter.ex

defmodule Sashite.Pmn.Formatter do
@moduledoc false
# Formats move maps into PMN strings.
#
# Validates the structural shape of the map (required keys, value types)
# but does NOT re-validate CELL/EPIN token contents. The contract is:
# maps produced by Parser.parse/1 are guaranteed to format correctly,
# and maps built by the caller are trusted to contain valid tokens.
#
# Assembly uses iodata lists to avoid intermediate binary allocations.
# Each form builds a list of fragments and converts once via
# IO.iodata_to_binary/1.
#
# Time complexity: O(1) — bounded number of map lookups and list cons.
# Space complexity: O(1) — iodata list of at most 7 elements.
# ── Public API ────────────────────────────────────────────────────────
@doc false
@spec format(map()) :: {:ok, String.t()} | {:error, atom()}
def format(%{form: :pass}), do: {:ok, "..."}
def format(%{form: :move_quiet} = move) do
with {:ok, src} <- require_string(move, :source),
{:ok, dst} <- require_string(move, :destination) do
{:ok, IO.iodata_to_binary([src, "-", dst | actor_suffix(move)])}
end
end
def format(%{form: :move_capture} = move) do
with {:ok, src} <- require_string(move, :source),
{:ok, dst} <- require_string(move, :destination) do
{:ok, IO.iodata_to_binary([src, "+", dst | actor_captured_suffix(move)])}
end
end
def format(%{form: :move_special} = move) do
with {:ok, src} <- require_string(move, :source),
{:ok, dst} <- require_string(move, :destination) do
{:ok, IO.iodata_to_binary([src, "~", dst | actor_captured_suffix(move)])}
end
end
def format(%{form: :static_capture} = move) do
with {:ok, square} <- require_string(move, :square) do
{:ok, IO.iodata_to_binary(["+", square | captured_suffix(move)])}
end
end
def format(%{form: :drop_quiet} = move) do
with {:ok, dst} <- require_string(move, :destination) do
{:ok, IO.iodata_to_binary([piece_prefix(move), "*", dst | actor_suffix(move)])}
end
end
def format(%{form: :drop_capture} = move) do
with {:ok, dst} <- require_string(move, :destination) do
{:ok, IO.iodata_to_binary([piece_prefix(move), ".", dst | actor_captured_suffix(move)])}
end
end
def format(%{form: :in_place_mutation} = move) do
with {:ok, square} <- require_string(move, :square),
{:ok, piece} <- require_string(move, :piece) do
{:ok, IO.iodata_to_binary([square, "=", piece])}
end
end
def format(%{form: _}), do: {:error, :invalid_form}
def format(move) when is_map(move), do: {:error, :invalid_form}
def format(_), do: {:error, :not_a_map}
@doc false
@spec format!(map()) :: String.t()
def format!(move) do
case format(move) do
{:ok, string} -> string
{:error, reason} -> raise ArgumentError, Atom.to_string(reason)
end
end
# ── Suffix builders ──────────────────────────────────────────────────
#
# Return iodata fragments for optional suffixes. The spec requires
# actor transformation (=piece) before capture transformation (/piece).
defp actor_suffix(%{actor: actor}) when is_binary(actor), do: ["=", actor]
defp actor_suffix(_), do: []
# captured_suffix/1 — when captured is nil, no suffix is emitted (captured piece
# is unchanged; omitting the suffix is the only valid form in that case). When
# captured is a string, the suffix is emitted (captured piece mutated; the suffix
# is mandatory). The caller is responsible for supplying the correct value based
# on Position context; this formatter has no access to the Position.
defp captured_suffix(%{captured: captured}) when is_binary(captured), do: ["/", captured]
defp captured_suffix(_), do: []
defp actor_captured_suffix(move) do
actor_suffix(move) ++ captured_suffix(move)
end
defp piece_prefix(%{piece: piece}) when is_binary(piece), do: piece
defp piece_prefix(_), do: ""
# ── Field extraction ─────────────────────────────────────────────────
defp require_string(move, key) do
case Map.fetch(move, key) do
{:ok, value} when is_binary(value) -> {:ok, value}
{:ok, _} -> {:error, :invalid_field_value}
:error -> {:error, :missing_field}
end
end
end