Decalove
A visual novel where the story is written for you, not just played by you.
Inspiration
Every visual novel player has had the same moment: you're deep in a route, emotionally invested, and then the game gives you three choices — none of which say what you actually want to say. You're locked into the writer's decision tree, and the character you care about responds to a script, not to you.
We wanted to break that. What if a visual novel could listen to what the player actually wants to do — in their own words — and write a scene that responds to it? Not a chatbot wearing a VN skin, but a real visual novel with pacing, tension arcs, relationship mechanics, and proper endings — where the prose between decision points is generated by AI but the player never notices the seams.
The idea was simple: keep everything players love about visual novels (characters with depth, slow-burn relationships, satisfying endings) and remove the one thing that limits them (a fixed script).
What it does
Decalove is a Ren'Py visual novel set in a high school slice-of-life world with four fully realized characters: Aiko (the overachieving class rep hiding exhaustion behind composure), Ren (the art club president who deflects vulnerability with humor), Mika (the track star hiding a knee injury), and Haruto (the deadpan library aide who's kinder than he'll ever admit).
The player reads dialogue, picks choices, and watches relationships evolve — just like any visual novel. But underneath, every scene is being written in real time by an AI story engine. The player can also bypass the choice menu entirely and type whatever they want to do in natural language ("I grab Mika's wrist and check if she's actually okay", "I leave without saying anything"). The engine interprets the intent, checks it against the current relationship state, and writes a scene that fits.
Key mechanics:
- Multi-axis relationships — affection, trust, respect, friendship, romance, familiarity, anger, and jealousy are all tracked independently per character.
- Dynamic character stances — teasing Aiko at affection 60 is a playful argument; at affection 20 it costs you trust. Same input, different scene.
- A 300+ step story arc across five narrative phases (prologue → first weeks → festival → summer → resolution) before any ending can trigger.
- Growth-based endings — who you end up with (or whether you end up alone) is determined by how far you moved a character relative to where they started, not by hitting an absolute threshold. Spread yourself thin and you finish alone.
- Zero loading screens — while the player reads the current scene, the next batch of story beats generates in the background. If the queue is briefly empty, the game shows atmospheric ambient narration ("The wind moves through the fence and keeps going") instead of a spinner.
How we built it
The project is split into two halves that never cross responsibilities:
Ren'Py Client (game/) — A stateless front-end that handles presentation, dialogue rendering, choice screens, free-text input, and dynamic gradient-based backgrounds (no static art assets needed). It communicates with the backend over REST using renpy.fetch and works on desktop, mobile, and web.
FastAPI Backend (api/) — A Python story engine running on FastAPI + Uvicorn with a multi-agent architecture:
- Director Agent — Deterministically plans each scene's shape: pacing (
quiet→building→charged→release), tension level, which character carries the scene, and whether the player's action is allowed to fail. No LLM involved here — pure game logic. - Memory Agent — Retrieves relevant episodic memories using a hybrid score (60% semantic relevance via embeddings + 30% importance + 10% recency) so characters remember what happened to them.
- Narrative Agent — Sends the Director's plan, character stances, memories, and player action to Google Gemini 3.7 Flash via OpenRouter with strict JSON Schema enforcement, producing a batch of story beats.
- Validation Agent — Enforces hard rules: strips any text where the AI wrote the player's actions, clamps relationship deltas to ±5 per step, corrects impossible location teleports, and guarantees 3–5 meaningful choices at every decision point. Strategy: repair first, truncate only if repair fails.
- Visual Agent — Generates scene backgrounds and character art via Gemini 3.1 Flash Image (cloud) or Stable Diffusion XL (local GPU), with content-addressable caching so identical scenes never regenerate.
A critical design decision: the engine owns the state, not the model. The LLM proposes relationship changes; the backend validates, clamps, and commits them — and only when a step is actually delivered to the player. A generated run nobody read changes nothing.
Persistence uses MongoDB (via Motor async driver) for sessions and story ledgers, and MinIO (S3-compatible) for generated art assets. But the entire stack gracefully degrades: without Docker, saves live in memory and art lands on the local filesystem. Without an API key, a Scripted Narrator takes over with authored template prose and character-specific rebuff banks — the game is fully playable offline, just not AI-written.
Challenges we ran into
Keeping the AI from writing the player's lines. The biggest danger in an AI-driven VN is the model deciding what the player does next. Our solution: every generated batch stops at the first moment requiring a player decision. The Validator then strips any second-person agency verbs that slipped through. The player always has the last word.
Preventing narrative derailment. Left unconstrained, an LLM will escalate drama to absurd levels or resolve tension too quickly. The Director solves this by computing pacing mathematically from game state — a charged scene is always followed by a release — and the 300-step floor gate prevents premature endings.
Making free-text input feel fair. When a player types "I confess my love to Aiko" on step 3 with affection at 12, the game can't just comply. The Director classifies the intent, checks it against relationship thresholds and character stances, and writes a scene where it plausibly fails — Aiko's guard goes up, not down. The game says yes to every input but doesn't promise it'll go well.
Hiding latency. LLM generation takes seconds. We solved this with a prefetch queue (generate the next batch while the player reads the current one) and ambient narration as a fallback — atmospheric lines that feel authored, not like loading indicators.
Ending fairness across characters. Characters start at wildly different baselines (Haruto begins at affection 5; Mika at friendship 30). Absolute thresholds would make some routes trivial and others impossible. Growth-relative scoring — measuring how far you moved someone from where they started — solved this elegantly.
Accomplishments that we're proud of
- 425 tests, zero external dependencies required. The entire test suite runs offline with no API key, no Docker, and no Ren'Py SDK. Integration tests auto-skip when services aren't available.
- A full simulated 300+ step playthrough test (
test_long_playthrough.py) that validates the entire narrative arc over HTTP — from prologue to ending. - Seamless degradation. Remove the API key: scripted narrator. Remove Docker: in-memory storage. Remove the GPU: placeholder art. The game always runs.
- The "thawing" mechanic. If a character's trust is low but familiarity is high (you've spent time together), their guard becomes habit rather than judgment — and time alone starts unlocking trust. This prevents relationship deadlocks and feels emotionally real.
- It actually feels like a visual novel. Despite being AI-generated, the pacing, decision points, and relationship arcs feel hand-authored. The seams don't show.
What we learned
- Structured output is non-negotiable for game engines. Free-form LLM text is unusable for a system that needs to parse character names, relationship deltas, and choice menus. JSON Schema enforcement via Gemini's structured output mode was the single most important technical decision.
- Deterministic planning before generation saves everything. Having the Director compute the scene's shape before the LLM writes a single word means the model can't derail the pacing, and the same Director logic works identically with or without an API key.
- Validation is a feature, not a safety net. The Validator isn't just catching errors — it's actively repairing outputs (renumbering choices, topping up menus, clamping deltas) so the game maintains quality even when the model is imperfect.
- State ownership matters more than prompt engineering. The decision to have the engine commit state changes only on delivery — not on generation — eliminated an entire class of consistency bugs around speculative generation and abandoned branches.
- Players will type things you never imagined. Free-text input is a Pandora's box. Intent classification into bounded action families, paired with relationship-aware outcome planning, was the only way to make it work without the game breaking.
What's next for Decalove
- Voice synthesis — Generate character voices in real time to match the AI-written dialogue, with per-character voice profiles.
- Player-uploaded character art — Let players provide their own character designs and have the engine adapt generated scenes to match.
- Multiplayer shared worlds — Multiple players in the same school, with characters remembering interactions across different players' timelines.
- Community story packs — Let creators define their own worlds, characters, and relationship mechanics using the engine as a platform, not just a single game.
- Mobile release — The Ren'Py client already supports mobile and web exports; the backend just needs a hosted deployment with proper scaling.
Built With
- agent
- ai
- fastapi
- gemma
Log in or sign up for Devpost to join the conversation.