Compare commits

..

4 Commits

9 changed files with 290 additions and 127 deletions

View File

@ -35,3 +35,17 @@ ifc-dev:
ifc-test: ifc-test:
cd ifc && make 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

View File

@ -49,6 +49,7 @@ name = "kifu-core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"grid", "grid",
"thiserror",
"tokio", "tokio",
] ]
@ -209,6 +210,37 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "tokio" name = "tokio"
version = "1.26.0" version = "1.26.0"
@ -237,7 +269,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.109",
] ]
[[package]] [[package]]

View File

@ -6,5 +6,6 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
tokio = { version = "1.26", features = [ "full" ] } tokio = { version = "1.26", features = [ "full" ] }
grid = { version = "0.9" } grid = { version = "0.9" }
thiserror = { version = "1" }

6
kifu/kifu-core/Makefile Normal file
View File

@ -0,0 +1,6 @@
test:
cargo watch -x 'nextest run'
test-oneshot:
cargo nextest run

View File

@ -1,30 +1,23 @@
use crate::{Color, Size}; use crate::{BoardError, Color, Size};
use grid::Grid;
use std::collections::HashSet; use std::collections::HashSet;
#[derive(Clone, Debug)]
pub enum Error {
InvalidPosition,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Board { pub struct Board {
pub size: Size, pub size: Size,
pub grid: Grid<Option<Color>>,
pub groups: Vec<Group>, pub groups: Vec<Group>,
} }
impl std::fmt::Display for Board { 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> {
print!(" "); write!(f, " ")?;
// for c in 'A'..'U' { // for c in 'A'..'U' {
for c in 0..19 { for c in 0..19 {
print!("{:2}", c); write!(f, "{:2}", c)?;
} }
println!(""); writeln!(f, "")?;
for row in 0..self.size.height { for row in 0..self.size.height {
print!(" {:2}", row); write!(f, " {:2}", row)?;
for column in 0..self.size.width { for column in 0..self.size.width {
match self.stone(&Coordinate { column, row }) { match self.stone(&Coordinate { column, row }) {
None => write!(f, " .")?, 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 { impl Board {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
@ -45,17 +58,16 @@ impl Board {
width: 19, width: 19,
height: 19, height: 19,
}, },
grid: Grid::new(19, 19),
groups: Vec::new(), groups: Vec::new(),
} }
} }
pub fn from_coordinates(coordinates: impl Iterator<Item = (Coordinate, Color)>) -> Self { pub fn from_coordinates(
let mut s = Self::new(); mut coordinates: impl Iterator<Item = (Coordinate, Color)>,
for (coordinate, color) in coordinates { ) -> Result<Self, BoardError> {
s.place_stone(coordinate, color); coordinates.try_fold(Self::new(), |board, (coordinate, color)| {
} board.place_stone(coordinate, color)
s })
} }
} }
@ -66,9 +78,9 @@ pub struct Coordinate {
} }
impl Board { 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) { if let Some(_) = self.stone(&coordinate) {
return Err(Error::InvalidPosition); return Err(BoardError::InvalidPosition);
} }
let mut friendly_group = self let mut friendly_group = self
@ -90,11 +102,6 @@ impl Board {
}; };
self.groups.push(friendly_group.clone()); 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); let adjacent_groups = self.adjacent_groups(&friendly_group);
for group in adjacent_groups { for group in adjacent_groups {
if self.liberties(&group) == 0 { if self.liberties(&group) == 0 {
@ -102,14 +109,18 @@ impl Board {
} }
} }
Ok(()) if self.liberties(&friendly_group) == 0 {
return Err(BoardError::SelfCapture);
}
Ok(self)
} }
pub fn stone(&self, coordinate: &Coordinate) -> Option<Color> { pub fn stone(&self, coordinate: &Coordinate) -> Option<Color> {
match self.grid.get(coordinate.row, coordinate.column) { self.groups
None => None, .iter()
Some(val) => *val, .find(|g| g.contains(coordinate))
} .map(|g| g.color)
} }
pub fn group(&self, coordinate: &Coordinate) -> Option<&Group> { pub fn group(&self, coordinate: &Coordinate) -> Option<&Group> {
@ -120,23 +131,15 @@ impl Board {
pub fn remove_group(&mut self, group: &Group) { pub fn remove_group(&mut self, group: &Group) {
self.groups.retain(|g| g != 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> { pub fn adjacent_groups(&self, group: &Group) -> Vec<Group> {
let adjacent_spaces = self.group_halo(group).into_iter(); let adjacent_spaces = self.group_halo(group).into_iter();
println!("adjacent spaces: {:?}", adjacent_spaces);
let mut grps: Vec<Group> = Vec::new(); let mut grps: Vec<Group> = Vec::new();
adjacent_spaces.for_each(|coord| match self.group(&coord) { adjacent_spaces.for_each(|coord| match self.group(&coord) {
None => return, None => return,
Some(adj) => { Some(adj) => {
println!("group found: {:?}", adj.color);
if group.color == adj.color { if group.color == adj.color {
return; return;
} }
@ -164,32 +167,6 @@ impl Board {
.into_iter() .into_iter()
.filter(|c| self.stone(&c) == None) .filter(|c| self.stone(&c) == None)
.count() .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> { pub fn adjacencies(&self, coordinate: &Coordinate) -> Vec<Coordinate> {
@ -217,41 +194,6 @@ impl Board {
v.into_iter().filter(|c| self.within_board(c)).collect() 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 { pub fn within_board(&self, coordinate: &Coordinate) -> bool {
coordinate.column < self.size.width && coordinate.row < self.size.height coordinate.column < self.size.width && coordinate.row < self.size.height
} }
@ -263,17 +205,11 @@ pub struct Group {
coordinates: HashSet<Coordinate>, coordinates: HashSet<Coordinate>,
} }
/*
impl Group { impl Group {
pub fn adjacencies(&self, max_column: usize, max_row: usize) -> HashSet<Coordinate> { fn contains(&self, coordinate: &Coordinate) -> bool {
self.coordinates self.coordinates.contains(coordinate)
.iter()
.map(|stone| stone.adjacencies(max_column, max_row))
.flatten()
.collect()
} }
} }
*/
#[cfg(test)] #[cfg(test)]
mod test { mod test {
@ -336,9 +272,14 @@ mod test {
(Coordinate { column: 6, row: 16 }, Color::White), (Coordinate { column: 6, row: 16 }, Color::White),
(Coordinate { column: 7, row: 17 }, Color::White), (Coordinate { column: 7, row: 17 }, Color::White),
(Coordinate { column: 7, row: 18 }, 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(), .into_iter(),
); )
.unwrap();
test(board); test(board);
} }
@ -377,7 +318,8 @@ mod test {
), ),
] ]
.into_iter(), .into_iter(),
); )
.unwrap();
assert!(board.group(&Coordinate { column: 18, row: 3 }).is_none()); assert!(board.group(&Coordinate { column: 18, row: 3 }).is_none());
assert_eq!( assert_eq!(
board board
@ -564,8 +506,7 @@ mod test {
#[test] #[test]
fn it_finds_adjacent_groups() { fn it_finds_adjacent_groups() {
with_example_board(|mut board| { with_example_board(|board| {
println!("{}", board);
let group = board let group = board
.group(&Coordinate { column: 0, row: 0 }) .group(&Coordinate { column: 0, row: 0 })
.cloned() .cloned()
@ -582,17 +523,23 @@ mod test {
#[test] #[test]
fn surrounding_a_group_removes_it() { fn surrounding_a_group_removes_it() {
with_example_board(|mut board| { with_example_board(|board| {
board.place_stone(Coordinate { column: 10, row: 9 }, Color::Black); let board = board
.place_stone(Coordinate { column: 10, row: 9 }, Color::Black)
.unwrap();
assert!(board.stone(&Coordinate { column: 9, row: 9 }).is_none()); 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: 0, row: 18 }).is_none());
assert!(board.stone(&Coordinate { column: 1, 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.stone(&Coordinate { column: 0, row: 17 }).is_none());
assert!(board.group(&Coordinate { column: 0, row: 18 }).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: 4, row: 17 }).is_none());
assert!(board.stone(&Coordinate { column: 5, 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: 6, row: 17 }).is_none());
@ -601,11 +548,86 @@ mod test {
}); });
} }
fn suicide_is_forbidden() { #[test]
assert!(false); 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));
}
});
} }
fn captures_preceed_self_capture() { #[test]
assert!(false); 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);
} }
} }

View File

@ -2,7 +2,7 @@ mod api;
pub use api::{CoreApp, Request, Response}; pub use api::{CoreApp, Request, Response};
mod types; mod types;
pub use types::{Color, Size}; pub use types::{BoardError, Color, Size};
pub mod ui; pub mod ui;
mod board; mod board;

View File

@ -3,6 +3,17 @@ use crate::{
board::{Board, Coordinate}, board::{Board, Coordinate},
}; };
use std::time::Duration; 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)] #[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)]
pub enum Color { pub enum Color {
@ -80,6 +91,8 @@ pub struct Player {
pub struct GameState { pub struct GameState {
pub board: Board, pub board: Board,
pub past_positions: Vec<Board>,
pub conversation: Vec<String>, pub conversation: Vec<String>,
pub current_player: Color, pub current_player: Color,
@ -94,6 +107,7 @@ impl GameState {
fn new() -> GameState { fn new() -> GameState {
GameState { GameState {
board: Board::new(), board: Board::new(),
past_positions: vec![],
conversation: vec![], conversation: vec![],
current_player: Color::Black, current_player: Color::Black,
white_player: Player { white_player: Player {
@ -109,11 +123,79 @@ impl GameState {
} }
} }
fn place_stone(&mut self, coordinate: Coordinate) { fn place_stone(&mut self, coordinate: Coordinate) -> Result<(), BoardError> {
self.board.place_stone(coordinate, self.current_player); 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 { 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,
} };
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)
);
} }
} }

View File

@ -667,6 +667,7 @@ name = "kifu-core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"grid", "grid",
"thiserror",
"tokio", "tokio",
] ]

5
kifu/kifu-gtk/Makefile Normal file
View File

@ -0,0 +1,5 @@
release:
cargo build --release
dev:
cargo watch -x 'run --bin kifu-gtk'