4. 麻将核心规则引擎
2026/4/13大约 13 分钟
麻将核心规则引擎
前面的章节让我们的麻将游戏"长了个好看的外表",现在该给它装上"大脑"了。规则引擎就是游戏的灵魂——它决定了谁能胡牌、什么操作是合法的、怎么计分。
生活化比喻
如果把麻将游戏比作一个裁判,规则引擎就是裁判的"规则手册"。当玩家打出一张牌时,规则引擎会翻看手册,判断:是否有人能碰?是否有人能胡?操作是否合法?
牌的数据结构
枚举定义
首先,我们需要在代码中定义"一张牌长什么样":
C
/// <summary>
/// 花色类型
/// </summary>
public enum SuitType
{
/// <summary>万子</summary>
Wan = 0,
/// <summary>条子</summary>
Tiao = 1,
/// <summary>筒子</summary>
Tong = 2,
/// <summary>风牌</summary>
Feng = 3,
/// <summary>箭牌</summary>
Jian = 4
}
/// <summary>
/// 风牌的具体值
/// </summary>
public enum WindValue
{
East = 1, // 东
South = 2, // 南
West = 3, // 西
North = 4 // 北
}
/// <summary>
/// 箭牌的具体值
/// </summary>
public enum DragonValue
{
Zhong = 1, // 中(红中)
Fa = 2, // 发(发财)
Bai = 3 // 白(白板)
}
/// <summary>
/// 单张麻将牌
/// </summary>
public class MahjongTile
{
/// <summary>花色</summary>
public SuitType Suit { get; }
/// <summary>数值(数牌1-9,风牌1-4,箭牌1-3)</summary>
public int Value { get; }
/// <summary>唯一ID(区分同花色同数值的4张牌)</summary>
public int TileId { get; }
/// <summary>
/// 构造一张牌
/// </summary>
public MahjongTile(SuitType suit, int value, int tileId)
{
Suit = suit;
Value = value;
TileId = tileId;
}
/// <summary>
/// 是否为数牌(万/条/筒)
/// </summary>
public bool IsNumberTile => Suit <= SuitType.Tong;
/// <summary>
/// 是否为字牌(风牌/箭牌)
/// </summary>
public bool IsHonorTile => Suit >= SuitType.Feng;
/// <summary>
/// 获取显示名称
/// </summary>
public string DisplayName
{
get
{
string suitName = Suit switch
{
SuitType.Wan => "万",
SuitType.Tiao => "条",
SuitType.Tong => "筒",
SuitType.Feng => new[] { "", "东", "南", "西", "北" }[Value],
SuitType.Jian => new[] { "", "中", "发", "白" }[Value],
_ => "?"
};
return IsNumberTile ? $"{Value}{suitName}" : suitName;
}
}
/// <summary>
/// 判断两张牌是否"相同"(花色和数值一样,不比较ID)
/// </summary>
public bool IsSameAs(MahjongTile other)
{
return Suit == other.Suit && Value == other.Value;
}
public override string ToString() => $"[{TileId}]{DisplayName}";
}GDScript
## 花色类型
enum SuitType {
WAN = 0, ## 万子
TIAO = 1, ## 条子
TONG = 2, ## 筒子
FENG = 3, ## 风牌
JIAN = 4 ## 箭牌
}
## 单张麻将牌
class_name MahjongTile
## 花色
var suit: int ## SuitType 枚举值
## 数值
var value: int
## 唯一ID
var tile_id: int
func _init(suit_val: int, value_val: int, id_val: int) -> void:
suit = suit_val
value = value_val
tile_id = id_val
## 是否为数牌
func is_number_tile() -> bool:
return suit <= SuitType.TONG
## 是否为字牌
func is_honor_tile() -> bool:
return suit >= SuitType.FENG
## 获取显示名称
func display_name() -> String:
var suit_names = ["万", "条", "筒"]
var wind_names = ["", "东", "南", "西", "北"]
var dragon_names = ["", "中", "发", "白"]
if is_number_tile():
return str(value) + suit_names[suit]
elif suit == SuitType.FENG:
return wind_names[value]
else:
return dragon_names[value]
## 判断两张牌是否相同
func is_same_as(other: MahjongTile) -> bool:
return suit == other.suit and value == other.value
func _to_string() -> String:
return "[%d]%s" % [tile_id, display_name()]牌型组合定义
组合类型
麻将中的"组合"是判断胡牌的基础。我们需要定义几种基本的组合:
| 组合类型 | 中文名 | 说明 | 示例 |
|---|---|---|---|
| Pair | 对子 | 两张相同的牌 | 五万+五万 |
| Triplet | 刻子 | 三张相同的牌 | 东风+东风+东风 |
| Sequence | 顺子 | 三张同花色连续的牌 | 三万+四万+五万 |
C
/// <summary>
/// 组合类型
/// </summary>
public enum MeldType
{
Pair, // 对子(将)
Triplet, // 刻子
Sequence // 顺子
}
/// <summary>
/// 一个牌型组合(对子/刻子/顺子)
/// </summary>
public class Meld
{
/// <summary>组合类型</summary>
public MeldType Type { get; }
/// <summary>组合中的牌</summary>
public List<MahjongTile> Tiles { get; }
/// <summary>来源:手牌/碰/杠/吃</summary>
public string Source { get; set; }
public Meld(MeldType type, List<MahjongTile> tiles)
{
Type = type;
Tiles = tiles;
Source = "hand";
}
/// <summary>
/// 创建对子
/// </summary>
public static Meld CreatePair(MahjongTile tile1, MahjongTile tile2)
{
return new Meld(MeldType.Pair, new List<MahjongTile> { tile1, tile2 });
}
/// <summary>
/// 创建刻子
/// </summary>
public static Meld CreateTriplet(MahjongTile t1, MahjongTile t2, MahjongTile t3)
{
return new Meld(MeldType.Triplet, new List<MahjongTile> { t1, t2, t3 });
}
/// <summary>
/// 创建顺子
/// </summary>
public static Meld CreateSequence(MahjongTile t1, MahjongTile t2, MahjongTile t3)
{
return new Meld(MeldType.Sequence, new List<MahjongTile> { t1, t2, t3 });
}
public override string ToString()
{
var tilesStr = string.Join("+", Tiles.Select(t => t.DisplayName));
return $"{Type}: {tilesStr}";
}
}GDScript
## 组合类型
enum MeldType {
PAIR, ## 对子
TRIPLET, ## 刻子
SEQUENCE ## 顺子
}
## 一个牌型组合
class_name Meld
## 组合类型
var type: int ## MeldType 枚举值
## 组合中的牌
var tiles: Array[MahjongTile]
## 来源
var source: String = "hand"
func _init(meld_type: int, meld_tiles: Array[MahjongTile]) -> void:
type = meld_type
tiles = meld_tiles
## 创建对子
static func create_pair(t1: MahjongTile, t2: MahjongTile) -> Meld:
return Meld.new(MeldType.PAIR, [t1, t2])
## 创建刻子
static func create_triplet(t1: MahjongTile, t2: MahjongTile, t3: MahjongTile) -> Meld:
return Meld.new(MeldType.TRIPLET, [t1, t2, t3])
## 创建顺子
static func create_sequence(t1: MahjongTile, t2: MahjongTile, t3: MahjongTile) -> Meld:
return Meld.new(MeldType.SEQUENCE, [t1, t2, t3])
func _to_string() -> String:
var names := []
for t in tiles:
names.append(t.display_name())
return "%s: %s" % [type, "+".join(names)]摸牌打牌流程
游戏状态管理
一局麻将有很多状态——轮到谁摸牌、牌堆还剩多少、当前是什么阶段等。我们需要一个"状态管理器"来追踪这些信息:
C
/// <summary>
/// 游戏阶段
/// </summary>
public enum GamePhase
{
Waiting, // 等待开始
Dealing, // 发牌中
Drawing, // 摸牌阶段
Discarding, // 打牌阶段
Responding, // 等待碰/杠/胡响应
Finished // 游戏结束
}
/// <summary>
/// 游戏状态
/// </summary>
public class GameState
{
/// <summary>当前阶段</summary>
public GamePhase Phase { get; set; } = GamePhase.Waiting;
/// <summary>当前轮到的玩家(0-3)</summary>
public int CurrentPlayer { get; set; }
/// <summary>庄家编号</summary>
public int Dealer { get; set; } = 0;
/// <summary>牌堆</summary>
public List<MahjongTile> Wall { get; set; } = new();
/// <summary>弃牌堆(每个玩家的)</summary>
public Dictionary<int, List<MahjongTile>> DiscardPiles { get; set; } = new();
/// <summary>每个玩家的手牌</summary>
public Dictionary<int, List<MahjongTile>> Hands { get; set; } = new();
/// <summary>每个玩家的明牌(碰/杠/吃的组合)</summary>
public Dictionary<int, List<Meld>> Melds { get; set; } = new();
/// <summary>最后打出的一张牌</summary>
public MahjongTile LastDiscard { get; set; }
/// <summary>最后打牌的玩家</summary>
public int LastDiscardPlayer { get; set; }
/// <summary>
/// 初始化一局游戏
/// </summary>
public void Initialize(List<MahjongTile> allTiles)
{
Phase = GamePhase.Dealing;
CurrentPlayer = Dealer;
// 初始化牌堆
Wall = new List<MahjongTile>(allTiles);
// 初始化4个玩家的手牌和弃牌堆
Hands = new Dictionary<int, List<MahjongTile>>();
DiscardPiles = new Dictionary<int, List<MahjongTile>>();
Melds = new Dictionary<int, List<Meld>>();
for (int i = 0; i < 4; i++)
{
Hands[i] = new List<MahjongTile>();
DiscardPiles[i] = new List<MahjongTile>();
Melds[i] = new List<Meld>();
}
// 发牌:每人13张(庄家14张)
for (int round = 0; round < 13; round++)
{
for (int player = 0; player < 4; player++)
{
Hands[player].Add(DrawFromWall());
}
}
// 庄家多摸一张
Hands[Dealer].Add(DrawFromWall());
Phase = GamePhase.Discarding;
}
/// <summary>
/// 从牌堆摸一张牌
/// </summary>
public MahjongTile DrawFromWall()
{
if (Wall.Count == 0)
{
return null; // 牌堆空了,流局
}
var tile = Wall[0];
Wall.RemoveAt(0);
return tile;
}
/// <summary>
/// 摸牌操作
/// </summary>
public MahjongTile PlayerDraw(int player)
{
if (Phase != GamePhase.Drawing)
{
GD.PrintErr($"当前阶段 {Phase} 不能摸牌");
return null;
}
var tile = DrawFromWall();
if (tile == null) return null;
Hands[player].Add(tile);
Phase = GamePhase.Discarding;
return tile;
}
/// <summary>
/// 打牌操作
/// </summary>
public bool PlayerDiscard(int player, MahjongTile tile)
{
if (Phase != GamePhase.Discarding || player != CurrentPlayer)
{
return false;
}
// 从手牌中移除
if (!Hands[player].Remove(tile))
{
GD.PrintErr("手牌中没有这张牌");
return false;
}
// 放入弃牌堆
DiscardPiles[player].Add(tile);
LastDiscard = tile;
LastDiscardPlayer = player;
// 切换到响应阶段,检查是否有人碰/杠/胡
Phase = GamePhase.Responding;
return true;
}
}GDScript
## 游戏阶段
enum GamePhase {
WAITING, ## 等待开始
DEALING, ## 发牌中
DRAWING, ## 摸牌阶段
DISCARDING, ## 打牌阶段
RESPONDING, ## 等待碰杠胡响应
FINISHED ## 游戏结束
}
## 游戏状态
class_name GameState
## 当前阶段
var phase: int = GamePhase.WAITING
## 当前玩家
var current_player: int
## 庄家
var dealer: int = 0
## 牌堆
var wall: Array[MahjongTile] = []
## 弃牌堆
var discard_piles: Dictionary = {}
## 手牌
var hands: Dictionary = {}
## 明牌
var melds: Dictionary = {}
## 最后打出的牌
var last_discard: MahjongTile
## 最后打牌的玩家
var last_discard_player: int
## 初始化一局游戏
func initialize(all_tiles: Array[MahjongTile]) -> void:
phase = GamePhase.DEALING
current_player = dealer
wall = all_tiles.duplicate()
hands = {}
discard_piles = {}
melds = {}
for i in range(4):
hands[i] = []
discard_piles[i] = []
melds[i] = []
# 发牌:每人13张
for round_i in range(13):
for player in range(4):
hands[player].append(draw_from_wall())
# 庄家多摸一张
hands[dealer].append(draw_from_wall())
phase = GamePhase.DISCARDING
## 从牌堆摸一张牌
func draw_from_wall() -> MahjongTile:
if wall.is_empty():
return null
var tile = wall[0]
wall.pop_front()
return tile
## 摸牌操作
func player_draw(player: int) -> MahjongTile:
if phase != GamePhase.DRAWING:
push_error("当前阶段不能摸牌")
return null
var tile = draw_from_wall()
if tile == null:
return null
hands[player].append(tile)
phase = GamePhase.DISCARDING
return tile
## 打牌操作
func player_discard(player: int, tile: MahjongTile) -> bool:
if phase != GamePhase.DISCARDING or player != current_player:
return false
if not hands[player].has(tile):
push_error("手牌中没有这张牌")
return false
hands[player].erase(tile)
discard_piles[player].append(tile)
last_discard = tile
last_discard_player = player
phase = GamePhase.RESPONDING
return true吃碰杠操作
吃牌规则
| 操作 | 条件 | 来源 |
|---|---|---|
| 吃 | 上家打的牌 + 自己手里的两张牌 = 一个顺子 | 只能吃上家的牌 |
| 碰 | 任何人打的牌 + 自己手里的两张同牌 = 一个刻子 | 任何人的牌 |
| 杠 | 自己摸的牌 + 手里三张同牌 = 四张一组 | 摸牌时或别人打牌时 |
吃牌限制
吃牌只能吃"上家"(你左边的人)打的牌。你不能吃对家或下家的牌。这是麻将的一个重要规则——它保证了游戏有足够的策略空间。
吃碰杠检测代码
C
/// <summary>
/// 吃碰杠检测器
/// </summary>
public partial class MeldChecker : Node
{
/// <summary>
/// 检测能否吃某张牌
/// </summary>
/// <param name="hand">手牌</param>
/// <param name="tile">要吃的牌</param>
/// <returns>所有可能的吃法</returns>
public List<List<MahjongTile>> CheckChi(List<MahjongTile> hand, MahjongTile tile)
{
var results = new List<List<MahjongTile>>();
// 只有数牌才能吃
if (!tile.IsNumberTile) return results;
// 找出手中同花色的牌
var sameSuit = hand.Where(t => t.Suit == tile.Suit).ToList();
// 检查三种顺子组合
// 组合1:tile-2, tile-1, tile(需要手里有 tile-2 和 tile-1)
TryAddChiCombo(results, sameSuit, tile, -2, -1);
// 组合2:tile-1, tile, tile+1(需要手里有 tile-1 和 tile+1)
TryAddChiCombo(results, sameSuit, tile, -1, 1);
// 组合3:tile, tile+1, tile+2(需要手里有 tile+1 和 tile+2)
TryAddChiCombo(results, sameSuit, tile, 1, 2);
return results;
}
private void TryAddChiCombo(List<List<MahjongTile>> results,
List<MahjongTile> sameSuit, MahjongTile tile, int offset1, int offset2)
{
int v1 = tile.Value + offset1;
int v2 = tile.Value + offset2;
// 检查数值范围(1-9)
if (v1 < 1 || v1 > 9 || v2 < 1 || v2 > 9) return;
// 检查手里是否有对应的牌
var t1 = sameSuit.FirstOrDefault(t => t.Value == v1);
var t2 = sameSuit.FirstOrDefault(t => t.Value == v2);
if (t1 != null && t2 != null)
{
results.Add(new List<MahjongTile> { t1, tile, t2 });
}
}
/// <summary>
/// 检测能否碰某张牌
/// </summary>
/// <returns>如果能碰,返回碰牌组合;否则返回null</returns>
public List<MahjongTile> CheckPong(List<MahjongTile> hand, MahjongTile tile)
{
// 找出手中有两张和 tile 相同的牌
var matches = hand.Where(t => t.IsSameAs(tile)).Take(2).ToList();
if (matches.Count == 2)
{
matches.Add(tile);
return matches;
}
return null;
}
/// <summary>
/// 检测能否杠某张牌
/// </summary>
public List<MahjongTile> CheckKong(List<MahjongTile> hand, MahjongTile tile)
{
// 找出手中有三张和 tile 相同的牌
var matches = hand.Where(t => t.IsSameAs(tile)).Take(3).ToList();
if (matches.Count == 3)
{
matches.Add(tile);
return matches;
}
return null;
}
}GDScript
extends Node
## 检测能否吃某张牌
func check_chi(hand: Array[MahjongTile], tile: MahjongTile) -> Array:
var results: Array = []
# 只有数牌才能吃
if not tile.is_number_tile():
return results
# 找出手中同花色的牌
var same_suit := hand.filter(func(t): return t.suit == tile.suit)
# 检查三种顺子组合
try_add_chi_combo(results, same_suit, tile, -2, -1)
try_add_chi_combo(results, same_suit, tile, -1, 1)
try_add_chi_combo(results, same_suit, tile, 1, 2)
return results
func try_add_chi_combo(results: Array, same_suit: Array, tile: MahjongTile,
offset1: int, offset2: int) -> void:
var v1 := tile.value + offset1
var v2 := tile.value + offset2
if v1 < 1 or v1 > 9 or v2 < 1 or v2 > 9:
return
var t1 = same_suit.filter(func(t): return t.value == v1)
var t2 = same_suit.filter(func(t): return t.value == v2)
if t1.size() > 0 and t2.size() > 0:
results.append([t1[0], tile, t2[0]])
## 检测能否碰某张牌
func check_pong(hand: Array[MahjongTile], tile: MahjongTile) -> Array:
var matches := hand.filter(func(t): return t.is_same_as(tile))
if matches.size() >= 2:
return [matches[0], matches[1], tile]
return []
## 检测能否杠某张牌
func check_kong(hand: Array[MahjongTile], tile: MahjongTile) -> Array:
var matches := hand.filter(func(t): return t.is_same_as(tile))
if matches.size() >= 3:
return [matches[0], matches[1], matches[2], tile]
return []胡牌判定算法
基本胡牌检测
胡牌判定的核心问题:给14张牌,能不能把它们拆分成 4个组合 + 1个对子?
生活化比喻
胡牌检测就像"拆包裹"——给你14件物品,你要把它们分成5组(4组3件+1组2件),每组必须是"合法的"。3件组可以是三件相同的(刻子)或三件连续同色的(顺子),2件组必须是两件相同的(对子)。
C
/// <summary>
/// 胡牌检测器
/// </summary>
public partial class WinChecker : Node
{
/// <summary>
/// 检测是否胡牌
/// </summary>
/// <param name="tiles">14张手牌</param>
/// <param name="winMelds">胡牌的组合方式(输出参数)</param>
/// <returns>是否胡牌</returns>
public bool CheckWin(List<MahjongTile> tiles, out List<Meld> winMelds)
{
winMelds = new List<Meld>();
if (tiles.Count != 14)
{
return false;
}
// 统计每种牌的数量
var counts = CountTiles(tiles);
// 尝试每一种对子作为"将"
foreach (var kvp in counts.ToList())
{
if (kvp.Value >= 2)
{
// 临时移除对子
counts[kvp.Key] -= 2;
// 检查剩余12张牌能否分成4组
if (CanDecompose(counts, 4))
{
// 恢复对子
counts[kvp.Key] += 2;
// 构建胡牌组合
winMelds = BuildMelds(tiles, kvp.Key);
return true;
}
// 恢复对子
counts[kvp.Key] += 2;
}
}
return false;
}
/// <summary>
/// 统计每种牌的数量
/// </summary>
private Dictionary<string, int> CountTiles(List<MahjongTile> tiles)
{
var counts = new Dictionary<string, int>();
foreach (var tile in tiles)
{
string key = $"{(int)tile.Suit}_{tile.Value}";
if (!counts.ContainsKey(key))
counts[key] = 0;
counts[key]++;
}
return counts;
}
/// <summary>
/// 递归检查:剩余的牌能否分成指定数量的组
/// </summary>
private bool CanDecompose(Dictionary<string, int> counts, int groupsNeeded)
{
if (groupsNeeded == 0)
{
// 所有牌都分配完了
return counts.Values.All(c => c == 0);
}
// 找到第一个还有剩余的牌
string firstKey = counts.FirstOrDefault(kvp => kvp.Value > 0).Key;
if (firstKey == null) return groupsNeeded == 0;
// 尝试作为刻子
if (counts[firstKey] >= 3)
{
counts[firstKey] -= 3;
if (CanDecompose(counts, groupsNeeded - 1))
{
counts[firstKey] += 3;
return true;
}
counts[firstKey] += 3;
}
// 尝试作为顺子(只有数牌才行)
var parts = firstKey.Split('_');
int suit = int.Parse(parts[0]);
int value = int.Parse(parts[1]);
if (suit <= 2 && value <= 7) // 万/条/筒,且数值<=7才能组成顺子
{
string key2 = $"{suit}_{value + 1}";
string key3 = $"{suit}_{value + 2}";
if (counts.GetValueOrDefault(key2, 0) > 0 &&
counts.GetValueOrDefault(key3, 0) > 0)
{
counts[firstKey]--;
counts[key2]--;
counts[key3]--;
if (CanDecompose(counts, groupsNeeded - 1))
{
counts[firstKey]++;
counts[key2]++;
counts[key3]++;
return true;
}
counts[firstKey]++;
counts[key2]++;
counts[key3]++;
}
}
return false;
}
/// <summary>
/// 构建胡牌的组合
/// </summary>
private List<Meld> BuildMelds(List<MahjongTile> tiles, string pairKey)
{
var melds = new List<Meld>();
// 简化:返回对子
var pairTiles = tiles.Where(t =>
$"{(int)t.Suit}_{t.Value}" == pairKey).Take(2).ToList();
melds.Add(Meld.CreatePair(pairTiles[0], pairTiles[1]));
return melds;
}
}GDScript
extends Node
## 检测是否胡牌
func check_win(tiles: Array[MahjongTile]) -> bool:
if tiles.size() != 14:
return false
# 统计每种牌的数量
var counts := count_tiles(tiles)
# 尝试每一种对子作为"将"
for key in counts.keys():
if counts[key] >= 2:
counts[key] -= 2
if can_decompose(counts, 4):
counts[key] += 2
return true
counts[key] += 2
return false
## 统计每种牌的数量
func count_tiles(tiles: Array[MahjongTile]) -> Dictionary:
var counts := {}
for tile in tiles:
var key := "%d_%d" % [tile.suit, tile.value]
if not counts.has(key):
counts[key] = 0
counts[key] += 1
return counts
## 递归检查能否分解
func can_decompose(counts: Dictionary, groups_needed: int) -> bool:
if groups_needed == 0:
for v in counts.values():
if v != 0:
return false
return true
# 找第一个有剩余的牌
var first_key := ""
for key in counts.keys():
if counts[key] > 0:
first_key = key
break
if first_key == "":
return groups_needed == 0
# 尝试作为刻子
if counts[first_key] >= 3:
counts[first_key] -= 3
if can_decompose(counts, groups_needed - 1):
counts[first_key] += 3
return true
counts[first_key] += 3
# 尝试作为顺子
var parts := first_key.split("_")
var suit := int(parts[0])
var value := int(parts[1])
if suit <= 2 and value <= 7:
var key2 := "%d_%d" % [suit, value + 1]
var key3 := "%d_%d" % [suit, value + 2]
if counts.get(key2, 0) > 0 and counts.get(key3, 0) > 0:
counts[first_key] -= 1
counts[key2] -= 1
counts[key3] -= 1
if can_decompose(counts, groups_needed - 1):
counts[first_key] += 1
counts[key2] += 1
counts[key3] += 1
return true
counts[first_key] += 1
counts[key2] += 1
counts[key3] += 1
return false特殊牌型检测
| 牌型 | 条件 | 番数(示例) |
|---|---|---|
| 平胡 | 基本胡牌 | 1番 |
| 七对 | 7个对子 | 4番 |
| 清一色 | 全部同花色 | 8番 |
| 大四喜 | 四个风牌刻子 | 88番 |
| 小四喜 | 三个风牌刻子+一个风牌对子 | 64番 |
| 字一色 | 全部是字牌 | 64番 |
| 十三幺 | 13种特殊牌+一对 | 88番 |
番数规则因地区而异
不同地区的麻将番数规则差异很大。上面的番数只是一个示例,实际开发时需要根据目标地区的规则来调整。
小结
本章我们实现了麻将规则引擎的核心部分:
- 牌的数据结构——用枚举和类表示花色、数值和牌面
- 牌型组合——对子、刻子、顺子的定义和创建
- 摸牌打牌流程——游戏状态管理和玩家操作
- 吃碰杠检测——判断玩家能否执行吃/碰/杠操作
- 胡牌判定——递归算法检测14张牌能否组成合法牌型
提示
规则引擎是整个游戏最复杂的部分之一。胡牌检测的递归算法虽然代码不多,但逻辑精巧——建议多花时间理解它的工作原理。
