Save work: Update game engine, maps, and add vision/pathing utilities. (Tests failing: vision raycast)
This commit is contained in:
Executable
+442
@@ -0,0 +1,442 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user