Skip to content

Commit

Permalink
Merge pull request #33 from mmacy/treasure-and-char-work
Browse files Browse the repository at this point in the history
Treasure and character work (part 1 of n)
  • Loading branch information
mmacy committed Jan 21, 2024
2 parents 0693db9 + 3ec7371 commit 82a20ab
Show file tree
Hide file tree
Showing 13 changed files with 465 additions and 211 deletions.
19 changes: 10 additions & 9 deletions osrgame/osrgame/screen.tcss
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ AdventureBrowserScreen {

CharacterScreen {
layout: grid;
grid-size: 4;
grid-columns: 1.5fr 1.25fr 1fr 1fr;
grid-rows: 25% 25% 50%;
grid-size: 2;
grid-columns: 1.5fr 1fr;
grid-rows: 25% 30% 45%;
background: $surface;
}

Expand Down Expand Up @@ -53,30 +53,31 @@ RadioSet {

#stat-block {
padding: 1;
column-span: 2;
column-span: 1;
}

#log {
column-span: 2;
column-span: 1;
}

#ability-block {
padding: 1;
column-span: 2;
column-span: 1;
}

#saving-throw-block {
padding: 1;
column-span: 2;
column-span: 1;
}

#item-block {
column-span: 3;
column-span: 1;
}

#char-buttons {
border: solid $surface-lighten-3;
layout: vertical;
layout: grid;
grid-size: 2;
align: center middle;
}

Expand Down
78 changes: 55 additions & 23 deletions osrgame/osrgame/screen_character.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class CharacterScreen(Screen):
("escape", "app.pop_screen", "Back"),
("n", "next_character", "Next character"),
("ctrl+n", "new_character", "New character"),
("ctrl+delete", "delete_character", "Delete character"),
]

def compose(self) -> ComposeResult:
Expand All @@ -35,36 +36,55 @@ def compose(self) -> ComposeResult:
def on_mount(self) -> None:
"""Perform actions when the widget is mounted."""
self.query_one(Log).border_subtitle = "LOG"
self.query_one(CharacterStatsBox).pc_name = self.app.adventure.active_party.active_character.name
self.query_one(CharacterStatsBox).pc_class = self.app.adventure.active_party.active_character.character_class
self.query_one(CharacterStatsBox).pc_level = self.app.adventure.active_party.active_character.character_class.current_level
self.query_one(CharacterStatsBox).pc_hp = self.app.adventure.active_party.active_character.character_class.hp
self.query_one(CharacterStatsBox).pc_ac = self.app.adventure.active_party.active_character.armor_class
self.query_one(
CharacterStatsBox
).pc_name = self.app.adventure.active_party.active_character.name
self.query_one(
CharacterStatsBox
).pc_class = self.app.adventure.active_party.active_character.character_class
self.query_one(
CharacterStatsBox
).pc_level = (
self.app.adventure.active_party.active_character.character_class.current_level
)
self.query_one(
CharacterStatsBox
).pc_hp = self.app.adventure.active_party.active_character.character_class.hp
self.query_one(
CharacterStatsBox
).pc_ac = self.app.adventure.active_party.active_character.armor_class
self.query_one(AbilityTable).update_table()
self.query_one(SavingThrowTable).update_table()
self.query_one(ItemTable).items = self.app.adventure.active_party.active_character.inventory.all_items
self.query_one(
ItemTable
).items = self.app.adventure.active_party.active_character.inventory.all_items

@on(Button.Pressed, "#btn_new_character")
def default_button_pressed(self) -> None:
def btn_new_character(self) -> None:
self.query_one(Log).write_line(f"Creating a new character...")
self.action_new_character()

def on_button_pressed(self, event: Button.Pressed) -> None:
@on(Button.Pressed, "#btn_delete_character")
def btn_delete_character(self) -> None:
self.action_delete_character()

@on(Button.Pressed, "#btn_roll_abilities")
def btn_roll_abilities(self) -> None:
pc = self.app.adventure.active_party.active_character
if event.button.id == "btn_roll_abilities":
self.reroll()
self.query_one(CharacterStatsBox).pc_ac = pc.armor_class

elif event.button.id == "btn_roll_hp":
hp_roll = pc.roll_hp()
pc.character_class.max_hp = max(hp_roll.total_with_modifier, 1)
pc.character_class.hp = pc.character_class.max_hp
roll_string = hp_roll.pretty_print()
self.query_one(Log).write_line(roll_string)
self.query_one(CharacterStatsBox).pc_hp = pc.character_class.max_hp

elif event.button.id == "btn_save_character":
pc.save_character()
self.query_one(Log).write_line("Character saved.")
self.reroll()
self.query_one(CharacterStatsBox).pc_ac = pc.armor_class

@on(Button.Pressed, "#btn_roll_hp")
def btn_roll_hp(self) -> None:
roll = self.app.adventure.active_party.active_character.roll_hp()
self.query_one(Log).write_line(f"HP roll: {roll.total_with_modifier} on {roll}.")
self.query_one(CharacterStatsBox).pc_hp = self.app.adventure.active_party.active_character.max_hit_points

@on(Button.Pressed, "#btn_save_character")
def btn_save_character(self) -> None:
pc = self.app.adventure.active_party.active_character
pc.save_character()
self.query_one(Log).write_line(f"Character {pc.name} saved.")

def action_clear_log(self) -> None:
"""An action to clear the log."""
Expand All @@ -77,8 +97,20 @@ def action_new_character(self) -> None:
def action_next_character(self) -> None:
"""An action to switch to the next character in the party."""
self.app.adventure.active_party.set_next_character_as_active()
self.query_one(Log).write_line(
f"Active character is now {self.app.adventure.active_party.active_character.name}."
)
self.on_mount()

def action_delete_character(self) -> None:
"""An action to delete the active character."""
character_to_remove = self.app.adventure.active_party.active_character
self.action_next_character()
self.app.adventure.active_party.remove_character(character_to_remove)
self.query_one(Log).write_line(
f"Character {character_to_remove.name} removed from party."
)

def on_event(self, event: Event) -> Coroutine[Any, Any, None]:
"""Handle events."""
# HACK: This is a hack to get the screen to update when the user switches to it.
Expand Down
7 changes: 4 additions & 3 deletions osrgame/osrgame/screen_modal_new_char.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ def cancel_button_pressed(self) -> None:
@on(Button.Pressed, "#btn_char_create")
def create_button_pressed(self) -> None:
character_name = self.query_one("#character_name").value
character_class_value = self.query_one(RadioSet).pressed_button.label.plain
character_class = CharacterClassType[character_class_value.upper()]
# Implement the character creation logic here
character_class_name = self.query_one(RadioSet).pressed_button.name
character_class = CharacterClassType[character_class_name]
character = self.app.adventure.active_party.create_character(character_name, character_class)
self.app.adventure.active_party.set_active_character(character)
self.app.pop_screen()
3 changes: 2 additions & 1 deletion osrgame/osrgame/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class CharacterClassRadioButtons(Container):
def compose(self) -> ComposeResult:
with RadioSet(id="character_class") as radio_set:
for character_class in CharacterClassType:
yield RadioButton(character_class.value, value=character_class.name)
yield RadioButton(character_class.value, name=character_class.name)


class WelcomeScreenButtons(Container):
Expand All @@ -31,6 +31,7 @@ def compose(self) -> ComposeResult:
yield Button("Roll abilities", id="btn_roll_abilities", classes="button")
yield Button("Roll HP", id="btn_roll_hp", classes="button")
yield Button("Save character", id="btn_save_character", classes="button")
yield Button("Delete character", id="btn_delete_character", classes="button")

class CharacterStatsBox(Container):
"""A container for the character stats like name, class, level, HP, and AC."""
Expand Down
84 changes: 33 additions & 51 deletions osrlib/osrlib/dice_roller.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
"""Dice roller module for rolling dice based on the nDn or Dn notation, supporting modifiers."""
import random
import re
import random, re
from collections import namedtuple

# TODO: Change total_with_modifier to total_without_modifier and make total the total_with_modifer

class DiceRoll(
namedtuple(
"RollResultBase",
Expand All @@ -15,6 +14,7 @@ class DiceRoll(
Args:
namedtuple (RollResult): The named tuple containing the number of dice, number of sides, base roll, modifier, total roll with modifier, and the individual rolls.
"""

def __str__(self):
"""
Returns a string representation of the dice roll based on the ndn notation, including modifiers if applicable.
Expand All @@ -31,8 +31,7 @@ def __str__(self):
return base

def pretty_print(self):
"""
Returns a human-readable string representation of the dice roll, including the total roll and any modifiers.
"""Returns a human-readable string representation of the dice roll, including the total roll and any modifiers.
Returns:
str: A string describing the dice roll and its outcome (e.g., 'Rolled 3d6 and got 11 (11)', 'Rolled 1d20+3 and got 9 (6 + 3)').
Expand All @@ -44,11 +43,15 @@ def pretty_print(self):


def roll_dice(notation: str, modifier: int = 0, drop_lowest: bool = False):
"""
Rolls dice based on the nDn or Dn notation, supporting modifiers.
"""Rolls dice based on the nDn or Dn notation and factors in optional modifiers. Also accepts a string representing a single integer value.
To guarantee the result of the roll, specify a single string-formatted integer for ``notation``. For example, to
guarantee a roll of 20, pass "20" in the ``notation`` parameter. The ``RollResult`` that's returned will always be a
single roll on a die whose number of sides is the ``notation`` value as are its ``RollResult.total`` and
``RollResult.total_with_modifier`` attribute values.
Args:
notation (str): The dice notation in ndn format with optional modifiers (e.g., '3d6', '1d20+5', '2d8-4').
notation (str): A string representation of a dice roll in ndn format with optional modifiers like '3d6', '1d20+5', or '2d8-4'. Or specify single integer as string like '1', '20', or '18'.
modifier (int): An optional additional integer modifier to add to the roll. Defaults to 0.
drop_lowest (bool): Whether to drop the lowest dice roll. Defaults to False.
Expand All @@ -68,21 +71,31 @@ def roll_dice(notation: str, modifier: int = 0, drop_lowest: bool = False):
>>> result = roll_dice('4d6', drop_lowest=True)
>>> print(result.pretty_print())
"""
rand_gen = random.SystemRandom()

notation = notation.replace(" ", "")
notation = _add_modifier_to_dice_notation(notation, modifier)
notation = notation.replace(" ", "").lower()

try:
# First check to see if the notation string is a single integer passed as a string.
# We need to support calls that pass in a specific value in order to guarantee that
# the "roll" returns that value. You might do this in scenarios like specifying a
# set number of monsters in an encounter or number of gold pieces in a reward. This
# also enables unit tests need a consistent roll results for their test cases.
num_sides = int(notation)
return DiceRoll(1, num_sides, num_sides, 0, num_sides, [num_sides])
except ValueError:
pass

match = re.match(r"(\d*)d(\d+)([+-]\d+)?", notation, re.IGNORECASE)
if not match:
raise ValueError(
"Invalid number of dice and sides. Use dn or ndn format like 'd6', '3d6', '3d6+2', or '3d6-2'."
)

num_dice, num_sides, modifier = match.groups()
num_dice, num_sides, notation_modifier = match.groups()
num_dice = int(num_dice) if num_dice else 1
num_sides = int(num_sides)
modifier = int(modifier) if modifier else 0

if num_sides not in [1, 2, 3, 4, 6, 8, 10, 12, 20, 100]:
raise ValueError("Invalid number of dice sides. Choose from 1, 2, 3, 4, 6, 8, 10, 12, 20, 100.")
modifier += int(notation_modifier) if notation_modifier else 0

rand_gen = random.SystemRandom()
die_rolls = [rand_gen.randint(1, num_sides) for _ in range(num_dice)]

if drop_lowest and len(die_rolls) > 1:
Expand All @@ -91,37 +104,6 @@ def roll_dice(notation: str, modifier: int = 0, drop_lowest: bool = False):
total = sum(die_rolls)
total_with_modifier = total + modifier

return DiceRoll(num_dice, num_sides, total, modifier, total_with_modifier, die_rolls)


def _add_modifier_to_dice_notation(notation: str, modifier: int) -> str:
"""
Adds a modifier to a dice notation string.
Args:
notation (str): Existing dice notation string, like '1d6' or '1d6+1'.
modifier (int): The integer modifier to add.
Returns:
str: The modified dice notation string.
Raises:
ValueError: If the input notation is invalid.
"""
match = re.match(r"(\d*)d(\d+)([+-]\d+)?", notation, re.IGNORECASE)
if not match:
raise ValueError(
"Invalid number of dice and sides. Use dn or ndn format like 'd6', '3d6', '3d6+2', or '3d6-2'."
)

num_dice, num_sides, existing_modifier = match.groups()

existing_modifier = int(existing_modifier) if existing_modifier else 0

new_modifier = existing_modifier + modifier
if new_modifier == 0:
return f"{num_dice}d{num_sides}"
elif new_modifier > 0:
return f"{num_dice}d{num_sides}+{new_modifier}"
else:
return f"{num_dice}d{num_sides}{new_modifier}"
return DiceRoll(
num_dice, num_sides, total, modifier, total_with_modifier, die_rolls
)
17 changes: 1 addition & 16 deletions osrlib/osrlib/dungeon.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,12 @@
from enum import Enum
import random, json, asyncio, uuid
from openai import OpenAI
from osrlib.enums import OpenAIModelVersion
from osrlib.enums import Direction, OpenAIModelVersion
from osrlib.game_manager import logger
from osrlib.encounter import Encounter
from osrlib.dice_roller import roll_dice


class Direction(Enum):
"""Enumeration for directions a player can go within a location.
Attributes:
NORTH, SOUTH, EAST, WEST, UP, DOWN: Cardinal directions and vertical movements.
"""

NORTH = "N"
SOUTH = "S"
EAST = "E"
WEST = "W"
UP = "U"
DOWN = "D"


class Exit:
"""Represents an exit leading from one location to another within a dungeon.
Expand Down
Loading

0 comments on commit 82a20ab

Please sign in to comment.