#!/usr/bin/env python3 """ The Glass Box League — Configuration Manager CLI Unified CLI for managing all game configurations: - Game settings (lobby options) - Prompt templates - Map data - Model configurations """ import argparse import json import os import sys from pathlib import Path from typing import Optional # Add project root to path PROJECT_ROOT = Path(__file__).parent.parent sys.path.insert(0, str(PROJECT_ROOT)) try: import yaml HAS_YAML = True except ImportError: HAS_YAML = False class ConfigManager: """Unified configuration manager.""" def __init__(self, project_root: Path = None): self.root = project_root or PROJECT_ROOT self.config_dir = self.root / "config" self.data_dir = self.root / "data" self.prompts_dir = self.config_dir / "prompts" # ------------------------------------------------------------------------- # Game Settings # ------------------------------------------------------------------------- def get_settings_path(self, format: str = "json") -> Path: """Get path to game settings file.""" return self.config_dir / f"game_settings.{format}" def load_settings(self) -> dict: """Load game settings (prefers YAML if available).""" yaml_path = self.get_settings_path("yaml") json_path = self.get_settings_path("json") if HAS_YAML and yaml_path.exists(): with open(yaml_path) as f: return yaml.safe_load(f) elif json_path.exists(): with open(json_path) as f: return json.load(f) return {} def save_settings(self, settings: dict, format: str = "json"): """Save game settings.""" path = self.get_settings_path(format) if format == "yaml" and HAS_YAML: with open(path, "w") as f: yaml.dump(settings, f, default_flow_style=False, sort_keys=False) else: with open(path, "w") as f: json.dump(settings, f, indent=2) print(f"✓ Saved settings to {path}") def get_setting(self, key: str) -> Optional[any]: """Get a single setting value (supports nested keys like 'game.num_impostors').""" settings = self.load_settings() # Handle nested keys parts = key.split(".") value = settings for part in parts: if isinstance(value, dict) and part in value: value = value[part] else: return None return value def set_setting(self, key: str, value: str): """Set a single setting value.""" settings = self.load_settings() # Auto-detect type parsed_value = self._parse_value(value) # Handle nested keys parts = key.split(".") current = settings for part in parts[:-1]: if part not in current: current[part] = {} current = current[part] current[parts[-1]] = parsed_value # Save in same format as loaded format = "yaml" if HAS_YAML and self.get_settings_path("yaml").exists() else "json" self.save_settings(settings, format) print(f"✓ Set {key} = {parsed_value}") def _parse_value(self, value: str): """Parse string value to appropriate type.""" # Boolean if value.lower() in ("true", "yes", "on"): return True if value.lower() in ("false", "no", "off"): return False # Number try: if "." in value: return float(value) return int(value) except ValueError: pass return value def list_settings(self): """List all game settings.""" settings = self.load_settings() self._print_settings(settings) def _print_settings(self, obj, prefix=""): """Recursively print settings.""" if isinstance(obj, dict): for key, value in obj.items(): full_key = f"{prefix}.{key}" if prefix else key if isinstance(value, dict): print(f"\n[{full_key}]") self._print_settings(value, full_key) else: print(f" {key}: {value}") else: print(f" {prefix}: {obj}") # ------------------------------------------------------------------------- # Prompt Templates # ------------------------------------------------------------------------- def list_prompts(self): """List all prompt templates.""" if not self.prompts_dir.exists(): print("No prompts directory found.") return print("\nPrompt Templates:") for path in sorted(self.prompts_dir.glob("*.md")): size = path.stat().st_size print(f" {path.stem}: {size} bytes") def show_prompt(self, name: str): """Show a prompt template.""" path = self.prompts_dir / f"{name}.md" if not path.exists(): print(f"Prompt '{name}' not found.") return print(f"=== {name}.md ===\n") print(path.read_text()) def edit_prompt(self, name: str): """Open prompt in default editor.""" path = self.prompts_dir / f"{name}.md" if not path.exists(): # Create empty template path.write_text(f"# {name.title()} Prompt\n\n") editor = os.environ.get("EDITOR", "nano") os.system(f"{editor} {path}") # ------------------------------------------------------------------------- # Maps # ------------------------------------------------------------------------- def list_maps(self): """List available maps.""" maps_dir = self.data_dir / "maps" if not maps_dir.exists(): print("No maps directory found.") return print("\nAvailable Maps:") for path in sorted(maps_dir.glob("*.json")): with open(path) as f: data = json.load(f) rooms = len(data.get("rooms", [])) edges = len(data.get("edges", [])) walls = len(data.get("walls", [])) print(f" {path.stem}: {rooms} rooms, {edges} edges, {walls} walls") def show_map_info(self, name: str): """Show detailed map information.""" path = self.data_dir / "maps" / f"{name}.json" if not path.exists(): print(f"Map '{name}' not found.") return with open(path) as f: data = json.load(f) print(f"\n=== {name} Map ===\n") # Canvas size if "canvas" in data: print(f"Canvas: {data['canvas']['width']}x{data['canvas']['height']} pixels") # Rooms print(f"\nRooms ({len(data.get('rooms', []))}):") for room in data.get("rooms", []): tasks = len(room.get("tasks", [])) vent = "vent" if room.get("vent") else "" print(f" {room['id']}: {room['name']} ({tasks} tasks) {vent}") # Spawn points if data.get("spawn_points"): print(f"\nSpawn Points: {', '.join(data['spawn_points'].keys())}") # ------------------------------------------------------------------------- # Validation # ------------------------------------------------------------------------- def validate(self): """Validate all configurations.""" errors = [] warnings = [] # Check settings settings = self.load_settings() if not settings: errors.append("Game settings file is empty or missing") else: # Validate ranges speed = self._get_nested(settings, "player_speed") or self._get_nested(settings, "player.player_speed") if speed and (speed < 50 or speed > 300): warnings.append(f"player_speed={speed} is unusual (typical: 75-150)") impostors = self._get_nested(settings, "num_impostors") or self._get_nested(settings, "game.num_impostors") if impostors and (impostors < 1 or impostors > 3): warnings.append(f"num_impostors={impostors} is unusual (typical: 1-3)") # Check maps maps_dir = self.data_dir / "maps" if maps_dir.exists(): for path in maps_dir.glob("*.json"): try: with open(path) as f: data = json.load(f) if not data.get("rooms"): errors.append(f"Map {path.name} has no rooms") except json.JSONDecodeError as e: errors.append(f"Map {path.name} is invalid JSON: {e}") # Check prompts if self.prompts_dir.exists(): for path in self.prompts_dir.glob("*.md"): content = path.read_text() if len(content) < 50: warnings.append(f"Prompt {path.name} is very short ({len(content)} chars)") # Report print("\n=== Configuration Validation ===\n") if errors: print("❌ Errors:") for e in errors: print(f" - {e}") if warnings: print("\n⚠️ Warnings:") for w in warnings: print(f" - {w}") if not errors and not warnings: print("✓ All configurations valid!") return len(errors) == 0 def _get_nested(self, d: dict, key: str): """Get nested dict value.""" parts = key.split(".") for part in parts: if isinstance(d, dict) and part in d: d = d[part] else: return None return d # ------------------------------------------------------------------------- # Reset/Defaults # ------------------------------------------------------------------------- def reset_settings(self): """Reset settings to defaults.""" defaults = { "map_name": "skeld", "num_impostors": 2, "player_speed": 100.0, "crewmate_vision": 1.0, "impostor_vision": 1.5, "kill_cooldown": 25.0, "kill_distance": "medium", "emergencies_per_player": 1, "emergency_cooldown": 15.0, "discussion_time": 30.0, "voting_time": 120.0, "confirm_ejects": True, "anonymous_votes": False, "visual_tasks": True, "taskbar_updates": "always", "common_tasks": 2, "long_tasks": 1, "short_tasks": 2, "sabotage_cooldown": 30.0, "reactor_timer": 45.0, "o2_timer": 45.0, "lights_vision_multiplier": 0.25, "max_discussion_rounds": 20, "convergence_threshold": 2 } self.save_settings(defaults) print("✓ Reset all settings to defaults") def main(): parser = argparse.ArgumentParser( description="Glass Box League Configuration Manager", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: %(prog)s settings list # List all game settings %(prog)s settings get kill_cooldown # Get a specific setting %(prog)s settings set kill_cooldown 30 # Set a setting %(prog)s settings reset # Reset to defaults %(prog)s prompts list # List prompt templates %(prog)s prompts show action # Show action prompt %(prog)s prompts edit discussion # Edit discussion prompt %(prog)s maps list # List available maps %(prog)s maps info skeld # Show skeld map info %(prog)s validate # Validate all configs """ ) subparsers = parser.add_subparsers(dest="command", help="Command to run") # Settings commands settings_parser = subparsers.add_parser("settings", help="Manage game settings") settings_sub = settings_parser.add_subparsers(dest="action") settings_sub.add_parser("list", help="List all settings") get_parser = settings_sub.add_parser("get", help="Get a setting value") get_parser.add_argument("key", help="Setting key (e.g., kill_cooldown)") set_parser = settings_sub.add_parser("set", help="Set a setting value") set_parser.add_argument("key", help="Setting key") set_parser.add_argument("value", help="New value") settings_sub.add_parser("reset", help="Reset to defaults") # Prompts commands prompts_parser = subparsers.add_parser("prompts", help="Manage prompt templates") prompts_sub = prompts_parser.add_subparsers(dest="action") prompts_sub.add_parser("list", help="List prompt templates") show_parser = prompts_sub.add_parser("show", help="Show a prompt") show_parser.add_argument("name", help="Prompt name (without .md)") edit_parser = prompts_sub.add_parser("edit", help="Edit a prompt") edit_parser.add_argument("name", help="Prompt name") # Maps commands maps_parser = subparsers.add_parser("maps", help="Manage map data") maps_sub = maps_parser.add_subparsers(dest="action") maps_sub.add_parser("list", help="List maps") info_parser = maps_sub.add_parser("info", help="Show map info") info_parser.add_argument("name", help="Map name") # Validate command subparsers.add_parser("validate", help="Validate all configurations") args = parser.parse_args() manager = ConfigManager() if args.command == "settings": if args.action == "list": manager.list_settings() elif args.action == "get": value = manager.get_setting(args.key) if value is not None: print(f"{args.key} = {value}") else: print(f"Setting '{args.key}' not found") elif args.action == "set": manager.set_setting(args.key, args.value) elif args.action == "reset": manager.reset_settings() else: settings_parser.print_help() elif args.command == "prompts": if args.action == "list": manager.list_prompts() elif args.action == "show": manager.show_prompt(args.name) elif args.action == "edit": manager.edit_prompt(args.name) else: prompts_parser.print_help() elif args.command == "maps": if args.action == "list": manager.list_maps() elif args.action == "info": manager.show_map_info(args.name) else: maps_parser.print_help() elif args.command == "validate": success = manager.validate() sys.exit(0 if success else 1) else: parser.print_help() if __name__ == "__main__": main()