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
1 changed files with 94 additions and 5 deletions
Showing only changes of commit 46b25cc6c5 - Show all commits

View File

@ -3,9 +3,9 @@ use config::define_config;
use config_derive::ConfigOption;
use serde::{Deserialize, Serialize};
use sgf::GameNode;
use uuid::Uuid;
use std::{cell::RefCell, fmt, path::PathBuf, time::Duration};
use std::{cell::RefCell, collections::VecDeque, fmt, path::PathBuf, time::Duration};
use thiserror::Error;
use uuid::Uuid;
define_config! {
LibraryPath(LibraryPath),
@ -235,12 +235,12 @@ impl GameState {
//
// So, what is the maximum depth of the tree? Follow all paths and see how far I get in every case.
// I could do this by just generating an intermediate tree and numbering each level.
pub struct Tree<T> {
nodes: Vec<Node<T>>,
}
struct Node<T> {
#[derive(Debug)]
pub struct Node<T> {
id: usize,
node: T,
parent: Option<usize>,
@ -294,6 +294,17 @@ impl<T> Tree<T> {
)
}
// Since I know the width of a node, now I want to figure out its placement in the larger
// scheme of things.
//
// One thought I have is that I could just develop a grid virtually and start placing nodes.
// Whenever I notice a collision, I can just move the node over. But I'd like to see if I can
// be a bit smarter than doing it as just a vec into which I place things, as though it's a
// game board. So, given a game node, I want to figure out it's position along the X axis.
//
// Just having the node is greatly insufficient. I can get better results if I'm calculating
// the position of its children.
//
// indent represents the indentation that should be applied to all children in this tree. It
// amounts to the position of the parent node.
pub fn position(&self, indent: usize, idx: usize) -> (usize, usize) {
@ -310,12 +321,22 @@ impl<T> Tree<T> {
println!("[{}] sibling width {}", idx, sibling_width);
(node.depth, indent + sibling_width)
}
// Root nodes won't have a parent, so just put them in the first column
None => (0, 0),
}
}
// Given a node, do a postorder traversal to figure out the width of the node based on all of
// its children. This is equivalent to the widest of all of its children at all depths.
//
// There are some collapse rules that I could take into account here, but that I haven't
// figured out yet. If two nodes are side by side, and one of them has some wide children but
// the other has no children, then they are effectively the same width. The second node only
// needs to be moved 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.
fn width(&self, id: usize) -> usize {
println!("[{}] calculating width", id);
let node = &self.nodes[id];
@ -333,6 +354,12 @@ impl<T> Tree<T> {
width
}
fn bfs_iter<'a>(&'a self) -> BFSIter<T> {
let mut queue = VecDeque::new();
queue.push_back(&self.nodes[0]);
BFSIter { tree: self, queue }
}
}
impl<'a> From<&'a GameNode> for Tree<Uuid> {
@ -365,9 +392,30 @@ impl<'a> From<&'a GameNode> for Tree<Uuid> {
}
}
pub struct BFSIter<'a, T> {
tree: &'a Tree<T>,
queue: VecDeque<&'a Node<T>>,
}
impl<'a, T> Iterator for BFSIter<'a, T> {
type Item = &'a Node<T>;
fn next(&mut self) -> Option<Self::Item> {
let retval = self.queue.pop_front();
if let Some(ref retval) = retval {
retval
.children
.iter()
.for_each(|idx| self.queue.push_back(&self.tree.nodes[*idx]));
}
retval
}
}
#[cfg(test)]
mod test {
use super::*;
use cool_asserts::assert_matches;
use sgf::{Move, MoveNode};
#[test]
@ -498,4 +546,45 @@ mod test {
assert_eq!(tree.position(0, 6), (1, 3));
assert_eq!(tree.position(0, 7), (1, 4));
}
#[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()));
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));
}
}