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.
- package/README.md +91 -13
- package/assets/bomberman/workspace.zip +0 -0
- package/assets/commit0/instance.json +30 -0
- package/assets/commit0/workspace.zip +0 -0
- package/assets/ctf/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/assets/tank/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,862 @@
|
|
|
1
|
+
"""本地对战与复盘。
|
|
2
|
+
|
|
3
|
+
arena.py check 检查环境
|
|
4
|
+
arena.py play A B [--seed N] [--style S] [--out 名字] 正式模式打一局(A 坐 0 号位),存回放
|
|
5
|
+
arena.py show 回放 回合 [--around K] 在终端里看某几个回合
|
|
6
|
+
arena.py batch A B [--games N] [--seed N] [--workers W] 正式模式批量,每张图换边各打一局
|
|
7
|
+
arena.py fast A B [--games N] [--seed N] [--workers W] 快速模式批量(只支持源码)
|
|
8
|
+
arena.py vs 新版 旧版 [--games N] [--mode fast|formal] 两个版本对比:胜平负、得分率和误差范围、有效样本
|
|
9
|
+
arena.py expand 回放 [--out 文件] 把回放展开成逐回合的局面和事件(JSON)
|
|
10
|
+
arena.py html 回放 [--out 文件] 把回放做成一个打开就能看的网页(play 存回放时会自动做一份)
|
|
11
|
+
arena.py cpus 列出本机的处理器拓扑(大核、小核、超线程)
|
|
12
|
+
arena.py speed [--cpu N] 测速:跑一段固定工作量,看机器状态有没有变
|
|
13
|
+
|
|
14
|
+
A、B 可以是 .cpp 源码(自动编译,源码没变不重编)或已经编译好的程序。
|
|
15
|
+
每次运行都会把参与的源码按内容存一份到 history/,并在 history/log.jsonl 记一行。
|
|
16
|
+
"""
|
|
17
|
+
import argparse
|
|
18
|
+
import json
|
|
19
|
+
import math
|
|
20
|
+
import os
|
|
21
|
+
import queue
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
import threading
|
|
25
|
+
import time
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
HERE = Path(__file__).resolve().parent
|
|
29
|
+
sys.path.insert(0, str(HERE))
|
|
30
|
+
import core # noqa: E402
|
|
31
|
+
import engine # noqa: E402
|
|
32
|
+
|
|
33
|
+
HISTORY = HERE / "history"
|
|
34
|
+
REPLAYS = HERE / "replays"
|
|
35
|
+
PARALLEL_NOTE = ("提醒:并行越多,同样的预算能做的事越少(抢缓存、超线程、落到小核上),用量也会有出入。"
|
|
36
|
+
"判断是否接近 {turn_ms} {unit}的时限,请以单局 play 的结果为准。")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def unit_of(inf):
|
|
40
|
+
return "CPU 毫秒" if inf["budget_kind"] == "cpu" else "毫秒"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def parse_pin(text):
|
|
44
|
+
""""16:18" 或 "16,17:18,19" → [[16], [18]]:冒号前是 A 绑的逻辑处理器,冒号后是 B 的。"""
|
|
45
|
+
try:
|
|
46
|
+
a, b = text.split(":")
|
|
47
|
+
return [[int(x) for x in a.split(",") if x != ""], [int(x) for x in b.split(",") if x != ""]]
|
|
48
|
+
except ValueError:
|
|
49
|
+
raise SystemExit(f"--pin 的写法是 A的处理器:B的处理器,比如 16:18 或 16,17:18,19;收到的是 {text}")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ====================================================================
|
|
53
|
+
# 参与者与版本存档
|
|
54
|
+
# ====================================================================
|
|
55
|
+
|
|
56
|
+
class Entry:
|
|
57
|
+
"""一个参与者:源码或可执行文件。"""
|
|
58
|
+
|
|
59
|
+
def __init__(self, spec):
|
|
60
|
+
self.spec = spec
|
|
61
|
+
self.path = find_program(spec)
|
|
62
|
+
self.is_source = core.is_source(self.path)
|
|
63
|
+
self.name = self.path.stem
|
|
64
|
+
self.data = self.path.read_bytes()
|
|
65
|
+
self.sha = core.sha_of(self.data)
|
|
66
|
+
self.saved = None # 存档里的文件名(只有源码才存)
|
|
67
|
+
|
|
68
|
+
def version(self):
|
|
69
|
+
return {"spec": self.spec, "name": self.name, "kind": "source" if self.is_source else "program",
|
|
70
|
+
"sha256": self.sha, "short": self.sha[:12], "saved": self.saved}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def find_program(spec):
|
|
74
|
+
tried = [Path(spec), HERE / spec]
|
|
75
|
+
if os.name == "nt" and not str(spec).lower().endswith((".exe", ".cpp")):
|
|
76
|
+
tried += [Path(spec + ".exe"), HERE / (spec + ".exe")]
|
|
77
|
+
for p in tried:
|
|
78
|
+
if p.is_file():
|
|
79
|
+
return p.resolve()
|
|
80
|
+
raise SystemExit(f"找不到程序:{spec}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def unique_names(entries):
|
|
84
|
+
names, seen = [], {}
|
|
85
|
+
for e in entries:
|
|
86
|
+
seen[e.name] = seen.get(e.name, 0) + 1
|
|
87
|
+
names.append(e.name if seen[e.name] == 1 else f"{e.name}#{seen[e.name]}")
|
|
88
|
+
return names
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def history_lock():
|
|
92
|
+
"""history/ 的跨进程锁:锁住 history/.lock 的第一个字节,返回文件描述符;拿不到返回 None。用完交给 history_unlock。
|
|
93
|
+
|
|
94
|
+
几个 arena.py 命令同时运行时,存档和追加运行记录都要一个一个来。锁挂在打开的文件上,进程不管怎么结束
|
|
95
|
+
系统都会放掉它,不会留下死锁。最多等 5 秒,等不到就不加锁往下做(宁可偶尔出一次错,也不卡住命令)。"""
|
|
96
|
+
fd = None
|
|
97
|
+
try:
|
|
98
|
+
HISTORY.mkdir(parents=True, exist_ok=True)
|
|
99
|
+
fd = os.open(HISTORY / ".lock", os.O_RDWR | os.O_CREAT, 0o644)
|
|
100
|
+
deadline = time.time() + 5.0
|
|
101
|
+
while True:
|
|
102
|
+
try:
|
|
103
|
+
if os.name == "nt":
|
|
104
|
+
import msvcrt
|
|
105
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
106
|
+
msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)
|
|
107
|
+
else:
|
|
108
|
+
import fcntl
|
|
109
|
+
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
110
|
+
return fd
|
|
111
|
+
except OSError:
|
|
112
|
+
if time.time() > deadline:
|
|
113
|
+
break
|
|
114
|
+
time.sleep(0.02)
|
|
115
|
+
except Exception:
|
|
116
|
+
pass
|
|
117
|
+
if fd is not None:
|
|
118
|
+
os.close(fd)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def history_unlock(fd):
|
|
123
|
+
if fd is None:
|
|
124
|
+
return
|
|
125
|
+
try:
|
|
126
|
+
if os.name == "nt":
|
|
127
|
+
import msvcrt
|
|
128
|
+
os.lseek(fd, 0, os.SEEK_SET)
|
|
129
|
+
msvcrt.locking(fd, msvcrt.LK_UNLCK, 1)
|
|
130
|
+
except OSError:
|
|
131
|
+
pass
|
|
132
|
+
finally:
|
|
133
|
+
os.close(fd) # 关掉文件,没放开的锁也随之放开
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def store_version(e):
|
|
137
|
+
"""把一份源码存进 history/,返回存档里的文件名。同一内容已经存过(包括别的命令刚刚存的)就用已有的那份。"""
|
|
138
|
+
short = e.sha[:12]
|
|
139
|
+
tmp = HISTORY / f"tmp-{os.getpid()}-{short}"
|
|
140
|
+
problem = None
|
|
141
|
+
try:
|
|
142
|
+
for attempt in range(8):
|
|
143
|
+
old = sorted(HISTORY.glob(f"*-{short}-*.cpp"))
|
|
144
|
+
if old:
|
|
145
|
+
return old[0].name
|
|
146
|
+
name = f"{time.strftime('%Y%m%d-%H%M%S')}-{short}-{core.safe_name(e.name, 'bot')}.cpp"
|
|
147
|
+
try:
|
|
148
|
+
tmp.write_bytes(e.data)
|
|
149
|
+
os.replace(tmp, HISTORY / name)
|
|
150
|
+
return name
|
|
151
|
+
except OSError as exc: # 文件正被别的进程开着(另一个命令、杀毒软件)时 Windows 会拒绝:等一下再试
|
|
152
|
+
problem = exc
|
|
153
|
+
time.sleep(0.05 * (attempt + 1))
|
|
154
|
+
raise problem
|
|
155
|
+
finally:
|
|
156
|
+
try:
|
|
157
|
+
tmp.unlink()
|
|
158
|
+
except OSError:
|
|
159
|
+
pass
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def archive(entries):
|
|
163
|
+
"""把每份源码按内容存进 history/(同一内容只存一次)。任何失败都只提示,不影响命令本身。
|
|
164
|
+
|
|
165
|
+
几个命令同时启动、带着同一份新源码时,会在同一秒往同一个文件名上写:加锁让它们一个一个来,
|
|
166
|
+
后到的直接用先到的那份。"""
|
|
167
|
+
for e in entries:
|
|
168
|
+
if not e.is_source:
|
|
169
|
+
continue
|
|
170
|
+
lock = history_lock()
|
|
171
|
+
try:
|
|
172
|
+
e.saved = store_version(e)
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
print(f"(版本存档没有成功:{exc}。不影响本次运行。)", file=sys.stderr)
|
|
175
|
+
finally:
|
|
176
|
+
history_unlock(lock)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def write_log(command, argv, entries, summary, started, error=None):
|
|
180
|
+
"""在 history/log.jsonl 追加一行。任何失败都只提示,不影响命令本身。
|
|
181
|
+
|
|
182
|
+
追加时加锁:Windows 上"移到文件末尾"和"写"是两步,几个命令同时写完时会写到同一个位置、互相盖掉。"""
|
|
183
|
+
try:
|
|
184
|
+
record = {"time": time.strftime("%Y-%m-%dT%H:%M:%S%z", time.localtime(started)), "epoch": round(started, 3),
|
|
185
|
+
"seconds": round(time.time() - started, 2), "command": command, "argv": list(argv),
|
|
186
|
+
"versions": [e.version() for e in entries], "summary": summary}
|
|
187
|
+
if error:
|
|
188
|
+
record["error"] = str(error)[:500]
|
|
189
|
+
HISTORY.mkdir(parents=True, exist_ok=True)
|
|
190
|
+
line = (json.dumps(record, ensure_ascii=False) + "\n").encode("utf-8")
|
|
191
|
+
lock = history_lock()
|
|
192
|
+
try:
|
|
193
|
+
fd = os.open(HISTORY / "log.jsonl", os.O_WRONLY | os.O_APPEND | os.O_CREAT | getattr(os, "O_BINARY", 0), 0o644)
|
|
194
|
+
try:
|
|
195
|
+
os.write(fd, line)
|
|
196
|
+
finally:
|
|
197
|
+
os.close(fd)
|
|
198
|
+
finally:
|
|
199
|
+
history_unlock(lock)
|
|
200
|
+
except Exception as exc:
|
|
201
|
+
print(f"(运行记录没有写成:{exc}。不影响本次运行。)", file=sys.stderr)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def build(entry, kind):
|
|
205
|
+
"""编译一个参与者。kind 为 "exe"(正式模式)或 "dll"(快速模式)。"""
|
|
206
|
+
if not entry.is_source:
|
|
207
|
+
if kind == "dll":
|
|
208
|
+
raise SystemExit(f"快速模式只支持源码:{entry.spec} 是已经编译好的程序。和它对战请用 batch,或 vs --mode formal。")
|
|
209
|
+
return str(entry.path)
|
|
210
|
+
try:
|
|
211
|
+
return str(core.build_bot(entry.path, kind))
|
|
212
|
+
except RuntimeError as exc:
|
|
213
|
+
raise SystemExit(f"{entry.spec}:{exc}")
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
# ====================================================================
|
|
217
|
+
# 批量对战
|
|
218
|
+
# ====================================================================
|
|
219
|
+
|
|
220
|
+
def make_jobs(games, seed, style):
|
|
221
|
+
"""每张图换边各打一局:第 k 张图的种子是 seed + k,先 A 坐 0 号位,再 B 坐 0 号位。"""
|
|
222
|
+
return [(seed + k // 2, style, k % 2) for k in range(games)]
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def run_formal(commands, names, jobs, workers, record=False, pins=None):
|
|
226
|
+
"""正式模式打一批。每个并行位是一个独立的 Python 进程(自己起的),逐局领任务。返回与 jobs 等长的列表。
|
|
227
|
+
|
|
228
|
+
record=True 时每个结果另带 "orders"(逐回合实际执行的命令)、"final"(终局回合段)、"faults"、"stderr"。
|
|
229
|
+
pins:可选,每个并行位一对 [A 的逻辑处理器列表, B 的],这个并行位上的 bot 进程绑在这些处理器上。"""
|
|
230
|
+
results = [None] * len(jobs)
|
|
231
|
+
todo = queue.Queue()
|
|
232
|
+
for k in range(len(jobs)):
|
|
233
|
+
todo.put(k)
|
|
234
|
+
procs, lock = [], threading.Lock()
|
|
235
|
+
stop = threading.Event()
|
|
236
|
+
|
|
237
|
+
def start():
|
|
238
|
+
p = subprocess.Popen([sys.executable, "-X", "utf8", str(HERE / "arena.py"), "_worker"],
|
|
239
|
+
stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=str(HERE))
|
|
240
|
+
core.adopt(p)
|
|
241
|
+
with lock:
|
|
242
|
+
procs.append(p)
|
|
243
|
+
return p
|
|
244
|
+
|
|
245
|
+
def work(slot):
|
|
246
|
+
proc = None
|
|
247
|
+
pin = pins[slot] if pins else None
|
|
248
|
+
while not stop.is_set():
|
|
249
|
+
try:
|
|
250
|
+
k = todo.get_nowait()
|
|
251
|
+
except queue.Empty:
|
|
252
|
+
break
|
|
253
|
+
seed, style, swap = jobs[k]
|
|
254
|
+
order = [1, 0] if swap else [0, 1]
|
|
255
|
+
job = {"cmds": [commands[i] for i in order], "names": [names[i] for i in order], "seed": seed,
|
|
256
|
+
"style": style, "record": record,
|
|
257
|
+
"affinity": [pin[i] for i in order] if pin else None}
|
|
258
|
+
try:
|
|
259
|
+
if proc is None or proc.poll() is not None:
|
|
260
|
+
proc = start()
|
|
261
|
+
proc.stdin.write((json.dumps(job) + "\n").encode("utf-8"))
|
|
262
|
+
proc.stdin.flush()
|
|
263
|
+
raw = proc.stdout.readline()
|
|
264
|
+
if not raw:
|
|
265
|
+
raise RuntimeError(f"裁判进程异常退出(代码 {proc.poll()})")
|
|
266
|
+
out = json.loads(raw.decode("utf-8"))
|
|
267
|
+
except Exception as exc:
|
|
268
|
+
if stop.is_set():
|
|
269
|
+
break
|
|
270
|
+
out = {"error": f"{type(exc).__name__}: {exc}"}
|
|
271
|
+
core.stop_process(proc)
|
|
272
|
+
proc = None
|
|
273
|
+
results[k] = out
|
|
274
|
+
if proc is not None:
|
|
275
|
+
try:
|
|
276
|
+
proc.stdin.close()
|
|
277
|
+
proc.wait(timeout=5)
|
|
278
|
+
except Exception:
|
|
279
|
+
pass
|
|
280
|
+
|
|
281
|
+
threads = [threading.Thread(target=work, args=(w,), daemon=True) for w in range(max(1, min(workers, len(jobs))))]
|
|
282
|
+
try:
|
|
283
|
+
for t in threads:
|
|
284
|
+
t.start()
|
|
285
|
+
while any(t.is_alive() for t in threads):
|
|
286
|
+
for t in threads:
|
|
287
|
+
t.join(timeout=0.2)
|
|
288
|
+
except KeyboardInterrupt:
|
|
289
|
+
stop.set()
|
|
290
|
+
raise
|
|
291
|
+
finally:
|
|
292
|
+
stop.set()
|
|
293
|
+
with lock:
|
|
294
|
+
for p in procs: # 只结束自己起的进程;它们起的 bot 进程会跟着结束
|
|
295
|
+
core.stop_process(p)
|
|
296
|
+
out = []
|
|
297
|
+
for (seed, style, swap), r in zip(jobs, results):
|
|
298
|
+
if r is None or "error" in r:
|
|
299
|
+
out.append({"seed": seed, "style": style, "swap": swap, "error": (r or {}).get("error", "没有跑完")})
|
|
300
|
+
continue
|
|
301
|
+
order = [1, 0] if swap else [0, 1] # order[座位] = 第几个参与者
|
|
302
|
+
usage = [None, None]
|
|
303
|
+
for seat, who in enumerate(order):
|
|
304
|
+
b = r["bots"][seat]
|
|
305
|
+
usage[who] = {"first_ms": b["first_ms"], "avg_ms": b["avg_ms"], "max_ms": b["max_ms"],
|
|
306
|
+
"over": b["timeouts"], "illegal": b["illegal"], "dead": b["dead"]}
|
|
307
|
+
out.append({"seed": seed, "style": style, "swap": swap, "turns": r["turns"], "winner": r["winner"],
|
|
308
|
+
"reason": r["reason"], "sig": r["sig"], "usage": usage})
|
|
309
|
+
if record:
|
|
310
|
+
out[-1].update({k: r[k] for k in ("orders", "final", "faults", "stderr", "used_ms")})
|
|
311
|
+
return out
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def worker_main():
|
|
315
|
+
"""批量对战的一个并行位:从标准输入逐行读任务,打完一局回一行结果。"""
|
|
316
|
+
out = sys.stdout.buffer
|
|
317
|
+
try:
|
|
318
|
+
for raw in sys.stdin.buffer:
|
|
319
|
+
job = json.loads(raw.decode("utf-8"))
|
|
320
|
+
try:
|
|
321
|
+
record = bool(job.get("record"))
|
|
322
|
+
result, replay = core.run_match(engine.Game, job["cmds"], job["names"], job["seed"], record=record,
|
|
323
|
+
affinity=job.get("affinity"), style=job["style"])
|
|
324
|
+
if record:
|
|
325
|
+
result.update({k: replay[k] for k in ("orders", "faults", "stderr", "used_ms")})
|
|
326
|
+
else:
|
|
327
|
+
result.pop("final", None)
|
|
328
|
+
answer = result
|
|
329
|
+
except Exception as exc: # 裁判这一侧的错:报给主进程,这一局不算数
|
|
330
|
+
answer = {"error": f"{type(exc).__name__}: {exc}"}
|
|
331
|
+
out.write((json.dumps(answer, ensure_ascii=False) + "\n").encode("utf-8"))
|
|
332
|
+
out.flush()
|
|
333
|
+
except KeyboardInterrupt:
|
|
334
|
+
pass
|
|
335
|
+
finally:
|
|
336
|
+
engine.shutdown()
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
REASONS = {"base": "基地被毁", "hp": "到回合上限比基地血量", "kills": "到回合上限比击毁数", "stars": "到回合上限比吃星数",
|
|
340
|
+
"damage": "到回合上限比基地伤害", "draw": "到回合上限平局", "fault": "故障判负"}
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def reason_text(reason):
|
|
344
|
+
if reason.startswith("both_"):
|
|
345
|
+
return "两个基地同回合被毁," + {"draw": "平局"}.get(reason[5:], "比" + {"kills": "击毁数", "stars": "吃星数",
|
|
346
|
+
"damage": "基地伤害", "hp": "基地血量"}.get(reason[5:], reason[5:]))
|
|
347
|
+
return REASONS.get(reason, reason)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def summarize(names, results):
|
|
351
|
+
"""汇总一批对局(站在第一个参与者 A 的角度)。返回 (摘要字典, 要打印的若干行)。"""
|
|
352
|
+
good = [r for r in results if "error" not in r]
|
|
353
|
+
errors = [r for r in results if "error" in r]
|
|
354
|
+
win = draw = loss = 0
|
|
355
|
+
seat_wdl = [[0, 0, 0], [0, 0, 0]]
|
|
356
|
+
by_map, reasons, lost = {}, {}, []
|
|
357
|
+
for r in good:
|
|
358
|
+
seat_a = 1 if r["swap"] else 0
|
|
359
|
+
pts = 0.5 if r["winner"] < 0 else (1.0 if r["winner"] == seat_a else 0.0)
|
|
360
|
+
win, draw, loss = win + (pts == 1.0), draw + (pts == 0.5), loss + (pts == 0.0)
|
|
361
|
+
seat_wdl[seat_a][0 if pts == 1.0 else 1 if pts == 0.5 else 2] += 1
|
|
362
|
+
by_map.setdefault((r["seed"], r["style"]), {})[r["swap"]] = (pts, r["sig"])
|
|
363
|
+
reasons[r["reason"]] = reasons.get(r["reason"], 0) + 1
|
|
364
|
+
if pts == 0.0:
|
|
365
|
+
lost.append(f"种子 {r['seed']}({seat_a} 号位)")
|
|
366
|
+
n = len(good)
|
|
367
|
+
lines = []
|
|
368
|
+
summary = {"games": n, "wins": win, "draws": draw, "losses": loss, "judge_errors": len(errors)}
|
|
369
|
+
if n:
|
|
370
|
+
# 误差范围按图算:一张图上换边的两局合成一个样本,图与图之间才是独立的。
|
|
371
|
+
per_map = [sum(p for p, _ in m.values()) / len(m) for m in by_map.values()]
|
|
372
|
+
mean = sum(per_map) / len(per_map)
|
|
373
|
+
half = None
|
|
374
|
+
if len(per_map) >= 2:
|
|
375
|
+
var = sum((x - mean) ** 2 for x in per_map) / (len(per_map) - 1)
|
|
376
|
+
half = 1.96 * math.sqrt(var / len(per_map))
|
|
377
|
+
rate = (win + 0.5 * draw) / n
|
|
378
|
+
full = [m for m in by_map.values() if len(m) == 2]
|
|
379
|
+
same = sum(1 for m in full if m[0][1] == m[1][1])
|
|
380
|
+
summary.update(score_rate=round(rate, 4), ci95=None if half is None else round(half, 4), maps=len(by_map),
|
|
381
|
+
paired_maps=len(full), identical_maps=same, reasons=reasons)
|
|
382
|
+
ci = f" ± {100 * half:.1f}%(95% 区间,按图计)" if half is not None else "(图太少,算不出误差范围)"
|
|
383
|
+
lines.append(f" {names[0]}:胜 {win} 平 {draw} 负 {loss} 得分率 {100 * rate:.1f}%{ci}")
|
|
384
|
+
lines.append(f" {names[0]} 坐 0 号位:胜 {seat_wdl[0][0]} 平 {seat_wdl[0][1]} 负 {seat_wdl[0][2]} "
|
|
385
|
+
f"坐 1 号位:胜 {seat_wdl[1][0]} 平 {seat_wdl[1][1]} 负 {seat_wdl[1][2]}")
|
|
386
|
+
lines.append(f" {len(full)} 张图里有 {same} 张换边后的两局逐回合完全相同"
|
|
387
|
+
f"(两个程序在这些图上的表现没有任何差别),能看出差别的有 {len(full) - same} 张")
|
|
388
|
+
lines.append(" 结束方式:" + ",".join(f"{reason_text(k)} {v}" for k, v in sorted(reasons.items(), key=lambda kv: -kv[1])))
|
|
389
|
+
usage = []
|
|
390
|
+
for who in range(2):
|
|
391
|
+
us = [r["usage"][who] for r in good]
|
|
392
|
+
u = {"first_ms": max(x["first_ms"] for x in us), "avg_ms": sum(x["avg_ms"] for x in us) / len(us),
|
|
393
|
+
"max_ms": max(x["max_ms"] for x in us), "over": sum(x["over"] for x in us),
|
|
394
|
+
"illegal": sum(x["illegal"] for x in us), "dead": sum(1 for x in us if x.get("dead"))}
|
|
395
|
+
usage.append({k: round(v, 3) for k, v in u.items()})
|
|
396
|
+
summary["usage"] = usage
|
|
397
|
+
width = max(len(x) for x in names)
|
|
398
|
+
for who in range(2):
|
|
399
|
+
u = usage[who]
|
|
400
|
+
lines.append(f" 用量 {names[who]:<{width}}:首回合最慢 {u['first_ms']:.1f}ms,每回合平均 {u['avg_ms']:.3f}ms、"
|
|
401
|
+
f"最慢 {u['max_ms']:.1f}ms,超预算 {u['over']} 回合,输出不合法 {u['illegal']} 回合"
|
|
402
|
+
+ (f",故障判负 {u['dead']} 局" if u["dead"] else ""))
|
|
403
|
+
if lost:
|
|
404
|
+
lines.append(f" {names[0]} 输掉的局:" + "、".join(lost[:12]) + (f" 等 {len(lost)} 局" if len(lost) > 12 else ""))
|
|
405
|
+
if errors:
|
|
406
|
+
lines.append(f" 有 {len(errors)} 局因为裁判这一侧出错没有结果(不计入上面的统计)。第一条:{errors[0]['error'][:200]}")
|
|
407
|
+
return summary, lines
|
|
408
|
+
|
|
409
|
+
|
|
410
|
+
def brief_line(names, s):
|
|
411
|
+
"""一行汇总:谁对谁、胜-平-负、得分率 ± 误差范围、局数、有无故障。批量命令把它放在输出的最后一行,方便截取。"""
|
|
412
|
+
faults = []
|
|
413
|
+
for who, u in enumerate(s.get("usage", [])):
|
|
414
|
+
parts = [f"{text} {u[key]} {unit}" for key, text, unit in (("over", "超预算", "回合"), ("illegal", "输出不合法", "回合"),
|
|
415
|
+
("dead", "故障判负", "局")) if u[key]]
|
|
416
|
+
if parts:
|
|
417
|
+
faults.append(f"{names[who]} " + "、".join(parts))
|
|
418
|
+
if s["judge_errors"]:
|
|
419
|
+
faults.append(f"裁判出错没有结果 {s['judge_errors']} 局")
|
|
420
|
+
fault_text = "故障:" + (";".join(faults) if faults else "无")
|
|
421
|
+
if not s["games"]:
|
|
422
|
+
return f"汇总:{names[0]} 对 {names[1]} 没有打成的局 {fault_text}"
|
|
423
|
+
ci = "" if s["ci95"] is None else f" ± {100 * s['ci95']:.1f}%"
|
|
424
|
+
return (f"汇总:{names[0]} 对 {names[1]} {s['wins']}-{s['draws']}-{s['losses']}(胜-平-负) "
|
|
425
|
+
f"得分率 {100 * s['score_rate']:.1f}%{ci} {s['games']} 局 {fault_text}")
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def series(mode, entries, games, seed, style, workers, pins=None, last_line=True):
|
|
429
|
+
"""打一批并打印汇总。返回摘要。last_line=False 时不打印最后那一行汇总(vs 后面还有话要说,由它自己最后打印)。"""
|
|
430
|
+
inf = engine.info()
|
|
431
|
+
names = unique_names(entries)
|
|
432
|
+
jobs = make_jobs(games, seed, engine.style_number(style))
|
|
433
|
+
began = time.perf_counter()
|
|
434
|
+
if mode == "fast":
|
|
435
|
+
libs = [build(e, "dll") for e in entries]
|
|
436
|
+
workers = workers or max(1, min(8, (os.cpu_count() or 4) // 2))
|
|
437
|
+
try:
|
|
438
|
+
raw = engine.run_fast(libs[0], libs[1], jobs, workers=workers, names=names)
|
|
439
|
+
except engine.FastError as exc:
|
|
440
|
+
raise SystemExit(f"快速模式中止:{exc}\n(快速模式下两个 bot 在同一个进程里,一方出事整批都停。)")
|
|
441
|
+
results = []
|
|
442
|
+
for r in raw:
|
|
443
|
+
for u in r["usage"]:
|
|
444
|
+
u["dead"] = None
|
|
445
|
+
seat_a = 1 if r["swap"] else 0
|
|
446
|
+
if r["reason"] == "fault":
|
|
447
|
+
loser_seats = [0, 1] if r["winner"] < 0 else [1 - r["winner"]]
|
|
448
|
+
for s in loser_seats:
|
|
449
|
+
r["usage"][0 if s == seat_a else 1]["dead"] = "连续输出不合法"
|
|
450
|
+
results.append(r)
|
|
451
|
+
else:
|
|
452
|
+
commands = [[build(e, "exe")] for e in entries]
|
|
453
|
+
workers = len(pins) if pins else (workers or max(1, min(8, (os.cpu_count() or 4) // 4)))
|
|
454
|
+
results = run_formal(commands, names, jobs, workers, pins=pins)
|
|
455
|
+
seconds = time.perf_counter() - began
|
|
456
|
+
maps = (games + 1) // 2
|
|
457
|
+
label = "快速模式" if mode == "fast" else "正式模式"
|
|
458
|
+
print(f"{inf['engine']} {label} {names[0]} 对 {names[1]} {games} 局"
|
|
459
|
+
f"({maps} 张图,种子 {seed}–{seed + maps - 1},每张图换边各打一局) 并行 {workers} 用时 {seconds:.1f} 秒")
|
|
460
|
+
summary, lines = summarize(names, results)
|
|
461
|
+
print("\n".join(lines))
|
|
462
|
+
if games % 2:
|
|
463
|
+
print(" 注意:局数是奇数,最后一张图只打了一个座位。")
|
|
464
|
+
if mode == "fast":
|
|
465
|
+
print(f" 快速模式下预算只统计、不判罚;用量的单位是 {unit_of(inf)},只算到调用 bot 的那个线程。")
|
|
466
|
+
else:
|
|
467
|
+
print(f" 用量的单位是 {unit_of(inf)}。" + PARALLEL_NOTE.format(turn_ms=inf["turn_ms"], unit=unit_of(inf)))
|
|
468
|
+
summary.update(mode=mode, seed=seed, style=style, workers=workers, seconds=round(seconds, 2))
|
|
469
|
+
if last_line:
|
|
470
|
+
print(brief_line(names, summary))
|
|
471
|
+
return summary
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
# ====================================================================
|
|
475
|
+
# 各个命令
|
|
476
|
+
# ====================================================================
|
|
477
|
+
|
|
478
|
+
def cmd_play(args, entries):
|
|
479
|
+
inf = engine.info()
|
|
480
|
+
names = unique_names(entries)
|
|
481
|
+
commands = [[build(e, "exe")] for e in entries]
|
|
482
|
+
style = engine.style_number(args.style)
|
|
483
|
+
try:
|
|
484
|
+
result, replay = core.run_match(engine.Game, commands, names, args.seed, style=style,
|
|
485
|
+
affinity=parse_pin(args.pin) if args.pin else None)
|
|
486
|
+
except core.MatchError as exc:
|
|
487
|
+
raise SystemExit(f"裁判这一侧出错,这一局没有结果:{exc}")
|
|
488
|
+
replay["versions"] = [e.version() for e in entries]
|
|
489
|
+
path = core.save_replay(replay, REPLAYS / (core.safe_name(args.out, "latest") + ".json"))
|
|
490
|
+
w = result["winner"]
|
|
491
|
+
outcome = "平局" if w < 0 else f"{w} 号位 {names[w]} 获胜"
|
|
492
|
+
print(f"{inf['engine']} 正式模式 种子 {args.seed} 风格 {args.style}")
|
|
493
|
+
print(f"结果:{outcome}({reason_text(result['reason'])}),共 {result['turns']} 回合。"
|
|
494
|
+
f"基地血量 {result['base_hp'][0]}:{result['base_hp'][1]},击毁 {result['kills'][0]}:{result['kills'][1]},"
|
|
495
|
+
f"吃星 {result['stars_eaten'][0]}:{result['stars_eaten'][1]}")
|
|
496
|
+
for i, (n, b) in enumerate(zip(names, result["bots"])):
|
|
497
|
+
extra = f" 判负:{b['dead']}" if b["dead"] else ""
|
|
498
|
+
print(f" {i} 号位 {n:12s} 首回合 {b['first_ms']:.1f}(预算 {inf['first_ms']}),之后每回合平均 {b['avg_ms']:.2f}、"
|
|
499
|
+
f"最多 {b['max_ms']:.2f}(预算 {inf['turn_ms']}),超预算 {b['timeouts']} 回合,输出不合法 {b['illegal']} 回合{extra}")
|
|
500
|
+
print(f" 用量的单位是 {unit_of(inf)}" + (":bot 进程自己消耗的 CPU 时间,所有线程合计。" if inf["budget_kind"] == "cpu" else "。"))
|
|
501
|
+
for f in replay["faults"][:10]:
|
|
502
|
+
print(f" 故障:第 {f['turn']} 回合 {f['seat']} 号位 {f['detail']}")
|
|
503
|
+
if len(replay["faults"]) > 10:
|
|
504
|
+
print(f" ……共 {len(replay['faults'])} 条故障,都在回放里")
|
|
505
|
+
for i, n in enumerate(names):
|
|
506
|
+
lines = replay["stderr"][i]
|
|
507
|
+
if lines:
|
|
508
|
+
print(f" {i} 号位 {n} 的调试输出(最后 {min(10, len(lines))} 行,共 {len(lines)} 行,全部在回放里):")
|
|
509
|
+
for turn, text in lines[-10:]:
|
|
510
|
+
print(f" [回合 {turn}] {text}")
|
|
511
|
+
rel = os.path.relpath(path, HERE).replace("\\", "/")
|
|
512
|
+
print(f"回放:{rel} 看某一回合:arena.py show {rel} 回合号 --around 2")
|
|
513
|
+
try: # 网页版回放;做不成只提示,不影响这一局的结果
|
|
514
|
+
page = write_html(engine.expand(replay), path.with_suffix(".html"))
|
|
515
|
+
print(f"网页回放:{os.path.relpath(page, HERE).replace(chr(92), '/')} 用浏览器打开就能看")
|
|
516
|
+
except Exception as exc:
|
|
517
|
+
print(f"(网页回放没有生成:{exc}。不影响本次结果。)", file=sys.stderr)
|
|
518
|
+
return {"seed": args.seed, "style": args.style, "winner": w, "reason": result["reason"], "turns": result["turns"],
|
|
519
|
+
"sig": result["sig"], "replay": rel,
|
|
520
|
+
"bots": [{k: b[k] for k in ("dead", "timeouts", "illegal", "first_ms", "avg_ms", "max_ms")}
|
|
521
|
+
for b in result["bots"]]}
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
TANK_CHARS = ["FGA", "fga"] # 0 号方大写,1 号方小写:F 快速、G 火力、A 装甲
|
|
525
|
+
TYPE_CN = {"fast": "快速", "fire": "火力", "armor": "装甲"}
|
|
526
|
+
ARROWS = {"U": "^", "D": "v", "L": "<", "R": ">"}
|
|
527
|
+
|
|
528
|
+
|
|
529
|
+
def describe_event(ev):
|
|
530
|
+
t = ev["type"]
|
|
531
|
+
tank = lambda i: f"{i} 号车" # noqa: E731
|
|
532
|
+
if t == "fire":
|
|
533
|
+
return f"{tank(ev['tank'])}向 {ev['dir']} 开炮"
|
|
534
|
+
if t == "blocked":
|
|
535
|
+
return f"{tank(ev['tank'])}第 {ev['sub']} 小步没走成"
|
|
536
|
+
if t == "cancel":
|
|
537
|
+
return f"子弹在 ({ev['x']},{ev['y']}) {'对穿' if ev['how'] == 'cross' else '同格'}抵消"
|
|
538
|
+
if t == "hit":
|
|
539
|
+
return f"{tank(ev['tank'])}在 ({ev['x']},{ev['y']}) 被 {ev['by']} 号车的子弹打中"
|
|
540
|
+
if t == "base_hit":
|
|
541
|
+
return f"{ev['seat']} 号方基地被 {ev['by']} 号车打中"
|
|
542
|
+
if t == "wall":
|
|
543
|
+
return f"({ev['x']},{ev['y']}) 的{'砖墙' if ev['kind'] == 'brick' else '钢墙'}被打掉"
|
|
544
|
+
if t == "death":
|
|
545
|
+
return f"{tank(ev['tank'])}在 ({ev['x']},{ev['y']}) 被毁" + (f"(被 {ev['by']} 号车复活炸毁)" if ev["by"] >= 0 else "")
|
|
546
|
+
if t == "respawn":
|
|
547
|
+
return f"{tank(ev['tank'])}在 ({ev['x']},{ev['y']}) 复活"
|
|
548
|
+
if t == "bullet_cleared":
|
|
549
|
+
return f"{tank(ev['tank'])}复活,({ev['x']},{ev['y']}) 上的子弹消失"
|
|
550
|
+
if t == "star":
|
|
551
|
+
return f"{tank(ev['tank'])}在 ({ev['x']},{ev['y']}) 吃到星星,升到 {ev['level']} 级"
|
|
552
|
+
if t == "supply":
|
|
553
|
+
return f"({ev['x']},{ev['y']}) 出了一颗星星"
|
|
554
|
+
return ev["text"]
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def cmd_show(args):
|
|
558
|
+
path = Path(args.replay)
|
|
559
|
+
try:
|
|
560
|
+
data = engine.expand(core.load_replay(path))
|
|
561
|
+
except (OSError, ValueError, KeyError) as exc:
|
|
562
|
+
raise SystemExit(f"回放读不了:{args.replay}({exc})")
|
|
563
|
+
names, size, frames = data["names"], data["size"], data["frames"]
|
|
564
|
+
res = data["result"]
|
|
565
|
+
last = frames[-1]["turn"]
|
|
566
|
+
print(f"回放 {args.replay}:{names[0]}(0 号位,在下方,大写)对 {names[1]}(1 号位,在上方,小写),共 {last} 回合,"
|
|
567
|
+
f"{'平局' if res['winner'] < 0 else str(res['winner']) + ' 号位获胜'}({reason_text(res['reason'])})")
|
|
568
|
+
print("图例:. 空地 # 砖墙 @ 钢墙 ~ 水 0/1 基地 * 星星 ^v<> 子弹(+ 表示一格里有几发) "
|
|
569
|
+
"F/G/A = 0 号方的快速/火力/装甲,f/g/a = 1 号方的")
|
|
570
|
+
shown = 0
|
|
571
|
+
for f in frames:
|
|
572
|
+
if abs(f["turn"] - args.turn) > args.around:
|
|
573
|
+
continue
|
|
574
|
+
shown += 1
|
|
575
|
+
grid = [list(row) for row in f["map"]]
|
|
576
|
+
for x, y in f["stars"]:
|
|
577
|
+
grid[y][x] = "*"
|
|
578
|
+
for b in f["bullets"]:
|
|
579
|
+
grid[b["y"]][b["x"]] = "+" if grid[b["y"]][b["x"]] in "^v<>+" else ARROWS[b["dir"]]
|
|
580
|
+
for t in f["tanks"]:
|
|
581
|
+
if t["alive"]:
|
|
582
|
+
grid[t["y"]][t["x"]] = TANK_CHARS[t["seat"]][t["id"] % 3]
|
|
583
|
+
print(f"\n===== 第 {f['turn']} 回合结算后 基地血量 {f['base_hp'][0]}:{f['base_hp'][1]} 击毁 {f['kills'][0]}:{f['kills'][1]}"
|
|
584
|
+
f" 吃星 {f['stars_eaten'][0]}:{f['stars_eaten'][1]} =====")
|
|
585
|
+
if f["orders"]:
|
|
586
|
+
for seat in range(2):
|
|
587
|
+
o = f["orders"][seat]
|
|
588
|
+
ms = f["used_ms"][seat] if f["used_ms"] and f["used_ms"][seat] is not None else None
|
|
589
|
+
print(f"命令 {seat} 号位:快速 {o[0]:3s} 火力 {o[1]:3s} 装甲 {o[2]:3s}" + (f" 用量 {ms:.2f}ms" if ms is not None else ""))
|
|
590
|
+
for fault in f["faults"]:
|
|
591
|
+
print(f"故障 {fault['seat']} 号位:{fault['detail']}")
|
|
592
|
+
if f["events"]:
|
|
593
|
+
print("事件:" + ";".join(describe_event(e) for e in f["events"]))
|
|
594
|
+
print(" " + "".join(str(x % 10) for x in range(size)))
|
|
595
|
+
for y, row in enumerate(grid):
|
|
596
|
+
print(f"{y:3d} " + "".join(row))
|
|
597
|
+
for t in f["tanks"]:
|
|
598
|
+
who = f"{t['id']} 号车 {t['seat']} 号方{TYPE_CN[t['type']]}({TANK_CHARS[t['seat']][t['id'] % 3]})"
|
|
599
|
+
if t["alive"]:
|
|
600
|
+
print(f" {who} 在 ({t['x']},{t['y']}) 血 {t['hp']} 等级 {t['level']} 场上子弹 {t['bullets_out']}/{t['bullet_cap']}"
|
|
601
|
+
f" {'可以开炮' if t['can_fire'] else '装填还要 ' + str(t['reload_left']) + ' 回合' if t['reload_left'] else '子弹已达上限'}")
|
|
602
|
+
else:
|
|
603
|
+
print(f" {who} 不在场,还要 {t['respawn_in']} 回合复活")
|
|
604
|
+
if f["bullets"]:
|
|
605
|
+
print(" 子弹:" + " ".join(f"({b['x']},{b['y']}){ARROWS[b['dir']]} {b['owner']} 号车的" + ("(能破钢)" if b["steel"] else "")
|
|
606
|
+
for b in f["bullets"]))
|
|
607
|
+
for seat in range(2):
|
|
608
|
+
lines = [text for turn, text in data["stderr"][seat] if turn == f["turn"]] if data.get("stderr") else []
|
|
609
|
+
for text in lines[:8]:
|
|
610
|
+
print(f" 调试输出 {seat} 号位:{text}")
|
|
611
|
+
if not shown:
|
|
612
|
+
print(f"\n这局只有第 0–{last} 回合,没有第 {args.turn} 回合。")
|
|
613
|
+
if not data["consistent"]:
|
|
614
|
+
print("\n注意:重新结算出的终局和回放里记的不一样,这份回放可能不是用当前的引擎打的。")
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
def cmd_expand(args):
|
|
618
|
+
path = Path(args.replay)
|
|
619
|
+
try:
|
|
620
|
+
data = engine.expand(core.load_replay(path))
|
|
621
|
+
except (OSError, ValueError, KeyError) as exc:
|
|
622
|
+
raise SystemExit(f"回放读不了:{args.replay}({exc})")
|
|
623
|
+
name = path.name[:-3] if path.name.endswith(".gz") else path.name
|
|
624
|
+
out = Path(args.out) if args.out else path.with_name(Path(name).stem + ".frames.json")
|
|
625
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
626
|
+
out.write_text(json.dumps(data, ensure_ascii=False, separators=(",", ":")), encoding="utf-8")
|
|
627
|
+
print(f"已展开 {len(data['frames'])} 帧(第 0–{data['frames'][-1]['turn']} 回合):{out}")
|
|
628
|
+
|
|
629
|
+
|
|
630
|
+
# ---- 网页回放:把展开后的数据嵌进 viewer.html ----
|
|
631
|
+
|
|
632
|
+
VIEWER = HERE / "viewer.html"
|
|
633
|
+
VIEWER_MARK = "/*__TANK_DATA__*/null" # viewer.html 里留给数据的位置,只出现一次
|
|
634
|
+
|
|
635
|
+
|
|
636
|
+
def write_html(frames, out):
|
|
637
|
+
"""把展开后的回放(engine.expand 的结果)嵌进 viewer.html,存成 out。返回 out。"""
|
|
638
|
+
page = VIEWER.read_text(encoding="utf-8")
|
|
639
|
+
if page.count(VIEWER_MARK) != 1:
|
|
640
|
+
raise ValueError("viewer.html 里没有找到放数据的位置")
|
|
641
|
+
text = json.dumps(frames, ensure_ascii=False, separators=(",", ":"))
|
|
642
|
+
# 数据是直接写进 <script> 里的:把 < 和两个行分隔符转义掉,调试输出里出现 </script> 之类也不会把网页截断
|
|
643
|
+
text = text.replace("<", "\\u003c").replace("\u2028", "\\u2028").replace("\u2029", "\\u2029")
|
|
644
|
+
out = Path(out)
|
|
645
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
646
|
+
out.write_text(page.replace(VIEWER_MARK, text), encoding="utf-8")
|
|
647
|
+
return out
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def cmd_html(args):
|
|
651
|
+
path = Path(args.replay)
|
|
652
|
+
try:
|
|
653
|
+
data = core.load_replay(path)
|
|
654
|
+
if data.get("format") != engine.FRAMES_FORMAT: # 给的是原始回放就先展开;已经展开过的直接用
|
|
655
|
+
data = engine.expand(data)
|
|
656
|
+
except (OSError, ValueError, KeyError) as exc:
|
|
657
|
+
raise SystemExit(f"回放读不了:{args.replay}({exc})")
|
|
658
|
+
name = path.name[:-3] if path.name.endswith(".gz") else path.name
|
|
659
|
+
stem = Path(name).stem
|
|
660
|
+
stem = stem[:-7] if stem.endswith(".frames") else stem
|
|
661
|
+
try:
|
|
662
|
+
out = write_html(data, Path(args.out) if args.out else path.with_name(stem + ".html"))
|
|
663
|
+
except (OSError, ValueError) as exc:
|
|
664
|
+
raise SystemExit(f"网页没有生成:{exc}")
|
|
665
|
+
print(f"网页回放({len(data['frames']) - 1} 回合):{out} 用浏览器打开就能看")
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def cmd_cpus():
|
|
669
|
+
cores = core.cpu_topology()
|
|
670
|
+
if not cores:
|
|
671
|
+
print(f"这个系统上读不到处理器拓扑。逻辑处理器共 {os.cpu_count()} 个。")
|
|
672
|
+
return
|
|
673
|
+
classes = sorted({c["class"] for c in cores}, reverse=True)
|
|
674
|
+
print(f"物理核 {len(cores)} 个,逻辑处理器 {sum(len(c['cpus']) for c in cores)} 个。")
|
|
675
|
+
for cls in classes:
|
|
676
|
+
group = [c for c in cores if c["class"] == cls]
|
|
677
|
+
label = "同构" if len(classes) == 1 else ("大核" if cls == classes[0] else "小核")
|
|
678
|
+
ht = sum(1 for c in group if len(c["cpus"]) > 1)
|
|
679
|
+
print(f"{label}(效率等级 {cls}):{len(group)} 个物理核" + (f",其中 {ht} 个开了超线程" if ht else ",没有超线程"))
|
|
680
|
+
print(" 每个物理核的逻辑处理器编号:" + " ".join("+".join(map(str, c["cpus"])) for c in group))
|
|
681
|
+
print("同一个物理核的两个逻辑处理器(用 + 连着的)会互相抢,绑核时一个物理核只放一个程序。")
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
def run_speed(cpu=None, repeat=5):
|
|
685
|
+
try:
|
|
686
|
+
exe = core.build_tool("speed")
|
|
687
|
+
except RuntimeError as exc:
|
|
688
|
+
raise SystemExit(f"测速程序编译不了:{exc}")
|
|
689
|
+
cmd = [str(exe), "--repeat", str(repeat)] + (["--cpu", str(cpu)] if cpu is not None else [])
|
|
690
|
+
r = subprocess.run(cmd, capture_output=True, encoding="utf-8", errors="replace", timeout=300)
|
|
691
|
+
words = dict(w.split("=", 1) for w in r.stdout.split()[1:] if "=" in w) if r.returncode == 0 else None
|
|
692
|
+
if not words:
|
|
693
|
+
raise SystemExit(f"测速程序没有跑成:{(r.stderr or r.stdout).strip()[:200]}")
|
|
694
|
+
return {"cpu": cpu, "ghz": float(words["ghz"]), "loop_ms": float(words["loop_ms"]),
|
|
695
|
+
"engine_ms": float(words["engine_ms"]), "engine_cpu_ms": float(words["engine_cpu_ms"]),
|
|
696
|
+
"units_per_ms": int(words["per_ms"]), "units_source": words["source"]}
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def cmd_speed(args):
|
|
700
|
+
targets = [(f"逻辑处理器 {args.cpu}", args.cpu)] if args.cpu is not None else [("不绑核", None)]
|
|
701
|
+
if args.cpu is None:
|
|
702
|
+
cores = core.cpu_topology()
|
|
703
|
+
classes = sorted({c["class"] for c in cores}, reverse=True)
|
|
704
|
+
for cls in classes:
|
|
705
|
+
first = next(c for c in cores if c["class"] == cls)
|
|
706
|
+
label = "同构核" if len(classes) == 1 else ("大核" if cls == classes[0] else "小核")
|
|
707
|
+
targets.append((f"{label}(逻辑处理器 {first['cpus'][0]})", first["cpus"][0]))
|
|
708
|
+
rows = []
|
|
709
|
+
for label, cpu in targets:
|
|
710
|
+
m = run_speed(cpu, args.repeat)
|
|
711
|
+
rows.append({"where": label, **m})
|
|
712
|
+
print(f"{label}:实际主频约 {m['ghz']:.2f} GHz;固定工作量 墙上 {m['engine_ms']:.1f} 毫秒,CPU {m['engine_cpu_ms']:.1f} 毫秒")
|
|
713
|
+
m = rows[0]
|
|
714
|
+
print(f"1 CPU 毫秒 = {m['units_per_ms']} 个读数单位(来源:{m['units_source']})。"
|
|
715
|
+
"各项都是重复几次取最快的一次;和上一次测的差出 10% 以上,说明机器状态变了(电源模式、别的任务)。")
|
|
716
|
+
return {"speed": rows}
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def cmd_check(entries):
|
|
720
|
+
if sys.version_info < (3, 10):
|
|
721
|
+
raise SystemExit(f"需要 Python 3.10 以上,当前是 {sys.version.split()[0]}")
|
|
722
|
+
compiler = core.find_compiler()
|
|
723
|
+
if not compiler:
|
|
724
|
+
raise SystemExit("没有找到 C++ 编译器(g++ 或 clang++)。请把这个情况报告出来。")
|
|
725
|
+
if not all(ord(ch) < 128 for ch in str(HERE)):
|
|
726
|
+
print(f"注意:当前目录的路径里有非英文字符({HERE}),编译器可能打不开。建议放到纯英文路径下。")
|
|
727
|
+
try:
|
|
728
|
+
inf = engine.info()
|
|
729
|
+
core.build_tool("fast")
|
|
730
|
+
except (core.MatchError, RuntimeError) as exc:
|
|
731
|
+
raise SystemExit(f"裁判程序编译或运行不了:{exc}\n"
|
|
732
|
+
"(如果是刚编译出的程序被系统拦截,请确认本目录在允许运行新程序的位置。)")
|
|
733
|
+
print(f"Python {sys.version.split()[0]};编译器 {compiler};引擎 {inf['engine']};"
|
|
734
|
+
f"预算:首回合 {inf['first_ms']} {unit_of(inf)},之后每回合 {inf['turn_ms']} {unit_of(inf)}"
|
|
735
|
+
+ (f"(本机 1 CPU 毫秒 = {inf['units_per_ms']} 个周期)。" if inf["budget_kind"] == "cpu" and os.name == "nt" else "。"))
|
|
736
|
+
if not entries:
|
|
737
|
+
print("没有找到 submission/bot.cpp,只检查了裁判程序。")
|
|
738
|
+
return {"ok": True, "bot": False}
|
|
739
|
+
e = entries[0]
|
|
740
|
+
exe, dll = build(e, "exe"), build(e, "dll")
|
|
741
|
+
try:
|
|
742
|
+
result, _ = core.run_match(engine.Game, [[exe], [exe]], ["bot", "bot#2"], 1, record=False)
|
|
743
|
+
except core.MatchError as exc:
|
|
744
|
+
raise SystemExit(f"试跑出错:{exc}\n(如果是刚编译出的程序被系统拦截,请确认本目录在允许运行新程序的位置。)")
|
|
745
|
+
bad = [b for b in result["bots"] if b["faults"] or b["dead"]]
|
|
746
|
+
if bad:
|
|
747
|
+
raise SystemExit(f"{e.spec} 自己打自己时出了故障:{bad[0]['dead'] or '有超时或不合法的输出'}。用 play 看细节。")
|
|
748
|
+
try:
|
|
749
|
+
fast = engine.run_fast(dll, dll, [(1, -1, 0)], names=("bot", "bot#2"))[0]
|
|
750
|
+
except engine.FastError as exc:
|
|
751
|
+
raise SystemExit(f"快速模式试跑出错:{exc}")
|
|
752
|
+
same = fast["sig"] == result["sig"]
|
|
753
|
+
print(f"正常:{e.spec} 能编译,正式模式和快速模式各自打完了一局({result['turns']} 回合)"
|
|
754
|
+
+ (",两种模式打出的对局逐回合相同。" if same else
|
|
755
|
+
"。两种模式打出的对局不同:如果 bot 的决策依赖时间(比如按剩余预算决定搜多深)这是正常的,否则请检查有没有用到未初始化的变量。"))
|
|
756
|
+
return {"ok": True, "bot": True, "same_in_both_modes": same}
|
|
757
|
+
|
|
758
|
+
|
|
759
|
+
def main():
|
|
760
|
+
core.setup_console()
|
|
761
|
+
if sys.argv[1:2] == ["_worker"]:
|
|
762
|
+
return worker_main()
|
|
763
|
+
ap = argparse.ArgumentParser(description="坦克大战本地对战与复盘", formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
764
|
+
epilog=__doc__)
|
|
765
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
766
|
+
sub.add_parser("check", help="检查环境")
|
|
767
|
+
p = sub.add_parser("play", help="正式模式打一局")
|
|
768
|
+
p.add_argument("a")
|
|
769
|
+
p.add_argument("b")
|
|
770
|
+
p.add_argument("--seed", type=int, default=1)
|
|
771
|
+
p.add_argument("--style", default="auto", help="地图风格:auto(按种子轮换)、open、brick、water、steel")
|
|
772
|
+
p.add_argument("--out", default="latest", help="回放存成 replays/名字.json")
|
|
773
|
+
p.add_argument("--pin", default=None, help="把两个程序绑到指定的逻辑处理器:A的:B的,比如 16:18(默认不绑)")
|
|
774
|
+
for name, games, text in (("batch", 20, "正式模式批量"), ("fast", 200, "快速模式批量"), ("vs", 100, "两个版本对比")):
|
|
775
|
+
p = sub.add_parser(name, help=text)
|
|
776
|
+
p.add_argument("a")
|
|
777
|
+
p.add_argument("b")
|
|
778
|
+
p.add_argument("--games", type=int, default=games, help=f"局数(默认 {games};每张图换边各打一局)")
|
|
779
|
+
p.add_argument("--seed", type=int, default=1, help="第一张图的种子")
|
|
780
|
+
p.add_argument("--style", default="auto")
|
|
781
|
+
p.add_argument("--workers", type=int, default=0, help="并行数")
|
|
782
|
+
if name == "batch":
|
|
783
|
+
p.add_argument("--pin", nargs="+", default=None,
|
|
784
|
+
help="每个并行位一对 A的:B的 逻辑处理器,比如 --pin 16:18 20:22;给了它并行数就是对数")
|
|
785
|
+
if name == "vs":
|
|
786
|
+
p.add_argument("--mode", choices=["auto", "fast", "formal"], default="auto",
|
|
787
|
+
help="auto:两边都是源码用快速模式,否则用正式模式")
|
|
788
|
+
p = sub.add_parser("show", help="在终端里看某几个回合")
|
|
789
|
+
p.add_argument("replay")
|
|
790
|
+
p.add_argument("turn", type=int)
|
|
791
|
+
p.add_argument("--around", type=int, default=0)
|
|
792
|
+
p = sub.add_parser("expand", help="把回放展开成逐回合的局面和事件")
|
|
793
|
+
p.add_argument("replay")
|
|
794
|
+
p.add_argument("--out", default=None)
|
|
795
|
+
p = sub.add_parser("html", help="把回放做成一个打开就能看的网页")
|
|
796
|
+
p.add_argument("replay")
|
|
797
|
+
p.add_argument("--out", default=None)
|
|
798
|
+
sub.add_parser("cpus", help="列出本机的处理器拓扑")
|
|
799
|
+
p = sub.add_parser("speed", help="测速")
|
|
800
|
+
p.add_argument("--cpu", type=int, default=None, help="只测这个逻辑处理器")
|
|
801
|
+
p.add_argument("--repeat", type=int, default=5)
|
|
802
|
+
args = ap.parse_args()
|
|
803
|
+
|
|
804
|
+
started = time.time()
|
|
805
|
+
entries, summary, error = [], None, None
|
|
806
|
+
try:
|
|
807
|
+
if args.cmd in ("show", "expand", "html", "cpus"):
|
|
808
|
+
return {"show": cmd_show, "expand": cmd_expand, "html": cmd_html, "cpus": lambda a: cmd_cpus()}[args.cmd](args)
|
|
809
|
+
if args.cmd == "speed":
|
|
810
|
+
summary = cmd_speed(args)
|
|
811
|
+
return
|
|
812
|
+
if args.cmd == "check":
|
|
813
|
+
bot = HERE / "submission" / "bot.cpp"
|
|
814
|
+
entries = [Entry("submission/bot.cpp")] if bot.is_file() else []
|
|
815
|
+
else:
|
|
816
|
+
entries = [Entry(args.a), Entry(args.b)]
|
|
817
|
+
if args.cmd != "play" and args.games < 1:
|
|
818
|
+
raise SystemExit("--games 至少为 1")
|
|
819
|
+
archive(entries)
|
|
820
|
+
if args.cmd == "check":
|
|
821
|
+
summary = cmd_check(entries)
|
|
822
|
+
elif args.cmd == "play":
|
|
823
|
+
summary = cmd_play(args, entries)
|
|
824
|
+
elif args.cmd == "batch":
|
|
825
|
+
summary = series("formal", entries, args.games, args.seed, args.style, args.workers,
|
|
826
|
+
pins=[parse_pin(x) for x in args.pin] if args.pin else None)
|
|
827
|
+
elif args.cmd == "fast":
|
|
828
|
+
summary = series("fast", entries, args.games, args.seed, args.style, args.workers)
|
|
829
|
+
else:
|
|
830
|
+
mode = args.mode
|
|
831
|
+
if mode == "auto":
|
|
832
|
+
mode = "fast" if all(e.is_source for e in entries) else "formal"
|
|
833
|
+
if mode == "formal":
|
|
834
|
+
print("有一方是已经编译好的程序,用正式模式对比。")
|
|
835
|
+
summary = series(mode, entries, args.games, args.seed, args.style, args.workers, last_line=False)
|
|
836
|
+
s = summary
|
|
837
|
+
if s.get("games"):
|
|
838
|
+
useful = s["paired_maps"] - s["identical_maps"]
|
|
839
|
+
print(f"怎么读:得分率高于 50% 且误差范围不跨过 50%,才能说 {entries[0].name} 更强。"
|
|
840
|
+
f"这次真正提供信息的是 {useful} 张图;换边后两局完全相同的图上,两个版本各赢一局或都平,不说明谁强。")
|
|
841
|
+
print(brief_line(unique_names(entries), s)) # 一行汇总放在最后
|
|
842
|
+
except KeyboardInterrupt:
|
|
843
|
+
error = "被中断"
|
|
844
|
+
print("\n已中断,子进程已清理。", file=sys.stderr)
|
|
845
|
+
raise SystemExit(130)
|
|
846
|
+
except ValueError as exc:
|
|
847
|
+
error = exc
|
|
848
|
+
raise SystemExit(str(exc))
|
|
849
|
+
except core.MatchError as exc:
|
|
850
|
+
error = exc
|
|
851
|
+
raise SystemExit(f"裁判这一侧出错:{exc}")
|
|
852
|
+
except SystemExit as exc:
|
|
853
|
+
error = exc.code if isinstance(exc.code, str) else None
|
|
854
|
+
raise
|
|
855
|
+
finally:
|
|
856
|
+
if args.cmd not in ("show", "expand", "html", "cpus"):
|
|
857
|
+
write_log(args.cmd, sys.argv[1:], entries, summary, started, error)
|
|
858
|
+
engine.shutdown()
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
if __name__ == "__main__":
|
|
862
|
+
main()
|