Commit ·
e0a096c
1
Parent(s): 84417b3
updates
Browse files- .gitignore +2 -1
- api.py +24 -4
- backend/agent/agent.py +26 -20
- backend/environment/ability_registry.py +6 -6
- design.md +0 -222
- frontend/static/index.html +248 -2
.gitignore
CHANGED
|
@@ -1,3 +1,4 @@
|
|
| 1 |
*.env
|
| 2 |
*.venv/
|
| 3 |
-
*pycache_*/
|
|
|
|
|
|
| 1 |
*.env
|
| 2 |
*.venv/
|
| 3 |
+
*pycache_*/
|
| 4 |
+
.agents/
|
api.py
CHANGED
|
@@ -23,6 +23,8 @@ class StartGameRequest(BaseModel):
|
|
| 23 |
max_agents: int = 8
|
| 24 |
num_chests: int = 15
|
| 25 |
agent_names: list[str] | None = None
|
|
|
|
|
|
|
| 26 |
|
| 27 |
|
| 28 |
def _serialize(aid: str) -> dict:
|
|
@@ -80,8 +82,8 @@ def _game_state() -> dict:
|
|
| 80 |
|
| 81 |
|
| 82 |
BOT_NAMES = [
|
| 83 |
-
"
|
| 84 |
-
"
|
| 85 |
]
|
| 86 |
|
| 87 |
|
|
@@ -106,7 +108,13 @@ async def start_game(req: StartGameRequest):
|
|
| 106 |
for i, name in enumerate(names):
|
| 107 |
if i >= req.max_agents:
|
| 108 |
break
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
|
| 111 |
return {"message": f"Game started with {len(names)} agents", "turn": 0}
|
| 112 |
|
|
@@ -143,7 +151,19 @@ async def run_full_game(req: StartGameRequest):
|
|
| 143 |
for i, name in enumerate(names):
|
| 144 |
if i >= req.max_agents:
|
| 145 |
break
|
| 146 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 147 |
|
| 148 |
await engine.run_game()
|
| 149 |
return _game_state()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
max_agents: int = 8
|
| 24 |
num_chests: int = 15
|
| 25 |
agent_names: list[str] | None = None
|
| 26 |
+
system_prompt: str | None = None
|
| 27 |
+
system_prompts: dict[str, str] | None = None
|
| 28 |
|
| 29 |
|
| 30 |
def _serialize(aid: str) -> dict:
|
|
|
|
| 82 |
|
| 83 |
|
| 84 |
BOT_NAMES = [
|
| 85 |
+
"Leo", "Sam", "Mia", "Alex",
|
| 86 |
+
"Zoe", "Kai", "Toby", "Zane",
|
| 87 |
]
|
| 88 |
|
| 89 |
|
|
|
|
| 108 |
for i, name in enumerate(names):
|
| 109 |
if i >= req.max_agents:
|
| 110 |
break
|
| 111 |
+
aid = f"agent_{i}"
|
| 112 |
+
prompt = None
|
| 113 |
+
if req.system_prompts:
|
| 114 |
+
prompt = req.system_prompts.get(aid) or req.system_prompts.get(name)
|
| 115 |
+
if not prompt:
|
| 116 |
+
prompt = req.system_prompt
|
| 117 |
+
engine.spawn_agent(agent_id=aid, name=name, system_prompt=prompt)
|
| 118 |
|
| 119 |
return {"message": f"Game started with {len(names)} agents", "turn": 0}
|
| 120 |
|
|
|
|
| 151 |
for i, name in enumerate(names):
|
| 152 |
if i >= req.max_agents:
|
| 153 |
break
|
| 154 |
+
aid = f"agent_{i}"
|
| 155 |
+
prompt = None
|
| 156 |
+
if req.system_prompts:
|
| 157 |
+
prompt = req.system_prompts.get(aid) or req.system_prompts.get(name)
|
| 158 |
+
if not prompt:
|
| 159 |
+
prompt = req.system_prompt
|
| 160 |
+
engine.spawn_agent(agent_id=aid, name=name, system_prompt=prompt)
|
| 161 |
|
| 162 |
await engine.run_game()
|
| 163 |
return _game_state()
|
| 164 |
+
|
| 165 |
+
|
| 166 |
+
@app.get("/api/game/default_prompt")
|
| 167 |
+
def get_default_prompt(grid_size: int = 20):
|
| 168 |
+
from backend.agent.agent import Agent
|
| 169 |
+
return {"default_prompt": Agent.get_default_system_prompt(grid_size=grid_size)}
|
backend/agent/agent.py
CHANGED
|
@@ -26,26 +26,32 @@ class Agent:
|
|
| 26 |
if system_prompt:
|
| 27 |
self.system_prompt = system_prompt
|
| 28 |
else:
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
async def decide(self, messages: list, tools: list[dict]) -> list[ToolCall]:
|
| 51 |
import json
|
|
|
|
| 26 |
if system_prompt:
|
| 27 |
self.system_prompt = system_prompt
|
| 28 |
else:
|
| 29 |
+
self.system_prompt = self.get_default_system_prompt(config)
|
| 30 |
+
|
| 31 |
+
@staticmethod
|
| 32 |
+
def get_default_system_prompt(config: GameConfig | None = None, grid_size: int | None = None) -> str:
|
| 33 |
+
from backend.environment.config import GameConfig
|
| 34 |
+
cfg = config or GameConfig()
|
| 35 |
+
gs = grid_size if grid_size is not None else cfg.grid_size
|
| 36 |
+
kill_sc = cfg.kill_score
|
| 37 |
+
srv_sc = cfg.survival_score_per_turn
|
| 38 |
+
att_dmg = cfg.attack_damage
|
| 39 |
+
att_rng = cfg.attack_range
|
| 40 |
+
|
| 41 |
+
return (
|
| 42 |
+
"You are a competitive AI agent in a grid-based battle royale game on a "
|
| 43 |
+
f"{gs}x{gs} grid. "
|
| 44 |
+
"Your goal is to maximize your score and be the last agent standing by eliminating others, collecting loot, and surviving.\n"
|
| 45 |
+
"CRITICAL STRATEGIC RULES:\n"
|
| 46 |
+
"1. ALWAYS call observe() at the start of your turn to get details on your HP, abilities, cooldowns, nearby tiles with loot, and visible agents.\n"
|
| 47 |
+
f"2. SCORING: Eliminating an agent yields +{kill_sc} points. Surviving only gives +{srv_sc} point(s) per turn. Passive play will lose. Actively hunt other agents, especially those with low HP.\n"
|
| 48 |
+
"3. ABILITIES: Walk over tiles marked with '[has loot]' to automatically collect them. Once acquired, activate them using activate_ability(ability, args):\n"
|
| 49 |
+
f" - 'attack': args={{'x': target_x, 'y': target_y}}. Attack an agent within {att_rng} tiles (Manhattan distance <= {att_rng}) for {att_dmg} damage (3 uses). If they are shielded, their shield breaks instead. Focus fire to eliminate them!\n"
|
| 50 |
+
" - 'dash': args={{'dx': dx, 'dy': dy}}. Teleport up to 3 tiles (dx/dy between -3 and 3) to chase agents, escape, or grab loot (3 uses).\n"
|
| 51 |
+
" - 'shield': args={}. Activate a shield to completely block the next incoming attack (2 uses).\n"
|
| 52 |
+
" - 'heal': args={}. Restore 40 HP when your health is low (2 uses).\n"
|
| 53 |
+
"4. MOVE: Use move(dx, dy) to move 1 step to an adjacent tile (dx/dy in -1, 0, 1) and automatically pick up any loot on that tile."
|
| 54 |
+
)
|
| 55 |
|
| 56 |
async def decide(self, messages: list, tools: list[dict]) -> list[ToolCall]:
|
| 57 |
import json
|
backend/environment/ability_registry.py
CHANGED
|
@@ -100,29 +100,29 @@ ABILITY_REGISTRY: dict[str, AbilityDef] = {
|
|
| 100 |
"attack": AbilityDef(
|
| 101 |
name="attack",
|
| 102 |
cooldown=1,
|
| 103 |
-
max_uses=
|
| 104 |
-
description="Attack an agent within 2 tiles for 35 damage",
|
| 105 |
handler=attack_handler,
|
| 106 |
),
|
| 107 |
"dash": AbilityDef(
|
| 108 |
name="dash",
|
| 109 |
cooldown=2,
|
| 110 |
max_uses=3,
|
| 111 |
-
description="Teleport up to 3 tiles in a direction",
|
| 112 |
handler=dash_handler,
|
| 113 |
),
|
| 114 |
"shield": AbilityDef(
|
| 115 |
name="shield",
|
| 116 |
cooldown=3,
|
| 117 |
-
max_uses=
|
| 118 |
-
description="Become immune to the next attack",
|
| 119 |
handler=shield_handler,
|
| 120 |
),
|
| 121 |
"heal": AbilityDef(
|
| 122 |
name="heal",
|
| 123 |
cooldown=2,
|
| 124 |
max_uses=2,
|
| 125 |
-
description="Restore 40 HP",
|
| 126 |
handler=heal_handler,
|
| 127 |
),
|
| 128 |
}
|
|
|
|
| 100 |
"attack": AbilityDef(
|
| 101 |
name="attack",
|
| 102 |
cooldown=1,
|
| 103 |
+
max_uses=3,
|
| 104 |
+
description="Attack an agent within 2 tiles for 35 damage (3 uses)",
|
| 105 |
handler=attack_handler,
|
| 106 |
),
|
| 107 |
"dash": AbilityDef(
|
| 108 |
name="dash",
|
| 109 |
cooldown=2,
|
| 110 |
max_uses=3,
|
| 111 |
+
description="Teleport up to 3 tiles in a direction (3 uses)",
|
| 112 |
handler=dash_handler,
|
| 113 |
),
|
| 114 |
"shield": AbilityDef(
|
| 115 |
name="shield",
|
| 116 |
cooldown=3,
|
| 117 |
+
max_uses=2,
|
| 118 |
+
description="Become immune to the next attack (2 uses)",
|
| 119 |
handler=shield_handler,
|
| 120 |
),
|
| 121 |
"heal": AbilityDef(
|
| 122 |
name="heal",
|
| 123 |
cooldown=2,
|
| 124 |
max_uses=2,
|
| 125 |
+
description="Restore 40 HP (2 uses)",
|
| 126 |
handler=heal_handler,
|
| 127 |
),
|
| 128 |
}
|
design.md
DELETED
|
@@ -1,222 +0,0 @@
|
|
| 1 |
-
# Grid Royale — Design Doc
|
| 2 |
-
|
| 3 |
-
## Game Mode: Battle Royale (MVP)
|
| 4 |
-
|
| 5 |
-
8 agents drop onto a 20×20 grid. Last agent standing wins. Loot chests
|
| 6 |
-
contain abilities and points. Killing an agent drops loot — pick up
|
| 7 |
-
their leftover abilities and score.
|
| 8 |
-
|
| 9 |
-
---
|
| 10 |
-
|
| 11 |
-
## Core Abstraction
|
| 12 |
-
|
| 13 |
-
### Environment — single source of truth for ALL state
|
| 14 |
-
|
| 15 |
-
```python
|
| 16 |
-
@dataclass
|
| 17 |
-
class Environment:
|
| 18 |
-
grid: dict[tuple[int, int], Tile]
|
| 19 |
-
agents: dict[str, AgentState]
|
| 20 |
-
turn: int
|
| 21 |
-
config: GameConfig
|
| 22 |
-
```
|
| 23 |
-
|
| 24 |
-
### Tile — lightweight, holds terrain + optional loot
|
| 25 |
-
|
| 26 |
-
```python
|
| 27 |
-
@dataclass
|
| 28 |
-
class LootItem:
|
| 29 |
-
type: Literal["ability", "points"]
|
| 30 |
-
ability_name: str | None = None
|
| 31 |
-
points: int | None = None
|
| 32 |
-
rarity: str = "common"
|
| 33 |
-
source: str = "chest" # "chest" or the agent_id who dropped it
|
| 34 |
-
|
| 35 |
-
@dataclass
|
| 36 |
-
class Tile:
|
| 37 |
-
terrain: str
|
| 38 |
-
loot: list[LootItem] | None = None
|
| 39 |
-
```
|
| 40 |
-
|
| 41 |
-
No wrapper classes. No separate `DeathCache` / `Chest`. Just a list of `LootItem` on the tile — whether it came from a chest or a dead agent.
|
| 42 |
-
|
| 43 |
-
### Game Config
|
| 44 |
-
|
| 45 |
-
```python
|
| 46 |
-
@dataclass
|
| 47 |
-
class GameConfig:
|
| 48 |
-
mode: Literal["battle_royale"] = "battle_royale"
|
| 49 |
-
grid_size: int = 20
|
| 50 |
-
max_turns: int = 50
|
| 51 |
-
max_agents: int = 8
|
| 52 |
-
base_hp: int = 100
|
| 53 |
-
attack_damage: int = 25
|
| 54 |
-
attack_range: int = 1
|
| 55 |
-
num_chests: int = 15
|
| 56 |
-
chest_respawn_interval: int = 5
|
| 57 |
-
chest_value_range: tuple = (10, 50)
|
| 58 |
-
kill_score: int = 50
|
| 59 |
-
survival_score_per_turn: int = 5
|
| 60 |
-
win_bonus: int = 100
|
| 61 |
-
```
|
| 62 |
-
|
| 63 |
-
### Agent State
|
| 64 |
-
|
| 65 |
-
```python
|
| 66 |
-
@dataclass
|
| 67 |
-
class AgentState:
|
| 68 |
-
id: str
|
| 69 |
-
name: str
|
| 70 |
-
pos: list[int]
|
| 71 |
-
hp: int
|
| 72 |
-
alive: bool
|
| 73 |
-
score: int
|
| 74 |
-
kills: int
|
| 75 |
-
shielded: bool # passive buff from shield ability
|
| 76 |
-
abilities: list[AbilityInstance] # acquired abilities with runtime state
|
| 77 |
-
messages: list[dict]
|
| 78 |
-
mailbox: list[Message]
|
| 79 |
-
|
| 80 |
-
@dataclass
|
| 81 |
-
class AbilityInstance:
|
| 82 |
-
name: str
|
| 83 |
-
last_used_turn: int = -1 # for cooldown
|
| 84 |
-
uses_remaining: int | None = None # None = infinite
|
| 85 |
-
```
|
| 86 |
-
|
| 87 |
-
### Agent — stateless brain (owns LLM client)
|
| 88 |
-
|
| 89 |
-
```python
|
| 90 |
-
class Agent:
|
| 91 |
-
id: str
|
| 92 |
-
model: str
|
| 93 |
-
provider: str
|
| 94 |
-
client: AsyncOpenAI
|
| 95 |
-
|
| 96 |
-
async def decide(self, messages: list, tools: list) -> list[ToolCall]:
|
| 97 |
-
# one LLM call
|
| 98 |
-
```
|
| 99 |
-
|
| 100 |
-
The brain is ephemeral — owns how the agent thinks (model, provider, client).
|
| 101 |
-
The state of what the agent knows (messages, position, HP, abilities) lives
|
| 102 |
-
in `AgentState` inside `Environment`.
|
| 103 |
-
|
| 104 |
-
### Tools — static schemas, shared by all agents
|
| 105 |
-
|
| 106 |
-
Exactly 4 tools. Never changes. No cache misses.
|
| 107 |
-
|
| 108 |
-
| Tool | Effect |
|
| 109 |
-
|---|---|
|
| 110 |
-
| `move(dx, dy)` | Move to adjacent tile. Auto-loots loot on tile if present. |
|
| 111 |
-
| `observe()` | Return visible tiles + agents within observation radius. |
|
| 112 |
-
| `think(content)` | Internal monologue — appended to conversation, no side effect. |
|
| 113 |
-
| `activate_ability(name, args)` | Dispatch to ability handler. Checks cooldown + remaining uses. |
|
| 114 |
-
|
| 115 |
-
`activate_ability` takes a flexible `args: object` that each handler
|
| 116 |
-
interprets differently. Example abilities:
|
| 117 |
-
|
| 118 |
-
```python
|
| 119 |
-
# attack — needs a position target
|
| 120 |
-
activate_ability(name="attack", args={"x": 5, "y": 7})
|
| 121 |
-
|
| 122 |
-
# shield — self-cast, no args
|
| 123 |
-
activate_ability(name="shield", args={})
|
| 124 |
-
|
| 125 |
-
# swap (hypothetical rare) — needs a target agent ID
|
| 126 |
-
activate_ability(name="swap", args={"target_id": "agent_3"})
|
| 127 |
-
```
|
| 128 |
-
|
| 129 |
-
### Abilities — registered internally, not as separate tools
|
| 130 |
-
|
| 131 |
-
```python
|
| 132 |
-
ABILITY_REGISTRY = {
|
| 133 |
-
"attack": {"cooldown": 1, "uses": None, "handler": attack_handler},
|
| 134 |
-
"dash": {"cooldown": 2, "uses": 3, "handler": dash_handler},
|
| 135 |
-
"shield": {"cooldown": 3, "uses": None, "handler": shield_handler},
|
| 136 |
-
"heal": {"cooldown": 2, "uses": 2, "handler": heal_handler},
|
| 137 |
-
}
|
| 138 |
-
```
|
| 139 |
-
|
| 140 |
-
`activate_ability` checks: does agent have this ability? Can it be used
|
| 141 |
-
this turn (cooldown + remaining uses)? Then dispatches to the handler.
|
| 142 |
-
|
| 143 |
-
### Scoring
|
| 144 |
-
|
| 145 |
-
| Event | Points |
|
| 146 |
-
|---|---|
|
| 147 |
-
| Kill | +50 |
|
| 148 |
-
| Per turn alive | +5 |
|
| 149 |
-
| Loot collected | 10–100 (by rarity) |
|
| 150 |
-
| Win (last alive) | +100 |
|
| 151 |
-
|
| 152 |
-
### Engine — orchestrates the turn loop
|
| 153 |
-
|
| 154 |
-
```python
|
| 155 |
-
class Engine:
|
| 156 |
-
env: Environment
|
| 157 |
-
agents: dict[str, Agent]
|
| 158 |
-
|
| 159 |
-
async def step(self):
|
| 160 |
-
# Phase 1: all agents decide (parallel)
|
| 161 |
-
# Phase 2: execute up to 3 tool calls round-robin
|
| 162 |
-
# Post-turn: check eliminations → drop loot, respawn chests
|
| 163 |
-
self.env.turn += 1
|
| 164 |
-
```
|
| 165 |
-
|
| 166 |
-
Elimination flow:
|
| 167 |
-
1. `attack_handler` reduces HP → if HP ≤ 0, mark `alive=False`
|
| 168 |
-
2. Place `LootItem` entries on their tile (1-2 random abilities + 50% score,
|
| 169 |
-
`source = dead_agent_id`)
|
| 170 |
-
3. Credit killer with +50 score, increment kills
|
| 171 |
-
|
| 172 |
-
Respawn (not in BR — only for hypothetical DM mode later): revived agents
|
| 173 |
-
start at random empty tile with base HP and **no abilities**.
|
| 174 |
-
|
| 175 |
-
---
|
| 176 |
-
|
| 177 |
-
## File Layout
|
| 178 |
-
|
| 179 |
-
```
|
| 180 |
-
grid-royale/
|
| 181 |
-
backend/
|
| 182 |
-
environment/
|
| 183 |
-
__init__.py
|
| 184 |
-
environment.py # Environment dataclass
|
| 185 |
-
tile.py # Tile, LootItem
|
| 186 |
-
config.py # GameConfig
|
| 187 |
-
ability_registry.py # ABILITY_REGISTRY + handlers
|
| 188 |
-
scoring.py # Score logic
|
| 189 |
-
loot_tables.py # Loot generation
|
| 190 |
-
agent/
|
| 191 |
-
__init__.py
|
| 192 |
-
state.py # AgentState, AbilityInstance
|
| 193 |
-
agent.py # Agent brain: decide(messages, tools)
|
| 194 |
-
llm_client.py # Provider configs + factory
|
| 195 |
-
tools/
|
| 196 |
-
__init__.py # ALL_TOOLS, TOOL_SCHEMAS (4 tools)
|
| 197 |
-
move.py
|
| 198 |
-
think.py
|
| 199 |
-
observe.py
|
| 200 |
-
activate_ability.py
|
| 201 |
-
engine.py # Engine (turn loop + eliminations + scoring)
|
| 202 |
-
api.py # FastAPI routes
|
| 203 |
-
main.py # uvicorn entry point
|
| 204 |
-
frontend/
|
| 205 |
-
dashboard.py # Gradio app — grid view, game log, agent cards
|
| 206 |
-
|
| 207 |
-
No `static/index.html`. Gradio serves the UI server-side (Python-native,
|
| 208 |
-
fast for hackathon). Frontend is a thin visualization layer on top of the
|
| 209 |
-
API — reads `env` snapshot and renders it.
|
| 210 |
-
|
| 211 |
-
## Key Design Properties
|
| 212 |
-
|
| 213 |
-
| Concept | Lives where | Mutated by |
|
| 214 |
-
|---|---|---|
|
| 215 |
-
| Grid + terrain | `env.grid` | tools |
|
| 216 |
-
| Loot on tiles | `env.grid[x,y].loot` | engine (spawn), MoveTool (pickup) |
|
| 217 |
-
| Agent HP, score, abilities | `env.agents[aid]` | tools |
|
| 218 |
-
| LLM conversation | `env.agents[aid].messages` | agent.decide(), tools |
|
| 219 |
-
| Tool schemas | `tools/__init__.py` | never (static, 4 tools) |
|
| 220 |
-
| Ability logic | `environment/ability_registry.py` | handlers |
|
| 221 |
-
| Scoring logic | `environment/scoring.py` | engine |
|
| 222 |
-
| Loot tables | `environment/loot_tables.py` | engine |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
frontend/static/index.html
CHANGED
|
@@ -150,6 +150,104 @@ body {
|
|
| 150 |
width: 28px; text-align: right;
|
| 151 |
}
|
| 152 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
.lobby-deploy {
|
| 154 |
width: 100%; margin-top: 24px;
|
| 155 |
padding: 15px; border: none; border-radius: 12px;
|
|
@@ -1007,6 +1105,15 @@ body {
|
|
| 1007 |
<span class="range-val" id="maxTurnsVal">100</span>
|
| 1008 |
</div>
|
| 1009 |
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1010 |
<button class="lobby-deploy" id="startBtn">⚔ Deploy Agents</button>
|
| 1011 |
</div>
|
| 1012 |
</div>
|
|
@@ -1212,12 +1319,136 @@ const AGENT_COLORS = {
|
|
| 1212 |
animate();
|
| 1213 |
})();
|
| 1214 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1215 |
['gridSize','numAgents','numChests','maxTurns'].forEach(id => {
|
| 1216 |
const el = document.getElementById(id);
|
| 1217 |
const val = document.getElementById(id+'Val');
|
| 1218 |
-
el.addEventListener('input', () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1219 |
});
|
| 1220 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1221 |
/* ═══════════ THREE.JS ═══════════ */
|
| 1222 |
const TILE = 1.6;
|
| 1223 |
let app = null;
|
|
@@ -2491,9 +2722,24 @@ async function startGame() {
|
|
| 2491 |
const numAgents=parseInt(document.getElementById('numAgents').value);
|
| 2492 |
const numChests=parseInt(document.getElementById('numChests').value);
|
| 2493 |
const maxTurns=parseInt(document.getElementById('maxTurns').value);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2494 |
document.getElementById('startBtn').disabled=true;
|
| 2495 |
document.getElementById('startBtn').textContent='Deploying...';
|
| 2496 |
-
state = await api('POST','/api/game/start',{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2497 |
state = await api('GET','/api/game/state');
|
| 2498 |
gameStarted=true;
|
| 2499 |
nameMaterialCache={};
|
|
|
|
| 150 |
width: 28px; text-align: right;
|
| 151 |
}
|
| 152 |
|
| 153 |
+
.lobby-prompt-wrap {
|
| 154 |
+
margin-top: 10px;
|
| 155 |
+
margin-bottom: 12px;
|
| 156 |
+
width: 100%;
|
| 157 |
+
}
|
| 158 |
+
#systemPromptInput {
|
| 159 |
+
width: 100%;
|
| 160 |
+
height: 120px;
|
| 161 |
+
background: var(--surface);
|
| 162 |
+
border: 1px solid var(--border);
|
| 163 |
+
border-radius: 8px;
|
| 164 |
+
color: var(--text);
|
| 165 |
+
font-family: 'JetBrains Mono', monospace;
|
| 166 |
+
font-size: 10px;
|
| 167 |
+
line-height: 1.4;
|
| 168 |
+
padding: 8px 10px;
|
| 169 |
+
resize: vertical;
|
| 170 |
+
outline: none;
|
| 171 |
+
transition: border-color 0.2s, box-shadow 0.2s;
|
| 172 |
+
}
|
| 173 |
+
#systemPromptInput:focus {
|
| 174 |
+
border-color: var(--active-tab-color, var(--accent));
|
| 175 |
+
box-shadow: 0 0 10px var(--active-tab-glow, var(--accent-glow));
|
| 176 |
+
}
|
| 177 |
+
|
| 178 |
+
.lobby-prompt-tabs {
|
| 179 |
+
display: flex;
|
| 180 |
+
gap: 8px;
|
| 181 |
+
margin-top: 10px;
|
| 182 |
+
margin-bottom: 2px;
|
| 183 |
+
flex-wrap: wrap;
|
| 184 |
+
width: 100%;
|
| 185 |
+
}
|
| 186 |
+
.lobby-tab {
|
| 187 |
+
padding: 6px 12px;
|
| 188 |
+
border-radius: 6px;
|
| 189 |
+
background: var(--surface2);
|
| 190 |
+
border: 1px solid var(--border);
|
| 191 |
+
color: var(--text-dim);
|
| 192 |
+
font-family: 'Space Grotesk', sans-serif;
|
| 193 |
+
font-size: 11px;
|
| 194 |
+
font-weight: 600;
|
| 195 |
+
cursor: pointer;
|
| 196 |
+
transition: all 0.2s ease;
|
| 197 |
+
user-select: none;
|
| 198 |
+
}
|
| 199 |
+
.lobby-tab:hover {
|
| 200 |
+
border-color: var(--text-muted);
|
| 201 |
+
color: var(--text);
|
| 202 |
+
}
|
| 203 |
+
.lobby-tab.active {
|
| 204 |
+
background: var(--surface);
|
| 205 |
+
border-color: var(--tab-color, var(--accent));
|
| 206 |
+
color: var(--tab-color, var(--accent));
|
| 207 |
+
box-shadow: 0 0 8px var(--tab-glow, var(--accent-glow));
|
| 208 |
+
}
|
| 209 |
+
|
| 210 |
+
.lobby-agent-edit-row {
|
| 211 |
+
display: flex;
|
| 212 |
+
align-items: center;
|
| 213 |
+
gap: 10px;
|
| 214 |
+
margin-top: 10px;
|
| 215 |
+
margin-bottom: 8px;
|
| 216 |
+
width: 100%;
|
| 217 |
+
background: var(--surface2);
|
| 218 |
+
border: 1px solid var(--border);
|
| 219 |
+
border-radius: 8px;
|
| 220 |
+
padding: 8px 12px;
|
| 221 |
+
transition: border-color 0.2s, box-shadow 0.2s;
|
| 222 |
+
}
|
| 223 |
+
.lobby-agent-edit-row:focus-within {
|
| 224 |
+
border-color: var(--active-tab-color, var(--accent));
|
| 225 |
+
box-shadow: 0 0 10px var(--active-tab-glow, var(--accent-glow));
|
| 226 |
+
}
|
| 227 |
+
.lobby-agent-color-dot {
|
| 228 |
+
width: 10px;
|
| 229 |
+
height: 10px;
|
| 230 |
+
border-radius: 50%;
|
| 231 |
+
background: var(--accent);
|
| 232 |
+
box-shadow: 0 0 8px var(--accent-glow);
|
| 233 |
+
flex-shrink: 0;
|
| 234 |
+
transition: all 0.2s ease;
|
| 235 |
+
}
|
| 236 |
+
.lobby-agent-name-input {
|
| 237 |
+
flex: 1;
|
| 238 |
+
background: transparent;
|
| 239 |
+
border: none;
|
| 240 |
+
outline: none;
|
| 241 |
+
color: var(--text);
|
| 242 |
+
font-family: 'Space Grotesk', sans-serif;
|
| 243 |
+
font-size: 13px;
|
| 244 |
+
font-weight: 600;
|
| 245 |
+
padding: 0;
|
| 246 |
+
}
|
| 247 |
+
.lobby-agent-name-input::placeholder {
|
| 248 |
+
color: var(--text-dim);
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
.lobby-deploy {
|
| 252 |
width: 100%; margin-top: 24px;
|
| 253 |
padding: 15px; border: none; border-radius: 12px;
|
|
|
|
| 1105 |
<span class="range-val" id="maxTurnsVal">100</span>
|
| 1106 |
</div>
|
| 1107 |
</div>
|
| 1108 |
+
<div class="lobby-section-label" style="margin-top: 16px;">Agent Customization</div>
|
| 1109 |
+
<div class="lobby-prompt-tabs" id="agentPromptTabs"></div>
|
| 1110 |
+
<div class="lobby-agent-edit-row">
|
| 1111 |
+
<span class="lobby-agent-color-dot" id="lobbyAgentColorDot"></span>
|
| 1112 |
+
<input type="text" id="lobbyAgentNameInput" class="lobby-agent-name-input" placeholder="Agent Name">
|
| 1113 |
+
</div>
|
| 1114 |
+
<div class="lobby-prompt-wrap">
|
| 1115 |
+
<textarea id="systemPromptInput" placeholder="Configure the agent system prompt..."></textarea>
|
| 1116 |
+
</div>
|
| 1117 |
<button class="lobby-deploy" id="startBtn">⚔ Deploy Agents</button>
|
| 1118 |
</div>
|
| 1119 |
</div>
|
|
|
|
| 1319 |
animate();
|
| 1320 |
})();
|
| 1321 |
|
| 1322 |
+
const BOT_NAMES = ["Leo", "Sam", "Mia", "Alex", "Zoe", "Kai", "Toby", "Zane"];
|
| 1323 |
+
let agentNames = [...BOT_NAMES];
|
| 1324 |
+
let agentSystemPrompts = {};
|
| 1325 |
+
let activePromptAgent = 'agent_0';
|
| 1326 |
+
let promptManuallyEdited = {};
|
| 1327 |
+
|
| 1328 |
+
function saveCurrentPrompt() {
|
| 1329 |
+
const promptInput = document.getElementById('systemPromptInput');
|
| 1330 |
+
if (promptInput && activePromptAgent) {
|
| 1331 |
+
agentSystemPrompts[activePromptAgent] = promptInput.value;
|
| 1332 |
+
}
|
| 1333 |
+
}
|
| 1334 |
+
|
| 1335 |
+
function loadCurrentPrompt() {
|
| 1336 |
+
const promptInput = document.getElementById('systemPromptInput');
|
| 1337 |
+
if (promptInput && activePromptAgent) {
|
| 1338 |
+
promptInput.value = agentSystemPrompts[activePromptAgent] || '';
|
| 1339 |
+
}
|
| 1340 |
+
}
|
| 1341 |
+
|
| 1342 |
+
function renderPromptTabs() {
|
| 1343 |
+
const numAgents = parseInt(document.getElementById('numAgents').value);
|
| 1344 |
+
const tabsContainer = document.getElementById('agentPromptTabs');
|
| 1345 |
+
if (!tabsContainer) return;
|
| 1346 |
+
|
| 1347 |
+
tabsContainer.innerHTML = '';
|
| 1348 |
+
|
| 1349 |
+
const activeIdx = parseInt(activePromptAgent.split('_')[1]);
|
| 1350 |
+
if (activeIdx >= numAgents) {
|
| 1351 |
+
activePromptAgent = 'agent_0';
|
| 1352 |
+
}
|
| 1353 |
+
|
| 1354 |
+
for (let i = 0; i < numAgents; i++) {
|
| 1355 |
+
const aid = `agent_${i}`;
|
| 1356 |
+
const name = agentNames[i] || `Agent ${i}`;
|
| 1357 |
+
const color = AGENT_COLORS[aid] || '#e8e6e3';
|
| 1358 |
+
|
| 1359 |
+
const tab = document.createElement('div');
|
| 1360 |
+
tab.className = 'lobby-tab';
|
| 1361 |
+
if (aid === activePromptAgent) {
|
| 1362 |
+
tab.classList.add('active');
|
| 1363 |
+
tab.style.setProperty('--tab-color', color);
|
| 1364 |
+
tab.style.setProperty('--tab-glow', `${color}4D`);
|
| 1365 |
+
|
| 1366 |
+
const promptInput = document.getElementById('systemPromptInput');
|
| 1367 |
+
if (promptInput) {
|
| 1368 |
+
promptInput.style.setProperty('--active-tab-color', color);
|
| 1369 |
+
promptInput.style.setProperty('--active-tab-glow', `${color}66`);
|
| 1370 |
+
}
|
| 1371 |
+
|
| 1372 |
+
const colorDot = document.getElementById('lobbyAgentColorDot');
|
| 1373 |
+
if (colorDot) {
|
| 1374 |
+
colorDot.style.background = color;
|
| 1375 |
+
colorDot.style.boxShadow = `0 0 10px ${color}`;
|
| 1376 |
+
}
|
| 1377 |
+
|
| 1378 |
+
const nameInput = document.getElementById('lobbyAgentNameInput');
|
| 1379 |
+
if (nameInput) {
|
| 1380 |
+
nameInput.value = name;
|
| 1381 |
+
}
|
| 1382 |
+
}
|
| 1383 |
+
tab.textContent = name;
|
| 1384 |
+
tab.addEventListener('click', () => {
|
| 1385 |
+
saveCurrentPrompt();
|
| 1386 |
+
activePromptAgent = aid;
|
| 1387 |
+
renderPromptTabs();
|
| 1388 |
+
loadCurrentPrompt();
|
| 1389 |
+
});
|
| 1390 |
+
tabsContainer.appendChild(tab);
|
| 1391 |
+
}
|
| 1392 |
+
}
|
| 1393 |
+
|
| 1394 |
+
async function updateAllPrompts() {
|
| 1395 |
+
const gridSize = document.getElementById('gridSize').value;
|
| 1396 |
+
try {
|
| 1397 |
+
const data = await api('GET', `/api/game/default_prompt?grid_size=${gridSize}`);
|
| 1398 |
+
if (data && data.default_prompt) {
|
| 1399 |
+
const maxAgents = 8;
|
| 1400 |
+
for (let i = 0; i < maxAgents; i++) {
|
| 1401 |
+
const aid = `agent_${i}`;
|
| 1402 |
+
if (!promptManuallyEdited[aid]) {
|
| 1403 |
+
agentSystemPrompts[aid] = data.default_prompt;
|
| 1404 |
+
}
|
| 1405 |
+
}
|
| 1406 |
+
loadCurrentPrompt();
|
| 1407 |
+
}
|
| 1408 |
+
} catch (err) {
|
| 1409 |
+
console.error("Failed to fetch default prompts", err);
|
| 1410 |
+
}
|
| 1411 |
+
}
|
| 1412 |
+
|
| 1413 |
['gridSize','numAgents','numChests','maxTurns'].forEach(id => {
|
| 1414 |
const el = document.getElementById(id);
|
| 1415 |
const val = document.getElementById(id+'Val');
|
| 1416 |
+
el.addEventListener('input', () => {
|
| 1417 |
+
val.textContent = el.value;
|
| 1418 |
+
if (id === 'gridSize') {
|
| 1419 |
+
updateAllPrompts();
|
| 1420 |
+
} else if (id === 'numAgents') {
|
| 1421 |
+
renderPromptTabs();
|
| 1422 |
+
}
|
| 1423 |
+
});
|
| 1424 |
+
});
|
| 1425 |
+
|
| 1426 |
+
document.getElementById('lobbyAgentNameInput').addEventListener('input', (e) => {
|
| 1427 |
+
if (activePromptAgent) {
|
| 1428 |
+
const activeIdx = parseInt(activePromptAgent.split('_')[1]);
|
| 1429 |
+
const newName = e.target.value.trim() || `Agent ${activeIdx}`;
|
| 1430 |
+
agentNames[activeIdx] = newName;
|
| 1431 |
+
|
| 1432 |
+
const activeTab = document.querySelector('#agentPromptTabs .lobby-tab.active');
|
| 1433 |
+
if (activeTab) {
|
| 1434 |
+
activeTab.textContent = newName;
|
| 1435 |
+
}
|
| 1436 |
+
}
|
| 1437 |
+
});
|
| 1438 |
+
|
| 1439 |
+
document.getElementById('systemPromptInput').addEventListener('input', () => {
|
| 1440 |
+
if (activePromptAgent) {
|
| 1441 |
+
promptManuallyEdited[activePromptAgent] = true;
|
| 1442 |
+
saveCurrentPrompt();
|
| 1443 |
+
}
|
| 1444 |
});
|
| 1445 |
|
| 1446 |
+
// Initialize on load
|
| 1447 |
+
(async () => {
|
| 1448 |
+
renderPromptTabs();
|
| 1449 |
+
await updateAllPrompts();
|
| 1450 |
+
})();
|
| 1451 |
+
|
| 1452 |
/* ═══════════ THREE.JS ═══════════ */
|
| 1453 |
const TILE = 1.6;
|
| 1454 |
let app = null;
|
|
|
|
| 2722 |
const numAgents=parseInt(document.getElementById('numAgents').value);
|
| 2723 |
const numChests=parseInt(document.getElementById('numChests').value);
|
| 2724 |
const maxTurns=parseInt(document.getElementById('maxTurns').value);
|
| 2725 |
+
|
| 2726 |
+
saveCurrentPrompt();
|
| 2727 |
+
const systemPrompts = {};
|
| 2728 |
+
for (let i = 0; i < numAgents; i++) {
|
| 2729 |
+
const aid = `agent_${i}`;
|
| 2730 |
+
systemPrompts[aid] = agentSystemPrompts[aid] || '';
|
| 2731 |
+
}
|
| 2732 |
+
|
| 2733 |
document.getElementById('startBtn').disabled=true;
|
| 2734 |
document.getElementById('startBtn').textContent='Deploying...';
|
| 2735 |
+
state = await api('POST','/api/game/start',{
|
| 2736 |
+
grid_size:gridSize,
|
| 2737 |
+
max_agents:numAgents,
|
| 2738 |
+
num_chests:numChests,
|
| 2739 |
+
max_turns:maxTurns,
|
| 2740 |
+
agent_names:agentNames.slice(0, numAgents),
|
| 2741 |
+
system_prompts:systemPrompts
|
| 2742 |
+
});
|
| 2743 |
state = await api('GET','/api/game/state');
|
| 2744 |
gameStarted=true;
|
| 2745 |
nameMaterialCache={};
|