Skip to content

Commit

Permalink
Merge pull request #48 from mmacy/docstring-work-still
Browse files Browse the repository at this point in the history
first-pass XP from treasure award
  • Loading branch information
mmacy committed Feb 4, 2024
2 parents 3e5a85f + a54186d commit a180562
Show file tree
Hide file tree
Showing 24 changed files with 858 additions and 967 deletions.
1 change: 1 addition & 0 deletions docs/reference/dungeon_assistant.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
::: osrlib.dungeon_assistant
1 change: 0 additions & 1 deletion docs/reference/dungeon_master.md

This file was deleted.

1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ theme:
- header.autohide

markdown_extensions:
- admonition
- attr_list
- md_in_html
- pymdownx.emoji:
Expand Down
8 changes: 4 additions & 4 deletions osrgame/osrgame/osrgame.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from osrlib.adventure import Adventure
from osrlib.constants import ADVENTURE_NAMES, DUNGEON_NAMES
from osrlib.dungeon import Dungeon
from osrlib.dungeon_master import DungeonMaster
from osrlib.dungeon_assistant import DungeonAssistant
from osrlib.game_manager import logger
from osrlib.party import get_default_party
from osrlib.enums import OpenAIModelVersion
Expand All @@ -20,7 +20,7 @@ class OSRConsole(App):
player_party = None
adventure = None
dungeon_master = None
openai_model = OpenAIModelVersion.GPT35TURBO
openai_model = OpenAIModelVersion.GPT4TURBO # Use any value other than NONE to enable AI

CSS_PATH = "screen.tcss"

Expand Down Expand Up @@ -78,7 +78,7 @@ def set_active_adventure(self, adventure: Adventure = None) -> None:
dungeon = Dungeon.get_random_dungeon(random.choice(DUNGEON_NAMES),
"The first level of the home of the ancient wizard lich Glofarnux, its "
"entrance hidden in a forgotten glade deep in the cursed Mystic Forest.",
num_locations=50, use_ai=True, openai_model=self.openai_model)
num_locations=50, openai_model=self.openai_model)
dungeon.set_start_location(1)

if dungeon.validate_location_connections():
Expand All @@ -95,7 +95,7 @@ def start_session(self) -> str:
if self.adventure is None:
self.set_active_adventure(adventure=None)

self.dungeon_master = DungeonMaster(self.adventure, openai_model=self.openai_model)
self.dungeon_master = DungeonAssistant(self.adventure, openai_model=self.openai_model)
dm_start_session_response = self.dungeon_master.start_session()
logger.debug(f"DM start session response: {dm_start_session_response}")

Expand Down
12 changes: 6 additions & 6 deletions osrgame/osrgame/screen_explore.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def perform_move_action(self, direction: Direction, log_message: str) -> None:
dm_response = self.dungeon_master.move_party(direction)

self.query_one("#dm_log").write_line(
""#"> " + str(self.dungeon_master.adventure.active_dungeon.current_location)
""#"> " + str(self.dungeon_master.adventure.active_dungeon.current_party_location)
)
self.query_one("#dm_log").write_line(wrap_text(dm_response))

Expand All @@ -54,11 +54,11 @@ def perform_move_action(self, direction: Direction, log_message: str) -> None:
def check_for_encounter(self) -> None:
"""Check for an encounter and execute battle if there are monsters in the encounter."""
if (
self.dungeon_master.adventure.active_dungeon.current_location.encounter
and not self.dungeon_master.adventure.active_dungeon.current_location.encounter.is_ended
self.dungeon_master.adventure.active_dungeon.current_party_location.encounter
and not self.dungeon_master.adventure.active_dungeon.current_party_location.encounter.is_ended
):
encounter = (
self.dungeon_master.adventure.active_dungeon.current_location.encounter
self.dungeon_master.adventure.active_dungeon.current_party_location.encounter
)

if (
Expand Down Expand Up @@ -117,11 +117,11 @@ def action_summarize(self) -> None:
self.query_one("#player_log").write_line("> Describe location")
formatted_message = self.dungeon_master.format_user_message(
"Please describe this location again, including specifying the exit that we entered from and which exit or exits, if any, we haven't yet explored: " \
+ str(self.dungeon_master.adventure.active_dungeon.current_location.json)
+ str(self.dungeon_master.adventure.active_dungeon.current_party_location)
)
dm_response = self.dungeon_master.player_message(formatted_message)
self.query_one("#dm_log").write_line(
""#"> " + str(self.dungeon_master.adventure.active_dungeon.current_location)
""#"> " + str(self.dungeon_master.adventure.active_dungeon.current_party_location)
)
self.query_one("#dm_log").write_line(wrap_text(dm_response))
self.query_one("#dm_log").write_line("---")
Expand Down
687 changes: 339 additions & 348 deletions osrgame/poetry.lock

Large diffs are not rendered by default.

17 changes: 10 additions & 7 deletions osrlib/osrlib/ability.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,13 @@
"""Defines player character abilities."""
"""Defines `PlayerCharacter` abilities and their modifiers.
Abilities are inherent traits that every [PlayerCharacter][osrlib.player_character.PlayerCharacter] possesses in
varying degrees. They provide different modifiers (bonuses or penalties) that can affect gameplay mechanics like dice
rolls during game play or core aspects of the character like whether they're especially hard to hit (lower AC) because
of a high [Dexterity][osrlib.ability.Dexterity] score or whether they know additional languages because of a high
[Intelligence][osrlib.ability.Intelligence] score.
"""

from abc import ABC, abstractmethod

Expand All @@ -8,12 +17,6 @@
class Ability(ABC):
"""Abstract base class for character abilities.
Abilities are inherent traits that every character possesses in varying degrees.
They provide different modifiers (bonuses or penalties) that can affect gameplay
mechanics like dice rolls during game play or core aspects of the character like
whether they're especially hard to hit (lower AC) because of a high dexterity
score or whether they know additional languages because of a high intelligence score.
Attributes:
score (int): The raw ability score.
modifiers (dict): A mapping of the ability's modifier types to modifier values based on the
Expand Down
6 changes: 2 additions & 4 deletions osrlib/osrlib/dice_roller.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,11 +29,10 @@ class DiceRoll(
"""

def __str__(self):
"""
Return a string representation of the dice roll in ndn notation, including modifiers if applicable.
"""Returns a string representation of the dice roll in `ndn` notation, including modifiers if applicable.
Returns:
str: A string in ndn notation (e.g., '3d6', '1d20+5', '2d8-4').
str: A string in ndn notation like '3d6', '1d20+5', or '2d8-4'.
"""
base = f"{self.num_dice}d{self.num_sides}"
if self.modifier > 0:
Expand All @@ -59,7 +58,6 @@ def roll_dice(notation: str, modifier: int = 0, drop_lowest: bool = False) -> Di
"""Roll dice based on the nDn or Dn notation and factor in optional modifiers.
Examples:
```python
roll_dice('3d6') # DiceRoll object representing a roll of three six-sided dice.
roll_dice('1d20+5') # DiceRoll object for rolling one twenty-sided die with a +5 modifier.
Expand Down
102 changes: 40 additions & 62 deletions osrlib/osrlib/dungeon.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@
"""
from typing import List
from enum import Enum
import random, json, asyncio, uuid
import random, json, uuid
from openai import OpenAI
from osrlib.enums import Direction, OpenAIModelVersion
from osrlib.game_manager import logger
Expand Down Expand Up @@ -186,9 +185,7 @@ def __str__(self) -> str:
@property
def json(self):
"""Returns a JSON representation of the location."""
json_location = json.dumps(
self.to_dict(), default=lambda o: o.__dict__, separators=(",", ":")
)
json_location = json.dumps(self.to_dict(), default=lambda o: o.__dict__, separators=(",", ":"))
logger.debug(json_location)
return json_location

Expand All @@ -213,9 +210,7 @@ def add_exit(self, exit: Exit):
ValueError: If an exit already exists in the same direction.
"""
if self.get_exit(exit.direction):
raise ExitAlreadyExistsError(
f"An exit already exists in the {exit.direction.name} direction."
)
raise ExitAlreadyExistsError(f"An exit already exists in the {exit.direction.name} direction.")
self.exits.append(exit)

def to_dict(self):
Expand Down Expand Up @@ -310,47 +305,48 @@ def __init__(
name (str, optional): The name of the dungeon. Should be unique within an `Adventure`.
description (str, optional): A description of the dungeon appropriate for display to the player.
locations (List[Location], optional): The collection of locations within the dungeon.
start_location_id (int, optional): The `Location` in the dungeon in which the `Party` starts their exploration of the dungeon.
id (str, optional): The ID of the dungeon; must be unique within the `Advenure`. Defaults to `str(uuid.uuid4())`.
start_location_id (int, optional): The ID of the `Location` the `Party` should begin exploration of the
dungeon. If you supply this argument, the `current_party_location` is set
to this location automatically.
id (str, optional): The ID of the dungeon; must be unique within the `Adventure`.
"""
self.name = name
self.description = description
self.locations = locations
self.start_location_id = start_location_id
self.current_location = None
self.set_start_location(start_location_id) # Also sets self.current_location
self.current_party_location = self.set_start_location(start_location_id)
self.party_is_exploring = False
self.id = id

def set_start_location(self, location_id: int) -> Location:
"""Sets the starting location for the dungeon and also sets the current location to the starting [Location][osrlib.dungeon.Location].
"""Sets the `Location` where `Party` starts exploring the dungeon if a location with the specified ID exists within the dungeon.
If it exists, this method also sets the `current_party_location` to that location.
Args:
location_id (int): The ID of the location to set as the starting location.
location_id (int): The ID of the `Location` where the party should begin their exploration of the dungeon.
Returns:
Location: The starting location if it exists, otherwise `None`.
"""
logger.debug(f"Setting starting location to location with ID {location_id}.")
start_location = self.get_location_by_id(location_id)
if start_location:
self.current_location = start_location
logger.debug(f"Starting location set to {start_location}.")
return start_location
self.start_location = self.get_location_by_id(location_id)
if self.start_location:
logger.debug(f"Starting location set to {self.start_location}.")
self.current_party_location = self.start_location
return self.start_location
else:
return None

def add_location(self, location: Location) -> None:
"""Adds a location to the dungeon.
"""Adds the location to the dungeon.
Args:
location (Location): The location to add to the dungeon.
"""
if location.id not in [loc.id for loc in self.locations]:
self.locations.append(location)
else:
exception = LocationAlreadyExistsError(
f"Location with ID {location.id} already exists in the dungeon."
)
exception = LocationAlreadyExistsError(f"Location with ID {location.id} already exists in the dungeon.")
logger.exception(exception)
raise exception

Expand All @@ -365,9 +361,7 @@ def get_location_by_id(self, location_id: int) -> Location:
"""
return next((loc for loc in self.locations if loc.id == location_id), None)

def get_location_by_direction(
self, location: Location, direction: Direction
) -> Location:
def get_location_by_direction(self, location: Location, direction: Direction) -> Location:
"""Get the location in the specified direction from the given location.
Args:
Expand All @@ -384,7 +378,7 @@ def get_location_by_direction(
return None

def move(self, direction: Direction) -> Location:
"""Moves the party to the location in the specified direction if there's an exit in that direction, and sets the dungeon's current location to the new location.
"""Moves the party to the location in the specified direction if there's an exit in that direction and sets the dungeon's current location to the new location.
You should set the new location's `is_visited` attribute to `True` after you've completed any processing you like
when the party arrives at a location for the first time.
Expand All @@ -410,7 +404,7 @@ def move(self, direction: Direction) -> Location:
... print(f"The party moved to new location {new_location}, and here's a super detailed description...")
... new_location.is_visited = True
Party moved to new location LOC ID: 2 Size: 10'W x 10'L Exits: [NORTH:1] Keywords: ['rust', 'armory'], and here's a super detailed description...
>>> dungeon.current_location == new_location
>>> dungeon.current_party_location == new_location
True
```
Expand All @@ -420,30 +414,28 @@ def move(self, direction: Direction) -> Location:
Returns:
Location: The location the party moved to if they were able to move in the specified direction, otherwise `None`.
"""
new_location = self.get_location_by_direction(self.current_location, direction)
new_location = self.get_location_by_direction(self.current_party_location, direction)
if not new_location:
logger.debug(
f"No location {direction.name} of {self.current_location}. The only exits are: "
+ ", ".join(str(exit) for exit in self.current_location.exits)
f"No location {direction.name} of {self.current_party_location}. The only exits are: "
+ ", ".join(str(exit) for exit in self.current_party_location.exits)
+ "."
)
return None

self.current_location = new_location
self.current_party_location = new_location

# NOTE: We do NOT set is_visited to True here because we need to give the caller the opportunity to check and
# perform any actions that need to be done when the party enters a location for the first time. They can then
# set is_visited to True when they're done performing those actions.
if self.current_location.is_visited:
if self.current_party_location.is_visited:
logger.debug(
f"Party moved to previously visited (is_visited = True) location {self.current_location}."
f"Party moved to previously visited (is_visited = True) location {self.current_party_location}."
)
else:
logger.debug(
f"Party moved to new (is_visited = False) location {self.current_location}."
)
logger.debug(f"Party moved to new (is_visited = False) location {self.current_party_location}.")

return self.current_location
return self.current_party_location

def validate_location_connections(self) -> bool:
"""Verifies whether every [Location][osrlib.dungeon.Location] in the dungeon is connected to at least one other location and that a connection in the opposite direction exists. For example, if location A has an exit EAST to location B, then location B must have an exit WEST to location A.
Expand Down Expand Up @@ -530,9 +522,7 @@ def get_dungeon_location_keywords(
"content": f"{dungeon.name}\n{dungeon.description}\n{len(dungeon.locations)}",
},
]
logger.debug(
f"Getting keywords for dungeon '{dungeon.name}' from OpenAI API..."
)
logger.debug(f"Getting keywords for dungeon '{dungeon.name}' from OpenAI API...")

client = OpenAI()
openai_model = openai_model.value
Expand All @@ -544,20 +534,15 @@ def get_dungeon_location_keywords(
)
llm_response = completion.choices[0].message.content

decoded_json_string = (
bytes(llm_response, "utf-8").decode("unicode_escape").strip('"')
)
logger.debug(
f"Keywords for dungeon {dungeon.name} from OpenAI API: {decoded_json_string}"
)
decoded_json_string = bytes(llm_response, "utf-8").decode("unicode_escape").strip('"')
logger.debug(f"Keywords for dungeon {dungeon.name} from OpenAI API: {decoded_json_string}")
return decoded_json_string

@staticmethod
def get_random_dungeon(
name: str = "Random Dungeon",
description: str = "",
num_locations: int = 10,
use_ai: bool = False,
level: int = 1,
openai_model: OpenAIModelVersion = OpenAIModelVersion.DEFAULT,
) -> "Dungeon":
Expand All @@ -567,7 +552,6 @@ def get_random_dungeon(
name (str): The name of the dungeon.
description (str): A brief description providing context or history for the dungeon.
num_locations (int): The number of locations to generate in the dungeon.
use_ai (bool): Indicates whether to use the OpenAI API to generate keywords for each [Location][osrlib.dungeon.Location] in the dungeon.
level (int): The level of the dungeon. Determines the hit die (and thus the difficulty) of monsters in encounters in the dungeon.
openai_model (OpenAIModelVersion): The OpenAI model to use when generating keywords for the locations in the dungeon.
Expand Down Expand Up @@ -616,16 +600,12 @@ def get_random_dungeon(
break

if description == "":
description = (
f"A randomly generated dungeon with {num_locations} locations."
)
description = f"A randomly generated dungeon with {num_locations} locations."

dungeon = Dungeon(name, description, locations, start_location_id=1)

if use_ai:
location_keywords_json = Dungeon.get_dungeon_location_keywords(
dungeon, openai_model
)
if openai_model is not OpenAIModelVersion.NONE:
location_keywords_json = Dungeon.get_dungeon_location_keywords(dungeon, openai_model)
location_keywords_dict = json.loads(location_keywords_json)
for location_id_str, keywords in location_keywords_dict.items():
location_id = int(location_id_str)
Expand All @@ -645,19 +625,17 @@ def to_dict(self):
"name": self.name,
"description": self.description,
"locations": [location.to_dict() for location in self.locations],
"start_location_id": self.current_location.id, # save the current location as the start location on load
"start_location_id": self.current_party_location.id, # save the current location as the start location on load
"id": self.id,
}

@classmethod
def from_dict(cls, data):
"""Returns a ``Dungeon`` instance from a dictionary representation of the dungeon. Useful as a post-deserialization step when loading from a permanent data store."""
"""Returns a `Dungeon` instance from a dictionary representation of the dungeon. Useful as a post-deserialization step when loading from a permanent data store."""
return cls(
data["name"],
data["description"],
[Location.from_dict(location_data) for location_data in data["locations"]],
data[
"start_location_id"
], # will be the location that was current when the dungeon was saved
data["start_location_id"], # will be the location that was current when the dungeon was saved
data["id"],
)
Loading

0 comments on commit a180562

Please sign in to comment.