use super::graph::{RedstoneGraph, RedstoneNodeKind};
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct GraphFeatures {
pub node_count: usize,
pub edge_count: usize,
pub node_kind_counts: BTreeMap<String, usize>,
pub has_cycles: bool,
pub is_combinational: bool,
pub critical_path: u32,
pub delay_weighted_depth: u32,
pub scc_count: usize,
pub largest_scc: usize,
pub weakly_connected_components: usize,
pub max_fan_in: usize,
pub max_fan_out: usize,
pub approx_input_count: usize,
pub approx_output_count: usize,
}
impl GraphFeatures {
pub fn to_json(&self) -> Result<String, String> {
serde_json::to_string(self).map_err(|e| e.to_string())
}
pub fn from_json(s: &str) -> Result<Self, String> {
serde_json::from_str(s).map_err(|e| e.to_string())
}
}
fn kind_name(kind: &RedstoneNodeKind) -> &'static str {
match kind {
RedstoneNodeKind::Repeater { .. } => "Repeater",
RedstoneNodeKind::Comparator { .. } => "Comparator",
RedstoneNodeKind::Torch => "Torch",
RedstoneNodeKind::Lamp => "Lamp",
RedstoneNodeKind::Button => "Button",
RedstoneNodeKind::Lever => "Lever",
RedstoneNodeKind::PressurePlate => "PressurePlate",
RedstoneNodeKind::Trapdoor => "Trapdoor",
RedstoneNodeKind::Wire => "Wire",
RedstoneNodeKind::Constant => "Constant",
RedstoneNodeKind::NoteBlock => "NoteBlock",
}
}
fn delay_weight(kind: &RedstoneNodeKind) -> u32 {
match kind {
RedstoneNodeKind::Repeater { delay } => u32::from(*delay),
_ => 1,
}
}
impl RedstoneGraph {
fn out_adjacency(&self) -> Vec<Vec<usize>> {
let n = self.nodes.len();
let mut out: Vec<Vec<usize>> = vec![Vec::new(); n];
for node in &self.nodes {
for link in &node.inputs {
if link.from < n {
out[link.from].push(node.id);
}
}
}
out
}
pub fn node_kind_counts(&self) -> BTreeMap<String, usize> {
let mut counts: BTreeMap<String, usize> = BTreeMap::new();
for node in &self.nodes {
*counts.entry(kind_name(&node.kind).to_string()).or_insert(0) += 1;
}
counts
}
pub fn strongly_connected_components(&self) -> Vec<Vec<usize>> {
let n = self.nodes.len();
let out = self.out_adjacency();
self.tarjan_scc(&out, n)
}
fn tarjan_scc(&self, out: &[Vec<usize>], n: usize) -> Vec<Vec<usize>> {
const UNVISITED: usize = usize::MAX;
let mut index = vec![UNVISITED; n];
let mut lowlink = vec![0usize; n];
let mut on_stack = vec![false; n];
let mut stack: Vec<usize> = Vec::new();
let mut next_index = 0usize;
let mut sccs: Vec<Vec<usize>> = Vec::new();
let mut work: Vec<(usize, usize)> = Vec::new();
for root in 0..n {
if index[root] != UNVISITED {
continue;
}
work.push((root, 0));
while let Some(&(v, child_pos)) = work.last() {
if child_pos == 0 {
index[v] = next_index;
lowlink[v] = next_index;
next_index += 1;
stack.push(v);
on_stack[v] = true;
}
let mut recursed = false;
let mut pos = child_pos;
while pos < out[v].len() {
let w = out[v][pos];
if index[w] == UNVISITED {
*work.last_mut().unwrap() = (v, pos + 1);
work.push((w, 0));
recursed = true;
break;
} else if on_stack[w] {
lowlink[v] = lowlink[v].min(index[w]);
}
pos += 1;
}
if recursed {
continue;
}
if lowlink[v] == index[v] {
let mut component = Vec::new();
loop {
let w = stack.pop().unwrap();
on_stack[w] = false;
component.push(w);
if w == v {
break;
}
}
sccs.push(component);
}
work.pop();
if let Some(&(parent, _)) = work.last() {
lowlink[parent] = lowlink[parent].min(lowlink[v]);
}
}
}
sccs
}
pub fn has_cycles(&self) -> bool {
for node in &self.nodes {
if node.inputs.iter().any(|l| l.from == node.id) {
return true;
}
}
self.strongly_connected_components()
.iter()
.any(|scc| scc.len() > 1)
}
pub fn is_combinational(&self) -> bool {
!self.has_cycles()
}
pub fn weakly_connected_components(&self) -> usize {
let n = self.nodes.len();
if n == 0 {
return 0;
}
let mut uf = UnionFind::new(n);
for node in &self.nodes {
for link in &node.inputs {
if link.from < n {
uf.union(node.id, link.from);
}
}
}
let mut roots = std::collections::HashSet::new();
for i in 0..n {
roots.insert(uf.find(i));
}
roots.len()
}
pub fn critical_path(&self) -> u32 {
self.longest_weighted_path(WeightMode::NodeCount)
}
pub fn delay_weighted_depth(&self) -> u32 {
self.longest_weighted_path(WeightMode::Delay)
}
pub fn max_fan_in(&self) -> usize {
self.nodes.iter().map(|n| n.inputs.len()).max().unwrap_or(0)
}
pub fn max_fan_out(&self) -> usize {
self.out_adjacency()
.iter()
.map(|o| o.len())
.max()
.unwrap_or(0)
}
pub fn features(&self) -> GraphFeatures {
let n = self.nodes.len();
let out = self.out_adjacency();
let sccs = self.tarjan_scc(&out, n);
let has_self_loop = self
.nodes
.iter()
.any(|node| node.inputs.iter().any(|l| l.from == node.id));
let has_cycles = has_self_loop || sccs.iter().any(|scc| scc.len() > 1);
let scc_count = sccs.len();
let largest_scc = sccs.iter().map(|s| s.len()).max().unwrap_or(0);
let critical_path = self.longest_path_with_sccs(&out, &sccs, WeightMode::NodeCount);
let delay_weighted_depth = self.longest_path_with_sccs(&out, &sccs, WeightMode::Delay);
let max_fan_in = self.max_fan_in();
let max_fan_out = out.iter().map(|o| o.len()).max().unwrap_or(0);
let mut approx_input_count = 0usize;
let mut approx_output_count = 0usize;
for node in &self.nodes {
match node.kind {
RedstoneNodeKind::Lever
| RedstoneNodeKind::Button
| RedstoneNodeKind::PressurePlate => approx_input_count += 1,
RedstoneNodeKind::Lamp
| RedstoneNodeKind::Trapdoor
| RedstoneNodeKind::NoteBlock => approx_output_count += 1,
_ => {}
}
}
GraphFeatures {
node_count: n,
edge_count: self.edge_count(),
node_kind_counts: self.node_kind_counts(),
has_cycles,
is_combinational: !has_cycles,
critical_path,
delay_weighted_depth,
scc_count,
largest_scc,
weakly_connected_components: self.weakly_connected_components(),
max_fan_in,
max_fan_out,
approx_input_count,
approx_output_count,
}
}
fn longest_weighted_path(&self, mode: WeightMode) -> u32 {
let n = self.nodes.len();
let out = self.out_adjacency();
let sccs = self.tarjan_scc(&out, n);
self.longest_path_with_sccs(&out, &sccs, mode)
}
fn longest_path_with_sccs(
&self,
out: &[Vec<usize>],
sccs: &[Vec<usize>],
mode: WeightMode,
) -> u32 {
let n = self.nodes.len();
if n == 0 {
return 0;
}
let c = sccs.len();
let mut comp_of = vec![0usize; n];
for (ci, scc) in sccs.iter().enumerate() {
for &node_id in scc {
comp_of[node_id] = ci;
}
}
let mut weight = vec![0u32; c];
for (ci, scc) in sccs.iter().enumerate() {
let mut w = 0u32;
for &node_id in scc {
w = w.saturating_add(match mode {
WeightMode::NodeCount => 1,
WeightMode::Delay => delay_weight(&self.nodes[node_id].kind),
});
}
weight[ci] = w;
}
let mut cond_out: Vec<Vec<usize>> = vec![Vec::new(); c];
let mut indeg = vec![0usize; c];
let mut seen: std::collections::HashSet<(usize, usize)> = std::collections::HashSet::new();
for u in 0..n {
let cu = comp_of[u];
for &v in &out[u] {
let cv = comp_of[v];
if cu != cv && seen.insert((cu, cv)) {
cond_out[cu].push(cv);
indeg[cv] += 1;
}
}
}
let mut queue: Vec<usize> = (0..c).filter(|&i| indeg[i] == 0).collect();
let mut topo: Vec<usize> = Vec::with_capacity(c);
let mut indeg_mut = indeg.clone();
let mut head = 0;
while head < queue.len() {
let u = queue[head];
head += 1;
topo.push(u);
for &v in &cond_out[u] {
indeg_mut[v] -= 1;
if indeg_mut[v] == 0 {
queue.push(v);
}
}
}
let mut best = weight.clone();
for &u in &topo {
let bu = best[u];
for &v in &cond_out[u] {
let cand = bu.saturating_add(weight[v]);
if cand > best[v] {
best[v] = cand;
}
}
}
best.into_iter().max().unwrap_or(0)
}
}
#[derive(Clone, Copy)]
enum WeightMode {
NodeCount,
Delay,
}
struct UnionFind {
parent: Vec<usize>,
size: Vec<usize>,
}
impl UnionFind {
fn new(n: usize) -> Self {
UnionFind {
parent: (0..n).collect(),
size: vec![1; n],
}
}
fn find(&mut self, mut x: usize) -> usize {
while self.parent[x] != x {
self.parent[x] = self.parent[self.parent[x]];
x = self.parent[x];
}
x
}
fn union(&mut self, a: usize, b: usize) {
let (mut ra, mut rb) = (self.find(a), self.find(b));
if ra == rb {
return;
}
if self.size[ra] < self.size[rb] {
std::mem::swap(&mut ra, &mut rb);
}
self.parent[rb] = ra;
self.size[ra] += self.size[rb];
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::simulation::graph::{
ComparatorMode, LinkKind, RedstoneGraph, RedstoneLink, RedstoneNode, RedstoneNodeKind,
};
use crate::simulation::MchprsWorld;
use crate::{BlockState, UniversalSchematic};
fn node(id: usize, kind: RedstoneNodeKind, inputs: Vec<usize>) -> RedstoneNode {
RedstoneNode {
id,
kind,
pos: None,
facing_diode: false,
powered: false,
repeater_locked: false,
output_strength: 0,
aliased_blocks: Vec::new(),
inputs: inputs
.into_iter()
.map(|from| RedstoneLink {
from,
kind: LinkKind::Default,
strength: 0,
})
.collect(),
}
}
fn create_simple_redstone_line() -> UniversalSchematic {
let mut schematic = UniversalSchematic::new("Simple Redstone Line".to_string());
for x in 0..16 {
schematic.set_block(
x,
0,
0,
&BlockState::new("minecraft:gray_concrete".to_string()),
);
}
for x in 1..15 {
let mut wire = BlockState::new("minecraft:redstone_wire".to_string());
wire.set_property("power", "0");
wire.set_property("east", "side");
wire.set_property("west", "side");
wire.set_property("north", "none");
wire.set_property("south", "none");
schematic.set_block(x, 1, 0, &wire);
}
let mut lever = BlockState::new("minecraft:lever".to_string());
lever.set_property("facing", "east");
lever.set_property("powered", "false");
lever.set_property("face", "floor");
schematic.set_block(0, 1, 0, &lever);
let mut lamp = BlockState::new("minecraft:redstone_lamp".to_string());
lamp.set_property("lit", "false");
schematic.set_block(15, 1, 0, &lamp);
schematic
}
#[test]
fn test_real_fixture_combinational() {
let schematic = create_simple_redstone_line();
let world = MchprsWorld::new(schematic).expect("world creation should succeed");
let graph = world
.export_graph()
.expect("graph extraction should succeed");
assert!(
graph.is_combinational(),
"lever->lamp should be combinational"
);
assert!(!graph.has_cycles(), "lever->lamp should have no cycles");
assert_eq!(
graph.weakly_connected_components(),
1,
"the whole line is one weak component"
);
let counts = graph.node_kind_counts();
assert!(counts.contains_key("Lever"), "kind counts include Lever");
assert!(counts.contains_key("Lamp"), "kind counts include Lamp");
assert!(
graph.critical_path() >= 2,
"critical path should span at least lever..lamp, got {}",
graph.critical_path()
);
}
#[test]
fn test_two_node_cycle() {
let graph = RedstoneGraph {
nodes: vec![
node(0, RedstoneNodeKind::Torch, vec![1]),
node(1, RedstoneNodeKind::Torch, vec![0]),
],
};
assert!(graph.has_cycles());
assert!(!graph.is_combinational());
let sccs = graph.strongly_connected_components();
let big: Vec<_> = sccs.iter().filter(|s| s.len() == 2).collect();
assert_eq!(big.len(), 1, "exactly one SCC of size 2");
assert_eq!(sccs.len(), 1, "two mutually-cyclic nodes form one SCC");
let f = graph.features();
assert_eq!(f.scc_count, 1);
assert_eq!(f.largest_scc, 2);
assert!(f.has_cycles);
}
#[test]
fn test_self_loop() {
let graph = RedstoneGraph {
nodes: vec![node(0, RedstoneNodeKind::Torch, vec![0])],
};
assert!(graph.has_cycles(), "a self-loop is a cycle");
assert!(graph.features().has_cycles);
assert!(!graph.is_combinational());
}
#[test]
fn test_fan_in() {
let graph = RedstoneGraph {
nodes: vec![
node(0, RedstoneNodeKind::Lever, vec![]),
node(1, RedstoneNodeKind::Lever, vec![]),
node(2, RedstoneNodeKind::Lever, vec![]),
node(3, RedstoneNodeKind::Lamp, vec![0, 1, 2]),
],
};
assert_eq!(graph.max_fan_in(), 3);
}
#[test]
fn test_fan_out() {
let graph = RedstoneGraph {
nodes: vec![
node(0, RedstoneNodeKind::Lever, vec![]),
node(1, RedstoneNodeKind::Lamp, vec![0]),
node(2, RedstoneNodeKind::Lamp, vec![0]),
node(3, RedstoneNodeKind::Lamp, vec![0]),
],
};
assert_eq!(graph.max_fan_out(), 3);
}
#[test]
fn test_weakly_connected_components_two_pairs() {
let graph = RedstoneGraph {
nodes: vec![
node(0, RedstoneNodeKind::Lever, vec![]),
node(1, RedstoneNodeKind::Lamp, vec![0]),
node(2, RedstoneNodeKind::Lever, vec![]),
node(3, RedstoneNodeKind::Lamp, vec![2]),
],
};
assert_eq!(graph.weakly_connected_components(), 2);
}
#[test]
fn test_delay_weighted_depth() {
let graph = RedstoneGraph {
nodes: vec![
node(0, RedstoneNodeKind::Lever, vec![]),
node(1, RedstoneNodeKind::Repeater { delay: 4 }, vec![0]),
node(2, RedstoneNodeKind::Lamp, vec![1]),
],
};
assert_eq!(graph.critical_path(), 3, "3 nodes on the chain");
assert_eq!(
graph.delay_weighted_depth(),
6,
"1 + 4 (repeater delay) + 1 along the chain"
);
}
#[test]
fn test_features_json_round_trip() {
let graph = RedstoneGraph {
nodes: vec![
node(0, RedstoneNodeKind::Lever, vec![]),
node(
1,
RedstoneNodeKind::Comparator {
mode: ComparatorMode::Compare,
far_input: None,
},
vec![0],
),
node(2, RedstoneNodeKind::Lamp, vec![1]),
],
};
let f = graph.features();
let json = f.to_json().expect("serialize");
let back = GraphFeatures::from_json(&json).expect("deserialize");
assert_eq!(f, back, "GraphFeatures JSON round-trips");
}
}