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

View File

@ -1,15 +1,17 @@
from typing import List, Set, Dict, Tuple, Optional, TYPE_CHECKING
import random
import bang.cards as cs
import bang.expansions.fistful_of_cards.card_events as ce
import bang.expansions.high_noon.card_events as ceh
import bang.expansions.wild_west_show.card_events as cew
import bang.expansions.wild_west_show.characters as chw
import bang.expansions.gold_rush.shop_cards as grc
import bang.expansions.train_robbery.stations as trs
import bang.expansions.train_robbery.trains as trt
from globals import G
if TYPE_CHECKING:
from bang.game import Game
from bang.players import Player
class Deck:
@ -35,6 +37,9 @@ class Deck:
self.game = game
self.event_cards: 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] = []
if "fistful_of_cards" in game.expansions:
self.event_cards.extend(ce.get_all_events(game.rng))
@ -47,6 +52,12 @@ class Deck:
game.rng.shuffle(self.event_cards_wildwestshow)
self.event_cards_wildwestshow.insert(0, None)
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:
game.rng.shuffle(self.event_cards)
self.event_cards = self.event_cards[:12]
@ -106,12 +117,21 @@ class Deck:
self.shop_cards[i].reset_card()
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:
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:
return self.scrap_pile[-1]
return self.scrap_pile[-n_cards:]
else:
return None
@ -176,7 +196,7 @@ class Deck:
else:
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:
return
card.reset_card()

View File

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

View File

@ -1,7 +1,7 @@
from bang.cards import *
import bang.roles as r
import bang.players as pl
from globals import G
from globals import G, PendingAction
class ShopCardKind(IntEnum):
BROWN = 0 # Se lequipaggiamento ha il bordo marrone, applicane subito leffetto e poi scartalo.
@ -47,7 +47,7 @@ class Bicchierino(ShopCard):
'is_player': True
} for p in player.game.get_alive_players()]
player.choose_text = 'choose_bicchierino'
player.pending_action = pl.PendingAction.CHOOSE
player.pending_action = PendingAction.CHOOSE
player.notify_self()
return super().play_card(player, against, _with)
@ -64,7 +64,7 @@ class Bottiglia(ShopCard):
for i in range(len(player.available_cards)):
player.available_cards[i].must_be_used = True
player.choose_text = 'choose_bottiglia'
player.pending_action = pl.PendingAction.CHOOSE
player.pending_action = PendingAction.CHOOSE
player.notify_self()
return super().play_card(player, against, _with)
@ -79,7 +79,7 @@ class Complice(ShopCard):
for i in range(len(player.available_cards)):
player.available_cards[i].must_be_used = True
player.choose_text = 'choose_complice'
player.pending_action = pl.PendingAction.CHOOSE
player.pending_action = PendingAction.CHOOSE
player.notify_self()
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)]
player.available_cards.append({'name': player.name, 'number':0,'icon': 'you', 'is_character': True})
player.choose_text = 'choose_ricercato'
player.pending_action = pl.PendingAction.CHOOSE
player.pending_action = PendingAction.CHOOSE
player.notify_self()
return True
# 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
from bang.cards import Card, Suit, Bang, Mancato
import bang.expansions.fistful_of_cards.card_events as ce
from globals import G
from globals import G, PendingAction
class Fantasma(Card):
@ -15,7 +15,7 @@ class Fantasma(Card):
if (player.game.check_event(ce.IlGiudice)) or not self.can_be_used_now:
return False
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.available_cards = [
{
@ -60,11 +60,7 @@ class Lemat(Card):
for p in player.game.get_visible_players(player)
)
):
from bang.players import PendingAction
player.available_cards = player.hand.copy()
player.pending_action = PendingAction.CHOOSE
player.choose_text = "choose_play_as_bang"
player.set_choose_action("choose_play_as_bang", player.hand.copy())
player.notify_self()
return False
@ -185,7 +181,7 @@ class Sventagliata(
if p["name"] != player.name and p["name"] != t.name and p["dist"]
]
if len(player.available_cards) > 0:
player.pending_action = pl.PendingAction.CHOOSE
player.pending_action = PendingAction.CHOOSE
player.choose_text = "choose_sventagliata"
else:
player.available_cards = []

View File

@ -21,12 +21,11 @@ class BlackFlower(Character):
for p in player.game.get_visible_players(player)
)
) 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.pending_action = PendingAction.CHOOSE
player.choose_text = "choose_play_as_bang"
player.set_choose_action(
"choose_play_as_bang",
[c for c in player.hand if c.suit == cs.Suit.CLUBS],
)
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.characters as grch
import bang.expansions.the_valley_of_shadows.cards as tvosc
import bang.expansions.train_robbery.trains as trt
from metrics import Metrics
from globals import G
from globals import G, PendingAction
debug_commands = [
@ -57,8 +58,11 @@ debug_commands = [
"help": "Remove a card from hand/equip - sample /removecard 0",
},
{"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": "/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": "/cardinfo", "help": "Get card data - sample /cardinfo handindex"},
{"cmd": "/mebot", "help": "Toggles bot mode"},
@ -93,6 +97,7 @@ class Game:
"gold_rush",
"the_valley_of_shadows",
"wild_west_show",
"train_robbery",
]
self.shutting_down = False
self.is_competitive = False
@ -354,6 +359,7 @@ class Game:
roles_str += f"|{role}|{str(current_roles.count(role))}"
G.sio.emit("chat_message", room=self.name, data=f"_allroles{roles_str}")
self.play_turn()
self.notify_stations()
def choose_characters(self):
n = self.characters_to_distribute
@ -459,7 +465,7 @@ class Game:
def discard_others(self, attacker: pl.Player, card_name: str = None):
self.attack_in_progress = True
attacker.pending_action = pl.PendingAction.WAIT
attacker.pending_action = PendingAction.WAIT
attacker.notify_self()
self.waiting_for = 0
self.ready_count = 0
@ -469,7 +475,7 @@ class Game:
self.waiting_for += 1
p.notify_self()
if self.waiting_for == 0:
attacker.pending_action = pl.PendingAction.PLAY
attacker.pending_action = PendingAction.PLAY
attacker.notify_self()
self.attack_in_progress = False
elif card_name == "Poker":
@ -477,7 +483,7 @@ class Game:
def attack_others(self, attacker: pl.Player, card_name: str = None):
self.attack_in_progress = True
attacker.pending_action = pl.PendingAction.WAIT
attacker.pending_action = PendingAction.WAIT
attacker.notify_self()
self.waiting_for = 0
self.ready_count = 0
@ -487,7 +493,7 @@ class Game:
self.waiting_for += 1
p.notify_self()
if self.waiting_for == 0:
attacker.pending_action = pl.PendingAction.PLAY
attacker.pending_action = PendingAction.PLAY
attacker.notify_self()
self.attack_in_progress = False
if self.pending_winners and not self.someone_won:
@ -495,7 +501,7 @@ class Game:
def indian_others(self, attacker: pl.Player):
self.attack_in_progress = True
attacker.pending_action = pl.PendingAction.WAIT
attacker.pending_action = PendingAction.WAIT
attacker.notify_self()
self.waiting_for = 0
self.ready_count = 0
@ -505,7 +511,7 @@ class Game:
self.waiting_for += 1
p.notify_self()
if self.waiting_for == 0:
attacker.pending_action = pl.PendingAction.PLAY
attacker.pending_action = PendingAction.PLAY
attacker.notify_self()
self.attack_in_progress = False
if self.pending_winners and not self.someone_won:
@ -542,26 +548,26 @@ class Game:
self.attack_in_progress = True
self.ready_count = 0
self.waiting_for = 1
attacker.pending_action = pl.PendingAction.WAIT
attacker.pending_action = PendingAction.WAIT
attacker.notify_self()
self.get_player_named(target_username).notify_self()
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):
p = self.get_player_named(target_username)
if p != attacker and p.get_discarded(
attacker,
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.waiting_for = 1
attacker.pending_action = pl.PendingAction.WAIT
attacker.pending_action = PendingAction.WAIT
attacker.notify_self()
self.get_player_named(target_username).notify_self()
else:
attacker.pending_action = pl.PendingAction.CHOOSE
attacker.pending_action = PendingAction.CHOOSE
attacker.target_p = target_username
if isinstance(card, cs.CatBalou):
attacker.choose_action = "discard"
@ -575,7 +581,7 @@ class Game:
):
self.ready_count = 0
self.waiting_for = 1
attacker.pending_action = pl.PendingAction.WAIT
attacker.pending_action = PendingAction.WAIT
attacker.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):
self.ready_count = 0
self.waiting_for = 1
attacker.pending_action = pl.PendingAction.WAIT
attacker.pending_action = PendingAction.WAIT
attacker.notify_self()
self.get_player_named(target_username).notify_self()
def emporio(self):
pls = self.get_alive_players()
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].available_cards = self.available_cards
G.sio.emit(
@ -612,7 +618,7 @@ class Game:
)
player.hand.append(card)
player.available_cards = []
player.pending_action = pl.PendingAction.WAIT
player.pending_action = PendingAction.WAIT
player.notify_self()
pls = self.get_alive_players()
next_player = pls[
@ -631,14 +637,14 @@ class Game:
next_player.hand.append(self.available_cards.pop())
next_player.notify_self()
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()
elif next_player == self.players[self.turn]:
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()
else:
next_player.pending_action = pl.PendingAction.CHOOSE
next_player.pending_action = PendingAction.CHOOSE
next_player.choose_text = "choose_card_to_get"
next_player.available_cards = self.available_cards
G.sio.emit(
@ -724,7 +730,7 @@ class Game:
elif self.poker_on and not any(
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.turn
].choose_text = f"choose_from_poker;{min(2, tmp)}"
@ -735,10 +741,10 @@ class Game:
print("attack completed, next attack")
atk = self.attack_queue.pop(0)
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()
else:
self.players[self.turn].pending_action = pl.PendingAction.PLAY
self.players[self.turn].pending_action = PendingAction.PLAY
self.poker_on = False
self.players[self.turn].notify_self()
@ -842,6 +848,7 @@ class Game:
)
):
self.deck.flip_event()
self.deck.move_train_forward()
if self.check_event(ce.RouletteRussa):
self.is_russian_roulette_on = True
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__),
)
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):
print(f"{self.name}: scrap")
room = self.name if sid is None else sid
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:
G.sio.emit("scrap", room=room, data=None)
@ -1015,9 +1037,9 @@ class Game:
self.deck.draw(True, player=player.attacker)
player.attacker.notify_self()
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()
player.pending_action = pl.PendingAction.WAIT
player.pending_action = PendingAction.WAIT
if player.is_dead:
return
@ -1220,6 +1242,29 @@ class Game:
def get_alive_players(self):
return [p for p in self.players if not p.is_dead or p.is_ghost]
def get_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):
return [
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 enum import IntEnum
import json
from random import random, randrange, sample, uniform, randint
import socketio
import bang.deck as deck
from random import randrange, sample, uniform, randint
import bang.roles as r
import bang.cards as cs
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.the_valley_of_shadows.cards as tvosc
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 globals import G
from globals import G, PendingAction
import sys
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:
def is_admin(self):
return self.discord_id in {"244893980960096266", "539795574019457034"}
@ -102,6 +92,7 @@ class Player:
self.is_bot = bot
self.discord_token = discord_token
self.discord_id = None
self.did_choose_callback = None
self.played_cards = 0
self.avatar = ""
self.last_played_card: cs.Card = None
@ -280,25 +271,25 @@ class Player:
if self.pending_action == PendingAction.DRAW and self.game.check_event(
ce.Peyote
):
self.available_cards = [
{"icon": "🔴", "noDesc": True},
{"icon": "", "noDesc": True},
]
self.is_drawing = True
self.choose_text = "choose_guess"
self.pending_action = PendingAction.CHOOSE
self.set_choose_action(
"choose_guess",
[
{"icon": "🔴", "noDesc": True},
{"icon": "", "noDesc": True},
],
)
elif (
self.can_play_ranch
and self.pending_action == PendingAction.PLAY
and self.game.check_event(ce.Ranch)
):
self.can_play_ranch = False
self.available_cards = [c for c in self.hand]
self.discarded_cards = []
self.available_cards.append({"icon": "", "noDesc": True})
self.is_playing_ranch = True
self.choose_text = "choose_ranch"
self.pending_action = PendingAction.CHOOSE
self.set_choose_action(
"choose_ranch", [c for c in self.hand] + [{"icon": "", "noDesc": True}]
)
elif (
self.character
and self.character.check(self.game, chars.SuzyLafayette)
@ -338,9 +329,7 @@ class Player:
self.game.players[self.game.turn].notify_self()
self.scrapped_cards = 0
self.previous_pending_action = self.pending_action
self.pending_action = PendingAction.CHOOSE
self.choose_text = "choose_sid_scrap"
self.available_cards = self.hand
self.set_choose_action("choose_sid_scrap", self.hand)
self.lives += 1
ser = self.__dict__.copy()
@ -748,6 +737,9 @@ class Player:
self.has_played_bang = False
self.special_use_count = 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):
hand = len(self.hand)
for _ in range(hand):
@ -782,35 +774,36 @@ class Player:
for p in self.game.get_alive_players()
)
):
self.available_cards = [
{
"name": p.name,
"icon": p.role.icon
if (self.game.initial_players == 3)
else "⭐️"
if isinstance(p.role, r.Sheriff)
else "🤠",
"alt_text": "".join(["❤️"] * p.lives)
+ "".join(["💀"] * (p.max_lives - p.lives)),
"avatar": p.avatar,
"is_character": True,
"is_player": True,
}
for p in self.game.get_alive_players()
if p != self and p.lives < p.max_lives
]
self.available_cards.append({"icon": "", "noDesc": True})
self.choose_text = "choose_fratelli_di_sangue"
self.pending_action = PendingAction.CHOOSE
self.set_choose_action(
"choose_fratelli_di_sangue",
[
{
"name": p.name,
"icon": p.role.icon
if (self.game.initial_players == 3)
else "⭐️"
if isinstance(p.role, r.Sheriff)
else "🤠",
"alt_text": "".join(["❤️"] * p.lives)
+ "".join(["💀"] * (p.max_lives - p.lives)),
"avatar": p.avatar,
"is_character": True,
"is_player": True,
}
for p in self.game.get_alive_players()
if p != self and p.lives < p.max_lives
]
+ [{"icon": "", "noDesc": True}],
)
self.is_giving_life = True
elif (
self.game.check_event(ceh.NuovaIdentita)
and self.not_chosen_character is not None
and not again
):
self.available_cards = [self.character, self.not_chosen_character]
self.choose_text = "choose_nuova_identita"
self.pending_action = PendingAction.CHOOSE
self.set_choose_action(
"choose_nuova_identita", [self.character, self.not_chosen_character]
)
elif not self.game.check_event(ce.Lazo) and any(
(
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
):
self.is_using_checchino = True
self.available_cards = [
{
"name": p["name"],
"icon": p["role"].icon
if (self.game.initial_players == 3)
else "⭐️"
if p["is_sheriff"]
else "🤠",
"alt_text": "".join(["❤️"] * p["lives"])
+ "".join(["💀"] * (p["max_lives"] - p["lives"])),
"is_character": True,
"is_player": True,
}
for p in self.game.get_visible_players(self)
if p["dist"] <= self.get_sight()
]
self.available_cards.append({"icon": "", "noDesc": True})
self.choose_text = "choose_cecchino"
self.pending_action = PendingAction.CHOOSE
self.set_choose_action(
"choose_cecchino",
[
{
"name": p["name"],
"icon": p["role"].icon
if (self.game.initial_players == 3)
else "⭐️"
if p["is_sheriff"]
else "🤠",
"alt_text": "".join(["❤️"] * p["lives"])
+ "".join(["💀"] * (p["max_lives"] - p["lives"])),
"is_character": True,
"is_player": True,
}
for p in self.game.get_visible_players(self)
if p["dist"] <= self.get_sight()
]
+ [{"icon": "", "noDesc": True}],
)
self.notify_self()
if (
self.is_my_turn
@ -884,15 +878,15 @@ class Player:
self.notify_self()
elif self.character.check(self.game, chars.KitCarlson) and not self.is_ghost:
self.is_drawing = True
self.available_cards = [self.game.deck.draw() for i in range(3)]
self.choose_text = "choose_card_to_get"
self.pending_action = PendingAction.CHOOSE
self.set_choose_action(
"choose_card_to_get", [self.game.deck.draw() for i in range(3)]
)
self.notify_self()
elif self.character.check(self.game, grch.DutchWill) and not self.is_ghost:
self.is_drawing = True
self.available_cards = [self.game.deck.draw() for i in range(2)]
self.choose_text = "choose_card_to_get"
self.pending_action = PendingAction.CHOOSE
self.set_choose_action(
"choose_card_to_get", [self.game.deck.draw() for i in range(2)]
)
self.notify_self()
elif (
self.character.check(self.game, chd.PatBrennan)
@ -902,10 +896,10 @@ class Player:
and len(self.game.get_player_named(pile).equipment) > 0
):
self.is_drawing = True
self.available_cards = self.game.get_player_named(pile).equipment
self.pat_target = pile
self.choose_text = "choose_card_to_get"
self.pending_action = PendingAction.CHOOSE
self.set_choose_action(
"choose_card_to_get", self.game.get_player_named(pile).equipment
)
self.notify_self()
else:
self.pending_action = PendingAction.PLAY
@ -1004,12 +998,13 @@ class Player:
def manette(self):
if self.game.check_event(ceh.Manette):
self.choose_text = "choose_manette"
self.available_cards = [
{"name": "", "icon": "♦♣♥♠"[s], "alt_text": "", "noDesc": True}
for s in [0, 1, 2, 3]
]
self.pending_action = PendingAction.CHOOSE
self.set_choose_action(
"choose_manette",
[
{"name": "", "icon": "♦♣♥♠"[s], "alt_text": "", "noDesc": True}
for s in [0, 1, 2, 3]
],
)
def pick(self):
if self.pending_action != PendingAction.PICK:
@ -1399,6 +1394,9 @@ class Player:
self.target_p = self.rissa_targets.pop(0).name
print(f"rissa targets: {self.rissa_targets}")
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":
player = self.game.get_player_named(
self.available_cards[card_index]["name"]
@ -1871,6 +1869,8 @@ class Player:
self.expected_response.append(cs.Bang(0, 0).name)
if self.character.check(self.game, chw.BigSpencer):
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.notify_self()
@ -1933,7 +1933,7 @@ class Player:
self.notify_self()
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.available_cards = self.hand.copy()
if card_name == "Tornado":
@ -1946,6 +1946,13 @@ class Player:
self.available_cards.append(
{"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
else:
if self.can_escape(card_name) or self.character.check(
@ -1995,6 +2002,17 @@ class Player:
self.attacker = attacker
self.attacking_card = card_name
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 (
isinstance(attacker, Player)
and attacker.character.check(self.game, tvosch.ColoradoBill)
@ -2100,9 +2118,7 @@ class Player:
if len(equipments) == 0:
return False
else:
self.choose_text = "choose_dalton"
self.pending_action = PendingAction.CHOOSE
self.available_cards = equipments
self.set_choose_action("choose_dalton", equipments)
return True
def get_indians(self, attacker):
@ -2112,6 +2128,11 @@ class Player:
(isinstance(c, grc.Calumet) for c in self.gold_rush_equipment)
):
return False
# check for trt.PrisonerCar
if any(
(isinstance(c, trt.PrisonerCar) for c in self.equipment)
):
return False
if (
not self.game.is_competitive
and not any(
@ -2145,6 +2166,11 @@ class Player:
def get_dueled(self, attacker):
self.attacker = attacker
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 (
not self.game.is_competitive
and not any(
@ -2501,26 +2527,29 @@ class Player:
self.character.special(self, data)
def gold_rush_discard(self):
self.available_cards = [
{
"name": p.name,
"icon": p.role.icon
if (self.game.initial_players == 3)
else "⭐️"
if isinstance(p.role, r.Sheriff)
else "🤠",
"is_character": True,
"avatar": p.avatar,
"alt_text": "".join(["🎴️"] * len(p.gold_rush_equipment)),
"is_player": True,
}
for p in self.game.get_alive_players()
if p != self
and any((e.number + 1 <= self.gold_nuggets for e in p.gold_rush_equipment))
]
self.available_cards.append({"icon": "", "noDesc": True})
self.choose_text = "gold_rush_discard"
self.pending_action = PendingAction.CHOOSE
self.set_choose_action(
"gold_rush_discard",
[
{
"name": p.name,
"icon": p.role.icon
if (self.game.initial_players == 3)
else "⭐️"
if isinstance(p.role, r.Sheriff)
else "🤠",
"is_character": True,
"avatar": p.avatar,
"alt_text": "".join(["🎴️"] * len(p.gold_rush_equipment)),
"is_player": True,
}
for p in self.game.get_alive_players()
if p != self
and any(
(e.number + 1 <= self.gold_nuggets for e in p.gold_rush_equipment)
)
]
+ [{"icon": "", "noDesc": True}],
)
self.notify_self()
def buy_gold_rush_card(self, index):
@ -2548,8 +2577,59 @@ class Player:
self.game.deck.shop_deck.append(card)
self.game.deck.shop_cards[index] = None
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()
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):
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:
@ -2659,6 +2739,8 @@ class Player:
and not self.equipment[i].can_be_used_now
):
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)):
if self.hand[i].must_be_used:
self.hand[i].must_be_used = False

View File

@ -1,6 +1,17 @@
from enum import IntEnum
class G:
sio = None
def __init__(self):
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 bang.game import Game
from bang.players import PendingAction, Player
from globals import G
from bang.players import Player
from globals import G, PendingAction
from metrics import Metrics
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_all()
room.notify_gold_rush_shop()
room.notify_stations()
room.notify_event_card()
room.notify_event_card_wildwestshow(sid)
else:
@ -678,6 +679,14 @@ def buy_gold_rush_card(sid, data: int):
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
@bang_handler
def chat_message(sid, msg, pl=None):
@ -1029,7 +1038,9 @@ def chat_message(sid, msg, pl=None):
cmd = msg.split()
if len(cmd) >= 2:
import bang.expansions.train_robbery.trains as trt
cards = cs.get_starting_deck(ses.game.expansions)
cards.extend(trt.get_all_cards())
card_names = " ".join(cmd[1:]).split(",")
for cn in card_names:
ses.equipment.append(
@ -1088,6 +1099,21 @@ def chat_message(sid, msg, pl=None):
"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():
sio.emit("mount_status", room=sid)
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__)
)
@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
@bang_handler

View File

@ -1,3 +1,4 @@
from typing import Any, List
import pytest
from bang.characters import Character
from bang.game import Game
@ -8,7 +9,7 @@ from globals import G
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.expansions = expansions
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
def set_events(g: Game, event_cards):
def set_events(g: Game, event_cards) -> None:
g.deck.event_cards = event_cards
def current_player(g: Game):
def current_player(g: Game) -> Player:
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)]
def current_player_with_cards(g: Game, cards):
def current_player_with_cards(g: Game, cards: List[Any]) -> Player:
p = current_player(g)
p.draw("")
p.hand = cards

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,8 @@
from bang.characters import Character
from bang.deck import Deck
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.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.deck import Deck
from bang.game import Game
from bang.players import Player, PendingAction
from bang.players import Player
import bang.cards as cs
from globals import PendingAction
# 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.deck import Deck
from bang.game import Game
from bang.players import Player, PendingAction
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

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.cards import Card, Suit
import bang.roles as roles
from bang.players import PendingAction
from globals import PendingAction
# 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.cards import Card, Suit
import bang.roles as roles
from bang.players import PendingAction
from globals import PendingAction
# test Camposanto

View File

@ -11,6 +11,7 @@
'gold-rush': card.expansion === 'gold_rush',
brown: card.kind === 0,
black: card.kind === 1,
'train-piece': card.type && card.type === 'train',
}"
>
<h4>{{ cardName }}</h4>
@ -64,7 +65,7 @@ export default {
},
suit() {
if (this.card && !isNaN(this.card.suit)) {
let x = ["♦️", "♣️", "♥️", "♠️", "🤑"];
let x = ["♦️", "♣️", "♥️", "♠️", "🤑", "🚂"];
return x[this.card.suit];
} else if (this.card.suit) {
return this.card.suit;
@ -115,7 +116,7 @@ export default {
#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: "";
background-image: radial-gradient(var(--bg-color) 13%, #0000 5%),
radial-gradient(var(--bg-color) 14%, transparent 5%),
@ -231,6 +232,18 @@ export default {
padding: 4pt;
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 {
position: absolute;
width: 36pt;
@ -338,6 +351,7 @@ export default {
}
.cant-play {
filter: brightness(0.5);
cursor: not-allowed;
}
.expansion {
position: absolute;
@ -347,4 +361,24 @@ export default {
border-radius: 100%;
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>

View File

@ -21,7 +21,8 @@
@click="fillCmd(msg.cmd)">{{msg.cmd}} <i class="std-text" style="font-size:8pt;">{{msg.help}}</i></p>
</div>
<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')"/>
</form>
</div>
@ -164,6 +165,12 @@ export default {
this.text = cmd;
document.getElementById('my-msg').focus();
},
tabComplete() {
if (this.commandSuggestion.length > 0) {
let cmd = this.commandSuggestion[0].cmd;
this.text = cmd + ' ';
}
},
playEffects(path) {
const promise = (new Audio(path)).play();
if(promise !== undefined){

View File

@ -1,7 +1,7 @@
<template>
<div >
<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[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)}"/>
@ -11,6 +11,10 @@
<card :card="goldRushCardBack" :donotlocalize="true" class="gold-rush back last-event" @click.native="goldRushShopOpen = !goldRushShopOpen"/>
</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 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;"/>
@ -38,15 +42,14 @@
</div>
<transition name="list">
<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>
</transition>
<transition name="list">
<div v-if="goldRushDesc">
<p class="center-stuff">🤑 <i>{{$t(`cards.${goldRushDesc.name}.desc`)}}</i> 🤑</p>
<p class="center-stuff">🤑 <b>{{goldRushDesc.number - gold_rush_discount}} 💵</b> 🤑</p>
</div>
<div v-if="stationDesc">
<p class="center-stuff"><i>{{stationDesc}}</i></p>
</div>
</transition>
<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>
@ -59,6 +62,7 @@
<script>
import Card from '@/components/Card.vue'
import StationCard from '@/components/StationCard.vue'
export default {
name: 'Deck',
@ -67,6 +71,7 @@ export default {
},
components: {
Card,
StationCard
},
data: () => ({
card: {
@ -88,8 +93,11 @@ export default {
goldRushCards: [],
gold_nuggets: 0,
goldRushDesc: null,
stationDesc: null,
can_gold_rush_discard: false,
gold_rush_discount: 0,
currentStations: [],
currentTrain: [],
}),
sockets: {
self(self){
@ -123,6 +131,11 @@ export default {
console.log('GOLD RUSH:'+ cards)
this.goldRushCards = JSON.parse(cards)
},
stations(stations) {
let msg = JSON.parse(stations)
this.currentStations = msg.stations
this.currentTrain = msg.current_train
},
},
computed: {
endTurnCard() {
@ -163,6 +176,15 @@ export default {
},
},
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) {
if (this.pending_action !== false && this.pending_action < 2) {
// console.log('action')
@ -189,6 +211,14 @@ export default {
setGoldRushDesc(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() {
if (window.innerWidth < 1000) {

View File

@ -127,15 +127,32 @@
</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>
</template>
<script>
import Card from '@/components/Card.vue'
import StationCard from './StationCard.vue'
export default {
name: 'Help',
components: {
Card,
StationCard,
},
props: {
inGame: Boolean
@ -152,6 +169,8 @@ export default {
goldrushcards: [],
valleyofshadowscards: [],
wildwestshowcards: [],
trainrobberycards: [],
trainrobberystations: [],
}),
computed: {
endTurnCard() {
@ -196,6 +215,14 @@ export default {
...x,
}))
},
trainrobberycards_info(cardsJson) {
this.trainrobberycards = JSON.parse(cardsJson).cards.map(x=>({
...x,
}))
this.trainrobberystations = JSON.parse(cardsJson).stations.map(x=>({
...x,
}))
},
},
mounted() {
this.$socket.emit('get_cards')
@ -205,6 +232,7 @@ export default {
this.$socket.emit('get_goldrushcards')
this.$socket.emit('get_valleyofshadowscards')
this.$socket.emit('get_wildwestshowcards')
this.$socket.emit('get_trainrobberycards')
document.getElementById('help').scrollIntoView();
}
}

View File

@ -646,8 +646,15 @@ export default {
console.log("card_scrapped no deck");
return;
}
let decelOffset = cumulativeOffset(decel);
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) {
console.log("card_scrapped no phand");
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",
"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!",
"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": {
"spectators": " | A spectator is watching the game | {n} spectators are watching the game",
"chat": "Chat",
@ -153,7 +158,9 @@
"choose_emporio": ";{0}; has chosen ;{1}; from General Store.",
"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};",
"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": {
"leggedelwest": "He must play this card on this turn if possible."
@ -855,6 +862,130 @@
"Wild West Show": {
"name": "Wild West Show",
"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": {
@ -906,7 +1037,9 @@
"highnooncards": "High Noon - Event Cards",
"foccards": "Fistful of Cards - Event 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": {
"sepia": "Sepia",

View File

@ -153,7 +153,8 @@
"choose_emporio": ";{0}; ha scelto ;{1}; da Emporio.",
"shotgun_scrap": "Quando lo shotgun ha colpito ;{0}; gli ha tolto una carta (;{1};)",
"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": {
"leggedelwest": "Ed è obbligato a usarla nel suo turno, se possibile"
@ -855,6 +856,130 @@
"Wild West Show": {
"name": "Wild West Show",
"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": {

View File

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

View File

@ -28,13 +28,19 @@ export const expansionsMap = {
icon: '👻',
back: true,
expansion: 'the-valley-of-shadows',
status: 'beta',
},
'wild_west_show': {
name: 'Wild West Show',
icon: '🎪',
back: true,
expansion: 'wild-west-show',
status: 'alpha',
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