Spaces:
Running
Running
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
- evaluate.py +18 -1
- evaluate_runner.py +26 -4
- 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 +
|
| 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.
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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) ==
|
| 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)
|