Implement the basic rules of Go #40
14
Makefile
14
Makefile
|
@ -35,3 +35,17 @@ ifc-dev:
|
|||
ifc-test:
|
||||
cd ifc && make test
|
||||
|
||||
kifu-core/dev:
|
||||
cd kifu/kifu-core && make test
|
||||
|
||||
kifu-core/test:
|
||||
cd kifu/kifu-core && make test
|
||||
|
||||
kifu-core/test-oneshot:
|
||||
cd kifu/kifu-core && make test-oneshot
|
||||
|
||||
kifu-gtk:
|
||||
cd kifu/kifu-gtk && make release
|
||||
|
||||
kifu-gtk/dev:
|
||||
cd kifu/kifu-gtk && make dev
|
||||
|
|
|
@ -38,6 +38,7 @@
|
|||
pkgs.pkg-config
|
||||
pkgs.sqlite
|
||||
pkgs.rustup
|
||||
pkgs.cargo-nextest
|
||||
pkgs.crate2nix
|
||||
];
|
||||
LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";
|
||||
|
|
|
@ -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,8 @@ dependencies = [
|
|||
name = "kifu-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"grid",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
@ -85,6 +96,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"
|
||||
|
@ -193,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"
|
||||
|
@ -221,7 +269,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
|
|||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -6,4 +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" ] }
|
||||
tokio = { version = "1.26", features = [ "full" ] }
|
||||
grid = { version = "0.9" }
|
||||
thiserror = { version = "1" }
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
|
||||
test:
|
||||
cargo watch -x 'nextest run'
|
||||
|
||||
test-oneshot:
|
||||
cargo nextest run
|
|
@ -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)]
|
||||
|
|
|
@ -0,0 +1,633 @@
|
|||
use crate::{BoardError, Color, Size};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Board {
|
||||
pub size: Size,
|
||||
pub groups: Vec<Group>,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Board {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
|
||||
write!(f, " ")?;
|
||||
// for c in 'A'..'U' {
|
||||
for c in 0..19 {
|
||||
write!(f, "{:2}", c)?;
|
||||
}
|
||||
writeln!(f, "")?;
|
||||
|
||||
for row in 0..self.size.height {
|
||||
write!(f, " {: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 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 {
|
||||
size: Size {
|
||||
width: 19,
|
||||
height: 19,
|
||||
},
|
||||
groups: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[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<Self, BoardError> {
|
||||
if let Some(_) = self.stone(&coordinate) {
|
||||
return Err(BoardError::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());
|
||||
|
||||
let adjacent_groups = self.adjacent_groups(&friendly_group);
|
||||
for group in adjacent_groups {
|
||||
if self.liberties(&group) == 0 {
|
||||
self.remove_group(&group);
|
||||
}
|
||||
}
|
||||
|
||||
if self.liberties(&friendly_group) == 0 {
|
||||
return Err(BoardError::SelfCapture);
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
pub fn stone(&self, coordinate: &Coordinate) -> Option<Color> {
|
||||
self.groups
|
||||
.iter()
|
||||
.find(|g| g.contains(coordinate))
|
||||
.map(|g| g.color)
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
pub fn adjacent_groups(&self, group: &Group) -> Vec<Group> {
|
||||
let adjacent_spaces = self.group_halo(group).into_iter();
|
||||
let mut grps: Vec<Group> = Vec::new();
|
||||
|
||||
adjacent_spaces.for_each(|coord| match self.group(&coord) {
|
||||
None => return,
|
||||
Some(adj) => {
|
||||
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()
|
||||
}
|
||||
|
||||
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 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 {
|
||||
fn contains(&self, coordinate: &Coordinate) -> bool {
|
||||
self.coordinates.contains(coordinate)
|
||||
}
|
||||
}
|
||||
|
||||
#[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),
|
||||
/* */
|
||||
(Coordinate { column: 17, row: 0 }, Color::White),
|
||||
(Coordinate { column: 17, row: 1 }, Color::White),
|
||||
(Coordinate { column: 18, row: 1 }, Color::White),
|
||||
]
|
||||
.into_iter(),
|
||||
)
|
||||
.unwrap();
|
||||
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(),
|
||||
)
|
||||
.unwrap();
|
||||
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(|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(|board| {
|
||||
let board = board
|
||||
.place_stone(Coordinate { column: 10, row: 9 }, Color::Black)
|
||||
.unwrap();
|
||||
assert!(board.stone(&Coordinate { column: 9, row: 9 }).is_none());
|
||||
|
||||
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());
|
||||
|
||||
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());
|
||||
assert!(board.stone(&Coordinate { column: 4, row: 18 }).is_none());
|
||||
assert!(board.stone(&Coordinate { column: 6, row: 18 }).is_none());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn self_capture_is_forbidden() {
|
||||
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 board = board.clone();
|
||||
let res = board.place_stone(Coordinate { column: 5, row: 18 }, Color::Black);
|
||||
assert_eq!(res, Err(BoardError::SelfCapture));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[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,5 +2,8 @@ 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;
|
||||
pub use board::*;
|
||||
|
|
|
@ -1,16 +1,30 @@
|
|||
use crate::api::PlayStoneRequest;
|
||||
use crate::{
|
||||
api::PlayStoneRequest,
|
||||
board::{Board, Coordinate},
|
||||
};
|
||||
use std::time::Duration;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
#[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 {
|
||||
Black,
|
||||
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 {
|
||||
|
@ -36,7 +50,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 => {}
|
||||
}
|
||||
|
@ -74,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,
|
||||
|
||||
|
@ -88,6 +107,7 @@ impl GameState {
|
|||
fn new() -> GameState {
|
||||
GameState {
|
||||
board: Board::new(),
|
||||
past_positions: vec![],
|
||||
conversation: vec![],
|
||||
current_player: Color::Black,
|
||||
white_player: Player {
|
||||
|
@ -103,46 +123,78 @@ 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) -> Result<(), BoardError> {
|
||||
let board = self.board.clone();
|
||||
let new_board = board.place_stone(coordinate, self.current_player)?;
|
||||
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Board {
|
||||
pub size: Size,
|
||||
pub spaces: Vec<Option<Color>>,
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
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,
|
||||
}
|
||||
#[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);
|
||||
}
|
||||
|
||||
pub fn place_stone(&mut self, column: u8, row: u8, stone: Color) {
|
||||
let addr = self.addr(column, row);
|
||||
self.spaces[addr] = Some(stone);
|
||||
#[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);
|
||||
}
|
||||
|
||||
pub fn stone(&self, column: u8, row: u8) -> Option<Color> {
|
||||
let addr = self.addr(column, row);
|
||||
self.spaces[addr]
|
||||
}
|
||||
#[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();
|
||||
|
||||
fn addr(&self, column: u8, row: u8) -> usize {
|
||||
((row as usize) * (self.size.width as usize) + (column as usize)) as usize
|
||||
state.place_stone(Coordinate { column: 8, row: 9 }).unwrap();
|
||||
assert_eq!(
|
||||
state.place_stone(Coordinate { column: 9, row: 9 }),
|
||||
Err(BoardError::Ko)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use crate::types::{Color, Size};
|
||||
use crate::{
|
||||
api::{PlayStoneRequest, Request},
|
||||
types::Board,
|
||||
Board, Coordinate,
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
|
@ -14,12 +14,14 @@ pub struct Jitter {
|
|||
pub struct StoneElement {
|
||||
pub color: Color,
|
||||
pub jitter: Jitter,
|
||||
pub liberties: Option<u8>,
|
||||
}
|
||||
|
||||
impl StoneElement {
|
||||
pub fn new(color: Color) -> Self {
|
||||
pub fn new(color: Color, liberties: Option<u8>) -> Self {
|
||||
Self {
|
||||
color,
|
||||
liberties,
|
||||
jitter: Jitter { x: 0, y: 0 },
|
||||
}
|
||||
}
|
||||
|
@ -71,9 +73,10 @@ 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 },
|
||||
liberties: None,
|
||||
color,
|
||||
}),
|
||||
None => IntersectionElement::Empty(Request::PlayStoneRequest(
|
||||
|
|
|
@ -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,8 @@ dependencies = [
|
|||
name = "kifu-core"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"grid",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
|
@ -751,6 +762,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"
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
release:
|
||||
cargo build --release
|
||||
|
||||
dev:
|
||||
cargo watch -x 'run --bin kifu-gtk'
|
|
@ -141,7 +141,7 @@ impl ObjectImpl for BoardPrivate {
|
|||
(0..19).for_each(|row| {
|
||||
match board.stone(row, col) {
|
||||
IntersectionElement::Filled(stone) => {
|
||||
pen.stone(&context, row, col, stone.color);
|
||||
pen.stone(&context, row, col, stone.color, stone.liberties);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
@ -270,12 +270,27 @@ impl Pen {
|
|||
let _ = context.fill();
|
||||
}
|
||||
|
||||
fn stone(&self, context: &cairo::Context, row: u8, col: u8, color: Color) {
|
||||
fn stone(
|
||||
&self,
|
||||
context: &cairo::Context,
|
||||
row: u8,
|
||||
col: u8,
|
||||
color: Color,
|
||||
liberties: Option<u8>,
|
||||
) {
|
||||
match color {
|
||||
Color::White => context.set_source_rgb(0.9, 0.9, 0.9),
|
||||
Color::Black => context.set_source_rgb(0.0, 0.0, 0.0),
|
||||
};
|
||||
self.draw_stone(context, row, col);
|
||||
|
||||
if let Some(liberties) = liberties {
|
||||
let stone_location = self.stone_location(row, col);
|
||||
context.set_source_rgb(1., 0., 1.);
|
||||
context.set_font_size(32.);
|
||||
context.move_to(stone_location.0 - 10., stone_location.1 + 10.);
|
||||
let _ = context.show_text(&format!("{}", liberties));
|
||||
}
|
||||
}
|
||||
|
||||
fn ghost_stone(&self, context: &cairo::Context, row: u8, col: u8, color: Color) {
|
||||
|
@ -288,13 +303,15 @@ impl Pen {
|
|||
|
||||
fn draw_stone(&self, context: &cairo::Context, row: u8, col: u8) {
|
||||
let radius = self.hspace_between / 2. - 2.;
|
||||
context.arc(
|
||||
self.x_offset + (col as f64) * self.hspace_between,
|
||||
self.y_offset + (row as f64) * self.vspace_between,
|
||||
radius,
|
||||
0.0,
|
||||
2.0 * std::f64::consts::PI,
|
||||
);
|
||||
let (x_loc, y_loc) = self.stone_location(row, col);
|
||||
context.arc(x_loc, y_loc, radius, 0.0, 2.0 * std::f64::consts::PI);
|
||||
let _ = context.fill();
|
||||
}
|
||||
|
||||
fn stone_location(&self, row: u8, col: u8) -> (f64, f64) {
|
||||
(
|
||||
self.x_offset + (col as f64) * self.hspace_between,
|
||||
self.y_offset + (row as f64) * self.vspace_between,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue