# Sashité Sanki Engine
[](https://crates.io/crates/sashite-sanki-engine)
[](https://docs.rs/sashite-sanki-engine)
[](https://github.com/sashite/sanki-engine.rs/blob/main/LICENSE)
Rules engine for the **Sanki** game suite — `chess`, `ogi`, and `xiongqi`, all
played on an 8×8 board — built for [Sashité](https://sashite.com/). A pure rules
engine with **no Nostr dependency**, published under the Apache-2.0 license.
The adjudication layer (race resolution, verdict assembly over an abstract event
model) lives in the companion crate
[`sashite-sanki-arbiter`](https://github.com/sashite/sanki-arbiter.rs), which
depends on this one.
## Architecture
The crate is layered, each layer building only on those below it:
- **L0 — geometry** (`movement`, `position`): the board, the pieces, and the
**attack relation** every rule above is phrased in. Public and useful on its
own — check detection, castling legality and uchifuzume are all written
against `movement::attack`, and so is anything else that needs to know what a
piece bears on.
- **L1 — kernel** (`kernel`): a pure per-ply transition
(`legality → apply → canonicalize → tick → terminal`). No I/O, no Nostr.
- The `engine` module is the ergonomic **façade** over the kernel.
## Design guarantees
- **Panic-free by construction.** Crate lints forbid `unsafe`, and deny
`unwrap`/`expect`/`panic`, slice indexing, and overflowing arithmetic. The
kernel never fails on a well-formed input: an illegal move is a structured
**rejection** (`StepResult::Illegal`, with its `IllegalReason`), never a
panic.
- **One legality, everywhere.** The `engine` façade applies the full rule
system — including ōgi's uchifuzume (a Fu drop may not deliver checkmate),
rejected as `IllegalReason::Uchifuzume` by `validate`/`apply`, excluded from
`legal_moves`, and reflected in `status`'s checkmate/stalemate
classification — exactly the legality the kernel enforces per ply.
- **Ten statuses, no `illegalmove`.** An illegal ply is a **rejection**, never
a termination: `validate`/`apply` return the precise `IllegalReason`, and the
kernel's `StepResult::Illegal` hands the untouched state back — the player
keeps the turn, per statuses-sanki (an illegal Ply is skipped, never a loss).
- **Deterministic.** Every entry point is a pure function of its inputs; the
per-session concerns (clocks, the history that repetition, the move-limit, and
the absolute 300-move cap depend on) live in the `kernel`.
- **Three variants, one engine.** Chess, ōgi, and xiongqi share a single move,
capture, and hand-conversion model; cross-variant interactions follow one
common model. The one deliberately variant-specific terminal rule is
**dead-position detection** (rules-of-*.md §Dead-Position Detection): the
material-only drawn configurations the engine detects depend on the session's
variant pairing — chess has four, xiongqi one, pure ōgi none.
## Usage
```toml
[dependencies]
sashite-sanki-engine = "0.10"
```
```rust
use sashite_sanki_engine::domain::half_move::Move;
use sashite_sanki_engine::domain::outcome::Verdict;
use sashite_sanki_engine::engine;
use sashite_sanki_engine::position::Position;
// A position is parsed from its canonical FEEN.
let position = Position::parse("4k^3/8/8/8/8/8/8/R3K^3 / W/w").expect("valid Sanki FEEN");
// Its intrinsic status: no checkmate, stalemate, or dead-position draw here.
assert_eq!(engine::status(&position), Verdict::Ongoing);
// Every legal move for the side to move can be enumerated.
assert!(!engine::legal_moves(&position).is_empty());
// A move is a kind-3423 content array: [from, to, actor].
let mv = Move::parse(r#"["a1","a4",null]"#).expect("valid ply content");
assert!(engine::validate(&position, &mv).is_ok());
// Applying a legal move returns the canonical resulting position.
let next = engine::apply(&position, &mv).expect("legal move");
assert_eq!(next.to_feen(), "4k^3/8/8/8/R7/8/8/4K^3 / w/W");
// `status` also detects terminations — here a back-rank checkmate.
let mated = Position::parse("R6-k^/6pp/8/8/8/8/8/4K^3 / w/W").expect("valid FEEN");
assert!(engine::status(&mated).is_terminated());
// The façade applies the full rule system — ōgi's uchifuzume included:
// a Fu drop that would deliver checkmate is rejected, never applied.
let ogi = Position::parse("7k^/8/5N2/8/8/8/8/4K^1R1 F/ J/j").expect("valid Sanki FEEN");
let mating_drop = Move::parse(r#"[null,"h7","fu"]"#).expect("valid ply content");
assert!(engine::validate(&ogi, &mating_drop).is_err());
```
The four entry points of `engine` are `legal_moves`, `validate`, `apply`, and
`status`. They are pure functions over a `Position`; for clocks, repetition, the
move-limit, and the absolute 300-move cap, drive the `kernel` directly.
The core types above can be brought into scope at once with
`use sashite_sanki_engine::prelude::*;`, which also re-exports the `engine`
module.
## Reading a position beneath the façade
`status` says a position is checkmate. It does not say *which* pieces deliver
it, and some questions turn on exactly that — a double check is two attackers, a
smothered mate is one and it is a Knight. `movement::attack` answers in three
readings of one relation, and none of them needs the kernel:
```rust
use sashite_sanki_engine::domain::side::Side;
use sashite_sanki_engine::domain::square::Square;
use sashite_sanki_engine::movement::attack;
use sashite_sanki_engine::position::Position;
let mated = Position::parse("R6-k^/6pp/8/8/8/8/8/4K^3 / w/W").expect("valid FEEN");
let variant = mated.variant_of(Side::First);
let royal = Square::parse("h8").expect("valid square");
let rook = Square::parse("a8").expect("valid square");
// Is the royal attacked at all? The check test — it stops at the first attacker.
assert!(attack::is_attacked(royal, Side::First, variant, |s| mated.piece_at(s)));
// By which pieces? Here exactly one, and this is the rook that mates.
let checkers = attack::attackers_of(royal, Side::First, variant, |s| mated.piece_at(s));
assert_eq!(checkers, vec![rook]);
// Does one named piece bear on one named square?
assert!(attack::attacks_from(rook, variant, royal, |s| mated.piece_at(s)));
```
The relation ignores the occupant of the target square, so `attacks_from` also
answers "does this piece **defend** that one". The variant parameter is the
variant of the *attacking* side — `variant_of(side)`, never `active_variant()` —
because a foot-soldier's attack pattern is the one geometry its letter does not
settle: a chess Pawn bears on its forward diagonals, an ōgi Fu straight ahead.
## Input formats
- A **position** is a FEEN string (board, hands, and styles + active player),
parsed by `Position::parse`.
- A **move** is the kind-3423 `content`: a three-element array
`[from, to, actor]`, e.g. `["a1","a4",null]` for a board move or
`[null,"h7","fu"]` for an ōgi drop. See the
[Sashité specifications](https://sashite.com/) for the encodings.
## Adjudication
This crate validates and applies moves and reports a position's intrinsic status.
Session-level adjudication — ruling on a game from its public events (plies,
attestations, adjudication requests), ranking termination causes by attestation
time — is provided by the companion crate
[`sashite-sanki-arbiter`](https://github.com/sashite/sanki-arbiter.rs).
## Built on
`sashite-feen`, `sashite-qi`, `sashite-epin`, `sashite-sin`, with `serde`.
## Minimum supported Rust version
Rust 1.81.
## License
Licensed under the [Apache License, Version 2.0](https://github.com/sashite/sanki-engine.rs/blob/main/LICENSE).