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.
- package/README.md +91 -13
- package/assets/commit0/instance.json +30 -0
- package/assets/commit0/workspace.zip +0 -0
- package/assets/featurebench/hidden/test_regression.py +690 -0
- package/assets/featurebench/instance.json +16 -0
- package/assets/featurebench/workspace.zip +0 -0
- package/assets/swe-bench-pro/hidden/test.patch +36 -0
- package/assets/swe-bench-pro/instance.json +10 -0
- package/assets/swe-bench-pro/workspace.zip +0 -0
- package/eval/README.md +18 -0
- package/eval/arena/arena/README.md +21 -0
- package/eval/arena/arena/bt.py +104 -0
- package/eval/arena/arena/clock.py +15 -0
- package/eval/arena/arena/core.py +265 -0
- package/eval/arena/arena/run.cmd +24 -0
- package/eval/arena/arena/run.sh +11 -0
- package/eval/arena/arena/schedule.py +79 -0
- package/eval/arena/arena/tournament.py +183 -0
- package/eval/arena/bomberman/bots/README.md +16 -0
- package/eval/arena/bomberman/bots/deepseek-v4.1-flash.cpp +447 -0
- package/eval/arena/bomberman/bots/doubao-seed-2.1.cpp +542 -0
- package/eval/arena/bomberman/bots/glm-5.3.cpp +406 -0
- package/eval/arena/bomberman/bots/gpt-6-astra.cpp +768 -0
- package/eval/arena/bomberman/bots/gpt-6-sol.cpp +444 -0
- package/eval/arena/bomberman/bots/kimi-k3.cpp +432 -0
- package/eval/arena/bomberman/bots/mimo-v2.6-pro.cpp +529 -0
- package/eval/arena/bomberman/bots/opus-5.5.cpp +548 -0
- package/eval/arena/bomberman/bots/qwen-3.8-max.cpp +836 -0
- package/eval/arena/bomberman/tournament/README.md +11 -0
- package/eval/arena/bomberman/tournament/bt.py +56 -0
- package/eval/arena/bomberman/tournament/evaluate.py +199 -0
- package/eval/arena/bomberman/tournament/run.py +79 -0
- package/eval/arena/bomberman/tournament/schedule-2to10.json.gz +0 -0
- package/eval/arena/bomberman/workspace/MANIFEST.json +13 -0
- package/eval/arena/bomberman/workspace/README.md +29 -0
- package/eval/arena/bomberman/workspace/RULES.md +100 -0
- package/eval/arena/bomberman/workspace/baseline.cpp +240 -0
- package/eval/arena/bomberman/workspace/clock.py +25 -0
- package/eval/arena/bomberman/workspace/engine/arena.py +198 -0
- package/eval/arena/bomberman/workspace/engine/build.py +35 -0
- package/eval/arena/bomberman/workspace/engine/engine.py +256 -0
- package/eval/arena/bomberman/workspace/engine/verify_replay.py +24 -0
- package/eval/arena/bomberman/workspace/run.cmd +28 -0
- package/eval/arena/bomberman/workspace/run.sh +16 -0
- package/eval/arena/bomberman/workspace/viewer/index.html +42 -0
- package/eval/arena/ctf/bots/README.md +18 -0
- package/eval/arena/ctf/bots/deepseek-v4.1-flash.cpp +601 -0
- package/eval/arena/ctf/bots/doubao-seed-2.1-turbo.cpp +320 -0
- package/eval/arena/ctf/bots/fable-5.1.cpp +634 -0
- package/eval/arena/ctf/bots/glm-5.3.cpp +689 -0
- package/eval/arena/ctf/bots/gpt-6-astra.cpp +657 -0
- package/eval/arena/ctf/bots/gpt-6.1-sol.cpp +725 -0
- package/eval/arena/ctf/bots/kimi-k3.cpp +667 -0
- package/eval/arena/ctf/bots/mimo-v2.6-pro.cpp +533 -0
- package/eval/arena/ctf/bots/opus-5.5.cpp +645 -0
- package/eval/arena/ctf/bots/qwen-3.8-max.cpp +726 -0
- package/eval/arena/ctf/bots/sonnet-5.5.cpp +569 -0
- package/eval/arena/ctf/tournament/README.md +15 -0
- package/eval/arena/ctf/tournament/formal-2to8.json +31 -0
- package/eval/arena/ctf/tournament/formal-9to15.json +31 -0
- package/eval/arena/ctf/workspace/README.md +41 -0
- package/eval/arena/ctf/workspace/RULES.md +119 -0
- package/eval/arena/ctf/workspace/arena.py +186 -0
- package/eval/arena/ctf/workspace/baseline.cpp +257 -0
- package/eval/arena/ctf/workspace/clock.py +15 -0
- package/eval/arena/ctf/workspace/core.py +265 -0
- package/eval/arena/ctf/workspace/engine.py +389 -0
- package/eval/arena/ctf/workspace/run.cmd +24 -0
- package/eval/arena/ctf/workspace/run.sh +11 -0
- package/eval/arena/ctf/workspace/viewer.html +71 -0
- package/eval/arena/land/bots/README.md +17 -0
- package/eval/arena/land/bots/deepseek-4.1f@workbuddy#1/bot.cpp +608 -0
- package/eval/arena/land/bots/glm-5.3@workbuddy#1/bot.cpp +521 -0
- package/eval/arena/land/bots/gpt-6-astra@codex#1/bot.cpp +373 -0
- package/eval/arena/land/bots/gpt-6-sol@codex#1/bot.cpp +341 -0
- package/eval/arena/land/bots/kimi-k3@workbuddy#1/bot.cpp +492 -0
- package/eval/arena/land/bots/mimo-2.6-pro@zcode#1/bot.cpp +652 -0
- package/eval/arena/land/bots/mystery-model@zcode#1/bot.cpp +466 -0
- package/eval/arena/land/bots/opus-5.5@claude-code#1/bot.cpp +630 -0
- package/eval/arena/land/bots/sonnet-5@claude-code#1/bot.cpp +619 -0
- package/eval/arena/land/tournament/make_replay_page.py +17 -0
- package/eval/arena/land/tournament/rate.py +290 -0
- package/eval/arena/land/tournament/round_robin.py +172 -0
- package/eval/arena/land/tournament/sparring.cpp +356 -0
- package/eval/arena/land/workspace/RULES.md +114 -0
- package/eval/arena/land/workspace/TASK.md +46 -0
- package/eval/arena/land/workspace/anchors/.build/greedy_f2e6d2dea5ca +0 -0
- package/eval/arena/land/workspace/anchors/greedy.cpp +194 -0
- package/eval/arena/land/workspace/anchors/random.cpp +80 -0
- package/eval/arena/land/workspace/clock.py +64 -0
- package/eval/arena/land/workspace/engine/build.py +33 -0
- package/eval/arena/land/workspace/engine/game.py +273 -0
- package/eval/arena/land/workspace/engine/referee.py +308 -0
- package/eval/arena/land/workspace/starter/.build/bot_be32b45b4699 +0 -0
- package/eval/arena/land/workspace/starter/bot.cpp +82 -0
- package/eval/arena/land/workspace/viewer/viewer.html +210 -0
- package/eval/arena/tank/bots/README.md +24 -0
- package/eval/arena/tank/bots/half1/deepseek-v4.1-flash.cpp +367 -0
- package/eval/arena/tank/bots/half1/doubao-seed-evolving.cpp +402 -0
- package/eval/arena/tank/bots/half1/gemini-4-argon.cpp +1132 -0
- package/eval/arena/tank/bots/half1/glm-5.3.cpp +551 -0
- package/eval/arena/tank/bots/half1/gpt-6-astra.cpp +233 -0
- package/eval/arena/tank/bots/half1/gpt-6.1-sol-ultrafast.cpp +350 -0
- package/eval/arena/tank/bots/half1/gpt-6.1-sol.cpp +78 -0
- package/eval/arena/tank/bots/half1/grok-4.7.cpp +1099 -0
- package/eval/arena/tank/bots/half1/haiku-5.5.cpp +210 -0
- package/eval/arena/tank/bots/half1/kimi-k3.cpp +353 -0
- package/eval/arena/tank/bots/half1/mimo-v2.6-pro.cpp +336 -0
- package/eval/arena/tank/bots/half1/muse-wonder-full.cpp +560 -0
- package/eval/arena/tank/bots/half1/opus-5.5.cpp +435 -0
- package/eval/arena/tank/bots/half1/qwen-3.8-max.cpp +537 -0
- package/eval/arena/tank/bots/half2/deepseek-v4.1-flash.cpp +425 -0
- package/eval/arena/tank/bots/half2/doubao-seed-evolving.cpp +308 -0
- package/eval/arena/tank/bots/half2/gemini-4-argon.cpp +1390 -0
- package/eval/arena/tank/bots/half2/glm-5.3.cpp +651 -0
- package/eval/arena/tank/bots/half2/gpt-6-astra.cpp +241 -0
- package/eval/arena/tank/bots/half2/gpt-6.1-sol-ultrafast.cpp +356 -0
- package/eval/arena/tank/bots/half2/gpt-6.1-sol.cpp +87 -0
- package/eval/arena/tank/bots/half2/grok-4.7.cpp +1205 -0
- package/eval/arena/tank/bots/half2/haiku-5.5.cpp +210 -0
- package/eval/arena/tank/bots/half2/kimi-k3.cpp +487 -0
- package/eval/arena/tank/bots/half2/mimo-v2.6-pro.cpp +336 -0
- package/eval/arena/tank/bots/half2/muse-wonder-full.cpp +560 -0
- package/eval/arena/tank/bots/half2/opus-5.5.cpp +476 -0
- package/eval/arena/tank/bots/half2/qwen-3.8-max.cpp +392 -0
- package/eval/arena/tank/tournament/README.md +24 -0
- package/eval/arena/tank/tournament/bt.py +218 -0
- package/eval/arena/tank/tournament/final.json +202 -0
- package/eval/arena/tank/tournament/halftime.json +146 -0
- package/eval/arena/tank/tournament/progress.json +911 -0
- package/eval/arena/tank/tournament/schedule.py +100 -0
- package/eval/arena/tank/tournament/sparring.cpp +928 -0
- package/eval/arena/tank/tournament/tournament.py +1426 -0
- package/eval/arena/tank/workspace/README.md +169 -0
- package/eval/arena/tank/workspace/RULES.md +443 -0
- package/eval/arena/tank/workspace/arena.py +862 -0
- package/eval/arena/tank/workspace/baseline.cpp +259 -0
- package/eval/arena/tank/workspace/clock.py +103 -0
- package/eval/arena/tank/workspace/core.py +712 -0
- package/eval/arena/tank/workspace/engine.py +573 -0
- package/eval/arena/tank/workspace/history/.lock +0 -0
- package/eval/arena/tank/workspace/history/20261017-193610-08715287f91f-bot.cpp +109 -0
- package/eval/arena/tank/workspace/history/20261017-193619-f00b2d5635fa-gpt-6-astra.cpp +241 -0
- package/eval/arena/tank/workspace/history/20261017-193643-63319e0a2b1a-gpt-6_1-sol-ultrafast.cpp +356 -0
- package/eval/arena/tank/workspace/history/20261017-193647-41c1341d0a8a-gemini-4-argon.cpp +1390 -0
- package/eval/arena/tank/workspace/history/20261017-193652-e34c0f45539a-gpt-6_1-sol.cpp +87 -0
- package/eval/arena/tank/workspace/history/20261017-193654-c026348ce96e-opus-5_5.cpp +476 -0
- package/eval/arena/tank/workspace/history/20261017-193655-4a21366e94d1-muse-wonder-full.cpp +560 -0
- package/eval/arena/tank/workspace/history/20261017-193657-dbc4e9969c52-mimo-v2_6-pro.cpp +336 -0
- package/eval/arena/tank/workspace/history/20261017-193703-723cd036eb31-grok-4_7.cpp +1205 -0
- package/eval/arena/tank/workspace/history/20261017-193707-1ebbb227be68-deepseek-v4_1-flash.cpp +425 -0
- package/eval/arena/tank/workspace/history/20261017-193709-aeb0e7202ad5-doubao-seed-evolving.cpp +308 -0
- package/eval/arena/tank/workspace/history/20261017-193710-b21ebeb9bc76-kimi-k3.cpp +487 -0
- package/eval/arena/tank/workspace/history/20261017-193712-54cd86531905-haiku-5_5.cpp +210 -0
- package/eval/arena/tank/workspace/history/20261017-193714-efdb8e35c0bf-qwen-3_8-max.cpp +392 -0
- package/eval/arena/tank/workspace/history/20261017-193715-681423c23fdb-glm-5_3.cpp +651 -0
- package/eval/arena/tank/workspace/history/20261017-193717-1a8b3c75181d-sparring.cpp +928 -0
- package/eval/arena/tank/workspace/history/20261017-193720-1e0803a541e8-baseline.cpp +259 -0
- package/eval/arena/tank/workspace/history/log.jsonl +103 -0
- package/eval/arena/tank/workspace/judge/cycles.hpp +53 -0
- package/eval/arena/tank/workspace/judge/fast.cpp +337 -0
- package/eval/arena/tank/workspace/judge/referee.cpp +125 -0
- package/eval/arena/tank/workspace/judge/speed.cpp +99 -0
- package/eval/arena/tank/workspace/run.cmd +31 -0
- package/eval/arena/tank/workspace/run.sh +23 -0
- package/eval/arena/tank/workspace/tank.hpp +1762 -0
- package/eval/arena/tank/workspace/tank_main.cpp +215 -0
- package/eval/arena/tank/workspace/viewer.html +1150 -0
- package/eval/arena/warehouse/workspace/SPEC.md +130 -0
- package/eval/arena/warehouse/workspace/TASK.md +71 -0
- package/eval/arena/warehouse/workspace/baseline/common.h +88 -0
- package/eval/arena/warehouse/workspace/baseline/main.cpp +27 -0
- package/eval/arena/warehouse/workspace/baseline/old_stuff.cpp +87 -0
- package/eval/arena/warehouse/workspace/baseline/sim.cpp +427 -0
- package/eval/arena/warehouse/workspace/baseline/util.cpp +220 -0
- package/eval/arena/warehouse/workspace/clock.py +42 -0
- package/eval/arena/warehouse/workspace/src/common.h +88 -0
- package/eval/arena/warehouse/workspace/src/main.cpp +27 -0
- package/eval/arena/warehouse/workspace/src/old_stuff.cpp +87 -0
- package/eval/arena/warehouse/workspace/src/sim.cpp +427 -0
- package/eval/arena/warehouse/workspace/src/util.cpp +220 -0
- package/eval/arena/warehouse/workspace/tools/bench.py +119 -0
- package/eval/arena/warehouse/workspace/tools/check_input.py +161 -0
- package/eval/arena/warehouse/workspace/tools/gen.py +132 -0
- package/eval/arena/warehouse/workspace/tools/measure.py +107 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.in +1420 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.out +9162 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.in +1465 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.out +3439 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_flood.in +2757 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_flood.out +3126 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_small.in +731 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_small.out +2578 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_wide.in +528 -0
- package/eval/arena/warehouse/workspace/workloads/public/pub_wide.out +2026 -0
- package/eval/bomberman.py +20 -0
- package/eval/commit0.py +21 -0
- package/eval/common.py +61 -0
- package/eval/ctf.py +20 -0
- package/eval/featurebench.py +25 -0
- package/eval/land.py +20 -0
- package/eval/legacy/bomberman-prices.json +4 -0
- package/eval/legacy/bomberman.py +161 -0
- package/eval/legacy/commit0.py +91 -0
- package/eval/legacy/ctf-prices.json +4 -0
- package/eval/legacy/ctf.py +178 -0
- package/eval/legacy/featurebench.py +103 -0
- package/eval/legacy/land-prices.json +4 -0
- package/eval/legacy/land.py +177 -0
- package/eval/legacy/swe_bench_pro.py +116 -0
- package/eval/legacy/tank-prices.json +3 -0
- package/eval/legacy/tank.py +408 -0
- package/eval/legacy/warehouse-prices.json +4 -0
- package/eval/legacy/warehouse.py +365 -0
- package/eval/swe-bench-pro.py +25 -0
- package/eval/tank.py +20 -0
- package/eval/warehouse.py +20 -0
- package/lib/client/index.js +376 -63
- package/lib/client/index.js.map +1 -1
- package/lib/index.js +684 -27
- 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()
|