208 lines
5.7 KiB
Go
208 lines
5.7 KiB
Go
// 大富翁在房间内的动作处理、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
|
||
}
|