Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 16 additions & 10 deletions codeclash/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
from codeclash.agents.abstract import Player
from codeclash.agents.dummy import Dummy
from codeclash.agents.minisweagent import MiniSWEAgent
from codeclash.agents.utils import GameContext
from codeclash.constants import DIR_WORK
from codeclash.games.abstract import CodeGame


def get_agent(config: dict, game: CodeGame) -> Player:
def get_agent(config: dict, prompts: dict, game: CodeGame) -> Player:
agents = {
"dummy": Dummy,
"mini": MiniSWEAgent,
Expand All @@ -14,12 +16,16 @@ def get_agent(config: dict, game: CodeGame) -> Player:
environment = game.get_environment(
f"{game.game_id}.{config['name']}"
) # NOTE: MUST be branch_name (defined in agents/abstract.py)
template_vars = {
"game_name": game.name,
"game_id": game.game_id,
"rounds": game.rounds,
"round": 1,
"player_id": config["name"],
"game_description": game.config.get("description", ""),
}
return agents(config, environment, template_vars)
return agents(
config,
environment,
GameContext(
id=game.game_id,
name=game.name,
player_id=config["name"],
prompts=prompts,
round=1,
rounds=game.rounds,
working_dir=str(DIR_WORK),
),
)
24 changes: 12 additions & 12 deletions codeclash/agents/abstract.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from dotenv import load_dotenv
from minisweagent import Environment

from codeclash.agents.utils import GameContext
from codeclash.constants import DIR_LOGS, GH_ORG
from codeclash.utils.environment import assert_zero_exit_code
from codeclash.utils.log import get_logger
Expand All @@ -16,39 +17,38 @@ def __init__(
self,
config: dict,
environment: Environment,
template_vars: dict,
game_context: GameContext,
):
self.config = config
self.name = config["name"]
self.environment = environment
self.template_vars = template_vars
self.game_context = game_context
self.game_context.render_and_set_prompts()
self.logger = get_logger(
self.name,
log_path=DIR_LOGS / template_vars["game_id"] / f"{self.name}.log",
log_path=DIR_LOGS / game_context.id / f"{self.name}.log",
emoji="👤",
)

@property
def branch_name(self):
"""Get the branch name for the agent's codebase."""
return f"{self.template_vars['game_id']}.{self.name}"
return f"{self.game_context.id}.{self.name}"

def commit(self):
"""Commit changes to the agent's codebase."""
rounds = self.template_vars["rounds"]
r, rounds = self.game_context.round, self.game_context.rounds
for cmd in [
"git add -A",
f"git commit --allow-empty -m 'Round {self.round}/{rounds} Update'",
f"git commit --allow-empty -m 'Round {r}/{rounds} Update'",
]:
assert_zero_exit_code(self.environment.execute(cmd), logger=self.logger)
self.logger.info(
f"Committed changes for {self.name} for round {self.round}/{rounds}"
)
self.logger.info(f"Committed changes for {self.name} for round {r}/{rounds}")

def on_round_update(self, new_round: int):
"""Update the agent's round to match the game round."""
self.round = new_round
self.template_vars["round"] = new_round
self.game_context.round = new_round
self.game_context.render_and_set_prompts()

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

for cmd in [
"git remote remove origin",
f"git remote add origin https://x-access-token:{token}@github.com/{GH_ORG}/{self.template_vars['game_name']}.git",
f"git remote add origin https://x-access-token:{token}@github.com/{GH_ORG}/{self.game_context.name}.git",
f"git push origin {self.branch_name}",
]:
assert_zero_exit_code(self.environment.execute(cmd), logger=self.logger)
Expand Down
18 changes: 10 additions & 8 deletions codeclash/agents/minisweagent.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
from rich.console import Console

from codeclash.agents.abstract import Player
from codeclash.agents.utils import resolve_api_key
from codeclash.agents.utils import GameContext, resolve_api_key
from codeclash.constants import DIR_LOGS


Expand All @@ -30,15 +30,15 @@ def __init__(
model: Model,
env: Environment,
name: str,
template_vars: dict,
game_context: GameContext,
*,
logger: logging.Logger,
config_class: Callable = AgentConfig,
**kwargs,
):
super().__init__(model, env, config_class=config_class, **kwargs)
self.name = name
self.template_vars = template_vars
self.game_context = game_context
self.console = Console()
self.logger = logger

Expand All @@ -56,7 +56,7 @@ def render_template(self, template: str, **kwargs) -> str:
| asdict(self.env.config)
| asdict(self.model.config)
| platform.uname()._asdict()
| self.template_vars
| self.game_context.to_dict()
)
return Template(template).render(**kwargs, **cs, **os.environ)

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

def __init__(self, config: dict, environment: Environment, template_vars: dict):
super().__init__(config, environment=environment, template_vars=template_vars)
def __init__(
self, config: dict, environment: Environment, game_context: GameContext
):
super().__init__(config, environment=environment, game_context=game_context)
self.agent = ClashAgent(
LitellmModel(
model_name=config["model"],
model_kwargs={"api_key": resolve_api_key(config["model"])},
),
self.environment,
self.name,
template_vars,
game_context,
logger=self.logger,
**yaml.safe_load(Path(config["config"]).read_text())["agent"],
)
Expand All @@ -96,7 +98,7 @@ def run(self):
save_traj(
self.agent, # type: ignore
DIR_LOGS
/ f"{self.template_vars['game_id']}/{self.name}_r{self.template_vars['round']}.traj.json",
/ f"{self.game_context.id}/{self.name}_r{self.game_context.round}.traj.json",
exit_status=exit_status,
result=result,
)
Expand Down
44 changes: 44 additions & 0 deletions codeclash/agents/utils.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import os
from dataclasses import asdict, dataclass

from dotenv import load_dotenv
from jinja2 import Template

load_dotenv()

Expand All @@ -10,3 +12,45 @@ def resolve_api_key(model: str) -> str:
return os.getenv("ANTHROPIC_API_KEY")
if "gpt" in model:
return os.getenv("OPENAI_API_KEY")


@dataclass
class GameContext:
"""
A class that gives agent access to a partial view of the game state.

NOTE: Instead of passing `game` directly as a reference to the agent,
we create this interface instead to make the communication of game state
more explicit and controlled. We go with this loose coupling to avoid
making the agent too dependent on the entire game object.
"""

id: str
name: str
player_id: str
prompts: dict
round: int
rounds: int
working_dir: str

def render_and_set_prompts(self):

@klieret klieret Aug 14, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I see, this is because we need to render variables within the game description?

But wouldn't the clean way then simply be to have a render_prompts method that gets called when we use this object in the agent?

Why do we assign it with setattr and then later delete attrs in to_dict etc.? That feels pretty unnatural and confusing. Normally, we just want to track state in the object (without any duplication), and then either use properties or methods to transform it (but maybe I'm missing something)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah ok yeah this could be cleaner. But basically

  • we define a set of prompts in CodeClash configs
  • and then we define a set of prompts in Mini configs

The Mini config will reference ({{...}}) a prompt defined in the CodeClash config. But then the CodeClash config will reference the game state, and it needs to be updated as the game proceeds (e.g. round).

So then if i do Template(Mini template).render(CodeClash template), it won't populate the CodeClash template correctly with the metadata.

So this extra function is essentially to populate the CodeClash template first. Basically, this was the approach I came up to handle render a template, that is then rendered in another template.

There could be a more elegant way to do this. I was going for the simplest, most flexible solution. I think it's ok, but open to changing this if there's something obviously easier?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally happy to just merge this and if I can come up with something more elegant, I can just create a PR or something? I was thinking about this yesterday a little bit and I think there might be slightly more elegant ways to do this, but I need to experiment a bit (it would be equivalent)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sg sg yeah like we discussed in meeting - I'll resolve ur comments right now 🙏🏼 and then just merge, so we can just move on for now.

"""Render and set prompts using the current game context."""
context = asdict(self)
del context["prompts"]
for key, template_str in self.prompts.items():
rendered = Template(template_str).render(**context)
setattr(self, key, rendered)

def to_dict(self):
"""Convert the GameContext to a dictionary, including dynamically added attributes."""
result = asdict(self)
declared = set(self.__dataclass_fields__)
for attr in dir(self):
if (
not attr.startswith("_")
and attr not in declared
and not callable(getattr(self, attr))
):
result[attr] = getattr(self, attr)
del result["prompts"]
return result
36 changes: 31 additions & 5 deletions configs/battlesnake.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,39 @@ game:
players:
- agent: mini
name: p1
config: configs/mini/battlesnake.yaml
config: configs/mini/default.yaml
model: claude-sonnet-4-20250514
- agent: dummy
name: p2
prompt: |
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.
prompts:
game_description: |
You are a software developer ({{player_id}}) competing in a coding game called BattleSnake.
Your bot (`main.py`) controls a snake on a grid-based board.
Snakes collect food, avoid collisions, and try to outlast their opponents.
- Round: {{round}}/{{rounds}}
- Goal: Improve your bot, win the next round.

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.
Your task: improve the bot in `main.py`, located in {{working_dir}}.
{{working_dir}} is your codebase, which contains both your bot and supporting assets.

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!
The details of the game are fully available within this codebase.
- `docs/`: Game documentation
- `logs/`: Past rounds and outcomes
- `trajs/`: History of your edits
- and a lot more. It's up to you to explore and utilize these resources.

⚠️ You won't remember past rounds.
So if you want to carry knowledge forward — leave tools, notes, or strategies in the codebase.
Good documentation means you (and others) can pick up right where you left off.

If you'd hate to repeat a step next round, encode it now — as a script, a note, or a tool.

Improve the bot however you like — experiment, document, iterate. Some ideas:
- Build analysis tools
- Create bot variants to test
- Track strategies across rounds
How you choose to evolve and document is up to you. Good luck!

<important>
When you are finished making changes, issue the following command: `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT`.
</important>
22 changes: 2 additions & 20 deletions configs/mini/battlesnake.yaml → configs/mini/default.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,28 +16,10 @@ agent:

Failure to follow these rules will cause your response to be rejected.
instance_template: |
You are currently writing a bot to play a game called BattleSnake.
- The game is currently on round {{round}} of {{rounds}}.
- The logs and information about past games are in the `logs/` directory.

Your goal is to write a bot that beats the other players.
Your bot must be written and represented entirely by the `main.py` file.
Any other files or directories will not be included.

{{game_description}}
========================================================
You can execute bash commands and edit files to implement the necessary changes.

## Recommended Workflow

This workflows should be done step-by-step so that you can iterate on your changes and Any possible problems.

1. Analyze the codebase by finding and reading relevant files
2. Create a script to reproduce the issue
3. Edit the source code to resolve the issue
4. Verify your fix works by running your script again
5. Test edge cases to ensure your fix is robust
6. Submit your changes and finish your work by issuing the following command: `echo COMPLETE_TASK_AND_SUBMIT_FINAL_OUTPUT`.
Do not combine it with Any other command. <important>After this command, you cannot continue working on this task.</important>

## Important Rules

1. Every response must contain exactly one action
Expand Down
4 changes: 2 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ def main(config_path: str, cleanup: bool = False, push_agent: bool = False):
config = yaml.safe_load(f)
game: CodeGame = get_game(config)
agents: list[Player] = []
for agent in config["players"]:
agents.append(get_agent(agent, game))
for agent_conf in config["players"]:
agents.append(get_agent(agent_conf, config["prompts"], game))

try:
for _ in range(game.rounds):
Expand Down
Loading