Document the Goban representation in Core
This commit is contained in:
parent
fae6d0f94a
commit
91e27ced6b
|
@ -1,9 +1,35 @@
|
||||||
|
/*
|
||||||
|
Copyright 2024, Savanni D'Gerinel <savanni@luminescent-dreams.com>
|
||||||
|
|
||||||
|
This file is part of On the Grid.
|
||||||
|
|
||||||
|
On the Grid is free software: you can redistribute it and/or modify it under the terms of
|
||||||
|
the GNU General Public License as published by the Free Software Foundation, either version 3 of
|
||||||
|
the License, or (at your option) any later version.
|
||||||
|
|
||||||
|
On the Grid is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
||||||
|
without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License along with On the Grid. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// TBQH, I don't recall what state this object is in, but I do know that I might have some troubles
|
||||||
|
// integrating it with a game record. Some of the time here is going to be me reading (and
|
||||||
|
// documenting) my code from almost a year ago.
|
||||||
|
//
|
||||||
use crate::{BoardError, Color, Size};
|
use crate::{BoardError, Color, Size};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Default)]
|
#[derive(Clone, Debug, Default)]
|
||||||
pub struct Goban {
|
pub struct Goban {
|
||||||
|
/// The size of the board. Usually this is symetrical, but I have actually played a 5x25 game.
|
||||||
|
/// These are fun for novelty, but don't lend much to understanding the game.
|
||||||
pub size: Size,
|
pub size: Size,
|
||||||
|
|
||||||
|
/// I found that it was easiest to track groups of stones than to track individual stones on the
|
||||||
|
/// board. So, I just keep track of all of the groups.
|
||||||
pub groups: Vec<Group>,
|
pub groups: Vec<Group>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,10 +88,16 @@ impl Goban {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Generate a board state from an iterator of coordinates and the color of any stone present on
|
||||||
|
/// the board. As we walk through the iterator, we play each stone as though it were being
|
||||||
|
/// played in a game.
|
||||||
|
///
|
||||||
|
/// This would not work at all if we wanted to set up an impossible board state, given that
|
||||||
|
/// groups of stones get automatically removed once surrounded.
|
||||||
pub fn from_coordinates(
|
pub fn from_coordinates(
|
||||||
mut coordinates: impl Iterator<Item = (Coordinate, Color)>,
|
mut coordinates: impl IntoIterator<Item = (Coordinate, Color)>,
|
||||||
) -> Result<Self, BoardError> {
|
) -> Result<Self, BoardError> {
|
||||||
coordinates.try_fold(Self::new(), |board, (coordinate, color)| {
|
coordinates.into_iter().try_fold(Self::new(), |board, (coordinate, color)| {
|
||||||
board.place_stone(coordinate, color)
|
board.place_stone(coordinate, color)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -78,37 +110,78 @@ pub struct Coordinate {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Goban {
|
impl Goban {
|
||||||
|
/// place_stone is the most fundamental function of this object. This is as though a player put
|
||||||
|
/// a stone on the board and evaluated the consequences.
|
||||||
|
///
|
||||||
|
/// This function does not enforce turn order.
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
///
|
||||||
|
/// ```
|
||||||
|
/// use otg_core::{Color, Size, Coordinate, Goban};
|
||||||
|
/// use cool_asserts::assert_matches;
|
||||||
|
///
|
||||||
|
/// let goban = Goban::new();
|
||||||
|
/// assert_eq!(goban.size, Size{ width: 19, height: 19 });
|
||||||
|
/// let move_result = goban.place_stone(Coordinate{ column: 4, row: 4 }, Color::Black);
|
||||||
|
/// assert_matches!(move_result, Goban);
|
||||||
|
/// ```
|
||||||
pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result<Self, BoardError> {
|
pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result<Self, BoardError> {
|
||||||
|
// Bail out immediately if there is already a stone at this location.
|
||||||
if self.stone(&coordinate).is_some() {
|
if self.stone(&coordinate).is_some() {
|
||||||
return Err(BoardError::InvalidPosition);
|
return Err(BoardError::InvalidPosition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Find all friendly groups adjacent to this stone. First, calculate the adjacent
|
||||||
|
// coordinates. Then see if there is any group which contains that coordinate. If not, this
|
||||||
|
// stone forms a new group of its own.
|
||||||
|
//
|
||||||
|
// A little subtle here is that this stone will be added to *every* adjoining friendly
|
||||||
|
// group. This normally means only that a group gets bigger, but it could also cause two
|
||||||
|
// groups to share a stone, which means they're now a single group.
|
||||||
let mut friendly_group = self
|
let mut friendly_group = self
|
||||||
.adjacencies(&coordinate)
|
.adjacencies(&coordinate)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|c| self.stone(c) == Some(color))
|
.filter(|c| self.stone(c) == Some(color))
|
||||||
.filter_map(|c| self.group(&c).map(|g| g.coordinates.clone()))
|
.filter_map(|c| self.group(&c).map(|g| g.coordinates.clone()))
|
||||||
|
|
||||||
|
// In fact, this last step actually connects the coordinates of those friendly groups
|
||||||
|
// into a single large group.
|
||||||
.fold(HashSet::new(), |acc, set| {
|
.fold(HashSet::new(), |acc, set| {
|
||||||
acc.union(&set).cloned().collect()
|
acc.union(&set).cloned().collect()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// This is a little misnamed. This is a HashSet, not a full Group.
|
||||||
friendly_group.insert(coordinate);
|
friendly_group.insert(coordinate);
|
||||||
|
|
||||||
|
// Remove all groups which contain the stones overlapping with this friendly group.
|
||||||
self.groups
|
self.groups
|
||||||
.retain(|g| g.coordinates.is_disjoint(&friendly_group));
|
.retain(|g| g.coordinates.is_disjoint(&friendly_group));
|
||||||
|
|
||||||
|
// Generate a new friendly group given the coordinates.
|
||||||
let friendly_group = Group {
|
let friendly_group = Group {
|
||||||
color,
|
color,
|
||||||
coordinates: friendly_group,
|
coordinates: friendly_group,
|
||||||
};
|
};
|
||||||
|
// Now add the group back to the board.
|
||||||
self.groups.push(friendly_group.clone());
|
self.groups.push(friendly_group.clone());
|
||||||
|
|
||||||
|
// Now, find all groups adjacent to this one. Those are the only groups that this move is
|
||||||
|
// going to impact. Calculate their liberties.
|
||||||
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 {
|
||||||
|
// Any group that has been reduced to 0 liberties should now be removed from the board.
|
||||||
|
//
|
||||||
|
// TODO: capture rules: we're not counting captured stones yet. Okay with some scoring
|
||||||
|
// methods, but not all.
|
||||||
if self.liberties(&group) == 0 {
|
if self.liberties(&group) == 0 {
|
||||||
self.remove_group(&group);
|
self.remove_group(&group);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now, recalculate the liberties of this friendly group. If this group has been reduced to
|
||||||
|
// zero liberties, after all captures have been accounted for, the move is an illegal
|
||||||
|
// self-capture. Drop all of the work we've done and return an error.
|
||||||
if self.liberties(&friendly_group) == 0 {
|
if self.liberties(&friendly_group) == 0 {
|
||||||
return Err(BoardError::SelfCapture);
|
return Err(BoardError::SelfCapture);
|
||||||
}
|
}
|
||||||
|
@ -116,24 +189,24 @@ impl Goban {
|
||||||
Ok(self)
|
Ok(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn stone(&self, coordinate: &Coordinate) -> Option<Color> {
|
fn stone(&self, coordinate: &Coordinate) -> Option<Color> {
|
||||||
self.groups
|
self.groups
|
||||||
.iter()
|
.iter()
|
||||||
.find(|g| g.contains(coordinate))
|
.find(|g| g.contains(coordinate))
|
||||||
.map(|g| g.color)
|
.map(|g| g.color)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn group(&self, coordinate: &Coordinate) -> Option<&Group> {
|
fn group(&self, coordinate: &Coordinate) -> Option<&Group> {
|
||||||
self.groups
|
self.groups
|
||||||
.iter()
|
.iter()
|
||||||
.find(|g| g.coordinates.contains(coordinate))
|
.find(|g| g.coordinates.contains(coordinate))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_group(&mut self, group: &Group) {
|
fn remove_group(&mut self, group: &Group) {
|
||||||
self.groups.retain(|g| g != group);
|
self.groups.retain(|g| g != group);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn adjacent_groups(&self, group: &Group) -> Vec<Group> {
|
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();
|
||||||
let mut grps: Vec<Group> = Vec::new();
|
let mut grps: Vec<Group> = Vec::new();
|
||||||
|
|
||||||
|
@ -153,7 +226,7 @@ impl Goban {
|
||||||
grps
|
grps
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn group_halo(&self, group: &Group) -> HashSet<Coordinate> {
|
fn group_halo(&self, group: &Group) -> HashSet<Coordinate> {
|
||||||
group
|
group
|
||||||
.coordinates
|
.coordinates
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -161,14 +234,14 @@ impl Goban {
|
||||||
.collect::<HashSet<Coordinate>>()
|
.collect::<HashSet<Coordinate>>()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn liberties(&self, group: &Group) -> usize {
|
fn liberties(&self, group: &Group) -> usize {
|
||||||
self.group_halo(group)
|
self.group_halo(group)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|c| self.stone(c).is_none())
|
.filter(|c| self.stone(c).is_none())
|
||||||
.count()
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn adjacencies(&self, coordinate: &Coordinate) -> Vec<Coordinate> {
|
fn adjacencies(&self, coordinate: &Coordinate) -> Vec<Coordinate> {
|
||||||
let mut v = Vec::new();
|
let mut v = Vec::new();
|
||||||
if coordinate.column > 0 {
|
if coordinate.column > 0 {
|
||||||
v.push(Coordinate {
|
v.push(Coordinate {
|
||||||
|
@ -193,7 +266,7 @@ impl Goban {
|
||||||
v.into_iter().filter(|c| self.within_board(c)).collect()
|
v.into_iter().filter(|c| self.within_board(c)).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn within_board(&self, coordinate: &Coordinate) -> bool {
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{io::Read, path::PathBuf};
|
use std::{io::Read, path::PathBuf};
|
||||||
|
|
||||||
use sgf::{parse_sgf, Game};
|
use sgf::{parse_sgf, GameRecord};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
|
@ -21,12 +21,12 @@ impl From<std::io::Error> for Error {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
games: Vec<Game>,
|
games: Vec<GameRecord>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
pub fn open_path(path: PathBuf) -> Result<Database, Error> {
|
pub fn open_path(path: PathBuf) -> Result<Database, Error> {
|
||||||
let mut games: Vec<Game> = Vec::new();
|
let mut games: Vec<GameRecord> = Vec::new();
|
||||||
|
|
||||||
let extension = PathBuf::from("sgf").into_os_string();
|
let extension = PathBuf::from("sgf").into_os_string();
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ impl Database {
|
||||||
Ok(Database { games })
|
Ok(Database { games })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn all_games(&self) -> impl Iterator<Item = &Game> {
|
pub fn all_games(&self) -> impl Iterator<Item = &GameRecord> {
|
||||||
self.games.iter()
|
self.games.iter()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -84,11 +84,11 @@ mod test {
|
||||||
Database::open_path(PathBuf::from("fixtures/five_games/")).expect("database to open");
|
Database::open_path(PathBuf::from("fixtures/five_games/")).expect("database to open");
|
||||||
assert_eq!(db.all_games().count(), 5);
|
assert_eq!(db.all_games().count(), 5);
|
||||||
|
|
||||||
assert_matches!(db.all_games().find(|g| g.info.black_player == Some("Steve".to_owned())),
|
assert_matches!(db.all_games().find(|g| g.black_player.name == Some("Steve".to_owned())),
|
||||||
Some(game) => {
|
Some(game) => {
|
||||||
assert_eq!(game.info.black_player, Some("Steve".to_owned()));
|
assert_eq!(game.black_player.name, Some("Steve".to_owned()));
|
||||||
assert_eq!(game.info.white_player, Some("Savanni".to_owned()));
|
assert_eq!(game.white_player.name, Some("Savanni".to_owned()));
|
||||||
assert_eq!(game.info.date, vec![Date::Date(chrono::NaiveDate::from_ymd_opt(2023, 4, 19).unwrap())]);
|
assert_eq!(game.dates, vec![Date::Date(chrono::NaiveDate::from_ymd_opt(2023, 4, 19).unwrap())]);
|
||||||
// assert_eq!(game.info.komi, Some(6.5));
|
// assert_eq!(game.info.komi, Some(6.5));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -16,7 +16,7 @@ You should have received a copy of the GNU General Public License along with On
|
||||||
|
|
||||||
use crate::{Core, Config};
|
use crate::{Core, Config};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sgf::Game;
|
use sgf::GameRecord;
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum LibraryRequest {
|
pub enum LibraryRequest {
|
||||||
|
@ -25,14 +25,14 @@ pub enum LibraryRequest {
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub enum LibraryResponse {
|
pub enum LibraryResponse {
|
||||||
Games(Vec<Game>)
|
Games(Vec<GameRecord>)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_list_games(model: &Core) -> LibraryResponse {
|
async fn handle_list_games(model: &Core) -> LibraryResponse {
|
||||||
let library = model.library();
|
let library = model.library();
|
||||||
match *library {
|
match *library {
|
||||||
Some(ref library) => {
|
Some(ref library) => {
|
||||||
let info = library.all_games().map(|g| g.clone()).collect::<Vec<Game>>();
|
let info = library.all_games().map(|g| g.clone()).collect::<Vec<GameRecord>>();
|
||||||
LibraryResponse::Games(info)
|
LibraryResponse::Games(info)
|
||||||
}
|
}
|
||||||
None => LibraryResponse::Games(vec![]),
|
None => LibraryResponse::Games(vec![]),
|
||||||
|
|
Loading…
Reference in New Issue