kotlin 推牌九(麻将)小游戏

一、项目整体架构与核心功能

该Kotlin程序完整实现了传统推牌九游戏的核心逻辑,包含牌组生成发牌逻辑牌型判定输赢对比四大核心模块,支持多玩家(闲家)与庄家的多轮对局,并输出每轮的牌型、点数及输赢结果。整体代码采用面向对象设计,通过数据类、枚举类封装牌的属性与类型,逻辑分层清晰,易于扩展和维护。

核心功能清单

  1. 生成32张标准推牌九牌组;
  2. 支持自定义玩家数量,自动计算可玩轮数;
  3. 洗牌后按轮次为庄家、闲家分发手牌;
  4. 判定手牌的特殊牌型(至尊、对子、天杠/地杠)与普通点数;
  5. 对比庄家与闲家手牌大小,输出每轮输赢结果;
  6. 格式化输出对局信息,提升可读性。

二、核心数据结构设计

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张)的特殊牌型:

  1. 先判定"至尊"(三万+六万);
  2. 再判定"对子"(两张牌displayName相同),并关联PaiType返回对应对子类型;
  3. 最后判定"天杠"(白板+8点)、"地杠"(二饼+8点);
  4. 无特殊牌型则返回NORMAL

3. 牌型结果计算(checkPaiGow)

整合"特殊牌型"与"普通点数",返回易读的牌型描述:

  • 有特殊牌型:返回"牌型名 (牌1 + 牌2)"(如"至尊(三万+六万) (三万 + 六万)");
  • 普通牌型:返回"点数==>X (牌1 + 牌2)"(X为两张牌点数和对10取余)。

4. 手牌对比(comparePaiGowHands)

推牌九输赢判定的核心算法,对比逻辑分层清晰:

  1. 特殊牌型优先:对比两张手牌的SpecialType优先级,优先级高则赢;
  2. 普通牌型对比
    • 先比点数(和对10取余),点数大则赢;
    • 点数相同则对比单张牌的最大优先级(通过Pai的compareTo方法)。

5. 输赢判定(judgeWinner)

遍历所有闲家手牌,调用comparePaiGowHands对比庄家手牌,生成格式化的输赢结果:

  • 闲家赢:标注赢的原因(特殊牌型/点数/单牌优先级);
  • 闲家输:标注输的原因;
  • 平局:按规则判定"庄家赢"。

6. 主函数(main)

游戏流程的入口,核心逻辑:

  1. 定义玩家列表,计算总人数、每轮牌数、可玩轮数;
  2. 生成并洗牌(仅洗牌一次,供所有轮次使用);
  3. 按轮次循环:分发手牌 → 判定牌型 → 输出输赢 → 格式化打印;
  4. 游戏结束后输出总轮数。

四、代码设计亮点

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面向对象编程的教学案例。通过少量优化(如洗牌逻辑、异常处理),可进一步提升程序的实用性和健壮性。

相关推荐
JMchen1232 小时前
跨平台相机方案深度对比:CameraX vs. Flutter Camera vs. React Native
java·经验分享·数码相机·flutter·react native·kotlin·dart
DokiDoki之父1 天前
边写软件边学kotlin(一):Kotlin语法初认识:
android·开发语言·kotlin
fundroid2 天前
Kotlin 泛型进阶:in、out 与 reified 实战
android·开发语言·kotlin
JMchen1233 天前
现代Android图像处理管道:从CameraX到OpenGL的60fps实时滤镜架构
android·图像处理·架构·kotlin·android studio·opengl·camerax
JMchen1234 天前
Android CameraX深度解析:从Camera1到CameraX的相机架构演进
android·java·数码相机·架构·kotlin·移动开发·android-studio
倔强的石头1064 天前
【Linux指南】进程控制系列(五)实战 —— 微型 Shell 命令行解释器实现
linux·运维·kotlin
Hz4535 天前
Android Jetpack核心组件协同实战:Navigation 3.X+Lifecycle+Flow+Hilt的架构革新
android·kotlin
JMchen1235 天前
Android音频编码原理与实践:从AAC到Opus,深入解析音频编码技术与移动端实现
android·经验分享·学习·kotlin·android studio·音视频·aac
JMchen1235 天前
Android音频处理全解析:从3A算法到空间音频,打造专业级音频体验
android·经验分享·算法·kotlin·android studio·音视频