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 {}