有限状态机

基于有限状态机(FSM)的Go语言斗地主服务端设计

引言:状态机与棋牌游戏的完美契合

斗地主作为一款经典的三人棋牌游戏,其游戏流程天然具有明确的阶段划分:发牌、叫地主、出牌、结算等。这种清晰的阶段性特征与有限状态机(FSM)的概念高度契合。本文将详细介绍如何使用Go语言实现一个基于FSM的斗地主游戏服务端,涵盖状态设计、网络通信、并发控制等核心内容。

一、斗地主游戏状态分析

1.1 游戏流程分解

典型的斗地主游戏包含以下主要状态:

stateDiagram
    [*] --> Waiting  // 等待玩家加入
    Waiting --> Dealing  // 全部准备后发牌
    Dealing --> Bidding  // 进入叫地主阶段
    Bidding --> Playing  // 确定地主后开始出牌
    Playing --> GameOver  // 游戏结束
    GameOver --> Waiting  // 重新开始

1.2 状态转移事件

每个状态之间的转移由特定事件触发:

状态转移触发条件
Waiting → Dealing所有玩家准备就绪
Dealing → Bidding发牌完成
Bidding → Playing地主确定且加倍完成
Playing → GameOver有玩家出完手牌

二、Go语言FSM实现

2.1 状态接口设计

首先定义状态接口和基础结构:

// game/state.go
package game

type State interface {
    Enter(room *Room)       // 进入状态
    Exit(room *Room)        // 退出状态
    HandleMessage(room *Room, player *Player, msg *Message) // 处理消息
    Tick(room *Room)        // 状态心跳
}

// 基础状态嵌入结构
type BaseState struct{}

func (s *BaseState) Enter(room *Room)       {}
func (s *BaseState) Exit(room *Room)        {}
func (s *BaseState) HandleMessage(room *Room, player *Player, msg *Message) {}
func (s *BaseState) Tick(room *Room)        {}

2.2 具体状态实现

​等待状态示例​​:

// game/waiting_state.go
package game

type WaitingState struct {
    BaseState
    readyPlayers map[int]bool // 记录准备玩家
}

func NewWaitingState() *WaitingState {
    return &WaitingState{
        readyPlayers: make(map[int]bool),
    }
}

func (s *WaitingState) Enter(room *Room) {
    room.Broadcast(&Message{
        Type:    "STATE_WAITING",
        Content: "等待其他玩家准备...",
    })
}

func (s *WaitingState) HandleMessage(room *Room, player *Player, msg *Message) {
    switch msg.Type {
    case "PLAYER_READY":
        s.readyPlayers[player.ID] = true
        if len(s.readyPlayers) == 3 {
            room.ChangeState(NewDealingState())
        }
    }
}

​发牌状态示例​​:

// game/dealing_state.go
package game

type DealingState struct {
    BaseState
    timer *time.Timer
}

func NewDealingState() *DealingState {
    return &DealingState{}
}

func (s *DealingState) Enter(room *Room) {
    // 执行发牌逻辑
    room.DealCards()
    
    // 设置状态超时
    s.timer = time.AfterFunc(5*time.Second, func() {
        room.ChangeState(NewBiddingState())
    })
}

func (s *DealingState) Exit(room *Room) {
    s.timer.Stop()
}

2.3 状态上下文管理

游戏房间作为状态机的上下文:

// game/room.go
package game

type Room struct {
    currentState State
    players      [3]*Player
    stateMutex   sync.Mutex
    // ...其他房间属性
}

func (r *Room) ChangeState(newState State) {
    r.stateMutex.Lock()
    defer r.stateMutex.Unlock()
    
    if r.currentState != nil {
        r.currentState.Exit(r)
    }
    
    r.currentState = newState
    r.currentState.Enter(r)
}

func (r *Room) HandleMessage(player *Player, msg *Message) {
    r.stateMutex.Lock()
    defer r.stateMutex.Unlock()
    
    if r.currentState != nil {
        r.currentState.HandleMessage(r, player, msg)
    }
}

三、网络通信设计

3.1 消息协议定义

// network/message.go
package network

type MessageType string

const (
    MsgStateChange  MessageType = "STATE_CHANGE"
    MsgPlayerAction MessageType = "PLAYER_ACTION"
    // ...其他消息类型
)

type Message struct {
    Type    MessageType   `json:"type"`
    Content interface{}   `json:"content"`
    PlayerID int          `json:"player_id,omitempty"`
}

3.2 WebSocket集成

// network/server.go
package network

func StartServer() {
    http.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
        conn, err := upgrader.Upgrade(w, r, nil)
        if err != nil {
            log.Println("Upgrade error:", err)
            return
        }
        
        go handleConnection(conn)
    })
    
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func handleConnection(conn *websocket.Conn) {
    player := game.NewPlayer()
    defer conn.Close()
    
    for {
        _, msgBytes, err := conn.ReadMessage()
        if err != nil {
            break
        }
        
        var msg Message
        if err := json.Unmarshal(msgBytes, &msg); err != nil {
            continue
        }
        
        room := game.GetRoomByPlayer(player.ID)
        if room != nil {
            room.HandleMessage(player, &msg)
        }
    }
}

四、关键游戏逻辑实现

4.1 叫地主状态实现

// game/bidding_state.go
package game

type BiddingState struct {
    BaseState
    currentBidder int
    maxBid        int
    passCount     int
    timer         *time.Timer
}

func (s *BiddingState) Enter(room *Room) {
    s.currentBidder = room.GetNextPlayer(-1)
    s.maxBid = 0
    s.passCount = 0
    
    room.Broadcast(&Message{
        Type: "BIDDING_START",
        Content: map[string]interface{}{
            "currentPlayer": s.currentBidder,
            "maxBid":       s.maxBid,
        },
    })
    
    s.timer = time.AfterFunc(15*time.Second, s.onBidTimeout)
}

func (s *BiddingState) HandleMessage(room *Room, player *Player, msg *Message) {
    if msg.Type != "PLAYER_BID" || player.ID != s.currentBidder {
        return
    }
    
    bid := msg.Content.(int)
    if bid > s.maxBid && bid <= 3 {
        s.maxBid = bid
        s.passCount = 0
    } else if bid == 0 { // 0表示不叫
        s.passCount++
    }
    
    if s.passCount >= 2 && s.maxBid > 0 {
        room.SetLandlord(s.currentBidder, s.maxBid)
        room.ChangeState(NewPlayingState())
        return
    }
    
    s.currentBidder = room.GetNextPlayer(s.currentBidder)
    room.BroadcastBidInfo()
    
    // 重置计时器
    s.timer.Reset(15 * time.Second)
}

4.2 出牌状态实现

// game/playing_state.go
package game

type PlayingState struct {
    BaseState
    currentPlayer  int
    lastCards      []Card
    lastPlayer     int
    timer          *time.Timer
    consecutivePass int
}

func (s *PlayingState) HandleMessage(room *Room, player *Player, msg *Message) {
    if msg.Type != "PLAY_CARDS" || player.ID != s.currentPlayer {
        return
    }
    
    cards := parseCards(msg.Content)
    if !room.IsValidPlay(player.ID, cards, s.lastCards, s.lastPlayer) {
        player.SendError("出牌不合法")
        return
    }
    
    if len(cards) > 0 {
        s.lastCards = cards
        s.lastPlayer = player.ID
        s.consecutivePass = 0
        room.BroadcastPlay(player.ID, cards)
        
        if player.CardsLeft() == 0 {
            room.ChangeState(NewGameOverState(player.ID))
            return
        }
    } else {
        s.consecutivePass++
        if s.consecutivePass >= 2 {
            s.lastCards = nil
            s.lastPlayer = -1
        }
    }
    
    s.currentPlayer = room.GetNextPlayer(s.currentPlayer)
    room.BroadcastTurn(s.currentPlayer)
    s.timer.Reset(30 * time.Second)
}

五、并发控制与性能优化

5.1 状态安全的并发访问

// game/room.go
func (r *Room) processMessage(player *Player, msg *Message) {
    // 使用带缓冲的通道处理消息
    select {
    case r.messageQueue <- messageWrapper{player, msg}:
    default:
        player.SendError("服务器繁忙")
    }
}

func (r *Room) startMessageLoop() {
    go func() {
        for wrapper := range r.messageQueue {
            r.HandleMessage(wrapper.player, wrapper.msg)
        }
    }()
}

5.2 定时器管理优化

// game/state.go
type StateBase struct {
    timers []*time.Timer
}

func (s *StateBase) AddTimer(d time.Duration, f func()) {
    timer := time.AfterFunc(d, f)
    s.timers = append(s.timers, timer)
}

func (s *StateBase) ClearTimers() {
    for _, t := range s.timers {
        t.Stop()
    }
    s.timers = nil
}

六、测试与调试

6.1 状态机单元测试

// game/state_test.go
func TestBiddingState(t *testing.T) {
    room := NewTestRoom()
    state := NewBiddingState()
    
    // 测试正常叫分流程
    state.Enter(room)
    state.HandleMessage(room, room.players[0], &Message{Type: "PLAYER_BID", Content: 1})
    assert.Equal(t, 1, state.maxBid)
    
    // 测试超时处理
    state.onBidTimeout()
    assert.True(t, room.currentState instanceof.GameOverState)
}

6.2 集成测试方案

func TestFullGameFlow(t *testing.T) {
    // 创建测试房间和玩家
    room := NewTestRoom()
    players := []*TestPlayer{NewTestPlayer(), NewTestPlayer(), NewTestPlayer()}
    
    // 模拟完整游戏流程
    room.Join(players[0])
    room.Join(players[1])
    room.Join(players[2])
    
    // 所有玩家准备
    for _, p := range players {
        room.HandleMessage(p, &Message{Type: "PLAYER_READY"})
    }
    
    // 验证状态转移
    assert.IsType(t, &DealingState{}, room.currentState)
    
    // ...继续模拟后续游戏流程
}

七、扩展与演进

7.1 支持断线重连

// game/playing_state.go
func (s *PlayingState) HandleReconnect(player *Player) {
    player.Send(&Message{
        Type: "GAME_SNAPSHOT",
        Content: map[string]interface{}{
            "state":        "PLAYING",
            "current":     s.currentPlayer,
            "lastCards":   s.lastCards,
            "lastPlayer":  s.lastPlayer,
            "yourCards":   player.Cards(),
        },
    })
}

7.2 状态持久化

// game/room.go
func (r *Room) SaveState() (*RoomState, error) {
    state := &RoomState{
        StateName: reflect.TypeOf(r.currentState).Name(),
        Players:   r.players,
        // 其他需要保存的属性
    }
    
    if savable, ok := r.currentState.(StateSaver); ok {
        state.StateData = savable.Save()
    }
    
    return state, nil
}

总结:FSM在游戏服务端中的价值

通过有限状态机架构实现的斗地主服务端具有以下优势:

  1. ​清晰的逻辑划分​​:每个游戏阶段都有明确的状态对应
  2. ​易于扩展维护​​:新增游戏规则或状态不会影响现有逻辑
  3. ​稳定的时序控制​​:状态转移机制确保游戏流程正确性
  4. ​良好的可测试性​​:每个状态可以独立测试

Go语言的并发特性与FSM模式相得益彰,通过goroutine和channel可以优雅地处理游戏中的异步事件。这种架构不仅适用于斗地主,也可以推广到其他棋牌类游戏甚至更复杂的游戏系统中。

随着游戏逻辑复杂度的增加,可以考虑在FSM基础上引入分层状态机、行为树等更高级的技术,但FSM始终是游戏服务端开发中最基础、最可靠的设计模式之一。

滚动至顶部