Merge branch 'wild-west-show' into dev

This commit is contained in:
Alberto Xamin 2023-03-12 11:44:42 +00:00
commit d98c7de4b9
No known key found for this signature in database
GPG Key ID: 5ABFCD8A22EA6F5D
19 changed files with 1611 additions and 806 deletions

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -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

View File

@ -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,

View File

@ -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

View File

@ -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:

View File

@ -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()
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

View File

@ -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

View File

@ -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)

View File

@ -1,267 +1,299 @@
<template>
<div :class="{ card: true, avatarred:card.avatar, equipment: card.is_equipment, character:card.is_character, back:card.is_back, 'usable-next-turn':card.usable_next_turn, 'must-be-used':card.must_be_used, 'gold-rush': card.expansion === 'gold_rush', 'brown':card.kind === 0, 'black':card.kind === 1,}">
<h4>{{cardName}}</h4>
<div v-if="card.avatar" class="avatar" :style="`background-image: url(${card.avatar});`"></div>
<div :class="{emoji:true, bottomed:card.avatar}">{{emoji}}</div>
<div class="alt_text">{{card.alt_text}}</div>
<div class="suit">{{number}}<span :style="`${(card.suit !== undefined && card.suit%2 === 0)? 'color:red':''}`">{{suit}}</span></div>
<div class="expansion" v-if="card.expansion_icon">{{card.expansion_icon}}</div>
</div>
<div
:class="{
card: true,
avatarred: card.avatar,
equipment: card.is_equipment,
character: card.is_character,
back: card.is_back,
'usable-next-turn': card.usable_next_turn,
'must-be-used': card.must_be_used,
'gold-rush': card.expansion === 'gold_rush',
brown: card.kind === 0,
black: card.kind === 1,
}"
>
<h4>{{ cardName }}</h4>
<div
v-if="card.avatar"
class="avatar"
:style="`background-image: url(${card.avatar});`"
></div>
<div :class="{ emoji: true, bottomed: card.avatar }">{{ emoji }}</div>
<div class="alt_text">{{ card.alt_text }}</div>
<div class="suit">
{{ number
}}<span
:style="`${
card.suit !== undefined && card.suit % 2 === 0 ? 'color:red' : ''
}`"
>{{ suit }}</span
>
</div>
<div class="expansion" v-if="card.expansion_icon">
{{ card.expansion_icon }}
</div>
</div>
</template>
<script>
export default {
name: 'Card',
props: {
card: Object,
donotlocalize: Boolean
},
computed: {
cardName(){
// console.log(this.$t(`cards.${this.card.name}.name`))
if (!this.donotlocalize && this.$t(`cards.${this.card.name}.name`) !== `cards.${this.card.name}.name`) {
return this.$t(`cards.${this.card.name}.name`)
}
if (this.card.name == "you") {
return this.$t('you')
}
return this.card.name
},
emoji(){
return this.card.icon != "you" ? this.card.icon : this.$t('you')
},
suit() {
if (this.card && !isNaN(this.card.suit)) {
let x = ['♦️','♣️','♥️','♠️','🤑']
return x[this.card.suit];
} else if (this.card.suit) {
return this.card.suit;
}
return '';
},
number() {
if (isNaN(this.card.suit))
return this.card.number
if (this.card.number === 1) return 'A'
else if (this.card.number === 11) return 'J'
else if (this.card.number === 12) return 'Q'
else if (this.card.number === 13) return 'K'
else return this.card.number
}
}
}
name: "Card",
props: {
card: Object,
donotlocalize: Boolean,
},
computed: {
cardName() {
// console.log(this.$t(`cards.${this.card.name}.name`))
if (
!this.donotlocalize &&
this.$t(`cards.${this.card.name}.name`) !==
`cards.${this.card.name}.name`
) {
return this.$t(`cards.${this.card.name}.name`);
}
if (this.card.name == "you") {
return this.$t("you");
}
return this.card.name;
},
emoji() {
return this.card.icon != "you" ? this.card.icon : this.$t("you");
},
suit() {
if (this.card && !isNaN(this.card.suit)) {
let x = ["♦️", "♣️", "♥️", "♠️", "🤑"];
return x[this.card.suit];
} else if (this.card.suit) {
return this.card.suit;
}
return "";
},
number() {
if (isNaN(this.card.suit)) return this.card.number;
if (this.card.number === 1) return "A";
else if (this.card.number === 11) return "J";
else if (this.card.number === 12) return "Q";
else if (this.card.number === 13) return "K";
else return this.card.number;
},
},
};
</script>
<style>
.card {
cursor: pointer;
width: 60pt;
min-width:60pt;
height: 100pt;
margin: 12pt;
background: var(--bg-color);
box-shadow:
0 0 0 3pt #987e51,
0 0 0 6pt var(--bg-color),
0 0 5pt 6pt #aaa;
border-radius: 6pt;
position: relative;
transition: all 0.5s ease-in-out;
color: var(--font-color);
/* overflow: hidden; */
text-overflow: ellipsis;
word-wrap: normal;
/* word-wrap: break-word; */
cursor: pointer;
width: 60pt;
min-width: 60pt;
height: 100pt;
margin: 12pt;
background: var(--bg-color);
box-shadow: 0 0 0 3pt #987e51, 0 0 0 6pt var(--bg-color), 0 0 5pt 6pt #aaa;
border-radius: 6pt;
position: relative;
transition: all 0.5s ease-in-out;
color: var(--font-color);
text-overflow: ellipsis;
word-wrap: normal;
}
.avatarred {
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
display: flex;
align-items: center;
justify-content: center;
flex-wrap: wrap;
}
.card.back{
color:white;
background: repeating-linear-gradient(
45deg,
#987e51,
#987e51 5px,
#816b45 5px,
#816b45 10px
);
.card.back {
color: white;
background: repeating-linear-gradient(
45deg,
#987e51,
#987e51 5px,
#816b45 5px,
#816b45 10px
);
}
.card:not(.back,.fistful-of-cards,.high-noon,.gold-rush):before{
content: "";
background-image: radial-gradient(var(--bg-color) 13%, #0000 5%),
radial-gradient(var(--bg-color) 14%, transparent 5%),
radial-gradient(var(--bg-color) 8%, transparent 5%);
background-position: -12px 0, 12px 14px, 0 -12pt;
background-size: 50px 50px;
position: absolute;
top: -10px;
left: -10px;
bottom: -4px;
right: -4px;
.card:not(.back, .fistful-of-cards, .high-noon, .gold-rush):before {
content: "";
background-image: radial-gradient(var(--bg-color) 13%, #0000 5%),
radial-gradient(var(--bg-color) 14%, transparent 5%),
radial-gradient(var(--bg-color) 8%, transparent 5%);
background-position: -12px 0, 12px 14px, 0 -12pt;
background-size: 50px 50px;
position: absolute;
top: -10px;
left: -10px;
bottom: -4px;
right: -4px;
}
.card.equipment {
box-shadow:
0 0 0 3pt #5c5e83,
0 0 0 6pt var(--bg-color),
0 0 5pt 6pt #aaa;
box-shadow: 0 0 0 3pt #5c5e83, 0 0 0 6pt var(--bg-color), 0 0 5pt 6pt #aaa;
}
.card.character {
box-shadow:
0 0 0 3pt #7c795b,
0 0 0 6pt var(--bg-color),
0 0 5pt 6pt #aaa;
box-shadow: 0 0 0 3pt #7c795b, 0 0 0 6pt var(--bg-color), 0 0 5pt 6pt #aaa;
}
.card.usable-next-turn {
box-shadow:
0 0 0 3pt #6aa16e, 0 0 0 6pt var(--bg-color), 0 0 5pt 6pt #aaa
box-shadow: 0 0 0 3pt #6aa16e, 0 0 0 6pt var(--bg-color), 0 0 5pt 6pt #aaa;
}
.card.high-noon{
box-shadow: 0 0 0pt 4pt var(--bg-color), 0 0 5pt 4pt #aaa;
border: 2pt dotted rgb(198 78 45);
.card.wild-west-show {
box-shadow: 0 0 0pt 4pt var(--bg-color), 0 0 5pt 4pt #aaa;
border: 2pt dotted #987e51;
}
.card.fistful-of-cards{
box-shadow: 0 0 0pt 4pt var(--bg-color), 0 0 5pt 4pt #aaa;
border: 2pt dashed rgb(50 122 172);
.card.high-noon {
box-shadow: 0 0 0pt 4pt var(--bg-color), 0 0 5pt 4pt #aaa;
border: 2pt dotted rgb(198 78 45);
}
.card.back.fistful-of-cards{
color:var(--bg-color);
background: repeating-linear-gradient(
45deg,
rgb(50 122 172),
rgb(50 122 172) 5px,
rgb(30 102 152) 5px,
rgb(30 102 152) 10px
);
border: 2pt solid rgb(50 122 172);
.card.fistful-of-cards {
box-shadow: 0 0 0pt 4pt var(--bg-color), 0 0 5pt 4pt #aaa;
border: 2pt dashed rgb(50 122 172);
}
.card.back.high-noon{
color:var(--bg-color);
background: repeating-linear-gradient(
45deg,
rgb(198 78 45),
rgb(198 78 45) 5px,
rgb(178 58 25) 5px,
rgb(178 58 25) 10px
);
border: 2pt solid rgb(198 78 45);
.card.back.fistful-of-cards {
background: repeating-linear-gradient(
45deg,
rgb(50 122 172),
rgb(50 122 172) 5px,
rgb(30 102 152) 5px,
rgb(30 102 152) 10px
);
border: 2pt solid rgb(50 122 172);
}
.card.back.the-valley-of-shadows{
color:var(--bg-color);
background: repeating-linear-gradient(
45deg,
rgb(98 88 85),
rgb(98 88 85) 5px,
rgb(78 68 65) 5px,
rgb(78 68 65) 10px
);
box-shadow:
0 0 0 3pt rgb(98 88 85),
0 0 0 6pt var(--bg-color),
0 0 5pt 6pt #aaa;
.card.back.high-noon {
background: repeating-linear-gradient(
45deg,
rgb(198 78 45),
rgb(198 78 45) 5px,
rgb(178 58 25) 5px,
rgb(178 58 25) 10px
);
border: 2pt solid rgb(198 78 45);
}
.card.back.the-valley-of-shadows {
background: repeating-linear-gradient(
45deg,
rgb(98 88 85),
rgb(98 88 85) 5px,
rgb(78 68 65) 5px,
rgb(78 68 65) 10px
);
box-shadow: 0 0 0 3pt rgb(98 88 85), 0 0 0 6pt var(--bg-color),
0 0 5pt 6pt #aaa;
}
.card.back.cant-play {
transform: scale(0.9);
filter: grayscale(1);
opacity: 0.5;
transform: scale(0.9);
filter: grayscale(1);
opacity: 0.5;
}
.card.back.cant-play:hover {
transform: scale(0.95);
filter: grayscale(0.6);
opacity: 0.8;
transform: scale(0.95);
filter: grayscale(0.6);
opacity: 0.8;
}
.beta::after {
content: "Beta";
position: absolute;
bottom: -12pt;
right: -12pt;
background: red;
font-size: 10pt;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
font-weight: bold;
padding: 4pt;
border-radius: 12pt;
content: "Beta";
position: absolute;
bottom: -12pt;
right: -12pt;
background: red;
font-size: 10pt;
color: white;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
font-weight: bold;
padding: 4pt;
border-radius: 12pt;
}
.alpha::after {
content: "Alpha";
position: absolute;
bottom: -12pt;
right: -12pt;
background: red;
font-size: 10pt;
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
font-weight: bold;
padding: 4pt;
border-radius: 12pt;
}
.avatar {
position: absolute;
width: 36pt;
margin: auto;
top: 25%;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
border-radius: 36pt;
height: 36pt;
position: absolute;
width: 36pt;
margin: auto;
top: 25%;
background-position: center;
background-size: contain;
background-repeat: no-repeat;
border-radius: 36pt;
height: 36pt;
}
.card.brown.gold-rush {
box-shadow: 0 0 0pt 4pt var(--bg-color), 0 0 5pt 4pt #aaa;
border: 2pt dotted #9C7340;
box-shadow: 0 0 0pt 4pt var(--bg-color), 0 0 5pt 4pt #aaa;
border: 2pt dotted #9c7340;
}
.card.black.gold-rush {
box-shadow: 0 0 0pt 4pt var(--bg-color), 0 0 5pt 4pt #aaa;
border: 2pt dotted #000;
box-shadow: 0 0 0pt 4pt var(--bg-color), 0 0 5pt 4pt #aaa;
border: 2pt dotted #000;
}
.card.back.gold-rush {
background: repeating-linear-gradient(347deg, #ffb32f, #987e51 );
background: repeating-linear-gradient(347deg, #ffb32f, #987e51);
}
.card h4 {
position: absolute;
text-align: center;
width: 100%;
top: -10pt;
font-size: 11pt;
position: absolute;
text-align: center;
width: 100%;
top: -10pt;
font-size: 11pt;
}
.card.back h4{
font-size: 12pt;
.card.back h4 {
font-size: 12pt;
}
.card .emoji {
position: absolute;
text-align: center;
width: 100%;
font-size:26pt;
top: 35%;
position: absolute;
text-align: center;
width: 100%;
font-size: 26pt;
top: 35%;
}
.emoji.bottomed {
top: 45%;
left: 8pt;
top: 45%;
left: 8pt;
}
.card.must-be-used {
filter: drop-shadow(0 0 5px red);
filter: drop-shadow(0 0 5px red);
}
.fistful-of-cards .emoji, .high-noon .emoji, .exp-pack .emoji{
top:auto !important;
bottom:15% !important;
.fistful-of-cards .emoji,
.high-noon .emoji,
.card.wild-west-show .emoji,
.exp-pack .emoji {
top: auto !important;
bottom: 15% !important;
}
.card .suit {
position: absolute;
bottom: 3pt;
left:3pt;
position: absolute;
bottom: 3pt;
left: 3pt;
}
.card.character .suit {
font-size: small;
right: 3pt;
text-align: center;
font-size: small;
right: 3pt;
text-align: center;
}
.alt_text {
right: 3pt;
text-align: center;
position: absolute;
font-size: small;
bottom: 20pt;
left: 3pt;
right: 3pt;
text-align: center;
position: absolute;
font-size: small;
bottom: 20pt;
left: 3pt;
}
.cant-play {
filter: brightness(0.5);
filter: brightness(0.5);
}
.expansion {
position: absolute;
bottom: -5pt;
right: -5pt;
background: var(--bg-color);
border-radius: 100%;
transform: scale(0.8);
position: absolute;
bottom: -5pt;
right: -5pt;
background: var(--bg-color);
border-radius: 100%;
transform: scale(0.8);
}
</style>

File diff suppressed because it is too large Load Diff

View File

@ -36,6 +36,7 @@
<button :class="{'btn': true, 'cant-play':(pending_action != 2)}" :disabled="pending_action != 2" v-if="!(eventCard && eventCard.name == 'Sbornia') && is_my_turn && character.name === 'Raddie Snake' && special_use_count < 2 && gold_nuggets >=1" @click="()=>{$socket.emit('special', {})}">{{$t('special_ability')}}</button>
<button :class="{'btn': true, 'cant-play':(pending_action != 2)}" :disabled="pending_action != 2" v-if="!(eventCard && eventCard.name == 'Sbornia') && is_my_turn && character.name === 'Der Spot Burst Ringer' && special_use_count < 1" @click="()=>{$socket.emit('special', {})}">{{$t('special_ability')}}</button>
<button :class="{'btn': true, 'cant-play':(pending_action != 2)}" :disabled="pending_action != 2" v-if="!(eventCard && eventCard.name == 'Sbornia') && is_my_turn && character.name === 'Black Flower' && special_use_count < 1" @click="()=>{$socket.emit('special', {})}">{{$t('special_ability')}}</button>
<button :class="{'btn': true, 'cant-play':(pending_action != 2)}" :disabled="pending_action != 2" v-if="!(eventCard && eventCard.name == 'Sbornia') && is_my_turn && character.name === 'Flint Westwood' && special_use_count < 1" @click="()=>{$socket.emit('special', {})}">{{$t('special_ability')}}</button>
</div>
<div v-if="lives > 0 || is_ghost" style="position:relative">
<span id="hand_text">{{$t('hand')}}</span>

View File

@ -1,20 +1,30 @@
<template>
<div style="position:absolute;transform:scale(0.4);bottom:52pt;">
<div :class="{card:true, back:true, delay:ismyturn}" v-for="(n, i) in ncards"
:style="`position:absolute; transform:rotate(${(i-ncards/2)*2}deg) translate(${i*15}px,0); animation-delay:${0.1*i}s`"
v-bind:key="n" :alt="i">
<h4 v-if="n==ncards">PewPew!</h4>
<div class="emoji" v-if="n==ncards">💥</div>
<div v-if="!(cards && cards.length >0)">
<div :class="{card:true, back:true, delay:ismyturn}" v-for="(n, i) in ncards"
:style="`position:absolute; transform:rotate(${(i-ncards/2)*2}deg) translate(${i*15}px,0); animation-delay:${0.1*i}s`"
v-bind:key="n" :alt="i">
<h4 v-if="n==ncards">PewPew!</h4>
<div class="emoji" v-if="n==ncards">💥</div>
</div>
</div>
<div v-else>
<card :card="c" :key="c" v-for="(c, i) in cards"
:class="{delay:ismyturn, zoomable:true}"
:style="`position:absolute; transform:rotate(${(i-ncards/2)*5}deg) translate(${(i-ncards/3)*40}px,0); animation-delay:${0.1*i}s`"/>
</div>
</div>
</template>
<script>
import Card from './Card.vue'
export default {
components: { Card },
name: 'TinyHand',
props: {
ncards: Number,
ismyturn: Boolean
cards: Array,
ismyturn: Boolean,
},
}
</script>
@ -24,6 +34,9 @@ export default {
animation-duration: 2s;
animation-iteration-count: infinite;
}
.zoomable:hover {
z-index: 1;
}
@keyframes updown {
0% {
top: 0;

View File

@ -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",

View File

@ -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",

View File

@ -4,4 +4,5 @@ export const emojiMap = {
'high_noon': '🔥',
'fistful_of_cards': '🎴',
'the_valley_of_shadows': '👻',
'wild_west_show': '🎪',
}

View File

@ -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',
}
}