NDL Pattern: Dialogue Generation

ndl pattern dialogue conversation social

Overview

Dialogue generation in NDL handles character speech, conversation flow, and social interaction. By combining talk(), convey(), and social action constructs with entity references, NDL enables dynamic multi-character conversations while maintaining deterministic control over who speaks, what they’re communicating, and the social context.

Core Philosophy

Unlike traditional dialogue systems that use branching trees or pre-written lines, NDL describes the intent and structure of dialogue, letting the LLM generate actual words while the game controls the conversation flow.

Separation of Concerns

  1. Game logic determines:

    • Who is speaking to whom
    • What topic/information is being conveyed
    • Conversation flow and turn order
    • Emotional context and social dynamics
  2. NDL describes:

    • Speaker and audience references
    • Message intent or perspective
    • Conversational manner (formal, casual, aggressive, etc.)
  3. LLM generates:

    • Actual dialogue text
    • Character voice and personality
    • Appropriate language for context

Basic Dialogue Patterns

Simple Statement

talk($"John", $"Mary") -> convey("quest information")

Generated Narrative:

John: “Mary, I’ve learned something important about the artifact. The merchant told me it’s hidden in the old temple ruins, guarded by ancient wards.”

Multi-Person Dialogue

talk($"Mark", $"John"^$"Sue") -> convey("desire to explore dungeon", perspective="adventurous")

Design Notes:

  • ^ operator (conjunction) specifies multiple listeners
  • perspective modifier colors how the message is delivered
  • All audience members receive same message in same turn

Generated Narrative:

Mark: “Hey, what do you say we check out that dungeon everyone’s been talking about? I heard there’s treasure deeper down, and I’m itching for some action!”

Conversation Exchange

talk($"player", $"guard") -> convey("request to enter city")
-> wait("response")
-> talk($"guard", $"player") -> convey("denial", perspective="authoritative")
-> wait("player decision")

Generated Narrative:

You: “I need to enter the city. I have urgent business with the council.”

Guard: “No one passes without proper papers. Council’s orders. Move along.”

Advanced Dialogue Patterns

Persuasion Attempt

talk($"player", $"merchant") -> convey("lower price request", perspective="persuasive")
-> roll("persuasion") -> result("success")
-> talk($"merchant", $"player") -> convey("agreement", perspective="reluctant")

Generated Narrative:

You: “Come on, I’m a regular customer. Surely you can give me a better deal on this?”

[Roll: Persuasion - Success]

Merchant: “Alright, alright. I suppose I can knock off a few coins. But don’t go telling everyone, or I’ll lose business.”

Failed Social Interaction

talk($"player", $"noble") -> convey("introduce self", perspective="casual")
-> result("offended")
-> talk($"noble", $"player") -> convey("dismissal", perspective="insulted")

Generated Narrative:

You: “Hey there! Name’s Jack. What’s yours?”

Noble: “How dare you address me so informally! I am Lord Blackwood, and you will show proper respect or remove yourself from my presence immediately!”

Group Conversation

talk($"narrator", $"party") -> convey("situation description")
-> talk($"wizard", $"party") -> convey("analysis", perspective="intellectual")
-> talk($"warrior", $"party") -> convey("action plan", perspective="direct")
-> talk($"rogue", $"party") -> convey("alternative approach", perspective="sneaky")

Pattern: Round-robin discussion where each character contributes their perspective on the same topic.

Dialogue with Emotional Context

Manner Modifiers in Dialogue

talk($"player", $"enemy") ~ "threateningly" -> convey("demand for surrender")
talk($"npc", $"player") ~ "whispering" -> convey("secret information")
talk($"lover", $"player") ~ "tenderly" -> convey("affection")

Pattern: The ~ modifier affects how something is said, influencing tone, volume, and delivery style.

Intention in Dialogue

talk($"player", $"suspect") -> convey("casual question", intention="gather information without raising suspicion")

Generated Narrative:

You: “So, you’ve been living in town long? I’m new here myself.” (You’re trying to get them talking without seeming suspicious)

Combined Manner and Intention

talk($"diplomat", $"warlord") ~ "formally" -> convey("peace proposal", intention="prevent war while showing strength")

Generated Narrative:

Diplomat: “My lord, we come bearing terms from the High Council. We seek peace between our nations, but make no mistake—we are prepared to defend our borders if necessary. Let us find a path that honors both our peoples.”

Conversation Flow Control

Interruption

talk($"villain", $"player") -> convey("monologue about plan")
-> do($"player", "interrupt") ~ "suddenly"
-> talk($"player", $"villain") -> convey("challenge", perspective="defiant")

Silence / No Response

talk($"player", $"stranger") -> convey("greeting")
-> wait("response")
-> result("no response")
-> describe("stranger ignores you")

Overheard Conversation

talk($"conspirator1", $"conspirator2") -> convey("plot details")
-> system_response("player is listening from hiding")
-> describe("player learns information")

Pattern: Speaker and audience don’t include player, but player entity gains knowledge.

Quest Dialogue Integration

Quest Stage-Dependent Dialogue

From User-monkeyrithms implementation:

# Quest not started
talk($"questgiver", $"player") -> convey("quest offer", perspective="concerned")
 
# Quest in progress
talk($"questgiver", $"player") -> convey("encouragement", perspective="hopeful")
 
# Quest completed
talk($"questgiver", $"player") -> convey("gratitude and reward", perspective="relieved")

Implementation Note:

“I drew most of my game design inspiration from Elder Scrolls games, which use a ‘quest stage’ variable that updates according to pre-existing conditions, and it changes the dialogue prompts that NPCs are given throughout the game. It works really well when I combined it with LLM+function call.”

Information Gating

talk($"player", $"innkeeper") -> convey("ask about local rumors")
-> search($"innkeeper knowledge base", intention="find rumor about dragon")
-> result("rumor found")
-> talk($"innkeeper", $"player") -> convey("dragon rumor", perspective="gossipy")

Auto-Chat / Background Conversations

NPC-to-NPC Dialogue (Player Observing)

From User-monkeyrithms implementation:

system_response("auto-chat mode activated")
-> talk($"npc1", $"npc2") -> convey("philosophy discussion")
-> wait("response")
-> talk($"npc2", $"npc1") -> convey("counter-argument")
-> wait("response")
-> talk($"npc1", $"npc2") -> convey("agreement", perspective="reluctant")

Design Notes:

“the fact that you could tell two LLMs to talk to each other about philosophy and they actually will, for as long as you want, without a script, is kind of a cool narrative device (although they do tend to go off the rails after a while)”

Prompting Tip:

“right now they are just told to talk about stuff and they decided to talk about markets. I probably need to include phrases like don’t start with the other character’s name, because that got repetitive”

Scripted NPC Events

# Player observes two NPCs talking
talk($"guard", $"merchant") -> convey("inspection notice")
-> wait("merchant response")
-> talk($"merchant", $"guard") -> convey("complaint", perspective="annoyed")
-> wait("conversation concludes")
-> system_response("player learned about increased patrols")

Use Case: Ambient worldbuilding - player overhears conversations that reveal setting details or quest hooks.

Social Actions Beyond Talk

Non-Verbal Communication

do($"player", "gesture") ~ "pointing at map" -> target($"guide")
-> convey("indicate location")
do($"npc", "signal") ~ "hand sign" -> target($"ally")
-> convey("danger warning")

Emotive Actions

do($"character", "laugh") ~ "mockingly" -> target($"player")
-> convey("derision")
do($"player", "nod") ~ "respectfully" -> target($"elder")
-> convey("understanding and respect")

Conversation State Management

Tracking Conversation Topics

def generate_dialogue_ndl(speaker: Entity, listener: Entity,
                         conversation_state: ConversationState) -> str:
    """Generate NDL for dialogue based on conversation state."""
 
    # Determine topic based on relationship and quest states
    if conversation_state.first_meeting:
        topic = "introduction"
        perspective = "cautious"
    elif conversation_state.active_quest:
        topic = f"quest update: {conversation_state.active_quest.name}"
        perspective = "focused"
    elif conversation_state.recent_conflict:
        topic = "apology" if speaker.disposition > 50 else "grievance"
        perspective = "tense"
    else:
        topic = "small talk"
        perspective = "friendly"
 
    ndl = f'talk(${speaker.id}, ${listener.id})'
    ndl += f' -> convey("{topic}", perspective="{perspective}")'
 
    return ndl

Conversation Branching

talk($"npc", $"player") -> convey("question with two implications")
-> wait("player choice")
# Game presents: [Agree] or [Refuse]
 
# If player chooses Agree:
-> talk($"player", $"npc") -> convey("agreement")
-> talk($"npc", $"player") -> convey("next quest step")
 
# If player chooses Refuse:
-> talk($"player", $"npc") -> convey("refusal")
-> talk($"npc", $"player") -> convey("disappointment")
-> result("relationship decreased")

Dynamic Conversation Generation

Context Steering

From User-veritasr:

talk($"nobles", $"assembly") -> convey("current affairs discussion")
-> system_response("context: recent law about trade tariffs")
-> wait("topic shift to law")

Implementation Note:

“now lets see if we can wiggle the context in the background and get them to steer the conversation..maybe, the latest law coming into effect”

Personality-Driven Dialogue

# Brave character
talk($"hero", $"party") -> convey("encouragement before battle", perspective="courageous")
 
# Cowardly character
talk($"coward", $"party") -> convey("express fear", perspective="terrified")
 
# Sarcastic character
talk($"rogue", $"party") -> convey("observation", perspective="sarcastic")

Pattern: perspective acts as a personality lens for dialogue generation.

Dialogue Mode Transitions

Normal → Conversation Mode

do($"player", "approach") -> target($"npc")
-> system_response("entering conversation mode")
-> talk($"player", $"npc") -> convey("greeting")

Conversation → Combat Mode

talk($"player", $"bandit") -> convey("refusal to pay toll", perspective="defiant")
-> result("negotiation failed")
-> system_response("combat initiated")
-> do($"bandit", "attack") -> target($"player")

Group Conversation End Condition

From User-veritasr:

“So my question is.. How does it know when the conversation is over? What keeps it from chatting forever in the background?”

Solution Pattern:

talk($"npc1", $"npc2") -> convey("topic discussion")
-> wait("response")
-> talk($"npc2", $"npc1") -> convey("conclusion", perspective="final")
-> system_response("conversation ended")

Or use a turn/topic counter:

def auto_chat(npc1: Entity, npc2: Entity, max_turns: int = 5):
    for turn in range(max_turns):
        if turn % 2 == 0:
            ndl = f'talk(${npc1.id}, ${npc2.id}) -> convey("topic {turn//2 + 1}")'
        else:
            ndl = f'talk(${npc2.id}, ${npc1.id}) -> convey("response to topic {turn//2 + 1}")'
 
        yield ndl + ' -> wait("response")'
 
    # End conversation
    yield 'system_response("conversation concluded")'

Common Anti-Patterns

❌ Don’t: Let LLM Choose Speaker

# WRONG - ambiguous speaker
convey("information about quest")
# Who is speaking? LLM might hallucinate the wrong character

✅ Do: Always Specify Speaker and Listener

# CORRECT - explicit speaker and audience
talk($"questgiver", $"player") -> convey("information about quest")

❌ Don’t: Mix Multiple Intents in One Convey

# WRONG - too much in one statement
talk($"npc", $"player") -> convey("greeting and quest offer and personal backstory")

✅ Do: Separate Conversational Beats

# CORRECT - one topic per turn
talk($"npc", $"player") -> convey("greeting")
-> wait("player response")
-> talk($"player", $"npc") -> convey("ask about quest")
-> wait("npc response")
-> talk($"npc", $"player") -> convey("quest offer")

❌ Don’t: Assume Dialogue Order Without Sequencing

# WRONG - no sequence control
talk($"a", $"b") -> convey("question")
talk($"b", $"a") -> convey("answer")
# LLM might generate both simultaneously, breaking conversation flow

✅ Do: Use Sequencing for Turn Order

# CORRECT - explicit turn order
talk($"a", $"b") -> convey("question")
-> wait("response")
-> talk($"b", $"a") -> convey("answer")

Genre-Specific Variations

Fantasy RPG Dialogue

talk($"wizard", $"apprentice") ~ "arcane terminology"
-> convey("magical instruction", perspective="scholarly")

Noir Detective Dialogue

talk($"detective", $"informant") ~ "cynical tone"
-> convey("demand for information", intention="intimidate into honesty")

Sci-Fi Diplomatic Exchange

talk($"captain", $"alien ambassador") ~ "universal translator"
-> convey("peaceful intentions", perspective="formal diplomatic protocol")

Horror / Sanity Dialogue

talk($"player", $"apparition") -> convey("question reality")
-> result("no coherent response")
-> describe("words echo strangely, filled with static")
-> system_response("sanity decreased")

Integration with Social Systems

Relationship Tracking

talk($"player", $"npc") -> convey("compliment", perspective="sincere")
-> result("positive reaction")
-> system_response("relationship +10")

Reputation Effects

talk($"player", $"guild_master") ~ "formally" -> convey("report successful mission")
-> result("impressed")
-> system_response("guild reputation +25")
-> talk($"guild_master", $"player") -> convey("promotion offer")

Social Skill Checks

talk($"player", $"guard") -> convey("convincing lie", intention="gain entry")
-> roll("deception") -> result("failure")
-> talk($"guard", $"player") -> convey("suspicion and alarm", perspective="hostile")
-> system_response("guards alerted")

Performance Considerations

Token Efficiency

Dialogue NDL is lightweight:

  • Simple exchange: ~20-30 tokens
  • Complex multi-turn: ~50-80 tokens
  • Generated output: 40-100 tokens per speaker turn

Model Requirements

  • Minimum: 7B-8B models work adequately for dialogue
  • Recommended: 8B-13B for personality consistency
  • Optimal: 13B+ for complex multi-character conversations

Caching Strategies

For repeated conversations with the same NPC:

# Cache character voice/personality with first dialogue
@cache_voice(speaker_id)
def generate_dialogue(speaker: Entity, message: str) -> str:
    ndl = f'talk(${speaker.id}, $"player") -> convey("{message}")'
    return process_ndl(ndl)

Testing Dialogue Patterns

Validation Checklist

  • Correct speaker attribution (no mixed-up character voices)
  • Appropriate dialogue for character personality
  • Conversational flow feels natural (no abrupt topic jumps)
  • Perspective modifiers affect tone consistently
  • Multi-character conversations maintain distinct voices
  • Emotional context (manner, intention) reflected in word choice
  • Quest state changes reflected in NPC dialogue
  • Conversation endings feel natural, not abrupt

Common Edge Cases

  1. Character speaks to self:

    talk($"player", $"player") -> convey("internal monologue")
    # Some implementations use "think()" instead
  2. Broadcast message (speaker to all present):

    talk($"crier", $"crowd") -> convey("public announcement")
  3. Silent protagonist response:

    talk($"npc", $"player") -> convey("question")
    -> wait("player chooses response")
    -> system_response("player selected: [dialogue option 2]")
    -> talk($"npc", $"player") -> convey("response to option 2")

Real-World Implementation

From User-monkeyrithms ReallmCraft:

  • Quest dialogue changes based on stage variable
  • Auto-chat mode for NPC-to-NPC conversations
  • Schedule-based conversations trigger when player observes
  • Conversation prompts injected with relationship context

From User-veritasr philosophy:

“having concrete objectives helps the llm to define your intention.. I’m using this now in ST to guide generation in a specific direction.. Because I want THESE types of events to occur. It causes it to generate certain TYPES of stories.”

Further Reading


Pattern Status: Validated in Production (ReallmCraft) Model Compatibility: 7B+ models, optimal at 13B+ Complexity: Intermediate to Advanced Use Cases: RPGs, interactive fiction, social simulation, quest systems