rsshogi 1.2.4

Reusable Rust shogi primitives for board state, move generation, legality, and record parsing.
Documentation
use super::{File, Rank};
use core::{
    fmt,
    ops::{Index, IndexMut},
    str::FromStr,
};

#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[repr(transparent)]
pub struct Square(i8);

const fn raw_to_file() -> [i8; 256] {
    let mut values = [0; 256];
    let mut index = 0;
    while index < values.len() {
        values[index] = (index as u8 as i8) / 9;
        index += 1;
    }
    values
}

const fn raw_to_rank() -> [i8; 256] {
    let mut values = [0; 256];
    let mut index = 0;
    while index < values.len() {
        values[index] = (index as u8 as i8) % 9;
        index += 1;
    }
    values
}

static RAW_TO_FILE: [i8; 256] = raw_to_file();
static RAW_TO_RANK: [i8; 256] = raw_to_rank();

impl Square {
    pub const COUNT: usize = 81;
    pub const COUNT_WITH_NONE: usize = 82;

    pub fn iter() -> impl Iterator<Item = Self> {
        (0..Self::COUNT).map(Self::from_index)
    }
    #[must_use]
    pub const fn new(raw: i8) -> Self {
        Self(raw)
    }
    #[must_use]
    pub const fn raw(self) -> i8 {
        self.0
    }
    #[must_use]
    pub const fn to_index(self) -> usize {
        self.0 as usize
    }
    #[must_use]
    pub const fn is_valid(self) -> bool {
        self.0 >= 0 && self.0 < Self::COUNT as i8
    }
    #[must_use]
    pub const fn is_on_board(self) -> bool {
        self.is_valid()
    }
    #[must_use]
    pub const fn from_index(index: usize) -> Self {
        Self(index as i8)
    }
    #[must_use]
    pub const fn to_board_index(self) -> usize {
        self.0 as usize
    }
    #[must_use]
    pub const fn to_index_with_none(self) -> usize {
        self.0 as usize
    }
    #[must_use]
    pub const fn is_none(self) -> bool {
        self.0 == 81
    }
    #[must_use]
    pub const fn file(self) -> File {
        File::new(RAW_TO_FILE[self.0 as u8 as usize])
    }
    #[must_use]
    pub const fn rank(self) -> Rank {
        Rank::new(RAW_TO_RANK[self.0 as u8 as usize])
    }
    #[must_use]
    pub const fn from_file_rank(file: File, rank: Rank) -> Self {
        Self(file.raw() * 9 + rank.raw())
    }
    #[must_use]
    pub const fn distance(self, other: Self) -> i8 {
        let df = self.file().raw() - other.file().raw();
        let dr = self.rank().raw() - other.rank().raw();
        let af = if df < 0 { -df } else { df };
        let ar = if dr < 0 { -dr } else { dr };
        if af > ar { af } else { ar }
    }
    #[must_use]
    pub const fn flip(self) -> Self {
        flip(self)
    }
    #[must_use]
    pub fn from_usi(value: &str) -> Option<Self> {
        value.parse().ok()
    }

    #[must_use]
    pub const fn mirror_file(self) -> Self {
        Self::from_file_rank(File::new(8 - self.file().raw()), self.rank())
    }
    #[must_use]
    pub fn to_usi(self) -> String {
        self.to_string()
    }
}
impl FromStr for Square {
    type Err = &'static str;
    fn from_str(value: &str) -> Result<Self, Self::Err> {
        if value == "none" {
            return Ok(Self::NONE);
        }
        let b = value.as_bytes();
        if b.len() != 2 || !(b'1'..=b'9').contains(&b[0]) || !(b'a'..=b'i').contains(&b[1]) {
            return Err("invalid USI square");
        }
        Ok(Self::from_file_rank(File::new((b[0] - b'1') as i8), Rank::new((b[1] - b'a') as i8)))
    }
}
impl fmt::Display for Square {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}{}", self.file(), self.rank())
    }
}

pub const SQ_ZERO: Square = Square::new(0);
pub const SQ_NONE: Square = Square::new(81);
impl Square {
    pub const NONE: Self = SQ_NONE;
}
pub const SQ_D: i8 = 1;
pub const SQ_U: i8 = -1;
pub const SQ_R: i8 = -9;
pub const SQ_L: i8 = 9;
pub const SQ_RU: i8 = -10;
pub const SQ_RD: i8 = -8;
pub const SQ_LU: i8 = 8;
pub const SQ_LD: i8 = 10;
macro_rules! squares { ($($name:ident = $raw:expr),* $(,)?) => { $(pub const $name: Square = Square::new($raw);)* }; }
squares!(
    SQ_11 = 0,
    SQ_12 = 1,
    SQ_13 = 2,
    SQ_14 = 3,
    SQ_15 = 4,
    SQ_16 = 5,
    SQ_17 = 6,
    SQ_18 = 7,
    SQ_19 = 8,
    SQ_21 = 9,
    SQ_22 = 10,
    SQ_23 = 11,
    SQ_24 = 12,
    SQ_25 = 13,
    SQ_26 = 14,
    SQ_27 = 15,
    SQ_28 = 16,
    SQ_29 = 17,
    SQ_31 = 18,
    SQ_32 = 19,
    SQ_33 = 20,
    SQ_34 = 21,
    SQ_35 = 22,
    SQ_36 = 23,
    SQ_37 = 24,
    SQ_38 = 25,
    SQ_39 = 26,
    SQ_41 = 27,
    SQ_42 = 28,
    SQ_43 = 29,
    SQ_44 = 30,
    SQ_45 = 31,
    SQ_46 = 32,
    SQ_47 = 33,
    SQ_48 = 34,
    SQ_49 = 35,
    SQ_51 = 36,
    SQ_52 = 37,
    SQ_53 = 38,
    SQ_54 = 39,
    SQ_55 = 40,
    SQ_56 = 41,
    SQ_57 = 42,
    SQ_58 = 43,
    SQ_59 = 44,
    SQ_61 = 45,
    SQ_62 = 46,
    SQ_63 = 47,
    SQ_64 = 48,
    SQ_65 = 49,
    SQ_66 = 50,
    SQ_67 = 51,
    SQ_68 = 52,
    SQ_69 = 53,
    SQ_71 = 54,
    SQ_72 = 55,
    SQ_73 = 56,
    SQ_74 = 57,
    SQ_75 = 58,
    SQ_76 = 59,
    SQ_77 = 60,
    SQ_78 = 61,
    SQ_79 = 62,
    SQ_81 = 63,
    SQ_82 = 64,
    SQ_83 = 65,
    SQ_84 = 66,
    SQ_85 = 67,
    SQ_86 = 68,
    SQ_87 = 69,
    SQ_88 = 70,
    SQ_89 = 71,
    SQ_91 = 72,
    SQ_92 = 73,
    SQ_93 = 74,
    SQ_94 = 75,
    SQ_95 = 76,
    SQ_96 = 77,
    SQ_97 = 78,
    SQ_98 = 79,
    SQ_99 = 80
);

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct SquareTable<T, const N: usize>([T; N]);
impl<T, const N: usize> SquareTable<T, N> {
    pub const fn new(values: [T; N]) -> Self {
        Self(values)
    }

    #[must_use]
    pub const fn len(&self) -> usize {
        N
    }

    #[must_use]
    pub const fn is_empty(&self) -> bool {
        N == 0
    }
    #[must_use]
    pub const fn as_slice(&self) -> &[T] {
        &self.0
    }
    pub fn iter(&self) -> core::slice::Iter<'_, T> {
        self.0.iter()
    }
}
impl<T, const N: usize> Index<usize> for SquareTable<T, N> {
    type Output = T;
    fn index(&self, index: usize) -> &Self::Output {
        &self.0[index]
    }
}
impl<T, const N: usize> Index<Square> for SquareTable<T, N> {
    type Output = T;
    fn index(&self, index: Square) -> &Self::Output {
        &self.0[index.to_index()]
    }
}
impl<T, const N: usize> IndexMut<usize> for SquareTable<T, N> {
    fn index_mut(&mut self, index: usize) -> &mut Self::Output {
        &mut self.0[index]
    }
}
impl<T, const N: usize> IntoIterator for SquareTable<T, N> {
    type Item = T;
    type IntoIter = core::array::IntoIter<T, N>;
    fn into_iter(self) -> Self::IntoIter {
        self.0.into_iter()
    }
}
impl<'a, T, const N: usize> IntoIterator for &'a SquareTable<T, N> {
    type Item = &'a T;
    type IntoIter = core::slice::Iter<'a, T>;
    fn into_iter(self) -> Self::IntoIter {
        self.0.iter()
    }
}
#[must_use]
pub const fn flip(square: Square) -> Square {
    Square::new(80 - square.raw())
}
#[must_use]
pub const fn is_promotable_move(color: super::Color, from: Square, to: Square) -> bool {
    match color {
        super::Color::BLACK => from.rank().raw() <= 2 || to.rank().raw() <= 2,
        super::Color::WHITE => from.rank().raw() >= 6 || to.rank().raw() >= 6,
    }
}
#[must_use]
pub const fn is_promotable_square(color: super::Color, square: Square) -> bool {
    match color {
        super::Color::BLACK => square.rank().raw() <= 2,
        super::Color::WHITE => square.rank().raw() >= 6,
    }
}

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

    use super::{SQ_11, SQ_91, SQ_99, Square};

    #[test]
    fn usi_and_file_mirror_are_fallible_and_symmetric() {
        assert_eq!(Square::from_usi("1a"), Some(SQ_11));
        assert_eq!(Square::from_usi("9a"), Some(SQ_91));
        assert_eq!(Square::from_usi("0a"), None);
        assert_eq!(SQ_11.mirror_file(), SQ_91);
        assert_eq!(SQ_11.mirror_file().mirror_file(), SQ_11);
    }

    #[test]
    fn iterator_visits_all_valid_squares_in_raw_order() {
        let squares: Vec<_> = Square::iter().collect();
        assert_eq!(squares.len(), Square::COUNT);
        assert_eq!(squares.first(), Some(&SQ_11));
        assert_eq!(squares.last(), Some(&SQ_99));
        assert!(squares.iter().enumerate().all(|(raw, square)| square.raw() == raw as i8));
    }

    #[test]
    fn from_str_accepts_none_and_reports_python_compatible_errors() {
        assert_eq!(Square::from_str("none"), Ok(Square::NONE));
        assert_eq!(Square::from_str("1j"), Err("invalid USI square"));
    }

    #[test]
    fn coordinate_lookups_match_signed_arithmetic_for_every_raw_value() {
        for raw in i8::MIN..=i8::MAX {
            let square = Square::new(raw);
            assert_eq!(square.file().raw(), raw / 9, "raw file {raw}");
            assert_eq!(square.rank().raw(), raw % 9, "raw rank {raw}");
        }
    }

    #[test]
    fn all_squares_preserve_file_major_mapping() {
        for square in Square::iter() {
            assert_eq!(square.raw(), square.file().raw() * 9 + square.rank().raw());
            assert_eq!(Square::from_file_rank(square.file(), square.rank()), square);
        }
    }
}