Files
2026-08-14 13:17:03 +08:00

574 lines
18 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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-10 号先手
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
}
}