Merge pull request #505 from albertoxamin/train-robbery

Add Train robbery expansion
This commit is contained in:
Alberto Xamin 2024-06-14 18:16:09 +03:00 committed by GitHub
commit 5d0e46f205
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 2238 additions and 864 deletions

View File

@ -20,6 +20,7 @@ class Suit(IntEnum):
HEARTS = 2 # ♥ HEARTS = 2 # ♥
SPADES = 3 # ♠ SPADES = 3 # ♠
GOLD = 4 # 🤑 GOLD = 4 # 🤑
TRAIN = 5 # 🚂
class Card(ABC): class Card(ABC):
@ -66,7 +67,7 @@ class Card(ABC):
def __str__(self) -> str: def __str__(self) -> str:
if str(self.suit).isnumeric(): if str(self.suit).isnumeric():
char = ["♦️", "♣️", "♥️", "♠️", "🤑"][int(self.suit)] char = ["♦️", "♣️", "♥️", "♠️", "🤑", "🚋"][int(self.suit)]
else: else:
char = self.suit char = self.suit
return f"{self.name} {char}{self.number}" return f"{self.name} {char}{self.number}"
@ -370,14 +371,18 @@ class Birra(Card):
player.game.deck.draw(True, player=p) player.game.deck.draw(True, player=p)
p.notify_self() p.notify_self()
if "gold_rush" in player.game.expansions and self.number != 42: if "gold_rush" in player.game.expansions and self.number != 42:
from bang.players import PendingAction player.set_choose_action(
"choose_birra_function",
player.available_cards = [ [
{"name": "Pepita", "icon": "💵️", "alt_text": "1", "noDesc": True}, {
self, "name": "Pepita",
] "icon": "💵️",
player.choose_text = "choose_birra_function" "alt_text": "1",
player.pending_action = PendingAction.CHOOSE "noDesc": True,
},
self,
],
)
player.notify_self() player.notify_self()
return True return True
if ( if (

View File

@ -1,15 +1,17 @@
from typing import List, Set, Dict, Tuple, Optional, TYPE_CHECKING from typing import List, Set, Dict, Tuple, Optional, TYPE_CHECKING
import random
import bang.cards as cs import bang.cards as cs
import bang.expansions.fistful_of_cards.card_events as ce import bang.expansions.fistful_of_cards.card_events as ce
import bang.expansions.high_noon.card_events as ceh 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.card_events as cew
import bang.expansions.wild_west_show.characters as chw import bang.expansions.wild_west_show.characters as chw
import bang.expansions.gold_rush.shop_cards as grc import bang.expansions.gold_rush.shop_cards as grc
import bang.expansions.train_robbery.stations as trs
import bang.expansions.train_robbery.trains as trt
from globals import G from globals import G
if TYPE_CHECKING: if TYPE_CHECKING:
from bang.game import Game from bang.game import Game
from bang.players import Player
class Deck: class Deck:
@ -35,6 +37,9 @@ class Deck:
self.game = game self.game = game
self.event_cards: List[ce.CardEvent] = [] self.event_cards: List[ce.CardEvent] = []
self.event_cards_wildwestshow: List[ce.CardEvent] = [] self.event_cards_wildwestshow: List[ce.CardEvent] = []
self.stations: List[trs.StationCard] = []
self.train_pile: List[trt.TrainCard] = []
self.current_train: List[trt.TrainCard] = []
endgame_cards: List[ce.CardEvent] = [] endgame_cards: List[ce.CardEvent] = []
if "fistful_of_cards" in game.expansions: if "fistful_of_cards" in game.expansions:
self.event_cards.extend(ce.get_all_events(game.rng)) self.event_cards.extend(ce.get_all_events(game.rng))
@ -47,6 +52,12 @@ class Deck:
game.rng.shuffle(self.event_cards_wildwestshow) game.rng.shuffle(self.event_cards_wildwestshow)
self.event_cards_wildwestshow.insert(0, None) self.event_cards_wildwestshow.insert(0, None)
self.event_cards_wildwestshow.append(cew.get_endgame_card()) self.event_cards_wildwestshow.append(cew.get_endgame_card())
if "train_robbery" in game.expansions:
self.stations = game.rng.sample(trs.get_all_stations(), len(game.players))
self.train_pile = trt.get_all_cards(game.rng)
self.current_train = [trt.get_locomotives(game.rng)[0]] + self.train_pile[
:3
]
if len(self.event_cards) > 0: if len(self.event_cards) > 0:
game.rng.shuffle(self.event_cards) game.rng.shuffle(self.event_cards)
self.event_cards = self.event_cards[:12] self.event_cards = self.event_cards[:12]
@ -106,12 +117,21 @@ class Deck:
self.shop_cards[i].reset_card() self.shop_cards[i].reset_card()
self.game.notify_gold_rush_shop() self.game.notify_gold_rush_shop()
def move_train_forward(self):
if len(self.stations) == 0:
return
if len(self.current_train) == len(self.stations) + 4:
return
if len(self.current_train) > 0:
self.current_train.append(None)
self.game.notify_stations()
def peek(self, n_cards: int) -> list: def peek(self, n_cards: int) -> list:
return self.cards[:n_cards] return self.cards[:n_cards]
def peek_scrap_pile(self) -> cs.Card: def peek_scrap_pile(self, n_cards: int=1) -> List[cs.Card]:
if len(self.scrap_pile) > 0: if len(self.scrap_pile) > 0:
return self.scrap_pile[-1] return self.scrap_pile[-n_cards:]
else: else:
return None return None
@ -176,7 +196,7 @@ class Deck:
else: else:
return self.draw() return self.draw()
def scrap(self, card: cs.Card, ignore_event=False, player=None): def scrap(self, card: cs.Card, ignore_event:bool=False, player:'Player'=None):
if card.number == 42: if card.number == 42:
return return
card.reset_card() card.reset_card()

View File

@ -1,5 +1,6 @@
from typing import List from typing import List
from bang.characters import Character from bang.characters import Character
from globals import PendingAction
class PixiePete(Character): class PixiePete(Character):
@ -165,8 +166,6 @@ class DocHolyday(Character):
def special(self, player, data): def special(self, player, data):
if super().special(player, data): if super().special(player, data):
from bang.players import PendingAction
if ( if (
player.special_use_count < 1 player.special_use_count < 1
and player.pending_action == PendingAction.PLAY and player.pending_action == PendingAction.PLAY

View File

@ -1,7 +1,7 @@
from bang.cards import * from bang.cards import *
import bang.roles as r import bang.roles as r
import bang.players as pl import bang.players as pl
from globals import G from globals import G, PendingAction
class ShopCardKind(IntEnum): class ShopCardKind(IntEnum):
BROWN = 0 # Se lequipaggiamento ha il bordo marrone, applicane subito leffetto e poi scartalo. BROWN = 0 # Se lequipaggiamento ha il bordo marrone, applicane subito leffetto e poi scartalo.
@ -47,7 +47,7 @@ class Bicchierino(ShopCard):
'is_player': True 'is_player': True
} for p in player.game.get_alive_players()] } for p in player.game.get_alive_players()]
player.choose_text = 'choose_bicchierino' player.choose_text = 'choose_bicchierino'
player.pending_action = pl.PendingAction.CHOOSE player.pending_action = PendingAction.CHOOSE
player.notify_self() player.notify_self()
return super().play_card(player, against, _with) return super().play_card(player, against, _with)
@ -64,7 +64,7 @@ class Bottiglia(ShopCard):
for i in range(len(player.available_cards)): for i in range(len(player.available_cards)):
player.available_cards[i].must_be_used = True player.available_cards[i].must_be_used = True
player.choose_text = 'choose_bottiglia' player.choose_text = 'choose_bottiglia'
player.pending_action = pl.PendingAction.CHOOSE player.pending_action = PendingAction.CHOOSE
player.notify_self() player.notify_self()
return super().play_card(player, against, _with) return super().play_card(player, against, _with)
@ -79,7 +79,7 @@ class Complice(ShopCard):
for i in range(len(player.available_cards)): for i in range(len(player.available_cards)):
player.available_cards[i].must_be_used = True player.available_cards[i].must_be_used = True
player.choose_text = 'choose_complice' player.choose_text = 'choose_complice'
player.pending_action = pl.PendingAction.CHOOSE player.pending_action = PendingAction.CHOOSE
player.notify_self() player.notify_self()
return super().play_card(player, against, _with) return super().play_card(player, against, _with)
@ -175,7 +175,7 @@ class Ricercato(ShopCard):
} for p in player.game.get_alive_players() if p != player and not isinstance(p.role, r.Sheriff)] } for p in player.game.get_alive_players() if p != player and not isinstance(p.role, r.Sheriff)]
player.available_cards.append({'name': player.name, 'number':0,'icon': 'you', 'is_character': True}) player.available_cards.append({'name': player.name, 'number':0,'icon': 'you', 'is_character': True})
player.choose_text = 'choose_ricercato' player.choose_text = 'choose_ricercato'
player.pending_action = pl.PendingAction.CHOOSE player.pending_action = PendingAction.CHOOSE
player.notify_self() player.notify_self()
return True return True
# la giochi su un altro giocatore, ricompensa di 2 carte e 1 pepita a chi lo uccide # la giochi su un altro giocatore, ricompensa di 2 carte e 1 pepita a chi lo uccide

View File

@ -3,7 +3,7 @@ import bang.roles as r
import bang.players as pl import bang.players as pl
from bang.cards import Card, Suit, Bang, Mancato from bang.cards import Card, Suit, Bang, Mancato
import bang.expansions.fistful_of_cards.card_events as ce import bang.expansions.fistful_of_cards.card_events as ce
from globals import G from globals import G, PendingAction
class Fantasma(Card): class Fantasma(Card):
@ -15,7 +15,7 @@ class Fantasma(Card):
if (player.game.check_event(ce.IlGiudice)) or not self.can_be_used_now: if (player.game.check_event(ce.IlGiudice)) or not self.can_be_used_now:
return False return False
if len(player.game.get_dead_players(include_ghosts=False)) > 0: if len(player.game.get_dead_players(include_ghosts=False)) > 0:
player.pending_action = pl.PendingAction.CHOOSE player.pending_action = PendingAction.CHOOSE
player.choose_text = "choose_fantasma" player.choose_text = "choose_fantasma"
player.available_cards = [ player.available_cards = [
{ {
@ -60,11 +60,7 @@ class Lemat(Card):
for p in player.game.get_visible_players(player) for p in player.game.get_visible_players(player)
) )
): ):
from bang.players import PendingAction player.set_choose_action("choose_play_as_bang", player.hand.copy())
player.available_cards = player.hand.copy()
player.pending_action = PendingAction.CHOOSE
player.choose_text = "choose_play_as_bang"
player.notify_self() player.notify_self()
return False return False
@ -185,7 +181,7 @@ class Sventagliata(
if p["name"] != player.name and p["name"] != t.name and p["dist"] if p["name"] != player.name and p["name"] != t.name and p["dist"]
] ]
if len(player.available_cards) > 0: if len(player.available_cards) > 0:
player.pending_action = pl.PendingAction.CHOOSE player.pending_action = PendingAction.CHOOSE
player.choose_text = "choose_sventagliata" player.choose_text = "choose_sventagliata"
else: else:
player.available_cards = [] player.available_cards = []

View File

@ -21,12 +21,11 @@ class BlackFlower(Character):
for p in player.game.get_visible_players(player) for p in player.game.get_visible_players(player)
) )
) and super().special(player, data): ) and super().special(player, data):
from bang.players import PendingAction
player.available_cards = [c for c in player.hand if c.suit == cs.Suit.CLUBS]
player.special_use_count += 1 player.special_use_count += 1
player.pending_action = PendingAction.CHOOSE player.set_choose_action(
player.choose_text = "choose_play_as_bang" "choose_play_as_bang",
[c for c in player.hand if c.suit == cs.Suit.CLUBS],
)
player.notify_self() player.notify_self()

View File

@ -0,0 +1,315 @@
from typing import TYPE_CHECKING
import bang.cards as cs
from globals import PendingAction
if TYPE_CHECKING:
from bang.players import Player
class StationCard:
def __init__(self, name: str):
self.name = name
self.expansion = "train_robbery"
self.price: list[dict] = []
self.attached_train = None
def discard_and_buy_train(self, player: "Player", card_index: int):
"""Discard the card and buy the train"""
if self.attached_train is None:
return
card = player.available_cards.pop(card_index)
for i, card in enumerate(player.hand):
if card == self:
player.hand.pop(i)
break
else:
player.lives -= 1
card = player.hand.pop(card_index)
player.game.deck.scrap(card, True, player=player)
player.equipment.append(self.attached_train)
self.attached_train = None
player.pending_action = PendingAction.PLAY
def check_price(self, player: "Player") -> bool:
"""Check if the card can be used to rob the train"""
return len(player.hand) > 0
class BoomTown(StationCard):
"""Discard a Bang! to rob the train"""
def __init__(self):
super().__init__("Boom Town")
self.price = [cs.Bang(0, 0).__dict__]
def check_price(self, player: "Player"):
if super().check_price(player) and all(
not isinstance(c, cs.Bang) for c in player.hand
):
return False
player.set_choose_action(
"choose_buy_train",
[c for c in player.hand if isinstance(c, cs.Bang)],
self.discard_and_buy_train,
)
return True
class Caticor(StationCard):
"""Discard a Cat Balou or Panico to rob the train"""
def __init__(self):
super().__init__("Caticor")
self.price = [cs.CatBalou(0, 0).__dict__, cs.Panico(0, 0).__dict__]
def check_price(self, player: "Player"):
if super().check_price(player) and all(
not (isinstance(card, cs.CatBalou) or isinstance(card, cs.Panico))
for card in player.hand
):
return False
player.set_choose_action(
"choose_buy_train",
[
c
for c in player.hand
if isinstance(c, cs.CatBalou) or isinstance(c, cs.Panico)
],
self.discard_and_buy_train,
)
return True
class CreepyCreek(StationCard):
"""Discard a card of spades to rob the train"""
def __init__(self):
super().__init__("Creepy Creek")
self.price = [{"icon": "♠️"}]
def check_price(self, player: "Player"):
if super().check_price(player) and all(
card.suit != cs.Suit.SPADES for card in player.hand
):
return False
player.set_choose_action(
"choose_buy_train",
[c for c in player.hand if c.suit == cs.Suit.SPADES],
self.discard_and_buy_train,
)
return True
class CrownsHole(StationCard):
"""Discard a beer to rob the train"""
def __init__(self):
super().__init__("Crowns Hole")
self.price = [cs.Birra(0, 0).__dict__]
def check_price(self, player: "Player"):
if super().check_price(player) and all(
not isinstance(card, cs.Birra) for card in player.hand
):
return False
player.set_choose_action(
"choose_buy_train",
[c for c in player.hand if isinstance(c, cs.Birra)],
self.discard_and_buy_train,
)
return True
class Deadwood(StationCard):
"""Discard an equipment card to rob the train"""
def __init__(self):
super().__init__("Deadwood")
self.price = [{"is_equipment": True}]
def check_price(self, player: "Player"):
if super().check_price(player) and all(
not card.is_equipment for card in player.hand
):
return False
player.set_choose_action(
"choose_buy_train",
[c for c in player.hand if c.is_equipment],
self.discard_and_buy_train,
)
return True
class Dodgeville(StationCard):
"""Discard a Missed! to rob the train"""
def __init__(self):
super().__init__("Dodgeville")
self.price = [cs.Mancato(0, 0).__dict__]
def check_price(self, player: "Player"):
if super().check_price(player) and all(
not isinstance(card, cs.Mancato) for card in player.hand
):
return False
player.set_choose_action(
"choose_buy_train",
[c for c in player.hand if isinstance(c, cs.Mancato)],
self.discard_and_buy_train,
)
return True
class FortWorth(StationCard):
"""Discard a card with number 10, J, Q, K, A to rob the train"""
def __init__(self):
super().__init__("Fort Worth")
self.price = [{"icon": "10\nJ\nQ\nK\nA"}]
def check_price(self, player: "Player"):
if super().check_price(player) and all(
card.number not in {1, 10, 11, 12, 13} for card in player.hand
):
return False
player.set_choose_action(
"choose_buy_train",
[c for c in player.hand if c.number in {1, 10, 11, 12, 13}],
self.discard_and_buy_train,
)
return True
class Frisco(StationCard):
"""Discard a card of clubs to rob the train"""
def __init__(self):
super().__init__("Frisco")
self.price = [{"icon": "♣️"}]
def check_price(self, player: "Player"):
if super().check_price(player) and all(
card.suit != cs.Suit.CLUBS for card in player.hand
):
return False
player.set_choose_action(
"choose_buy_train",
[c for c in player.hand if c.suit == cs.Suit.CLUBS],
self.discard_and_buy_train,
)
return True
class MinersOath(StationCard):
"""Discard a card of diamonds to rob the train"""
def __init__(self):
super().__init__("Miners Oath")
self.price = [{"icon": "♦️"}]
def check_price(self, player: "Player"):
if super().check_price(player) and all(
card.suit != cs.Suit.DIAMONDS for card in player.hand
):
return False
player.set_choose_action(
"choose_buy_train",
[c for c in player.hand if c.suit == cs.Suit.DIAMONDS],
self.discard_and_buy_train,
)
return True
class SanTafe(StationCard):
"""Discard a card of hearts to rob the train"""
def __init__(self):
super().__init__("San Tafe")
self.price = [{"icon": "♥️"}]
def check_price(self, player: "Player"):
if super().check_price(player) and all(
card.suit != cs.Suit.HEARTS for card in player.hand
):
return False
player.set_choose_action(
"choose_buy_train",
[c for c in player.hand if c.suit == cs.Suit.HEARTS],
self.discard_and_buy_train,
)
return True
class Tombrock(StationCard):
"""Lose 1 life point to rob the train"""
def __init__(self):
super().__init__("Tombrock")
self.price = [{"icon": "💔"}]
def check_price(self, player: "Player"):
if player.lives <= 1:
return False
player.set_choose_action(
"choose_buy_train",
[{"icon": "💔"}],
self.discard_and_buy_train,
)
return True
class Yooma(StationCard):
"""Discard a card with number between 2 and 9 to rob the train"""
def __init__(self):
super().__init__("Yooma")
self.price = [{"icon": "2-9"}]
def check_price(self, player: "Player"):
if super().check_price(player) and all(
not (2 <= card.number <= 9) for card in player.hand
):
return False
player.set_choose_action(
"choose_buy_train",
[c for c in player.hand if 2 <= c.number <= 9],
self.discard_and_buy_train,
)
return True
class VirginiaTown(StationCard):
"""Discard two cards to rob the train"""
def __init__(self):
super().__init__("Virginia Town")
self.price = [{}, {}]
def check_price(self, player: "Player"):
if super().check_price(player) and len(player.hand) < 2:
return False
player.set_choose_action(
"choose_buy_train",
player.hand.copy(),
self.discard_and_buy_train,
)
return True
def get_all_stations():
"""Return a list of all the station cards"""
return [
BoomTown(),
Caticor(),
CreepyCreek(),
CrownsHole(),
Deadwood(),
Dodgeville(),
FortWorth(),
Frisco(),
MinersOath(),
SanTafe(),
Tombrock(),
Yooma(),
VirginiaTown(),
]

View File

@ -0,0 +1,406 @@
import random
from bang.cards import Card, Bang, Panico, CatBalou, Mancato
from typing import TYPE_CHECKING
from globals import G, PendingAction
if TYPE_CHECKING:
from bang.players import Player
class TrainCard(Card):
def __init__(self, name: str, is_locomotive: bool = False):
super().__init__(suit=5, number=0, name=name)
self.expansion_icon = "🚂"
self.is_equipment = True
self.is_locomotive = is_locomotive
self.expansion = "train_robbery"
self.type = "train"
self.implemented = True
# Circus Wagon: gli altri giocatori
# scartano una carta, in senso orario, a
# partire dal giocatore alla tua sinistra.
# Express Car: non puoi svolgere
# un altro turno extra dopo quello
# ottenuto da questo effetto, anche se
# riesci a giocare di nuovo Express Car.
# Ghost Car: giocabile su un
# qualsiasi giocatore, anche se già
# eliminato, te compreso. Non può
# essere giocato sullo Sceriffo.
# Se quel giocatore è/viene eliminato,
# invece ritorna/resta in gioco, senza
# punti vita. Non può guadagnare né
# perdere punti vita, e viene considerato
# un personaggio in gioco per tutti gli
# effetti (condizioni di vittoria, distanza
# tra giocatori, abilità dei personaggi,
# ecc.). Non avendo punti vita, deve
# scartare la sua intera mano alla fine
# del turno, ma può tenere qualsiasi
# carta in gioco di fronte a sé, incluso
# Ghost Car. Tuttavia, è eliminato
# dal gioco non appena Ghost Car
# viene scartato: nessuna ricompensa
# viene assegnata in questo caso se il
# giocatore è un Fuorilegge, e le abilità
# dei personaggi (ad es. Vulture Sam)
# non si attivano
# Lounge Car: i vagoni che peschi
# non contano per il normale limite di
# acquisizione di 1 vagone per turno. Se
# sei lo Sceriffo e peschi Ghost Car, devi
# darlo a un altro giocatore.
# Lumber Flatcar: gioca su un
# qualsiasi giocatore (compreso te).
# Finché questa carta è in gioco, quel
# giocatore vede gli altri giocatori a
# distanza aumentata di 1.
# Private Car: questo effetto non
# ti protegge da Gatling, Knife Revolver,
# labilità di Evan Babbit, e così via.
# Sleeper Car: puoi anche usare
# leffetto una volta per turno con
# Indiani!, Duello, ecc.
class Ironhorse(TrainCard):
"""LOCOMOTIVA:
Ogni giocatore, incluso colui che ha attivato l'effetto, è bersaglio di un BANG!
Nessun giocatore è responsabile dell'eventuale perdita di punti vita.
Se tutti i giocatori vengono eliminati allo stesso tempo, i Fuorilegge vincono.
"""
def __init__(self):
super().__init__("Ironhorse", is_locomotive=True)
self.icon = "🚂"
def play_card(self, player, against=None, _with=None) -> bool:
player.game.attack(player, player.name, card_name=self.name)
player.game.attack_others(player, card_name=self.name)
return True
class Leland(TrainCard):
"""
LOCOMOTIVA: svolgi l'effetto dell'Emporio, cominciando dal giocatore di turno e procedendo in senso orario.
"""
def __init__(self):
super().__init__("Leland", is_locomotive=True)
self.icon = "🚂"
def play_card(self, player, against=None, _with=None) -> bool:
player.game.emporio(player)
return True
class BaggageCar(TrainCard):
"""Scartalo: ottieni l'effetto di un Mancato!, Panico!, Cat Balou o di un BANG! extra.
Discard this for a Missed! Panic!, Cat Balou, or an extra BANG!"""
def __init__(self):
super().__init__("Baggage Car")
self.icon = "🚋🛄"
def choose_callback(self, player: 'Player', card_index):
player.hand.append(player.available_cards[card_index])
player.pending_action = PendingAction.PLAY
def play_card(self, player, against=None, _with=None) -> bool:
player.set_choose_action(
"choose_baggage_car",
[Bang(4, 42), Mancato(4, 42), CatBalou(4, 42), Panico(4, 42)],
self.choose_callback,
)
return True
class Caboose(TrainCard):
"""Puoi scartare un altra tua carta bordo blu incuso un vagone come se fosse un Mancato!"""
def __init__(self):
super().__init__("Caboose")
self.icon = "🚋"
def play_card(self, player, against=None, _with=None) -> bool:
return False
class CattleTruck(TrainCard):
"""Scartalo: guarda le 3 carte in cima agli scarti e pescane I"""
def __init__(self):
super().__init__("Cattle Truck")
self.icon = "🚋🐄"
def choose_card_callback(self, player: 'Player', card_index):
chosen_card = player.available_cards.pop(card_index)
player.game.deck.scrap_pile.pop(-card_index)
player.hand.append(chosen_card)
player.pending_action = PendingAction.PLAY
player.notify_self()
def play_card(self, player, against=None, _with=None) -> bool:
drawn_cards = player.game.deck.peek_scrap_pile(n_cards=3)
player.set_choose_action(
"choose_cattle_truck",
drawn_cards,
self.choose_card_callback,
)
return True
class CircusWagon(TrainCard):
"""Scartalo: ogni altro giocatore deve scartare una carta che ha in gioco."""
def __init__(self):
super().__init__("Circus Wagon", is_locomotive=True)
self.icon = "🚋🎪"
def play_card(self, player, against=None, _with=None) -> bool:
player.game.discard_others(player, card_name=self.name)
return True
@classmethod
def choose_circus_wagon(cls, player: 'Player', card_index):
player.game.deck.scrap(player.hand.pop(card_index), player=player)
player.pending_action = PendingAction.WAIT
player.game.responders_did_respond_resume_turn()
player.notify_self()
class CoalHopper(TrainCard):
"""Scartalo: pesca una carta e scarta un vagone in gioco davanti a un giocatore a tua scelta."""
def __init__(self):
super().__init__("Coal Hopper")
self.icon = "🚋🔥"
self.need_target = True
def play_card(self, player, against=None, _with=None) -> bool:
if against is not None and len(player.game.get_player_named(against).equipment) > 0:
player.game.steal_discard(player, against, self)
return True
class DiningCar(TrainCard):
"""A inizio turno, "estrai!": se è Cuori, recuperi I punto vita."""
def __init__(self):
super().__init__("Dining Car")
self.icon = "🚋🍽"
def play_card(self, player, against=None, _with=None) -> bool:
return False
class ExpressCar(TrainCard):
"""Scarta tutte le carte in mano, poi gioca un altro turno"""
def __init__(self):
super().__init__("Express Car")
self.icon = "🚋⚡"
def play_card(self, player, against=None, _with=None) -> bool:
while len(player.hand) > 0:
player.game.deck.scrap(player.hand.pop(0), player=player)
player.notify_self()
player.play_turn()
return True
class GhostCar(TrainCard):
"""Giocalo su chiunque tranne lo Sceritfo. Se vieni eliminato, invece resta in gioco, ma non puo guadagnare ne perdere punti vita."""
def __init__(self):
super().__init__("Ghost Car")
self.icon = "🚋👻"
self.implemented = False
def play_card(self, player, against=None, _with=None) -> bool:
return False
class LoungeCar(TrainCard):
"""Scartalo: pesca 2 vagoni dal mazzo, mettine I in gioco di fronte a te e 1 di fronte a un altro giocatore."""
def __init__(self):
super().__init__("Lounge Car")
self.icon = "🚋🛋"
self.implemented = False
def play_card(self, player, against=None, _with=None) -> bool:
return True
class LumberFlatcar(TrainCard):
"""Giocalo su un qualsiasi giocatore (compreso te). Finché questa carta è in gioco, quel giocatore vede gli altri giocatori a distanza aumentata di 1."""
def __init__(self):
super().__init__("Lumber Flatcar")
self.icon = "🚋🪵"
self.sight_mod = -1
def play_card(self, player, against=None, _with=None) -> bool:
return False
class MailCar(TrainCard):
"""Scartalo: pesca 3 carte e dai 1 di esse a un altro giocatore a tua scelta."""
def __init__(self):
super().__init__("Mail Car")
self.icon = "🚋📮"
def choose_card_callback(self, player: 'Player', card_index):
chosen_card = player.available_cards.pop(card_index)
player.hand.extend(player.available_cards)
player.set_choose_action(
"choose_other_player",
player.game.get_other_players(player),
lambda p, other_player_index: self.choose_player_callback(p, other_player_index, chosen_card)
)
def choose_player_callback(self, player: 'Player', other_player_index, chosen_card):
pl_name = player.game.get_other_players(player)[other_player_index]["name"]
other_player = player.game.get_player_named(pl_name)
other_player.hand.append(chosen_card)
G.sio.emit(
"card_drawn",
room=player.game.name,
data={"player": pl_name, "pile": player.name},
)
other_player.notify_self()
player.pending_action = PendingAction.PLAY
def play_card(self, player, against=None, _with=None) -> bool:
drawn_cards = [player.game.deck.draw(player=player) for _ in range(3)]
player.set_choose_action(
"choose_mail_car",
drawn_cards,
self.choose_card_callback,
)
return True
class ObservationCar(TrainCard):
"""Tu vedi gli altri a distanza -1. Gli altri a vedono a distanza +1."""
def __init__(self):
super().__init__("Observation Car")
self.icon = "🚋👀"
self.sight_mod = 1
self.vis_mod = 1
def play_card(self, player, against=None, _with=None) -> bool:
return False
class PassengerCar(TrainCard):
"""Scartalo: pesca una carta (o in mano o in gioco) da un altro giocatore"""
def __init__(self):
super().__init__("Passenger Car")
self.icon = "🚋🚶"
self.range = 99
self.need_target = True
def play_card(self, player, against=None, _with=None) -> bool:
if (
against is not None
and (len(player.equipment) > 0 or len(player.equipment) > 0)
):
player.game.steal_discard(player, against, self)
return True
return False
class PrisonerCar(TrainCard):
"""Le carte Duello e Indiani! giocate dagli altri giocatori non hanno effetto su di te."""
def __init__(self):
super().__init__("Prisoner Car")
self.icon = "🚋👮🏻‍♂️"
def play_card(self, player, against=None, _with=None) -> bool:
return False
class PrivateCar(TrainCard):
"""Se non hai carte in mano, non puoi essere bersaglio di carte BANG"""
def __init__(self):
super().__init__("Private Car")
self.icon = "🚋💁🏻"
def play_card(self, player, against=None, _with=None) -> bool:
return False
class SleeperCar(TrainCard):
"""Una volta per turno, puoi scartare un'altra tua carta a bordo blu incluso."""
def __init__(self):
super().__init__("Sleeper Car")
self.icon = "🚋🛌"
def choose_card_callback(self, player: 'Player', card_index):
player.game.deck.scrap(player.equipment.pop(card_index), player=player)
player.pending_action = PendingAction.PLAY
self.usable_next_turn = True
self.can_be_used_now = False
player.notify_self()
def play_card(self, player, against=None, _with=None) -> bool:
if not self.can_be_used_now:
return False
player.set_choose_action(
"choose_sleeper_car",
player.equipment,
self.choose_card_callback,
)
return False
def get_all_cards(rng=random):
"""Return a list of all train cards in the expansion"""
cars = [
BaggageCar(),
Caboose(),
CattleTruck(),
CircusWagon(),
CoalHopper(),
DiningCar(),
ExpressCar(),
GhostCar(),
LoungeCar(),
LumberFlatcar(),
MailCar(),
ObservationCar(),
PassengerCar(),
PrisonerCar(),
PrivateCar(),
SleeperCar(),
]
cars = [c for c in cars if c.implemented]
rng.shuffle(cars)
return cars
def get_locomotives(rng=random):
"""Return a list of all locomotive cards in the expansion"""
locs = [
Ironhorse(),
Leland(),
]
rng.shuffle(locs)
return locs

View File

@ -17,8 +17,9 @@ import bang.expansions.wild_west_show.card_events as cew
import bang.expansions.gold_rush.shop_cards as grc import bang.expansions.gold_rush.shop_cards as grc
import bang.expansions.gold_rush.characters as grch import bang.expansions.gold_rush.characters as grch
import bang.expansions.the_valley_of_shadows.cards as tvosc import bang.expansions.the_valley_of_shadows.cards as tvosc
import bang.expansions.train_robbery.trains as trt
from metrics import Metrics from metrics import Metrics
from globals import G from globals import G, PendingAction
debug_commands = [ debug_commands = [
@ -57,8 +58,11 @@ debug_commands = [
"help": "Remove a card from hand/equip - sample /removecard 0", "help": "Remove a card from hand/equip - sample /removecard 0",
}, },
{"cmd": "/getcard", "help": "Get a brand new card - sample /getcard Birra"}, {"cmd": "/getcard", "help": "Get a brand new card - sample /getcard Birra"},
{"cmd": "/equipcard", "help": "Equip a brand new card - sample /getcard Barile"},
{"cmd": "/meinfo", "help": "Get player data"}, {"cmd": "/meinfo", "help": "Get player data"},
{"cmd": "/gameinfo", "help": "Get game data"}, {"cmd": "/gameinfo", "help": "Get game data"},
{"cmd": "/deckinfo", "help": "Get deck data"},
{"cmd": "/trainfw", "help": "move train forward"},
{"cmd": "/playerinfo", "help": "Get player data - sample /playerinfo player"}, {"cmd": "/playerinfo", "help": "Get player data - sample /playerinfo player"},
{"cmd": "/cardinfo", "help": "Get card data - sample /cardinfo handindex"}, {"cmd": "/cardinfo", "help": "Get card data - sample /cardinfo handindex"},
{"cmd": "/mebot", "help": "Toggles bot mode"}, {"cmd": "/mebot", "help": "Toggles bot mode"},
@ -93,6 +97,7 @@ class Game:
"gold_rush", "gold_rush",
"the_valley_of_shadows", "the_valley_of_shadows",
"wild_west_show", "wild_west_show",
"train_robbery",
] ]
self.shutting_down = False self.shutting_down = False
self.is_competitive = False self.is_competitive = False
@ -354,6 +359,7 @@ class Game:
roles_str += f"|{role}|{str(current_roles.count(role))}" roles_str += f"|{role}|{str(current_roles.count(role))}"
G.sio.emit("chat_message", room=self.name, data=f"_allroles{roles_str}") G.sio.emit("chat_message", room=self.name, data=f"_allroles{roles_str}")
self.play_turn() self.play_turn()
self.notify_stations()
def choose_characters(self): def choose_characters(self):
n = self.characters_to_distribute n = self.characters_to_distribute
@ -459,7 +465,7 @@ class Game:
def discard_others(self, attacker: pl.Player, card_name: str = None): def discard_others(self, attacker: pl.Player, card_name: str = None):
self.attack_in_progress = True self.attack_in_progress = True
attacker.pending_action = pl.PendingAction.WAIT attacker.pending_action = PendingAction.WAIT
attacker.notify_self() attacker.notify_self()
self.waiting_for = 0 self.waiting_for = 0
self.ready_count = 0 self.ready_count = 0
@ -469,7 +475,7 @@ class Game:
self.waiting_for += 1 self.waiting_for += 1
p.notify_self() p.notify_self()
if self.waiting_for == 0: if self.waiting_for == 0:
attacker.pending_action = pl.PendingAction.PLAY attacker.pending_action = PendingAction.PLAY
attacker.notify_self() attacker.notify_self()
self.attack_in_progress = False self.attack_in_progress = False
elif card_name == "Poker": elif card_name == "Poker":
@ -477,7 +483,7 @@ class Game:
def attack_others(self, attacker: pl.Player, card_name: str = None): def attack_others(self, attacker: pl.Player, card_name: str = None):
self.attack_in_progress = True self.attack_in_progress = True
attacker.pending_action = pl.PendingAction.WAIT attacker.pending_action = PendingAction.WAIT
attacker.notify_self() attacker.notify_self()
self.waiting_for = 0 self.waiting_for = 0
self.ready_count = 0 self.ready_count = 0
@ -487,7 +493,7 @@ class Game:
self.waiting_for += 1 self.waiting_for += 1
p.notify_self() p.notify_self()
if self.waiting_for == 0: if self.waiting_for == 0:
attacker.pending_action = pl.PendingAction.PLAY attacker.pending_action = PendingAction.PLAY
attacker.notify_self() attacker.notify_self()
self.attack_in_progress = False self.attack_in_progress = False
if self.pending_winners and not self.someone_won: if self.pending_winners and not self.someone_won:
@ -495,7 +501,7 @@ class Game:
def indian_others(self, attacker: pl.Player): def indian_others(self, attacker: pl.Player):
self.attack_in_progress = True self.attack_in_progress = True
attacker.pending_action = pl.PendingAction.WAIT attacker.pending_action = PendingAction.WAIT
attacker.notify_self() attacker.notify_self()
self.waiting_for = 0 self.waiting_for = 0
self.ready_count = 0 self.ready_count = 0
@ -505,7 +511,7 @@ class Game:
self.waiting_for += 1 self.waiting_for += 1
p.notify_self() p.notify_self()
if self.waiting_for == 0: if self.waiting_for == 0:
attacker.pending_action = pl.PendingAction.PLAY attacker.pending_action = PendingAction.PLAY
attacker.notify_self() attacker.notify_self()
self.attack_in_progress = False self.attack_in_progress = False
if self.pending_winners and not self.someone_won: if self.pending_winners and not self.someone_won:
@ -542,26 +548,26 @@ class Game:
self.attack_in_progress = True self.attack_in_progress = True
self.ready_count = 0 self.ready_count = 0
self.waiting_for = 1 self.waiting_for = 1
attacker.pending_action = pl.PendingAction.WAIT attacker.pending_action = PendingAction.WAIT
attacker.notify_self() attacker.notify_self()
self.get_player_named(target_username).notify_self() self.get_player_named(target_username).notify_self()
elif not attacker.is_my_turn or len(self.attack_queue) == 0: elif not attacker.is_my_turn or len(self.attack_queue) == 0:
self.players[self.turn].pending_action = pl.PendingAction.PLAY self.players[self.turn].pending_action = PendingAction.PLAY
def steal_discard(self, attacker: pl.Player, target_username: str, card: cs.Card): def steal_discard(self, attacker: pl.Player, target_username: str, card: cs.Card):
p = self.get_player_named(target_username) p = self.get_player_named(target_username)
if p != attacker and p.get_discarded( if p != attacker and p.get_discarded(
attacker, attacker,
card_name=card.name, card_name=card.name,
action="steal" if isinstance(card, cs.Panico) else "discard", action="steal" if (isinstance(card, cs.Panico) or isinstance(card, trt.PassengerCar)) else "discard",
): ):
self.ready_count = 0 self.ready_count = 0
self.waiting_for = 1 self.waiting_for = 1
attacker.pending_action = pl.PendingAction.WAIT attacker.pending_action = PendingAction.WAIT
attacker.notify_self() attacker.notify_self()
self.get_player_named(target_username).notify_self() self.get_player_named(target_username).notify_self()
else: else:
attacker.pending_action = pl.PendingAction.CHOOSE attacker.pending_action = PendingAction.CHOOSE
attacker.target_p = target_username attacker.target_p = target_username
if isinstance(card, cs.CatBalou): if isinstance(card, cs.CatBalou):
attacker.choose_action = "discard" attacker.choose_action = "discard"
@ -575,7 +581,7 @@ class Game:
): ):
self.ready_count = 0 self.ready_count = 0
self.waiting_for = 1 self.waiting_for = 1
attacker.pending_action = pl.PendingAction.WAIT attacker.pending_action = PendingAction.WAIT
attacker.notify_self() attacker.notify_self()
self.get_player_named(target_username).notify_self() self.get_player_named(target_username).notify_self()
@ -583,14 +589,14 @@ class Game:
if self.get_player_named(target_username).get_dueled(attacker=attacker): if self.get_player_named(target_username).get_dueled(attacker=attacker):
self.ready_count = 0 self.ready_count = 0
self.waiting_for = 1 self.waiting_for = 1
attacker.pending_action = pl.PendingAction.WAIT attacker.pending_action = PendingAction.WAIT
attacker.notify_self() attacker.notify_self()
self.get_player_named(target_username).notify_self() self.get_player_named(target_username).notify_self()
def emporio(self): def emporio(self):
pls = self.get_alive_players() pls = self.get_alive_players()
self.available_cards = [self.deck.draw(True) for i in range(len(pls))] self.available_cards = [self.deck.draw(True) for i in range(len(pls))]
self.players[self.turn].pending_action = pl.PendingAction.CHOOSE self.players[self.turn].pending_action = PendingAction.CHOOSE
self.players[self.turn].choose_text = "choose_card_to_get" self.players[self.turn].choose_text = "choose_card_to_get"
self.players[self.turn].available_cards = self.available_cards self.players[self.turn].available_cards = self.available_cards
G.sio.emit( G.sio.emit(
@ -612,7 +618,7 @@ class Game:
) )
player.hand.append(card) player.hand.append(card)
player.available_cards = [] player.available_cards = []
player.pending_action = pl.PendingAction.WAIT player.pending_action = PendingAction.WAIT
player.notify_self() player.notify_self()
pls = self.get_alive_players() pls = self.get_alive_players()
next_player = pls[ next_player = pls[
@ -631,14 +637,14 @@ class Game:
next_player.hand.append(self.available_cards.pop()) next_player.hand.append(self.available_cards.pop())
next_player.notify_self() next_player.notify_self()
G.sio.emit("emporio", room=self.name, data='{"name":"","cards":[]}') G.sio.emit("emporio", room=self.name, data='{"name":"","cards":[]}')
self.players[self.turn].pending_action = pl.PendingAction.PLAY self.players[self.turn].pending_action = PendingAction.PLAY
self.players[self.turn].notify_self() self.players[self.turn].notify_self()
elif next_player == self.players[self.turn]: elif next_player == self.players[self.turn]:
G.sio.emit("emporio", room=self.name, data='{"name":"","cards":[]}') G.sio.emit("emporio", room=self.name, data='{"name":"","cards":[]}')
self.players[self.turn].pending_action = pl.PendingAction.PLAY self.players[self.turn].pending_action = PendingAction.PLAY
self.players[self.turn].notify_self() self.players[self.turn].notify_self()
else: else:
next_player.pending_action = pl.PendingAction.CHOOSE next_player.pending_action = PendingAction.CHOOSE
next_player.choose_text = "choose_card_to_get" next_player.choose_text = "choose_card_to_get"
next_player.available_cards = self.available_cards next_player.available_cards = self.available_cards
G.sio.emit( G.sio.emit(
@ -724,7 +730,7 @@ class Game:
elif self.poker_on and not any( elif self.poker_on and not any(
c.number == 1 for c in self.deck.scrap_pile[-tmp:] c.number == 1 for c in self.deck.scrap_pile[-tmp:]
): ):
self.players[self.turn].pending_action = pl.PendingAction.CHOOSE self.players[self.turn].pending_action = PendingAction.CHOOSE
self.players[ self.players[
self.turn self.turn
].choose_text = f"choose_from_poker;{min(2, tmp)}" ].choose_text = f"choose_from_poker;{min(2, tmp)}"
@ -735,10 +741,10 @@ class Game:
print("attack completed, next attack") print("attack completed, next attack")
atk = self.attack_queue.pop(0) atk = self.attack_queue.pop(0)
self.attack(atk[0], atk[1], atk[2], atk[3], skip_queue=True) self.attack(atk[0], atk[1], atk[2], atk[3], skip_queue=True)
elif self.players[self.turn].pending_action == pl.PendingAction.CHOOSE: elif self.players[self.turn].pending_action == PendingAction.CHOOSE:
self.players[self.turn].notify_self() self.players[self.turn].notify_self()
else: else:
self.players[self.turn].pending_action = pl.PendingAction.PLAY self.players[self.turn].pending_action = PendingAction.PLAY
self.poker_on = False self.poker_on = False
self.players[self.turn].notify_self() self.players[self.turn].notify_self()
@ -842,6 +848,7 @@ class Game:
) )
): ):
self.deck.flip_event() self.deck.flip_event()
self.deck.move_train_forward()
if self.check_event(ce.RouletteRussa): if self.check_event(ce.RouletteRussa):
self.is_russian_roulette_on = True self.is_russian_roulette_on = True
if self.players[self.turn].get_banged(self.deck.event_cards[0]): if self.players[self.turn].get_banged(self.deck.event_cards[0]):
@ -917,11 +924,26 @@ class Game:
data=json.dumps(self.deck.shop_cards, default=lambda o: o.__dict__), data=json.dumps(self.deck.shop_cards, default=lambda o: o.__dict__),
) )
def notify_stations(self, sid=None):
if "train_robbery" in self.expansions:
room = self.name if sid is None else sid
G.sio.emit(
"stations",
room=room,
data=json.dumps(
{
"stations": self.deck.stations,
"current_train": self.deck.current_train,
},
default=lambda o: o.__dict__,
),
)
def notify_scrap_pile(self, sid=None): def notify_scrap_pile(self, sid=None):
print(f"{self.name}: scrap") print(f"{self.name}: scrap")
room = self.name if sid is None else sid room = self.name if sid is None else sid
if self.deck.peek_scrap_pile(): if self.deck.peek_scrap_pile():
G.sio.emit("scrap", room=room, data=self.deck.peek_scrap_pile().__dict__) G.sio.emit("scrap", room=room, data=self.deck.peek_scrap_pile()[0].__dict__)
else: else:
G.sio.emit("scrap", room=room, data=None) G.sio.emit("scrap", room=room, data=None)
@ -1015,9 +1037,9 @@ class Game:
self.deck.draw(True, player=player.attacker) self.deck.draw(True, player=player.attacker)
player.attacker.notify_self() player.attacker.notify_self()
print(f"{self.name}: player {player.name} died") print(f"{self.name}: player {player.name} died")
if self.waiting_for > 0 and player.pending_action == pl.PendingAction.RESPOND: if self.waiting_for > 0 and player.pending_action == PendingAction.RESPOND:
self.responders_did_respond_resume_turn() self.responders_did_respond_resume_turn()
player.pending_action = pl.PendingAction.WAIT player.pending_action = PendingAction.WAIT
if player.is_dead: if player.is_dead:
return return
@ -1220,6 +1242,29 @@ class Game:
def get_alive_players(self): def get_alive_players(self):
return [p for p in self.players if not p.is_dead or p.is_ghost] return [p for p in self.players if not p.is_dead or p.is_ghost]
def get_other_players(self, player:pl.Player):
return [{
"name": p.name,
"dist": 0,
"lives": p.lives,
"max_lives": p.max_lives,
"is_sheriff": isinstance(p.role, roles.Sheriff),
"cards": len(p.hand) + len(p.equipment),
"is_ghost": p.is_ghost,
"is_bot": p.is_bot,
"icon": p.role.icon
if (
p.role is not None
and (
self.initial_players == 3
or isinstance(p.role, roles.Sheriff)
)
)
else "🤠",
"avatar": p.avatar,
"role": p.role,
} for p in self.get_alive_players() if p != player]
def get_dead_players(self, include_ghosts=True): def get_dead_players(self, include_ghosts=True):
return [ return [
p for p in self.players if p.is_dead and (include_ghosts or not p.is_ghost) p for p in self.players if p.is_dead and (include_ghosts or not p.is_ghost)

View File

@ -1,9 +1,6 @@
from __future__ import annotations from __future__ import annotations
from enum import IntEnum
import json import json
from random import random, randrange, sample, uniform, randint from random import randrange, sample, uniform, randint
import socketio
import bang.deck as deck
import bang.roles as r import bang.roles as r
import bang.cards as cs import bang.cards as cs
import bang.expansions.dodge_city.cards as csd import bang.expansions.dodge_city.cards as csd
@ -17,9 +14,11 @@ import bang.expansions.gold_rush.shop_cards as grc
import bang.expansions.gold_rush.characters as grch import bang.expansions.gold_rush.characters as grch
import bang.expansions.the_valley_of_shadows.cards as tvosc import bang.expansions.the_valley_of_shadows.cards as tvosc
import bang.expansions.the_valley_of_shadows.characters as tvosch import bang.expansions.the_valley_of_shadows.characters as tvosch
from typing import List, TYPE_CHECKING import bang.expansions.train_robbery.stations as trs
import bang.expansions.train_robbery.trains as trt
from typing import List, TYPE_CHECKING, Callable
from metrics import Metrics from metrics import Metrics
from globals import G from globals import G, PendingAction
import sys import sys
if TYPE_CHECKING: if TYPE_CHECKING:
@ -45,15 +44,6 @@ robot_pictures = [
] ]
class PendingAction(IntEnum):
PICK = 0
DRAW = 1
PLAY = 2
RESPOND = 3
WAIT = 4
CHOOSE = 5
class Player: class Player:
def is_admin(self): def is_admin(self):
return self.discord_id in {"244893980960096266", "539795574019457034"} return self.discord_id in {"244893980960096266", "539795574019457034"}
@ -102,6 +92,7 @@ class Player:
self.is_bot = bot self.is_bot = bot
self.discord_token = discord_token self.discord_token = discord_token
self.discord_id = None self.discord_id = None
self.did_choose_callback = None
self.played_cards = 0 self.played_cards = 0
self.avatar = "" self.avatar = ""
self.last_played_card: cs.Card = None self.last_played_card: cs.Card = None
@ -280,25 +271,25 @@ class Player:
if self.pending_action == PendingAction.DRAW and self.game.check_event( if self.pending_action == PendingAction.DRAW and self.game.check_event(
ce.Peyote ce.Peyote
): ):
self.available_cards = [
{"icon": "🔴", "noDesc": True},
{"icon": "", "noDesc": True},
]
self.is_drawing = True self.is_drawing = True
self.choose_text = "choose_guess" self.set_choose_action(
self.pending_action = PendingAction.CHOOSE "choose_guess",
[
{"icon": "🔴", "noDesc": True},
{"icon": "", "noDesc": True},
],
)
elif ( elif (
self.can_play_ranch self.can_play_ranch
and self.pending_action == PendingAction.PLAY and self.pending_action == PendingAction.PLAY
and self.game.check_event(ce.Ranch) and self.game.check_event(ce.Ranch)
): ):
self.can_play_ranch = False self.can_play_ranch = False
self.available_cards = [c for c in self.hand]
self.discarded_cards = [] self.discarded_cards = []
self.available_cards.append({"icon": "", "noDesc": True})
self.is_playing_ranch = True self.is_playing_ranch = True
self.choose_text = "choose_ranch" self.set_choose_action(
self.pending_action = PendingAction.CHOOSE "choose_ranch", [c for c in self.hand] + [{"icon": "", "noDesc": True}]
)
elif ( elif (
self.character self.character
and self.character.check(self.game, chars.SuzyLafayette) and self.character.check(self.game, chars.SuzyLafayette)
@ -338,9 +329,7 @@ class Player:
self.game.players[self.game.turn].notify_self() self.game.players[self.game.turn].notify_self()
self.scrapped_cards = 0 self.scrapped_cards = 0
self.previous_pending_action = self.pending_action self.previous_pending_action = self.pending_action
self.pending_action = PendingAction.CHOOSE self.set_choose_action("choose_sid_scrap", self.hand)
self.choose_text = "choose_sid_scrap"
self.available_cards = self.hand
self.lives += 1 self.lives += 1
ser = self.__dict__.copy() ser = self.__dict__.copy()
@ -748,6 +737,9 @@ class Player:
self.has_played_bang = False self.has_played_bang = False
self.special_use_count = 0 self.special_use_count = 0
self.bang_used = 0 self.bang_used = 0
if any((isinstance(c, trt.DiningCar) for c in self.equipment)):
if self.game.deck.pick_and_scrap().suit == cs.Suit.HEARTS:
self.lives = min(self.lives + 1, self.max_lives)
if self.game.check_event(cew.DarlingValentine): if self.game.check_event(cew.DarlingValentine):
hand = len(self.hand) hand = len(self.hand)
for _ in range(hand): for _ in range(hand):
@ -782,35 +774,36 @@ class Player:
for p in self.game.get_alive_players() for p in self.game.get_alive_players()
) )
): ):
self.available_cards = [ self.set_choose_action(
{ "choose_fratelli_di_sangue",
"name": p.name, [
"icon": p.role.icon {
if (self.game.initial_players == 3) "name": p.name,
else "⭐️" "icon": p.role.icon
if isinstance(p.role, r.Sheriff) if (self.game.initial_players == 3)
else "🤠", else "⭐️"
"alt_text": "".join(["❤️"] * p.lives) if isinstance(p.role, r.Sheriff)
+ "".join(["💀"] * (p.max_lives - p.lives)), else "🤠",
"avatar": p.avatar, "alt_text": "".join(["❤️"] * p.lives)
"is_character": True, + "".join(["💀"] * (p.max_lives - p.lives)),
"is_player": True, "avatar": p.avatar,
} "is_character": True,
for p in self.game.get_alive_players() "is_player": True,
if p != self and p.lives < p.max_lives }
] for p in self.game.get_alive_players()
self.available_cards.append({"icon": "", "noDesc": True}) if p != self and p.lives < p.max_lives
self.choose_text = "choose_fratelli_di_sangue" ]
self.pending_action = PendingAction.CHOOSE + [{"icon": "", "noDesc": True}],
)
self.is_giving_life = True self.is_giving_life = True
elif ( elif (
self.game.check_event(ceh.NuovaIdentita) self.game.check_event(ceh.NuovaIdentita)
and self.not_chosen_character is not None and self.not_chosen_character is not None
and not again and not again
): ):
self.available_cards = [self.character, self.not_chosen_character] self.set_choose_action(
self.choose_text = "choose_nuova_identita" "choose_nuova_identita", [self.character, self.not_chosen_character]
self.pending_action = PendingAction.CHOOSE )
elif not self.game.check_event(ce.Lazo) and any( elif not self.game.check_event(ce.Lazo) and any(
( (
isinstance(c, cs.Dinamite) isinstance(c, cs.Dinamite)
@ -840,25 +833,26 @@ class Player:
and sum((c.name == cs.Bang(0, 0).name for c in self.hand)) >= 2 and sum((c.name == cs.Bang(0, 0).name for c in self.hand)) >= 2
): ):
self.is_using_checchino = True self.is_using_checchino = True
self.available_cards = [ self.set_choose_action(
{ "choose_cecchino",
"name": p["name"], [
"icon": p["role"].icon {
if (self.game.initial_players == 3) "name": p["name"],
else "⭐️" "icon": p["role"].icon
if p["is_sheriff"] if (self.game.initial_players == 3)
else "🤠", else "⭐️"
"alt_text": "".join(["❤️"] * p["lives"]) if p["is_sheriff"]
+ "".join(["💀"] * (p["max_lives"] - p["lives"])), else "🤠",
"is_character": True, "alt_text": "".join(["❤️"] * p["lives"])
"is_player": True, + "".join(["💀"] * (p["max_lives"] - p["lives"])),
} "is_character": True,
for p in self.game.get_visible_players(self) "is_player": True,
if p["dist"] <= self.get_sight() }
] for p in self.game.get_visible_players(self)
self.available_cards.append({"icon": "", "noDesc": True}) if p["dist"] <= self.get_sight()
self.choose_text = "choose_cecchino" ]
self.pending_action = PendingAction.CHOOSE + [{"icon": "", "noDesc": True}],
)
self.notify_self() self.notify_self()
if ( if (
self.is_my_turn self.is_my_turn
@ -884,15 +878,15 @@ class Player:
self.notify_self() self.notify_self()
elif self.character.check(self.game, chars.KitCarlson) and not self.is_ghost: elif self.character.check(self.game, chars.KitCarlson) and not self.is_ghost:
self.is_drawing = True self.is_drawing = True
self.available_cards = [self.game.deck.draw() for i in range(3)] self.set_choose_action(
self.choose_text = "choose_card_to_get" "choose_card_to_get", [self.game.deck.draw() for i in range(3)]
self.pending_action = PendingAction.CHOOSE )
self.notify_self() self.notify_self()
elif self.character.check(self.game, grch.DutchWill) and not self.is_ghost: elif self.character.check(self.game, grch.DutchWill) and not self.is_ghost:
self.is_drawing = True self.is_drawing = True
self.available_cards = [self.game.deck.draw() for i in range(2)] self.set_choose_action(
self.choose_text = "choose_card_to_get" "choose_card_to_get", [self.game.deck.draw() for i in range(2)]
self.pending_action = PendingAction.CHOOSE )
self.notify_self() self.notify_self()
elif ( elif (
self.character.check(self.game, chd.PatBrennan) self.character.check(self.game, chd.PatBrennan)
@ -902,10 +896,10 @@ class Player:
and len(self.game.get_player_named(pile).equipment) > 0 and len(self.game.get_player_named(pile).equipment) > 0
): ):
self.is_drawing = True self.is_drawing = True
self.available_cards = self.game.get_player_named(pile).equipment
self.pat_target = pile self.pat_target = pile
self.choose_text = "choose_card_to_get" self.set_choose_action(
self.pending_action = PendingAction.CHOOSE "choose_card_to_get", self.game.get_player_named(pile).equipment
)
self.notify_self() self.notify_self()
else: else:
self.pending_action = PendingAction.PLAY self.pending_action = PendingAction.PLAY
@ -1004,12 +998,13 @@ class Player:
def manette(self): def manette(self):
if self.game.check_event(ceh.Manette): if self.game.check_event(ceh.Manette):
self.choose_text = "choose_manette" self.set_choose_action(
self.available_cards = [ "choose_manette",
{"name": "", "icon": "♦♣♥♠"[s], "alt_text": "", "noDesc": True} [
for s in [0, 1, 2, 3] {"name": "", "icon": "♦♣♥♠"[s], "alt_text": "", "noDesc": True}
] for s in [0, 1, 2, 3]
self.pending_action = PendingAction.CHOOSE ],
)
def pick(self): def pick(self):
if self.pending_action != PendingAction.PICK: if self.pending_action != PendingAction.PICK:
@ -1399,6 +1394,9 @@ class Player:
self.target_p = self.rissa_targets.pop(0).name self.target_p = self.rissa_targets.pop(0).name
print(f"rissa targets: {self.rissa_targets}") print(f"rissa targets: {self.rissa_targets}")
self.notify_self() self.notify_self()
elif self.did_choose_callback is not None:
self.did_choose_callback(self, card_index)
self.notify_self()
elif self.choose_text == "choose_ricercato": elif self.choose_text == "choose_ricercato":
player = self.game.get_player_named( player = self.game.get_player_named(
self.available_cards[card_index]["name"] self.available_cards[card_index]["name"]
@ -1871,6 +1869,8 @@ class Player:
self.expected_response.append(cs.Bang(0, 0).name) self.expected_response.append(cs.Bang(0, 0).name)
if self.character.check(self.game, chw.BigSpencer): if self.character.check(self.game, chw.BigSpencer):
self.expected_response = [] self.expected_response = []
if any((isinstance(c, trt.Caboose) for c in self.equipment)):
self.expected_response.append([c.name for c in self.equipment if not c.usable_next_turn])
self.on_failed_response_cb = self.take_damage_response self.on_failed_response_cb = self.take_damage_response
self.notify_self() self.notify_self()
@ -1933,7 +1933,7 @@ class Player:
self.notify_self() self.notify_self()
def get_discarded(self, attacker=None, card_name=None, action=None): def get_discarded(self, attacker=None, card_name=None, action=None):
if card_name in {"Tornado", "Poker", "Bandidos"}: if card_name in {"Tornado", "Poker", "Bandidos", "Circus Wagon"}:
self.pending_action = PendingAction.CHOOSE self.pending_action = PendingAction.CHOOSE
self.available_cards = self.hand.copy() self.available_cards = self.hand.copy()
if card_name == "Tornado": if card_name == "Tornado":
@ -1946,6 +1946,13 @@ class Player:
self.available_cards.append( self.available_cards.append(
{"name": "-1hp", "icon": "💔", "noDesc": True} {"name": "-1hp", "icon": "💔", "noDesc": True}
) )
if card_name == "Circus Wagon":
from bang.expansions.train_robbery.trains import CircusWagon
self.set_choose_action(
"choose_circus_wagon",
self.hand.copy(),
CircusWagon.choose_circus_wagon,
)
return True return True
else: else:
if self.can_escape(card_name) or self.character.check( if self.can_escape(card_name) or self.character.check(
@ -1995,6 +2002,17 @@ class Player:
self.attacker = attacker self.attacker = attacker
self.attacking_card = card_name self.attacking_card = card_name
print(f"attacker -> {attacker}") print(f"attacker -> {attacker}")
# check for trt.PrivateCar
if (card_name == "Bang!" and any(
(isinstance(c, trt.PrivateCar) for c in self.equipment)
) and len(self.hand) == 0):
self.take_no_damage_response()
G.sio.emit(
"chat_message",
room=self.game.name,
data=f"_in_private_car|{self.name}|{attacker.name}",
)
return False
if ( if (
isinstance(attacker, Player) isinstance(attacker, Player)
and attacker.character.check(self.game, tvosch.ColoradoBill) and attacker.character.check(self.game, tvosch.ColoradoBill)
@ -2100,9 +2118,7 @@ class Player:
if len(equipments) == 0: if len(equipments) == 0:
return False return False
else: else:
self.choose_text = "choose_dalton" self.set_choose_action("choose_dalton", equipments)
self.pending_action = PendingAction.CHOOSE
self.available_cards = equipments
return True return True
def get_indians(self, attacker): def get_indians(self, attacker):
@ -2112,6 +2128,11 @@ class Player:
(isinstance(c, grc.Calumet) for c in self.gold_rush_equipment) (isinstance(c, grc.Calumet) for c in self.gold_rush_equipment)
): ):
return False return False
# check for trt.PrisonerCar
if any(
(isinstance(c, trt.PrisonerCar) for c in self.equipment)
):
return False
if ( if (
not self.game.is_competitive not self.game.is_competitive
and not any( and not any(
@ -2145,6 +2166,11 @@ class Player:
def get_dueled(self, attacker): def get_dueled(self, attacker):
self.attacker = attacker self.attacker = attacker
self.attacking_card = "Duello" self.attacking_card = "Duello"
if not self.is_my_turn and any(
(isinstance(c, trt.PrisonerCar) for c in self.equipment)
):
self.take_no_damage_response()
return False
if (self.game.check_event(ceh.Sermone) and self.is_my_turn) or ( if (self.game.check_event(ceh.Sermone) and self.is_my_turn) or (
not self.game.is_competitive not self.game.is_competitive
and not any( and not any(
@ -2501,26 +2527,29 @@ class Player:
self.character.special(self, data) self.character.special(self, data)
def gold_rush_discard(self): def gold_rush_discard(self):
self.available_cards = [ self.set_choose_action(
{ "gold_rush_discard",
"name": p.name, [
"icon": p.role.icon {
if (self.game.initial_players == 3) "name": p.name,
else "⭐️" "icon": p.role.icon
if isinstance(p.role, r.Sheriff) if (self.game.initial_players == 3)
else "🤠", else "⭐️"
"is_character": True, if isinstance(p.role, r.Sheriff)
"avatar": p.avatar, else "🤠",
"alt_text": "".join(["🎴️"] * len(p.gold_rush_equipment)), "is_character": True,
"is_player": True, "avatar": p.avatar,
} "alt_text": "".join(["🎴️"] * len(p.gold_rush_equipment)),
for p in self.game.get_alive_players() "is_player": True,
if p != self }
and any((e.number + 1 <= self.gold_nuggets for e in p.gold_rush_equipment)) for p in self.game.get_alive_players()
] if p != self
self.available_cards.append({"icon": "", "noDesc": True}) and any(
self.choose_text = "gold_rush_discard" (e.number + 1 <= self.gold_nuggets for e in p.gold_rush_equipment)
self.pending_action = PendingAction.CHOOSE )
]
+ [{"icon": "", "noDesc": True}],
)
self.notify_self() self.notify_self()
def buy_gold_rush_card(self, index): def buy_gold_rush_card(self, index):
@ -2548,8 +2577,59 @@ class Player:
self.game.deck.shop_deck.append(card) self.game.deck.shop_deck.append(card)
self.game.deck.shop_cards[index] = None self.game.deck.shop_cards[index] = None
self.game.deck.fill_gold_rush_shop() self.game.deck.fill_gold_rush_shop()
G.sio.emit(
"card_scrapped",
room=self.game.name,
data={"player": self.name, "pile": "gold_rush", "card": card.__dict__},
)
self.notify_self() self.notify_self()
def buy_train(self, index):
if self.pending_action != PendingAction.PLAY:
return
print(
f"{self.name} wants to buy train card on station index {index} in room {self.game.name}"
)
station: trs.StationCard = self.game.deck.stations[index]
train_index = len(self.game.deck.current_train) - 5 - index
if train_index < 0 or train_index >= len(self.game.deck.current_train):
return
train: trt.TrainCard = self.game.deck.current_train[train_index]
if train is not None and not train.is_locomotive:
if station.check_price(self):
print(f"{station=} {train=}")
station.attached_train = train
G.sio.emit(
"chat_message",
room=self.game.name,
data=f"_bought_train|{self.name}|{station.name}|{train.name}",
)
G.sio.emit(
"card_scrapped",
room=self.game.name,
data={"player": self.name, "pile": "train_robbery", "card": train.__dict__},
)
# shift train forward
for i in range(train_index, len(self.game.deck.current_train) - 1):
self.game.deck.current_train[i] = self.game.deck.current_train[
i + 1
]
self.game.notify_stations()
# self.game.deck.current_train[train_index] = None
self.notify_self()
def set_choose_action(
self,
choose_text: str,
available_cards: List,
did_choose_callback: Callable[['Player', int], None] = None,
):
self.pending_action = PendingAction.CHOOSE
self.choose_text = choose_text
self.available_cards = available_cards
self.did_choose_callback = did_choose_callback
def check_can_end_turn(self): def check_can_end_turn(self):
must_be_used_cards = [c for c in self.hand if c.must_be_used] must_be_used_cards = [c for c in self.hand if c.must_be_used]
if self.game.check_event(ce.LeggeDelWest) and len(must_be_used_cards) > 0: if self.game.check_event(ce.LeggeDelWest) and len(must_be_used_cards) > 0:
@ -2659,6 +2739,8 @@ class Player:
and not self.equipment[i].can_be_used_now and not self.equipment[i].can_be_used_now
): ):
self.equipment[i].can_be_used_now = True self.equipment[i].can_be_used_now = True
if isinstance(self.equipment[i], trt.TrainCard):
self.equipment[i].usable_next_turn = False
for i in range(len(self.hand)): for i in range(len(self.hand)):
if self.hand[i].must_be_used: if self.hand[i].must_be_used:
self.hand[i].must_be_used = False self.hand[i].must_be_used = False

View File

@ -1,6 +1,17 @@
from enum import IntEnum
class G: class G:
sio = None sio = None
def __init__(self): def __init__(self):
pass pass
class PendingAction(IntEnum):
PICK = 0
DRAW = 1
PLAY = 2
RESPOND = 3
WAIT = 4
CHOOSE = 5

View File

@ -16,8 +16,8 @@ import socketio
from discord_webhook import DiscordWebhook from discord_webhook import DiscordWebhook
from bang.game import Game from bang.game import Game
from bang.players import PendingAction, Player from bang.players import Player
from globals import G from globals import G, PendingAction
from metrics import Metrics from metrics import Metrics
sys.setrecursionlimit(10**6) # this should prevents bots from stopping sys.setrecursionlimit(10**6) # this should prevents bots from stopping
@ -346,6 +346,7 @@ def get_me(sid, data):
room.notify_scrap_pile(sid) room.notify_scrap_pile(sid)
room.notify_all() room.notify_all()
room.notify_gold_rush_shop() room.notify_gold_rush_shop()
room.notify_stations()
room.notify_event_card() room.notify_event_card()
room.notify_event_card_wildwestshow(sid) room.notify_event_card_wildwestshow(sid)
else: else:
@ -678,6 +679,14 @@ def buy_gold_rush_card(sid, data: int):
ses.buy_gold_rush_card(data) ses.buy_gold_rush_card(data)
@sio.event
@bang_handler
def buy_train(sid, data: int):
ses: Player = sio.get_session(sid)
ses.game.rpc_log.append(f"{ses.name};buy_train;{data}")
ses.buy_train(data)
@sio.event @sio.event
@bang_handler @bang_handler
def chat_message(sid, msg, pl=None): def chat_message(sid, msg, pl=None):
@ -1029,7 +1038,9 @@ def chat_message(sid, msg, pl=None):
cmd = msg.split() cmd = msg.split()
if len(cmd) >= 2: if len(cmd) >= 2:
import bang.expansions.train_robbery.trains as trt
cards = cs.get_starting_deck(ses.game.expansions) cards = cs.get_starting_deck(ses.game.expansions)
cards.extend(trt.get_all_cards())
card_names = " ".join(cmd[1:]).split(",") card_names = " ".join(cmd[1:]).split(",")
for cn in card_names: for cn in card_names:
ses.equipment.append( ses.equipment.append(
@ -1088,6 +1099,21 @@ def chat_message(sid, msg, pl=None):
"type": "json", "type": "json",
}, },
) )
elif "/deckinfo" in msg:
sio.emit(
"chat_message",
room=sid,
data={
"color": "",
"text": json.dumps(
ses.game.deck.__dict__,
default=lambda o: f"<{o.__class__.__name__}() not serializable>",
),
"type": "json",
},
)
elif "/trainfw" in msg:
ses.game.deck.move_train_forward()
elif "/status" in msg and ses.is_admin(): elif "/status" in msg and ses.is_admin():
sio.emit("mount_status", room=sid) sio.emit("mount_status", room=sid)
elif "/meinfo" in msg: elif "/meinfo" in msg:
@ -1282,6 +1308,23 @@ def get_wildwestshowcards(sid):
"wwscards_info", room=sid, data=json.dumps(chs, default=lambda o: o.__dict__) "wwscards_info", room=sid, data=json.dumps(chs, default=lambda o: o.__dict__)
) )
@sio.event
@bang_handler
def get_trainrobberycards(sid):
print("get_trainrobberycards")
import bang.expansions.train_robbery.cards as trc
import bang.expansions.train_robbery.stations as trs
import bang.expansions.train_robbery.trains as trt
chs = []
chs.extend(trt.get_locomotives())
chs.extend(trt.get_all_cards())
sio.emit(
"trainrobberycards_info", room=sid, data=json.dumps({
"cards": chs,
"stations": trs.get_all_stations()
}, default=lambda o: o.__dict__)
)
@sio.event @sio.event
@bang_handler @bang_handler

View File

@ -1,3 +1,4 @@
from typing import Any, List
import pytest import pytest
from bang.characters import Character from bang.characters import Character
from bang.game import Game from bang.game import Game
@ -8,7 +9,7 @@ from globals import G
G.sio = DummySocket() G.sio = DummySocket()
def started_game(expansions, players=4, character=Character("test_char", 4)): def started_game(expansions=[], players=4, character=Character("test_char", 4)) -> Game:
g = Game("test") g = Game("test")
g.expansions = expansions g.expansions = expansions
ps = [Player(f"p{i}", f"p{i}") for i in range(players)] ps = [Player(f"p{i}", f"p{i}") for i in range(players)]
@ -23,19 +24,19 @@ def started_game(expansions, players=4, character=Character("test_char", 4)):
return g return g
def set_events(g: Game, event_cards): def set_events(g: Game, event_cards) -> None:
g.deck.event_cards = event_cards g.deck.event_cards = event_cards
def current_player(g: Game): def current_player(g: Game) -> Player:
return g.players[g.turn] return g.players[g.turn]
def next_player(g: Game): def next_player(g: Game) -> Player:
return g.players[(g.turn + 1) % len(g.players)] return g.players[(g.turn + 1) % len(g.players)]
def current_player_with_cards(g: Game, cards): def current_player_with_cards(g: Game, cards: List[Any]) -> Player:
p = current_player(g) p = current_player(g)
p.draw("") p.draw("")
p.hand = cards p.hand = cards

View File

@ -3,7 +3,8 @@ from bang.characters import Character
from bang.cards import * from bang.cards import *
from bang.deck import Deck from bang.deck import Deck
from bang.game import Game from bang.game import Game
from bang.players import Player, PendingAction from bang.players import Player
from globals import PendingAction
# test card Barile # test card Barile
def test_barile(): def test_barile():

View File

@ -2,7 +2,8 @@ from random import randint
from bang.characters import * from bang.characters import *
from bang.deck import Deck from bang.deck import Deck
from bang.game import Game from bang.game import Game
from bang.players import Player, PendingAction from bang.players import Player
from globals import PendingAction
from bang.cards import * from bang.cards import *
def test_bartcassidy(): def test_bartcassidy():

View File

@ -3,7 +3,7 @@ from bang.characters import Character
from bang.expansions.dodge_city.cards import * from bang.expansions.dodge_city.cards import *
from bang.deck import Deck from bang.deck import Deck
from bang.game import Game from bang.game import Game
from bang.players import Player, PendingAction from bang.players import Player
import bang.cards as cs import bang.cards as cs
# test Borraccia # test Borraccia

View File

@ -1,8 +1,9 @@
from bang.deck import Deck from bang.deck import Deck
from bang.game import Game from bang.game import Game
from bang.players import Player, PendingAction from bang.players import Player
from bang.roles import * from bang.roles import *
from bang.cards import * from bang.cards import *
from globals import PendingAction
from tests import started_game from tests import started_game

View File

@ -1,7 +1,8 @@
from bang.characters import Character from bang.characters import Character
from bang.deck import Deck from bang.deck import Deck
from bang.game import Game from bang.game import Game
from bang.players import Player, PendingAction from bang.players import Player
from globals import PendingAction
from bang.roles import * from bang.roles import *
from bang.cards import * from bang.cards import *

View File

@ -0,0 +1,24 @@
from random import randint
from bang.characters import Character
from bang.expansions.train_robbery.trains import *
from bang.deck import Deck
from bang.game import Game
from bang.players import Player
import bang.cards as cs
from globals import PendingAction
from tests import started_game, set_events, current_player, next_player, current_player_with_cards
def test_cattle_truck():
g = started_game()
g.deck.scrap_pile = [cs.CatBalou(0,1), cs.CatBalou(0,2), cs.CatBalou(0,3)]
p = current_player_with_cards(g, [CattleTruck()])
p.play_card(0)
assert p.pending_action == PendingAction.CHOOSE
p.choose(0)
assert p.pending_action == PendingAction.PLAY
assert len(p.hand) == 1
assert len(g.deck.scrap_pile) == 2

View File

@ -3,8 +3,9 @@ from bang.characters import Character
from bang.expansions.the_valley_of_shadows.characters import * from bang.expansions.the_valley_of_shadows.characters import *
from bang.deck import Deck from bang.deck import Deck
from bang.game import Game from bang.game import Game
from bang.players import Player, PendingAction from bang.players import Player
import bang.cards as cs import bang.cards as cs
from globals import PendingAction
# test TucoFranziskaner # test TucoFranziskaner
def test_TucoFranziskaner(): def test_TucoFranziskaner():

View File

@ -3,8 +3,9 @@ from bang.characters import Character
from bang.expansions.the_valley_of_shadows.cards import * from bang.expansions.the_valley_of_shadows.cards import *
from bang.deck import Deck from bang.deck import Deck
from bang.game import Game from bang.game import Game
from bang.players import Player, PendingAction from bang.players import Player
import bang.cards as cs import bang.cards as cs
from globals import PendingAction
from tests import started_game, set_events, current_player, next_player, current_player_with_cards from tests import started_game, set_events, current_player, next_player, current_player_with_cards

View File

@ -4,7 +4,7 @@ from tests import started_game, set_events, current_player, next_player, current
from bang.expansions.wild_west_show.characters import * from bang.expansions.wild_west_show.characters import *
from bang.cards import Card, Suit from bang.cards import Card, Suit
import bang.roles as roles import bang.roles as roles
from bang.players import PendingAction from globals import PendingAction
# test TerenKill # test TerenKill

View File

@ -4,7 +4,7 @@ from tests import started_game, set_events, current_player, next_player, current
from bang.expansions.wild_west_show.card_events import * from bang.expansions.wild_west_show.card_events import *
from bang.cards import Card, Suit from bang.cards import Card, Suit
import bang.roles as roles import bang.roles as roles
from bang.players import PendingAction from globals import PendingAction
# test Camposanto # test Camposanto

View File

@ -11,6 +11,7 @@
'gold-rush': card.expansion === 'gold_rush', 'gold-rush': card.expansion === 'gold_rush',
brown: card.kind === 0, brown: card.kind === 0,
black: card.kind === 1, black: card.kind === 1,
'train-piece': card.type && card.type === 'train',
}" }"
> >
<h4>{{ cardName }}</h4> <h4>{{ cardName }}</h4>
@ -64,7 +65,7 @@ export default {
}, },
suit() { suit() {
if (this.card && !isNaN(this.card.suit)) { if (this.card && !isNaN(this.card.suit)) {
let x = ["♦️", "♣️", "♥️", "♠️", "🤑"]; let x = ["♦️", "♣️", "♥️", "♠️", "🤑", "🚂"];
return x[this.card.suit]; return x[this.card.suit];
} else if (this.card.suit) { } else if (this.card.suit) {
return this.card.suit; return this.card.suit;
@ -115,7 +116,7 @@ export default {
#816b45 10px #816b45 10px
); );
} }
.card:not(.back, .fistful-of-cards, .high-noon, .gold-rush):before { .card:not(.back, .fistful-of-cards, .high-noon, .gold-rush, .train-piece):before {
content: ""; content: "";
background-image: radial-gradient(var(--bg-color) 13%, #0000 5%), background-image: radial-gradient(var(--bg-color) 13%, #0000 5%),
radial-gradient(var(--bg-color) 14%, transparent 5%), radial-gradient(var(--bg-color) 14%, transparent 5%),
@ -231,6 +232,18 @@ export default {
padding: 4pt; padding: 4pt;
border-radius: 12pt; border-radius: 12pt;
} }
.wip::after {
content: "WIP";
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 { .avatar {
position: absolute; position: absolute;
width: 36pt; width: 36pt;
@ -338,6 +351,7 @@ export default {
} }
.cant-play { .cant-play {
filter: brightness(0.5); filter: brightness(0.5);
cursor: not-allowed;
} }
.expansion { .expansion {
position: absolute; position: absolute;
@ -347,4 +361,24 @@ export default {
border-radius: 100%; border-radius: 100%;
transform: scale(0.8); transform: scale(0.8);
} }
.train-piece {
background: linear-gradient(180deg, rgba(218,101,64,1) 0%, rgba(217,197,184,1) 13%, rgba(217,197,184,1) 53%, rgba(235,169,95,1) 61%, rgba(158,81,55,1) 91%, rgba(158,81,55,1) 100%);
box-shadow: 0 0 0pt 2pt var(--font-color), 0 0 5pt 2pt #aaa;
}
.train-piece .emoji {
transform: scaleX(-1);
/* filter: grayscale(1); */
}
.train-piece .suit, .train-piece .expansion {
display: none;
}
.train-piece h4 {
position: absolute;
text-align: center;
width: 100%;
bottom: -10pt;
top: unset;
font-size: 11pt;
color: #FFE27E;
}
</style> </style>

View File

@ -21,7 +21,8 @@
@click="fillCmd(msg.cmd)">{{msg.cmd}} <i class="std-text" style="font-size:8pt;">{{msg.help}}</i></p> @click="fillCmd(msg.cmd)">{{msg.cmd}} <i class="std-text" style="font-size:8pt;">{{msg.help}}</i></p>
</div> </div>
<form @submit="sendChatMessage" id="msg-form"> <form @submit="sendChatMessage" id="msg-form">
<input id="my-msg" autocomplete="off" v-model="text" style="flex-grow:2;"/> <input id="my-msg" autocomplete="off" v-model="text" style="flex-grow:2;"
@keydown.tab.prevent="tabComplete($event.target.value)"/>
<input id="submit-message" type="submit" class="btn" :value="$t('submit')"/> <input id="submit-message" type="submit" class="btn" :value="$t('submit')"/>
</form> </form>
</div> </div>
@ -164,6 +165,12 @@ export default {
this.text = cmd; this.text = cmd;
document.getElementById('my-msg').focus(); document.getElementById('my-msg').focus();
}, },
tabComplete() {
if (this.commandSuggestion.length > 0) {
let cmd = this.commandSuggestion[0].cmd;
this.text = cmd + ' ';
}
},
playEffects(path) { playEffects(path) {
const promise = (new Audio(path)).play(); const promise = (new Audio(path)).play();
if(promise !== undefined){ if(promise !== undefined){

View File

@ -1,7 +1,7 @@
<template> <template>
<div > <div >
<div class="deck"> <div class="deck">
<div class="deck" :style="`position:relative;${goldRushShopOpen?'border: 2px dashed #6a6a6a42;border-radius:8pt':''}`" v-if="goldRushCards.length > 0" > <div class="deck" id="gold-rush-deck" :style="`position:relative;${goldRushShopOpen?'border: 2px dashed #6a6a6a42;border-radius:8pt':''}`" v-if="goldRushCards.length > 0" >
<card @pointerenter.native="()=>{setGoldRushDesc(goldRushCards[0])}" @pointerleave.native="goldRushDesc=null" :style="goldRushShopOpen?``:`position:absolute; top:0; right:0; transform: rotate(95deg) translate(30px, -40px) scale(0.6)`" v-if="goldRushCards.length > 0" :key="goldRushCards[0].name" :card="goldRushCards[0]" :class="{'shop-open':goldRushShopOpen, 'cant-play': pending_action !==2 || gold_nuggets < goldRushCards[0].number - gold_rush_discount}" @click.native="() => {buy_gold_rush_card(0)}"/> <card @pointerenter.native="()=>{setGoldRushDesc(goldRushCards[0])}" @pointerleave.native="goldRushDesc=null" :style="goldRushShopOpen?``:`position:absolute; top:0; right:0; transform: rotate(95deg) translate(30px, -40px) scale(0.6)`" v-if="goldRushCards.length > 0" :key="goldRushCards[0].name" :card="goldRushCards[0]" :class="{'shop-open':goldRushShopOpen, 'cant-play': pending_action !==2 || gold_nuggets < goldRushCards[0].number - gold_rush_discount}" @click.native="() => {buy_gold_rush_card(0)}"/>
<card @pointerenter.native="()=>{setGoldRushDesc(goldRushCards[1])}" @pointerleave.native="goldRushDesc=null" :style="goldRushShopOpen?``:`position:absolute; top:0; right:0; transform: rotate(90deg) translate(0, -40px) scale(0.6)`" v-if="goldRushCards.length > 1" :key="goldRushCards[1].name" :card="goldRushCards[1]" :class="{'shop-open':goldRushShopOpen, 'cant-play': pending_action !==2 || gold_nuggets < goldRushCards[1].number - gold_rush_discount}" @click.native="() => {buy_gold_rush_card(1)}"/> <card @pointerenter.native="()=>{setGoldRushDesc(goldRushCards[1])}" @pointerleave.native="goldRushDesc=null" :style="goldRushShopOpen?``:`position:absolute; top:0; right:0; transform: rotate(90deg) translate(0, -40px) scale(0.6)`" v-if="goldRushCards.length > 1" :key="goldRushCards[1].name" :card="goldRushCards[1]" :class="{'shop-open':goldRushShopOpen, 'cant-play': pending_action !==2 || gold_nuggets < goldRushCards[1].number - gold_rush_discount}" @click.native="() => {buy_gold_rush_card(1)}"/>
<card @pointerenter.native="()=>{setGoldRushDesc(goldRushCards[2])}" @pointerleave.native="goldRushDesc=null" :style="goldRushShopOpen?``:`position:absolute; top:0; right:0; transform: rotate(85deg) translate(-30px, -40px) scale(0.6)`" v-if="goldRushCards.length > 2" :key="goldRushCards[2].name" :card="goldRushCards[2]" :class="{'shop-open':goldRushShopOpen, 'cant-play': pending_action !==2 || gold_nuggets < goldRushCards[2].number - gold_rush_discount}" @click.native="() => {buy_gold_rush_card(2)}"/> <card @pointerenter.native="()=>{setGoldRushDesc(goldRushCards[2])}" @pointerleave.native="goldRushDesc=null" :style="goldRushShopOpen?``:`position:absolute; top:0; right:0; transform: rotate(85deg) translate(-30px, -40px) scale(0.6)`" v-if="goldRushCards.length > 2" :key="goldRushCards[2].name" :card="goldRushCards[2]" :class="{'shop-open':goldRushShopOpen, 'cant-play': pending_action !==2 || gold_nuggets < goldRushCards[2].number - gold_rush_discount}" @click.native="() => {buy_gold_rush_card(2)}"/>
@ -11,6 +11,10 @@
<card :card="goldRushCardBack" :donotlocalize="true" class="gold-rush back last-event" @click.native="goldRushShopOpen = !goldRushShopOpen"/> <card :card="goldRushCardBack" :donotlocalize="true" class="gold-rush back last-event" @click.native="goldRushShopOpen = !goldRushShopOpen"/>
</div> </div>
</div> </div>
<div v-if="currentStations.length > 0" id="train-robbery-deck" class="deck" :style="`position:relative;border: 2px dashed #6a6a6a42;border-radius:8pt;align-items: flex-end;flex-direction:row;`" >
<station-card @click.native="()=>{buyTrain(i)}" v-for="station, i in currentStations" :key="station.name" :card="station" :price="station.price" :trainPiece="trainPieceForStation(i)"
@pointerenter.native="()=>{setStationDesc(i)}" @pointerleave.native="stationDesc = null"/>
</div>
<div v-if="eventCard" style="position:relative"> <div v-if="eventCard" style="position:relative">
<div class="card fistful-of-cards" style="position:relative; bottom:-3pt;right:-3pt;"/> <div class="card fistful-of-cards" style="position:relative; bottom:-3pt;right:-3pt;"/>
<div class="card fistful-of-cards" style="position:absolute; bottom:-1.5pt;right:-1.5pt;"/> <div class="card fistful-of-cards" style="position:absolute; bottom:-1.5pt;right:-1.5pt;"/>
@ -38,15 +42,14 @@
</div> </div>
<transition name="list"> <transition name="list">
<p v-if="eventCard" class="center-stuff"><b>{{eventDesc}}</b></p> <p v-if="eventCard" class="center-stuff"><b>{{eventDesc}}</b></p>
</transition>
<transition name="list">
<p v-if="eventCardWildWestShow && !eventCardWildWestShow.back" class="center-stuff">🎪 <b>{{eventDescWildWestShow}}</b> 🎪</p> <p v-if="eventCardWildWestShow && !eventCardWildWestShow.back" class="center-stuff">🎪 <b>{{eventDescWildWestShow}}</b> 🎪</p>
</transition>
<transition name="list">
<div v-if="goldRushDesc"> <div v-if="goldRushDesc">
<p class="center-stuff">🤑 <i>{{$t(`cards.${goldRushDesc.name}.desc`)}}</i> 🤑</p> <p class="center-stuff">🤑 <i>{{$t(`cards.${goldRushDesc.name}.desc`)}}</i> 🤑</p>
<p class="center-stuff">🤑 <b>{{goldRushDesc.number - gold_rush_discount}} 💵</b> 🤑</p> <p class="center-stuff">🤑 <b>{{goldRushDesc.number - gold_rush_discount}} 💵</b> 🤑</p>
</div> </div>
<div v-if="stationDesc">
<p class="center-stuff"><i>{{stationDesc}}</i></p>
</div>
</transition> </transition>
<div style="margin-bottom:6pt;margin-bottom: 6pt;display: flex;flex-direction: column;"> <div style="margin-bottom:6pt;margin-bottom: 6pt;display: flex;flex-direction: column;">
<button class="btn" v-if="pending_action == 2 && can_gold_rush_discard" @click="$socket.emit('gold_rush_discard')">{{$t('gold_rush_discard')}}</button> <button class="btn" v-if="pending_action == 2 && can_gold_rush_discard" @click="$socket.emit('gold_rush_discard')">{{$t('gold_rush_discard')}}</button>
@ -59,6 +62,7 @@
<script> <script>
import Card from '@/components/Card.vue' import Card from '@/components/Card.vue'
import StationCard from '@/components/StationCard.vue'
export default { export default {
name: 'Deck', name: 'Deck',
@ -67,6 +71,7 @@ export default {
}, },
components: { components: {
Card, Card,
StationCard
}, },
data: () => ({ data: () => ({
card: { card: {
@ -88,8 +93,11 @@ export default {
goldRushCards: [], goldRushCards: [],
gold_nuggets: 0, gold_nuggets: 0,
goldRushDesc: null, goldRushDesc: null,
stationDesc: null,
can_gold_rush_discard: false, can_gold_rush_discard: false,
gold_rush_discount: 0, gold_rush_discount: 0,
currentStations: [],
currentTrain: [],
}), }),
sockets: { sockets: {
self(self){ self(self){
@ -123,6 +131,11 @@ export default {
console.log('GOLD RUSH:'+ cards) console.log('GOLD RUSH:'+ cards)
this.goldRushCards = JSON.parse(cards) this.goldRushCards = JSON.parse(cards)
}, },
stations(stations) {
let msg = JSON.parse(stations)
this.currentStations = msg.stations
this.currentTrain = msg.current_train
},
}, },
computed: { computed: {
endTurnCard() { endTurnCard() {
@ -163,6 +176,15 @@ export default {
}, },
}, },
methods: { methods: {
trainPieceForStation(i) {
let index = this.currentTrain.length-5-i;
if (index < 0 || index >= this.currentTrain.length)
return null;
return this.currentTrain[index];
},
buyTrain(i) {
this.$socket.emit('buy_train', i)
},
action(pile) { action(pile) {
if (this.pending_action !== false && this.pending_action < 2) { if (this.pending_action !== false && this.pending_action < 2) {
// console.log('action') // console.log('action')
@ -189,6 +211,14 @@ export default {
setGoldRushDesc(card) { setGoldRushDesc(card) {
this.goldRushDesc = card this.goldRushDesc = card
}, },
setStationDesc(index) {
console.log('setStationDesc', index)
this.stationDesc = this.$t(`cards.${this.currentStations[index].name}.desc`)
const trainPiece = this.trainPieceForStation(index)
if (trainPiece) {
this.stationDesc += '\n\n🚂' + this.$t(`cards.${trainPiece.name}.desc`)
}
},
}, },
mounted() { mounted() {
if (window.innerWidth < 1000) { if (window.innerWidth < 1000) {

View File

@ -127,15 +127,32 @@
</div> </div>
</div> </div>
</div> </div>
<h2 id="trainrobberycards">{{$t('help.trainrobberycards')}}</h2>
<div class="flexy-cards-wrapper">
<div v-for="(c, i) in trainrobberycards" v-bind:key="c.name ? (c.name+c.number) : i" class="flexy-cards">
<Card :card="c" class="train-robbery" @pointerenter.native="''" @pointerleave.native="''"/>
<div style="margin-left:6pt;">
<p>{{$t(`cards.${c.name}.desc`)}}</p>
</div>
</div>
<div v-for="(c, i) in trainrobberystations" v-bind:key="c.name ? (c.name+c.number) : i" class="flexy-cards">
<StationCard :card="c" class="train-robbery" @pointerenter.native="''" @pointerleave.native="''" :price="c.price"/>
<div style="margin-left:6pt;">
<p>{{$t(`cards.${c.name}.desc`)}}</p>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import Card from '@/components/Card.vue' import Card from '@/components/Card.vue'
import StationCard from './StationCard.vue'
export default { export default {
name: 'Help', name: 'Help',
components: { components: {
Card, Card,
StationCard,
}, },
props: { props: {
inGame: Boolean inGame: Boolean
@ -152,6 +169,8 @@ export default {
goldrushcards: [], goldrushcards: [],
valleyofshadowscards: [], valleyofshadowscards: [],
wildwestshowcards: [], wildwestshowcards: [],
trainrobberycards: [],
trainrobberystations: [],
}), }),
computed: { computed: {
endTurnCard() { endTurnCard() {
@ -196,6 +215,14 @@ export default {
...x, ...x,
})) }))
}, },
trainrobberycards_info(cardsJson) {
this.trainrobberycards = JSON.parse(cardsJson).cards.map(x=>({
...x,
}))
this.trainrobberystations = JSON.parse(cardsJson).stations.map(x=>({
...x,
}))
},
}, },
mounted() { mounted() {
this.$socket.emit('get_cards') this.$socket.emit('get_cards')
@ -205,6 +232,7 @@ export default {
this.$socket.emit('get_goldrushcards') this.$socket.emit('get_goldrushcards')
this.$socket.emit('get_valleyofshadowscards') this.$socket.emit('get_valleyofshadowscards')
this.$socket.emit('get_wildwestshowcards') this.$socket.emit('get_wildwestshowcards')
this.$socket.emit('get_trainrobberycards')
document.getElementById('help').scrollIntoView(); document.getElementById('help').scrollIntoView();
} }
} }

View File

@ -646,8 +646,15 @@ export default {
console.log("card_scrapped no deck"); console.log("card_scrapped no deck");
return; return;
} }
let decelOffset = cumulativeOffset(decel);
let phand = document.getElementById(`${data.player}-hand`); let phand = document.getElementById(`${data.player}-hand`);
if (data.pile == "train_robbery") {
decel = phand
phand = document.getElementById("train-robbery-deck");
} else if (data.pile == "gold_rush") {
decel = phand
phand = document.getElementById("gold-rush-deck");
}
let decelOffset = cumulativeOffset(decel);
if (!phand) { if (!phand) {
console.log("card_scrapped no phand"); console.log("card_scrapped no phand");
return; return;

View File

@ -0,0 +1,126 @@
<template>
<div>
<div :class="{stationcard:true, 'cant-play':!trainPiece || trainPiece.is_locomotive}">
<h4>{{ cardName }}</h4>
<div :class="{ emoji: true, bottomed: card.avatar }">{{ emoji }}</div>
<div class="alt_text">{{ card.alt_text }}</div>
<div class="price">
<card v-for="c, i in price" :key="i" :card="c"/>
</div>
</div>
<card v-if="trainPiece" :card="trainPiece" :class="{'cant-play':trainPiece.is_locomotive}"/>
</div>
</template>
<script>
import Card from '@/components/Card.vue'
export default {
name: "StationCard",
props: {
card: Object,
price: Array,
trainPiece: Object,
donotlocalize: Boolean,
},
components: {
Card
},
computed: {
cardName() {
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 scoped>
.stationcard {
cursor: pointer;
width: 60pt;
min-width: 60pt;
height: 60pt;
margin: 6pt;
border-radius: 16pt 16pt 2pt 2pt;
position: relative;
transition: all 0.5s ease-in-out;
text-overflow: ellipsis;
word-wrap: normal;
color: white;
background: repeating-linear-gradient(
0deg,
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);
box-shadow: 0 0 0pt 2pt var(--font-color), 0 0 5pt 2pt #aaa;
}
.stationcard h4 {
position: absolute;
text-align: center;
width: 100%;
top: -15pt;
font-size: 10pt;
}
.alt_text {
right: 3pt;
text-align: center;
position: absolute;
font-size: small;
bottom: 20pt;
left: 3pt;
}
.stationcard .price {
justify-content: center;
/* left: -12pt; */
margin-top: -20pt;
/* left: 0; */
display: flex;
width: 60pt;
transform: scale(0.3);
}
.price .card {
transform: rotate(14deg);
}
.train-piece {
margin: 6pt;
}
.cant-play {
cursor: not-allowed;
}
.stationcard .card {
cursor: unset;
}
</style>

View File

@ -100,6 +100,11 @@
"copy": "Copy invite", "copy": "Copy invite",
"no_players_in_range": "You can't see the other players, equip a weapon or a scope!", "no_players_in_range": "You can't see the other players, equip a weapon or a scope!",
"cantplaycard": "You can't play this card like that!", "cantplaycard": "You can't play this card like that!",
"choose_baggage_car": "Discard this for a Missed! Panic!, Cat Balou, or an extra BANG!",
"choose_mail_car": "Choose which card to give to another player",
"choose_other_player": "Choose the player to give the card to",
"choose_sleeper_car": "Choose a card to discard for Sleeper Car",
"choose_buy_train": "Discard a card to rob the train, and get the carriage",
"chat": { "chat": {
"spectators": " | A spectator is watching the game | {n} spectators are watching the game", "spectators": " | A spectator is watching the game | {n} spectators are watching the game",
"chat": "Chat", "chat": "Chat",
@ -153,7 +158,9 @@
"choose_emporio": ";{0}; has chosen ;{1}; from General Store.", "choose_emporio": ";{0}; has chosen ;{1}; from General Store.",
"shotgun_scrap": "When the shotgun hit ;{0}; a card flew away from his hand (;{1};)", "shotgun_scrap": "When the shotgun hit ;{0}; a card flew away from his hand (;{1};)",
"taglia_reward": "💰 ;{1}; got a card from the bounty on ;{0};", "taglia_reward": "💰 ;{1}; got a card from the bounty on ;{0};",
"snake_bit": "🐍 ;{0}; was bitten by the Rattle Snake." "snake_bit": "🐍 ;{0}; was bitten by the Rattle Snake.",
"in_private_car": "🚋💁🏻 ;{0}; is in a private car and couldn't get Bang!ed by ;{1};",
"bought_train": "🚂 ;{0}; robbed the train at ;{1}; and got the ;{2};"
}, },
"foc": { "foc": {
"leggedelwest": "He must play this card on this turn if possible." "leggedelwest": "He must play this card on this turn if possible."
@ -855,6 +862,130 @@
"Wild West Show": { "Wild West Show": {
"name": "Wild West Show", "name": "Wild West Show",
"desc": "The goal for each player becomes: \"Be the last one standing!\"" "desc": "The goal for each player becomes: \"Be the last one standing!\""
},
"Boom Town": {
"name": "Boom Town",
"desc": "Discard a Bang! to rob the train"
},
"Caticor": {
"name": "Caticor",
"desc": "Discard a Cat Balou or Panico to rob the train"
},
"Creepy Creek": {
"name": "Creepy Creek",
"desc": "Discard a card of spades to rob the train"
},
"Crowns Hole": {
"name": "Crown's Hole",
"desc": "Discard a beer to rob the train"
},
"Deadwood": {
"name": "Deadwood",
"desc": "Discard an equipment card to rob the train"
},
"Dodgeville": {
"name": "Dodgeville",
"desc": "Discard a Missed! to rob the train"
},
"Fort Worth": {
"name": "Fort Worth",
"desc": "Discard a card with number 10, J, Q, K, A to rob the train"
},
"Frisco": {
"name": "Frisco",
"desc": "Discard a card of clubs to rob the train"
},
"Miners Oath": {
"name": "Miner's Oath",
"desc": "Discard a card of diamonds to rob the train"
},
"San Tafe": {
"name": "San Tafe",
"desc": "Discard a card of hearts to rob the train"
},
"Tombrock": {
"name": "Tombrock",
"desc": "Lose 1 life point to rob the train"
},
"Yooma": {
"name": "Yooma",
"desc": "Discard a card with number between 2 and 9 to rob the train"
},
"Virginia Town": {
"name": "Virginia Town",
"desc": "Discard two cards to rob the train"
},
"Ironhorse": {
"name": "Ironhorse",
"desc": "Each player, including the one who activated the effect, is targeted by a BANG!\nNo player is responsible for any loss of life points.\nIf all players are eliminated at the same time, the Outlaws win."
},
"Leland": {
"name": "Leland",
"desc": "LOCOMOTIVE: perform the effect of the Emporium, starting with the current player and proceeding clockwise."
},
"Baggage Car": {
"name": "Baggage Car",
"desc": "Discard this card to gain the effect of a Missed!, Panic!, Cat Balou, or an extra BANG! card."
},
"Caboose": {
"name": "Caboose",
"desc": "To discard one of your blue-bordered cards, include a car as if it were a Missed!"
},
"Cattle Truck": {
"name": "Cattle Truck",
"desc": "Discard this card to look at the top 3 cards of the discard pile and choose 1 to add to your hand."
},
"Circus Wagon": {
"name": "Circus Wagon",
"desc": "Discard this card. Each other player must discard a card that he has in play."
},
"Coal Hopper": {
"name": "Coal Hopper",
"desc": "Discard this card to draw a card and discard a car in play in front of any player of your choice."
},
"Dining Car": {
"name": "Dining Car",
"desc": "At the beginning of your turn, \"pick!\": if it's Hearts, regain 1 life point."
},
"Express Car": {
"name": "Express Car",
"desc": "Discard all your hand cards and take another turn."
},
"Ghost Car": {
"name": "Ghost Car",
"desc": "Play it on any player except the Sheriff. If you are eliminated, you remain in play, but cannot look at or lose any life points."
},
"Lounge Car": {
"name": "Lounge Car",
"desc": "Discard this card to draw 2 cards from the deck and put 1 of them in play in front of you and 1 in front of another player."
},
"Lumber Flatcar": {
"name": "Lumber Flatcar",
"desc": "Play this card on any player (including yourself). While this card is in play, that player sees all other players at a distance increased by 1."
},
"Mail Car": {
"name": "Mail Car",
"desc": "Discard this card to draw 3 cards and give 1 of them to another player of your choice."
},
"Observation Car": {
"name": "Observation Car",
"desc": "You see other players at a distance reduced by 1. Other players see you at a distance increased by 1."
},
"Passenger Car": {
"name": "Passenger Car",
"desc": "Discard this card to draw a card (from hand or in play) from another player."
},
"Prisoner Car": {
"name": "Prisoner Car",
"desc": "Duel and Indians! cards played by other players have no effect on you."
},
"Private Car": {
"name": "Private Car",
"desc": "If you have no cards in hand, you cannot be targeted by BANG! cards."
},
"Sleeper Car": {
"name": "Sleeper Car",
"desc": "Once per turn, you may discard another of your blue-bordered cards in play."
} }
}, },
"help": { "help": {
@ -906,7 +1037,9 @@
"highnooncards": "High Noon - Event Cards", "highnooncards": "High Noon - Event Cards",
"foccards": "Fistful of Cards - Event Cards", "foccards": "Fistful of Cards - Event Cards",
"goldrushcards": "Gold Rush Cards", "goldrushcards": "Gold Rush Cards",
"valleyofshadowscards": "The Valley of Shadows Cards" "valleyofshadowscards": "The Valley of Shadows Cards",
"wildwestshowcards": "Wild West Show Cards",
"trainrobberycards": "Train Robbery Cards"
}, },
"theme": { "theme": {
"sepia": "Sepia", "sepia": "Sepia",

View File

@ -153,7 +153,8 @@
"choose_emporio": ";{0}; ha scelto ;{1}; da Emporio.", "choose_emporio": ";{0}; ha scelto ;{1}; da Emporio.",
"shotgun_scrap": "Quando lo shotgun ha colpito ;{0}; gli ha tolto una carta (;{1};)", "shotgun_scrap": "Quando lo shotgun ha colpito ;{0}; gli ha tolto una carta (;{1};)",
"taglia_reward": "💰 ;{1}; ha ottenuto ricompensa dalla taglia su ;{0};", "taglia_reward": "💰 ;{1}; ha ottenuto ricompensa dalla taglia su ;{0};",
"snake_bit": "🐍 ;{0}; è stato morso dal Serpente a Sonagli." "snake_bit": "🐍 ;{0}; è stato morso dal Serpente a Sonagli.",
"in_private_car": "🚋💁🏻 ;{0}; è in una carrozza privata e non è stato attaccato da ;{1};"
}, },
"foc": { "foc": {
"leggedelwest": "Ed è obbligato a usarla nel suo turno, se possibile" "leggedelwest": "Ed è obbligato a usarla nel suo turno, se possibile"
@ -855,6 +856,130 @@
"Wild West Show": { "Wild West Show": {
"name": "Wild West Show", "name": "Wild West Show",
"desc": "L'obiettivo di ogni giocatore diventa: \"Rimani l'ultimo in gioco!\"" "desc": "L'obiettivo di ogni giocatore diventa: \"Rimani l'ultimo in gioco!\""
},
"Boom Town": {
"name": "Boom Town",
"desc": "Scarta una Bang! per derubare il treno"
},
"Caticor": {
"name": "Caticor",
"desc": "Scarta una Cat Balou o un Panico per derubare il treno"
},
"Creepy Creek": {
"name": "Creepy Creek",
"desc": "Scarta una carta di picche per derubare il treno"
},
"Crowns Hole": {
"name": "Crown's Hole",
"desc": "Scarta una birra per derubare il treno"
},
"Deadwood": {
"name": "Deadwood",
"desc": "Scarta una carta equipaggiamento per derubare il treno"
},
"Dodgeville": {
"name": "Dodgeville",
"desc": "Scarta un Mancato! per derubare il treno"
},
"Fort Worth": {
"name": "Fort Worth",
"desc": "Scarta una carta con il numero 10, J, Q, K, A per derubare il treno"
},
"Frisco": {
"name": "Frisco",
"desc": "Scarta una carta di fiori per derubare il treno"
},
"Miners Oath": {
"name": "Miner's Oath",
"desc": "Scarta una carta di quadri per derubare il treno"
},
"San Tafe": {
"name": "San Tafe",
"desc": "Scarta una carta di cuori per derubare il treno"
},
"Tombrock": {
"name": "Tombrock",
"desc": "Per derubare il treno, perdi 1 punto vita"
},
"Yooma": {
"name": "Yooma",
"desc": "Scarta una carta con un numero compreso tra 2 e 9 per derubare il treno"
},
"Virginia Town": {
"name": "Virginia Town",
"desc": "Scarta due carte per derubare il treno"
},
"Ironhorse": {
"name": "Ironhorse",
"desc": "LOCOMOTIVA: Ogni giocatore, incluso colui che ha attivato l'effetto, è bersaglio di un BANG!\nNessun giocatore è responsabile dell'eventuale perdita di punti vita.\nSe tutti i giocatori vengono eliminati allo stesso tempo, i Fuorilegge vincono."
},
"Leland": {
"name": "Leland",
"desc": "LOCOMOTIVA: svolgi l'effetto dell'Emporio, cominciando dal giocatore di turno e procedendo in senso orario."
},
"Baggage Car": {
"name": "Baggage Car",
"desc": "Scartalo: ottieni l'effetto di un Mancato!, Panico!, Cat Balou o di un BANG! extra.\nDiscard this for a Missed!Panic!, Cat Balou, or an extra BANG!"
},
"Caboose": {
"name": "Caboose",
"desc": "Pro scartare un aura tua carta bordo bin incuso un vagone come se fosse un Mancato!"
},
"Cattle Truck": {
"name": "Cattle Truck",
"desc": "Scartalo: guarda le 3 carte in cima agli scarti e pescane I"
},
"Circus Wagon": {
"name": "Circus Wagon",
"desc": "Scartalo: ogni altro giocatore deve scartare una carta che ha in gioco."
},
"Coal Hopper": {
"name": "Coal Hopper",
"desc": "Scartalo: pesca una carta e scarta un vagone in gioco davanti a un giocatore a ma scelta."
},
"Dining Car": {
"name": "Dining Car",
"desc": "A inizio turno, \"estrai!\": se è Cuori, recuperi I punto vita."
},
"Express Car": {
"name": "Express Car",
"desc": "Scarta tutte le carte in mano, poi gioca un altro turno"
},
"Ghost Car": {
"name": "Ghost Car",
"desc": "Giocalo su chiunque tranne lo Sceritfo. Se vieni eliminato, invece resti in gioco, ma non puol guada nare ne perdere punk vita."
},
"Lounge Car": {
"name": "Lounge Car",
"desc": "Scartalo: pesca 2 vagoni dal mazzo, mettine I in gioco di fronte a te e 1 di fronte a un altro giocatore."
},
"Lumber Flatcar": {
"name": "Lumber Flatcar",
"desc": "Giocalo su un qualsiasi giocatore (compreso te). Finché questa carta è in gioco, quel giocatore vede gli altri giocatori a distanza aumentata di 1."
},
"Mail Car": {
"name": "Mail Car",
"desc": "Scartalo: pesca 3 carte e dai 1 di esse a un altro giocatore a tua scelta."
},
"Observation Car": {
"name": "Observation Car",
"desc": "Tu vedi gli altri a distanza -1. Gli altri a vedono a distanza +1."
},
"Passenger Car": {
"name": "Passenger Car",
"desc": "Scartalo: pesca una carta (in mano o in gioco) da un altro giocatore"
},
"Prisoner Car": {
"name": "Prisoner Car",
"desc": "Le carte Duello e Indiani! giocate dagli altri giocatori non hanno effetto su di te."
},
"Private Car": {
"name": "Private Car",
"desc": "se non hai carte in mano. non puoi essere bersaelio di carte BANG"
},
"Sleeper Car": {
"name": "Sleeper Car",
"desc": "Una volta per turno, puoi scartare un'altra tua carta a bordo blu incluso."
} }
}, },
"help": { "help": {

View File

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

View File

@ -28,13 +28,19 @@ export const expansionsMap = {
icon: '👻', icon: '👻',
back: true, back: true,
expansion: 'the-valley-of-shadows', expansion: 'the-valley-of-shadows',
status: 'beta',
}, },
'wild_west_show': { 'wild_west_show': {
name: 'Wild West Show', name: 'Wild West Show',
icon: '🎪', icon: '🎪',
back: true, back: true,
expansion: 'wild-west-show', expansion: 'wild-west-show',
status: 'alpha', status: 'beta',
},
'train_robbery': {
name: 'The Great Train Robbery',
icon: '🚂',
back: true,
expansion: 'train-roobbery',
status: 'wip',
} }
} }

File diff suppressed because it is too large Load Diff