支持多人数对局与 AI 补位:玩法声明人数范围、匹配分队列与超时补 AI、房间玩家名册
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
package snake // 与 snake.go 同包,可直接访问 state 与方向工具
|
||||
|
||||
import (
|
||||
"encoding/json" // 标准库:生成 turn 动作的 payload
|
||||
"math/rand" // 标准库:随机化候选方向,制造拟人失误
|
||||
|
||||
"git.lzcwy.cn/zhengchiliu/zt-online-game-backend/internal/game"
|
||||
)
|
||||
|
||||
// TickAI 为 AI 蛇生成转向指令(实现 game.AIBrain)。
|
||||
//
|
||||
// 拟人要点:
|
||||
// - 只在方向需要改变时才发指令(真人也不会每帧狂按方向键)
|
||||
// - 候选方向里混入随机顺序,偶尔会走"不是最优"的一步,反应不会过于机械
|
||||
// - 绝不使用全局最优解,只做「朝最近食物 + 保证这一步不死」的贪心判断
|
||||
func (g *Game) TickAI(now int64, s game.State) []game.AIAction {
|
||||
st, ok := s.(*state) // 断言为本玩法状态
|
||||
if !ok || st.Over { // 类型错误或已结束
|
||||
return nil
|
||||
}
|
||||
|
||||
out := make([]game.AIAction, 0, len(st.Snakes))
|
||||
for _, sn := range st.Snakes {
|
||||
if !sn.Alive || !game.IsAIUID(sn.UserID) { // 只操作存活的 AI 蛇
|
||||
continue
|
||||
}
|
||||
dir := pickDir(st, sn) // 选方向
|
||||
if dir == sn.Dir { // 与当前方向一致:无需发指令
|
||||
continue
|
||||
}
|
||||
if len(sn.Body) > 1 && (sn.Dir+2)%4 == dir { // 禁止 180 度掉头
|
||||
continue
|
||||
}
|
||||
payload, err := json.Marshal(turnPayload{Dir: dirName(dir)}) // 组装动作
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out = append(out, game.AIAction{UID: sn.UserID, Action: game.Action{Name: "turn", Payload: payload}})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// pickDir 选一个尽量朝向最近食物、且下一步不会立刻死掉的方向。
|
||||
func pickDir(st *state, sn *snakeState) int {
|
||||
hr, hc := st.rowOf(sn.Body[0]), st.colOf(sn.Body[0]) // 当前蛇头
|
||||
|
||||
// 1) 找最近的食物,派生期望方向(先缩小行差,再缩小列差)
|
||||
want := make([]int, 0, 2)
|
||||
best, bestDist := -1, int(1<<30)
|
||||
for _, f := range st.Foods {
|
||||
fr, fc := st.rowOf(f), st.colOf(f)
|
||||
if d := absInt(fr-hr) + absInt(fc-hc); d < bestDist {
|
||||
bestDist, best = d, f
|
||||
}
|
||||
}
|
||||
if best >= 0 {
|
||||
fr, fc := st.rowOf(best), st.colOf(best)
|
||||
if fr > hr {
|
||||
want = append(want, dirDown)
|
||||
} else if fr < hr {
|
||||
want = append(want, dirUp)
|
||||
}
|
||||
if fc > hc {
|
||||
want = append(want, dirRight)
|
||||
} else if fc < hc {
|
||||
want = append(want, dirLeft)
|
||||
}
|
||||
}
|
||||
|
||||
// 2) 保持当前方向次之
|
||||
want = append(want, sn.Dir)
|
||||
|
||||
// 3) 其余方向随机排序:制造拟人失误,避免四条 AI 走出完全相同的路线
|
||||
rest := make([]int, 0, 4)
|
||||
for d := 0; d < 4; d++ {
|
||||
if d == sn.Dir { // 已在 want 中
|
||||
continue
|
||||
}
|
||||
dup := false
|
||||
for _, w := range want {
|
||||
if w == d {
|
||||
dup = true
|
||||
}
|
||||
}
|
||||
if !dup {
|
||||
rest = append(rest, d)
|
||||
}
|
||||
}
|
||||
rand.Shuffle(len(rest), func(i, j int) { rest[i], rest[j] = rest[j], rest[i] })
|
||||
|
||||
// 4) 按顺序取第一个安全方向;都不安全就维持原方向(坦然赴死,符合真实对局)
|
||||
for _, d := range append(want, rest...) {
|
||||
if isSafe(st, sn, d) {
|
||||
return d
|
||||
}
|
||||
}
|
||||
return sn.Dir
|
||||
}
|
||||
|
||||
// isSafe 判断朝该方向走一步是否安全:不撞墙、不撞到任意蛇身。
|
||||
func isSafe(st *state, sn *snakeState, dir int) bool {
|
||||
hr, hc := st.rowOf(sn.Body[0]), st.colOf(sn.Body[0])
|
||||
nr, nc := hr+dirOffsets[dir][0], hc+dirOffsets[dir][1]
|
||||
if nr < 0 || nr >= st.Size || nc < 0 || nc >= st.Size { // 越界即撞墙
|
||||
return false
|
||||
}
|
||||
next := st.idx(nr, nc)
|
||||
for _, other := range st.Snakes { // 检查所有蛇身
|
||||
for k, b := range other.Body {
|
||||
if k == len(other.Body)-1 { // 尾节点本回合会让位,忽略
|
||||
continue
|
||||
}
|
||||
if b == next { // 撞身
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// absInt 返回整数绝对值。
|
||||
func absInt(v int) int {
|
||||
if v < 0 {
|
||||
return -v
|
||||
}
|
||||
return v
|
||||
}
|
||||
Reference in New Issue
Block a user