从零到一:用纯JavaScript实现中国麻将游戏的实战分享
从零到一:用纯JavaScript实现中国麻将游戏的实战分享
作者:Eddygit
日期:2026-10-06
项目地址:https://gitcode.com/Eddygit/mahjong-game
一、技术路线
1.1 为什么选择纯HTML/JS单文件方案?
在项目立项阶段,我考虑过几种技术方案:
| 方案 | 优点 | 缺点 | 最终选择 |
|---|---|---|---|
| React/Vue + 构建工具 | 组件化、状态管理清晰 | 需要Node环境、构建复杂 | ❌ |
| Canvas 2D 渲染 | 性能好、自定义程度高 | 开发量大、调试不便 | ❌ |
| 纯HTML/CSS/JS单文件 | 零依赖、即开即玩、易分享 | 代码集中、维护稍难 | ✅ |
最终选择单文件方案,核心考量是可分享性——一个HTML文件发给别人,双击就能玩,这是最纯粹的工具形态。
1.2 架构设计
index.html
├── HTML结构层(游戏界面布局)
├── CSS样式层(牌面设计 + 动画效果)
└── JavaScript逻辑层
├── 牌定义模块(createAllTiles)
├── 胡牌判定模块(canHu / checkSets / canHu7Pairs)
├── AI决策模块(aiChooseDiscard)
├── 渲染模块(render)
└── 游戏流程模块(initGame / advanceTurn)
1.3 数据结构设计
每张牌用一个对象表示:
{ suit: 'wan', value: 3, id: 42 }
// suit: 花色(wan万/tiao条/tong筒/feng风/jian箭)
// value: 数值(1-9)
// id: 唯一标识(0-135)
这种设计的好处是:
sameTile(a, b)判断简单:只比 suit 和 valuetileKey(t)排序方便:suitOrder * 100 + value- id 用于追踪具体牌的流向
二、核心技术实现
2.1 胡牌判定算法——递归回溯
这是整个项目最核心也最有意思的部分。
基本思路:标准胡牌 = n个顺子/刻子 + 1个将(对子)
算法步骤:
- 遍历每种牌,尝试将其作为"将"(减去2张)
- 对剩余牌递归尝试分解为顺子或刻子
- 如果能全部分解,则可胡
function canHu(hand) {
let counts = handToCounts(hand);
for (let key of keys) {
if (counts[key] >= 2) {
counts[key] -= 2; // 尝试做将
if (checkSets(counts)) return true; // 递归检查剩余
counts[key] += 2; // 回溯
}
}
return false;
}
checkSets 递归逻辑:
- 找到第一张还有剩余的牌
- 尝试组成刻子(3张相同)→ 递归
- 尝试组成顺子(3张连续数牌)→ 递归
- 都不行则返回 false
七对子单独检测:14张牌恰好7组对子。
2.2 AI出牌策略
AI采用基于牌效率的简单评分策略:
function aiChooseDiscard(hand) {
// 对每张牌计算"孤立度"分数
// 分数越高 = 越该打出
// - 字牌单张:+10(最该打)
// - 数牌单张:+5
// - 边张(1/9):+3
// - 孤张(左右无邻牌):+2
}
这是一个非常简化的策略,真实麻将AI需要考虑:
- 听牌检测(打出后能听哪些牌)
- 对手弃牌推理(猜对方手牌)
- 局势评估(攻守判断)
2.3 CSS牌面设计
不使用任何图片,纯CSS绘制麻将牌面:
.tile {
width: 42px; height: 60px;
background: linear-gradient(145deg, #fff, #e8e8e8);
border: 2px solid #bbb;
border-radius: 6px;
/* ... */
}
万条筒用数字+汉字表示,风牌箭牌用汉字表示,不同花色用不同颜色区分。
三、实战经验
3.1 开发流程
- 先搭骨架:HTML结构 + 基础CSS,确认布局合理
- 再写逻辑:牌定义 → 发牌 → 出牌 → 胡牌判定
- 后做交互:碰杠胡按钮 → AI决策 → 渲染更新
- 最后优化:动画效果 → 提示文案 → 边界处理
3.2 踩过的坑
坑1:胡牌判定遗漏七对子
最初只实现了标准胡牌(顺子+刻子+将),测试时发现七对子无法胡。单独添加 canHu7Pairs 检测解决。
坑2:碰牌后忘记补牌
碰牌后手牌减少2张(加上别人的1张组成3张明刻),但手牌数量应保持13张。碰牌后需要轮到碰牌者出牌,而不是继续下一家。
坑3:AI出牌后忘记检查玩家操作
AI出牌后需要检查玩家是否能碰/杠/胡,最初直接 advanceTurn() 跳过了,导致玩家无法操作。
3.3 调试技巧
- 用 Node.js 提取纯逻辑函数做单元测试(绕过DOM依赖)
console.log打印手牌和游戏状态辅助调试- 先测胡牌判定(最复杂),再测游戏流程
四、存在问题与优化空间
4.1 当前不足
| 问题 | 严重程度 | 说明 |
|---|---|---|
| 无听牌提示 | 中 | 玩家不知道自己离胡牌还差什么 |
| AI太简单 | 中 | AI不会防守,不会推理对手手牌 |
| 无番种计算 | 中 | 胡牌后不计算具体番数和台数 |
| 无音效 | 低 | 缺少摸牌、出牌的音效反馈 |
| 无历史记录 | 低 | 不记录对局历史和胜率统计 |
| 无花牌 | 低 | 省略了春夏秋冬梅兰竹菊8张花牌 |
4.2 优化方向
短期优化:
- 添加听牌检测和提示
- 改进AI策略:加入听牌检测和基本防守
- 添加更多番种:清一色、一条龙、十三幺等
- 添加音效和动画
中期优化:
- 拆分为多文件项目(模块化)
- 添加在线对战功能(WebSocket)
- 实现完整四川麻将或国标麻将规则
- 添加排行榜和成就系统
长期优化:
- 用 Canvas/WebGL 重写渲染层
- 引入蒙特卡洛AI或深度学习AI
- 支持自定义规则和牌型
- 做成PWA支持离线游玩
4.3 技术反思
单文件 vs 模块化:
单文件方案在项目初期开发效率很高,但随着功能增加,代码维护难度上升。如果后续要添加更多功能,建议拆分为模块化项目。但对于一个"小工具"的定位,单文件的简洁性是无可替代的优势。
递归 vs 动态规划:
胡牌判定用递归回溯足够了(136张牌,手牌最多14张,递归深度有限)。如果要做更复杂的牌型分析(如计算所有可能的胡法),可以考虑动态规划优化。
五、总结
这个项目证明了:用最简单的技术栈,也能做出有趣的交互应用。纯HTML/CSS/JS不需要任何构建工具和环境配置,降低了使用门槛,提高了可分享性。
麻将游戏的复杂度主要在规则实现(胡牌判定、番种计算),而不在技术框架。选择合适的技术方案,比追求"先进"的技术更重要。
本文为原创技术分享,转载请注明出处。
- 点赞
- 收藏
- 关注作者
评论(0)