sashite-qi 0.1.0

Qi: an immutable, format-agnostic position model for two-player board games (chess, shogi, xiangqi, and beyond).
Documentation
//! Board and hand transformations, turn handling, and generic piece types.

use sashite_qi::{Error, Player, Qi};

#[test]
fn place_and_clear_pieces() {
    let pos = Qi::new(&[8, 8], "C", "c")
        .unwrap()
        .board_diff([
            (4, Some("K")),
            (60, Some("k")),
            (0, Some("R")),
            (63, Some("r")),
        ])
        .unwrap();
    assert_eq!(pos.piece_count(), 4);
    assert_eq!(pos.board_piece_count(), 4);
    assert_eq!(pos.square(4), Some(&"K"));
    assert_eq!(pos.square(60), Some(&"k"));
    assert_eq!(pos.square(1), None);

    // Clearing and overwriting update the count correctly.
    let pos2 = pos.board_diff([(4, None), (0, Some("Q"))]).unwrap();
    assert_eq!(pos2.piece_count(), 3);
    assert_eq!(pos2.square(4), None);
    assert_eq!(pos2.square(0), Some(&"Q"));
}

#[test]
fn duplicate_index_in_one_diff() {
    let pos = Qi::new(&[2], "C", "c")
        .unwrap()
        .board_diff([(0, Some("a")), (0, None)]) // add then clear the same square
        .unwrap();
    assert_eq!(pos.piece_count(), 0);
    assert_eq!(pos.square(0), None);
}

#[test]
fn board_index_out_of_range() {
    let res = Qi::new(&[2, 2], "C", "c")
        .unwrap()
        .board_diff([(4, Some("x"))]);
    assert_eq!(res, Err(Error::IndexOutOfRange));
}

#[test]
fn board_too_many_pieces() {
    let res = Qi::new(&[2], "C", "c")
        .unwrap()
        .board_diff([(0, Some("a")), (1, Some("b"))]) // 2 pieces on 2 squares — OK
        .and_then(|p| p.first_hand_diff([("c", 1)])); // a 3rd piece on 2 squares
    assert_eq!(res, Err(Error::TooManyPieces));
}

#[test]
fn hand_add_remove_and_counts() {
    let pos = Qi::new(&[8, 8], "C", "c")
        .unwrap()
        .first_hand_diff([("P", 2), ("B", 1)])
        .unwrap()
        .first_hand_diff([("B", -1), ("P", 1)]) // remove the B, add a P
        .unwrap();
    assert_eq!(pos.first_hand_count(&"P"), 3);
    assert_eq!(pos.first_hand_count(&"B"), 0); // removed entirely
    assert_eq!(pos.hand_piece_count(), 3);
    assert_eq!(pos.piece_count(), 3);

    let items: Vec<(&str, usize)> = pos.first_hand().map(|(p, c)| (*p, c)).collect();
    assert_eq!(items, vec![("P", 3)]); // B is gone
}

#[test]
fn hand_underflow() {
    let res = Qi::new(&[8, 8], "C", "c")
        .unwrap()
        .first_hand_diff([("P", -1)]);
    assert_eq!(res, Err(Error::HandUnderflow));

    let res2 = Qi::new(&[8, 8], "C", "c")
        .unwrap()
        .first_hand_diff([("P", 2)])
        .and_then(|p| p.first_hand_diff([("P", -3)]));
    assert_eq!(res2, Err(Error::HandUnderflow));
}

#[test]
fn hand_zero_delta_is_noop() {
    let pos = Qi::new(&[8, 8], "C", "c")
        .unwrap()
        .first_hand_diff([("P", 0)])
        .unwrap();
    assert_eq!(pos.first_hand_count(&"P"), 0);
    assert_eq!(pos.hand_piece_count(), 0);
}

#[test]
fn second_hand_is_independent() {
    let pos = Qi::new(&[8, 8], "C", "c")
        .unwrap()
        .first_hand_diff([("P", 1)])
        .unwrap()
        .second_hand_diff([("p", 2)])
        .unwrap();
    assert_eq!(pos.first_hand_count(&"P"), 1);
    assert_eq!(pos.second_hand_count(&"p"), 2);
    assert_eq!(pos.first_hand_count(&"p"), 0);
    assert_eq!(pos.hand_piece_count(), 3);
}

#[test]
fn toggle_and_with_turn() {
    let pos: Qi<&str, &str> = Qi::new(&[8, 8], "C", "c").unwrap();
    assert_eq!(pos.clone().toggle().turn(), Player::Second);
    assert_eq!(pos.clone().toggle().toggle().turn(), Player::First);
    assert_eq!(pos.with_turn(Player::Second).turn(), Player::Second);
}

#[test]
fn works_with_integer_pieces() {
    let pos = Qi::new(&[4], 1u8, 2u8)
        .unwrap()
        .board_diff([(0, Some(42u32)), (3, Some(7u32))])
        .unwrap();
    assert_eq!(pos.piece_count(), 2);
    assert_eq!(pos.square(0), Some(&42));
    assert_eq!(pos.first_style(), &1u8);
}