package ai import ( "context" "fmt" "strings" "time" "nl-game-api-gin/internal/gamecore/ddz" ) // llmDdzAI 大模型斗地主 AI:把候选出牌列表交给 LLM 挑选,非法/超时由规则 AI 兜底 type llmDdzAI struct { client *LLMClient // LLM 客户端 difficulty string // 难度(决定提示词人设与温度) fallback DdzAI // 规则 AI 兜底 name string // 显示名 } // Name AI 显示名 func (a *llmDdzAI) Name() string { return a.name } // DecideBid 叫地主:把手牌交给 LLM 判断叫不叫 func (a *llmDdzAI) DecideBid(hand []int) (bool, string) { ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) defer cancel() system := styleFor(a.difficulty) + "你在玩斗地主,现在是叫地主阶段。" + "只输出 JSON:{\"choice\":0或1,\"say\":\"一句台词\"},0=不叫,1=叫地主。" user := fmt.Sprintf("你的手牌:%s\n手牌强度参考分:%d(5分以上算强牌)\n请决定是否叫地主。", ddz.CardsName(hand), ddz.HandStrength(hand)) // 带返回校验与重试的决策调用(传输层自动重试 + 输出非法自动纠错重问) if d, err := a.client.ChatDecision(ctx, system, user, temperatureFor(a.difficulty), 1); err == nil { return d.Choice == 1, d.Say } // LLM 多次重试仍失败:走规则兜底 return a.fallback.DecideBid(hand) } // DecideRob 抢地主:直接复用规则 AI 的牌力判断(避免为一次表态多花一轮 LLM 往返) func (a *llmDdzAI) DecideRob(hand []int) (bool, string) { return a.fallback.DecideRob(hand) } // DecidePlay 出牌:服务端生成全部合法候选,LLM 只需选编号(天然保证合法性) // 提示词中带入身份与队友信息:平民身份的 LLM 会被引导与另一位平民配合 func (a *llmDdzAI) DecidePlay(hand []int, last *ddz.Combo, pctx PlayContext) ([]int, string) { moves := ddz.GenMoves(hand, last) if len(moves) == 0 { return nil, "要不起。" } // 候选清单:0=过牌(跟牌时才有),1..n=具体出牌 options := []string{} canPass := last != nil if canPass { options = append(options, "0: 过牌(不出)") } offset := len(options) // 候选太多时截取(保留最小若干手 + 最大一手 + 炸弹)避免提示词过长 shown := moves if len(shown) > 12 { shown = append(append([][]int{}, moves[:10]...), moves[len(moves)-2:]...) } for i, mv := range shown { combo := ddz.Parse(mv) options = append(options, fmt.Sprintf("%d: %s", i+offset, combo.Desc())) } lastDesc := "你是本轮首家,自由出牌" if last != nil { lastDesc = "上家出了「" + last.Desc() + "」,你需要压过它" if pctx.LastIsPartner { lastDesc += "(这是你队友出的牌)" } else if !pctx.IsLandlord { lastDesc += "(这是地主出的牌)" } } // 身份与配合原则 role := "你是地主,1 打 2,两位平民会联手对付你。" if !pctx.IsLandlord { role = fmt.Sprintf( "你是平民,与另一位平民组队对抗地主(地主剩 %d 张牌,队友剩 %d 张牌)。"+ "配合原则:队友的牌尽量不压(除非你能直接出完);队友只剩 1-2 张时优先喂出最小的单张或对子;"+ "炸弹和大牌留着对付地主,不要浪费在队友身上。", pctx.LandlordCards, pctx.PartnerCards) } system := styleFor(a.difficulty) + "你在玩斗地主。" + role + "只输出 JSON:{\"choice\":候选编号,\"say\":\"一句台词\"},不要输出其他内容。" user := fmt.Sprintf("你的手牌:%s\n%s\n候选操作:\n%s\n请选择一个候选编号。", ddz.CardsName(hand), lastDesc, strings.Join(options, "\n")) ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) defer cancel() // 带返回校验与重试的决策调用(choice 越界或非 JSON 输出会自动纠错重问) if d, err := a.client.ChatDecision(ctx, system, user, temperatureFor(a.difficulty), len(options)-1); err == nil { if canPass && d.Choice == 0 { return nil, d.Say } idx := d.Choice - offset if idx >= 0 && idx < len(shown) { return shown[idx], d.Say } } // LLM 多次重试仍失败:走规则兜底 return a.fallback.DecidePlay(hand, last, pctx) }