Files

169 lines
5.3 KiB
Go
Raw Permalink Normal View History

2026-08-14 13:17:03 +08:00
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),
}
}