use crate::{BoardError, Color, Size}; use std::collections::HashSet; #[derive(Clone, Debug, Default)] pub struct Goban { pub size: Size, pub groups: Vec, } impl std::fmt::Display for Goban { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { write!(f, " ")?; // for c in 'A'..'U' { for c in 0..19 { write!(f, "{:2}", c)?; } writeln!(f)?; for row in 0..self.size.height { write!(f, " {: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 PartialEq for Goban { 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; } } true } } impl Goban { pub fn new() -> Self { Self { size: Size { width: 19, height: 19, }, groups: Vec::new(), } } pub fn from_coordinates( mut coordinates: impl Iterator, ) -> Result { coordinates.try_fold(Self::new(), |board, (coordinate, color)| { board.place_stone(coordinate, color) }) } } #[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)] pub struct Coordinate { pub column: u8, pub row: u8, } impl Goban { pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result { if self.stone(&coordinate).is_some() { return Err(BoardError::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); 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 { return Err(BoardError::SelfCapture); } Ok(self) } 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 => {} 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() .flat_map(|c| self.adjacencies(c)) .collect::>() } pub fn liberties(&self, group: &Group) -> usize { self.group_halo(group) .into_iter() .filter(|c| self.stone(c).is_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(Goban)) { let board = Goban::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(), ) .unwrap(); test(board); } #[test] fn it_gets_adjacencies_for_coordinate() { let board = Goban::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 = Goban::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(), ) .unwrap(); 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: Goban| { 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(|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(|board| { let board = board .place_stone(Coordinate { column: 10, row: 9 }, Color::Black) .unwrap(); assert!(board.stone(&Coordinate { column: 9, row: 9 }).is_none()); 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()); 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()); 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(|board| { { let board = board.clone(); let res = board.place_stone(Coordinate { column: 18, row: 0 }, Color::Black); assert_eq!(res, Err(BoardError::SelfCapture)); } { let board = board.clone(); let res = board.place_stone(Coordinate { column: 5, row: 18 }, Color::Black); assert_eq!(res, Err(BoardError::SelfCapture)); } }); } #[test] fn validate_group_comparisons() { { let b1 = Goban::from_coordinates( vec![(Coordinate { column: 7, row: 9 }, Color::White)].into_iter(), ) .unwrap(); let b2 = Goban::from_coordinates( vec![(Coordinate { column: 7, row: 9 }, Color::White)].into_iter(), ) .unwrap(); assert_eq!(b1, b2); } { let b1 = Goban::from_coordinates( vec![ (Coordinate { column: 7, row: 9 }, Color::White), (Coordinate { column: 8, row: 10 }, Color::White), ] .into_iter(), ) .unwrap(); let b2 = Goban::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 = Goban::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); } }