ai-hex 0.1.0

A small Hex rules and Monte Carlo tree search engine
Documentation
use std::fmt;
use std::str::FromStr;

pub const MIN_SIZE: u8 = 9;
pub const MAX_SIZE: u8 = 24;
pub const STRIDE: u16 = MAX_SIZE as u16;
pub const MAX_CELLS: usize = MAX_SIZE as usize * MAX_SIZE as usize;

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct BoardSize(u8);

impl BoardSize {
    pub fn new(size: u8) -> Result<Self, BoardSizeError> {
        if (MIN_SIZE..=MAX_SIZE).contains(&size) {
            Ok(Self(size))
        } else {
            Err(BoardSizeError(size))
        }
    }

    pub const fn get(self) -> u8 {
        self.0
    }

    #[cfg(test)]
    pub(crate) const fn for_test(size: u8) -> Self {
        assert!(size > 0 && size <= MAX_SIZE);
        Self(size)
    }

    pub const fn cell_count(self) -> u16 {
        self.0 as u16 * self.0 as u16
    }

    pub(crate) const fn contains(self, cell: Cell) -> bool {
        cell.file() < self.0 && cell.rank() < self.0
    }
}

impl Default for BoardSize {
    fn default() -> Self {
        Self(13)
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct BoardSizeError(u8);

impl fmt::Display for BoardSizeError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(
            formatter,
            "board size must be from {MIN_SIZE} through {MAX_SIZE}"
        )
    }
}

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

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub struct Cell(u16);

impl Cell {
    pub const fn new(file: u8, rank: u8) -> Option<Self> {
        if file < MAX_SIZE && rank < MAX_SIZE {
            Some(Self::from_coords(file, rank))
        } else {
            None
        }
    }

    pub(crate) const fn from_coords(file: u8, rank: u8) -> Self {
        Self(rank as u16 * STRIDE + file as u16)
    }

    pub(crate) const fn from_index(index: u16) -> Self {
        Self(index)
    }

    pub(crate) fn from_dense(index: u16, size: BoardSize) -> Self {
        let width = u16::from(size.get());
        Self::from_coords((index % width) as u8, (index / width) as u8)
    }

    pub const fn file(self) -> u8 {
        (self.0 % STRIDE) as u8
    }

    pub const fn rank(self) -> u8 {
        (self.0 / STRIDE) as u8
    }

    pub const fn index(self) -> u16 {
        self.0
    }
}

impl fmt::Display for Cell {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let file = char::from(b'a' + self.file());
        write!(formatter, "{file}{}", self.rank() + 1)
    }
}

#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum Move {
    Place(Cell),
    Swap,
}

impl Move {
    pub const fn place(file: u8, rank: u8) -> Option<Self> {
        match Cell::new(file, rank) {
            Some(cell) => Some(Self::Place(cell)),
            None => None,
        }
    }

    pub const fn cell(self) -> Option<Cell> {
        match self {
            Self::Place(cell) => Some(cell),
            Self::Swap => None,
        }
    }

    pub(crate) const fn order_key(self) -> u16 {
        match self {
            Self::Place(cell) => cell.index(),
            Self::Swap => u16::MAX,
        }
    }
}

impl fmt::Display for Move {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Self::Place(cell) => cell.fmt(formatter),
            Self::Swap => formatter.write_str("swap"),
        }
    }
}

impl FromStr for Move {
    type Err = ParseMoveError;

    fn from_str(value: &str) -> Result<Self, Self::Err> {
        if value.eq_ignore_ascii_case("swap") {
            return Ok(Self::Swap);
        }
        let bytes = value.as_bytes();
        if !(2..=3).contains(&bytes.len()) || !bytes[0].is_ascii_alphabetic() {
            return Err(ParseMoveError);
        }
        let file = bytes[0].to_ascii_lowercase().wrapping_sub(b'a');
        if file >= MAX_SIZE {
            return Err(ParseMoveError);
        }
        let rank = value[1..].parse::<u8>().map_err(|_| ParseMoveError)?;
        if !(1..=MAX_SIZE).contains(&rank) {
            return Err(ParseMoveError);
        }
        Ok(Self::Place(Cell::from_coords(file, rank - 1)))
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ParseMoveError;

impl fmt::Display for ParseMoveError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        formatter.write_str("expected a cell from a1 through x24, or swap")
    }
}

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