diff --git a/concordia/components/game_master/__init__.py b/concordia/components/game_master/__init__.py index 60a4bcb5..8fc8567a 100644 --- a/concordia/components/game_master/__init__.py +++ b/concordia/components/game_master/__init__.py @@ -15,6 +15,7 @@ """Library of components specifically for generative game masters.""" from concordia.components.game_master import conversation +from concordia.components.game_master import coordination_payoffs from concordia.components.game_master import current_scene from concordia.components.game_master import direct_effect from concordia.components.game_master import inventory diff --git a/concordia/factory/environment/basic_game_master.py b/concordia/factory/environment/basic_game_master.py index 97d2f6de..b70c901b 100644 --- a/concordia/factory/environment/basic_game_master.py +++ b/concordia/factory/environment/basic_game_master.py @@ -23,6 +23,7 @@ from concordia.associative_memory import importance_function from concordia.clocks import game_clock from concordia.components import game_master as gm_components +from concordia.document import interactive_document from concordia.environment import game_master from concordia.environment.scenes import runner from concordia.language_model import language_model @@ -47,7 +48,14 @@ def build_game_master( memory: associative_memory.AssociativeMemory | None = None, supporting_players_at_fixed_locations: Sequence[str] | None = None, additional_components: Sequence[component.Component] | None = tuple([]), + thought_chain: ( + Sequence[ + Callable[[interactive_document.InteractiveDocument, str, str], str] + ] + | None + ) = None, npc_context: str = '', + max_conversation_length: int = 10, verbose: bool = False, ) -> tuple[game_master.GameMaster, associative_memory.AssociativeMemory]: """Build a game master (i.e., an environment). @@ -70,7 +78,9 @@ def build_game_master( characters who never move are located. additional_components: Add more components specific to the current environment. + thought_chain: The thought chain to use for the game master. npc_context: extra context provided only to non-player characters + max_conversation_length: The maximum number of turns in a conversation. verbose: whether or not to print verbose debug information Returns: @@ -82,24 +92,26 @@ def build_game_master( game_master_memory = associative_memory.AssociativeMemory( sentence_embedder=embedder, importance=importance_model.importance, - clock=clock.now) + clock=clock.now, + ) player_names = [player.name for player in players] scenario_knowledge = generic_components.constant.ConstantComponent( - state='\n'.join(shared_memories), - name='Background:\n') + state='\n'.join(shared_memories), name='Background:\n' + ) if supporting_players_at_fixed_locations is not None: supporting_character_locations_if_any = ( generic_components.constant.ConstantComponent( state='\n'.join(supporting_players_at_fixed_locations), - name='Notes:\n')) + name='Notes:\n', + ) + ) else: supporting_character_locations_if_any = ( - generic_components.constant.ConstantComponent( - state='', - name='Notes:\n')) + generic_components.constant.ConstantComponent(state='', name='Notes:\n') + ) player_status = gm_components.player_status.PlayerStatus( clock_now=clock.now, @@ -117,6 +129,7 @@ def build_game_master( components=[player_status], cap_nonplayer_characters=cap_nonplayer_characters_in_conversation, shared_context=f'{shared_context}\n{npc_context}', + max_conversation_length=max_conversation_length, ) direct_effect_externality = gm_components.direct_effect.DirectEffect( @@ -128,13 +141,15 @@ def build_game_master( ) relevant_events = gm_components.relevant_events.RelevantEvents( - clock.now, model, game_master_memory) + clock.now, model, game_master_memory + ) time_display = gm_components.time_display.TimeDisplay(clock) # Create the game master's thought chain account_for_agency_of_others = thought_chains_lib.AccountForAgencyOfOthers( - model=model, players=players, verbose=False) - thought_chain = [ + model=model, players=players, verbose=False + ) + thought_chain = thought_chain or [ thought_chains_lib.extract_direct_quote, thought_chains_lib.attempt_to_most_likely_outcome, thought_chains_lib.result_to_effect_caused_by_active_player, @@ -205,17 +220,19 @@ def create_html_log( model: The language model to use. primary_environment: The main game master. secondary_environments: Sequence of secondary game masters. + Returns: An HTML string log of the simulation. """ - primary_gm_memories = ( - primary_environment.get_memory().retrieve_recent(k=10000, add_time=True)) + primary_gm_memories = primary_environment.get_memory().retrieve_recent( + k=10000, add_time=True + ) detailed_story = '\n'.join(primary_gm_memories) episode_summary = model.sample_text( - f'Sequence of events:\n{detailed_story}'+ - '\nNarratively summarize the above temporally ordered ' + - 'sequence of events. Write it as a news report. Summary:\n', + f'Sequence of events:\n{detailed_story}' + + '\nNarratively summarize the above temporally ordered ' + + 'sequence of events. Write it as a news report. Summary:\n', max_tokens=3500, terminators=(), ) @@ -223,11 +240,13 @@ def create_html_log( history_sources = [primary_environment] + list(secondary_environments) histories_html = [ html_lib.PythonObjectToHTMLConverter(history.get_history()).convert() - for history in history_sources] + for history in history_sources + ] histories_names = [history.name for history in history_sources] gm_mem_html = html_lib.PythonObjectToHTMLConverter( - primary_gm_memories).convert() + primary_gm_memories + ).convert() tabbed_html = html_lib.combine_html_pages( histories_html + [gm_mem_html], histories_names + ['GM'], diff --git a/examples/modular/environment/modules/modern_london_social_context.py b/examples/modular/environment/modules/modern_london_social_context.py new file mode 100644 index 00000000..6cb10078 --- /dev/null +++ b/examples/modular/environment/modules/modern_london_social_context.py @@ -0,0 +1,56 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A set of social context descriptions.""" + +SOCIAL_CONTEXT = [ + ( + "The sun peeks through the morning" + " mist as a {players} gather near the entrance of Hackney" + " Marshes. They stretch and laugh, their colorful running attire" + " contrasting with the green expanse. A few dog walkers pass by, their" + " furry companions excitedly sniffing the air. " + "{player_name} just arrived." + ), + ( + "The aroma of freshly brewed coffee and" + " artisan pastries fills the air. {players} sit at a long wooden table" + " under a striped awning, their laughter mingling with the chatter of" + " the bustling market. A street musician strums a guitar nearby, adding" + " a bohemian touch to the scene. " + "{player_name} just arrived." + ), + ( + "Sunlight dances on the water as" + " {players} cycle along the towpath of Regent's Canal. They" + " pause to admire the colorful houseboats and wave at fellow cyclists." + " The gentle sound of water lapping against the canal banks creates a" + " peaceful atmosphere. " + "{player_name} just arrived." + ), + ( + "Vibrant murals and graffiti adorn the brick walls of Shoreditch." + " {players} wander through the streets, their eyes wide with wonder as" + " they discover hidden gems of urban art. The smell of street food" + " wafts from nearby vendors, tempting them to take a break." + " {player_name} just arrived." + ), + ( + "A checkered blanket is spread out on the" + " lush green lawn of Victoria Park. {players} lounge in the sunshine," + " sharing snacks and stories. The laughter of children playing nearby" + " adds a joyful backdrop to the scene. " + "{player_name} just arrived." + ), +] diff --git a/examples/modular/environment/modules/player_names.py b/examples/modular/environment/modules/player_names.py new file mode 100644 index 00000000..d35e7b8a --- /dev/null +++ b/examples/modular/environment/modules/player_names.py @@ -0,0 +1,70 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A set of names to be used for creating characters.""" + +FIRST_NAMES = [ + "Aaliyah", + "Adrian", + "Aurora", + "Benjamin", + "Camila", + "Daniel", + "Eleanor", + "Ethan", + "Fiona", + "Gabriel", + "Harper", + "Isaac", + "Jasmine", + "Julian", + "Kaitlyn", + "Leo", + "Maya", + "Mateo", + "Naomi", + "Noah", + "Olivia", + "Owen", + "Penelope", + "Quinn", + "Riley", + "Samuel", + "Sophia", + "Theodore", + "Valentina", + "William", + "Xander", + "Yasmine", + "Zachary", + "Amelia", + "Alexander", + "Ava", + "Liam", + "Mia", + "Elijah", + "Isabella", + "James", + "Emily", + "Lucas", + "Abigail", + "Mason", + "Harper", + "Logan", + "Evelyn", + "Oliver", + "Chloe", + "Jackson", + "Ella", +] diff --git a/examples/modular/environment/pub_coordination.py b/examples/modular/environment/pub_coordination.py new file mode 100644 index 00000000..551d875c --- /dev/null +++ b/examples/modular/environment/pub_coordination.py @@ -0,0 +1,619 @@ +# Copyright 2024 DeepMind Technologies Limited. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""A Concordia Environment Configuration.""" + +from collections.abc import Callable, Mapping, Sequence +import datetime +import random +import types + +from concordia import components as generic_components +from concordia.agents import basic_agent +from concordia.associative_memory import associative_memory +from concordia.associative_memory import blank_memories +from concordia.associative_memory import formative_memories +from concordia.associative_memory import importance_function +from concordia.clocks import game_clock +from concordia.components import game_master as gm_components +from concordia.environment import game_master +from examples.modular.environment.modules import modern_london_social_context +from examples.modular.environment.modules import player_names +from examples.modular.environment.modules import player_traits_and_styles +from concordia.factory.agent import basic_agent__main_role +from concordia.factory.agent import basic_agent__supporting_role +from concordia.factory.environment import basic_game_master +from concordia.language_model import language_model +from concordia.thought_chains import thought_chains as thought_chains_lib +from concordia.typing import agent as agent_lib +from concordia.typing import scene as scene_lib +from concordia.utils import concurrency +from concordia.utils import measurements as measurements_lib +import sentence_transformers + +Runnable = Callable[[], str] +ItemTypeConfig = gm_components.inventory.ItemTypeConfig + + +MAJOR_TIME_STEP = datetime.timedelta(minutes=30) +MINOR_TIME_STEP = datetime.timedelta(seconds=10) +SETUP_TIME = datetime.datetime(hour=20, year=2024, month=10, day=1) +START_TIME = datetime.datetime(hour=12, year=2024, month=10, day=2) + +DECISION_SCENE_TYPE = 'choice' + + +TIME_INCREMENT_BETWEEN_SCENES = datetime.timedelta(hours=24) + +NUM_MAIN_PLAYERS = 5 +NUM_SUPPORTING_PLAYERS = 2 + +NUM_BACKGROUND_WORLD_ELEMENTS = 7 +NUM_MAIN_PLAYER_WORLD_ELEMENTS = 2 +NUM_SUPPORTING_PLAYER_WORLD_ELEMENTS = 5 +NUM_LAUDANUM_ADVERTISEMENTS = 2 + +SCENARIO_PREMISE = [ + 'The year is 2024. Today England plays Spain in the Euro finals.' +] + +FIRST_NAMES = player_names.FIRST_NAMES +SOCIAL_CONTEXT = modern_london_social_context.SOCIAL_CONTEXT +PUB_QUALITY = {'The Princess of Wales': 1.0, 'The Crooked Billet': 0.9} +NUM_GAMES = 2 +pub_preferences = { + 'The Princess of Wales': [ + ( + 'The Princess of Wales boasts an extensive collection of rare' + ' whiskies, perfect for the discerning connoisseur.' + ), + ( + 'The pub hosts a weekly poetry night, attracting a vibrant and' + ' creative crowd.' + ), + ( + 'The Princess of Wales is known for its luxurious, plush seating' + ' and elegant decor, providing a haven of comfort.' + ), + ( + "The pub's chef crafts innovative dishes with locally sourced" + ' ingredients, offering a culinary adventure with each visit.' + ), + ( + 'The Princess of Wales boasts a secret, hidden room, perfect for' + ' those seeking clandestine meetings and whispered conversations.' + ), + ], + 'The Crooked Billet': [ + ( + 'The Crooked Billet hosts lively folk music sessions, filling the' + ' air with energetic melodies and foot-stomping rhythms.' + ), + ( + "The pub's friendly and welcoming atmosphere makes everyone feel" + ' like a regular, fostering a sense of community.' + ), + ( + "The Crooked Billet's rustic charm and weathered wooden beams exude" + ' a sense of history and tradition.' + ), + ( + "The pub's hearty, home-cooked meals are the perfect comfort food" + " after a long day's journey." + ), + ( + 'The Crooked Billet has a hidden tunnel leading to a nearby forest,' + ' offering a quick escape route for those in need.' + ), + ], +} + + +def get_shared_memories_and_context() -> tuple[Sequence[str], str]: + """Return the shared memories and context for all agents and game master.""" + shared_memories = [ + 'The European football cup is on.', + 'Games are best watched in pubs with a lot of friends.', + ] + shared_context = ( + 'The year is 2024. The place is London, Hackney. The European football' + ' cup is on.\n' + ) + return shared_memories, shared_context + + +def configure_players() -> tuple[ + list[formative_memories.AgentConfig], + list[formative_memories.AgentConfig], +]: + """Configure the players. + + Args: + Returns: + main_player_configs: configs for the main characters + supporting_player_configs: configs for the supporting characters + """ + pubs = ['The Princess of Wales', 'The Crooked Billet'] + social_classes = ['working', 'middle', 'upper'] + teams_to_support = ['England', 'Spain'] + names = random.sample(FIRST_NAMES, NUM_MAIN_PLAYERS) + all_players = ', '.join(names) + player_configs = [] + for i in range(NUM_MAIN_PLAYERS): + name = names[i] + fav_pub = pubs[i % 2] + + social_class = random.choice(social_classes) + reasons = random.choice(pub_preferences[fav_pub]) + config = formative_memories.AgentConfig( + name=name, + gender=random.choice(['male', 'female']), + date_of_birth=datetime.datetime(year=1980, month=4, day=28), + formative_ages=[16, 20], + goal=( + f'Watch the game in the same pub as {all_players}. {name} would' + f' prefer {fav_pub}' + ), + context=( + f"{all_players}' are best friends." + f'Born in London, {name} has a favoirite pub which is {fav_pub}.' + f' They are also aware of the following:{reasons}' + ), + traits=( + f"{name}'s personality is like " + + player_traits_and_styles.get_trait(flowery=True) + ), + extras={ + 'player_specific_memories': [ + f'{name} is wealthy and a member of the {social_class} class.', + ( + f'{name} supports {random.choice(teams_to_support)} in' + ' football.' + ), + ], + 'main_character': True, + 'preference': {pub: 1.0 if pub == fav_pub else 0.8 for pub in pubs}, + }, + ) + player_configs.append(config) + + main_player_configs = [ + player for player in player_configs if player.extras['main_character'] + ] + supporting_player_configs = [ + player for player in player_configs if not player.extras['main_character'] + ] + + return main_player_configs, supporting_player_configs + + +CoordinationPayoffs = gm_components.coordination_payoffs.CoordinationPayoffs + + +def add_choice_scene_spec( + model: language_model.LanguageModel, + game_master_memory: associative_memory.AssociativeMemory, + players: Sequence[basic_agent.BasicAgent], + clock: game_clock.MultiIntervalClock, + player_configs: Sequence[formative_memories.AgentConfig], + scene_type_name: str, + verbose: bool = False, +) -> tuple[scene_lib.SceneTypeSpec, CoordinationPayoffs]: + """Add a minigame scene spec. + + Args: + model: the language model to use. + game_master_memory: the game master memory to use. + players: the players to use. + clock: the clock to use. + player_configs: the player configs to use. + scene_type_name: the name of the scene type. + verbose: whether to print verbose output or not. + + Returns: + choice_scene_type: the choice scene type. + """ + defection_option = 'The Princess of Wales' + cooperation_option = 'The Crooked Billet' + action_spec = agent_lib.ActionSpec( + call_to_action='Which pub would {name} go to watch the game?', + output_type=agent_lib.OutputType.CHOICE, + options=(defection_option, cooperation_option), + tag='choice', + ) + player_multipliers = { + cfg.name: cfg.extras['preference'] for cfg in player_configs + } + option_multiplier = PUB_QUALITY + + coordination_payoffs = CoordinationPayoffs( + model=model, + memory=game_master_memory, + option_multipliers=option_multiplier, + player_multipliers=player_multipliers, + resolution_scene=DECISION_SCENE_TYPE, + players=players, + acting_player_names=[cfg.name for cfg in player_configs], + outcome_summarization_fn=outcome_summary_fn, + clock_now=clock.now, + name='scoring function', + verbose=verbose, + ) + decision_env = game_master.GameMaster( + model=model, + memory=game_master_memory, + clock=clock, + name=f'{scene_type_name} decision environment', + players=players, + components=[coordination_payoffs], + action_spec=action_spec, + update_thought_chain=[thought_chains_lib.identity], + randomise_initiative=True, + player_observes_event=False, + concurrent_externalities=False, + verbose=verbose, + ) + + premise = { + player.name: [ + f'{player.name} realises it is time to go watch the game at a pub.' + ] + for player in players + } + + choice_scene_type = scene_lib.SceneTypeSpec( + name=scene_type_name, + premise=premise, + action_spec=action_spec, + override_game_master=decision_env, + ) + return choice_scene_type, coordination_payoffs + + +def configure_scenes( + model: language_model.LanguageModel, + game_master_memory: associative_memory.AssociativeMemory, + players: Sequence[basic_agent.BasicAgent], + clock: game_clock.MultiIntervalClock, + main_player_configs: Sequence[formative_memories.AgentConfig], + supporting_player_configs: Sequence[formative_memories.AgentConfig], +) -> tuple[Sequence[scene_lib.SceneSpec], CoordinationPayoffs]: + """Configure the scene storyboard structure. + + Args: + model: the language model to use. + game_master_memory: the game master memory to use. + players: the players to use. + clock: the clock to use. + main_player_configs: configs for the main characters + supporting_player_configs: configs for the supporting characters + + Returns: + scenes: a sequence of scene specifications + """ + + player_configs = list(main_player_configs) + list(supporting_player_configs) + + choice_scene_spec, coordination_payoffs = add_choice_scene_spec( + model=model, + game_master_memory=game_master_memory, + players=players, + clock=clock, + player_configs=main_player_configs, + scene_type_name=DECISION_SCENE_TYPE, + ) + + scenes = [] + for i in range(NUM_GAMES): + scene = random.choice(SOCIAL_CONTEXT) + scene_specs = { + 'social': scene_lib.SceneTypeSpec( + name='day', + premise={ + cfg.name: [ + scene.format( + player_name=cfg.name, + players=', '.join([player.name for player in players]), + ) + ] + for cfg in player_configs + }, + ), + } + + scene_specs[DECISION_SCENE_TYPE] = choice_scene_spec + scenes = scenes + [ + scene_lib.SceneSpec( + scene_type=scene_specs['social'], + start_time=START_TIME + i * TIME_INCREMENT_BETWEEN_SCENES, + participant_configs=main_player_configs, + num_rounds=1, + ), + scene_lib.SceneSpec( + scene_type=scene_specs[DECISION_SCENE_TYPE], + start_time=START_TIME + + i * TIME_INCREMENT_BETWEEN_SCENES + + datetime.timedelta(hours=8), + participant_configs=main_player_configs, + num_rounds=1, + ), + ] + return (scenes, coordination_payoffs) + + +def outcome_summary_fn( + # `binary_joint_action` should be type Mapping[str, bool] (ie bool not int). + joint_action: Mapping[str, str], + rewards: Mapping[str, float], +) -> Mapping[str, str]: + """Summarize the outcome of a decision scene.""" + + results = {} + for name, score in rewards.items(): + if score > 0.9: + outcome_str = 'had a great time watching the game!' + elif score > 0.5: + outcome_str = ( + 'had an ok time watching the game, but it could have been better if' + ' more friends showed up' + ) + else: + outcome_str = ( + 'had a bad time watching the game, since barely any of their friends' + ' showed up' + ) + results[name] = outcome_str + + print(joint_action) + print(results) + return results + + +class Simulation(Runnable): + """Define the simulation API object for the launch script to interact with.""" + + def __init__( + self, + model: language_model.LanguageModel, + embedder: sentence_transformers.SentenceTransformer, + measurements: measurements_lib.Measurements, + agent_module: types.ModuleType = basic_agent__main_role, + resident_visitor_modules: Sequence[types.ModuleType] | None = None, + ): + """Initialize the simulation object. + + The launch script assumes this API object has a run() method. + + Args: + model: the language model to use. + embedder: the sentence transformer to use. + measurements: the measurements object to use. + agent_module: the agent module to use for all main characters. + resident_visitor_modules: optionally, use different modules for majority + and minority parts of the focal population. + """ + if resident_visitor_modules is None: + self._two_focal_populations = False + self._agent_module = agent_module + else: + self._two_focal_populations = True + self._resident_agent_module, self._visitor_agent_module = ( + resident_visitor_modules + ) + + self._model = model + self._embedder = embedder + self._measurements = measurements + + self._clock = game_clock.MultiIntervalClock( + start=SETUP_TIME, + step_sizes=[MAJOR_TIME_STEP, MINOR_TIME_STEP]) + + importance_model = importance_function.AgentImportanceModel(self._model) + importance_model_gm = importance_function.ConstantImportanceModel() + self._blank_memory_factory = blank_memories.MemoryFactory( + model=self._model, + embedder=self._embedder, + importance=importance_model.importance, + clock_now=self._clock.now, + ) + shared_memories, shared_context = get_shared_memories_and_context() + self._formative_memory_factory = formative_memories.FormativeMemoryFactory( + model=self._model, + shared_memories=shared_memories, + blank_memory_factory_call=self._blank_memory_factory.make_blank_memory, + current_date=SETUP_TIME, + ) + + main_player_configs, supporting_player_configs = configure_players() + random.shuffle(main_player_configs) + + num_main_players = len(main_player_configs) + num_supporting_players = len(supporting_player_configs) + + self._all_memories = {} + + main_player_memory_futures = [] + with concurrency.executor(max_workers=num_main_players) as pool: + for player_config in main_player_configs: + future = pool.submit(self._make_player_memories, + config=player_config) + main_player_memory_futures.append(future) + for player_config, future in zip(main_player_configs, + main_player_memory_futures): + self._all_memories[player_config.name] = future.result() + + if num_supporting_players > 0: + supporting_player_memory_futures = [] + with concurrency.executor(max_workers=num_supporting_players) as pool: + for player_config in supporting_player_configs: + future = pool.submit(self._make_player_memories, + config=player_config) + supporting_player_memory_futures.append(future) + for player_config, future in zip(supporting_player_configs, + supporting_player_memory_futures): + self._all_memories[player_config.name] = future.result() + + main_players = [] + for idx, player_config in enumerate(main_player_configs): + kwargs = dict( + config=player_config, + model=self._model, + memory=self._all_memories[player_config.name], + clock=self._clock, + update_time_interval=MAJOR_TIME_STEP, + ) + if self._two_focal_populations: + if idx == 0: + player = self._visitor_agent_module.build_agent(**kwargs) + else: + player = self._resident_agent_module.build_agent(**kwargs) + else: + player = self._agent_module.build_agent(**kwargs) + + main_players.append(player) + + supporting_players = [] + for player_config in supporting_player_configs: + conversation_style = generic_components.constant.ConstantComponent( + name='guiding principle of good conversation', + state=player_traits_and_styles.get_conversation_style( + player_config.name)) + player = basic_agent__supporting_role.build_agent( + config=player_config, + model=self._model, + memory=self._all_memories[player_config.name], + clock=self._clock, + update_time_interval=MAJOR_TIME_STEP, + additional_components=[conversation_style], + ) + supporting_players.append(player) + + self._all_players = main_players + supporting_players + + game_master_memory = associative_memory.AssociativeMemory( + sentence_embedder=self._embedder, + importance=importance_model_gm.importance, + clock=self._clock.now) + + self._primary_environment, self._game_master_memory = ( + basic_game_master.build_game_master( + model=self._model, + embedder=self._embedder, + importance_model=importance_model_gm, + clock=self._clock, + players=self._all_players, + shared_memories=shared_memories, + shared_context=shared_context, + blank_memory_factory=self._blank_memory_factory, + cap_nonplayer_characters_in_conversation=0, + memory=game_master_memory, + thought_chain=[thought_chains_lib.identity], + ) + ) + self._scenes, self._coordination_payoffs = configure_scenes( + model=self._model, + game_master_memory=game_master_memory, + players=self._all_players, + clock=self._clock, + main_player_configs=main_player_configs, + supporting_player_configs=supporting_player_configs, + ) + + self._secondary_environments = [] + + self._init_premise_memories( + setup_time=SETUP_TIME, + main_player_configs=main_player_configs, + supporting_player_configs=supporting_player_configs, + shared_memories=shared_memories, + scenario_premise=SCENARIO_PREMISE, + ) + + def _make_player_memories(self, config: formative_memories.AgentConfig): + """Make memories for a player.""" + mem = self._formative_memory_factory.make_memories(config) + # Inject player-specific memories declared in the agent config. + for extra_memory in config.extras['player_specific_memories']: + mem.add(f'{extra_memory}', tags=['initial_player_specific_memory']) + return mem + + def _init_premise_memories( + self, + setup_time: datetime.datetime, + main_player_configs: list[formative_memories.AgentConfig], + supporting_player_configs: list[formative_memories.AgentConfig], + shared_memories: Sequence[str], + scenario_premise: Sequence[str], + ) -> None: + """Initialize player memories. + + Args: + setup_time: the time to set the clock to before initializing memories + main_player_configs: configs for the main characters + supporting_player_configs: configs for the supporting characters + shared_memories: memories shared by all players, the game master, and NPCs + scenario_premise: premise observation shared by all players and the game + master. + """ + player_configs = main_player_configs + supporting_player_configs + self._clock.set(setup_time) + + for premise in scenario_premise: + self._game_master_memory.add(premise) + for player in self._all_players: + player.observe(premise) + + for shared_memory in shared_memories: + self._game_master_memory.add(shared_memory) + for player in self._all_players: + player.observe(shared_memory) + + # The game master also observes all the player-specific memories. + for player_config in player_configs: + extra_memories = player_config.extras['player_specific_memories'] + for extra_memory in extra_memories: + self._game_master_memory.add(extra_memory) + + def __call__(self) -> str: + """Run the simulation. + + Returns: + html_results_log: browseable log of the simulation in HTML format + """ + html_results_log = basic_game_master.run_simulation( + model=self._model, + players=self._all_players, + primary_environment=self._primary_environment, + secondary_environments=self._secondary_environments, + clock=self._clock, + scenes=self._scenes, + ) + + print('Overall scores per player:') + player_scores = self._coordination_payoffs.get_scores() + if self._two_focal_populations: + idx = 0 + for player_name, score in player_scores.items(): + if idx == 0: + print('Visitor') + else: + print('Resident') + print(f' {player_name}: {score}') + idx += 1 + else: + for player_name, score in player_scores.items(): + print(f'{player_name}: {score}') + + return html_results_log