game-builder-ai / game_agent.py
AnshulRaj's picture
Sync game_agent.py from GitHub project
9509be8 verified
Raw
History Blame Contribute Delete
13.2 kB
"""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