feat: 完成肉鸽 copilot

This commit is contained in:
MistEO
2022-06-18 22:58:46 +08:00
parent 68c75b087a
commit d4e1a32199
11 changed files with 256 additions and 360 deletions

View File

@@ -1,65 +1,26 @@
# 肉鸽战斗流程协议
`resource/roguelike/*.json` 的使用方法及各字段说明
`resource/roguelike_copilot.json` 的使用方法及各字段说明
## 完整字段一览
```jsonc
{
"stage_name": "驯兽小屋", // 关卡名,必选
"actions": [ // 战斗中的操作。有序,执行完前一个才会去执行下一个。必选
{
"type": "部署", // 操作类型,可选,默认 "Deploy"
// "Deploy" | "Skill" | "Retreat" | "AllSkill"
// "部署" | "技能" | "撤退" | "全部技能"
// 中英文皆可,效果相同
// 若为 "部署", 当费用不够时,会一直等待到费用够
// 若为 "技能", 当技能 cd 没转好时,会直接跳过当前项。
// 考虑不同干员技能效果及 cd 差异巨大,谨慎使用(例如 山 2 技能 这种,假如已经开了,你又开一次,就给关了)
// "全部技能", 会一次性使用所有 skill_usgae == 3 的干员的技能
"kills": 0, // 击杀数条件,如果没达到就一直等待。可选,默认为 0。直接执行
// TODO: 其他条件
// TODO: "condition_type": 0, // 执行条件间的关系,可选,默认 0
// // 0 - 且; 1 - 或
"roles": [ // 该位置使用的职业。可选,默认为空,任何职业都可以
// 有序,会先使用靠前的职业
// 会根据位置类型自动区分是高台还是地面。若设置了不可放置的职业,则忽略该职业
// 如果这里面的职业一个都没有,则会跳过该位置转而执行下一条
"近卫",
"先锋",
"重装",
"召唤物"
],
"waiting_cost": false, // 如果有 roles 中有靠前的职业,但费用不够,是否等待。可选,默认 false
// 为 true 时,会使用 roles 中,当前有的、最靠前的职业(一直等他费用够)
// 为 false 时,会使用 roles 中,当前有的、费用够的中最靠前的职业
"location": [ 5, 5 ], // 操作的干员的位置。必选
"direction": "左", // 部署干员的干员朝向。 type 为 "部署" 时必选
// "Left" | "Right" | "Up" | "Down" | "None"
// "左" | "右" | "上" | "下" | "无"
// 中英文皆可,效果相同
"pre_delay": 0, // 前置延时。可选,默认 0, 单位毫秒
"rear_delay": 0, // 后置延时。可选,默认 0, 单位毫秒
"timeout": 999999999 // 超时时间。当 type 为 "部署" 时可选。默认 INT_MAX, 单位毫秒
// 等待超时则放弃当前动作, 转而执行下一个动作
},
{ // 简单举例
"location": [6, 6],
"roles": [
"近卫",
"先锋",
"重装",
"召唤物"
],
"direction": "左"
}
]
}
[
{
"stage_name": "驯兽小屋", // 关卡名,必选
"replacement_home": [ // 替换掉家门的位置,可选,默认为空
[ 2, 8 ] // 会使用这里面的坐标来代替蓝门的坐标,然后堵门
],
"key_kills": [ // 关键击杀数,可选,默认为空
15, // 若不为空,干员技能会攒着,不是好了就放,而是等到击杀数达到时再释放
... // 当跑完数组里所有的元素之后,再把干员技能变成好了就放
]
},
{
"stage_name": "巡逻队",
"replacement_home": [
[ 2, 8 ]
]
}
]
```

View File

@@ -1,28 +0,0 @@
{
"stage_name": "意外",
"actions": [
{
"location": [
5,
3
],
"roles": [
"近卫",
"先锋",
"重装",
"召唤物"
],
"direction": "左"
},
{
"location": [
5,
2
],
"roles": [
"辅助"
],
"direction": "左"
}
]
}

View File

@@ -0,0 +1,20 @@
[
{
"stage_name": "驯兽小屋",
"replacement_home": [
[
8,
3
]
]
},
{
"stage_name": "巡逻队",
"replacement_home": [
[
8,
3
]
]
}
]

View File

@@ -37,11 +37,32 @@
"level": 5,
"skill": 1
},
{
"name": "陈",
"level": 6,
"skill": 2
},
{
"name": "拉普兰德",
"level": 5,
"skill": 2
},
{
"name": "阿米娅",
"level": 5,
"skill": 1
},
{
"name": "史尔特尔",
"level": 6,
"skill": 2
},
{
"name": "银灰",
"level": 6,
"skill": 3,
"alternate_skill": 1
},
{
"name": "赫拉格",
"level": 6,
@@ -53,7 +74,7 @@
"skill": 1
},
{
"name": "拉普兰德",
"name": "幽灵鲨",
"level": 5,
"skill": 2
},
@@ -62,27 +83,11 @@
"level": 4,
"skill": 1
},
{
"name": "陈",
"level": 6,
"skill": 2
},
{
"name": "斯卡蒂",
"level": 6,
"skill": 1
},
{
"name": "阿米娅",
"level": 5,
"skill": 1
},
{
"name": "银灰",
"level": 6,
"skill": 3,
"alternate_skill": 1
},
{
"name": "Sharp",
"level": 5,
@@ -94,7 +99,7 @@
"skill": 1
},
{
"name": "幽灵鲨",
"name": "星极",
"level": 5,
"skill": 2
},
@@ -105,9 +110,9 @@
"alternate_skill": 2
},
{
"name": "断罪者",
"level": 4,
"skill": 1
"name": "柏喙",
"level": 5,
"skill": 2
},
{
"name": "芳汀",
@@ -115,9 +120,9 @@
"skill": 2
},
{
"name": "星极",
"level": 5,
"skill": 2
"name": "断罪者",
"level": 4,
"skill": 1
},
{
"name": "战车",
@@ -204,11 +209,6 @@
"level": 5,
"skill": 2
},
{
"name": "柏喙",
"level": 5,
"skill": 2
},
{
"name": "布洛卡",
"level": 5,
@@ -246,11 +246,6 @@
}
],
"Pioneer": [
{
"name": "芬",
"level": 3,
"skill": 1
},
{
"name": "风笛",
"level": 6,
@@ -287,6 +282,11 @@
"level": 5,
"skill": 2
},
{
"name": "芬",
"level": 3,
"skill": 1
},
{
"name": "清道夫",
"level": 4,
@@ -307,6 +307,21 @@
"level": 4,
"skill": 1
},
{
"name": "红豆",
"level": 4,
"skill": 2
},
{
"name": "苇草",
"level": 5,
"skill": 1
},
{
"name": "格拉尼",
"level": 5,
"skill": 1
},
{
"name": "琴柳",
"level": 6,
@@ -341,21 +356,6 @@
"name": "翎羽",
"level": 3,
"skill": 1
},
{
"name": "红豆",
"level": 4,
"skill": 2
},
{
"name": "苇草",
"level": 5,
"skill": 1
},
{
"name": "格拉尼",
"level": 5,
"skill": 1
}
],
"Tank": [
@@ -367,8 +367,17 @@
{
"name": "塞雷娅",
"level": 6,
"skill": 2,
"alternate_skill": 1
"skill": 2
},
{
"name": "暮落",
"level": 5,
"skill": 2
},
{
"name": "星熊",
"level": 6,
"skill": 2
},
{
"name": "斑点",
@@ -401,11 +410,6 @@
"level": 5,
"skill": 1
},
{
"name": "星熊",
"level": 6,
"skill": 2
},
{
"name": "雷蛇",
"level": 5,
@@ -461,11 +465,6 @@
"level": 4,
"skill": 2
},
{
"name": "暮落",
"level": 5,
"skill": 2
},
{
"name": "极光",
"level": 5,
@@ -713,11 +712,6 @@
"level": 6,
"skill": 3
},
{
"name": "伊芙利特",
"level": 6,
"skill": 2
},
{
"name": "阿米娅",
"level": 5,
@@ -733,6 +727,11 @@
"level": 6,
"skill": 1
},
{
"name": "伊芙利特",
"level": 6,
"skill": 2
},
{
"name": "天火",
"level": 5,
@@ -998,11 +997,6 @@
}
],
"Medic": [
{
"name": "安赛尔",
"level": 3,
"skill": 1
},
{
"name": "凯尔希",
"level": 6,
@@ -1090,6 +1084,11 @@
"level": 5,
"skill": 1
},
{
"name": "安赛尔",
"level": 3,
"skill": 1
},
{
"name": "闪灵",
"level": 6,
@@ -1122,22 +1121,12 @@
}
],
"Support": [
{
"name": "浊心斯卡蒂",
"level": 6,
"skill": 2
},
{
"name": "令",
"level": 6,
"skill": 3,
"alternate_skill": 2
},
{
"name": "梓兰",
"level": 3,
"skill": 1
},
{
"name": "波登可",
"level": 4,
@@ -1148,16 +1137,17 @@
"level": 6,
"skill": 1
},
{
"name": "安洁莉娜",
"level": 6,
"skill": 1
},
{
"name": "铃兰",
"level": 6,
"skill": 2
},
{
"name": "安洁莉娜",
"level": 6,
"skill": 3,
"alternate_skill": 2
},
{
"name": "巫恋",
"level": 5,
@@ -1178,6 +1168,16 @@
"level": 5,
"skill": 1
},
{
"name": "梓兰",
"level": 3,
"skill": 1
},
{
"name": "浊心斯卡蒂",
"level": 6,
"skill": 2
},
{
"name": "月禾",
"level": 5,

View File

@@ -110,16 +110,10 @@ namespace asst
}
};
struct RoguelikeBattleAction
struct RoguelikeBattleData
{
int kills = 0;
BattleActionType type = BattleActionType::Deploy;
std::vector<BattleRole> roles;
bool waiting_cost = false;
Point location;
BattleDeployDirection direction = BattleDeployDirection::Right;
int pre_delay = 0;
int rear_delay = 0;
int time_out = INT_MAX;
std::string stage_name;
std::vector<Point> replacement_home;
std::vector<int> key_kills;
};
}

View File

@@ -22,7 +22,7 @@ bool asst::Resource::load(const std::string& dir)
constexpr static auto InfrastCfgFilename = "infrast.json";
constexpr static auto InfrastTempls = "template/infrast";
//constexpr static const char* CopilotCfgDirname = "copilot";
constexpr static auto RoguelikeCfgDirname = "roguelike";
constexpr static auto RoguelikeCfgDirname = "roguelike_copilot.json";
constexpr static auto OcrResourceFilename = "PaddleOCR";
constexpr static auto TilesCalcResourceFilename = "Arknights-Tile-Pos";
constexpr static auto StageDropsCfgFilename = "stages.json";
@@ -91,17 +91,15 @@ bool asst::Resource::load(const std::string& dir)
// }
//}
if (std::filesystem::exists(dir + RoguelikeCfgDirname)) {
for (const auto& entry : std::filesystem::directory_iterator(dir + RoguelikeCfgDirname)) {
if (entry.path().extension() != ".json") {
continue;
}
if (!m_roguelike_cfg_unique_ins.load(entry.path().string())) {
m_last_error = entry.path().string() + " Load failed";
return false;
}
if (!m_roguelike_cfg_unique_ins.load(dir + RoguelikeCfgDirname)) {
if (!m_loaded) {
m_last_error = std::string(RoguelikeCfgDirname) + ": " + m_roguelike_cfg_unique_ins.get_last_error();
return false;
}
}
else {
overload = true;
}
if (!m_infrast_cfg_unique_ins.load(dir + InfrastCfgFilename)) {
if (!m_loaded) {

View File

@@ -117,6 +117,40 @@ bool asst::RoguelikeBattleTaskPlugin::get_stage_info()
calced = true;
}
auto opt = Resrc.roguelike().get_stage_data(m_stage_name);
if (opt && !opt->replacement_home.empty()) {
m_homes = opt->replacement_home;
std::string log_str = "[ ";
for (auto& home : m_homes) {
if (m_normal_tile_info.find(home) == m_normal_tile_info.end()) {
Log.error("No replacement home point", home.x, home.y);
}
log_str += "( " + std::to_string(home.x) + ", " + std::to_string(home.y) + " ), ";
}
log_str += "]";
Log.info("replacement home:", log_str);
}
else {
for (const auto& [loc, side] : m_normal_tile_info) {
if (side.key == TilePack::TileKey::Home) {
m_homes.emplace_back(loc);
}
}
}
if (opt && !opt->key_kills.empty()) {
std::string log_str = "[ ";
for (const auto& kills : opt->key_kills) {
m_key_kills.emplace(kills);
log_str += std::to_string(kills) + ", ";
}
log_str += "]";
Log.info("key kills:", log_str);
}
if (m_homes.empty()) {
Log.error("Unknown home pos");
}
if (calced) {
auto cb_info = basic_info_with_what("StageInfo");
auto& details = cb_info["details"];
@@ -307,10 +341,14 @@ void asst::RoguelikeBattleTaskPlugin::clear()
m_opers_used = false;
m_pre_hp = 0;
m_homes.clear();
decltype(m_key_kills) empty;
m_key_kills.swap(empty);
m_cur_home_index = 0;
m_stage_name.clear();
m_side_tile_info.clear();
m_used_tiles.clear();
m_kills = 0;
m_total_kills = 0;
for (auto& [key, status] : m_restore_status) {
m_status->set_number(key, status);
@@ -320,6 +358,10 @@ void asst::RoguelikeBattleTaskPlugin::clear()
bool asst::RoguelikeBattleTaskPlugin::try_possible_skill(const cv::Mat& image)
{
if (!check_key_kills(image)) {
return false;
}
auto task_ptr = Task.get("BattleAutoSkillFlag");
const Rect& skill_roi_move = task_ptr->rect_move;
@@ -356,6 +398,29 @@ bool asst::RoguelikeBattleTaskPlugin::try_possible_skill(const cv::Mat& image)
return used;
}
bool asst::RoguelikeBattleTaskPlugin::check_key_kills(const cv::Mat& image)
{
if (m_key_kills.empty()) {
return true;
}
int need_kills = m_key_kills.front();
BattleImageAnalyzer analyzer(image);
if (m_total_kills) {
analyzer.set_pre_total_kills(m_total_kills);
}
analyzer.set_target(BattleImageAnalyzer::Target::Kills);
if (analyzer.analyze()) {
m_kills = analyzer.get_kills();
m_total_kills = analyzer.get_total_kills();
if (m_kills >= need_kills) {
m_key_kills.pop();
return true;
}
}
return false;
}
bool asst::RoguelikeBattleTaskPlugin::wait_start()
{
auto start_time = std::chrono::system_clock::now();
@@ -392,6 +457,15 @@ bool asst::RoguelikeBattleTaskPlugin::wait_start()
std::string text = "( " + std::to_string(loc.x) + ", " + std::to_string(loc.y) + " )";
cv::putText(image, text, cv::Point(info.pos.x - 30, info.pos.y), 1, 1.2, cv::Scalar(0, 0, 255), 2);
}
// 识别一帧总击杀数
BattleImageAnalyzer kills_analyzer(image);
kills_analyzer.set_target(BattleImageAnalyzer::Target::Kills);
if (kills_analyzer.analyze()) {
m_kills = kills_analyzer.get_kills();
m_total_kills = kills_analyzer.get_total_kills();
}
#ifdef WIN32
cv::imwrite("map/" + utils::utf8_to_ansi(m_stage_name) + ".png", image);
#else
@@ -449,16 +523,6 @@ asst::RoguelikeBattleTaskPlugin::DeployInfo asst::RoguelikeBattleTaskPlugin::cal
break;
}
if (m_homes.empty()) {
for (const auto& [loc, side] : m_side_tile_info) {
if (side.key == TilePack::TileKey::Home) {
m_homes.emplace_back(loc);
}
}
if (m_homes.empty()) {
Log.error("Unknown home pos");
}
}
if (m_cur_home_index >= m_homes.size()) {
m_cur_home_index = 0;
}

View File

@@ -30,6 +30,7 @@ namespace asst
bool retreat(const Point& point);
void clear();
bool try_possible_skill(const cv::Mat& image);
bool check_key_kills(const cv::Mat& image);
bool wait_start();
struct DeployInfo
@@ -46,10 +47,13 @@ namespace asst
bool m_opers_used = false;
int m_pre_hp = 0;
int m_kills = 0;
int m_total_kills = 0;
std::unordered_map<Point, TilePack::TileInfo> m_side_tile_info;
std::unordered_map<Point, TilePack::TileInfo> m_normal_tile_info;
std::vector<Point> m_homes;
std::queue<int> m_key_kills;
size_t m_cur_home_index = 0;
std::unordered_map<Point, std::string> m_used_tiles;
std::unordered_map<std::string, int64_t> m_restore_status;

View File

@@ -4,158 +4,34 @@
#include "Logger.hpp"
std::optional<asst::RoguelikeBattleData> asst::RoguelikeCopilotConfiger::get_stage_data(const std::string& stage_name) const
{
auto it = m_stage_data.find(stage_name);
if (it == m_stage_data.end()) {
return std::nullopt;
}
return it->second;
}
bool asst::RoguelikeCopilotConfiger::parse(const json::value& json)
{
std::string stage_name = json.at("stage_name").as_string();
std::vector<RoguelikeBattleAction> actions_vec;
for (const auto& action_info : json.at("actions").as_array()) {
RoguelikeBattleAction action;
static const std::unordered_map<std::string, BattleActionType> ActionTypeMapping = {
{ "Deploy", BattleActionType::Deploy },
{ "DEPLOY", BattleActionType::Deploy },
{ "deploy", BattleActionType::Deploy },
{ "部署", BattleActionType::Deploy },
{ "Skill", BattleActionType::UseSkill },
{ "SKILL", BattleActionType::UseSkill },
{ "skill", BattleActionType::UseSkill },
{ "技能", BattleActionType::UseSkill },
{ "Retreat", BattleActionType::Retreat },
{ "RETREAT", BattleActionType::Retreat },
{ "retreat", BattleActionType::Retreat },
{ "撤退", BattleActionType::Retreat },
{ "AllSkill", BattleActionType::UseAllSkill },
{ "allskill", BattleActionType::UseAllSkill },
{ "ALLSKILL", BattleActionType::UseAllSkill },
{ "Allskill", BattleActionType::UseAllSkill },
{ "全部技能", BattleActionType::UseAllSkill },
};
std::string type_str = action_info.get("type", "Deploy");
if (auto iter = ActionTypeMapping.find(type_str);
iter != ActionTypeMapping.end()) {
action.type = iter->second;
}
else {
action.type = BattleActionType::Deploy;
}
action.kills = action_info.get("kills", 0);
if (action_info.contains("roles")) {
for (const auto& role_info : action_info.at("roles").as_array()) {
static const std::unordered_map<std::string, BattleRole> RoleMapping = {
{ "MEDIC", BattleRole::Medic },
{ "Medic", BattleRole::Medic },
{ "medic", BattleRole::Medic },
{ "医疗", BattleRole::Medic },
{ "WARRIOR", BattleRole::Warrior },
{ "Warrior", BattleRole::Warrior },
{ "warrior", BattleRole::Warrior },
{ "近卫", BattleRole::Warrior },
{ "SPECIAL", BattleRole::Special },
{ "Special", BattleRole::Special },
{ "special", BattleRole::Special },
{ "特种", BattleRole::Special },
{ "SNIPER", BattleRole::Sniper },
{ "Sniper", BattleRole::Sniper },
{ "sniper", BattleRole::Sniper },
{ "狙击", BattleRole::Sniper },
{ "PIONEER", BattleRole::Pioneer },
{ "Pioneer", BattleRole::Pioneer },
{ "pioneer", BattleRole::Pioneer },
{ "先锋", BattleRole::Pioneer },
{ "TANK", BattleRole::Tank },
{ "Tank", BattleRole::Tank },
{ "tank", BattleRole::Tank },
{ "重装", BattleRole::Tank },
{ "SUPPORT", BattleRole::Support },
{ "Support", BattleRole::Support },
{ "support", BattleRole::Support },
{ "辅助", BattleRole::Support },
{ "CASTER", BattleRole::Caster },
{ "Caster", BattleRole::Caster },
{ "caster", BattleRole::Caster },
{ "术师", BattleRole::Caster },
{ "Drone", BattleRole::Drone },
{ "DRONE", BattleRole::Drone },
{ "drone", BattleRole::Drone },
{ "召唤物", BattleRole::Drone },
{ "无人机", BattleRole::Drone }
};
std::string role_name = role_info.as_string();
if (auto iter = RoleMapping.find(role_name);
iter != RoleMapping.end()) {
action.roles.emplace_back(iter->second);
}
else {
Log.error("Unknown role", role_name);
}
for (const auto& stage_info : json.as_array()) {
std::string stage_name = stage_info.at("stage_name").as_string();
RoguelikeBattleData data;
data.stage_name = stage_name;
if (stage_info.contains("replacement_home")) {
for (const auto& points : stage_info.at("replacement_home").as_array()) {
const auto& points_array = points.as_array();
Point point(points_array.at(0).as_integer(), points_array.at(1).as_integer());
data.replacement_home.push_back(point);
}
}
action.location.x = action_info.get("location", 0, 0);
action.location.y = action_info.get("location", 1, 0);
static const std::unordered_map<std::string, BattleDeployDirection> DeployDirectionMapping = {
{ "Right", BattleDeployDirection::Right },
{ "RIGHT", BattleDeployDirection::Right },
{ "right", BattleDeployDirection::Right },
{ "", BattleDeployDirection::Right },
{ "Left", BattleDeployDirection::Left },
{ "LEFT", BattleDeployDirection::Left },
{ "left", BattleDeployDirection::Left },
{ "", BattleDeployDirection::Left },
{ "Up", BattleDeployDirection::Up },
{ "UP", BattleDeployDirection::Up },
{ "up", BattleDeployDirection::Up },
{ "", BattleDeployDirection::Up },
{ "Down", BattleDeployDirection::Down },
{ "DOWN", BattleDeployDirection::Down },
{ "down", BattleDeployDirection::Down },
{ "", BattleDeployDirection::Down },
{ "None", BattleDeployDirection::None },
{ "NONE", BattleDeployDirection::None },
{ "none", BattleDeployDirection::None },
{ "", BattleDeployDirection::None },
};
std::string direction_str = action_info.get("direction", "Right");
if (auto iter = DeployDirectionMapping.find(direction_str);
iter != DeployDirectionMapping.end()) {
action.direction = iter->second;
if (stage_info.contains("key_kills")) {
for (const auto& key_kill : stage_info.at("key_kills").as_array()) {
data.key_kills.emplace_back(key_kill.as_integer());
}
}
else {
action.direction = BattleDeployDirection::Right;
}
action.waiting_cost = action_info.get("waiting_cost", false);
action.pre_delay = action_info.get("pre_delay", 0);
action.rear_delay = action_info.get("rear_delay", 0);
action.time_out = action_info.get("timeout", INT_MAX);
actions_vec.emplace_back(std::move(action));
m_stage_data.emplace(std::move(stage_name), std::move(data));
}
m_battle_actions[std::move(stage_name)] = std::move(actions_vec);
return true;
}

View File

@@ -1,6 +1,8 @@
#pragma once
#include "AbstractConfiger.h"
#include <optional>
#include "AsstBattleDef.h"
namespace asst
@@ -10,17 +12,10 @@ namespace asst
public:
virtual ~RoguelikeCopilotConfiger() = default;
bool contains_actions(const std::string& stage_name) const noexcept
{
return m_battle_actions.find(stage_name) != m_battle_actions.cend();
}
std::optional<RoguelikeBattleData> get_stage_data(const std::string& stage_name) const;
auto get_actions(const std::string& stage_name) const noexcept
{
return m_battle_actions.at(stage_name);
}
protected:
virtual bool parse(const json::value& json) override;
std::unordered_map<std::string, std::vector<RoguelikeBattleAction>> m_battle_actions;
std::unordered_map<std::string, RoguelikeBattleData> m_stage_data;
};
}

View File

@@ -40,14 +40,26 @@ bool asst::RoguelikeRecruitTaskPlugin::_run()
};
for (const auto& info : analyzer.get_result()) {
if (info.elite == 0 ||
(info.elite == 1 && info.level < 50)) {
// 先看看有没有精二的
if (info.elite != 2) {
continue;
}
recruit_oper(info);
break;
}
if (!recruited) {
for (const auto& info : analyzer.get_result()) {
// 拿个精一 50 以上的
if (info.elite == 0 ||
(info.elite == 1 && info.level < 50)) {
continue;
}
recruit_oper(info);
break;
}
}
if (!recruited) {
Log.info("All are lower");
// 随便招个精一的