Set up the game review page along with #229

Merged
savanni merged 24 commits from otg/game-review into main 2024-03-31 23:37:51 +00:00
7 changed files with 126 additions and 31 deletions
Showing only changes of commit d9bb9d92e5 - Show all commits

View File

@ -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. // documenting) my code from almost a year ago.
// //
use crate::{BoardError, Color, Size}; use crate::{BoardError, Color, Size};
use sgf::{GameNode, Move, MoveNode};
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. /// The size of the board. Usually this is symetrical, but I have actually played a 5x25 game.
@ -97,7 +97,9 @@ impl Goban {
pub fn from_coordinates( pub fn from_coordinates(
coordinates: impl IntoIterator<Item = (Coordinate, Color)>, coordinates: impl IntoIterator<Item = (Coordinate, Color)>,
) -> Result<Self, BoardError> { ) -> Result<Self, BoardError> {
coordinates.into_iter().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)
}) })
} }
@ -144,7 +146,6 @@ impl Goban {
.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 // In fact, this last step actually connects the coordinates of those friendly groups
// into a single large group. // into a single large group.
.fold(HashSet::new(), |acc, set| { .fold(HashSet::new(), |acc, set| {
@ -189,7 +190,54 @@ impl Goban {
Ok(self) Ok(self)
} }
fn stone(&self, coordinate: &Coordinate) -> Option<Color> { /// 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<Item = &'a GameNode>,
) -> Result<Goban, BoardError> {
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<Goban, BoardError> {
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<Color> {
self.groups self.groups
.iter() .iter()
.find(|g| g.contains(coordinate)) .find(|g| g.contains(coordinate))

View File

@ -52,6 +52,15 @@ pub enum Color {
White, 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)] #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Size { pub struct Size {
pub width: u8, pub width: u8,

View File

@ -84,9 +84,9 @@ impl AppWindow {
s s
} }
pub fn open_game_review(&self, _game: GameRecord) { pub fn open_game_review(&self, game_record: 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(), game_record);
let layout = gtk::Box::builder() let layout = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical) .orientation(gtk::Orientation::Vertical)

View File

@ -24,6 +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 sgf::GameRecord;
use crate::{components::Goban, CoreApi}; use crate::{components::Goban, CoreApi};
pub struct GameReviewPrivate {} pub struct GameReviewPrivate {}
@ -50,9 +51,11 @@ glib::wrapper! {
} }
impl GameReview { impl GameReview {
pub fn new(api: CoreApi) -> Self { pub fn new(api: CoreApi, record: GameRecord) -> Self {
let s: Self = Object::builder().build(); let s: Self = Object::builder().build();
let board_repr = otg_core::Goban::default();
let board = Goban::new(otg_core::Goban::default()); let board = Goban::new(otg_core::Goban::default());
/* /*
s.attach(&board, 0, 0, 2, 2); s.attach(&board, 0, 0, 2, 2);

View File

@ -320,19 +320,19 @@ impl TryFrom<&parser::Node> for GameNode {
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)]
pub struct MoveNode { pub struct MoveNode {
id: Uuid, pub id: Uuid,
color: Color, pub color: Color,
mv: Move, pub mv: Move,
children: Vec<GameNode>, pub children: Vec<GameNode>,
time_left: Option<Duration>, pub time_left: Option<Duration>,
moves_left: Option<usize>, pub moves_left: Option<usize>,
name: Option<String>, pub name: Option<String>,
evaluation: Option<Evaluation>, pub evaluation: Option<Evaluation>,
value: Option<f64>, pub value: Option<f64>,
comments: Option<String>, pub comments: Option<String>,
annotation: Option<Annotation>, pub annotation: Option<Annotation>,
unknown_props: Vec<(String, String)>, pub unknown_props: Vec<(String, String)>,
} }
impl MoveNode { impl MoveNode {

View File

@ -1,15 +1,19 @@
mod date; mod date;
mod game;
mod parser;
mod types;
use std::{fs::File, io::Read}; mod game;
pub use date::Date; pub use game::{GameNode, GameRecord, MoveNode};
pub use game::GameRecord;
pub use parser::parse_collection; mod parser;
use thiserror::Error; pub use parser::Move;
mod types;
pub use types::*; pub use types::*;
pub use date::Date;
pub use parser::parse_collection;
use std::{fs::File, io::Read};
use thiserror::Error;
#[derive(Debug)] #[derive(Debug)]
pub enum Error { pub enum Error {
InvalidField, InvalidField,
@ -67,7 +71,8 @@ impl From<nom::error::Error<&str>> for ParseError {
/// 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.into_iter() let games = games
.into_iter()
.map(|game| GameRecord::try_from(&game)) .map(|game| GameRecord::try_from(&game))
.collect::<Vec<Result<GameRecord, game::GameError>>>(); .collect::<Vec<Result<GameRecord, game::GameError>>>();
@ -77,7 +82,9 @@ pub fn parse_sgf(input: &str) -> Result<Vec<Result<GameRecord, game::GameError>>
/// Given a path, parse all of the games stored in that file. /// Given a path, parse all of the games stored in that file.
/// ///
/// See also `parse_sgf` /// See also `parse_sgf`
pub fn parse_sgf_file(path: &std::path::Path) -> Result<Vec<Result<GameRecord, game::GameError>>, Error> { 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);

View File

@ -295,6 +295,26 @@ pub enum Move {
Pass, 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 // KO
// MN // MN
// N // N
@ -1184,6 +1204,14 @@ k<. Hard line breaks are all other linebreaks.",
); );
parse_tree::<nom::error::VerboseError<&str>>(&data).unwrap(); parse_tree::<nom::error::VerboseError<&str>>(&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)] #[cfg(test)]