633 lines
21 KiB
Rust
633 lines
21 KiB
Rust
use crate::{BoardError, Color, Size};
|
|
use std::collections::HashSet;
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct Goban {
|
|
pub size: Size,
|
|
pub groups: Vec<Group>,
|
|
}
|
|
|
|
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<Item = (Coordinate, Color)>,
|
|
) -> Result<Self, BoardError> {
|
|
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<Self, BoardError> {
|
|
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<Color> {
|
|
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<Group> {
|
|
let adjacent_spaces = self.group_halo(group).into_iter();
|
|
let mut grps: Vec<Group> = 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<Coordinate> {
|
|
group
|
|
.coordinates
|
|
.iter()
|
|
.flat_map(|c| self.adjacencies(c))
|
|
.collect::<HashSet<Coordinate>>()
|
|
}
|
|
|
|
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<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.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<Coordinate>,
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|