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] }