sashite-qi 0.1.0

Qi: an immutable, format-agnostic position model for two-player board games (chess, shogi, xiangqi, and beyond).
Documentation
//! Optional `serde` support for [`Qi`].
//!
//! Serialization emits the logical position — shape, board, hands, styles, and
//! turn — with each hand encoded as a sequence of `(piece, count)` pairs rather
//! than a map, so the output is portable across formats that restrict map keys
//! to strings (JSON among them). The cached piece totals are not emitted; they
//! are re-derived on decode.
//!
//! Deserialization rebuilds the position through the validating constructor and
//! transformations, so a decoded [`Qi`] satisfies exactly the same invariants
//! (shape bounds, board/shape agreement, piece cardinality) as one built by
//! hand. A malformed or inconsistent payload is rejected with a serde error.

use alloc::vec::Vec;

use serde::de::Error as _;
use serde::{Deserialize, Deserializer, Serialize, Serializer};

use crate::{Player, Qi};

/// Borrowed wire form, used for serialization (no clones of `P` or `S`).
#[derive(Serialize)]
struct QiOut<'a, P, S> {
    shape: &'a [usize],
    board: &'a [Option<P>],
    first_hand: Vec<(&'a P, usize)>,
    second_hand: Vec<(&'a P, usize)>,
    first_style: &'a S,
    second_style: &'a S,
    turn: Player,
}

/// Owned wire form, used for deserialization.
#[derive(Deserialize)]
struct QiIn<P, S> {
    shape: Vec<usize>,
    board: Vec<Option<P>>,
    first_hand: Vec<(P, usize)>,
    second_hand: Vec<(P, usize)>,
    first_style: S,
    second_style: S,
    turn: Player,
}

impl<P: Serialize, S: Serialize> Serialize for Qi<P, S> {
    fn serialize<Sr>(&self, serializer: Sr) -> Result<Sr::Ok, Sr::Error>
    where
        Sr: Serializer,
    {
        QiOut {
            shape: self.shape(),
            board: self.board(),
            first_hand: self.first_hand().collect(),
            second_hand: self.second_hand().collect(),
            first_style: self.first_style(),
            second_style: self.second_style(),
            turn: self.turn(),
        }
        .serialize(serializer)
    }
}

impl<'de, P, S> Deserialize<'de> for Qi<P, S>
where
    P: Ord + Deserialize<'de>,
    S: Deserialize<'de>,
{
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: Deserializer<'de>,
    {
        let raw = QiIn::<P, S>::deserialize(deserializer)?;

        let qi =
            Qi::new(&raw.shape, raw.first_style, raw.second_style).map_err(D::Error::custom)?;

        if raw.board.len() != qi.square_count() {
            return Err(D::Error::custom(
                "serialized board length does not match the shape",
            ));
        }

        let placements = raw
            .board
            .into_iter()
            .enumerate()
            .filter(|(_, square)| square.is_some());
        let qi = qi.board_diff(placements).map_err(D::Error::custom)?;

        let first = hand_deltas::<D::Error, _>(raw.first_hand)?;
        let second = hand_deltas::<D::Error, _>(raw.second_hand)?;
        let qi = qi.first_hand_diff(first).map_err(D::Error::custom)?;
        let qi = qi.second_hand_diff(second).map_err(D::Error::custom)?;

        Ok(qi.with_turn(raw.turn))
    }
}

/// Converts hand items into the `(piece, delta)` form expected by the hand
/// transformations, rejecting any count too large to be a positive `i32` delta
/// (which would, in any case, exceed the board's capacity).
fn hand_deltas<E, P>(items: Vec<(P, usize)>) -> Result<Vec<(P, i32)>, E>
where
    E: serde::de::Error,
{
    items
        .into_iter()
        .map(|(piece, count)| {
            i32::try_from(count)
                .map(|delta| (piece, delta))
                .map_err(|_| E::custom("hand count is too large"))
        })
        .collect()
}