def _matchmaker_function(self, sleep_seconds: int = 60, is_test: bool = False): log_message("Starting matchmaker for region={}".format(self._region)) while not self._stop.is_set(): games = self._gamedb.find_matchmaker_games(region=self._region) if len(games) >= 1: for game_snap in games: game_dict = game_snap.to_dict() players_stream = game_snap.reference.collection( "players").stream() players_list = [] for player in players_stream: players_list.append(player) if self._region in STV_I18N_TABLE: schedule = STV_I18N_TABLE[self._region] else: schedule = STV_I18N_TABLE['US'] try: now_utc = datetime.datetime.utcnow().strftime( '%Y-%m-%d') if self._check_start_time( schedule=schedule, now_dt_with_tz=datetime.datetime.now( ).astimezone(), is_test=is_test) and now_utc != game_dict.get( "last_checked_date" ): # TODO: Do these checks in query if game_dict["count_players"] >= self._min_players: if self._game_options is None: self._game_options = GameOptions( game_schedule=schedule, game_wait_sleep_interval_sec=1 if is_test else 30) g = Game(game_id=game_dict["id"], options=self._game_options) self._start_game(game=g, game_snap=game_snap, players=players_list, game_dict=game_dict, is_test=is_test) else: self._reschedule_or_cancel_game( game_snap=game_snap, game_dict=game_dict, players=players_list) except Exception as e: log_message( f"Game {str(game_dict)} is corrupt: {str(e)} Cancelling." ) self._cancel_game( game_snap=game_snap, players=players_list, reason="an internal data corruption error") time.sleep(sleep_seconds) log_message("Stopped matchmaker for region={}".format(self._region))
def __init__(self, json_config_path: Text, game_options: GameOptions = None) -> None: with open(json_config_path, 'r') as f: config = json.loads(f.read()) self._url = config['url'] self._client = boto3.client( 'sqs', aws_access_key_id=config['aws_access_key_id'], aws_secret_access_key=config['aws_secret_access_key'], region_name='us-east-2' ) # store a reference to game options for timezone specific # event deserialization from the AWS queue. self._game_options = game_options if game_options else GameOptions( game_schedule=STV_I18N_TABLE['US'] )
def test_generate_tribes(self): gamedb = MockDatabase() gamedb.find_user = mock.MagicMock(return_value=mock.MagicMock()) gamedb._games[_TEST_GAME_ID] = Game() players = self.generate_players(gamedb=gamedb) options = GameOptions(target_team_size=_TEAM_SIZE) data = MatchMakerRoundRobin.generate_tribes(game_id=_TEST_GAME_ID, players=players, game_options=options, gamedb=gamedb) players = data['players'] teams = data['teams'] tribes = data['tribes'] self.assertEqual(len(players), 20) self.assertEqual(len(teams), 2) self.assertEqual(len(tribes), 2)
def __init__(self, json_config_path: str, game_id: str, game_options: GameOptions = None) -> None: self.game_id = game_id with open(json_config_path, 'r') as f: name = f'VIRUS-{game_id}-{str(uuid.uuid4())}.fifo' config = json.loads(f.read()) self._client = boto3.client( 'sqs', aws_access_key_id=config['aws_access_key_id'], aws_secret_access_key=config['aws_secret_access_key'], region_name='us-east-2') self._client.create_queue(QueueName=name, Attributes={'FifoQueue': 'true'}) # After you create a queue, you must wait at least one second after the queue # is created to be able to use the queue. time.sleep(_AWS_SQS_INIT_TIME_SEC) self._url = self._client.get_queue_url(QueueName=name)['QueueUrl'] # store a reference to game options for timezone specific # event deserialization from the AWS queue. self._game_options = game_options if game_options else GameOptions( game_schedule=STV_I18N_TABLE['US'])
def test_integration(self): test_id = str(uuid.uuid4()) # Inject a game ID (test uuid) into the gamedb. use a globally unique country code to ensure # that only this game gets scheduled. game_id = FirestoreDB.add_game( json_config_path=_TEST_FIRESTORE_INSTANCE_JSON_PATH, hashtag=_TEST_GAME_HASHTAG, country_code=f'US-{test_id}') gamedb = FirestoreDB( json_config_path=_TEST_FIRESTORE_INSTANCE_JSON_PATH, game_id=game_id) log_message(f'Running integration test for game {game_id}') # create an integration test subsystem log stream that specifically captures # game inputs and outputs at the SMS boundary. test_log_stream = GameIntegrationTestLogStream(game_id=game_id, test_id=test_id) # add test challenges for challenge in _TEST_CHALLENGES: gamedb.add_challenge(challenge=challenge) # if non-existent, create users associated with players. NOTE: critical for FE code # to create users when players sign up. for player_info in [*_EMULATED_PLAYERS, *_REAL_PLAYERS]: FirestoreDB.add_user( json_config_path=_TEST_FIRESTORE_INSTANCE_JSON_PATH, name=player_info[0], tiktok=player_info[1], phone_number=player_info[2]) # Inject players into gamedb. set the phone number of the players to a known, or emulated device. emulated_players = [] for player_info in _EMULATED_PLAYERS: player = gamedb.player(name=player_info[0], tiktok=player_info[1], phone_number=player_info[2]) emulated_players.append( EmulatedPlayer(id=player.id, name=player.name, tiktok=player.tiktok, phone_number=player.phone_number, test_stream=test_log_stream, gamedb=gamedb)) for player_info in _REAL_PLAYERS: player = gamedb.player(name=player_info[0], tiktok=player_info[1], phone_number=player_info[2]) # mock the scheduling method in MM service so that we can force schedule # after initialization. save_check_start_time_fn = MatchmakerService._check_start_time check_start_time_fn = mock.MagicMock() check_start_time_fn.return_value = False MatchmakerService._check_start_time = check_start_time_fn # create device emulator for Twilio SMS events. patch this # into the game engine class for the test. notification_emulator = FakeTwilioSMSNotifier( json_config_path=_TEST_TWILIO_SMS_CONFIG_PATH, game_id=game_id, emulated_devices=emulated_players) build_notifier_fn = mock.MagicMock() build_notifier_fn.return_value = notification_emulator Engine._get_sms_notifier = build_notifier_fn # Instantiate a local matchmaker service. make sure all services have the right configs, # e.g. twilio should be using the prod service phone number so that it can actually send messages. # set the clock mode on (2) to async so that game events happen immediately during testing. service = MatchmakerService( matchmaker=MatchMakerRoundRobin(), region=f'US-{test_id}', gamedb=gamedb, game_options=GameOptions( game_clock_mode=GameClockMode.ASYNC, game_wait_sleep_interval_sec=_TEST_SLEEP_INTERVAL, multi_tribe_min_tribe_size=2, engine_worker_thread_count=5, tribe_council_time_sec=_TEST_SLEEP_INTERVAL)) # mock the MM Twilio client service._get_sms_notifier = mock.MagicMock( return_value=notification_emulator) try: service.start_matchmaker_daemon(sleep_seconds=1) # force schedule the game in MM (1). check_start_time_fn.return_value = True MatchmakerService._check_start_time = check_start_time_fn while gamedb.game_from_id(game_id).count_players > 1: time.sleep(_TEST_SLEEP_INTERVAL) finally: MatchmakerService._check_start_time = save_check_start_time_fn service.set_stop() test_dict = test_log_stream.to_dict() if _SAVE_TEST_LOGS: persisted_test_logs = test_log_stream.persist() log_message(persisted_test_logs) with open(f'game_logs/stream.{test_id}.json', 'w+') as f: f.write(persisted_test_logs) active_players = list() for player in gamedb.stream_players( active_player_predicate_value=True): active_players.append(player) winner = active_players[0] found_winner_message = False for output in test_dict['outputs']: if 'You are the last survivor and WINNER' in output[ 'message'] and output['name'] == winner.name: found_winner_message = True self.assertTrue(found_winner_message)
def setUp(self): self._game_options = GameOptions() self._game_options.game_schedule = STV_I18N_TABLE['US']
player_id='player/foo', tribe_id='tribe/bar', challenge_id='challenge/foo', team_id='team/bar', url='https://www.tiktok.com/@ifrc/video/6816427450801736965') _TEST_TEAM1 = Team(id='id/foo1', name='team/bar', count_players=5, tribe_id='tribe/foo') _TEST_TEAM2 = Team(id='id/foo2', name='team/bar', count_players=5, tribe_id='tribe/foo') _TEST_TRIBE1 = Tribe(id='id/foo1', name='SIDAMA', count_players=1e6) _TEST_TRIBE2 = Tribe(id='id/foo2', name='TIGRAWAY', count_players=500e3) _TEST_GAME_OPTIONS = GameOptions(game_schedule=STV_I18N_TABLE['US']) _gamedb = FirestoreDB(json_config_path=_TEST_FIRESTORE_INSTANCE_JSON_PATH, game_id=_TEST_GAME_ID) _TEST_EVENTS = [ events.NotifyTribalChallengeEvent(game_id=_TEST_GAME_ID, game_options=_TEST_GAME_OPTIONS, challenge=_TEST_CHALLENGE), events.NotifyPlayerScoreEvent(game_id=_TEST_GAME_ID, game_options=_TEST_GAME_OPTIONS, player=_TEST_PLAYER1, challenge=_TEST_CHALLENGE, entry=_TEST_ENTRY, points=100), events.NotifyTeamReassignmentEvent(game_id=_TEST_GAME_ID,
def setUp(self): self.game_options = GameOptions() self.game_options.game_schedule = STV_I18N_TABLE['US'] self.twilio = TwilioSMSNotifier( json_config_path=_TEST_TWILIO_SMS_CONFIG_PATH)
for rank in range(num_scores): score, player_id = heapq.heappop(top_scores) log_message("Player {} rank {} with score {}.".format( player_id, rank, score)) # all but the highest scorer lose if rank < (num_scores - 1): losing_players.append(gamedb.player_from_id(player_id)) return losing_players def _merge_tribes(self, tribe1: Tribe, tribe2: Tribe, new_tribe_name: Text, gamedb: Database) -> Tribe: new_tribe = gamedb.tribe(name=new_tribe_name) gamedb.batch_update_tribe(from_tribe=tribe1, to_tribe=new_tribe) gamedb.batch_update_tribe(from_tribe=tribe2, to_tribe=new_tribe) return new_tribe if __name__ == '__main__': options = GameOptions() game = Game(game_id=str(uuid.uuid4), options=options) # TODO(brandon) for production each game should instantiate a separate AWS FIFO # on the fly. engine = Engine(options=options, sqs_config_path=_AMAZON_SQS_PROD_CONF_JSON_PATH) database = FirestoreDB(json_config_path=_FIRESTORE_PROD_CONF_JSON_PATH) game.play(tribe1=database.tribe_from_id(_TRIBE_1_ID), tribe2=database.tribe_from_id(_TRIBE_2_ID), gamedb=database, engine=engine)
def __init__(self): options = GameOptions() self._game = Game(game_id=str(uuid.uuid4), options=options) self._frontend = MockFrontend() self._sms_endpoint = MockSMSEndpoint()
class MatchMakerRoundRobinTest(unittest.TestCase): @staticmethod def generate_players(gamedb: MockDatabase = None): players = [_TEST_PLAYER1] for x in range(2, 21): player = copy.deepcopy(_TEST_PLAYER1) player.id = str(uuid.uuid4()) players.append(player) if gamedb: for player in players: gamedb._players[player.id] = player return players @mock.patch.object(FirestoreDB, 'find_user', return_value=mock.MagicMock()) def test_generate_teams(self, _): players = self.generate_players() teams = MatchMakerRoundRobin.generate_teams(game_id=_TEST_GAME_ID, players=players, team_size=_TEAM_SIZE) self.assertEqual(len(teams), 2) def test_generate_teams_insufficient_players(self): players = [_TEST_PLAYER1] with self.assertRaises(Exception) as context: MatchMakerRoundRobin.generate_teams(game_id=_TEST_GAME_ID, players=players, team_size=_TEAM_SIZE) self.assertTrue(context.exception) def test_generate_tribes(self): gamedb = MockDatabase() gamedb.find_user = mock.MagicMock(return_value=mock.MagicMock()) gamedb._games[_TEST_GAME_ID] = Game() players = self.generate_players(gamedb=gamedb) options = GameOptions(target_team_size=_TEAM_SIZE) data = MatchMakerRoundRobin.generate_tribes(game_id=_TEST_GAME_ID, players=players, game_options=options, gamedb=gamedb) players = data['players'] teams = data['teams'] tribes = data['tribes'] self.assertEqual(len(players), 20) self.assertEqual(len(teams), 2) self.assertEqual(len(tribes), 2) @parameterized.expand([ (MatchMakerRoundRobin, 10, GameOptions(target_team_size=5), 2, 2, 10, 1, 5, 5), (MatchMakerRoundRobin, 20, GameOptions(target_team_size=5), 2, 4, 20, 2, 10, 5), (MatchMakerRoundRobin, 100, GameOptions(target_team_size=10), 2, 10, 100, 5, 50, 10), ]) def test_generate_tribes_count_initialization( self, algorithm_type, number_of_joined_players, game_options, expected_game_count_tribes, expected_game_count_teams, expected_game_count_players, expected_tribe_count_teams, expected_tribe_count_players, expected_team_count_players): # create a game game_id = FirestoreDB.add_game( json_config_path=_TEST_FIRESTORE_INSTANCE_JSON_PATH, hashtag='hashtag/foo') _gamedb._game_id = game_id # generate mock players with an id attribute and add the players to the game players = list() for i in range(0, number_of_joined_players): name = 'name/foo' tiktok = 'tiktok/bar' phone_number = f'+1000000000{i}' player = _gamedb.player(name=name, tiktok=tiktok, phone_number=phone_number) FirestoreDB.add_user( json_config_path=_TEST_FIRESTORE_INSTANCE_JSON_PATH, name=name, tiktok=tiktok, phone_number=phone_number) players.append(player) # read counts for game, all tribes, all teams and verify that they are correct game_info_dict = algorithm_type.generate_tribes( game_id=game_id, players=players, game_options=game_options, gamedb=_gamedb) game = _gamedb.game_from_id(game_id) self.assertEqual(game.count_tribes, expected_game_count_tribes) self.assertEqual(game.count_teams, expected_game_count_teams) self.assertEqual(game.count_players, expected_game_count_players) for tribe in game_info_dict['tribes']: tribe_ref = _gamedb.tribe_from_id(tribe.id) self.assertEqual(tribe_ref.count_teams, expected_tribe_count_teams) self.assertEqual(tribe_ref.count_players, expected_tribe_count_players) for team in game_info_dict['teams']: self.assertEqual( _gamedb.team_from_id(team.id).count_players, expected_team_count_players)