Skip to content

Commit c8b6282

Browse files
committed
Update viewer for new output format
1 parent 96a3665 commit c8b6282

5 files changed

Lines changed: 112 additions & 42 deletions

File tree

.cursor/rules/viewer.mdc

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ The input folders are all subfolders of `logs/`
2222

2323
The input folder has the following files
2424

25-
* results.json
26-
* main.log
27-
* round_$i.log, where $i is the round.
28-
* p$i_r$j.traj.log, where $i$ is the player (there's always 1 or 2 players) and $j is the round number
25+
* metadata.json
26+
* tournament.log
27+
* rounds/$i/sim_$j.log, where $i is the round and $j is the simulation number (repetition of game)
28+
* players/$player_id/$player_id_r$i.traj.json, where $j is the round number
29+
* players/$player_id/player.log
2930
* Trajectory files do not exist for the last round
3031

3132
Here's how a `.traj.log` file looks:

codeclash/viewer/app.py

Lines changed: 66 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -72,36 +72,55 @@ def __init__(self, log_dir: Path):
7272

7373
def parse_game_metadata(self) -> GameMetadata:
7474
"""Parse overall game metadata"""
75-
# Look for results.json or metadata.json
76-
results_file = self.log_dir / "results.json"
77-
if not results_file.exists():
78-
# Check for metadata.json as fallback
79-
metadata_file = self.log_dir / "metadata.json"
80-
if metadata_file.exists():
81-
results = json.loads(metadata_file.read_text())
82-
else:
83-
results = {"status": "No results file found"}
75+
# Look for metadata.json
76+
metadata_file = self.log_dir / "metadata.json"
77+
if metadata_file.exists():
78+
results = json.loads(metadata_file.read_text())
8479
else:
85-
results = json.loads(results_file.read_text())
80+
results = {"status": "No metadata file found"}
8681

87-
# Parse main.log if it exists
88-
main_log_file = self.log_dir / "game.log"
89-
main_log = main_log_file.read_text() if main_log_file.exists() else "No main log found"
82+
# Parse tournament.log if it exists
83+
main_log_file = self.log_dir / "tournament.log"
84+
main_log = main_log_file.read_text() if main_log_file.exists() else "No tournament log found"
9085

91-
# Parse round logs
86+
# Parse round directories and their sim logs
9287
rounds = []
93-
round_files = sorted(self.log_dir.glob("round_*.log"))
94-
for round_file in round_files:
95-
round_content = round_file.read_text()
96-
rounds.append({"filename": round_file.name, "content": round_content})
88+
rounds_dir = self.log_dir / "rounds"
89+
if rounds_dir.exists():
90+
# Get all round directories (sorted numerically)
91+
round_dirs = sorted([d for d in rounds_dir.iterdir() if d.is_dir()], key=lambda x: int(x.name))
92+
93+
for round_dir in round_dirs:
94+
round_num = int(round_dir.name)
95+
96+
# Collect all sim logs for this round
97+
sim_logs = []
98+
sim_files = sorted(round_dir.glob("sim_*.log"), key=lambda x: int(x.stem.split("_")[1]))
99+
100+
for sim_file in sim_files:
101+
sim_content = sim_file.read_text()
102+
sim_logs.append({"filename": sim_file.name, "content": sim_content})
103+
104+
# Check for round results
105+
results_file = round_dir / "results.json"
106+
round_results = None
107+
if results_file.exists():
108+
round_results = json.loads(results_file.read_text())
109+
110+
rounds.append({"round_num": round_num, "sim_logs": sim_logs, "results": round_results})
97111

98112
return GameMetadata(results=results, main_log=main_log, rounds=rounds)
99113

100114
def parse_trajectory(self, player_id: int, round_num: int) -> TrajectoryInfo | None:
101115
"""Parse a specific trajectory file"""
116+
# Look in players/$player_id/ directory
117+
player_dir = self.log_dir / "players" / f"p{player_id}"
118+
if not player_dir.exists():
119+
return None
120+
102121
# Try both .json and .log extensions
103122
for ext in [".json", ".log"]:
104-
traj_file = self.log_dir / f"p{player_id}_r{round_num}.traj{ext}"
123+
traj_file = player_dir / f"p{player_id}_r{round_num}.traj{ext}"
105124
if traj_file.exists():
106125
try:
107126
data = json.loads(traj_file.read_text())
@@ -127,18 +146,34 @@ def parse_trajectory(self, player_id: int, round_num: int) -> TrajectoryInfo | N
127146
def get_available_trajectories(self) -> list[tuple]:
128147
"""Get list of available trajectory files as (player_id, round_num) tuples"""
129148
trajectories = []
130-
for traj_file in self.log_dir.glob("p*_r*.traj.*"):
131-
# Extract player and round from filename like p1_r2.traj.json
132-
parts = traj_file.stem.split(".") # Remove extension
133-
if parts:
134-
name_part = parts[0] # p1_r2
135-
try:
136-
player_part, round_part = name_part.split("_")
137-
player_id = int(player_part[1:]) # Remove 'p' prefix
138-
round_num = int(round_part[1:]) # Remove 'r' prefix
139-
trajectories.append((player_id, round_num))
140-
except (ValueError, IndexError):
141-
continue
149+
players_dir = self.log_dir / "players"
150+
151+
if not players_dir.exists():
152+
return trajectories
153+
154+
# Iterate through player directories
155+
for player_dir in players_dir.iterdir():
156+
if not player_dir.is_dir():
157+
continue
158+
159+
try:
160+
# Extract player_id from directory name (e.g., "p1" -> 1)
161+
player_id = int(player_dir.name[1:]) # Remove 'p' prefix
162+
except (ValueError, IndexError):
163+
continue
164+
165+
# Find trajectory files in this player's directory
166+
for traj_file in player_dir.glob("p*_r*.traj.*"):
167+
# Extract round from filename like p1_r2.traj.json
168+
parts = traj_file.stem.split(".") # Remove extension
169+
if parts:
170+
name_part = parts[0] # p1_r2
171+
try:
172+
_, round_part = name_part.split("_")
173+
round_num = int(round_part[1:]) # Remove 'r' prefix
174+
trajectories.append((player_id, round_num))
175+
except (ValueError, IndexError):
176+
continue
142177

143178
return sorted(trajectories)
144179

codeclash/viewer/static/css/style.css

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,23 @@ details summary {
225225
margin-left: 1rem;
226226
}
227227

228+
.sim-foldout {
229+
border-left: 2px solid var(--text-muted);
230+
margin-left: 1rem;
231+
margin-bottom: 0.5rem;
232+
}
233+
234+
.sim-foldout summary {
235+
padding: 0.5rem;
236+
font-size: 0.9rem;
237+
color: var(--text-secondary);
238+
cursor: pointer;
239+
}
240+
241+
.sim-foldout summary:hover {
242+
background-color: var(--bg-tertiary);
243+
}
244+
228245
/* Round separator */
229246
.round-separator {
230247
height: 2px;

codeclash/viewer/templates/index.html

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,30 @@ <h2>📊 Game Results</h2>
5858
<h2>🎯 Game Rounds</h2>
5959

6060
{% for round_data in metadata.rounds %}
61-
{% set round_num = loop.index %}
61+
{% set round_num = round_data.round_num %}
6262

63-
<!-- Round Log -->
63+
<!-- Round Results (if available) -->
64+
{% if round_data.results %}
6465
<details class="foldout round-foldout">
65-
<summary>🎮 Round {{ round_num }} Log</summary>
66+
<summary>🏆 Round {{ round_num }} Results</summary>
6667
<div class="log-content">
67-
<pre><code>{{ round_data.content }}</code></pre>
68+
<pre><code>{{ round_data.results | tojson(indent=2) }}</code></pre>
69+
</div>
70+
</details>
71+
{% endif %}
72+
73+
<!-- Round Simulation Logs -->
74+
<details class="foldout round-foldout">
75+
<summary>🎮 Round {{ round_num }} Simulation Logs ({{ round_data.sim_logs|length }} sims)</summary>
76+
<div class="log-content">
77+
{% for sim_log in round_data.sim_logs %}
78+
<details class="foldout sim-foldout">
79+
<summary>{{ sim_log.filename }}</summary>
80+
<div class="log-content">
81+
<pre><code>{{ sim_log.content }}</code></pre>
82+
</div>
83+
</details>
84+
{% endfor %}
6885
</div>
6986
</details>
7087

configs/pvp/battlesnake.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
tournament:
2-
rounds: 25
2+
rounds: 2
33
game:
44
name: BattleSnake
5-
sims_per_round: 100
5+
sims_per_round: 20
66
args:
77
width: 11
88
height: 11
@@ -19,7 +19,7 @@ players:
1919
config:
2020
agent: !include mini/default.yaml
2121
model:
22-
model_name: anthropic/claude-sonnet-4-20250514
22+
model_name: openai/gpt-5-mini
2323
prompts:
2424
game_description: |
2525
You are a software developer ({{player_id}}) competing in a coding game called BattleSnake.

0 commit comments

Comments
 (0)