Skip to content

Commit ddca6b7

Browse files
committed
Add GameContext
1 parent 67c9441 commit ddca6b7

7 files changed

Lines changed: 115 additions & 57 deletions

File tree

codeclash/agents/__init__.py

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
from codeclash.agents.abstract import Player
22
from codeclash.agents.dummy import Dummy
33
from codeclash.agents.minisweagent import MiniSWEAgent
4+
from codeclash.agents.utils import GameContext
45
from codeclash.games.abstract import CodeGame
56

67

7-
def get_agent(config: dict, game: CodeGame) -> Player:
8+
def get_agent(config: dict, prompts: dict, game: CodeGame) -> Player:
89
agents = {
910
"dummy": Dummy,
1011
"mini": MiniSWEAgent,
@@ -14,12 +15,17 @@ def get_agent(config: dict, game: CodeGame) -> Player:
1415
environment = game.get_environment(
1516
f"{game.game_id}.{config['name']}"
1617
) # NOTE: MUST be branch_name (defined in agents/abstract.py)
17-
template_vars = {
18-
"game_name": game.name,
19-
"game_id": game.game_id,
20-
"rounds": game.rounds,
21-
"round": 1,
22-
"player_id": config["name"],
23-
"game_description": game.config.get("description", ""),
24-
}
25-
return agents(config, environment, template_vars)
18+
return agents(
19+
config,
20+
environment,
21+
GameContext(
22+
id=game.game_id,
23+
name=game.name,
24+
num_players=len(game.config["players"]),
25+
player_ids=[p["name"] for p in game.config["players"]],
26+
player_id=config["name"],
27+
prompts=prompts,
28+
round=1,
29+
rounds=game.rounds,
30+
),
31+
)

codeclash/agents/abstract.py

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from dotenv import load_dotenv
55
from minisweagent import Environment
66

7+
from codeclash.agents.utils import GameContext
78
from codeclash.constants import DIR_LOGS, GH_ORG
89
from codeclash.utils.environment import assert_zero_exit_code
910
from codeclash.utils.log import get_logger
@@ -16,39 +17,38 @@ def __init__(
1617
self,
1718
config: dict,
1819
environment: Environment,
19-
template_vars: dict,
20+
game_context: GameContext,
2021
):
2122
self.config = config
2223
self.name = config["name"]
2324
self.environment = environment
24-
self.template_vars = template_vars
25+
self.game_context = game_context
26+
self.game_context.render_and_set_prompts()
2527
self.logger = get_logger(
2628
self.name,
27-
log_path=DIR_LOGS / template_vars["game_id"] / f"{self.name}.log",
29+
log_path=DIR_LOGS / game_context.id / f"{self.name}.log",
2830
emoji="👤",
2931
)
3032

3133
@property
3234
def branch_name(self):
3335
"""Get the branch name for the agent's codebase."""
34-
return f"{self.template_vars['game_id']}.{self.name}"
36+
return f"{self.game_context.id}.{self.name}"
3537

3638
def commit(self):
3739
"""Commit changes to the agent's codebase."""
38-
rounds = self.template_vars["rounds"]
40+
r, rounds = self.game_context.round, self.game_context.rounds
3941
for cmd in [
4042
"git add -A",
41-
f"git commit --allow-empty -m 'Round {self.round}/{rounds} Update'",
43+
f"git commit --allow-empty -m 'Round {r}/{rounds} Update'",
4244
]:
4345
assert_zero_exit_code(self.environment.execute(cmd), logger=self.logger)
44-
self.logger.info(
45-
f"Committed changes for {self.name} for round {self.round}/{rounds}"
46-
)
46+
self.logger.info(f"Committed changes for {self.name} for round {r}/{rounds}")
4747

4848
def on_round_update(self, new_round: int):
4949
"""Update the agent's round to match the game round."""
50-
self.round = new_round
51-
self.template_vars["round"] = new_round
50+
self.game_context.round = new_round
51+
self.game_context.render_and_set_prompts()
5252

5353
def push(self):
5454
"""Push codebase to a branch on the game's remote repository."""
@@ -58,7 +58,7 @@ def push(self):
5858

5959
for cmd in [
6060
"git remote remove origin",
61-
f"git remote add origin https://x-access-token:{token}@github.com/{GH_ORG}/{self.template_vars['game_name']}.git",
61+
f"git remote add origin https://x-access-token:{token}@github.com/{GH_ORG}/{self.game_context.name}.git",
6262
f"git push origin {self.branch_name}",
6363
]:
6464
assert_zero_exit_code(self.environment.execute(cmd), logger=self.logger)

codeclash/agents/minisweagent.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
from rich.console import Console
1616

1717
from codeclash.agents.abstract import Player
18-
from codeclash.agents.utils import resolve_api_key
18+
from codeclash.agents.utils import GameContext, resolve_api_key
1919
from codeclash.constants import DIR_LOGS
2020

2121

@@ -30,15 +30,15 @@ def __init__(
3030
model: Model,
3131
env: Environment,
3232
name: str,
33-
template_vars: dict,
33+
game_context: GameContext,
3434
*,
3535
logger: logging.Logger,
3636
config_class: Callable = AgentConfig,
3737
**kwargs,
3838
):
3939
super().__init__(model, env, config_class=config_class, **kwargs)
4040
self.name = name
41-
self.template_vars = template_vars
41+
self.game_context = game_context
4242
self.console = Console()
4343
self.logger = logger
4444

@@ -56,7 +56,7 @@ def render_template(self, template: str, **kwargs) -> str:
5656
| asdict(self.env.config)
5757
| asdict(self.model.config)
5858
| platform.uname()._asdict()
59-
| self.template_vars
59+
| self.game_context.to_dict()
6060
)
6161
return Template(template).render(**kwargs, **cs, **os.environ)
6262

@@ -68,16 +68,18 @@ def run(self) -> tuple[str, str]:
6868
class MiniSWEAgent(Player):
6969
"""Player with agentic code editing capabilities"""
7070

71-
def __init__(self, config: dict, environment: Environment, template_vars: dict):
72-
super().__init__(config, environment=environment, template_vars=template_vars)
71+
def __init__(
72+
self, config: dict, environment: Environment, game_context: GameContext
73+
):
74+
super().__init__(config, environment=environment, game_context=game_context)
7375
self.agent = ClashAgent(
7476
LitellmModel(
7577
model_name=config["model"],
7678
model_kwargs={"api_key": resolve_api_key(config["model"])},
7779
),
7880
self.environment,
7981
self.name,
80-
template_vars,
82+
game_context,
8183
logger=self.logger,
8284
**yaml.safe_load(Path(config["config"]).read_text())["agent"],
8385
)
@@ -96,7 +98,7 @@ def run(self):
9698
save_traj(
9799
self.agent, # type: ignore
98100
DIR_LOGS
99-
/ f"{self.template_vars['game_id']}/{self.name}_r{self.template_vars['round']}.traj.json",
101+
/ f"{self.game_context.id}/{self.name}_r{self.game_context.round}.traj.json",
100102
exit_status=exit_status,
101103
result=result,
102104
)

codeclash/agents/utils.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import os
2+
from dataclasses import asdict, dataclass
23

34
from dotenv import load_dotenv
5+
from jinja2 import Template
46

57
load_dotenv()
68

@@ -10,3 +12,44 @@ def resolve_api_key(model: str) -> str:
1012
return os.getenv("ANTHROPIC_API_KEY")
1113
if "gpt" in model:
1214
return os.getenv("OPENAI_API_KEY")
15+
16+
17+
@dataclass
18+
class GameContext:
19+
"""
20+
A class that gives agent access to a partial view of the game state.
21+
22+
NOTE: Instead of passing `game` directly as a reference to the agent,
23+
we create this interface instead to make the communication of game state
24+
more explicit and controlled. We go with this loose coupling to avoid
25+
making the agent too dependent on the entire game object.
26+
"""
27+
28+
id: str
29+
name: str
30+
player_id: str
31+
prompts: dict
32+
round: int
33+
rounds: int
34+
35+
def render_and_set_prompts(self):
36+
"""Render and set prompts using the current game context."""
37+
context = asdict(self)
38+
del context["prompts"]
39+
for key, template_str in self.prompts.items():
40+
rendered = Template(template_str).render(**context)
41+
setattr(self, key, rendered)
42+
43+
def to_dict(self):
44+
"""Convert the GameContext to a dictionary, including dynamically added attributes."""
45+
result = asdict(self)
46+
declared = set(self.__dataclass_fields__)
47+
for attr in dir(self):
48+
if (
49+
not attr.startswith("_")
50+
and attr not in declared
51+
and not callable(getattr(self, attr))
52+
):
53+
result[attr] = getattr(self, attr)
54+
del result["prompts"]
55+
return result

configs/battlesnake.yaml

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,38 @@ game:
88
players:
99
- agent: mini
1010
name: p1
11-
config: configs/mini/battlesnake.yaml
11+
config: configs/mini/default.yaml
1212
model: claude-sonnet-4-20250514
1313
- agent: dummy
1414
name: p2
15-
prompt: |
16-
You are a software development agent participating in a competitive coding game called BattleSnake. You'll manage and evolve your game-playing bot's code, with the details of the game available within the codebase for you to explore.
15+
prompts:
16+
game_description: |
17+
You are a software developer ({{player_id}}) competing in a coding game called BattleSnake.
18+
Your bot (`main.py`) controls a snake on a grid-based board. Snakes collect food, avoid collisions, and try to outlast their opponents.
19+
- Round: {{round}} / {{rounds}}
20+
- Opponents: {{player_ids}}
21+
- Goal: Win the next round.
1722
18-
You'll play multiple rounds, and after each round, you can refine the bot. Since you won't have memory between rounds, you'll need to keep any useful information inside the codebase itself -- whether that's through notes, comments, or even by creating tools or additional scripts to compare different strategies.
23+
Your task: improve the bot in `main.py`, located in {{working_dir}}, the codebase containing both your bot and supporting assets.
1924
20-
Feel free to innovate: you might leave behind analysis tools, create multiple versions of the bot to test against each other, or find other creative ways to track and improve your performance over time. How you choose to evolve and document is up to you. Good luck!
25+
The details of the game are fully available within this codebase.
26+
- `docs/`: Game documentation
27+
- `logs/`: Past rounds and outcomes
28+
- `trajs/`: History of your edits
29+
- and a lot more. It's up to you to explore and utilize these resources.
30+
31+
⚠️ You won't remember past rounds.
32+
So if you want to carry knowledge forward — leave tools, notes, or strategies in the codebase.
33+
Good documentation means you (and others) can pick up right where you left off.
34+
35+
If you'd hate to repeat a step next round, encode it now — as a script, a note, or a tool.
36+
37+
Improve the bot however you like — experiment, document, iterate. Some ideas:
38+
- Build analysis tools
39+
- Create bot variants to test
40+
- Track strategies across rounds
41+
How you choose to evolve and document is up to you. Good luck!
42+
43+
<important>
44+
When you are finished making changes, issue the following command: `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT`.
45+
</important>
Lines changed: 2 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -16,28 +16,10 @@ agent:
1616
1717
Failure to follow these rules will cause your response to be rejected.
1818
instance_template: |
19-
You are currently writing a bot to play a game called BattleSnake.
20-
- The game is currently on round {{round}} of {{rounds}}.
21-
- The logs and information about past games are in the `logs/` directory.
22-
23-
Your goal is to write a bot that beats the other players.
24-
Your bot must be written and represented entirely by the `main.py` file.
25-
Any other files or directories will not be included.
26-
19+
{{game_description}}
20+
========================================================
2721
You can execute bash commands and edit files to implement the necessary changes.
2822
29-
## Recommended Workflow
30-
31-
This workflows should be done step-by-step so that you can iterate on your changes and Any possible problems.
32-
33-
1. Analyze the codebase by finding and reading relevant files
34-
2. Create a script to reproduce the issue
35-
3. Edit the source code to resolve the issue
36-
4. Verify your fix works by running your script again
37-
5. Test edge cases to ensure your fix is robust
38-
6. Submit your changes and finish your work by issuing the following command: `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT`.
39-
Do not combine it with Any other command. <important>After this command, you cannot continue working on this task.</important>
40-
4123
## Important Rules
4224
4325
1. Every response must contain exactly one action

main.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ def main(config_path: str, cleanup: bool = False, push_agent: bool = False):
1313
config = yaml.safe_load(f)
1414
game: CodeGame = get_game(config)
1515
agents: list[Player] = []
16-
for agent in config["players"]:
17-
agents.append(get_agent(agent, game))
16+
for agent_conf in config["players"]:
17+
agents.append(get_agent(agent_conf, config["prompts"], game))
1818

1919
try:
2020
for _ in range(game.rounds):

0 commit comments

Comments
 (0)