11import re
22import shlex
3+ from collections import defaultdict
4+ from concurrent .futures import ThreadPoolExecutor , as_completed
35
46from codeclash .agents .player import Player
57from codeclash .arenas .arena import CodeArena , RoundStats
8+ from codeclash .constants import RESULT_TIE
69
7- COREWAR_LOG = "sim .log"
10+ COREWAR_LOG = "sim_{idx} .log"
811
912
1013class CoreWarArena (CodeArena ):
@@ -23,49 +26,73 @@ def __init__(self, config, **kwargs):
2326 else :
2427 self .run_cmd_round += f" -{ arg } { val } "
2528
26- def execute_round (self , agents : list [Player ]):
29+ def _run_single_simulation (self , agents : list [Player ], idx : int ):
30+ # Shift agents by idx to vary starting positions
31+ agents = agents [idx :] + agents [:idx ]
2732 args = [f"/{ agent .name } /{ self .submission } " for agent in agents ]
2833 cmd = (
2934 f"{ self .run_cmd_round } { shlex .join (args )} "
3035 f"-r { self .game_config ['sims_per_round' ]} "
31- f"> { self .log_env / COREWAR_LOG } ;"
36+ f"> { self .log_env / COREWAR_LOG . format ( idx = idx ) } ;"
3237 )
3338 self .logger .info (f"Running game: { cmd } " )
3439 response = self .environment .execute (cmd )
3540 assert response ["returncode" ] == 0 , response
3641
42+ def execute_round (self , agents : list [Player ]):
43+ with ThreadPoolExecutor (4 ) as executor :
44+ futures = [executor .submit (self ._run_single_simulation , agents , idx ) for idx in range (len (agents ))]
45+ for future in as_completed (futures ):
46+ future .result ()
47+
3748 def get_results (self , agents : list [Player ], round_num : int , stats : RoundStats ):
38- with open (self .log_round (round_num ) / COREWAR_LOG ) as f :
39- result_output = f .read ()
40- self .logger .debug (f"Determining winner from result output: { result_output } " )
41- scores = []
42- n = len (agents ) * 2
43- lines = result_output .strip ().split ("\n " )
49+ scores , wins = defaultdict (int ), defaultdict (int )
50+ for idx in range (len (agents )):
51+ shift = agents [idx :] + agents [:idx ] # Shift agents by idx to match simulation order
52+ with open (self .log_round (round_num ) / COREWAR_LOG .format (idx = idx )) as f :
53+ result_output = f .read ()
4454
45- # Get the last n lines which contain the scores (closer to original)
46- relevant_lines = lines [ - n :] if len ( lines ) >= n else lines
47- relevant_lines = [ l for l in relevant_lines if len (l . strip ()) > 0 ]
48- self . logger . debug ( f"Relevant lines for scoring: { relevant_lines } " )
55+ # Get the last n lines which contain the scores (closer to original)
56+ lines = result_output . strip (). split ( " \n " )
57+ relevant_lines = lines [ - len ( shift ) * 2 :] if len (lines ) >= len ( shift ) * 2 else lines
58+ relevant_lines = [ l for l in relevant_lines if len ( l . strip ()) > 0 ]
4959
50- # Go through each line; we assume score position is correlated with agent index
51- for line in relevant_lines :
52- match = re .search (r".*\sby\s.*\sscores\s(\d+)" , line )
53- if match :
54- score = int (match .group (1 ))
55- scores .append (score )
60+ # Go through each line; score position is correlated with agent index
61+ for i , line in enumerate (relevant_lines ):
62+ match = re .search (r".*\sby\s.*\sscores\s(\d+)" , line )
63+ if match :
64+ scores [shift [i ].name ] += int (match .group (1 ))
5665
57- if scores :
58- if len (scores ) != len (agents ):
59- self .logger .error (f"Have { len (scores )} scores but { len (agents )} agents" )
60- stats .winner = agents [scores .index (max (scores ))].name
61- stats .scores = {agent .name : score for agent , score in zip (agents , scores )}
62- else :
63- self .logger .debug ("No scores found, returning unknown" )
64- stats .winner = "unknown"
65- stats .scores = {agent .name : 0 for agent in agents }
66+ # Last line corresponds to absolute number of wins
67+ last = relevant_lines [- 1 ][len ("Results:" ) :].strip ()
68+ for i , w in enumerate (last .split ()[:- 1 ]): # NOTE: Omitting ties (last entry)
69+ wins [shift [i ].name ] += int (w )
6670
67- for player , score in stats .scores .items ():
68- stats .player_stats [player ].score = score
71+ if len (wins ) != len (agents ):
72+ # Should not happen
73+ self .logger .error (f"Have { len (wins )} wins but { len (agents )} agents" )
74+
75+ # Bookkeeping
76+ stats .scores = {a .name : wins [a .name ] for a in agents }
77+ for a in agents :
78+ stats .player_stats [a .name ].score = wins [a .name ]
79+
80+ # Determine overall winner by highest wins, then highest score
81+ max_wins = max (wins .values (), default = 0 )
82+ potential_winners = [name for name , w in wins .items () if w == max_wins ]
83+ if len (potential_winners ) == 1 :
84+ stats .winner = potential_winners [0 ]
85+ else :
86+ # Tie-break by score
87+ max_score = - 1
88+ winner = RESULT_TIE
89+ for name in potential_winners :
90+ if scores [name ] > max_score :
91+ max_score = scores [name ]
92+ winner = name
93+ elif scores [name ] == max_score :
94+ winner = RESULT_TIE
95+ stats .winner = winner
6996
7097 def validate_code (self , agent : Player ) -> tuple [bool , str | None ]:
7198 if self .submission not in agent .environment .execute ("ls" )["output" ]:
0 commit comments