8. 棋牌游戏通用框架
2026/4/14大约 6 分钟
棋牌游戏通用框架
棋牌游戏看似种类繁多,但底层架构高度相似:都有规则引擎、回合管理、玩家操作验证和结果判定。本章设计一套可复用的棋牌游戏通用框架。
8.1 框架整体架构
GameController(游戏控制器)
├── RuleEngine(规则引擎) ← 纯逻辑,无 UI
├── TurnManager(回合管理器)
├── PlayerManager(玩家管理器)
│ ├── HumanPlayer(人类玩家)
│ └── AIPlayer(AI 玩家)
├── NetworkManager(网络管理器)← 可选
└── SpectatorSystem(观战系统)← 可选8.2 规则引擎
规则引擎是框架的核心,负责验证操作合法性、计算结果、判断游戏结束条件。它是纯逻辑代码,不依赖任何 UI 或网络,便于单独测试。
C#
using Godot;
using System.Collections.Generic;
// 游戏操作的基类(玩家的每一步操作)
public abstract class GameAction
{
public int PlayerId;
public long Timestamp;
}
// 游戏状态基类(棋盘/牌局的完整状态快照)
public abstract class GameState
{
public int CurrentPlayer;
public int TurnNumber;
public bool IsGameOver;
public int Winner = -1; // -1 表示无胜者(平局或未结束)
public abstract GameState Clone(); // 深拷贝,用于 AI 模拟
}
// 规则引擎接口
public interface IRuleEngine<TState, TAction>
where TState : GameState
where TAction : GameAction
{
// 验证操作是否合法
bool IsValidAction(TState state, TAction action);
// 执行操作,返回新状态(不修改原状态)
TState ApplyAction(TState state, TAction action);
// 获取当前玩家所有合法操作
List<TAction> GetLegalActions(TState state, int playerId);
// 判断游戏是否结束,并设置胜者
bool CheckGameOver(TState state);
}
// 五子棋规则引擎示例
public class GomokuRuleEngine : IRuleEngine<GomokuState, GomokuAction>
{
private const int BOARD_SIZE = 15;
private const int WIN_COUNT = 5;
public bool IsValidAction(GomokuState state, GomokuAction action)
{
if (state.IsGameOver) return false;
if (action.PlayerId != state.CurrentPlayer) return false;
if (action.Row < 0 || action.Row >= BOARD_SIZE) return false;
if (action.Col < 0 || action.Col >= BOARD_SIZE) return false;
return state.Board[action.Row, action.Col] == 0; // 该位置必须为空
}
public GomokuState ApplyAction(GomokuState state, GomokuAction action)
{
var newState = (GomokuState)state.Clone();
newState.Board[action.Row, action.Col] = action.PlayerId;
newState.TurnNumber++;
newState.CurrentPlayer = 3 - state.CurrentPlayer; // 1→2, 2→1 切换
CheckGameOver(newState);
return newState;
}
public List<GomokuAction> GetLegalActions(GomokuState state, int playerId)
{
var actions = new List<GomokuAction>();
for (int r = 0; r < BOARD_SIZE; r++)
for (int c = 0; c < BOARD_SIZE; c++)
if (state.Board[r, c] == 0)
actions.Add(new GomokuAction { PlayerId = playerId, Row = r, Col = c });
return actions;
}
public bool CheckGameOver(GomokuState state)
{
// 检查最后一次落子是否形成五连
for (int r = 0; r < BOARD_SIZE; r++)
{
for (int c = 0; c < BOARD_SIZE; c++)
{
int player = state.Board[r, c];
if (player == 0) continue;
if (CheckLine(state.Board, r, c, 1, 0, player) || // 横
CheckLine(state.Board, r, c, 0, 1, player) || // 竖
CheckLine(state.Board, r, c, 1, 1, player) || // 斜
CheckLine(state.Board, r, c, 1, -1, player)) // 反斜
{
state.IsGameOver = true;
state.Winner = player;
return true;
}
}
}
return false;
}
private bool CheckLine(int[,] board, int startR, int startC, int dr, int dc, int player)
{
int count = 0;
for (int i = 0; i < WIN_COUNT; i++)
{
int r = startR + i * dr;
int c = startC + i * dc;
if (r < 0 || r >= BOARD_SIZE || c < 0 || c >= BOARD_SIZE) break;
if (board[r, c] == player) count++;
else break;
}
return count == WIN_COUNT;
}
}
public class GomokuState : GameState
{
public int[,] Board = new int[15, 15];
public override GameState Clone()
{
var clone = new GomokuState
{
CurrentPlayer = CurrentPlayer,
TurnNumber = TurnNumber,
IsGameOver = IsGameOver,
Winner = Winner,
};
System.Array.Copy(Board, clone.Board, Board.Length);
return clone;
}
}
public class GomokuAction : GameAction
{
public int Row, Col;
}GDScript
# rule_engine.gd - 规则引擎基类
class_name RuleEngine
func is_valid_action(state: Dictionary, action: Dictionary) -> bool:
return false
func apply_action(state: Dictionary, action: Dictionary) -> Dictionary:
return state.duplicate(true)
func get_legal_actions(state: Dictionary, player_id: int) -> Array:
return []
func check_game_over(state: Dictionary) -> bool:
return false
# ---
# gomoku_rule_engine.gd - 五子棋规则引擎
class_name GomokuRuleEngine extends RuleEngine
const BOARD_SIZE := 15
const WIN_COUNT := 5
func is_valid_action(state: Dictionary, action: Dictionary) -> bool:
if state.get("is_game_over", false): return false
if action.get("player_id") != state.get("current_player"): return false
var r: int = action.get("row", -1)
var c: int = action.get("col", -1)
if r < 0 or r >= BOARD_SIZE or c < 0 or c >= BOARD_SIZE: return false
return state.board[r][c] == 0
func apply_action(state: Dictionary, action: Dictionary) -> Dictionary:
var new_state := state.duplicate(true)
new_state.board[action.row][action.col] = action.player_id
new_state.turn_number += 1
new_state.current_player = 3 - state.current_player
check_game_over(new_state)
return new_state
func check_game_over(state: Dictionary) -> bool:
for r in BOARD_SIZE:
for c in BOARD_SIZE:
var player: int = state.board[r][c]
if player == 0: continue
if _check_line(state.board, r, c, 1, 0, player) or \
_check_line(state.board, r, c, 0, 1, player) or \
_check_line(state.board, r, c, 1, 1, player) or \
_check_line(state.board, r, c, 1, -1, player):
state.is_game_over = true
state.winner = player
return true
return false
func _check_line(board: Array, sr: int, sc: int, dr: int, dc: int, player: int) -> bool:
var count := 0
for i in WIN_COUNT:
var r := sr + i * dr
var c := sc + i * dc
if r < 0 or r >= BOARD_SIZE or c < 0 or c >= BOARD_SIZE: break
if board[r][c] == player: count += 1
else: break
return count == WIN_COUNT8.3 回合管理器
C#
using Godot;
using System.Collections.Generic;
public partial class TurnManager : Node
{
[Signal] public delegate void TurnStartedEventHandler(int playerId);
[Signal] public delegate void TurnEndedEventHandler(int playerId);
[Signal] public delegate void GameOverEventHandler(int winner);
private List<int> _playerOrder = new();
private int _currentIndex = 0;
private float _turnTimeLimit = 30f; // 每回合限时30秒
private float _timeRemaining;
private bool _isRunning = false;
public int CurrentPlayer => _playerOrder.Count > 0 ? _playerOrder[_currentIndex] : -1;
public void StartGame(List<int> playerIds)
{
_playerOrder = new List<int>(playerIds);
_currentIndex = 0;
_isRunning = true;
StartNextTurn();
}
private void StartNextTurn()
{
_timeRemaining = _turnTimeLimit;
EmitSignal(SignalName.TurnStarted, CurrentPlayer);
GD.Print($"[Turn] Player {CurrentPlayer}'s turn, {_turnTimeLimit}s");
}
public override void _Process(double delta)
{
if (!_isRunning) return;
_timeRemaining -= (float)delta;
if (_timeRemaining <= 0)
{
// 超时,强制结束回合(可以自动出牌或跳过)
GD.Print($"[Turn] Player {CurrentPlayer} timed out");
EndTurn();
}
}
// 结束当前回合,切换到下一玩家
public void EndTurn()
{
EmitSignal(SignalName.TurnEnded, CurrentPlayer);
_currentIndex = (_currentIndex + 1) % _playerOrder.Count;
StartNextTurn();
}
public float GetTimeRemaining() => _timeRemaining;
public float GetTimeProgress() => _timeRemaining / _turnTimeLimit;
}GDScript
extends Node
class_name TurnManager
signal turn_started(player_id: int)
signal turn_ended(player_id: int)
signal game_over(winner: int)
var _player_order: Array[int] = []
var _current_index: int = 0
var _turn_time_limit: float = 30.0
var _time_remaining: float = 0.0
var _is_running: bool = false
var current_player: int:
get: return _player_order[_current_index] if _player_order.size() > 0 else -1
func start_game(player_ids: Array[int]) -> void:
_player_order = player_ids.duplicate()
_current_index = 0
_is_running = true
_start_next_turn()
func _start_next_turn() -> void:
_time_remaining = _turn_time_limit
turn_started.emit(current_player)
func _process(delta: float) -> void:
if not _is_running: return
_time_remaining -= delta
if _time_remaining <= 0:
print("[Turn] Player %d timed out" % current_player)
end_turn()
func end_turn() -> void:
turn_ended.emit(current_player)
_current_index = (_current_index + 1) % _player_order.size()
_start_next_turn()
func get_time_remaining() -> float: return _time_remaining
func get_time_progress() -> float: return _time_remaining / _turn_time_limit8.4 观战系统
观战系统让非参赛玩家能实时观看对局。
C#
using Godot;
using System.Collections.Generic;
public partial class SpectatorSystem : Node
{
private List<long> _spectators = new(); // 观战者的网络 ID
private string _latestStateSnapshot = "";
// 玩家申请加入观战
[Rpc(MultiplayerApi.RpcMode.AnyPeer)]
public void RequestSpectate()
{
if (!Multiplayer.IsServer()) return;
long spectatorId = Multiplayer.GetRemoteSenderId();
if (!_spectators.Contains(spectatorId))
{
_spectators.Add(spectatorId);
GD.Print($"[Spectator] {spectatorId} joined as spectator");
}
// 发送当前游戏状态给新观战者
if (!string.IsNullOrEmpty(_latestStateSnapshot))
RpcId(spectatorId, nameof(ReceiveGameState), _latestStateSnapshot);
}
// 广播游戏状态给所有观战者
public void BroadcastToSpectators(string stateJson)
{
_latestStateSnapshot = stateJson;
foreach (long id in _spectators)
RpcId(id, nameof(ReceiveGameState), stateJson);
}
// 观战者接收游戏状态并更新 UI
[Rpc(MultiplayerApi.RpcMode.Authority)]
public void ReceiveGameState(string stateJson)
{
GD.Print($"[Spectator] Received state update");
// 更新观战界面(只读,无操作权限)
UpdateSpectatorUI(stateJson);
}
private void UpdateSpectatorUI(string stateJson)
{
// 根据状态更新棋盘显示
}
}GDScript
extends Node
class_name SpectatorSystem
var _spectators: Array[int] = []
var _latest_snapshot: String = ""
@rpc("any_peer")
func request_spectate() -> void:
if not multiplayer.is_server(): return
var spectator_id := multiplayer.get_remote_sender_id()
if not spectator_id in _spectators:
_spectators.append(spectator_id)
print("[Spectator] %d joined as spectator" % spectator_id)
if _latest_snapshot != "":
rpc_id(spectator_id, "receive_game_state", _latest_snapshot)
func broadcast_to_spectators(state_json: String) -> void:
_latest_snapshot = state_json
for id in _spectators:
rpc_id(id, "receive_game_state", state_json)
@rpc("authority")
func receive_game_state(state_json: String) -> void:
_update_spectator_ui(state_json)
func _update_spectator_ui(_state_json: String) -> void:
pass # 子类实现具体 UI 更新8.5 框架使用示例
以下展示如何用这套框架快速搭建一个五子棋游戏:
# game_controller.gd
extends Node
var rule_engine := GomokuRuleEngine.new()
var turn_manager := TurnManager.new()
var game_state: Dictionary
func _ready() -> void:
add_child(turn_manager)
turn_manager.turn_started.connect(_on_turn_started)
# 初始化游戏状态
game_state = {
"board": [],
"current_player": 1,
"turn_number": 0,
"is_game_over": false,
"winner": -1,
}
for i in 15:
game_state.board.append([])
for j in 15:
game_state.board[i].append(0)
turn_manager.start_game([1, 2])
func player_place_piece(row: int, col: int) -> void:
var action := { "player_id": game_state.current_player, "row": row, "col": col }
if not rule_engine.is_valid_action(game_state, action):
return
game_state = rule_engine.apply_action(game_state, action)
_update_board_display()
if game_state.is_game_over:
print("Player %d wins!" % game_state.winner)
else:
turn_manager.end_turn()
func _on_turn_started(player_id: int) -> void:
print("Player %d's turn" % player_id)框架扩展
这套框架可以轻松扩展到其他棋牌游戏:
- 麻将:继承
RuleEngine,实现碰、杠、和牌等规则 - 扑克:实现手牌管理、出牌顺序、组合比较等逻辑
- 围棋:实现提子、气的计算、劫争判断等规则
