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,392 @@
|
|
|
1
|
+
// bot.cpp —— 坦克大战(策略型,不做搜索)
|
|
2
|
+
//
|
|
3
|
+
// 上半场的教训:带 max-min 搜索的版本每回合烧 26 CPU 毫秒却排在基准之下,而成绩表里
|
|
4
|
+
// 第 5 名每回合只用 0.5 毫秒。这个游戏的胜负手是"攻城竞速 + 星星等级差",是长期规划
|
|
5
|
+
// 问题,2~3 层的战术搜索看不见它,反而让车变得畏缩(任何动作都被假想的全知对手惩罚)。
|
|
6
|
+
// 所以这一版完全不做搜索:分工 + 明确的优先级规则,每回合只用几十微秒。
|
|
7
|
+
//
|
|
8
|
+
// 三条主线:
|
|
9
|
+
// 攻城 —— 一辆车去对方基地的炮位(<=2 格且炮线通 = 每回合稳定 1 点伤害),路上先用炮开砖。
|
|
10
|
+
// 防守 —— 对方有车瞄着我方基地时,派一辆车去挡炮线 / 打那辆车。
|
|
11
|
+
// 争星 —— 场上有星就派一辆确实更近的车去吃;2 级快速型(5 血、不装填、两步)能碾压 1 血车。
|
|
12
|
+
#include "tank.hpp"
|
|
13
|
+
|
|
14
|
+
#include <algorithm>
|
|
15
|
+
#include <cstring>
|
|
16
|
+
|
|
17
|
+
using namespace tank;
|
|
18
|
+
|
|
19
|
+
static const int INF = 9999;
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------- 场
|
|
22
|
+
static int g_datt[2][CELLS]; // [seat] 到"能打到对方基地的炮位"的代价(砖算 3 步)
|
|
23
|
+
static int g_dsup[CELLS]; // 到最近补给点的步数
|
|
24
|
+
static int g_dsp[MAX_SUPPLY][CELLS]; // 到第 k 个补给点的步数(分工时按点分配,避免都挤一个点)
|
|
25
|
+
static int g_tgt[6]; // R_STAR 的车去哪个补给点
|
|
26
|
+
static int g_dhunt[2][CELLS]; // [seat] 到最近敌方坦克的步数
|
|
27
|
+
static int g_ddef[2][CELLS]; // [seat] 到拦截点(挡炮线 / 打攻城车)的步数
|
|
28
|
+
static bool g_need_def[2];
|
|
29
|
+
static int g_attacker_dist[2]; // [seat] 对方攻城车到我方基地的距离(没有则 INF)
|
|
30
|
+
static unsigned char g_threat[CELLS]; // 本回合会有子弹飞到这格
|
|
31
|
+
static unsigned char g_exp[2][CELLS]; // [seat] 这格在对方能开炮的车的炮线上
|
|
32
|
+
static unsigned char g_mine[CELLS]; // 这格有我方车
|
|
33
|
+
static bool g_has_star;
|
|
34
|
+
|
|
35
|
+
enum Role { R_SIEGE = 0, R_DEF, R_STAR, R_HUNT };
|
|
36
|
+
static int g_role[6];
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------- 基础几何
|
|
39
|
+
// 连坦克一起看:从 pos 到 base 的炮线是否通,返回距离。
|
|
40
|
+
static int shot_now(const State& s, int pos, int base) {
|
|
41
|
+
int best = INF;
|
|
42
|
+
for (int d = 0; d < 4; ++d) {
|
|
43
|
+
int c = pos, n = 0;
|
|
44
|
+
for (;;) {
|
|
45
|
+
c += DIR_DELTA[d];
|
|
46
|
+
if (++n > 8) break;
|
|
47
|
+
if (c == base) { if (n < best) best = n; break; }
|
|
48
|
+
const uint8_t g = s.grid[c];
|
|
49
|
+
if (g != EMPTY && g != WATER) break;
|
|
50
|
+
if (tank_at(s, c) >= 0) break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return best;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// 只看地形:从 pos 打到 base 的攻城代价(最多允许一堵砖),打不到返回 INF。
|
|
57
|
+
static int ray_cost(const State& s, int pos, int base) {
|
|
58
|
+
int best = INF;
|
|
59
|
+
for (int d = 0; d < 4; ++d) {
|
|
60
|
+
int c = pos, n = 0, nb = 0;
|
|
61
|
+
for (;;) {
|
|
62
|
+
c += DIR_DELTA[d];
|
|
63
|
+
if (++n > 5) break;
|
|
64
|
+
if (c == base) {
|
|
65
|
+
const int cost = 3 * nb + 2 * (n > 2 ? n - 2 : 0);
|
|
66
|
+
if (cost < best) best = cost;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
const uint8_t g = s.grid[c];
|
|
70
|
+
if (g == EMPTY || g == WATER) continue;
|
|
71
|
+
if (g == BRICK) { if (++nb > 1) break; continue; }
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return best;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------- 场计算
|
|
79
|
+
static void bfs_cost(const State& s, const unsigned char* src, int* dist) {
|
|
80
|
+
static int q[CELLS];
|
|
81
|
+
for (int c = 0; c < CELLS; ++c) dist[c] = INF;
|
|
82
|
+
int head = 0, tail = 0;
|
|
83
|
+
for (int c = 0; c < CELLS; ++c)
|
|
84
|
+
if (src[c]) { dist[c] = 0; q[tail++] = c; }
|
|
85
|
+
while (head < tail) {
|
|
86
|
+
const int c = q[head++];
|
|
87
|
+
for (int k = 0; k < 4; ++k) {
|
|
88
|
+
const int nb = c + DIR_DELTA[k];
|
|
89
|
+
const uint8_t g = s.grid[nb];
|
|
90
|
+
if (g != EMPTY && g != BRICK) continue;
|
|
91
|
+
if (dist[nb] > dist[c] + 1) { dist[nb] = dist[c] + 1; q[tail++] = nb; }
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static void compute_fields(const State& s, int me) {
|
|
97
|
+
static unsigned char src[CELLS];
|
|
98
|
+
for (int c = 0; c < CELLS; ++c) {
|
|
99
|
+
g_threat[c] = 0; g_exp[0][c] = 0; g_exp[1][c] = 0; g_mine[c] = 0; src[c] = 0;
|
|
100
|
+
}
|
|
101
|
+
for (int i = 0; i < 6; ++i)
|
|
102
|
+
if (s.tank[i].alive && seat_of(i) == me) g_mine[s.tank[i].pos] = 1;
|
|
103
|
+
|
|
104
|
+
// 已在飞的子弹:本回合两个小步会飞到哪
|
|
105
|
+
for (int k = 0; k < s.n_bullets; ++k) {
|
|
106
|
+
const Bullet& b = s.bullet[k];
|
|
107
|
+
int c = b.pos;
|
|
108
|
+
for (int st = 0; st < 2; ++st) {
|
|
109
|
+
c += DIR_DELTA[b.dir];
|
|
110
|
+
if (c < 0 || c >= CELLS) break;
|
|
111
|
+
g_threat[c] = 1;
|
|
112
|
+
const uint8_t g = s.grid[c];
|
|
113
|
+
if (g != EMPTY && g != WATER) break;
|
|
114
|
+
if (tank_at(s, c) >= 0) break;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
// 对方能开炮的车,炮线前 5 格是危险格(站上去就是面对面互射,子弹互相抵消、白送节奏)
|
|
118
|
+
for (int i = 0; i < 6; ++i) {
|
|
119
|
+
if (!s.tank[i].alive || !can_fire(s, i)) continue;
|
|
120
|
+
const int victim = 1 - seat_of(i);
|
|
121
|
+
for (int d = 0; d < 4; ++d) {
|
|
122
|
+
int c = s.tank[i].pos;
|
|
123
|
+
for (int n = 0; n < 5; ++n) {
|
|
124
|
+
c += DIR_DELTA[d];
|
|
125
|
+
if (c < 0 || c >= CELLS) break;
|
|
126
|
+
const uint8_t g = s.grid[c];
|
|
127
|
+
if (g == BRICK || g == STEEL || g == OUTSIDE || g == BASE0 || g == BASE1) break;
|
|
128
|
+
g_exp[victim][c] = 1;
|
|
129
|
+
if (tank_at(s, c) >= 0) break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 攻城场:炮位 = 从这格沿某方向最多 5 格、途中最多一堵砖就能打到对方基地
|
|
135
|
+
for (int seat = 0; seat < 2; ++seat) {
|
|
136
|
+
const int base = s.base_pos[1 - seat];
|
|
137
|
+
for (int c = 0; c < CELLS; ++c) src[c] = 0;
|
|
138
|
+
const int n = s.P.size;
|
|
139
|
+
for (int y = 0; y < n; ++y)
|
|
140
|
+
for (int x = 0; x < n; ++x) {
|
|
141
|
+
const int c = cell_of(x, y);
|
|
142
|
+
if (s.grid[c] != EMPTY) continue;
|
|
143
|
+
if (ray_cost(s, c, base) < INF) src[c] = 1;
|
|
144
|
+
}
|
|
145
|
+
bfs_cost(s, src, g_datt[seat]);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// 补给点场:既算"到最近补给点",也算"到第 k 个补给点"
|
|
149
|
+
for (int c = 0; c < CELLS; ++c) src[c] = 0;
|
|
150
|
+
for (int k = 0; k < s.n_supply; ++k) src[s.supply[k]] = 1;
|
|
151
|
+
bfs_cost(s, src, g_dsup);
|
|
152
|
+
for (int k = 0; k < s.n_supply; ++k) {
|
|
153
|
+
for (int c = 0; c < CELLS; ++c) src[c] = 0;
|
|
154
|
+
src[s.supply[k]] = 1;
|
|
155
|
+
bfs_cost(s, src, g_dsp[k]);
|
|
156
|
+
}
|
|
157
|
+
g_has_star = s.star_mask != 0;
|
|
158
|
+
|
|
159
|
+
// 猎杀场:到最近的敌方坦克
|
|
160
|
+
for (int seat = 0; seat < 2; ++seat) {
|
|
161
|
+
for (int c = 0; c < CELLS; ++c) src[c] = 0;
|
|
162
|
+
for (int i = 0; i < 6; ++i)
|
|
163
|
+
if (s.tank[i].alive && seat_of(i) != seat) src[s.tank[i].pos] = 1;
|
|
164
|
+
bfs_cost(s, src, g_dhunt[seat]);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// 拦截场:对方有车瞄着 seat 的基地时,把那些车和它们炮线上的格子当目标
|
|
168
|
+
for (int seat = 0; seat < 2; ++seat) {
|
|
169
|
+
for (int c = 0; c < CELLS; ++c) src[c] = 0;
|
|
170
|
+
const int mybase = s.base_pos[seat];
|
|
171
|
+
int nd = 0, bestd = INF;
|
|
172
|
+
for (int i = 0; i < 6; ++i) {
|
|
173
|
+
if (!s.tank[i].alive || seat_of(i) == seat) continue;
|
|
174
|
+
const int d = shot_now(s, s.tank[i].pos, mybase);
|
|
175
|
+
if (d > 5) continue;
|
|
176
|
+
++nd;
|
|
177
|
+
if (d < bestd) bestd = d;
|
|
178
|
+
src[s.tank[i].pos] = 1;
|
|
179
|
+
for (int k = 0; k < 4; ++k) { // 炮线上、我和它之间的空格:站上去就挡住这一炮
|
|
180
|
+
int c = s.tank[i].pos, n = 0;
|
|
181
|
+
for (;;) {
|
|
182
|
+
c += DIR_DELTA[k];
|
|
183
|
+
if (++n >= d) break;
|
|
184
|
+
if (s.grid[c] != EMPTY) break;
|
|
185
|
+
src[c] = 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
g_need_def[seat] = nd > 0;
|
|
190
|
+
g_attacker_dist[seat] = bestd;
|
|
191
|
+
if (nd) bfs_cost(s, src, g_ddef[seat]);
|
|
192
|
+
else for (int c = 0; c < CELLS; ++c) g_ddef[seat][c] = INF;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ---------------------------------------------------------------- 打分
|
|
197
|
+
// 这一炮打出去会打到什么。
|
|
198
|
+
static int fire_score(const State& s, int i, int d) {
|
|
199
|
+
const int me = seat_of(i), foe = 1 - me;
|
|
200
|
+
int c = s.tank[i].pos;
|
|
201
|
+
int n = 0, bricks = 0;
|
|
202
|
+
for (;;) {
|
|
203
|
+
c += DIR_DELTA[d];
|
|
204
|
+
if (++n > 16) break;
|
|
205
|
+
const int v = tank_at(s, c);
|
|
206
|
+
if (v >= 0) {
|
|
207
|
+
if (seat_of(v) != me) {
|
|
208
|
+
int sc = 900 - 40 * n;
|
|
209
|
+
if (s.tank[v].hp <= 1) sc += 500; // 一发带走
|
|
210
|
+
if (s.tank[v].level >= 1) sc += 200; // 打掉对面的等级更值
|
|
211
|
+
return sc;
|
|
212
|
+
}
|
|
213
|
+
return -4000; // 打队友
|
|
214
|
+
}
|
|
215
|
+
if (c == s.base_pos[foe]) return bricks == 0 ? (20000 - 300 * n) : 1200;
|
|
216
|
+
if (c == s.base_pos[me]) return -20000; // 打自己基地
|
|
217
|
+
const uint8_t g = s.grid[c];
|
|
218
|
+
if (g == BRICK) {
|
|
219
|
+
if (++bricks > 1) return 40;
|
|
220
|
+
// 打掉这堵砖之后炮线就通了?继续往前看能不能到对方基地
|
|
221
|
+
int cc = c, nn = n;
|
|
222
|
+
for (;;) {
|
|
223
|
+
cc += DIR_DELTA[d];
|
|
224
|
+
if (++nn > 6) break;
|
|
225
|
+
if (cc == s.base_pos[foe]) return 1700 - 120 * nn;
|
|
226
|
+
if (cc == s.base_pos[me]) return -3000;
|
|
227
|
+
const uint8_t g2 = s.grid[cc];
|
|
228
|
+
if (g2 == EMPTY || g2 == WATER) continue;
|
|
229
|
+
if (tank_at(s, cc) >= 0) break;
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
return 90;
|
|
233
|
+
}
|
|
234
|
+
if (g == STEEL) return (s.tank[i].level >= 2) ? 60 : -60;
|
|
235
|
+
if (g == OUTSIDE) return -30;
|
|
236
|
+
// 空地 / 水:继续飞
|
|
237
|
+
}
|
|
238
|
+
return -20;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
static inline int goal_dist(const State& s, int i, int c) {
|
|
242
|
+
int g;
|
|
243
|
+
switch (g_role[i]) {
|
|
244
|
+
case R_DEF: g = g_ddef[seat_of(i)][c]; break;
|
|
245
|
+
case R_STAR: g = (g_tgt[i] < s.n_supply) ? g_dsp[g_tgt[i]][c] : g_dsup[c]; break;
|
|
246
|
+
case R_HUNT: g = g_dhunt[seat_of(i)][c]; break;
|
|
247
|
+
default: g = g_datt[seat_of(i)][c]; break;
|
|
248
|
+
}
|
|
249
|
+
if (g >= INF) g = 120;
|
|
250
|
+
if (g > 120) g = 120;
|
|
251
|
+
return g;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
static int score_cmd(const State& s, int i, Cmd cmd) {
|
|
255
|
+
const Tank& t = s.tank[i];
|
|
256
|
+
const int me = seat_of(i);
|
|
257
|
+
|
|
258
|
+
if (is_fire(cmd)) return fire_score(s, i, cmd - 5);
|
|
259
|
+
|
|
260
|
+
if (cmd == STAY) {
|
|
261
|
+
int sc = 0;
|
|
262
|
+
if (g_threat[t.pos]) sc -= 700;
|
|
263
|
+
if (g_exp[me][t.pos]) sc -= 90;
|
|
264
|
+
if (g_has_star && star_at(s, t.pos) && t.level < s.P.max_level) sc += 3000;
|
|
265
|
+
// 已经占住补给点、就等这批星出在脚下:别动
|
|
266
|
+
if (g_role[i] == R_STAR && g_tgt[i] < s.n_supply && t.pos == s.supply[g_tgt[i]]) sc += 1500;
|
|
267
|
+
return sc;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const int d1 = is_move(cmd) ? cmd - 1 : (cmd - 9) >> 2;
|
|
271
|
+
const int p = t.pos;
|
|
272
|
+
const int p1 = p + DIR_DELTA[d1];
|
|
273
|
+
int mid = p1, fin = p1;
|
|
274
|
+
if (s.grid[p1] != EMPTY) { mid = p; fin = p; } // 第一步走不成
|
|
275
|
+
if (is_move2(cmd)) {
|
|
276
|
+
const int d2 = (cmd - 9) & 3;
|
|
277
|
+
const int p2 = mid + DIR_DELTA[d2];
|
|
278
|
+
if (s.grid[p2] == EMPTY) fin = p2;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
int sc = 130 * (goal_dist(s, i, p) - goal_dist(s, i, fin));
|
|
282
|
+
|
|
283
|
+
if (g_threat[fin]) sc -= 900; // 走进子弹的落点(必中,一定要躲)
|
|
284
|
+
if (g_threat[mid]) sc -= 500;
|
|
285
|
+
if (g_threat[p]) sc += 400; // 离开子弹的落点
|
|
286
|
+
// 面对面互射 = 子弹互相抵消、谁也不赚。但这只是"不划算",不能压过推进:
|
|
287
|
+
// 上半场就是这一项给得太大,结果三辆车在原地横着挪、永远到不了对方基地。
|
|
288
|
+
if (g_exp[me][fin]) sc -= 0;
|
|
289
|
+
if (g_exp[me][p]) sc += 0;
|
|
290
|
+
|
|
291
|
+
if (g_mine[fin]) sc -= 200; // 和队友抢格(会双双走不成)
|
|
292
|
+
if (g_has_star && star_at(s, fin) && t.level < s.P.max_level) sc += 2500;
|
|
293
|
+
if (is_move2(cmd) && fin != mid) sc += 25; // 快速型一回合两步
|
|
294
|
+
return sc;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// ---------------------------------------------------------------- 分工
|
|
298
|
+
// 下一批星星还要几个回合末才出(已经出在场上则返回 0)。
|
|
299
|
+
static int star_wait(const State& s) {
|
|
300
|
+
if (s.star_mask) return 0;
|
|
301
|
+
const int nt = s.turn + 1;
|
|
302
|
+
if (nt < s.P.supply_first) return s.P.supply_first - nt;
|
|
303
|
+
const int k = (nt - s.P.supply_first) % s.P.supply_period;
|
|
304
|
+
return k == 0 ? 0 : s.P.supply_period - k;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
static void assign_roles(const State& s, int me) {
|
|
308
|
+
for (int k = 0; k < 6; ++k) { g_role[k] = R_HUNT; g_tgt[k] = 0; }
|
|
309
|
+
int alive[3], na = 0;
|
|
310
|
+
for (int t = 0; t < 3; ++t) {
|
|
311
|
+
const int i = tank_id(me, t);
|
|
312
|
+
if (s.tank[i].alive) alive[na++] = i;
|
|
313
|
+
}
|
|
314
|
+
if (na == 0) return;
|
|
315
|
+
|
|
316
|
+
auto tt = [&](int i, const int* field) {
|
|
317
|
+
int d = field[s.tank[i].pos];
|
|
318
|
+
if (d >= INF) d = 200;
|
|
319
|
+
return double(d) / s.P.types[type_of(i)].speed;
|
|
320
|
+
};
|
|
321
|
+
bool used[6] = {};
|
|
322
|
+
|
|
323
|
+
// ---- 1) 攻城永远先留一辆 ----
|
|
324
|
+
// 基地每回合最多掉 1 血,两边都在攻城就是平局节奏;只有我攻城、对方不攻才是赢。
|
|
325
|
+
// 所以哪怕基地正挨打,也必须留一辆去啃对方基地,否则会输在基地血量这个第一顺位比较项上。
|
|
326
|
+
{
|
|
327
|
+
int best = -1; double bv = 1e18;
|
|
328
|
+
for (int k = 0; k < na; ++k) { const int i = alive[k]; const double v = tt(i, g_datt[me]); if (v < bv) { bv = v; best = i; } }
|
|
329
|
+
if (best >= 0) { g_role[best] = R_SIEGE; used[best] = true; }
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ---- 2) 防守:对方有车瞄着我方基地 ----
|
|
333
|
+
if (g_need_def[me]) {
|
|
334
|
+
int best = -1; double bv = 1e18;
|
|
335
|
+
for (int k = 0; k < na; ++k) { const int i = alive[k]; if (used[i]) continue; const double v = tt(i, g_ddef[me]); if (v < bv) { bv = v; best = i; } }
|
|
336
|
+
if (best >= 0) { g_role[best] = R_DEF; used[best] = true; }
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ---- 3) 争星:只用剩下的一辆 ----
|
|
340
|
+
// 这是本局的胜负手(2 级车能碾压 1 血车),而且必须按"点"分配,否则几辆车挤同一个点。
|
|
341
|
+
// 没出星但一批快到了也要提前占位:出星那回合站在点上,下一回合末就吃到。
|
|
342
|
+
const int wait = star_wait(s);
|
|
343
|
+
if (wait <= 12) {
|
|
344
|
+
int nk = 0, ks[MAX_SUPPLY];
|
|
345
|
+
for (int k = 0; k < s.n_supply; ++k) {
|
|
346
|
+
const bool has = (s.star_mask >> k) & 1;
|
|
347
|
+
if (has || wait <= 12) ks[nk++] = k;
|
|
348
|
+
}
|
|
349
|
+
bool taken[MAX_SUPPLY] = {}; int sent = 0;
|
|
350
|
+
for (int a = 0; a < na; ++a) {
|
|
351
|
+
const int i = alive[a];
|
|
352
|
+
if (used[i] || s.tank[i].level >= s.P.max_level) continue;
|
|
353
|
+
int bk = -1; double bv = 1e18;
|
|
354
|
+
for (int q = 0; q < nk; ++q) {
|
|
355
|
+
const int k = ks[q];
|
|
356
|
+
if (taken[k]) continue;
|
|
357
|
+
const double v = tt(i, g_dsp[k]);
|
|
358
|
+
const double adj = ((s.star_mask >> k) & 1) ? v : v + 1.0; // 已出星的点更急
|
|
359
|
+
if (adj < bv) { bv = adj; bk = k; }
|
|
360
|
+
}
|
|
361
|
+
if (bk < 0) continue;
|
|
362
|
+
taken[bk] = true;
|
|
363
|
+
g_role[i] = R_STAR; g_tgt[i] = bk; used[i] = true;
|
|
364
|
+
if (++sent >= 2) break;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
// 剩下的:也去攻城(打基地才是胜负),别在中路和对面换命
|
|
368
|
+
for (int k = 0; k < na; ++k) if (!used[alive[k]]) g_role[alive[k]] = R_SIEGE;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ---------------------------------------------------------------- 对外接口
|
|
372
|
+
void init(const State& s, int me) { (void)s; (void)me; }
|
|
373
|
+
|
|
374
|
+
Orders decide(const State& s, int me) {
|
|
375
|
+
Orders orders;
|
|
376
|
+
compute_fields(s, me);
|
|
377
|
+
assign_roles(s, me);
|
|
378
|
+
|
|
379
|
+
for (int t = 0; t < 3; ++t) {
|
|
380
|
+
const int i = tank_id(me, t);
|
|
381
|
+
if (!s.tank[i].alive) { orders.cmd[t] = STAY; continue; }
|
|
382
|
+
Cmd list[N_CMDS];
|
|
383
|
+
const int n = legal_commands(s, i, list);
|
|
384
|
+
int bestk = 0, bestsc = score_cmd(s, i, list[0]);
|
|
385
|
+
for (int k = 1; k < n; ++k) {
|
|
386
|
+
const int sc = score_cmd(s, i, list[k]);
|
|
387
|
+
if (sc > bestsc) { bestsc = sc; bestk = k; }
|
|
388
|
+
}
|
|
389
|
+
orders.cmd[t] = list[bestk];
|
|
390
|
+
}
|
|
391
|
+
return orders;
|
|
392
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# 复现正式赛
|
|
2
|
+
|
|
3
|
+
| 文件 | 内容 |
|
|
4
|
+
|---|---|
|
|
5
|
+
| `tournament.py`、`bt.py`、`schedule.py` | 这一集的对战平台:冻结赛程、编译、绑核、逐批测速、可中断续跑、Bradley–Terry 评分。和比赛时逐字节相同。它是为 1 对 1 重写的版本,和仓库根目录 `arena/`(第 3 集起的多人平台)不是同一份 |
|
|
6
|
+
| `halftime.json`、`final.json`、`progress.json` | 中场赛、终场赛、进步赛的配置。程序路径换成了仓库里的相对路径,`seed` 写的是当时抽到的赛程种子,去掉了本机编译器的路径(不写时先看环境变量 `CXX`,再找 `PATH` 里的 g++;只把 w64devkit 放进仓库 `tools/` 的,先设好 `CXX`,见 `../README.md` 的"马上看 / 马上玩")。名字(`Astra`、`Opus@1` 等)没有改,赛程按名字排序,改了就不是同一份赛程 |
|
|
7
|
+
| `sparring.cpp` | 出题方参照:出题方写的搜索型 bot,成绩表里的 `sparring`。这是比赛时实际编译的文件(源码前面由赛事脚本加了 4 个宏,固定每回合的搜索量,所以它的结果不随机器状态变化)。比赛时不发给选手 |
|
|
8
|
+
|
|
9
|
+
在本目录运行(终场赛在比赛电脑上约 70 分钟;可以随时中断,再运行同一条命令接着跑):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
python tournament.py plan final.json # 冻结赛程,写到 run-final/赛程.json
|
|
13
|
+
python tournament.py run run-final # 开打
|
|
14
|
+
python tournament.py rate run-final # 出成绩:run-final/成绩.md
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
中场赛、进步赛把 `final` 换成 `halftime`、`progress`。进步赛的进步幅度要和中场赛合并拟合,比赛时由主办方的赛事脚本完成,算法见 `../results/附表.md` 的说明。
|
|
18
|
+
|
|
19
|
+
需要注意:
|
|
20
|
+
|
|
21
|
+
- **选手包目录必须是纯英文路径**:`tournament.py` 会拒绝含中文等非 ASCII 字符的 `kit` 路径(Windows 上的 g++ 在某些区域设置下打不开这种路径)。仓库放在中文路径下时,把整个 `tank/` 复制到纯英文路径再跑。
|
|
22
|
+
- 地图、座位、对阵可以一模一样地重建:开源前用这三份配置重建赛程,`games_sha` 和原赛程完全相同。
|
|
23
|
+
- 逐局结果不一定完全相同。对局本身没有随机,但很多 bot 按 `budget_left()` 决定搜到哪里停,机器状态不同搜得深浅就不同。开源前在比赛电脑上重打终场赛的前 352 局,338 局逐回合相同,347 局胜负相同。总排名应当一致,相邻并列的名次可能互换。
|
|
24
|
+
- 默认只用大核、每局两个 bot 各占一个物理核(`pin: big`),同时打几局由本机的核数决定。`python tournament.py cpus final.json` 可以先看绑核方案。用量的上限是 50 CPU 毫秒,机器太忙会让按时间停的 bot 搜得更浅。
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"""Bradley–Terry 实力值(1 对 1)。
|
|
2
|
+
|
|
3
|
+
口径:
|
|
4
|
+
- 胜记 1,平记 0.5;
|
|
5
|
+
- 每对交过手的程序加一局虚拟平局(全胜、全负时实力值不会趋于无穷);
|
|
6
|
+
- 基准 = 0,实力强 10 倍记 +400(和 Elo 同一把尺:差 400 分 ≈ 10:1);弱于基准是负数;
|
|
7
|
+
- 区间:按地图整块重抽样(同一张图上的所有对局一起抽,一起放回),每次重新拟合,取 2.5% 和 97.5% 分位;
|
|
8
|
+
- 并列:按实力值排好之后,相邻两名“实力值之差”的 95% 区间含 0 就算并列(并列可以连成一串)。
|
|
9
|
+
差的区间是在每次重抽样里直接算差得到的。不用“两人各自的区间是否重叠”来判:各自的区间里还含着基准自身的噪声,
|
|
10
|
+
两个人对基准的偏差又是一起动的,用它判并列偏保守(同样的数据,能分开的差距大约要大一倍)。
|
|
11
|
+
|
|
12
|
+
输入的一局是 {"map": 地图的标识, "a": 名字, "b": 名字, "score": a 的得分(1 / 0.5 / 0)}。
|
|
13
|
+
"map" 可以是任何可哈希的值;几场比赛合在一起拟合时,用 (哪一场, 图号) 这样的元组,重抽样会在每一场内部各抽各的。
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import math
|
|
18
|
+
import random
|
|
19
|
+
from collections import defaultdict
|
|
20
|
+
|
|
21
|
+
SCALE = 400 / math.log(10)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def tally(rows):
|
|
25
|
+
"""按地图汇总:{地图: {(a, b): [局数, a 的得分]}},其中 a < b。"""
|
|
26
|
+
by_map = defaultdict(lambda: defaultdict(lambda: [0.0, 0.0]))
|
|
27
|
+
for r in rows:
|
|
28
|
+
a, b, s = r["a"], r["b"], float(r["score"])
|
|
29
|
+
if a == b:
|
|
30
|
+
continue
|
|
31
|
+
if a > b:
|
|
32
|
+
a, b, s = b, a, 1.0 - s
|
|
33
|
+
cell = by_map[r["map"]][a, b]
|
|
34
|
+
cell[0] += 1.0
|
|
35
|
+
cell[1] += s
|
|
36
|
+
return by_map
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def merge(by_map, maps):
|
|
40
|
+
"""把若干张图(可重复)的汇总加在一起。"""
|
|
41
|
+
total = defaultdict(lambda: [0.0, 0.0])
|
|
42
|
+
for m in maps:
|
|
43
|
+
for pair, (g, w) in by_map[m].items():
|
|
44
|
+
cell = total[pair]
|
|
45
|
+
cell[0] += g
|
|
46
|
+
cell[1] += w
|
|
47
|
+
return total
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def fit(pairs, names, anchor, virtual=1.0, start=None, tol=1e-9, max_iter=200000):
|
|
51
|
+
"""pairs:{(a, b): [局数, a 的得分]}。返回 {名字: 实力值};没有任何对局的名字是 None。
|
|
52
|
+
|
|
53
|
+
virtual:每对交过手的程序加几局虚拟平局。start:上一次的解(对数实力),用来热启动。"""
|
|
54
|
+
games = {}
|
|
55
|
+
for (a, b), (g, w) in pairs.items():
|
|
56
|
+
if g > 0:
|
|
57
|
+
games[a, b] = (g + virtual, w + virtual / 2)
|
|
58
|
+
active = sorted({n for pair in games for n in pair})
|
|
59
|
+
if anchor not in active:
|
|
60
|
+
raise ValueError(f"基准 {anchor} 没有任何对局,定不了零点")
|
|
61
|
+
_check_connected(games, active)
|
|
62
|
+
wins = dict.fromkeys(active, 0.0)
|
|
63
|
+
edges = []
|
|
64
|
+
for (a, b), (g, w) in games.items():
|
|
65
|
+
wins[a] += w
|
|
66
|
+
wins[b] += g - w
|
|
67
|
+
edges.append((a, b, g))
|
|
68
|
+
p = {n: math.exp((start or {}).get(n) or 0.0) for n in active}
|
|
69
|
+
for _ in range(max_iter):
|
|
70
|
+
denom = dict.fromkeys(active, 0.0)
|
|
71
|
+
for a, b, g in edges:
|
|
72
|
+
v = g / (p[a] + p[b])
|
|
73
|
+
denom[a] += v
|
|
74
|
+
denom[b] += v
|
|
75
|
+
new = {n: wins[n] / denom[n] for n in active}
|
|
76
|
+
center = sum(math.log(v) for v in new.values()) / len(new)
|
|
77
|
+
scale = math.exp(-center)
|
|
78
|
+
delta = 0.0
|
|
79
|
+
for n in active:
|
|
80
|
+
new[n] *= scale
|
|
81
|
+
delta = max(delta, abs(math.log(new[n] / p[n])))
|
|
82
|
+
p = new
|
|
83
|
+
if delta < tol:
|
|
84
|
+
break
|
|
85
|
+
origin = math.log(p[anchor])
|
|
86
|
+
out = dict.fromkeys(names)
|
|
87
|
+
for n in active:
|
|
88
|
+
out[n] = SCALE * (math.log(p[n]) - origin)
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _check_connected(games, active):
|
|
93
|
+
"""对战关系必须连成一片,否则几块之间的实力值没法比。"""
|
|
94
|
+
parent = {n: n for n in active}
|
|
95
|
+
|
|
96
|
+
def find(x):
|
|
97
|
+
while parent[x] != x:
|
|
98
|
+
parent[x] = parent[parent[x]]
|
|
99
|
+
x = parent[x]
|
|
100
|
+
return x
|
|
101
|
+
|
|
102
|
+
for a, b in games:
|
|
103
|
+
parent[find(a)] = find(b)
|
|
104
|
+
roots = {find(n) for n in active}
|
|
105
|
+
if len(roots) > 1:
|
|
106
|
+
raise ValueError(f"对战关系分成了 {len(roots)} 块,互相之间没有交手,没法放在一把尺上")
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def rate(rows, names, anchor, boot=1000, seed=0, virtual=1.0, diffs=None, stats=None):
|
|
110
|
+
"""返回 {名字: {"rating", "low", "high"}}(没有对局的名字三项都是 None),以及实际重抽样的次数。
|
|
111
|
+
|
|
112
|
+
区间按地图整块重抽样。地图标识是元组时,按元组的第一项分层(每一层内部各抽各的,层的图数不变)。
|
|
113
|
+
diffs:[(a, b), ...]。给了它就多返回一项 {(a, b): {"diff", "low", "high"}},是 a 减 b 的实力值之差和它的区间
|
|
114
|
+
(在每次重抽样里算差,比两个区间相减准)。
|
|
115
|
+
stats:{键: 函数}。函数收到一次拟合的结果 {名字: 实力值},返回一个数(算不了返回 None),比如"几个程序实力值的标准差"。
|
|
116
|
+
给了它就在最后多返回一项 {键: {"value", "low", "high", "sd", "n"}}:value 是用全部数据算的,low / high 是重抽样的
|
|
117
|
+
95% 区间,sd 是这个数在重抽样里的标准差(也就是它的抽样误差),n 是算出来的次数。"""
|
|
118
|
+
by_map = tally(rows)
|
|
119
|
+
maps = list(by_map)
|
|
120
|
+
point = fit(merge(by_map, maps), names, anchor, virtual)
|
|
121
|
+
start = {n: v / SCALE for n, v in point.items() if v is not None}
|
|
122
|
+
strata = defaultdict(list)
|
|
123
|
+
for m in maps:
|
|
124
|
+
strata[m[0] if isinstance(m, tuple) else None].append(m)
|
|
125
|
+
rng = random.Random(seed)
|
|
126
|
+
samples = defaultdict(list)
|
|
127
|
+
done = 0
|
|
128
|
+
for _ in range(boot if len(maps) >= 2 else 0):
|
|
129
|
+
pick = []
|
|
130
|
+
for group in strata.values():
|
|
131
|
+
pick += rng.choices(group, k=len(group))
|
|
132
|
+
try:
|
|
133
|
+
est = fit(merge(by_map, pick), names, anchor, virtual, start=start, tol=1e-7)
|
|
134
|
+
except ValueError:
|
|
135
|
+
continue # 这次抽到的图里基准没有出场或者对战关系断开了(只在图极少时会发生)
|
|
136
|
+
done += 1
|
|
137
|
+
for n, v in est.items():
|
|
138
|
+
if v is not None:
|
|
139
|
+
samples[n].append(v)
|
|
140
|
+
for a, b in diffs or ():
|
|
141
|
+
if est.get(a) is not None and est.get(b) is not None:
|
|
142
|
+
samples[a, b].append(est[a] - est[b])
|
|
143
|
+
for key, fn in (stats or {}).items():
|
|
144
|
+
value = fn(est)
|
|
145
|
+
if value is not None:
|
|
146
|
+
samples["stat", key].append(value)
|
|
147
|
+
|
|
148
|
+
def interval(key, v):
|
|
149
|
+
s = sorted(samples[key])
|
|
150
|
+
if len(s) < 20:
|
|
151
|
+
return None, None
|
|
152
|
+
low, high = s[int(0.025 * (len(s) - 1))], s[int(math.ceil(0.975 * (len(s) - 1)))]
|
|
153
|
+
return min(low, v), max(high, v)
|
|
154
|
+
|
|
155
|
+
out = {}
|
|
156
|
+
for n in names:
|
|
157
|
+
v = point.get(n)
|
|
158
|
+
if v is None:
|
|
159
|
+
out[n] = {"rating": None, "low": None, "high": None}
|
|
160
|
+
continue
|
|
161
|
+
low, high = interval(n, v)
|
|
162
|
+
out[n] = {"rating": v, "low": low, "high": high}
|
|
163
|
+
stat_out = {}
|
|
164
|
+
for key, fn in (stats or {}).items():
|
|
165
|
+
value = fn(point)
|
|
166
|
+
s = samples["stat", key]
|
|
167
|
+
low, high = interval(("stat", key), value) if value is not None else (None, None)
|
|
168
|
+
sd = None
|
|
169
|
+
if len(s) >= 20:
|
|
170
|
+
mean = sum(s) / len(s)
|
|
171
|
+
sd = math.sqrt(sum((x - mean) ** 2 for x in s) / (len(s) - 1))
|
|
172
|
+
stat_out[key] = {"value": value, "low": low, "high": high, "sd": sd, "n": len(s)}
|
|
173
|
+
if diffs is None:
|
|
174
|
+
return (out, done) if stats is None else (out, done, stat_out)
|
|
175
|
+
diff_out = {}
|
|
176
|
+
for a, b in diffs:
|
|
177
|
+
if point.get(a) is None or point.get(b) is None:
|
|
178
|
+
diff_out[a, b] = {"diff": None, "low": None, "high": None}
|
|
179
|
+
continue
|
|
180
|
+
d = point[a] - point[b]
|
|
181
|
+
low, high = interval((a, b), d)
|
|
182
|
+
diff_out[a, b] = {"diff": d, "low": low, "high": high}
|
|
183
|
+
return (out, done, diff_out) if stats is None else (out, done, diff_out, stat_out)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def tie_groups_by_diff(order, diffs):
|
|
187
|
+
"""order:按实力值从高到低排好的名字。diffs:rate(..., diffs=相邻的各对) 返回的 {(高的, 低的): {"diff", "low", "high"}}。
|
|
188
|
+
|
|
189
|
+
相邻两名之差的 95% 区间含 0(下界 ≤ 0)就并列;并列可以连成一串。差的区间算不出来(图太少)时不并列。
|
|
190
|
+
返回 [[一组并列的名字], ...]。"""
|
|
191
|
+
groups = []
|
|
192
|
+
for n in order:
|
|
193
|
+
if groups:
|
|
194
|
+
d = diffs.get((groups[-1][-1], n))
|
|
195
|
+
if d is not None and d["low"] is not None and d["low"] <= 0.0:
|
|
196
|
+
groups[-1].append(n)
|
|
197
|
+
continue
|
|
198
|
+
groups.append([n])
|
|
199
|
+
return groups
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def tie_groups(order, ratings):
|
|
203
|
+
"""旧判法,成绩表已经不用它,留着做对比:相邻两名各自的区间有重叠就并列。返回 [[一组并列的名字], ...]。"""
|
|
204
|
+
groups = []
|
|
205
|
+
for n in order:
|
|
206
|
+
r = ratings[n]
|
|
207
|
+
if groups:
|
|
208
|
+
prev = ratings[groups[-1][-1]]
|
|
209
|
+
if None not in (r["low"], r["high"], prev["low"], prev["high"]) and r["high"] >= prev["low"]:
|
|
210
|
+
groups[-1].append(n)
|
|
211
|
+
continue
|
|
212
|
+
groups.append([n])
|
|
213
|
+
return groups
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def expected(ra, rb):
|
|
217
|
+
"""实力值 ra 的程序对 rb 的程序的期望得分率。"""
|
|
218
|
+
return 1.0 / (1.0 + 10 ** ((rb - ra) / 400.0))
|