rsshogi 1.0.2

Reusable Rust shogi primitives for board state, move generation, legality, and record parsing.
Documentation
use super::bit_io::{BitCursorOverflow, BitReader, BitWriter, bytes_to_words, words_to_bytes};
use super::{Ply, Position};
use crate::board::parser::generate_sfen;
use crate::board::position::BoardArray;
use crate::types::{Color, EnteringKingRule, Hand, HandPiece, Piece, PieceType, Square};
use std::fmt;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct HuffmanCodedPos {
    pub data: [u8; 32],
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HuffmanCodedPosError {
    InvalidCursor,
    InvalidPieceCode,
    InvalidKingSquare(u16),
}

impl fmt::Display for HuffmanCodedPosError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::InvalidCursor => write!(f, "invalid huffman cursor"),
            Self::InvalidPieceCode => write!(f, "invalid huffman piece code"),
            Self::InvalidKingSquare(sq) => {
                write!(f, "invalid king square in HuffmanCodedPos: {sq}")
            }
        }
    }
}

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

impl From<BitCursorOverflow> for HuffmanCodedPosError {
    fn from(_: BitCursorOverflow) -> Self {
        Self::InvalidCursor
    }
}

#[derive(Clone, Copy)]
struct HuffmanCode {
    code: u8,
    bits: u8,
}

#[derive(Clone, Copy)]
struct PieceCode {
    piece: Piece,
    code: HuffmanCode,
}

#[derive(Clone, Copy)]
struct HandCode {
    piece: Option<Piece>,
    code: HuffmanCode,
}

const BOARD_CODES: [PieceCode; 29] = [
    PieceCode { piece: Piece::NONE, code: HuffmanCode { code: 0x00, bits: 1 } },
    PieceCode { piece: Piece::B_PAWN, code: HuffmanCode { code: 0x01, bits: 4 } },
    PieceCode { piece: Piece::B_LANCE, code: HuffmanCode { code: 0x03, bits: 6 } },
    PieceCode { piece: Piece::B_KNIGHT, code: HuffmanCode { code: 0x07, bits: 6 } },
    PieceCode { piece: Piece::B_SILVER, code: HuffmanCode { code: 0x0b, bits: 6 } },
    PieceCode { piece: Piece::B_BISHOP, code: HuffmanCode { code: 0x1f, bits: 8 } },
    PieceCode { piece: Piece::B_ROOK, code: HuffmanCode { code: 0x3f, bits: 8 } },
    PieceCode { piece: Piece::B_GOLD, code: HuffmanCode { code: 0x0f, bits: 6 } },
    PieceCode { piece: Piece::B_PRO_PAWN, code: HuffmanCode { code: 0x09, bits: 4 } },
    PieceCode { piece: Piece::B_PRO_LANCE, code: HuffmanCode { code: 0x23, bits: 6 } },
    PieceCode { piece: Piece::B_PRO_KNIGHT, code: HuffmanCode { code: 0x27, bits: 6 } },
    PieceCode { piece: Piece::B_PRO_SILVER, code: HuffmanCode { code: 0x2b, bits: 6 } },
    PieceCode { piece: Piece::B_HORSE, code: HuffmanCode { code: 0x9f, bits: 8 } },
    PieceCode { piece: Piece::B_DRAGON, code: HuffmanCode { code: 0xbf, bits: 8 } },
    PieceCode { piece: Piece::W_PAWN, code: HuffmanCode { code: 0x05, bits: 4 } },
    PieceCode { piece: Piece::W_LANCE, code: HuffmanCode { code: 0x13, bits: 6 } },
    PieceCode { piece: Piece::W_KNIGHT, code: HuffmanCode { code: 0x17, bits: 6 } },
    PieceCode { piece: Piece::W_SILVER, code: HuffmanCode { code: 0x1b, bits: 6 } },
    PieceCode { piece: Piece::W_BISHOP, code: HuffmanCode { code: 0x5f, bits: 8 } },
    PieceCode { piece: Piece::W_ROOK, code: HuffmanCode { code: 0x7f, bits: 8 } },
    PieceCode { piece: Piece::W_GOLD, code: HuffmanCode { code: 0x2f, bits: 6 } },
    PieceCode { piece: Piece::W_PRO_PAWN, code: HuffmanCode { code: 0x0d, bits: 4 } },
    PieceCode { piece: Piece::W_PRO_LANCE, code: HuffmanCode { code: 0x33, bits: 6 } },
    PieceCode { piece: Piece::W_PRO_KNIGHT, code: HuffmanCode { code: 0x37, bits: 6 } },
    PieceCode { piece: Piece::W_PRO_SILVER, code: HuffmanCode { code: 0x3b, bits: 6 } },
    PieceCode { piece: Piece::W_HORSE, code: HuffmanCode { code: 0xdf, bits: 8 } },
    PieceCode { piece: Piece::W_DRAGON, code: HuffmanCode { code: 0xff, bits: 8 } },
    PieceCode { piece: Piece::B_KING, code: HuffmanCode { code: 0x00, bits: 0 } },
    PieceCode { piece: Piece::W_KING, code: HuffmanCode { code: 0x00, bits: 0 } },
];

const HAND_CODES: [HandCode; 21] = [
    HandCode { piece: Some(Piece::B_PAWN), code: HuffmanCode { code: 0x00, bits: 3 } },
    HandCode { piece: Some(Piece::W_PAWN), code: HuffmanCode { code: 0x04, bits: 3 } },
    HandCode { piece: Some(Piece::B_LANCE), code: HuffmanCode { code: 0x01, bits: 5 } },
    HandCode { piece: Some(Piece::W_LANCE), code: HuffmanCode { code: 0x11, bits: 5 } },
    HandCode { piece: Some(Piece::B_KNIGHT), code: HuffmanCode { code: 0x03, bits: 5 } },
    HandCode { piece: Some(Piece::W_KNIGHT), code: HuffmanCode { code: 0x13, bits: 5 } },
    HandCode { piece: Some(Piece::B_SILVER), code: HuffmanCode { code: 0x05, bits: 5 } },
    HandCode { piece: Some(Piece::W_SILVER), code: HuffmanCode { code: 0x15, bits: 5 } },
    HandCode { piece: Some(Piece::B_GOLD), code: HuffmanCode { code: 0x07, bits: 5 } },
    HandCode { piece: Some(Piece::W_GOLD), code: HuffmanCode { code: 0x17, bits: 5 } },
    HandCode { piece: Some(Piece::B_BISHOP), code: HuffmanCode { code: 0x1f, bits: 7 } },
    HandCode { piece: Some(Piece::W_BISHOP), code: HuffmanCode { code: 0x5f, bits: 7 } },
    HandCode { piece: Some(Piece::B_ROOK), code: HuffmanCode { code: 0x3f, bits: 7 } },
    HandCode { piece: Some(Piece::W_ROOK), code: HuffmanCode { code: 0x7f, bits: 7 } },
    HandCode { piece: None, code: HuffmanCode { code: 0x02, bits: 3 } },
    HandCode { piece: None, code: HuffmanCode { code: 0x09, bits: 5 } },
    HandCode { piece: None, code: HuffmanCode { code: 0x0b, bits: 5 } },
    HandCode { piece: None, code: HuffmanCode { code: 0x0d, bits: 5 } },
    HandCode { piece: None, code: HuffmanCode { code: 0x1d, bits: 5 } },
    HandCode { piece: None, code: HuffmanCode { code: 0x0f, bits: 7 } },
    HandCode { piece: None, code: HuffmanCode { code: 0x2f, bits: 7 } },
];

const PIECEBOX_CODES: [(PieceType, HuffmanCode); 7] = [
    (PieceType::PAWN, HuffmanCode { code: 0x02, bits: 3 }),
    (PieceType::LANCE, HuffmanCode { code: 0x09, bits: 5 }),
    (PieceType::KNIGHT, HuffmanCode { code: 0x0b, bits: 5 }),
    (PieceType::SILVER, HuffmanCode { code: 0x0d, bits: 5 }),
    (PieceType::GOLD, HuffmanCode { code: 0x1d, bits: 5 }),
    (PieceType::BISHOP, HuffmanCode { code: 0x0f, bits: 7 }),
    (PieceType::ROOK, HuffmanCode { code: 0x2f, bits: 7 }),
];

const HAND_PIECE_ORDER: [PieceType; 7] = [
    PieceType::PAWN,
    PieceType::LANCE,
    PieceType::KNIGHT,
    PieceType::SILVER,
    PieceType::GOLD,
    PieceType::BISHOP,
    PieceType::ROOK,
];

fn piecebox_index(piece_type: PieceType) -> usize {
    match piece_type {
        PieceType::PAWN => 0,
        PieceType::LANCE => 1,
        PieceType::KNIGHT => 2,
        PieceType::SILVER => 3,
        PieceType::GOLD => 4,
        PieceType::BISHOP => 5,
        PieceType::ROOK => 6,
        _ => unreachable!("piecebox_index only accepts hand pieces"),
    }
}

fn board_code_for_piece(piece: Piece) -> HuffmanCode {
    BOARD_CODES
        .iter()
        .find(|entry| entry.piece == piece)
        .map(|entry| entry.code)
        .expect("piece must be encodable in HuffmanCodedPos")
}

fn hand_code_for_piece(piece: Piece) -> HuffmanCode {
    HAND_CODES
        .iter()
        .find(|entry| entry.piece == Some(piece))
        .map(|entry| entry.code)
        .expect("hand piece must be encodable in HuffmanCodedPos")
}

fn piecebox_code_for_piece_type(piece_type: PieceType) -> HuffmanCode {
    PIECEBOX_CODES
        .iter()
        .find(|(candidate, _)| *candidate == piece_type)
        .map(|(_, code)| *code)
        .expect("piecebox piece must be encodable in HuffmanCodedPos")
}

impl Position {
    #[must_use]
    pub fn to_huffman_coded_pos(&self) -> HuffmanCodedPos {
        let mut words = [0u64; 4];
        let mut writer = BitWriter::new(&mut words);

        writer.write_one_bit(self.turn().raw() != 0);

        for color in [Color::BLACK, Color::WHITE] {
            let king_sq = self.king_square(color);
            let king_idx = u16::try_from(king_sq.raw()).expect("square fits in u16");
            writer.write_n_bits(king_idx, 7);
        }

        let mut piecebox_count = [18, 4, 4, 4, 4, 2, 2];

        for sq_idx in 0..Square::COUNT {
            let sq = Square::from_index(sq_idx);
            let piece = self.piece_on(sq);
            if piece.piece_type() == PieceType::KING {
                continue;
            }
            let code = board_code_for_piece(piece);
            writer.write_n_bits(u16::from(code.code), code.bits);
            if piece != Piece::NONE {
                let base = piece.piece_type().demote();
                let idx = piecebox_index(base);
                piecebox_count[idx] -= 1;
            }
        }

        for color in [Color::BLACK, Color::WHITE] {
            for piece_type in HAND_PIECE_ORDER {
                let hand_piece =
                    HandPiece::from_piece_type(piece_type).expect("hand piece must exist");
                let count = self.hand(color).count(hand_piece);
                for _ in 0..count {
                    let piece = Piece::from_parts(color, piece_type);
                    let code = hand_code_for_piece(piece);
                    writer.write_n_bits(u16::from(code.code), code.bits);
                }
                let idx = piecebox_index(piece_type);
                piecebox_count[idx] -= i32::try_from(count).expect("hand count fits in i32");
            }
        }

        for piece_type in HAND_PIECE_ORDER {
            let count = piecebox_count[piecebox_index(piece_type)];
            let code = piecebox_code_for_piece_type(piece_type);
            for _ in 0..count {
                writer.write_n_bits(u16::from(code.code), code.bits);
            }
        }

        debug_assert!(writer.cursor() == 256, "HuffmanCodedPos must be 256 bits");
        HuffmanCodedPos { data: words_to_bytes(&words) }
    }

    pub fn huffman_coded_pos_unpack(
        packed: &HuffmanCodedPos,
    ) -> Result<String, HuffmanCodedPosError> {
        let words = bytes_to_words(&packed.data);
        let mut reader = BitReader::new(&words);
        let (board, hands, side_to_move) = unpack_raw(&mut reader)?;
        let mut pos = Self::empty();
        pos.board = board;
        pos.hands = hands;
        pos.set_side_to_move(side_to_move);
        pos.ply = 0;
        Ok(generate_sfen(&pos))
    }

    pub fn set_huffman_coded_pos(
        &mut self,
        packed: &HuffmanCodedPos,
        ply: Ply,
    ) -> Result<(), HuffmanCodedPosError> {
        let words = bytes_to_words(&packed.data);
        let mut reader = BitReader::new(&words);
        let (board, hands, side_to_move) = unpack_raw(&mut reader)?;

        self.board = board;
        self.hands = hands;
        self.set_side_to_move(side_to_move);
        self.ply = ply;
        self.entering_king_rule = EnteringKingRule::None;
        self.entering_king_point = [0, 0];

        self.rebuild_bitboards();

        let keys = self.compute_keys();
        self.board_key = keys.board_key;
        self.hand_key = keys.hand_key;
        self.zobrist = self.board_key ^ self.hand_key;

        self.reset_state_stack_to_current_position();
        self.update_entering_point();
        self.debug_assert_partial_keys_consistent();

        Ok(())
    }
}

fn unpack_raw(
    reader: &mut BitReader<'_>,
) -> Result<(BoardArray, [Hand; Color::COUNT], Color), HuffmanCodedPosError> {
    let side_to_move = if reader.read_one_bit()? { Color::WHITE } else { Color::BLACK };

    let mut board = BoardArray::empty();
    for color in [Color::BLACK, Color::WHITE] {
        let sq_raw = reader.read_n_bits(7)?;
        let sq = Square::new(i8::try_from(sq_raw).expect("square fits in i8"));
        if !sq.is_on_board() {
            return Err(HuffmanCodedPosError::InvalidKingSquare(sq_raw));
        }
        board.set(sq, Piece::from_parts(color, PieceType::KING));
    }

    for sq_idx in 0..Square::COUNT {
        let sq = Square::from_index(sq_idx);
        if board.get(sq).piece_type() == PieceType::KING {
            continue;
        }
        let piece = read_board_piece(reader)?;
        if piece != Piece::NONE {
            board.set(sq, piece);
        }
    }

    let mut hands = [Hand::ZERO; Color::COUNT];
    while reader.cursor() < 256 {
        let piece = read_hand_piece(reader)?;
        if piece == Piece::NONE {
            continue;
        }
        let hand_piece =
            HandPiece::from_piece_type(piece.piece_type()).expect("decoded hand piece is valid");
        hands[piece.color().to_index()].add(hand_piece, 1);
    }

    if reader.cursor() != 256 {
        return Err(HuffmanCodedPosError::InvalidCursor);
    }

    Ok((board, hands, side_to_move))
}

fn read_board_piece(reader: &mut BitReader<'_>) -> Result<Piece, HuffmanCodedPosError> {
    let mut code = 0u8;
    let mut bits = 0u8;
    loop {
        code |= u8::from(reader.read_one_bit()?) << bits;
        bits += 1;
        for entry in BOARD_CODES {
            if entry.code.bits == 0 {
                continue;
            }
            if entry.code.code == code && entry.code.bits == bits {
                return Ok(entry.piece);
            }
        }
        if bits > 8 {
            return Err(HuffmanCodedPosError::InvalidPieceCode);
        }
    }
}

fn read_hand_piece(reader: &mut BitReader<'_>) -> Result<Piece, HuffmanCodedPosError> {
    let mut code = 0u8;
    let mut bits = 0u8;
    loop {
        code |= u8::from(reader.read_one_bit()?) << bits;
        bits += 1;
        for entry in HAND_CODES {
            if entry.code.code == code && entry.code.bits == bits {
                return Ok(entry.piece.unwrap_or(Piece::NONE));
            }
        }
        if bits > 7 {
            return Err(HuffmanCodedPosError::InvalidPieceCode);
        }
    }
}