Files

241 lines
6.9 KiB
Go
Raw Permalink Normal View History

2026-08-14 13:17:03 +08:00
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
}