README 排名评定 游戏教程 安装使用教程 开发指南 Lua接口文档 Python接口文档 C/C++接口文档 地图编辑器说明 自动评测教程

开发指南与示例AI说明

在本文中,我将为你简单地介绍一下Commander项目并手把手带你实现一个简单的AI示例,以方便你快速上手这个项目。

在第一部分中,我们简要地介绍整个项目的基本框架,以及一些你需要知道的注意事项。

A Tour of Commander

在你下载到的源码文件夹中,大致会包含以下一些文件夹,这里略去了你不太需要关心的部分:

`shell 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适应更加复杂的场景。

AI 文件夹探秘

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做出更加详细的说明。首先,让我们看一看你在这场赛事中都需要做些什么:

  1. 填写相关信息并提交报名表,下载项目源文件
  2. 按照安装文档说明,配置love2d等相关环境,试着跑几局游戏来快速了解项目
  3. 阅读包含本文在内的几篇文档来对项目有整体的了解,接着自主探索项目文件
  4. 选择你的开发语言,开发AI接口,并逐步完善
  5. 提交你的AI实现文件(Core.lua或者API.cpp)

下面我们会带领你一步步实现项目源码中的示例AI程序,当然,我们这里只是抛砖引玉,示例AI的功能并不强大,不过相信通过研习这份代码,你将对项目有进一步的认知。

示例AI的工作逻辑介绍

我们将要实现的AI是一个基于“摇骰子“的随机自走机器人。它的主要功能就是:

  1. 随机选择一个占有的位置作为起点
  2. 向四周随机移动

接下来,我将分别介绍 C++ 和 Lua 中对这个逻辑的实现。

C++ 开发指南与示例AI实现

一份空白AI实现模板

`C++ #include #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;
}

LUAREGFUNC(UserImplementation, C_API(userMain)) `

示例AI

前面我们介绍过AI的交互逻辑——通过每次时钟的跳动来唤醒AI核心。这里我们也将从 C++ AI 核心userMain()开始,首先搭建AI的实现框架,这里是一份完整的userMain()

` C++ 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;

} `

下面我们将其拆分,介绍每一部分的详细功能:

Lua 开发指南与示例AI实现

第二份空白AI实现模板

`lua Core = {}

--定义各种变量 local ...

--各种功能函数,如Core.init,Core.Record function Core.XXX()

--你的实现过程
...

end

-- 主函数,AI运行时不断执行此函数中过程 function Core.Main() --你的实现过程

...

end

return Core `

重要函数

`lua -- 获取此点的归属,0代表未占领 CGameMap.GetBelong(x,y)

-- 获取ArmID这一方王的位置,ArmyID是一个全局变量,代表自己的id(不要用来搞歪门邪道哦) CGameMap.GetKingPos(ArmyID)

-- 获取此点的类型,返回 -- "NODETYPEKING"(王) "NODETYPEFORT"(堡垒) -- "NODETYPEHILL"(山) "NODETYPEBLANK"(空格) -- 中的一个 CGameMap.GetNodeType(x,y)

-- 获取此点兵力,返回数值 CGameMap.GetUnitNum(x,y)

-- 判断当前能否看到此点,返回true/false CGameMap.GetVision(x,y) `

示例AI之二

​ 这里我们先给出一份完整的AI实现,然后在后面进行拆分讲解

`lua 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 `

​ 接下来我们来拆分讲解这个函数:

`lua Core = {}

local IsInit = false local id `

​ 首先是定义变量,在这里我定义了2个此文件中的全局变量,IsInit记录是否初始化过,id记录代表自己阵营的数字。

`lua  function Core.init() --我的id id = AI_SDK.armyID end `

​ 在init函数中,我获取自己的id。

`lua 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。

以下是在主函数中对各功能函数的调用

`lua function Core.Main() -- 接收返回的数组 local Collar = {} local Num, X, Y, i,ID -- 移动的兵力,0.5代表一半 local move_ratio = 0.5 `

​ 定义一些变量。

`lua if IsInit == false then Core.init() IsInit = true end Collar, Num = Core.Record() `

​ 若未初始化则进行初始化,然后记录所有属于自己的点。

`lua

	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,则选择“王”这一点。

`lua

	-- 随机在六格方向上移动
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看起来不那么傻,简单加了一些判断条件,具体请看注释。