简介:项目源代码以全国大学生数字冰壶人工智能挑战赛为背景,同时提供强化学习与传统算法两套方案,面向参赛学生以及人工智能、自动化、物联网等专业的高校学习者。压缩包共10个文件,总大小约400KB,核心内容以Python和C++源码为主,辅以hdf5模型数据、md项目介绍、spec运行配置等,结构清晰,方便直接运行与二次开发。项目功能经过测试,配套说明与设计文档齐全,可作为课程设计、毕业设计或竞赛复现的起点,也可根据自身需求替换策略模块,实现不同投掷与决策逻辑。资源中已包含项目介绍文档,上手门槛较低,适合用于学习强化学习、传统路径规划与对抗策略的实战结合;代码中注释与结构设计兼顾可读性与扩展性,模型文件可直接加载运行,方便对照算法效果。目前已有113人学习下载,感兴趣的同学可以直接取用参考。
1. 数字冰壶挑战赛源码包:这不是游戏外挂,而是一套对抗决策系统
「全国大学生数字冰壶人工智能挑战赛」表面上是比谁的程序会投壶,实际比的是「给定当前全场局面,在几十秒内选出一个能扭转比分的一投」。这个源码包里的 Python 与 C++ 两份实现,正是为同一个目标服务的:Python 负责决策与数据解读,C++ 负责让仿真推演跑得更快。对参赛学生,它是「读完就能跑通完整流程」的脚手架;对正在做人工智能方向课设或项目的人,它也是一份少见的「模拟器 + 策略 + 回放验证」全链路范例。真正让多数队伍翻车的不是投不准,而是局面读不懂——这也是本文要从源码结构一路拆到验证的原因。
2. 拆源码包结构:Python 与 C++ 的分工、文件布局与最小运行闭环
2.1 zip 里大概率装着什么:按比赛流程推文件结构
拿到一个比赛源码包,先别急着编译。比赛类 zip 通常装着「官方模拟器接口说明 + 两套参考实现 + 历史对局记录」这三类东西。按数字冰壶的比赛流程:开局前读地图(营垒圆心、自由防守区位置),每轮双方交替投壶,AI 要拿到全部已落位壶的坐标,再给出「出射速度 + 出射角度」。所以源码包的结构大概率长这样:
digital_curling_challenge/ ├── README.md # 环境要求与运行顺序,第一个要读的文件 ├── docs/ # 比赛规则与模拟器协议说明 ├── data/ │ ├── map_meta.json # 冰面尺寸、营垒圆心坐标 │ └── replay_logs/ # 历史对局回放,用于离线分析 ├── src_python/ │ ├── strategy.py # 决策主循环:读局面、选策略 │ ├── simulator.py # 轻量轨迹模拟,用于快速试投 │ └── tools/ # 回放解析、结果统计 ├── src_cpp/ │ ├── physics.cpp # 滑行与碰撞的数值求解 │ ├── search.cpp # 蒙特卡洛树搜索 / 随机推演 │ └── CMakeLists.txt └── scripts/ ├── run_match.sh # 一键启一场练习赛 └── run_benchmark.sh # 批量离线测试对新手来说,第一优先读 README 与 docs 里的「协议说明」,而不是直接读算法。数字冰壶这类对抗 AI 里七成 bug 出在对数据格式的理解上:壶的坐标是米还是厘米、角度是弧度还是度、速度有没有归一化,这三处错一处,后面全白做。读包时我习惯按这个顺序翻:先看 README 里的运行命令,再看 docs 里的协议字段,最后才打开源码目录。如果发现 data/replay_logs 里有历史对局,那就赚到了——这是后面验证策略最重要的素材。
2.2 先跑通最小闭环:从 Python 主循环进入 C++ 计算
常见做法是保留官方的 Python 交互库做「收发包」,把计算密集型部分下沉到 C++。最小闭环不必一次打通所有策略,先验证「Python 能调起 C++ 算一个落点」就够。接法有两种:把 C++ 编译成可执行程序用 subprocess 调用,或者编译成动态库用 ctypes 加载。早期调试用 subprocess 更直观,日志一打就能看到输入输出;等需要频繁传大量坐标时再切成 ctypes,避免进程启动的开销。
# scripts/test_python_cpp_bridge.py import ctypes import math # 加载编译好的动态库 lib = ctypes.CDLL("./build/libphysics.so") # 声明 C++ 导出函数的参数与返回类型,避免类型不匹配 lib.simulate_rock.restype = ctypes.c_double lib.simulate_rock.argtypes = [ctypes.c_double, ctypes.c_double, ctypes.c_int] def landing_x(speed, angle_rad, rock_id=0): """调用 C++ 侧模拟器,返回最终落点的 x 坐标""" return lib.simulate_rock(speed, angle_rad, rock_id) if __name__ == "__main__": # 用 2.0 m/s、0 度角验证桥梁是否通 print("landing_x:", landing_x(2.0, 0.0))逻辑说明:CDLL加载的是编译出的共享库,restype与argtypes分别声明返回值和参数的 C 类型,缺了这行声明,Python 会默认按 32 位整数解析返回值,double 结果会被截断成垃圾数值。rock_id用于在 C++ 侧区分多颗壶的查询,最小验证时传 0 即可。
参数说明:speed单位要与 C++ 侧一致,通常比赛协议里会用 m/s;angle_rad是弧度制,如果官方接口给的是角度制,要在 bridge 层完成转换,不要在 C++ 内部反复换算。动态库路径用相对路径时,记得从项目根目录启动 Python,否则会报找不到 so 文件。
# build 目录下编译(假设源码在 src_cpp/) cd build && cmake .. && cmake --build . -j4编译命令说明:cmake ..读取源码包根目录的 CMakeLists,--build . -j4用 4 个并行任务加速编译。比赛机器若是老处理器,把-j4改成-j2更稳,编译时间多一点但不容易内存爆掉。
2.3 把环境配齐:Python 版本与 vscode 的 C++ 调试设置
很多队伍在队友机器上能跑,到自己机器上报错,问题基本出在环境不一致。Python 端固定 3.10 或 3.11,用 venv 建独立环境,numpy 等核心库锁版本;C++ 端需要支持 C++17 的编译器,Windows 用 MinGW 或 Visual Studio,Linux 用 g++。如果源码包给了 CMakeLists,优先用 CMake 而不是手写 Makefile,因为比赛后期要调编译选项,比如-march=native提升浮点性能。
{ "version": "2.0.0", "tasks": [ { "label": "build physics", "type": "shell", "command": "cd build && cmake .. && cmake --build .", "group": "build" } ] }vscode 用户在.vscode/tasks.json里配置这样一个编译任务,就能用快捷键触发 C++ 构建,而不必来回切终端。这里的group: build让任务出现在默认构建命令里,command里的命令与前面手敲的编译链保持一致,避免两套入口产生差异。
cmake_minimum_required(VERSION 3.16) project(curling_physics LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) add_library(physics SHARED src/physics.cpp) target_include_directories(physics PUBLIC include)CMake 参数说明:SHARED表示生成动态库,这样 Python 才能用 ctypes 加载;CMAKE_CXX_STANDARD 17开启 C++17,代码里才能用std::optional、结构化绑定这些特性。如果源码包里同时有多个 cpp 文件互相引用,记得把它们全部加进add_library,漏文件会导致链接阶段报未定义引用。
提示:vscode 里调试 C++ 时,如果断点打不上,先确认编译时有没有加
-g调试符号。Release 模式默认会优化掉局部变量,断点命中但变量看不到是正常现象。
这个环节最常见的翻车点,是在 Python 代码里直接 import 一个还没编译的扩展名。先确认build/libphysics.so真实存在,再谈运行。环境配置不是玄学,但「队友机器能跑、你的机器不行」十有八九就是这里。
3. 把投壶决策写成 Python 策略:从受力模型到路径选择
3.1 数字冰壶的物理模型:冰面摩擦、扫冰与碰撞简化的边界
数字冰壶与真实冰壶最大的不同在于:真实冰壶靠扫冰改变摩擦,而比赛仿真里「投掷后能否扫冰」由官方规则决定。多数大学生赛事的仿真器使用简化模型:壶在冰面上滑行,受到恒定摩擦或速度比例阻尼;壶与壶碰撞简化为圆-圆弹性碰撞。所以写策略前,先看官方文档怎么描述这几个关键参数:
| 参数 | 含义 | 常见取值区间 | 影响 |
|---|---|---|---|
| mu | 冰面动摩擦系数 | 0.01~0.05 | 决定减速快慢与最终停点 |
| e | 碰撞恢复系数 | 0.5~0.9 | 决定碰撞后两壶速度分配 |
| dt | 仿真步长 | 0.01~0.1 秒 | 决定积分精度与耗时 |
写代码前我有一条底线原则:不在函数里二选一,而是把 mu、e、dt 全部做成从配置读取的常量。宁可多写三行参数解析,也别把魔法数字写死在两处。后文第 5 章会看到,Python 与 C++ 结果不一致,多半是这两套常量复制粘贴后只改了一边。
如果官方规则支持扫冰,策略还需要额外输出扫冰强度;但很多赛题把扫冰做成「不可控」的固定值,只让 AI 决定出手参数。动手前先查协议里有没有sweep字段,没有就别给决策函数加这个输出,白加的输出会被判协议错误。
3.2 用 Python 实现「给定目标点求投掷参数」
策略里最基础也最常用的子问题是:已知当前所有静止壶的位置,想让新壶最终停在某个目标点 (tx, ty),求投掷速度 v 和角度 theta。由于摩擦力模型下壶的轨迹是直线(摩擦只减速不偏转),可以先用二分法反推 v,再用角度直接指向修正点。下面给一版可直接运行的最小实现:
import math MU = 0.02 # 冰面摩擦系数,来自官方文档或实测标定 G = 9.81 # 重力加速度,用于摩擦减速计算 DT = 0.02 # 积分步长,越小越准但越慢 def step(pos, vel, mu=MU, dt=DT): """一个时间步:速度按恒定摩擦减速,位置按平均速度推进""" speed = math.hypot(vel[0], vel[1]) dec = mu * G if speed - dec * dt <= 0: return pos, (0.0, 0.0) vx = vel[0] - dec * dt * (vel[0] / speed) vy = vel[1] - dec * dt * (vel[1] / speed) nx = pos[0] + (vel[0] + vx) / 2.0 * dt ny = pos[1] + (vel[1] + vy) / 2.0 * dt return (nx, ny), (vx, vy) def simulate(start, vel, n_steps=200): """返回最终停点与轨迹点序列""" pos, v = start, vel trajectory = [pos] for _ in range(n_steps): pos, v = step(pos, v) trajectory.append(pos) if math.hypot(*v) < 1e-6: break return pos, trajectory def solve_velocity(target, start=(0, 0), tolerance=0.005): """用二分法反推投掷速度,方向直接指向目标点""" lo, hi = 0.5, 5.0 angle = math.atan2(target[1] - start[1], target[0] - start[0]) while hi - lo > 1e-4: v_mid = (lo + hi) / 2.0 vel = (v_mid * math.cos(angle), v_mid * math.sin(angle)) end, _ = simulate(start, vel) if math.hypot(end[0] - target[0], end[1] - target[1]) < tolerance: return v_mid, angle if end[0] < target[0]: lo = v_mid else: hi = v_mid return (lo + hi) / 2.0, angle逻辑说明:simulate内部用半隐式推进,位置用步首与步末速度的平均值,比纯显式欧拉稳定;step里先算当前合速度方向,再把摩擦减速度分解到 x、y 两个轴,保证速度方向不因摩擦而偏转——这一点对冰壶很重要,因为真实冰壶的弧线来自旋转,简化模型里不应该出现。solve_velocity的二分方向依赖「速度越大落点越远」的单调性,对固定角度成立;当目标点位于起点正左方时,atan2给出的角度是 180 度,cos为负,二分判别条件需要按 x 方向单调性调整,或者把坐标系翻转后再求。
参数说明:tolerance=0.005是最终落点与目标的欧氏距离阈值,0.005 米(半厘米)已经远高于比赛判分需求,通常放宽到 0.02 能显著减少二分次数;n_steps=200对应 4 秒滑行时间,如果仿真器里壶从出手到停稳要 6 秒,就改成 300;lo/hi=0.5/5.0是速度搜索区间,按地图大小和官方出手上限调整,投掷距离越远上限越要放宽。
3.3 在决策循环里接入策略:典型 turn 函数骨架
有了「点到参数」的求解器,下一步是把它放进真正的比赛主循环。比赛协议通常一回合调用一次你的决策函数,传入当前局面列表,要求返回出手参数。我的做法是维护一个「预计算动作表」:赛前把所有候选目标点的速度和角度算好存下来,运行时只查表再按实时局面修正。
def decide_turn(my_rocks, opp_rocks, home_pos): """ my_rocks / opp_rocks: 已落位壶的 (x, y) 列表 home_pos: 本局营垒圆心坐标 返回 (speed, angle) """ # 1. 先看局面的得分状态:大本营内对方壶多,就转为击打 my_in = sum(1 for r in my_rocks if inside(r, home_pos, 1.8)) opp_in = sum(1 for r in opp_rocks if inside(r, home_pos, 1.8)) if opp_in > my_in: # 2. 击打目标选大本营里离圆心最近的对方壶 target = nearest_opponent_to_center(opp_rocks, home_pos) else: # 3. 己方占优时,把新壶补到圆心附近留作保护 target = (home_pos[0] + 0.3, home_pos[1] + 0.3) speed, angle = solve_velocity(target) return speed, angle这里inside是判断壶是否在大本营半径内的辅助函数,nearest_opponent_to_center按到圆心距离排序取最近者。这个骨架把「条件判断」和「轨迹求解」解耦:条件判断用规则,越简单越不容易错;轨迹求解交给物理模拟。等后期需要提升上限时,再把 if-else 替换成蒙特卡洛搜索,而solve_velocity可以原封不动作为搜索的落点估计器。
注意:
solve_velocity没有考虑「路上有别的壶会被撞开」。真正比赛里每次投掷前要把目标点旁边己方壶的位置也考虑进去,否则你会把自己队友的壶撞飞。碰撞处理放在第 4 章讲,策略层先保证「没人挡路」这一步正确。
4. 用 C++ 重写核心计算:性能提升与「双端一致」的代价
4.1 什么样的代码值得从 Python 搬到 C++
Python 版策略在限时内可以跑出几百组模拟,但如果要做蒙特卡洛树搜索、每回合推演几千局,就轮到 C++ 出场。典型要搬的三类代码:碰撞批处理(一回合里多次「投出新壶 → 撞飞旧壶 → 再静止」的推演)、随机局面采样(从当前局面生成大量未来分支的概率评估)、轨迹预计算(把整张冰面的网格目标点离线算成速度-角度表)。
不要搬的是纯 IO 和策略选择逻辑。决策树、局面标签这类代码留在 Python,改起来快,也方便打日志;把 C++ 当成高性能数值库来用,而不是把整个项目用 C++ 重写一遍。双端开发最忌讳「Python 里实现一次逻辑,C++ 里凭记忆再写一次」,同一份策略两处维护,改一处漏一处。
从性能预期上说,同样的物理循环,C++ 开启-O2后通常比 Python 快一两个数量级。但这不代表 C++ 方案就一定赢——如果每回合只需要 200 次推演,Python 的 0.2 秒和 C++ 的 0.01 秒在比赛里没有本质区别;真正值得付出双端成本的是每回合几千次以上的搜索型策略。
4.2 一个 C++ 投掷求解器的最小实现
C++ 侧的核心是一个和 Python 行为一致的模拟器。下面代码能处理「滑行 + 一次碰撞」的关键逻辑,也是动态库里simulate_rock函数的主体:
#include <cmath> #include <vector> struct Rock { double x = 0, y = 0, vx = 0, vy = 0; }; constexpr double MU = 0.02; constexpr double G = 9.81; constexpr double DT = 0.02; constexpr double RADIUS = 0.075; void step(Rock& r) { double speed = std::hypot(r.vx, r.vy); double dec = MU * G; if (speed - dec * DT > 0) { r.vx -= dec * DT * r.vx / speed; r.vy -= dec * DT * r.vy / speed; } else { r.vx = r.vy = 0; } // 与 Python 版保持一致:用更新后的速度推位置的平均速度法 r.x += (r.vx + (r.vx == 0 ? 0 : r.vx)) * DT / 2.0; r.y += (r.vy + (r.vy == 0 ? 0 : r.vy)) * DT / 2.0; }上面这段step的位置更新写法有点绕,正常的半隐式写法需要保留上一时刻的速度。为了不误导,实际建议这样写:
void step(Rock& r) { double speed = std::hypot(r.vx, r.vy); double dec = MU * G; double old_vx = r.vx, old_vy = r.vy; if (speed - dec * DT > 0) { r.vx -= dec * DT * r.vx / speed; r.vy -= dec * DT * r.vy / speed; } else { r.vx = r.vy = 0; } r.x += (old_vx + r.vx) / 2.0 * DT; r.y += (old_vy + r.vy) / 2.0 * DT; }这段代码与 Python 版step的差异只在「先把旧速度存下来」,物理行为完全一致。step返回后,碰撞检测与解算在collide函数里做:
void collide(Rock& a, Rock& b, double restitution = 0.85) { double dx = b.x - a.x, dy = b.y - a.y; double dist = std::hypot(dx, dy); if (dist > 2 * RADIUS) return; // 没接触就不处理 double nx = dx / dist, ny = dy / dist; double rvx = b.vx - a.vx, rvy = b.vy - a.vy; double vn = rvx * nx + rvy * ny; if (vn <= 0) return; // 相互远离时不处理,避免重复弹开 double imp = -(1 + restitution) * vn / 2.0; a.vx -= imp * nx; a.vy -= imp * ny; b.vx += imp * nx; b.vy += imp * ny; // 位置重叠修正:直接推到恰好接触,防止高速下穿透 double overlap = 2 * RADIUS - dist; a.x -= overlap / 2.0 * nx; a.y -= overlap / 2.0 * ny; b.x += overlap / 2.0 * nx; b.y += overlap / 2.0 * ny; }逻辑说明:collide先沿两壶连线方向求法向相对速度vn,只有接近时(vn > 0)才处理;冲量imp按恢复系数分配,restitution=0.85表示碰撞后保留 85% 的法向相对速度。位置修正那两行是为了解决「高速壶撞低速壶时,一个步长内可能重叠超过壶直径」的穿透问题,把重叠量对半分给两壶。
参数说明:restitution是碰撞恢复系数,取值 0.5~0.9。比赛现场如果发现撞飞后的落点明显偏大或偏小,优先调它而不是调摩擦系数;2 * RADIUS是两壶接触距离,本代码设壶半径为 0.075 米,对应真实冰壶半径,如果官方地图单位是厘米,记得换算。
4.3 双端一致性的三个约定:坐标、物理参数与随机数
Python 和 C++ 算出来的结果不一样,是混合架构里最常见的血泪来源。我的办法是把三件事固化成规范。
坐标约定:统一使用以冰壶起点为原点、沿投掷方向为正 x 的局部坐标系。传给 C++ 前完成坐标变换,C++ 内部不碰任何「全局 + 角度」换算,避免两套三角函数带来精度差。比赛协议给的是全局坐标,bridge 层负责把目标点换算到局部系,C++ 算完再把落点换算回去。
物理参数表:把 MU、G、DT、RADIUS、RESTITUTION 全部收进一个physics_config.json,Python 读它,C++ 通过解析函数也读它,谁都不许在源码里写死数值。双端一致性测试固定跑 50 组用例,比对落点误差,超过 1 厘米就报错:
def test_python_cpp_consistency(): for case in physics_cases: px, py = python_simulate(case) cx, cy = cpp_simulate(case) assert math.hypot(px - cx, py - cy) < 0.01, case逻辑说明:physics_cases是一组预先录制好的输入输出对,覆盖不同速度、角度、碰撞场景。断言里 0.01 单位是米,即双端误差不得超过 1 厘米。这个测试不是用来证明「谁更准」,而是防止改了一边参数忘了另一边。
随机数策略:蒙特卡洛用到的随机采样,C++ 用std::mt19937(seed),Python 用numpy.random.default_rng(seed),两边各固定种子。但要清楚它们生成的是不同序列,对比结果时必须在「同一组预先抽好的随机数文件」上做,而不是期待两端随机数流相同。比赛复现靠的是「记录种子 + 记录动作序列」,不是靠两端随机数一致性。
5. 实战避坑:5 个让我翻过车的数字冰壶细节
5.1 现象:同一参数在 Python 与 C++ 下落点差 0.3 米
原因:八成是物理参数不一致。最常见的是 Python 读的是一个配置文件,C++ 侧因为图省事把 MU 直接写在头文件里,后来只改了 Python 端配置,C++ 端还留着旧值。0.02 与 0.025 的摩擦系数差异,在 5 米滑行距离上能累计出约 0.3 米的落点偏移,这个量级足够把壶从大本营推到界外。解决:把双端物理常量统一收进physics_config.json,C++ 侧启动时也走文件读取,并且把 5.1 的差异测试加进 CI,每次改完跑一遍;跑不过就不准上线。
5.2 现象:本地强无敌,比赛模拟器里频繁出界
原因:本地验证用的模拟器太「温柔」。比如没有实现自由防守区规则,或者碰撞检测的步长太大导致漏检——高速壶穿过另一颗壶的瞬间没触发碰撞,轨迹完全错掉。解决:每次策略改动后,先用官方协议里的标准对局用例做回归;拿不到官方用例,就把自己的模拟器参数调保守:摩擦系数取文档上限、碰撞步长减半。这样能保证「更苛刻条件下也不出界」,而不是在本地模拟里自我感动。
5.3 现象:程序被判定超时,瓶颈根本不在策略复杂度
原因:决策超时往往发生在「赛前加载阶段」。比如 C++ 动态库初始化太慢、把整张轨迹预计算表放在每回合重建、或者是 Python 启动时重复 import 大模块。解决:把所有离线可算的内容放到程序启动阶段,比赛主循环里只做查表与轻量修正;C++ 侧对象用静态常驻内存,避免每回合反复 new/delete。这一条不改算法,只改生命周期,就能把单回合耗时从 2 秒压到 200 毫秒级。
5.4 现象:对手代码能跑,你的机器却报缺库
原因:Python 环境不一致。最常见是 numpy 版本与源码包要求不符,或 Windows 上缺 C++ 运行库,报错信息里出现msvcp140.dll not found。解决:严格按源码包 README 创建 venv 并执行pip install -r requirements.txt;C++ 侧用 CMake 拉取依赖后,把所有需要的 dll 拷贝到可执行文件同目录,避免靠系统 PATH 碰运气。vscode 里配置好 C/C++ 环境后,先跑一次官方示例确认工具链完整,再动自己的代码。
5.5 现象:复现比赛结果时,同一局走出了不同轨迹
原因:蒙特卡洛随机数没有固定种子,或者决策里隐含了未初始化变量。C++ 里常见的坑是定义了Rock r;后只给部分成员赋值,另一个成员带着栈上垃圾值参与运算。解决:全局只允许一个随机数源,每局开始时显式设置 seed 并登记到回放日志;再把每次决策的输入局面与输出参数也写进日志,赛后能精确重放。这既是查 bug 的后悔药,也是向组委会提交材料时的自证证据。
6. 用回放验证策略:三个指标判断你的 AI 是不是真变强了
6.1 回放日志怎么看:先盯这三个量
回放日志里字段很多,但我不建议一上来就看「得分」,比分容易被一两个运气球带偏。我一般先看三个更稳定的量,它们能直接指出策略的短板:
| 指标 | 计算方法 | 说明 |
|---|---|---|
| 落点精度 | 每次投掷停点与目标点距离的中位数 | 反映反推速度和角度是否稳定 |
| 先手有效性 | 前四壶里每壶能否留在自由防守区 | 开局策略是否过于激进 |
| 碰撞收益 | 击打后对方壶被清出大本营的数量 | 碰撞求解是否靠谱 |
这三个量可以在tools/analyze_replay.py里用几十行代码统计出来。我的习惯是每改一版策略,先离线跑 200 局回放,把三个指标分别和上一版对比:落点精度中位数提升 5 厘米以上才算有效改动,否则回滚。
6.2 最后一条经验:把「能跑」和「能赢」分开评价
源码包拿到手后的第一周,目标永远是「完整跑通一场比赛」;第二周之后,目标变成「在固定回放集上赢过上一版」。前者检验工程能力,后者检验策略质量。很多队伍把两天时间花在调碰撞系数上,却忘了先确认自己的决策是否在「该防守时防守、该击打时击打」。
我踩过最深的一次坑,是把所有算力都用在提高单壶落点精度上,结果 200 局回放里赢的局数反而少了——因为太追求落点,频繁尝试穿过对方壶堆的窄缝,失败率掩盖了精度收益。后来我把「落点精度」「先手有效性」「碰撞收益」三个指标固定每周复盘一次,策略改动先过回放再上比赛,整体胜率才稳定住。
现在我的习惯是:不管什么新点子,先让它在离线回放上把「能跑」这关过了,再谈上对抗。不管你是要交比赛代码,还是把这个源码包当作 Python 与 C++ 混合项目的人工智能实战参考,这条流程都通用。希望帮到你。
本文还有配套的精品资源,点击获取