···11+use crate::board::Board;
22+use crate::color::Color;
33+use crate::coord::Coord;
44+use crate::error::GoError;
55+66+#[derive(Clone, Debug, PartialEq, Eq)]
77+pub struct Game<const W: usize, const H: usize> {
88+ board: Board<W, H>,
99+ to_move: Color,
1010+ captures: (u16, u16),
1111+ prev_board: Option<Board<W, H>>,
1212+}
1313+1414+impl<const W: usize, const H: usize> Game<W, H> {
1515+ pub fn new() -> Self {
1616+ Game {
1717+ board: Board::new(),
1818+ to_move: Color::Black,
1919+ captures: (0, 0),
2020+ prev_board: None,
2121+ }
2222+ }
2323+2424+ pub fn board(&self) -> &Board<W, H> {
2525+ &self.board
2626+ }
2727+2828+ pub fn to_move(&self) -> Color {
2929+ self.to_move
3030+ }
3131+3232+ pub fn captures(&self) -> (u16, u16) {
3333+ self.captures
3434+ }
3535+3636+ pub fn prev_board(&self) -> Option<&Board<W, H>> {
3737+ self.prev_board.as_ref()
3838+ }
3939+4040+ /// Play a stone at the given coordinate for the current player.
4141+ /// Validates turn order, occupancy, suicide, and simple ko.
4242+ pub fn play(&mut self, coord: Coord<W, H>) -> Result<(), GoError> {
4343+ // Check occupancy
4444+ if !self.board.is_empty(coord) {
4545+ return Err(GoError::Occupied);
4646+ }
4747+4848+ // Save current board for ko check
4949+ let board_before = self.board.clone();
5050+5151+ // Place the stone and get captures
5252+ let captured = self.board.place(coord, self.to_move)?;
5353+5454+ // Suicide check: if no captures and the new stone has no liberties
5555+ if captured.is_empty() {
5656+ let group = self.board.group_at(coord).ok_or(GoError::Suicide)?;
5757+ if group.liberty_count() == 0 {
5858+ // Revert
5959+ self.board = board_before;
6060+ return Err(GoError::Suicide);
6161+ }
6262+ }
6363+6464+ // Ko check: if exactly one stone captured and board matches previous state
6565+ if captured.len() == 1
6666+ && let Some(ref prev) = self.prev_board
6767+ && self.board == *prev
6868+ {
6969+ // Revert
7070+ self.board = board_before;
7171+ return Err(GoError::Ko);
7272+ }
7373+7474+ // Update captures
7575+ let capture_count = captured.len() as u16;
7676+ match self.to_move {
7777+ Color::Black => self.captures.0 += capture_count,
7878+ Color::White => self.captures.1 += capture_count,
7979+ }
8080+8181+ // Update state
8282+ self.prev_board = Some(board_before);
8383+ self.to_move = self.to_move.opposite();
8484+8585+ Ok(())
8686+ }
8787+8888+ /// Pass the current turn.
8989+ pub fn pass(&mut self) {
9090+ self.prev_board = Some(self.board.clone());
9191+ self.to_move = self.to_move.opposite();
9292+ }
9393+}
9494+9595+impl<const W: usize, const H: usize> Default for Game<W, H> {
9696+ fn default() -> Self {
9797+ Self::new()
9898+ }
9999+}
100100+101101+#[cfg(test)]
102102+mod tests {
103103+ use super::*;
104104+105105+ fn c5(x: u8, y: u8) -> Coord<5, 5> {
106106+ Coord::try_from((x, y)).unwrap()
107107+ }
108108+109109+ fn c3(x: u8, y: u8) -> Coord<3, 3> {
110110+ Coord::try_from((x, y)).unwrap()
111111+ }
112112+113113+ #[test]
114114+ fn new_game_starts_with_black() {
115115+ let game = Game::<5, 5>::new();
116116+ assert_eq!(game.to_move(), Color::Black);
117117+ assert_eq!(game.captures(), (0, 0));
118118+ assert!(game.prev_board().is_none());
119119+ }
120120+121121+ #[test]
122122+ fn play_switches_turn() {
123123+ let mut game = Game::<5, 5>::new();
124124+ game.play(c5(2, 2)).unwrap();
125125+ assert_eq!(game.to_move(), Color::White);
126126+ game.play(c5(3, 3)).unwrap();
127127+ assert_eq!(game.to_move(), Color::Black);
128128+ }
129129+130130+ #[test]
131131+ fn play_occupied() {
132132+ let mut game = Game::<5, 5>::new();
133133+ game.play(c5(2, 2)).unwrap();
134134+ assert_eq!(game.play(c5(2, 2)), Err(GoError::Occupied));
135135+ }
136136+137137+ #[test]
138138+ fn play_tracks_captures() {
139139+ let mut game = Game::<5, 5>::new();
140140+ // Black surrounds and captures white at (1,1)
141141+ game.play(c5(0, 1)).unwrap(); // B
142142+ game.play(c5(1, 1)).unwrap(); // W
143143+ game.play(c5(1, 0)).unwrap(); // B
144144+ game.play(c5(4, 4)).unwrap(); // W
145145+ game.play(c5(2, 1)).unwrap(); // B
146146+ game.play(c5(4, 3)).unwrap(); // W
147147+ game.play(c5(1, 2)).unwrap(); // B captures W at (1,1)
148148+ assert_eq!(game.captures(), (1, 0));
149149+ assert!(game.board().is_empty(c5(1, 1)));
150150+ }
151151+152152+ #[test]
153153+ fn suicide_is_rejected() {
154154+ let mut game = Game::<5, 5>::new();
155155+ // Black surrounds white at (1,1), captures it
156156+ game.play(c5(0, 1)).unwrap(); // B
157157+ game.play(c5(4, 4)).unwrap(); // W
158158+ game.play(c5(1, 0)).unwrap(); // B
159159+ game.play(c5(4, 3)).unwrap(); // W
160160+ game.play(c5(2, 1)).unwrap(); // B
161161+ game.play(c5(4, 2)).unwrap(); // W
162162+ game.play(c5(1, 2)).unwrap(); // B captures W at (1,1)
163163+ // Now it's W's turn; playing at (1,1) is suicide (empty but no liberties)
164164+ assert_eq!(game.play(c5(1, 1)), Err(GoError::Suicide));
165165+ }
166166+167167+ #[test]
168168+ fn simple_ko_is_rejected() {
169169+ // Ko on 5x5: B plays (2,1) capturing W at (3,1).
170170+ // W cannot immediately recapture at (3,1).
171171+ // Setup around (2,1)-(3,1):
172172+ // . W .
173173+ // B W B
174174+ // . W .
175175+ // with B also at (4,1) to give W(3,1) only one liberty (at 2,1)
176176+ let mut game = Game::<5, 5>::new();
177177+ game.play(c5(3, 0)).unwrap(); // B
178178+ game.play(c5(3, 1)).unwrap(); // W
179179+ game.play(c5(3, 2)).unwrap(); // B
180180+ game.play(c5(1, 1)).unwrap(); // W
181181+ game.play(c5(4, 1)).unwrap(); // B
182182+ game.play(c5(2, 0)).unwrap(); // W
183183+ game.play(c5(0, 0)).unwrap(); // B
184184+ game.play(c5(2, 2)).unwrap(); // W
185185+ game.play(c5(0, 1)).unwrap(); // B
186186+ game.play(c5(0, 2)).unwrap(); // W
187187+ game.play(c5(2, 1)).unwrap(); // B captures W at (3,1)
188188+ // White tries immediate recapture at (3,1) - ko
189189+ assert_eq!(game.play(c5(3, 1)), Err(GoError::Ko));
190190+ }
191191+192192+ #[test]
193193+ fn ko_resolved_after_intervening_move() {
194194+ let mut game = Game::<5, 5>::new();
195195+ game.play(c5(3, 0)).unwrap(); // B
196196+ game.play(c5(3, 1)).unwrap(); // W
197197+ game.play(c5(3, 2)).unwrap(); // B
198198+ game.play(c5(1, 1)).unwrap(); // W
199199+ game.play(c5(4, 1)).unwrap(); // B
200200+ game.play(c5(2, 0)).unwrap(); // W
201201+ game.play(c5(0, 0)).unwrap(); // B
202202+ game.play(c5(2, 2)).unwrap(); // W
203203+ game.play(c5(0, 1)).unwrap(); // B
204204+ game.play(c5(0, 2)).unwrap(); // W
205205+ game.play(c5(2, 1)).unwrap(); // B captures W at (3,1)
206206+ // Ko: white cannot recapture immediately
207207+ assert_eq!(game.play(c5(3, 1)), Err(GoError::Ko));
208208+ // White plays elsewhere
209209+ game.play(c5(4, 4)).unwrap(); // W
210210+ // Black plays elsewhere
211211+ game.play(c5(0, 3)).unwrap(); // B
212212+ // Now white can recapture
213213+ assert!(game.play(c5(3, 1)).is_ok());
214214+ }
215215+216216+ #[test]
217217+ fn pass_switches_turn() {
218218+ let mut game = Game::<5, 5>::new();
219219+ assert_eq!(game.to_move(), Color::Black);
220220+ game.pass();
221221+ assert_eq!(game.to_move(), Color::White);
222222+ game.pass();
223223+ assert_eq!(game.to_move(), Color::Black);
224224+ }
225225+226226+ #[test]
227227+ fn pass_saves_board_state() {
228228+ let mut game = Game::<3, 3>::new();
229229+ game.play(c3(0, 0)).unwrap(); // B
230230+ game.pass(); // W pass saves board after B's move
231231+ assert!(game.prev_board().is_some());
232232+ assert!(!game.prev_board().unwrap().is_empty(c3(0, 0)));
233233+ }
234234+235235+ #[test]
236236+ fn suicide_in_corner() {
237237+ let mut game = Game::<3, 3>::new();
238238+ // Black occupies (0,1) and (1,0)
239239+ game.play(c3(0, 1)).unwrap(); // B
240240+ game.play(c3(2, 2)).unwrap(); // W
241241+ game.play(c3(1, 0)).unwrap(); // B
242242+ // Now it's W's turn; (0,0) is surrounded by black and edge
243243+ assert_eq!(game.play(c3(0, 0)), Err(GoError::Suicide));
244244+ }
245245+246246+ #[test]
247247+ fn play_updates_board() {
248248+ let mut game = Game::<5, 5>::new();
249249+ game.play(c5(2, 2)).unwrap();
250250+ assert_eq!(game.board().get(c5(2, 2)), Some(Color::Black));
251251+ }
252252+}
+2
src/lib.rs
···22pub mod color;
33pub mod coord;
44pub mod error;
55+pub mod game;
56pub mod group;
6778pub use board::Board;
89pub use color::Color;
910pub use coord::Coord;
1011pub use error::GoError;
1212+pub use game::Game;
1113pub use group::Group;