Skip to content

Commit b51ad57

Browse files
committed
Add transparent codebase analysis
1 parent 23f17db commit b51ad57

2 files changed

Lines changed: 236 additions & 0 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Analysis for the subsection "Transparent codebases reveal models’ capacity for opponent analysis." in the CodeClash paper.
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
import json
2+
from collections import defaultdict
3+
from pathlib import Path
4+
5+
import matplotlib.pyplot as plt
6+
import numpy as np
7+
8+
from codeclash.analysis.viz.utils import ASSETS_DIR, FONT_BOLD, MODEL_TO_COLOR, MODEL_TO_DISPLAY_NAME
9+
from codeclash.constants import LOCAL_LOG_DIR
10+
11+
12+
def compute_win_rates(folder_list):
13+
wins = defaultdict(int)
14+
total = defaultdict(int)
15+
16+
for folder in folder_list:
17+
metadata = json.load(open(folder / "metadata.json"))
18+
19+
# Count wins per round
20+
for round_key, round_data in metadata["round_stats"].items():
21+
if round_key == "overall":
22+
continue
23+
winner = round_data.get("winner")
24+
for player in round_data.get("scores", {}).keys():
25+
total[player] += 1
26+
if player == winner:
27+
wins[player] += 1
28+
29+
return {player: wins[player] / total[player] if total[player] > 0 else 0 for player in total.keys()}
30+
31+
32+
def analyze_opponent_code_access(folder_list):
33+
"""Check trajectory logs for commands accessing /opponent_codebases/"""
34+
access_counts = defaultdict(lambda: {"accessed": 0, "total_rounds": 0})
35+
36+
for folder in folder_list:
37+
players_dir = folder / "players"
38+
if not players_dir.exists():
39+
continue
40+
41+
for player_dir in players_dir.iterdir():
42+
if not player_dir.is_dir():
43+
continue
44+
player_name = player_dir.name
45+
46+
# Check all trajectory files
47+
for traj_file in player_dir.glob("*.traj.json"):
48+
traj = json.load(open(traj_file))
49+
50+
accessed_opponent = False
51+
# Look through messages for commands accessing opponent code
52+
for msg in traj.get("messages", []):
53+
if msg.get("role") == "assistant":
54+
content = msg.get("content", "")
55+
if "/opponent_codebases/" in content or "opponent_codebases" in content:
56+
accessed_opponent = True
57+
break
58+
59+
access_counts[player_name]["total_rounds"] += 1
60+
if accessed_opponent:
61+
access_counts[player_name]["accessed"] += 1
62+
63+
# Compute rates
64+
rates = {}
65+
for player, counts in access_counts.items():
66+
rate = counts["accessed"] / counts["total_rounds"] if counts["total_rounds"] > 0 else 0
67+
rates[player] = {"rate": rate, "accessed": counts["accessed"], "total": counts["total_rounds"]}
68+
69+
return rates
70+
71+
72+
def compute_exploitation_advantage():
73+
"""Calculate win rate change from normal to transparent for each model"""
74+
advantages = {}
75+
76+
for model in transparent_wr.keys():
77+
normal_rate = normal_wr.get(model, 0)
78+
transparent_rate = transparent_wr.get(model, 0)
79+
80+
# Absolute and relative change
81+
absolute_change = transparent_rate - normal_rate
82+
relative_change = (transparent_rate - normal_rate) / normal_rate if normal_rate > 0 else 0
83+
84+
advantages[model] = {
85+
"normal": normal_rate,
86+
"transparent": transparent_rate,
87+
"absolute_change": absolute_change,
88+
"relative_change": relative_change,
89+
}
90+
91+
return advantages
92+
93+
94+
def analyze_temporal_opponent_access(folder_list):
95+
"""Track opponent code access by round number (early vs late tournament)"""
96+
access_by_round = defaultdict(lambda: {"accessed": 0, "total": 0})
97+
access_by_model_round = defaultdict(lambda: defaultdict(lambda: {"accessed": 0, "total": 0}))
98+
99+
for folder in folder_list:
100+
players_dir = folder / "players"
101+
if not players_dir.exists():
102+
continue
103+
104+
for player_dir in players_dir.iterdir():
105+
if not player_dir.is_dir():
106+
continue
107+
player_name = player_dir.name
108+
109+
for traj_file in player_dir.glob("*_r*.traj.json"):
110+
# Extract round number from filename
111+
round_num = int(traj_file.stem.split("_r")[-1].split(".")[0])
112+
113+
traj = json.load(open(traj_file))
114+
accessed_opponent = False
115+
116+
for msg in traj.get("messages", []):
117+
if msg.get("role") == "assistant":
118+
content = msg.get("content", "")
119+
if "/opponent_codebases/" in content or "opponent_codebases" in content:
120+
accessed_opponent = True
121+
break
122+
123+
access_by_round[round_num]["total"] += 1
124+
access_by_model_round[player_name][round_num]["total"] += 1
125+
126+
if accessed_opponent:
127+
access_by_round[round_num]["accessed"] += 1
128+
access_by_model_round[player_name][round_num]["accessed"] += 1
129+
130+
return access_by_round, access_by_model_round
131+
132+
133+
if __name__ == "__main__":
134+
normal = sorted(
135+
[
136+
x.parent
137+
for x in Path(LOCAL_LOG_DIR).rglob("metadata.json")
138+
if "Halite" in x.parent.name
139+
and ".p2." in x.parent.name
140+
and "transparent" not in x.parent.name
141+
and (
142+
("claude-sonnet-4-5" in x.parent.name and "gemini-2.5-pro" in x.parent.name)
143+
or ("claude-sonnet-4-5" in x.parent.name and ".gpt-5." in x.parent.name)
144+
or ("gemini-2.5-pro" in x.parent.name and ".gpt-5." in x.parent.name)
145+
)
146+
]
147+
)
148+
folders = sorted(
149+
[x.parent for x in Path(LOCAL_LOG_DIR).rglob("metadata.json") if x.parent.name.endswith(".transparent")]
150+
)
151+
len(folders), len(normal)
152+
153+
# 1. Win Rates in Transparent vs. Normal Settings
154+
transparent_wr = compute_win_rates(folders)
155+
normal_wr = compute_win_rates(normal)
156+
157+
print("TRANSPARENT Win Rates:")
158+
for model, wr in sorted(transparent_wr.items(), key=lambda x: x[1], reverse=True):
159+
print(f" {model}: {wr:.3f}")
160+
161+
print("\nNORMAL Win Rates:")
162+
for model, wr in sorted(normal_wr.items(), key=lambda x: x[1], reverse=True):
163+
print(f" {model}: {wr:.3f}")
164+
165+
# 2. Opponent Code Access Analysis
166+
opponent_analysis = analyze_opponent_code_access(folders)
167+
168+
print("Opponent Code Access Rates (Transparent Setting):")
169+
for model, stats in sorted(opponent_analysis.items(), key=lambda x: x[1]["rate"], reverse=True):
170+
print(f" {model}: {stats['rate']:.1%} ({stats['accessed']}/{stats['total']} rounds)")
171+
172+
# 3. Exploitation Advantage Calculation
173+
exploitation = compute_exploitation_advantage()
174+
175+
print("Exploitation Advantage (Win Rate Changes):")
176+
for model, stats in sorted(exploitation.items(), key=lambda x: x[1]["absolute_change"], reverse=True):
177+
print(f" {model}:")
178+
print(f" Normal: {stats['normal']:.3f} → Transparent: {stats['transparent']:.3f}")
179+
print(f" Change: {stats['absolute_change']:+.3f} ({stats['relative_change']:+.1%})")
180+
181+
# 4. Temporal Evolution: Do Models look at opponent code more over time
182+
overall_temporal, model_temporal = analyze_temporal_opponent_access(folders)
183+
184+
# Show early (rounds 1-5), mid (rounds 6-10), vs late (rounds 11-15) for each model
185+
print("\nOpponent Code Access: Early (R1-5) vs Mid (R6-10) vs Late (R11-15) Rounds\n")
186+
187+
temporal_data = {}
188+
for model in sorted(model_temporal.keys()):
189+
early_acc = sum(model_temporal[model][r]["accessed"] for r in range(1, 6))
190+
early_tot = sum(model_temporal[model][r]["total"] for r in range(1, 6))
191+
mid_acc = sum(model_temporal[model][r]["accessed"] for r in range(6, 11))
192+
mid_tot = sum(model_temporal[model][r]["total"] for r in range(6, 11))
193+
late_acc = sum(model_temporal[model][r]["accessed"] for r in range(11, 16))
194+
late_tot = sum(model_temporal[model][r]["total"] for r in range(11, 16))
195+
196+
early_rate = early_acc / early_tot if early_tot > 0 else 0
197+
mid_rate = mid_acc / mid_tot if mid_tot > 0 else 0
198+
late_rate = late_acc / late_tot if late_tot > 0 else 0
199+
200+
temporal_data[model] = {"Early (R1-5)": early_rate, "Mid (R6-10)": mid_rate, "Late (R11-15)": late_rate}
201+
202+
print(f" {model}:")
203+
print(f" Early: {early_rate:.1%} ({early_acc}/{early_tot})")
204+
print(f" Mid: {mid_rate:.1%} ({mid_acc}/{mid_tot})")
205+
print(f" Late: {late_rate:.1%} ({late_acc}/{late_tot})")
206+
207+
# Create bar chart visualization
208+
fig, ax = plt.subplots(figsize=(6, 6))
209+
210+
periods = ["Early\n(R1-5)", "Mid\n(R6-10)", "Late\n(R11-15)"]
211+
x = np.arange(len(periods))
212+
width = 0.2
213+
214+
models = sorted(temporal_data.keys())
215+
216+
for i, model in enumerate(models):
217+
display_name = MODEL_TO_DISPLAY_NAME.get(model, model)
218+
color = MODEL_TO_COLOR.get(model, None)
219+
rates = [temporal_data[model][period] * 100 for period in ["Early (R1-5)", "Mid (R6-10)", "Late (R11-15)"]]
220+
ax.bar(x + i * width, rates, width, label=display_name, color=color)
221+
222+
# ax.set_xlabel('Rounds', fontsize=18, fontproperties=FONT_BOLD)
223+
ax.set_ylabel("Opponent Code Access Rate (%)", fontsize=18, fontproperties=FONT_BOLD)
224+
ax.set_xticks(x + width)
225+
ax.set_xticklabels(periods, fontproperties=FONT_BOLD, fontsize=18)
226+
ax.tick_params(axis="y", labelsize=18)
227+
for label in ax.get_yticklabels():
228+
label.set_fontproperties(FONT_BOLD)
229+
FONT_BOLD.set_size(16)
230+
ax.legend(prop=FONT_BOLD, fontsize=14)
231+
ax.grid(axis="y", alpha=0.3)
232+
233+
plt.tight_layout()
234+
plt.savefig(ASSETS_DIR / "bar_chart_temporal_opponent_access.png", dpi=300, bbox_inches="tight")
235+
print(f"\nSaved visualization to {ASSETS_DIR / 'bar_chart_temporal_opponent_access.png'}")

0 commit comments

Comments
 (0)