喜欢玩日本麻将的雀友都知道,麻将一般是3-4人玩的,但是如何连3个人都凑不齐呢?那么,2人玩法就应运而生了!具体玩法请移步至萌娘百科十七步查阅…
关于如何从34张牌中取13张牌组成最大番数听牌
下面开始进入正题,正常情况下,如果要从34张牌中取13张,算了下排列组合,要进行:34!/13!/21!=927983760次运算,太恐怖了,一般程序猿应该不会干这种事吧。
没办法了,只好逐个拆解分析,只提取有效的结果进行比对…
一、听牌公式分析
日本麻将一般有的听牌模式有三大种类:
1、国士无双:十三张幺九牌+任意一种幺九牌
这个就简单了,一共14种情况,傻瓜式判断→_→
2、七对子:七种不同的对子
要七种,而且是不同的对子,因为是要听牌,所以组合是六种对子+第七种的一张牌,这种情况下,肯定要先将所有含有2张以上的牌记录下来,如果大于等于六种对子就进去分析,每次取其中6组对子,此时最大的判断次数是:17!/6!/11!=12376,勉强还能接受吧,实际情况下都是个位数的对子,所以应该会判断得更少。
3、一般情况:4组牌加1对
4组牌,可以是顺子,可以是刻子,,也是因为要听牌,所以最终的组合形式有:
4组顺子、刻子+1张牌:
(*32!/4!/28!+32!/3!/29!*11+32!/2!/30!11!/2!/9!+3211!/3!/8!+11!/4!/7!)34=4195940
3组顺子、刻子+2组对子:
(32!/3!/29!+32!/2!/30!11+3211!/2!/9!+11!/3!/8!)(17!/2!/15!)=1678376
3组顺子、刻子+1组对子+1组缺一张的顺子:
(32!/3!/29!+32!/2!/30!11+3211!/2!/9!+11!/3!/8!)1733=6923301
(其中:顺子最大组合有:32,刻子最大组合有:11,对子的最大组合有:17,缺一张顺子的最大组合有:33)
综上所述,最大的判断次数为:12810007,好吧,还是很多,但这只是个极限数,大多情况下顺子+刻子的组合数量是不会超过个位数的,所以判断次数会远远小于这个数,安心地实践吧。
二、代码分析
首先,要进行组合,就要找出组合的元素先。
记录的元素有:所有存在的牌及对应的数量,所有的对子、刻子、顺子、缺一张的顺子
/**
* 十七步算法:根据34张牌挑选出13张最优解牌谱
* --限定条件(确定:门清,可能:立直、一发、河底,无赤宝牌)
*/
public static boolean calcGame34ToResultScores(int calcWay,
Game34Result game34Result, List<MjCard> cardNums,
boolean isDealer, boolean isLiZhi, boolean isYiFa, boolean isFinalPick,
MjWind groundWind, MjWind selfWind,
int lizhiCount, int roundCount,
List<MjCard> indicators, List<MjCard> indicatorsIns) {
// 用于计算的分值的元素
int env = convert2JpnEnvironment(isLiZhi, false, false, isFinalPick,
false, isYiFa, false, false, groundWind, selfWind, true); // 环境变量
// 计算宝牌
int[] doras = convertCard2TileDoras(indicators);
// 计算里宝牌
int[] doraIns = null;
if (isLiZhi) {
doraIns = convertCard2TileDoras(indicatorsIns);
} else {
doraIns = new int[indicatorsIns.size()];
Arrays.fill(doraIns, -1);
}
game34Result.init(env, calcWay, doras, doraIns);
// 记录所有牌,并分类
Tile[] tileMap = new Tile[34]; // 记录所有存在的牌
int[] tileCounts = new int[34];// 记录所有存在的牌对应的数量
Arrays.fill(tileCounts, 0);
List<Integer> pairTiles = new ArrayList<Integer>(); // 记录所有的对子
List<Integer> pungTiles = new ArrayList<Integer>(); // 记录所有的刻子
long byteTile = 0; // 用二进制记录存在的牌
for (MjCard card : cardNums) {
Tile tile = convertCard2JpnTile(card, selfWind, false);
tile.calDora(doras);
tile.calInDora(doraIns);
tileMap[tile.Value()] = tile;
tileCounts[tile.Value()]++;
byteTile |= (1L << tile.Value());
if (tileCounts[tile.Value()] == 2) { // 对子
pairTiles.add(tile.Value());
} else if (tileCounts[tile.Value()] == 3) { // 刻子
pungTiles.add(tile.Value());
}
}
// 记录所有可用的点炮牌
boolean[] canBombs = new boolean[34];
Tile[] bombTiles = new Tile[34];
Arrays.fill(canBombs, false);
List<Integer> tileKeys = new ArrayList<Integer>(); // 记录所有存在的牌的索引
List<Integer> junkoTiles = new ArrayList<Integer>(); // 记录所有的顺子
List<Integer> junkoQbTiles = new ArrayList<Integer>(); // 记录所有缺边章的顺子,形如23缺1或4
List<Integer> junkoQzTiles = new ArrayList<Integer>(); // 记录所有缺中章的顺子,形如13缺2
long junkoByte = 0x3fe030180L; // 只含有8万、9万、8饼、9饼、8索、9索、东、南、西、北、白、发、中
long junkoQbByte = 0x3fc020100L; // 只含有9万、9饼、9索、东、南、西、北、白、发、中
// long junkoQzByte = junkoByte; // 只含有8万、9万、8饼、9饼、8索、9索、东、南、西、北、白、发、中
for (int i = 0; i < tileCounts.length; i++) {
if (tileCounts[i] < 4) {
canBombs[i] = true;
bombTiles[i] = new Tile(i, false, ramdomWind(selfWind));
bombTiles[i].calDora(doras);
bombTiles[i].calInDora(doraIns);
}
if (tileCounts[i] > 0) {
tileKeys.add(i);
}
long tmpByte = 1L << i;
if ((tmpByte & junkoByte) != tmpByte) {
switch (tileCounts[i]) {
case 4:
if (tileCounts[i + 1] > 3 && tileCounts[i + 2] > 3) junkoTiles.add(i);
case 3:
if (tileCounts[i + 1] > 2 && tileCounts[i + 2] > 2) junkoTiles.add(i);
case 2:
if (tileCounts[i + 1] > 1 && tileCounts[i + 2] > 1) junkoTiles.add(i);
case 1:
if(tileCounts[i + 1] > 0 && tileCounts[i + 2] > 0) junkoTiles.add(i);
if (tileCounts[i + 2] > 0) junkoQzTiles.add(i);
break;
case 0:
default:
break;
}
}
if ((tmpByte & junkoQbByte) != tmpByte) {
if (tileCounts[i] > 0 && tileCounts[i + 1] > 0) junkoQbTiles.add(i);
}
}
// 判断特殊牌型(国士无双、七对子)
// 1.国士无双
calcGame34ForGuoShiWuShuang(game34Result, byteTile, tileMap, tileCounts, canBombs, bombTiles);
// 2.七对子
calcGame34ForQiDuiZi(game34Result, pairTiles, tileMap, tileKeys, canBombs, bombTiles);
// 3.检测所有顺子、刻子的排列组合(单骑、听嵌章、双面听)
calcGame34ForGroup(game34Result, junkoTiles, pungTiles, tileCounts, tileMap, tileKeys,
canBombs, bombTiles, pairTiles, junkoQbTiles, junkoQzTiles);
return true;
}
其中,Game34Result用于记录和保存结果
public static class Game34Result {
public int env; // 环境变量
public int calcWay; // 计量方式:0:分值优先,1:番数优先
public List<Tile> addedTiles; // 和牌(最高分时)
public List<List<Tile>> handTiles; // 门清牌(最高分时)
public List<Score> scores; // 存在多种结果时的分值
public List<long[]> cmpNums; // 用于快速比较的数字,将14张牌转换成2个long数字
public int[] doras; // 宝牌组
public int[] doraIns; // 里宝牌组
public Score maxScore; // 最大结果
public int level; // 显示等级:1、一般情况;2、不听宝牌
public Score level1Score; // 当显示2级结果时,不能超过的1级结果的上限
public boolean isInit = false;
}
接下来,就要进行分类判断
这里为了快速进行比对,使用一个long表示的二进制数,麻将有34种,所以要64位的数据
1、国士无双
private static void calcGame34ForGuoShiWuShuang(Game34Result game34Result,
long byteTile, Tile[] tileMap, int[] tileCounts,
boolean[] canBombs, Tile[] bombTiles) {
long gswsByte = 0x3fc060301L; // 只含有1万、9万、1饼、9饼、1索、9索、东、南、西、北、白、发、中
if ((gswsByte & byteTile) == gswsByte) { // 国士无双十三面
List<Tile> tmpHandTiles = new ArrayList<Tile>();
for (int yaojiu : JpnSetting.YaoJiu) { // 首先添加13张幺九牌
tmpHandTiles.add(tileMap[yaojiu]);
}
for (int yaojiu : JpnSetting.YaoJiu) { // 再遍历所有可用的点炮牌
if (!canBombs[yaojiu]) continue;
Tile tmpAddedTile = bombTiles[yaojiu];
// 取分值最大的结果
game34Result.LogMaxScore(tmpAddedTile, tmpHandTiles, null);
}
} else { // 国士无双缺一张
for (int yaojiu : JpnSetting.YaoJiu) {
long tmpGswsByte = gswsByte ^ (1L << yaojiu);
if ((tmpGswsByte & byteTile) == tmpGswsByte) {
// 判断是否存在2只及以上的幺九牌
boolean isExistYaojiuAbove2 = false;
List<Tile> yaojiuAbove2 = new ArrayList<Tile>();
for (int yaojiu2 : JpnSetting.YaoJiu) {
if (tileCounts[yaojiu2] >= 2) {
isExistYaojiuAbove2 = true;
yaojiuAbove2.add(tileMap[yaojiu2]);
}
}
if (!isExistYaojiuAbove2) break; // 不存在则退出
List<Tile> tmpHandTiles = new ArrayList<Tile>();
for (int yaojiu2 : JpnSetting.YaoJiu) {
if (tileMap[yaojiu2] == null) continue; // 无此牌,则跳过
long keyByte = 1L << yaojiu2;
if ((keyByte & tmpGswsByte) != keyByte) continue; // 如果不是缺一张的幺九牌,则跳过
tmpHandTiles.add(tileMap[yaojiu2]);
}
Tile tmpAddedTile = bombTiles[yaojiu]; // 点炮牌唯一
for (Tile yaojiuTile : yaojiuAbove2) { // 遍历存在2张的幺九牌
List<Tile> tmpHandTiles2 = new ArrayList<Tile>(tmpHandTiles);
tmpHandTiles2.add(yaojiuTile);
// 取分值最大的结果
game34Result.LogMaxScore(tmpAddedTile, tmpHandTiles2, null);
}
break; // 符合其中一种,另外8种情况可以直接排除
}
}
}
}
2、七对子
private static void calcGame34ForQiDuiZi(Game34Result game34Result,
List<Integer> pairTiles, Tile[] tileMap, List<Integer> tileKeys,
boolean[] canBombs, Tile[] bombTiles) {
if (pairTiles.size() >= 6) {
for (int i = 0; i < (1 << pairTiles.size()); i++) { // 2的n次方则为所有组合的次数
int countOfbyte1 = CountOfByte1(i);
if (countOfbyte1 == 6) { // 取所有6组不同对子的组合
long tmpByte = 0;
List<Tile> tmpHandTiles = new ArrayList<Tile>();
for (int j = 0; j < pairTiles.size(); j++) {
if (((i >> j) & 1) == 1) { // 二进制位为1则加入
int value = pairTiles.get(j);
tmpByte |= (1L << value);
tmpHandTiles.add(tileMap[value]);
tmpHandTiles.add(tileMap[value]);
}
}
// 遍历所有可用的点炮牌
for (int key : tileKeys) {
if (!canBombs[key]) continue; // 如果有4张相同的牌,则跳过
long keyByte = 1L << key;
if ((keyByte & tmpByte) == keyByte) continue; // 如果是6对组合中的值,则跳过
List<Tile> tmpKeyHandTiles = new ArrayList<Tile>(tmpHandTiles);
tmpKeyHandTiles.add(tileMap[key]);
Tile tmpAddedTile = bombTiles[key];
// 取分值最大的结果
game34Result.LogMaxScore(tmpAddedTile, tmpKeyHandTiles, null);
}
}
}
}
}
3、一般情况
private static void calcGame34ForGroup(Game34Result game34Result,
List<Integer> junkoTiles, List<Integer> pungTiles,
int[] tileCounts, Tile[] tileMap, List<Integer> tileKeys,
boolean[] canBombs, Tile[] bombTiles,
List<Integer> pairTiles, List<Integer> junkoQbTiles, List<Integer> junkoQzTiles) {
int junkoAndPungCount = junkoTiles.size() + pungTiles.size();
if (junkoAndPungCount >= 3) {
for (int i = 0; i < (1 << junkoAndPungCount); i++) { // 2的n次方则为所有组合的次数
int countOfbyte1 = CountOfByte1(i);
if (countOfbyte1 == 4 || countOfbyte1 == 3) { // 单骑找出4种组合, 听嵌章、双面听则找出3种
boolean isOverload = false;
int[] tmpTileCounts = tileCounts.clone();
List<Tile> tmpHandTiles = new ArrayList<Tile>();
List<Group> allGroups = new ArrayList<Group>();
for (int j = 0; j < pungTiles.size(); j++) { // 从所有刻子中取组合
if (((i >> j) & 1) == 1) { // 二进制位为1则加入
int value = pungTiles.get(j);
if (tmpTileCounts[value] < 3) {isOverload = true; break;}
tmpTileCounts[value] -= 3;
tmpHandTiles.add(tileMap[value]);
tmpHandTiles.add(tileMap[value]);
tmpHandTiles.add(tileMap[value]);
allGroups.add(new Groups.Pung(value, GroupState.MenQing));
}
}
if (isOverload) continue;
for (int j = 0; j < junkoTiles.size(); j++) { // 从所有顺子中取组合
if (((i >> (j + pungTiles.size())) & 1) == 1) { // 二进制位为1则加入
int value = junkoTiles.get(j);
if (tmpTileCounts[value]-- <= 0) {isOverload = true; break;}
tmpHandTiles.add(tileMap[value]);
if (tmpTileCounts[value + 1]-- <= 0) {isOverload = true; break;}
tmpHandTiles.add(tileMap[value + 1]);
if (tmpTileCounts[value + 2]-- <= 0) {isOverload = true; break;}
tmpHandTiles.add(tileMap[value + 2]);
allGroups.add(new Groups.Junko(value, GroupState.MenQing));
}
}
if (isOverload) continue;
if (countOfbyte1 == 4) { // 单骑
// 遍历所有可用的点炮牌
for (int key : tileKeys) {
if (!canBombs[key]) continue; // 如果有4张相同的牌,则跳过
if (tmpTileCounts[key] == 0) continue; // 如果是使用完的手牌,则跳过
List<Group> tmpAllGroups = new ArrayList<Groups.Group>(allGroups);
tmpAllGroups.add(new Groups.Pair(key, GroupState.HePai));
List<Tile> tmp4BHandTiles = new ArrayList<Tile>(tmpHandTiles);
tmp4BHandTiles.add(tileMap[key]);
Tile tmpAddedTile = bombTiles[key];
// 取分值最大的结果
game34Result.LogMaxScore(tmpAddedTile, tmp4BHandTiles, tmpAllGroups);
}
} else if (countOfbyte1 == 3) { // 顺子听嵌章、顺子双面听、两对双面听
for (int j = 0; j < pairTiles.size(); j++) { // 先取出1个对子
int[] tmp2TileCounts = tmpTileCounts.clone();
List<Tile> tmp2HandTiles = new ArrayList<Tile>(tmpHandTiles);
List<Group> tmp2AllGroups = new ArrayList<Groups.Group>(allGroups);
Tile tmpAddedTile;
int value = pairTiles.get(j);
if (tmp2TileCounts[value] < 2) continue;
tmp2TileCounts[value] -= 2;
tmp2HandTiles.add(tileMap[value]);
tmp2HandTiles.add(tileMap[value]);
tmp2AllGroups.add(new Groups.Pair(value, GroupState.MenQing));
for (int k = 0; k < junkoQbTiles.size(); k++) { // 顺子双面听
List<Tile> tmp3HandTiles = new ArrayList<Tile>(tmp2HandTiles);
List<Group> tmp3AllGroups = new ArrayList<Groups.Group>(tmp2AllGroups);
int QbValue = junkoQbTiles.get(k);
if (tmp2TileCounts[QbValue] <= 0) continue;
tmp3HandTiles.add(tileMap[QbValue]);
if (tmp2TileCounts[QbValue + 1] <= 0) continue;
tmp3HandTiles.add(tileMap[QbValue + 1]);
int num = QbValue % 9;
if (num == 0) { // 12听3
if (!canBombs[QbValue + 2]) continue;
tmpAddedTile = bombTiles[QbValue + 2];
tmp3AllGroups.add(new Groups.Junko(QbValue, GroupState.HePai, 2));
// 取分值最大的结果
game34Result.LogMaxScore(tmpAddedTile, tmp3HandTiles, tmp3AllGroups);
} else if (num == 8) { // 89听7
if (!canBombs[QbValue - 1]) continue;
tmpAddedTile = bombTiles[QbValue - 1];
tmp3AllGroups.add(new Groups.Junko(QbValue - 1, GroupState.HePai, 0));
// 取分值最大的结果
game34Result.LogMaxScore(tmpAddedTile, tmp3HandTiles, tmp3AllGroups);
} else { // 23听1、4
if (canBombs[QbValue - 1]) {
tmpAddedTile = bombTiles[QbValue - 1];
List<Group> tmp4AllGroups = new ArrayList<Groups.Group>(tmp3AllGroups);
tmp4AllGroups.add(new Groups.Junko(QbValue - 1, GroupState.HePai, 0));
// 取分值最大的结果
game34Result.LogMaxScore(tmpAddedTile, tmp3HandTiles, tmp4AllGroups);
}
if (canBombs[QbValue + 2]) {
tmpAddedTile = bombTiles[QbValue + 2];
List<Group> tmp4AllGroups = new ArrayList<Groups.Group>(tmp3AllGroups);
tmp4AllGroups.add(new Groups.Junko(QbValue, GroupState.HePai, 2));
// 取分值最大的结果
game34Result.LogMaxScore(tmpAddedTile, tmp3HandTiles, tmp4AllGroups);
}
}
}
for (int k = 0; k < junkoQzTiles.size(); k++) { // 顺子听嵌章
List<Tile> tmp3HandTiles = new ArrayList<Tile>(tmp2HandTiles);
List<Group> tmp3AllGroups = new ArrayList<Groups.Group>(tmp2AllGroups);
int QzValue = junkoQzTiles.get(k);
if (tmp2TileCounts[QzValue] <= 0) continue;
tmp3HandTiles.add(tileMap[QzValue]);
if (tmp2TileCounts[QzValue + 2] <= 0) continue;
tmp3HandTiles.add(tileMap[QzValue + 2]);
if (!canBombs[QzValue + 1]) continue;
tmpAddedTile = bombTiles[QzValue + 1];
tmp3AllGroups.add(new Groups.Junko(QzValue, GroupState.HePai, 1));
// 取分值最大的结果
game34Result.LogMaxScore(tmpAddedTile, tmp3HandTiles, tmp3AllGroups);
}
}
if (pairTiles.size() >= 2) { // 两对双面听
for (int j = 0; j < (1 << pairTiles.size()); j++) { // 2的n次方则为所有组合的次数
if (CountOfByte1(j) == 2) { // 取所有2组不同对子的组合
List<Tile> tmp2HandTiles = new ArrayList<Tile>(tmpHandTiles);
int[] tmpAddedKeys = new int[2];
int index = 0;
boolean canDone = true;
for (int k = 0; k < pairTiles.size(); k++) {
if (((j >> k) & 1) == 1) { // 二进制位为1则加入
int value = pairTiles.get(k);
if (tmpTileCounts[value] < 2) {
canDone = false;
break;
}
tmp2HandTiles.add(tileMap[value]);
tmp2HandTiles.add(tileMap[value]);
tmpAddedKeys[index++] = value;
}
}
if (!canDone) continue;
for (int key : tmpAddedKeys) {
if (!canBombs[key]) continue; // 如果有4张相同的牌,则跳过
List<Group> tmp2AllGroups = new ArrayList<Groups.Group>(allGroups);
Tile tmpAddedTile = bombTiles[key];
if (key == tmpAddedKeys[0]) {
tmp2AllGroups.add(new Groups.Pung(tmpAddedKeys[0], GroupState.HePai));
tmp2AllGroups.add(new Groups.Pair(tmpAddedKeys[1], GroupState.MenQing));
game34Result.LogMaxScore(tmpAddedTile, tmp2HandTiles, tmp2AllGroups);
} else if (key == tmpAddedKeys[1]) {
tmp2AllGroups.add(new Groups.Pung(tmpAddedKeys[1], GroupState.HePai));
tmp2AllGroups.add(new Groups.Pair(tmpAddedKeys[0], GroupState.MenQing));
game34Result.LogMaxScore(tmpAddedTile, tmp2HandTiles, tmp2AllGroups);
}
}
}
}
}
}
}
}
}
}
所有步骤的最终都要进行LogMaxScore,即番数的分析和筛选,其中番数的分析请阅读我的上一篇文章日本麻将记点器,筛选的方式有两种:分值优先和番数优先。分值优先(calcWay=0)即只记录最大的相同分值的组合(例如:6、7番跳满为相同分值),番数优先(calcWay=1)即只记录最大番数的组合(这个不用讲了吧)。
另外,十七步里面最难听的牌应该就是宝牌了(我是死也不会打的!!!),当所有最大的结果只能听宝牌时,这是需要退而求其次,降低番数来听牌,所以我还设置了一个level,当level>1时,只需要设置一个组合上限,然后所有组合均小于该组合,就能得到次级结果。
public void LogMaxScore(Tile tmpAddedTile, List<Tile> tmpHandTiles,
List<Group> allGroups) {
// 先分析番数
List<Tile> tmpAllTiles = new ArrayList<Tile>(tmpHandTiles);
tmpAllTiles.add(tmpAddedTile);
List<IGroups> gList = new ArrayList<IGroups>();
if (allGroups != null) gList.add(new GroupCollection(allGroups));
ITiles tiles = new TileCollection(tmpAllTiles, tmpAllTiles, tmpAddedTile);
Score tmpScore = mGame.getScore(tiles, gList, env);
if (tmpScore == null || !tmpScore.hasYaku()) {
return;
}
// 再做筛选比对
if (level > 1) { // 当level大于1时,所有结果均需小于上级最大结果
int levelCmp = calcWay == 0 ? ScoreSystem.compareForGame34(level1Score, tmpScore) :
ScoreSystem.compare(level1Score, tmpScore);
if (levelCmp <= 0) return;
}
long[] cmpNum = convert2CmpNums(tmpAllTiles, tmpAddedTile); // 使用两个long来记录牌谱
if (maxScore == null) { // 当无组合时
maxScore = tmpScore;
scores.add(tmpScore);
addedTiles.add(tmpAddedTile);
handTiles.add(tmpHandTiles);
cmpNums.add(cmpNum);
} else {
int cmp = calcWay == 0 ? ScoreSystem.compareForGame34(maxScore, tmpScore) :
ScoreSystem.compare(maxScore, tmpScore); // 选择比对方式
if (cmp <= 0) {
if (cmp != 0) { // 如果遇到更大的结果,则先清空
scores.clear();
addedTiles.clear();
handTiles.clear();
cmpNums.clear();
}
int replaceIndex = -1;
for (int i = 0; i < cmpNums.size(); i++) {
long[] tmpNum = cmpNums.get(i);
if (tmpNum[0] == cmpNum[0] && tmpNum[1] == cmpNum[1]) {
// 就算是牌谱相同,但是不同的组合也会导致平和的缺失,此时要比较总番数来替换组合
if (calcWay == 0 && tmpScore.FullYaku() == 0
&& tmpScore.AllFanValue() > scores.get(i).AllFanValue()) {
replaceIndex = i;
break;
}
return;
}
}
maxScore = tmpScore;
if (replaceIndex >= 0) { // 当牌谱相同但组合不同时,则需要替换
scores.set(replaceIndex, tmpScore);
addedTiles.set(replaceIndex, tmpAddedTile);
handTiles.set(replaceIndex, tmpHandTiles);
cmpNums.set(replaceIndex, cmpNum);
} else { // 相同结果,直接加入
scores.add(tmpScore);
addedTiles.add(tmpAddedTile);
handTiles.add(tmpHandTiles);
cmpNums.add(cmpNum);
}
}
}
}
三、总结
经测试,输出结果大多在1-2s内,总体时间还是可以接受的。实践出真知,这只是我的初步分析而已,也需要大量的测试才能发现更多未知的bug,而这次只需要拉上你的一位好友就可以愉快地玩耍啦,当然如果各位大神还有其他想法的话欢迎赐教!
附源代码地址:
https://github.com/WaSuper/Mahjong