Skip to main content

ai_hex/
position.rs

1use crate::board::{BoardSize, Cell, Move};
2use crate::connectivity::{connection_path, has_connection, insert, occupied};
3use std::fmt;
4
5#[derive(Clone, Copy, Debug, Eq, PartialEq)]
6pub enum Color {
7    Red,
8    Blue,
9}
10
11impl Color {
12    pub const fn other(self) -> Self {
13        match self {
14            Self::Red => Self::Blue,
15            Self::Blue => Self::Red,
16        }
17    }
18
19    pub const fn as_str(self) -> &'static str {
20        match self {
21            Self::Red => "red",
22            Self::Blue => "blue",
23        }
24    }
25}
26
27#[derive(Clone, Copy, Debug, Eq, PartialEq)]
28pub enum Seat {
29    One,
30    Two,
31}
32
33impl Seat {
34    pub const fn other(self) -> Self {
35        match self {
36            Self::One => Self::Two,
37            Self::Two => Self::One,
38        }
39    }
40
41    pub const fn as_str(self) -> &'static str {
42        match self {
43            Self::One => "one",
44            Self::Two => "two",
45        }
46    }
47}
48
49#[derive(Clone, Copy, Debug, Eq, PartialEq)]
50pub enum SwapRule {
51    Enabled,
52    Disabled,
53}
54
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub enum GameResult {
57    Ongoing,
58    Win(Seat),
59}
60
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
62pub struct Position {
63    size: BoardSize,
64    red: [u64; 9],
65    blue: [u64; 9],
66    seat_to_move: Seat,
67    colors_swapped: bool,
68    swap_rule: SwapRule,
69    swap_available: bool,
70    actions: u16,
71    stones: u16,
72    last_placement: Option<Cell>,
73    result: GameResult,
74}
75
76impl Position {
77    pub const fn new(size: BoardSize, swap_rule: SwapRule) -> Self {
78        Self {
79            size,
80            red: [0; 9],
81            blue: [0; 9],
82            seat_to_move: Seat::One,
83            colors_swapped: false,
84            swap_rule,
85            swap_available: false,
86            actions: 0,
87            stones: 0,
88            last_placement: None,
89            result: GameResult::Ongoing,
90        }
91    }
92
93    pub fn from_moves(
94        size: BoardSize,
95        swap_rule: SwapRule,
96        moves: &[Move],
97    ) -> Result<Self, MoveError> {
98        moves
99            .iter()
100            .try_fold(Self::new(size, swap_rule), |position, &mv| {
101                position.play(mv)
102            })
103    }
104
105    pub const fn size(self) -> BoardSize {
106        self.size
107    }
108
109    pub const fn seat_to_move(self) -> Seat {
110        self.seat_to_move
111    }
112
113    pub const fn color_to_move(self) -> Color {
114        self.color_for_seat(self.seat_to_move)
115    }
116
117    pub const fn color_for_seat(self, seat: Seat) -> Color {
118        match (seat, self.colors_swapped) {
119            (Seat::One, false) | (Seat::Two, true) => Color::Red,
120            (Seat::Two, false) | (Seat::One, true) => Color::Blue,
121        }
122    }
123
124    pub const fn seat_for_color(self, color: Color) -> Seat {
125        match (color, self.colors_swapped) {
126            (Color::Red, false) | (Color::Blue, true) => Seat::One,
127            (Color::Blue, false) | (Color::Red, true) => Seat::Two,
128        }
129    }
130
131    pub const fn colors_swapped(self) -> bool {
132        self.colors_swapped
133    }
134
135    pub const fn swap_rule(self) -> SwapRule {
136        self.swap_rule
137    }
138
139    pub const fn swap_available(self) -> bool {
140        self.swap_available
141    }
142
143    pub const fn actions(self) -> u16 {
144        self.actions
145    }
146
147    pub const fn stones(self) -> u16 {
148        self.stones
149    }
150
151    pub const fn last_placement(self) -> Option<Cell> {
152        self.last_placement
153    }
154
155    pub const fn result(self) -> GameResult {
156        self.result
157    }
158
159    pub fn color_at(self, cell: Cell) -> Option<Color> {
160        if !self.size.contains(cell) {
161            return None;
162        }
163        if occupied(&self.red, cell) {
164            Some(Color::Red)
165        } else if occupied(&self.blue, cell) {
166            Some(Color::Blue)
167        } else {
168            None
169        }
170    }
171
172    pub fn is_legal(self, mv: Move) -> bool {
173        if self.result != GameResult::Ongoing {
174            return false;
175        }
176        match mv {
177            Move::Swap => self.swap_available,
178            Move::Place(cell) => self.size.contains(cell) && self.color_at(cell).is_none(),
179        }
180    }
181
182    pub fn legal_moves(self) -> Vec<Move> {
183        if self.result != GameResult::Ongoing {
184            return Vec::new();
185        }
186        let mut moves = Vec::with_capacity(self.empty_count() + usize::from(self.swap_available));
187        if self.swap_available {
188            moves.push(Move::Swap);
189        }
190        for dense in 0..self.size.cell_count() {
191            let cell = Cell::from_dense(dense, self.size);
192            if self.color_at(cell).is_none() {
193                moves.push(Move::Place(cell));
194            }
195        }
196        moves
197    }
198
199    pub fn play(mut self, mv: Move) -> Result<Self, MoveError> {
200        if self.result != GameResult::Ongoing {
201            return Err(MoveError::GameOver);
202        }
203        match mv {
204            Move::Swap if self.swap_available => {
205                self.colors_swapped = !self.colors_swapped;
206                self.swap_available = false;
207                self.last_placement = None;
208                self.seat_to_move = self.seat_to_move.other();
209                self.actions += 1;
210                Ok(self)
211            }
212            Move::Swap => Err(MoveError::SwapUnavailable),
213            Move::Place(cell) if !self.size.contains(cell) => Err(MoveError::OutsideBoard),
214            Move::Place(cell) if self.color_at(cell).is_some() => Err(MoveError::Occupied),
215            Move::Place(cell) => {
216                let mover = self.seat_to_move;
217                let color = self.color_for_seat(mover);
218                insert(self.bits_mut(color), cell);
219                self.stones += 1;
220                self.last_placement = Some(cell);
221                self.actions += 1;
222                self.swap_available =
223                    self.stones == 1 && self.actions == 1 && self.swap_rule == SwapRule::Enabled;
224                self.seat_to_move = self.seat_to_move.other();
225                if has_connection(self.bits(color), self.size, color) {
226                    self.result = GameResult::Win(mover);
227                    self.swap_available = false;
228                }
229                Ok(self)
230            }
231        }
232    }
233
234    pub fn winning_path(self) -> Vec<Cell> {
235        let GameResult::Win(seat) = self.result else {
236            return Vec::new();
237        };
238        let color = self.color_for_seat(seat);
239        connection_path(self.bits(color), self.size, color).unwrap_or_default()
240    }
241
242    pub(crate) fn empty_count(self) -> usize {
243        usize::from(self.size.cell_count() - self.stones)
244    }
245
246    pub(crate) fn empty_cells(self) -> Vec<Cell> {
247        (0..self.size.cell_count())
248            .map(|dense| Cell::from_dense(dense, self.size))
249            .filter(|&cell| self.color_at(cell).is_none())
250            .collect()
251    }
252
253    pub(crate) fn connects_after(self, color: Color, cell: Cell) -> bool {
254        if self.color_at(cell).is_some() || !self.size.contains(cell) {
255            return false;
256        }
257        let mut bits = *self.bits(color);
258        insert(&mut bits, cell);
259        has_connection(&bits, self.size, color)
260    }
261
262    pub(crate) fn stone_count(self, color: Color) -> u32 {
263        self.bits(color).iter().map(|word| word.count_ones()).sum()
264    }
265
266    pub(crate) fn rollout_decline_swap(&mut self) {
267        self.swap_available = false;
268    }
269
270    pub(crate) fn rollout_place(&mut self, cell: Cell) {
271        let color = self.color_to_move();
272        insert(self.bits_mut(color), cell);
273        self.stones += 1;
274        self.last_placement = Some(cell);
275        self.actions += 1;
276        self.swap_available = false;
277        self.seat_to_move = self.seat_to_move.other();
278    }
279
280    pub(crate) fn winner_on_full_board(self) -> Seat {
281        if has_connection(&self.red, self.size, Color::Red) {
282            self.seat_for_color(Color::Red)
283        } else {
284            debug_assert!(has_connection(&self.blue, self.size, Color::Blue));
285            self.seat_for_color(Color::Blue)
286        }
287    }
288
289    fn bits(&self, color: Color) -> &[u64; 9] {
290        match color {
291            Color::Red => &self.red,
292            Color::Blue => &self.blue,
293        }
294    }
295
296    fn bits_mut(&mut self, color: Color) -> &mut [u64; 9] {
297        match color {
298            Color::Red => &mut self.red,
299            Color::Blue => &mut self.blue,
300        }
301    }
302}
303
304#[derive(Clone, Copy, Debug, Eq, PartialEq)]
305pub enum MoveError {
306    GameOver,
307    OutsideBoard,
308    Occupied,
309    SwapUnavailable,
310}
311
312impl fmt::Display for MoveError {
313    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
314        formatter.write_str(match self {
315            Self::GameOver => "the game is over",
316            Self::OutsideBoard => "the cell is outside this board",
317            Self::Occupied => "the cell is occupied",
318            Self::SwapUnavailable => "the swap move is not available",
319        })
320    }
321}
322
323impl std::error::Error for MoveError {}