// Package monopoly 大富翁核心规则:24 格环形棋盘、掷骰移动、买地收租、机会卡与破产判定 // 只负责纯游戏逻辑,房间调度 / 网络推送由 room 包完成 package monopoly import ( "fmt" "math/rand" ) // 地块类型 const ( TileGo = "go" // 起点:经过或停留 +200 TileProp = "prop" // 可购买地产 TileChance = "chance" // 机会卡 TileTax = "tax" // 缴税 TileJail = "jail" // 监狱(路过无事,被送进来休息一回合) TileGoToJail = "gotojail" // 前往监狱 TilePark = "park" // 免费停车(无事发生) ) // 阶段 const ( PhaseRoll = "roll" // 等待当前玩家掷骰 PhaseBuy = "buy" // 等待当前玩家决定是否购买脚下地产 PhaseUpgrade = "upgrade" // 等待当前玩家决定是否升级自己脚下的地产 PhaseOver = "over" // 对局结束 ) // 数值配置 const ( StartMoney = 1500 // 开局资金 GoBonus = 200 // 经过/停留起点奖励 MaxRounds = 25 // 轮数上限:到达后按总资产分胜负 JailIndex = 6 // 监狱所在格 MaxLevel = 3 // 地产最高等级(1 小屋 / 2 楼房 / 3 酒店) ) // Tile 一个地块 type Tile struct { Idx int `json:"idx"` // 格子序号(0-23 顺时针) Type string `json:"type"` // 地块类型 Name string `json:"name"` // 显示名称 Price int `json:"price"` // 地价(非地产为 0) Group int `json:"group"` // 颜色组编号(-1 表示非地产) Owner int `json:"owner"` // 归属座位(-1 无主) Level int `json:"level"` // 建筑等级(0 空地 / 1 小屋 / 2 楼房 / 3 酒店,租金按等级翻倍) } // Player 一名玩家的资产状态 type Player struct { Seat int `json:"seat"` // 座位号 Money int `json:"money"` // 现金 Pos int `json:"pos"` // 当前位置 Out bool `json:"out"` // 是否已破产出局 Skip int `json:"skip"` // 还需休息的回合数(监狱/机会卡) } // Event 结构化动画事件:随状态广播下发,前端据此按顺序播放骰子/走格/金钱等动效 // Kind 取值: // roll 掷骰(D1/D2 为两颗骰子点数) // move 沿棋盘顺时针走格(From→To,可能跨过起点) // teleport 直达传送(入狱 / 机会卡直达起点,From→To 播放飞行动画) // cash 金钱变动(Amount 正收负支,Text 为原因) // chance 抽到机会卡(Text 为卡面文案,前端翻牌展示) // buy 买下地产(Tile 为地块序号) // bankrupt 破产出局 // rest 休息中跳过回合 type Event struct { Kind string `json:"kind"` // 事件类型 Seat int `json:"seat"` // 动作主体座位 D1 int `json:"d1"` // roll:骰子一点数 D2 int `json:"d2"` // roll:骰子二点数 From int `json:"from"` // move/teleport:起始格 To int `json:"to"` // move/teleport:结束格 Amount int `json:"amount"` // cash:变动金额(正收负支) Tile int `json:"tile"` // buy:地块序号 Text string `json:"text"` // cash/chance:文案(P%d 占位符由房间层替换昵称) } // Game 一局大富翁 type Game struct { Tiles []*Tile // 24 个地块 Players []*Player // 全部玩家(按座位) Turn int // 当前行动座位 Phase string // roll / buy / over Dice [2]int // 最近一次骰子 Round int // 当前轮数(回到 0 号玩家算新一轮) Pending int // 待购买的地块序号(buy 阶段有效) Winners []int // 结束后的获胜座位 Log []string // 最近动作的文字描述(房间取走后广播) Events []Event // 最近动作的动画事件(房间取走后随状态下发) } // buildTiles 构建 24 格棋盘:4 角 + 14 块地产(7 组×2)+ 4 机会 + 2 税 func buildTiles() []*Tile { type def struct { typ string name string price int group int } defs := []def{ {TileGo, "起点", 0, -1}, // 0 {TileProp, "北京", 350, 0}, // 1 {TileProp, "上海", 340, 0}, // 2 {TileChance, "机会", 0, -1}, // 3 {TileTax, "所得税·150", 0, -1}, // 4 {TileProp, "广州", 300, 1}, // 5 {TileJail, "监狱", 0, -1}, // 6 {TileProp, "深圳", 300, 1}, // 7 {TileProp, "杭州", 260, 2}, // 8 {TileChance, "机会", 0, -1}, // 9 {TileProp, "成都", 250, 2}, // 10 {TileProp, "重庆", 220, 3}, // 11 {TilePark, "免费停车", 0, -1}, // 12 {TileProp, "武汉", 210, 3}, // 13 {TileProp, "西安", 180, 4}, // 14 {TileChance, "机会", 0, -1}, // 15 {TileTax, "养路费·100", 0, -1}, // 16 {TileProp, "南京", 170, 4}, // 17 {TileGoToJail, "入狱", 0, -1}, // 18 {TileProp, "苏州", 140, 5}, // 19 {TileProp, "天津", 130, 5}, // 20 {TileChance, "机会", 0, -1}, // 21 {TileProp, "长沙", 100, 6}, // 22 {TileProp, "青岛", 90, 6}, // 23 } tiles := make([]*Tile, len(defs)) for i, d := range defs { tiles[i] = &Tile{Idx: i, Type: d.typ, Name: d.name, Price: d.price, Group: d.group, Owner: -1} } return tiles } // NewGame 创建 n 人对局(座位 0..n-1),0 号先手 func NewGame(n int) *Game { players := make([]*Player, n) for i := range players { players[i] = &Player{Seat: i, Money: StartMoney} } return &Game{ Tiles: buildTiles(), Players: players, Turn: 0, Phase: PhaseRoll, Round: 1, Pending: -1, Winners: nil, } } // logf 追加一条动作描述 func (g *Game) logf(format string, args ...any) { g.Log = append(g.Log, fmt.Sprintf(format, args...)) } // DrainLog 取走并清空累计的动作描述 func (g *Game) DrainLog() []string { out := g.Log g.Log = nil return out } // ev 追加一条动画事件 func (g *Game) ev(e Event) { g.Events = append(g.Events, e) } // DrainEvents 取走并清空累计的动画事件 func (g *Game) DrainEvents() []Event { out := g.Events g.Events = nil return out } // UpgradeCost 升一级建筑的费用:地价的一半 func UpgradeCost(t *Tile) int { return t.Price / 2 } // rentOf 地产租金:地价 30% 为基础,每级建筑翻一倍,同组全部集齐再翻倍 func (g *Game) rentOf(t *Tile) int { rent := (t.Price * 30 / 100) << t.Level if g.ownsFullGroup(t.Owner, t.Group) { rent *= 2 } return rent } // ownsFullGroup 某座位是否集齐指定颜色组 func (g *Game) ownsFullGroup(seat, group int) bool { if group < 0 || seat < 0 { return false } for _, t := range g.Tiles { if t.Group == group && t.Owner != seat { return false } } return true } // AssetsOf 总资产 = 现金 + 名下地产地价 + 建筑投入 func (g *Game) AssetsOf(seat int) int { total := g.Players[seat].Money for _, t := range g.Tiles { if t.Owner == seat { total += t.Price + t.Level*UpgradeCost(t) } } return total } // activeCount 未出局玩家数 func (g *Game) activeCount() int { n := 0 for _, p := range g.Players { if !p.Out { n++ } } return n } // Roll 当前玩家掷骰并解算落点;可能进入 buy 阶段或直接轮到下家 func (g *Game) Roll(seat int) error { if g.Phase != PhaseRoll { return fmt.Errorf("当前不在掷骰阶段") } if g.Turn != seat { return fmt.Errorf("还没轮到你掷骰") } p := g.Players[seat] g.Dice[0] = rand.Intn(6) + 1 g.Dice[1] = rand.Intn(6) + 1 steps := g.Dice[0] + g.Dice[1] oldPos := p.Pos p.Pos = (p.Pos + steps) % len(g.Tiles) g.ev(Event{Kind: "roll", Seat: seat, D1: g.Dice[0], D2: g.Dice[1]}) g.ev(Event{Kind: "move", Seat: seat, From: oldPos, To: p.Pos}) // 经过或落在起点:领取奖励 if p.Pos < oldPos || p.Pos == 0 { p.Money += GoBonus g.ev(Event{Kind: "cash", Seat: seat, Amount: GoBonus, Text: "经过起点奖励"}) g.logf("P%d 掷出 %d+%d,经过起点领取 %d 元", seat, g.Dice[0], g.Dice[1], GoBonus) } else { g.logf("P%d 掷出 %d+%d,前进 %d 格", seat, g.Dice[0], g.Dice[1], steps) } g.resolveTile(p) return nil } // resolveTile 解算玩家落点效果(可能置为 buy 阶段 / 破产 / 轮到下家) func (g *Game) resolveTile(p *Player) { t := g.Tiles[p.Pos] switch t.Type { case TileProp: if t.Owner == -1 { // 无主地产:买得起就进入购买决策,否则直接过 if p.Money >= t.Price { g.Phase = PhaseBuy g.Pending = t.Idx g.logf("P%d 来到无主地产「%s」(%d 元),可以选择买下", p.Seat, t.Name, t.Price) return } g.logf("P%d 来到「%s」但现金不足 %d 元,只能路过", p.Seat, t.Name, t.Price) } else if t.Owner != p.Seat { // 他人地产:付租金 rent := g.rentOf(t) g.payTo(p, g.Players[t.Owner], rent, fmt.Sprintf("落在 P%d 的「%s」支付租金", t.Owner, t.Name)) } else if t.Level < MaxLevel && p.Money >= UpgradeCost(t) { // 自己的地产且可加盖:进入升级决策 g.Phase = PhaseUpgrade g.Pending = t.Idx g.logf("P%d 回到自己的「%s」,可以花 %d 元升级建筑", p.Seat, t.Name, UpgradeCost(t)) return } else { g.logf("P%d 回到自己的地盘「%s」,休息一下", p.Seat, t.Name) } case TileChance: g.applyChance(p) case TileTax: tax := 150 if t.Idx == 16 { tax = 100 } g.payTo(p, nil, tax, fmt.Sprintf("落在「%s」缴税", t.Name)) case TileGoToJail: g.ev(Event{Kind: "teleport", Seat: p.Seat, From: p.Pos, To: JailIndex, Text: "被押送进监狱"}) p.Pos = JailIndex p.Skip = 1 g.logf("P%d 被押送进监狱,休息 1 回合", p.Seat) case TileJail, TilePark, TileGo: g.logf("P%d 在「%s」稍作停留", p.Seat, t.Name) } g.endTurn() } // applyChance 机会卡:六种随机事件 func (g *Game) applyChance(p *Player) { switch rand.Intn(6) { case 0: g.ev(Event{Kind: "chance", Seat: p.Seat, Text: "路边捡到红包 +150 元"}) p.Money += 150 g.ev(Event{Kind: "cash", Seat: p.Seat, Amount: 150, Text: "机会卡红包"}) g.logf("P%d 抽到机会卡:路边捡到红包 +150 元", p.Seat) case 1: g.ev(Event{Kind: "chance", Seat: p.Seat, Text: "违章停车,罚款 100 元"}) g.payTo(p, nil, 100, "抽到机会卡:违章停车罚款") case 2: g.ev(Event{Kind: "chance", Seat: p.Seat, Text: "银行发放利息 +100 元"}) p.Money += 100 g.ev(Event{Kind: "cash", Seat: p.Seat, Amount: 100, Text: "银行利息"}) g.logf("P%d 抽到机会卡:银行发放利息 +100 元", p.Seat) case 3: g.ev(Event{Kind: "chance", Seat: p.Seat, Text: "乘坐地铁直达起点,领取 200 元"}) g.ev(Event{Kind: "teleport", Seat: p.Seat, From: p.Pos, To: 0, Text: "直达起点"}) p.Pos = 0 p.Money += GoBonus g.ev(Event{Kind: "cash", Seat: p.Seat, Amount: GoBonus, Text: "起点奖励"}) g.logf("P%d 抽到机会卡:直达起点并领取 %d 元", p.Seat, GoBonus) case 4: g.ev(Event{Kind: "chance", Seat: p.Seat, Text: "感冒卧床,休息 1 回合"}) p.Skip = 1 g.logf("P%d 抽到机会卡:感冒卧床,休息 1 回合", p.Seat) default: old := p.Pos p.Pos = (p.Pos + 3) % len(g.Tiles) if p.Pos < old { p.Money += GoBonus } t := g.Tiles[p.Pos] g.ev(Event{Kind: "chance", Seat: p.Seat, Text: fmt.Sprintf("搭上顺风车,前进 3 格到「%s」", t.Name)}) g.ev(Event{Kind: "move", Seat: p.Seat, From: old, To: p.Pos}) if p.Pos < old { g.ev(Event{Kind: "cash", Seat: p.Seat, Amount: GoBonus, Text: "经过起点奖励"}) } g.logf("P%d 抽到机会卡:顺风车前进 3 格到「%s」", p.Seat, t.Name) // 简化:顺风车落点只结算租金/税,不再连环触发购买与机会 if t.Type == TileProp && t.Owner != -1 && t.Owner != p.Seat { g.payTo(p, g.Players[t.Owner], g.rentOf(t), fmt.Sprintf("落在 P%d 的「%s」支付租金", t.Owner, t.Name)) } } } // payTo 支付:to 为 nil 表示交给银行;付不起即破产(剩余现金全部给债主) func (g *Game) payTo(from, to *Player, amount int, why string) { if amount > from.Money { // 破产:现金清给债主,名下地产全部释放 if to != nil && from.Money > 0 { to.Money += from.Money g.ev(Event{Kind: "cash", Seat: to.Seat, Amount: from.Money, Text: "收到对方全部现金"}) } from.Money = 0 from.Out = true for _, t := range g.Tiles { if t.Owner == from.Seat { t.Owner = -1 t.Level = 0 } } g.ev(Event{Kind: "bankrupt", Seat: from.Seat, Amount: amount, Text: why}) g.logf("P%d %s %d 元但现金不足,破产出局!名下地产全部拍卖", from.Seat, why, amount) g.checkLastStanding() return } from.Money -= amount g.ev(Event{Kind: "cash", Seat: from.Seat, Amount: -amount, Text: why}) if to != nil { to.Money += amount g.ev(Event{Kind: "cash", Seat: to.Seat, Amount: amount, Text: "收到租金"}) } g.logf("P%d %s %d 元", from.Seat, why, amount) } // Buy 购买决策:buy=false 表示放弃 func (g *Game) Buy(seat int, buy bool) error { if g.Phase != PhaseBuy { return fmt.Errorf("当前不在购买阶段") } if g.Turn != seat { return fmt.Errorf("还没轮到你决策") } t := g.Tiles[g.Pending] p := g.Players[seat] if buy && p.Money >= t.Price { p.Money -= t.Price t.Owner = seat g.ev(Event{Kind: "buy", Seat: seat, Tile: t.Idx, Text: t.Name}) g.ev(Event{Kind: "cash", Seat: seat, Amount: -t.Price, Text: "购地支出"}) if g.ownsFullGroup(seat, t.Group) { g.logf("P%d 花 %d 元买下「%s」,集齐同色地段租金翻倍!", seat, t.Price, t.Name) } else { g.logf("P%d 花 %d 元买下「%s」", seat, t.Price, t.Name) } } else { g.logf("P%d 放弃购买「%s」", seat, t.Name) } g.Pending = -1 g.endTurn() return nil } // Upgrade 建筑升级决策:up=false 表示放弃 func (g *Game) Upgrade(seat int, up bool) error { if g.Phase != PhaseUpgrade { return fmt.Errorf("当前不在升级阶段") } if g.Turn != seat { return fmt.Errorf("还没轮到你决策") } t := g.Tiles[g.Pending] p := g.Players[seat] cost := UpgradeCost(t) if up && t.Level < MaxLevel && p.Money >= cost { p.Money -= cost t.Level++ g.ev(Event{Kind: "upgrade", Seat: seat, Tile: t.Idx, Amount: t.Level, Text: t.Name}) g.ev(Event{Kind: "cash", Seat: seat, Amount: -cost, Text: "建筑升级支出"}) names := []string{"", "小屋", "楼房", "酒店"} g.logf("P%d 花 %d 元把「%s」升级为%s(租金翻倍)", seat, cost, t.Name, names[t.Level]) } else { g.logf("P%d 暂不升级「%s」", seat, t.Name) } g.Pending = -1 g.endTurn() return nil } // endTurn 回合收尾:对局未结束时把行动权交给下一个未出局玩家 // 注意:进入购买/升级阶段的路径在 resolveTile 中提前返回,不会走到这里 func (g *Game) endTurn() { if g.Phase == PhaseOver { return } g.Phase = PhaseRoll g.advanceTurn() } // advanceTurn 找到下一个可行动的玩家;处理休息回合与轮数上限 func (g *Game) advanceTurn() { if g.checkLastStanding() { return } for i := 0; i < len(g.Players)*2; i++ { g.Turn = (g.Turn + 1) % len(g.Players) if g.Turn == 0 { g.Round++ if g.Round > MaxRounds { g.finishByAssets() return } } p := g.Players[g.Turn] if p.Out { continue } if p.Skip > 0 { p.Skip-- g.ev(Event{Kind: "rest", Seat: p.Seat}) g.logf("P%d 在休息中,跳过本回合", p.Seat) continue } g.Phase = PhaseRoll return } // 理论上不可达(存在未出局玩家时必能找到),兜底按资产结算 g.finishByAssets() } // checkLastStanding 只剩一名玩家时立即结束 func (g *Game) checkLastStanding() bool { if g.activeCount() > 1 { return false } for _, p := range g.Players { if !p.Out { g.Winners = []int{p.Seat} } } g.Phase = PhaseOver g.logf("其余玩家全部破产,对局结束!") return true } // finishByAssets 轮数打满:按总资产最高者获胜(并列全算赢) func (g *Game) finishByAssets() { best := -1 for _, p := range g.Players { if p.Out { continue } if a := g.AssetsOf(p.Seat); a > best { best = a } } g.Winners = nil for _, p := range g.Players { if !p.Out && g.AssetsOf(p.Seat) == best { g.Winners = append(g.Winners, p.Seat) } } g.Phase = PhaseOver g.logf("%d 轮已满,按总资产结算胜负!", MaxRounds) } // ForceFinish 提前按总资产结算(房间层在真人全部出局时调用,避免纯 AI 空跑) func (g *Game) ForceFinish() { if g.Phase != PhaseOver { g.finishByAssets() } } // Eliminate 玩家中途退出:视为破产出局,地产释放 func (g *Game) Eliminate(seat int) { p := g.Players[seat] if p.Out { return } p.Out = true p.Money = 0 for _, t := range g.Tiles { if t.Owner == seat { t.Owner = -1 t.Level = 0 } } // 正轮到退出者时把行动权交给下家 if g.Phase != PhaseOver && g.Turn == seat { g.Pending = -1 g.advanceTurn() } else { g.checkLastStanding() } } // AIShouldUpgrade 规则 AI 升级决策:手里越宽裕越愿意加盖,高手集齐同色组必升 func AIShouldUpgrade(g *Game, seat int, difficulty string) bool { t := g.Tiles[g.Pending] rest := g.Players[seat].Money - UpgradeCost(t) switch difficulty { case "easy": return rest >= 500 && rand.Float64() < 0.6 case "hard": if g.ownsFullGroup(seat, t.Group) && rest >= 100 { return true } return rest >= 250 default: return rest >= 350 } } // AIShouldBuy 规则 AI 购买决策:难度越高越会算账 func AIShouldBuy(g *Game, seat int, difficulty string) bool { t := g.Tiles[g.Pending] p := g.Players[seat] rest := p.Money - t.Price switch difficulty { case "easy": // 新手:手里宽裕才买,还带点随缘 return rest >= 400 && rand.Float64() < 0.7 case "hard": // 高手:能凑齐同色组必买,普通地块留 150 备用金即可 otherOwned := false for _, ot := range g.Tiles { if ot.Group == t.Group && ot.Idx != t.Idx && ot.Owner == seat { otherOwned = true } } if otherOwned && rest >= 0 { return true } return rest >= 150 default: return rest >= 250 } }