Implement game tree navigation #237
|
@ -213,7 +213,7 @@ impl Goban {
|
|||
///
|
||||
/// 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));
|
||||
/// assert_eq!(goban.stone(&Coordinate{ row: 15, column: 3 }), Some(Color::Black));
|
||||
/// ```
|
||||
pub fn apply_moves<'a>(
|
||||
self,
|
||||
|
|
|
@ -28,6 +28,8 @@ use sgf::{GameRecord, Player};
|
|||
use std::sync::{Arc, RwLock};
|
||||
|
||||
struct GameReviewViewModelPrivate {
|
||||
// This is the ID of the current position in the game. The ID is specific to the GameRecord,
|
||||
// not the ReviewTree.
|
||||
current_position: Option<NodeId>,
|
||||
game: GameRecord,
|
||||
review_tree: DepthTree,
|
||||
|
@ -35,7 +37,7 @@ struct GameReviewViewModelPrivate {
|
|||
|
||||
#[derive(Clone)]
|
||||
pub struct GameReviewViewModel {
|
||||
imp: Arc<RwLock<GameReviewViewModelPrivate>>,
|
||||
inner: Arc<RwLock<GameReviewViewModelPrivate>>,
|
||||
}
|
||||
|
||||
impl GameReviewViewModel {
|
||||
|
@ -49,7 +51,7 @@ impl GameReviewViewModel {
|
|||
};
|
||||
|
||||
Self {
|
||||
imp: Arc::new(RwLock::new(GameReviewViewModelPrivate {
|
||||
inner: Arc::new(RwLock::new(GameReviewViewModelPrivate {
|
||||
current_position,
|
||||
game,
|
||||
review_tree,
|
||||
|
@ -58,52 +60,222 @@ impl GameReviewViewModel {
|
|||
}
|
||||
|
||||
pub fn black_player(&self) -> Player {
|
||||
self.imp.read().unwrap().game.black_player.clone()
|
||||
self.inner.read().unwrap().game.black_player.clone()
|
||||
}
|
||||
|
||||
pub fn white_player(&self) -> Player {
|
||||
self.imp.read().unwrap().game.white_player.clone()
|
||||
self.inner.read().unwrap().game.white_player.clone()
|
||||
}
|
||||
|
||||
pub fn game_view(&self) -> Goban {
|
||||
let imp = self.imp.read().unwrap();
|
||||
let mainline = imp.game.mainline();
|
||||
match mainline {
|
||||
Some(mainline) => Goban::default()
|
||||
.apply_moves(mainline.map(|nr| nr.data()))
|
||||
.unwrap(),
|
||||
None => Goban::default(),
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
let mut path: Vec<NodeId> = vec![];
|
||||
let mut current_id = inner.current_position;
|
||||
while current_id.is_some() {
|
||||
let current = current_id.unwrap();
|
||||
path.push(current);
|
||||
current_id = inner.game.trees[0]
|
||||
.get(current)
|
||||
.unwrap()
|
||||
.parent()
|
||||
.map(|parent| parent.node_id());
|
||||
}
|
||||
|
||||
path.reverse();
|
||||
Goban::default()
|
||||
.apply_moves(
|
||||
path.into_iter()
|
||||
.map(|node_id| inner.game.trees[0].get(node_id).unwrap().data()),
|
||||
)
|
||||
.unwrap()
|
||||
|
||||
/*
|
||||
if let Some(start) = inner.current_position {
|
||||
let mut current_id = start;
|
||||
let mut path = vec![current_id.clone()];
|
||||
|
||||
while let
|
||||
/*
|
||||
let mut current_node = inner.game.trees[0].get(current_position).unwrap();
|
||||
let mut path = vec![current_node.data()];
|
||||
while let Some(parent) = current_node.parent() {
|
||||
path.push(parent.data());
|
||||
current_node = parent;
|
||||
}
|
||||
*/
|
||||
|
||||
path.reverse();
|
||||
Goban::default().apply_moves(path).unwrap()
|
||||
} else {
|
||||
Goban::default()
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
pub fn map_tree<F>(&self, f: F)
|
||||
where
|
||||
F: Fn(NodeRef<'_, SizeNode>, Option<NodeId>),
|
||||
{
|
||||
let imp = self.imp.read().unwrap();
|
||||
let inner = self.inner.read().unwrap();
|
||||
|
||||
for node in imp.review_tree.bfs_iter() {
|
||||
f(node, imp.current_position);
|
||||
for node in inner.review_tree.bfs_iter() {
|
||||
f(node, inner.current_position);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tree_max_depth(&self) -> usize {
|
||||
self.imp.read().unwrap().review_tree.max_depth()
|
||||
self.inner.read().unwrap().review_tree.max_depth()
|
||||
}
|
||||
|
||||
pub fn move_forward(&self) {
|
||||
unimplemented!()
|
||||
// When moving forward on the tree, I grab the first child by default. I can then just advance
|
||||
// the board state by applying the child.
|
||||
pub fn next_move(&self) {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
let current_position = inner.current_position.clone();
|
||||
match current_position {
|
||||
Some(current_position) => {
|
||||
let current_id = current_position.clone();
|
||||
let node = inner.game.trees[0].get(current_id).unwrap();
|
||||
if let Some(next_id) = node.first_child().map(|child| child.node_id()) {
|
||||
inner.current_position = Some(next_id);
|
||||
}
|
||||
}
|
||||
None => {
|
||||
inner.current_position = inner.game.trees[0].root().map(|node| node.node_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn move_backward(&self) {
|
||||
unimplemented!()
|
||||
// When moving backwards, I jump up to the parent. I'll then rebuild the board state from the
|
||||
// root.
|
||||
pub fn previous_move(&mut self) {
|
||||
let mut inner = self.inner.write().unwrap();
|
||||
if let Some(current_position) = inner.current_position {
|
||||
let current_node = inner.game.trees[0]
|
||||
.get(current_position)
|
||||
.expect("current_position should always correspond to a node in the tree");
|
||||
if let Some(parent_node) = current_node.parent() {
|
||||
inner.current_position = Some(parent_node.node_id());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn next_variant(&self) {
|
||||
unimplemented!()
|
||||
println!("move to the next variant amongst the options available");
|
||||
}
|
||||
|
||||
pub fn previous_variant(&self) {
|
||||
unimplemented!()
|
||||
println!("move to the previous variant amongst the options available");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::{Color, Coordinate};
|
||||
use std::path::Path;
|
||||
|
||||
fn with_game_record<F>(test: F)
|
||||
where
|
||||
F: FnOnce(GameReviewViewModel),
|
||||
{
|
||||
let records = sgf::parse_sgf_file(&Path::new("../../sgf/test_data/branch_test.sgf"))
|
||||
.expect("to successfully load the test file");
|
||||
let record = records[0]
|
||||
.as_ref()
|
||||
.expect("to have successfully loaded the test record");
|
||||
|
||||
let view_model = GameReviewViewModel::new(record.clone());
|
||||
|
||||
test(view_model);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_generates_a_mainline_board() {
|
||||
with_game_record(|view| {
|
||||
let goban = view.game_view();
|
||||
|
||||
for row in 0..18 {
|
||||
for column in 0..18 {
|
||||
if row == 3 && column == 3 {
|
||||
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White));
|
||||
} else if row == 15 && column == 3 {
|
||||
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White));
|
||||
} else if row == 3 && column == 15 {
|
||||
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
|
||||
} else if row == 15 && column == 14 {
|
||||
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
|
||||
} else {
|
||||
assert_eq!(
|
||||
goban.stone(&Coordinate { row, column }),
|
||||
None,
|
||||
"{} {}",
|
||||
row,
|
||||
column
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_moves_to_the_previous_mainline_move() {
|
||||
with_game_record(|mut view| {
|
||||
view.previous_move();
|
||||
let goban = view.game_view();
|
||||
|
||||
for row in 0..18 {
|
||||
for column in 0..18 {
|
||||
if row == 3 && column == 3 {
|
||||
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White));
|
||||
} else if row == 3 && column == 15 {
|
||||
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
|
||||
} else if row == 15 && column == 14 {
|
||||
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
|
||||
} else {
|
||||
assert_eq!(
|
||||
goban.stone(&Coordinate { row, column }),
|
||||
None,
|
||||
"{} {}",
|
||||
row,
|
||||
column
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn it_moves_to_the_next_node() {
|
||||
with_game_record(|mut view| {
|
||||
view.previous_move();
|
||||
view.next_move();
|
||||
let goban = view.game_view();
|
||||
|
||||
for row in 0..18 {
|
||||
for column in 0..18 {
|
||||
if row == 3 && column == 3 {
|
||||
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White));
|
||||
} else if row == 15 && column == 3 {
|
||||
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::White));
|
||||
} else if row == 3 && column == 15 {
|
||||
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
|
||||
} else if row == 15 && column == 14 {
|
||||
assert_eq!(goban.stone(&Coordinate { row, column }), Some(Color::Black));
|
||||
} else {
|
||||
assert_eq!(
|
||||
goban.stone(&Coordinate { row, column }),
|
||||
None,
|
||||
"{} {}",
|
||||
row,
|
||||
column
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,6 +17,8 @@ You should have received a copy of the GNU General Public License along with On
|
|||
use crate::{CoreApi, ResourceManager};
|
||||
use adw::prelude::*;
|
||||
|
||||
use glib::Propagation;
|
||||
use gtk::{gdk::Key, EventControllerKey};
|
||||
use otg_core::{
|
||||
settings::{SettingsRequest, SettingsResponse},
|
||||
CoreRequest, CoreResponse, GameReviewViewModel,
|
||||
|
@ -102,6 +104,22 @@ impl AppWindow {
|
|||
layout.append(&header);
|
||||
layout.append(&game_review.widget());
|
||||
|
||||
// This controller ensures that navigational keypresses get sent to the game review so that
|
||||
// they're not changing the cursor focus in the app.
|
||||
let keypress_controller = EventControllerKey::new();
|
||||
keypress_controller.connect_key_pressed({
|
||||
move |s, key, _, _| {
|
||||
println!("layout keypress: {}", key);
|
||||
if s.forward(&game_review.widget()) {
|
||||
Propagation::Stop
|
||||
} else {
|
||||
Propagation::Proceed
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
layout.add_controller(keypress_controller);
|
||||
|
||||
let page = adw::NavigationPage::builder()
|
||||
.can_pop(true)
|
||||
.title("Game Review")
|
||||
|
|
|
@ -101,6 +101,12 @@ impl Goban {
|
|||
s
|
||||
}
|
||||
|
||||
pub fn set_board_state(&mut self, board_state: otg_core::Goban) {
|
||||
println!("updating board state");
|
||||
*self.imp().board_state.borrow_mut() = board_state;
|
||||
self.queue_draw();
|
||||
}
|
||||
|
||||
fn redraw(&self, ctx: &cairo::Context, width: i32, height: i32) {
|
||||
println!("{} x {}", width, height);
|
||||
/*
|
||||
|
|
|
@ -22,8 +22,14 @@ You should have received a copy of the GNU General Public License along with On
|
|||
// I'll get all of the information about the game from the core, and then render everything in the
|
||||
// UI. So this will be a heavy lift on the UI side.
|
||||
|
||||
use crate::{components::{Goban, PlayerCard, ReviewTree}, ResourceManager};
|
||||
use gtk::{prelude::*};
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
use crate::{
|
||||
components::{Goban, PlayerCard, ReviewTree},
|
||||
ResourceManager,
|
||||
};
|
||||
use glib::Propagation;
|
||||
use gtk::{gdk::Key, prelude::*, EventControllerKey};
|
||||
use otg_core::{Color, GameReviewViewModel};
|
||||
|
||||
/*
|
||||
|
@ -57,23 +63,54 @@ impl GameReview {
|
|||
}
|
||||
*/
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct GameReview {
|
||||
widget: gtk::Box,
|
||||
goban: Rc<RefCell<Option<Goban>>>,
|
||||
|
||||
resources: ResourceManager,
|
||||
view: GameReviewViewModel,
|
||||
view: Rc<RefCell<GameReviewViewModel>>,
|
||||
}
|
||||
|
||||
impl GameReview {
|
||||
pub fn new(view: GameReviewViewModel, resources: ResourceManager) -> Self {
|
||||
let widget = gtk::Box::builder().build();
|
||||
|
||||
let view = Rc::new(RefCell::new(view));
|
||||
|
||||
let s = Self {
|
||||
widget,
|
||||
goban: Rc::new(RefCell::new(None)),
|
||||
resources,
|
||||
view,
|
||||
};
|
||||
|
||||
let keypress_controller = EventControllerKey::new();
|
||||
keypress_controller.connect_key_pressed({
|
||||
let s = s.clone();
|
||||
move |_, key, _, _| {
|
||||
println!("keystroke: {}", key);
|
||||
let mut view = s.view.borrow_mut();
|
||||
match key {
|
||||
Key::Down => view.next_move(),
|
||||
Key::Up => view.previous_move(),
|
||||
Key::Left => view.previous_variant(),
|
||||
Key::Right => view.next_variant(),
|
||||
_ => {
|
||||
return Propagation::Proceed;
|
||||
}
|
||||
}
|
||||
|
||||
match *s.goban.borrow_mut() {
|
||||
Some(ref mut goban) => goban.set_board_state(view.game_view()),
|
||||
None => {}
|
||||
};
|
||||
Propagation::Stop
|
||||
}
|
||||
});
|
||||
|
||||
s.widget.add_controller(keypress_controller);
|
||||
|
||||
s.render();
|
||||
|
||||
s
|
||||
|
@ -83,7 +120,7 @@ impl GameReview {
|
|||
// It's actually really bad to be just throwing away errors. Panics make everyone unhappy.
|
||||
// This is not a fatal error, so I'll replace this `unwrap` call with something that
|
||||
// renders the board and notifies the user of a problem that cannot be resolved.
|
||||
let board_repr = self.view.game_view();
|
||||
let board_repr = self.view.borrow().game_view();
|
||||
let board = Goban::new(board_repr, self.resources.clone());
|
||||
|
||||
/*
|
||||
|
@ -101,7 +138,7 @@ impl GameReview {
|
|||
// The review tree needs to know the record for being able to render all of the nodes. Once
|
||||
// keyboard input is being handled, the tree will have to be updated on each keystroke in
|
||||
// order to show the user where they are within the game record.
|
||||
let review_tree = ReviewTree::new(self.view.clone());
|
||||
let review_tree = ReviewTree::new(self.view.borrow().clone());
|
||||
|
||||
// I think most keyboard focus is going to end up being handled here in GameReview, as
|
||||
// keystrokes need to affect both the goban and the review tree simultanesouly. Possibly
|
||||
|
@ -114,14 +151,19 @@ impl GameReview {
|
|||
.build();
|
||||
|
||||
player_information_section
|
||||
.append(&PlayerCard::new(Color::Black, &self.view.black_player()));
|
||||
.append(&PlayerCard::new(Color::Black, &self.view.borrow().black_player()));
|
||||
player_information_section
|
||||
.append(&PlayerCard::new(Color::White, &self.view.white_player()));
|
||||
.append(&PlayerCard::new(Color::White, &self.view.borrow().white_player()));
|
||||
|
||||
self.widget.append(&board);
|
||||
sidebar.append(&player_information_section);
|
||||
sidebar.append(&review_tree.widget());
|
||||
self.widget.append(&sidebar);
|
||||
|
||||
*self.goban.borrow_mut() = Some(board);
|
||||
}
|
||||
|
||||
fn redraw(&self) {
|
||||
}
|
||||
|
||||
pub fn widget(&self) -> gtk::Widget {
|
||||
|
|
|
@ -4,7 +4,7 @@ mod game;
|
|||
pub use game::{GameNode, GameRecord, GameTree, MoveNode, Player};
|
||||
|
||||
mod parser;
|
||||
pub use parser::{parse_collection, Move};
|
||||
pub use parser::{parse_collection, Move, Size};
|
||||
|
||||
mod types;
|
||||
pub use types::*;
|
||||
|
|
|
@ -302,10 +302,11 @@ impl Move {
|
|||
Move::Move(s) => {
|
||||
if s.len() == 2 {
|
||||
let mut parts = s.chars();
|
||||
let row_char = parts.next().unwrap();
|
||||
let row = row_char as u8 - b'a';
|
||||
let column_char = parts.next().unwrap();
|
||||
let column = column_char as u8 - b'a';
|
||||
let row_char = parts.next().unwrap();
|
||||
let row = row_char as u8 - b'a';
|
||||
println!("[{}] {} [{}] {}", row_char, row, column_char, column);
|
||||
Some((row, column))
|
||||
} else {
|
||||
unimplemented!("moves must contain exactly two characters");
|
||||
|
|
Loading…
Reference in New Issue