diff --git a/backend/bang/cards.py b/backend/bang/cards.py index 3406a0e..0a3d03c 100644 --- a/backend/bang/cards.py +++ b/backend/bang/cards.py @@ -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 ( diff --git a/backend/bang/deck.py b/backend/bang/deck.py index 7bd7fda..c671ae0 100644 --- a/backend/bang/deck.py +++ b/backend/bang/deck.py @@ -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() diff --git a/backend/bang/expansions/dodge_city/characters.py b/backend/bang/expansions/dodge_city/characters.py index 81db24f..1f287e3 100644 --- a/backend/bang/expansions/dodge_city/characters.py +++ b/backend/bang/expansions/dodge_city/characters.py @@ -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 diff --git a/backend/bang/expansions/gold_rush/shop_cards.py b/backend/bang/expansions/gold_rush/shop_cards.py index 5ff68c9..e75a3ae 100644 --- a/backend/bang/expansions/gold_rush/shop_cards.py +++ b/backend/bang/expansions/gold_rush/shop_cards.py @@ -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 l’equipaggiamento ha il bordo marrone, applicane subito l’effetto 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 diff --git a/backend/bang/expansions/the_valley_of_shadows/cards.py b/backend/bang/expansions/the_valley_of_shadows/cards.py index b178257..9a9ca52 100644 --- a/backend/bang/expansions/the_valley_of_shadows/cards.py +++ b/backend/bang/expansions/the_valley_of_shadows/cards.py @@ -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 = [] diff --git a/backend/bang/expansions/the_valley_of_shadows/characters.py b/backend/bang/expansions/the_valley_of_shadows/characters.py index cdea867..af771a8 100644 --- a/backend/bang/expansions/the_valley_of_shadows/characters.py +++ b/backend/bang/expansions/the_valley_of_shadows/characters.py @@ -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() diff --git a/backend/bang/expansions/train_robbery/cards.py b/backend/bang/expansions/train_robbery/cards.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/bang/expansions/train_robbery/characters.py b/backend/bang/expansions/train_robbery/characters.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/bang/expansions/train_robbery/stations.py b/backend/bang/expansions/train_robbery/stations.py new file mode 100644 index 0000000..0d5bfc2 --- /dev/null +++ b/backend/bang/expansions/train_robbery/stations.py @@ -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(), + ] diff --git a/backend/bang/expansions/train_robbery/trains.py b/backend/bang/expansions/train_robbery/trains.py new file mode 100644 index 0000000..27b356d --- /dev/null +++ b/backend/bang/expansions/train_robbery/trains.py @@ -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, +# l’abilità di Evan Babbit, e così via. +# Sleeper Car: puoi anche usare +# l’effetto 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 diff --git a/backend/bang/game.py b/backend/bang/game.py index c0bad29..919e6e2 100644 --- a/backend/bang/game.py +++ b/backend/bang/game.py @@ -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) diff --git a/backend/bang/players.py b/backend/bang/players.py index a4fc9aa..3900c46 100644 --- a/backend/bang/players.py +++ b/backend/bang/players.py @@ -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 diff --git a/backend/globals.py b/backend/globals.py index 2c31932..3d4f607 100644 --- a/backend/globals.py +++ b/backend/globals.py @@ -1,6 +1,17 @@ +from enum import IntEnum + class G: sio = None def __init__(self): - pass \ No newline at end of file + pass + + +class PendingAction(IntEnum): + PICK = 0 + DRAW = 1 + PLAY = 2 + RESPOND = 3 + WAIT = 4 + CHOOSE = 5 diff --git a/backend/server.py b/backend/server.py index 3208915..12bfabb 100644 --- a/backend/server.py +++ b/backend/server.py @@ -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 diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py index 3e57ac0..061082d 100644 --- a/backend/tests/__init__.py +++ b/backend/tests/__init__.py @@ -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 diff --git a/backend/tests/cards_test.py b/backend/tests/cards_test.py index 67732f7..91cdacc 100644 --- a/backend/tests/cards_test.py +++ b/backend/tests/cards_test.py @@ -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(): diff --git a/backend/tests/character_test.py b/backend/tests/character_test.py index 3963f97..fdc5ad5 100644 --- a/backend/tests/character_test.py +++ b/backend/tests/character_test.py @@ -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(): diff --git a/backend/tests/dodge_city_test.py b/backend/tests/dodge_city_test.py index 5d7ac38..b2bbb8a 100644 --- a/backend/tests/dodge_city_test.py +++ b/backend/tests/dodge_city_test.py @@ -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 diff --git a/backend/tests/game_test.py b/backend/tests/game_test.py index 280138c..5ae066b 100644 --- a/backend/tests/game_test.py +++ b/backend/tests/game_test.py @@ -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 diff --git a/backend/tests/roles_test.py b/backend/tests/roles_test.py index 9f2c34f..34353bd 100644 --- a/backend/tests/roles_test.py +++ b/backend/tests/roles_test.py @@ -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 * diff --git a/backend/tests/test_trains.py b/backend/tests/test_trains.py new file mode 100644 index 0000000..cc7e979 --- /dev/null +++ b/backend/tests/test_trains.py @@ -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 diff --git a/backend/tests/valley_of_shadows_characters_test.py b/backend/tests/valley_of_shadows_characters_test.py index a144a68..5ba723c 100644 --- a/backend/tests/valley_of_shadows_characters_test.py +++ b/backend/tests/valley_of_shadows_characters_test.py @@ -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(): diff --git a/backend/tests/valley_of_shadows_test.py b/backend/tests/valley_of_shadows_test.py index 63e5251..e361e53 100644 --- a/backend/tests/valley_of_shadows_test.py +++ b/backend/tests/valley_of_shadows_test.py @@ -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 diff --git a/backend/tests/wild_west_show_characters_test.py b/backend/tests/wild_west_show_characters_test.py index ca3ec32..b022bf4 100644 --- a/backend/tests/wild_west_show_characters_test.py +++ b/backend/tests/wild_west_show_characters_test.py @@ -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 diff --git a/backend/tests/wild_west_show_events_test.py b/backend/tests/wild_west_show_events_test.py index 4a85cbc..9af8851 100644 --- a/backend/tests/wild_west_show_events_test.py +++ b/backend/tests/wild_west_show_events_test.py @@ -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 diff --git a/frontend/src/components/Card.vue b/frontend/src/components/Card.vue index cfad2e7..9dcebb6 100644 --- a/frontend/src/components/Card.vue +++ b/frontend/src/components/Card.vue @@ -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', }" >

{{ cardName }}

@@ -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; +} \ No newline at end of file diff --git a/frontend/src/components/Chat.vue b/frontend/src/components/Chat.vue index d0af5c8..d9a249b 100644 --- a/frontend/src/components/Chat.vue +++ b/frontend/src/components/Chat.vue @@ -21,7 +21,8 @@ @click="fillCmd(msg.cmd)">{{msg.cmd}} {{msg.help}}

- +
@@ -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){ diff --git a/frontend/src/components/Deck.vue b/frontend/src/components/Deck.vue index 0970f50..4d8fe89 100644 --- a/frontend/src/components/Deck.vue +++ b/frontend/src/components/Deck.vue @@ -1,7 +1,7 @@