一、项目整体架构与核心功能
该Kotlin程序完整实现了传统推牌九游戏的核心逻辑,包含牌组生成 、发牌逻辑 、牌型判定 、输赢对比四大核心模块,支持多玩家(闲家)与庄家的多轮对局,并输出每轮的牌型、点数及输赢结果。整体代码采用面向对象设计,通过数据类、枚举类封装牌的属性与类型,逻辑分层清晰,易于扩展和维护。
核心功能清单
- 生成32张标准推牌九牌组;
- 支持自定义玩家数量,自动计算可玩轮数;
- 洗牌后按轮次为庄家、闲家分发手牌;
- 判定手牌的特殊牌型(至尊、对子、天杠/地杠)与普通点数;
- 对比庄家与闲家手牌大小,输出每轮输赢结果;
- 格式化输出对局信息,提升可读性。
二、核心数据结构设计
1. 牌类型枚举(PaiType)
定义推牌九牌的基础分类,按传统规则划分优先级(天 > 地 > 人 > 鹅 > 长牌 > 短牌 > 杂牌),每个类型包含priority(优先级)和typeName(类型名称)两个核心属性,为牌型对比提供基础依据。
kotlin
enum class PaiType(val priority: Int, val typeName: String) {
TIAN(7, "天"), // 最大
DI(6, "地"),
REN(5, "人"),
E(4, "鹅"),
CHANG(3, "长牌"),
DUAN(2, "短牌"),
ZA(1, "杂牌") // 最小
}
2. 特殊牌型枚举(SpecialType)
封装推牌九的核心特殊牌型,优先级从高到低覆盖"至尊(三万+六万)""对子(白板/二饼等)""天杠/地杠",是输赢判定的核心依据:
kotlin
enum class SpecialType(val priority: Int, val displayName: String) {
SANWAN_LIUWAN(13, "至尊(三万+六万)"), // 最大
DUI_BAIBAN(12, "对白板"),
DUI_ERBING(11, "对二饼"),
DUI_BAIBING(10, "对八饼"),
DUI_SIBING(9, "对四饼"),
DUI_CHANGPAI(8, "对子长牌"),
DUI_DUANPAI(7, "对子短牌"),
DUI_ZAPAI(6, "对子杂牌"),
TIANGANG(5, "天杠"), // 白板+8点
DIGANG(4, "地杠"), // 二饼+8点
NORMAL(0, "普通")
}
3. 牌数据类(Pai)
封装单张牌的全量属性,实现Comparable接口支持牌的大小对比,是整个游戏的核心数据载体:
kotlin
data class Pai(
val type: PaiType, // 基础牌型(天地人鹅等)
val name: String, // 牌名
val displayName: String, // 显示名(如"白板""二饼")
val points: Int, // 点数(用于计算总点数)
val id: Int // 唯一标识(用于判断对子)
) : Comparable<Pai> {
override fun compareTo(other: Pai): Int {
// 先比牌型优先级,再比点数
if (this.type.priority != other.type.priority) {
return this.type.priority.compareTo(other.type.priority)
}
return this.points.compareTo(other.points)
}
}
三、核心方法解析
1. 牌组生成(generatePaiGowDeck)
严格遵循传统推牌九32张牌的规则生成牌组,分为四部分:
- 天地人鹅:各2张(共8张);
- 长牌:发财、六饼、四条各2张(共6张);
- 短牌:红中、一饼、七饼、六条各2张(共8张);
- 杂牌:九饼、八条、七条、五饼各2张 + 三万/六万各1张(共10张)。
通过repeat(2)实现"各2张"的规则,确保牌组的规范性。
2. 特殊牌型检查(checkSpecialType)
核心判定逻辑,按优先级从高到低检查手牌(2张)的特殊牌型:
- 先判定"至尊"(三万+六万);
- 再判定"对子"(两张牌displayName相同),并关联PaiType返回对应对子类型;
- 最后判定"天杠"(白板+8点)、"地杠"(二饼+8点);
- 无特殊牌型则返回
NORMAL。
3. 牌型结果计算(checkPaiGow)
整合"特殊牌型"与"普通点数",返回易读的牌型描述:
- 有特殊牌型:返回"牌型名 (牌1 + 牌2)"(如"至尊(三万+六万) (三万 + 六万)");
- 普通牌型:返回"点数==>X (牌1 + 牌2)"(X为两张牌点数和对10取余)。
4. 手牌对比(comparePaiGowHands)
推牌九输赢判定的核心算法,对比逻辑分层清晰:
- 特殊牌型优先:对比两张手牌的SpecialType优先级,优先级高则赢;
- 普通牌型对比 :
- 先比点数(和对10取余),点数大则赢;
- 点数相同则对比单张牌的最大优先级(通过Pai的compareTo方法)。
5. 输赢判定(judgeWinner)
遍历所有闲家手牌,调用comparePaiGowHands对比庄家手牌,生成格式化的输赢结果:
- 闲家赢:标注赢的原因(特殊牌型/点数/单牌优先级);
- 闲家输:标注输的原因;
- 平局:按规则判定"庄家赢"。
6. 主函数(main)
游戏流程的入口,核心逻辑:
- 定义玩家列表,计算总人数、每轮牌数、可玩轮数;
- 生成并洗牌(仅洗牌一次,供所有轮次使用);
- 按轮次循环:分发手牌 → 判定牌型 → 输出输赢 → 格式化打印;
- 游戏结束后输出总轮数。
四、代码设计亮点
1. 面向对象封装
- 用
Pai数据类封装单张牌的所有属性,职责单一; - 用枚举类封装牌型/特殊牌型的优先级,避免魔法值,提升可读性;
- 核心逻辑拆分为独立方法(如
checkSpecialType/comparePaiGowHands),符合"单一职责原则"。
2. 可扩展性强
- 新增牌型仅需扩展
PaiType/SpecialType枚举,无需修改核心对比逻辑; - 调整玩家数量仅需修改
playerNames列表,程序自动计算轮数; - 自定义输赢规则仅需修改
judgeWinner方法,不影响其他模块。
3. 可读性优化
- 格式化输出(如
padEnd补全字符、分隔线),提升控制台展示效果; - 方法名/变量名语义化(如
generatePaiGowDeck/bankerHand),代码自注释; - 分层逻辑清晰,从"生成牌组"到"判定输赢"形成完整闭环,易于理解。
五、完整代码实现
以下是基于Kotlin实现的推牌九(麻将)游戏完整代码,包含了从牌组生成、发牌到判定胜负的全流程:
kotlin
package com.baolan.www.test001
import kotlin.random.Random
fun main() {
val playerNames = listOf("张三", "李四", "王二")
// 计算总人数和总把数
val totalPlayers = playerNames.size + 1 // 闲家 + 庄家
val cardsPerRound = totalPlayers * 2 // 每把需要的牌数
val totalRounds = 32 / cardsPerRound // 总把数
println()
println("玩家:庄家、 ${playerNames.joinToString("、 ")}")
println()
// 洗牌一次,用于所有回合
val deck = generatePaiGowDeck().shuffled(Random)
// 找出最长的名字长度(以字符数计算)
val maxNameLength = playerNames.map { it.length }.maxOrNull() ?: 0
val bankerLength = "庄家".length
val alignLength = maxOf(maxNameLength, bankerLength)
// 玩所有把数
for (round in 1..totalRounds) {
println("━".repeat(60))
println("第${round}把".padStart(30 + 3))
// 计算当前这把的起始索引
val startIdx = (round - 1) * cardsPerRound
// 庄家拿2张牌
val bankerHand = deck.subList(startIdx, startIdx + 2)
// 动态为每个闲家发牌
val playerHands = mutableListOf<List<Pai>>()
for (i in playerNames.indices) {
val playerStartIdx = startIdx + 2 + i * 2
val hand = deck.subList(playerStartIdx, playerStartIdx + 2)
playerHands.add(hand)
}
// 打印所有手牌
// println("【手牌】")
// println("${"庄家".padEnd(alignLength, ' ')}的手牌: ${bankerHand.joinToString(" ") { it.displayName }}")
// playerNames.forEachIndexed { idx, name ->
// println("${name.padEnd(alignLength, ' ')}的手牌: ${playerHands[idx].joinToString(" ") { it.displayName }}")
// }
// 计算推牌九结果
val bankerResult = checkPaiGow(bankerHand)
val playerResults = mutableListOf<String>()
playerHands.forEach { hand ->
playerResults.add(checkPaiGow(hand))
}
// 打印推牌九结果
println("【牌型】")
println("${"庄家".padEnd(alignLength, ' ')}: $bankerResult")
playerNames.forEachIndexed { idx, name ->
println("${name.padEnd(alignLength, ' ')}: ${playerResults[idx]}")
}
// 判定输赢
val results = judgeWinner(bankerResult, bankerHand, playerResults, playerHands)
println("\n【判定结果】")
results.forEachIndexed { idx, result ->
println("${playerNames[idx].padEnd(alignLength, ' ')}: $result")
}
println()
println()
}
println("=".repeat(60))
println("游戏结束!共进行了${totalRounds}把")
println("=".repeat(60))
}
// 推牌九的牌数据类
data class Pai(
val type: PaiType,
val name: String,
val displayName: String,
val points: Int,
val id: Int // 用于识别具体是哪张牌(用于判断对子)
) : Comparable<Pai> {
override fun compareTo(other: Pai): Int {
// 先比较牌型优先级
if (this.type.priority != other.type.priority) {
return this.type.priority.compareTo(other.type.priority)
}
// 同类型比较点数
return this.points.compareTo(other.points)
}
}
// 牌的类型枚举
enum class PaiType(val priority: Int, val typeName: String) {
TIAN(7, "天"), // 最大
DI(6, "地"),
REN(5, "人"),
E(4, "鹅"),
CHANG(3, "长牌"),
DUAN(2, "短牌"),
ZA(1, "杂牌") // 最小
}
// 生成推牌九牌组(32张牌)
fun generatePaiGowDeck(): List<Pai> {
val deck = mutableListOf<Pai>()
// 第一排:天地人鹅(各2张)
repeat(2) {
deck.add(Pai(PaiType.TIAN, "天牌", "白板", 12, 1))
deck.add(Pai(PaiType.DI, "地牌", "二饼", 2, 2))
deck.add(Pai(PaiType.REN, "人牌", "八饼", 8, 3))
deck.add(Pai(PaiType.E, "鹅牌", "四饼", 4, 4))
}
// 第二排:长牌(各2张)
repeat(2) {
deck.add(Pai(PaiType.CHANG, "长牌", "发财", 10, 5))
deck.add(Pai(PaiType.CHANG, "长牌", "六饼", 6, 6))
deck.add(Pai(PaiType.CHANG, "长牌", "四条", 4, 7))
}
// 第三排:短牌(各2张)
repeat(2) {
deck.add(Pai(PaiType.DUAN, "短牌", "红中", 10, 8))
deck.add(Pai(PaiType.DUAN, "短牌", "一饼", 1, 9))
deck.add(Pai(PaiType.DUAN, "短牌", "七饼", 7, 10))
deck.add(Pai(PaiType.DUAN, "短牌", "六条", 6, 11))
}
// 第四排:杂牌(各2张,三万和六万各1张)
repeat(2) {
deck.add(Pai(PaiType.ZA, "杂牌", "九饼", 9, 12))
deck.add(Pai(PaiType.ZA, "杂牌", "八条", 8, 13))
deck.add(Pai(PaiType.ZA, "杂牌", "七条", 7, 14))
deck.add(Pai(PaiType.ZA, "杂牌", "五饼", 5, 15))
}
// 三万和六万各1张
deck.add(Pai(PaiType.ZA, "杂牌", "三万", 3, 16))
deck.add(Pai(PaiType.ZA, "杂牌", "六万", 6, 17))
return deck
}
// 特殊牌型枚举
enum class SpecialType(val priority: Int, val displayName: String) {
SANWAN_LIUWAN(13, "至尊(三万+六万)"), // 最大
DUI_BAIBAN(12, "对白板"),
DUI_ERBING(11, "对二饼"),
DUI_BAIBING(10, "对八饼"),
DUI_SIBING(9, "对四饼"),
DUI_CHANGPAI(8, "对子长牌"),
DUI_DUANPAI(7, "对子短牌"),
DUI_ZAPAI(6, "对子杂牌"),
TIANGANG(5, "天杠"), // 白板+8点
DIGANG(4, "地杠"), // 二饼+8点
NORMAL(0, "普通")
}
// 检查特殊牌型
fun checkSpecialType(hand: List<Pai>): SpecialType {
if (hand.size != 2) return SpecialType.NORMAL
val pai1 = hand[0]
val pai2 = hand[1]
// 检查三万+六万(至尊)
if ((pai1.displayName == "三万" && pai2.displayName == "六万") ||
(pai1.displayName == "六万" && pai2.displayName == "三万")) {
return SpecialType.SANWAN_LIUWAN
}
// 检查对子(同一种牌)
if (pai1.displayName == pai2.displayName) {
return when (pai1.type) {
PaiType.TIAN -> SpecialType.DUI_BAIBAN
PaiType.DI -> SpecialType.DUI_ERBING
PaiType.REN -> SpecialType.DUI_BAIBING
PaiType.E -> SpecialType.DUI_SIBING
PaiType.CHANG -> SpecialType.DUI_CHANGPAI
PaiType.DUAN -> SpecialType.DUI_DUANPAI
PaiType.ZA -> SpecialType.DUI_ZAPAI
}
}
// 检查天杠(白板+任意8点的牌)
if ((pai1.displayName == "白板" && pai2.points == 8) ||
(pai2.displayName == "白板" && pai1.points == 8)) {
return SpecialType.TIANGANG
}
// 检查地杠(二饼+任意8点的牌)
if ((pai1.displayName == "二饼" && pai2.points == 8) ||
(pai2.displayName == "二饼" && pai1.points == 8)) {
return SpecialType.DIGANG
}
return SpecialType.NORMAL
}
// 检查推牌九结果
fun checkPaiGow(hand: List<Pai>): String {
if (hand.size != 2) return "错误:手牌数量不对"
val specialType = checkSpecialType(hand)
val points = hand.sumOf { it.points } % 10
return if (specialType != SpecialType.NORMAL) {
"${specialType.displayName} (${hand[0].displayName} + ${hand[1].displayName})"
} else {
"点数==>$points (${hand[0].displayName} + ${hand[1].displayName})"
}
}
// 获取手牌点数
fun getHandPoints(hand: List<Pai>): Int {
return hand.sumOf { it.points } % 10
}
// 获取最大的牌
fun getBestPai(hand: List<Pai>): Pai {
return hand.maxOrNull() ?: hand[0]
}
// 比较两手牌的大小
// 返回值: 1表示hand1赢, -1表示hand2赢, 0表示平局
fun comparePaiGowHands(hand1: List<Pai>, hand2: List<Pai>): Int {
val special1 = checkSpecialType(hand1)
val special2 = checkSpecialType(hand2)
// 如果有特殊牌型,先比较特殊牌型
if (special1 != SpecialType.NORMAL || special2 != SpecialType.NORMAL) {
return when {
special1.priority > special2.priority -> 1
special1.priority < special2.priority -> -1
else -> 0
}
}
// 都是普通牌型,比较点数
val points1 = getHandPoints(hand1)
val points2 = getHandPoints(hand2)
when {
points1 > points2 -> return 1
points1 < points2 -> return -1
}
// 点数相同,比较最大的牌
val best1 = getBestPai(hand1)
val best2 = getBestPai(hand2)
return when {
best1 > best2 -> 1
best1 < best2 -> -1
else -> 0
}
}
// 判定输赢
fun judgeWinner(
bankerResult: String,
bankerHand: List<Pai>,
playersResults: List<String>,
playerHands: List<List<Pai>>
): List<String> {
val results = mutableListOf<String>()
val bankerSpecial = checkSpecialType(bankerHand)
val bankerPoints = getHandPoints(bankerHand)
val bankerBest = getBestPai(bankerHand)
playerHands.forEachIndexed { idx, playerHand ->
val playerSpecial = checkSpecialType(playerHand)
val playerPoints = getHandPoints(playerHand)
val playerBest = getBestPai(playerHand)
val compareResult = comparePaiGowHands(playerHand, bankerHand)
val resultStr = when (compareResult) {
1 -> {
// 闲家赢
if (playerSpecial != SpecialType.NORMAL) {
"赢 (${playerSpecial.displayName} > ${if (bankerSpecial != SpecialType.NORMAL) bankerSpecial.displayName else "点数$bankerPoints"})"
} else if (bankerSpecial != SpecialType.NORMAL) {
"赢 (点数$playerPoints > ${bankerSpecial.displayName})"
} else {
val diff = if (playerPoints > bankerPoints) {
"点数较大 ($playerPoints > $bankerPoints)"
} else {
"点数相同但牌型较大 (${playerBest.displayName} > ${bankerBest.displayName})"
}
"赢 ($diff)"
}
}
-1 -> {
// 闲家输
if (bankerSpecial != SpecialType.NORMAL) {
"输 (${if (playerSpecial != SpecialType.NORMAL) playerSpecial.displayName else "点数$playerPoints"} < ${bankerSpecial.displayName})"
} else if (playerSpecial != SpecialType.NORMAL) {
"输 (${playerSpecial.displayName} < 点数$bankerPoints)"
} else {
val diff = if (playerPoints < bankerPoints) {
"点数较小 ($playerPoints < $bankerPoints)"
} else {
"点数相同但牌型较小 (${playerBest.displayName} < ${bankerBest.displayName})"
}
"输 ($diff)"
}
}
else -> "平局 庄家赢"
}
results.add(resultStr)
}
return results
}
六、运行示例
运行程序后,会得到类似以下的输出结果:
玩家:庄家、 张三、 李四、 王二
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第1把
【牌型】
庄家: 点数==>7 (六万 + 一饼)
张三: 点数==>5 (一饼 + 四条)
李四: 点数==>8 (四条 + 四饼)
王二: 点数==>2 (六条 + 六饼)
【判定结果】
张三: 输 (点数较小 (5 < 7))
李四: 赢 (点数较大 (8 > 7))
王二: 输 (点数较小 (2 < 7))
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第2把
【牌型】
庄家: 点数==>8 (八饼 + 红中)
张三: 点数==>9 (九饼 + 发财)
李四: 点数==>4 (七饼 + 七条)
王二: 点数==>4 (四饼 + 发财)
【判定结果】
张三: 赢 (点数较大 (9 > 8))
李四: 输 (点数较小 (4 < 8))
王二: 输 (点数较小 (4 < 8))
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第3把
【牌型】
庄家: 点数==>5 (八条 + 七饼)
张三: 天杠 (八条 + 白板)
李四: 点数==>7 (二饼 + 五饼)
王二: 点数==>2 (七条 + 五饼)
【判定结果】
张三: 赢 (天杠 > 点数5)
李四: 赢 (点数较大 (7 > 5))
王二: 输 (点数较小 (2 < 5))
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第4把
【牌型】
庄家: 点数==>8 (六条 + 二饼)
张三: 点数==>3 (红中 + 三万)
李四: 点数==>8 (白板 + 六饼)
王二: 点数==>7 (九饼 + 八饼)
【判定结果】
张三: 输 (点数较小 (3 < 8))
李四: 赢 (点数相同但牌型较大 (白板 > 二饼))
王二: 输 (点数较小 (7 < 8))
============================================================
游戏结束!共进行了4把
============================================================
七、总结
该程序完整还原了推牌九游戏的核心规则,从数据结构设计到逻辑实现都体现了面向对象的思想,代码分层清晰、可读性高,既适合作为传统棋牌游戏数字化的示例,也可作为Kotlin面向对象编程的教学案例。通过少量优化(如洗牌逻辑、异常处理),可进一步提升程序的实用性和健壮性。