Implement the basic rules of Go #40
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue