Implement the basic rules of Go #40

Merged
savanni merged 13 commits from feature/go-rules into main 2023-05-04 02:34:40 +00:00
14 changed files with 857 additions and 54 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

@ -38,6 +38,7 @@
pkgs.pkg-config pkgs.pkg-config
pkgs.sqlite pkgs.sqlite
pkgs.rustup pkgs.rustup
pkgs.cargo-nextest
pkgs.crate2nix pkgs.crate2nix
]; ];
LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib"; LIBCLANG_PATH="${pkgs.llvmPackages.libclang.lib}/lib";

View File

@ -26,6 +26,15 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 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]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.2.6" version = "0.2.6"
@ -39,6 +48,8 @@ dependencies = [
name = "kifu-core" name = "kifu-core"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"grid",
"thiserror",
"tokio", "tokio",
] ]
@ -85,6 +96,12 @@ dependencies = [
"windows-sys", "windows-sys",
] ]
[[package]]
name = "no-std-compat"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b93853da6d84c2e3c7d730d6473e8817692dd89be387eb01b94d7f108ecb5b8c"
[[package]] [[package]]
name = "num_cpus" name = "num_cpus"
version = "1.15.0" version = "1.15.0"
@ -193,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"
@ -221,7 +269,7 @@ checksum = "d266c00fde287f55d3f1c3e96c500c362a2b8c695076ec180f27918820bc6df8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn", "syn 1.0.109",
] ]
[[package]] [[package]]

View File

@ -6,4 +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" }
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

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

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

@ -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);
}
}

View File

@ -2,5 +2,8 @@ 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;
pub use board::*;

View File

@ -1,16 +1,30 @@
use crate::api::PlayStoneRequest; use crate::{
api::PlayStoneRequest,
board::{Board, Coordinate},
};
use std::time::Duration; 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 { pub enum Color {
Black, Black,
White, White,
} }
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Size { pub struct Size {
pub width: u8, pub width: usize,
pub height: u8, pub height: usize,
} }
impl Default for Size { impl Default for Size {
@ -36,7 +50,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 => {}
} }
@ -74,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,
@ -88,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 {
@ -103,46 +123,78 @@ impl GameState {
} }
} }
fn place_stone(&mut self, column: u8, row: u8) { fn place_stone(&mut self, coordinate: Coordinate) -> Result<(), BoardError> {
self.board.place_stone(column, row, self.current_player); 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 { 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(())
} }
} }
pub struct Board { #[cfg(test)]
pub size: Size, mod test {
pub spaces: Vec<Option<Color>>, use super::*;
}
impl Board { #[test]
fn new() -> Self { fn current_player_changes_after_move() {
let mut spaces = Vec::new(); let mut state = GameState::new();
for _ in 0..19 * 19 { assert_eq!(state.current_player, Color::Black);
spaces.push(None); state.place_stone(Coordinate { column: 9, row: 9 }).unwrap();
} assert_eq!(state.current_player, Color::White);
Self {
size: Size {
width: 19,
height: 19,
},
spaces,
}
} }
pub fn place_stone(&mut self, column: u8, row: u8, stone: Color) { #[test]
let addr = self.addr(column, row); fn current_player_remains_the_same_after_self_capture() {
self.spaces[addr] = Some(stone); 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> { #[test]
let addr = self.addr(column, row); fn ko_rules_are_enforced() {
self.spaces[addr] 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 { state.place_stone(Coordinate { column: 8, row: 9 }).unwrap();
((row as usize) * (self.size.width as usize) + (column as usize)) as usize assert_eq!(
state.place_stone(Coordinate { column: 9, row: 9 }),
Err(BoardError::Ko)
);
} }
} }

View File

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

View File

@ -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, Board, Coordinate,
}; };
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
@ -14,12 +14,14 @@ pub struct Jitter {
pub struct StoneElement { pub struct StoneElement {
pub color: Color, pub color: Color,
pub jitter: Jitter, pub jitter: Jitter,
pub liberties: Option<u8>,
} }
impl StoneElement { impl StoneElement {
pub fn new(color: Color) -> Self { pub fn new(color: Color, liberties: Option<u8>) -> Self {
Self { Self {
color, color,
liberties,
jitter: Jitter { x: 0, y: 0 }, jitter: Jitter { x: 0, y: 0 },
} }
} }
@ -71,9 +73,10 @@ 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 },
liberties: None,
color, color,
}), }),
None => IntersectionElement::Empty(Request::PlayStoneRequest( None => IntersectionElement::Empty(Request::PlayStoneRequest(

View File

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

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'

View File

@ -141,7 +141,7 @@ impl ObjectImpl for BoardPrivate {
(0..19).for_each(|row| { (0..19).for_each(|row| {
match board.stone(row, col) { match board.stone(row, col) {
IntersectionElement::Filled(stone) => { 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(); 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 { match color {
Color::White => context.set_source_rgb(0.9, 0.9, 0.9), Color::White => context.set_source_rgb(0.9, 0.9, 0.9),
Color::Black => context.set_source_rgb(0.0, 0.0, 0.0), Color::Black => context.set_source_rgb(0.0, 0.0, 0.0),
}; };
self.draw_stone(context, row, col); 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) { 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) { fn draw_stone(&self, context: &cairo::Context, row: u8, col: u8) {
let radius = self.hspace_between / 2. - 2.; let radius = self.hspace_between / 2. - 2.;
context.arc( let (x_loc, y_loc) = self.stone_location(row, col);
self.x_offset + (col as f64) * self.hspace_between, context.arc(x_loc, y_loc, radius, 0.0, 2.0 * std::f64::consts::PI);
self.y_offset + (row as f64) * self.vspace_between,
radius,
0.0,
2.0 * std::f64::consts::PI,
);
let _ = context.fill(); 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,
)
}
} }