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