amogus/scripts/config_manager.py

443 lines
15 KiB
Python
Executable File

#!/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()