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

相关推荐
zhangphil2 小时前
Android Page 3 Flow读sql数据库媒体文件,Kotlin
android·kotlin
小书房2 小时前
Kotlin使用体验及理解1
android·开发语言·kotlin
Kapaseker3 小时前
我想让同事知道我很懂 Compose 怎么办?
android·kotlin
jinanwuhuaguo17 小时前
OpenClaw工程解剖——RAG、向量织构与“记忆宫殿”的索引拓扑学(第十三篇)
android·开发语言·人工智能·kotlin·拓扑学·openclaw
jinanwuhuaguo21 小时前
OpenClaw协议霸权——从 MCP 标准到意图封建化的政治经济学(第十八篇)
android·人工智能·kotlin·拓扑学·openclaw
zhangphil1 天前
Android sql查媒体数据封装room Dao构造AndroidViewModel,RecyclerView宫格展示,Kotlin
android·kotlin
jinanwuhuaguo1 天前
反熵共同体——OpenClaw的宇宙热力学本体论(第十七篇)
大数据·人工智能·安全·架构·kotlin·openclaw
pengyu1 天前
【Kotlin 协程修仙录 · 筑基境 · 中阶】 | 身份证与通行证:CoroutineContext 的深度解剖
android·kotlin
夏沫琅琊1 天前
android 短信读取与导出技术
android·kotlin
Kapaseker1 天前
客官,你误会 Compose Strong Skipping 了
android·kotlin