dsh-eval-guard 0.2.2 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/README.md +91 -13
  2. package/assets/bomberman/workspace.zip +0 -0
  3. package/assets/commit0/instance.json +30 -0
  4. package/assets/commit0/workspace.zip +0 -0
  5. package/assets/ctf/workspace.zip +0 -0
  6. package/assets/featurebench/hidden/test_regression.py +690 -0
  7. package/assets/featurebench/instance.json +16 -0
  8. package/assets/featurebench/workspace.zip +0 -0
  9. package/assets/swe-bench-pro/hidden/test.patch +36 -0
  10. package/assets/swe-bench-pro/instance.json +10 -0
  11. package/assets/swe-bench-pro/workspace.zip +0 -0
  12. package/assets/tank/workspace.zip +0 -0
  13. package/eval/README.md +18 -0
  14. package/eval/arena/arena/README.md +21 -0
  15. package/eval/arena/arena/bt.py +104 -0
  16. package/eval/arena/arena/clock.py +15 -0
  17. package/eval/arena/arena/core.py +265 -0
  18. package/eval/arena/arena/run.cmd +24 -0
  19. package/eval/arena/arena/run.sh +11 -0
  20. package/eval/arena/arena/schedule.py +79 -0
  21. package/eval/arena/arena/tournament.py +183 -0
  22. package/eval/arena/bomberman/bots/README.md +16 -0
  23. package/eval/arena/bomberman/bots/deepseek-v4.1-flash.cpp +447 -0
  24. package/eval/arena/bomberman/bots/doubao-seed-2.1.cpp +542 -0
  25. package/eval/arena/bomberman/bots/glm-5.3.cpp +406 -0
  26. package/eval/arena/bomberman/bots/gpt-6-astra.cpp +768 -0
  27. package/eval/arena/bomberman/bots/gpt-6-sol.cpp +444 -0
  28. package/eval/arena/bomberman/bots/kimi-k3.cpp +432 -0
  29. package/eval/arena/bomberman/bots/mimo-v2.6-pro.cpp +529 -0
  30. package/eval/arena/bomberman/bots/opus-5.5.cpp +548 -0
  31. package/eval/arena/bomberman/bots/qwen-3.8-max.cpp +836 -0
  32. package/eval/arena/bomberman/tournament/README.md +11 -0
  33. package/eval/arena/bomberman/tournament/bt.py +56 -0
  34. package/eval/arena/bomberman/tournament/evaluate.py +199 -0
  35. package/eval/arena/bomberman/tournament/run.py +79 -0
  36. package/eval/arena/bomberman/tournament/schedule-2to10.json.gz +0 -0
  37. package/eval/arena/bomberman/workspace/MANIFEST.json +13 -0
  38. package/eval/arena/bomberman/workspace/README.md +29 -0
  39. package/eval/arena/bomberman/workspace/RULES.md +100 -0
  40. package/eval/arena/bomberman/workspace/baseline.cpp +240 -0
  41. package/eval/arena/bomberman/workspace/clock.py +25 -0
  42. package/eval/arena/bomberman/workspace/engine/arena.py +198 -0
  43. package/eval/arena/bomberman/workspace/engine/build.py +35 -0
  44. package/eval/arena/bomberman/workspace/engine/engine.py +256 -0
  45. package/eval/arena/bomberman/workspace/engine/verify_replay.py +24 -0
  46. package/eval/arena/bomberman/workspace/run.cmd +28 -0
  47. package/eval/arena/bomberman/workspace/run.sh +16 -0
  48. package/eval/arena/bomberman/workspace/viewer/index.html +42 -0
  49. package/eval/arena/ctf/bots/README.md +18 -0
  50. package/eval/arena/ctf/bots/deepseek-v4.1-flash.cpp +601 -0
  51. package/eval/arena/ctf/bots/doubao-seed-2.1-turbo.cpp +320 -0
  52. package/eval/arena/ctf/bots/fable-5.1.cpp +634 -0
  53. package/eval/arena/ctf/bots/glm-5.3.cpp +689 -0
  54. package/eval/arena/ctf/bots/gpt-6-astra.cpp +657 -0
  55. package/eval/arena/ctf/bots/gpt-6.1-sol.cpp +725 -0
  56. package/eval/arena/ctf/bots/kimi-k3.cpp +667 -0
  57. package/eval/arena/ctf/bots/mimo-v2.6-pro.cpp +533 -0
  58. package/eval/arena/ctf/bots/opus-5.5.cpp +645 -0
  59. package/eval/arena/ctf/bots/qwen-3.8-max.cpp +726 -0
  60. package/eval/arena/ctf/bots/sonnet-5.5.cpp +569 -0
  61. package/eval/arena/ctf/tournament/README.md +15 -0
  62. package/eval/arena/ctf/tournament/formal-2to8.json +31 -0
  63. package/eval/arena/ctf/tournament/formal-9to15.json +31 -0
  64. package/eval/arena/ctf/workspace/README.md +41 -0
  65. package/eval/arena/ctf/workspace/RULES.md +119 -0
  66. package/eval/arena/ctf/workspace/arena.py +186 -0
  67. package/eval/arena/ctf/workspace/baseline.cpp +257 -0
  68. package/eval/arena/ctf/workspace/clock.py +15 -0
  69. package/eval/arena/ctf/workspace/core.py +265 -0
  70. package/eval/arena/ctf/workspace/engine.py +389 -0
  71. package/eval/arena/ctf/workspace/run.cmd +24 -0
  72. package/eval/arena/ctf/workspace/run.sh +11 -0
  73. package/eval/arena/ctf/workspace/viewer.html +71 -0
  74. package/eval/arena/land/bots/README.md +17 -0
  75. package/eval/arena/land/bots/deepseek-4.1f@workbuddy#1/bot.cpp +608 -0
  76. package/eval/arena/land/bots/glm-5.3@workbuddy#1/bot.cpp +521 -0
  77. package/eval/arena/land/bots/gpt-6-astra@codex#1/bot.cpp +373 -0
  78. package/eval/arena/land/bots/gpt-6-sol@codex#1/bot.cpp +341 -0
  79. package/eval/arena/land/bots/kimi-k3@workbuddy#1/bot.cpp +492 -0
  80. package/eval/arena/land/bots/mimo-2.6-pro@zcode#1/bot.cpp +652 -0
  81. package/eval/arena/land/bots/mystery-model@zcode#1/bot.cpp +466 -0
  82. package/eval/arena/land/bots/opus-5.5@claude-code#1/bot.cpp +630 -0
  83. package/eval/arena/land/bots/sonnet-5@claude-code#1/bot.cpp +619 -0
  84. package/eval/arena/land/tournament/make_replay_page.py +17 -0
  85. package/eval/arena/land/tournament/rate.py +290 -0
  86. package/eval/arena/land/tournament/round_robin.py +172 -0
  87. package/eval/arena/land/tournament/sparring.cpp +356 -0
  88. package/eval/arena/land/workspace/RULES.md +114 -0
  89. package/eval/arena/land/workspace/TASK.md +46 -0
  90. package/eval/arena/land/workspace/anchors/.build/greedy_f2e6d2dea5ca +0 -0
  91. package/eval/arena/land/workspace/anchors/greedy.cpp +194 -0
  92. package/eval/arena/land/workspace/anchors/random.cpp +80 -0
  93. package/eval/arena/land/workspace/clock.py +64 -0
  94. package/eval/arena/land/workspace/engine/build.py +33 -0
  95. package/eval/arena/land/workspace/engine/game.py +273 -0
  96. package/eval/arena/land/workspace/engine/referee.py +308 -0
  97. package/eval/arena/land/workspace/starter/.build/bot_be32b45b4699 +0 -0
  98. package/eval/arena/land/workspace/starter/bot.cpp +82 -0
  99. package/eval/arena/land/workspace/viewer/viewer.html +210 -0
  100. package/eval/arena/tank/bots/README.md +24 -0
  101. package/eval/arena/tank/bots/half1/deepseek-v4.1-flash.cpp +367 -0
  102. package/eval/arena/tank/bots/half1/doubao-seed-evolving.cpp +402 -0
  103. package/eval/arena/tank/bots/half1/gemini-4-argon.cpp +1132 -0
  104. package/eval/arena/tank/bots/half1/glm-5.3.cpp +551 -0
  105. package/eval/arena/tank/bots/half1/gpt-6-astra.cpp +233 -0
  106. package/eval/arena/tank/bots/half1/gpt-6.1-sol-ultrafast.cpp +350 -0
  107. package/eval/arena/tank/bots/half1/gpt-6.1-sol.cpp +78 -0
  108. package/eval/arena/tank/bots/half1/grok-4.7.cpp +1099 -0
  109. package/eval/arena/tank/bots/half1/haiku-5.5.cpp +210 -0
  110. package/eval/arena/tank/bots/half1/kimi-k3.cpp +353 -0
  111. package/eval/arena/tank/bots/half1/mimo-v2.6-pro.cpp +336 -0
  112. package/eval/arena/tank/bots/half1/muse-wonder-full.cpp +560 -0
  113. package/eval/arena/tank/bots/half1/opus-5.5.cpp +435 -0
  114. package/eval/arena/tank/bots/half1/qwen-3.8-max.cpp +537 -0
  115. package/eval/arena/tank/bots/half2/deepseek-v4.1-flash.cpp +425 -0
  116. package/eval/arena/tank/bots/half2/doubao-seed-evolving.cpp +308 -0
  117. package/eval/arena/tank/bots/half2/gemini-4-argon.cpp +1390 -0
  118. package/eval/arena/tank/bots/half2/glm-5.3.cpp +651 -0
  119. package/eval/arena/tank/bots/half2/gpt-6-astra.cpp +241 -0
  120. package/eval/arena/tank/bots/half2/gpt-6.1-sol-ultrafast.cpp +356 -0
  121. package/eval/arena/tank/bots/half2/gpt-6.1-sol.cpp +87 -0
  122. package/eval/arena/tank/bots/half2/grok-4.7.cpp +1205 -0
  123. package/eval/arena/tank/bots/half2/haiku-5.5.cpp +210 -0
  124. package/eval/arena/tank/bots/half2/kimi-k3.cpp +487 -0
  125. package/eval/arena/tank/bots/half2/mimo-v2.6-pro.cpp +336 -0
  126. package/eval/arena/tank/bots/half2/muse-wonder-full.cpp +560 -0
  127. package/eval/arena/tank/bots/half2/opus-5.5.cpp +476 -0
  128. package/eval/arena/tank/bots/half2/qwen-3.8-max.cpp +392 -0
  129. package/eval/arena/tank/tournament/README.md +24 -0
  130. package/eval/arena/tank/tournament/bt.py +218 -0
  131. package/eval/arena/tank/tournament/final.json +202 -0
  132. package/eval/arena/tank/tournament/halftime.json +146 -0
  133. package/eval/arena/tank/tournament/progress.json +911 -0
  134. package/eval/arena/tank/tournament/schedule.py +100 -0
  135. package/eval/arena/tank/tournament/sparring.cpp +928 -0
  136. package/eval/arena/tank/tournament/tournament.py +1426 -0
  137. package/eval/arena/tank/workspace/README.md +169 -0
  138. package/eval/arena/tank/workspace/RULES.md +443 -0
  139. package/eval/arena/tank/workspace/arena.py +862 -0
  140. package/eval/arena/tank/workspace/baseline.cpp +259 -0
  141. package/eval/arena/tank/workspace/clock.py +103 -0
  142. package/eval/arena/tank/workspace/core.py +712 -0
  143. package/eval/arena/tank/workspace/engine.py +573 -0
  144. package/eval/arena/tank/workspace/history/.lock +0 -0
  145. package/eval/arena/tank/workspace/history/20261017-193610-08715287f91f-bot.cpp +109 -0
  146. package/eval/arena/tank/workspace/history/20261017-193619-f00b2d5635fa-gpt-6-astra.cpp +241 -0
  147. package/eval/arena/tank/workspace/history/20261017-193643-63319e0a2b1a-gpt-6_1-sol-ultrafast.cpp +356 -0
  148. package/eval/arena/tank/workspace/history/20261017-193647-41c1341d0a8a-gemini-4-argon.cpp +1390 -0
  149. package/eval/arena/tank/workspace/history/20261017-193652-e34c0f45539a-gpt-6_1-sol.cpp +87 -0
  150. package/eval/arena/tank/workspace/history/20261017-193654-c026348ce96e-opus-5_5.cpp +476 -0
  151. package/eval/arena/tank/workspace/history/20261017-193655-4a21366e94d1-muse-wonder-full.cpp +560 -0
  152. package/eval/arena/tank/workspace/history/20261017-193657-dbc4e9969c52-mimo-v2_6-pro.cpp +336 -0
  153. package/eval/arena/tank/workspace/history/20261017-193703-723cd036eb31-grok-4_7.cpp +1205 -0
  154. package/eval/arena/tank/workspace/history/20261017-193707-1ebbb227be68-deepseek-v4_1-flash.cpp +425 -0
  155. package/eval/arena/tank/workspace/history/20261017-193709-aeb0e7202ad5-doubao-seed-evolving.cpp +308 -0
  156. package/eval/arena/tank/workspace/history/20261017-193710-b21ebeb9bc76-kimi-k3.cpp +487 -0
  157. package/eval/arena/tank/workspace/history/20261017-193712-54cd86531905-haiku-5_5.cpp +210 -0
  158. package/eval/arena/tank/workspace/history/20261017-193714-efdb8e35c0bf-qwen-3_8-max.cpp +392 -0
  159. package/eval/arena/tank/workspace/history/20261017-193715-681423c23fdb-glm-5_3.cpp +651 -0
  160. package/eval/arena/tank/workspace/history/20261017-193717-1a8b3c75181d-sparring.cpp +928 -0
  161. package/eval/arena/tank/workspace/history/20261017-193720-1e0803a541e8-baseline.cpp +259 -0
  162. package/eval/arena/tank/workspace/history/log.jsonl +103 -0
  163. package/eval/arena/tank/workspace/judge/cycles.hpp +53 -0
  164. package/eval/arena/tank/workspace/judge/fast.cpp +337 -0
  165. package/eval/arena/tank/workspace/judge/referee.cpp +125 -0
  166. package/eval/arena/tank/workspace/judge/speed.cpp +99 -0
  167. package/eval/arena/tank/workspace/run.cmd +31 -0
  168. package/eval/arena/tank/workspace/run.sh +23 -0
  169. package/eval/arena/tank/workspace/tank.hpp +1762 -0
  170. package/eval/arena/tank/workspace/tank_main.cpp +215 -0
  171. package/eval/arena/tank/workspace/viewer.html +1150 -0
  172. package/eval/arena/warehouse/workspace/SPEC.md +130 -0
  173. package/eval/arena/warehouse/workspace/TASK.md +71 -0
  174. package/eval/arena/warehouse/workspace/baseline/common.h +88 -0
  175. package/eval/arena/warehouse/workspace/baseline/main.cpp +27 -0
  176. package/eval/arena/warehouse/workspace/baseline/old_stuff.cpp +87 -0
  177. package/eval/arena/warehouse/workspace/baseline/sim.cpp +427 -0
  178. package/eval/arena/warehouse/workspace/baseline/util.cpp +220 -0
  179. package/eval/arena/warehouse/workspace/clock.py +42 -0
  180. package/eval/arena/warehouse/workspace/src/common.h +88 -0
  181. package/eval/arena/warehouse/workspace/src/main.cpp +27 -0
  182. package/eval/arena/warehouse/workspace/src/old_stuff.cpp +87 -0
  183. package/eval/arena/warehouse/workspace/src/sim.cpp +427 -0
  184. package/eval/arena/warehouse/workspace/src/util.cpp +220 -0
  185. package/eval/arena/warehouse/workspace/tools/bench.py +119 -0
  186. package/eval/arena/warehouse/workspace/tools/check_input.py +161 -0
  187. package/eval/arena/warehouse/workspace/tools/gen.py +132 -0
  188. package/eval/arena/warehouse/workspace/tools/measure.py +107 -0
  189. package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.in +1420 -0
  190. package/eval/arena/warehouse/workspace/workloads/public/pub_crowded.out +9162 -0
  191. package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.in +1465 -0
  192. package/eval/arena/warehouse/workspace/workloads/public/pub_dynamic.out +3439 -0
  193. package/eval/arena/warehouse/workspace/workloads/public/pub_flood.in +2757 -0
  194. package/eval/arena/warehouse/workspace/workloads/public/pub_flood.out +3126 -0
  195. package/eval/arena/warehouse/workspace/workloads/public/pub_small.in +731 -0
  196. package/eval/arena/warehouse/workspace/workloads/public/pub_small.out +2578 -0
  197. package/eval/arena/warehouse/workspace/workloads/public/pub_wide.in +528 -0
  198. package/eval/arena/warehouse/workspace/workloads/public/pub_wide.out +2026 -0
  199. package/eval/bomberman.py +20 -0
  200. package/eval/commit0.py +21 -0
  201. package/eval/common.py +61 -0
  202. package/eval/ctf.py +20 -0
  203. package/eval/featurebench.py +25 -0
  204. package/eval/land.py +20 -0
  205. package/eval/legacy/bomberman-prices.json +4 -0
  206. package/eval/legacy/bomberman.py +161 -0
  207. package/eval/legacy/commit0.py +91 -0
  208. package/eval/legacy/ctf-prices.json +4 -0
  209. package/eval/legacy/ctf.py +178 -0
  210. package/eval/legacy/featurebench.py +103 -0
  211. package/eval/legacy/land-prices.json +4 -0
  212. package/eval/legacy/land.py +177 -0
  213. package/eval/legacy/swe_bench_pro.py +116 -0
  214. package/eval/legacy/tank-prices.json +3 -0
  215. package/eval/legacy/tank.py +408 -0
  216. package/eval/legacy/warehouse-prices.json +4 -0
  217. package/eval/legacy/warehouse.py +365 -0
  218. package/eval/swe-bench-pro.py +25 -0
  219. package/eval/tank.py +20 -0
  220. package/eval/warehouse.py +20 -0
  221. package/lib/client/index.js +376 -63
  222. package/lib/client/index.js.map +1 -1
  223. package/lib/index.js +684 -27
  224. package/package.json +2 -1
@@ -0,0 +1,573 @@
1
+ """坦克大战的裁判接口。这里没有任何规则:规则只在 tank.hpp 里。
2
+
3
+ 本文件做三件事,都是把活儿转给用 tank.hpp 编出来的小程序:
4
+ Game 平台要求的对局接口。每回合把双方的命令转给裁判核心(judge/referee.cpp),取回新的局面。
5
+ expand 把回放展开成逐回合的局面和事件(给 show 和回放查看器用)。
6
+ run_fast 快速模式:调用 judge/fast.cpp 编出来的对局程序。
7
+
8
+ 文本的格式(局面、命令、事件)是 tank.hpp 定的,这里只做拆字段,不做任何结算。
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import math
13
+ import queue
14
+ import shutil
15
+ import subprocess
16
+ import threading
17
+ import zlib
18
+ from pathlib import Path
19
+
20
+ import core
21
+
22
+ REPLAY_FORMAT = "tank-replay-1"
23
+ FRAMES_FORMAT = "tank-frames-1"
24
+ TYPE_NAMES = ["fast", "fire", "armor"]
25
+
26
+
27
+ class EngineError(core.MatchError):
28
+ """裁判核心出了问题(起不来、回复看不懂)。属于裁判这一侧的错,不算在选手头上。"""
29
+
30
+
31
+ # ====================================================================
32
+ # 裁判核心进程
33
+ # ====================================================================
34
+
35
+ class Referee:
36
+ """一个常驻的裁判核心进程。一问一答,按行通信。"""
37
+
38
+ def __init__(self):
39
+ try:
40
+ exe = core.build_tool("referee")
41
+ except RuntimeError as exc:
42
+ raise EngineError(f"裁判核心编译不了:{exc}")
43
+ try:
44
+ self.proc = subprocess.Popen([str(exe)], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
45
+ except OSError as exc:
46
+ raise EngineError(f"裁判核心无法启动:{exe}({exc})")
47
+ core.adopt(self.proc)
48
+ words = self.ask("HELLO").split()
49
+ if len(words) < 9 or words[0] != "OK":
50
+ raise EngineError("裁判核心的应答不对")
51
+ self.info = {"engine": words[1], "budget_kind": core.BUDGET_KIND, "first_ms": int(words[2]),
52
+ "turn_ms": int(words[3]), "wall_first_ms": int(words[4]), "wall_turn_ms": int(words[5]),
53
+ "units_per_ms": int(words[6]) if core.BUDGET_KIND == "cpu" else 1,
54
+ "units_source": words[7] if core.BUDGET_KIND == "cpu" else "wall", "styles": words[8:]}
55
+ core.configure_budget(self.info["units_per_ms"])
56
+
57
+ def send(self, text):
58
+ try:
59
+ self.proc.stdin.write(text.encode("utf-8"))
60
+ self.proc.stdin.flush()
61
+ except (OSError, ValueError) as exc:
62
+ raise EngineError(f"裁判核心已退出({exc})")
63
+
64
+ def line(self):
65
+ raw = self.proc.stdout.readline()
66
+ if not raw:
67
+ raise EngineError("裁判核心已退出")
68
+ return raw.decode("utf-8", errors="replace").rstrip("\r\n")
69
+
70
+ def ask(self, request):
71
+ self.send(request + "\n")
72
+ return self.line()
73
+
74
+ def until(self, last):
75
+ """一直读到内容为 last 的那一行(含)。"""
76
+ out = []
77
+ while True:
78
+ text = self.line()
79
+ out.append(text)
80
+ if text == last:
81
+ return out
82
+
83
+ def close(self):
84
+ core.stop_process(self.proc)
85
+
86
+
87
+ _idle = []
88
+ _idle_lock = threading.Lock()
89
+
90
+
91
+ def _acquire():
92
+ with _idle_lock:
93
+ if _idle:
94
+ return _idle.pop()
95
+ return Referee()
96
+
97
+
98
+ def _release(ref):
99
+ with _idle_lock:
100
+ _idle.append(ref)
101
+
102
+
103
+ def shutdown():
104
+ """结束本进程起的所有空闲裁判核心。"""
105
+ with _idle_lock:
106
+ refs, _idle[:] = list(_idle), []
107
+ for r in refs:
108
+ r.close()
109
+
110
+
111
+ def info():
112
+ """引擎版本、预算、地图风格名(从裁判核心读来,也就是从 tank.hpp 读来)。"""
113
+ ref = _acquire()
114
+ try:
115
+ return dict(ref.info)
116
+ finally:
117
+ _release(ref)
118
+
119
+
120
+ def style_number(style):
121
+ """地图风格:-1 或 auto = 按种子轮换;也可以写风格名或编号。"""
122
+ if style is None or str(style) in ("-1", "auto", ""):
123
+ return -1
124
+ names = info()["styles"]
125
+ if str(style) in names:
126
+ return names.index(str(style))
127
+ if str(style).isdigit() and int(style) < len(names):
128
+ return int(style)
129
+ raise ValueError(f"地图风格只能是 auto、{'、'.join(names)} 或 0–{len(names) - 1}")
130
+
131
+
132
+ # ====================================================================
133
+ # 文本拆字段(格式见 RULES.md / tank.hpp 第 8 节)
134
+ # ====================================================================
135
+
136
+ def split_state(text):
137
+ """完整局面文本 → (布局段, 回合段)。"""
138
+ at = text.index("TURN ")
139
+ return text[:at], text[at:]
140
+
141
+
142
+ def parse_layout(text):
143
+ """布局段 → {"params", "size", "bases", "spawns", "supply"}。"""
144
+ params, out = {}, {}
145
+ for row in text.splitlines():
146
+ w = row.split()
147
+ if not w:
148
+ continue
149
+ if w[0] == "P":
150
+ params[w[1]] = w[3:] if w[1] == "tiebreak" else int(w[2])
151
+ elif w[0] == "BASES":
152
+ v = list(map(int, w[1:]))
153
+ out["bases"] = [v[0:2], v[2:4]]
154
+ elif w[0] == "SPAWNS":
155
+ v = list(map(int, w[1:]))
156
+ out["spawns"] = [v[k:k + 2] for k in range(0, 12, 2)]
157
+ elif w[0] == "SUPPLY":
158
+ v = list(map(int, w[2:]))
159
+ out["supply"] = [v[k:k + 2] for k in range(0, len(v), 2)]
160
+ out["params"] = params
161
+ out["size"] = params.get("size", 15)
162
+ return out
163
+
164
+
165
+ def parse_turn(text):
166
+ """回合段 → 字典。只是拆字段。"""
167
+ rows = text.splitlines() if isinstance(text, str) else list(text)
168
+ f = {"tanks": [], "bullets": [], "stars": [], "map": []}
169
+ k = 0
170
+ while k < len(rows):
171
+ w = rows[k].split()
172
+ k += 1
173
+ if not w:
174
+ continue
175
+ head = w[0]
176
+ if head == "TURN":
177
+ f.update(turn=int(w[1]), done=w[2] == "1", winner=int(w[3]), reason=w[4])
178
+ elif head == "BASEHP":
179
+ f["base_hp"] = [int(w[1]), int(w[2])]
180
+ elif head == "KILLS":
181
+ f["kills"] = [int(w[1]), int(w[2])]
182
+ elif head == "STARCOUNT":
183
+ f["stars_eaten"] = [int(w[1]), int(w[2])]
184
+ elif head == "BASEDMG":
185
+ f["base_dmg"] = [int(w[1]), int(w[2])]
186
+ elif head == "MAP":
187
+ while k < len(rows) and not rows[k].startswith("TANKS"):
188
+ f["map"].append(rows[k].strip())
189
+ k += 1
190
+ elif head == "T":
191
+ i = int(w[1])
192
+ f["tanks"].append({"id": i, "seat": i // 3, "type": TYPE_NAMES[i % 3], "x": int(w[2]), "y": int(w[3]),
193
+ "hp": int(w[4]), "level": int(w[5]), "alive": w[6] == "1"})
194
+ elif head == "B":
195
+ f["bullets"].append({"x": int(w[1]), "y": int(w[2]), "dir": w[3], "owner": int(w[4]),
196
+ "steel": w[5] == "1"})
197
+ elif head == "STARS":
198
+ for _ in range(int(w[1])):
199
+ f["stars"].append(list(map(int, rows[k].split())))
200
+ k += 1
201
+ return f
202
+
203
+
204
+ # 事件行 → 字典。每种事件的字段名按 tank.hpp 的 event_text 的顺序。
205
+ _EVENT_FIELDS = {
206
+ "fire": ["tank", "dir"],
207
+ "blocked": ["tank", "sub"],
208
+ "cancel": ["how", "x", "y", "sub", "owner1", "owner2"],
209
+ "hit": ["tank", "by", "x", "y", "sub", "swap"],
210
+ "base_hit": ["seat", "by", "sub"],
211
+ "wall": ["x", "y", "kind", "sub"],
212
+ "death": ["tank", "x", "y", "by"],
213
+ "respawn": ["tank", "x", "y"],
214
+ "bullet_cleared": ["x", "y", "tank"],
215
+ "star": ["tank", "x", "y", "level"],
216
+ "supply": ["x", "y"],
217
+ "base_regen": ["seat"],
218
+ }
219
+
220
+
221
+ def parse_event(text):
222
+ w = text.split()
223
+ ev = {"type": w[0], "text": text}
224
+ for name, value in zip(_EVENT_FIELDS.get(w[0], []), w[1:]):
225
+ ev[name] = int(value) if value.lstrip("-").isdigit() else value
226
+ return ev
227
+
228
+
229
+ # ====================================================================
230
+ # Game:平台接口
231
+ # ====================================================================
232
+
233
+ class Game:
234
+ """一局坦克大战。所有结算都在裁判核心里(tank.hpp),这里只转手。"""
235
+
236
+ TIME_LIMITS = None # 思考预算 (首回合秒数, 之后每回合秒数),第一次开局时从裁判核心读来
237
+ WALL_CAPS = None # 按 CPU 计时时的墙上保险上限,同上
238
+
239
+ def __init__(self, n=2, seed=1, style=-1, start=None):
240
+ if n != 2:
241
+ raise ValueError("坦克大战是两方对战")
242
+ self.seed, self.style = int(seed), int(style)
243
+ self.ref = _acquire()
244
+ try:
245
+ if Game.TIME_LIMITS is None:
246
+ Game.TIME_LIMITS = (self.ref.info["first_ms"] / 1000.0, self.ref.info["turn_ms"] / 1000.0)
247
+ Game.WALL_CAPS = (self.ref.info["wall_first_ms"] / 1000.0, self.ref.info["wall_turn_ms"] / 1000.0)
248
+ self._seat_line = f"BUDGET {self.ref.info['budget_kind']} {self.ref.info['units_per_ms']}"
249
+ if start is None:
250
+ if self.ref.ask(f"NEW {self.seed} {self.style}") != "OK":
251
+ raise EngineError("裁判核心开局失败")
252
+ self.start = "\n".join(self.ref.until("END")) + "\n"
253
+ else:
254
+ self.ref.send("LOAD\n" + start)
255
+ answer = self.ref.line()
256
+ if answer != "OK":
257
+ raise EngineError(f"起始局面读不进去:{answer}")
258
+ self.start = start
259
+ except BaseException:
260
+ self._drop()
261
+ raise
262
+ self.layout_text, self.turn_text = split_state(self.start)
263
+ self._orders = [] # 每回合 [0 号方命令, 1 号方命令],实际执行的
264
+ self._checked = [None, None]
265
+ self.forfeited = []
266
+ self._read_turn_line()
267
+
268
+ def _read_turn_line(self):
269
+ w = self.turn_text.split(None, 5)
270
+ self.turn, self.done, self.winner, self.reason = int(w[1]), w[2] == "1", int(w[3]), w[4]
271
+
272
+ def _drop(self):
273
+ """裁判核心状态不明,不再放回空闲池。"""
274
+ if self.ref is not None:
275
+ self.ref.close()
276
+ self.ref = None
277
+
278
+ def close(self):
279
+ if self.ref is not None:
280
+ _release(self.ref)
281
+ self.ref = None
282
+
283
+ def encode(self, i):
284
+ """发给第 i 方的输入:开局是 "SEAT i BUDGET 计时方式 系数" 加完整局面,之后每回合只发回合段。"""
285
+ if self.turn == 0:
286
+ return f"SEAT {i} {self._seat_line}\n{self.start}"
287
+ return self.turn_text
288
+
289
+ def decode(self, i, line):
290
+ """检查第 i 方的一行回复。合法返回规范写法(如 "UL FU S"),不合法抛出 ValueError。"""
291
+ line = " ".join(line.split("\n")[0].split())
292
+ try:
293
+ answer = self.ref.ask(f"CHECK {i} {line}")
294
+ except EngineError:
295
+ self._drop()
296
+ raise
297
+ if answer.startswith("OK "):
298
+ self._checked[i] = answer[3:]
299
+ return self._checked[i]
300
+ if answer.startswith("ERR"):
301
+ raise ValueError(answer[4:] or "格式不对")
302
+ self._drop()
303
+ raise EngineError(f"裁判核心的回复看不懂:{answer[:80]}")
304
+
305
+ def step(self, orders):
306
+ """结算一回合。orders[i] 是 decode 的返回值;None 表示这一回合三辆车都不动。"""
307
+ applied = []
308
+ for i in range(2):
309
+ o = orders[i] if orders is not None and i < len(orders) else None
310
+ if o is None:
311
+ # 这一方不动。decode 可能已经在裁判核心里记了一条,用一条"都不动"盖掉。
312
+ self.decode(i, f"{self.turn + 1} S S S")
313
+ o = "S S S"
314
+ elif self._checked[i] != o:
315
+ o = self.decode(i, f"{self.turn + 1} {o}")
316
+ applied.append(o)
317
+ try:
318
+ if self.ref.ask("STEP") != "OK":
319
+ raise EngineError("裁判核心结算失败")
320
+ self.turn_text = "\n".join(self.ref.until("END")) + "\n"
321
+ except EngineError:
322
+ self._drop()
323
+ raise
324
+ self._checked = [None, None]
325
+ self._orders.append(applied)
326
+ self._read_turn_line()
327
+ return {"turn": self.turn, "orders": applied}
328
+
329
+ def forfeit(self, seats):
330
+ """这些座位因故障判负:对局就此结束,这一回合不结算。两方同时故障算平局。"""
331
+ self.forfeited = sorted(set(seats))
332
+ self.done = True
333
+ self.winner = -1 if len(self.forfeited) >= 2 else 1 - self.forfeited[0]
334
+ self.reason = "fault"
335
+
336
+ def header(self):
337
+ inf = self.ref.info if self.ref else info()
338
+ return {"format": REPLAY_FORMAT, "engine": inf["engine"], "seed": self.seed, "style": self.style,
339
+ "budget": {"kind": inf["budget_kind"], "first_ms": inf["first_ms"], "turn_ms": inf["turn_ms"],
340
+ "wall_first_ms": inf["wall_first_ms"], "wall_turn_ms": inf["wall_turn_ms"],
341
+ "units_per_ms": inf["units_per_ms"], "max_strikes": core.MAX_STRIKES},
342
+ "start": self.start}
343
+
344
+ def frame(self):
345
+ return parse_turn(self.turn_text)
346
+
347
+ def result(self):
348
+ f = parse_turn(self.turn_text)
349
+ score = [0.5, 0.5] if self.winner < 0 else [1.0 if self.winner == i else 0.0 for i in range(2)]
350
+ return {"score": score, "winner": self.winner, "reason": self.reason, "turns": self.turn,
351
+ "base_hp": f["base_hp"], "kills": f["kills"], "stars_eaten": f["stars_eaten"],
352
+ "base_dmg": f["base_dmg"], "forfeited": list(self.forfeited), "final": self.turn_text}
353
+
354
+ def signature(self):
355
+ """对局签名:逐回合实际执行的命令 + 终局局面(+ 谁因故障判负)。和 judge/fast.cpp 的算法相同。"""
356
+ return signature_of(self._orders, self.turn_text, self.forfeited)
357
+
358
+
359
+ def signature_of(orders, final_text, forfeited=()):
360
+ log = "".join(f"{a}|{b}\n" for a, b in orders)
361
+ tail = final_text
362
+ if forfeited:
363
+ tail += "FAULT " + ("1" if 0 in forfeited else "0") + ("1" if 1 in forfeited else "0") + "\n"
364
+ return f"{zlib.crc32(log.encode()):08x}{zlib.crc32(tail.encode()):08x}"
365
+
366
+
367
+ # ====================================================================
368
+ # 回放展开
369
+ # ====================================================================
370
+
371
+ def expand(replay):
372
+ """把回放展开成逐回合的局面和事件。返回的字典可以直接存成 JSON 给查看器用(格式见 README)。
373
+
374
+ 局面由回放里的起始局面和每回合的命令重新结算得到,用的是同一个引擎。"""
375
+ layout_text, _ = split_state(replay["start"])
376
+ lay = parse_layout(layout_text)
377
+ ref = _acquire()
378
+ try:
379
+ ref.send("LOAD\n" + replay["start"])
380
+ answer = ref.line()
381
+ if answer != "OK":
382
+ raise EngineError(f"回放的起始局面读不进去:{answer}")
383
+ first = "\n".join(ref.ask("STATE").splitlines() + ref.until("END"))
384
+ _, turn_text = split_state(first + "\n")
385
+
386
+ def with_info(frame, rows):
387
+ for row in rows:
388
+ w = row.split()
389
+ if w and w[0] == "X":
390
+ frame["tanks"][int(w[1])].update(respawn_in=int(w[2]), reload_left=int(w[3]),
391
+ bullets_out=int(w[4]), bullet_cap=int(w[5]),
392
+ can_fire=w[6] == "1")
393
+ return frame
394
+
395
+ ref.send("INFO\n")
396
+ frame = with_info(parse_turn(turn_text), ref.until("DONE"))
397
+ frame.update(orders=None, used_ms=None, events=[], faults=[])
398
+ frames = [frame]
399
+ faults_by_turn = {}
400
+ for f in replay.get("faults", []):
401
+ faults_by_turn.setdefault(f["turn"], []).append(f)
402
+ used = replay.get("used_ms") or []
403
+ for k, pair in enumerate(replay["orders"]):
404
+ turn = frames[-1]["turn"] + 1
405
+ for seat in range(2):
406
+ answer = ref.ask(f"CHECK {seat} {turn} {pair[seat]}")
407
+ if not answer.startswith("OK"):
408
+ raise EngineError(f"回放第 {turn} 回合的命令不合法:{pair[seat]}")
409
+ if ref.ask("STEPX") != "OK":
410
+ raise EngineError("裁判核心结算失败")
411
+ rows = ref.until("DONE")
412
+ events = [parse_event(r[3:]) for r in rows if r.startswith("EV ")]
413
+ body = [r for r in rows if not r.startswith("EV ")]
414
+ end = body.index("END")
415
+ frame = with_info(parse_turn(body[:end + 1]), body[end + 1:])
416
+ frame.update(orders=[p.split() for p in pair], used_ms=used[k] if k < len(used) else None,
417
+ events=events, faults=faults_by_turn.get(turn, []))
418
+ frames.append(frame)
419
+ final_text = "\n".join(body[:end + 1]) + "\n"
420
+ if not replay["orders"]:
421
+ final_text = turn_text
422
+ except EngineError:
423
+ ref.close()
424
+ ref = None
425
+ raise
426
+ finally:
427
+ if ref is not None:
428
+ _release(ref)
429
+ result = replay.get("result", {})
430
+ recorded = result.get("final")
431
+ return {
432
+ "format": FRAMES_FORMAT, "engine": replay.get("engine"), "mode": replay.get("mode"),
433
+ "seed": replay.get("seed"), "style": replay.get("style"), "names": replay.get("names"),
434
+ "budget": replay.get("budget"), "params": lay["params"], "size": lay["size"], "bases": lay["bases"],
435
+ "spawns": lay["spawns"], "supply": lay["supply"],
436
+ "result": {k: v for k, v in result.items() if k != "final"},
437
+ "faults": replay.get("faults", []), "stderr": replay.get("stderr", []),
438
+ "consistent": recorded is None or recorded == final_text,
439
+ "frames": frames,
440
+ }
441
+
442
+
443
+ # ====================================================================
444
+ # 快速模式
445
+ # ====================================================================
446
+
447
+ class FastError(RuntimeError):
448
+ """快速模式里有 bot 崩溃、卡死或抛异常,整批停止。message 里写明是谁、哪一局。"""
449
+
450
+
451
+ def _parse_usage(text):
452
+ first, avg, slowest, over, illegal = text.split(":")
453
+ return {"first_ms": float(first), "avg_ms": float(avg), "max_ms": float(slowest), "over": int(over),
454
+ "illegal": int(illegal)}
455
+
456
+
457
+ def run_fast(lib_a, lib_b, jobs, workers=1, log=False, hang_seconds=60, names=("A", "B")):
458
+ """快速模式打一批。jobs 是 [(种子, 风格, 换边)],换边为 0 时 A 坐 0 号位。返回与 jobs 等长的结果列表。
459
+
460
+ 每个结果:{"seed", "style", "swap", "turns", "winner"(座位,-1 平), "reason", "sig", "usage": [A, B]},
461
+ log=True 时另有 "orders"(逐回合实际执行的命令)、"final"(终局回合段)、"faults"。
462
+ 任何一方崩溃、卡死、抛异常:抛出 FastError。"""
463
+ lib_a, lib_b = Path(lib_a), Path(lib_b)
464
+ if lib_a.resolve() == lib_b.resolve():
465
+ # 同一个文件在一个进程里只能加载一次,自己打自己时给第二份另起一个文件名。
466
+ twin = lib_b.with_name(lib_b.stem + "-twin" + lib_b.suffix)
467
+ if not twin.is_file():
468
+ tmp = twin.with_name(f"tmp-{threading.get_ident()}-{twin.name}")
469
+ shutil.copyfile(lib_b, tmp)
470
+ try:
471
+ tmp.replace(twin)
472
+ except OSError:
473
+ if not twin.is_file():
474
+ raise
475
+ lib_b = twin
476
+ try:
477
+ exe = core.build_tool("fast")
478
+ except RuntimeError as exc:
479
+ raise EngineError(f"快速模式的对局程序编译不了:{exc}")
480
+ results = [None] * len(jobs)
481
+ workers = max(1, min(workers, len(jobs) or 1))
482
+ size = max(1, min(200, math.ceil(len(jobs) / workers)))
483
+ chunks = queue.Queue()
484
+ for at in range(0, len(jobs), size):
485
+ chunks.put(list(range(at, min(len(jobs), at + size))))
486
+ failure, running, lock = [], [], threading.Lock()
487
+
488
+ def describe(k):
489
+ seed, style, swap = jobs[k]
490
+ seats = f"{names[1]} 坐 0 号位、{names[0]} 坐 1 号位" if swap else f"{names[0]} 坐 0 号位、{names[1]} 坐 1 号位"
491
+ return f"第 {k + 1} 局(种子 {seed},{seats})"
492
+
493
+ def work():
494
+ while not failure:
495
+ try:
496
+ ids = chunks.get_nowait()
497
+ except queue.Empty:
498
+ return
499
+ cmd = [str(exe), str(lib_a), str(lib_b), "--hang", str(hang_seconds)] + (["--log"] if log else []) + (["--wall"] if core.BUDGET_KIND == "wall" else [])
500
+ text = "".join(f"{k} {jobs[k][0]} {jobs[k][1]} {jobs[k][2]}\n" for k in ids)
501
+ try:
502
+ proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL)
503
+ except OSError as exc:
504
+ failure.append(EngineError(f"快速模式的对局程序无法启动:{exe}({exc})"))
505
+ return
506
+ core.adopt(proc)
507
+ with lock:
508
+ running.append(proc)
509
+ out, _ = proc.communicate(text.encode())
510
+ with lock:
511
+ running.remove(proc)
512
+ if failure:
513
+ return
514
+ current, orders, faults, final, finished = None, [], [], None, False
515
+ rows = out.decode("utf-8", errors="replace").splitlines()
516
+ at = 0
517
+ while at < len(rows):
518
+ w = rows[at].split(None, 3)
519
+ at += 1
520
+ if not w:
521
+ continue
522
+ if w[0] == "B":
523
+ current, orders, faults, final = int(w[1]), [], [], None
524
+ elif w[0] == "O":
525
+ orders.append(rows[at - 1].split(None, 2)[2].split("|"))
526
+ elif w[0] == "F":
527
+ faults.append({"turn": int(w[1]), "seat": int(w[2]), "kind": "illegal", "detail": w[3]})
528
+ elif w[0] == "FINAL":
529
+ end = rows.index("END", at)
530
+ final = "\n".join(rows[at:end + 1]) + "\n"
531
+ at = end + 1
532
+ elif w[0] == "G":
533
+ g = rows[at - 1].split()
534
+ k = int(g[1])
535
+ results[k] = {"seed": jobs[k][0], "style": jobs[k][1], "swap": jobs[k][2], "turns": int(g[5]),
536
+ "winner": int(g[6]), "reason": g[7], "sig": g[8],
537
+ "usage": [_parse_usage(g[9]), _parse_usage(g[10])], "faults": faults}
538
+ if log:
539
+ results[k].update(orders=orders, final=final)
540
+ current = None
541
+ elif w[0] == "X":
542
+ x = rows[at - 1].split(None, 3)
543
+ who = {"A": names[0], "B": names[1]}.get(x[2], "不确定是哪一方")
544
+ where = describe(int(x[1])) if x[1].lstrip("-").isdigit() and int(x[1]) >= 0 else "开局前"
545
+ failure.append(FastError(f"{where}:{who} 出事了:{x[3] if len(x) > 3 else ''}"))
546
+ return
547
+ elif w[0] == "DONE":
548
+ finished = True
549
+ if not finished:
550
+ where = describe(current) if current is not None else "某一局"
551
+ failure.append(FastError(f"{where}:对局进程异常退出({core.describe_exit(proc.returncode)}),"
552
+ f"不确定是哪一方造成的。可以用 play 在正式模式下重打这一局来确认。"))
553
+ return
554
+
555
+ threads = [threading.Thread(target=work, daemon=True) for _ in range(workers)]
556
+ try:
557
+ for t in threads:
558
+ t.start()
559
+ while any(t.is_alive() for t in threads):
560
+ for t in threads:
561
+ t.join(timeout=0.2)
562
+ if failure:
563
+ break
564
+ finally:
565
+ with lock:
566
+ for proc in list(running): # 只结束自己起的进程
567
+ try:
568
+ proc.kill()
569
+ except Exception:
570
+ pass
571
+ if failure:
572
+ raise failure[0]
573
+ return results
File without changes
@@ -0,0 +1,109 @@
1
+ // template.cpp —— 起步模板。能合法行动,打法最朴素:三辆车都朝对方基地走,
2
+ // 炮线上是对方基地(中间只隔着砖)就开炮,路被砖挡住就把砖打掉。
3
+ // 它不躲子弹、不分工、不看对方坦克,也不用引擎前推,所以很弱。请把它当作接口的用法示例,然后重写。
4
+ //
5
+ // 你要实现的只有两个函数:
6
+ // void init(const tank::State& s, int me); 每局开始调用一次,me 是你的座位(0 在下方,1 在上方)
7
+ // tank::Orders decide(const tank::State& s, int me); 每回合调用一次,返回三辆车的命令
8
+ // 全局变量可以跨回合保留。tank.hpp 里有完整的规则实现:tank::step 可以把局面精确前推一回合。
9
+ #include "tank.hpp"
10
+
11
+ using namespace tank;
12
+
13
+ static int dist_to_base[CELLS]; // 每个格子到对方基地要走几步(砖墙当作可以走,因为能打掉)
14
+
15
+ // 跨回合记的东西(全局变量会保留):用来发现"卡住了"。
16
+ static int last_pos[3]; // 每辆车上一回合在哪一格
17
+ static int stuck_turns[3]; // 每辆车已经连续几回合没挪地方
18
+ static int last_enemy_hp; // 上一回合对方基地的血量
19
+ static int quiet_turns; // 对方基地已经连续几回合没掉血
20
+
21
+ // 从对方基地出发做一次广度优先搜索,填好 dist_to_base。钢墙、水、己方基地、界外都走不了。
22
+ static void compute_distances(const State& s, int me) {
23
+ static int queue[CELLS];
24
+ for (int c = 0; c < CELLS; ++c) dist_to_base[c] = 9999;
25
+ int head = 0, tail = 0;
26
+ const int target = s.base_pos[1 - me];
27
+ dist_to_base[target] = 0;
28
+ queue[tail++] = target;
29
+ while (head < tail) {
30
+ const int c = queue[head++];
31
+ for (int d = 0; d < 4; ++d) {
32
+ const int next = c + DIR_DELTA[d]; // 四周有一圈界外格,不用判断越界
33
+ const uint8_t g = s.grid[next];
34
+ if ((g == EMPTY || g == BRICK) && dist_to_base[next] > dist_to_base[c] + 1) {
35
+ dist_to_base[next] = dist_to_base[c] + 1;
36
+ queue[tail++] = next;
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ // 从 pos 朝方向 d 看过去:一路只有空地、水、砖墙,然后是对方基地,就返回 true。
43
+ // 注意:这里只看地形(s.grid),不看这条线上有没有坦克;线上的坦克会先挨这一炮,要另用 tank_at 查。
44
+ static bool base_in_line(const State& s, int me, int pos, int d) {
45
+ for (int c = pos + DIR_DELTA[d];; c += DIR_DELTA[d]) {
46
+ const uint8_t g = s.grid[c];
47
+ if (g == BASE0 + (1 - me)) return true;
48
+ if (g != EMPTY && g != WATER && g != BRICK) return false; // 钢墙、己方基地、界外
49
+ }
50
+ }
51
+
52
+ void init(const State& s, int me) {
53
+ // 首回合有 2 秒,可以在这里做预计算。模板只把跨回合的记录清零。
54
+ for (int type = 0; type < 3; ++type) { last_pos[type] = -1; stuck_turns[type] = 0; }
55
+ last_enemy_hp = s.base_hp[1 - me];
56
+ quiet_turns = 0;
57
+ }
58
+
59
+ Orders decide(const State& s, int me) {
60
+ Orders orders; // 默认三辆车都不动
61
+ compute_distances(s, me); // 砖墙会被打掉,所以每回合重算(很快)
62
+ quiet_turns = s.base_hp[1 - me] < last_enemy_hp ? 0 : quiet_turns + 1;
63
+ last_enemy_hp = s.base_hp[1 - me];
64
+
65
+ for (int type = 0; type < 3; ++type) { // 0 快速型,1 火力型,2 装甲型
66
+ const int i = tank_id(me, type); // 这辆车在 s.tank[] 里的编号
67
+ const Tank& t = s.tank[i];
68
+ if (!t.alive) { last_pos[type] = -1; continue; } // 被毁后等复活,命令会被忽略
69
+ const bool ready = can_fire(s, i); // 装填完毕、场上子弹没到上限
70
+ stuck_turns[type] = t.pos == last_pos[type] ? stuck_turns[type] + 1 : 0;
71
+ last_pos[type] = t.pos;
72
+
73
+ // 0) 站在原地 4 回合了,对方基地这 4 回合也没掉血:多半是和对面的车对射、子弹互相抵消,
74
+ // 或者被别的车堵住了。往旁边挪一格(三辆车、不同回合挑的方向不一样),换个位置再说。
75
+ if (stuck_turns[type] >= 4 && quiet_turns >= 4) {
76
+ for (int k = 0; k < 4; ++k) {
77
+ const int d = (2 + type + s.turn / 4 + k) % 4;
78
+ if (s.grid[t.pos + DIR_DELTA[d]] == EMPTY && tank_at(s, t.pos + DIR_DELTA[d]) < 0) {
79
+ orders.cmd[type] = cmd_move(d);
80
+ break;
81
+ }
82
+ }
83
+ continue;
84
+ }
85
+
86
+ // 1) 对方基地在炮线上:开炮(装填中就原地等)。
87
+ int fire_dir = -1;
88
+ for (int d = 0; d < 4; ++d)
89
+ if (base_in_line(s, me, t.pos, d)) fire_dir = d;
90
+ if (fire_dir >= 0) {
91
+ if (ready) orders.cmd[type] = cmd_fire(fire_dir);
92
+ continue;
93
+ }
94
+
95
+ // 2) 否则朝离对方基地更近的邻格走;那一格是砖墙就先把它打掉。
96
+ int best_dir = -1, best = dist_to_base[t.pos];
97
+ for (int d = 0; d < 4; ++d) {
98
+ const int next = t.pos + DIR_DELTA[d];
99
+ if (dist_to_base[next] < best) { best = dist_to_base[next]; best_dir = d; }
100
+ }
101
+ if (best_dir < 0) continue;
102
+ if (s.grid[t.pos + DIR_DELTA[best_dir]] == BRICK) {
103
+ if (ready) orders.cmd[type] = cmd_fire(best_dir);
104
+ } else {
105
+ orders.cmd[type] = cmd_move(best_dir); // 快速型可以用 cmd_move2(d1, d2) 一回合走两步,模板没有用
106
+ }
107
+ }
108
+ return orders;
109
+ }