Files
nl-game-api/internal/room/room_ludo.go
2026-08-14 13:17:03 +08:00

162 lines
4.2 KiB
Go

// 飞行棋在房间内的动作处理、AI/托管行动与状态快照
package room
import (
"fmt"
"time"
"nl-game-api-gin/internal/gamecore/ludo"
)
// handleLudoRollLocked 真人掷骰(需持锁)
func (r *Room) handleLudoRollLocked(seatIdx int, c *Client) {
if r.status != "playing" || r.ludoGame == nil {
c.pushError("对局未开始")
return
}
if err := r.ludoGame.Roll(seatIdx); err != nil {
c.pushError(err.Error())
return
}
r.afterLudoActionLocked()
}
// handleLudoMoveLocked 真人选棋移动(需持锁)
func (r *Room) handleLudoMoveLocked(seatIdx, plane int, c *Client) {
if r.status != "playing" || r.ludoGame == nil {
c.pushError("对局未开始")
return
}
if err := r.ludoGame.Move(seatIdx, plane); err != nil {
c.pushError(err.Error())
return
}
r.afterLudoActionLocked()
}
// aiLudoActLocked AI/托管行动:掷骰后按估值选棋(需持锁)
// escrow=true 为超时托管:使用中等难度规则决策
func (r *Room) aiLudoActLocked(seatIdx int, escrow bool) {
g := r.ludoGame
if g == nil || r.status != "playing" || g.Turn != seatIdx {
return
}
diff := r.AIDifficulty
if escrow {
diff = "medium"
}
if g.Phase == ludo.PhaseRoll {
g.Roll(seatIdx)
} else if g.Phase == ludo.PhaseMove {
if plane := ludo.AIChoosePlane(g, seatIdx, diff); plane >= 0 {
g.Move(seatIdx, plane)
}
}
r.afterLudoActionLocked()
}
// ludoAnimMs 估算一批动画事件的前端播放时长(毫秒),用于调度预留观看时间
func ludoAnimMs(events []ludo.Event) int {
ms := 0
for _, e := range events {
switch e.Kind {
case "roll":
ms += 1000
case "nomove":
ms += 600
case "takeoff":
ms += 650
case "move":
// 反弹时实际走的格数等于骰子点数,正常前进为 To-From
steps := e.To - e.From
if e.Die > steps {
steps = e.Die
}
ms += steps*150 + 150
case "jump":
ms += 650
case "fly":
ms += 750
case "capture":
ms += 950
case "arrive":
ms += 700
case "bonus":
ms += 500
case "sixpenalty":
ms += 950
}
}
if ms > 4500 {
ms = 4500
}
return ms
}
// afterLudoActionLocked 动作后的统一收尾:收集动画事件、广播战报、结算或调度下一回合(需持锁)
func (r *Room) afterLudoActionLocked() {
g := r.ludoGame
for _, line := range g.DrainLog() {
r.broadcastChatLocked(-1, "系统", "", r.replaceSeatNames(line), false)
}
// 真人全部退赛/离开:立即按进度提前结算,避免纯 AI 空跑到掷骰上限
if g.Phase != ludo.PhaseOver {
humanAlive := false
for _, s := range r.seats {
if s.UserID > 0 && g.Active[s.Index] {
humanAlive = true
}
}
if !humanAlive {
g.ForceFinish()
for _, line := range g.DrainLog() {
r.broadcastChatLocked(-1, "系统", "", r.replaceSeatNames(line), false)
}
}
}
// 收集本次动作的动画事件:递增序号、按事件估算播放时长
r.ludoEvents = g.DrainEvents()
r.ludoSeq++
r.animMs = ludoAnimMs(r.ludoEvents)
if g.Phase == ludo.PhaseOver {
winners := []int{}
desc := "对局结束"
if g.Winner >= 0 {
winners = append(winners, g.Winner)
desc = fmt.Sprintf("%s 率先完成飞行,获得胜利!", r.seats[g.Winner].Name)
}
// 结算延迟到最后一段动画播完再广播,避免飞机还没落地就弹出结果
r.turnSeq++ // 使遗留的回合超时定时器失效
if r.turnTimer != nil {
r.turnTimer.Stop()
}
delay := time.Duration(r.animMs+600) * time.Millisecond
time.AfterFunc(delay, func() {
r.mu.Lock()
defer r.mu.Unlock()
// 等待期间房间可能已被解散,不再结算与广播
if r.closed {
return
}
r.settleLocked(winners, desc)
r.broadcastStateLocked()
})
} else {
r.scheduleTurnLocked()
}
r.broadcastStateLocked()
}
// ludoStateLocked 飞行棋状态快照(信息全公开,需持锁)
func (r *Room) ludoStateLocked() map[string]any {
g := r.ludoGame
return map[string]any{
"phase": g.Phase, "turn": g.Turn, "die": g.Die,
"planes": g.Planes, "active": g.Active,
"movable": g.Movable(g.Turn), "last_move": g.LastMv,
"rolls": g.Rolls, "max_rolls": ludo.MaxRolls,
// 动作序号 + 动画事件:前端凭连续序号判断播放动画还是直接同步
"seq": r.ludoSeq, "events": r.ludoEvents,
}
}