导入
—— 什么环节只要用算法判断一次,就能知道是否听牌立直、还差什么牌就可以荣和自摸?
—— 只要在缺一张手牌(如1、4、7、10、13张时)的情况下判断是否听牌、听哪些牌,就可以为上面的复杂判断提供基础。
网上大部分方法会用大量遍历、查表等方法,解决这个问题这也就是我探索新方法的初衷
正题
为了简明地探讨这个问题,我先举一个已经和牌的例子:
如果未立直被点北风,那就是个很惨的役牌1番40符233
为了更好看清,我分为了4个面子和1个雀头,这时如果拿走一张牌让他变成听牌的形式,那就有3种情况:
首先下一个定义:几张连续或相同的牌,我称为 1 块(Block),上面例子中就用空格分开了块
- 拿走顺子的边张后,还剩下 5 块
- 拿走顺子的中张,变成 6 块(这是听牌情况下,块数最多的情况)
- 拿走雀头的一张,还剩 5 块
此外,和牌时还有更复杂的复合形式,即有一块里既有雀头又有面子,这里举个简单的例子:
他如果缺一张一万或四万时,其中有一块不完整的便既含有雀头也含有面子:
这些便是所有情况的基本形式,我把他分为3大类,5小类,这时出现了预告里的图片,这五类我会依次介绍:
- 面子完整型(3n):顾名思义,只含有面子(即刻子或顺子)的块,牌数为3n,可以直接判断:
- 雀头完整型(3n+2):包含一个雀头,虽然不是3的倍数,但依然较完整,去对(去掉一个对子)就可以判断出缺(听)的牌:
- 半不完整型(3n+1):即嵌(坎)张,所以在听牌时都会成对出现,所以取中张(两块中间的那张)就可以了:
- 面子不完整型(3n+2):即面子完整型缺一张牌(但不会形成两块),听牌时会和雀头完整型一起出现,形成多面听或者双碰,用遍历(在该块的范围内遍历,遍历的次数较少)的方法可以找出:
(根据不同牌型,遍历 不同的牌数 ~ 不同的牌数+2 次就可以)(至少3次、至多9次)
- 全不完整型(3n+4):最复杂的一种,即雀头完整型缺一张牌,因为不知道缺在雀头还是在面子上,所以只能用遍历(遍历的次数也较少)后再去对来处理:
综上,所有牌型都可以分为如上五种情况处理,可以算是一种归类或者剪枝(?)
接下来便可以写代码了,首先得先按数量分成几块(Block),才能进行更深层的操作:
代码实现
首先,这个算法是针对每一家的,所以写在 Opponent 类下面,并在旁边创建新 Block 类:
(由于听的牌并不是特指牌山里的某一张,所以这里的 chong 不是指针)
//main.hpp
class Opponent
{
public:
bool ting{ false }; //是否听牌
vector<Tile> chong; //可以和的牌(听的牌)(铳)
void ting_judge(); //听牌判断(在摸牌前判断)
...
public: //下面是会用到的函数
_int_ operator<<(Tile* card) //(进张)摸牌
{
_int_ ru{ 0 };
while (ru < tile.size() && *card >* tile[ru]) //找到进张插入的位置
++ru;
tile.insert(tile.begin() + ru, card); //插入牌
return ru; //返回该牌的位置
}
__forceinline Tile*& operator[](_int_ num) { return tile[num]; } //直接访问牌元素
_int_ link(_int_ num) //(前张牌序号)两张手牌间关系
{ //理论上不会越界,但是以防万一,用try块保护
try { return *tile.at((_ull_)num + 1) - *tile.at(num); }
catch (out_of_range) { return 100; } //(尽量大的数)
}
}
//ting.hpp
class Block { ... }
void Opponent::ting_judge() { ... }
开始写 ting_judge() 函数里的内容:
首先清空听牌的状态
ting = false;
chong.clear(); //清空铳牌
...
然后是特殊牌型的判断(国士、七对):
由于算法很简单也很多样,我就不做详细介绍,只有大致介绍:
- 国士:这里我的牌对应数字的定义(详见(一)§ 3)有一些优势:牌的序号*8就是对应的幺九牌,此外用 que 和 duo 两个 bool 型变量便可以轻松实现
- 七对:由于日麻没有龙七对的役种,只好逐张判断,一般情况下偶数序号牌和下一张是相同的,而奇数的和下张不是相同,如果有单张会用 dan 的 bool 型变量记录(按照cpp习惯数组第一个序号是0)
...
if (fulu.empty()) //如果没有副露(特殊牌型判断)
{ //国士牌型判断
bool que{ false }; //是否缺了某张幺九牌
bool duo{ false }; //是否多了某张幺九牌
_int_ que_tile{ 0 }; //缺的幺九牌
for (_int_ i{ 0 }; i < 13; ++i) //判断十三张幺九牌的拥有情况
if (*tile[i] == (i + que - duo) * 8); //如果刚好位置映射
else if (*tile[i] == (i + que - duo - 1) * 8) //如果和上张映射幺九牌一样
{
if (duo == true) //如果已经有一个多的牌
goto guoshi_end;
else duo = true; //记录有多牌
}
else if (*tile[i] == (i + que - duo + 1) * 8) //如果和下张映射幺九牌一样
{
if (que == true) //如果已经有一个缺牌
goto guoshi_end; //不是国士
else que = true; //记录有缺牌
que_tile = i * 8; //记录缺的牌
}
else goto guoshi_end; //有不是幺九即无听
if (duo == true) //若有多张
{
if (que == true)
chong.emplace_back(que_tile); //记听一面
else chong.emplace_back(96); //记听一面(中)(中在最后不会被que记录)
}
else for (_int_ i{ 0 }; i < 13; ++i) //若不缺张
chong.emplace_back(i * 8); //记听十三面
ting = true;
return;
guoshi_end:
//七对牌型判断
bool dan{ false }; //多出来的单张
_int_ dan_tile{ 0 }; //单张牌位置
for (_int_ i{ 0 }; i < 12; ++i) //判断关系
if ((i + dan) % 2 ^ _int_(bool(link(i)))) //如果偶数位关系对应不是相同,或奇数位不是其他
{ //直接异或运算无法排除龙七对
if (link(i) == 0) //如果这个错误关系是相同
goto qidui_end; //龙七对
else if (dan == true) //如果已经有单牌了
goto qidui_end; //不是七对子
else
{
dan = true; //记录单张
dan_tile = *tile[i]; //记录序号
}
}
if (dan == false) //如果没查到单张
dan_tile = *tile[12]; //那单张就是最后一个
chong.emplace_back(dan_tile); //记听一面
ting = true;
//有可能复合二杯口,故听牌后不退出
}
qidui_end:
...
接下来是判断完整型:
首先写 Block 类:
其成员拥有3个,在代码片中有注释;通常在创建新 block 时就已经确定了其中 loc 的值,所以写在构造函数
class Block //块(其中牌关系只有相同或连续)
{
public:
_int_ loc{ 0 }; //块内首张牌的序号(大概可以写成const,但没必要)
_int_ len{ 1 }; //块内牌数(至少一张)
bool type{ true }; //类型(真(3n)为完整型(由整数个面子组成),假为不完整型(含有雀头、不完整的面子))
Block(_int_ _loc) : loc{ _loc } {}
...
}
然后写 block 的判断:
3n张牌的初步被判断为完整型(true),其他为不完整型(false)
不难发现,在如下这种听牌情况时,听牌块数达到了最多的6块,不完整(包括雀头完整型)的块数达到了最多的3块:
而且在找到下一块的开头时,也会得到上一块的总长度,所以把上一块收尾和下一块的开头写在同一个循环体内
于是我们写如下代码:
...
_int_ block_err{ 0 }; //不完整的块数(最多3个)
vector<Block> block; //块(最多6个)
block.reserve(6);
block.emplace_back(0); //***第一块的开始***
for (_int_ i{ 0 }; i < tile.size() - 1; ++i)
if (link(i) > 1) //当关系不是相同或连续
{
block.back().len = i - block.back().loc + 1; //记录上一块的长度
block.back().type = !(block.back().len % 3); //筛选完整型Lv.1
block_err += !block.back().type; //记录
if (block.size() == 6 - fulu.size() || block_err == 4) //若块序号达到(6 - 副露数)或有4个不整型
return; //无听
block.emplace_back(i + 1); //括号里是块内首张牌的序号
}
block.back().len = (_int_)tile.size() - block.back().loc; //记录上一块的长度
block.back().type = !(block.back().len % 3); //筛选完整型Lv.1
block_err += !block.back().type; //记录 ***最后一块的结束***
if (block_err == 4) //若有4个不完整型
return; //无听
...
注意:emplace_back() 固然很好用,但是要看清类型。如 vector< T > a,括号可以认为是 T 的构造函数,里面只能填 T 类型的对象(符合拷贝构造函数)或者填写 T 构造函数的参数列表;如果是 vector< T* > a,那只能填 T* 类型的对象(拷贝构造函数)。否则会在 vector 或 xmemory 头文件报错 C2440,很难排查!
下面就要写重要的判断面子完整型函数(block_complete):
类型判断
block_complete
导入
为了更好看清每块的内部结构,我需要继续细分:
定义:块(Block)内所有相同的牌分为 1 组(Group)
如此,例如:
示意图:整张图都是属于一个块的,每一列都是一个组
然后想象自己是程序,从最左边的第0组开始,一组一组地判断:
先杠刻子:如果遇到3个没杠掉的圈,3个一起杠掉;
再杠顺子:剩下的没杠掉的如果不满3个,在本组每杠掉1个,下组和下下组也杠掉1个(也是共杠三个);
如果这组要杠掉1个,而下组或下下组不够的杠了,说明不是完整型,反之如果刚好杠完就是完整型。
拿上图举例:
第一次 ——
第二次 ——
第三次 ——
第四次 ——
判断出这是面子完整型了,很简单吧?
如果是如下的牌型呢?
第一次 ——
第二次 ——
第三次 ——
杠到第四次时,发现四万有2张,理应杠掉五万和六万各两张,但是不够了,所以这不是面子完整型
这种方法可以正确分离所有的类型,除了三连刻无法识别成三条顺子:
但是就听牌来说,这并不会影响到是否听牌、听哪些牌的判断,而且之后改进也十分容易
代码实现
根据原理,实现这个并不难(写在 Block 类下):
注:“ * ”星号框起来的部分是为了以后对接“去对”的操作,现在并没有什么用 ╮(╯▽╰)╭
其中 block_card 的 bool 型容器记录每张牌是属于刻子还是顺子的,被当做刻(前面被竖着杠掉的)记做 flase ,当做顺(被斜杠杠掉的)记做 true
bool block_complete(Opponent& op, _int_ quetou_loc = -1) //(判断的牌组,雀头的序号(-1为没有雀头))筛选完整型Lv.2
{
class Group //组
{
public:
Group(_int_ _loc_) :loc_{ _loc_ } {} //构造
const _int_ loc_{ 0 }; //组内首张牌的序号
_int_ len_{ 1 }; //组内牌数
_int_ confirmed{ 0 }; //已确认(顺/刻)的牌数量
};
vector<Group> group;
group.emplace_back(loc);
_int_ i{ loc };
for (; i < loc + len - 1; ++i) //判断块内每个关系
if (op.link(i) == 1) //当关系是连续
{
group.back().len_ = i + 1 - group.back().loc_;
group.emplace_back(i + 1); //记录多一个组
}
group.back().len_ = i + 1 - group.back().loc_;
const _int_ group_num{ (_int_)group.size() }; //包含的组数
vector<bool> block_card(len, true); //判断块内每张牌,若该牌属于顺子即记为顺(true),否则记为刻(flase)
//*******************************************************************
if (quetou_loc != -1) //有雀头
{
++++group[quetou_loc].confirmed; //雀头处有两个刻
block_card[(_ull_)group[quetou_loc].loc_ - loc] = false;
block_card[(_ull_)group[quetou_loc].loc_ - loc + 1] = false;
}
//*******************************************************************
for (_ull_ i{ 0 }; i < group_num; ++i) //每次循环记录一个组
{
switch (group[i].len_ - group[i].confirmed) //该组牌数
{
case 0:
continue; //开始下一循环
break;
case 1: //都是顺
if (group_num > i + 2)
{
++group[i + 1].confirmed; //确定后面几组有2张是顺
++group[i + 2].confirmed;
continue;
}
break;
case 2: //都是顺
if (group_num > i + 2)
{
++++group[i + 1].confirmed; //确定后面几组有2张是顺
++++group[i + 2].confirmed;
continue;
}
break;
case 4: //3刻1顺
if (group_num > i + 2)
{
++group[i + 1].confirmed; //确定后面几组有2张是顺
++group[i + 2].confirmed;
block_card[(_ull_)group[i].loc_ - loc] = false; //三张是刻
block_card[(_ull_)group[i].loc_ - loc + 1] = false;
block_card[(_ull_)group[i].loc_ - loc + 2] = false;
continue;
}
break;
case 3: //都是刻
block_card[(_ull_)group[i].loc_ - loc] = false; //三张是刻
block_card[(_ull_)group[i].loc_ - loc + 1] = false;
block_card[(_ull_)group[i].loc_ - loc + 2] = false;
continue;
break;
default: //可能是负数
break;
}
type = false; //不完整型
return false;
}
return true;
}
...
const _int_ block_num{ (_int_)block.size() }; //块数
for (_int_ num{ 0 }; num < block_num; ++num)
if (block[num].type == true) //只判断通过完整型Lv.1的
if (block[num].block_complete(*this) == false) //筛选完整型Lv.2,若不完整
return; //无听
...
其他不完整型判断
导入
有了 block_complete() 函数,剩下的一切都很明朗了:只要想办法往面子完整型上凑就好了
之前说了如果是听牌的牌型,不完整型(err_block)最多只能有3个,那分别有1、2、3个时,会有特征吗?
答案是有,而且有很明显的分类
- 只有 1 个时:该不完整型一定是全不完整型,例:
- 有 2 个时:会有一个雀头完整型和一个面子不完整型,例:
- 有 3 个时:会有一个雀头完整型和两个半不完整型,如:
所以可以用一个switch语句,来讨论这三种情况:
- 由于只有半不完整型会用取中张的方法,所以就不用特意写个函数了
- 遍历有两种模式,一种遍历后直接判断是否完整(面子不完整型),一种遍历后还要去对,所以参数列表里还有个 const bool 类型
class Block
{
public:
...
bool block_qudui(Opponent& op) { ... } //(判断的牌组)雀头完整型
bool block_bianli(Opponent& op, const bool mode) { ... } //(判断的牌组,假为面子不完整型,真为全不完整型)
};
void Opponent::ting_judge()
{
...
switch (block_err)
{
case 1: //有一块不完整型(一块全不完整型(3n+4))
{
for (_int_ err_block{ 0 }; err_block < block_num; ++err_block)
if (block[err_block].type == false) //是不完整型
{
if (block[err_block].block_bianli(*this, true))
break;
else return; //无听
}
break;
}
case 2: //有两块不完整型(一块面子不完整型(3n+2),一块雀头完整型(3n+2))
{
_int_ err_block_1{ 0 };
for (; err_block_1 < block_num; ++err_block_1)
if (block[err_block_1].type == false) //是不完整型
break; //记录
_int_ err_block_2{ err_block_1 + 1 }; //第二个不完整型
for (; err_block_2 < block_num; ++err_block_2)
if (block[err_block_2].type == false) //是不完整型
break; //记录
if (err_block_2 >= block_num) //如果后者的数字大于等于组数
return; //无听
bool temp = false;
//去对和遍历顺序不能互换,因为如果遍历成功会直接写入铳牌,而去对成功不会写入铳牌,因为&&的短路所以去对写在前面
if (blocks[err_block_2].block_ignore_pair(*this) && blocks[err_block_1].block_traversal(*this, false) //满足任意一个,但是两条语句都要执行
| blocks[err_block_1].block_ignore_pair(*this) && blocks[err_block_2].block_traversal(*this, false)) //按位或不会出现短路
break; //听牌
else return; //无听
}
case 3: //有三块不完整型(两块半不完整型(3n+1),一块雀头完整型(3n+2))
{
_int_ err_block{ 0 };
for (; err_block < block_num; ++err_block)
if (block[err_block].type == false) //是不完整型
if (block[err_block].len % 3 == 1) //如果牌数是3的倍数+1
break; //记录err_loc
Tile temp_chong{ *tile[(_ull_)block[err_block].loc + block[err_block].len - 1] + 1 }; //中间隔的牌(临时记录铳牌)
if (temp_chong != *tile[block[(_ull_)err_block + 1].loc] - 1) //如果它和下一张牌不连续
return; //无听
for (_int_ quetou_block{ 0 }; quetou_block < block_num; ++quetou_block)
if (block[quetou_block].type == false) //是不完整型
if (block[quetou_block].len % 3 == 2) //如果牌数是3的倍数+2(雀头)
if (block[quetou_block].block_qudui(*this) == false) //如果不是雀头块
return; //无听
Opponent temp_op; //临时用来判断的牌组
Block temp_block{ 0 };
temp_block.len = block[err_block].len + 1 + block[(_ull_)err_block + 1].len; //这两个不完整型总长度+一张中间隔的牌
_int_ i{ 0 };
for (; i < block[err_block].len; ++i)
temp_op[i] = tile[(_ull_)block[err_block].loc + i]; //复制该不完整型
temp_op[i] = &temp_chong; //记录中间的牌
for (; i < temp_block.len; ++i)
temp_op[i] = tile[(_ull_)block[err_block].loc + i]; //复制该不完整型
if (temp_block.block_complete(temp_op) == true) //如果该牌组完整
chong.emplace_back(temp_chong); //记听一面
break;
}
}
ting = true; //听牌
}
有了上面的解释,这段代码应该不难理解,现在改写遍历和去对的算法了:
- 遍历:就是在所在块上,加上任意一张,在不形成新块的情况下使它成为完整型,例如:
所以说遍历次数比 不同牌的数量 多 0-2 次(至少3次、至多9次)就可以,在上图情况下需要遍历不同的牌数 2+2 次,而如果块中含有老头牌或是字牌块,遍历次数能减少1-2次 - 去对:找到所有的对子,每次去掉一个,查看他是否完整;这里直接传递给 block_complete(),让他的 block_card 直接认为那个对子是刻子的一部分,就可以排除对子了
代码实现
这段代码写在 Block 类下,也不是很冗长:
bool block_qudui(Opponent& op) //(判断的牌组)雀头完整型
{
for (_int_ i{ loc }, temp_group_num{ 0 }; i < loc + len - 1; ++i)
{
if (op.link(i) == 1) //当关系是连续
++temp_group_num; //组数加一
else if (block_complete(op, temp_group_num) == true) //当关系是相同,判断是否是雀头完整型
return true; //该组是雀头完整型
}
return false; //不完整
}
bool block_bianli(Opponent& op, const bool mode) //(判断的牌组,假为面子不完整型,真为全不完整型)
{
_int_ first{ *op[loc] - 1 }; //可能的首张牌
if (*op[loc] % 16 == 0 || *op[loc] / 8 > 5) //如果首张是一万、筒、索或字牌
++first; //first没有前一张,加回op[loc]
_int_ last = *op[loc + len - 1] + 1; //可能的末张牌
if (*op[loc + len - 1] % 16 == 8 || *op[loc + len - 1] / 8 > 5) //如果末张是九万、筒、索或字牌
--last; //last没有后一张,减回op[loc]
Block temp_block{ 0 }; //新牌组对应的块
temp_block.len = len + 1;
_int_ temp_tile{ first };
_int_ temp{ 0 };
Opponent temp_op;
for (_int_ i{ 0 }; i < last - first + 1; ++i, ++temp_tile) //每张牌都插入尝试一次(遍历)
{
for (_int_ j{ loc }; j < loc + len; ++j) //重新复制所有牌
temp_op.tile.emplace_back(op[j]);
temp = temp_op << new Tile(temp_tile); //插入尝试的牌
if (mode && temp_block.block_qudui(temp_op) //(真)全不完整型且遍历、去对后完整
|| !mode && temp_block.block_complete(temp_op)) //(假)面子不完整型且遍历后完整
op.chong.emplace_back(temp_tile);
delete temp_op[temp];
temp_op.tile.clear();
}
if (op.chong.empty()) //可能本来它就不为空,不过在这里不影响(将来算符时可能改动)
return false;
else return true;
以上听牌的算法就写完了,总计 270 多行,个人实验可以判断出日麻里所有听的牌(不考虑振听、空听情况下),大家可以自己实验一下2333
完整代码
#ifndef TING_HPP
#define TING_HPP
class Block //块(其中牌关系只有相同或连续)
{
public:
_int_ loc{ 0 }; //块内首张牌的序号
_int_ len{ 1 }; //块内牌数(至少一张)
bool type{ true }; //类型(真(3n)为完整型(由整数个面子组成),假为不完整型(含有雀头、不完整的面子))
Block(_int_ _loc) : loc{ _loc } {}
bool block_complete(Opponent& op, _int_ quetou_loc = -1) //(判断的牌组,雀头的序号(-1为没有雀头))筛选完整型Lv.2
{
class Group //组
{
public:
Group(_int_ _loc_) :loc_{ _loc_ } {} //构造
const _int_ loc_{ 0 }; //组内首张牌的序号
_int_ len_{ 1 }; //组内牌数
_int_ confirmed{ 0 }; //已确认(顺/刻)的牌数量
};
vector<Group> group;
group.emplace_back(loc);
_int_ i{ loc };
for (; i < loc + len - 1; ++i) //判断块内每个关系
if (op.link(i) == 1) //当关系是连续
{
group.back().len_ = i + 1 - group.back().loc_;
group.emplace_back(i + 1); //记录多一个组
}
group.back().len_ = i + 1 - group.back().loc_;
const _int_ group_num{ (_int_)group.size() }; //包含的组数
vector<bool> block_card(len, true); //判断块内每张牌,若该牌属于顺子即记为顺(true),否则记为刻(flase)
if (quetou_loc != -1) //有雀头
{
++++group[quetou_loc].confirmed; //雀头处有两个刻
block_card[(_ull_)group[quetou_loc].loc_ - loc] = false;
block_card[(_ull_)group[quetou_loc].loc_ - loc + 1] = false;
}
for (_ull_ i{ 0 }; i < group_num; ++i) //每次循环记录一个组
{
switch (group[i].len_ - group[i].confirmed) //该组牌数
{
case 0:
continue; //开始下一循环
break;
case 1: //都是顺
if (group_num > i + 2)
{
++group[i + 1].confirmed; //确定后面几组有2张是顺
++group[i + 2].confirmed;
continue;
}
break;
case 2: //都是顺
if (group_num > i + 2)
{
++++group[i + 1].confirmed; //确定后面几组有2张是顺
++++group[i + 2].confirmed;
continue;
}
break;
case 4: //3刻1顺
if (group_num > i + 2)
{
++group[i + 1].confirmed; //确定后面几组有2张是顺
++group[i + 2].confirmed;
block_card[(_ull_)group[i].loc_ - loc] = false; //三张是刻
block_card[(_ull_)group[i].loc_ - loc + 1] = false;
block_card[(_ull_)group[i].loc_ - loc + 2] = false;
continue;
}
break;
case 3: //都是刻
block_card[(_ull_)group[i].loc_ - loc] = false; //三张是刻
block_card[(_ull_)group[i].loc_ - loc + 1] = false;
block_card[(_ull_)group[i].loc_ - loc + 2] = false;
continue;
break;
default: //可能是负数
break;
}
type = false; //不完整型
return false;
}
return true;
}
bool block_qudui(Opponent& op) //(判断的牌组)雀头完整型
{
for (_int_ i{ loc }, temp_group_num{ 0 }; i < loc + len - 1; ++i)
{
if (op.link(i) == 1) //当关系是连续
++temp_group_num; //组数加一
else if (block_complete(op, temp_group_num) == true) //当关系是相同,判断是否是雀头完整型
return true; //该组是雀头完整型
}
return false; //不完整
}
bool block_bianli(Opponent& op, const bool mode) //(判断的牌组,假为面子不完整型,真为全不完整型)
{
_int_ first{ *op[loc] - 1 }; //可能的首张牌
if (*op[loc] % 16 == 0 || *op[loc] / 8 > 5) //如果首张是一万、筒、索或字牌
++first; //first没有前一张,加回op[loc]
_int_ last = *op[loc + len - 1] + 1; //可能的末张牌
if (*op[loc + len - 1] % 16 == 8 || *op[loc + len - 1] / 8 > 5) //如果末张是九万、筒、索或字牌
--last; //last没有后一张,减回op[loc]
Block temp_block{ 0 }; //新牌组对应的块
temp_block.len = len + 1;
_int_ temp_tile{ first };
_int_ temp{ 0 };
Opponent temp_op;
for (_int_ i{ 0 }; i < last - first + 1; ++i, ++temp_tile) //每张牌都插入尝试一次(遍历)
{
for (_int_ j{ loc }; j < loc + len; ++j) //重新复制所有牌
temp_op.tile.emplace_back(op[j]);
temp = temp_op << new Tile(temp_tile); //插入尝试的牌
if (mode && temp_block.block_qudui(temp_op) //(真)全不完整型且遍历、去对后完整
|| !mode && temp_block.block_complete(temp_op)) //(假)面子不完整型且遍历后完整
op.chong.emplace_back(temp_tile);
delete temp_op[temp];
temp_op.tile.clear();
}
if (op.chong.empty()) //可能本来它就不为空,不过在这里不影响(将来算符时可能改动)
return false;
else return true;
}
};
void Opponent::ting_judge()
{
ting = false;
chong.clear(); //清空铳牌
if (fulu.empty()) //如果没有副露(特殊牌型判断)
{ //国士牌型判断
bool que{ false }; //是否缺了某张幺九牌
bool duo{ false }; //是否多了某张幺九牌
_int_ que_tile{ 0 }; //缺的幺九牌
for (_int_ i{ 0 }; i < 13; ++i) //判断十三张幺九牌的拥有情况
if (*tile[i] == (i + que - duo) * 8); //如果刚好位置映射
else if (*tile[i] == (i + que - duo - 1) * 8) //如果和上张映射幺九牌一样
{
if (duo == true) //如果已经有一个多的牌
goto guoshi_end;
else duo = true; //记录有多牌
}
else if (*tile[i] == (i + que - duo + 1) * 8) //如果和下张映射幺九牌一样
{
if (que == true) //如果已经有一个缺牌
goto guoshi_end; //不是国士
else que = true; //记录有缺牌
que_tile = i * 8; //记录缺的牌
}
else goto guoshi_end; //有不是幺九即无听
if (duo == true) //若有多张
{
if (que == true)
chong.emplace_back(que_tile); //记听一面
else chong.emplace_back(96); //记听一面(中)(中在最后不会被que记录)
}
else for (_int_ i{ 0 }; i < 13; ++i) //若不缺张
chong.emplace_back(i * 8); //记听十三面
ting = true;
return;
guoshi_end:
//七对牌型判断
bool dan{ false }; //多出来的单张
_int_ dan_tile{ 0 }; //单张牌位置
for (_int_ i{ 0 }; i < 12; ++i) //判断关系
if ((i + dan) % 2 ^ _int_(bool(link(i)))) //如果偶数位关系对应不是相同,或奇数位不是其他
{ //直接异或运算无法排除龙七对
if (link(i) == 0) //如果这个错误关系是相同
goto qidui_end; //龙七对
else if (dan == true) //如果已经有单牌了
goto qidui_end; //不是七对子
else
{
dan = true; //记录单张
dan_tile = *tile[i]; //记录序号
}
}
if (dan == false) //如果没查到单张
dan_tile = *tile[12]; //那单张就是最后一个
chong.emplace_back(dan_tile); //记听一面
ting = true;
//有可能复合二杯口,故听牌后不退出
}
qidui_end:
_int_ block_err{ 0 }; //不完整的块数(最多3个)
vector<Block> block; //块(最多6个)
block.reserve(6);
block.emplace_back(0); //第一块
for (_int_ i{ 0 }; i < tile.size() - 1; ++i)
if (link(i) > 1) //当关系不是相同或连续
{
block.back().len = i - block.back().loc + 1; //记录上一块的长度
block.back().type = !(block.back().len % 3); //筛选完整型Lv.1
block_err += !block.back().type; //记录
if (block.size() == 6 - fulu.size() || block_err == 4) //若块序号达到(6 - 副露数)或有4个不完整型
return; //无听
block.emplace_back(i + 1); //括号里是块内首张牌的序号
}
block.back().len = (_int_)tile.size() - block.back().loc; //记录上一块的长度
block.back().type = !(block.back().len % 3); //筛选完整型Lv.1
block_err += !block.back().type; //记录
if (block_err == 4) //若有4个不完整型
return; //无听
const _int_ block_num{ (_int_)block.size() }; //块数
for (_int_ num{ 0 }; num < block_num; ++num)
if (block[num].type == true) //只判断通过完整型Lv.1的
if (block[num].block_complete(*this) == false) //筛选完整型Lv.2,若不完整
return; //无听
switch (block_err)
{
case 1: //有一块不完整型(一块全不完整型(3n+4))
{
for (_int_ err_block{ 0 }; err_block < block_num; ++err_block)
if (block[err_block].type == false) //是不完整型
{
if (block[err_block].block_bianli(*this, true))
break;
else return; //无听
}
break;
}
case 2: //有两块不完整型(一块面子不完整型(3n+2),一块雀头完整型(3n+2))
{
_int_ err_block_1{ 0 };
for (; err_block_1 < block_num; ++err_block_1)
if (block[err_block_1].type == false) //是不完整型
break; //记录
_int_ err_block_2{ err_block_1 + 1 }; //第二个不完整型
for (; err_block_2 < block_num; ++err_block_2)
if (block[err_block_2].type == false) //是不完整型
break; //记录
if (err_block_2 >= block_num) //如果后者的数字大于等于组数
return; //无听
bool temp = false;
//去对和遍历顺序不能互换,因为如果遍历成功会直接写入铳牌,而去对成功不会写入铳牌,因为&&的短路所以去对写在前面
if (blocks[err_block_2].block_ignore_pair(*this) && blocks[err_block_1].block_traversal(*this, false) //满足任意一个,但是两条语句都要执行
| blocks[err_block_1].block_ignore_pair(*this) && blocks[err_block_2].block_traversal(*this, false)) //按位或不会出现短路
break; //听牌
else return; //无听
}
case 3: //有三块不完整型(两块半不完整型(3n+1),一块雀头完整型(3n+2))
{
_int_ err_block{ 0 };
for (; err_block < block_num; ++err_block)
if (block[err_block].type == false) //是不完整型
if (block[err_block].len % 3 == 1) //如果牌数是3的倍数+1
break; //记录err_loc
Tile temp_chong{ *tile[(_ull_)block[err_block].loc + block[err_block].len - 1] + 1 }; //中间隔的牌(临时记录铳牌)
if (temp_chong != *tile[block[(_ull_)err_block + 1].loc] - 1) //如果它和下一张牌不连续
return; //无听
for (_int_ quetou_block{ 0 }; quetou_block < block_num; ++quetou_block)
if (block[quetou_block].type == false) //是不完整型
if (block[quetou_block].len % 3 == 2) //如果牌数是3的倍数+2(雀头)
if (block[quetou_block].block_qudui(*this) == false) //如果不是雀头块
return; //无听
Opponent temp_op; //临时用来判断的牌组
Block temp_block{ 0 };
temp_block.len = block[err_block].len + 1 + block[(_ull_)err_block + 1].len; //这两个不完整型总长度+一张中间隔的牌
_int_ i{ 0 };
for (; i < block[err_block].len; ++i)
temp_op[i] = tile[(_ull_)block[err_block].loc + i]; //复制该不完整型
temp_op[i] = &temp_chong; //记录中间的牌
for (; i < temp_block.len; ++i)
temp_op[i] = tile[(_ull_)block[err_block].loc + i]; //复制该不完整型
if (temp_block.block_complete(temp_op) == true) //如果该牌组完整
chong.emplace_back(temp_chong); //记听一面
break;
}
}
ting = true; //听牌
}
#endif
附带C#代码实现
using System.Collections.Generic;
using System.Linq;
namespace Cs.Classes
{
/// <summary>
/// 块(其中牌关系只有相同或连续)
/// </summary>
public class Block
{
/// <summary>
/// 块内首张牌的序号
/// </summary>
public int Loc { get; }
/// <summary>
/// 块内牌数(至少一张)
/// </summary>
public int Len { get; set; } = 1;
/// <summary>
/// 类型(真(3n)为完整型(由整数个面子组成),假为不完整型(含有雀头、不完整的面子))
/// </summary>
public bool Type { get; set; } = true;
public Block(int loc) => Loc = loc;
/// <summary>
/// 筛选完整型Lv.2
/// </summary>
/// <param name="op">判断的牌组</param>
/// <param name="eyesLoc">雀头的序号(-1为没有雀头)</param>
public bool BlockComplete(Opponent op, int eyesLoc = -1)
{
var group = new List<Group> { new(Loc) };
for (var i = Loc; i < Loc + Len - 1; ++i) //判断块内每个关系
if (op.Link(i) == 1) //当关系是连续
{
group.Last().Len = i + 1 - group.Last().Loc;
group.Add(new (i + 1)); //记录多一个组
}
group.Last().Len = Loc + Len - group.Last().Loc;
var groupNum = group.Count; //包含的组数
var blockCard = new bool[Len]; //判断块内每张牌,若该牌属于顺子即记为顺(true),否则记为刻(flase)
for (var i = 0; i < Len; ++i)
blockCard[i] = true;
if (eyesLoc != -1) //有雀头
{
++group[eyesLoc].Confirmed; //雀头处有两个刻
++group[eyesLoc].Confirmed;
blockCard[group[eyesLoc].Loc - Loc] = false;
blockCard[group[eyesLoc].Loc - Loc + 1] = false;
}
for (var i = 0; i < groupNum; ++i) //每次循环记录一个组
{
switch (group[i].Len - group[i].Confirmed) //该组牌数
{
case 0:
continue; //开始下一循环
case 1: //都是顺
if (groupNum > i + 2)
{
++group[i + 1].Confirmed; //确定后面几组有2张是顺
++group[i + 2].Confirmed;
continue;
}
break;
case 2: //都是顺
if (groupNum > i + 2)
{
++group[i + 1].Confirmed; //确定后面几组有2张是顺
++group[i + 1].Confirmed;
++group[i + 2].Confirmed;
++group[i + 2].Confirmed;
continue;
}
break;
case 4: //3刻1顺
if (groupNum > i + 2)
{
++group[i + 1].Confirmed; //确定后面几组有2张是顺
++group[i + 2].Confirmed;
blockCard[group[i].Loc - Loc] = false; //三张是刻
blockCard[group[i].Loc - Loc + 1] = false;
blockCard[group[i].Loc - Loc + 2] = false;
continue;
}
break;
case 3: //都是刻
blockCard[group[i].Loc - Loc] = false; //三张是刻
blockCard[group[i].Loc - Loc + 1] = false;
blockCard[group[i].Loc - Loc + 2] = false;
continue;
default: //可能是负数
break;
}
Type = false; //不完整型
return false;
}
return true;
}
/// <summary>
/// 去对(雀头完整型)
/// </summary>
/// <param name="op">判断的牌组</param>
/// <returns>是否听牌</returns>
public bool BlockIgnorePair(Opponent op)
{
for (int i = Loc, tempGroupNum = 0; i < Loc + Len - 1; ++i)
{
if (op.Link(i) == 1) //当关系是连续
++tempGroupNum; //组数加一
else if (BlockComplete(op, tempGroupNum)) //当关系是相同,判断是否是雀头完整型
return true; //该组是雀头完整型
}
return false; //不完整
}
/// <summary>
/// 遍历
/// </summary>
/// <param name="op">判断的牌组</param>
/// <param name="mode">假为面子不完整型,真为全不完整型</param>
/// <returns>是否听牌</returns>
public bool BlockTraversal(Opponent op, bool mode)
{
var first = op[Loc].Val - 1; //可能的首张牌
if ((op[Loc].Val & 15) == 0 || op[Loc].Val >> 3 > 5) //如果首张是一万、筒、索或字牌
++first; //first没有前一张,加回op[loc]
var last = op[Loc + Len - 1].Val + 1; //可能的末张牌
if ((op[Loc + Len - 1].Val & 15) == 8 || op[Loc + Len - 1].Val >> 3 > 5) //如果末张是九万、筒、索或字牌
--last; //last没有后一张,减回op[loc]
var tempBlock = new Block(0) { Len = Len + 1 }; //新牌组对应的块
var tempTile = first;
var tempOp = new Opponent();
for (var i = 0; i < last - first + 1; ++i, ++tempTile) //每张牌都插入尝试一次(遍历)
{
for (var j = Loc; j < Loc + Len; ++j) //重新复制所有牌
tempOp.Hands.Add(new Tile(op[j].Val));
tempOp.TileIn(new Tile(tempTile)); //插入尝试的牌
if (mode && tempBlock.BlockIgnorePair(tempOp) //(真)全不完整型且遍历、去对后完整
|| !mode && tempBlock.BlockComplete(tempOp)) //(假)面子不完整型且遍历后完整
op.ReadyHands.Add(new Tile(tempTile));
tempOp.Hands.Clear();
}
return op.ReadyHands.Count != 0; //可能本来它就不为空,不过在这里不影响(将来算符时可能改动)
}
}
}
*****
using System.Collections.Generic;
using System.Linq;
namespace Cs.Classes
{
public class Opponent
{
/// <summary>
/// 手牌区
/// </summary>
public List<Tile> Hands { get; } = new(14);
/// <summary>
/// 副露区
/// </summary>
public List<Meld> Melds { get; } = new(4);
/// <summary>
/// 牌河(舍张)
/// </summary>
public List<Tile> Discards { get; } = new(30);
/// <summary>
/// 可以和的牌
/// </summary>
public List<Tile> ReadyHands { get; } = new(13);
/// <summary>
/// 是否听牌
/// </summary>
public bool IsReadyHand { get; private set; }
/// <summary>
/// 打牌
/// </summary>
/// <param name="index">打的牌的序号</param>
/// <param name="now">now</param>
public void TileOut(int index, ref Tile now)
{
now = Hands[index];
Discards.Add(Hands[index]); //放入牌河
Hands.RemoveAt(index); //删除元素
}
/// <summary>
/// 摸牌
/// </summary>
/// <param name="card">进张</param>
/// <returns>插入牌的位置</returns>
public int TileIn(Tile card)
{
var ru = 0;
while (ru < Hands.Count && card.Val > Hands[ru].Val) //找到进张插入的位置
++ru;
Hands.Insert(ru, card); //插入牌
return ru;
}
/// <summary>
/// 直接访问牌元素
/// </summary>
public Tile this[int index]
{
get => Hands[index];
set => Hands[index] = value;
}
/// <summary>
/// 两张手牌间关系
/// </summary>
/// <param name="num">前张牌序号</param>
public int Link(int num)
{
try
{
return Hands[num + 1].Val - Hands[num].Val;
}
catch (System.Exception)
{
return int.MaxValue;
} //(尽量大的数)
}
/// <summary>
/// 听牌判断(在摸牌前判断)
/// </summary>
public void ReadyHandJudge()
{
IsReadyHand = false;
ReadyHands.Clear(); //清空铳牌
if (Melds is { Count: 0 }) //如果没有副露(特殊牌型判断)
{
//国士牌型判断
var que = false; //是否缺了某张幺九牌(0或1)
var duo = false; //是否多了某张幺九牌(0或1)
var queTile = 0; //缺的幺九牌
for (var i = 0; i < 13; ++i) //判断十三张幺九牌的拥有情况
if (Hands[i].Val == (i + (que ? 1 : 0) - (duo ? 1 : 0)) << 3) ; //如果刚好位置映射
else if (Hands[i].Val == (i + (que ? 1 : 0) - (duo ? 1 : 0) - 1) << 3) //如果和上张映射幺九牌一样
{
if (duo) //如果已经有一个多的牌
goto ThirteenOrphans;
else duo = true; //记录有多牌
}
else if (Hands[i].Val == (i + (que ? 1 : 0) - (duo ? 1 : 0) + 1) << 3) //如果和下张映射幺九牌一样
{
if (que) //如果已经有一个缺牌
goto ThirteenOrphans; //不是国士
else que = true; //记录有缺牌
queTile = i << 3; //记录缺的牌
}
else goto ThirteenOrphans; //有不是幺九即无听
if (duo) //若有多张,记听一面或记听一面(中)(中在最后不会被que记录)
ReadyHands.Add(que ? new Tile(queTile) : new Tile(96));
else for (var i = 0; i < 13; ++i) //若不缺张
ReadyHands.Add(new Tile(i << 3)); //记听十三面
IsReadyHand = true;
return;
ThirteenOrphans: //七对牌型判断
var dan = false; //多出来的单张
var danTile = 0; //单张牌位置
for (var i = 0; i < 12; ++i) //判断关系
if (((i + (duo ? 1 : 0)) % 2 ^ (Link(i) > 0 ? 1 : 0)) > 0) //如果偶数位关系对应不是相同,或奇数位不是其他
{
//直接异或运算无法排除龙七对
if (Link(i) == 0) //如果这个错误关系是相同
goto SevenPairs; //龙七对
else if (dan) //如果已经有单牌了
goto SevenPairs; //不是七对子
else
{
dan = true; //记录单张
danTile = Hands[i].Val; //记录序号
}
}
if (!dan) //如果没查到单张
danTile = Hands[12].Val; //那单张就是最后一个
ReadyHands.Add(new Tile(danTile)); //记听一面
IsReadyHand = true;
//有可能复合二杯口,故听牌后不退出
}
SevenPairs:
var blockErr = 0; //不完整的块数(最多3个)
var block = new List<Block>(6) { new Block(0) }; //第一块(最多6块)
for (var i = 0; i < Hands.Count - 1; ++i)
if (Link(i) > 1) //当关系不是相同或连续
{
block.Last().Len = i - block.Last().Loc + 1; //记录上一块的长度
block.Last().Type = block.Last().Len % 3 <= 0; //筛选完整型Lv.1
if (!block.Last().Type) //记录
++blockErr;
if (block.Count == 6 - Melds.Count || blockErr == 4) //若块序号达到(6 - 副露数)或有4个不完整型
return; //无听
block.Add(new Block(i + 1)); //括号里是块内首张牌的序号
}
block.Last().Len = Hands.Count - block.Last().Loc; //记录上一块的长度
block.Last().Type = block.Last().Len % 3 <= 0; //筛选完整型Lv.1
if (!block.Last().Type) //记录
++blockErr;
if (blockErr == 4) //若有4个不完整型
return; //无听
if (block.Where(each => each.Type) //只判断通过完整型Lv.1的
.Any(each => !each.BlockComplete(this))) //筛选完整型Lv.2,若不完整
return; //无听
switch (blockErr)
{
case 1: //有一块不完整型(一块全不完整型(3n+4))
{
foreach (var errBlock in block.Where(errBlock => !errBlock.Type)) //是不完整型
if (errBlock.BlockTraversal(this, true))
break;
else return; //无听
break;
}
case 2: //有两块不完整型(一块面子不完整型(3n+2),一块雀头完整型(3n+2))
{
var errBlock1 = 0;
for (; errBlock1 < block.Count; ++errBlock1)
if (!block[errBlock1].Type) //是不完整型
break; //记录
var errBlock2 = errBlock1 + 1; //第二个不完整型
for (; errBlock2 < block.Count; ++errBlock2)
if (!block[errBlock2].Type) //是不完整型
break; //记录
if (errBlock2 >= block.Count) //如果后者的数字大于等于组数
return; //无听
//去对和遍历顺序不能互换,因为如果遍历成功会直接写入铳牌,而去对成功不会写入铳牌,因为&&的短路所以去对写在前面
if ((block[errBlock1].BlockTraversal(this, false) && block[errBlock2].BlockIgnorePair(this)) //满足任意一个,但是两条语句都要执行
| (block[errBlock2].BlockTraversal(this, false) && block[errBlock1].BlockIgnorePair(this))) //按位或不会出现短路
break; //听牌
else return; //无听
}
case 3: //有三块不完整型(两块半不完整型(3n+1),一块雀头完整型(3n+2))
{
var errBlock = 0;
for (; errBlock < block.Count; ++errBlock)
if (!block[errBlock].Type) //是不完整型
if (block[errBlock].Len % 3 == 1) //如果牌数是3的倍数+1
break; //记录errLoc
var tempReadyHands = new Tile(Hands[block[errBlock].Loc + block[errBlock].Len - 1].Val + 1); //中间隔的牌(临时记录铳牌)
if (tempReadyHands.Val != Hands[block[errBlock + 1].Loc].Val - 1) //如果它和下一张牌不连续
return; //无听
if (block.Where(eyesBlock => !eyesBlock.Type) //是不完整型
.Where(eyesBlock => eyesBlock.Len % 3 == 2) //如果牌数是3的倍数+2(雀头)
.Any(eyesBlock => !eyesBlock.BlockIgnorePair(this))) //如果不是雀头块
return; //无听
var tempOp = new Opponent(); //临时用来判断的牌组
var tempBlock = new Block(0) { Len = block[errBlock].Len + 1 + block[errBlock + 1].Len }; //这两个不完整型总长度+一张中间隔的牌
var i = 0;
for (; i < block[errBlock].Len; ++i)
tempOp[i] = Hands[block[errBlock].Loc + i]; //复制该不完整型
tempOp[i] = tempReadyHands; //记录中间的牌
for (; i < tempBlock.Len; ++i)
tempOp[i] = Hands[block[errBlock].Loc + i]; //复制该不完整型
if (tempBlock.BlockComplete(tempOp)) //如果该牌组完整
ReadyHands.Add(tempReadyHands); //记听一面
break;
}
}
IsReadyHand = true; //听牌
}
}
}
规则参考
[1] 日麻百科
[2] 资深麻友
[3] 雀魂麻将
[4] 雀姬麻将