From 78af31f6f907d48a7a4ea74666af4bde414c72df Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Fri, 28 Apr 2023 01:21:23 -0400 Subject: [PATCH] Overhaul the board representation Groups become more first-class objects, where the grid becomes useful for quick lookups of values. --- kifu/kifu-core/Cargo.lock | 16 + kifu/kifu-core/Cargo.toml | 3 +- kifu/kifu-core/src/api.rs | 4 +- kifu/kifu-core/src/board.rs | 611 +++++++++++++++++++++++++ kifu/kifu-core/src/lib.rs | 3 + kifu/kifu-core/src/types.rs | 509 +------------------- kifu/kifu-core/src/ui/playing_field.rs | 6 +- kifu/kifu-core/src/ui/types.rs | 4 +- kifu/kifu-gtk/Cargo.lock | 16 + 9 files changed, 664 insertions(+), 508 deletions(-) create mode 100644 kifu/kifu-core/src/board.rs diff --git a/kifu/kifu-core/Cargo.lock b/kifu/kifu-core/Cargo.lock index 59b31e4..f723d62 100644 --- a/kifu/kifu-core/Cargo.lock +++ b/kifu/kifu-core/Cargo.lock @@ -26,6 +26,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "grid" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0634107a3a005070dd73e27e74ecb691a94e9e5ba7829f434db7fbf73a6b5c47" +dependencies = [ + "no-std-compat", +] + [[package]] name = "hermit-abi" version = "0.2.6" @@ -39,6 +48,7 @@ dependencies = [ name = "kifu-core" version = "0.1.0" dependencies = [ + "grid", "tokio", ] @@ -85,6 +95,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "num_cpus" version = "1.15.0" diff --git a/kifu/kifu-core/Cargo.toml b/kifu/kifu-core/Cargo.toml index ad2dec2..8287207 100644 --- a/kifu/kifu-core/Cargo.toml +++ b/kifu/kifu-core/Cargo.toml @@ -6,4 +6,5 @@ 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" ] } +tokio = { version = "1.26", features = [ "full" ] } +grid = { version = "0.9" } diff --git a/kifu/kifu-core/src/api.rs b/kifu/kifu-core/src/api.rs index 983a5bc..5abd9d4 100644 --- a/kifu/kifu-core/src/api.rs +++ b/kifu/kifu-core/src/api.rs @@ -10,8 +10,8 @@ pub enum Request { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct PlayStoneRequest { - pub column: u8, - pub row: u8, + pub column: usize, + pub row: usize, } #[derive(Clone, Debug)] diff --git a/kifu/kifu-core/src/board.rs b/kifu/kifu-core/src/board.rs new file mode 100644 index 0000000..ac07290 --- /dev/null +++ b/kifu/kifu-core/src/board.rs @@ -0,0 +1,611 @@ +use crate::{Color, Size}; +use grid::Grid; +use std::collections::HashSet; + +#[derive(Clone, Debug)] +pub enum Error { + InvalidPosition, +} + +#[derive(Clone, Debug)] +pub struct Board { + pub size: Size, + pub grid: Grid>, + pub groups: Vec, +} + +impl std::fmt::Display for Board { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + print!(" "); + // for c in 'A'..'U' { + for c in 0..19 { + print!("{:2}", c); + } + println!(""); + + for row in 0..self.size.height { + print!(" {:2}", row); + for column in 0..self.size.width { + match self.stone(&Coordinate { column, row }) { + None => write!(f, " .")?, + Some(Color::Black) => write!(f, " X")?, + Some(Color::White) => write!(f, " O")?, + } + } + writeln!(f, "")?; + } + Ok(()) + } +} + +impl Board { + pub fn new() -> Self { + Self { + size: Size { + width: 19, + height: 19, + }, + grid: Grid::new(19, 19), + groups: Vec::new(), + } + } + + pub fn from_coordinates(coordinates: impl Iterator) -> Self { + let mut s = Self::new(); + for (coordinate, color) in coordinates { + s.place_stone(coordinate, color); + } + s + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)] +pub struct Coordinate { + pub column: usize, + pub row: usize, +} + +impl Board { + pub fn place_stone(&mut self, coordinate: Coordinate, color: Color) -> Result<(), Error> { + if let Some(_) = self.stone(&coordinate) { + return Err(Error::InvalidPosition); + } + + let mut friendly_group = self + .adjacencies(&coordinate) + .into_iter() + .filter(|c| self.stone(c) == Some(color)) + .filter_map(|c| self.group(&c).map(|g| g.coordinates.clone())) + .fold(HashSet::new(), |acc, set| { + acc.union(&set).cloned().collect() + }); + + friendly_group.insert(coordinate.clone()); + + self.groups + .retain(|g| g.coordinates.is_disjoint(&friendly_group)); + let friendly_group = Group { + color, + coordinates: friendly_group, + }; + self.groups.push(friendly_group.clone()); + + match self.grid.get_mut(coordinate.row, coordinate.column) { + None => return Err(Error::InvalidPosition), + Some(space) => *space = Some(color), + } + + let adjacent_groups = self.adjacent_groups(&friendly_group); + for group in adjacent_groups { + if self.liberties(&group) == 0 { + self.remove_group(&group); + } + } + + Ok(()) + } + + pub fn stone(&self, coordinate: &Coordinate) -> Option { + match self.grid.get(coordinate.row, coordinate.column) { + None => None, + Some(val) => *val, + } + } + + pub fn group(&self, coordinate: &Coordinate) -> Option<&Group> { + self.groups + .iter() + .find(|g| g.coordinates.contains(coordinate)) + } + + pub fn remove_group(&mut self, group: &Group) { + self.groups.retain(|g| g != group); + for coord in group.coordinates.iter() { + match self.grid.get_mut(coord.row, coord.column) { + None => (), + Some(v) => *v = None, + } + } + } + + pub fn adjacent_groups(&self, group: &Group) -> Vec { + let adjacent_spaces = self.group_halo(group).into_iter(); + println!("adjacent spaces: {:?}", adjacent_spaces); + let mut grps: Vec = Vec::new(); + + adjacent_spaces.for_each(|coord| match self.group(&coord) { + None => return, + Some(adj) => { + println!("group found: {:?}", adj.color); + if group.color == adj.color { + return; + } + if grps.iter().any(|g| g.coordinates.contains(&coord)) { + return; + } + grps.push(adj.clone()); + } + }); + + grps + } + + pub fn group_halo(&self, group: &Group) -> HashSet { + group + .coordinates + .iter() + .map(|c| self.adjacencies(c)) + .flatten() + .collect::>() + } + + pub fn liberties(&self, group: &Group) -> usize { + self.group_halo(group) + .into_iter() + .filter(|c| self.stone(&c) == None) + .count() + + // let adjacencies: HashSet = self.adjacencies(group.coordinates.iter()).collect(); + /* + println!("adjacencies: {:?}", adjacencies); + let opposing_spaces = adjacencies + .iter() + .filter(|coord| match self.grid.get(coord.row, coord.column) { + None => true, + Some(&None) => false, + Some(&Some(c)) => c != group.color, + }) + .cloned() + .collect::>(); + println!("opposition: {:?}", opposing_spaces); + + adjacencies.len() - opposing_spaces.len() + */ + + /* + 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() + */ + } + + pub fn adjacencies(&self, coordinate: &Coordinate) -> Vec { + let mut v = Vec::new(); + if coordinate.column > 0 { + v.push(Coordinate { + column: coordinate.column - 1, + row: coordinate.row, + }); + } + if coordinate.row > 0 { + v.push(Coordinate { + column: coordinate.column, + row: coordinate.row - 1, + }); + } + v.push(Coordinate { + column: coordinate.column + 1, + row: coordinate.row, + }); + v.push(Coordinate { + column: coordinate.column, + row: coordinate.row + 1, + }); + v.into_iter().filter(|c| self.within_board(c)).collect() + } + + /* + pub fn adjacencies<'a>( + &'a self, + coordinates: impl Iterator + 'a, + ) -> impl Iterator + 'a { + coordinates + .map(|coordinate| { + let mut v = Vec::new(); + if coordinate.column > 0 { + v.push(Coordinate { + column: coordinate.column - 1, + row: coordinate.row, + }); + } + if coordinate.row > 0 { + v.push(Coordinate { + column: coordinate.column, + row: coordinate.row - 1, + }); + } + v.push(Coordinate { + column: coordinate.column + 1, + row: coordinate.row, + }); + v.push(Coordinate { + column: coordinate.column, + row: coordinate.row + 1, + }); + v + }) + .flatten() + .filter(|coordinate| self.within_board(coordinate)) + } + */ + + pub fn within_board(&self, coordinate: &Coordinate) -> bool { + coordinate.column < self.size.width && coordinate.row < self.size.height + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Group { + color: Color, + coordinates: HashSet, +} + +/* +impl Group { + pub fn adjacencies(&self, max_column: usize, max_row: usize) -> HashSet { + self.coordinates + .iter() + .map(|stone| stone.adjacencies(max_column, max_row)) + .flatten() + .collect() + } +} +*/ + +#[cfg(test)] +mod test { + use super::*; + + /* Two players (Black and White) take turns and Black plays first + * Stones are placed on the line intersections and not moved. + * A stone with no liberties is removed from the board. + * A group of stones of the same color share liberties. + * A stone at the edge of the board has only three liberties. + * A stone at the corner of the board has only two liberties. + * A stone may not be placed in a suicidal position. + * A stone placed in a suicidal position is legal if it captures other stones first. + */ + + fn with_example_board(test: impl FnOnce(Board)) { + let board = Board::from_coordinates( + vec![ + (Coordinate { column: 3, row: 3 }, Color::White), + (Coordinate { column: 3, row: 4 }, Color::White), + /* */ + (Coordinate { column: 8, row: 3 }, Color::Black), + (Coordinate { column: 9, row: 3 }, Color::Black), + (Coordinate { column: 9, row: 4 }, Color::Black), + /* */ + (Coordinate { column: 15, row: 3 }, Color::White), + (Coordinate { column: 15, row: 4 }, Color::White), + (Coordinate { column: 15, row: 5 }, Color::White), + (Coordinate { column: 14, row: 4 }, Color::White), + /* */ + (Coordinate { column: 3, row: 8 }, Color::White), + (Coordinate { column: 3, row: 9 }, Color::White), + (Coordinate { column: 4, row: 9 }, Color::White), + (Coordinate { column: 3, row: 10 }, Color::Black), + /* */ + (Coordinate { column: 0, row: 0 }, Color::White), + (Coordinate { column: 1, row: 0 }, Color::White), + (Coordinate { column: 0, row: 1 }, Color::White), + /* */ + (Coordinate { column: 9, row: 9 }, Color::White), + (Coordinate { column: 8, row: 9 }, Color::Black), + (Coordinate { column: 9, row: 8 }, Color::Black), + (Coordinate { column: 9, row: 10 }, Color::Black), + /* */ + (Coordinate { column: 0, row: 17 }, Color::White), + (Coordinate { column: 1, row: 18 }, Color::White), + (Coordinate { column: 0, row: 18 }, Color::White), + (Coordinate { column: 0, row: 16 }, Color::Black), + (Coordinate { column: 1, row: 17 }, Color::Black), + /* */ + (Coordinate { column: 4, row: 17 }, Color::Black), + (Coordinate { column: 5, row: 17 }, Color::Black), + (Coordinate { column: 6, row: 17 }, Color::Black), + (Coordinate { column: 4, row: 18 }, Color::Black), + (Coordinate { column: 6, row: 18 }, Color::Black), + (Coordinate { column: 3, row: 17 }, Color::White), + (Coordinate { column: 3, row: 18 }, Color::White), + (Coordinate { column: 4, row: 16 }, Color::White), + (Coordinate { column: 5, row: 16 }, Color::White), + (Coordinate { column: 6, row: 16 }, Color::White), + (Coordinate { column: 7, row: 17 }, Color::White), + (Coordinate { column: 7, row: 18 }, Color::White), + ] + .into_iter(), + ); + test(board); + } + + #[test] + fn it_gets_adjacencies_for_coordinate() { + let board = Board::new(); + for column in 0..19 { + for row in 0..19 { + for coordinate in board.adjacencies(&Coordinate { column, row }) { + assert!( + board.within_board(&coordinate), + "{} {}: {:?}", + column, + row, + coordinate + ); + } + } + } + } + + #[test] + fn it_counts_individual_liberties() { + 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!(board.group(&Coordinate { column: 18, row: 3 }).is_none()); + 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() { + with_example_board(|board: Board| { + let test_cases = vec![ + ( + board.clone(), + Coordinate { column: 0, row: 0 }, + Some(Group { + color: Color::White, + coordinates: vec![ + Coordinate { column: 0, row: 0 }, + Coordinate { column: 1, row: 0 }, + Coordinate { column: 0, row: 1 }, + ] + .into_iter() + .collect(), + }), + Some(3), + ), + ( + board.clone(), + Coordinate { column: 1, row: 0 }, + Some(Group { + color: Color::White, + coordinates: vec![ + Coordinate { column: 0, row: 0 }, + Coordinate { column: 1, row: 0 }, + Coordinate { column: 0, row: 1 }, + ] + .into_iter() + .collect(), + }), + Some(3), + ), + ( + board.clone(), + Coordinate { column: 9, row: 9 }, + Some(Group { + color: Color::White, + coordinates: vec![Coordinate { column: 9, row: 9 }].into_iter().collect(), + }), + Some(1), + ), + ( + board.clone(), + 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.clone(), + Coordinate { column: 9, row: 3 }, + Some(Group { + color: Color::Black, + coordinates: vec![ + Coordinate { column: 8, row: 3 }, + Coordinate { column: 9, row: 3 }, + Coordinate { column: 9, row: 4 }, + ] + .into_iter() + .collect(), + }), + Some(7), + ), + ( + board.clone(), + Coordinate { column: 15, row: 4 }, + Some(Group { + color: Color::White, + coordinates: vec![ + Coordinate { column: 15, row: 3 }, + Coordinate { column: 15, row: 4 }, + Coordinate { column: 15, row: 5 }, + Coordinate { column: 14, row: 4 }, + ] + .into_iter() + .collect(), + }), + Some(8), + ), + ( + board.clone(), + Coordinate { column: 3, row: 9 }, + Some(Group { + color: Color::White, + coordinates: vec![ + Coordinate { column: 3, row: 8 }, + Coordinate { column: 3, row: 9 }, + Coordinate { column: 4, row: 9 }, + ] + .into_iter() + .collect(), + }), + Some(6), + ), + ( + board.clone(), + Coordinate { column: 0, row: 18 }, + Some(Group { + color: Color::White, + coordinates: vec![ + Coordinate { column: 0, row: 17 }, + Coordinate { column: 0, row: 18 }, + Coordinate { column: 1, row: 18 }, + ] + .into_iter() + .collect(), + }), + Some(1), + ), + ( + board.clone(), + Coordinate { column: 0, row: 17 }, + Some(Group { + color: Color::White, + coordinates: vec![ + Coordinate { column: 0, row: 17 }, + Coordinate { column: 0, row: 18 }, + Coordinate { column: 1, row: 18 }, + ] + .into_iter() + .collect(), + }), + Some(1), + ), + ]; + + println!("{}", board); + for (board, coordinate, group, liberties) in test_cases { + assert_eq!(board.group(&coordinate), group.as_ref()); + assert_eq!( + board.group(&coordinate).map(|g| board.liberties(&g)), + liberties, + "{:?}", + coordinate + ); + } + }); + } + + #[test] + fn it_finds_adjacent_groups() { + with_example_board(|mut board| { + println!("{}", board); + let group = board + .group(&Coordinate { column: 0, row: 0 }) + .cloned() + .unwrap(); + assert_eq!(board.adjacent_groups(&group), Vec::new()); + + let group = board + .group(&Coordinate { column: 3, row: 10 }) + .cloned() + .unwrap(); + assert_eq!(board.adjacent_groups(&group).len(), 1); + }); + } + + #[test] + fn surrounding_a_group_removes_it() { + with_example_board(|mut board| { + board.place_stone(Coordinate { column: 10, row: 9 }, Color::Black); + assert!(board.stone(&Coordinate { column: 9, row: 9 }).is_none()); + + board.place_stone(Coordinate { column: 2, row: 18 }, Color::Black); + 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); + 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()); + assert!(board.stone(&Coordinate { column: 4, row: 18 }).is_none()); + assert!(board.stone(&Coordinate { column: 6, row: 18 }).is_none()); + }); + } + + fn suicide_is_forbidden() { + assert!(false); + } + + fn captures_preceed_self_capture() { + assert!(false); + } +} diff --git a/kifu/kifu-core/src/lib.rs b/kifu/kifu-core/src/lib.rs index 9c22bc8..b9ad5cc 100644 --- a/kifu/kifu-core/src/lib.rs +++ b/kifu/kifu-core/src/lib.rs @@ -4,3 +4,6 @@ pub use api::{CoreApp, Request, Response}; mod types; pub use types::{Color, Size}; pub mod ui; + +mod board; +pub use board::*; diff --git a/kifu/kifu-core/src/types.rs b/kifu/kifu-core/src/types.rs index a67c802..6686f54 100644 --- a/kifu/kifu-core/src/types.rs +++ b/kifu/kifu-core/src/types.rs @@ -1,5 +1,8 @@ -use crate::api::PlayStoneRequest; -use std::{collections::HashSet, time::Duration}; +use crate::{ + api::PlayStoneRequest, + board::{Board, Coordinate}, +}; +use std::time::Duration; #[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)] pub enum Color { @@ -7,10 +10,10 @@ pub enum Color { White, } -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] pub struct Size { - pub width: u8, - pub height: u8, + pub width: usize, + pub height: usize, } impl Default for Size { @@ -114,499 +117,3 @@ impl GameState { } } } - -#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)] -pub struct Coordinate { - pub column: u8, - pub row: u8, -} - -impl Coordinate { - fn within_board(&self, width: u8, height: u8) -> bool { - self.column < width && self.row < height - } - - fn adjacencies(&self, width: u8, height: 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 < width - 1 { - Some(Coordinate { - column: self.column + 1, - row: self.row, - }) - } else { - None - }, - if self.row < height - 1 { - 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, - pub spaces: Vec>, -} - -impl std::fmt::Display for Board { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { - print!(" "); - // for c in 'A'..'U' { - for c in 0..19 { - print!("{:2}", c); - } - println!(""); - - for row in 0..self.size.height { - print!(" {:2}", row); - for column in 0..self.size.width { - match self.stone(Coordinate { column, row }) { - None => write!(f, " .")?, - Some(Color::Black) => write!(f, " X")?, - Some(Color::White) => write!(f, " O")?, - } - } - writeln!(f, "")?; - } - Ok(()) - } -} - -impl Board { - fn new() -> Self { - let mut spaces = Vec::new(); - for _ in 0..19 * 19 { - spaces.push(None); - } - Self { - size: Size { - width: 19, - height: 19, - }, - spaces, - } - } - - 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); - - let mut to_remove = HashSet::new(); - let mut visited = HashSet::new(); - - for adjacency in coordinate.adjacencies(self.size.width, self.size.height) { - if visited.contains(&adjacency) { - continue; - } - if let Some(group) = self.group(adjacency) { - if self.liberties(&group) == 0 { - to_remove = to_remove.union(&group.coordinates).cloned().collect() - } - visited = visited.union(&group.coordinates).cloned().collect() - } - } - - for coordinate in to_remove { - let addr = self.addr(coordinate.column, coordinate.row); - self.spaces[addr] = None; - } - } - - 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::>(); - fringes - .iter() - .for_each(|coordinate| println!("group fringes: {:?}", coordinate)); - 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 { - let addr = ((row as usize) * (self.size.width as usize) + (column as usize)) as usize; - if addr >= 361 { - panic!("invalid address: {}, {} {}", addr, column, row); - } - addr - } -} - -#[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::*; - - /* Two players (Black and White) take turns and Black plays first - * Stones are placed on the line intersections and not moved. - * A stone with no liberties is removed from the board. - * A group of stones of the same color share liberties. - * A stone at the edge of the board has only three liberties. - * A stone at the corner of the board has only two liberties. - * A stone may not be placed in a suicidal position. - * A stone placed in a suicidal position is legal if it captures other stones first. - */ - - fn with_example_board(test: impl FnOnce(Board)) { - let board = Board::from_coordinates( - vec![ - (Coordinate { column: 3, row: 3 }, Color::White), - (Coordinate { column: 3, row: 4 }, Color::White), - /* */ - (Coordinate { column: 8, row: 3 }, Color::Black), - (Coordinate { column: 9, row: 3 }, Color::Black), - (Coordinate { column: 9, row: 4 }, Color::Black), - /* */ - (Coordinate { column: 15, row: 3 }, Color::White), - (Coordinate { column: 15, row: 4 }, Color::White), - (Coordinate { column: 15, row: 5 }, Color::White), - (Coordinate { column: 14, row: 4 }, Color::White), - /* */ - (Coordinate { column: 3, row: 8 }, Color::White), - (Coordinate { column: 3, row: 9 }, Color::White), - (Coordinate { column: 4, row: 9 }, Color::White), - (Coordinate { column: 3, row: 10 }, Color::Black), - /* */ - (Coordinate { column: 0, row: 0 }, Color::White), - (Coordinate { column: 1, row: 0 }, Color::White), - (Coordinate { column: 0, row: 1 }, Color::White), - /* */ - (Coordinate { column: 9, row: 9 }, Color::White), - (Coordinate { column: 8, row: 9 }, Color::Black), - (Coordinate { column: 9, row: 8 }, Color::Black), - (Coordinate { column: 9, row: 10 }, Color::Black), - /* */ - (Coordinate { column: 0, row: 17 }, Color::White), - (Coordinate { column: 1, row: 18 }, Color::White), - (Coordinate { column: 0, row: 18 }, Color::White), - (Coordinate { column: 0, row: 16 }, Color::Black), - (Coordinate { column: 1, row: 17 }, Color::Black), - ] - .into_iter(), - ); - test(board); - } - - #[test] - fn it_gets_adjacencies_for_a_coordinate() { - for column in 0..19 { - for row in 0..19 { - for coordinate in (Coordinate { column, row }.adjacencies(19, 19)) { - assert!( - coordinate.within_board(19, 19), - "{} {}: {:?}", - column, - row, - coordinate - ); - } - } - } - } - - #[test] - fn it_counts_individual_liberties() { - 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!(board.group(Coordinate { column: 18, row: 3 }).is_none()); - 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() { - with_example_board(|board: Board| { - let test_cases = vec![ - ( - board.clone(), - Coordinate { column: 0, row: 0 }, - Some(Group { - color: Color::White, - coordinates: vec![ - Coordinate { column: 0, row: 0 }, - Coordinate { column: 1, row: 0 }, - Coordinate { column: 0, row: 1 }, - ] - .into_iter() - .collect(), - }), - Some(3), - ), - ( - board.clone(), - Coordinate { column: 1, row: 0 }, - Some(Group { - color: Color::White, - coordinates: vec![ - Coordinate { column: 0, row: 0 }, - Coordinate { column: 1, row: 0 }, - Coordinate { column: 0, row: 1 }, - ] - .into_iter() - .collect(), - }), - Some(3), - ), - ( - board.clone(), - Coordinate { column: 9, row: 9 }, - Some(Group { - color: Color::White, - coordinates: vec![Coordinate { column: 9, row: 9 }].into_iter().collect(), - }), - Some(1), - ), - ( - board.clone(), - 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.clone(), - Coordinate { column: 9, row: 3 }, - Some(Group { - color: Color::Black, - coordinates: vec![ - Coordinate { column: 8, row: 3 }, - Coordinate { column: 9, row: 3 }, - Coordinate { column: 9, row: 4 }, - ] - .into_iter() - .collect(), - }), - Some(7), - ), - ( - board.clone(), - Coordinate { column: 15, row: 4 }, - Some(Group { - color: Color::White, - coordinates: vec![ - Coordinate { column: 15, row: 3 }, - Coordinate { column: 15, row: 4 }, - Coordinate { column: 15, row: 5 }, - Coordinate { column: 14, row: 4 }, - ] - .into_iter() - .collect(), - }), - Some(8), - ), - ( - board.clone(), - Coordinate { column: 3, row: 9 }, - Some(Group { - color: Color::White, - coordinates: vec![ - Coordinate { column: 3, row: 8 }, - Coordinate { column: 3, row: 9 }, - Coordinate { column: 4, row: 9 }, - ] - .into_iter() - .collect(), - }), - Some(6), - ), - ( - board.clone(), - Coordinate { column: 0, row: 18 }, - Some(Group { - color: Color::White, - coordinates: vec![ - Coordinate { column: 0, row: 17 }, - Coordinate { column: 0, row: 18 }, - Coordinate { column: 1, row: 18 }, - ] - .into_iter() - .collect(), - }), - Some(1), - ), - ( - board.clone(), - Coordinate { column: 0, row: 17 }, - Some(Group { - color: Color::White, - coordinates: vec![ - Coordinate { column: 0, row: 17 }, - Coordinate { column: 0, row: 18 }, - Coordinate { column: 1, row: 18 }, - ] - .into_iter() - .collect(), - }), - Some(1), - ), - ]; - - 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 surrounding_a_stone_removes_it() { - with_example_board(|mut board| { - println!("{}", board); - board.place_stone(Coordinate { column: 10, row: 9 }, Color::Black); - println!("{}", board); - assert!(board.stone(Coordinate { column: 9, row: 9 }).is_none()); - assert!(false); - }); - } - - fn sorrounding_a_group_removes_it() { - assert!(false); - } - - fn suicide_is_forbidden() { - assert!(false); - } - - fn captures_preceed_self_capture() { - assert!(false); - } -} diff --git a/kifu/kifu-core/src/ui/playing_field.rs b/kifu/kifu-core/src/ui/playing_field.rs index cdc9c12..abd5456 100644 --- a/kifu/kifu-core/src/ui/playing_field.rs +++ b/kifu/kifu-core/src/ui/playing_field.rs @@ -1,5 +1,7 @@ -use crate::types::Color; -use crate::{types::GameState, ui::types}; +use crate::{ + types::{Color, GameState}, + ui::types, +}; #[derive(Clone, Debug)] pub struct PlayingFieldView { diff --git a/kifu/kifu-core/src/ui/types.rs b/kifu/kifu-core/src/ui/types.rs index f027ebf..2257d80 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, Coordinate}, + Board, Coordinate, }; #[derive(Clone, Copy, Debug, PartialEq)] @@ -73,7 +73,7 @@ impl From<&Board> for BoardElement { let spaces: Vec = (0..board.size.height) .map(|row| { (0..board.size.width) - .map(|column| match board.stone(Coordinate { column, row }) { + .map(|column| match board.stone(&Coordinate { column, row }) { Some(color) => IntersectionElement::Filled(StoneElement { jitter: Jitter { x: 0, y: 0 }, liberties: Some(0), diff --git a/kifu/kifu-gtk/Cargo.lock b/kifu/kifu-gtk/Cargo.lock index 7e5c456..f6bc0df 100644 --- a/kifu/kifu-gtk/Cargo.lock +++ b/kifu/kifu-gtk/Cargo.lock @@ -488,6 +488,15 @@ dependencies = [ "system-deps", ] +[[package]] +name = "grid" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0634107a3a005070dd73e27e74ecb691a94e9e5ba7829f434db7fbf73a6b5c47" +dependencies = [ + "no-std-compat", +] + [[package]] name = "gsk4" version = "0.6.3" @@ -657,6 +666,7 @@ dependencies = [ name = "kifu-core" version = "0.1.0" dependencies = [ + "grid", "tokio", ] @@ -751,6 +761,12 @@ dependencies = [ "getrandom", ] +[[package]] +name = "no-std-compat" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c" + [[package]] name = "num-integer" version = "0.1.45"