徐子怡 等
林富生 宋志峰 徐自立 余联庆
摘 要:为实现程序自动打牌,必须首先正确显示四家手牌。针对正确显示手牌这一需求,设计了武汉麻将游戏,构造排序算法、皮子癞子获取算法和起牌算法等。即在起始阶段摇骰子以确定起牌的起始位置和庄家,然后有序显示各家手牌。通过对武汉麻将起牌规则的机器学习,将口语化的规则用计算机语言准确描述,以实现正确起牌。为了验证算法的有效性,使用QT框架构建了武汉麻将自动打牌软件进行相应的实验验证,实验结果表明:起牌算法可以正确起牌,并显示四家手牌,具有一定的实用性。
关键词:武汉麻将;QT框架;起牌算法
中图分类号:TP311.1 文献标识码:A 文章编号:2096-4706(2023)23-0083-07
Design of Wuhan Mahjong Game Starting Card Based on QT
XU Ziyi1,2,3, LIN Fusheng1,2,3, SONG Zhifeng1,2,3, XU Zili1,2, YU Lianqing1
(1.School of Mechanical Engineering and Automation, Wuhan Textile University, Wuhan 430200, China;
2.Hubei Provincial Engineering Research Center of 3D Textile, Wuhan 430200, China;
3.Hubei Provincial Key Laboratory of Digital Textile Equipment, Wuhan 430200, China)
Abstract: To achieve automatic card playing in the program, it is necessary to first display the four hands correctly. In view of the need to display the hand cards correctly, Wuhan Mahjong game is designed, and sorting algorithm, skin scab acquisition algorithm and starting card algorithm are constructed. At the beginning of the Mahjong game, it rolls the dice to determine the starting position and dealer of the cards, and then displays the cards of each player in an orderly manner. Through machine learning of the rules of starting card in Wuhan Mahjong, the spoken rules are accurately described in computer language to achieve correct starting of cards. In order to verify the effectiveness of the algorithm, a Wuhan Mahjong automatic card playing software is constructed using the QT framework for corresponding experimental verification. The experimental results show that the starting card algorithm can correctly play cards and display four hands, which has certain practicality.
Keywords: Wuhan Mahjong; QT framework; starting card algorithm
0 引 言
計算机博弈一直是验证人工智能理论的试金石,也是人工智能最活跃的研究领域之一[1],麻将属于非完全信息博弈,相对完全信息博弈更为困难。为了方便玩家快速进入游戏,供休闲娱乐[2],设计并开发了武汉麻将游戏软件,该软件可以满足玩家随时随地进入游戏,还能使计算机学习到最佳的打牌算法实现自动打牌[3]。其中最主要的是在开始打牌之前需要对牌进行初始化,自动按规则发牌,正确显示四家手牌,为打牌做好准备。通过对武汉麻将规则的学习以及计算机语言的描述,可以实现智能麻将。本文主要针对麻将初始化、发牌、显示手牌进行研究。
1 武汉麻将简介
武汉麻将,又称开口翻、红中癞子杠,核心是二五八、癞子、七皮四赖、开口翻和口口翻。麻将牌:筒、条、万、风,共136张牌。其中万、条、筒各九种,风牌七种,没有梅、兰、竹、菊、春、夏、秋、冬。普通牌可以吃、碰和杠,但红中不能吃、碰红中,只能杠;癞子可代替任何牌张,但不能吃、碰,可以杠癞子。
武汉麻将顺时针发牌,逆时针打牌。开局时4位玩家以东、南、西、北入座,每人起手摸13张牌,庄(东风位置)玩家起手多摸1张牌,共计14张,之后由庄家位玩家开始按逆时针出牌操作[1]。通过两点数的和(add)确定起始拿牌方位,再通过较小的点子数(nums)确定留牌墩数(add为1、5、9时从东方起牌,留nums墩;add为2、6、10时从南方起牌,留nums墩;add为3、7、11时从西方起牌,留nums墩;add为12、8、4时从北方起牌,留nums墩)。
文中关于武汉麻将的名词解释:
癞子:武汉麻将“癞子”是在四个选手闲家抓完13张牌庄家抓完第14张牌后翻取的第一张牌加一就是“癞子”。
皮子:武汉麻将“皮子”是在四个选手闲家抓完13张牌庄家抓完第14张牌后翻取的第一张牌。
玩家:武汉麻将游戏由4名玩家组成。
牌库:牌的总张数。武汉麻将分为条、筒、万、风四门,共计136张。
牌墙:洗完牌开局使牌墙共136张牌,发完初始手牌后,牌墙剩下的牌为83张。
局面:某位玩家在某一个时刻可以观察到的所有信息[1]。
一墩:2张牌。
2 程序实现
2.1 QT介绍
本文中的麻将游戏是采用了一个跨平台的图形用户界面应用程序框架QT,是在Windows开发环境下结合QT和C++技术开发的一款游戏[4]。QT具有跨平台的优良性和面向对象,具有丰富的API,库的特点,即使用QT开发的软件,相同的代码可以在任何支持的平台上编译与运行,而不需要修改(或修改极少)源代码并且会自动依平台的不同,表现平台特有的图形界面风格。QT的良好封装机制使得QT的模块化程度非常高,可重用性较好,极大地提高了开发者的工作效率[5]。在计算机网络全面普及形势下,互联网应用屡见不鲜,而作为大众化网络娱乐方式,网络游戏用户不断增多[6]。网络麻将因此备受喜爱,根据用户的产品需求,确定软件的设计策略[7],利用C++语言进行软件的开发与设计,可以使得应用软件的编程代码在不同的平台下进行工作[8]。
2.2 游戏的流程图
软件运行时步骤流程:运行程序首先需要洗牌将牌墙显示,然后摇骰子显示出点子数同时进行起牌(52张牌),使得牌墙对应部分的牌消失,显示出庄家和四家手牌(每家13张牌),发庄家的第14张牌,再取皮子、癞子,最后显示排序后的四家手牌(每家的皮子、癞子显示在手牌首部,普通牌根据牌型聚集,根据数字由小到大排序)。流程如图1所示。
2.3 数据结构设计
系统的主要数据结构是一个结构体,这个结构体Card表示牌。结构体中的属性包括QString类型的suit表示“万、条、筒、风”,int类型的牌上的数point,int类型的四家num[9],如图2所示。
2.4 函数设计
首先确定上、下、左、右分别为西、东、南、北。
2.4.1 洗牌算法
随机交换两张牌,使牌墙随机无序显示136张普通牌,洗牌发牌初始化将所有容器(比如手牌容器、手牌排序容器、手牌保存容器)清空,所有按钮也设置为空,避免错误。将洗好的牌装入一个全局Cards_One_Game容器中,Cards_One_Game是一个每局存储所有牌的容器,大小为136。再将所有牌转换为String类型用于显示(这一算法见下)。
2.4.2 表述牌Card
为了在界面显示出牌,在整个程序里表述每张牌,将card转换为String类型的字符串,每张牌根据其牌型,数可以唯一确定一个字符串。这里需要的资源是135张普通牌的图片,135张癞子牌的图片,135张皮子牌的图片,一张红中杠的图片,分别放入四个文件夹中。需要用到QT资源系统将这四个文件夹加载,以方便牌的加载显示与替换,防止图标被修改或图标文件丢了程序界面就不能正常显示,使程序更为可靠。以万字牌为例:万字牌的suit属性可以为“万”“万(皮)”“万(癞)”,不同的字对应不同的资源文件,根据不同的suit从资源文件中不同的文件夹中加载对应的图片资源,运用arg()函数实现根据point填充字符串。为了方便这里使用静态加载方式,直接将资源数据存储在可执行文件中以保证即使删除了文件夹中的图标也不会影响界面图标的加载。
2.4.3 确定起牌起始位置算法
随机确定两个1到6之间的数并显示到界面固定区域实现摇骰子。两数的和为add,两数中较小的一个数赋值给nums,可以由add确定起牌起始处的方位,nums为留牌墩数,一起确定起牌起始位置(start_pc_btn)。
当add等于1或者5或者9时,从东家面前开始拿牌,起牌开始处n为0;当add等于2或者6或者10时,从南家面前开始拿牌,n为102;当add等于3或者7或者11时,从西家面前开始拿牌,n为68;当add等于4或者8或者12时,从北家面前开始拿牌,n为34;开局一人拿13张牌,一共52张,则52张牌在牌墙中消失,不显示。倘若起牌开始处大于136,则起牌开始处更新为start_pc_btn%136。
2.4.4 确定庄家
点击显示庄家按钮,根据确定的庄家显示东、南、西、北、相应的图片[10]。
2.4.5 起牌算法
起牌是一家拿两墩(4张),再下一家拿,四家拿完16张牌为一轮,如此重复拿三轮,最后每家再拿一张即可拿够52张牌。需要通过三个for循环嵌套实现,第一次为轮次,总共三轮,需要一个计数器k保证三轮。在每一轮中需判断玩家的拿牌顺序,第二次为四位玩家的循环,需要一个计数器z保证四家,从庄家开始拿牌,玩家根据东南西北顺序拿牌,初始时计数器z为庄家,每一家拿完兩墩牌计数器加一,同时对计数器进行判断:计数器小于等于4,如果计数器大于4则用z%4更新z。在每家拿四张牌中需要判断每张牌的合法性,所以需要一个计数器i小于四(i从0开始)的循环确保每家拿四张牌,因为一共136张牌,不可能拿到第137张牌,所以每拿一张牌都要对这张牌的序号进行判断,再将每家拿到的牌装入相应容器内。起牌开始处start_pc_btn加前几轮四家共拿牌数(轮数×16)加这一轮前几家所拿牌数((计数器z-1)×4)加自己所拿的四张牌的计数器i(0到3)的和需要小于136(牌的序号从0开始),否则总和模136来更新序号y。
最后每一家按序拿一张牌,这里只需要一次玩家循环,对每张牌序号的判断,然后存入容器,排序显示,更新index(指向牌墙中剩余牌的第一张),52张牌发牌完成。
2.4.6 发庄家最后一张牌
庄家拿第53张牌即index指向的那张牌,从容器Cards_One_Game中取出Cards_One_Game[index]保存到手牌保存容器,排序显示,牌墙中对应位置设为空,index加一。
2.4.7 取皮子癞子
庄家翻一张牌,这张牌以及其前面的一张牌叫皮子,这张牌后面的一张牌叫癞子;特别的当红中被翻,西、北、红中都为皮子;红中不能是癞子,翻北风,则发财是癞子;由庄家翻到的牌确定皮子2(pizi2)得到皮子1(pizi1)、皮子0(pizi0)、癞子(laizi)。庄家翻的牌即是index(发完53张牌后)所指向的牌,得到pizi2,调用getPizi(pizi2),getLaizi(pizi2)函数得到皮子1、癞子。由于每张牌由数据结构card描述,并且万、条、筒的point都是1到9,字牌的point是10到16,当皮子2是数牌时,癞子和其他的皮子也是数牌根据规则即可推出;当皮子2是风牌时,根据东、南、西、北、中、发、白的顺序也可以推出皮子、癞子;当皮子2不为红中时皮子0(pizi0)为空不显示。
2.4.8 排序算法
手牌完全显示时或者每次摸牌时,需要将数牌、字牌按序(同一牌型相邻,相同牌型根据point从小到大)显示,并将皮子癞子显示在手牌前面。在没有确定皮子、癞子时,起52张牌也需要排序,把数牌、字牌按序显示。所以排序算法中要分为两种情况,一种是在未确定皮子、癞子,各家发了13张牌时的手牌排序显示;一种是在确定了皮子、癞子,庄家已经发了第14张牌时的手牌排序显示。
在未确定皮子、癞子时通过for循环对容器进行遍历,统计万、条、筒、风牌(通过牌的suit属性)计数并装入相应的容器中。
在确定皮子、癞子后通过for循环对容器进行遍历统计不为皮子、癞子(通过牌的point,suit属性)的万、条、筒加入容器,计数;再统计皮子牌和癞子牌,加入容器,计数(与皮子字相同、数相同的牌就是皮子;与癞子字相同、数相同的牌就是癞子),注意52张手牌完全显示时,手牌中的皮子癞子牌上没显示皮/癞的标记,所以suit属性还是“万、条、筒、东风……”,但在开始打牌后,suit属性就可能是“万(皮)/(癞)、条(皮)/(癞)、筒(皮)/(癞)、东风(皮)/(癞)……”或者“万、条、筒、东风……”。所以为了程序的可扩展性,需要对suit属性进行多重判断,在满足suit属性相等的同时point属性也要相等,然后加入容器,计数。
定义一个比较器compless(),重载运算符“<”,用牌的point属性比较两张牌的大小,std::sort是C++ STL中最重要的算法之一,std::sort封装了快速排序算法,可以与for_each算法相提并论,当我们有排序需要时,可能最先想到的就是它。这个算法是一个接口模板,在内部实现可能会根据不同情况使用不同的算法。在使用形式上存在两种方式,一种是使用小于运算符进行比较,一种使用传入的函数对象(仿函数)进行比较。这里使用后一种。
在上面获得的各个容器中从容器的开头到结尾用仿函数进行比较,在皮子癞子容器中将suit属性为“万、条、筒、东风……”改为“万(皮)/(癞)、条(皮)/(癞)、筒(皮)/(癞)、东风(皮)/(癞)……”再排序。
用排序好的牌更新容器,返回此容器。
2.5 核心算法
起52张牌部分伪代码:
算法1:起牌算法
输入:136张牌
输出:四家手牌
1)if 轮次k<3 then
2) if 玩家数z<=4 then
3) if z>4 更新z;
4) if 牌的序号合法then
5) 从全局牌容器中取出对应序号的牌加入玩家手牌容器中;
6) else
7) 更新牌的序号;
8) 从全局牌容器中取出对应序号的牌加入玩家手牌容器中;
9) end if
10) end if
11) end if
12)end if
13)if 玩家数z<=4 then
14) 四家按顺序拿一张牌,从全局牌容器中取出对应序号的牌加入玩家手牌容器中;
15)end if
排序算法的伪代码:
算法2:排序算法
输入:每家的手牌
输出:排序后的手牌
1)定义万、条、筒、风、皮子、癞子的计数器,容器,p=一张手牌
2) if 已確定皮子
3) if 手牌的suit属性=“万”(以万字牌为例)
4) if 皮子的suit属性!=“万”
5) 将这张手牌p加入万字牌容器,计数;
6) end if
7) else if
8) if 手牌p的point属性!=皮子的point属性and手牌p的point属性!=癞子的point属性
9) 将这张手牌p加入万字牌容器,计数;
10) end if
11) if p的suit==“万(皮)”or”万”
12) if p的suit==皮子的suit and p的point==皮子的point
13) 将这张手牌p加入皮子牌容器,计数
14) end if
15) end if
16) if p的suit==“万(癞)”or”万”
18) if p的suit==癞子的suit and p的point==癞子的point
19) 将这张手牌p加入癞子牌容器,计数
20) end if
21) end if
22)end if
23)if 未确定皮子
24) 根据suit属性将相应的牌加入对应容器,计数
25)end if
26)if 皮子or癞子的计数器!=0
27) 将皮子or癞子排序
28) if 皮子癞子未赋值
29) 对皮子癞子进行赋值
30) end if
31)將其他牌排序
32)返回排序后的牌
3 结果及分析
程序运行时界面布局:左上角为皮子癞子显示区域,左侧栏为功能按钮区,右边为成牌、手牌、牌墙、牌池显示区,如图3所示。
进入有游戏界面需要手动洗牌,起牌,确定庄家,取皮子癞子。点击洗牌按钮,牌墙随机显示136张牌,手牌为空,牌池为空,如图4所示。
点击摇骰子和起牌按钮,点子部分出现两个范围为1到6的随机数,根据这两个数确定起始起牌位置,然后牌墙的52张牌消失。如图5所示,两个点子数是1、3,所以从北方拿牌,留一墩牌,图中黑色框圈出的即为起的52牌。
点击显示庄家按钮显示庄家。
点击显示手牌按钮,调用确定庄家函数,起牌函数,将牌墙中消失的52张牌按规则显示在四家手牌位置,如图6黑色框部分所示。
点击发庄家最后一张牌按钮将庄家的第14张牌发牌,排序显示,并删除牌墙中的后一张牌,牌墙更新,庄家手牌显示更新,如图7所示。
点击取皮子,癞子按钮,调用相关函数并显示皮子癞子在相关区域。
根据排序算法将手牌中的皮子癞子显示到手牌首部,其他牌按序显示,得到排序后的手牌,如图8所示。
4 结 论
通过对武汉麻将规则的学习以及相关算法的分析,确定出了麻将起牌过程中的关键点,借助C++语言,QT平台显示最终起牌效果。研究发现,起牌核心是摇骰子确定点子数,通过两点数和与较小点子数确定起始拿牌具体位置;排序算法应用于摸牌和起牌函数中,在确定了皮子、癞子前后,牌面显示有所差异,确定了皮子、癞子后,对比之下需要统计皮子、癞子的数目,并将皮子、癞子排在手牌前面。通过一系列的算法分析,以C++语言编程实现起牌效果的显示,从而使游戏可以顺利进行。
参考文献:
[1] 李淑琴,李奕.一种多重优先经验回放的麻将游戏数据利用方法 [J].重庆理工大学学报:自然科学,2022,36(12):162-169.
[2] 李霞丽,王昭琦,刘博等.麻将博弈AI构建方法综述 [J/OL].智能系统学报:1-12(2023-07-31).http://kns.cnki.net/kcms/detail/23.1538.TP.20230731.1007.002.html.
[3] 程卫星,郝爱民.一种基于智能体的游戏消息公平处理方法 [J].计算机科学,2008(3):283-288.
[4] 陈龙,吴龙飞.基于Qt的数字图像处理实验演示系统 [J].实验室研究与探索,2018,37(7):170-173+202.
[5] 刘坤,涂德浴,朱庆,等.基于Qt的钢管壁厚在线检测软件设计 [J].机床与液压,2023,51(7):93-99.
[6] 李樊.基于VR技术的三维射击游戏开发研究 [J].自动化技术与应用,2021,40(1):163-166.
[7] 张少玉.基于信息化的软件开发策略 [J].信息技术与信息化,2022(11):94-97.
[8] 汤晓军.基于C++语言的跨平台软件开发设计 [J].信息技术与信息化,2022(3):36-39.
[9] 李淑琴,冯浩东.牌型预测与蒙特卡洛模拟结合的麻将博弈策略 [J].重庆理工大学学报:自然科学,2022,36(12):148-154.
[10] 黄勇,尚亚东,陈秀华,等.麻将桌上的一个概率问题 [J].数学的实践与认识,2015,45(16):132-136.
作者简介:徐子怡(2000—),女,汉族,湖北仙桃人,硕士研究生在读,研究方向:人工智能;通讯作者:林富生(1965—),男,汉族,江苏台州人,教授,博士,研究方向:人工智能;宋志峰(1972—),男,汉族,湖北孝感人,副教授,硕士,研究方向:计算机集成制造系统;徐自立(1964—),男,汉族,湖北武汉人,教授,博士,研究方向:耐磨与抗磨材料;余联庆(1972—),男,汉族,湖北咸宁人,教授,博士,研究方向:机器人机构学。