package ai import ( "nl-game-api-gin/internal/config" "nl-game-api-gin/internal/gamecore/ddz" "nl-game-api-gin/internal/gamecore/xiangqi" "nl-game-api-gin/internal/model" "nl-game-api-gin/internal/service" ) // Provider 提供方标识 const ( ProviderSpark = "spark" // 讯飞星火 Lite ProviderDeepSeek = "deepseek" // DeepSeek ProviderRule = "rule" // 本地规则 AI ) // Difficulty 难度标识 const ( DiffEasy = "easy" // 简单 DiffMedium = "medium" // 中等 DiffHard = "hard" // 困难 ) // PlayContext 斗地主出牌决策上下文:身份与各家剩牌情况 // 平民 AI 依赖这些信息与另一位平民打配合(不压队友、喂牌、封锁地主) type PlayContext struct { IsLandlord bool // 自己是否地主 LastIsPartner bool // 当前要压的牌是否队友(另一位平民)所出 PartnerCards int // 队友剩牌数(自己是地主时为 0) LandlordCards int // 地主剩牌数 OppMin int // 敌方最少剩牌数(地主视角=两平民的最小值;平民视角=地主剩牌) } // DdzAI 斗地主 AI 决策接口 type DdzAI interface { // Name AI 显示名称(房间座位与聊天展示) Name() string // DecideBid 叫地主决策:返回是否叫地主 + 一句台词 DecideBid(hand []int) (bool, string) // DecideRob 抢地主决策:返回是否抢 + 一句台词(抢一次倍数×2) DecideRob(hand []int) (bool, string) // DecidePlay 出牌决策:last 为需要压的牌(nil=自由出牌) // 返回出的牌(nil=过牌)+ 一句台词 DecidePlay(hand []int, last *ddz.Combo, ctx PlayContext) ([]int, string) } // ChessAI 中国象棋 AI 决策接口 type ChessAI interface { // Name AI 显示名称 Name() string // DecideMove 走子决策:给定棋盘与执子方,返回着法 + 一句台词 DecideMove(b *xiangqi.Board, side int) (xiangqi.Move, string) } // GlobalProvider 全站统一的对战 AI 提供方(后台「AI 设置」页配置,玩家前台只选难度) // 未配置或值非法时回退本地规则 AI func GlobalProvider() string { p := service.GetConfig(model.ConfKeyAIProvider, ProviderRule) if p != ProviderSpark && p != ProviderDeepSeek { return ProviderRule } return p } // providerName 提供方的中文显示名 func providerName(provider string) string { switch provider { case ProviderSpark: return "星火AI" case ProviderDeepSeek: return "DeepSeekAI" default: return "电脑AI" } } // effectiveConf 计算提供方的生效配置:后台(数据库)非空项覆盖 config.yaml // 这样管理员在后台改完 Key 立即生效,无需重启服务 func effectiveConf(provider string) config.LLMConf { var base config.LLMConf var kKey, kBase, kModel string switch provider { case ProviderSpark: base, kKey, kBase, kModel = config.C.AI.Spark, model.ConfKeyAISparkKey, model.ConfKeyAISparkBase, model.ConfKeyAISparkModel case ProviderDeepSeek: base, kKey, kBase, kModel = config.C.AI.DeepSeek, model.ConfKeyAIDeepSeekKey, model.ConfKeyAIDeepSeekBase, model.ConfKeyAIDeepSeekModel default: return config.LLMConf{} } if v := service.GetConfig(kKey, ""); v != "" { base.APIKey = v } if v := service.GetConfig(kBase, ""); v != "" { base.BaseURL = v } if v := service.GetConfig(kModel, ""); v != "" { base.Model = v } return base } // clientFor 按提供方构造 LLM 客户端(未配置 Key 返回 nil) func clientFor(provider string) *LLMClient { switch provider { case ProviderSpark, ProviderDeepSeek: return newLLMClient(effectiveConf(provider)) default: return nil } } // ClientForTest 暴露给后台连通性测试使用的客户端构造(未配置 Key 返回 nil) func ClientForTest(provider string) *LLMClient { return clientFor(provider) } // temperatureFor 难度对应的采样温度:难度越低随机性越大 func temperatureFor(difficulty string) float64 { switch difficulty { case DiffEasy: return 1.2 case DiffHard: return 0.3 default: return 0.7 } } // styleFor 难度对应的提示词人设(影响 LLM 决策风格) func styleFor(difficulty string) string { switch difficulty { case DiffEasy: return "你是一位刚学会玩的新手,水平一般,偶尔会犯错,说话呆萌可爱。" case DiffHard: return "你是一位身经百战的顶尖高手,每一步都追求最优解,说话简短犀利、气场十足。" default: return "你是一位经验不错的业余玩家,打法稳健,说话轻松幽默。" } } // NewDdzAI 斗地主 AI 工厂:按「提供方 × 难度」组装实例 // LLM 提供方未配置 Key 时自动降级为规则 AI func NewDdzAI(provider, difficulty string) DdzAI { rule := &ruleDdzAI{difficulty: difficulty, name: providerName(ProviderRule)} client := clientFor(provider) if client == nil { return rule } // LLM 实例持有规则 AI 作为兜底 return &llmDdzAI{ client: client, difficulty: difficulty, fallback: rule, name: providerName(provider), } } // NewChessAI 中国象棋 AI 工厂:按「提供方 × 难度」组装实例 func NewChessAI(provider, difficulty string) ChessAI { rule := &ruleChessAI{difficulty: difficulty, name: providerName(ProviderRule)} client := clientFor(provider) if client == nil { return rule } return &llmChessAI{ client: client, difficulty: difficulty, fallback: rule, name: providerName(provider), } }