Skip to main content

arimaa_engine/
display.rs

1use anyhow::Result;
2use std::fmt::{self, Display, Formatter};
3use std::str::FromStr;
4
5use super::constants::{BOARD_HEIGHT, BOARD_WIDTH};
6use super::game_state::GameState;
7use super::piece_board::PieceBoard;
8use super::{Piece, Square};
9
10impl Display for GameState {
11    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
12        let move_number = self.get_move_number();
13        let curr_player = if self.is_p1_turn_to_move() { "g" } else { "s" };
14        let piece_board = &self.get_piece_board();
15        writeln!(f, "{}{}", move_number, curr_player)?;
16
17        writeln!(f, " +-----------------+")?;
18
19        for row_idx in 0..BOARD_HEIGHT {
20            write!(f, "{}|", BOARD_HEIGHT - row_idx)?;
21            for col_idx in 0..BOARD_WIDTH {
22                let idx = (row_idx * BOARD_WIDTH + col_idx) as u8;
23                let square = Square::from_index(idx);
24                let letter = if let Some(piece) = piece_board.get_piece_type_at_square(&square) {
25                    let is_p1_piece = is_p1_piece(square.as_bit_board(), piece_board);
26                    convert_piece_to_letter(&piece, is_p1_piece)
27                } else if idx == 18 || idx == 21 || idx == 42 || idx == 45 {
28                    "x".to_string()
29                } else {
30                    " ".to_string()
31                };
32
33                write!(f, " {}", letter)?;
34            }
35            writeln!(f, " |")?;
36        }
37
38        writeln!(f, " +-----------------+")?;
39        writeln!(f, "   a b c d e f g h")?;
40
41        Ok(())
42    }
43}
44
45impl FromStr for GameState {
46    type Err = anyhow::Error;
47
48    fn from_str(s: &str) -> Result<Self> {
49        // Compact AEI format: {optional_number}{player} [{64-char-board}]
50        // e.g. "g [rredchrrrhrdcmrr                                MHHEDDCRRRRRRRRC]"
51        //      "1g [rredchrrrhrdcmrr                                MHHEDDCRRRRRRRRC]"
52        let compact_regex = regex::Regex::new(r"^\s*(?:(\d+))?([gswb])\s*\[([^\]]+)\]").unwrap();
53        if let Some(caps) = compact_regex.captures(s) {
54            let move_number: usize = caps.get(1).map_or(1, |m| m.as_str().parse().unwrap_or(1));
55            let player = caps.get(2).unwrap().as_str();
56            let p1_turn_to_move = player != "s" && player != "b";
57            let pieces_str = caps.get(3).unwrap().as_str();
58
59            let mut p1_pieces = 0u64;
60            let mut elephants = 0u64;
61            let mut camels = 0u64;
62            let mut horses = 0u64;
63            let mut dogs = 0u64;
64            let mut cats = 0u64;
65            let mut rabbits = 0u64;
66
67            for (i, charr) in pieces_str.chars().enumerate().take(BOARD_WIDTH * BOARD_HEIGHT) {
68                if let Some((piece, is_p1)) = convert_char_to_piece(charr) {
69                    let row_idx = i / BOARD_WIDTH;
70                    let col_idx = i % BOARD_WIDTH;
71                    let idx = (row_idx * BOARD_WIDTH + col_idx) as u8;
72                    let square = Square::from_index(idx);
73                    let square_bit = square.as_bit_board();
74
75                    match piece {
76                        Piece::Elephant => elephants |= square_bit,
77                        Piece::Camel => camels |= square_bit,
78                        Piece::Horse => horses |= square_bit,
79                        Piece::Dog => dogs |= square_bit,
80                        Piece::Cat => cats |= square_bit,
81                        Piece::Rabbit => rabbits |= square_bit,
82                    }
83
84                    if is_p1 {
85                        p1_pieces |= square_bit;
86                    }
87                }
88            }
89
90            let piece_board = PieceBoard::new(p1_pieces, elephants, camels, horses, dogs, cats, rabbits);
91            return Ok(GameState::new(p1_turn_to_move, move_number, piece_board));
92        }
93
94        // Visual board format with | separators:
95        // e.g. "2g\n +-...-+\n8| r r r ... |\n7| ..."
96        let lines: Vec<_> = s.split('|').enumerate().filter(|(i, _)| i % 2 == 1).map(|(_, s)| s).collect();
97        let regex = regex::Regex::new(r"^\s*(\d+)([gswb])").unwrap();
98
99        let (move_number, p1_turn_to_move) = regex.captures(s.split('|').next().unwrap()).map_or((2, true), |c| {
100            (c.get(1).unwrap().as_str().parse().unwrap(), c.get(2).unwrap().as_str() != "s" && c.get(2).unwrap().as_str() != "b")
101        });
102
103        let mut p1_pieces = 0;
104        let mut elephants = 0;
105        let mut camels = 0;
106        let mut horses = 0;
107        let mut dogs = 0;
108        let mut cats = 0;
109        let mut rabbits = 0;
110
111        for (row_idx, line) in lines.iter().enumerate() {
112            for (col_idx, charr) in line.chars().enumerate().filter(|(i, _)| i % 2 == 1).map(|(_, s)| s).enumerate() {
113                let idx = (row_idx * BOARD_WIDTH + col_idx) as u8;
114                let square = Square::from_index(idx);
115                if let Some((piece, is_p1)) = convert_char_to_piece(charr) {
116                    let square_bit = square.as_bit_board();
117
118                    match piece {
119                        Piece::Elephant => elephants |= square_bit,
120                        Piece::Camel => camels |= square_bit,
121                        Piece::Horse => horses |= square_bit,
122                        Piece::Dog => dogs |= square_bit,
123                        Piece::Cat => cats |= square_bit,
124                        Piece::Rabbit => rabbits |= square_bit,
125                    }
126
127                    if is_p1 {
128                        p1_pieces |= square_bit;
129                    }
130                }
131            }
132        }
133
134        let piece_board = PieceBoard::new(p1_pieces, elephants, camels, horses, dogs, cats, rabbits);
135
136        Ok(GameState::new(p1_turn_to_move, move_number, piece_board))
137    }
138}
139
140fn convert_char_to_piece(c: char) -> Option<(Piece, bool)> {
141    let is_p1 = c.is_uppercase();
142
143    let piece = match c {
144        'E' | 'e' => Some(Piece::Elephant),
145        'M' | 'm' => Some(Piece::Camel),
146        'H' | 'h' => Some(Piece::Horse),
147        'D' | 'd' => Some(Piece::Dog),
148        'C' | 'c' => Some(Piece::Cat),
149        'R' | 'r' => Some(Piece::Rabbit),
150        _ => None,
151    };
152
153    piece.map(|p| (p, is_p1))
154}
155
156fn is_p1_piece(square_bit: u64, piece_board: &PieceBoard) -> bool {
157    let p1_piece_mask = piece_board.get_player_piece_mask(true);
158    (square_bit & p1_piece_mask) != 0
159}
160
161pub fn convert_piece_to_letter(piece: &Piece, is_p1: bool) -> String {
162    let letter = match piece {
163        Piece::Elephant => "E",
164        Piece::Camel => "M",
165        Piece::Horse => "H",
166        Piece::Dog => "D",
167        Piece::Cat => "C",
168        Piece::Rabbit => "R",
169    };
170
171    if is_p1 { letter.to_string() } else { letter.to_lowercase() }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::super::{Piece, take_actions};
177    use super::*;
178
179    #[must_use]
180    fn place_gold_pieces(game_state: GameState) -> GameState {
181        take_actions!(game_state => e1, d1, a1, h1, c1, f1, b1, g1)
182    }
183
184    #[must_use]
185    fn place_silver_pieces(game_state: GameState) -> GameState {
186        take_actions!(game_state => e8, d8, a8, h8, c8, f8, b8, g8)
187    }
188
189    fn initial_play_state() -> GameState {
190        let game_state = GameState::initial();
191        let game_state = place_gold_pieces(game_state);
192        place_silver_pieces(game_state)
193    }
194
195    #[test]
196    fn test_gamestate_fromstr() {
197        let game_state: GameState = "
198            +-----------------+
199            8|   r   r r   r   |
200            7| m   h     e   c |
201            6|   r x r r x r   |
202            5| h   d     c   d |
203            4| E   H         M |
204            3|   R x R R H R   |
205            2| D   C     C   D |
206            1|   R   R R   R   |
207            +-----------------+
208               a b c d e f g h"
209            .parse()
210            .unwrap();
211
212        let piece_board = game_state.get_piece_board();
213        assert_eq!(
214            piece_board.get_bits_for_piece(Piece::Rabbit, true),
215            0b__01011010__00000000__01011010__00000000__00000000__00000000__00000000__00000000
216        );
217        assert_eq!(
218            piece_board.get_bits_for_piece(Piece::Cat, true),
219            0b__00000000__00100100__00000000__00000000__00000000__00000000__00000000__00000000
220        );
221        assert_eq!(
222            piece_board.get_bits_for_piece(Piece::Dog, true),
223            0b__00000000__10000001__00000000__00000000__00000000__00000000__00000000__00000000
224        );
225        assert_eq!(
226            piece_board.get_bits_for_piece(Piece::Horse, true),
227            0b__00000000__00000000__00100000__00000100__00000000__00000000__00000000__00000000
228        );
229        assert_eq!(
230            piece_board.get_bits_for_piece(Piece::Camel, true),
231            0b__00000000__00000000__00000000__10000000__00000000__00000000__00000000__00000000
232        );
233        assert_eq!(
234            piece_board.get_bits_for_piece(Piece::Elephant, true),
235            0b__00000000__00000000__00000000__00000001__00000000__00000000__00000000__00000000
236        );
237
238        assert_eq!(
239            piece_board.get_bits_for_piece(Piece::Rabbit, false),
240            0b__00000000__00000000__00000000__00000000__00000000__01011010__00000000__01011010
241        );
242        assert_eq!(
243            piece_board.get_bits_for_piece(Piece::Cat, false),
244            0b__00000000__00000000__00000000__00000000__00100000__00000000__10000000__00000000
245        );
246        assert_eq!(
247            piece_board.get_bits_for_piece(Piece::Dog, false),
248            0b__00000000__00000000__00000000__00000000__10000100__00000000__00000000__00000000
249        );
250        assert_eq!(
251            piece_board.get_bits_for_piece(Piece::Horse, false),
252            0b__00000000__00000000__00000000__00000000__00000001__00000000__00000100__00000000
253        );
254        assert_eq!(
255            piece_board.get_bits_for_piece(Piece::Camel, false),
256            0b__00000000__00000000__00000000__00000000__00000000__00000000__00000001__00000000
257        );
258        assert_eq!(
259            piece_board.get_bits_for_piece(Piece::Elephant, false),
260            0b__00000000__00000000__00000000__00000000__00000000__00000000__00100000__00000000
261        );
262    }
263
264    #[test]
265    fn test_gamestate_fromstr_move_number_default() {
266        let game_state: GameState = "
267            +-----------------+
268            8|   r   r r   r   |
269            7| m   h     e   c |
270            6|   r x r r x r   |
271            5| h   d     c   d |
272            4| E   H         M |
273            3|   R x R R H R   |
274            2| D   C     C   D |
275            1|   R   R R   R   |
276            +-----------------+
277               a b c d e f g h"
278            .parse()
279            .unwrap();
280
281        assert_eq!(game_state.get_move_number(), 2);
282        assert!(game_state.is_p1_turn_to_move());
283    }
284
285    #[test]
286    fn test_gamestate_fromstr_move_number() {
287        let game_state: GameState = "
288            5s
289            +-----------------+
290            8|   r   r r   r   |
291            7| m   h     e   c |
292            6|   r x r r x r   |
293            5| h   d     c   d |
294            4| E   H         M |
295            3|   R x R R H R   |
296            2| D   C     C   D |
297            1|   R   R R   R   |
298            +-----------------+
299               a b c d e f g h"
300            .parse()
301            .unwrap();
302
303        assert_eq!(game_state.get_move_number(), 5);
304        assert!(!game_state.is_p1_turn_to_move());
305    }
306
307    #[test]
308    fn test_gamestate_fromstr_player() {
309        let game_state: GameState = "
310            176b
311            +-----------------+
312            8|   r   r r   r   |
313            7| m   h     e   c |
314            6|   r x r r x r   |
315            5| h   d     c   d |
316            4| E   H         M |
317            3|   R x R R H R   |
318            2| D   C     C   D |
319            1|   R   R R   R   |
320            +-----------------+
321               a b c d e f g h"
322            .parse()
323            .unwrap();
324
325        assert_eq!(game_state.get_move_number(), 176);
326        assert!(!game_state.is_p1_turn_to_move());
327    }
328
329    #[test]
330    fn test_gamestate_fromstr_player_w() {
331        let game_state: GameState = "
332            13w
333            +-----------------+
334            8|   r   r r   r   |
335            7| m   h     e   c |
336            6|   r x r r x r   |
337            5| h   d     c   d |
338            4| E   H         M |
339            3|   R x R R H R   |
340            2| D   C     C   D |
341            1|   R   R R   R   |
342            +-----------------+
343               a b c d e f g h"
344            .parse()
345            .unwrap();
346
347        assert_eq!(game_state.get_move_number(), 13);
348        assert!(game_state.is_p1_turn_to_move());
349    }
350
351    #[test]
352    fn test_gamestate_to_str() {
353        let expected_output = "2g
354 +-----------------+
3558| h c d m e d c h |
3567| r r r r r r r r |
3576|     x     x     |
3585|                 |
3594|                 |
3603|     x     x     |
3612| R R R R R R R R |
3621| H C D M E D C H |
363 +-----------------+
364   a b c d e f g h
365";
366
367        assert_eq!(format!("{}", initial_play_state()), expected_output);
368    }
369
370    #[test]
371    fn test_gamestate_from_str_and_to_str() {
372        let orig_str = "14g
373 +-----------------+
3748|   r   r r   r   |
3757| m   h     e   c |
3766|   r x r r x r   |
3775| h   d     c   d |
3784| E   H         M |
3793|   R x R R x R   |
3802| D   C     C   D |
3811|   R   R R   R   |
382 +-----------------+
383   a b c d e f g h
384";
385
386        let game_state: GameState = orig_str.parse().unwrap();
387        let new_str = format!("{}", game_state);
388
389        assert_eq!(new_str, orig_str);
390    }
391
392    #[test]
393    fn test_gamestate_fromstr_compact_initial() {
394        // Compact AEI format equivalent of the initial play state
395        // Row 8→1: hcdmedch / rrrrrrrr / (empty x4) / RRRRRRRR / HCDMEDCH
396        let expected_output = "2g
397 +-----------------+
3988| h c d m e d c h |
3997| r r r r r r r r |
4006|     x     x     |
4015|                 |
4024|                 |
4033|     x     x     |
4042| R R R R R R R R |
4051| H C D M E D C H |
406 +-----------------+
407   a b c d e f g h
408";
409
410        let game_state: GameState = "2g [hcdmedchrrrrrrrr                                RRRRRRRRHCDMEDCH]"
411            .parse()
412            .unwrap();
413        assert_eq!(format!("{}", game_state), expected_output);
414    }
415
416    #[test]
417    fn test_gamestate_fromstr_compact_no_move_number() {
418        // "g [...]" with no move number should default move_number to 1, gold to move
419        let game_state: GameState = "g [rredchrrrhrdcmrr                                MHHEDDCRRRRRRRRC]"
420            .parse()
421            .unwrap();
422        assert_eq!(game_state.get_move_number(), 1);
423        assert!(game_state.is_p1_turn_to_move());
424        // Verify a sample of the piece placements
425        let piece_board = game_state.get_piece_board();
426        // Gold elephant should be at d2 (row index 6, col index 3 → square index 51)
427        assert_ne!(piece_board.get_bits_for_piece(Piece::Elephant, true), 0);
428        // Silver elephant should be at c8 (row index 0, col index 2 → square index 2)
429        assert_ne!(piece_board.get_bits_for_piece(Piece::Elephant, false), 0);
430    }
431
432    #[test]
433    fn test_gamestate_fromstr_compact_silver_to_move() {
434        let game_state: GameState = "5s [hcdmedchrrrrrrrr                                RRRRRRRRHCDMEDCH]"
435            .parse()
436            .unwrap();
437        assert_eq!(game_state.get_move_number(), 5);
438        assert!(!game_state.is_p1_turn_to_move());
439    }
440}