My attempt at writing a go (the board game) engine, in Rust
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: add Game<W,H> state management

- track to_move, captures, prev_board
- play() with suicide/ko/turn validation
- pass support

+254
+252
src/game.rs
··· 1 + use crate::board::Board; 2 + use crate::color::Color; 3 + use crate::coord::Coord; 4 + use crate::error::GoError; 5 + 6 + #[derive(Clone, Debug, PartialEq, Eq)] 7 + pub struct Game<const W: usize, const H: usize> { 8 + board: Board<W, H>, 9 + to_move: Color, 10 + captures: (u16, u16), 11 + prev_board: Option<Board<W, H>>, 12 + } 13 + 14 + impl<const W: usize, const H: usize> Game<W, H> { 15 + pub fn new() -> Self { 16 + Game { 17 + board: Board::new(), 18 + to_move: Color::Black, 19 + captures: (0, 0), 20 + prev_board: None, 21 + } 22 + } 23 + 24 + pub fn board(&self) -> &Board<W, H> { 25 + &self.board 26 + } 27 + 28 + pub fn to_move(&self) -> Color { 29 + self.to_move 30 + } 31 + 32 + pub fn captures(&self) -> (u16, u16) { 33 + self.captures 34 + } 35 + 36 + pub fn prev_board(&self) -> Option<&Board<W, H>> { 37 + self.prev_board.as_ref() 38 + } 39 + 40 + /// Play a stone at the given coordinate for the current player. 41 + /// Validates turn order, occupancy, suicide, and simple ko. 42 + pub fn play(&mut self, coord: Coord<W, H>) -> Result<(), GoError> { 43 + // Check occupancy 44 + if !self.board.is_empty(coord) { 45 + return Err(GoError::Occupied); 46 + } 47 + 48 + // Save current board for ko check 49 + let board_before = self.board.clone(); 50 + 51 + // Place the stone and get captures 52 + let captured = self.board.place(coord, self.to_move)?; 53 + 54 + // Suicide check: if no captures and the new stone has no liberties 55 + if captured.is_empty() { 56 + let group = self.board.group_at(coord).ok_or(GoError::Suicide)?; 57 + if group.liberty_count() == 0 { 58 + // Revert 59 + self.board = board_before; 60 + return Err(GoError::Suicide); 61 + } 62 + } 63 + 64 + // Ko check: if exactly one stone captured and board matches previous state 65 + if captured.len() == 1 66 + && let Some(ref prev) = self.prev_board 67 + && self.board == *prev 68 + { 69 + // Revert 70 + self.board = board_before; 71 + return Err(GoError::Ko); 72 + } 73 + 74 + // Update captures 75 + let capture_count = captured.len() as u16; 76 + match self.to_move { 77 + Color::Black => self.captures.0 += capture_count, 78 + Color::White => self.captures.1 += capture_count, 79 + } 80 + 81 + // Update state 82 + self.prev_board = Some(board_before); 83 + self.to_move = self.to_move.opposite(); 84 + 85 + Ok(()) 86 + } 87 + 88 + /// Pass the current turn. 89 + pub fn pass(&mut self) { 90 + self.prev_board = Some(self.board.clone()); 91 + self.to_move = self.to_move.opposite(); 92 + } 93 + } 94 + 95 + impl<const W: usize, const H: usize> Default for Game<W, H> { 96 + fn default() -> Self { 97 + Self::new() 98 + } 99 + } 100 + 101 + #[cfg(test)] 102 + mod tests { 103 + use super::*; 104 + 105 + fn c5(x: u8, y: u8) -> Coord<5, 5> { 106 + Coord::try_from((x, y)).unwrap() 107 + } 108 + 109 + fn c3(x: u8, y: u8) -> Coord<3, 3> { 110 + Coord::try_from((x, y)).unwrap() 111 + } 112 + 113 + #[test] 114 + fn new_game_starts_with_black() { 115 + let game = Game::<5, 5>::new(); 116 + assert_eq!(game.to_move(), Color::Black); 117 + assert_eq!(game.captures(), (0, 0)); 118 + assert!(game.prev_board().is_none()); 119 + } 120 + 121 + #[test] 122 + fn play_switches_turn() { 123 + let mut game = Game::<5, 5>::new(); 124 + game.play(c5(2, 2)).unwrap(); 125 + assert_eq!(game.to_move(), Color::White); 126 + game.play(c5(3, 3)).unwrap(); 127 + assert_eq!(game.to_move(), Color::Black); 128 + } 129 + 130 + #[test] 131 + fn play_occupied() { 132 + let mut game = Game::<5, 5>::new(); 133 + game.play(c5(2, 2)).unwrap(); 134 + assert_eq!(game.play(c5(2, 2)), Err(GoError::Occupied)); 135 + } 136 + 137 + #[test] 138 + fn play_tracks_captures() { 139 + let mut game = Game::<5, 5>::new(); 140 + // Black surrounds and captures white at (1,1) 141 + game.play(c5(0, 1)).unwrap(); // B 142 + game.play(c5(1, 1)).unwrap(); // W 143 + game.play(c5(1, 0)).unwrap(); // B 144 + game.play(c5(4, 4)).unwrap(); // W 145 + game.play(c5(2, 1)).unwrap(); // B 146 + game.play(c5(4, 3)).unwrap(); // W 147 + game.play(c5(1, 2)).unwrap(); // B captures W at (1,1) 148 + assert_eq!(game.captures(), (1, 0)); 149 + assert!(game.board().is_empty(c5(1, 1))); 150 + } 151 + 152 + #[test] 153 + fn suicide_is_rejected() { 154 + let mut game = Game::<5, 5>::new(); 155 + // Black surrounds white at (1,1), captures it 156 + game.play(c5(0, 1)).unwrap(); // B 157 + game.play(c5(4, 4)).unwrap(); // W 158 + game.play(c5(1, 0)).unwrap(); // B 159 + game.play(c5(4, 3)).unwrap(); // W 160 + game.play(c5(2, 1)).unwrap(); // B 161 + game.play(c5(4, 2)).unwrap(); // W 162 + game.play(c5(1, 2)).unwrap(); // B captures W at (1,1) 163 + // Now it's W's turn; playing at (1,1) is suicide (empty but no liberties) 164 + assert_eq!(game.play(c5(1, 1)), Err(GoError::Suicide)); 165 + } 166 + 167 + #[test] 168 + fn simple_ko_is_rejected() { 169 + // Ko on 5x5: B plays (2,1) capturing W at (3,1). 170 + // W cannot immediately recapture at (3,1). 171 + // Setup around (2,1)-(3,1): 172 + // . W . 173 + // B W B 174 + // . W . 175 + // with B also at (4,1) to give W(3,1) only one liberty (at 2,1) 176 + let mut game = Game::<5, 5>::new(); 177 + game.play(c5(3, 0)).unwrap(); // B 178 + game.play(c5(3, 1)).unwrap(); // W 179 + game.play(c5(3, 2)).unwrap(); // B 180 + game.play(c5(1, 1)).unwrap(); // W 181 + game.play(c5(4, 1)).unwrap(); // B 182 + game.play(c5(2, 0)).unwrap(); // W 183 + game.play(c5(0, 0)).unwrap(); // B 184 + game.play(c5(2, 2)).unwrap(); // W 185 + game.play(c5(0, 1)).unwrap(); // B 186 + game.play(c5(0, 2)).unwrap(); // W 187 + game.play(c5(2, 1)).unwrap(); // B captures W at (3,1) 188 + // White tries immediate recapture at (3,1) - ko 189 + assert_eq!(game.play(c5(3, 1)), Err(GoError::Ko)); 190 + } 191 + 192 + #[test] 193 + fn ko_resolved_after_intervening_move() { 194 + let mut game = Game::<5, 5>::new(); 195 + game.play(c5(3, 0)).unwrap(); // B 196 + game.play(c5(3, 1)).unwrap(); // W 197 + game.play(c5(3, 2)).unwrap(); // B 198 + game.play(c5(1, 1)).unwrap(); // W 199 + game.play(c5(4, 1)).unwrap(); // B 200 + game.play(c5(2, 0)).unwrap(); // W 201 + game.play(c5(0, 0)).unwrap(); // B 202 + game.play(c5(2, 2)).unwrap(); // W 203 + game.play(c5(0, 1)).unwrap(); // B 204 + game.play(c5(0, 2)).unwrap(); // W 205 + game.play(c5(2, 1)).unwrap(); // B captures W at (3,1) 206 + // Ko: white cannot recapture immediately 207 + assert_eq!(game.play(c5(3, 1)), Err(GoError::Ko)); 208 + // White plays elsewhere 209 + game.play(c5(4, 4)).unwrap(); // W 210 + // Black plays elsewhere 211 + game.play(c5(0, 3)).unwrap(); // B 212 + // Now white can recapture 213 + assert!(game.play(c5(3, 1)).is_ok()); 214 + } 215 + 216 + #[test] 217 + fn pass_switches_turn() { 218 + let mut game = Game::<5, 5>::new(); 219 + assert_eq!(game.to_move(), Color::Black); 220 + game.pass(); 221 + assert_eq!(game.to_move(), Color::White); 222 + game.pass(); 223 + assert_eq!(game.to_move(), Color::Black); 224 + } 225 + 226 + #[test] 227 + fn pass_saves_board_state() { 228 + let mut game = Game::<3, 3>::new(); 229 + game.play(c3(0, 0)).unwrap(); // B 230 + game.pass(); // W pass saves board after B's move 231 + assert!(game.prev_board().is_some()); 232 + assert!(!game.prev_board().unwrap().is_empty(c3(0, 0))); 233 + } 234 + 235 + #[test] 236 + fn suicide_in_corner() { 237 + let mut game = Game::<3, 3>::new(); 238 + // Black occupies (0,1) and (1,0) 239 + game.play(c3(0, 1)).unwrap(); // B 240 + game.play(c3(2, 2)).unwrap(); // W 241 + game.play(c3(1, 0)).unwrap(); // B 242 + // Now it's W's turn; (0,0) is surrounded by black and edge 243 + assert_eq!(game.play(c3(0, 0)), Err(GoError::Suicide)); 244 + } 245 + 246 + #[test] 247 + fn play_updates_board() { 248 + let mut game = Game::<5, 5>::new(); 249 + game.play(c5(2, 2)).unwrap(); 250 + assert_eq!(game.board().get(c5(2, 2)), Some(Color::Black)); 251 + } 252 + }
+2
src/lib.rs
··· 2 2 pub mod color; 3 3 pub mod coord; 4 4 pub mod error; 5 + pub mod game; 5 6 pub mod group; 6 7 7 8 pub use board::Board; 8 9 pub use color::Color; 9 10 pub use coord::Coord; 10 11 pub use error::GoError; 12 + pub use game::Game; 11 13 pub use group::Group;