diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 2fe0e63..f902814 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -3,7 +3,7 @@ on: pull_request: jobs: - buildx: + build-amd64: runs-on: ubuntu-latest steps: - @@ -31,6 +31,24 @@ jobs: --output "type=image,push=false" \ --tag albertoxamin/bang:test \ --file ./Dockerfile ./ + build-arm64: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Set up Docker Buildx + uses: crazy-max/ghaction-docker-buildx@v3 + - + name: Cache Docker layers + uses: actions/cache@v3 + id: cache + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- - name: Docker Buildx (test build arm64) run: | @@ -41,3 +59,31 @@ jobs: --output "type=image,push=false" \ --tag albertoxamin/bang:test \ --file ./Dockerfile ./ + build-armv-7: + runs-on: ubuntu-latest + steps: + - + name: Checkout + uses: actions/checkout@v3 + - + name: Set up Docker Buildx + uses: crazy-max/ghaction-docker-buildx@v3 + - + name: Cache Docker layers + uses: actions/cache@v3 + id: cache + with: + path: /tmp/.buildx-cache + key: ${{ runner.os }}-buildx-${{ github.sha }} + restore-keys: | + ${{ runner.os }}-buildx- + - + name: Docker Buildx (test build armv-7) + run: | + docker buildx build \ + --cache-from "type=local,src=/tmp/.buildx-cache" \ + --cache-to "type=local,dest=/tmp/.buildx-cache" \ + --platform linux/arm/v7 \ + --output "type=image,push=false" \ + --tag albertoxamin/bang:test \ + --file ./Dockerfile ./ diff --git a/backend/bang/cards.py b/backend/bang/cards.py index 17970d9..1c6a2b5 100644 --- a/backend/bang/cards.py +++ b/backend/bang/cards.py @@ -216,7 +216,7 @@ class Bang(Card): def play_card(self, player, against, _with=None): 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 != 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 != None): # 42 gold rush: return False elif against != None: import bang.characters as chars @@ -377,7 +377,7 @@ class Mancato(Card): def play_card(self, player, against, _with=None): import bang.characters as chars if against != 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 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 @@ -445,7 +445,7 @@ class WellsFargo(Card): def get_starting_deck(expansions:List[str]) -> List[Card]: - from bang.expansions import DodgeCity + from bang.expansions import DodgeCity, TheValleyOfShadows base_cards = [ Barile(Suit.SPADES, 'Q'), Barile(Suit.SPADES, 'K'), @@ -530,5 +530,7 @@ def get_starting_deck(expansions:List[str]) -> List[Card]: ] if 'dodge_city' in expansions: base_cards.extend(DodgeCity.get_cards()) + if 'the_valley_of_shadows' in expansions: + base_cards.extend(TheValleyOfShadows.get_cards()) return base_cards diff --git a/backend/bang/characters.py b/backend/bang/characters.py index 47c66c4..460f996 100644 --- a/backend/bang/characters.py +++ b/backend/bang/characters.py @@ -9,6 +9,7 @@ class Character(ABC): self.max_lives = max_lives self.sight_mod = sight_mod self.visibility_mod = visibility_mod + self.is_character = True self.pick_mod = pick_mod self.desc = desc self.icon = 'πŸ€·β€β™‚οΈ' @@ -145,7 +146,7 @@ class WillyTheKid(Character): self.icon = 'πŸŽ‰' def all_characters(expansions: List[str]): - from bang.expansions import DodgeCity + from bang.expansions import DodgeCity, TheValleyOfShadows base_chars = [ BartCassidy(), BlackJack(), @@ -168,4 +169,6 @@ def all_characters(expansions: List[str]): base_chars.extend(DodgeCity.get_characters()) if 'gold_rush' in expansions: base_chars.extend(GoldRush.get_characters()) + if 'the_valley_of_shadows' in expansions: + base_chars.extend(TheValleyOfShadows.get_characters()) return base_chars \ No newline at end of file diff --git a/backend/bang/deck.py b/backend/bang/deck.py index 26a207a..cb138e1 100644 --- a/backend/bang/deck.py +++ b/backend/bang/deck.py @@ -52,7 +52,7 @@ class Deck: self.game.notify_event_card() def fill_gold_rush_shop(self): - if not any([c == None for c in self.shop_cards]): + if not any((c == None for c in self.shop_cards)): return for i in range(3): if self.shop_cards[i] == None: diff --git a/backend/bang/expansions/__init__.py b/backend/bang/expansions/__init__.py index 616b8c7..ebae2dd 100644 --- a/backend/bang/expansions/__init__.py +++ b/backend/bang/expansions/__init__.py @@ -13,3 +13,12 @@ class GoldRush(): def get_characters(): from bang.expansions.gold_rush import characters return characters.all_characters() + +class TheValleyOfShadows(): + def get_characters(): + from bang.expansions.the_valley_of_shadows import characters + return characters.all_characters() + + def get_cards(): + from bang.expansions.the_valley_of_shadows import cards + return cards.get_starting_deck() diff --git a/backend/bang/expansions/dodge_city/cards.py b/backend/bang/expansions/dodge_city/cards.py index 8dd7b49..af0f7aa 100644 --- a/backend/bang/expansions/dodge_city/cards.py +++ b/backend/bang/expansions/dodge_city/cards.py @@ -75,7 +75,7 @@ class Rissa(CatBalou): def play_card(self, player, against, _with): if _with != None: - if len([p for p in player.game.players if p != player and (len(p.hand)+len(p.equipment)) > 0]) == 0: + if not any((p != player and (len(p.hand)+len(p.equipment)) > 0 for p in player.game.players)): return False #se sono qui vuol dire che ci sono giocatori con carte in mano oltre a me player.rissa_targets = [] diff --git a/backend/bang/expansions/dodge_city/characters.py b/backend/bang/expansions/dodge_city/characters.py index d2c96de..0e1d3cd 100644 --- a/backend/bang/expansions/dodge_city/characters.py +++ b/backend/bang/expansions/dodge_city/characters.py @@ -90,9 +90,9 @@ class ChuckWengam(Character): if player.lives > 1 and player.is_my_turn: import bang.expansions.gold_rush.shop_cards as grc player.lives -= 1 - if len([c for c in player.gold_rush_equipment if isinstance(c, grc.Talismano)]) > 0: + if any((isinstance(c, grc.Talismano) for c in player.gold_rush_equipment)): player.gold_nuggets += 1 - if len([c for c in player.gold_rush_equipment if isinstance(c, grc.Stivali)]) > 0: + if any((isinstance(c, grc.Stivali) for c in player.gold_rush_equipment)): player.hand.append(player.game.deck.draw(True)) player.hand.append(player.game.deck.draw(True)) player.hand.append(player.game.deck.draw(True)) diff --git a/backend/bang/expansions/the_valley_of_shadows/__init__.py b/backend/bang/expansions/the_valley_of_shadows/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/bang/expansions/the_valley_of_shadows/cards.py b/backend/bang/expansions/the_valley_of_shadows/cards.py new file mode 100644 index 0000000..b1549b9 --- /dev/null +++ b/backend/bang/expansions/the_valley_of_shadows/cards.py @@ -0,0 +1,231 @@ +from typing import List +import bang.roles as r +import bang.players as pl +from bang.cards import Card, Suit, Bang, Mancato +import bang.expansions.fistful_of_cards.card_events as ce + +class Fantasma(Card): + def __init__(self, suit, number): + super().__init__(suit, 'Fantasma', number, is_equipment=True) + self.icon = 'πŸ‘»οΈ' #porta in vita i giocatori morti ma non + + def play_card(self, player, against, _with=None): + if (player.game.check_event(ce.IlGiudice)): + return False + if len(player.game.get_dead_players(include_ghosts=False)) > 0: + player.pending_action = pl.PendingAction.CHOOSE + player.choose_text = 'choose_fantasma' + player.available_cards = [{ + 'name': p.name, + 'icon': p.role.icon if(player.game.initial_players == 3) else '⭐️' if isinstance(p.role, r.Sheriff) else '🀠', + 'avatar': p.avatar, + 'alt_text': ''.join(['❀️']*p.lives)+''.join(['πŸ’€']*(p.max_lives-p.lives)), + 'is_character': True, + 'noDesc': True + } for p in player.game.get_dead_players(include_ghosts=False)] + player.game.deck.scrap(self, True) + return True + return False + +class Lemat(Card): + def __init__(self, suit, number): + super().__init__(suit, 'Lemat', number, is_equipment=True, is_weapon=True, range=1) + self.icon = 'πŸ”«' # ogni carta puΓ² essere usata come bang + #TODO + +class SerpenteASonagli(Card): + def __init__(self, suit, number): + super().__init__(suit, 'SerpenteASonagli', number, is_equipment=True) + self.need_target = True + self.icon = '🐍️' # Ogni turno pesca se il seme picche -1hp + self.alt_text = "♠️ =πŸ’”" + + def play_card(self, player, against, _with=None): + if (player.game.check_event(ce.IlGiudice)): + return False + if against != None: + self.reset_card() + player.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 Shotgun(Card): + def __init__(self, suit, number): + super().__init__(suit, 'Shotgun', number, is_equipment=True, is_weapon=True, range=1) + self.icon = 'πŸ”«' # Ogni volta che colpisci un giocatore deve scartare una carta + +class Taglia(Card): + def __init__(self, suit, number): + super().__init__(suit, 'Taglia', number, is_equipment=True) + self.need_target = True + self.icon = 'πŸ’°' # chiunque colpisca il giocatore con la taglia pesca una carta dal mazzo, si toglie solo con panico, cat balou, dalton + + def play_card(self, player, against, _with=None): + if (player.game.check_event(ce.IlGiudice)): + return False + if against != None: + self.reset_card() + player.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 UltimoGiro(Card): + def __init__(self, suit, number): + super().__init__(suit, 'UltimoGiro', number) + self.icon = 'πŸ₯‚' + # self.desc = 'Recupera 1 vita' + # self.desc_eng = 'Regain 1 HP' + self.alt_text = "🍺" + + def play_card(self, player, against, _with=None): + super().play_card(player, against) + player.lives = min(player.lives+1, player.max_lives) + player.notify_self() + return True + +class Tomahawk(Card): + def __init__(self, suit, number): + super().__init__(suit, 'Tomahawk', number, range=2) + self.icon = 'πŸͺ“️' + self.alt_text = "2πŸ”Ž πŸ’₯" + # "Spara a un giocatore a distanza 2" + self.need_target = True + + def play_card(self, player, against, _with=None): + if against != None and player.game.can_card_reach(self, player, against): + super().play_card(player, against=against) + player.game.attack(player, against, card_name=self.name) + return True + return False + +class Tornado(Card): + def __init__(self, suit, number): + super().__init__(suit, 'Tornado', number) + self.icon = 'πŸŒͺ️' + + def play_card(self, player, against, _with=None): + player.game.discard_others(player, card_name=self.name) + return True + +class Sventagliata(Bang): + def __init__(self, suit, number): + super().__init__(suit, number) + self.name = 'Sventagliata' + self.icon = '🎏' + self.alt_text = "πŸ’₯πŸ’₯" # spara al target e anche, a uno a distanza 1 dal target + self.need_target = True + + def play_card(self, player, against, _with=None): + if against != None: + #TODO + # super().play_card(player, against=against) + # player.game.attack(player, against, card_name=self.name) + return True + return False + +class Salvo(Card): + def __init__(self, suit, number): + super().__init__(suit, 'Salvo', number) + self.icon = 'πŸ˜‡οΈ' + self.alt_text = "πŸ‘€πŸ˜‡οΈ" + self.need_target = True + + def play_card(self, player, against, _with=None): + if against != None: + #TODO + # super().play_card(player, against=against) + # player.game.attack(player, against, card_name=self.name) + return True + return False + +class Mira(Card): + def __init__(self, suit, number): + super().__init__(suit, 'Mira', number) + self.icon = 'πŸ‘ŒπŸ»' + self.alt_text = "πŸ’₯πŸƒπŸ’”πŸ’”" + self.need_target = True + self.need_with = True + + def play_card(self, player, against, _with=None): + if against != None: + #TODO + # super().play_card(player, against=against) + # player.game.attack(player, against, card_name=self.name) + return True + return False + +class Bandidos(Card): + def __init__(self, suit, number): + super().__init__(suit, 'Bandidos', number) + self.icon = '🀠️' + self.alt_text = "πŸ‘€πŸƒπŸƒ/πŸ’”" + + def play_card(self, player, against, _with=None): + player.game.discard_others(player, card_name=self.name) + return True + +class Fuga(Card): + def __init__(self, suit, number): + super().__init__(suit, 'Fuga', number) + self.icon = 'πŸƒπŸ»' + self.alt_text = "❌" + + def play_card(self, player, against, _with=None): + #TODO + # super().play_card(player, against=against) + # player.game.attack(player, against) + return True + +class Poker(Card): + def __init__(self, suit, number): + super().__init__(suit, 'Poker', number) + self.icon = 'πŸƒ' + self.alt_text = "πŸ‘€πŸƒ πŸƒπŸƒ" + + def play_card(self, player, against, _with=None): + #TODO + # super().play_card(player, against=against) + # player.game.attack(player, against) + return True + +class RitornoDiFiamma(Mancato): + def __init__(self, suit, number): + super().__init__(suit, number) + self.name = 'RitornoDiFiamma' + self.icon = 'πŸ”₯' + self.alt_text = "πŸ˜… | πŸ’₯" + + def play_card(self, player, against, _with=None): + return False + + def use_card(self, player): + player.notify_self() + +def get_starting_deck() -> List[Card]: + cards = [ + Fantasma(Suit.SPADES, 9), + Fantasma(Suit.SPADES, 10), + # Lemat(Suit.DIAMONDS, 4), + SerpenteASonagli(Suit.HEARTS, 7), + Shotgun(Suit.SPADES, 'K'), + Taglia(Suit.CLUBS, 9), + UltimoGiro(Suit.DIAMONDS, 8), + Tomahawk(Suit.DIAMONDS, 'A'), + # Sventagliata(Suit.SPADES, 2), + # Salvo(Suit.HEARTS, 5), + Bandidos(Suit.DIAMONDS,'Q'), # gli altri giocatori scelgono se scartare 2 carte o perdere 1 punto vita + # Fuga(Suit.HEARTS, 3), # evita l'effetto di carte marroni (tipo panico cat balou) di cui sei bersaglio + # Mira(Suit.CLUBS, 6), + # Poker(Suit.HEARTS, 'J'), # tutti gli altri scartano 1 carta a scelta, se non ci sono assi allora pesca 2 dal mazzo + RitornoDiFiamma(Suit.CLUBS, 'Q'), # un mancato che fa bang + Tornado(Suit.CLUBS, "A"), + ] + for c in cards: + c.expansion_icon = 'πŸ‘»οΈ' + return cards diff --git a/backend/bang/expansions/the_valley_of_shadows/characters.py b/backend/bang/expansions/the_valley_of_shadows/characters.py new file mode 100644 index 0000000..f2cc016 --- /dev/null +++ b/backend/bang/expansions/the_valley_of_shadows/characters.py @@ -0,0 +1,10 @@ +from typing import List +from bang.characters import Character + +def all_characters() -> List[Character]: + cards = [ + + ] + for c in cards: + c.expansion_icon = 'πŸ‘»οΈ' + return cards diff --git a/backend/bang/game.py b/backend/bang/game.py index 3548454..9cdcc13 100644 --- a/backend/bang/game.py +++ b/backend/bang/game.py @@ -5,6 +5,7 @@ import socketio import eventlet import bang.players as pl +import bang.cards as cs import bang.characters as characters import bang.expansions.dodge_city.characters as chd from bang.deck import Deck @@ -15,6 +16,34 @@ import bang.expansions.gold_rush.shop_cards as grc import bang.expansions.gold_rush.characters as grch from metrics import Metrics +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':'/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':'/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}, +] + class Game: def __init__(self, name, sio:socketio): super().__init__() @@ -52,6 +81,11 @@ class Game: self.rpc_log = [] self.is_replay = False + def shuffle_players(self): + if not self.started: + random.shuffle(self.players) + self.notify_room() + def reset(self): for p in self.players: if (p.sid == p.name): @@ -137,7 +171,7 @@ class Game: # chat_message(None, cmd[2], player) if i == fast_forward: self.replay_speed = 1.0 - + self.notify_room() eventlet.sleep(max(self.replay_speed, 0.1)) eventlet.sleep(6) if self.is_replay: @@ -145,11 +179,11 @@ class Game: def notify_room(self, sid=None): - if len([p for p in self.players if p.character == None]) != 0 or sid: + if any((p.character == None for p in self.players)) or sid: self.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 != None, 'is_bot': p.is_bot} for p in self.players], + 'players': [{'name':p.name, 'ready': p.character != 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, @@ -158,30 +192,7 @@ class Game: }) self.sio.emit('debug', room=self.name, data=self.debug) if self.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':'/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':'/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':'/mebot', 'help':'Toggles bot mode'}, - {'cmd':'/getnuggets', 'help':'Adds nuggets to yourself - sample /getnuggets 5'}, - {'cmd':'/startwithseed', 'help':'start the game with custom seed'}] - self.sio.emit('commands', room=self.name, data=commands) + self.sio.emit('commands', room=self.name, data=[x for x in debug_commands if 'admin' not in x]) else: self.sio.emit('commands', room=self.name, data=[{'cmd':'/debug', 'help':'Toggles the debug mode'}]) self.sio.emit('spectators', room=self.name, data=len(self.spectators)) @@ -203,6 +214,11 @@ class Game: self.disconnect_bot = not self.disconnect_bot self.notify_room() + def feature_flags(self): + if 'the_valley_of_shadows' not in self.expansions: + self.available_expansions.append('the_valley_of_shadows') + self.notify_room() + def add_player(self, player: pl.Player): if player.is_bot and len(self.players) >= 8: return @@ -212,6 +228,8 @@ class Game: 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') self.notify_room() @@ -231,7 +249,7 @@ class Game: def notify_character_selection(self): self.notify_room() - if len([p for p in self.players if p.character == None]) == 0: + if not any((p.character == None for p in self.players)): for i in range(len(self.players)): print(self.name, self.players[i].name, self.players[i].character) self.sio.emit('chat_message', room=self.name, data=f'_choose_character|{self.players[i].name}|{self.players[i].character.name}') @@ -303,6 +321,22 @@ class Game: self.players[i].notify_self() self.notify_event_card() + 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 p.get_discarded(attacker=attacker, card_name=card_name): + self.waiting_for += 1 + p.notify_self() + if self.waiting_for == 0: + attacker.pending_action = pl.PendingAction.PLAY + attacker.notify_self() + self.attack_in_progress = False + def attack_others(self, attacker: pl.Player, card_name:str=None): self.attack_in_progress = True attacker.pending_action = pl.PendingAction.WAIT @@ -339,6 +373,11 @@ 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): + 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 True + def attack(self, attacker: pl.Player, target_username:str, double:bool=False, card_name:str=None): if self.get_player_named(target_username).get_banged(attacker=attacker, double=double, card_name=card_name): self.ready_count = 0 @@ -426,11 +465,11 @@ class Game: print(f'{self.name}: stop roulette') target_pl.lives -= 1 target_pl.heal_if_needed() - if len([c for c in target_pl.gold_rush_equipment if isinstance(c, grc.Talismano)]) > 0: + 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 len([c for c in target_pl.gold_rush_equipment if isinstance(c, grc.Stivali)]) > 0: + if any((isinstance(c, grc.Stivali) for c in target_pl.gold_rush_equipment)): target_pl.hand.append(self.deck.draw(True)) target_pl.notify_self() self.is_russian_roulette_on = False @@ -503,14 +542,14 @@ class Game: pl.hand.append(self.deck.draw()) pl.hand.append(self.deck.draw()) pl.notify_self() - elif self.check_event(ceh.CittaFantasma): + 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') 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] != None: print(f'{self.name}: flip new event {self.deck.event_cards[0].name}') @@ -600,9 +639,9 @@ class Game: player.game = None if self.disconnect_bot and self.started: player.is_bot = True - if len([p for p in self.players if not p.is_bot]) == 0: + if not any((not p.is_bot for p in self.players)): eventlet.sleep(5) - if len([p for p in self.players if not p.is_bot]) == 0: + if not any((not p.is_bot for p in self.players)): print(f'{self.name}: no players left in game, shutting down') self.shutting_down = True self.players = [] @@ -620,7 +659,7 @@ class Game: # else: # player.lives = 0 # self.players.remove(player) - if len([p for p in self.players if not p.is_bot]) == 0: + if not any((not p.is_bot for p in self.players)): print(f'{self.name}: no players left in game, shutting down') self.shutting_down = True self.players = [] @@ -637,7 +676,7 @@ class Game: 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 len([c for c in player.gold_rush_equipment if isinstance(c, grc.Ricercato)]) > 0 and player.attacker and player.attacker in self.players: + 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 player.attacker.hand.append(self.deck.draw(True)) player.attacker.hand.append(self.deck.draw(True)) @@ -763,14 +802,15 @@ class Game: 'is_ghost': pls[j].is_ghost, 'is_bot': pls[j].is_bot, 'icon': pls[j].role.icon if (pls[j].role is not None) 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): - return [p for p in self.players if p.is_dead] + 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)] def notify_all(self): if self.started: @@ -788,6 +828,7 @@ class Game: '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()] diff --git a/backend/bang/players.py b/backend/bang/players.py index 8daf297..641892f 100644 --- a/backend/bang/players.py +++ b/backend/bang/players.py @@ -12,10 +12,23 @@ import bang.expansions.fistful_of_cards.card_events as ce import bang.expansions.high_noon.card_events as ceh import bang.expansions.gold_rush.shop_cards as grc import bang.expansions.gold_rush.characters as grch +import bang.expansions.the_valley_of_shadows.cards as tvosc import eventlet from typing import List from metrics import Metrics +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', +] + class PendingAction(IntEnum): PICK = 0 DRAW = 1 @@ -26,13 +39,46 @@ class PendingAction(IntEnum): class Player: - def __init__(self, name, sid, sio, bot=False): + def is_admin(self): + return self.discord_id in {'244893980960096266'} + + def get_avatar(self): + import requests + headers = { + 'Authorization': 'Bearer ' + self.discord_token, + } + r = requests.get('https://discordapp.com/api/users/@me', headers=headers) + if r.status_code == 200: + res = r.json() + self.avatar = f'https://cdn.discordapp.com/avatars/{res["id"]}/{res["avatar"]}.png' + if self.game: + self.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() + self.sio.emit('chat_message', room=self.sid, data={'color':'green', 'text':'(you are admin)'}) + if self.game: + self.game.notify_room() + self.sio.emit('me', data=self.name, room=self.sid) + else: + print('error getting avatar', r.status_code, r.text) + print(r) + + def __init__(self, name, sid, sio, bot=False, discord_token=None): import bang.game as g super().__init__() self.name = name self.sid = sid self.sio = sio self.is_bot = bot + self.discord_token = discord_token + self.discord_id = None + self.avatar = '' + if self.is_bot: + self.avatar = robot_pictures[randrange(len(robot_pictures))] + if self.discord_token: + sio.start_background_task(self.get_avatar) self.game: g = None self.reset() @@ -163,6 +209,10 @@ class Player: self.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 not self.game.check_event(ceh.CittaFantasma): + self.is_ghost = False if self.is_ghost: self.lives = 0 if self.pending_action == PendingAction.DRAW and self.game.check_event(ce.Peyote): self.available_cards = [{ @@ -187,7 +237,7 @@ class Player: self.hand.append(self.game.deck.draw(True)) 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 len([c for c in self.gold_rush_equipment if isinstance(c, grc.Zaino)]) > 0: + 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) @@ -202,11 +252,13 @@ class Player: self.choose_text = 'choose_sid_scrap' self.available_cards = self.hand self.lives += 1 + ser = self.__dict__.copy() ser.pop('game') ser.pop('sio') 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') @@ -214,7 +266,7 @@ class Player: ser['attacker'] = self.attacker.name ser['sight'] = self.get_sight() ser['sight_extra'] = self.get_sight(False) -1 - ser['can_gold_rush_discard'] = len([p for p in self.game.get_alive_players() if p != self and len([e for e in p.gold_rush_equipment if e.number <= self.gold_nuggets + 1]) > 0]) > 0 + 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) @@ -223,14 +275,12 @@ class Player: self.pending_action = PendingAction.WAIT ser['hand'] = [] ser['equipment'] = [] - self.sio.emit('self', room=self.sid, data=json.dumps( - ser, default=lambda o: o.__dict__)) + self.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 self.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() - self.sio.emit('self', room=self.sid, data=json.dumps( - ser, default=lambda o: o.__dict__)) + self.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 != None and not self.game.shutting_down: @@ -253,10 +303,10 @@ class Player: 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)] - 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 any([type(c) == type(x) and not (c.is_weapon and c.must_be_used) for x in self.equipment])] + 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)] + 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}') @@ -265,7 +315,7 @@ class Player: 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]): + 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}') @@ -273,6 +323,8 @@ class Player: 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: + continue if self.play_card(self.hand.index(c)): return elif len(misc) > 0: @@ -318,7 +370,7 @@ class Player: return break maxcards = self.lives if not self.character.check(self.game, chd.SeanMallory) else 10 - if maxcards == self.lives and len([c for c in self.gold_rush_equipment if isinstance(c, grc.Cinturone)]) > 0: + 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}') @@ -376,19 +428,19 @@ class Player: 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 self.is_waiting_for_action = True self.has_played_bang = False self.special_use_count = 0 self.bang_used = 0 if self.game.check_event(ceh.MezzogiornoDiFuoco): - self.attacker = None self.lives -= 1 - if len([c for c in self.gold_rush_equipment if isinstance(c, grc.Talismano)]) > 0: + 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 len([c for c in self.gold_rush_equipment if isinstance(c, grc.Stivali)]) > 0: + if any((isinstance(c, grc.Stivali) for c in self.gold_rush_equipment)): self.hand.append(self.game.deck.draw(True)) if self.character.check(self.game, chars.BartCassidy) and self.lives > 0: self.hand.append(self.game.deck.draw(True)) @@ -398,11 +450,12 @@ class Player: 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 len([p for p in self.game.get_alive_players() if p != self and p.lives < p.max_lives]): + 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, 'noDesc': True } for p in self.game.get_alive_players() if p != self and p.lives < p.max_lives] @@ -414,7 +467,7 @@ class Player: self.available_cards = [self.character, self.not_chosen_character] 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) 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: @@ -426,7 +479,7 @@ class Player: 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 len([c for c in self.hand if c.name == cs.Bang(0,0).name]) >= 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'], @@ -439,11 +492,12 @@ class Player: self.choose_text = 'choose_cecchino' self.pending_action = PendingAction.CHOOSE self.notify_self() - elif self.is_my_turn and self.pending_action == PendingAction.PLAY and pile == 'event' and self.game.check_event(ce.Rimbalzo) and len([c for c in self.hand if c.name == cs.Bang(0,0).name]) > 0: + 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, 'noDesc': True } for p in self.game.get_alive_players() if len(p.equipment) > 0 and p != self] self.available_cards.append({'icon': '❌', 'noDesc': True}) @@ -480,7 +534,7 @@ class Player: self.pending_action = PendingAction.PLAY 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 (len([c for c in self.gold_rush_equipment if isinstance(c, grc.Piccone)]) > 0): num += 1 + 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): @@ -522,9 +576,9 @@ class Player: if self.pending_action != PendingAction.PICK: return pickable_cards = 1 + self.character.pick_mod - if len([c for c in self.gold_rush_equipment if isinstance(c, grc.FerroDiCavallo)]) > 0: + if any((isinstance(c, grc.FerroDiCavallo) for c in self.gold_rush_equipment)): pickable_cards += 1 - if self.is_my_turn: + if self.is_my_turn and self.attacker == None: for i in range(len(self.equipment)): if i < len(self.equipment) and isinstance(self.equipment[i], cs.Dinamite): while pickable_cards > 0: @@ -535,11 +589,11 @@ class Player: 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 len([c for c in self.gold_rush_equipment if isinstance(c, grc.Talismano)]) > 0: + 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 len([c for c in self.gold_rush_equipment if isinstance(c, grc.Stivali)]) > 0: + if any((isinstance(c, grc.Stivali) for c in self.gold_rush_equipment)): self.hand.append(self.game.deck.draw()) self.hand.append(self.game.deck.draw()) self.hand.append(self.game.deck.draw()) @@ -557,7 +611,7 @@ class Player: 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) 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)): @@ -578,7 +632,21 @@ class Player: self.sio.emit('chat_message', room=self.game.name, data=f'_prison_free|{self.name}') break break - if any([isinstance(c, cs.Prigione) for c in self.equipment]): + for i in range(len(self.equipment)): + if isinstance(self.equipment[i], tvosc.SerpenteASonagli): + while pickable_cards > 0: + pickable_cards -= 1 + picked: cs.Card = self.game.deck.pick_and_scrap() + print(f'Did pick {picked}') + self.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 + self.sio.emit('chat_message', room=self.game.name, data=f'_snake_bit|{self.name}') + break + if any((isinstance(c, cs.Prigione) for c in self.equipment)): self.notify_self() return if isinstance(self.real_character, chd.VeraCuster): @@ -594,7 +662,7 @@ 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 @@ -627,9 +695,9 @@ class Player: 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 and not (card.usable_next_turn and card.can_be_used_now)) - if not(against != None and (self.game.get_player_named(against).character.check(self.game, chd.ApacheKid) or len([c for c in self.game.get_player_named(against).gold_rush_equipment if isinstance(c, grc.Calumet)]) > 0) 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): + 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) and not (card.usable_next_turn and card.can_be_used_now)) + if not(against != 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) @@ -665,7 +733,7 @@ class Player: target = self.game.get_player_named(self.target_p) card = None if (target.name == self.name): - card = self.equipment.pop(card_index) + 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: @@ -688,7 +756,7 @@ class Player: elif self.choose_text == 'choose_ricercato': player = self.game.get_player_named(self.available_cards[card_index]['name']) player.sio.emit('chat_message', room=player.game.name, data=f'_play_card_against|{self.name}|Ricercato|{player.name}') - if len([c for c in player.gold_rush_equipment if isinstance(c, grc.Ricercato)]) > 0: + if any((isinstance(c, grc.Ricercato) for c in player.gold_rush_equipment)): self.game.deck.shop_deck.append(grc.Ricercato()) else: player.gold_rush_equipment.append(grc.Ricercato()) @@ -753,6 +821,34 @@ class Player: player.notify_self() self.pending_action = PendingAction.PLAY self.notify_self() + 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.equipment.append(self.game.deck.scrap_pile.pop(-1)) + player.notify_self() + self.game.notify_all() + self.sio.emit('chat_message', room=player.game.name, data=f'_play_card_against|{player.name}|Fantasma|{player.name}') + self.pending_action = PendingAction.PLAY + self.notify_self() + elif 'choose_tornado' in self.choose_text: + if card_index <= len(self.available_cards): + self.game.deck.scrap_pile.append(self.hand.pop(card_index)) + self.hand.append(self.game.deck.draw()) + self.hand.append(self.game.deck.draw()) + self.pending_action = PendingAction.WAIT + self.game.responders_did_respond_resume_turn() + self.notify_self() + elif 'choose_bandidos' in self.choose_text: + if card_index <= len(self.available_cards): + self.available_cards.pop(card_index) + self.game.deck.scrap_pile.append(self.hand.pop(card_index)) + self.mancato_needed -= 1 + else: + self.lives -= 1 + if self.mancato_needed <= 0: + self.pending_action = PendingAction.WAIT + self.game.responders_did_respond_resume_turn() + self.notify_self() 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 @@ -771,11 +867,11 @@ class Player: player = self.game.get_player_named(self.available_cards[card_index]['name']) player.lives += 1 self.lives -= 1 - if len([c for c in self.gold_rush_equipment if isinstance(c, grc.Talismano)]) > 0: + 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 len([c for c in self.gold_rush_equipment if isinstance(c, grc.Stivali)]) > 0: + if any((isinstance(c, grc.Stivali) for c in self.gold_rush_equipment)): self.hand.append(self.game.deck.draw()) player.notify_self() self.sio.emit('chat_message', room=self.game.name, data=f'_fratelli_sangue|{self.name}|{player.name}') @@ -855,7 +951,7 @@ class Player: 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 len([c for c in self.gold_rush_equipment if isinstance(c, grc.Piccone)]) > 0: + 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 @@ -863,7 +959,7 @@ class Player: 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 len([c for c in self.gold_rush_equipment if isinstance(c, grc.Piccone)]) > 0: + if self.game.check_event(ceh.IlTreno) and any((isinstance(c, grc.Piccone) for c in self.gold_rush_equipment)): self.hand.append(self.game.deck.draw()) self.is_drawing = False self.pending_action = PendingAction.PLAY @@ -879,7 +975,7 @@ class Player: #legge del west non si applica perchΓ¨ la seconda carta viene scartata if self.game.check_event(ceh.IlTreno): self.hand.append(self.game.deck.draw()) - if len([c for c in self.gold_rush_equipment if isinstance(c, grc.Piccone)]) > 0: + if any((isinstance(c, grc.Piccone) for c in self.gold_rush_equipment)): self.hand.append(self.game.deck.draw()) self.gold_nuggets += 1 self.is_drawing = False @@ -903,9 +999,9 @@ class Player: def barrel_pick(self): pickable_cards = 1 + self.character.pick_mod - if len([c for c in self.gold_rush_equipment if isinstance(c, grc.FerroDiCavallo)]) > 0: + if any((isinstance(c, grc.FerroDiCavallo) for c in self.gold_rush_equipment)): pickable_cards += 1 - if len([c for c in self.equipment if isinstance(c, cs.Barile)]) > 0 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 @@ -919,8 +1015,8 @@ class Player: if self.mancato_needed <= 0: self.game.responders_did_respond_resume_turn(did_lose=False) return - if not self.game.is_competitive and len([c for c in self.hand if 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)]) == 0\ - and len([c for c in self.equipment if c.can_be_used_now and isinstance(c, cs.Mancato)]) == 0: + 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: @@ -933,9 +1029,9 @@ class Player: def barrel_pick_no_dmg(self): pickable_cards = 1 + self.character.pick_mod - if len([c for c in self.equipment if isinstance(c, cs.Barile)]) > 0 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 len([c for c in self.gold_rush_equipment if isinstance(c, grc.FerroDiCavallo)]) > 0: + if any((isinstance(c, grc.FerroDiCavallo) for c in self.gold_rush_equipment)): pickable_cards += 1 while pickable_cards > 0: pickable_cards -= 1 @@ -949,8 +1045,8 @@ class Player: if self.mancato_needed <= 0: self.game.responders_did_respond_resume_turn(did_lose=False) return - if not self.game.is_competitive and len([c for c in self.hand if 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)]) == 0\ - and len([c for c in self.equipment if c.can_be_used_now and isinstance(c, cs.Mancato)]) == 0: + 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: @@ -961,6 +1057,17 @@ class Player: self.on_failed_response_cb = self.take_no_damage_response self.notify_self() + def get_discarded(self, attacker=None, card_name=None): + self.pending_action = PendingAction.CHOOSE + self.available_cards = self.hand.copy() + if card_name == 'Tornado': + self.choose_text = 'choose_tornado' + 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}) + return True + def get_banged(self, attacker, double=False, no_dmg=False, card_index=None, card_name=None): self.attacker = attacker self.attacking_card = card_name @@ -973,9 +1080,9 @@ class Player: 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 len([c for c in self.equipment if isinstance(c, cs.Barile)]) == 0 and not self.character.check(self.game, chars.Jourdonnais)\ - and len([c for c in self.hand if (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)]) == 0\ - and len([c for c in self.equipment if c.can_be_used_now and isinstance(c, cs.Mancato)]) == 0: + 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)): print('Cant defend') if not no_dmg: self.take_damage_response() @@ -983,7 +1090,7 @@ class Player: self.take_no_damage_response() return False else: - if ((not self.game.check_event(ce.Lazo) and len([c for c in self.equipment if isinstance(c, cs.Barile)]) > 0) \ + 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') @@ -1019,8 +1126,8 @@ class Player: def get_indians(self, attacker): self.attacker = attacker self.attacking_card = "Indiani!" - if self.character.check(self.game, chd.ApacheKid) or len([c for c in self.gold_rush_equipment if isinstance(c, grc.Calumet)]) > 0: return False - if not self.game.is_competitive and len([c for c in self.hand if isinstance(c, cs.Bang) or (self.character.check(self.game, chars.CalamityJanet) and isinstance(c, cs.Mancato))]) == 0: + 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)): print('Cant defend') self.take_damage_response() return False @@ -1037,7 +1144,7 @@ class Player: 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 len([c for c in self.hand if isinstance(c, cs.Bang) or (self.character.check(self.game, chars.CalamityJanet) and isinstance(c, cs.Mancato))]) == 0): + 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 (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) @@ -1052,7 +1159,7 @@ class Player: return True def heal_if_needed(self): - while self.lives <= 0 and len(self.game.get_alive_players()) > 2 and len([c for c in self.hand if isinstance(c, cs.Birra)]) > 0 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: @@ -1066,6 +1173,7 @@ class Player: def take_damage_response(self): self.lives -= 1 + self.sio.emit('hurt', room=self.sid, data=f'') if self.lives > 0: if self.character.check(self.game, chars.BartCassidy): self.sio.emit('chat_message', room=self.game.name, @@ -1077,15 +1185,24 @@ class Player: self.sio.emit('chat_message', room=self.game.name, data=f'_special_el_gringo|{self.name}|{self.attacker.name}') self.attacker.notify_self() + if self.attacker and any((isinstance(c, tvosc.Taglia) for c in self.equipment)): + self.attacker.hand.append(self.game.deck.draw(True)) + self.sio.emit('chat_message', room=self.game.name, + data=f'_taglia_reward|{self.name}|{self.attacker.name}') + self.attacker.notify_self() + if self.attacker and len(self.hand) > 0 and any((isinstance(c, tvosc.Shotgun) for c in self.attacker.equipment)): + c = self.hand.pop(randrange(0, len(self.hand))) + self.game.deck.scrap(c, True) + self.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: if (isinstance(self.attacker, Player)): self.attacker.gold_nuggets += 1 self.attacker.notify_self() - if len([c for c in self.gold_rush_equipment if isinstance(c, grc.Talismano)]) > 0: + 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 len([c for c in self.gold_rush_equipment if isinstance(c, grc.Stivali)]) > 0: + if any((isinstance(c, grc.Stivali) for c in self.gold_rush_equipment)): self.hand.append(self.game.deck.draw(True)) self.heal_if_needed() self.mancato_needed = 0 @@ -1137,7 +1254,15 @@ class Player: self.molly_discarded_cards = 0 self.notify_self() self.game.responders_did_respond_resume_turn(did_lose=False) + if isinstance(card, tvosc.RitornoDiFiamma): + self.game.attack(self, self.attacker.name, card_name=card.name) 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) + if isinstance(card, tvosc.RitornoDiFiamma) and self.attacker: + self.game.attack(self, self.attacker.name, card_name=card.name) else: self.pending_action = PendingAction.RESPOND self.notify_self() @@ -1205,9 +1330,10 @@ class Player: '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)), 'noDesc': True - } for p in self.game.get_alive_players() if p != self and len([e for e in p.gold_rush_equipment if e.number + 1 <= self.gold_nuggets]) > 0] + } 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 @@ -1234,23 +1360,23 @@ class Player: 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 len([p for p in self.game.get_alive_players() if (not p.character.check(self.game, chd.ApacheKid) and not any([isinstance(c, grc.Calumet) for c in p.gold_rush_equipment]))]) == 0: + 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 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 len([p for p in self.game.get_visible_players(self) if self.get_sight() >= p['dist']]) == 0: + 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): return True - elif isinstance(card, cs.Panico) and len([p for p in self.game.get_visible_players(self) if self.get_sight(False) >= p['dist']]) == 0 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 len([p for p in self.game.get_visible_players(self) if self.get_sight(False) >= p['dist']]) == 0: + 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 len([p for p in self.game.get_visible_players(self) if not p['is_sheriff']]) == 0: + 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 len([c for c in self.equipment if c.name == card.name]) > 0: + elif not card.is_weapon and any((c.name == card.name for c in self.equipment)): return True return False return True @@ -1260,7 +1386,7 @@ class Player: 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 len([c for c in self.gold_rush_equipment if isinstance(c, grc.Cinturone)]) > 0: + 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") @@ -1290,7 +1416,7 @@ class Player: self.play_turn(can_play_vendetta=False) return ##Ghost## - if self.is_dead and self.is_ghost and self.game.check_event(ceh.CittaFantasma): + 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))): self.is_ghost = False for i in range(len(self.hand)): self.game.deck.scrap(self.hand.pop(), True) diff --git a/backend/bang/roles.py b/backend/bang/roles.py index 32eb94d..9032e40 100644 --- a/backend/bang/roles.py +++ b/backend/bang/roles.py @@ -21,7 +21,7 @@ class Sheriff(Role): def on_player_death(self, alive_players: list, initial_players: int, dead_role=None, attacker_role=None): if initial_players == 3 and len(alive_players) == 1: return True - elif initial_players != 3 and not any([isinstance(p.role, Outlaw) or isinstance(p.role, Renegade) for p in alive_players]): + elif initial_players != 3 and not any((isinstance(p.role, Outlaw) or isinstance(p.role, Renegade) for p in alive_players)): print("The Sheriff won!") return True return False @@ -42,7 +42,7 @@ class Vice(Role): return True elif initial_players == 3 and attacker_role != None: return isinstance(dead_role, Renegade) and isinstance(attacker_role, Vice) - elif initial_players != 3 and not any([isinstance(p.role, Outlaw) or isinstance(p.role, Renegade) for p in alive_players]): + elif initial_players != 3 and not any((isinstance(p.role, Outlaw) or isinstance(p.role, Renegade) for p in alive_players)): print("The Vice won!") return True return False @@ -62,9 +62,9 @@ class Outlaw(Role): return True elif initial_players == 3 and attacker_role != None: return isinstance(dead_role, Vice) and isinstance(attacker_role, Outlaw) - elif (initial_players != 3 and (not any([isinstance(p.role, Sheriff) for p in alive_players])) - and (any([isinstance(p.role, Outlaw) for p in alive_players]) - or any([isinstance(p.role, Renegade) for p in alive_players]) and len(alive_players) > 1)): + elif (initial_players != 3 and (not any((isinstance(p.role, Sheriff) for p in alive_players))) + and (any((isinstance(p.role, Outlaw) for p in alive_players)) + or any((isinstance(p.role, Renegade) for p in alive_players)) and len(alive_players) > 1)): print("The Outlaw won!") return True return False diff --git a/backend/server.py b/backend/server.py index 9b43579..7f68f56 100644 --- a/backend/server.py +++ b/backend/server.py @@ -14,11 +14,16 @@ from discord_webhook import DiscordWebhook from metrics import Metrics from ddtrace import tracer -Metrics.init() - -import sys +import sys +import traceback sys.setrecursionlimit(10**6) # this should prevents bots from stopping +import logging +logging.basicConfig(filename='out.log', level='ERROR') +from functools import wraps + +Metrics.init() + sio = socketio.Server(cors_allowed_origins="*") static_files={ @@ -45,13 +50,30 @@ games: List[Game] = [] online_players = 0 blacklist: List[str] = [] +def send_to_debug(error): + for g in games: + if g.debug or any((p.is_admin() for p in g.players)): + sio.emit('chat_message', room=g.name, data={'color': f'red','text':json.dumps({'ERROR':error}), 'type':'json'}) + +def bang_handler(func): + @wraps(func) + def wrapper_func(*args, **kwargs): + try: + func(*args, **kwargs) + except Exception as e: + logging.exception(e) + print(traceback.format_exc()) + send_to_debug(traceback.format_exc()) + return wrapper_func + def advertise_lobbies(): sio.emit('lobbies', room='lobby', data=[{'name': g.name, 'players': len(g.players), 'locked': g.password != ''} for g in games if not g.started and len(g.players) < 10 and not g.is_hidden]) - sio.emit('spectate_lobbies', room='lobby', data=[{'name': g.name, 'players': len(g.players), 'locked': g.password != ''} for g in games if g.started]) - Metrics.send_metric('lobbies', points=[len([g for g in games if not g.is_replay])]) + sio.emit('spectate_lobbies', room='lobby', data=[{'name': g.name, 'players': len(g.players), 'locked': g.password != ''} for g in games if g.started and not g.is_hidden]) + Metrics.send_metric('lobbies', points=[sum(not g.is_replay for g in games)]) Metrics.send_metric('online_players', points=[online_players]) @sio.event +@bang_handler def connect(sid, environ): global online_players online_players += 1 @@ -61,40 +83,46 @@ def connect(sid, environ): 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.event +@bang_handler def report(sid, text): + print(f'New report from {sid}: {text}') ses: Player = sio.get_session(sid) data='' if hasattr(ses, 'game'): data = "\n".join(ses.game.rpc_log[:-1]).strip() data = data +"\n@@@\n" +text #print(data) - response = requests.post("https://www.toptal.com/developers/hastebin/documents", data) + response = requests.post("https://hastebin.com/documents", data.encode('utf-8')) 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 report, replay at https://www.toptal.com/developers/hastebin/{key}') + webhook = DiscordWebhook(url=os.environ['DISCORD_WEBHOOK'], content=f'New bug report, replay at https://bang.xamin.it/game?replay={key}') response = webhook.execute() sio.emit('chat_message', room=sid, data={'color': f'green','text':f'Report OK'}) else: print("WARNING: DISCORD_WEBHOOK not found") Metrics.send_event('BUG_REPORT', event_data=text) - print(f'New bug report, replay at https://www.toptal.com/developers/hastebin/{key}') + 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): - sio.save_session(sid, Player(username, sid, sio)) + dt = username["discord_token"] if 'discord_token' in username else None + sio.save_session(sid, Player(username["name"], sid, sio, discord_token=dt)) print(f'{sid} is now {username}') advertise_lobbies() elif ses.game == None or not ses.game.started: + username = username["name"] print(f'{sid} changed username to {username}') prev = ses.name - if len([p for p in ses.game.players if p.name == username]) > 0: + 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 @@ -104,19 +132,34 @@ def set_username(sid, username): @tracer.wrap @sio.event +@bang_handler def get_me(sid, room): if isinstance(sio.get_session(sid), Player): 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: - sio.save_session(sid, Player('player', sid, sio)) + dt = room["discord_token"] if 'discord_token' in room else None + sio.save_session(sid, Player('player', sid, sio, discord_token=dt)) + if 'replay' in room and room['replay'] != None: + create_room(sid, room['replay']) + sid = sio.get_session(sid) + sid.game.is_hidden = True + eventlet.sleep(0.5) + response = requests.get(f"https://hastebin.com/raw/{room['replay']}") + if response.status_code != 200: + 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) + sid.game.replay(log) + return de_games = [g for g in games if g.name == room['name']] if len(de_games) == 1 and not de_games[0].started: join_room(sid, room) elif len(de_games) == 1 and de_games[0].started: print('room exists') - if room['username'] != None and any([p.name == room['username'] for p in de_games[0].players if p.is_bot]): + if room['username'] != None and any((p.name == room['username'] for p in de_games[0].players if (p.is_bot or (dt != None and p.discord_token == dt)))): print('getting inside the bot') bot = [p for p in de_games[0].players if p.is_bot and p.name == room['username'] ][0] bot.sid = sid @@ -126,6 +169,7 @@ def get_me(sid, room): de_games[0].notify_room(sid) eventlet.sleep(0.1) de_games[0].notify_all() + de_games[0].notify_scrap_pile(sid) sio.emit('role', room=sid, data=json.dumps(bot.role, default=lambda o: o.__dict__)) bot.notify_self() if len(bot.available_characters) > 0: @@ -135,6 +179,8 @@ def get_me(sid, room): sio.get_session(sid).game = de_games[0] sio.enter_room(sid, de_games[0].name) de_games[0].notify_room(sid) + eventlet.sleep(0.1) + de_games[0].notify_event_card(sid) de_games[0].notify_scrap_pile(sid) de_games[0].notify_all() @@ -146,7 +192,7 @@ def get_me(sid, room): sio.emit('me', data={'error':'Wrong password/Cannot connect'}, room=sid) else: sio.emit('me', data=sio.get_session(sid).name, room=sid) - if room['username'] == None or any([p.name == room['username'] for p in sio.get_session(sid).game.players]): + if room['username'] == None or any((p.name == room['username'] for p in sio.get_session(sid).game.players)): sio.emit('change_username', room=sid) else: sio.emit('chat_message', room=sio.get_session(sid).game.name, data=f"_change_username|{sio.get_session(sid).name}|{room['username']}") @@ -157,6 +203,7 @@ def get_me(sid, room): @tracer.wrap @sio.event +@bang_handler def disconnect(sid): global online_players online_players -= 1 @@ -164,16 +211,18 @@ def disconnect(sid): sio.emit('players', room='lobby', data=online_players) if sio.get_session(sid).game and sio.get_session(sid).disconnect(): sio.close_room(sio.get_session(sid).game.name) - games.pop(games.index(sio.get_session(sid).game)) + if sio.get_session(sid).game in games: + games.pop(games.index(sio.get_session(sid).game)) print('disconnect ', sid) advertise_lobbies() Metrics.send_metric('online_players', points=[online_players]) @tracer.wrap @sio.event +@bang_handler def create_room(sid, room_name): if sio.get_session(sid).game == None: - while len([g for g in games if g.name == room_name]): + while any((g.name == room_name for g in games)): room_name += f'_{random.randint(0,100)}' sio.leave_room(sid, 'lobby') sio.enter_room(sid, room_name) @@ -186,26 +235,31 @@ def create_room(sid, room_name): advertise_lobbies() @sio.event +@bang_handler def private(sid): g = sio.get_session(sid).game g.set_private() advertise_lobbies() @sio.event +@bang_handler def toggle_expansion(sid, expansion_name): g = sio.get_session(sid).game g.toggle_expansion(expansion_name) @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() @tracer.wrap @sio.event +@bang_handler def join_room(sid, room): room_name = room['name'] i = [g.name for g in games].index(room_name) @@ -215,7 +269,7 @@ def join_room(sid, room): print(f'{sid} joined a room named {room_name}') sio.leave_room(sid, 'lobby') sio.enter_room(sid, room_name) - while len([p for p in games[i].players if p.name == sio.get_session(sid).name]): + while any((p.name == sio.get_session(sid).name and not p.is_bot for p in games[i].players)): sio.get_session(sid).name += f'_{random.randint(0,100)}' sio.emit('me', data=sio.get_session(sid).name, room=sid) games[i].add_player(sio.get_session(sid)) @@ -236,8 +290,9 @@ Sockets for the status page @tracer.wrap @sio.event +@bang_handler def get_all_rooms(sid, deploy_key): - if 'DEPLOY_KEY' in os.environ and deploy_key == os.environ['DEPLOY_KEY']: + if ('DEPLOY_KEY' in os.environ and deploy_key == os.environ['DEPLOY_KEY']) or sio.get_session(sid).is_admin(): sio.emit('all_rooms', room=sid, data=[{ 'name': g.name, 'hidden': g.is_hidden, @@ -252,13 +307,15 @@ def get_all_rooms(sid, deploy_key): } for g in games]) @sio.event +@bang_handler def kick(sid, data): - if 'DEPLOY_KEY' in os.environ and data['key'] == os.environ['DEPLOY_KEY']: + if ('DEPLOY_KEY' in os.environ and data['key'] == os.environ['DEPLOY_KEY']) or sio.get_session(sid).is_admin(): sio.emit('kicked', room=data['sid']) @sio.event +@bang_handler def hide_toogle(sid, data): - if 'DEPLOY_KEY' in os.environ and data['key'] == os.environ['DEPLOY_KEY']: + if ('DEPLOY_KEY' in os.environ and data['key'] == os.environ['DEPLOY_KEY']) or sio.get_session(sid).is_admin(): game = [g for g in games if g.name==data['room']] if len(games) > 0: game[0].is_hidden = not game[0].is_hidden @@ -274,6 +331,7 @@ Sockets for the game """ @sio.event +@bang_handler def start_game(sid): ses: Player = sio.get_session(sid) ses.game.start_game() @@ -281,6 +339,13 @@ def start_game(sid): @tracer.wrap @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}') @@ -289,12 +354,14 @@ def set_character(sid, name): ses.set_character(name) @sio.event +@bang_handler def refresh(sid): ses: Player = sio.get_session(sid) ses.notify_self() @tracer.wrap @sio.event +@bang_handler def draw(sid, pile): ses: Player = sio.get_session(sid) ses.game.rpc_log.append(f'{ses.name};draw;{pile}') @@ -302,6 +369,7 @@ def draw(sid, pile): @tracer.wrap @sio.event +@bang_handler def pick(sid): ses: Player = sio.get_session(sid) ses.game.rpc_log.append(f'{ses.name};pick') @@ -309,6 +377,7 @@ def pick(sid): @tracer.wrap @sio.event +@bang_handler def end_turn(sid): ses: Player = sio.get_session(sid) ses.game.rpc_log.append(f'{ses.name};end_turn') @@ -316,6 +385,7 @@ def end_turn(sid): @tracer.wrap @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)}') @@ -323,6 +393,7 @@ def play_card(sid, data): @tracer.wrap @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}') @@ -330,6 +401,7 @@ def respond(sid, card_index): @tracer.wrap @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}') @@ -337,6 +409,7 @@ def choose(sid, card_index): @tracer.wrap @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}') @@ -351,6 +424,7 @@ def special(sid, data): @tracer.wrap @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;') @@ -358,6 +432,7 @@ def gold_rush_discard(sid): @tracer.wrap @sio.event +@bang_handler 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}') @@ -365,6 +440,7 @@ def buy_gold_rush_card(sid, data:int): @tracer.wrap @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}') @@ -379,7 +455,7 @@ def chat_message(sid, msg, pl=None): sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'Only 1 bot at the time'}) else: bot = Player(f'AI_{random.randint(0,10)}', 'bot', sio, bot=True) - while any([p for p in ses.game.players if p.name == bot.name]): + while any((p for p in ses.game.players if p.name == bot.name)): bot = Player(f'AI_{random.randint(0,10)}', 'bot', sio, bot=True) ses.game.add_player(bot) bot.bot_spin() @@ -388,7 +464,7 @@ def chat_message(sid, msg, pl=None): _cmd = msg.split() if len(_cmd) >= 2: replay_id = _cmd[1] - response = requests.get(f"https://www.toptal.com/developers/hastebin/raw/{replay_id}") + response = requests.get(f"https://hastebin.com/raw/{replay_id}") log = response.text.splitlines() ses.game.spectators.append(ses) if len(_cmd) == 2: @@ -407,7 +483,7 @@ def chat_message(sid, msg, pl=None): ses.game.start_game(int(msg.split()[1])) return elif '/removebot' in msg and not ses.game.started: - if any([p.is_bot for p in ses.game.players]): + if any((p.is_bot for p in ses.game.players)): [p for p in ses.game.players if p.is_bot][-1].disconnect() return elif '/togglecomp' in msg and ses.game: @@ -418,13 +494,13 @@ def chat_message(sid, msg, pl=None): 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]: # 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'}) return - if not ses.game.debug: + 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: cmd = msg.split() @@ -544,6 +620,27 @@ def chat_message(sid, msg, pl=None): 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.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'}) + 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(',') + 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.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'}) + cmd = msg.split() + if len(cmd) >= 2: + from bang.expansions import DodgeCity, TheValleyOfShadows + if cmd[1] == 'dodgecity': + ses.hand = DodgeCity.get_cards() + ses.notify_self() + 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'}) import bang.cards as cs @@ -552,16 +649,26 @@ def chat_message(sid, msg, pl=None): ses.gold_nuggets += int(cmd[1]) ses.notify_self() elif '/gameinfo' in msg: - sio.emit('chat_message', room=sid, data={'color': f'','text':f'info: {dict(filter(lambda x:x[0] != "rpc_log",ses.game.__dict__.items()))}'}) + 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':f'info: {ses.__dict__}'}) + 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: + 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: + 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: ses.is_bot = not ses.is_bot if (ses.is_bot): ses.was_player = True ses.bot_spin() elif '/arcadekick' in msg and ses.game.started: - if len([p for p in ses.game.players if p.pending_action != PendingAction.WAIT]) == 0: + 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'}) ses.game.next_turn() else: @@ -582,6 +689,7 @@ 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']) @@ -594,12 +702,14 @@ def get_cards(sid): 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']) 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 = [] @@ -608,6 +718,7 @@ def get_highnooncards(sid): 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 = [] @@ -616,6 +727,7 @@ def get_foccards(sid): 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() @@ -626,9 +738,34 @@ def get_goldrushcards(sid): cards = [cards_dict[i] for i in cards_dict] sio.emit('goldrushcards_info', room=sid, data=json.dumps(cards, 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__)) + +@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'], + }) + if res.status_code == 200: + sio.emit('discord_auth_succ', room=sid, data=res.json()) + def pool_metrics(): sio.sleep(60) - Metrics.send_metric('lobbies', points=[len([g for g in games if not g.is_replay])]) + Metrics.send_metric('lobbies', points=[sum(not g.is_replay for g in games)]) Metrics.send_metric('online_players', points=[online_players]) pool_metrics() @@ -650,6 +787,10 @@ class CustomProxyFix(object): return [''] return self.app(environ, start_response) + +discord_ci = '1059452581027532880' +discord_cs = 'Mc8ZlMQhayzi1eOqWFtGHs3L0iXCzaEu' + if __name__ == '__main__': sio.start_background_task(pool_metrics) eventlet.wsgi.server(eventlet.listen(('', 5001)), CustomProxyFix(app)) diff --git a/backend/tests/character_test.py b/backend/tests/character_test.py index 740f757..daaf109 100644 --- a/backend/tests/character_test.py +++ b/backend/tests/character_test.py @@ -297,8 +297,6 @@ def test_SlabTheKiller(): g.players[g.turn].play_card(0, g.players[(g.turn+1)%2].name) assert g.players[(g.turn+1)%2].pending_action == PendingAction.RESPOND g.players[(g.turn+1)%2].respond(0) - assert g.players[(g.turn+1)%2].pending_action == PendingAction.RESPOND - g.players[(g.turn+1)%2].respond(-1) assert g.players[(g.turn+1)%2].pending_action == PendingAction.WAIT assert g.players[(g.turn+1)%2].lives == 3 diff --git a/backend/tests/valley_of_shadows_test.py b/backend/tests/valley_of_shadows_test.py new file mode 100644 index 0000000..c477ab2 --- /dev/null +++ b/backend/tests/valley_of_shadows_test.py @@ -0,0 +1,142 @@ +from random import randint +from bang.characters import Character +from bang.expansions.the_valley_of_shadows.cards import * +from tests.dummy_socket import DummySocket +from bang.deck import Deck +from bang.game import Game +from bang.players import Player, PendingAction +import bang.cards as cs + +# test UltimoGiro +def test_ultimo_giro(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(3)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + ultimo_giro_guy = g.players[g.turn] + ultimo_giro_guy.draw('') + ultimo_giro_guy.lives = 3 + ultimo_giro_guy.hand = [UltimoGiro(0,0)] + assert ultimo_giro_guy.lives == 3 + ultimo_giro_guy.play_card(0) + assert ultimo_giro_guy.lives == 4 + +# test Tomahawk +def test_tomahawk(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(6)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + tomahawk_guy = g.players[g.turn] + tomahawk_guy.draw('') + tomahawk_guy.hand = [Tomahawk(0,0)] + assert len(tomahawk_guy.hand) == 1 + tomahawk_guy.play_card(0, g.players[(g.turn+3)%6].name) + assert len(tomahawk_guy.hand) == 1 + tomahawk_guy.play_card(0, g.players[(g.turn+1)%6].name) + assert len(tomahawk_guy.hand) == 0 + +# test Fantasma +def test_fantasma(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(3)] + for p in ps: + g.add_player(p) + g.start_game() + + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + fantasma_guy = g.players[g.turn] + fantasma_guy.lives = 0 + fantasma_guy.notify_self() + pl = g.players[g.turn] + pl.draw('') + pl.hand = [Fantasma(0,0)] + pl.play_card(0) + assert pl.pending_action == PendingAction.CHOOSE + assert pl.available_cards[0]['name'] == fantasma_guy.name + pl.choose(0) + assert pl.pending_action == PendingAction.PLAY + assert len(fantasma_guy.equipment) == 1 and isinstance(fantasma_guy.equipment[0], Fantasma) + +# test SerpenteASonagli +def test_serpente_a_sonagli(): + sio = DummySocket() + g = Game('test', sio) + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(3)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + p = g.players[g.turn] + serp = g.players[(g.turn+1)%3] + p.draw('') + p.hand = [SerpenteASonagli(0,0)] + assert len(p.hand) == 1 + p.play_card(0, serp.name) + assert len(p.hand) == 0 + assert len(serp.equipment) == 1 and isinstance(serp.equipment[0], SerpenteASonagli) + p.end_turn() + assert serp.pending_action == PendingAction.PICK + g.deck.cards[0] = Bang(Suit.SPADES, 5) + serp.pick() + assert serp.lives == 3 + serp.draw('') + serp.hand = [SerpenteASonagli(0,0)] + serp.play_card(0, g.players[(g.turn+1)%3].name) + assert len(serp.hand) == 0 + serp.end_turn() + assert g.players[g.turn].pending_action == PendingAction.PICK + g.deck.cards[0] = Bang(Suit.HEARTS, 5) + g.players[g.turn].pick() + assert g.players[g.turn].lives == 4 + +# test RitornoDiFiamma +def test_ritorno_di_fiamma(): + sio = DummySocket() + g = Game('test', sio) + g.expansions = ['the_valley_of_shadows'] + ps = [Player(f'p{i}', f'p{i}', sio) for i in range(3)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [Character('test_char', 4)] + p.set_character(p.available_characters[0].name) + p = g.players[g.turn] + p1 = g.players[(g.turn+1)%3] + p.draw('') + p.hand = [Bang(1, 1)] + p1.hand = [RitornoDiFiamma(0,0)] + p.play_card(0, p1.name) + assert len(p.hand) == 0 + assert len(p1.hand) == 1 + p1.respond(0) + assert len(p1.hand) == 0 + assert p.lives == 3 + p.end_turn() + p1.draw('') + p1.hand = [Bang(1, 1)] + p.equipment = [cs.Barile(0,0)] + p.hand = [RitornoDiFiamma(0,0)] + p1.play_card(0, p.name) + assert p.pending_action == PendingAction.PICK + g.deck.cards[0] = Bang(Suit.SPADES, 5) + p.pick() + assert p.pending_action == PendingAction.RESPOND + p.respond(0) + assert p1.lives == 3 diff --git a/frontend/package.json b/frontend/package.json index 16600f6..01c6ea9 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,7 +17,8 @@ "vue": "^2.6.14", "vue-clipboard2": "^0.3.3", "vue-i18n": "^8.27.1", - "vue-router": "^3.5.4", + "vue-json-viewer": "^2.2.22", + "vue-router": "^3.5.3", "vue-socket.io": "^3.0.10" }, "devDependencies": { diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 14e7005..fa95f74 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -135,6 +135,7 @@ export default { this.$i18n.locale = userLang.split('-')[0] } this.detectColorScheme() + if (window.location.origin.indexOf('localhost') !== -1) return; datadogRum.init({ applicationId: '076b1a5e-16a9-44eb-b320-27afd32c57a5', clientToken: 'pub1cc4d0d6ea0a7235aa1eab86e7a192d4', diff --git a/frontend/src/components/Card.vue b/frontend/src/components/Card.vue index d1dd002..552c035 100644 --- a/frontend/src/components/Card.vue +++ b/frontend/src/components/Card.vue @@ -1,7 +1,8 @@ @@ -29,13 +30,19 @@ export default { type: String, default: '', }, + timer: { + type: Number, + default: 0, + }, text: String, hintText: String, playAudio: Boolean, }, data: () => ({ desc: '', - realCancelText: '' + realCancelText: '', + remainingTime: 0, + intervalID: '', }), computed: { showCancelBtn() { @@ -49,15 +56,29 @@ export default { //console.log(card) if (card.noDesc || card.name == null || card.name == "PewPew!") this.desc = "" - else if (card.is_character) - this.desc = card.name else if (card.goal) this.desc = this.$t(`cards.${card.name}.name`) else if (card.desc) this.desc = (this.$i18n.locale=='it'?card.desc:card.desc_eng) else this.desc = this.$t(`cards.${card.name}.desc`) - } + }, + countDown() { + if (this.remainingTime > 0) { + this.remainingTime--; + } else { + this.select(this.cards[0]); + window.clearInterval(this.intervalID); + } + }, + internalCancel() { + if (this.intervalID) window.clearInterval(this.intervalID); + this.cancel(); + }, + internalSelect(card) { + if (this.intervalID) window.clearInterval(this.intervalID); + this.select(card); + }, }, mounted() { this.realCancelText = this.cancelText @@ -70,6 +91,11 @@ export default { if (this.playAudio) { (new Audio(show_sfx)).play(); } + this.remainingTime = this.timer; + if (this.timer != 0 && this.remainingTime == this.timer) { + if (this.intervalID) window.clearInterval(this.intervalID); + this.intervalID = window.setInterval(this.countDown, 1000); + } }, } diff --git a/frontend/src/components/Help.vue b/frontend/src/components/Help.vue index a7159dc..9fc6e49 100644 --- a/frontend/src/components/Help.vue +++ b/frontend/src/components/Help.vue @@ -108,6 +108,15 @@ +

{{$t('help.valleyofshadowscards')}}

+
+
+ +
+

{{$t(`cards.${c.name}.desc`)}}

+
+
+
@@ -426,6 +456,9 @@ export default { justify-content: space-evenly; margin-bottom: 12pt; } +#admin-status { + position:absolute;width:100%;height:100%;overflow:auto;background:var(--bg-color); opacity: 0.8; +} #turn-indicator{ position: absolute; width: 100%; @@ -471,5 +504,11 @@ background-position-x: 80px; min-width: 25vw; max-width: 25vw; } + .player-in-table { + transition: all 0.2s ease-in-out; + } + .player-in-table:hover { + transform: translateY(-5px) scale(1.05); + } } diff --git a/frontend/src/components/Menu.vue b/frontend/src/components/Menu.vue index d0e0109..0285674 100644 --- a/frontend/src/components/Menu.vue +++ b/frontend/src/components/Menu.vue @@ -18,6 +18,7 @@ + Login with Discord

{{$t("online_players")}}{{onlinePlayers}}

@@ -71,8 +72,12 @@ export default { isInLobby: false, onlinePlayers: 0, randomTip: '', + discordPic: '', }), computed: { + redirectUrl() { + return 'https://discordapp.com/api/oauth2/authorize?client_id=1059452581027532880&response_type=code&scope=identify&redirect_uri=' + window.location.origin; + }, noLobbyAvailable() { return this.openLobbies && this.openLobbies.length == 0 }, @@ -85,6 +90,7 @@ export default { number: this.$t('you'), icon: '🀠', is_character: true, + avatar: this.discordPic, } }, version() { @@ -105,14 +111,20 @@ export default { players(num) { this.onlinePlayers = num; // console.log('PLAYERS:' + num) - } + }, + discord_auth_succ(data) { + if (data.access_token) { + localStorage.setItem('discord_token', data.access_token) + this.login() + } + }, }, methods: { setUsername(e){ if (this.username.trim().length > 0){ this.didSetUsername = true localStorage.setItem('username', this.username) - this.$socket.emit('set_username', this.username) + this.$socket.emit('set_username', {name:this.username}) e.preventDefault(); } }, @@ -145,8 +157,34 @@ export default { init() { location.reload(); }, + login() { + fetch('https://discordapp.com/api/users/@me', { + headers: { + 'Authorization': 'Bearer ' + localStorage.getItem('discord_token') + } + }) + .then(response => response.json()) + .then(data => { + console.log(data) + this.username = data.username + this.didSetUsername = true + this.discordPic = `https://cdn.discordapp.com/avatars/${data.id}/${data.avatar}.png` + localStorage.setItem('username', this.username) + this.$socket.emit('set_username', {name: this.username, discord_token: localStorage.getItem('discord_token')}) + }).catch(err => { + console.error(err) + localStorage.removeItem('discord_token') + this.$router.replace({query: []}) + }) + } }, mounted() { + if (localStorage.getItem('discord_token')) { + this.login() + } else if (this.$route.query.code) { + this.$socket.emit('discord_auth', {code:this.$route.query.code, origin:window.location.origin}) + this.$router.replace({query: []}) + } this.randomTip = `tip_${1+Math.floor(Math.random() * 8)}` if (localStorage.getItem('username')) this.username = localStorage.getItem('username') diff --git a/frontend/src/components/Player.vue b/frontend/src/components/Player.vue index 15d1d8f..58e8c03 100644 --- a/frontend/src/components/Player.vue +++ b/frontend/src/components/Player.vue @@ -37,6 +37,7 @@
{{$t('hand')}} + {{hand.length}}/{{maxHandLength()}} - - + + - + - + @@ -69,6 +70,7 @@ :cards="notScrappedHand" :select="holydayScrapAdd" :cancel="() => {holydayScrap = false;scrapHand=[]}"/> +
@@ -134,6 +136,7 @@ export default { committed_suit_manette: null, gold_nuggets: 0, cantplaycard: false, + hurt: false, }), sockets: { role(role) { @@ -205,6 +208,12 @@ export default { this.notifycard = null }.bind(this), 4000) }, + hurt() { + this.hurt = true + setTimeout(function(){ + this.hurt = false + }.bind(this), 500) + }, cant_play_card() { this.cantplaycard = true setTimeout(function(){ @@ -234,6 +243,7 @@ export default { name: player.name, number: player.dist !== undefined ? `${player.dist}β›°` : '', icon: this.noStar ? player.icon : player.is_sheriff ? '⭐' : '🀠', + avatar: player.avatar, is_character: true, }}) return vis @@ -255,6 +265,7 @@ export default { number: player.dist !== undefined ? `${player.dist}β›°` : '', icon: this.noStar ? player.icon : player.is_sheriff ? '⭐' : '🀠', alt_text: Array(player.lives+1).join('❀️')+Array(player.max_lives-player.lives+1).join('πŸ’€'), + avatar: player.avatar, is_character: true, }}) if (this.card_against && this.card_against.can_target_self) { @@ -510,6 +521,10 @@ export default { margin: 10pt 0pt; overflow:auto; } +.hurt-notify { + pointer-events: none; + animation: disappear 0.5s ease-in forwards; +} .turn-notify { pointer-events: none; animation: disappear 2s ease-in forwards; diff --git a/frontend/src/components/Status.vue b/frontend/src/components/Status.vue index 58fc9c5..48f1fb7 100644 --- a/frontend/src/components/Status.vue +++ b/frontend/src/components/Status.vue @@ -31,9 +31,15 @@