-
Notifications
You must be signed in to change notification settings - Fork 3
Feature/generate levels graph #6
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
19b042d
994b62b
a85e083
92c0cbd
077d730
cc5b59c
602f0fd
5a60376
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,16 +1,21 @@ | ||
[package] | ||
name = "generate-pre-receive-hook" | ||
name = "make-git-better-scripts" | ||
version = "0.1.0" | ||
authors = ["Shay Nehmad <[email protected]>"] | ||
edition = "2018" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[lib] | ||
name = "common" | ||
path = "src/lib/lib.rs" | ||
|
||
[dependencies] | ||
structopt = "0.3.13" | ||
serde = { version = "1.0", features = ["derive"] } | ||
serde_json = "1.0" | ||
toml = "0.5" | ||
tinytemplate = "1.0.4" | ||
simple_logger = "1.6.0" | ||
log = "0.4" | ||
|
||
petgraph = "0.5" |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
use log::{debug, info}; | ||
use petgraph::algo::is_cyclic_directed; | ||
|
||
use petgraph::dot::{Config, Dot}; | ||
use petgraph::graph::NodeIndex; | ||
use petgraph::{Directed, Graph}; | ||
use serde::Serialize; | ||
use std::fs; | ||
use std::io::Write; | ||
use structopt::StructOpt; | ||
use tinytemplate::TinyTemplate; | ||
|
||
use common::{GameConfig, Level}; | ||
|
||
type LevelsGraph = Graph<Level, i32, Directed>; | ||
|
||
#[derive(Debug, StructOpt)] | ||
#[structopt(about = "A script to generate a levels graph from a game config.")] | ||
struct Cli { | ||
#[structopt(parse(from_os_str), help = "Path to game config file to read")] | ||
game_config_path: std::path::PathBuf, | ||
|
||
#[structopt(parse(from_os_str), help = "Path to the graph template file to read")] | ||
template_path: std::path::PathBuf, | ||
|
||
#[structopt( | ||
parse(from_os_str), | ||
default_value = "output/levelgraph.html", | ||
help = "Path to output file (creates if doesn't exist)" | ||
)] | ||
output_path: std::path::PathBuf, | ||
|
||
#[structopt( | ||
short = "v", | ||
long = "verbose", | ||
help = "Show more information about the actions taken" | ||
)] | ||
verbose: bool, | ||
} | ||
|
||
/// Recursive function that populates the game graph | ||
/// | ||
/// If receives a graph initialized with the first level as a root node. | ||
fn add_level_nodes_to_graph<'a>( | ||
current_level: Level, | ||
current_node: &'a NodeIndex, | ||
levels_graph: &'a mut LevelsGraph, | ||
game_config: &'a GameConfig, | ||
) { | ||
if current_level.flags.len() == 0 { | ||
return; | ||
}; | ||
|
||
for flag in current_level.flags { | ||
debug!("level {} flag {}", current_level.title, flag); | ||
let mut levels_iterator = game_config.levels.iter(); | ||
let found = levels_iterator.find(|x| x.title == flag); | ||
match found { | ||
Some(x) => { | ||
debug!( | ||
"The flag does point to another level, {}. Adding level as node to graph", | ||
x.title | ||
); | ||
let new_node = levels_graph.add_node(x.clone()); | ||
debug!("Adding edge from {} to {}", current_level.title, x.title); | ||
levels_graph.add_edge(*current_node, new_node, 0); | ||
debug!("Recursive calling add nodes on {}", x.title); | ||
add_level_nodes_to_graph(x.clone(), &new_node, levels_graph, &game_config); | ||
} | ||
None => { | ||
debug!("The flag doesn't point to another level - no need to recurse"); | ||
} | ||
} | ||
} | ||
} | ||
|
||
fn create_graph_from_game_config(game_config: &GameConfig) -> LevelsGraph { | ||
let mut levels_graph = LevelsGraph::new(); | ||
|
||
let first_level = game_config.levels[0].clone(); | ||
let tree_root = levels_graph.add_node(first_level.clone()); | ||
add_level_nodes_to_graph(first_level, &tree_root, &mut levels_graph, &game_config); | ||
|
||
levels_graph | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn test_create_graph_from_game_config() { | ||
let first_level = Level { | ||
title: String::from("first"), | ||
branch: String::from("first"), | ||
solution_checker: String::from("first"), | ||
flags: vec!["second".to_string()], | ||
}; | ||
let second_level = Level { | ||
title: String::from("second"), | ||
branch: String::from("sec"), | ||
solution_checker: String::from("sec"), | ||
flags: vec!["another".to_string(), "asdf".to_string()], | ||
}; | ||
|
||
let game_conf = GameConfig { | ||
levels: vec![first_level, second_level], | ||
}; | ||
let graph = create_graph_from_game_config(&game_conf); | ||
|
||
assert_eq!(graph.node_count(), 2); | ||
assert_eq!(graph.edge_count(), 1); | ||
assert!(graph.is_directed()); | ||
assert!(!is_cyclic_directed(&graph)); | ||
} | ||
} | ||
|
||
#[derive(Serialize)] | ||
struct Context { | ||
sandspider2234 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
levels_graph_as_dot: String, | ||
} | ||
|
||
fn main() { | ||
let args = Cli::from_args(); | ||
|
||
if args.verbose { | ||
simple_logger::init_with_level(log::Level::Debug).unwrap(); | ||
} else { | ||
simple_logger::init_with_level(log::Level::Info).unwrap(); | ||
}; | ||
|
||
info!("Reading script from {:?}", args.game_config_path); | ||
let game_config_file_contents = fs::read_to_string(args.game_config_path).unwrap(); | ||
let game_config: GameConfig = toml::from_str(&game_config_file_contents).unwrap(); | ||
|
||
let levels_graph = create_graph_from_game_config(&game_config); | ||
|
||
let levels_graph_as_dot = Dot::with_config(&levels_graph, &[Config::EdgeNoLabel]); | ||
let context = Context { | ||
levels_graph_as_dot: format!("{}", levels_graph_as_dot), | ||
}; | ||
|
||
debug!("Generated graph:\n{:?}", levels_graph_as_dot); | ||
|
||
info!("Reading template from {:?}", args.template_path); | ||
let template_file_contents = fs::read_to_string(args.template_path).unwrap(); | ||
|
||
let mut tt = TinyTemplate::new(); | ||
let template_name = "levels_graph"; | ||
tt.add_template(template_name, &template_file_contents) | ||
.unwrap(); | ||
let rendered = tt.render(template_name, &context).unwrap(); | ||
|
||
debug!("########## RENDERED TEMPLATE ##########"); | ||
debug!("{}\n", rendered); | ||
|
||
let mut output_dir = args.output_path.clone(); | ||
output_dir.pop(); | ||
fs::create_dir_all(&output_dir).expect("Failed to create parent dir"); | ||
let mut output_file = fs::File::create(&args.output_path).expect("Couldn't create file!"); | ||
output_file.write_all(&rendered.as_bytes()).unwrap(); | ||
|
||
info!("Wrote rendered file to {:?}", args.output_path); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
<div id="mynetwork"></div> | ||
|
||
<script type="text/javascript"> | ||
var DOTstring = ` | ||
{levels_graph_as_dot | unescaped} | ||
`; | ||
var parsedData = vis.parseDOTNetwork(DOTstring); | ||
|
||
var data = \{ | ||
nodes: parsedData.nodes, | ||
edges: parsedData.edges | ||
} | ||
|
||
// create a network | ||
var container = document.getElementById('mynetwork'); | ||
|
||
var options = \{ | ||
autoResize: true, | ||
nodes: \{ | ||
shape: "box", | ||
shadow: true, | ||
color: "#e8eef2", | ||
font: "20px arial black" | ||
}, | ||
edges: \{ | ||
color: "#e8eef2", | ||
}, | ||
physics: \{ | ||
enabled: true, | ||
solver: "hierarchicalRepulsion", | ||
}, | ||
layout: \{ | ||
hierarchical: \{ | ||
direction: "LR", | ||
levelSeperation: 70, | ||
nodeSpacing: 33, | ||
} | ||
} | ||
}; | ||
|
||
// initialize your network! | ||
var network = new vis.Network(container, data, options); | ||
network.on("click", function(params) \{ | ||
if (1 == params.nodes.length) \{ | ||
levelName = data.nodes[params.nodes[0]].label; | ||
console.log("Clicked on one node, it's this node: " + levelName); | ||
document.location.href = "http://localhost:1313/levels/" + levelName; | ||
|
||
} | ||
}); | ||
</script> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
use serde::{Deserialize, Serialize}; | ||
use std::fmt; | ||
|
||
#[derive(Debug, Clone, Default, Deserialize, Serialize)] | ||
pub struct Level { | ||
pub title: String, | ||
pub branch: String, | ||
pub solution_checker: String, | ||
pub flags: Vec<String>, | ||
} | ||
|
||
impl fmt::Display for Level { | ||
// This trait requires `fmt` with this exact signature. | ||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | ||
// Write strictly the first element into the supplied output | ||
// stream: `f`. Returns `fmt::Result` which indicates whether the | ||
// operation succeeded or failed. Note that `write!` uses syntax which | ||
// is very similar to `println!`. | ||
write!(f, "{}", self.title) | ||
} | ||
} | ||
sandspider2234 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
#[derive(Debug, Default, Deserialize, Serialize)] | ||
pub struct GameConfig { | ||
pub levels: Vec<Level>, | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use super::*; | ||
|
||
#[test] | ||
fn test_level_display() { | ||
let level = Level { | ||
title: "tit".to_string(), | ||
branch: "bra".to_string(), | ||
solution_checker: "sol".to_string(), | ||
flags: vec!["fla".to_string()], | ||
}; | ||
assert_eq!(format!("{}", level), "tit".to_string()); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A bit of trailing whitespace at the end :)
https://marketplace.visualstudio.com/items?itemName=shardulm94.trailing-spaces
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.