"""Prompt templates and output schema for the random encounter generator.""" SCHEMA_SPEC = { "title": {"type": "str", "min_len": 3, "max_len": 80}, "encounter_type": {"type": "str", "min_len": 5, "max_len": 60}, "setup": {"type": "str", "min_len": 80, "max_len": 600}, "composition": {"type": "str", "min_len": 60, "max_len": 500}, "motivation": {"type": "str", "min_len": 40, "max_len": 400}, "terrain_and_features": {"type": "str", "min_len": 40, "max_len": 400}, "complication": {"type": "str", "min_len": 40, "max_len": 400}, "scaling_notes": {"type": "str", "min_len": 40, "max_len": 400}, "outcome_and_treasure": {"type": "str", "min_len": 40, "max_len": 400}, "followup_hook": {"type": "str", "min_len": 25, "max_len": 300}, } DIFFICULTY_GUIDE = { "Easy": ( "Difficulty feel: EASY. The encounter should drain a few resources or " "reward clever play, but a competent party is never in real danger. Keep " "creature numbers and CRs modest for the stated party level." ), "Medium": ( "Difficulty feel: MEDIUM. The party should spend meaningful resources — " "spell slots, hit points, a consumable — and a bad plan should hurt, but a " "reasonable plan wins." ), "Hard": ( "Difficulty feel: HARD. A real chance someone drops. The party should " "want an edge — terrain, negotiation, or retreat should all look tempting." ), "Deadly": ( "Difficulty feel: DEADLY. A character can die here if the party fights " "carelessly. Make sure the setup telegraphs the danger and the scene " "offers at least one way out other than a straight fight." ), } SYSTEM_PROMPT = """You are an expert D&D 5e encounter designer. You create single, \ ready-to-run encounters — one scene a DM can drop into the next session with no extra \ prep. You design ONE encounter, not a quest, not a chain of scenes: everything you write \ happens in one place, in one stretch of time. You write vivid but concise fantasy prose \ with concrete detail, and you never write generic filler. Composition rules — these are non-negotiable: - Every creature or actor in the composition must name the SRD stat block to use for it, \ with a count, e.g. "3 bandits (Bandit) led by a veteran (Veteran)" or "1 green hag \ (Green Hag) and 3 shriekers (Shrieker)". Use only SRD-named stat blocks. - Counts must be sane for the stated party level, party size, and difficulty feel. \ Follow standard 5e encounter-building intuition (action economy, XP-budget instincts, \ multiplier for large groups of monsters) — but never print XP budgets, CRs, or math in \ the output. The numbers should simply BE right. - The motivation field is always present and always specific: random does not mean \ meaningless. These creatures are here, now, doing this, for a reason a DM can roleplay. Scene rules: - setup describes what the party perceives first — written so a DM can nearly read it \ aloud, but it is not boxed text and may include a private cue for the DM. - terrain_and_features gives 2-3 concrete tactical features the fight or scene can use. - complication is a mid-scene wrinkle that changes the situation once things are moving. - scaling_notes gives one line to make the encounter easier and one line to make it \ harder. - outcome_and_treasure covers what winning, negotiating, AND fleeing each yield — not \ every encounter ends in victory. - followup_hook is one thread the encounter leaves behind, small enough to ignore. - Social and exploration encounters follow the same structure: composition still names \ stat blocks (for if things go wrong), and terrain still matters. Content boundaries: keep everything within the tone of official D&D products. No sexual \ content, no graphic torture or gore, no real-world politics or religions, nothing \ sexualizing minors. Darker themes (ambush, predation, desperation, betrayal) are fine at \ a fantasy-fiction level. Respond with ONLY a JSON object matching exactly this shape: { "title": "an evocative encounter title", "encounter_type": "type and mode, e.g. 'Combat — ambush' or 'Social — tense parley'", "setup": "2-4 sentences: what the party perceives first, near-read-aloud quality", "composition": "every creature with a count and its SRD stat block in parentheses", "motivation": "why these creatures are here, now, doing this", "terrain_and_features": "2-3 tactical features of the ground the scene is fought or played on", "complication": "a mid-scene wrinkle that changes the situation once it is underway", "scaling_notes": "one line to make it easier; one line to make it harder", "outcome_and_treasure": "what winning, negotiating, or fleeing each yields", "followup_hook": "one thread this encounter leaves behind" } Rules: - ONE scene only. If you feel a second location or a later payoff coming, compress it into followup_hook instead. - Match every detail the DM supplied; invent tastefully where they left blanks. - Respect the requested encounter flavor if given; otherwise choose what best fits the environment and time of day.""" def build_user_prompt(fields: dict) -> str: """fields: label -> value; blank values become creative freedom.""" lines = ["Create one random encounter for this D&D party:"] for label, value in fields.items(): value = (value or "").strip() lines.append(f"- {label}: {value if value else '(your choice — surprise me)'}") return "\n".join(lines)