Compare commits
5 Commits
95dc194d5d
...
f5429340a1
Author | SHA1 | Date |
---|---|---|
Savanni D'Gerinel | f5429340a1 | |
Savanni D'Gerinel | d929afed2d | |
Savanni D'Gerinel | c30d4e6714 | |
Savanni D'Gerinel | 91e27ced6b | |
Savanni D'Gerinel | fae6d0f94a |
|
@ -17,19 +17,11 @@ You should have received a copy of the GNU General Public License along with On
|
||||||
use crate::{
|
use crate::{
|
||||||
database::Database,
|
database::Database,
|
||||||
library, settings,
|
library, settings,
|
||||||
types::{AppState, Config, ConfigOption, GameState, LibraryPath, Player, Rank},
|
types::{Config, LibraryPath},
|
||||||
};
|
|
||||||
use async_std::{
|
|
||||||
channel::{Receiver, Sender},
|
|
||||||
stream,
|
|
||||||
task::spawn,
|
|
||||||
};
|
};
|
||||||
|
use async_std::channel::{Receiver, Sender};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::sync::{Arc, RwLock, RwLockReadGuard};
|
||||||
future::Future,
|
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, RwLock, RwLockReadGuard},
|
|
||||||
};
|
|
||||||
|
|
||||||
pub trait Observable<T> {
|
pub trait Observable<T> {
|
||||||
fn subscribe(&self) -> Receiver<T>;
|
fn subscribe(&self) -> Receiver<T>;
|
||||||
|
@ -168,14 +160,14 @@ impl Core {
|
||||||
*self.library.write().unwrap() = Some(db);
|
*self.library.write().unwrap() = Some(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn library<'a>(&'a self) -> RwLockReadGuard<'_, Option<Database>> {
|
pub fn library(&self) -> RwLockReadGuard<'_, Option<Database>> {
|
||||||
self.library.read().unwrap()
|
self.library.read().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
|
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
|
||||||
match request {
|
match request {
|
||||||
CoreRequest::Library(request) => library::handle(&self, request).await.into(),
|
CoreRequest::Library(request) => library::handle(self, request).await.into(),
|
||||||
CoreRequest::Settings(request) => settings::handle(&self, request).await.into(),
|
CoreRequest::Settings(request) => settings::handle(self, request).await.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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();
|
||||||
|
|
||||||
|
@ -42,13 +42,10 @@ impl Database {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
match parse_sgf(&buffer) {
|
match parse_sgf(&buffer) {
|
||||||
Ok(sgfs) => {
|
Ok(sgfs) => {
|
||||||
for sgf in sgfs {
|
let mut sgfs = sgfs.into_iter().flatten().collect::<Vec<sgf::GameRecord>>();
|
||||||
if let Ok(sgf) = sgf {
|
games.append(&mut sgfs);
|
||||||
games.push(sgf);
|
|
||||||
}
|
}
|
||||||
}
|
Err(err) => println!("Error parsing {:?}: {:?}", entry.path(), err),
|
||||||
}
|
|
||||||
Err(err) => println!("Error parsing {:?}", entry.path()),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -59,7 +56,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 +81,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));
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
|
@ -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)>,
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -19,8 +19,8 @@ extern crate config_derive;
|
||||||
mod api;
|
mod api;
|
||||||
pub use api::{Core, CoreNotification, CoreRequest, CoreResponse, Observable};
|
pub use api::{Core, CoreNotification, CoreRequest, CoreResponse, Observable};
|
||||||
|
|
||||||
mod board;
|
mod goban;
|
||||||
pub use board::*;
|
pub use goban::*;
|
||||||
|
|
||||||
mod database;
|
mod database;
|
||||||
|
|
||||||
|
|
|
@ -14,9 +14,9 @@ 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/>.
|
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/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::{Core, Config};
|
use crate::{Core};
|
||||||
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().cloned().collect::<Vec<GameRecord>>();
|
||||||
LibraryResponse::Games(info)
|
LibraryResponse::Games(info)
|
||||||
}
|
}
|
||||||
None => LibraryResponse::Games(vec![]),
|
None => LibraryResponse::Games(vec![]),
|
||||||
|
|
|
@ -14,7 +14,7 @@ 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/>.
|
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/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use crate::{types::LibraryPath, Core, Config};
|
use crate::{Core, Config};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
use crate::{
|
use crate::goban::{Coordinate, Goban};
|
||||||
board::{Coordinate, Goban},
|
|
||||||
database::Database,
|
|
||||||
};
|
|
||||||
use config::define_config;
|
use config::define_config;
|
||||||
use config_derive::ConfigOption;
|
use config_derive::ConfigOption;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
@ -70,6 +67,32 @@ impl Default for Size {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// AppState stores all of the important state to a full running version of the application.
|
||||||
|
/// However, this version of AppState is in pretty sorry shape.
|
||||||
|
///
|
||||||
|
/// What are the states of the app?
|
||||||
|
///
|
||||||
|
/// - in review
|
||||||
|
/// - in a game
|
||||||
|
/// - connections to the internet
|
||||||
|
/// - connections to local applications, such as Leela Zero
|
||||||
|
/// - the current configuration
|
||||||
|
/// - the games database
|
||||||
|
/// - If in a game, the current state of the game. Delegated to GameState.
|
||||||
|
/// - If in review, the current state of the review.
|
||||||
|
///
|
||||||
|
/// Some of these states are concurrent. It's quite possible to have online connections running and
|
||||||
|
/// to be reviewing a game while, for instance, waiting for an opponent on OGS to make a move.
|
||||||
|
///
|
||||||
|
/// I get to ignore a lot of these things for now. Not playing online. Not playing at all,
|
||||||
|
/// actually. We'll come back to that.
|
||||||
|
///
|
||||||
|
/// Plus, it gets more fuzzy, because some of the application state is really UI state. For
|
||||||
|
/// instance, the state of a game review is purely UI.
|
||||||
|
///
|
||||||
|
/// So... AppState probably isn't great for now, but maybe it will become so later. I think I'm
|
||||||
|
/// going to ignore it until I need it.
|
||||||
|
/*
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub game: Option<GameState>,
|
pub game: Option<GameState>,
|
||||||
|
@ -95,9 +118,11 @@ impl AppState {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum Rank {
|
pub enum Rank {
|
||||||
|
|
||||||
Kyu(u8),
|
Kyu(u8),
|
||||||
Dan(u8),
|
Dan(u8),
|
||||||
Pro(u8),
|
Pro(u8),
|
||||||
|
@ -164,6 +189,9 @@ impl Default for GameState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameState {
|
impl GameState {
|
||||||
|
// Legacy code. I recall that this is no longer used (but will be used again) because I
|
||||||
|
// commented out so much code when I was overhauling the architecture of this app.
|
||||||
|
#[allow(dead_code)]
|
||||||
fn place_stone(&mut self, coordinate: Coordinate) -> Result<(), BoardError> {
|
fn place_stone(&mut self, coordinate: Coordinate) -> Result<(), BoardError> {
|
||||||
let board = self.board.clone();
|
let board = self.board.clone();
|
||||||
let new_board = board.place_stone(coordinate, self.current_player)?;
|
let new_board = board.place_stone(coordinate, self.current_player)?;
|
||||||
|
|
|
@ -21,7 +21,7 @@ use otg_core::{
|
||||||
settings::{SettingsRequest, SettingsResponse},
|
settings::{SettingsRequest, SettingsResponse},
|
||||||
Config, CoreRequest, CoreResponse,
|
Config, CoreRequest, CoreResponse,
|
||||||
};
|
};
|
||||||
use sgf::Game;
|
use sgf::GameRecord;
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use crate::views::{GameReview, HomeView, SettingsView};
|
use crate::views::{GameReview, HomeView, SettingsView};
|
||||||
|
@ -84,7 +84,7 @@ impl AppWindow {
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_game_review(&self, _game: Game) {
|
pub fn open_game_review(&self, _game: GameRecord) {
|
||||||
let header = adw::HeaderBar::new();
|
let header = adw::HeaderBar::new();
|
||||||
let game_review = GameReview::new(self.core.clone());
|
let game_review = GameReview::new(self.core.clone());
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,144 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// I have an old Board class which I'm going to update. I'll just copy over the rendering code, but
|
||||||
|
// at the same time I am going to work pretty heavily on the API.
|
||||||
|
//
|
||||||
|
// For a game review, the board needs to interact very well with a game record. So I have to keep
|
||||||
|
// in mind all of that as I work on the API.
|
||||||
|
//
|
||||||
|
// Also, this is going to be a cross-platform application. Today it is Gnome + Rust, but as I
|
||||||
|
// progress I will also need a Progressive Web App so that I can run this on my tablet. Especially
|
||||||
|
// useful if I'm out socializing and happen to be talking to somebody who would enjoy a relaxing
|
||||||
|
// game. Anyway, that is going to impact some of my API decisions.
|
||||||
|
//
|
||||||
|
// First, though, I need to rename my game record.
|
||||||
|
//
|
||||||
|
// Now, let's get the existing code compiling again.
|
||||||
|
//
|
||||||
|
// Okay, that wasn't so bad. I'm a little confused that I don't have a code action for renaming a
|
||||||
|
// symbol, but I'll fix that some other time. Anyway, now let's focus on the goban.
|
||||||
|
|
||||||
|
// Now, we know what kind of object we have for the current board representation. Let's make use of
|
||||||
|
// that.
|
||||||
|
|
||||||
|
use crate::perftrace;
|
||||||
|
use gio::resources_lookup_data;
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{
|
||||||
|
gdk_pixbuf::{InterpType, Pixbuf},
|
||||||
|
prelude::*,
|
||||||
|
subclass::prelude::*,
|
||||||
|
};
|
||||||
|
use image::io::Reader as ImageReader;
|
||||||
|
use std::{cell::RefCell, io::Cursor, rc::Rc};
|
||||||
|
|
||||||
|
const WIDTH: i32 = 800;
|
||||||
|
const HEIGHT: i32 = 800;
|
||||||
|
const MARGIN: i32 = 20;
|
||||||
|
|
||||||
|
// Internal representation of the Goban drawing area.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct GobanPrivate {
|
||||||
|
board_state: Rc<RefCell<otg_core::Goban>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GobanPrivate {}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for GobanPrivate {
|
||||||
|
const NAME: &'static str = "Goban";
|
||||||
|
type Type = Goban;
|
||||||
|
type ParentType = gtk::DrawingArea;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for GobanPrivate {}
|
||||||
|
impl WidgetImpl for GobanPrivate {}
|
||||||
|
impl DrawingAreaImpl for GobanPrivate {}
|
||||||
|
|
||||||
|
// This Goban, being in the `components` crate, is merely the rendering of a board. This is not
|
||||||
|
// the primary representation of the board.
|
||||||
|
//
|
||||||
|
// In a game of Go, there are certain rules about what are valid moves and what are not.
|
||||||
|
// Internally, I want to keep track of those, and doing so requires a few things.
|
||||||
|
//
|
||||||
|
// - We can never repeat a game state (though I think maybe that is allowed in a few rulesets, but
|
||||||
|
// I'm coding to the AGA ruleset)
|
||||||
|
// - We can never play a suicidal move
|
||||||
|
//
|
||||||
|
// Finally, updating the board state is non-GUI logic. So, sorry, might be dropping away from GUI
|
||||||
|
// code for a while to work on the backend representation, some of which already exists.
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct Goban(ObjectSubclass<GobanPrivate>) @extends gtk::DrawingArea, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Goban {
|
||||||
|
pub fn new(board_state: otg_core::Goban) -> Self {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
|
||||||
|
*s.imp().board_state.borrow_mut() = board_state;
|
||||||
|
s.set_width_request(WIDTH);
|
||||||
|
s.set_height_request(HEIGHT);
|
||||||
|
|
||||||
|
s.set_draw_func({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_, ctx, width, height| {
|
||||||
|
perftrace("render drawing area", || s.redraw(ctx, width, height));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redraw(&self, ctx: &cairo::Context, width: i32, height: i32) {
|
||||||
|
println!("{} x {}", width, height);
|
||||||
|
/*
|
||||||
|
let wood_texture = resources_lookup_data(
|
||||||
|
"/com/luminescent-dreams/otg-gtk/wood_texture.jpg",
|
||||||
|
gio::ResourceLookupFlags::NONE,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let background = ImageReader::new(Cursor::new(wood_texture))
|
||||||
|
.with_guessed_format()
|
||||||
|
.unwrap()
|
||||||
|
.decode();
|
||||||
|
let background = background.map(|background| {
|
||||||
|
Pixbuf::from_bytes(
|
||||||
|
&glib::Bytes::from(background.as_bytes()),
|
||||||
|
gtk::gdk_pixbuf::Colorspace::Rgb,
|
||||||
|
false,
|
||||||
|
8,
|
||||||
|
background.width() as i32,
|
||||||
|
background.height() as i32,
|
||||||
|
background.to_rgb8().sample_layout().height_stride as i32,
|
||||||
|
)
|
||||||
|
.scale_simple(WIDTH, HEIGHT, InterpType::Nearest)
|
||||||
|
});
|
||||||
|
|
||||||
|
match background {
|
||||||
|
Ok(Some(ref background)) => {
|
||||||
|
ctx.set_source_pixbuf(background, 0., 0.);
|
||||||
|
ctx.paint().expect("paint should never fail");
|
||||||
|
}
|
||||||
|
Ok(None) | Err(_) => ctx.set_source_rgb(0.7, 0.7, 0.7),
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
ctx.set_source_rgb(0.7, 0.7, 0.7);
|
||||||
|
let _ = ctx.paint();
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,12 +2,12 @@ use adw::{prelude::*, subclass::prelude::*};
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::glib;
|
use gtk::glib;
|
||||||
// use otg_core::ui::GamePreviewElement;
|
// use otg_core::ui::GamePreviewElement;
|
||||||
use sgf::Game;
|
use sgf::GameRecord;
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GameObjectPrivate {
|
pub struct GameObjectPrivate {
|
||||||
game: Rc<RefCell<Option<Game>>>,
|
game: Rc<RefCell<Option<GameRecord>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
|
@ -23,13 +23,13 @@ glib::wrapper! {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameObject {
|
impl GameObject {
|
||||||
pub fn new(game: Game) -> Self {
|
pub fn new(game: GameRecord) -> Self {
|
||||||
let s: Self = Object::builder().build();
|
let s: Self = Object::builder().build();
|
||||||
*s.imp().game.borrow_mut() = Some(game);
|
*s.imp().game.borrow_mut() = Some(game);
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn game(&self) -> Option<Game> {
|
pub fn game(&self) -> Option<GameRecord> {
|
||||||
self.imp().game.borrow().clone()
|
self.imp().game.borrow().clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -54,7 +54,7 @@ impl Default for LibraryPrivate {
|
||||||
|
|
||||||
fn make_factory<F>(bind: F) -> gtk::SignalListItemFactory
|
fn make_factory<F>(bind: F) -> gtk::SignalListItemFactory
|
||||||
where
|
where
|
||||||
F: Fn(Game) -> String + 'static,
|
F: Fn(GameRecord) -> String + 'static,
|
||||||
{
|
{
|
||||||
let factory = gtk::SignalListItemFactory::new();
|
let factory = gtk::SignalListItemFactory::new();
|
||||||
factory.connect_setup(|_, list_item| {
|
factory.connect_setup(|_, list_item| {
|
||||||
|
@ -155,7 +155,7 @@ impl Default for Library {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Library {
|
impl Library {
|
||||||
pub fn new(on_select: impl Fn(Game) + 'static) -> Library {
|
pub fn new(on_select: impl Fn(GameRecord) + 'static) -> Library {
|
||||||
let s = Library::default();
|
let s = Library::default();
|
||||||
|
|
||||||
s.imp().list_view.connect_activate({
|
s.imp().list_view.connect_activate({
|
||||||
|
@ -177,7 +177,7 @@ impl Library {
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_games(&self, games: Vec<Game>) {
|
pub fn set_games(&self, games: Vec<GameRecord>) {
|
||||||
let games = games
|
let games = games
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(GameObject::new)
|
.map(GameObject::new)
|
||||||
|
|
|
@ -1,5 +1,21 @@
|
||||||
mod board;
|
/*
|
||||||
pub use board::Board;
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// mod board;
|
||||||
|
// pub use board::Board;
|
||||||
|
|
||||||
// mod chat;
|
// mod chat;
|
||||||
// pub use chat::Chat;
|
// pub use chat::Chat;
|
||||||
|
@ -10,6 +26,9 @@ pub use board::Board;
|
||||||
// mod game_preview;
|
// mod game_preview;
|
||||||
// pub use game_preview::GamePreview;
|
// pub use game_preview::GamePreview;
|
||||||
|
|
||||||
|
mod goban;
|
||||||
|
pub use goban::Goban;
|
||||||
|
|
||||||
mod library;
|
mod library;
|
||||||
pub use library::Library;
|
pub use library::Library;
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@ You should have received a copy of the GNU General Public License along with On
|
||||||
|
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
use crate::{components::Board, CoreApi};
|
use crate::{components::Goban, CoreApi};
|
||||||
|
|
||||||
pub struct GameReviewPrivate {}
|
pub struct GameReviewPrivate {}
|
||||||
|
|
||||||
|
@ -38,26 +38,30 @@ impl Default for GameReviewPrivate {
|
||||||
impl ObjectSubclass for GameReviewPrivate {
|
impl ObjectSubclass for GameReviewPrivate {
|
||||||
const NAME: &'static str = "GameReview";
|
const NAME: &'static str = "GameReview";
|
||||||
type Type = GameReview;
|
type Type = GameReview;
|
||||||
type ParentType = gtk::Grid;
|
type ParentType = gtk::Box;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ObjectImpl for GameReviewPrivate {}
|
impl ObjectImpl for GameReviewPrivate {}
|
||||||
impl WidgetImpl for GameReviewPrivate {}
|
impl WidgetImpl for GameReviewPrivate {}
|
||||||
impl GridImpl for GameReviewPrivate {}
|
impl BoxImpl for GameReviewPrivate {}
|
||||||
|
|
||||||
glib::wrapper! {
|
glib::wrapper! {
|
||||||
pub struct GameReview(ObjectSubclass<GameReviewPrivate>) @extends gtk::Grid, gtk::Widget, @implements gtk::Accessible;
|
pub struct GameReview(ObjectSubclass<GameReviewPrivate>) @extends gtk::Box, gtk::Widget, @implements gtk::Accessible;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameReview {
|
impl GameReview {
|
||||||
pub fn new(api: CoreApi) -> Self {
|
pub fn new(api: CoreApi) -> Self {
|
||||||
let s: Self = Object::builder().build();
|
let s: Self = Object::builder().build();
|
||||||
|
|
||||||
let board = Board::new(api);
|
let board = Goban::new(otg_core::Goban::default());
|
||||||
|
/*
|
||||||
s.attach(&board, 0, 0, 2, 2);
|
s.attach(&board, 0, 0, 2, 2);
|
||||||
s.attach(>k::Label::new(Some("white player")), 0, 2, 1, 1);
|
s.attach(>k::Label::new(Some("white player")), 0, 2, 1, 1);
|
||||||
s.attach(>k::Label::new(Some("black player")), 0, 2, 1, 2);
|
s.attach(>k::Label::new(Some("black player")), 0, 2, 1, 2);
|
||||||
s.attach(>k::Label::new(Some("chat")), 1, 2, 2, 2);
|
s.attach(>k::Label::new(Some("chat")), 1, 2, 2, 2);
|
||||||
|
*/
|
||||||
|
|
||||||
|
s.append(&board);
|
||||||
|
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,7 @@ use otg_core::{
|
||||||
library::{LibraryRequest, LibraryResponse},
|
library::{LibraryRequest, LibraryResponse},
|
||||||
CoreRequest, CoreResponse,
|
CoreRequest, CoreResponse,
|
||||||
};
|
};
|
||||||
use sgf::Game;
|
use sgf::GameRecord;
|
||||||
use std::{cell::RefCell, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -131,7 +131,7 @@ glib::wrapper! {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl HomeView {
|
impl HomeView {
|
||||||
pub fn new(api: CoreApi, on_select_game: impl Fn(Game) + 'static) -> Self {
|
pub fn new(api: CoreApi, on_select_game: impl Fn(GameRecord) + 'static) -> Self {
|
||||||
let s: Self = Object::builder().build();
|
let s: Self = Object::builder().build();
|
||||||
s.set_spacing(4);
|
s.set_spacing(4);
|
||||||
s.set_homogeneous(false);
|
s.set_homogeneous(false);
|
||||||
|
|
|
@ -49,11 +49,11 @@ pub struct Player {
|
||||||
/// level, the interpreter will reject any games that have setup properties and move properties
|
/// level, the interpreter will reject any games that have setup properties and move properties
|
||||||
/// mixed in a single node. If there are other semantic problems, the interpreter will reject
|
/// mixed in a single node. If there are other semantic problems, the interpreter will reject
|
||||||
/// those, as well. Where the function of the parser is to understand and correct fundamental
|
/// those, as well. Where the function of the parser is to understand and correct fundamental
|
||||||
/// syntax issues, the result of the Game is to have a fully-understood game. However, this doesn't
|
/// syntax issues, the result of the GameRecord is to have a fully-understood game. However, this
|
||||||
/// (yet?) go quite to the level of apply the game type (i.e., this is Go, Chess, Yinsh, or
|
/// doesn't (yet?) go quite to the level of apply the game type (i.e., this is Go, Chess, Yinsh, or
|
||||||
/// whatever).
|
/// whatever).
|
||||||
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
|
||||||
pub struct Game {
|
pub struct GameRecord {
|
||||||
pub game_type: GameType,
|
pub game_type: GameType,
|
||||||
|
|
||||||
// TODO: board size is not necessary in all games. Hive has no defined board size.
|
// TODO: board size is not necessary in all games. Hive has no defined board size.
|
||||||
|
@ -81,7 +81,7 @@ pub struct Game {
|
||||||
pub children: Vec<GameNode>,
|
pub children: Vec<GameNode>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Game {
|
impl GameRecord {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
game_type: GameType,
|
game_type: GameType,
|
||||||
board_size: Size,
|
board_size: Size,
|
||||||
|
@ -116,7 +116,7 @@ impl Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Node for Game {
|
impl Node for GameRecord {
|
||||||
fn children<'a>(&'a self) -> Vec<&'a GameNode> {
|
fn children<'a>(&'a self) -> Vec<&'a GameNode> {
|
||||||
self.children.iter().collect::<Vec<&'a GameNode>>()
|
self.children.iter().collect::<Vec<&'a GameNode>>()
|
||||||
}
|
}
|
||||||
|
@ -127,7 +127,7 @@ impl Node for Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TryFrom<&parser::Tree> for Game {
|
impl TryFrom<&parser::Tree> for GameRecord {
|
||||||
type Error = GameError;
|
type Error = GameError;
|
||||||
|
|
||||||
fn try_from(tree: &parser::Tree) -> Result<Self, Self::Error> {
|
fn try_from(tree: &parser::Tree) -> Result<Self, Self::Error> {
|
||||||
|
@ -503,7 +503,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_can_create_an_empty_game_tree() {
|
fn it_can_create_an_empty_game_tree() {
|
||||||
let tree = Game::new(
|
let tree = GameRecord::new(
|
||||||
GameType::Go,
|
GameType::Go,
|
||||||
Size {
|
Size {
|
||||||
width: 19,
|
width: 19,
|
||||||
|
@ -517,7 +517,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn it_can_add_moves_to_a_game() {
|
fn it_can_add_moves_to_a_game() {
|
||||||
let mut game = Game::new(
|
let mut game = GameRecord::new(
|
||||||
GameType::Go,
|
GameType::Go,
|
||||||
Size {
|
Size {
|
||||||
width: 19,
|
width: 19,
|
||||||
|
@ -693,16 +693,16 @@ mod file_test {
|
||||||
use parser::parse_collection;
|
use parser::parse_collection;
|
||||||
use std::{fs::File, io::Read};
|
use std::{fs::File, io::Read};
|
||||||
|
|
||||||
fn with_text(text: &str, f: impl FnOnce(Vec<Game>)) {
|
fn with_text(text: &str, f: impl FnOnce(Vec<GameRecord>)) {
|
||||||
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(text).unwrap();
|
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(text).unwrap();
|
||||||
let games = games
|
let games = games
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|game| Game::try_from(&game).expect("game to parse"))
|
.map(|game| GameRecord::try_from(&game).expect("game to parse"))
|
||||||
.collect::<Vec<Game>>();
|
.collect::<Vec<GameRecord>>();
|
||||||
f(games);
|
f(games);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn with_file(path: &std::path::Path, f: impl FnOnce(Vec<Game>)) {
|
fn with_file(path: &std::path::Path, f: impl FnOnce(Vec<GameRecord>)) {
|
||||||
let mut file = File::open(path).unwrap();
|
let mut file = File::open(path).unwrap();
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
let _ = file.read_to_string(&mut text);
|
let _ = file.read_to_string(&mut text);
|
||||||
|
|
|
@ -5,7 +5,7 @@ mod types;
|
||||||
|
|
||||||
use std::{fs::File, io::Read};
|
use std::{fs::File, io::Read};
|
||||||
pub use date::Date;
|
pub use date::Date;
|
||||||
pub use game::Game;
|
pub use game::GameRecord;
|
||||||
pub use parser::parse_collection;
|
pub use parser::parse_collection;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
@ -58,16 +58,26 @@ impl From<nom::error::Error<&str>> for ParseError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_sgf(input: &str) -> Result<Vec<Result<Game, game::GameError>>, Error> {
|
/// Given raw text of an SGF file, parse all of the games within that file.
|
||||||
|
///
|
||||||
|
/// The outermost Result is for any errors that happen in opening and reading the file, or if hte
|
||||||
|
/// outermost part of the file format is invalid.
|
||||||
|
///
|
||||||
|
/// The inner Result is for errors in each individual game in the file. All of the other games can
|
||||||
|
/// still be kept as valid.
|
||||||
|
pub fn parse_sgf(input: &str) -> Result<Vec<Result<GameRecord, game::GameError>>, Error> {
|
||||||
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(&input)?;
|
let (_, games) = parse_collection::<nom::error::VerboseError<&str>>(&input)?;
|
||||||
let games = games.into_iter()
|
let games = games.into_iter()
|
||||||
.map(|game| Game::try_from(&game))
|
.map(|game| GameRecord::try_from(&game))
|
||||||
.collect::<Vec<Result<Game, game::GameError>>>();
|
.collect::<Vec<Result<GameRecord, game::GameError>>>();
|
||||||
|
|
||||||
Ok(games)
|
Ok(games)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_sgf_file(path: &std::path::Path) -> Result<Vec<Result<Game, game::GameError>>, Error> {
|
/// Given a path, parse all of the games stored in that file.
|
||||||
|
///
|
||||||
|
/// See also `parse_sgf`
|
||||||
|
pub fn parse_sgf_file(path: &std::path::Path) -> Result<Vec<Result<GameRecord, game::GameError>>, Error> {
|
||||||
let mut file = File::open(path).unwrap();
|
let mut file = File::open(path).unwrap();
|
||||||
let mut text = String::new();
|
let mut text = String::new();
|
||||||
let _ = file.read_to_string(&mut text);
|
let _ = file.read_to_string(&mut text);
|
||||||
|
|
Loading…
Reference in New Issue