rsshogi 1.2.2

Reusable Rust shogi primitives for board state, move generation, legality, and record parsing.
Documentation
use super::Color;
use core::{fmt, str::FromStr};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct PieceType(i8);
impl PieceType {
    pub const NONE: Self = Self(0);
    pub const PAWN: Self = Self(1);
    pub const LANCE: Self = Self(2);
    pub const KNIGHT: Self = Self(3);
    pub const SILVER: Self = Self(4);
    pub const BISHOP: Self = Self(5);
    pub const ROOK: Self = Self(6);
    pub const GOLD: Self = Self(7);
    pub const KING: Self = Self(8);
    pub const PRO_PAWN: Self = Self(9);
    pub const PRO_LANCE: Self = Self(10);
    pub const PRO_KNIGHT: Self = Self(11);
    pub const PRO_SILVER: Self = Self(12);
    pub const HORSE: Self = Self(13);
    pub const DRAGON: Self = Self(14);
    pub const GOLD_LIKE: Self = Self(15);
    pub const PIECE_TYPE_PROMOTE: i8 = 8;
    pub const COUNT: usize = 16;
    pub const PIECE_HAND_ZERO: Self = Self::PAWN;
    pub const HAND_TABLE_SIZE: usize = 8;
    pub const fn new(raw: i8) -> Self {
        Self(raw)
    }
    pub const fn raw(self) -> i8 {
        self.0
    }
    pub const fn to_index(self) -> usize {
        self.0 as usize
    }
    pub const fn is_valid(self) -> bool {
        self.0 >= 0 && self.0 < Self::COUNT as i8
    }
    pub const fn is_promoted(self) -> bool {
        matches!(self.0, 9..=14)
    }
    pub const fn is_promotable(self) -> bool {
        matches!(self.0, 1..=6)
    }
    pub const fn is_unpromotable(self) -> bool {
        !self.is_promotable()
    }
    pub const fn promote(self) -> Self {
        Self::new(self.raw() + if self.is_promotable() { 8 } else { 0 })
    }
    pub const fn demote(self) -> Self {
        Self::new(self.raw() - if self.is_promoted() { 8 } else { 0 })
    }
    pub const fn is_hand_piece(self) -> bool {
        matches!(self.0, 1..=7)
    }
    pub const fn is_minor(self) -> bool {
        matches!(self.0, 2..=4 | 7 | 9..=12)
    }
    pub fn iter() -> impl Iterator<Item = Self> {
        (0..Self::GOLD_LIKE.raw()).map(Self::new)
    }
    pub fn raw_pieces() -> impl DoubleEndedIterator<Item = Self> + ExactSizeIterator {
        (Self::PAWN.raw()..=Self::KING.raw()).map(Self::new)
    }
    pub fn hand_pieces() -> impl DoubleEndedIterator<Item = Self> + ExactSizeIterator {
        (1..Self::HAND_TABLE_SIZE as i8).map(Self::new)
    }
}
impl TryFrom<i8> for PieceType {
    type Error = &'static str;
    fn try_from(v: i8) -> Result<Self, Self::Error> {
        if Self::new(v).is_valid() { Ok(Self::new(v)) } else { Err("invalid piece type") }
    }
}
impl fmt::Display for PieceType {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(match self.0 {
            1 => "P",
            2 => "L",
            3 => "N",
            4 => "S",
            5 => "B",
            6 => "R",
            7 => "G",
            8 => "K",
            9 => "+P",
            10 => "+L",
            11 => "+N",
            12 => "+S",
            13 => "+B",
            14 => "+R",
            _ => "",
        })
    }
}
impl FromStr for PieceType {
    type Err = &'static str;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let plus = s.starts_with('+');
        match s.trim_start_matches('+').to_ascii_uppercase().as_str() {
            "P" => Ok(if plus { Self::PRO_PAWN } else { Self::PAWN }),
            "L" => Ok(if plus { Self::PRO_LANCE } else { Self::LANCE }),
            "N" => Ok(if plus { Self::PRO_KNIGHT } else { Self::KNIGHT }),
            "S" => Ok(if plus { Self::PRO_SILVER } else { Self::SILVER }),
            "B" => Ok(if plus { Self::HORSE } else { Self::BISHOP }),
            "R" => Ok(if plus { Self::DRAGON } else { Self::ROOK }),
            "G" => Ok(Self::GOLD),
            "K" => Ok(Self::KING),
            _ => Err("invalid piece type"),
        }
    }
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct Piece(i8);
impl Piece {
    pub const NONE: Self = Self(0);
    pub const COLOR_BIT: i8 = 16;
    pub const PROMOTE_OFFSET: i8 = 8;
    pub const COUNT: usize = 32;
    pub const HAND_TABLE_SIZE: usize = 8;
    pub const RAW_COUNT: usize = 8;
}
macro_rules! pieces{($($n:ident=$v:expr),*)=>{$(pub const $n:Piece=Piece($v);)*};}
pieces!(
    B_PAWN = 1,
    B_LANCE = 2,
    B_KNIGHT = 3,
    B_SILVER = 4,
    B_BISHOP = 5,
    B_ROOK = 6,
    B_GOLD = 7,
    B_KING = 8,
    B_PRO_PAWN = 9,
    B_PRO_LANCE = 10,
    B_PRO_KNIGHT = 11,
    B_PRO_SILVER = 12,
    B_HORSE = 13,
    B_DRAGON = 14,
    B_GOLD_LIKE = 15,
    W_PAWN = 17,
    W_LANCE = 18,
    W_KNIGHT = 19,
    W_SILVER = 20,
    W_BISHOP = 21,
    W_ROOK = 22,
    W_GOLD = 23,
    W_KING = 24,
    W_PRO_PAWN = 25,
    W_PRO_LANCE = 26,
    W_PRO_KNIGHT = 27,
    W_PRO_SILVER = 28,
    W_HORSE = 29,
    W_DRAGON = 30,
    W_GOLD_LIKE = 31
);
impl Piece {
    pub const B_PAWN: Self = B_PAWN;
    pub const B_LANCE: Self = B_LANCE;
    pub const B_KNIGHT: Self = B_KNIGHT;
    pub const B_SILVER: Self = B_SILVER;
    pub const B_BISHOP: Self = B_BISHOP;
    pub const B_ROOK: Self = B_ROOK;
    pub const B_GOLD: Self = B_GOLD;
    pub const B_KING: Self = B_KING;
    pub const B_PRO_PAWN: Self = B_PRO_PAWN;
    pub const B_PRO_LANCE: Self = B_PRO_LANCE;
    pub const B_PRO_KNIGHT: Self = B_PRO_KNIGHT;
    pub const B_PRO_SILVER: Self = B_PRO_SILVER;
    pub const B_HORSE: Self = B_HORSE;
    pub const B_DRAGON: Self = B_DRAGON;
    pub const B_GOLD_LIKE: Self = B_GOLD_LIKE;
    pub const W_PAWN: Self = W_PAWN;
    pub const W_LANCE: Self = W_LANCE;
    pub const W_KNIGHT: Self = W_KNIGHT;
    pub const W_SILVER: Self = W_SILVER;
    pub const W_BISHOP: Self = W_BISHOP;
    pub const W_ROOK: Self = W_ROOK;
    pub const W_GOLD: Self = W_GOLD;
    pub const W_KING: Self = W_KING;
    pub const W_PRO_PAWN: Self = W_PRO_PAWN;
    pub const W_PRO_LANCE: Self = W_PRO_LANCE;
    pub const W_PRO_KNIGHT: Self = W_PRO_KNIGHT;
    pub const W_PRO_SILVER: Self = W_PRO_SILVER;
    pub const W_HORSE: Self = W_HORSE;
    pub const W_DRAGON: Self = W_DRAGON;
    pub const W_GOLD_LIKE: Self = W_GOLD_LIKE;
    pub const fn new(v: i8) -> Self {
        Self(v)
    }
    pub const fn raw(self) -> i8 {
        self.0
    }
    pub const fn to_index(self) -> usize {
        self.0 as usize
    }
    pub const fn is_empty(self) -> bool {
        self.0 == 0
    }
    pub const fn color(self) -> Color {
        Color::new((self.0 as u8 >> 4) & 1)
    }
    pub const fn piece_type(self) -> PieceType {
        PieceType::new(self.0 & 15)
    }
    pub const fn from_parts(c: Color, p: PieceType) -> Self {
        Self(c.raw() * 16 + p.raw())
    }
    pub const fn promote(self) -> Self {
        Self::from_parts(self.color(), self.piece_type().promote())
    }
    pub const fn demote(self) -> Self {
        Self::from_parts(self.color(), self.piece_type().demote())
    }
    pub const fn base_piece_type(self) -> PieceType {
        self.piece_type().demote()
    }
    pub const fn unpromoted_piece(self) -> Self {
        self.demote()
    }
    pub const fn is_promoted(self) -> bool {
        self.piece_type().is_promoted()
    }
    pub const fn is_unpromotable(self) -> bool {
        self.piece_type().is_unpromotable()
    }
    pub const fn is_long_range(self) -> bool {
        matches!(self.piece_type().raw(), 2 | 5 | 6 | 13 | 14)
    }
    pub const fn is_valid(self) -> bool {
        self.0 >= 0 && self.0 < Self::COUNT as i8 && self.0 != Self::COLOR_BIT
    }
}
impl TryFrom<i8> for Piece {
    type Error = &'static str;
    fn try_from(v: i8) -> Result<Self, Self::Error> {
        if Self(v).is_valid() { Ok(Self(v)) } else { Err("invalid piece") }
    }
}
impl FromStr for Piece {
    type Err = &'static str;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let bytes = s.as_bytes();
        let symbol_index = match bytes {
            [b'+', _] => 1,
            [_] => 0,
            _ => return Err("invalid piece"),
        };
        let piece_type = PieceType::from_str(s)?;
        let symbol = bytes[symbol_index] as char;
        let color = if symbol.is_ascii_lowercase() { Color::WHITE } else { Color::BLACK };
        Ok(Self::from_parts(color, piece_type))
    }
}

impl fmt::Display for Piece {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let s = self.piece_type().to_string();
        if self.is_empty() {
            Ok(())
        } else if self.color() == Color::WHITE {
            f.write_str(&s.to_ascii_lowercase())
        } else {
            f.write_str(&s)
        }
    }
}

#[cfg(test)]
mod tests {
    use core::str::FromStr;

    use super::{Piece, PieceType};

    #[test]
    fn piece_type_raw_and_minor_contracts() {
        let minor = [2, 3, 4, 7, 9, 10, 11, 12];
        for raw in 0..PieceType::COUNT as i8 {
            assert_eq!(PieceType::new(raw).is_minor(), minor.contains(&raw));
        }
        assert_eq!(PieceType::new(-1).raw(), -1);
        assert!(!PieceType::new(-1).is_valid());
    }

    #[test]
    fn piece_helpers_preserve_color_and_base_type() {
        assert_eq!(Piece::W_HORSE.base_piece_type(), PieceType::BISHOP);
        assert_eq!(Piece::W_HORSE.unpromoted_piece(), Piece::W_BISHOP);
        assert!(Piece::B_ROOK.is_long_range());
        assert!(!Piece::new(16).is_valid());
    }

    #[test]
    fn piece_from_str_preserves_color_and_promotion() {
        assert_eq!(Piece::from_str("P"), Ok(Piece::B_PAWN));
        assert_eq!(Piece::from_str("p"), Ok(Piece::W_PAWN));
        assert_eq!(Piece::from_str("+B"), Ok(Piece::B_HORSE));
        assert_eq!(Piece::from_str("+r"), Ok(Piece::W_DRAGON));
        assert!(Piece::from_str("").is_err());
        assert!(Piece::from_str("PP").is_err());
        assert!(Piece::from_str("++P").is_err());
    }

    #[test]
    fn public_piece_type_iterator_excludes_internal_gold_like() {
        let types: Vec<_> = PieceType::iter().collect();
        assert_eq!(types.len(), 15);
        assert_eq!(types.first(), Some(&PieceType::NONE));
        assert_eq!(types.last(), Some(&PieceType::DRAGON));
        assert!(!types.contains(&PieceType::GOLD_LIKE));
        assert_eq!(
            PieceType::raw_pieces().collect::<Vec<_>>(),
            vec![
                PieceType::PAWN,
                PieceType::LANCE,
                PieceType::KNIGHT,
                PieceType::SILVER,
                PieceType::BISHOP,
                PieceType::ROOK,
                PieceType::GOLD,
                PieceType::KING,
            ]
        );
        assert_eq!(
            PieceType::hand_pieces().collect::<Vec<_>>(),
            vec![
                PieceType::PAWN,
                PieceType::LANCE,
                PieceType::KNIGHT,
                PieceType::SILVER,
                PieceType::BISHOP,
                PieceType::ROOK,
                PieceType::GOLD,
            ]
        );
    }
}