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,1762 @@
|
|
|
1
|
+
// tank.hpp —— 坦克大战规则引擎(单头文件,C++20,只用标准库)
|
|
2
|
+
//
|
|
3
|
+
// 这份头文件是规则的权威出处:裁判用它结算,bot 直接包含它做精确前推。
|
|
4
|
+
// 结算完全确定、没有随机,对两个座位完全对称。
|
|
5
|
+
//
|
|
6
|
+
// 第一次读,先看这三处:
|
|
7
|
+
//
|
|
8
|
+
// 规则数值在哪 第 2 节的 struct Params,前半部分"本届比赛的规则":地图、基地血量、三种车型、
|
|
9
|
+
// 升级效果、星星、复活、胜负。每一项的注释就是规则。局面里带着一份:s.P。
|
|
10
|
+
//
|
|
11
|
+
// step 怎么用 tank::State next = s; // 局面是定长结构,直接按值拷贝
|
|
12
|
+
// tank::step(next, 命令0, 命令1); // 精确结算一回合;命令0 / 命令1 是 0 号方 / 1 号方的 Orders
|
|
13
|
+
// tank::step_as(next, me, 我方, 对方); // 同上,按自己的视角给
|
|
14
|
+
// 每辆车能下哪些命令:legal_commands(s, 坦克编号, out)。
|
|
15
|
+
// 结算的每一步对应哪条规则,写在第 5 节 step 的注释里。
|
|
16
|
+
//
|
|
17
|
+
// 预算怎么查 tank::budget_left() 返回本回合还剩多少 CPU 毫秒(首回合 2000,之后每回合 50,见第 9 节):
|
|
18
|
+
// while (tank::budget_left() > 3) { /* 再搜一层 */ }
|
|
19
|
+
//
|
|
20
|
+
// 目录(按出现顺序):
|
|
21
|
+
// 1. 坐标、方向、命令编码
|
|
22
|
+
// 2. Params:规则数值
|
|
23
|
+
// 3. State:完整局面
|
|
24
|
+
// 4. 查询:子弹上限、装填、复活倒计时、某格上的坦克
|
|
25
|
+
// 5. step:结算一回合
|
|
26
|
+
// 6. 合法命令枚举
|
|
27
|
+
// 7. 地图生成与开局:generate_map / new_game / make_game
|
|
28
|
+
// 8. 局面与文本互转:to_text / from_text / 命令行的读写
|
|
29
|
+
// 9. budget_left:思考预算
|
|
30
|
+
//
|
|
31
|
+
// 约定:
|
|
32
|
+
// 坐标 x 向右、y 向下,左上角是 (0,0)。0 号方在下(y 大的一侧),1 号方在上。
|
|
33
|
+
// 坦克编号 0/1/2 = 0 号方的 快速/火力/装甲,3/4/5 = 1 号方的 快速/火力/装甲。
|
|
34
|
+
// s.turn 是"已经结算完的回合数",开局为 0;第一次 step 结算的是第 1 回合。
|
|
35
|
+
#pragma once
|
|
36
|
+
|
|
37
|
+
#include <chrono>
|
|
38
|
+
#include <cstdint>
|
|
39
|
+
#include <initializer_list>
|
|
40
|
+
#include <string>
|
|
41
|
+
#include <string_view>
|
|
42
|
+
#include <type_traits>
|
|
43
|
+
#include <vector>
|
|
44
|
+
#if !defined(_WIN32)
|
|
45
|
+
#include <time.h> // 思考预算:线程 CPU 时间(见第 9 节)
|
|
46
|
+
#endif
|
|
47
|
+
|
|
48
|
+
namespace tank {
|
|
49
|
+
|
|
50
|
+
inline constexpr const char* ENGINE_VERSION = "tank-cpp-0.1";
|
|
51
|
+
|
|
52
|
+
// ====================================================================
|
|
53
|
+
// 1. 坐标、方向、命令编码
|
|
54
|
+
// ====================================================================
|
|
55
|
+
|
|
56
|
+
inline constexpr int MAX_SIZE = 21; // 地图边长上限
|
|
57
|
+
inline constexpr int STRIDE = MAX_SIZE + 2; // 格子编号的行宽:地图四周各留一圈"界外"
|
|
58
|
+
inline constexpr int CELLS = STRIDE * STRIDE;
|
|
59
|
+
inline constexpr int MAX_SUPPLY = 5; // 补给点数量上限
|
|
60
|
+
inline constexpr int MAX_BULLETS = 24; // 场上子弹数量上限(6 辆 × 每辆最多 4 发)
|
|
61
|
+
inline constexpr int NO_CELL = 0; // 不在场的坦克的位置;换算成坐标是 (-1,-1)
|
|
62
|
+
|
|
63
|
+
// 格子编号:局面里所有位置都用一个整数表示,四周有一圈 OUTSIDE,所以"向某方向走一格"
|
|
64
|
+
// 就是加一个常数,不用判断边界。
|
|
65
|
+
constexpr int cell_of(int x, int y) { return (y + 1) * STRIDE + (x + 1); }
|
|
66
|
+
constexpr int cell_x(int c) { return c % STRIDE - 1; }
|
|
67
|
+
constexpr int cell_y(int c) { return c / STRIDE - 1; }
|
|
68
|
+
|
|
69
|
+
// 地形。BASE0 / BASE1 是两方基地;OUTSIDE 是界外。
|
|
70
|
+
enum Cell : uint8_t { EMPTY = 0, BRICK = 1, STEEL = 2, WATER = 3, BASE0 = 4, BASE1 = 5, OUTSIDE = 6 };
|
|
71
|
+
// (界外叫 OUTSIDE 而不叫 OUT,是因为 <windows.h> 把 OUT 定义成了宏。)
|
|
72
|
+
inline constexpr char CELL_CHARS[] = ".#@~01"; // 文本里的写法(OUTSIDE 不出现在文本里)
|
|
73
|
+
|
|
74
|
+
// 方向:0=U(y-1) 1=D(y+1) 2=L(x-1) 3=R(x+1)。d ^ 1 是反方向。
|
|
75
|
+
enum Dir : int { UP = 0, DOWN = 1, LEFT = 2, RIGHT = 3 };
|
|
76
|
+
inline constexpr int DIR_DELTA[4] = {-STRIDE, STRIDE, -1, 1};
|
|
77
|
+
inline constexpr int DIR_DX[4] = {0, 0, -1, 1};
|
|
78
|
+
inline constexpr int DIR_DY[4] = {-1, 1, 0, 0};
|
|
79
|
+
inline constexpr char DIR_CHARS[] = "UDLR";
|
|
80
|
+
|
|
81
|
+
// 车型。
|
|
82
|
+
enum Type : int { FAST = 0, FIRE = 1, ARMOR = 2 };
|
|
83
|
+
inline constexpr const char* TYPE_NAMES[3] = {"fast", "fire", "armor"};
|
|
84
|
+
constexpr int tank_id(int seat, int type) { return 3 * seat + type; }
|
|
85
|
+
constexpr int seat_of(int tank) { return tank / 3; }
|
|
86
|
+
constexpr int type_of(int tank) { return tank % 3; }
|
|
87
|
+
|
|
88
|
+
// 命令编码(一辆车一个字节):
|
|
89
|
+
// 0 不动 文本 S
|
|
90
|
+
// 1..4 向 U/D/L/R 走一步 文本 U D L R
|
|
91
|
+
// 5..8 向 U/D/L/R 开炮 文本 FU FD FL FR
|
|
92
|
+
// 9..24 快速型连走两步,9 + 4*d1 + d2(先 d1 后 d2) 文本两个方向字母,如 UL
|
|
93
|
+
// 其他数值一律当作不动。非快速型给了两步时只执行第一步。
|
|
94
|
+
using Cmd = uint8_t;
|
|
95
|
+
inline constexpr Cmd STAY = 0;
|
|
96
|
+
inline constexpr int N_CMDS = 25;
|
|
97
|
+
constexpr Cmd cmd_move(int d) { return Cmd(1 + d); }
|
|
98
|
+
constexpr Cmd cmd_fire(int d) { return Cmd(5 + d); }
|
|
99
|
+
constexpr Cmd cmd_move2(int d1, int d2) { return Cmd(9 + 4 * d1 + d2); }
|
|
100
|
+
constexpr bool is_move(Cmd c) { return c >= 1 && c <= 4; }
|
|
101
|
+
constexpr bool is_fire(Cmd c) { return c >= 5 && c <= 8; }
|
|
102
|
+
constexpr bool is_move2(Cmd c) { return c >= 9 && c <= 24; }
|
|
103
|
+
|
|
104
|
+
// 一方一回合的命令:cmd[FAST]、cmd[FIRE]、cmd[ARMOR]。不在场的坦克的命令会被忽略。
|
|
105
|
+
struct Orders {
|
|
106
|
+
Cmd cmd[3] = {STAY, STAY, STAY};
|
|
107
|
+
bool operator==(const Orders&) const = default;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// ====================================================================
|
|
111
|
+
// 2. Params:规则数值
|
|
112
|
+
// ====================================================================
|
|
113
|
+
// 分两组。前一组是本届比赛的规则,默认值就是正式赛用的数值。
|
|
114
|
+
// 后一组在本届比赛中固定为默认值,仅用于测试,写 bot 时不用关心。
|
|
115
|
+
// Params 存在 State 里(s.P),step 只读它。
|
|
116
|
+
|
|
117
|
+
struct TankType {
|
|
118
|
+
int hp; // 初始血量
|
|
119
|
+
int speed; // 每回合最多走几步(1 或 2)
|
|
120
|
+
int bullets; // 0 级时的场上子弹上限
|
|
121
|
+
int reload; // 0 级时开炮后要装填几回合(0 = 不用装填,可以每回合都开炮)
|
|
122
|
+
bool operator==(const TankType&) const = default;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// 超时或同归于尽时依次比较的项目。
|
|
126
|
+
enum Tiebreak : int { TB_BASE = 0, TB_KILLS = 1, TB_STARS = 2, TB_DAMAGE = 3 };
|
|
127
|
+
inline constexpr const char* TIEBREAK_NAMES[4] = {"base", "kills", "stars", "damage"};
|
|
128
|
+
|
|
129
|
+
struct Params {
|
|
130
|
+
// ================================================================
|
|
131
|
+
// 第一组:本届比赛的规则
|
|
132
|
+
// ================================================================
|
|
133
|
+
|
|
134
|
+
// ---- 地图 ----
|
|
135
|
+
int size = 15; // 地图边长
|
|
136
|
+
int ring = 1; // 基地外面围几层砖墙
|
|
137
|
+
bool spawn_near = true; // 三辆车都在围墙外一格出生:围墙前面那一行的左前、正前、右前
|
|
138
|
+
bool spawn_flip = true; // 两边的车型排布左右对调(地形上下镜像;比如 1 号方的快速型在左,0 号方的就在右)
|
|
139
|
+
int supply_points = 3; // 补给点数量,都在中线上
|
|
140
|
+
|
|
141
|
+
// ---- 基地 ----
|
|
142
|
+
int base_hp = 24; // 基地血量
|
|
143
|
+
int base_hit_cap = 1; // 基地每回合最多扣 1 血:同一回合里不管几发子弹命中(两个小步合计,
|
|
144
|
+
// 敌我的子弹都算),只有先命中的那一发扣血,其余的照样消失但不扣血
|
|
145
|
+
|
|
146
|
+
// ---- 车型:顺序固定为 快速、火力、装甲 ----
|
|
147
|
+
TankType types[3] = {
|
|
148
|
+
{1, 2, 1, 1}, // 快速型:1 血,每回合 2 步,子弹上限 1,开炮后装填 1 回合
|
|
149
|
+
{1, 1, 1, 0}, // 火力型:1 血,每回合 1 步,子弹上限 1,不用装填(0 级时唯一能连射的车)
|
|
150
|
+
{2, 1, 1, 1}, // 装甲型:2 血,每回合 1 步,子弹上限 1,开炮后装填 1 回合
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// ---- 升级(吃一颗星升一级;被毁后等级清零,升级得到的东西全部失去)----
|
|
154
|
+
// 每升一级 到 1 级 到 2 级
|
|
155
|
+
// 快速型 当前血量 +2 不再需要装填(子弹上限仍是 1) 子弹能打掉钢墙
|
|
156
|
+
// 火力型 当前血量 +2 子弹上限 +1(变成 2;它本来就不装填) 子弹能打掉钢墙
|
|
157
|
+
// 装甲型 当前血量 +2 不再需要装填(子弹上限仍是 1) 子弹能打掉钢墙
|
|
158
|
+
// 1 级的效果到 2 级仍然保留。满级的坦克不再拾取星星。
|
|
159
|
+
int max_level = 2; // 最高等级
|
|
160
|
+
int level_hp = 2; // 每升一级,当前血量加多少
|
|
161
|
+
bool level1_no_reload = true; // 1 级及以上:要装填的车型不再需要装填;本来就不装填的车型改为子弹上限 + level1_bullets
|
|
162
|
+
int level1_bullets = 1; // 见上一行
|
|
163
|
+
bool level2_steel = true; // 2 级及以上:打出的子弹能打掉钢墙(按开炮时的等级定)
|
|
164
|
+
|
|
165
|
+
// ---- 星星 ----
|
|
166
|
+
int supply_first = 10; // 第一批星星在第 10 回合末出现
|
|
167
|
+
int supply_period = 20; // 之后每 20 回合一批(第 30、50、70… 回合末);已经有星星的补给点不再出
|
|
168
|
+
|
|
169
|
+
// ---- 复活 ----
|
|
170
|
+
int respawn = 3; // 第 T 回合末被毁,第 T+3 回合末在自己的出生点复活
|
|
171
|
+
bool respawn_telefrag = true; // 复活时出生点被敌方坦克占着:那辆敌车被毁(算一次击毁);被己方占着则顺延
|
|
172
|
+
bool respawn_clear_bullets = true; // 复活时出生点上的子弹直接消失
|
|
173
|
+
|
|
174
|
+
// ---- 子弹与胜负 ----
|
|
175
|
+
bool bullet_cancel = true; // 两发子弹对穿或同格时互相抵消
|
|
176
|
+
int max_turns = 300; // 回合上限
|
|
177
|
+
// 到回合上限、或两个基地同回合归零时,依次比 tiebreak[0..n_tiebreak):基地血量 → 击毁数 → 吃星数,都相同算平局
|
|
178
|
+
int n_tiebreak = 3;
|
|
179
|
+
int tiebreak[4] = {TB_BASE, TB_KILLS, TB_STARS}; // 没用到的项保持 0
|
|
180
|
+
|
|
181
|
+
// ================================================================
|
|
182
|
+
// 第二组:本届比赛中固定为默认值,仅用于测试,写 bot 时不用关心
|
|
183
|
+
// ================================================================
|
|
184
|
+
int start_level[2] = {0, 0}; // 两方开局等级
|
|
185
|
+
int base_armor = 0; // 开炮时等级低于这个数的子弹打不动基地;0 = 谁都能打
|
|
186
|
+
int base_regen = 0; // 基地连续这么多回合没挨打就回 1 血;0 = 不回血
|
|
187
|
+
// 下面三个是旧版规则的兼容开关,本届比赛中恒为 false:
|
|
188
|
+
bool proto_base_dmg_by_bullet_order = false; // true(旧):同一小步里敌我子弹同时命中一个基地而只有一发扣血时,
|
|
189
|
+
// 伤害记给谁取决于子弹的排列顺序(对两个座位不对称)
|
|
190
|
+
// false(正式规则):敌方的子弹优先(见 step 的判定 4)
|
|
191
|
+
bool proto_supply_feeds_tank = false; // true(旧):出星时补给点上有坦克,星星不出现,坦克直接吃掉
|
|
192
|
+
// false(正式规则):星星照常出现,下一回合末的拾取步骤才被吃
|
|
193
|
+
bool proto_pickup_at_max_level = false; // true(旧):满级的坦克也拾取,星星消失、计入吃星数、没有别的效果
|
|
194
|
+
// false(正式规则):满级的坦克不拾取,星星留在原地,不计入吃星数
|
|
195
|
+
|
|
196
|
+
bool operator==(const Params&) const = default;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// 逐项列出 Params 的名字和成员,文本读写都走这里。新增参数时在结构体里加成员,再在这里加一行。
|
|
200
|
+
// (tiebreak 是列表,文本读写单独处理。)
|
|
201
|
+
template <class P, class F>
|
|
202
|
+
void for_each_param(P& p, F&& f) {
|
|
203
|
+
f("size", p.size);
|
|
204
|
+
f("max_turns", p.max_turns);
|
|
205
|
+
f("base_hp", p.base_hp);
|
|
206
|
+
f("respawn", p.respawn);
|
|
207
|
+
f("supply_first", p.supply_first);
|
|
208
|
+
f("supply_period", p.supply_period);
|
|
209
|
+
f("supply_points", p.supply_points);
|
|
210
|
+
f("bullet_cancel", p.bullet_cancel);
|
|
211
|
+
static const char* const type_keys[3][4] = {
|
|
212
|
+
{"fast_hp", "fast_speed", "fast_bullets", "fast_reload"},
|
|
213
|
+
{"fire_hp", "fire_speed", "fire_bullets", "fire_reload"},
|
|
214
|
+
{"armor_hp", "armor_speed", "armor_bullets", "armor_reload"},
|
|
215
|
+
};
|
|
216
|
+
for (int k = 0; k < 3; ++k) {
|
|
217
|
+
f(type_keys[k][0], p.types[k].hp);
|
|
218
|
+
f(type_keys[k][1], p.types[k].speed);
|
|
219
|
+
f(type_keys[k][2], p.types[k].bullets);
|
|
220
|
+
f(type_keys[k][3], p.types[k].reload);
|
|
221
|
+
}
|
|
222
|
+
f("max_level", p.max_level);
|
|
223
|
+
f("level_hp", p.level_hp);
|
|
224
|
+
f("level1_bullets", p.level1_bullets);
|
|
225
|
+
f("level2_steel", p.level2_steel);
|
|
226
|
+
f("respawn_telefrag", p.respawn_telefrag);
|
|
227
|
+
f("respawn_clear_bullets", p.respawn_clear_bullets);
|
|
228
|
+
f("ring", p.ring);
|
|
229
|
+
f("spawn_near", p.spawn_near);
|
|
230
|
+
f("spawn_flip", p.spawn_flip);
|
|
231
|
+
f("level1_no_reload", p.level1_no_reload);
|
|
232
|
+
f("start_level0", p.start_level[0]);
|
|
233
|
+
f("start_level1", p.start_level[1]);
|
|
234
|
+
f("base_hit_cap", p.base_hit_cap);
|
|
235
|
+
f("base_armor", p.base_armor);
|
|
236
|
+
f("base_regen", p.base_regen);
|
|
237
|
+
f("proto_base_dmg_by_bullet_order", p.proto_base_dmg_by_bullet_order); // 兼容开关,恒为 false
|
|
238
|
+
f("proto_supply_feeds_tank", p.proto_supply_feeds_tank); // 兼容开关,恒为 false
|
|
239
|
+
f("proto_pickup_at_max_level", p.proto_pickup_at_max_level); // 兼容开关,恒为 false
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 参数是否在引擎支持的范围内。通过返回空串,否则返回原因。
|
|
243
|
+
inline std::string check_params(const Params& p) {
|
|
244
|
+
if (p.size < 5 || p.size > MAX_SIZE) return "size 应在 5.." + std::to_string(MAX_SIZE);
|
|
245
|
+
if (p.max_turns < 1 || p.max_turns > 30000) return "max_turns 应在 1..30000";
|
|
246
|
+
if (p.base_hp < 1 || p.base_hp > 30000) return "base_hp 应在 1..30000";
|
|
247
|
+
if (p.respawn < 0 || p.respawn > 30000) return "respawn 应在 0..30000";
|
|
248
|
+
if (p.supply_first < 0 || p.supply_first > 30000) return "supply_first 应在 0..30000";
|
|
249
|
+
if (p.supply_period < 1) return "supply_period 至少为 1";
|
|
250
|
+
if (p.supply_points < 0 || p.supply_points > MAX_SUPPLY) return "supply_points 应在 0..5";
|
|
251
|
+
for (const TankType& t : p.types) {
|
|
252
|
+
if (t.hp < 1 || t.hp > 100) return "车型血量应在 1..100";
|
|
253
|
+
if (t.speed < 1 || t.speed > 2) return "车型速度只能是 1 或 2";
|
|
254
|
+
if (t.bullets < 0 || t.reload < 0 || t.reload > 100) return "车型的子弹上限和装填回合数不能为负";
|
|
255
|
+
if (t.bullets + (p.level1_bullets > 0 ? p.level1_bullets : 0) > MAX_BULLETS / 6)
|
|
256
|
+
return "每辆车的场上子弹上限(含升级)不能超过 " + std::to_string(MAX_BULLETS / 6);
|
|
257
|
+
}
|
|
258
|
+
if (p.max_level < 0 || p.max_level > 100) return "max_level 应在 0..100";
|
|
259
|
+
if (p.level_hp < 0 || p.level_hp > 100 || p.level1_bullets < 0) return "level_hp / level1_bullets 不能为负";
|
|
260
|
+
if (p.n_tiebreak < 0 || p.n_tiebreak > 4) return "n_tiebreak 应在 0..4";
|
|
261
|
+
for (int k = 0; k < p.n_tiebreak; ++k)
|
|
262
|
+
if (p.tiebreak[k] < 0 || p.tiebreak[k] > 3) return "tiebreak 项目未知";
|
|
263
|
+
if (p.ring < 1 || p.ring > 2) return "ring 只能是 1 或 2";
|
|
264
|
+
for (int lv : p.start_level)
|
|
265
|
+
if (lv < 0 || lv > 100) return "start_level 应在 0..100";
|
|
266
|
+
if (p.base_hit_cap < 0 || p.base_armor < 0 || p.base_regen < 0) return "base_hit_cap / base_armor / base_regen 不能为负";
|
|
267
|
+
return "";
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ====================================================================
|
|
271
|
+
// 3. State:完整局面
|
|
272
|
+
// ====================================================================
|
|
273
|
+
|
|
274
|
+
inline constexpr int NEVER_FIRED = -99; // last_fire 的初值:还没开过炮(或刚复活)
|
|
275
|
+
|
|
276
|
+
struct Tank {
|
|
277
|
+
int16_t pos = NO_CELL; // 所在格子;不在场时为 NO_CELL
|
|
278
|
+
int16_t hp = 0; // 当前血量;不在场时为 0
|
|
279
|
+
int16_t level = 0; // 等级;被毁后清零
|
|
280
|
+
bool alive = false; // 是否在场
|
|
281
|
+
int16_t respawn_turn = 0; // 不在场时:从这个回合的回合末起可以复活(出生点被己方占着会顺延);在场时为 0
|
|
282
|
+
int16_t last_fire = NEVER_FIRED; // 上一次成功开炮的回合;不在场或复活后为 NEVER_FIRED
|
|
283
|
+
bool operator==(const Tank&) const = default;
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
struct Bullet {
|
|
287
|
+
int16_t pos; // 所在格子
|
|
288
|
+
int8_t dir; // 飞行方向
|
|
289
|
+
int8_t owner; // 打出它的坦克编号
|
|
290
|
+
bool steel; // 能否打掉钢墙(按开炮时的等级定)
|
|
291
|
+
int8_t level; // 开炮时坦克的等级(只有第二组参数 base_armor 用到)
|
|
292
|
+
bool operator==(const Bullet&) const = default;
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// 结束原因。两个基地同回合归零时 reason 是决出胜负的那一项(或 R_DRAW),
|
|
296
|
+
// 是否"同归于尽"看 base_hp 是否都为 0。
|
|
297
|
+
enum Reason : uint8_t { R_NONE = 0, R_BASE = 1, R_HP = 2, R_KILLS = 3, R_STARS = 4, R_DAMAGE = 5, R_DRAW = 6 };
|
|
298
|
+
|
|
299
|
+
struct State {
|
|
300
|
+
Params P;
|
|
301
|
+
|
|
302
|
+
// ---- 布局里不变的部分 ----
|
|
303
|
+
int16_t base_pos[2] = {NO_CELL, NO_CELL}; // 两方基地所在格
|
|
304
|
+
int16_t spawn[6] = {}; // 每辆坦克的出生点(按坦克编号)
|
|
305
|
+
int16_t n_supply = 0; // 补给点数量
|
|
306
|
+
int16_t supply[MAX_SUPPLY] = {}; // 补给点所在格
|
|
307
|
+
|
|
308
|
+
// ---- 随对局变化的部分 ----
|
|
309
|
+
int16_t turn = 0; // 已经结算完的回合数
|
|
310
|
+
bool done = false; // 是否已结束
|
|
311
|
+
int8_t winner = -1; // 胜方座位;-1 = 未结束或平局
|
|
312
|
+
Reason reason = R_NONE; // 结束原因
|
|
313
|
+
int16_t base_hp[2] = {0, 0}; // 基地血量
|
|
314
|
+
int16_t base_quiet[2] = {0, 0}; // 基地已连续多少回合没挨打(只有第二组参数 base_regen 用到)
|
|
315
|
+
int16_t kills[2] = {0, 0}; // 各方的击毁数:对方坦克被毁一次算一次,不论被谁打的
|
|
316
|
+
int16_t stars_eaten[2] = {0, 0}; // 各方吃到的星星数(满级的坦克不拾取,不计数)
|
|
317
|
+
int16_t base_dmg[2] = {0, 0}; // 各方对敌方基地造成的伤害(打自己基地不算)
|
|
318
|
+
Tank tank[6];
|
|
319
|
+
int16_t n_bullets = 0;
|
|
320
|
+
Bullet bullet[MAX_BULLETS] = {}; // 前 n_bullets 项有效,按开炮先后排列;后面的项恒为全 0
|
|
321
|
+
uint8_t star_mask = 0; // 第 k 位为 1:supply[k] 上有一颗星星
|
|
322
|
+
uint64_t wall_hash = 0; // 当前砖墙和钢墙的指纹;墙没变它就不变,可以拿来缓存寻路结果
|
|
323
|
+
uint8_t grid[CELLS] = {}; // 当前地形(Cell),用 cell_of(x, y) 取下标
|
|
324
|
+
|
|
325
|
+
bool operator==(const State&) const = default;
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
// 一堵墙对 wall_hash 的贡献。wall_hash = 所有现存砖墙、钢墙格子的 wall_key 异或起来。
|
|
329
|
+
constexpr uint64_t wall_key(int cell) {
|
|
330
|
+
uint64_t z = (uint64_t(cell) + 1) * 0x9E3779B97F4A7C15ULL;
|
|
331
|
+
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
|
|
332
|
+
z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
|
|
333
|
+
return z ^ (z >> 31);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// ====================================================================
|
|
337
|
+
// 4. 查询
|
|
338
|
+
// ====================================================================
|
|
339
|
+
//
|
|
340
|
+
// ★★ 注意:s.grid 里只有地形(空地、砖墙、钢墙、水、基地),没有坦克、子弹和星星。 ★★
|
|
341
|
+
// 这一节的函数各管一件事,没有哪个替你回答"这一炮打出去会打到什么"。自己沿一个方向逐格看 s.grid
|
|
342
|
+
// 来判断炮线通不通时(模板 bot.cpp 里的 base_in_line 就是这种只看地形的写法),路上的坦克是看不到的:
|
|
343
|
+
// 子弹会命中途中遇到的第一辆坦克然后消失(不分敌我,队友也算),到不了它后面的墙和基地。
|
|
344
|
+
// 要把坦克算进去,每一格另外查 tank_at(s, c)。地形方面:砖墙、钢墙挡子弹(砖墙被打掉,
|
|
345
|
+
// 钢墙只有 2 级的子弹打得掉),水不挡子弹。
|
|
346
|
+
// 同类的"只看一部分":
|
|
347
|
+
// can_fire / reload_left 只回答炮打不打得出去,不管打到什么;reload_left 只算装填,不算子弹上限。
|
|
348
|
+
// legal_commands 只看地形和这辆车自己的装填、子弹数,不看任何坦克(见第 6 节)。
|
|
349
|
+
// base_open_sides 只看开局的地形(Layout),不看坦克,也不随墙被打掉而变(见第 7 节)。
|
|
350
|
+
// star_at 只在补给点上才可能为真;满级的车站在上面也不会吃掉它。
|
|
351
|
+
|
|
352
|
+
// 这一格上在场的坦克编号,没有则 -1。本回合内血量已被打到 0、但回合末才移除的坦克也算在场。
|
|
353
|
+
inline int tank_at(const State& s, int cell) {
|
|
354
|
+
for (int i = 0; i < 6; ++i)
|
|
355
|
+
if (s.tank[i].alive && s.tank[i].pos == cell) return i;
|
|
356
|
+
return -1;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// 这一格是不是一个现在有星星的补给点。不是补给点的格子一律为 false。
|
|
360
|
+
inline bool star_at(const State& s, int cell) {
|
|
361
|
+
for (int k = 0; k < s.n_supply; ++k)
|
|
362
|
+
if (s.supply[k] == cell) return (s.star_mask >> k) & 1;
|
|
363
|
+
return false;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 第 i 辆坦克留在场上的子弹数。
|
|
367
|
+
inline int bullets_out(const State& s, int i) {
|
|
368
|
+
int n = 0;
|
|
369
|
+
for (int k = 0; k < s.n_bullets; ++k) n += (s.bullet[k].owner == i);
|
|
370
|
+
return n;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// 第 i 辆坦克当前的场上子弹上限。0 级是车型的 bullets;1 级及以上,本来就不装填的车型(火力型)
|
|
374
|
+
// 上限 + level1_bullets,要装填的车型(快速、装甲)上限不变(它们 1 级的好处是不再装填)。
|
|
375
|
+
inline int bullet_cap(const State& s, int i) {
|
|
376
|
+
const TankType& t = s.P.types[type_of(i)];
|
|
377
|
+
if (s.tank[i].level >= 1 && (!s.P.level1_no_reload || t.reload == 0)) return t.bullets + s.P.level1_bullets;
|
|
378
|
+
return t.bullets;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 第 i 辆坦克开炮后要装填几回合(按它现在的等级):0 级是车型的 reload,1 级及以上是 0。
|
|
382
|
+
inline int reload_turns(const State& s, int i) {
|
|
383
|
+
int rl = s.P.types[type_of(i)].reload;
|
|
384
|
+
if (rl && s.tank[i].level >= 1 && s.P.level1_no_reload) rl = 0;
|
|
385
|
+
return rl;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// 第 i 辆坦克的装填还要等几个回合(0 = 装填完毕)。
|
|
389
|
+
// 注意:只算装填。装填完了但上一发子弹还在场上、已达子弹上限时照样打不出去——"下一回合能不能开炮"请用 can_fire。
|
|
390
|
+
inline int reload_left(const State& s, int i) {
|
|
391
|
+
int left = reload_turns(s, i) - (s.turn - s.tank[i].last_fire);
|
|
392
|
+
return left > 0 ? left : 0;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// 第 i 辆坦克下一回合下开炮命令,是否真的会打出子弹(在场、装填完毕、未达子弹上限)。
|
|
396
|
+
// 注意:只回答"打不打得出去",和朝哪个方向、路上有什么无关。子弹数按此刻数:这一回合稍后才消失的子弹仍占名额。
|
|
397
|
+
inline bool can_fire(const State& s, int i) {
|
|
398
|
+
return s.tank[i].alive && reload_left(s, i) == 0 && bullets_out(s, i) < bullet_cap(s, i) &&
|
|
399
|
+
s.n_bullets < MAX_BULLETS;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// 不在场的坦克最早还要等几个回合末才复活(至少为 1;出生点被己方占着会一直停在 1)。在场时为 0。
|
|
403
|
+
inline int respawn_in(const State& s, int i) {
|
|
404
|
+
if (s.tank[i].alive) return 0;
|
|
405
|
+
int w = s.tank[i].respawn_turn - s.turn;
|
|
406
|
+
return w > 1 ? w : 1;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
inline bool is_over(const State& s) { return s.done; }
|
|
410
|
+
inline int winner(const State& s) { return s.winner; } // 0 / 1;-1 = 未结束或平局
|
|
411
|
+
inline bool both_bases_down(const State& s) { return s.base_hp[0] <= 0 && s.base_hp[1] <= 0; }
|
|
412
|
+
|
|
413
|
+
// 各方的得分:胜 1、负 0、平 0.5(未结束时也是 0.5)。
|
|
414
|
+
inline double score(const State& s, int seat) { return s.winner < 0 ? 0.5 : (s.winner == seat ? 1.0 : 0.0); }
|
|
415
|
+
|
|
416
|
+
// ====================================================================
|
|
417
|
+
// 5. step:结算一回合
|
|
418
|
+
// ====================================================================
|
|
419
|
+
|
|
420
|
+
// 事件记录(可选)。给回放和查看器用;搜索时不要传,step 会快一些。
|
|
421
|
+
enum EventType : uint8_t {
|
|
422
|
+
EV_FIRE, // 开炮成功 a=坦克 b=方向
|
|
423
|
+
EV_BLOCKED, // 这一步没走成 a=坦克 sub
|
|
424
|
+
EV_CANCEL_CROSS, // 两发子弹对穿抵消 x y sub a、b=两发的所属坦克
|
|
425
|
+
EV_CANCEL_SAME, // 同格子弹抵消 x y sub
|
|
426
|
+
EV_HIT, // 坦克被打中 a=被打中的坦克 b=开炮的坦克 x y sub
|
|
427
|
+
EV_HIT_SWAP, // 同上,是和子弹互换位置判的命中
|
|
428
|
+
EV_BASE_HIT, // 基地被打中 a=基地所属座位 b=开炮的坦克 sub
|
|
429
|
+
EV_WALL_BRICK, // 砖墙被打掉 x y sub
|
|
430
|
+
EV_WALL_STEEL, // 钢墙被打掉 x y sub
|
|
431
|
+
EV_DEATH, // 回合末被毁 a=坦克 x y b=压出生点被谁的复活炸毁(否则 -1)
|
|
432
|
+
EV_RESPAWN, // 回合末复活 a=坦克 x y
|
|
433
|
+
EV_BULLET_CLEARED, // 复活时出生点上的子弹消失 x y a=复活的坦克
|
|
434
|
+
EV_STAR, // 吃到星星 a=坦克 x y b=吃完后的等级
|
|
435
|
+
EV_SUPPLY, // 补给点出了一颗星星 x y
|
|
436
|
+
EV_BASE_REGEN, // 基地回 1 血(第二组参数 base_regen) a=座位
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
struct Event {
|
|
440
|
+
EventType type;
|
|
441
|
+
int8_t sub; // 第几小步(1 或 2);回合初、回合末的事件为 0
|
|
442
|
+
int8_t a, b;
|
|
443
|
+
int8_t x, y;
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
struct EventLog {
|
|
447
|
+
std::vector<Event> list; // 本回合发生的事,按发生顺序;每次 step 开头清空
|
|
448
|
+
};
|
|
449
|
+
|
|
450
|
+
namespace detail {
|
|
451
|
+
inline void add_event(EventLog* log, EventType type, int sub, int a, int b, int cell) {
|
|
452
|
+
if (log) log->list.push_back(Event{type, int8_t(sub), int8_t(a), int8_t(b), int8_t(cell_x(cell)), int8_t(cell_y(cell))});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 吃到一颗星星:升一级并加 level_hp 点血,吃星数 +1。
|
|
456
|
+
// (只有打开兼容开关时满级的坦克才会走到这里,那时不升级、不加血,只记吃星数。)
|
|
457
|
+
inline void level_up(State& s, int i, EventLog* log) {
|
|
458
|
+
Tank& t = s.tank[i];
|
|
459
|
+
if (t.level < s.P.max_level) {
|
|
460
|
+
t.level += 1;
|
|
461
|
+
t.hp += int16_t(s.P.level_hp);
|
|
462
|
+
}
|
|
463
|
+
s.stars_eaten[seat_of(i)] += 1;
|
|
464
|
+
add_event(log, EV_STAR, 0, i, t.level, t.pos);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// 坦克被毁:从场上移除,等级清零,定下复活回合。
|
|
468
|
+
inline void destroy_tank(State& s, int i) {
|
|
469
|
+
Tank& t = s.tank[i];
|
|
470
|
+
t.alive = false;
|
|
471
|
+
t.pos = NO_CELL;
|
|
472
|
+
t.hp = 0;
|
|
473
|
+
t.level = 0;
|
|
474
|
+
t.last_fire = NEVER_FIRED;
|
|
475
|
+
t.respawn_turn = int16_t(s.turn + s.P.respawn);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
inline void tiebreak_values(const State& s, int key, int& a, int& b) {
|
|
479
|
+
switch (key) {
|
|
480
|
+
case TB_BASE: a = s.base_hp[0]; b = s.base_hp[1]; break;
|
|
481
|
+
case TB_KILLS: a = s.kills[0]; b = s.kills[1]; break;
|
|
482
|
+
case TB_STARS: a = s.stars_eaten[0]; b = s.stars_eaten[1]; break;
|
|
483
|
+
default: a = s.base_dmg[0]; b = s.base_dmg[1]; break;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
} // namespace detail
|
|
487
|
+
|
|
488
|
+
// 结算一回合。seat0 / seat1 分别是 0 号方、1 号方的命令。已结束的局面不再变化。
|
|
489
|
+
//
|
|
490
|
+
// 一回合的顺序:
|
|
491
|
+
// 开炮 → 小步 1(移动、子弹前进、判定)→ 小步 2(同上)→ 回合末(被毁、复活、拾取、补给)→ 胜负
|
|
492
|
+
inline void step(State& s, const Orders& seat0, const Orders& seat1, EventLog* log = nullptr) {
|
|
493
|
+
if (log) log->list.clear();
|
|
494
|
+
if (s.done) return;
|
|
495
|
+
const Params& P = s.P;
|
|
496
|
+
const int turn = ++s.turn;
|
|
497
|
+
using detail::add_event;
|
|
498
|
+
|
|
499
|
+
int move1[6], move2[6]; // 每辆车第 1、2 小步要走的方向;-1 = 这一小步不走
|
|
500
|
+
int base_hits_now[2] = {0, 0}; // 本回合每个基地已经挨了几发(两个小步合计;基地每回合限伤用)
|
|
501
|
+
|
|
502
|
+
// ---------------- 开炮(回合开始时)----------------
|
|
503
|
+
// 子弹生成在开炮坦克自己所在的格子,带方向和所属;本回合的两个小步里它照常前进。
|
|
504
|
+
// 子弹上限按此刻(回合开始、任何子弹前进之前)场上的子弹数算;装填中或已达上限时开炮命令视为不动,
|
|
505
|
+
// 也不重新计装填。开炮的回合不移动。
|
|
506
|
+
for (int i = 0; i < 6; ++i) {
|
|
507
|
+
move1[i] = move2[i] = -1;
|
|
508
|
+
Tank& t = s.tank[i];
|
|
509
|
+
if (!t.alive) continue; // 不在场的坦克的命令被忽略
|
|
510
|
+
const int c = (i < 3 ? seat0 : seat1).cmd[i % 3];
|
|
511
|
+
if (c >= 1 && c <= 4) {
|
|
512
|
+
move1[i] = c - 1;
|
|
513
|
+
} else if (c >= 5 && c <= 8) {
|
|
514
|
+
if (bullets_out(s, i) < bullet_cap(s, i) && turn - t.last_fire > reload_turns(s, i) &&
|
|
515
|
+
s.n_bullets < MAX_BULLETS) {
|
|
516
|
+
t.last_fire = int16_t(turn);
|
|
517
|
+
s.bullet[s.n_bullets++] = Bullet{t.pos, int8_t(c - 5), int8_t(i),
|
|
518
|
+
t.level >= 2 && P.level2_steel, int8_t(t.level)};
|
|
519
|
+
add_event(log, EV_FIRE, 0, i, c - 5, t.pos);
|
|
520
|
+
}
|
|
521
|
+
} else if (c >= 9 && c <= 24) {
|
|
522
|
+
move1[i] = (c - 9) >> 2;
|
|
523
|
+
if (P.types[i % 3].speed >= 2) move2[i] = (c - 9) & 3; // 非快速型只执行第一步
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// ---------------- 两个小步 ----------------
|
|
528
|
+
for (int sub = 1; sub <= 2; ++sub) {
|
|
529
|
+
const int* mv = sub == 1 ? move1 : move2;
|
|
530
|
+
|
|
531
|
+
// (a) 坦克移动。一步能走成的条件:
|
|
532
|
+
// 目标格是空地(不是砖、钢、水、基地、界外);
|
|
533
|
+
// 目标格在本小步开始时没有坦克(所以面对面互换都不动,也不能跟在正要离开的车后面进它的格子);
|
|
534
|
+
// 没有别的坦克在本小步也要进同一格(两车抢同一格都不动,不分敌我)。
|
|
535
|
+
// 快速型第一步没走成,第二步照常从当时所在的格子独立判定。
|
|
536
|
+
// 本回合已经被打到 0 血的坦克回合末才移除,在那之前照常占格、照常走。
|
|
537
|
+
int want_tank[6], want_cell[6], n_want = 0;
|
|
538
|
+
for (int i = 0; i < 6; ++i) {
|
|
539
|
+
if (mv[i] < 0) continue;
|
|
540
|
+
const int target = s.tank[i].pos + DIR_DELTA[mv[i]];
|
|
541
|
+
if (s.grid[target] == EMPTY && tank_at(s, target) < 0) {
|
|
542
|
+
want_tank[n_want] = i;
|
|
543
|
+
want_cell[n_want++] = target;
|
|
544
|
+
} else {
|
|
545
|
+
add_event(log, EV_BLOCKED, sub, i, -1, s.tank[i].pos);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
int moved_tank[6], moved_from[6], moved_to[6], n_moved = 0;
|
|
549
|
+
for (int a = 0; a < n_want; ++a) {
|
|
550
|
+
bool contested = false;
|
|
551
|
+
for (int b = 0; b < n_want; ++b) contested |= (b != a && want_cell[b] == want_cell[a]);
|
|
552
|
+
if (contested) {
|
|
553
|
+
add_event(log, EV_BLOCKED, sub, want_tank[a], -1, s.tank[want_tank[a]].pos);
|
|
554
|
+
} else {
|
|
555
|
+
moved_tank[n_moved] = want_tank[a];
|
|
556
|
+
moved_from[n_moved] = s.tank[want_tank[a]].pos;
|
|
557
|
+
moved_to[n_moved++] = want_cell[a];
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// 走得成的同时走。它们的目标格在本小步开始时都是空的、彼此不同,所以逐个改位置就等于同时改。
|
|
561
|
+
for (int m = 0; m < n_moved; ++m) s.tank[moved_tank[m]].pos = int16_t(moved_to[m]);
|
|
562
|
+
|
|
563
|
+
const int nb = s.n_bullets;
|
|
564
|
+
if (nb == 0) continue;
|
|
565
|
+
|
|
566
|
+
// (b) 所有子弹前进 1 格。
|
|
567
|
+
for (int k = 0; k < nb; ++k) s.bullet[k].pos += int16_t(DIR_DELTA[s.bullet[k].dir]);
|
|
568
|
+
|
|
569
|
+
// (c) 判定。gone[k] = 第 k 发子弹在本小步消失。
|
|
570
|
+
bool gone[MAX_BULLETS] = {};
|
|
571
|
+
|
|
572
|
+
// 判定 1(对穿,子弹对子弹):两发子弹在本小步互换了位置,都消失。关掉 bullet_cancel 时不做。
|
|
573
|
+
if (P.bullet_cancel && nb > 1) {
|
|
574
|
+
for (int a = 0; a < nb; ++a) {
|
|
575
|
+
const Bullet& ba = s.bullet[a];
|
|
576
|
+
for (int b = a + 1; b < nb; ++b) {
|
|
577
|
+
const Bullet& bb = s.bullet[b];
|
|
578
|
+
if (bb.dir == (ba.dir ^ 1) && ba.pos - DIR_DELTA[ba.dir] == bb.pos) {
|
|
579
|
+
gone[a] = gone[b] = true;
|
|
580
|
+
add_event(log, EV_CANCEL_CROSS, sub, ba.owner, bb.owner, ba.pos);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
// 判定 1(对穿,子弹对坦克):一发子弹和一辆坦克在本小步互换了位置,算命中这辆坦克。
|
|
586
|
+
// 已经在子弹对穿里抵消的子弹不再参与。这一条不受 bullet_cancel 影响。
|
|
587
|
+
for (int m = 0; m < n_moved; ++m) {
|
|
588
|
+
Tank& t = s.tank[moved_tank[m]];
|
|
589
|
+
for (int k = 0; k < nb; ++k) {
|
|
590
|
+
const Bullet& b = s.bullet[k];
|
|
591
|
+
if (!gone[k] && b.pos == moved_from[m] && b.pos - DIR_DELTA[b.dir] == t.pos) {
|
|
592
|
+
gone[k] = true;
|
|
593
|
+
t.hp -= 1;
|
|
594
|
+
add_event(log, EV_HIT_SWAP, sub, moved_tank[m], b.owner, t.pos);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
// 判定 2–5:按子弹所在格的内容处理。墙体的变化攒到这一轮结束再生效,
|
|
599
|
+
// 所以同一小步打中同一堵墙的几发子弹都消失。
|
|
600
|
+
int16_t brick_hit[MAX_BULLETS], steel_hit[MAX_BULLETS];
|
|
601
|
+
int n_brick = 0, n_steel = 0;
|
|
602
|
+
int own_took[2] = {0, 0}; // 本小步里每个基地被自己人的子弹扣掉的血(记伤害时用,见判定 4)
|
|
603
|
+
for (int k = 0; k < nb; ++k) {
|
|
604
|
+
if (gone[k]) continue;
|
|
605
|
+
const Bullet& b = s.bullet[k];
|
|
606
|
+
switch (s.grid[b.pos]) {
|
|
607
|
+
case EMPTY: {
|
|
608
|
+
// 判定 5:格里有坦克,坦克扣 1 血,子弹消失;同一格里有几发算几发。不分敌我。
|
|
609
|
+
const int v = tank_at(s, b.pos);
|
|
610
|
+
if (v >= 0) {
|
|
611
|
+
gone[k] = true;
|
|
612
|
+
s.tank[v].hp -= 1;
|
|
613
|
+
add_event(log, EV_HIT, sub, v, b.owner, b.pos);
|
|
614
|
+
}
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
case WATER: // 水不挡子弹
|
|
618
|
+
break;
|
|
619
|
+
case BRICK: // 判定 3:砖墙和子弹都消失
|
|
620
|
+
gone[k] = true;
|
|
621
|
+
brick_hit[n_brick++] = b.pos;
|
|
622
|
+
break;
|
|
623
|
+
case STEEL: // 判定 3:子弹消失;能破钢的子弹把钢墙也打掉
|
|
624
|
+
gone[k] = true;
|
|
625
|
+
if (b.steel) steel_hit[n_steel++] = b.pos;
|
|
626
|
+
break;
|
|
627
|
+
case OUTSIDE: // 判定 2:出界的子弹消失
|
|
628
|
+
gone[k] = true;
|
|
629
|
+
break;
|
|
630
|
+
default: { // 判定 4:子弹消失,基地扣 1 血。打自己的基地也扣血。
|
|
631
|
+
// 限伤:每个基地每回合最多扣 base_hit_cap 血。本回合命中这个基地的子弹(两个小步合计,
|
|
632
|
+
// 不分敌我)合在一起数,超出的照样消失但不扣血。基地扣多少血与子弹的先后无关。
|
|
633
|
+
// "对敌方基地的伤害"(base_dmg)记给谁:先到的小步优先;同一小步里敌方的子弹优先于
|
|
634
|
+
// 基地自己人的子弹(自己人打中自己的基地不记伤害)。
|
|
635
|
+
const int seat = s.grid[b.pos] - BASE0;
|
|
636
|
+
const bool enemy = seat_of(b.owner) != seat;
|
|
637
|
+
gone[k] = true;
|
|
638
|
+
base_hits_now[seat] += 1;
|
|
639
|
+
if (P.base_armor && b.level < P.base_armor) {
|
|
640
|
+
base_hits_now[seat] -= 1; // 第二组参数 base_armor:打不动,不算挨打
|
|
641
|
+
} else if (s.base_hp[seat] > 0 && !(P.base_hit_cap && base_hits_now[seat] > P.base_hit_cap)) {
|
|
642
|
+
s.base_hp[seat] -= 1;
|
|
643
|
+
if (enemy) s.base_dmg[1 - seat] += 1;
|
|
644
|
+
else own_took[seat] += 1;
|
|
645
|
+
} else if (enemy && own_took[seat] > 0 && !P.proto_base_dmg_by_bullet_order) {
|
|
646
|
+
// 这一小步里扣掉的那点血先被自己人的子弹"占"了:改记给同一小步命中的这发敌方子弹
|
|
647
|
+
own_took[seat] -= 1;
|
|
648
|
+
s.base_dmg[1 - seat] += 1;
|
|
649
|
+
}
|
|
650
|
+
add_event(log, EV_BASE_HIT, sub, seat, b.owner, b.pos);
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
for (int k = 0; k < n_brick; ++k)
|
|
656
|
+
if (s.grid[brick_hit[k]] == BRICK) {
|
|
657
|
+
s.grid[brick_hit[k]] = EMPTY;
|
|
658
|
+
s.wall_hash ^= wall_key(brick_hit[k]);
|
|
659
|
+
add_event(log, EV_WALL_BRICK, sub, -1, -1, brick_hit[k]);
|
|
660
|
+
}
|
|
661
|
+
for (int k = 0; k < n_steel; ++k)
|
|
662
|
+
if (s.grid[steel_hit[k]] == STEEL) {
|
|
663
|
+
s.grid[steel_hit[k]] = EMPTY;
|
|
664
|
+
s.wall_hash ^= wall_key(steel_hit[k]);
|
|
665
|
+
add_event(log, EV_WALL_STEEL, sub, -1, -1, steel_hit[k]);
|
|
666
|
+
}
|
|
667
|
+
// 判定 6:剩下的子弹里,同一格有两发以上的都消失(不要求方向相对,不分敌我,水面上也一样)。
|
|
668
|
+
// 关掉 bullet_cancel 时不做。
|
|
669
|
+
if (P.bullet_cancel) {
|
|
670
|
+
bool left[MAX_BULLETS];
|
|
671
|
+
for (int k = 0; k < nb; ++k) left[k] = !gone[k];
|
|
672
|
+
for (int k = 1; k < nb; ++k) {
|
|
673
|
+
if (!left[k]) continue;
|
|
674
|
+
for (int j = 0; j < k; ++j)
|
|
675
|
+
if (left[j] && s.bullet[j].pos == s.bullet[k].pos) {
|
|
676
|
+
gone[k] = gone[j] = true;
|
|
677
|
+
add_event(log, EV_CANCEL_SAME, sub, -1, -1, s.bullet[k].pos);
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
// 去掉消失的子弹,其余保持原来的先后顺序。
|
|
683
|
+
int keep = 0;
|
|
684
|
+
for (int k = 0; k < nb; ++k)
|
|
685
|
+
if (!gone[k]) s.bullet[keep++] = s.bullet[k];
|
|
686
|
+
for (int k = keep; k < nb; ++k) s.bullet[k] = Bullet{};
|
|
687
|
+
s.n_bullets = int16_t(keep);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ---------------- 回合末:被毁 → 复活 → 拾取 → 补给 ----------------
|
|
691
|
+
|
|
692
|
+
// 被毁:血量 ≤ 0 的坦克从场上移除,等级清零,对方击毁数 +1(不论被谁打的),respawn 回合后复活。
|
|
693
|
+
for (int i = 0; i < 6; ++i) {
|
|
694
|
+
Tank& t = s.tank[i];
|
|
695
|
+
if (t.alive && t.hp <= 0) {
|
|
696
|
+
add_event(log, EV_DEATH, 0, i, -1, t.pos);
|
|
697
|
+
detail::destroy_tank(s, i);
|
|
698
|
+
s.kills[1 - seat_of(i)] += 1;
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// 复活:到了回合数的坦克在自己的出生点复活,血量回到车型初始值,不带装填。
|
|
703
|
+
// 出生点被己方坦克占着:顺延到空出来为止。
|
|
704
|
+
// 出生点被敌方坦克占着:那辆敌车被毁(算己方一次击毁),本车照常复活。
|
|
705
|
+
// 出生点上的子弹直接消失。
|
|
706
|
+
// 先按复活前的局面定下谁复活、谁被炸毁,再一起执行,结果与编号顺序无关。
|
|
707
|
+
// 本回合刚被毁的车让出的格子,当回合就能用来复活。
|
|
708
|
+
int plan_tank[6], plan_victim[6], n_plan = 0;
|
|
709
|
+
for (int i = 0; i < 6; ++i) {
|
|
710
|
+
if (s.tank[i].alive || s.tank[i].respawn_turn > turn) continue;
|
|
711
|
+
const int v = tank_at(s, s.spawn[i]);
|
|
712
|
+
if (v >= 0 && (seat_of(v) == seat_of(i) || !P.respawn_telefrag)) continue;
|
|
713
|
+
plan_tank[n_plan] = i;
|
|
714
|
+
plan_victim[n_plan++] = v;
|
|
715
|
+
}
|
|
716
|
+
for (int k = 0; k < n_plan; ++k) {
|
|
717
|
+
const int v = plan_victim[k];
|
|
718
|
+
if (v < 0) continue;
|
|
719
|
+
add_event(log, EV_DEATH, 0, v, plan_tank[k], s.tank[v].pos);
|
|
720
|
+
detail::destroy_tank(s, v);
|
|
721
|
+
s.kills[seat_of(plan_tank[k])] += 1;
|
|
722
|
+
}
|
|
723
|
+
for (int k = 0; k < n_plan; ++k) {
|
|
724
|
+
const int i = plan_tank[k];
|
|
725
|
+
Tank& t = s.tank[i];
|
|
726
|
+
t.alive = true;
|
|
727
|
+
t.pos = s.spawn[i];
|
|
728
|
+
t.hp = int16_t(P.types[i % 3].hp);
|
|
729
|
+
t.level = 0;
|
|
730
|
+
t.last_fire = NEVER_FIRED;
|
|
731
|
+
t.respawn_turn = 0;
|
|
732
|
+
add_event(log, EV_RESPAWN, 0, i, -1, t.pos);
|
|
733
|
+
if (P.respawn_clear_bullets && s.n_bullets > 0) {
|
|
734
|
+
int keep = 0;
|
|
735
|
+
const int nb = s.n_bullets;
|
|
736
|
+
for (int b = 0; b < nb; ++b)
|
|
737
|
+
if (s.bullet[b].pos != t.pos) s.bullet[keep++] = s.bullet[b];
|
|
738
|
+
if (keep != nb) {
|
|
739
|
+
for (int b = keep; b < nb; ++b) s.bullet[b] = Bullet{};
|
|
740
|
+
s.n_bullets = int16_t(keep);
|
|
741
|
+
add_event(log, EV_BULLET_CLEARED, 0, i, -1, t.pos);
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// 拾取:站在有星星的补给点上的坦克吃掉星星,升一级,吃星数 +1。
|
|
747
|
+
// 满级的坦克不拾取:星星留在原地,不计入吃星数(别的车来了照样能吃)。
|
|
748
|
+
// 刚被毁的坦克不会拾取;刚复活的会。
|
|
749
|
+
// (proto_pickup_at_max_level 是旧版规则的兼容开关:满级也拾取,星星消失,只记吃星数。本届比赛中不启用。)
|
|
750
|
+
if (s.star_mask) {
|
|
751
|
+
for (int i = 0; i < 6; ++i) {
|
|
752
|
+
if (!s.tank[i].alive) continue;
|
|
753
|
+
if (s.tank[i].level >= P.max_level && !P.proto_pickup_at_max_level) continue;
|
|
754
|
+
for (int k = 0; k < s.n_supply; ++k)
|
|
755
|
+
if (((s.star_mask >> k) & 1) && s.supply[k] == s.tank[i].pos) {
|
|
756
|
+
s.star_mask &= uint8_t(~(1u << k));
|
|
757
|
+
detail::level_up(s, i, log);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// 补给:第 supply_first 回合末出第一批,之后每 supply_period 回合一批。
|
|
763
|
+
// 每个当前没有星星的补给点出一颗。点上有坦克也照常出现,要到下一回合末的拾取步骤才被吃
|
|
764
|
+
// (那时还站在上面的话)。补给在拾取之后,所以一辆车一回合最多升一级。
|
|
765
|
+
// (proto_supply_feeds_tank 是旧版规则的兼容开关:点上有坦克时星星不出现,坦克直接吃掉。本届比赛中不启用。)
|
|
766
|
+
if (turn >= P.supply_first && (turn - P.supply_first) % P.supply_period == 0) {
|
|
767
|
+
for (int k = 0; k < s.n_supply; ++k) {
|
|
768
|
+
if ((s.star_mask >> k) & 1) continue;
|
|
769
|
+
const int v = P.proto_supply_feeds_tank ? tank_at(s, s.supply[k]) : -1;
|
|
770
|
+
if (v < 0) {
|
|
771
|
+
s.star_mask |= uint8_t(1u << k);
|
|
772
|
+
add_event(log, EV_SUPPLY, 0, -1, -1, s.supply[k]);
|
|
773
|
+
} else {
|
|
774
|
+
detail::level_up(s, v, log);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// 基地回血(第二组参数 base_regen,本届比赛不启用)。
|
|
780
|
+
if (P.base_regen) {
|
|
781
|
+
for (int seat = 0; seat < 2; ++seat) {
|
|
782
|
+
if (base_hits_now[seat] || s.base_hp[seat] <= 0) {
|
|
783
|
+
s.base_quiet[seat] = 0;
|
|
784
|
+
} else if (s.base_hp[seat] < P.base_hp) {
|
|
785
|
+
s.base_quiet[seat] += 1;
|
|
786
|
+
if (s.base_quiet[seat] >= P.base_regen) {
|
|
787
|
+
s.base_quiet[seat] = 0;
|
|
788
|
+
s.base_hp[seat] += 1;
|
|
789
|
+
add_event(log, EV_BASE_REGEN, 0, seat, -1, NO_CELL);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// ---------------- 胜负 ----------------
|
|
796
|
+
// 一方基地归零,另一方获胜。基地在第 1 小步就归零时这一回合照常结算完,
|
|
797
|
+
// 所以可能两个基地同回合归零。两个基地同回合归零,或到回合上限:
|
|
798
|
+
// 依次比 tiebreak 里的项目(默认 基地血量 → 击毁数 → 吃星数),都相同算平局。
|
|
799
|
+
const int h0 = s.base_hp[0], h1 = s.base_hp[1];
|
|
800
|
+
if (h0 <= 0 || h1 <= 0 || turn >= P.max_turns) {
|
|
801
|
+
s.done = true;
|
|
802
|
+
if (h0 <= 0 && h1 > 0) {
|
|
803
|
+
s.winner = 1;
|
|
804
|
+
s.reason = R_BASE;
|
|
805
|
+
} else if (h1 <= 0 && h0 > 0) {
|
|
806
|
+
s.winner = 0;
|
|
807
|
+
s.reason = R_BASE;
|
|
808
|
+
} else {
|
|
809
|
+
s.winner = -1;
|
|
810
|
+
s.reason = R_DRAW;
|
|
811
|
+
for (int k = 0; k < P.n_tiebreak; ++k) {
|
|
812
|
+
int a, b;
|
|
813
|
+
detail::tiebreak_values(s, P.tiebreak[k], a, b);
|
|
814
|
+
if (a != b) {
|
|
815
|
+
s.winner = a > b ? 0 : 1;
|
|
816
|
+
s.reason = Reason(R_HP + P.tiebreak[k]);
|
|
817
|
+
break;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// 从某一方的视角结算:me 的命令和对手的命令。
|
|
825
|
+
inline void step_as(State& s, int me, const Orders& mine, const Orders& theirs, EventLog* log = nullptr) {
|
|
826
|
+
if (me == 0) step(s, mine, theirs, log);
|
|
827
|
+
else step(s, theirs, mine, log);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
// ====================================================================
|
|
831
|
+
// 6. 合法命令枚举
|
|
832
|
+
// ====================================================================
|
|
833
|
+
|
|
834
|
+
// 把一条命令换成"效果必然相同的最简写法"。必然相同指:不论其他五辆车下什么命令,
|
|
835
|
+
// 这一回合结算出的局面都完全一样(只有 EV_BLOCKED 事件可能不同)。规则:
|
|
836
|
+
// 不在场的坦克、编码超出范围 → 不动
|
|
837
|
+
// 走一步,但目标格现在不是空地 → 不动(移动在子弹之前,墙来不及被打掉)
|
|
838
|
+
// 开炮,但装填中或已达子弹上限 → 不动
|
|
839
|
+
// 非快速型的两步 → 只留第一步
|
|
840
|
+
// 快速型两步,第二步无论如何都走不成 → 只留第一步
|
|
841
|
+
// 快速型两步,第一步目标现在不是空地 → "第 1 小步原地等,第 2 小步走 d2",统一写成同一个编码
|
|
842
|
+
// "无论如何都走不成"只认水、基地、界外;砖墙和钢墙可能在第 1 小步被打掉,第 2 小步就能进去。
|
|
843
|
+
inline Cmd canonical_command(const State& s, int i, Cmd c) {
|
|
844
|
+
const Tank& t = s.tank[i];
|
|
845
|
+
if (!t.alive || c == STAY || c >= N_CMDS) return STAY;
|
|
846
|
+
auto open_now = [&](int cell) { return s.grid[cell] == EMPTY; };
|
|
847
|
+
auto never_open = [&](int cell) {
|
|
848
|
+
const uint8_t g = s.grid[cell];
|
|
849
|
+
return g == WATER || g == BASE0 || g == BASE1 || g == OUTSIDE;
|
|
850
|
+
};
|
|
851
|
+
if (is_move(c)) return open_now(t.pos + DIR_DELTA[c - 1]) ? c : STAY;
|
|
852
|
+
if (is_fire(c)) return can_fire(s, i) ? c : STAY;
|
|
853
|
+
const int d1 = (c - 9) >> 2, d2 = (c - 9) & 3;
|
|
854
|
+
if (s.P.types[type_of(i)].speed < 2) return canonical_command(s, i, cmd_move(d1));
|
|
855
|
+
if (!open_now(t.pos + DIR_DELTA[d1])) {
|
|
856
|
+
if (never_open(t.pos + DIR_DELTA[d2])) return STAY;
|
|
857
|
+
for (int e = 0; e < 4; ++e)
|
|
858
|
+
if (!open_now(t.pos + DIR_DELTA[e])) return cmd_move2(e, d2);
|
|
859
|
+
}
|
|
860
|
+
if (never_open(t.pos + DIR_DELTA[d1] + DIR_DELTA[d2]) && never_open(t.pos + DIR_DELTA[d2])) return cmd_move(d1);
|
|
861
|
+
return c;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// 第 i 辆坦克下一回合"有意义"的命令。写进 out(至少 N_CMDS 项),返回条数;按编码从小到大排,第一条总是 STAY。
|
|
865
|
+
//
|
|
866
|
+
// ★★ 它是"去重后的命令表",不是"保证能执行成功的命令表"。用之前记住三件事: ★★
|
|
867
|
+
// 1. 去掉了哪些写法——效果必然与留下的某一条相同的(规则见上面的 canonical_command):
|
|
868
|
+
// 走一步,但目标格现在不是空地 等于不动
|
|
869
|
+
// 开炮,但装填中或子弹已达上限 等于不动
|
|
870
|
+
// 快速型两步,第二步无论如何都走不成 等于只走第一步
|
|
871
|
+
// 快速型两步,第一步的目标格现在不是空地 等于"第 1 小步原地等、第 2 小步走 d2",
|
|
872
|
+
// 同一个 d2 的几种写法只留一种(d1 取编号最小的那个走不成的方向)
|
|
873
|
+
// 不在场的坦克只有 STAY。被去掉的写法照样可以下,不算犯规,只是效果和留下的某一条完全一样。
|
|
874
|
+
// 2. 留下的命令仍可能走不成。这里只看地形和这辆车自己的装填、子弹数,不看任何坦克:
|
|
875
|
+
// 目标格上现在站着坦克(敌我都算)、或者别的坦克在同一小步也要进那一格时,这一步走不成、留在原地,
|
|
876
|
+
// 这取决于其他五辆车这一回合怎么下。开炮命令只保证子弹打得出去,不保证打到东西。
|
|
877
|
+
// 3. 快速型两步命令保留得比较宽:"无论如何都走不成"只认水、基地、界外。第二步朝砖墙或钢墙走的保留
|
|
878
|
+
// (墙可能在第 1 小步被打掉,第 2 小步就能进去);第二步有两个可能的落点(第一步走成后的那一格,
|
|
879
|
+
// 和第一步被别的坦克挡住时从原地走的那一格),两个都是水、基地、界外才去掉。
|
|
880
|
+
// 所以表里会有"按现在的地形第二步走不进去"的两步命令:墙没被打掉时,它们的效果等于只走第一步。
|
|
881
|
+
//
|
|
882
|
+
// 结果恰好是所有满足 canonical_command(s, i, c) == c 的 c(这里直接算,比逐条调用快)。
|
|
883
|
+
inline int legal_commands(const State& s, int i, Cmd* out) {
|
|
884
|
+
int n = 0;
|
|
885
|
+
out[n++] = STAY;
|
|
886
|
+
const Tank& t = s.tank[i];
|
|
887
|
+
if (!t.alive) return n;
|
|
888
|
+
// 四个邻格:现在能不能进(空地)、是不是永远进不了(水、基地、界外)。
|
|
889
|
+
bool open_now[4], never_open[4];
|
|
890
|
+
for (int d = 0; d < 4; ++d) {
|
|
891
|
+
const uint8_t g = s.grid[t.pos + DIR_DELTA[d]];
|
|
892
|
+
open_now[d] = g == EMPTY;
|
|
893
|
+
never_open[d] = g >= WATER;
|
|
894
|
+
}
|
|
895
|
+
for (int d = 0; d < 4; ++d)
|
|
896
|
+
if (open_now[d]) out[n++] = cmd_move(d);
|
|
897
|
+
if (can_fire(s, i))
|
|
898
|
+
for (int d = 0; d < 4; ++d) out[n++] = cmd_fire(d);
|
|
899
|
+
if (s.P.types[type_of(i)].speed >= 2) {
|
|
900
|
+
int first_blocked = -1; // 编号最小的"第一步必然走不成"的方向
|
|
901
|
+
for (int d = 3; d >= 0; --d)
|
|
902
|
+
if (!open_now[d]) first_blocked = d;
|
|
903
|
+
for (int d1 = 0; d1 < 4; ++d1)
|
|
904
|
+
for (int d2 = 0; d2 < 4; ++d2) {
|
|
905
|
+
bool keep;
|
|
906
|
+
if (!open_now[d1]) {
|
|
907
|
+
keep = d1 == first_blocked && !never_open[d2]; // "原地等一小步再走 d2",只留一种写法
|
|
908
|
+
} else {
|
|
909
|
+
const uint8_t g = s.grid[t.pos + DIR_DELTA[d1] + DIR_DELTA[d2]];
|
|
910
|
+
keep = !(g >= WATER && never_open[d2]); // 第二步无论如何走不成的,等于只走一步
|
|
911
|
+
}
|
|
912
|
+
if (keep) out[n++] = cmd_move2(d1, d2);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
return n;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// 协议允许第 i 辆坦克写的全部命令(不看局面):快速型 25 条,其余 9 条。
|
|
919
|
+
inline int all_commands(const State& s, int i, Cmd* out) {
|
|
920
|
+
const int n = s.P.types[type_of(i)].speed >= 2 ? N_CMDS : 9;
|
|
921
|
+
for (int c = 0; c < n; ++c) out[c] = Cmd(c);
|
|
922
|
+
return n;
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
// ====================================================================
|
|
926
|
+
// 7. 地图生成与开局
|
|
927
|
+
// ====================================================================
|
|
928
|
+
|
|
929
|
+
struct XY {
|
|
930
|
+
int x, y;
|
|
931
|
+
bool operator==(const XY&) const = default;
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
// 一张地图的布局。rows[y][x] 是 CELL_CHARS 里的字符。
|
|
935
|
+
struct Layout {
|
|
936
|
+
int size = 0;
|
|
937
|
+
char rows[MAX_SIZE][MAX_SIZE + 1] = {};
|
|
938
|
+
XY base[2] = {}; // 两方基地
|
|
939
|
+
XY spawn[6] = {}; // 六辆坦克的出生点(按坦克编号)
|
|
940
|
+
int n_supply = 0;
|
|
941
|
+
XY supply[MAX_SUPPLY] = {}; // 补给点
|
|
942
|
+
int ring = 1; // 基地外的砖墙层数(只有 check_layout 用)
|
|
943
|
+
};
|
|
944
|
+
|
|
945
|
+
// 按布局和参数开一局。布局里的 size 优先于 P.size。
|
|
946
|
+
inline State new_game(const Params& params, const Layout& lay) {
|
|
947
|
+
State s;
|
|
948
|
+
s.P = params;
|
|
949
|
+
s.P.size = lay.size;
|
|
950
|
+
for (int c = 0; c < CELLS; ++c) s.grid[c] = OUTSIDE;
|
|
951
|
+
for (int y = 0; y < lay.size; ++y)
|
|
952
|
+
for (int x = 0; x < lay.size; ++x) {
|
|
953
|
+
Cell g = EMPTY;
|
|
954
|
+
switch (lay.rows[y][x]) {
|
|
955
|
+
case '#': g = BRICK; break;
|
|
956
|
+
case '@': g = STEEL; break;
|
|
957
|
+
case '~': g = WATER; break;
|
|
958
|
+
case '0': g = BASE0; break;
|
|
959
|
+
case '1': g = BASE1; break;
|
|
960
|
+
default: g = EMPTY; break;
|
|
961
|
+
}
|
|
962
|
+
s.grid[cell_of(x, y)] = g;
|
|
963
|
+
if (g == BRICK || g == STEEL) s.wall_hash ^= wall_key(cell_of(x, y));
|
|
964
|
+
}
|
|
965
|
+
for (int seat = 0; seat < 2; ++seat) {
|
|
966
|
+
s.base_pos[seat] = int16_t(cell_of(lay.base[seat].x, lay.base[seat].y));
|
|
967
|
+
s.base_hp[seat] = int16_t(s.P.base_hp);
|
|
968
|
+
}
|
|
969
|
+
s.n_supply = int16_t(lay.n_supply);
|
|
970
|
+
for (int k = 0; k < lay.n_supply; ++k) s.supply[k] = int16_t(cell_of(lay.supply[k].x, lay.supply[k].y));
|
|
971
|
+
for (int i = 0; i < 6; ++i) {
|
|
972
|
+
Tank& t = s.tank[i];
|
|
973
|
+
s.spawn[i] = int16_t(cell_of(lay.spawn[i].x, lay.spawn[i].y));
|
|
974
|
+
t.pos = s.spawn[i];
|
|
975
|
+
t.alive = true;
|
|
976
|
+
t.level = int16_t(s.P.start_level[seat_of(i)]);
|
|
977
|
+
t.hp = int16_t(s.P.types[type_of(i)].hp + s.P.level_hp * t.level);
|
|
978
|
+
}
|
|
979
|
+
return s;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// 从局面取回布局(地形是当前的,被打掉的墙不会回来)。
|
|
983
|
+
inline Layout layout_of(const State& s) {
|
|
984
|
+
Layout lay;
|
|
985
|
+
lay.size = s.P.size;
|
|
986
|
+
lay.ring = s.P.ring;
|
|
987
|
+
for (int y = 0; y < lay.size; ++y)
|
|
988
|
+
for (int x = 0; x < lay.size; ++x) lay.rows[y][x] = CELL_CHARS[s.grid[cell_of(x, y)]];
|
|
989
|
+
for (int seat = 0; seat < 2; ++seat) lay.base[seat] = XY{cell_x(s.base_pos[seat]), cell_y(s.base_pos[seat])};
|
|
990
|
+
for (int i = 0; i < 6; ++i) lay.spawn[i] = XY{cell_x(s.spawn[i]), cell_y(s.spawn[i])};
|
|
991
|
+
lay.n_supply = s.n_supply;
|
|
992
|
+
for (int k = 0; k < s.n_supply; ++k) lay.supply[k] = XY{cell_x(s.supply[k]), cell_y(s.supply[k])};
|
|
993
|
+
return lay;
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// 地图生成用的随机数:splitmix64,加拒绝采样取范围。只用整数运算,
|
|
997
|
+
// 所以同一个种子在任何编译器、任何标准库下都生成同一张图。
|
|
998
|
+
struct Rng {
|
|
999
|
+
uint64_t state;
|
|
1000
|
+
explicit constexpr Rng(uint64_t seed) : state(seed) {}
|
|
1001
|
+
constexpr uint64_t next() {
|
|
1002
|
+
uint64_t z = (state += 0x9E3779B97F4A7C15ULL);
|
|
1003
|
+
z = (z ^ (z >> 30)) * 0xBF58476D1CE4E5B9ULL;
|
|
1004
|
+
z = (z ^ (z >> 27)) * 0x94D049BB133111EBULL;
|
|
1005
|
+
return z ^ (z >> 31);
|
|
1006
|
+
}
|
|
1007
|
+
// [0, n) 上的均匀整数,n ≥ 1。
|
|
1008
|
+
constexpr int below(int n) {
|
|
1009
|
+
const uint64_t m = uint64_t(n);
|
|
1010
|
+
const uint64_t limit = UINT64_MAX - UINT64_MAX % m; // m 的整数倍,丢掉尾巴保证均匀
|
|
1011
|
+
uint64_t r = next();
|
|
1012
|
+
while (r >= limit) r = next();
|
|
1013
|
+
return int(r % m);
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
|
|
1017
|
+
// 地图风格。各风格里砖、钢、水占"上半场可放格子"的千分比。
|
|
1018
|
+
enum Style : int { STYLE_OPEN = 0, STYLE_BRICK = 1, STYLE_WATER = 2, STYLE_STEEL = 3 };
|
|
1019
|
+
inline constexpr int N_STYLES = 4;
|
|
1020
|
+
inline constexpr const char* STYLE_NAMES[N_STYLES] = {"open", "brick", "water", "steel"};
|
|
1021
|
+
inline constexpr int STYLE_DENSITY[N_STYLES][3] = {
|
|
1022
|
+
// 砖 钢 水
|
|
1023
|
+
{100, 20, 20}, // 开阔
|
|
1024
|
+
{340, 30, 20}, // 多砖
|
|
1025
|
+
{100, 20, 170}, // 多水
|
|
1026
|
+
{130, 140, 20}, // 多钢
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
// seat 号方的基地能从左、右、正前中的几个方向被打到(定义见 check_layout 的第 4 条)。
|
|
1030
|
+
// 注意:这是对开局地图(Layout)的静态检查,只看地形:砖墙、水不算挡,不看坦克;
|
|
1031
|
+
// 对局中墙被打掉、有车堵在炮线上,它都不知道。它不是"此刻有没有炮线"的查询。
|
|
1032
|
+
inline int base_open_sides(const Layout& lay, int seat) {
|
|
1033
|
+
const int n = lay.size, R = lay.ring;
|
|
1034
|
+
const XY b = lay.base[seat];
|
|
1035
|
+
auto in_wall = [&](int x, int y) { // 这个基地和它的围墙占的那一块
|
|
1036
|
+
return x >= b.x - R && x <= b.x + R && y >= b.y - R && y <= b.y + R;
|
|
1037
|
+
};
|
|
1038
|
+
// 进攻方(另一方)的坦克从出生点出发、不进围墙能走到哪些格子。
|
|
1039
|
+
bool reach[MAX_SIZE][MAX_SIZE] = {};
|
|
1040
|
+
XY queue[MAX_SIZE * MAX_SIZE];
|
|
1041
|
+
int head = 0, tail = 0;
|
|
1042
|
+
for (int k = 0; k < 3; ++k) {
|
|
1043
|
+
const XY p = lay.spawn[3 * (1 - seat) + k];
|
|
1044
|
+
if (!reach[p.y][p.x] && !in_wall(p.x, p.y)) {
|
|
1045
|
+
reach[p.y][p.x] = true;
|
|
1046
|
+
queue[tail++] = p;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
while (head < tail) {
|
|
1050
|
+
const XY p = queue[head++];
|
|
1051
|
+
for (int d = 0; d < 4; ++d) {
|
|
1052
|
+
const int x = p.x + DIR_DX[d], y = p.y + DIR_DY[d];
|
|
1053
|
+
if (x < 0 || y < 0 || x >= n || y >= n || reach[y][x] || in_wall(x, y)) continue;
|
|
1054
|
+
const char ch = lay.rows[y][x];
|
|
1055
|
+
if (ch != '.' && ch != '#') continue;
|
|
1056
|
+
reach[y][x] = true;
|
|
1057
|
+
queue[tail++] = XY{x, y};
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
// 左、右、正前(朝地图中间)三条线。
|
|
1061
|
+
const int line_dx[3] = {-1, 1, 0};
|
|
1062
|
+
const int line_dy[3] = {0, 0, b.y < n / 2 ? 1 : -1};
|
|
1063
|
+
int open = 0;
|
|
1064
|
+
for (int side = 0; side < 3; ++side) {
|
|
1065
|
+
for (int k = 1;; ++k) {
|
|
1066
|
+
const int x = b.x + k * line_dx[side], y = b.y + k * line_dy[side];
|
|
1067
|
+
if (x < 0 || y < 0 || x >= n || y >= n) break;
|
|
1068
|
+
const char ch = lay.rows[y][x];
|
|
1069
|
+
if (ch == '@' || ch == '0' || ch == '1') break; // 钢墙挡住了这条线
|
|
1070
|
+
if (!in_wall(x, y) && reach[y][x]) { // 围墙外、走得到、能站:一个开炮位
|
|
1071
|
+
++open;
|
|
1072
|
+
break;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
return open;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// 检查一张布局是否满足生成器的保证。通过返回空串,否则返回原因。
|
|
1080
|
+
// 1. 地形上下镜像对称(两个基地互为镜像);
|
|
1081
|
+
// 2. 出生点、补给点都是空地,补给点都在中线上;
|
|
1082
|
+
// 3. 把砖墙当空地(钢墙和水不能过)时,两个基地、六个出生点、所有补给点互相连通;
|
|
1083
|
+
// 4. 每个基地至少能从左、右、正前三个方向中的两个被打到。一个方向"能被打到"是指:从基地沿这个方向
|
|
1084
|
+
// 往外的那条线上,围墙之外有一格可以站坦克(空地,或者能打掉的砖墙),它和基地之间没有钢墙
|
|
1085
|
+
// (砖墙能打掉、水不挡子弹,都不算挡),而且进攻方的坦克不穿过这个基地的围墙就能走到这一格
|
|
1086
|
+
// (把砖墙当空地,钢墙和水不能过)。
|
|
1087
|
+
inline std::string check_layout(const Layout& lay) {
|
|
1088
|
+
const int n = lay.size;
|
|
1089
|
+
if (n < 5 || n > MAX_SIZE) return "尺寸超出范围";
|
|
1090
|
+
auto at = [&](int x, int y) { return lay.rows[y][x]; };
|
|
1091
|
+
for (int y = 0; y < n; ++y)
|
|
1092
|
+
for (int x = 0; x < n; ++x) {
|
|
1093
|
+
const char a = at(x, y), b = at(x, n - 1 - y);
|
|
1094
|
+
const bool bases = (a == '0' && b == '1') || (a == '1' && b == '0');
|
|
1095
|
+
if (a != b && !bases) return "地形不对称";
|
|
1096
|
+
}
|
|
1097
|
+
for (int i = 0; i < 6; ++i)
|
|
1098
|
+
if (at(lay.spawn[i].x, lay.spawn[i].y) != '.') return "出生点不是空地";
|
|
1099
|
+
for (int k = 0; k < lay.n_supply; ++k)
|
|
1100
|
+
if (at(lay.supply[k].x, lay.supply[k].y) != '.' || lay.supply[k].y != n / 2) return "补给点不是中线空地";
|
|
1101
|
+
bool seen[MAX_SIZE][MAX_SIZE] = {};
|
|
1102
|
+
XY queue[MAX_SIZE * MAX_SIZE];
|
|
1103
|
+
int head = 0, tail = 0;
|
|
1104
|
+
queue[tail++] = lay.base[0];
|
|
1105
|
+
seen[lay.base[0].y][lay.base[0].x] = true;
|
|
1106
|
+
while (head < tail) {
|
|
1107
|
+
const XY p = queue[head++];
|
|
1108
|
+
for (int d = 0; d < 4; ++d) {
|
|
1109
|
+
const int x = p.x + DIR_DX[d], y = p.y + DIR_DY[d];
|
|
1110
|
+
if (x < 0 || y < 0 || x >= n || y >= n || seen[y][x]) continue;
|
|
1111
|
+
const char ch = at(x, y);
|
|
1112
|
+
if (ch != '.' && ch != '#' && ch != '0' && ch != '1') continue;
|
|
1113
|
+
seen[y][x] = true;
|
|
1114
|
+
queue[tail++] = XY{x, y};
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
if (!seen[lay.base[1].y][lay.base[1].x]) return "两个基地不连通";
|
|
1118
|
+
for (int i = 0; i < 6; ++i)
|
|
1119
|
+
if (!seen[lay.spawn[i].y][lay.spawn[i].x]) return "出生点不连通";
|
|
1120
|
+
for (int k = 0; k < lay.n_supply; ++k)
|
|
1121
|
+
if (!seen[lay.supply[k].y][lay.supply[k].x]) return "补给点不连通";
|
|
1122
|
+
for (int seat = 0; seat < 2; ++seat)
|
|
1123
|
+
if (base_open_sides(lay, seat) < 2) return "基地能被打到的方向不足两个";
|
|
1124
|
+
return "";
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
// 按种子生成一张地图。style 传 -1 表示按种子轮换四种风格。
|
|
1128
|
+
// 用到的参数:size(不小于 11 的奇数)、supply_points(1..5)、ring、spawn_near、spawn_flip。
|
|
1129
|
+
// 同样的种子、风格和这几项参数总是生成同一张图;spawn_flip 只改 0 号方的车型排布,不改地形。
|
|
1130
|
+
//
|
|
1131
|
+
// 生成的图满足 check_layout 的全部保证(对称、连通、每个基地至少两个方向能被打到),另外:
|
|
1132
|
+
// 基地所在的那几行里,围墙两侧各 3 格之内没有钢墙和水;
|
|
1133
|
+
// 基地在己方底边正中,外面围 ring 层砖;补给点在中线上、左右对称;
|
|
1134
|
+
// 哪种车型用哪个出生位由种子决定;1 号方三辆车的出生位上下翻过去就是 0 号方的三个出生位,
|
|
1135
|
+
// spawn_flip 时 0 号方的车型排布再左右对调(比如 1 号方的快速型在左,0 号方的快速型就在右)。
|
|
1136
|
+
inline Layout generate_map(uint64_t seed, int style, const Params& P) {
|
|
1137
|
+
if (style < 0 || style >= N_STYLES) style = int(seed % N_STYLES);
|
|
1138
|
+
const int n = P.size, cx = n / 2, mid = n / 2, R = P.ring;
|
|
1139
|
+
|
|
1140
|
+
// 种子和影响地形的参数混在一起作为随机数的起点。
|
|
1141
|
+
uint64_t h = Rng(seed).next();
|
|
1142
|
+
for (int v : {style, n, P.supply_points, R, int(P.spawn_near)}) h = Rng(h ^ (uint64_t(v) + 1) * 0xD6E8FEB86659FD93ULL).next();
|
|
1143
|
+
Rng rng(h);
|
|
1144
|
+
|
|
1145
|
+
Layout lay;
|
|
1146
|
+
lay.size = n;
|
|
1147
|
+
lay.ring = R;
|
|
1148
|
+
lay.base[0] = XY{cx, n - 1};
|
|
1149
|
+
lay.base[1] = XY{cx, 0};
|
|
1150
|
+
|
|
1151
|
+
// 1 号方(上方)的三个出生位。
|
|
1152
|
+
XY slot[3];
|
|
1153
|
+
const int front = R + 1;
|
|
1154
|
+
if (P.spawn_near) {
|
|
1155
|
+
slot[0] = XY{cx - R - 1, front};
|
|
1156
|
+
slot[1] = XY{cx + R + 1, front};
|
|
1157
|
+
slot[2] = XY{cx, front};
|
|
1158
|
+
} else {
|
|
1159
|
+
const int off = n / 4 + 1;
|
|
1160
|
+
slot[0] = XY{cx - off, 0};
|
|
1161
|
+
slot[1] = XY{cx + off, 0};
|
|
1162
|
+
slot[2] = XY{cx, front};
|
|
1163
|
+
}
|
|
1164
|
+
// 哪种车型用哪个出生位:洗牌。
|
|
1165
|
+
int perm[3] = {0, 1, 2};
|
|
1166
|
+
for (int k = 2; k > 0; --k) {
|
|
1167
|
+
const int j = rng.below(k + 1);
|
|
1168
|
+
const int tmp = perm[k];
|
|
1169
|
+
perm[k] = perm[j];
|
|
1170
|
+
perm[j] = tmp;
|
|
1171
|
+
}
|
|
1172
|
+
for (int k = 0; k < 3; ++k) {
|
|
1173
|
+
const XY p = slot[perm[k]];
|
|
1174
|
+
lay.spawn[3 + k] = p;
|
|
1175
|
+
lay.spawn[k] = P.spawn_flip ? XY{n - 1 - p.x, n - 1 - p.y} : XY{p.x, n - 1 - p.y};
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
// 补给点:都在中线上,左右对称。
|
|
1179
|
+
{
|
|
1180
|
+
const int d = n / 3, k = P.supply_points;
|
|
1181
|
+
int xs[MAX_SUPPLY] = {};
|
|
1182
|
+
if (k == 1) { xs[0] = cx; }
|
|
1183
|
+
else if (k == 2) { xs[0] = cx - d; xs[1] = cx + d; }
|
|
1184
|
+
else if (k == 3) { xs[0] = cx - d; xs[1] = cx; xs[2] = cx + d; }
|
|
1185
|
+
else if (k == 4) { xs[0] = cx - d; xs[1] = cx - d / 2; xs[2] = cx + d / 2; xs[3] = cx + d; }
|
|
1186
|
+
else if (k == 5) { xs[0] = cx - d; xs[1] = cx - d / 2; xs[2] = cx; xs[3] = cx + d / 2; xs[4] = cx + d; }
|
|
1187
|
+
lay.n_supply = k < 0 ? 0 : (k > MAX_SUPPLY ? MAX_SUPPLY : k);
|
|
1188
|
+
for (int j = 0; j < lay.n_supply; ++j) lay.supply[j] = XY{xs[j], mid};
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
// 上半场(含中线)里固定的格子:基地和围墙。fixed[y][x] 为 0 表示不固定。
|
|
1192
|
+
char fixed[MAX_SIZE][MAX_SIZE] = {};
|
|
1193
|
+
for (int y = 0; y <= R; ++y)
|
|
1194
|
+
for (int x = cx - R; x <= cx + R; ++x) fixed[y][x] = '#';
|
|
1195
|
+
fixed[0][cx] = '1';
|
|
1196
|
+
|
|
1197
|
+
// 基地所在的那几行(围墙占的行)里,围墙两侧各 3 格之内不放钢墙和水,只可能是空地或砖墙。
|
|
1198
|
+
// 这样基地的左、右两条线总有开炮位,不会出现"两侧被钢墙或水封死、只剩正面一列能打"的图。
|
|
1199
|
+
auto base_side = [&](int x, int y) { return y <= R && x >= cx - R - 3 && x <= cx + R + 3; };
|
|
1200
|
+
|
|
1201
|
+
// 不放随机障碍的格子:固定格、补给点、出生位和它四周。
|
|
1202
|
+
bool keep[MAX_SIZE][MAX_SIZE] = {};
|
|
1203
|
+
for (int y = 0; y <= mid; ++y)
|
|
1204
|
+
for (int x = 0; x < n; ++x) keep[y][x] = fixed[y][x] != 0;
|
|
1205
|
+
for (int j = 0; j < lay.n_supply; ++j) keep[mid][lay.supply[j].x] = true;
|
|
1206
|
+
for (const XY& p : slot) {
|
|
1207
|
+
keep[p.y][p.x] = true;
|
|
1208
|
+
for (int d = 0; d < 4; ++d) {
|
|
1209
|
+
const int x = p.x + DIR_DX[d], y = p.y + DIR_DY[d];
|
|
1210
|
+
if (x >= 0 && y >= 0 && x < n && y <= mid) keep[y][x] = true;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
XY free_cells[MAX_SIZE * MAX_SIZE];
|
|
1214
|
+
int n_free = 0;
|
|
1215
|
+
for (int y = 0; y <= mid; ++y)
|
|
1216
|
+
for (int x = 0; x < n; ++x)
|
|
1217
|
+
if (!keep[y][x]) free_cells[n_free++] = XY{x, y};
|
|
1218
|
+
|
|
1219
|
+
char half[MAX_SIZE][MAX_SIZE]; // 上半场(含中线)的地形;下半场是它的镜像
|
|
1220
|
+
auto build_rows = [&]() {
|
|
1221
|
+
for (int y = 0; y < n; ++y) {
|
|
1222
|
+
for (int x = 0; x < n; ++x) {
|
|
1223
|
+
char ch = half[y <= mid ? y : n - 1 - y][x];
|
|
1224
|
+
if (y > mid && ch == '1') ch = '0';
|
|
1225
|
+
lay.rows[y][x] = ch;
|
|
1226
|
+
}
|
|
1227
|
+
lay.rows[y][n] = 0;
|
|
1228
|
+
}
|
|
1229
|
+
};
|
|
1230
|
+
auto reset_half = [&]() {
|
|
1231
|
+
for (int y = 0; y <= mid; ++y)
|
|
1232
|
+
for (int x = 0; x < n; ++x) half[y][x] = fixed[y][x] ? fixed[y][x] : '.';
|
|
1233
|
+
};
|
|
1234
|
+
// 撒一种障碍:反复随机挑一个起点、横或竖、长度 1..max_len,沿途放到目标数量为止;
|
|
1235
|
+
// 碰到保留格、已有障碍或出了上半场就停下这一条。
|
|
1236
|
+
auto bars = [&](char kind, int permille, int max_len) {
|
|
1237
|
+
const int target = (permille * n_free + 500) / 1000;
|
|
1238
|
+
int placed = 0;
|
|
1239
|
+
for (int guard = 0; placed < target && guard < 2000; ++guard) {
|
|
1240
|
+
const XY start = free_cells[rng.below(n_free)];
|
|
1241
|
+
const bool horizontal = rng.below(2) == 0;
|
|
1242
|
+
const int len = 1 + rng.below(max_len);
|
|
1243
|
+
for (int k = 0; k < len; ++k) {
|
|
1244
|
+
const int x = horizontal ? start.x + k : start.x;
|
|
1245
|
+
const int y = horizontal ? start.y : start.y + k;
|
|
1246
|
+
if (x >= n || y > mid || keep[y][x] || half[y][x] != '.') break;
|
|
1247
|
+
if (kind != '#' && base_side(x, y)) break;
|
|
1248
|
+
half[y][x] = kind;
|
|
1249
|
+
if (++placed >= target) break;
|
|
1250
|
+
}
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
|
|
1254
|
+
const int* dens = STYLE_DENSITY[style];
|
|
1255
|
+
for (int attempt = 0; attempt < 500 && n_free > 0; ++attempt) {
|
|
1256
|
+
reset_half();
|
|
1257
|
+
bars('~', dens[2], 4);
|
|
1258
|
+
bars('@', dens[1], 3);
|
|
1259
|
+
bars('#', dens[0], 4);
|
|
1260
|
+
build_rows();
|
|
1261
|
+
if (check_layout(lay).empty()) return lay;
|
|
1262
|
+
}
|
|
1263
|
+
// 500 次都不满足连通性(正常参数下不会发生):退回一张没有随机障碍的图,它一定连通。
|
|
1264
|
+
reset_half();
|
|
1265
|
+
build_rows();
|
|
1266
|
+
return lay;
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// 按种子开一局:generate_map + new_game。
|
|
1270
|
+
inline State make_game(uint64_t seed, int style = -1, const Params& P = Params{}) {
|
|
1271
|
+
return new_game(P, generate_map(seed, style, P));
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// ====================================================================
|
|
1275
|
+
// 8. 局面与文本互转
|
|
1276
|
+
// ====================================================================
|
|
1277
|
+
// 格式见 协议.md。一份完整的局面文本 = 布局段(参数、基地、出生点、补给点)+ 回合段(其余全部)。
|
|
1278
|
+
// to_text / from_text 互为逆运算:from_text(to_text(s)) == s。
|
|
1279
|
+
|
|
1280
|
+
inline std::string cmd_text(Cmd c) {
|
|
1281
|
+
if (c == STAY || c >= N_CMDS) return "S";
|
|
1282
|
+
if (c <= 4) return std::string(1, DIR_CHARS[c - 1]);
|
|
1283
|
+
if (c <= 8) return std::string("F") + DIR_CHARS[c - 5];
|
|
1284
|
+
return std::string(1, DIR_CHARS[(c - 9) >> 2]) + DIR_CHARS[(c - 9) & 3];
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
// 解析一条命令的文本。成功返回 true。
|
|
1288
|
+
inline bool parse_cmd(std::string_view tok, Cmd& out) {
|
|
1289
|
+
auto dir = [](char ch) { return ch == 'U' ? 0 : ch == 'D' ? 1 : ch == 'L' ? 2 : ch == 'R' ? 3 : -1; };
|
|
1290
|
+
if (tok == "S") { out = STAY; return true; }
|
|
1291
|
+
if (tok.size() == 1 && dir(tok[0]) >= 0) { out = cmd_move(dir(tok[0])); return true; }
|
|
1292
|
+
if (tok.size() == 2 && tok[0] == 'F' && dir(tok[1]) >= 0) { out = cmd_fire(dir(tok[1])); return true; }
|
|
1293
|
+
if (tok.size() == 2 && dir(tok[0]) >= 0 && dir(tok[1]) >= 0) { out = cmd_move2(dir(tok[0]), dir(tok[1])); return true; }
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
inline const char* reason_name(Reason r) {
|
|
1298
|
+
static const char* const names[] = {"none", "base", "hp", "kills", "stars", "damage", "draw"};
|
|
1299
|
+
return names[r];
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
// 结束原因的文本:none / base / hp / kills / stars / damage / draw,
|
|
1303
|
+
// 两个基地同回合归零时前面加 both_。
|
|
1304
|
+
inline std::string reason_text(const State& s) {
|
|
1305
|
+
if (!s.done) return "none";
|
|
1306
|
+
return (both_bases_down(s) ? std::string("both_") : std::string()) + reason_name(s.reason);
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
namespace detail {
|
|
1310
|
+
struct Tokens { // 按空白切词
|
|
1311
|
+
std::string_view text;
|
|
1312
|
+
size_t at = 0;
|
|
1313
|
+
bool next(std::string_view& tok) {
|
|
1314
|
+
while (at < text.size() && (text[at] == ' ' || text[at] == '\n' || text[at] == '\r' || text[at] == '\t')) ++at;
|
|
1315
|
+
if (at >= text.size()) return false;
|
|
1316
|
+
const size_t begin = at;
|
|
1317
|
+
while (at < text.size() && !(text[at] == ' ' || text[at] == '\n' || text[at] == '\r' || text[at] == '\t')) ++at;
|
|
1318
|
+
tok = text.substr(begin, at - begin);
|
|
1319
|
+
return true;
|
|
1320
|
+
}
|
|
1321
|
+
bool next_int(int& v) {
|
|
1322
|
+
std::string_view tok;
|
|
1323
|
+
if (!next(tok) || tok.empty()) return false;
|
|
1324
|
+
size_t k = 0;
|
|
1325
|
+
bool neg = false;
|
|
1326
|
+
if (tok[0] == '-') { neg = true; k = 1; }
|
|
1327
|
+
if (k >= tok.size() || tok.size() - k > 9) return false;
|
|
1328
|
+
int x = 0;
|
|
1329
|
+
for (; k < tok.size(); ++k) {
|
|
1330
|
+
if (tok[k] < '0' || tok[k] > '9') return false;
|
|
1331
|
+
x = x * 10 + (tok[k] - '0');
|
|
1332
|
+
}
|
|
1333
|
+
v = neg ? -x : x;
|
|
1334
|
+
return true;
|
|
1335
|
+
}
|
|
1336
|
+
bool expect(std::string_view word) {
|
|
1337
|
+
std::string_view tok;
|
|
1338
|
+
return next(tok) && tok == word;
|
|
1339
|
+
}
|
|
1340
|
+
};
|
|
1341
|
+
|
|
1342
|
+
inline void put(std::string& out, int v) {
|
|
1343
|
+
out += std::to_string(v);
|
|
1344
|
+
out += ' ';
|
|
1345
|
+
}
|
|
1346
|
+
inline void end_line(std::string& out) {
|
|
1347
|
+
if (!out.empty() && out.back() == ' ') out.back() = '\n';
|
|
1348
|
+
else out += '\n';
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
inline bool fail(std::string* err, const char* msg) {
|
|
1352
|
+
if (err) *err = msg;
|
|
1353
|
+
return false;
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
inline bool in_map(const State& s, int x, int y) { return x >= 0 && y >= 0 && x < s.P.size && y < s.P.size; }
|
|
1357
|
+
|
|
1358
|
+
// 读布局段,写进 s 的 P、base_pos、spawn、supply。
|
|
1359
|
+
inline bool read_layout(Tokens& in, State& s, std::string* err) {
|
|
1360
|
+
int v = 0;
|
|
1361
|
+
if (!in.expect("TANK-STATE") || !in.next_int(v) || v != 1) return fail(err, "开头应为 TANK-STATE 1");
|
|
1362
|
+
Params P;
|
|
1363
|
+
std::string_view tok;
|
|
1364
|
+
for (;;) {
|
|
1365
|
+
const size_t mark = in.at;
|
|
1366
|
+
if (!in.next(tok)) return fail(err, "布局段不完整");
|
|
1367
|
+
if (tok != "P") { in.at = mark; break; }
|
|
1368
|
+
std::string_view key;
|
|
1369
|
+
if (!in.next(key)) return fail(err, "P 后面缺参数名");
|
|
1370
|
+
if (key == "tiebreak") {
|
|
1371
|
+
if (!in.next_int(P.n_tiebreak) || P.n_tiebreak < 0 || P.n_tiebreak > 4) return fail(err, "tiebreak 的项数不对");
|
|
1372
|
+
for (int k = 0; k < P.n_tiebreak; ++k) {
|
|
1373
|
+
if (!in.next(tok)) return fail(err, "tiebreak 不完整");
|
|
1374
|
+
int found = -1;
|
|
1375
|
+
for (int j = 0; j < 4; ++j)
|
|
1376
|
+
if (tok == TIEBREAK_NAMES[j]) found = j;
|
|
1377
|
+
if (found < 0) return fail(err, "tiebreak 项目未知");
|
|
1378
|
+
P.tiebreak[k] = found;
|
|
1379
|
+
}
|
|
1380
|
+
for (int k = P.n_tiebreak; k < 4; ++k) P.tiebreak[k] = 0;
|
|
1381
|
+
continue;
|
|
1382
|
+
}
|
|
1383
|
+
if (!in.next_int(v)) return fail(err, "参数值应为整数");
|
|
1384
|
+
bool known = false;
|
|
1385
|
+
for_each_param(P, [&](const char* name, auto& member) {
|
|
1386
|
+
if (key == name) {
|
|
1387
|
+
member = static_cast<std::remove_reference_t<decltype(member)>>(v);
|
|
1388
|
+
known = true;
|
|
1389
|
+
}
|
|
1390
|
+
});
|
|
1391
|
+
if (!known) return fail(err, "未知参数");
|
|
1392
|
+
}
|
|
1393
|
+
const std::string bad = check_params(P);
|
|
1394
|
+
if (!bad.empty()) {
|
|
1395
|
+
if (err) *err = bad;
|
|
1396
|
+
return false;
|
|
1397
|
+
}
|
|
1398
|
+
s.P = P;
|
|
1399
|
+
auto read_cell = [&](int16_t& cell) {
|
|
1400
|
+
int x, y;
|
|
1401
|
+
if (!in.next_int(x) || !in.next_int(y) || !in_map(s, x, y)) return false;
|
|
1402
|
+
cell = int16_t(cell_of(x, y));
|
|
1403
|
+
return true;
|
|
1404
|
+
};
|
|
1405
|
+
if (!in.expect("BASES") || !read_cell(s.base_pos[0]) || !read_cell(s.base_pos[1])) return fail(err, "BASES 行不对");
|
|
1406
|
+
if (!in.expect("SPAWNS")) return fail(err, "缺 SPAWNS");
|
|
1407
|
+
for (int i = 0; i < 6; ++i)
|
|
1408
|
+
if (!read_cell(s.spawn[i])) return fail(err, "SPAWNS 行不对");
|
|
1409
|
+
if (!in.expect("SUPPLY") || !in.next_int(v) || v < 0 || v > MAX_SUPPLY) return fail(err, "SUPPLY 行不对");
|
|
1410
|
+
s.n_supply = int16_t(v);
|
|
1411
|
+
for (int k = 0; k < MAX_SUPPLY; ++k) s.supply[k] = 0;
|
|
1412
|
+
for (int k = 0; k < s.n_supply; ++k)
|
|
1413
|
+
if (!read_cell(s.supply[k])) return fail(err, "SUPPLY 行不对");
|
|
1414
|
+
return true;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// 读回合段,写进 s 的其余部分。s 里要已经有布局段的内容。
|
|
1418
|
+
inline bool read_turn(Tokens& in, State& s, std::string* err) {
|
|
1419
|
+
int a, b, c, d;
|
|
1420
|
+
std::string_view tok;
|
|
1421
|
+
if (!in.expect("TURN") || !in.next_int(a) || !in.next_int(b) || !in.next_int(c) || !in.next(tok))
|
|
1422
|
+
return fail(err, "TURN 行不对");
|
|
1423
|
+
if (a < 0 || a > 30000 || b < 0 || b > 1 || c < -1 || c > 1) return fail(err, "TURN 行的数值不对");
|
|
1424
|
+
s.turn = int16_t(a);
|
|
1425
|
+
s.done = b != 0;
|
|
1426
|
+
s.winner = int8_t(c);
|
|
1427
|
+
if (tok.substr(0, 5) == "both_") tok.remove_prefix(5);
|
|
1428
|
+
int reason = -1;
|
|
1429
|
+
for (int k = 0; k <= R_DRAW; ++k)
|
|
1430
|
+
if (tok == reason_name(Reason(k))) reason = k;
|
|
1431
|
+
if (reason < 0) return fail(err, "结束原因未知");
|
|
1432
|
+
s.reason = Reason(reason);
|
|
1433
|
+
auto pair = [&](const char* word, int16_t* dst) {
|
|
1434
|
+
if (!in.expect(word) || !in.next_int(a) || !in.next_int(b) || a < 0 || b < 0 || a > 30000 || b > 30000) return false;
|
|
1435
|
+
dst[0] = int16_t(a);
|
|
1436
|
+
dst[1] = int16_t(b);
|
|
1437
|
+
return true;
|
|
1438
|
+
};
|
|
1439
|
+
if (!pair("BASEHP", s.base_hp) || !pair("BASEQUIET", s.base_quiet) || !pair("KILLS", s.kills) ||
|
|
1440
|
+
!pair("STARCOUNT", s.stars_eaten) || !pair("BASEDMG", s.base_dmg))
|
|
1441
|
+
return fail(err, "BASEHP / BASEQUIET / KILLS / STARCOUNT / BASEDMG 行不对");
|
|
1442
|
+
if (!in.expect("MAP")) return fail(err, "缺 MAP");
|
|
1443
|
+
const int n = s.P.size;
|
|
1444
|
+
for (int k = 0; k < CELLS; ++k) s.grid[k] = OUTSIDE;
|
|
1445
|
+
s.wall_hash = 0;
|
|
1446
|
+
for (int y = 0; y < n; ++y) {
|
|
1447
|
+
if (!in.next(tok) || int(tok.size()) != n) return fail(err, "MAP 的行长不对");
|
|
1448
|
+
for (int x = 0; x < n; ++x) {
|
|
1449
|
+
int g = -1;
|
|
1450
|
+
for (int k = 0; k < 6; ++k)
|
|
1451
|
+
if (tok[x] == CELL_CHARS[k]) g = k;
|
|
1452
|
+
if (g < 0) return fail(err, "MAP 里有未知字符");
|
|
1453
|
+
s.grid[cell_of(x, y)] = uint8_t(g);
|
|
1454
|
+
if (g == BRICK || g == STEEL) s.wall_hash ^= wall_key(cell_of(x, y));
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (!in.expect("TANKS") || !in.next_int(a) || a != 6) return fail(err, "TANKS 行不对");
|
|
1458
|
+
for (int i = 0; i < 6; ++i) {
|
|
1459
|
+
int id, x, y, hp, level, alive, respawn_turn, last_fire;
|
|
1460
|
+
if (!in.expect("T") || !in.next_int(id) || !in.next_int(x) || !in.next_int(y) || !in.next_int(hp) ||
|
|
1461
|
+
!in.next_int(level) || !in.next_int(alive) || !in.next_int(respawn_turn) || !in.next_int(last_fire))
|
|
1462
|
+
return fail(err, "T 行不完整");
|
|
1463
|
+
if (id != i || alive < 0 || alive > 1 || hp < -1000 || hp > 30000 || level < 0 || level > 100 ||
|
|
1464
|
+
respawn_turn < 0 || respawn_turn > 30000 || last_fire < NEVER_FIRED || last_fire > 30000)
|
|
1465
|
+
return fail(err, "T 行的数值不对");
|
|
1466
|
+
if (alive ? !in_map(s, x, y) : !(x == -1 && y == -1)) return fail(err, "T 行的坐标不对");
|
|
1467
|
+
Tank& t = s.tank[i];
|
|
1468
|
+
t.pos = int16_t(alive ? cell_of(x, y) : NO_CELL);
|
|
1469
|
+
t.hp = int16_t(hp);
|
|
1470
|
+
t.level = int16_t(level);
|
|
1471
|
+
t.alive = alive != 0;
|
|
1472
|
+
t.respawn_turn = int16_t(respawn_turn);
|
|
1473
|
+
t.last_fire = int16_t(last_fire);
|
|
1474
|
+
}
|
|
1475
|
+
if (!in.expect("BULLETS") || !in.next_int(a) || a < 0 || a > MAX_BULLETS) return fail(err, "BULLETS 行不对");
|
|
1476
|
+
s.n_bullets = int16_t(a);
|
|
1477
|
+
for (int k = 0; k < MAX_BULLETS; ++k) s.bullet[k] = Bullet{};
|
|
1478
|
+
for (int k = 0; k < s.n_bullets; ++k) {
|
|
1479
|
+
int x, y;
|
|
1480
|
+
if (!in.expect("B") || !in.next_int(x) || !in.next_int(y) || !in.next(tok) || !in.next_int(b) ||
|
|
1481
|
+
!in.next_int(c) || !in.next_int(d))
|
|
1482
|
+
return fail(err, "B 行不完整");
|
|
1483
|
+
int dir = -1;
|
|
1484
|
+
for (int j = 0; j < 4; ++j)
|
|
1485
|
+
if (tok.size() == 1 && tok[0] == DIR_CHARS[j]) dir = j;
|
|
1486
|
+
if (!in_map(s, x, y) || dir < 0 || b < 0 || b > 5 || c < 0 || c > 1 || d < 0 || d > 100) return fail(err, "B 行的数值不对");
|
|
1487
|
+
s.bullet[k] = Bullet{int16_t(cell_of(x, y)), int8_t(dir), int8_t(b), c != 0, int8_t(d)};
|
|
1488
|
+
}
|
|
1489
|
+
if (!in.expect("STARS") || !in.next_int(a) || a < 0 || a > s.n_supply) return fail(err, "STARS 行不对");
|
|
1490
|
+
s.star_mask = 0;
|
|
1491
|
+
for (int k = 0; k < a; ++k) {
|
|
1492
|
+
int x, y;
|
|
1493
|
+
if (!in.next_int(x) || !in.next_int(y)) return fail(err, "星星坐标不完整");
|
|
1494
|
+
int found = -1;
|
|
1495
|
+
for (int j = 0; j < s.n_supply; ++j)
|
|
1496
|
+
if (s.supply[j] == cell_of(x, y)) found = j;
|
|
1497
|
+
if (found < 0) return fail(err, "星星不在补给点上");
|
|
1498
|
+
s.star_mask |= uint8_t(1u << found);
|
|
1499
|
+
}
|
|
1500
|
+
if (!in.expect("END")) return fail(err, "缺 END");
|
|
1501
|
+
return true;
|
|
1502
|
+
}
|
|
1503
|
+
} // namespace detail
|
|
1504
|
+
|
|
1505
|
+
// 布局段:参数、基地、出生点、补给点。一局之内不变。
|
|
1506
|
+
inline std::string layout_text(const State& s) {
|
|
1507
|
+
using detail::put;
|
|
1508
|
+
using detail::end_line;
|
|
1509
|
+
std::string out = "TANK-STATE 1\n";
|
|
1510
|
+
for_each_param(s.P, [&](const char* name, const auto& member) {
|
|
1511
|
+
out += "P ";
|
|
1512
|
+
out += name;
|
|
1513
|
+
out += ' ';
|
|
1514
|
+
put(out, int(member));
|
|
1515
|
+
end_line(out);
|
|
1516
|
+
});
|
|
1517
|
+
out += "P tiebreak ";
|
|
1518
|
+
put(out, s.P.n_tiebreak);
|
|
1519
|
+
for (int k = 0; k < s.P.n_tiebreak; ++k) {
|
|
1520
|
+
out += TIEBREAK_NAMES[s.P.tiebreak[k]];
|
|
1521
|
+
out += ' ';
|
|
1522
|
+
}
|
|
1523
|
+
end_line(out);
|
|
1524
|
+
out += "BASES ";
|
|
1525
|
+
for (int seat = 0; seat < 2; ++seat) { put(out, cell_x(s.base_pos[seat])); put(out, cell_y(s.base_pos[seat])); }
|
|
1526
|
+
end_line(out);
|
|
1527
|
+
out += "SPAWNS ";
|
|
1528
|
+
for (int i = 0; i < 6; ++i) { put(out, cell_x(s.spawn[i])); put(out, cell_y(s.spawn[i])); }
|
|
1529
|
+
end_line(out);
|
|
1530
|
+
out += "SUPPLY ";
|
|
1531
|
+
put(out, s.n_supply);
|
|
1532
|
+
for (int k = 0; k < s.n_supply; ++k) { put(out, cell_x(s.supply[k])); put(out, cell_y(s.supply[k])); }
|
|
1533
|
+
end_line(out);
|
|
1534
|
+
return out;
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
// 回合段:随对局变化的全部内容。
|
|
1538
|
+
inline std::string turn_text(const State& s) {
|
|
1539
|
+
using detail::put;
|
|
1540
|
+
using detail::end_line;
|
|
1541
|
+
std::string out;
|
|
1542
|
+
out.reserve(700);
|
|
1543
|
+
out += "TURN ";
|
|
1544
|
+
put(out, s.turn); put(out, s.done ? 1 : 0); put(out, s.winner);
|
|
1545
|
+
out += reason_text(s);
|
|
1546
|
+
end_line(out);
|
|
1547
|
+
auto pair = [&](const char* word, const int16_t* v) {
|
|
1548
|
+
out += word;
|
|
1549
|
+
out += ' ';
|
|
1550
|
+
put(out, v[0]); put(out, v[1]);
|
|
1551
|
+
end_line(out);
|
|
1552
|
+
};
|
|
1553
|
+
pair("BASEHP", s.base_hp);
|
|
1554
|
+
pair("BASEQUIET", s.base_quiet);
|
|
1555
|
+
pair("KILLS", s.kills);
|
|
1556
|
+
pair("STARCOUNT", s.stars_eaten);
|
|
1557
|
+
pair("BASEDMG", s.base_dmg);
|
|
1558
|
+
out += "MAP\n";
|
|
1559
|
+
for (int y = 0; y < s.P.size; ++y) {
|
|
1560
|
+
for (int x = 0; x < s.P.size; ++x) out += CELL_CHARS[s.grid[cell_of(x, y)]];
|
|
1561
|
+
out += '\n';
|
|
1562
|
+
}
|
|
1563
|
+
out += "TANKS 6\n";
|
|
1564
|
+
for (int i = 0; i < 6; ++i) {
|
|
1565
|
+
const Tank& t = s.tank[i];
|
|
1566
|
+
out += "T ";
|
|
1567
|
+
put(out, i); put(out, cell_x(t.pos)); put(out, cell_y(t.pos)); put(out, t.hp); put(out, t.level);
|
|
1568
|
+
put(out, t.alive ? 1 : 0); put(out, t.respawn_turn); put(out, t.last_fire);
|
|
1569
|
+
end_line(out);
|
|
1570
|
+
}
|
|
1571
|
+
out += "BULLETS ";
|
|
1572
|
+
put(out, s.n_bullets);
|
|
1573
|
+
end_line(out);
|
|
1574
|
+
for (int k = 0; k < s.n_bullets; ++k) {
|
|
1575
|
+
const Bullet& b = s.bullet[k];
|
|
1576
|
+
out += "B ";
|
|
1577
|
+
put(out, cell_x(b.pos)); put(out, cell_y(b.pos));
|
|
1578
|
+
out += DIR_CHARS[b.dir];
|
|
1579
|
+
out += ' ';
|
|
1580
|
+
put(out, b.owner); put(out, b.steel ? 1 : 0); put(out, b.level);
|
|
1581
|
+
end_line(out);
|
|
1582
|
+
}
|
|
1583
|
+
int n_stars = 0;
|
|
1584
|
+
for (int k = 0; k < s.n_supply; ++k) n_stars += (s.star_mask >> k) & 1;
|
|
1585
|
+
out += "STARS ";
|
|
1586
|
+
put(out, n_stars);
|
|
1587
|
+
end_line(out);
|
|
1588
|
+
for (int k = 0; k < s.n_supply; ++k)
|
|
1589
|
+
if ((s.star_mask >> k) & 1) {
|
|
1590
|
+
put(out, cell_x(s.supply[k])); put(out, cell_y(s.supply[k]));
|
|
1591
|
+
end_line(out);
|
|
1592
|
+
}
|
|
1593
|
+
out += "END\n";
|
|
1594
|
+
return out;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// 完整局面 → 文本。
|
|
1598
|
+
inline std::string to_text(const State& s) { return layout_text(s) + turn_text(s); }
|
|
1599
|
+
|
|
1600
|
+
// 文本 → 完整局面。成功返回 true;失败返回 false,err 里是原因,s 的内容不确定。
|
|
1601
|
+
inline bool from_text(std::string_view text, State& s, std::string* err = nullptr) {
|
|
1602
|
+
detail::Tokens in{text};
|
|
1603
|
+
s = State{};
|
|
1604
|
+
return detail::read_layout(in, s, err) && detail::read_turn(in, s, err);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
// 只读回合段,更新 s 里随对局变化的部分(s 要已经有同一局的布局段内容)。
|
|
1608
|
+
inline bool read_turn_text(std::string_view text, State& s, std::string* err = nullptr) {
|
|
1609
|
+
detail::Tokens in{text};
|
|
1610
|
+
return detail::read_turn(in, s, err);
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
// 一方一回合的回复行:"<回合号> <快速型的命令> <火力型的命令> <装甲型的命令>",回合号 = s.turn + 1。
|
|
1614
|
+
inline std::string orders_text(const State& s, const Orders& o) {
|
|
1615
|
+
return std::to_string(s.turn + 1) + " " + cmd_text(o.cmd[0]) + " " + cmd_text(o.cmd[1]) + " " + cmd_text(o.cmd[2]);
|
|
1616
|
+
}
|
|
1617
|
+
|
|
1618
|
+
// 解析回复行。'#' 之后是备注,忽略。以下情况整行非法、返回 false(裁判把这一回合三辆车都当不动):
|
|
1619
|
+
// 回合号不是 s.turn + 1;不是恰好 3 条命令;出现不认识的写法;给非快速型写了两步。
|
|
1620
|
+
inline bool parse_orders(std::string_view line, const State& s, Orders& out, std::string* err = nullptr) {
|
|
1621
|
+
const size_t hash = line.find('#');
|
|
1622
|
+
if (hash != std::string_view::npos) line = line.substr(0, hash);
|
|
1623
|
+
detail::Tokens in{line};
|
|
1624
|
+
int t = 0;
|
|
1625
|
+
if (!in.next_int(t) || t != s.turn + 1) return detail::fail(err, "行首应为本回合编号");
|
|
1626
|
+
Orders o;
|
|
1627
|
+
std::string_view tok;
|
|
1628
|
+
for (int k = 0; k < 3; ++k) {
|
|
1629
|
+
if (!in.next(tok)) return detail::fail(err, "应为:回合号 + 3 条命令");
|
|
1630
|
+
if (!parse_cmd(tok, o.cmd[k])) return detail::fail(err, "命令只能是 S、U/D/L/R、两个方向字母或 FU/FD/FL/FR");
|
|
1631
|
+
if (is_move2(o.cmd[k]) && s.P.types[k].speed < 2) return detail::fail(err, "这种车型每回合只能走一步");
|
|
1632
|
+
}
|
|
1633
|
+
if (in.next(tok)) return detail::fail(err, "应为:回合号 + 3 条命令");
|
|
1634
|
+
out = o;
|
|
1635
|
+
return true;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
// 一条事件的文本(回放用)。格式见 协议.md。
|
|
1639
|
+
inline std::string event_text(const Event& e) {
|
|
1640
|
+
using detail::put;
|
|
1641
|
+
std::string out;
|
|
1642
|
+
auto xy = [&]() { put(out, e.x); put(out, e.y); };
|
|
1643
|
+
switch (e.type) {
|
|
1644
|
+
case EV_FIRE: out = "fire "; put(out, e.a); out += DIR_CHARS[e.b]; break;
|
|
1645
|
+
case EV_BLOCKED: out = "blocked "; put(out, e.a); put(out, e.sub); break;
|
|
1646
|
+
case EV_CANCEL_CROSS: out = "cancel cross "; xy(); put(out, e.sub); put(out, e.a); put(out, e.b); break;
|
|
1647
|
+
case EV_CANCEL_SAME: out = "cancel same "; xy(); put(out, e.sub); break;
|
|
1648
|
+
case EV_HIT: out = "hit "; put(out, e.a); put(out, e.b); xy(); put(out, e.sub); put(out, 0); break;
|
|
1649
|
+
case EV_HIT_SWAP: out = "hit "; put(out, e.a); put(out, e.b); xy(); put(out, e.sub); put(out, 1); break;
|
|
1650
|
+
case EV_BASE_HIT: out = "base_hit "; put(out, e.a); put(out, e.b); put(out, e.sub); break;
|
|
1651
|
+
case EV_WALL_BRICK: out = "wall "; xy(); out += "brick "; put(out, e.sub); break;
|
|
1652
|
+
case EV_WALL_STEEL: out = "wall "; xy(); out += "steel "; put(out, e.sub); break;
|
|
1653
|
+
case EV_DEATH: out = "death "; put(out, e.a); xy(); put(out, e.b); break;
|
|
1654
|
+
case EV_RESPAWN: out = "respawn "; put(out, e.a); xy(); break;
|
|
1655
|
+
case EV_BULLET_CLEARED: out = "bullet_cleared "; xy(); put(out, e.a); break;
|
|
1656
|
+
case EV_STAR: out = "star "; put(out, e.a); xy(); put(out, e.b); break;
|
|
1657
|
+
case EV_SUPPLY: out = "supply "; xy(); break;
|
|
1658
|
+
case EV_BASE_REGEN: out = "base_regen "; put(out, e.a); break;
|
|
1659
|
+
}
|
|
1660
|
+
if (!out.empty() && out.back() == ' ') out.pop_back();
|
|
1661
|
+
return out;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// ====================================================================
|
|
1665
|
+
// 9. 思考预算
|
|
1666
|
+
// ====================================================================
|
|
1667
|
+
// 每回合的思考有预算:首回合(init 加第一次 decide)BUDGET_FIRST_MS,之后每回合 BUDGET_TURN_MS。
|
|
1668
|
+
// 单位是"CPU 毫秒":量的是你的程序自己消耗的 CPU 时间,机器忙的时候被挂起、等输入的时间都不算。
|
|
1669
|
+
// bot 只需要用 budget_left():
|
|
1670
|
+
//
|
|
1671
|
+
// while (tank::budget_left() > 3) { /* 再搜一层 */ }
|
|
1672
|
+
//
|
|
1673
|
+
// 起点由主办方的主循环设好,bot 不要自己调 budget_setup / budget_begin。
|
|
1674
|
+
// 裁判量的是你的整个进程(所有线程合计),比 budget_left() 看到的多不到 1 毫秒,留 2–3 毫秒余量即可。
|
|
1675
|
+
// 注意:budget_left() 只看得到调用它的那个线程。开了别的线程的话,那些线程的用量要自己加上。
|
|
1676
|
+
// 超出预算的回合三辆车都不动,连续 3 次判负。
|
|
1677
|
+
//
|
|
1678
|
+
// 【计时的"缝"】怎么量只在两处:这里的 detail::budget_reading()(bot 侧),和裁判 core.py 里的
|
|
1679
|
+
// meter_read()(裁判侧)。尺子有两把,由裁判在开局时告诉主循环用哪一把:
|
|
1680
|
+
// BUDGET_CPU Windows 上是线程的周期计数(QueryThreadCycleTime),除以裁判给的"每 CPU 毫秒多少周期";
|
|
1681
|
+
// Linux / macOS 上是线程的 CPU 时间(CLOCK_THREAD_CPUTIME_ID,按分析实现,未实测)。
|
|
1682
|
+
// BUDGET_WALL 墙上时钟。后备和调试用。
|
|
1683
|
+
|
|
1684
|
+
inline constexpr int BUDGET_FIRST_MS = 2000; // 首回合的预算(含 init 和程序启动)
|
|
1685
|
+
inline constexpr int BUDGET_TURN_MS = 50; // 之后每回合的预算
|
|
1686
|
+
inline constexpr int BUDGET_WALL_FIRST_MS = 4000; // 按 CPU 计时时的墙上保险上限:首回合
|
|
1687
|
+
inline constexpr int BUDGET_WALL_TURN_MS = 2000; // 按 CPU 计时时的墙上保险上限:之后每回合(兜住睡眠、阻塞)
|
|
1688
|
+
|
|
1689
|
+
enum BudgetKind : int { BUDGET_NONE = 0, BUDGET_CPU = 1, BUDGET_WALL = 2 };
|
|
1690
|
+
|
|
1691
|
+
namespace detail {
|
|
1692
|
+
#if defined(_WIN32)
|
|
1693
|
+
namespace win { // 只声明用到的两个系统函数,不引入 <windows.h>
|
|
1694
|
+
extern "C" __declspec(dllimport) void* __stdcall GetCurrentThread();
|
|
1695
|
+
extern "C" __declspec(dllimport) int __stdcall QueryThreadCycleTime(void* thread, unsigned long long* cycles);
|
|
1696
|
+
} // namespace win
|
|
1697
|
+
inline constexpr bool BUDGET_CPU_AVAILABLE = true;
|
|
1698
|
+
#elif defined(CLOCK_THREAD_CPUTIME_ID)
|
|
1699
|
+
inline constexpr bool BUDGET_CPU_AVAILABLE = true;
|
|
1700
|
+
#else
|
|
1701
|
+
inline constexpr bool BUDGET_CPU_AVAILABLE = false;
|
|
1702
|
+
#endif
|
|
1703
|
+
|
|
1704
|
+
struct BudgetState {
|
|
1705
|
+
int kind = BUDGET_NONE; // 主循环设过没有、用哪把尺
|
|
1706
|
+
double per_ms = 1; // 读数的多少个单位算 1 毫秒
|
|
1707
|
+
double start = 0; // 本回合起点的读数
|
|
1708
|
+
double limit = 0; // 本回合的预算(毫秒)
|
|
1709
|
+
};
|
|
1710
|
+
inline BudgetState budget_state;
|
|
1711
|
+
|
|
1712
|
+
// 【缝 · bot 侧】当前读数,只增不减。BUDGET_CPU:调用它的这个线程从创建起消耗的 CPU 量
|
|
1713
|
+
// (Windows 为周期数,其他系统为纳秒);否则是墙上时钟的毫秒数。
|
|
1714
|
+
inline double budget_reading() {
|
|
1715
|
+
if (budget_state.kind == BUDGET_CPU) {
|
|
1716
|
+
#if defined(_WIN32)
|
|
1717
|
+
unsigned long long cycles = 0;
|
|
1718
|
+
win::QueryThreadCycleTime(win::GetCurrentThread(), &cycles);
|
|
1719
|
+
return double(cycles);
|
|
1720
|
+
#elif defined(CLOCK_THREAD_CPUTIME_ID)
|
|
1721
|
+
timespec ts{};
|
|
1722
|
+
clock_gettime(CLOCK_THREAD_CPUTIME_ID, &ts);
|
|
1723
|
+
return double(ts.tv_sec) * 1e9 + double(ts.tv_nsec);
|
|
1724
|
+
#endif
|
|
1725
|
+
}
|
|
1726
|
+
return std::chrono::duration<double, std::milli>(std::chrono::steady_clock::now().time_since_epoch()).count();
|
|
1727
|
+
}
|
|
1728
|
+
} // namespace detail
|
|
1729
|
+
|
|
1730
|
+
// 选尺子。由主循环在开局时调用一次;bot 不要调用。per_ms:BUDGET_CPU 下读数的多少个单位算 1 CPU 毫秒
|
|
1731
|
+
// (Windows 是裁判测定的每毫秒周期数,其他系统是 1000000)。这里没有 CPU 尺子可用时退回墙上时钟,返回实际用的那把。
|
|
1732
|
+
inline int budget_setup(int kind, double per_ms) {
|
|
1733
|
+
if (kind == BUDGET_CPU && detail::BUDGET_CPU_AVAILABLE && per_ms > 0) {
|
|
1734
|
+
detail::budget_state.kind = BUDGET_CPU;
|
|
1735
|
+
detail::budget_state.per_ms = per_ms;
|
|
1736
|
+
} else {
|
|
1737
|
+
detail::budget_state.kind = BUDGET_WALL;
|
|
1738
|
+
detail::budget_state.per_ms = 1;
|
|
1739
|
+
}
|
|
1740
|
+
return detail::budget_state.kind;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
// 设定本回合的起点(此刻)和预算。由主循环调用;bot 不要调用。
|
|
1744
|
+
inline void budget_begin(double limit_ms) {
|
|
1745
|
+
detail::budget_state.limit = limit_ms;
|
|
1746
|
+
detail::budget_state.start = detail::budget_reading();
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
// 本回合已经用掉多少毫秒。主循环没有设过时为 0。
|
|
1750
|
+
inline double budget_used() {
|
|
1751
|
+
if (detail::budget_state.kind == BUDGET_NONE) return 0.0;
|
|
1752
|
+
return (detail::budget_reading() - detail::budget_state.start) / detail::budget_state.per_ms;
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// 本回合还剩多少思考预算(毫秒,带小数;用超了是负数)。
|
|
1756
|
+
// 没有主循环时(比如自己写 main 直接调 decide 做实验)没有起点,返回一个很大的数。
|
|
1757
|
+
inline double budget_left() {
|
|
1758
|
+
if (detail::budget_state.kind == BUDGET_NONE) return 1e12;
|
|
1759
|
+
return detail::budget_state.limit - budget_used();
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
} // namespace tank
|