From e3b168ff57adb123ba480ac4694fd08ffe91f379 Mon Sep 17 00:00:00 2001 From: Alberto Xamin Date: Sun, 2 Apr 2023 13:47:08 +0100 Subject: [PATCH] card event refactoring and moved wild-west-show to its own pile --- backend/bang/cards.py | 421 ++-- backend/bang/deck.py | 84 +- .../fistful_of_cards/card_events.py | 162 +- .../bang/expansions/high_noon/card_events.py | 159 +- .../expansions/wild_west_show/card_events.py | 79 +- backend/bang/game.py | 936 ++++--- backend/bang/players.py | 2163 ++++++++++++----- backend/server.py | 1097 ++++++--- backend/tests/__init__.py | 11 +- backend/tests/game_test.py | 56 +- frontend/src/components/Deck.vue | 40 +- frontend/src/components/Lobby.vue | 16 +- 12 files changed, 3772 insertions(+), 1452 deletions(-) diff --git a/backend/bang/cards.py b/backend/bang/cards.py index 98432f0..75a1225 100644 --- a/backend/bang/cards.py +++ b/backend/bang/cards.py @@ -11,6 +11,7 @@ if TYPE_CHECKING: from bang.players import Player from bang.game import Game + class Suit(IntEnum): DIAMONDS = 0 # ♦ CLUBS = 1 # ♣ @@ -20,14 +21,20 @@ class Suit(IntEnum): class Card(ABC): - sym = { - 'A': 1, - 'J': 11, - 'Q': 12, - 'K': 13 - } + sym = {"A": 1, "J": 11, "Q": 12, "K": 13} - def __init__(self, suit: Suit, name: str, number, is_equipment: bool = False, is_weapon: bool = False, vis_mod: int = 0, sight_mod: int = 0, range: int = 99, desc: str = ''): + def __init__( + self, + suit: Suit, + name: str, + number, + is_equipment: bool = False, + is_weapon: bool = False, + vis_mod: int = 0, + sight_mod: int = 0, + range: int = 99, + desc: str = "", + ): super().__init__() self.name = name self.suit = suit @@ -41,22 +48,24 @@ class Card(ABC): self.sight_mod = sight_mod self.range = range if self.range != 0 and self.range != 99: - self.alt_text = f'{self.range} 🔍' - self.desc = desc # deprecated, has been replaced by the card's description in the localization files (see i18n folder) - self.need_target = False # Cards that need a target like Bang - self.can_target_self = False # for example Panico and CatBalou - self.can_be_used_now = True # to check wether the green card can be used now - self.usable_next_turn = False # it will be True for Green Cards - self.need_with = False # it will be true for cards that require a card to be discarded with - self.need_with_only = '' # names of the cards allowed to be discarded with - self.must_be_used = False # used by LeggeDelWest + self.alt_text = f"{self.range} 🔍" + self.desc = desc # deprecated, has been replaced by the card's description in the localization files (see i18n folder) + self.need_target = False # Cards that need a target like Bang + self.can_target_self = False # for example Panico and CatBalou + self.can_be_used_now = True # to check wether the green card can be used now + self.usable_next_turn = False # it will be True for Green Cards + self.need_with = ( + False # it will be true for cards that require a card to be discarded with + ) + self.need_with_only = "" # names of the cards allowed to be discarded with + self.must_be_used = False # used by LeggeDelWest def __str__(self): if str(self.suit).isnumeric(): - char = ['♦️', '♣️', '♥️', '♠️', '🤑'][int(self.suit)] + char = ["♦️", "♣️", "♥️", "♠️", "🤑"][int(self.suit)] else: char = self.suit - return f'{self.name} {char}{self.number}' + return f"{self.name} {char}{self.number}" return super().__str__() def num_suit(self): @@ -70,8 +79,14 @@ class Card(ABC): if self.must_be_used: self.must_be_used = False - def play_card(self, player:Player, against:str=None, _with:int=None):#self --> carta - if (player.game.check_event(ce.IlGiudice)) and self.usable_next_turn and not self.can_be_used_now: + def play_card( + self, player: Player, against: str = None, _with: int = None + ): # self --> carta + if ( + (player.game.check_event(ce.IlGiudice)) + and self.usable_next_turn + and not self.can_be_used_now + ): return False if self.is_equipment: if (player.game.check_event(ce.IlGiudice)) or not self.can_be_used_now: @@ -86,38 +101,53 @@ class Card(ABC): break if not has_weapon: player.equipment.append(self) - elif self.name in [c.name for c in player.equipment if not isinstance(c, Dinamite)]: + elif self.name in [ + c.name for c in player.equipment if not isinstance(c, Dinamite) + ]: return False else: player.equipment.append(self) self.must_be_used = False self.can_be_used_now = False if against: - G.sio.emit('card_against', room=player.game.name, data={'player': player.name, 'target': against, 'card': self.__dict__}) - G.sio.emit('chat_message', room=player.game.name, - data=f'_play_card_against|{player.name}|{self.name}|{against}') + G.sio.emit( + "card_against", + room=player.game.name, + data={"player": player.name, "target": against, "card": self.__dict__}, + ) + G.sio.emit( + "chat_message", + room=player.game.name, + data=f"_play_card_against|{player.name}|{self.name}|{against}", + ) else: - G.sio.emit('chat_message', room=player.game.name, - data=f'_play_card|{player.name}|{self.name}') + G.sio.emit( + "chat_message", + room=player.game.name, + data=f"_play_card|{player.name}|{self.name}", + ) return True def use_card(self, player): pass - def is_duplicate_card(self, player:Player): - return any(c.name==self.name for c in player.equipment) or any(c.name==self.name for c in player.gold_rush_equipment) + def is_duplicate_card(self, player: Player): + return any(c.name == self.name for c in player.equipment) or any( + c.name == self.name for c in player.gold_rush_equipment + ) - def check_suit(self, game:Game, accepted:List[Suit]): + def check_suit(self, game: Game, accepted: List[Suit]): if game.check_event(ceh.Benedizione): return Suit.HEARTS in accepted elif game.check_event(ceh.Maledizione): return Suit.SPADES in accepted return self.suit in accepted + class Barile(Card): def __init__(self, suit, number): - super().__init__(suit, 'Barile', number, is_equipment=True) - self.icon = '🛢' + super().__init__(suit, "Barile", number, is_equipment=True) + self.icon = "🛢" self.alt_text = "♥️=😅" # self.desc = "Quando sei bersagliato da un Bang puoi estrarre la prima carta dalla cima del mazzo, se la carta estratta è del seme Cuori allora vale come un Mancato" # self.desc_eng = "When someone plays a Bang against you. You can flip the first card from the deck, if the suit is Hearts then it counts as a Missed card" @@ -125,8 +155,8 @@ class Barile(Card): class Dinamite(Card): def __init__(self, suit, number): - super().__init__(suit, 'Dinamite', number, is_equipment=True) - self.icon = '🧨' + super().__init__(suit, "Dinamite", number, is_equipment=True) + self.icon = "🧨" self.alt_text = "2-9♠️ = 🤯" # self.desc = "Giocando la Dinamite, posizionala davanti a te, resterà innocua per un intero giro. All'inizio del prossimo turno prima di pescare e prima di una eventuale estrazione (es. Prigione), estrai una carta dalla cima del mazzo. Se esce una carta tra il 2 il 9 di picche (compresi) allora la dinamite esplode: perdi 3 vite e scarta la carta, altrimenti passa la dinamite al giocatore successivo, il quale estrarà a sua volta dopo che tu avrai passato il tuo turno" # self.desc_eng = "When playing Dynamite, place it in front of you, it will remain harmless for a whole round. At the beginning of the next turn before drawing and before any card flip (eg Prison), flip a card from the top of the deck. If a card is between 2 and 9 of spades (inclusive) then the dynamite explodes: you lose 3 lives and discard the card, otherwise pass the dynamite to the next player, who will draw in turn after you have ended your turn" @@ -134,8 +164,8 @@ class Dinamite(Card): class Mirino(Card): def __init__(self, suit, number): - super().__init__(suit, 'Mirino', number, is_equipment=True, sight_mod=1) - self.icon = '🔎' + super().__init__(suit, "Mirino", number, is_equipment=True, sight_mod=1) + self.icon = "🔎" self.alt_text = "-1" # self.desc = "Tu vedi gli altri giocatori a distanza -1" # self.desc_eng = "You see the other players at distance -1" @@ -143,8 +173,8 @@ class Mirino(Card): class Mustang(Card): def __init__(self, suit, number): - super().__init__(suit, 'Mustang', number, is_equipment=True, vis_mod=1) - self.icon = '🐎' + super().__init__(suit, "Mustang", number, is_equipment=True, vis_mod=1) + self.icon = "🐎" self.alt_text = "+1" # self.desc = "Gli altri giocatori ti vedono a distanza +1" # self.desc_eng = "The other players see you at distance +1" @@ -152,100 +182,130 @@ class Mustang(Card): class Prigione(Card): def __init__(self, suit, number): - super().__init__(suit, 'Prigione', number, is_equipment=True) - self.icon = '⛓' + super().__init__(suit, "Prigione", number, is_equipment=True) + self.icon = "⛓" # self.desc = "Equipaggia questa carta a un altro giocatore, tranne lo Sceriffo. Il giocatore scelto all'inizio del suo turno, prima di pescare dovrà estrarre: se esce Cuori scarta questa carta e gioca normalmente il turno, altrimenti scarta questa carta e salta il turno" # self.desc_eng = "Equip this card to another player, except the Sheriff. The player chosen at the beginning of his turn, must flip a card before drawing: if it's Hearts, discard this card and play the turn normally, otherwise discard this card and skip the turn" self.need_target = True self.alt_text = "♥️= 🆓" def play_card(self, player, against, _with=None): - if (player.game.check_event(ce.IlGiudice)): - return False - if against is not None and not isinstance(player.game.get_player_named(against).role, r.Sheriff): + if player.game.check_event(ce.IlGiudice): + return False + if against is not None and not isinstance( + player.game.get_player_named(against).role, r.Sheriff + ): self.can_be_used_now = False - G.sio.emit('chat_message', room=player.game.name, - data=f'_play_card_against|{player.name}|{self.name}|{against}') + G.sio.emit( + "chat_message", + room=player.game.name, + data=f"_play_card_against|{player.name}|{self.name}|{against}", + ) player.game.get_player_named(against).equipment.append(self) player.game.get_player_named(against).notify_self() return True return False + class Remington(Card): def __init__(self, suit, number): - super().__init__(suit, 'Remington', number, - is_equipment=True, is_weapon=True, range=3) - self.icon = '🔫' + super().__init__( + suit, "Remington", number, is_equipment=True, is_weapon=True, range=3 + ) + self.icon = "🔫" # self.desc = "Puoi sparare a un giocatore che sia distante 3 o meno" # self.desc_eng = "You can shoot another player at distance 3 or less" class RevCarabine(Card): def __init__(self, suit, number): - super().__init__(suit, 'Rev Carabine', number, - is_equipment=True, is_weapon=True, range=4) - self.icon = '🔫' + super().__init__( + suit, "Rev Carabine", number, is_equipment=True, is_weapon=True, range=4 + ) + self.icon = "🔫" # self.desc = "Puoi sparare a un giocatore che sia distante 4 o meno" # self.desc_eng = "You can shoot another player at distance 4 or less" class Schofield(Card): def __init__(self, suit, number): - super().__init__(suit, 'Schofield', number, - is_equipment=True, is_weapon=True, range=2) - self.icon = '🔫' + super().__init__( + suit, "Schofield", number, is_equipment=True, is_weapon=True, range=2 + ) + self.icon = "🔫" # self.desc = "Puoi sparare a un giocatore che sia distante 2 o meno" # self.desc_eng = "You can shoot another player at distance 2 or less" class Volcanic(Card): def __init__(self, suit, number): - super().__init__(suit, 'Volcanic', number, - is_equipment=True, is_weapon=True, range=1) - self.icon = '🔫' + super().__init__( + suit, "Volcanic", number, is_equipment=True, is_weapon=True, range=1 + ) + self.icon = "🔫" # self.desc = "Puoi sparare a un giocatore che sia distante 1 o meno, tuttavia puoi giocare quanti bang vuoi" # self.desc_eng = "You can shoot another player at distance 1 or less, however you no longer have the limit of 1 Bang" class Winchester(Card): def __init__(self, suit, number): - super().__init__(suit, 'Winchester', number, - is_equipment=True, is_weapon=True, range=5) - self.icon = '🔫' + super().__init__( + suit, "Winchester", number, is_equipment=True, is_weapon=True, range=5 + ) + self.icon = "🔫" # self.desc = "Puoi sparare a un giocatore che sia distante 5 o meno" # self.desc_eng = "You can shoot another player at distance 5 or less" class Bang(Card): def __init__(self, suit, number): - super().__init__(suit, 'Bang!', number) - self.icon = '💥' + super().__init__(suit, "Bang!", number) + self.icon = "💥" # self.desc = "Spara a un giocatore a distanza raggiungibile. Se non hai armi la distanza di default è 1" # self.desc_eng = "Shoot a player in sight. If you do not have weapons, your is sight is 1" self.need_target = True def play_card(self, player, against, _with=None): - if player.game.check_event(ceh.Sermone) and not self.number == 42: # 42 gold rush + if ( + player.game.check_event(ceh.Sermone) and not self.number == 42 + ): # 42 gold rush return False - if ((player.has_played_bang and not self.number == 42) and (not any((isinstance(c, Volcanic) for c in player.equipment)) or player.game.check_event(ce.Lazo)) and against is not None): # 42 gold rush: + if ( + (player.has_played_bang and not self.number == 42) + and ( + not any((isinstance(c, Volcanic) for c in player.equipment)) + or player.game.check_event(ce.Lazo) + ) + and against is not None + ): # 42 gold rush: return False elif against is not None: import bang.characters as chars + super().play_card(player, against=against) - if not (self.number == 42 and self.suit == Suit.GOLD): # 42 gold rush + if not (self.number == 42 and self.suit == Suit.GOLD): # 42 gold rush player.bang_used += 1 - player.has_played_bang = True if not player.game.check_event(ceh.Sparatoria) else player.bang_used > 1 + player.has_played_bang = ( + True + if not player.game.check_event(ceh.Sparatoria) + else player.bang_used > 1 + ) if player.character.check(player.game, chars.WillyTheKid): player.has_played_bang = False - player.game.attack(player, against, double=player.character.check(player.game, chars.SlabTheKiller), card_name=self.name) + player.game.attack( + player, + against, + double=player.character.check(player.game, chars.SlabTheKiller), + card_name=self.name, + ) return True return False class Birra(Card): def __init__(self, suit, number): - super().__init__(suit, 'Birra', number) - self.icon = '🍺' + super().__init__(suit, "Birra", number) + self.icon = "🍺" # self.desc = "Gioca questa carta per recuperare un punto vita. Non puoi andare oltre al limite massimo del tuo personaggio. Se stai per perdere l'ultimo punto vita puoi giocare questa carta anche nel turno dell'avversario. La birra non ha più effetto se ci sono solo due giocatori" # self.desc_eng = "Play this card to regain a life point. You cannot heal more than your character's maximum limit. If you are about to lose your last life point, you can also play this card on your opponent's turn. Beer no longer takes effect if there are only two players" @@ -254,47 +314,68 @@ class Birra(Card): return False if not skipChecks: import bang.expansions.gold_rush.characters as grch - madamYto = [p for p in player.game.get_alive_players() if p.character.check(player.game, grch.MadamYto) and self.number != 42] + + madamYto = [ + p + for p in player.game.get_alive_players() + if p.character.check(player.game, grch.MadamYto) and self.number != 42 + ] for p in madamYto: player.game.deck.draw(True, player=p) p.notify_self() - if 'gold_rush' in player.game.expansions and self.number != 42: + if "gold_rush" in player.game.expansions and self.number != 42: from bang.players import PendingAction - player.available_cards = [{ - 'name': 'Pepita', - 'icon': '💵️', - 'alt_text': '1', - 'noDesc': True - }, self] - player.choose_text = 'choose_birra_function' + + player.available_cards = [ + {"name": "Pepita", "icon": "💵️", "alt_text": "1", "noDesc": True}, + self, + ] + player.choose_text = "choose_birra_function" player.pending_action = PendingAction.CHOOSE player.notify_self() return True - if (len(player.game.get_alive_players()) != 2 or self.number == 42) and player.lives < player.max_lives: + if ( + len(player.game.get_alive_players()) != 2 or self.number == 42 + ) and player.lives < player.max_lives: super().play_card(player, against=against) - player.lives = min(player.lives+1, player.max_lives) + player.lives = min(player.lives + 1, player.max_lives) import bang.expansions.dodge_city.characters as chd + if player.character.check(player.game, chd.TequilaJoe): - player.lives = min(player.lives+1, player.max_lives) + player.lives = min(player.lives + 1, player.max_lives) return True - elif len(player.game.get_alive_players()) == 2 or player.lives == player.max_lives: - G.sio.emit('chat_message', room=player.game.name, - data=f'_spilled_beer|{player.name}|{self.name}') + elif ( + len(player.game.get_alive_players()) == 2 + or player.lives == player.max_lives + ): + G.sio.emit( + "chat_message", + room=player.game.name, + data=f"_spilled_beer|{player.name}|{self.name}", + ) return True return False class CatBalou(Card): def __init__(self, suit, number): - super().__init__(suit, 'Cat Balou', number) - self.icon = '💃' + super().__init__(suit, "Cat Balou", number) + self.icon = "💃" # self.desc = "Fai scartare una carta a un qualsiasi giocatore, scegli a caso dalla mano, oppure fra quelle che ha in gioco" # self.desc_eng = "Choose and discard a card from any other player." self.need_target = True self.can_target_self = True def play_card(self, player, against, _with=None): - if against is not None and (len(player.game.get_player_named(against).hand) + len(player.game.get_player_named(against).equipment)) > 0 and (player.name != against or len(player.equipment) > 0): + if ( + against is not None + and ( + len(player.game.get_player_named(against).hand) + + len(player.game.get_player_named(against).equipment) + ) + > 0 + and (player.name != against or len(player.equipment) > 0) + ): super().play_card(player, against=against) player.game.steal_discard(player, against, self) return True @@ -303,25 +384,29 @@ class CatBalou(Card): class Diligenza(Card): def __init__(self, suit, number): - super().__init__(suit, 'Diligenza', number) - self.icon = '🚡' + super().__init__(suit, "Diligenza", number) + self.icon = "🚡" self.alt_text = "🎴🎴" # self.desc = "Pesca 2 carte dalla cima del mazzo" # self.desc_eng = "Draw 2 cards from the deck." def play_card(self, player, against, _with=None): - G.sio.emit('chat_message', room=player.game.name, - data=f'_diligenza|{player.name}|{self.name}') + G.sio.emit( + "chat_message", + room=player.game.name, + data=f"_diligenza|{player.name}|{self.name}", + ) for i in range(2): player.game.deck.draw(True, player) + player.game.deck.flip_wildwestshow() return True class Duello(Card): def __init__(self, suit, number): - super().__init__(suit, 'Duello', number) + super().__init__(suit, "Duello", number) self.need_target = True - self.icon = '⚔️' + self.icon = "⚔️" # self.desc = "Gioca questa carta contro un qualsiasi giocatore. A turno, cominciando dal tuo avversario, potete scartare una carta Bang!, il primo giocatore che non lo fa perde 1 vita" # self.desc_eng = "Play this card against any player. In turn, starting with your opponent, you can discard a Bang! Card, the first player who does not do so loses 1 life." @@ -335,8 +420,8 @@ class Duello(Card): class Emporio(Card): def __init__(self, suit, number): - super().__init__(suit, 'Emporio', number) - self.icon = '🏪' + super().__init__(suit, "Emporio", number) + self.icon = "🏪" # self.desc = "Scopri dal mazzo tante carte quanto il numero di giocatori vivi, a turno, partendo da te, scegliete una carta e aggiungetela alla vostra mano" # self.desc_eng = "Put on the table N cards from the deck, where N is the number of alive players, in turn, starting with you, choose a card and add it to your hand" @@ -348,8 +433,8 @@ class Emporio(Card): class Gatling(Card): def __init__(self, suit, number): - super().__init__(suit, 'Gatling', number) - self.icon = '🛰' + super().__init__(suit, "Gatling", number) + self.icon = "🛰" # self.desc = "Spara a tutti gli altri giocatori" # self.desc_eng = "Shoot all the other players" self.alt_text = "👥💥" @@ -362,8 +447,8 @@ class Gatling(Card): class Indiani(Card): def __init__(self, suit, number): - super().__init__(suit, 'Indiani!', number) - self.icon = '🏹' + super().__init__(suit, "Indiani!", number) + self.icon = "🏹" # self.desc = "Tutti gli altri giocatori devono scartare un Bang! o perdere una vita" # self.desc_eng = "All the other players must discard a Bang! or lose 1 Health Point" @@ -375,22 +460,35 @@ class Indiani(Card): class Mancato(Card): def __init__(self, suit, number): - super().__init__(suit, 'Mancato!', number) - self.icon = '😅' + super().__init__(suit, "Mancato!", number) + self.icon = "😅" # self.desc = "Usa questa carta per annullare un bang" # self.desc_eng = "Use this card to cancel the effect of a bang" def play_card(self, player, against, _with=None): import bang.characters as chars - if against is not None and player.character.check(player.game, chars.CalamityJanet): - if player.has_played_bang and (not any((isinstance(c, Volcanic) for c in player.equipment)) or player.game.check_event(ce.Lazo)): + + if against is not None and player.character.check( + player.game, chars.CalamityJanet + ): + if player.has_played_bang and ( + not any((isinstance(c, Volcanic) for c in player.equipment)) + or player.game.check_event(ce.Lazo) + ): return False if player.game.check_event(ceh.Sermone): return False - G.sio.emit('chat_message', room=player.game.name, - data=f'_special_calamity|{player.name}|{self.name}|{against}') + G.sio.emit( + "chat_message", + room=player.game.name, + data=f"_special_calamity|{player.name}|{self.name}|{against}", + ) player.bang_used += 1 - player.has_played_bang = True if not player.game.check_event(ceh.Sparatoria) else player.bang_used > 1 + player.has_played_bang = ( + True + if not player.game.check_event(ceh.Sparatoria) + else player.bang_used > 1 + ) player.game.attack(player, against, card_name=self.name) return True return False @@ -398,15 +496,23 @@ class Mancato(Card): class Panico(Card): def __init__(self, suit, number): - super().__init__(suit, 'Panico!', number, range=1) - self.icon = '😱' + super().__init__(suit, "Panico!", number, range=1) + self.icon = "😱" self.need_target = True self.can_target_self = True # self.desc = "Pesca una carta da un giocatore a distanza 1, scegli a caso dalla mano, oppure fra quelle che ha in gioco" # self.desc_eng = "Steal a card from a player at distance 1" def play_card(self, player, against, _with=None): - if against is not None and (len(player.game.get_player_named(against).hand) + len(player.game.get_player_named(against).equipment)) > 0 and (player.name != against or len(player.equipment) > 0): + if ( + against is not None + and ( + len(player.game.get_player_named(against).hand) + + len(player.game.get_player_named(against).equipment) + ) + > 0 + and (player.name != against or len(player.equipment) > 0) + ): super().play_card(player, against=against) player.game.steal_discard(player, against, self) return True @@ -415,58 +521,66 @@ class Panico(Card): class Saloon(Card): def __init__(self, suit, number): - super().__init__(suit, 'Saloon', number) + super().__init__(suit, "Saloon", number) # self.desc = "Tutti i giocatori recuperano un punto vita compreso chi gioca la carta" # self.desc_eng = "Everyone heals 1 Health point" - self.icon = '🍻' + self.icon = "🍻" self.alt_text = "👥🍺" def play_card(self, player, against, _with=None): - G.sio.emit('chat_message', room=player.game.name, - data=f'_saloon|{player.name}|{self.name}') + G.sio.emit( + "chat_message", + room=player.game.name, + data=f"_saloon|{player.name}|{self.name}", + ) for p in player.game.get_alive_players(): - p.lives = min(p.lives+1, p.max_lives) + p.lives = min(p.lives + 1, p.max_lives) p.notify_self() return True class WellsFargo(Card): def __init__(self, suit, number): - super().__init__(suit, 'WellsFargo', number) + super().__init__(suit, "WellsFargo", number) # self.desc = "Pesca 3 carte dalla cima del mazzo" # self.desc_eng = "Draw 3 cards from the deck" - self.icon = '💸' + self.icon = "💸" self.alt_text = "🎴🎴🎴" def play_card(self, player, against, _with=None): - G.sio.emit('chat_message', room=player.game.name, - data=f'_wellsfargo|{player.name}|{self.name}') - for i in range(3): + G.sio.emit( + "chat_message", + room=player.game.name, + data=f"_wellsfargo|{player.name}|{self.name}", + ) + for _ in range(3): player.game.deck.draw(True, player) + player.game.deck.flip_wildwestshow() return True -def get_starting_deck(expansions:List[str]) -> List[Card]: +def get_starting_deck(expansions: List[str]) -> List[Card]: from bang.expansions import DodgeCity, TheValleyOfShadows + base_cards = [ - Barile(Suit.SPADES, 'Q'), - Barile(Suit.SPADES, 'K'), + Barile(Suit.SPADES, "Q"), + Barile(Suit.SPADES, "K"), Dinamite(Suit.HEARTS, 2), - Mirino(Suit.SPADES, 'A'), + Mirino(Suit.SPADES, "A"), Mustang(Suit.HEARTS, 8), Mustang(Suit.HEARTS, 9), - Prigione(Suit.SPADES, 'J'), + Prigione(Suit.SPADES, "J"), Prigione(Suit.HEARTS, 4), Prigione(Suit.SPADES, 10), - Remington(Suit.CLUBS, 'K'), - RevCarabine(Suit.CLUBS, 'A'), - Schofield(Suit.CLUBS, 'J'), - Schofield(Suit.CLUBS, 'Q'), - Schofield(Suit.SPADES, 'K'), + Remington(Suit.CLUBS, "K"), + RevCarabine(Suit.CLUBS, "A"), + Schofield(Suit.CLUBS, "J"), + Schofield(Suit.CLUBS, "Q"), + Schofield(Suit.SPADES, "K"), Volcanic(Suit.SPADES, 10), Volcanic(Suit.CLUBS, 10), Winchester(Suit.SPADES, 8), - Bang(Suit.SPADES, 'A'), + Bang(Suit.SPADES, "A"), Bang(Suit.DIAMONDS, 2), Bang(Suit.DIAMONDS, 3), Bang(Suit.DIAMONDS, 4), @@ -476,10 +590,10 @@ def get_starting_deck(expansions:List[str]) -> List[Card]: Bang(Suit.DIAMONDS, 8), Bang(Suit.DIAMONDS, 9), Bang(Suit.DIAMONDS, 10), - Bang(Suit.DIAMONDS, 'J'), - Bang(Suit.DIAMONDS, 'Q'), - Bang(Suit.DIAMONDS, 'K'), - Bang(Suit.DIAMONDS, 'A'), + Bang(Suit.DIAMONDS, "J"), + Bang(Suit.DIAMONDS, "Q"), + Bang(Suit.DIAMONDS, "K"), + Bang(Suit.DIAMONDS, "A"), Bang(Suit.CLUBS, 2), Bang(Suit.CLUBS, 3), Bang(Suit.CLUBS, 4), @@ -488,34 +602,34 @@ def get_starting_deck(expansions:List[str]) -> List[Card]: Bang(Suit.CLUBS, 7), Bang(Suit.CLUBS, 8), Bang(Suit.CLUBS, 9), - Bang(Suit.HEARTS, 'Q'), - Bang(Suit.HEARTS, 'K'), - Bang(Suit.HEARTS, 'A'), + Bang(Suit.HEARTS, "Q"), + Bang(Suit.HEARTS, "K"), + Bang(Suit.HEARTS, "A"), Birra(Suit.HEARTS, 6), Birra(Suit.HEARTS, 7), Birra(Suit.HEARTS, 8), Birra(Suit.HEARTS, 9), Birra(Suit.HEARTS, 10), - Birra(Suit.HEARTS, 'J'), - CatBalou(Suit.HEARTS, 'K'), + Birra(Suit.HEARTS, "J"), + CatBalou(Suit.HEARTS, "K"), CatBalou(Suit.DIAMONDS, 9), CatBalou(Suit.DIAMONDS, 10), - CatBalou(Suit.DIAMONDS, 'J'), + CatBalou(Suit.DIAMONDS, "J"), Diligenza(Suit.SPADES, 9), Diligenza(Suit.SPADES, 9), - Duello(Suit.DIAMONDS, 'Q'), - Duello(Suit.SPADES, 'J'), + Duello(Suit.DIAMONDS, "Q"), + Duello(Suit.SPADES, "J"), Duello(Suit.CLUBS, 8), Emporio(Suit.CLUBS, 9), - Emporio(Suit.SPADES, 'Q'), + Emporio(Suit.SPADES, "Q"), Gatling(Suit.HEARTS, 10), - Indiani(Suit.DIAMONDS, 'K'), - Indiani(Suit.DIAMONDS, 'A'), + Indiani(Suit.DIAMONDS, "K"), + Indiani(Suit.DIAMONDS, "A"), Mancato(Suit.CLUBS, 10), - Mancato(Suit.CLUBS, 'J'), - Mancato(Suit.CLUBS, 'Q'), - Mancato(Suit.CLUBS, 'K'), - Mancato(Suit.CLUBS, 'A'), + Mancato(Suit.CLUBS, "J"), + Mancato(Suit.CLUBS, "Q"), + Mancato(Suit.CLUBS, "K"), + Mancato(Suit.CLUBS, "A"), Mancato(Suit.SPADES, 2), Mancato(Suit.SPADES, 3), Mancato(Suit.SPADES, 4), @@ -523,16 +637,15 @@ def get_starting_deck(expansions:List[str]) -> List[Card]: Mancato(Suit.SPADES, 6), Mancato(Suit.SPADES, 7), Mancato(Suit.SPADES, 8), - Panico(Suit.HEARTS, 'J'), - Panico(Suit.HEARTS, 'Q'), - Panico(Suit.HEARTS, 'A'), + Panico(Suit.HEARTS, "J"), + Panico(Suit.HEARTS, "Q"), + Panico(Suit.HEARTS, "A"), Panico(Suit.DIAMONDS, 8), Saloon(Suit.HEARTS, 5), WellsFargo(Suit.HEARTS, 3), ] - if 'dodge_city' in expansions: + if "dodge_city" in expansions: base_cards.extend(DodgeCity.get_cards()) - if 'the_valley_of_shadows' in expansions: + if "the_valley_of_shadows" in expansions: base_cards.extend(TheValleyOfShadows.get_cards()) return base_cards - diff --git a/backend/bang/deck.py b/backend/bang/deck.py index 0cd14ab..f5c3e22 100644 --- a/backend/bang/deck.py +++ b/backend/bang/deck.py @@ -10,8 +10,12 @@ from globals import G if TYPE_CHECKING: from bang.game import Game + + class Deck: - def __init__(self, game: 'Game'): + """Class that handles all deck dealing information""" + + def __init__(self, game: "Game"): super().__init__() self.cards: List[cs.Card] = cs.get_starting_deck(game.expansions) self.mancato_cards: List[str] = [] @@ -30,45 +34,74 @@ class Deck: self.all_cards_str.append(c.name) self.game = game self.event_cards: List[ce.CardEvent] = [] + self.event_cards_wildwestshow: List[ce.CardEvent] = [] endgame_cards: List[ce.CardEvent] = [] - if 'fistful_of_cards' in game.expansions: + if "fistful_of_cards" in game.expansions: self.event_cards.extend(ce.get_all_events(game.rng)) endgame_cards.append(ce.get_endgame_card()) - if 'high_noon' in game.expansions: + if "high_noon" in game.expansions: self.event_cards.extend(ceh.get_all_events(game.rng)) endgame_cards.append(ceh.get_endgame_card()) - if 'wild_west_show' in game.expansions: - self.event_cards.extend(cew.get_all_events(game.rng)) - endgame_cards.append(cew.get_endgame_card()) + if "wild_west_show" in game.expansions: + self.event_cards_wildwestshow.extend(cew.get_all_events(game.rng)) + game.rng.shuffle(self.event_cards_wildwestshow) + self.event_cards_wildwestshow.insert(0, None) + self.event_cards_wildwestshow.append(cew.get_endgame_card()) if len(self.event_cards) > 0: game.rng.shuffle(self.event_cards) self.event_cards = self.event_cards[:12] self.event_cards.insert(0, None) - self.event_cards.insert(0, None) # 2 perchè iniziale, e primo flip dallo sceriffo + self.event_cards.insert( + 0, None + ) # 2 perchè iniziale, e primo flip dallo sceriffo self.event_cards.append(game.rng.choice(endgame_cards)) game.rng.shuffle(self.cards) self.shop_deck: List[grc.ShopCard] = [] self.shop_cards: List[grc.ShopCard] = [] - if 'gold_rush' in game.expansions: + if "gold_rush" in game.expansions: self.shop_cards = [None, None, None] self.shop_deck = grc.get_cards() game.rng.shuffle(self.shop_deck) self.fill_gold_rush_shop() self.scrap_pile: List[cs.Card] = [] - print(f'Deck initialized with {len(self.cards)} cards') + print(f"Deck initialized with {len(self.cards)} cards") def flip_event(self): - if len(self.event_cards) > 0 and not (isinstance(self.event_cards[0], ce.PerUnPugnoDiCarte) or isinstance(self.event_cards[0], ceh.MezzogiornoDiFuoco) or isinstance(self.event_cards[0], cew.WildWestShow)): + """Flip event for regular Sheriff turn (High Noon, Fistful of Cards)""" + if len(self.event_cards) > 0 and not ( + isinstance(self.event_cards[0], ce.PerUnPugnoDiCarte) + or isinstance(self.event_cards[0], ceh.MezzogiornoDiFuoco) + ): self.event_cards.append(self.event_cards.pop(0)) + if len(self.event_cards) > 0 and self.event_cards[0] is not None: + self.event_cards[0].on_flipped(self.game) self.game.notify_event_card() self.game.notify_all() + def flip_wildwestshow(self): + """Flip event for Wild West Show only""" + if len(self.event_cards_wildwestshow) > 0 and not isinstance( + self.event_cards_wildwestshow[0], cew.WildWestShow + ): + self.event_cards_wildwestshow.append(self.event_cards_wildwestshow.pop(0)) + if ( + len(self.event_cards_wildwestshow) > 0 + and self.event_cards_wildwestshow[0] is not None + ): + self.event_cards_wildwestshow[0].on_flipped(self.game) + self.game.notify_event_card_wildwestshow() + self.game.notify_all() + def fill_gold_rush_shop(self): + """ + As gold_rush shop cards are stored in a fixed 3 space array, + this function replaces the None values with new cards. + """ if not any((c is None for c in self.shop_cards)): return for i in range(3): if self.shop_cards[i] is None: - print(f'replacing gr-card {i}') + print(f"replacing gr-card {i}") self.shop_cards[i] = self.shop_deck.pop(0) self.shop_cards[i].reset_card() self.game.notify_gold_rush_shop() @@ -102,14 +135,22 @@ class Deck: def put_on_top(self, card: cs.Card): self.cards.insert(0, card) - def draw(self, ignore_event = False, player=None) -> cs.Card: - if self.game.check_event(ce.MinieraAbbandonata) and len(self.scrap_pile) > 0 and not ignore_event: + def draw(self, ignore_event=False, player=None) -> cs.Card: + if ( + self.game.check_event(ce.MinieraAbbandonata) + and len(self.scrap_pile) > 0 + and not ignore_event + ): return self.draw_from_scrap_pile() card = self.cards.pop(0) if len(self.cards) == 0: self.reshuffle() if player is not None and self.game.replay_speed > 0: - G.sio.emit('card_drawn', room=self.game.name, data={'player': player.name, 'pile': 'deck'}) + G.sio.emit( + "card_drawn", + room=self.game.name, + data={"player": player.name, "pile": "deck"}, + ) player.hand.append(card) return card @@ -127,15 +168,24 @@ class Deck: else: return self.draw() - def scrap(self, card: cs.Card, ignore_event = False, player=None): - if card.number == 42: return + def scrap(self, card: cs.Card, ignore_event=False, player=None): + if card.number == 42: + return card.reset_card() if self.game.check_event(ce.MinieraAbbandonata) and not ignore_event: self.put_on_top(card) else: self.scrap_pile.append(card) if player is not None and self.game.replay_speed > 0: - G.sio.emit('card_scrapped', room=self.game.name, data={'player': player.name, 'card':card.__dict__, 'pile': 'scrap'}) + G.sio.emit( + "card_scrapped", + room=self.game.name, + data={ + "player": player.name, + "card": card.__dict__, + "pile": "scrap", + }, + ) G.sio.sleep(0.6) self.game.notify_scrap_pile() else: diff --git a/backend/bang/expansions/fistful_of_cards/card_events.py b/backend/bang/expansions/fistful_of_cards/card_events.py index ce7bbf9..cd184cf 100644 --- a/backend/bang/expansions/fistful_of_cards/card_events.py +++ b/backend/bang/expansions/fistful_of_cards/card_events.py @@ -1,106 +1,198 @@ from abc import ABC, abstractmethod import random +import bang.players as players +import bang.roles as r +import bang.cards as cs + +from globals import G + class CardEvent(ABC): + """Base class for all event cards""" + def __init__(self, name, icon): self.name = name self.icon = icon + def on_flipped(self, game): + """Default on flipped event + + Args: + game (Game): the game object + """ + print(f"{game.name}: flip new event {self.name}") + G.sio.emit( + "chat_message", + room=game.name, + data={ + "color": "orange", + "text": f"_flip_event|{self.name}", + }, + ) + return + + def on_clicked(self, game, player): + """Default on clicked event + + Args: + game (Game): the game object + player (Player): the player that clicked the card + """ + print(f"{game.name}: {player.name} clicked event {self.name}") + return + + class Agguato(CardEvent): + """La distanza base tra 2 qualsiasi giocatori è 1 + + The distance between 2 players is always 1""" + def __init__(self): super().__init__("Agguato", "🛁") - #self.desc = "La distanza base tra 2 qualsiasi giocatori è 1" - #self.desc_eng = "The base distance from any 2 players is 1" + class Cecchino(CardEvent): + """Nel proprio turno i giocatori possono + scartare 2 Bang assieme per sparare un bang che necessita 2 mancato (clicca la carta) + + In your turn you can discard 2 Bang together to shoot a bang that needs 2 miss (click the card) + """ + def __init__(self): super().__init__("Cecchino", "👁") - #self.desc = "Nel proprio turno i giocatori possono scartare 2 Bang assieme per sparare un bang che necessita 2 mancato (clicca la carta)" - #self.desc_eng = "During their turn, players can discard 2 Bang! to shoot a bang that requires 2 missed (click the card)" + class DeadMan(CardEvent): + """Al proprio turno il giocatore che è morto per primo torna in vita con 2 vite e 2 carte + + The first player that died returns back to life with 2 hp and 2 cards""" + def __init__(self): super().__init__("Dead Man", "⚰️") - #self.desc = "Al proprio turno il giocatore che è morto per primo torna in vita con 2 vite e 2 carte" - #self.desc_eng = "The first player that died returns back to life with 2 hp and 2 cards" + + def on_flipped(self, game): + game.did_resuscitate_deadman = False + return super().on_flipped(game) + class FratelliDiSangue(CardEvent): + """All'inizio del proprio turno, i giocatori possono perdere 1 vita (tranne l'ultimo) per darla a un altro giocatore""" + def __init__(self): super().__init__("Fratelli Di Sangue", "💉") - #self.desc = "All'inizio del proprio turno, i giocatori possono perdere 1 vita (tranne l'ultimo) per darla a un altro giocatore" - #self.desc_eng = "At the begin of their turn, payers can lose 1 hp (except the last one) to give it to another player" + class IlGiudice(CardEvent): + """Non si possono equipaggiare carte a se stessi o agli altri""" + def __init__(self): super().__init__("Il Giudice", "👨‍⚖️") - #self.desc = "Non si possono equipaggiare carte a se stessi o agli altri" - #self.desc_eng = "You can't equip cards on your or other players" + class Lazo(CardEvent): + """Le carte equipaggiate non hanno effetto""" + def __init__(self): super().__init__("Lazo", "📿") - #self.desc = "Le carte equipaggiate non hanno effetto" - #self.desc_eng = "Cards in the equipment slot do not work" + class LeggeDelWest(CardEvent): + """I giocatori mostrano la seconda carta che pescano e sono obbligati a usarla in quel turno (se possibile)""" + def __init__(self): super().__init__("Legge Del West", "⚖️") - #self.desc = "I giocatori mostrano la seconda carta che pescano e sono obbligati a usarla in quel turno (se possibile)" - #self.desc_eng = "Every player shows the second card that they draw and must use it in that round (if it is possible)" + class LiquoreForte(CardEvent): + """I giocatori possono evitare di pescare per recuperare 1 vita (clicca sulla carta evento per farlo)""" + def __init__(self): super().__init__("Liquore Forte", "🥃") - #self.desc = "I giocatori possono evitare di pescare per recuperare 1 vita (clicca sulla carta evento per farlo)" - #self.desc_eng = "Players can skip drawing to regain 1 HP (click on the event card to use)" + class MinieraAbbandonata(CardEvent): + """I giocatori pescano dagli scarti nella loro fase 1 e scartano in cima al mazzo nella loro fase 3 (se gli scarti finiscono, è necessario pescare e scartare in cima al mazzo)""" + def __init__(self): super().__init__("Miniera Abbandonata", "⛏") - #self.desc = "I giocatori pescano dagli scarti nella loro fase 1 e scartano in cima al mazzo nella loro fase 3 (se gli scarti finiscono, è necessario pescare e scartare in cima al mazzo)" - #self.desc_eng = "Players draw from the discarded pile in their phase 1 and discard to the top of the deck during phase 3 (if the discaded pile runs out, they must draw and discard on top of the deck)" + class PerUnPugnoDiCarte(CardEvent): + """All'inizio del proprio turno, il giocatore subisce tanti bang quante carte ha in mano""" + def __init__(self): super().__init__("Per Un Pugno Di Carte", "🎴") - #self.desc = "All'inizio del proprio turno, il giocatore subisce tanti bang quante carte ha in mano" - #self.desc_eng = "On the beginning of his turn, the player is target of as many Bang as how many cards he has in his hand" + class Peyote(CardEvent): + """Invece che pescare il giocatore prova a indovinare il colore del seme, se lo indovina aggiunge la carta alla mano e continua provando ad indovinare la carta successiva""" + def __init__(self): super().__init__("Peyote", "🌵") - #self.desc = "Invece che pescare il giocatore prova a indovinare il colore del seme, se lo indovina aggiunge la carta alla mano e continua provando ad indovinare la carta successiva" - #self.desc_eng = "Instead of drawing, the player tries to guess the color of the suit, if he's right he adds the card to the hand and continues trying to guess the next card" + class Ranch(CardEvent): + """Dopo aver pescato il giocatore può scartare quante carte vuole dalla mano e pescarne altrettante dal mazzo""" + def __init__(self): super().__init__("Ranch", "🐮") - #self.desc = "Dopo aver pescato il giocatore può scartare quante carte vuole dalla mano e pescarne altrettante dal mazzo" - #self.desc_eng = "After drawing, the player can discard as many cards as he wants from his hand and draw as many from the deck" + class Rimbalzo(CardEvent): + """Il giocatore di turno può giocare bang contro le carte equipaggiate dagli altri giocatori, se non giocano mancato vengono scartate (clicca la carta evento)""" + def __init__(self): super().__init__("Rimbalzo", "⏮") - #self.desc = "Il giocatore di turno può giocare bang contro le carte equipaggiate dagli altri giocatori, se non giocano mancato vengono scartate (clicca la carta evento)" - #self.desc_eng = "The player can play bang against the cards equipped by the other players, if they do not play miss they are discarded (click the event card)" + + def on_clicked(self, game, player): + super().on_clicked(game, player) + if any((c.name == cs.Bang(0, 0).name for c in player.hand)): + player.available_cards = [ + { + "name": p.name, + "icon": p.role.icon + if (game.initial_players == 3) + else "⭐️" + if isinstance(p.role, r.Sheriff) + else "🤠", + "is_character": True, + "avatar": p.avatar, + "is_player": True, + } + for p in game.get_alive_players() + if len(p.equipment) > 0 and p != player + ] + player.available_cards.append({"icon": "❌", "noDesc": True}) + player.choose_text = "choose_rimbalzo_player" + player.pending_action = players.PendingAction.CHOOSE + player.using_rimbalzo = 1 + player.notify_self() + class RouletteRussa(CardEvent): + """A partire dallo sceriffo, ogni giocatore scarta 1 mancato, il primo che non lo fa perde 2 vite""" + def __init__(self): super().__init__("Roulette Russa", "🇷🇺") - #self.desc = "A partire dallo sceriffo, ogni giocatore scarta 1 mancato, il primo che non lo fa perde 2 vite" - #self.desc_eng = "Starting from the sheriff, every player discards 1 missed, the first one that doesn't loses 2 HP" + # self.desc_eng = "Starting from the sheriff, every player discards 1 missed, the first one that doesn't loses 2 HP" + class Vendetta(CardEvent): + """Alla fine del proprio turno il giocatore estrae dal mazzo, se esce ♥️ gioca un altro turno (ma non estrae di nuovo)""" + def __init__(self): super().__init__("Vendetta", "😤") - #self.desc = "Alla fine del proprio turno il giocatore estrae dal mazzo, se esce ♥️ gioca un altro turno (ma non estrae di nuovo)" - #self.desc_eng = "When ending the turn, the player flips a card from the deck, if it's ♥️ he plays another turn (but he does not flip another card)" + # self.desc_eng = "When ending the turn, the player flips a card from the deck, if it's ♥️ he plays another turn (but he does not flip another card)" + def get_endgame_card(): end_game = PerUnPugnoDiCarte() - end_game.expansion = 'fistful-of-cards' + end_game.expansion = ( # pylint: disable=attribute-defined-outside-init + "fistful-of-cards" + ) return end_game + def get_all_events(rng=random): cards = [ Agguato(), @@ -119,6 +211,8 @@ def get_all_events(rng=random): Vendetta(), ] rng.shuffle(cards) - for c in cards: - c.expansion = 'fistful-of-cards' - return cards \ No newline at end of file + for card in cards: + card.expansion = ( # pylint: disable=attribute-defined-outside-init + "fistful-of-cards" + ) + return cards diff --git a/backend/bang/expansions/high_noon/card_events.py b/backend/bang/expansions/high_noon/card_events.py index 5826827..493ab8a 100644 --- a/backend/bang/expansions/high_noon/card_events.py +++ b/backend/bang/expansions/high_noon/card_events.py @@ -1,119 +1,184 @@ import random +from globals import G + from bang.expansions.fistful_of_cards.card_events import CardEvent + class Benedizione(CardEvent): + """Tutte le carte sono considerate di cuori ♥️""" + def __init__(self): super().__init__("Benedizione", "🙏") - #self.desc = "Tutte le carte sono considerate di cuori ♥️" - #self.desc_eng = "All cards are of hearts ♥️" + # self.desc_eng = "All cards are of hearts ♥️" + class Maledizione(CardEvent): + """Tutte le carte sono considerate di picche ♠""" + def __init__(self): super().__init__("Maledizione", "🤬") - #self.desc = "Tutte le carte sono considerate di picche ♠" - #self.desc_eng = "All cards are of spades ♠" + # self.desc_eng = "All cards are of spades ♠" + class Sbornia(CardEvent): + """I personaggi perdono le loro abilità speciali""" + def __init__(self): super().__init__("Sbornia", "🥴") - #self.desc = "I personaggi perdono le loro abilità speciali" - #self.desc_eng = "The characters lose their special abilities" + # self.desc_eng = "The characters lose their special abilities" + class Sete(CardEvent): + """I giocatori pescano 1 carta in meno nella loro fase 1""" + def __init__(self): super().__init__("Sete", "🥵") - #self.desc = "I giocatori pescano 1 carta in meno nella loro fase 1" - #self.desc_eng = "Players only draw 1 card at the start of their turn" + # self.desc_eng = "Players only draw 1 card at the start of their turn" + class IlTreno(CardEvent): + """I giocatori pescano 1 carta extra nella loro fase 1""" + def __init__(self): super().__init__("Il Treno", "🚂") - #self.desc = "I giocatori pescano 1 carta extra nella loro fase 1" - #self.desc_eng = "Players draw 1 extra card" + # self.desc_eng = "Players draw 1 extra card" + class IlReverendo(CardEvent): + """Non si possono giocare le carte Birra""" + def __init__(self): super().__init__("Il Reverendo", "⛪️") - #self.desc = "Non si possono giocare le carte Birra" - #self.desc_eng = "Beers can't be played" + # self.desc_eng = "Beers can't be played" + class IlDottore(CardEvent): + """Il/i giocatore/i con meno vite ne recupera/no una""" + def __init__(self): super().__init__("Il Dottore", "👨‍⚕️") - #self.desc = "Il/i giocatore/i con meno vite ne recupera/no una" - #self.desc_eng = "The player with the least amount of HP gets healed 1" + # self.desc_eng = "The player with the least amount of HP gets healed 1" + + def on_flipped(self, game): + super().on_flipped(game) + most_hurt = [ + p.lives for p in game.players if p.lives > 0 and p.max_lives > p.lives + ] + if len(most_hurt) > 0: + hurt_players = [p for p in game.players if p.lives == min(most_hurt)] + for p in hurt_players: + if p.lives != p.max_lives: + p.lives += 1 + G.sio.emit( + "chat_message", + room=game.name, + data=f"_doctor_heal|{p.name}", + ) + p.notify_self() + return + class Sermone(CardEvent): + """I giocatori non possono giocare Bang! durante il loro turno""" + def __init__(self): super().__init__("Sermone", "✝️") - #self.desc = "I giocatori non possono giocare Bang! durante il loro turno" - #self.desc_eng = "Players can't play Bang! during their turn" + # self.desc_eng = "Players can't play Bang! during their turn" + class Sparatoria(CardEvent): + """Il limite di Bang! per turno è 2 invece che 1""" + def __init__(self): super().__init__("Sparatoria", "🔫🔫") - #self.desc = "Il limite di Bang! per turno è 2 invece che 1" - #self.desc_eng = "The turn Bang! limit is 2" + # self.desc_eng = "The turn Bang! limit is 2" + class CorsaAllOro(CardEvent): + """Si gioca per un intero giro in senso antiorario, tuttavia gli effetti delle carte rimangono invariati""" + def __init__(self): super().__init__("Corsa All Oro", "🌟") - #self.desc = "Si gioca per un intero giro in senso antiorario, tuttavia gli effetti delle carte rimangono invariati" - #self.desc_eng = "Turns are played counter clockwise" + # self.desc_eng = "Turns are played counter clockwise" + class IDalton(CardEvent): + """Chi ha carte blu in gioco ne scarta 1 a sua scelta""" + def __init__(self): super().__init__("I Dalton", "🙇‍♂️") - #self.desc = "Chi ha carte blu in gioco ne scarta 1 a sua scelta" - #self.desc_eng = "Players that have blue cards equipped, discard 1 of those card of their choice" + # self.desc_eng = "Players that have blue cards equipped, discard 1 of those card of their choice" + + def on_flipped(self, game): + game.waiting_for = 0 + game.ready_count = 0 + game.dalton_on = True + for p in game.players: + if p.get_dalton(): + game.waiting_for += 1 + p.notify_self() + if game.waiting_for != 0: + return + game.dalton_on = False + return super().on_flipped(game) + class Manette(CardEvent): + """Dopo aver pescato in fase 1, il giocatore di turno dichiara un seme: potrà usare solamente carte di quel seme nel suo turno""" + def __init__(self): super().__init__("Manette", "🔗") - #self.desc = "Dopo aver pescato in fase 1, il giocatore di turno dichiara un seme: potrà usare solamente carte di quel seme nel suo turno" - #self.desc_eng = "After drawing in phase 1, the player declares a suit. He will be able to use only cards of that suit for that turn" + # self.desc_eng = "After drawing in phase 1, the player declares a suit. He will be able to use only cards of that suit for that turn" + class NuovaIdentita(CardEvent): + """All'inizio del proprio turno, ogni giocatore potrà decidere se sostituire il suo personaggio attuale con quello era stato proposto ad inizio partita, se lo fa riparte con 2 punti vita""" + def __init__(self): super().__init__("Nuova Identita", "🕶") - #self.desc = "All'inizio del proprio turno, ogni giocatore potrà decidere se sostituire il suo personaggio attuale con quello era stato proposto ad inizio partita, se lo fa riparte con 2 punti vita" - #self.desc_eng = "At the beginning of their turn, each player can choose to change its character with the other shown at the game start. If he does so he restarts from 2 HP." + # self.desc_eng = "At the beginning of their turn, each player can choose to change its character with the other shown at the game start. If he does so he restarts from 2 HP." + class CittaFantasma(CardEvent): + """Tutti i giocatori morti tornano in vita al proprio turno, non possono morire e pescano 3 carte invece che 2. Quando terminano il turno tornano morti.""" + def __init__(self): super().__init__("Città Fantasma", "👻") - #self.desc = "Tutti i giocatori morti tornano in vita al proprio turno, non possono morire e pescano 3 carte invece che 2. Quando terminano il turno tornano morti." - #self.desc_eng = "All dead players come back to life in their turn, they can't die and draw 3 cards instead of 2. When they end their turn the die." + # self.desc_eng = "All dead players come back to life in their turn, they can't die and draw 3 cards instead of 2. When they end their turn the die." + class MezzogiornoDiFuoco(CardEvent): + """Ogni giocatore perde 1 punto vita all'inizio del turno""" + def __init__(self): super().__init__("Mezzogiorno di Fuoco", "🔥") - #self.desc = "Ogni giocatore perde 1 punto vita all'inizio del turno" - #self.desc_eng = "Every player loses 1 HP when their turn starts" + # self.desc_eng = "Every player loses 1 HP when their turn starts" + def get_endgame_card(): end_game = MezzogiornoDiFuoco() - end_game.expansion = 'high-noon' + end_game.expansion = "high-noon" # pylint: disable=attribute-defined-outside-init return end_game + def get_all_events(rng=random): cards = [ - Benedizione(), - Maledizione(), - CittaFantasma(), - CorsaAllOro(), - IDalton(), - IlDottore(), - IlReverendo(), - IlTreno(), - Sbornia(), - Sermone(), - Sete(), - Sparatoria(), - Manette(), - NuovaIdentita(), + Benedizione(), + Maledizione(), + CittaFantasma(), + CorsaAllOro(), + IDalton(), + IlDottore(), + IlReverendo(), + IlTreno(), + Sbornia(), + Sermone(), + Sete(), + Sparatoria(), + Manette(), + NuovaIdentita(), ] rng.shuffle(cards) for c in cards: - c.expansion = 'high-noon' - return cards \ No newline at end of file + c.expansion = "high-noon" # pylint: disable=attribute-defined-outside-init + return cards diff --git a/backend/bang/expansions/wild_west_show/card_events.py b/backend/bang/expansions/wild_west_show/card_events.py index 666f1e1..20387aa 100644 --- a/backend/bang/expansions/wild_west_show/card_events.py +++ b/backend/bang/expansions/wild_west_show/card_events.py @@ -1,4 +1,8 @@ import random +import bang.cards as cs +import bang.roles as roles +import bang.players as players +from globals import G from bang.expansions.fistful_of_cards.card_events import CardEvent @@ -9,74 +13,143 @@ from bang.expansions.fistful_of_cards.card_events import CardEvent # # I giocatori non possono parlare (ma possono gesticolare, mugugnare...). Chi parla perde 1 punto vita. # # NOT IMPLEMENTED + class Camposanto(CardEvent): """ All'inizio del proprio turno, ogni giocatore eliminato torna in gioco con 1 punto vita. Pesca il ruolo a caso fra quelli dei giocatori eliminati. """ + def __init__(self): super().__init__("Camposanto", "⚰") + class DarlingValentine(CardEvent): """ All'inizio del proprio turno, ogni giocatore scarta le carte in mano e ne pesca dal mazzo altrettante. """ + def __init__(self): super().__init__("Darling Valentine", "💋") + class DorothyRage(CardEvent): """ Nel proprio turno, ogni giocatore può obbligarne un altro a giocare una carta. """ + def __init__(self): super().__init__("Dorothy Rage", "👩‍⚖️") + class HelenaZontero(CardEvent): """ Quando Helena entra in gioco, "estrai!": se esce Cuori o Quadri, rimescola i ruoli attivi tranne lo Sceriffo, e ridistribuiscili a caso. """ + def __init__(self): super().__init__("Helena Zontero", "💞") + def on_flipped(self, game): + c = game.deck.pick_and_scrap() + G.sio.emit( + "chat_message", + room=game.name, + data=f"_flipped|Helena Zontero|{c.name}|{c.num_suit()}", + ) + if c.check_suit(game, [cs.Suit.HEARTS, cs.Suit.DIAMONDS]): + G.sio.emit( + "chat_message", + room=game.name, + data=f"_swapped_roles|Helena Zontero|{c.name}|{c.num_suit()}", + ) + pls = [p for p in game.players if not isinstance(p.role, roles.Sheriff)] + newroles = [p.role for p in pls] + random.shuffle(newroles) + for p in pls: + p.set_role(newroles.pop(random.randint(0, len(newroles) - 1))) + + return super().on_flipped(game) + + class LadyRosaDelTexas(CardEvent): """ Nel proprio turno, ogni giocatore può scambiarsi di posto con quello alla sua destra, il quale salta il prossimo turno. """ + def __init__(self): super().__init__("Lady Rosa del Texas", "🩰") + def on_clicked(self, game, player): + super().on_clicked(game, player) + nextp = game.next_player() + i, j = game.players_map[player.name], game.players_map[nextp.name] + game.players[i], game.players[j] = nextp, player + game.players_map[player.name], game.players_map[nextp.name] = j, i + game.turn = j + game.notify_all() + + class MissSusanna(CardEvent): """ Nel proprio turno ogni giocatore deve giocare almeno 3 carte. Se non lo fa, perde 1 punto vita. """ + def __init__(self): super().__init__("Miss Susanna", "👩‍🎤") + class RegolamentoDiConti(CardEvent): """ Tutte le carte possono essere giocate come se fossero BANG!. Le carte BANG! come se fossero Mancato! """ + def __init__(self): super().__init__("Regolamento di Conti", "🤠") + def on_clicked(self, game, player): + super().on_clicked(game, player) + if len(player.hand) > 0: + if not player.has_played_bang and any( + ( + player.get_sight() >= p["dist"] + for p in game.get_visible_players(player) + ) + ): + player.available_cards = player.hand.copy() + player.pending_action = players.PendingAction.CHOOSE + player.choose_text = "choose_play_as_bang" + player.notify_self() + + class Sacagaway(CardEvent): """ Tutti i giocatori giocano a carte scoperte (tranne il ruolo!). """ + def __init__(self): super().__init__("Sacagaway", "🌄") + class WildWestShow(CardEvent): """ L'obiettivo di ogni giocatore diventa: "Rimani l'ultimo in gioco!" """ + def __init__(self): super().__init__("Wild West Show", "🎪") + def on_flipped(self, game): + for player in game.players: + player.set_role(roles.Renegade()) + return super().on_flipped(game) + + def get_endgame_card(): end_game = WildWestShow() - end_game.expansion = 'wild-west-show' + end_game.expansion = "wild-west-show" return end_game + def get_all_events(rng=random): cards = [ Camposanto(), @@ -90,5 +163,5 @@ def get_all_events(rng=random): ] rng.shuffle(cards) for c in cards: - c.expansion = 'wild-west-show' - return cards \ No newline at end of file + c.expansion = "wild-west-show" + return cards diff --git a/backend/bang/game.py b/backend/bang/game.py index 6c1d739..dfd68ca 100644 --- a/backend/bang/game.py +++ b/backend/bang/game.py @@ -21,33 +21,56 @@ from globals import G debug_commands = [ - {'cmd':'/debug', 'help':'Toggles the debug mode'}, - {'cmd':'/set_chars', 'help':'Set how many characters to distribute - sample /set_chars 3'}, - {'cmd':'/suicide', 'help':'Kills you'}, - {'cmd':'/nextevent', 'help':'Flip the next event card'}, - {'cmd':'/notify', 'help':'Send a message to a player - sample /notify player hi!'}, - {'cmd':'/show_cards', 'help':'View the hand of another - sample /show_cards player'}, - {'cmd':'/ddc', 'help':'Destroy all cards - sample /ddc player'}, - {'cmd':'/dsh', 'help':'Set health - sample /dsh player'}, + {"cmd": "/debug", "help": "Toggles the debug mode"}, + { + "cmd": "/set_chars", + "help": "Set how many characters to distribute - sample /set_chars 3", + }, + {"cmd": "/suicide", "help": "Kills you"}, + {"cmd": "/nextevent", "help": "Flip the next event card"}, + { + "cmd": "/notify", + "help": "Send a message to a player - sample /notify player hi!", + }, + { + "cmd": "/show_cards", + "help": "View the hand of another - sample /show_cards player", + }, + {"cmd": "/ddc", "help": "Destroy all cards - sample /ddc player"}, + {"cmd": "/dsh", "help": "Set health - sample /dsh player"}, # {'cmd':'/togglebot', 'help':''}, - {'cmd':'/cancelgame', 'help':'Stops the current game'}, - {'cmd':'/startgame', 'help':'Force starts the game'}, - {'cmd':'/setbotspeed', 'help':'Changes the bot response time - sample /setbotspeed 0.5'}, + {"cmd": "/cancelgame", "help": "Stops the current game"}, + {"cmd": "/startgame", "help": "Force starts the game"}, + { + "cmd": "/setbotspeed", + "help": "Changes the bot response time - sample /setbotspeed 0.5", + }, # {'cmd':'/addex', 'help':''}, - {'cmd':'/setcharacter', 'help':'Changes your current character - sample /setcharacter Willy The Kid'}, - {'cmd':'/setevent', 'help':'Changes the event deck - sample /setevent 0 Manette'}, - {'cmd':'/removecard', 'help':'Remove a card from hand/equip - sample /removecard 0'}, - {'cmd':'/getcard', 'help':'Get a brand new card - sample /getcard Birra'}, - {'cmd':'/meinfo', 'help':'Get player data'}, - {'cmd':'/gameinfo', 'help':'Get game data'}, - {'cmd':'/playerinfo', 'help':'Get player data - sample /playerinfo player'}, - {'cmd':'/cardinfo', 'help':'Get card data - sample /cardinfo handindex'}, - {'cmd':'/mebot', 'help':'Toggles bot mode'}, - {'cmd':'/getnuggets', 'help':'Adds nuggets to yourself - sample /getnuggets 5'}, - {'cmd':'/startwithseed', 'help':'start the game with custom seed'}, - {'cmd':'/getset', 'help':'get extension set of cards sample - /get valley', 'admin':True}, + { + "cmd": "/setcharacter", + "help": "Changes your current character - sample /setcharacter Willy The Kid", + }, + {"cmd": "/setevent", "help": "Changes the event deck - sample /setevent 0 Manette"}, + { + "cmd": "/removecard", + "help": "Remove a card from hand/equip - sample /removecard 0", + }, + {"cmd": "/getcard", "help": "Get a brand new card - sample /getcard Birra"}, + {"cmd": "/meinfo", "help": "Get player data"}, + {"cmd": "/gameinfo", "help": "Get game data"}, + {"cmd": "/playerinfo", "help": "Get player data - sample /playerinfo player"}, + {"cmd": "/cardinfo", "help": "Get card data - sample /cardinfo handindex"}, + {"cmd": "/mebot", "help": "Toggles bot mode"}, + {"cmd": "/getnuggets", "help": "Adds nuggets to yourself - sample /getnuggets 5"}, + {"cmd": "/startwithseed", "help": "start the game with custom seed"}, + { + "cmd": "/getset", + "help": "get extension set of cards sample - /get valley", + "admin": True, + }, ] + class Game: def __init__(self, name): super().__init__() @@ -60,9 +83,16 @@ class Game: self.ready_count = 0 self.waiting_for = 0 self.initial_players = 0 - self.password = '' + self.password = "" self.expansions: List[str] = [] - self.available_expansions = ['dodge_city', 'fistful_of_cards', 'high_noon', 'gold_rush', 'the_valley_of_shadows', 'wild_west_show'] + self.available_expansions = [ + "dodge_city", + "fistful_of_cards", + "high_noon", + "gold_rush", + "the_valley_of_shadows", + "wild_west_show", + ] self.shutting_down = False self.is_competitive = False self.disconnect_bot = True @@ -77,8 +107,8 @@ class Game: self.pending_winners: List[pl.Player] = [] self.someone_won = False self.attack_in_progress = False - self.characters_to_distribute = 2 # personaggi da dare a inizio partita - self.debug = self.name == 'debug' + self.characters_to_distribute = 2 # personaggi da dare a inizio partita + self.debug = self.name == "debug" self.dead_roles: List[roles.Role] = [] self.is_changing_pwd = False self.is_hidden = False @@ -94,12 +124,12 @@ class Game: def reset(self): for p in self.players: - if (p.sid == p.name): + if p.sid == p.name: p.is_bot = True - if (self.is_replay): + if self.is_replay: self.is_replay = False - print('replay ended') - print(f'{self.name}: resetting lobby') + print("replay ended") + print(f"{self.name}: resetting lobby") self.players.extend(self.spectators) self.spectators = [] for bot in [p for p in self.players if p.is_bot]: @@ -107,8 +137,8 @@ class Game: bot.is_bot = False else: bot.game = None - self.players = [p for p in self.players if not p.is_bot and p.sid != 'a_replay'] - print(f'{self.name}: players: {self.players}') + self.players = [p for p in self.players if not p.is_bot and p.sid != "a_replay"] + print(f"{self.name}: players: {self.players}") self.started = False self.is_handling_death = False self.waiting_for = 0 @@ -122,59 +152,65 @@ class Game: eventlet.sleep(0.5) self.notify_room() - def replay(self, log, speed=1.0, fast_forward = -1): + def replay(self, log, speed=1.0, fast_forward=-1): self.players = [] self.is_hidden = True self.is_replay = True self.replay_speed = speed - for i in range(len(log)-1): - print('replay:', i, 'of', len(log)-3, '->', log[i]) + for i in range(len(log) - 1): + print("replay:", i, "of", len(log) - 3, "->", log[i]) if len(self.spectators) == 0: break - if (log[i] == "@@@"): + if log[i] == "@@@": eventlet.sleep(10) if self.is_replay: self.reset() return - cmd = log[i].split(';') - if cmd[1] == 'players': - self.expansions = json.loads(cmd[4].replace("'",'"')) + cmd = log[i].split(";") + if cmd[1] == "players": + self.expansions = json.loads(cmd[4].replace("'", '"')) self.is_competitive = bool(cmd[5]) - pnames = json.loads(cmd[3].replace("'",'"')) + pnames = json.loads(cmd[3].replace("'", '"')) for p in pnames: - self.add_player(pl.Player(p, 'a_replay', bot=False)) + self.add_player(pl.Player(p, "a_replay", bot=False)) continue - if cmd[1] == 'start_game': + if cmd[1] == "start_game": self.start_game(int(cmd[2])) continue player = [p for p in self.players if p.name == cmd[0]][0] - if cmd[1] == 'set_character': - if player.character is not None and isinstance(player.real_character, chd.VeraCuster): - player.set_available_character([p.character for p in self.get_alive_players() if p != player]) + if cmd[1] == "set_character": + if player.character is not None and isinstance( + player.real_character, chd.VeraCuster + ): + player.set_available_character( + [p.character for p in self.get_alive_players() if p != player] + ) player.set_character(cmd[2]) - if cmd[1] == 'draw': + if cmd[1] == "draw": player.draw(cmd[2]) - if cmd[1] == 'pick': + if cmd[1] == "pick": player.pick() - if cmd[1] == 'end_turn': + if cmd[1] == "end_turn": player.end_turn() - if cmd[1] == 'play_card': + if cmd[1] == "play_card": data = json.loads(cmd[2]) if len(data) != 0: - player.play_card(data['index'], data['against'], data['with']) + player.play_card(data["index"], data["against"], data["with"]) else: - player.special(data) #TODO: remove this, is only for the typo in the log - if cmd[1] == 'respond': + player.special( + data + ) # TODO: remove this, is only for the typo in the log + if cmd[1] == "respond": player.respond(int(cmd[2])) - if cmd[1] == 'choose': + if cmd[1] == "choose": player.choose(int(cmd[2])) - if cmd[1] == 'scrap': + if cmd[1] == "scrap": player.scrap(int(cmd[2])) - if cmd[1] == 'special': + if cmd[1] == "special": player.special(json.loads(cmd[2])) - if cmd[1] == 'gold_rush_discard': + if cmd[1] == "gold_rush_discard": player.gold_rush_discard() - if cmd[1] == 'buy_gold_rush_card': + if cmd[1] == "buy_gold_rush_card": player.buy_gold_rush_card(int(cmd[2])) # if cmd[1] == 'chat_message': # chat_message(None, cmd[2], player) @@ -189,27 +225,47 @@ class Game: def notify_room(self, sid=None): if any((p.character is None for p in self.players)) or sid: - G.sio.emit('room', room=self.name if not sid else sid, data={ - 'name': self.name, - 'started': self.started, - 'players': [{'name':p.name, 'ready': p.character is not None, 'is_bot': p.is_bot, 'avatar': p.avatar} for p in self.players], - 'password': self.password, - 'is_competitive': self.is_competitive, - 'disconnect_bot': self.disconnect_bot, - 'expansions': self.expansions, - 'available_expansions': self.available_expansions, - 'is_replay': self.is_replay, - }) - G.sio.emit('debug', room=self.name, data=self.debug) + G.sio.emit( + "room", + room=self.name if not sid else sid, + data={ + "name": self.name, + "started": self.started, + "players": [ + { + "name": p.name, + "ready": p.character is not None, + "is_bot": p.is_bot, + "avatar": p.avatar, + } + for p in self.players + ], + "password": self.password, + "is_competitive": self.is_competitive, + "disconnect_bot": self.disconnect_bot, + "expansions": self.expansions, + "available_expansions": self.available_expansions, + "is_replay": self.is_replay, + }, + ) + G.sio.emit("debug", room=self.name, data=self.debug) if self.debug: - G.sio.emit('commands', room=self.name, data=[x for x in debug_commands if 'admin' not in x]) + G.sio.emit( + "commands", + room=self.name, + data=[x for x in debug_commands if "admin" not in x], + ) else: - G.sio.emit('commands', room=self.name, data=[{'cmd':'/debug', 'help':'Toggles the debug mode'}]) - G.sio.emit('spectators', room=self.name, data=len(self.spectators)) + G.sio.emit( + "commands", + room=self.name, + data=[{"cmd": "/debug", "help": "Toggles the debug mode"}], + ) + G.sio.emit("spectators", room=self.name, data=len(self.spectators)) def toggle_expansion(self, expansion_name): if not self.started: - print(f'{self.name}: toggling', expansion_name) + print(f"{self.name}: toggling", expansion_name) if expansion_name in self.expansions: self.expansions.remove(expansion_name) else: @@ -225,8 +281,11 @@ class Game: self.notify_room() def feature_flags(self): - if 'the_valley_of_shadows' not in self.expansions and 'the_valley_of_shadows' not in self.available_expansions : - self.available_expansions.append('the_valley_of_shadows') + if ( + "the_valley_of_shadows" not in self.expansions + and "the_valley_of_shadows" not in self.available_expansions + ): + self.available_expansions.append("the_valley_of_shadows") self.notify_room() def add_player(self, player: pl.Player): @@ -235,24 +294,26 @@ class Game: if player in self.players or len(self.players) >= 10: return if len(self.players) > 7: - if 'dodge_city' not in self.expansions: - self.expansions.append('dodge_city') + if "dodge_city" not in self.expansions: + self.expansions.append("dodge_city") player.join_game(self) if player.is_admin(): self.feature_flags() self.players.append(player) - print(f'{self.name}: Added player {player.name} to game') + print(f"{self.name}: Added player {player.name} to game") self.notify_room() - G.sio.emit('chat_message', room=self.name, data=f'_joined|{player.name}') + G.sio.emit("chat_message", room=self.name, data=f"_joined|{player.name}") def set_private(self): if not self.is_changing_pwd: self.is_changing_pwd = True - if self.password == '': - self.password = ''.join(random.choice("AEIOUJKZT123456789") for x in range(6)) - print(self.name, 'is now private pwd', self.password) + if self.password == "": + self.password = "".join( + random.choice("AEIOUJKZT123456789") for x in range(6) + ) + print(self.name, "is now private pwd", self.password) else: - self.password = '' + self.password = "" self.notify_room() eventlet.sleep(0.2) self.is_changing_pwd = False @@ -262,40 +323,50 @@ class Game: if not any((p.character is None for p in self.players)): for i in range(len(self.players)): print(self.name, self.players[i].name, self.players[i].character) - G.sio.emit('chat_message', room=self.name, data=f'_choose_character|{self.players[i].name}|{self.players[i].character.name}') + G.sio.emit( + "chat_message", + room=self.name, + data=f"_choose_character|{self.players[i].name}|{self.players[i].character.name}", + ) self.players[i].prepare() for k in range(self.players[i].max_lives): self.deck.draw(player=self.players[i]) self.players[i].notify_self() current_roles = [x.role.name for x in self.players] self.rng.shuffle(current_roles) - cr = '' + cr = "" for x in current_roles: - if (x not in cr): - cr += '|' +x + '|' + str(current_roles.count(x)) - G.sio.emit('chat_message', room=self.name, data=f'_allroles{cr}') + if x not in cr: + cr += "|" + x + "|" + str(current_roles.count(x)) + G.sio.emit("chat_message", room=self.name, data=f"_allroles{cr}") self.play_turn() def choose_characters(self): n = self.characters_to_distribute - char_cards = self.rng.sample(characters.all_characters(self.expansions), len(self.players)*n) - for i in range(len(self.players)): - self.players[i].set_available_character(char_cards[i * n : i * n + n]) + char_cards = self.rng.sample( + characters.all_characters(self.expansions), len(self.players) * n + ) + for i, player in enumerate(self.players): + player.set_available_character(char_cards[i * n : i * n + n]) def start_game(self, SEED=None): if self.started: return - print(f'{self.name}: GAME IS STARING') + print(f"{self.name}: GAME IS STARING") if SEED is None: - import time + import time # pylint: disable=import-outside-toplevel + SEED = int(time.time()) - print(f'{self.name}: SEED IS {SEED}') - self.SEED = SEED - self.rpc_log = [f';players;{len(self.players)};{[p.name for p in self.players]};{self.expansions};{self.is_competitive}', f';start_game;{SEED}'] + print(f"{self.name}: SEED IS {SEED}") + self.SEED = SEED # pylint: disable=invalid-name + self.rpc_log = [ + f";players;{len(self.players)};{[p.name for p in self.players]};{self.expansions};{self.is_competitive}", + f";start_game;{SEED}", + ] self.rng = random.Random(SEED) self.players_map = {c.name: i for i, c in enumerate(self.players)} - G.sio.emit('chat_message', room=self.name, data=f'_starting') - G.sio.emit('start', room=self.name) + G.sio.emit("chat_message", room=self.name, data="_starting") + G.sio.emit("start", room=self.name) self.started = True self.someone_won = False self.attack_in_progress = False @@ -304,42 +375,81 @@ class Game: self.initial_players = len(self.players) self.distribute_roles() self.choose_characters() - if 'gold_rush' in self.expansions: + if "gold_rush" in self.expansions: self.notify_gold_rush_shop() if not self.is_replay: - Metrics.send_metric('start_game', points=[1], tags=([f"exp:{e}" for e in self.expansions] + [f"players:{self.initial_players}", f"competitive:{self.is_competitive}"])) + Metrics.send_metric( + "start_game", + points=[1], + tags=( + [f"exp:{e}" for e in self.expansions] + + [ + f"players:{self.initial_players}", + f"competitive:{self.is_competitive}", + ] + ), + ) def distribute_roles(self): available_roles: List[roles.Role] = [] if len(self.players) == 3: available_roles = [ - roles.Vice('Elimina il Rinnegato 🦅, se non lo elimini tu elimina anche il Fuorilegge', 'Kill the Renegade 🦅, if you are not the one who kills him then kill the Outlaw!'), - roles.Renegade('Elimina il Fuorilegge 🐺, se non lo elimini tu elimina anche il Vice', 'Kill the Outlaw 🐺, if you are not the one who kills him then kill the Vice!'), - roles.Outlaw('Elimina il Vice 🎖, se non lo elimini tu elimina anche il Rinnegato', 'Kill the Vice 🎖, if you are not the one who kills him then kill the Renegade!') + roles.Vice( + "Elimina il Rinnegato 🦅, se non lo elimini tu elimina anche il Fuorilegge", + "Kill the Renegade 🦅, if you are not the one who kills him then kill the Outlaw!", + ), + roles.Renegade( + "Elimina il Fuorilegge 🐺, se non lo elimini tu elimina anche il Vice", + "Kill the Outlaw 🐺, if you are not the one who kills him then kill the Vice!", + ), + roles.Outlaw( + "Elimina il Vice 🎖, se non lo elimini tu elimina anche il Rinnegato", + "Kill the Vice 🎖, if you are not the one who kills him then kill the Renegade!", + ), ] elif len(self.players) >= 4: - available_roles = [roles.Sheriff(), roles.Renegade(), roles.Outlaw(), roles.Outlaw(), roles.Vice(), roles.Outlaw(), roles.Vice(), roles.Renegade(), roles.Outlaw(), roles.Vice(), roles.Outlaw()] - available_roles = available_roles[:len(self.players)] + available_roles = [ + roles.Sheriff(), + roles.Renegade(), + roles.Outlaw(), + roles.Outlaw(), + roles.Vice(), + roles.Outlaw(), + roles.Vice(), + roles.Renegade(), + roles.Outlaw(), + roles.Vice(), + roles.Outlaw(), + ] + available_roles = available_roles[: len(self.players)] else: available_roles = [roles.Renegade(), roles.Renegade()] self.rng.shuffle(available_roles) - for i in range(len(self.players)): - self.players[i].set_role(available_roles[i]) - if isinstance(available_roles[i], roles.Sheriff) or (len(available_roles) == 3 and isinstance(available_roles[i], roles.Vice)): + for i, player in enumerate(self.players): + player.set_role(available_roles[i]) + if isinstance(available_roles[i], roles.Sheriff) or ( + len(available_roles) == 3 and isinstance(available_roles[i], roles.Vice) + ): if isinstance(available_roles[i], roles.Sheriff): - G.sio.emit('chat_message', room=self.name, data=f'_sheriff|{self.players[i].name}') + G.sio.emit( + "chat_message", + room=self.name, + data=f"_sheriff|{player.name}", + ) self.turn = i - self.players[i].notify_self() + player.notify_self() self.notify_event_card() + if "wild_west_show" in self.expansions: + self.notify_event_card_wildwestshow() - def discard_others(self, attacker: pl.Player, card_name:str=None): + def discard_others(self, attacker: pl.Player, card_name: str = None): self.attack_in_progress = True attacker.pending_action = pl.PendingAction.WAIT attacker.notify_self() self.waiting_for = 0 self.ready_count = 0 for p in self.get_alive_players(): - if len(p.hand) > 0 and (p != attacker or card_name == 'Tornado'): + if len(p.hand) > 0 and (p != attacker or card_name == "Tornado"): if p.get_discarded(attacker=attacker, card_name=card_name): self.waiting_for += 1 p.notify_self() @@ -347,10 +457,10 @@ class Game: attacker.pending_action = pl.PendingAction.PLAY attacker.notify_self() self.attack_in_progress = False - elif card_name == 'Poker': + elif card_name == "Poker": self.poker_on = True - def attack_others(self, attacker: pl.Player, card_name:str=None): + def attack_others(self, attacker: pl.Player, card_name: str = None): self.attack_in_progress = True attacker.pending_action = pl.PendingAction.WAIT attacker.notify_self() @@ -386,17 +496,34 @@ class Game: if self.pending_winners and not self.someone_won: return self.announces_winners() - def can_card_reach(self, card: cs.Card, player: pl.Player, target:str): + def can_card_reach(self, card: cs.Card, player: pl.Player, target: str): if card and card.range != 0 and card.range < 99: - return not any((True for p in self.get_visible_players(player) if p['name'] == target and p['dist'] > card.range)) + return not any( + ( + True + for p in self.get_visible_players(player) + if p["name"] == target and p["dist"] > card.range + ) + ) return True - def attack(self, attacker: pl.Player, target_username:str, double:bool=False, card_name:str=None, skip_queue:bool=False): + def attack( + self, + attacker: pl.Player, + target_username: str, + double: bool = False, + card_name: str = None, + skip_queue: bool = False, + ): if self.attack_in_progress and not skip_queue: self.attack_queue.append((attacker, target_username, double, card_name)) - print(f'attack in progress, queueing the attack queue len:{len(self.attack_queue)}') + print( + f"attack in progress, queueing the attack queue len:{len(self.attack_queue)}" + ) return - if self.get_player_named(target_username).get_banged(attacker=attacker, double=double, card_name=card_name): + if self.get_player_named(target_username).get_banged( + attacker=attacker, double=double, card_name=card_name + ): self.attack_in_progress = True self.ready_count = 0 self.waiting_for = 1 @@ -406,9 +533,13 @@ class Game: elif not attacker.is_my_turn: self.players[self.turn].pending_action = pl.PendingAction.PLAY - def steal_discard(self, attacker: pl.Player, target_username:str, card:cs.Card): + def steal_discard(self, attacker: pl.Player, target_username: str, card: cs.Card): p = self.get_player_named(target_username) - if p != attacker and p.get_discarded(attacker, card_name=card.name, action='steal' if isinstance(card, cs.Panico) else 'discard'): + if p != attacker and p.get_discarded( + attacker, + card_name=card.name, + action="steal" if isinstance(card, cs.Panico) else "discard", + ): self.ready_count = 0 self.waiting_for = 1 attacker.pending_action = pl.PendingAction.WAIT @@ -418,20 +549,22 @@ class Game: attacker.pending_action = pl.PendingAction.CHOOSE attacker.target_p = target_username if isinstance(card, cs.CatBalou): - attacker.choose_action = 'discard' + attacker.choose_action = "discard" elif isinstance(card, cs.Panico): - attacker.choose_action = 'steal' + attacker.choose_action = "steal" attacker.notify_self() - def rimbalzo(self, attacker: pl.Player, target_username:str, card_index:int): - if self.get_player_named(target_username).get_banged(attacker=attacker, no_dmg=True, card_index=card_index): + def rimbalzo(self, attacker: pl.Player, target_username: str, card_index: int): + if self.get_player_named(target_username).get_banged( + attacker=attacker, no_dmg=True, card_index=card_index + ): self.ready_count = 0 self.waiting_for = 1 attacker.pending_action = pl.PendingAction.WAIT attacker.notify_self() self.get_player_named(target_username).notify_self() - def duel(self, attacker: pl.Player, target_username:str): + def duel(self, attacker: pl.Player, target_username: str): if self.get_player_named(target_username).get_dueled(attacker=attacker): self.ready_count = 0 self.waiting_for = 1 @@ -443,78 +576,111 @@ class Game: pls = self.get_alive_players() self.available_cards = [self.deck.draw(True) for i in range(len(pls))] self.players[self.turn].pending_action = pl.PendingAction.CHOOSE - self.players[self.turn].choose_text = 'choose_card_to_get' + self.players[self.turn].choose_text = "choose_card_to_get" self.players[self.turn].available_cards = self.available_cards - G.sio.emit('emporio', room=self.name, data=json.dumps( - {'name':self.players[self.turn].name,'cards': self.available_cards}, default=lambda o: o.__dict__)) + G.sio.emit( + "emporio", + room=self.name, + data=json.dumps( + {"name": self.players[self.turn].name, "cards": self.available_cards}, + default=lambda o: o.__dict__, + ), + ) self.players[self.turn].notify_self() - def respond_emporio(self, player:pl.Player, i:int): + def respond_emporio(self, player: pl.Player, i: int): card = self.available_cards.pop(i) - G.sio.emit('chat_message', room=self.name, data=f'_choose_emporio|{player.name}|{card.name}') + G.sio.emit( + "chat_message", + room=self.name, + data=f"_choose_emporio|{player.name}|{card.name}", + ) player.hand.append(card) player.available_cards = [] player.pending_action = pl.PendingAction.WAIT player.notify_self() pls = self.get_alive_players() - nextPlayer = pls[(pls.index(self.players[self.turn])+(len(pls)-len(self.available_cards))) % len(pls)] + nextPlayer = pls[ + ( + pls.index(self.players[self.turn]) + + (len(pls) - len(self.available_cards)) + ) + % len(pls) + ] if len(self.available_cards) == 1: nextPlayer.hand.append(self.available_cards.pop()) nextPlayer.notify_self() - G.sio.emit('emporio', room=self.name, data='{"name":"","cards":[]}') + G.sio.emit("emporio", room=self.name, data='{"name":"","cards":[]}') self.players[self.turn].pending_action = pl.PendingAction.PLAY self.players[self.turn].notify_self() elif nextPlayer == self.players[self.turn]: - G.sio.emit('emporio', room=self.name, data='{"name":"","cards":[]}') + G.sio.emit("emporio", room=self.name, data='{"name":"","cards":[]}') self.players[self.turn].pending_action = pl.PendingAction.PLAY self.players[self.turn].notify_self() else: nextPlayer.pending_action = pl.PendingAction.CHOOSE - nextPlayer.choose_text = 'choose_card_to_get' + nextPlayer.choose_text = "choose_card_to_get" nextPlayer.available_cards = self.available_cards - G.sio.emit('emporio', room=self.name, data=json.dumps( - {'name':nextPlayer.name,'cards': self.available_cards}, default=lambda o: o.__dict__)) + G.sio.emit( + "emporio", + room=self.name, + data=json.dumps( + {"name": nextPlayer.name, "cards": self.available_cards}, + default=lambda o: o.__dict__, + ), + ) nextPlayer.notify_self() - def get_player_named(self, name:str, next=False) -> pl.Player: + def get_player_named(self, name: str, next=False) -> pl.Player: if next: - return self.players[(self.players_map[name]+1) % len(self.players)] + return self.players[(self.players_map[name] + 1) % len(self.players)] return self.players[self.players_map[name]] def responders_did_respond_resume_turn(self, did_lose=False): - print(f'{self.name}: did_lose', did_lose) + print(f"{self.name}: did_lose", did_lose) if self.player_bangs > 0 and self.check_event(ce.PerUnPugnoDiCarte): self.player_bangs -= 1 if self.player_bangs >= 1: - print(f'{self.name}: bang again') + print(f"{self.name}: bang again") if self.players[self.turn].get_banged(self.deck.event_cards[0]): self.players[self.turn].notify_self() else: self.responders_did_respond_resume_turn() else: - print(f'{self.name}: ok play turn now') + print(f"{self.name}: ok play turn now") self.player_bangs = 0 self.players[self.turn].play_turn() elif self.is_russian_roulette_on and self.check_event(ce.RouletteRussa): pls = self.get_alive_players() if did_lose: - target_pl = pls[(pls.index(self.players[self.turn]) + self.player_bangs) % len(pls)] - print(f'{self.name}: stop roulette') + target_pl = pls[ + (pls.index(self.players[self.turn]) + self.player_bangs) % len(pls) + ] + print(f"{self.name}: stop roulette") target_pl.lives -= 1 target_pl.heal_if_needed() - if any((isinstance(c, grc.Talismano) for c in target_pl.gold_rush_equipment)): + if any( + ( + isinstance(c, grc.Talismano) + for c in target_pl.gold_rush_equipment + ) + ): target_pl.gold_nuggets += 1 if target_pl.character.check(self, grch.SimeonPicos): target_pl.gold_nuggets += 1 - if any((isinstance(c, grc.Stivali) for c in target_pl.gold_rush_equipment)): + if any( + (isinstance(c, grc.Stivali) for c in target_pl.gold_rush_equipment) + ): self.deck.draw(True, player=target_pl) target_pl.notify_self() self.is_russian_roulette_on = False self.players[self.turn].play_turn() else: self.player_bangs += 1 - target_pl = pls[(pls.index(self.players[self.turn]) + self.player_bangs) % len(pls)] - print(f'{self.name}: next in line {target_pl.name}') + target_pl = pls[ + (pls.index(self.players[self.turn]) + self.player_bangs) % len(pls) + ] + print(f"{self.name}: next in line {target_pl.name}") if target_pl.get_banged(self.deck.event_cards[0]): target_pl.notify_self() else: @@ -530,14 +696,22 @@ class Game: return self.announces_winners() if self.dalton_on: self.dalton_on = False - print(f'{self.name}: notifying {self.players[self.turn].name} about his turn') + print( + f"{self.name}: notifying {self.players[self.turn].name} about his turn" + ) self.players[self.turn].play_turn() - elif self.poker_on and not any(c.number == 1 for c in self.deck.scrap_pile[-tmp:]): + elif self.poker_on and not any( + c.number == 1 for c in self.deck.scrap_pile[-tmp:] + ): self.players[self.turn].pending_action = pl.PendingAction.CHOOSE - self.players[self.turn].choose_text = f'choose_from_poker;{min(2, tmp)}' - self.players[self.turn].available_cards = self.deck.scrap_pile[-tmp:] + self.players[ + self.turn + ].choose_text = f"choose_from_poker;{min(2, tmp)}" + self.players[self.turn].available_cards = self.deck.scrap_pile[ + -tmp: + ] elif self.attack_queue: - print('attack completed, next attack') + print("attack completed, next attack") atk = self.attack_queue.pop(0) self.attack(atk[0], atk[1], atk[2], atk[3], skip_queue=True) elif self.players[self.turn].pending_action == pl.PendingAction.CHOOSE: @@ -548,10 +722,10 @@ class Game: self.players[self.turn].notify_self() def announces_winners(self, winners=None): - if (winners is None): - print(f'{self.name}: WE HAVE A WINNER - pending winners') + if winners is None: + print(f"{self.name}: WE HAVE A WINNER - pending winners") else: - print(f'{self.name}: WE HAVE A WINNER') + print(f"{self.name}: WE HAVE A WINNER") for p in self.players: if winners is None: p.win_status = p in self.pending_winners @@ -560,15 +734,23 @@ class Game: if p.win_status and not (isinstance(p.role, roles.Renegade) and p.is_dead): if not self.someone_won: self.someone_won = True - G.sio.emit('chat_message', room=self.name, data=f'_won|{p.name}|{p.role.name}') + G.sio.emit( + "chat_message", room=self.name, data=f"_won|{p.name}|{p.role.name}" + ) if not self.is_replay: - Metrics.send_metric('player_win', points=[1], tags=[f"char:{p.character.name}", f"role:{p.role.name}"]) + Metrics.send_metric( + "player_win", + points=[1], + tags=[f"char:{p.character.name}", f"role:{p.role.name}"], + ) p.notify_self() - if hasattr(G.sio, 'is_fake'): - print('announces_winners(): Running for tests, you will have to call reset manually!') + if hasattr(G.sio, "is_fake"): + print( + "announces_winners(): Running for tests, you will have to call reset manually!" + ) return for i in range(5): - G.sio.emit('chat_message', room=self.name, data=f'_lobby_reset|{5-i}') + G.sio.emit("chat_message", room=self.name, data=f"_lobby_reset|{5-i}") eventlet.sleep(1) return self.reset() @@ -579,93 +761,89 @@ class Game: def play_turn(self): self.incremental_turn += 1 if not self.is_replay: - Metrics.send_metric('incremental_turn', points=[self.incremental_turn], tags=[f'game:{self.SEED}']) + Metrics.send_metric( + "incremental_turn", + points=[self.incremental_turn], + tags=[f"game:{self.SEED}"], + ) if self.players[self.turn].is_dead: - pl = sorted(self.get_dead_players(), key=lambda x:x.death_turn)[0] - if self.check_event([ce.DeadMan, cew.Camposanto]) and not self.did_resuscitate_deadman and pl == self.players[self.turn]: - print(f'{self.name}: {self.players[self.turn]} is dead, revive') + pl = sorted(self.get_dead_players(), key=lambda x: x.death_turn)[0] + if ( + self.check_event([ce.DeadMan, cew.Camposanto]) + and not self.did_resuscitate_deadman + and pl == self.players[self.turn] + ): + print(f"{self.name}: {self.players[self.turn]} is dead, revive") if self.check_event(ce.DeadMan): self.did_resuscitate_deadman = True pl.lives = 2 elif self.check_event(cew.Camposanto): pl.lives = 1 - pl.set_role = self.dead_roles.pop(random.randint(0, len(self.dead_roles)-1)) + pl.set_role = self.dead_roles.pop( + random.randint(0, len(self.dead_roles) - 1) + ) pl.is_dead = False pl.is_ghost = False self.deck.draw(player=pl) self.deck.draw(player=pl) - if (ghost := next((c for c in pl.equipment if isinstance(c, tvosc.Fantasma)), None)) is not None: + if ( + ghost := next( + (c for c in pl.equipment if isinstance(c, tvosc.Fantasma)), None + ) + ) is not None: self.deck.scrap(ghost) pl.equipment.remove(ghost) pl.notify_self() - elif self.check_event(ceh.CittaFantasma) or self.players[self.turn].is_ghost: - print(f'{self.name}: {self.players[self.turn]} is dead, event ghost') + elif ( + self.check_event(ceh.CittaFantasma) or self.players[self.turn].is_ghost + ): + print(f"{self.name}: {self.players[self.turn]} is dead, event ghost") self.players[self.turn].is_ghost = True else: - print(f'{self.name}: {self.players[self.turn]} is dead, next turn') + print(f"{self.name}: {self.players[self.turn]} is dead, next turn") return self.next_turn() self.player_bangs = 0 - if isinstance(self.players[self.turn].role, roles.Sheriff) or ((self.initial_players == 3 and isinstance(self.players[self.turn].role, roles.Vice) and not self.players[self.turn].is_ghost) or (self.initial_players == 3 and any((p for p in self.players if p.is_dead and p.role.name == 'Vice')) and isinstance(self.players[self.turn].role, roles.Renegade))): + if isinstance(self.players[self.turn].role, roles.Sheriff) or ( + ( + self.initial_players == 3 + and isinstance(self.players[self.turn].role, roles.Vice) + and not self.players[self.turn].is_ghost + ) + or ( + self.initial_players == 3 + and any( + (p for p in self.players if p.is_dead and p.role.name == "Vice") + ) + and isinstance(self.players[self.turn].role, roles.Renegade) + ) + ): self.deck.flip_event() - if len(self.deck.event_cards) > 0 and self.deck.event_cards[0] is not None: - print(f'{self.name}: flip new event {self.deck.event_cards[0].name}') - G.sio.emit('chat_message', room=self.name, data={'color': f'orange','text':f'_flip_event|{self.deck.event_cards[0].name}'}) - if self.check_event(cew.HelenaZontero): - c = self.deck.pick_and_scrap() - G.sio.emit('chat_message', room=self.name, data=f'_flipped|Helena Zontero|{c.name}|{c.num_suit()}') - if c.check_suit(self, [cs.Suit.HEARTS, cs.Suit.DIAMONDS]): - G.sio.emit('chat_message', room=self.name, data=f'_swapped_roles|Helena Zontero|{c.name}|{c.num_suit()}') - pls = [p for p in self.players if not isinstance(p.role, roles.Sheriff)] - newroles = [p.role for p in pls] - random.shuffle(newroles) - for p in pls: - p.set_role(newroles.pop(random.randint(0, len(newroles)-1))) - if self.check_event(ce.DeadMan): - self.did_resuscitate_deadman = False - elif self.check_event(ce.RouletteRussa): + if self.check_event(ce.RouletteRussa): self.is_russian_roulette_on = True if self.players[self.turn].get_banged(self.deck.event_cards[0]): self.players[self.turn].notify_self() else: self.responders_did_respond_resume_turn(did_lose=True) return - elif self.check_event(ceh.IlDottore): - most_hurt = [p.lives for p in self.players if p.lives > 0 and p.max_lives > p.lives] - if len(most_hurt) > 0: - hurt_players = [p for p in self.players if p.lives == min(most_hurt)] - for p in hurt_players: - if p.lives != p.max_lives: - p.lives += 1 - G.sio.emit('chat_message', room=self.name, data=f'_doctor_heal|{p.name}') - p.notify_self() - elif self.check_event(ceh.IDalton): - self.waiting_for = 0 - self.ready_count = 0 - self.dalton_on = True - for p in self.players: - if p.get_dalton(): - self.waiting_for += 1 - p.notify_self() - if self.waiting_for != 0: - return - self.dalton_on = False - elif self.check_event(cew.WildWestShow): - for p in self.players: - p.set_role(roles.Renegade()) - - if self.check_event(ce.PerUnPugnoDiCarte) and len(self.players[self.turn].hand) > 0: + if ( + self.check_event(ce.PerUnPugnoDiCarte) + and len(self.players[self.turn].hand) > 0 + ): self.player_bangs = len(self.players[self.turn].hand) if self.players[self.turn].get_banged(self.deck.event_cards[0]): self.players[self.turn].notify_self() else: self.responders_did_respond_resume_turn() else: - print(f'{self.name}: notifying {self.players[self.turn].name} about his turn') + print( + f"{self.name}: notifying {self.players[self.turn].name} about his turn" + ) self.players[self.turn].play_turn() def next_turn(self): - if self.shutting_down: return - print(f'{self.name}: {self.players[self.turn].name} invoked next turn') + if self.shutting_down: + return + print(f"{self.name}: {self.players[self.turn].name} invoked next turn") if self.pending_winners and not self.someone_won: return self.announces_winners() pls = self.get_alive_players() @@ -680,29 +858,54 @@ class Game: if len(self.deck.event_cards) > 0: room = self.name if sid is None else sid if self.deck.event_cards[0] is not None: - G.sio.emit('event_card', room=room, data=self.deck.event_cards[0].__dict__) + G.sio.emit( + "event_card", room=room, data=self.deck.event_cards[0].__dict__ + ) else: - G.sio.emit('event_card', room=room, data=None) + G.sio.emit("event_card", room=room, data=None) + + def notify_event_card_wildwestshow(self, sid=None): + if len(self.deck.event_cards_wildwestshow) > 0: + room = self.name if sid is None else sid + if self.deck.event_cards_wildwestshow[0] is not None: + G.sio.emit( + "event_card_wildwestshow", + room=room, + data=self.deck.event_cards_wildwestshow[0].__dict__, + ) + else: + G.sio.emit("event_card_wildwestshow", room=room, data=None) def notify_gold_rush_shop(self, sid=None): - if 'gold_rush' in self.expansions and self.deck and self.deck.shop_cards and len(self.deck.shop_cards) > 0: + if ( + "gold_rush" in self.expansions + and self.deck + and self.deck.shop_cards + and len(self.deck.shop_cards) > 0 + ): room = self.name if sid is None else sid - print(f'{self.name}: gold_rush_shop room={room}, data={self.deck.shop_cards}') - G.sio.emit('gold_rush_shop', room=room, data=json.dumps(self.deck.shop_cards, default=lambda o: o.__dict__)) + print( + f"{self.name}: gold_rush_shop room={room}, data={self.deck.shop_cards}" + ) + G.sio.emit( + "gold_rush_shop", + room=room, + data=json.dumps(self.deck.shop_cards, default=lambda o: o.__dict__), + ) def notify_scrap_pile(self, sid=None): - print(f'{self.name}: scrap') + print(f"{self.name}: scrap") room = self.name if sid is None else sid if self.deck.peek_scrap_pile(): - G.sio.emit('scrap', room=room, data=self.deck.peek_scrap_pile().__dict__) + G.sio.emit("scrap", room=room, data=self.deck.peek_scrap_pile().__dict__) else: - G.sio.emit('scrap', room=room, data=None) + G.sio.emit("scrap", room=room, data=None) def handle_disconnect(self, player: pl.Player): - print(f'{self.name}: player {player.name} left the game') + print(f"{self.name}: player {player.name} left the game") if player in self.spectators: self.spectators.remove(player) - G.sio.emit('spectators', room=self.name, data=len(self.spectators)) + G.sio.emit("spectators", room=self.name, data=len(self.spectators)) return False if player.is_bot and not self.started: player.game = None @@ -711,13 +914,13 @@ class Game: if not any((not p.is_bot for p in self.players)): eventlet.sleep(5) if not any((not p.is_bot for p in self.players)): - print(f'{self.name}: no players left in game, shutting down') + print(f"{self.name}: no players left in game, shutting down") self.shutting_down = True self.players = [] self.spectators = [] self.deck = None return True - eventlet.sleep(15) # he may reconnect + eventlet.sleep(15) # he may reconnect if player.is_bot: player.was_player = False if len(player.available_characters) > 0: @@ -727,48 +930,73 @@ class Game: self.player_death(player=player, disconnected=True) # else: # player.lives = 0 - # self.players.remove(player) + # self.players.remove(player) if not any((not p.is_bot for p in self.players)): - print(f'{self.name}: no players left in game, shutting down') + print(f"{self.name}: no players left in game, shutting down") self.shutting_down = True self.players = [] self.spectators = [] self.deck = None return True - else: return False + else: + return False def player_death(self, player: pl.Player, disconnected=False): - if not player in self.players or player.is_ghost: return + if not player in self.players or player.is_ghost: + return self.is_handling_death = True import bang.expansions.dodge_city.characters as chd - print(f'{self.name}: the killer is {player.attacker}') + + print(f"{self.name}: the killer is {player.attacker}") if player.character and player.role: if not self.is_replay: - Metrics.send_metric('player_death', points=[1], tags=[f"char:{player.character.name}", f"role:{player.role.name}"]) - if any((isinstance(c, grc.Ricercato) for c in player.gold_rush_equipment)) and player.attacker and player.attacker in self.players: + Metrics.send_metric( + "player_death", + points=[1], + tags=[f"char:{player.character.name}", f"role:{player.role.name}"], + ) + if ( + any((isinstance(c, grc.Ricercato) for c in player.gold_rush_equipment)) + and player.attacker + and player.attacker in self.players + ): player.attacker.gold_nuggets += 1 self.deck.draw(True, player=player.attacker) self.deck.draw(True, player=player.attacker) player.attacker.notify_self() # se lo sceriffo uccide il proprio vice - if player.attacker and player.attacker in self.players and isinstance(player.attacker.role, roles.Sheriff) and isinstance(player.role, roles.Vice): + if ( + player.attacker + and player.attacker in self.players + and isinstance(player.attacker.role, roles.Sheriff) + and isinstance(player.role, roles.Vice) + ): for i in range(len(player.attacker.hand)): - self.deck.scrap(player.attacker.hand.pop(), True, player=player.attacker) + self.deck.scrap( + player.attacker.hand.pop(), True, player=player.attacker + ) for i in range(len(player.attacker.equipment)): - self.deck.scrap(player.attacker.equipment.pop(), True, player=player.attacker) + self.deck.scrap( + player.attacker.equipment.pop(), True, player=player.attacker + ) for i in range(len(player.attacker.gold_rush_equipment)): self.deck.shop_deck.append(player.attacker.gold_rush_equipment.pop()) player.attacker.notify_self() - elif player.attacker and player.attacker in self.players and (isinstance(player.role, roles.Outlaw) or self.initial_players == 3): + elif ( + player.attacker + and player.attacker in self.players + and (isinstance(player.role, roles.Outlaw) or self.initial_players == 3) + ): for i in range(3): self.deck.draw(True, player=player.attacker) player.attacker.notify_self() - print(f'{self.name}: player {player.name} died') + print(f"{self.name}: player {player.name} died") if self.waiting_for > 0 and player.pending_action == pl.PendingAction.RESPOND: self.responders_did_respond_resume_turn() player.pending_action = pl.PendingAction.WAIT - if player.is_dead: return + if player.is_dead: + return if not self.started: self.players.remove(player) elif disconnected: @@ -783,42 +1011,75 @@ class Game: # self.dead_players.append(corpse) self.notify_room() self.dead_roles.append(player.role) - G.sio.emit('chat_message', room=self.name, data=f'_died|{player.name}') + G.sio.emit("chat_message", room=self.name, data=f"_died|{player.name}") for p in self.players: if not p.is_bot: p.notify_self() # self.players_map = {c.name: i for i, c in enumerate(self.players)} if self.started: - G.sio.emit('chat_message', room=self.name, data=f'_died_role|{player.name}|{player.role.name}') - print(f'{self.name}: Check win status') + G.sio.emit( + "chat_message", + room=self.name, + data=f"_died_role|{player.name}|{player.role.name}", + ) + print(f"{self.name}: Check win status") attacker_role = None if player.attacker and player.attacker in self.players: attacker_role = player.attacker.role - winners = [p for p in self.players if p.role is not None and p.role.on_player_death([p for p in self.get_alive_players() if not p.is_ghost], initial_players=self.initial_players, dead_role=player.role, attacker_role=attacker_role)] - if not self.attack_in_progress and len(winners) > 0 and not self.someone_won: + winners = [ + p + for p in self.players + if p.role is not None + and p.role.on_player_death( + [p for p in self.get_alive_players() if not p.is_ghost], + initial_players=self.initial_players, + dead_role=player.role, + attacker_role=attacker_role, + ) + ] + if ( + not self.attack_in_progress + and len(winners) > 0 + and not self.someone_won + ): return self.announces_winners(winners) - elif len(winners) > 0 and not self.someone_won: # non tutti hanno risposto, ma ci sono vincitori. + elif ( + len(winners) > 0 and not self.someone_won + ): # non tutti hanno risposto, ma ci sono vincitori. self.pending_winners = winners else: - if not isinstance(player.role, roles.Sheriff) and not self.initial_players == 3: - G.sio.emit('notify_dead_role', room=self.name, data={ - 'name': player.name, - 'lives': 0, - 'max_lives': player.max_lives, - 'is_ghost': player.is_ghost, - 'is_bot': player.is_bot, - 'icon': '🤠', - 'avatar': player.avatar, - 'role': player.role.__dict__, - }) - - for i in range(len(player.gold_rush_equipment)): - self.deck.shop_deck.append(player.gold_rush_equipment.pop()) # vulture sam doesnt get these cards + if ( + not isinstance(player.role, roles.Sheriff) + and not self.initial_players == 3 + ): + G.sio.emit( + "notify_dead_role", + room=self.name, + data={ + "name": player.name, + "lives": 0, + "max_lives": player.max_lives, + "is_ghost": player.is_ghost, + "is_bot": player.is_bot, + "icon": "🤠", + "avatar": player.avatar, + "role": player.role.__dict__, + }, + ) - #il giocatore quando muore perde tutte le pepite se non è pistolero ombra + for i in range(len(player.gold_rush_equipment)): + self.deck.shop_deck.append( + player.gold_rush_equipment.pop() + ) # vulture sam doesnt get these cards + + # il giocatore quando muore perde tutte le pepite se non è pistolero ombra player.gold_nuggets = 0 - vulture = [p for p in self.get_alive_players() if p.character.check(self, characters.VultureSam)] + vulture = [ + p + for p in self.get_alive_players() + if p.character.check(self, characters.VultureSam) + ] if len(vulture) == 0: for i in range(len(player.hand)): self.deck.scrap(player.hand.pop(), True) @@ -826,11 +1087,11 @@ class Game: self.deck.scrap(player.equipment.pop(), True) elif len(vulture) == 2: for i in range(len(player.hand)): - vulture[i%2].hand.append(player.hand.pop()) - vulture[i%2].hand[-1].reset_card() + vulture[i % 2].hand.append(player.hand.pop()) + vulture[i % 2].hand[-1].reset_card() for i in range(len(player.equipment)): - vulture[i%2].hand.append(player.equipment.pop()) - vulture[i%2].hand[-1].reset_card() + vulture[i % 2].hand.append(player.equipment.pop()) + vulture[i % 2].hand[-1].reset_card() vulture[0].notify_self() vulture[1].notify_self() else: @@ -842,22 +1103,34 @@ class Game: vulture[0].hand[-1].reset_card() vulture[0].notify_self() - greg = [p for p in self.get_alive_players() if p.character.check(self, chd.GregDigger)] + greg = [ + p + for p in self.get_alive_players() + if p.character.check(self, chd.GregDigger) + ] for i in range(len(greg)): - greg[i].lives = min(greg[i].lives+2, greg[i].max_lives) - herb = [p for p in self.get_alive_players() if p.character.check(self, chd.HerbHunter)] + greg[i].lives = min(greg[i].lives + 2, greg[i].max_lives) + herb = [ + p + for p in self.get_alive_players() + if p.character.check(self, chd.HerbHunter) + ] for i in range(len(herb)): self.deck.draw(True, player=herb[i]) self.deck.draw(True, player=herb[i]) herb[i].notify_self() - #se Vulture Sam o Herb Hounter è uno sceriffo e ha appena ucciso il suo Vice, deve scartare le carte che ha pescato con la sua abilità - if player.attacker and player.attacker in self.get_alive_players() and isinstance(player.attacker.role, roles.Sheriff) and isinstance(player.role, roles.Vice): + # se Vulture Sam o Herb Hounter è uno sceriffo e ha appena ucciso il suo Vice, deve scartare le carte che ha pescato con la sua abilità + if ( + player.attacker + and player.attacker in self.get_alive_players() + and isinstance(player.attacker.role, roles.Sheriff) + and isinstance(player.role, roles.Vice) + ): for i in range(len(player.attacker.hand)): self.deck.scrap(player.attacker.hand.pop(), True) player.attacker.notify_self() - self.is_handling_death = False if corpse.is_my_turn: corpse.is_my_turn = False @@ -865,57 +1138,96 @@ class Game: self.next_turn() def check_event(self, ev): - if self.deck is None or len(self.deck.event_cards) == 0: return False + if self.deck is None: + return False if isinstance(ev, type): - return isinstance(self.deck.event_cards[0], ev) + return ( + len(self.deck.event_cards) > 0 + and isinstance(self.deck.event_cards[0], ev) + ) or ( + len(self.deck.event_cards_wildwestshow) > 0 + and isinstance(self.deck.event_cards_wildwestshow[0], ev) + ) else: - return any(isinstance(self.deck.event_cards[0], evc) for evc in ev) + return any(self.check_event(evc) for evc in ev) - def get_visible_players(self, player: pl.Player): # returns a dictionary because we need to add the distance + def get_visible_players( + self, player: pl.Player + ): # returns a dictionary because we need to add the distance pls = self.get_alive_players() - if len(pls) == 0 or player not in pls: return [] + if len(pls) == 0 or player not in pls: + return [] i = pls.index(player) mindist = 99 if not self.check_event(ce.Agguato) else 1 - return [{ - 'name': pls[j].name, - 'dist': min([abs(i - j), (i+ abs(j-len(pls))), (j+ abs(i-len(pls))), mindist]) + pls[j].get_visibility(), - 'lives': pls[j].lives, - 'max_lives': pls[j].max_lives, - 'is_sheriff': isinstance(pls[j].role, roles.Sheriff), - 'cards': len(pls[j].hand)+len(pls[j].equipment), - 'is_ghost': pls[j].is_ghost, - 'is_bot': pls[j].is_bot, - 'icon': pls[j].role.icon if (pls[j].role is not None and (self.initial_players == 3 or isinstance(pls[j].role, roles.Sheriff))) else '🤠', - 'avatar': pls[j].avatar, - 'role': pls[j].role, - } for j in range(len(pls)) if i != j] + return [ + { + "name": pls[j].name, + "dist": min( + [ + abs(i - j), + (i + abs(j - len(pls))), + (j + abs(i - len(pls))), + mindist, + ] + ) + + pls[j].get_visibility(), + "lives": pls[j].lives, + "max_lives": pls[j].max_lives, + "is_sheriff": isinstance(pls[j].role, roles.Sheriff), + "cards": len(pls[j].hand) + len(pls[j].equipment), + "is_ghost": pls[j].is_ghost, + "is_bot": pls[j].is_bot, + "icon": pls[j].role.icon + if ( + pls[j].role is not None + and ( + self.initial_players == 3 + or isinstance(pls[j].role, roles.Sheriff) + ) + ) + else "🤠", + "avatar": pls[j].avatar, + "role": pls[j].role, + } + for j in range(len(pls)) + if i != j + ] def get_alive_players(self): return [p for p in self.players if not p.is_dead or p.is_ghost] def get_dead_players(self, include_ghosts=True): - return [p for p in self.players if p.is_dead and (include_ghosts or not p.is_ghost)] + return [ + p for p in self.players if p.is_dead and (include_ghosts or not p.is_ghost) + ] def notify_all(self): if self.started and self.replay_speed > 0: show_cards = self.check_event(cew.Sacagaway) - data = [{ - 'name': p.name, - 'ncards': len(p.hand), - 'hand_cards': [c.__dict__ for c in p.hand] if show_cards else [], - 'equipment': [e.__dict__ for e in p.equipment], - 'gold_rush_equipment': [e.__dict__ for e in p.gold_rush_equipment], - 'lives': p.lives, - 'max_lives': p.max_lives, - 'gold_nuggets': p.gold_nuggets, - 'is_sheriff': isinstance(p.role, roles.Sheriff), - 'is_my_turn': p.is_my_turn, - 'pending_action': p.pending_action, - 'character': p.character.__dict__ if p.character else None, - 'real_character': p.real_character.__dict__ if p.real_character else None, - 'icon': p.role.icon if self.initial_players == 3 and p.role else '🤠', - 'avatar': p.avatar, - 'is_ghost': p.is_ghost, - 'is_bot': p.is_bot, - } for p in self.get_alive_players()] - G.sio.emit('players_update', room=self.name, data=data) + data = [ + { + "name": p.name, + "ncards": len(p.hand), + "hand_cards": [c.__dict__ for c in p.hand] if show_cards else [], + "equipment": [e.__dict__ for e in p.equipment], + "gold_rush_equipment": [e.__dict__ for e in p.gold_rush_equipment], + "lives": p.lives, + "max_lives": p.max_lives, + "gold_nuggets": p.gold_nuggets, + "is_sheriff": isinstance(p.role, roles.Sheriff), + "is_my_turn": p.is_my_turn, + "pending_action": p.pending_action, + "character": p.character.__dict__ if p.character else None, + "real_character": p.real_character.__dict__ + if p.real_character + else None, + "icon": p.role.icon + if self.initial_players == 3 and p.role + else "🤠", + "avatar": p.avatar, + "is_ghost": p.is_ghost, + "is_bot": p.is_bot, + } + for p in self.get_alive_players() + ] + G.sio.emit("players_update", room=self.name, data=data) diff --git a/backend/bang/players.py b/backend/bang/players.py index afd5a43..cc5051d 100644 --- a/backend/bang/players.py +++ b/backend/bang/players.py @@ -26,24 +26,25 @@ if TYPE_CHECKING: from bang.game import Game robot_pictures = [ - 'https://i.imgur.com/40rAFIb.jpg', - 'https://i.imgur.com/gG77VRR.jpg', - 'https://i.imgur.com/l2DTQeH.jpg', - 'https://i.imgur.com/aPM2gix.jpg', - 'https://i.imgur.com/ep5EB8c.jpg', - 'https://i.imgur.com/qsOWIsf.jpg', - 'https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/240/apple/325/robot_1f916.png', - 'https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/240/openmoji/338/robot_1f916.png', - 'https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/240/microsoft/319/robot_1f916.png', - 'https://i.imgur.com/0plPBZR.png', - 'https://i.imgur.com/DScxoSX.png', - 'https://i.imgur.com/YzPQChj.png', - 'https://i.imgur.com/v3BvnX4.png', - 'https://i.imgur.com/1XHsE9f.png', - 'https://i.imgur.com/q0CrH2c.png', - 'https://i.imgur.com/Z5vXgd4.png', + "https://i.imgur.com/40rAFIb.jpg", + "https://i.imgur.com/gG77VRR.jpg", + "https://i.imgur.com/l2DTQeH.jpg", + "https://i.imgur.com/aPM2gix.jpg", + "https://i.imgur.com/ep5EB8c.jpg", + "https://i.imgur.com/qsOWIsf.jpg", + "https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/240/apple/325/robot_1f916.png", + "https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/240/openmoji/338/robot_1f916.png", + "https://emojipedia-us.s3.dualstack.us-west-1.amazonaws.com/thumbs/240/microsoft/319/robot_1f916.png", + "https://i.imgur.com/0plPBZR.png", + "https://i.imgur.com/DScxoSX.png", + "https://i.imgur.com/YzPQChj.png", + "https://i.imgur.com/v3BvnX4.png", + "https://i.imgur.com/1XHsE9f.png", + "https://i.imgur.com/q0CrH2c.png", + "https://i.imgur.com/Z5vXgd4.png", ] + class PendingAction(IntEnum): PICK = 0 DRAW = 1 @@ -52,16 +53,18 @@ class PendingAction(IntEnum): WAIT = 4 CHOOSE = 5 + class Player: def is_admin(self): - return self.discord_id in {'244893980960096266', '539795574019457034'} + return self.discord_id in {"244893980960096266", "539795574019457034"} def _get_avatar(self): import requests + headers = { - 'Authorization': 'Bearer ' + self.discord_token, + "Authorization": "Bearer " + self.discord_token, } - r = requests.get('https://discordapp.com/api/users/@me', headers=headers) + r = requests.get("https://discordapp.com/api/users/@me", headers=headers) if r.status_code == 200: res = r.json() print(res) @@ -70,17 +73,26 @@ class Player: else: self.avatar = f'https://cdn.discordapp.com/avatars/{res["id"]}/{res["avatar"]}.png' if self.game: - G.sio.emit('chat_message', room=self.game.name, data=f'_change_username|{self.name}|{res["username"]}') - self.name = res['username'] - self.discord_id = res['id'] + G.sio.emit( + "chat_message", + room=self.game.name, + data=f'_change_username|{self.name}|{res["username"]}', + ) + self.name = res["username"] + self.discord_id = res["id"] if self.is_admin(): - if self.game: self.game.feature_flags() - G.sio.emit('chat_message', room=self.sid, data={'color':'green', 'text':'(you are admin)'}) + if self.game: + self.game.feature_flags() + G.sio.emit( + "chat_message", + room=self.sid, + data={"color": "green", "text": "(you are admin)"}, + ) if self.game: self.game.notify_room() - G.sio.emit('me', data=self.name, room=self.sid) + G.sio.emit("me", data=self.name, room=self.sid) else: - print('error getting avatar', r.status_code, r.text) + print("error getting avatar", r.status_code, r.text) print(r) def __init__(self, name, sid, bot=False, discord_token=None): @@ -91,7 +103,7 @@ class Player: self.discord_token = discord_token self.discord_id = None self.played_cards = 0 - self.avatar = '' + self.avatar = "" if self.is_bot: self.avatar = robot_pictures[randrange(len(robot_pictures))] if self.discord_token: @@ -142,54 +154,76 @@ class Player: self.can_play_vendetta = True self.can_play_again_don_bell = True self.is_giving_life = False - self.choose_text = 'choose_card_to_get' - self.using_rimbalzo = 0 # 0 no, 1 scegli giocatore, 2 scegli carta + self.choose_text = "choose_card_to_get" + self.using_rimbalzo = 0 # 0 no, 1 scegli giocatore, 2 scegli carta self.bang_used = 0 self.gold_nuggets = 0 self.gold_rush_equipment: List[grc.ShopCard] = [] self.was_player = False self.setaccio_count = 0 - self.choose_action = '' + self.choose_action = "" def join_game(self, game): self.game = game - print(f'I {self.name} joined {self.game}') + print(f"I {self.name} joined {self.game}") def disconnect(self): - if self.is_admin() and self.game.debug and self.game.started and getattr(sys, 'gettrace', None)(): return False + if ( + self.is_admin() + and self.game.debug + and self.game.started + and getattr(sys, "gettrace", None)() + ): + return False return self.game.handle_disconnect(self) def set_role(self, role: r.Role): self.role = role print(f'{self.name}: I am a {role.name}, my goal is "{role.goal}"') - G.sio.emit('role', room=self.sid, data=json.dumps( - role, default=lambda o: o.__dict__)) + G.sio.emit( + "role", room=self.sid, data=json.dumps(role, default=lambda o: o.__dict__) + ) def set_character(self, character: str): print(self.available_characters, character) if self.character is None: try: - self.character = next(x for x in self.available_characters if x.name == character) + self.character = next( + x for x in self.available_characters if x.name == character + ) except: # fix for wrong character encoding in the first part of some characters like Jose delgrado - self.character = next(x for x in self.available_characters if x.name.split()[1] == character.split()[1]) - if 'high_noon' in self.game.expansions: + self.character = next( + x + for x in self.available_characters + if x.name.split()[1] == character.split()[1] + ) + if "high_noon" in self.game.expansions: # questo viene utilizzato per la carta nuova identità - self.not_chosen_character = next(x for x in self.available_characters if x.name != character) + self.not_chosen_character = next( + x for x in self.available_characters if x.name != character + ) else: self.not_chosen_character = None self.real_character = self.character self.available_characters = [] - print(f'{self.name}: I chose character {self.character.name}') - G.sio.emit('chat_message', room=self.game.name, - data=f'_did_choose_character|{self.name}') + print(f"{self.name}: I chose character {self.character.name}") + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_did_choose_character|{self.name}", + ) self.game.notify_character_selection() elif self.real_character and isinstance(self.real_character, chd.VeraCuster): self.character = next( - x for x in self.available_characters if x.name == character) + x for x in self.available_characters if x.name == character + ) self.available_characters = [] - G.sio.emit('chat_message', room=self.game.name, - data=f'_did_choose_character|{self.name}') + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_did_choose_character|{self.name}", + ) self.pending_action = PendingAction.DRAW self.notify_self() @@ -199,36 +233,39 @@ class Player: self.hand = [] self.equipment = [] self.pending_action = PendingAction.WAIT - self.noStar = (self.game.initial_players == 3) + self.noStar = self.game.initial_players == 3 def set_available_character(self, available): self.available_characters = available - print(f'{self.name}: I have to choose between {available}') + print(f"{self.name}: I have to choose between {available}") if not self.is_bot: - G.sio.emit('characters', room=self.sid, data=json.dumps( - available, default=lambda o: o.__dict__)) + G.sio.emit( + "characters", + room=self.sid, + data=json.dumps(available, default=lambda o: o.__dict__), + ) else: char_name = available[randrange(0, len(available))].name - self.game.rpc_log.append(f'{self.name};set_character;{char_name}') + self.game.rpc_log.append(f"{self.name};set_character;{char_name}") self.set_character(char_name) - def notify_card(self, player, card, message=''): + def notify_card(self, player, card, message=""): try: card = card.__dict__ except: pass - mess = { - 'player': player.name, - 'card': card, - 'message':message - } - print('notifying card') - G.sio.emit('notify_card', room=self.sid, data=mess) + mess = {"player": player.name, "card": card, "message": message} + print("notifying card") + G.sio.emit("notify_card", room=self.sid, data=mess) def notify_self(self): if any((True for c in self.equipment if isinstance(c, tvosc.Fantasma))): self.is_ghost = True - elif self.is_ghost and self.is_my_turn and not self.game.check_event(ceh.CittaFantasma): + elif ( + self.is_ghost + and self.is_my_turn + and not self.game.check_event(ceh.CittaFantasma) + ): self.end_turn(forced=True) elif self.is_ghost and not self.game.check_event(ceh.CittaFantasma): self.is_ghost = False @@ -236,215 +273,442 @@ class Player: self.game.deck.scrap(self.hand.pop(), True) for i in range(len(self.equipment)): self.game.deck.scrap(self.equipment.pop(), True) - if self.is_ghost: self.lives = 0 - if self.pending_action == PendingAction.DRAW and self.game.check_event(ce.Peyote): - self.available_cards = [{ - 'icon': '🔴', - 'noDesc': True - },{ - 'icon': '⚫', - 'noDesc': True - }] + if self.is_ghost: + self.lives = 0 + if self.pending_action == PendingAction.DRAW and self.game.check_event( + ce.Peyote + ): + self.available_cards = [ + {"icon": "🔴", "noDesc": True}, + {"icon": "⚫", "noDesc": True}, + ] self.is_drawing = True - self.choose_text = 'choose_guess' + self.choose_text = "choose_guess" self.pending_action = PendingAction.CHOOSE - elif self.can_play_ranch and self.pending_action == PendingAction.PLAY and self.game.check_event(ce.Ranch): + elif ( + self.can_play_ranch + and self.pending_action == PendingAction.PLAY + and self.game.check_event(ce.Ranch) + ): self.can_play_ranch = False self.available_cards = [c for c in self.hand] self.discarded_cards = [] - self.available_cards.append({'icon': '✅','noDesc': True}) + self.available_cards.append({"icon": "✅", "noDesc": True}) self.is_playing_ranch = True - self.choose_text = 'choose_ranch' + self.choose_text = "choose_ranch" self.pending_action = PendingAction.CHOOSE - elif self.character and self.character.check(self.game, chars.SuzyLafayette) and self.lives > 0 and len(self.hand) == 0 and ( not self.is_my_turn or self.pending_action == PendingAction.PLAY): + elif ( + self.character + and self.character.check(self.game, chars.SuzyLafayette) + and self.lives > 0 + and len(self.hand) == 0 + and (not self.is_my_turn or self.pending_action == PendingAction.PLAY) + ): self.game.deck.draw(True, player=self) if self.lives <= 0 and self.max_lives > 0 and not self.is_dead: - print('dying, attacker', self.attacker) - if self.gold_nuggets >= 2 and any((isinstance(c, grc.Zaino) for c in self.gold_rush_equipment)): + print("dying, attacker", self.attacker) + if self.gold_nuggets >= 2 and any( + (isinstance(c, grc.Zaino) for c in self.gold_rush_equipment) + ): for i in range(len(self.gold_rush_equipment)): if isinstance(self.gold_rush_equipment[i], grc.Zaino): self.gold_rush_equipment[i].play_card(self, None) - return # play card will notify the player + return # play card will notify the player if self.character.check(self.game, chw.TerenKill): picked: cs.Card = self.game.deck.pick_and_scrap() - G.sio.emit('chat_message', room=self.game.name, data=f'_flipped|{self.name}|{picked.name}|{picked.num_suit()}') + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_flipped|{self.name}|{picked.name}|{picked.num_suit()}", + ) if not picked.check_suit(self.game, [cs.Suit.SPADES]): self.lives = 1 self.game.deck.draw(True, player=self) - if self.character.check(self.game, chars.SidKetchum) and len(self.hand) > 1 and self.lives == 0: + if ( + self.character.check(self.game, chars.SidKetchum) + and len(self.hand) > 1 + and self.lives == 0 + ): if self.game.players[self.game.turn] != self: - self.game.players[self.game.turn].pending_action = PendingAction.WAIT + self.game.players[ + self.game.turn + ].pending_action = PendingAction.WAIT self.game.players[self.game.turn].notify_self() self.scrapped_cards = 0 self.previous_pending_action = self.pending_action self.pending_action = PendingAction.CHOOSE - self.choose_text = 'choose_sid_scrap' + self.choose_text = "choose_sid_scrap" self.available_cards = self.hand self.lives += 1 - + ser = self.__dict__.copy() - ser.pop('game') - ser.pop('sid') - ser.pop('on_pick_cb') - ser.pop('discord_token') - ser.pop('on_failed_response_cb') - ser.pop('attacker') - ser.pop('rissa_targets') + ser.pop("game") + ser.pop("sid") + ser.pop("on_pick_cb") + ser.pop("discord_token") + ser.pop("on_failed_response_cb") + ser.pop("attacker") + ser.pop("rissa_targets") if self.attacker: - ser['attacker'] = self.attacker.name - ser['sight'] = self.get_sight() - ser['sight_extra'] = self.get_sight(False) -1 - ser['can_gold_rush_discard'] = any((p != self and any((e.number <= self.gold_nuggets + 1 for e in p.gold_rush_equipment)) for p in self.game.get_alive_players())) + ser["attacker"] = self.attacker.name + ser["sight"] = self.get_sight() + ser["sight_extra"] = self.get_sight(False) - 1 + ser["can_gold_rush_discard"] = any( + ( + p != self + and any( + (e.number <= self.gold_nuggets + 1 for e in p.gold_rush_equipment) + ) + for p in self.game.get_alive_players() + ) + ) if self.character: - ser['gold_rush_discount'] = 1 if self.character.check(self.game, grch.PrettyLuzena) and self.special_use_count < 1 else 0 - ser['lives'] = max(ser['lives'], 0) + ser["gold_rush_discount"] = ( + 1 + if self.character.check(self.game, grch.PrettyLuzena) + and self.special_use_count < 1 + else 0 + ) + ser["lives"] = max(ser["lives"], 0) if self.lives <= 0 and self.max_lives > 0 and not self.is_dead: self.pending_action = PendingAction.WAIT - ser['hand'] = [] - ser['equipment'] = [] - G.sio.emit('self', room=self.sid, data=json.dumps(ser, default=lambda o: o.__dict__)) + ser["hand"] = [] + ser["equipment"] = [] + G.sio.emit( + "self", + room=self.sid, + data=json.dumps(ser, default=lambda o: o.__dict__), + ) self.game.player_death(self) - if self.game and self.game.started: # falso quando un bot viene eliminato dalla partita - G.sio.emit('self_vis', room=self.sid, data=json.dumps(self.game.get_visible_players(self), default=lambda o: o.__dict__)) + if ( + self.game and self.game.started + ): # falso quando un bot viene eliminato dalla partita + G.sio.emit( + "self_vis", + room=self.sid, + data=json.dumps( + self.game.get_visible_players(self), default=lambda o: o.__dict__ + ), + ) self.game.notify_all() - G.sio.emit('self', room=self.sid, data=json.dumps(ser, default=lambda o: o.__dict__)) + G.sio.emit( + "self", room=self.sid, data=json.dumps(ser, default=lambda o: o.__dict__) + ) def bot_spin(self): while self.is_bot and self.game is not None and not self.game.shutting_down: - G.sio.sleep(max(0.2, uniform(self.game.bot_speed/2-0.1, self.game.bot_speed))) + G.sio.sleep( + max(0.2, uniform(self.game.bot_speed / 2 - 0.1, self.game.bot_speed)) + ) if self.lives > 0 or self.is_ghost: self.bot_logic() def bot_logic(self): - if self.game is None or self.game.shutting_down: return - if self.pending_action is not None and self.pending_action != PendingAction.WAIT: + if self.game is None or self.game.shutting_down: + return + if ( + self.pending_action is not None + and self.pending_action != PendingAction.WAIT + ): # eventlet.sleep(uniform(self.game.bot_speed/2-0.1, self.game.bot_speed)) pass else: return if self.pending_action == PendingAction.PICK: - self.game.rpc_log.append(f'{self.name};pick;') + self.game.rpc_log.append(f"{self.name};pick;") self.pick() elif self.pending_action == PendingAction.DRAW: - self.game.rpc_log.append(f'{self.name};draw;') - self.draw('') + self.game.rpc_log.append(f"{self.name};draw;") + self.draw("") elif self.pending_action == PendingAction.PLAY: - non_blocked_cards = [card for card in self.hand if (not self.game.check_event(ceh.Manette) or card.suit == self.committed_suit_manette) and (not self.game.check_event(ce.IlGiudice) or not card.usable_next_turn) ] - equippables = [c for c in non_blocked_cards if (c.is_equipment or c.usable_next_turn) and not isinstance(c, cs.Prigione) and not c.need_target and not any((type(c) == type(x) and not (c.is_weapon and c.must_be_used) for x in self.equipment))] - misc = [c for c in non_blocked_cards if not c.need_target and (isinstance(c, cs.WellsFargo) or isinstance(c, cs.Indiani) or isinstance(c, cs.Gatling) or isinstance(c, cs.Diligenza) or isinstance(c, cs.Emporio) or ((isinstance(c, cs.Birra) and self.lives < self.max_lives or c.must_be_used) and not self.game.check_event(ceh.IlReverendo)) or (c.need_with and len(self.hand) > 1 and not (isinstance(c, csd.Whisky) and self.lives == self.max_lives))) - and not (not c.can_be_used_now and self.game.check_event(ce.IlGiudice)) and not c.is_equipment] - need_target = [c for c in non_blocked_cards if c.need_target and c.can_be_used_now and not (c.need_with and len(self.hand) < 2) and not (type(c) == type(cs.Bang) and (self.game.check_event(ceh.Sermone) or (self.has_played_bang and (not any((isinstance(c, cs.Volcanic) for c in self.equipment)) or self.game.check_event(ce.Lazo))))) and not (isinstance(c, cs.Prigione) and self.game.check_event(ce.IlGiudice)) or isinstance(c, cs.Duello) or isinstance(c, cs.CatBalou) or isinstance(c, csd.Pugno)] - green_cards = [c for c in self.equipment if not self.game.check_event(ce.Lazo) and not isinstance(c, cs.Mancato) and c.usable_next_turn and c.can_be_used_now] + non_blocked_cards = [ + card + for card in self.hand + if ( + not self.game.check_event(ceh.Manette) + or card.suit == self.committed_suit_manette + ) + and ( + not self.game.check_event(ce.IlGiudice) or not card.usable_next_turn + ) + ] + equippables = [ + c + for c in non_blocked_cards + if (c.is_equipment or c.usable_next_turn) + and not isinstance(c, cs.Prigione) + and not c.need_target + and not any( + ( + type(c) == type(x) and not (c.is_weapon and c.must_be_used) + for x in self.equipment + ) + ) + ] + misc = [ + c + for c in non_blocked_cards + if not c.need_target + and ( + isinstance(c, cs.WellsFargo) + or isinstance(c, cs.Indiani) + or isinstance(c, cs.Gatling) + or isinstance(c, cs.Diligenza) + or isinstance(c, cs.Emporio) + or ( + ( + isinstance(c, cs.Birra) + and self.lives < self.max_lives + or c.must_be_used + ) + and not self.game.check_event(ceh.IlReverendo) + ) + or ( + c.need_with + and len(self.hand) > 1 + and not ( + isinstance(c, csd.Whisky) and self.lives == self.max_lives + ) + ) + ) + and not (not c.can_be_used_now and self.game.check_event(ce.IlGiudice)) + and not c.is_equipment + ] + need_target = [ + c + for c in non_blocked_cards + if c.need_target + and c.can_be_used_now + and not (c.need_with and len(self.hand) < 2) + and not ( + type(c) == type(cs.Bang) + and ( + self.game.check_event(ceh.Sermone) + or ( + self.has_played_bang + and ( + not any( + (isinstance(c, cs.Volcanic) for c in self.equipment) + ) + or self.game.check_event(ce.Lazo) + ) + ) + ) + ) + and not ( + isinstance(c, cs.Prigione) and self.game.check_event(ce.IlGiudice) + ) + or isinstance(c, cs.Duello) + or isinstance(c, cs.CatBalou) + or isinstance(c, csd.Pugno) + ] + green_cards = [ + c + for c in self.equipment + if not self.game.check_event(ce.Lazo) + and not isinstance(c, cs.Mancato) + and c.usable_next_turn + and c.can_be_used_now + ] if self.game.debug: - print(f'hand: {self.hand}') - print(f'non_blocked: {non_blocked_cards}') - print(f'equippables: {equippables}') - print(f'misc: {misc}') - print(f'need_target: {need_target}') - print(f'green_cards: {green_cards}') - if self.gold_nuggets > 0 and any((c.number <= self.gold_nuggets for c in self.game.deck.shop_cards)): + print(f"hand: {self.hand}") + print(f"non_blocked: {non_blocked_cards}") + print(f"equippables: {equippables}") + print(f"misc: {misc}") + print(f"need_target: {need_target}") + print(f"green_cards: {green_cards}") + if self.gold_nuggets > 0 and any( + (c.number <= self.gold_nuggets for c in self.game.deck.shop_cards) + ): for i in range(len(self.game.deck.shop_cards)): if self.game.deck.shop_cards[i].number <= self.gold_nuggets: - self.game.rpc_log.append(f'{self.name};buy_gold_rush_card;{i}') + self.game.rpc_log.append(f"{self.name};buy_gold_rush_card;{i}") self.buy_gold_rush_card(i) return if len(equippables) > 0 and not self.game.check_event(ce.IlGiudice): for c in equippables: - if isinstance(c, tvosc.Fantasma) and len(self.game.get_dead_players(include_ghosts=False)) == 0: + if ( + isinstance(c, tvosc.Fantasma) + and len(self.game.get_dead_players(include_ghosts=False)) == 0 + ): continue if self.play_card(self.hand.index(c)): return elif len(misc) > 0: for c in misc: - if c.need_with and len(self.hand) > 1 and self.play_card(self.hand.index(c), _with=sample([j for j in range(len(self.hand)) if j != self.hand.index(c)], 1)[0]): + if ( + c.need_with + and len(self.hand) > 1 + and self.play_card( + self.hand.index(c), + _with=sample( + [ + j + for j in range(len(self.hand)) + if j != self.hand.index(c) + ], + 1, + )[0], + ) + ): return elif self.play_card(self.hand.index(c)): return elif len(need_target) > 0: for c in need_target: - _range = self.get_sight() if c.name == 'Bang!' or c.name == "Pepperbox" else c.range - others = [p for p in self.game.get_visible_players(self) if _range >= p['dist'] and not (isinstance(self.role, r.Vice) and p['is_sheriff'] and not c.must_be_used) and p['lives'] > 0 and not - ((isinstance(c, cs.CatBalou) or isinstance(c, cs.Panico)) and p['cards'] == 0) - and not (p['is_sheriff'] and isinstance(c, cs.Prigione))] - if (isinstance(c, cs.Panico) or isinstance(c, cs.Panico))and len(self.equipment) > 0: - others.append({'name': self.name, 'is_sheriff': isinstance(self.role, r.Sheriff)}) + _range = ( + self.get_sight() + if c.name == "Bang!" or c.name == "Pepperbox" + else c.range + ) + others = [ + p + for p in self.game.get_visible_players(self) + if _range >= p["dist"] + and not ( + isinstance(self.role, r.Vice) + and p["is_sheriff"] + and not c.must_be_used + ) + and p["lives"] > 0 + and not ( + (isinstance(c, cs.CatBalou) or isinstance(c, cs.Panico)) + and p["cards"] == 0 + ) + and not (p["is_sheriff"] and isinstance(c, cs.Prigione)) + ] + if (isinstance(c, cs.Panico) or isinstance(c, cs.Panico)) and len( + self.equipment + ) > 0: + others.append( + { + "name": self.name, + "is_sheriff": isinstance(self.role, r.Sheriff), + } + ) if len(others) == 0 or c not in self.hand: continue target = others[randrange(0, len(others))] - if target['is_sheriff'] and isinstance(self.role, r.Renegade): + if target["is_sheriff"] and isinstance(self.role, r.Renegade): target = others[randrange(0, len(others))] if not c.need_with: - if self.play_card(self.hand.index(c), against=target['name']): + if self.play_card(self.hand.index(c), against=target["name"]): return elif len(self.hand) > 1: - if self.play_card(self.hand.index(c), against=target['name'], _with=sample([j for j in range(len(self.hand)) if j != self.hand.index(c)], 1)[0]): + if self.play_card( + self.hand.index(c), + against=target["name"], + _with=sample( + [ + j + for j in range(len(self.hand)) + if j != self.hand.index(c) + ], + 1, + )[0], + ): return elif len(green_cards) > 0: for c in green_cards: - if not isinstance(c, cs.Mancato) and c.usable_next_turn and c.can_be_used_now: + if ( + not isinstance(c, cs.Mancato) + and c.usable_next_turn + and c.can_be_used_now + ): if not c.need_target: - if self.play_card(len(self.hand)+self.equipment.index(c)): + if self.play_card(len(self.hand) + self.equipment.index(c)): return else: - _range = self.get_sight() if c.name == "Pepperbox" else c.range - others = [p for p in self.game.get_visible_players(self) if _range >= p['dist'] and not (isinstance(self.role, r.Vice) and p['is_sheriff'])] + _range = ( + self.get_sight() if c.name == "Pepperbox" else c.range + ) + others = [ + p + for p in self.game.get_visible_players(self) + if _range >= p["dist"] + and not ( + isinstance(self.role, r.Vice) and p["is_sheriff"] + ) + ] if len(others) == 0: continue target = others[randrange(0, len(others))] - if target['is_sheriff'] and isinstance(self.role, r.Renegade): + if target["is_sheriff"] and isinstance( + self.role, r.Renegade + ): target = others[randrange(0, len(others))] - if self.play_card(len(self.hand)+self.equipment.index(c), against=target['name']): + if self.play_card( + len(self.hand) + self.equipment.index(c), + against=target["name"], + ): return break - maxcards = self.lives if not self.character.check(self.game, chd.SeanMallory) else 10 - if maxcards == self.lives and any((isinstance(c, grc.Cinturone) for c in self.gold_rush_equipment)): + maxcards = ( + self.lives + if not self.character.check(self.game, chd.SeanMallory) + else 10 + ) + if maxcards == self.lives and any( + (isinstance(c, grc.Cinturone) for c in self.gold_rush_equipment) + ): maxcards = 8 if len(self.hand) > maxcards: - self.game.rpc_log.append(f'{self.name};scrap;{0}') + self.game.rpc_log.append(f"{self.name};scrap;{0}") self.scrap(0) else: - self.game.rpc_log.append(f'{self.name};end_turn') + self.game.rpc_log.append(f"{self.name};end_turn") self.end_turn() elif self.pending_action == PendingAction.RESPOND: did_respond = False for i in range(len(self.hand)): - if self.hand[i].can_be_used_now and (self.hand[i].name in self.expected_response or self.character.check(self.game, chd.ElenaFuente)): - self.game.rpc_log.append(f'{self.name};respond;{i}') + if self.hand[i].can_be_used_now and ( + self.hand[i].name in self.expected_response + or self.character.check(self.game, chd.ElenaFuente) + ): + self.game.rpc_log.append(f"{self.name};respond;{i}") self.respond(i) did_respond = True break for i in range(len(self.equipment)): - if not self.game.check_event(ce.Lazo) and self.equipment[i].name in self.expected_response: - self.game.rpc_log.append(f'{self.name};respond;{len(self.hand)+i}') - self.respond(len(self.hand)+i) + if ( + not self.game.check_event(ce.Lazo) + and self.equipment[i].name in self.expected_response + ): + self.game.rpc_log.append(f"{self.name};respond;{len(self.hand)+i}") + self.respond(len(self.hand) + i) did_respond = True break if not did_respond: - self.game.rpc_log.append(f'{self.name};respond;{-1}') + self.game.rpc_log.append(f"{self.name};respond;{-1}") self.respond(-1) elif self.pending_action == PendingAction.CHOOSE: if not self.target_p: card_index = randrange(0, len(self.available_cards)) - self.game.rpc_log.append(f'{self.name};choose;{card_index}') + self.game.rpc_log.append(f"{self.name};choose;{card_index}") self.choose(card_index) else: target = self.game.get_player_named(self.target_p) - if len(target.hand)+len(target.equipment) == 0: + if len(target.hand) + len(target.equipment) == 0: self.pending_action = PendingAction.PLAY self.notify_self() else: try: - card_index = randrange(0, len(target.hand)+len(target.equipment)) + card_index = randrange( + 0, len(target.hand) + len(target.equipment) + ) self.choose(card_index) - self.game.rpc_log.append(f'{self.name};choose;{card_index}') + self.game.rpc_log.append(f"{self.name};choose;{card_index}") except: self.choose(0) - self.game.rpc_log.append(f'{self.name};choose;{0}') + self.game.rpc_log.append(f"{self.name};choose;{0}") - - def play_turn(self, can_play_vendetta = True, again = False): - if ((self.lives == 0 or self.is_dead) and not self.is_ghost) or (self.is_ghost and any(isinstance(c, tvosc.Fantasma) for c in self.equipment) and self.game.check_event(ce.Lazo)): + def play_turn(self, can_play_vendetta=True, again=False): + if ((self.lives == 0 or self.is_dead) and not self.is_ghost) or ( + self.is_ghost + and any(isinstance(c, tvosc.Fantasma) for c in self.equipment) + and self.game.check_event(ce.Lazo) + ): return self.end_turn(forced=True) self.scrapped_cards = 0 self.setaccio_count = 0 @@ -453,9 +717,8 @@ class Player: self.is_playing_ranch = False self.can_play_vendetta = can_play_vendetta if not again: - G.sio.emit('chat_message', room=self.game.name, - data=f'_turn|{self.name}') - print(f'{self.name}: I was notified that it is my turn') + G.sio.emit("chat_message", room=self.game.name, data=f"_turn|{self.name}") + print(f"{self.name}: I was notified that it is my turn") self.was_shot = False self.attacker = None self.is_my_turn = True @@ -478,103 +741,148 @@ class Player: self.game.deck.draw(True, player=self) if self.character.check(self.game, chars.BartCassidy) and self.lives > 0: self.game.deck.draw(True, player=self) - G.sio.emit('chat_message', room=self.game.name, data=f'_special_bart_cassidy|{self.name}') + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_special_bart_cassidy|{self.name}", + ) self.heal_if_needed() if self.lives <= 0: return self.notify_self() - #non è un elif perchè vera custer deve fare questo poi cambiare personaggio - if self.game.check_event(ce.FratelliDiSangue) and self.lives > 1 and not self.is_giving_life and sum(p != self and p.lives < p.max_lives for p in self.game.get_alive_players()): - self.available_cards = [{ - 'name': p.name, - 'icon': p.role.icon if(self.game.initial_players == 3) else '⭐️' if isinstance(p.role, r.Sheriff) else '🤠', - 'alt_text': ''.join(['❤️']*p.lives)+''.join(['💀']*(p.max_lives-p.lives)), - 'avatar': p.avatar, - 'is_character': True, - 'is_player': True - } for p in self.game.get_alive_players() if p != self and p.lives < p.max_lives] - self.available_cards.append({'icon': '❌', 'noDesc': True}) - self.choose_text = 'choose_fratelli_di_sangue' + # non è un elif perchè vera custer deve fare questo poi cambiare personaggio + if ( + self.game.check_event(ce.FratelliDiSangue) + and self.lives > 1 + and not self.is_giving_life + and sum( + p != self and p.lives < p.max_lives + for p in self.game.get_alive_players() + ) + ): + self.available_cards = [ + { + "name": p.name, + "icon": p.role.icon + if (self.game.initial_players == 3) + else "⭐️" + if isinstance(p.role, r.Sheriff) + else "🤠", + "alt_text": "".join(["❤️"] * p.lives) + + "".join(["💀"] * (p.max_lives - p.lives)), + "avatar": p.avatar, + "is_character": True, + "is_player": True, + } + for p in self.game.get_alive_players() + if p != self and p.lives < p.max_lives + ] + self.available_cards.append({"icon": "❌", "noDesc": True}) + self.choose_text = "choose_fratelli_di_sangue" self.pending_action = PendingAction.CHOOSE self.is_giving_life = True - elif self.game.check_event(ceh.NuovaIdentita) and self.not_chosen_character is not None and not again: + elif ( + self.game.check_event(ceh.NuovaIdentita) + and self.not_chosen_character is not None + and not again + ): self.available_cards = [self.character, self.not_chosen_character] - self.choose_text = 'choose_nuova_identita' + self.choose_text = "choose_nuova_identita" self.pending_action = PendingAction.CHOOSE - elif not self.game.check_event(ce.Lazo) and any((isinstance(c, cs.Dinamite) or isinstance(c, cs.Prigione) or isinstance(c, tvosc.SerpenteASonagli) for c in self.equipment)): + elif not self.game.check_event(ce.Lazo) and any( + ( + isinstance(c, cs.Dinamite) + or isinstance(c, cs.Prigione) + or isinstance(c, tvosc.SerpenteASonagli) + for c in self.equipment + ) + ): self.is_giving_life = False self.pending_action = PendingAction.PICK else: self.is_giving_life = False if isinstance(self.real_character, chd.VeraCuster): - self.set_available_character([p.character for p in self.game.get_alive_players() if p != self]) + self.set_available_character( + [p.character for p in self.game.get_alive_players() if p != self] + ) else: self.pending_action = PendingAction.DRAW self.notify_self() def draw(self, pile): - if self.is_my_turn and self.pending_action == PendingAction.PLAY and pile == 'event' and self.game.check_event(ce.Cecchino) and sum((c.name == cs.Bang(0,0).name for c in self.hand)) >= 2: + if ( + self.is_my_turn + and self.pending_action == PendingAction.PLAY + and pile == "event" + and self.game.check_event(ce.Cecchino) + and sum((c.name == cs.Bang(0, 0).name for c in self.hand)) >= 2 + ): self.is_using_checchino = True - self.available_cards = [{ - 'name': p['name'], - 'icon': p['role'].icon if(self.game.initial_players == 3) else '⭐️' if p['is_sheriff'] else '🤠', - 'alt_text': ''.join(['❤️']*p['lives'])+''.join(['💀']*(p['max_lives']-p['lives'])), - 'is_character': True, - 'is_player': True - } for p in self.game.get_visible_players(self) if p['dist'] <= self.get_sight()] - self.available_cards.append({'icon': '❌', 'noDesc': True}) - self.choose_text = 'choose_cecchino' + self.available_cards = [ + { + "name": p["name"], + "icon": p["role"].icon + if (self.game.initial_players == 3) + else "⭐️" + if p["is_sheriff"] + else "🤠", + "alt_text": "".join(["❤️"] * p["lives"]) + + "".join(["💀"] * (p["max_lives"] - p["lives"])), + "is_character": True, + "is_player": True, + } + for p in self.game.get_visible_players(self) + if p["dist"] <= self.get_sight() + ] + self.available_cards.append({"icon": "❌", "noDesc": True}) + self.choose_text = "choose_cecchino" self.pending_action = PendingAction.CHOOSE self.notify_self() - if self.is_my_turn and self.pending_action == PendingAction.PLAY and pile == 'event' and self.game.check_event(cew.RegolamentoDiConti) and len(self.hand) > 0: - if not self.has_played_bang and any((self.get_sight() >= p['dist'] for p in self.game.get_visible_players(self))): - self.available_cards = self.hand.copy() - self.pending_action = PendingAction.CHOOSE - self.choose_text = 'choose_play_as_bang' - self.notify_self() - elif self.is_my_turn and self.pending_action == PendingAction.PLAY and pile == 'event' and self.game.check_event(cew.LadyRosaDelTexas): - nextp = self.game.next_player() - i, j = self.game.players_map[self.name], self.game.players_map[nextp.name] - self.game.players[i], self.game.players[j] = nextp, self - self.game.players_map[self.name], self.game.players_map[nextp.name] = j, i - self.game.turn = j - self.game.notify_all() - elif self.is_my_turn and self.pending_action == PendingAction.PLAY and pile == 'event' and self.game.check_event(ce.Rimbalzo) and any((c.name == cs.Bang(0,0).name for c in self.hand)): - self.available_cards = [{ - 'name': p.name, - 'icon': p.role.icon if(self.game.initial_players == 3) else '⭐️' if isinstance(p.role, r.Sheriff) else '🤠', - 'is_character': True, - 'avatar': p.avatar, - 'is_player': True - } for p in self.game.get_alive_players() if len(p.equipment) > 0 and p != self] - self.available_cards.append({'icon': '❌', 'noDesc': True}) - self.choose_text = 'choose_rimbalzo_player' - self.pending_action = PendingAction.CHOOSE - self.using_rimbalzo = 1 - self.notify_self() + if ( + self.is_my_turn + and self.pending_action == PendingAction.PLAY + and pile == "event" + ): + self.game.deck.event_cards[0].on_clicked(self.game, self) + elif ( + self.is_my_turn + and self.pending_action == PendingAction.PLAY + and pile == "event_wildwestshow" + ): + self.game.deck.event_cards_wildwestshow[0].on_clicked(self.game, self) if self.pending_action != PendingAction.DRAW: return - if pile == 'event' and self.lives < self.max_lives and self.game.check_event(ce.LiquoreForte): + if ( + pile == "event" + and self.lives < self.max_lives + and self.game.check_event(ce.LiquoreForte) + ): self.lives += 1 self.pending_action = PendingAction.PLAY self.notify_self() elif self.character.check(self.game, chars.KitCarlson) and not self.is_ghost: self.is_drawing = True self.available_cards = [self.game.deck.draw() for i in range(3)] - self.choose_text = 'choose_card_to_get' + self.choose_text = "choose_card_to_get" self.pending_action = PendingAction.CHOOSE self.notify_self() elif self.character.check(self.game, grch.DutchWill) and not self.is_ghost: self.is_drawing = True self.available_cards = [self.game.deck.draw() for i in range(2)] - self.choose_text = 'choose_card_to_get' + self.choose_text = "choose_card_to_get" self.pending_action = PendingAction.CHOOSE self.notify_self() - elif self.character.check(self.game, chd.PatBrennan) and type(pile) == str and pile != self.name and pile in self.game.players_map and len(self.game.get_player_named(pile).equipment) > 0: + elif ( + self.character.check(self.game, chd.PatBrennan) + and type(pile) == str + and pile != self.name + and pile in self.game.players_map + and len(self.game.get_player_named(pile).equipment) > 0 + ): self.is_drawing = True self.available_cards = self.game.get_player_named(pile).equipment self.pat_target = pile - self.choose_text = 'choose_card_to_get' + self.choose_text = "choose_card_to_get" self.pending_action = PendingAction.CHOOSE self.notify_self() else: @@ -583,34 +891,90 @@ class Player: hsize = len(self.hand) for p in self.game.get_alive_players(): if p != self and len(p.hand) > hsize: - G.sio.emit('card_drawn', room=self.game.name, data={'player': self.name, 'pile': p.name}) - self.hand.append(p.hand.pop(randint(0, len(p.hand)-1))) - num = 2 if not self.character.check(self.game, chd.BillNoface) else self.max_lives-self.lives+1 - if self.character.check(self.game, chd.PixiePete): num += 1 - if self.character.check(self.game, tvosch.TucoFranziskaner) and not any((True for c in self.equipment if not c.usable_next_turn)): num += 2 - if (any((isinstance(c, grc.Piccone) for c in self.gold_rush_equipment))): num += 1 - if self.game.check_event(ceh.IlTreno) or (self.is_ghost and self.game.check_event(ceh.CittaFantasma)): num += 1 - elif self.game.check_event(ceh.Sete): num -= 1 + G.sio.emit( + "card_drawn", + room=self.game.name, + data={"player": self.name, "pile": p.name}, + ) + self.hand.append(p.hand.pop(randint(0, len(p.hand) - 1))) + num = ( + 2 + if not self.character.check(self.game, chd.BillNoface) + else self.max_lives - self.lives + 1 + ) + if self.character.check(self.game, chd.PixiePete): + num += 1 + if self.character.check(self.game, tvosch.TucoFranziskaner) and not any( + (True for c in self.equipment if not c.usable_next_turn) + ): + num += 2 + if any((isinstance(c, grc.Piccone) for c in self.gold_rush_equipment)): + num += 1 + if self.game.check_event(ceh.IlTreno) or ( + self.is_ghost and self.game.check_event(ceh.CittaFantasma) + ): + num += 1 + elif self.game.check_event(ceh.Sete): + num -= 1 for i in range(num): - if i == 0 and pile == 'scrap' and self.character.check(self.game, chars.PedroRamirez): + if ( + i == 0 + and pile == "scrap" + and self.character.check(self.game, chars.PedroRamirez) + ): self.hand.append(self.game.deck.draw_from_scrap_pile()) - G.sio.emit('chat_message', room=self.game.name, data=f'_draw_from_scrap|{self.name}') - elif i == 0 and type(pile) == str and pile != self.name and pile in self.game.players_map and self.character.check(self.game, chars.JesseJones) and len(self.game.get_player_named(pile).hand) > 0: - self.hand.append(self.game.get_player_named(pile).hand.pop( randrange(0, len(self.game.get_player_named(pile).hand)))) + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_draw_from_scrap|{self.name}", + ) + elif ( + i == 0 + and type(pile) == str + and pile != self.name + and pile in self.game.players_map + and self.character.check(self.game, chars.JesseJones) + and len(self.game.get_player_named(pile).hand) > 0 + ): + self.hand.append( + self.game.get_player_named(pile).hand.pop( + randrange(0, len(self.game.get_player_named(pile).hand)) + ) + ) self.game.get_player_named(pile).notify_self() - G.sio.emit('chat_message', room=self.game.name, data=f'_draw_from_player|{self.name}|{pile}') + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_draw_from_player|{self.name}|{pile}", + ) elif i == 1: card: cs.Card = self.game.deck.draw() - if (self.character.check(self.game, chars.BlackJack) or self.game.check_event(ce.LeggeDelWest)): + if self.character.check( + self.game, chars.BlackJack + ) or self.game.check_event(ce.LeggeDelWest): for p in self.game.get_alive_players(): if p != self: - p.notify_card(self, card, 'blackjack_special' if self.character.check(self.game, chars.BlackJack) else 'foc.leggedelwest') + p.notify_card( + self, + card, + "blackjack_special" + if self.character.check(self.game, chars.BlackJack) + else "foc.leggedelwest", + ) if self.game.check_event(ce.LeggeDelWest): card.must_be_used = True - if self.character.check(self.game, chars.BlackJack) and card.check_suit(self.game, [cs.Suit.HEARTS, cs.Suit.DIAMONDS]): + if self.character.check( + self.game, chars.BlackJack + ) and card.check_suit( + self.game, [cs.Suit.HEARTS, cs.Suit.DIAMONDS] + ): self.game.deck.draw(player=self) self.hand.append(card) - G.sio.emit('card_drawn', room=self.game.name, data={'player': self.name, 'pile': pile}) + G.sio.emit( + "card_drawn", + room=self.game.name, + data={"player": self.name, "pile": pile}, + ) else: self.game.deck.draw(player=self) self.manette() @@ -618,13 +982,11 @@ class Player: def manette(self): if self.game.check_event(ceh.Manette): - self.choose_text = 'choose_manette' - self.available_cards = [{ - 'name': '', - 'icon': '♦♣♥♠'[s], - 'alt_text': '', - 'noDesc': True - } for s in [0,1,2,3]] + self.choose_text = "choose_manette" + self.available_cards = [ + {"name": "", "icon": "♦♣♥♠"[s], "alt_text": "", "noDesc": True} + for s in [0, 1, 2, 3] + ] self.pending_action = PendingAction.CHOOSE def pick(self): @@ -635,38 +997,79 @@ class Player: pickable_cards += 1 if self.is_my_turn and self.attacker is None: for i in range(len(self.equipment)): - if i < len(self.equipment) and isinstance(self.equipment[i], cs.Dinamite): + if i < len(self.equipment) and isinstance( + self.equipment[i], cs.Dinamite + ): while pickable_cards > 0: pickable_cards -= 1 picked: cs.Card = self.game.deck.pick_and_scrap() - print(f'Did pick {picked}') - G.sio.emit('chat_message', room=self.game.name, - data=f'_flipped|{self.name}|{picked.name}|{picked.num_suit()}') - if picked.check_suit(self.game, [cs.Suit.SPADES]) and 2 <= picked.number <= 9 and pickable_cards == 0: + print(f"Did pick {picked}") + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_flipped|{self.name}|{picked.name}|{picked.num_suit()}", + ) + if ( + picked.check_suit(self.game, [cs.Suit.SPADES]) + and 2 <= picked.number <= 9 + and pickable_cards == 0 + ): self.lives -= 3 - if any((isinstance(c, grc.Talismano) for c in self.gold_rush_equipment)): + if any( + ( + isinstance(c, grc.Talismano) + for c in self.gold_rush_equipment + ) + ): self.gold_nuggets += 3 if self.character.check(self.game, grch.SimeonPicos): self.gold_nuggets += 3 - if any((isinstance(c, grc.Stivali) for c in self.gold_rush_equipment)): + if any( + ( + isinstance(c, grc.Stivali) + for c in self.gold_rush_equipment + ) + ): self.game.deck.draw(player=self) self.game.deck.draw(player=self) self.game.deck.draw(player=self) self.attacker = None - self.game.deck.scrap(self.equipment.pop(i), True, player=self) - G.sio.emit('chat_message', room=self.game.name, data=f'_explode|{self.name}') + self.game.deck.scrap( + self.equipment.pop(i), True, player=self + ) + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_explode|{self.name}", + ) self.heal_if_needed() - if self.character.check(self.game, chars.BartCassidy) and self.lives > 0: + if ( + self.character.check(self.game, chars.BartCassidy) + and self.lives > 0 + ): for i in range(3): self.game.deck.draw(True, player=self) - G.sio.emit('chat_message', room=self.game.name, data=f'_special_bart_cassidy|{self.name}') - print(f'{self.name} Boom, -3 hp') + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_special_bart_cassidy|{self.name}", + ) + print(f"{self.name} Boom, -3 hp") break else: - self.game.next_player().equipment.append(self.equipment.pop(i)) + self.game.next_player().equipment.append( + self.equipment.pop(i) + ) self.game.next_player().notify_self() break - if any((isinstance(c, cs.Dinamite) or isinstance(c, cs.Prigione) or isinstance(c, tvosc.SerpenteASonagli) for c in self.equipment)): + if any( + ( + isinstance(c, cs.Dinamite) + or isinstance(c, cs.Prigione) + or isinstance(c, tvosc.SerpenteASonagli) + for c in self.equipment + ) + ): self.notify_self() return for i in range(len(self.equipment)): @@ -674,17 +1077,35 @@ class Player: while pickable_cards > 0: pickable_cards -= 1 picked: cs.Card = self.game.deck.pick_and_scrap() - print(f'Did pick {picked}') - G.sio.emit('chat_message', room=self.game.name, - data=f'_flipped|{self.name}|{picked.name}|{picked.num_suit()}') - if not picked.check_suit(self.game, [cs.Suit.HEARTS]) and pickable_cards == 0: - self.game.deck.scrap(self.equipment.pop(i), True, player=self) - G.sio.emit('chat_message', room=self.game.name, data=f'_prison_turn|{self.name}') + print(f"Did pick {picked}") + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_flipped|{self.name}|{picked.name}|{picked.num_suit()}", + ) + if ( + not picked.check_suit(self.game, [cs.Suit.HEARTS]) + and pickable_cards == 0 + ): + self.game.deck.scrap( + self.equipment.pop(i), True, player=self + ) + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_prison_turn|{self.name}", + ) self.end_turn(forced=True) return elif pickable_cards == 0: - self.game.deck.scrap(self.equipment.pop(i), True, player=self) - G.sio.emit('chat_message', room=self.game.name, data=f'_prison_free|{self.name}') + self.game.deck.scrap( + self.equipment.pop(i), True, player=self + ) + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_prison_free|{self.name}", + ) break break for i in range(len(self.equipment)): @@ -692,29 +1113,52 @@ class Player: while pickable_cards > 0: pickable_cards -= 1 picked: cs.Card = self.game.deck.pick_and_scrap() - print(f'Did pick {picked}') - G.sio.emit('chat_message', room=self.game.name, - data=f'_flipped|{self.name}|{picked.name}|{picked.num_suit()}') + print(f"Did pick {picked}") + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_flipped|{self.name}|{picked.name}|{picked.num_suit()}", + ) if not picked.check_suit(self.game, [cs.Suit.SPADES]): break elif pickable_cards == 0: self.lives -= 1 - G.sio.emit('chat_message', room=self.game.name, data=f'_snake_bit|{self.name}') + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_snake_bit|{self.name}", + ) if self.character.check(self.game, chars.BartCassidy): - G.sio.emit('chat_message', room=self.game.name, data=f'_special_bart_cassidy|{self.name}') + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_special_bart_cassidy|{self.name}", + ) self.game.deck.draw(True, player=self) - if any((isinstance(c, grc.Talismano) for c in self.gold_rush_equipment)): + if any( + ( + isinstance(c, grc.Talismano) + for c in self.gold_rush_equipment + ) + ): self.gold_nuggets += 1 if self.character.check(self.game, grch.SimeonPicos): self.gold_nuggets += 1 - if any((isinstance(c, grc.Stivali) for c in self.gold_rush_equipment)): + if any( + ( + isinstance(c, grc.Stivali) + for c in self.gold_rush_equipment + ) + ): self.game.deck.draw(True, player=self) break if any((isinstance(c, cs.Prigione) for c in self.equipment)): - self.notify_self() #TODO perchè solo le prigioni? e multiple dinamiti come si comportano con veracuster? + self.notify_self() # TODO perchè solo le prigioni? e multiple dinamiti come si comportano con veracuster? return if isinstance(self.real_character, chd.VeraCuster): - self.set_available_character([p.character for p in self.game.get_alive_players() if p != self]) + self.set_available_character( + [p.character for p in self.game.get_alive_players() if p != self] + ) else: self.pending_action = PendingAction.DRAW self.notify_self() @@ -726,7 +1170,11 @@ class Player: playable_cards = [] for i in range(len(self.hand)): card = self.hand[i] - if isinstance(card, cs.Bang) and self.has_played_bang and not any((isinstance(c, cs.Volcanic) for c in self.equipment)): + if ( + isinstance(card, cs.Bang) + and self.has_played_bang + and not any((isinstance(c, cs.Volcanic) for c in self.equipment)) + ): continue elif isinstance(card, cs.Birra) and self.lives >= self.max_lives: continue @@ -736,98 +1184,207 @@ class Player: def play_card(self, hand_index: int, against=None, _with=None): if self.is_bot: - data = { - "index": hand_index, - "against": against, - "with": _with - } - self.game.rpc_log.append(f'{self.name};play_card;{json.dumps(data)}') - print(self.name, 'wants to play card ', hand_index, ' against:', against, ' with:', _with) - if not self.is_my_turn or self.pending_action != PendingAction.PLAY or self.game.is_handling_death: - print('but cannot') + data = {"index": hand_index, "against": against, "with": _with} + self.game.rpc_log.append(f"{self.name};play_card;{json.dumps(data)}") + print( + self.name, + "wants to play card ", + hand_index, + " against:", + against, + " with:", + _with, + ) + if ( + not self.is_my_turn + or self.pending_action != PendingAction.PLAY + or self.game.is_handling_death + ): + print("but cannot") return - if not (0 <= hand_index < len(self.hand) + len(self.equipment) + len(self.gold_rush_equipment)): - print('but the card index is out of range') + if not ( + 0 + <= hand_index + < len(self.hand) + len(self.equipment) + len(self.gold_rush_equipment) + ): + print("but the card index is out of range") return - elif len(self.hand) + len(self.equipment) <= hand_index < len(self.hand) + len(self.equipment) + len(self.gold_rush_equipment) and len(self.gold_rush_equipment): - print('which is a gold rush black card') - card: grc.ShopCard = self.gold_rush_equipment[hand_index - len(self.hand) - len(self.equipment)] + elif len(self.hand) + len(self.equipment) <= hand_index < len(self.hand) + len( + self.equipment + ) + len(self.gold_rush_equipment) and len(self.gold_rush_equipment): + print("which is a gold rush black card") + card: grc.ShopCard = self.gold_rush_equipment[ + hand_index - len(self.hand) - len(self.equipment) + ] return card.play_card(self) from_hand = hand_index < len(self.hand) - card: cs.Card = self.hand.pop(hand_index) if hand_index < len(self.hand) else self.equipment.pop(hand_index-len(self.hand)) + card: cs.Card = ( + self.hand.pop(hand_index) + if hand_index < len(self.hand) + else self.equipment.pop(hand_index - len(self.hand)) + ) withCard: cs.Card = None if _with is not None: - withCard = self.hand.pop(_with) if hand_index > _with else self.hand.pop(_with - 1) - print(self.name, 'is playing ', card, ' against:', against, ' with:', _with) + withCard = ( + self.hand.pop(_with) if hand_index > _with else self.hand.pop(_with - 1) + ) + print(self.name, "is playing ", card, " against:", against, " with:", _with) did_play_card = False - event_blocks_card = (self.game.check_event(ce.IlGiudice) and (card.is_equipment or (card.usable_next_turn and not card.can_be_used_now))) or (self.game.check_event(ce.Lazo) and card.usable_next_turn and card.can_be_used_now) or ((self.game.check_event(ceh.Manette) and card.suit != self.committed_suit_manette)) - if not(against is not None and (self.game.get_player_named(against).character.check(self.game, chd.ApacheKid) or any((isinstance(c, grc.Calumet) for c in self.game.get_player_named(against).gold_rush_equipment))) and card.check_suit(self.game, [cs.Suit.DIAMONDS])) or (isinstance(card, grc.ShopCard) and card.kind == grc.ShopCardKind.BLACK) and not event_blocks_card: - if (against == self.name and not isinstance(card, csd.Tequila) and not isinstance(card, cs.Panico) and not isinstance(card, cs.CatBalou)) or event_blocks_card: + event_blocks_card = ( + ( + self.game.check_event(ce.IlGiudice) + and ( + card.is_equipment + or (card.usable_next_turn and not card.can_be_used_now) + ) + ) + or ( + self.game.check_event(ce.Lazo) + and card.usable_next_turn + and card.can_be_used_now + ) + or ( + ( + self.game.check_event(ceh.Manette) + and card.suit != self.committed_suit_manette + ) + ) + ) + if ( + not ( + against is not None + and ( + self.game.get_player_named(against).character.check( + self.game, chd.ApacheKid + ) + or any( + ( + isinstance(c, grc.Calumet) + for c in self.game.get_player_named( + against + ).gold_rush_equipment + ) + ) + ) + and card.check_suit(self.game, [cs.Suit.DIAMONDS]) + ) + or (isinstance(card, grc.ShopCard) and card.kind == grc.ShopCardKind.BLACK) + and not event_blocks_card + ): + if ( + against == self.name + and not isinstance(card, csd.Tequila) + and not isinstance(card, cs.Panico) + and not isinstance(card, cs.CatBalou) + ) or event_blocks_card: did_play_card = False else: did_play_card = card.play_card(self, against, withCard) - if not card.is_equipment and not card.usable_next_turn and not (isinstance(card, grc.ShopCard) and card.kind == grc.ShopCardKind.BLACK) or (event_blocks_card and not (card.usable_next_turn and card.can_be_used_now)): + if ( + not card.is_equipment + and not card.usable_next_turn + and not ( + isinstance(card, grc.ShopCard) and card.kind == grc.ShopCardKind.BLACK + ) + or ( + event_blocks_card + and not (card.usable_next_turn and card.can_be_used_now) + ) + ): if did_play_card: self.game.deck.scrap(card, True) else: self.hand.insert(hand_index, card) if withCard: self.hand.insert(_with, withCard) - G.sio.emit('cant_play_card', room=self.sid) - elif (card.usable_next_turn and card.can_be_used_now) or (isinstance(card, grc.ShopCard) and card.kind == grc.ShopCardKind.BLACK) or event_blocks_card: + G.sio.emit("cant_play_card", room=self.sid) + elif ( + (card.usable_next_turn and card.can_be_used_now) + or (isinstance(card, grc.ShopCard) and card.kind == grc.ShopCardKind.BLACK) + or event_blocks_card + ): if did_play_card: self.game.deck.scrap(card, True) else: - self.equipment.insert(hand_index-len(self.hand), card) + self.equipment.insert(hand_index - len(self.hand), card) elif card.is_equipment or (card.usable_next_turn and not card.can_be_used_now): if not did_play_card: if from_hand: self.hand.insert(hand_index, card) else: - self.equipment.insert(hand_index-len(self.hand), card) + self.equipment.insert(hand_index - len(self.hand), card) else: did_play_card = True if not self.game.is_replay: - Metrics.send_metric('play_card', points=[1], tags=[f'success:{did_play_card}', f'card:{card.name}', f'bot:{self.is_bot}', f'exp:{card.expansion if "expansion" in card.__dict__ else "vanilla"}']) + Metrics.send_metric( + "play_card", + points=[1], + tags=[ + f"success:{did_play_card}", + f"card:{card.name}", + f"bot:{self.is_bot}", + f'exp:{card.expansion if "expansion" in card.__dict__ else "vanilla"}', + ], + ) print("did play card:", did_play_card) if did_play_card: self.played_cards += 1 self.notify_self() if self.is_bot: - return did_play_card or card.is_equipment or (card.usable_next_turn and not card.can_be_used_now) + return ( + did_play_card + or card.is_equipment + or (card.usable_next_turn and not card.can_be_used_now) + ) def choose(self, card_index): if self.pending_action != PendingAction.CHOOSE: return - if self.target_p and self.target_p != '': # panico, cat balou, rissa + if self.target_p and self.target_p != "": # panico, cat balou, rissa target = self.game.get_player_named(self.target_p) card = None - if (target.name == self.name): - card = self.equipment.pop(card_index if card_index < len(target.hand) else card_index - len(target.hand)) + if target.name == self.name: + card = self.equipment.pop( + card_index + if card_index < len(target.hand) + else card_index - len(target.hand) + ) elif card_index >= len(target.hand): card = target.equipment.pop(card_index - len(target.hand)) else: card = target.hand.pop(card_index) target.notify_self() - if self.choose_action == 'steal': - G.sio.emit('card_drawn', room=self.game.name, data={'player': self.name, 'pile': target.name}) + if self.choose_action == "steal": + G.sio.emit( + "card_drawn", + room=self.game.name, + data={"player": self.name, "pile": target.name}, + ) card.reset_card() - if card.name != "Fantasma" or self.name != target.name: #se si uccide facendo panico su fantasma la carta non gli viene messa in mano + if ( + card.name != "Fantasma" or self.name != target.name + ): # se si uccide facendo panico su fantasma la carta non gli viene messa in mano self.hand.append(card) else: self.game.deck.scrap(card, True, player=target) - if self.event_type != 'rissa' or len(self.rissa_targets) == 0: - self.event_type = '' - self.target_p = '' - self.choose_action = '' + if self.event_type != "rissa" or len(self.rissa_targets) == 0: + self.event_type = "" + self.target_p = "" + self.choose_action = "" self.pending_action = PendingAction.PLAY else: self.target_p = self.rissa_targets.pop(0).name - print(f'rissa targets: {self.rissa_targets}') + print(f"rissa targets: {self.rissa_targets}") self.notify_self() - elif self.choose_text == 'choose_ricercato': - player = self.game.get_player_named(self.available_cards[card_index]['name']) - G.sio.emit('chat_message', room=player.game.name, data=f'_play_card_against|{self.name}|Ricercato|{player.name}') + elif self.choose_text == "choose_ricercato": + player = self.game.get_player_named( + self.available_cards[card_index]["name"] + ) + G.sio.emit( + "chat_message", + room=player.game.name, + data=f"_play_card_against|{self.name}|Ricercato|{player.name}", + ) if any((isinstance(c, grc.Ricercato) for c in player.gold_rush_equipment)): self.game.deck.shop_deck.append(grc.Ricercato()) else: @@ -835,91 +1392,135 @@ class Player: player.notify_self() self.pending_action = PendingAction.PLAY self.notify_self() - elif self.choose_text == 'choose_sid_scrap': + elif self.choose_text == "choose_sid_scrap": self.scrapped_cards += 1 self.game.deck.scrap(self.hand.pop(card_index), True, player=self) if self.scrapped_cards == 2: self.available_cards = [] self.pending_action = self.previous_pending_action if self.game.players[self.game.turn] != self: - self.game.players[self.game.turn].pending_action = PendingAction.PLAY + self.game.players[ + self.game.turn + ].pending_action = PendingAction.PLAY self.game.players[self.game.turn].notify_self() self.notify_self() - elif self.choose_text == 'choose_bicchierino': - player = self.game.get_player_named(self.available_cards[card_index]['name']) - G.sio.emit('chat_message', room=self.game.name, data=f'_play_card_for|{self.name}|{"Bicchierino"}|{player.name}') - player.lives = min(player.lives+1, player.max_lives) + elif self.choose_text == "choose_bicchierino": + player = self.game.get_player_named( + self.available_cards[card_index]["name"] + ) + G.sio.emit( + "chat_message", + room=self.game.name, + data=f'_play_card_for|{self.name}|{"Bicchierino"}|{player.name}', + ) + player.lives = min(player.lives + 1, player.max_lives) self.pending_action = PendingAction.PLAY self.notify_self() - elif self.choose_text == 'choose_birra_function': + elif self.choose_text == "choose_birra_function": if card_index == 0: self.gold_nuggets += 1 - G.sio.emit('chat_message', room=self.game.name, data=f'_get_nugget|{self.name}') + G.sio.emit( + "chat_message", room=self.game.name, data=f"_get_nugget|{self.name}" + ) else: - cs.Birra(1,1).play_card(self, skipChecks=True) + cs.Birra(1, 1).play_card(self, skipChecks=True) self.pending_action = PendingAction.PLAY self.notify_self() - elif self.choose_text == 'choose_bottiglia': - G.sio.emit('chat_message', room=self.game.name, data=f'_play_card|{self.name}|{"Bottiglia"}') + elif self.choose_text == "choose_bottiglia": + G.sio.emit( + "chat_message", + room=self.game.name, + data=f'_play_card|{self.name}|{"Bottiglia"}', + ) if isinstance(self.available_cards[card_index], cs.Birra): - self.lives = min(self.lives+1, self.max_lives) + self.lives = min(self.lives + 1, self.max_lives) else: self.hand.append(self.available_cards[card_index]) self.pending_action = PendingAction.PLAY self.notify_self() - elif self.choose_text == 'choose_complice': - G.sio.emit('chat_message', room=self.game.name, data=f'_play_card|{self.name}|{"Bottiglia"}') + elif self.choose_text == "choose_complice": + G.sio.emit( + "chat_message", + room=self.game.name, + data=f'_play_card|{self.name}|{"Bottiglia"}', + ) self.hand.append(self.available_cards[card_index]) self.pending_action = PendingAction.PLAY self.notify_self() - elif self.choose_text == 'gold_rush_discard': + elif self.choose_text == "gold_rush_discard": if card_index == len(self.available_cards) - 1: self.pending_action = PendingAction.PLAY else: - player = self.game.get_player_named(self.available_cards[card_index]['name']) - self.available_cards = [c for c in player.gold_rush_equipment if c.number+1 <= self.gold_nuggets] - self.available_cards.append({'icon': '❌', 'noDesc': True}) - self.choose_text = 'gold_rush_discard_2|' + player.name + player = self.game.get_player_named( + self.available_cards[card_index]["name"] + ) + self.available_cards = [ + c + for c in player.gold_rush_equipment + if c.number + 1 <= self.gold_nuggets + ] + self.available_cards.append({"icon": "❌", "noDesc": True}) + self.choose_text = "gold_rush_discard_2|" + player.name self.notify_self() - elif 'gold_rush_discard_2' in self.choose_text: + elif "gold_rush_discard_2" in self.choose_text: if card_index == len(self.available_cards) - 1: self.pending_action = PendingAction.PLAY else: self.gold_nuggets -= self.available_cards[card_index].number + 1 - player = self.game.get_player_named(self.choose_text.split('|')[1]) + player = self.game.get_player_named(self.choose_text.split("|")[1]) player.gold_rush_equipment.remove(self.available_cards[card_index]) self.game.deck.shop_deck.append(self.available_cards[card_index]) - G.sio.emit('chat_message', room=self.game.name, data=f'_gold_rush_pay_discard|{self.name}|{player.name}|{self.available_cards[card_index].name}') + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_gold_rush_pay_discard|{self.name}|{player.name}|{self.available_cards[card_index].name}", + ) player.notify_self() self.pending_action = PendingAction.PLAY self.notify_self() - elif 'choose_fantasma' in self.choose_text: + elif "choose_fantasma" in self.choose_text: if card_index <= len(self.available_cards): - player = self.game.get_player_named(self.available_cards[card_index]['name']) + player = self.game.get_player_named( + self.available_cards[card_index]["name"] + ) player.equipment.append(self.game.deck.scrap_pile.pop(-1)) player.notify_self() self.game.notify_all() - G.sio.emit('chat_message', room=player.game.name, data=f'_play_card_against|{self.name}|Fantasma|{player.name}') + G.sio.emit( + "chat_message", + room=player.game.name, + data=f"_play_card_against|{self.name}|Fantasma|{player.name}", + ) self.pending_action = PendingAction.PLAY self.notify_self() - elif 'choose_sventagliata' in self.choose_text: + elif "choose_sventagliata" in self.choose_text: if card_index <= len(self.available_cards): - og = self.available_cards[card_index]['original_target'] - player = self.game.get_player_named(self.available_cards[card_index]['name']) - player.game.attack(self, og, card_name='Sventagliata') - player.game.attack(self, player.name, card_name='Sventagliata') - G.sio.emit('chat_message', room=player.game.name, data=f'_play_card_against|{self.name}|Sventagliata|{og}') - G.sio.emit('chat_message', room=player.game.name, data=f'_play_card_against|{self.name}|Sventagliata|{player.name}') + og = self.available_cards[card_index]["original_target"] + player = self.game.get_player_named( + self.available_cards[card_index]["name"] + ) + player.game.attack(self, og, card_name="Sventagliata") + player.game.attack(self, player.name, card_name="Sventagliata") + G.sio.emit( + "chat_message", + room=player.game.name, + data=f"_play_card_against|{self.name}|Sventagliata|{og}", + ) + G.sio.emit( + "chat_message", + room=player.game.name, + data=f"_play_card_against|{self.name}|Sventagliata|{player.name}", + ) self.pending_action = PendingAction.PLAY self.notify_self() - elif 'choose_play_as_bang' in self.choose_text: + elif "choose_play_as_bang" in self.choose_text: if card_index <= len(self.available_cards): self.hand.remove(self.available_cards[card_index]) self.game.deck.scrap(self.available_cards[card_index], player=self) self.hand.append(cs.Bang(self.available_cards[card_index].suit, 42)) self.pending_action = PendingAction.PLAY self.notify_self() - elif 'choose_tornado' in self.choose_text: + elif "choose_tornado" in self.choose_text: if card_index <= len(self.available_cards): self.game.deck.scrap(self.hand.pop(card_index), player=self) self.game.deck.draw(player=self) @@ -927,22 +1528,23 @@ class Player: self.pending_action = PendingAction.WAIT self.game.responders_did_respond_resume_turn() self.notify_self() - elif 'choose_poker' in self.choose_text: + elif "choose_poker" in self.choose_text: if card_index <= len(self.available_cards): self.game.deck.scrap(self.hand.pop(card_index), player=self) self.pending_action = PendingAction.WAIT self.game.responders_did_respond_resume_turn() self.notify_self() - elif 'choose_from_poker' in self.choose_text: - st_idx = len(self.game.deck.scrap_pile)-len(self.available_cards) + elif "choose_from_poker" in self.choose_text: + st_idx = len(self.game.deck.scrap_pile) - len(self.available_cards) self.available_cards.pop(card_index) self.hand.append(self.game.deck.scrap_pile.pop(st_idx + card_index)) self.game.notify_scrap_pile() - if self.choose_text.split(';')[1] == '1': + if self.choose_text.split(";")[1] == "1": self.pending_action = PendingAction.PLAY - else: self.choose_text = 'choose_from_poker;1' + else: + self.choose_text = "choose_from_poker;1" self.notify_self() - elif 'choose_bandidos' in self.choose_text: + elif "choose_bandidos" in self.choose_text: if card_index < len(self.hand): self.available_cards.pop(card_index) self.game.deck.scrap(self.hand.pop(card_index), player=self) @@ -954,70 +1556,111 @@ class Player: self.pending_action = PendingAction.WAIT self.game.responders_did_respond_resume_turn() self.notify_self() - elif 'choose_flint_special' == self.choose_text: + elif "choose_flint_special" == self.choose_text: if card_index < len(self.hand): - self.available_cards = [{ - 'name': p.name, - 'icon': p.role.icon if(self.game.initial_players == 3) else '⭐️' if p['is_sheriff'] else '🤠', - 'avatar': p.avatar, - 'is_character': True, - 'is_player': True - } for p in self.game.get_alive_players() if p.name != self.name and len(p.hand) > 0] - self.choose_text = f'choose_flint_special;{card_index}' + self.available_cards = [ + { + "name": p.name, + "icon": p.role.icon + if (self.game.initial_players == 3) + else "⭐️" + if p["is_sheriff"] + else "🤠", + "avatar": p.avatar, + "is_character": True, + "is_player": True, + } + for p in self.game.get_alive_players() + if p.name != self.name and len(p.hand) > 0 + ] + self.choose_text = f"choose_flint_special;{card_index}" self.notify_self() - elif 'choose_flint_special' in self.choose_text: + elif "choose_flint_special" in self.choose_text: if card_index < len(self.available_cards): - my_card = self.hand.pop(int(self.choose_text.split(';')[1])) - other_player = self.game.get_player_named(self.available_cards[card_index]['name']) + my_card = self.hand.pop(int(self.choose_text.split(";")[1])) + other_player = self.game.get_player_named( + self.available_cards[card_index]["name"] + ) for i in range(min(2, len(other_player.hand))): - self.hand.append(other_player.hand.pop(randint(0, len(other_player.hand)-1))) + self.hand.append( + other_player.hand.pop(randint(0, len(other_player.hand) - 1)) + ) other_player.hand.append(my_card) other_player.notify_self() self.pending_action = PendingAction.PLAY self.notify_self() - elif self.game.check_event(ceh.NuovaIdentita) and self.choose_text == 'choose_nuova_identita': - if card_index == 1: # the other character + elif ( + self.game.check_event(ceh.NuovaIdentita) + and self.choose_text == "choose_nuova_identita" + ): + if card_index == 1: # the other character self.character = self.not_chosen_character self.real_character = self.character self.max_lives = self.character.max_lives + self.role.health_mod self.lives = 2 - G.sio.emit('chat_message', room=self.game.name, data=f'_choose_character|{self.name}|{self.character.name}') - self.play_turn(again = True) - elif self.game.check_event(ceh.Manette) and self.choose_text == 'choose_manette': + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_choose_character|{self.name}|{self.character.name}", + ) + self.play_turn(again=True) + elif ( + self.game.check_event(ceh.Manette) and self.choose_text == "choose_manette" + ): self.committed_suit_manette = cs.Suit(card_index) - G.sio.emit('chat_message', room=self.game.name, data=f'_choose_manette|{self.name}|{"♦♣♥♠"[card_index]}') + G.sio.emit( + "chat_message", + room=self.game.name, + data=f'_choose_manette|{self.name}|{"♦♣♥♠"[card_index]}', + ) self.pending_action = PendingAction.PLAY self.notify_self() elif self.is_giving_life and self.game.check_event(ce.FratelliDiSangue): try: - player = self.game.get_player_named(self.available_cards[card_index]['name']) + player = self.game.get_player_named( + self.available_cards[card_index]["name"] + ) player.lives += 1 self.lives -= 1 player.notify_self() - G.sio.emit('chat_message', room=self.game.name, data=f'_fratelli_sangue|{self.name}|{player.name}') - except: pass - self.play_turn(again = True) + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_fratelli_sangue|{self.name}|{player.name}", + ) + except: + pass + self.play_turn(again=True) elif self.is_using_checchino and self.game.check_event(ce.Cecchino): try: - if self.available_cards[card_index]['name'] != '': + if self.available_cards[card_index]["name"] != "": for _ in range(2): - card = next(c for c in self.hand if c.name == cs.Bang(0,0).name) + card = next( + c for c in self.hand if c.name == cs.Bang(0, 0).name + ) self.hand.remove(card) self.game.deck.scrap(card, True) self.pending_action = PendingAction.PLAY - self.game.attack(self, self.available_cards[card_index]['name'], double=True) + self.game.attack( + self, self.available_cards[card_index]["name"], double=True + ) except: self.pending_action = PendingAction.PLAY self.is_using_checchino = False self.notify_self() elif self.using_rimbalzo > 0 and self.game.check_event(ce.Rimbalzo): - if self.using_rimbalzo == 1 and 'name' in self.available_cards[card_index]: - self.rimbalzo_p = self.available_cards[card_index]['name'] - self.available_cards = self.game.get_player_named(self.available_cards[card_index]['name']).equipment - self.choose_text = 'choose_rimbalzo_card' + if self.using_rimbalzo == 1 and "name" in self.available_cards[card_index]: + self.rimbalzo_p = self.available_cards[card_index]["name"] + self.available_cards = self.game.get_player_named( + self.available_cards[card_index]["name"] + ).equipment + self.choose_text = "choose_rimbalzo_card" self.using_rimbalzo = 2 - elif self.using_rimbalzo == 2 and 'name' in self.available_cards[card_index].__dict__: - card = next(c for c in self.hand if c.name == cs.Bang(0,0).name) + elif ( + self.using_rimbalzo == 2 + and "name" in self.available_cards[card_index].__dict__ + ): + card = next(c for c in self.hand if c.name == cs.Bang(0, 0).name) self.hand.remove(card) self.game.deck.scrap(card, True) self.using_rimbalzo = 0 @@ -1026,7 +1669,7 @@ class Player: self.game.rimbalzo(self, self.rimbalzo_p, card_index) else: self.using_rimbalzo = 0 - self.rimbalzo_p = '' + self.rimbalzo_p = "" self.pending_action = PendingAction.PLAY self.notify_self() elif self.is_playing_ranch and self.game.check_event(ce.Ranch): @@ -1042,7 +1685,9 @@ class Player: self.discarded_cards.append(self.available_cards.pop(card_index)) self.notify_self() elif self.game.dalton_on and self.game.check_event(ceh.IDalton): - card = next(c for c in self.equipment if c == self.available_cards[card_index]) + card = next( + c for c in self.equipment if c == self.available_cards[card_index] + ) self.equipment.remove(card) self.game.deck.scrap(card, True, player=self) self.pending_action = PendingAction.WAIT @@ -1051,35 +1696,54 @@ class Player: elif self.is_drawing and self.game.check_event(ce.Peyote): self.is_drawing = False card = self.game.deck.draw() - G.sio.emit('chat_message', room=self.game.name, data=f"_guess|{self.name}|{self.available_cards[card_index]['icon']}") + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_guess|{self.name}|{self.available_cards[card_index]['icon']}", + ) self.available_cards = [] - if card_index == card.suit%2: + if card_index == card.suit % 2: self.hand.append(card) - G.sio.emit('chat_message', room=self.game.name, data=f"_guess_right|{self.name}") + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_guess_right|{self.name}", + ) self.pending_action = PendingAction.DRAW else: self.game.deck.scrap(card) - G.sio.emit('chat_message', room=self.game.name, data=f"_guess_wrong|{self.name}") + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_guess_wrong|{self.name}", + ) self.pending_action = PendingAction.PLAY self.notify_self() # specifico per personaggio elif self.is_drawing and self.character.check(self.game, chars.KitCarlson): card: cs.Card = self.available_cards.pop(card_index) - if len(self.available_cards) == 1: #ho pescato la seconda carta + if len(self.available_cards) == 1: # ho pescato la seconda carta if self.game.check_event(ce.LeggeDelWest): card.must_be_used = True self.hand.append(card) pickable_stop = 1 - if self.game.check_event(ceh.Sete): pickable_stop += 1 - if self.game.check_event(ceh.IlTreno) or any((isinstance(c, grc.Piccone) for c in self.gold_rush_equipment)): + if self.game.check_event(ceh.Sete): + pickable_stop += 1 + if self.game.check_event(ceh.IlTreno) or any( + (isinstance(c, grc.Piccone) for c in self.gold_rush_equipment) + ): pickable_stop -= 1 if len(self.available_cards) == pickable_stop: - if len(self.available_cards) > 0: #la carta non scelta la rimettiamo in cima al mazzo + if ( + len(self.available_cards) > 0 + ): # la carta non scelta la rimettiamo in cima al mazzo self.game.deck.put_on_top(self.available_cards.pop()) - if len(self.available_cards) > 0: #se sono rimaste carte le scartiamo + if len(self.available_cards) > 0: # se sono rimaste carte le scartiamo self.game.deck.scrap(self.available_cards.pop()) - #se c'è sia treno che piccone pesco un'altra carta - if self.game.check_event(ceh.IlTreno) and any((isinstance(c, grc.Piccone) for c in self.gold_rush_equipment)): + # se c'è sia treno che piccone pesco un'altra carta + if self.game.check_event(ceh.IlTreno) and any( + (isinstance(c, grc.Piccone) for c in self.gold_rush_equipment) + ): self.game.deck.draw(player=self) self.is_drawing = False self.pending_action = PendingAction.PLAY @@ -1088,11 +1752,15 @@ class Player: # specifico per personaggio elif self.is_drawing and self.character.check(self.game, grch.DutchWill): if not self.game.check_event(ceh.Sete): - self.hand.append(self.available_cards.pop(card_index)) #prendo la carta scelta + self.hand.append( + self.available_cards.pop(card_index) + ) # prendo la carta scelta else: - self.game.deck.scrap(self.available_cards.pop(0), True) #non pesco carte - self.game.deck.scrap(self.available_cards.pop(0), True) #scarto l'altra - #legge del west non si applica perchè la seconda carta viene scartata + self.game.deck.scrap( + self.available_cards.pop(0), True + ) # non pesco carte + self.game.deck.scrap(self.available_cards.pop(0), True) # scarto l'altra + # legge del west non si applica perchè la seconda carta viene scartata if self.game.check_event(ceh.IlTreno): self.game.deck.draw(player=self) if any((isinstance(c, grc.Piccone) for c in self.gold_rush_equipment)): @@ -1104,13 +1772,17 @@ class Player: self.notify_self() # specifico per personaggio elif self.is_drawing and self.character.check(self.game, chd.PatBrennan): - #non pesca per niente dal mazzo + # non pesca per niente dal mazzo self.is_drawing = False card = self.available_cards.pop(card_index) card.reset_card() self.hand.append(card) self.available_cards = [] - G.sio.emit('card_drawn', room=self.game.name, data={'player': self.name, 'pile': self.pat_target}) + G.sio.emit( + "card_drawn", + room=self.game.name, + data={"player": self.name, "pile": self.pat_target}, + ) self.game.get_player_named(self.pat_target).notify_self() self.pending_action = PendingAction.PLAY self.manette() @@ -1122,28 +1794,54 @@ class Player: pickable_cards = 1 + self.character.pick_mod if any((isinstance(c, grc.FerroDiCavallo) for c in self.gold_rush_equipment)): pickable_cards += 1 - if any((isinstance(c, cs.Barile) for c in self.equipment)) and self.character.check(self.game, chars.Jourdonnais): + if any( + (isinstance(c, cs.Barile) for c in self.equipment) + ) and self.character.check(self.game, chars.Jourdonnais): pickable_cards = 2 while pickable_cards > 0: pickable_cards -= 1 picked: cs.Card = self.game.deck.pick_and_scrap() - print(f'Did pick {picked}') - G.sio.emit('chat_message', room=self.game.name, - data=f'_flipped|{self.name}|{picked.name}|{picked.num_suit()}') + print(f"Did pick {picked}") + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_flipped|{self.name}|{picked.name}|{picked.num_suit()}", + ) if picked.check_suit(self.game, [cs.Suit.HEARTS]): self.mancato_needed -= 1 self.notify_self() if self.mancato_needed <= 0: self.game.responders_did_respond_resume_turn(did_lose=False) return - if not self.game.is_competitive and not any((isinstance(c, cs.Mancato) or (self.character.check(self.game, chars.CalamityJanet) and isinstance(c, cs.Bang)) or self.character.check(self.game, chd.ElenaFuente) for c in self.hand))\ - and not any((c.can_be_used_now and isinstance(c, cs.Mancato) for c in self.equipment)): + if ( + not self.game.is_competitive + and not any( + ( + isinstance(c, cs.Mancato) + or ( + self.character.check(self.game, chars.CalamityJanet) + and isinstance(c, cs.Bang) + ) + or self.character.check(self.game, chd.ElenaFuente) + for c in self.hand + ) + ) + and not any( + ( + c.can_be_used_now and isinstance(c, cs.Mancato) + for c in self.equipment + ) + ) + ): self.take_damage_response() self.game.responders_did_respond_resume_turn(did_lose=True) else: self.pending_action = PendingAction.RESPOND self.expected_response = self.game.deck.mancato_cards.copy() - if self.character.check(self.game, chars.CalamityJanet) and cs.Bang(0, 0).name not in self.expected_response: + if ( + self.character.check(self.game, chars.CalamityJanet) + and cs.Bang(0, 0).name not in self.expected_response + ): self.expected_response.append(cs.Bang(0, 0).name) if self.character.check(self.game, chw.BigSpencer): self.expected_response = [] @@ -1152,30 +1850,56 @@ class Player: def barrel_pick_no_dmg(self): pickable_cards = 1 + self.character.pick_mod - if any((isinstance(c, cs.Barile) for c in self.equipment)) and self.character.check(self.game, chars.Jourdonnais): + if any( + (isinstance(c, cs.Barile) for c in self.equipment) + ) and self.character.check(self.game, chars.Jourdonnais): pickable_cards = 2 if any((isinstance(c, grc.FerroDiCavallo) for c in self.gold_rush_equipment)): pickable_cards += 1 while pickable_cards > 0: pickable_cards -= 1 picked: cs.Card = self.game.deck.pick_and_scrap() - print(f'Did pick {picked}') - G.sio.emit('chat_message', room=self.game.name, - data=f'_flipped|{self.name}|{picked.name}|{picked.num_suit()}') + print(f"Did pick {picked}") + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_flipped|{self.name}|{picked.name}|{picked.num_suit()}", + ) if picked.check_suit(self.game, [cs.Suit.HEARTS]): self.mancato_needed -= 1 self.notify_self() if self.mancato_needed <= 0: self.game.responders_did_respond_resume_turn(did_lose=False) return - if not self.game.is_competitive and not any((isinstance(c, cs.Mancato) or (self.character.check(self.game, chars.CalamityJanet) and isinstance(c, cs.Bang)) or self.character.check(self.game, chd.ElenaFuente) for c in self.hand))\ - and not any((c.can_be_used_now and isinstance(c, cs.Mancato) for c in self.equipment)): + if ( + not self.game.is_competitive + and not any( + ( + isinstance(c, cs.Mancato) + or ( + self.character.check(self.game, chars.CalamityJanet) + and isinstance(c, cs.Bang) + ) + or self.character.check(self.game, chd.ElenaFuente) + for c in self.hand + ) + ) + and not any( + ( + c.can_be_used_now and isinstance(c, cs.Mancato) + for c in self.equipment + ) + ) + ): self.take_no_damage_response() self.game.responders_did_respond_resume_turn(did_lose=True) else: self.pending_action = PendingAction.RESPOND self.expected_response = self.game.deck.mancato_cards.copy() - if self.character.check(self.game, chars.CalamityJanet) and cs.Bang(0, 0).name not in self.expected_response: + if ( + self.character.check(self.game, chars.CalamityJanet) + and cs.Bang(0, 0).name not in self.expected_response + ): self.expected_response.append(cs.Bang(0, 0).name) if self.character.check(self.game, chw.BigSpencer): self.expected_response = [] @@ -1183,28 +1907,32 @@ class Player: self.notify_self() def get_discarded(self, attacker=None, card_name=None, action=None): - if card_name in {'Tornado', 'Poker', 'Bandidos'}: + if card_name in {"Tornado", "Poker", "Bandidos"}: self.pending_action = PendingAction.CHOOSE self.available_cards = self.hand.copy() - if card_name == 'Tornado': - self.choose_text = 'choose_tornado' - if card_name == 'Poker': - self.choose_text = 'choose_poker' - if card_name == 'Bandidos': - self.choose_text = 'choose_bandidos' + if card_name == "Tornado": + self.choose_text = "choose_tornado" + if card_name == "Poker": + self.choose_text = "choose_poker" + if card_name == "Bandidos": + self.choose_text = "choose_bandidos" self.mancato_needed = min(2, len(self.hand)) - self.available_cards.append({'name': '-1hp', 'icon': '💔', 'noDesc': True}) + self.available_cards.append( + {"name": "-1hp", "icon": "💔", "noDesc": True} + ) return True else: - if self.can_escape(card_name) or self.character.check(self.game, tvosch.MickDefender): + if self.can_escape(card_name) or self.character.check( + self.game, tvosch.MickDefender + ): self.pending_action = PendingAction.RESPOND self.mancato_needed = 1 self.attacker = attacker self.attacking_card = card_name - self.expected_response = ['Fuga'] + self.expected_response = ["Fuga"] if self.can_escape(with_mancato=True): self.expected_response.append(cs.Mancato(0, 0).name) - if action == 'steal': + if action == "steal": self.on_failed_response_cb = self.take_steal_response else: self.on_failed_response_cb = self.take_discard_response @@ -1214,29 +1942,44 @@ class Player: def take_steal_response(self): self.attacker.pending_action = PendingAction.CHOOSE self.attacker.target_p = self.name - self.attacker.choose_text = 'steal' + self.attacker.choose_text = "steal" self.take_no_damage_response() def take_discard_response(self): self.attacker.pending_action = PendingAction.CHOOSE self.attacker.target_p = self.name - self.attacker.choose_text = 'discard' + self.attacker.choose_text = "discard" self.take_no_damage_response() - - def can_escape(self, card_name:str=None, with_mancato:bool=False): - if card_name == 'Bang!' or card_name in self.game.deck.green_cards: + + def can_escape(self, card_name: str = None, with_mancato: bool = False): + if card_name == "Bang!" or card_name in self.game.deck.green_cards: return False if any((isinstance(c, tvosc.Fuga) for c in self.hand)) and not with_mancato: return True return with_mancato and self.character.check(self.game, tvosch.MickDefender) - def get_banged(self, attacker, double:bool=False, no_dmg:bool=False, card_index:int|None=None, card_name:str|None=None): + def get_banged( + self, + attacker, + double: bool = False, + no_dmg: bool = False, + card_index: int | None = None, + card_name: str | None = None, + ): self.attacker = attacker self.attacking_card = card_name - print(f'attacker -> {attacker}') - if isinstance(attacker, Player) and attacker.character.check(self.game, tvosch.ColoradoBill) and card_name == 'Bang!': + print(f"attacker -> {attacker}") + if ( + isinstance(attacker, Player) + and attacker.character.check(self.game, tvosch.ColoradoBill) + and card_name == "Bang!" + ): picked: cs.Card = self.game.deck.pick_and_scrap() - G.sio.emit('chat_message', room=self.game.name, data=f'_flipped|{attacker.name}|{picked.name}|{picked.num_suit()}') + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_flipped|{attacker.name}|{picked.name}|{picked.num_suit()}", + ) if picked.check_suit(self.game, [cs.Suit.SPADES]): self.take_damage_response() return False @@ -1247,33 +1990,74 @@ class Player: self.dmg_card_index = -1 for i in range(len(self.equipment)): if self.equipment[i].can_be_used_now: - print('usable', self.equipment[i]) - if (not self.game.is_competitive and not any((isinstance(c, cs.Barile) for c in self.equipment)) and not self.character.check(self.game, chars.Jourdonnais)\ - and not any(((isinstance(c, cs.Mancato) and c.can_be_used_now) or (self.character.check(self.game, chars.CalamityJanet) and isinstance(c, cs.Bang)) or self.character.check(self.game, chd.ElenaFuente) for c in self.hand))\ - and not any((c.can_be_used_now and isinstance(c, cs.Mancato) for c in self.equipment)) and not self.can_escape(card_name)) or card_name=='Mira': - print('Cant defend') + print("usable", self.equipment[i]) + if ( + not self.game.is_competitive + and not any((isinstance(c, cs.Barile) for c in self.equipment)) + and not self.character.check(self.game, chars.Jourdonnais) + and not any( + ( + (isinstance(c, cs.Mancato) and c.can_be_used_now) + or ( + self.character.check(self.game, chars.CalamityJanet) + and isinstance(c, cs.Bang) + ) + or self.character.check(self.game, chd.ElenaFuente) + for c in self.hand + ) + ) + and not any( + ( + c.can_be_used_now and isinstance(c, cs.Mancato) + for c in self.equipment + ) + ) + and not self.can_escape(card_name) + ) or card_name == "Mira": + print("Cant defend") if not no_dmg: self.take_damage_response() else: self.take_no_damage_response() return False else: - if ((not self.game.check_event(ce.Lazo) and any((isinstance(c, cs.Barile) for c in self.equipment))) \ - and not (self.game.players[self.game.turn].character.check(self.game, chd.BelleStar) and isinstance(attacker, Player))) \ - or self.character.check(self.game, chars.Jourdonnais): #se ho un barile e non c'è lazo e non mi sta attaccando Belle Star o se sono Jourdonnais - print('has barrel') + if ( + ( + not self.game.check_event(ce.Lazo) + and any((isinstance(c, cs.Barile) for c in self.equipment)) + ) + and not ( + self.game.players[self.game.turn].character.check( + self.game, chd.BelleStar + ) + and isinstance(attacker, Player) + ) + ) or self.character.check( + self.game, chars.Jourdonnais + ): # se ho un barile e non c'è lazo e non mi sta attaccando Belle Star o se sono Jourdonnais + print("has barrel") self.pending_action = PendingAction.PICK if not no_dmg: self.on_pick_cb = self.barrel_pick else: self.on_pick_cb = self.barrel_pick_no_dmg else: - print('has mancato') + print("has mancato") self.pending_action = PendingAction.RESPOND self.expected_response = self.game.deck.mancato_cards.copy() - if self.attacker and self.attacker in self.game.get_alive_players() and attacker.character.check(self.game, chd.BelleStar) or self.game.check_event(ce.Lazo): - self.expected_response = self.game.deck.mancato_cards_not_green_or_blue.copy() - if self.character.check(self.game, chars.CalamityJanet) and cs.Bang(0, 0).name not in self.expected_response: + if ( + self.attacker + and self.attacker in self.game.get_alive_players() + and attacker.character.check(self.game, chd.BelleStar) + or self.game.check_event(ce.Lazo) + ): + self.expected_response = ( + self.game.deck.mancato_cards_not_green_or_blue.copy() + ) + if ( + self.character.check(self.game, chars.CalamityJanet) + and cs.Bang(0, 0).name not in self.expected_response + ): self.expected_response.append(cs.Bang(0, 0).name) if self.character.check(self.game, chw.BigSpencer): self.expected_response = [] @@ -1290,7 +2074,7 @@ class Player: if len(equipments) == 0: return False else: - self.choose_text = 'choose_dalton' + self.choose_text = "choose_dalton" self.pending_action = PendingAction.CHOOSE self.available_cards = equipments return True @@ -1298,78 +2082,152 @@ class Player: def get_indians(self, attacker): self.attacker = attacker self.attacking_card = "Indiani!" - if self.character.check(self.game, chd.ApacheKid) or any((isinstance(c, grc.Calumet) for c in self.gold_rush_equipment)): return False - if not self.game.is_competitive and not any((isinstance(c, cs.Bang) or (self.character.check(self.game, chars.CalamityJanet) and isinstance(c, cs.Mancato)) for c in self.hand)) and not self.can_escape(): - print('Cant defend') + if self.character.check(self.game, chd.ApacheKid) or any( + (isinstance(c, grc.Calumet) for c in self.gold_rush_equipment) + ): + return False + if ( + not self.game.is_competitive + and not any( + ( + isinstance(c, cs.Bang) + or ( + self.character.check(self.game, chars.CalamityJanet) + and isinstance(c, cs.Mancato) + ) + for c in self.hand + ) + ) + and not self.can_escape() + ): + print("Cant defend") self.take_damage_response() return False else: - print('has bang') + print("has bang") self.pending_action = PendingAction.RESPOND self.expected_response = [cs.Bang(0, 0).name, tvosc.Fuga(0, 0).name] - if (self.character.check(self.game, chars.CalamityJanet) or self.can_escape(with_mancato=True)) and cs.Mancato(0, 0).name not in self.expected_response: + if ( + self.character.check(self.game, chars.CalamityJanet) + or self.can_escape(with_mancato=True) + ) and cs.Mancato(0, 0).name not in self.expected_response: self.expected_response.append(cs.Mancato(0, 0).name) - self.event_type = 'indians' + self.event_type = "indians" self.on_failed_response_cb = self.take_damage_response return True def get_dueled(self, attacker): self.attacker = attacker self.attacking_card = "Duello" - if (self.game.check_event(ceh.Sermone) and self.is_my_turn) or (not self.game.is_competitive and not any((isinstance(c, cs.Bang) or isinstance(c, tvosc.Fuga) or (self.character.check(self.game, chars.CalamityJanet) and isinstance(c, cs.Mancato)) for c in self.hand))): - print('Cant defend') + if (self.game.check_event(ceh.Sermone) and self.is_my_turn) or ( + not self.game.is_competitive + and not any( + ( + isinstance(c, cs.Bang) + or isinstance(c, tvosc.Fuga) + or ( + self.character.check(self.game, chars.CalamityJanet) + and isinstance(c, cs.Mancato) + ) + for c in self.hand + ) + ) + ): + print("Cant defend") self.take_damage_response() self.game.responders_did_respond_resume_turn(did_lose=True) return False else: self.pending_action = PendingAction.RESPOND self.expected_response = [cs.Bang(0, 0).name, tvosc.Fuga(0, 0).name] - if (self.character.check(self.game, chars.CalamityJanet) or self.can_escape(with_mancato=True)) and cs.Mancato(0, 0).name not in self.expected_response: + if ( + self.character.check(self.game, chars.CalamityJanet) + or self.can_escape(with_mancato=True) + ) and cs.Mancato(0, 0).name not in self.expected_response: self.expected_response.append(cs.Mancato(0, 0).name) - self.event_type = 'duel' + self.event_type = "duel" self.on_failed_response_cb = self.take_damage_response return True def heal_if_needed(self): - while self.lives <= 0 and len(self.game.get_alive_players()) > 2 and any((isinstance(c, cs.Birra) for c in self.hand)) and not self.game.check_event(ceh.IlReverendo): + while ( + self.lives <= 0 + and len(self.game.get_alive_players()) > 2 + and any((isinstance(c, cs.Birra) for c in self.hand)) + and not self.game.check_event(ceh.IlReverendo) + ): for i in range(len(self.hand)): if isinstance(self.hand[i], cs.Birra): - if self.character.check(self.game, chd.MollyStark) and not self.is_my_turn: + if ( + self.character.check(self.game, chd.MollyStark) + and not self.is_my_turn + ): self.game.deck.draw(True, player=self) - self.lives += 1 if not self.character.check(self.game, chd.TequilaJoe) else 2 + self.lives += ( + 1 if not self.character.check(self.game, chd.TequilaJoe) else 2 + ) self.lives = min(self.lives, self.max_lives) self.game.deck.scrap(self.hand.pop(i), True, player=self) - G.sio.emit('chat_message', room=self.game.name, - data=f'_beer_save|{self.name}') + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_beer_save|{self.name}", + ) break def take_damage_response(self): self.lives -= 1 - G.sio.emit('hurt', room=self.sid, data=f'') + G.sio.emit("hurt", room=self.sid, data=f"") if self.lives > 0: if self.character.check(self.game, chars.BartCassidy): - G.sio.emit('chat_message', room=self.game.name, - data=f'_special_bart_cassidy|{self.name}') + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_special_bart_cassidy|{self.name}", + ) self.game.deck.draw(True, player=self) - elif self.character.check(self.game, chars.ElGringo) and self.attacker and self.attacker in self.game.get_alive_players() and len(self.attacker.hand) > 0: - self.hand.append(self.attacker.hand.pop(randrange(0, len(self.attacker.hand)))) + elif ( + self.character.check(self.game, chars.ElGringo) + and self.attacker + and self.attacker in self.game.get_alive_players() + and len(self.attacker.hand) > 0 + ): + self.hand.append( + self.attacker.hand.pop(randrange(0, len(self.attacker.hand))) + ) self.hand[-1].reset_card() - G.sio.emit('card_drawn', room=self.game.name, data={'player': self.name, 'pile': self.attacker.name}) - G.sio.emit('chat_message', room=self.game.name, - data=f'_special_el_gringo|{self.name}|{self.attacker.name}') + G.sio.emit( + "card_drawn", + room=self.game.name, + data={"player": self.name, "pile": self.attacker.name}, + ) + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_special_el_gringo|{self.name}|{self.attacker.name}", + ) self.attacker.notify_self() if isinstance(self.attacker, Player) and not self.game.check_event(ce.Lazo): if any((isinstance(c, tvosc.Taglia) for c in self.equipment)): self.game.deck.draw(True, player=self.attacker) - G.sio.emit('chat_message', room=self.game.name, - data=f'_taglia_reward|{self.name}|{self.attacker.name}') + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_taglia_reward|{self.name}|{self.attacker.name}", + ) self.attacker.notify_self() - if len(self.hand) > 0 and any((isinstance(cd, tvosc.Shotgun) for cd in self.attacker.equipment)): + if len(self.hand) > 0 and any( + (isinstance(cd, tvosc.Shotgun) for cd in self.attacker.equipment) + ): c = self.hand.pop(randrange(0, len(self.hand))) self.game.deck.scrap(c, True, player=self) - G.sio.emit('chat_message', room=self.game.name, data=f'_shotgun_scrap|{self.name}|{c.name}') - if self.attacker and 'gold_rush' in self.game.expansions and not self.is_ghost: - if (isinstance(self.attacker, Player)): + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_shotgun_scrap|{self.name}|{c.name}", + ) + if self.attacker and "gold_rush" in self.game.expansions and not self.is_ghost: + if isinstance(self.attacker, Player): self.attacker.gold_nuggets += 1 self.attacker.notify_self() if any((isinstance(c, grc.Talismano) for c in self.gold_rush_equipment)): @@ -1382,59 +2240,121 @@ class Player: self.mancato_needed = 0 self.expected_response = [] self.attacking_card = None - self.event_type = '' + self.event_type = "" self.notify_self() self.attacker = None def take_no_damage_response(self): - if self.dmg_card_index is not None and self.dmg_card_index != -1 and self.game.check_event(ce.Rimbalzo): + if ( + self.dmg_card_index is not None + and self.dmg_card_index != -1 + and self.game.check_event(ce.Rimbalzo) + ): self.game.deck.scrap(self.equipment.pop(self.dmg_card_index), player=self) self.dmg_card_index = -1 self.mancato_needed = 0 self.expected_response = [] self.attacking_card = None - self.event_type = '' + self.event_type = "" self.notify_self() self.attacker = None def respond(self, hand_index): - if self.pending_action != PendingAction.RESPOND: return + if self.pending_action != PendingAction.RESPOND: + return self.pending_action = PendingAction.WAIT - if hand_index != -1 and hand_index < (len(self.hand)+len(self.equipment)) and ( - ((hand_index < len(self.hand) and self.hand[hand_index].name in self.expected_response) or self.character.check(self.game, chd.ElenaFuente)) or - (0 <= hand_index-len(self.hand) < len(self.equipment) and self.equipment[hand_index-len(self.hand)].name in self.expected_response)): - card = self.hand.pop(hand_index) if hand_index < len(self.hand) else self.equipment.pop(hand_index-len(self.hand)) - #hand_index < len(self.hand) with the '<=' due to the hand.pop - if self.character.check(self.game, chd.MollyStark) and 0 <= hand_index <= len(self.hand) and not self.is_my_turn and self.event_type != 'duel': - if hasattr(self.attacker,'character') and self.attacker.character.check(self.game, chars.SlabTheKiller) and isinstance(card, cs.Mancato): + if ( + hand_index != -1 + and hand_index < (len(self.hand) + len(self.equipment)) + and ( + ( + ( + hand_index < len(self.hand) + and self.hand[hand_index].name in self.expected_response + ) + or self.character.check(self.game, chd.ElenaFuente) + ) + or ( + 0 <= hand_index - len(self.hand) < len(self.equipment) + and self.equipment[hand_index - len(self.hand)].name + in self.expected_response + ) + ) + ): + card = ( + self.hand.pop(hand_index) + if hand_index < len(self.hand) + else self.equipment.pop(hand_index - len(self.hand)) + ) + # hand_index < len(self.hand) with the '<=' due to the hand.pop + if ( + self.character.check(self.game, chd.MollyStark) + and 0 <= hand_index <= len(self.hand) + and not self.is_my_turn + and self.event_type != "duel" + ): + if ( + hasattr(self.attacker, "character") + and self.attacker.character.check(self.game, chars.SlabTheKiller) + and isinstance(card, cs.Mancato) + ): self.molly_discarded_cards += 1 else: self.game.deck.draw(True, player=self) card.use_card(self) - print(f'{self.game.name}: {self.name} responded with {card.name}') - G.sio.emit('chat_message', room=self.game.name, data=f'_respond|{self.name}|{card.name}') + print(f"{self.game.name}: {self.name} responded with {card.name}") + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_respond|{self.name}|{card.name}", + ) self.game.deck.scrap(card, True, player=self) self.notify_self() self.mancato_needed -= 1 if isinstance(card, tvosc.RitornoDiFiamma): self.game.attack(self, self.attacker.name, card_name=card.name) if self.mancato_needed <= 0: - if self.event_type == 'duel': - if isinstance(card, tvosc.Fuga) or (isinstance(card, cs.Mancato) and self.character.check(self.game, tvosch.MickDefender)): + if self.event_type == "duel": + if isinstance(card, tvosc.Fuga) or ( + isinstance(card, cs.Mancato) + and self.character.check(self.game, tvosch.MickDefender) + ): self.game.responders_did_respond_resume_turn(did_lose=False) else: self.game.duel(self, self.attacker.name) - if self.character.check(self.game, chd.MollyStark) and hand_index < len(self.hand) and not self.is_my_turn: + if ( + self.character.check(self.game, chd.MollyStark) + and hand_index < len(self.hand) + and not self.is_my_turn + ): self.molly_discarded_cards += 1 else: - if self.character.check(self.game, chd.MollyStark) and not self.is_my_turn: + if ( + self.character.check(self.game, chd.MollyStark) + and not self.is_my_turn + ): for i in range(self.molly_discarded_cards): self.game.deck.draw(True, player=self) self.molly_discarded_cards = 0 self.notify_self() self.game.responders_did_respond_resume_turn(did_lose=False) - self.event_type = '' - elif not any(((isinstance(c, cs.Mancato) and c.can_be_used_now) or (self.character.check(self.game, chars.CalamityJanet) and isinstance(c, cs.Bang)) or self.character.check(self.game, chd.ElenaFuente) for c in self.hand)) and not any((c.can_be_used_now and isinstance(c, cs.Mancato) for c in self.equipment)): + self.event_type = "" + elif not any( + ( + (isinstance(c, cs.Mancato) and c.can_be_used_now) + or ( + self.character.check(self.game, chars.CalamityJanet) + and isinstance(c, cs.Bang) + ) + or self.character.check(self.game, chd.ElenaFuente) + for c in self.hand + ) + ) and not any( + ( + c.can_be_used_now and isinstance(c, cs.Mancato) + for c in self.equipment + ) + ): self.on_failed_response_cb() if self.game: self.game.responders_did_respond_resume_turn(did_lose=True) @@ -1447,7 +2367,12 @@ class Player: self.game.deck.draw(True, player=self) self.molly_discarded_cards = 0 self.notify_self() - elif self.attacker and self.attacker in self.game.get_alive_players() and self.attacker.character.check(self.game, chd.MollyStark) and self.is_my_turn: + elif ( + self.attacker + and self.attacker in self.game.get_alive_players() + and self.attacker.character.check(self.game, chd.MollyStark) + and self.is_my_turn + ): for i in range(self.attacker.molly_discarded_cards): self.attacker.game.deck.draw(True, player=self.attacker) self.attacker.molly_discarded_cards = 0 @@ -1458,7 +2383,7 @@ class Player: if self.mancato_needed <= 0: self.attacker = None - def get_sight(self, countWeapon=True): #come vedo io gli altri + def get_sight(self, countWeapon=True): # come vedo io gli altri if not self.character: return 0 if self.game.check_event(ce.Lazo): @@ -1470,28 +2395,54 @@ class Player: range += card.range else: aim += card.sight_mod - return max(1,range) + aim + (self.character.sight_mod if not self.game.check_event(ceh.Sbornia) else 0) + return ( + max(1, range) + + aim + + ( + self.character.sight_mod + if not self.game.check_event(ceh.Sbornia) + else 0 + ) + ) - def get_visibility(self): #come mi vedono gli altri - if not self.character or not self.game or not self.game.players[self.game.turn].character: + def get_visibility(self): # come mi vedono gli altri + if ( + not self.character + or not self.game + or not self.game.players[self.game.turn].character + ): return 0 covers = 0 - ch_vis_mod = self.character.visibility_mod if not self.game.check_event(ceh.Sbornia) else 0 - if self.game.check_event(ce.Lazo) or self.game.players[self.game.turn].character.check(self.game, chd.BelleStar): + ch_vis_mod = ( + self.character.visibility_mod + if not self.game.check_event(ceh.Sbornia) + else 0 + ) + if self.game.check_event(ce.Lazo) or self.game.players[ + self.game.turn + ].character.check(self.game, chd.BelleStar): return ch_vis_mod for card in self.equipment: covers += card.vis_mod return ch_vis_mod + covers def scrap(self, card_index): - if len(self.hand) == 0 or len(self.hand) <= card_index: return self.notify_self() + if len(self.hand) == 0 or len(self.hand) <= card_index: + return self.notify_self() if self.is_my_turn or self.character.check(self.game, chars.SidKetchum): self.scrapped_cards += 1 card = self.hand.pop(card_index) - if self.character.check(self.game, chars.SidKetchum) and self.scrapped_cards == 2: + if ( + self.character.check(self.game, chars.SidKetchum) + and self.scrapped_cards == 2 + ): self.scrapped_cards = 0 - self.lives = min(self.lives+1, self.max_lives) - elif self.character.check(self.game, chd.JoseDelgado) and card.is_equipment and self.special_use_count < 2: + self.lives = min(self.lives + 1, self.max_lives) + elif ( + self.character.check(self.game, chd.JoseDelgado) + and card.is_equipment + and self.special_use_count < 2 + ): self.game.deck.draw(True, player=self) self.game.deck.draw(True, player=self) self.special_use_count += 1 @@ -1502,28 +2453,48 @@ class Player: self.character.special(self, data) def gold_rush_discard(self): - self.available_cards = [{ - 'name': p.name, - 'icon': p.role.icon if(self.game.initial_players == 3) else '⭐️' if isinstance(p.role, r.Sheriff) else '🤠', - 'is_character': True, - 'avatar': p.avatar, - 'alt_text': ''.join(['🎴️'] * len(p.gold_rush_equipment)), - 'is_player': True - } for p in self.game.get_alive_players() if p != self and any((e.number + 1 <= self.gold_nuggets for e in p.gold_rush_equipment))] - self.available_cards.append({'icon': '❌', 'noDesc': True}) - self.choose_text = 'gold_rush_discard' + self.available_cards = [ + { + "name": p.name, + "icon": p.role.icon + if (self.game.initial_players == 3) + else "⭐️" + if isinstance(p.role, r.Sheriff) + else "🤠", + "is_character": True, + "avatar": p.avatar, + "alt_text": "".join(["🎴️"] * len(p.gold_rush_equipment)), + "is_player": True, + } + for p in self.game.get_alive_players() + if p != self + and any((e.number + 1 <= self.gold_nuggets for e in p.gold_rush_equipment)) + ] + self.available_cards.append({"icon": "❌", "noDesc": True}) + self.choose_text = "gold_rush_discard" self.pending_action = PendingAction.CHOOSE self.notify_self() def buy_gold_rush_card(self, index): - print(f'{self.name} wants to buy gr-card index {index} in room {self.game.name}') + print( + f"{self.name} wants to buy gr-card index {index} in room {self.game.name}" + ) card: cs.Card = self.game.deck.shop_cards[index] discount = 0 - if self.character.check(self.game, grch.PrettyLuzena) and self.special_use_count < 1: + if ( + self.character.check(self.game, grch.PrettyLuzena) + and self.special_use_count < 1 + ): discount = 1 - if self.pending_action == PendingAction.PLAY and self.gold_nuggets >= card.number - discount: + if ( + self.pending_action == PendingAction.PLAY + and self.gold_nuggets >= card.number - discount + ): self.gold_nuggets -= card.number - discount - if self.character.check(self.game, grch.PrettyLuzena) and self.special_use_count < 1: + if ( + self.character.check(self.game, grch.PrettyLuzena) + and self.special_use_count < 1 + ): self.special_use_count += 1 if card.play_card(self): self.game.deck.shop_deck.append(card) @@ -1535,24 +2506,83 @@ class Player: must_be_used_cards = [c for c in self.hand if c.must_be_used] if self.game.check_event(ce.LeggeDelWest) and len(must_be_used_cards) > 0: card = must_be_used_cards[0] - print(f'Legge del west card: {card.name}') - print(self.has_played_bang and not (any((isinstance(c, cs.Volcanic) for c in self.equipment)) and type(card) == type(cs.Bang))) - if card.suit == cs.Suit.DIAMONDS and card.need_target and not any(((not p.character.check(self.game, chd.ApacheKid) and not any((isinstance(c, grc.Calumet) for c in p.gold_rush_equipment))) for p in self.game.get_alive_players())): + print(f"Legge del west card: {card.name}") + print( + self.has_played_bang + and not ( + any((isinstance(c, cs.Volcanic) for c in self.equipment)) + and type(card) == type(cs.Bang) + ) + ) + if ( + card.suit == cs.Suit.DIAMONDS + and card.need_target + and not any( + ( + ( + not p.character.check(self.game, chd.ApacheKid) + and not any( + ( + isinstance(c, grc.Calumet) + for c in p.gold_rush_equipment + ) + ) + ) + for p in self.game.get_alive_players() + ) + ) + ): if isinstance(card, cs.Bang): - return True + return True else: - return len(self.equipment) == 0 # se non ho carte equipaggiamento - elif (isinstance(card, cs.Bang) or (isinstance(card, cs.Mancato) and self.character.check(self.game, chars.CalamityJanet))) and self.has_played_bang and not any((isinstance(c, cs.Volcanic) for c in self.equipment)) or not any((self.get_sight() >= p['dist'] for p in self.game.get_visible_players(self))): + return len(self.equipment) == 0 # se non ho carte equipaggiamento + elif ( + ( + isinstance(card, cs.Bang) + or ( + isinstance(card, cs.Mancato) + and self.character.check(self.game, chars.CalamityJanet) + ) + ) + and self.has_played_bang + and not any((isinstance(c, cs.Volcanic) for c in self.equipment)) + or not any( + ( + self.get_sight() >= p["dist"] + for p in self.game.get_visible_players(self) + ) + ) + ): return True - elif isinstance(card, cs.Mancato) or (card.need_with and len(self.hand) < 2): + elif isinstance(card, cs.Mancato) or ( + card.need_with and len(self.hand) < 2 + ): return True - elif isinstance(card, cs.Panico) and not any((self.get_sight(False) >= p['dist'] for p in self.game.get_visible_players(self))) and len(self.equipment) == 0: + elif ( + isinstance(card, cs.Panico) + and not any( + ( + self.get_sight(False) >= p["dist"] + for p in self.game.get_visible_players(self) + ) + ) + and len(self.equipment) == 0 + ): return True - elif isinstance(card, csd.Pugno) and not any((self.get_sight(False) >= p['dist'] for p in self.game.get_visible_players(self))): + elif isinstance(card, csd.Pugno) and not any( + ( + self.get_sight(False) >= p["dist"] + for p in self.game.get_visible_players(self) + ) + ): return True - elif isinstance(card, cs.Prigione) and not any((not p['is_sheriff'] for p in self.game.get_visible_players(self))): + elif isinstance(card, cs.Prigione) and not any( + (not p["is_sheriff"] for p in self.game.get_visible_players(self)) + ): return True - elif not card.is_weapon and any((c.name == card.name for c in self.equipment)): + elif not card.is_weapon and any( + (c.name == card.name for c in self.equipment) + ): return True return False return True @@ -1561,38 +2591,77 @@ class Player: print(f"{self.name} wants to end his turn") if not self.is_my_turn and not forced: return - maxcards = self.lives if not self.character.check(self.game, chd.SeanMallory) else 10 - if maxcards == self.lives and any((isinstance(c, grc.Cinturone) for c in self.gold_rush_equipment)): + maxcards = ( + self.lives if not self.character.check(self.game, chd.SeanMallory) else 10 + ) + if maxcards == self.lives and any( + (isinstance(c, grc.Cinturone) for c in self.gold_rush_equipment) + ): maxcards = 8 if len(self.hand) > maxcards and not forced: - print(f"{self.name}: I have to many cards in my hand and I can't end the turn") + print( + f"{self.name}: I have to many cards in my hand and I can't end the turn" + ) elif not self.check_can_end_turn(): print(f"{self.name}: I must play the legge del west card") elif self.pending_action == PendingAction.PLAY or forced: for i in range(len(self.equipment)): - if self.equipment[i].usable_next_turn and not self.equipment[i].can_be_used_now: + if ( + self.equipment[i].usable_next_turn + and not self.equipment[i].can_be_used_now + ): self.equipment[i].can_be_used_now = True for i in range(len(self.hand)): if self.hand[i].must_be_used: self.hand[i].must_be_used = False self.has_played_bang = False - ##Vendetta## - if not forced and self.game.check_event(ce.Vendetta) and self.can_play_vendetta: + ##Vendetta## + if ( + not forced + and self.game.check_event(ce.Vendetta) + and self.can_play_vendetta + ): picked: cs.Card = self.game.deck.pick_and_scrap() - G.sio.emit('chat_message', room=self.game.name, data=f'_flipped|{self.name}|{picked.name}|{picked.num_suit()}') + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_flipped|{self.name}|{picked.name}|{picked.num_suit()}", + ) if picked.check_suit(self.game, [cs.Suit.HEARTS]): self.play_turn(can_play_vendetta=False) return - ##Don Bell## - if not forced and self.character.check(self.game, grch.DonBell) and self.can_play_again_don_bell: + ##Don Bell## + if ( + not forced + and self.character.check(self.game, grch.DonBell) + and self.can_play_again_don_bell + ): picked: cs.Card = self.game.deck.pick_and_scrap() - G.sio.emit('chat_message', room=self.game.name, data=f'_flipped|{self.name}|{picked.name}|{picked.num_suit()}') + G.sio.emit( + "chat_message", + room=self.game.name, + data=f"_flipped|{self.name}|{picked.name}|{picked.num_suit()}", + ) self.can_play_again_don_bell = False if picked.check_suit(self.game, [cs.Suit.HEARTS, cs.Suit.DIAMONDS]): self.play_turn(can_play_vendetta=False) return - ##Ghost## - if (self.is_dead and self.is_ghost and self.game.check_event(ceh.CittaFantasma) and not any((True for c in self.equipment if isinstance(c, tvosc.Fantasma)))) or (self.is_dead and forced and self.is_ghost and not any((True for c in self.equipment if isinstance(c, tvosc.Fantasma)))): + ##Ghost## + if ( + self.is_dead + and self.is_ghost + and self.game.check_event(ceh.CittaFantasma) + and not any( + (True for c in self.equipment if isinstance(c, tvosc.Fantasma)) + ) + ) or ( + self.is_dead + and forced + and self.is_ghost + and not any( + (True for c in self.equipment if isinstance(c, tvosc.Fantasma)) + ) + ): self.is_ghost = False for i in range(len(self.hand)): self.game.deck.scrap(self.hand.pop(), True) diff --git a/backend/server.py b/backend/server.py index c67db0c..947fdf1 100644 --- a/backend/server.py +++ b/backend/server.py @@ -1,59 +1,65 @@ -import os +import faulthandler import json -import time +import logging +import os +import pickle import random -from typing import List -import eventlet -import socketio - -from bang.game import Game -from bang.players import Player, PendingAction - -import requests -from discord_webhook import DiscordWebhook -from metrics import Metrics - import sys import traceback -sys.setrecursionlimit(10**6) # this should prevents bots from stopping - -import logging -logging.basicConfig(filename='out.log', level='ERROR') +import urllib.parse from functools import wraps +from typing import List + +import eventlet +import requests +import socketio +from discord_webhook import DiscordWebhook + +from bang.game import Game +from bang.players import PendingAction, Player from globals import G +from metrics import Metrics + +sys.setrecursionlimit(10**6) # this should prevents bots from stopping + + +logging.basicConfig(filename="out.log", level="ERROR") + Metrics.init() sio = socketio.Server(cors_allowed_origins="*") G.sio = sio -import faulthandler faulthandler.enable() -static_files={ - '/': {'content_type': 'text/html', 'filename': 'index.html'}, - '/game': {'content_type': 'text/html', 'filename': 'index.html'}, - '/help': {'content_type': 'text/html', 'filename': 'index.html'}, - '/status': {'content_type': 'text/html', 'filename': 'index.html'}, - # '/robots.txt': {'content_type': 'text/html', 'filename': 'robots.txt'}, - '/favicon.ico': {'filename': 'favicon.ico'}, - '/img/icons': './img/icons', - '/manifest.webmanifest': {'filename': 'manifest.webmanifest'}, - '/assets':'./assets', - #'/css': './css', - #'/media': './media', - #'/js': './js', +static_files = { + "/": {"content_type": "text/html", "filename": "index.html"}, + "/game": {"content_type": "text/html", "filename": "index.html"}, + "/help": {"content_type": "text/html", "filename": "index.html"}, + "/status": {"content_type": "text/html", "filename": "index.html"}, + # '/robots.txt': {'content_type': 'text/html', 'filename': 'robots.txt'}, + "/favicon.ico": {"filename": "favicon.ico"}, + "/img/icons": "./img/icons", + "/manifest.webmanifest": {"filename": "manifest.webmanifest"}, + "/assets": "./assets", + #'/css': './css', + #'/media': './media', + #'/js': './js', +} +if "UseRobots" in os.environ and os.environ["UseRobots"].upper() == "TRUE": + static_files["/robots.txt"] = { + "content_type": "text/html", + "filename": "robots.txt", } -if "UseRobots" in os.environ and os.environ['UseRobots'].upper() == "TRUE": - static_files['/robots.txt'] = {'content_type': 'text/html', 'filename': 'robots.txt'} -for file in [f for f in os.listdir('.') if '.js' in f or '.map' in f or '.html' in f]: - static_files[f'/{file}'] = f'./{file}' +for file in [f for f in os.listdir(".") if ".js" in f or ".map" in f or ".html" in f]: + static_files[f"/{file}"] = f"./{file}" HASTEBIN_HEADERS = { - 'Authorization': 'Bearer 2cf615e88992970f3396663c5bfb2f599151192bcef5fa99f5569c2e29617ba7cdcfa5df14f90afc61bcf0fe6e475ae49cba98ebded3e0b7b3fdf0c648c76496', - 'content-type': 'text/plain' + "Authorization": "Bearer 2cf615e88992970f3396663c5bfb2f599151192bcef5fa99f5569c2e29617ba7cdcfa5df14f90afc61bcf0fe6e475ae49cba98ebded3e0b7b3fdf0c648c76496", + "content-type": "text/plain", } app = socketio.WSGIApp(sio, static_files=static_files) @@ -61,21 +67,43 @@ games: dict[str, Game] = {} online_players = 0 blacklist: List[str] = [] + def send_to_debug(error): for g in games.values(): if g.debug: - sio.emit('chat_message', room=g.name, data={'color': f'red','text':json.dumps({'ERROR':error}), 'type':'json'}) + sio.emit( + "chat_message", + room=g.name, + data={ + "color": "red", + "text": json.dumps({"ERROR": error}), + "type": "json", + }, + ) elif any((p.is_admin() for p in g.players)): for p in g.players: if p.is_admin(): - sio.emit('chat_message', room=p.sid, data={'color': f'red','text':json.dumps({'ERROR':error}), 'type':'json'}) + sio.emit( + "chat_message", + room=p.sid, + data={ + "color": "red", + "text": json.dumps({"ERROR": error}), + "type": "json", + }, + ) + + +SAVE_LOCK = False + -save_lock = False def bang_handler(func): + """Decorator to handle exceptions in custom sockets handlers.""" + @wraps(func) def wrapper_func(*args, **kwargs): - global save_lock - save_lock = True + global SAVE_LOCK + SAVE_LOCK = True try: func(*args, **kwargs) except Exception as e: @@ -83,115 +111,208 @@ def bang_handler(func): print(traceback.format_exc()) send_to_debug(traceback.format_exc()) finally: - save_lock = False + SAVE_LOCK = False + return wrapper_func + def count_bots_in_game(game): + """Count the number of bots in a game.""" return sum(1 for p in game.players if p.is_bot) + def advertise_lobbies(): - open_lobbies = [g for g in games.values() if 0 < len(g.players) < 10 and not g.is_hidden][-10:] - sio.emit('lobbies', room='lobby', data=[{'name': g.name, 'players': len(g.players), 'bots': count_bots_in_game(g), 'locked': g.password != '', 'expansions': g.expansions} for g in open_lobbies if not g.started]) - sio.emit('spectate_lobbies', room='lobby', data=[{'name': g.name, 'players': len(g.players), 'bots': count_bots_in_game(g), 'locked': g.password != '', 'expansions': g.expansions} for g in open_lobbies if g.started]) - Metrics.send_metric('lobbies', points=[sum(not g.is_replay for g in games.values())]) - Metrics.send_metric('online_players', points=[online_players]) + open_lobbies = [ + g for g in games.values() if 0 < len(g.players) < 10 and not g.is_hidden + ][-10:] + sio.emit( + "lobbies", + room="lobby", + data=[ + { + "name": g.name, + "players": len(g.players), + "bots": count_bots_in_game(g), + "locked": g.password != "", + "expansions": g.expansions, + } + for g in open_lobbies + if not g.started + ], + ) + sio.emit( + "spectate_lobbies", + room="lobby", + data=[ + { + "name": g.name, + "players": len(g.players), + "bots": count_bots_in_game(g), + "locked": g.password != "", + "expansions": g.expansions, + } + for g in open_lobbies + if g.started + ], + ) + Metrics.send_metric( + "lobbies", points=[sum(not g.is_replay for g in games.values())] + ) + Metrics.send_metric("online_players", points=[online_players]) + @sio.event @bang_handler def connect(sid, environ): global online_players online_players += 1 - print('connect ', sid) - sio.enter_room(sid, 'lobby') - sio.emit('players', room='lobby', data=online_players) - Metrics.send_metric('online_players', points=[online_players]) + print("connect ", sid) + sio.enter_room(sid, "lobby") + sio.emit("players", room="lobby", data=online_players) + Metrics.send_metric("online_players", points=[online_players]) + @sio.event @bang_handler def get_online_players(sid): global online_players - sio.emit('players', room='lobby', data=online_players) + sio.emit("players", room="lobby", data=online_players) + @sio.event @bang_handler def report(sid, text): - print(f'New report from {sid}: {text}') + print(f"New report from {sid}: {text}") ses: Player = sio.get_session(sid) - data='' - if hasattr(ses, 'game'): + data = "" + if hasattr(ses, "game"): data = "\n".join(ses.game.rpc_log[:-1]).strip() - data = data +"\n@@@\n" +text - response = requests.post("https://hastebin.com/documents", data.encode('utf-8'), headers=HASTEBIN_HEADERS) - key = json.loads(response.text).get('key') - if "DISCORD_WEBHOOK" in os.environ and len(os.environ['DISCORD_WEBHOOK']) > 0: - webhook = DiscordWebhook(url=os.environ['DISCORD_WEBHOOK'], content=f'New bug reported by {ses.name}, replay at https://bang.xamin.it/game?replay={key}\nRaw: https://hastebin.com/{key}\nTotal actions:{len(ses.game.rpc_log)}\nExpansions:{ses.game.expansions}\nInfo: {text}') + data = data + "\n@@@\n" + text + response = requests.post( + "https://hastebin.com/documents", data.encode("utf-8"), headers=HASTEBIN_HEADERS + ) + key = json.loads(response.text).get("key") + if "DISCORD_WEBHOOK" in os.environ and len(os.environ["DISCORD_WEBHOOK"]) > 0: + webhook = DiscordWebhook( + url=os.environ["DISCORD_WEBHOOK"], + content=f"New bug reported by {ses.name}, replay at https://bang.xamin.it/game?replay={key}\nRaw: https://hastebin.com/{key}\nTotal actions:{len(ses.game.rpc_log)}\nExpansions:{ses.game.expansions}\nInfo: {text}", + ) response = webhook.execute() - sio.emit('chat_message', room=sid, data={'color': f'green','text':f'Report OK'}) + sio.emit( + "chat_message", room=sid, data={"color": f"green", "text": f"Report OK"} + ) if not any((p.pending_action != PendingAction.WAIT for p in ses.game.players)): - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'TRYING AUTO FIX BY KICKING THE ARCADE CABINET'}) + sio.emit( + "chat_message", + room=ses.game.name, + data={ + "color": "red", + "text": f"TRYING AUTO FIX BY KICKING THE ARCADE CABINET", + }, + ) ses.game.next_turn() if any((p.pending_action == PendingAction.WAIT for p in ses.game.players)): - sio.emit('chat_message', room=ses.game.name, data={'color': f'green','text':f'IT WORKED!11!!!, we will still fix the bug (for real) though'}) + sio.emit( + "chat_message", + room=ses.game.name, + data={ + "color": f"green", + "text": f"IT WORKED!11!!!, we will still fix the bug (for real) though", + }, + ) else: print("WARNING: DISCORD_WEBHOOK not found") - Metrics.send_event('BUG_REPORT', event_data=text) - print(f'New bug report, replay at https://bang.xamin.it/game?replay={key}') + Metrics.send_event("BUG_REPORT", event_data=text) + print(f"New bug report, replay at https://bang.xamin.it/game?replay={key}") + @sio.event @bang_handler def set_username(sid, username): ses = sio.get_session(sid) if not isinstance(ses, Player): - dt = username["discord_token"] if 'discord_token' in username else None - sio.save_session(sid, Player(username.get('name', 'player'), sid, discord_token=dt)) - print(f'{sid} is now {username}') + dt = username["discord_token"] if "discord_token" in username else None + sio.save_session( + sid, Player(username.get("name", "player"), sid, discord_token=dt) + ) + print(f"{sid} is now {username}") advertise_lobbies() elif ses.game is None or not ses.game.started: username = username["name"] - print(f'{sid} changed username to {username}') + print(f"{sid} changed username to {username}") prev = ses.name if ses.game and any((p.name == username for p in ses.game.players)): ses.name = f"{username}_{random.randint(0,100)}" else: ses.name = username - sio.emit('chat_message', room=ses.game.name, data=f'_change_username|{prev}|{ses.name}') - sio.emit('me', data=ses.name, room=sid) + sio.emit( + "chat_message", + room=ses.game.name, + data=f"_change_username|{prev}|{ses.name}", + ) + sio.emit("me", data=ses.name, room=sid) ses.game.notify_room() + @sio.event @bang_handler def get_me(sid, data): if isinstance(sio.get_session(sid), Player): - sio.emit('me', data=sio.get_session(sid).name, room=sid) + sio.emit("me", data=sio.get_session(sid).name, room=sid) if sio.get_session(sid).game: sio.get_session(sid).game.notify_room() else: - dt = data["discord_token"] if 'discord_token' in data else None - sio.save_session(sid, Player('player', sid, discord_token=dt)) - if 'replay' in data and data['replay'] is not None: - create_room(sid, data['replay']) + dt = data["discord_token"] if "discord_token" in data else None + sio.save_session(sid, Player("player", sid, discord_token=dt)) + if "replay" in data and data["replay"] is not None: + create_room(sid, data["replay"]) sid = sio.get_session(sid) sid.game.is_hidden = True eventlet.sleep(0.5) - response = requests.get(f"https://hastebin.com/raw/{data['replay']}", headers=HASTEBIN_HEADERS) + response = requests.get( + f"https://hastebin.com/raw/{data['replay']}", headers=HASTEBIN_HEADERS + ) if response.status_code != 200: - sio.emit('chat_message', room=sid, data={'color': f'green','text':f'Invalid replay code'}) + sio.emit( + "chat_message", + room=sid, + data={"color": f"green", "text": f"Invalid replay code"}, + ) return log = response.text.splitlines() sid.game.spectators.append(sid) - if 'ffw' not in data: + if "ffw" not in data: sid.game.replay(log) else: - sid.game.replay(log, speed=0, fast_forward=int(data['ffw'])) + sid.game.replay(log, speed=0, fast_forward=int(data["ffw"])) return - if data['name'] in games and (room := games[data['name']]) is not None: + if data["name"] in games and (room := games[data["name"]]) is not None: if not room.started: join_room(sid, data) elif room.started: - print('room exists') - if data['username'] is not None and any((p.name == data['username'] for p in room.players if (p.is_bot or (dt is not None and p.discord_token == dt) or p.sid is None))): - print('getting inside the bot') - bot = [p for p in room.players if (p.is_bot or (dt is not None and p.discord_token == dt) or p.sid is None) and p.name == data['username']][0] + print("room exists") + if data["username"] is not None and any( + ( + p.name == data["username"] + for p in room.players + if ( + p.is_bot + or (dt is not None and p.discord_token == dt) + or p.sid is None + ) + ) + ): + print("getting inside the bot") + bot = [ + p + for p in room.players + if ( + p.is_bot + or (dt is not None and p.discord_token == dt) + or p.sid is None + ) + and p.name == data["username"] + ][0] bot.sid = sid bot.is_bot = False sio.enter_room(sid, room.name) @@ -200,66 +321,87 @@ def get_me(sid, data): eventlet.sleep(0.1) room.notify_all() room.notify_scrap_pile(sid) - sio.emit('role', room=sid, data=json.dumps(bot.role, default=lambda o: o.__dict__)) + sio.emit( + "role", + room=sid, + data=json.dumps(bot.role, default=lambda o: o.__dict__), + ) bot.notify_self() if len(bot.available_characters) > 0: bot.set_available_character(bot.available_characters) - else: #spectate + else: # spectate room.spectators.append(sio.get_session(sid)) sio.get_session(sid).game = room sio.enter_room(sid, room.name) room.notify_room(sid) eventlet.sleep(0.1) room.notify_event_card(sid) + room.notify_event_card_wildwestshow(sid) room.notify_scrap_pile(sid) room.notify_all() room.notify_gold_rush_shop() room.notify_event_card() + room.notify_event_card_wildwestshow(sid) else: - create_room(sid, data['name']) + create_room(sid, data["name"]) if (p := sio.get_session(sid)).game is None: - sio.emit('me', data={'error':'Wrong password/Cannot connect'}, room=sid) + sio.emit("me", data={"error": "Wrong password/Cannot connect"}, room=sid) else: - sio.emit('me', data=p.name, room=sid) - if data['username'] is None or any((pl.name == data['username'] for pl in p.game.players if not ((dt is not None and pl.discord_token == dt) or pl.sid is None))): - sio.emit('change_username', room=sid) + sio.emit("me", data=p.name, room=sid) + if data["username"] is None or any( + ( + pl.name == data["username"] + for pl in p.game.players + if not ( + (dt is not None and pl.discord_token == dt) or pl.sid is None + ) + ) + ): + sio.emit("change_username", room=sid) else: - sio.emit('chat_message', room=p.game.name, data=f"_change_username|{p.name}|{data['username']}") - p.name = data['username'] - sio.emit('me', data=p.name, room=sid) + sio.emit( + "chat_message", + room=p.game.name, + data=f"_change_username|{p.name}|{data['username']}", + ) + p.name = data["username"] + sio.emit("me", data=p.name, room=sid) if not p.game.started: p.game.notify_room() + @sio.event @bang_handler def disconnect(sid): global online_players online_players -= 1 if (p := sio.get_session(sid)) is not None and isinstance(p, Player): - sio.emit('players', room='lobby', data=online_players) + sio.emit("players", room="lobby", data=online_players) if p.game and p.disconnect(): sio.close_room(p.game.name) games.pop(p.game.name) - print('disconnect ', sid) + print("disconnect ", sid) advertise_lobbies() - Metrics.send_metric('online_players', points=[online_players]) + Metrics.send_metric("online_players", points=[online_players]) + @sio.event @bang_handler def create_room(sid, room_name): if (p := sio.get_session(sid)).game is None: while room_name in games: - room_name += f'_{random.randint(0, 10000)}' - sio.leave_room(sid, 'lobby') + room_name += f"_{random.randint(0, 10000)}" + sio.leave_room(sid, "lobby") sio.enter_room(sid, room_name) g = Game(room_name) g.add_player(p) if room_name in blacklist: g.is_hidden = True games[room_name] = g - print(f'{sid} created a room named {room_name}') + print(f"{sid} created a room named {room_name}") advertise_lobbies() + @sio.event @bang_handler def private(sid): @@ -267,6 +409,7 @@ def private(sid): g.set_private() advertise_lobbies() + @sio.event @bang_handler def toggle_expansion(sid, expansion_name): @@ -274,29 +417,40 @@ def toggle_expansion(sid, expansion_name): g.toggle_expansion(expansion_name) advertise_lobbies() + @sio.event @bang_handler def toggle_comp(sid): sio.get_session(sid).game.toggle_competitive() + @sio.event @bang_handler def toggle_replace_with_bot(sid): sio.get_session(sid).game.toggle_disconnect_bot() + @sio.event @bang_handler def join_room(sid, room): - room_name = room['name'] - if games[room_name].password != '' and games[room_name].password != room.get('password', '').upper(): + room_name = room["name"] + if ( + games[room_name].password != "" + and games[room_name].password != room.get("password", "").upper() + ): return if not games[room_name].started: - print(f'{sid} joined a room named {room_name}') - sio.leave_room(sid, 'lobby') + print(f"{sid} joined a room named {room_name}") + sio.leave_room(sid, "lobby") sio.enter_room(sid, room_name) - while any((p.name == sio.get_session(sid).name and not p.is_bot for p in games[room_name].players)): - sio.get_session(sid).name += f'_{random.randint(0,100)}' - sio.emit('me', data=sio.get_session(sid).name, room=sid) + while any( + ( + p.name == sio.get_session(sid).name and not p.is_bot + for p in games[room_name].players + ) + ): + sio.get_session(sid).name += f"_{random.randint(0,100)}" + sio.emit("me", data=sio.get_session(sid).name, room=sid) games[room_name].add_player(sio.get_session(sid)) advertise_lobbies() else: @@ -309,64 +463,100 @@ def join_room(sid, room): games[room_name].notify_room(sid) games[room_name].notify_all() + """ Sockets for the status page """ + @sio.event @bang_handler def get_all_rooms(sid, deploy_key): ses = sio.get_session(sid) - if ('DEPLOY_KEY' in os.environ and deploy_key == os.environ['DEPLOY_KEY']) or (isinstance(ses, Player) and ses.is_admin()): - sio.emit('all_rooms', room=sid, data=[{ - 'name': g.name, - 'hidden': g.is_hidden, - 'players': [{'name':p.name, 'bot': p.is_bot, 'health': p.lives, 'sid': p.sid} for p in g.players], - 'password': g.password, - 'expansions': g.expansions, - 'started': g.started, - 'current_turn': g.turn, - 'incremental_turn': g.incremental_turn, - 'debug': g.debug, - 'spectators': len(g.spectators) - } for g in games.values()]) + if ("DEPLOY_KEY" in os.environ and deploy_key == os.environ["DEPLOY_KEY"]) or ( + isinstance(ses, Player) and ses.is_admin() + ): + sio.emit( + "all_rooms", + room=sid, + data=[ + { + "name": g.name, + "hidden": g.is_hidden, + "players": [ + { + "name": p.name, + "bot": p.is_bot, + "health": p.lives, + "sid": p.sid, + } + for p in g.players + ], + "password": g.password, + "expansions": g.expansions, + "started": g.started, + "current_turn": g.turn, + "incremental_turn": g.incremental_turn, + "debug": g.debug, + "spectators": len(g.spectators), + } + for g in games.values() + ], + ) + @sio.event @bang_handler def kick(sid, data): ses = sio.get_session(sid) - if ('DEPLOY_KEY' in os.environ and 'key' in data and data['key'] == os.environ['DEPLOY_KEY']) or (isinstance(ses, Player) and ses.is_admin()): - sio.emit('kicked', room=data['sid']) + if ( + "DEPLOY_KEY" in os.environ + and "key" in data + and data["key"] == os.environ["DEPLOY_KEY"] + ) or (isinstance(ses, Player) and ses.is_admin()): + sio.emit("kicked", room=data["sid"]) + @sio.event @bang_handler def reset(sid, data): global games ses = sio.get_session(sid) - if ('DEPLOY_KEY' in os.environ and 'key' in data and data['key'] == os.environ['DEPLOY_KEY']) or (isinstance(ses, Player) and ses.is_admin()): + if ( + "DEPLOY_KEY" in os.environ + and "key" in data + and data["key"] == os.environ["DEPLOY_KEY"] + ) or (isinstance(ses, Player) and ses.is_admin()): for g in games.values(): - sio.emit('kicked', room=g.name) + sio.emit("kicked", room=g.name) games = {} + @sio.event @bang_handler def hide_toogle(sid, data): ses = sio.get_session(sid) - if ('DEPLOY_KEY' in os.environ and 'key' in data and data['key'] == os.environ['DEPLOY_KEY']) or (isinstance(ses, Player) and ses.is_admin()): - game = games['room'] + if ( + "DEPLOY_KEY" in os.environ + and "key" in data + and data["key"] == os.environ["DEPLOY_KEY"] + ) or (isinstance(ses, Player) and ses.is_admin()): + game = games["room"] if len(games) > 0: game[0].is_hidden = not game[0].is_hidden if game[0].is_hidden: - if not data['room'] in blacklist: - blacklist.append(data['room']) - elif data['room'] in blacklist: - blacklist.remove(data['room']) + if not data["room"] in blacklist: + blacklist.append(data["room"]) + elif data["room"] in blacklist: + blacklist.remove(data["room"]) advertise_lobbies() + """ Sockets for the game """ + @sio.event @bang_handler def start_game(sid): @@ -374,121 +564,147 @@ def start_game(sid): ses.game.start_game() advertise_lobbies() + @sio.event @bang_handler def shuffle_players(sid): ses: Player = sio.get_session(sid) ses.game.shuffle_players() + @sio.event @bang_handler def set_character(sid, name): ses: Player = sio.get_session(sid) - ses.game.rpc_log.append(f'{ses.name};set_character;{name}') + ses.game.rpc_log.append(f"{ses.name};set_character;{name}") if not ses.game.is_replay: - Metrics.send_metric('set_character', points=[1], tags=[f"char:{name}"]) + Metrics.send_metric("set_character", points=[1], tags=[f"char:{name}"]) ses.set_character(name) + @sio.event @bang_handler def refresh(sid): ses: Player = sio.get_session(sid) ses.notify_self() + @sio.event @bang_handler def draw(sid, pile): ses: Player = sio.get_session(sid) - ses.game.rpc_log.append(f'{ses.name};draw;{pile}') + ses.game.rpc_log.append(f"{ses.name};draw;{pile}") ses.draw(pile) + @sio.event @bang_handler def pick(sid): ses: Player = sio.get_session(sid) - ses.game.rpc_log.append(f'{ses.name};pick') + ses.game.rpc_log.append(f"{ses.name};pick") ses.pick() + @sio.event @bang_handler def end_turn(sid): ses: Player = sio.get_session(sid) - ses.game.rpc_log.append(f'{ses.name};end_turn') + ses.game.rpc_log.append(f"{ses.name};end_turn") ses.end_turn() + @sio.event @bang_handler def play_card(sid, data): ses: Player = sio.get_session(sid) - ses.game.rpc_log.append(f'{ses.name};play_card;{json.dumps(data)}') - ses.play_card(data['index'], data['against'], data['with']) + ses.game.rpc_log.append(f"{ses.name};play_card;{json.dumps(data)}") + ses.play_card(data["index"], data["against"], data["with"]) + @sio.event @bang_handler def respond(sid, card_index): ses: Player = sio.get_session(sid) - ses.game.rpc_log.append(f'{ses.name};respond;{card_index}') + ses.game.rpc_log.append(f"{ses.name};respond;{card_index}") ses.respond(card_index) + @sio.event @bang_handler def choose(sid, card_index): ses: Player = sio.get_session(sid) - ses.game.rpc_log.append(f'{ses.name};choose;{card_index}') + ses.game.rpc_log.append(f"{ses.name};choose;{card_index}") ses.choose(card_index) + @sio.event @bang_handler def scrap(sid, card_index): ses: Player = sio.get_session(sid) - ses.game.rpc_log.append(f'{ses.name};scrap;{card_index}') + ses.game.rpc_log.append(f"{ses.name};scrap;{card_index}") ses.scrap(card_index) + @sio.event def special(sid, data): ses: Player = sio.get_session(sid) - ses.game.rpc_log.append(f'{ses.name};special;{json.dumps(data)}') + ses.game.rpc_log.append(f"{ses.name};special;{json.dumps(data)}") ses.special(data) + @sio.event @bang_handler def gold_rush_discard(sid): ses: Player = sio.get_session(sid) - ses.game.rpc_log.append(f'{ses.name};gold_rush_discard;') + ses.game.rpc_log.append(f"{ses.name};gold_rush_discard;") ses.gold_rush_discard() + @sio.event @bang_handler -def buy_gold_rush_card(sid, data:int): +def buy_gold_rush_card(sid, data: int): ses: Player = sio.get_session(sid) - ses.game.rpc_log.append(f'{ses.name};buy_gold_rush_card;{data}') + ses.game.rpc_log.append(f"{ses.name};buy_gold_rush_card;{data}") ses.buy_gold_rush_card(data) + @sio.event @bang_handler def chat_message(sid, msg, pl=None): ses: Player = sio.get_session(sid) if pl is None else pl - ses.game.rpc_log.append(f'{ses.name};chat_message;{msg}') + ses.game.rpc_log.append(f"{ses.name};chat_message;{msg}") if len(msg) > 0: - if msg[0] == '/': - commands = msg.split(';') + if msg[0] == "/": + commands = msg.split(";") for msg in commands: - if '/addbot' in msg and not ses.game.started: + if "/addbot" in msg and not ses.game.started: if len(msg.split()) > 1: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'Only 1 bot at the time'}) + sio.emit( + "chat_message", + room=ses.game.name, + data={"color": "red", "text": "Only 1 bot at the time"}, + ) else: - bot = Player(f'AI_{random.randint(0,10)}', 'bot', bot=True) + bot = Player(f"AI_{random.randint(0,10)}", "bot", bot=True) while any((p for p in ses.game.players if p.name == bot.name)): - bot = Player(f'AI_{random.randint(0,10)}', 'bot', bot=True) + bot = Player(f"AI_{random.randint(0,10)}", "bot", bot=True) ses.game.add_player(bot) advertise_lobbies() sio.start_background_task(bot.bot_spin) return - if '/replay' in msg and not '/replayspeed' in msg and not '/replaypov' in msg: + if ( + "/replay" in msg + and not "/replayspeed" in msg + and not "/replaypov" in msg + ): _cmd = msg.split() if len(_cmd) >= 2: replay_id = _cmd[1] - response = requests.get(f"https://hastebin.com/raw/{replay_id}", headers=HASTEBIN_HEADERS) + response = requests.get( + f"https://hastebin.com/raw/{replay_id}", + headers=HASTEBIN_HEADERS, + timeout=5, + ) log = response.text.splitlines() ses.game.spectators.append(ses) if len(_cmd) == 2: @@ -497,83 +713,130 @@ def chat_message(sid, msg, pl=None): line = int(_cmd[2]) ses.game.replay(log, speed=0, fast_forward=line) return - if '/replayspeed' in msg: + if "/replayspeed" in msg: _cmd = msg.split() if len(_cmd) == 2: ses.game.replay_speed = float(_cmd[1]) return - if '/replaypov' in msg: + if "/replaypov" in msg: _cmd = msg.split() if len(_cmd) > 1: - name = ' '.join(_cmd[1:]) + name = " ".join(_cmd[1:]) for p in ses.game.players: if p.sid == ses.sid: - p.sid = '' + p.sid = "" pl = ses.game.get_player_named(name) pl.sid = ses.sid pl.set_role(pl.role) pl.notify_self() return - if '/startwithseed' in msg and not ses.game.started: + if "/startwithseed" in msg and not ses.game.started: if len(msg.split()) > 1: ses.game.start_game(int(msg.split()[1])) return - elif '/removebot' in msg and not ses.game.started: + if "/removebot" in msg and not ses.game.started: if any((p.is_bot for p in ses.game.players)): [p for p in ses.game.players if p.is_bot][-1].disconnect() advertise_lobbies() return - elif '/togglecomp' in msg and ses.game: + if "/togglecomp" in msg and ses.game: ses.game.toggle_competitive() return - if '/debug' in msg: + if "/debug" in msg: cmd = msg.split() - if len(cmd) == 2 and 'DEPLOY_KEY' in os.environ and cmd[1] == os.environ['DEPLOY_KEY']: # solo chi ha la deploy key può attivare la modalità debug + if ( + len(cmd) == 2 + and "DEPLOY_KEY" in os.environ + and cmd[1] == os.environ["DEPLOY_KEY"] + ): # solo chi ha la deploy key può attivare la modalità debug ses.game.debug = not ses.game.debug ses.game.notify_room() - elif ses == ses.game.players[0] or ses.is_admin(): # solo l'owner può attivare la modalità debug + elif ( + ses == ses.game.players[0] or ses.is_admin() + ): # solo l'owner può attivare la modalità debug ses.game.debug = not ses.game.debug ses.game.notify_room() if ses.game.debug: - sio.emit('chat_message', room=sid, data={'color': f'red','text':f'debug mode is now active, only the owner of the room can disable it with /debug'}) + sio.emit( + "chat_message", + room=sid, + data={ + "color": "red", + "text": "debug mode is now active, only the owner of the room can disable it with /debug", + }, + ) return if not ses.game.debug and not ses.is_admin(): - sio.emit('chat_message', room=sid, data={'color': f'','text':f'debug mode is not active, only the owner of the room can enable it with /debug'}) - elif '/set_chars' in msg and not ses.game.started: + sio.emit( + "chat_message", + room=sid, + data={ + "color": "", + "text": "debug mode is not active, only the owner of the room can enable it with /debug", + }, + ) + elif "/set_chars" in msg and not ses.game.started: cmd = msg.split() if len(cmd) == 2 and int(cmd[1]) > 0: ses.game.characters_to_distribute = int(cmd[1]) - elif '/suicide' in msg and ses.game.started and ses.lives > 0: + elif "/suicide" in msg and ses.game.started and ses.lives > 0: ses.lives = 0 ses.notify_self() - elif '/nextevent' in msg and ses.game.started: + elif "/nextevent" in msg and ses.game.started: ses.game.deck.flip_event() - elif '/notify' in msg and ses.game.started: + elif "/notify" in msg and ses.game.started: cmd = msg.split() if len(cmd) >= 3: if cmd[1] in ses.game.players_map: - ses.game.get_player_named(cmd[1]).notify_card(ses, { - 'name': ' '.join(cmd[2:]), - 'icon': '🚨', - 'suit': 4, - 'number': ' '.join(cmd[2:]) - }) + ses.game.get_player_named(cmd[1]).notify_card( + ses, + { + "name": " ".join(cmd[2:]), + "icon": "🚨", + "suit": 4, + "number": " ".join(cmd[2:]), + }, + ) else: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} bad format'}) - elif '/show_cards' in msg and ses.game.started: + sio.emit( + "chat_message", + room=sid, + data={"color": "", "text": f"{msg} bad format"}, + ) + elif "/show_cards" in msg and ses.game.started: cmd = msg.split() if len(cmd) == 2: if cmd[1] in ses.game.players_map: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and is looking at {cmd[1]} hand'}) + sio.emit( + "chat_message", + room=ses.game.name, + data={ + "color": "red", + "text": f"🚨 {ses.name} is in debug mode and is looking at {cmd[1]} hand", + }, + ) for c in ses.game.get_player_named(cmd[1]).hand: ses.notify_card(ses, c) eventlet.sleep(0.3) else: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} bad format'}) - elif '/ddc' in msg and ses.game.started: # debug destroy cards usage: [/ddc *] [/ddc username] - cmd = msg.split() + sio.emit( + "chat_message", + room=sid, + data={"color": "", "text": f"{msg} bad format"}, + ) + elif ( + "/ddc" in msg and ses.game.started + ): # debug destroy cards usage: [/ddc *] [/ddc username] + cmd = msg.split() if len(cmd) == 2: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode destroyed {cmd[1]} cards'}) + sio.emit( + "chat_message", + room=ses.game.name, + data={ + "color": "red", + "text": f"🚨 {ses.name} is in debug mode destroyed {cmd[1]} cards", + }, + ) if cmd[1] == "*": for p in ses.game.players_map: ses.game.get_player_named(p).hand = [] @@ -584,54 +847,106 @@ def chat_message(sid, msg, pl=None): ses.game.get_player_named(cmd[1]).equipment = [] ses.game.get_player_named(cmd[1]).notify_self() else: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} bad format'}) - elif '/dsh' in msg and ses.game.started: #debug set health usage [/dsh * hp] [/dsh username hp] + sio.emit( + "chat_message", + room=sid, + data={"color": "", "text": f"{msg} bad format"}, + ) + elif ( + "/dsh" in msg and ses.game.started + ): # debug set health usage [/dsh * hp] [/dsh username hp] cmd = msg.split() if len(cmd) == 3: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and is changing {cmd[1]} health'}) + sio.emit( + "chat_message", + room=ses.game.name, + data={ + "color": "red", + "text": f"🚨 {ses.name} is in debug mode and is changing {cmd[1]} health", + }, + ) if cmd[1] == "*": for p in ses.game.players_map: - ses.game.get_player_named(p).lives = min(int(cmd[2]), ses.game.get_player_named(p).max_lives) + ses.game.get_player_named(p).lives = min( + int(cmd[2]), ses.game.get_player_named(p).max_lives + ) ses.game.get_player_named(p).notify_self() elif cmd[1] in ses.game.players_map: - ses.game.get_player_named(cmd[1]).lives = min(int(cmd[2]), ses.game.get_player_named(cmd[1]).max_lives) + ses.game.get_player_named(cmd[1]).lives = min( + int(cmd[2]), ses.game.get_player_named(cmd[1]).max_lives + ) ses.game.get_player_named(cmd[1]).notify_self() else: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} bad format'}) - elif '/togglebot' in msg and ses.game: + sio.emit( + "chat_message", + room=sid, + data={"color": "", "text": f"{msg} bad format"}, + ) + elif "/togglebot" in msg and ses.game: ses.game.toggle_disconnect_bot() - elif '/cancelgame' in msg and ses.game.started: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} stopped the current game'}) + elif "/cancelgame" in msg and ses.game.started: + sio.emit( + "chat_message", + room=ses.game.name, + data={ + "color": "red", + "text": f"🚨 {ses.name} stopped the current game", + }, + ) ses.game.reset() - elif '/startgame' in msg and not ses.game.started: + elif "/startgame" in msg and not ses.game.started: ses.game.start_game() - elif '/setbotspeed' in msg: + elif "/setbotspeed" in msg: ses.game.bot_speed = float(msg.split()[1]) - elif '/addex' in msg and not ses.game.started: + elif "/addex" in msg and not ses.game.started: cmd = msg.split() if len(cmd) == 2: - cmd[1] = cmd[1].replace('foc', 'fistful_of_cards') + cmd[1] = cmd[1].replace("foc", "fistful_of_cards") if cmd[1] not in ses.game.available_expansions: ses.game.available_expansions.append(cmd[1]) ses.game.notify_room() else: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} bad format'}) - elif '/setcharacter' in msg: + sio.emit( + "chat_message", + room=sid, + data={"color": "", "text": f"{msg} bad format"}, + ) + elif "/setcharacter" in msg: import bang.characters as characters + cmd = msg.split() if len(cmd) >= 2: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and changed character'}) + sio.emit( + "chat_message", + room=ses.game.name, + data={ + "color": "red", + "text": f"🚨 {ses.name} is in debug mode and changed character", + }, + ) chs = characters.all_characters(ses.game.expansions) - ses.character = [c for c in chs if c.name == ' '.join(cmd[1:])][0] + ses.character = [c for c in chs if c.name == " ".join(cmd[1:])][ + 0 + ] ses.real_character = ses.character ses.notify_self() - elif '/setevent' in msg and ses.game and ses.game.deck: #add event before the position /setevent (position) 0 (name) Peyote + elif ( + "/setevent" in msg and ses.game and ses.game.deck + ): # add event before the position /setevent (position) 0 (name) Peyote cmd = msg.split() if len(cmd) >= 3: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and changed event'}) + sio.emit( + "chat_message", + room=ses.game.name, + data={ + "color": "red", + "text": f"🚨 {ses.name} is in debug mode and changed event", + }, + ) import bang.expansions.fistful_of_cards.card_events as ce import bang.expansions.high_noon.card_events as ceh import bang.expansions.wild_west_show.card_events as cew + chs = [] chs.extend(ce.get_all_events()) chs.append(ce.get_endgame_card()) @@ -639,228 +954,402 @@ def chat_message(sid, msg, pl=None): chs.append(ceh.get_endgame_card()) chs.extend(cew.get_all_events()) chs.append(cew.get_endgame_card()) - ses.game.deck.event_cards.insert(int(cmd[1]), [c for c in chs if c is not None and c.name == ' '.join(cmd[2:])][0]) + ses.game.deck.event_cards.insert( + int(cmd[1]), + [ + c + for c in chs + if c is not None and c.name == " ".join(cmd[2:]) + ][0], + ) ses.game.notify_event_card() - elif '/removecard' in msg: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and removed a card'}) + elif "/removecard" in msg: + sio.emit( + "chat_message", + room=ses.game.name, + data={ + "color": "red", + "text": f"🚨 {ses.name} is in debug mode and removed a card", + }, + ) cmd = msg.split() if len(cmd) == 2: if int(cmd[1]) < len(ses.hand): ses.hand.pop(int(cmd[1])) else: - ses.equipment.pop(int(cmd[1])-len(ses.hand)) + ses.equipment.pop(int(cmd[1]) - len(ses.hand)) ses.notify_self() - elif '/getcard' in msg: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and got a card'}) + elif "/getcard" in msg: + sio.emit( + "chat_message", + room=ses.game.name, + data={ + "color": "red", + "text": f"🚨 {ses.name} is in debug mode and got a card", + }, + ) import bang.cards as cs + cmd = msg.split() if len(cmd) >= 2: - cards = cs.get_starting_deck(ses.game.expansions) - card_names = ' '.join(cmd[1:]).split(',') + cards = cs.get_starting_deck(ses.game.expansions) + card_names = " ".join(cmd[1:]).split(",") for cn in card_names: - ses.hand.append([c for c in cards if c.name.lower() == cn.lower() or c.name[0:-1].lower() == cn.lower()][0]) + ses.hand.append( + [ + c + for c in cards + if c.name.lower() == cn.lower() + or c.name[0:-1].lower() == cn.lower() + ][0] + ) ses.notify_self() - elif '/equipcard' in msg: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and got a card'}) + elif "/equipcard" in msg: + sio.emit( + "chat_message", + room=ses.game.name, + data={ + "color": "red", + "text": f"🚨 {ses.name} is in debug mode and got a card", + }, + ) import bang.cards as cs + cmd = msg.split() if len(cmd) >= 2: - cards = cs.get_starting_deck(ses.game.expansions) - card_names = ' '.join(cmd[1:]).split(',') + cards = cs.get_starting_deck(ses.game.expansions) + card_names = " ".join(cmd[1:]).split(",") for cn in card_names: - ses.equipment.append([c for c in cards if c.name.lower() == cn.lower() or c.name[0:-1].lower() == cn.lower()][0]) + ses.equipment.append( + [ + c + for c in cards + if c.name.lower() == cn.lower() + or c.name[0:-1].lower() == cn.lower() + ][0] + ) ses.notify_self() - elif '/getset' in msg: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and got a card'}) + elif "/getset" in msg: + sio.emit( + "chat_message", + room=ses.game.name, + data={ + "color": "red", + "text": f"🚨 {ses.name} is in debug mode and got a card", + }, + ) cmd = msg.split() if len(cmd) >= 2: from bang.expansions import DodgeCity, TheValleyOfShadows - if cmd[1] == 'dodgecity': + + if cmd[1] == "dodgecity": ses.hand = DodgeCity.get_cards() ses.notify_self() - elif 'valley' in cmd[1].lower(): + elif "valley" in cmd[1].lower(): ses.hand = TheValleyOfShadows.get_cards() ses.notify_self() - elif '/getnuggets' in msg: - sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and got nuggets'}) + elif "/getnuggets" in msg: + sio.emit( + "chat_message", + room=ses.game.name, + data={ + "color": "red", + "text": f"🚨 {ses.name} is in debug mode and got nuggets", + }, + ) import bang.cards as cs + cmd = msg.split() if len(cmd) == 2: ses.gold_nuggets += int(cmd[1]) ses.notify_self() - elif '/gameinfo' in msg: - sio.emit('chat_message', room=sid, data={'color': f'', 'text':json.dumps(ses.game.__dict__, default=lambda o: f'<{o.__class__.__name__}() not serializable>'), 'type': 'json'}) - elif '/status' in msg and ses.is_admin(): - sio.emit('mount_status', room=sid) - elif '/meinfo' in msg: - sio.emit('chat_message', room=sid, data={'color': f'', 'text':json.dumps(ses.__dict__, default=lambda o: f'<{o.__class__.__name__}() not serializable>'), 'type': 'json'}) - elif '/playerinfo' in msg: + elif "/gameinfo" in msg: + sio.emit( + "chat_message", + room=sid, + data={ + "color": "", + "text": json.dumps( + ses.game.__dict__, + default=lambda o: f"<{o.__class__.__name__}() not serializable>", + ), + "type": "json", + }, + ) + elif "/status" in msg and ses.is_admin(): + sio.emit("mount_status", room=sid) + elif "/meinfo" in msg: + sio.emit( + "chat_message", + room=sid, + data={ + "color": "", + "text": json.dumps( + ses.__dict__, + default=lambda o: f"<{o.__class__.__name__}() not serializable>", + ), + "type": "json", + }, + ) + elif "/playerinfo" in msg: cmd = msg.split() if len(cmd) == 2: - sio.emit('chat_message', room=sid, data={'color': f'', 'text':json.dumps(ses.game.get_player_named(cmd[1]).__dict__, default=lambda o: f'<{o.__class__.__name__}() not serializable>'), 'type': 'json'}) - elif '/cardinfo' in msg: + sio.emit( + "chat_message", + room=sid, + data={ + "color": "", + "text": json.dumps( + ses.game.get_player_named(cmd[1]).__dict__, + default=lambda o: f"<{o.__class__.__name__}() not serializable>", + ), + "type": "json", + }, + ) + elif "/cardinfo" in msg: cmd = msg.split() if len(cmd) == 2: - sio.emit('chat_message', room=sid, data={'color': f'', 'text':json.dumps(ses.hand[int(cmd[1])].__dict__, default=lambda o: f'<{o.__class__.__name__}() not serializable>'), 'type': 'json'}) - elif '/mebot' in msg: + sio.emit( + "chat_message", + room=sid, + data={ + "color": "", + "text": json.dumps( + ses.hand[int(cmd[1])].__dict__, + default=lambda o: f"<{o.__class__.__name__}() not serializable>", + ), + "type": "json", + }, + ) + elif "/mebot" in msg: ses.is_bot = not ses.is_bot - if (ses.is_bot): + if ses.is_bot: ses.was_player = True sio.start_background_task(ses.bot_spin) - elif '/arcadekick' in msg and ses.game.started: - if not any((p.pending_action != PendingAction.WAIT for p in ses.game.players)): - sio.emit('chat_message', room=ses.game.name, data={'color': f'','text':f'KICKING THE ARCADE CABINET'}) + elif "/arcadekick" in msg and ses.game.started: + if not any( + ( + p.pending_action != PendingAction.WAIT + for p in ses.game.players + ) + ): + sio.emit( + "chat_message", + room=ses.game.name, + data={"color": "", "text": "KICKING THE ARCADE CABINET"}, + ) ses.game.next_turn() else: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'{msg} COMMAND NOT FOUND'}) + sio.emit( + "chat_message", + room=sid, + data={"color": "", "text": f"{msg} COMMAND NOT FOUND"}, + ) else: # get a color from sid - color = sid.encode('utf-8').hex()[0:6] - #bg color will be slightly darker and transparent - bg_color = f'{int(color[0:2],16)-10:02x}{int(color[2:4],16)-10:02x}{int(color[4:6],16)-10:02x}20' - sio.emit('chat_message', room=ses.game.name, data={'color': f'#{color}', 'bgcolor': f'#{bg_color}','text':f'[{ses.name}]: {msg}'}) + color = sid.encode("utf-8").hex()[0:6] + # bg color will be slightly darker and transparent + bg_color = f"{int(color[0:2],16)-10:02x}{int(color[2:4],16)-10:02x}{int(color[4:6],16)-10:02x}20" + sio.emit( + "chat_message", + room=ses.game.name, + data={ + "color": f"#{color}", + "bgcolor": f"#{bg_color}", + "text": f"[{ses.name}]: {msg}", + }, + ) if not ses.game.is_replay: - Metrics.send_metric('chat_message', points=[1], tags=[f'game:{ses.game.name.replace(" ","_")}']) - + Metrics.send_metric( + "chat_message", + points=[1], + tags=[f'game:{ses.game.name.replace(" ","_")}'], + ) """ Sockets for the help screen """ + @sio.event @bang_handler def get_cards(sid): import bang.cards as c - cards = c.get_starting_deck(['dodge_city']) - cards_dict = {} - for ca in cards: - if ca.name not in cards_dict: - cards_dict[ca.name] = ca - cards = [cards_dict[i] for i in cards_dict] - sio.emit('cards_info', room=sid, data=json.dumps(cards, default=lambda o: o.__dict__)) - Metrics.send_metric('help_screen_viewed', points=[1]) + + cards = c.get_starting_deck(["dodge_city"]) + cards_dict = {ca.name: ca for ca in cards} + sio.emit( + "cards_info", + room=sid, + data=json.dumps(list(cards_dict.values()), default=lambda o: o.__dict__), + ) + Metrics.send_metric("help_screen_viewed", points=[1]) + @sio.event @bang_handler def get_characters(sid): import bang.characters as ch - cards = ch.all_characters(['dodge_city', 'gold_rush', 'the_valley_of_shadows', 'wild_west_show']) - sio.emit('characters_info', room=sid, data=json.dumps(cards, default=lambda o: o.__dict__)) + + cards = ch.all_characters( + ["dodge_city", "gold_rush", "the_valley_of_shadows", "wild_west_show"] + ) + sio.emit( + "characters_info", + room=sid, + data=json.dumps(cards, default=lambda o: o.__dict__), + ) + @sio.event @bang_handler def get_highnooncards(sid): import bang.expansions.high_noon.card_events as ceh + chs = [] chs.extend(ceh.get_all_events()) chs.append(ceh.get_endgame_card()) - sio.emit('highnooncards_info', room=sid, data=json.dumps(chs, default=lambda o: o.__dict__)) + sio.emit( + "highnooncards_info", + room=sid, + data=json.dumps(chs, default=lambda o: o.__dict__), + ) + @sio.event @bang_handler def get_foccards(sid): import bang.expansions.fistful_of_cards.card_events as ce + chs = [] chs.extend(ce.get_all_events()) chs.append(ce.get_endgame_card()) - sio.emit('foccards_info', room=sid, data=json.dumps(chs, default=lambda o: o.__dict__)) + sio.emit( + "foccards_info", room=sid, data=json.dumps(chs, default=lambda o: o.__dict__) + ) + @sio.event @bang_handler def get_goldrushcards(sid): import bang.expansions.gold_rush.shop_cards as grc + cards = grc.get_cards() - cards_dict = {} - for ca in cards: - if ca.name not in cards_dict: - cards_dict[ca.name] = ca - cards = [cards_dict[i] for i in cards_dict] - sio.emit('goldrushcards_info', room=sid, data=json.dumps(cards, default=lambda o: o.__dict__)) + cards_dict = {ca.name: ca for ca in cards} + sio.emit( + "goldrushcards_info", + room=sid, + data=json.dumps(list(cards_dict.values()), default=lambda o: o.__dict__), + ) + @sio.event @bang_handler def get_valleyofshadowscards(sid): import bang.expansions.the_valley_of_shadows.cards as tvos + cards = tvos.get_starting_deck() - cards_dict = {} - for ca in cards: - if ca.name not in cards_dict: - cards_dict[ca.name] = ca - cards = [cards_dict[i] for i in cards_dict] - sio.emit('valleyofshadows_info', room=sid, data=json.dumps(cards, default=lambda o: o.__dict__)) + cards_dict = {ca.name: ca for ca in cards} + sio.emit( + "valleyofshadows_info", + room=sid, + data=json.dumps(list(cards_dict.values()), default=lambda o: o.__dict__), + ) + @sio.event @bang_handler def get_wildwestshowcards(sid): import bang.expansions.wild_west_show.card_events as wwce + chs = [] chs.extend(wwce.get_all_events()) chs.append(wwce.get_endgame_card()) - sio.emit('wwscards_info', room=sid, data=json.dumps(chs, default=lambda o: o.__dict__)) + sio.emit( + "wwscards_info", room=sid, data=json.dumps(chs, default=lambda o: o.__dict__) + ) + @sio.event @bang_handler def discord_auth(sid, data): - res = requests.post('https://discord.com/api/oauth2/token', data={ - 'client_id': '1059452581027532880', - 'client_secret': 'Mc8ZlMQhayzi1eOqWFtGHs3L0iXCzaEu', - 'grant_type': 'authorization_code', - 'redirect_uri': data['origin'], - 'code': data['code'], - }) + res = requests.post( + "https://discord.com/api/oauth2/token", + data={ + "client_id": "1059452581027532880", + "client_secret": "Mc8ZlMQhayzi1eOqWFtGHs3L0iXCzaEu", + "grant_type": "authorization_code", + "redirect_uri": data["origin"], + "code": data["code"], + }, + timeout=2, + ) if res.status_code == 200: - sio.emit('discord_auth_succ', room=sid, data=res.json()) + sio.emit("discord_auth_succ", room=sid, data=res.json()) def pool_metrics(): while True: sio.sleep(60) - Metrics.send_metric('lobbies', points=[sum(not g.is_replay for g in games.values())]) - Metrics.send_metric('online_players', points=[online_players]) + Metrics.send_metric( + "lobbies", points=[sum(not g.is_replay for g in games.values())] + ) + Metrics.send_metric("online_players", points=[online_players]) -import urllib.parse -class CustomProxyFix(object): + +class CustomProxyFix: def __init__(self, app): self.app = app - print('init') + print("init") def __call__(self, environ, start_response): - path = environ.get('PATH_INFO', '') - if 'ddproxy' in path: - newurl = urllib.parse.unquote(environ['QUERY_STRING'].replace('ddforward=', '')) - heads = {'X-Forwarded-For': environ['REMOTE_ADDR']} - for h in environ['headers_raw']: - heads[h[0]] = h[1] - r = requests.post(newurl, data=environ['wsgi.input'].read(), headers=heads) - start_response('200 OK', []) - return [''] + path = environ.get("PATH_INFO", "") + if "ddproxy" in path: + newurl = urllib.parse.unquote( + environ["QUERY_STRING"].replace("ddforward=", "") + ) + heads = {"X-Forwarded-For": environ["REMOTE_ADDR"]} + for header in environ["headers_raw"]: + heads[header[0]] = header[1] + requests.post( + newurl, data=environ["wsgi.input"].read(), headers=heads, timeout=4 + ) + start_response("200 OK", []) + return [""] return self.app(environ, start_response) -discord_ci = '1059452581027532880' -discord_cs = 'Mc8ZlMQhayzi1eOqWFtGHs3L0iXCzaEu' -import pickle def save_games(): - global save_lock while True: sio.sleep(2) - if not save_lock: + if not SAVE_LOCK: if not os.path.exists("save"): os.mkdir("save") - with open('./save/games.pickle', 'wb') as f: - pickle.dump([g for g in games.values() if g.started and not g.is_replay and not g.is_hidden and len(g.players) > 0], f) + with open("./save/games.pickle", "wb") as save_file: + pickle.dump( + [ + g + for g in games.values() + if g.started + and not g.is_replay + and not g.is_hidden + and len(g.players) > 0 + ], + save_file, + ) -if __name__ == '__main__': - if os.path.exists('./save/games.pickle'): + +if __name__ == "__main__": + if os.path.exists("./save/games.pickle"): try: - with open('./save/games.pickle', 'rb') as file: + with open("./save/games.pickle", "rb") as file: temp_g = pickle.load(file) games = {g.name: g for g in temp_g} for g in games.values(): g.spectators = [] for p in g.players: - if p.sid != 'bot': + if p.sid != "bot": sio.start_background_task(p.disconnect) else: sio.start_background_task(p.bot_spin) @@ -868,4 +1357,4 @@ if __name__ == '__main__': pass sio.start_background_task(save_games) sio.start_background_task(pool_metrics) - eventlet.wsgi.server(eventlet.listen(('', 5001)), CustomProxyFix(app)) + eventlet.wsgi.server(eventlet.listen(("", 5001)), CustomProxyFix(app)) diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py index 02a3aa6..3e57ac0 100644 --- a/backend/tests/__init__.py +++ b/backend/tests/__init__.py @@ -4,18 +4,21 @@ from bang.game import Game from bang.players import Player from tests.dummy_socket import DummySocket from globals import G + G.sio = DummySocket() -def started_game(expansions, players=4, character=Character('test_char', 4)): - g = Game('test') +def started_game(expansions, players=4, character=Character("test_char", 4)): + g = Game("test") g.expansions = expansions - ps = [Player(f'p{i}', f'p{i}') for i in range(players)] + ps = [Player(f"p{i}", f"p{i}") for i in range(players)] for p in ps: g.add_player(p) g.start_game() for p in ps: p.available_characters = [character] + if "high_noon" in expansions: + p.available_characters.append(Character("test_char2", 4)) p.set_character(p.available_characters[0].name) return g @@ -34,6 +37,6 @@ def next_player(g: Game): def current_player_with_cards(g: Game, cards): p = current_player(g) - p.draw('') + p.draw("") p.hand = cards return p diff --git a/backend/tests/game_test.py b/backend/tests/game_test.py index c9cf45e..280138c 100644 --- a/backend/tests/game_test.py +++ b/backend/tests/game_test.py @@ -3,16 +3,17 @@ from bang.game import Game from bang.players import Player, PendingAction from bang.roles import * from bang.cards import * +from tests import started_game + # test that game can start def test_game_start(): - - g = Game('test') - p1 = Player('p1', 'p1') + g = Game("test") + p1 = Player("p1", "p1") g.add_player(p1) - p2 = Player('p2', 'p2') + p2 = Player("p2", "p2") g.add_player(p2) - p3 = Player('p3', 'p3') + p3 = Player("p3", "p3") g.add_player(p3) assert p1.role is None assert p2.role is None @@ -34,57 +35,68 @@ def test_game_start(): assert p3.character is not None assert g.players[g.turn].pending_action == PendingAction.DRAW + # test that dodge_city is added to games with more than 8 players def test_dodge_city(): - - g = Game('test') + g = Game("test") for i in range(9): - p = Player(f'p{i}', f'p{i}') + p = Player(f"p{i}", f"p{i}") g.add_player(p) - assert 'dodge_city' in g.expansions + assert "dodge_city" in g.expansions + # test that a game with 2 players has only renegade as role def test_renegade_only(): - - g = Game('test') - p1 = Player('p1', 'p1') + g = Game("test") + p1 = Player("p1", "p1") g.add_player(p1) - p2 = Player('p2', 'p2') + p2 = Player("p2", "p2") g.add_player(p2) g.start_game() assert isinstance(g.players[0].role, Renegade) assert isinstance(g.players[1].role, Renegade) + # test that a game with 3 player has Renegade, Vice and Outlaw as roles def test_renegade_vice_outlaw(): - - g = Game('test') + g = Game("test") for i in range(3): - p = Player(f'p{i}', f'p{i}') + p = Player(f"p{i}", f"p{i}") g.add_player(p) g.start_game() roles = {p.role.name for p in g.players} assert len(roles) == 3 + # test that a game with 4 players has all roles except the deputy def test_4_players_roles(): - - g = Game('test') + g = Game("test") for i in range(4): - p = Player(f'p{i}', f'p{i}') + p = Player(f"p{i}", f"p{i}") g.add_player(p) g.start_game() roles = {p.role.name for p in g.players} assert len(roles) == 3 + # test that a game with 5 players has all roles def test_5_players_roles(): - - g = Game('test') + g = Game("test") for i in range(5): - p = Player(f'p{i}', f'p{i}') + p = Player(f"p{i}", f"p{i}") g.add_player(p) g.start_game() roles = {p.role.name for p in g.players} assert len(roles) == 4 + +def test_expansions(): + started_game( + [ + "high_noon", + "dodge_city", + "gold_rush", + "the_valley_of_shadows", + "wild_west_show", + ] + ) diff --git a/frontend/src/components/Deck.vue b/frontend/src/components/Deck.vue index 3aa070d..0970f50 100644 --- a/frontend/src/components/Deck.vue +++ b/frontend/src/components/Deck.vue @@ -14,7 +14,12 @@
- + +
+
+
+
+
@@ -34,6 +39,9 @@

{{eventDesc}}

+ +

🎪 {{eventDescWildWestShow}} 🎪

+

🤑️ {{$t(`cards.${goldRushDesc.name}.desc`)}} 🤑️

@@ -71,6 +79,7 @@ export default { }, lastScrap: null, eventCard: null, + eventCardWildWestShow: null, previousScrap: null, pending_action: false, isPlaying: true, @@ -102,6 +111,14 @@ export default { expansion: 'fistful-of-cards', } : card }, + event_card_wildwestshow(card) { + this.eventCardWildWestShow = card == false ? { + name: 'Wild West Show', + icon: '🎪', + back: true, + expansion: 'wild-west-show', + } : card + }, gold_rush_shop(cards) { console.log('GOLD RUSH:'+ cards) this.goldRushCards = JSON.parse(cards) @@ -122,13 +139,28 @@ export default { classes[this.eventCard.expansion] = true return classes }, + eventWwsClasses() { + let classes = { + 'last-event':true, + 'back':this.eventCardWildWestShow.back, + 'wild-west-show':true, + } + return classes + }, eventDesc() { this.eventCard; if (this.eventCard.name !== 'PewPew!'){ return this.$t(`cards.${this.eventCard.name}.desc`) } return "" - } + }, + eventDescWildWestShow() { + this.eventCardWildWestShow; + if (this.eventCardWildWestShow.name !== 'PewPew!'){ + return this.$t(`cards.${this.eventCardWildWestShow.name}.desc`) + } + return "" + }, }, methods: { action(pile) { @@ -143,9 +175,9 @@ export default { buy_gold_rush_card(index) { this.$socket.emit('buy_gold_rush_card', index) }, - event() { + event(pile='event') { if (this.pending_action !== false) { - this.$socket.emit('draw', 'event') + this.$socket.emit('draw', pile) } }, setdesc() { diff --git a/frontend/src/components/Lobby.vue b/frontend/src/components/Lobby.vue index 61eab6b..74c4073 100644 --- a/frontend/src/components/Lobby.vue +++ b/frontend/src/components/Lobby.vue @@ -66,6 +66,7 @@ :donotlocalize="true" :card="startGameCard" @click.native="startGame" + style="margin-top: 36pt;" />
- {{ + {{ getActionEmoji(p) }}
@@ -186,6 +187,7 @@ :donotlocalize="true" :card="shufflePlayersCard" @click.native="shufflePlayers" + style="margin-top: 36pt;" class="fistful-of-cards" /> @@ -803,13 +805,18 @@ export default { right: -35pt; transform: scale(0.45); transform-origin: 50% 0%; - top: 10pt; + top: 4pt; } .tiny-health { display: flex; justify-content: space-evenly; transform: scale(0.8); - margin-bottom: -4pt; + margin-top: -16pt; + position: absolute; + z-index: 1; + top: 0; + left: 0; + right: 0; } .tiny-equipment .card { transform: rotate(2deg); @@ -823,7 +830,7 @@ export default { } .tiny-character { position: absolute; - transform: scale(0.5) translate(-80px, -40px); + transform: translate(-30pt, -30pt) scale(0.5); top: 0; } .players-table { @@ -888,6 +895,7 @@ export default { } .player-in-table { transition: all 0.2s ease-in-out; + margin-top: 26pt; } .player-in-table:hover { transform: translateY(-5px) scale(1.05);