Skip to main content

ai_othello/
mv.rs

1use std::fmt;
2use std::str::FromStr;
3
4/// One square, with `a1` as the least-significant bit.
5#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
6pub struct Square(u8);
7
8impl Square {
9    pub const COUNT: usize = 64;
10
11    /// Creates a square from its rank-first zero-based index.
12    ///
13    /// # Panics
14    ///
15    /// Panics when `index` is greater than 63.
16    pub const fn new(index: u8) -> Self {
17        assert!(index < 64, "square index must be 0..64");
18        Self(index)
19    }
20
21    pub const fn index(self) -> usize {
22        self.0 as usize
23    }
24
25    pub const fn file(self) -> u8 {
26        self.0 % 8
27    }
28
29    pub const fn rank(self) -> u8 {
30        self.0 / 8
31    }
32
33    pub const fn mirrored(self) -> Self {
34        Self::new(self.rank() * 8 + (7 - self.file()))
35    }
36
37    pub fn all() -> impl Iterator<Item = Self> {
38        (0..64).map(Self)
39    }
40}
41
42impl fmt::Display for Square {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        write!(
45            f,
46            "{}{}",
47            char::from(b'a' + self.file()),
48            char::from(b'1' + self.rank())
49        )
50    }
51}
52
53impl FromStr for Square {
54    type Err = &'static str;
55
56    fn from_str(value: &str) -> Result<Self, Self::Err> {
57        let bytes = value.as_bytes();
58        if bytes.len() != 2
59            || !(b'a'..=b'h').contains(&bytes[0])
60            || !(b'1'..=b'8').contains(&bytes[1])
61        {
62            return Err("square must be a1..h8");
63        }
64        Ok(Self::new((bytes[1] - b'1') * 8 + (bytes[0] - b'a')))
65    }
66}
67
68/// Othello pass is a real move because it changes the side to move.
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
70pub enum Move {
71    Place(Square),
72    Pass,
73}
74
75impl Move {
76    pub const fn square(self) -> Option<Square> {
77        match self {
78            Self::Place(square) => Some(square),
79            Self::Pass => None,
80        }
81    }
82
83    pub const fn mirrored(self) -> Self {
84        match self {
85            Self::Place(square) => Self::Place(square.mirrored()),
86            Self::Pass => Self::Pass,
87        }
88    }
89}
90
91impl fmt::Display for Move {
92    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93        match self {
94            Self::Place(square) => square.fmt(f),
95            Self::Pass => f.write_str("pass"),
96        }
97    }
98}
99
100impl FromStr for Move {
101    type Err = &'static str;
102
103    fn from_str(value: &str) -> Result<Self, Self::Err> {
104        if value.eq_ignore_ascii_case("pass") {
105            Ok(Self::Pass)
106        } else {
107            value.parse().map(Self::Place)
108        }
109    }
110}
111
112/// Fixed-capacity list large enough for every Othello move.
113#[derive(Debug, Clone, Copy, PartialEq, Eq)]
114pub struct MoveList {
115    moves: [Move; Square::COUNT],
116    len: u8,
117}
118
119impl Default for MoveList {
120    fn default() -> Self {
121        Self {
122            moves: [Move::Pass; Square::COUNT],
123            len: 0,
124        }
125    }
126}
127
128impl MoveList {
129    pub const fn len(self) -> usize {
130        self.len as usize
131    }
132
133    pub const fn is_empty(self) -> bool {
134        self.len == 0
135    }
136
137    pub fn as_slice(&self) -> &[Move] {
138        &self.moves[..self.len()]
139    }
140
141    pub(crate) fn push(&mut self, mv: Move) {
142        debug_assert!(self.len() < self.moves.len());
143        self.moves[self.len()] = mv;
144        self.len += 1;
145    }
146}
147
148impl IntoIterator for MoveList {
149    type Item = Move;
150    type IntoIter = std::iter::Take<std::array::IntoIter<Move, { Square::COUNT }>>;
151
152    fn into_iter(self) -> Self::IntoIter {
153        self.moves.into_iter().take(self.len())
154    }
155}