├── .gitignore ├── README.md ├── build.gradle ├── gradle.properties ├── settings.gradle └── src └── main ├── kotlin ├── dada │ └── douDiZhu │ │ ├── Config.kt │ │ ├── DouDiZhu.kt │ │ ├── Game.kt │ │ ├── PlayerData.kt │ │ ├── Table.kt │ │ ├── command │ │ └── Command.kt │ │ ├── data │ │ ├── Card.kt │ │ ├── CardSet.kt │ │ ├── Combination.kt │ │ └── _Combination.kt │ │ └── utils.kt └── note.txt └── resources └── META-INF └── services └── net.mamoe.mirai.console.plugin.jvm.JvmPlugin /.gitignore: -------------------------------------------------------------------------------- 1 | # User-specific stuff 2 | .idea/ 3 | 4 | *.iml 5 | *.ipr 6 | *.iws 7 | 8 | # IntelliJ 9 | out/ 10 | # mpeltonen/sbt-idea plugin 11 | .idea_modules/ 12 | 13 | # JIRA plugin 14 | atlassian-ide-plugin.xml 15 | 16 | # Compiled class file 17 | *.class 18 | 19 | # Log file 20 | *.log 21 | 22 | # BlueJ files 23 | *.ctxt 24 | 25 | # Package Files # 26 | *.jar 27 | *.war 28 | *.nar 29 | *.ear 30 | *.zip 31 | *.tar.gz 32 | *.rar 33 | 34 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 35 | hs_err_pid* 36 | 37 | *~ 38 | 39 | # temporary files which can be created if a process still has a handle open of a deleted file 40 | .fuse_hidden* 41 | 42 | # KDE directory preferences 43 | .directory 44 | 45 | # Linux trash folder which might appear on any partition or disk 46 | .Trash-* 47 | 48 | # .nfs files are created when an open file is removed but is still being accessed 49 | .nfs* 50 | 51 | # General 52 | .DS_Store 53 | .AppleDouble 54 | .LSOverride 55 | 56 | # Icon must end with two \r 57 | Icon 58 | 59 | # Thumbnails 60 | ._* 61 | 62 | # Files that might appear in the root of a volume 63 | .DocumentRevisions-V100 64 | .fseventsd 65 | .Spotlight-V100 66 | .TemporaryItems 67 | .Trashes 68 | .VolumeIcon.icns 69 | .com.apple.timemachine.donotpresent 70 | 71 | # Directories potentially created on remote AFP share 72 | .AppleDB 73 | .AppleDesktop 74 | Network Trash Folder 75 | Temporary Items 76 | .apdisk 77 | 78 | # Windows thumbnail cache files 79 | Thumbs.db 80 | Thumbs.db:encryptable 81 | ehthumbs.db 82 | ehthumbs_vista.db 83 | 84 | # Dump file 85 | *.stackdump 86 | 87 | # Folder config file 88 | [Dd]esktop.ini 89 | 90 | # Recycle Bin used on file shares 91 | $RECYCLE.BIN/ 92 | 93 | # Windows Installer files 94 | *.cab 95 | *.msi 96 | *.msix 97 | *.msm 98 | *.msp 99 | 100 | # Windows shortcuts 101 | *.lnk 102 | 103 | .gradle 104 | build/ 105 | 106 | # Ignore Gradle GUI config 107 | gradle-app.setting 108 | 109 | # Cache of project 110 | .gradletasknamecache 111 | 112 | **/build/ 113 | 114 | # Common working directory 115 | run/ 116 | 117 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 118 | !gradle-wrapper.jar 119 | 120 | # Local Test Launch point 121 | src/test/kotlin/RunTerminal.kt 122 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # doudizhu 1.1.0 2 | 3 | 一个能在QQ上斗地主的mirai插件。 4 | 5 | 使用方法: 6 | 7 | * 首先你要熟知[mirai console](https://github.com/mamoe/mirai-console/tree/03ebfd2278e9e8f051ce7f2786fb9a33efd2dbeb)的使用方法,成功在console上运行一个bot。 8 | 9 | * 将本插件添加至plugins文件夹。 10 | * 在**游戏群**内发送“创建游戏”,即可创建一个游戏。 11 | * 创建游戏后,发送“上桌”即可加入游戏 12 | * 当上桌人数达3人后,任意玩家发送“开始游戏”即斗地主,一个能在QQ上斗地主的mirai插件。适用于mirai-console 2.6.7+ 13 | 14 | [release](https://github.com/kono-dada/doudizhu/releases/ 15 | 16 | 使用方法: 17 | 18 | 将本插件添加至plugins文件夹。 19 | 20 | 在游戏群内发送“创建游戏”,即可创建一个游戏。 21 | 22 | 创建游戏后,发送“上桌”即可加入游戏 23 | 24 | 当上桌人数达3人后,任意玩家发送“开始游戏”即可开始斗 25 | 地主。出牌阶段,发送“/<你要出的牌>”在与bot的私聊或者群聊中即可出牌。如“/10jqka”就表示出了一个顺子。 26 | 27 | 当管理员发送“结束游戏”时,游戏会被强制结束。 28 | 29 | 注意,游玩斗地主的群内不要开启发言频率限制,否则会导致bot发不出消息而报错,产生收走了你的牌但没有跳出回合的bug。 30 | 31 | 插件的特性: 32 | 33 | 覆盖全部的斗地主规则。 34 | 能够自动识别玩家出的牌是否合法。 35 | 在私聊中告知玩家所剩的牌。 36 | v1.1新特性: 37 | 38 | 加入货币系统,斗地主能赢钱了 39 | 支持胜率的统计与查询 40 | 未来可能会有的特性: 41 | 42 | 还没想到,欢迎大家留言 43 | 游戏指令: 44 | 45 | 破产时申请补助:在群内输入“/d beg” 46 | 查询自己的point数量与胜率:在群内输入“/d me” 47 | console指令: 48 | 49 | 添加群为游戏群:在console输入指令"/dc addgroup <群号>"即可。如"/dc addgroup 123456789"。 50 | 添加管理员:在console输入指令"/dc addadmin "即可。如"/dc addadmin 123456789" -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'org.jetbrains.kotlin.jvm' version '1.5.10' 3 | id 'org.jetbrains.kotlin.plugin.serialization' version '1.5.10' 4 | 5 | id 'net.mamoe.mirai-console' version '2.6.7' 6 | } 7 | 8 | group = 'dada' 9 | version = '1.0-SNAPSHOT' 10 | 11 | repositories { 12 | maven { url 'https://maven.aliyun.com/repository/public' } 13 | mavenCentral() 14 | } 15 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "douDiZhu" -------------------------------------------------------------------------------- /src/main/kotlin/dada/douDiZhu/Config.kt: -------------------------------------------------------------------------------- 1 | package dada.douDiZhu 2 | 3 | import net.mamoe.mirai.console.data.AutoSavePluginConfig 4 | import net.mamoe.mirai.console.data.value 5 | 6 | object Config : AutoSavePluginConfig("config") { 7 | val admin: MutableList by value() 8 | val groups: MutableList by value() 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/dada/douDiZhu/DouDiZhu.kt: -------------------------------------------------------------------------------- 1 | package dada.douDiZhu 2 | 3 | import dada.douDiZhu.command.Command 4 | import dada.douDiZhu.command.DouDiZhuConsoleCommand 5 | import kotlinx.coroutines.launch 6 | import net.mamoe.mirai.console.command.CommandManager.INSTANCE.register 7 | import net.mamoe.mirai.console.permission.AbstractPermitteeId 8 | import net.mamoe.mirai.console.permission.PermissionService.Companion.permit 9 | import net.mamoe.mirai.console.plugin.jvm.JvmPluginDescription 10 | import net.mamoe.mirai.console.plugin.jvm.KotlinPlugin 11 | import net.mamoe.mirai.event.globalEventChannel 12 | import net.mamoe.mirai.event.subscribeGroupMessages 13 | import net.mamoe.mirai.utils.info 14 | 15 | object DouDiZhu : KotlinPlugin( 16 | JvmPluginDescription( 17 | id = "dada.douDiZhu", 18 | version = "1.0-SNAPSHOT", 19 | ) 20 | ) { 21 | override fun onEnable() { 22 | logger.info { "Plugin loaded" } 23 | 24 | PlayerData.reload() 25 | Config.reload() 26 | 27 | Command.register() 28 | DouDiZhuConsoleCommand.register() 29 | 30 | Config.groups.forEach { 31 | AbstractPermitteeId.AnyMember(it).permit(Command.permission) 32 | } 33 | 34 | globalEventChannel().subscribeGroupMessages { 35 | case("创建游戏"){ 36 | //只有允许的群聊可以玩斗地主 37 | if (group.id in Config.groups) { 38 | launch { Game(group).gameStart() } 39 | subject.sendMessage("创建成功(底分:200)!发送“上桌”即可参与游戏") 40 | } 41 | } 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/main/kotlin/dada/douDiZhu/Game.kt: -------------------------------------------------------------------------------- 1 | package dada.douDiZhu 2 | 3 | import dada.douDiZhu.data.* 4 | import kotlinx.coroutines.CompletableJob 5 | import kotlinx.coroutines.Job 6 | import kotlinx.coroutines.SupervisorJob 7 | import kotlinx.coroutines.coroutineScope 8 | import net.mamoe.mirai.contact.Group 9 | import net.mamoe.mirai.contact.Member 10 | import net.mamoe.mirai.event.EventPriority 11 | import net.mamoe.mirai.event.events.GroupMessageEvent 12 | import net.mamoe.mirai.event.events.MessageEvent 13 | import net.mamoe.mirai.event.globalEventChannel 14 | import net.mamoe.mirai.event.subscribeGroupMessages 15 | import net.mamoe.mirai.message.data.At 16 | import net.mamoe.mirai.message.data.Message 17 | import net.mamoe.mirai.message.data.PlainText 18 | import net.mamoe.mirai.message.data.content 19 | 20 | /** 21 | * ### 游戏类 22 | * 主要分为[准备][prepare]、[发牌][faPai]、[抢地主][qiangDiZhu]、[开始][startDouDiZhu]、[结算][settle]五个阶段 23 | * 24 | * [Game][Game]本身继承[CompletableJob],在[Game][Game]内各种各样的Job都是自身的ChildJob。 25 | * 26 | * 每一个阶段创建一个ChildJob来构建一个新的eventChannel,以便在接收到特定指令的时候结束掉整个时间通道 27 | * 28 | * **在[prepare]与[settle]中,需要用到CoinManager来查询明乃币,更改明乃币。** 29 | * 30 | * @param gameGroup 游戏所在的群 31 | * @param basicBet 底分 32 | */ 33 | class Game(private val gameGroup: Group, private val basicBet: Int = 200) : CompletableJob by SupervisorJob() { 34 | private var diZhuPai = CardSet() 35 | 36 | private val table = Table() 37 | 38 | private var magnification = 1 39 | 40 | /** 41 | * ### 游戏的入口 42 | * 在游戏被创建时,即开启一个“开始游戏”和“结束游戏”的监听。前着可以拦截二次的创建指令,后者可以随时使游戏结束。 43 | */ 44 | suspend fun gameStart() { 45 | coroutineScope { 46 | val channel = globalEventChannel() 47 | .parentJob(this@Game) 48 | .filterIsInstance() 49 | .filter { event: GroupMessageEvent -> event.group == gameGroup } 50 | 51 | // 当游戏存在时拦截创建游戏的指令 52 | channel.subscribeGroupMessages(priority = EventPriority.HIGH) { 53 | (case("创建游戏") and sentFrom(gameGroup)) reply { 54 | this.intercept() 55 | "已经有一个游戏了" 56 | } 57 | //强制结束游戏 58 | //只有Config中的admin可以结束游戏 59 | (case("结束游戏") and sentFrom(gameGroup)) { 60 | if (sender.id in Config.admin) { 61 | group.sendMessage("结束成功") 62 | this@Game.cancel() 63 | } 64 | } 65 | "当前玩家" reply { "当前玩家:${table.players.map { it.nick }}" } 66 | } 67 | } 68 | prepare() 69 | } 70 | 71 | /** 72 | * ### 准备 73 | * 玩家从此处进入游戏 74 | */ 75 | private suspend fun prepare() { 76 | var started = false 77 | /* 78 | 创建一个job用于终结订阅器,这个job是this的子job 79 | */ 80 | val prepareJob = Job(this) 81 | val scopedChannel = coroutineScope { 82 | globalEventChannel().parentJob(prepareJob) 83 | .filterIsInstance() 84 | .filter { event: GroupMessageEvent -> event.group == gameGroup } 85 | } 86 | val job = scopedChannel.subscribeGroupMessages { 87 | (case("上桌") and sentFrom(gameGroup)) reply { 88 | if (!sender.enough(200)) { 89 | "你的point不够200个哦,你没钱了" 90 | } else if (table.enter(sender)) { 91 | "加入成功\n当前玩家:${table.players.map { it.nick }}" 92 | } else { 93 | "人满了或你已经在游戏中了,无法加入" 94 | } 95 | } 96 | case("下桌") { 97 | if (sender in table.players) { 98 | table.players.remove(sender) 99 | subject.sendMessage("<${sender.nick}>下桌成功") 100 | } 101 | } 102 | (case("开始游戏")) reply { 103 | if (sender in table.players) { 104 | if (table.isFull()) { 105 | started = true 106 | prepareJob.cancel() 107 | } else { 108 | "还没满人,无法开始\n" + "当前玩家:${table.players.map { it.nick }}" 109 | } 110 | } 111 | } 112 | } 113 | //等待job结束,即成功开始游戏 114 | job.join() 115 | if (started) faPai() 116 | } 117 | 118 | private suspend fun faPai() { 119 | val cardSet = 120 | (Card.cards() + Card.cards() + Card.cards() + Card.cards() + Card.jokers()).shuffled().toCardSet() 121 | diZhuPai = cardSet.subList(51, 54).toCardSet() 122 | cardSet.removeAt(53) 123 | cardSet.removeAt(52) 124 | cardSet.removeAt(51) 125 | table.handCard = mutableMapOf( 126 | Pair(table.players[0], HandCards(cardSet.subList(0, 17))), 127 | Pair(table.players[1], HandCards(cardSet.subList(17, 34))), 128 | Pair(table.players[2], HandCards(cardSet.subList(34, 51))) 129 | ) 130 | for (player in table.players) { 131 | player.sendMessage(player.handCards().toSortedString()) 132 | } 133 | qiangDiZhu() 134 | } 135 | 136 | /** 137 | * 逻辑: 138 | * 1.创建一个迭代器,轮换地返回一个玩家 139 | * 2.在迭代时,每前进一个玩家,打开一次监听任务,监听其是否抢地主,直到有答复 140 | * 3.关闭job 141 | */ 142 | private suspend fun qiangDiZhu() { 143 | var qiang = false 144 | for (player in table.players) { 145 | if (this.isActive) 146 | reply(At(player) + PlainText("轮到你抢地主了,是否要抢地主?")) 147 | 148 | val qiangDiZhuJob = Job(this) 149 | 150 | val gameEventChannel = coroutineScope { 151 | globalEventChannel() 152 | .parentJob(qiangDiZhuJob) 153 | .filterIsInstance() 154 | .filter { event: GroupMessageEvent -> event.sender == player } 155 | } 156 | //2 157 | val job = if (this.isActive) { 158 | gameEventChannel.subscribeGroupMessages { 159 | (case("抢地主") or case("我抢") or case("抢")){ 160 | qiang = true 161 | qiangDiZhuJob.cancel() 162 | } 163 | (case("不抢")){ 164 | qiangDiZhuJob.cancel() 165 | } 166 | } 167 | } else { 168 | return 169 | } 170 | 171 | job.join() 172 | if (qiang) { 173 | reply(At(player) + PlainText("当上了地主")) 174 | reply("地主牌是$diZhuPai") 175 | table.index = table.players.indexOf(player) 176 | table.diZhu = player 177 | table.nongMin = table.players.mapNotNull { if (it != table.diZhu) it else null } 178 | player.handCards().addAll(diZhuPai) 179 | player.sendMessage("你当上了地主,你的地主牌是$diZhuPai") 180 | player.sendMessage("你现在有\n${player.handCards().toSortedString()}") 181 | break 182 | } 183 | } 184 | if (qiang) 185 | startDouDiZhu() //如果有人抢地主就开始 186 | else 187 | faPai() //如果无人抢地主就重新发牌 188 | } 189 | 190 | private suspend fun startDouDiZhu() { 191 | var isRunning = true 192 | 193 | var lastCardSet = CardSet() 194 | var lastCombination: Combination = NotACombination 195 | var lastPlayer = table.players[table.index] //上次出牌的玩家。如果连续两个玩家没出牌,就知道该更新牌型了 196 | 197 | /* 198 | 玩家轮流出牌。 199 | */ 200 | for (player in table.iterator()) { 201 | if (player == lastPlayer) lastCombination = NotACombination 202 | 203 | if (this.isActive) 204 | reply(At(player) + "轮到你出牌了") 205 | val startJob = Job(this) 206 | 207 | val gameEventChannel = coroutineScope { 208 | globalEventChannel() 209 | .parentJob(startJob) 210 | .filterIsInstance() 211 | .filter { event: MessageEvent -> event.sender.id == player.id } 212 | } 213 | 214 | /* 215 | 玩家每次发送以“/“开头的消息,都视为一次出牌请求。出牌请求有多种处理结果,包括 216 | 1.玩家并没有要出的牌 217 | 2.玩家出牌不符合规则(与上家出的牌不是同类型的,或者比上家出的小) 218 | 3.玩家把所有的牌都出完了,赢得游戏 219 | 4.玩家顺利出牌,并进入下家的回合 220 | 5.玩家跳过 221 | 以上结果都会迎来onEvent的结束,但只有情况4和情况5会顺利进入下一家的回合 222 | 其中,情况3会直接进入settle环节 223 | */ 224 | val job = if (this.isActive) { 225 | gameEventChannel.subscribeAlways playCard@{ 226 | if (message.content[0].toString() == "/") { 227 | val rawCardsString = message.content.substring(1) 228 | val deserializedCards = rawCardsString.deserializeToCard() 229 | if (!(player.handCards() have deserializedCards)) { 230 | player.sendMessage("没在你的牌中找到你想出的牌哦") 231 | return@playCard 232 | } 233 | val comb = deserializedCards.findCombination() //玩家想出的牌 234 | if (comb == NotACombination) { 235 | player.sendMessage("没看懂你想要出什么牌啦") 236 | return@playCard 237 | } 238 | /* 239 | 有可能可以出牌的情况: 240 | 1.牌权回到自己手上 241 | 2.出了和上一次相同牌型并且比他大 242 | 3.上一次不是炸弹,但这次是炸弹 243 | */ 244 | if ( 245 | lastCombination == NotACombination 246 | || (lastCombination.sameType(comb) 247 | && comb > lastCombination 248 | && deserializedCards.size == lastCardSet.size) 249 | || (lastCombination !is Bomb && comb is Bomb) 250 | ) { 251 | player.play(deserializedCards) 252 | /* 253 | 炸弹有特殊回复,并且翻倍 254 | */ 255 | if (comb is Bomb) { 256 | magnification *= 2 257 | reply("炸弹!<${player.nick}>出了$comb") 258 | reply("当前倍率:$magnification") 259 | } else reply("<${player.nick}>出了$comb") 260 | if (player.handCards().size == 2) reply("<${player.nick}>只剩两张牌了哦!感觉ta要赢了") 261 | if (player.handCards().size == 1) reply("<${player.nick}>只剩一张牌了哦!感觉ta要赢了") 262 | 263 | //获胜判断 264 | if (player.handCards().size == 0) { 265 | settle(player) 266 | isRunning = false 267 | startJob.cancel() 268 | return@playCard 269 | } 270 | player.sendMessage("你还剩\n ${player.handCards()}") 271 | 272 | lastCardSet = deserializedCards 273 | lastCombination = comb 274 | lastPlayer = player 275 | startJob.cancel() 276 | return@playCard 277 | } 278 | player.sendMessage("你出的牌貌似不符合规则哦") 279 | return@playCard 280 | } 281 | 282 | if (message.content == "要不起" || message.content == "不要" || message.content == "过") { 283 | if (lastPlayer == player) player.sendMessage("这是你的回合,不可以不出哦") 284 | else { 285 | reply("<${player.nick}>选择了不出") 286 | startJob.cancel() 287 | } 288 | } 289 | } 290 | }else return 291 | job.join() 292 | if (!isRunning) { 293 | this.cancel() 294 | break 295 | } 296 | } 297 | } 298 | 299 | /* 300 | 获胜结算 301 | 斗地主的获胜结算依赖CoinManager,即明乃币的管理系统。 302 | */ 303 | private suspend fun settle(winner: Member) { 304 | //获胜场次,总场次的变化 305 | winner.douDiZhuData.winTimes += 1 306 | for (player in table.players) { 307 | player.douDiZhuData.gameTimes += 1 308 | } 309 | 310 | val amount = basicBet * magnification 311 | if (winner == table.diZhu) { 312 | winner.addPoints(amount * 2) 313 | table.nongMin.forEach { 314 | it.pay(amount) 315 | } 316 | reply( 317 | "地主赢了\n" + 318 | "<${winner.nick}>赢得了${amount * 2}个point\n" + "\n" + 319 | "<${table.nongMin[0]}.nick>、<${table.nongMin[1].nick}>输掉了${amount}个point" 320 | ) 321 | } else { 322 | table.diZhu.pay(amount * 2) 323 | table.nongMin.forEach { 324 | it.addPoints(amount) 325 | } 326 | reply( 327 | "农民赢了\n" + 328 | "<${table.nongMin[0].nick}>、<${table.nongMin[1].nick}>赢得了了${amount}个point" + "\n" + 329 | "<${table.diZhu.nick}>输掉了${amount * 2}个个point\n" 330 | ) 331 | } 332 | } 333 | 334 | //工具函数,别忘了 335 | private fun Member.handCards(): HandCards = table.handCard[this]!! 336 | 337 | private fun Member.play(cardSet: CardSet) = handCards().play(cardSet) 338 | 339 | private suspend fun reply(msg: Message) { 340 | gameGroup.sendMessage(msg) 341 | } 342 | 343 | private suspend fun reply(msg: String) { 344 | gameGroup.sendMessage(msg) 345 | } 346 | 347 | } 348 | -------------------------------------------------------------------------------- /src/main/kotlin/dada/douDiZhu/PlayerData.kt: -------------------------------------------------------------------------------- 1 | package dada.douDiZhu 2 | 3 | import kotlinx.serialization.Serializable 4 | import net.mamoe.mirai.console.data.AutoSavePluginData 5 | import net.mamoe.mirai.console.data.value 6 | import java.util.* 7 | 8 | object PlayerData : AutoSavePluginData("playerData") { 9 | var data: MutableMap by value() 10 | } 11 | 12 | @Serializable 13 | data class CustomData( 14 | var coins: Int = 0, 15 | var winTimes: Int = 0, 16 | var gameTimes: Int = 0, 17 | var lastApplyTime: Long = 0L 18 | ){ 19 | fun addPoints(number: Int) { 20 | if (number <= 0) throw Exception("AddMinusPoints") 21 | coins += number 22 | } 23 | 24 | fun pay(number: Int) { 25 | if (number <= 0) throw Exception("PayMinusPoints") 26 | coins -= number 27 | } 28 | 29 | fun dailyApply(): String { 30 | val lastTieDay = lastApplyTime / 1000 / 60 / 60 / 24 31 | val today = Date().time / 1000 / 60 / 60 / 24 32 | return if (today - lastTieDay >= 1) { 33 | coins += 500 34 | lastApplyTime = Date().time 35 | "又输光了吗……喏,这是500个point,别再输了哦" 36 | } else 37 | "你今天已经领取过500个point了,别得寸进尺了哦!" 38 | } 39 | } -------------------------------------------------------------------------------- /src/main/kotlin/dada/douDiZhu/Table.kt: -------------------------------------------------------------------------------- 1 | package dada.douDiZhu 2 | 3 | import dada.douDiZhu.data.HandCards 4 | import net.mamoe.mirai.contact.Member 5 | 6 | /** 7 | * ### 牌桌 8 | * 记录玩家数据(手牌等),构造迭代器以进行周而复始的回合制游戏 9 | */ 10 | class Table : Iterable { 11 | val players = mutableListOf() 12 | 13 | var index = 0 14 | 15 | lateinit var diZhu: Member 16 | lateinit var nongMin: List 17 | 18 | /* 19 | 构建玩家到手牌的映射 20 | */ 21 | lateinit var handCard: MutableMap 22 | 23 | fun enter(player: Member): Boolean { 24 | return if (!isFull() && player !in players) { 25 | players.add(player) 26 | true 27 | } else { 28 | false 29 | } 30 | } 31 | 32 | fun isFull(): Boolean { 33 | return players.size == 3 34 | } 35 | 36 | override fun iterator(): Iterator { 37 | return TableIterator(players, index) 38 | } 39 | 40 | class TableIterator(private val players: List, private var index: Int = 0) : Iterator { 41 | override fun next(): Member { 42 | val p = players[index] 43 | if (index < 2) { 44 | index++ 45 | } else index = 0 46 | return p 47 | } 48 | 49 | override fun hasNext(): Boolean { 50 | return true 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/main/kotlin/dada/douDiZhu/command/Command.kt: -------------------------------------------------------------------------------- 1 | package dada.douDiZhu.command 2 | 3 | import dada.douDiZhu.* 4 | import net.mamoe.mirai.console.command.CompositeCommand 5 | import net.mamoe.mirai.console.command.ConsoleCommandSender 6 | import net.mamoe.mirai.console.command.UserCommandSender 7 | import net.mamoe.mirai.console.permission.AbstractPermitteeId 8 | import net.mamoe.mirai.console.permission.PermissionService.Companion.permit 9 | 10 | object Command : CompositeCommand( 11 | DouDiZhu, 12 | "d", 13 | ) { 14 | @SubCommand("beg") 15 | suspend fun UserCommandSender.beg() { 16 | val msg = user.data.dailyApply() 17 | subject.sendMessage(msg) 18 | } 19 | 20 | //查询玩家的胜率 21 | @SubCommand("me") 22 | suspend fun UserCommandSender.me() { 23 | subject.sendMessage("<${user.nick}>现在有${user.data.coins}个point,总共进行了${user.gameTimes}场游戏," + 24 | "获胜${user.winTimes}场,胜率${user.winRate}") 25 | } 26 | 27 | 28 | } 29 | 30 | /** 31 | * 斗地主管理指令 32 | * 可以进行管理员和群聊的添加 33 | */ 34 | object DouDiZhuConsoleCommand : CompositeCommand( 35 | DouDiZhu, 36 | "doudizhucommand", "dc" 37 | ) { 38 | @SubCommand("addadmin") 39 | suspend fun ConsoleCommandSender.addAdmin(id: Long) { 40 | Config.admin.add(id) 41 | AbstractPermitteeId.ExactUser(id).permit(Command.permission) 42 | sendMessage("OK") 43 | } 44 | 45 | @SubCommand("addgroup") 46 | suspend fun ConsoleCommandSender.addGroup(id: Long) { 47 | Config.groups.add(id) 48 | AbstractPermitteeId.AnyMember(id).permit(Command.permission) 49 | sendMessage("OK") 50 | } 51 | } -------------------------------------------------------------------------------- /src/main/kotlin/dada/douDiZhu/data/Card.kt: -------------------------------------------------------------------------------- 1 | package dada.douDiZhu.data 2 | 3 | /** 4 | * ### 牌 5 | * @param value 牌的值,可以看做牌的大小关系 6 | * @param id 牌的字符串形式 7 | */ 8 | enum class Card(val value: Int, val id: String) { 9 | THREE(1, "3"), 10 | FOUR(2, "4"), 11 | FIVE(3, "5"), 12 | SIX(4, "6"), 13 | SEVEN(5, "7"), 14 | EIGHT(6, "8"), 15 | NINE(7, "9"), 16 | TEN(8, "10"), 17 | J(9, "J"), 18 | Q(10, "Q"), 19 | K(11, "K"), 20 | A(12, "A"), 21 | TWO(13, "2"), 22 | GREY_JOKER(14, "鬼"), 23 | COLORFUL_JOKER(15, "王"), 24 | 25 | //在玩家乱出牌的时候用的牌类型 26 | NOT_A_CARD(100, ""); 27 | 28 | internal companion object { 29 | internal fun deserializeFromString(id: String): Card = 30 | values().firstOrNull { it.id == id.toUpperCase() } ?: NOT_A_CARD 31 | 32 | internal fun deserializeFromInt(value: Int): Card = 33 | values().firstOrNull { it.value == value } ?: NOT_A_CARD 34 | 35 | internal fun cards(): CardSet = 36 | CardSet(A, TWO, THREE, FOUR, FIVE, SIX, SEVEN, EIGHT, NINE, TEN, J, Q, K) 37 | 38 | internal fun jokers(): CardSet = CardSet(GREY_JOKER, COLORFUL_JOKER) 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/dada/douDiZhu/data/CardSet.kt: -------------------------------------------------------------------------------- 1 | package dada.douDiZhu.data 2 | 3 | 4 | /** 5 | * ### 牌的集合 6 | * 牌的集合,继承[ArrayList] 7 | * 可以传入多张牌,也可以传入一个包含了牌的列表 8 | */ 9 | open class CardSet : ArrayList { 10 | constructor(vararg elements: Card) : super(elements.toMutableList()) 11 | constructor(list:List) : super(list) 12 | 13 | //手牌的包含关系 14 | //用来判断玩家是否拥有他想要打出的手牌 15 | infix fun have(another: CardSet): Boolean { 16 | val copy = ArrayList(this) 17 | another.forEach { 18 | if (!copy.remove(it)) return false 19 | } 20 | return true 21 | } 22 | 23 | fun sortByValue() = sortBy { it.value } 24 | 25 | /** 26 | * 排序后变成字符串 27 | */ 28 | fun toSortedString(): String { 29 | sortByValue() 30 | var str = "" 31 | forEach { 32 | str += "[${it.id}]" 33 | } 34 | return str 35 | } 36 | 37 | /** 38 | * 不排序变成字符串 39 | */ 40 | override fun toString(): String { 41 | var str = "" 42 | forEach { 43 | str += "[${it.id}]" 44 | } 45 | return str 46 | } 47 | 48 | override fun equals(other: Any?): Boolean { 49 | return if (other is CardSet) { 50 | this have other && other have this 51 | } else false 52 | } 53 | 54 | //没什么卵用,但是不写会有警告,看着不爽 55 | override fun hashCode(): Int { 56 | return 0 57 | } 58 | 59 | } 60 | 61 | /** 62 | * ### 玩家的手牌 63 | */ 64 | class HandCards(elements: MutableList) : CardSet(elements) { 65 | 66 | //出牌 67 | fun play(cardSet: CardSet) { 68 | cardSet.forEach { 69 | this.remove(it) 70 | } 71 | } 72 | } 73 | 74 | 75 | fun List.toCardSet(): CardSet { 76 | return CardSet(*this.toTypedArray()) 77 | } 78 | 79 | /** 80 | * 懒得写四带两张的解析,干脆把全部四带两张的枚举了吧 81 | */ 82 | object AllOfQuadrupleWithTwoSingles : ArrayList() { 83 | init { 84 | for (i in (1..13)) { 85 | val qua = Card.deserializeFromInt(i) 86 | for (j in (1..15)) { 87 | for (k in (1..15)) { 88 | add(CardSet(qua, qua, qua, qua, Card.deserializeFromInt(j), Card.deserializeFromInt(k))) 89 | } 90 | } 91 | } 92 | } 93 | } -------------------------------------------------------------------------------- /src/main/kotlin/dada/douDiZhu/data/Combination.kt: -------------------------------------------------------------------------------- 1 | package dada.douDiZhu.data 2 | 3 | /** 4 | * ### 牌的组合 5 | * 在玩家打牌后,[解析器][deserializeToCard]会把玩家希望出的牌解析为一个[组合][Combination], 6 | * 其中包含了合法的组合与[不合法的组合][NotACombination] 7 | * @param comparableCard 由于组合是可比较大小的,每个子类需要递交一个[用于比较大小的牌][comparableCard]。 8 | 例如[三带一][TripleWithSingle] 4445中,用于比较大小的牌就是4 9 | */ 10 | open class Combination(private val comparableCard: Card) { 11 | fun sameType(another: Combination): Boolean = (this::class == another::class || (this is Bomb && another is Bomb)) 12 | 13 | open operator fun compareTo(other: Combination): Int = when { 14 | comparableCard > other.comparableCard -> 1 15 | comparableCard == other.comparableCard -> 0 16 | comparableCard < other.comparableCard -> -1 17 | else -> 0 18 | } 19 | 20 | } 21 | 22 | /** 23 | * ### 在玩家乱出牌时用的组合 24 | */ 25 | object NotACombination : Combination(Card.NOT_A_CARD) 26 | 27 | /** 28 | * ### 单牌 29 | */ 30 | class Single(private val card: Card) : Combination(card) { 31 | override fun toString(): String = CardSet(card).toString() 32 | } 33 | 34 | fun CardSet.isSingle(): Single? = if (size == 1) Single(get(0)) else null 35 | 36 | /** 37 | * ### 对子 38 | */ 39 | class Double(private val card: Card) : Combination(card) { 40 | override fun toString(): String = CardSet(card, card).toString() 41 | } 42 | 43 | fun CardSet.isDouble(): Double? = 44 | if (size == 2 && this[1] == this[0]) 45 | Double(this[1]) 46 | else null 47 | 48 | /** 49 | * ### 三张 50 | */ 51 | class Triple(private val card: Card) : Combination(card) { 52 | override fun toString(): String = CardSet(card, card, card).toString() 53 | } 54 | 55 | fun CardSet.isTriple(): Triple? = 56 | if (size == 3 && all { it == this[0] }) Triple(this[0]) else null 57 | 58 | /** 59 | * ### 炸弹 60 | * 炸弹单独算接口,因为出牌时不用符合上一个组合的规则 61 | */ 62 | abstract class Bomb(comparableCard: Card) : Combination(comparableCard) 63 | 64 | /** 65 | * ### 普通炸弹 66 | */ 67 | class OrdinaryBomb(private val card: Card) : Bomb(card) { 68 | override fun toString(): String = CardSet(card, card, card, card).toString() 69 | } 70 | 71 | fun CardSet.isBomb(): OrdinaryBomb? = 72 | if (size == 4 && all { it == this[0] }) OrdinaryBomb(this[0]) else null 73 | 74 | /** 75 | * ### 王炸 76 | */ 77 | class JokerBomb : Bomb(Card.NOT_A_CARD) { 78 | override fun toString(): String = "[鬼][王]" 79 | } 80 | 81 | fun CardSet.isJokerBomb(): JokerBomb? = 82 | if (size == 2 && Card.COLORFUL_JOKER in this && Card.GREY_JOKER in this) 83 | JokerBomb() 84 | else null 85 | 86 | /** 87 | * ### 三带一 88 | */ 89 | class TripleWithSingle(private val triple: Card, private val single: Card) : Combination(triple) { 90 | override fun toString(): String = CardSet(triple, triple, triple, single).toString() 91 | } 92 | 93 | /* 94 | 解析思路: 95 | 1.这个组合必须是size==4,它除去重复项后的set只有2个元素 96 | 2.set中随便挑一个元素first,如果它在this中出现了1次,就是single;如果出现3次,就是triple;如果是其他情况,就啥也不是 97 | 3.下同 98 | */ 99 | fun CardSet.isTripleWithSingle(): TripleWithSingle? { 100 | val set = toSet() 101 | return if (size == 4 && set.size == 2) { 102 | val first = set.toList()[0] 103 | val second = set.toList()[1] 104 | var numberOfFirst = 0 105 | for (card in this) { 106 | if (card == first) numberOfFirst += 1 107 | } 108 | when (numberOfFirst) { 109 | 1 -> TripleWithSingle(second, first) 110 | 3 -> TripleWithSingle(first, second) 111 | else -> null 112 | } 113 | } else null 114 | } 115 | 116 | /** 117 | * ### 三带一对 118 | */ 119 | class TripleWithDouble(private val triple: Card, private val double: Card) : Combination(triple) { 120 | override fun toString(): String = CardSet(triple, triple, triple, double, double).toString() 121 | } 122 | 123 | fun CardSet.isTripleWithDouble(): TripleWithDouble? { 124 | val set = toSet() 125 | return if (size == 5 && set.size == 2) { 126 | val first = set.toList()[0] 127 | val second = set.toList()[1] 128 | var numberOfFirst = 0 129 | for (card in this) { 130 | if (card == first) numberOfFirst += 1 131 | } 132 | when (numberOfFirst) { 133 | 2 -> TripleWithDouble(second, first) 134 | 3 -> TripleWithDouble(first, second) 135 | else -> null 136 | } 137 | } else null 138 | } 139 | 140 | /** 141 | * ### 四带一 142 | */ 143 | class QuadrupleWithSingle(private val quadruple: Card, private val single: Card) : Combination(quadruple) { 144 | override fun toString(): String = CardSet(quadruple, quadruple, quadruple, quadruple, single).toString() 145 | } 146 | 147 | fun CardSet.isQuadrupleWithSingle(): QuadrupleWithSingle? { 148 | val set = toSet() 149 | return if (size == 5 && set.size == 2) { 150 | val first = set.toList()[0] 151 | val second = set.toList()[1] 152 | var numberOfFirst = 0 153 | for (card in this) { 154 | if (card == first) numberOfFirst += 1 155 | } 156 | when (numberOfFirst) { 157 | 1 -> QuadrupleWithSingle(second, first) 158 | 4 -> QuadrupleWithSingle(first, second) 159 | else -> null 160 | } 161 | } else null 162 | } 163 | 164 | /** 165 | * ### 四带二 166 | */ 167 | class QuadrupleWithTwoSingles(private val quadruple: Card, private val rest: CardSet) : Combination(quadruple) { 168 | override fun toString(): String = CardSet(quadruple, quadruple, quadruple, quadruple, rest[0], rest[1]).toString() 169 | } 170 | 171 | /* 172 | 旧逻辑: 173 | 1.产生一个去除重复项的set 174 | 2.遍历这个set,如果其中一个card在this中出现了4次,则认为这是一个四带二 175 | 3.用qua记录出现了四次的牌,剩下的塞进rest 176 | 177 | 新逻辑: 178 | 1.构建一个包含所有可能的四带二的集合,检查是否存在 179 | */ 180 | fun CardSet.isQuadrupleWithTwoSingles(): QuadrupleWithTwoSingles? { 181 | for (cardSet in AllOfQuadrupleWithTwoSingles) { 182 | if (this == cardSet) return QuadrupleWithTwoSingles(cardSet[0], cardSet.subList(4, 6).toCardSet()) 183 | } 184 | return null 185 | } 186 | 187 | /** 188 | * ### 顺子 189 | */ 190 | class Smooth(private val start: Card, private val end: Card) : Combination(start) { 191 | override fun toString(): String { 192 | val cardSet = CardSet() 193 | for (i in (start.value..end.value)) { 194 | cardSet += Card.deserializeFromInt(i) 195 | } 196 | return cardSet.toString() 197 | } 198 | } 199 | 200 | /* 201 | 解析思路: 202 | 1.先把牌排序,最后一张不能超过“2” 203 | 2.如果每个牌都比前一个牌大1,就是顺子,否则不是顺子 204 | 3.连对也是一样 205 | */ 206 | fun CardSet.isSmooth(): Smooth? { 207 | sortByValue() 208 | return if (this[lastIndex].value <= 12 && this.size >= 5) { 209 | for (i in (0 until size - 1)) { 210 | if (this[i + 1].value - this[i].value != 1) return null 211 | } 212 | return Smooth(this[0], this[lastIndex]) 213 | } else null 214 | } 215 | 216 | /** 217 | * ### 连对 218 | */ 219 | class DoubleSeries(private val start: Card, private val end: Card) : Combination(start) { 220 | override fun toString(): String { 221 | val cardSet = CardSet() 222 | for (i in (start.value..end.value)) { 223 | cardSet += Card.deserializeFromInt(i) 224 | cardSet += Card.deserializeFromInt(i) 225 | } 226 | return cardSet.toString() 227 | } 228 | } 229 | 230 | /* 231 | 类似上面的算法,这次步长为2 232 | */ 233 | fun CardSet.isDoubleSeries(): DoubleSeries? { 234 | sortByValue() 235 | return if (this[lastIndex].value <= 12 && this.size >= 6) { 236 | for (i in ((0..size - 3) step 2)) { 237 | if ( 238 | this[i + 2].value - this[i].value != 1 || 239 | this[i] != this[i + 1] || 240 | this[i + 2].value != this[i + 3].value 241 | ) return null 242 | } 243 | return DoubleSeries(this[0], this[lastIndex]) 244 | } else null 245 | } 246 | 247 | /** 248 | * ### 连三 249 | */ 250 | class TripleSeries(private val triple: CardSet) : Combination(triple[0]) { 251 | override fun toString(): String { 252 | val cardSet = CardSet() 253 | triple.sortByValue() 254 | for (i in triple) cardSet.addAll(CardSet(i, i, i)) 255 | return cardSet.toString() 256 | } 257 | } 258 | 259 | fun CardSet.isTripleSeries(): TripleSeries? { 260 | val set = toSet().toList().toCardSet() 261 | for (i in set) if (count { it == i } != 3) return null 262 | for (i in (0 until set.size - 1)) if (set[i + 1].value - set[i].value != 1) return null 263 | return TripleSeries(set) 264 | } 265 | 266 | /** 267 | * ### 飞机 268 | */ 269 | class PlaneWithSingle(private val triple: CardSet, private val single: CardSet) : Combination(triple[0]) { 270 | override fun toString(): String { 271 | val cardSet = CardSet() 272 | for (i in triple) 273 | cardSet.addAll(CardSet(i, i, i)) 274 | cardSet.sortByValue() 275 | for (i in single) 276 | cardSet.add(i) 277 | return cardSet.toString() 278 | } 279 | } 280 | 281 | fun CardSet.isPlaneWithSingle(): PlaneWithSingle? { 282 | if (size % 4 != 0) return null 283 | val single = this.subList(size - size / 4, lastIndex + 1).toCardSet() 284 | val triple = this.subList(0, size - size / 4).toCardSet() 285 | //以下三句:判断triple的部分是否合法 286 | val tripleSet = triple.toSet().toList().toCardSet() 287 | for (card in tripleSet) if (triple.count { card == it } != 3) return null 288 | for (i in (0 until tripleSet.size - 1)) if (tripleSet[i + 1].value - tripleSet[i].value != 1) return null 289 | 290 | return PlaneWithSingle(tripleSet, single) 291 | } 292 | 293 | /** 294 | * ### 飞机带一对 295 | */ 296 | class PlaneWithDouble(private val triple: CardSet, private val double: CardSet) : Combination(triple[0]) { 297 | override fun toString(): String { 298 | val cardSet = CardSet() 299 | for (i in triple) 300 | cardSet.addAll(CardSet(i, i, i)) 301 | cardSet.sortByValue() 302 | for (i in double) 303 | cardSet.addAll(CardSet(i, i)) 304 | return cardSet.toString() 305 | } 306 | } 307 | 308 | fun CardSet.isPlaneWithDouble(): PlaneWithDouble? { 309 | if (size % 5 != 0) return null 310 | val double = this.subList(size - (size / 5) * 2, lastIndex + 1).toCardSet() 311 | val triple = this.subList(0, size - (size / 5) * 2).toCardSet() 312 | val tripleSet = triple.toSet().toList().toCardSet() 313 | for (card in tripleSet) if (triple.count { card == it } != 3) return null 314 | for (i in (0 until tripleSet.size - 1)) if (tripleSet[i + 1].value - tripleSet[i].value != 1) return null 315 | 316 | val doubleSet = double.toSet().toList().toCardSet() 317 | for (card in doubleSet) if (double.count { card == it } != 3) return null 318 | return PlaneWithDouble(tripleSet, doubleSet) 319 | } -------------------------------------------------------------------------------- /src/main/kotlin/dada/douDiZhu/data/_Combination.kt: -------------------------------------------------------------------------------- 1 | package dada.douDiZhu.data 2 | 3 | import dada.douDiZhu.DouDiZhu 4 | 5 | /** 6 | * 把字符串变成CardSet 7 | */ 8 | fun String.deserializeToCard(): CardSet { 9 | val cardSet = CardSet() 10 | var raw = this 11 | 12 | //接下来处理“10”这个数字 13 | //递归搜索10, 14 | fun find10() { 15 | val indexOfTen = raw.indexOf("10") 16 | if (indexOfTen == -1) { 17 | return 18 | } else { 19 | cardSet += Card.TEN 20 | raw = raw.removeRange(indexOfTen, indexOfTen + 2) 21 | find10() 22 | } 23 | } 24 | find10() 25 | //结束10的处理,剩下的字符串中没有10. 26 | raw.forEach { cardSet += Card.deserializeFromString(it.toString()) } 27 | return cardSet 28 | } 29 | 30 | /** 31 | * 找到对应的[Combination]。如果是乱出牌的,则返回[NotACombination] 32 | */ 33 | fun CardSet.findCombination(): Combination { 34 | return listOf( 35 | isSingle(), 36 | isDouble(), 37 | isTriple(), 38 | isBomb(), 39 | isJokerBomb(), 40 | isTripleWithSingle(), 41 | isTripleWithDouble(), 42 | isQuadrupleWithSingle(), 43 | isQuadrupleWithTwoSingles(), 44 | isSmooth(), 45 | isDoubleSeries(), 46 | isTripleSeries(), 47 | isPlaneWithSingle(), 48 | isPlaneWithDouble() 49 | ).firstOrNull { it != null } ?: NotACombination 50 | } 51 | -------------------------------------------------------------------------------- /src/main/kotlin/dada/douDiZhu/utils.kt: -------------------------------------------------------------------------------- 1 | package dada.douDiZhu 2 | 3 | import net.mamoe.mirai.contact.User 4 | 5 | val User.douDiZhuData 6 | get() = PlayerData.data.getOrPut(this.id) { CustomData(0, 0) } 7 | 8 | val User.winTimes 9 | get() = douDiZhuData.winTimes 10 | 11 | val User.gameTimes 12 | get() = douDiZhuData.gameTimes 13 | 14 | val User.winRate 15 | get() = winTimes.toFloat() / gameTimes.toFloat() 16 | 17 | val User.data 18 | get() = PlayerData.data.getOrPut(this.id) { CustomData() } 19 | 20 | fun User.addPoints(number: Int) { 21 | this.data.addPoints(number) 22 | } 23 | 24 | fun User.pay(number: Int) { 25 | this.data.pay(number) 26 | } 27 | 28 | fun User.enough(number: Int): Boolean = this.data.coins >= number -------------------------------------------------------------------------------- /src/main/kotlin/note.txt: -------------------------------------------------------------------------------- 1 | //已废弃,这是识别飞机的沙雕过程 2 | /* 3 | 吐了,头一次写这么复杂的 4 | 先把有三张的挑出来,分为以下情况: 5 | 1.飞机不带单牌 -> 判断三连是否为数字连续的n组 6 | 2.三飞带的三张牌是同一张牌(总共12张牌) -> 判断是否存在三组数字连续的三连 7 | 3.正常的飞机 -> 判断三连是否为数字连续的n组,并且单牌数量与三连组数一样 8 | 9 | fun CardSet.isPlane(): Plane? { 10 | //求某张牌在this中的出现次数 11 | fun time(card: Card): Int { 12 | var n = 0 13 | forEach { 14 | if (it == card) n += 1 15 | } 16 | return n 17 | } 18 | 19 | val set = toSet() 20 | if (size % 4 == 0) { 21 | val timesMap = set.associateWith { time(it) } //牌与其在this中出现次数的映射 22 | val triple = mapNotNull { if (timesMap[it] == 3) it else null }.toCardSet() //含有三张的牌 23 | triple.sortByValue() 24 | removeAll(triple)//只剩下没有出现三次的牌 25 | if (triple.size != size) { 26 | if (size == 0) { //如果单牌的数量与三连的数量不符合且单牌不是0张 27 | if (triple.size == 4) { //有可能是333444555777这种三飞带的单牌是同一张 28 | return if (triple[2].value - triple[1].value == 1) { 29 | when { 30 | triple[1].value - triple[0].value == 1 -> Plane( 31 | triple.subList(0, 3).toCardSet(), 32 | triple.subList(3, 4).toCardSet() 33 | ) 34 | triple[3].value - triple[2].value == 1 -> Plane( 35 | triple.subList(1, 4).toCardSet(), 36 | triple.subList(0, 1).toCardSet() 37 | ) 38 | else -> null 39 | } 40 | } else null 41 | } else { //有可能是多连飞,不带单 42 | for (i in (0 until triple.size - 1)) { 43 | if (triple[i + 1].value - triple[i].value != 1) return null 44 | } 45 | return Plane(triple, CardSet()) 46 | } 47 | } else return null 48 | } 49 | for (i in (0 until triple.size - 1)) { //排除上述情况后,判断三连是否连续 50 | if (triple[i + 1].value - triple[i].value != 1) return null 51 | } 52 | return Plane(triple, this) 53 | } else return null 54 | } 55 | */ 56 | 57 | //识别四带二的旧过程 58 | /* 59 | val set = toSet() 60 | return if (size == 6) { 61 | var qua = Card.NOT_A_CARD 62 | for (card in set) { 63 | var times = 0 64 | for (i in this) { 65 | if (i == card) times += 1 66 | } 67 | if (times == 4) { 68 | qua = card 69 | break 70 | } 71 | } 72 | return if (qua != Card.NOT_A_CARD) { 73 | this.remove(qua) 74 | QuadrupleWithTwoSingles(qua, this) 75 | } else null 76 | } else null 77 | */ -------------------------------------------------------------------------------- /src/main/resources/META-INF/services/net.mamoe.mirai.console.plugin.jvm.JvmPlugin: -------------------------------------------------------------------------------- 1 | dada.douDiZhu.DouDiZhu --------------------------------------------------------------------------------