Overhaul the board representation

Groups become more first-class objects, where the grid becomes useful for quick lookups of values.
This commit is contained in:
Savanni D'Gerinel 2023-04-28 01:21:23 -04:00
parent 52e27318fe
commit 78af31f6f9
9 changed files with 664 additions and 508 deletions

View File

@ -26,6 +26,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "grid"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0634107a3a005070dd73e27e74ecb691a94e9e5ba7829f434db7fbf73a6b5c47"
dependencies = [
"no-std-compat",
]
[[package]]
name = "hermit-abi"
version = "0.2.6"
@ -39,6 +48,7 @@ dependencies = [
name = "kifu-core"
version = "0.1.0"
dependencies = [
"grid",
"tokio",
]
@ -85,6 +95,12 @@ dependencies = [
"windows-sys",
]
[[package]]
name = "no-std-compat"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
[[package]]
name = "num_cpus"
version = "1.15.0"

View File

@ -6,4 +6,5 @@ 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" ] }
tokio = { version = "1.26", features = [ "full" ] }
grid = { version = "0.9" }

View File

@ -10,8 +10,8 @@ pub enum Request {
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PlayStoneRequest {
pub column: u8,
pub row: u8,
pub column: usize,
pub row: usize,
}
#[derive(Clone, Debug)]

611
kifu/kifu-core/src/board.rs Normal file
View File

@ -0,0 +1,611 @@
use crate::{Color, Size};
use grid::Grid;
use std::collections::HashSet;
#[derive(Clone, Debug)]
pub enum Error {
InvalidPosition,
}
#[derive(Clone, Debug)]
pub struct Board {
pub size: Size,
pub grid: Grid<Option<Color>>,
pub groups: Vec<Group>,
}
impl std::fmt::Display for Board {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
print!(" ");
// for c in 'A'..'U' {
for c in 0..19 {
print!("{:2}", c);
}
println!("");
for row in 0..self.size.height {
print!(" {:2}", row);
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 {
pub fn new() -> Self {
Self {
size: Size {
width: 19,
height: 19,
},
grid: Grid::new(19, 19),
groups: Vec::new(),
}
}
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
}
}
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
pub struct Coordinate {
pub column: usize,
pub row: usize,
}
impl Board {
pub fn place_stone(&mut self, coordinate: Coordinate, color: Color) -> Result<(), Error> {
if let Some(_) = self.stone(&coordinate) {
return Err(Error::InvalidPosition);
}
let mut friendly_group = self
.adjacencies(&coordinate)
.into_iter()
.filter(|c| self.stone(c) == Some(color))
.filter_map(|c| self.group(&c).map(|g| g.coordinates.clone()))
.fold(HashSet::new(), |acc, set| {
acc.union(&set).cloned().collect()
});
friendly_group.insert(coordinate.clone());
self.groups
.retain(|g| g.coordinates.is_disjoint(&friendly_group));
let friendly_group = Group {
color,
coordinates: friendly_group,
};
self.groups.push(friendly_group.clone());
match self.grid.get_mut(coordinate.row, coordinate.column) {
None => return Err(Error::InvalidPosition),
Some(space) => *space = Some(color),
}
let adjacent_groups = self.adjacent_groups(&friendly_group);
for group in adjacent_groups {
if self.liberties(&group) == 0 {
self.remove_group(&group);
}
}
Ok(())
}
pub fn stone(&self, coordinate: &Coordinate) -> Option<Color> {
match self.grid.get(coordinate.row, coordinate.column) {
None => None,
Some(val) => *val,
}
}
pub fn group(&self, coordinate: &Coordinate) -> Option<&Group> {
self.groups
.iter()
.find(|g| g.coordinates.contains(coordinate))
}
pub fn remove_group(&mut self, group: &Group) {
self.groups.retain(|g| g != group);
for coord in group.coordinates.iter() {
match self.grid.get_mut(coord.row, coord.column) {
None => (),
Some(v) => *v = None,
}
}
}
pub fn adjacent_groups(&self, group: &Group) -> Vec<Group> {
let adjacent_spaces = self.group_halo(group).into_iter();
println!("adjacent spaces: {:?}", adjacent_spaces);
let mut grps: Vec<Group> = Vec::new();
adjacent_spaces.for_each(|coord| match self.group(&coord) {
None => return,
Some(adj) => {
println!("group found: {:?}", adj.color);
if group.color == adj.color {
return;
}
if grps.iter().any(|g| g.coordinates.contains(&coord)) {
return;
}
grps.push(adj.clone());
}
});
grps
}
pub fn group_halo(&self, group: &Group) -> HashSet<Coordinate> {
group
.coordinates
.iter()
.map(|c| self.adjacencies(c))
.flatten()
.collect::<HashSet<Coordinate>>()
}
pub fn liberties(&self, group: &Group) -> usize {
self.group_halo(group)
.into_iter()
.filter(|c| self.stone(&c) == None)
.count()
// let adjacencies: HashSet<Coordinate> = self.adjacencies(group.coordinates.iter()).collect();
/*
println!("adjacencies: {:?}", adjacencies);
let opposing_spaces = adjacencies
.iter()
.filter(|coord| match self.grid.get(coord.row, coord.column) {
None => true,
Some(&None) => false,
Some(&Some(c)) => c != group.color,
})
.cloned()
.collect::<HashSet<Coordinate>>();
println!("opposition: {:?}", opposing_spaces);
adjacencies.len() - opposing_spaces.len()
*/
/*
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()
*/
}
pub fn adjacencies(&self, coordinate: &Coordinate) -> Vec<Coordinate> {
let mut v = Vec::new();
if coordinate.column > 0 {
v.push(Coordinate {
column: coordinate.column - 1,
row: coordinate.row,
});
}
if coordinate.row > 0 {
v.push(Coordinate {
column: coordinate.column,
row: coordinate.row - 1,
});
}
v.push(Coordinate {
column: coordinate.column + 1,
row: coordinate.row,
});
v.push(Coordinate {
column: coordinate.column,
row: coordinate.row + 1,
});
v.into_iter().filter(|c| self.within_board(c)).collect()
}
/*
pub fn adjacencies<'a>(
&'a self,
coordinates: impl Iterator<Item = &'a Coordinate> + 'a,
) -> impl Iterator<Item = Coordinate> + 'a {
coordinates
.map(|coordinate| {
let mut v = Vec::new();
if coordinate.column > 0 {
v.push(Coordinate {
column: coordinate.column - 1,
row: coordinate.row,
});
}
if coordinate.row > 0 {
v.push(Coordinate {
column: coordinate.column,
row: coordinate.row - 1,
});
}
v.push(Coordinate {
column: coordinate.column + 1,
row: coordinate.row,
});
v.push(Coordinate {
column: coordinate.column,
row: coordinate.row + 1,
});
v
})
.flatten()
.filter(|coordinate| self.within_board(coordinate))
}
*/
pub fn within_board(&self, coordinate: &Coordinate) -> bool {
coordinate.column < self.size.width && coordinate.row < self.size.height
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Group {
color: Color,
coordinates: HashSet<Coordinate>,
}
/*
impl Group {
pub fn adjacencies(&self, max_column: usize, max_row: usize) -> 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.
*/
fn with_example_board(test: impl FnOnce(Board)) {
let board = Board::from_coordinates(
vec![
(Coordinate { column: 3, row: 3 }, Color::White),
(Coordinate { column: 3, row: 4 }, Color::White),
/* */
(Coordinate { column: 8, row: 3 }, Color::Black),
(Coordinate { column: 9, row: 3 }, Color::Black),
(Coordinate { column: 9, row: 4 }, Color::Black),
/* */
(Coordinate { column: 15, row: 3 }, Color::White),
(Coordinate { column: 15, row: 4 }, Color::White),
(Coordinate { column: 15, row: 5 }, Color::White),
(Coordinate { column: 14, row: 4 }, Color::White),
/* */
(Coordinate { column: 3, row: 8 }, Color::White),
(Coordinate { column: 3, row: 9 }, Color::White),
(Coordinate { column: 4, row: 9 }, Color::White),
(Coordinate { column: 3, row: 10 }, Color::Black),
/* */
(Coordinate { column: 0, row: 0 }, Color::White),
(Coordinate { column: 1, row: 0 }, Color::White),
(Coordinate { column: 0, row: 1 }, Color::White),
/* */
(Coordinate { column: 9, row: 9 }, Color::White),
(Coordinate { column: 8, row: 9 }, Color::Black),
(Coordinate { column: 9, row: 8 }, Color::Black),
(Coordinate { column: 9, row: 10 }, Color::Black),
/* */
(Coordinate { column: 0, row: 17 }, Color::White),
(Coordinate { column: 1, row: 18 }, Color::White),
(Coordinate { column: 0, row: 18 }, Color::White),
(Coordinate { column: 0, row: 16 }, Color::Black),
(Coordinate { column: 1, row: 17 }, Color::Black),
/* */
(Coordinate { column: 4, row: 17 }, Color::Black),
(Coordinate { column: 5, row: 17 }, Color::Black),
(Coordinate { column: 6, row: 17 }, Color::Black),
(Coordinate { column: 4, row: 18 }, Color::Black),
(Coordinate { column: 6, row: 18 }, Color::Black),
(Coordinate { column: 3, row: 17 }, Color::White),
(Coordinate { column: 3, row: 18 }, Color::White),
(Coordinate { column: 4, row: 16 }, Color::White),
(Coordinate { column: 5, row: 16 }, Color::White),
(Coordinate { column: 6, row: 16 }, Color::White),
(Coordinate { column: 7, row: 17 }, Color::White),
(Coordinate { column: 7, row: 18 }, Color::White),
]
.into_iter(),
);
test(board);
}
#[test]
fn it_gets_adjacencies_for_coordinate() {
let board = Board::new();
for column in 0..19 {
for row in 0..19 {
for coordinate in board.adjacencies(&Coordinate { column, row }) {
assert!(
board.within_board(&coordinate),
"{} {}: {:?}",
column,
row,
coordinate
);
}
}
}
}
#[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!(board.group(&Coordinate { column: 18, row: 3 }).is_none());
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() {
with_example_board(|board: Board| {
let test_cases = vec![
(
board.clone(),
Coordinate { column: 0, row: 0 },
Some(Group {
color: Color::White,
coordinates: vec![
Coordinate { column: 0, row: 0 },
Coordinate { column: 1, row: 0 },
Coordinate { column: 0, row: 1 },
]
.into_iter()
.collect(),
}),
Some(3),
),
(
board.clone(),
Coordinate { column: 1, row: 0 },
Some(Group {
color: Color::White,
coordinates: vec![
Coordinate { column: 0, row: 0 },
Coordinate { column: 1, row: 0 },
Coordinate { column: 0, row: 1 },
]
.into_iter()
.collect(),
}),
Some(3),
),
(
board.clone(),
Coordinate { column: 9, row: 9 },
Some(Group {
color: Color::White,
coordinates: vec![Coordinate { column: 9, row: 9 }].into_iter().collect(),
}),
Some(1),
),
(
board.clone(),
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.clone(),
Coordinate { column: 9, row: 3 },
Some(Group {
color: Color::Black,
coordinates: vec![
Coordinate { column: 8, row: 3 },
Coordinate { column: 9, row: 3 },
Coordinate { column: 9, row: 4 },
]
.into_iter()
.collect(),
}),
Some(7),
),
(
board.clone(),
Coordinate { column: 15, row: 4 },
Some(Group {
color: Color::White,
coordinates: vec![
Coordinate { column: 15, row: 3 },
Coordinate { column: 15, row: 4 },
Coordinate { column: 15, row: 5 },
Coordinate { column: 14, row: 4 },
]
.into_iter()
.collect(),
}),
Some(8),
),
(
board.clone(),
Coordinate { column: 3, row: 9 },
Some(Group {
color: Color::White,
coordinates: vec![
Coordinate { column: 3, row: 8 },
Coordinate { column: 3, row: 9 },
Coordinate { column: 4, row: 9 },
]
.into_iter()
.collect(),
}),
Some(6),
),
(
board.clone(),
Coordinate { column: 0, row: 18 },
Some(Group {
color: Color::White,
coordinates: vec![
Coordinate { column: 0, row: 17 },
Coordinate { column: 0, row: 18 },
Coordinate { column: 1, row: 18 },
]
.into_iter()
.collect(),
}),
Some(1),
),
(
board.clone(),
Coordinate { column: 0, row: 17 },
Some(Group {
color: Color::White,
coordinates: vec![
Coordinate { column: 0, row: 17 },
Coordinate { column: 0, row: 18 },
Coordinate { column: 1, row: 18 },
]
.into_iter()
.collect(),
}),
Some(1),
),
];
println!("{}", board);
for (board, coordinate, group, liberties) in test_cases {
assert_eq!(board.group(&coordinate), group.as_ref());
assert_eq!(
board.group(&coordinate).map(|g| board.liberties(&g)),
liberties,
"{:?}",
coordinate
);
}
});
}
#[test]
fn it_finds_adjacent_groups() {
with_example_board(|mut board| {
println!("{}", board);
let group = board
.group(&Coordinate { column: 0, row: 0 })
.cloned()
.unwrap();
assert_eq!(board.adjacent_groups(&group), Vec::new());
let group = board
.group(&Coordinate { column: 3, row: 10 })
.cloned()
.unwrap();
assert_eq!(board.adjacent_groups(&group).len(), 1);
});
}
#[test]
fn surrounding_a_group_removes_it() {
with_example_board(|mut board| {
board.place_stone(Coordinate { column: 10, row: 9 }, Color::Black);
assert!(board.stone(&Coordinate { column: 9, row: 9 }).is_none());
board.place_stone(Coordinate { column: 2, row: 18 }, Color::Black);
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);
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());
assert!(board.stone(&Coordinate { column: 4, row: 18 }).is_none());
assert!(board.stone(&Coordinate { column: 6, row: 18 }).is_none());
});
}
fn suicide_is_forbidden() {
assert!(false);
}
fn captures_preceed_self_capture() {
assert!(false);
}
}

View File

@ -4,3 +4,6 @@ pub use api::{CoreApp, Request, Response};
mod types;
pub use types::{Color, Size};
pub mod ui;
mod board;
pub use board::*;

View File

@ -1,5 +1,8 @@
use crate::api::PlayStoneRequest;
use std::{collections::HashSet, time::Duration};
use crate::{
api::PlayStoneRequest,
board::{Board, Coordinate},
};
use std::time::Duration;
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
pub enum Color {
@ -7,10 +10,10 @@ pub enum Color {
White,
}
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Size {
pub width: u8,
pub height: u8,
pub width: usize,
pub height: usize,
}
impl Default for Size {
@ -114,499 +117,3 @@ impl GameState {
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
pub struct Coordinate {
pub column: u8,
pub row: u8,
}
impl Coordinate {
fn within_board(&self, width: u8, height: u8) -> bool {
self.column < width && self.row < height
}
fn adjacencies(&self, width: u8, height: 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 < width - 1 {
Some(Coordinate {
column: self.column + 1,
row: self.row,
})
} else {
None
},
if self.row < height - 1 {
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> {
print!(" ");
// for c in 'A'..'U' {
for c in 0..19 {
print!("{:2}", c);
}
println!("");
for row in 0..self.size.height {
print!(" {:2}", row);
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();
for _ in 0..19 * 19 {
spaces.push(None);
}
Self {
size: Size {
width: 19,
height: 19,
},
spaces,
}
}
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);
let mut to_remove = HashSet::new();
let mut visited = HashSet::new();
for adjacency in coordinate.adjacencies(self.size.width, self.size.height) {
if visited.contains(&adjacency) {
continue;
}
if let Some(group) = self.group(adjacency) {
if self.liberties(&group) == 0 {
to_remove = to_remove.union(&group.coordinates).cloned().collect()
}
visited = visited.union(&group.coordinates).cloned().collect()
}
}
for coordinate in to_remove {
let addr = self.addr(coordinate.column, coordinate.row);
self.spaces[addr] = None;
}
}
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>>();
fringes
.iter()
.for_each(|coordinate| println!("group fringes: {:?}", 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 {
let addr = ((row as usize) * (self.size.width as usize) + (column as usize)) as usize;
if addr >= 361 {
panic!("invalid address: {}, {} {}", addr, column, row);
}
addr
}
}
#[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.
*/
fn with_example_board(test: impl FnOnce(Board)) {
let board = Board::from_coordinates(
vec![
(Coordinate { column: 3, row: 3 }, Color::White),
(Coordinate { column: 3, row: 4 }, Color::White),
/* */
(Coordinate { column: 8, row: 3 }, Color::Black),
(Coordinate { column: 9, row: 3 }, Color::Black),
(Coordinate { column: 9, row: 4 }, Color::Black),
/* */
(Coordinate { column: 15, row: 3 }, Color::White),
(Coordinate { column: 15, row: 4 }, Color::White),
(Coordinate { column: 15, row: 5 }, Color::White),
(Coordinate { column: 14, row: 4 }, Color::White),
/* */
(Coordinate { column: 3, row: 8 }, Color::White),
(Coordinate { column: 3, row: 9 }, Color::White),
(Coordinate { column: 4, row: 9 }, Color::White),
(Coordinate { column: 3, row: 10 }, Color::Black),
/* */
(Coordinate { column: 0, row: 0 }, Color::White),
(Coordinate { column: 1, row: 0 }, Color::White),
(Coordinate { column: 0, row: 1 }, Color::White),
/* */
(Coordinate { column: 9, row: 9 }, Color::White),
(Coordinate { column: 8, row: 9 }, Color::Black),
(Coordinate { column: 9, row: 8 }, Color::Black),
(Coordinate { column: 9, row: 10 }, Color::Black),
/* */
(Coordinate { column: 0, row: 17 }, Color::White),
(Coordinate { column: 1, row: 18 }, Color::White),
(Coordinate { column: 0, row: 18 }, Color::White),
(Coordinate { column: 0, row: 16 }, Color::Black),
(Coordinate { column: 1, row: 17 }, Color::Black),
]
.into_iter(),
);
test(board);
}
#[test]
fn it_gets_adjacencies_for_a_coordinate() {
for column in 0..19 {
for row in 0..19 {
for coordinate in (Coordinate { column, row }.adjacencies(19, 19)) {
assert!(
coordinate.within_board(19, 19),
"{} {}: {:?}",
column,
row,
coordinate
);
}
}
}
}
#[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!(board.group(Coordinate { column: 18, row: 3 }).is_none());
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() {
with_example_board(|board: Board| {
let test_cases = vec![
(
board.clone(),
Coordinate { column: 0, row: 0 },
Some(Group {
color: Color::White,
coordinates: vec![
Coordinate { column: 0, row: 0 },
Coordinate { column: 1, row: 0 },
Coordinate { column: 0, row: 1 },
]
.into_iter()
.collect(),
}),
Some(3),
),
(
board.clone(),
Coordinate { column: 1, row: 0 },
Some(Group {
color: Color::White,
coordinates: vec![
Coordinate { column: 0, row: 0 },
Coordinate { column: 1, row: 0 },
Coordinate { column: 0, row: 1 },
]
.into_iter()
.collect(),
}),
Some(3),
),
(
board.clone(),
Coordinate { column: 9, row: 9 },
Some(Group {
color: Color::White,
coordinates: vec![Coordinate { column: 9, row: 9 }].into_iter().collect(),
}),
Some(1),
),
(
board.clone(),
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.clone(),
Coordinate { column: 9, row: 3 },
Some(Group {
color: Color::Black,
coordinates: vec![
Coordinate { column: 8, row: 3 },
Coordinate { column: 9, row: 3 },
Coordinate { column: 9, row: 4 },
]
.into_iter()
.collect(),
}),
Some(7),
),
(
board.clone(),
Coordinate { column: 15, row: 4 },
Some(Group {
color: Color::White,
coordinates: vec![
Coordinate { column: 15, row: 3 },
Coordinate { column: 15, row: 4 },
Coordinate { column: 15, row: 5 },
Coordinate { column: 14, row: 4 },
]
.into_iter()
.collect(),
}),
Some(8),
),
(
board.clone(),
Coordinate { column: 3, row: 9 },
Some(Group {
color: Color::White,
coordinates: vec![
Coordinate { column: 3, row: 8 },
Coordinate { column: 3, row: 9 },
Coordinate { column: 4, row: 9 },
]
.into_iter()
.collect(),
}),
Some(6),
),
(
board.clone(),
Coordinate { column: 0, row: 18 },
Some(Group {
color: Color::White,
coordinates: vec![
Coordinate { column: 0, row: 17 },
Coordinate { column: 0, row: 18 },
Coordinate { column: 1, row: 18 },
]
.into_iter()
.collect(),
}),
Some(1),
),
(
board.clone(),
Coordinate { column: 0, row: 17 },
Some(Group {
color: Color::White,
coordinates: vec![
Coordinate { column: 0, row: 17 },
Coordinate { column: 0, row: 18 },
Coordinate { column: 1, row: 18 },
]
.into_iter()
.collect(),
}),
Some(1),
),
];
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 surrounding_a_stone_removes_it() {
with_example_board(|mut board| {
println!("{}", board);
board.place_stone(Coordinate { column: 10, row: 9 }, Color::Black);
println!("{}", board);
assert!(board.stone(Coordinate { column: 9, row: 9 }).is_none());
assert!(false);
});
}
fn sorrounding_a_group_removes_it() {
assert!(false);
}
fn suicide_is_forbidden() {
assert!(false);
}
fn captures_preceed_self_capture() {
assert!(false);
}
}

View File

@ -1,5 +1,7 @@
use crate::types::Color;
use crate::{types::GameState, ui::types};
use crate::{
types::{Color, GameState},
ui::types,
};
#[derive(Clone, Debug)]
pub struct PlayingFieldView {

View File

@ -1,7 +1,7 @@
use crate::types::{Color, Size};
use crate::{
api::{PlayStoneRequest, Request},
types::{Board, Coordinate},
Board, Coordinate,
};
#[derive(Clone, Copy, Debug, PartialEq)]
@ -73,7 +73,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(Coordinate { column, row }) {
.map(|column| match board.stone(&Coordinate { column, row }) {
Some(color) => IntersectionElement::Filled(StoneElement {
jitter: Jitter { x: 0, y: 0 },
liberties: Some(0),

View File

@ -488,6 +488,15 @@ dependencies = [
"system-deps",
]
[[package]]
name = "grid"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0634107a3a005070dd73e27e74ecb691a94e9e5ba7829f434db7fbf73a6b5c47"
dependencies = [
"no-std-compat",
]
[[package]]
name = "gsk4"
version = "0.6.3"
@ -657,6 +666,7 @@ dependencies = [
name = "kifu-core"
version = "0.1.0"
dependencies = [
"grid",
"tokio",
]
@ -751,6 +761,12 @@ dependencies = [
"getrandom",
]
[[package]]
name = "no-std-compat"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
[[package]]
name = "num-integer"
version = "0.1.45"