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