rsshogi 1.0.0

Reusable Rust shogi primitives for board state, move generation, legality, and record parsing.
Documentation
use crate::board::{HuffmanCodedPos, Position};
use crate::types::{AperyMove, GameResult, Move};
use std::fmt;
use std::io::{self, Read, Write};

pub const HCPE_ENTRY_BYTES: usize = 38;

#[repr(i8)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HcpeGameResult {
    Draw = 0,
    BlackWin = 1,
    WhiteWin = 2,
}

impl TryFrom<i8> for HcpeGameResult {
    type Error = HcpeError;

    fn try_from(value: i8) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(Self::Draw),
            1 => Ok(Self::BlackWin),
            2 => Ok(Self::WhiteWin),
            _ => Err(HcpeError::InvalidGameResult(value)),
        }
    }
}

impl TryFrom<GameResult> for HcpeGameResult {
    type Error = HcpeError;

    fn try_from(value: GameResult) -> Result<Self, Self::Error> {
        match value {
            GameResult::BlackWin => Ok(Self::BlackWin),
            GameResult::WhiteWin => Ok(Self::WhiteWin),
            GameResult::DrawByRepetition
            | GameResult::DrawByMaxPlies
            | GameResult::DrawByImpasse => Ok(Self::Draw),
            _ => Err(HcpeError::UnsupportedGameResult(value)),
        }
    }
}

impl fmt::Display for HcpeGameResult {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Draw => write!(f, "Draw"),
            Self::BlackWin => write!(f, "BlackWin"),
            Self::WhiteWin => write!(f, "WhiteWin"),
        }
    }
}

#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct HuffmanCodedPosAndEval {
    pub hcp: HuffmanCodedPos,
    pub eval: i16,
    pub best_move16: AperyMove,
    pub game_result: HcpeGameResult,
    pub dummy: u8,
}

impl HuffmanCodedPosAndEval {
    #[must_use]
    pub const fn new(
        hcp: HuffmanCodedPos,
        eval: i16,
        best_move16: AperyMove,
        game_result: HcpeGameResult,
    ) -> Self {
        Self { hcp, eval, best_move16, game_result, dummy: 0 }
    }

    #[must_use]
    pub fn from_position(
        position: &Position,
        eval: i16,
        best_move: Move,
        game_result: HcpeGameResult,
    ) -> Self {
        Self::new(position.to_huffman_coded_pos(), eval, best_move.to_apery(), game_result)
    }
}

#[derive(Debug)]
pub enum HcpeError {
    InvalidLength(usize),
    InvalidGameResult(i8),
    UnsupportedGameResult(GameResult),
    Io(io::Error),
}

impl fmt::Display for HcpeError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidLength(len) => write!(f, "invalid hcpe length: {len}"),
            Self::InvalidGameResult(value) => write!(f, "invalid hcpe game result: {value}"),
            Self::UnsupportedGameResult(value) => {
                write!(f, "unsupported GameResult for hcpe export: {value:?}")
            }
            Self::Io(err) => write!(f, "{err}"),
        }
    }
}

impl std::error::Error for HcpeError {}

impl From<io::Error> for HcpeError {
    fn from(value: io::Error) -> Self {
        Self::Io(value)
    }
}

#[must_use]
pub fn encode_entry(entry: &HuffmanCodedPosAndEval) -> [u8; HCPE_ENTRY_BYTES] {
    let mut out = [0u8; HCPE_ENTRY_BYTES];
    out[..32].copy_from_slice(&entry.hcp.data);
    out[32..34].copy_from_slice(&entry.eval.to_le_bytes());
    out[34..36].copy_from_slice(&entry.best_move16.raw().to_le_bytes());
    out[36] = entry.game_result as i8 as u8;
    out[37] = entry.dummy;
    out
}

pub fn decode_entry(bytes: &[u8]) -> Result<HuffmanCodedPosAndEval, HcpeError> {
    if bytes.len() != HCPE_ENTRY_BYTES {
        return Err(HcpeError::InvalidLength(bytes.len()));
    }
    let mut hcp = [0u8; 32];
    hcp.copy_from_slice(&bytes[..32]);
    let eval = i16::from_le_bytes([bytes[32], bytes[33]]);
    let best_move16 = AperyMove::from_raw(u16::from_le_bytes([bytes[34], bytes[35]]));
    let game_result = HcpeGameResult::try_from(bytes[36] as i8)?;
    Ok(HuffmanCodedPosAndEval {
        hcp: HuffmanCodedPos { data: hcp },
        eval,
        best_move16,
        game_result,
        dummy: bytes[37],
    })
}

pub fn decode_entries<R: Read>(reader: &mut R) -> Result<Vec<HuffmanCodedPosAndEval>, HcpeError> {
    let mut entries = Vec::new();
    loop {
        let mut buf = [0u8; HCPE_ENTRY_BYTES];
        let mut read = 0usize;
        while read < HCPE_ENTRY_BYTES {
            match reader.read(&mut buf[read..])? {
                0 if read == 0 => return Ok(entries),
                0 => return Err(HcpeError::InvalidLength(read)),
                n => read += n,
            }
        }
        entries.push(decode_entry(&buf)?);
    }
}

pub fn write_entries<W: Write>(
    writer: &mut W,
    entries: &[HuffmanCodedPosAndEval],
) -> Result<(), HcpeError> {
    for entry in entries {
        writer.write_all(&encode_entry(entry))?;
    }
    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::board;
    use crate::types::Move;
    use std::mem::size_of;

    #[test]
    fn test_hcpe_layout_and_roundtrip() {
        assert_eq!(size_of::<HuffmanCodedPosAndEval>(), HCPE_ENTRY_BYTES);

        board::init();
        let position = board::hirate_position();
        let entry = HuffmanCodedPosAndEval::from_position(
            &position,
            123,
            Move::from_usi("7g7f").unwrap(),
            HcpeGameResult::BlackWin,
        );
        let encoded = encode_entry(&entry);
        let decoded = decode_entry(&encoded).unwrap();
        assert_eq!(decoded, entry);
    }

    #[test]
    fn test_hcpe_io_helpers_roundtrip() {
        board::init();
        let position = board::hirate_position();
        let entries = vec![
            HuffmanCodedPosAndEval::from_position(
                &position,
                1,
                Move::from_usi("7g7f").unwrap(),
                HcpeGameResult::Draw,
            ),
            HuffmanCodedPosAndEval::from_position(
                &position,
                -2,
                Move::from_usi("2g2f").unwrap(),
                HcpeGameResult::WhiteWin,
            ),
        ];
        let mut bytes = Vec::new();
        write_entries(&mut bytes, &entries).unwrap();
        let decoded = decode_entries(&mut bytes.as_slice()).unwrap();
        assert_eq!(decoded, entries);
    }

    #[test]
    fn test_hcpe_game_result_rejects_unsupported_values() {
        assert!(HcpeGameResult::try_from(GameResult::BlackWinByDeclaration).is_err());
        let mut bytes = [0u8; HCPE_ENTRY_BYTES];
        bytes[36] = 9;
        assert!(decode_entry(&bytes).is_err());
    }
}