// 大富翁在房间内的动作处理、AI/托管行动与状态快照 package room import ( "fmt" "strings" "time" "nl-game-api-gin/internal/gamecore/monopoly" ) // replaceSeatNames 把战报中的 P0-P3 座位占位符替换为玩家昵称(需持锁) func (r *Room) replaceSeatNames(line string) string { for i := len(r.seats) - 1; i >= 0; i-- { line = strings.ReplaceAll(line, fmt.Sprintf("P%d", i), r.seats[i].Name) } return line } // handleMonoRollLocked 真人掷骰(需持锁) func (r *Room) handleMonoRollLocked(seatIdx int, c *Client) { if r.status != "playing" || r.monoGame == nil { c.pushError("对局未开始") return } if err := r.monoGame.Roll(seatIdx); err != nil { c.pushError(err.Error()) return } r.afterMonoActionLocked() } // handleMonoBuyLocked 真人购买决策(需持锁) func (r *Room) handleMonoBuyLocked(seatIdx int, buy bool, c *Client) { if r.status != "playing" || r.monoGame == nil { c.pushError("对局未开始") return } if err := r.monoGame.Buy(seatIdx, buy); err != nil { c.pushError(err.Error()) return } r.afterMonoActionLocked() } // handleMonoUpgradeLocked 真人建筑升级决策(需持锁) func (r *Room) handleMonoUpgradeLocked(seatIdx int, up bool, c *Client) { if r.status != "playing" || r.monoGame == nil { c.pushError("对局未开始") return } if err := r.monoGame.Upgrade(seatIdx, up); err != nil { c.pushError(err.Error()) return } r.afterMonoActionLocked() } // aiMonoActLocked AI/托管行动:按阶段掷骰、购买或升级(需持锁) // escrow=true 为超时托管:只掷骰、不替真人买地/升级 func (r *Room) aiMonoActLocked(seatIdx int, escrow bool) { g := r.monoGame if g == nil || r.status != "playing" || g.Turn != seatIdx { return } switch g.Phase { case monopoly.PhaseRoll: g.Roll(seatIdx) case monopoly.PhaseBuy: buy := false if !escrow { buy = monopoly.AIShouldBuy(g, seatIdx, r.AIDifficulty) } g.Buy(seatIdx, buy) case monopoly.PhaseUpgrade: up := false if !escrow { up = monopoly.AIShouldUpgrade(g, seatIdx, r.AIDifficulty) } g.Upgrade(seatIdx, up) } r.afterMonoActionLocked() } // monoAnimMs 估算一批动画事件的前端播放时长(毫秒),用于调度预留观看时间 func monoAnimMs(events []monopoly.Event, boardLen int) int { ms := 0 for _, e := range events { switch e.Kind { case "roll": ms += 1100 case "move": steps := e.To - e.From if steps <= 0 { steps += boardLen } ms += steps*170 + 200 case "teleport": ms += 900 case "chance": ms += 1700 case "cash": ms += 300 case "buy": ms += 900 case "upgrade": ms += 900 case "bankrupt": ms += 900 case "rest": ms += 400 } } if ms > 5000 { ms = 5000 } return ms } // afterMonoActionLocked 动作后的统一收尾:收集动画事件、广播战报、结算或调度下一回合(需持锁) func (r *Room) afterMonoActionLocked() { g := r.monoGame for _, line := range g.DrainLog() { r.broadcastChatLocked(-1, "系统", "", r.replaceSeatNames(line), false) } // 真人全部破产/退出:立即按资产结算,避免纯 AI 空跑到轮数上限 if g.Phase != monopoly.PhaseOver { humanAlive := false for _, s := range r.seats { if s.UserID > 0 && !g.Players[s.Index].Out { humanAlive = true } } if !humanAlive { g.ForceFinish() for _, line := range g.DrainLog() { r.broadcastChatLocked(-1, "系统", "", r.replaceSeatNames(line), false) } } } // 收集本次动作的动画事件:替换昵称占位符、递增序号、按事件估算播放时长 evs := g.DrainEvents() for i := range evs { evs[i].Text = r.replaceSeatNames(evs[i].Text) } r.monoEvents = evs r.monoSeq++ r.animMs = monoAnimMs(evs, len(g.Tiles)) if g.Phase == monopoly.PhaseOver { names := []string{} for _, w := range g.Winners { names = append(names, r.seats[w].Name) } winners, desc := g.Winners, fmt.Sprintf("对局结束,%s 获胜!", strings.Join(names, "、")) // 结算延迟到最后一段动画播完再广播,避免棋子还没走完就弹出结果 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() } // monoStateLocked 大富翁状态快照(信息全公开,各座位一致,需持锁) func (r *Room) monoStateLocked() map[string]any { g := r.monoGame players := make([]map[string]any, len(g.Players)) for i, p := range g.Players { players[i] = map[string]any{ "seat": p.Seat, "money": p.Money, "pos": p.Pos, "out": p.Out, "skip": p.Skip, "assets": g.AssetsOf(i), } } st := map[string]any{ "phase": g.Phase, "turn": g.Turn, "dice": g.Dice, "round": g.Round, "max_rounds": monopoly.MaxRounds, "tiles": g.Tiles, "players": players, // 动作序号 + 动画事件:前端凭连续序号判断播放动画还是直接同步 "seq": r.monoSeq, "events": r.monoEvents, } // 购买/升级阶段附带待决策地块信息(kind 区分弹哪种卡片) if g.Phase == monopoly.PhaseBuy && g.Pending >= 0 { t := g.Tiles[g.Pending] st["pending"] = map[string]any{"kind": "buy", "tile": t.Idx, "name": t.Name, "price": t.Price} } if g.Phase == monopoly.PhaseUpgrade && g.Pending >= 0 { t := g.Tiles[g.Pending] st["pending"] = map[string]any{ "kind": "upgrade", "tile": t.Idx, "name": t.Name, "price": monopoly.UpgradeCost(t), "level": t.Level, } } return st }