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,11 @@
|
|
|
1
|
+
# 复现正式赛
|
|
2
|
+
|
|
3
|
+
`evaluate.py`、`bt.py` 是正式赛用的评测和评分脚本,原样保留。`run.py` 是开源版加的小工具:先用正式赛的编译参数把 C++ 选手编译好,再调用 `evaluate.py`。
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
python tournament/run.py --formal --output rerun/对战 # 冻结赛程,8930 局;4 并发约需数小时,可用 --workers 调整
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`schedule-2to10.json.gz` 是正式赛的冻结赛程(地图种子、座位顺序),`--formal` 会自动解压使用。`evaluate.py` 会先核对选手包文件的哈希(`workspace/MANIFEST.json`),选手包里的文件被改动时会拒绝运行。
|
|
10
|
+
|
|
11
|
+
部分 bot 按时间控制搜索深度,所以逐局结果不保证与 `results/games.json.gz` 完全一致,但总排名应当一致。
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Bradley–Terry, equal total weight per match size, explicit baseline anchor."""
|
|
2
|
+
import math
|
|
3
|
+
from collections import Counter,defaultdict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def comparisons(rows,names,sizes):
|
|
7
|
+
valid=[row for row in rows if row['size'] in sizes and row['result']['valid_for_scoring']]
|
|
8
|
+
counts=Counter(row['size'] for row in valid)
|
|
9
|
+
if any(not counts[size] for size in sizes):raise ValueError('部分人数尚无有效对局')
|
|
10
|
+
pairs=defaultdict(lambda:[0.0,0.0])
|
|
11
|
+
total=len(valid)
|
|
12
|
+
for row in valid:
|
|
13
|
+
r=row['result'];n=row['size'];weight=total/(len(sizes)*counts[n]*(n*(n-1)/2))
|
|
14
|
+
for i in range(n):
|
|
15
|
+
for j in range(i+1,n):
|
|
16
|
+
a,b=r['names'][i],r['names'][j]
|
|
17
|
+
win=1.0 if r['ranks'][i]<r['ranks'][j] else 0.5 if r['ranks'][i]==r['ranks'][j] else 0.0
|
|
18
|
+
if a>b:a,b,win=b,a,1-win
|
|
19
|
+
pairs[a,b][0]+=weight;pairs[a,b][1]+=weight*win
|
|
20
|
+
if any(a not in names or b not in names for a,b in pairs):raise ValueError('未知选手')
|
|
21
|
+
return pairs,dict(counts)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def fit(rows,names,sizes,anchor):
|
|
25
|
+
if anchor not in names:raise ValueError('基准必须作为真实参赛者包含在名单中')
|
|
26
|
+
pairs,counts=comparisons(rows,names,sizes)
|
|
27
|
+
reached={anchor}
|
|
28
|
+
while True:
|
|
29
|
+
new=reached|{b for a,b in pairs if a in reached}|{a for a,b in pairs if b in reached}
|
|
30
|
+
if new==reached:break
|
|
31
|
+
reached=new
|
|
32
|
+
if reached!=set(names):raise ValueError('尚有选手未通过交手关系连接到基准')
|
|
33
|
+
# Same finite-estimate convention as the earlier territory contest:
|
|
34
|
+
# one virtual draw per observed pair, after size weighting.
|
|
35
|
+
games={pair:(g+1,w+.5) for pair,(g,w) in pairs.items()}
|
|
36
|
+
wins={name:0.0 for name in names}
|
|
37
|
+
for (a,b),(g,w) in games.items():wins[a]+=w;wins[b]+=g-w
|
|
38
|
+
strength={name:1.0 for name in names}
|
|
39
|
+
for iteration in range(20000):
|
|
40
|
+
denom={name:0.0 for name in names}
|
|
41
|
+
for (a,b),(g,w) in games.items():
|
|
42
|
+
v=g/(strength[a]+strength[b]);denom[a]+=v;denom[b]+=v
|
|
43
|
+
updated={name:wins[name]/denom[name] for name in names}
|
|
44
|
+
center=sum(math.log(v) for v in updated.values())/len(names)
|
|
45
|
+
updated={name:math.exp(math.log(v)-center) for name,v in updated.items()}
|
|
46
|
+
delta=max(abs(math.log(updated[n]/strength[n])) for n in names)
|
|
47
|
+
strength=updated
|
|
48
|
+
if delta<1e-11:break
|
|
49
|
+
else:raise ValueError('BT 未收敛,暂不输出实力值')
|
|
50
|
+
origin=math.log(strength[anchor])
|
|
51
|
+
rating={n:400/math.log(10)*(math.log(strength[n])-origin) for n in names}
|
|
52
|
+
rating={n:0.0 if abs(v)<1e-10 else v for n,v in rating.items()}
|
|
53
|
+
rating[anchor]=0.0
|
|
54
|
+
return dict(anchor=anchor,ratings=rating,valid_games_by_size=counts,iterations=iteration+1,
|
|
55
|
+
size_weight='equal_total',scale='400 * log10(strength / baseline_strength)',
|
|
56
|
+
smoothing='one virtual draw per observed pair after weighting')
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Evaluate a roster with explicit 2–10-player matches, no filler bots."""
|
|
2
|
+
import argparse
|
|
3
|
+
import hashlib
|
|
4
|
+
import itertools
|
|
5
|
+
import json
|
|
6
|
+
import random
|
|
7
|
+
import secrets
|
|
8
|
+
import sys
|
|
9
|
+
import time
|
|
10
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
11
|
+
from collections import deque
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from bt import fit
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def run_match(job):
|
|
17
|
+
"""One isolated match; preserve the frozen referee and its time limits."""
|
|
18
|
+
number,game,workspace,output,resume,bot_hashes=job
|
|
19
|
+
sys.path.insert(0,str(Path(workspace)/'engine'))
|
|
20
|
+
from arena import play
|
|
21
|
+
from verify_replay import verify
|
|
22
|
+
stem=Path(output)/'replays'/f'{number:05d}'
|
|
23
|
+
start=time.perf_counter()
|
|
24
|
+
if resume and stem.with_suffix('.json').exists():
|
|
25
|
+
verify(stem.with_suffix('.json'))
|
|
26
|
+
result=json.loads(stem.with_suffix('.json').read_text(encoding='utf-8'))['result']
|
|
27
|
+
if result['seed']!=game['seed'] or result['names']!=[n for n,_ in game['roster']]:
|
|
28
|
+
raise ValueError('Recovered match differs')
|
|
29
|
+
if any(result['hashes'].get(f'bot_{i}')!=bot_hashes[name] for i,(name,_) in enumerate(game['roster'])):
|
|
30
|
+
raise ValueError('Recovered bot hashes differ')
|
|
31
|
+
else:
|
|
32
|
+
result=play([n+'='+p for n,p in game['roster']],game['seed'],str(stem))
|
|
33
|
+
verify(stem.with_suffix('.json'))
|
|
34
|
+
return dict(match=number,size=game['size'],result=result),time.perf_counter()-start
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def match_results(jobs,workers):
|
|
38
|
+
if workers==1:
|
|
39
|
+
for job in jobs:yield run_match(job)
|
|
40
|
+
return
|
|
41
|
+
# Keep only a small bounded queue; checkpoint in original schedule order.
|
|
42
|
+
with ProcessPoolExecutor(max_workers=workers) as pool:
|
|
43
|
+
jobs=iter(jobs);pending=deque()
|
|
44
|
+
for _ in range(workers*2):
|
|
45
|
+
job=next(jobs,None)
|
|
46
|
+
if job is None:break
|
|
47
|
+
pending.append(pool.submit(run_match,job))
|
|
48
|
+
while pending:
|
|
49
|
+
yield pending.popleft().result()
|
|
50
|
+
job=next(jobs,None)
|
|
51
|
+
if job is not None:pending.append(pool.submit(run_match,job))
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def make_schedule(entrants, sizes, seed_count, seed):
|
|
55
|
+
count=len(entrants)
|
|
56
|
+
if count<2: raise ValueError('at least two entrants required')
|
|
57
|
+
if seed_count<1: raise ValueError('seed count must be positive')
|
|
58
|
+
if not sizes or len(set(sizes))!=len(sizes) or any(n<2 or n>min(10,count) for n in sizes):
|
|
59
|
+
raise ValueError('each match size must be unique and between 2 and min(10, entrant count)')
|
|
60
|
+
rng=random.Random(seed)
|
|
61
|
+
result=[]
|
|
62
|
+
for size in sizes:
|
|
63
|
+
seeds=rng.sample(range(1,2**31),seed_count)
|
|
64
|
+
for group in itertools.combinations(entrants,size):
|
|
65
|
+
for game_seed in seeds:
|
|
66
|
+
# The same roster orders and maps apply to each entrant group.
|
|
67
|
+
forward=list(group)
|
|
68
|
+
orders=[forward] if size<=2 else [forward,list(reversed(forward))]
|
|
69
|
+
for order in orders:
|
|
70
|
+
for shift in range(size):
|
|
71
|
+
result.append(dict(size=size,seed=game_seed,roster=order[shift:]+order[:shift]))
|
|
72
|
+
return result
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def report(rows, entrants, sizes, anchor=None):
|
|
76
|
+
lines=['# 对战统计','','单局分=生存分+0.5×击杀份额;第一率按单局合计分排名。',
|
|
77
|
+
'', '|人数|选手|有效局|场均生存分|场均击杀分|场均合计分|独占第一率|含并列第一率|故障局|',
|
|
78
|
+
'|---:|---|---:|---:|---:|---:|---:|---:|---:|']
|
|
79
|
+
for size in sizes:
|
|
80
|
+
for name,_ in entrants:
|
|
81
|
+
values=[]
|
|
82
|
+
for row in rows:
|
|
83
|
+
r=row['result']
|
|
84
|
+
if row['size']!=size or not r['valid_for_scoring'] or name not in r['names']:continue
|
|
85
|
+
i=r['names'].index(name);rank=r['ranks'][i];ties=r['ranks'].count(rank)
|
|
86
|
+
values.append((r['survival_points'][i],r['kill_points'][i],r['points'][i],
|
|
87
|
+
rank==1 and ties==1,rank==1,r['errors'][i]>0))
|
|
88
|
+
if not values:continue
|
|
89
|
+
av=[sum(v[k] for v in values)/len(values) for k in range(5)]
|
|
90
|
+
lines.append(f'|{size}|{name}|{len(values)}|{av[0]:.3f}|{av[1]:.3f}|{av[2]:.3f}|{av[3]:.1%}|{av[4]:.1%}|{sum(v[5] for v in values)}|')
|
|
91
|
+
if anchor:
|
|
92
|
+
lines+=['','## BT 实力值','','各人数总权重相等;基准为 0,固定尺度,不按最高分重缩放。','']
|
|
93
|
+
try:
|
|
94
|
+
result=fit(rows,[n for n,_ in entrants],sizes,anchor)
|
|
95
|
+
lines+=['|选手|BT 实力值|','|---|---:|']
|
|
96
|
+
for name,value in sorted(result['ratings'].items(),key=lambda x:-x[1]):
|
|
97
|
+
lines.append(f'|{name}|{value:.2f}|')
|
|
98
|
+
except ValueError as e:lines.append('暂不可汇总:'+str(e))
|
|
99
|
+
invalid=sum(not r['result']['valid_for_scoring'] for r in rows)
|
|
100
|
+
lines+=['',f'完成 {len(rows)} 局,启动失败不计分 {invalid} 局。',
|
|
101
|
+
'名次不能单独证明没有作弊;故障详情、源代码及运行记录需保留。']
|
|
102
|
+
return '\n'.join(lines)+'\n'
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def main():
|
|
106
|
+
ap=argparse.ArgumentParser()
|
|
107
|
+
ap.add_argument('--workspace',type=Path,required=True,help='clean referee workspace')
|
|
108
|
+
ap.add_argument('--bots',nargs='+',required=True,help='unique Name=program entries; order is frozen slot order')
|
|
109
|
+
ap.add_argument('--sizes',type=int,nargs='+',required=True,help='explicit match sizes, no fillers')
|
|
110
|
+
ap.add_argument('--anchor',required=True,help='name of the public baseline included in --bots')
|
|
111
|
+
ap.add_argument('--seeds',type=int,default=5,help='number of maps per size')
|
|
112
|
+
ap.add_argument('--seed',type=int,help='schedule seed; otherwise generated and recorded')
|
|
113
|
+
ap.add_argument('--output',type=Path,required=True)
|
|
114
|
+
ap.add_argument('--plan-only',action='store_true',help='save schedule without running bots')
|
|
115
|
+
ap.add_argument('--schedule',type=Path,help='frozen slot schedule; bot order defines slot IDs')
|
|
116
|
+
ap.add_argument('--resume',action='store_true',help='resume completed matches only after hash checks')
|
|
117
|
+
ap.add_argument('--workers',type=int,default=1,help='concurrent matches; calibrate on the host before increasing')
|
|
118
|
+
ap.add_argument('--report-every',type=int,default=25,help='refresh aggregate report every N games; results checkpoint every game')
|
|
119
|
+
args=ap.parse_args()
|
|
120
|
+
if args.workers<1 or args.report_every<1:ap.error('workers and report-every must be positive')
|
|
121
|
+
entrants=[]
|
|
122
|
+
for entry in args.bots:
|
|
123
|
+
name,sep,path=entry.partition('=')
|
|
124
|
+
if not sep or not name.strip() or not Path(path).is_file():ap.error('Use Name=existing-program entries')
|
|
125
|
+
if any(c in name for c in '|\n\r'):ap.error('Names cannot contain pipes or line breaks')
|
|
126
|
+
entrants.append((name,str(Path(path).resolve())))
|
|
127
|
+
if len({name for name,_ in entrants})!=len(entrants):ap.error('Names must be unique')
|
|
128
|
+
if args.anchor not in {name for name,_ in entrants}:ap.error('--anchor must name a bot included in --bots')
|
|
129
|
+
seed=args.seed if args.seed is not None else secrets.randbits(63)
|
|
130
|
+
if not args.schedule:
|
|
131
|
+
try:schedule=make_schedule(entrants,args.sizes,args.seeds,seed)
|
|
132
|
+
except ValueError as e:ap.error(str(e))
|
|
133
|
+
if args.schedule:
|
|
134
|
+
frozen=json.loads(args.schedule.read_text(encoding='utf-8'))
|
|
135
|
+
if frozen['sizes']!=args.sizes:ap.error('Frozen schedule sizes differ')
|
|
136
|
+
slot_ids={slot for g in frozen['entries'] for slot in g['slots']}
|
|
137
|
+
if slot_ids!=set(range(len(entrants))):ap.error('Frozen roster size differs')
|
|
138
|
+
if entrants[frozen['baseline_slot']][0]!=args.anchor:ap.error('Baseline must occupy its frozen slot')
|
|
139
|
+
schedule=[]
|
|
140
|
+
for g in frozen['entries']:
|
|
141
|
+
if not 2<=g['size']<=10 or len(g['slots'])!=g['size'] or len(set(g['slots']))!=g['size']:ap.error('Invalid frozen match')
|
|
142
|
+
schedule.append(dict(size=g['size'],seed=g['seed'],roster=[entrants[i] for i in g['slots']]))
|
|
143
|
+
seed=frozen['schedule_seed']
|
|
144
|
+
if args.output.exists() and not args.resume:ap.error('Output exists; use --resume or a new directory')
|
|
145
|
+
if args.resume and not args.output.exists():ap.error('Nothing to resume')
|
|
146
|
+
workspace=args.workspace.resolve()
|
|
147
|
+
manifest=json.loads((workspace/'MANIFEST.json').read_text(encoding='utf-8'))
|
|
148
|
+
for name,digest in manifest.items():
|
|
149
|
+
file=(workspace/name).resolve()
|
|
150
|
+
if not file.is_relative_to(workspace):ap.error('Invalid manifest path')
|
|
151
|
+
if hashlib.sha256(file.read_bytes()).hexdigest()!=digest:ap.error('Referee package modified: '+name)
|
|
152
|
+
sys.path.insert(0,str(workspace/'engine'))
|
|
153
|
+
from arena import play
|
|
154
|
+
from engine import VERSION
|
|
155
|
+
from verify_replay import verify
|
|
156
|
+
if args.schedule and frozen['version']!=VERSION:ap.error('Frozen schedule engine version differs')
|
|
157
|
+
identity=dict(version=VERSION,anchor=args.anchor,sizes=args.sizes,execution=dict(workers=args.workers),
|
|
158
|
+
scoring_code={p.name:hashlib.sha256(p.read_bytes()).hexdigest() for p in (Path(__file__),Path(__file__).with_name('bt.py'))},
|
|
159
|
+
engine_manifest=manifest,bot_hashes={name:hashlib.sha256(Path(path).read_bytes()).hexdigest() for name,path in entrants},
|
|
160
|
+
schedule=dict(schedule_seed=seed,entries=schedule))
|
|
161
|
+
identity=json.loads(json.dumps(identity))
|
|
162
|
+
rows=[]
|
|
163
|
+
if args.resume:
|
|
164
|
+
if json.loads((args.output/'identity.json').read_text(encoding='utf-8'))!=identity:ap.error('Referee, bots or schedule changed; resume rejected')
|
|
165
|
+
saved=args.output/'results.json'
|
|
166
|
+
if saved.exists():rows=json.loads(saved.read_text(encoding='utf-8'))
|
|
167
|
+
if len(rows)>len(schedule):ap.error('Too many saved results')
|
|
168
|
+
for number,row in enumerate(rows,1):
|
|
169
|
+
g=schedule[number-1];r=row['result']
|
|
170
|
+
if row['match']!=number or row['size']!=g['size'] or r['seed']!=g['seed'] or r['names']!=[n for n,_ in g['roster']]:ap.error('Saved match differs')
|
|
171
|
+
path=args.output/'replays'/f'{number:05d}.json';verify(path)
|
|
172
|
+
if json.loads(path.read_text(encoding='utf-8'))['result']!=r:ap.error('Saved result/replay mismatch')
|
|
173
|
+
else:
|
|
174
|
+
args.output.mkdir(parents=True)
|
|
175
|
+
(args.output/'identity.json').write_text(json.dumps(identity,ensure_ascii=False,indent=2),encoding='utf-8')
|
|
176
|
+
(args.output/'schedule.json').write_text(json.dumps(identity['schedule'],ensure_ascii=False,indent=2),encoding='utf-8')
|
|
177
|
+
print(f'{len(entrants)} entrants; sizes={args.sizes}; {len(schedule)} games; no fillers.',flush=True)
|
|
178
|
+
if args.plan_only:return
|
|
179
|
+
started=time.perf_counter();report_seconds=0;checkpoint_seconds=0;match_seconds=0
|
|
180
|
+
jobs=[(i,g,str(workspace),str(args.output),args.resume,identity['bot_hashes']) for i,g in enumerate(schedule,1) if i>len(rows)]
|
|
181
|
+
for row,duration in match_results(jobs,args.workers):
|
|
182
|
+
number=row['match'];result=row['result'];rows.append(row);match_seconds+=duration
|
|
183
|
+
tick=time.perf_counter()
|
|
184
|
+
temp=args.output/'results.tmp'
|
|
185
|
+
temp.write_text(json.dumps(rows,ensure_ascii=False,indent=2),encoding='utf-8');temp.replace(args.output/'results.json')
|
|
186
|
+
checkpoint_seconds+=time.perf_counter()-tick
|
|
187
|
+
if number%args.report_every==0 or number==len(schedule):
|
|
188
|
+
tick=time.perf_counter()
|
|
189
|
+
(args.output/'汇总.md').write_text(report(rows,entrants,args.sizes,args.anchor),encoding='utf-8')
|
|
190
|
+
report_seconds+=time.perf_counter()-tick
|
|
191
|
+
print(f'{number}/{len(schedule)} ranks={result["ranks"]} errors={result["errors"]}',flush=True)
|
|
192
|
+
try:rating=fit(rows,[n for n,_ in entrants],args.sizes,args.anchor)
|
|
193
|
+
except ValueError as e:rating={'unavailable':str(e)}
|
|
194
|
+
(args.output/'bt.json').write_text(json.dumps(rating,ensure_ascii=False,indent=2),encoding='utf-8')
|
|
195
|
+
(args.output/'performance.json').write_text(json.dumps(dict(workers=args.workers,wall_seconds=time.perf_counter()-started,
|
|
196
|
+
match_worker_seconds=match_seconds,checkpoint_seconds=checkpoint_seconds,report_seconds=report_seconds),indent=2),encoding='utf-8')
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
if __name__=='__main__':main()
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""先把 C++ 选手编译好,再调用正式赛用的 evaluate.py。挑战脚本和复现正式赛都走这里。
|
|
2
|
+
|
|
3
|
+
python tournament/run.py --output 输出目录 --sizes 2 5 10 --seeds 3 --bots 名字=程序.cpp ...
|
|
4
|
+
python tournament/run.py --formal --output 输出目录 复现正式赛(8930 局,冻结赛程)
|
|
5
|
+
"""
|
|
6
|
+
import argparse
|
|
7
|
+
import hashlib
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
|
|
14
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
15
|
+
HERE = Path(__file__).resolve().parent
|
|
16
|
+
ROOT = HERE.parent
|
|
17
|
+
FORMAL = ["GPT-6 Astra=bots/gpt-6-astra.cpp", "GPT-6 Sol=bots/gpt-6-sol.cpp", "Opus 5.5=bots/opus-5.5.cpp", "Kimi K3=bots/kimi-k3.cpp",
|
|
18
|
+
"GLM 5.3=bots/glm-5.3.cpp", "DeepSeek V4.1 Flash=bots/deepseek-v4.1-flash.cpp", "豆包 Seed 2.1=bots/doubao-seed-2.1.cpp",
|
|
19
|
+
"Qwen 3.8 Max=bots/qwen-3.8-max.cpp", "MiMo V2.6 Pro=bots/mimo-v2.6-pro.cpp", "Base=workspace/baseline.cpp"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def find_compiler():
|
|
23
|
+
cxx = os.environ.get("CXX") or shutil.which("g++") or shutil.which("clang++")
|
|
24
|
+
if not cxx and os.name == "nt": # 没加进 PATH 的 w64devkit:仓库根目录下的 tools/,或 C 盘根目录
|
|
25
|
+
for root in [*(a / "tools" for a in HERE.parents), Path("C:/")]:
|
|
26
|
+
if (root / "w64devkit/bin/g++.exe").is_file():
|
|
27
|
+
return str(root / "w64devkit/bin/g++.exe")
|
|
28
|
+
return cxx
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def compile_all(entries, build):
|
|
32
|
+
build.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
out = []
|
|
34
|
+
for e in entries:
|
|
35
|
+
name, src = e.split("=", 1)
|
|
36
|
+
src = (ROOT / src).resolve() if not Path(src).is_absolute() else Path(src)
|
|
37
|
+
if src.suffix.lower() != ".cpp":
|
|
38
|
+
out.append(f"{name}={src}"); continue
|
|
39
|
+
digest = hashlib.sha256(src.read_bytes()).hexdigest()[:12]
|
|
40
|
+
exe = build / f"{src.stem}-{digest}{'.exe' if os.name == 'nt' else ''}"
|
|
41
|
+
if not exe.is_file():
|
|
42
|
+
print(f"编译 {name}:{src.name}", flush=True)
|
|
43
|
+
cxx = find_compiler()
|
|
44
|
+
if not cxx:
|
|
45
|
+
raise SystemExit("没有找到 C++ 编译器(g++ 或 clang++),安装方法见仓库根目录 README。")
|
|
46
|
+
# 与正式赛相同的编译参数(见 workspace/engine/build.py)
|
|
47
|
+
r = subprocess.run([cxx, *(["-static"] if os.name == "nt" else []), "-std=c++20", "-O2", str(src), "-o", str(exe)],
|
|
48
|
+
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
|
49
|
+
if r.returncode or not exe.is_file():
|
|
50
|
+
raise SystemExit(f"{name} 编译失败:\n{r.stdout}\n{r.stderr}")
|
|
51
|
+
out.append(f"{name}={exe}")
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def main():
|
|
56
|
+
ap = argparse.ArgumentParser()
|
|
57
|
+
ap.add_argument("--output", type=Path, required=True)
|
|
58
|
+
ap.add_argument("--bots", nargs="+")
|
|
59
|
+
ap.add_argument("--sizes", type=int, nargs="+")
|
|
60
|
+
ap.add_argument("--seeds", type=int, default=3)
|
|
61
|
+
ap.add_argument("--formal", action="store_true", help="用冻结的正式赛赛程(2–10 人,8930 局)")
|
|
62
|
+
ap.add_argument("--workers", type=int, default=max(1, min(8, (os.cpu_count() or 2) // 2)))
|
|
63
|
+
a = ap.parse_args()
|
|
64
|
+
bots = compile_all(FORMAL if a.formal else a.bots, ROOT / "workspace" / "build")
|
|
65
|
+
args = [sys.executable, "-X", "utf8", "-u", str(HERE / "evaluate.py"), "--workspace", str(ROOT / "workspace"), "--bots", *bots,
|
|
66
|
+
"--anchor", "Base", "--output", str(a.output), "--workers", str(a.workers), "--report-every", "100"]
|
|
67
|
+
if a.formal:
|
|
68
|
+
import gzip
|
|
69
|
+
sched = a.output.parent / "schedule-2to10.json"
|
|
70
|
+
sched.parent.mkdir(parents=True, exist_ok=True)
|
|
71
|
+
sched.write_bytes(gzip.decompress((HERE / "schedule-2to10.json.gz").read_bytes()))
|
|
72
|
+
args += ["--sizes", *map(str, range(2, 11)), "--schedule", str(sched)]
|
|
73
|
+
else:
|
|
74
|
+
args += ["--sizes", *map(str, a.sizes), "--seeds", str(a.seeds)]
|
|
75
|
+
raise SystemExit(subprocess.call(args))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
if __name__ == "__main__":
|
|
79
|
+
main()
|
|
Binary file
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"baseline.cpp": "a4ca3e21d2f8a75ca9a0c52fbec8d46442ba62031f0f666a825f76edbc573097",
|
|
3
|
+
"clock.py": "9841b6a5485507b8cb40a6391e9d984b4a1c7d275f4644df344aa206f0077742",
|
|
4
|
+
"engine/arena.py": "293b5f7f63cc93b39fe2a480ed7a07053d00e5d64f1c81299628368ad44668ff",
|
|
5
|
+
"engine/build.py": "d430ff44d877a6f8d1e71d1084e00f8b80abab0535e2b914fdf26deec0bd10ab",
|
|
6
|
+
"engine/engine.py": "e543412fce0c918825b741ec53f751a94daedcb288905ae261f9a3140bf57396",
|
|
7
|
+
"engine/verify_replay.py": "5d54e98fe4385a4cf31a5c08cde9363d8128e2a6252fa5b65873fe3583ae0164",
|
|
8
|
+
"README.md": "6880de795f428f1ba2dd74074f04207b14cb02fac7b3c7da5c2b33c3e33bf5f3",
|
|
9
|
+
"RULES.md": "374f9a615babde524517fa690e94fec7ccbb357782cd3fe2c002c0e297573c6b",
|
|
10
|
+
"run.cmd": "b526ba862199ae83f0c97f5a8cfa103d8e3881d24994ab49d2daa16c336a658a",
|
|
11
|
+
"run.sh": "2e41d6f16386eb3ccc069e3d911a89cc4aac67ef61bce28e38e9204c780b8c4b",
|
|
12
|
+
"viewer/index.html": "d3e14ae5c5c4d67b7d676965d2eb0ea76cd8660d488679d20044121d63a4493c"
|
|
13
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# 炸弹人对抗
|
|
2
|
+
|
|
3
|
+
编写尽可能强的 bot,同一程序须支持2–10人。游戏规则和通信协议见 RULES.md;baseline.cpp 是可修改的公开练习对手。各场人数由实际参赛配置决定。正式赛多轮对战,使用 Bradley–Terry 模型拟合实力值,公开基准为0分线。单局分为生存分+0.5×击杀份额,按合计分排名,同分并列。
|
|
4
|
+
|
|
5
|
+
解压起计时,争取60分钟内完成开发与测试,90分钟必须停止修改并交卷,无需启动计时器。允许本地工具、编译、模拟与复盘;禁止联网、调用其他AI、读取其他选手资料、修改裁判或计时器、干扰对手。比赛中的 bot 按标准输入输出协议独立决策,不访问文件或网络。
|
|
6
|
+
|
|
7
|
+
提交 `submission/bot.cpp`(C++20),简述策略。
|
|
8
|
+
|
|
9
|
+
## 本地使用
|
|
10
|
+
|
|
11
|
+
以下在 workspace 目录运行。Windows 使用 `run.cmd`;Linux 替换为 `sh run.sh`,并去掉可执行文件名的 `.exe`。只需要现有 Python;C++程序还需要现有编译器,无第三方Python依赖。
|
|
12
|
+
|
|
13
|
+
- 剩余时间:`run.cmd clock.py`
|
|
14
|
+
- 编译基准:`run.cmd engine/build.py --compile --output baseline.exe`
|
|
15
|
+
- 编译C++提交:`run.cmd engine/build.py --compile --source submission/bot.cpp --output bot.exe`
|
|
16
|
+
- 对战:`run.cmd engine/arena.py --bots Mine=bot.exe Base=baseline.exe --seed 1 --replay replays/duel.html`
|
|
17
|
+
- 文本复盘:`run.cmd engine/arena.py --inspect replays/duel.json --turn 20 --context 2`
|
|
18
|
+
|
|
19
|
+
多人对战增加 `Name=程序路径`,每个名字唯一,总计2–10个;可加入重复基准实例练习,但正式赛程不默认补位。带空格的路径需将整个 `Name=路径` 参数加引号。
|
|
20
|
+
|
|
21
|
+
每局自动保存 JSON 和 HTML。HTML可逐帧查看该步动作、实际落点、事件、最终得分和名次;文本复盘还展示完整棋盘与故障详情。结合具体回合改进,不必提交测试报告。
|
|
22
|
+
|
|
23
|
+
## 时间与环境
|
|
24
|
+
|
|
25
|
+
开发90分钟与回合预算不同:bot首回合2000ms,之后50ms。回合限时包含通信与系统调度。
|
|
26
|
+
|
|
27
|
+
用 `clock.py` 查看剩余时间,无需初始化。不要覆盖解压、修改计时器或调整系统时间;到时自行停止修改并交卷。
|
|
28
|
+
|
|
29
|
+
环境故障请报告组织方,不修改系统权限、防护或安装软件。编译命令只编译,不会额外启动你的程序;运行能力通过对战验证。
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# 炸弹人规则 v0.5-public-state(公开)
|
|
2
|
+
|
|
3
|
+
## 目标与行动
|
|
4
|
+
|
|
5
|
+
每人控制一个角色,争取最高的生存与击杀合计分。每回合所有存活玩家看到相同的完整公开状态,独立选择 U/D/L/R/S(上下左右/等待)与放弹位 0/1。没有攻击按钮、装备、道具、生命值或复活。角色被爆炸、残留火焰、缩圈击中即淘汰。
|
|
6
|
+
|
|
7
|
+
坐标从 0 开始,左上角 (0,0),x 向右,y 向下。`#` 永久墙,`+` 可破坏箱子,`.` 空地。玩家不能重叠;允许相邻两人同时交换位置,也允许多人形成移动环。不因碰撞死亡。非法地形移动原地停留,方向可以直接反向。
|
|
8
|
+
|
|
9
|
+
所有人数:炸弹容量 2,爆炸射程 3 格(不含中心),引信 4 回合,火焰覆盖爆炸回合及下一回合。绝对行动回合 t 从 0 开始;t 放的炸弹在 t+4 的爆炸阶段引爆。放弹在原位置,随后移动;放弹和移动可以同回合执行。
|
|
10
|
+
|
|
11
|
+
## 各人数参数
|
|
12
|
+
|
|
13
|
+
| 人数 | 棋盘 | 回合上限 | 首次缩圈的行动回合 | 每隔几回合再缩一层 |
|
|
14
|
+
|---:|---:|---:|---:|---:|
|
|
15
|
+
|2|13×13|300|150|20|
|
|
16
|
+
|3|15×15|340|175|20|
|
|
17
|
+
|4|17×17|450|250|25|
|
|
18
|
+
|5|19×19|475|225|25|
|
|
19
|
+
|6|21×21|525|250|25|
|
|
20
|
+
|7|21×21|550|275|25|
|
|
21
|
+
|8|23×23|600|300|25|
|
|
22
|
+
|9|25×25|650|325|25|
|
|
23
|
+
|10|25×25|675|350|25|
|
|
24
|
+
|
|
25
|
+
2、4人使用表中固定参数;其余人数参数公式:边长 `2*ceil(sqrt(14*p))+1`,首缩 `150+25*(p-2)`,上限 `首缩+间隔*(边长//2)+25`。3 人间隔20,其余25。后期会自然缩至无可生存格,所以正常游戏通常早于上限结束。
|
|
26
|
+
|
|
27
|
+
## 地图
|
|
28
|
+
|
|
29
|
+
外圈为永久墙;内部 x、y 同为偶数的格子也是永久墙。其余格子按生成器以 0.42 概率放箱子;出生点曼哈顿距离2内不放箱子。出生位置至少两条初始可走出口,清除箱子后的道路连通。箱子分布开局公开,对局过程无随机数。
|
|
30
|
+
|
|
31
|
+
2 人出生在对角,地图180°对称;4人在四角,地图90°对称。其他人数在内侧边界的奇坐标位置环上等距取点(整数取整),种子决定起点,箱子独立生成。这些人数不保证单局出生点完全等价。地图生成代码见 `make_map()`。
|
|
32
|
+
|
|
33
|
+
## 一回合的精确阶段
|
|
34
|
+
|
|
35
|
+
输入是本回合开始、尚未执行以下阶段的状态。按顺序处理:
|
|
36
|
+
|
|
37
|
+
1. **清理和缩圈**:删除已过期火焰;所有协议错误/超时玩家同回合淘汰;到缩圈回合时,将距外边界不大于新层数的格子变成永久墙。首缩是层1(原外墙层0)。新墙上的玩家立即死亡,不能在该回合移动逃出。新墙上的炸弹直接移除,不爆炸;箱子和火焰也移除。
|
|
38
|
+
2. **放弹**:在玩家移动前的位置尝试放置。存活、当前位置无炸弹、自己的在场炸弹少于2,才有资格。多个有资格玩家在同格同时请求放弹时,所有请求失败,避免按 ID 偏袒。没资格的请求不参与冲突。放弹失败不影响移动,也不算协议错误。本回合即将爆炸的旧炸弹仍占用容量。
|
|
39
|
+
3. **移动**:上下左右移动一格或等待,所有玩家同时处理。墙、箱子、炸弹阻挡进入;被阻挡则原地停留。玩家可站在脚下原有/刚放的炸弹上等待,也可以离开;离开后不可走回仍有炸弹的格子。地形判定后,若多个玩家的目标相同(包括等待者的当前位置),这些玩家全部停留。随后若某人的目标被已停留者占用,他也停留,反复传播直至稳定。其余移动同时成功,包括两人交换、多人成环、跟随离开的玩家。没有按 ID 抢占的优先级。
|
|
40
|
+
4. **爆炸**:计时到期或处于旧火焰上的炸弹引爆,向四方向延伸最多3格。永久墙阻断且不着火;箱子格着火、箱子被毁,但阻断继续传播;炸弹格着火、立即连锁引爆,同时阻断这一条射线。所有爆炸统一使用本阶段开始时的箱子/炸弹分布:先炸掉箱子不让本回合另一股火焰穿过去,先爆炸的炸弹仍是这个阶段其他射线的阻挡物。
|
|
41
|
+
5. **火焰与淘汰**:删除引爆炸弹,移除被炸箱子,火焰在爆炸回合 t 和 t+1 有效。移动结束后处于旧/新火焰的玩家同时淘汰。死者已放的炸弹保留、继续计时,除非后来被缩圈删除。然后回合数加1。
|
|
42
|
+
|
|
43
|
+
所有阶段执行完整后才判断结束。即使缩圈后只剩一人,他仍需经受本回合爆炸;可以最终无人存活。同回合不同阶段死亡者仍并列。
|
|
44
|
+
|
|
45
|
+
## 结束与排名
|
|
46
|
+
|
|
47
|
+
存活人数≤1,或达到回合上限,结束。单局分 = 生存分 + 0.5 × 击杀份额。
|
|
48
|
+
|
|
49
|
+
- 生存分:每多活过一名对手得 1 分;与一名对手同回合淘汰或同为终局幸存者,彼此各得 0.5 分。终局幸存者比所有死者晚淘汰。设人数为 n、生存名次为 r、该名次人数为 t,则生存分为 `n-r-(t-1)/2`。
|
|
50
|
+
- 击杀份额:死者所在格的有效火焰按炸弹原主人归属,排除死者本人,不同主人均分一个击杀。同一主人多股火焰只算一次;连锁爆炸不转移下游炸弹归属;旧火焰保留来源;已死亡的主人仍可获份额。缩圈、协议错误、仅自身火焰造成的死亡没有击杀份额。
|
|
51
|
+
- 按单局分从高到低排名,同分并列,不另设破平条件。名次为 `1 + 分数严格高于自己的玩家数`。裁判使用精确分数比较;最后幸存者不保证单局第一。
|
|
52
|
+
|
|
53
|
+
例如五人局:第二名生存分 3,独占三个击杀得 1.5,合计 4.5;零击杀的最后幸存者得 4 分。
|
|
54
|
+
|
|
55
|
+
放弹数、破箱数仅展示,不计分。一个箱子同回合被多个玩家炸到时,每个命中的玩家都记一次破箱。
|
|
56
|
+
|
|
57
|
+
正式赛按单局排名转换为两两胜负,平局计半胜,用 Bradley–Terry 模型拟合实力。不同参赛人数的比赛总权重相等,公开基准锚定 0;不按当期最高分重新缩放。
|
|
58
|
+
|
|
59
|
+
本届九位选手及一个公开基准参加2、5、10人三档比赛,各档总权重为1/3,每档20张地图。双人全配对并交换座位;五人平衡分组并正反轮换座位;十人全员正反轮换座位。合计2600局,各选手960局。五人赛每对选手交手80、90或100局,各选手总出场及各座位次数相同。
|
|
60
|
+
|
|
61
|
+
BT 显示值为 `400×log10(选手实力/基准实力)`,弱于基准显示负值。每对已交手选手在加权统计后加入一次虚拟平局,使全胜或全负也得到有限估计。
|
|
62
|
+
|
|
63
|
+
## 文本协议
|
|
64
|
+
|
|
65
|
+
进程启动后仅一次:
|
|
66
|
+
|
|
67
|
+
```text
|
|
68
|
+
INIT 边长 玩家数 自己ID 回合上限 引信 射程 容量 火焰回合数 首缩回合 缩圈间隔
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
每回合:
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
TURN t
|
|
75
|
+
BOARD
|
|
76
|
+
...(边长行,字符 # + .)
|
|
77
|
+
P id x y alive
|
|
78
|
+
...(玩家数行,alive为0/1,ID从0起)
|
|
79
|
+
BOMBS k
|
|
80
|
+
B owner x y explode_at
|
|
81
|
+
...(k行,explode_at为绝对行动回合)
|
|
82
|
+
FLAMES f
|
|
83
|
+
F x y last_lethal_turn
|
|
84
|
+
...(f行,最后致命回合含端点)
|
|
85
|
+
STATUS 玩家数
|
|
86
|
+
S id death_turn kill_numerator kill_denominator
|
|
87
|
+
...(玩家数行;存活者death_turn为-1,击杀份额为精确分数)
|
|
88
|
+
SOURCES s
|
|
89
|
+
O x y owner last_lethal_turn
|
|
90
|
+
...(s行;当前仍有效的每格火焰来源,每位主人一行)
|
|
91
|
+
END
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
STATUS 是截至本次输入的累计击杀份额与淘汰回合;SOURCES 保留各主人各自的火焰到期时间。所有选手接收相同公开状态,自身ID由INIT指定。必须读取至END后再回答。
|
|
95
|
+
|
|
96
|
+
回答恰一行,单空格分隔:`t U 0`。大写 U/D/L/R/S;0/1;行尾换行并 flush。支持 LF/CRLF,每行最多256字节。禁止提前输出未来回合,禁止 stdout 调试内容,非法输出或超时立即淘汰。
|
|
97
|
+
|
|
98
|
+
首回合2000ms,之后每回合50ms。从开始发送输入到完整回答行抵达计时,包含输入输出和系统调度;进程创建时间不计入首回合预算。
|
|
99
|
+
|
|
100
|
+
程序超时、退出或违反输出协议时淘汰。
|