217 lines
6.3 KiB
Go
217 lines
6.3 KiB
Go
package ai
|
||
|
||
import (
|
||
"math/rand"
|
||
|
||
"nl-game-api-gin/internal/gamecore/ddz"
|
||
)
|
||
|
||
// ruleDdzAI 本地规则斗地主 AI:不依赖网络,永远可用
|
||
type ruleDdzAI struct {
|
||
difficulty string // 难度:影响叫牌阈值与出牌策略
|
||
name string // 显示名
|
||
}
|
||
|
||
// Name AI 显示名
|
||
func (a *ruleDdzAI) Name() string { return a.name }
|
||
|
||
// DecideBid 叫地主:按手牌强度阈值决定(难度越高越敢叫强牌)
|
||
func (a *ruleDdzAI) DecideBid(hand []int) (bool, string) {
|
||
strength := ddz.HandStrength(hand)
|
||
threshold := 5
|
||
switch a.difficulty {
|
||
case DiffEasy:
|
||
// 简单:随缘叫牌
|
||
threshold = 4 + rand.Intn(3)
|
||
case DiffHard:
|
||
threshold = 6
|
||
}
|
||
if strength >= threshold {
|
||
return true, "这把牌不错,我来当地主!"
|
||
}
|
||
return false, "牌太散了,不叫。"
|
||
}
|
||
|
||
// DecideRob 抢地主:抢意味着倍数翻倍,门槛比叫地主高 2 分(牌力显著强才抢)
|
||
func (a *ruleDdzAI) DecideRob(hand []int) (bool, string) {
|
||
strength := ddz.HandStrength(hand)
|
||
threshold := 7
|
||
switch a.difficulty {
|
||
case DiffEasy:
|
||
// 简单:偶尔头脑发热跟着抢
|
||
threshold = 6 + rand.Intn(3)
|
||
case DiffHard:
|
||
threshold = 8
|
||
}
|
||
if strength >= threshold {
|
||
return true, "这牌必须抢!翻倍走起!"
|
||
}
|
||
return false, "不抢,你来吧。"
|
||
}
|
||
|
||
// DecidePlay 出牌决策:中等/困难难度的平民会与另一位平民打配合
|
||
// (不压队友的牌、队友快跑完时喂单张/对子、炸弹只留给地主、封锁残局地主)
|
||
func (a *ruleDdzAI) DecidePlay(hand []int, last *ddz.Combo, ctx PlayContext) ([]int, string) {
|
||
moves := ddz.GenMoves(hand, last)
|
||
if len(moves) == 0 {
|
||
return nil, "要不起。"
|
||
}
|
||
// 任何难度:有能一手出完的牌直接打出去收工
|
||
for _, mv := range moves {
|
||
if len(mv) == len(hand) {
|
||
return mv, "我走啦!"
|
||
}
|
||
}
|
||
// 敌方剩牌很少时进入紧张状态:地主看两个平民,平民只看地主(不把队友当威胁)
|
||
danger := ctx.OppMin > 0 && ctx.OppMin <= 2
|
||
if a.difficulty == DiffEasy {
|
||
// 简单:不懂配合,30% 概率直接过牌,否则随机出非炸弹牌
|
||
if last != nil && rand.Float64() < 0.3 {
|
||
return nil, "先过一手~"
|
||
}
|
||
pick := moves[rand.Intn(len(moves))]
|
||
combo := ddz.Parse(pick)
|
||
// 新手舍不得放炸弹
|
||
if (combo.Type == ddz.ComboBomb || combo.Type == ddz.ComboRocket) && len(moves) > 1 {
|
||
pick = moves[0]
|
||
}
|
||
return pick, ""
|
||
}
|
||
// —— 平民配合(中等/困难)——
|
||
if !ctx.IsLandlord {
|
||
if last != nil && ctx.LastIsPartner {
|
||
// 要压的是队友的牌:队友快跑完或牌已够大(J 以上)时直接让牌
|
||
if ctx.PartnerCards <= 4 || last.MainRank >= 11 {
|
||
return nil, "队友你走,我不拆台~"
|
||
}
|
||
// 队友前期的小牌可以用小牌顺手接管节奏(不动炸弹、不动 2 和王)
|
||
if mv := smallestQuiet(moves, 12); mv != nil {
|
||
return mv, ""
|
||
}
|
||
return nil, "你继续~"
|
||
}
|
||
if last == nil {
|
||
// 自由出牌:优先喂牌给快跑完的队友 / 封锁只剩一两张的地主
|
||
if mv := farmerLead(moves, ctx); mv != nil {
|
||
return mv, ""
|
||
}
|
||
}
|
||
}
|
||
// —— 常规压制(对地主的牌或地主自身的策略)——
|
||
switch a.difficulty {
|
||
case DiffHard:
|
||
// 困难:敌方快出完时敢放炸弹压制;否则出能压住的最小牌,且尽量不拆炸弹
|
||
if danger {
|
||
// 有炸弹直接炸(moves 已把炸弹排在最后)
|
||
lastMove := moves[len(moves)-1]
|
||
if c := ddz.Parse(lastMove); c.Type == ddz.ComboBomb || c.Type == ddz.ComboRocket {
|
||
return lastMove, "看我王牌!"
|
||
}
|
||
// 没炸弹就出最大的非炸弹牌
|
||
return moves[len(moves)-1], "顶住!"
|
||
}
|
||
return a.pickSmart(hand, moves), ""
|
||
default:
|
||
// 中等:出能压住的最小一手(moves 已按牌力升序)
|
||
pick := moves[0]
|
||
if c := ddz.Parse(pick); (c.Type == ddz.ComboBomb || c.Type == ddz.ComboRocket) && last != nil && !danger {
|
||
// 不紧急时不放炸弹,选择过牌
|
||
return nil, "先忍一手。"
|
||
}
|
||
return pick, ""
|
||
}
|
||
}
|
||
|
||
// smallestQuiet 最小的"安静"牌:非炸弹且主牌点数不超过 maxRank(接队友牌时避免浪费火力)
|
||
func smallestQuiet(moves [][]int, maxRank int) []int {
|
||
for _, mv := range moves {
|
||
c := ddz.Parse(mv)
|
||
if c.Type == ddz.ComboBomb || c.Type == ddz.ComboRocket {
|
||
continue
|
||
}
|
||
if c.MainRank <= maxRank {
|
||
return mv
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// smallestOfType 最小的指定牌型(moves 已按牌力升序)
|
||
func smallestOfType(moves [][]int, t ddz.ComboType) []int {
|
||
for _, mv := range moves {
|
||
if c := ddz.Parse(mv); c.Type == t {
|
||
return mv
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// farmerLead 平民自由出牌时的配合选择,返回 nil 表示走常规策略:
|
||
// 队友剩 1 张喂最小单张、剩 2 张喂最小对子;地主只剩一两张时不送小单牌
|
||
func farmerLead(moves [][]int, ctx PlayContext) []int {
|
||
// 喂牌:队友只差一两张就赢,把节奏让给队友
|
||
if ctx.PartnerCards == 1 {
|
||
if mv := smallestOfType(moves, ddz.ComboSingle); mv != nil {
|
||
return mv
|
||
}
|
||
}
|
||
if ctx.PartnerCards == 2 {
|
||
if mv := smallestOfType(moves, ddz.ComboPair); mv != nil {
|
||
return mv
|
||
}
|
||
if mv := smallestOfType(moves, ddz.ComboSingle); mv != nil {
|
||
return mv
|
||
}
|
||
}
|
||
// 封锁:地主只剩 1-2 张时避免送小单牌,优先甩长牌型,否则顶最大的非炸弹牌
|
||
if ctx.LandlordCards > 0 && ctx.LandlordCards <= 2 {
|
||
for _, mv := range moves {
|
||
if len(mv) >= 3 {
|
||
if c := ddz.Parse(mv); c.Type != ddz.ComboBomb && c.Type != ddz.ComboRocket {
|
||
return mv
|
||
}
|
||
}
|
||
}
|
||
for i := len(moves) - 1; i >= 0; i-- {
|
||
if c := ddz.Parse(moves[i]); c.Type != ddz.ComboBomb && c.Type != ddz.ComboRocket {
|
||
return moves[i]
|
||
}
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// pickSmart 困难难度的挑牌:优先出"不拆散手牌结构"的最小牌
|
||
// 简化实现:跳过会拆散炸弹的牌,其余取最小
|
||
func (a *ruleDdzAI) pickSmart(hand []int, moves [][]int) []int {
|
||
// 统计手牌中构成炸弹的点数
|
||
bombRanks := map[int]bool{}
|
||
cnt := map[int]int{}
|
||
for _, c := range hand {
|
||
cnt[ddz.Rank(c)]++
|
||
}
|
||
for r, k := range cnt {
|
||
if k == 4 {
|
||
bombRanks[r] = true
|
||
}
|
||
}
|
||
for _, mv := range moves {
|
||
combo := ddz.Parse(mv)
|
||
// 完整炸弹不算拆
|
||
if combo.Type == ddz.ComboBomb || combo.Type == ddz.ComboRocket {
|
||
continue
|
||
}
|
||
breaksBomb := false
|
||
for _, c := range mv {
|
||
if bombRanks[ddz.Rank(c)] {
|
||
breaksBomb = true
|
||
break
|
||
}
|
||
}
|
||
if !breaksBomb {
|
||
return mv
|
||
}
|
||
}
|
||
return moves[0]
|
||
}
|