Conversation engine
Spider
Bubble
A Turkish bath house with Shahmaran, Erlik, Ay Dede and Alara sitting in the steam, built in the ten days between the 20th and the 30th of January 2025.
I wrote the talking twice: first as a Python generator that assembles conversations out of pools of phrases, then as a port of it into C#. On the last day I sat down and wrote the four characters by hand instead, one word underlined in every answer for the player to take away, and their files carry no responses block, which is the one thing the generated half of the loader is gated on. The pools in data.json holda great many distinct three-line conversations and nobody ever asked for one of them, so they are all still sitting in the file unsaid.
5 scenario files2,237 lines of my C#32 underlined words2 of them match nothing
turn it β
I Β· the crank
Spin any phrase and watch which line of which array it lands on.
This is generate-interactive.py rebuilt in the browser and pointed at the real pools, which are being fetched as you read. Choose a file along the top:data.json is the Python engine's own, example_scenario.json is the port of it that went into Unity and the four named ones are the characters I wrote by hand and never wired a generator into. Click any phrase to spin that slot on its own, or click the console and use the keys the script prints across the top of the terminal.
The Python engineβs own pools. Four objects, all three rungs and the only file with a top-level speculations array.
- this conversation is one of
- β
- phrases in this file's pools
- β
- bytes your browser fetched
- β
the roll nothing committed yet
The two scripts differ in one thing
generate-random.py picks the third line from any of the eight action items in the file without caring what the second line was about, andgenerate-interactive.py reads left_items orright_items off the outcome you chose and offers only those. Flip the switch and the total moves from β to β, because the outcomes in data.json never name three of the eight.
Where the third clause went
The Python template has three parts and the loader's has two, so switching the template above drops the third clause while you watch and divides the count by exactly fifteen. Nothing in my C# ever reads mystical_questions, though the field is declared at ConversationModels.cs:82 and every one of the six files carries fifteen of them per object.
return data["question_structure"].replace(
"[observation]", observation
).replace(
"[cause]", cause
).replace(
"[mystical_question]", mystical_question
), object_keyII Β· the gate
The loader looks for a colon with nothing after it.
LoadConversationFromJson finds the parts of the file it needs by searching the raw text for exact strings, and three of the strings it looks for have no space after the colon while every file on disk is pretty-printed with one. Press the probe and watchIndexOf run against the actual bytes of whichever file you pick.
int structureStart = jsonString.IndexOf("\"question_structure\":\"") + 21;
int structureEnd = jsonString.IndexOf("\"", structureStart);
if (structureStart >= 21 && structureEnd != -1) { β¦ }
else jsonData.question_structure = "[observation] Is it because [cause]";int start = jsonString.IndexOf($"\"{arrayName}\":[") + arrayName.Length + 4;
if (start < arrayName.Length + 4 || end == -1) return new string[0];The branch that builds generated questions
if (jsonData.responses.outcomes.items != null && jsonData.responses.outcomes.items.Count > 0)
Four of the five scenario files have no responses key at all, onlyconversations, objects and actions. The dictionary comes back empty, the count is zero and the branch is stepped over, so what Shahmaran, Erlik, Ay Dede and Alara offer you is exactly the eight questions I typed.
The rung with no case
The third rung is written and reachable from nothing. LoadActionOptions at line 316, ExecuteAction at 359 and AddResponseOption at 876 have no call site anywhere under Assets/GGJ2025, and the switch inOnClickConversation carries three cases:
case "response": // line 118 case "simple": // line 124 case "question": // line 129 default: // logs "Unknown option type"
There is no case for "action", and the R2 rung on the console above is running that method's logic anyway, which is the only reason you can read a line of it. The C# and the Python build the same sentence out of the same two arrays:
string actionText = $"Maybe you should {action} {itemData.name}, {result}";
return f"Maybe you should {action} {item_data['name']}, {result}"One underline where it does not belong
When the pools were copied into Unity as example_scenario.json, all sixty observations gained a capital letter, because the loader's template drops theTell me, why off the front and the phrase has to start the sentence. Fourteen of the fifteen acknowledgements were raised the same way, and the one left alone already began with an I. One other edit went in: the seventh cause under water_systemarrived carrying the game's own markup.
CreateDialogOption runs the inventory regex across every label it builds, so a generated question drawing that cause would try to pick up an object calledhosting. No such item exists among the thirty-three, so the lookup returns nothing and the game writes its warning to the log.
III Β· the words
Thirty-two words I could take out of people's mouths.
This half of it shipped: click the underlined word in any answer and the regex lifts it off the sentence, looks it up by label in the inventory manager and drops it on the shelf as an object. Two of the thirty-two match no item, so they stay where they are and the game writes a warning to the log instead.
The lookup is on the literal word
var matches = System.Text.RegularExpressions.Regex.Matches(text, @"<u>(.*?)</u>"); AC.InvItem itemToAdd = AC.KickStarter.inventoryManager.GetItem(itemName); AC.KickStarter.runtimeInventory.Add(itemToAdd.id);
GetItem matches on item.label == _name, so I filled the manager with the underlined words themselves and thirty-one of its thirty-three entries carry one as a label: perch, crevices, pleassse, silly,the. Two of Alara's, help and water, have no entry, so those lookups return null and log
Could not find item '{itemName}' in inventory managerTwo of the thirty-three are both labelled move, at id 6 and id 22; one more is a leftover called Inventory item 1 at id 0. The recipe wants id 6.
You cannot make a door out of one person
Neither live recipe can be filled out of one mouth. unlock is Shahmaran's androom is Erlik's, and both of them sit in Outside; move is Ay Dede's and door is Alara's, and both of them sit in Hammam. Each pair fuses to the item labelled key at id 5 and fires an action list that turns the crafting menu off and switches scene.
- 0Start
- 1Outside
- 2Hammam
- 3Exit
- 4Loading
Build order out of EditorBuildSettings.asset.Recipe_Door_OnCreate switches to 2 and Recipe_Door2_OnCreate to 3, so the first key takes you into Hammam and the second takes you out toExit. Crafting_OnFailRecipe holds one action, a pause, with nothing after it. The third recipe asks for item ids 1 and 3, which no item in the manager carries.
IV Β· the frozen roll
Every question in the inspections is a line out of data.json.
hammam_mystery.twee sits in the root of the repo, a Harlowe story with the ifid HAMMAM-MYSTERY-2025 and fourteen passages, each carrying the coordinate the editor left it at. Seven repeat the snake's conversation as it sits inexample_scenario.json and four are inspections whose three question lines are single entries in data.json. Walk into one and every line is traced back to the pool it came out of.
β
int numOptionsToShow = 8; // Always show 3 of each option, regardless of combinations count
The comment says three and the number says eight; the prefab agrees with the number: it carries eight buttons,
btnOption1andbtnOption1 (1)through(7).
Ten days
- 20 Janf4d4bfcInitial commit
- 22 Jan8903674wip(chat-engine): initial prototype for simple dynamic story generation
- 24 Jan6659e2cfeat(chat-engine): add conversation system and JSON-based dialogue options
- 24 Janba16942fix(chat-engine): fixed order of stages
- 26 Jan939d852final commit
- 26 Jan1999a60build
- 28 Jan220a2acfix(everything): it works!
- 30 Janfd9a14bexperiment(bubbles): playing with bubbles
- 30 Jan34c91aerefactor(ui): adjust conversation UI layout and parameters
These are nine of the twenty-two commits with the dates and messages verbatim: the generator lands on the 22nd, the port on the 24th and all four hand-written characters arrive together on the 26th in the one called final commit
.
My C#, on top of Adventure Creator 1.82.4
- Logic/ConversationFromFile.cs970the loader, the regex, the three rungs
- Actions/ActionConversationFromFile.cs380the Adventure Creator action that starts one
- Effects/BubbleFloat.cs248drift, wobble, spawn radius, lifetime
- Models/ConversationModels.cs243the JSON shapes and two hand-rolled dictionary parsers
- UI/DialogOptionHeightAdjuster.cs146squares every option button around its text
- UI/VirtualizedConversationUI.cs111pooling for the option buttons
- Utils/JsonParser.cs49FindMatchingBrace, which is what the parsers stand on
- Systems/ConversationSystem.cs39an interface the loader declares at line 48 and never calls
- UI/ButtonToFront.cs31hover raises a bubble above its neighbours
- Actions/LeaveConversation.cs20one button, one call, EndConversation
- ten files2,237lines, counted with wc
What is in the build




The two backgrounds are painted at 2743 by 1351 and both of them are up there behind this page. Nothing in the repo records who painted any of it, so I'm not going to say, and nothing in the repo states the jam theme either. ProjectSettings.assetgives the company as Spider Bubbles, the product as GGJ2025 and the version as 0.1.0, and the Mac build left a folder on my machine naming the app it went out asThe Bath House.app
.
The five scenario files, data.json and hammam_mystery.twee ship with this page byte for byte and are fetched and parsed in your browser, so every phrase, index and count above is the file's own. The item and recipe table is extracted rather than quoted, out of 1,890 lines of Unity YAML, and it says so in its own _note field. Where I could not verify something, it is not here.
Spider Bubble Β· Global Game Jam 2025 Β· DR McCulloch
β back home
