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 _merge_teams(self, target_team_size: int, tribe: Tribe, gamedb: Database, engine: Engine): # team merging is only necessary when the size of the team == 2 # once a team size == 2, it should be merged with another team. the optimal # choice is to keep team sizes as close to the intended size as possible # find all teams with size = 2, these players need to be merged small_teams = gamedb.stream_teams( from_tribe=tribe, team_size_predicate_value=2) merge_candidates = Queue() for team in small_teams: log_message("Found team of 2. Deacticating team {}.".format(team)) # do not deactivate the last active team in the tribe if gamedb.count_teams(from_tribe=tribe, active_team_predicate_value=True) > 1: gamedb.deactivate_team(team) for player in gamedb.list_players(from_team=team): log_message("Adding merge candidate {}.".format(player)) merge_candidates.put(player) sorted_teams = gamedb.stream_teams( from_tribe=tribe, order_by_size=True, descending=False) log_message("Redistributing merge candidates...") # round robin redistribution strategy # simplest case, could use more thought. visited = {} while not merge_candidates.empty() and sorted_teams: for team in sorted_teams: other_options_available = team.id not in visited visited[team.id] = True if (team.size >= target_team_size and other_options_available): log_message("Team {} has size >= target {} and other options are available. " "Continuing search...".format(team, target_team_size)) continue player = merge_candidates.get() if player.team_id == team.id: continue log_message("Merging player {} from team {} into team {}.".format( player, player.team_id, team.id)) player.team_id = team.id team.size = team.size + 1 gamedb.save(team) gamedb.save(player) # notify player of new team assignment engine.add_event(events.NotifyTeamReassignmentEvent(game_id=self._game_id, game_options=self._options, player=player, team=team))
def _run_multi_tribe_council(self, winning_tribe: Tribe, losing_tribe: Tribe, gamedb: Database, engine: Engine): self._wait_for_tribal_council_start_time() # fraction of teams in losing tribe must vote non_immune_teams = list() for team in gamedb.stream_teams(from_tribe=losing_tribe): log_message(message="Found losing team {}.".format(team), game_id=self._game_id) immunity_granted = random.random( ) < self._options.multi_tribe_team_immunity_likelihood if not immunity_granted: non_immune_teams.append(team) else: engine.add_event( events.NotifyImmunityAwardedEvent( game_id=self._game_id, game_options=self._options, team=team)) # announce winner and tribal council for losing tribe gamedb.clear_votes() engine.add_event( events.NotifyMultiTribeCouncilEvent(game_id=self._game_id, game_options=self._options, winning_tribe=winning_tribe, losing_tribe=losing_tribe)) self._wait_for_tribal_council_end_time() # count votes for team in non_immune_teams: log_message( message="Counting votes for non-immune team {}.".format(team), game_id=self._game_id) voted_out_player = self._get_voted_out_player(team=team, gamedb=gamedb) if voted_out_player: gamedb.deactivate_player(player=voted_out_player) log_message( message="Deactivated player {}.".format(voted_out_player), game_id=self._game_id) engine.add_event( events.NotifyPlayerVotedOutEvent( game_id=self._game_id, game_options=self._options, player=voted_out_player)) # notify all players of what happened at tribal council engine.add_event( events.NotifyTribalCouncilCompletionEvent( game_id=self._game_id, game_options=self._options))
def _run_multi_tribe_council(self, winning_tribe: Tribe, losing_tribe: Tribe, gamedb: Database, engine: Engine): non_immune_teams = list() for team in gamedb.stream_teams(from_tribe=losing_tribe): log_message("Found losing team {}.".format(team)) immunity_granted = random.random() < self._options.multi_tribe_team_immunity_likelihood if not immunity_granted: non_immune_teams.append(team) else: engine.add_event(events.NotifyImmunityAwardedEvent( game_id=self._game_id, game_options=self._options, team=team)) # announce winner and tribal council for losing tribe tribal_council_start_timestamp = _unixtime() gamedb.clear_votes() engine.add_event(events.NotifyMultiTribeCouncilEvent(game_id=self._game_id, game_options=self._options, winning_tribe=winning_tribe, losing_tribe=losing_tribe)) # wait for votes while (((_unixtime() - tribal_council_start_timestamp) < self._options.multi_tribe_council_time_sec) and not self._stop.is_set()): log_message("Waiting for tribal council to end.") time.sleep(self._options.game_wait_sleep_interval_sec) # count votes for team in non_immune_teams: log_message("Counting votes for non-immune team {}.".format(team)) voted_out_player = self._get_voted_out_player( team=team, gamedb=gamedb) if voted_out_player: gamedb.deactivate_player(player=voted_out_player) log_message("Deactivated player {}.".format(voted_out_player)) engine.add_event(events.NotifyPlayerVotedOutEvent(game_id=self._game_id, game_options=self._options, player=voted_out_player)) # notify all players of what happened at tribal council engine.add_event(events.NotifyTribalCouncilCompletionEvent( game_id=self._game_id, game_options=self._options))
def _merge_teams(self, target_team_size: int, tribe: Tribe, gamedb: Database, engine: Engine): with engine: # team merging is only necessary when the size of the team == 2 # once a team size == 2, it should be merged with another team, because # a self preservation vote lands in a deadlock. in general, the optimal # choice is to keep team sizes as close to the intended size as possible # up until a merge becomes necessary. # find all teams with size == 2, these players need to be merged small_teams = gamedb.stream_teams(from_tribe=tribe, team_size_predicate_value=2) merge_candidates = Queue() for team in small_teams: # do not deactivate the last active team in the tribe count_teams = gamedb.count_teams( from_tribe=tribe, active_team_predicate_value=True) if count_teams > 1: log_message( message="Found team of 2. Deactivating team {}.". format(team), game_id=self._game_id) gamedb.deactivate_team(team) for player in gamedb.list_players(from_team=team): log_message( message="Adding merge candidate {}.".format(player), game_id=self._game_id) merge_candidates.put(player) sorted_teams = gamedb.stream_teams(from_tribe=tribe, order_by_size=True, descending=False) log_message(message="Redistributing merge candidates...", game_id=self._game_id) # round robin redistribution strategy # simplest case, could use more thought. visited = {} while not merge_candidates.empty() and sorted_teams: for team in itertools.cycle(sorted_teams): team.count_players = _team_count_players(team, gamedb) other_options_available = team.id not in visited visited[team.id] = True if (team.count_players >= target_team_size and other_options_available): log_message( message= "Team {} has size >= target {} and other options are available. " "Continuing search...".format( team, target_team_size), game_id=self._game_id) continue player = None try: player = merge_candidates.get_nowait() except Empty: log_message(message="Merge candidates empty.") return if player.team_id == team.id: log_message( message= f"Player {str(player)} already on team {str(team)}. Continuing." ) continue log_message( message="Merging player {} from team {} into team {}.". format(player, player.team_id, team.id), game_id=self._game_id) player.team_id = team.id team.count_players += 1 gamedb.save(team) gamedb.save(player) # notify player of new team assignment engine.add_event( events.NotifyTeamReassignmentEvent( game_id=self._game_id, game_options=self._options, player=player, team=team))