四川麻将判断胡牌(非递归),找到要听的牌

四川麻将胡牌规则,参考腾讯麻将“血流成河”规则

image

详细代码如下:

package main

import (
	"bytes"
	"fmt"
	"strings"
)

func main() {
	var (
		tile = []string{
			"56756744422222m",     // 超过4张牌,记为4张
			"675456234m32155p4s",  // 没有缺一门,可以打一张后听牌
			"112233m11224455p",    // 七对
			"11223355667799s",     // 清七对
			"1122335566s3333m",    // 龙七对
			"11m111222555666p",    // 碰碰胡
			"11122255566677p",     // 清碰
			"567567444222m3m3m",   // 清一色
			"1111333345667899m",   // 两个杠的牌
			"111133335555777799m", // 清十八罗汉
			"22244455667m3p3p",    // 计算听牌
			"22244455667m43p3p",   // 打一张,计算听哪些牌
			"2224445666789m",      // 计算听牌
			"12224445666789m",     // 打一张,计算听哪些牌
		}
	)

	for _, t := range tile {
		tt := StrToTile(&t) // 字符串转为tile34,更新字符串
		fmt.Printf("当前牌: [%s]\n", t)
		fmt.Println(MahjongSolution(tt))
		fmt.Println("-------------------------------")
	}
}

const TileMax = 34

func StrToTile(s *string) []int {
	tile := make([]int, TileMax)
	if s == nil || len(*s) < 2 {
		*s = "" // 至少2个字符1张牌,不满足则置为空
		return tile
	}

	var (
		ok = false
		i  = len(*s) - 1
		si = -1
		ni int
	)
	for ; i >= 0; i-- {
		switch c := (*s)[i]; c {
		case 'm':
			si = 0 // [0,8]m = [1,9]万
		case 'p':
			si = 9 // [9,18]p = [1,9]筒
		case 's':
			si = 18 // [18,26]s = [1,9]条
		case 'z':
			si = 27 // [27,33]z = [1,7] = 东,南,西,北,白,发,中
		case '1', '2', '3', '4', '5', '6', '7', '8', '9':
			if si >= 0 {
				if ni = int(c-'1') + si; ni < TileMax && tile[ni] < 4 {
					tile[ni]++
					ok = true
				}
			}
		}
	}

	if ok {
		*s = TileToStr(tile) // 有牌时序列化并排序
	} else {
		*s = "" // 无牌时置为空
	}
	return tile
}

func TileToStr(tile []int) string {
	var (
		i, j int
		lt   = len(tile) // 支持lt<34的不完全入参
		tmp  strings.Builder
	)

	ok := false // 拼接万牌
	for i = 0; i < 9 && i < lt; i++ {
		for j = tile[i]; j > 0; j-- {
			tmp.WriteByte(byte(i + '1'))
			ok = true
		}
	}
	if ok {
		tmp.WriteByte('m')
	}

	ok = false // 拼接筒牌
	for i = 9; i < 18 && i < lt; i++ {
		for j = tile[i]; j > 0; j-- {
			tmp.WriteByte(byte(i - 9 + '1'))
			ok = true
		}
	}
	if ok {
		tmp.WriteByte('p')
	}

	ok = false // 拼接条牌
	for i = 18; i < 27 && i < lt; i++ {
		for j = tile[i]; j > 0; j-- {
			tmp.WriteByte(byte(i - 18 + '1'))
			ok = true
		}
	}
	if ok {
		tmp.WriteByte('s')
	}

	ok = false // 拼接字牌
	for i = 27; i < TileMax && i < lt; i++ {
		for j = tile[i]; j > 0; j-- {
			tmp.WriteByte(byte(i - 27 + '1'))
			ok = true
		}
	}
	if ok {
		tmp.WriteByte('z')
	}
	return tmp.String()
}

func NumToTile(v int) []byte {
	if v < 0 || v >= TileMax {
		return []byte{'0', '-'} // 不合法数据
	}
	if v >= 27 {
		return []byte{byte(v - 27 + '1'), 'z'}
	}
	if v >= 18 {
		return []byte{byte(v - 18 + '1'), 's'}
	}
	if v >= 9 {
		return []byte{byte(v - 9 + '1'), 'p'}
	}
	return []byte{byte(v + '1'), 'm'}
}

type MahjongResult struct {
	IsSame bool // 是否清一色
	Type   int  // 牌类型
	Jiang  int  // 将牌

	NumKe     int   // 刻子数量
	NumShun   int   // 顺子数量
	NumGang   int   // 杠数量
	ArrayKe   []int // 刻子数组
	ArrayShun []int // 顺子数组
	ArrayGang []int // 杠数组
}

func (mr *MahjongResult) String() string {
	b := bytes.NewBufferString("{")
	nv := NumToTile(mr.Jiang)
	if nv[1] == '-' {
		return "" // 将不合法
	}
	if mr.IsSame {
		b.WriteString("清一色,")
	}
	switch mr.Type {
	case 1:
		b.WriteString("七对}")
		return b.String()
	case 2:
		b.WriteString("龙七对}")
		return b.String()
	}

	b.WriteByte(nv[0])
	b.WriteByte(nv[0])
	b.WriteByte(nv[1])
	b.WriteByte(',')
	for _, v := range mr.ArrayKe {
		nv = NumToTile(v)
		b.WriteByte(nv[0])
		b.WriteByte(nv[0])
		b.WriteByte(nv[0])
		b.WriteByte(nv[1])
		b.WriteByte(',')
	}
	for _, v := range mr.ArrayShun {
		nv = NumToTile(v)
		b.WriteByte(nv[0])
		b.WriteByte(nv[0] + 1)
		b.WriteByte(nv[0] + 2)
		b.WriteByte(nv[1])
		b.WriteByte(',')
	}
	for _, v := range mr.ArrayGang {
		nv = NumToTile(v)
		b.WriteByte(nv[0])
		b.WriteByte(nv[0])
		b.WriteByte(nv[0])
		b.WriteByte(nv[0])
		b.WriteByte(nv[1])
		b.WriteByte(',')
	}
	// 移除最后1个','号,并将结果括起来
	b.Truncate(b.Len() - 1)
	b.WriteByte('}')
	return b.String()
}

func MahjongWin(tile []int) (res []*MahjongResult) {
	color, cnt := 0, [5]int{}
	for i, v := range tile {
		if v <= 0 || v > 4 {
			tile[i] = 0 // 纠正数量错误的牌
			continue
		}

		nv := NumToTile(i)
		cnt[v]++    // 记录对应数量牌的个数
		cnt[0] += v // 记录牌总数

		switch nv[1] {
		case 's':
			color |= 4 // 存在条
		case 'p':
			color |= 2 // 存在筒
		case 'm':
			color |= 1 // 存在万
		case 'z', '-':
			return nil // 四川麻将没有字牌
		}
	}

	isSame := color == 1 || color == 2 || color == 4 // 清一色
	if cnt[0] == 14 {
		if cnt[2] == 7 {
			return []*MahjongResult{{
				IsSame: isSame,
				Type:   1, // 七对
			}}
		}

		if cnt[2] == 5 && cnt[4] == 1 {
			// 注意,4张的牌不能杠出和碰出
			return []*MahjongResult{{
				IsSame: isSame,
				Type:   2, // 龙七对
			}}
		}
	}

	var (
		appNum = []func(tp []int, k, s, g *[]int){
			func(tp []int, k, s, g *[]int) {
				for j := 0; j < TileMax; j++ {
					if tp[j] >= 3 {
						tp[j] -= 3 // 取刻子
						*k = append(*k, j)
					}
				}
			},
			func(tp []int, k, s, g *[]int) {
				var a, b, c int
				for a = 0; a < 3; a++ {
					for b = 0; b < 7; {
						c = 9*a + b
						if tp[c] >= 1 && tp[c+1] >= 1 && tp[c+2] >= 1 {
							tp[c]--
							tp[c+1]--
							tp[c+2]-- // 取顺子
							*s = append(*s, c)
						} else {
							b++
						}
					}
				}
			},
			func(tp []int, k, s, g *[]int) {
				for j := 0; j < TileMax; j++ {
					if tp[j] >= 4 {
						tp[j] = 0 // 取杠牌
						*g = append(*g, j)
					}
				}
			},
		}

		tp = make([]int, len(tile))
		mp = make(map[string]struct{})
	)
	for i := 0; i < TileMax; i++ {
		if tile[i] < 2 {
			continue // 跳过不能做雀头的牌
		}

		for _, an := range [][]int{
			{0, 1},    // 刻子,顺子
			{1, 0},    // 顺子,刻子
			{2, 0, 1}, // 杠,刻子,顺子
			{2, 1, 0}, // 杠,顺子,刻子
		} {
			copy(tp, tile)
			tp[i] -= 2 // 取雀头

			var keNum, shunNum, gangNum []int
			for _, anv := range an {
				appNum[anv](tp, &keNum, &shunNum, &gangNum)
			}

			ok := true
			for _, vt := range tp {
				if vt != 0 {
					ok = false
					break
				}
			}
			if ok { // 胡牌,记录牌型,结果去重
				key := fmt.Sprintf("%d%v%v%v", i, keNum, shunNum, gangNum)
				if _, ok = mp[key]; !ok {
					res = append(res, &MahjongResult{
						IsSame:    isSame,
						Type:      3,
						NumKe:     len(keNum),
						NumShun:   len(shunNum),
						NumGang:   len(gangNum),
						Jiang:     i,
						ArrayKe:   keNum,
						ArrayShun: shunNum,
						ArrayGang: gangNum,
					})
					mp[key] = struct{}{}
				}
			}
		}
	}
	return
}

func MahjongSolution(tile []int) string {
	status := MahjongWin(tile)
	if len(status) > 0 {
		return fmt.Sprint(status)
	}

	ting := func() (res []string) {
		for i, v := range tile {
			if v < 4 {
				tile[i]++
				status = MahjongWin(tile)
				tile[i]--
				if len(status) > 0 {
					nv := NumToTile(i) // 假设得到这张牌可以胡牌则听这张牌
					res = append(res, fmt.Sprintf("%v听%c%c", status, nv[0], nv[1]))
				}
			}
		}
		return
	}
	resp := ting() // 当前听哪些牌

	for i, v := range tile {
		if v > 0 {
			nv := NumToTile(i)

			tile[i]--
			status = MahjongWin(tile)
			if len(status) > 0 { // 假设打出这张牌,直接胡牌(正常打牌不会有这种情况)
				resp = append(resp, fmt.Sprintf("打%c%c, %v", nv[0], nv[1], status))
			}
			res := ting()
			tile[i]++

			for _, rv := range res { // 假设打出这张牌,能听哪些牌
				resp = append(resp, fmt.Sprintf("打%c%c, %s", nv[0], nv[1], rv))
			}
		}
	}
	return strings.Join(resp, "\n")
}

结果如下:

当前牌: [2222444556677m]
[{清一色,44m,222m,234m,567m,567m} {清一色,77m,222m,234m,456m,456m}]听3m
打4m, [{清一色,44m,567m,567m,2222m} {清一色,77m,456m,456m,2222m}]
打7m, [{清一色,44m,456m,567m,2222m}]
-------------------------------
当前牌: [234455667m12355p4s]
打4s, [{55p,234m,456m,567m,123p}]
-------------------------------
当前牌: [112233m11224455p]
[{七对}]
-------------------------------
当前牌: [11223355667799s]
[{清一色,七对}]
-------------------------------
当前牌: [3333m1122335566s]
[{龙七对}]
-------------------------------
当前牌: [11m111222555666p]
[{11m,111p,222p,555p,666p}]
-------------------------------
当前牌: [11122255566677p]
[{清一色,77p,111p,222p,555p,666p}]
-------------------------------
当前牌: [22233444556677m]
[{清一色,33m,222m,444m,567m,567m}]
-------------------------------
当前牌: [1111333345667899m]
[{清一色,99m,456m,678m,1111m,3333m}]
-------------------------------
当前牌: [111133335555777799m]
[{清一色,99m,1111m,3333m,5555m,7777m}]
-------------------------------
当前牌: [22244455667m33p]
[{33p,222m,444m,456m,567m}]听4m
[{33p,222m,444m,567m,567m}]听7m
[{44m,222m,333p,456m,567m}]听3p
-------------------------------
当前牌: [22244455667m334p]
打3p, [{44m,222m,456m,567m,234p}]听2p
打3p, [{44m,222m,456m,567m,345p}]听5p
打4p, [{33p,222m,444m,456m,567m}]听4m
打4p, [{33p,222m,444m,567m,567m}]听7m
打4p, [{44m,222m,333p,456m,567m}]听3p
-------------------------------
当前牌: [2224445666789m]
[{清一色,44m,222m,666m,345m,789m}]听3m
[{清一色,66m,222m,444m,456m,789m}]听4m
[{清一色,55m,222m,444m,666m,789m}]听5m
[{清一色,44m,222m,666m,456m,789m}]听6m
[{清一色,66m,222m,444m,567m,789m}]听7m
-------------------------------
当前牌: [12224445666789m]
打1m, [{清一色,44m,222m,666m,345m,789m}]听3m
打1m, [{清一色,66m,222m,444m,456m,789m}]听4m
打1m, [{清一色,55m,222m,444m,666m,789m}]听5m
打1m, [{清一色,44m,222m,666m,456m,789m}]听6m
打1m, [{清一色,66m,222m,444m,567m,789m}]听7m
打5m, [{清一色,11m,222m,444m,666m,789m}]听1m
打5m, [{清一色,22m,444m,666m,123m,789m}]听3m
-------------------------------
posted @ 2023-03-04 21:01  janbar  阅读(255)  评论(0编辑  收藏  举报