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 group finding and liberty counting

- add Group<W,H> struct with stones and liberties
- implement Board::group_at() via BFS flood-fill
- add tests for single stones, groups, shared liberties, rings
- add inline comments to group algorithm and tests

+274
+73
src/board.rs
··· 1 1 use crate::color::Color; 2 2 use crate::coord::Coord; 3 3 use crate::error::GoError; 4 + use crate::group::Group; 4 5 5 6 #[derive(Clone, Debug, PartialEq, Eq, Hash)] 6 7 pub struct Board<const W: usize, const H: usize> { ··· 41 42 pub fn is_empty(&self, coord: Coord<W, H>) -> bool { 42 43 self.get(coord).is_none() 43 44 } 45 + 46 + /// Find the connected group (stones + liberties) at the given coordinate. 47 + /// Returns `None` if the coordinate is empty. 48 + pub fn group_at(&self, coord: Coord<W, H>) -> Option<Group<W, H>> { 49 + // Abort if the starting point has no stone. 50 + let color = self.get(coord)?; 51 + 52 + // Tracking sets to avoid revisiting squares. 53 + let mut stone_visited = vec![false; W * H]; 54 + let mut liberty_visited = vec![false; W * H]; 55 + // Accumulators for the result. 56 + let mut stones = Vec::new(); 57 + let mut liberties = Vec::new(); 58 + // BFS frontier. 59 + let mut queue = Vec::new(); 60 + 61 + // Seed the search with the starting stone. 62 + let start_idx = Self::index_of(coord); 63 + stone_visited[start_idx] = true; 64 + queue.push(coord); 65 + 66 + // Explore all reachable stones of the same color. 67 + while let Some(current) = queue.pop() { 68 + stones.push(current); 69 + 70 + // Look at the four orthogonal neighbors. 71 + let x = current.x() as i8; 72 + let y = current.y() as i8; 73 + let neighbors = [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)]; 74 + 75 + for (nx, ny) in neighbors { 76 + let nx = nx as u8; 77 + let ny = ny as u8; 78 + let Ok(neighbor) = Coord::<W, H>::try_from((nx, ny)) else { 79 + continue; // skip out of bounds 80 + }; 81 + let nidx = Self::index_of(neighbor); 82 + match self.get(neighbor) { 83 + // Same color -> part of the group, enqueue if unseen. 84 + Some(c) if c == color => { 85 + if !stone_visited[nidx] { 86 + stone_visited[nidx] = true; 87 + queue.push(neighbor); 88 + } 89 + } 90 + // Empty point -> liberty, record if unseen. 91 + None => { 92 + if !liberty_visited[nidx] { 93 + liberty_visited[nidx] = true; 94 + liberties.push(neighbor); 95 + } 96 + } 97 + // Opposite color -> ignore. 98 + _ => {} 99 + } 100 + } 101 + } 102 + 103 + Some(Group::new(color, stones, liberties)) 104 + } 44 105 } 45 106 46 107 impl<const W: usize, const H: usize> Default for Board<W, H> { ··· 55 116 56 117 #[test] 57 118 fn new_board_is_empty() { 119 + // Fresh board has no stones on any intersection. 58 120 let board = Board::<3, 3>::new(); 59 121 for x in 0..3 { 60 122 for y in 0..3 { ··· 67 129 68 130 #[test] 69 131 fn place_and_get() { 132 + // Placing a stone makes it retrievable. 70 133 let mut board = Board::<5, 5>::new(); 71 134 let coord: Coord<5, 5> = (2, 3).try_into().unwrap(); 72 135 ··· 77 140 78 141 #[test] 79 142 fn place_occupied() { 143 + // Placing on an occupied point fails with Occupied error. 80 144 let mut board = Board::<5, 5>::new(); 81 145 let coord: Coord<5, 5> = (1, 1).try_into().unwrap(); 82 146 ··· 86 150 87 151 #[test] 88 152 fn remove_stone() { 153 + // Removing a stone clears the intersection. 89 154 let mut board = Board::<5, 5>::new(); 90 155 let coord: Coord<5, 5> = (0, 0).try_into().unwrap(); 91 156 ··· 99 164 100 165 #[test] 101 166 fn multiple_stones() { 167 + // Multiple placements on different coordinates are independent. 102 168 let mut board = Board::<9, 9>::new(); 103 169 let a: Coord<9, 9> = (0, 0).try_into().unwrap(); 104 170 let b: Coord<9, 9> = (8, 8).try_into().unwrap(); ··· 115 181 116 182 #[test] 117 183 fn default_is_empty() { 184 + // Board created via Default is fully empty. 118 185 let board = Board::<5, 5>::default(); 119 186 let coord: Coord<5, 5> = (2, 2).try_into().unwrap(); 120 187 assert!(board.is_empty(coord)); ··· 135 202 proptest! { 136 203 #[test] 137 204 fn new_board_is_empty((x, y) in (0u8..5, 0u8..5)) { 205 + // Property: every coordinate on a new board is empty. 138 206 let board = Board::<5, 5>::new(); 139 207 let coord: Coord<5, 5> = (x, y).try_into().unwrap(); 140 208 prop_assert!(board.is_empty(coord)); ··· 143 211 144 212 #[test] 145 213 fn place_then_get(coord in coord_5x5(), c in color()) { 214 + // Property: placing a stone makes get return it. 146 215 let mut board = Board::<5, 5>::new(); 147 216 prop_assert!(board.place(coord, c).is_ok()); 148 217 prop_assert_eq!(board.get(coord), Some(c)); ··· 151 220 152 221 #[test] 153 222 fn place_occupied(coord in coord_5x5()) { 223 + // Property: placing on an occupied point always fails. 154 224 let mut board = Board::<5, 5>::new(); 155 225 board.place(coord, Color::Black).unwrap(); 156 226 prop_assert_eq!(board.place(coord, Color::White), Err(GoError::Occupied)); ··· 158 228 159 229 #[test] 160 230 fn place_remove_then_empty(coord in coord_5x5(), c in color()) { 231 + // Property: removing a placed stone restores emptiness. 161 232 let mut board = Board::<5, 5>::new(); 162 233 board.place(coord, c).unwrap(); 163 234 board.remove(coord); ··· 172 243 col1 in color(), 173 244 col2 in color(), 174 245 ) { 246 + // Property: distinct coordinates hold independent values. 175 247 prop_assume!(c1 != c2); 176 248 let mut board = Board::<5, 5>::new(); 177 249 board.place(c1, col1).unwrap(); ··· 182 254 183 255 #[test] 184 256 fn is_empty_iff_get_is_none(coord in coord_5x5(), c in color()) { 257 + // Property: is_empty is equivalent to get returning None. 185 258 let mut board = Board::<5, 5>::new(); 186 259 prop_assert_eq!(board.is_empty(coord), board.get(coord).is_none()); 187 260 board.place(coord, c).unwrap();
+199
src/group.rs
··· 1 + use crate::{Color, Coord}; 2 + 3 + #[derive(Clone, Debug, PartialEq, Eq)] 4 + pub struct Group<const W: usize, const H: usize> { 5 + color: Color, 6 + stones: Vec<Coord<W, H>>, 7 + liberties: Vec<Coord<W, H>>, 8 + } 9 + 10 + impl<const W: usize, const H: usize> Group<W, H> { 11 + pub(crate) fn new(color: Color, stones: Vec<Coord<W, H>>, liberties: Vec<Coord<W, H>>) -> Self { 12 + Self { 13 + color, 14 + stones, 15 + liberties, 16 + } 17 + } 18 + } 19 + 20 + impl<const W: usize, const H: usize> Group<W, H> { 21 + pub fn color(&self) -> Color { 22 + self.color 23 + } 24 + 25 + pub fn stones(&self) -> &[Coord<W, H>] { 26 + &self.stones 27 + } 28 + 29 + pub fn liberties(&self) -> &[Coord<W, H>] { 30 + &self.liberties 31 + } 32 + 33 + pub fn liberty_count(&self) -> usize { 34 + self.liberties.len() 35 + } 36 + } 37 + 38 + #[cfg(test)] 39 + mod tests { 40 + use super::*; 41 + use crate::Board; 42 + 43 + fn c5(x: u8, y: u8) -> Coord<5, 5> { 44 + Coord::try_from((x, y)).unwrap() 45 + } 46 + 47 + fn c3(x: u8, y: u8) -> Coord<3, 3> { 48 + Coord::try_from((x, y)).unwrap() 49 + } 50 + 51 + #[test] 52 + fn group_at_empty() { 53 + // Querying an empty intersection returns None. 54 + let board = Board::<5, 5>::new(); 55 + assert_eq!(board.group_at(c5(2, 2)), None); 56 + } 57 + 58 + #[test] 59 + fn single_stone_center() { 60 + // A lone stone in the center has 4 liberties and a 1-stone group. 61 + let mut board = Board::<5, 5>::new(); 62 + board.place(c5(2, 2), Color::Black).unwrap(); 63 + 64 + let group = board.group_at(c5(2, 2)).unwrap(); 65 + assert_eq!(group.color(), Color::Black); 66 + assert_eq!(group.stones().len(), 1); 67 + assert!(group.stones().contains(&c5(2, 2))); 68 + assert_eq!(group.liberty_count(), 4); 69 + } 70 + 71 + #[test] 72 + fn single_stone_corner() { 73 + // A corner stone has only 2 liberties. 74 + let mut board = Board::<5, 5>::new(); 75 + board.place(c5(0, 0), Color::Black).unwrap(); 76 + 77 + let group = board.group_at(c5(0, 0)).unwrap(); 78 + assert_eq!(group.liberty_count(), 2); 79 + } 80 + 81 + #[test] 82 + fn single_stone_edge() { 83 + // An edge stone has 3 liberties. 84 + let mut board = Board::<5, 5>::new(); 85 + board.place(c5(0, 2), Color::Black).unwrap(); 86 + 87 + let group = board.group_at(c5(0, 2)).unwrap(); 88 + assert_eq!(group.liberty_count(), 3); 89 + } 90 + 91 + #[test] 92 + fn two_stone_group() { 93 + // Orthogonally adjacent stones of the same color form one group. 94 + let mut board = Board::<5, 5>::new(); 95 + board.place(c5(2, 2), Color::Black).unwrap(); 96 + board.place(c5(2, 3), Color::Black).unwrap(); 97 + 98 + let group = board.group_at(c5(2, 2)).unwrap(); 99 + assert_eq!(group.color(), Color::Black); 100 + assert_eq!(group.stones().len(), 2); 101 + assert!(group.stones().contains(&c5(2, 2))); 102 + assert!(group.stones().contains(&c5(2, 3))); 103 + } 104 + 105 + #[test] 106 + fn two_stone_group_liberties() { 107 + // Two adjacent stones share 6 unique liberties (4+4-2). 108 + let mut board = Board::<5, 5>::new(); 109 + board.place(c5(2, 2), Color::Black).unwrap(); 110 + board.place(c5(2, 3), Color::Black).unwrap(); 111 + 112 + let group = board.group_at(c5(2, 2)).unwrap(); 113 + assert_eq!(group.liberty_count(), 6); 114 + } 115 + 116 + #[test] 117 + fn diagonal_not_connected() { 118 + // Diagonal stones are not connected and form separate groups. 119 + let mut board = Board::<5, 5>::new(); 120 + board.place(c5(2, 2), Color::Black).unwrap(); 121 + board.place(c5(3, 3), Color::Black).unwrap(); 122 + 123 + let group1 = board.group_at(c5(2, 2)).unwrap(); 124 + let group2 = board.group_at(c5(3, 3)).unwrap(); 125 + assert_eq!(group1.stones().len(), 1); 126 + assert_eq!(group2.stones().len(), 1); 127 + } 128 + 129 + #[test] 130 + fn different_colors_not_connected() { 131 + // Adjacent stones of different colors do not join the same group. 132 + let mut board = Board::<5, 5>::new(); 133 + board.place(c5(2, 2), Color::Black).unwrap(); 134 + board.place(c5(2, 3), Color::White).unwrap(); 135 + 136 + let black_group = board.group_at(c5(2, 2)).unwrap(); 137 + let white_group = board.group_at(c5(2, 3)).unwrap(); 138 + assert_eq!(black_group.color(), Color::Black); 139 + assert_eq!(white_group.color(), Color::White); 140 + assert_eq!(black_group.stones().len(), 1); 141 + assert_eq!(white_group.stones().len(), 1); 142 + } 143 + 144 + #[test] 145 + fn surrounded_single_stone_no_liberties() { 146 + // A stone surrounded on all 4 sides has 0 liberties. 147 + let mut board = Board::<3, 3>::new(); 148 + board.place(c3(1, 1), Color::Black).unwrap(); 149 + board.place(c3(0, 1), Color::White).unwrap(); 150 + board.place(c3(2, 1), Color::White).unwrap(); 151 + board.place(c3(1, 0), Color::White).unwrap(); 152 + board.place(c3(1, 2), Color::White).unwrap(); 153 + 154 + let group = board.group_at(c3(1, 1)).unwrap(); 155 + assert_eq!(group.liberty_count(), 0); 156 + } 157 + 158 + #[test] 159 + fn group_shares_liberties() { 160 + // Two horizontally adjacent stones share no liberties; total is 6. 161 + let mut board = Board::<5, 5>::new(); 162 + board.place(c5(2, 2), Color::Black).unwrap(); 163 + board.place(c5(3, 2), Color::Black).unwrap(); 164 + 165 + let group = board.group_at(c5(2, 2)).unwrap(); 166 + assert_eq!(group.liberty_count(), 6); 167 + } 168 + 169 + #[test] 170 + fn l_shaped_group() { 171 + // An L-shaped group of 3 stones counts 7 unique liberties. 172 + let mut board = Board::<5, 5>::new(); 173 + board.place(c5(2, 2), Color::Black).unwrap(); 174 + board.place(c5(3, 2), Color::Black).unwrap(); 175 + board.place(c5(2, 3), Color::Black).unwrap(); 176 + 177 + let group = board.group_at(c5(2, 2)).unwrap(); 178 + assert_eq!(group.stones().len(), 3); 179 + assert_eq!(group.liberty_count(), 7); 180 + } 181 + 182 + #[test] 183 + fn ring_group_internal_liberties() { 184 + // A ring encloses an empty point, which counts as a liberty. 185 + let mut board = Board::<5, 5>::new(); 186 + board.place(c5(1, 1), Color::Black).unwrap(); 187 + board.place(c5(2, 1), Color::Black).unwrap(); 188 + board.place(c5(3, 1), Color::Black).unwrap(); 189 + board.place(c5(1, 2), Color::Black).unwrap(); 190 + board.place(c5(3, 2), Color::Black).unwrap(); 191 + board.place(c5(1, 3), Color::Black).unwrap(); 192 + board.place(c5(2, 3), Color::Black).unwrap(); 193 + board.place(c5(3, 3), Color::Black).unwrap(); 194 + 195 + let group = board.group_at(c5(1, 1)).unwrap(); 196 + assert_eq!(group.stones().len(), 8); 197 + assert!(group.liberties().contains(&c5(2, 2))); 198 + } 199 + }
+2
src/lib.rs
··· 2 2 pub mod color; 3 3 pub mod coord; 4 4 pub mod error; 5 + pub mod group; 5 6 6 7 pub use board::Board; 7 8 pub use color::Color; 8 9 pub use coord::Coord; 9 10 pub use error::GoError; 11 + pub use group::Group;