package gobang // 与 gobang.go 同包,可直接访问 state 与 checkWin import ( "encoding/json" // 标准库:生成 place 动作的 payload "math/rand" // 标准库:思考时长与落点随机,避免每局一模一样 "git.lzcwy.cn/zhengchiliu/zt-online-game-backend/internal/game" ) // TickAI 为 AI 玩家生成落子动作(实现 game.AIBrain)。 // // 拟人要点:先「思考」0.6~1.5 秒再落子,绝不秒下;落点策略按 // 「能赢就赢 → 对手要赢就堵 → 贴着已有棋子下 → 中心兜底」递减, // 既有棋力又不会强到反常。 func (g *Game) TickAI(now int64, s game.State) []game.AIAction { st, ok := s.(*state) // 断言为本玩法状态 if !ok || st.Winner != 0 || len(st.Players) != 2 { // 未开局或已分胜负 return nil } turnUID := st.Players[st.Turn] // 当前该走的人 if !game.IsAIUID(turnUID) { // 不是 AI 的回合:丢弃未落子的计划 g.aiPlanIdx = 0 g.aiHasPlan = false return nil } if !g.aiHasPlan { // 还没想好:选点并开始思考 idx := g.pickMove(st, turnUID) if idx < 0 { // 无点可下(棋盘满) return nil } g.aiPlanIdx = idx g.aiHasPlan = true // 注意 now 是秒级时间戳,房间 tick 粒度也是 1 秒,因此思考时长只能按「秒」计: // 思考 1~2 秒再落子,既像人在思考,又不会让玩家等太久。 g.aiThinkAt = now + int64(1+rand.Intn(2)) return nil } if now < g.aiThinkAt { // 思考时间未到:继续等 return nil } idx := g.aiPlanIdx // 到点落子 g.aiHasPlan = false g.aiPlanIdx = 0 payload, err := json.Marshal(placePayload{Row: idx / boardSize, Col: idx % boardSize}) if err != nil { // 序列化失败:本回合不下 return nil } return []game.AIAction{{UID: turnUID, Action: game.Action{Name: "place", Payload: payload}}} } // pickMove 选落点:赢 → 堵 → 贴子 → 中心。 func (g *Game) pickMove(st *state, aiUID int64) int { aiMark := markOf(st, aiUID) // AI 执黑(1) 还是执白(2) foeMark := 3 - aiMark // 对手的棋子 // 1) 能直接连五获胜:立刻下 for i := 0; i < len(st.Board); i++ { if st.Board[i] != 0 { // 只考虑空点 continue } st.Board[i] = aiMark // 试落 win := checkWin(st.Board, i/boardSize, i%boardSize, aiMark) // 判断是否成五 st.Board[i] = 0 // 还原 if win { return i } } // 2) 对手下一手就要成五:必须堵 for i := 0; i < len(st.Board); i++ { if st.Board[i] != 0 { continue } st.Board[i] = foeMark win := checkWin(st.Board, i/boardSize, i%boardSize, foeMark) st.Board[i] = 0 if win { return i } } // 3) 贴着已有棋子下(像人一样连片),从候选里随机取一个 cands := make([]int, 0, 64) for i := 0; i < len(st.Board); i++ { if st.Board[i] != 0 { // 空点 continue } if !hasNeighbor(st.Board, i, 2) { // 两格内没有棋子则跳过 continue } cands = append(cands, i) } if len(cands) > 0 { return cands[rand.Intn(len(cands))] } // 4) 兜底:天元(棋盘中心) mid := boardSize / 2 if st.Board[mid*boardSize+mid] == 0 { return mid*boardSize + mid } for i := 0; i < len(st.Board); i++ { // 再兜底:第一个空点 if st.Board[i] == 0 { return i } } return -1 // 棋盘已满 } // markOf 返回该玩家的棋子颜色(先手=1 黑,后手=2 白)。 func markOf(st *state, uid int64) int8 { for i, p := range st.Players { if p == uid { return int8(i + 1) } } return 1 } // hasNeighbor 判断 radius 格范围内是否存在棋子(用于「贴着下」的选点)。 func hasNeighbor(board []int8, idx, radius int) bool { r0, c0 := idx/boardSize, idx%boardSize for dr := -radius; dr <= radius; dr++ { for dc := -radius; dc <= radius; dc++ { r, c := r0+dr, c0+dc if r < 0 || r >= boardSize || c < 0 || c >= boardSize { // 越界 continue } if board[r*boardSize+c] != 0 { // 有棋子 return true } } } return false }