UNO游戏设计(II):实现基本的数字牌功能
UNO游戏设计(II):实现基本的数字牌功能
流程回顾
对于只含有数字牌的对局,实现起来相对简单,但是却可以借此把框架准备完善。
首先回顾UNO流程:
- 创建房间(指定房间号)
- 加入房间
- 锁定房间并开始游戏
- 随机指定先手
- 按照牌局规则出牌或摸牌
- 当一方牌摸完之后则胜利
在此之前,我们先做一些基础工作。
如果有未导入的库函数,请参考以下内容在头部导入:
from nonebot import on_command, get_driver, on_message
from nonebot.adapters.onebot.v11 import Bot, Event, Message, MessageSegment, GroupMessageEvent, PrivateMessageEvent
from nonebot.params import CommandArg
from nonebot.typing import T_State
import os
import random as r
import time
import json
准备牌局
首先定义一些基本量:
# 假设UNO游戏最大玩家数量 |
用一个大字典来装载每个牌局的状态。这个状态应该包括:
- 玩家列表及玩家手牌
- 牌堆
- 弃牌堆
- 当前弃牌堆牌顶的状态(决定接收下家什么类型的牌)
- 当前回合位置
- 出牌顺序(只有两个,正或反)
- ……
暂且只考虑这些状态。把这一切都定义在UNO类中:
class UNO: |
将上述变量名列表展示:
变量名 | 含义 | 类型 |
---|---|---|
players |
存储玩家昵称的列表 | list[str] |
playerids |
存储玩家QQ号的列表 | list[int] |
hands |
记录玩家手牌的字典,键为索引,值为手牌 | dict[int, list[str]] |
deck |
剩余牌堆的卡牌列表 | list[str] |
discard_pile |
弃牌堆的卡牌列表 | list[str] |
current_player_index |
当前出牌玩家的索引 | int |
direction |
出牌方向(1
表示顺时针,-1 表示逆时针) |
int |
game_started |
游戏是否已开始的标志 | bool |
now_stat |
当前弃牌堆牌顶的状态,存储为包含两个字符串的列表 | list[str, str] |
准备牌堆
需要初始化牌堆。根据之前的介绍,数字牌由红、黄、蓝、绿四种颜色组成,每种颜色有19张,包括数字0~9;其中数字1~9各有2张,数字0有1张。
# UNO class内 |
实例化:
uno = UNO() |
查看一下牌堆结果:
['红0', '红1', '红2', '红3', '红4', '红5', '红6', '红7', '红8', '红9', '红1', '红2', '红3', '红4', '红5', '红6', '红7', '红8', '红9', '黄0', '黄1', '黄2', '黄3', '黄4', '黄5', '黄6', '黄7', '黄8', '黄9', '黄1', '黄2', '黄3', '黄4', '黄5', '黄6', '黄7', '黄8', '黄9', '绿0', '绿1', '绿2', '绿3', '绿4', '绿5', '绿6', '绿7', '绿8', '绿9', '绿1', '绿2', '绿3', '绿4', '绿5', '绿6', '绿7', '绿8', '绿9', '蓝0', '蓝1', '蓝2', '蓝3', '蓝4', '蓝5', '蓝6', '蓝7', '蓝8', '蓝9', '蓝1', '蓝2', '蓝3', '蓝4', '蓝5', '蓝6', '蓝7', '蓝8', '蓝9'] |
分别是未洗牌和已洗牌的牌堆。
准备玩家
玩家需要在QQ群里报名。可以以群号为单位。
- 需要有人率先创建房间。
- 群聊的参与人可报名。报名时在群里公布。
- 必须是该群成员。
- 人数到齐之后,可开始游戏。
- 人数不能超过最大人数,也不能低于2。
每个玩家对应一个索引。这一个索引对应手牌、玩家id、玩家名。玩家id用于发私聊时查找,玩家名用于称呼。
create_game = on_command("创建uno房间") |
如此操作,即准备好人员。
为了方便游戏进度的存取,需要一个json
文件,并且封装两个函数分别用于保存和读取。保存在同文件夹的uno_data.json
中。
base=os.path.dirname(__file__) |
每次有新人加入时都存档一下。

当两人加入房间之后,查看存档:
{ |
这样便可实时查看状态,以便调试。
开始牌局
开始牌局,需要有一人在群聊中输入“一起uno”。
牌局开始时,先要查看:
- 是否有创建游戏
- 游戏是否已开始
- 玩家数量足够
|
如果不满足,需要在群里阻止游戏开始。如果满足,则:
将状态改为开始游戏
洗牌
发牌
指定先手
保存游戏进度(此后将不再强调这一步)
在类中定义发牌、指定先手:
class UNO: |
在实例中完成步骤:
game.game_started = True |
检验一下上述代码的运行情况:

{ |
可以看到,现在牌局已经建立,每人发到了7张牌。
注意:因为存档时是针对
UNO
类进行存档,因此保存的内容为games
下的一个game
(一次只能保存正在进行的那个游戏)。可以考虑将存取函数定义在类外,以保存所有游戏。
但是,玩家还没有拿到牌。玩家拿的牌应该在私聊里发出,且每次玩家的牌发生变化的时候都应在私聊中给出。首先在起始时应该群发一次:
for i, hands in game.hands.items(): |
这样做可以每5个为一行,发到玩家的私聊中。
或者可以定义一个异步函数方便后续的使用(因为展示手牌的过程比较频繁);手牌再分发之前也可以先排个序。在定义函数时,可以设置既能群发又能发给指定玩家。
手牌的顺序如果按照字符串本身的顺序,根据字符串的排序原理是可以实现按照颜色的分类的。此处也可自己定义顺序。
async def send_hand_cards(bot: Bot, game: UNO, n: int = 5, user_id: int = None): |
进行牌局
现在已经准备好牌局,接下来可以开始牌局了。开始牌局前需要先指定一套逻辑,即某玩家是否可以出某张牌。这一套逻辑根据规则的复杂程度必然会越来越复杂。在只有数字牌的时候,规则很简单:
出牌条件
- 该玩家在游戏中
- 现在轮到该玩家出牌
- 玩家手里有这张牌
- 当前状态中颜色、数字至少有一个匹配(或者为空)
让我们在UNO
类中建立该逻辑。如果不能出牌,需要返回一个理由。而理由总是真,因此返回False
为可以出牌,返回理由为不能出牌。
def cannot_play_card(self, card: str, playerid: int) -> bool: |
其次,我们还需制定一套将出了的牌解析为当前状态的逻辑:
解析状态逻辑
当然,在仅有数字牌的情况下,只需要分开颜色和数字即可。
class UNO: |
出牌
让我们从第一名玩家开始。
第一名玩家开始时,由于出的牌满足not self.discard_pile
,因此只要在手中的牌都可以出。出牌可以在私聊进行,然后机器人会提醒玩家结果。当然,出牌应该有口令才对。为了简化,采用“出”作为出牌的口令。
- 确定在游戏中
- 确定符合出牌条件
- 出牌
- 打出即可
- 如果出完,则宣布胜利
- 切换到下家
- 在群聊中宣布
- 给出牌的玩家私发手牌
play_card = on_command("出") # 出牌 |
摸牌
如果发现自己无牌可出或者不想出牌,可以摸一张牌,并且移动到下家。摸牌的口令是“摸”。摸牌时,在群聊中公布谁摸了一张,但不会在群里透露牌面。
- 确定符合摸牌条件(正在自己的回合中)
- 摸牌(并私聊告知)
- 切换到下家
- 在群聊中宣布
- 给出牌的玩家私发手牌
如果牌堆已空,应该将弃牌堆洗好牌放入牌堆(除了弃牌堆顶的一张牌)。但是为了简化过程,先不考虑这个,仅仅是抛出提示“牌堆已空”。
draw_card = on_command("摸") # 摸牌 |
测试
让我们实际操作一局来检验。
撄宁: 02-02 23:19:39 |
最终胜利时,保存的状态如下:
{ |
程序优化
- 在群里公布时,展示出手牌的数量。
- 轮到某玩家时私发提醒,并提醒该玩家需要应对哪张牌或什么状态。
- 如果玩家需要帮助,可以即刻获得命令的帮助以及玩法的帮助。
- 可以随时查看弃牌堆和牌堆的张数。
完整代码
from nonebot import on_command, get_driver, on_message |