diff --git a/backend/bang/characters.py b/backend/bang/characters.py index 620599a..5117b81 100644 --- a/backend/bang/characters.py +++ b/backend/bang/characters.py @@ -177,4 +177,6 @@ def all_characters(expansions: List[str]): base_chars.extend(GoldRush.get_characters()) if 'the_valley_of_shadows' in expansions: base_chars.extend(TheValleyOfShadows.get_characters()) + if 'wild_west_show' in expansions: + base_chars.extend(WildWestShow.get_characters()) return base_chars \ No newline at end of file diff --git a/backend/bang/deck.py b/backend/bang/deck.py index 5c49d6d..0cd14ab 100644 --- a/backend/bang/deck.py +++ b/backend/bang/deck.py @@ -1,13 +1,17 @@ -from typing import List, Set, Dict, Tuple, Optional +from typing import List, Set, Dict, Tuple, Optional, TYPE_CHECKING import random import bang.cards as cs import bang.expansions.fistful_of_cards.card_events as ce import bang.expansions.high_noon.card_events as ceh +import bang.expansions.wild_west_show.card_events as cew +import bang.expansions.wild_west_show.characters as chw import bang.expansions.gold_rush.shop_cards as grc from globals import G +if TYPE_CHECKING: + from bang.game import Game class Deck: - def __init__(self, game): + def __init__(self, game: 'Game'): super().__init__() self.cards: List[cs.Card] = cs.get_starting_deck(game.expansions) self.mancato_cards: List[str] = [] @@ -33,6 +37,9 @@ class Deck: if 'high_noon' in game.expansions: self.event_cards.extend(ceh.get_all_events(game.rng)) endgame_cards.append(ceh.get_endgame_card()) + if 'wild_west_show' in game.expansions: + self.event_cards.extend(cew.get_all_events(game.rng)) + endgame_cards.append(cew.get_endgame_card()) if len(self.event_cards) > 0: game.rng.shuffle(self.event_cards) self.event_cards = self.event_cards[:12] @@ -51,9 +58,10 @@ class Deck: print(f'Deck initialized with {len(self.cards)} cards') def flip_event(self): - if len(self.event_cards) > 0 and not (isinstance(self.event_cards[0], ce.PerUnPugnoDiCarte) or isinstance(self.event_cards[0], ceh.MezzogiornoDiFuoco)): + if len(self.event_cards) > 0 and not (isinstance(self.event_cards[0], ce.PerUnPugnoDiCarte) or isinstance(self.event_cards[0], ceh.MezzogiornoDiFuoco) or isinstance(self.event_cards[0], cew.WildWestShow)): self.event_cards.append(self.event_cards.pop(0)) self.game.notify_event_card() + self.game.notify_all() def fill_gold_rush_shop(self): if not any((c is None for c in self.shop_cards)): @@ -76,7 +84,16 @@ class Deck: def pick_and_scrap(self) -> cs.Card: card = self.cards.pop(0) - self.scrap_pile.append(card) + jpain = None + for p in self.game.players: + if p.character.check(self.game, chw.JohnPain) and len(p.hand) < 6: + jpain = p + break + if jpain: + jpain.hand.append(card) + jpain.notify_self() + else: + self.scrap_pile.append(card) if len(self.cards) == 0: self.reshuffle() self.game.notify_scrap_pile() diff --git a/backend/bang/expansions/__init__.py b/backend/bang/expansions/__init__.py index ebae2dd..220866e 100644 --- a/backend/bang/expansions/__init__.py +++ b/backend/bang/expansions/__init__.py @@ -22,3 +22,8 @@ class TheValleyOfShadows(): def get_cards(): from bang.expansions.the_valley_of_shadows import cards return cards.get_starting_deck() + +class WildWestShow(): + def get_characters(): + from bang.expansions.wild_west_show import characters + return characters.all_characters() \ No newline at end of file diff --git a/backend/bang/expansions/wild_west_show/card_events.py b/backend/bang/expansions/wild_west_show/card_events.py new file mode 100644 index 0000000..666f1e1 --- /dev/null +++ b/backend/bang/expansions/wild_west_show/card_events.py @@ -0,0 +1,94 @@ +import random + +from bang.expansions.fistful_of_cards.card_events import CardEvent + + +# class Bavaglio(CardEvent): +# def __init__(self): +# super().__init__("Bavaglio", "🀐") +# # I giocatori non possono parlare (ma possono gesticolare, mugugnare...). Chi parla perde 1 punto vita. +# # NOT IMPLEMENTED + +class Camposanto(CardEvent): + """ + All'inizio del proprio turno, ogni giocatore eliminato torna in gioco con 1 punto vita. Pesca il ruolo a caso fra quelli dei giocatori eliminati. + """ + def __init__(self): + super().__init__("Camposanto", "⚰") + +class DarlingValentine(CardEvent): + """ + All'inizio del proprio turno, ogni giocatore scarta le carte in mano e ne pesca dal mazzo altrettante. + """ + def __init__(self): + super().__init__("Darling Valentine", "πŸ’‹") + +class DorothyRage(CardEvent): + """ + Nel proprio turno, ogni giocatore puΓ² obbligarne un altro a giocare una carta. + """ + def __init__(self): + super().__init__("Dorothy Rage", "πŸ‘©β€βš–οΈ") + +class HelenaZontero(CardEvent): + """ + Quando Helena entra in gioco, "estrai!": se esce Cuori o Quadri, rimescola i ruoli attivi tranne lo Sceriffo, e ridistribuiscili a caso. + """ + def __init__(self): + super().__init__("Helena Zontero", "πŸ’ž") + +class LadyRosaDelTexas(CardEvent): + """ + Nel proprio turno, ogni giocatore puΓ² scambiarsi di posto con quello alla sua destra, il quale salta il prossimo turno. + """ + def __init__(self): + super().__init__("Lady Rosa del Texas", "🩰") + +class MissSusanna(CardEvent): + """ + Nel proprio turno ogni giocatore deve giocare almeno 3 carte. Se non lo fa, perde 1 punto vita. + """ + def __init__(self): + super().__init__("Miss Susanna", "πŸ‘©β€πŸŽ€") + +class RegolamentoDiConti(CardEvent): + """ + Tutte le carte possono essere giocate come se fossero BANG!. Le carte BANG! come se fossero Mancato! + """ + def __init__(self): + super().__init__("Regolamento di Conti", "🀠") + +class Sacagaway(CardEvent): + """ + Tutti i giocatori giocano a carte scoperte (tranne il ruolo!). + """ + def __init__(self): + super().__init__("Sacagaway", "πŸŒ„") + +class WildWestShow(CardEvent): + """ + L'obiettivo di ogni giocatore diventa: "Rimani l'ultimo in gioco!" + """ + def __init__(self): + super().__init__("Wild West Show", "πŸŽͺ") + +def get_endgame_card(): + end_game = WildWestShow() + end_game.expansion = 'wild-west-show' + return end_game + +def get_all_events(rng=random): + cards = [ + Camposanto(), + DarlingValentine(), + # DorothyRage(), + HelenaZontero(), + LadyRosaDelTexas(), + MissSusanna(), + RegolamentoDiConti(), + Sacagaway(), + ] + rng.shuffle(cards) + for c in cards: + c.expansion = 'wild-west-show' + return cards \ No newline at end of file diff --git a/backend/bang/expansions/wild_west_show/characters.py b/backend/bang/expansions/wild_west_show/characters.py new file mode 100644 index 0000000..b8d28d1 --- /dev/null +++ b/backend/bang/expansions/wild_west_show/characters.py @@ -0,0 +1,93 @@ +from typing import List +from bang.characters import Character + +class BigSpencer(Character): + """ + Inizia con 5 carte. Non puΓ² giocare Mancato! + """ + def __init__(self): + super().__init__("Big Spencer", max_lives=9) + self.icon = '🫘' + +class FlintWestwood(Character): + """ + Nel suo turno puΓ² scambiare una carta dalla mano con 2 carte a caso dalla mano di un altro giocatore. + > NOTE: La carta dalla tua mano Γ¨ a scelta, non a caso. Se il giocatore bersaglio ha una sola carta, ne ricevi solo una. + """ + def __init__(self): + super().__init__("Flint Westwood", max_lives=4) + self.icon = 'πŸ”«' + + def special(self, player, data): + if not player.is_my_turn or not any((len(p.hand) > 0 for p in player.game.get_alive_players())) or not super().special(player, data): + return False + from bang.players import PendingAction + player.available_cards = player.hand.copy() + player.choose_text = 'choose_flint_special' + player.pending_action = PendingAction.CHOOSE + player.special_use_count += 1 + player.notify_self() + +class GaryLooter(Character): + """ + Pesca tutte le carte in eccesso scartate dagli altri giocatori a fine turno. + """ + def __init__(self): + super().__init__("Gary Looter", max_lives=5) + self.icon = 'πŸ₯²' + +class GreygoryDeckard(Character): + """ + All'inizio del suo turno puΓ² pescare 2 personaggi a caso. Ha tutte le abilitΓ  dei personaggi pescati. + """ + def __init__(self): + super().__init__("Greygory Deckard", max_lives=4) + self.icon = 'πŸ‘¨β€πŸ¦³' + +class JohnPain(Character): + """ + Se ha meno di 6 carte in mano, quando un giocatore "estrae!" John aggiunge alla mano la carta appena estratta. + """ + def __init__(self): + super().__init__("John Pain", max_lives=4) + self.icon = 'πŸ€•' + +class LeeVanKliff(Character): + """ + Nel suo turno, puΓ² scartare un BANG! per ripetere l'effetto di una carta a bordo marrone che ha appena giocato. + """ + def __init__(self): + super().__init__("Lee Van Kliff", max_lives=4) + self.icon = 'πŸ‘¨β€πŸ¦²' + +class TerenKill(Character): + """ + Ogni volta che sarebbe eliminato "estrai!": se non Γ¨ Picche, Teren resta a 1 punto vita e pesca 1 carta. + """ + def __init__(self): + super().__init__("Teren Kill", max_lives=3) + self.icon = 'πŸ‘¨β€πŸ¦°' + +class YoulGrinner(Character): + """ + Prima di pescare, i giocatori con piΓΉ carte in mano di lui devono dargli una carta a scelta. + """ + def __init__(self): + super().__init__("Youl Grinner", max_lives=4) + self.icon = '🀑' + +def all_characters() -> List[Character]: + cards = [ + BigSpencer(), + FlintWestwood(), + # GaryLooter(), + # GreygoryDeckard(), + JohnPain(), + # LeeVanKliff(), + TerenKill(), + YoulGrinner(), + ] + for c in cards: + c.expansion_icon = 'πŸŽͺ' + c.expansion = 'wild_west_show' + return cards diff --git a/backend/bang/game.py b/backend/bang/game.py index 82f006e..6c1d739 100644 --- a/backend/bang/game.py +++ b/backend/bang/game.py @@ -12,6 +12,7 @@ from bang.deck import Deck import bang.roles as roles import bang.expansions.fistful_of_cards.card_events as ce import bang.expansions.high_noon.card_events as ceh +import bang.expansions.wild_west_show.card_events as cew 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 @@ -61,7 +62,7 @@ class Game: self.initial_players = 0 self.password = '' self.expansions: List[str] = [] - self.available_expansions = ['dodge_city', 'fistful_of_cards', 'high_noon', 'gold_rush', 'the_valley_of_shadows'] + self.available_expansions = ['dodge_city', 'fistful_of_cards', 'high_noon', 'gold_rush', 'the_valley_of_shadows', 'wild_west_show'] self.shutting_down = False self.is_competitive = False self.disconnect_bot = True @@ -78,6 +79,7 @@ class Game: self.attack_in_progress = False self.characters_to_distribute = 2 # personaggi da dare a inizio partita self.debug = self.name == 'debug' + self.dead_roles: List[roles.Role] = [] self.is_changing_pwd = False self.is_hidden = False self.rng = random.Random() @@ -113,6 +115,7 @@ class Game: self.incremental_turn = 0 self.turn = 0 self.pending_winners = [] + self.dead_roles: List[roles.Role] = [] for p in self.players: p.reset() p.notify_self() @@ -579,12 +582,16 @@ class Game: Metrics.send_metric('incremental_turn', points=[self.incremental_turn], tags=[f'game:{self.SEED}']) if self.players[self.turn].is_dead: pl = sorted(self.get_dead_players(), key=lambda x:x.death_turn)[0] - if self.check_event(ce.DeadMan) and not self.did_resuscitate_deadman and pl == self.players[self.turn]: + if self.check_event([ce.DeadMan, cew.Camposanto]) and not self.did_resuscitate_deadman and pl == self.players[self.turn]: print(f'{self.name}: {self.players[self.turn]} is dead, revive') - self.did_resuscitate_deadman = True + if self.check_event(ce.DeadMan): + self.did_resuscitate_deadman = True + pl.lives = 2 + elif self.check_event(cew.Camposanto): + pl.lives = 1 + pl.set_role = self.dead_roles.pop(random.randint(0, len(self.dead_roles)-1)) pl.is_dead = False pl.is_ghost = False - pl.lives = 2 self.deck.draw(player=pl) self.deck.draw(player=pl) if (ghost := next((c for c in pl.equipment if isinstance(c, tvosc.Fantasma)), None)) is not None: @@ -603,6 +610,16 @@ class Game: if len(self.deck.event_cards) > 0 and self.deck.event_cards[0] is not None: print(f'{self.name}: flip new event {self.deck.event_cards[0].name}') G.sio.emit('chat_message', room=self.name, data={'color': f'orange','text':f'_flip_event|{self.deck.event_cards[0].name}'}) + if self.check_event(cew.HelenaZontero): + c = self.deck.pick_and_scrap() + G.sio.emit('chat_message', room=self.name, data=f'_flipped|Helena Zontero|{c.name}|{c.num_suit()}') + if c.check_suit(self, [cs.Suit.HEARTS, cs.Suit.DIAMONDS]): + G.sio.emit('chat_message', room=self.name, data=f'_swapped_roles|Helena Zontero|{c.name}|{c.num_suit()}') + pls = [p for p in self.players if not isinstance(p.role, roles.Sheriff)] + newroles = [p.role for p in pls] + random.shuffle(newroles) + for p in pls: + p.set_role(newroles.pop(random.randint(0, len(newroles)-1))) if self.check_event(ce.DeadMan): self.did_resuscitate_deadman = False elif self.check_event(ce.RouletteRussa): @@ -632,6 +649,9 @@ class Game: if self.waiting_for != 0: return self.dalton_on = False + elif self.check_event(cew.WildWestShow): + for p in self.players: + p.set_role(roles.Renegade()) if self.check_event(ce.PerUnPugnoDiCarte) and len(self.players[self.turn].hand) > 0: self.player_bangs = len(self.players[self.turn].hand) @@ -762,6 +782,7 @@ class Game: # if not disconnected: # self.dead_players.append(corpse) self.notify_room() + self.dead_roles.append(player.role) G.sio.emit('chat_message', room=self.name, data=f'_died|{player.name}') for p in self.players: if not p.is_bot: @@ -845,7 +866,10 @@ class Game: def check_event(self, ev): if self.deck is None or len(self.deck.event_cards) == 0: return False - return isinstance(self.deck.event_cards[0], ev) + if isinstance(ev, type): + return isinstance(self.deck.event_cards[0], ev) + else: + return any(isinstance(self.deck.event_cards[0], evc) for evc in ev) def get_visible_players(self, player: pl.Player): # returns a dictionary because we need to add the distance pls = self.get_alive_players() @@ -874,9 +898,11 @@ class Game: def notify_all(self): if self.started and self.replay_speed > 0: + show_cards = self.check_event(cew.Sacagaway) data = [{ 'name': p.name, 'ncards': len(p.hand), + 'hand_cards': [c.__dict__ for c in p.hand] if show_cards else [], 'equipment': [e.__dict__ for e in p.equipment], 'gold_rush_equipment': [e.__dict__ for e in p.gold_rush_equipment], 'lives': p.lives, diff --git a/backend/bang/players.py b/backend/bang/players.py index 8bcd78f..afd5a43 100644 --- a/backend/bang/players.py +++ b/backend/bang/players.py @@ -1,7 +1,7 @@ from __future__ import annotations from enum import IntEnum import json -from random import random, randrange, sample, uniform +from random import random, randrange, sample, uniform, randint import socketio import bang.deck as deck import bang.roles as r @@ -11,6 +11,8 @@ import bang.characters as chars import bang.expansions.dodge_city.characters as chd import bang.expansions.fistful_of_cards.card_events as ce import bang.expansions.high_noon.card_events as ceh +import bang.expansions.wild_west_show.card_events as cew +import bang.expansions.wild_west_show.characters as chw 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 @@ -88,6 +90,7 @@ class Player: self.is_bot = bot self.discord_token = discord_token self.discord_id = None + self.played_cards = 0 self.avatar = '' if self.is_bot: self.avatar = robot_pictures[randrange(len(robot_pictures))] @@ -262,6 +265,12 @@ class Player: if isinstance(self.gold_rush_equipment[i], grc.Zaino): self.gold_rush_equipment[i].play_card(self, None) return # play card will notify the player + if self.character.check(self.game, chw.TerenKill): + picked: cs.Card = self.game.deck.pick_and_scrap() + G.sio.emit('chat_message', room=self.game.name, data=f'_flipped|{self.name}|{picked.name}|{picked.num_suit()}') + if not picked.check_suit(self.game, [cs.Suit.SPADES]): + self.lives = 1 + self.game.deck.draw(True, player=self) if self.character.check(self.game, chars.SidKetchum) and len(self.hand) > 1 and self.lives == 0: if self.game.players[self.game.turn] != self: self.game.players[self.game.turn].pending_action = PendingAction.WAIT @@ -439,6 +448,7 @@ class Player: return self.end_turn(forced=True) self.scrapped_cards = 0 self.setaccio_count = 0 + self.played_cards = 0 self.can_play_ranch = True self.is_playing_ranch = False self.can_play_vendetta = can_play_vendetta @@ -453,6 +463,11 @@ class Player: self.has_played_bang = False self.special_use_count = 0 self.bang_used = 0 + if self.game.check_event(cew.DarlingValentine): + hand = len(self.hand) + for _ in range(hand): + self.game.deck.scrap(self.hand.pop(0), True, player=self) + self.game.deck.draw(True, player=self) if self.game.check_event(ceh.MezzogiornoDiFuoco): self.lives -= 1 if any((isinstance(c, grc.Talismano) for c in self.gold_rush_equipment)): @@ -511,6 +526,19 @@ class Player: self.choose_text = 'choose_cecchino' self.pending_action = PendingAction.CHOOSE self.notify_self() + if self.is_my_turn and self.pending_action == PendingAction.PLAY and pile == 'event' and self.game.check_event(cew.RegolamentoDiConti) and len(self.hand) > 0: + if not self.has_played_bang and any((self.get_sight() >= p['dist'] for p in self.game.get_visible_players(self))): + self.available_cards = self.hand.copy() + self.pending_action = PendingAction.CHOOSE + self.choose_text = 'choose_play_as_bang' + self.notify_self() + elif self.is_my_turn and self.pending_action == PendingAction.PLAY and pile == 'event' and self.game.check_event(cew.LadyRosaDelTexas): + nextp = self.game.next_player() + i, j = self.game.players_map[self.name], self.game.players_map[nextp.name] + self.game.players[i], self.game.players[j] = nextp, self + self.game.players_map[self.name], self.game.players_map[nextp.name] = j, i + self.game.turn = j + self.game.notify_all() elif self.is_my_turn and self.pending_action == PendingAction.PLAY and pile == 'event' and self.game.check_event(ce.Rimbalzo) and any((c.name == cs.Bang(0,0).name for c in self.hand)): self.available_cards = [{ 'name': p.name, @@ -551,6 +579,12 @@ class Player: self.notify_self() else: self.pending_action = PendingAction.PLAY + if self.character.check(self.game, chw.YoulGrinner): + hsize = len(self.hand) + for p in self.game.get_alive_players(): + if p != self and len(p.hand) > hsize: + G.sio.emit('card_drawn', room=self.game.name, data={'player': self.name, 'pile': p.name}) + self.hand.append(p.hand.pop(randint(0, len(p.hand)-1))) num = 2 if not self.character.check(self.game, chd.BillNoface) else self.max_lives-self.lives+1 if self.character.check(self.game, chd.PixiePete): num += 1 if self.character.check(self.game, tvosch.TucoFranziskaner) and not any((True for c in self.equipment if not c.usable_next_turn)): num += 2 @@ -756,6 +790,8 @@ class Player: if not self.game.is_replay: Metrics.send_metric('play_card', points=[1], tags=[f'success:{did_play_card}', f'card:{card.name}', f'bot:{self.is_bot}', f'exp:{card.expansion if "expansion" in card.__dict__ else "vanilla"}']) print("did play card:", did_play_card) + if did_play_card: + self.played_cards += 1 self.notify_self() if self.is_bot: return did_play_card or card.is_equipment or (card.usable_next_turn and not card.can_be_used_now) @@ -918,6 +954,27 @@ class Player: self.pending_action = PendingAction.WAIT self.game.responders_did_respond_resume_turn() self.notify_self() + elif 'choose_flint_special' == self.choose_text: + if card_index < len(self.hand): + self.available_cards = [{ + 'name': p.name, + 'icon': p.role.icon if(self.game.initial_players == 3) else '⭐️' if p['is_sheriff'] else '🀠', + 'avatar': p.avatar, + 'is_character': True, + 'is_player': True + } for p in self.game.get_alive_players() if p.name != self.name and len(p.hand) > 0] + self.choose_text = f'choose_flint_special;{card_index}' + self.notify_self() + elif 'choose_flint_special' in self.choose_text: + if card_index < len(self.available_cards): + my_card = self.hand.pop(int(self.choose_text.split(';')[1])) + other_player = self.game.get_player_named(self.available_cards[card_index]['name']) + for i in range(min(2, len(other_player.hand))): + self.hand.append(other_player.hand.pop(randint(0, len(other_player.hand)-1))) + other_player.hand.append(my_card) + other_player.notify_self() + self.pending_action = PendingAction.PLAY + self.notify_self() elif self.game.check_event(ceh.NuovaIdentita) and self.choose_text == 'choose_nuova_identita': if card_index == 1: # the other character self.character = self.not_chosen_character @@ -1088,6 +1145,8 @@ class Player: self.expected_response = self.game.deck.mancato_cards.copy() if self.character.check(self.game, chars.CalamityJanet) and cs.Bang(0, 0).name not in self.expected_response: self.expected_response.append(cs.Bang(0, 0).name) + if self.character.check(self.game, chw.BigSpencer): + self.expected_response = [] self.on_failed_response_cb = self.take_damage_response self.notify_self() @@ -1118,6 +1177,8 @@ class Player: self.expected_response = self.game.deck.mancato_cards.copy() if self.character.check(self.game, chars.CalamityJanet) and cs.Bang(0, 0).name not in self.expected_response: self.expected_response.append(cs.Bang(0, 0).name) + if self.character.check(self.game, chw.BigSpencer): + self.expected_response = [] self.on_failed_response_cb = self.take_no_damage_response self.notify_self() @@ -1214,6 +1275,8 @@ class Player: self.expected_response = self.game.deck.mancato_cards_not_green_or_blue.copy() if self.character.check(self.game, chars.CalamityJanet) and cs.Bang(0, 0).name not in self.expected_response: self.expected_response.append(cs.Bang(0, 0).name) + if self.character.check(self.game, chw.BigSpencer): + self.expected_response = [] if self.can_escape(card_name, with_mancato=False): self.expected_response.append(tvosc.Fuga(0, 0).name) if not no_dmg: @@ -1536,6 +1599,9 @@ class Player: for i in range(len(self.equipment)): self.game.deck.scrap(self.equipment.pop(), True) self.is_my_turn = False + if self.played_cards < 3 and self.game.check_event(cew.MissSusanna): + self.lives -= 1 + self.played_cards = 0 self.can_play_again_don_bell = True self.committed_suit_manette = None self.pending_action = PendingAction.WAIT diff --git a/backend/server.py b/backend/server.py index c42dda1..6a21aa8 100644 --- a/backend/server.py +++ b/backend/server.py @@ -631,11 +631,14 @@ def chat_message(sid, msg, pl=None): sio.emit('chat_message', room=ses.game.name, data={'color': f'red','text':f'🚨 {ses.name} is in debug mode and changed event'}) import bang.expansions.fistful_of_cards.card_events as ce import bang.expansions.high_noon.card_events as ceh + import bang.expansions.wild_west_show.card_events as cew chs = [] chs.extend(ce.get_all_events()) chs.append(ce.get_endgame_card()) chs.extend(ceh.get_all_events()) chs.append(ceh.get_endgame_card()) + chs.extend(cew.get_all_events()) + chs.append(cew.get_endgame_card()) ses.game.deck.event_cards.insert(int(cmd[1]), [c for c in chs if c is not None and c.name == ' '.join(cmd[2:])][0]) ses.game.notify_event_card() elif '/removecard' in msg: diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py index e79239c..02a3aa6 100644 --- a/backend/tests/__init__.py +++ b/backend/tests/__init__.py @@ -1,3 +1,39 @@ +import pytest +from bang.characters import Character +from bang.game import Game +from bang.players import Player from tests.dummy_socket import DummySocket from globals import G -G.sio = DummySocket() \ No newline at end of file +G.sio = DummySocket() + + +def started_game(expansions, players=4, character=Character('test_char', 4)): + g = Game('test') + g.expansions = expansions + ps = [Player(f'p{i}', f'p{i}') for i in range(players)] + for p in ps: + g.add_player(p) + g.start_game() + for p in ps: + p.available_characters = [character] + p.set_character(p.available_characters[0].name) + return g + + +def set_events(g: Game, event_cards): + g.deck.event_cards = event_cards + + +def current_player(g: Game): + return g.players[g.turn] + + +def next_player(g: Game): + return g.players[(g.turn + 1) % len(g.players)] + + +def current_player_with_cards(g: Game, cards): + p = current_player(g) + p.draw('') + p.hand = cards + return p diff --git a/backend/tests/wild_west_show_characters_test.py b/backend/tests/wild_west_show_characters_test.py new file mode 100644 index 0000000..ca3ec32 --- /dev/null +++ b/backend/tests/wild_west_show_characters_test.py @@ -0,0 +1,34 @@ + +from tests import started_game, set_events, current_player, next_player, current_player_with_cards + +from bang.expansions.wild_west_show.characters import * +from bang.cards import Card, Suit +import bang.roles as roles +from bang.players import PendingAction + + +# test TerenKill +def test_TerenKill(): + g = started_game(['wild_west_show'], 4, TerenKill()) + p = current_player_with_cards(g, []) + p.lives = 0 + g.deck.cards = [Card(Suit.HEARTS, 'card', 0), Card(Suit.HEARTS, 'card', 0)] + p.notify_self() + assert p.lives == 1 + assert len(p.hand) == 1 + p.lives = 0 + g.deck.cards = [Card(Suit.SPADES, 'card', 0), Card(Suit.HEARTS, 'card', 0)] + p.notify_self() + assert p.lives == 0 + + +# test YoulGrinner +def test_YoulGrinner(): + g = started_game(['wild_west_show'], 4, YoulGrinner()) + p = current_player(g) + p.hand = [] + p.draw('') + assert len(p.hand) == 5 + for pl in g.players: + if pl != p: + assert len(pl.hand) == 3 diff --git a/backend/tests/wild_west_show_events_test.py b/backend/tests/wild_west_show_events_test.py new file mode 100644 index 0000000..4a85cbc --- /dev/null +++ b/backend/tests/wild_west_show_events_test.py @@ -0,0 +1,98 @@ + +from tests import started_game, set_events, current_player, next_player, current_player_with_cards + +from bang.expansions.wild_west_show.card_events import * +from bang.cards import Card, Suit +import bang.roles as roles +from bang.players import PendingAction + + +# test Camposanto +def test_camposanto(): + g = started_game(['wild_west_show'], 4) + set_events(g, [Camposanto()]) + current_player_with_cards(g, []).end_turn() + p = current_player_with_cards(g, []) + p.lives = 0 + p.notify_self() + p1 = current_player_with_cards(g, []) + p1.lives = 0 + p1.notify_self() + current_player_with_cards(g, []).end_turn() + current_player_with_cards(g, []).end_turn() + assert p.is_my_turn + assert p.lives == 1 + current_player_with_cards(g, []).end_turn() + assert p1.is_my_turn + assert p1.lives == 1 + + +# test DarlingValentine +def test_darling_valentine(): + g = started_game(['wild_west_show'], 4) + set_events(g, [DarlingValentine()]) + p = next_player(g) + hand = p.hand.copy() + current_player_with_cards(g, []).end_turn() + assert hand != current_player(g).hand + + +# test DorothyRage + +# test HelenaZontero +def test_helena_zontero(): + g = started_game(['wild_west_show'], 8) + set_events(g, [None, HelenaZontero()]) + roles = [p.role.name for p in g.players] + for i in range(len(g.players)-1): + current_player_with_cards(g, []).end_turn() + g.deck.cards = [Card(Suit.HEARTS, 'card', 0)]*5 + current_player_with_cards(g, []).end_turn() + roles2 = [p.role.name for p in g.players] + assert roles != roles2 + +# test LadyRosaDelTexas +def test_LadyRosaDelTexas(): + g = started_game(['wild_west_show'], 4) + set_events(g, [LadyRosaDelTexas()]) + p = current_player_with_cards(g, [Card(0,'card',0)]*4) + t = g.turn + p.draw('event') + assert g.turn == (t+1)%len(g.players) + +# test MissSusanna +def test_miss_suzanna(): + g = started_game(['wild_west_show'], 4) + set_events(g, [MissSusanna()]) + p = current_player_with_cards(g, []) + p.end_turn() + assert p.lives == 4 # sceriffo 5-1 + p = current_player_with_cards(g, [Card(0,'card',0)]*4) + p.play_card(0) + p.play_card(0) + p.play_card(0) + p.end_turn() + assert p.lives == 4 + p = current_player_with_cards(g, []) + p.end_turn() + assert p.lives == 3 + + +# test RegolamentoDiConti +def test_RegolamentoDiConti(): + g = started_game(['wild_west_show'], 4) + set_events(g, [RegolamentoDiConti()]) + p = current_player_with_cards(g, [Card(0,'card',0)]*4) + p.draw('event') + assert p.pending_action == PendingAction.CHOOSE + p.choose(0) + + +# test WildWestShow +def test_WildWestShow(): + g = started_game(['wild_west_show'], 8) + set_events(g, [None, WildWestShow()]) + for i in range(len(g.players)): + current_player_with_cards(g, []).end_turn() + for p in g.players: + assert isinstance(p.role, roles.Renegade) \ No newline at end of file diff --git a/frontend/src/components/Card.vue b/frontend/src/components/Card.vue index 9fd488b..a1540f7 100644 --- a/frontend/src/components/Card.vue +++ b/frontend/src/components/Card.vue @@ -1,267 +1,299 @@ \ No newline at end of file diff --git a/frontend/src/components/Lobby.vue b/frontend/src/components/Lobby.vue index b9e06b1..61eab6b 100644 --- a/frontend/src/components/Lobby.vue +++ b/frontend/src/components/Lobby.vue @@ -1,622 +1,896 @@ diff --git a/frontend/src/components/Player.vue b/frontend/src/components/Player.vue index 685616e..c7ea4d7 100644 --- a/frontend/src/components/Player.vue +++ b/frontend/src/components/Player.vue @@ -36,6 +36,7 @@ +
{{$t('hand')}} diff --git a/frontend/src/components/TinyHand.vue b/frontend/src/components/TinyHand.vue index f3c3c90..860f8f9 100644 --- a/frontend/src/components/TinyHand.vue +++ b/frontend/src/components/TinyHand.vue @@ -1,20 +1,30 @@ @@ -24,6 +34,9 @@ export default { animation-duration: 2s; animation-iteration-count: infinite; } +.zoomable:hover { + z-index: 1; +} @keyframes updown { 0% { top: 0; diff --git a/frontend/src/i18n/en.json b/frontend/src/i18n/en.json index 3bd627c..2cb0f2c 100644 --- a/frontend/src/i18n/en.json +++ b/frontend/src/i18n/en.json @@ -77,6 +77,7 @@ "choose_ricercato": "Choose who you will play Wanted against.", "choose_birra_function": "Choose between getting 1 gold nugget by discarding beer or if you want to play the beer.", "choose_play_as_bang": "Choose which card to play as Bang!", + "choose_flint_special": "Choose which card to swap.", "emporio_others": "{0} is choosing which card to get from the General Store", "you_died": "YOU DIED", "spectate": "SPECTATE", diff --git a/frontend/src/i18n/it.json b/frontend/src/i18n/it.json index afca9c4..4222422 100644 --- a/frontend/src/i18n/it.json +++ b/frontend/src/i18n/it.json @@ -77,6 +77,7 @@ "choose_ricercato": "Scegli il giocatore su cui vuoi giocare Ricercato", "choose_birra_function": "Scegli tra ottenere 1 pepita scartando la birra oppure giocare la birra.", "choose_play_as_bang": "Scegli che carta giocare come Bang!", + "choose_flint_special": "Scegli che carta scabiare.", "emporio_others": "{0} sta scegliendo che carta prendere dall'emporio", "you_died": "SEI MORTO", "spectate": "SPETTATORE", diff --git a/frontend/src/utils/emoji-map.js b/frontend/src/utils/emoji-map.js index 7e61c32..ec5ec17 100644 --- a/frontend/src/utils/emoji-map.js +++ b/frontend/src/utils/emoji-map.js @@ -4,4 +4,5 @@ export const emojiMap = { 'high_noon': 'πŸ”₯', 'fistful_of_cards': '🎴', 'the_valley_of_shadows': 'πŸ‘»', + 'wild_west_show': 'πŸŽͺ', } \ No newline at end of file diff --git a/frontend/src/utils/expansions-map.js b/frontend/src/utils/expansions-map.js index ae6a70e..0b3ccc5 100644 --- a/frontend/src/utils/expansions-map.js +++ b/frontend/src/utils/expansions-map.js @@ -1,11 +1,11 @@ export const expansionsMap = { - 'dodge_city' : { + 'dodge_city': { name: 'Dodge City', icon: 'πŸ„', back: true, expansion: 'dodge-city', }, - 'fistful_of_cards' : { + 'fistful_of_cards': { name: 'Fistful of Cards', icon: '🎴', back: true, @@ -28,5 +28,13 @@ export const expansionsMap = { icon: 'πŸ‘»', back: true, expansion: 'the-valley-of-shadows', + status: 'beta', + }, + 'wild_west_show': { + name: 'Wild West Show', + icon: 'πŸŽͺ', + back: true, + expansion: 'wild-west-show', + status: 'alpha', } } \ No newline at end of file