Files
nl-game-api/internal/ai/ddz_rule.go
2026-08-14 13:17:03 +08:00

217 lines
6.3 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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]
}