Skip to main content

Crate sashite_sanki_engine

Crate sashite_sanki_engine 

Source
Expand description

§Sashité Sanki Engine

Crates.io Docs.rs License

Rules engine for the Sanki game suite — chess, ogi, and xiongqi, all played on an 8×8 board — built for Sashité. 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, 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

[dependencies]
sashite-sanki-engine = "0.10"
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:

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 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.

§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.

Modules§

apply
Applying the visible mutations of a move already judged legal: the move itself, Rook relocation (castling), removing the victim (ordinary capture or en passant), crediting the capture to hand, placing the promoted piece, dropping from hand, and passing the turn to the opponent.
canonicalize
Canonicalization: recomposes the transient board markers on the position produced by a ply, so that the FEEN the kernel returns is canonical.
capture
Capture transformation — capture_transform.
clock
Per-ply time accounting: spend the elapsed time on the mover’s clock, apply the increment, advance or repeat quota periods, and detect time-out.
domain
Domain value types — pure, with no rules logic.
engine
Clean public API for the Sanki rules engine.
error
Boundary errors.
kernel
L1 — pure per-ply kernel: step(state, ply, attestation_at) -> Outcome. legality -> apply -> canonicalize -> tick clock -> terminal. ALWAYS returns an Outcome (illegality encoded, never an Err).
legality
Legality: pseudo-legal -> legal, special moves, drop restrictions.
movement
Movement components and dispatch (variant, letter) → rule.
position
Position model: a wrapper around sashite_qi::Qi<Epin, Sin>.
prelude
Convenience re-exports of the most commonly used items.
terminal
Terminations derived from the position (and history).