Skip to content

Player (Abstract Base)

The Player class is the abstract base class for all agents in CodeClash.

Overview

Players are agents that:

  1. Receive game context and logs
  2. Analyze the current state
  3. Modify their code to improve performance
  4. Submit code for validation and execution

Key Concepts

Player Lifecycle

  1. Initialization: Player container created with game code
  2. Round Loop: For each round:
  3. pre_run_hook(): Prepare for the round
  4. run(): Agent modifies code
  5. post_run_hook(): Commit changes, save diffs
  6. Metadata: Collect statistics and git history

Docker Environment

Each player has an isolated Docker container with:

  • Game repository checked out
  • Git initialized on unique branch
  • Access to game logs

Git Integration

Players maintain git history:

  • Each round creates a commit
  • Git tags mark round checkpoints
  • Diffs track code evolution

Class Reference

codeclash.agents.player.Player

Player(config: dict, environment: DockerEnvironment, game_context: GameContext, push: bool = False)

Bases: ABC

Source code in codeclash/agents/player.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
def __init__(
    self,
    config: dict,
    environment: DockerEnvironment,
    game_context: GameContext,
    push: bool = False,
) -> None:
    self.config = config
    self.name = config["name"]
    self._player_unique_id = str(uuid.uuid4())
    """Unique ID that doesn't clash even across multiple games. Used for git tags."""
    self.environment = environment
    self.game_context = game_context
    self.push = push
    self.logger = get_logger(
        self.name,
        log_path=self.game_context.log_local / "players" / self.name / "player.log",
        emoji="👤",
    )
    self._metadata = {
        "name": self.name,
        "player_unique_id": self._player_unique_id,
        "created_timestamp": int(time.time()),
        "config": self.config,
        "initial_commit_hash": self._get_commit_hash(),
        "branch_name": self._branch_name,
        "round_tags": {},  # mapping round -> tag
        "agent_stats": {},  # mapping round -> agent stats
    }

    if branch := config.get("branch_init"):
        self.logger.info(f"Checking out branch {branch}")
        assert_zero_exit_code(self.environment.execute(f"git checkout {branch}"), logger=self.logger)

    if self.push:
        self.logger.info("Will push agent gameplay as branch to remote repository after each round")
        token = os.getenv("GITHUB_TOKEN")
        if not token:
            raise ValueError("GITHUB_TOKEN environment variable is required")
        for cmd in [
            "git remote remove origin",
            f"git remote add origin https://x-access-token:{token}@github.com/{GH_ORG}/{self.game_context.name}.git",
        ]:
            assert_zero_exit_code(self.environment.execute(cmd), logger=self.logger)

config instance-attribute

config = config

name instance-attribute

name = config['name']

environment instance-attribute

environment = environment

game_context instance-attribute

game_context = game_context

push instance-attribute

push = push

logger instance-attribute

logger = get_logger(name, log_path=log_local / 'players' / name / 'player.log', emoji='👤')

pre_run_hook

pre_run_hook(*, new_round: int) -> None

Should be called before we call the run method.

Source code in codeclash/agents/player.py
67
68
69
70
71
def pre_run_hook(self, *, new_round: int) -> None:
    """Should be called before we call the run method."""
    if new_round == 1:
        self._tag_round(0)
    self.game_context.round = new_round

post_run_hook

post_run_hook(*, round: int) -> None

Should be called after we called the run method.

Source code in codeclash/agents/player.py
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
def post_run_hook(self, *, round: int) -> None:
    """Should be called after we called the run method."""
    self._commit()

    # Write all changes to separate JSON file
    self._write_changes_to_file(round=round)

    if self.push:
        for cmd in [
            f"git push origin {self._branch_name}",
            "git push origin --tags",
        ]:
            assert_zero_exit_code(self.environment.execute(cmd), logger=self.logger)
        self.logger.info(f"Pushed {self.name} commit history to remote repository (branch {self._branch_name})")

run abstractmethod

run() -> None

Given the observation / recap, update the codebase

Source code in codeclash/agents/player.py
114
115
116
@abstractmethod
def run(self) -> None:
    """Given the observation / recap, update the codebase"""

get_metadata

get_metadata() -> dict

Get metadata for the agent.

Source code in codeclash/agents/player.py
118
119
120
def get_metadata(self) -> dict:
    """Get metadata for the agent."""
    return self._metadata

reset_and_apply_patch

reset_and_apply_patch(patch: str, *, base_commit: str = '', filter_patch: bool = True) -> None

Clean all uncommitted changes. If base_commit is provided, reset to that commit. Then apply the patch to the codebase.

Source code in codeclash/agents/player.py
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
def reset_and_apply_patch(self, patch: str, *, base_commit: str = "", filter_patch: bool = True) -> None:
    """Clean all uncommitted changes. If base_commit is provided, reset to that commit.
    Then apply the patch to the codebase.
    """
    # Need to clean before we copy over the patch (else it's gonna be removed by git clean)
    self.logger.debug(
        assert_zero_exit_code(self.environment.execute(f"git reset --hard {base_commit} && git clean -fd"))
    )

    patch = filter_git_diff(patch) if filter_patch else patch

    if not patch.strip():
        self.logger.debug("No patch to apply, skipping")
        return

    create_file_in_container(
        container=self.environment,  # type: ignore
        content=patch,
        dest_path="tmp_patch.txt",
    )

    commands = ["git status", "git apply tmp_patch.txt", "rm -f tmp_patch.txt"]
    cmd = " && ".join(commands)
    self.logger.debug(f"Executing command: {cmd}")
    assert_zero_exit_code(self.environment.execute(cmd), logger=self.logger)