sashite-qi 0.1.0

Qi: an immutable, format-agnostic position model for two-player board games (chess, shogi, xiangqi, and beyond).
Documentation
//! A small end-to-end walkthrough of the `Qi` position model: build a board,
//! place pieces, make a capturing move, and inspect the result.
//!
//! `Qi` is rule-agnostic — it does not judge whether a move is legal; it only
//! records what a position is. Run with:
//!
//! ```sh
//! cargo run --example basic
//! ```

use sashite_qi::{Player, Qi};

fn main() -> Result<(), sashite_qi::Error> {
    // An 8×8 board. The two styles here are plain string tags; any type works.
    // Pieces are strings too: uppercase for the first player, lowercase for the
    // second. Squares use flat, row-major indices (index = rank * files + file).
    let start = Qi::new(&[8, 8], "C", "c")?.board_diff([
        (4, Some("K")),  // first player's king   (rank 0, file 4)
        (0, Some("R")),  // first player's rook    (rank 0, file 0)
        (60, Some("k")), // second player's king   (rank 7, file 4)
        (59, Some("q")), // second player's queen  (rank 7, file 3)
    ])?;

    println!("Initial position ({} to move):", side(start.turn()));
    print_board(&start);
    println!("pieces on board: {}\n", start.piece_count());

    // The first player captures the queen on square 59 with the rook: vacate the
    // source (0), overwrite the destination (59), pocket the captured piece in
    // the first player's hand, then pass the turn.
    let after = start
        .board_diff([(0, None), (59, Some("R"))])?
        .first_hand_diff([("q", 1)])?
        .toggle();

    println!("After the capture on 59 ({} to move):", side(after.turn()));
    print_board(&after);
    println!("pieces on board: {}", after.board_piece_count());
    println!(
        "first player's hand holds {} piece(s):",
        after.hand_piece_count()
    );
    for (piece, count) in after.first_hand() {
        println!("  {count} × {piece}");
    }

    Ok(())
}

/// Renders a 2D board rank by rank (`.` marks an empty square).
fn print_board(pos: &Qi<&str, &str>) {
    let files = pos.shape()[1];
    for (index, square) in pos.board().iter().enumerate() {
        match square {
            Some(piece) => print!(" {piece}"),
            None => print!(" ."),
        }
        if (index + 1) % files == 0 {
            println!();
        }
    }
}

/// A human-readable label for the active player.
fn side(player: Player) -> &'static str {
    match player {
        Player::First => "first player",
        Player::Second => "second player",
    }
}