DungeonMaster-AI / src /utils /formatters.py
bhupesh-sf's picture
first commit
f8ba6bf verified
"""
DungeonMaster AI - Output Formatters
Utilities for formatting game data for display in chat and UI components.
"""
from typing import Any
def format_dice_roll(
notation: str,
individual_rolls: list[int],
modifier: int,
total: int,
is_check: bool = False,
dc: int | None = None,
success: bool | None = None,
) -> str:
"""
Format a dice roll result for display.
Args:
notation: The dice notation (e.g., "2d6+3")
individual_rolls: List of individual die results
modifier: The modifier applied to the roll
total: The final total
is_check: Whether this was an ability check/save
dc: Difficulty class if this was a check
success: Whether the check succeeded (if applicable)
Returns:
Formatted string for display
"""
# Format individual dice
dice_str = ", ".join(str(r) for r in individual_rolls)
# Build the breakdown
if modifier > 0:
breakdown = f"[{dice_str}] + {modifier}"
elif modifier < 0:
breakdown = f"[{dice_str}] - {abs(modifier)}"
else:
breakdown = f"[{dice_str}]"
# Check for critical (d20 rolls)
is_critical = False
is_fumble = False
if len(individual_rolls) == 1 and individual_rolls[0] == 20:
is_critical = "d20" in notation.lower()
if len(individual_rolls) == 1 and individual_rolls[0] == 1:
is_fumble = "d20" in notation.lower()
# Build result string
result_parts = [f"{notation} = {breakdown} = **{total}**"]
if is_critical:
result_parts.append("**CRITICAL!**")
elif is_fumble:
result_parts.append("*Critical Failure!*")
if is_check and dc is not None:
if success:
result_parts.append(f"vs DC {dc}: **Success!**")
else:
result_parts.append(f"vs DC {dc}: *Failure*")
return " ".join(result_parts)
def format_hp_change(
character_name: str,
previous_hp: int,
current_hp: int,
max_hp: int,
is_damage: bool,
) -> str:
"""
Format an HP change for display.
Args:
character_name: Name of the character
previous_hp: HP before the change
current_hp: HP after the change
max_hp: Maximum HP
is_damage: True if damage, False if healing
Returns:
Formatted string for display
"""
change = abs(current_hp - previous_hp)
if is_damage:
if current_hp <= 0:
return f"**{character_name}** takes {change} damage and falls unconscious! (0/{max_hp} HP)"
elif current_hp <= max_hp // 4:
return f"**{character_name}** takes {change} damage and is badly wounded! ({current_hp}/{max_hp} HP)"
else:
return f"**{character_name}** takes {change} damage. ({current_hp}/{max_hp} HP)"
else:
if current_hp >= max_hp:
return f"**{character_name}** is fully healed! ({max_hp}/{max_hp} HP)"
else:
return f"**{character_name}** heals {change} HP. ({current_hp}/{max_hp} HP)"
def format_combat_turn(
combatant_name: str,
initiative: int,
is_player: bool,
round_number: int,
) -> str:
"""
Format a combat turn announcement.
Args:
combatant_name: Name of the combatant whose turn it is
initiative: Their initiative value
is_player: Whether this is a player character
round_number: Current combat round
Returns:
Formatted string for display
"""
if is_player:
return f"**Round {round_number}** - It's your turn, **{combatant_name}**! (Initiative: {initiative})"
else:
return f"**Round {round_number}** - **{combatant_name}** takes their turn. (Initiative: {initiative})"
def format_ability_modifier(score: int) -> str:
"""
Format an ability score modifier for display.
Args:
score: The ability score (1-30)
Returns:
Formatted modifier string (e.g., "+2" or "-1")
"""
modifier = (score - 10) // 2
if modifier >= 0:
return f"+{modifier}"
return str(modifier)
def format_currency(gp: int = 0, sp: int = 0, cp: int = 0) -> str:
"""
Format currency for display.
Args:
gp: Gold pieces
sp: Silver pieces
cp: Copper pieces
Returns:
Formatted currency string
"""
parts = []
if gp > 0:
parts.append(f"{gp} gp")
if sp > 0:
parts.append(f"{sp} sp")
if cp > 0:
parts.append(f"{cp} cp")
if not parts:
return "0 gp"
return ", ".join(parts)
def format_condition_list(conditions: list[str]) -> str:
"""
Format a list of conditions for display.
Args:
conditions: List of condition names
Returns:
Formatted string or empty message
"""
if not conditions:
return "None"
return ", ".join(conditions)
def format_initiative_order(
combatants: list[dict[str, Any]],
current_turn_index: int,
) -> str:
"""
Format the initiative order for display.
Args:
combatants: List of combatant dictionaries with name, initiative, hp info
current_turn_index: Index of current turn combatant
Returns:
Formatted initiative order string
"""
lines = ["**Initiative Order:**"]
for i, combatant in enumerate(combatants):
marker = ">" if i == current_turn_index else " "
name = combatant.get("name", "Unknown")
init = combatant.get("initiative", 0)
hp_current = combatant.get("hp_current", 0)
hp_max = combatant.get("hp_max", 1)
hp_percent = (hp_current / hp_max) * 100 if hp_max > 0 else 0
if hp_percent > 50:
status = "Healthy"
elif hp_percent > 25:
status = "Wounded"
elif hp_percent > 0:
status = "Critical"
else:
status = "Down"
lines.append(f"{marker} {init:2d} | {name} ({status})")
return "\n".join(lines)
def format_character_summary(character: dict[str, Any]) -> str:
"""
Format a character summary for context.
Args:
character: Character data dictionary
Returns:
Formatted character summary
"""
name = character.get("name", "Unknown")
race = character.get("race", "Unknown")
char_class = character.get("class", "Unknown")
level = character.get("level", 1)
hp = character.get("hit_points", {})
hp_current = hp.get("current", 0)
hp_max = hp.get("maximum", 1)
ac = character.get("armor_class", 10)
conditions = character.get("conditions", [])
summary = f"**{name}** - Level {level} {race} {char_class}\n"
summary += f"HP: {hp_current}/{hp_max} | AC: {ac}\n"
if conditions:
summary += f"Conditions: {format_condition_list(conditions)}"
return summary
def format_adventure_intro(adventure_data: dict[str, Any]) -> str:
"""
Format an adventure introduction for the opening narration.
Args:
adventure_data: Adventure JSON data
Returns:
Formatted introduction text
"""
metadata = adventure_data.get("metadata", {})
starting_scene = adventure_data.get("starting_scene", {})
name = metadata.get("name", "Unnamed Adventure")
description = metadata.get("description", "")
narrative = starting_scene.get("narrative", "Your adventure begins...")
intro = f"# {name}\n\n"
if description:
intro += f"*{description}*\n\n"
intro += "---\n\n"
intro += str(narrative)
return intro