LokeshReddy001 commited on
Commit
e0a096c
·
1 Parent(s): 84417b3
.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
- "Vex", "Nyx", "Zara", "Kael",
84
- "Rook", "Lyra", "Orin", "Sage",
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
- engine.spawn_agent(agent_id=f"agent_{i}", name=name)
 
 
 
 
 
 
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
- engine.spawn_agent(agent_id=f"agent_{i}", name=name)
 
 
 
 
 
 
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
- gs = config.grid_size if config else 20
30
- kill_sc = config.kill_score if config else 150
31
- srv_sc = config.survival_score_per_turn if config else 1
32
- att_dmg = config.attack_damage if config else 35
33
- att_rng = config.attack_range if config else 2
34
-
35
- self.system_prompt = (
36
- "You are a competitive AI agent in a grid-based battle royale game on a "
37
- f"{gs}x{gs} grid. "
38
- "Your goal is to maximize your score and be the last agent standing by eliminating others, collecting loot, and surviving.\n"
39
- "CRITICAL STRATEGIC RULES:\n"
40
- "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"
41
- 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"
42
- "3. ABILITIES: Walk over tiles marked with '[has loot]' to automatically collect them. Once acquired, activate them using activate_ability(ability, args):\n"
43
- f" - 'attack': args={{'x': target_x, 'y': target_y}}. Attack an agent within {att_rng} tiles (Manhattan distance <= {att_rng}) for {att_dmg} damage. If they are shielded, their shield breaks instead. Focus fire to eliminate them!\n"
44
- " - 'dash': args={{'dx': dx, 'dy': dy}}. Teleport up to 3 tiles (dx/dy between -3 and 3) to chase agents, escape, or grab loot.\n"
45
- " - 'shield': args={}. Activate a shield to completely block the next incoming attack.\n"
46
- " - 'heal': args={}. Restore 40 HP when your health is low.\n"
47
- "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."
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=None,
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=None,
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', () => { val.textContent = el.value; });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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',{grid_size:gridSize,max_agents:numAgents,num_chests:numChests,max_turns:maxTurns});
 
 
 
 
 
 
 
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={};