Implement game tree navigation #237
|
@ -115,8 +115,6 @@ pub struct Core {
|
||||||
|
|
||||||
impl Core {
|
impl Core {
|
||||||
pub fn new(config: Config) -> Self {
|
pub fn new(config: Config) -> Self {
|
||||||
println!("config: {:?}", config);
|
|
||||||
|
|
||||||
let library = match config.get::<LibraryPath>() {
|
let library = match config.get::<LibraryPath>() {
|
||||||
Some(ref path) if path.to_path_buf().exists() => {
|
Some(ref path) if path.to_path_buf().exists() => {
|
||||||
Some(Database::open_path(path.to_path_buf()).unwrap())
|
Some(Database::open_path(path.to_path_buf()).unwrap())
|
||||||
|
|
|
@ -213,7 +213,7 @@ impl Goban {
|
||||||
///
|
///
|
||||||
/// assert_eq!(goban.stone(&Coordinate{ row: 3, column: 3 }), Some(Color::Black));
|
/// 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: 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>(
|
pub fn apply_moves<'a>(
|
||||||
self,
|
self,
|
||||||
|
@ -611,7 +611,6 @@ mod test {
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
println!("{}", board);
|
|
||||||
for (board, coordinate, group, liberties) in test_cases {
|
for (board, coordinate, group, liberties) in test_cases {
|
||||||
assert_eq!(board.group(&coordinate), group.as_ref());
|
assert_eq!(board.group(&coordinate), group.as_ref());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
|
|
@ -32,3 +32,6 @@ mod types;
|
||||||
pub use types::{
|
pub use types::{
|
||||||
BoardError, Color, Config, ConfigOption, DepthTree, LibraryPath, Player, Rank, Size,
|
BoardError, Color, Config, ConfigOption, DepthTree, LibraryPath, Player, Rank, Size,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod view_models;
|
||||||
|
pub use view_models::GameReviewViewModel;
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
use crate::goban::{Coordinate, Goban};
|
use crate::goban::{Coordinate, Goban};
|
||||||
use config::define_config;
|
use config::define_config;
|
||||||
use config_derive::ConfigOption;
|
use config_derive::ConfigOption;
|
||||||
|
use nary_tree::NodeRef;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use sgf::GameTree;
|
use sgf::GameTree;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -242,6 +243,39 @@ pub struct Tree<T> {
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
// https://llimllib.github.io/pymag-trees/
|
||||||
|
// I want to take advantage of the Wetherell Shannon algorithm, but I want some variations. In
|
||||||
|
// their diagram, they got a tree that looks like this.
|
||||||
|
//
|
||||||
|
// O
|
||||||
|
// |\
|
||||||
|
// O O
|
||||||
|
// |\ \ \
|
||||||
|
// O O O O
|
||||||
|
// |\ |\
|
||||||
|
// O O O O
|
||||||
|
//
|
||||||
|
// In the same circumstance, what I want is this:
|
||||||
|
//
|
||||||
|
// O--
|
||||||
|
// | \
|
||||||
|
// O O
|
||||||
|
// |\ |\
|
||||||
|
// O O O O
|
||||||
|
// |\
|
||||||
|
// O O
|
||||||
|
//
|
||||||
|
// In order to keep things from being overly smooshed, I want to ensure that if a branch overlaps
|
||||||
|
// with another branch, there is some extra drawing space. This might actually be similar to adding
|
||||||
|
// the principal that "A parent should be centered over its children".
|
||||||
|
//
|
||||||
|
// So, given a tree, I need to know how many children exist at each level. Then I build parents
|
||||||
|
// atop the children. At level 3, I have four children, and that happens to be the maximum width of
|
||||||
|
// the graph.
|
||||||
|
//
|
||||||
|
// A bottom-up traversal:
|
||||||
|
// - Figure out the number of nodes at each depth
|
||||||
|
|
||||||
pub struct DepthTree(nary_tree::Tree<SizeNode>);
|
pub struct DepthTree(nary_tree::Tree<SizeNode>);
|
||||||
|
|
||||||
impl Deref for DepthTree {
|
impl Deref for DepthTree {
|
||||||
|
@ -252,12 +286,17 @@ impl Deref for DepthTree {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Default for DepthTree {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self(nary_tree::Tree::new())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct SizeNode {
|
pub struct SizeNode {
|
||||||
/// Use this to map back to the node in the original game tree. This way we know how to
|
/// Use this to map back to the node in the original game tree. This way we know how to
|
||||||
/// correspond from a node in the review tree back to there.
|
/// correspond from a node in the review tree back to there.
|
||||||
#[allow(dead_code)]
|
pub game_node_id: nary_tree::NodeId,
|
||||||
game_node_id: nary_tree::NodeId,
|
|
||||||
|
|
||||||
/// How deep into the tree is this node?
|
/// How deep into the tree is this node?
|
||||||
depth: usize,
|
depth: usize,
|
||||||
|
@ -273,32 +312,17 @@ impl SizeNode {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DepthTree {
|
impl DepthTree {
|
||||||
// My previous work to convert from a node tree to this tree-with-width dependend on the node tree
|
|
||||||
// being a recursive data structure. Now I need to find a way to convert a slab tree to this width
|
|
||||||
// tree.
|
|
||||||
//
|
|
||||||
// It all feels like a lot of custom weirdness. I shouldn't need a bunch of custom data structures,
|
|
||||||
// so I want to eliminate the "Tree" above and keep using the slab tree. I think I should be able
|
|
||||||
// to build these Node objects without needing a custom data structure.
|
|
||||||
fn new() -> Self {
|
|
||||||
Self(nary_tree::Tree::new())
|
|
||||||
/*
|
|
||||||
Tree {
|
|
||||||
nodes: vec![Node {
|
|
||||||
id: 0,
|
|
||||||
node: root,
|
|
||||||
parent: None,
|
|
||||||
depth: 0,
|
|
||||||
width: RefCell::new(None),
|
|
||||||
children: vec![],
|
|
||||||
}],
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
pub fn node(&self, idx: usize) -> &T {
|
pub fn node(&self, idx: usize) -> &T {
|
||||||
&self.nodes[idx].node
|
&self.nodes[idx].content
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parent(&self, node: &Node<T>) -> Option<&Node<T>> {
|
||||||
|
if let Some(parent_idx) = node.parent {
|
||||||
|
self.nodes.get(parent_idx)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a node to the parent specified by parent_idx. Return the new index. This cannot be used
|
// Add a node to the parent specified by parent_idx. Return the new index. This cannot be used
|
||||||
|
@ -309,7 +333,7 @@ impl DepthTree {
|
||||||
|
|
||||||
self.nodes.push(Node {
|
self.nodes.push(Node {
|
||||||
id: next_idx,
|
id: next_idx,
|
||||||
node,
|
content: node,
|
||||||
parent: Some(parent_idx),
|
parent: Some(parent_idx),
|
||||||
depth: parent.depth + 1,
|
depth: parent.depth + 1,
|
||||||
width: RefCell::new(None),
|
width: RefCell::new(None),
|
||||||
|
@ -328,7 +352,6 @@ impl DepthTree {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.traverse_pre_order()
|
.traverse_pre_order()
|
||||||
.fold(0, |max, node| {
|
.fold(0, |max, node| {
|
||||||
println!("node depth: {}", node.data().depth);
|
|
||||||
if node.data().depth > max {
|
if node.data().depth > max {
|
||||||
node.data().depth
|
node.data().depth
|
||||||
} else {
|
} else {
|
||||||
|
@ -487,7 +510,7 @@ impl<'a> From<&'a GameTree> for DepthTree {
|
||||||
|
|
||||||
Self(tree)
|
Self(tree)
|
||||||
}
|
}
|
||||||
None => Self::new(),
|
None => Self::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -529,7 +552,7 @@ pub struct BFSIter<'a, T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, T> Iterator for BFSIter<'a, T> {
|
impl<'a, T> Iterator for BFSIter<'a, T> {
|
||||||
type Item = &'a T;
|
type Item = NodeRef<'a, T>;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
let retval = self.queue.pop_front();
|
let retval = self.queue.pop_front();
|
||||||
|
@ -538,7 +561,7 @@ impl<'a, T> Iterator for BFSIter<'a, T> {
|
||||||
.children()
|
.children()
|
||||||
.for_each(|noderef| self.queue.push_back(noderef));
|
.for_each(|noderef| self.queue.push_back(noderef));
|
||||||
}
|
}
|
||||||
retval.map(|retval| retval.data())
|
retval
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -630,7 +653,7 @@ mod test {
|
||||||
)))
|
)))
|
||||||
.node_id();
|
.node_id();
|
||||||
|
|
||||||
let node_d = game_tree
|
let _node_d = game_tree
|
||||||
.get_mut(node_c)
|
.get_mut(node_c)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.append(GameNode::MoveNode(MoveNode::new(
|
.append(GameNode::MoveNode(MoveNode::new(
|
||||||
|
@ -639,7 +662,7 @@ mod test {
|
||||||
)))
|
)))
|
||||||
.node_id();
|
.node_id();
|
||||||
|
|
||||||
let node_e = game_tree
|
let _node_e = game_tree
|
||||||
.get_mut(node_c)
|
.get_mut(node_c)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.append(GameNode::MoveNode(MoveNode::new(
|
.append(GameNode::MoveNode(MoveNode::new(
|
||||||
|
@ -648,7 +671,7 @@ mod test {
|
||||||
)))
|
)))
|
||||||
.node_id();
|
.node_id();
|
||||||
|
|
||||||
let node_f = game_tree
|
let _node_f = game_tree
|
||||||
.get_mut(node_c)
|
.get_mut(node_c)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.append(GameNode::MoveNode(MoveNode::new(
|
.append(GameNode::MoveNode(MoveNode::new(
|
||||||
|
@ -657,7 +680,7 @@ mod test {
|
||||||
)))
|
)))
|
||||||
.node_id();
|
.node_id();
|
||||||
|
|
||||||
let node_g = game_tree
|
let _node_g = game_tree
|
||||||
.get_mut(node_a)
|
.get_mut(node_a)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.append(GameNode::MoveNode(MoveNode::new(
|
.append(GameNode::MoveNode(MoveNode::new(
|
||||||
|
@ -732,66 +755,4 @@ mod test {
|
||||||
assert_eq!(tree.position(test_tree.node_g), (1, 4));
|
assert_eq!(tree.position(test_tree.node_g), (1, 4));
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
#[ignore]
|
|
||||||
#[test]
|
|
||||||
fn breadth_first_iter() {
|
|
||||||
/*
|
|
||||||
let mut node_a = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_b = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_c = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_d = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_e = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_f = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_g = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_h = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_i = MoveNode::new(sgf::Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
|
|
||||||
let game = GameRecord::new(
|
|
||||||
GameType::Go,
|
|
||||||
Size {
|
|
||||||
width: 19,
|
|
||||||
height: 19,
|
|
||||||
},
|
|
||||||
Player {
|
|
||||||
name: Some("Black".to_owned()),
|
|
||||||
rank: None,
|
|
||||||
team: None,
|
|
||||||
},
|
|
||||||
Player {
|
|
||||||
name: Some("White".to_owned()),
|
|
||||||
rank: None,
|
|
||||||
team: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_d.clone()));
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_e.clone()));
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_f.clone()));
|
|
||||||
|
|
||||||
node_b.children.push(GameNode::MoveNode(node_c.clone()));
|
|
||||||
|
|
||||||
node_h.children.push(GameNode::MoveNode(node_i.clone()));
|
|
||||||
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_b.clone()));
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_g.clone()));
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_h.clone()));
|
|
||||||
|
|
||||||
let game_tree = GameNode::MoveNode(node_a.clone());
|
|
||||||
|
|
||||||
let tree = Tree::from(&game_tree);
|
|
||||||
|
|
||||||
let mut iter = tree.bfs_iter();
|
|
||||||
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_a.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_b.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_g.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_h.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_c.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_i.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_d.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_e.id));
|
|
||||||
assert_matches!(iter.next(), Some(Node { node: uuid, .. }) => assert_eq!(*uuid, node_f.id));
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,281 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Currenty my game review is able to show the current game and its tree. Now, I want to start
|
||||||
|
// tracking where in the tree that I am. This should be a combination of the abstract Tree and the
|
||||||
|
// gameTree. Chances are, if I just keep track of where I am in the abstract tree, I can find the
|
||||||
|
// relevant node in the game tree and then reproduce the line to get to that node.
|
||||||
|
//
|
||||||
|
// Moving through the game review tree shouldn't require a full invocatian. This object, and most
|
||||||
|
// other view models, should be exported to the UI.
|
||||||
|
|
||||||
|
use crate::{types::SizeNode, DepthTree, Goban};
|
||||||
|
use nary_tree::{NodeId, NodeRef};
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct GameReviewViewModel {
|
||||||
|
inner: Arc<RwLock<GameReviewViewModelPrivate>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl GameReviewViewModel {
|
||||||
|
pub fn new(game: GameRecord) -> Self {
|
||||||
|
let (review_tree, current_position) = if !game.trees.is_empty() {
|
||||||
|
let review_tree = DepthTree::from(&game.trees[0]);
|
||||||
|
let current_position = game.mainline().unwrap().last().map(|nr| nr.node_id());
|
||||||
|
(review_tree, current_position)
|
||||||
|
} else {
|
||||||
|
(DepthTree::default(), None)
|
||||||
|
};
|
||||||
|
|
||||||
|
Self {
|
||||||
|
inner: Arc::new(RwLock::new(GameReviewViewModelPrivate {
|
||||||
|
current_position,
|
||||||
|
game,
|
||||||
|
review_tree,
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn black_player(&self) -> Player {
|
||||||
|
self.inner.read().unwrap().game.black_player.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn white_player(&self) -> Player {
|
||||||
|
self.inner.read().unwrap().game.white_player.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn game_view(&self) -> Goban {
|
||||||
|
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 inner = self.inner.read().unwrap();
|
||||||
|
|
||||||
|
for node in inner.review_tree.bfs_iter() {
|
||||||
|
f(node, inner.current_position);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tree_max_depth(&self) -> usize {
|
||||||
|
self.inner.read().unwrap().review_tree.max_depth()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
println!("move to the next variant amongst the options available");
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn previous_variant(&self) {
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
/*
|
||||||
|
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 game_review;
|
||||||
|
pub use game_review::GameReviewViewModel;
|
||||||
|
|
|
@ -17,12 +17,14 @@ You should have received a copy of the GNU General Public License along with On
|
||||||
use crate::{CoreApi, ResourceManager};
|
use crate::{CoreApi, ResourceManager};
|
||||||
use adw::prelude::*;
|
use adw::prelude::*;
|
||||||
|
|
||||||
|
use glib::Propagation;
|
||||||
|
use gtk::{gdk::Key, EventControllerKey};
|
||||||
use otg_core::{
|
use otg_core::{
|
||||||
settings::{SettingsRequest, SettingsResponse},
|
settings::{SettingsRequest, SettingsResponse},
|
||||||
CoreRequest, CoreResponse,
|
CoreRequest, CoreResponse, GameReviewViewModel,
|
||||||
};
|
};
|
||||||
use sgf::GameRecord;
|
use sgf::GameRecord;
|
||||||
use std::{rc::Rc, sync::{Arc, RwLock}};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
use crate::views::{GameReview, HomeView, SettingsView};
|
use crate::views::{GameReview, HomeView, SettingsView};
|
||||||
|
|
||||||
|
@ -91,13 +93,32 @@ impl AppWindow {
|
||||||
|
|
||||||
pub fn open_game_review(&self, game_record: 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(), game_record, self.resources.clone());
|
let game_review = GameReview::new(
|
||||||
|
GameReviewViewModel::new(game_record),
|
||||||
|
self.resources.clone(),
|
||||||
|
);
|
||||||
|
|
||||||
let layout = gtk::Box::builder()
|
let layout = gtk::Box::builder()
|
||||||
.orientation(gtk::Orientation::Vertical)
|
.orientation(gtk::Orientation::Vertical)
|
||||||
.build();
|
.build();
|
||||||
layout.append(&header);
|
layout.append(&header);
|
||||||
layout.append(&game_review);
|
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()
|
let page = adw::NavigationPage::builder()
|
||||||
.can_pop(true)
|
.can_pop(true)
|
||||||
|
|
|
@ -37,16 +37,10 @@ You should have received a copy of the GNU General Public License along with On
|
||||||
|
|
||||||
use crate::{perftrace, Resource, ResourceManager};
|
use crate::{perftrace, Resource, ResourceManager};
|
||||||
|
|
||||||
use gio::resources_lookup_data;
|
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{
|
use gtk::{gdk_pixbuf::Pixbuf, prelude::*, subclass::prelude::*};
|
||||||
gdk_pixbuf::{Colorspace, InterpType, Pixbuf},
|
|
||||||
prelude::*,
|
|
||||||
subclass::prelude::*,
|
|
||||||
};
|
|
||||||
use image::{io::Reader as ImageReader, ImageError};
|
|
||||||
use otg_core::{Color, Coordinate};
|
use otg_core::{Color, Coordinate};
|
||||||
use std::{cell::RefCell, io::Cursor, rc::Rc};
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
const WIDTH: i32 = 800;
|
const WIDTH: i32 = 800;
|
||||||
const HEIGHT: i32 = 800;
|
const HEIGHT: i32 = 800;
|
||||||
|
@ -107,17 +101,12 @@ impl Goban {
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
fn redraw(&self, ctx: &cairo::Context, width: i32, height: i32) {
|
pub fn set_board_state(&mut self, board_state: otg_core::Goban) {
|
||||||
println!("{} x {}", width, height);
|
*self.imp().board_state.borrow_mut() = board_state;
|
||||||
/*
|
self.queue_draw();
|
||||||
let background = load_pixbuf(
|
}
|
||||||
"/com/luminescent-dreams/otg-gtk/wood_texture.jpg",
|
|
||||||
false,
|
|
||||||
WIDTH + 40,
|
|
||||||
HEIGHT + 40,
|
|
||||||
);
|
|
||||||
*/
|
|
||||||
|
|
||||||
|
fn redraw(&self, ctx: &cairo::Context, width: i32, height: i32) {
|
||||||
let background = self
|
let background = self
|
||||||
.imp()
|
.imp()
|
||||||
.resource_manager
|
.resource_manager
|
||||||
|
@ -257,11 +246,11 @@ impl Pen {
|
||||||
let (x_loc, y_loc) = self.stone_location(row, col);
|
let (x_loc, y_loc) = self.stone_location(row, col);
|
||||||
match color {
|
match color {
|
||||||
Color::White => match self.white_stone {
|
Color::White => match self.white_stone {
|
||||||
Some(ref white_stone) => ctx.set_source_pixbuf(&white_stone, x_loc, y_loc),
|
Some(ref white_stone) => ctx.set_source_pixbuf(white_stone, x_loc, y_loc),
|
||||||
None => ctx.set_source_rgb(0.9, 0.9, 0.9),
|
None => ctx.set_source_rgb(0.9, 0.9, 0.9),
|
||||||
},
|
},
|
||||||
Color::Black => match self.black_stone {
|
Color::Black => match self.black_stone {
|
||||||
Some(ref black_stone) => ctx.set_source_pixbuf(&black_stone, x_loc, y_loc),
|
Some(ref black_stone) => ctx.set_source_pixbuf(black_stone, x_loc, y_loc),
|
||||||
None => ctx.set_source_rgb(0.0, 0.0, 0.0),
|
None => ctx.set_source_rgb(0.0, 0.0, 0.0),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -309,34 +298,3 @@ impl Pen {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_pixbuf(
|
|
||||||
path: &str,
|
|
||||||
transparency: bool,
|
|
||||||
width: i32,
|
|
||||||
height: i32,
|
|
||||||
) -> Result<Option<Pixbuf>, ImageError> {
|
|
||||||
let image_bytes = resources_lookup_data(path, gio::ResourceLookupFlags::NONE).unwrap();
|
|
||||||
|
|
||||||
let image = ImageReader::new(Cursor::new(image_bytes))
|
|
||||||
.with_guessed_format()
|
|
||||||
.unwrap()
|
|
||||||
.decode();
|
|
||||||
image.map(|image| {
|
|
||||||
let stride = if transparency {
|
|
||||||
image.to_rgba8().sample_layout().height_stride
|
|
||||||
} else {
|
|
||||||
image.to_rgb8().sample_layout().height_stride
|
|
||||||
};
|
|
||||||
Pixbuf::from_bytes(
|
|
||||||
&glib::Bytes::from(image.as_bytes()),
|
|
||||||
Colorspace::Rgb,
|
|
||||||
transparency,
|
|
||||||
8,
|
|
||||||
image.width() as i32,
|
|
||||||
image.height() as i32,
|
|
||||||
stride as i32,
|
|
||||||
)
|
|
||||||
.scale_simple(width, height, InterpType::Nearest)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -15,50 +15,42 @@ You should have received a copy of the GNU General Public License along with On
|
||||||
*/
|
*/
|
||||||
|
|
||||||
use cairo::Context;
|
use cairo::Context;
|
||||||
use glib::Object;
|
use gtk::prelude::*;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
use otg_core::GameReviewViewModel;
|
||||||
use otg_core::DepthTree;
|
|
||||||
use sgf::GameRecord;
|
|
||||||
use std::{cell::RefCell, rc::Rc};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
const WIDTH: i32 = 200;
|
const WIDTH: i32 = 200;
|
||||||
const HEIGHT: i32 = 800;
|
const HEIGHT: i32 = 800;
|
||||||
|
|
||||||
#[derive(Default)]
|
const RADIUS: f64 = 7.5;
|
||||||
pub struct ReviewTreePrivate {
|
const HIGHLIGHT_WIDTH: f64 = 4.;
|
||||||
record: Rc<RefCell<Option<GameRecord>>>,
|
const SPACING: f64 = 30.;
|
||||||
tree: Rc<RefCell<Option<DepthTree>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[derive(Clone)]
|
||||||
impl ObjectSubclass for ReviewTreePrivate {
|
pub struct ReviewTree {
|
||||||
const NAME: &'static str = "ReviewTree";
|
widget: gtk::ScrolledWindow,
|
||||||
type Type = ReviewTree;
|
drawing_area: gtk::DrawingArea,
|
||||||
type ParentType = gtk::DrawingArea;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ObjectImpl for ReviewTreePrivate {}
|
view: GameReviewViewModel,
|
||||||
impl WidgetImpl for ReviewTreePrivate {}
|
|
||||||
impl DrawingAreaImpl for ReviewTreePrivate {}
|
|
||||||
|
|
||||||
glib::wrapper! {
|
|
||||||
pub struct ReviewTree(ObjectSubclass<ReviewTreePrivate>) @extends gtk::Widget, gtk::DrawingArea;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ReviewTree {
|
impl ReviewTree {
|
||||||
pub fn new(record: GameRecord) -> Self {
|
pub fn new(view: GameReviewViewModel) -> ReviewTree {
|
||||||
let s: Self = Object::new();
|
let drawing_area = gtk::DrawingArea::new();
|
||||||
|
let widget = gtk::ScrolledWindow::builder().child(&drawing_area).build();
|
||||||
|
|
||||||
// TODO: there can be more than one tree, especially in instructional files. Either unify
|
widget.set_width_request(WIDTH);
|
||||||
// them into a single tree in the GameTree, or draw all of them here.
|
widget.set_height_request(HEIGHT);
|
||||||
*s.imp().tree.borrow_mut() = Some(DepthTree::from(&record.trees[0]));
|
|
||||||
*s.imp().record.borrow_mut() = Some(record);
|
|
||||||
|
|
||||||
s.set_width_request(WIDTH);
|
// TODO: figure out the maximum width of the tree so that we can also set a width request
|
||||||
s.set_height_request(HEIGHT);
|
drawing_area.set_height_request(view.tree_max_depth() as i32 * SPACING as i32);
|
||||||
|
|
||||||
s.set_draw_func({
|
let s = Self {
|
||||||
|
widget,
|
||||||
|
drawing_area,
|
||||||
|
view,
|
||||||
|
};
|
||||||
|
|
||||||
|
s.drawing_area.set_draw_func({
|
||||||
let s = s.clone();
|
let s = s.clone();
|
||||||
move |_, ctx, width, height| {
|
move |_, ctx, width, height| {
|
||||||
s.redraw(ctx, width, height);
|
s.redraw(ctx, width, height);
|
||||||
|
@ -68,168 +60,63 @@ impl ReviewTree {
|
||||||
s
|
s
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn redraw(&self, ctx: &Context, _width: i32, _height: i32) {
|
pub fn queue_draw(&self) {
|
||||||
let tree: &Option<DepthTree> = &self.imp().tree.borrow();
|
self.drawing_area.queue_draw();
|
||||||
match tree {
|
}
|
||||||
Some(ref tree) => {
|
|
||||||
for node in tree.bfs_iter() {
|
fn redraw(&self, ctx: &Context, _width: i32, _height: i32) {
|
||||||
// draw a circle given the coordinates of the nodes
|
#[allow(deprecated)]
|
||||||
// I don't know the indent. How do I keep track of that? Do I track the position of
|
let context = WidgetExt::style_context(&self.widget);
|
||||||
// the parent? do I need to just make it more intrinsically a part of the position
|
#[allow(deprecated)]
|
||||||
// code?
|
let foreground_color = context.lookup_color("sidebar_fg_color").unwrap();
|
||||||
ctx.set_source_rgb(0.7, 0.7, 0.7);
|
#[allow(deprecated)]
|
||||||
let (row, column) = node.position();
|
let accent_color = context.lookup_color("accent_color").unwrap();
|
||||||
let y = (row as f64) * 20. + 10.;
|
|
||||||
let x = (column as f64) * 20. + 10.;
|
self.view.map_tree(move |node, current| {
|
||||||
ctx.arc(x, y, 5., 0., 2. * std::f64::consts::PI);
|
let parent = node.parent();
|
||||||
|
ctx.set_source_rgb(
|
||||||
|
foreground_color.red().into(),
|
||||||
|
foreground_color.green().into(),
|
||||||
|
foreground_color.blue().into(),
|
||||||
|
);
|
||||||
|
let (row, column) = node.data().position();
|
||||||
|
let y = (row as f64) * SPACING + RADIUS * 2.;
|
||||||
|
let x = (column as f64) * SPACING + RADIUS * 2.;
|
||||||
|
ctx.arc(x, y, RADIUS, 0., 2. * std::f64::consts::PI);
|
||||||
|
let _ = ctx.fill();
|
||||||
|
|
||||||
|
if let Some(parent) = parent {
|
||||||
|
ctx.set_line_width(1.);
|
||||||
|
let (row, column) = parent.data().position();
|
||||||
|
let py = (row as f64) * SPACING + RADIUS * 2.;
|
||||||
|
let px = (column as f64) * SPACING + RADIUS * 2.;
|
||||||
|
ctx.move_to(px, py);
|
||||||
|
ctx.line_to(x, y);
|
||||||
let _ = ctx.stroke();
|
let _ = ctx.stroke();
|
||||||
}
|
}
|
||||||
}
|
|
||||||
None => {
|
|
||||||
// if there is no tree present, then there's nothing to draw!
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://llimllib.github.io/pymag-trees/
|
if current == Some(node.data().game_node_id) {
|
||||||
// I want to take advantage of the Wetherell Shannon algorithm, but I want some variations. In
|
ctx.set_line_width(HIGHLIGHT_WIDTH);
|
||||||
// their diagram, they got a tree that looks like this.
|
ctx.set_source_rgb(
|
||||||
//
|
accent_color.red().into(),
|
||||||
// O
|
accent_color.green().into(),
|
||||||
// |\
|
accent_color.blue().into(),
|
||||||
// O O
|
|
||||||
// |\ \ \
|
|
||||||
// O O O O
|
|
||||||
// |\ |\
|
|
||||||
// O O O O
|
|
||||||
//
|
|
||||||
// In the same circumstance, what I want is this:
|
|
||||||
//
|
|
||||||
// O--
|
|
||||||
// | \
|
|
||||||
// O O
|
|
||||||
// |\ |\
|
|
||||||
// O O O O
|
|
||||||
// |\
|
|
||||||
// O O
|
|
||||||
//
|
|
||||||
// In order to keep things from being overly smooshed, I want to ensure that if a branch overlaps
|
|
||||||
// with another branch, there is some extra drawing space. This might actually be similar to adding
|
|
||||||
// the principal that "A parent should be centered over its children".
|
|
||||||
//
|
|
||||||
// So, given a tree, I need to know how many children exist at each level. Then I build parents
|
|
||||||
// atop the children. At level 3, I have four children, and that happens to be the maximum width of
|
|
||||||
// the graph.
|
|
||||||
//
|
|
||||||
// A bottom-up traversal:
|
|
||||||
// - Figure out the number of nodes at each depth
|
|
||||||
|
|
||||||
/*
|
|
||||||
struct Tree {
|
|
||||||
width: Vec<usize>, // the total width of the tree at each depth
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod test {
|
|
||||||
use super::*;
|
|
||||||
use sgf::{Color, GameNode, Move, MoveNode};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_calculates_width_for_single_node() {
|
|
||||||
let node = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
|
|
||||||
|
|
||||||
assert_eq!(node_width(&node), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn it_calculates_width_for_node_with_children() {
|
|
||||||
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_b = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
|
|
||||||
let node_c = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
|
|
||||||
let node_d = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
|
|
||||||
|
|
||||||
node_a.children.push(node_b);
|
|
||||||
node_a.children.push(node_c);
|
|
||||||
node_a.children.push(node_d);
|
|
||||||
|
|
||||||
assert_eq!(node_width(&GameNode::MoveNode(node_a)), 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A
|
|
||||||
// B E
|
|
||||||
// C D
|
|
||||||
#[test]
|
|
||||||
fn it_calculates_width_with_one_deep_child() {
|
|
||||||
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_b = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_c = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_d = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_e = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
|
|
||||||
node_b.children.push(GameNode::MoveNode(node_c));
|
|
||||||
node_b.children.push(GameNode::MoveNode(node_d));
|
|
||||||
assert_eq!(node_width(&GameNode::MoveNode(node_b.clone())), 2);
|
|
||||||
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_b));
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_e));
|
|
||||||
assert_eq!(node_width(&GameNode::MoveNode(node_a)), 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A
|
|
||||||
// B G H
|
|
||||||
// C I
|
|
||||||
// D E F
|
|
||||||
#[test]
|
|
||||||
fn it_calculates_a_complex_tree() {
|
|
||||||
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_b = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_c = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_d = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_e = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_f = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_g = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let mut node_h = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_i = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_d));
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_e));
|
|
||||||
node_c.children.push(GameNode::MoveNode(node_f));
|
|
||||||
assert_eq!(node_width(&GameNode::MoveNode(node_c.clone())), 3);
|
|
||||||
|
|
||||||
node_b.children.push(GameNode::MoveNode(node_c));
|
|
||||||
assert_eq!(node_width(&GameNode::MoveNode(node_b.clone())), 3);
|
|
||||||
|
|
||||||
node_h.children.push(GameNode::MoveNode(node_i));
|
|
||||||
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_b));
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_g));
|
|
||||||
node_a.children.push(GameNode::MoveNode(node_h));
|
|
||||||
// This should be 4 if I were collapsing levels correctly, but it is 5 until I return to
|
|
||||||
// figure that step out.
|
|
||||||
assert_eq!(node_width(&GameNode::MoveNode(node_a.clone())), 5);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn a_nodes_children_get_separate_columns() {
|
|
||||||
let mut node_a = MoveNode::new(Color::Black, Move::Move("dp".to_owned()));
|
|
||||||
let node_b = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
|
|
||||||
let node_c = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
|
|
||||||
let node_d = GameNode::MoveNode(MoveNode::new(Color::Black, Move::Move("dp".to_owned())));
|
|
||||||
|
|
||||||
node_a.children.push(node_b.clone());
|
|
||||||
node_a.children.push(node_c.clone());
|
|
||||||
node_a.children.push(node_d.clone());
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
node_children_columns(&GameNode::MoveNode(node_a)),
|
|
||||||
vec![0, 1, 2]
|
|
||||||
);
|
);
|
||||||
|
ctx.arc(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
RADIUS + HIGHLIGHT_WIDTH / 2.,
|
||||||
|
0.,
|
||||||
|
2. * std::f64::consts::PI,
|
||||||
|
);
|
||||||
|
let _ = ctx.stroke();
|
||||||
|
ctx.set_line_width(2.);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
pub fn widget(&self) -> gtk::Widget {
|
||||||
fn text_renderer() {
|
self.widget.clone().upcast::<gtk::Widget>()
|
||||||
assert!(false);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -49,8 +49,8 @@ pub struct ResourceManager {
|
||||||
resources: Rc<RefCell<HashMap<String, Resource>>>,
|
resources: Rc<RefCell<HashMap<String, Resource>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ResourceManager {
|
impl Default for ResourceManager {
|
||||||
pub fn new() -> Self {
|
fn default() -> Self {
|
||||||
let mut resources = HashMap::new();
|
let mut resources = HashMap::new();
|
||||||
|
|
||||||
for (path, xres, yres, transparency) in [
|
for (path, xres, yres, transparency) in [
|
||||||
|
@ -88,7 +88,9 @@ impl ResourceManager {
|
||||||
resources: Rc::new(RefCell::new(resources)),
|
resources: Rc::new(RefCell::new(resources)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ResourceManager {
|
||||||
pub fn resource(&self, path: &str) -> Option<Resource> {
|
pub fn resource(&self, path: &str) -> Option<Resource> {
|
||||||
self.resources.borrow().get(path).cloned()
|
self.resources.borrow().get(path).cloned()
|
||||||
}
|
}
|
||||||
|
@ -123,7 +125,6 @@ impl ResourceManager {
|
||||||
.scale_simple(width, height, InterpType::Nearest)
|
.scale_simple(width, height, InterpType::Nearest)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn perftrace<F, A>(trace_name: &str, f: F) -> A
|
pub fn perftrace<F, A>(trace_name: &str, f: F) -> A
|
||||||
|
|
|
@ -122,7 +122,7 @@ fn main() {
|
||||||
|
|
||||||
app.connect_activate({
|
app.connect_activate({
|
||||||
move |app| {
|
move |app| {
|
||||||
let resources = ResourceManager::new();
|
let resources = ResourceManager::default();
|
||||||
let core_api = CoreApi { core: core.clone() };
|
let core_api = CoreApi { core: core.clone() };
|
||||||
let app_window = AppWindow::new(app, core_api, resources);
|
let app_window = AppWindow::new(app, core_api, resources);
|
||||||
|
|
||||||
|
|
|
@ -22,16 +22,21 @@ 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
|
// 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.
|
// UI. So this will be a heavy lift on the UI side.
|
||||||
|
|
||||||
use crate::{
|
use std::{cell::RefCell, rc::Rc};
|
||||||
components::{Goban, PlayerCard, ReviewTree}, CoreApi, ResourceManager
|
|
||||||
};
|
|
||||||
use glib::Object;
|
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
|
||||||
use otg_core::Color;
|
|
||||||
use sgf::GameRecord;
|
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
components::{Goban, PlayerCard, ReviewTree},
|
||||||
|
ResourceManager,
|
||||||
|
};
|
||||||
|
use glib::Propagation;
|
||||||
|
use gtk::{gdk::Key, prelude::*, EventControllerKey};
|
||||||
|
use otg_core::{Color, GameReviewViewModel};
|
||||||
|
|
||||||
|
/*
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct GameReviewPrivate {}
|
pub struct GameReviewPrivate {
|
||||||
|
model: Rc<RefCell<Option<GameReviewViewModel>>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[glib::object_subclass]
|
#[glib::object_subclass]
|
||||||
impl ObjectSubclass for GameReviewPrivate {
|
impl ObjectSubclass for GameReviewPrivate {
|
||||||
|
@ -52,14 +57,76 @@ impl GameReview {
|
||||||
pub fn new(_api: CoreApi, record: GameRecord, resources: ResourceManager) -> Self {
|
pub fn new(_api: CoreApi, record: GameRecord, resources: ResourceManager) -> Self {
|
||||||
let s: Self = Object::builder().build();
|
let s: Self = Object::builder().build();
|
||||||
|
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct GameReview {
|
||||||
|
widget: gtk::Box,
|
||||||
|
goban: Rc<RefCell<Option<Goban>>>,
|
||||||
|
review_tree: Rc<RefCell<Option<ReviewTree>>>,
|
||||||
|
|
||||||
|
resources: ResourceManager,
|
||||||
|
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: Default::default(),
|
||||||
|
review_tree: Default::default(),
|
||||||
|
resources,
|
||||||
|
view,
|
||||||
|
};
|
||||||
|
|
||||||
|
let keypress_controller = EventControllerKey::new();
|
||||||
|
keypress_controller.connect_key_pressed({
|
||||||
|
let s = s.clone();
|
||||||
|
move |_, 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 => {}
|
||||||
|
};
|
||||||
|
match *s.review_tree.borrow() {
|
||||||
|
Some(ref tree) => tree.queue_draw(),
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
Propagation::Stop
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
s.widget.add_controller(keypress_controller);
|
||||||
|
|
||||||
|
s.render();
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render(&self) {
|
||||||
// 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.
|
||||||
// This is not a fatal error, so I'll replace this `unwrap` call with something that
|
// 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.
|
// renders the board and notifies the user of a problem that cannot be resolved.
|
||||||
let board_repr = match record.mainline() {
|
let board_repr = self.view.borrow().game_view();
|
||||||
Some(iter) => otg_core::Goban::default().apply_moves(iter).unwrap(),
|
let board = Goban::new(board_repr, self.resources.clone());
|
||||||
None => otg_core::Goban::default(),
|
|
||||||
};
|
|
||||||
let board = Goban::new(board_repr, resources);
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
s.attach(&board, 0, 0, 2, 2);
|
s.attach(&board, 0, 0, 2, 2);
|
||||||
|
@ -76,7 +143,7 @@ impl GameReview {
|
||||||
// The review tree needs to know the record for being able to render all of the nodes. Once
|
// 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
|
// 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.
|
// order to show the user where they are within the game record.
|
||||||
let review_tree = ReviewTree::new(record.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
|
// 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
|
// keystrokes need to affect both the goban and the review tree simultanesouly. Possibly
|
||||||
|
@ -88,14 +155,24 @@ impl GameReview {
|
||||||
.spacing(4)
|
.spacing(4)
|
||||||
.build();
|
.build();
|
||||||
|
|
||||||
player_information_section.append(&PlayerCard::new(Color::Black, &record.black_player));
|
player_information_section
|
||||||
player_information_section.append(&PlayerCard::new(Color::White, &record.white_player));
|
.append(&PlayerCard::new(Color::Black, &self.view.borrow().black_player()));
|
||||||
|
player_information_section
|
||||||
|
.append(&PlayerCard::new(Color::White, &self.view.borrow().white_player()));
|
||||||
|
|
||||||
s.append(&board);
|
self.widget.append(&board);
|
||||||
sidebar.append(&player_information_section);
|
sidebar.append(&player_information_section);
|
||||||
sidebar.append(&review_tree);
|
sidebar.append(&review_tree.widget());
|
||||||
s.append(&sidebar);
|
self.widget.append(&sidebar);
|
||||||
|
|
||||||
s
|
*self.goban.borrow_mut() = Some(board);
|
||||||
|
*self.review_tree.borrow_mut() = Some(review_tree);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn redraw(&self) {
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn widget(&self) -> gtk::Widget {
|
||||||
|
self.widget.clone().upcast::<gtk::Widget>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,8 +2,8 @@ use crate::{
|
||||||
parser::{self, Annotation, Evaluation, Move, SetupInstr, Size, UnknownProperty},
|
parser::{self, Annotation, Evaluation, Move, SetupInstr, Size, UnknownProperty},
|
||||||
Color, Date, GameResult, GameType,
|
Color, Date, GameResult, GameType,
|
||||||
};
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use nary_tree::{NodeId, NodeMut, NodeRef, Tree};
|
use nary_tree::{NodeId, NodeMut, NodeRef, Tree};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet, VecDeque},
|
collections::{HashMap, HashSet, VecDeque},
|
||||||
fmt,
|
fmt,
|
||||||
|
@ -136,7 +136,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) -> Option<impl Iterator<Item = &'_ GameNode>> {
|
pub fn mainline(&self) -> Option<impl Iterator<Item = NodeRef<'_, GameNode>>> {
|
||||||
if !self.trees.is_empty() {
|
if !self.trees.is_empty() {
|
||||||
Some(MainlineIter {
|
Some(MainlineIter {
|
||||||
next: self.trees[0].root(),
|
next: self.trees[0].root(),
|
||||||
|
@ -405,7 +405,7 @@ pub struct MainlineIter<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> Iterator for MainlineIter<'a> {
|
impl<'a> Iterator for MainlineIter<'a> {
|
||||||
type Item = &'a GameNode;
|
type Item = NodeRef<'a, GameNode>;
|
||||||
|
|
||||||
fn next(&mut self) -> Option<Self::Item> {
|
fn next(&mut self) -> Option<Self::Item> {
|
||||||
if let Some(next) = self.next.take() {
|
if let Some(next) = self.next.take() {
|
||||||
|
@ -413,7 +413,7 @@ impl<'a> Iterator for MainlineIter<'a> {
|
||||||
self.next = next
|
self.next = next
|
||||||
.first_child()
|
.first_child()
|
||||||
.and_then(|child| self.tree.get(child.node_id()));
|
.and_then(|child| self.tree.get(child.node_id()));
|
||||||
Some(ret.data())
|
Some(ret)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
@ -634,6 +634,7 @@ mod test {
|
||||||
assert_eq!(tree.nodes().len(), 0);
|
assert_eq!(tree.nodes().len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
#[test]
|
#[test]
|
||||||
fn it_can_add_moves_to_a_game() {
|
fn it_can_add_moves_to_a_game() {
|
||||||
let mut game = GameRecord::new(
|
let mut game = GameRecord::new(
|
||||||
|
@ -660,6 +661,7 @@ mod test {
|
||||||
assert_eq!(nodes[1].id(), second_move.id);
|
assert_eq!(nodes[1].id(), second_move.id);
|
||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
#[ignore]
|
#[ignore]
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -820,6 +822,7 @@ mod path_test {
|
||||||
let moves = game
|
let moves = game
|
||||||
.mainline()
|
.mainline()
|
||||||
.expect("there should be a mainline in this file")
|
.expect("there should be a mainline in this file")
|
||||||
|
.map(|nr| nr.data())
|
||||||
.collect::<Vec<&GameNode>>();
|
.collect::<Vec<&GameNode>>();
|
||||||
assert_matches!(moves[0], GameNode::MoveNode(node) => {
|
assert_matches!(moves[0], GameNode::MoveNode(node) => {
|
||||||
assert_eq!(node.color, Color::Black);
|
assert_eq!(node.color, Color::Black);
|
||||||
|
@ -845,6 +848,7 @@ mod path_test {
|
||||||
let moves = game
|
let moves = game
|
||||||
.mainline()
|
.mainline()
|
||||||
.expect("there should be a mainline in this file")
|
.expect("there should be a mainline in this file")
|
||||||
|
.map(|nr| nr.data())
|
||||||
.collect::<Vec<&GameNode>>();
|
.collect::<Vec<&GameNode>>();
|
||||||
assert_matches!(moves[1], GameNode::MoveNode(node) => {
|
assert_matches!(moves[1], GameNode::MoveNode(node) => {
|
||||||
assert_eq!(node.color, Color::White);
|
assert_eq!(node.color, Color::White);
|
||||||
|
|
|
@ -4,7 +4,7 @@ mod game;
|
||||||
pub use game::{GameNode, GameRecord, GameTree, MoveNode, Player};
|
pub use game::{GameNode, GameRecord, GameTree, MoveNode, Player};
|
||||||
|
|
||||||
mod parser;
|
mod parser;
|
||||||
pub use parser::{parse_collection, Move};
|
pub use parser::{parse_collection, Move, Size};
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
|
|
|
@ -302,10 +302,10 @@ impl Move {
|
||||||
Move::Move(s) => {
|
Move::Move(s) => {
|
||||||
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 = row_char as u8 - b'a';
|
|
||||||
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 - b'a';
|
||||||
|
let row_char = parts.next().unwrap();
|
||||||
|
let row = row_char as u8 - b'a';
|
||||||
Some((row, column))
|
Some((row, column))
|
||||||
} else {
|
} else {
|
||||||
unimplemented!("moves must contain exactly two characters");
|
unimplemented!("moves must contain exactly two characters");
|
||||||
|
|
Loading…
Reference in New Issue