169 lines
5.3 KiB
Go
169 lines
5.3 KiB
Go
|
|
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),
|
|||
|
|
}
|
|||
|
|
}
|