def add_player(self, user: User): if user.game is not None: raise InvalidGameState("user_in_game", "user already in game") if len(self.players) >= self.options.player_limit: raise InvalidGameState("game_full", "the game is full") # check that there are enough white cards to distribute if self.game_running: total_cards_available = self.white_deck.total_cards() + sum( len(player.hand) for player in self.players) if total_cards_available < (config.game.hand_size + 2) * (len(self.players) + 1): raise InvalidGameState( "too_few_white_cards", "too few white cards in the game for any more players") # create the user player = Player(user) self.players.append(player) user.added_to_game(self, player) # sync state to players self.send_event({ "type": "player_join", "player": player.to_event_json(), }) self.send_updates(full_resync=True, to=player) self.send_updates(UpdateType.players)
def _handle_kick_player(self, content: dict): try: user_id = UserID(UUID(hex=content["user"])) except (KeyError, ValueError): raise InvalidRequest("invalid user") if user_id == self.user.id: raise InvalidGameState("self_kick", "can't kick yourself") try: player = self.user.game.players.find_by("id", user_id) except KeyError: raise InvalidGameState("player_not_in_game", "the player is not in the game") self.user.game.remove_player(player, LeaveReason.host_kick)
def remove_player(self, player: Player, reason: LeaveReason): if player not in self.players: raise InvalidGameState("user_not_in_game", "user not in game") self.send_event({ "type": "player_leave", "player": player.to_event_json(), "reason": reason.name, }) was_host = player == self.host # notify the user object while game state is still valid player.user.removed_from_game() # remove the player now so they won't get further messages self.players.remove(player) # send updates to the player now to ensure they get notified self._send_pending_updates(to=player) # nuke the game if no players remain if len(self.players) == 0: # TODO: need to cancel tasks or something? self.server.remove_game(self) return # end the game if only 2 players remain if len(self.players) <= 2 and self.game_running: self.send_event({"type": "too_few_players"}) self.stop_game() return # cancel the round if the card czar leaves (but if they idled, this will be handled by _judge_idle_timer) if player == self.card_czar and reason != LeaveReason.idle \ and self.state in (GameState.playing, GameState.judging): self.send_event({"type": "card_czar_leave"}) # TODO: check if the side effects of _cancel_round() affect the rest of this function self._cancel_round() if was_host: self.send_event({ "type": "host_leave", "old_host": player.to_event_json(), "new_host": self.host.to_event_json(), }) # discard the player's hand self.white_deck.discard_all(player.hand) # discard the player's played cards if round not decided yet if self.state in (GameState.playing, GameState.judging ) and player.id in self.current_round.white_cards: played_cards = self.current_round.white_cards.pop(player.id) self.white_deck.discard_all(played_cards) # make sure to sync the played cards if necessary self.send_updates(UpdateType.game) # check if all remaining players have played if self.state == GameState.playing: self._check_all_played() # sync state to players self.send_updates(UpdateType.players)
def choose_winner(self, round_id: RoundID, winning_card: WhiteCardID): if self.state != GameState.judging: raise InvalidGameState( "invalid_round_state", "the winner is not being chosen for the round") if round_id != self.current_round.id: raise InvalidGameState("wrong_round", "the round is not being played") # figure out the winner from the winning card try: winner_id = single( player for player, cards in self.current_round.white_cards.items() if cards[0].slot_id == winning_card) except ValueError: raise InvalidGameState("invalid_winner", "no such card played") winner = self.players.find_by("id", winner_id) self.card_czar.idle_rounds = 0 # count the score and start the next round self.current_round.winner = winner winner.score += 1 self._set_state(GameState.round_ended) # sync state to players self.send_updates(UpdateType.game, UpdateType.players)
def start_game(self): """Start the game, resetting it if it has ended.""" # reset the game if one has already been played if self.state == GameState.game_ended: self.stop_game() if self.state != GameState.not_started: raise InvalidGameState("game_already_started", "game is already ongoing") # ensure that there are enough players if len(self.players) < 3: raise InvalidGameState("too_few_players", "too few players") # prepare the deck and ensure that there are enough cards # TODO make these errors another type instead of InvalidGameState? self._build_decks() if self.black_deck.total_cards() == 0: raise InvalidGameState("too_few_black_cards", "no black cards in selected packs") if self.white_deck.total_cards() < (config.game.hand_size + 2) * len( self.players): raise InvalidGameState( "too_few_white_cards", "too few white cards in selected packs for this many players") # start the game self._start_next_round()
def _handle_game_options(self, content: dict): changes = {} for field in fields(GameOptions): if field.name in content: if self.user.game.game_running and field.name not in GameOptions.updateable_ingame: raise InvalidGameState( "option_locked", f"{field.name} can't be changed while the game is ongoing" ) value = content[field.name] if field.name == "card_packs": try: value = tuple( self.server.card_packs.find_by( "id", CardPackID(UUID(uuid))) for uuid in value) except (TypeError, ValueError, KeyError): raise InvalidRequest("invalid card_packs list") changes[field.name] = value try: new_options = replace(self.user.game.options, **changes) self.user.game.update_options(new_options) except ConfigError as ex: raise GameError("invalid_options", str(ex)) from None
def play_white_cards(self, round_id: RoundID, player: Player, cards: Sequence[Tuple[WhiteCardID, Optional[str]]]): if self.state != GameState.playing: raise InvalidGameState( "invalid_round_state", "white cards are not being played for the round") if round_id != self.current_round.id: raise InvalidGameState("wrong_round", "the round is not being played") if not self.current_round.needs_to_play(player): raise InvalidGameState( "already_played", "you already played white cards for the round") # validate the cards if len(set(slot_id for slot_id, _ in cards)) != len(cards): raise InvalidGameState("invalid_white_cards", "duplicate cards chosen") if len(cards) != self.current_round.black_card.pick_count: raise InvalidGameState("invalid_white_cards", "wrong number of cards chosen") # find the cards in the player's hand cards_to_play = [] for slot_id, text in cards: try: card = single(card for card in player.hand if card.slot_id == slot_id) except ValueError: raise InvalidGameState("card_not_in_hand", "you do not have the chosen cards") if text is not None: card = card.write_blank(text) cards_to_play.append(card) # play the cards from the hand for card in cards_to_play: player.play_card(card) self.current_round.white_cards[player.id] = cards_to_play player.idle_rounds = 0 # start judging if necessary self._check_all_played() # sync state to players self.send_updates(UpdateType.players) self.send_updates(UpdateType.hand, UpdateType.game, to=player)
def write_blank(self, text: str) -> WhiteCard: if not self.blank: raise InvalidGameState("invalid_white_cards", "card is not a blank") return WhiteCard(self.slot_id, text, True)
def play_card(self, card: WhiteCard): for hand_card in self.hand: if hand_card.slot_id == card.slot_id: self.hand.remove(hand_card) return raise InvalidGameState("card_not_in_hand", "you do not have the card")
def added_to_game(self, game: Game, player: Player): assert not self.game, "user already in game" if not self.connection: raise InvalidGameState("user_not_connected", "user not connected") self.game = game self.player = player
def _wrapped(self: GameConnection, content: dict): if self.user.player != self.user.game.host: raise InvalidGameState("user_not_host", "you are not the host") return method(self, content)
def _wrapped(self: GameConnection, content: dict): if self.user.player != self.user.game.card_czar: raise InvalidGameState("user_not_czar", "you are not the card czar") return method(self, content)
def _wrapped(self: GameConnection, content: dict): if not self.user.game: raise InvalidGameState("user_not_in_game", "user not in game") return method(self, content)