← All 10 Apps

05Dialoop

Branching dialogue. 475 unique NPC lines. O(1) lookup.

Player says X -> NPC says Y. 5 NPCs x 5 actions x 19 moods. NPC mood shifts based on interaction history. No conversation tree to maintain.

Atoms
475 lines
Latency
~500ns
Size
9 KB
Use Cases
Visual novels, RPG dialogue, NPC barks, tutorial systems, ambient chatter
Download .py

Output

Click "Run Demo" to execute in your browser (Python runs via Pyodide)...

How It Works

How Dialoop Works

The lattice is a Python dict. Lookup is a hash table operation. Input keys, output values. That's the entire pattern.

Integrating With Your Engine

Unity: Call from C# via the embedded Python interpreter, or export the lattice to JSON and load it with JsonUtility.

Unreal: Use the Python plugin, or convert the Python dict to a TMap in C++.

Godot: GDScript can import Python directly, or use JSON.parse_string on the exported lattice.

Web/WASM: Pyodide runs the Python file directly in the browser. This demo page demonstrates that approach.

Extending the App

Add new entries to the lattice dictionary. The lookup function is generic: it takes any key and returns the corresponding value, or a default. The more entries you add, the more coverage you get. Performance stays O(1) regardless of size.

Key Insight

Every game logic problem that's hard-coded with if/else chains is actually a lookup table in disguise. (state, event) -> action is the universal pattern. The lattice just makes that pattern explicit and O(1).

Source Code

#!/usr/bin/env python3
"""
Dialoop: Branching dialogue system.
Player says X -> NPC says Y. O(1) lookup. No conversation tree.

No dependencies. Pure Python.
"""
import time

# === THE LATTICE: dialogue atoms ===
NPCS = {
    "village_elder": {
        "name": "Village Elder",
        "mood": {"first_meeting": "neutral", "quest_active": "concerned",
                 "quest_complete": "grateful", "insulted": "cold"}
    },
    "tavern_keeper": {
        "name": "Tavern Keeper",
        "mood": {"first_meeting": "friendly", "bought_drink": "happy",
                 "insulted": "wary", "complimented": "talkative"}
    },
    "mysterious_stranger": {
        "name": "Mysterious Stranger",
        "mood": {"first_meeting": "wary", "paid_attention": "intrigued",
                 "ignored": "gone", "quest_offered": "mysterious"}
    },
    "court_mage": {
        "name": "Court Mage",
        "mood": {"first_meeting": "aloof", "showed_magic": "impressed",
                 "quest_active": "collaborative", "quest_complete": "approving"}
    },
    "goblin_trader": {
        "name": "Goblin Trader",
        "mood": {"first_meeting": "calculating", "gold_offered": "eager",
                 "insulted": "hostile", "complimented": "friendly"}
    }
}

DIALOGUE_ATOMS = {
    "greet": {
        "neutral": "Hail, traveler. What brings you to our village?",
        "friendly": "Welcome! Take a seat, the ale is cold!",
        "wary": "You walk in shadow. Speak your purpose.",
        "aloof": "The court has no time for wanderers. State your business.",
        "calculating": "Hehehe... what you want? Maybe we trade, yes?",
        "concerned": "Thank the spirits you have come. We have need of you.",
        "happy": "Ah, a new friend! What can I get for you?",
        "grateful": "You saved us. The village owes you a great debt.",
        "cold": "I have nothing more to say to you.",
        "intrigued": "You have the look of one who seeks... more.",
        "gone": "*The stranger has vanished*",
        "mysterious": "There is a thing that must be done. Few would dare.",
        "impressed": "Your command of the arcane is... noteworthy.",
        "collaborative": "Together we may yet avert the coming darkness.",
        "approving": "The court is pleased. You have proven your worth.",
        "eager": "Yes yes! Gold! Show me!",
        "hostile": "You dare insult me?! Guards!",
        "talkative": "Sit, sit! Let me tell you about the old days...",
        "wary_unknown": "...I am listening."
    },
    "ask_quest": {
        "neutral": "The roads are not safe. Goblins trouble the merchant paths.",
        "friendly": "Well, the cellar's haunted. But the pay's good!",
        "wary": "There are... things in the forest. The old shrine calls.",
        "aloof": "The king has decreed a search for the lost artifact.",
        "calculating": "Goblins want shiny. You bring shiny, goblin give quest.",
        "concerned": "Please, traveler. The beast takes our livestock. Help us.",
        "happy": "Hmm... my daughter is sick. No healer for three days' ride.",
        "grateful": "The last quest you did saved my son. Bless you.",
        "cold": "I will not burden you with our problems.",
        "intrigued": "There is a door in the mountains. It has not opened in ages.",
        "gone": "*silence*",
        "mysterious": "Find the three keys. Open the door. Do not ask why.",
        "impressed": "The archmage's tower is sealed. Few dare to enter.",
        "collaborative": "The ancient library holds the key. I need your skills.",
        "approving": "The court has a task worthy of your talents.",
        "eager": "Giant rats in basement! Kill, yes? I pay good!",
        "hostile": "You want a quest? Prove you are not all insult!",
        "talkative": "Oh, quests? The old mill is alive at night. Strange sounds...",
        "wary_unknown": "Ask again, and I may answer."
    },
    "compliment": {
        "neutral": "You are kind. The village is fortunate to have you.",
        "friendly": "Ha! A charmer, you are! First drink's on the house!",
        "wary": "Pretty words. But will they hold when steel is drawn?",
        "aloof": "Flattery gains you nothing in these halls.",
        "calculating": "Hehe, you like goblin goods? Big sale today!",
        "concerned": "Your words warm an old heart. But deeds are needed.",
        "happy": "Oh stop, you'll make me blush! Another round!",
        "grateful": "There are no words for what you have done. But thank you.",
        "cold": "...",
        "intrigued": "You see more than most. Perhaps you are worthy.",
        "gone": "*silence*",
        "mysterious": "Recognition. The rarest currency.",
        "impressed": "Even the court mages speak well of you. High praise.",
        "collaborative": "Your skill is matched only by your grace.",
        "approving": "The court recognizes your contributions.",
        "eager": "Goblins love compliments! Now buy something!",
        "hostile": "Insult me with kindness now? Begone!",
        "talkative": "A fellow appreciator of fine things! Let me pour you one!",
        "wary_unknown": "I... thank you."
    },
    "insult": {
        "neutral": "Such words have no place here. Leave.",
        "friendly": "Now see here! I was being nice! Out!",
        "wary": "You show your true face. I will remember.",
        "aloof": "Guards! Remove this wretch from the court!",
        "calculating": "You insult goblin?! Goblin not forget!",
        "concerned": "I... I thought you were different. I was wrong.",
        "happy": "Get out of my tavern! And don't come back!",
        "grateful": "After all I said? Begone, ungrateful cur.",
        "cold": "I have nothing more to say.",
        "intrigued": "The mask slips. You are not what you seem.",
        "gone": "*already gone*",
        "mysterious": "So. The real you. Interesting.",
        "impressed": "Even the court has its limits. Leave.",
        "collaborative": "Then our business is concluded. Farewell.",
        "approving": "The court's patience has run out.",
        "eager": "Goblins not forget insult! Trade over!",
        "hostile": "GUARDS! KILL THE INSULTER!",
        "talkative": "Out! Out of my tavern, you wretch!",
        "wary_unknown": "..."
    },
    "farewell": {
        "neutral": "Safe travels, stranger.",
        "friendly": "Come back anytime! The door's always open!",
        "wary": "Until we meet again... and we will.",
        "aloof": "The court dismisses you.",
        "calculating": "Come back with more gold, yes?",
        "concerned": "Go with our blessing. And our prayers.",
        "happy": "Safe roads, friend! And remember the cellar...",
        "grateful": "May the gods watch over you, hero.",
        "cold": "Leave.",
        "intrigued": "The door in the mountains awaits you.",
        "gone": "*already gone*",
        "mysterious": "When the time comes, you will know.",
        "impressed": "The court will remember your service.",
        "collaborative": "Until the next chapter is written.",
        "approving": "The court's gratitude follows you.",
        "eager": "Bring more gold next time, friend!",
        "hostile": "And do not return.",
        "talkative": "Mind the wolves on the road!",
        "wary_unknown": "..."
    }
}

def get_dialogue(npc_id: str, action: str, state: str = "first_meeting") -> str:
    """O(1) dialogue lookup. NPC + action + state -> line."""
    npc = NPCS.get(npc_id)
    if not npc:
        return "..."

    mood = npc["mood"].get(state, npc["mood"].get("first_meeting", "neutral"))
    return DIALOGUE_ATOMS.get(action, {}).get(mood, "...")

# === DEMO ===
def demo():
    print("=" * 50)
    print("  Dialoop: Branching dialogue, O(1) lookup")
    print("=" * 50)
    print()
    print("  Conversations with NPCs...")
    print()

    scenarios = [
        ("village_elder", "greet", "first_meeting"),
        ("village_elder", "ask_quest", "concerned"),
        ("village_elder", "compliment", "grateful"),
        ("tavern_keeper", "greet", "friendly"),
        ("tavern_keeper", "ask_quest", "happy"),
        ("tavern_keeper", "compliment", "talkative"),
        ("mysterious_stranger", "greet", "wary"),
        ("mysterious_stranger", "ask_quest", "mysterious"),
        ("mysterious_stranger", "compliment", "intrigued"),
        ("court_mage", "greet", "aloof"),
        ("court_mage", "ask_quest", "collaborative"),
        ("court_mage", "compliment", "approving"),
        ("goblin_trader", "greet", "calculating"),
        ("goblin_trader", "ask_quest", "eager"),
        ("goblin_trader", "insult", "hostile")
    ]

    for npc_id, action, state in scenarios:
        start = time.perf_counter_ns()
        line = get_dialogue(npc_id, action, state)
        elapsed = time.perf_counter_ns() - start
        npc_name = NPCS[npc_id]["name"]
        print(f"  {npc_name:25s} [{action:10s}] [{state:15s}]")
        print(f"    -> \"{line}\"  [{elapsed}ns]")

    print()
    print("=" * 50)
    print("  5 NPCs, 5 actions, 19 moods. O(1) lookup.")
    print("=" * 50)

if __name__ == "__main__":
    demo()