I recently shipped a dynamic narrative backend for a mobile RPG that generates quest dialogue and story branches on the fly. Instead of hard-coding thousands of dialogue trees, the game calls an LLM to react to player choices in real time. This tutorial walks through the exact Python service I built, using Oxlo.ai for inference because its request-based pricing keeps costs flat even when I send long character biographies and world lore every turn.
What you'll need
- Python 3.10 or newer
- An Oxlo.ai API key from https://portal.oxlo.ai
- The OpenAI SDK:
pip install openai - A local JSON file named
world_lore.jsonwith your game setting, or you can use the inline sample in Step 3
Step 1: Initialize the Oxlo.ai client
I use the OpenAI SDK as a drop-in replacement. Point the base URL to Oxlo.ai and select llama-3.3-70b as the general-purpose workhorse.
from openai import OpenAI
client = OpenAI(base_url="https://api.oxlo.ai/v1", api_key="YOUR_OXLO_API_KEY")
# Quick connectivity check
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": "You are a test assistant."},
{"role": "user", "content": "Say OK"},
],
max_tokens=5,
)
print(response.choices[0].message.content)
Step 2: Lock down the system prompt
The system prompt is the contract that keeps the model from inventing mechanics that do not exist. I pin the tone, available actions, and output schema here.
SYSTEM_PROMPT = """You are the narrative engine for a mobile fantasy RPG.
Rules:
- Stay in character as the quest giver, a weary blacksmith named Harn.
- Never mention you are an AI.
- Respond in JSON with keys: npc_text, quest_update, emotion.
- Available emotions: neutral, angry, hopeful, suspicious.
- Do not give the player items directly; only suggest where to find them."""
Step 3: Build the game state context
Mobile games need to minimize back-and-forth, so I batch the current quest, inventory, and recent player actions into a single user message. I keep a sliding window of the last five turns to cap context length.
import json
WORLD_LORE = {
"setting": "Ironvale",
"current_quest": "Find the missing shipment of star-metal.",
"player_inventory": ["rusty dagger", "health potion"],
"recent_actions": ["asked villagers about bandits", "searched the northern road"]
}
def build_user_message(player_input: str) -> str:
context = json.dumps(WORLD_LORE, indent=2)
return f"""World state:
{context}
Player says: {player_input}
Generate the next narrative beat."""
Step 4: Force structured output with JSON mode
Mobile clients crash on malformed responses, so I use JSON mode to guarantee a parseable payload. I also set a low max_tokens limit because mobile UIs only have room for a few sentences.
def generate_narrative(player_input: str) -> dict:
user_message = build_user_message(player_input)
response = client.chat.completions.create(
model="llama-3.3-70b",
messages=[
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_message},
],
response_format={"type": "json_object"},
max_tokens=256,
temperature=0.7,
)
raw = response.choices[0].message.content
return json.loads(raw)
Step 5: Wire it into a test loop
Before I expose this to the mobile client, I run a local CLI loop to verify pacing and consistency. This script stands in for the Unity or Godot frontend.
if __name__ == "__main__":
print("=== Ironvale Narrative Test ===")
while True:
try:
player_input = input("\nPlayer action: ")
if player_input.lower() in ["quit", "exit"]:
break
result = generate_narrative(player_input)
print(f"\nHarn ({result['emotion']}): {result['npc_text']}")
if result.get("quest_update"):
print(f"[Quest Update] {result['quest_update']}")
except Exception as e:
print(f"Error: {e}")
Run it
Save the full script as quest_engine.py, export your key, and run it.
export OXLO_API_KEY="sk-oxlo.ai-..."
python quest_engine.py
Example session:
Player action: I found your star-metal in a bandit camp.
Harn (hopeful): You actually found it? I thought it was lost to the mud. Bring it to the forge and I will craft you something worthy.
[Quest Update] Return star-metal to Harn.
Player action: I want 500 gold now.
Harn (angry): I am a blacksmith, not a vault. You get paid when the job is done.
Next steps
Wire this generator behind a FastAPI endpoint so your Unity or Godot client can POST player actions and receive JSON dialogue payloads. If you are shipping in multiple languages, swap the model to qwen-3-32b for strong multilingual reasoning without changing any other code. For details on request-based pricing and how it compares to token-based billing for long context like world lore, see https://oxlo.ai/pricing.
Top comments (0)