Files
nl-game-api/internal/ai/chess_llm.go

89 lines
2.8 KiB
Go
Raw Permalink Normal View History

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