Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,9 @@ marimo/_static/
marimo/_lsp/
__marimo__/

# Singularity
*.sif


# >>>>>>>>> CUSTOM THINGS ON TOP OF FILE <<<<<<<<<
docs/visualization/*.json
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Check out our [arXiv paper](https://arxiv.org/abs/2511.00839) and [website](http

- **Python 3.11+**
- **[uv](https://docs.astral.sh/uv/)** - Fast Python package manager
- **Docker** - For running games in containers
- **Docker** or **Singularity** - For running games in containers
- **Git**

### Installation
Expand All @@ -58,8 +58,9 @@ uv run python main.py configs/test/battlesnake.yaml
```

> [!TIP]
> CodeClash requires Docker to create execution environments. CodeClash was developed and tested on Ubuntu 22.04.4 LTS.
> CodeClash requires Docker or Singularity to create execution environments. CodeClash was developed and tested on Ubuntu 22.04.4 LTS.
> The same instructions should work for Mac. If not, check out [#81](https://github.com/CodeClash-ai/CodeClash/issues/81) for an alternative solution.
> To use Singularity instead of Docker, set the environment variable: `export CODECLASH_RUNTIME=singularity`

<details>
<summary>Alternative: Using pip (not recommended)</summary>
Expand Down
5 changes: 2 additions & 3 deletions codeclash/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
from minisweagent.environments.docker import DockerEnvironment

from codeclash.agents.dummy_agent import Dummy
from codeclash.agents.minisweagent import MiniSWEAgent
from codeclash.agents.player import Player
from codeclash.agents.utils import GameContext
from codeclash.utils.environment import ContainerEnvironment


def get_agent(config: dict, game_context: GameContext, environment: DockerEnvironment) -> Player:
def get_agent(config: dict, game_context: GameContext, environment: ContainerEnvironment) -> Player:
agents = {
"dummy": Dummy,
"mini": MiniSWEAgent,
Expand Down
7 changes: 3 additions & 4 deletions codeclash/agents/minisweagent.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@

from minisweagent import Model
from minisweagent.agents.default import AgentConfig, DefaultAgent
from minisweagent.environments.docker import DockerEnvironment
from minisweagent.models import get_model
from minisweagent.models.test_models import DeterministicModel
from minisweagent.run.utils.save import save_traj

from codeclash import REPO_DIR
from codeclash.agents.player import Player
from codeclash.agents.utils import GameContext
from codeclash.utils.environment import copy_to_container
from codeclash.utils.environment import ContainerEnvironment, copy_to_container

os.environ["MSWEA_MODEL_RETRY_STOP_AFTER_ATTEMPT"] = "90"
os.environ["LITELLM_MODEL_REGISTRY_PATH"] = str(
Expand All @@ -30,7 +29,7 @@ class ClashAgent(DefaultAgent):
def __init__(
self,
model: Model,
env: DockerEnvironment,
env: ContainerEnvironment,
*,
logger: logging.Logger,
config_class: Callable = AgentConfig,
Expand All @@ -47,7 +46,7 @@ def add_message(self, role: str, content: str, **kwargs):
class MiniSWEAgent(Player):
"""Player with agentic code editing capabilities"""

def __init__(self, config: dict, environment: DockerEnvironment, game_context: GameContext):
def __init__(self, config: dict, environment: ContainerEnvironment, game_context: GameContext):
super().__init__(config, environment=environment, game_context=game_context)

def run(self):
Expand Down
7 changes: 3 additions & 4 deletions codeclash/agents/player.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@
from abc import ABC, abstractmethod

from dotenv import load_dotenv
from minisweagent.environments.docker import DockerEnvironment

from codeclash.agents.utils import GameContext
from codeclash.constants import GH_ORG
from codeclash.tournaments.utils.git_utils import extract_modified_code_file_paths_from_diff, filter_git_diff
from codeclash.utils.environment import assert_zero_exit_code, create_file_in_container
from codeclash.utils.environment import ContainerEnvironment, assert_zero_exit_code, create_file_in_container
from codeclash.utils.log import get_logger

load_dotenv()
Expand All @@ -20,7 +19,7 @@ class Player(ABC):
def __init__(
self,
config: dict,
environment: DockerEnvironment,
environment: ContainerEnvironment,
game_context: GameContext,
) -> None:
self.config = config
Expand Down Expand Up @@ -170,7 +169,7 @@ def _get_commit_hash(self) -> str:
self.environment.execute("git rev-parse HEAD"),
logger=self.logger,
)
return out["output"].strip()
return out["output"].strip().splitlines()[-1] # take last line only to strip any Singularity warnings

def _commit(self) -> None:
"""Commit changes to the agent's codebase."""
Expand Down
123 changes: 93 additions & 30 deletions codeclash/arenas/arena.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,22 @@
from typing import Any

from minisweagent.environments.docker import DockerEnvironment
from minisweagent.environments.singularity import SingularityEnvironment

from codeclash.agents.player import Player
from codeclash.constants import DIR_LOGS, DIR_WORK, GH_ORG, RESULT_TIE
from codeclash.utils.aws import is_running_in_aws_batch, pull_game_container_aws_ecr
from codeclash.utils.environment import assert_zero_exit_code, copy_between_containers, copy_from_container
from codeclash.utils.environment import ContainerEnvironment, assert_zero_exit_code, copy_between_containers, copy_from_container
from codeclash.utils.log import get_logger


def get_runtime() -> str:
"""Get the container runtime from the CODECLASH_RUNTIME environment variable.
Defaults to 'docker' if not set.
"""
return os.environ.get("CODECLASH_RUNTIME", "docker").lower()


class PlayerStats:
def __init__(self, name: str):
self.name = name
Expand Down Expand Up @@ -99,8 +107,8 @@ def __init__(self, config: dict, *, tournament_id: str, local_output_dir: Path,
self.log_env: Path = DIR_LOGS
self.log_local: Path = local_output_dir
self.logger = get_logger(self.name, log_path=self.log_local / "game.log", emoji="🏓")
self.environment: DockerEnvironment = self.get_environment()
"""The running docker environment for executing the game"""
self.environment: ContainerEnvironment = self.get_environment()
"""The running container environment for executing the game"""

@property
def game_config(self) -> dict:
Expand All @@ -114,11 +122,51 @@ def game_id(self) -> str:
def image_name(self) -> str:
return f"codeclash/{self.name.lower()}"

@property
def sif_path(self) -> Path:
"""Path to the Singularity .sif image file, located next to the arena definition."""
arena_file = Path(inspect.getfile(self.__class__))
return arena_file.parent / f"{self.name.lower()}.sif"

def build_image(self):
"""
Build a Docker image for the game using the Dockerfile in the codebase.
If running in AWS, pull the image from the AWS Docker registry instead.
Build a container image for the game.

For Docker: builds from the Dockerfile in the codebase. If running in AWS,
pulls the image from the AWS Docker registry instead.
For Singularity: builds a .sif file from the .def file if it doesn't already exist.
"""
if get_runtime() == "singularity":
self._build_singularity_image()
else:
self._build_docker_image()

def _build_singularity_image(self):
"""Build a Singularity .sif image from the .def file."""
if self.sif_path.exists():
self.logger.debug(f"Singularity image {self.sif_path} already exists")
return

arena_file = Path(inspect.getfile(self.__class__))
def_path = arena_file.parent / f"{self.name}.def"
if not def_path.exists():
raise RuntimeError(f"Singularity definition file not found: {def_path}")

self.logger.info(f"Building Singularity image {self.sif_path} from {def_path}")
result = subprocess.run(
f"singularity build --fakeroot {self.sif_path} {def_path}",
shell=True,
capture_output=True,
text=True,
)
if result.returncode == 0:
self.logger.info(f"Built Singularity image {self.sif_path}")
else:
self.logger.error(f"Failed to build Singularity image: {result.stderr}\n{result.stdout}")
raise RuntimeError(f"Failed to build Singularity image: {result.stderr}")

def _build_docker_image(self):
"""Build a Docker image from the Dockerfile."""
if is_running_in_aws_batch():
pull_game_container_aws_ecr(game_name=self.name, image_name=self.image_name, logger=self.logger)

Expand Down Expand Up @@ -148,9 +196,9 @@ def build_image(self):
text=True,
)
if result.returncode == 0:
self.logger.info(f"Built Docker image {self.image_name}")
self.logger.info(f"Built Docker image {self.image_name}")
else:
self.logger.error(f"Failed to build Docker image: {result.stderr}\n{result.stdout}{result.stderr}")
self.logger.error(f"Failed to build Docker image: {result.stderr}\n{result.stdout}{result.stderr}")
raise RuntimeError(f"Failed to build Docker image: {result.stderr}")

def copy_logs_from_env(self, round_num: int) -> None:
Expand Down Expand Up @@ -178,39 +226,54 @@ def end(self, cleanup: bool = False):
def log_round(self, round_num: int) -> Path:
return self.log_local / "rounds" / str(round_num)

def get_environment(self, branch_name: str | None = None) -> DockerEnvironment:
"""Get docker container ID with the game code installed."""
def get_environment(self, branch_name: str | None = None) -> ContainerEnvironment:
"""Get a container environment with the game code installed."""
self.build_image()
if not self._keep_containers:
run_args = ["--rm"]

env_vars = {
"GITHUB_TOKEN": os.getenv("GITHUB_TOKEN", ""),
"PAGER": "cat",
"MANPAGER": "cat",
"LESS": "-R",
"PIP_PROGRESS_BAR": "off",
"TQDM_DISABLE": "1",
}

if get_runtime() == "singularity":
environment = SingularityEnvironment(
image=str(self.sif_path),
cwd=str(DIR_WORK),
env=env_vars,
timeout=36000, # 10h in seconds
logger=self.logger,
)
else:
run_args = []
environment = DockerEnvironment(
image=self.image_name,
cwd=str(DIR_WORK),
env={
"GITHUB_TOKEN": os.getenv("GITHUB_TOKEN", ""),
"PAGER": "cat",
"MANPAGER": "cat",
"LESS": "-R",
"PIP_PROGRESS_BAR": "off",
"TQDM_DISABLE": "1",
},
container_timeout="10h",
logger=self.logger,
run_args=run_args,
)
if not self._keep_containers:
run_args = ["--rm"]
else:
run_args = []
environment = DockerEnvironment(
image=self.image_name,
cwd=str(DIR_WORK),
env=env_vars,
container_timeout="10h",
logger=self.logger,
run_args=run_args,
)

branch_name = self.game_id if branch_name is None else branch_name

# Logger setting will likely not take effect for initial container creation logs
environment.logger = get_logger("environment", emoji="🪴")
# Use local (not --global) git config so it persists in the repo's .git/config.
# Singularity's --contain flag creates an ephemeral home per exec call,
# so --global config written to ~/.gitconfig is lost between calls.
for cmd in [
f"git branch {branch_name}",
f"git checkout {branch_name}",
'git config --global user.email "player@codeclash.com"',
'git config --global user.name "Player"',
"git config --global commit.gpgsign false",
'git config user.email "player@codeclash.com"',
'git config user.name "Player"',
"git config commit.gpgsign false",
]:
assert_zero_exit_code(environment.execute(cmd), logger=self.logger)
return environment
Expand Down
20 changes: 20 additions & 0 deletions codeclash/arenas/battlecode23/BattleCode23.def
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Bootstrap: docker
From: eclipse-temurin:8-jdk

%environment
export JAVA_HOME=/opt/java/openjdk
export GRADLE_USER_HOME=/root/.gradle

%post
apt-get update && apt-get install -y --no-install-recommends \
git curl unzip && \
rm -rf /var/lib/apt/lists/*

git clone https://github.com/CodeClash-ai/BattleCode2023.git /workspace \
&& cd /workspace \
&& git remote set-url origin https://github.com/CodeClash-ai/BattleCode2023.git

cd /workspace && chmod +x gradlew && ./gradlew update

%labels
Author CodeClash
3 changes: 2 additions & 1 deletion codeclash/arenas/battlecode23/battlecode23.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,8 @@ def _compile_agent(self, agent: Player, idx: int) -> str | None:
return None

# Save compiled classes outside build/ (gradle clean deletes build/)
classes_dir = f"/tmp/agent{idx}_classes"
# Use /opt as Singularity's --contain clears /tmp across execute() calls
classes_dir = f"/opt/agent{idx}_classes"
self.environment.execute(
f"rm -rf {classes_dir}; mkdir -p {classes_dir}; cp -r build/classes/* {classes_dir}/"
)
Expand Down
20 changes: 20 additions & 0 deletions codeclash/arenas/battlecode24/BattleCode24.def
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Bootstrap: docker
From: eclipse-temurin:8-jdk

%environment
export JAVA_HOME=/opt/java/openjdk
export GRADLE_USER_HOME=/root/.gradle

%post
apt-get update && apt-get install -y --no-install-recommends \
git curl unzip && \
rm -rf /var/lib/apt/lists/*

git clone https://github.com/CodeClash-ai/BattleCode2024.git /workspace \
&& cd /workspace \
&& git remote set-url origin https://github.com/CodeClash-ai/BattleCode2024.git

cd /workspace && chmod +x gradlew && ./gradlew update

%labels
Author CodeClash
3 changes: 2 additions & 1 deletion codeclash/arenas/battlecode24/battlecode24.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ def _compile_agent(self, agent: Player, idx: int) -> str | None:
return None

# Save compiled classes outside build/ (gradle clean deletes build/)
classes_dir = f"/tmp/agent{idx}_classes"
# Use /opt as Singularity's --contain clears /tmp across execute() calls
classes_dir = f"/opt/agent{idx}_classes"
self.environment.execute(
f"rm -rf {classes_dir}; mkdir -p {classes_dir}; cp -r build/classes/* {classes_dir}/"
)
Expand Down
20 changes: 20 additions & 0 deletions codeclash/arenas/battlecode25/BattleCode25.def
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Bootstrap: docker
From: python:3.12-slim-bookworm

%environment
export PYTHONDONTWRITEBYTECODE=1
export PYTHONUNBUFFERED=1

%post
apt-get update && apt-get install -y --no-install-recommends \
build-essential git curl && \
rm -rf /var/lib/apt/lists/*

git clone https://github.com/CodeClash-ai/BattleCode2025.git /workspace \
&& cd /workspace \
&& git remote set-url origin https://github.com/CodeClash-ai/BattleCode2025.git

cd /workspace && python run.py update

%labels
Author CodeClash
Loading