|
"""This module implements the classes needed to represent the fictional world of the game. |
|
|
|
The world class includes references to the several components (Items, Locations, Character), |
|
and methods to update according to the detected changes by a language model. |
|
""" |
|
|
|
import re |
|
from typing import Type |
|
|
|
|
|
class Component: |
|
"""A class to represent a component of the world. |
|
|
|
The components considered in the PAYADOR approach are Items, Locations and Characters. |
|
""" |
|
def __init__ (self, name:str, descriptions: 'list[str]'): |
|
|
|
self.name = name |
|
"""the name of the component""" |
|
|
|
self.descriptions = descriptions |
|
"""a set of natural language descriptions for the component""" |
|
|
|
class Puzzle (Component): |
|
"""A class to represent a Puzzle""" |
|
|
|
def __init__(self, name:str, descriptions: 'list[str]', problem: str, answer: str): |
|
|
|
super().__init__(name, descriptions) |
|
"""inherited from Component""" |
|
|
|
self.problem = problem |
|
"""the main problem to be solved""" |
|
|
|
self.answer = answer |
|
"""a possible answer to the riddle or puzzle""" |
|
|
|
class Item (Component): |
|
"""A class to represent an Item.""" |
|
def __init__ (self, name:str, descriptions: 'list[str]', gettable: bool = True): |
|
|
|
super().__init__(name, descriptions) |
|
"""inherited from Component""" |
|
|
|
self.gettable = gettable |
|
"""indicates if the Item can be taken by the player""" |
|
|
|
class Location (Component): |
|
"""A class to represent a Location in the world.""" |
|
def __init__ (self, name:str, descriptions: 'list[str]', items: 'list[Item]' = None, connecting_locations: 'list[Location]' = None): |
|
|
|
super().__init__(name, descriptions) |
|
"""inherited from Component""" |
|
|
|
self.items = items or [] |
|
"""a list of the items available in that location""" |
|
|
|
self.connecting_locations = connecting_locations or [] |
|
"""a list of the reachable locations from itself.""" |
|
|
|
self.blocked_locations = {} |
|
"""a dictionary with the name of a location as key and <location,obstacle,symmetric> as value. |
|
A blocked passage between self and a location means that it |
|
will be reachable from [self] after overcoming the [obstacle]. |
|
The symmetric variable is a boolean that indicates if, when unblocked, |
|
[self] will also be reachable from [location]. |
|
""" |
|
|
|
def block_passage(self, location: 'Location', obstacle, symmetric: bool = True): |
|
"""Block a passage between self and location using an obstacle.""" |
|
if location in self.connecting_locations: |
|
if location.name not in self.blocked_locations: |
|
self.blocked_locations[location.name] = (location, obstacle, symmetric) |
|
self.connecting_locations = [x for x in self.connecting_locations if x is not location] |
|
else: |
|
raise Exception(f"Error: A blocked passage to {location.name} already exists") |
|
else: |
|
raise Exception(f"Error: Two non-conected locations cannot be blocked") |
|
|
|
def unblock_passage(self, location: 'Location'): |
|
"""Unblock a passage between self and location by adding it to the connecting locations of self. |
|
|
|
In case that the block was symmetric, self will be added to the connecting locations of location. |
|
""" |
|
if self.blocked_locations[location.name]: |
|
self.connecting_locations += [location] |
|
if self.blocked_locations[location.name][2] and self not in location.connecting_locations: |
|
location.connecting_locations += [self] |
|
del self.blocked_locations[location.name] |
|
else: |
|
raise Exception("Error: That is not a blocked passage") |
|
|
|
class Character (Component): |
|
"""A class to represent a character.""" |
|
def __init__ (self, name:str, descriptions: 'list[str]', location:Location, inventory: 'list[Item]' = None): |
|
|
|
super().__init__(name, descriptions) |
|
"""inherited from Component""" |
|
|
|
self.inventory = inventory or [] |
|
"""a set of Items the carachter has""" |
|
|
|
self.location = location |
|
"""the location of the character""" |
|
|
|
self.visited_locations = {self.location.name: []} |
|
"""a dictionary that contains the successive descriptions of the visited places""" |
|
|
|
def move(self, new_location: Location): |
|
"""Move the character to a new location.""" |
|
if new_location in self.location.connecting_locations: |
|
self.location = new_location |
|
if self.location.name not in self.visited_locations: |
|
self.visited_locations[self.location.name] = [] |
|
else: |
|
raise Exception(f"Error: {new_location.name} is not reachable") |
|
|
|
def save_item(self,item: Item, item_location_or_owner): |
|
"""Add an item to the character inventory.""" |
|
if item.gettable: |
|
if item not in self.inventory: |
|
self.inventory += [item] |
|
if item_location_or_owner.__class__.__name__ == 'Character': |
|
item_location_or_owner.inventory = [i for i in item_location_or_owner.inventory if i is not item] |
|
elif item_location_or_owner.__class__.__name__ == 'Location': |
|
item_location_or_owner.items = [i for i in item_location_or_owner.items if i is not item] |
|
else: |
|
raise Exception(f"Error: {item.name} is already in your inventory") |
|
else: |
|
raise Exception(f"Error: {item.name} cannot be taken") |
|
|
|
def drop_item (self, item: Item): |
|
"""Leave an item in the current location.""" |
|
self.inventory = [i for i in self.inventory if i is not item] |
|
self.location.items += [item] |
|
|
|
def give_item (self, character: 'Character', item: Item): |
|
"""Give an item to another character.""" |
|
try: |
|
character.save_item(item, self) |
|
except Exception as e: |
|
print(e) |
|
|
|
|
|
class World: |
|
"""A class to represent the fictional world, with references to every component.""" |
|
def __init__ (self, player: Character) -> None: |
|
|
|
self.items = {} |
|
"""a dictionary of all the Items in the world, with their names as values""" |
|
|
|
self.characters = {} |
|
"""a dictionary of all the Characters in the world, with their names as values""" |
|
|
|
self.locations = {} |
|
"""a dictionary of all the Locations in the world, with their names as values""" |
|
|
|
self.player = player |
|
"""a character for the player""" |
|
|
|
self.objective = None |
|
"""the current objective for the player in this world""" |
|
|
|
def set_objective (self, first_component: Type[Component], second_component: Type[Component]): |
|
|
|
first_component_class = first_component.__class__.__name__ |
|
second_component_class = second_component.__class__.__name__ |
|
|
|
if first_component_class == "Character" and second_component_class == "Character": |
|
self.objective = (first_component, second_component) |
|
elif (first_component_class == "Character" and second_component_class in ["Location", "Item"]) or (second_component_class == "Character" and first_component_class in ["Location", "Item"]): |
|
self.objective = (first_component, second_component) |
|
elif (first_component_class == "Item" and second_component_class == "Location") or (second_component_class == "Item" and first_component_class == "Location"): |
|
self.objective = (first_component, second_component) |
|
else: |
|
raise Exception(f"Error: Cannot set objective with classes {first_component_class} and {second_component_class}") |
|
|
|
def check_objective(self) -> bool: |
|
|
|
done = False |
|
first_component_class = self.objective[0].__class__.__name__ |
|
second_component_class = self.objective[1].__class__.__name__ |
|
|
|
if first_component_class == "Character" and second_component_class == "Character": |
|
if self.objective[0].location == self.objective[1].location: done = True |
|
elif first_component_class == "Character" and second_component_class == "Location": |
|
if self.objective[0].location == self.objective[1]: done = True |
|
elif first_component_class == "Character" and second_component_class == "Item": |
|
if self.objective[1] in self.objective[0].inventory: done = True |
|
elif first_component_class == "Item" and second_component_class == "Location": |
|
if self.objective[0] in self.objective[1].items: done = True |
|
|
|
return done |
|
|
|
def add_location (self,location: Location) -> None: |
|
"""Add a location to the world.""" |
|
if location.name in self.locations: |
|
raise Exception(f"Error: Already exists a location called '{location.name}'") |
|
else: |
|
self.locations[location.name] = location |
|
|
|
def add_item (self, item: Item) -> None: |
|
"""Add an item to the world.""" |
|
if item.name in self.items: |
|
raise Exception(f"Error: Already exists an item called '{item.name}'") |
|
else: |
|
self.items[item.name] = item |
|
|
|
def add_character (self, character: Character) -> None: |
|
"""Add a character to the world.""" |
|
if character.name in self.characters: |
|
raise Exception(f"Error: Already exists a character called '{character.name}'") |
|
else: |
|
self.characters[character.name] = character |
|
|
|
def add_locations (self,locations: 'list[Location]') -> None: |
|
""""Add a set of locations to the world.""" |
|
for location in locations: |
|
self.add_location(location) |
|
|
|
def add_items (self, items: 'list[Item]') -> None: |
|
"""Add a set of items to the world.""" |
|
for item in items: |
|
self.add_item(item) |
|
|
|
def add_characters (self, characters: 'list[Character]') -> None: |
|
"""Add a set of characters to the world.""" |
|
for character in characters: |
|
self.add_character(character) |
|
|
|
def render_world(self, *, language:str = 'en', detail_components:bool = True) -> str: |
|
"""Return the fictional world as a natural language description, using simple sentences. |
|
|
|
The components described are only those the player can see in the current location. |
|
If detail_components is False, then the descriptions for each component are not included. |
|
""" |
|
rendered_world = '' |
|
|
|
if language == 'es': |
|
rendered_world = self.__render_world_spanish(detail_components = detail_components) |
|
else: |
|
rendered_world = self.__render_world_english(detail_components = detail_components) |
|
|
|
return rendered_world |
|
|
|
def __render_world_spanish(self, *, detail_components:bool = True) -> str: |
|
"""Return the fictional world as a natural language description, using simple sentences in Spanish. |
|
|
|
The components described are only those the player can see in the current location. |
|
If detail_components is False, then the descriptions for each component are not included. |
|
""" |
|
player_location = self.player.location |
|
reachable_locations = [f"<{p.name}>" for p in player_location.connecting_locations] |
|
blocked_passages = [f"<{p}> bloqueado por <{player_location.blocked_locations[p][1].name}>" for p in player_location.blocked_locations.keys()] |
|
characters_in_the_scene = [character for character in self.characters.values() if character.location is player_location] |
|
|
|
|
|
world_description = f'El jugador está en <{player_location.name}>\n' |
|
|
|
if reachable_locations: |
|
world_description += f'Desde <{player_location.name}> el jugador puede ir a: {(", ").join(reachable_locations)}\n' |
|
else: |
|
world_description += f'Desde <{player_location.name}> el jugador puede ir a: None\n' |
|
|
|
if blocked_passages: |
|
world_description += f'Desde <{player_location.name}> hay pasajes bloqueados hacia: {(", ").join(blocked_passages)}\n' |
|
else: |
|
world_description += f'Desde <{player_location.name}> hay pasajes bloqueados hacia: None\n' |
|
|
|
if self.player.inventory: |
|
world_description += f'El jugador tiene los siguientes objetos en su inventario: {(", ").join([f"<{i.name}>" for i in self.player.inventory])}\n' |
|
else: |
|
world_description += f'El jugador tiene los siguientes objetos en su inventario: None\n' |
|
|
|
if player_location.items: |
|
world_description += f'El jugador puede ver los siguientes objetos: {(", ").join([f"<{i.name}>" for i in player_location.items])}\n' |
|
else: |
|
world_description += f'El jugador puede ver los siguientes objetos: None\n' |
|
|
|
if characters_in_the_scene: |
|
world_description += f'El jugador puede ver a los siguientes personajes: {(", ").join([f"<{c.name}>" for c in characters_in_the_scene])}' |
|
else: |
|
world_description += f'El jugador puede ver a los siguientes personajes: None' |
|
|
|
details = "" |
|
if detail_components: |
|
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)] |
|
puzzles_in_the_scene = [blocked_values[1] for blocked_values in player_location.blocked_locations.values() if isinstance(blocked_values[1], Puzzle)] |
|
|
|
details += "\nAquí hay una descripción de cada componente.\n" |
|
details += f"<{player_location.name}>: Este es el lugar en el que está el jugador. {('. ').join(player_location.descriptions)}.\n" |
|
details += "Personajes:\n" |
|
details += f"- <Jugador>: El jugador está actuando como <{self.player.name}>. {('. ').join(self.player.descriptions)}.\n" |
|
if len(characters_in_the_scene)>0: |
|
for character in characters_in_the_scene: |
|
details += f"- <{character.name}>: {('. ').join(character.descriptions)}." |
|
if len(character.inventory)>0: |
|
details += f"Este personaje tiene los siguientes objetos en su inventario: {(', ').join([f'<{i.name}>' for i in character.inventory])}\n" |
|
items_in_the_scene+= character.inventory |
|
else: |
|
details += "\n" |
|
if len(items_in_the_scene)>0: |
|
details+="Objetos:\n" |
|
for item in items_in_the_scene: |
|
details += f"- <{item.name}>: {('. ').join(item.descriptions)}\n" |
|
if len(puzzles_in_the_scene)>0: |
|
details+="Puzzles:\n" |
|
for puzzle in puzzles_in_the_scene: |
|
details+= f'- <{puzzle.name}>: {(". ").join(puzzle.descriptions)}. El acertijo a resolver es: "{puzzle.problem}". La respuesta esperada, que NO PUEDES decirle al jugador (JAMÁS) es: "{puzzle.answer}".\n' |
|
|
|
return world_description + '\n' + details |
|
|
|
def __render_world_english(self, *, detail_components:bool = True) -> str: |
|
"""Return the fictional world as a natural language description, using simple sentences in English. |
|
|
|
The components described are only those the player can see in the current location. |
|
If detail_components is False, then the descriptions for each component are not included. |
|
""" |
|
player_location = self.player.location |
|
reachable_locations = [f"<{p.name}>" for p in player_location.connecting_locations] |
|
blocked_passages = [f"<{p}> blocked by <{player_location.blocked_locations[p][1].name}>" for p in player_location.blocked_locations.keys()] |
|
characters_in_the_scene = [character for character in self.characters.values() if character.location is player_location] |
|
|
|
|
|
world_description = f'The player is in <{player_location.name}>\n' |
|
|
|
if reachable_locations: |
|
world_description += f'From <{player_location.name}> the player can access: {(", ").join(reachable_locations)}\n' |
|
else: |
|
world_description += f'From <{player_location.name}> the player can access: None\n' |
|
|
|
if blocked_passages: |
|
world_description += f'From <{player_location.name}> there are blocked passages to: {(", ").join(blocked_passages)}\n' |
|
else: |
|
world_description += f'From <{player_location.name}> there are blocked passages to: None\n' |
|
|
|
if self.player.inventory: |
|
world_description += f'The player has the following objects in the inventory: {(", ").join([f"<{i.name}>" for i in self.player.inventory])}\n' |
|
else: |
|
world_description += f'The player has the following objects in the inventory: None\n' |
|
|
|
if player_location.items: |
|
world_description += f'The player can see the following objects: {(", ").join([f"<{i.name}>" for i in player_location.items])}\n' |
|
else: |
|
world_description += f'The player can see the following objects: None\n' |
|
|
|
if characters_in_the_scene: |
|
world_description += f'The player can see the following characters: {(", ").join([f"<{c.name}>" for c in characters_in_the_scene])}' |
|
else: |
|
world_description += f'The player can see the following characters: None' |
|
|
|
details = "" |
|
if detail_components: |
|
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)] |
|
puzzles_in_the_scene = [blocked_values[1] for blocked_values in player_location.blocked_locations.values() if isinstance(blocked_values[1], Puzzle)] |
|
|
|
details += "\nHere is a description of each component.\n" |
|
details += f"<{player_location.name}>: This is the player's location. {('. ').join(player_location.descriptions)}.\n" |
|
details += "Characters:\n" |
|
details += f"- <Player>: The player is acting as <{self.player.name}>. {('. ').join(self.player.descriptions)}.\n" |
|
if len(characters_in_the_scene)>0: |
|
for character in characters_in_the_scene: |
|
details += f"- <{character.name}>: {('. ').join(character.descriptions)}." |
|
if len(character.inventory)>0: |
|
details += f" This character has the following items: {(', ').join([f'<{i.name}>' for i in character.inventory])}\n" |
|
items_in_the_scene+= character.inventory |
|
else: |
|
details += "\n" |
|
if len(items_in_the_scene)>0: |
|
details+="Objects:\n" |
|
for item in items_in_the_scene: |
|
details += f"- <{item.name}>: {('. ').join(item.descriptions)}\n" |
|
if len(puzzles_in_the_scene)>0: |
|
details+="Puzzles:\n" |
|
for puzzle in puzzles_in_the_scene: |
|
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' |
|
|
|
return world_description + '\n' + details |
|
|
|
def update (self, updates: str) -> None: |
|
"""Does the changes in the world according to the output of the language model. |
|
|
|
The possible changes considered are: |
|
- an object was moved |
|
- a location is now reachable |
|
- the position of the player changed. |
|
""" |
|
self.parse_moved_objects(updates) |
|
self.parse_blocked_passages(updates) |
|
self.parse_location_change(updates) |
|
|
|
def parse_moved_objects (self, updates: str) -> None: |
|
"""Parse the output of the language model to update the position of objects. |
|
|
|
There are three cases: |
|
- the player has a new item |
|
- the player gave an item to other character |
|
- the player dropped an item. |
|
""" |
|
parsed_objects = re.findall(r".*Moved object:\s*(.+)",updates) |
|
if 'None' not in parsed_objects: |
|
parsed_objects_split = re.findall(r"<[^<>]*?>.*?<[^<>]*?>",parsed_objects[0]) |
|
for parsed_object in parsed_objects_split: |
|
pair = re.findall(r"<([^<>]*?)>.*?<([^<>]*?)>",parsed_object) |
|
try: |
|
world_item = self.items[pair[0][0]] |
|
|
|
if pair[0][1] in ['Inventory', 'Inventario', 'Player', 'Jugador', self.player.name]: |
|
item_location = [character for character in list(self.characters.values()) if world_item in character.inventory] |
|
item_location += [location for location in list(self.locations.values()) if world_item in location.items] |
|
self.player.save_item(world_item, item_location[0]) |
|
|
|
elif pair[0][1] in self.characters: |
|
self.player.give_item(self.characters[pair[0][1]], world_item) |
|
|
|
else: |
|
self.player.drop_item(world_item) |
|
except Exception as e: |
|
print(e) |
|
|
|
def parse_blocked_passages (self, updates: str) -> None: |
|
"""Parse the output of the language model to update the reachable locations.""" |
|
parsed_blocked_passages = re.findall(r".*Blocked passages now available:\s*(.+)",updates) |
|
if 'None' not in parsed_blocked_passages: |
|
parsed_blocked_passages_split = re.findall(r"<([^<>]*?)>",parsed_blocked_passages[0]) |
|
for parsed_passage in parsed_blocked_passages_split: |
|
try: |
|
self.locations[self.player.location.name].unblock_passage(self.locations[parsed_passage]) |
|
except Exception as e: |
|
print (e) |
|
|
|
def parse_location_change (self, updates: str) -> None: |
|
"""Parse the output of the language model to update the position of the player.""" |
|
parsed_location_change = re.findall(r".*Your location changed: (.+)",updates) |
|
if "None" not in parsed_location_change: |
|
parsed_location_change_split = re.findall(r"<([^<>]*?)>",parsed_location_change[0]) |
|
try: |
|
self.player.move(self.locations[parsed_location_change_split[0]]) |
|
except Exception as e: |
|
print(e) |
|
|
|
|