diff --git a/UnoVRBot/modules/uno.py b/UnoVRBot/modules/uno.py index c267dc0..ad0af27 100644 --- a/UnoVRBot/modules/uno.py +++ b/UnoVRBot/modules/uno.py @@ -16,6 +16,7 @@ import logging from dataclasses import dataclass, field from enum import Enum, auto from typing import Optional +from matplotlib.colors import same_color from pyrogram.types import Message from pyrogram.errors import RPCError from pyrogram.enums import ChatType @@ -71,6 +72,7 @@ class UNOCard: placed_by: "UNOPlayer" = None # Extra metadata metadata: Optional[dict[str, int | str | bool]] = None + placed_when: int = -1 def get_default_deck(n_decks: int = 1) -> deque[UNOCard]: @@ -80,7 +82,7 @@ def get_default_deck(n_decks: int = 1) -> deque[UNOCard]: """ result = [] - for __ in range(n_decks): + for __ in range(0, n_decks, 1): # Numbers: 1 to 9 for each color (twice for each color) for color in [UNOCardColor.GREEN, UNOCardColor.RED, UNOCardColor.YELLOW, UNOCardColor.BLUE]: @@ -104,9 +106,9 @@ def get_default_deck(n_decks: int = 1) -> deque[UNOCard]: result.append(UNOCard(UNOCardType.DRAW, color, metadata={"exhausted": False})) # Wild and wild draw cards for _ in range(4): - result.append(UNOCard(UNOCardType.WILD, UNOCardColor.BLACK, stackable=False, metadata={"exhausted": False})) + result.append(UNOCard(UNOCardType.WILD, UNOCardColor.BLACK, stackable=False, metadata={"exhausted": False, "color": None})) for _ in range(4): - result.append(UNOCard(UNOCardType.WILD_DRAW, UNOCardColor.BLACK, stackable=False, metadata={"exhausted": False})) + result.append(UNOCard(UNOCardType.WILD_DRAW, UNOCardColor.BLACK, stackable=False, metadata={"exhausted": False, "color": None})) random.shuffle(result) return deque(result) @@ -139,6 +141,7 @@ class UNOGame: current_pos: int = 0 current_player: Optional[UNOPlayer] = None winners: list[UNOPlayer] = field(default_factory=list) + round: int = 0 @dataclass @@ -517,7 +520,7 @@ def pick_first_card(game: UNOGame): break -def distribute_cards(game: UNOGame, n: int = 1): +def distribute_cards(game: UNOGame, n: int = 7): """ Distributes n cards to all players evenly as if they were being given in real life @@ -528,9 +531,6 @@ def distribute_cards(game: UNOGame, n: int = 1): while not all(len(player.cards) == n for player in game.players): for player in game.players: player.cards.append(game.deck.popleft()) - for player in game.players: - for card in player.cards: - card.placed_by = player @Client.on_message(filters.command("begin")) @@ -604,6 +604,7 @@ def card_to_string(card: UNOCard) -> str: UNOCardColor.GREEN: "🟢", UNOCardColor.YELLOW: "🟡", } + print(maps) result = "" match card.kind: case UNOCardType.NUMBER: @@ -646,13 +647,80 @@ async def info(_: Client, message: Message): logging.error(f"An unexpected RPC error occurred: {type(rpc_error).__name__} -> {rpc_error}") -async def check_special_cards(client: Client, game: UNOGame): +def pick_next_player(game: UNOGame): + """ + Picks the next player in the queue and schedules + their turn + """ + + while True: + game.current_pos += 1 + game.current_player = game.players[game.current_pos % len(game.players)] + if game.current_player.won: + # Skip players who won already + continue + else: + break + + +async def check_special_cards(client: Client, game: UNOGame): """ Performs some checks on the effects of special cards and sends some messages to users to make them aware of any changes that occur """ + try: + draw = 0 + cards: list[UNOCard] = [] + i = 0 + while game.table[i].kind in {UNOCardType.WILD, UNOCardType.WILD_DRAW, + UNOCardType.REVERSE, UNOCardType.SKIP, + UNOCardType.DRAW} and not game.table[i].metadata["exhausted"]: + cards.append(game.table[i]) + i += 1 + for card in cards: + card.metadata["exhausted"] = True + match card.kind: + case UNOCardType.WILD | UNOCardType.WILD_DRAW as k: + await client.send_message(game.current_player.id, "Choose the color for the new top card between red, green and blue") + USERS[game.table[0].placed_by][0] = PlayerAction.CHOOSING_COLOR + found = False + if k == UNOCardType.WILD_DRAW: + for next_card in game.current_player.cards: + if next_card.kind == UNOCardType.WILD_DRAW: + found = True + break + if not found: + draw += 4 + case UNOCardType.REVERSE: + for user in game.players: + if user != card.placed_by: + await client.send_message(user.id, "The direction of play reverses!") + game.players = list(reversed(game.players)) + case UNOCardType.DRAW: + found = False + for next_card in game.current_player.cards: + if next_card.kind == UNOCardType.DRAW: + found = True + break + if not found: + draw += 2 + case UNOCardType.SKIP: + for user in game.players: + if user != card.placed_by: + await client.send_message(user.id, "A turn has been skipped!") + pick_next_player(game) + if draw: + await client.send_message(game.current_player.id, f"You are foced to draw {draw} cards!") + for _ in range(draw): + if not game.deck: + reshuffle_table_in_deck(game) + game.current_player.cards.append(game.deck.popleft()) + await client.send_message(game.current_player.id, f"You draw a card. It's {card_to_string(game.current_player.cards[-1])}") + except RPCError as rpc_error: + logging.error(f"An unexpected RPC error occurred: {type(rpc_error).__name__} -> {rpc_error}") + @Client.on_message(filters.command("pass")) async def pass_turn(client: Client, message: Message): @@ -663,6 +731,8 @@ async def pass_turn(client: Client, message: Message): try: if (data := USERS[message.from_user.id])[0] == PlayerAction.NONE: await message.reply_text("You haven't created or joined a game lobby yet. Send /play to create it or /join to join an existing one", parse_mode=ParseMode.MARKDOWN) + elif data[0] == PlayerAction.CHOOSING_COLOR: + await message.reply_text("You need to choose a color between red, green, blue and yellow before passing your turn") else: lobby = data[1] for player in lobby.game.players: @@ -678,17 +748,8 @@ async def pass_turn(client: Client, message: Message): await message.reply_text("You have to draw or throw a card before passing your turn") return else: - while True: - if lobby.game.direction == UNODirection.CLOCKWISE: - lobby.game.current_pos += 1 - else: - lobby.game.current_pos -= 1 - lobby.game.current_player = lobby.game.players[lobby.game.current_pos % len(lobby.game.players)] - if lobby.game.current_player.won: - # Skip players who won already - continue - else: - break + pick_next_player(lobby.game) + lobby.game.round += 1 USERS[message.from_user.id] = (PlayerAction.END_TURN, USERS[message.from_user.id][1]) name = f"{message.from_user.first_name or ''}{message.from_user.last_name or ''}" next_name = lobby.game.current_player.name @@ -761,24 +822,31 @@ def check_next_throw(game: UNOGame, card: UNOCard) -> bool: """ top = game.table[0] - result = False - match card.kind: - case UNOCardType.NUMBER as k if top.kind == k: - if card.metadata["value"] == top.metadata["value"]: - result = True - elif card.color == top.color: - result = True - # Can't stack colors! - if top.placed_by and card.color == top.color and card.placed_by is top.placed_by: - result = False - case UNOCardType.DRAW | UNOCardType.REVERSE | UNOCardType.SKIP as k: - result = top.kind == k and top.color == card.color + top_color = top.color + card_color = card.color + if top.kind in {UNOCardType.WILD, UNOCardType.WILD_DRAW}: + top_color = top.metadata["color"] + if card.kind in {UNOCardType.WILD, UNOCardType.WILD_DRAW}: + card_color = card.metadata["color"] + same_color = card_color == top_color + same_turn = top.placed_when == game.round + match top.kind: + case UNOCardType.NUMBER as k if card.kind == k: + # Same number or same color in this turn + return top.metadata["value"] == card.metadata["value"] or (same_color and not same_turn) + case UNOCardType.NUMBER as k if card.kind != k: + # Same as above, without the number check + return same_color and not same_turn + case UNOCardType.SKIP | UNOCardType.REVERSE | UNOCardType.DRAW: + result = same_color and not same_turn + if top.kind == card.kind: + # Can you stack this special card in a turn? + result = result and top.stackable + return result case UNOCardType.WILD | UNOCardType.WILD_DRAW: - result = True + return False # TODO case _: - result = False - result = result and top.stackable - return result + return False @Client.on_message(filters.command("throw")) @@ -815,6 +883,8 @@ async def throw(client: Client, message: Message): if card_no > len(player.cards) or card_no == 0: await message.reply_text(f"Invalid card: you can choose between 1 and {len(player.cards)}") return + player.cards[card_no - 1].placed_when = lobby.game.round + player.cards[card_no - 1].placed_by = player if not check_next_throw(lobby.game, player.cards[card_no - 1]): await message.reply_text("You can't throw that card") return @@ -864,6 +934,6 @@ async def throw(client: Client, message: Message): msg = f"Game status: \n- Card on the table: {card_to_string(lobby.game.table[0])}\n\n- Current Player: {lobby.game.current_player.name}\n\n- Your cards:\n" for i, card in enumerate(player.cards): msg += f"{i + 1}. {card_to_string(card)}\n" - await client.send_message(lobby.game.current_player.id, msg) + await client.send_message(lobby.game.current_player.id, msg) except RPCError as rpc_error: logging.error(f"An unexpected RPC error occurred: {type(rpc_error).__name__} -> {rpc_error}")