mecha10-cli 0.1.47

Mecha10 CLI tool
Documentation
//! Environment catalog loading and management

use crate::paths;
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnvironmentCatalog {
    pub version: String,
    pub environments: Vec<Environment>,
    #[serde(default)]
    pub tags: HashMap<String, String>,
    #[serde(default)]
    pub difficulty_levels: HashMap<String, String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Environment {
    pub id: String,
    pub path: String,
    pub name: String,
    pub description: String,
    pub tags: Vec<String>,
    pub required_sensors: Vec<String>,
    #[serde(default)]
    pub optional_sensors: Vec<String>,
    pub robot_platforms: Vec<String>,
    #[serde(default)]
    pub task_nodes: Vec<String>,
    pub difficulty: String,
    #[serde(default)]
    pub dimensions: Vec<f32>,
    #[serde(default)]
    pub features: EnvironmentFeatures,
    #[serde(default)]
    pub curriculum_stage: i32,
    #[serde(default)]
    pub estimated_duration_s: i32,
    #[serde(default)]
    pub reward_config: HashMap<String, f32>,
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct EnvironmentFeatures {
    #[serde(default)]
    pub floor: bool,
    #[serde(default)]
    pub walls: bool,
    #[serde(default)]
    pub obstacles: String,
    #[serde(default)]
    pub obstacle_count: i32,
    #[serde(default)]
    pub tables: i32,
    #[serde(default)]
    pub waypoints: i32,
    #[serde(default)]
    pub goal_zones: i32,
    #[serde(default)]
    pub npcs: bool,
    #[serde(default)]
    pub npc_count: i32,
    #[serde(default)]
    pub dynamic: bool,
}

impl EnvironmentCatalog {
    /// Load catalog from default location
    pub fn load_default() -> Result<Self> {
        let catalog_path = Self::default_catalog_path()?;
        Self::load(&catalog_path)
    }

    /// Load catalog from specific path
    pub fn load(path: &Path) -> Result<Self> {
        let content =
            fs::read_to_string(path).with_context(|| format!("Failed to read catalog file: {}", path.display()))?;

        let catalog: EnvironmentCatalog =
            serde_json::from_str(&content).with_context(|| "Failed to parse catalog.json")?;

        Ok(catalog)
    }

    /// Get default catalog path (monorepo location)
    fn default_catalog_path() -> Result<PathBuf> {
        // Try to find monorepo root
        let current_dir = std::env::current_dir()?;

        // Look for packages/simulation/environments/robot-tasks/catalog.json
        let mut search_dir = current_dir.clone();
        loop {
            let catalog_path = search_dir
                .join("packages")
                .join("simulation")
                .join("environments")
                .join("robot-tasks")
                .join("catalog.json");

            if catalog_path.exists() {
                return Ok(catalog_path);
            }

            // Move up one directory
            if let Some(parent) = search_dir.parent() {
                search_dir = parent.to_path_buf();
            } else {
                break;
            }
        }

        // Fallback: relative to current directory
        Ok(PathBuf::from(paths::framework::ROBOT_TASKS_CATALOG))
    }

    /// Find environment by ID
    pub fn find_by_id(&self, id: &str) -> Option<&Environment> {
        self.environments.iter().find(|e| e.id == id)
    }

    /// Get all environments with specific tag
    pub fn filter_by_tag(&self, tag: &str) -> Vec<&Environment> {
        self.environments
            .iter()
            .filter(|e| e.tags.contains(&tag.to_string()))
            .collect()
    }

    /// Get all environments for specific platform
    pub fn filter_by_platform(&self, platform: &str) -> Vec<&Environment> {
        self.environments
            .iter()
            .filter(|e| e.robot_platforms.contains(&platform.to_string()))
            .collect()
    }

    /// Get all environments with specific difficulty
    pub fn filter_by_difficulty(&self, difficulty: &str) -> Vec<&Environment> {
        self.environments
            .iter()
            .filter(|e| e.difficulty == difficulty)
            .collect()
    }

    /// Get environments matching task node
    pub fn filter_by_task_node(&self, task_node: &str) -> Vec<&Environment> {
        self.environments
            .iter()
            .filter(|e| e.task_nodes.contains(&task_node.to_string()))
            .collect()
    }
}

impl Environment {
    /// Check if environment is compatible with robot (has all required sensors)
    pub fn is_compatible_with_sensors(&self, sensors: &[String]) -> bool {
        self.required_sensors.iter().all(|required| sensors.contains(required))
    }

    /// Check if environment is compatible with platform
    pub fn is_compatible_with_platform(&self, platform: &str) -> bool {
        self.robot_platforms.contains(&platform.to_string())
    }

    /// Get absolute path to environment scene
    ///
    /// Resolves the environment path relative to the catalog location.
    /// The catalog is at packages/simulation/environments/robot-tasks/catalog.json
    /// and environment paths are relative to the catalog (e.g., ../basic_arena/basic_arena.tscn)
    pub fn get_absolute_path(&self, base_path: &Path) -> PathBuf {
        base_path
            .join("packages")
            .join("simulation")
            .join("environments")
            .join(&self.path)
    }
}