574 lines
18 KiB
Go
574 lines
18 KiB
Go
// 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
|
||
}
|
||
}
|