Skip to content

Repository files navigation

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.

About

Rules engine for the Sanki game suite (chess/ogi/xiongqi on 8×8), built for Sashité.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages