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),
|
||
}
|
||
}
|