Implement the basic rules of Go #40
|
@ -49,6 +49,7 @@ name = "kifu-core"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"grid",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
@ -209,6 +210,37 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.12",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.26.0"
|
||||
|
@ -237,7 +269,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -6,5 +6,6 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.26", features = [ "full" ] }
|
||||
grid = { version = "0.9" }
|
||||
tokio = { version = "1.26", features = [ "full" ] }
|
||||
grid = { version = "0.9" }
|
||||
thiserror = { version = "1" }
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
use crate::{Color, Size};
|
||||
use grid::Grid;
|
||||
use crate::{BoardError, Color, Size};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum Error {
|
||||
InvalidPosition,
|
||||
SelfCapture,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Board {
|
||||
pub size: Size,
|
||||
|
@ -16,15 +9,15 @@ pub struct Board {
|
|||
|
||||
impl std::fmt::Display for Board {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
print!(" ");
|
||||
write!(f, " ")?;
|
||||
// for c in 'A'..'U' {
|
||||
for c in 0..19 {
|
||||
print!("{:2}", c);
|
||||
write!(f, "{:2}", c)?;
|
||||
}
|
||||
println!("");
|
||||
writeln!(f, "")?;
|
||||
|
||||
for row in 0..self.size.height {
|
||||
print!(" {:2}", row);
|
||||
write!(f, " {:2}", row)?;
|
||||
for column in 0..self.size.width {
|
||||
match self.stone(&Coordinate { column, row }) {
|
||||
None => write!(f, " .")?,
|
||||
|
@ -38,6 +31,26 @@ impl std::fmt::Display for Board {
|
|||
}
|
||||
}
|
||||
|
||||
impl PartialEq for Board {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
if self.size != other.size {
|
||||
return false;
|
||||
}
|
||||
|
||||
for group in self.groups.iter() {
|
||||
if !other.groups.contains(&group) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for group in other.groups.iter() {
|
||||
if !self.groups.contains(&group) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
impl Board {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
|
@ -49,12 +62,12 @@ impl Board {
|
|||
}
|
||||
}
|
||||
|
||||
pub 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 from_coordinates(
|
||||
mut coordinates: impl Iterator<Item = (Coordinate, Color)>,
|
||||
) -> Result<Self, BoardError> {
|
||||
coordinates.try_fold(Self::new(), |board, (coordinate, color)| {
|
||||
board.place_stone(coordinate, color)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,13 +78,11 @@ pub struct Coordinate {
|
|||
}
|
||||
|
||||
impl Board {
|
||||
pub fn place_stone(&mut self, coordinate: Coordinate, color: Color) -> Result<(), Error> {
|
||||
pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result<Self, BoardError> {
|
||||
if let Some(_) = self.stone(&coordinate) {
|
||||
return Err(Error::InvalidPosition);
|
||||
return Err(BoardError::InvalidPosition);
|
||||
}
|
||||
|
||||
let old_groups = self.groups.clone();
|
||||
|
||||
let mut friendly_group = self
|
||||
.adjacencies(&coordinate)
|
||||
.into_iter()
|
||||
|
@ -99,11 +110,10 @@ impl Board {
|
|||
}
|
||||
|
||||
if self.liberties(&friendly_group) == 0 {
|
||||
self.groups = old_groups;
|
||||
return Err(Error::SelfCapture);
|
||||
return Err(BoardError::SelfCapture);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn stone(&self, coordinate: &Coordinate) -> Option<Color> {
|
||||
|
@ -268,7 +278,8 @@ mod test {
|
|||
(Coordinate { column: 18, row: 1 }, Color::White),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
test(board);
|
||||
}
|
||||
|
||||
|
@ -307,7 +318,8 @@ mod test {
|
|||
),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
)
|
||||
.unwrap();
|
||||
assert!(board.group(&Coordinate { column: 18, row: 3 }).is_none());
|
||||
assert_eq!(
|
||||
board
|
||||
|
@ -494,8 +506,7 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn it_finds_adjacent_groups() {
|
||||
with_example_board(|mut board| {
|
||||
println!("{}", board);
|
||||
with_example_board(|board| {
|
||||
let group = board
|
||||
.group(&Coordinate { column: 0, row: 0 })
|
||||
.cloned()
|
||||
|
@ -512,17 +523,23 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn surrounding_a_group_removes_it() {
|
||||
with_example_board(|mut board| {
|
||||
board.place_stone(Coordinate { column: 10, row: 9 }, Color::Black);
|
||||
with_example_board(|board| {
|
||||
let board = board
|
||||
.place_stone(Coordinate { column: 10, row: 9 }, Color::Black)
|
||||
.unwrap();
|
||||
assert!(board.stone(&Coordinate { column: 9, row: 9 }).is_none());
|
||||
|
||||
board.place_stone(Coordinate { column: 2, row: 18 }, Color::Black);
|
||||
let board = board
|
||||
.place_stone(Coordinate { column: 2, row: 18 }, Color::Black)
|
||||
.unwrap();
|
||||
assert!(board.stone(&Coordinate { column: 0, row: 18 }).is_none());
|
||||
assert!(board.stone(&Coordinate { column: 1, row: 18 }).is_none());
|
||||
assert!(board.stone(&Coordinate { column: 0, row: 17 }).is_none());
|
||||
assert!(board.group(&Coordinate { column: 0, row: 18 }).is_none());
|
||||
|
||||
board.place_stone(Coordinate { column: 5, row: 18 }, Color::White);
|
||||
let board = board
|
||||
.place_stone(Coordinate { column: 5, row: 18 }, Color::White)
|
||||
.unwrap();
|
||||
assert!(board.stone(&Coordinate { column: 4, row: 17 }).is_none());
|
||||
assert!(board.stone(&Coordinate { column: 5, row: 17 }).is_none());
|
||||
assert!(board.stone(&Coordinate { column: 6, row: 17 }).is_none());
|
||||
|
@ -533,16 +550,84 @@ mod test {
|
|||
|
||||
#[test]
|
||||
fn self_capture_is_forbidden() {
|
||||
with_example_board(|mut board| {
|
||||
let res = board.place_stone(Coordinate { column: 18, row: 0 }, Color::Black);
|
||||
assert_eq!(res, Err(Error::SelfCapture));
|
||||
with_example_board(|board| {
|
||||
{
|
||||
let board = board.clone();
|
||||
let res = board.place_stone(Coordinate { column: 18, row: 0 }, Color::Black);
|
||||
assert_eq!(res, Err(BoardError::SelfCapture));
|
||||
}
|
||||
|
||||
let res = board.place_stone(Coordinate { column: 5, row: 18 }, Color::Black);
|
||||
assert_eq!(res, Err(Error::SelfCapture));
|
||||
{
|
||||
let board = board.clone();
|
||||
let res = board.place_stone(Coordinate { column: 5, row: 18 }, Color::Black);
|
||||
assert_eq!(res, Err(BoardError::SelfCapture));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn captures_preceed_self_capture() {
|
||||
assert!(false);
|
||||
#[test]
|
||||
fn validate_group_comparisons() {
|
||||
{
|
||||
let b1 = Board::from_coordinates(
|
||||
vec![(Coordinate { column: 7, row: 9 }, Color::White)].into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
let b2 = Board::from_coordinates(
|
||||
vec![(Coordinate { column: 7, row: 9 }, Color::White)].into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(b1, b2);
|
||||
}
|
||||
|
||||
{
|
||||
let b1 = Board::from_coordinates(
|
||||
vec![
|
||||
(Coordinate { column: 7, row: 9 }, Color::White),
|
||||
(Coordinate { column: 8, row: 10 }, Color::White),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
let b2 = Board::from_coordinates(
|
||||
vec![
|
||||
(Coordinate { column: 8, row: 10 }, Color::White),
|
||||
(Coordinate { column: 7, row: 9 }, Color::White),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(b1, b2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_boards_can_be_compared() {
|
||||
let board = Board::from_coordinates(
|
||||
vec![
|
||||
(Coordinate { column: 7, row: 9 }, Color::White),
|
||||
(Coordinate { column: 8, row: 8 }, Color::White),
|
||||
(Coordinate { column: 8, row: 10 }, Color::White),
|
||||
(Coordinate { column: 9, row: 9 }, Color::White),
|
||||
(Coordinate { column: 10, row: 9 }, Color::Black),
|
||||
(Coordinate { column: 9, row: 8 }, Color::Black),
|
||||
(Coordinate { column: 9, row: 10 }, Color::Black),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let b1 = board
|
||||
.clone()
|
||||
.place_stone(Coordinate { column: 8, row: 9 }, Color::Black)
|
||||
.unwrap();
|
||||
|
||||
let b2 = b1
|
||||
.clone()
|
||||
.place_stone(Coordinate { column: 9, row: 9 }, Color::White)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(board, b2);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ mod api;
|
|||
pub use api::{CoreApp, Request, Response};
|
||||
|
||||
mod types;
|
||||
pub use types::{Color, Size};
|
||||
pub use types::{BoardError, Color, Size};
|
||||
pub mod ui;
|
||||
|
||||
mod board;
|
||||
|
|
|
@ -3,6 +3,17 @@ use crate::{
|
|||
board::{Board, Coordinate},
|
||||
};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Debug, PartialEq, Error)]
|
||||
pub enum BoardError {
|
||||
#[error("Position is invalid")]
|
||||
InvalidPosition,
|
||||
#[error("Self-capture is forbidden")]
|
||||
SelfCapture,
|
||||
#[error("Ko")]
|
||||
Ko,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
|
||||
pub enum Color {
|
||||
|
@ -80,6 +91,8 @@ pub struct Player {
|
|||
|
||||
pub struct GameState {
|
||||
pub board: Board,
|
||||
pub past_positions: Vec<Board>,
|
||||
|
||||
pub conversation: Vec<String>,
|
||||
pub current_player: Color,
|
||||
|
||||
|
@ -94,6 +107,7 @@ impl GameState {
|
|||
fn new() -> GameState {
|
||||
GameState {
|
||||
board: Board::new(),
|
||||
past_positions: vec![],
|
||||
conversation: vec![],
|
||||
current_player: Color::Black,
|
||||
white_player: Player {
|
||||
|
@ -109,11 +123,79 @@ impl GameState {
|
|||
}
|
||||
}
|
||||
|
||||
fn place_stone(&mut self, coordinate: Coordinate) {
|
||||
self.board.place_stone(coordinate, self.current_player);
|
||||
fn place_stone(&mut self, coordinate: Coordinate) -> Result<(), BoardError> {
|
||||
let board = self.board.clone();
|
||||
let new_board = board.place_stone(coordinate, self.current_player)?;
|
||||
|
||||
println!("past positions: {}", self.past_positions.len());
|
||||
if self.past_positions.contains(&new_board) {
|
||||
return Err(BoardError::Ko);
|
||||
}
|
||||
|
||||
self.past_positions.push(self.board.clone());
|
||||
self.board = new_board;
|
||||
match self.current_player {
|
||||
Color::White => self.current_player = Color::Black,
|
||||
Color::Black => self.current_player = Color::White,
|
||||
}
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn current_player_changes_after_move() {
|
||||
let mut state = GameState::new();
|
||||
assert_eq!(state.current_player, Color::Black);
|
||||
state.place_stone(Coordinate { column: 9, row: 9 }).unwrap();
|
||||
assert_eq!(state.current_player, Color::White);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn current_player_remains_the_same_after_self_capture() {
|
||||
let mut state = GameState::new();
|
||||
state.board = Board::from_coordinates(
|
||||
vec![
|
||||
(Coordinate { column: 17, row: 0 }, Color::White),
|
||||
(Coordinate { column: 17, row: 1 }, Color::White),
|
||||
(Coordinate { column: 18, row: 1 }, Color::White),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
state.current_player = Color::Black;
|
||||
|
||||
assert_eq!(
|
||||
state.place_stone(Coordinate { column: 18, row: 0 }),
|
||||
Err(BoardError::SelfCapture)
|
||||
);
|
||||
assert_eq!(state.current_player, Color::Black);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ko_rules_are_enforced() {
|
||||
let mut state = GameState::new();
|
||||
state.board = Board::from_coordinates(
|
||||
vec![
|
||||
(Coordinate { column: 7, row: 9 }, Color::White),
|
||||
(Coordinate { column: 8, row: 8 }, Color::White),
|
||||
(Coordinate { column: 8, row: 10 }, Color::White),
|
||||
(Coordinate { column: 9, row: 9 }, Color::White),
|
||||
(Coordinate { column: 10, row: 9 }, Color::Black),
|
||||
(Coordinate { column: 9, row: 8 }, Color::Black),
|
||||
(Coordinate { column: 9, row: 10 }, Color::Black),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
state.place_stone(Coordinate { column: 8, row: 9 }).unwrap();
|
||||
assert_eq!(
|
||||
state.place_stone(Coordinate { column: 9, row: 9 }),
|
||||
Err(BoardError::Ko)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -667,6 +667,7 @@ name = "kifu-core"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"grid",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
|
Loading…
Reference in New Issue