From d6b424d335184e27f6389a63f59badc633d63550 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Sun, 24 Mar 2024 11:03:40 -0400 Subject: [PATCH] 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 --- otg/core/src/goban.rs | 60 ++++++++++++++++++++++++++++---- otg/core/src/types.rs | 9 +++++ otg/gtk/src/app_window.rs | 4 +-- otg/gtk/src/views/game_review.rs | 5 ++- sgf/src/game.rs | 24 ++++++------- sgf/src/lib.rs | 27 ++++++++------ sgf/src/parser.rs | 28 +++++++++++++++ 7 files changed, 126 insertions(+), 31 deletions(-) diff --git a/otg/core/src/goban.rs b/otg/core/src/goban.rs index c565c1e..c167cf0 100644 --- a/otg/core/src/goban.rs +++ b/otg/core/src/goban.rs @@ -19,9 +19,9 @@ You should have received a copy of the GNU General Public License along with On // documenting) my code from almost a year ago. // use crate::{BoardError, Color, Size}; +use sgf::{GameNode, Move, MoveNode}; use std::collections::HashSet; - #[derive(Clone, Debug, Default)] pub struct Goban { /// The size of the board. Usually this is symetrical, but I have actually played a 5x25 game. @@ -97,9 +97,11 @@ impl Goban { pub fn from_coordinates( coordinates: impl IntoIterator, ) -> Result { - coordinates.into_iter().try_fold(Self::new(), |board, (coordinate, color)| { - board.place_stone(coordinate, color) - }) + coordinates + .into_iter() + .try_fold(Self::new(), |board, (coordinate, color)| { + board.place_stone(coordinate, color) + }) } } @@ -144,7 +146,6 @@ impl Goban { .into_iter() .filter(|c| self.stone(c) == Some(color)) .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| { @@ -189,7 +190,54 @@ impl Goban { Ok(self) } - fn stone(&self, coordinate: &Coordinate) -> Option { + /// Apply a list of moves to the board and return the final board. The moves will be played as + /// though they are live moves played normally, but this function is for generating a board + /// state from a game record. All of the moves will be played in the order given. This does not + /// allow for the branching which is natural in a game review. + /// + /// # Examples + /// + /// ``` + /// use otg_core::{Color, Size, Coordinate, Goban}; + /// use cool_asserts::assert_matches; + /// use sgf::{GameNode, MoveNode, Move}; + /// + /// let goban = Goban::new(); + /// let moves = vec![ + /// GameNode::MoveNode(MoveNode::new(sgf::Color::Black, Move::Move("dd".to_owned()))), + /// GameNode::MoveNode(MoveNode::new(sgf::Color::White, Move::Move("pp".to_owned()))), + /// GameNode::MoveNode(MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()))), + /// ]; + /// let moves_: Vec<&GameNode> = moves.iter().collect(); + /// let goban = goban.apply_moves(moves_).expect("the test to have valid moves"); + /// + /// assert_eq!(goban.stone(&Coordinate{ row: 3, column: 3 }), Some(Color::Black)); + /// assert_eq!(goban.stone(&Coordinate{ row: 15, column: 15 }), Some(Color::White)); + /// assert_eq!(goban.stone(&Coordinate{ row: 3, column: 15 }), Some(Color::Black)); + /// ``` + pub fn apply_moves<'a>( + self, + moves: impl IntoIterator, + ) -> Result { + let mut s = self; + for m in moves.into_iter() { + let s = match m { + GameNode::MoveNode(node) => s = s.apply_move_node(node)?, + GameNode::SetupNode(_n) => unimplemented!("setup nodes aren't processed yet"), + }; + } + Ok(s) + } + + fn apply_move_node(self, m: &MoveNode) -> Result { + if let Some((row, column)) = m.mv.coordinate() { + self.place_stone(Coordinate { row, column }, Color::from(&m.color)) + } else { + Ok(self) + } + } + + pub fn stone(&self, coordinate: &Coordinate) -> Option { self.groups .iter() .find(|g| g.contains(coordinate)) diff --git a/otg/core/src/types.rs b/otg/core/src/types.rs index c910beb..89ed73c 100644 --- a/otg/core/src/types.rs +++ b/otg/core/src/types.rs @@ -52,6 +52,15 @@ pub enum Color { White, } +impl From<&sgf::Color> for Color { + fn from(c: &sgf::Color) -> Self { + match c { + sgf::Color::Black => Self::Black, + sgf::Color::White => Self::White, + } + } +} + #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Size { pub width: u8, diff --git a/otg/gtk/src/app_window.rs b/otg/gtk/src/app_window.rs index 6dd60a2..b152387 100644 --- a/otg/gtk/src/app_window.rs +++ b/otg/gtk/src/app_window.rs @@ -84,9 +84,9 @@ impl AppWindow { s } - pub fn open_game_review(&self, _game: GameRecord) { + pub fn open_game_review(&self, game_record: GameRecord) { let header = adw::HeaderBar::new(); - let game_review = GameReview::new(self.core.clone()); + let game_review = GameReview::new(self.core.clone(), game_record); let layout = gtk::Box::builder() .orientation(gtk::Orientation::Vertical) diff --git a/otg/gtk/src/views/game_review.rs b/otg/gtk/src/views/game_review.rs index 7c6ac42..170f8d1 100644 --- a/otg/gtk/src/views/game_review.rs +++ b/otg/gtk/src/views/game_review.rs @@ -24,6 +24,7 @@ You should have received a copy of the GNU General Public License along with On use glib::Object; use gtk::{prelude::*, subclass::prelude::*}; +use sgf::GameRecord; use crate::{components::Goban, CoreApi}; pub struct GameReviewPrivate {} @@ -50,9 +51,11 @@ glib::wrapper! { } impl GameReview { - pub fn new(api: CoreApi) -> Self { + pub fn new(api: CoreApi, record: GameRecord) -> Self { let s: Self = Object::builder().build(); + let board_repr = otg_core::Goban::default(); + let board = Goban::new(otg_core::Goban::default()); /* s.attach(&board, 0, 0, 2, 2); diff --git a/sgf/src/game.rs b/sgf/src/game.rs index dd492cd..4a983a2 100644 --- a/sgf/src/game.rs +++ b/sgf/src/game.rs @@ -320,19 +320,19 @@ impl TryFrom<&parser::Node> for GameNode { #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] pub struct MoveNode { - id: Uuid, - color: Color, - mv: Move, - children: Vec, + pub id: Uuid, + pub color: Color, + pub mv: Move, + pub children: Vec, - time_left: Option, - moves_left: Option, - name: Option, - evaluation: Option, - value: Option, - comments: Option, - annotation: Option, - unknown_props: Vec<(String, String)>, + pub time_left: Option, + pub moves_left: Option, + pub name: Option, + pub evaluation: Option, + pub value: Option, + pub comments: Option, + pub annotation: Option, + pub unknown_props: Vec<(String, String)>, } impl MoveNode { diff --git a/sgf/src/lib.rs b/sgf/src/lib.rs index f16bf09..c205984 100644 --- a/sgf/src/lib.rs +++ b/sgf/src/lib.rs @@ -1,15 +1,19 @@ mod date; -mod game; -mod parser; -mod types; -use std::{fs::File, io::Read}; -pub use date::Date; -pub use game::GameRecord; -pub use parser::parse_collection; -use thiserror::Error; +mod game; +pub use game::{GameNode, GameRecord, MoveNode}; + +mod parser; +pub use parser::Move; + +mod types; pub use types::*; +pub use date::Date; +pub use parser::parse_collection; +use std::{fs::File, io::Read}; +use thiserror::Error; + #[derive(Debug)] pub enum Error { InvalidField, @@ -67,7 +71,8 @@ impl From> for ParseError { /// still be kept as valid. pub fn parse_sgf(input: &str) -> Result>, Error> { let (_, games) = parse_collection::>(&input)?; - let games = games.into_iter() + let games = games + .into_iter() .map(|game| GameRecord::try_from(&game)) .collect::>>(); @@ -77,7 +82,9 @@ pub fn parse_sgf(input: &str) -> Result> /// 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>, Error> { +pub fn parse_sgf_file( + path: &std::path::Path, +) -> Result>, Error> { let mut file = File::open(path).unwrap(); let mut text = String::new(); let _ = file.read_to_string(&mut text); diff --git a/sgf/src/parser.rs b/sgf/src/parser.rs index e60007f..8de8e28 100644 --- a/sgf/src/parser.rs +++ b/sgf/src/parser.rs @@ -295,6 +295,26 @@ pub enum Move { Pass, } +impl Move { + pub fn coordinate(&self) -> Option<(u8, u8)> { + match self { + Move::Pass => None, + Move::Move(s) => { + if s.len() == 2 { + let mut parts = s.chars(); + let row_char = parts.next().unwrap(); + let row = row_char as u8 - 'a' as u8; + let column_char = parts.next().unwrap(); + let column = column_char as u8 - 'a' as u8; + Some((row, column)) + } else { + unimplemented!("moves must contain exactly two characters"); + } + } + } + } +} + // KO // MN // N @@ -1184,6 +1204,14 @@ k<. Hard line breaks are all other linebreaks.", ); parse_tree::>(&data).unwrap(); } + + #[test] + fn it_can_convert_moves_to_coordinates() { + assert_eq!(Move::Pass.coordinate(), None); + assert_eq!(Move::Move("dd".to_owned()).coordinate(), Some((3, 3))); + assert_eq!(Move::Move("jj".to_owned()).coordinate(), Some((9, 9))); + assert_eq!(Move::Move("pp".to_owned()).coordinate(), Some((15, 15))); + } } #[cfg(test)]