無向グラフの生成関数

Aug 26, 2025 2 min

前提

$ rustc -Vv
rustc 1.87.0 (17067e9ac 2025-05-09)
binary: rustc
commit-hash: 17067e9ac6d7ecb70e50f92c1944e545188d2359
commit-date: 2025-05-09
host: x86_64-pc-windows-msvc
release: 1.87.0
LLVM version: 20.1.1

実装

use rand::Rng;
use rand::prelude::IndexedMutRandom;
use rand::prelude::SliceRandom;
use rand::rng;
use std::cmp::Ordering;
use std::collections::BinaryHeap;

/// ノードの役割
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum NodeType {
    Start,
    Goal,
    EventPlaceholder,
    Empty,
}

#[derive(Debug, Clone)]
pub struct Edge {
    pub to: usize,   // 接続先のノードID
    pub weight: u32, // コスト
}

#[derive(Debug, Clone)]
pub struct Node {
    pub id: usize,
    pub node_type: NodeType,
    pub edges: Vec<Edge>,
}

impl Node {
    fn new(id: usize) -> Self {
        Node {
            id,
            node_type: NodeType::Empty,
            edges: Vec::new(),
        }
    }
    fn degree(&self) -> usize {
        self.edges.len()
    }
}

/// グラフの構造と特性を定義するためのパラメータ群。
#[derive(Debug, Clone)]
pub struct GraphParams {
    /// 始点から最も近い終点までの最短経路長(コスト)の最小値を指定します。
    /// 最短経路問題に関連する制約です。グラフ生成後にダイクストラ法を用いて
    /// Startノードから各Goalノードへの最短経路コストを計算し、その最小値がこの値を
    /// 下回らないことを保証します。
    /// グラフの広がりや、タスク達成に必要な最低限の労力を担保します。
    pub min_steps: u32,

    /// グラフを構成する頂点(Vertex)の総数。
    /// グラフの「位数(Order)」とも呼ばれる、グラフの基本的なサイズを定義します。
    /// 多くのグラフアルゴリズムの計算量は、頂点数 `N`(または `|V|`)と辺数 `E`(または `|E|`)の
    /// 多項式で表現されるため、この値は生成パフォーマンスに直接影響します。
    pub max_nodes: usize,

    /// グラフ内の任意の頂点が持つことができる辺(Edge)の最大数。
    /// 頂点の「次数(Degree)」に対する上限 `Δ(G)` を設ける制約です。
    /// この値を小さく設定すると、辺の数が少なくなり「スパースグラフ(疎なグラフ)」が
    /// 生成されやすくなります。特に `max_degree = 2` の場合、生成されるグラフは
    /// パスグラフと閉路グラフの組み合わせに限定されます。
    /// 逆に値を大きくすると、デンスグラフ(密なグラフ)の生成が可能になります。
    pub max_degree: usize,

    /// 「行き止まり」同士を接続して閉路を形成する割合(0.0~1.0)。
    /// グラフの木構造(バックボーン)を生成した後、次数が1の頂点(葉頂点、Leaf)同士を
    /// 新たな辺で接続し、閉路(Cycle)を導入する処理の強度を制御します。
    /// - `0.0` の場合: グラフは木(Tree)となり、閉路は存在せず、辺の数は `N-1` となります。
    /// - `> 0.0` の場合: 辺が追加され、グラフの「環状数(Cyclomatic Number)」、
    ///   すなわち `E - N + 1`(連結グラフの場合)が増加します。
    ///   このパラメータは、グラフの「木らしさ(Treeness)」を調整する役割を持ち、
    ///   値が高いほどメッシュ状の複雑なトポロジーに近づきます。
    pub dead_end_merge_ratio: f64,

    /// グラフの「複雑さ」を制御する抽象的な係数。
    /// 生成されるグラフの構造的特性に影響を与えるためのヒューリスティックな値。
    /// Goalノードの数やEventPlaceholderノードの発生率を内生的に決定するために使用されます。
    /// 値を高くすると、攻略すべき目標地点が増えたり、イベントが発生する場所が増えたりして、探索がより複雑になります。
    pub complexity_factor: f64,
}

#[derive(Debug)]
pub struct Graph {
    pub nodes: Vec<Node>,
}

impl Graph {
    /// パラメータ構造体を受け取って新しいグラフを生成する
    pub fn new(params: &GraphParams) -> Option<Graph> {
        const MAX_ATTEMPTS: usize = 100; // min_stepsを満たすグラフ生成の最大試行回数

        for _ in 0..MAX_ATTEMPTS {
            if let Some(graph) = generate_graph_internal(
                params.max_nodes,
                params.max_degree,
                params.dead_end_merge_ratio,
                params.complexity_factor,
            ) {
                // 生成されたグラフが min_steps を満たすか検証
                let start_node_id = graph
                    .nodes
                    .iter()
                    .find(|n| n.node_type == NodeType::Start)
                    .map(|n| n.id);

                if let Some(start_id) = start_node_id {
                    let distances = dijkstra(&graph, start_id);
                    let min_dist_to_goal = graph
                        .nodes
                        .iter()
                        .filter(|n| n.node_type == NodeType::Goal)
                        .filter_map(|n| distances[n.id])
                        .min()
                        .unwrap_or(u32::MAX);

                    if min_dist_to_goal >= params.min_steps {
                        return Some(graph); // 成功
                    }
                }
            }
        }
        None // 最大試行回数内に条件を満たすグラフを生成できなかった
    }

    /// 2ノード間に双方向のエッジを追加する
    fn add_edge(&mut self, from: usize, to: usize, weight: u32) {
        self.nodes[from].edges.push(Edge { to, weight });
        self.nodes[to].edges.push(Edge { to: from, weight });
    }

    /// グラフの構造をターミナルに分かりやすく表示する
    pub fn display_as_tree(&self) {
        println!("\n--- Graph Structure (Tree View) ---");
        if let Some(start_node) = self.nodes.iter().find(|n| n.node_type == NodeType::Start) {
            let mut visited = vec![false; self.nodes.len()];

            // ルート(Startノード)の表示
            println!("● Node {} (Start)", start_node.id);

            // 再帰的に枝を表示開始
            self.display_branch_recursive(start_node.id, &mut visited, String::new());
        } else {
            println!("No start node found to begin the tree display.");
        }
        println!("-----------------------------------\n");
    }

    /// `display_as_tree`のための再帰的なヘルパー関数
    fn display_branch_recursive(&self, node_id: usize, visited: &mut Vec<bool>, prefix: String) {
        // 現在のノードを訪問済みにする
        visited[node_id] = true;

        // 表示すべき接続先(まだ訪問していないノード)をフィルタリング
        let neighbors_to_visit: Vec<&Edge> = self.nodes[node_id]
            .edges
            .iter()
            .filter(|edge| !visited[edge.to])
            .collect();

        let num_neighbors = neighbors_to_visit.len();

        for (i, edge) in neighbors_to_visit.iter().enumerate() {
            let target_node = &self.nodes[edge.to];

            // 最後の枝かどうかで記号を変える
            let connector = if i == num_neighbors - 1 {
                "└──"
            } else {
                "├──"
            };

            // 次の階層のプレフィックスを準備
            let next_prefix = if i == num_neighbors - 1 {
                prefix.clone() + "    "
            } else {
                prefix.clone() + "|   "
            };

            // 接続先のノード情報を表示
            let symbol = match target_node.node_type {
                NodeType::Start | NodeType::Goal => "●",
                _ => "○",
            };
            let type_str = format!("{:?}", target_node.node_type);
            println!(
                "{}{} [w:{}] {} Node {} ({})",
                prefix, connector, edge.weight, symbol, target_node.id, type_str
            );

            // 再帰呼び出しでさらに深く探索
            self.display_branch_recursive(target_node.id, visited, next_prefix);
        }
    }
}

/// グラフ生成の内部ロジック
fn generate_graph_internal(
    max_nodes: usize,
    max_degree: usize,
    dead_end_merge_ratio: f64,
    complexity_factor: f64,
) -> Option<Graph> {
    if max_nodes < 2 || max_degree < 1 {
        return None;
    }

    let mut rng = rng();
    let mut graph = Graph {
        nodes: (0..max_nodes).map(Node::new).collect(),
    };

    // --- Step 1: complexity_factor からパラメータを内生的に決定 ---
    let calculated_goals = 1.0 + (max_nodes as f64 * complexity_factor * 0.1).floor();
    let num_goals = (calculated_goals as usize).min(max_nodes.saturating_sub(1));
    let event_rate = (0.5 * complexity_factor).min(1.0);

    // --- Step 2: ノードの役割を割り当て ---
    let mut node_indices: Vec<usize> = (0..max_nodes).collect();
    node_indices.shuffle(&mut rng);

    // Startノード
    let start_id = node_indices.pop().unwrap();
    graph.nodes[start_id].node_type = NodeType::Start;

    // Goalノード
    for _ in 0..num_goals {
        if let Some(id) = node_indices.pop() {
            graph.nodes[id].node_type = NodeType::Goal;
        }
    }

    // EventPlaceholderノード
    let num_events = (node_indices.len() as f64 * event_rate).floor() as usize;
    for _ in 0..num_events {
        if let Some(id) = node_indices.pop() {
            graph.nodes[id].node_type = NodeType::EventPlaceholder;
        }
    }

    // --- Step 3: 木構造(バックボーン)を構築 ---
    let mut unvisited_nodes: Vec<usize> = (0..max_nodes).collect();
    unvisited_nodes.shuffle(&mut rng);

    // 最初にランダムなノードを取り出し、訪問済みリスト(木の一部)に追加
    let mut visited_nodes: Vec<usize> = Vec::with_capacity(max_nodes);
    if let Some(first_node) = unvisited_nodes.pop() {
        visited_nodes.push(first_node);
    } else {
        return None; // ノードがない場合は終了
    }

    let mut edges_count = 0;

    // 未訪問のノードがなくなるまでループ
    while let Some(u) = unvisited_nodes.pop() {
        // u を接続するための接続先を、既に訪問済みのノードからランダムに探す
        // max_degree 制約が厳しい場合、一度で接続先が見つからない可能性があるため、
        // ある程度の回数試行する。
        const MAX_CONNECTION_ATTEMPTS: usize = 10; // 接続試行の上限
        let mut connected = false;

        for _ in 0..MAX_CONNECTION_ATTEMPTS {
            // 既に木に属しているノードからランダムにvを選ぶ
            let v = *visited_nodes.choose_mut(&mut rng).unwrap();

            // 次数制約をチェック
            if graph.nodes[u].degree() < max_degree && graph.nodes[v].degree() < max_degree {
                let weight = assign_weight(&mut rng);
                graph.add_edge(u, v, weight);
                edges_count += 1;
                connected = true;
                break; // 接続に成功したので試行ループを抜ける
            }
        }

        if !connected {
            // 何度試行しても接続できなかった場合。
            // よりロバストにするなら、次数に空きがあるノードを全探索するなどの
            // フォールバック処理も考えられるが、多くの場合これでうまくいく。
            // ここでは生成失敗とする。
            return None;
        }

        // uを訪問済みリストに追加
        visited_nodes.push(u);
    }
    if edges_count < max_nodes - 1 {
        return None;
    }

    // --- Step 4: 行き止まりを結合して閉路を作る ---
    if dead_end_merge_ratio > 0.0 {
        let mut dead_ends: Vec<usize> = graph
            .nodes
            .iter()
            .filter(|n| n.degree() == 1)
            .map(|n| n.id)
            .collect();
        dead_ends.shuffle(&mut rng);

        let num_merges = ((dead_ends.len() / 2) as f64 * dead_end_merge_ratio).floor() as usize;

        for i in 0..num_merges {
            let u = dead_ends[2 * i];
            let v = dead_ends[2 * i + 1];

            let is_connected = graph.nodes[u].edges.iter().any(|edge| edge.to == v);
            if !is_connected
                && graph.nodes[u].degree() < max_degree
                && graph.nodes[v].degree() < max_degree
            {
                let weight = assign_weight(&mut rng);
                graph.add_edge(u, v, weight);
            }
        }
    }

    Some(graph)
}

/// 確率に基づいてエッジの重みを割り当て
fn assign_weight<R: Rng + ?Sized>(rng: &mut R) -> u32 {
    match rng.random::<f64>() {
        x if x < 0.70 => 1, // 70%
        x if x < 0.90 => 2, // 20%
        x if x < 0.98 => 3, // 8%
        _ => 4,             // 2%
    }
}

// --- Dijkstra法 ---
#[derive(Copy, Clone, Eq, PartialEq)]
struct State {
    cost: u32,
    position: usize,
}

impl Ord for State {
    fn cmp(&self, other: &Self) -> Ordering {
        other
            .cost
            .cmp(&self.cost)
            .then_with(|| self.position.cmp(&other.position))
    }
}

impl PartialOrd for State {
    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
        Some(self.cmp(other))
    }
}

/// 指定した始点から全ノードへの最短経路コストを計算
fn dijkstra(graph: &Graph, start_node_id: usize) -> Vec<Option<u32>> {
    let mut dist: Vec<Option<u32>> = vec![None; graph.nodes.len()];
    let mut pq = BinaryHeap::new();

    dist[start_node_id] = Some(0);
    pq.push(State {
        cost: 0,
        position: start_node_id,
    });

    while let Some(State { cost, position }) = pq.pop() {
        if let Some(d) = dist[position] {
            if cost > d {
                continue;
            }
        }

        for edge in &graph.nodes[position].edges {
            let next = State {
                cost: cost + edge.weight,
                position: edge.to,
            };
            if dist[next.position].is_none() || next.cost < dist[next.position].unwrap() {
                pq.push(next);
                dist[next.position] = Some(next.cost);
            }
        }
    }
    dist
}

fn main() {
    // パラメータを構造体で定義
    let params = GraphParams {
        min_steps: 10,
        max_nodes: 50,
        max_degree: 4,
        dead_end_merge_ratio: 0.2,
        complexity_factor: 0.5,
    };

    println!("Generating graph with parameters: {:?}", params);

    // 構造体を渡してグラフを生成
    if let Some(graph) = Graph::new(&params) {
        println!(
            "Successfully generated a graph with {} nodes.",
            graph.nodes.len()
        );
        let start_node = graph
            .nodes
            .iter()
            .find(|n| n.node_type == NodeType::Start)
            .unwrap();
        let goal_nodes_count = graph
            .nodes
            .iter()
            .filter(|n| n.node_type == NodeType::Goal)
            .count();
        println!("- Start node: {}", start_node.id);
        println!("- Goal nodes: {}", goal_nodes_count);

        let distances = dijkstra(&graph, start_node.id);
        let min_dist = graph
            .nodes
            .iter()
            .filter(|n| n.node_type == NodeType::Goal)
            .filter_map(|n| distances[n.id])
            .min()
            .unwrap_or(u32::MAX);
        println!("- Minimum steps to a goal: {}", min_dist);

        graph.display_as_tree();
    } else {
        println!(
            "Failed to generate a graph that meets the criteria within the specified attempts."
        );
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::VecDeque;

    fn is_connected(graph: &Graph) -> bool {
        if graph.nodes.is_empty() {
            return true;
        }
        let mut visited = vec![false; graph.nodes.len()];
        let mut queue = VecDeque::new();
        queue.push_back(0);
        visited[0] = true;
        let mut count = 1;
        while let Some(u) = queue.pop_front() {
            for edge in &graph.nodes[u].edges {
                if !visited[edge.to] {
                    visited[edge.to] = true;
                    queue.push_back(edge.to);
                    count += 1;
                }
            }
        }
        count == graph.nodes.len()
    }

    #[test]
    fn test_graph_generation_scenarios() {
        // テストケースをパラメータ構造体のベクターとして定義
        let test_cases = vec![
            (
                "【基本】標準的なパラメータ",
                GraphParams {
                    min_steps: 5,
                    max_nodes: 30,
                    max_degree: 4,
                    dead_end_merge_ratio: 0.3,
                    complexity_factor: 0.5,
                },
            ),
            (
                "【純粋な木】閉路なし",
                GraphParams {
                    min_steps: 0,
                    max_nodes: 25,
                    max_degree: 5,
                    dead_end_merge_ratio: 0.0,
                    complexity_factor: 0.2,
                },
            ),
            (
                "【閉路が多い】行き止まりをすべて結合",
                GraphParams {
                    min_steps: 0,
                    max_nodes: 25,
                    max_degree: 5,
                    dead_end_merge_ratio: 1.0,
                    complexity_factor: 0.4,
                },
            ),
            (
                "【線形/低密度】次数が2に制限される",
                GraphParams {
                    min_steps: 10,
                    max_nodes: 40,
                    max_degree: 2,
                    dead_end_merge_ratio: 0.1,
                    complexity_factor: 0.3,
                },
            ),
            (
                "【高密度】次数上限が高い",
                GraphParams {
                    min_steps: 3,
                    max_nodes: 50,
                    max_degree: 10,
                    dead_end_merge_ratio: 0.5,
                    complexity_factor: 0.6,
                },
            ),
            (
                "【大規模】ノード数が多い",
                GraphParams {
                    min_steps: 10,
                    max_nodes: 100,
                    max_degree: 5,
                    dead_end_merge_ratio: 0.2,
                    complexity_factor: 0.5,
                },
            ),
            (
                "【高難度】min_stepsの要求が厳しい",
                GraphParams {
                    min_steps: 20,
                    max_nodes: 50,
                    max_degree: 3,
                    dead_end_merge_ratio: 0.1,
                    complexity_factor: 0.7,
                },
            ),
        ];

        for (name, params) in test_cases {
            println!("--- Testing Scenario: {} ---", name);

            if let Some(graph) = Graph::new(&params) {
                assert_eq!(
                    graph.nodes.len(),
                    params.max_nodes,
                    "[{}] Node count mismatch",
                    name
                );
                assert!(is_connected(&graph), "[{}] Graph is not connected", name);
                let start_nodes = graph
                    .nodes
                    .iter()
                    .filter(|n| n.node_type == NodeType::Start)
                    .count();
                assert_eq!(
                    start_nodes, 1,
                    "[{}] There should be exactly one start node",
                    name
                );
                let goal_nodes = graph
                    .nodes
                    .iter()
                    .filter(|n| n.node_type == NodeType::Goal)
                    .count();
                assert!(
                    goal_nodes >= 1,
                    "[{}] There should be at least one goal node",
                    name
                );

                for node in &graph.nodes {
                    assert!(
                        node.degree() <= params.max_degree,
                        "[{}] Node {} exceeds max degree (degree: {}, max: {})",
                        name,
                        node.id,
                        node.degree(),
                        params.max_degree
                    );
                }

                let total_degree: usize = graph.nodes.iter().map(|n| n.degree()).sum();
                let num_edges = total_degree / 2;
                if params.dead_end_merge_ratio == 0.0 {
                    assert_eq!(
                        num_edges,
                        params.max_nodes - 1,
                        "[{}] A pure tree must have N-1 edges",
                        name
                    );
                } else {
                    assert!(
                        num_edges >= params.max_nodes - 1,
                        "[{}] With merging, edges should be >= N-1",
                        name
                    );
                }

                let start_id = graph
                    .nodes
                    .iter()
                    .find(|n| n.node_type == NodeType::Start)
                    .unwrap()
                    .id;
                let distances = dijkstra(&graph, start_id);
                let min_dist_to_goal = graph
                    .nodes
                    .iter()
                    .filter(|n| n.node_type == NodeType::Goal)
                    .filter_map(|n| distances[n.id])
                    .min()
                    .unwrap_or(u32::MAX);
                assert!(
                    min_dist_to_goal >= params.min_steps,
                    "[{}] Graph does not meet min_steps requirement. (min_dist: {}, expected: >= {})",
                    name,
                    min_dist_to_goal,
                    params.min_steps
                );
            } else {
                println!(
                    "Warning: [{}] Failed to generate a graph that meets the criteria within the specified attempts.",
                    name
                );
            }
        }
    }

    #[test]
    fn test_generation_failure_on_bad_params() {
        let params_too_few_nodes = GraphParams {
            min_steps: 0,
            max_nodes: 1,
            max_degree: 1,
            dead_end_merge_ratio: 0.0,
            complexity_factor: 0.5,
        };
        assert!(
            Graph::new(&params_too_few_nodes).is_none(),
            "Should fail with too few nodes"
        );

        let params_zero_degree = GraphParams {
            min_steps: 0,
            max_nodes: 10,
            max_degree: 0,
            dead_end_merge_ratio: 0.0,
            complexity_factor: 0.5,
        };
        assert!(
            Graph::new(&params_zero_degree).is_none(),
            "Should fail with max_degree of 0"
        );
    }
}

#[cfg(test)]
mod enhanced_tests {
    use super::*;
    use std::collections::HashMap;
    use std::collections::VecDeque;

    /// グラフの連結性をBFSで確認
    fn is_connected(graph: &Graph) -> bool {
        if graph.nodes.is_empty() {
            return true;
        }
        let mut visited = vec![false; graph.nodes.len()];
        let mut queue = VecDeque::new();
        queue.push_back(0);
        visited[0] = true;
        let mut count = 1;
        while let Some(u) = queue.pop_front() {
            for edge in &graph.nodes[u].edges {
                if !visited[edge.to] {
                    visited[edge.to] = true;
                    queue.push_back(edge.to);
                    count += 1;
                }
            }
        }
        count == graph.nodes.len()
    }

    #[test]
    fn test_bidirectional_edges() {
        let params = GraphParams {
            min_steps: 0,
            max_nodes: 50,
            max_degree: 5,
            dead_end_merge_ratio: 0.5,
            complexity_factor: 0.5,
        };
        let graph = Graph::new(&params).expect("Graph generation failed");

        for node in &graph.nodes {
            for edge in &node.edges {
                let reciprocal = &graph.nodes[edge.to].edges;
                assert!(
                    reciprocal.iter().any(|e| e.to == node.id),
                    "Edge from {} to {} is not bidirectional",
                    node.id,
                    edge.to
                );
            }
        }
    }

    #[test]
    fn test_multiple_goal_shortest_path() {
        let params = GraphParams {
            min_steps: 5,
            max_nodes: 30,
            max_degree: 4,
            dead_end_merge_ratio: 0.2,
            complexity_factor: 0.8,
        };
        let graph = Graph::new(&params).expect("Graph generation failed");
        let start_id = graph
            .nodes
            .iter()
            .find(|n| n.node_type == NodeType::Start)
            .unwrap()
            .id;
        let distances = dijkstra(&graph, start_id);

        let goal_nodes: Vec<&Node> = graph
            .nodes
            .iter()
            .filter(|n| n.node_type == NodeType::Goal)
            .collect();
        assert!(!goal_nodes.is_empty(), "Should have at least one goal node");

        for goal in goal_nodes {
            let dist = distances[goal.id].unwrap();
            assert!(
                dist >= params.min_steps,
                "Goal {} distance {} < min_steps {}",
                goal.id,
                dist,
                params.min_steps
            );
        }
    }

    #[test]
    fn test_goal_distance_statistics() {
        let params = GraphParams {
            min_steps: 5,
            max_nodes: 50,
            max_degree: 4,
            dead_end_merge_ratio: 0.2,
            complexity_factor: 0.5,
        };

        if let Some(graph) = Graph::new(&params) {
            let start_id = graph
                .nodes
                .iter()
                .find(|n| n.node_type == NodeType::Start)
                .unwrap()
                .id;

            let distances = dijkstra(&graph, start_id);

            let goal_distances: Vec<u32> = graph
                .nodes
                .iter()
                .filter(|n| n.node_type == NodeType::Goal)
                .filter_map(|n| distances[n.id])
                .collect();

            assert!(!goal_distances.is_empty(), "No goal nodes found");

            let max_dist = *goal_distances.iter().max().unwrap();
            let avg_dist = goal_distances.iter().sum::<u32>() as f64 / goal_distances.len() as f64;

            println!(
                "Goal nodes distance stats - max: {}, avg: {:.2}, min_steps required: {}",
                max_dist, avg_dist, params.min_steps
            );

            assert!(
                max_dist <= params.max_nodes as u32 * 4,
                "Max distance too large"
            );
            assert!(
                avg_dist >= params.min_steps as f64,
                "Average distance below min_steps"
            );
        } else {
            panic!("Graph generation failed");
        }
    }

    #[test]
    fn test_edge_weight_distribution() {
        let mut counter: HashMap<u32, usize> = HashMap::new();
        let mut rng = rng();
        for _ in 0..10000 {
            let w = assign_weight(&mut rng);
            *counter.entry(w).or_default() += 1;
        }
        // 基本的な範囲確認
        assert!(counter.contains_key(&1), "Weight 1 missing");
        assert!(counter.contains_key(&2), "Weight 2 missing");
        assert!(counter.contains_key(&3), "Weight 3 missing");
        assert!(counter.contains_key(&4), "Weight 4 missing");
        // おおよその分布確認(誤差10%以内)
        let total: f64 = counter.values().sum::<usize>() as f64;
        let p1 = counter[&1] as f64 / total;
        let p2 = counter[&2] as f64 / total;
        let p3 = counter[&3] as f64 / total;
        let p4 = counter[&4] as f64 / total;
        assert!((p1 - 0.7).abs() < 0.1, "Weight 1 probability out of range");
        assert!((p2 - 0.2).abs() < 0.05, "Weight 2 probability out of range");
        assert!(
            (p3 - 0.08).abs() < 0.03,
            "Weight 3 probability out of range"
        );
        assert!(
            (p4 - 0.02).abs() < 0.02,
            "Weight 4 probability out of range"
        );
    }

    #[test]
    fn test_random_generation_consistency() {
        let params = GraphParams {
            min_steps: 5,
            max_nodes: 30,
            max_degree: 4,
            dead_end_merge_ratio: 0.3,
            complexity_factor: 0.5,
        };
        for _ in 0..10 {
            let graph = Graph::new(&params).expect("Graph generation failed");
            assert!(is_connected(&graph));
            for node in &graph.nodes {
                assert!(node.degree() <= params.max_degree);
            }
        }
    }

    #[test]
    fn test_large_scale_graph() {
        let params = GraphParams {
            min_steps: 10,
            max_nodes: 1000,
            max_degree: 5,
            dead_end_merge_ratio: 0.2,
            complexity_factor: 0.5,
        };
        let graph = Graph::new(&params).expect("Failed to generate large graph");
        assert_eq!(graph.nodes.len(), 1000);
        assert!(is_connected(&graph));
    }
}

~Yu Tokunaga