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