dsh-eval-guard 0.2.3 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (221) hide show
  1. package/README.md +91 -13
  2. package/assets/commit0/instance.json +30 -0
  3. package/assets/commit0/workspace.zip +0 -0
  4. package/assets/featurebench/hidden/test_regression.py +690 -0
  5. package/assets/featurebench/instance.json +16 -0
  6. package/assets/featurebench/workspace.zip +0 -0
  7. package/assets/swe-bench-pro/hidden/test.patch +36 -0
  8. package/assets/swe-bench-pro/instance.json +10 -0
  9. package/assets/swe-bench-pro/workspace.zip +0 -0
  10. package/eval/README.md +18 -0
  11. package/eval/arena/arena/README.md +21 -0
  12. package/eval/arena/arena/bt.py +104 -0
  13. package/eval/arena/arena/clock.py +15 -0
  14. package/eval/arena/arena/core.py +265 -0
  15. package/eval/arena/arena/run.cmd +24 -0
  16. package/eval/arena/arena/run.sh +11 -0
  17. package/eval/arena/arena/schedule.py +79 -0
  18. package/eval/arena/arena/tournament.py +183 -0
  19. package/eval/arena/bomberman/bots/README.md +16 -0
  20. package/eval/arena/bomberman/bots/deepseek-v4.1-flash.cpp +447 -0
  21. package/eval/arena/bomberman/bots/doubao-seed-2.1.cpp +542 -0
  22. package/eval/arena/bomberman/bots/glm-5.3.cpp +406 -0
  23. package/eval/arena/bomberman/bots/gpt-6-astra.cpp +768 -0
  24. package/eval/arena/bomberman/bots/gpt-6-sol.cpp +444 -0
  25. package/eval/arena/bomberman/bots/kimi-k3.cpp +432 -0
  26. package/eval/arena/bomberman/bots/mimo-v2.6-pro.cpp +529 -0
  27. package/eval/arena/bomberman/bots/opus-5.5.cpp +548 -0
  28. package/eval/arena/bomberman/bots/qwen-3.8-max.cpp +836 -0
  29. package/eval/arena/bomberman/tournament/README.md +11 -0
  30. package/eval/arena/bomberman/tournament/bt.py +56 -0
  31. package/eval/arena/bomberman/tournament/evaluate.py +199 -0
  32. package/eval/arena/bomberman/tournament/run.py +79 -0
  33. package/eval/arena/bomberman/tournament/schedule-2to10.json.gz +0 -0
  34. package/eval/arena/bomberman/workspace/MANIFEST.json +13 -0
  35. package/eval/arena/bomberman/workspace/README.md +29 -0
  36. package/eval/arena/bomberman/workspace/RULES.md +100 -0
  37. package/eval/arena/bomberman/workspace/baseline.cpp +240 -0
  38. package/eval/arena/bomberman/workspace/clock.py +25 -0
  39. package/eval/arena/bomberman/workspace/engine/arena.py +198 -0
  40. package/eval/arena/bomberman/workspace/engine/build.py +35 -0
  41. package/eval/arena/bomberman/workspace/engine/engine.py +256 -0
  42. package/eval/arena/bomberman/workspace/engine/verify_replay.py +24 -0
  43. package/eval/arena/bomberman/workspace/run.cmd +28 -0
  44. package/eval/arena/bomberman/workspace/run.sh +16 -0
  45. package/eval/arena/bomberman/workspace/viewer/index.html +42 -0
  46. package/eval/arena/ctf/bots/README.md +18 -0
  47. package/eval/arena/ctf/bots/deepseek-v4.1-flash.cpp +601 -0
  48. package/eval/arena/ctf/bots/doubao-seed-2.1-turbo.cpp +320 -0
  49. package/eval/arena/ctf/bots/fable-5.1.cpp +634 -0
  50. package/eval/arena/ctf/bots/glm-5.3.cpp +689 -0
  51. package/eval/arena/ctf/bots/gpt-6-astra.cpp +657 -0
  52. package/eval/arena/ctf/bots/gpt-6.1-sol.cpp +725 -0
  53. package/eval/arena/ctf/bots/kimi-k3.cpp +667 -0
  54. package/eval/arena/ctf/bots/mimo-v2.6-pro.cpp +533 -0
  55. package/eval/arena/ctf/bots/opus-5.5.cpp +645 -0
  56. package/eval/arena/ctf/bots/qwen-3.8-max.cpp +726 -0
  57. package/eval/arena/ctf/bots/sonnet-5.5.cpp +569 -0
  58. package/eval/arena/ctf/tournament/README.md +15 -0
  59. package/eval/arena/ctf/tournament/formal-2to8.json +31 -0
  60. package/eval/arena/ctf/tournament/formal-9to15.json +31 -0
  61. package/eval/arena/ctf/workspace/README.md +41 -0
  62. package/eval/arena/ctf/workspace/RULES.md +119 -0
  63. package/eval/arena/ctf/workspace/arena.py +186 -0
  64. package/eval/arena/ctf/workspace/baseline.cpp +257 -0
  65. package/eval/arena/ctf/workspace/clock.py +15 -0
  66. package/eval/arena/ctf/workspace/core.py +265 -0
  67. package/eval/arena/ctf/workspace/engine.py +389 -0
  68. package/eval/arena/ctf/workspace/run.cmd +24 -0
  69. package/eval/arena/ctf/workspace/run.sh +11 -0
  70. package/eval/arena/ctf/workspace/viewer.html +71 -0
  71. package/eval/arena/land/bots/README.md +17 -0
  72. package/eval/arena/land/bots/deepseek-4.1f@workbuddy#1/bot.cpp +608 -0
  73. package/eval/arena/land/bots/glm-5.3@workbuddy#1/bot.cpp +521 -0
  74. package/eval/arena/land/bots/gpt-6-astra@codex#1/bot.cpp +373 -0
  75. package/eval/arena/land/bots/gpt-6-sol@codex#1/bot.cpp +341 -0
  76. package/eval/arena/land/bots/kimi-k3@workbuddy#1/bot.cpp +492 -0
  77. package/eval/arena/land/bots/mimo-2.6-pro@zcode#1/bot.cpp +652 -0
  78. package/eval/arena/land/bots/mystery-model@zcode#1/bot.cpp +466 -0
  79. package/eval/arena/land/bots/opus-5.5@claude-code#1/bot.cpp +630 -0
  80. package/eval/arena/land/bots/sonnet-5@claude-code#1/bot.cpp +619 -0
  81. package/eval/arena/land/tournament/make_replay_page.py +17 -0
  82. package/eval/arena/land/tournament/rate.py +290 -0
  83. package/eval/arena/land/tournament/round_robin.py +172 -0
  84. package/eval/arena/land/tournament/sparring.cpp +356 -0
  85. package/eval/arena/land/workspace/RULES.md +114 -0
  86. package/eval/arena/land/workspace/TASK.md +46 -0
  87. package/eval/arena/land/workspace/anchors/.build/greedy_f2e6d2dea5ca +0 -0
  88. package/eval/arena/land/workspace/anchors/greedy.cpp +194 -0
  89. package/eval/arena/land/workspace/anchors/random.cpp +80 -0
  90. package/eval/arena/land/workspace/clock.py +64 -0
  91. package/eval/arena/land/workspace/engine/build.py +33 -0
  92. package/eval/arena/land/workspace/engine/game.py +273 -0
  93. package/eval/arena/land/workspace/engine/referee.py +308 -0
  94. package/eval/arena/land/workspace/starter/.build/bot_be32b45b4699 +0 -0
  95. package/eval/arena/land/workspace/starter/bot.cpp +82 -0
  96. package/eval/arena/land/workspace/viewer/viewer.html +210 -0
  97. package/eval/arena/tank/bots/README.md +24 -0
  98. package/eval/arena/tank/bots/half1/deepseek-v4.1-flash.cpp +367 -0
  99. package/eval/arena/tank/bots/half1/doubao-seed-evolving.cpp +402 -0
  100. package/eval/arena/tank/bots/half1/gemini-4-argon.cpp +1132 -0
  101. package/eval/arena/tank/bots/half1/glm-5.3.cpp +551 -0
  102. package/eval/arena/tank/bots/half1/gpt-6-astra.cpp +233 -0
  103. package/eval/arena/tank/bots/half1/gpt-6.1-sol-ultrafast.cpp +350 -0
  104. package/eval/arena/tank/bots/half1/gpt-6.1-sol.cpp +78 -0
  105. package/eval/arena/tank/bots/half1/grok-4.7.cpp +1099 -0
  106. package/eval/arena/tank/bots/half1/haiku-5.5.cpp +210 -0
  107. package/eval/arena/tank/bots/half1/kimi-k3.cpp +353 -0
  108. package/eval/arena/tank/bots/half1/mimo-v2.6-pro.cpp +336 -0
  109. package/eval/arena/tank/bots/half1/muse-wonder-full.cpp +560 -0
  110. package/eval/arena/tank/bots/half1/opus-5.5.cpp +435 -0
  111. package/eval/arena/tank/bots/half1/qwen-3.8-max.cpp +537 -0
  112. package/eval/arena/tank/bots/half2/deepseek-v4.1-flash.cpp +425 -0
  113. package/eval/arena/tank/bots/half2/doubao-seed-evolving.cpp +308 -0
  114. package/eval/arena/tank/bots/half2/gemini-4-argon.cpp +1390 -0
  115. package/eval/arena/tank/bots/half2/glm-5.3.cpp +651 -0
  116. package/eval/arena/tank/bots/half2/gpt-6-astra.cpp +241 -0
  117. package/eval/arena/tank/bots/half2/gpt-6.1-sol-ultrafast.cpp +356 -0
  118. package/eval/arena/tank/bots/half2/gpt-6.1-sol.cpp +87 -0
  119. package/eval/arena/tank/bots/half2/grok-4.7.cpp +1205 -0
  120. package/eval/arena/tank/bots/half2/haiku-5.5.cpp +210 -0
  121. package/eval/arena/tank/bots/half2/kimi-k3.cpp +487 -0
  122. package/eval/arena/tank/bots/half2/mimo-v2.6-pro.cpp +336 -0
  123. package/eval/arena/tank/bots/half2/muse-wonder-full.cpp +560 -0
  124. package/eval/arena/tank/bots/half2/opus-5.5.cpp +476 -0
  125. package/eval/arena/tank/bots/half2/qwen-3.8-max.cpp +392 -0
  126. package/eval/arena/tank/tournament/README.md +24 -0
  127. package/eval/arena/tank/tournament/bt.py +218 -0
  128. package/eval/arena/tank/tournament/final.json +202 -0
  129. package/eval/arena/tank/tournament/halftime.json +146 -0
  130. package/eval/arena/tank/tournament/progress.json +911 -0
  131. package/eval/arena/tank/tournament/schedule.py +100 -0
  132. package/eval/arena/tank/tournament/sparring.cpp +928 -0
  133. package/eval/arena/tank/tournament/tournament.py +1426 -0
  134. package/eval/arena/tank/workspace/README.md +169 -0
  135. package/eval/arena/tank/workspace/RULES.md +443 -0
  136. package/eval/arena/tank/workspace/arena.py +862 -0
  137. package/eval/arena/tank/workspace/baseline.cpp +259 -0
  138. package/eval/arena/tank/workspace/clock.py +103 -0
  139. package/eval/arena/tank/workspace/core.py +712 -0
  140. package/eval/arena/tank/workspace/engine.py +573 -0
  141. package/eval/arena/tank/workspace/history/.lock +0 -0
  142. package/eval/arena/tank/workspace/history/20261017-193610-08715287f91f-bot.cpp +109 -0
  143. package/eval/arena/tank/workspace/history/20261017-193619-f00b2d5635fa-gpt-6-astra.cpp +241 -0
  144. package/eval/arena/tank/workspace/history/20261017-193643-63319e0a2b1a-gpt-6_1-sol-ultrafast.cpp +356 -0
  145. package/eval/arena/tank/workspace/history/20261017-193647-41c1341d0a8a-gemini-4-argon.cpp +1390 -0
  146. package/eval/arena/tank/workspace/history/20261017-193652-e34c0f45539a-gpt-6_1-sol.cpp +87 -0
  147. package/eval/arena/tank/workspace/history/20261017-193654-c026348ce96e-opus-5_5.cpp +476 -0
  148. package/eval/arena/tank/workspace/history/20261017-193655-4a21366e94d1-muse-wonder-full.cpp +560 -0
  149. package/eval/arena/tank/workspace/history/20261017-193657-dbc4e9969c52-mimo-v2_6-pro.cpp +336 -0
  150. package/eval/arena/tank/workspace/history/20261017-193703-723cd036eb31-grok-4_7.cpp +1205 -0
  151. package/eval/arena/tank/workspace/history/20261017-193707-1ebbb227be68-deepseek-v4_1-flash.cpp +425 -0
  152. package/eval/arena/tank/workspace/history/20261017-193709-aeb0e7202ad5-doubao-seed-evolving.cpp +308 -0
  153. package/eval/arena/tank/workspace/history/20261017-193710-b21ebeb9bc76-kimi-k3.cpp +487 -0
  154. package/eval/arena/tank/workspace/history/20261017-193712-54cd86531905-haiku-5_5.cpp +210 -0
  155. package/eval/arena/tank/workspace/history/20261017-193714-efdb8e35c0bf-qwen-3_8-max.cpp +392 -0
  156. package/eval/arena/tank/workspace/history/20261017-193715-681423c23fdb-glm-5_3.cpp +651 -0
  157. package/eval/arena/tank/workspace/history/20261017-193717-1a8b3c75181d-sparring.cpp +928 -0
  158. package/eval/arena/tank/workspace/history/20261017-193720-1e0803a541e8-baseline.cpp +259 -0
  159. package/eval/arena/tank/workspace/history/log.jsonl +103 -0
  160. package/eval/arena/tank/workspace/judge/cycles.hpp +53 -0
  161. package/eval/arena/tank/workspace/judge/fast.cpp +337 -0
  162. package/eval/arena/tank/workspace/judge/referee.cpp +125 -0
  163. package/eval/arena/tank/workspace/judge/speed.cpp +99 -0
  164. package/eval/arena/tank/workspace/run.cmd +31 -0
  165. package/eval/arena/tank/workspace/run.sh +23 -0
  166. package/eval/arena/tank/workspace/tank.hpp +1762 -0
  167. package/eval/arena/tank/workspace/tank_main.cpp +215 -0
  168. package/eval/arena/tank/workspace/viewer.html +1150 -0
  169. package/eval/arena/warehouse/workspace/SPEC.md +130 -0
  170. package/eval/arena/warehouse/workspace/TASK.md +71 -0
  171. package/eval/arena/warehouse/workspace/baseline/common.h +88 -0
  172. package/eval/arena/warehouse/workspace/baseline/main.cpp +27 -0
  173. package/eval/arena/warehouse/workspace/baseline/old_stuff.cpp +87 -0
  174. package/eval/arena/warehouse/workspace/baseline/sim.cpp +427 -0
  175. package/eval/arena/warehouse/workspace/baseline/util.cpp +220 -0
  176. package/eval/arena/warehouse/workspace/clock.py +42 -0
  177. package/eval/arena/warehouse/workspace/src/common.h +88 -0
  178. package/eval/arena/warehouse/workspace/src/main.cpp +27 -0
  179. package/eval/arena/warehouse/workspace/src/old_stuff.cpp +87 -0
  180. package/eval/arena/warehouse/workspace/src/sim.cpp +427 -0
  181. package/eval/arena/warehouse/workspace/src/util.cpp +220 -0
  182. package/eval/arena/warehouse/workspace/tools/bench.py +119 -0
  183. package/eval/arena/warehouse/workspace/tools/check_input.py +161 -0
  184. package/eval/arena/warehouse/workspace/tools/gen.py +132 -0
  185. package/eval/arena/warehouse/workspace/tools/measure.py +107 -0
  186. package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.in +1420 -0
  187. package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.out +9162 -0
  188. package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.in +1465 -0
  189. package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.out +3439 -0
  190. package/eval/arena/warehouse/workspace/workloads/public/pub_flood.in +2757 -0
  191. package/eval/arena/warehouse/workspace/workloads/public/pub_flood.out +3126 -0
  192. package/eval/arena/warehouse/workspace/workloads/public/pub_small.in +731 -0
  193. package/eval/arena/warehouse/workspace/workloads/public/pub_small.out +2578 -0
  194. package/eval/arena/warehouse/workspace/workloads/public/pub_wide.in +528 -0
  195. package/eval/arena/warehouse/workspace/workloads/public/pub_wide.out +2026 -0
  196. package/eval/bomberman.py +20 -0
  197. package/eval/commit0.py +21 -0
  198. package/eval/common.py +61 -0
  199. package/eval/ctf.py +20 -0
  200. package/eval/featurebench.py +25 -0
  201. package/eval/land.py +20 -0
  202. package/eval/legacy/bomberman-prices.json +4 -0
  203. package/eval/legacy/bomberman.py +161 -0
  204. package/eval/legacy/commit0.py +91 -0
  205. package/eval/legacy/ctf-prices.json +4 -0
  206. package/eval/legacy/ctf.py +178 -0
  207. package/eval/legacy/featurebench.py +103 -0
  208. package/eval/legacy/land-prices.json +4 -0
  209. package/eval/legacy/land.py +177 -0
  210. package/eval/legacy/swe_bench_pro.py +116 -0
  211. package/eval/legacy/tank-prices.json +3 -0
  212. package/eval/legacy/tank.py +408 -0
  213. package/eval/legacy/warehouse-prices.json +4 -0
  214. package/eval/legacy/warehouse.py +365 -0
  215. package/eval/swe-bench-pro.py +25 -0
  216. package/eval/tank.py +20 -0
  217. package/eval/warehouse.py +20 -0
  218. package/lib/client/index.js +376 -63
  219. package/lib/client/index.js.map +1 -1
  220. package/lib/index.js +684 -27
  221. package/package.json +2 -1
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python3
2
+ """Single-workspace tank evaluator wrapper."""
3
+ from __future__ import annotations
4
+ import shutil, tempfile
5
+ from pathlib import Path
6
+ from common import EVAL_ROOT, candidate_row, emit, fail, parse_workspace, run_legacy
7
+
8
+ workspace = parse_workspace()
9
+ with tempfile.TemporaryDirectory(prefix='bomber-eval-') as td:
10
+ temp = Path(td)
11
+ bot = workspace / 'submission' / 'bot.cpp'
12
+ if not bot.is_file():
13
+ raise SystemExit(fail({'task': 'bomberman', 'ok': False, 'error': 'missing submission/bot.cpp'}))
14
+ (temp / 'candidate').mkdir()
15
+ shutil.copy2(bot, temp / 'candidate' / 'bot.cpp')
16
+ result = run_legacy(EVAL_ROOT / 'legacy' / 'bomberman.py', ['--submissions', str(temp), '--task', str(EVAL_ROOT / 'arena' / 'bomberman'), '--tier', 'quick'], temp / 'out')
17
+ if not result.get('ok'):
18
+ raise SystemExit(fail({'task': 'bomberman', 'ok': False, 'error': result.get('error', 'evaluation failed')}))
19
+ row = candidate_row(result['summary'])
20
+ emit({'task': 'bomberman', 'ok': True, 'score': row.get('score', row.get('rating', row.get('theta', 0))), 'rating': row.get('rating', row.get('theta', 0)), 'wins': row.get('wins', 0), 'losses': row.get('losses', 0), 'draws': row.get('draws', 0)})
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env python3
2
+ """Single-workspace Commit0 evaluator wrapper."""
3
+ from __future__ import annotations
4
+ import os, shutil, tempfile
5
+ from pathlib import Path
6
+ from common import EVAL_ROOT, candidate_row, clean_copy, emit, fail, parse_workspace, run_legacy
7
+
8
+ workspace = parse_workspace()
9
+ with tempfile.TemporaryDirectory(prefix='commit0-eval-') as td:
10
+ temp = Path(td)
11
+ candidate = temp / 'candidate' / 'workspace'
12
+ candidate.parent.mkdir(parents=True)
13
+ clean_copy(workspace, candidate, ignore_venv=True)
14
+ venv = workspace / '.venv'
15
+ if venv.is_dir():
16
+ os.symlink(venv, candidate / '.venv')
17
+ result = run_legacy(EVAL_ROOT / 'legacy' / 'commit0.py', ['--submissions', str(temp)], temp / 'out')
18
+ if not result.get('ok'):
19
+ raise SystemExit(fail({'task': 'commit0', 'ok': False, 'error': result.get('error', 'evaluation failed')}))
20
+ row = candidate_row(result['summary'])
21
+ emit({'task': 'commit0', 'ok': True, 'score': row.get('score', 0), 'passed': row.get('passed', 0), 'failed': row.get('failed', 0), 'errors': row.get('errors', 0), 'status': row.get('status')})
package/eval/common.py ADDED
@@ -0,0 +1,61 @@
1
+ """Shared helpers for single-workspace evaluator wrappers."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import tempfile
11
+ from pathlib import Path
12
+
13
+ EVAL_ROOT = Path(__file__).resolve().parent
14
+
15
+
16
+ def parse_workspace() -> Path:
17
+ parser = argparse.ArgumentParser(description='single-workspace evaluator')
18
+ parser.add_argument('--workspace', required=True)
19
+ args = parser.parse_args()
20
+ return Path(args.workspace).resolve()
21
+
22
+
23
+ def emit(payload: dict) -> None:
24
+ target = os.environ.get('EVAL_RESULT_FILE')
25
+ if target is None or target == '':
26
+ print(json.dumps(payload, ensure_ascii=False, indent=2))
27
+ return
28
+ Path(target).write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding='utf-8')
29
+
30
+
31
+ def clean_copy(source: Path, target: Path, ignore_venv: bool = False) -> None:
32
+ def ignore(directory: str, names: list[str]) -> set[str]:
33
+ if ignore_venv and Path(directory) == source:
34
+ return {name for name in names if name == '.venv'}
35
+ return set()
36
+ shutil.copytree(source, target, symlinks=True, ignore=ignore)
37
+
38
+
39
+ def run_legacy(script: Path, args: list[str], out: Path) -> dict:
40
+ cmd = [sys.executable, '-X', 'utf8', str(script), *args, '--out', str(out)]
41
+ env = {k: v for k, v in os.environ.items() if k not in {'PYTHONPATH', 'PYTHONHOME'}}
42
+ completed = subprocess.run(cmd, cwd=str(EVAL_ROOT), capture_output=True, text=True, encoding='utf-8', errors='replace', env=env, timeout=1800)
43
+ if completed.returncode != 0:
44
+ return {'ok': False, 'error': (completed.stderr or completed.stdout or 'evaluator failed')[-4000:]}
45
+ summary = out / 'summary.json'
46
+ if not summary.is_file():
47
+ return {'ok': False, 'error': 'summary.json missing'}
48
+ return {'ok': True, 'summary': json.loads(summary.read_text(encoding='utf-8'))}
49
+
50
+
51
+ def candidate_row(summary: dict, name: str = 'candidate') -> dict:
52
+ rows = summary.get('results') or summary.get('participants') or summary.get('rows') or []
53
+ for row in rows:
54
+ if row.get('name') == name:
55
+ return row
56
+ return {}
57
+
58
+
59
+ def fail(payload: dict) -> int:
60
+ emit(payload)
61
+ return 1
package/eval/ctf.py ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python3
2
+ """Single-workspace tank evaluator wrapper."""
3
+ from __future__ import annotations
4
+ import shutil, tempfile
5
+ from pathlib import Path
6
+ from common import EVAL_ROOT, candidate_row, emit, fail, parse_workspace, run_legacy
7
+
8
+ workspace = parse_workspace()
9
+ with tempfile.TemporaryDirectory(prefix='ctf-eval-') as td:
10
+ temp = Path(td)
11
+ bot = workspace / 'submission' / 'bot.cpp'
12
+ if not bot.is_file():
13
+ raise SystemExit(fail({'task': 'ctf', 'ok': False, 'error': 'missing submission/bot.cpp'}))
14
+ (temp / 'candidate').mkdir()
15
+ shutil.copy2(bot, temp / 'candidate' / 'bot.cpp')
16
+ result = run_legacy(EVAL_ROOT / 'legacy' / 'ctf.py', ['--submissions', str(temp), '--task', str(EVAL_ROOT / 'arena' / 'ctf'), '--tier', 'quick'], temp / 'out')
17
+ if not result.get('ok'):
18
+ raise SystemExit(fail({'task': 'ctf', 'ok': False, 'error': result.get('error', 'evaluation failed')}))
19
+ row = candidate_row(result['summary'])
20
+ emit({'task': 'ctf', 'ok': True, 'score': row.get('score', row.get('rating', row.get('theta', 0))), 'rating': row.get('rating', row.get('theta', 0)), 'wins': row.get('wins', 0), 'losses': row.get('losses', 0), 'draws': row.get('draws', 0)})
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+ """Single-workspace SWE-Bench Pro evaluator wrapper."""
3
+ from __future__ import annotations
4
+ import os, tempfile
5
+ from pathlib import Path
6
+ from common import EVAL_ROOT, candidate_row, clean_copy, emit, fail, parse_workspace, run_legacy
7
+
8
+ workspace = parse_workspace()
9
+ with tempfile.TemporaryDirectory(prefix='feature-eval-') as td:
10
+ temp = Path(td)
11
+ candidate = temp / 'candidate' / 'workspace'
12
+ candidate.parent.mkdir(parents=True)
13
+ clean_copy(workspace, candidate, ignore_venv=True)
14
+ venv = workspace / '.venv'
15
+ if venv.is_dir():
16
+ os.symlink(venv, candidate / '.venv')
17
+ result = run_legacy(EVAL_ROOT / 'legacy' / 'featurebench.py', ['--submissions', str(temp)], temp / 'out')
18
+ if not result.get('ok'):
19
+ raise SystemExit(fail({'task': 'featurebench', 'ok': False, 'error': result.get('error', 'evaluation failed')}))
20
+ row = candidate_row(result['summary'])
21
+ passed = row.get('passed', 0)
22
+ failed = row.get('failed', 0)
23
+ errors = row.get('errors', 0)
24
+ resolved = failed == 0 and errors == 0 and passed > 0
25
+ emit({'task': 'featurebench', 'ok': True, 'score': 100.0 if resolved else 0.0, 'resolved': resolved, 'passRate': row.get('score', 0), 'passed': passed, 'failed': failed, 'errors': errors, 'status': row.get('status')})
package/eval/land.py ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env python3
2
+ """Single-workspace tank evaluator wrapper."""
3
+ from __future__ import annotations
4
+ import shutil, tempfile
5
+ from pathlib import Path
6
+ from common import EVAL_ROOT, candidate_row, emit, fail, parse_workspace, run_legacy
7
+
8
+ workspace = parse_workspace()
9
+ with tempfile.TemporaryDirectory(prefix='land-eval-') as td:
10
+ temp = Path(td)
11
+ bot = workspace / 'submission' / 'bot.cpp'
12
+ if not bot.is_file():
13
+ raise SystemExit(fail({'task': 'land', 'ok': False, 'error': 'missing submission/bot.cpp'}))
14
+ (temp / 'candidate').mkdir()
15
+ shutil.copy2(bot, temp / 'candidate' / 'bot.cpp')
16
+ result = run_legacy(EVAL_ROOT / 'legacy' / 'land.py', ['--submissions', str(temp), '--task', str(EVAL_ROOT / 'arena' / 'land'), '--tier', 'quick'], temp / 'out')
17
+ if not result.get('ok'):
18
+ raise SystemExit(fail({'task': 'land', 'ok': False, 'error': result.get('error', 'evaluation failed')}))
19
+ row = candidate_row(result['summary'])
20
+ emit({'task': 'land', 'ok': True, 'score': row.get('score', row.get('rating', row.get('theta', 0))), 'rating': row.get('rating', row.get('theta', 0)), 'wins': row.get('wins', 0), 'losses': row.get('losses', 0), 'draws': row.get('draws', 0)})
@@ -0,0 +1,4 @@
1
+ {
2
+ "USD": {},
3
+ "CNY": {}
4
+ }
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env python3
2
+ """Independent evaluation for bomberman bot submissions.
3
+
4
+ Reads submissions/<name>/{bot.cpp,run/,cost.json}, then runs the official
5
+ tournament/run.py with the baseline anchor and all submissions. A missing
6
+ run/ directory is fine: only the bot is evaluated.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import re
13
+ import subprocess
14
+ import sys
15
+ import time
16
+ from pathlib import Path
17
+
18
+ ROOT = Path(__file__).resolve().parent
19
+ DEFAULT_TASK = ROOT.parent / "meow-ai-arena" / "bomberman"
20
+ TIERS = {"quick": (["2"], 1), "full": (["2", "5", "10"], 1), "formal": (["2", "5", "10"], 3)}
21
+ STATIC_PATTERNS = [
22
+ (re.compile(r"#\s*include\s*<(fstream|filesystem|unistd\.h|windows\.h|winsock2?\.h|sys/|netinet/|arpa/|dlfcn\.h)>"), "可疑头文件"),
23
+ (re.compile(r"\b(fopen|freopen|ifstream|ofstream|filesystem::)\b"), "文件 IO"),
24
+ (re.compile(r"\b(system|popen|execv?p?e?|fork|posix_spawn|CreateProcess)\b"), "启动进程"),
25
+ (re.compile(r"\b(socket|connect|bind|listen|accept|sendto|recvfrom|getaddrinfo)\b"), "网络调用"),
26
+ (re.compile(r"\b(std::thread|pthread_create|_beginthread)\b"), "线程"),
27
+ ]
28
+
29
+
30
+ def load_json(path: Path):
31
+ try:
32
+ return json.loads(path.read_text(encoding="utf-8"))
33
+ except Exception:
34
+ return None
35
+
36
+
37
+ def static_scan(source: Path) -> list:
38
+ findings = []
39
+ for number, line in enumerate(source.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
40
+ for pattern, label in STATIC_PATTERNS:
41
+ if pattern.search(line):
42
+ findings.append({"line": number, "kind": label, "text": line.strip()[:160]})
43
+ return findings
44
+
45
+
46
+ def money(directory: Path, run: dict, usage: dict, cost: dict, prices: dict):
47
+ if isinstance(cost, dict):
48
+ try:
49
+ return float(cost.get("total", 0) or 0), "manual", str(cost.get("currency", "USD"))
50
+ except (TypeError, ValueError):
51
+ pass
52
+ if not usage:
53
+ return 0.0, "missing", "USD"
54
+ provider = str(run.get("provider") or "")
55
+ model = str(run.get("model") or "")
56
+ table = prices.get(provider + "/" + model) or prices.get(model) or {}
57
+ if not table:
58
+ return 0.0, "missing", "USD"
59
+ def rate(name: str) -> float:
60
+ return float(table.get(name, 0) or 0) / 1_000_000.0
61
+ total = float(usage.get("inputTokens", 0) or 0) * rate("input")
62
+ total += float(usage.get("outputTokens", 0) or 0) * rate("output")
63
+ total += float(usage.get("cacheReadTokens", 0) or 0) * rate("cacheRead")
64
+ total += float(usage.get("cacheWriteTokens", 0) or 0) * rate("cacheWrite")
65
+ return total, "tokens", "USD"
66
+
67
+
68
+ def main() -> int:
69
+ parser = argparse.ArgumentParser(description="bomberman-eval")
70
+ parser.add_argument("--tier", choices=sorted(TIERS), default="quick")
71
+ parser.add_argument("--submissions", default=str(ROOT / "submissions"))
72
+ parser.add_argument("--task", default=str(DEFAULT_TASK))
73
+ parser.add_argument("--with-history", action="store_true")
74
+ parser.add_argument("--out", default="")
75
+ parser.add_argument("--workers", type=int, default=2)
76
+ args = parser.parse_args()
77
+
78
+ task = Path(args.task).resolve()
79
+ workspace = task / "workspace"
80
+ if not (task / "tournament" / "run.py").is_file():
81
+ print("bomberman task not found:", task, file=sys.stderr)
82
+ return 2
83
+ submissions = Path(args.submissions).resolve()
84
+ dirs = [p for p in sorted(submissions.iterdir()) if p.is_dir() and (p / "bot.cpp").is_file()] if submissions.is_dir() else []
85
+ if not dirs:
86
+ print("no submissions found under", submissions, file=sys.stderr)
87
+ return 2
88
+
89
+ out = Path(args.out).resolve() if args.out else ROOT / "results" / time.strftime("%Y%m%d-%H%M%S")
90
+ out.mkdir(parents=True, exist_ok=True)
91
+ run_dir = out / "tournament"
92
+ names = {}
93
+ programs = {"Base": str(workspace / "baseline.cpp")}
94
+ bots = ["Base=" + programs["Base"]]
95
+ for directory in dirs:
96
+ name = re.sub(r"[^A-Za-z0-9_.-]+", "-", directory.name).strip("-") or "submission"
97
+ names[name] = directory
98
+ programs[name] = str(directory / "bot.cpp")
99
+ bots.append(name + "=" + programs[name])
100
+ if args.with_history:
101
+ for bot in sorted((task / "bots").glob("*.cpp")):
102
+ history = "history-" + bot.stem
103
+ programs[history] = str(bot)
104
+ bots.append(history + "=" + programs[history])
105
+
106
+ sizes, seeds = TIERS[args.tier]
107
+ cmd = [sys.executable, "-X", "utf8", str(task / "tournament" / "run.py"), "--output", str(run_dir),
108
+ "--bots", *bots, "--sizes", *sizes, "--seeds", str(seeds), "--workers", str(max(1, args.workers))]
109
+ print("tier=%s sizes=%s seeds=%d bots=%d" % (args.tier, sizes, seeds, len(bots)), flush=True)
110
+ completed = subprocess.run(cmd, capture_output=True, text=True, encoding="utf-8", errors="replace")
111
+ if completed.returncode != 0:
112
+ print(completed.stdout[-4000:], completed.stderr[-4000:], file=sys.stderr)
113
+ return 2
114
+ ratings_doc = load_json(run_dir / "bt.json") or {}
115
+ ratings = ratings_doc.get("ratings", {})
116
+ prices = load_json(ROOT / "prices.json") or {}
117
+
118
+ rows = []
119
+ for name, program in programs.items():
120
+ directory = names.get(name)
121
+ if directory is not None:
122
+ sub_run = directory / "run"
123
+ run = load_json(sub_run / "run.json") or {}
124
+ usage = load_json(sub_run / "usage.json") or {}
125
+ cost = load_json(directory / "cost.json") or load_json(sub_run / "cost.json")
126
+ amount, source, currency = money(directory, run, usage, cost, prices)
127
+ violations = 0
128
+ if (sub_run / "violations.jsonl").is_file():
129
+ violations = len([line for line in (sub_run / "violations.jsonl").read_text(encoding="utf-8").splitlines() if line.strip()])
130
+ scan = static_scan(directory / "bot.cpp")
131
+ kind = "submission"
132
+ else:
133
+ run = {}
134
+ amount, source, currency = 0.0, "missing", "USD"
135
+ violations = 0
136
+ scan = static_scan(Path(program)) if Path(program).suffix == ".cpp" else []
137
+ kind = "baseline" if name == "Base" else "history"
138
+ rows.append({
139
+ "name": name,
140
+ "kind": kind,
141
+ "rating": round(float(ratings.get(name, 0) or 0), 2),
142
+ "cost": round(amount, 4), "costSource": source, "currency": currency,
143
+ "violations": violations,
144
+ "static": scan,
145
+ "run": run,
146
+ })
147
+ rows.sort(key=lambda row: (-row["rating"], row["name"]))
148
+ payload = {"generatedAt": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "tier": args.tier, "sizes": sizes, "seeds": seeds, "results": rows}
149
+ (out / "summary.json").write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
150
+ lines = ["# bomberman-eval 结果", "", "- 时间:%s" % payload["generatedAt"], "- 档位:%s(人数 %s,种子数 %d)" % (args.tier, ",".join(sizes), seeds), "", "## 排行榜", "", "| 名次 | 名称 | BT 实力值 | 成本 | 价格来源 | 违规 | 静态扫描 |", "|---:|---|---:|---:|---|---:|---|"]
151
+ for rank, row in enumerate(rows, 1):
152
+ scan = "未发现" if not row["static"] else "%d 项" % len(row["static"])
153
+ lines.append("| %d | %s | %+.2f | %.4f %s | %s | %d | %s |" % (rank, row["name"], row["rating"], row["cost"], row["currency"], row["costSource"], row["violations"], scan))
154
+ lines += ["", "## 说明", "", "- 使用官方 tournament/run.py 与 BT 评分;Base(baseline)为 0 分锚点。", "- 没有 run/ 时仍评测 bot,只是成本/用量显示 missing。", "- 静态扫描只报告,不取消资格。"]
155
+ (out / "summary.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
156
+ print("wrote", out / "summary.md")
157
+ return 0
158
+
159
+
160
+ if __name__ == "__main__":
161
+ raise SystemExit(main())
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env python3
2
+ """Evaluate Commit0 single-library submissions.
3
+
4
+ Submission layout:
5
+ submissions/<name>/workspace/ (copy of the agent workspace, including .venv)
6
+
7
+ Score is the unit-test pass rate. Missing run/ statistics are skipped.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import re
14
+ import subprocess
15
+ import sys
16
+ import time
17
+ from pathlib import Path
18
+
19
+ ROOT = Path(__file__).resolve().parent
20
+ SUMMARY = re.compile(r"(\d+) passed")
21
+ FAILED = re.compile(r"(\d+) failed")
22
+ ERRORS = re.compile(r"(\d+) error")
23
+
24
+
25
+ def load_json(path: Path):
26
+ try:
27
+ return json.loads(path.read_text(encoding="utf-8"))
28
+ except Exception:
29
+ return None
30
+
31
+
32
+ def find_workspace(directory: Path) -> Path | None:
33
+ for candidate in (directory / "workspace", directory):
34
+ if (candidate / "tests").is_dir() and (candidate / "TASK.md").is_file():
35
+ return candidate
36
+ return None
37
+
38
+
39
+ def run_tests(workspace: Path) -> tuple[int, int, int, str]:
40
+ venv = workspace / ".venv" / "bin" / "python"
41
+ python = str(venv) if venv.is_file() else "python3"
42
+ env = {k: v for k, v in __import__("os").environ.items() if k not in {"PYTHONPATH", "PYTHONHOME"}}
43
+ completed = subprocess.run([python, "-m", "pytest", "tests", "-q", "--tb=no"], cwd=workspace,
44
+ capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=1800, env=env)
45
+ output = (completed.stdout or "") + "\n" + (completed.stderr or "")
46
+ passed = int(SUMMARY.search(output).group(1)) if SUMMARY.search(output) else 0
47
+ failed = int(FAILED.search(output).group(1)) if FAILED.search(output) else 0
48
+ errors = int(ERRORS.search(output).group(1)) if ERRORS.search(output) else 0
49
+ if completed.returncode != 0 and passed == 0 and failed == 0 and errors == 0:
50
+ errors = 1
51
+ return passed, failed, errors, output[-3000:]
52
+
53
+
54
+ def main() -> int:
55
+ parser = argparse.ArgumentParser(description="commit0-eval")
56
+ parser.add_argument("--submissions", default=str(ROOT / "submissions"))
57
+ parser.add_argument("--out", default="")
58
+ args = parser.parse_args()
59
+ root = Path(args.submissions).resolve()
60
+ directories = [p for p in sorted(root.iterdir()) if p.is_dir()] if root.is_dir() else []
61
+ if not directories:
62
+ print("no submissions under", root, file=sys.stderr)
63
+ return 2
64
+ out = Path(args.out).resolve() if args.out else ROOT / "results" / time.strftime("%Y%m%d-%H%M%S")
65
+ out.mkdir(parents=True, exist_ok=True)
66
+ rows = []
67
+ for directory in directories:
68
+ workspace = find_workspace(directory)
69
+ if workspace is None:
70
+ rows.append({"name": directory.name, "status": "missing-workspace", "score": 0.0, "passed": 0, "failed": 0, "errors": 0})
71
+ continue
72
+ try:
73
+ passed, failed, errors, detail = run_tests(workspace)
74
+ total = passed + failed + errors
75
+ score = 100.0 * passed / total if total else 0.0
76
+ rows.append({"name": directory.name, "status": "ok", "score": round(score, 2), "passed": passed, "failed": failed, "errors": errors, "detail": detail})
77
+ except Exception as error:
78
+ rows.append({"name": directory.name, "status": "error", "score": 0.0, "passed": 0, "failed": 0, "errors": 1, "detail": str(error)})
79
+ rows.sort(key=lambda row: (-row["score"], row["name"]))
80
+ payload = {"generatedAt": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "benchmark": "commit0", "instance": "commit-0/tinydb", "results": rows}
81
+ (out / "summary.json").write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
82
+ lines = ["# Commit0 · tinydb 排行", "", "- 指标:单元测试通过率(passed / passed+failed+errors)", "", "| 名次 | 名称 | 得分 | 通过 | 失败 | 错误 | 状态 |", "|---:|---|---:|---:|---:|---:|---|"]
83
+ for rank, row in enumerate(rows, 1):
84
+ lines.append("| %d | %s | %.1f | %d | %d | %d | %s |" % (rank, row["name"], row["score"], row["passed"], row["failed"], row["errors"], row["status"]))
85
+ (out / "summary.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
86
+ print("wrote", out / "summary.md")
87
+ return 0
88
+
89
+
90
+ if __name__ == "__main__":
91
+ raise SystemExit(main())
@@ -0,0 +1,4 @@
1
+ {
2
+ "USD": {},
3
+ "CNY": {}
4
+ }
@@ -0,0 +1,178 @@
1
+ #!/usr/bin/env python3
2
+ """Independent evaluation for ctf (capture the flag) bot submissions.
3
+
4
+ Reads submissions/<name>/{bot.cpp,run/,cost.json}, then runs the official
5
+ arena plan/run/rate over submissions plus the baseline anchor. A missing
6
+ run/ directory is fine: only the bot is evaluated.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import argparse
11
+ import json
12
+ import re
13
+ import subprocess
14
+ import sys
15
+ import time
16
+ from pathlib import Path
17
+
18
+ ROOT = Path(__file__).resolve().parent
19
+ DEFAULT_TASK = ROOT.parent / "meow-ai-arena" / "ctf"
20
+ TIERS = {
21
+ "quick": ([2, 3], 2, 2),
22
+ "full": ([2, 3, 5, 8], 3, 4),
23
+ "formal": ([2, 3, 4, 5, 6, 8, 10, 13], 8, 7),
24
+ }
25
+ STATIC_PATTERNS = [
26
+ (re.compile(r"#\s*include\s*<(fstream|filesystem|unistd\.h|windows\.h|winsock2?\.h|sys/|netinet/|arpa/|dlfcn\.h)>"), "可疑头文件"),
27
+ (re.compile(r"\b(fopen|freopen|ifstream|ofstream|filesystem::)\b"), "文件 IO"),
28
+ (re.compile(r"\b(system|popen|execv?p?e?|fork|posix_spawn|CreateProcess)\b"), "启动进程"),
29
+ (re.compile(r"\b(socket|connect|bind|listen|accept|sendto|recvfrom|getaddrinfo)\b"), "网络调用"),
30
+ (re.compile(r"\b(std::thread|pthread_create|_beginthread)\b"), "线程"),
31
+ ]
32
+
33
+
34
+ def load_json(path: Path):
35
+ try:
36
+ return json.loads(path.read_text(encoding="utf-8"))
37
+ except Exception:
38
+ return None
39
+
40
+
41
+ def static_scan(source: Path) -> list:
42
+ findings = []
43
+ for number, line in enumerate(source.read_text(encoding="utf-8", errors="replace").splitlines(), 1):
44
+ for pattern, label in STATIC_PATTERNS:
45
+ if pattern.search(line):
46
+ findings.append({"line": number, "kind": label, "text": line.strip()[:160]})
47
+ return findings
48
+
49
+
50
+ def money(directory: Path, run: dict, usage: dict, cost: dict, prices: dict):
51
+ if isinstance(cost, dict):
52
+ try:
53
+ return float(cost.get("total", 0) or 0), "manual", str(cost.get("currency", "USD"))
54
+ except (TypeError, ValueError):
55
+ pass
56
+ if not usage:
57
+ return 0.0, "missing", "USD"
58
+ provider = str(run.get("provider") or "")
59
+ model = str(run.get("model") or "")
60
+ table = prices.get(provider + "/" + model) or prices.get(model) or {}
61
+ if not table:
62
+ return 0.0, "missing", "USD"
63
+ def rate(name: str) -> float:
64
+ return float(table.get(name, 0) or 0) / 1_000_000.0
65
+ total = float(usage.get("inputTokens", 0) or 0) * rate("input")
66
+ total += float(usage.get("outputTokens", 0) or 0) * rate("output")
67
+ total += float(usage.get("cacheReadTokens", 0) or 0) * rate("cacheRead")
68
+ total += float(usage.get("cacheWriteTokens", 0) or 0) * rate("cacheWrite")
69
+ return total, "tokens", "USD"
70
+
71
+
72
+ def main() -> int:
73
+ parser = argparse.ArgumentParser(description="ctf-eval")
74
+ parser.add_argument("--tier", choices=sorted(TIERS), default="quick")
75
+ parser.add_argument("--submissions", default=str(ROOT / "submissions"))
76
+ parser.add_argument("--task", default=str(DEFAULT_TASK))
77
+ parser.add_argument("--with-history", action="store_true")
78
+ parser.add_argument("--out", default="")
79
+ parser.add_argument("--workers", type=int, default=2)
80
+ args = parser.parse_args()
81
+
82
+ task = Path(args.task).resolve()
83
+ workspace = task / "workspace"
84
+ arena = task.parent / "arena"
85
+ if not (arena / "tournament.py").is_file() or not (workspace / "engine.py").is_file():
86
+ print("ctf task not found:", task, file=sys.stderr)
87
+ return 2
88
+ submissions = Path(args.submissions).resolve()
89
+ dirs = [p for p in sorted(submissions.iterdir()) if p.is_dir() and (p / "bot.cpp").is_file()] if submissions.is_dir() else []
90
+ if not dirs:
91
+ print("no submissions found under", submissions, file=sys.stderr)
92
+ return 2
93
+
94
+ out = Path(args.out).resolve() if args.out else ROOT / "results" / time.strftime("%Y%m%d-%H%M%S")
95
+ games_dir = out / "games"
96
+ out.mkdir(parents=True, exist_ok=True)
97
+ players = {"Base": str(workspace / "baseline.cpp")}
98
+ names = {}
99
+ for directory in dirs:
100
+ name = re.sub(r"[^A-Za-z0-9_.-]+", "-", directory.name).strip("-") or "submission"
101
+ names[name] = directory
102
+ players[name] = str(directory / "bot.cpp")
103
+ if args.with_history:
104
+ for bot in sorted((task / "bots").glob("*.cpp")):
105
+ players["history-" + bot.stem] = str(bot)
106
+
107
+ sizes, maps_per_size, copies = TIERS[args.tier]
108
+ config = {
109
+ "game": str(workspace / "engine.py"),
110
+ "anchor": "Base",
111
+ "players": players,
112
+ "sizes": sizes,
113
+ "maps_per_size": maps_per_size,
114
+ "copies": copies,
115
+ "replays": False,
116
+ "build_dir": str(out / "build"),
117
+ "seed": 20261017,
118
+ }
119
+ config_path = out / "config.json"
120
+ config_path.write_text(json.dumps(config, ensure_ascii=False, indent=1), encoding="utf-8")
121
+ tournament = str(arena / "tournament.py")
122
+ steps = [["plan", str(config_path), str(games_dir)], ["run", str(games_dir), "--workers", str(max(1, args.workers))], ["rate", str(games_dir)]]
123
+ print("tier=%s sizes=%s maps=%d players=%d" % (args.tier, sizes, maps_per_size, len(players)), flush=True)
124
+ for step in steps:
125
+ completed = subprocess.run([sys.executable, "-X", "utf8", tournament, *step], capture_output=True, text=True,
126
+ encoding="utf-8", errors="replace")
127
+ if completed.returncode != 0:
128
+ print(completed.stdout[-4000:], completed.stderr[-4000:], file=sys.stderr)
129
+ return 2
130
+ scores = load_json(games_dir / "成绩.json") or {}
131
+ raw_ratings = scores.get("ratings", {})
132
+ ratings = {name: (value[0] if isinstance(value, list) and value else value) for name, value in raw_ratings.items()}
133
+ prices = load_json(ROOT / "prices.json") or {}
134
+
135
+ rows = []
136
+ for name, program in players.items():
137
+ directory = names.get(name)
138
+ if directory is not None:
139
+ sub_run = directory / "run"
140
+ run = load_json(sub_run / "run.json") or {}
141
+ usage = load_json(sub_run / "usage.json") or {}
142
+ cost = load_json(directory / "cost.json") or load_json(sub_run / "cost.json")
143
+ amount, source, currency = money(directory, run, usage, cost, prices)
144
+ violations = 0
145
+ if (sub_run / "violations.jsonl").is_file():
146
+ violations = len([line for line in (sub_run / "violations.jsonl").read_text(encoding="utf-8").splitlines() if line.strip()])
147
+ scan = static_scan(directory / "bot.cpp")
148
+ kind = "submission"
149
+ else:
150
+ run = {}
151
+ amount, source, currency = 0.0, "missing", "USD"
152
+ violations = 0
153
+ scan = static_scan(Path(program)) if Path(program).suffix == ".cpp" else []
154
+ kind = "baseline" if name == "Base" else "history"
155
+ rows.append({
156
+ "name": name,
157
+ "kind": kind,
158
+ "rating": round(float(ratings.get(name, 0) or 0), 2),
159
+ "cost": round(amount, 4), "costSource": source, "currency": currency,
160
+ "violations": violations,
161
+ "static": scan,
162
+ "run": run,
163
+ })
164
+ rows.sort(key=lambda row: (-row["rating"], row["name"]))
165
+ payload = {"generatedAt": time.strftime("%Y-%m-%dT%H:%M:%S%z"), "tier": args.tier, "sizes": sizes, "mapsPerSize": maps_per_size, "results": rows}
166
+ (out / "summary.json").write_text(json.dumps(payload, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
167
+ lines = ["# ctf-eval 结果", "", "- 时间:%s" % payload["generatedAt"], "- 档位:%s(人数 %s,每档 %d 张图)" % (args.tier, ",".join(map(str, sizes)), maps_per_size), "", "## 排行榜", "", "| 名次 | 名称 | BT 实力值 | 成本 | 价格来源 | 违规 | 静态扫描 |", "|---:|---|---:|---:|---|---:|---|"]
168
+ for rank, row in enumerate(rows, 1):
169
+ scan = "未发现" if not row["static"] else "%d 项" % len(row["static"])
170
+ lines.append("| %d | %s | %+.2f | %.4f %s | %s | %d | %s |" % (rank, row["name"], row["rating"], row["cost"], row["currency"], row["costSource"], row["violations"], scan))
171
+ lines += ["", "## 说明", "", "- 使用官方 arena/tournament.py 的 plan/run/rate;Base(baseline)为 0 分锚点。", "- 没有 run/ 时仍评测 bot,只是成本/用量显示 missing。", "- 静态扫描只报告,不取消资格。"]
172
+ (out / "summary.md").write_text("\n".join(lines) + "\n", encoding="utf-8")
173
+ print("wrote", out / "summary.md")
174
+ return 0
175
+
176
+
177
+ if __name__ == "__main__":
178
+ raise SystemExit(main())