89 lines
2.8 KiB
Go
89 lines
2.8 KiB
Go
package ai
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"nl-game-api-gin/internal/gamecore/xiangqi"
|
||
)
|
||
|
||
// llmChessAI 大模型象棋 AI:服务端先用搜索引擎给候选着法排序,
|
||
// 再按难度截取候选交给 LLM 挑选(困难=只给高分着法,简单=全量随机)
|
||
type llmChessAI struct {
|
||
client *LLMClient // LLM 客户端
|
||
difficulty string // 难度
|
||
fallback ChessAI // 规则 AI 兜底
|
||
name string // 显示名
|
||
}
|
||
|
||
// Name AI 显示名
|
||
func (a *llmChessAI) Name() string { return a.name }
|
||
|
||
// boardText 把棋盘渲染成文本(LLM 提示词用)
|
||
func boardText(b *xiangqi.Board) string {
|
||
var sb strings.Builder
|
||
sb.WriteString(" 0 1 2 3 4 5 6 7 8\n")
|
||
for r := 0; r < 10; r++ {
|
||
sb.WriteString(fmt.Sprintf("%d ", r))
|
||
for c := 0; c < 9; c++ {
|
||
p := b[r][c]
|
||
if p == 0 {
|
||
sb.WriteString(". ")
|
||
} else {
|
||
sb.WriteString(xiangqi.PieceName(p) + " ")
|
||
}
|
||
}
|
||
sb.WriteString("\n")
|
||
}
|
||
return sb.String()
|
||
}
|
||
|
||
// DecideMove 走子:候选着法按引擎评分排序后交 LLM 选择
|
||
func (a *llmChessAI) DecideMove(b *xiangqi.Board, side int) (xiangqi.Move, string) {
|
||
// 用 2 层搜索为候选排序(困难截取前6,中等前12,简单全部)
|
||
ranked := b.RankedMoves(side, 2)
|
||
if len(ranked) == 0 {
|
||
return xiangqi.Move{}, ""
|
||
}
|
||
limit := len(ranked)
|
||
switch a.difficulty {
|
||
case DiffHard:
|
||
limit = 6
|
||
case DiffMedium:
|
||
limit = 12
|
||
}
|
||
if limit > len(ranked) {
|
||
limit = len(ranked)
|
||
}
|
||
shown := ranked[:limit]
|
||
// 组装候选清单(附带吃子信息帮助 LLM 判断)
|
||
options := make([]string, 0, len(shown))
|
||
for i, m := range shown {
|
||
target := b[m.ToR][m.ToC]
|
||
capture := ""
|
||
if target != 0 {
|
||
capture = "(吃" + xiangqi.PieceName(target) + ")"
|
||
}
|
||
options = append(options, fmt.Sprintf("%d: %s从(%d,%d)走到(%d,%d)%s",
|
||
i, b.MoveDesc(m), m.FromR, m.FromC, m.ToR, m.ToC, capture))
|
||
}
|
||
sideName := "红方"
|
||
if side == xiangqi.Black {
|
||
sideName = "黑方"
|
||
}
|
||
system := styleFor(a.difficulty) + "你在下中国象棋,执" + sideName + "。" +
|
||
"只输出 JSON:{\"choice\":候选编号,\"say\":\"一句台词\"},不要输出其他内容。"
|
||
user := fmt.Sprintf("当前棋盘(行0在上为黑方,行9在下为红方):\n%s\n候选着法(已按棋力排序,越靠前越好):\n%s\n请选择一个候选编号。",
|
||
boardText(b), strings.Join(options, "\n"))
|
||
ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second)
|
||
defer cancel()
|
||
// 带返回校验与重试的决策调用(传输层自动重试 + 输出非法自动纠错重问)
|
||
if d, err := a.client.ChatDecision(ctx, system, user, temperatureFor(a.difficulty), len(shown)-1); err == nil {
|
||
return shown[d.Choice], d.Say
|
||
}
|
||
// LLM 多次重试仍失败:走规则兜底
|
||
return a.fallback.DecideMove(b, side)
|
||
}
|