开发指南与示例AI说明
在本文中,我将为你简单地介绍一下Commander项目并手把手带你实现一个简单的AI示例,以方便你快速上手这个项目。
在第一部分中,我们简要地介绍整个项目的基本框架,以及一些你需要知道的注意事项。
A Tour of Commander
在你下载到的源码文件夹中,大致会包含以下一些文件夹,这里略去了你不太需要关心的部分:
Commander> tree
├─Client 客户端
│ └─AI AI的相关文件
│ └─AI_SDK 用户接口
├─Data 地图文件
├─lib 编译后的动态链接库
├─MapEditor 地图编辑器
├─Savedata 游戏对局存档文件
├─Server 服务端
└─src_cpp C++源文件
├─include 重要的头文件们
├─UserAPI 用户接口
└─UserImplementation 你的AI实现
下面我们将介绍一些重要的位置,希望能在相关的介绍中带领你进入对Commander项目的探索。
Client, Server 和 AI 文件夹
首先,我们将介绍有关客户端,服务端以及游戏对局的一些概念。
在Client,Server文件夹中,储存了很大部分你会用到的源码文件。在开发的过程中,你可以通过浏览源码的方式来探索整个项目,我们已经为你写好了很多有用的功能,期待你的发现和使用~
开始游戏
在Commander中,默认我们采用三玩家模式,即一个服务端,三个客户端。默认的地图文件储存在Commader/Data/3Player.map
,你也可以通过MapEditor
来编辑地图以使AI适应更加复杂的场景。
如何快速开始游戏(Windows)
在开始游戏之前,你应该确保
love2d
安装正确user: Commander> ./QuickRun_3Player.bat
我们之后也会以上面这种格式标识控制台命令。要运行这样一段命令,你需要打开一个新的控制台页面,并使用cd等命令切换到
>
前所示的文件夹路径,接着运行>
后面的命令即可这样一条指令可以快速的为你打开一个服务端和三个客户端,可以满足大部分调试用途了。不过,也存在有些时候我们需要手动运行游戏,比如:让不同的客户端运行多个AI以实现AI对战,你的常用系统为Linux,等。
如何手动运行游戏
要手动运行游戏,你需要打开四个(默认情况下)控制台窗口,分别定位到
Commander/Server
(一个)和Commander/Client
(三个),分别运行下列指令:user: Client(Server)> lovec .
lovec
会打开一个新的love2d
实例,.
代表运行当前目录下的源码文件。
你也可以使用以下命令来非阻塞式启动程序,这样最少只需要打开一个控制台窗口:user: Client(Server)> start lovec .
如果将以上语句中的
lovec
变为love
,程序运行时将不会显示控制台窗口。
如何修改启动选项
在根目录下建立
ClientTask.txt
和ServerTask.txt
。客户端和服务端启动时会自动读取相应的文件。 ClientTask.txt应包含十行,每两行为一组,第一行为启动选项,第二行为值。五组的值从上到下依次为:true
或false
,代表客户端是否以自动对战模式运行- 自动对战模式下的超时时间(秒)
- 地图目录,若为default则使用Data
- 地图文件名,若为default则使用3Player.map
Lua
或C++
,代表加载的AI所使用的语言
命令组可以不按顺序或缺失。
默认情况下,ClientTask.txt内容如下所示:[autoMatch] false [timeOut] 1e10 [mapDict] default [mapName] default [AIlang] C++
自动读取这一文件不会使客户端行为发生变化。而如下的ClientTask.txt
[autoMatch] false [timeOut] 1e10 [mapDict] maps_3player [mapName] 0.map [AIlang] Lua
会加载/maps_3player/0.map作为地图,并在按
A
切换到AI时使用Lua实现的AI。
与ClientTask.txt类似,ServerTask.txt包含十二行,六组依次为:true
或false
,代表服务端是否以自动对战模式运行- 自动对战模式下的超时时间(秒)
- 地图目录,若为default则使用Data
- 地图文件名,若为default则使用3Player.map
- 回放文件夹名,若为default则使用游戏开始时间
- 回放文件目录,若为default则使用Savedata
如下的ServerTask.txt会加载/maps_3player/0.map作为地图,并将回放文件夹命名为round1,保存在根目录下的AvsB文件夹中(如果文件夹不存在将被自动创建)。
[autoMatch] false [timeOut] 1e10 [mapDict] maps_3player [mapName] 0.map [saveName] round1 [saveDict] AvsB
客户端与服务端加载的地图应当相同。有关自动对战的更多信息,请参考自动评测教程。
如何切换到AI模式
进入游戏后,客户端会在缺省下进入欢迎页面,如果点击中间第一个按钮即可以玩家身份参与游戏,当三个玩家都准备就绪后游戏自动开始。而若要以AI身份参与,你需要在欢迎页面按下
a
键,这时,系统会自动跳转到准备中状态。以AI身份开始游戏会将控制权转移到你编写的AI,你将只有拖拽缩放视图的能力。
如何联机游戏
与你的朋友们一起来一场激烈刺激的对决吧!要开启联机游戏,你需要将服务端,以及三个客户端位于同一局域网(如校园网)下,使用服务端设备运行Commander Server,并获取本机的内网IP地址(
Windows
使用ipconfig
,linux
使用ifconfig -a
,Mac
在已连接的WIFI处查看连接信息,以Windows
下为例):user: ~> ipconfig Wireless LAN adapter WLAN: Connection-specific DNS Suffix . : IPv6 Address. . . . . . . . . . . : xxxxxxxxxxxxxxxxxxx Link-local IPv6 Address . . . . . : xxxxxxxxxxxxxxxxxxx IPv4 Address. . . . . . . . . . . : 10.123.231.213 Subnet Mask . . . . . . . . . . . : 255.255.128.0 Default Gateway . . . . . . . . . : xxxxxxxxxxxxxxxxxxx 上面的10.123.231.213就是我们需要的ip地址
接下来请所有参赛选手修改客户端的配置以连接到服务端:
你需要修改
Client/ClientSock.lua:ClientSock.Init()
中的Client = Sock.newClient("localhost", 22122)
里localhost
为之前一步所获得的IP地址,如:Client = Sock.newClient("10.123.231.213", 22122)
这样,一局联网游戏前的配置工作就已经就绪了,接下来运行游戏即可~
AI 文件夹探秘
AI的交互机制
在AI模式下,游戏是怎样执行的呢?事实上,整个对局游戏运行在一个全局的时钟下,每半秒这个时钟就会“跳动”一次,我们称为一个step,在每个step下,用户(AI)可以执行一些指令如移动。AI的交互逻辑也是相似的,在每个step下,系统会自动调取AI的移动策略,在
lua
下是Core.Main()
,而在C++下是userMain()
,就如同每个C语言程序都要调用main()
函数一样,每次step,AI的运行逻辑以上面的两个Core.Main(),userMain()
开始。我们将在后面对示例AI的介绍中清楚地看到这一点。选择你的开发语言
目前来讲,我们支持的开发语言有C++和Lua两种(后续还会持续添加)。如果你想要使用熟悉的C/C++语言,或者使用C++来实现机器学习等算法,那么请你将用户配置改为C++语言实现(配置方法见下文);如果你想尝试一种新的语言,Lua也一定会给你耳目一新的感觉,我们的游戏逻辑代码大部分是基于Lua进行开发的。
要更改用户实现语言的选择,你需要修改
Client/Ai/UserImplementationType.txt
下的内容,将第二行改为C++
或者Lua
。注意,我们的识别器是大小写敏感的,因此请严格安装格式填写!(识别器的代码在AI_SDK.lua: AI_SDK.Init()
)如果选择C++作为开发语言,你需要实现
src_cpp/UserImplementation/API.cpp
中的userMain()
函数,对应的用户API为src_cpp/include/UserAPI.hpp
。如果选择Lua作为开发语言,你需要实现
Client/AI/Core.lua
中的Core.Main()
函数,对应的用户API为Client/AI/AI_SDK/AI_SDK.lua
。
C++ 源文件
无论你选择C++还是Lua作为你的参赛语言,了解C++源文件都是有必要的,这里的源文件指的是src_cpp/include
下的几个重要的头文件GameMap.hpp
,UserAPI.hpp
,Verification.hpp
等。这些头文件提供了一些实用的接口,我们希望你可以在开发的过程中,逐步探索整个项目,自然你就需要了解这些头文件所包含的内容了。
Lua State
这里我们不会对C++源文件部分展开过多的介绍,毕竟这只是一篇入门指南,更多的探索需要你自己来完成。不过,在开始你的探索之前,有这样一个概念需要你了解——Lua State,Lua 与 C++交互的重要工具。
本质上,Lua 与 C++ 交互是以共享栈空间实现的,这个共享栈就是 Lua State。以 C++ 调用 Lua 的成员函数为例。你需要首先找到该函数所挂靠的表(table),将其压入栈中,接着在栈中将表“解压缩”,得到表的所有成员,这里面就包含了你所需要的成员函数,接下来,你需要将这个函数显式地提取到栈顶,并将所有调用参数压入栈。在执行结束后,所有的返回值都会被 Lua State 存放在栈中,你只需要依次使他们pop出来就可以了。
有关 Lua State, 你可以在网络上找到丰富的相关资料,这里不多赘述。
单一实例模式
在阅读C++头文件时,你会经常看到一个古怪的单词Singleton
,并且你也会看到一些奇奇怪怪的用法,比如禁用的构造函数。实际上,这时因为我们广泛使用了单一实例模式来进行开发的原因。
在面向对象程序设计中,通常来讲,一个类可以有多个实例(对象),然而在特殊情况下,比如在我们的游戏中,依据游戏下只能够拥有一份地图,一个用户也只有一个身份,等等。这样的场景下,我们需要对类实例的个数进行限制。在C++语言的学习中,我们也曾遇到过相似的场景,那里,我们会使用一种叫做static
的关键字来修饰对象来保证只会有一个这样的对象实例。
单一实例模式也就是为实现这样的应用场景而生的,你大可以不去管它的实现技术细节(当然,如果你感兴趣,可以查看UserAPI()::Singleton()
中的代码),只需要知道,当你需要调用C++的某些类库的接口时,以MAP
为例,你只需用一个引用来获取对应的实例即可,如MAP& mmap = MAP::Singleton()
。有些时候,在第一次使用时,需要你为这个对象执行初始化任务,这时,会需要你进行参数的传入,如在C++的用户API中,我们需要你手动提供 Lua State 来初始化这个对象:UserAPI& API = UserAPI::Singleton(luaState)
。具体可以参考对应的文档或者代码,你也可以在网络上找到很多有关单一实例模式的文章。
进一步探索项目
这里,我们给出两张重要的程序流程图,希望可以帮助你进一步了解我们的项目。
图一:
图二:
毕竟我们的文档暂时还无法覆盖所有的话题,因此还需要你作为参赛者,主动地探索整个项目。如果有问题也可以及时通过用户群等方式与我们取得联系~
开发指南
下面这部分,我将对开发一个AI做出更加详细的说明。首先,让我们看一看你在这场赛事中都需要做些什么:
- 填写相关信息并提交报名表,下载项目源文件
- 按照安装文档说明,配置love2d等相关环境,试着跑几局游戏来快速了解项目
- 阅读包含本文在内的几篇文档来对项目有整体的了解,接着自主探索项目文件
- 选择你的开发语言,开发AI接口,并逐步完善
- 提交你的AI实现文件(
Core.lua
或者API.cpp
)
下面我们会带领你一步步实现项目源码中的示例AI程序,当然,我们这里只是抛砖引玉,示例AI的功能并不强大,不过相信通过研习这份代码,你将对项目有进一步的认知。
示例AI的工作逻辑介绍
我们将要实现的AI是一个基于“摇骰子“的随机自走机器人。它的主要功能就是:
- 随机选择一个占有的位置作为起点
- 向四周随机移动
接下来,我将分别介绍 C++ 和 Lua 中对这个逻辑的实现。
C++ 开发指南与示例AI实现
一份空白AI实现模板
#include <iostream>
#include "GameMap.hpp"
#include "LuaAPI.hpp"
#include "UserAPI.hpp"
#include "Verification.hpp"
using namespace std;
// Your Code
static int userMain(lua_State *luaState) {
UserAPI &API = UserAPI::Singleton(luaState);
MAP &mmap = MAP::Singleton();
int id = VERIFICATION::Singleton().GetArmyID();
static bool init = false;
if (!init) {
init = true;
// Initialization
// Your Code
}
// Your Code
return 0;
}
LUA_REG_FUNC(UserImplementation, C_API(userMain))
示例AI
前面我们介绍过AI的交互逻辑——通过每次时钟的跳动来唤醒AI核心。这里我们也将从 C++ AI 核心userMain()
开始,首先搭建AI的实现框架,这里是一份完整的userMain()
:
static int userMain(lua_State *luaState) {
UserAPI &API = UserAPI::Singleton(luaState);
MAP &mmap = MAP::Singleton();
id = VERIFICATION::Singleton().GetArmyID();
static bool init = false;
if (!init) {
init = true;
API.selected_pos(API.king_pos());
}
testInfo();
double move_ratio = 0.5;
if (API.selected_pos().x == -1 ||
API.selected_pos().y == -1 || // 选择位置非法
mmap.GetBelong(API.selected_pos()) != id || // 不可移动
mmap.GetUnitNum(API.selected_pos()) < 2) { // 兵力过少
API.selected_pos(API.king_pos());
}
random_select();
for (int i = 1; i <= 3 && !move_from_select(); i++) {
random_select();
}
return 0;
}
下面我们将其拆分,介绍每一部分的详细功能:
获取类库的单一实例:
UserAPI &API = UserAPI::Singleton(luaState); MAP &mmap = MAP::Singleton(); id = VERIFICATION::Singleton().GetArmyID();
有关单一实例,前面我们已经有过相关的介绍,这里我们做一些补充说明。
UserAPI
是我们提供的用户API接口,主要提供了执行移动功能以及和 Lua 交互的相关接口,在初次使用时需要传入luaState
作为参数。MAP
位于GameMap.hpp
,主要提供了游戏地图上的信息获取功能,如获取某个格子上的兵力、归属等信息。Verification
主要用于用户信息验证,你可以通过上面第三行的语句获取自己的军队编号。
第一次运行时进行初始化
在第一次运行AI核心时,我们希望将king的位置设置为选中点,这时需要进行一定的初始化:
static bool init = false; if (!init) { init = true; API.selected_pos(API.king_pos()); // 将这行代码换成你的初始化代码即可 }
这里的static对象和单例是异曲同工的,你只需要根据这份代码进行修改即可,静态对象的语法细节在AI开发中并不重要。
有关
selected_pos
,这是用户API中所提供的一个函数,它有两个函数重载,一个用于设定选中点,另一个用于获取选中点。而king_pos
就只有一个重载,因为你不能重新设定king的位置。这里涉及到
GameMap
中定义的一个简单的数据类型VECTOR
,你可以简单理解为二维平面上的坐标。在顺利初始化后,我们也希望在每次调用AI核心是输出一些调试信息
testInfo(); --> void testInfo() { UserAPI &API = UserAPI::Singleton(); // 因为不是第一次获取UserAPI单例,不需要传入参数 MAP &mmap = MAP::Singleton(); cout << "Current selected pos: " << API.selected_pos().x << ", " << API.selected_pos().y; cout << " Belongs: " << mmap.GetBelong(API.selected_pos()); cout << " UnitNum: " << mmap.GetUnitNum(API.selected_pos()) << endl; }
这里用到了几个常用的API接口:
selected_pos
前面已经介绍过,这里我们用到的是获取选中点的功能,他返回一个VECTOR
对象,我们可以通过.
运算符访问它的XY坐标GetBelong
用于获取某个格子上点的归属,也就是军队编号,如果这个编号与我们之前用VERIFICATION
获取的自身军队编号一致,那么这个格子就是归属于你的。特别地,由于战争迷雾的存在,我们不保证每次都可以获取到准确的信息,一般来讲,你只能获取你周边一部分格子的详细信息GetUnitNum
用于获取格子上的兵力情况,同样因为战争迷雾存在而有相关限制
判断当前位置是否不是一个理想的出发点
在一些特殊情况下,我们当前的选中点可能并不那么理想,这时我们希望它回到king的位置重新开始选择
double move_ratio = 0.5; if (API.selected_pos().x == -1 || API.selected_pos().y == -1 || // 选择位置非法 mmap.GetBelong(API.selected_pos()) != id || // 不可移动 mmap.GetUnitNum(API.selected_pos()) < 2) { // 兵力过少 API.selected_pos(API.king_pos()); }
move_ratio
是我们自己定义的一个变量,他表面我们将调遣多少比例的兵力参与下一步移动。
随机选择并随机移动
random_select(); for (int i = 1; i <= 3 && !move_from_select(); i++) { random_select(); }
可以看到,我们会随机地选择一些位置,并尝试从以这个位置为起点向外随机的移动,这个操作有一定失败的可能,因此我们需要多执行几次。
注意事项
注意,
userMain()
的返回值代表了他执行的成功与否,默认情况下,请返回0,否则程序将认为出现错误而立即停止运行,具体表现为”崩溃“。如果你发现程序不能正确执行你的代码,可以检查是否有正确的返回值。
至此,我们已经搭建了一个大致的框架,下面我们只需向目前还未实现的random_select()
和move_from_select()
填充代码即可。
随机选择
在这一部分代码中,我们希望从刚刚选定的初始位置随机地选取一个新的点作为选定位置。做法是递归地从一个已选位置选定其周边的六个位置之一,或者选定自身来停止递归。
// (递归地)随机从已有的领地中选择一个点作为出发点 // 每次从当前选定点开始,从 六个方向的邻接点 或 当前位置 // 总共7个选项的所有可选位置中 // 随机的选择一项,如果选择位置不是当前点,则递归调用自己 void random_select() { UserAPI &API = UserAPI::Singleton(); MAP &mmap = MAP::Singleton(); // 从当前位置出发的所有可选位置 vector<VECTOR> options = {API.selected_pos()}; // 初始只有自身位置,如果随机选到了这个点,则退出递归 for (int i = 0; i < 6; i++) { VECTOR apos = after_move_pos(API.selected_pos(), i); if (mmap.GetType(apos) == NODE_TYPE::HILL) continue; // 山丘不可到达 if (mmap.GetBelong(apos) != id) continue; // 这个位置不属于自己 if (mmap.GetUnitNum(apos) < 2) continue; // 兵力过少 // 该位置是一个可选位置 cout << "find a optional position: " << apos << endl; options.push_back(apos); } cout << "options :" << options.size() << endl; // 没有可选项,只能选自己 if (options.size() == 1) return; // 随机选择的选项 int choice = rand() % options.size(); // 不改变位置,直接返回 if (options[choice] == API.selected_pos()) return; API.selected_pos(options[choice]); cout << "Selection: " << options[choice] << endl; random_select(); }
相信根据注释的描述,你应该对这段代码的大致逻辑能够有一个大致的了解,这里我们再对几个小问题进行补充:
有关移动方向
Commander中使用六边形地图,因此会有六个移动方向,在C++实现中,direction从0开始到5为止。
有关
NODE_TYPE
Commander中定义了四种地形:
BLANK, HILL, FORT, KING
,在源码中,你可能还会看到其他的类型,但是,目前版本中只支持了以上四种类型。这四种类型中,HILL
是不可到达的。相关内容请查看游戏教程文档。after_move_pos()
VECTOR after_move_pos(VECTOR cur, int direction) { return cur + DIR[cur.x % 2][direction]; }
六边形地图中,处于奇偶数行的格子向同一方向移动的坐标变换规则并不完全相同(如:向右上角移动,(2,2)位置的点需要将X坐标减一,而(3,2)位置的点需要将X坐标减一并且将Y坐标加一),我们在
GameMap
中已经给出了奇偶数行向不同方向移动的坐标变化规则,依照上面的使用方式即可。
随机移动
bool move_from_select() { cout << "try to move" << endl; UserAPI &API = UserAPI::Singleton(); MAP &mmap = MAP::Singleton(); double move_ratio = 0.5; // 判断当前位置周围是否有敌人 for (int i = 0; i < 6; i++) { VECTOR apos = after_move_pos(API.selected_pos(), i); if (id == mmap.GetBelong(apos)) continue; // 平凡情况 if (mmap.GetType(apos) == NODE_TYPE::HILL) continue; // 山丘 不可通过 if (mmap.GetType(apos) == NODE_TYPE::FORT && mmap.GetUnitNum(API.selected_pos()) < 1.5 * mmap.GetUnitNum(apos)) continue; // 无法占有或占有后容易被夺去 double tmp; if (tmp = get_rand_percentage() > 0.5) { cout << "Skip, tmp = " << tmp << endl; continue; } // 随机跳过 cout << "Prepare to move " << API.selected_pos() << "->"; cout << apos << endl; move_ratio = get_rand_percentage(0.4, 0.8); // 随机移动 API.move_to(apos, move_ratio); return true; } return false; }
同样的,这里我们假设你可以看懂这份代码,只对一些细节进行补充:
随机数生成
double get_rand_percentage(double lower_bound = 0.0, double upper_bound = 1.0) { if (lower_bound < -0.00001 || lower_bound > 1.00001) cout << "Invalid lower_bound" << endl; if (upper_bound < -0.00001 || upper_bound > 1.00001) cout << "Invalid upper_bound" << endl; double ret = lower_bound + (rand() % 10000) * (upper_bound - lower_bound) / 10000.0; cout << "RNG (" << lower_bound << ", " << upper_bound << ") = " << ret << endl; return ret; }
这里实现了一个实用的随机数生成器,用户给定上下界后,返回在这个区间中的一个浮点。
Lua 开发指南与示例AI实现
第二份空白AI实现模板
Core = {}
--定义各种变量
local ...
--各种功能函数,如Core.init,Core.Record
function Core.XXX()
--你的实现过程
...
end
-- 主函数,AI运行时不断执行此函数中过程
function Core.Main()
--你的实现过程
...
end
return Core
重要函数
-- 获取此点的归属,0代表未占领
CGameMap.GetBelong(x,y)
-- 获取ArmID这一方王的位置,ArmyID是一个全局变量,代表自己的id(不要用来搞歪门邪道哦)
CGameMap.GetKingPos(ArmyID)
-- 获取此点的类型,返回
-- "NODE_TYPE_KING"(王) "NODE_TYPE_FORT"(堡垒)
-- "NODE_TYPE_HILL"(山) "NODE_TYPE_BLANK"(空格)
-- 中的一个
CGameMap.GetNodeType(x,y)
-- 获取此点兵力,返回数值
CGameMap.GetUnitNum(x,y)
-- 判断当前能否看到此点,返回true/false
CGameMap.GetVision(x,y)
示例AI之二
这里我们先给出一份完整的AI实现,然后在后面进行拆分讲解
Core = {}
local IsInit = false
local id
function Core.init()
--我的id
id = AI_SDK.armyID
end
function Core.Record()
-- 这个数组记录了哪些是我的点
local Collar = {}
local X, Y, Num, pos
Num = 0
-- 遍历整个地图
for i = 0, 23, 1 do
for j = 0, 23, 1 do
X = i
Y = j
if CGameMap.GetBelong(X, Y) == id then
table.insert(Collar, {X = X, Y = Y})
Num = Num + 1
end
end
end
-- 返回属于自己点的坐标和数量
return Collar, Num
end
function Core.Main()
local Collar = {}
local Num, X, Y, i
local move_ratio = 0.5
local ID
if IsInit == false then
Core.init()
IsInit = true
end
Collar, Num = Core.Record()
-- 随机选择自己的点
i = 1
while i <= Num / 2 + 1 do
local choice = math.random(Num)
X, Y = Collar[choice].X, Collar[choice].Y
if CGameMap.GetUnitNum(X, Y) > 2 then
break
end
i = i + 1
end
AI_SDK.setSelected(X, Y)
ID = CGameMap.GetBelong(X, Y)
-- 如果此点非法或是军队数量小于二,选择 王
if X == -1 or Y == -1 or ID ~= id or CGameMap.GetUnitNum(X, Y) < 2 then
AI_SDK.setSelected(CGameMap.GetKingPos(id))
end
-- 随机在六格方向上移动
while true do
local X1, Y1
i = math.random(6)
X1, Y1 =
AI_SDK.DirectionToDestination(
AI_SDK.SelectPos.x,
AI_SDK.SelectPos.y,
i
)
if CGameMap.GetNodeType(X1, Y1) == "NODE_TYPE_KING" then
break
end
if
CGameMap.GetNodeType(X1, Y1) == "NODE_TYPE_FORT" and
CGameMap.GetUnitNum(AI_SDK.SelectPos.x, AI_SDK.SelectPos.y) >
CGameMap.GetUnitNum(X1, Y1)
then
break
end
if CGameMap.GetNodeType(X1, Y1) == "NODE_TYPE_BLANK" then
break
end
if CGameMap.GetNodeType(X1, Y1) ~= "NODE_TYPE_HILL" then
break
end
end
AI_SDK.MoveByDirection(
AI_SDK.SelectPos.x,
AI_SDK.SelectPos.y,
move_ratio,
i
)
return
end
return Core
接下来我们来拆分讲解这个函数:
Core = {}
local IsInit = false
local id
首先是定义变量,在这里我定义了2个此文件中的全局变量,IsInit记录是否初始化过,id记录代表自己阵营的数字。
function Core.init()
--我的id
id = AI_SDK.armyID
end
在init函数中,我获取自己的id。
function Core.Record()
-- 这个数组记录了哪些是我的点
local Collar = {}
local X, Y, Num, pos
Num = 0
-- 遍历整个地图
for i = 0, 23, 1 do
for j = 0, 23, 1 do
X = i
Y = j
if CGameMap.GetBelong(X, Y) == id then
table.insert(Collar, {X = X, Y = Y})
Num = Num + 1
end
end
end
-- 返回属于自己点的坐标和数量
return Collar, Num
end
在Record函数中,我想要做到的事返回所有属于我的点的坐标,以及这些点的数量,所以我定义了一个局部数组Collar和Num。之后遍历整个地图,如果发现此点属于自己,就插入此点的坐标{ X , Y }组成的数组到Collar。最后返回Collar和Num。
以下是在主函数中对各功能函数的调用
function Core.Main()
-- 接收返回的数组
local Collar = {}
local Num, X, Y, i,ID
-- 移动的兵力,0.5代表一半
local move_ratio = 0.5
定义一些变量。
if IsInit == false then
Core.init()
IsInit = true
end
Collar, Num = Core.Record()
若未初始化则进行初始化,然后记录所有属于自己的点。
i = 1
-- 随机选择自己的点
while i <= Num / 2 + 1 do
local choice = math.random(Num)
X, Y = Collar[choice].X, Collar[choice].Y
if CGameMap.GetUnitNum(X, Y) > 2 then
break
end
i = i + 1
end
AI_SDK.setSelected(X, Y)
ID = CGameMap.GetBelong(X, Y)
-- 如果此点非法或是军队数量小于二,选择 王
if X == -1 or Y == -1 or ID ~= id or CGameMap.GetUnitNum(X, Y) < 2 then
AI_SDK.setSelected(CGameMap.GetKingPos(id))
end
通过进行若干次循环选择属于自己的某个点,如果此点兵力大于2则跳出循环。
之后将此点设为选中点,获取此点的ID。然后判断:若此点非法(X==-1,Y==-1),或是此点不属于自己,或是此点兵力为1,则选择“王”这一点。
-- 随机在六格方向上移动
while true do
local X1, Y1, NodeType
--获取1~6中的随机数作为选定方向
i = math.random(6)
--获取:假设向选定方向移动后得到的终点的坐标
X1, Y1 =
AI_SDK.DirectionToDestination(
AI_SDK.SelectPos.x,
AI_SDK.SelectPos.y,
i
)
--获取此点的类型
NodeType = CGameMap.GetNodeType(X1, Y1)
--如果是“王”,则直接跳出循环,优先攻击
if NodeType == "NODE_TYPE_KING" then
move_ratio=1
break
end
--如果是堡垒,判断兵力大于此堡垒,修改移动兵力后跳出
if
NodeType == "NODE_TYPE_FORT" and
CGameMap.GetUnitNum(AI_SDK.SelectPos.x, AI_SDK.SelectPos.y) >
CGameMap.GetUnitNum(X1, Y1)
then
move_ratio=1
break
end
--如果是空格,直接跳出循环
if NodeType == "NODE_TYPE_BLANK" then
break
end
end
--向设定的点移动
AI_SDK.MoveByDirection(
AI_SDK.SelectPos.x,
AI_SDK.SelectPos.y,
move_ratio,
i
)
return
end
return Core
这一段实现了随机移动,为了使AI看起来不那么傻,简单加了一些判断条件,具体请看注释。