board_game/games/ataxx/
io.rs

1use std::fmt::{Debug, Display, Formatter, Write};
2
3use itertools::Itertools;
4
5use crate::board::Player;
6use crate::games::ataxx::{AtaxxBoard, Move};
7use crate::util::bitboard::BitBoard8;
8use crate::util::coord::Coord8;
9
10pub fn coord_to_uai(coord: Coord8) -> String {
11    let x = coord.x();
12    let y = coord.y();
13    format!("{}{}", (b'a' + x) as char, y + 1)
14}
15
16pub fn coord_from_uai(s: &str) -> Coord8 {
17    assert_eq!(s.len(), 2);
18    let x = s.as_bytes()[0] - b'a';
19    let y = (s.as_bytes()[1] - b'0') - 1;
20    Coord8::from_xy(x, y)
21}
22
23impl Debug for Move {
24    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
25        write!(f, "{}", self.to_uai())
26    }
27}
28
29impl Display for Move {
30    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
31        write!(f, "{}", self.to_uai())
32    }
33}
34
35#[derive(Debug, Eq, PartialEq)]
36pub struct InvalidUaiMove(String);
37
38impl Move {
39    pub fn to_uai(self) -> String {
40        match self {
41            Move::Pass => "0000".to_string(),
42            Move::Copy { to } => coord_to_uai(to),
43            Move::Jump { from, to } => format!("{}{}", coord_to_uai(from), coord_to_uai(to)),
44        }
45    }
46
47    pub fn from_uai(s: &str) -> Result<Move, InvalidUaiMove> {
48        match s {
49            "0000" => Ok(Move::Pass),
50            _ if s.len() == 2 => Ok(Move::Copy { to: coord_from_uai(s) }),
51            _ if s.len() == 4 => Ok(Move::Jump {
52                from: coord_from_uai(&s[..2]),
53                to: coord_from_uai(&s[2..]),
54            }),
55            _ => Err(InvalidUaiMove(s.to_owned())),
56        }
57    }
58}
59
60#[derive(Debug, Clone)]
61pub struct InvalidAtaxxFen {
62    pub fen: String,
63    pub reason: &'static str,
64}
65
66impl AtaxxBoard {
67    pub fn from_fen(fen: &str) -> Result<AtaxxBoard, InvalidAtaxxFen> {
68        let err = |reason| InvalidAtaxxFen {
69            fen: fen.into(),
70            reason,
71        };
72
73        let blocks = fen.split(' ').collect_vec();
74        let [board_str, next_str, half_str, full_str] = match &*blocks {
75            &[a, b, c, d] => [a, b, c, d],
76            _ => return Err(err("Not all 4 components present")),
77        };
78
79        // figure out the size, then parse the tiles and gaps
80        let mut board = if board_str == "/" {
81            AtaxxBoard::empty(0)
82        } else {
83            let rows = board_str.split('/').collect_vec();
84
85            let size = rows.len();
86            if size > AtaxxBoard::MAX_SIZE as usize {
87                return Err(err("More rows than maximum board size"));
88            }
89
90            let mut board = AtaxxBoard::empty(size as u8);
91            for (y, &line) in rows.iter().rev().enumerate().rev() {
92                let mut x = 0;
93
94                for c in line.chars() {
95                    if x >= size {
96                        return Err(err("Too many columns for size"));
97                    }
98
99                    let tile = BitBoard8::coord(Coord8::from_xy(x as u8, y as u8));
100
101                    match c {
102                        'x' => board.tiles_a |= tile,
103                        'o' => board.tiles_b |= tile,
104                        '-' => board.gaps |= tile,
105                        d if d.is_ascii_digit() => {
106                            x += d.to_digit(10).unwrap() as usize;
107                            continue;
108                        }
109                        _ => return Err(err("Invalid character in board")),
110                    }
111
112                    x += 1;
113                }
114            }
115
116            board
117        };
118
119        // parse other details
120        board.next_player = match next_str {
121            "x" => Player::A,
122            "o" => Player::B,
123            _ => return Err(err("Invalid next player")),
124        };
125
126        board.moves_since_last_copy = half_str.parse::<u8>().map_err(|_| err("Invalid half counter"))?;
127        let _ = full_str.parse::<u32>().map_err(|_| err("Invalid full counter"))?;
128
129        board.update_outcome();
130        board.assert_valid();
131
132        Ok(board)
133    }
134
135    pub fn to_fen(&self) -> String {
136        let mut s = String::new();
137
138        for y in (0..self.size).rev() {
139            if y != self.size - 1 {
140                write!(&mut s, "/").unwrap();
141            }
142
143            let mut empty_count = 0;
144
145            for x in 0..self.size {
146                let coord = Coord8::from_xy(x, y);
147
148                if self.free_tiles().has(coord) {
149                    empty_count += 1;
150                } else {
151                    if empty_count != 0 {
152                        write!(&mut s, "{}", empty_count).unwrap();
153                        empty_count = 0;
154                    }
155
156                    match self.tile(coord) {
157                        Some(player) => {
158                            write!(&mut s, "{}", player_symbol(player)).unwrap();
159                        }
160                        None => {
161                            assert!(self.gaps.has(coord));
162                            write!(&mut s, "-").unwrap();
163                        }
164                    }
165                }
166            }
167
168            if empty_count != 0 {
169                write!(&mut s, "{}", empty_count).unwrap();
170            }
171        }
172
173        write!(
174            &mut s,
175            " {} {} 1",
176            player_symbol(self.next_player),
177            self.moves_since_last_copy
178        )
179        .unwrap();
180
181        s
182    }
183}
184
185impl Debug for AtaxxBoard {
186    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
187        write!(f, "AtaxxBoard(\"{}\")", self.to_fen())
188    }
189}
190
191impl Display for AtaxxBoard {
192    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
193        writeln!(f, "FEN: {}", self.to_fen())?;
194
195        for y in (0..self.size).rev() {
196            write!(f, "{} ", y + 1)?;
197
198            for x in 0..self.size {
199                let coord = Coord8::from_xy(x, y);
200                let tuple = (self.gaps.has(coord), self.tile(coord));
201                let c = match tuple {
202                    (true, None) => '-',
203                    (false, None) => '.',
204                    (false, Some(player)) => player_symbol(player),
205                    (true, Some(_)) => unreachable!("Tile with block cannot have player"),
206                };
207
208                write!(f, "{}", c)?;
209            }
210
211            if y == 3 {
212                write!(
213                    f,
214                    "    {}  {}",
215                    player_symbol(self.next_player),
216                    self.moves_since_last_copy
217                )?;
218            }
219            writeln!(f)?;
220        }
221        write!(f, "  ")?;
222        for x in 0..self.size {
223            write!(f, "{}", (b'a' + x) as char)?;
224        }
225        writeln!(f)?;
226
227        Ok(())
228    }
229}
230
231fn player_symbol(player: Player) -> char {
232    match player {
233        Player::A => 'x',
234        Player::B => 'o',
235    }
236}