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 253 additions and 38 deletions
Showing only changes of commit 037484e7b4 - Show all commits

View File

@ -1,7 +1,7 @@
use crate::api::PlayStoneRequest; use crate::api::PlayStoneRequest;
use std::time::Duration; use std::{collections::HashSet, time::Duration};
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
pub enum Color { pub enum Color {
Black, Black,
White, White,
@ -36,7 +36,10 @@ impl AppState {
pub fn place_stone(&mut self, req: PlayStoneRequest) { pub fn place_stone(&mut self, req: PlayStoneRequest) {
match self.game { match self.game {
Some(ref mut game) => { Some(ref mut game) => {
game.place_stone(req.column, req.row); game.place_stone(Coordinate {
column: req.column,
row: req.row,
});
} }
None => {} None => {}
} }
@ -103,8 +106,8 @@ impl GameState {
} }
} }
fn place_stone(&mut self, column: u8, row: u8) { fn place_stone(&mut self, coordinate: Coordinate) {
self.board.place_stone(column, row, self.current_player); self.board.place_stone(coordinate, self.current_player);
match self.current_player { match self.current_player {
Color::White => self.current_player = Color::Black, Color::White => self.current_player = Color::Black,
Color::Black => self.current_player = Color::White, Color::Black => self.current_player = Color::White,
@ -112,6 +115,53 @@ impl GameState {
} }
} }
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
pub struct Coordinate {
pub column: u8,
pub row: u8,
}
impl Coordinate {
fn adjacencies(&self, max_column: u8, max_row: u8) -> impl Iterator<Item = Coordinate> {
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 < max_column {
Some(Coordinate {
column: self.column + 1,
row: self.row,
})
} else {
None
},
if self.row < max_row {
Some(Coordinate {
column: self.column,
row: self.row + 1,
})
} else {
None
},
]
.into_iter()
.filter_map(|v| v)
}
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Board { pub struct Board {
pub size: Size, pub size: Size,
@ -122,7 +172,7 @@ 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> {
for row in 0..self.size.height { for row in 0..self.size.height {
for column in 0..self.size.width { for column in 0..self.size.width {
match self.stone(column, row) { match self.stone(Coordinate { column, row }) {
None => write!(f, " . ")?, None => write!(f, " . ")?,
Some(Color::Black) => write!(f, " X ")?, Some(Color::Black) => write!(f, " X ")?,
Some(Color::White) => write!(f, " O ")?, Some(Color::White) => write!(f, " O ")?,
@ -149,21 +199,85 @@ impl Board {
} }
} }
pub fn place_stone(&mut self, column: u8, row: u8, stone: Color) { fn from_coordinates(coordinates: impl Iterator<Item = (Coordinate, Color)>) -> Self {
let addr = self.addr(column, row); 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); self.spaces[addr] = Some(stone);
} }
pub fn stone(&self, column: u8, row: u8) -> Option<Color> { pub fn stone(&self, coordinate: Coordinate) -> Option<Color> {
let addr = self.addr(column, row); let addr = self.addr(coordinate.column, coordinate.row);
self.spaces[addr] self.spaces[addr]
} }
pub fn group(&self, coordinate: Coordinate) -> Option<Group> {
let mut visited: HashSet<Coordinate> = 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::<Vec<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::<Vec<Coordinate>>(),
);
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::<Vec<Coordinate>>()
.len()
}
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 ((row as usize) * (self.size.width as usize) + (column as usize)) as usize
} }
} }
#[derive(Clone, Debug, PartialEq)]
pub struct Group {
color: Color,
coordinates: HashSet<Coordinate>,
}
impl Group {
fn adjacencies(&self, max_column: u8, max_row: u8) -> HashSet<Coordinate> {
self.coordinates
.iter()
.map(|stone| stone.adjacencies(max_column, max_row))
.flatten()
.collect()
}
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
@ -178,43 +292,144 @@ 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.
*/ */
#[test]
fn it_shows_stones() {
let mut board = Board::new();
board.place_stone(3, 2, Color::White);
board.place_stone(2, 3, Color::White);
board.place_stone(4, 3, Color::White);
board.place_stone(3, 3, Color::Black);
println!("{}", board);
assert!(false);
}
#[test] #[test]
fn it_counts_individual_liberties() { fn it_counts_individual_liberties() {
let mut board = Board::new(); let board = Board::from_coordinates(
board.place_stone(3, 3, Color::White); vec![
board.place_stone(0, 3, Color::White); (Coordinate { column: 3, row: 3 }, Color::White),
board.place_stone(0, 0, Color::White); (Coordinate { column: 0, row: 3 }, Color::White),
println!("{}", board); (Coordinate { column: 0, row: 0 }, Color::White),
assert!(false); (Coordinate { column: 18, row: 9 }, Color::Black),
(
Coordinate {
column: 18,
row: 18,
},
Color::Black,
),
]
.into_iter(),
);
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] #[test]
fn stones_share_liberties() { fn stones_share_liberties() {
let mut board = Board::new(); let test_cases = vec![
board.place_stone(3, 3, Color::White); (
board.place_stone(3, 4, Color::White); Board::from_coordinates(
board.place_stone(3, 5, Color::White); vec![
board.place_stone(4, 4, Color::White); (Coordinate { column: 3, row: 3 }, Color::White),
println!("{}", board); (Coordinate { column: 3, row: 4 }, Color::White),
assert!(false); ]
.into_iter(),
),
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::from_coordinates(
vec![
(Coordinate { column: 3, row: 3 }, Color::White),
(Coordinate { column: 3, row: 4 }, Color::White),
(Coordinate { column: 4, row: 4 }, Color::White),
]
.into_iter(),
),
Coordinate { column: 3, row: 4 },
Some(Group {
color: Color::White,
coordinates: vec![
Coordinate { column: 3, row: 3 },
Coordinate { column: 3, row: 4 },
Coordinate { column: 4, row: 4 },
]
.into_iter()
.collect(),
}),
Some(7),
),
(
Board::from_coordinates(
vec![
(Coordinate { column: 3, row: 3 }, Color::White),
(Coordinate { column: 3, row: 4 }, Color::White),
(Coordinate { column: 4, row: 4 }, Color::White),
(Coordinate { column: 3, row: 5 }, Color::White),
]
.into_iter(),
),
Coordinate { column: 3, row: 4 },
Some(Group {
color: Color::White,
coordinates: vec![
Coordinate { column: 3, row: 3 },
Coordinate { column: 3, row: 4 },
Coordinate { column: 4, row: 4 },
Coordinate { column: 3, row: 5 },
]
.into_iter()
.collect(),
}),
Some(8),
),
];
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] #[test]
fn opposing_stones_reduce_liberties() { fn opposing_stones_reduce_liberties() {
let mut board = Board::new(); let mut board = Board::new();
board.place_stone(3, 3, Color::White); board.place_stone(Coordinate { column: 3, row: 3 }, Color::White);
board.place_stone(3, 4, Color::Black); board.place_stone(Coordinate { column: 3, row: 4 }, Color::Black);
println!("{}", board); println!("{}", board);
assert!(false); assert!(false);
} }

View File

@ -1,7 +1,7 @@
use crate::types::{Color, Size}; use crate::types::{Color, Size};
use crate::{ use crate::{
api::{PlayStoneRequest, Request}, api::{PlayStoneRequest, Request},
types::Board, types::{Board, Coordinate},
}; };
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
@ -71,7 +71,7 @@ impl From<&Board> for BoardElement {
let spaces: Vec<IntersectionElement> = (0..board.size.height) let spaces: Vec<IntersectionElement> = (0..board.size.height)
.map(|row| { .map(|row| {
(0..board.size.width) (0..board.size.width)
.map(|column| match board.stone(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 },
color, color,