支持多人数对局与 AI 补位:玩法声明人数范围、匹配分队列与超时补 AI、房间玩家名册
This commit is contained in:
@@ -37,7 +37,11 @@ type state struct {
|
||||
}
|
||||
|
||||
// Game 实现 game.Game 接口。
|
||||
type Game struct{}
|
||||
type Game struct {
|
||||
aiPlanIdx int // AI 计划落点(配合 aiHasPlan 使用)
|
||||
aiHasPlan bool // AI 是否已有落子计划
|
||||
aiThinkAt int64 // AI 落子时刻,模拟思考耗时
|
||||
}
|
||||
|
||||
// New 创建玩法实例:每局一个,状态互不影响。
|
||||
func New() *Game { return &Game{} }
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user