443 lines
15 KiB
Python
Executable File
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()
|