sashite-qi 0.1.0

Qi: an immutable, format-agnostic position model for two-player board games (chess, shogi, xiangqi, and beyond).
Documentation
# sashite-qi

[![Crates.io](https://img.shields.io/crates/v/sashite-qi.svg)](https://crates.io/crates/sashite-qi)
[![Docs.rs](https://docs.rs/sashite-qi/badge.svg)](https://docs.rs/sashite-qi)
[![CI](https://github.com/sashite/qi.rs/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/sashite/qi.rs/actions/workflows/ci.yml)
[![License](https://img.shields.io/crates/l/sashite-qi.svg)](https://github.com/sashite/qi.rs/blob/main/LICENSE)

> An immutable, format-agnostic position model for two-player board games.

## Overview

`Qi` models a board-game **position** as defined by the
[Sashité Game Protocol](https://sashite.dev/game-protocol/). A position encodes
exactly four things:

| Component | Field(s)                          | Description                                            |
|-----------|-----------------------------------|-------------------------------------------------------|
| Board     | `board`                           | Flat slice of squares, indexed in row-major order     |
| Hands     | `first_hand`, `second_hand`       | Off-board pieces held by each player, as `piece → count` |
| Styles    | `first_style`, `second_style`     | One style value per player side                       |
| Turn      | `turn`                            | The active player (`First` or `Second`)               |

The type is **generic over the piece type `P` and the style type `S`**, so it is
independent of any notation. You choose how a piece and a style are represented —
a typed identifier, an interned id, a `&str`, a `String`, … Empty squares are
`None`.

This generality is what lets the Sashité notation crates build on it: the
[FEEN](https://sashite.dev/specs/feen/1.0.0/) position format plugs in
[EPIN](https://sashite.dev/specs/epin/1.0.0/) pieces and
[SIN](https://sashite.dev/specs/sin/1.0.0/) styles, with no string round-tripping
and no loss of type information.

## Quick Start

```rust
use sashite_qi::{Player, Qi};

// An empty 8×8 board. "C" / "c" are style identifiers (here: Chess for each side).
let position = Qi::new(&[8, 8], "C", "c")?
    .board_diff([(4, Some("K")), (60, Some("k"))])? // kings
    .board_diff([(0, Some("R")), (63, Some("r"))])? // rooks in the corners
    .toggle();                                      // hand over to the second player

assert_eq!(position.turn(), Player::Second);
assert_eq!(position.square(4), Some(&"K"));
assert_eq!(position.square(60), Some(&"k"));
assert_eq!(position.piece_count(), 4);
# Ok::<(), sashite_qi::Error>(())
```

Each transformation **consumes the position and returns a new one**, moving — not
copying — the storage. To keep a previous state, `clone()` it explicitly.

## Installation

```sh
cargo add sashite-qi
```

Or add it to `Cargo.toml`:

```toml
[dependencies]
sashite-qi = "0.1"
```

`sashite-qi` is `no_std` (it links only `core` and `alloc`), forbids `unsafe`,
and has **no required dependencies**. The minimum supported Rust version is
**1.81**.

## Cargo features

- **`serde`** *(off by default)* — implements `Serialize` / `Deserialize` for
  `Qi<P, S>` (requires `P` and `S` to be (de)serializable). The wire form is the
  logical position — shape, board, hands, styles, turn — with each hand written
  as a sequence of `(piece, count)` pairs, so the output is portable across
  formats that restrict map keys to strings (JSON included). Decoding rebuilds
  the position through the validating constructor, so a deserialized `Qi` upholds
  the same invariants as one built by hand. Enabling the feature keeps the crate
  `no_std`.

```toml
[dependencies]
sashite-qi = { version = "0.1", features = ["serde"] }
```

## Usage

### Construction

```rust
use sashite_qi::Qi;

let _2d = Qi::new(&[8, 8], "C", "c")?;     // 8×8
let _1d = Qi::new(&[8], "G", "g")?;        // 1D
let _3d = Qi::new(&[5, 5, 5], "R", "r")?;  // 3D
# Ok::<(), sashite_qi::Error>(())
```

`new` starts every square empty (`None`), both hands empty, and the first player
to move. It validates the shape only (the piece and style types are checked by the
compiler, so there are no string-length or nil checks).

### Placing and clearing pieces

`board_diff` addresses squares by **flat index** (see [Board structure](#board-structure)).
Each change is `(index, Some(piece))` or `(index, None)`.

```rust
use sashite_qi::Qi;

let position = Qi::new(&[3, 3], "C", "c")?
    .board_diff([(4, Some("K"))])?  // place a king on the centre square
    .board_diff([(4, None), (0, Some("Q"))])?; // move it off, place a queen on a1

assert_eq!(position.square(0), Some(&"Q"));
assert_eq!(position.square(4), None);
assert_eq!(position.piece_count(), 1);
# Ok::<(), sashite_qi::Error>(())
```

Piece counts are tracked incrementally, so a diff costs time proportional to the
number of changes, not to the board size.

### Hands

Hands are `piece → count` maps. Each change is `(piece, delta)`: a positive delta
adds copies, a negative delta removes them, zero is a no-op.

```rust
use sashite_qi::Qi;

let position = Qi::new(&[8, 8], "C", "c")?
    .first_hand_diff([("P", 2), ("B", 1)])?
    .first_hand_diff([("B", -1), ("P", 1)])?; // remove a B, add a P

assert_eq!(position.first_hand_count(&"P"), 3);
assert_eq!(position.first_hand_count(&"B"), 0); // removed entirely
assert_eq!(position.hand_piece_count(), 3);
# Ok::<(), sashite_qi::Error>(())
```

### A move, end to end

The protocol does not prescribe how captures are modelled. `board_diff` does not
track what was previously on a square, so a captured piece is added to a hand
separately.

```rust
use sashite_qi::Qi;

let start = Qi::new(&[8, 8], "C", "c")?
    .board_diff([(12, Some("P")), (28, Some("p"))])?;

// First player captures on square 28 and slides their pawn there.
let after = start
    .board_diff([(28, Some("P")), (12, None)])? // overwrite defender, vacate source
    .first_hand_diff([("p", 1)])?               // pocket the captured piece
    .toggle();                                  // hand over the turn
# Ok::<(), sashite_qi::Error>(())
```

### Accessors

| Method                         | Returns                            | Description                                     |
|--------------------------------|------------------------------------|-------------------------------------------------|
| `shape()`                      | `&[usize]`                         | Dimension sizes, outermost first                |
| `dimension_count()`            | `usize`                            | Number of dimensions                            |
| `square_count()`               | `usize`                            | Total squares on the board                      |
| `piece_count()`                | `usize`                            | Pieces on the board plus both hands             |
| `board_piece_count()`          | `usize`                            | Pieces on the board                             |
| `hand_piece_count()`           | `usize`                            | Pieces across both hands                        |
| `turn()`                       | `Player`                           | The active player                               |
| `first_style()` / `second_style()` | `&S`                           | Each player's style                             |
| `board()`                      | `&[Option<P>]`                     | The board as a flat slice                       |
| `square(index)`                | `Option<&P>`                       | The piece at an index (`None` if empty/invalid) |
| `first_hand()` / `second_hand()` | `impl Iterator<Item = (&P, usize)>` | Hand items in key order                        |
| `first_hand_count(&P)` / `second_hand_count(&P)` | `usize`          | Copies of a piece held (0 if absent)            |

### Constants

| Constant                | Value    | Description                          |
|-------------------------|----------|--------------------------------------|
| `MAX_DIMENSIONS`        | `3`      | Maximum number of board dimensions   |
| `MAX_DIMENSION_SIZE`    | `255`    | Maximum size of any single dimension |
| `MAX_SQUARE_COUNT`      | `65_025` | Maximum total squares (`255 × 255`)  |

## Board structure

### Shape and dimensionality

`shape()` returns the dimension sizes, outermost first. The number of squares is
their product, which must not exceed `MAX_SQUARE_COUNT`.

| Dimensionality | Constructor             | `shape()`     |
|----------------|-------------------------|---------------|
| 1D             | `Qi::new(&[8], …)`      | `[8]`         |
| 2D             | `Qi::new(&[8, 8], …)`   | `[8, 8]`      |
| 3D             | `Qi::new(&[5, 5, 5], …)`| `[5, 5, 5]`   |

The total-square cap is independent of dimensionality: 3D boards are fully
supported as long as the product stays within the limit (e.g. `40×40×40 = 64_000`
is fine; `255×255×2` is not).

### Flat indexing

Squares are addressed by a single integer in **row-major order**.

**1D** with shape `[f]`: `index = file`.

**2D** with shape `[r, f]` (`r` ranks, `f` files): `index = rank × f + file`.

For a 3×3 board (shape `[3, 3]`):

```text
             file
           0   1   2
        ┌────┬────┬────┐
rank 0  │  0 │  1 │  2 │
        ├────┼────┼────┤
rank 1  │  3 │  4 │  5 │
        ├────┼────┼────┤
rank 2  │  6 │  7 │  8 │
        └────┴────┴────┘
```

Square `(rank = 1, file = 2)` → index `1 × 3 + 2 = 5`.

**3D** with shape `[l, r, f]` (`l` layers, `r` ranks, `f` files):
`index = layer × (r × f) + rank × f + file`.

### Piece cardinality

The total number of pieces — board squares plus both hands — must never exceed the
number of squares. For a board of *n* squares and *p* pieces: **0 ≤ p ≤ n**. This
invariant is checked on every transformation.

```rust
use sashite_qi::{Error, Qi};

let full = Qi::new(&[2], "C", "c")?
    .board_diff([(0, Some("a")), (1, Some("b"))])?; // 2 pieces on 2 squares: OK

// A third piece (here in hand) would exceed the board.
assert_eq!(full.first_hand_diff([("c", 1)]), Err(Error::TooManyPieces));
# Ok::<(), sashite_qi::Error>(())
```

## Errors

All fallible operations return [`Error`]:

| Variant             | Cause                                                         |
|---------------------|--------------------------------------------------------------|
| `EmptyShape`        | The shape had no dimensions                                  |
| `TooManyDimensions` | More than `MAX_DIMENSIONS` dimensions                        |
| `DimensionTooSmall` | A dimension of size 0                                        |
| `DimensionTooLarge` | A dimension larger than `MAX_DIMENSION_SIZE`                 |
| `TooManySquares`    | The product of the dimensions exceeds `MAX_SQUARE_COUNT`     |
| `IndexOutOfRange`   | A `board_diff` index is not a square on the board            |
| `HandUnderflow`     | Removing more copies of a piece than are held                |
| `TooManyPieces`     | The piece count would exceed the square count               |

Construction validates the shape in order: dimension count, then each dimension
size, then the total square count.

## Generic over pieces and styles

`P` (piece) and `S` (style) are type parameters. The only trait bound is `P: Ord`,
required by the two hand-diff methods and the hand-count lookups (hands are
ordered maps); construction, `board_diff`, `toggle`, and the board accessors place
no bound on `P` or `S`. `Debug`, `Clone`, `PartialEq`, `Eq`, and `Hash` are derived
conditionally, so a `Qi` is comparable and hashable — usable as a position-identity
key — whenever its `P` and `S` are.

Any type works as a piece or style:

```rust
use sashite_qi::Qi;

// Integer pieces, byte styles.
let position = Qi::new(&[4], 1u8, 2u8)?
    .board_diff([(0, Some(42u32)), (3, Some(7u32))])?;

assert_eq!(position.square(0), Some(&42));
assert_eq!(position.first_style(), &1u8);
# Ok::<(), sashite_qi::Error>(())
```

One ergonomic note: because `new` does not take a piece argument, the piece type
`P` cannot be inferred for a position that is never given a piece. In that case,
annotate it (`let position: Qi<&str, &str> = Qi::new(&[8, 8], "C", "c")?;`). In
normal use the first `board_diff` fixes `P`.

## Design

- **Immutable, move-based.** Transformations consume the position and return a new
  one, giving value semantics with zero-copy moves. Clone for a snapshot. A `Qi`
  is safe to use as a map key, cache entry, or history record.
- **Bounded by construction.** Dimensions, dimension sizes, total squares, and the
  piece count are all bounded and checked, so a `Qi` is safe to build from
  untrusted input with no extra sanitization.
- **Performance-oriented internals.** The board is a flat `Vec` for O(1) indexed
  access; hands are `piece → count` maps; piece totals are maintained
  incrementally, so a diff costs O(changes), not O(board size).
- **`no_std`, no `unsafe`, no required dependencies.** Only `core` and `alloc`;
  built under a forbid-`unsafe` lint policy; `serde` is an optional add-on.

## Ecosystem

`Qi` is the positional core of the [Sashité](https://sashite.dev/) ecosystem. It
models *what a position is* (board, hands, styles, turn) without prescribing *how
positions are serialized* or *what moves are legal*:

- [FEEN]https://sashite.dev/specs/feen/1.0.0/ — a canonical string encoding for positions
- [EPIN]https://sashite.dev/specs/epin/1.0.0/ — piece token syntax
- [SIN]https://sashite.dev/specs/sin/1.0.0/ — style token syntax
- [Game Protocol]https://sashite.dev/game-protocol/ — the shared conceptual foundation

## License

Available as open source under the terms of the
[Apache License 2.0](https://opensource.org/licenses/Apache-2.0).