Flutter 框架跨平台鸿蒙开发 - 鸿蒙麻将游戏应用
欢迎加入开源鸿蒙跨平台社区:
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 整体架构图
2.2 类图设计
2.3 数据流程图
2.4 游戏流程
三、核心模块设计
3.1 数据模型设计
3.1.1 牌类型枚举 (MahjongType)
enum MahjongType {
wan, // 万子
tiao, // 条子
tong, // 筒子
feng, // 风牌
jian // 箭牌
}
3.1.2 牌类分布
3.1.3 麻将牌模型 (MahjongTile)
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 胡牌基本条件
3.2.2 面子类型
| 面子类型 | 构成 | 示例 |
|---|---|---|
| 刻子 | 三张相同的牌 | 三万、三万、三万 |
| 顺子 | 三张连续同类型牌 | 一万、二万、三万 |
| 将牌 | 两张相同的牌 | 五条、五条 |
3.2.3 胡牌判断核心代码
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 界面布局
3.4.2 手牌区布局
┌─────────────────────────────────────────────────────────────┐
│ 你的手牌 │
│ ┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐┌──┐ 新摸│
│ │一││一││二││三││四││五││五││六││七││八││东││东││白│ ←── │九││
│ │万││万││万││万││万││万││万││万││万││万││ ││ ││ │ │万││
│ └──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘└──┘ └──┘│
└─────────────────────────────────────────────────────────────┘
3.5 状态管理
3.5.1 核心状态变量
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 回合切换
// 玩家出牌后切换到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 游戏初始化
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 摸牌处理
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 出牌处理
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 碰牌功能
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出牌决策
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 标准胡牌牌型
6.1.2 特殊牌型
| 牌型 | 说明 | 难度 |
|---|---|---|
| 七对 | 七个对子胡牌 | 中等 |
| 十三幺 | 十三张幺九牌各一张 | 困难 |
| 清一色 | 全部为同一花色 | 困难 |
| 字一色 | 全部为风牌或箭牌 | 极难 |
6.2 基本术语
| 术语 | 含义 |
|---|---|
| 听牌 | 只差一张牌即可胡牌 |
| 自摸 | 自己摸到胡牌 |
| 点炮 | 打出的牌被别人胡 |
| 明牌 | 碰杠后展示的牌组 |
| 暗牌 | 未展示的手牌 |
| 牌墙 | 未摸的牌堆 |
6.3 番型计算
6.3.1 基本番型
6.3.2 番型对照表
| 番型 | 番数 | 条件 |
|---|---|---|
| 平胡 | 1 | 四组顺子+一对将 |
| 对对胡 | 2 | 四组刻子+一对将 |
| 混一色 | 3 | 只有字牌+一种花色 |
| 清一色 | 6 | 只有同一种花色 |
| 七对 | 3 | 七个对子 |
| 字一色 | 10 | 全部为字牌 |
七、扩展功能规划
7.1 后续版本规划
7.2 功能扩展建议
7.2.1 AI难度选择
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 运行命令
# 查看可用设备
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对手采用评分策略进行出牌决策,综合考虑相同牌数量和相邻牌关系,提供具有一定挑战性的对战体验。
方桌之上,百牌争锋,智胜千里!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)