由于麻将胡牌规则固定(只是牌型不同):3m+2(3指的是顺子、刻子,2指的是将牌(一对))。比如有个牌型123万789条白白白中中,这个手牌就是胡牌的牌型,其中m=3。
想要形成胡牌的牌型,我们需要保留当前牌型中的顺子、刻子和对,以及可以形成成顺子的差一张牌的牌(比如13,差一张2。比如34,差一个2或者5。)。这样子拆分后的牌剩下的只有单牌,所以我们出牌时可以先将单牌打出,然后再根据摸得新的牌,重新分析,最后形成听牌牌型。当有事件(吃碰杠)时,我们可以对每一个事件分析一次牌型,然后根据形成的牌型所产生的单牌、以及可以形成单牌的牌的权重比,来进行计算当前事件触发后对于牌型改变的优先级。(比如:对于单牌权重要占用比例比较大,可以是0.6以上;然后13牌比例次之,可以是0.25,最后的23牌可以是0.15)。
要分析牌型,我们就需要为牌型新建一个结构体:
//分析牌型工具,首先找出所有的顺子,然后再讲所有刻子找到,然后找出可以形成顺子的牌(比如 23 ,13之类的),然后剩余的牌,就是可以打出的牌
//一种牌型(里面的都是索引)
type TileType struct {
Array_S []byte //顺子切片,只记录第一个
Array_k []byte //刻子切片,只记录第一个
Array_j []byte //可以做将牌的牌,只记录第一个
Array_d []byte //单牌
Array_1_3 []byte //间隔相差1张牌的牌,记录第一个
Array_34 []byte //左右相差一张牌的牌,记录第一个
Array_tile_index []byte // 所有牌索引
mask byte //什么动作之后的分析牌型。比如,普通出牌时的摸牌操作,或吃碰杠等
tile byte //动作牌值
}
在上面的结构体中,我们是以顺子座位第一顺位,然后才是刻子、对子、34、13。
接下来就是对于牌型的提取:
//分析牌型
func AnaTileType(tileIndexs []byte, mask byte, tile byte) *TileType {
tt := &TileType{}
tt.mask = mask
tt.tile = tile
tt.Array_tile_index = append(tt.Array_tile_index, tileIndexs...)
ti := tt.Array_tile_index
//提取顺子123 23 13
for i := byte(0); i < 7; i++ {
v := ti[i]
if v != 0 && ti[i+1] != 0 && ti[i+2] != 0 { //顺子
tt.Array_S = append(tt.Array_S, i)
ti[i]--
ti[i+1]--
ti[i+2]--
}
}
//提取将 刻子
for i, v := range ti {
if v == 2 {
tt.Array_j = append(tt.Array_j, byte(i))
ti[i] = 0
continue
}
if v == 3 {
tt.Array_k = append(tt.Array_k, byte(i))
ti[i] = 0
}
}
//提取 类似 34 的顺子模型
for i := byte(0); i < 7; i++ {
if ti[i] != 0 && ti[i+1] != 0 && ti[i+2] == 0 {
tt.Array_34 = append(tt.Array_34, i)
ti[i]--
ti[i+1]--
}
}
//提取类似 13 的顺子模型
for i := byte(0); i < 7; i++ {
if ti[i] != 0 && ti[i+1] == 0 && ti[i+2] != 0 {
tt.Array_1_3 = append(tt.Array_1_3, i)
ti[i]--
ti[i+2]--
}
}
//提取单牌
for i, v := range ti {
if v == 1 {
tt.Array_d = append(tt.Array_d, byte(i))
}
}
share.LOG.Debug("---牌型:%s", tt.ToString())
return tt
}
接下来就是对出牌的分析了:
/*
ai逻辑
1 先检查听牌
2 去掉单个字牌
3 去掉间隔2个空位的不连续的牌
4 去掉间隔1个空位的不连续的牌
5 去除连续牌数为4、7 中的一张牌,让牌型成为无将胡牌型。如2344万,去掉四万
6 去除连续牌数为3、6、9 中的一张牌,有将则打成一吃二,成为无将听牌型(如233万,去掉三万);无将则打成有将胡牌牌型(如233万,去掉2万)
7 去除连续牌数为2、5、8中的一张牌,让牌型成为有将听牌型。如23445万,去掉5万
8 从将牌中打出一张牌
*/
//接牌 出牌逻辑(没有触发事件:胡 杠)
func (this *ErMahjong_AI) PlayLogic(msg *PGAME.Data_Play, play_res *PGAME.Play) {
t1 := time.Now().Unix()
//取出剩余的牌
if msg.Ext != nil && len(msg.Ext) != 0 {
this.hand.leftTiles = msg.Ext
}
share.LOG.Debug("-------剩余的牌:%v", this.hand.leftTiles)
//是否摸牌
if msg.GetDraw() { //有摸牌,将牌值放入手牌
share.LOG.Debug("---------摸牌:%d", msg.Tiles.Value[0])
if !this.isTing { //如果听牌,直接将摸到的牌打出去
n := 0
//将摸到的牌放入手牌
if num, found := this.hand.tiles_num[msg.Tiles.GetValue()[0]]; found {
n = num + 1
}
this.hand.tiles_num[msg.Tiles.GetValue()[0]] = n
this.hand.tiles[msg.Tiles.GetId()[0]] = msg.Tiles.Value[0]
this.hand.tilesIndex[ValueToIndex(msg.Tiles.Value[0])]++
share.LOG.Debug("------------摸牌之后:%v", this.hand.tilesIndex)
}
}
var drawIds []byte
if msg.GetDraw() {
drawIds = msg.GetTiles().Id
}
//定义出牌id
play_id, tingSend := this.PlayByTing(drawIds)
share.LOG.Debug("玩家手牌tile:%v", this.hand.tiles)
fmt.Println("--------出牌id:", play_id)
play_res.Action = PGAME.Action_ACTION_NULL.Enum()
play_res.Id = proto.Int32(int32(play_id))
if tingSend {
play_res.Action = PGAME.Action_ACTION_TING.Enum()
}
share.LOG.Debug("检查是否听牌花费时间:%d", time.Now().Unix()-t1)
return
}
需要根据是否听牌来出牌:
//根据是否听牌,返回出牌id
/*
如果没有听牌则需要先检查听牌,然后根据是否听牌分析出牌
如果已经听牌,则直接打出摸牌的那张牌
params:
ids:摸牌数组
returns:
playId:出牌id
tingSend :是否发送听牌消息
*/
func (this *ErMahjong_AI) PlayByTing(ids []byte) (playId byte, tingSend bool) {
//检查是否听啤
if this.isTing {
playId = ids[0]
} else { //没有听牌,需要先检查是否听牌
tingMap := CheckTing(this.hand.tilesIndex)
index := -1 //出牌索引
if len(tingMap) > 0 { //如果有听牌,判断该打哪张牌去听牌,一般默认使用可以听最多的牌
maxLen := 0
tingSend = true
for k, v := range tingMap {
if len(v) > maxLen {
index = int(k)
maxLen = len(v)
}
}
this.isTing = true
this.tilesTing = tingMap[byte(index)]
share.LOG.Debug("--------机器人听牌,%v", this.tilesTing)
} else { //没有听牌,需要分析打出哪张牌最好
this.TileType = AnaTileType(this.hand.tilesIndex, 0, 0)
//检查应该出哪张牌
index = PlayByTileType(this.hand.tilesIndex, this.TileType)
}
b := byte(index)
va := IndexToValue(b) //将出牌的索引转换成面值
flag := false
//遍历手牌,找到那个出牌的id
for id, v := range this.hand.tiles {
if v == va {
playId = id
flag = true
break
}
}
if !flag {//如果上述步骤都没有找到出牌,则打出刚摸得牌,或者打出手牌中最大牌值的牌
if len(ids) ==0 {//如果没有摸排,打出手牌中牌值最大的牌
maxValue:=byte(0)
maxId := byte(0)
for id,v := range this.hand.tiles {
if v>maxValue {
maxId = id
}
}
playId = maxId
}else {
playId = ids[0]
}
}
}
return
}
检查听牌:
//检查听啤
/**
params:
tileIndexs:电脑手牌索引,已经将摸得牌放入其中的
returns:
[]byte:返回可以胡牌的牌值
*/
func CheckTing(tileIndexs []byte) map[byte][]byte {
//key-打出去的牌索引,value:可以听得牌切片
tingM := make(map[byte][]byte, 0)
for i, v := range tileIndexs {
if v != 0 {
tileIndexs[i]--
ts := make([]byte, 0)
//share.LOG.Debug("------checkTing--i:%d",i)
//fmt.Println("i:",i)
//遍历所有牌型(将每一个牌型加入到手牌中,然后判断是否胡牌)
for j := byte(0); j < 34; j++ {
//去掉除了万牌和字牌的其他牌
if j == 9 {
j = 27
}
if byte(i) == j { //过滤掉刚打出去的牌
continue
}
tileIndexs[j]++
//share.LOG.Debug("------*(*(**(**&**(**(**:%d",j)
//fmt.Println("j:",j)
isHu := AnalyseTilesIndex(tileIndexs, mahjongTable) //判断是否胡牌
if isHu { //如果胡牌,将当前牌值,添加到听啤切片中
ts = append(ts, IndexToValue(j))
}
tileIndexs[j]--
}
if len(ts) > 0 { //如果听啤切片长度不为0,即有听啤,将听牌切片添加到map中,
tingM[byte(i)] = ts
}
tileIndexs[i]++
}
}
return tingM
}
根据分析的牌型出牌:
//根据TileType选择出牌,返回出牌索引
func PlayByTileType(tilesIndexs []byte, tt *TileType) int {
share.LOG.Debug("顺子:%v,将:%d,刻子:%v,34:%v,13:%v,单牌:%v", tt.Array_S, tt.Array_j, tt.Array_k, tt.Array_34, tt.Array_1_3, tt.Array_d)
//判断单牌是否存在
if len(tt.Array_d) > 0 {
return int(tt.Array_d[len(tt.Array_d)-1])
}
//如果单牌打完,可以打出 13 类型牌
if len(tt.Array_1_3) > 0 {
return int(tt.Array_1_3[len(tt.Array_1_3)-1])
}
//如果13牌打完,可以打出23
if len(tt.Array_34) > 0 {
return int(tt.Array_34[len(tt.Array_34)-1])
}
//如果23打完,可以打将牌
if len(tt.Array_j) > 0 {
return int(tt.Array_j[len(tt.Array_j)-1])
}
//如果上述牌都打完,则可以选择最大的牌打
i := len(tilesIndexs) - 1
for ; i >= 0; i-- {
if tilesIndexs[i] != 0 {
break
}
}
return i
}
接下来是对事件的处理:
//event事件分析处理,主要是吃 碰 杠,胡牌事件直接胡,不需要处理
/*
左吃 0x01
中吃 0x02
右吃 0x04
碰 0x08
杠 0x10
思路:
1.麻将胡牌方式公式:m*AAA + n*ABC +AA 基本上满足(特殊的牌型不是很多,相比于普通的牌型来说)
2.机器人操作有个原则,首先尽量形成顺子,对于事件来说,分析每一个事件,然后假设事件成功触发,分析触发后的牌型,要使得单个牌型变少,或者变好。单个牌型指的是1 3、78 或者单牌5,当然78这种牌要比13容易胡,所以最后在
选择上要根据这个方面来考虑。
*/
/**
params
mask:事件掩码
tileValues 触发事件那张牌得牌值
*/
func (this *ErMahjong_AI) AiEventLogic(mask int32, tileValues []byte) int {
value := tileValues[0] //触发事件牌的面值
index := ValueToIndex(value) //触发事件牌的索引
var handIndexs []byte
handIndexs = append(handIndexs, this.hand.tilesIndex...)
resultAn := make(map[byte]*TileType, 0) //每种事件牌型判断结果 key值为事件掩码,value为TileTYpe类型
i := byte(3)
iMax := byte(8)
if this.isTing { //如果听牌了,只需要检测是否触发了杠牌事件
iMax = byte(4)
}
for ; i < iMax; i++ {
if mask&(int32(0x80)>>i) > 0 { //杠 碰 右吃 中吃 左吃
switch i {
case 3: //杠
share.LOG.Debug("杠事件,杠牌值:%d", value)
handIndexs[index] = 0
//判断牌型
tt := AnaTileType(handIndexs, 0x10, value)
handIndexs[index] = 3
resultAn[0x10] = tt
case 4: //碰
share.LOG.Debug("碰事件,碰牌值:%d", value)
handIndexs[index] = 0
tt := AnaTileType(handIndexs, 0x08, value)
handIndexs[index] = 2
resultAn[0x08] = tt
case 5: //右吃
share.LOG.Debug("右吃事件,右吃牌值:%d", value)
handIndexs[index] --
handIndexs[index-1] --
handIndexs[index-2]--
tt := AnaTileType(handIndexs, 0x04, value)
handIndexs[index] ++
handIndexs[index-1]++
handIndexs[index-2]++
resultAn[0x04] = tt
case 6: //中吃
share.LOG.Debug("中吃事件,中吃牌值:%d", value)
handIndexs[index] --
handIndexs[index-1] --
handIndexs[index+1]--
tt := AnaTileType(handIndexs, 0x02, value)
handIndexs[index] ++
handIndexs[index-1] ++
handIndexs[index+1]++
resultAn[0x02] = tt
case 7: //左吃
share.LOG.Debug("左吃事件,左吃牌值:%d", value)
handIndexs[index] --
handIndexs[index+1] --
handIndexs[index+2]--
tt := AnaTileType(handIndexs, 0x01, value)
handIndexs[index] ++
handIndexs[index+1] ++
handIndexs[index+2]++
resultAn[0x01] = tt
}
}
}
return int(AnalysisEventTileTypeMap(resultAn, this.TileType))
}
//分析触发事件形成的牌型TileType
/**
params
tts: 触发事件形成的牌型map集合
lastTileType :触发事件之前的牌型
returns
byte:返回可以出发事件的掩码
*/
func AnalysisEventTileTypeMap(tts map[byte]*TileType, lastTileType *TileType) byte {
//遍历每个事件触发后的牌型,分析,找出一个最好的牌型
/**
最好的牌型其实就是 单牌为0 和23、13类的牌型为0,
所以最终比较结果还是以这三个为准
这三个标准中,要以单牌、13、23的优先级进行比较可以给这三个值加一个权值,比如:0.5,0.3,0.2
最终的计算方式为: 0x5*单牌个数 + 0.3*13个数 + 0.2*23个数
结果越小牌型越好
*/
//事件之前牌型加权计算值
lastQ := 0.5*float32(len(lastTileType.Array_d)) + 0.3*float32(len(lastTileType.Array_1_3)) + 0.2*float32(len(lastTileType.Array_34))
share.LOG.Debug("出牌权值:%d", lastQ)
//最终相应事件掩码
resultMask := byte(0x00)
//计算事件牌型的加权值
for mask, v := range tts {
nowQ := 0.5*float32(len(v.Array_d)) + 0.3*float32(len(v.Array_1_3)) + 0.2*float32(len(v.Array_34))
share.LOG.Debug("当前事件权值:%d,上一个事件权值:%d", nowQ, lastQ)
if nowQ <= lastQ {
resultMask = mask
lastQ = nowQ
}
}
share.LOG.Debug("----------ailogic:是否触发事件:%d",resultMask)
return resultMask
}
下面是一下工具:
//将手牌的tile_num转换成索引切片
func mapToIndex(tile_num map[byte]int) []byte {
tiles := make([]byte, 0)
for value, num := range tile_num {
for i := 0; i < num; i++ {
tiles = append(tiles, value)
}
}
return ToIndexArray(tiles)
}
//索引转面值切片
func IndexToValues(indexs []byte) ([]byte) {
res := make([]byte, 0)
for i := byte(0); i < byte(len(indexs)); i++ {
v := indexs[i]
if v == 0 {
continue
}
for j := byte(0); j < v; j++ {
if i < 27 {
res = append(res, (i/9)<<4|(i%9+1))
} else {
res = append(res, byte(0x30|(i-27+1)))
}
}
}
return res
}
//索引转牌面
func IndexToValue(index byte) (byte) {
if index < 27 {
var color byte = index / 9
var color2 byte = color << 4
var val byte = index%9 + 1
var ret = color2 | val
return ret
} else {
return byte(0x30 | (index - 27 + 1))
}
}
//牌面转索引
func ValueToIndex(tile byte) (byte) {
return ((tile&0xF0)>>4)*9 + (tile & 0x0F) - 1
}
//牌值数组转索引切片
func ToIndexArray(array []byte) ([]byte) {
var indexArray [34]byte
for _, v := range array {
i := ((v&0xF0)>>4)*9 + (v & 0x0F) - 1
indexArray[i] ++
}
return indexArray[:]
}
//根据索引,查表判断是否胡牌
/**
params:
tileIndexs: 要判断的牌的索引
table;胡牌麻将表
*/
func AnalyseTilesIndex(tileIndexs []byte, table map[uint32][]uint32) (bool) {
pos := make([]int, 14)
key := CalCulateKey(tileIndexs, pos)
_, found := table[key]
return found
}
//计算牌型key
func CalCulateKey(tiles []byte, pos []int) uint32 {
p := -1
var x uint32 = 0
pos_p := 0
b := false
for i := 0; i < 3; i++ {
for j := 0; j < 9; j++ {
if tiles[i*9+j] == 0 {
if b {
b = false
x |= 0x1 << uint(p)
p++
}
} else {
p++
b = true
pos[pos_p] = i*9 + j
pos_p ++
switch tiles[i*9+j] {
case 2:
x |= 0x3 << uint(p)
p += 2
case 3:
x |= 0xF << uint(p)
p += 4
case 4:
x |= 0x3F << uint(p)
p += 6
}
}
}
if b {
b = false
x |= 0x1 << uint(p)
p++
}
}
for i := 27; i <= 33; i ++ {
if tiles[i] > 0 {
p ++
pos[pos_p] = i
pos_p ++
switch tiles[i] {
case 2:
x |= 0x3 << uint(p)
p += 2
case 3:
x |= 0xF << uint(p)
p += 4
case 4:
x |= 0x3F << uint(p)
p += 6
}
x |= 0x1 << uint(p)
p ++
}
}
return x
}
//将麻将数组转换成汉字
func MahjongSToChinese(tiles []byte) string {
str := ""
for _, v := range tiles {
cbValue := int(v & 0x0F)
cbColor := int((v & 0xF0) >> 4)
if cbColor < 3 {
str += " " + fmt.Sprintf("%s%s", ValueToChinese(cbValue), ColorToChinese(cbColor))
} else {
str += " " + HonorToChinese(cbValue)
}
}
return str
}
//将单个麻将牌转换成汉子
func MahjongToChinese(tile byte) string {
cbValue := int(tile & 0x0F)
cbColor := int((tile & 0xF0) >> 4)
if cbColor < 3 {
return fmt.Sprintf("%s%s ", ValueToChinese(cbValue), ColorToChinese(cbColor))
} else {
return HonorToChinese(cbValue)
}
}
func ValueToChinese(v int) string {
switch v {
case 1:
return "一"
case 2:
return "二"
case 3:
return "三"
case 4:
return "四"
case 5:
return "五"
case 6:
return "六"
case 7:
return "七"
case 8:
return "八"
case 9:
return "九"
}
return "错"
}
func ColorToChinese(c int) string {
switch c {
case 0:
return "万"
case 1:
return "条"
case 2:
return "筒"
}
return "错"
}
func HonorToChinese(h int) string {
switch h {
case 1:
return "东"
case 2:
return "南"
case 3:
return "西"
case 4:
return "北"
case 5:
return "中"
case 6:
return "发"
case 7:
return "白"
}
return "错"
}
经过测试ai对人类的比赛中,ai胜率基本为65以上。
以上的算法中,并没有加入对桌面上已经打出的牌的分析,接下来我们可以对桌面上已经打出的牌进行分析,比如分析听牌时选择听剩余比较多的那张。这是的机器人的难度为高,如果还想加强难度,可以在游戏服务器逻辑处对发牌进行控制,可以控制玩家输赢(当然这种就比较贱了,某t经常会这么干)。