Move Encoding — Sanki
=====================

`reference` `suite-support`

This is a supporting document for the Sashité chess-family NIP suite. It does not define a new event kind or protocol mechanism; it specifies the move encoding that Sashité's clients and every verifier use in the `content` field of Ply events (kind `3423`) when playing and replaying sessions in the `sanki` game context, together with the per-slot and turn-order conventions Sashité enforces for those sessions.

The Ply NIP (kind `3423`) deliberately leaves the `content` field's encoding open to the application layer, and treats turn order as a convention rather than a protocol rule. This document is that application-layer specification for Sashité's Sanki game.

## Scope

This document covers:

- The structured move encoding — the `[source, destination, actor]` array — used in Ply `content` for Sanki, and its **square** and **piece-name** sub-formats.
- **Slot candidates and selection** — a slot may hold several candidate Plies; immutable events, idempotent retries, and the deterministic **two-window** selection among a bounded number of candidates — the **latest legal premove** (committed before the preceding move), else the **earliest legal live move** (the right to err: any illegal candidate is skipped, never a loss).
- **Within-step ordering** (strict alternation) for the Sanki turn order.
- **Premove** conventions.

This document does NOT define:

- Move legality or any rule-system semantics — those are the rule engine's responsibility, exactly as in kind `3423`.
- The session's initial position encoding — see [Position Encoding — Sanki](position-encoding-sanki.md).
- The termination statuses or the verdict-resolution procedure — see [Statuses — Sanki](statuses-sanki.md).
- Move encodings for other games or other rule systems.

## Choice of encoding

For sessions in the `sanki` game context, Sashité encodes each Ply's `content` as a structured **`[source, destination, actor]` JSON array**, not as a textual move notation such as SAN.

The reason mirrors the choice of FEEN over FEN/SFEN for positions (see [Position Encoding — Sanki](position-encoding-sanki.md)): Sanki is a multi-variant game. The two players MAY use different variants — `chess`, `ogi`, `xiongqi` — on a shared 8×8 board, and `ogi` (Sashité's shōgi adaptation) inherits the drop mechanic, in which a piece enters from an off-board reserve. A textual notation like SAN is mono-variant and board-to-board by construction: it cannot natively express a drop, a move in a non-chess variant, or a multi-variant session. SAN remains the natural choice for *pure classical chess* — and is the illustrative encoding in kind `3423` — but it does not cover Sanki's full range.

The structured array is **rule-light**: it carries exactly the source square (or `null` for a drop), the destination square, and an optional actor piece type (for drops and for transformation choices). A consumer can reconstruct the board change for any Sanki variant from the array plus the current position, without a full rule engine and without a variant-specific notation parser. Move legality, capture identification, and all other rule-system semantics remain the responsibility of the rule engine, exactly as in kind `3423`.

## Content format

The `content` field of a Sanki Ply MUST contain a valid JSON array of exactly three elements:

```
[<src>, <dst>, <actor>]
```

- Position 0 (**source**, `<src>`): either `null` (a placement from off-board) or a valid **square** string.
- Position 1 (**destination**, `<dst>`): always a valid **square** string.
- Position 2 (**actor**, `<actor>`): either `null` or a valid **piece-name** string.

When both position 0 and position 1 are non-null squares, they MUST be different.

Position 2 (actor) is non-null in exactly the following cases, and `null` otherwise:

- Position 0 (source) is `null` — the move is a placement from off-board — in which case the actor identifies the piece type being placed.
- The move involves a piece transformation whose target identity is not fully determined by the rule system (the moving player has a meaningful choice), in which case the actor identifies the chosen target type.

A transformation whose target is uniquely determined by the rule system MUST have a `null` actor.

The `content` field MUST NOT carry any data other than this JSON array, and MUST NOT exceed the 256-character bound set by kind `3423`.

### Square format

A **square** value is a non-empty ASCII string identifying a location on the board, structured as a sequence of one, two, or three dimensions:

- The **first** dimension is one or two lowercase Latin letters representing a positive index in base-26 (`a` = 1, `b` = 2, …, `z` = 26, `aa` = 27, …, `iv` = 256).
- A **second** dimension, if present, is a positive integer in decimal with no leading zero, in the range 1 to 256.
- A **third** dimension, if present, is one or two uppercase Latin letters representing a positive index in base-26 (`A` = 1, …, `Z` = 26, `AA` = 27, …, `IV` = 256).

A square value MUST match the following regular expression:

```regex
^[a-z]{1,2}(?:[1-9][0-9]{0,2}[A-Z]{0,2})?$
```

Matching MUST be case-sensitive and applied to the full string (no leading or trailing whitespace, no multiline anchoring). Additionally, the total string length MUST be 1 to 7 characters inclusive, and each dimension's index value MUST be 1 to 256 inclusive.

Examples of valid squares:

- `a1`, `h8` — 2D coordinates on a small board.
- `i9`, `aa10` — 2D coordinates on larger boards.
- `a1A` — 3D coordinate.
- `iv256IV` — maximum coordinate on a 256×256×256 board.

Sanki is played on an 8×8 board, so Sanki Plies use only the 2D subset `a1`–`h8`. The broader format is retained so the same encoding serves variants on larger or higher-dimensional boards. Square coordinates address the board described by the session's FEEN initial position (see [Position Encoding — Sanki](position-encoding-sanki.md)).

### Piece-name format

A **piece-name** value is a non-empty ASCII string consisting only of lowercase Latin letters (`a` through `z`), 1 to 16 characters inclusive:

```regex
^[a-z]{1,16}$
```

This document does not prescribe a piece vocabulary. The specific piece-name tokens recognized for each Sanki variant are defined by Sashité's piece identifier documentation (see [sashite.dev](https://sashite.dev/)) and interpreted by the rule engine.

## Slot candidates and selection

A Ply's slot is `(session, signer, step)`. Each published Ply is **immutable**: it cannot be edited, withdrawn, or cancelled. The protocol does not forbid a player from publishing **more than one Ply for the same slot**; the Plies sharing a slot are that slot's **candidates**, and exactly one becomes the slot's **canonical** half-move (or none, leaving the slot unfilled), chosen by the selection rule below. A later candidate never alters an earlier one; it is simply another alternative the selection may prefer.

- Re-submissions with **identical** `content` are harmless idempotent retries (e.g., re-broadcasting after a transient relay failure), not distinct alternatives. Clients SHOULD re-broadcast the original signed event rather than sign a new one; when distinct identical-content events nonetheless exist, the canonical one among them is selected per kind `3423` §Race resolution (smallest canonical timing, then smallest event id).
- Candidates with **different** `content` are alternatives for the slot, resolved by the selection rule below. Sanki does **not** sanction holding several contents for one slot — there is no "equivocation" termination; a non-selected candidate is simply ineffective.

### Selection rule

When the natural-state computation (kind `3425`) reaches slot `S`, it has already fixed the chain through the preceding slots, so the position at `S`, the boundary `T` — the **maximum canonical timing among the preceding half-moves** in the play order (t₀ for the session's first half-move) — and each candidate's legality in the position are all determined. `T` usually equals the preceding half-move's canonical timing; it differs exactly when that half-move was a selected **premove**, whose *anterior* timing would otherwise rewind the boundary. `T` **never rewinds**: a ply committed before the previous position resolved is blind to it and must classify as anterior at the next slot too, and the clock anchor built on `T` must not bill the next mover for time before the position was theirs to answer ([Time Accounting — Sanki](time-accounting-sanki.md) §Elapsed time). `T` is the **boundary** that splits `S`'s candidates into two windows by their canonical timing:

- **Anterior candidates** — canonical timing **strictly before** `T` (`t(ply) < T`): Plies committed *before* the preceding move existed, i.e. **premoves**.
- **Informed candidates** — canonical timing **at or after** `T` (`t(ply) ≥ T`): Plies committed *with knowledge of* the preceding move, i.e. **live moves**.

The canonical Ply is chosen by trying the two windows in order, **anterior first**:

1. **Anterior window (premoves) — latest legal wins.** Among the anterior candidates, take at most the `K` **most recent** by canonical timing (then largest event id; §Bounding) and scan them newest-first: the canonical Ply is the **first legal one** encountered — that is, the *latest* legal premove. A re-premove (a newer premove for the same slot) thus supersedes an older one, and a premove that turns out illegal is passed over in favour of the next-newest legal premove.
2. **Informed window (live moves) — earliest legal wins.** If **no** anterior candidate is legal, then among the informed candidates take at most the `K` **earliest** by canonical timing (then smallest event id; §Bounding) and scan them oldest-first: the canonical Ply is the **first legal one** encountered — that is, the *earliest* legal live move. Having played a legal move in full knowledge of the position, a player does not overwrite it with a later live move for the same slot.
3. **Unfilled.** If neither window yields a legal candidate within its cap `K`, the slot is **unfilled**: the chain stops here, exactly as for an empty slot (§Within-step ordering), and the player must still produce a legal Ply (their clock running, per [Time Accounting — Sanki](time-accounting-sanki.md)).

An **illegal candidate is always skipped**, in either window, whether it was committed as a premove or played live: an illegal Ply is a non-event. There is **no `illegalmove` termination** — an illegal Ply never ends the game; it is simply passed over, and the clock keeps running until a legal Ply fills the slot. Legality is a **precondition in both windows**; what a candidate's window governs is only *which* legal candidate binds when several exist — the latest among premoves, the earliest among live moves.

**Rationale.** Earlier drafts made a slot hold a single definitive content and sanctioned any divergence — or any illegal move — as an `illegalmove` termination. That sanction served two purposes: a **security** property (defeating *trapping* the opponent into answering a non-canonical move, and *withholding* an earlier-timed content to reveal it late and *rewrite* the chain) and a harsh **game rule** (an illegal move loses). The security purpose is met independently of any selection detail: a candidate's content is public as soon as it has canonical timing — in attested mode an honest timestamper attests only publicly-observed events, and in self-timed mode timing is conferred by the relay accepting the whole event, content inline — so no candidate can be timed yet hidden, and neither attack has material to work with (see [Trust Model](trust-model.md)). The harsh rule is dropped too: **every** illegal candidate is skipped, premove or live, never a loss.

It is *because* an illegal candidate can no longer lose that the anterior/informed distinction is safe to use for **ordering**. An earlier design that let an illegal *informed* move lose the game made any timing-based distinction a footgun — a premove racing the opponent's move, landing just after it and counting as informed, could lose though the player intended a speculation. With that outcome gone, a candidate's window can no longer convert a misfire into a defeat; it only decides, among the **legal** candidates, which one binds: the **latest** legal premove (so re-premoving to change one's mind works) or, absent any legal premove, the **earliest** legal live move (so a move played in full knowledge is committed, not overwritten). The residual effect of the boundary is benign — it can only reorder *legal* candidates, and only when a player holds several straddling `T`; a lone premove wins its slot whichever window it lands in. Selection stays fully deterministic from public events, and the cap `K` of §Bounding keeps the work finite.

### Bounding a slot's candidates

Since an illegal candidate is skipped rather than fatal, a player could otherwise inflate every consumer's work by flooding illegal candidates before a legal one, each tried for legality in turn. Three guards bound it:

- **Candidate cap (normative, `K`).** The cap applies **per window** (§Selection rule). In the **anterior** window a consumer considers at most the `K` **most recent** candidates by canonical timing (then largest event id) and takes the latest legal among them; in the **informed** window, at most the `K` **earliest** by canonical timing (then smallest event id) and takes the earliest legal among them — at most `2K` legality tests per slot. `K` is a small integer chosen so it never constrains honest play — a player needs at most one legal Ply per slot, plus the occasional re-premove or retry — while capping the tests no matter how many candidates are flooded. **The reference value is `K = 8`**, carried by the rule-system document (`session.candidate_cap`, [Rule System — Sanki](rule-system-sanki.md)) so that every consumer of a session uses the same one. The cap is a parameter of the document, not of the deployment, but every consumer of the same sessioyment MUST use the same `K`: the canonical Ply — and therefore the verdict — depends on which candidates fall inside the cap, so consumers using different values could derive different canonical chains. A player flooding their **own** window past `K` only harms themselves (the qualifying legal Ply falls outside the cap, so the slot may go unfilled and their clock runs), so the cap opens no attack on the opponent.
- **Proof of work (per-Ply).** Every Ply carries a required NIP-13 `nonce` tag enforced by the relay's advertised minimum difficulty (kind `3423` §Proof-of-work tag), so each event in a flood costs real work to mint. Because a Ply is **clock-timed**, that difficulty is kept **low** — mining latency is charged to the mover — so PoW is a deterrent against bulk flooding, not the primary bound (kind `3423` §Security considerations).
- **Relay rate-limit (operational).** An event with no canonical timing is not a candidate (§Selection rule; [Race Resolution](race-resolution.md)), so the timing authority is the gate: in **self-timed mode** the serving relay SHOULD rate-limit acceptances per signer; in **attested mode** the designated timestamper SHOULD rate-limit its attestations. Either way an abusive flood is throttled at the source. None of the three adds consumer-side semantics beyond the cap.

The cap `K` keeps a slot's legality tests bounded and fully deterministic from public events, while leaving honest play — one legal move, with room to correct a misfired premove — completely unhindered.

## Within-step ordering

In the `sanki` game context, the `step` value is each player's **own move ordinal** (kind `3423` §Step semantics and play order): a player's first Ply carries `step` `1`, their second `step` `2`, and so on, independently for each player. The slot of a Ply is `(session, signer, step)`. Because the signer is part of the slot, a player **cannot** occupy the opponent's slots — the "step ownership" violation that a shared half-move numbering would permit is structurally inexpressible.

Sashité interleaves the two sequences by **strict alternation**: within a given step value, the player in seat `first` (per the Game Session's `seat` tags) moves before the player in seat `second`. The global half-move order of a session is therefore (step 1, `first`), (step 1, `second`), (step 2, `first`), (step 2, `second`), … — the conventional move numbering of chess scoresheets. Sanki has no consecutive-move situations; a game family that allowed them would define a different interleaving, as kind `3423` permits.

**Effect on the canonical chain.** At the cutoff, the natural-state computation (kind `3425`) consumes the slots in this interleaved order: for each step value k, seat `first`'s slot then seat `second`'s. At each slot it applies the selection rule (§Slot candidates and selection) to that slot's candidates within the cutoff, taking the boundary `T` of §Selection rule — the maximum canonical timing so far, never rewound by a selected premove — for the slot. The chain stops at the first slot for which **no candidate is legal in either window (within the cap `K`)**. Candidates for slots the order has not yet reached (pending premoves, §Premoves) simply wait; they cannot disrupt the opponent's progression.

## Premoves

A **premove** is a Ply signed for one of the signer's future slots — a `(signer, step)` whose position in the interleaved play order (§Within-step ordering) has not yet been reached, and which is therefore committed *before* the preceding half-move exists. Since every slot a player can sign is their own, a premove always pre-fills the signer's own future steps. A player MAY publish premoves in advance — a whole forced sequence at once, or speculative moves arbitrarily deep — and MAY **re-premove** a slot to change their mind.

By construction a premove's canonical timing precedes that of the half-move it will follow, so at its slot it falls in the **anterior window** (§Selection rule). This gives premoves their two defining behaviours:

- **Re-premove supersedes.** Because the anterior window binds the **latest** legal premove, a newer premove for the same slot overrides an older one — the player's most recent intent wins — among the `K` most recent premoves considered (§Bounding).
- **Right to err.** If a premove is **illegal** when its slot is reached, it is **skipped** — a misfired speculation is a non-event, never a loss — and the slot falls to the next-newest legal premove, or, if no premove is legal, to the **earliest legal live move**; if nothing legal remains within the caps, the slot stays unfilled and the player plays live, their clock running (per [Time Accounting — Sanki](time-accounting-sanki.md)).

The right to err is **unconditional**: a premove that turns out illegal never risks the game, whether committed deep in advance or landing at the same instant as the opponent's move. What the anterior window adds on top is only *ordering among legal premoves* — the latest wins — never a way to lose. A live move, by contrast, is committed **on its first legal instance** (the informed window): having seen the position and played a legal move, the player does not overwrite it with a later one.

A premove consumes **no clock** for the interval before the opponent moved: committed during the opponent's turn, its canonical timing precedes the anchor, so its elapsed clamps to zero (see [Time Accounting — Sanki](time-accounting-sanki.md)). This charging follows from its timing; the same timing is what places it in the anterior window for selection.

**Rationale.** Premoves let a player act during the opponent's thinking time, or commit a sequence in advance. The slot structure — the signer being part of the slot — ensures a premove can only pre-fill the player's own steps. The anterior/informed split makes the two intents behave as players expect: a premove is a revisable intention (latest wins, safe to be wrong), a live move is a commitment (first legal stands). The forgiving treatment (illegal skipped, never a loss) is what makes the split safe to rest on timing — a mis-timed premove can at worst be reordered among *legal* candidates, never turned into a defeat. The safety of the forgiving treatment, and its immunity to content-withholding, rest on the timing authority making a candidate's content public as soon as it is timed (§Slot candidates and selection, *Rationale*; [Trust Model](trust-model.md)); the finiteness of the work rests on the cap `K` (§Bounding).

## Implementation notes

The structured array is plain JSON: producing and parsing it requires only a JSON serializer and the square / piece-name validators above — no variant-specific notation parser. A consumer that can parse the session's FEEN initial position (see [Position Encoding — Sanki](position-encoding-sanki.md)) can interpret the coordinates of every Ply in that session, since both address the same board.

## References

- Kind `3423` — Ply (the event whose `content` field this document specifies; defines §Race resolution and the §Step semantics and play order convention whose interleaving this document fixes for Sanki)
- Kind `3425` — Conclusion (the natural-state chain consumed in the interleaved play order)
- [Position Encoding — Sanki](position-encoding-sanki.md) — sibling document; the session's initial position and board coordinates
- [Statuses — Sanki](statuses-sanki.md) — the termination statuses (an illegal Ply is not one: it is skipped, never a loss)
- [Trust Model](trust-model.md) — the timing-authority assumption (a candidate's content is public once timed) the forgiving selection relies on
- [Time Accounting — Sanki](time-accounting-sanki.md) — clock accounting; the clock that runs while a slot waits for a qualifying Ply
- [Race Resolution](race-resolution.md) — deterministic canonical selection among competing Plies
- [Sashité Game Protocol](https://sashite.dev/game-protocol/) — broader protocol context for Sashité's piece and style identifiers
