diff --git a/kifu/kifu-core/src/types.rs b/kifu/kifu-core/src/types.rs index a6f5b3c..cf7e197 100644 --- a/kifu/kifu-core/src/types.rs +++ b/kifu/kifu-core/src/types.rs @@ -1,7 +1,7 @@ use crate::api::PlayStoneRequest; -use std::time::Duration; +use std::{collections::HashSet, time::Duration}; -#[derive(Clone, Copy, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)] pub enum Color { Black, White, @@ -36,7 +36,10 @@ impl AppState { pub fn place_stone(&mut self, req: PlayStoneRequest) { match self.game { Some(ref mut game) => { - game.place_stone(req.column, req.row); + game.place_stone(Coordinate { + column: req.column, + row: req.row, + }); } None => {} } @@ -103,8 +106,8 @@ impl GameState { } } - fn place_stone(&mut self, column: u8, row: u8) { - self.board.place_stone(column, row, self.current_player); + fn place_stone(&mut self, coordinate: Coordinate) { + self.board.place_stone(coordinate, self.current_player); match self.current_player { Color::White => self.current_player = Color::Black, Color::Black => self.current_player = Color::White, @@ -112,6 +115,53 @@ impl GameState { } } +#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)] +pub struct Coordinate { + pub column: u8, + pub row: u8, +} + +impl Coordinate { + fn adjacencies(&self, max_column: u8, max_row: u8) -> impl Iterator { + vec![ + if self.column > 0 { + Some(Coordinate { + column: self.column - 1, + row: self.row, + }) + } else { + None + }, + if self.row > 0 { + Some(Coordinate { + column: self.column, + row: self.row - 1, + }) + } else { + None + }, + if self.column < max_column { + Some(Coordinate { + column: self.column + 1, + row: self.row, + }) + } else { + None + }, + if self.row < max_row { + Some(Coordinate { + column: self.column, + row: self.row + 1, + }) + } else { + None + }, + ] + .into_iter() + .filter_map(|v| v) + } +} + #[derive(Clone, Debug)] pub struct Board { pub size: Size, @@ -122,7 +172,7 @@ impl std::fmt::Display for Board { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { for row in 0..self.size.height { for column in 0..self.size.width { - match self.stone(column, row) { + match self.stone(Coordinate { column, row }) { None => write!(f, " . ")?, Some(Color::Black) => write!(f, " X ")?, Some(Color::White) => write!(f, " O ")?, @@ -149,21 +199,85 @@ impl Board { } } - pub fn place_stone(&mut self, column: u8, row: u8, stone: Color) { - let addr = self.addr(column, row); + 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 place_stone(&mut self, coordinate: Coordinate, stone: Color) { + let addr = self.addr(coordinate.column, coordinate.row); self.spaces[addr] = Some(stone); } - pub fn stone(&self, column: u8, row: u8) -> Option { - let addr = self.addr(column, row); + pub fn stone(&self, coordinate: Coordinate) -> Option { + let addr = self.addr(coordinate.column, coordinate.row); self.spaces[addr] } + pub fn group(&self, coordinate: Coordinate) -> Option { + let mut visited: HashSet = HashSet::new(); + + if let Some(color) = self.stone(coordinate) { + let mut fringes = coordinate + .adjacencies(self.size.width as u8 - 1, self.size.height as u8 - 1) + .collect::>(); + visited.insert(coordinate); + + while let Some(coordinate) = fringes.pop() { + if self.stone(coordinate) == Some(color) { + if !visited.contains(&coordinate) { + fringes.append( + &mut coordinate + .adjacencies(self.size.width as u8 - 1, self.size.height as u8 - 1) + .collect::>(), + ); + visited.insert(coordinate); + } + } + } + + Some(Group { + color, + coordinates: visited, + }) + } else { + None + } + } + + pub fn liberties(&self, group: &Group) -> usize { + group + .adjacencies(self.size.width as u8 - 1, self.size.height as u8 - 1) + .into_iter() + .filter(|coordinate| self.stone(*coordinate).is_none()) + .collect::>() + .len() + } + fn addr(&self, column: u8, row: u8) -> usize { ((row as usize) * (self.size.width as usize) + (column as usize)) as usize } } +#[derive(Clone, Debug, PartialEq)] +pub struct Group { + color: Color, + coordinates: HashSet, +} + +impl Group { + fn adjacencies(&self, max_column: u8, max_row: u8) -> HashSet { + self.coordinates + .iter() + .map(|stone| stone.adjacencies(max_column, max_row)) + .flatten() + .collect() + } +} + #[cfg(test)] mod test { use super::*; @@ -178,43 +292,144 @@ mod test { * A stone placed in a suicidal position is legal if it captures other stones first. */ - #[test] - fn it_shows_stones() { - let mut board = Board::new(); - board.place_stone(3, 2, Color::White); - board.place_stone(2, 3, Color::White); - board.place_stone(4, 3, Color::White); - board.place_stone(3, 3, Color::Black); - println!("{}", board); - assert!(false); - } - #[test] fn it_counts_individual_liberties() { - let mut board = Board::new(); - board.place_stone(3, 3, Color::White); - board.place_stone(0, 3, Color::White); - board.place_stone(0, 0, Color::White); - println!("{}", board); - assert!(false); + let board = Board::from_coordinates( + vec![ + (Coordinate { column: 3, row: 3 }, Color::White), + (Coordinate { column: 0, row: 3 }, Color::White), + (Coordinate { column: 0, row: 0 }, Color::White), + (Coordinate { column: 18, row: 9 }, Color::Black), + ( + Coordinate { + column: 18, + row: 18, + }, + Color::Black, + ), + ] + .into_iter(), + ); + assert_eq!( + board + .group(Coordinate { column: 3, row: 3 }) + .map(|g| board.liberties(&g)), + Some(4) + ); + assert_eq!( + board + .group(Coordinate { column: 0, row: 3 }) + .map(|g| board.liberties(&g)), + Some(3) + ); + assert_eq!( + board + .group(Coordinate { column: 0, row: 0 }) + .map(|g| board.liberties(&g)), + Some(2) + ); + assert_eq!( + board + .group(Coordinate { column: 18, row: 9 }) + .map(|g| board.liberties(&g)), + Some(3) + ); + assert_eq!( + board + .group(Coordinate { + column: 18, + row: 18 + }) + .map(|g| board.liberties(&g)), + Some(2) + ); } #[test] fn stones_share_liberties() { - let mut board = Board::new(); - board.place_stone(3, 3, Color::White); - board.place_stone(3, 4, Color::White); - board.place_stone(3, 5, Color::White); - board.place_stone(4, 4, Color::White); - println!("{}", board); - assert!(false); + let test_cases = vec![ + ( + Board::from_coordinates( + vec![ + (Coordinate { column: 3, row: 3 }, Color::White), + (Coordinate { column: 3, row: 4 }, Color::White), + ] + .into_iter(), + ), + Coordinate { column: 3, row: 4 }, + Some(Group { + color: Color::White, + coordinates: vec![ + Coordinate { column: 3, row: 3 }, + Coordinate { column: 3, row: 4 }, + ] + .into_iter() + .collect(), + }), + Some(6), + ), + ( + Board::from_coordinates( + vec![ + (Coordinate { column: 3, row: 3 }, Color::White), + (Coordinate { column: 3, row: 4 }, Color::White), + (Coordinate { column: 4, row: 4 }, Color::White), + ] + .into_iter(), + ), + Coordinate { column: 3, row: 4 }, + Some(Group { + color: Color::White, + coordinates: vec![ + Coordinate { column: 3, row: 3 }, + Coordinate { column: 3, row: 4 }, + Coordinate { column: 4, row: 4 }, + ] + .into_iter() + .collect(), + }), + Some(7), + ), + ( + Board::from_coordinates( + vec![ + (Coordinate { column: 3, row: 3 }, Color::White), + (Coordinate { column: 3, row: 4 }, Color::White), + (Coordinate { column: 4, row: 4 }, Color::White), + (Coordinate { column: 3, row: 5 }, Color::White), + ] + .into_iter(), + ), + Coordinate { column: 3, row: 4 }, + Some(Group { + color: Color::White, + coordinates: vec![ + Coordinate { column: 3, row: 3 }, + Coordinate { column: 3, row: 4 }, + Coordinate { column: 4, row: 4 }, + Coordinate { column: 3, row: 5 }, + ] + .into_iter() + .collect(), + }), + Some(8), + ), + ]; + + for (board, coordinate, group, liberties) in test_cases { + assert_eq!(board.group(coordinate), group); + assert_eq!( + board.group(coordinate).map(|g| board.liberties(&g)), + liberties + ); + } } #[test] fn opposing_stones_reduce_liberties() { let mut board = Board::new(); - board.place_stone(3, 3, Color::White); - board.place_stone(3, 4, Color::Black); + board.place_stone(Coordinate { column: 3, row: 3 }, Color::White); + board.place_stone(Coordinate { column: 3, row: 4 }, Color::Black); println!("{}", board); assert!(false); } diff --git a/kifu/kifu-core/src/ui/types.rs b/kifu/kifu-core/src/ui/types.rs index 39e2904..ef9f21c 100644 --- a/kifu/kifu-core/src/ui/types.rs +++ b/kifu/kifu-core/src/ui/types.rs @@ -1,7 +1,7 @@ use crate::types::{Color, Size}; use crate::{ api::{PlayStoneRequest, Request}, - types::Board, + types::{Board, Coordinate}, }; #[derive(Clone, Copy, Debug, PartialEq)] @@ -71,7 +71,7 @@ impl From<&Board> for BoardElement { let spaces: Vec = (0..board.size.height) .map(|row| { (0..board.size.width) - .map(|column| match board.stone(column, row) { + .map(|column| match board.stone(Coordinate { column, row }) { Some(color) => IntersectionElement::Filled(StoneElement { jitter: Jitter { x: 0, y: 0 }, color,