欢迎加入开源鸿蒙跨平台社区:
https://openharmonycrossplatform.csdn.net
一、项目概述
运行效果图




1.1 应用简介
麻将作为中华传统智力游戏的瑰宝,承载着深厚的文化底蕴与智慧结晶。本应用采用经典绿色牌桌设计,136张麻将牌完整呈现,万、条、筒、风、箭五大牌类各具特色。玩家与AI对弈,体验摸牌、出牌、碰、杠、胡等核心玩法,在方寸之间感受国粹魅力。
游戏支持碰牌、杠牌等组合操作,明牌展示清晰直观。胡牌判断采用递归回溯算法,准确识别各种牌型。AI对手基于评分策略进行出牌决策,提供具有一定挑战性的对战体验。
1.2 核心功能
| 功能模块 | 功能描述 | 实现方式 |
|---|---|---|
| 牌墙管理 | 136张牌洗牌发牌 | Random.shuffle |
| 手牌显示 | 玩家手牌展示 | Wrap布局 |
| 摸牌操作 | 从牌墙获取新牌 | 队列移除 |
| 出牌操作 | 打出一张手牌 | 双击交互 |
| 碰牌功能 | 三张相同成组 | 条件判断 |
| 杠牌功能 | 四张相同成组 | 条件判断 |
| 胡牌判断 | 牌型合法性验证 | 递归回溯 |
| AI对战 | 智能出牌决策 | 评分策略 |
1.3 牌型配置
| 牌类 | 牌名 | 数量 | 说明 |
|---|---|---|---|
| 万子 | 一万至九万 | 36张 | 每种4张,红色标识 |
| 条子 | 一条至九条 | 36张 | 每种4张,绿色标识 |
| 筒子 | 一筒至九筒 | 36张 | 每种4张,蓝色标识 |
| 风牌 | 东南西北 | 16张 | 每种4张,紫色标识 |
| 箭牌 | 中发白 | 12张 | 每种4张,各有特色 |
1.4 技术栈
| 技术领域 | 技术选型 | 版本要求 |
|---|---|---|
| 开发框架 | Flutter | >= 3.0.0 |
| 编程语言 | Dart | >= 2.17.0 |
| 设计规范 | Material Design 3 | - |
| 状态管理 | setState | - |
| 目标平台 | 鸿蒙OS | API 21+ |
1.5 项目结构
lib/
└── main_mahjong.dart
├── MahjongApp # 应用入口
├── MahjongType # 牌类型枚举
├── WindType # 风牌枚举
├── JianType # 箭牌枚举
├── MahjongTile # 麻将牌模型
└── MahjongGame # 游戏主页面
├── _buildGameInfo() # 游戏信息栏
├── _buildAIHand() # AI手牌区
├── _buildDiscardArea() # 出牌区
├── _buildPlayerMelds() # 明牌区
├── _buildPlayerHand() # 玩家手牌区
└── _buildActionButtons() # 操作按钮区
二、系统架构
2.1 整体架构图
Game Logic
Presentation Layer
Data Layer
AllTiles
牌墙数据
PlayerHand
玩家手牌
AIHand
AI手牌
DiscardPile
出牌堆
PlayerMelds
明牌组
游戏信息栏
剩余牌数
当前回合
手牌数量
手牌区域
玩家手牌
AI手牌
新摸牌
操作区域
摸牌/出牌
碰/杠/胡
摸牌处理
_drawTile
出牌处理
_discardTile
碰杠判断
_canPeng/_canGang
胡牌检测
_checkWin
AI决策
_aiTurn
2.2 类图设计
contains
manages
has
references
references
MahjongApp
+Widget build()
MahjongGame
-List<MahjongTile> _allTiles
-List<MahjongTile> _playerHand
-List<MahjongTile> _aiHand
-List<MahjongTile> _discardPile
-List<List<MahjongTile>> _playerMelds
-MahjongTile? _currentTile
-MahjongTile? _lastDiscarded
-bool _isPlayerTurn
-bool _gameOver
+Widget build()
-void _initGame()
-void _drawTile()
-void _discardTile()
-void _peng()
-void _gang()
-bool _checkWin()
-void _aiTurn()
MahjongTile
+MahjongType type
+int value
+bool isHidden
+String displayName
+Color tileColor
+String typeSymbol
<<enumeration>>
MahjongType
wan
tiao
tong
feng
jian
<<enumeration>>
WindType
dong
nan
xi
bei
<<enumeration>>
JianType
zhong
fa
bai
2.3 数据流程图
玩家
可以胡
不能胡
AI
胡牌
出牌
游戏开始
初始化牌墙
洗牌发牌
每人13张
当前回合
摸牌
判断胡牌
胡牌获胜
选择出牌
进入AI回合
AI摸牌
AI判断胡牌
AI获胜
AI出牌决策
返回玩家回合
游戏结束
2.4 游戏流程
AI对手 游戏系统 玩家 AI对手 游戏系统 玩家 alt [可以碰/杠] alt [可以胡牌] 点击摸牌 返回新牌 双击出牌 通知AI回合 AI摸牌 AI出牌决策 返回玩家回合 显示碰/杠按钮 点击碰/杠 更新明牌区 点击胡牌 显示胜利
三、核心模块设计
3.1 数据模型设计
3.1.1 牌类型枚举 (MahjongType)
dart
enum MahjongType {
wan, // 万子
tiao, // 条子
tong, // 筒子
feng, // 风牌
jian // 箭牌
}
3.1.2 牌类分布
26% 26% 26% 12% 9% 麻将牌分布(共136张) 万子 条子 筒子 风牌 箭牌
3.1.3 麻将牌模型 (MahjongTile)
dart
class MahjongTile {
final MahjongType type; // 牌类型
final int value; // 牌面值(1-9或1-4或1-3)
bool isHidden; // 是否隐藏(背面)
String get displayName {
switch (type) {
case MahjongType.wan:
return '$value万';
case MahjongType.tiao:
return '$value条';
case MahjongType.tong:
return '$value筒';
case MahjongType.feng:
const windNames = ['东', '南', '西', '北'];
return windNames[value - 1];
case MahjongType.jian:
const jianNames = ['中', '发', '白'];
return jianNames[value - 1];
}
}
Color get tileColor {
// 万子红、条子绿、筒子蓝、风牌紫
// 箭牌:中红、发绿、白黑
}
}
3.2 胡牌算法实现
3.2.1 胡牌基本条件
不是3n+2张
是3n+2张
0张
2张
是
否
其他
是
否
是
否
开始判断
手牌数量
不能胡牌
对手牌排序
递归移除面子
剩余牌数
找到将牌对
是否为对子?
可以胡牌
继续递归
尝试移除刻子
成功移除?
尝试移除顺子
成功移除?
3.2.2 面子类型
| 面子类型 | 构成 | 示例 |
|---|---|---|
| 刻子 | 三张相同的牌 | 三万、三万、三万 |
| 顺子 | 三张连续同类型牌 | 一万、二万、三万 |
| 将牌 | 两张相同的牌 | 五条、五条 |
3.2.3 胡牌判断核心代码
dart
bool _canFormWinningHand(List<MahjongTile> tiles) {
if (tiles.isEmpty) return true;
// 尝试移除刻子(三张相同)
if (tiles.length >= 3 &&
tiles[0] == tiles[1] &&
tiles[0] == tiles[2]) {
var newTiles = List<MahjongTile>.from(tiles);
newTiles.removeRange(0, 3);
if (_canFormWinningHand(newTiles)) return true;
}
// 尝试移除顺子(三张连续)
if (tiles[0].type == MahjongType.wan ||
tiles[0].type == MahjongType.tiao ||
tiles[0].type == MahjongType.tong) {
// 查找连续的三张牌
int firstValue = tiles[0].value;
bool hasSecond = tiles.any((t) =>
t.type == tiles[0].type && t.value == firstValue + 1);
bool hasThird = tiles.any((t) =>
t.type == tiles[0].type && t.value == firstValue + 2);
if (hasSecond && hasThird) {
var newTiles = List<MahjongTile>.from(tiles);
// 移除这三张牌后递归判断
if (_canFormWinningHand(newTiles)) return true;
}
}
return false;
}
3.3 碰杠操作实现
3.3.1 碰牌条件判断
是
否
出牌区有牌
手牌中有两张相同?
可以碰牌
不能碰牌
移除两张手牌
加上出牌区牌
形成明牌组
展示在明牌区
3.3.2 杠牌条件判断
是
否
出牌区有牌
手牌中有三张相同?
可以杠牌
不能杠牌
移除三张手牌
加上出牌区牌
形成明牌组
额外摸一张牌
3.4 页面结构设计
3.4.1 界面布局
游戏界面
游戏信息栏
AI手牌区
出牌区
玩家明牌区
玩家手牌区
操作按钮区
消息提示栏
剩余牌数
当前回合
手牌数量
摸牌按钮
出牌按钮
碰牌按钮
杠牌按钮
胡牌按钮
3.4.2 手牌区布局
┌─────────────────────────────────────────────────────────────┐
│ 你的手牌 │
│ ┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐ 新摸│
│ │一││一││二││三││四││五││五││六││七││八││东││东││白│ ←── │九││
│ │万││万││万││万││万││万││万││万││万││万││ ││ ││ │ │万││
│ └──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘ └──┘│
└─────────────────────────────────────────────────────────────┘
3.5 状态管理
3.5.1 核心状态变量
dart
class _MahjongGameState extends State<MahjongGame> {
final List<MahjongTile> _allTiles = []; // 牌墙
final List<MahjongTile> _playerHand = []; // 玩家手牌
final List<MahjongTile> _aiHand = []; // AI手牌
final List<MahjongTile> _discardPile = []; // 出牌堆
final List<List<MahjongTile>> _playerMelds = []; // 玩家明牌
final List<List<MahjongTile>> _aiMelds = []; // AI明牌
MahjongTile? _currentTile; // 当前摸的牌
MahjongTile? _lastDiscarded; // 最后打出的牌
bool _isPlayerTurn = true; // 是否玩家回合
bool _gameOver = false; // 游戏是否结束
String _gameMessage = ''; // 游戏消息
int _remainingTiles = 0; // 剩余牌数
MahjongTile? _selectedTile; // 选中的牌
}
3.5.2 回合切换
dart
// 玩家出牌后切换到AI回合
_isPlayerTurn = false;
// AI出牌后切换到玩家回合
_isPlayerTurn = true;
四、UI设计规范
4.1 配色方案
游戏采用经典绿色牌桌风格:
| 颜色类型 | 色值 | 用途 |
|---|---|---|
| 主色 | Green.shade800 | AppBar背景 |
| 牌桌背景 | Green.shade900 | 游戏背景渐变 |
| 万子文字 | Red | 一万至九万 |
| 条子文字 | Green | 一条至九条 |
| 筒子文字 | Blue | 一筒至九筒 |
| 风牌文字 | Purple | 东南西北 |
| 红中文字 | Red | 中 |
| 发财文字 | Green | 发 |
| 白板文字 | Black | 白 |
| 牌面背景 | White | 麻将牌底色 |
4.2 牌面样式
4.2.1 麻将牌布局
┌─────────────────────────────────────┐
│ ╭─────╮ │
│ │ 三 │ │
│ │ 万 │ │
│ ╰─────╯ │
│ 白色底+彩色文字 │
│ 圆角矩形+阴影效果 │
└─────────────────────────────────────┘
4.2.2 牌面颜色对照
| 牌类 | 文字颜色 | 示例 |
|---|---|---|
| 万子 | 红色 | 三万 |
| 条子 | 绿色 | 七条 |
| 筒子 | 蓝色 | 五筒 |
| 风牌 | 紫色 | 东风 |
| 红中 | 红色 | 中 |
| 发财 | 绿色 | 发 |
| 白板 | 黑色 | 白 |
4.3 组件规范
4.3.1 游戏信息栏
┌─────────────────────────────────────────────────────────────┐
│ 剩余牌 回合 手牌 │
│ 68 你的回合 13张 │
└─────────────────────────────────────────────────────────────┘
4.3.2 操作按钮区
┌─────────────────────────────────────────────────────────────┐
│ 👆 摸牌 ✉ 出牌 📚 碰 📊 杠 🎉 胡 │
└─────────────────────────────────────────────────────────────┘
4.4 交互设计
4.4.1 点击操作
| 操作 | 手势 | 效果 |
|---|---|---|
| 选择牌 | 单击手牌 | 黄色边框高亮 |
| 出牌 | 双击手牌 | 打出选中的牌 |
| 摸牌 | 点击按钮 | 从牌墙获取新牌 |
| 碰/杠/胡 | 点击按钮 | 执行对应操作 |
4.4.2 视觉反馈
选中牌
黄色边框高亮
显示牌面信息
双击出牌
牌移动到出牌区
五、核心功能实现
5.1 游戏初始化
dart
void _initGame() {
_allTiles.clear();
_playerHand.clear();
_aiHand.clear();
_discardPile.clear();
_playerMelds.clear();
_aiMelds.clear();
_currentTile = null;
_lastDiscarded = null;
_isPlayerTurn = true;
_gameOver = false;
_gameMessage = '';
_selectedTile = null;
// 生成万、条、筒各36张
for (int i = 1; i <= 9; i++) {
for (int j = 0; j < 4; j++) {
_allTiles.add(MahjongTile(type: MahjongType.wan, value: i));
_allTiles.add(MahjongTile(type: MahjongType.tiao, value: i));
_allTiles.add(MahjongTile(type: MahjongType.tong, value: i));
}
}
// 生成风牌16张
for (int i = 1; i <= 4; i++) {
for (int j = 0; j < 4; j++) {
_allTiles.add(MahjongTile(type: MahjongType.feng, value: i));
}
}
// 生成箭牌12张
for (int i = 1; i <= 3; i++) {
for (int j = 0; j < 4; j++) {
_allTiles.add(MahjongTile(type: MahjongType.jian, value: i));
}
}
// 随机洗牌
_allTiles.shuffle(Random());
// 发牌:每人13张
for (int i = 0; i < 13; i++) {
_playerHand.add(_allTiles.removeLast());
_aiHand.add(_allTiles.removeLast());
}
_sortHand(_playerHand);
_sortHand(_aiHand);
_remainingTiles = _allTiles.length;
}
5.2 摸牌处理
dart
void _drawTile() {
if (_allTiles.isEmpty) {
setState(() {
_gameOver = true;
_gameMessage = '牌已摸完,流局!';
});
return;
}
final tile = _allTiles.removeLast();
_remainingTiles = _allTiles.length;
if (_isPlayerTurn) {
setState(() {
_currentTile = tile;
_gameMessage = '摸到: ${tile.displayName}';
});
} else {
_aiHand.add(tile);
_sortHand(_aiHand);
_aiTurn();
}
}
5.3 出牌处理
dart
void _discardTile(MahjongTile tile) {
if (_isPlayerTurn) {
if (_currentTile != null && tile == _currentTile) {
// 直接打出新摸的牌
setState(() {
_lastDiscarded = tile;
_currentTile = null;
_discardPile.add(tile);
_isPlayerTurn = false;
_gameMessage = '你打出: ${tile.displayName}';
_selectedTile = null;
});
Future.delayed(const Duration(milliseconds: 500), () {
_aiTurn();
});
} else if (_currentTile == null) {
// 没有新摸牌时打出
setState(() {
_playerHand.remove(tile);
_lastDiscarded = tile;
_discardPile.add(tile);
_isPlayerTurn = false;
_gameMessage = '你打出: ${tile.displayName}';
_selectedTile = null;
});
Future.delayed(const Duration(milliseconds: 500), () {
_aiTurn();
});
} else {
// 用手牌交换新摸牌后打出
setState(() {
_playerHand.remove(tile);
_playerHand.add(_currentTile!);
_sortHand(_playerHand);
_lastDiscarded = tile;
_currentTile = null;
_discardPile.add(tile);
_isPlayerTurn = false;
_gameMessage = '你打出: ${tile.displayName}';
_selectedTile = null;
});
Future.delayed(const Duration(milliseconds: 500), () {
_aiTurn();
});
}
}
}
5.4 碰牌功能
dart
void _peng() {
if (_lastDiscarded == null || !_canPeng(_playerHand, _lastDiscarded!)) return;
List<MahjongTile> pengTiles = [];
int count = 0;
for (int i = _playerHand.length - 1; i >= 0 && count < 2; i--) {
if (_playerHand[i] == _lastDiscarded) {
pengTiles.add(_playerHand.removeAt(i));
count++;
}
}
pengTiles.add(_lastDiscarded!);
_playerMelds.add(pengTiles);
_discardPile.remove(_lastDiscarded);
setState(() {
_lastDiscarded = null;
_gameMessage = '碰!${pengTiles.first.displayName}';
});
}
bool _canPeng(List<MahjongTile> hand, MahjongTile tile) {
int count = hand.where((t) => t == tile).length;
return count >= 2;
}
5.5 AI出牌决策
dart
MahjongTile? _findBestDiscard(List<MahjongTile> hand) {
if (hand.isEmpty) return null;
Map<MahjongTile, int> scores = {};
for (var tile in hand) {
int score = 0;
// 相同牌数量越多,保留价值越高
int sameCount = hand.where((t) => t == tile).length;
score += sameCount * 10;
// 有相邻牌可组成顺子,保留价值高
if (tile.type == MahjongType.wan ||
tile.type == MahjongType.tiao ||
tile.type == MahjongType.tong) {
bool hasPrev = hand.any((t) =>
t.type == tile.type && t.value == tile.value - 1);
bool hasNext = hand.any((t) =>
t.type == tile.type && t.value == tile.value + 1);
if (hasPrev || hasNext) score += 5;
}
scores[tile] = score;
}
// 选择得分最低的牌打出
var sortedTiles = hand.toList()
..sort((a, b) => (scores[a] ?? 0).compareTo(scores[b] ?? 0));
return sortedTiles.first;
}
六、麻将知识拓展
6.1 基本牌型
6.1.1 标准胡牌牌型
胡牌牌型
4组面子 + 1对将牌
刻子×4 + 将牌
顺子×4 + 将牌
刻子+顺子混合 + 将牌
6.1.2 特殊牌型
| 牌型 | 说明 | 难度 |
|---|---|---|
| 七对 | 七个对子胡牌 | 中等 |
| 十三幺 | 十三张幺九牌各一张 | 困难 |
| 清一色 | 全部为同一花色 | 困难 |
| 字一色 | 全部为风牌或箭牌 | 极难 |
6.2 基本术语
| 术语 | 含义 |
|---|---|
| 听牌 | 只差一张牌即可胡牌 |
| 自摸 | 自己摸到胡牌 |
| 点炮 | 打出的牌被别人胡 |
| 明牌 | 碰杠后展示的牌组 |
| 暗牌 | 未展示的手牌 |
| 牌墙 | 未摸的牌堆 |
6.3 番型计算
6.3.1 基本番型
胡牌
牌型判断
平胡
对对胡
清一色
七对
1番
2番
6番
6.3.2 番型对照表
| 番型 | 番数 | 条件 |
|---|---|---|
| 平胡 | 1 | 四组顺子+一对将 |
| 对对胡 | 2 | 四组刻子+一对将 |
| 混一色 | 3 | 只有字牌+一种花色 |
| 清一色 | 6 | 只有同一种花色 |
| 七对 | 3 | 七个对子 |
| 字一色 | 10 | 全部为字牌 |
七、扩展功能规划
7.1 后续版本规划
2024-01-07 2024-01-14 2024-01-21 2024-01-28 2024-02-04 2024-02-11 2024-02-18 2024-02-25 2024-03-03 2024-03-10 2024-03-17 2024-03-24 核心游戏逻辑 牌型生成与洗牌 碰杠胡功能 AI难度选择 番型计分系统 听牌提示 多人对战 复盘功能 牌谱保存 V1.0 基础版本 V1.1 增强版本 V1.2 进阶版本 麻将游戏开发计划
7.2 功能扩展建议
7.2.1 AI难度选择
dart
enum AIDifficulty {
easy, // 简单:随机出牌
normal, // 普通:基础策略
hard, // 困难:深度搜索
}
| 难度 | 算法 | 特点 |
|---|---|---|
| 简单 | 随机出牌 | 适合新手 |
| 普通 | 评分策略 | 当前实现 |
| 困难 | 深度搜索 | 高手挑战 |
7.2.2 番型计分
| 功能 | 说明 |
|---|---|
| 自动计番 | 胡牌后自动计算番数 |
| 番型展示 | 显示胡牌番型 |
| 分数累计 | 记录总分数 |
7.2.3 听牌提示
| 功能 | 说明 |
|---|---|
| 听牌检测 | 判断是否听牌 |
| 听牌显示 | 显示听哪些牌 |
| 剩余张数 | 显示听牌剩余张数 |
八、注意事项
8.1 开发注意事项
-
牌数校验:确保136张牌完整生成
-
状态同步:出牌后及时更新游戏状态
-
边界检查:数组访问前检查边界
-
性能优化:胡牌判断避免过度递归
8.2 游戏体验优化
🎮 游戏体验建议 🎮
- 添加摸牌出牌音效
- 显示听牌提示
- 支持牌桌主题切换
- 添加思考时间限制
8.3 常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 胡牌判断错误 | 递归逻辑问题 | 检查回溯条件 |
| 碰杠异常 | 条件判断错误 | 检查数量统计 |
| AI出牌卡顿 | 算法效率低 | 优化评分计算 |
| 界面显示异常 | 状态未更新 | 检查setState |
九、运行说明
9.1 环境要求
| 环境 | 版本要求 |
|---|---|
| Flutter SDK | >= 3.0.0 |
| Dart SDK | >= 2.17.0 |
| 鸿蒙OS | API 21+ |
9.2 运行命令
bash
# 查看可用设备
flutter devices
# 运行到鸿蒙设备
flutter run -d 127.0.0.1:5555 lib/main_mahjong.dart
# 运行到Windows
flutter run -d windows -t lib/main_mahjong.dart
# 代码分析
flutter analyze lib/main_mahjong.dart
十、总结
麻将游戏应用通过经典的玩法设计和绿色牌桌风格,为玩家提供了真实的麻将体验。游戏采用136张标准麻将牌,万、条、筒、风、箭五大牌类各具特色;代码结构清晰,遵循Flutter最佳实践;碰、杠、胡等核心操作完整实现。
核心玩法涵盖摸牌、出牌、碰牌、杠牌、胡牌判断等完整流程,满足麻将爱好者的基本需求。特别值得一提的是胡牌判断算法,采用递归回溯方式,能够准确识别刻子、顺子、将牌的组合关系,确保游戏的专业性。
界面设计采用经典绿色牌桌风格,牌面清晰美观。万子红色、条子绿色、筒子蓝色的配色方案,让玩家一眼就能识别牌类。碰杠后的明牌展示区,让游戏进程一目了然。AI对手采用评分策略进行出牌决策,综合考虑相同牌数量和相邻牌关系,提供具有一定挑战性的对战体验。
方桌之上,百牌争锋,智胜千里!