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游戏最大玩家数量
MAX_PLAYERS = 8
# 假设每个玩家初始手牌数量
INITIAL_HAND_SIZE = 7

用一个大字典来装载每个牌局的状态。这个状态应该包括:

  • 玩家列表及玩家手牌
  • 牌堆
  • 弃牌堆
  • 当前弃牌堆牌顶的状态(决定接收下家什么类型的牌)
  • 当前回合位置
  • 出牌顺序(只有两个,正或反)
  • ……

暂且只考虑这些状态。把这一切都定义在UNO类中:

class UNO:
def __init__(self):
self.players: list[str] = [] # 玩家昵称列表
self.player_ids: list[int] = [] # 玩家QQ号列表
self.hands: dict[int, list[str]] = {} # 玩家手牌
self.deck: list[str] = [] # 剩余牌堆
self.discard_pile: list[str] = [] # 弃牌堆
self.current_player_index: int = 0 # 当前玩家索引
self.direction: int = 1 # 出牌方向(顺时针或逆时针)
self.game_started: bool = False # 游戏是否已经开始
self.now_stat: list[str, str] = [] #当前弃牌堆牌顶的状态

将上述变量名列表展示:

变量名 含义 类型
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内
def initialize_deck(self):
# 初始化牌堆
colors = ['红', '黄', '绿', '蓝']
number_cards = list(range(0, 10)) + list(range(1, 10)) # 每个数字两张,红0到红9、红1到红9
self.deck = []
for color in colors:
self.deck += [f"{color}{num}" for num in number_cards]
print(self.deck)
# 洗牌
r.shuffle(self.deck)

实例化:

uno = UNO()
uno.initialize_deck()
print(uno.deck)

查看一下牌堆结果:

['红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']

['绿6', '红3', '蓝4', '黄7', '红9', '红6', '绿1', '蓝5', '蓝6', '绿7', '蓝1', '黄9', '黄8', '蓝7', '黄4', '黄4', '红2', '红0', '蓝2', '绿9', '蓝3', '绿5', '黄0', '绿3', '黄2', '黄6', '黄3', '黄2', '蓝1', '蓝8', '红9', '蓝4', '红1', '蓝3', '绿2', '红6', '红1', '黄6', '蓝9', '红7', '绿1', '蓝0', '红8', '绿4', '红8', '红3', '绿4', '黄5', '绿8', '黄3', '黄1', '绿7', '绿3', '绿6', '蓝7', '黄7', '绿0', '黄9', '红2', '蓝8', '蓝2', '红7', '蓝6', '红5', '绿5', '绿9', '蓝9', '黄1', '绿2', '绿8', '黄5', '黄8', '红5', '红4', '红4', '蓝5']

分别是未洗牌和已洗牌的牌堆。

准备玩家

玩家需要在QQ群里报名。可以以群号为单位。

  • 需要有人率先创建房间。
  • 群聊的参与人可报名。报名时在群里公布。
    • 必须是该群成员。
  • 人数到齐之后,可开始游戏。
    • 人数不能超过最大人数,也不能低于2。

每个玩家对应一个索引。这一个索引对应手牌、玩家id、玩家名。玩家id用于发私聊时查找,玩家名用于称呼。

create_game = on_command("创建uno房间")
join_game = on_command("加入uno房间")
start_game = on_command("一起uno")

@create_game.handle()
async def create_game_handle(bot: Bot, event: GroupMessageEvent):
group_id = event.group_id
if group_id not in games:
games[group_id] = UNO()
await bot.send(event, "UNO游戏已创建!请玩家使用 '加入uno房间' 命令加入游戏,使用 '一起uno' 命令开始游戏。")
else:
await bot.send(event, "该游戏已创建,请等待开始。")

@join_game.handle()
async def join_game_handle(bot: Bot, event: GroupMessageEvent):
group_id = event.group_id
user_id = event.user_id
nickname = event.sender.nickname
game = games.get(group_id)
# 处理非法情况
if not game or game.game_started:
await bot.send(event, "本群没有待加入的游戏或游戏已开始!")
return
if len(game.players) >= MAX_PLAYERS:
await bot.send(event, "玩家已满!")
return
# 正常加入游戏
if user_id in game.playerids:
await bot.send(event, f"{nickname} 已成功加入游戏 {group_id}!")
return
game.players.append(nickname)
game.playerids.append(user_id)
await bot.send(event, f"{nickname} 已加入游戏!当前玩家数量:{len(game.players)}")

如此操作,即准备好人员。

为了方便游戏进度的存取,需要一个json文件,并且封装两个函数分别用于保存和读取。保存在同文件夹的uno_data.json中。

base=os.path.dirname(__file__)
json_path=os.path.join(base,"uno_data.json")

class UNO:
def save(self):
with open(json_path,"w",encoding='utf-8') as f:
json.dump(self.__dict__,f,ensure_ascii=False,indent=4)

def load(self):
with open(json_path,"r",encoding='utf-8') as f:
self.__dict__=json.load(f)

game.save() #join_game_handle内

每次有新人加入时都存档一下。

加入房间

当两人加入房间之后,查看存档:

{
"players": [
"撄宁",
"云间"
],
"playerids": [
xxx(int),
xxx(int)
],
"hands": {},
"deck": [],
"discard_pile": [],
"current_player_index": 0,
"direction": 1,
"game_started": false
"now_stat": []
}

这样便可实时查看状态,以便调试。

开始牌局

开始牌局,需要有一人在群聊中输入“一起uno”。

牌局开始时,先要查看:

  • 是否有创建游戏
  • 游戏是否已开始
  • 玩家数量足够
@start_game.handle()
async def start_game_handle(bot: Bot, event: GroupMessageEvent):
group_id = event.group_id
game = games.get(group_id)
if not game or game.game_started:
await bot.send(event, "本群没有待开始的游戏或游戏已开始!")
return
if len(game.players) < 2:
await bot.send(event, "玩家数量不足!")
return

如果不满足,需要在群里阻止游戏开始。如果满足,则:

  • 将状态改为开始游戏

  • 洗牌

  • 发牌

  • 指定先手

  • 保存游戏进度(此后将不再强调这一步)

在类中定义发牌、指定先手:

class UNO:
def initialize_card(self):
# 初始发牌
for i in range(len(self.playerids)):
self.hands[i] = [self.deck.pop() for _ in range(INITIAL_HAND_SIZE)]
# 随机选择一名玩家作为先手
if self.playerids:
self.current_player_index = r.randint(0, len(self.playerids)-1)

在实例中完成步骤:

game.game_started = True
game.initialize_deck()
game.initialize_card()
game.save()
await bot.send(event, f"UNO游戏开始!游戏号:{group_id}\n玩家{len(game.playerids)}人:{'、'.join(game.players)}")
await bot.send(event, f"先手随机指定为:{game.players[game.current_player_index]}")

检验一下上述代码的运行情况:

一起UNO
{
"players": [
"云间",
"撄宁"
],
"playerids": [
xxx,
xxx
],
"hands": {
"0": [
"黄5",
"绿5",
"绿9",
"黄8",
"黄6",
"红1",
"黄9"
],
"1": [
"绿8",
"红6",
"绿3",
"蓝1",
"蓝3",
"黄3",
"黄1"
]
},
"deck": [
"红5",
"绿0",
"红1",
"红9",
"绿3",
"绿1",
"蓝2",
"绿6",
"绿6",
"红0",
"蓝4",
"绿8",
"红8",
"红9",
"红2",
"黄8",
"蓝4",
"红7",
"绿7",
"红3",
"黄4",
"绿7",
"红4",
"黄5",
"黄7",
"红2",
"蓝8",
"黄2",
"蓝2",
"红7",
"蓝1",
"红5",
"绿1",
"黄0",
"蓝9",
"黄6",
"绿4",
"绿4",
"黄4",
"黄1",
"红4",
"红3",
"黄7",
"蓝7",
"黄3",
"蓝5",
"红8",
"黄9",
"蓝5",
"绿9",
"绿5",
"蓝8",
"绿2",
"蓝9",
"蓝0",
"红6",
"黄2",
"蓝6",
"蓝3",
"绿2",
"蓝7",
"蓝6"
],
"discard_pile": [],
"current_player_index": 0,
"direction": 1,
"game_started": true,
"now_stat": []
}

可以看到,现在牌局已经建立,每人发到了7张牌。

注意:因为存档时是针对UNO类进行存档,因此保存的内容为games下的一个game(一次只能保存正在进行的那个游戏)。可以考虑将存取函数定义在类外,以保存所有游戏。

但是,玩家还没有拿到牌。玩家拿的牌应该在私聊里发出,且每次玩家的牌发生变化的时候都应在私聊中给出。首先在起始时应该群发一次:

for i, hands in game.hands.items():
hand_message = '\n'.join([' '.join(hands[j:j+4]) for j in range(0, len(hands), 5)])
await bot.send_private_msg(user_id=game.playerids[i], message=f"你的手牌:\n{hand_message}")

这样做可以每5个为一行,发到玩家的私聊中。

或者可以定义一个异步函数方便后续的使用(因为展示手牌的过程比较频繁);手牌再分发之前也可以先排个序。在定义函数时,可以设置既能群发又能发给指定玩家。

手牌的顺序如果按照字符串本身的顺序,根据字符串的排序原理是可以实现按照颜色的分类的。此处也可自己定义顺序。

async def send_hand_cards(bot: Bot, game: UNO, n: int = 5, user_id: int = None):
if user_id:
hands = game.hands[game.playerids.index(user_id)]
hands.sort()
hand_message = '\n'.join([' '.join(hands[j:j+4]) for j in range(0, len(hands), n)])
await bot.send_private_msg(user_id=user_id, message=f"你的手牌 ({len(hands)}张)\n{hand_message}")
return
else:
for i, hands in game.hands.items():
hands.sort()
hand_message = '\n'.join([' '.join(hands[j:j+4]) for j in range(0, len(hands), n)])
await bot.send_private_msg(user_id=game.playerids[i], message=f"你的手牌 ({len(hands)}张)\n{hand_message}")
return

进行牌局

现在已经准备好牌局,接下来可以开始牌局了。开始牌局前需要先指定一套逻辑,即某玩家是否可以出某张牌。这一套逻辑根据规则的复杂程度必然会越来越复杂。在只有数字牌的时候,规则很简单:

出牌条件

  • 该玩家在游戏中
  • 现在轮到该玩家出牌
  • 玩家手里有这张牌
  • 当前状态中颜色、数字至少有一个匹配(或者为空)

让我们在UNO类中建立该逻辑。如果不能出牌,需要返回一个理由。而理由总是真,因此返回False为可以出牌,返回理由为不能出牌。

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].digit(): # 如果是数字牌
if card[0] == self.now_stat[0] or card[1] == self.now_stat[1]: # 如果颜色或数字匹配
return False
else:
return "颜色或数字不匹配!"
else:
return "不是数字牌!"

其次,我们还需制定一套将出了的牌解析为当前状态的逻辑:

解析状态逻辑

当然,在仅有数字牌的情况下,只需要分开颜色和数字即可。

class UNO:
def update_now_stat(self, card: str):
# 更新当前弃牌堆牌顶的状态
if card[0] in ("红", "黄", "绿", "蓝") and card[1].isdigit(): # 如果是数字牌
self.now_stat = [card[0], card[1]]

出牌

让我们从第一名玩家开始。

第一名玩家开始时,由于出的牌满足not self.discard_pile,因此只要在手中的牌都可以出。出牌可以在私聊进行,然后机器人会提醒玩家结果。当然,出牌应该有口令才对。为了简化,采用“出”作为出牌的口令。

  • 确定在游戏中
  • 确定符合出牌条件
  • 出牌
    • 打出即可
    • 如果出完,则宣布胜利
  • 切换到下家
  • 在群聊中宣布
  • 给出牌的玩家私发手牌
play_card = on_command("出") # 出牌

@play_card.handle()
async def play_card_handle(bot: Bot, event: PrivateMessageEvent, message: Message = CommandArg()):
user_id = event.user_id
card = str(message).strip()
if not card:
await bot.send(event, "未输入要出的牌!")
return
# 查找玩家所在的游戏
for group_id_, game in games.items():
if not (game.game_started and (user_id in game.playerids)):
await bot.send(event, "你不在任何一个已经开始的游戏中!")
else:
group_id = group_id_
break

game = games.get(group_id)
nickname = game.players[game.playerids.index(user_id)]
# now_stat = game.now_stat

# 检查是否可以出牌
cannot_play_reason = game.cannot_play_card(card, user_id)
if cannot_play_reason:
await bot.send(event, cannot_play_reason)
return

# 出牌
game.hands[game.current_player_index].remove(card)
game.discard_pile.append(card)
game.update_now_stat(card) # 更新当前弃牌堆牌顶的状态


# 检查是否胜利
if not game.hands[game.current_player_index]:
await bot.send_group_msg(group_id=group_id, message=f"{game.players[game.current_player_index]} 胜利!游戏结束!")
del games[group_id]
return

# 切换到下家
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} 出了 {card}\n轮到 {game.players[game.current_player_index]} 出牌")
await send_hand_cards(bot, game, user_id=user_id) # 发送手牌信息

摸牌

如果发现自己无牌可出或者不想出牌,可以摸一张牌,并且移动到下家。摸牌的口令是“摸”。摸牌时,在群聊中公布谁摸了一张,但不会在群里透露牌面。

  • 确定符合摸牌条件(正在自己的回合中)
  • 摸牌(并私聊告知)
  • 切换到下家
  • 在群聊中宣布
  • 给出牌的玩家私发手牌

如果牌堆已空,应该将弃牌堆洗好牌放入牌堆(除了弃牌堆顶的一张牌)。但是为了简化过程,先不考虑这个,仅仅是抛出提示“牌堆已空”。

draw_card = on_command("摸")  # 摸牌

@draw_card.handle()
async def draw_card_handle(bot: Bot, event: PrivateMessageEvent, message: Message = CommandArg()):
user_id = event.user_id
# 查找玩家所在的游戏
for group_id_, game in games.items():
if not (game.game_started and (user_id in game.playerids)):
await bot.send(event, "你不在任何一个已经开始的游戏中!")
else:
group_id = group_id_
break

game = games.get(group_id)
nickname = game.players[game.playerids.index(user_id)]

if game.current_player_index != game.playerids.index(user_id):
await bot.send(event, "还没轮到你摸牌!")
return

# 摸牌
if not game.deck:
await bot.send(event, "牌堆已空,无法摸牌!")
return

new_card = game.deck.pop()
game.hands[game.current_player_index].append(new_card)

# 切换到下家
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} 摸了一张牌\n轮到 {game.players[game.current_player_index]} 出牌")
await bot.send_private_msg(user_id=user_id, message=f"你摸了一张牌:{new_card}")
await send_hand_cards(bot, game, user_id=user_id) # 发送手牌信息

测试

让我们实际操作一局来检验。

撄宁: 02-02 23:19:39
创建uno房间

BNWJ_bot: 02-02 23:19:39
UNO游戏已创建!请玩家使用 '加入uno房间' 命令加入游戏,使用 '一起uno' 命令开始游戏。

撄宁: 02-02 23:19:43
加入uno房间

BNWJ_bot: 02-02 23:19:43
撄宁 已加入游戏!当前玩家数量:1

撄宁: 02-02 23:19:47
一起uno

BNWJ_bot: 02-02 23:19:47
玩家数量不足!

云间: 02-02 23:20:07
加入uno房间

BNWJ_bot: 02-02 23:20:08
云间 已加入游戏!当前玩家数量:2

云间: 02-02 23:20:12
一起uno

BNWJ_bot: 02-02 23:20:13
UNO游戏开始!游戏号:460739046
玩家2人:撄宁、云间

BNWJ_bot: 02-02 23:20:13
先手随机指定为:云间;手牌已发送至私聊中。

BNWJ_bot: 02-02 23:20:23
云间 出了 红2
轮到 撄宁 出牌

BNWJ_bot: 02-02 23:20:43
撄宁 摸了一张牌
轮到 云间 出牌

BNWJ_bot: 02-02 23:21:47
云间 出了 红5
轮到 撄宁 出牌

BNWJ_bot: 02-02 23:22:05
撄宁 出了 红8
轮到 云间 出牌

BNWJ_bot: 02-02 23:22:13
云间 出了 红4
轮到 撄宁 出牌

BNWJ_bot: 02-02 23:22:20
撄宁 出了 红1
轮到 云间 出牌

BNWJ_bot: 02-02 23:22:25
云间 出了 蓝1
轮到 撄宁 出牌

BNWJ_bot: 02-02 23:22:33
撄宁 出了 蓝5
轮到 云间 出牌

BNWJ_bot: 02-02 23:23:33
云间 出了 蓝3
轮到 撄宁 出牌

BNWJ_bot: 02-02 23:23:45
撄宁 出了 蓝6
轮到 云间 出牌

BNWJ_bot: 02-02 23:23:51
云间 摸了一张牌
轮到 撄宁 出牌

BNWJ_bot: 02-02 23:24:07
撄宁 出了 蓝2
轮到 云间 出牌

BNWJ_bot: 02-02 23:24:14
云间 摸了一张牌
轮到 撄宁 出牌

BNWJ_bot: 02-02 23:24:26
撄宁 出了 蓝7
轮到 云间 出牌

BNWJ_bot: 02-02 23:24:32
云间 出了 蓝4
轮到 撄宁 出牌

BNWJ_bot: 02-02 23:24:37
撄宁 摸了一张牌
轮到 云间 出牌

BNWJ_bot: 02-02 23:24:44
云间 出了 绿4
轮到 撄宁 出牌

BNWJ_bot: 02-02 23:24:50
撄宁 摸了一张牌
轮到 云间 出牌

BNWJ_bot: 02-02 23:24:55
云间 出了 绿9
轮到 撄宁 出牌

BNWJ_bot: 02-02 23:25:05
撄宁 摸了一张牌
轮到 云间 出牌

BNWJ_bot: 02-02 23:26:08
云间 胜利!游戏结束!

最终胜利时,保存的状态如下:

{
"players": [
"撄宁",
"云间"
],
"playerids": [
xxx,
xxx
],
"hands": {
"0": [
"红8",
"绿0",
"黄1",
"黄6",
"黄2"
],
"1": [
"黄9"
]
},
"deck": [
"黄5",
"绿3",
"红3",
"绿7",
"蓝5",
"绿3",
"绿8",
"红2",
"黄5",
"红3",
"黄3",
"红1",
"黄2",
"红0",
"黄9",
"蓝3",
"蓝8",
"红5",
"绿5",
"黄8",
"红6",
"蓝2",
"黄4",
"红7",
"黄7",
"蓝8",
"绿9",
"绿6",
"蓝4",
"蓝9",
"蓝9",
"绿5",
"黄7",
"黄0",
"绿1",
"黄8",
"黄4",
"绿1",
"黄3",
"蓝1",
"绿4",
"红9",
"黄6",
"蓝7",
"绿8",
"蓝6",
"绿7",
"绿2",
"黄1",
"绿2",
"红4",
"绿6",
"红7",
"红9",
"蓝0",
"红6"
],
"discard_pile": [
"红2",
"红5",
"红8",
"红4",
"红1",
"蓝1",
"蓝5",
"蓝3",
"蓝6",
"蓝2",
"蓝7",
"蓝4",
"绿4",
"绿9"
],
"current_player_index": 1,
"direction": 1,
"game_started": true,
"now_stat": [
"绿",
"9"
]
}

程序优化

  • 在群里公布时,展示出手牌的数量。
  • 轮到某玩家时私发提醒,并提醒该玩家需要应对哪张牌或什么状态。
  • 如果玩家需要帮助,可以即刻获得命令的帮助以及玩法的帮助。
  • 可以随时查看弃牌堆和牌堆的张数。

完整代码

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 nonebot
import random as r
import time
import json

# 假设UNO游戏最大玩家数量
MAX_PLAYERS = 8
# 假设每个玩家初始手牌数量
INITIAL_HAND_SIZE = 7

base=os.path.dirname(__file__)
json_path=os.path.join(base,"uno_data.json")



class UNO:
def __init__(self):
self.players: list[str] = [] # 玩家昵称列表
self.playerids: list[int] = [] # 玩家QQ号列表
self.hands: dict[int, list[str]] = {} # 玩家手牌
self.deck: list[str] = [] # 剩余牌堆
self.discard_pile: list[str] = [] # 弃牌堆
self.current_player_index: int = 0 # 当前玩家索引
self.direction: int = 1 # 出牌方向(顺时针或逆时针)
self.game_started: bool = False # 游戏是否已经开始
self.now_stat: list[str, str] = [] #当前弃牌堆牌顶的状态

def initialize_deck(self):
# 初始化牌堆
colors = ['红', '黄', '绿', '蓝']
number_cards = list(range(0, 10)) + list(range(1, 10)) # 每个数字两张,红0到红9、红1到红9
self.deck = []
for color in colors:
self.deck += [f"{color}{num}" for num in number_cards]
print(self.deck)
# 洗牌
r.shuffle(self.deck)

def save(self):
with open(json_path,"w",encoding='utf-8') as f:
json.dump(self.__dict__,f,ensure_ascii=False,indent=4)

def load(self):
with open(json_path,"r",encoding='utf-8') as f:
self.__dict__=json.load(f)

def initialize_card(self):
# 初始发牌
for i in range(len(self.playerids)):
self.hands[i] = [self.deck.pop() for _ in range(INITIAL_HAND_SIZE)]
# 随机选择一名玩家作为先手
if self.playerids:
self.current_player_index = r.randint(0, len(self.playerids)-1)

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 "颜色或数字不匹配!"
else:
return "不是数字牌!"

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

# 游戏状态存储
games: dict[int, UNO] = {} # 群号: 游戏状态

# uno = UNO()
# uno.initialize_deck()
# print(uno.deck)

create_game = on_command("创建uno房间")
join_game = on_command("加入uno房间")
start_game = on_command("一起uno")

@create_game.handle()
async def create_game_handle(bot: Bot, event: GroupMessageEvent):
group_id = event.group_id
if group_id not in games:
games[group_id] = UNO()
await bot.send(event, "UNO游戏已创建!请玩家使用 '加入uno房间' 命令加入游戏,使用 '一起uno' 命令开始游戏。")
else:
await bot.send(event, "该游戏已创建,请等待开始。")

@join_game.handle()
async def join_game_handle(bot: Bot, event: GroupMessageEvent):
group_id = event.group_id
user_id = event.user_id
nickname = event.sender.nickname
game = games.get(group_id)
# 处理非法情况
if not game or game.game_started:
await bot.send(event, "本群没有待加入的游戏或游戏已开始!")
return
if len(game.players) >= MAX_PLAYERS:
await bot.send(event, "玩家已满!")
return
# 正常加入游戏
if user_id in game.playerids:
await bot.send(event, f"{nickname} 已成功加入游戏 {group_id}!")
return
game.players.append(nickname)
game.playerids.append(user_id)
await bot.send(event, f"{nickname} 已加入游戏!当前玩家数量:{len(game.players)}")
game.save()

async def send_hand_cards(bot: Bot, game: UNO, n: int = 5, user_id: int = None):
if user_id:
hands = game.hands[game.playerids.index(user_id)]
hands.sort()
hand_message = '\n'.join([' '.join(hands[j:j+4]) for j in range(0, len(hands), n)])
await bot.send_private_msg(user_id=user_id, message=f"你的手牌 ({len(hands)}张)\n{hand_message}")
return
else:
for i, hands in game.hands.items():
hands.sort()
hand_message = '\n'.join([' '.join(hands[j:j+4]) for j in range(0, len(hands), n)])
await bot.send_private_msg(user_id=game.playerids[i], message=f"你的手牌 ({len(hands)}张)\n{hand_message}")
return

@start_game.handle()
async def start_game_handle(bot: Bot, event: GroupMessageEvent):
group_id = event.group_id
game = games.get(group_id) # 获取game
if not game or game.game_started:
await bot.send(event, "本群没有待开始的游戏或游戏已开始!")
return
if len(game.players) < 2:
await bot.send(event, "玩家数量不足!")
return
game.game_started = True
game.initialize_deck()
game.initialize_card()
game.save()
await bot.send(event, f"UNO游戏开始!游戏号:{group_id}\n玩家{len(game.playerids)}人:{'、'.join(game.players)}")
await bot.send(event, f"先手随机指定为:{game.players[game.current_player_index]};手牌已发送至私聊中。")
await send_hand_cards(bot, game)


play_card = on_command("出") # 出牌

@play_card.handle()
async def play_card_handle(bot: Bot, event: PrivateMessageEvent, message: Message = CommandArg()):
user_id = event.user_id
card = str(message).strip()
if not card:
await bot.send(event, "未输入要出的牌!")
return
# 查找玩家所在的游戏
for group_id_, game in games.items():
if not (game.game_started and (user_id in game.playerids)):
await bot.send(event, "你不在任何一个已经开始的游戏中!")
else:
group_id = group_id_
break

game = games.get(group_id)
nickname = game.players[game.playerids.index(user_id)]
# now_stat = game.now_stat

# 检查是否可以出牌
cannot_play_reason = game.cannot_play_card(card, user_id)
if cannot_play_reason:
await bot.send(event, cannot_play_reason)
return

# 出牌
game.hands[game.current_player_index].remove(card)
game.discard_pile.append(card)
game.update_now_stat(card) # 更新当前弃牌堆牌顶的状态


# 检查是否胜利
if not game.hands[game.current_player_index]:
await bot.send_group_msg(group_id=group_id, message=f"{game.players[game.current_player_index]} 胜利!游戏结束!")
del games[group_id]
return

# 切换到下家
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} 出了 {card}\n轮到 {game.players[game.current_player_index]} 出牌")
await send_hand_cards(bot, game, user_id=user_id) # 发送手牌信息


draw_card = on_command("摸") # 摸牌

@draw_card.handle()
async def draw_card_handle(bot: Bot, event: PrivateMessageEvent, message: Message = CommandArg()):
user_id = event.user_id
# 查找玩家所在的游戏
for group_id_, game in games.items():
if not (game.game_started and (user_id in game.playerids)):
await bot.send(event, "你不在任何一个已经开始的游戏中!")
else:
group_id = group_id_
break

game = games.get(group_id)
nickname = game.players[game.playerids.index(user_id)]

if game.current_player_index != game.playerids.index(user_id):
await bot.send(event, "还没轮到你摸牌!")
return

# 摸牌
if not game.deck:
await bot.send(event, "牌堆已空,无法摸牌!")
return

new_card = game.deck.pop()
game.hands[game.current_player_index].append(new_card)

# 切换到下家
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} 摸了一张牌\n轮到 {game.players[game.current_player_index]} 出牌")
await bot.send_private_msg(user_id=user_id, message=f"你摸了一张牌:{new_card}")
await send_hand_cards(bot, game, user_id=user_id) # 发送手牌信息