Compare commits

..

2 Commits

Author SHA1 Message Date
Savanni D'Gerinel 037484e7b4 Count liberties 2023-04-13 22:38:35 -04:00
Savanni D'Gerinel 6d51ae8479 Add tests 2023-04-13 19:43:40 -04:00
2 changed files with 319 additions and 11 deletions

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,11 +115,75 @@ 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,
pub spaces: Vec<Option<Color>>,
}
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(Coordinate { column, row }) {
None => write!(f, " . ")?,
Some(Color::Black) => write!(f, " X ")?,
Some(Color::White) => write!(f, " O ")?,
}
}
writeln!(f, "")?;
}
Ok(())
}
}
impl Board {
fn new() -> Self {
let mut spaces = Vec::new();
@ -132,17 +199,258 @@ 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::*;
/* 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.
*/
#[test]
fn it_counts_individual_liberties() {
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 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(Coordinate { column: 3, row: 3 }, Color::White);
board.place_stone(Coordinate { column: 3, row: 4 }, Color::Black);
println!("{}", board);
assert!(false);
}
#[test]
fn surrounding_a_stone_remove_it() {
assert!(false);
}
#[test]
fn sorrounding_a_group_removes_it() {
assert!(false);
}
#[test]
fn suicide_is_forbidden() {
assert!(false);
}
#[test]
fn captures_preceed_self_capture() {
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,