Jordancole21's picture
Deploy native Gradio SEO page and tool
43904b7 verified
Raw
History Blame Contribute Delete
3.08 kB
"""Monster builder: input handling, generation, rendering, export."""
import html
import random
import tempfile
from pathlib import Path
from .shared import ai_client
from .shared.ratelimit import RateLimiter
from .prompts import SCHEMA_SPEC, SYSTEM_PROMPT, build_user_prompt
from .fixtures import FIXTURE_DROWNLIGHT_SCUTTLER
limiter = RateLimiter()
CHALLENGE_RATINGS = ["1/8", "1/4", "1/2"] + [str(n) for n in range(1, 21)]
SIZES = ["Tiny", "Small", "Medium", "Large", "Huge", "Gargantuan"]
CREATURE_TYPES = [
"Aberration", "Beast", "Celestial", "Construct", "Dragon", "Elemental",
"Fey", "Fiend", "Giant", "Humanoid", "Monstrosity", "Ooze", "Plant", "Undead",
]
COMBAT_ROLES = [
"Brute", "Skirmisher", "Artillery", "Controller", "Lurker", "Leader",
"Minion swarm",
]
MAX_FIELD = 300
MAX_NOTES = 800
def randomize():
"""Fill the core dropdowns with a random buildable combination."""
return (
random.choice(CHALLENGE_RATINGS),
random.choice(SIZES),
random.choice(CREATURE_TYPES),
random.choice(COMBAT_ROLES),
)
def _clip(value: str, limit: int) -> str:
return (value or "").strip()[:limit]
def generate(concept, cr, size, creature_type, role, environment, notes,
request=None):
"""Returns (markdown, plain_text, download_path, error_message)."""
allowed, message = limiter.check(request)
if not allowed:
return None, None, None, message
fields = {
"Monster concept": _clip(concept, MAX_FIELD),
"Challenge rating target": _clip(cr, 20),
"Size": _clip(size, 40),
"Creature type": _clip(creature_type, MAX_FIELD),
"Combat role": _clip(role, MAX_FIELD),
"Environment": _clip(environment, MAX_FIELD),
"Extra notes": _clip(notes, MAX_NOTES),
}
user_prompt = build_user_prompt(fields)
try:
data = ai_client.generate_json(
SYSTEM_PROMPT, user_prompt, SCHEMA_SPEC,
fixture=FIXTURE_DROWNLIGHT_SCUTTLER,
)
except ai_client.AIUnavailable as exc:
return None, None, None, str(exc)
md = render_markdown(data)
path = Path(tempfile.mkdtemp()) / "dnd-monster.md"
path.write_text(md, encoding="utf-8")
return md, md, str(path), None
def render_markdown(d: dict) -> str:
traits = "\n".join(f"- {t}" for t in d["traits"])
actions = "\n".join(f"- {a}" for a in d["actions"])
return f"""## {d['name']}
*{d['size_type_alignment']}*
{d['lore']}
**Armor Class** {d['armor_class']} \n**Hit Points** {d['hit_points']} \n**Speed** {d['speed']} \n**Ability Scores** {d['ability_scores']} \n**Saves & Skills** {d['saves_and_skills']} \n**Defenses** {d['defenses']} \n**Senses & Languages** {d['senses_and_languages']} \n**Challenge** {d['challenge']}
### Traits
{traits}
### Actions
{actions}
### Tactics
{d['tactics']}
### Loot
{d['loot']}
"""
def render_error(message: str) -> str:
return (
'<div class="lf-output" role="alert" style="border-color:#A7343B;">'
f"<p>{html.escape(message)}</p></div>"
)