yxc20098 commited on
Commit
65eceb8
ยท
1 Parent(s): 9422de7

Add minimum game count, difficulty multiplier, and updated scoring formula

Browse files

- Require minimum 5 games for benchmark validity (prevents single-game
lucky wins from topping leaderboard)
- Add difficulty multiplier to composite score (Beginner 0.5x to Hard 1.2x)
- Update scoring formula: 50% win / 20% military / 20% economy / 10% speed
- No-combat military efficiency returns 0.0 instead of 0.5

Files changed (3) hide show
  1. evaluate.py +18 -1
  2. evaluate_runner.py +26 -4
  3. tests/test_evaluate.py +49 -1
evaluate.py CHANGED
@@ -46,6 +46,7 @@ RESULTS_COLUMNS = [
46
  "agent_name",
47
  "agent_type",
48
  "opponent",
 
49
  "games",
50
  "win_rate",
51
  "score",
@@ -58,6 +59,14 @@ RESULTS_COLUMNS = [
58
  "replay_url",
59
  ]
60
 
 
 
 
 
 
 
 
 
61
 
62
  def parse_args() -> argparse.Namespace:
63
  parser = argparse.ArgumentParser(
@@ -116,6 +125,9 @@ def parse_args() -> argparse.Namespace:
116
  )
117
  args = parser.parse_args()
118
 
 
 
 
119
  # Auto-detect agent type
120
  if args.agent_type is None:
121
  type_map = {"scripted": "Scripted", "llm": "LLM", "mcp": "Scripted", "custom": "RL"}
@@ -205,7 +217,12 @@ async def run_evaluation(args: argparse.Namespace) -> Dict[str, Any]:
205
  "opponent": args.opponent,
206
  "games": total,
207
  "win_rate": round(100.0 * wins / max(total, 1), 1),
208
- "score": round(compute_composite_score_from_games(game_results), 1),
 
 
 
 
 
209
  "avg_kills": round(sum(g["kills_cost"] for g in game_results) / max(total, 1)),
210
  "avg_deaths": round(sum(g["deaths_cost"] for g in game_results) / max(total, 1)),
211
  "kd_ratio": round(
 
46
  "agent_name",
47
  "agent_type",
48
  "opponent",
49
+ "difficulty",
50
  "games",
51
  "win_rate",
52
  "score",
 
59
  "replay_url",
60
  ]
61
 
62
+ DIFFICULTY_MULTIPLIER = {
63
+ "Beginner": 0.5,
64
+ "Easy": 0.7,
65
+ "Medium": 0.85,
66
+ "Normal": 1.0,
67
+ "Hard": 1.2,
68
+ }
69
+
70
 
71
  def parse_args() -> argparse.Namespace:
72
  parser = argparse.ArgumentParser(
 
125
  )
126
  args = parser.parse_args()
127
 
128
+ if args.games < 5:
129
+ parser.error("Minimum 5 games required for benchmark validity")
130
+
131
  # Auto-detect agent type
132
  if args.agent_type is None:
133
  type_map = {"scripted": "Scripted", "llm": "LLM", "mcp": "Scripted", "custom": "RL"}
 
217
  "opponent": args.opponent,
218
  "games": total,
219
  "win_rate": round(100.0 * wins / max(total, 1), 1),
220
+ "score": round(
221
+ compute_composite_score_from_games(game_results)
222
+ * DIFFICULTY_MULTIPLIER.get(args.opponent, 1.0),
223
+ 1,
224
+ ),
225
+ "difficulty": args.opponent,
226
  "avg_kills": round(sum(g["kills_cost"] for g in game_results) / max(total, 1)),
227
  "avg_deaths": round(sum(g["deaths_cost"] for g in game_results) / max(total, 1)),
228
  "kd_ratio": round(
evaluate_runner.py CHANGED
@@ -17,6 +17,14 @@ DEFAULT_SERVER = "https://openra-rl-openra-rl.hf.space"
17
  MAX_STEPS_PER_GAME = 5000
18
  STEP_TIMEOUT = 60.0
19
 
 
 
 
 
 
 
 
 
20
 
21
  # โ”€โ”€ Scoring (inlined from openra_rl_util/rubrics.py) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
22
 
@@ -46,7 +54,7 @@ def compute_game_metrics(obs: Dict[str, Any]) -> Dict[str, Any]:
46
 
47
 
48
  def compute_composite_score(game_results: List[Dict[str, Any]]) -> float:
49
- """Compute OpenRA-Bench composite score: 50% win + 25% military + 25% economy."""
50
  total = len(game_results)
51
  if total == 0:
52
  return 0.0
@@ -57,7 +65,7 @@ def compute_composite_score(game_results: List[Dict[str, Any]]) -> float:
57
  for g in game_results:
58
  kills, deaths = g["kills_cost"], g["deaths_cost"]
59
  total_cost = kills + deaths
60
- mil_scores.append(kills / total_cost if total_cost > 0 else 0.5)
61
  avg_mil = sum(mil_scores) / total
62
 
63
  econ_scores = []
@@ -66,7 +74,13 @@ def compute_composite_score(game_results: List[Dict[str, Any]]) -> float:
66
  econ_scores.append(assets / (assets + 10000) if assets >= 0 else 0.0)
67
  avg_econ = sum(econ_scores) / total
68
 
69
- return 100.0 * (0.5 * win_rate + 0.25 * avg_mil + 0.25 * avg_econ)
 
 
 
 
 
 
70
 
71
 
72
  # โ”€โ”€ Server communication โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
@@ -136,6 +150,9 @@ async def run_evaluation(
136
  Returns:
137
  Dict with all fields needed for results.csv.
138
  """
 
 
 
139
  game_results: List[Dict[str, Any]] = []
140
 
141
  async with httpx.AsyncClient(timeout=STEP_TIMEOUT) as client:
@@ -154,7 +171,12 @@ async def run_evaluation(
154
  "opponent": opponent,
155
  "games": total,
156
  "win_rate": round(100.0 * wins / max(total, 1), 1),
157
- "score": round(compute_composite_score(game_results), 1),
 
 
 
 
 
158
  "avg_kills": round(sum(g["kills_cost"] for g in game_results) / max(total, 1)),
159
  "avg_deaths": round(sum(g["deaths_cost"] for g in game_results) / max(total, 1)),
160
  "kd_ratio": round(
 
17
  MAX_STEPS_PER_GAME = 5000
18
  STEP_TIMEOUT = 60.0
19
 
20
+ DIFFICULTY_MULTIPLIER = {
21
+ "Beginner": 0.5,
22
+ "Easy": 0.7,
23
+ "Medium": 0.85,
24
+ "Normal": 1.0,
25
+ "Hard": 1.2,
26
+ }
27
+
28
 
29
  # โ”€โ”€ Scoring (inlined from openra_rl_util/rubrics.py) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
30
 
 
54
 
55
 
56
  def compute_composite_score(game_results: List[Dict[str, Any]]) -> float:
57
+ """Compute OpenRA-Bench composite score: 50% win + 20% military + 20% economy + 10% speed."""
58
  total = len(game_results)
59
  if total == 0:
60
  return 0.0
 
65
  for g in game_results:
66
  kills, deaths = g["kills_cost"], g["deaths_cost"]
67
  total_cost = kills + deaths
68
+ mil_scores.append(kills / total_cost if total_cost > 0 else 0.0)
69
  avg_mil = sum(mil_scores) / total
70
 
71
  econ_scores = []
 
74
  econ_scores.append(assets / (assets + 10000) if assets >= 0 else 0.0)
75
  avg_econ = sum(econ_scores) / total
76
 
77
+ speed_scores = []
78
+ for g in game_results:
79
+ ticks = g.get("ticks", 0)
80
+ speed_scores.append(1.0 / (1.0 + ticks / 3000))
81
+ avg_speed = sum(speed_scores) / total
82
+
83
+ return 100.0 * (0.5 * win_rate + 0.20 * avg_mil + 0.20 * avg_econ + 0.10 * avg_speed)
84
 
85
 
86
  # โ”€โ”€ Server communication โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
150
  Returns:
151
  Dict with all fields needed for results.csv.
152
  """
153
+ if num_games < 5:
154
+ raise ValueError("Minimum 5 games required for benchmark validity")
155
+
156
  game_results: List[Dict[str, Any]] = []
157
 
158
  async with httpx.AsyncClient(timeout=STEP_TIMEOUT) as client:
 
171
  "opponent": opponent,
172
  "games": total,
173
  "win_rate": round(100.0 * wins / max(total, 1), 1),
174
+ "score": round(
175
+ compute_composite_score(game_results)
176
+ * DIFFICULTY_MULTIPLIER.get(opponent, 1.0),
177
+ 1,
178
+ ),
179
+ "difficulty": opponent,
180
  "avg_kills": round(sum(g["kills_cost"] for g in game_results) / max(total, 1)),
181
  "avg_deaths": round(sum(g["deaths_cost"] for g in game_results) / max(total, 1)),
182
  "kd_ratio": round(
tests/test_evaluate.py CHANGED
@@ -151,7 +151,7 @@ class TestAppendResults:
151
  assert "score" in RESULTS_COLUMNS
152
  assert "win_rate" in RESULTS_COLUMNS
153
  assert "replay_url" in RESULTS_COLUMNS
154
- assert len(RESULTS_COLUMNS) == 13
155
 
156
 
157
  class TestScoringUsesUtil:
@@ -175,3 +175,51 @@ class TestScoringUsesUtil:
175
  from rubrics import compute_game_metrics
176
  from openra_rl_util.rubrics import compute_game_metrics as util_fn
177
  assert compute_game_metrics is util_fn
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  assert "score" in RESULTS_COLUMNS
152
  assert "win_rate" in RESULTS_COLUMNS
153
  assert "replay_url" in RESULTS_COLUMNS
154
+ assert len(RESULTS_COLUMNS) == 14
155
 
156
 
157
  class TestScoringUsesUtil:
 
175
  from rubrics import compute_game_metrics
176
  from openra_rl_util.rubrics import compute_game_metrics as util_fn
177
  assert compute_game_metrics is util_fn
178
+
179
+
180
+ class TestMinGames:
181
+ """Minimum game count validation."""
182
+
183
+ def test_evaluate_runner_rejects_few_games(self):
184
+ """run_evaluation should reject num_games < 5."""
185
+ import pytest
186
+ from evaluate_runner import run_evaluation
187
+ with pytest.raises(ValueError, match="Minimum 5 games"):
188
+ import asyncio
189
+ asyncio.run(run_evaluation("test", "Normal", 3, "http://fake"))
190
+
191
+
192
+ class TestDifficultyMultiplier:
193
+ """Difficulty scaling for benchmark scores."""
194
+
195
+ def test_hard_scores_higher(self):
196
+ from evaluate_runner import DIFFICULTY_MULTIPLIER
197
+ assert DIFFICULTY_MULTIPLIER["Hard"] > DIFFICULTY_MULTIPLIER["Normal"]
198
+
199
+ def test_beginner_scores_lower(self):
200
+ from evaluate_runner import DIFFICULTY_MULTIPLIER
201
+ assert DIFFICULTY_MULTIPLIER["Beginner"] < DIFFICULTY_MULTIPLIER["Normal"]
202
+
203
+ def test_normal_is_baseline(self):
204
+ from evaluate_runner import DIFFICULTY_MULTIPLIER
205
+ assert DIFFICULTY_MULTIPLIER["Normal"] == 1.0
206
+
207
+
208
+ class TestCompositeScoreUpdate:
209
+ """Updated composite score formula."""
210
+
211
+ def test_no_combat_military_is_zero(self):
212
+ from evaluate_runner import compute_composite_score
213
+ games = [
214
+ {"win": True, "kills_cost": 0, "deaths_cost": 0, "assets_value": 10000, "ticks": 2000},
215
+ ]
216
+ score = compute_composite_score(games)
217
+ # 50%*1.0 + 20%*0.0 + 20%*0.5 + 10%*(1/(1+2/3))
218
+ # = 50 + 0 + 10 + 6.0 = 66.0
219
+ assert abs(score - 66.0) < 1.0
220
+
221
+ def test_speed_bonus_for_fast_games(self):
222
+ from evaluate_runner import compute_composite_score
223
+ fast = [{"win": True, "kills_cost": 5000, "deaths_cost": 1000, "assets_value": 10000, "ticks": 500}]
224
+ slow = [{"win": True, "kills_cost": 5000, "deaths_cost": 1000, "assets_value": 10000, "ticks": 10000}]
225
+ assert compute_composite_score(fast) > compute_composite_score(slow)