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 index 34b94ad..ed8f828 100644 --- a/backend/bang/expansions/the_valley_of_shadows/cards.py +++ b/backend/bang/expansions/the_valley_of_shadows/cards.py @@ -1,11 +1,31 @@ from typing import List -from bang.cards import Card, Suit +import bang.roles as r +import bang.players as pl +from bang.cards import Card, Suit, Bang +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 - #TODO + 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): @@ -19,11 +39,22 @@ class SerpenteASonagli(Card): self.need_target = True self.icon = '๐Ÿ๏ธ' # Ogni turno pesca se il seme picche -1hp self.alt_text = "โ™ ๏ธ =๐Ÿ’”" - #TODO + + 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, range=1) + 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 #TODO @@ -57,16 +88,17 @@ class Tomahawk(Card): self.need_target = True def play_card(self, player, against, _with=None): - if against != None: + if against != None and player.game.can_card_reach(self, player, against): super().play_card(player, against=against) - player.game.attack(player, against) + player.game.attack(player, against, card_name=self.name) return True return False -class Sventagliata(Card): +class Sventagliata(Bang): def __init__(self, suit, number): - super().__init__(suit, 'Sventagliata', number) - self.icon = '๐Ÿ’•๏ธ' + 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 @@ -74,7 +106,7 @@ class Sventagliata(Card): if against != None: #TODO # super().play_card(player, against=against) - # player.game.attack(player, against) + # player.game.attack(player, against, card_name=self.name) return True return False @@ -89,7 +121,7 @@ class Salvo(Card): if against != None: #TODO # super().play_card(player, against=against) - # player.game.attack(player, against) + # player.game.attack(player, against, card_name=self.name) return True return False @@ -97,7 +129,7 @@ class Mira(Card): def __init__(self, suit, number): super().__init__(suit, 'Mira', number) self.icon = '๐Ÿ‘Œ๐Ÿป' - self.alt_text = "๐Ÿ’ฅ๐Ÿƒ | ๐Ÿ‘ค๐Ÿ’ฅ๐Ÿ’ฅ" + self.alt_text = "๐Ÿ’ฅ๐Ÿƒ๐Ÿ’”๐Ÿ’”" self.need_target = True self.need_with = True @@ -105,7 +137,7 @@ class Mira(Card): if against != None: #TODO # super().play_card(player, against=against) - # player.game.attack(player, against) + # player.game.attack(player, against, card_name=self.name) return True return False @@ -113,7 +145,7 @@ class Bandidos(Card): def __init__(self, suit, number): super().__init__(suit, 'Bandidos', number) self.icon = '๐Ÿค ๏ธ' - self.alt_text = "๐Ÿ‘ค๐Ÿƒ๐Ÿƒ | ๐Ÿ‘ค๐Ÿ’”" + self.alt_text = "๐Ÿ‘ค๐Ÿƒ๐Ÿƒ/๐Ÿ’”" def play_card(self, player, against, _with=None): #TODO @@ -161,19 +193,19 @@ def get_starting_deck() -> List[Card]: cards = [ Fantasma(Suit.SPADES, 9), Fantasma(Suit.SPADES, 10), - Lemat(Suit.DIAMONDS, 4), + # Lemat(Suit.DIAMONDS, 4), SerpenteASonagli(Suit.HEARTS, 7), - Shotgun(Suit.SPADES, 'K'), - Taglia(Suit.CLUBS, 9), + # 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 + # 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 ] for c in cards: c.expansion_icon = '๐Ÿ‘ป๏ธ' diff --git a/backend/bang/game.py b/backend/bang/game.py index 1679b32..f45b2c1 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,33 @@ 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':'/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__() @@ -158,31 +186,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':'/playerinfo', 'help':'Get player data - sample /playerinfo player'}, - {'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)) @@ -205,7 +209,8 @@ class Game: self.notify_room() def feature_flags(self): - self.available_expansions.append('the_valley_of_shadows') + 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): @@ -346,6 +351,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 @@ -510,7 +520,7 @@ 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: @@ -777,8 +787,8 @@ class Game: 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: diff --git a/backend/bang/players.py b/backend/bang/players.py index 2f2defa..b3efa50 100644 --- a/backend/bang/players.py +++ b/backend/bang/players.py @@ -12,6 +12,7 @@ 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 @@ -208,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 = [{ @@ -247,6 +252,7 @@ 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') @@ -269,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: @@ -449,6 +453,7 @@ class Player: '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] @@ -460,7 +465,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: @@ -490,6 +495,7 @@ 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, '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}) @@ -624,7 +630,20 @@ 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 picked.check_suit(self.game, [cs.Suit.SPADES]) and pickable_cards == 0: + self.lives -= 1 + self.sio.emit('chat_message', room=self.game.name, data=f'_snake_bit|{self.name}') + self.end_turn(forced=True) + return + if any((isinstance(c, cs.Prigione) for c in self.equipment)): self.notify_self() return if isinstance(self.real_character, chd.VeraCuster): @@ -799,6 +818,15 @@ 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 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 @@ -1251,6 +1279,7 @@ 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] @@ -1336,7 +1365,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/tests/valley_of_shadows_test.py b/backend/tests/valley_of_shadows_test.py new file mode 100644 index 0000000..3768f76 --- /dev/null +++ b/backend/tests/valley_of_shadows_test.py @@ -0,0 +1,71 @@ +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 + +# 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) diff --git a/frontend/src/i18n/it.json b/frontend/src/i18n/it.json index d66cd92..a90b083 100644 --- a/frontend/src/i18n/it.json +++ b/frontend/src/i18n/it.json @@ -713,7 +713,7 @@ }, "Sventagliata": { "name": "Sventagliata", - "desc": "Conta come l'unico BANGI del turno. Anche un giocatore a tua scelta a distanza 1 dal bersaglio (se ce, te escluso) รจ bersaglio di un BANG.." + "desc": "Conta come l'unico BANG! del turno. Anche un giocatore a tua scelta a distanza 1 dal bersaglio (se ce, te escluso) รจ bersaglio di un BANG." }, "UltimoGiro": { "name": "Ultimo Giro",