|
| 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