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 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 {
Black,
White,
@ -36,7 +36,10 @@ impl AppState {
pub fn place_stone(&mut self, req: PlayStoneRequest) {
match self.game {
Some(ref mut game) => {
game.place_stone(req.column, req.row);
game.place_stone(Coordinate {
column: req.column,
row: req.row,
});
}
None => {}
}
@ -103,8 +106,8 @@ impl GameState {
}
}
fn place_stone(&mut self, column: u8, row: u8) {
self.board.place_stone(column, row, self.current_player);
fn place_stone(&mut self, coordinate: Coordinate) {
self.board.place_stone(coordinate, self.current_player);
match self.current_player {
Color::White => self.current_player = Color::Black,
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)]
pub struct Board {
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> {
for row in 0..self.size.height {
for column in 0..self.size.width {
match self.stone(column, row) {
match self.stone(Coordinate { column, row }) {
None => write!(f, " . ")?,
Some(Color::Black) => write!(f, " X ")?,
Some(Color::White) => write!(f, " O ")?,
@ -149,21 +199,85 @@ impl Board {
}
}
pub fn place_stone(&mut self, column: u8, row: u8, stone: Color) {
let addr = self.addr(column, row);
fn from_coordinates(coordinates: impl Iterator<Item = (Coordinate, Color)>) -> Self {
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);
}
pub fn stone(&self, column: u8, row: u8) -> Option<Color> {
let addr = self.addr(column, row);
pub fn stone(&self, coordinate: Coordinate) -> Option<Color> {
let addr = self.addr(coordinate.column, coordinate.row);
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 {
((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)]
mod test {
use super::*;
@ -178,43 +292,144 @@ mod test {
* 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]
fn it_counts_individual_liberties() {
let mut board = Board::new();
board.place_stone(3, 3, Color::White);
board.place_stone(0, 3, Color::White);
board.place_stone(0, 0, Color::White);
println!("{}", board);
assert!(false);
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_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() {
let mut board = Board::new();
board.place_stone(3, 3, Color::White);
board.place_stone(3, 4, Color::White);
board.place_stone(3, 5, Color::White);
board.place_stone(4, 4, Color::White);
println!("{}", board);
assert!(false);
let test_cases = vec![
(
Board::from_coordinates(
vec![
(Coordinate { column: 3, row: 3 }, Color::White),
(Coordinate { column: 3, 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 },
]
.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]
fn opposing_stones_reduce_liberties() {
let mut board = Board::new();
board.place_stone(3, 3, Color::White);
board.place_stone(3, 4, Color::Black);
board.place_stone(Coordinate { column: 3, row: 3 }, Color::White);
board.place_stone(Coordinate { column: 3, row: 4 }, Color::Black);
println!("{}", board);
assert!(false);
}

View File

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