package ddz import ( "errors" "math/rand" ) // Phase 对局阶段 type Phase string // 四个阶段:叫地主 → 抢地主 → 出牌 → 结束 const ( PhaseBidding Phase = "bidding" // 叫地主阶段 PhaseRobbing Phase = "robbing" // 抢地主阶段(有人叫地主后,其余两家各有一次抢的机会,每抢一次倍数×2) PhasePlaying Phase = "playing" // 出牌阶段 PhaseOver Phase = "over" // 对局结束 ) // PlayEvent 一次出牌事件(广播与回放用) type PlayEvent struct { Seat int `json:"seat"` // 出牌座位 Cards []int `json:"cards"` // 出的牌(空=过) Desc string `json:"desc"` // 牌型描述("过" 或 "对子 55") } // Game 斗地主一局的完整状态(由房间持有,加锁访问) type Game struct { Hands [3][]int // 三家手牌(按点数降序) Bottom []int // 3张底牌(地主确定后并入地主手牌) Landlord int // 地主座位(-1=未确定;抢地主阶段为当前地主候选) Phase Phase // 当前阶段 Turn int // 当前行动座位 LastCombo *Combo // 需要压的牌(nil=自由出牌) LastSeat int // 出 LastCombo 的座位 PassCount int // 连续过牌次数(两家都过则重新自由出牌) BidStart int // 叫地主起始座位 BidCount int // 叫地主已表态人数 FirstCall int // 最先叫地主的座位(抢地主阶段其不再表态) RobActed int // 抢地主阶段已表态人数 Multiplier int // 叫抢倍数(叫地主=1,每被抢一次×2;炸弹与春天在结算时另乘) Winner int // 获胜座位(地主赢=地主座位;农民赢=先出完的农民座位;-1=未结束) Bombs int // 本局已出炸弹数(每个炸弹结算倍数×2) History []PlayEvent // 出牌历史(最近的在末尾) } // NewGame 开新局:发牌并随机确定叫地主起始座位 func NewGame() *Game { hands, bottom := Deal() start := rand.Intn(3) return &Game{ Hands: hands, Bottom: bottom, Landlord: -1, Phase: PhaseBidding, Turn: start, BidStart: start, FirstCall: -1, Multiplier: 1, LastSeat: -1, Winner: -1, } } // Bid 叫地主表态:call=true 表示叫地主,随后进入抢地主阶段 // 返回 redeal=true 表示三家都不叫需要重新发牌 func (g *Game) Bid(seat int, call bool) (redeal bool, err error) { if g.Phase != PhaseBidding { return false, errors.New("当前不在叫地主阶段") } if seat != g.Turn { return false, errors.New("还没轮到你表态") } g.BidCount++ if call { // 叫地主:成为地主候选,进入抢地主阶段(其余两家按顺序各有一次抢的机会) g.Landlord = seat g.FirstCall = seat g.Phase = PhaseRobbing g.RobActed = 0 g.Turn = (seat + 1) % 3 return false, nil } // 不叫:轮到下家;三家都不叫则重新发牌 if g.BidCount >= 3 { return true, nil } g.Turn = (g.Turn + 1) % 3 return false, nil } // Rob 抢地主表态:rob=true 时该座位成为新的地主候选且倍数×2 // 两位非首叫玩家依次表态完后确定地主进入出牌阶段(简化规则:首叫者无反抢) func (g *Game) Rob(seat int, rob bool) error { if g.Phase != PhaseRobbing { return errors.New("当前不在抢地主阶段") } if seat != g.Turn { return errors.New("还没轮到你表态") } g.RobActed++ if rob { g.Landlord = seat g.Multiplier *= 2 } if g.RobActed >= 2 { // 两家都已表态:地主确定,底牌并入手牌,地主先出 g.Hands[g.Landlord] = append(g.Hands[g.Landlord], g.Bottom...) SortDesc(g.Hands[g.Landlord]) g.Phase = PhasePlaying g.Turn = g.Landlord g.LastCombo = nil g.LastSeat = -1 return nil } // 轮到另一位非首叫玩家表态 next := (g.Turn + 1) % 3 if next == g.FirstCall { next = (next + 1) % 3 } g.Turn = next return nil } // Play 出牌:校验牌在手、牌型合法、能压上家,然后推进回合 func (g *Game) Play(seat int, cards []int) (*Combo, error) { if g.Phase != PhasePlaying { return nil, errors.New("当前不在出牌阶段") } if seat != g.Turn { return nil, errors.New("还没轮到你出牌") } if len(cards) == 0 { return nil, errors.New("请选择要出的牌") } combo := Parse(cards) if combo == nil { return nil, errors.New("不是合法牌型") } if !combo.Beats(g.LastCombo) { return nil, errors.New("压不过上家的牌") } rest := removeCards(g.Hands[seat], cards) if rest == nil { return nil, errors.New("出的牌不在手牌中") } g.Hands[seat] = rest g.LastCombo = combo g.LastSeat = seat g.PassCount = 0 if combo.Type == ComboBomb || combo.Type == ComboRocket { g.Bombs++ } g.History = append(g.History, PlayEvent{Seat: seat, Cards: cards, Desc: combo.Desc()}) // 出完牌即获胜 if len(rest) == 0 { g.Phase = PhaseOver g.Winner = seat return combo, nil } g.Turn = (g.Turn + 1) % 3 return combo, nil } // Pass 过牌:自由出牌时不允许过;两家连过后回到最后出牌者自由出牌 func (g *Game) Pass(seat int) error { if g.Phase != PhasePlaying { return errors.New("当前不在出牌阶段") } if seat != g.Turn { return errors.New("还没轮到你") } if g.LastCombo == nil || g.LastSeat == seat { return errors.New("轮到你自由出牌,不能过") } g.PassCount++ g.History = append(g.History, PlayEvent{Seat: seat, Cards: nil, Desc: "过"}) g.Turn = (g.Turn + 1) % 3 // 两家都过:回到最后出牌者,重新自由出牌 if g.Turn == g.LastSeat { g.LastCombo = nil g.PassCount = 0 } return nil } // LandlordWon 结算辅助:地主是否获胜(Phase 为 Over 时有效) func (g *Game) LandlordWon() bool { return g.Winner == g.Landlord } // IsFarmer 判断座位是否为农民 func (g *Game) IsFarmer(seat int) bool { return g.Landlord >= 0 && seat != g.Landlord } // SpringMultiplier 春天倍数(对局结束后调用): // 地主获胜且两位农民一张牌都没出过 = 春天 ×2; // 农民获胜且地主只出过开局第一手 = 反春 ×2;其余返回 1 func (g *Game) SpringMultiplier() int { if g.Phase != PhaseOver || g.Landlord < 0 { return 1 } lordPlays, farmerPlays := 0, 0 for _, e := range g.History { if len(e.Cards) == 0 { continue } if e.Seat == g.Landlord { lordPlays++ } else { farmerPlays++ } } if g.LandlordWon() && farmerPlays == 0 { return 2 } if !g.LandlordWon() && lordPlays <= 1 { return 2 } return 1 } // CurrentMultiplier 当前展示倍数 = 叫抢倍数 × 2^炸弹数(上限 64 防积分爆炸) func (g *Game) CurrentMultiplier() int { m := g.Multiplier if m < 1 { m = 1 } for i := 0; i < g.Bombs && m < 64; i++ { m *= 2 } if m > 64 { m = 64 } return m } // TotalMultiplier 结算总倍数 = 叫抢倍数 × 2^炸弹数 × 春天倍数(上限 64) func (g *Game) TotalMultiplier() int { m := g.CurrentMultiplier() * g.SpringMultiplier() if m > 64 { m = 64 } return m }