本项目包含一个用C++实现的日本麻将游戏,提供C++与Python接口,以便:
* 实现不同种类的AI Agent
* 人类游玩(并不
C++环境下:
- 运行内存 约800KB
- 单局时间 约10ms(10.1s with 1000 plays)
测试方法:
- 基于main.cpp中test_passive_table_auto,运行1000次,取运行时间平均值
- 其中每个动作都是由std::uniform_distribution进行随机选择
关于内存:
- 本项目中主要使用stl容器,没有使用到任何new/delete运算符,因此不会发生内存泄露问题。
通过对天凤2020年30万局段位战牌谱进行重放式测试。在其中99.9%以上的牌局中表现一致。剩下的牌局中主要是分数计算上,因为双倍役满/翻dora时机/包牌等导致不一致。
测试只能保证操作能回放,不能保证是否出现错误的操作待选项(例如无法ron的情况仍然有ron的选项)。这一点只能通过大量实战进行测试。
git clone https://github.com/Agony5757/mahjong/
cd mahjong
sudo apt-get upgrade
sudo apt-get update
sudo apt-get install -y clang cmake
python setup.py install
初始化
循环
{
获取状态(get_phase), 判断:
case 1 (SelfAction状态)
拥有3n+2张牌 允许: 出牌(立直), 自摸, 暗杠, 加杠, 九种九牌
case 2 (ResponseAction状态)
拥有3n+1张牌 允许: 吃, 碰, 杠, 荣, 抢杠, 抢暗杠
case 3 (GameOver状态:流局/荣/自摸……)
退出循环
执行操作(make_selection)
}
获取结果:役/分数/连庄/本场/供托……
#include <iostream>
#include "Table.h"
int main()
{
Table t;
t.set_write_log(true);
t.game_init();
try {
while (t.get_phase() != Table::GAME_OVER)
{
fmt::print("{}\n", t.to_string());
fmt::print("{} is making selection.\n", t.who_make_selection());
if (t.get_phase() < 4)
{
fmt::print("SelfAction phase.\n");
auto& actions = t.get_self_actions();
/* inspect all available actions here */
int selection;
/* replace this with your selection method */
t.make_selection(selection);
}
else
{
fmt::print("Response Action phase.\n");
auto& actions = t.get_response_actions();
/* inspect all available (response) actions here */
int selection;
/* replace this with your selection method */
t.make_selection(selection);
}
}
fmt::print("Result: \n{}", t.get_result().to_string());
}
catch (std::exception& e) {
// output replay & debug codes
t.print_debug_replay();
}
return 0;
}
Tile: 牌对象,保证唯一性,在初始化阶段同时初始化,理论生命周期等同于Table - BaseTile:牌枚举,详见Tile.h - id: 和天凤0~135编码一致的编码 - red_dora: 红宝牌标记
Tile*: 标记一个牌,可以被任意复制,移动。不保证唯一性。
BaseTile:见Tile
BaseAction:动作的枚举,包括所有可以主动执行的操作(吃碰杠立出胡etc),不含“摸牌”,因为摸牌是被动执行的
SelfAction:自身动作的结构体(和ResponseAction结构相同) ResponseAction:回复动作的结构体 - BaseAction:见BaseAction - correspond_tiles:vector<Tile*> 表示这个动作关联的牌型(sorted),详见Action.h内注释
get_action_index:提供一种可以从vector<SelfAction/ResponseAction>中,根据BaseAction和Tile*/BaseTile寻找动作的方式
包括一系列玩家自身持有的状态和数据
- riichi/double_riichi :立直/双立直
- 门清
- wind:自风
- 亲
- 振听(同巡/舍牌/立直)
- score:点
- hand:手牌
- river:牌河 : 见[玩家.牌河]
- 副露s:副露 : 见[玩家.副露]
- 听牌
- 一发标记
- 第一巡标记
RiverTile:所有牌河中的牌(弃牌,not include 暗杠/加杠)
- tile*:对应的牌
- number:是全局第几个弃牌(所有人公用一套id)
- remain:表示这张牌是否被鸣走(尽管被鸣走,牌河中也会存在一个复制)
- fromhand:手切/摸切
牌被打出去时,会立即加入牌河中,如果被鸣走,则remain=false
Type: 属于的类型(Chi, Pon,大明杠,加杠,暗杠)
tiles:vector<Tile*> 对应的牌(加杠之后会把Pon->加杠)
take:对于Chi,Pon,大明杠,加杠,第几张牌是横过来的(拿的别人的牌)
参数类型: unordered_map: string, string 表示元数据
metadata现在支持的Key为: "yama": "1z1z.... 牌山初始化(默认随机开始) "oya": "0"/"1"/"2"/"3" 亲家,默认为"0" "wind": 东风局/南风局/西风局分别为"east","north","south",默认为"east" "deal": "from_0"/"from_oya" 从谁开始发牌(从0号玩家/从庄家开始发牌),默认为"from_0"
可以进一步通过访问成员来对数据进行获取,包括:
(pybind11封装的接口形式下)
.def_readonly("dora_spec", &Table::dora_spec)
.def_readonly("DORA", &Table::宝牌指示牌)
.def_readonly("URA_DORA", &Table::里宝牌指示牌)
.def_readonly("YAMA", &Table::牌山)
.def_readonly("players", &Table::players)
.def_readonly("turn", &Table::turn)
.def_readonly("last_action", &Table::last_action)
.def_readonly("game_wind", &Table::场风)
.def_readonly("last_action", &Table::last_action)
.def_readonly("oya", &Table::庄家)
.def_readonly("honba", &Table::n本场)
.def_readonly("riichibo", &Table::n立直棒)
仅在从get_phase/get_phase_mt中获取到GAME_OVER/GAME_OVER_MT值之后,可以获取结果
返回类型是Result类型,参见Result部分的介绍
0,1,2,3分别为每个玩家的主动阶段。 4,5,6,7为回复阶段(鸣牌/pass)。 8,9,10,11为抢杠。 12,13,14,15为抢暗杠。 16为GameOver
数据类型是SelfAction和ResponseAction的列表。参见这两个类的介绍。
参数类型为int,表示在允许执行的动作的列表中的第几个。
(参见MahjongPy/MahjongPy.cpp和Mahjong/Yaku.h)
(pybind11封装的接口形式下)
.value("RonAgari", ResultType::荣和终局)
.value("TsumoAgari", ResultType::自摸终局)
.value("IntervalRyuuKyoku", ResultType::中途流局)
.value("NoTileRyuuKyoku", ResultType::荒牌流局)
.value("RyuuKyokuMangan", ResultType::流局满贯)
包含一系列可以访问的属性
(pybind11封装的接口形式下)
.def_readonly("result_type", &Result::result_type)
.def_readonly("results", &Result::results)
.def_readonly("score", &Result::score)
包含一系列可以访问的属性
(pybind11封装的接口形式下)
.def_readonly("yakus", &CounterResult::yakus)
.def_readonly("fan", &CounterResult::fan)
.def_readonly("fu", &CounterResult::fu)
.def_readonly("score1", &CounterResult::score1)
.def_readonly("score2", &CounterResult::score2)