| from fastapi import FastAPI, HTTPException |
| from fastapi.middleware.cors import CORSMiddleware |
| from pydantic import BaseModel |
| from backend.environment import GameConfig |
| from backend.engine import Engine |
| from backend.tools import TOOL_SCHEMAS |
|
|
| app = FastAPI(title="Grid Royale API") |
|
|
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
|
|
| engine: Engine | None = None |
|
|
|
|
| @app.on_event("startup") |
| async def _seed_default_game(): |
| """Pre-create a game so visitors see a live arena instead of a lobby.""" |
| global engine |
| try: |
| cfg = GameConfig(grid_size=20, max_turns=100, max_agents=4, num_chests=15) |
| eng = Engine(config=cfg) |
| eng.generate_grid() |
| eng.scatter_chests() |
| for i, name in enumerate(_default_names(4)): |
| eng.spawn_agent(agent_id=f"agent_{i}", name=name) |
| engine = eng |
| except Exception as exc: |
| import logging |
| logging.getLogger("uvicorn.error").warning(f"Default game seed failed: {exc}") |
|
|
|
|
| class StartGameRequest(BaseModel): |
| grid_size: int = 20 |
| max_turns: int = 50 |
| max_agents: int = 8 |
| num_chests: int = 15 |
| agent_names: list[str] | None = None |
| system_prompt: str | None = None |
| system_prompts: dict[str, str] | None = None |
|
|
|
|
| def _serialize(aid: str) -> dict: |
| if not engine: |
| return {} |
| a = engine.env.agents[aid] |
| return { |
| "id": a.id, |
| "name": a.name, |
| "x": a.pos[0], |
| "y": a.pos[1], |
| "hp": a.hp, |
| "alive": a.alive, |
| "score": a.score, |
| "kills": a.kills, |
| "shielded": a.shielded, |
| "abilities": [ |
| {"name": ab.name, "last_used_turn": ab.last_used_turn, "uses_remaining": ab.uses_remaining} |
| for ab in a.abilities |
| ], |
| "messages": a.messages[-24:], |
| } |
|
|
|
|
| def _game_state() -> dict: |
| if not engine: |
| return {} |
| alive = engine.env.alive_agents() |
| tiles = {} |
| for (x, y), tile in engine.env.grid.items(): |
| tiles[f"{x},{y}"] = { |
| "terrain": tile.terrain, |
| "loot": [ |
| { |
| "type": item.type, |
| "ability_name": item.ability_name, |
| "points": item.points, |
| "rarity": item.rarity, |
| "source": item.source, |
| } |
| for item in tile.loot |
| ] if tile.loot else [], |
| } |
| return { |
| "turn": engine.env.turn, |
| "grid_size": engine.env.config.grid_size, |
| "max_turns": engine.env.config.max_turns, |
| "agents": [_serialize(aid) for aid in engine.env.agents], |
| "tiles": tiles, |
| "game_over": len(alive) <= 1 or engine.env.turn >= engine.env.config.max_turns, |
| "winner": alive[0].name if len(alive) == 1 else None, |
| "turn_log": engine.last_turn_log, |
| "tool_schemas": TOOL_SCHEMAS, |
| } |
|
|
|
|
| BOT_NAMES = [ |
| "Vex", "Nyx", "Kael", "Zara", |
| "Cypher", "Helix", "Apex", "Blaze", |
| ] |
|
|
|
|
| def _default_names(count: int) -> list[str]: |
| return BOT_NAMES[:count] |
|
|
|
|
| @app.post("/api/game/start") |
| async def start_game(req: StartGameRequest): |
| global engine |
| config = GameConfig( |
| grid_size=req.grid_size, |
| max_turns=req.max_turns, |
| max_agents=req.max_agents, |
| num_chests=req.num_chests, |
| ) |
| engine = Engine(config=config) |
| engine.generate_grid() |
| engine.scatter_chests() |
|
|
| names = req.agent_names or _default_names(req.max_agents) |
| for i, name in enumerate(names): |
| if i >= req.max_agents: |
| break |
| aid = f"agent_{i}" |
| prompt = None |
| if req.system_prompts: |
| prompt = req.system_prompts.get(aid) or req.system_prompts.get(name) |
| if not prompt: |
| prompt = req.system_prompt |
| engine.spawn_agent(agent_id=aid, name=name, system_prompt=prompt) |
|
|
| return {"message": f"Game started with {len(names)} agents", "turn": 0} |
|
|
|
|
| @app.post("/api/game/step") |
| async def game_step(): |
| if not engine: |
| raise HTTPException(400, "No game running") |
| await engine.step() |
| return _game_state() |
|
|
|
|
| @app.get("/api/game/state") |
| async def game_state(): |
| if not engine: |
| raise HTTPException(400, "No game running") |
| return _game_state() |
|
|
|
|
| @app.post("/api/game/run") |
| async def run_full_game(req: StartGameRequest): |
| global engine |
| config = GameConfig( |
| grid_size=req.grid_size, |
| max_turns=req.max_turns, |
| max_agents=req.max_agents, |
| num_chests=req.num_chests, |
| ) |
| engine = Engine(config=config) |
| engine.generate_grid() |
| engine.scatter_chests() |
|
|
| names = req.agent_names or _default_names(req.max_agents) |
| for i, name in enumerate(names): |
| if i >= req.max_agents: |
| break |
| aid = f"agent_{i}" |
| prompt = None |
| if req.system_prompts: |
| prompt = req.system_prompts.get(aid) or req.system_prompts.get(name) |
| if not prompt: |
| prompt = req.system_prompt |
| engine.spawn_agent(agent_id=aid, name=name, system_prompt=prompt) |
|
|
| await engine.run_game() |
| return _game_state() |
|
|
|
|
| @app.get("/api/game/default_prompt") |
| def get_default_prompt(grid_size: int = 20): |
| from backend.agent.agent import Agent |
| return {"default_prompt": Agent.get_default_system_prompt(grid_size=grid_size)} |
|
|
|
|
| @app.post("/api/model/warm") |
| async def warm_model(): |
| from backend.agent.llm_client import get_llm_client |
| try: |
| client = get_llm_client() |
| response = await client.chat.completions.create( |
| model="google/gemma-4-26B-A4B-it", |
| messages=[{"role": "user", "content": "ping"}], |
| max_tokens=5 |
| ) |
| if response and response.choices: |
| return { |
| "status": "success", |
| "message": "Model warmed up successfully!", |
| "response": response.choices[0].message.content |
| } |
| return {"status": "warning", "message": "No choices returned from model."} |
| except Exception as e: |
| return {"status": "error", "detail": str(e)} |
|
|