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

169 lines
5.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 (
"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),
}
}