Skip to content

SDK 文档

SDK文件结构

在游戏包下发的 AI_SDK 文件夹下,你可以看到这样的文件结构:

.
+-- main.cpp
+-- Action.hpp
+-- Action.cpp
+-- sdk
|   +-- ai_client.hpp
|   +-- logic.hpp
|   +-- jsoncpp
|   +-- card.h
|   +-- ……
+-- CMakeLists.txt
+-- example_ai

其中在 sdk 文件夹中的文件是我们给你提供的一套 C++ SDK ,包括了与 judger 的通信、按协议解析收到的信息或打包需要做出的操作、各种游戏中需要用的如牌、手牌状态、记牌器等类的封装和方法的提供以及如计算役种、计算听牌等常见逻辑方法。

example_ai 是我们提供的样例 ai ,你也可以通过参考熟悉 sdk 该如何上手。

选手填写函数

如果选手想要实现自己的AI,应该填写 Action.cpp 中的 solve 函数,该函数说明如下:

功能

已知当前局面信息,当前需要你的AI进行操作,给定了你的AI可以进行的所有合法操作,你需要选择一种操作执行。

函数原型

int AI::solve(std::vector<Action> actions, std::map<std::string, std::pair<int, int> > indexes){

}

参数

传入的第一个参数 actions 是关于类 Action 的列表,里面存储了所有合法的操作,同一种操作类型将占据连续的一段下标。关于 Action 类的具体定义请查看本文档中关于 Action 类的说明。

传入的第二个参数 indexes 是一个映射表,它的 key 是操作的名称(字符串类型,可查看本文档关于 Action 类的说明中提到的操作字符串表示),它的 value 是一个 pair ,其第一个值表示该种操作在 actions 出现的首个下标(整数类型),第二个值表示该种操作在 actions 中出现的次数(整数类型)。

返回值

返回值是一个整数类型,表示选择 actions 中下标多少的对应操作。

如果你的返回值非法(如返回值不在 actions 的合法下标范围中),我们将为你做出默认合法行动。

更多说明

在 Action.hpp 中,我们为你定义了 AI 类, solve 函数便是 AI 类中的一个成员方法。

你可以通过查看对 AI 类的介绍了解你可以在 solve 函数中获得哪些信息以及如何使用各种操作及判断接口。

预定义类介绍

AI / AIClient

AIClient 类定义于 sdk/ai_client.hpp 中,用于完成与 judger 的通信,包括解析信息与封装信息。

AI 类定义于 Action.hpp 中,公有继承自 AIClient 类,是玩家实现的 AI 实例。

成员变量

int benchang(protected)

本场数,代表现在场供里有多少本场棒(一个本场棒代表 300 点)。

int jibang(protected)

积棒数,代表现在场供里有多少立直棒(一个立直棒代表 1000 点)。

int round(protected)

轮数,代表目前是第几轮,从 0 开始编号,其中 0~3 代表东风场, 4~7 代表南风场。

std::vector< std::string > dora(protected)

当前可以看到的表宝牌指示牌的列表,每一项是一个字符串(可查看本文档关于牌的字符串表示的说明)。

std::string doras(protected)

表宝牌集合的字符串表示,每两个字符表示一张表宝牌(可查看本文档关于牌的字符串表示的说明),如 6p1s

Record recorder(protected)

记牌器,可查看本文档关于 Record 类的具体定义。

int my_player_id(protected)

玩家自己的 id ,表示游戏开始时坐在哪一家,如东一局东家 id 为 0,南家 id 为1。

State my_state(protected)

玩家的状态,包括是否立直、手牌及副露情况等,可查看本文档关于 State 类的具体定义。

PlayerInfo players[4] (protected)

各玩家的状态,包括点数、是否立直、手牌数及副露情况、牌河等,下标为玩家的 id ,可查看本文档关于 PlayerInfo 类的具体定义。

成员函数

State trans(State, Action)

非常好用的转移函数!

给定一个状态和一个合法操作,得到该状态经由该操作后得到的状态。

注意,你需要保证传入的操作对于该状态是合法操作!否则可能导致出错!

std::string Get_Yps()

获得对于该玩家而言,这一轮的役牌有哪些,整合为一个字符串,每两个字符表示一张役牌(可查看本文档关于牌的字符串表示的说明),按顺序排布,最后结尾额外加上一个#

例如,东一局东家会得到 0z4z5z6z# (东白发中)。

bool is_YP(std::string card)

给定一张牌的字符串表示(可查看本文档关于牌的字符串表示的说明),判断其是否是对于本轮本玩家而言的役牌, true 表示是役牌。

例如,东一局东家传入 1z (南),则会得到 false 。

其他成员函数

其余的成员函数都用于与 judger 交互,接收 judger 的信息并解析,在需要 AI 操作时调用 solve ,接收AI 的操作并打包信息发给 judger,一般而言你不需要明白它们的工作原理。如果你想要了解,可以配合通讯协议文档并阅读代码和注释。

Action

Action 类定义于 sdk/ai_client.hpp 中,封装了一种操作的类型和参数。

操作类型的字符串表示将会在下面介绍,操作本身的字符串表示将由操作类型的字符串表示与参数通过 # 拼接而成,如 chi#3m0mmoqieli#0pskip#

成员变量

int type(public)

描述操作类型,预定义了枚举变量,可以在代码中查看。

操作类型的数字、实际操作与字符串表示( solve 函数的参数 indexes 就使用这种字符串表示索引)对应如下:

type 操作 字符串表示
0 chi
1 peng
2 暗杠 angang
3 加杠 jiagang
4 明杠 gang
5 手切立直 lizhi
6 摸切立直 moqieli
7 自摸 zimo
8 hu
9 手切切牌 qiepai
10 摸切切牌 moqie
11 跳过 skip
12 非法操作 invalid

各操作可以通过阅读游戏简易规则得知其发动时机及操作内涵。

std::string payload(public)

表示各操作的参数,操作类型、参数格式与内涵的对应如下:

操作类型 参数格式 内涵
chi 3m0m 用来吃的牌
peng 5s0s 用来碰的牌
angang 5m 暗杠的杠牌,不含红五
jiagang
gang 5m5m0m 用来杠的牌
lizhi 0p 切哪张牌宣布立直
moqieli 0p 切哪张牌宣布立直(一定是刚摸的这张)
zimo
hu
qiepai 0p 切哪张牌
moqie 0p 切哪张牌(一定是刚摸的这张)
skip
invalid

成员函数

std::string to_string()

获得操作的字符串表示。

State

State 类定义于 sdk/ai_client.hpp 中,封装了玩家的状态,包括是否立直、手牌及副露情况等。

成员变量

int id(public)

玩家自己的 id ,表示游戏开始时坐在哪一家,如东一局东家 id 为 0,南家 id 为1。

int is_lizhi(public)

玩家是否立直, 1 表示已立直, 0 表示未立直。

bool is_menqing(public)

玩家是否还是门前清状态, 1 表示是门前清状态, 0 表示不是门前清状态。

std::string last_card(public)

玩家最后接收的牌:

1、玩家最后摸到的牌,如到出牌回合摸牌,开杠之后摸牌(摸到的牌不会立即加入手牌中)。

2、需要玩家响应的牌,如上家打出的能吃的牌、对家打出的能碰的牌、下家打出的能和的牌。

int from(public)

玩家最后接收的牌来自哪个 id 的玩家。

std::vector< Card > handcards(public)

玩家当前手牌的列表,每一项是一个 Card 类,保证有序。可查看本文档关于 Card 类的具体定义。

std::vector< CardTuple* > show_cards(public)

玩家当前非手牌区的列表(吃后的顺子、碰后的刻子、杠或加杠的杠子与暗杠),每一项是一个 CardTuple 类的指针,按进入非手牌区时间有序(通过加杠将明刻变成明杠不改变顺序)。可查看本文档关于 CardTuple 类的具体定义。

CardRiver draw_cards(public)

玩家当前的牌河,可查看本文档关于 CardRiver 类的具体定义。

成员函数

void Reset()

重置状态,包括手牌、非手牌情况,立直状态回归未立直,门清状态回归门清,初始化最后接收牌和最后接收牌来向,重置牌河。

void Insert(Card)

往手牌中加入一张牌。

void Show(CardTuple*)

往非手牌区中加入一组牌组。

void Draw(Card)

删除手牌中一张牌(如果手牌中不存在同样的牌,则不操作),区分红五与普通五。

bool is_exist(Card)

返回手牌中是否存在某种牌,区分红五与普通五。

bool is_exist_unreded(Card card)

返回手牌中是否存在某种牌,不区分红五与普通五。

int count(Card)

返回手牌中有多少张某种牌,区分红五与普通五。

std::vector< Card > get_diff_cards()

获得手牌中种类互不相同的牌的列表,区分红五与普通五。

std::string to_string()

获得当前状态的无序牌型表示,可查看本文档关于无序牌型表示的具体定义。

PlayerInfo

PlayerInfo 类定义于 sdk/ai_client.hpp 中,封装了玩家的状态,包括点数、是否立直、手牌数及副露情况、牌河等。

成员变量

int id(public)

玩家自己的 id ,表示游戏开始时坐在哪一家,如东一局东家 id 为 0,南家 id 为1。

int point(public)

玩家的点数,初始为 25000 点。

bool is_lizhi(public)

玩家是否立直, true 表示已经立直。

int sp_cnt(public)

玩家当前的手牌数。

std::vector< CardTuple* > show_cards(public)

玩家当前非手牌区的列表(吃后的顺子、碰后的刻子、杠或加杠的杠子与暗杠),每一项是一个 CardTuple 类的指针,按进入非手牌区时间有序(通过加杠将明刻变成明杠不改变顺序)。可查看本文档关于 CardTuple 类的具体定义。

CardRiver draw_cards(public)

玩家当前的牌河,可查看本文档关于 CardRiver 类的具体定义。

成员函数

void Reset()

重置一个玩家的信息,包括清空手牌数、清空非手牌区以及重置牌河。

void Show(CardTuple*)

往非手牌区中加入一组牌组。

Record

成员变量

std::map< std::string, int > mp(public)

一个牌(用字符串表示)到剩余可见数的映射表。

成员函数

void Reset()

重置映射表。

Card

Card 类定义于 sdk/card.h 中,部分成员函数实现于 sdk/card.cpp 中,该类封装了一张牌应该有的状态,并提供了一些预定义枚举类型(如有需要可以去代码中查看)。

牌类提供了多种方便构造方法,包括直接使用字符串构造,如有需要请查看代码。

牌有以下两种字符串表示:

纯粹字符串表示:由两个字符组成,第一个字符表示数字,第二个字符表示类别,万用 m 表示,饼用 p 表示,索用 s 表示,字用 z 表示。

例如红五万用 0m 表示,九索用 9s 表示,东用 0z 表示。

字符串表示:由两到三个字符组成,前两个字符表示纯粹字符串表示,如果存在第三个字符,有两种可能:

  1. $ ——表示该牌是横置的,如牌河中的立直宣言牌、副露里来自舍家的牌等。
  2. ! ——表示该牌是最后一张接受牌,如刚摸上来的牌、别家放炮的铳牌等。

成员变量

int type(public)

一张牌的类型,对应如下:

type 类型
0
1
2
3
int num(public)

牌的数字。

对于类型为万、饼或索的牌,数字的范围为 [0, 9] ,其中 0 表示红五,其余数字表示对应数字的普通牌。

对于类型为字的牌,数字和实际的对应如下:

num
0
1
2 西
3
4
5
6
int id(public)

一张牌的唯一 id ,可以通过 type 和 num 计算得来。

id = type * 10 + num + 1

int tuple_type(public)

牌在牌组中的类型,1表示竖置的牌,2表示横置的牌(在字符串表示中最后会加$),3表示最后得到的牌(在字符串表示中最后会加!)

bool is_moqie(public)

是否为被摸切掉的牌,默认为 false , true 表示是被摸切掉的牌。(当玩家查看他人牌河中的牌时,这是一个有用信息)

成员函数

int GetNum()

获得牌的数字大小,红五会得到 5 而不是 0 ,用于比较同类型牌的大小。

std::string GetString()

获得牌的纯粹字符串表示。

std::string to_string()

获得牌的字符串表示。

CardTuple

CardTuple 类定义于 sdk/cardtuple.h 中,部分成员函数实现于 sdk/cardtuple.cpp 中,该类是一个抽象类表示牌组,其继承类有顺子、刻子、对子、明杠子和暗杠。

每个类的构造方法都有传入各张牌或传入字符串表示两种,对于后者你需要按照各类牌组的字符串表示规则传入,将在下面有所介绍。

每个类中都保存了组成该牌组的各张牌,以及一些额外的必要信息:

  1. 对于顺子类,各张牌已按从小到大的顺序排好, from_other 将会保存从上家吃来的是从小到大数第几张牌(从 1 开始计数,如果 from_other 为 0 可以表示这是暗顺)。
  2. 对于刻子类,区分红五与普通五, from_other 将会保存是从哪家碰来的, 1 表示从上家碰的, 2 表示从对家碰的, 3 表示从下家碰的, 0 表示是暗刻。你同样可以通过查看对应顺序的牌得知碰来的是不是红五(如 from_other 为 2 ,那么 card2 表示从 对家那碰的牌)。
  3. 对于明杠子类,区分红五与普通五, from_other 将会保存是从哪家碰或杠来的(与刻子类一致), is_jia_gang 保存是否是经过加杠得到的杠子。 你同样可以通过查看对应顺序的牌得知红五的情况,第一张牌代表上家的牌,第二张牌代表对家的牌,第三张牌代表加杠的牌(如果有),第四张牌代表下家的牌(如 from_other 为 2 , is_jia_gang 为 true ,代表从对家碰牌后用摸到的同牌加杠,可以查看 card2 和 card3 确认碰的和摸到的杠材分别是不是红五)。
  4. 对于暗杠类,不区分红五与普通五,只有一个成员变量 card 保存这是关于哪种牌的暗杠(关于 5 的暗杠不能出现 0 ,此谓不区分红五与普通五)。
  5. 对于对子类,只保存两张牌分别是什么,你可以查看其中有没有红五。

关于各个牌组的字符串表示:

  1. 对于顺子类,以大写 C 开头,接下来还有 7 个字符,表示一个吃出来的顺子面子,将被吃的牌放在第一位,后面跟一个$,其余两张按数字大小排在后(如 C4p$3p0p)。
  2. 对于刻子类,以大写 P 开头,接下来还有 7 个字符,表示一个碰出来的刻子面子,在三张牌某一张牌的后面加入$表示从哪一家碰来的,同时这张牌也是对应家舍出的碰材。第一张代表上家,第二张代表对家,第三张代表下家(如 P5s0s$5s)。
  3. 对于明杠子类,以大写 G 开头,接下来还有 9~10 个字符,表示一个被明杠或加杠出来的杠子面子,在四张牌某一张或两张牌的后面会加入\(,同时这张牌也是对应家舍出的碰材/杠材或自家的加杠牌。第一张代表上家,第二张代表对家,第三张代表自家,第四张代表下家。出现两个\)的情况必然是加杠(如 G5m5m5m$0m$)。
  4. 对于暗杠类,以大写 A 开头,接下来只有 2 个字符,表示这是哪张牌的暗杠(如 A5m)。
  5. 对于对子类,以大写 Q 开头,接下来有 4 个字符,表示是哪两张牌(如 Q5m0m)。

CardRiver

CardRiver 类定义于 sdk/cardriver.h 中,部分成员函数实现于 sdk/cardriver.cpp 中,该类用于保存牌河相关信息,并提供一些方便的接口。

牌河的字符串表示是一个有序牌集(可查看本文档关于有序牌集的说明部分),其由各张牌的字符串表示按顺序拼接而成,并在末尾加上 #

例如: 2m0p1z$4s2p#

成员变量

std::vector< Card > cards(public)

按顺序保存牌河中的各张牌的牌列表。

成员函数

void Insert(Card)

往牌河中加入一张牌。

bool is_exist(Card)

判断牌河中是否存在同种牌,不区分红五与普通五(将它们视为同种牌),可以接受传入红五(将会判断牌河中是否存在同类型的五)。

void Reset()

清空牌河的排列表。

std::string to_string()

获得牌河的字符串表示。

牌型表示

牌型表示将会介绍各种接口对于牌型的表示方法,如果你希望使用 sdk/logic.hpp 中提供的逻辑接口,请先熟悉这部分的表示。

有序牌集

按顺序表示每张牌,横置牌后面加 $ ,用 # 结尾。

示例——

5m2p0s2z$0z3m#

无序牌型

由两部分用 # 拼接而成:

1、手牌区的牌,按万、筒、索、字和数字大小的顺序排列,每张牌由 2 个字符表示。数牌是数字+花色,数字上特例是红宝牌数字为 0 ,花色上万、筒、索分别用 m 、 p 、 s 表示。字牌东用 0z 表示,南用 1z 表示,西用 2z 表示,北用 3z 表示,白用 4z 表示,发用 5z 表示,中用 6z 表示。

2、非手牌区的牌,按进入非手牌区的时间顺序从早到晚排列,每个面子使用牌组的字符串表示(可查看本文档中 CardTuple 类中的说明),并在结尾有一个 _ 作分割。

示例——

1m2m!2m2m3m#C4p$3p0p_P5s0s$5s_G5m5m5m$0m$_

1m1m1m2m3m4m5m6m7m!7m8m9m9m9m#

手牌区里是一万、三个二万、一个三万,一组一二三万当顺子,剩余两个二万是雀头。非手牌区是一个用三筒和红五筒吃了上家四筒的顺子,碰了对家红五索的五索刻子,碰了下家的红五万后摸到五万加杠。

第二个例子是门清状态。

有序牌型

开头有一个大写字母, M 表示这是面子牌型, D 表示这是对子牌型, G 表示这是国士无双牌型,后面跟两个 _

对于对子牌型,按万、筒、索、字和数字大小的顺序将张牌排列,共计 7 个对子 14 张牌。

示例——

D__1m1m3m3m5m!0m9s9s5s5s1z1z4z4z

对于国士无双牌型,需要用 0 和 1 表示是否是国士无双十三面, 1 表示是。以及多出来的牌和最后摸到的牌(详见示例)。

示例——

G__0_9p1s!

=> 1m9m1p9p9p1s!9s0z1z2z3z4z5z6z

G__1_9p!

=> 1m9m1p9p9p!1s9s0z1z2z3z4z5z6z

对于面子牌型,同样用 # 拼接手牌区和非手牌区,每边以面子为单位用 _ 分隔,手牌区雀头将会放在最后(大写字母为 Q ),最后摸到的牌后面会加 !

示例——

M__C1m2m!3m_C4m0m6m_Q5m0m#P9m$9m9m_C8m$7m9m_

逻辑接口

在 sdk/logic.hpp 中,我们为你提供了许多的静态方法,方便进行各种判断,你可以阅读本文档牌型表示部分后根据该部分的说明使用。

在此只介绍几个重要的接口(你可以在 sdk/logic.hpp 中根据名字的拼音缩写找到,看到更详细的参数类型信息),其余的接口如有需要可以去源码中查看。

和牌型接口(将无序牌型转化为可能的有序牌型)

内涵:判断一副牌型是否符合和牌型(面子型 4*3+2 ,对子型 7*2 ,国士无双型),不判断是否有役,给出所有可能的和牌型

传入:无序牌型

返回:有序牌型的列表,每一项都是一种可能的和牌型

返回格式:用 % 来连接不同的有序牌型,结尾也有 %

示例——

M__C1m2m!3m_C1m2m3m_C7m8m9m_C7m8m9m_Q5m0m#%D__1m1m2m!2m3m3m5m0m7m7m8m8m9m9m%

算役接口(判断无序牌型是否有役)

内涵:判断一副牌型是否有役,并给出最大的“牌型番数”和符数。“牌型番数”是不考虑红宝牌、里宝牌、偶然役(岭上开花、河底摸鱼、海底摸鱼、抢杠)、悬赏役(一发)的番数计算。

传入:无序牌型,是否自摸,立直状态,场风,役牌的有序牌集,表宝牌的有序牌集

返回:是否符合和牌型,是否有役,“牌型番数”是多少,符数是多少,和牌型是什么,由哪些役及分别多少番组成

返回格式:用/连接四个数字,第一个数字1/0表示是/否符合和牌型,第二个数字1/0表示有/无役,倒数第二个数字(用两个字符表示,个位数补0)表示“牌型番数”(若无役则无意义),最后一个数字表示符数。然后用+开始连接和牌型、役种和番数序列,役种用拼音,和番数间用/隔开。

示例——

1/1/03/30+M__C1m2m!3m_C1m2m3m_C7m8m9m_C7m8m9m_Q5m0m+duanyaojiu/1+baopai/2

0/0/00/00

听牌接口(判断无序牌型是否听牌)

内涵:判断一副牌是否听牌,听哪些牌(不含红五),每张听牌是否有役,有多大“牌型番数”,整幅牌是否振听(也包括立直振听和同巡振听)

传入:无序牌型,立直状态(2/1 (两)立直 0 没立直),场风,役牌的有序牌集,是否振听中,表宝牌的有序牌集

返回:是否振听,听牌面的列表(每一项描述了所听的一张牌、该听牌是否有役以及有多少的“牌型番数”)

返回格式:不同项间用+相连,第一个数字1/0表示是/否振听,听牌面列表每一项用/连接听牌、1/0表示是/否有役和“牌型番数”,“牌型番数”占两个字符

示例——

0+6p/1/03+9p/0/03

和牌接口(判断无序牌型接受一种牌后是否和牌)

内涵:判断一副牌在接受一张牌后能否和牌,和多大的牌(符数在该接口中计算),需要考虑振听(舍张振听在接口内判断,立直振听与同巡振听通过参数传入),需要考虑和牌时机(也就是偶然役与悬赏役),需要考虑所有宝牌(也包括红宝牌和立直后翻出的里宝牌)

使用场景:判断是否放铳或自摸

传入:无序牌型,最后接受牌,和牌方式(自摸/荣和),立直状态,是否庄家,是否是无人鸣牌的第一巡,场风,役牌的有序牌集,是否振听中,是否接受海底牌,是否接受岭上牌,是否加杠,(是否燕返),(是否杠振),是否一发,宝牌的有序牌集

返回:是否和牌(不考虑振听),是否振听,和牌基本点数(在算点接口定义),和牌型是什么,由哪些役及分别多少番组成

返回格式:用/连接五个数字,第一个数字2/1/0分别表示满足和牌但无役/能和牌/不能和牌,第二个数字1/0表示是/否振听,第三个数字表示番数,第四个数字表示符数,第五个数字表示和牌基本点数。然后用+开始连接和牌型、役种和番数序列,役种用拼音,和番数间用/隔开。

示例——

1/1/01/30/240+M__C1m2m!3m_C1m2m3m_C7m8m9m_C7m8m9m_Q5m0m+duanyaojiu/1

算点接口(计算一次和牌最终得点)

内涵:根据和牌基本点数(庄家和牌得点是基本点数的6倍,闲家则是4倍),和牌家是否是庄家,场供情况,计算最终打点

使用场景:和牌后结算

传入:位置号,庄家位置号,放铳位置号(自摸为-1),和牌基本点数,本场场供数,积棒场供数

返回:点数变动的列表,每一项描述了一个玩家的点数变动情况

返回格式:用/连接四个数字,每个数字表示对应位置号的玩家点数变动情况,正数首字符是+,负数首字符是-

示例——

-4000/+8000/-2000/-2000