Inspiration
How to Build a Generative Solo Journaling RPG Engine Using Claude and Python
Solo journaling roleplaying games are a massive trend. Discover how to build a local Python engine that uses Claude 3.5 Sonnet as an intelligent, fair-playing 'Oracle' to drive procedural narrative adventures.
Updated 9/8/2026
The Mechanics of the Solitary Chronicle
Solo journaling RPGs—like Thousand Year Old Vampire or Apothecaria—are fascinating creative exercises. You roll dice, draw cards, consult a table of prompts, and then write down your character's response in a physical or digital journal. It is a slow, deeply personal form of interactive storytelling.
But what if you want to play a game where the world reacts to your journal entries in real-time, without relying on static lookup tables?
By building a lightweight Python engine and using Claude as your generative "Oracle," you can create a dynamic, self-sustaining journaling game. In this setup, Python manages the rigid mechanical state (your health, inventory, and location), while Claude reads your handwritten journal entries, evaluates your performance, determines the consequences, and generates the next narrative prompt.
We aren't talking about a simple chatbot that ticks a list of generic tropes. We are building a structured state-machine that treats the LLM as a systemic game master.
The Architecture of the Engine
To build this, we need a clear separation of concerns.
- The Python Controller: Tracks variables (e.g.,
health,gold,corruption_level,inventory), saves your journal history to JSON, and handles the loop. - The Oracle (Claude): Receives the current game state, the player’s latest journal entry, and outputs only structured JSON containing the narrative outcome, the updated inventory, and the next day's prompt.
Using structured outputs prevents the LLM from hallucinating new rules or ignoring your inventory limits. If you need a refresher on how to enforce strict JSON schemas with API calls, explore our glossary.
Step 1: Setting Up the Game State
Let’s write the basic Python schema for our game state. We will save this as a local JSON file so your adventure can persist across multiple sessions.
`python
game_state = {
"character_name": "Alistair",
"stats": {
"resolve": 10,
"supplies": 5,
"dread": 0
},
"inventory": ["rusty lantern", "tarnished compass"],
"current_day": 1,
"journal_history": [],
"last_prompt": "You stand at the edge of the Whispering Mire. The fog is thick enough to chew. Why did you choose this godforsaken path?"
}
`
Step 2: Designing the Oracle Prompt
To make this work, Claude needs to act as a fair referee. It must read the player's journal entry, evaluate how they dealt with the last_prompt, change the character's stats based on the narrative, and supply a new prompt.
We will use a system prompt that forces Claude to return a JSON object with a highly specific structure. You can refine this template using our interactive prompt generator:
`text
You are the Oracle for a grim solo journaling RPG set in a decaying clockwork city.
Your task is to evaluate the player's journal entry and update the game state.
Review the player's action: 1. If they attempted something risky without adequate tools in their inventory, penalise their 'resolve' or 'supplies'. 2. If they wrote a compelling, logical solution, reward them or keep their stats stable. 3. Increase 'dread' if they linger too long in dangerous areas.
You must respond ONLY with a raw JSON object matching this schema:
{
"narrative_reaction": "A brief 3-sentence description of the immediate environmental consequence of their action.",
"stat_changes": {
"resolve": -1, // integer adjustment
"supplies": 0,
"dread": 1
},
"inventory_changes": {
"added": [],
"removed": []
},
"next_prompt": "The prompt for the next day's journal entry."
}
`
Step 3: Writing the Game Loop in Python
Now, we tie it all together with a Python script. This script loads your current game state, prints the prompt, waits for you to write your journal entry, packages everything up for the Claude API, and saves the updated state.
`python
import json
import os
import anthropic
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
def run_game_turn(player_journal_entry): # Load current state with open("save_game.json", "r") as f: state = json.load(f) # Construct the payload for Claude prompt = f""" Current Game State: {json.dumps(state, indent=2)} Player's Journal Entry for Day {state['current_day']}: \"{player_journal_entry}\" Process this turn and return the JSON response. """ try: response = client.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=1000, temperature=0.7, system="System instructions for the Oracle go here...", messages=[{"role": "user", "content": prompt}] ) turn_data = json.loads(response.content[0].text) # Update state based on Oracle decision state["stats"]["resolve"] += turn_data["stat_changes"]["resolve"] state["stats"]["supplies"] += turn_data["stat_changes"]["supplies"] state["stats"]["dread"] += turn_data["stat_changes"]["dread"] for item in turn_data["inventory_changes"]["added"]: state["inventory"].append(item) for item in turn_data["inventory_changes"]["removed"]: if item in state["inventory"]: state["inventory"].remove(item) state["journal_history"].append({ "day": state["current_day"], "prompt": state["last_prompt"], "entry": player_journal_entry, "outcome": turn_data["narrative_reaction"] }) state["current_day"] += 1 state["last_prompt"] = turn_data["next_prompt"] # Save updated state with open("save_game.json", "w") as f: json.dump(state, f, indent=2) print(f"\nOracle Reaction: {turn_data['narrative_reaction']}") print(f"New Prompt: {turn_data['next_prompt']}") except Exception as e: print(f"An error occurred. Check your API configuration: {e}") # For API troubleshooting, consult https://claude-support.com
Quick console wrapper to run your turn if __name__ == "__main__": # Assumes save_game.json already exists in your folder with open("save_game.json", "r") as f: current_state = json.load(f) print(f"\n--- DAY {current_state['current_day']} ---") print(current_state["last_prompt"]) entry = input("\nWrite your journal entry: ") run_game_turn(entry) ```
Designing Your Own Rulesets
The beauty of this architecture is its total flexibility. You aren't limited to fantasy or gothic horror. You can swap out the prompts and stats to build a sci-fi salvager game where you track hull_integrity and oxygen, or a cozy magic-shop simulator where you track reputation and herbal_stock.
Because Claude acts as an interpreter, it reads between the lines of your writing. If you write a beautifully poetic entry about your character crying over their broken pocket watch, Claude can pick up on that emotional context and lower your resolve without you needing to manually trigger a penalty. It makes the game feel incredibly organic, responsive, and wonderfully eerie.
Keep going
Build something with the prompt generator, decode the jargon in the glossary, or compare the tools on our platform deep-dives.