The House That Remembers A developer built The House That Remembers, a text-based D&D-style adventure game inspired by Edgar Allan Poe's The Fall of the House of Usher, submitted to the Hacktoberfest Open-Source AI Challenge Week 4. Rather than letting the model act as an open-ended Dungeon Master, the developer moved game logic into a deterministic engine that tracks room, inventory, clues and turn count across five story stages, leaving the Gemma model to handle narration only. Each turn re-sends a system prompt, stored memory, conversation history and the new user message to work around the model's lack of retained context between independent API calls. This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine https://dev.to/challenges/hacktoberfest-week4-2026-10-26 Because I love the spooky season, D&D, and reading twisty thrillers 😅, I decided to build a short D&D-style game. Meet The House That Remembers, a text-based adventure inspired by Edgar Allan Poe’s 1839 short story The Fall of the House of Usher. The letter arrived three nights ago, its paper creased and stained, the handwriting unmistakably your friend's - though the words were unlike anything you had ever known him to write. "Please, you must come. There are things in this house I can no longer explain. I'm afraid it may be too late for me. You're the only person I can trust." Now, you stand before his home. Rain lashes against your face, and thunder rolls across the darkened sky. The house looms above you, its crooked silhouette barely visible through the mist. Tall, narrow windows stare down like hollow eyes. Ivy claws at the weathered stone walls, and the iron gates behind you groan in the wind, slowly swinging shut. You remember this house from your childhood, but something about it feels wrong. You used to play outside, spend time helping your friend with chores in the kitchen, explore the family library, and even sleep over in the guest room. The air is unnaturally cold, carrying the scent of wet earth and rotting leaves. Though no lights burn inside, you cannot shake the feeling that someone is watching you from the upper windows. For a moment, lightning illuminates the facade - and you could swear a shadow has just moved behind the glass. Then the thunder cracks. The front door stands slightly ajar. Your friend invited you here for help. But as you stare into the darkness beyond the threshold, a troubling thought creeps into your mind: Did he invite you into the house… or did the house invite you in? The online demo runs on Render. Responses may take a little longer because the model is running on CPU. This is my submission for the Hacktoberfest Open-Source AI Challenge: Week 4. This week's theme is "Ghost in the Machine": build something spooky, or something that honors the past in the spirit of Día de los Muertos, using open-weight models or open-source AI. Because I love the spooky season, D&D, and reading crazy thrillers 😅, I decided to build a short D&D-style game. I call it The House That Remembers , and it's inspired by Edgar Allan Poe's 1839 story The Fall of the House of Usher . The story folder contains major spoilers, so if you want to enjoy the game, I recommend avoiding those files. Tech stack: app.py - UI and chat interface Trigger warning… My initial idea was simple: write the story, give it to the model as one big prompt, and let the AI act as the Dungeon Master DM or narrator. I quickly discovered that this approach wasn't reliable. The story could get stuck, and progression and pacing were inconsistent. Giving the model more freedom didn't necessarily make the experience better. So I started to define some conditions and states that had to be reached, and eventually my whole approach changed. I moved most of the logic to the engine, and let the model focus on narration. I divided the story into 5 stages, each stage with its own progression conditions. The engine tracks the game state - the room, inventory, clues, number of turns... game state is then used by the engine and the model. However, another thing emerged. Like a chatbot, the narrator needs context: what happened earlier, what the player discovered, and what the characters already know. Gemma doesn't automatically retain conversation history between independent API calls. The application has to provide the relevant context with each request. Conceptually, each turn combines: system prompt + stored memory + conversation history + new user message The model "remembers" because you'd re-send everything each time, but context windows are finite. RAG-style memory is often a popular choice, but for this relatively small project, I decided to go with something simpler, and in the end built a combination of " rolling summary " - have the model summarize older turns into a compact paragraph, then keep that summary plus recent messages verbatim - and " fact-based memory " - this is the story, facts, the rules, stored in key-value style, then inject the relevant ones into the system prompt. I built prompt.py which essentially puts together the prompt for the model each turn. Layers top to bottom : This layered approach gave me the best consistency. There is a trade-off, of course. The more tightly you constrain the narrator, the less freedom it has to improvise. Depending on the player's choices, the experience can sometimes feel a little restrictive. But for this game, I found that reliable story progression was more important. There are 4 endings in total, each triggered when specific conditions are met. That gives players a reason to revisit the house and discover what they missed. The engine also prevents the story from getting stuck indefinitely, and the player will always reach an ending. And yes, there's an Easter egg hidden in the story. Wink wink. Think of this as a tabletop role-playing game in text form. You play a character, describe what they do, and the AI acts as the DM: narrating events, maintaining the atmosphere, and playing the non-player characters NPCs . Describe your actions, think about your goal and what you hope to achieve. There is no right or wrong way to play this game. The narrator distinguishes spoken dialogue, physical actions, exploration, and questions about the player's memories. You can make the intent clear in natural language: The sidebar tracks the Past, Present, and Echoes fragments needed to finish the story. Two clues in each category complete a fragment; the house also holds atmospheric and optional lore discoveries that do not count toward fragment progress. Pay attention to what you discover. The details may matter later. The tech stack is intentionally simple: I tested both gemma4:e4b and gemma4:e2b . The smaller model was faster, but it struggled to narrate the story consistently, even when the game engine's conditions had been met. It was a useful reminder that model size involves trade-offs. A smaller model may be faster and lighter, but it doesn't necessarily follow a complex narrative as reliably. More prompt engineering might help, but for this project, I decided to stick with gemma4:e4b. There are two ways currently - locally and online. The local run has an advantage of having a private session, everything runs on your machine and after the initial download, it can run offline. For online experience - that was actually a challenge Deploying models is not as simple as it may seem. After some help and discussion with Changsu Seong https://dev.to/scs0209/what-happens-when-you-put-an-ai-in-a-ghost-story-32h2 thank you I managed to deploy the website and the model using the Render Hackathon Participant credits. The trade-off is performance. The model runs on CPU, so responses are slower than they would be on suitable local hardware. In my tests, responses took around 24 seconds on average, with longer waits after periods of inactivity. I'll keep the demo running until the end of Hacktoberfest or until the available credits run out, whichever comes first. 😅 By default, the app uses Ollama locally. Install Ollama and download the model: ollama pull gemma4:e4b Make sure the Ollama service is running. Then, from the repository root, install the project dependencies. You can optionally create and activate a virtual environment first: py -m venv .venv .\.venv\Scripts\Activate.ps1 python -m pip install -r requirements.txt Start the app with: python -m streamlit run .\the-house-that-remembers\app.py Streamlit prints a local URL in the terminal; open it in your browser to view the app. After the initial download, it can run on your machine offline. Deployed on Render: https://the-house-that-remembers.onrender.com/ https://the-house-that-remembers.onrender.com/ For me, one of the biggest benefits of open-weight models is the ability to run them locally. With Gemma and Ollama, the game can run entirely on a player's machine. Once the model is downloaded, no remote inference service is needed, and the conversation doesn't have to leave the computer. That privacy aspect is one of the things I find most appealing about local LLMs. Open models also make experimentation more accessible. I could build a small narrative game, test different model sizes, explore prompt engineering, and learn how much of the experience should be handled by the model versus ordinary application code. The online deployment also taught me something practical: running an open model is possible without owning dedicated hardware, but hosting costs and inference speed still matter. For a small project, finding the right balance between accessibility, privacy, reliability, and cost is part of the challenge. I used GitHub Copilot as my coding agent, especially for validating the story flow and debugging the game logic. One particularly useful contribution was a test engine that checked whether specific phrases triggered - or correctly didn't trigger - game-state updates. At one point, I couldn't reach a particular ending. Copilot helped me find a missing phrase condition that prevented the relevant state change from happening. That kind of testing was valuable because a narrative game can appear to work during casual play while still hiding progression bugs in less common paths. In Untitled-1.txt you can see results from the last game. It is a lot better, but there are still a few problems. { "skill": "python-fact-grounded-coding" } Skill "python-fact-grounded-coding" loaded successfully. Follow the instructions in the skill context. I’ll trace the recorded gameplay issues to the game engine, prompt, and story canon, then make targeted changes and validate them with focused tests. The gameplay state confirms the friend’s location and story clues are managed separately from the model’s narration, so I’m checking those boundaries and the existing tests before adjusting both. {"pattern":"^ def |STAGE RULES|BACK =|VERTICAL =|MOVE VERBS|TAKE VERBS |sister|friend|name|stage|ambient|inventory","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-rememb... Output too large to read at once 13.9 KB . Saved to: C:\Users\lucia\AppData\Local\Temp\copilot-tool-output-1793207778528-2.txt Consider using tools like rg for searching , head/tail for viewing start/end , view with view range for specific sections , or jq for JSON to examine portions of the output. Preview first 500 chars : C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:DOOR DENIAL MAX = 3 friend's denial must be at or below this to open the door C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py: "A figure emerges from the shadows. Your friend.\n" C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py: "ambient": True, C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-reme {"pattern":"^ def |CORE =|FRIEND EXAMPLES|STAGES = |sister|friend|Current room|EXITS|inventory|room","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py"... Output too large to read at once 8.9 KB . Saved to: C:\Users\lucia\AppData\Local\Temp\copilot-tool-output-1793207778584-3.txt Consider using tools like rg for searching , head/tail for viewing start/end , view with view range for specific sections , or jq for JSON to examine portions of the output. Preview first 500 chars : C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py: 2. story block premise, friend sheet, letter static per story C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py: 4. state block room, exits, inventory, clues changes per turn C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers". C:\Users {"pattern":"sister|friend|early behavior|personality|voice|letter text|Library Book|note|item|book","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\story\\cano... Output too large to read at once 10.9 KB . Saved to: C:\Users\lucia\AppData\Local\Temp\copilot-tool-output-1793207778634-4.txt Consider using tools like rg for searching , head/tail for viewing start/end , view with view range for specific sections , or jq for JSON to examine portions of the output. Preview first 500 chars : C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json: "summary": "The player receives a desperate letter from a childhood friend and travels to the house his family has lived in since he was a boy. The friend is pale, exhausted, frightened, and relieved to see the player. The house appears to remember a family history its surviving resident cannot face.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json: "player { "pattern": "class|unittest|pytest|def test", "paths": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers", "glob": " .py", "output mode": "files with matches", "head limit": 100 } C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py { "query": "Pylance MCP Python LSP workspace symbols diagnostics function references call hierarchy" } "pylance-mcp-server-pylanceLSP", "pylance-mcp-server-pylanceAnalyze", "pylance-mcp-server-pylanceDocuments", "pylance-mcp-server-pylanceImports", "pylance-mcp-server-pylanceTypeAuthoring" { "pattern": "^def |^BACK =|^VERTICAL =|^MOVE VERBS|^TAKE VERBS", "paths": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py", "output mode": "content", "-n": true } grep content: 23 matches across 1 file s under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers engine.py 23 match es : 238: BACK = { 245: VERTICAL = { 255: def new game : 281: def fragments complete game : 291: def exit labels game, room name : 304: MOVE VERBS = "go", "enter", "head", "walk", "move", "step", "climb", "ascend", 306: TAKE VERBS = "take", "grab", "pick", "pocket", "steal" 351: def mentions text, words : 355: def find rooms text : 359: def find feature text, room : 366: def end game, ending, detail : 371: def classify intent raw action, game : 395: def result outcome, detail, new clue=None, ambient=None, friend=None : 409: def examine game, room name, name, feat : 424: def next unnoticed game, room name : 431: def unfinished game, room name : 438: def path step game, start, goal fn : 457: def nudge game : 497: def try door game : 525: def confront game : 551: def resolve game, text : 690: def advance stage game : 704: def apply action game, raw action : { "pattern": "^def |^CORE =|^FRIEND EXAMPLES|^STAGES =", "paths": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py", "output mode": "content", "-n": true } C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:15:CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers". C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:31:FRIEND EXAMPLES = """ "You came. I... sorry. You came. Good." C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:36:STAGES = C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:90:def denial band denial : C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:98:def story block canon : C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:115:def stage block game : C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:135:def state block game : C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:155:def engine note game : C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:215:def build messages game, canon : C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:223:def generation options game : C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:232:def tidy text : C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:249:def maybe summarize game, model : {"pattern":"^ \" character dynamics|items|inventory|house history|revelation order \"|\"name\"|\"takeable\"|\"aliases\"|\"early behavior\"|\"friend\"|\"sister\"|\"letter text\"","paths":"C:\\Users\... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:57: "sister": { C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:61: "friend": { C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:79: "name": "Memory imprint", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:83: "name": "Distortion", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:87: "name": "Escalation", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:91: "name": "No free resolution", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:95: "name": "No omniscient player", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:100: "friend": { C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:103: "early behavior": "He may try to spend ordinary, comforting time with the player—reading, watching a film, or talking as old friends would.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:136: "letter text": "Dear friend,\n\nPlease, you must come. There are things in this house I can no longer explain. I fear it is too late for me. You are the only one I can trust.\n\nI don't know what you will find when you get here, but you must come. Please. If you care about me at all, you will come.\n\nI'll be waiting.\n\nYour friend,\nE.", {"method":"textDocument/documentSymbol","params":{"textDocument":{"uri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py"}},"workspaceRoot":"C:\\Users\\luc... {"name":"AMBIENT AFTER TURNS","kind":14,"range":{"start":{"line":10,"character":0},"end":{"line":10,"character":19}},"selectionRange":{"start":{"line":10,"character":0},"end":{"line":10,"character":19}},"children": },{"name":"AMBIENT COOLDOWN","kind":14,"range":{"start":{"line":11,"character":0},"end":{"line":11,"character":16}},"selectionRange":{"start":{"line":11,"character":0},"end":{"line":11,"character":16}},"children": },{"name":"FRAGMENT CLUES NEEDED","kind":14,"range":{"start":{"line":12,"character":0},"end":{"line":12,"character":21}},"selectionRange":{"start":{"line":12,"character":0},"end":{"line":12,"character":21}},"children": },{"name":"DOOR DENIAL MAX","kind":14,"range":{"start":{"line":13,"character":0},"end":{"line":13,"character":15}},"selectionRange":{"start":{"line":13,"character":0},"end":{"line":13,"character":15}},"children": },{"name":"PUSH LIMIT","kind":14,"range":{"start":{"line":14,"character":0},"end":{"line":14,"character":10}},"selectionRange":{"start":{"line":14,"character":0},"end":{"line":14,"character":10}},"children": },{"name":"SUPPORT LIMIT","kind":14,"range":{"start":{"line":15,"character":0},"end":{"line":15,"character":13}},"selectionRange":{"start":{"line":15,"character":0},"end":{"line":15,"character":13}},"children": },{"name":"MAX TURNS","kind":14,"range":{"start":{"line":16,"character":0},"end":{"line":16,"character":9}},"selectionRange":{"start":{"line":16,"character":0},"end":{"line":16,"character":9}},"children": },{"name":"NUDGE AFTER TURNS","kind":14,"range":{"start":{"line":17,"character":0},"end":{"line":17,"character":17}},"selectionRange":{"start":{"line":17,"character":0},"end":{"line":17,"character":17}},"children": },{"name":"NUDGE COOLDOWN","kind":14,"range":{"start":{"line":18,"character":0},"end":{"line":18,"character":14}},"selectionRange":{"start":{"line":18,"character":0},"end":{"line":18,"character":14}},"children": },{"name":"ROOMS","kind":14,"range":{"start":{"line":26,"character":0},"end":{"line":26,"character":5}},"selectionRange":{"start":{"line":26,"character":0},"end":{"line":26,"character":5}},"children": },{"name":"CLUE FRAGMENT","kind":14,"range":{"start":{"line":228,"character":0},"end":{"line":228,"character":13}},"selectionRange":{"start":{"line":228,"character":0},"end":{"line":228,"character":13}},"children": },{"name":"BACK","kind":14,"range":{"start":{"line":237,"character":0},"end":{"line":237,"character":4}},"selectionRange":{"start":{"line":237,"character":0},"end":{"line":237,"character":4}},"children": },{"name":"VERTICAL","kind":14,"range":{"start":{"line":244,"character":0},"end":{"line":244,"character":8}},"selectionRange":{"start":{"line":244,"character":0},"end":{"line":244,"character":8}},"children": },{"name":"new game","kind":12,"range":{"start":{"line":254,"character":0},"end":{"line":278,"character":5}},"selectionRange":{"start":{"line":254,"character":4},"end":{"line":254,"character":12}},"children": },{"name":"fragments complete","kind":12,"range":{"start":{"line":280,"character":0},"end":{"line":287,"character":71}},"selectionRange":{"start":{"line":280,"character":4},"end":{"line":280,"character":22}},"children": {"name":"game","kind":13,"range":{"start":{"line":280,"character":23},"end":{"line":280,"character":27}},"selectionRange":{"start":{"line":280,"character":23},"end":{"line":280,"character":27}},"children": },{"name":"counts","kind":13,"range":{"start":{"line":282,"character":4},"end":{"line":282,"character":10}},"selectionRange":{"start":{"line":282,"character":4},"end":{"line":282,"character":10}},"children": },{"name":"clue","kind":13,"range":{"start":{"line":283,"character":8},"end":{"line":283,"character":12}},"selectionRange":{"start":{"line":283,"character":8},"end":{"line":283,"character":12}},"children": },{"name":"frag","kind":13,"range":{"start":{"line":284,"character":8},"end":{"line":284,"character":12}},"selectionRange":{"start":{"line":284,"character":8},"end":{"line":284,"character":12}},"children": } },{"name":"exit labels","kind":12,"range":{"start":{"line":290,"character":0},"end":{"line":298,"character":17}},"selectionRange":{"start":{"line":290,"character":4},"end":{"line":290,"character":15}},"children": {"name":"game","kind":13,"range":{"start":{"line":290,"character":16},"end":{"line":290,"character":20}},"selectionRange":{"start":{"line":290,"character":16},"end":{"line":290,"character":20}},"children": },{"name":"room name","kind":13,"range":{"start":{"line":290,"character":22},"end":{"line":290,"character":31}},"selectionRange":{"start":{"line":290,"character":22},"end":{"line":290,"character":31}},"children": },{"name":"room","kind":13,"range":{"start":{"line":292,"character":4},"end":{"line":292,"character":8}},"selectionRange":{"start":{"line":292,"character":4},"end":{"line":292,"character":8}},"children": },{"name":"gated","kind":13,"range":{"start":{"line":293,"character":4},"end":{"line":293,"character":9}},"selectionRange":{"start":{"line":293," Truncated — full output 10.1 KB temporarily saved to C:\Users\lucia\AppData\Local\Temp\1793207825709-copilot-tool-output-d8ba9a49c6e342e7a0201fb51d7e0317.txt {"method":"textDocument/documentSymbol","params":{"textDocument":{"uri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py"}},"workspaceRoot":"C:\\Users\\luc... {"name":"CORE","kind":14,"range":{"start":{"line":14,"character":0},"end":{"line":14,"character":4}},"selectionRange":{"start":{"line":14,"character":0},"end":{"line":14,"character":4}},"children": },{"name":"FRIEND EXAMPLES","kind":14,"range":{"start":{"line":30,"character":0},"end":{"line":30,"character":15}},"selectionRange":{"start":{"line":30,"character":0},"end":{"line":30,"character":15}},"children": },{"name":"STAGES","kind":14,"range":{"start":{"line":35,"character":0},"end":{"line":35,"character":6}},"selectionRange":{"start":{"line":35,"character":0},"end":{"line":35,"character":6}},"children": },{"name":"ENDINGS","kind":14,"range":{"start":{"line":68,"character":0},"end":{"line":68,"character":7}},"selectionRange":{"start":{"line":68,"character":0},"end":{"line":68,"character":7}},"children": },{"name":"denial band","kind":12,"range":{"start":{"line":89,"character":0},"end":{"line":94,"character":76}},"selectionRange":{"start":{"line":89,"character":4},"end":{"line":89,"character":15}},"children": {"name":"denial","kind":13,"range":{"start":{"line":89,"character":16},"end":{"line":89,"character":22}},"selectionRange":{"start":{"line":89,"character":16},"end":{"line":89,"character":22}},"children": } },{"name":"story block","kind":12,"range":{"start":{"line":97,"character":0},"end":{"line":111,"character":5}},"selectionRange":{"start":{"line":97,"character":4},"end":{"line":97,"character":15}},"children": {"name":"canon","kind":13,"range":{"start":{"line":97,"character":16},"end":{"line":97,"character":21}},"selectionRange":{"start":{"line":97,"character":16},"end":{"line":97,"character":21}},"children": },{"name":"f","kind":13,"range":{"start":{"line":98,"character":4},"end":{"line":98,"character":5}},"selectionRange":{"start":{"line":98,"character":4},"end":{"line":98,"character":5}},"children": } },{"name":"stage block","kind":12,"range":{"start":{"line":114,"character":0},"end":{"line":131,"character":5}},"selectionRange":{"start":{"line":114,"character":4},"end":{"line":114,"character":15}},"children": {"name":"game","kind":13,"range":{"start":{"line":114,"character":16},"end":{"line":114,"character":20}},"selectionRange":{"start":{"line":114,"character":16},"end":{"line":114,"character":20}},"children": },{"name":"text","kind":13,"range":{"start":{"line":116,"character":8},"end":{"line":116,"character":12}},"selectionRange":{"start":{"line":116,"character":8},"end":{"line":116,"character":12}},"children": },{"name":"s","kind":13,"range":{"start":{"line":124,"character":4},"end":{"line":124,"character":5}},"selectionRange":{"start":{"line":124,"character":4},"end":{"line":124,"character":5}},"children": } },{"name":"state block","kind":12,"range":{"start":{"line":134,"character":0},"end":{"line":151,"character":27}},"selectionRange":{"start":{"line":134,"character":4},"end":{"line":134,"character":15}},"children": {"name":"game","kind":13,"range":{"start":{"line":134,"character":16},"end":{"line":134,"character":20}},"selectionRange":{"start":{"line":134,"character":16},"end":{"line":134,"character":20}},"children": },{"name":"room","kind":13,"range":{"start":{"line":137,"character":4},"end":{"line":137,"character":8}},"selectionRange":{"start":{"line":137,"character":4},"end":{"line":137,"character":8}},"children": },{"name":"lines","kind":13,"range":{"start":{"line":138,"character":4},"end":{"line":138,"character":9}},"selectionRange":{"start":{"line":138,"character":4},"end":{"line":138,"character":9}},"children": } },{"name":" engine note","kind":12,"range":{"start":{"line":154,"character":0},"end":{"line":211,"character":44}},"selectionRange":{"start":{"line":154,"character":4},"end":{"line":154,"character":16}},"children": {"name":"game","kind":13,"range":{"start":{"line":154,"character":17},"end":{"line":154,"character":21}},"selectionRange":{"start":{"line":154,"character":17},"end":{"line":154,"character":21}},"children": },{"name":"r","kind":13,"range":{"start":{"line":155,"character":4},"end":{"line":155,"character":5}},"selectionRange":{"start":{"line":155,"character":4},"end":{"line":155,"character":5}},"children": },{"name":"note","kind":13,"range":{"start":{"line":158,"character":4},"end":{"line":158,"character":8}},"selectionRange":{"start":{"line":158,"character":4},"end":{"line":158,"character":8}},"children": },{"name":"intent reminders","kind":13,"range":{"start":{"line":175,"character":4},"end":{"line":175,"character":20}},"selectionRange":{"start":{"line":175,"character":4},"end":{"line":175,"character":20}},"children": },{"name":"reminder","kind":13,"range":{"start":{"line":204,"character":4},"end":{"line":204,"character":12}},"selectionRange":{"start":{"line":204,"character":4},"end":{"line":204,"character":12}},"children": } },{"name":"build messages","kind":12,"range":{"start":{"line":214,"character":0},"end":{"line":219,"character":57}},"selectionRange":{"start":{"line":214,"character":4},"end":{"line":214,"character":18}},"children": {"name":"ga Truncated — full output 9.0 KB temporarily saved to C:\Users\lucia\AppData\Local\Temp\1793207826594-copilot-tool-output-fdbd771d654246348cbf582d160a8c86.txt I found why these show up: movement rejects non-adjacent destinations, the prompt has no authoritative friend location or scheduled sister event, and takeable items are only added by the take command. I’m addressing those at the engine/prompt level, while keeping the book’s local-history reveal distinct from the friend’s family secret. Begin Patch Update File: the-house-that-remembers/engine.py @@ def new game : return { "room": "Entrance Hall", + "friend room": "Entrance Hall", "inventory": ... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/engine.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/engine.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/engine.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/engine.py @@ -255,6 +255,7 @@ def new game : return { "room": "Entrance Hall", + "friend room": "Entrance Hall", "inventory": "The Letter" , "clues": , "visited": "Entrance Hall" , @@ -271,6 +272,7 @@ "support given": 0, "door open": False, whether the parents' bedroom door is open "truth known": False, whether the player has learned the truth about the house + "sister seen": False, "ending": None, None or an ending id "last result": None, engine result for the current turn "history": {"role": "assistant", "content": "\"You came. I didn't think you would... Thank you.\"\n\n"} , @@ -304,6 +306,11 @@ MOVE VERBS = "go", "enter", "head", "walk", "move", "step", "climb", "ascend", "descend", "return", "run", "visit", "follow", "proceed" TAKE VERBS = "take", "grab", "pick", "pocket", "steal" +FRIEND FOLLOWS RE = re.compile + r"\b ?:with me|with us|together|follow ?:s|ing ? me|follow him|" + r"come with me|walk with ?:him|my friend |lead him|guide me|" + r"wait for him to follow \b" + LEAVE RE = re.compile r"\b leave|exit|flee|escape \b. \b house|mansion|manor \b" @@ -435,25 +442,31 @@ return room name not in game "visited" or next unnoticed game, room name is not None -def path step game, start, goal fn : - """Breadth-first search over open exits. Returns first step, goal room or None.""" +def path to game, start, goal fn : + """Breadth-first search over open exits. Returns the route and goal room.""" from collections import deque seen = {start} - queue = deque start, None + queue = deque start, while queue: - room, first = queue.popleft + room, path = queue.popleft for nxt in ROOMS room "exits" : gate = ROOMS room .get "gated", {} .get nxt if gate and not game gate or nxt in seen: continue seen.add nxt - step = first or nxt + route = path, nxt if goal fn nxt : - return step, nxt - queue.append nxt, step + return route, nxt + queue.append nxt, route return None - - + + +def path step game, start, goal fn : + """Return the first room on the shortest open route to a goal.""" + hit = path to game, start, goal fn + return hit 0 0 , hit 1 if hit else None + + def nudge game : """Hint for steering a lingering player onward. None if there is nowhere to send them.""" room name = game "room" @@ -591,30 +604,36 @@ elif re.search r"\b down|downstairs \b", text and room name, "down" in VERTICAL: target = VERTICAL room name, "down" if target: - if target in room "exits" : - flag = room.get "gated", {} .get target - if flag and not game flag : - return result - "blocked", - f"The door to the {target} is shut and locked. It will not simply open; " - "the player would have to try the door itself.", - - first time = target not in game "visited" + route = path to game, room name, lambda r: r == target + if route: + path, destination = route + first time rooms = + for step in path: + if step not in game "visited" : + game "visited" .append step + first time rooms.append step + if step == "Parents' Bedroom": + game "truth known" = True + game "denial" = min game "denial" , 1 game "room" = target game "room turns" = 0 game "nudges here" = 0 - if first time: - game "visited" .append target - if target == "Parents' Bedroom": - game "truth known" = True - game "denial" = min game "denial" , 1 detail = f"The player moved to the {target}." - if first time: - detail += " It is their first time in this room." + if len path 1: + detail += f" They passed through {', '.join path :-1 } on the way." + if first time rooms: + detail += + f" First visit to: {', '.join first time rooms }." + + if FRIEND FOLLOWS RE.search text and game "friend room" == room name: + game "friend room" = target + detail += " The friend accompanied the player." + elif game "friend room" = target: + detail += f" The friend remains in the {game 'friend room' }." return result "moved", detail return result "blocked", - f"The {target} cannot be reached directly from the {room name}. " + f"The {target} cannot be reached from the {room name}. " f"Reachable from here: {', '.join exit labels game, room name }.", if room name in rooms named: @@ -640,6 +659,10 @@ : name, feat = found noticed, new clue = examine game, room name, name, feat + item = feat.get "takeable" + if item and item not in game "inventory" : + game "inventory" .append item + noticed += f" The {item} is added to the player's inventory." return result "examined", f"The player examined the {name}. They notice: {noticed}", @@ -780,6 +803,13 @@ for in result "new clue" .split "; " : game "denial" = max 0, game "denial" - 1 result "stage advanced" = advance stage game + if game "stage" = 3 and not game "sister seen" and not game "ending" : + game "sister seen" = True + result "sister" = + "A faint figure resolves into the friend's sister. She appears briefly " + "and says, 'You shouldn't be here. Please. It's safer if you go.' " + "Then she is gone." + game "last result" = result print "Game state:" Begin Patch Update File: the-house-that-remembers/engine.py @@ - "keywords": "book", "history", "pamphlet", "volume", "reading table" , + "keywords": "book", "h... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/engine.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/engine.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/engine.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/engine.py @@ -93,7 +93,7 @@ "history": { "ambient": True, - "keywords": "book", "history", "pamphlet", "volume", "reading table" , + "keywords": "book", "history", "pamphlet", "volume", "reading table", "page", "passage", "text" , "takeable": "Library Book", "looks": "The missing book lies open on a reading table: a water-stained local history. One passage tells of the house's first owner, who buried his sister before she was truly gone and died soon after. The house stood empty for years before anyone came.", Begin Patch Add File: the-house-that-remembers/tests/test engine.py +import contextlib +import io +import json +import unittest +from pathlib import Path + +import engine +from prompt import s... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/tests/test engine.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/tests/test engine.py create file mode 100644 index 0000000..0000000 --- a/dev/null +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/tests/test engine.py @@ -1,0 +1,85 @@ +import contextlib +import io +import json +import unittest +from pathlib import Path + +import engine +from prompt import state block, story block + + +CANON = json.loads + Path file .resolve .parents 1 / "story" / "canon.json" .read text + encoding="utf-8" + + + + +def act game, action : + with contextlib.redirect stdout io.StringIO : + return engine.apply action game, action + + +class GameEngineTests unittest.TestCase : + def test routes to non adjacent room and keeps friend in place self : + game = engine.new game + game "room" = "Library" + game "friend room" = "Library" + game "visited" .append "Library" + + result = act game, "go to the upper landing" + + self.assertEqual result "outcome" , "moved" + self.assertEqual game "room" , "Upper Landing" + self.assertIn "Grand Staircase", game "visited" + self.assertEqual game "friend room" , "Library" + self.assertIn "The friend remains in the Library.", result "detail" + self.assertIn "Friend location: Library.", state block game + + def test friend can accompany player when invited self : + game = engine.new game + game "room" = "Library" + game "friend room" = "Library" + + result = act game, "go to the upper landing with me" + + self.assertEqual game "friend room" , "Upper Landing" + self.assertIn "The friend accompanied the player.", result "detail" + self.assertIn "Friend location: with the player", state block game + + def test examining takeable features adds items to inventory self : + game = engine.new game + game "room" = "Library" + + book result = act game, "examine the book" + self.assertEqual book result "outcome" , "examined" + self.assertIn "Library Book", game "inventory" + self.assertIn "buried his sister", book result "detail" + + game "room" = "Upper Landing" + note result = act game, "read the note" + self.assertEqual note result "outcome" , "examined" + self.assertIn "Upper Landing Note", game "inventory" + + def test sister appears once when stage three is active self : + game = engine.new game + game "stage" = 3 + + first = act game, "look around" + second = act game, "look around" + + self.assertIn "sister", first + self.assertIn "friend's sister", first "sister" + self.assertNotIn "sister", second + self.assertTrue game "sister seen" + + def test prompt does not treat letter initial as his name self : + prompt = story block CANON + + self.assertIn "his name is not established", prompt + self.assertIn "do not call him 'E.'", prompt + + +if name == " main ": + unittest.main + {"method":"textDocument/diagnostic","params":{"textDocument":{"uri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py"}},"workspaceRoot":"C:\\Users\\lucia\\... { "kind": "full", "items": { "range": { "start": { "line": 232, "character": 15 }, "end": { "line": 232, "character": 17 } }, "message": "\" n\" is not accessed", "severity": 4, "source": "Pylance" }, { "range": { "start": { "line": 423, "character": 20 }, "end": { "line": 423, "character": 25 } }, "message": "\" frag\" is not accessed", "severity": 4, "source": "Pylance" }, { "range": { "start": { "line": 608, "character": 22 }, "end": { "line": 608, "character": 34 } }, "message": "\" destination\" is not accessed", "severity": 4, "source": "Pylance" } } {"method":"textDocument/diagnostic","params":{"textDocument":{"uri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py"}},"workspaceRoot":"C:\\Users\\lucia\\... { "kind": "full", "items": } Begin Patch Update File: the-house-that-remembers/engine.py @@ - "keywords": "book", "history", "pamphlet", "volume", "reading table", "page", "passage", "text" , + ... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/engine.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/engine.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/engine.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/engine.py @@ -93,7 +93,16 @@ "history": { "ambient": True, - "keywords": "book", "history", "pamphlet", "volume", "reading table", "page", "passage", "text" , + "keywords": + "book", + "history", + "pamphlet", + "volume", + "reading table", + "page", + "passage", + "text", + , "takeable": "Library Book", "looks": "The missing book lies open on a reading table: a water-stained local history. One passage tells of the house's first owner, who buried his sister before she was truly gone and died soon after. The house stood empty for years before anyone came.", @@ -606,7 +615,7 @@ if target: route = path to game, room name, lambda r: r == target if route: - path, destination = route + path, = route first time rooms = for step in path: if step not in game "visited" : @@ -726,6 +735,8 @@ def apply action game, raw action : """Resolve one player action, update game state, return the engine result.""" + game.setdefault "friend room", game "room" + game.setdefault "sister seen", False text = raw action.lower intent = classify intent raw action, game classify before the action can change the room seen before = len game "examined" measured BEFORE resolving, so first looks count as fresh Begin Patch Update File: README.md @@ - Recall an established memory: What do I remember about my friend? -- Move: I walk to the library. +- Move directly to a destination, even when it i... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/README.md b/c:/Users/lucia/Documents/coding stuff/hacktober/README.md index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/README.md +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/README.md @@ -45,7 +45,8 @@ - Combine speech and an action: I pat him on the back and say, "Let's go." - Explore or examine: Look around or Examine the family photos. - Recall an established memory: What do I remember about my friend? -- Move: I walk to the library. +- Move directly to a destination, even when it is several rooms away: I go to the upper landing. +- Examine a book or note to read it and add it to your inventory; no separate pickup command is needed. Memory answers are limited to details already established in the story; the narrator should not invent shared history. The core behavior and regression tests are now in place. I’ve also updated the player tips and added a visible friend-location indicator; I’m running final checks for the modified Python and reviewing the diff for unintended changes. {"method":"textDocument/diagnostic","params":{"textDocument":{"uri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py"}},"workspaceRoot":"C:\\Users\\lucia\\... { "kind": "full", "items": { "range": { "start": { "line": 241, "character": 15 }, "end": { "line": 241, "character": 17 } }, "message": "\" n\" is not accessed", "severity": 4, "source": "Pylance" }, { "range": { "start": { "line": 432, "character": 20 }, "end": { "line": 432, "character": 25 } }, "message": "\" frag\" is not accessed", "severity": 4, "source": "Pylance" } } {"method":"textDocument/diagnostic","params":{"textDocument":{"uri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/tests/test engine.py"}},"workspaceRoot":"C:\\Use... { "kind": "full", "items": } { "query": "Pylance MCP syntax errors validate Python file syntax textDocument/file syntax errors" } "pylance-mcp-server-pylanceFileSyntaxErrors", "pylance-mcp-server-pylanceSyntaxErrors", "pylance-mcp-server-pylanceLSP", "pylance-mcp-server-pylanceDocuments", "pylance-mcp-server-pylanceAnalyze" Begin Patch Update File: the-house-that-remembers/prompt.py @@ - if r.get "friend" : + friend is here = game.get "friend room", game "room" == game "room" + if r.get "friend" and ... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/prompt.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/prompt.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/prompt.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/prompt.py @@ -181,7 +181,8 @@ note += f"\nGentle push to move the story on weave in naturally, never force : {r 'nudge' }" if r "new clue" : note += f"\nNew clue found: {r 'new clue' }" - if r.get "friend" : + friend is here = game.get "friend room", game "room" == game "room" + if r.get "friend" and friend is here: note += f"\nFriend's likely reaction optional : {r 'friend' }" if r.get "sister" : note += f"\nSister's appearance to narrate this turn: {r 'sister' }" @@ -197,11 +198,13 @@ , "physical action": "Treat this as a physical action by the player. Acknowledge what they actually did " - "and the friend's plausible, restrained response; do not invent additional player actions." + "and, only if he is in this room, the friend's restrained response. Do not invent " + "additional player actions." , "action and dialogue": "Preserve both parts of the player's input: acknowledge their physical action and " - "have the friend respond to their spoken words. Do not add actions for the player." + "have the friend respond only if he is in this room. Do not add actions for the player " + "or invent a reply from elsewhere." , "examination": "Treat this as an examination. Use the ENGINE RESULT as the only new observation; " Begin Patch Update File: the-house-that-remembers/tests/test engine.py @@ -from prompt import state block, story block +from prompt import engine note, state block, story block @@ def te... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/tests/test engine.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/tests/test engine.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/tests/test engine.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/tests/test engine.py @@ -5,7 +5,7 @@ from pathlib import Path import engine -from prompt import state block, story block +from prompt import engine note, state block, story block CANON = json.loads @@ -72,6 +72,19 @@ self.assertIn "friend's sister", first "sister" self.assertNotIn "sister", second self.assertTrue game "sister seen" + game "turn count" = game "last sister turn" + engine.SISTER COOLDOWN - 1 + third = act game, "look around" + self.assertIn "sister", third + self.assertIn "again", third "sister" + + def test remote friend reaction is not included in prompt self : + game = engine.new game + game "room" = "Upper Landing" + game "friend room" = "Library" + act game, "examine the door" + + self.assertNotIn "Friend's likely reaction", engine note game + self.assertIn "cannot hear them", state block game def test prompt does not treat letter initial as his name self : prompt = story block CANON {"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m unittest discover -s tests -v","description":"Run expanded game regression tests","... test examining takeable features adds items to inventory test engine.GameEngineTests.test examining takeable features adds items to inventory ... ok test friend can accompany player when invited test engine.GameEngineTests.test friend can accompany player when invited ... ok test prompt does not treat letter initial as his name test engine.GameEngineTests.test prompt does not treat letter initial as his name ... ok test remote friend reaction is not included in prompt test engine.GameEngineTests.test remote friend reaction is not included in prompt ... ok test routes to non adjacent room and keeps friend in place test engine.GameEngineTests.test routes to non adjacent room and keeps friend in place ... ok test sister appears once when stage three is active test engine.GameEngineTests.test sister appears once when stage three is active ... ok ---------------------------------------------------------------------- Ran 6 tests in 0.002s OK