Skip to content

Commit

Permalink
Merge pull request #29 from mmacy/tui-character-create
Browse files Browse the repository at this point in the history
TUI cleanup and supporting osrlib fixes
  • Loading branch information
mmacy committed Nov 27, 2023
2 parents 63cfc97 + f928a22 commit 1d261ab
Show file tree
Hide file tree
Showing 11 changed files with 140 additions and 118 deletions.
25 changes: 10 additions & 15 deletions osrgame/osrgame/osrgame.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@ class OSRConsole(App):
player_party = None
adventure = None
dungeon_master = None
openai_model = OpenAIModelVersion.GPT4TURBO

CSS_PATH = "screen.tcss"

BINDINGS = [
("c", "character", "Character"),
# ("e", "explore", "Explore"),
# ("ctrl+l", "load", "Load"),
("escape", "app.pop_screen", "Pop screen"),
("escape", "previous_screen", "Previous screen"),
("q", "quit", "Quit"),
]

Expand All @@ -41,17 +39,14 @@ def compose(self) -> ComposeResult:

def on_mount(self) -> None:
self.title = "OSR Console"
self.sub_title = "Adventures in turn-based text"
self.sub_title = f"Adventures in turn-based text (model: {self.openai_model.value})"

def action_character(self) -> None:
"""Show the character screen."""
self.push_screen("screen_character")
### Actions ###

def action_explore(self) -> None:
"""Show the explore screen."""
if self.adventure is None:
self.set_active_adventure()
self.push_screen("screen_explore")
def action_previous_screen(self) -> None:
"""Return to the previous screen."""
if len(self.screen_stack) > 1:
self.pop_screen()

def action_quit(self) -> None:
"""Quit the application."""
Expand Down Expand Up @@ -80,7 +75,7 @@ def set_active_adventure(self, adventure: Adventure = None) -> None:
dungeon = Dungeon.get_random_dungeon("Dungeon of the Raving Mage",
"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=OpenAIModelVersion.GPT35TURBO)
num_locations=50, use_ai=True, openai_model=self.openai_model)
dungeon.set_start_location(1)

if dungeon.validate_location_connections():
Expand All @@ -97,7 +92,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=OpenAIModelVersion.GPT35TURBO)
self.dungeon_master = DungeonMaster(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
7 changes: 3 additions & 4 deletions osrgame/osrgame/screen_adventure_browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from textual.app import ComposeResult
from textual.containers import Container, VerticalScroll
from textual.screen import Screen
from textual.widgets import DirectoryTree, Footer, Header, Static
from textual.widgets import Footer, Header, Static

from osrlib.adventure import Adventure

Expand All @@ -16,8 +16,7 @@ class AdventureBrowserScreen(Screen):
"""File browser for selecting an adventure to load."""

BINDINGS = [
("q", "quit", "Quit"),
("l", "load_adventure", "Load adventure"),
("ctrl+l", "load_adventure", "Load selected adventure"),
]

adventure_file_path = None
Expand Down Expand Up @@ -70,9 +69,9 @@ def action_load_adventure(self) -> None:
try:
loaded_adventure = Adventure.load_adventure(self.adventure_file_path)
self.app.set_active_adventure(loaded_adventure)
self.app.start_session()
self.app.pop_screen()
self.app.push_screen("screen_explore")
self.app.start_session()
except Exception:
# Get the traceback and display it in the code view.
code_view = self.query_one("#code", Static)
Expand Down
111 changes: 52 additions & 59 deletions osrgame/osrgame/screen_explore.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
from textual.app import ComposeResult
from textual.screen import Screen
from textual.widgets import Header, Footer, Log, Placeholder
from textual.widgets import Header, Footer, Log

from osrlib.adventure import Adventure
from osrlib.dungeon import Direction
from osrlib.utils import wrap_text

Expand All @@ -11,25 +10,23 @@

class ExploreScreen(Screen):
BINDINGS = [
("b", "start_session", "Begin session"),
("n", "move_north", "North"),
("s", "move_south", "South"),
("e", "move_east", "East"),
("w", "move_west", "West"),
("k", "clear_logs", "Clear logs"),
("?", "summarize", "Summarize session"),
("?", "summarize", "Describe location"),
("c", "character", "Character screen"),
("h", "heal_party", "Heal party"),
("ctrl+s", "save_game", "Save game"),
("ctrl+h", "heal_party", "Heal party"),
]

dungeon_master = None
save_path = None

def compose(self) -> ComposeResult:
yield Header(show_clock=True)
yield PartyRosterTable(id="pc_party_table", classes="box") # upper-left
yield Log(id="dm_log", auto_scroll=True, classes="box") # right (row-span=2)
yield Log(id="player_log", auto_scroll=True, classes="box") # lower-left
yield Log(id="dm_log", auto_scroll=True, classes="box") # right (row-span=2)
yield Log(id="player_log", auto_scroll=True, classes="box") # lower-left
yield Footer()

def on_mount(self) -> None:
Expand All @@ -38,114 +35,110 @@ def on_mount(self) -> None:
self.query_one("#dm_log", Log).border_title = "Adventure Log"
self.query_one(PartyRosterTable).border_title = "Adventuring Party"
self.query_one(PartyRosterTable).update_table()
self.action_summarize()

def perform_move_action(self, direction: Direction, log_message: str) -> None:
"""Move the party in the specified direction, execute battle (if any), and log the results."""

self.query_one("#player_log").write_line("---")
self.query_one("#player_log").write_line(log_message)

dm_response = self.dungeon_master.move_party(direction)

self.query_one("#dm_log").write_line("---")
self.query_one("#dm_log").write_line("> " + str(self.dungeon_master.adventure.active_dungeon.current_location))
self.query_one("#dm_log").write_line(
"> " + str(self.dungeon_master.adventure.active_dungeon.current_location)
)
self.query_one("#dm_log").write_line(wrap_text(dm_response))
self.query_one("#dm_log").write_line(f"[model: {self.dungeon_master.openai_model}]")
self.query_one("#dm_log").write_line(" ")

self.check_for_encounter()

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:
encounter = self.dungeon_master.adventure.active_dungeon.current_location.encounter

self.query_one("#dm_log").write_line("> Encounter!")

# TODO: Check whether monsters were surprised, and if so, give the player a chance to flee.
self.query_one("#player_log").write_line("Fight!")
if (
self.dungeon_master.adventure.active_dungeon.current_location.encounter
and not self.dungeon_master.adventure.active_dungeon.current_location.encounter.is_ended
):
encounter = (
self.dungeon_master.adventure.active_dungeon.current_location.encounter
)

if (
encounter.monster_party is not None
and len(encounter.monster_party.members) > 0
):
self.query_one("#dm_log").write_line("> Encounter:")
self.query_one("#dm_log").write_line(f" {encounter.monster_party}".replace("\n", "\n "))

# TODO: Check whether monsters were surprised, and if so, give the player a chance to flee.
self.query_one("#player_log").write_line("> Fight!")

encounter.start_encounter(self.dungeon_master.adventure.active_party)
encounter_log = encounter.get_encounter_log()
dm_response = self.dungeon_master.summarize_battle(encounter_log)
self.query_one("#dm_log").write_line(wrap_text(dm_response))
self.query_one("#dm_log").write_line(f"[model: {self.dungeon_master.openai_model}]")
self.query_one("#dm_log").write_line(" ")

self.query_one("#pc_party_table").update_table()
self.query_one("#dm_log").write_line("---")

def action_quit(self) -> None:
"""Quit the application."""
self.app.exit()

def action_move_north(self) -> None:
"""Move the party north."""
self.perform_move_action(Direction.NORTH, "Moving north...")
self.perform_move_action(Direction.NORTH, "> Move north")

def action_move_south(self) -> None:
"""Move the party south."""
self.perform_move_action(Direction.SOUTH, "Moving south...")
self.perform_move_action(Direction.SOUTH, "> Move south")

def action_move_east(self) -> None:
"""Move the party east."""
self.perform_move_action(Direction.EAST, "Moving east...")
self.perform_move_action(Direction.EAST, "> Move east")

def action_move_west(self) -> None:
"""Move the party west."""
self.perform_move_action(Direction.WEST, "Moving west...")
self.perform_move_action(Direction.WEST, "> Move west")

def action_move_up(self) -> None:
"""Move the party up."""
self.perform_move_action(Direction.UP, "Climbing up the stairs...")
self.perform_move_action(Direction.UP, "> Ascend stairs")

def action_move_down(self) -> None:
"""Move the party down."""
self.perform_move_action(Direction.DOWN, "Descending the stairs...")
self.perform_move_action(Direction.DOWN, "> Descend stairs")

def action_clear_logs(self) -> None:
def clear_logs(self) -> None:
"""An action to clear the logs."""
self.query_one("#player_log").clear()
self.query_one("#dm_log").clear()

def action_summarize(self) -> None:
"""An action to summarize the session."""
self.query_one("#player_log").write_line("Summarizing session...")
self.query_one("#player_log").write_line("===")
formatted_message = self.dungeon_master.format_user_message("Please provide a journal entry for the adventurers that summarizes the locations they've seen and encounters they've had in this game session. Include only what the player characters in the adventuring party would know. Include stats at the very end with number of locations visited, monsters killed, PCs killed, and total experience points earned.")
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)
)
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)
)
self.query_one("#dm_log").write_line(wrap_text(dm_response))
self.query_one("#dm_log").write_line(f"[model: {self.dungeon_master.openai_model}]")
self.query_one("#dm_log").write_line("===")
self.query_one("#dm_log").write_line("---")

def action_save_game(self) -> None:
"""An action to save the game."""
self.query_one("#player_log").write_line("> Save adventure")
self.query_one("#player_log").write_line("---")
self.query_one("#dm_log").write_line("> Saving adventure...")
self.save_path = self.dungeon_master.adventure.save_adventure()
self.query_one("#dm_log").write_line(f"Adventure '{self.dungeon_master.adventure.name}' saved to {self.save_path}.")
self.query_one("#dm_log").write_line("===")
def action_character(self) -> None:
"""Show the character screen."""
self.app.push_screen("screen_character")

def action_heal_party(self) -> None:
"""An action to heal the party."""
self.query_one("#player_log").write_line("> Heal party")
self.query_one("#player_log").write_line("---")
self.query_one("#dm_log").write_line("> Healing party...")
self.dungeon_master.adventure.active_party.heal_party()
self.query_one("#dm_log").write_line("Party healed.")
self.query_one("#player_log").write_line(" Party healed.")
self.query_one("#pc_party_table").update_table()


def action_load_game(self) -> None:
def action_save_game(self) -> None:
"""An action to save the game."""
self.query_one("#player_log").write_line("> Load adventure")
self.query_one("#player_log").write_line("---")
self.query_one("#dm_log").write_line("> Loading adventure...")
if self.save_path is None:
self.query_one("#dm_log").write_line("No save path found.")
else:
self.app.adventure = Adventure.load_adventure(self.save_path)
loaded_adventure = Adventure.load_adventure(self.save_path)
self.dungeon_master.adventure = loaded_adventure
self.query_one("#dm_log").write_line(f"Adventure '{loaded_adventure.name}' loaded from {self.save_path}.")
self.query_one("#pc_party_table").update_table()
self.query_one("#player_log").write_line("> Save adventure")
save_path = self.dungeon_master.adventure.save_adventure()
self.query_one("#player_log").write_line(f" Saved to: {save_path}")
8 changes: 1 addition & 7 deletions osrgame/osrgame/screen_welcome.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,11 @@


class WelcomeScreen(Screen):
BINDINGS = [
("escape", "app.pop_screen", "Pop screen"),
("q", "quit", "Quit"),
]

def compose(self) -> ComposeResult:
yield Header(show_clock=True, id="header")
yield WelcomeScreenButtons(id="welcome-buttons")
yield Footer()

def on_mount(self) -> None:
pass

Expand Down Expand Up @@ -47,8 +42,7 @@ def quit_button_pressed(self) -> None:
def action_start_default_adventure(self) -> None:
"""Start the default adventure, which is an adventure with one randomly generated dungeon."""
self.app.set_active_adventure(adventure=None)
dm_response = self.app.start_session()
# TODO: set up a textual reactive property for the DM response and watch that in all the screens
dm_response = self.app.start_session() # TODO: dm_response should be a textual reactive property
self.app.push_screen("screen_explore")

def action_load_adventure(self) -> None:
Expand Down
5 changes: 3 additions & 2 deletions osrgame/osrgame/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def compose(self) -> ComposeResult:
def on_mount(self) -> None:
"""Perform actions when the widget is mounted."""
table = self.query_one(DataTable)
table.add_columns("Name", "Class", "Level", "HP", "AC")
table.add_columns("Name", "Class", "Level", "HP", "AC", "XP")

def update_table(self):
party = self.app.adventure.active_party
Expand All @@ -104,8 +104,9 @@ def update_table(self):
pc.name,
pc.character_class.class_type.value,
Text(str(pc.level), justify="center"),
Text(f"{str(pc.hit_points)}/{str(pc.character_class.max_hp)}", justify="center"),
Text(f"{str(pc.hit_points)}/{str(pc.max_hit_points)}", justify="center"),
Text(str(pc.armor_class), justify="center"),
Text(str(pc.xp) + "/" + str(pc.xp_needed_for_next_level), justify="center"),
]
table.add_row(*row_data, key=pc.name)

Expand Down
2 changes: 1 addition & 1 deletion osrgame/poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion osrlib/osrlib/dungeon.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def __init__(

def __str__(self):
exits_str = ", ".join(str(exit) for exit in self.exits)
return f"Loc ID: {self.id} Size: {str(self.dimensions['width'])}'W x {str(self.dimensions['length'])}'L Exits: [{exits_str}] Keywords: {self.keywords}"
return f"LOC ID: {self.id} Size: {str(self.dimensions['width'])}'W x {str(self.dimensions['length'])}'L Exits: [{exits_str}] Keywords: {self.keywords}"

@property
def json(self):
Expand Down
Loading

0 comments on commit 1d261ab

Please sign in to comment.