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

241 lines
6.9 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 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
}