# `Sashite.Pmn`
[🔗](https://github.com/sashite/pmn.ex/blob/main/lib/sashite/pmn.ex#L1)

PMN (Portable Move Notation) implementation for Elixir.

This library provides parsing, formatting, and validation of PMN move strings
as specified in the [PMN Specification v1.0.0](https://sashite.dev/specs/pmn/1.0.0/).

PMN encodes Moves (ordered sequences of Actions) using an operator-based syntax:

- `-` movement to empty square
- `+` capture (infix: move+capture, prefix: static capture)
- `~` special movement with possible implicit effects
- `*` drop to empty square
- `.` drop with capture
- `=` in-place mutation
- `...` pass move

## Security

This implementation is designed for untrusted input:

- Input length is validated first (DoS protection)
- Sub-tokens are validated by CELL and EPIN libraries
- No regex engine (no ReDoS vector)
- All error atoms are compile-time literals

## Examples

    iex> Sashite.Pmn.parse("e2-e4")
    {:ok, %{form: :move_quiet, source: "e2", destination: "e4", actor: nil}}

    iex> Sashite.Pmn.format(%{form: :move_quiet, source: "e2", destination: "e4", actor: nil})
    {:ok, "e2-e4"}

    iex> Sashite.Pmn.valid?("P*e5")
    true

    iex> Sashite.Pmn.parse("...")
    {:ok, %{form: :pass}}

# `format`

```elixir
@spec format(map()) :: {:ok, String.t()} | {:error, atom()}
```

Formats a structured map into a PMN string.

Returns `{:ok, string}` on success, or `{:error, reason}` on failure.

## Examples

    iex> Sashite.Pmn.format(%{form: :pass})
    {:ok, "..."}

    iex> Sashite.Pmn.format(%{form: :move_quiet, source: "e2", destination: "e4", actor: nil})
    {:ok, "e2-e4"}

    iex> Sashite.Pmn.format(%{form: :move_quiet, source: "e7", destination: "e8", actor: "Q"})
    {:ok, "e7-e8=Q"}

    iex> Sashite.Pmn.format(%{form: :move_capture, source: "b7", destination: "a8", actor: "Q", captured: "r"})
    {:ok, "b7+a8=Q/r"}

    iex> Sashite.Pmn.format(%{form: :move_special, source: "e1", destination: "g1", actor: nil, captured: nil})
    {:ok, "e1~g1"}

    iex> Sashite.Pmn.format(%{form: :static_capture, square: "d4", captured: nil})
    {:ok, "+d4"}

    iex> Sashite.Pmn.format(%{form: :static_capture, square: "d4", captured: "p"})
    {:ok, "+d4/p"}

    iex> Sashite.Pmn.format(%{form: :drop_quiet, piece: "P", destination: "e5", actor: nil})
    {:ok, "P*e5"}

    iex> Sashite.Pmn.format(%{form: :drop_quiet, piece: nil, destination: "e5", actor: nil})
    {:ok, "*e5"}

    iex> Sashite.Pmn.format(%{form: :drop_capture, piece: "L", destination: "b4", actor: nil, captured: nil})
    {:ok, "L.b4"}

    iex> Sashite.Pmn.format(%{form: :in_place_mutation, square: "e4", piece: "+P"})
    {:ok, "e4=+P"}

    iex> Sashite.Pmn.format(%{form: :unknown})
    {:error, :invalid_form}

    iex> Sashite.Pmn.format(%{form: :move_quiet})
    {:error, :missing_field}

Note: for capture forms, pass `captured: nil` when the captured piece is unchanged, and
`captured: "EPIN"` when it mutates. The formatter cannot infer this from the map alone —
the caller is responsible for supplying the correct value based on Position context.

# `format!`

```elixir
@spec format!(map()) :: String.t()
```

Formats a structured map into a PMN string, raising `ArgumentError` on invalid input.

## Examples

    iex> Sashite.Pmn.format!(%{form: :move_quiet, source: "e2", destination: "e4", actor: nil})
    "e2-e4"

    iex> Sashite.Pmn.format!(%{form: :drop_quiet, piece: "P", destination: "e5", actor: nil})
    "P*e5"

# `max_string_length`

```elixir
@spec max_string_length() :: pos_integer()
```

Returns the maximum string length for a PMN move (25).

The longest valid move is `"iv256IV~iv256IV=+K^'/+K^'"` (max CELL + operator +
max CELL + actor transformation + capture transformation).

## Examples

    iex> Sashite.Pmn.max_string_length()
    25

# `parse`

```elixir
@spec parse(String.t()) :: {:ok, map()} | {:error, atom()}
```

Parses a PMN string into a structured map.

Returns `{:ok, map}` on success, or `{:error, reason}` on failure.

## Examples

    iex> Sashite.Pmn.parse("...")
    {:ok, %{form: :pass}}

    iex> Sashite.Pmn.parse("e2-e4")
    {:ok, %{form: :move_quiet, source: "e2", destination: "e4", actor: nil}}

    iex> Sashite.Pmn.parse("e7-e8=Q")
    {:ok, %{form: :move_quiet, source: "e7", destination: "e8", actor: "Q"}}

    iex> Sashite.Pmn.parse("d1+f3/n")
    {:ok, %{form: :move_capture, source: "d1", destination: "f3", actor: nil, captured: "n"}}

    iex> Sashite.Pmn.parse("b7+a8=Q/r")
    {:ok, %{form: :move_capture, source: "b7", destination: "a8", actor: "Q", captured: "r"}}

    iex> Sashite.Pmn.parse("e1~g1")
    {:ok, %{form: :move_special, source: "e1", destination: "g1", actor: nil, captured: nil}}

    iex> Sashite.Pmn.parse("+d4")
    {:ok, %{form: :static_capture, square: "d4", captured: nil}}

    iex> Sashite.Pmn.parse("+d4/p")
    {:ok, %{form: :static_capture, square: "d4", captured: "p"}}

    iex> Sashite.Pmn.parse("P*e5")
    {:ok, %{form: :drop_quiet, piece: "P", destination: "e5", actor: nil}}

    iex> Sashite.Pmn.parse("*e5")
    {:ok, %{form: :drop_quiet, piece: nil, destination: "e5", actor: nil}}

    iex> Sashite.Pmn.parse("L.b4")
    {:ok, %{form: :drop_capture, piece: "L", destination: "b4", actor: nil, captured: nil}}

    iex> Sashite.Pmn.parse("e4=+P")
    {:ok, %{form: :in_place_mutation, square: "e4", piece: "+P"}}

    iex> Sashite.Pmn.parse("")
    {:error, :empty_input}

    iex> Sashite.Pmn.parse("A1-e4")
    {:error, :invalid_cell_token}

Note: for capture forms, `captured: nil` means no mutation occurred — the captured piece
enters the hand unchanged. This is the only valid representation in that case. If the
captured piece's Piece Identity changes, `captured` holds the post-mutation EPIN and
the `/<captured>` suffix is mandatory. The parser accepts both; semantic conformance
(suffix present iff mutation) depends on Position context and is enforced at a higher level.

# `parse!`

```elixir
@spec parse!(String.t()) :: map()
```

Parses a PMN string, raising `ArgumentError` on invalid input.

## Examples

    iex> Sashite.Pmn.parse!("e2-e4")
    %{form: :move_quiet, source: "e2", destination: "e4", actor: nil}

    iex> Sashite.Pmn.parse!("P*e5")
    %{form: :drop_quiet, piece: "P", destination: "e5", actor: nil}

# `valid?`

```elixir
@spec valid?(any()) :: boolean()
```

Returns `true` if the string is a valid PMN move.

This function never raises; it returns `false` for any invalid input,
including non-string values.

## Examples

    iex> Sashite.Pmn.valid?("e2-e4")
    true

    iex> Sashite.Pmn.valid?("...")
    true

    iex> Sashite.Pmn.valid?("P*e5")
    true

    iex> Sashite.Pmn.valid?("e7-e8=Q")
    true

    iex> Sashite.Pmn.valid?("b7+a8=Q/r")
    true

    iex> Sashite.Pmn.valid?("")
    false

    iex> Sashite.Pmn.valid?("A1-e4")
    false

    iex> Sashite.Pmn.valid?(nil)
    false

---

*Consult [api-reference.md](api-reference.md) for complete listing*
