"""smolagents orchestrator for the spec-driven arcade renderer. The agent chooses one of a small catalog of render tools and supplies JSON-like arguments. Tools validate with Pydantic and render deterministic p5.js games. """ from __future__ import annotations import os from pathlib import Path from typing import Any from dotenv import load_dotenv from pydantic import ValidationError from smolagents import InferenceClientModel, LiteLLMModel, ToolCallingAgent, tool from game_engine import GameEngine, GameSpec, render_spec_html load_dotenv(dotenv_path=Path(__file__).with_name(".env")) _engine: GameEngine | None = None def _build_spec(archetype: str, **kwargs: Any) -> GameSpec: data = { "archetype": archetype, "tags": [archetype, "agent-render"], **{key: value for key, value in kwargs.items() if value is not None}, } tags = data.get("tags") or [] if archetype not in tags: tags = [archetype, *tags] data["tags"] = tags[:5] return GameSpec.model_validate(data) def _render(spec: GameSpec) -> str: if _engine is None: return "Error: game engine not initialized." _engine.current_spec = spec _engine.current_html = render_spec_html(spec) return ( f"Rendered {spec.title} with `{spec.archetype}`. " f"Goal: {spec.goal}. Controls: {spec.controls}." ) @tool def render_dodge_game( title: str, theme: str, goal: str, player_name: str = "runner", difficulty: int = 5, player_speed: int = 5, hazard_count: int = 8, hazard_speed: int = 5, collectible_count: int = 4, goal_score: int = 20, twist: str = "falling hazards with glowing pickups", ) -> str: """Render a dodge game from validated design fields. Args: title: Short game title. theme: Visual theme and mood. goal: Playable objective. player_name: Player character noun. difficulty: 1-10 difficulty. player_speed: 1-10 movement responsiveness. hazard_count: Number of hazards, 0-20. hazard_speed: 1-10 hazard speed. collectible_count: Number of collectibles, 0-20. goal_score: Score needed to win, 1-100. twist: Strange or delightful flavor detail. """ try: spec = _build_spec( "dodge", title=title, theme=theme, goal=goal, player_name=player_name, difficulty=difficulty, player_speed=player_speed, hazard_count=hazard_count, hazard_speed=hazard_speed, collectible_count=collectible_count, goal_score=goal_score, controls="Arrow keys or WASD to move", twist=twist, ) except ValidationError as e: return f"Validation failed for dodge spec: {e}" return _render(spec) @tool def render_collector_game( title: str, theme: str, goal: str, player_name: str = "collector", difficulty: int = 5, player_speed: int = 5, hazard_count: int = 5, hazard_speed: int = 4, collectible_count: int = 8, goal_score: int = 30, twist: str = "collectibles pulse like tiny wishes", ) -> str: """Render a collector game from validated design fields. Args: title: Short game title. theme: Visual theme and mood. goal: Playable objective. player_name: Player character noun. difficulty: 1-10 difficulty. player_speed: 1-10 movement responsiveness. hazard_count: Number of enemies/hazards, 0-20. hazard_speed: 1-10 hazard speed. collectible_count: Number of collectibles, 0-20. goal_score: Score needed to win, 1-100. twist: Strange or delightful flavor detail. """ try: spec = _build_spec( "collector", title=title, theme=theme, goal=goal, player_name=player_name, difficulty=difficulty, player_speed=player_speed, hazard_count=hazard_count, hazard_speed=hazard_speed, collectible_count=collectible_count, goal_score=goal_score, controls="Arrow keys or WASD to move", twist=twist, ) except ValidationError as e: return f"Validation failed for collector spec: {e}" return _render(spec) @tool def render_jumper_game( title: str, theme: str, goal: str, player_name: str = "jumper", difficulty: int = 5, player_speed: int = 5, jump_strength: int = 6, hazard_count: int = 3, hazard_speed: int = 4, collectible_count: int = 7, goal_score: int = 20, twist: str = "reachable platforms arranged like a tiny pilgrimage", ) -> str: """Render a platform jumper with validated, reachable platform rules. Args: title: Short game title. theme: Visual theme and mood. goal: Playable objective. player_name: Player character noun. difficulty: 1-10 difficulty. player_speed: 1-10 horizontal movement responsiveness. jump_strength: 1-10 jump height. hazard_count: Number of strange hazards, 0-20. hazard_speed: 1-10 hazard speed. collectible_count: Number of platforms/collectibles, 0-20. goal_score: Score target, 1-100. twist: Strange or delightful flavor detail. """ try: spec = _build_spec( "jumper", title=title, theme=theme, goal=goal, player_name=player_name, difficulty=difficulty, player_speed=player_speed, jump_strength=jump_strength, hazard_count=hazard_count, hazard_speed=hazard_speed, collectible_count=collectible_count, goal_score=goal_score, controls="Left/right to move, Space/W/Up to jump", twist=twist, ) except ValidationError as e: return f"Validation failed for jumper spec: {e}" return _render(spec) @tool def render_snake_game( title: str, theme: str, goal: str, player_name: str = "snake", difficulty: int = 5, player_speed: int = 5, collectible_count: int = 1, goal_score: int = 50, twist: str = "direct-control snake trail that only moves on key press", ) -> str: """Render a direct-control snake/grid game from validated design fields. Args: title: Short game title. theme: Visual theme and mood. goal: Playable objective. player_name: Player character noun. difficulty: 1-10 difficulty. player_speed: 1-10 movement responsiveness. collectible_count: Number of active collectibles, 0-20. goal_score: Score needed to win, 1-100. twist: Strange or delightful flavor detail. """ try: spec = _build_spec( "snake", title=title, theme=theme, goal=goal, player_name=player_name, difficulty=difficulty, player_speed=player_speed, hazard_count=0, hazard_speed=3, collectible_count=collectible_count, goal_score=goal_score, controls="Arrow keys or WASD move one grid cell", twist=twist, direct_control=True, ) except ValidationError as e: return f"Validation failed for snake spec: {e}" return _render(spec) @tool def render_strange_mix_game( title: str, primary_archetype: str, secondary_archetype: str, theme: str, goal: str, player_name: str = "wanderer", difficulty: int = 6, weirdness: int = 9, player_speed: int = 5, jump_strength: int = 6, hazard_count: int = 7, hazard_speed: int = 5, collectible_count: int = 7, goal_score: int = 30, twist: str = "two arcade rules accidentally sharing one dream", ) -> str: """Render a strange but playable blend of two render archetypes. Use when the user asks for weird, strange, surreal, mixed, cursed, whimsical, or surprising gameplay. Args: title: Short game title. primary_archetype: Main render type: dodge, collector, jumper, or snake. secondary_archetype: Second render type to blend in. theme: Visual theme and mood. goal: Playable objective. player_name: Player character noun. difficulty: 1-10 difficulty. weirdness: 1-10 strangeness; use 7-10 for this tool. player_speed: 1-10 movement responsiveness. jump_strength: 1-10 jump height for jumper mixes. hazard_count: Number of hazards, 0-20. hazard_speed: 1-10 hazard speed. collectible_count: Number of collectibles/platforms, 0-20. goal_score: Score needed to win, 1-100. twist: Strange or delightful flavor detail. """ try: spec = _build_spec( primary_archetype, title=title, secondary_archetype=secondary_archetype, weirdness=weirdness, tags=[primary_archetype, secondary_archetype, "strange-mix"], theme=theme, goal=goal, player_name=player_name, difficulty=difficulty, player_speed=player_speed, jump_strength=jump_strength, hazard_count=hazard_count, hazard_speed=hazard_speed, collectible_count=collectible_count, goal_score=goal_score, controls="Arrow keys/WASD; Space jumps in jumper mixes", twist=twist, ) except ValidationError as e: return f"Validation failed for strange mix spec: {e}" return _render(spec) _AGENT_INSTRUCTIONS = """\ You are the Arcade Director for a Gradio game builder. You never write JavaScript. You choose exactly one render tool: - render_dodge_game - render_collector_game - render_jumper_game - render_snake_game - render_strange_mix_game Use these tools as the only way to create playable games. The tool validates and renders the game. If a tool returns a validation error, fix the arguments and call the tool again. Choose the render type from the user's idea: - dodge: avoid falling/projectile hazards - collector: gather items while avoiding enemies - jumper: platforms, jumping, reaching a final platform - snake: grid movement, eating/growing/trails - strange mix: user asks for weird, strange, cursed, surreal, mixed, dreamlike, or surprising; blend two of the four render types Keep responses short. After a tool succeeds, tell the user the title, render tag, controls, goal, and strange mix if used. """ class GameOrchestrator: """Wraps smolagents ToolCallingAgent + deterministic renderer tools.""" def __init__( self, model: str = "Qwen/Qwen2.5-Coder-32B-Instruct", ollama_host: str = "http://127.0.0.1:11434", provider: str | None = None, ) -> None: global _engine provider = (provider or os.getenv("MODEL_PROVIDER", "hf")).lower() self.engine = GameEngine(model=model, ollama_host=ollama_host, provider=provider) _engine = self.engine self.model_id = model self.ollama_host = ollama_host self.provider = provider self._prev_html: str | None = None if provider == "ollama": llm = LiteLLMModel( model_id=f"ollama_chat/{model}", api_base=ollama_host, api_key="ollama", num_ctx=8192, ) else: token = ( os.getenv("HF_TOKEN") or os.getenv("HUGGINGFACEHUB_API_TOKEN") or os.getenv("HUGGING_TOKEN") ) llm = InferenceClientModel( model_id=model, token=token, timeout=120, ) self.agent = ToolCallingAgent( tools=[ render_dodge_game, render_collector_game, render_jumper_game, render_snake_game, render_strange_mix_game, ], model=llm, instructions=_AGENT_INSTRUCTIONS, max_steps=4, verbosity_level=1, ) def chat(self, message: str) -> tuple[str, str | None]: """Process a user message. Returns (reply_text, game_html_or_None).""" try: result = self.agent.run(message, reset=False) except Exception as e: return f"Something went wrong while directing the arcade: {e}", None html_changed = self.engine.current_html != self._prev_html new_html = self.engine.current_html if html_changed else None if html_changed: self._prev_html = self.engine.current_html return str(result), new_html def new_game(self) -> None: """Reset state for a fresh game session.""" self.engine.reset() self._prev_html = None self.agent.memory.reset() def has_game(self) -> bool: return self.engine.current_html is not None