sgongora27 commited on
Commit
2b659a0
·
1 Parent(s): 9f6e678
Files changed (7) hide show
  1. app.py +150 -0
  2. config.ini +7 -0
  3. environment.yml +11 -0
  4. example_worlds.py +162 -0
  5. models.py +54 -0
  6. prompts.py +218 -0
  7. world.py +357 -0
app.py ADDED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ from gradio import ChatMessage
3
+ import re
4
+ import sys
5
+ import time
6
+ import json
7
+ import os
8
+ import jsonpickle
9
+ import configparser
10
+
11
+ import example_worlds
12
+ from models import GeminiModel
13
+ from prompts import prompt_narrate_current_scene, prompt_world_update, prompt_describe_objective
14
+
15
+ PATH_GAMELOGS = 'logs'
16
+
17
+ # config
18
+ config = configparser.ConfigParser()
19
+ config.read('config.ini')
20
+
21
+ # language of the game
22
+ language = config['Options']['Language']
23
+
24
+ # Initialize the model and disable the safety settings
25
+ reasoning_model_name = config['Models']['ReasoningModel']
26
+ narrative_model_name = config['Models']['NarrativeModel']
27
+ reasoning_model = GeminiModel("API_key", reasoning_model_name)
28
+ narrative_model = GeminiModel("API_key", narrative_model_name)
29
+
30
+ # Create a name for the log file
31
+ timestamp = time.time()
32
+ today = time.gmtime(timestamp)
33
+ log_filename = f"{today[0]}_{today[1]}_{today[2]}_{str(int(time.time()))[-5:]}.json"
34
+
35
+ # The game loop
36
+ def game_loop(message, history):
37
+ global last_player_position
38
+ global number_of_turns
39
+ global game_log_dictionary
40
+
41
+ number_of_turns+=1
42
+ game_log_dictionary[number_of_turns] = {}
43
+ game_log_dictionary[number_of_turns]["date"] = time.ctime(time.time())
44
+ game_log_dictionary[number_of_turns]["previous_symbolic_world_state"] = jsonpickle.encode(world, unpicklable=False)
45
+ game_log_dictionary[number_of_turns]["previous_rendered_world_state"] = world.render_world()
46
+ game_log_dictionary[number_of_turns]["user_input"] = message
47
+
48
+
49
+ answer = ""
50
+
51
+ # Get the changes in the world
52
+ prompt_update = prompt_world_update(world.render_world(), message, language=language)
53
+ response_update = reasoning_model.prompt_model(prompt_update)
54
+
55
+ # Show the detected changes in the fictional world
56
+ print("🛠️ Predicted outcomes of the player input 🛠️")
57
+ print(f"> Player input: {message}")
58
+ try:
59
+ predicted_outcomes = re.sub(r'#([^#]*?)#','',response_update)
60
+ print(f"{predicted_outcomes}\n")
61
+ game_log_dictionary[number_of_turns]["predicted_outcomes"] = predicted_outcomes
62
+
63
+ except Exception as e:
64
+ print (f"Error: {e}")
65
+
66
+ # World update
67
+ world.update(response_update)
68
+ game_log_dictionary[number_of_turns]["updated_symbolic_world_state"] = jsonpickle.encode(world, unpicklable=True)
69
+ game_log_dictionary[number_of_turns]["updated_rendered_world_state"] = world.render_world()
70
+
71
+ if last_player_position is not world.player.location:
72
+ #Narrate new scene
73
+ last_player_position = world.player.location
74
+ new_scene_narration = narrative_model.prompt_model(
75
+ prompt_narrate_current_scene(world.render_world(),
76
+ previous_narrations = world.player.visited_locations[world.player.location.name],
77
+ language=language)
78
+
79
+ )
80
+ world.player.visited_locations[world.player.location.name]+=[new_scene_narration]
81
+ answer += f"\n{new_scene_narration}\n\n"
82
+ else:
83
+ #Narrate actions in the current scene
84
+ try:
85
+ answer+= f"{re.findall(r'#([^#]*?)#',response_update)[0]}\n"
86
+ except Exception as e:
87
+ print (f"Error: {e}")
88
+
89
+
90
+ print(f"\n🌎 World state 🌍\n>Player input: {message}\n{world.render_world()}\n")
91
+
92
+ if world.check_objective():
93
+ if language=='es':
94
+ answer += "\n\n🎯¡Completaste el objetivo!"
95
+ else:
96
+ answer += "\n\n🎯You have completed your quest!"
97
+
98
+ game_log_dictionary[number_of_turns]["narration"] = answer
99
+
100
+ # Dump the whole gamelog to a json file after this turn
101
+ with open(os.path.join(PATH_GAMELOGS,log_filename), 'w', encoding='utf-8') as f:
102
+ json.dump(game_log_dictionary, f, ensure_ascii=False, indent=4)
103
+
104
+ return answer.replace("<",r"\<").replace(">", r"\>")
105
+
106
+ # Instantiate the world
107
+ world_id = config["Options"]["WorldID"]
108
+ world = example_worlds.get_world(world_id, language=language)
109
+
110
+ # Initialize variables
111
+ last_player_position = world.player.location
112
+ number_of_turns = 0
113
+ game_log_dictionary = {}
114
+ game_log_dictionary["narrative_model_name"] = narrative_model_name
115
+ game_log_dictionary["reasoning_model_name"] = reasoning_model_name
116
+
117
+ print(f"\n🌎 World state 🌍\n{world.render_world()}\n")
118
+ game_log_dictionary[0] = {}
119
+ game_log_dictionary[0]["date"] = time.ctime(time.time())
120
+ game_log_dictionary[0]["initial_symbolic_world_state"] = jsonpickle.encode(world, unpicklable=True)
121
+ game_log_dictionary[0]["initial_rendered_world_state"] = world.render_world()
122
+
123
+ #Generate a description of the starting scene
124
+ starting_narration = narrative_model.prompt_model(
125
+ prompt_narrate_current_scene(world.render_world(),
126
+ previous_narrations = world.player.visited_locations[world.player.location.name],
127
+ language=language,
128
+ starting_scene=True))
129
+ world.player.visited_locations[world.player.location.name]+=[starting_narration]
130
+
131
+ #Generate a description of the main objective
132
+ narrated_objective = narrative_model.prompt_model(prompt_describe_objective(world.objective, language=language))
133
+ starting_narration += f"\n\n🎯 {re.findall(r'#([^#]*?)#',narrated_objective)[0]}"
134
+ game_log_dictionary[0]["starting_narration"] = starting_narration
135
+
136
+ with open(os.path.join(PATH_GAMELOGS,log_filename), 'w', encoding='utf-8') as f:
137
+ json.dump(game_log_dictionary, f, ensure_ascii=False, indent=4)
138
+
139
+ #Instantiate the Gradio app
140
+ gradio_interface = gr.ChatInterface(
141
+ fn=game_loop,
142
+ chatbot = gr.Chatbot(height=500, value=[{"role": "assistant", "content": starting_narration.replace("<",r"\<").replace(">", r"\>")}],
143
+ bubble_full_width = False, show_copy_button = False, type='messages'),
144
+ textbox=gr.Textbox(placeholder="What do you want to do?", container=False, scale=5),
145
+ title="PAYADOR",
146
+ theme="Soft",
147
+ type='messages',
148
+ )
149
+
150
+ gradio_interface.launch(inbrowser=False)
config.ini ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ [Options]
2
+ Language = es
3
+ WorldID = 1
4
+
5
+ [Models]
6
+ NarrativeModel = gemini-1.0-pro
7
+ ReasoningModel = gemini-1.5-flash
environment.yml ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: payador
2
+ channels:
3
+ - conda-forge
4
+ - defaults
5
+ dependencies:
6
+ - python=3.12
7
+ - pip
8
+ - pip:
9
+ - google-generativeai==0.8
10
+ - jsonpickle==4.0
11
+ - gradio==5.*
example_worlds.py ADDED
@@ -0,0 +1,162 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Includes two example worlds to experiment with different scenarios."""
2
+
3
+ import random
4
+ from world import Character, Item, Location, Puzzle, World
5
+
6
+
7
+ def get_world(arg: str, language='en') -> World:
8
+ if arg=='2':
9
+ if language == 'es':
10
+ return get_world_2_spanish()
11
+ else:
12
+ return get_world_2_english()
13
+ else:
14
+ if language == 'es':
15
+ return get_world_1_spanish()
16
+ else:
17
+ return get_world_1_english()
18
+
19
+ def get_world_1_english() -> World:
20
+ """A simple fictional world, used as an example in Figure 3 of the paper."""
21
+ item_1 = Item("Apple",
22
+ ["A fruit that can be eaten", "It is round-shaped and green"])
23
+ item_2 = Item("Toy car",
24
+ ["A tiny toy purple car", "It looks brand new"])
25
+ item_3 = Item("Mate",
26
+ ["A classical mate, ready to drink!", "It contains some yerba", "You can drink this to boost your energy!"])
27
+
28
+ place_1 = Location("Garden",
29
+ ["A beautiful garden", "There is a statue in the center"],
30
+ items = [item_2])
31
+ place_2 = Location("Cabin",
32
+ ["A small cabin", "It looks like no one has lived here for a while"])
33
+ place_3 = Location("Mansion hall",
34
+ ["A big hall", "There is a big staircase"])
35
+
36
+ two_random_numbers = [random.randrange(0, 10) for i in range(2)]
37
+ puzzle1 = Puzzle("puzzle",["There's a symbol of a microphone and below a letter that says how to open the door"],
38
+ f"To unlock this door, you have to say out loud the sum of {str(two_random_numbers[0])} and {str(two_random_numbers[1])}.",
39
+ f"The answer is {str(two_random_numbers[0] + two_random_numbers[1])} ")
40
+
41
+ place_1.connecting_locations+=[place_2,place_3]
42
+ place_2.connecting_locations+=[place_1]
43
+ place_3.connecting_locations+=[place_1]
44
+ place_1.block_passage(place_3,puzzle1)
45
+
46
+ player = Character("Alicia",
47
+ ["She is wearing a long skirt","She likes to sing"],
48
+ inventory=[item_1],
49
+ location=place_1)
50
+ npc = Character("Javier",
51
+ ["He has a long beard", "He loves to restore furtniture"],
52
+ inventory=[item_3],
53
+ location=place_3)
54
+
55
+ the_world = World(player)
56
+ the_world.add_locations([place_1, place_2, place_3])
57
+ the_world.add_items([item_1, item_2, item_3])
58
+ the_world.add_character(npc)
59
+ the_world.set_objective(item_2,place_3)
60
+
61
+ return the_world
62
+
63
+ def get_world_1_spanish() -> World:
64
+ item_1 = Item("Manzana",
65
+ ["Una fruta que puede ser comida", "Es redonda y verde"])
66
+ item_2 = Item("Auto de juguete",
67
+ ["Un pequeño auto de juguete de color púrpura", "Luce como recién comprado"])
68
+ item_3 = Item("Mate",
69
+ ["Un mate clásico ¡listo para tomar!", "Contiene algo de yerba", "¡Puedes tomar esto para mejorar tu energía!"])
70
+
71
+ place_1 = Location("Jardín",
72
+ ["Un jardín hermoso", "Hay una estatua en el centro"],
73
+ items = [item_2])
74
+ place_2 = Location("Cabaña",
75
+ ["Una pequeña cabaña", "Parece que nadie ha vivido acá por un tiempo"])
76
+ place_3 = Location("Hall de la Mansión",
77
+ ["Un hall grande", "Hay una enorme escalera principal"])
78
+
79
+ two_random_numbers = [random.randrange(0, 10) for i in range(2)]
80
+ puzzle1 = Puzzle("puzzle",["Hay un dibujo de un micrófono y debajo un letrero, con la premisa para abrir la puerta"],
81
+ f"Para desbloquear esta puerta, hay que decir en voz alta la suma de {str(two_random_numbers[0])} y {str(two_random_numbers[1])}.",
82
+ f"La respuesta es {str(two_random_numbers[0] + two_random_numbers[1])} ")
83
+
84
+ place_1.connecting_locations+=[place_2,place_3]
85
+ place_2.connecting_locations+=[place_1]
86
+ place_3.connecting_locations+=[place_1]
87
+ place_1.block_passage(place_3,puzzle1)
88
+
89
+
90
+ player = Character("Alicia",
91
+ ["Está usando una falda larga","Le gusta cantar"],
92
+ inventory=[item_1],
93
+ location=place_1)
94
+ npc = Character("Javier",
95
+ ["Tiene una barba larga", "Le encanta restaurar muebles"],
96
+ inventory=[item_3],
97
+ location=place_3)
98
+
99
+
100
+
101
+ the_world = World(player)
102
+ the_world.add_locations([place_1, place_2, place_3])
103
+ the_world.add_items([item_1, item_2, item_3])
104
+ the_world.add_character(npc)
105
+ the_world.set_objective(item_2,place_3)
106
+
107
+
108
+ return the_world
109
+
110
+ def get_world_2_english() -> World:
111
+ """Use this world to test more complex cases, like blocked passages between locations."""
112
+ item_1 = Item("Apple",
113
+ ["A fruit that can be eaten", "It is round-shaped and red"])
114
+ item_2 = Item("Key",
115
+ ["A key to open a lock", "It is golden", "It is engraved with a strange coat of arms"])
116
+ item_3 = Item("A grey Hammer",
117
+ ["A great grey hammer that can be used to break things", "It is so heavy..."])
118
+ item_4 = Item("Lock",
119
+ ["A strong lock engraved with a coat of arms", "It seems that you cannot open it with your hands"])
120
+ item_5 = Item("Note",
121
+ ["A paper with a note", "You can read 'Go to the kitchen to know the truth'"])
122
+ item_6 = Item("Flashlight",
123
+ ["A flashlight without batteries"])
124
+ item_7 = Item("A green Hammer",
125
+ ["A small green hammer", "It is just a toy and you cannot break anything with it."])
126
+ item_8 = Item("A wall of flames",
127
+ ["The heat is really intense but it is a small fire anyway"])
128
+ item_9 = Item("A metal flower",
129
+ ["A strange flower"])
130
+ item_10 = Item("A fire extinguisher",
131
+ ["You can control small fires with this."])
132
+
133
+ place_3 = Location ("Garden",
134
+ ["A small garden below the kitchen"],
135
+ items = [item_9])
136
+ place_2 = Location("Kitchen",
137
+ ["A beautiful well-lit kitchen"],
138
+ items = [item_6,item_10])
139
+ place_2.connecting_locations = [place_3]
140
+ place_2.block_passage(place_3, item_8, symmetric=False)
141
+
142
+ place_1 = Location("Cellar",
143
+ ["There is a metal door locked by a lock", "You can see damp patches on the walls"],
144
+ items = [item_2, item_3, item_5, item_7])
145
+ place_1.connecting_locations = [place_2]
146
+ place_1.block_passage(place_2, item_4)
147
+
148
+ player = Character("Cid",
149
+ ["A tall soldier"],
150
+ inventory = [item_1],
151
+ location = place_1)
152
+ npc = Character("Elvira",
153
+ ["A little girl", "Her favorite food is apple pie, but she enjoys eating any fruit", "She can't read yet"],
154
+ location= place_1)
155
+
156
+ the_world = World(player)
157
+ the_world.add_locations([place_1,place_2,place_3])
158
+ the_world.add_items([item_1,item_2,item_3,item_4,item_5,
159
+ item_6, item_7,item_8, item_9, item_10])
160
+ the_world.add_character(npc)
161
+
162
+ return the_world
models.py ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Load models to use them as a narrator and a common-sense oracle in the PAYADOR pipeline."""
2
+ import google.generativeai as genai
3
+ import requests
4
+
5
+
6
+ class GeminiModel():
7
+ def __init__ (self, api_key_file:str, model_name:str = "gemini-pro") -> None:
8
+ """"Initialize the Gemini model using an API key."""
9
+ self.safety_settings = [
10
+ {
11
+ "category": "HARM_CATEGORY_DANGEROUS",
12
+ "threshold": "BLOCK_NONE",
13
+ },
14
+ {
15
+ "category": "HARM_CATEGORY_HARASSMENT",
16
+ "threshold": "BLOCK_NONE",
17
+ },
18
+ {
19
+ "category": "HARM_CATEGORY_HATE_SPEECH",
20
+ "threshold": "BLOCK_NONE",
21
+ },
22
+ {
23
+ "category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
24
+ "threshold": "BLOCK_NONE",
25
+ },
26
+ {
27
+ "category": "HARM_CATEGORY_DANGEROUS_CONTENT",
28
+ "threshold": "BLOCK_NONE",
29
+ },
30
+ ]
31
+ genai.configure(api_key=get_api_key(api_key_file))
32
+ self.model = genai.GenerativeModel(model_name)
33
+
34
+ def prompt_model(self,prompt: str) -> str:
35
+ """Prompt the Gemini model."""
36
+ return self.model.generate_content(prompt, safety_settings=self.safety_settings).text
37
+
38
+
39
+ def prompt_HF_API (prompt: str, model: str = "microsoft/Phi-3-mini-4k-instruct", api_key_file: str = "HF_API_key"):
40
+ API_URL = f"https://api-inference.huggingface.co/models/{model}"
41
+
42
+ headers = {"Authorization": f"Bearer {get_api_key(api_key_file)}"}
43
+ payload = {"inputs": prompt}
44
+
45
+ output = requests.post(API_URL, headers=headers, json=payload).json()
46
+
47
+ return output[0]["generated_text"]
48
+
49
+ def get_api_key(path: str) -> str:
50
+ """Load an API key from path."""
51
+ key = ""
52
+ with open(path) as f:
53
+ key = f.readline()
54
+ return key
prompts.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ def prompt_describe_objective (objective, language:str = 'en') -> str:
2
+ prompt = ""
3
+
4
+ if language == 'es':
5
+ prompt = prompt_describe_objective_spanish(objective)
6
+ else:
7
+ prompt = prompt_describe_objective_english(objective)
8
+
9
+ return prompt
10
+
11
+ def prompt_describe_objective_english (objective) -> str:
12
+
13
+ prompt = "Please, provide an alternative way to narrate the following objective, using simple language: "
14
+ first_component_class = objective[0].__class__.__name__
15
+ second_component_class = objective[1].__class__.__name__
16
+
17
+ if first_component_class == "Character" and second_component_class == "Location":
18
+ prompt+= f'"You have to go to <{objective[1].name}>."'
19
+ elif first_component_class == "Character" and second_component_class == "Item":
20
+ prompt+= f'"<{objective[0].name}> has to get the item <{objective[1].name}>."'
21
+ elif first_component_class == "Item" and second_component_class == "Location":
22
+ prompt+= f'"You have to leave item <{objective[0].name}> in place <{objective[1].name}>."'
23
+
24
+ prompt+="\nPut your generated narration between # characters. For example: # You have to get the <key> # or # You have to reach the <castle> #"
25
+
26
+ return prompt
27
+
28
+ def prompt_describe_objective_spanish (objective) -> str:
29
+
30
+ prompt = "Por favor, dame una forma alternativa de narrar el siguiente objetivo, usando lenguaje simple: "
31
+ first_component_class = objective[0].__class__.__name__
32
+ second_component_class = objective[1].__class__.__name__
33
+
34
+ if first_component_class == "Character" and second_component_class == "Location":
35
+ prompt+= f'"Tienes que ir a <{objective[1].name}>."'
36
+ elif first_component_class == "Character" and second_component_class == "Item":
37
+ prompt+= f'"<{objective[0].name}> tiene que conseguir el objeto <{objective[1].name}>."'
38
+ elif first_component_class == "Item" and second_component_class == "Location":
39
+ prompt+= f'"Tienes que dejar el objeto <{objective[0].name}> en el lugar <{objective[1].name}>."'
40
+
41
+ prompt+="\nPon tu narración generada entre caracteres #. Por ejemplo: # Tienes que conseguir la <llave> # o # Tienes que llegaral <Castillo> #"
42
+
43
+ return prompt
44
+
45
+ def prompt_narrate_current_scene (world_state: str, previous_narrations: 'list[str]', language: str = 'en', starting_scene: bool = False) -> str:
46
+ prompt = ""
47
+
48
+ if language == 'es':
49
+ prompt = prompt_narrate_current_scene_spanish(world_state, previous_narrations, starting_scene)
50
+ else:
51
+ prompt = prompt_narrate_current_scene_english(world_state, previous_narrations, starting_scene)
52
+
53
+
54
+ return prompt
55
+
56
+ def prompt_narrate_current_scene_english (world_state: str, previous_narrations: 'list[str]', starting_scene: bool = False) -> str:
57
+
58
+ prompt = "You are a storyteller. Take the following state of the world and narrate it in a few sentences. Be careful not to include details that contradict the current state of the world or that move the story forward. Also, try to use simple sentences and do not overuse poetic language.\n"
59
+
60
+ if starting_scene:
61
+ prompt += "\nTake into account that this is the first scene in the story: introduce the main character, creating a small background story and why that character is in that specific location.\n"
62
+ elif len(previous_narrations)==0:
63
+ prompt += "Take into account that the player already knows what the main character looks like, so do not mention anything about that. However, it is the first time the player visits this place, so make sure to describe it exhaustively."
64
+ else:
65
+ prompt += "Take into account that the player already knows what the main character looks like, so do not mention anything about that. Additionally, it is not the first time the player visits this place. Next I’ll give you some previous narrations of this same location (from oldest to newest) so you can be sure to not repeat the same details again:\n"
66
+ for narration in previous_narrations:
67
+ prompt+=f'- {narration}\n'
68
+
69
+ prompt+= "\nRemember: you are talking to the player, describing what his or her character has and what he or she can see or feel."
70
+
71
+ prompt +=f"""This is the state of the world at the moment:
72
+ {world_state}
73
+ """
74
+
75
+ return prompt
76
+
77
+ def prompt_narrate_current_scene_spanish (world_state: str, previous_narrations: 'list[str]', starting_scene: bool = False) -> str:
78
+
79
+ prompt = f"""Eres un narrador. Toma el siguiente estado del mundo y nárralo en unas pocas oraciones. Ten cuidado de no incluir detalles que contradigan el estado del mundo actual, o que hagan avanzar la historia. Además, intenta usar oraciones simples, sin abusar del lenguaje poético."""
80
+
81
+ if starting_scene:
82
+ prompt += "\nTen en cuenta que esta es la primera escena en la historia narradad: presenta al personaje del jugador, creando un pequeño trasfondo y por qué este personaje está en ese lugar específicamente.\n"
83
+ elif len(previous_narrations)==0:
84
+ prompt += "Ten en cuenta que el jugador ya conoce a su personaje, y cómo se ve, así que no menciones nada sobre esto. Sin embargo, es la primera vez que el jugador visita este lugar, así que describelo exaustivamente."
85
+ else:
86
+ prompt += "Ten en cuenta que el jugador ya conoce a s upersonaje, y cómo se ve, así que no menciones nada sobre esto. Además, no es la primera vez que el jugador visita este lugar. A continuación te daré algunas narraciones previas de este mismo lugar (de la más antigua a la más nueva), así te puedes asegurar de no repetir los mismos detalles de nuevo:\n"
87
+ for narration in previous_narrations:
88
+ prompt+=f'- {narration}\n'
89
+
90
+ prompt+= "\nRecuerda: le estás hablando al jugador, describiendo lo que su personaje tiene y lo que puede sentir o ver."
91
+
92
+ prompt +=f"""Este es el estado del mundo en este momnto:
93
+ {world_state}
94
+ """
95
+
96
+ return prompt
97
+
98
+ def prompt_world_update (world_state: str, input: str, language: str = 'en') -> str:
99
+ prompt = ""
100
+
101
+ if language == 'es':
102
+ prompt = prompt_world_update_spanish(world_state, input)
103
+ else:
104
+ prompt = prompt_world_update_english(world_state, input)
105
+
106
+
107
+ return prompt
108
+
109
+ def prompt_world_update_spanish (world_state: str, input: str) -> str:
110
+ prompt = f"""Eres un narrador. Estás manejando un mundo ficticio, y el jugador puede interactuar con él. Este es el estado del mundo en este momento:
111
+ {world_state}\n\n
112
+ Explica los cambios en el mundo a partir de las acciones del jugador en esta entrada "{input}".
113
+
114
+ Aquí hay algunas aclaraciones:
115
+ (A) Si un pasaje está bloqueado, significa que el jugador debe desbloquearlo antes de poder acceder al lugar. Aunque el jugador te diga que va a acceder al lugar bloqueado, tienes que estar seguro de que está cumpliendo con lo pedido para permitirle desbloquear el acceso, por ejemplo usando una llave o resolviendo un puzzle.
116
+ (B) Presta atención a a la descripción de los componentes y sus capacidades.
117
+ (C) No asumas que lo que dice el jugador siempre tiene sentido; quizás esas acciones intentan hacer algo que el mundo no lo permite.
118
+ (D) Sigue siempre el siguiente formato con las tres categorías, usando "None" en cada caso si no hay cambios y repite la categoría por cada caso:
119
+ - Moved object: <object> now is in <new_location>
120
+ - Blocked passages now available: <now_reachable_location>
121
+ - Your location changed: <new_location>
122
+
123
+ Aquí hay algunos ejemplos sobre el formato, descrito en el anterior punto (D).
124
+ Ejemplo 1
125
+ - Moved object: <hacha> now is in <Inventory>
126
+ - Blocked passages now available: None
127
+ - Your location changed: None
128
+
129
+ Ejemplo 2
130
+ - Moved object: None
131
+ - Blocked passages now available: None
132
+ - Your location changed: <Jardín>
133
+
134
+ Ejemplo 3
135
+ - Moved object: <banana> now is in <Inventory>, <botella> now is in <Inventory>, <hacha> now is in <Hall principal>
136
+ - Blocked passages now available: None
137
+ - Your location changed: None
138
+
139
+ Ejemplo 4
140
+ - Moved object: <banana> now is in <Inventory>, <botella> now is in <Inventory>, <hacha> now is in <Hall principal>
141
+ - Blocked passages now available: <Pequeña habitación>
142
+ - Your location changed: None
143
+
144
+ Ejemplo 5
145
+ - Moved object: <banana> now is in <Inventory>, <botella> now is in <Inventory>, <hacha> now is in <Hall principal>
146
+ - Blocked passages now available: <Pequeña habitación>
147
+ - Your location changed: <Pequeña habitación>
148
+
149
+ Ejemplo 6
150
+ - Moved object: <libro> now is in <John>, <lápiz> now is in <Inventory>
151
+ - Blocked passages now available: None
152
+ - Your location changed: None
153
+
154
+ Ejemplo 7
155
+ - Moved object: <computadora> now is in <Susan>
156
+ - Blocked passages now available: None
157
+ - Your location changed: None
158
+
159
+ Por último, puedes agregar una pequeña oración final narrando los cambios detecados en el estado del mundo (¡sin hacer avanzar la historia y sin crear detalles no incluidos en el estado del mundo!) o respondiendo una pregunta del jugador sobre el estado del mundo, usando el formato: #<tu oración final>#
160
+ """
161
+
162
+ return prompt
163
+
164
+ def prompt_world_update_english (world_state: str, input: str) -> str:
165
+ prompt = f"""You are a storyteller. You are managing a fictional world, and the player can interact with it. This is the state of the world at the moment:
166
+ {world_state}\n\n
167
+ Explain the changes in the world after the player actions in this input "{input}".
168
+
169
+ Here are some clarifications:
170
+ (A) If a passage is blocked, then the player must unblock it before being able to reach the place. Even if the player tells you that he is going to access the locked location, you have to be sure that he is complying with what you asked to allow him to unlock the access, for example by using a key or solving a puzzle.
171
+ (B) Pay atenttion to the description of the components and their capabilities.
172
+ (C) Do not assume that the player input always make sense; maybe those actions try to do something that the world does not allow.
173
+ (D) Follow always the following format with the three categories, using "None" in each case if there are no changes and repeat the category for each case:
174
+ - Moved object: <object> now is in <new_location>
175
+ - Blocked passages now available: <now_reachable_location>
176
+ - Your location changed: <new_location>
177
+
178
+ Here you have some examples.
179
+ Example 1
180
+ - Moved object: <axe> now is in <Inventory>
181
+ - Blocked passages now available: None
182
+ - Your location changed: None
183
+
184
+ Example 2
185
+ - Moved object: None
186
+ - Blocked passages now available: None
187
+ - Your location changed: <Garden>
188
+
189
+ Example 3
190
+ - Moved object: <banana> now is in <Inventory>, <bottle> now is in <Inventory>, <axe> now is in <Main Hall>
191
+ - Blocked passages now available: None
192
+ - Your location changed: None
193
+
194
+ Example 4
195
+ - Moved object: <banana> now is in <Inventory>, <bottle> now is in <Inventory>, <axe> now is in <Main Hall>
196
+ - Blocked passages now available: <Small room>
197
+ - Your location changed: None
198
+
199
+ Example 5
200
+ - Moved object: <banana> now is in <Inventory>, <bottle> now is in <Inventory>, <axe> now is in <Main Hall>
201
+ - Blocked passages now available: <Small room>
202
+ - Your location changed: <Small room>
203
+
204
+ Example 6
205
+ - Moved object: <book> now is in <John>, <pencil> now is in <Inventory>
206
+ - Blocked passages now available: None
207
+ - Your location changed: None
208
+
209
+ Example 7
210
+ - Moved object: <computer> now is in <Susan>
211
+ - Blocked passages now available: None
212
+ - Your location changed: None
213
+
214
+
215
+ Finally, you can add a final short sentence narrating the detected changes in the state of the world (without moving the story forward and creating details not included in the state of the world!) or answering a question of the player about the state of the world, using the format: #<your final sentence>#
216
+ """
217
+
218
+ return prompt
world.py ADDED
@@ -0,0 +1,357 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """This module implements the classes needed to represent the fictional world of the game.
2
+
3
+ The world class includes references to the several components (Items, Locations, Character),
4
+ and methods to update according to the detected changes by a language model.
5
+ """
6
+
7
+ import re
8
+ from typing import Type
9
+
10
+
11
+ class Component:
12
+ """A class to represent a component of the world.
13
+
14
+ The components considered in the PAYADOR approach are Items, Locations and Characters.
15
+ """
16
+ def __init__ (self, name:str, descriptions: 'list[str]'):
17
+
18
+ self.name = name
19
+ """the name of the component"""
20
+
21
+ self.descriptions = descriptions
22
+ """a set of natural language descriptions for the component"""
23
+
24
+ class Puzzle (Component):
25
+ """A class to represent a Puzzle"""
26
+
27
+ def __init__(self, name:str, descriptions: 'list[str]', problem: str, answer: str):
28
+
29
+ super().__init__(name, descriptions)
30
+ """inherited from Component"""
31
+
32
+ self.problem = problem
33
+ """the main problem to be solved"""
34
+
35
+ self.answer = answer
36
+ """a possible answer to the riddle or puzzle"""
37
+
38
+ class Item (Component):
39
+ """A class to represent an Item."""
40
+ def __init__ (self, name:str, descriptions: 'list[str]', gettable: bool = True):
41
+
42
+ super().__init__(name, descriptions)
43
+ """inherited from Component"""
44
+
45
+ self.gettable = gettable
46
+ """indicates if the Item can be taken by the player"""
47
+
48
+ class Location (Component):
49
+ """A class to represent a Location in the world."""
50
+ def __init__ (self, name:str, descriptions: 'list[str]', items: 'list[Item]' = None, connecting_locations: 'list[Location]' = None):
51
+
52
+ super().__init__(name, descriptions)
53
+ """inherited from Component"""
54
+
55
+ self.items = items or []
56
+ """a list of the items available in that location"""
57
+
58
+ self.connecting_locations = connecting_locations or []
59
+ """a list of the reachable locations from itself."""
60
+
61
+ self.blocked_locations = {}
62
+ """a dictionary with the name of a location as key and <location,obstacle,symmetric> as value.
63
+ A blocked passage between self and a location means that it
64
+ will be reachable from [self] after overcoming the [obstacle].
65
+ The symmetric variable is a boolean that indicates if, when unblocked,
66
+ [self] will also be reachable from [location].
67
+ """
68
+
69
+ def block_passage(self, location: 'Location', obstacle, symmetric: bool = True):
70
+ """Block a passage between self and location using an obstacle."""
71
+ if location in self.connecting_locations:
72
+ if location.name not in self.blocked_locations:
73
+ self.blocked_locations[location.name] = (location, obstacle, symmetric)
74
+ self.connecting_locations = [x for x in self.connecting_locations if x is not location]
75
+ else:
76
+ raise Exception(f"Error: A blocked passage to {location.name} already exists")
77
+ else:
78
+ raise Exception(f"Error: Two non-conected locations cannot be blocked")
79
+
80
+ def unblock_passage(self, location: 'Location'):
81
+ """Unblock a passage between self and location by adding it to the connecting locations of self.
82
+
83
+ In case that the block was symmetric, self will be added to the connecting locations of location.
84
+ """
85
+ if self.blocked_locations[location.name]:
86
+ self.connecting_locations += [location]
87
+ if self.blocked_locations[location.name][2] and self not in location.connecting_locations:
88
+ location.connecting_locations += [self]
89
+ del self.blocked_locations[location.name]
90
+ else:
91
+ raise Exception("Error: That is not a blocked passage")
92
+
93
+ class Character (Component):
94
+ """A class to represent a character."""
95
+ def __init__ (self, name:str, descriptions: 'list[str]', location:Location, inventory: 'list[Item]' = None):
96
+
97
+ super().__init__(name, descriptions)
98
+ """inherited from Component"""
99
+
100
+ self.inventory = inventory or []
101
+ """a set of Items the carachter has"""
102
+
103
+ self.location = location
104
+ """the location of the character"""
105
+
106
+ self.visited_locations = {self.location.name: []}
107
+ """a dictionary that contains the successive descriptions of the visited places"""
108
+
109
+ def move(self, new_location: Location):
110
+ """Move the character to a new location."""
111
+ if new_location in self.location.connecting_locations:
112
+ self.location = new_location
113
+ if self.location.name not in self.visited_locations:
114
+ self.visited_locations[self.location.name] = []
115
+ else:
116
+ raise Exception(f"Error: {new_location.name} is not reachable")
117
+
118
+ def save_item(self,item: Item, item_location_or_owner):
119
+ """Add an item to the character inventory."""
120
+ if item.gettable:
121
+ if item not in self.inventory:
122
+ self.inventory += [item]
123
+ if item_location_or_owner.__class__.__name__ == 'Character':
124
+ item_location_or_owner.inventory = [i for i in item_location_or_owner.inventory if i is not item]
125
+ elif item_location_or_owner.__class__.__name__ == 'Location':
126
+ item_location_or_owner.items = [i for i in item_location_or_owner.items if i is not item]
127
+ else:
128
+ raise Exception(f"Error: {item.name} is already in your inventory")
129
+ else:
130
+ raise Exception(f"Error: {item.name} cannot be taken")
131
+
132
+ def drop_item (self, item: Item):
133
+ """Leave an item in the current location."""
134
+ self.inventory = [i for i in self.inventory if i is not item]
135
+ self.location.items += [item]
136
+
137
+ def give_item (self, character: 'Character', item: Item):
138
+ """Give an item to another character."""
139
+ try:
140
+ character.save_item(item, self)
141
+ except Exception as e:
142
+ print(e)
143
+
144
+
145
+ class World:
146
+ """A class to represent the fictional world, with references to every component."""
147
+ def __init__ (self, player: Character) -> None:
148
+
149
+ self.items = {}
150
+ """a dictionary of all the Items in the world, with their names as values"""
151
+
152
+ self.characters = {}
153
+ """a dictionary of all the Characters in the world, with their names as values"""
154
+
155
+ self.locations = {}
156
+ """a dictionary of all the Locations in the world, with their names as values"""
157
+
158
+ self.player = player
159
+ """a character for the player"""
160
+
161
+ self.objective = None
162
+ """the current objective for the player in this world"""
163
+
164
+ def set_objective (self, first_component: Type[Component], second_component: Type[Component]):
165
+
166
+ first_component_class = first_component.__class__.__name__
167
+ second_component_class = second_component.__class__.__name__
168
+
169
+ if first_component_class == second_component_class:
170
+ raise Exception(f"Error: Objectives with same two classes {first_component_class} are not allowed")
171
+ elif (first_component_class == "Character" and second_component_class in ["Location", "Item"]) or (second_component_class == "Character" and first_component_class in ["Location", "Item"]):
172
+ self.objective = (first_component, second_component)
173
+ elif (first_component_class == "Item" and second_component_class == "Location") or (second_component_class == "Item" and first_component_class == "Location"):
174
+ self.objective = (first_component, second_component)
175
+ else:
176
+ raise Exception(f"Error: Cannot set objective with classes {first_component_class} and {second_component_class}")
177
+
178
+ def check_objective(self) -> bool:
179
+
180
+ done = False
181
+ first_component_class = self.objective[0].__class__.__name__
182
+ second_component_class = self.objective[1].__class__.__name__
183
+
184
+ if first_component_class == "Character" and second_component_class == "Location":
185
+ if self.objective[0].location == self.objective[1]: done = True
186
+ elif first_component_class == "Character" and second_component_class == "Item":
187
+ if self.objective[1] in self.objective[0].inventory: done = True
188
+ elif first_component_class == "Item" and second_component_class == "Location":
189
+ if self.objective[0] in self.objective[1].items: done = True
190
+
191
+ return done
192
+
193
+ def add_location (self,location: Location) -> None:
194
+ """Add a location to the world."""
195
+ if location.name in self.locations:
196
+ raise Exception(f"Error: Already exists a location called '{location.name}'")
197
+ else:
198
+ self.locations[location.name] = location
199
+
200
+ def add_item (self, item: Item) -> None:
201
+ """Add an item to the world."""
202
+ if item.name in self.items:
203
+ raise Exception(f"Error: Already exists an item called '{item.name}'")
204
+ else:
205
+ self.items[item.name] = item
206
+
207
+ def add_character (self, character: Character) -> None:
208
+ """Add a character to the world."""
209
+ if character.name in self.characters:
210
+ raise Exception(f"Error: Already exists a character called '{character.name}'")
211
+ else:
212
+ self.characters[character.name] = character
213
+
214
+ def add_locations (self,locations: 'list[Location]') -> None:
215
+ """"Add a set of locations to the world."""
216
+ for location in locations:
217
+ self.add_location(location)
218
+
219
+ def add_items (self, items: 'list[Item]') -> None:
220
+ """Add a set of items to the world."""
221
+ for item in items:
222
+ self.add_item(item)
223
+
224
+ def add_characters (self, characters: 'list[Character]') -> None:
225
+ """Add a set of characters to the world."""
226
+ for character in characters:
227
+ self.add_character(character)
228
+
229
+ def render_world(self, *, detail_components:bool = True) -> str:
230
+ """Return the fictional world as a natural language description, using simple sentences.
231
+
232
+ The components described are only those the player can see in the current location.
233
+ If detail_components is False, then the descriptions for each component are not included.
234
+ """
235
+ player_location = self.player.location
236
+ reachable_locations = [f"<{p.name}>" for p in player_location.connecting_locations]
237
+ blocked_passages = [f"<{p}> blocked by <{player_location.blocked_locations[p][1].name}>" for p in player_location.blocked_locations.keys()]
238
+ characters_in_the_scene = [character for character in self.characters.values() if character.location is player_location]
239
+
240
+
241
+ world_description = f'You are in <{player_location.name}>\n'
242
+
243
+ if reachable_locations:
244
+ world_description += f'From <{player_location.name}> you can access: {(", ").join(reachable_locations)}\n'
245
+ else:
246
+ world_description += f'From <{player_location.name}> you can access: None\n'
247
+
248
+ if blocked_passages:
249
+ world_description += f'From <{player_location.name}> there are blocked passages to: {(", ").join(blocked_passages)}\n'
250
+ else:
251
+ world_description += f'From <{player_location.name}> there are blocked passages to: None\n'
252
+
253
+ if self.player.inventory:
254
+ world_description += f'You have the following items in your inventory: {(", ").join([f"<{i.name}>" for i in self.player.inventory])}\n'
255
+ else:
256
+ world_description += f'You have the following items in your inventory: None\n'
257
+
258
+ if player_location.items:
259
+ world_description += f'If you look around, you can see the following items: {(", ").join([f"<{i.name}>" for i in player_location.items])}\n'
260
+ else:
261
+ world_description += f'If you look around, you can see the following items: None\n'
262
+
263
+ if characters_in_the_scene:
264
+ world_description += f'You can see some people: {(", ").join([f"<{c.name}>" for c in characters_in_the_scene])}'
265
+ else:
266
+ world_description += f'You can see some people: None'
267
+
268
+ details = ""
269
+ if detail_components:
270
+ items_in_the_scene = player_location.items + self.player.inventory + [blocked_values[1] for blocked_values in player_location.blocked_locations.values() if isinstance(blocked_values[1], Item)]
271
+ puzzles_in_the_scene = [blocked_values[1] for blocked_values in player_location.blocked_locations.values() if isinstance(blocked_values[1], Puzzle)]
272
+
273
+ details += "\nHere is a description of each component.\n"
274
+ details += f"<{player_location.name}>: This is the player's location. {('. ').join(player_location.descriptions)}.\n"
275
+ details += "Characters:\n"
276
+ details += f"- <Player>: The player is acting as {self.player.name}. {('. ').join(self.player.descriptions)}.\n"
277
+ if len(characters_in_the_scene)>0:
278
+ for character in characters_in_the_scene:
279
+ details += f"- <{character.name}>: {('. ').join(character.descriptions)}."
280
+ if len(character.inventory)>0:
281
+ details += f" This character has the following items: {(', ').join([f'<{i.name}>' for i in character.inventory])}\n"
282
+ items_in_the_scene+= character.inventory
283
+ else:
284
+ details += "\n"
285
+ if len(items_in_the_scene)>0:
286
+ details+="Objects:\n"
287
+ for item in items_in_the_scene:
288
+ details += f"- <{item.name}>: {('. ').join(item.descriptions)}\n"
289
+ if len(puzzles_in_the_scene)>0:
290
+ details+="Puzzles:\n"
291
+ for puzzle in puzzles_in_the_scene:
292
+ details+= f'- <{puzzle.name}>: {('. ').join(puzzle.descriptions)}. The riddle to solve is: "{puzzle.problem}". The expected answer, that you CANNOT tell the player (EVER) is: "{puzzle.answer}".\n'
293
+
294
+ return world_description + '\n' + details
295
+
296
+ def update (self, updates: str) -> None:
297
+ """Does the changes in the world according to the output of the language model.
298
+
299
+ The possible changes considered are:
300
+ - an object was moved
301
+ - a location is now reachable
302
+ - the position of the player changed.
303
+ """
304
+ self.parse_moved_objects(updates)
305
+ self.parse_blocked_passages(updates)
306
+ self.parse_location_change(updates)
307
+
308
+ def parse_moved_objects (self, updates: str) -> None:
309
+ """Parse the output of the language model to update the position of objects.
310
+
311
+ There are three cases:
312
+ - the player has a new item
313
+ - the player gave an item to other character
314
+ - the player dropped an item.
315
+ """
316
+ parsed_objects = re.findall(r".*Moved object:\s*(.+)",updates)
317
+ if 'None' not in parsed_objects:
318
+ parsed_objects_split = re.findall(r"<[^<>]*?>.*?<[^<>]*?>",parsed_objects[0])
319
+ for parsed_object in parsed_objects_split:
320
+ pair = re.findall(r"<([^<>]*?)>.*?<([^<>]*?)>",parsed_object)
321
+ try:
322
+ world_item = self.items[pair[0][0]]
323
+
324
+ if pair[0][1] in ['Inventory', 'Inventario', 'Player', 'Jugador', self.player.name]: #(save_item case)
325
+ item_location = [character for character in list(self.characters.values()) if world_item in character.inventory]
326
+ item_location += [location for location in list(self.locations.values()) if world_item in location.items]
327
+ self.player.save_item(world_item, item_location[0])
328
+
329
+ elif pair[0][1] in self.characters: #(give_item case)
330
+ self.player.give_item(self.characters[pair[0][1]], world_item)
331
+
332
+ else: #(drop_item case)
333
+ self.player.drop_item(world_item)
334
+ except Exception as e:
335
+ print(e)
336
+
337
+ def parse_blocked_passages (self, updates: str) -> None:
338
+ """Parse the output of the language model to update the reachable locations."""
339
+ parsed_blocked_passages = re.findall(r".*Blocked passages now available:\s*(.+)",updates)
340
+ if 'None' not in parsed_blocked_passages:
341
+ parsed_blocked_passages_split = re.findall(r"<([^<>]*?)>",parsed_blocked_passages[0])
342
+ for parsed_passage in parsed_blocked_passages_split:
343
+ try:
344
+ self.locations[self.player.location.name].unblock_passage(self.locations[parsed_passage])
345
+ except Exception as e:
346
+ print (e)
347
+
348
+ def parse_location_change (self, updates: str) -> None:
349
+ """Parse the output of the language model to update the position of the player."""
350
+ parsed_location_change = re.findall(r".*Your location changed: (.+)",updates)
351
+ if "None" not in parsed_location_change:
352
+ parsed_location_change_split = re.findall(r"<([^<>]*?)>",parsed_location_change[0])
353
+ try:
354
+ self.player.move(self.locations[parsed_location_change_split[0]])
355
+ except Exception as e:
356
+ print(e)
357
+