保存时间:2026/3/29 15:55:41
不用一开始就完美编码,先“能解析、能输入”,后面再优化特征。
[占领金矿, 升级城堡, 招募步兵, 英雄A去右上, 英雄B守家, ...][部队1攻击目标X, 放魔法Y到区域Z, 等待, 撤退]AI 只输出“决策”,不输出“鼠标坐标/键盘按键”。引擎负责把“决策”翻译成具体操作(就像自动驾驶的“转向角”→“方向盘转动”)。
.sav.sav → State 向量回合开始 → 玩家操作 → 回合结束 → AI 操作 → ...get_state() → 返回当前 Stateexecute_action(action) → 执行 AI 输出的 Actionh3mparser、heroes3-save-parser 等(GitHub 可搜).sav → 提取 State(资源、兵力、城堡、英雄、天数等)CPlayer/CHumanPlayer 类,新增 CAIPlayer 类OnTurnStart() → 调用 AI 接口获取 StateOnTurnEnd() → 执行 AI 输出的 Action/get_state、/execute_actionreset() → 开始新游戏step(action) → 执行动作 → 返回 (next_state, reward, done, info)exploration_rate(探索率,越高越“菜”)mistake_rate(失误率,越高越“像人”)aggressiveness(激进程度)“AI 不需要知道具体操作逻辑,只知道结果好坏,用存盘作为训练数据,数据驱动,从结果入手。”
[State向量, Action, Reward, NextState, Done]State(t)(当前存盘)ΔState(t到t+1的变化)→ 映射为战略意图| 数据量级 | 对局质量 | 训练目标 | AI水平 | 体验 |
|---|---|---|---|---|
| 100局 | 普通玩家,风格杂乱 | 行为克隆(模仿人类) | 弱AI:会基本发育、占矿、造兵,偶尔犯傻 | 能玩,但很容易被针对,像新手 |
| 500局 | 同一玩家/风格相近玩家 | 行为克隆 + 简单分类 | 中弱AI:有明显风格(偏发育/偏rush),会基本扩张 | 像会玩的新手,能给普通玩家压力 |
| 2000局 | 高质量对局(高手/多风格) | 行为克隆 + 意图分类 + 简单RL | 中强AI:会资源管理、英雄分配、多线操作,能战胜大部分 casual 玩家 | 像有经验的老玩家,前期压制,中期胶着 |
| 10000局 | 高质量对局 + 自我对弈 | 模仿学习初始化 + PPO微调 | 强AI:会长期规划、骗招、翻盘,能战胜大部分普通玩家 | 像真人高手,有失误,有风格,后期给玩家机会 |
| 50000局+ | 海量自我对弈 + 人类数据 | 深度RL + 多智能体训练 | 顶尖AI:接近人类顶级水平,会针对玩家风格调整 | 像职业选手,动态难度,体验拉满 |
[局号, 回合数, State向量, 玩家ID, 胜负][State向量, 意图标签, 胜负]输入层:N 维(比如 64/128/256,从存盘解析来)
隐藏层 1:128 神经元,ReLU
隐藏层 2:64 神经元,ReLU
输出层:2 神经元,Softmax(赢/输概率)
损失:CrossEntropyLoss
优化:Adam(学习率 1e-3 ~ 1e-4)
输入层:N 维(同上)
隐藏层 1:256 神经元,ReLU
隐藏层 2:128 神经元,ReLU
输出层:5 神经元,Softmax
损失:CrossEntropyLoss
优化:Adam
libtorch/ 目录(包含 include/、lib/、share/)cmake_minimum_required(VERSION 3.18)
project(hero3_ai_mlp)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# LibTorch 路径
list(APPEND CMAKE_PREFIX_PATH "/path/to/libtorch")
find_package(Torch REQUIRED)
add_executable(hero3_ai main.cpp mlp_model.cpp dataset.cpp)
target_link_libraries(hero3_ai "${TORCH_LIBRARIES}")
# Windows 下需要复制 DLL
if(MSVC)
file(GLOB TORCH_DLLS "${TORCH_INSTALL_PREFIX}/lib/*.dll")
add_custom_command(TARGET hero3_ai POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${TORCH_DLLS}
$<TARGET_FILE_DIR:hero3_ai>)
endif()
feature_1,feature_2,...,feature_N,label(label=0 输,1 赢)feature_1,feature_2,...,feature_N,label(label=0~4 对应 5 类意图)Dataset 类加载 CSV(代码模板下面给)mlp_model.h#pragma once
#include <torch/torch.h>
struct MLPClassifier : torch::nn::Module {
MLPClassifier(int input_dim, int hidden_dim1, int hidden_dim2, int num_classes)
: fc1(register_module("fc1", torch::nn::Linear(input_dim, hidden_dim1))),
fc2(register_module("fc2", torch::nn::Linear(hidden_dim1, hidden_dim2))),
fc3(register_module("fc3", torch::nn::Linear(hidden_dim2, num_classes))),
relu(register_module("relu", torch::nn::ReLU())),
softmax(register_module("softmax", torch::nn::Softmax(1))) {}
torch::Tensor forward(torch::Tensor x) {
x = relu(fc1(x));
x = relu(fc2(x));
x = fc3(x);
return softmax(x);
}
torch::nn::Linear fc1, fc2, fc3;
torch::nn::ReLU relu;
torch::nn::Softmax softmax;
};
main.cpp(训练胜负预测,直接跑)#include "mlp_model.h"
#include "dataset.h" // 你自己写的 CSV 加载类
int main() {
// 1. 超参数(直接调)
const int input_dim = 64; // 存盘解析后的特征维度
const int hidden_dim1 = 128;
const int hidden_dim2 = 64;
const int num_classes = 2; // 胜负 2 分类
const int batch_size = 32;
const int epochs = 50;
const double lr = 1e-3;
// 2. 设备(CPU 先跑,后面可开 GPU)
torch::Device device(torch::kCPU);
// torch::Device device(torch::kCUDA); // 有 GPU 再开
// 3. 模型 + 优化器 + 损失
MLPClassifier model(input_dim, hidden_dim1, hidden_dim2, num_classes);
model->to(device);
torch::optim::Adam optimizer(model->parameters(), torch::optim::AdamOptions(lr));
torch::nn::CrossEntropyLoss criterion;
// 4. 数据加载(CSV → Dataset → DataLoader)
auto dataset = Hero3Dataset("train.csv").map(torch::data::transforms::Stack<>());
auto data_loader = torch::data::make_data_loader(
std::move(dataset),
torch::data::DataLoaderOptions().batch_size(batch_size)
);
// 5. 训练循环
for (int epoch = 0; epoch < epochs; ++epoch) {
model->train();
double running_loss = 0.0;
int correct = 0;
int total = 0;
for (auto& batch : *data_loader) {
auto data = batch.data.to(device);
auto labels = batch.target.to(device);
optimizer->zero_grad();
auto outputs = model->forward(data);
auto loss = criterion(outputs, labels);
loss->backward();
optimizer->step();
running_loss += loss->item<double>();
// 计算准确率
auto _, predicted = torch::max(outputs, 1);
total += labels.size(0);
correct += (predicted == labels).sum().item<int>();
}
double avg_loss = running_loss / total;
double acc = 100.0 * correct / total;
printf("Epoch %02d | Loss: %.4f | Acc: %.2f%%\n", epoch+1, avg_loss, acc);
}
// 6. 保存模型(.pt 文件,后面 VCMA 直接加载推理)
torch::save(model, "hero3_mlp_winloss.pt");
printf("Model saved to hero3_mlp_winloss.pt\n");
return 0;
}
inference.cpp(VCMA 里直接调用)#include "mlp_model.h"
#include <torch/torch.h>
// 加载模型
std::shared_ptr<MLPClassifier> load_model(const std::string& model_path, int input_dim, int num_classes) {
auto model = std::make_shared<MLPClassifier>(input_dim, 128, 64, num_classes);
torch::load(model, model_path);
model->eval(); // 推理模式
return model;
}
// 推理:输入特征向量 → 输出赢/输概率
torch::Tensor predict_winloss(std::shared_ptr<MLPClassifier> model, const std::vector<float>& features) {
torch::Device device(torch::kCPU);
torch::Tensor input = torch::tensor(features, torch::kFloat32).unsqueeze(0).to(device);
torch::NoGradGuard no_grad; // 推理禁用梯度
auto output = model->forward(input);
return output;
}
// 使用示例
int main() {
auto model = load_model("hero3_mlp_winloss.pt", 64, 2);
std::vector<float> state(64, 0.5f); // 从存盘解析来的 64 维特征
auto prob = predict_winloss(model, state);
printf("Win prob: %.2f%%, Lose prob: %.2f%%\n",
prob[0][1].item<float>()*100, prob[0][0].item<float>()*100);
return 0;
}
Hero3Dataset).pt 模型放进 VCMA 工程[0,1] 或 [-1,1],否则 MLP 不收敛x = (x - min) / (max - min)torch::nn::Dropout(0.2))# 基础编译工具
sudo apt update && sudo apt install -y build-essential cmake git
# LibTorch 依赖(可选,先装着,后面对比用)
sudo apt install -y libopenblas-dev liblapack-dev
# 可选:Python 仅用于生成模拟 CSV(不用学深度学习,只当数据工具)
sudo apt install -y python3 python3-pip
pip3 install pandas numpy
# 创建工作目录
mkdir -p ~/hero3_ai && cd ~/hero3_ai
# 下载 LibTorch 1.13.0(稳定版,适配 Ubuntu 22.04)
wget https://download.pytorch.org/libtorch/cpu/libtorch-cxx11-abi-shared-with-deps-1.13.0%2Bcpu.zip
unzip libtorch-cxx11-abi-shared-with-deps-1.13.0+cpu.zip
# 创建测试工程
mkdir -p libtorch_test && cd libtorch_test
# CMakeLists.txt
cat > CMakeLists.txt << 'EOF'
cmake_minimum_required(VERSION 3.18)
project(libtorch_test)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
list(APPEND CMAKE_PREFIX_PATH "$ENV{HOME}/hero3_ai/libtorch")
find_package(Torch REQUIRED)
add_executable(test_libtorch test.cpp)
target_link_libraries(test_libtorch "${TORCH_LIBRARIES}")
EOF
# test.cpp(极简张量操作)
cat > test.cpp << 'EOF'
#include <torch/torch.h>
#include <iostream>
int main() {
torch::Tensor a = torch::rand({2, 3});
torch::Tensor b = torch::rand({3, 2});
torch::Tensor c = torch::matmul(a, b);
std::cout << "a:\n" << a << "\n\nb:\n" << b << "\n\nc:\n" << c << std::endl;
return 0;
}
EOF
# 编译运行
mkdir build && cd build
cmake ..
make -j$(nproc)
./test_libtorch
| 维度范围 | 特征类型 | 模拟值范围 | 说明 |
|---|---|---|---|
| 0-7 | 资源 | 0~1(归一化) | 金、木、石、水银、硫磺、水晶、宝石、硫磺(8 种) |
| 8-15 | 城堡 | 0~1 | 城镇数量、城堡等级、国会、铁匠铺、兵营等(8 个) |
| 16-23 | 英雄 | 0~1 | 英雄数量、平均等级、总兵力、移动力、技能数等(8 个) |
| 24-31 | 地图 | 0~1 | 矿点占领数、巢穴数、探索率、关键点位控制(8 个) |
| 32-39 | 外交 | 0~1 | 结盟数、宣战数、交易次数(8 个,补 0 填充) |
| 40-47 | 全局 | 0~1 | 天数、回合数、玩家颜色、胜负状态(8 个) |
| 48-63 | 预留 | 0~1 | 扩展用(16 个,先填 0) |
[0, 1],避免 MLP 不收敛。sim_winloss.csv)f0,f1,...,f63,label(label=0 输,1 赢)cd ~/hero3_ai
cat > gen_sim_data.py << 'EOF'
import numpy as np
import pandas as pd
np.random.seed(42)
n_samples = 1000
n_features = 64
# 生成 0~1 随机特征
X = np.random.rand(n_samples, n_features)
# 定义胜负规则(模拟真实逻辑)
def get_label(row):
gold = row[0] # 金币
castle_lvl = row[9] # 城堡等级
army = row[18] # 总兵力
mines = row[24] # 矿点占领
# 赢:满足 2 条以上
win_cond = sum([gold > 0.5, castle_lvl > 0.6, army > 0.7, mines > 0.5]) >= 2
# 输:资源/兵力极低 或 不满足赢
lose_cond = (gold < 0.2 and army < 0.3) or not win_cond
return 1 if win_cond else 0
# 生成标签
y = np.apply_along_axis(get_label, 1, X)
# 保存 CSV
df = pd.DataFrame(X, columns=[f"f{i}" for i in range(n_features)])
df["label"] = y
df.to_csv("sim_winloss.csv", index=False)
print("sim_winloss.csv 生成完成,1000 行样本")
EOF
# 运行生成
python3 gen_sim_data.py
sim_intent.csv)f0,f1,...,f63,label(label=0~4 对应 5 类意图)cat > gen_sim_intent.py << 'EOF'
import numpy as np
import pandas as pd
np.random.seed(42)
n_samples = 1000
n_features = 64
X = np.random.rand(n_samples, n_features)
def get_intent(row):
gold = row[0]
castle_lvl = row[9]
army = row[18]
mines = row[24]
explore = row[26]
enemy = row[32]
if gold < 0.4 or castle_lvl < 0.5:
return 0 # 发育
elif explore < 0.3:
return 1 # 探索
elif mines < 0.4 and army > 0.5:
return 2 # 扩张
elif army > 0.7 and enemy > 0.5:
return 3 # 战斗
else:
return 4 # 运营
y = np.apply_along_axis(get_intent, 1, X)
df = pd.DataFrame(X, columns=[f"f{i}" for i in range(n_features)])
df["label"] = y
df.to_csv("sim_intent.csv", index=False)
print("sim_intent.csv 生成完成,1000 行样本")
EOF
python3 gen_sim_intent.py
sim_winloss.csv 和 sim_intent.csv,各 1000 行。~/hero3_ai/mlp_libtorch)mlp_libtorch/
├── CMakeLists.txt
├── main.cpp # 训练 + 推理入口
├── mlp_model.h # MLP 模型定义
├── dataset.h # CSV 数据加载
└── build/ # 编译目录
mlp_model.h#pragma once
#include <torch/torch.h>
struct MLP : torch::nn::Module {
MLP(int input_dim, int hidden1, int hidden2, int num_classes)
: fc1(register_module("fc1", torch::nn::Linear(input_dim, hidden1))),
fc2(register_module("fc2", torch::nn::Linear(hidden1, hidden2))),
fc3(register_module("fc3", torch::nn::Linear(hidden2, num_classes))),
relu(torch::nn::ReLU()),
softmax(torch::nn::Softmax(1)) {}
torch::Tensor forward(torch::Tensor x) {
x = relu(fc1(x));
x = relu(fc2(x));
x = fc3(x);
return softmax(x);
}
torch::nn::Linear fc1, fc2, fc3;
torch::nn::ReLU relu;
torch::nn::Softmax softmax;
};
dataset.h#pragma once
#include <torch/torch.h>
#include <vector>
#include <string>
#include <fstream>
#include <sstream>
struct CSVDataset : torch::data::Dataset<CSVDataset> {
CSVDataset(const std::string& csv_path, bool is_classification = true) {
std::ifstream file(csv_path);
std::string line, cell;
// 跳过表头
std::getline(file, line);
while (std::getline(file, line)) {
std::stringstream line_stream(line);
std::vector<float> row;
int label = 0;
// 读取特征
for (int i = 0; i < 64; ++i) {
std::getline(line_stream, cell, ',');
row.push_back(std::stof(cell));
}
// 读取标签
std::getline(line_stream, cell, ',');
label = std::stoi(cell);
// 转换为张量
torch::Tensor feat = torch::tensor(row, torch::kFloat32);
torch::Tensor lab = torch::tensor(label, torch::kInt64);
features_.push_back(feat);
labels_.push_back(lab);
}
}
torch::data::Example<> get(size_t index) override {
return {features_[index], labels_[index]};
}
torch::optional<size_t> size() const override {
return features_.size();
}
std::vector<torch::Tensor> features_;
std::vector<torch::Tensor> labels_;
};
CMakeLists.txtcmake_minimum_required(VERSION 3.18)
project(hero3_mlp_libtorch)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# LibTorch 路径
list(APPEND CMAKE_PREFIX_PATH "$ENV{HOME}/hero3_ai/libtorch")
find_package(Torch REQUIRED)
# 包含头文件
include_directories(.)
# 可执行文件
add_executable(mlp_train main.cpp)
target_link_libraries(mlp_train "${TORCH_LIBRARIES}")
# 编译选项
target_compile_options(mlp_train PRIVATE -Wall -Wextra -O2)
main.cpp(训练 + 推理一体)#include "mlp_model.h"
#include "dataset.h"
#include <iostream>
#include <string>
// 训练函数
void train(const std::string& csv_path, const std::string& model_save_path,
int input_dim, int num_classes, int epochs = 50, int batch_size = 32) {
// 设备
torch::Device device(torch::kCPU);
// 数据加载
auto dataset = CSVDataset(csv_path).map(torch::data::transforms::Stack<>());
auto data_loader = torch::data::make_data_loader(
std::move(dataset),
torch::data::DataLoaderOptions().batch_size(batch_size).shuffle(true)
);
// 模型 + 优化器 + 损失
MLP model(input_dim, 128, 64, num_classes);
model->to(device);
torch::optim::Adam optimizer(model->parameters(), torch::optim::AdamOptions(1e-3));
torch::nn::CrossEntropyLoss criterion;
// 训练循环
model->train();
for (int epoch = 0; epoch < epochs; ++epoch) {
double total_loss = 0.0;
int correct = 0, total = 0;
for (auto& batch : *data_loader) {
auto data = batch.data.to(device);
auto labels = batch.target.to(device);
optimizer->zero_grad();
auto outputs = model->forward(data);
auto loss = criterion(outputs, labels);
loss->backward();
optimizer->step();
total_loss += loss->item<double>();
// 计算准确率
auto _, pred = torch::max(outputs, 1);
correct += (pred == labels).sum().item<int>();
total += labels.size(0);
}
double avg_loss = total_loss / total;
double acc = 100.0 * correct / total;
printf("Epoch %02d | Loss: %.4f | Acc: %.2f%%\n", epoch+1, avg_loss, acc);
}
// 保存模型
torch::save(model, model_save_path);
printf("模型已保存到: %s\n", model_save_path.c_str());
}
// 推理函数
void infer(const std::string& model_path, int input_dim, int num_classes) {
// 加载模型
MLP model(input_dim, 128, 64, num_classes);
torch::load(model, model_path);
model->eval();
// 模拟输入(64 维 0~1 随机向量)
std::vector<float> input_vec(64);
for (int i = 0; i < 64; ++i) {
input_vec[i] = static_cast<float>(rand()) / RAND_MAX;
}
// 推理
torch::Tensor input = torch::tensor(input_vec, torch::kFloat32).unsqueeze(0);
torch::NoGradGuard no_grad;
auto output = model->forward(input);
auto prob = output.squeeze(0);
auto _, pred = torch::max(output, 1);
// 输出结果
printf("\n=== 推理结果 ===\n");
printf("输入向量前 5 维: %.3f, %.3f, %.3f, %.3f, %.3f\n",
input_vec[0], input_vec[1], input_vec[2], input_vec[3], input_vec[4]);
printf("预测类别: %d\n", pred.item<int>());
printf("各类别概率: ");
for (int i = 0; i < num_classes; ++i) {
printf("%.2f%% ", prob[i].item<float>() * 100);
}
printf("\n");
}
int main(int argc, char* argv[]) {
if (argc != 4) {
printf("用法: %s <任务类型: winloss/intent> <csv路径> <模型保存路径>\n", argv[0]);
printf("示例: %s winloss ../sim_winloss.csv winloss_model.pt\n", argv[0]);
return 1;
}
std::string task = argv[1];
std::string csv_path = argv[2];
std::string model_path = argv[3];
int input_dim = 64;
int num_classes = (task == "winloss") ? 2 : 5;
// 训练
printf("=== 开始训练 %s 模型 ===\n", task.c_str());
train(csv_path, model_path, input_dim, num_classes);
// 推理
printf("\n=== 开始推理 ===\n");
infer(model_path, input_dim, num_classes);
return 0;
}
# 创建工程目录
mkdir -p ~/hero3_ai/mlp_libtorch && cd ~/hero3_ai/mlp_libtorch
# 复制上面 4 个文件到当前目录
# 编译
mkdir build && cd build
cmake ..
make -j$(nproc)
# 训练胜负预测模型
./mlp_train winloss ../../sim_winloss.csv winloss_model.pt
# 训练意图预测模型
./mlp_train intent ../../sim_intent.csv intent_model.pt
winloss_model.pt 和 intent_model.pt// mlp_plain.h(纯 0 依赖 MLP)
#pragma once
#include <vector>
#include <cmath>
#include <cstdlib>
#include <ctime>
#include <algorithm>
class PlainMLP {
public:
PlainMLP(const std::vector<int>& layers) : layers_(layers) {
srand(time(0));
// 初始化权重和偏置
for (int i = 0; i < layers.size() - 1; ++i) {
std::vector<std::vector<float>> w(layers[i], std::vector<float>(layers[i+1]));
std::vector<float> b(layers[i+1], 0.0f);
// 随机初始化权重 [-0.5, 0.5]
for (auto& row : w) {
for (auto& val : row) {
val = static_cast<float>(rand()) / RAND_MAX - 0.5f;
}
}
weights_.push_back(w);
biases_.push_back(b);
}
}
// 前向传播
std::vector<float> forward(const std::vector<float>& x) {
std::vector<float> a = x;
for (int i = 0; i < weights_.size(); ++i) {
std::vector<float> z(layers_[i+1], 0.0f);
// 矩阵乘法 + 偏置
for (int j = 0; j < layers_[i+1]; ++j) {
for (int k = 0; k < layers_[i]; ++k) {
z[j] += a[k] * weights_[i][k][j];
}
z[j] += biases_[i][j];
}
// ReLU 激活(除输出层)
a.resize(layers_[i+1]);
for (int j = 0; j < layers_[i+1]; ++j) {
a[j] = (i == weights_.size()-1) ? z[j] : std::max(0.0f, z[j]);
}
}
// Softmax 输出层
softmax(a);
return a;
}
// 训练(SGD,简化版)
void train(const std::vector<std::vector<float>>& X, const std::vector<int>& y,
int epochs = 100, float lr = 0.01f) {
int n_samples = X.size();
for (int epoch = 0; epoch < epochs; ++epoch) {
float total_loss = 0.0f;
int correct = 0;
// 随机打乱
std::vector<int> idx(n_samples);
for (int i = 0; i < n_samples; ++i) idx[i] = i;
std::random_shuffle(idx.begin(), idx.end());
for (int i : idx) {
// 前向
std::vector<float> pred = forward(X[i]);
// 损失(交叉熵)
total_loss += -std::log(pred[y[i]] + 1e-8f);
// 准确率
int p = std::max_element(pred.begin(), pred.end()) - pred.begin();
if (p == y[i]) correct++;
// 反向传播(简化版,仅输出层梯度)
std::vector<float> delta = pred;
delta[y[i]] -= 1.0f;
// 更新权重和偏置
for (int j = 0; j < layers_.back(); ++j) {
biases_.back()[j] -= lr * delta[j];
for (int k = 0; k < layers_[layers_.size()-2]; ++k) {
weights_.back()[k][j] -= lr * delta[j] * last_activation_[k];
}
}
}
float avg_loss = total_loss / n_samples;
float acc = 100.0f * correct / n_samples;
printf("Epoch %03d | Loss: %.4f | Acc: %.2f%%\n", epoch+1, avg_loss, acc);
}
}
private:
std::vector<int> layers_;
std::vector<std::vector<std::vector<float>>> weights_;
std::vector<std::vector<float>> biases_;
std::vector<float> last_activation_;
void softmax(std::vector<float>& x) {
float max_val = *std::max_element(x.begin(), x.end());
float sum = 0.0f;
for (auto& val : x) {
val = std::exp(val - max_val);
sum += val;
}
for (auto& val : x) {
val /= sum;
}
}
};
mkdir -p ~/hero3_ai/mlp_plain && cd ~/hero3_ai/mlp_plain
# 编写 main.cpp(加载 CSV + 训练 + 推理)
# 编译:g++ -std=c++17 main.cpp -o mlp_plain -O2
# 运行:./mlp_plain
sim_winloss.csv + sim_intent.csv)[0, 1],否则 MLP 不收敛.pt 模型并推理)| 维度 | Wesnoth (Battle for Wesnoth) | VCMA (英雄无敌3 重制) | 对你的影响 |
|---|---|---|---|
| 存盘格式 | 明文/半结构化(WML)+ 二进制混合,文档齐全,社区有成熟 parser | 二进制闭兼容格式(英雄无敌3 原始存盘),解析依赖逆向,文档极少 | Wesnoth 存盘解析成本低一个数量级,你不用花大量时间逆向 |
| AI 接口 | 原生支持 AI 玩家、Lua 脚本、C++ 插件,有成熟的 ai/ 模块,可直接替换决策逻辑 | 基于原始 H3 引擎重构,AI 是硬编码,没有标准化接口,需要自己改源码加接口 | Wesnoth 可以直接“插 AI”,不用从零造接口 |
| 状态复杂度 | 战术为主,战略层简单(资源、城镇、单位、地形),状态空间远小于 H3 | 战略+战术深度极高(英雄、魔法、宝物、资源链、多线运营),状态爆炸 | Wesnoth 更容易训练出“可用 AI”,不会一开始就被状态复杂度卡死 |
| C++ 工程 | 现代 C++(C++17+),CMake 规范,模块化清晰,编译友好 | 偏老式 C++,依赖 SDL,代码耦合度高,编译/调试更繁琐 | Wesnoth 更容易嵌入你的 LibTorch MLP |
| 社区与资料 | 活跃开源社区,大量 AI 模组、存盘解析工具、文档 | 小众重制社区,资料零散,AI 相关几乎空白 | 遇到问题更容易搜到解决方案 |
| 玩法相似性 | 回合制战棋,资源+单位+地形+城镇,和 H3 战略层逻辑高度同源 | 纯正英雄无敌3,玩法深度更高,但上手成本高 | Wesnoth 练手,学到的经验可 1:1 迁移到 H3 |
.gz 压缩,解压后是 WML(Wesnoth Markup Language)):[game]:全局状态(回合数、玩家、地图)[side]:玩家势力(资源、单位、城镇、科技)[unit]:单位(位置、血量、攻击、防御、经验、状态)[tile]:地图地形(移动成本、防御加成)[event]:触发状态(任务、剧情).gz),解压看 WML 结构。ai/ 模块,找到 ai_default.cpp 等决策文件。gold、food、village 占领数、unit 数量与位置、回合数ΔState(比如“村数从 3→4”“金币从 100→50”“兵力从 10→15”)直接对应战略动作(占村、招兵、移动)[玩家操作 / AI 决策]
↓
[战略层:模型(MLP/LLM/RL)]
- 输入:Wesnoth 存盘状态向量(金币、村数、兵力、回合数…)
- 输出:战略意图(招兵/占村/进攻/防守/移动)
↓
[战术层:原生 Wesnoth 引擎]
- 执行:战斗、单位移动、技能释放、地形计算
- 输出:战斗结果、兵力变化、村占领状态
↓
[存盘更新 → 下回合战略决策]
.gz → .txt,WML 格式)状态向量(32 维足够):
- 0-3: 金币、粮食、村数、回合数(归一化到 [0,1])
- 4-11: 各类型单位数量(剑士、弓箭手、骑兵…)
- 12-19: 敌方单位数量、村数、金币
- 20-31: 预留(地图探索度、关键点位控制等)
[状态向量, 战略意图标签, 胜负]src/ai/ai_default.cpp、src/ai/ai_context.cpp)// 伪代码:Wesnoth AI 决策入口
void ai_default::do_turn(ai_context& ctx) {
// 1. 提取当前战略状态(从 ctx 直接读,比存盘更快)
vector<float> state = extract_strategy_state(ctx);
// 2. MLP 推理:输出战略意图
int intent = mlp_infer(state); // 0=招兵,1=占村,2=进攻,3=防守,4=移动
// 3. 执行战略意图(调用 Wesnoth 原生 API)
switch(intent) {
case 0: recruit_units(ctx); break; // 招兵
case 1: capture_village(ctx); break; // 占村
case 2: attack_enemy(ctx); break; // 进攻
case 3: defend_base(ctx); break; // 防守
case 4: move_hero(ctx); break; // 移动
}
}
输入向量 (d) → 线性层 (d → h1) → ReLU → 线性层 (h1 → h2) → ReLU → ... → 输出层 (h_k → c)
W 和偏置 b 组成。Attention(Q, K, V) = Softmax(QK^T / √d_k) V
输入序列 (n, d) → 嵌入 + 位置编码 → 多头注意力 → 前馈网络 (FFN = MLP) → ... → 输出
| 维度 | MLP | Transformer | 对你的战略层 AI 的影响 |
|---|---|---|---|
| 输入 | 固定维度向量 | 变长序列 / 向量 | 战略层状态是固定维度向量,MLP 完美匹配 |
| 数据量 | 小~中(百~千) | 大~极大(万~百万) | 你只有几百~几千局存盘,MLP 刚好 |
| 训练速度 | 快(秒~分钟) | 慢(小时~天) | 预研阶段要快速迭代,MLP 完胜 |
| 推理速度 | 极快(μs 级) | 慢(ms~s 级) | 游戏回合必须实时,MLP 无压力 |
| 算力消耗 | 极低(CPU 足矣) | 极高(GPU 必须) | 你是游戏 AI,不是数据中心,MLP 合适 |
| 部署 | 极轻(几 KB~MB) | 极重(几十 MB~GB) | 嵌入 VCMA/Wesnoth,MLP 无缝集成 |
| 稳定性 | 极高(几乎必收敛) | 低(易不收敛) | 预研要确定性结果,MLP 可靠 |
| 能力上限 | 中(适合简单~中等复杂映射) | 极高(适合复杂序列 / 长程依赖) | 战略层复杂度中等,MLP 足够 |
| 实现难度 | 低(C++ 可手写) | 高(需成熟框架) | 你要快速验证 pipeline,MLP 最优 |
当前资源 + 兵力 + 据点 + 回合 → 应该做什么(招兵/占村/进攻/防守)