Documentation pass

This commit is contained in:
Dane Johnson 2022-06-09 12:20:35 -05:00
parent d362058b20
commit 80688d7a16
4 changed files with 215 additions and 28 deletions

View File

@ -176,9 +176,7 @@ impl BoardBuilderApp {
for edge in &node.edges {
let other_node = &board.nodes[edge];
if board.config.horizontal_wrapping && node.should_wrap_horizontal(other_node) {
let mut nodes = [node, other_node];
nodes.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap());
let [left_node, right_node] = nodes;
let [left_node, right_node] = node.horizontal_order(other_node);
let y_mid = left_node.y_mid(right_node);
painter.line_segment(
[view.inv_xform(0.0, y_mid), view.inv_xform(left_node.x, left_node.y)],
@ -248,11 +246,7 @@ impl CreateNodeDialog {
self.open = true;
self.board = board;
self.node = Node {
x,
y,
..Node::default()
}
self.node = Node::new(x, y);
}
fn ui(&mut self, ctx: &Context) {

Binary file not shown.

View File

@ -1,3 +1,23 @@
// gamenite - A graph library for board games
// Copyright (C) 2022 Dane Johnson
// This program 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.
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
//! Module to allow reading and writing to zipped board files
//!
//! This modules utilizes [serde] to write and read the board as a JSON object to a compressed file.
//! It also allows an image to stored, using [image] to convert to a png and saving it alongside the
//! graph json object.
use crate::Board;
use std::io;
@ -8,6 +28,7 @@ use std::path::Path;
use image::io::Reader as ImageReader;
use image::DynamicImage;
/// Writes the board and optionally an image to the file indicated by `path`
pub fn write_board_to_file(board: &Board, image: Option<&DynamicImage>, path: &Path) -> io::Result<()> {
let file = File::create(path)?;
let mut ar = zip::ZipWriter::new(file);
@ -25,6 +46,7 @@ pub fn write_board_to_file(board: &Board, image: Option<&DynamicImage>, path: &P
Ok(())
}
/// Reads the board and possibly an image from the file indicated by `path`
pub fn read_board_from_file(path: &Path) -> io::Result<(Board, Option<DynamicImage>)> {
let file = File::open(path)?;
let mut ar = zip::ZipArchive::new(file)?;
@ -42,12 +64,14 @@ pub fn read_board_from_file(path: &Path) -> io::Result<(Board, Option<DynamicIma
Ok((board, Some(image)))
}
/// Converts a [DynamicImage] to the PNG format
pub fn encode_png(image: &DynamicImage) -> Vec<u8> {
let mut cursor = Cursor::new(Vec::new());
image.write_to(&mut cursor, image::ImageOutputFormat::Png).unwrap();
cursor.into_inner()
}
/// Converts a PNG image to a [DynamicImage]
pub fn decode_png(buf: &[u8]) -> DynamicImage {
let cursor = Cursor::new(buf);
let mut reader = ImageReader::new(cursor);

View File

@ -1,3 +1,29 @@
// gamenite - A graph library for board games
// Copyright (C) 2022 Dane Johnson
// This program 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.
// This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
//! A graph library for board games
//!
//! When writing programs revolving around tabletop board games, often the graph is the preferred data
//! structure for representing the game board, especially in games that are neither linear nor grid-based,
//! i.e. _Risk_. This crate provides utilities for building and interacting with graph-based board states.
//!
//! More specifically, we provide [Board], a [HashMap] based graph with configurable labels and options.
//! With it we provide [Node], representing a location on the board, with connections to other Nodes.
//! Finally, we provide an [io] package to facilitate loading and saving these boards, and a related image to
//! a compressed file.
pub mod io;
use serde::{ Serialize, Deserialize };
@ -5,7 +31,9 @@ use std::collections::{ HashMap, HashSet };
#[derive(Serialize, Deserialize, Debug)]
#[serde(default)]
/// Configuration options for a [Board]
pub struct Config {
/// Whether the board wraps around the horizontal edges. Games like _Risk_ make use of this
pub horizontal_wrapping: bool,
}
impl Default for Config {
@ -16,55 +44,134 @@ impl Default for Config {
}
}
#[derive(Serialize, Deserialize, Debug, Default, Clone)]
#[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)]
#[serde(default)]
/// A space or connection on a [Board]
///
/// Nodes are [serde] [Serialize] and [Deserialize]
pub struct Node {
/// A [0, 1] based horizontal node position
pub x: f32,
/// A [0, 1] based vertical node position
pub y: f32,
/// A human-readable identifier for this node. It need not be unique
pub name: String,
/// A set of edges connecting this node to other nodes on a [Board]
pub edges: HashSet<usize>,
/// A [HashMap] of label names to values, indicating which labels apply to this node
pub labels: HashMap<String, String>,
}
impl Node {
/// Constructs a new anonymous, unconnected and unlabeled node at the given xy coordinate
/// # Panics
/// This function panics if x and y are not in the range [0, 1]
pub fn new(x: f32, y: f32) -> Self {
if !(0.0..=1.0).contains(&x) || !(0.0..=1.0).contains(&y) {
panic!("Node coordinates are not in range [0,1]");
}
Node {x, y, ..Default::default()}
}
/// Tests if the horizontal distance from this node to another is shorter by wrapping
/// over the board edge
/// # Examples
/// ```
/// use gamenite::Node;
///
/// let node1 = Node::new(0.1, 0.0);
/// let node2 = Node::new(0.2, 0.0);
/// let node3 = Node::new(0.9, 0.0);
/// assert!(!node1.should_wrap_horizontal(&node1));
/// assert!(node1.should_wrap_horizontal(&node3));
/// ```
pub fn should_wrap_horizontal(&self, other: &Node) -> bool {
let mut xs = [self.x, other.x];
xs.sort_by(|a, b| a.partial_cmp(b).unwrap());
let xs = self.horizontal_order(other).map(|n| n.x);
xs[0] + (1.0 - xs[1]) < xs[1] - xs[0]
}
/// Finds the vertical position whereby a line drawn from this node to the nearest edge
/// will have the same slope as a line drawn from the other node to the other edge at the
/// same position
/// # Examples
/// ```
/// use gamenite::Node;
///
/// let node1 = Node::new(0.2, 0.3);
/// let node2 = Node::new(0.7, 0.1);
/// let y_mid = node1.y_mid(&node2);
/// assert!(y_mid >= 0.21 && y_mid <= 0.23);
/// ```
pub fn y_mid(&self, other: &Node) -> f32 {
let mut coords = [(self.x, self.y), (other.x, other.y)];
coords.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
let [(x1, y1), (x2, y2)] = coords;
let [(x1, y1), (x2, y2)] = self.horizontal_order(other).map(Node::as_coords);
y1 - x1 * (y1 - y2) / (x1 + 1.0 - x2)
}
/// Gets the xy coordinates of the node a pair of [f32s](f32)
pub fn as_coords(&self) -> (f32, f32) {
(self.x, self.y)
}
/// Returns this node and another ordered by x coordinate
/// # Examples
/// ```
/// use gamenite::Node;
///
/// let node1 = Node::new(0.1, 0.0);
/// let node2 = Node::new(0.2, 0.0);
/// assert_eq!(node1.horizontal_order(&node2), [&node1, &node2]);
/// assert_eq!(node2.horizontal_order(&node1), [&node1, &node2]);
/// ```
pub fn horizontal_order<'a>(&'a self, other: &'a Node) -> [&'a Node; 2] {
if self.x < other.x {
[self, other]
} else {
[other, self]
}
}
}
#[derive(Serialize, Deserialize, Debug, Default)]
#[serde(default)]
/// A graph of [Nodes](Node) and related information
///
/// The graph is [serde] [Serialize] and [Deserialize].
///
/// Graphs might have labels, which are arbitrarily named keys, each with one of
/// several possible values. Each [Node] then defines it's value for each label.
///
/// As an example, in the game _Risk_ each territory is assigned to a continent. Therefore
/// the board might have a label key "Continent" with values "North America", "Asia", "Australia" etc.
/// and each node would define it's "Continent" label, for example the node named "Alaska" would
/// map "Continent" to "North America".
pub struct Board {
/// A mapping of labels to their available values
pub labels: HashMap<String, HashSet<String>>,
/// The [Nodes](Node) on this board, keyed by id
pub nodes: HashMap<usize, Node>,
/// The board configuration
pub config: Config,
}
impl Board {
pub fn add_node(&mut self, x: f32, y: f32, name: String) {
self.nodes.insert(self.next_id(), Node {
x,
y,
name,
edges: HashSet::new(),
labels: HashMap::new(),
});
}
/// Insert a [Node] into the graph, using the next available id.
/// Returns the id this node was assigned to
pub fn insert_node(&mut self, node: Node) -> usize {
let id = self.next_id();
self.nodes.insert(id, node);
id
}
/// Remove a [Node] from the graph, breaking all edges referring to
/// it and marking its id for reuse
/// # Examples
/// ```
/// use gamenite::{ Board, Node };
///
/// let mut board = Board::default();
///
/// let id1 = board.insert_node(Node::new(0.0, 0.0));
/// let id2 = board.insert_node(Node::new(0.0, 0.0));
/// board.add_edge(id2, id1);
///
/// board.remove_node(id1);
/// assert!(!board.nodes.contains_key(&id1));
/// assert!(!board.nodes[&id2].edges.contains(&id1));
/// ```
pub fn remove_node(&mut self, id: usize) {
// We remove this node from the graph, then drop it from each
// other nodes edge.
@ -73,22 +180,60 @@ impl Board {
node.edges.remove(&id);
}
}
/// Add a label with specified keys and values
pub fn insert_label(&mut self, key: &str, values: &[&str]) {
self.labels.insert(
key.to_string(),
values.iter().map(|s| s.to_string()).collect(),
);
}
/// Remove a label key from use on this board, and remove it from any nodes
/// # Examples
/// ```
/// use gamenite::{ Board, Node };
///
/// let mut board = Board::default();
/// let mut node = Node::new(0.0, 0.0);
///
/// board.insert_label("Fruit", &["Apples", "Pears"]);
/// node.labels.insert("Fruit".to_string(), "Apples".to_string());
/// let id = board.insert_node(node);
///
/// board.remove_label_key("Fruit");
/// assert!(!board.labels.contains_key("&Fruit"));
/// assert!(!board.nodes[&id].labels.contains_key("&Fruit"));
/// ```
pub fn remove_label_key(&mut self, key: &str) {
self.labels.remove(key);
for node in self.nodes.values_mut() {
node.labels.remove(key);
}
}
/// Removes a value from the label keyed by key
pub fn remove_label_value(&mut self, key: &str, value: &str) {
if let Some(l) = self.labels.get_mut(key) {
l.remove(value);
}
}
/// Adds an edge from a node to another by ids
/// # Examples
/// ```
/// use gamenite::{ Board, Node };
///
/// let mut board = Board::default();
/// let id1 = board.insert_node(Node::new(0.0, 0.0));
/// let id2 = board.insert_node(Node::new(0.0, 0.0));
///
/// board.add_edge(id1, id2);
/// assert!(board.nodes[&id1].edges.contains(&id2));
/// assert!(!board.nodes[&id2].edges.contains(&id1));
pub fn add_edge(&mut self, from: usize, to: usize) {
let node = self.nodes.get_mut(&from).expect("Could not find node");
node.edges.insert(to);
}
/// Finds the nearest node to a given position
pub fn nearest_node(&self, x: f32, y: f32) -> Option<usize> {
let f = |n: &Node| dist_sq((n.x, n.y), (x, y));
let mut iter = self.nodes.iter();
@ -130,9 +275,32 @@ fn inv_lerp(v0: f32, v1: f32, a: f32) -> f32 {
(a - v0) / (v1 - v0)
}
/// Trait to allow transformations from the (\[0,1\], \[0,1\]) coordinate space of the
/// graph to another coordinate space
/// # Examples
/// ```
/// use gamenite::{ CoordTransformer, Node };
/// use std::ops::Range;
///
/// // Convert to Cartesian coordinates
/// struct CartesianCoords { domain: Range<f32>, range: Range<f32>};
/// impl CoordTransformer<(f32, f32)> for CartesianCoords {
/// fn origin(&self) -> (f32, f32) { (self.domain.start, self.range.start) }
/// fn extremes(&self) -> (f32, f32) { (self.domain.end, self.range.end) }
/// }
///
/// // Cartesian coordinates from ([-50, 30], [-10, 100])
/// let graph = CartesianCoords { domain: -50.0..30.0, range: -10.0..100.0 };
/// let node = Node::new(0.5, 0.5);
/// assert_eq!(graph.inv_xform(node.x, node.y), (-10.0, 45.0));
/// assert_eq!(graph.xform((-10.0, 45.0)), (node.x, node.y));
/// ```
pub trait CoordTransformer<I: Into<(f32, f32)> + From<(f32, f32)>> {
/// Returns the smallest values that a coordinate can take on
fn origin(&self) -> I;
/// Returns the largest values that a coordinate can take on
fn extremes(&self) -> I;
/// Returns the coordinate mapped into the [Board] standard (\[0, 1\], \[0, 1\]) coordinate space
fn xform(&self, pos: I) -> (f32, f32) {
let (sx, sy) = self.origin().into();
let (ex, ey) = self.extremes().into();
@ -142,6 +310,7 @@ pub trait CoordTransformer<I: Into<(f32, f32)> + From<(f32, f32)>> {
inv_lerp(sy, ey, y),
)
}
/// Returns the board coordinate mapped into this space
fn inv_xform(&self, x: f32, y: f32) -> I {
let (sx, sy) = self.origin().into();
let (ex, ey) = self.extremes().into();