def messages(self, gamedb: Database) -> List[SMSEventMessage]: player_messages = [] losing_teams = gamedb.stream_teams(from_tribe=self.losing_tribe) winning_teams = gamedb.stream_teams(from_tribe=self.winning_tribe) for team in losing_teams: losing_players = [] # TODO(brandon): optimize this. for player in gamedb.list_players(from_team=team): losing_players.append(player) options_map = messages.players_as_formatted_options_map( players=losing_players) for player in losing_players: gamedb.ballot(player_id=player.id, options=options_map.options, challenge_id=None) # NOTE: UX isn't perfect here because we'll show the player's own name # as an option to vote out. For MVP this helps with scale because the alternative # requires sending a different message to every player (as opposed to every team) # which is about a 5x cost increase for SMS. player_messages.append( SMSEventMessage( content=messages. NOTIFY_MULTI_TRIBE_COUNCIL_EVENT_LOSING_MSG_FMT.format( header=messages.game_sms_header(gamedb=gamedb), tribe=self.losing_tribe.name, time=self.game_options.game_schedule. localized_time_string(self.game_options.game_schedule. daily_tribal_council_end_time), options=options_map.formatted_string), recipient_phone_numbers=[ p.phone_number for p in losing_players ])) winning_player_phone_numbers = [] for team in winning_teams: winning_players = gamedb.list_players(from_team=team) winning_player_phone_numbers.extend( [p.phone_number for p in winning_players]) player_messages.append( SMSEventMessage( content=messages. NOTIFY_MULTI_TRIBE_COUNCIL_EVENT_WINNING_MSG_FMT.format( header=messages.game_sms_header(gamedb=gamedb), winning_tribe=self.winning_tribe.name, losing_tribe=self.losing_tribe.name, time=self.game_options.game_schedule.localized_time_string( self.game_options.game_schedule. daily_tribal_council_end_time), challenge_time=self.game_options.game_schedule. localized_time_string(self.game_options.game_schedule. daily_challenge_start_time)), recipient_phone_numbers=winning_player_phone_numbers)) return player_messages
def messages(self, gamedb: Database) -> List[SMSEventMessage]: player_messages = [] players = gamedb.stream_players(active_player_predicate_value=True) # TODO(brandon): parallelize # NOTE(brandon) this is going to be problematic at scale. we're sending personalized links to # every player in the game, which costs 1 API call / player within Twilio. If we can make these links # standard then a single Notify API call can address all users in a single game. Non-critical for MVP. for player in players: player_messages.append( SMSEventMessage( content=messages.NOTIFY_TRIBAL_CHALLENGE_EVENT_MSG_FMT. format( header=messages.game_sms_header(gamedb=gamedb), challenge=self.challenge.name, # TODO(brandon) refactor into common routes location link= "{hostname}/challenge-submission/{player_id}/{game_id}/{challenge_id}" .format(hostname=messages.VIR_US_HOSTNAME, game_id=self.game_id, player_id=player.id, challenge_id=self.challenge.id), time=self.game_options.game_schedule. localized_time_string(self.game_options.game_schedule. daily_challenge_end_time)), recipient_phone_numbers=[player.phone_number])) return player_messages
def _play_game(self, game: Game, game_snap: DocumentSnapshot, players: list, game_dict: dict, is_test: bool = False): log_message("Starting a game", game_id=game_dict.get("id"), additional_tags=game_dict) if is_test: database = MockDatabase() engine = MockPlayEngine().CreateEngine(database) else: # NOTE(brandon): the game DB instance used by the matchmaker is for searching over all games. when we create # a game instance, we also supply new game DB and engine objects that have the specific game ID. database = FirestoreDB(json_config_path=json_config_path, game_id=game._game_id) engine = Engine(options=game._options, game_id=game._game_id, sqs_config_path=_TEST_AMAZON_SQS_CONFIG_PATH, twilio_config_path=_TEST_TWILIO_SMS_CONFIG_PATH, gamedb=database) try: game_data = self._matchmaker.generate_tribes( game_id=game._game_id, players=players, game_options=game._options, gamedb=database) tribes = game_data['tribes'] message = messages.NOTIFY_GAME_STARTED_EVENT_MSG_FMT.format( header=messages.game_sms_header( hashtag=game_dict.get('hashtag')), game=game_dict.get('hashtag')) self._notify_players(game_id=game._game_id, players=players, message=message) if self._is_mvp: # NOTE(brandon): changing to thread for now. can't pickle non-primitive engine object. game_thread = threading.Thread(target=game.play, args=(tribes[0], tribes[1], database, engine)) game_thread.start() else: # start on new GCP instance pass except MatchMakerError as e: # Catches error from matchmaker algorithm message = "Matchmaker Error: {}".format(e) log_message(message=message, game_id=game._game_id) self._set_game_has_started(game_snap=game_snap, game=game, value=False) self._notify_players(game_id=game._game_id, players=players, message=message) self._reschedule_or_cancel_game(game_snap=game_snap, game_dict=game_dict, players=players)
def messages(self, gamedb: Database) -> List[SMSEventMessage]: player_messages = [] count_players = gamedb.count_players( from_tribe=gamedb.tribe_from_id(self.winning_player.tribe_id)) # TODO(brandon): unclear why this can't be done in a single bulk message. for player in self.losing_players: options_map = messages.players_as_formatted_options_map( players=self.losing_players, exclude_player=player) # NOTE(brandon): we perform this synchronously to guarantee that ballots are # created in the DB before SMS messages go out to users. gamedb.ballot(player_id=player.id, options=options_map.options, challenge_id=None) player_messages.append( SMSEventMessage( content=messages. NOTIFY_SINGLE_TEAM_COUNCIL_EVENT_LOSING_MSG_FMT.format( header=messages.game_sms_header(gamedb=gamedb), winner=messages.format_tiktok_username( self.winning_player.tiktok), players=count_players, time=self.game_options.game_schedule. localized_time_string(self.game_options.game_schedule. daily_challenge_end_time), options=options_map.formatted_string), recipient_phone_numbers=[player.phone_number])) options_map = messages.players_as_formatted_options_map( players=self.losing_players, exclude_player=self.winning_player) gamedb.ballot(player_id=self.winning_player.id, options=options_map.options, challenge_id=None) player_messages.append( SMSEventMessage( content=messages. NOTIFY_SINGLE_TEAM_COUNCIL_EVENT_WINNING_MSG_FMT.format( header=messages.game_sms_header(gamedb=gamedb), players=count_players, time=self.game_options.game_schedule.localized_time_string( self.game_options.game_schedule. daily_challenge_end_time), options=options_map.formatted_string), recipient_phone_numbers=[self.winning_player.phone_number])) return player_messages
def messages(self, gamedb: Database) -> List[SMSEventMessage]: game_hashtag = gamedb.game_from_id(id=self.game_id).hashtag return [ SMSEventMessage( content=messages. NOTIFY_WINNER_ANNOUNCEMENT_EVENT_WINNER_MSG_FMT.format( header=messages.game_sms_header(gamedb=gamedb), game=game_hashtag), recipient_phone_numbers=[self.winner.phone_number]), SMSEventMessage( content=messages. NOTIFY_WINNER_ANNOUNCEMENT_EVENT_GENERAL_MSG_FMT.format( header=messages.game_sms_header(gamedb=gamedb), player=messages.format_tiktok_username(self.winner.tiktok), game=game_hashtag), recipient_phone_numbers=[ p.phone_number for p in gamedb.stream_players( active_player_predicate_value=False) ]) ]
def messages(self, gamedb: Database) -> List[SMSEventMessage]: return [ SMSEventMessage( content=messages.NOTIFY_PLAYER_SCORE_EVENT_MSG_FMT.format( header=messages.game_sms_header(gamedb=gamedb), points=self.points, time=self.game_options.game_schedule.localized_time_string( self.game_options.game_schedule. daily_challenge_end_time)), recipient_phone_numbers=[self.player.phone_number]) ]
def messages(self, gamedb: Database) -> List[SMSEventMessage]: player_messages = [] team = gamedb.team_from_id(id=self.player.team_id) teammate_phone_numbers = [ p.phone_number for p in gamedb.list_players(from_team=team) if p.id != self.player.id ] player_messages.append( SMSEventMessage( content=messages.NOTIFY_PLAYER_VOTED_OUT_TEAM_MSG_FMT.format( header=messages.game_sms_header(gamedb=gamedb), player=messages.format_tiktok_username(self.player.tiktok), time=self.game_options.game_schedule.localized_time_string( self.game_options.game_schedule. daily_challenge_start_time)), recipient_phone_numbers=teammate_phone_numbers)) player_messages.append( SMSEventMessage( content=messages.NOTIFY_PLAYER_VOTED_OUT_MSG_FMT.format( header=messages.game_sms_header(gamedb=gamedb)), recipient_phone_numbers=[self.player.phone_number])) return player_messages
def messages(self, gamedb: Database) -> List[SMSEventMessage]: return [ SMSEventMessage( content=messages.NOTIFY_IMMUNITY_AWARDED_EVENT_MSG_FMT.format( header=messages.game_sms_header(gamedb=gamedb), date=self.game_options.game_schedule. tomorrow_localized_string, time=self.game_options.game_schedule.localized_time_string( self.game_options.game_schedule. daily_challenge_start_time)), recipient_phone_numbers=[ p.phone_number for p in gamedb.list_players(from_team=self.team) ]) ]
def messages(self, gamedb: Database) -> List[SMSEventMessage]: team_players = gamedb.list_players(from_team=self.team) return [ SMSEventMessage( content=messages.NOTIFY_TEAM_REASSIGNMENT_EVENT_MSG_FMT.format( header=messages.game_sms_header(gamedb=gamedb), team=messages.players_as_formatted_list( players=team_players), date=self.game_options.game_schedule. tomorrow_localized_string, time=self.game_options.game_schedule.localized_time_string( self.game_options.game_schedule. daily_challenge_start_time)), recipient_phone_numbers=[self.player.phone_number]) ]
def messages(self, gamedb: Database) -> List[SMSEventMessage]: return [ SMSEventMessage( content=messages. NOTIFY_TRIBAL_COUNCIL_COMPLETION_EVENT_MSG_FMT.format( header=messages.game_sms_header(gamedb=gamedb), date=self.game_options.game_schedule. tomorrow_localized_string, time=self.game_options.game_schedule.localized_time_string( self.game_options.game_schedule. daily_challenge_start_time)), recipient_phone_numbers=[ p.phone_number for p in gamedb.stream_players( active_player_predicate_value=True) ]) ]
def _reschedule_or_cancel_game(self, game_snap: DocumentSnapshot, game_dict: dict, players: list): log_message(message="Rescheduling or cancelling game", game_id=game_dict.get("id")) now_date = datetime.datetime.utcnow().strftime('%Y-%m-%d') if 'times_rescheduled' not in game_dict: game_dict['times_rescheduled'] = 0 if 'max_reschedules' not in game_dict: game_dict['max_reschedules'] = 1 if (game_dict.get("times_rescheduled") if game_dict.get("times_rescheduled") else 0) < game_dict.get("max_reschedules"): # Reschedule the game by setting current UTC date to last_checked_date. # Server will then not check the game until following week # Assume times_rescheduled is optional and max_reschedules is True times_rescheduled = game_dict["times_rescheduled"] + \ 1 if game_dict.get("times_rescheduled") else 1 field_updates = { 'last_checked_date': now_date, 'times_rescheduled': times_rescheduled } try: game_snap.reference.update(field_updates) log_message(message="Game successfully rescheduled", game_id=game_dict.get("id")) schedule = STV_I18N_TABLE[self._region] notif_message = messages.NOTIFY_GAME_RESCHEDULED_EVENT_MSG_FMT.format( header=messages.game_sms_header( hashtag=game_dict.get('hashtag')), game=game_dict.get("hashtag"), reason="insufficient players", date=schedule.nextweek_localized_string, time=schedule.localized_time_string( schedule.daily_challenge_start_time)) self._notify_players(game_id=game_dict.get("id"), players=players, message=notif_message) except Exception as e: log_message(message="Error rescheduling game: {}".format(e), game_id=game_dict.get("id")) else: self._cancel_game(game_snap=game_snap, players=players)
def _cancel_game(self, game_snap: DocumentSnapshot, players: list, reason: str = "insufficient players") -> None: # Cancel the game game_dict = game_snap.to_dict() field_updates = { 'to_be_deleted': True, } game_snap.reference.update(field_updates) log_message(message="Cancelled the game (set to_be_deleted flag)", game_id=game_dict.get("id")) notif_message = messages.NOTIFY_GAME_CANCELLED_EVENT_MSG_FMT.format( header=messages.game_sms_header(hashtag=game_dict.get('hashtag')), game=game_dict.get("hashtag"), reason=reason) self._notify_players(game_id=game_dict.get("id"), players=players, message=notif_message)
def messages(self, gamedb: Database) -> List[SMSEventMessage]: options_map = messages.players_as_formatted_options_map( players=self.finalists) # TODO(brandon): this is slow and expensive, but it should work. for player in gamedb.stream_players(): gamedb.ballot(player_id=player.id, challenge_id=None, options=options_map.options, is_for_win=True) return [ SMSEventMessage( content=messages.NOTIFY_FINAL_TRIBAL_COUNCIL_EVENT_MSG_FMT. format( header=messages.game_sms_header(gamedb=gamedb), players=len(self.finalists), game=gamedb.game_from_id(id=self.game_id).hashtag, time=self.game_options.game_schedule.localized_time_string( self.game_options.game_schedule. daily_tribal_council_end_time), options=options_map.formatted_string), recipient_phone_numbers=[ p.phone_number for p in gamedb.stream_players() ]) ]
def message_content(self, gamedb: Database) -> str: return messages.NOTIFY_IMMUNITY_AWARDED_EVENT_MSG_FMT.format( header=messages.game_sms_header(gamedb=gamedb), date=self.game_options.game_schedule.tomorrow_localized_string, time=self.game_options.game_schedule.localized_time_string( self.game_options.game_schedule.daily_challenge_start_time))