use crate::{Color, Size}; use grid::Grid; use std::collections::HashSet; #[derive(Clone, Debug, PartialEq)] pub enum Error { InvalidPosition, SelfCapture, } #[derive(Clone, Debug)] pub struct Board { pub size: Size, 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, }, 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 old_groups = self.groups.clone(); 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()); let adjacent_groups = self.adjacent_groups(&friendly_group); for group in adjacent_groups { if self.liberties(&group) == 0 { self.remove_group(&group); } } if self.liberties(&friendly_group) == 0 { self.groups = old_groups; return Err(Error::SelfCapture); } Ok(()) } pub fn stone(&self, coordinate: &Coordinate) -> Option { self.groups .iter() .find(|g| g.contains(coordinate)) .map(|g| g.color) } 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); } pub fn adjacent_groups(&self, group: &Group) -> Vec { let adjacent_spaces = self.group_halo(group).into_iter(); let mut grps: Vec = Vec::new(); adjacent_spaces.for_each(|coord| match self.group(&coord) { None => return, Some(adj) => { 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() } 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 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 { fn contains(&self, coordinate: &Coordinate) -> bool { self.coordinates.contains(coordinate) } } #[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), /* */ (Coordinate { column: 17, row: 0 }, Color::White), (Coordinate { column: 17, row: 1 }, Color::White), (Coordinate { column: 18, row: 1 }, 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()); }); } #[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)); let res = board.place_stone(Coordinate { column: 5, row: 18 }, Color::Black); assert_eq!(res, Err(Error::SelfCapture)); }); } fn captures_preceed_self_capture() { assert!(false); } }