From 06157604b9b42b8c6925631f4f3de63abafd8ff1 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Wed, 3 May 2023 22:22:22 -0400 Subject: [PATCH] Implemented self_capture and ko rules --- kifu/kifu-core/Cargo.lock | 34 +++++++- kifu/kifu-core/Cargo.toml | 5 +- kifu/kifu-core/src/board.rs | 165 +++++++++++++++++++++++++++--------- kifu/kifu-core/src/lib.rs | 2 +- kifu/kifu-core/src/types.rs | 88 ++++++++++++++++++- kifu/kifu-gtk/Cargo.lock | 1 + 6 files changed, 248 insertions(+), 47 deletions(-) diff --git a/kifu/kifu-core/Cargo.lock b/kifu/kifu-core/Cargo.lock index f723d62..6880834 100644 --- a/kifu/kifu-core/Cargo.lock +++ b/kifu/kifu-core/Cargo.lock @@ -49,6 +49,7 @@ name = "kifu-core" version = "0.1.0" dependencies = [ "grid", + "thiserror", "tokio", ] @@ -209,6 +210,37 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.12", +] + [[package]] name = "tokio" version = "1.26.0" @@ -237,7 +269,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 1.0.109", ] [[package]] diff --git a/kifu/kifu-core/Cargo.toml b/kifu/kifu-core/Cargo.toml index 8287207..f7c8c30 100644 --- a/kifu/kifu-core/Cargo.toml +++ b/kifu/kifu-core/Cargo.toml @@ -6,5 +6,6 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -tokio = { version = "1.26", features = [ "full" ] } -grid = { version = "0.9" } +tokio = { version = "1.26", features = [ "full" ] } +grid = { version = "0.9" } +thiserror = { version = "1" } diff --git a/kifu/kifu-core/src/board.rs b/kifu/kifu-core/src/board.rs index 4390c53..55a3fc5 100644 --- a/kifu/kifu-core/src/board.rs +++ b/kifu/kifu-core/src/board.rs @@ -1,13 +1,6 @@ -use crate::{Color, Size}; -use grid::Grid; +use crate::{BoardError, Color, Size}; use std::collections::HashSet; -#[derive(Clone, Debug, PartialEq)] -pub enum Error { - InvalidPosition, - SelfCapture, -} - #[derive(Clone, Debug)] pub struct Board { pub size: Size, @@ -16,15 +9,15 @@ pub struct Board { impl std::fmt::Display for Board { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - print!(" "); + write!(f, " ")?; // for c in 'A'..'U' { for c in 0..19 { - print!("{:2}", c); + write!(f, "{:2}", c)?; } - println!(""); + writeln!(f, "")?; for row in 0..self.size.height { - print!(" {:2}", row); + write!(f, " {:2}", row)?; for column in 0..self.size.width { match self.stone(&Coordinate { column, row }) { None => write!(f, " .")?, @@ -38,6 +31,26 @@ impl std::fmt::Display for Board { } } +impl PartialEq for Board { + fn eq(&self, other: &Self) -> bool { + if self.size != other.size { + return false; + } + + for group in self.groups.iter() { + if !other.groups.contains(&group) { + return false; + } + } + for group in other.groups.iter() { + if !self.groups.contains(&group) { + return false; + } + } + return true; + } +} + impl Board { pub fn new() -> Self { Self { @@ -49,12 +62,12 @@ impl Board { } } - pub fn from_coordinates(coordinates: impl Iterator) -> Self { - let mut s = Self::new(); - for (coordinate, color) in coordinates { - s.place_stone(coordinate, color); - } - s + pub fn from_coordinates( + mut coordinates: impl Iterator, + ) -> Result { + coordinates.try_fold(Self::new(), |board, (coordinate, color)| { + board.place_stone(coordinate, color) + }) } } @@ -65,13 +78,11 @@ pub struct Coordinate { } impl Board { - pub fn place_stone(&mut self, coordinate: Coordinate, color: Color) -> Result<(), Error> { + pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result { if let Some(_) = self.stone(&coordinate) { - return Err(Error::InvalidPosition); + return Err(BoardError::InvalidPosition); } - let old_groups = self.groups.clone(); - let mut friendly_group = self .adjacencies(&coordinate) .into_iter() @@ -99,11 +110,10 @@ impl Board { } if self.liberties(&friendly_group) == 0 { - self.groups = old_groups; - return Err(Error::SelfCapture); + return Err(BoardError::SelfCapture); } - Ok(()) + Ok(self) } pub fn stone(&self, coordinate: &Coordinate) -> Option { @@ -268,7 +278,8 @@ mod test { (Coordinate { column: 18, row: 1 }, Color::White), ] .into_iter(), - ); + ) + .unwrap(); test(board); } @@ -307,7 +318,8 @@ mod test { ), ] .into_iter(), - ); + ) + .unwrap(); assert!(board.group(&Coordinate { column: 18, row: 3 }).is_none()); assert_eq!( board @@ -494,8 +506,7 @@ mod test { #[test] fn it_finds_adjacent_groups() { - with_example_board(|mut board| { - println!("{}", board); + with_example_board(|board| { let group = board .group(&Coordinate { column: 0, row: 0 }) .cloned() @@ -512,17 +523,23 @@ mod test { #[test] fn surrounding_a_group_removes_it() { - with_example_board(|mut board| { - board.place_stone(Coordinate { column: 10, row: 9 }, Color::Black); + with_example_board(|board| { + let board = board + .place_stone(Coordinate { column: 10, row: 9 }, Color::Black) + .unwrap(); assert!(board.stone(&Coordinate { column: 9, row: 9 }).is_none()); - board.place_stone(Coordinate { column: 2, row: 18 }, Color::Black); + let board = board + .place_stone(Coordinate { column: 2, row: 18 }, Color::Black) + .unwrap(); assert!(board.stone(&Coordinate { column: 0, row: 18 }).is_none()); assert!(board.stone(&Coordinate { column: 1, row: 18 }).is_none()); assert!(board.stone(&Coordinate { column: 0, row: 17 }).is_none()); assert!(board.group(&Coordinate { column: 0, row: 18 }).is_none()); - board.place_stone(Coordinate { column: 5, row: 18 }, Color::White); + let board = board + .place_stone(Coordinate { column: 5, row: 18 }, Color::White) + .unwrap(); assert!(board.stone(&Coordinate { column: 4, row: 17 }).is_none()); assert!(board.stone(&Coordinate { column: 5, row: 17 }).is_none()); assert!(board.stone(&Coordinate { column: 6, row: 17 }).is_none()); @@ -533,16 +550,84 @@ mod test { #[test] fn self_capture_is_forbidden() { - with_example_board(|mut board| { - let res = board.place_stone(Coordinate { column: 18, row: 0 }, Color::Black); - assert_eq!(res, Err(Error::SelfCapture)); + with_example_board(|board| { + { + let board = board.clone(); + let res = board.place_stone(Coordinate { column: 18, row: 0 }, Color::Black); + assert_eq!(res, Err(BoardError::SelfCapture)); + } - let res = board.place_stone(Coordinate { column: 5, row: 18 }, Color::Black); - assert_eq!(res, Err(Error::SelfCapture)); + { + let board = board.clone(); + let res = board.place_stone(Coordinate { column: 5, row: 18 }, Color::Black); + assert_eq!(res, Err(BoardError::SelfCapture)); + } }); } - fn captures_preceed_self_capture() { - assert!(false); + #[test] + fn validate_group_comparisons() { + { + let b1 = Board::from_coordinates( + vec![(Coordinate { column: 7, row: 9 }, Color::White)].into_iter(), + ) + .unwrap(); + let b2 = Board::from_coordinates( + vec![(Coordinate { column: 7, row: 9 }, Color::White)].into_iter(), + ) + .unwrap(); + + assert_eq!(b1, b2); + } + + { + let b1 = Board::from_coordinates( + vec![ + (Coordinate { column: 7, row: 9 }, Color::White), + (Coordinate { column: 8, row: 10 }, Color::White), + ] + .into_iter(), + ) + .unwrap(); + let b2 = Board::from_coordinates( + vec![ + (Coordinate { column: 8, row: 10 }, Color::White), + (Coordinate { column: 7, row: 9 }, Color::White), + ] + .into_iter(), + ) + .unwrap(); + + assert_eq!(b1, b2); + } + } + + #[test] + fn two_boards_can_be_compared() { + let board = Board::from_coordinates( + vec![ + (Coordinate { column: 7, row: 9 }, Color::White), + (Coordinate { column: 8, row: 8 }, Color::White), + (Coordinate { column: 8, row: 10 }, Color::White), + (Coordinate { column: 9, row: 9 }, Color::White), + (Coordinate { column: 10, row: 9 }, Color::Black), + (Coordinate { column: 9, row: 8 }, Color::Black), + (Coordinate { column: 9, row: 10 }, Color::Black), + ] + .into_iter(), + ) + .unwrap(); + + let b1 = board + .clone() + .place_stone(Coordinate { column: 8, row: 9 }, Color::Black) + .unwrap(); + + let b2 = b1 + .clone() + .place_stone(Coordinate { column: 9, row: 9 }, Color::White) + .unwrap(); + + assert_eq!(board, b2); } } diff --git a/kifu/kifu-core/src/lib.rs b/kifu/kifu-core/src/lib.rs index b9ad5cc..8b598bb 100644 --- a/kifu/kifu-core/src/lib.rs +++ b/kifu/kifu-core/src/lib.rs @@ -2,7 +2,7 @@ mod api; pub use api::{CoreApp, Request, Response}; mod types; -pub use types::{Color, Size}; +pub use types::{BoardError, Color, Size}; pub mod ui; mod board; diff --git a/kifu/kifu-core/src/types.rs b/kifu/kifu-core/src/types.rs index 6686f54..3b43d16 100644 --- a/kifu/kifu-core/src/types.rs +++ b/kifu/kifu-core/src/types.rs @@ -3,6 +3,17 @@ use crate::{ board::{Board, Coordinate}, }; use std::time::Duration; +use thiserror::Error; + +#[derive(Debug, PartialEq, Error)] +pub enum BoardError { + #[error("Position is invalid")] + InvalidPosition, + #[error("Self-capture is forbidden")] + SelfCapture, + #[error("Ko")] + Ko, +} #[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)] pub enum Color { @@ -80,6 +91,8 @@ pub struct Player { pub struct GameState { pub board: Board, + pub past_positions: Vec, + pub conversation: Vec, pub current_player: Color, @@ -94,6 +107,7 @@ impl GameState { fn new() -> GameState { GameState { board: Board::new(), + past_positions: vec![], conversation: vec![], current_player: Color::Black, white_player: Player { @@ -109,11 +123,79 @@ impl GameState { } } - fn place_stone(&mut self, coordinate: Coordinate) { - self.board.place_stone(coordinate, self.current_player); + fn place_stone(&mut self, coordinate: Coordinate) -> Result<(), BoardError> { + let board = self.board.clone(); + let new_board = board.place_stone(coordinate, self.current_player)?; + + println!("past positions: {}", self.past_positions.len()); + if self.past_positions.contains(&new_board) { + return Err(BoardError::Ko); + } + + self.past_positions.push(self.board.clone()); + self.board = new_board; match self.current_player { Color::White => self.current_player = Color::Black, Color::Black => self.current_player = Color::White, - } + }; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn current_player_changes_after_move() { + let mut state = GameState::new(); + assert_eq!(state.current_player, Color::Black); + state.place_stone(Coordinate { column: 9, row: 9 }).unwrap(); + assert_eq!(state.current_player, Color::White); + } + + #[test] + fn current_player_remains_the_same_after_self_capture() { + let mut state = GameState::new(); + state.board = Board::from_coordinates( + vec![ + (Coordinate { column: 17, row: 0 }, Color::White), + (Coordinate { column: 17, row: 1 }, Color::White), + (Coordinate { column: 18, row: 1 }, Color::White), + ] + .into_iter(), + ) + .unwrap(); + state.current_player = Color::Black; + + assert_eq!( + state.place_stone(Coordinate { column: 18, row: 0 }), + Err(BoardError::SelfCapture) + ); + assert_eq!(state.current_player, Color::Black); + } + + #[test] + fn ko_rules_are_enforced() { + let mut state = GameState::new(); + state.board = Board::from_coordinates( + vec![ + (Coordinate { column: 7, row: 9 }, Color::White), + (Coordinate { column: 8, row: 8 }, Color::White), + (Coordinate { column: 8, row: 10 }, Color::White), + (Coordinate { column: 9, row: 9 }, Color::White), + (Coordinate { column: 10, row: 9 }, Color::Black), + (Coordinate { column: 9, row: 8 }, Color::Black), + (Coordinate { column: 9, row: 10 }, Color::Black), + ] + .into_iter(), + ) + .unwrap(); + + state.place_stone(Coordinate { column: 8, row: 9 }).unwrap(); + assert_eq!( + state.place_stone(Coordinate { column: 9, row: 9 }), + Err(BoardError::Ko) + ); } } diff --git a/kifu/kifu-gtk/Cargo.lock b/kifu/kifu-gtk/Cargo.lock index f6bc0df..c7faf28 100644 --- a/kifu/kifu-gtk/Cargo.lock +++ b/kifu/kifu-gtk/Cargo.lock @@ -667,6 +667,7 @@ name = "kifu-core" version = "0.1.0" dependencies = [ "grid", + "thiserror", "tokio", ]