31 KiB
术语与标准译名
- tile:格子
- unit:单位(一个格子内军队的数量)
- stronghold:要塞(有时亦称为 city)
- capital:首都
- mountain:山区
- swamp:沼泽
- blank:空地
- lighten:明示的
- terrain:地图
- neutral:中立
- player:玩家
- team:队伍
- room:房间
- turn:回合
- half-turn:半回合
- round:轮
- modifiers:特殊规则
- leaderboard:队伍排名
规则
下面介绍游戏规则。
地图
地图是一个 w\times h
的长方形网格,各行各列均从 1
开始编号。每个格子包含一种类型,他们分别是空地、山区、沼泽、要塞和首都。在空地或要塞初始时有一些中立单位(数量可能为 0
或负数)。
时间以及单位的自然产生与消失
有半回合、回合、轮三种时间。 回合是在游戏中的屏幕左上角显示的。 每 25
回合间隔(例如 $25,50,75$)为一轮。玩家可以每半回合将一个方格的单位移动到另一个方格的单位(即每回合可以移动两次;通过任务队列来开展)要塞和首都每回合生成一个单位,沼泽每回合消失一个单位。 拥有的格子(包括沼泽和要塞等)每轮(即 25
回合)产生一个单位。
中立格子不会自动生成任何单位。
任务队列
每名玩家有一个独立的任务队列,里面保存着玩家计划的任务。每个半回合,处理器需要从每个玩家的任务队列中取出一个最早入队的操作,作为该玩家在此半回合的操作。若某一玩家的任务队列为空,则该玩家在此半回合无操作。玩家可以对其任务队列使用如下三种操作:
- 计划操作:向任务队列的末尾添加一个移动;
- 清空队列:清空任务队列;
- 撤销队尾:删除任务队列末尾的元素(若有)。
如果一名玩家在当前半回合的操作不合法,那么该玩家的任务队列将会被清空。
单位移动
每名玩家(以下简称“你”)每半回合可以移动一次(也可以选择不移动),每次移动你可以将某个你占领的格子上驻扎的单位移动到它曼哈顿距离为 1
的一个格子上。你不能移动到山区上,山区上不能驻扎任何单位。
如果你占领了一个方格,则在方格中移动总是会在方格上留下至少一个单位(也可以选择留下一半的单位,向下取整)。移动的单位数量必须为正数(不能为零),否则操作不合法。空方格只需花费一个单位即可占领方格(根据定义,空方格以前没有被占领过——因此你可以控制一个只有两个单位军队的空方格)。
若你的目标方格已经有敌军的单位,用目标方格上对方的单位数量减去你移动的单位数量,若为负数则目标方格变为你占领的格子,单位数量为你移动的单位数量减去目标方格上原有的对方的单位数量,若为非负数则目标方格任然为对方所有,单位数量变为目标方格上原有的对方的单位数量减去你移动的单位数量。
若你的目标方格为自己或队友占领的格子,则目标格子的单位数量变为移动的单位数量加上原有的单位数量。若目标格子不为队友的首都,则你会占领目标格子。
移动优先级
如果玩家 A 的移动优先于玩家 B,这意味着即使玩家 A 和玩家 B 同时下手,玩家 A 的动作也会先执行。 这可以对游戏产生影响。
玩家之间的移动优先级每半回合逐个交替一次,逐个交替意味着当前最优先的玩家在交替后变为优先级最低的玩家,其他玩家优先级顺次增加一位。 在给定的移动中,如果玩家 A 的移动优先级高于玩家 B,则玩家 B 的移动优先级将高于玩家 A 的下一步移动。 例如,在一个两人对战中如果您正追赶正在入侵您土地的敌人的尾巴,那么您最多只需要一个回合就可以阻止他们的入侵,因为您保证每隔一次移动就拥有优先于他们的移动。
视野
除非启用了特殊规则 Crystal Clear,玩家并不能获得游戏中的全部信息,玩家的视野包括:
- 地图上全部明示的格子(在启用特殊规则 Misty Veil 时,此项不生效);
- 任何一个与你或你的队友某个已经占领了的格子的切比雪夫距离小于等于
1
的格子(在启用特殊规则 Misty Veil 时,此项可能有变化)。
在你视野中的每个格子都会显示出格子的类型、占领方和驻扎的军队数量。对于你视野之外的区域,每个格子上的占领方和驻扎的军队数量对你隐藏,并且只透露部分格子类型信息。具体而言,你可能看到三种记号,分别是疑似空地、疑似山区、沼泽,这三种格子分别代表了
- 疑似空地:空地或首都;
- 疑似山区:山区或要塞;
- 沼泽:沼泽。
显然,在这个规则下,一个队伍中的全部玩家总是用于相同的视野和信息,因此之后我们将总是讨论某一队伍的视野,而不是某一玩家的视野。
队伍排名
在启用 Silent War 特殊规则时,此项不生效。游戏会实时向玩家展示一个队伍排名,队伍按照所有属于该队伍的玩家占领的格子上的单位之和从大到小排序,在每个队伍内,属于该队伍的玩家按占领的格子上的单位之和从大到小排序。对于每个玩家,将公开如下信息:
- 该玩家是否已经失败;
- 该玩家当前占领的格子数量之和;
- 该玩家当前占领的格子上的单位数量之和。
通知事件
在启用 Silent War 特殊规则时,此项不生效。当有玩家占领了其他(敌对)玩家的首都时,或者在有玩家掉线时,游戏会发布一条广播消息。
胜利条件
首都是你在游戏开始时控制的(唯一的)棋子,用皇冠符号表示。如果你的首都被敌人占领,你将输掉游戏,占领你的首都的玩家将控制你的所有格子和单位。当游戏中剩余的玩家均属于同一只队伍时,该队伍的全体玩家获胜。
当你占领了某个(敌对)玩家的首都时,你会接管他的占领全部全部格子。具体而言,每个由他占领的格子都变为由你占领,并且格子上的单位数量为他原有的单位数量除以 2
向下取整加 $1$。他的首都会变为由你占领的一个要塞。
玩家掉线
当一个玩家掉线后一段时间,适配器认为该玩家无重连的可能性后,会向处理器发送掉线通知。处理器收到掉线通知后,
- 若该玩家是其队伍中唯一一个还未输掉游戏的玩家,则其全部单位变为中立单位。
- 否则,寻找首都距离该玩家首都最近的同一队伍的玩家,并将该玩家所全部占领的格子所变为该队友所占领,并把他的首都变为一个要塞。这里的距离是指在不经过任何山区和要塞的前提下将单位从一个格子移动到另一个格子的最小步数。
每半回合处理次序
- 收集全部玩家的行动;
- 按移动优先级执行各个玩家的移动,并同时处理胜利条件;
- 执行方格上单位的生成和消失;
- 处理玩家掉线事件;
- 计算各个玩家的视野以及队伍排名。
特殊规则
下面介绍几种特殊规则,每个特殊规则以一个有符号整数作为参数,不过通常这个整数没有意义。
Leapfrog
当你占领了其他玩家的首都时,该你的的首都迁移到他的首都的位置,你原来的首都变为一座要塞。
City-State
每个玩家在开局时在其旁边生成一座已经由他占领了的要塞,该选项仅仅对随机生成的地图有效。
Misty Veil
每个玩家只能看见任何一个与你或你的队友某个已经占领了的格子的切比雪夫距离小于等于 x
的格子,这里的 x
为规则参数。注意在启用此规则时,明示的格子也不再总是视野的一部分。
Crystal Clear
移除战争迷雾,也即所有格子均对所有玩家明示。此规则的优先级高于规则 Misty Veil。
Silent War
移除队伍排名和通知事件消息。
交互
下面是适配器和处理器的交互方式,适配器会在控制器之间建立全双工通道。适配器向处理器传输数据简称为“给出”,处理器向适配器传输数据简称为“回答”。
对于处理器,数据由标准输入流(stdin
)给出,结果返回在标准输出流(stdout
),日志在标准异常流(stderr
)输出。注意每次在标准输出流返回数据后要刷新缓冲区。
日志
日志在标准异常流(stderr
)输出,日志包含调试(D
)、信息(I
)、警告(W
)、异常(E
)、致命(F
)五个等级,格式为时间、等级和详细信息,随后尾随控制字符 LF(\n
),即 {year}-{month}-{day} {hour}:{minute}:{second} [{level}] {msg}\n
。例如
2022-10-10 21:00:01 [W] discarded bad move(34 57E) from player 1: target is out of border.
2022-10-10 21:00:01 [W] 已丢弃非法移动 (34 57E) 来自玩家 1: 目标格在边界外.
日志等级与编码
日志分为五个等级,等级越高,错误越严重,使用如下表格:
等级名称 | 标识字母 | 通信编码 |
---|---|---|
调试 | D | 0x00 |
信息 | I | 0x01 |
警告 | W | 0x02 |
错误 | E | 0x03 |
致命 | F | 0x04 |
处理器内部使用一个日志过滤等级变量,当日志等级小于日志过滤等级时,这条日志应当被跳过。
玩家与队伍的标识
玩家和队伍使用单个无符号 8 位整数作为编号,均从 1
开始。编号为 0
的玩家和队伍为中立势力保留。
位置与坐标
棋盘的位置从 0
开始编号,一个二元组 (x,y)
可以唯一确定一个位置,即第 x
行第 y
列。
移动方向及其编码
每一步玩家可以向四个方向移动,这四个方向见下表:
朝向字母 | 方向字母 | 通信编码 | X坐标变化 | Y坐标变化 |
---|---|---|---|---|
U | N | 0x00 | -1 |
0 |
D | S | 0x01 | +1 |
0 |
L | W | 0x02 | 0 |
-1 |
R | E | 0x03 | 0 |
+1 |
特殊规则的编码
特殊规则的表格如下:
规则名称 | 通信编码 | 参数含义 |
---|---|---|
Leapfrog | 0x01 | 无 |
City-State | 0x02 | 无 |
Misty Veil | 0x03 | 可见距离 |
Crystal Clear | 0x04 | 无 |
Silent War | 0x05 | 无 |
格子类型的编码
当一个格子在视野内时,使用格子类型的通信编码。当其离开视野时,其类型变为“离开视野”(0x00),此时会使用预先加载的迷雾地形。
格子类型 | 通信编码 | 迷雾类型 | 迷雾通信编码 |
---|---|---|---|
(离开视野) | 0x00 | (不适用) | (不适用) |
山区 | 0x01 | 疑似山区 | 0x01 |
要塞 | 0x02 | 疑似山区 | 0x01 |
空地 | 0x03 | 疑似空地 | 0x02 |
首都 | 0x04 | 疑似空地 | 0x02 |
沼泽 | 0x05 | 沼泽 | 0x03 |
跨进程过程调用
为了实现适配器和处理器的交互,我们约定一种被称为跨进程过程调用的格式。在跨进程过程调用中,我们把处理器视为一个状态机,它接受各种函数调用,并根据函数的参数和内部状态返回一些值,同时更新内部状态。
通信过程总是使用二进制,字节序为小端序,处理器实现可以假定原生端序总是小端序。适配器调用函数时,会向处理器给出一个函数调用数据包,按顺序包含如下内容:
- 函数调用的标识符(8字节);
- 调用的函数的标识符(4字节整数);
- 函数参数的数据长度(4字节无符号整数);
- 函数参数的数据。
函数返回时,应当向适配器回答一个函数返回数据包,按顺序包含如下内容:
- 函数调用的标识符(8字节,值应当与调用数据包中的调用标识符相等);
- 函数返回的数据长度(4字节无符号整数);
- 函数返回的数据。
当函数返回的数据长度为 0xFF'FF'FF'FF 时,表示函数调用过程中遇到了异常。
结构化数据编码
在把一些数据编码为二进制数据时,逐个将其成员按照顺序紧密地放在一起。在编码整数或浮点数时,直接使用小端序的二进制表示。在编码字符串或不定长数组时,首先给出一个 4 字节的无符号整数表示长度,然后逐个给出其成员元素。
过程调用中的类型
我们首先定义以下类型为基础类型:
u8
、u16
、u32
、u64
:无符号 8/16/32/64 位整数;i8
、i16
、i32
、i64
:有符号 8/16/32/64 位整数;str
:字符串,本质上是u8
数组,使用 UTF-8 编码;T[]
:其中T
是一个类型,表示类型为T
的不定长数组;T[n]
:其中T
是一个类型,n
是一个正整数,表示类型为T
的长n
的数组。
定义以下复合类型:
类型 bool
:用于表示一个布尔值。
成员标识符 | 类型 | 含义 |
---|---|---|
value |
u8 |
值 |
类型 void
:用于表示一个空值。
(此类型没有成员)
类型 uuid
:用于表示一个全局唯一标识符。
成员标识符 | 类型 | 含义 |
---|---|---|
low |
u64 |
值的低 64 位 |
high |
u64 |
值的高 64 位 |
类型 DateTime
:用于表示一个日期时间,精确到秒。
成员标识符 | 类型 | 含义 |
---|---|---|
timestamp |
u64 |
时间的 UNIX 时间戳 |
类型 Player
:用于标识一个玩家。
成员标识符 | 类型 | 含义 |
---|---|---|
id |
u8 |
玩家编号 |
类型 Team
:用于标识一个队伍。
成员标识符 | 类型 | 含义 |
---|---|---|
id |
u8 |
队伍编号 |
类型 ImportTerrainConfig
:用于导入地图的参数,该类型用于 importTerrain
的参数。
成员标识符 | 类型 | 含义 |
---|---|---|
w |
u8 |
地图的列数 |
h |
u8 |
地图的行数 |
value |
str |
导入的地图的字符串表示 |
类型 RandomTerrainConfig
:用于随机生成地图的参数,该类型用于 randomTerrain
的参数。
成员标识符 | 类型 | 含义 |
---|---|---|
w |
u8 |
地图的列数 |
h |
u8 |
地图的行数 |
city_dense |
u8 |
地图生成中要塞格子的生成密度 |
mountain_dense |
u8 |
地图生成中山区格子的生成密度 |
swamp_dense |
u8 |
地图生成中沼泽格子的生成密度 |
light_dense |
u8 |
地图生成中明示格子的生成密度 |
类型 PlayerMove
:用于表示一次玩家操作,该类型用于 appendOrderQueue
的参数。
成员标识符 | 类型 | 含义 |
---|---|---|
player |
Player |
操作的玩家 |
x |
u8 |
移动操作出发格的 X 坐标 |
y |
u8 |
移动操作出发格的 Y 坐标 |
dir |
u8 |
移动的方向的通信编码 |
half |
bool |
是否只移动一半的单位 |
类型 PlayerRanking
:用于表示一名玩家在队伍排名上的信息,该类型用于 TeamRanking
的成员。
成员标识符 | 类型 | 含义 |
---|---|---|
player |
Player |
表示的玩家 |
is_defeated |
bool |
表示的玩家 |
land |
u16 |
玩家占领的格子的总数 |
unit |
i32 |
玩家拥有的单位总数 |
类型 TeamRanking
:用于表示一只队伍在队伍排名上的信息,该类型用于 GameTickState
的成员。
成员标识符 | 类型 | 含义 |
---|---|---|
team |
Team |
表示的队伍 |
land |
u16 |
队伍占领的格子的总数 |
unit |
i32 |
队伍拥有的单位总数 |
players |
PlayerRanking[] |
队伍中的玩家的信息 |
类型 Tile
:用于表示一个格子上的信息。
成员标识符 | 类型 | 含义 |
---|---|---|
owner |
Player |
占领此格的玩家 |
type |
u8 |
格子类型的通信编码 |
unit |
i32 |
格子上的单位数量 |
类型 TileDiff
:用于表示一个格子的视野变化,该类型用于 TeamViewDiff
的成员。
成员标识符 | 类型 | 含义 |
---|---|---|
x |
u8 |
此格子的 X 坐标 |
y |
u8 |
此格子的 Y 坐标 |
value |
Tile |
格子上的在视野内的值 |
类型 TeamViewDiff
:用于表示一个队伍的视野差分,该类型用于 GameTickState
的成员。
成员标识符 | 类型 | 含义 |
---|---|---|
team |
Team |
队伍 |
diffs |
TileDiff[] |
有变化的格子 |
类型 GameTickState
:用于表示一半回合结束时的状态,该类型用于 tick
的返回值。
成员标识符 | 类型 | 含义 |
---|---|---|
halfturn_id |
u32 |
当前的半回合数 |
defeated_players |
Player[] |
在这个半回合内被打败的玩家列表 |
alive_players |
Player[] |
剩余的未被击败的玩家 |
leaderboard |
TeamRanking[] |
队伍排名 |
diffs |
TeamViewDiff[] |
各个队伍的视野差分 |
msg |
str[] |
消息和事件通知 |
类型 Modifier
:用于表示一条特殊规则,该类型用于 InitInfo
的成员。
成员标识符 | 类型 | 含义 |
---|---|---|
key |
u32 |
特殊规则的通信编码 |
value |
i32 |
特殊规则的参数 |
类型 InitInfo
:用于游戏初始化的各种信息,该类型用于 init
函数的参数。
成员标识符 | 类型 | 含义 |
---|---|---|
n |
u8 |
玩家数量 |
player_team |
Team[] |
各个玩家的所在的队伍 |
modifiers |
Modifier[] |
特殊规则列表 |
类型 ReplayStep
:用于表示游戏回放的一个半回合内发生的信息,该类型用于 Replay
的成员。
成员标识符 | 类型 | 含义 |
---|---|---|
halfturn_id |
u32 |
当前的半回合数 |
player_moves |
PlayerMove[] |
各个玩家的本半回合的操作的列表 |
offline_events |
Player[] |
本半回合内掉线的玩家列表 |
类型 PlayerInfo
:用于表示一名玩家的完整信息。
成员标识符 | 类型 | 含义 |
---|---|---|
uid |
uuid |
玩家标识符 |
id |
Player |
玩家在游戏内的编号 |
team |
Team |
玩家在游戏内的队伍 |
name |
str |
玩家的显示名 |
类型 Replay
:用于表示游戏回放。
成员标识符 | 类型 | 含义 |
---|---|---|
rid |
uuid |
回放的标识符 |
start_time |
DateTime |
游戏的开始时间 |
speed |
i8 |
游戏速度 |
init_info |
InitInfo |
游戏初始化的配置 |
length |
u32 |
游戏进行的回合数 |
player_num |
u8 |
参与游戏的玩家数 |
players |
PlayerInfo[] |
各个玩家信息 |
winner |
Team |
取得胜利的队伍 |
terrain |
Tile[] |
第 0 个半回合开始时,地图各个格子的信息 |
steps |
ReplayStep[] |
各个半回合的信息 |
类型 GameInfo
:表示一局游戏设置的信息,用于 saveReplay
函数的参数。
成员标识符 | 类型 | 含义 |
---|---|---|
rid |
uuid |
回放的标识符 |
speed |
i8 |
游戏速度 |
players |
PlayerInfo[] |
各个玩家信息 |
处理器实现的函数
处理器需要实现以下函数:
标识符 | 函数名 | 函数功能 | 函数参数类型名 | 函数返回值类型名 |
---|---|---|---|---|
0 | setSeed |
设置随机种子 | u32 |
void |
1 | setLogLevel |
设置最低的日志等级 | u8 |
void |
2 | getError |
获得上一个错误 | void |
i32 |
3 | getErrorInfo |
获得上一个错误消息 | void |
str |
10 | importTerrain |
导入预设地图 | ImportTerrainConfig |
bool |
11 | randomTerrain |
生成随机地图 | RandomTerrainConfig |
bool |
20 | appendOrderQueue |
向玩家的行动队列的末尾添加一个移动 | PlayerMove |
void |
21 | clearMoveQueue |
清空玩家的行动队列 | Player |
void |
22 | popOrderQueue |
清除玩家的行动队列的最后一项 | Player |
void |
23 | setOffline |
通知玩家掉线 | Player |
void |
30 | init |
初始化 | void |
u8[] |
31 | tick |
执行下一个半回合 | void |
GameTickState |
40 | getKeyFrame |
返回某一队伍的视野 | Team |
Tile[] |
50 | saveReplay |
保存回放 | GameInfo |
Replay |
导入地图的格式
在调用 importTerrain
时,会给出参数 ImportTerrainConfig
。其中用字符串编码了要导入的地图各个格子的地形,两个格子的信息之间用逗号分隔,例如
m, ,gA99,-12,s,n-10,g 1,L_10,L_n5
每个格子的信息对应逗号之间的字符串,保证恰好有 h\times w-1
个逗号,其中第 i
行第 j
列的格子的信息在第 iw+j-1
个逗号后给出(特别的,第 0
行第 0
列的格子的信息在最开头)。若有 L_
前缀,表示该格子是明示格。然后紧随一个类型,表示该格子的主类型:
符号 | 格子类型 | 描述 |
---|---|---|
(空格) | 空地 | 该格子是无中立单位的空地 |
m |
山区 | 该格子是山区 |
n |
空地 | 该格子是可以有中立单位的空地 |
s |
沼泽 | 该格子是沼泽 |
g |
首都 | 该格子是候选首都 |
(无) | 要塞 | 该格子是要塞 |
除了候选首都之外,随后对于有中立军队的格子或要塞会紧随一个数字表示该格子上的初始中立单位数量,注意可能为负数或 $0$。候选首都的格式请参加下面的的“首都分配机制”小结。下面是一些描述格子的例子以及解释:
例子 | 解释 |
---|---|
14, |
驻扎有 14 个中立单位的要塞 |
gA99, |
队伍 A 的优先级为 99 的候选首都 |
g , |
剩余队伍的优先级为 100 的候选首都 |
s, |
沼泽 |
L_n-99, |
驻扎了 -99 个中立单位的明示空地 |
首都分配机制
若地图不使用首都标记时,在空地中随机选取一些地方作为玩家的首都。
地图使用无组队记号的首都标记时,候选首都由单个字母 g
标记而没有任何尾随信息。此时给每个玩家随机一个分配一个候选首都作为这个玩家的首都即可,若候选首都数量不够,则在空地中随机选取一些地方作为剩余玩家的首都。
地图使用有组队记号的首都标记时,候选首都在字母 g
后立即给出一个大写字母或空格,字母表示这个候选首都的队伍标识,空格表示这个候选首都为剩余候选首都,然后给出可能会一个 1
至 99
中的整数表示这个候选首都的优先级,若没有指定优先级,则候选首都优先级默认为 $100$。
此时给每个队伍随机分配对应一个队伍标识。对于一个队伍,创建一个候选列表,从大到小枚举优先级,并将其该队伍对应的队伍标识包含的全部该优先级的候选首都加入候选列表中,直到候选列表中包含至少等于该队玩家数量个候选首都为止。然后在列表中随机选取作为该队伍玩家的首都。若该队伍玩家数量多于队伍标识包含的全部的候选首都数量,则随机选择该队伍玩家数量减去队伍标识包含的全部的候选首都数量个玩家标记为“剩余玩家”,不参与该队伍的首都分配。
若队伍标识数量少于队伍数量,则随机选择队伍数量减去队伍标识数量个队伍,并将选出的队伍中的全部玩家标记为“剩余玩家”,选出的队伍不参与队伍标识分配。对于剩余玩家,创建一个候选列表,从大到小枚举优先级,并将其全部为该优先级的剩余首都都加入候选列表中,直到候选列表中包含至少等于剩余玩家数量个候选首都为止。然后在列表中随机选取作为剩余伍玩家的首都。若剩余玩家数多于剩余首都数,则在空地中随机选取一些地方作为剩余玩家的首都。在随机选择空地为首都时,至少要满足以下条件:
- 将山区视为不可走的点时,生成的地图中的首都、空地、要塞和沼泽连通;
- 将山区和首都视为不可走的点时,生成的地图中的空地、要塞和沼泽连通;
- 没有两名玩家的首都相邻。
若无力进行合适的首都分配,则应生成随机地图。但当以下条件都满足时,应确保能进行合适的首都分配:
- 地图中任意候选首都都不相邻;
- 队伍数量不超过队伍标识数量;
- 每个队伍的玩家数量都不超过每个队伍标识所包含的全部的候选首都数量。
错误处理
不能保证给出的地图总是正确,请判断格式不正确的地图(若将山区视为不可走的点时,导入地图中的候选首都、空地、要塞和沼泽不连通,则视为该导入地图格式不正确)。
随机地图的生成参数
在调用 randomTerrain
函数时,会传入 RandomTerrainConfig
作为随机地图的生成参数。city_dense
、mountain_dense
、swamp_dense
和 light_dense
这 4
个参数的类型均为 u8
,除以 255
后,得到一个 [0,1]
范围内实数,分别记作 $d_c,d_m,d_s,d_l$,这四个值的使用方法取决于具体的地图生成算法。
随机地图的生成方法
如何随机生成地图的具体方法应当是由处理器自行实现的,下面给出几种参考算法。
值得注意的是,不论使用何种生成算法,应当至少满足以下条件:
- 将山区和要塞视为不可走的点时,生成的地图中的空地和沼泽连通;
- 地图包括至少
16
个空地。
下面假设地图的大小为 h
行 w
列。
简单生成算法
简单地图生成前,首先根据公式计算各个类型的格子的计划生成量,其中 p_c,p_m,p_s,p_l
分别表示计划生成的塞、山区、沼泽和明示的格子数量:
\begin{aligned}
p_m&=d_mwh/3\\
p_c&=d_cp_m/2\\
p_s&=d_s(wh-p_m)/2\\
p_l&=d_lwh
\end{aligned}
第一步,在地图上随机选择恰好 p_m
个格子,将类型设置为山区,剩下的格子的类型设置为空地。
第二步,寻找一个将尽可能少的山区变为空地的方案,使得空地连通。
第三步,在山区中随机选择 p_c
个格子,将这些山区的格子变为要塞。并在空地中选择 p_s
个格子,将它们变为沼泽。
第四步,在整个地图上随机选择 p_l
个格子,将它们设置为明示格。
在恰当实现的情况下,该算法的时间复杂度为 $O(wh)$。
随机地图的首都分配方法
如何在随机地图上分配首都应当是由处理器自行实现的,下面给出几种参考算法。
值得注意的是,不论使用何种分配算法,应当至少满足以下条件:
- 将山区和首都视为不可走的点时,生成的地图中的空地、要塞和沼泽连通;
- 玩家的首都应当总是分配在空地上;
- 没有两名玩家的首都相邻。
下面假设地图的大小为 h
行 w
列,玩家数为 $n$,队伍数为 $t$。
势力染色分配算法
把空地格子作为图中的结点,在相邻的空地之间连一条无向边,那么我们找出所有不是割点空地,这些空地作为候选首都点。
我们在地图中随机选择恰好 t
个候选首都点,每个队伍分配一个点,作为该队伍的势力核心。我们称,一个候选首都点属于队伍 $x$,是指该这个格子距离队伍 x
的势力核心,比任何其他队伍的势力核心更近,如果有多个队伍的势力核心距该点的距离都是最小的,那么该点不属于任何一个队伍。这里的距离是指在不经过任何山区和要塞的前提下将单位从一个格子移动到另一个格子的最小步数。
对于每只队伍,我们在属于该队伍候选首都点中随机选择等于该队伍的玩家数量的不相邻的格子作为该队伍的玩家的首都。如果没有这么足够的候选首都点,则重新选择各个队伍的队伍的势力核心。
在恰当实现的情况下,该算法的时间复杂度为 $O(kwh)$,其中 k
为重试次数。
最小配对分配算法
把空地格子作为图中的结点,在相邻的空地之间连一条无向边,那么我们找出所有不是割点空地,这些空地作为候选首都点。
我们在地图中随机选择恰好 n
个不相邻候选首都点,然后寻找一种把这 n
个格子分配给 n
个玩家的方案,使得每个玩家的首都和它的队友的首都之间的距离的 k
次方的和尽可能小。这里的 k
是一个常数,可以考虑取 $1$、2
或 $3$。这里的距离是指在不经过任何山区和要塞的前提下将单位从一个格子移动到另一个格子的最小步数。
在使用状态压缩的动态规划和枚举子集的实现时,该算法的时间复杂度为 $O(pwh+t3^n)$。
组队情况
适配器会在调用 init
时传入玩家以及组队情况。具体而言,init
函数的参数是 InitInfo
,其中的 player_team[i]
表示第 i
名玩家的队伍。特别的,保证 player_team[0]
等于 $0$。
迷雾地形回响
当调用函数 init
时,控制器回答地图的每个格子的视野外状态,使用如下表格:
名称 | 通信编码 | 对应的格子类型 |
---|---|---|
疑似山区 | 0x01 | 山区或要塞 |
疑似空地 | 0x02 | 空地或首都 |
沼泽 | 0x03 | 沼泽 |
将每个格子的通信编码用一个 u8
表示,得到一个长度为 h\times w
的数组,作为 init
函数的返回值,其中第 i
行第 j
列的格子的信息存在数组的第 iw+j
个位置。例如,对于导入的地图(备注:实际上地图大小应当至少为 $5\times 5$,这里的 3\times 3
的地图只是为了方便举例子):
{w: 3, h: 3, value: "m, ,gA99,-12,s,n-10,g 1,L_10,L_n5"}
则迷雾地形回响为 [1, 2, 2, 1, 3, 2, 2, 1, 2]
。
视野差分
处理器内部会维护每只队伍,以及旁观者的当前视野。每当地图发生除了自然变化(即单位的自然产生与消失)之外的变化时,或者玩家的视野范围发生变化时,会计算新的视野与旧的视野的变化,称之为视野差分。
当一个格子发生变化时,这个变化用一个 TileDiff
类型描述,其中成员 x
和 y
表示格子的位置,value
表示格子变化后的状态。特别的,当一个格子格子离开视野时,value
的 Tile
的成员中的 owner
、type
和 unit
的值均为 $0$。
半回合处理回响
当调用 tick
函数时,处理器需按照游戏规则处理游戏逻辑,并算出需要向玩家展示的信息,返回一个 GameTickState
类型。
在每个半回合处理时,处理器应当按顺序执行以下操作:
- 收集全部玩家的行动;
- 按移动优先级执行各个玩家的移动,并同时处理胜利条件;
- 执行方格上单位的生成和消失;
- 处理玩家掉线事件;
- 计算各个玩家的视野差分以及队伍排名。
约束
- 可以假设每个格子所拥有的单位均在任何时刻均在
32
位整型范围内。 - 地图的大小 $5\leqslant w,h\leqslant 255$。
- 种子为整数 $-2^{31}\leqslant x\leqslant2^{31}-1$,可以假设任意两局游戏给出的种子均不相同。
- 玩家数和队伍数不超过 $16$。