UNO游戏设计(III):优化/功能牌实现

流程优化

本次解决两个优化:

  • 在群里公布时,展示出手牌的数量。
  • 轮到某玩家时私发提醒,并提醒该玩家需要应对哪张牌或什么状态。

展示手牌数量

在玩家名称后面以数字提醒即可:

python
len(game.hands[game.playerids.index(user_id)]) #上家
len(game.hands[game.current_player_index]) #下家

对于上家,因为只有user_id已知(发送信息时其他变量已经更新到下家了),所以需要先检索到玩家的索引,再找到手牌的数量;
对于下家,current_player_index即为所求。

此时手牌的数量为已经出过牌的数量!

选择合适的时机保存

  • 在宣布胜利之前保存,否则牌局将保存不到最后一步。

轮到某玩家时私发提醒

由于上家出的牌中可能有黑色万能牌,由上家指定颜色。所以上家指定的颜色会将状态更新,因此不如将当前的状态发给该玩家。

python
await bot.send_private_msg(user_id=game.playerids[game.current_player_index], message=f"轮到你出牌了!当前牌顶是 {game.now_stat[0]}{game.now_stat[1]}")

测试优化

再玩一局进行测试:

私聊界面
群聊界面

功能牌实现

功能牌有如下几种:

  • 功能牌:同样有红、黄、蓝、绿四种颜色,包含“跳过”、“反转”、“+2”三种功能,每种功能牌各有8张。
    • 跳过牌:下家无法出牌,轮至下下家出牌。
    • 反转牌:游戏顺序反转,由下家(原上家)出牌。
    • +2牌:下家摸2张牌,不能出牌,轮至下下家出牌。

首先需要在牌堆里更新这些牌。

python
for color in colors:
self.deck += [f"{color}跳过", f"{color}反转", f"{color}+2"]*2

洗牌前结果如下:

plaintext
['红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', '红跳过', '红反转', '红+2', '红跳过', '红反转', '红+2', '黄跳过', '黄反转', '黄+2', '黄跳过', '黄反转', '黄+2', '绿跳过', '绿反转', '绿+2', '绿跳过', '绿反转', '绿+2', '蓝跳过', '蓝反转', '蓝+2', '蓝跳过', '蓝反转', '蓝+2']

每张牌各2张,每种功能各8张。

每添加一张牌,需要增改以下内容:

  • 出牌条件
  • 当前状态逻辑(出一张牌会导致当前状态如何更新,一般只要把颜色和功能分离即可)
  • 牌本身的功能

跳过牌

跳过牌会跳过下家。

为了方便检索,令old_index为被跳过的下家,new_index为该出牌的下下家。当然,user_id为本轮出跳过牌的玩家。

python
# 跳过牌
if card[1:] == "跳过":
old_index = game.current_player_index
game.current_player_index = (game.current_player_index + game.direction) % len(game.playerids)
new_index = game.current_player_index
await bot.send_group_msg(group_id=group_id, message=f"{nickname}({len(game.hands[game.playerids.index(user_id)])}) 出了 {card}\n{game.players[old_index]}({len(game.hands[old_index])}) 被跳过了,轮到 {game.players[new_index]}({len(game.hands[new_index])}) 出牌")
# 发送出牌信息
else:
await bot.send_group_msg(group_id=group_id, message=f"{nickname}({len(game.hands[game.playerids.index(user_id)])}) 出了 {card}\n轮到 {game.players[game.current_player_index]}({len(game.hands[game.current_player_index])}) 出牌")
await bot.send_private_msg(user_id=game.playerids[game.current_player_index], message=f"轮到你出牌了!当前牌顶是{game.now_stat[0]}{game.now_stat[1]}") # 提醒出牌
await send_hand_cards(bot, game, user_id=user_id) # 发送手牌信息

当然,出牌和更新状态也需设置:

python
# UNO
def cannot_play_card(self, card: str, playerid: int) -> bool:
# 检查是否不可以出牌
if playerid not in self.playerids:
return "你不在游戏中!"
if self.current_player_index != self.playerids.index(playerid):
return "还没轮到你出牌!"
if card not in self.hands[self.current_player_index]:
return "你没有这张牌!"
if not self.discard_pile: # 如果弃牌堆为空 说明是先手 此时可以出任意牌
return False
if card[0] in ("红", "黄", "绿", "蓝") and card[1].isdigit(): # 如果是数字牌
if card[0] == self.now_stat[0] or card[1] == self.now_stat[1]: # 如果颜色或数字匹配
return False
else:
return "颜色或数字不匹配!"
elif card[0] in ("红", "黄", "绿", "蓝") and card[1:] in ("跳过", "反转", "+2"): # 如果是功能牌
if card[0] == self.now_stat[0] or card[1:] == self.now_stat[1]: # 如果颜色或功能匹配
return False
else:
return "颜色或功能不匹配!"
else:
return "不是数字牌或功能牌!"

def update_now_stat(self, card: str):
# 更新当前弃牌堆牌顶的状态
if card[0] in ("红", "黄", "绿", "蓝") and card[1].isdigit(): # 如果是数字牌
self.now_stat = [card[0], card[1]]
elif card[0] in ("红", "黄", "绿", "蓝") and card[1:] in ("跳过", "反转", "+2"): # 如果是功能牌
self.now_stat = [card[0], card[1:]]
else:
self.now_stat = []

此处一并将功能牌全部设置完成。

反转牌

反转牌会反转当前顺序。因为之前已经设置了出牌方向self.direction: int = 1,且定义了顺时针是1,因此只需取反即可,游戏继续;

需要注意的是,在前面已经切换了下家,因此反转牌需要无视切换。user_id是出牌的玩家,基于此来寻找下一索引可规避之前对下家的定义。

python
elif card[1:] == "反转":
game.direction *= -1
# 无视下家切换
game.current_player_index = (game.playerids.index(user_id) + game.direction) % len(game.playerids)
await bot.send_group_msg(group_id=group_id, message=f"{nickname}({len(game.hands[game.playerids.index(user_id)])}) 出了 {card}\n方向已反转,轮到 {game.players[game.current_player_index]}({len(game.hands[game.current_player_index])}) 出牌")

+2牌

此牌较为棘手,不仅要下家摸两张牌,而且如果下家有+2,可以不摸牌而继续出+2,将牌叠加。为此,当下家接收到+2时,可以选择:

  • 私聊输入“摸”,即决定摸相应数量的牌;
  • 私聊输入“出”,需要出一张+2牌。

这一切的前提是该玩家需要面临被+2的情况。因此需要先设置一个标签self.plus_2,这个标签的第一位是谁被出了+2,第二位是+2累积到了几。首先,一般情况下,应该是[-1, 0]。即如果是-1,不需要面临应对+2牌的问题。

出牌处

在出牌处,需要首先将plus_2设置为下家,并且先叠加两张。在群里公布:下家需要摸几张牌,或者继续叠加。接下来在私聊中告诉下家选择一项。

py
# +2 牌
elif card[1:] == "+2":
game.plus_2 = [game.current_player_index, game.plus_2[1]+2]
# 群聊公布
await bot.send_group_msg(group_id=group_id, message=f"{nickname}({len(game.hands[game.playerids.index(user_id)])}) 出了 {card}\n{game.players[game.current_player_index]}({len(game.hands[game.current_player_index])}) 被罚摸 {game.plus_2[1]} 张牌或继续叠加")
# 私聊选择
await bot.send_private_msg(user_id=game.playerids[game.current_player_index], message=f"轮到你出牌了!当前牌顶是{game.now_stat[0]}{game.now_stat[1]}\n你可以选择:\n- 输入“摸”,即决定摸 {game.plus_2[1]} 张牌;\n- 输入“出”命令以出牌继续叠加,需要出一张+2牌。")
await send_hand_cards(bot, game, user_id=user_id) # 发送手牌信息
return

在这里需要return,因为不需要后面普通的私聊提醒流程了。

出牌条件处

出牌条件的位置,需要设置一个强优先的判断:如果该玩家是被+2的玩家(不是只需要接+2牌的牌面即可,而是处于被罚牌的状态),那么是不能走常规的出牌流程的。

python
# 这里加入特殊牌发生效果
if self.playerids.index(playerid) == self.plus_2[0]: # 如果是+2牌
if card[1:] == "+2":
return False
else:
return "你必须出+2牌继续叠加,否则摸牌!"

注意:这里只需要提供一层过滤即可,通过条件判断之后自会在出牌处更改plus_2的状态。

更新状态处

更新状态的位置不需要设置,因为牌堆顶就是正常的+2牌。

摸牌处

摸牌处也是强优先判断,摸完牌之后重置plus_2的值。

python
# 特殊摸牌流程
if game.plus_2[0] == game.current_player_index and game.plus_2[1] > 0: # +2 牌的摸牌流程
# 牌堆牌不够
if len(game.deck) < game.plus_2[1]:
await bot.send_group_msg(group_id=group_id, message=f"牌堆牌数不够(剩{len(game.deck)}张),无法摸牌!")
return
new_cards = [game.deck.pop() for _ in range(game.plus_2[1])]
game.hands[game.current_player_index] += new_cards
game.plus_2 = [-1, 0] # 重置 +2 牌
game.current_player_index = (game.current_player_index + game.direction) % len(game.playerids)
game.save()
await bot.send_group_msg(group_id=group_id, message=f"{nickname}({len(game.hands[game.playerids.index(user_id)])}) 因 +2牌 摸了 {len(new_cards)} 张牌\n轮到 {game.players[game.current_player_index]}({len(game.hands[game.current_player_index])}) 出牌") # 群聊公布
await bot.send_private_msg(user_id=user_id, message=f"你摸了 {len(new_cards)} 张牌:{'、'.join(new_cards)}") # 私聊列举摸到的牌

到这里,三大功能牌以及+2的叠加功能就做完了。但是随着抽牌越来越频繁,牌堆不够的问题越来越显著。所以当因为牌堆牌不够导致无法摸牌时,需要一位成员在群里喊“洗牌”。

洗牌功能

洗牌就是将弃牌堆里除了顶上的一张牌之外的其他牌都加入到牌堆中,再shuffle()一下。

python
def reshuffle_deck(self): # 洗牌
if len(self.discard_pile) > 2:
top_card = self.discard_pile.pop()
self.deck = self.discard_pile + self.deck
self.discard_pile = [top_card]
r.shuffle(self.deck)

并且在群聊命令中加入洗牌命令。当然,只有弃牌堆牌不太少时才能洗牌(3张)。

python
reshuffle_deck = on_command("洗牌") # 洗牌

@reshuffle_deck.handle()
async def reshuffle_deck_handle(bot: Bot, event: GroupMessageEvent):
group_id = event.group_id
game = games.get(group_id)
if not game or not game.game_started:
return
if len(game.discard_pile) <= 3:
await bot.send(event, f"牌堆还满!弃牌堆牌数不够({len(game.discard_pile)}张),无法洗牌!")
return
game.reshuffle_deck()
game.save()
await bot.send(event, f"牌堆已洗牌!现在牌堆有 {len(game.deck)} 张牌,弃牌堆有 {len(game.discard_pile)} 张牌。")

测试功能

接下来测试功能是否完善。

游戏流程为:

  1. 创建游戏:群聊使用创建uno房间命令创建一个新的游戏房间。
  2. 加入游戏:玩家群聊使用加入uno房间命令加入游戏。
  3. 开始游戏:当有至少两名玩家加入后,群聊使用一起uno命令开始游戏。
  4. 出牌:玩家轮流私聊使用出 [牌名]命令出牌。
  5. 摸牌:私聊使用命令摸牌。
  6. 洗牌:当牌堆牌数不够时,群聊使用洗牌命令重新洗牌。
  7. 结束:当主动放弃时,群聊使用结束uno命令结束游戏。
+2牌的累积和摸牌
+2牌的私聊展示
洗牌展示
跳过与反转展示

继续优化

优化方向有:

  • 加入7-0规则
  • 加入抢牌规则
  • 加入强制出牌规则
  • 在创建房间时设置房间参数
  • 游戏中途如果放弃,可以切断游戏
  • 游戏结束后可以计算分值
  • 如果询问状态,可以展示当前的牌局状态,包括:
    • 当前回合的玩家
    • 牌局顺序以及座次
    • 当前牌堆数和弃牌堆数
    • 当前弃牌堆顶牌
    • 各玩家的手牌数量
  • 如果询问帮助,可以查看以下帮助
    • UNO的游戏规则
    • 命令指南
    • 单个牌的规则