Implement the basic rules of Go #40

Merged
savanni merged 13 commits from feature/go-rules into main 2023-05-04 02:34:40 +00:00
2 changed files with 243 additions and 121 deletions
Showing only changes of commit 52e27318fe - Show all commits

View File

@ -122,7 +122,11 @@ pub struct Coordinate {
} }
impl Coordinate { impl Coordinate {
fn adjacencies(&self, max_column: u8, max_row: u8) -> impl Iterator<Item = 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<Item = Coordinate> {
vec![ vec![
if self.column > 0 { if self.column > 0 {
Some(Coordinate { Some(Coordinate {
@ -140,7 +144,7 @@ impl Coordinate {
} else { } else {
None None
}, },
if self.column < max_column { if self.column < width - 1 {
Some(Coordinate { Some(Coordinate {
column: self.column + 1, column: self.column + 1,
row: self.row, row: self.row,
@ -148,7 +152,7 @@ impl Coordinate {
} else { } else {
None None
}, },
if self.row < max_row { if self.row < height - 1 {
Some(Coordinate { Some(Coordinate {
column: self.column, column: self.column,
row: self.row + 1, row: self.row + 1,
@ -171,11 +175,9 @@ pub struct Board {
impl std::fmt::Display for Board { impl std::fmt::Display for Board {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
print!(" "); print!(" ");
for c in 'A'..'U' { // for c in 'A'..'U' {
if c == 'I' { for c in 0..19 {
continue; print!("{:2}", c);
}
print!(" {}", c);
} }
println!(""); println!("");
@ -220,6 +222,26 @@ impl Board {
pub fn place_stone(&mut self, coordinate: Coordinate, stone: Color) { pub fn place_stone(&mut self, coordinate: Coordinate, stone: Color) {
let addr = self.addr(coordinate.column, coordinate.row); let addr = self.addr(coordinate.column, coordinate.row);
self.spaces[addr] = Some(stone); 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<Color> { pub fn stone(&self, coordinate: Coordinate) -> Option<Color> {
@ -234,6 +256,9 @@ impl Board {
let mut fringes = coordinate let mut fringes = coordinate
.adjacencies(self.size.width as u8 - 1, self.size.height as u8 - 1) .adjacencies(self.size.width as u8 - 1, self.size.height as u8 - 1)
.collect::<Vec<Coordinate>>(); .collect::<Vec<Coordinate>>();
fringes
.iter()
.for_each(|coordinate| println!("group fringes: {:?}", coordinate));
visited.insert(coordinate); visited.insert(coordinate);
while let Some(coordinate) = fringes.pop() { while let Some(coordinate) = fringes.pop() {
@ -268,7 +293,11 @@ impl Board {
} }
fn addr(&self, column: u8, row: u8) -> usize { fn addr(&self, column: u8, row: u8) -> usize {
((row as usize) * (self.size.width as usize) + (column as usize)) as 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
} }
} }
@ -302,6 +331,63 @@ mod test {
* A stone placed in a suicidal position is legal if it captures other stones first. * 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] #[test]
fn it_counts_individual_liberties() { fn it_counts_individual_liberties() {
let board = Board::from_coordinates( let board = Board::from_coordinates(
@ -358,122 +444,158 @@ mod test {
#[test] #[test]
fn stones_share_liberties() { fn stones_share_liberties() {
let board = Board::from_coordinates( with_example_board(|board: Board| {
vec![ let test_cases = vec![
(Coordinate { column: 3, row: 3 }, Color::White), (
(Coordinate { column: 3, row: 4 }, Color::White), board.clone(),
(Coordinate { column: 8, row: 3 }, Color::Black), Coordinate { column: 0, row: 0 },
(Coordinate { column: 9, row: 3 }, Color::Black), Some(Group {
(Coordinate { column: 9, row: 4 }, Color::Black), color: Color::White,
(Coordinate { column: 15, row: 3 }, Color::White), coordinates: vec![
(Coordinate { column: 15, row: 4 }, Color::White), Coordinate { column: 0, row: 0 },
(Coordinate { column: 15, row: 5 }, Color::White), Coordinate { column: 1, row: 0 },
(Coordinate { column: 14, row: 4 }, Color::White), Coordinate { column: 0, row: 1 },
(Coordinate { column: 3, row: 8 }, Color::White), ]
(Coordinate { column: 3, row: 9 }, Color::White), .into_iter()
(Coordinate { column: 4, row: 9 }, Color::White), .collect(),
(Coordinate { column: 3, row: 10 }, Color::Black), }),
] Some(3),
.into_iter(), ),
); (
println!("{}", board); board.clone(),
assert!(false); Coordinate { column: 1, row: 0 },
let test_cases = vec![ Some(Group {
( color: Color::White,
board.clone(), coordinates: vec![
Coordinate { column: 3, row: 4 }, Coordinate { column: 0, row: 0 },
Some(Group { Coordinate { column: 1, row: 0 },
color: Color::White, Coordinate { column: 0, row: 1 },
coordinates: vec![ ]
Coordinate { column: 3, row: 3 }, .into_iter()
Coordinate { column: 3, row: 4 }, .collect(),
] }),
.into_iter() Some(3),
.collect(), ),
}), (
Some(6), board.clone(),
), Coordinate { column: 9, row: 9 },
( Some(Group {
board.clone(), color: Color::White,
Coordinate { column: 9, row: 3 }, coordinates: vec![Coordinate { column: 9, row: 9 }].into_iter().collect(),
Some(Group { }),
color: Color::Black, Some(1),
coordinates: vec![ ),
Coordinate { column: 8, row: 3 }, (
Coordinate { column: 9, row: 3 }, board.clone(),
Coordinate { column: 9, row: 4 }, Coordinate { column: 3, row: 4 },
] Some(Group {
.into_iter() color: Color::White,
.collect(), coordinates: vec![
}), Coordinate { column: 3, row: 3 },
Some(7), Coordinate { column: 3, row: 4 },
), ]
( .into_iter()
board.clone(), .collect(),
Coordinate { column: 15, row: 4 }, }),
Some(Group { Some(6),
color: Color::White, ),
coordinates: vec![ (
Coordinate { column: 15, row: 3 }, board.clone(),
Coordinate { column: 15, row: 4 }, Coordinate { column: 9, row: 3 },
Coordinate { column: 15, row: 5 }, Some(Group {
Coordinate { column: 14, row: 4 }, color: Color::Black,
] coordinates: vec![
.into_iter() Coordinate { column: 8, row: 3 },
.collect(), Coordinate { column: 9, row: 3 },
}), Coordinate { column: 9, row: 4 },
Some(8), ]
), .into_iter()
( .collect(),
board.clone(), }),
Coordinate { column: 3, row: 9 }, Some(7),
Some(Group { ),
color: Color::White, (
coordinates: vec![ board.clone(),
Coordinate { column: 3, row: 8 }, Coordinate { column: 15, row: 4 },
Coordinate { column: 3, row: 9 }, Some(Group {
Coordinate { column: 4, row: 9 }, color: Color::White,
] coordinates: vec![
.into_iter() Coordinate { column: 15, row: 3 },
.collect(), Coordinate { column: 15, row: 4 },
}), Coordinate { column: 15, row: 5 },
Some(6), 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 { for (board, coordinate, group, liberties) in test_cases {
assert_eq!(board.group(coordinate), group); assert_eq!(board.group(coordinate), group);
assert_eq!( assert_eq!(
board.group(coordinate).map(|g| board.liberties(&g)), board.group(coordinate).map(|g| board.liberties(&g)),
liberties liberties
); );
} }
});
} }
#[test] #[test]
fn opposing_stones_reduce_liberties() {
let test_cases = vec![(
Board::from_coordinates(
vec![
(Coordinate { column: 3, row: 3 }, Color::White),
(Coordinate { column: 3, row: 4 }, Color::Black),
]
.into_iter(),
),
Coordinate { column: 3, row: 4 },
Some(6),
)];
println!("{}", test_cases[0].0);
for (board, coordinate, liberties) in test_cases {
assert_eq!(
board.group(coordinate).map(|g| board.liberties(&g)),
liberties
);
}
}
fn surrounding_a_stone_removes_it() { fn surrounding_a_stone_removes_it() {
assert!(false); 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() { fn sorrounding_a_group_removes_it() {

View File

@ -76,7 +76,7 @@ impl From<&Board> for BoardElement {
.map(|column| match board.stone(Coordinate { column, row }) { .map(|column| match board.stone(Coordinate { column, row }) {
Some(color) => IntersectionElement::Filled(StoneElement { Some(color) => IntersectionElement::Filled(StoneElement {
jitter: Jitter { x: 0, y: 0 }, jitter: Jitter { x: 0, y: 0 },
liberties: None, liberties: Some(0),
color, color,
}), }),
None => IntersectionElement::Empty(Request::PlayStoneRequest( None => IntersectionElement::Empty(Request::PlayStoneRequest(