Compare commits

..

13 Commits

Author SHA1 Message Date
Savanni D'Gerinel 5486c3ce37 Start on the foundations of a tree-drawing algorithm
I don't actually know what I'm doing. I've done some reading and from
that I'm doing experiments until I can better understand what I've read.
2024-03-25 18:11:03 -04:00
Savanni D'Gerinel 372f1e1e64 Create the drawing area for the review tree 2024-03-25 08:18:36 -04:00
Savanni D'Gerinel 045daea774 Render the board with the completed game state. 2024-03-24 16:16:50 -04:00
Savanni D'Gerinel d15613c40e Add more test data and ensure the mainline is returned even with branches 2024-03-24 16:04:24 -04:00
Savanni D'Gerinel 6fc2487937 Return the mainline of a game that has no branches in it. 2024-03-24 15:50:59 -04:00
Savanni D'Gerinel d6b424d335 Apply moves to the abstract board
To get here, I had to also build some conversion functions and make a
lot of things within the game record public
2024-03-24 11:03:40 -04:00
Savanni D'Gerinel 965223d227 Render the grid of the goban 2024-03-23 23:24:06 -04:00
Savanni D'Gerinel f5429340a1 Set the size of the drawing area 2024-03-23 21:57:56 -04:00
Savanni D'Gerinel d929afed2d Start on the new Goban component 2024-03-23 14:41:50 -04:00
Savanni D'Gerinel c30d4e6714 Minimal linting 2024-03-23 14:41:35 -04:00
Savanni D'Gerinel 91e27ced6b Document the Goban representation in Core 2024-03-23 14:14:53 -04:00
Savanni D'Gerinel fae6d0f94a Rename Game to GameRecord for disambiguation. 2024-03-23 13:40:06 -04:00
Savanni D'Gerinel 95dc194d5d Create the game review page and work on navigating to it with a navigation stack 2024-03-23 11:21:10 -04:00
15 changed files with 261 additions and 1388 deletions

1475
Cargo.nix

File diff suppressed because it is too large Load Diff

View File

@ -19,7 +19,7 @@ You should have received a copy of the GNU General Public License along with On
// documenting) my code from almost a year ago. // documenting) my code from almost a year ago.
// //
use crate::{BoardError, Color, Size}; use crate::{BoardError, Color, Size};
use sgf::{GameNode, MoveNode}; use sgf::{GameNode, Move, MoveNode};
use std::collections::HashSet; use std::collections::HashSet;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
@ -221,7 +221,7 @@ impl Goban {
) -> Result<Goban, BoardError> { ) -> Result<Goban, BoardError> {
let mut s = self; let mut s = self;
for m in moves.into_iter() { for m in moves.into_iter() {
match m { let s = match m {
GameNode::MoveNode(node) => s = s.apply_move_node(node)?, GameNode::MoveNode(node) => s = s.apply_move_node(node)?,
GameNode::SetupNode(_n) => unimplemented!("setup nodes aren't processed yet"), GameNode::SetupNode(_n) => unimplemented!("setup nodes aren't processed yet"),
}; };

View File

@ -16,22 +16,20 @@ You should have received a copy of the GNU General Public License along with On
use crate::CoreApi; use crate::CoreApi;
use adw::prelude::*; use adw::prelude::*;
use async_std::task::{block_on, spawn};
use otg_core::{ use otg_core::{
settings::{SettingsRequest, SettingsResponse}, settings::{SettingsRequest, SettingsResponse},
CoreRequest, CoreResponse, Config, CoreRequest, CoreResponse,
}; };
use sgf::GameRecord; 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};
/*
#[derive(Clone)] #[derive(Clone)]
enum AppView { enum AppView {
Home, Home,
} }
*/
// An application window should generally contain // An application window should generally contain
// - an overlay widget // - an overlay widget
@ -48,7 +46,7 @@ pub struct AppWindow {
// we can maintain the state of previous views. Since the two of these work together, they are // we can maintain the state of previous views. Since the two of these work together, they are
// a candidate for extraction into a new widget or a new struct. // a candidate for extraction into a new widget or a new struct.
stack: adw::NavigationView, stack: adw::NavigationView,
// view_states: Vec<AppView>, view_states: Vec<AppView>,
// Overlays are for transient content, such as about and settings, which can be accessed from // Overlays are for transient content, such as about and settings, which can be accessed from
// anywhere but shouldn't be part of the main application flow. // anywhere but shouldn't be part of the main application flow.
@ -65,7 +63,7 @@ impl AppWindow {
let window = Self::setup_window(app); let window = Self::setup_window(app);
let overlay = Self::setup_overlay(); let overlay = Self::setup_overlay();
let stack = adw::NavigationView::new(); let stack = adw::NavigationView::new();
// let view_states = vec![]; let view_states = vec![];
window.set_content(Some(&overlay)); window.set_content(Some(&overlay));
overlay.set_child(Some(&stack)); overlay.set_child(Some(&stack));
@ -73,7 +71,7 @@ impl AppWindow {
let s = Self { let s = Self {
window, window,
stack, stack,
// view_states, view_states,
overlay, overlay,
core, core,
settings_view_model: Default::default(), settings_view_model: Default::default(),
@ -81,7 +79,7 @@ impl AppWindow {
let home = s.setup_home(); let home = s.setup_home();
s.stack.push(&home); let _ = s.stack.push(&home);
s s
} }
@ -151,18 +149,23 @@ impl AppWindow {
pub fn close_overlay(&self) { pub fn close_overlay(&self) {
let mut view = self.settings_view_model.write().unwrap(); let mut view = self.settings_view_model.write().unwrap();
if let Some(ref mut settings) = *view { match *view {
self.overlay.remove_overlay(settings); Some(ref mut settings) => {
*view = None; self.overlay.remove_overlay(settings);
*view = None;
}
None => {}
} }
} }
fn setup_window(app: &adw::Application) -> adw::ApplicationWindow { fn setup_window(app: &adw::Application) -> adw::ApplicationWindow {
adw::ApplicationWindow::builder() let window = adw::ApplicationWindow::builder()
.application(app) .application(app)
.width_request(800) .width_request(800)
.height_request(500) .height_request(500)
.build() .build();
window
} }
fn setup_header() -> adw::HeaderBar { fn setup_header() -> adw::HeaderBar {

View File

@ -36,15 +36,16 @@ You should have received a copy of the GNU General Public License along with On
// that. // that.
use crate::perftrace; use crate::perftrace;
use gio::resources_lookup_data;
use glib::Object; use glib::Object;
use gtk::{ use gtk::{
gdk_pixbuf::{InterpType, Pixbuf},
prelude::*, prelude::*,
subclass::prelude::*, subclass::prelude::*,
}; };
use image::io::Reader as ImageReader;
use otg_core::{Color, Coordinate}; use otg_core::{Color, Coordinate};
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, io::Cursor, rc::Rc};
const WIDTH: i32 = 800; const WIDTH: i32 = 800;
const HEIGHT: i32 = 800; const HEIGHT: i32 = 800;
@ -240,7 +241,6 @@ impl Pen {
} }
} }
#[allow(dead_code)]
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) {
match color { match color {
Color::White => context.set_source_rgba(0.9, 0.9, 0.9, 0.5), Color::White => context.set_source_rgba(0.9, 0.9, 0.9, 0.5),

View File

@ -18,13 +18,14 @@ use cairo::Context;
use glib::Object; use glib::Object;
use gtk::{prelude::*, subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use sgf::{GameNode, GameRecord}; use sgf::{GameNode, GameRecord};
use std::{cell::RefCell, rc::Rc};
const WIDTH: i32 = 200; const WIDTH: i32 = 200;
const HEIGHT: i32 = 800; const HEIGHT: i32 = 800;
#[derive(Default)] #[derive(Default)]
pub struct ReviewTreePrivate { pub struct ReviewTreePrivate {
// record: Rc<RefCell<Option<GameRecord>>>, record: Rc<RefCell<Option<GameRecord>>>,
} }
#[glib::object_subclass] #[glib::object_subclass]
@ -43,7 +44,7 @@ glib::wrapper! {
} }
impl ReviewTree { impl ReviewTree {
pub fn new(_record: GameRecord) -> Self { pub fn new(record: GameRecord) -> Self {
let s: Self = Object::new(); let s: Self = Object::new();
s.set_width_request(WIDTH); s.set_width_request(WIDTH);
@ -59,7 +60,7 @@ impl ReviewTree {
s s
} }
pub fn redraw(&self, _ctx: &Context, _width: i32, _height: i32) { pub fn redraw(&self, ctx: &Context, width: i32, height: i32) {
// Implement the tree-drawing algorithm here // Implement the tree-drawing algorithm here
} }
} }
@ -112,14 +113,13 @@ struct Tree {
// out if it has children that would overlap the children of the first node. // out if it has children that would overlap the children of the first node.
// //
// My algorithm right now is likely to generate unnecessarily wide trees in a complex game review. // My algorithm right now is likely to generate unnecessarily wide trees in a complex game review.
#[allow(dead_code)]
fn node_width(node: &GameNode) -> usize { fn node_width(node: &GameNode) -> usize {
let children: &Vec<GameNode> = match node { let children: &Vec<GameNode> = match node {
GameNode::MoveNode(mn) => &mn.children, GameNode::MoveNode(mn) => &mn.children,
GameNode::SetupNode(sn) => &sn.children, GameNode::SetupNode(sn) => &sn.children,
}; };
if children.is_empty() { if children.len() == 0 {
return 1; return 1;
} }
@ -137,8 +137,7 @@ fn node_width(node: &GameNode) -> usize {
// //
// Just having the node is greatly insufficient. I can get better results if I'm calculating the // Just having the node is greatly insufficient. I can get better results if I'm calculating the
// position of its children. // position of its children.
#[allow(dead_code)] fn node_children_columns(node: &GameNode) -> Vec<usize> {
fn node_children_columns(_node: &GameNode) -> Vec<usize> {
vec![0, 1, 2] vec![0, 1, 2]
} }

View File

@ -21,10 +21,10 @@ pub use app_window::AppWindow;
mod views; mod views;
use async_std::task::{yield_now}; use async_std::task::{spawn, yield_now};
use otg_core::{Core, Observable, CoreRequest, CoreResponse}; use otg_core::{Core, Observable, CoreRequest, CoreResponse};
use std::{rc::Rc}; use std::{rc::Rc, sync::Arc};
use tokio::runtime::Runtime;
#[derive(Clone)] #[derive(Clone)]
pub struct CoreApi { pub struct CoreApi {
@ -51,7 +51,6 @@ where
/// LocalObserver creates a task on the current thread which watches the specified observer for notifications and calls the handler function with each one. /// LocalObserver creates a task on the current thread which watches the specified observer for notifications and calls the handler function with each one.
/// ///
/// The LocalObserver starts a task which listens for notifications during the constructor. When the observer goes out of scope, it will make a point of aborting the task. This combination means that anything which uses the observer can create it, hold on to a reference of it, and then drop it when done, and not have to do anything else with the observer object. /// The LocalObserver starts a task which listens for notifications during the constructor. When the observer goes out of scope, it will make a point of aborting the task. This combination means that anything which uses the observer can create it, hold on to a reference of it, and then drop it when done, and not have to do anything else with the observer object.
#[allow(dead_code)]
struct LocalObserver<T> { struct LocalObserver<T> {
join_handle: glib::JoinHandle<()>, join_handle: glib::JoinHandle<()>,
handler: Rc<dyn Fn(T)>, handler: Rc<dyn Fn(T)>,
@ -62,7 +61,6 @@ impl<T: 'static> LocalObserver<T> {
/// ///
/// observable -- any object which emits events /// observable -- any object which emits events
/// handler -- a function which can process events /// handler -- a function which can process events
#[allow(dead_code)]
fn new(observable: &dyn Observable<T>, handler: impl Fn(T) + 'static) -> Self { fn new(observable: &dyn Observable<T>, handler: impl Fn(T) + 'static) -> Self {
let listener = observable.subscribe(); let listener = observable.subscribe();
let handler = Rc::new(handler); let handler = Rc::new(handler);

View File

@ -4,15 +4,17 @@ use async_std::task::spawn;
use gio::ActionEntry; use gio::ActionEntry;
use otg_core::{Config, ConfigOption, Core, CoreNotification, LibraryPath, Observable}; use otg_core::{Config, ConfigOption, Core, CoreNotification, LibraryPath, Observable};
use otg_gtk::{ use otg_gtk::{
perftrace,
// ui::{ConfigurationPage, Home, PlayingField},
AppWindow, AppWindow,
CoreApi, CoreApi,
}; };
use std::sync::{Arc, RwLock};
const APP_ID_DEV: &str = "com.luminescent-dreams.otg.dev"; const APP_ID_DEV: &str = "com.luminescent-dreams.otg.dev";
const APP_ID_PROD: &str = "com.luminescent-dreams.otg"; const APP_ID_PROD: &str = "com.luminescent-dreams.otg";
// const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/otg/"; const RESOURCE_BASE_PATH: &str = "/com/luminescent-dreams/otg/";
async fn handler(notifications: Receiver<CoreNotification>, app_id: String) { async fn handler(notifications: Receiver<CoreNotification>, app_id: String) {
loop { loop {
@ -106,7 +108,7 @@ fn main() {
APP_ID_PROD APP_ID_PROD
}; };
let config = load_config(app_id); let config = load_config(&app_id);
let core = Core::new(config.clone()); let core = Core::new(config.clone());

View File

@ -28,10 +28,13 @@ use gtk::{prelude::*, subclass::prelude::*};
use otg_core::Color; use otg_core::Color;
use sgf::GameRecord; use sgf::GameRecord;
#[derive(Default)]
pub struct GameReviewPrivate {} pub struct GameReviewPrivate {}
impl Default for GameReviewPrivate {
fn default() -> Self {
Self {}
}
}
#[glib::object_subclass] #[glib::object_subclass]
impl ObjectSubclass for GameReviewPrivate { impl ObjectSubclass for GameReviewPrivate {
@ -49,7 +52,7 @@ glib::wrapper! {
} }
impl GameReview { impl GameReview {
pub fn new(_api: CoreApi, record: GameRecord) -> Self { pub fn new(api: CoreApi, record: GameRecord) -> Self {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
// It's actually really bad to be just throwing away errors. Panics make everyone unhappy. // It's actually really bad to be just throwing away errors. Panics make everyone unhappy.

View File

@ -22,7 +22,7 @@ use otg_core::{
CoreRequest, CoreResponse, CoreRequest, CoreResponse,
}; };
use sgf::GameRecord; use sgf::GameRecord;
use std::{cell::RefCell, rc::Rc};
/* /*
struct PlayerDataEntryPrivate { struct PlayerDataEntryPrivate {
@ -101,13 +101,19 @@ impl PlayerDataEntry {
} }
*/ */
#[derive(Default)]
pub struct HomePrivate { pub struct HomePrivate {
// black_player: Rc<RefCell<Option<PlayerDataEntry>>>, // black_player: Rc<RefCell<Option<PlayerDataEntry>>>,
// white_player: Rc<RefCell<Option<PlayerDataEntry>>>, // white_player: Rc<RefCell<Option<PlayerDataEntry>>>,
} }
impl Default for HomePrivate {
fn default() -> Self {
Self {
// black_player: Rc::new(RefCell::new(None)),
// white_player: Rc::new(RefCell::new(None)),
}
}
}
#[glib::object_subclass] #[glib::object_subclass]
impl ObjectSubclass for HomePrivate { impl ObjectSubclass for HomePrivate {
@ -152,7 +158,7 @@ impl HomeView {
s.append(&new_game_button); s.append(&new_game_button);
*/ */
let library = Library::new(on_select_game); let library = Library::new(move |game| on_select_game(game));
let library_view = gtk::ScrolledWindow::builder() let library_view = gtk::ScrolledWindow::builder()
.hscrollbar_policy(gtk::PolicyType::Never) .hscrollbar_policy(gtk::PolicyType::Never)
.min_content_width(360) .min_content_width(360)

View File

@ -14,11 +14,11 @@ 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 std::{cell::RefCell, rc::Rc}; use std::{borrow::Cow, cell::RefCell, path::Path, rc::Rc};
use adw::prelude::*; use adw::prelude::*;
use glib::Object; use glib::Object;
use gtk::{subclass::prelude::*}; use gtk::{prelude::*, subclass::prelude::*};
use otg_core::{Config, ConfigOption, LibraryPath}; use otg_core::{Config, ConfigOption, LibraryPath};
fn library_chooser_row( fn library_chooser_row(
@ -33,7 +33,7 @@ fn library_chooser_row(
.valign(gtk::Align::Center) .valign(gtk::Align::Center)
.build(); .build();
let _parent = parent.clone(); let parent = parent.clone();
let library_row = adw::ActionRow::builder() let library_row = adw::ActionRow::builder()
.title("Library Path") .title("Library Path")

View File

@ -1,3 +1,3 @@
[toolchain] [toolchain]
channel = "1.77.0" channel = "1.73.0"
targets = [ "wasm32-unknown-unknown" ] targets = [ "wasm32-unknown-unknown" ]

View File

@ -11,5 +11,9 @@ fn main() {
println!("{:?}", file); println!("{:?}", file);
let games = parse_sgf_file(&file).unwrap(); let games = parse_sgf_file(&file).unwrap();
games.into_iter().flatten().for_each(|sgf| println!("{:?}", sgf.white_player)); for sgf in games {
if let Ok(sgf) = sgf {
println!("{:?}", sgf.white_player);
}
}
} }

View File

@ -118,7 +118,7 @@ impl GameRecord {
/// Generate a list of moves which constitute the main line of the game. This is the game as it /// Generate a list of moves which constitute the main line of the game. This is the game as it
/// was actually played out, and by convention consists of the first node in each list of /// was actually played out, and by convention consists of the first node in each list of
/// children. /// children.
pub fn mainline(&self) -> Vec<&GameNode> { pub fn mainline<'a>(&'a self) -> Vec<&'a GameNode> {
let mut moves: Vec<&GameNode> = vec![]; let mut moves: Vec<&GameNode> = vec![];
let mut next = self.children.get(0); let mut next = self.children.get(0);
@ -150,7 +150,7 @@ impl Node for GameRecord {
self.children.iter().collect::<Vec<&'a GameNode>>() self.children.iter().collect::<Vec<&'a GameNode>>()
} }
fn add_child(&mut self, node: GameNode) -> &mut GameNode { fn add_child<'a>(&'a mut self, node: GameNode) -> &'a mut GameNode {
self.children.push(node); self.children.push(node);
self.children.last_mut().unwrap() self.children.last_mut().unwrap()
} }
@ -227,7 +227,7 @@ impl TryFrom<&parser::Tree> for GameRecord {
parser::Property::Round(v) => s.round = Some(v.clone()), parser::Property::Round(v) => s.round = Some(v.clone()),
parser::Property::Ruleset(v) => s.rules = Some(v.clone()), parser::Property::Ruleset(v) => s.rules = Some(v.clone()),
parser::Property::Source(v) => s.source = Some(v.clone()), parser::Property::Source(v) => s.source = Some(v.clone()),
parser::Property::TimeLimit(v) => s.time_limit = Some(*v), parser::Property::TimeLimit(v) => s.time_limit = Some(v.clone()),
parser::Property::Overtime(v) => s.overtime = Some(v.clone()), parser::Property::Overtime(v) => s.overtime = Some(v.clone()),
// parser::Property::Data(v) => s.transcriber = Some(v.clone()), // parser::Property::Data(v) => s.transcriber = Some(v.clone()),
_ => {} _ => {}
@ -238,7 +238,7 @@ impl TryFrom<&parser::Tree> for GameRecord {
.root .root
.next .next
.iter() .iter()
.map(GameNode::try_from) .map(|node| GameNode::try_from(node))
.collect::<Result<Vec<GameNode>, GameNodeError>>() .collect::<Result<Vec<GameNode>, GameNodeError>>()
.map_err(GameError::InvalidGameNode)?; .map_err(GameError::InvalidGameNode)?;
@ -257,17 +257,18 @@ pub trait Node {
fn nodes<'a>(&'a self) -> Vec<&'a GameNode> { fn nodes<'a>(&'a self) -> Vec<&'a GameNode> {
self.children() self.children()
.iter() .iter()
.flat_map(|node| { .map(|node| {
let mut children = node.nodes(); let mut children = node.nodes();
let mut v = vec![*node]; let mut v = vec![*node];
v.append(&mut children); v.append(&mut children);
v v
}) })
.flatten()
.collect::<Vec<&'a GameNode>>() .collect::<Vec<&'a GameNode>>()
} }
fn children(&self) -> Vec<&GameNode>; fn children<'a>(&'a self) -> Vec<&'a GameNode>;
fn add_child(&mut self, node: GameNode) -> &mut GameNode; fn add_child<'a>(&'a mut self, node: GameNode) -> &'a mut GameNode;
} }
impl GameNode { impl GameNode {
@ -280,21 +281,21 @@ impl GameNode {
} }
impl Node for GameNode { impl Node for GameNode {
fn children(&self) -> Vec<&GameNode> { fn children<'a>(&'a self) -> Vec<&'a GameNode> {
match self { match self {
GameNode::MoveNode(node) => node.children(), GameNode::MoveNode(node) => node.children(),
GameNode::SetupNode(node) => node.children(), GameNode::SetupNode(node) => node.children(),
} }
} }
fn nodes(&self) -> Vec<&GameNode> { fn nodes<'a>(&'a self) -> Vec<&'a GameNode> {
match self { match self {
GameNode::MoveNode(node) => node.nodes(), GameNode::MoveNode(node) => node.nodes(),
GameNode::SetupNode(node) => node.nodes(), GameNode::SetupNode(node) => node.nodes(),
} }
} }
fn add_child(&mut self, new_node: GameNode) -> &mut GameNode { fn add_child<'a>(&'a mut self, new_node: GameNode) -> &'a mut GameNode {
match self { match self {
GameNode::MoveNode(node) => node.add_child(new_node), GameNode::MoveNode(node) => node.add_child(new_node),
GameNode::SetupNode(node) => node.add_child(new_node), GameNode::SetupNode(node) => node.add_child(new_node),
@ -325,7 +326,7 @@ impl TryFrom<&parser::Node> for GameNode {
let children = n let children = n
.next .next
.iter() .iter()
.map(GameNode::try_from) .map(|n| GameNode::try_from(n))
.collect::<Result<Vec<Self>, Self::Error>>()?; .collect::<Result<Vec<Self>, Self::Error>>()?;
let node = match (move_node, setup_node) { let node = match (move_node, setup_node) {
@ -388,7 +389,7 @@ impl Node for MoveNode {
self.children.iter().collect::<Vec<&'a GameNode>>() self.children.iter().collect::<Vec<&'a GameNode>>()
} }
fn add_child(&mut self, node: GameNode) -> &mut GameNode { fn add_child<'a>(&'a mut self, node: GameNode) -> &'a mut GameNode {
self.children.push(node); self.children.push(node);
self.children.last_mut().unwrap() self.children.last_mut().unwrap()
} }
@ -416,7 +417,7 @@ impl TryFrom<&parser::Node> for MoveNode {
if s.time_left.is_some() { if s.time_left.is_some() {
return Err(Self::Error::ConflictingProperty); return Err(Self::Error::ConflictingProperty);
} }
s.time_left = Some(*duration); s.time_left = Some(duration.clone());
} }
parser::Property::Comment(cmt) => { parser::Property::Comment(cmt) => {
if s.comments.is_some() { if s.comments.is_some() {
@ -491,7 +492,7 @@ impl Node for SetupNode {
} }
#[allow(dead_code)] #[allow(dead_code)]
fn add_child(&mut self, _node: GameNode) -> &mut GameNode { fn add_child<'a>(&'a mut self, _node: GameNode) -> &'a mut GameNode {
unimplemented!() unimplemented!()
} }
} }
@ -508,7 +509,7 @@ impl TryFrom<&parser::Node> for SetupNode {
} }
#[allow(dead_code)] #[allow(dead_code)]
pub fn path_to_node(node: &GameNode, id: Uuid) -> Vec<&GameNode> { pub fn path_to_node<'a>(node: &'a GameNode, id: Uuid) -> Vec<&'a GameNode> {
if node.id() == id { if node.id() == id {
return vec![node]; return vec![node];
} }

View File

@ -70,7 +70,7 @@ impl From<nom::error::Error<&str>> for ParseError {
/// The inner Result is for errors in each individual game in the file. All of the other games can /// The inner Result is for errors in each individual game in the file. All of the other games can
/// still be kept as valid. /// still be kept as valid.
pub fn parse_sgf(input: &str) -> Result<Vec<Result<GameRecord, game::GameError>>, Error> { 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 let games = games
.into_iter() .into_iter()
.map(|game| GameRecord::try_from(&game)) .map(|game| GameRecord::try_from(&game))

View File

@ -10,7 +10,7 @@ use nom::{
IResult, Parser, IResult, Parser,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{fmt::Write, num::ParseIntError, time::Duration}; use std::{num::ParseIntError, time::Duration};
impl From<ParseSizeError> for Error { impl From<ParseSizeError> for Error {
fn from(_: ParseSizeError) -> Self { fn from(_: ParseSizeError) -> Self {
@ -303,9 +303,9 @@ impl Move {
if s.len() == 2 { if s.len() == 2 {
let mut parts = s.chars(); let mut parts = s.chars();
let row_char = parts.next().unwrap(); let row_char = parts.next().unwrap();
let row = row_char as u8 - b'a'; let row = row_char as u8 - 'a' as u8;
let column_char = parts.next().unwrap(); let column_char = parts.next().unwrap();
let column = column_char as u8 - b'a'; let column = column_char as u8 - 'a' as u8;
Some((row, column)) Some((row, column))
} else { } else {
unimplemented!("moves must contain exactly two characters"); unimplemented!("moves must contain exactly two characters");
@ -527,20 +527,22 @@ impl ToString for Property {
Property::WhiteRank(value) => format!("WR[{}]", value), Property::WhiteRank(value) => format!("WR[{}]", value),
Property::WhiteTeam(value) => format!("WT[{}]", value), Property::WhiteTeam(value) => format!("WT[{}]", value),
Property::Territory(Color::White, positions) => { Property::Territory(Color::White, positions) => {
positions format!(
.iter() "TW{}",
.fold("TW".to_owned(), |mut output, Position(p)| { positions
let _ = write!(output, "{}", p); .iter()
output .map(|Position(p)| format!("[{}]", p))
}) .collect::<String>()
)
} }
Property::Territory(Color::Black, positions) => { Property::Territory(Color::Black, positions) => {
positions format!(
.iter() "TB{}",
.fold("TB".to_owned(), |mut output, Position(p)| { positions
let _ = write!(output, "{}", p); .iter()
output .map(|Position(p)| format!("[{}]", p))
}) .collect::<String>()
)
} }
Property::Unknown(UnknownProperty { ident, value }) => { Property::Unknown(UnknownProperty { ident, value }) => {
format!("{}[{}]", ident, value) format!("{}[{}]", ident, value)
@ -963,7 +965,7 @@ fn parse_win_score<'a, E: nom::error::ParseError<&'a str>>() -> impl Parser<&'a
mod test { mod test {
use super::*; use super::*;
const EXAMPLE: &str = "(;FF[4]C[root](;C[a];C[b](;C[c]) const EXAMPLE: &'static str = "(;FF[4]C[root](;C[a];C[b](;C[c])
(;C[d];C[e])) (;C[d];C[e]))
(;C[f](;C[g];C[h];C[i]) (;C[f](;C[g];C[h];C[i])
(;C[j])))"; (;C[j])))";