dsh-eval-guard 0.2.2 → 0.3.0

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 (224) hide show
  1. package/README.md +91 -13
  2. package/assets/bomberman/workspace.zip +0 -0
  3. package/assets/commit0/instance.json +30 -0
  4. package/assets/commit0/workspace.zip +0 -0
  5. package/assets/ctf/workspace.zip +0 -0
  6. package/assets/featurebench/hidden/test_regression.py +690 -0
  7. package/assets/featurebench/instance.json +16 -0
  8. package/assets/featurebench/workspace.zip +0 -0
  9. package/assets/swe-bench-pro/hidden/test.patch +36 -0
  10. package/assets/swe-bench-pro/instance.json +10 -0
  11. package/assets/swe-bench-pro/workspace.zip +0 -0
  12. package/assets/tank/workspace.zip +0 -0
  13. package/eval/README.md +18 -0
  14. package/eval/arena/arena/README.md +21 -0
  15. package/eval/arena/arena/bt.py +104 -0
  16. package/eval/arena/arena/clock.py +15 -0
  17. package/eval/arena/arena/core.py +265 -0
  18. package/eval/arena/arena/run.cmd +24 -0
  19. package/eval/arena/arena/run.sh +11 -0
  20. package/eval/arena/arena/schedule.py +79 -0
  21. package/eval/arena/arena/tournament.py +183 -0
  22. package/eval/arena/bomberman/bots/README.md +16 -0
  23. package/eval/arena/bomberman/bots/deepseek-v4.1-flash.cpp +447 -0
  24. package/eval/arena/bomberman/bots/doubao-seed-2.1.cpp +542 -0
  25. package/eval/arena/bomberman/bots/glm-5.3.cpp +406 -0
  26. package/eval/arena/bomberman/bots/gpt-6-astra.cpp +768 -0
  27. package/eval/arena/bomberman/bots/gpt-6-sol.cpp +444 -0
  28. package/eval/arena/bomberman/bots/kimi-k3.cpp +432 -0
  29. package/eval/arena/bomberman/bots/mimo-v2.6-pro.cpp +529 -0
  30. package/eval/arena/bomberman/bots/opus-5.5.cpp +548 -0
  31. package/eval/arena/bomberman/bots/qwen-3.8-max.cpp +836 -0
  32. package/eval/arena/bomberman/tournament/README.md +11 -0
  33. package/eval/arena/bomberman/tournament/bt.py +56 -0
  34. package/eval/arena/bomberman/tournament/evaluate.py +199 -0
  35. package/eval/arena/bomberman/tournament/run.py +79 -0
  36. package/eval/arena/bomberman/tournament/schedule-2to10.json.gz +0 -0
  37. package/eval/arena/bomberman/workspace/MANIFEST.json +13 -0
  38. package/eval/arena/bomberman/workspace/README.md +29 -0
  39. package/eval/arena/bomberman/workspace/RULES.md +100 -0
  40. package/eval/arena/bomberman/workspace/baseline.cpp +240 -0
  41. package/eval/arena/bomberman/workspace/clock.py +25 -0
  42. package/eval/arena/bomberman/workspace/engine/arena.py +198 -0
  43. package/eval/arena/bomberman/workspace/engine/build.py +35 -0
  44. package/eval/arena/bomberman/workspace/engine/engine.py +256 -0
  45. package/eval/arena/bomberman/workspace/engine/verify_replay.py +24 -0
  46. package/eval/arena/bomberman/workspace/run.cmd +28 -0
  47. package/eval/arena/bomberman/workspace/run.sh +16 -0
  48. package/eval/arena/bomberman/workspace/viewer/index.html +42 -0
  49. package/eval/arena/ctf/bots/README.md +18 -0
  50. package/eval/arena/ctf/bots/deepseek-v4.1-flash.cpp +601 -0
  51. package/eval/arena/ctf/bots/doubao-seed-2.1-turbo.cpp +320 -0
  52. package/eval/arena/ctf/bots/fable-5.1.cpp +634 -0
  53. package/eval/arena/ctf/bots/glm-5.3.cpp +689 -0
  54. package/eval/arena/ctf/bots/gpt-6-astra.cpp +657 -0
  55. package/eval/arena/ctf/bots/gpt-6.1-sol.cpp +725 -0
  56. package/eval/arena/ctf/bots/kimi-k3.cpp +667 -0
  57. package/eval/arena/ctf/bots/mimo-v2.6-pro.cpp +533 -0
  58. package/eval/arena/ctf/bots/opus-5.5.cpp +645 -0
  59. package/eval/arena/ctf/bots/qwen-3.8-max.cpp +726 -0
  60. package/eval/arena/ctf/bots/sonnet-5.5.cpp +569 -0
  61. package/eval/arena/ctf/tournament/README.md +15 -0
  62. package/eval/arena/ctf/tournament/formal-2to8.json +31 -0
  63. package/eval/arena/ctf/tournament/formal-9to15.json +31 -0
  64. package/eval/arena/ctf/workspace/README.md +41 -0
  65. package/eval/arena/ctf/workspace/RULES.md +119 -0
  66. package/eval/arena/ctf/workspace/arena.py +186 -0
  67. package/eval/arena/ctf/workspace/baseline.cpp +257 -0
  68. package/eval/arena/ctf/workspace/clock.py +15 -0
  69. package/eval/arena/ctf/workspace/core.py +265 -0
  70. package/eval/arena/ctf/workspace/engine.py +389 -0
  71. package/eval/arena/ctf/workspace/run.cmd +24 -0
  72. package/eval/arena/ctf/workspace/run.sh +11 -0
  73. package/eval/arena/ctf/workspace/viewer.html +71 -0
  74. package/eval/arena/land/bots/README.md +17 -0
  75. package/eval/arena/land/bots/deepseek-4.1f@workbuddy#1/bot.cpp +608 -0
  76. package/eval/arena/land/bots/glm-5.3@workbuddy#1/bot.cpp +521 -0
  77. package/eval/arena/land/bots/gpt-6-astra@codex#1/bot.cpp +373 -0
  78. package/eval/arena/land/bots/gpt-6-sol@codex#1/bot.cpp +341 -0
  79. package/eval/arena/land/bots/kimi-k3@workbuddy#1/bot.cpp +492 -0
  80. package/eval/arena/land/bots/mimo-2.6-pro@zcode#1/bot.cpp +652 -0
  81. package/eval/arena/land/bots/mystery-model@zcode#1/bot.cpp +466 -0
  82. package/eval/arena/land/bots/opus-5.5@claude-code#1/bot.cpp +630 -0
  83. package/eval/arena/land/bots/sonnet-5@claude-code#1/bot.cpp +619 -0
  84. package/eval/arena/land/tournament/make_replay_page.py +17 -0
  85. package/eval/arena/land/tournament/rate.py +290 -0
  86. package/eval/arena/land/tournament/round_robin.py +172 -0
  87. package/eval/arena/land/tournament/sparring.cpp +356 -0
  88. package/eval/arena/land/workspace/RULES.md +114 -0
  89. package/eval/arena/land/workspace/TASK.md +46 -0
  90. package/eval/arena/land/workspace/anchors/.build/greedy_f2e6d2dea5ca +0 -0
  91. package/eval/arena/land/workspace/anchors/greedy.cpp +194 -0
  92. package/eval/arena/land/workspace/anchors/random.cpp +80 -0
  93. package/eval/arena/land/workspace/clock.py +64 -0
  94. package/eval/arena/land/workspace/engine/build.py +33 -0
  95. package/eval/arena/land/workspace/engine/game.py +273 -0
  96. package/eval/arena/land/workspace/engine/referee.py +308 -0
  97. package/eval/arena/land/workspace/starter/.build/bot_be32b45b4699 +0 -0
  98. package/eval/arena/land/workspace/starter/bot.cpp +82 -0
  99. package/eval/arena/land/workspace/viewer/viewer.html +210 -0
  100. package/eval/arena/tank/bots/README.md +24 -0
  101. package/eval/arena/tank/bots/half1/deepseek-v4.1-flash.cpp +367 -0
  102. package/eval/arena/tank/bots/half1/doubao-seed-evolving.cpp +402 -0
  103. package/eval/arena/tank/bots/half1/gemini-4-argon.cpp +1132 -0
  104. package/eval/arena/tank/bots/half1/glm-5.3.cpp +551 -0
  105. package/eval/arena/tank/bots/half1/gpt-6-astra.cpp +233 -0
  106. package/eval/arena/tank/bots/half1/gpt-6.1-sol-ultrafast.cpp +350 -0
  107. package/eval/arena/tank/bots/half1/gpt-6.1-sol.cpp +78 -0
  108. package/eval/arena/tank/bots/half1/grok-4.7.cpp +1099 -0
  109. package/eval/arena/tank/bots/half1/haiku-5.5.cpp +210 -0
  110. package/eval/arena/tank/bots/half1/kimi-k3.cpp +353 -0
  111. package/eval/arena/tank/bots/half1/mimo-v2.6-pro.cpp +336 -0
  112. package/eval/arena/tank/bots/half1/muse-wonder-full.cpp +560 -0
  113. package/eval/arena/tank/bots/half1/opus-5.5.cpp +435 -0
  114. package/eval/arena/tank/bots/half1/qwen-3.8-max.cpp +537 -0
  115. package/eval/arena/tank/bots/half2/deepseek-v4.1-flash.cpp +425 -0
  116. package/eval/arena/tank/bots/half2/doubao-seed-evolving.cpp +308 -0
  117. package/eval/arena/tank/bots/half2/gemini-4-argon.cpp +1390 -0
  118. package/eval/arena/tank/bots/half2/glm-5.3.cpp +651 -0
  119. package/eval/arena/tank/bots/half2/gpt-6-astra.cpp +241 -0
  120. package/eval/arena/tank/bots/half2/gpt-6.1-sol-ultrafast.cpp +356 -0
  121. package/eval/arena/tank/bots/half2/gpt-6.1-sol.cpp +87 -0
  122. package/eval/arena/tank/bots/half2/grok-4.7.cpp +1205 -0
  123. package/eval/arena/tank/bots/half2/haiku-5.5.cpp +210 -0
  124. package/eval/arena/tank/bots/half2/kimi-k3.cpp +487 -0
  125. package/eval/arena/tank/bots/half2/mimo-v2.6-pro.cpp +336 -0
  126. package/eval/arena/tank/bots/half2/muse-wonder-full.cpp +560 -0
  127. package/eval/arena/tank/bots/half2/opus-5.5.cpp +476 -0
  128. package/eval/arena/tank/bots/half2/qwen-3.8-max.cpp +392 -0
  129. package/eval/arena/tank/tournament/README.md +24 -0
  130. package/eval/arena/tank/tournament/bt.py +218 -0
  131. package/eval/arena/tank/tournament/final.json +202 -0
  132. package/eval/arena/tank/tournament/halftime.json +146 -0
  133. package/eval/arena/tank/tournament/progress.json +911 -0
  134. package/eval/arena/tank/tournament/schedule.py +100 -0
  135. package/eval/arena/tank/tournament/sparring.cpp +928 -0
  136. package/eval/arena/tank/tournament/tournament.py +1426 -0
  137. package/eval/arena/tank/workspace/README.md +169 -0
  138. package/eval/arena/tank/workspace/RULES.md +443 -0
  139. package/eval/arena/tank/workspace/arena.py +862 -0
  140. package/eval/arena/tank/workspace/baseline.cpp +259 -0
  141. package/eval/arena/tank/workspace/clock.py +103 -0
  142. package/eval/arena/tank/workspace/core.py +712 -0
  143. package/eval/arena/tank/workspace/engine.py +573 -0
  144. package/eval/arena/tank/workspace/history/.lock +0 -0
  145. package/eval/arena/tank/workspace/history/20261017-193610-08715287f91f-bot.cpp +109 -0
  146. package/eval/arena/tank/workspace/history/20261017-193619-f00b2d5635fa-gpt-6-astra.cpp +241 -0
  147. package/eval/arena/tank/workspace/history/20261017-193643-63319e0a2b1a-gpt-6_1-sol-ultrafast.cpp +356 -0
  148. package/eval/arena/tank/workspace/history/20261017-193647-41c1341d0a8a-gemini-4-argon.cpp +1390 -0
  149. package/eval/arena/tank/workspace/history/20261017-193652-e34c0f45539a-gpt-6_1-sol.cpp +87 -0
  150. package/eval/arena/tank/workspace/history/20261017-193654-c026348ce96e-opus-5_5.cpp +476 -0
  151. package/eval/arena/tank/workspace/history/20261017-193655-4a21366e94d1-muse-wonder-full.cpp +560 -0
  152. package/eval/arena/tank/workspace/history/20261017-193657-dbc4e9969c52-mimo-v2_6-pro.cpp +336 -0
  153. package/eval/arena/tank/workspace/history/20261017-193703-723cd036eb31-grok-4_7.cpp +1205 -0
  154. package/eval/arena/tank/workspace/history/20261017-193707-1ebbb227be68-deepseek-v4_1-flash.cpp +425 -0
  155. package/eval/arena/tank/workspace/history/20261017-193709-aeb0e7202ad5-doubao-seed-evolving.cpp +308 -0
  156. package/eval/arena/tank/workspace/history/20261017-193710-b21ebeb9bc76-kimi-k3.cpp +487 -0
  157. package/eval/arena/tank/workspace/history/20261017-193712-54cd86531905-haiku-5_5.cpp +210 -0
  158. package/eval/arena/tank/workspace/history/20261017-193714-efdb8e35c0bf-qwen-3_8-max.cpp +392 -0
  159. package/eval/arena/tank/workspace/history/20261017-193715-681423c23fdb-glm-5_3.cpp +651 -0
  160. package/eval/arena/tank/workspace/history/20261017-193717-1a8b3c75181d-sparring.cpp +928 -0
  161. package/eval/arena/tank/workspace/history/20261017-193720-1e0803a541e8-baseline.cpp +259 -0
  162. package/eval/arena/tank/workspace/history/log.jsonl +103 -0
  163. package/eval/arena/tank/workspace/judge/cycles.hpp +53 -0
  164. package/eval/arena/tank/workspace/judge/fast.cpp +337 -0
  165. package/eval/arena/tank/workspace/judge/referee.cpp +125 -0
  166. package/eval/arena/tank/workspace/judge/speed.cpp +99 -0
  167. package/eval/arena/tank/workspace/run.cmd +31 -0
  168. package/eval/arena/tank/workspace/run.sh +23 -0
  169. package/eval/arena/tank/workspace/tank.hpp +1762 -0
  170. package/eval/arena/tank/workspace/tank_main.cpp +215 -0
  171. package/eval/arena/tank/workspace/viewer.html +1150 -0
  172. package/eval/arena/warehouse/workspace/SPEC.md +130 -0
  173. package/eval/arena/warehouse/workspace/TASK.md +71 -0
  174. package/eval/arena/warehouse/workspace/baseline/common.h +88 -0
  175. package/eval/arena/warehouse/workspace/baseline/main.cpp +27 -0
  176. package/eval/arena/warehouse/workspace/baseline/old_stuff.cpp +87 -0
  177. package/eval/arena/warehouse/workspace/baseline/sim.cpp +427 -0
  178. package/eval/arena/warehouse/workspace/baseline/util.cpp +220 -0
  179. package/eval/arena/warehouse/workspace/clock.py +42 -0
  180. package/eval/arena/warehouse/workspace/src/common.h +88 -0
  181. package/eval/arena/warehouse/workspace/src/main.cpp +27 -0
  182. package/eval/arena/warehouse/workspace/src/old_stuff.cpp +87 -0
  183. package/eval/arena/warehouse/workspace/src/sim.cpp +427 -0
  184. package/eval/arena/warehouse/workspace/src/util.cpp +220 -0
  185. package/eval/arena/warehouse/workspace/tools/bench.py +119 -0
  186. package/eval/arena/warehouse/workspace/tools/check_input.py +161 -0
  187. package/eval/arena/warehouse/workspace/tools/gen.py +132 -0
  188. package/eval/arena/warehouse/workspace/tools/measure.py +107 -0
  189. package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.in +1420 -0
  190. package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.out +9162 -0
  191. package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.in +1465 -0
  192. package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.out +3439 -0
  193. package/eval/arena/warehouse/workspace/workloads/public/pub_flood.in +2757 -0
  194. package/eval/arena/warehouse/workspace/workloads/public/pub_flood.out +3126 -0
  195. package/eval/arena/warehouse/workspace/workloads/public/pub_small.in +731 -0
  196. package/eval/arena/warehouse/workspace/workloads/public/pub_small.out +2578 -0
  197. package/eval/arena/warehouse/workspace/workloads/public/pub_wide.in +528 -0
  198. package/eval/arena/warehouse/workspace/workloads/public/pub_wide.out +2026 -0
  199. package/eval/bomberman.py +20 -0
  200. package/eval/commit0.py +21 -0
  201. package/eval/common.py +61 -0
  202. package/eval/ctf.py +20 -0
  203. package/eval/featurebench.py +25 -0
  204. package/eval/land.py +20 -0
  205. package/eval/legacy/bomberman-prices.json +4 -0
  206. package/eval/legacy/bomberman.py +161 -0
  207. package/eval/legacy/commit0.py +91 -0
  208. package/eval/legacy/ctf-prices.json +4 -0
  209. package/eval/legacy/ctf.py +178 -0
  210. package/eval/legacy/featurebench.py +103 -0
  211. package/eval/legacy/land-prices.json +4 -0
  212. package/eval/legacy/land.py +177 -0
  213. package/eval/legacy/swe_bench_pro.py +116 -0
  214. package/eval/legacy/tank-prices.json +3 -0
  215. package/eval/legacy/tank.py +408 -0
  216. package/eval/legacy/warehouse-prices.json +4 -0
  217. package/eval/legacy/warehouse.py +365 -0
  218. package/eval/swe-bench-pro.py +25 -0
  219. package/eval/tank.py +20 -0
  220. package/eval/warehouse.py +20 -0
  221. package/lib/client/index.js +376 -63
  222. package/lib/client/index.js.map +1 -1
  223. package/lib/index.js +684 -27
  224. package/package.json +2 -1
@@ -0,0 +1,290 @@
1
+ """根据循环赛结果计算 Bradley-Terry 实力值和 0–100 分(单挑、混战各占一半)。
2
+
3
+ python tournament/rate.py results/results.jsonl --entries bots --out results/ratings
4
+ python tournament/rate.py 新结果.jsonl --entries bots --out 新评分 --frozen results/ratings.json
5
+
6
+ 方法
7
+ * 单挑和混战分开拟合。混战的每一局拆成 6 组两两比较(名次高者胜,同名次算平),
8
+ 和单挑一样当作"对局"处理。
9
+ * 每种模式:用全部对局做 Bradley-Terry 最大似然拟合(平局算半胜),MM 迭代求解;
10
+ 每对交过手的选手额外加一局虚拟平局,避免全胜/全负时估计发散。
11
+ θ = ln(实力),平移使 anchor-greedy = 0。Elo 刻度 = θ × 400 / ln10。
12
+ * 模型的 θ = 其各次运行 θ 的中位数。编译失败的运行(在 --entries 里但没有对局)按负无穷计。
13
+ * 每种模式的分数 = 100 × (θ模型 − θ贪心) / (θ该模式最强模型 − θ贪心),低于 0 记 0。
14
+ * 总分 = (单挑分 + 混战分) / 2,再按比例放大,使总分最高的模型 = 100。
15
+ * 95% 区间:bootstrap,每对选手的对局有放回重采样。
16
+ * --frozen:固定之前 ratings.json 中各 bot 的 θ 和各项满分基准,只估计新加入的 bot;
17
+ 新模型超过原基准时,按新基准重新换算并在报表中注明。
18
+ """
19
+ import argparse
20
+ import json
21
+ import math
22
+ import os
23
+ import random
24
+ import statistics
25
+ from collections import defaultdict
26
+
27
+ GREEDY = 'anchor-greedy'
28
+ ELO = 400 / math.log(10)
29
+ MODES = [('duel', '单挑'), ('melee', '混战')]
30
+
31
+
32
+ def model_of(bot_id):
33
+ return bot_id.split('#')[0]
34
+
35
+
36
+ def load(path):
37
+ games = {'duel': [], 'melee': []} # (bot_a, bot_b, score_of_a) in canonical order a < b
38
+ stats = {m: defaultdict(lambda: defaultdict(float)) for m in games}
39
+ with open(path, encoding='utf-8') as f:
40
+ for line in f:
41
+ r = json.loads(line)
42
+ mode = r.get('mode', 'duel')
43
+ n = r['names']
44
+ ranks = r.get('ranks')
45
+ if ranks is None: # 旧格式
46
+ w = r['winner']
47
+ ranks = [1, 1] if w == -1 else [1 if i == w else 2 for i in range(2)]
48
+ for i in range(len(n)):
49
+ for j in range(i + 1, len(n)):
50
+ a, b = n[i], n[j]
51
+ sa = 0.5 if ranks[i] == ranks[j] else float(ranks[i] < ranks[j])
52
+ if a > b:
53
+ a, b, sa = b, a, 1 - sa
54
+ games[mode].append((a, b, sa))
55
+ for i, bot in enumerate(n):
56
+ s = stats[mode][bot]
57
+ s['games'] += 1
58
+ s['rank'] += ranks[i]
59
+ s['wins'] += ranks[i] == 1 and ranks.count(1) == 1
60
+ s['points'] += 1.0 if (ranks[i] == 1 and ranks.count(1) == 1) else 0.5 if ranks[i] == 1 else 0.0
61
+ s['area'] += r['areas'][i]
62
+ s['timeouts'] += r['timeouts'][i]
63
+ s['forfeits'] += bool(r['forfeit'][i])
64
+ s['deaths'] += r['deaths'][i]
65
+ s['kills'] += r.get('kills', [0] * len(n))[i]
66
+ return games, stats
67
+
68
+
69
+ def fit(pairs, bots, frozen=None, iters=5000, tol=1e-8):
70
+ """pairs: {(a, b): (games, wins_of_a)}. Returns {bot: theta}."""
71
+ frozen = frozen or {}
72
+ idx = {b: i for i, b in enumerate(bots)}
73
+ n = len(bots)
74
+ wins = [0.0] * n
75
+ opp = [[] for _ in range(n)] # (j, games)
76
+ for (a, b), (g, wa) in pairs.items():
77
+ i, j = idx[a], idx[b]
78
+ g, wa = g + 1.0, wa + 0.5 # one virtual draw
79
+ wins[i] += wa
80
+ wins[j] += g - wa
81
+ opp[i].append((j, g))
82
+ opp[j].append((i, g))
83
+ p = [math.exp(frozen.get(b, 0.0)) for b in bots]
84
+ free = [i for i, b in enumerate(bots) if b not in frozen and opp[i]]
85
+ for _ in range(iters):
86
+ delta = 0.0
87
+ for i in free:
88
+ den = sum(g / (p[i] + p[j]) for j, g in opp[i])
89
+ new = wins[i] / den
90
+ delta = max(delta, abs(math.log(new / p[i])))
91
+ p[i] = new
92
+ if delta < tol:
93
+ break
94
+ theta = {b: math.log(p[idx[b]]) for b in bots if opp[idx[b]]}
95
+ if not frozen and GREEDY in theta:
96
+ shift = theta[GREEDY]
97
+ theta = {b: t - shift for b, t in theta.items()}
98
+ return theta
99
+
100
+
101
+ def aggregate(games):
102
+ pairs = defaultdict(lambda: [0, 0.0])
103
+ for a, b, sa in games:
104
+ pairs[(a, b)][0] += 1
105
+ pairs[(a, b)][1] += sa
106
+ return {k: tuple(v) for k, v in pairs.items()}
107
+
108
+
109
+ def model_scores(theta, runs_by_model, tmax_floor=None):
110
+ mt = {}
111
+ for m, runs in runs_by_model.items():
112
+ vals = [theta.get(r, -math.inf) for r in runs]
113
+ mt[m] = statistics.median(vals)
114
+ t0 = theta.get(GREEDY, 0.0)
115
+ contenders = [t for m, t in mt.items() if not m.startswith('anchor-') and t > -math.inf]
116
+ tmax = max(contenders) if contenders else t0
117
+ if tmax_floor is not None:
118
+ tmax = max(tmax, tmax_floor)
119
+ sc = {}
120
+ for m, t in mt.items():
121
+ sc[m] = 0.0 if tmax <= t0 or t == -math.inf else max(0.0, 100 * (t - t0) / (tmax - t0))
122
+ return mt, sc, tmax
123
+
124
+
125
+ def combine(mode_scores, total_floor=None):
126
+ """mode_scores: {mode: {model: score}} -> ({model: total}, raw_best)"""
127
+ modes = [m for m in mode_scores if mode_scores[m]]
128
+ models = set().union(*(mode_scores[m].keys() for m in modes)) if modes else set()
129
+ raw = {x: sum(mode_scores[m].get(x, 0.0) for m in modes) / len(modes) for x in models}
130
+ best = max([v for x, v in raw.items() if not x.startswith('anchor-')] or [0.0])
131
+ if total_floor is not None:
132
+ best = max(best, total_floor)
133
+ return {x: (100 * v / best if best > 0 else 0.0) for x, v in raw.items()}, best
134
+
135
+
136
+ def main():
137
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
138
+ ap.add_argument('results')
139
+ ap.add_argument('--entries', help='参赛文件夹,用于统计编译失败的运行')
140
+ ap.add_argument('--out', required=True, help='输出文件前缀(生成 .json 和 .md)')
141
+ ap.add_argument('--frozen', help='冻结的旧版 ratings.json,其中 bot 的 θ 保持不变')
142
+ ap.add_argument('--bootstrap', type=int, default=200)
143
+ ap.add_argument('--seed', type=int, default=0)
144
+ args = ap.parse_args()
145
+
146
+ games, stats = load(args.results)
147
+ active = [m for m, _ in MODES if games[m]]
148
+ bots = {m: sorted({g[0] for g in games[m]} | {g[1] for g in games[m]}) for m in active}
149
+ all_runs = set().union(*bots.values())
150
+ if args.entries:
151
+ all_runs |= {d for d in os.listdir(args.entries) if os.path.isfile(os.path.join(args.entries, d, 'bot.cpp'))}
152
+ runs_by_model = defaultdict(list)
153
+ for r in sorted(all_runs):
154
+ runs_by_model[model_of(r)].append(r)
155
+
156
+ prev = None
157
+ if args.frozen:
158
+ with open(args.frozen, encoding='utf-8') as f:
159
+ prev = json.load(f)
160
+
161
+ def frozen_of(m):
162
+ if not prev or m not in prev.get('modes', {}):
163
+ return {}, None
164
+ pm = prev['modes'][m]
165
+ return {b: t for b, t in pm['bots'].items() if b in bots[m]}, pm['theta_best']
166
+
167
+ theta, mt, msc, tmax = {}, {}, {}, {}
168
+ for m in active:
169
+ fz, fmax = frozen_of(m)
170
+ theta[m] = fit(aggregate(games[m]), bots[m], fz)
171
+ mt[m], msc[m], tmax[m] = model_scores(theta[m], runs_by_model, fmax)
172
+ total_floor = prev.get('total_raw_best') if prev else None
173
+ total, total_best = combine(msc, total_floor)
174
+ renormalized = []
175
+ if prev:
176
+ for m in active:
177
+ fz, fmax = frozen_of(m)
178
+ if fmax is not None and tmax[m] > fmax + 1e-12:
179
+ renormalized.append(dict(MODES)[m])
180
+ if total_floor is not None and total_best > total_floor + 1e-9:
181
+ renormalized.append('总分')
182
+
183
+ rng = random.Random(args.seed)
184
+ by_pair = {m: defaultdict(list) for m in active}
185
+ for m in active:
186
+ for a, b, sa in games[m]:
187
+ by_pair[m][(a, b)].append(sa)
188
+ boot = defaultdict(lambda: defaultdict(list)) # boot[mode or 'total'][model]
189
+ boot_rank = defaultdict(list)
190
+ for _ in range(args.bootstrap):
191
+ bsc, braw = {}, {}
192
+ for m in active:
193
+ fz, fmax = frozen_of(m)
194
+ pairs = {}
195
+ for k, v in by_pair[m].items():
196
+ s = [rng.choice(v) for _ in v]
197
+ pairs[k] = (len(s), sum(s))
198
+ th = fit(pairs, bots[m], fz, iters=2000, tol=1e-6)
199
+ _, bsc[m], _ = model_scores(th, runs_by_model, fmax)
200
+ for x, rs in runs_by_model.items(): # 同分(都被截到 0)时按原始实力区分
201
+ braw[x] = braw.get(x, 0.0) + max(th.get(r, -50.0) for r in rs)
202
+ for x, v in bsc[m].items():
203
+ boot[m][x].append(v)
204
+ btot, _ = combine(bsc, total_floor)
205
+ order = sorted((x for x in btot if not x.startswith('anchor-')), key=lambda x: (-btot[x], -braw.get(x, 0.0)))
206
+ for x, v in btot.items():
207
+ boot['total'][x].append(v)
208
+ for r, x in enumerate(order, 1):
209
+ boot_rank[x].append(r)
210
+
211
+ def ci(v):
212
+ if not v:
213
+ return [None, None]
214
+ v = sorted(v)
215
+ return [round(v[int(0.025 * (len(v) - 1))], 1), round(v[int(0.975 * (len(v) - 1))], 1)]
216
+
217
+ def rnd(t):
218
+ return None if t == -math.inf else round(t, 4)
219
+
220
+ raw = {x: sum(max(theta[m].get(r, -50.0) for r in rs) for m in active) for x, rs in runs_by_model.items()}
221
+ models = sorted(runs_by_model, key=lambda x: (-total.get(x, 0.0), -raw[x], x))
222
+ out = {
223
+ 'method': 'bradley-terry-mle, duel + melee (pairwise), 50/50',
224
+ 'total_raw_best': total_best,
225
+ 'renormalized_from_frozen': renormalized,
226
+ 'modes': {m: {'games': len(games[m]), 'theta_best': tmax[m],
227
+ 'bots': {b: round(t, 6) for b, t in theta[m].items()}} for m in active},
228
+ 'models': {x: {'score': round(total.get(x, 0.0), 2), 'score_ci95': ci(boot['total'][x]),
229
+ 'rank_ci95': ci(boot_rank[x]) if boot_rank[x] else None,
230
+ 'modes': {m: {'score': round(msc[m].get(x, 0.0), 2), 'score_ci95': ci(boot[m][x]),
231
+ 'theta': rnd(mt[m].get(x, -math.inf)),
232
+ 'runs': {r: rnd(theta[m].get(r, -math.inf)) for r in runs_by_model[x]}}
233
+ for m in active}}
234
+ for x in models},
235
+ }
236
+ os.makedirs(os.path.dirname(os.path.abspath(args.out)) or '.', exist_ok=True)
237
+ with open(args.out + '.json', 'w', encoding='utf-8') as f:
238
+ json.dump(out, f, ensure_ascii=False, indent=2)
239
+
240
+ names = dict(MODES)
241
+ lines = ['# 圈地对战 评分', '']
242
+ lines.append('单挑 %s 局;混战 %s 局(拆成 %s 组两两比较)。Bradley-Terry 最大似然;每种模式以 anchor-greedy = 0、'
243
+ '该模式最强模型 = 100 换算;总分 = 两种模式平均,再使最高者 = 100。区间为 bootstrap 95%%(%d 次)。' % (
244
+ len(games['duel']), sum(stats['melee'][b]['games'] for b in stats['melee']) // 4 or 0,
245
+ len(games['melee']), args.bootstrap))
246
+ lines.append('')
247
+ if renormalized:
248
+ lines += ['> 本次补测有模型超过了冻结版的满分基准(%s),已按新基准重新换算。' % '、'.join(renormalized), '']
249
+ head = '| 排名 | 模型 | 总分 | 95% 区间 | 名次区间 | ' + ' | '.join(
250
+ '%s分 | %s实力(Elo 刻度)' % (names[m], names[m]) for m in active) + ' |'
251
+ lines += [head, '|' + '---|' * (5 + 2 * len(active))]
252
+ rank = 0
253
+ for x in models:
254
+ if not x.startswith('anchor-'):
255
+ rank += 1
256
+ c = ci(boot['total'][x])
257
+ rc = ci(boot_rank[x]) if boot_rank[x] else None
258
+ cells = []
259
+ for m in active:
260
+ t = mt[m].get(x, -math.inf)
261
+ mc = ci(boot[m][x])
262
+ cells.append('%.1f(%s–%s)' % (msc[m].get(x, 0.0), mc[0], mc[1]))
263
+ cells.append('编译失败' if t == -math.inf else '%.0f' % (t * ELO))
264
+ lines.append('| %s | %s | %.1f | %s–%s | %s | %s |' % (
265
+ rank if not x.startswith('anchor-') else '锚点', x, total.get(x, 0.0), c[0], c[1],
266
+ '%d–%d' % (rc[0], rc[1]) if rc else '—', ' | '.join(cells)))
267
+ for m in active:
268
+ st = stats[m]
269
+ lines += ['', '## %s:各 bot 统计' % names[m], '']
270
+ if m == 'duel':
271
+ lines += ['| bot | 局数 | 得分率 | 平均面积 | 平均被杀 | 平均击杀 | 超时 | 判负 |', '|---|---|---|---|---|---|---|---|']
272
+ else:
273
+ lines += ['| bot | 局数 | 第一名占比 | 平均名次 | 平均面积 | 平均被杀 | 平均击杀 | 超时 | 判负 |',
274
+ '|---|---|---|---|---|---|---|---|---|']
275
+ for b in sorted(st, key=lambda b: st[b]['rank'] / st[b]['games']):
276
+ s = st[b]
277
+ g = s['games']
278
+ common = '%.0f | %.1f | %.1f | %d | %d' % (s['area'] / g, s['deaths'] / g, s['kills'] / g,
279
+ s['timeouts'], s['forfeits'])
280
+ if m == 'duel':
281
+ lines.append('| %s | %d | %.1f%% | %s |' % (b, g, 100 * s['points'] / g, common))
282
+ else:
283
+ lines.append('| %s | %d | %.1f%% | %.2f | %s |' % (b, g, 100 * s['wins'] / g, s['rank'] / g, common))
284
+ with open(args.out + '.md', 'w', encoding='utf-8') as f:
285
+ f.write('\n'.join(lines) + '\n')
286
+ print('\n'.join(lines))
287
+
288
+
289
+ if __name__ == '__main__':
290
+ main()
@@ -0,0 +1,172 @@
1
+ """圈地对战循环赛:1 对 1 单挑 + 4 人混战。
2
+
3
+ 每个参赛者是一个含 bot.cpp 的文件夹,文件夹名即 bot 编号,格式为 <模型>@<工具>#<序号>
4
+ (例如 claude-opus-5.5@claude-code#1)。'#' 前面相同的属于同一模型,互相不对战。
5
+ 三个基准 bot(anchor-random、anchor-greedy、anchor-sparring 出题方参考 bot)自动加入。
6
+
7
+ 单挑:每对选手使用同一批地图(种子 seed_base 起共 maps 张),每张图交换出生位各打一局。
8
+ 混战:共 melee_rounds 轮。每轮把全部 bot 随机分成若干桌、每桌 4 人(人数不是 4 的倍数时,
9
+ 随机抽几个 bot 多打一桌补齐),同一桌在同一张图上轮换座位打 4 局。
10
+ 结果逐局追加到 JSONL 文件;中断后重新运行同一命令,会从中断处继续。
11
+
12
+ python tournament/round_robin.py --entries bots --out results/rerun.jsonl --maps 200 --melee-rounds 300 --jobs 12
13
+ """
14
+ import argparse
15
+ import json
16
+ import os
17
+ import random
18
+ import sys
19
+ import time
20
+ from concurrent.futures import ProcessPoolExecutor, as_completed
21
+
22
+ HERE = os.path.dirname(os.path.abspath(__file__))
23
+ WORKSPACE = os.path.join(HERE, '..', 'workspace')
24
+ sys.path.insert(0, os.path.join(WORKSPACE, 'engine'))
25
+ import build # noqa: E402
26
+ import referee # noqa: E402
27
+
28
+ ANCHORS = {'anchor-random': os.path.join(WORKSPACE, 'anchors', 'random.cpp'),
29
+ 'anchor-greedy': os.path.join(WORKSPACE, 'anchors', 'greedy.cpp'),
30
+ # 出题方的参考 bot,不发给选手,只作为每期固定的强度参照
31
+ 'anchor-sparring': os.path.join(HERE, 'sparring.cpp')}
32
+ MELEE_SEED_OFFSET = 5000
33
+
34
+
35
+ def model_of(bot_id):
36
+ return bot_id.split('#')[0]
37
+
38
+
39
+ def play(job):
40
+ key, seats, exes, seed, replay = job
41
+ r = referee.run_match(exes, seats, seed, replay_path=replay)
42
+ r['key'] = key
43
+ if len(seats) == 2:
44
+ r['swap'] = key[3]
45
+ return r
46
+
47
+
48
+ def melee_tables(ids, rnd, seed_base):
49
+ """每轮的分桌:[[bot × 4], ...]。同一模型的多次运行不同桌。"""
50
+ rng = random.Random(seed_base * 1000 + rnd)
51
+ for _ in range(10000):
52
+ order = ids[:]
53
+ rng.shuffle(order)
54
+ tables = [order[i:i + 4] for i in range(0, len(order), 4)]
55
+ last = tables[-1]
56
+ while len(last) < 4:
57
+ cand = [b for b in ids if b not in last and all(model_of(b) != model_of(x) for x in last)]
58
+ if not cand:
59
+ break
60
+ last.append(rng.choice(cand))
61
+ if all(len(t) == 4 and len({model_of(b) for b in t}) == 4 for t in tables):
62
+ return tables
63
+ raise RuntimeError('无法分桌:不同模型的 bot 少于 4 个')
64
+
65
+
66
+ def main():
67
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
68
+ ap.add_argument('--entries', required=True, help='参赛文件夹(<编号>/bot.cpp)')
69
+ ap.add_argument('--out', required=True, help='对局结果 JSONL 文件')
70
+ ap.add_argument('--maps', type=int, default=5, help='单挑:每对选手的地图数,每张打两局(默认 5,即 10 局)')
71
+ ap.add_argument('--melee-rounds', type=int, default=10, help='混战轮数(默认 10),0 = 不打混战')
72
+ ap.add_argument('--seed-base', type=int, default=1000)
73
+ ap.add_argument('--jobs', type=int, default=max(1, (os.cpu_count() or 2) // 2 - 1),
74
+ help='同时进行的单挑局数;每局运行 2 个 bot,2×jobs 应小于 CPU 核数。'
75
+ '混战每局运行 4 个 bot,同时进行的局数自动减半')
76
+ ap.add_argument('--replays', help='保存回放的文件夹')
77
+ ap.add_argument('--replay-maps', type=int, default=1, help='只保存前 K 张地图(单挑)/ 前 K 轮(混战)的回放')
78
+ ap.add_argument('--only', choices=['duel', 'melee'], help='只打单挑或只打混战')
79
+ ap.add_argument('--no-anchor', action='append', default=[], metavar='ID', help='不加入某个基准 bot(可重复)')
80
+ ap.add_argument('--low-priority', action='store_true',
81
+ help='以低于正常的优先级运行(Windows 上 bot 进程会继承),把 CPU 优先让给其他程序')
82
+ args = ap.parse_args()
83
+ if args.low_priority and sys.platform == 'win32':
84
+ import ctypes
85
+ k32 = ctypes.windll.kernel32
86
+ k32.GetCurrentProcess.restype = ctypes.c_void_p
87
+ k32.SetPriorityClass(ctypes.c_void_p(k32.GetCurrentProcess()), 0x4000) # BELOW_NORMAL_PRIORITY_CLASS
88
+
89
+ build_dir = os.path.join(os.path.dirname(os.path.abspath(args.out)), 'build')
90
+ bots = {}
91
+ for bid, f in ANCHORS.items():
92
+ if bid in args.no_anchor:
93
+ continue
94
+ bots[bid] = build.compile_bot(f, build_dir)
95
+ for name in sorted(os.listdir(args.entries)):
96
+ src = os.path.join(args.entries, name, 'bot.cpp')
97
+ if not os.path.isfile(src):
98
+ continue
99
+ try:
100
+ bots[name] = build.compile_bot(src, build_dir)
101
+ except RuntimeError as e:
102
+ print('[编译失败,该次运行按最弱处理] %s\n%s' % (name, e))
103
+ print('bot 数量:%d' % len(bots))
104
+
105
+ done = set()
106
+ if os.path.exists(args.out):
107
+ with open(args.out, encoding='utf-8') as f:
108
+ for line in f:
109
+ r = json.loads(line)
110
+ if 'key' in r:
111
+ done.add(tuple(tuple(x) if isinstance(x, list) else x for x in r['key']))
112
+ else: # 旧格式(只有单挑)
113
+ a, b = r['names'][::-1] if r['swap'] else r['names']
114
+ done.add(('duel', a, b, r['swap'], r['seed']))
115
+
116
+ ids = sorted(bots)
117
+ duel_jobs, melee_jobs = [], []
118
+ if args.only != 'melee':
119
+ for i, a in enumerate(ids):
120
+ for b in ids[i + 1:]:
121
+ if model_of(a) == model_of(b):
122
+ continue
123
+ for m in range(args.maps):
124
+ seed = args.seed_base + m
125
+ for swap in (False, True):
126
+ # 注意:key 的顺序与旧格式兼容 ('duel', a, b, swap, seed)
127
+ key = ('duel', a, b, swap, seed)
128
+ if key in done:
129
+ continue
130
+ replay = None
131
+ if args.replays and m < args.replay_maps:
132
+ replay = os.path.join(args.replays, 'duel__%s__vs__%s__%d%s.json' % (
133
+ a.replace('#', '-'), b.replace('#', '-'), seed, '_swap' if swap else ''))
134
+ seats = [b, a] if swap else [a, b]
135
+ duel_jobs.append((key, seats, [bots[x] for x in seats], seed, replay))
136
+ duel_jobs.sort(key=lambda j: j[3]) # 按地图轮次推进:中途停下时各对阵局数均衡
137
+ if args.only != 'duel' and args.melee_rounds > 0:
138
+ for rnd in range(args.melee_rounds):
139
+ seed = args.seed_base + MELEE_SEED_OFFSET + rnd
140
+ for t, table in enumerate(melee_tables(ids, rnd, args.seed_base)):
141
+ for rot in range(4):
142
+ key = ('melee', tuple(table), rot, seed)
143
+ if key in done:
144
+ continue
145
+ seats = [table[(i + rot) % 4] for i in range(4)]
146
+ replay = None
147
+ if args.replays and rnd < args.replay_maps:
148
+ replay = os.path.join(args.replays, 'melee__r%02d_t%d_rot%d__%s.json' % (
149
+ rnd, t, rot, '__'.join(x.replace('#', '-') for x in seats)))
150
+ melee_jobs.append((key, seats, [bots[x] for x in seats], seed, replay))
151
+ print('待进行:单挑 %d 局,混战 %d 局(已完成:%d)' % (len(duel_jobs), len(melee_jobs), len(done)))
152
+
153
+ os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
154
+ with open(args.out, 'a', encoding='utf-8') as out:
155
+ for label, jobs, n_jobs in (('单挑', duel_jobs, args.jobs), ('混战', melee_jobs, max(1, args.jobs // 2))):
156
+ if not jobs:
157
+ continue
158
+ t0 = time.time()
159
+ with ProcessPoolExecutor(n_jobs) as pool:
160
+ futures = [pool.submit(play, j) for j in jobs]
161
+ for n, fut in enumerate(as_completed(futures), 1):
162
+ r = fut.result()
163
+ out.write(json.dumps(r, ensure_ascii=False) + '\n')
164
+ out.flush()
165
+ if n % 20 == 0 or n == len(jobs):
166
+ el = time.time() - t0
167
+ print('%s %d/%d 局,已用 %.0f 秒,预计还需 %.0f 秒' % (
168
+ label, n, len(jobs), el, el / n * (len(jobs) - n)), flush=True)
169
+
170
+
171
+ if __name__ == '__main__':
172
+ main()