This is a submission for the Hacktoberfest Open-Source AI Challenge Week 4: Ghost in the Machine
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 (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/
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
<shellId: 10 completed with exit code 0>
{"fileUri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py","workspaceRoot":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remember...
No syntax errors found in 'file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py'
{"fileUri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py","workspaceRoot":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remember...
No syntax errors found in 'file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py'
{"fileUri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/tests/test_engine.py","workspaceRoot":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-th...
No syntax errors found in 'file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/tests/test_engine.py'
{
"command": "git --no-pager diff --check; git --no-pager diff --stat; git status --short",
"description": "Check final whitespace and changed-file summary"
}
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
README.md | 3 +-
the-house-that-remembers/app.py | 4 ++
the-house-that-remembers/engine.py | 107 ++++++++++++++++++++++--------
the-house-that-remembers/prompt.py | 41 ++++++++----
the-house-that-remembers/story/canon.json | 4 +-
5 files changed, 116 insertions(+), 43 deletions(-)
M README.md
M the-house-that-remembers/app.py
M the-house-that-remembers/engine.py
M the-house-that-remembers/prompt.py
M the-house-that-remembers/story/canon.json
?? the-house-that-remembers/tests/
<shellId: 9 completed with exit code 0>
*** Begin Patch
*** Update File: the-house-that-remembers/engine.py
@@
- game.setdefault("friend_room", game["room"])
+ game.setdefault("friend_room", "Entrance Hall")
*** End Patch
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
@@ -737,7 +737,7 @@
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("friend_room", "Entrance Hall")
game.setdefault("sister_seen", False)
game.setdefault("last_sister_turn", -99)
text = raw_action.lower()
*** Begin Patch
*** Update File: the-house-that-remembers/prompt.py
@@
- friend_room = game.get("friend_room", game["room"])
+ friend_room = game.get("friend_room", "Entrance Hall")
@@
- frie...
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
@@ -140,7 +140,7 @@
from engine import ROOMS, exit_labels # local import avoids a cycle if engine grows
room = ROOMS[game["room"]]
- friend_room = game.get("friend_room", game["room"])
+ friend_room = game.get("friend_room", "Entrance Hall")
lines = [
"CURRENT STATE",
f"Room: {game['room']}",
@@ -181,7 +181,7 @@
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']}"
- friend_is_here = game.get("friend_room", game["room"]) == game["room"]
+ friend_is_here = game.get("friend_room", "Entrance Hall") == game["room"]
if r.get("friend") and friend_is_here:
note += f"\nFriend's likely reaction (optional): {r['friend']}"
if r.get("sister"):
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
- friend_room = game.get("friend_room", game["room"])
+ friend_room = game.get("friend_room", "Entrance Hall")
*** End Patch
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -135,7 +135,7 @@
st.write(game["room"])
st.markdown("### 🧍 Friend")
- friend_room = game.get("friend_room", game["room"])
+ friend_room = game.get("friend_room", "Entrance Hall")
st.write("Here with you" if friend_room == game["room"] else f"In the {friend_room}")
st.markdown("### 🎒 Inventory")
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m unittest discover -s tests -v","description":"Run final game regression tests","ini...
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
<shellId: 11 completed with exit code 0>
{"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": 242,
"character": 15
},
"end": {
"line": 242,
"character": 17
}
},
"message": "\"_n\" is not accessed",
"severity": 4,
"source": "Pylance"
},
{
"range": {
"start": {
"line": 434,
"character": 20
},
"end": {
"line": 434,
"character": 25
}
},
"message": "\"_frag\" is not accessed",
"severity": 4,
"source": "Pylance"
}
]
}
{
"fileUri": "file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/app.py",
"workspaceRoot": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers"
}
No syntax errors found in 'file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/app.py'
{
"command": "git --no-pager diff --check; git status --short; git --no-pager diff --numstat",
"description": "Confirm final worktree and whitespace status"
}
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
M README.md
M the-house-that-remembers/app.py
M the-house-that-remembers/engine.py
M the-house-that-remembers/prompt.py
M the-house-that-remembers/story/canon.json
?? the-house-that-remembers/tests/
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
2 1 README.md
4 0 the-house-that-remembers/app.py
80 27 the-house-that-remembers/engine.py
28 13 the-house-that-remembers/prompt.py
2 2 the-house-that-remembers/story/canon.json
<shellId: 12 completed with exit code 0>
Implemented the gameplay fixes:
The model likely hesitated because the prompt’s secrecy rules were broad; Poe’s 1839 work is public domain in the U.S., and the game’s book passage is an original summary rather than a quotation.
Validation: all 6 regression tests passed, and Python syntax checks passed. Pylance reports only two existing unused-variable warnings in engine.py.
Game state:
{'room': 'Entrance Hall',
'friend_room': 'Entrance Hall',
'inventory': ['The Letter'],
'clues': ['A musty smell lingers in the air',
'The clock on the wall is frozen at 3:15'],
'visited': ['Entrance Hall'],
'examined': {'Entrance Hall:smell': 1, 'Entrance Hall:clock': 1},
'stage': 0,
'turns_in_stage': 8,
'turn_count': 8,
'room_turns': 8,
'last_notice_turn': 5,
'last_nudge_turn': -99,
'nudges_here': 0,
'last_sister_turn': -99,
'denial': 6,
'pushed': 0,
'support_given': 0,
'door_open': False,
'truth_known': False,
'sister_seen': False,
'ending': None,
'last_result': {'nudge': None,
'outcome': 'no_change',
'detail': 'Nothing the engine tracks changed. Treat the '
'action as ordinary roleplay (talking, resting, '
'small gestures). Do not add items, clues, or '
'events. No room change happens: the player is '
'still in the Entrance Hall, and nobody leads them '
'elsewhere unless they choose an exit (Library, '
'Kitchen, Grand Staircase).',
'new_clue': None,
'ambient': None,
'friend': None,
'intent': 'dialogue',
'stage_advanced': False},
'history': [{'role': 'assistant',
'content': '"You came. I didn\'t think you would... Thank you."\n'
'\n'},
{'role': 'user',
'content': '"Yes, of course... Your letter worried me... What\'s '
'wrong?"'},
{'role': 'assistant',
'content': 'He shifts slightly, rubbing at his eyes with a '
'trembling hand. "It\'s... nothing much. Just tired. '
'You look well, though. What have you been doing all '
'these years?"'},
{'role': 'user', 'content': '"Tired? What do you mean?"'},
{'role': 'assistant',
'content': '"Just... heavy," he murmurs, glancing toward the '
'shadows near the grand staircase. A musty smell '
'lingers in the air, a mixture of old wood and dust. '
'"Don\'t worry about it. Do you want tea? There\'s... '
'yes. Tea."'},
{'role': 'user',
'content': '"Sure. We can sit, have some tea, and chat..."'},
{'role': 'assistant',
'content': "He manages a weak smile that doesn't reach his "
'hollow eyes. "It’s nice... just to sit down for a '
'minute." He looks toward the library door, then back '
'at you. "Do you remember how we used to explore in '
'there? The library?"'},
{'role': 'user',
'content': '"Yeah, I have always loved the library"'},
{'role': 'assistant',
'content': '"It was strange," your friend agrees quietly, '
'staring at the polished wood of the entrance hall '
'floor. "All those tall shelves... I miss that." He '
'hesitates, then looks around the vast space again. '
'"What about you? What does your life look like these '
'days?"'},
{'role': 'user',
'content': '"Well, not much, you know. Got a boring job in the '
'city and my own place"'},
{'role': 'assistant',
'content': '"A job... that sounds steady." He nods slowly, '
'looking past your shoulder toward the wall. "The '
"clock here... it's stopped, you know. Always has "
'been, maybe." He quickly glances away from the '
'timepiece and then asks, "Did you ever sleep over at '
'all? Do you remember those?"'},
{'role': 'user', 'content': '"Yeah, I do..."'},
{'role': 'assistant',
'content': 'He lets out a small, breathy sound that might have '
'been a laugh. "It was... simpler then." He pulls his '
'shoulders inward slightly, as if cold. "You should '
"sit down properly; you look like you've walked a "
'long way."'},
{'role': 'user', 'content': '"Yes, let\'s sit"'},
{'role': 'assistant',
'content': 'He nods, seeming relieved by the suggestion of rest. '
'"Better? Just... sitting here." He looks toward the '
'kitchen exit with a flicker of uncertainty. "If '
"you're hungry later, I can get something from the "
'kitchen, if that helps?"'},
{'role': 'user', 'content': '"I would like something to driink"'}],
'summary': '',
'summary_idx': 0}
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
300,
355
]
}
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
@@ -300,56 +300,56 @@
counts[frag] = counts.get(frag, 0) + 1
return {f for f, n in counts.items() if n >= FRAGMENT_CLUES_NEEDED}
def exit_labels(game, room_name):
"""Exit names for display, marking locked ones."""
room = ROOMS[room_name]
gated = room.get("gated", {})
labels = []
for e in room["exits"]:
flag = gated.get(e)
labels.append(f"{e} (door locked)" if flag and not game[flag] else e)
return labels
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"
r"|\bgo outside\b|\bwalk out\b"
)
NEGATION_RE = re.compile(r"\b(not|never|without)\b|n't\b|\bwont\b|\bdont\b")
TOGETHER_RE = re.compile(
r"\b(leave|go|walk|get out|come|take him|lead him)\b.*\b(together|with me|with him)\b"
r"|\b(together|with me)\b.*\b(leave|go|out)\b"
r"|\blet'?s (leave|get out)\b"
)
LOOK_AROUND_RE = re.compile(
r"\b(look|glance|scan|survey|search|explore|examine|inspect|study)\w*\b.*\b(around|about|room|surroundings|area)\b"
r"|\b(examine|inspect|search|study|explore|look)\w*\b.*\b(kitchen|library|hall|staircase|landing|bedroom)\b"
r"|\bwhat (do|can) i (see|notice|find)\b|\bnotice anything\b"
)
QUOTED_TEXT_RE = re.compile(r'"[^"]*"|“[^”]*”|‘[^’]*’')
MEMORY_RE = re.compile(
r"\b(remember|recall|used to|when we were (?:kids|children)|"
r"back when we|childhood)\b"
)
EXAMINE_RE = re.compile(
r"\b(examine|inspect|study|look at|check|read|listen to|smell|touch)\b"
)
PHYSICAL_ACTION_RE = re.compile(
r"\b(?:i\s+)?(?:pat|touch|sit|stand|reach|hold|hug|tap|point|"
r"turn|open|close|pick|take|walk|step|move|climb|enter|follow)\b"
)
SPEECH_RE = re.compile(
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
445,
535
]
}
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
@@ -445,91 +445,91 @@
if feat.get("ambient") and game["examined"].get(f"{room_name}:{name}", 0) == 0:
return name, feat
return None
def _unfinished(game, room_name):
"""A room still worth visiting: never visited, or details left to notice."""
if room_name == "Parents' Bedroom":
return False
return room_name not in game["visited"] or _next_unnoticed(game, room_name) is not 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, [])])
while queue:
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)
route = [*path, nxt]
if goal_fn(nxt):
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"]
if (room_name == "Upper Landing" and not game["door_open"]
and len(fragments_complete(game)) >= 3):
return (
"Everything here has been found, but the friend is not ready for the door. "
"Let him hesitate, almost speak, and look to the player. Patience and kindness may "
"help him. Do not open the door."
)
hit = _path_step(game, room_name, lambda r: _unfinished(game, r))
if not hit and not game["door_open"] and room_name != "Upper Landing":
hit = _path_step(game, room_name, lambda r: r == "Upper Landing")
if not hit:
return None
step, target = hit
where = f"the {step}" + (f", on the way to the {target}" if step != target else "")
level = min(game["nudges_here"] + 1, 3)
if level == 1:
return (
"There is nothing more to find here for now. Let the friend gently suggest moving on, "
f"toward {where}. The move happens only if the player chooses it."
)
if level == 2:
return (
"Still nothing new here. Let a faint sound or a change in the light from the direction "
f"of {where} draw attention that way. Do not say what it is. "
"The move happens only if the player chooses it."
)
return (
"The player has lingered. Let the friend drift toward the exit that leads to "
f"{where}, stop there, and look back. The player is still in the {room_name} "
"until they choose to move."
)
def _try_door(game):
if game["door_open"]:
return _result("no_change", "The bedroom door already stands open.")
if len(fragments_complete(game)) < 3:
return _result(
"blocked",
"The door will not give. It is not only locked: pushing at it feels like pushing "
"against something holding its breath. The friend begs the player to leave it. "
"The player does not yet understand enough to know why it matters.",
friend="He is frightened and pleads with the player to step away.",
)
if game["denial"] > DOOR_DENIAL_MAX:
return _result(
"blocked",
"The door stays shut. The friend's hand closes on the player's arm. He is not ready, "
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
575,
665
]
}
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
@@ -575,91 +575,91 @@
def _resolve(game, text):
intent = _classify_intent(text, game)
spoken = text # full text, quoted speech included
text = QUOTED_TEXT_RE.sub("", text).lower() # actions only: speech can't move or examine things
room_name = game["room"]
room = ROOMS[room_name]
rooms_named = _find_rooms(text)
negated = bool(NEGATION_RE.search(text))
spoken_negated = bool(NEGATION_RE.search(spoken))
if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
return _end(game, "leave_together",
"The player and the friend choose to leave the house together.")
if LEAVE_RE.search(text) and not negated:
if room_name == "Entrance Hall":
return _end(game, "escape_alone", "The player walks out of the house alone.")
return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
if not target:
if re.search(r"\bback\b", text) and room_name in BACK:
target = BACK[room_name]
elif re.search(r"\b(up|upstairs)\b", text) and (room_name, "up") in VERTICAL:
target = VERTICAL[(room_name, "up")]
elif re.search(r"\b(down|downstairs)\b", text) and (room_name, "down") in VERTICAL:
target = VERTICAL[(room_name, "down")]
if target:
route = _path_to(game, room_name, lambda r: r == target)
if route:
path, _ = 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
detail = f"The player moved to the {target}."
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 from the {room_name}. "
f"Reachable from here: {', '.join(exit_labels(game, room_name))}.",
)
if room_name in rooms_named:
return _result("no_change", f"The player is already in the {room_name}.")
found = _find_feature(text, room)
if found and _mentions(text, TAKE_VERBS):
name, feat = found
item = feat.get("takeable")
if not item:
return _result("blocked", f"The {name} can't be taken.")
if item in game["inventory"]:
return _result("no_change", f"The player already has the {item}.")
game["inventory"].append(item)
return _result("picked_up", f"The player picked up the {item}.")
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
735,
810
]
}
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
@@ -735,76 +735,76 @@
return False
def apply_action(game, raw_action):
"""Resolve one player action, update game state, return the engine result."""
game.setdefault("friend_room", "Entrance Hall")
game.setdefault("sister_seen", False)
game.setdefault("last_sister_turn", -99)
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
result = _resolve(game, text)
if result["outcome"] == "moved":
intent = "movement"
elif result["outcome"] == "picked_up":
intent = "physical_action"
elif result["outcome"] == "examined" and intent not in {
"examination",
"exploration",
}:
intent = "examination"
result["intent"] = intent
game["turn_count"] += 1
game["turns_in_stage"] += 1
if result["outcome"] != "moved":
game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
if (
result["outcome"] == "no_change"
and not game["ending"]
and not game["truth_known"]
and game["support_given"] < SUPPORT_LIMIT
and SUPPORT_RE.search(text)
):
game["support_given"] += 1
game["denial"] = max(0, game["denial"] - 1)
result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
if not game["ending"] and not game["truth_known"] and game["turn_count"] >= MAX_TURNS:
result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
if fresh:
game["last_notice_turn"] = game["turn_count"]
if (
not fresh
and result["outcome"] in ("no_change", "examined")
and not game["ending"]
and not game["truth_known"]
and game["room_turns"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_nudge_turn"] >= NUDGE_COOLDOWN
and _next_unnoticed(game, game["room"]) is None
):
nudge = _nudge(game)
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
15,
65
]
}
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
@@ -15,51 +15,51 @@
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. If he is elsewhere, he cannot hear or answer the player; do not write his dialogue or place him nearby unless the ENGINE RESULT moves him.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. If asked directly about the past, characters answer in fragments, contradict themselves, or change the subject.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good."
"Do you remember how we used to help in the kitchen? I was never much good at it."
"It's strange being back in the library. We used to spend whole afternoons in here."
"I haven't been sleeping much. The house is loud at night."
"Do you want tea? There's... yes. Tea. I can do that."
"I miss the old days. Now, everything is so... different. I don't know how to explain it."
"What have you been doing all these years? I mean... if you want to tell me.\""""
STAGES = [
{ # 0 - Arrival and uneasy normality
"now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet.",
"friend": "He tries to act normal and spend ordinary time together. He sometimes starts gentle small talk or asks about the player's life, drawing on the established childhood memories of playing outside, kitchen chores, the library, and sleepovers. Vary topics and do not repeat the same offer or line. He deflects direct questions about the house and does not bring up family members on his own.",
"steer": "Let the player settle in and explore. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
},
{ # 1 - Contradictions
"now": "Details about the friend's family do not quite add up.",
"friend": "He mentions his family casually, in the present tense, as if they are alive and nearby: his mother is resting, his father is busy upstairs, his sister is 'around'. He does not notice anything odd. If asked to see them, or asked directly about them, he answers vaguely and changes the subject.",
"steer": "Let this come up once or twice in passing, e.g. \"Mum's resting. She hasn't been sleeping well.\" Do not explain it.",
},
{ # 2 - Frozen house and family traces
"now": "The house is frozen in time: clocks are stopped, calendars are out of date, food never spoils, and places are set for people who are not there. Portraits can change between looks. The friend has lost track of time.",
"friend": "He says time is 'strange' or that he has lost count of the days. He grows uneasy when stopped clocks, old dates or the extra place settings are mentioned and tries to move on.",
"steer": "Describe frozen or changed details only when the engine provides them. Otherwise keep the house quietly wrong: a sound repeating, a door slightly off.",
},
{ # 3 - Sister's warnings
"now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.",
"friend": "He does not see her, or insists the player imagined it.",
"steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most, then gone. She speaks gently and urgently. She never threatens. Do not say what happened to the friend's family.",
},
{ # 4 - Parents' fragments
"now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.",
"friend": "He flinches at the knocking and tells the player, \"Whatever you do, don't answer it.\" Fragments of memory slip out, then he retreats.",
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\prompt.py",
"view_range": [
1,
20
]
}
*** Begin Patch
*** Update File: the-house-that-remembers/the-house-that-remembers/prompt.py
@@
- "friend": "He tries to act normal and spend ordinary time together. He sometimes starts gentle ...
the problem is that the friend is inconsistent, sort of. he invites for a tea and then starts small talking. I wanted to introduce small talk, but when it is appropriate. Do you know what I mean?
Yes—the friend should follow the thread of the conversation: if he offers tea and you accept, he should deal with the tea first, not jump to a new topic. Small talk fits in the quieter gaps, not over an unanswered request. I’ll make that priority explicit in the stage-0 prompt.
*** Begin Patch
*** Update File: the-house-that-remembers/prompt.py
@@
- "friend": "He tries to act normal and spend ordinary time together. He sometimes starts gentle small talk or asks about ...
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
@@ -42,8 +42,8 @@
STAGES = [
{ # 0 - Arrival and uneasy normality
"now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet.",
- "friend": "He tries to act normal and spend ordinary time together. He sometimes starts gentle small talk or asks about the player's life, drawing on the established childhood memories of playing outside, kitchen chores, the library, and sleepovers. Vary topics and do not repeat the same offer or line. He deflects direct questions about the house and does not bring up family members on his own.",
- "steer": "Let the player settle in and explore. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
+ "friend": "He tries to act normal and spend ordinary time together. Keep conversational continuity: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic. If he offers tea or a drink and the player accepts, acknowledge that and offer to get it or invite the player to the kitchen; do not switch to unrelated small talk. Use gentle small talk or ask about the player's life during natural s, when there is no unanswered request or immediate action to follow up. Draw only on established childhood memories of playing outside, kitchen chores, the library, and sleepovers. Vary topics and do not repeat the same offer or line. He deflects direct questions about the house and does not bring up family members on his own.",
+ "steer": "Let the player settle in and explore. When the friend invites them somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
},
{ # 1 - Contradictions
"now": "Details about the friend's family do not quite add up.",
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
34,
46
]
}
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
@@ -34,13 +34,13 @@
"Do you remember how we used to help in the kitchen? I was never much good at it."
"It's strange being back in the library. We used to spend whole afternoons in here."
"I haven't been sleeping much. The house is loud at night."
"Do you want tea? There's... yes. Tea. I can do that."
"I miss the old days. Now, everything is so... different. I don't know how to explain it."
"What have you been doing all these years? I mean... if you want to tell me.\""""
STAGES = [
{ # 0 - Arrival and uneasy normality
"now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet.",
"friend": "He tries to act normal and spend ordinary time together. Keep conversational continuity: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic. If he offers tea or a drink and the player accepts, acknowledge that and offer to get it or invite the player to the kitchen; do not switch to unrelated small talk. Use gentle small talk or ask about the player's life during natural s, when there is no unanswered request or immediate action to follow up. Draw only on established childhood memories of playing outside, kitchen chores, the library, and sleepovers. Vary topics and do not repeat the same offer or line. He deflects direct questions about the house and does not bring up family members on his own.",
"steer": "Let the player settle in and explore. When the friend invites them somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m unittest discover -s tests -v","description":"Run game regression tests","initial_w...
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.003s
OK
<shellId: 13 completed with exit code 0>
{"fileUri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py","workspaceRoot":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remember...
No syntax errors found in 'file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py'
{
"command": "git --no-pager diff --check; git --no-pager diff -- the-house-that-remembers/prompt.py",
"description": "Review the prompt-only change"
}
Output too large to read at once (11.2 KB). Saved to: C:\Users\lucia\AppData\Local\Temp\1793209041571-copilot-tool-output-8656-d14306cc-5aa4-40e1-8935-9a51630c162b.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):
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the work
<shellId: 14 completed with exit code 0>
Yes—that’s what I understand: small talk belongs in natural s, not when it interrupts an offer or request the friend should respond to. If the player accepts tea, he should follow up on the tea first, then make room for casual conversation afterward. He can still initiate small talk when there isn’t an immediate thread to follow.
I updated the stage-0 guidance in prompt.py to make that priority explicit, while keeping the rule that he invites the player to the kitchen rather than moving them automatically. All 6 regression tests pass, and the prompt file has no syntax errors.
Game state:
{'room': 'Kitchen',
'friend_room': 'Entrance Hall',
'inventory': ['The Letter'],
'clues': ['A musty smell lingers in the air',
'The clock on the wall is frozen at 3:15',
'A third cup sits set out at the table, as if someone else were '
'expected'],
'visited': ['Entrance Hall', 'Kitchen'],
'examined': {'Entrance Hall:smell': 1,
'Entrance Hall:clock': 1,
'Kitchen:cups': 1},
'stage': 1,
'turns_in_stage': 3,
'turn_count': 12,
'room_turns': 3,
'last_notice_turn': 11,
'last_nudge_turn': -99,
'nudges_here': 0,
'last_sister_turn': -99,
'denial': 5,
'pushed': 0,
'support_given': 0,
'door_open': False,
'truth_known': False,
'sister_seen': False,
'ending': None,
'last_result': {'nudge': None,
'outcome': 'no_change',
'detail': 'Nothing the engine tracks changed. Treat the '
'action as ordinary roleplay (talking, resting, '
'small gestures). Do not add items, clues, or '
'events. No room change happens: the player is '
'still in the Kitchen, and nobody leads them '
'elsewhere unless they choose an exit (Entrance '
'Hall).',
'new_clue': None,
'ambient': None,
'friend': None,
'intent': 'dialogue',
'stage_advanced': False},
'history': [{'role': 'assistant',
'content': '"You came. I didn\'t think you would... Thank you."\n'
'\n'},
{'role': 'user',
'content': '"Yes, of course... Your letter worried me"'},
{'role': 'assistant',
'content': 'He manages a faint nod, looking down at his hands. '
'"It\'s... it\'s just strange here now. I haven\'t '
'been sleeping much." He glances toward the staircase '
'before quickly averting his gaze again.'},
{'role': 'user', 'content': '"Did something happen?"'},
{'role': 'assistant',
'content': 'He shifts uncomfortably, running a pale hand over '
'the fabric of his shirt. "Nothing... nothing major. '
'Just things are different." The musty smell of old '
'wood and dust seemed to settle deeper into the '
'silence between you.'},
{'role': 'user', 'content': '"What do you mean by that?"'},
{'role': 'assistant',
'content': 'He hesitates, biting his lower lip until it turns '
'white. "It\'s just... the house feels different. '
'Like it remembers things I don\'t." He offers a weak '
"smile that doesn't reach his eyes."},
{'role': 'user', 'content': '"In what ways?"'},
{'role': 'assistant',
'content': 'He shrugs vaguely, looking around the cavernous hall '
'as if expecting something to materialize there. "I '
"don't know how to explain it. It just... sits "
'differently than before." He trails off, staring at '
'the dark wood of the front door.'},
{'role': 'user', 'content': 'examine entrance hall'},
{'role': 'assistant',
'content': 'You notice a tall clock against the wall, its hands '
'frozen precisely at three fifteen. The pendulum '
'hangs motionless in the quiet air. Your friend '
'glances over when you look toward it. "It probably '
'just needs winding," he murmurs before looking away '
'again.'},
{'role': 'user', 'content': '"Do you want to talk about it?"'},
{'role': 'assistant',
'content': 'He looks at you, the exhaustion etched deep around '
'his eyes. "Maybe later," he whispers, his voice '
'barely audible above the silence. He seems to '
'deflate slightly, as if the effort of conversation '
'is too much for him right now.'},
{'role': 'user',
'content': '"Sure... So what do you want to do?"'},
{'role': 'assistant',
'content': '"Well," your friend begins slowly, rubbing his '
'temples with both hands. "I... I could use some tea. '
"There's some in the kitchen. Would you like to "
'come?" He looks hopeful for a moment before his '
'anxiety pulls him back.'},
{'role': 'user', 'content': '"Yes, that sounds good"'},
{'role': 'assistant',
'content': 'He lets out a small, shaky breath of relief at your '
'agreement. "The kitchen... it\'s just down this '
'way." He gestures vaguely toward the archway leading '
'deeper into the house, but does not move to lead you '
'there himself.'},
{'role': 'user', 'content': 'I go to the kitchen'},
{'role': 'assistant',
'content': 'The familiar clutter of the kitchen greets you as '
'you enter. The rain continues its steady tick '
'against the dark window above the sink. Your friend '
'remains visible in the Entrance Hall, a still '
'silhouette near the front door.'},
{'role': 'user', 'content': '"Can I sit?"'},
{'role': 'assistant',
'content': 'Your friend does not follow you into the kitchen '
'immediately; he remains lingering near the threshold '
"of the Entrance Hall. He doesn't hear your words "
'from this distance. You are alone in the narrow '
'space now.'},
{'role': 'user', 'content': 'Is he coming?'},
{'role': 'assistant',
'content': 'From the Entrance Hall, you hear his hesitant '
'footsteps approaching slowly. He stops just inside '
'the doorway to the kitchen, watching you with wide '
'eyes. "I... I didn\'t know if you wanted me to '
'come," he murmurs softly. Two cups wait by the '
'teapot, and then your gaze drifts to a third cup set '
'out at the far end of the table, untouched.'},
{'role': 'user',
'content': '"Well, like I said. your letter sounded worrisome"'}],
'summary': 'The player went into the entrance hall and found a tall clock '
'whose hands were frozen at three fifteen. The narrator stated '
'that things felt strange in the house, saying it seemed like it '
'remembered things he did not. When asked for more detail, the '
'narrator explained the house "sits differently than before." '
'Finally, the player asked if the narrator wanted to talk about '
'it.',
'summary_idx': 12}
It says the friend is still in the entrance hall, but he should be in the kitchen
I see the mismatch: the movement rule only brings him along when the player explicitly says “with me,” but in this exchange the friend invited the player to the kitchen and they accepted. I’ll check how the engine receives conversation history, then make an accepted invitation carry through to the move without having the friend follow every time.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
165,
295
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -165,131 +165,131 @@
st.subheader("🔊 Atmosphere")
sound_enabled = st.checkbox("🌧️ Background rain", value=True)
if sound_enabled:
play_background_music(Path("assets") / "rain-and-thunder.mp3")
house_ambience_enabled = st.checkbox("🏚️ House ambience", value=True)
if house_ambience_enabled:
play_background_music(Path("assets") / "ambience.mp3")
st.markdown('<p class="muted">A GOTHIC HORROR ADVENTURE</p>',
unsafe_allow_html=True)
st.title("The House That Remembers")
with st.expander("The letter and arrival", expanded=True):
_, image_column, _ = st.columns([1, 2, 1])
with image_column:
show_image(
Path(__file__).resolve().parent / "assets" / "haunted_house.jpg",
width=480,
)
st.markdown(INTRO)
scene_column, conversation_column = st.columns([1, 1.25], gap="large")
room = ROOMS[game["room"]]
with scene_column:
with st.container(height="content", border=True):
st.markdown(f"### 📍 {game['room']}")
if "img" in room:
show_image(
Path(__file__).resolve().parent / "assets" / room["img"],
width='stretch',
)
else:
st.caption("No image is available for this room yet.")
st.markdown(
f'<div class="story">{room["description"]}</div>',
unsafe_allow_html=True,
)
with conversation_column:
thinking_placeholder = st.empty()
st.markdown("### What will you do?")
with st.container(height=480, border=True, autoscroll=True):
for message in game["history"]:
with st.chat_message(
message["role"],
avatar="🕯️" if message["role"] == "assistant" else "🧑",
):
st.markdown(message["content"])
if game["ending"]:
st.markdown("---")
st.markdown("**THE END**")
if "ollama_error" in st.session_state:
st.error(st.session_state.ollama_error)
action = st.chat_input(
"Describe what you do...",
disabled=bool(game["ending"]),
)
if action:
snapshot = copy.deepcopy(game) # restore this if the model call fails
game["history"].append({"role": "user", "content": action})
apply_action(game, action) # Python decides what actually happened
error_message = None
response = ""
try:
with thinking_placeholder.container():
with st.spinner("Thinking..."):
maybe_summarize(game, MODEL_NAME)
response = generate_response(game)
except ollama.ResponseError as error:
if error.status_code == 404:
error_message = (
f"Ollama could not find {MODEL_NAME}. "
f"Run `ollama pull {MODEL_NAME}` and try again."
)
else:
error_message = f"Ollama returned an error: {error.error}"
except ollama.RequestError as error:
error_message = (
"Could not connect to Ollama. Make sure Ollama is running, "
f"then try again. Details: {error}"
)
else:
if not response:
error_message = "Ollama returned an empty response. Please try again."
if error_message:
st.session_state.game = snapshot # undo the failed turn completely
st.session_state.ollama_error = error_message
else:
st.session_state.pop("ollama_error", None)
game["history"].append({"role": "assistant", "content": response})
st.rerun()
st.caption(f"Powered by Ollama · {MODEL_NAME}")
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
245,
310
]
}
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
@@ -245,66 +245,66 @@
}
BACK = {
"Library": "Entrance Hall",
"Kitchen": "Entrance Hall",
"Grand Staircase": "Entrance Hall",
"Upper Landing": "Grand Staircase",
"Parents' Bedroom": "Upper Landing",
}
VERTICAL = {
("Entrance Hall", "up"): "Grand Staircase",
("Grand Staircase", "up"): "Upper Landing",
("Grand Staircase", "down"): "Entrance Hall",
("Upper Landing", "down"): "Grand Staircase",
}
def new_game():
return {
"room": "Entrance Hall",
"friend_room": "Entrance Hall",
"inventory": ["The Letter"],
"clues": [],
"visited": ["Entrance Hall"],
"examined": {}, # "Room:feature" -> times examined
"stage": 0, # index into prompt.STAGES
"turns_in_stage": 0,
"turn_count": 0,
"room_turns": 0, # turns spent in the current room since arriving
"last_notice_turn": -99, # turn_count when a detail was last surfaced
"last_nudge_turn": -99, # turn_count when the friend was last nudged
"nudges_here": 0, # nudges in this room since last move (escalate)
"last_sister_turn": -99,
"denial": 8, # friend's denial (high = deflecting)
"pushed": 0,
"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"}],
"summary": "", # rolling summary of folded-away history
"summary_idx": 0, # history[:summary_idx] is already summarized
}
def fragments_complete(game):
"""Set of completed fragment kinds ('routine', 'rules', 'night')."""
counts = {}
for clue in game["clues"]:
frag = CLUE_FRAGMENT.get(clue)
if frag:
counts[frag] = counts.get(frag, 0) + 1
return {f for f, n in counts.items() if n >= FRAGMENT_CLUES_NEEDED}
def exit_labels(game, room_name):
"""Exit names for display, marking locked ones."""
room = ROOMS[room_name]
gated = room.get("gated", {})
labels = []
for e in room["exits"]:
flag = gated.get(e)
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
300,
365
]
}
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
@@ -300,66 +300,66 @@
counts[frag] = counts.get(frag, 0) + 1
return {f for f, n in counts.items() if n >= FRAGMENT_CLUES_NEEDED}
def exit_labels(game, room_name):
"""Exit names for display, marking locked ones."""
room = ROOMS[room_name]
gated = room.get("gated", {})
labels = []
for e in room["exits"]:
flag = gated.get(e)
labels.append(f"{e} (door locked)" if flag and not game[flag] else e)
return labels
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"
r"|\bgo outside\b|\bwalk out\b"
)
NEGATION_RE = re.compile(r"\b(not|never|without)\b|n't\b|\bwont\b|\bdont\b")
TOGETHER_RE = re.compile(
r"\b(leave|go|walk|get out|come|take him|lead him)\b.*\b(together|with me|with him)\b"
r"|\b(together|with me)\b.*\b(leave|go|out)\b"
r"|\blet'?s (leave|get out)\b"
)
LOOK_AROUND_RE = re.compile(
r"\b(look|glance|scan|survey|search|explore|examine|inspect|study)\w*\b.*\b(around|about|room|surroundings|area)\b"
r"|\b(examine|inspect|search|study|explore|look)\w*\b.*\b(kitchen|library|hall|staircase|landing|bedroom)\b"
r"|\bwhat (do|can) i (see|notice|find)\b|\bnotice anything\b"
)
QUOTED_TEXT_RE = re.compile(r'"[^"]*"|“[^”]*”|‘[^’]*’')
MEMORY_RE = re.compile(
r"\b(remember|recall|used to|when we were (?:kids|children)|"
r"back when we|childhood)\b"
)
EXAMINE_RE = re.compile(
r"\b(examine|inspect|study|look at|check|read|listen to|smell|touch)\b"
)
PHYSICAL_ACTION_RE = re.compile(
r"\b(?:i\s+)?(?:pat|touch|sit|stand|reach|hold|hug|tap|point|"
r"turn|open|close|pick|take|walk|step|move|climb|enter|follow)\b"
)
SPEECH_RE = re.compile(
r"\b(?:i\s+)?(?:say|ask|tell|reply|answer|whisper|speak|call)\b"
r"|\b(?:ask|tell) (?:him|her|your friend)\b"
)
DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b")
DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b")
FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b")
SUPPORT_RE = re.compile(
r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b"
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
585,
660
]
}
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
@@ -585,76 +585,76 @@
if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
return _end(game, "leave_together",
"The player and the friend choose to leave the house together.")
if LEAVE_RE.search(text) and not negated:
if room_name == "Entrance Hall":
return _end(game, "escape_alone", "The player walks out of the house alone.")
return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
if not target:
if re.search(r"\bback\b", text) and room_name in BACK:
target = BACK[room_name]
elif re.search(r"\b(up|upstairs)\b", text) and (room_name, "up") in VERTICAL:
target = VERTICAL[(room_name, "up")]
elif re.search(r"\b(down|downstairs)\b", text) and (room_name, "down") in VERTICAL:
target = VERTICAL[(room_name, "down")]
if target:
route = _path_to(game, room_name, lambda r: r == target)
if route:
path, _ = 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
detail = f"The player moved to the {target}."
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 from the {room_name}. "
f"Reachable from here: {', '.join(exit_labels(game, room_name))}.",
)
if room_name in rooms_named:
return _result("no_change", f"The player is already in the {room_name}.")
found = _find_feature(text, room)
if found and _mentions(text, TAKE_VERBS):
name, feat = found
item = feat.get("takeable")
if not item:
return _result("blocked", f"The {name} can't be taken.")
if item in game["inventory"]:
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\tests\\test_engine.py"
}
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
@@ -1,98 +1,98 @@
import contextlib
import io
import json
import unittest
from pathlib import Path
import engine
from prompt import _engine_note, 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"])
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)
self.assertIn("his name is not established", prompt)
self.assertIn("do not call him 'E.'", prompt)
if __name__ == "__main__":
unittest.main()
The engine sees the prior assistant turn, so it can distinguish “I accepted your invitation” from an ordinary move. I’ll track an accepted, room-specific invitation briefly and have the friend accompany the player only when they head to that destination.
*** Begin Patch
*** Update File: the-house-that-remembers/the-house-that-remembers/engine.py
@@
"room": "Entrance Hall",
"friend_room": "Entrance Hall",
+ "friend_follow_targe...
*** Begin Patch
*** Update File: engine.py
@@
"room": "Entrance Hall",
"friend_room": "Entrance Hall",
+ "friend_follow_target": None,
+ "friend_follow_expires": -1,
@@...
*** Begin Patch
*** Update File: the-house-that-remembers/engine.py
@@
"room": "Entrance Hall",
"friend_room": "Entrance Hall",
+ "friend_follow_target": None,
+ "frien...
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
@@ -266,6 +266,8 @@
return {
"room": "Entrance Hall",
"friend_room": "Entrance Hall",
+ "friend_follow_target": None,
+ "friend_follow_expires": -1,
"inventory": ["The Letter"],
"clues": [],
"visited": ["Entrance Hall"],
@@ -322,6 +324,14 @@
r"come with me|walk with (?:him|my friend)|lead him|guide me|"
r"wait for him to follow)\b"
)
+FRIEND_INVITATION_RE = re.compile(
+ r"\b(?:would you like to come|would you like to join|"
+ r"want to come|come with me|join me|shall we go|let'?s go)\b"
+)
+ACCEPT_INVITATION_RE = re.compile(
+ r"^\s*(?:yes|yeah|yep|sure|okay|ok|sounds good|that sounds good|"
+ r"i(?:'d| would) like that|please|absolutely|of course)\b"
+)
LEAVE_RE = re.compile(
r"\b(leave|exit|flee|escape)\b.*\b(house|mansion|manor)\b"
@@ -374,6 +384,29 @@
return [name for name, r in ROOMS.items() if _mentions(text, r["aliases"])]
+def _record_accepted_invitation(game, raw_action):
+ """Remember a nearby destination when the player accepts the friend's offer."""
+ action = raw_action.strip().strip("\"'“”‘’").lower()
+ if not ACCEPT_INVITATION_RE.search(action) or NEGATION_RE.search(action):
+ return
+
+ previous_assistant = next(
+ (
+ message["content"]
+ for message in reversed(game["history"][:-1])
+ if message["role"] == "assistant"
+ ),
+ "",
+ )
+ if not FRIEND_INVITATION_RE.search(previous_assistant.lower()):
+ return
+
+ destinations = _find_rooms(previous_assistant.lower())
+ if len(destinations) == 1 and destinations[0] != game["room"]:
+ game["friend_follow_target"] = destinations[0]
+ game["friend_follow_expires"] = game["turn_count"] + 5
+
+
def _find_feature(text, room):
for name, feat in room.get("features", {}).items():
if _mentions(text, feat["keywords"]):
@@ -636,11 +669,20 @@
detail += (
f" First visit to: {', '.join(first_time_rooms)}."
)
- if FRIEND_FOLLOWS_RE.search(text) and game["friend_room"] == room_name:
+ accepted_invitation = (
+ game.get("friend_follow_target") == target
+ and game["turn_count"] <= game.get("friend_follow_expires", -1)
+ )
+ if (
+ (FRIEND_FOLLOWS_RE.search(text) or accepted_invitation)
+ 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']}."
+ game["friend_follow_target"] = None
+ game["friend_follow_expires"] = -1
return _result("moved", detail)
return _result(
"blocked",
@@ -738,8 +780,11 @@
def apply_action(game, raw_action):
"""Resolve one player action, update game state, return the engine result."""
game.setdefault("friend_room", "Entrance Hall")
+ game.setdefault("friend_follow_target", None)
+ game.setdefault("friend_follow_expires", -1)
game.setdefault("sister_seen", False)
game.setdefault("last_sister_turn", -99)
+ _record_accepted_invitation(game, raw_action)
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: the-house-that-remembers/tests/test_engine.py
@@
def test_friend_can_accompany_player_when_invited(self):
game = engine.new_game()
game["room"] ...
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
@@ -47,6 +47,33 @@
self.assertIn("The friend accompanied the player.", result["detail"])
self.assertIn("Friend location: with the player", state_block(game))
+ def test_friend_follows_after_player_accepts_his_room_invitation(self):
+ game = engine.new_game()
+ game["history"].append(
+ {
+ "role": "assistant",
+ "content": "Would you like to come to the kitchen for tea?",
+ }
+ )
+ game["history"].append({"role": "user", "content": "Yes, that sounds good."})
+
+ act(game, "Yes, that sounds good.")
+ self.assertEqual(game["friend_follow_target"], "Kitchen")
+
+ game["history"].append(
+ {
+ "role": "assistant",
+ "content": "The kitchen is just down the hall.",
+ }
+ )
+ game["history"].append({"role": "user", "content": "I go to the kitchen."})
+ result = act(game, "I go to the kitchen.")
+
+ self.assertEqual(game["room"], "Kitchen")
+ self.assertEqual(game["friend_room"], "Kitchen")
+ self.assertIn("The friend accompanied the player.", result["detail"])
+ self.assertIsNone(game["friend_follow_target"])
+
def test_examining_takeable_features_adds_items_to_inventory(self):
game = engine.new_game()
game["room"] = "Library"
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m unittest discover -s tests -v","description":"Run invitation-follow behavior 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_friend_follows_after_player_accepts_his_room_invitation (test_engine.GameEngineTests.test_friend_follows_after_player_accepts_his_room_invitation) ... 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 7 tests in 0.003s
OK
<shellId: 16 completed with exit code 0>
{"fileUri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py","workspaceRoot":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remember...
No syntax errors found in 'file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py'
{"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": 242,
"character": 15
},
"end": {
"line": 242,
"character": 17
}
},
"message": "\"_n\" is not accessed",
"severity": 4,
"source": "Pylance"
},
{
"range": {
"start": {
"line": 467,
"character": 20
},
"end": {
"line": 467,
"character": 25
}
},
"message": "\"_frag\" is not accessed",
"severity": 4,
"source": "Pylance"
}
]
}
{
"command": "git --no-pager diff --check; git status --short; git --no-pager diff --stat",
"description": "Review worktree after invitation tracking"
}
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
M .gitignore
M README.md
M the-house-that-remembers/app.py
M the-house-that-remembers/engine.py
M the-house-that-remembers/prompt.py
M the-house-that-remembers/story/canon.json
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
.gitignore | 3 +-
README.md | 3 +-
the-house-that-remembers/app.py | 4 +
the-house-that-remembers/engine.py | 154 ++++++++++++++++++++++++------
the-house-that-remembers/prompt.py | 46 ++++++---
the-house-that-remembers/story/canon.json | 4 +-
6 files changed, 168 insertions(+), 46 deletions(-)
<shellId: 15 completed with exit code 0>
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\.gitignore"
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/.gitignore b/C:/Users/lucia/Documents/coding stuff/hacktober/.gitignore
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/.gitignore
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/.gitignore
@@ -1,6 +1,6 @@
.venv/
__pycache__/
*.py[cod]
.streamlit/secrets.toml
temp/
tests/
{"command":"git --no-pager diff -- .gitignore; git check-ignore -v the-house-that-remembers/tests/test_engine.py; git status --short --untracked-files=all","description":"Check why regression test is ...
diff --git a/.gitignore b/.gitignore
index 2ab0d58..1f317a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@
__pycache__/
*.py[cod]
.streamlit/secrets.toml
-temp/
\ No newline at end of file
+temp/
+tests/
\ No newline at end of file
.gitignore:6:tests/ the-house-that-remembers/tests/test_engine.py
M .gitignore
M README.md
M the-house-that-remembers/app.py
M the-house-that-remembers/engine.py
M the-house-that-remembers/prompt.py
M the-house-that-remembers/story/canon.json
<shellId: 17 completed with exit code 0>
{"message":"I fixed the friend-follow behavior and verified it with 7 passing tests. While checking the diff, I noticed `.gitignore` now has an unrecognized `tests/` entry, which makes the regression ...
User responded:
test_tracking: leave_ignored
Fixed the friend’s location continuity. When the player accepts an invitation to a specific room, the engine remembers that destination briefly; when the player then goes there, the friend accompanies them. Ordinary movement still leaves the friend where he is unless invited.
All 7 regression tests pass. The test file remains under tests/, as requested, but is ignored by .gitignore; I left that file unchanged. The engine change is in engine.py.
I see that the small talk is only mentioned in Stage 0, but it can occur any time. Can you makethe change more generic?
I’ll move the conversational-continuity and small-talk guidance into the shared narrator rules so it applies in every stage, and remove the stage-0-only version.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
1,
80
]
}
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
@@ -1,80 +1,80 @@
"""prompt.py - builds what the model sees each turn.
Layers (top to bottom):
1. CORE static narrator rules (cacheable prefix)
2. story block premise, friend sheet, letter (static per story)
3. stage block what may be revealed *now* (changes with stage)
4. state block room, exits, inventory, clues (changes per turn)
5. history last N messages (older -> summary)
6. engine result + reminder, attached to the player's latest message
"""
import re
import ollama
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. If he is elsewhere, he cannot hear or answer the player; do not write his dialogue or place him nearby unless the ENGINE RESULT moves him.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. If asked directly about the past, characters answer in fragments, contradict themselves, or change the subject.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good."
"Do you remember how we used to help in the kitchen? I was never much good at it."
"It's strange being back in the library. We used to spend whole afternoons in here."
"I haven't been sleeping much. The house is loud at night."
"Do you want tea? There's... yes. Tea. I can do that."
"I miss the old days. Now, everything is so... different. I don't know how to explain it."
"What have you been doing all these years? I mean... if you want to tell me.\""""
STAGES = [
{ # 0 - Arrival and uneasy normality
"now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet. Friend invited the player to the house because he is lonely and the history of the house is weighing on him. He is not aware of the truth, but he doesn't want to be alone.",
"friend": "He tries to act normal and spend ordinary time together. Keep conversational continuity: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic. If he offers tea or a drink and the player accepts, invite the player to the kitchen. Use gentle small talk or ask about the player's life during natural s, when there is no unanswered request or immediate action to follow up. Draw only on established childhood memories of playing outside, kitchen chores, the library, and sleepovers. Vary topics and do not repeat the same offer or line. He deflects direct questions about the house and does not bring up family members on his own.",
"steer": "Let the player settle in and explore. When the friend invites them somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
},
{ # 1 - Contradictions
"now": "Details about the friend's family do not quite add up.",
"friend": "He mentions his family casually, in the present tense, as if they are alive and nearby: his mother is resting, his father is busy upstairs, his sister is 'around'. He does not notice anything odd. If asked to see them, or asked directly about them, he answers vaguely and changes the subject.",
"steer": "Let this come up once or twice in passing, e.g. \"Mum's resting. She hasn't been sleeping well.\" Do not explain it.",
},
{ # 2 - Frozen house and family traces
"now": "The house is frozen in time: clocks are stopped, calendars are out of date, food never spoils, and places are set for people who are not there. Portraits can change between looks. The friend has lost track of time.",
"friend": "He says time is 'strange' or that he has lost count of the days. He grows uneasy when stopped clocks, old dates or the extra place settings are mentioned and tries to move on.",
"steer": "Describe frozen or changed details only when the engine provides them. Otherwise keep the house quietly wrong: a sound repeating, a door slightly off.",
},
{ # 3 - Sister's warnings
"now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.",
"friend": "He does not see her, or insists the player imagined it.",
"steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most, then gone. She speaks gently and urgently. She never threatens. Do not say what happened to the friend's family.",
},
{ # 4 - Parents' fragments
"now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.",
"friend": "He flinches at the knocking and tells the player, \"Whatever you do, don't answer it.\" Fragments of memory slip out, then he retreats.",
"steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.",
},
{ # 5 - Reconstructed truth
"now": "The bedroom door has opened. Inside, the family lie preserved as if asleep. The truth, to be revealed only gently and in fragments: after the father and son stopped speaking, the father locked himself, the mother and the sister in this room and told the son never to open the door. Cut off from help and drained by the house, they died inside. The friend knocked once, heard nothing, obeyed the rule out of anger and fear, and never opened it. The house kept the room as it was.",
"friend": "His denial breaks. He speaks in halting fragments and quiet realization, not speeches. He does not blame the player. He may say he cannot stay here, but never decides for the player.",
"steer": "Let the moment breathe: stillness, cold, grief. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.",
},
]
ENDINGS = {
"leave_together": (
"The player and the friend have faced the truth and choose to leave together. "
"Write the final scene in 4-6 sentences: the friend's grief and fragile steadiness, the house letting go "
"(the stopped clocks may tick once; the knocking and humming fall silent), the door, the rain easing. "
"No gore. Offer no further choices. End the story."
*** Begin Patch
*** Update File: the-house-that-remembers/prompt.py
@@
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- ...
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
@@ -20,6 +20,11 @@
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
+Friend conversation:
+- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
+- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
+- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
+
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
@@ -42,7 +47,7 @@
STAGES = [
{ # 0 - Arrival and uneasy normality
"now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet. Friend invited the player to the house because he is lonely and the history of the house is weighing on him. He is not aware of the truth, but he doesn't want to be alone.",
- "friend": "He tries to act normal and spend ordinary time together. Keep conversational continuity: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic. If he offers tea or a drink and the player accepts, invite the player to the kitchen. Use gentle small talk or ask about the player's life during natural s, when there is no unanswered request or immediate action to follow up. Draw only on established childhood memories of playing outside, kitchen chores, the library, and sleepovers. Vary topics and do not repeat the same offer or line. He deflects direct questions about the house and does not bring up family members on his own.",
+ "friend": "He tries to act normal and spend ordinary time together. He can ask about the player's life or mention an established childhood memory. He deflects direct questions about the house and does not bring up family members on his own.",
"steer": "Let the player settle in and explore. When the friend invites them somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
},
{ # 1 - Contradictions
{"fileUri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py","workspaceRoot":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remember...
No syntax errors found in 'file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py'
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m unittest discover -s tests -v","description":"Run game regression suite","initial_w...
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_friend_follows_after_player_accepts_his_room_invitation (test_engine.GameEngineTests.test_friend_follows_after_player_accepts_his_room_invitation) ... 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 7 tests in 0.003s
OK
<shellId: 18 completed with exit code 0>
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
12,
56
]
}
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
@@ -12,45 +12,45 @@
import ollama
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. If he is elsewhere, he cannot hear or answer the player; do not write his dialogue or place him nearby unless the ENGINE RESULT moves him.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. If asked directly about the past, characters answer in fragments, contradict themselves, or change the subject.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good."
"Do you remember how we used to help in the kitchen? I was never much good at it."
"It's strange being back in the library. We used to spend whole afternoons in here."
"I haven't been sleeping much. The house is loud at night."
"Do you want tea? There's... yes. Tea. I can do that."
"I miss the old days. Now, everything is so... different. I don't know how to explain it."
"What have you been doing all these years? I mean... if you want to tell me.\""""
STAGES = [
{ # 0 - Arrival and uneasy normality
"now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet. Friend invited the player to the house because he is lonely and the history of the house is weighing on him. He is not aware of the truth, but he doesn't want to be alone.",
"friend": "He tries to act normal and spend ordinary time together. He can ask about the player's life or mention an established childhood memory. He deflects direct questions about the house and does not bring up family members on his own.",
"steer": "Let the player settle in and explore. When the friend invites them somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
},
{ # 1 - Contradictions
"now": "Details about the friend's family do not quite add up.",
"friend": "He mentions his family casually, in the present tense, as if they are alive and nearby: his mother is resting, his father is busy upstairs, his sister is 'around'. He does not notice anything odd. If asked to see them, or asked directly about them, he answers vaguely and changes the subject.",
"steer": "Let this come up once or twice in passing, e.g. \"Mum's resting. She hasn't been sleeping well.\" Do not explain it.",
{
"command": "git --no-pager diff --check; git --no-pager diff -- the-house-that-remembers/prompt.py",
"description": "Verify generalized prompt diff"
}
Output too large to read at once (11.6 KB). Saved to: C:\Users\lucia\AppData\Local\Temp\1793209931883-copilot-tool-output-8656-4aafd0f7-886a-4ca8-85ae-c8784d7f204c.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):
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the work
<shellId: 19 completed with exit code 0>
Done. I moved the friend’s conversational-continuity and small-talk guidance into the shared narrator rules in prompt.py, so it applies in every stage. Stage 0 now only describes his early demeanor; small talk should fit his current state and the moment throughout the story.
Validation passed: all 7 regression tests pass, and the prompt has no syntax errors.
Game state:
{'room': 'Kitchen',
'friend_room': 'Entrance Hall',
'friend_follow_target': None,
'friend_follow_expires': -1,
'inventory': ['The Letter'],
'clues': ['A musty smell lingers in the air',
'The clock on the wall is frozen at 3:15',
'A third cup sits set out at the table, as if someone else were '
'expected',
'A kitchen chair is pulled out, as though someone had just stood '
'up'],
'visited': ['Entrance Hall', 'Kitchen'],
'examined': {'Entrance Hall:smell': 1,
'Entrance Hall:clock': 1,
'Kitchen:cups': 1,
'Kitchen:chair': 1},
'stage': 1,
'turns_in_stage': 4,
'turn_count': 11,
'room_turns': 4,
'last_notice_turn': 11,
'last_nudge_turn': -99,
'nudges_here': 0,
'last_sister_turn': -99,
'denial': 4,
'pushed': 0,
'support_given': 0,
'door_open': False,
'truth_known': False,
'sister_seen': False,
'ending': None,
'last_result': {'nudge': None,
'outcome': 'no_change',
'detail': 'Nothing the engine tracks changed. Treat the '
'action as ordinary roleplay (talking, resting, '
'small gestures). Do not add items, clues, or '
'events. No room change happens: the player is '
'still in the Kitchen, and nobody leads them '
'elsewhere unless they choose an exit (Entrance '
'Hall).',
'new_clue': 'A kitchen chair is pulled out, as though someone '
'had just stood up',
'ambient': 'One chair at the table is pulled out slightly, as '
'though someone had only just stood up from it.',
'friend': None,
'intent': 'dialogue',
'stage_advanced': False},
'history': [{'role': 'assistant',
'content': '"You came. I didn\'t think you would... Thank you."\n'
'\n'},
{'role': 'user',
'content': '"Of course... Your letter worried me. Are you '
'okay?"'},
{'role': 'assistant',
'content': 'He shifts slightly, rubbing at his temples with a '
'pale hand. "I\'m... I’m fine. It\'s just... things '
'here." He manages a weak smile before looking away '
'again. "What have you been up to? Still around the '
'city?"'},
{'role': 'user',
'content': '"Yeah... You know, got a boring job and moved to the '
'city. What about you?"'},
{'role': 'assistant',
'content': 'He stares at the floor for a moment, then shrugs '
'faintly. "Same old things, I suppose. Just... '
'staying put." The musty smell of old wood and dust '
'hangs heavy in the vast hall around you both. He '
'seems to deflate slightly under your gaze.'},
{'role': 'user', 'content': 'examine entrance hall'},
{'role': 'assistant',
'content': 'You notice the tall clock against the wall; its '
'hands are stuck at a quarter past three. The '
'pendulum hangs motionless, and nothing in this hall '
'seems to tick. He glances over at it briefly. "It '
'just needs winding," he murmurs, then looks back '
'toward you. "Did you want tea? There\'s... yes. '
'Tea."'},
{'role': 'user', 'content': '"Sure, tea sounds good"'},
{'role': 'assistant',
'content': 'He nods slowly, seeming relieved by the simple '
'agreement. "Good. I... I could use it." He hesitates '
'for a beat before gesturing vaguely toward an unseen '
'area. "I suppose we could go to the kitchen?"'},
{'role': 'user', 'content': '"Yes"'},
{'role': 'assistant',
'content': 'He nods again, the fragile hope flickering across '
'his face. "The kitchen... it\'s just down this way." '
"He doesn't move to lead you, simply waiting for your "
'next step toward that direction. "It\'s quiet in '
'there, usually."'},
{'role': 'user', 'content': '"Go to the kitchen"'},
{'role': 'assistant',
'content': 'He watches you move toward the Kitchen exit, his '
'expression unreadable for a moment. "Just... let me '
'know if you need anything," he says softly, his '
'voice barely above a whisper. He seems to gather '
'himself, then adds, "It\'s strange being back in '
'this place."'},
{'role': 'user', 'content': 'Go to the kitchen'},
{'role': 'assistant',
'content': 'The narrow kitchen greets you with its familiar '
'clutter and the steady tick of rain against the dark '
'window above the sink. The air here is heavy with a '
'musty scent. Your friend remains back in the '
'Entrance Hall. You can hear the quiet drumming of '
'the rain from that distant hall.'},
{'role': 'user', 'content': 'examine kitchen'},
{'role': 'assistant',
'content': 'Two cups rest neatly beside the teapot on the '
'counter. Further across the small table, a third cup '
'sits waiting, untouched and stark against the wood. '
'The rain continues its steady rhythm against the '
'windowpane.'},
{'role': 'user', 'content': 'Wait for my friend'},
{'role': 'assistant',
'content': 'You stand by the counter, listening only to the rain '
'and the silence of the kitchen. After a moment, you '
'hear his hesitant footsteps approaching from the '
'hall. "Sorry," he murmurs as he enters, looking '
'exhausted. "I just... I needed a minute."'},
{'role': 'user', 'content': '"No worries"'},
{'role': 'assistant',
'content': 'He looks at you, his eyes wide for a second before '
'they settle back into their weary focus. "It\'s '
'just... always something here," he says quietly, '
'gesturing vaguely around the cluttered space. He '
'shifts his weight, looking like he might collapse. '
'"Do you want me to pour the tea?"'},
{'role': 'user',
'content': '"Yes, that would be nice. It is so cold outside"'}],
'summary': '',
'summary_idx': 0}
It still says that the friend is in the entrance hall, but he is in the kitchen a pouring tea. Can we fix this?
The state shows two causes: the friend didn’t accompany the player on the kitchen move, and “Wait for my friend” was treated as ordinary roleplay, so the engine never recorded his arrival. I’ll make waiting for the friend an explicit engine-resolved action, so the narrator and authoritative location stay aligned.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
300,
410
]
}
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
@@ -300,111 +300,111 @@
def fragments_complete(game):
"""Set of completed fragment kinds ('routine', 'rules', 'night')."""
counts = {}
for clue in game["clues"]:
frag = CLUE_FRAGMENT.get(clue)
if frag:
counts[frag] = counts.get(frag, 0) + 1
return {f for f, n in counts.items() if n >= FRAGMENT_CLUES_NEEDED}
def exit_labels(game, room_name):
"""Exit names for display, marking locked ones."""
room = ROOMS[room_name]
gated = room.get("gated", {})
labels = []
for e in room["exits"]:
flag = gated.get(e)
labels.append(f"{e} (door locked)" if flag and not game[flag] else e)
return labels
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"
)
FRIEND_INVITATION_RE = re.compile(
r"\b(?:would you like to come|would you like to join|"
r"want to come|come with me|join me|shall we go|let'?s go)\b"
)
ACCEPT_INVITATION_RE = re.compile(
r"^\s*(?:yes|yeah|yep|sure|okay|ok|sounds good|that sounds good|"
r"i(?:'d| would) like that|please|absolutely|of course)\b"
)
LEAVE_RE = re.compile(
r"\b(leave|exit|flee|escape)\b.*\b(house|mansion|manor)\b"
r"|\bgo outside\b|\bwalk out\b"
)
NEGATION_RE = re.compile(r"\b(not|never|without)\b|n't\b|\bwont\b|\bdont\b")
TOGETHER_RE = re.compile(
r"\b(leave|go|walk|get out|come|take him|lead him)\b.*\b(together|with me|with him)\b"
r"|\b(together|with me)\b.*\b(leave|go|out)\b"
r"|\blet'?s (leave|get out)\b"
)
LOOK_AROUND_RE = re.compile(
r"\b(look|glance|scan|survey|search|explore|examine|inspect|study)\w*\b.*\b(around|about|room|surroundings|area)\b"
r"|\b(examine|inspect|search|study|explore|look)\w*\b.*\b(kitchen|library|hall|staircase|landing|bedroom)\b"
r"|\bwhat (do|can) i (see|notice|find)\b|\bnotice anything\b"
)
QUOTED_TEXT_RE = re.compile(r'"[^"]*"|“[^”]*”|‘[^’]*’')
MEMORY_RE = re.compile(
r"\b(remember|recall|used to|when we were (?:kids|children)|"
r"back when we|childhood)\b"
)
EXAMINE_RE = re.compile(
r"\b(examine|inspect|study|look at|check|read|listen to|smell|touch)\b"
)
PHYSICAL_ACTION_RE = re.compile(
r"\b(?:i\s+)?(?:pat|touch|sit|stand|reach|hold|hug|tap|point|"
r"turn|open|close|pick|take|walk|step|move|climb|enter|follow)\b"
)
SPEECH_RE = re.compile(
r"\b(?:i\s+)?(?:say|ask|tell|reply|answer|whisper|speak|call)\b"
r"|\b(?:ask|tell) (?:him|her|your friend)\b"
)
DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b")
DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b")
FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b")
SUPPORT_RE = re.compile(
r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b"
r"|\bit'?s (ok|okay|alright)\b"
)
def _mentions(text, words):
return any(re.search(rf"\b{re.escape(w)}\b", text) for w in words)
def _find_rooms(text):
return [name for name, r in ROOMS.items() if _mentions(text, r["aliases"])]
def _record_accepted_invitation(game, raw_action):
"""Remember a nearby destination when the player accepts the friend's offer."""
action = raw_action.strip().strip("\"'“”‘’").lower()
if not ACCEPT_INVITATION_RE.search(action) or NEGATION_RE.search(action):
return
previous_assistant = next(
(
message["content"]
for message in reversed(game["history"][:-1])
if message["role"] == "assistant"
),
"",
)
if not FRIEND_INVITATION_RE.search(previous_assistant.lower()):
return
destinations = _find_rooms(previous_assistant.lower())
if len(destinations) == 1 and destinations[0] != game["room"]:
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
610,
790
]
}
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
@@ -610,181 +610,181 @@
)
def _resolve(game, text):
intent = _classify_intent(text, game)
spoken = text # full text, quoted speech included
text = QUOTED_TEXT_RE.sub("", text).lower() # actions only: speech can't move or examine things
room_name = game["room"]
room = ROOMS[room_name]
rooms_named = _find_rooms(text)
negated = bool(NEGATION_RE.search(text))
spoken_negated = bool(NEGATION_RE.search(spoken))
if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
return _end(game, "leave_together",
"The player and the friend choose to leave the house together.")
if LEAVE_RE.search(text) and not negated:
if room_name == "Entrance Hall":
return _end(game, "escape_alone", "The player walks out of the house alone.")
return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
if not target:
if re.search(r"\bback\b", text) and room_name in BACK:
target = BACK[room_name]
elif re.search(r"\b(up|upstairs)\b", text) and (room_name, "up") in VERTICAL:
target = VERTICAL[(room_name, "up")]
elif re.search(r"\b(down|downstairs)\b", text) and (room_name, "down") in VERTICAL:
target = VERTICAL[(room_name, "down")]
if target:
route = _path_to(game, room_name, lambda r: r == target)
if route:
path, _ = 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
detail = f"The player moved to the {target}."
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)}."
)
accepted_invitation = (
game.get("friend_follow_target") == target
and game["turn_count"] <= game.get("friend_follow_expires", -1)
)
if (
(FRIEND_FOLLOWS_RE.search(text) or accepted_invitation)
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']}."
game["friend_follow_target"] = None
game["friend_follow_expires"] = -1
return _result("moved", detail)
return _result(
"blocked",
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:
return _result("no_change", f"The player is already in the {room_name}.")
found = _find_feature(text, room)
if found and _mentions(text, TAKE_VERBS):
name, feat = found
item = feat.get("takeable")
if not item:
return _result("blocked", f"The {name} can't be taken.")
if item in game["inventory"]:
return _result("no_change", f"The player already has the {item}.")
game["inventory"].append(item)
return _result("picked_up", f"The player picked up the {item}.")
if found and (
intent not in {"dialogue", "memory"}
or EXAMINE_RE.search(text)
or _mentions(text, TAKE_VERBS)
):
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}",
new_clue,
friend=feat.get("friend"),
)
if LOOK_AROUND_RE.search(text):
nxt = _next_unnoticed(game, room_name)
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, room_name, name, feat)
return _result(
"examined",
f"The player looked around the {room_name}. One thing stands out: {noticed}",
new_clue,
friend=feat.get("friend"),
)
return _result(
"examined",
f"The player looked around the {room_name} again. Nothing new stands out. "
"Describe only what the room description already says, in a fresh way.",
)
return _result(
"no_change",
"Nothing the engine tracks changed. Treat the action as ordinary roleplay "
"(talking, resting, small gestures). Do not add items, clues, or events. "
f"No room change happens: the player is still in the {room_name}, and nobody "
f"leads them elsewhere unless they choose an exit ({', '.join(exit_labels(game, room_name))}).",
)
STAGE_RULES = [
lambda g: g["turns_in_stage"] >= 5 and len(g["visited"]) >= 2, # 0 arrival
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 1, # 1 contradictions
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 4, # 2 paintings
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 7, # 3 sister's warnings
]
def _advance_stage(game):
if game["door_open"] and game["stage"] < 5:
game["stage"] = 5
game["turns_in_stage"] = 0
return True
if game["stage"] >= 4:
return False
if STAGE_RULES[game["stage"]](game):
game["stage"] += 1
game["turns_in_stage"] = 0
return True
return False
def apply_action(game, raw_action):
"""Resolve one player action, update game state, return the engine result."""
game.setdefault("friend_room", "Entrance Hall")
game.setdefault("friend_follow_target", None)
game.setdefault("friend_follow_expires", -1)
ga
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
825,
890
]
}
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
@@ -825,66 +825,66 @@
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
if (
result["outcome"] == "no_change"
and not game["ending"]
and not game["truth_known"]
and game["support_given"] < SUPPORT_LIMIT
and SUPPORT_RE.search(text)
):
game["support_given"] += 1
game["denial"] = max(0, game["denial"] - 1)
result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
if not game["ending"] and not game["truth_known"] and game["turn_count"] >= MAX_TURNS:
result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
if fresh:
game["last_notice_turn"] = game["turn_count"]
if (
not fresh
and result["outcome"] in ("no_change", "examined")
and not game["ending"]
and not game["truth_known"]
and game["room_turns"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_nudge_turn"] >= NUDGE_COOLDOWN
and _next_unnoticed(game, game["room"]) is None
):
nudge = _nudge(game)
if nudge:
result["nudge"] = nudge
game["last_nudge_turn"] = game["turn_count"]
game["nudges_here"] += 1
if result["new_clue"]:
for _ in result["new_clue"].split("; "):
game["denial"] = max(0, game["denial"] - 1)
result["stage_advanced"] = _advance_stage(game)
sister_due = (
not game["sister_seen"]
or game["turn_count"] - game["last_sister_turn"] >= SISTER_COOLDOWN
)
if game["stage"] >= 3 and sister_due and not game["ending"]:
appearance = (
"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."
if not game["sister_seen"]
else "The friend's sister appears briefly again and quietly repeats her "
"warning to leave. Then she is gone."
)
game["sister_seen"] = True
game["last_sister_turn"] = game["turn_count"]
result["sister"] = appearance
game["last_result"] = result
print("Game state:")
pprint(game, sort_dicts=False)
return result
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
12,
30
]
}
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
@@ -12,19 +12,19 @@
import ollama
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
{
"pattern": "**/test_engine.py",
"paths": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers"
}
No files matched the pattern.
*** Begin Patch
*** Update File: the-house-that-remembers/engine.py
@@
FRIEND_INVITATION_RE = re.compile(
- r"\b(?:would you like to come|would you like to join|"
- r"want to come|come with me|...
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
@@ -331,12 +331,18 @@
)
FRIEND_INVITATION_RE = re.compile(
r"\b(?:would you like to come|would you like to join|"
- r"want to come|come with me|join me|shall we go|let'?s go)\b"
+ r"want to come|come with me|join me|shall we go|let'?s go|"
+ r"(?:could|can|shall|should|would)\s+(?:we|you|i)\s+"
+ r"(?:go|head|come|walk)|we could go)\b"
)
ACCEPT_INVITATION_RE = re.compile(
r"^\s*(?:yes|yeah|yep|sure|okay|ok|sounds good|that sounds good|"
r"i(?:'d| would) like that|please|absolutely|of course)\b"
)
+WAIT_FOR_FRIEND_RE = re.compile(
+ r"\b(?:wait|waiting|stay)\b.*\b(?:for\s+)?(?:my\s+)?(?:friend|him)\b"
+ r"|\b(?:wait|waiting)\s+for\s+him\b"
+)
LEAVE_RE = re.compile(
r"\b(leave|exit|flee|escape)\b.*\b(house|mansion|manor)\b"
@@ -697,6 +703,26 @@
if room_name in rooms_named:
return _result("no_change", f"The player is already in the {room_name}.")
+ # --- wait for the friend ------------------------------------------------
+ if WAIT_FOR_FRIEND_RE.search(text) and not negated:
+ if game["friend_room"] == room_name:
+ return _result("no_change", "The friend is already here with the player.")
+ route = _path_to(game, game["friend_room"], lambda r: r == room_name)
+ if not route:
+ return _result(
+ "blocked",
+ f"The friend cannot reach the {room_name} from the "
+ f"{game['friend_room']} right now.",
+ )
+ game["friend_room"] = room_name
+ game["friend_follow_target"] = None
+ game["friend_follow_expires"] = -1
+ path, _ = route
+ detail = f"The friend came from the {path[0]} to join the player in the {room_name}."
+ if len(path) > 1:
+ detail += f" He passed through {', '.join(path[1:-1] + [room_name])}."
+ return _result("friend_arrived", detail)
+
found = _find_feature(text, room)
if found and _mentions(text, TAKE_VERBS):
I dont think it's a good idea to overly rely on specific keywords to be said. The model or the engine should be able to distinguish this. The problem before was that the friend stayed in the library, the player went upstairs, and the model thought the player was talking to the friend, and the friend was responding. So instead of those keywords, let's just say: keep the continuity in mind, or always make sure the friend is in the same room as the player if they want to speak with each other
Understood: no brittle phrase matching. I’ll remove the invitation/wait keyword tracking and make the rule intent-based: when the player starts a conversation while separated from the friend, the engine brings him into the player’s room before the narrator replies.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
255,
390
]
}
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,136 +255,136 @@
"Kitchen": "Entrance Hall",
"Grand Staircase": "Entrance Hall",
"Upper Landing": "Grand Staircase",
"Parents' Bedroom": "Upper Landing",
}
VERTICAL = {
("Entrance Hall", "up"): "Grand Staircase",
("Grand Staircase", "up"): "Upper Landing",
("Grand Staircase", "down"): "Entrance Hall",
("Upper Landing", "down"): "Grand Staircase",
}
def new_game():
return {
"room": "Entrance Hall",
"friend_room": "Entrance Hall",
"friend_follow_target": None,
"friend_follow_expires": -1,
"inventory": ["The Letter"],
"clues": [],
"visited": ["Entrance Hall"],
"examined": {}, # "Room:feature" -> times examined
"stage": 0, # index into prompt.STAGES
"turns_in_stage": 0,
"turn_count": 0,
"room_turns": 0, # turns spent in the current room since arriving
"last_notice_turn": -99, # turn_count when a detail was last surfaced
"last_nudge_turn": -99, # turn_count when the friend was last nudged
"nudges_here": 0, # nudges in this room since last move (escalate)
"last_sister_turn": -99,
"denial": 8, # friend's denial (high = deflecting)
"pushed": 0,
"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"}],
"summary": "", # rolling summary of folded-away history
"summary_idx": 0, # history[:summary_idx] is already summarized
}
def fragments_complete(game):
"""Set of completed fragment kinds ('routine', 'rules', 'night')."""
counts = {}
for clue in game["clues"]:
frag = CLUE_FRAGMENT.get(clue)
if frag:
counts[frag] = counts.get(frag, 0) + 1
return {f for f, n in counts.items() if n >= FRAGMENT_CLUES_NEEDED}
def exit_labels(game, room_name):
"""Exit names for display, marking locked ones."""
room = ROOMS[room_name]
gated = room.get("gated", {})
labels = []
for e in room["exits"]:
flag = gated.get(e)
labels.append(f"{e} (door locked)" if flag and not game[flag] else e)
return labels
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"
)
FRIEND_INVITATION_RE = re.compile(
r"\b(?:would you like to come|would you like to join|"
r"want to come|come with me|join me|shall we go|let'?s go|"
r"(?:could|can|shall|should|would)\s+(?:we|you|i)\s+"
r"(?:go|head|come|walk)|we could go)\b"
)
ACCEPT_INVITATION_RE = re.compile(
r"^\s*(?:yes|yeah|yep|sure|okay|ok|sounds good|that sounds good|"
r"i(?:'d| would) like that|please|absolutely|of course)\b"
)
WAIT_FOR_FRIEND_RE = re.compile(
r"\b(?:wait|waiting|stay)\b.*\b(?:for\s+)?(?:my\s+)?(?:friend|him)\b"
r"|\b(?:wait|waiting)\s+for\s+him\b"
)
LEAVE_RE = re.compile(
r"\b(leave|exit|flee|escape)\b.*\b(house|mansion|manor)\b"
r"|\bgo outside\b|\bwalk out\b"
)
NEGATION_RE = re.compile(r"\b(not|never|without)\b|n't\b|\bwont\b|\bdont\b")
TOGETHER_RE = re.compile(
r"\b(leave|go|walk|get out|come|take him|lead him)\b.*\b(together|with me|with him)\b"
r"|\b(together|with me)\b.*\b(leave|go|out)\b"
r"|\blet'?s (leave|get out)\b"
)
LOOK_AROUND_RE = re.compile(
r"\b(look|glance|scan|survey|search|explore|examine|inspect|study)\w*\b.*\b(around|about|room|surroundings|area)\b"
r"|\b(examine|inspect|search|study|explore|look)\w*\b.*\b(kitchen|library|hall|staircase|landing|bedroom)\b"
r"|\bwhat (do|can) i (see|notice|find)\b|\bnotice anything\b"
)
QUOTED_TEXT_RE = re.compile(r'"[^"]*"|“[^”]*”|‘[^’]*’')
MEMORY_RE = re.compile(
r"\b(remember|recall|used to|when we were (?:kids|children)|"
r"back when we|childhood)\b"
)
EXAMINE_RE = re.compile(
r"\b(examine|inspect|study|look at|check|read|listen to|smell|touch)\b"
)
PHYSICAL_ACTION_RE = re.compile(
r"\b(?:i\s+)?(?:pat|touch|sit|stand|reach|hold|hug|tap|point|"
r"turn|open|close|pick|take|walk|step|move|climb|enter|follow)\b"
)
SPEECH_RE = re.compile(
r"\b(?:i\s+)?(?:say|ask|tell|reply|answer|whisper|speak|call)\b"
r"|\b(?:ask|tell) (?:him|her|your friend)\b"
)
DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b")
DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b")
FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b")
SUPPORT_RE = re.compile(
r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b"
r"|\bit'?s (ok|okay|alright)\b"
)
def _mentions(text, words):
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
615,
780
]
}
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
@@ -615,166 +615,166 @@
" Nothing in the house changes.",
)
def _resolve(game, text):
intent = _classify_intent(text, game)
spoken = text # full text, quoted speech included
text = QUOTED_TEXT_RE.sub("", text).lower() # actions only: speech can't move or examine things
room_name = game["room"]
room = ROOMS[room_name]
rooms_named = _find_rooms(text)
negated = bool(NEGATION_RE.search(text))
spoken_negated = bool(NEGATION_RE.search(spoken))
if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
return _end(game, "leave_together",
"The player and the friend choose to leave the house together.")
if LEAVE_RE.search(text) and not negated:
if room_name == "Entrance Hall":
return _end(game, "escape_alone", "The player walks out of the house alone.")
return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
if not target:
if re.search(r"\bback\b", text) and room_name in BACK:
target = BACK[room_name]
elif re.search(r"\b(up|upstairs)\b", text) and (room_name, "up") in VERTICAL:
target = VERTICAL[(room_name, "up")]
elif re.search(r"\b(down|downstairs)\b", text) and (room_name, "down") in VERTICAL:
target = VERTICAL[(room_name, "down")]
if target:
route = _path_to(game, room_name, lambda r: r == target)
if route:
path, _ = 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
detail = f"The player moved to the {target}."
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)}."
)
accepted_invitation = (
game.get("friend_follow_target") == target
and game["turn_count"] <= game.get("friend_follow_expires", -1)
)
if (
(FRIEND_FOLLOWS_RE.search(text) or accepted_invitation)
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']}."
game["friend_follow_target"] = None
game["friend_follow_expires"] = -1
return _result("moved", detail)
return _result(
"blocked",
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:
return _result("no_change", f"The player is already in the {room_name}.")
if WAIT_FOR_FRIEND_RE.search(text) and not negated:
if game["friend_room"] == room_name:
return _result("no_change", "The friend is already here with the player.")
route = _path_to(game, game["friend_room"], lambda r: r == room_name)
if not route:
return _result(
"blocked",
f"The friend cannot reach the {room_name} from the "
f"{game['friend_room']} right now.",
)
game["friend_room"] = room_name
game["friend_follow_target"] = None
game["friend_follow_expires"] = -1
path, _ = route
detail = f"The friend came from the {path[0]} to join the player in the {room_name}."
if len(path) > 1:
detail += f" He passed through {', '.join(path[1:-1] + [room_name])}."
return _result("friend_arrived", detail)
found = _find_feature(text, room)
if found and _mentions(text, TAKE_VERBS):
name, feat = found
item = feat.get("takeable")
if not item:
return _result("blocked", f"The {name} can't be taken.")
if item in game["inventory"]:
return _result("no_change", f"The player already has the {item}.")
game["inventory"].append(item)
return _result("picked_up", f"The player picked up the {item}.")
if found and (
intent not in {"dialogue", "memory"}
or EXAMINE_RE.search(text)
or _mentions(text, TAKE_VERBS)
):
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}",
new_clue,
friend=feat.get("friend"),
)
if LOOK_AROUND_RE.search(text):
nxt = _next_unnoticed(game, room_name)
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, room_name, name, feat)
return _result(
"examined",
f"The player looked around the {room_name}. One thing stands out: {noticed}",
new_clue,
friend=feat.get("friend"),
)
return _result(
"examined",
f"The player looked around the {room_name} again. Nothing new stands out. "
"Describe only what the room description already says, in a fresh way.",
)
return _result(
"no_change",
"Nothing the engine tracks changed. Treat the action as ordinary roleplay "
"(talking, resting, small gestures). Do not add items, clues, or events. "
f"No room change happens: the player is still in the {room_name}, and nobody "
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
785,
850
]
}
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
@@ -785,66 +785,66 @@
STAGE_RULES = [
lambda g: g["turns_in_stage"] >= 5 and len(g["visited"]) >= 2, # 0 arrival
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 1, # 1 contradictions
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 4, # 2 paintings
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 7, # 3 sister's warnings
]
def _advance_stage(game):
if game["door_open"] and game["stage"] < 5:
game["stage"] = 5
game["turns_in_stage"] = 0
return True
if game["stage"] >= 4:
return False
if STAGE_RULES[game["stage"]](game):
game["stage"] += 1
game["turns_in_stage"] = 0
return True
return False
def apply_action(game, raw_action):
"""Resolve one player action, update game state, return the engine result."""
game.setdefault("friend_room", "Entrance Hall")
game.setdefault("friend_follow_target", None)
game.setdefault("friend_follow_expires", -1)
game.setdefault("sister_seen", False)
game.setdefault("last_sister_turn", -99)
_record_accepted_invitation(game, raw_action)
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
result = _resolve(game, text)
if result["outcome"] == "moved":
intent = "movement"
elif result["outcome"] == "picked_up":
intent = "physical_action"
elif result["outcome"] == "examined" and intent not in {
"examination",
"exploration",
}:
intent = "examination"
result["intent"] = intent
game["turn_count"] += 1
game["turns_in_stage"] += 1
if result["outcome"] != "moved":
game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
12,
42
]
}
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
@@ -12,31 +12,31 @@
import ollama
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. If he is elsewhere, he cannot hear or answer the player; do not write his dialogue or place him nearby unless the ENGINE RESULT moves him.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. If asked directly about the past, characters answer in fragments, contradict themselves, or change the subject.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good."
"Do you remember how we used to help in the kitchen? I was never much good at it."
"It's strange being back in the library. We used to spend whole afternoons in here."
"I haven't been sleeping much. The house is loud at night."
"Do you want tea? There's... yes. Tea. I can do that."
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\tests\\test_engine.py",
"view_range": [
1,
120
]
}
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
@@ -1,120 +1,120 @@
import contextlib
import io
import json
import unittest
from pathlib import Path
import engine
from prompt import _engine_note, 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_friend_follows_after_player_accepts_his_room_invitation(self):
game = engine.new_game()
game["history"].append(
{
"role": "assistant",
"content": "Would you like to come to the kitchen for tea?",
}
)
game["history"].append({"role": "user", "content": "Yes, that sounds good."})
act(game, "Yes, that sounds good.")
self.assertEqual(game["friend_follow_target"], "Kitchen")
game["history"].append(
{
"role": "assistant",
"content": "The kitchen is just down the hall.",
}
)
game["history"].append({"role": "user", "content": "I go to the kitchen."})
result = act(game, "I go to the kitchen.")
self.assertEqual(game["room"], "Kitchen")
self.assertEqual(game["friend_room"], "Kitchen")
self.assertIn("The friend accompanied the player.", result["detail"])
self.assertIsNone(game["friend_follow_target"])
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"])
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)
self.assertIn("his name is not established", prompt)
self.assertIn("do not call him 'E.'", prompt)
{
"command": "git status --short; git --no-pager diff -- .gitignore",
"description": "Confirm existing worktree changes"
}
M .gitignore
M README.md
M the-house-that-remembers/app.py
M the-house-that-remembers/engine.py
M the-house-that-remembers/prompt.py
M the-house-that-remembers/story/canon.json
diff --git a/.gitignore b/.gitignore
index 2ab0d58..1f317a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,4 +2,5 @@
__pycache__/
*.py[cod]
.streamlit/secrets.toml
-temp/
\ No newline at end of file
+temp/
+tests/
\ No newline at end of file
<shellId: 20 completed with exit code 0>
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
245,
430
]
}
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
@@ -245,186 +245,186 @@
text: frag
for room in ROOMS.values()
for feat in room.get("features", {}).values()
for (text, _n, frag) in feat.get("clues", [])
if frag
}
BACK = {
"Library": "Entrance Hall",
"Kitchen": "Entrance Hall",
"Grand Staircase": "Entrance Hall",
"Upper Landing": "Grand Staircase",
"Parents' Bedroom": "Upper Landing",
}
VERTICAL = {
("Entrance Hall", "up"): "Grand Staircase",
("Grand Staircase", "up"): "Upper Landing",
("Grand Staircase", "down"): "Entrance Hall",
("Upper Landing", "down"): "Grand Staircase",
}
def new_game():
return {
"room": "Entrance Hall",
"friend_room": "Entrance Hall",
"friend_follow_target": None,
"friend_follow_expires": -1,
"inventory": ["The Letter"],
"clues": [],
"visited": ["Entrance Hall"],
"examined": {}, # "Room:feature" -> times examined
"stage": 0, # index into prompt.STAGES
"turns_in_stage": 0,
"turn_count": 0,
"room_turns": 0, # turns spent in the current room since arriving
"last_notice_turn": -99, # turn_count when a detail was last surfaced
"last_nudge_turn": -99, # turn_count when the friend was last nudged
"nudges_here": 0, # nudges in this room since last move (escalate)
"last_sister_turn": -99,
"denial": 8, # friend's denial (high = deflecting)
"pushed": 0,
"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"}],
"summary": "", # rolling summary of folded-away history
"summary_idx": 0, # history[:summary_idx] is already summarized
}
def fragments_complete(game):
"""Set of completed fragment kinds ('routine', 'rules', 'night')."""
counts = {}
for clue in game["clues"]:
frag = CLUE_FRAGMENT.get(clue)
if frag:
counts[frag] = counts.get(frag, 0) + 1
return {f for f, n in counts.items() if n >= FRAGMENT_CLUES_NEEDED}
def exit_labels(game, room_name):
"""Exit names for display, marking locked ones."""
room = ROOMS[room_name]
gated = room.get("gated", {})
labels = []
for e in room["exits"]:
flag = gated.get(e)
labels.append(f"{e} (door locked)" if flag and not game[flag] else e)
return labels
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"
)
FRIEND_INVITATION_RE = re.compile(
r"\b(?:would you like to come|would you like to join|"
r"want to come|come with me|join me|shall we go|let'?s go|"
r"(?:could|can|shall|should|would)\s+(?:we|you|i)\s+"
r"(?:go|head|come|walk)|we could go)\b"
)
ACCEPT_INVITATION_RE = re.compile(
r"^\s*(?:yes|yeah|yep|sure|okay|ok|sounds good|that sounds good|"
r"i(?:'d| would) like that|please|absolutely|of course)\b"
)
WAIT_FOR_FRIEND_RE = re.compile(
r"\b(?:wait|waiting|stay)\b.*\b(?:for\s+)?(?:my\s+)?(?:friend|him)\b"
r"|\b(?:wait|waiting)\s+for\s+him\b"
)
LEAVE_RE = re.compile(
r"\b(leave|exit|flee|escape)\b.*\b(house|mansion|manor)\b"
r"|\bgo outside\b|\bwalk out\b"
)
NEGATION_RE = re.compile(r"\b(not|never|without)\b|n't\b|\bwont\b|\bdont\b")
TOGETHER_RE = re.compile(
r"\b(leave|go|walk|get out|come|take him|lead him)\b.*\b(together|with me|with him)\b"
r"|\b(together|with me)\b.*\b(leave|go|out)\b"
r"|\blet'?s (leave|get out)\b"
)
LOOK_AROUND_RE = re.compile(
r"\b(look|glance|scan|survey|search|explore|examine|inspect|study)\w*\b.*\b(around|about|room|surroundings|area)\b"
r"|\b(examine|inspect|search|study|explore|look)\w*\b.*\b(kitchen|library|hall|staircase|landing|bedroom)\b"
r"|\bwhat (do|can) i (see|notice|find)\b|\bnotice anything\b"
)
QUOTED_TEXT_RE = re.compile(r'"[^"]*"|“[^”]*”|‘[^’]*’')
MEMORY_RE = re.compile(
r"\b(remember|recall|used to|when we were (?:kids|children)|"
r"back when we|childhood)\b"
)
EXAMINE_RE = re.compile(
r"\b(examine|inspect|study|look at|check|read|listen to|smell|touch)\b"
)
PHYSICAL_ACTION_RE = re.compile(
r"\b(?:i\s+)?(?:pat|touch|sit|stand|reach|hold|hug|tap|point|"
r"turn|open|close|pick|take|walk|step|move|climb|enter|follow)\b"
)
SPEECH_RE = re.compile(
r"\b(?:i\s+)?(?:say|ask|tell|reply|answer|whisper|speak|call)\b"
r"|\b(?:ask|tell) (?:him|her|your friend)\b"
)
DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b")
DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b")
FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b")
SUPPORT_RE = re.compile(
r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b"
r"|\bit'?s (ok|okay|alright)\b"
)
def _mentions(text, words):
return any(re.search(rf"\b{re.escape(w)}\b", text) for w in words)
def _find_rooms(text):
return [name for name, r in ROOMS.items() if _mentions(text, r["aliases"])]
def _record_accepted_invitation(game, raw_action):
"""Remember a nearby destination when the player accepts the friend's offer."""
action = raw_action.strip().strip("\"'“”‘’").lower()
if not ACCEPT_INVITATION_RE.search(action) or NEGATION_RE.search(action):
return
previous_assistant = next(
(
message["content"]
for message in reversed(game["history"][:-1])
if message["role"] == "assistant"
),
"",
)
if not FRIEND_INVITATION_RE.search(previous_assistant.lower()):
return
destinations = _find_rooms(previous_assistant.lower())
if len(destinations) == 1 and destinations[0] != game["room"]:
game["friend_follow_target"] = destinations[0]
game["friend_follow_expires"] = game["turn_count"] + 5
def _find_feature(text, room):
for name, feat in room.get("features", {}).items():
if _mentions(text, feat["keywords"]):
return name, feat
return None
def _end(game, ending, detail):
game["ending"] = ending
return _result("ending", detail)
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
790,
885
]
}
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
@@ -790,96 +790,96 @@
lambda g: g["turns_in_stage"] >= 5 and len(g["visited"]) >= 2, # 0 arrival
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 1, # 1 contradictions
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 4, # 2 paintings
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 7, # 3 sister's warnings
]
def _advance_stage(game):
if game["door_open"] and game["stage"] < 5:
game["stage"] = 5
game["turns_in_stage"] = 0
return True
if game["stage"] >= 4:
return False
if STAGE_RULES[game["stage"]](game):
game["stage"] += 1
game["turns_in_stage"] = 0
return True
return False
def apply_action(game, raw_action):
"""Resolve one player action, update game state, return the engine result."""
game.setdefault("friend_room", "Entrance Hall")
game.setdefault("friend_follow_target", None)
game.setdefault("friend_follow_expires", -1)
game.setdefault("sister_seen", False)
game.setdefault("last_sister_turn", -99)
_record_accepted_invitation(game, raw_action)
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
result = _resolve(game, text)
if result["outcome"] == "moved":
intent = "movement"
elif result["outcome"] == "picked_up":
intent = "physical_action"
elif result["outcome"] == "examined" and intent not in {
"examination",
"exploration",
}:
intent = "examination"
result["intent"] = intent
game["turn_count"] += 1
game["turns_in_stage"] += 1
if result["outcome"] != "moved":
game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
if (
result["outcome"] == "no_change"
and not game["ending"]
and not game["truth_known"]
and game["support_given"] < SUPPORT_LIMIT
and SUPPORT_RE.search(text)
):
game["support_given"] += 1
game["denial"] = max(0, game["denial"] - 1)
result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
if not game["ending"] and not game["truth_known"] and game["turn_count"] >= MAX_TURNS:
result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
if fresh:
game["last_notice_turn"] = game["turn_count"]
if (
not fresh
and result["outcome"] in ("no_change", "examined")
and not game["ending"]
and not game["truth_known"]
and game["room_turns"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_nudge_turn"] >= NUDGE_COOLDOWN
and _next_unnoticed(game, game["room"]) is None
):
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
15,
30
]
}
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
@@ -15,16 +15,16 @@
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
165,
215
]
}
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
@@ -165,51 +165,51 @@
"helping the friend with kitchen chores; exploring the family library; "
"sleeping over in the guest room.",
]
if game["summary"]:
lines.append(f"Earlier in the story: {game['summary']}")
return "\n".join(lines)
def _engine_note(game):
r = game["last_result"]
if not r:
return ""
note = (
"\n\n[ENGINE RESULT - authoritative]\n"
f"Player intent: {r.get('intent', 'roleplay')}\n"
f"Outcome: {r['outcome']}\n"
f"Detail: {r['detail']}"
)
if r.get("ambient"):
note += (
"\nAmbient detail to weave into this turn in one quiet sentence, "
f"without making it a big moment: {r['ambient']}"
)
if r.get("nudge"):
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']}"
friend_is_here = game.get("friend_room", "Entrance Hall") == 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']}"
intent_reminders = {
"dialogue": (
"If the friend is in this room, treat the player's words as spoken to him and answer "
"directly in his natural, restrained voice. If he is elsewhere, do not invent a reply."
),
"memory": (
"Answer the player's memory question as their character, using only the established "
"player memories in CURRENT STATE or facts already established in conversation. "
"Do not invent a specific shared event or reveal hidden family history."
),
"physical_action": (
"Treat this as a physical action by the player. Acknowledge what they actually did "
"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 only if he is in this room. Do not add actions for the player "
"or invent a reply from elsewhere."
*** Begin Patch
*** Update File: the-house-that-remembers/engine.py
@@
return {
"room": "Entrance Hall",
"friend_room": "Entrance Hall",
- "friend_follow_target": None,
-...
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
@@ -271,8 +271,6 @@
return {
"room": "Entrance Hall",
"friend_room": "Entrance Hall",
- "friend_follow_target": None,
- "friend_follow_expires": -1,
"inventory": ["The Letter"],
"clues": [],
"visited": ["Entrance Hall"],
@@ -324,25 +322,6 @@
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"
-)
-FRIEND_INVITATION_RE = re.compile(
- r"\b(?:would you like to come|would you like to join|"
- r"want to come|come with me|join me|shall we go|let'?s go|"
- r"(?:could|can|shall|should|would)\s+(?:we|you|i)\s+"
- r"(?:go|head|come|walk)|we could go)\b"
-)
-ACCEPT_INVITATION_RE = re.compile(
- r"^\s*(?:yes|yeah|yep|sure|okay|ok|sounds good|that sounds good|"
- r"i(?:'d| would) like that|please|absolutely|of course)\b"
-)
-WAIT_FOR_FRIEND_RE = re.compile(
- r"\b(?:wait|waiting|stay)\b.*\b(?:for\s+)?(?:my\s+)?(?:friend|him)\b"
- r"|\b(?:wait|waiting)\s+for\s+him\b"
-)
LEAVE_RE = re.compile(
r"\b(leave|exit|flee|escape)\b.*\b(house|mansion|manor)\b"
@@ -395,29 +374,6 @@
return [name for name, r in ROOMS.items() if _mentions(text, r["aliases"])]
-def _record_accepted_invitation(game, raw_action):
- """Remember a nearby destination when the player accepts the friend's offer."""
- action = raw_action.strip().strip("\"'“”‘’").lower()
- if not ACCEPT_INVITATION_RE.search(action) or NEGATION_RE.search(action):
- return
-
- previous_assistant = next(
- (
- message["content"]
- for message in reversed(game["history"][:-1])
- if message["role"] == "assistant"
- ),
- "",
- )
- if not FRIEND_INVITATION_RE.search(previous_assistant.lower()):
- return
-
- destinations = _find_rooms(previous_assistant.lower())
- if len(destinations) == 1 and destinations[0] != game["room"]:
- game["friend_follow_target"] = destinations[0]
- game["friend_follow_expires"] = game["turn_count"] + 5
-
-
def _find_feature(text, room):
for name, feat in room.get("features", {}).items():
if _mentions(text, feat["keywords"]):
@@ -680,20 +636,8 @@
detail += (
f" First visit to: {', '.join(first_time_rooms)}."
)
- accepted_invitation = (
- game.get("friend_follow_target") == target
- and game["turn_count"] <= game.get("friend_follow_expires", -1)
- )
- if (
- (FRIEND_FOLLOWS_RE.search(text) or accepted_invitation)
- and game["friend_room"] == room_name
- ):
- game["friend_room"] = target
- detail += " The friend accompanied the player."
- elif game["friend_room"] != target:
+ if game["friend_room"] != target:
detail += f" The friend remains in the {game['friend_room']}."
- game["friend_follow_target"] = None
- game["friend_follow_expires"] = -1
return _result("moved", detail)
return _result(
"blocked",
@@ -703,26 +647,6 @@
if room_name in rooms_named:
return _result("no_change", f"The player is already in the {room_name}.")
- # --- wait for the friend ------------------------------------------------
- if WAIT_FOR_FRIEND_RE.search(text) and not negated:
- if game["friend_room"] == room_name:
- return _result("no_change", "The friend is already here with the player.")
- route = _path_to(game, game["friend_room"], lambda r: r == room_name)
- if not route:
- return _result(
- "blocked",
- f"The friend cannot reach the {room_name} from the "
- f"{game['friend_room']} right now.",
- )
- game["friend_room"] = room_name
- game["friend_follow_target"] = None
- game["friend_follow_expires"] = -1
- path, _ = route
- detail = f"The friend came from the {path[0]} to join the player in the {room_name}."
- if len(path) > 1:
- detail += f" He passed through {', '.join(path[1:-1] + [room_name])}."
- return _result("friend_arrived", detail)
-
found = _find_feature(text, room)
if found and _mentions(text, TAKE_VERBS):
@@ -811,11 +735,8 @@
def apply_action(game, raw_action):
"""Resolve one player action, update game state, return the engine result."""
game.setdefault("friend_room", "Entrance Hall")
- game.setdefault("friend_follow_target", None)
- game.setdefault("friend_follow_expires", -1)
game.setdefault("sister_seen", False)
game.setdefault("last_sister_turn", -99)
- _record_accepted_invitation(game, raw_action)
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
@@ -830,6 +751,17 @@
}:
intent = "examination"
result["intent"] = intent
+ if (
+ intent in {"dialogue", "action_and_dialogue"}
+ and not game["ending"]
+ and game["friend_room"] != game["room"]
+ ):
+ game["friend_room"] = game["room"]
+ result["friend_arrived"] = True
+ result["detail"] += (
+ f" The friend joins the player in the {game['room']} so they can speak "
+ "face to face."
+ )
game["turn_count"] += 1
game["turns_in_stage"] += 1
*** Begin Patch
*** Update File: the-house-that-remembers/prompt.py
@@
-- The friend's location in CURRENT STATE is authoritative. If he is elsewhere, he cannot hear or answer the player; do not write...
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
@@ -28,7 +28,7 @@
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
-- The friend's location in CURRENT STATE is authoritative. If he is elsewhere, he cannot hear or answer the player; do not write his dialogue or place him nearby unless the ENGINE RESULT moves him.
+- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT brings him into the player's room; narrate that arrival before his reply.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. If asked directly about the past, characters answer in fragments, contradict themselves, or change the subject.
@@ -194,10 +194,13 @@
note += f"\nFriend's likely reaction (optional): {r['friend']}"
if r.get("sister"):
note += f"\nSister's appearance to narrate this turn: {r['sister']}"
+ if r.get("friend_arrived"):
+ note += "\nThe friend has joined the player to speak face to face; narrate his arrival before his response."
intent_reminders = {
"dialogue": (
- "If the friend is in this room, treat the player's words as spoken to him and answer "
- "directly in his natural, restrained voice. If he is elsewhere, do not invent a reply."
+ "Treat the player's words as spoken to the friend and answer directly in his natural, "
+ "restrained voice. If the ENGINE RESULT says he joined the player, narrate his arrival "
+ "before the reply."
),
"memory": (
"Answer the player's memory question as their character, using only the established "
@@ -211,8 +214,8 @@
),
"action_and_dialogue": (
"Preserve both parts of the player's input: acknowledge their physical action and "
- "have the friend respond only if he is in this room. Do not add actions for the player "
- "or invent a reply from elsewhere."
+ "have the friend respond. If the ENGINE RESULT says he joined the player, narrate his "
+ "arrival before the reply. Do not add actions for the player."
),
"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
@@
- def test_friend_can_accompany_player_when_invited(self):
- game = engine.new_game()
- game["room"] ...
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
@@ -36,43 +36,19 @@
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):
+ def test_friend_joins_player_for_conversation_without_keyword_matching(self):
game = engine.new_game()
- game["room"] = "Library"
+ game["room"] = "Upper Landing"
game["friend_room"] = "Library"
- result = act(game, "go to the upper landing with me")
+ result = act(game, "Can I sit?")
+ self.assertEqual(result["intent"], "dialogue")
+ self.assertEqual(game["room"], "Upper Landing")
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_friend_follows_after_player_accepts_his_room_invitation(self):
- game = engine.new_game()
- game["history"].append(
- {
- "role": "assistant",
- "content": "Would you like to come to the kitchen for tea?",
- }
- )
- game["history"].append({"role": "user", "content": "Yes, that sounds good."})
-
- act(game, "Yes, that sounds good.")
- self.assertEqual(game["friend_follow_target"], "Kitchen")
-
- game["history"].append(
- {
- "role": "assistant",
- "content": "The kitchen is just down the hall.",
- }
- )
- game["history"].append({"role": "user", "content": "I go to the kitchen."})
- result = act(game, "I go to the kitchen.")
-
- self.assertEqual(game["room"], "Kitchen")
- self.assertEqual(game["friend_room"], "Kitchen")
- self.assertIn("The friend accompanied the player.", result["detail"])
- self.assertIsNone(game["friend_follow_target"])
+ self.assertTrue(result["friend_arrived"])
+ self.assertIn("face to face", result["detail"])
+ self.assertIn("joined the player", _engine_note(game))
def test_examining_takeable_features_adds_items_to_inventory(self):
game = engine.new_game()
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m unittest discover -s tests -v","description":"Run interaction continuity regression...
test_examining_takeable_features_adds_items_to_inventory (test_engine.GameEngineTests.test_examining_takeable_features_adds_items_to_inventory) ... ok
test_friend_joins_player_for_conversation_without_keyword_matching (test_engine.GameEngineTests.test_friend_joins_player_for_conversation_without_keyword_matching) ... 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.003s
OK
<shellId: 22 completed with exit code 0>
{"fileUri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py","workspaceRoot":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remember...
No syntax errors found in 'file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py'
{"fileUri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py","workspaceRoot":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remember...
No syntax errors found in 'file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py'
{
"command": "git --no-pager diff --check; git status --short; git --no-pager diff --stat",
"description": "Check diff cleanliness and modified files"
}
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
the-house-that-remembers/engine.py:116: trailing whitespace.
+
the-house-that-remembers/engine.py:238: trailing whitespace.
+
M .gitignore
M README.md
M the-house-that-remembers/app.py
M the-house-that-remembers/engine.py
M the-house-that-remembers/prompt.py
M the-house-that-remembers/story/canon.json
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
.gitignore | 3 +-
README.md | 3 +-
the-house-that-remembers/app.py | 4 +
the-house-that-remembers/engine.py | 121 ++++++++++++++++++++++--------
the-house-that-remembers/prompt.py | 54 +++++++++----
the-house-that-remembers/story/canon.json | 4 +-
6 files changed, 141 insertions(+), 48 deletions(-)
<shellId: 21 completed with exit code 0>
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
108,
121
]
}
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
@@ -108,14 +108,14 @@
"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.",
"The page hasn't changed. The ink is faded, but the passage is still legible.",
],
"clues": [("The house's first owner buried his sister alive and died soon after; the house stood empty for years", 1, None)],
},
},
},
"Kitchen": {
"description": (
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
230,
244
]
}
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
@@ -230,15 +230,15 @@
"img": "bedroom.jpg",
"aliases": ["parents' bedroom", "parents bedroom", "parent's bedroom", "parents' room",
"parents room", "master bedroom", "bedroom"],
"features": {
"bed": {
"keywords": ["bed", "figures", "family", "mother", "father", "sister", "parents"],
"looks": ["Three figures lie together on the bed, turned toward one another, as if asleep. None of them breathes, and nothing about them has changed at all."],
},
},
},
}
CLUE_FRAGMENT = {
{
"command": "git --no-pager diff -- the-house-that-remembers/engine.py the-house-that-remembers/prompt.py",
"description": "Inspect substantive engine and prompt diff"
}
Output too large to read at once (23.2 KB). Saved to: C:\Users\lucia\AppData\Local\Temp\1793210921584-copilot-tool-output-8656-786ae14a-2156-4ffa-a945-efcc5d743279.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):
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
diff --git a/the-house-that-remembers/engine.py b/the-house-that-remembers/engine.py
index 976ebe9..c6e90ea 100644
--- a/the-house-that-remembers/engine.py
+++ b/the-house-that-remembers/engine.py
@@ -8,15 +8,16 @@ from pprint import pprint
<shellId: 23 completed with exit code 0>
*** Begin Patch
*** Update File: the-house-that-remembers/engine.py
@@
],
"clues": [("The house's first owner buried his sister alive and died soon after; the house s...
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
@@ -113,7 +113,7 @@
"The page hasn't changed. The ink is faded, but the passage is still legible.",
],
"clues": [("The house's first owner buried his sister alive and died soon after; the house stood empty for years", 1, None)],
-
+
},
},
},
@@ -235,7 +235,7 @@
"keywords": ["bed", "figures", "family", "mother", "father", "sister", "parents"],
"looks": ["Three figures lie together on the bed, turned toward one another, as if asleep. None of them breathes, and nothing about them has changed at all."],
},
-
+
},
},
}
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m unittest discover -s tests -v","description":"Run final continuity regression tests...
test_examining_takeable_features_adds_items_to_inventory (test_engine.GameEngineTests.test_examining_takeable_features_adds_items_to_inventory) ... ok
test_friend_joins_player_for_conversation_without_keyword_matching (test_engine.GameEngineTests.test_friend_joins_player_for_conversation_without_keyword_matching) ... 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.003s
OK
<shellId: 24 completed with exit code 0>
{
"command": "git --no-pager diff --check; git status --short; git --no-pager diff --stat",
"description": "Verify final diff cleanliness"
}
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
M .gitignore
M README.md
M the-house-that-remembers/app.py
M the-house-that-remembers/engine.py
M the-house-that-remembers/prompt.py
M the-house-that-remembers/story/canon.json
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
.gitignore | 3 +-
README.md | 3 +-
the-house-that-remembers/app.py | 4 +
the-house-that-remembers/engine.py | 121 ++++++++++++++++++++++--------
the-house-that-remembers/prompt.py | 54 +++++++++----
the-house-that-remembers/story/canon.json | 4 +-
6 files changed, 141 insertions(+), 48 deletions(-)
<shellId: 25 completed with exit code 0>
{"pattern":"FRIEND_FOLLOWS_RE|FRIEND_INVITATION_RE|ACCEPT_INVITATION_RE|WAIT_FOR_FRIEND_RE|friend_follow_target|friend_follow_expires|_record_accepted_invitation","paths":"C:\\Users\\lucia\\Documents\...
No matches found.
Changed this to be intent-based rather than phrase-based. When the engine classifies an action as dialogue or action-plus-dialogue and the friend is in another room, it updates his location to the player’s room and tells the narrator to describe him joining before responding. Ordinary movement still leaves his location unchanged.
Removed the invitation and “wait for friend” keyword tracking. Updated the dialogue guidance and regression test in prompt.py and test_engine.py. All 6 tests pass, and git diff --check is clean.
'content': 'A musty smell lingers in the air, a mixture of old '
'wood and dust. He nods slowly, his gaze distant for '
'a moment. "It was quiet back then. Before things... '
'before everything got so complicated." He hesitates, '
'then gestures vaguely toward the exits. "We could go '
'anywhere. The kitchen, or up to the stairs..."'},
{'role': 'user', 'content': '"Sure"'},
{'role': 'assistant',
'content': 'He seems relieved by your simple agreement. "It\'s '
'just... a big place," he murmurs, running a hand '
'over the smooth, dark wood of a nearby banister. '
'"Maybe we could just sit for a bit? I was thinking '
'tea might help." He looks at you expectantly.'},
{'role': 'user',
'content': '"Yes, tea sounds nice. It is really cold outside. A '
'nice hot cup of tea would make us feel better, I '
'think"'}],
Geez, why would he suggest going upstairs lol. The kitchen/tea should be natural follow up
I agree—an upstairs suggestion is arbitrary here, especially when tea and the kitchen are already established. I’ll tighten the friend’s guidance so suggestions follow the player’s immediate needs and the story context, rather than offering destinations as a menu.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
12,
72
]
}
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
@@ -12,61 +12,61 @@
import ollama
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT brings him into the player's room; narrate that arrival before his reply.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. If asked directly about the past, characters answer in fragments, contradict themselves, or change the subject.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good."
"Do you remember how we used to help in the kitchen? I was never much good at it."
"It's strange being back in the library. We used to spend whole afternoons in here."
"I haven't been sleeping much. The house is loud at night."
"Do you want tea? There's... yes. Tea. I can do that."
"I miss the old days. Now, everything is so... different. I don't know how to explain it."
"What have you been doing all these years? I mean... if you want to tell me.\""""
STAGES = [
{ # 0 - Arrival and uneasy normality
"now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet. Friend invited the player to the house because he is lonely and the history of the house is weighing on him. He is not aware of the truth, but he doesn't want to be alone.",
"friend": "He tries to act normal and spend ordinary time together. He can ask about the player's life or mention an established childhood memory. He deflects direct questions about the house and does not bring up family members on his own.",
"steer": "Let the player settle in and explore. When the friend invites them somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
},
{ # 1 - Contradictions
"now": "Details about the friend's family do not quite add up.",
"friend": "He mentions his family casually, in the present tense, as if they are alive and nearby: his mother is resting, his father is busy upstairs, his sister is 'around'. He does not notice anything odd. If asked to see them, or asked directly about them, he answers vaguely and changes the subject.",
"steer": "Let this come up once or twice in passing, e.g. \"Mum's resting. She hasn't been sleeping well.\" Do not explain it.",
},
{ # 2 - Frozen house and family traces
"now": "The house is frozen in time: clocks are stopped, calendars are out of date, food never spoils, and places are set for people who are not there. Portraits can change between looks. The friend has lost track of time.",
"friend": "He says time is 'strange' or that he has lost count of the days. He grows uneasy when stopped clocks, old dates or the extra place settings are mentioned and tries to move on.",
"steer": "Describe frozen or changed details only when the engine provides them. Otherwise keep the house quietly wrong: a sound repeating, a door slightly off.",
},
{ # 3 - Sister's warnings
"now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.",
"friend": "He does not see her, or insists the player imagined it.",
"steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most, then gone. She speaks gently and urgently. She never threatens. Do not say what happened to the friend's family.",
},
{ # 4 - Parents' fragments
"now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.",
"friend": "He flinches at the knocking and tells the player, \"Whatever you do, don't answer it.\" Fragments of memory slip out, then he retreats.",
"steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.",
},
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
115,
155
]
}
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
@@ -115,41 +115,41 @@
"- Refer to him as 'your friend' or 'he'; do not call him 'E.' as though that were his name.\n"
f"- Condition: {f['initial_state']}\n"
f"- Personality: {f['personality']}\n"
f"- Voice: {f['voice']}\n"
f"- Early behavior: {f['early_behavior']}\n"
"- Example lines (for tone only; do not repeat them verbatim):\n"
f"{FRIEND_EXAMPLES}\n\n"
"THE LETTER HE SENT\n"
f"{canon['opening']['letter_text']}"
)
def stage_block(game):
if game["ending"]:
text = ENDINGS[game["ending"]]
if game["ending"] == "escape_alone":
text += (
" The player saw the truth in the bedroom before leaving; the friend stays behind with his family."
if game["truth_known"]
else " The player never learned what was upstairs; the friend stays behind in the house."
)
return "ENDING\n" + text
s = STAGES[game["stage"]]
return (
f"STORY NOW: {s['now']}\n"
f"FRIEND RIGHT NOW: {s['friend']}\n"
f"FRIEND'S CONDITION: {denial_band(game['denial'])}\n"
f"STEER TOWARD (gently, never force): {s['steer']}\n"
f"Turns spent in this part of the story: {game['turns_in_stage']}"
)
def state_block(game):
from engine import ROOMS, exit_labels # local import avoids a cycle if engine grows
room = ROOMS[game["room"]]
friend_room = game.get("friend_room", "Entrance Hall")
lines = [
"CURRENT STATE",
f"Room: {game['room']}",
f"Room description: {room['description'].strip()}",
{"pattern":"tea|kitchen|upstairs|suggest|small talk|conversation|friend","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py","C:\\Users\\lucia\\Documen...
Output too large to read at once (26.0 KB). Saved to: C:\Users\lucia\AppData\Local\Temp\copilot-tool-output-1793211192414-5.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: "friend": "He says it only needs winding, then changes the subject.",
C:\Users\lucia
*** Begin Patch
*** Update File: the-house-that-remembers/prompt.py
@@
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request...
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
@@ -24,6 +24,7 @@
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
+- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes.
@@ -39,7 +40,7 @@
"Do you remember how we used to help in the kitchen? I was never much good at it."
"It's strange being back in the library. We used to spend whole afternoons in here."
"I haven't been sleeping much. The house is loud at night."
- "Do you want tea? There's... yes. Tea. I can do that."
+ "Tea sounds good. The kitchen's just this way, if you'd like."
"I miss the old days. Now, everything is so... different. I don't know how to explain it."
"What have you been doing all these years? I mean... if you want to tell me.\""""
@@ -48,7 +49,7 @@
{ # 0 - Arrival and uneasy normality
"now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet. Friend invited the player to the house because he is lonely and the history of the house is weighing on him. He is not aware of the truth, but he doesn't want to be alone.",
"friend": "He tries to act normal and spend ordinary time together. He can ask about the player's life or mention an established childhood memory. He deflects direct questions about the house and does not bring up family members on his own.",
- "steer": "Let the player settle in and explore. When the friend invites them somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
+ "steer": "Let the player settle in and explore. Keep the friend's suggestions tied to what the player just said; if tea or a drink is wanted, the kitchen is the natural next step. Do not offer upstairs or list destinations without a reason. When the friend invites the player somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
},
{ # 1 - Contradictions
"now": "Details about the friend's family do not quite add up.",
{"fileUri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py","workspaceRoot":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remember...
No syntax errors found in 'file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py'
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m unittest discover -s tests -v","description":"Run game regression suite","initial_w...
test_examining_takeable_features_adds_items_to_inventory (test_engine.GameEngineTests.test_examining_takeable_features_adds_items_to_inventory) ... ok
test_friend_joins_player_for_conversation_without_keyword_matching (test_engine.GameEngineTests.test_friend_joins_player_for_conversation_without_keyword_matching) ... 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.003s
OK
<shellId: 27 completed with exit code 0>
{
"command": "git --no-pager diff --check; git status --short; git --no-pager diff --stat",
"description": "Check prompt change and worktree status"
}
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
M .gitignore
M README.md
M the-house-that-remembers/app.py
M the-house-that-remembers/engine.py
M the-house-that-remembers/prompt.py
M the-house-that-remembers/story/canon.json
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
.gitignore | 3 +-
README.md | 3 +-
the-house-that-remembers/app.py | 4 +
the-house-that-remembers/engine.py | 121 ++++++++++++++++++++++--------
the-house-that-remembers/prompt.py | 55 ++++++++++----
the-house-that-remembers/story/canon.json | 4 +-
6 files changed, 142 insertions(+), 48 deletions(-)
<shellId: 26 completed with exit code 0>
Agreed—the kitchen is the natural follow-up when tea comes up, not an arbitrary suggestion to go upstairs. I updated the shared conversation guidance and Stage 0 steer in prompt.py: suggestions should follow the player’s cue, and upstairs should only come up when there’s a reason. I also changed the tea example to model that flow.
All 6 regression tests pass, and the prompt has no syntax errors.
{"pattern":"friend_arrived|intent in \\{\"dialogue\"|friend_room\\] != game\\[\"room\"\\]|game\\[\"friend_room\"\\] = game\\[\"room\"\\]|def _classify_intent|has_speech","paths":"C:\\Users\\lucia\\Doc...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:389:def _classify_intent(raw_action, game):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:402: has_speech = has_quoted_speech or bool(SPEECH_RE.search(action_text))
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:404: if has_speech and has_physical_action:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:406: if has_speech or raw_action.strip().endswith("?"):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:755: intent in {"dialogue", "action_and_dialogue"}
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:759: game["friend_room"] = game["room"]
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:760: result["friend_arrived"] = True
{"pattern":"support_given|SUPPORT_LIMIT|SUPPORT_RE|Kindness lowers","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py","output_mode":"content","-n":tru...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:16:SUPPORT_LIMIT = 3 # times kindness can lower his denial
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:288: "support_given": 0,
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:364:SUPPORT_RE = re.compile(
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:786: # Kindness lowers his guard a little (a few times only).
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:791: and game["support_given"] < SUPPORT_LIMIT
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:792: and SUPPORT_RE.search(text)
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:794: game["support_given"] += 1
add things like:
We can do it together, it is going to be okay, we can face... i want to help / i can help / take my hand, give me your hand /
I’ll add those supportive phrases to the engine’s existing support matcher, including common wording variants, then run a focused check that they trigger the kindness effect.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
356,
370
]
}
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
@@ -356,15 +356,15 @@
r"\b(?:i\s+)?(?:say|ask|tell|reply|answer|whisper|speak|call)\b"
r"|\b(?:ask|tell) (?:him|her|your friend)\b"
)
DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b")
DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b")
FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b")
SUPPORT_RE = re.compile(
r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b"
r"|\bit'?s (ok|okay|alright)\b"
)
def _mentions(text, words):
return any(re.search(rf"\b{re.escape(w)}\b", text) for w in words)
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\tests\\test_engine.py",
"view_range": [
1,
150
]
}
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
@@ -1,150 +1,150 @@
import contextlib
import io
import json
import unittest
from pathlib import Path
import engine
from prompt import _engine_note, 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_joins_player_for_conversation_without_keyword_matching(self):
game = engine.new_game()
game["room"] = "Upper Landing"
game["friend_room"] = "Library"
result = act(game, "Can I sit?")
self.assertEqual(result["intent"], "dialogue")
self.assertEqual(game["room"], "Upper Landing")
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertTrue(result["friend_arrived"])
self.assertIn("face to face", result["detail"])
self.assertIn("joined the player", _engine_note(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"])
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)
self.assertIn("his name is not established", prompt)
self.assertIn("do not call him 'E.'", prompt)
if __name__ == "__main__":
unittest.main()
*** Begin Patch
*** Update File: the-house-that-remembers/engine.py
@@
SUPPORT_RE = re.compile(
- r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b"
- ...
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
@@ -364,6 +364,11 @@
SUPPORT_RE = re.compile(
r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b"
r"|\bit'?s (ok|okay|alright)\b"
+ r"|\b(?:we can (?:do (?:it|this)|face (?:it|this)|get through (?:it|this))"
+ r"|we(?:'ll| will) face (?:it|this)) together\b"
+ r"|\b(?:it'?s|it is) going to be (?:ok|okay|alright)\b"
+ r"|\b(?:i want to help|i can help|let me help|i'?d like to help)\b"
+ r"|\b(?:take my hand|give me your hand)\b"
)
def _mentions(text, words):
*** Begin Patch
*** Update File: the-house-that-remembers/tests/test_engine.py
@@
class GameEngineTests(unittest.TestCase):
+ def test_supportive_phrases_increase_support(self):
+ phrases =...
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
@@ -21,6 +21,27 @@
class GameEngineTests(unittest.TestCase):
+ def test_supportive_phrases_increase_support(self):
+ phrases = [
+ "We can do it together.",
+ "We can face this together.",
+ "It's going to be okay.",
+ "It is going to be alright.",
+ "I want to help.",
+ "I can help.",
+ "Take my hand.",
+ "Give me your hand.",
+ ]
+
+ for phrase in phrases:
+ with self.subTest(phrase=phrase):
+ game = engine.new_game()
+
+ act(game, phrase)
+
+ self.assertEqual(game["support_given"], 1)
+ self.assertEqual(game["denial"], 7)
+
def test_routes_to_non_adjacent_room_and_keeps_friend_in_place(self):
game = engine.new_game()
game["room"] = "Library"
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m unittest discover -s tests -v","description":"Run support phrase regression tests",...
test_examining_takeable_features_adds_items_to_inventory (test_engine.GameEngineTests.test_examining_takeable_features_adds_items_to_inventory) ... ok
test_friend_joins_player_for_conversation_without_keyword_matching (test_engine.GameEngineTests.test_friend_joins_player_for_conversation_without_keyword_matching) ... 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
test_supportive_phrases_increase_support (test_engine.GameEngineTests.test_supportive_phrases_increase_support) ... ok
----------------------------------------------------------------------
Ran 7 tests in 0.003s
OK
<shellId: 29 completed with exit code 0>
{"fileUri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py","workspaceRoot":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remember...
No syntax errors found in 'file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py'
{
"command": "git --no-pager diff --check; git status --short; git --no-pager diff --stat",
"description": "Check support matcher diff"
}
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
M .gitignore
M README.md
M the-house-that-remembers/app.py
M the-house-that-remembers/engine.py
M the-house-that-remembers/prompt.py
M the-house-that-remembers/story/canon.json
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
.gitignore | 3 +-
README.md | 3 +-
the-house-that-remembers/app.py | 4 +
the-house-that-remembers/engine.py | 126 +++++++++++++++++++++++-------
the-house-that-remembers/prompt.py | 55 +++++++++----
the-house-that-remembers/story/canon.json | 4 +-
6 files changed, 147 insertions(+), 48 deletions(-)
<shellId: 28 completed with exit code 0>
Added those supportive phrases to the matcher in engine.py. It now recognizes phrases such as “We can do it together,” “It’s going to be okay,” “I want to help,” and “Take my hand,” including the variants covered by the tests.
All 7 regression tests pass.
{'role': 'user',
'content': 'I stand up, go closer and and look into his face. '
'"What is in that room?" I show him the upper landing '
'note I took'},
{'role': 'assistant',
'content': 'The heavy silence of the library gives way as you '
'move through the Entrance Hall and up the Grand '
'Staircase toward the Upper Landing. As you reach the '
'narrow landing, a fleeting shadow drifts past—a '
'whispery form that seems to repeat its warning about '
'leaving before dissolving entirely. Your friend '
'remains in the Library below.'},
this is not correct
The action says “go closer” and mentions the upper-landing note as an object, but the narrator treated “go” plus “upper landing” as a room destination. I’ll reproduce that against the current resolver and add a regression test so an item reference won’t send the player upstairs.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
165,
255
]
}
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
@@ -165,91 +165,91 @@
},
},
},
"Grand Staircase": {
"description": (
"The staircase groans beneath your weight. At the "
"landing, a portrait hangs crookedly. From above "
"comes the faint sound of someone humming."
),
"exits": ["Entrance Hall", "Upper Landing"],
"img": "staircase.jpg",
"aliases": ["grand staircase", "staircase", "stairs", "upstairs"],
"features": {
"portrait": {
"ambient": True,
"keywords": ["portrait", "painting", "picture"],
"looks": [
"The portrait hangs crookedly. The faces are hard to make out in the dim light.",
"The portrait is different from before. Something about the faces has shifted, though you can't say what.",
],
"clues": [("The portrait on the landing changed between looks", 2, None)],
"friend": "He will not look at it.",
},
"humming": {
"ambient": True,
"keywords": ["humming", "hum", "music", "voice", "melody", "listen"],
"looks": [
"The humming drifts down from above. When you look up, it falters, then starts again from the beginning.",
"The same melody, again, from the very beginning.",
],
"clues": [("The humming upstairs falters whenever you listen", 1, 'night')],
"friend": "He seems not to hear it, or says it is only the house.",
},
},
},
"Upper Landing": {
"description": (
"The upper landing is long and narrow, lit by a single grey window. "
"Every door along it is shut. At the far end, a folded note is taped "
"to the frame of one door."
),
"exits": ["Grand Staircase", "Parents' Bedroom"],
"img": "upper_landing.jpg",
"gated": {"Parents' Bedroom": "door_open"},
"aliases": ["upper landing", "upstairs landing", "landing", "upper floor", "second floor", "upstairs", "first floor", "second story"],
"features": {
"door": {
"ambient": True,
"keywords": ["door", "note", "knock", "knocking"],
"takeable": "Upper Landing Note",
"looks": [
"The parents' bedroom door is shut. The folded note taped to its frame reads, in a careful hand: 'Do not open. Do not change anything.'",
"You press your ear to the wood. From inside comes a slow knock, three times. Then nothing.",
],
"clues": [
("A note on the parents' bedroom door reads: do not open, do not change anything", 1, "rules"),
("Slow knocking comes from inside the locked bedroom", 2, "night"),
],
"friend": "He will not come near the door.",
},
},
},
"Parents' Bedroom": {
"description": ("The parents' bedroom is cold and perfectly still. Three figures lie together on the bed, turned toward one another, as if asleep. No dust has settled on anything here. The air smells faintly of cold metal."),
"exits": ["Upper Landing"],
"img": "bedroom.jpg",
"aliases": ["parents' bedroom", "parents bedroom", "parent's bedroom", "parents' room",
"parents room", "master bedroom", "bedroom"],
"features": {
"bed": {
"keywords": ["bed", "figures", "family", "mother", "father", "sister", "parents"],
"looks": ["Three figures lie together on the bed, turned toward one another, as if asleep. None of them breathes, and nothing about them has changed at all."],
},
},
},
}
CLUE_FRAGMENT = {
text: frag
for room in ROOMS.values()
for feat in room.get("features", {}).values()
for (text, _n, frag) in feat.get("clues", [])
if frag
}
BACK = {
"Library": "Entrance Hall",
"Kitchen": "Entrance Hall",
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
300,
425
]
}
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
@@ -300,126 +300,126 @@
"""Set of completed fragment kinds ('routine', 'rules', 'night')."""
counts = {}
for clue in game["clues"]:
frag = CLUE_FRAGMENT.get(clue)
if frag:
counts[frag] = counts.get(frag, 0) + 1
return {f for f, n in counts.items() if n >= FRAGMENT_CLUES_NEEDED}
def exit_labels(game, room_name):
"""Exit names for display, marking locked ones."""
room = ROOMS[room_name]
gated = room.get("gated", {})
labels = []
for e in room["exits"]:
flag = gated.get(e)
labels.append(f"{e} (door locked)" if flag and not game[flag] else e)
return labels
MOVE_VERBS = ["go", "enter", "head", "walk", "move", "step", "climb", "ascend",
"descend", "return", "run", "visit", "follow", "proceed"]
TAKE_VERBS = ["take", "grab", "pick", "pocket", "steal"]
LEAVE_RE = re.compile(
r"\b(leave|exit|flee|escape)\b.*\b(house|mansion|manor)\b"
r"|\bgo outside\b|\bwalk out\b"
)
NEGATION_RE = re.compile(r"\b(not|never|without)\b|n't\b|\bwont\b|\bdont\b")
TOGETHER_RE = re.compile(
r"\b(leave|go|walk|get out|come|take him|lead him)\b.*\b(together|with me|with him)\b"
r"|\b(together|with me)\b.*\b(leave|go|out)\b"
r"|\blet'?s (leave|get out)\b"
)
LOOK_AROUND_RE = re.compile(
r"\b(look|glance|scan|survey|search|explore|examine|inspect|study)\w*\b.*\b(around|about|room|surroundings|area)\b"
r"|\b(examine|inspect|search|study|explore|look)\w*\b.*\b(kitchen|library|hall|staircase|landing|bedroom)\b"
r"|\bwhat (do|can) i (see|notice|find)\b|\bnotice anything\b"
)
QUOTED_TEXT_RE = re.compile(r'"[^"]*"|“[^”]*”|‘[^’]*’')
MEMORY_RE = re.compile(
r"\b(remember|recall|used to|when we were (?:kids|children)|"
r"back when we|childhood)\b"
)
EXAMINE_RE = re.compile(
r"\b(examine|inspect|study|look at|check|read|listen to|smell|touch)\b"
)
PHYSICAL_ACTION_RE = re.compile(
r"\b(?:i\s+)?(?:pat|touch|sit|stand|reach|hold|hug|tap|point|"
r"turn|open|close|pick|take|walk|step|move|climb|enter|follow)\b"
)
SPEECH_RE = re.compile(
r"\b(?:i\s+)?(?:say|ask|tell|reply|answer|whisper|speak|call)\b"
r"|\b(?:ask|tell) (?:him|her|your friend)\b"
)
DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b")
DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b")
FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b")
SUPPORT_RE = re.compile(
r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b"
r"|\bit'?s (ok|okay|alright)\b"
r"|\b(?:we can (?:do (?:it|this)|face (?:it|this)|get through (?:it|this))"
r"|we(?:'ll| will) face (?:it|this)) together\b"
r"|\b(?:it'?s|it is) going to be (?:ok|okay|alright)\b"
r"|\b(?:i want to help|i can help|let me help|i'?d like to help)\b"
r"|\b(?:take my hand|give me your hand)\b"
)
def _mentions(text, words):
return any(re.search(rf"\b{re.escape(w)}\b", text) for w in words)
def _find_rooms(text):
return [name for name, r in ROOMS.items() if _mentions(text, r["aliases"])]
def _find_feature(text, room):
for name, feat in room.get("features", {}).items():
if _mentions(text, feat["keywords"]):
return name, feat
return None
def _end(game, ending, detail):
game["ending"] = ending
return _result("ending", detail)
def _classify_intent(raw_action, game):
"""Classify what the player is doing so narration responds appropriately."""
action_text = QUOTED_TEXT_RE.sub("", raw_action).lower()
room = ROOMS[game["room"]]
if MEMORY_RE.search(raw_action.lower()):
return "memory"
if LOOK_AROUND_RE.search(action_text):
return "exploration"
if EXAMINE_RE.search(action_text) and _find_feature(action_text, room):
return "examination"
has_quoted_speech = bool(QUOTED_TEXT_RE.search(raw_action))
has_speech = has_quoted_speech or bool(SPEECH_RE.search(action_text))
has_physical_action = bool(PHYSICAL_ACTION_RE.search(action_text))
if has_speech and has_physical_action:
return "action_and_dialogue"
if has_speech or raw_action.strip().endswith("?"):
return "dialogue"
if has_physical_action:
return "physical_action"
return "roleplay"
def _result(outcome, detail, new_clue=None, ambient=None, friend=None):
return {
"nudge": None, # gentle push to move on (set by apply_action)
"outcome": outcome,
"detail": detail,
"new_clue": new_clue,
"ambient": ambient, # small detail to weave into a quiet turn
"friend": friend, # optional hint for how the friend reacts
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
600,
785
]
}
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
@@ -600,186 +600,186 @@
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
if not target:
if re.search(r"\bback\b", text) and room_name in BACK:
target = BACK[room_name]
elif re.search(r"\b(up|upstairs)\b", text) and (room_name, "up") in VERTICAL:
target = VERTICAL[(room_name, "up")]
elif re.search(r"\b(down|downstairs)\b", text) and (room_name, "down") in VERTICAL:
target = VERTICAL[(room_name, "down")]
if target:
route = _path_to(game, room_name, lambda r: r == target)
if route:
path, _ = 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
detail = f"The player moved to the {target}."
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 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 from the {room_name}. "
f"Reachable from here: {', '.join(exit_labels(game, room_name))}.",
)
if room_name in rooms_named:
return _result("no_change", f"The player is already in the {room_name}.")
found = _find_feature(text, room)
if found and _mentions(text, TAKE_VERBS):
name, feat = found
item = feat.get("takeable")
if not item:
return _result("blocked", f"The {name} can't be taken.")
if item in game["inventory"]:
return _result("no_change", f"The player already has the {item}.")
game["inventory"].append(item)
return _result("picked_up", f"The player picked up the {item}.")
if found and (
intent not in {"dialogue", "memory"}
or EXAMINE_RE.search(text)
or _mentions(text, TAKE_VERBS)
):
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}",
new_clue,
friend=feat.get("friend"),
)
if LOOK_AROUND_RE.search(text):
nxt = _next_unnoticed(game, room_name)
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, room_name, name, feat)
return _result(
"examined",
f"The player looked around the {room_name}. One thing stands out: {noticed}",
new_clue,
friend=feat.get("friend"),
)
return _result(
"examined",
f"The player looked around the {room_name} again. Nothing new stands out. "
"Describe only what the room description already says, in a fresh way.",
)
return _result(
"no_change",
"Nothing the engine tracks changed. Treat the action as ordinary roleplay "
"(talking, resting, small gestures). Do not add items, clues, or events. "
f"No room change happens: the player is still in the {room_name}, and nobody "
f"leads them elsewhere unless they choose an exit ({', '.join(exit_labels(game, room_name))}).",
)
STAGE_RULES = [
lambda g: g["turns_in_stage"] >= 5 and len(g["visited"]) >= 2, # 0 arrival
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 1, # 1 contradictions
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 4, # 2 paintings
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 7, # 3 sister's warnings
]
def _advance_stage(game):
if game["door_open"] and game["stage"] < 5:
game["stage"] = 5
game["turns_in_stage"] = 0
return True
if game["stage"] >= 4:
return False
if STAGE_RULES[game["stage"]](game):
game["stage"] += 1
game["turns_in_stage"] = 0
return True
return False
def apply_action(game, raw_action):
"""Resolve one player action, update game state, return the engine result."""
game.setdefault("friend_room", "Entrance Hall")
game.setdefault("sister_seen", False)
game.setdefault("last_sister_turn", -99)
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
result = _resolve(game, text)
if result["outcome"] == "moved":
intent = "movement"
elif result["outcome"] == "picked_up":
intent = "physical_action"
elif result["outcome"] == "examined" and intent not in {
"examination",
"exploration",
}:
intent = "examination"
result["intent"] = intent
if (
intent in {"dialogue", "action_and_dialogue"}
and not game["ending"]
and game["friend_room"] != game["room"]
):
game["friend_room"] = game["room"]
result["friend_arrived"] = True
result["detail"] += (
f" The friend joins the player in the {game['room']} so they can speak "
"face to face."
)
game["turn_count"] += 1
game["turns_in_stage"] += 1
if result["outcome"] != "moved":
game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
12,
37
]
}
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
@@ -12,26 +12,26 @@
import ollama
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT brings him into the player's room; narrate that arrival before his reply.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. If asked directly about the past, characters answer in fragments, contradict themselves, or change the subject.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\tests\\test_engine.py",
"view_range": [
1,
155
]
}
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
@@ -1,155 +1,155 @@
import contextlib
import io
import json
import unittest
from pathlib import Path
import engine
from prompt import _engine_note, 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_supportive_phrases_increase_support(self):
phrases = [
"We can do it together.",
"We can face this together.",
"It's going to be okay.",
"It is going to be alright.",
"I want to help.",
"I can help.",
"Take my hand.",
"Give me your hand.",
]
for phrase in phrases:
with self.subTest(phrase=phrase):
game = engine.new_game()
act(game, phrase)
self.assertEqual(game["support_given"], 1)
self.assertEqual(game["denial"], 7)
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_joins_player_for_conversation_without_keyword_matching(self):
game = engine.new_game()
game["room"] = "Upper Landing"
game["friend_room"] = "Library"
result = act(game, "Can I sit?")
self.assertEqual(result["intent"], "dialogue")
self.assertEqual(game["room"], "Upper Landing")
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertTrue(result["friend_arrived"])
self.assertIn("face to face", result["detail"])
self.assertIn("joined the player", _engine_note(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"])
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)
self.assertIn("his name is not established", prompt)
self.assertIn("do not call him 'E.'", prompt)
if __name__ == "__main__":
unittest.main()
{
"command": "git status --short; git --no-pager diff --check",
"description": "Check existing modifications and whitespace"
}
M .gitignore
M README.md
M the-house-that-remembers/app.py
M the-house-that-remembers/engine.py
M the-house-that-remembers/prompt.py
M the-house-that-remembers/story/canon.json
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
<shellId: 30 completed with exit code 0>
The resolver confirms the false positive: it sees “go closer” as a movement verb and matches “Upper Landing” inside “Upper Landing Note” as the destination. I’ll exclude names of inventory items when extracting room destinations, so a note mention won’t be mistaken for travel.
*** Begin Patch
*** Update File: the-house-that-remembers/engine.py
@@
def _find_rooms(text):
return [name for name, r in ROOMS.items() if _mentions(text, r["aliases"])]
+
+def _remove_invento...
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
@@ -379,6 +379,13 @@
return [name for name, r in ROOMS.items() if _mentions(text, r["aliases"])]
+def _remove_inventory_mentions(text, inventory):
+ """Keep room names inside carried item names from becoming movement targets."""
+ for item in sorted(inventory, key=len, reverse=True):
+ text = re.sub(rf"\b{re.escape(item.lower())}\b", " ", text)
+ return text
+
+
def _find_feature(text, room):
for name, feat in room.get("features", {}).items():
if _mentions(text, feat["keywords"]):
@@ -583,7 +590,7 @@
text = QUOTED_TEXT_RE.sub("", text).lower() # actions only: speech can't move or examine things
room_name = game["room"]
room = ROOMS[room_name]
- rooms_named = _find_rooms(text)
+ rooms_named = _find_rooms(_remove_inventory_mentions(text, game["inventory"]))
negated = bool(NEGATION_RE.search(text))
spoken_negated = bool(NEGATION_RE.search(spoken))
*** Begin Patch
*** Update File: the-house-that-remembers/tests/test_engine.py
@@
def test_friend_joins_player_for_conversation_without_keyword_matching(self):
game = engine.new_game()
...
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
@@ -71,6 +71,23 @@
self.assertIn("face to face", result["detail"])
self.assertIn("joined the player", _engine_note(game))
+ def test_room_name_in_carried_note_does_not_trigger_movement(self):
+ game = engine.new_game()
+ game["room"] = "Library"
+ game["friend_room"] = "Library"
+ game["inventory"].append("Upper Landing Note")
+
+ result = act(
+ game,
+ 'I stand up, go closer and look into his face. '
+ '"What is in that room?" I show him the upper landing note I took',
+ )
+
+ self.assertEqual(game["room"], "Library")
+ self.assertEqual(game["friend_room"], "Library")
+ self.assertNotEqual(result["outcome"], "moved")
+ self.assertEqual(result["intent"], "action_and_dialogue")
+
def test_examining_takeable_features_adds_items_to_inventory(self):
game = engine.new_game()
game["room"] = "Library"
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m unittest discover -s tests -v","description":"Run movement parsing regression tests...
test_examining_takeable_features_adds_items_to_inventory (test_engine.GameEngineTests.test_examining_takeable_features_adds_items_to_inventory) ... ok
test_friend_joins_player_for_conversation_without_keyword_matching (test_engine.GameEngineTests.test_friend_joins_player_for_conversation_without_keyword_matching) ... 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_room_name_in_carried_note_does_not_trigger_movement (test_engine.GameEngineTests.test_room_name_in_carried_note_does_not_trigger_movement) ... FAIL
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
test_supportive_phrases_increase_support (test_engine.GameEngineTests.test_supportive_phrases_increase_support) ... ok
FAIL: test_room_name_in_carried_note_does_not_trigger_movement (test_engine.GameEngineTests.test_room_name_in_carried_note_does_not_trigger_movement)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", line 89, in test_room_name_in_carried_note_does_not_trigger_movement
self.assertEqual(result["intent"], "action_and_dialogue")
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 'exploration' != 'action_and_dialogue'
- exploration
+ action_and_dialogue
----------------------------------------------------------------------
Ran 8 tests in 0.005s
FAILED (failures=1)
<shellId: 32 completed with exit code 1>
{"fileUri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py","workspaceRoot":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remember...
No syntax errors found in 'file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py'
{
"command": "git --no-pager diff --check; git --no-pager diff --stat; git status --short",
"description": "Review final parser fix"
}
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
.gitignore | 3 +-
README.md | 3 +-
the-house-that-remembers/app.py | 4 +
the-house-that-remembers/engine.py | 135 +++++++++++++++++++++++-------
the-house-that-remembers/prompt.py | 55 ++++++++----
the-house-that-remembers/story/canon.json | 4 +-
6 files changed, 155 insertions(+), 49 deletions(-)
M .gitignore
M README.md
M the-house-that-remembers/app.py
M the-house-that-remembers/engine.py
M the-house-that-remembers/prompt.py
M the-house-that-remembers/story/canon.json
<shellId: 31 completed with exit code 0>
The new test exposed a second false positive: “look into his face” was classified as room exploration because “landing” appeared later in the note reference. I’ll apply the same inventory-item filtering to intent classification, then rerun the regression.
*** Begin Patch
*** Update File: the-house-that-remembers/engine.py
@@
def _classify_intent(raw_action, game):
"""Classify what the player is doing so narration responds appropriately."""
a...
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
@@ -401,6 +401,7 @@
def _classify_intent(raw_action, game):
"""Classify what the player is doing so narration responds appropriately."""
action_text = QUOTED_TEXT_RE.sub("", raw_action).lower()
+ action_text = _remove_inventory_mentions(action_text, game["inventory"])
room = ROOMS[game["room"]]
if MEMORY_RE.search(raw_action.lower()):
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m unittest discover -s tests -v","description":"Rerun movement and intent regression ...
test_examining_takeable_features_adds_items_to_inventory (test_engine.GameEngineTests.test_examining_takeable_features_adds_items_to_inventory) ... ok
test_friend_joins_player_for_conversation_without_keyword_matching (test_engine.GameEngineTests.test_friend_joins_player_for_conversation_without_keyword_matching) ... 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_room_name_in_carried_note_does_not_trigger_movement (test_engine.GameEngineTests.test_room_name_in_carried_note_does_not_trigger_movement) ... FAIL
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
test_supportive_phrases_increase_support (test_engine.GameEngineTests.test_supportive_phrases_increase_support) ... ok
FAIL: test_room_name_in_carried_note_does_not_trigger_movement (test_engine.GameEngineTests.test_room_name_in_carried_note_does_not_trigger_movement)
----------------------------------------------------------------------
Traceback (most recent call last):
File "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", line 89, in test_room_name_in_carried_note_does_not_trigger_movement
self.assertEqual(result["intent"], "action_and_dialogue")
~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: 'examination' != 'action_and_dialogue'
- examination
+ action_and_dialogue
----------------------------------------------------------------------
Ran 8 tests in 0.005s
FAILED (failures=1)
<shellId: 34 completed with exit code 1>
{"fileUri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py","workspaceRoot":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remember...
No syntax errors found in 'file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py'
{
"command": "git --no-pager diff --check; git status --short",
"description": "Check final patch cleanliness"
}
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
M .gitignore
M README.md
M the-house-that-remembers/app.py
M the-house-that-remembers/engine.py
M the-house-that-remembers/prompt.py
M the-house-that-remembers/story/canon.json
<shellId: 33 completed with exit code 0>
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; @'\nimport engine\ntext = 'I stand up, go closer and and look into his face. \"What is in tha...
'i stand up, go closer and and look into his face. i show him the i took'
look around False
examine False None
physical True
speech True False
intent action_and_dialogue
<shellId: 35 completed with exit code 0>
*** Begin Patch
*** Update File: the-house-that-remembers/engine.py
@@
- rooms_named = _find_rooms(_remove_inventory_mentions(text, game["inventory"]))
+ room_parse_text = _remove_inventory_ment...
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
@@ -591,7 +591,8 @@
text = QUOTED_TEXT_RE.sub("", text).lower() # actions only: speech can't move or examine things
room_name = game["room"]
room = ROOMS[room_name]
- rooms_named = _find_rooms(_remove_inventory_mentions(text, game["inventory"]))
+ room_parse_text = _remove_inventory_mentions(text, game["inventory"])
+ rooms_named = _find_rooms(room_parse_text)
negated = bool(NEGATION_RE.search(text))
spoken_negated = bool(NEGATION_RE.search(spoken))
@@ -692,7 +693,7 @@
)
- if LOOK_AROUND_RE.search(text):
+ if LOOK_AROUND_RE.search(room_parse_text):
nxt = _next_unnoticed(game, room_name)
if nxt:
name, feat = nxt
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m unittest discover -s tests -v","description":"Run final movement parsing tests","in...
test_examining_takeable_features_adds_items_to_inventory (test_engine.GameEngineTests.test_examining_takeable_features_adds_items_to_inventory) ... ok
test_friend_joins_player_for_conversation_without_keyword_matching (test_engine.GameEngineTests.test_friend_joins_player_for_conversation_without_keyword_matching) ... 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_room_name_in_carried_note_does_not_trigger_movement (test_engine.GameEngineTests.test_room_name_in_carried_note_does_not_trigger_movement) ... 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
test_supportive_phrases_increase_support (test_engine.GameEngineTests.test_supportive_phrases_increase_support) ... ok
----------------------------------------------------------------------
Ran 8 tests in 0.005s
OK
<shellId: 37 completed with exit code 0>
{"fileUri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py","workspaceRoot":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remember...
No syntax errors found in 'file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py'
{
"command": "git --no-pager diff --check; git status --short",
"description": "Verify final patch cleanliness"
}
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it
M .gitignore
M README.md
M the-house-that-remembers/app.py
M the-house-that-remembers/engine.py
M the-house-that-remembers/prompt.py
M the-house-that-remembers/story/canon.json
<shellId: 36 completed with exit code 0>
Fixed the false movement trigger in engine.py. The resolver was interpreting “go closer” as a movement command and the words “upper landing” in the carried note’s name as the destination. It now excludes carried item names when parsing room destinations and exploration intent, so showing the note shouldn’t send the player upstairs.
All 8 regression tests pass, including the reported action, and the engine file has no syntax errors.
{"pattern":"STAGE_RULES|def _advance_stage|door_open|fragments_complete|stage.*4|stage.*5|turns_in_stage","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engin...
[grep content: 25 matches across 1 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
engine.py (25 match(es)):
208: "gated": {"Parents' Bedroom": "door_open"},
279: "turns_in_stage": 0,
289: "door_open": False, # whether the parents' bedroom door is open
299: def fragments_complete(game):
499: if (room_name == "Upper Landing" and not game["door_open"]
500: and len(fragments_complete(game)) >= 3):
508: if not hit and not game["door_open"] and room_name != "Upper Landing":
535: if game["door_open"]:
537: if len(fragments_complete(game)) < 3:
552: game["door_open"] = True
555: "door_opened",
563: if len(fragments_complete(game)) >= 3:
727: STAGE_RULES = [
728: lambda g: g["turns_in_stage"] >= 5 and len(g["visited"]) >= 2, # 0 arrival
729: lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 1, # 1 contradictions
730: lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 4, # 2 paintings
731: lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 7, # 3 sister's warnings
735: def _advance_stage(game):
736: if game["door_open"] and game["stage"] < 5:
737: game["stage"] = 5
738: game["turns_in_stage"] = 0
740: if game["stage"] >= 4:
742: if STAGE_RULES[game["stage"]](game):
744: game["turns_in_stage"] = 0
781: game["turns_in_stage"] += 1
{"pattern":"# 4 -|# 5 -|Parents' fragments|Reconstructed truth|door opens|locked bedroom|knocking","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py","...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:69: { # 4 - Parents' fragments
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:70: "now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.",
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:71: "friend": "He flinches at the knocking and tells the player, \"Whatever you do, don't answer it.\" Fragments of memory slip out, then he retreats.",
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:74: { # 5 - Reconstructed truth
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:85: "(the stopped clocks may tick once; the knocking and humming fall silent), the door, the rain easing. "
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:96: "the knocking and humming resume as though nothing happened, and he is left with the house. "
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
470,
555
]
}
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
@@ -470,86 +470,86 @@
"""Breadth-first search over open exits. Returns the route and goal room."""
from collections import deque
seen = {start}
queue = deque([(start, [])])
while queue:
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)
route = [*path, nxt]
if goal_fn(nxt):
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"]
if (room_name == "Upper Landing" and not game["door_open"]
and len(fragments_complete(game)) >= 3):
return (
"Everything here has been found, but the friend is not ready for the door. "
"Let him hesitate, almost speak, and look to the player. Patience and kindness may "
"help him. Do not open the door."
)
hit = _path_step(game, room_name, lambda r: _unfinished(game, r))
if not hit and not game["door_open"] and room_name != "Upper Landing":
hit = _path_step(game, room_name, lambda r: r == "Upper Landing")
if not hit:
return None
step, target = hit
where = f"the {step}" + (f", on the way to the {target}" if step != target else "")
level = min(game["nudges_here"] + 1, 3)
if level == 1:
return (
"There is nothing more to find here for now. Let the friend gently suggest moving on, "
f"toward {where}. The move happens only if the player chooses it."
)
if level == 2:
return (
"Still nothing new here. Let a faint sound or a change in the light from the direction "
f"of {where} draw attention that way. Do not say what it is. "
"The move happens only if the player chooses it."
)
return (
"The player has lingered. Let the friend drift toward the exit that leads to "
f"{where}, stop there, and look back. The player is still in the {room_name} "
"until they choose to move."
)
def _try_door(game):
if game["door_open"]:
return _result("no_change", "The bedroom door already stands open.")
if len(fragments_complete(game)) < 3:
return _result(
"blocked",
"The door will not give. It is not only locked: pushing at it feels like pushing "
"against something holding its breath. The friend begs the player to leave it. "
"The player does not yet understand enough to know why it matters.",
friend="He is frightened and pleads with the player to step away.",
)
if game["denial"] > DOOR_DENIAL_MAX:
return _result(
"blocked",
"The door stays shut. The friend's hand closes on the player's arm. He is not ready, "
"and the door will not open until he is.",
friend="He is trembling and cannot let the player open it yet.",
)
game["door_open"] = True
game["denial"] = min(game["denial"], 2)
return _result(
"door_opened",
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
670,
850
]
}
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
@@ -670,181 +670,181 @@
return _result("blocked", f"The {name} can't be taken.")
if item in game["inventory"]:
return _result("no_change", f"The player already has the {item}.")
game["inventory"].append(item)
return _result("picked_up", f"The player picked up the {item}.")
if found and (
intent not in {"dialogue", "memory"}
or EXAMINE_RE.search(text)
or _mentions(text, TAKE_VERBS)
):
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}",
new_clue,
friend=feat.get("friend"),
)
if LOOK_AROUND_RE.search(room_parse_text):
nxt = _next_unnoticed(game, room_name)
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, room_name, name, feat)
return _result(
"examined",
f"The player looked around the {room_name}. One thing stands out: {noticed}",
new_clue,
friend=feat.get("friend"),
)
return _result(
"examined",
f"The player looked around the {room_name} again. Nothing new stands out. "
"Describe only what the room description already says, in a fresh way.",
)
return _result(
"no_change",
"Nothing the engine tracks changed. Treat the action as ordinary roleplay "
"(talking, resting, small gestures). Do not add items, clues, or events. "
f"No room change happens: the player is still in the {room_name}, and nobody "
f"leads them elsewhere unless they choose an exit ({', '.join(exit_labels(game, room_name))}).",
)
STAGE_RULES = [
lambda g: g["turns_in_stage"] >= 5 and len(g["visited"]) >= 2, # 0 arrival
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 1, # 1 contradictions
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 4, # 2 paintings
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 7, # 3 sister's warnings
]
def _advance_stage(game):
if game["door_open"] and game["stage"] < 5:
game["stage"] = 5
game["turns_in_stage"] = 0
return True
if game["stage"] >= 4:
return False
if STAGE_RULES[game["stage"]](game):
game["stage"] += 1
game["turns_in_stage"] = 0
return True
return False
def apply_action(game, raw_action):
"""Resolve one player action, update game state, return the engine result."""
game.setdefault("friend_room", "Entrance Hall")
game.setdefault("sister_seen", False)
game.setdefault("last_sister_turn", -99)
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
result = _resolve(game, text)
if result["outcome"] == "moved":
intent = "movement"
elif result["outcome"] == "picked_up":
intent = "physical_action"
elif result["outcome"] == "examined" and intent not in {
"examination",
"exploration",
}:
intent = "examination"
result["intent"] = intent
if (
intent in {"dialogue", "action_and_dialogue"}
and not game["ending"]
and game["friend_room"] != game["room"]
):
game["friend_room"] = game["room"]
result["friend_arrived"] = True
result["detail"] += (
f" The friend joins the player in the {game['room']} so they can speak "
"face to face."
)
game["turn_count"] += 1
game["turns_in_stage"] += 1
if result["outcome"] != "moved":
game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
if (
result["outcome"] == "no_change"
and not game["ending"]
and not game["truth_known"]
and game["support_given"] < SUPPORT_LIMIT
and SUPPORT_RE.search(text)
):
game["support_given"] += 1
game["denial"] = max(0, game["denial"] - 1)
result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
if not game["ending"] and not game["truth_known"] and game["turn_count"] >= MAX_TURNS:
result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
if fresh:
game["last_notice_turn"] = game["turn_count"]
if (
not fresh
and result["outcome"] in ("no_change", "examined")
and not game["ending"]
and not game["truth_known"]
and game["room_turns"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_nudge_turn"] >= NUDGE_COOLDOWN
and _next_unnoticed(game, game["room"]) is None
):
nudge = _nudge(game)
if nudge:
result["nudge"] = nudge
game["last_nudge_turn"] = game["turn_count"]
game["nudges_here"] += 1
if result["new_clue"]:
for _ in result["new_clue"].split("; "):
game["denial"] = max(0, game["denial"] - 1)
result["stage_advanced"] = _advance_stage(game)
sister_due = (
not game["sister_seen"]
or game["turn_count"] - game["last_sister_turn"] >= SISTER_COOLDOWN
)
if game["stage"] >= 3 and sister_due and not game["ending"]:
appearance = (
"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."
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
40,
75
]
}
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
@@ -40,36 +40,36 @@
"Do you remember how we used to help in the kitchen? I was never much good at it."
"It's strange being back in the library. We used to spend whole afternoons in here."
"I haven't been sleeping much. The house is loud at night."
"Tea sounds good. The kitchen's just this way, if you'd like."
"I miss the old days. Now, everything is so... different. I don't know how to explain it."
"What have you been doing all these years? I mean... if you want to tell me.\""""
STAGES = [
{ # 0 - Arrival and uneasy normality
"now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet. Friend invited the player to the house because he is lonely and the history of the house is weighing on him. He is not aware of the truth, but he doesn't want to be alone.",
"friend": "He tries to act normal and spend ordinary time together. He can ask about the player's life or mention an established childhood memory. He deflects direct questions about the house and does not bring up family members on his own.",
"steer": "Let the player settle in and explore. Keep the friend's suggestions tied to what the player just said; if tea or a drink is wanted, the kitchen is the natural next step. Do not offer upstairs or list destinations without a reason. When the friend invites the player somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
},
{ # 1 - Contradictions
"now": "Details about the friend's family do not quite add up.",
"friend": "He mentions his family casually, in the present tense, as if they are alive and nearby: his mother is resting, his father is busy upstairs, his sister is 'around'. He does not notice anything odd. If asked to see them, or asked directly about them, he answers vaguely and changes the subject.",
"steer": "Let this come up once or twice in passing, e.g. \"Mum's resting. She hasn't been sleeping well.\" Do not explain it.",
},
{ # 2 - Frozen house and family traces
"now": "The house is frozen in time: clocks are stopped, calendars are out of date, food never spoils, and places are set for people who are not there. Portraits can change between looks. The friend has lost track of time.",
"friend": "He says time is 'strange' or that he has lost count of the days. He grows uneasy when stopped clocks, old dates or the extra place settings are mentioned and tries to move on.",
"steer": "Describe frozen or changed details only when the engine provides them. Otherwise keep the house quietly wrong: a sound repeating, a door slightly off.",
},
{ # 3 - Sister's warnings
"now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.",
"friend": "He does not see her, or insists the player imagined it.",
"steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most, then gone. She speaks gently and urgently. She never threatens. Do not say what happened to the friend's family.",
},
{ # 4 - Parents' fragments
"now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.",
"friend": "He flinches at the knocking and tells the player, \"Whatever you do, don't answer it.\" Fragments of memory slip out, then he retreats.",
"steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.",
},
{ # 5 - Reconstructed truth
"now": "The bedroom door has opened. Inside, the family lie preserved as if asleep. The truth, to be revealed only gently and in fragments: after the father and son stopped speaking, the father locked himself, the mother and the sister in this room and told the son never to open the door. Cut off from help and drained by the house, they died inside. The friend knocked once, heard nothing, obeyed the rule out of anger and fear, and never opened it. The house kept the room as it was.",
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
1,
250
]
}
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
@@ -1,250 +1,250 @@
"""engine.py - authoritative game state and rules.
Nothing in here calls the model. The model only ever sees the *result* of
what this module decides (see prompt.py).
"""
import re
from pprint import pprint
AMBIENT_AFTER_TURNS = 3 # turns spent in a room before a detail may surface
AMBIENT_COOLDOWN = 3 # minimum turns between any two noticed details
FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment
DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door
PUSH_LIMIT = 3 # blunt confrontations before the evidence -> he shuts down
SUPPORT_LIMIT = 3 # times kindness can lower his denial
MAX_TURNS = 90 # truth not faced by then -> "denial" ending
NUDGE_AFTER_TURNS = 4
NUDGE_COOLDOWN = 3
SISTER_COOLDOWN = 12
ROOMS = {
"Entrance Hall": {
"description": (
"The moment you step inside, a gust of wind slips past you. The heavy front door slams shut behind you with a deafening BANG. The sound echoes through the vast entrance hall, bouncing off the high ceilings before dissolving into silence. \n\n"
"A figure emerges from the shadows. Your friend.\n"
"He looks nothing like the person you remember. His eyes are hollow, his skin pale, ghostly, and dark circles hollow his eyes.\n\n"
"\"You came,\" he whispers, his voice trembling. \"I didn't think you would... Thank you.\"\n\n"
),
"exits": ["Library", "Kitchen", "Grand Staircase"],
"img": "hallway.jpg",
"aliases": ["entrance hall", "entrance", "hallway", "hall", "foyer"],
"features": {
"smell": {
"ambient": True,
"keywords": ["smell", "scent", "odor", "musty"],
"looks": ["A musty smell lingers in the air, a mixture of old wood and dust."],
"clues": [("A musty smell lingers in the air", 1, None)],
},
"clock": {
"ambient": True,
"keywords": ["clock", "clocks", "pendulum", "ticking"],
"looks": [
"A tall clock stands against the wall, its hands stopped at a quarter past three. The pendulum hangs perfectly still. The clock hasn't moved. Nothing in this hall ticks.",
],
"clues": [("The clock on the wall is frozen at 3:15", 1, 'rules')],
"friend": "He says it only needs winding, then changes the subject.",
},
},
},
"Library": {
"description": (
"Shelves of warped books climb toward a ceiling lost "
"in shadow. One narrow gap interrupts the dust on a "
"lower shelf, as though a book was recently removed. Family photos are hanging on the walls, but the faces are blurred and unrecognizable. "
),
"exits": ["Entrance Hall"],
"img": "library.jpg",
"aliases": ["library"],
"features": {
"shelf": {
"ambient": True,
"keywords": ["shelf", "shelves", "gap", "books", "bookshelf", "bookshelves"],
"looks": [
"The dust on a lower shelf is disturbed by a narrow, clean gap where a book was recently removed.",
"The gap is still there. Nothing nearby explains where the missing book went.",
],
"clues": [("A book was recently removed from the shelf", 1, None)],
"friend": "He says he doesn't remember taking a book, and changes the subject.",
},
"footsteps": {
"ambient": True,
"keywords": ["footsteps", "steps", "sound", "sounds", "ceiling", "overhead", "listen"],
"looks": [
"From somewhere above, a few slow footsteps cross the ceiling. After a , the very same footsteps cross it again.",
"The footsteps repeat, exactly the same, as though something is replaying.",
],
"clues": [("The same few footsteps repeat overhead", 1, 'night')],
"friend": "He says it is only the house settling, and changes the subject.",
},
"photos": {
"ambient": True,
"keywords": ["photo", "photos", "picture", "pictures", "frame", "frames"],
"looks": [
"The family photos are blurred and unrecognizable. You can't make out any faces.",
"The photos remain blurred. You can't tell who they are.",
],
"clues": [("Family photos on the wall are blurred and unrecognizable", 1, None)],
"friend": "He says the photos are just old and faded.",
},
"history": {
"ambient": True,
"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.",
"The page hasn't changed. The ink is faded, but the passage is still legible.",
],
"clues": [("The house's first owner buried his sister alive and died soon after; the house stood empty for years", 1, None)],
},
},
},
"Kitchen": {
"description": (
"The narrow kitchen is cluttered but familiar. Rain ticks "
"against a dark window above the sink. You remember this kitchen, as if you had never left."
),
"exits": ["Entrance Hall"],
"img": "kitchen.jpg",
"aliases": ["kitchen"],
"features": {
"cups": {
"ambient": True,
"keywords": ["cup", "cups", "mug", "mugs", "teapot", "kettle", "counter"],
"looks": [
"Two cups wait by the teapot. Then you notice a third at the far end of the table, set out and untouched.",
"The third cup hasn't moved. It sits exactly where someone would put it for a guest.",
],
"clues": [("A third cup sits set out at the table, as if someone else were expected", 1, 'routine')],
"friend": "If he notices the player noticing, he glances away and changes the subject.",
},
"chair": {
"ambient": True,
"keywords": ["chair", "chairs", "table", "seat"],
"looks": [
"One chair at the table is pulled out slightly, as though someone had only just stood up from it.",
"The chair stays where it is. The seat is cold.",
],
"clues": [("A kitchen chair is pulled out, as though someone had just stood up", 1, 'routine')],
},
"food": {
"ambient": True,
"keywords": ["food", "plate", "plates", "dish", "dishes"],
"looks": [
"A plate of food sits on the counter, untouched. The smell is faint but familiar.",
"The plate of food remains. It is cold, but the smell is still faintly familiar.",
"It seems like the food has been sitting there for a while, but it i
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
420,
530
]
}
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
@@ -420,111 +420,111 @@
return "dialogue"
if has_physical_action:
return "physical_action"
return "roleplay"
def _result(outcome, detail, new_clue=None, ambient=None, friend=None):
return {
"nudge": None, # gentle push to move on (set by apply_action)
"outcome": outcome,
"detail": detail,
"new_clue": new_clue,
"ambient": ambient, # small detail to weave into a quiet turn
"friend": friend, # optional hint for how the friend reacts
}
def _examine(game, room_name, name, feat):
"""Record one examination of a feature. Returns (what they notice, new clue(s) or None)."""
key = f"{room_name}:{name}"
n = game["examined"].get(key, 0) + 1
game["examined"][key] = n
looks = feat["looks"]
noticed = looks[min(n - 1, len(looks) - 1)]
found = []
for text, on_n, _frag in feat.get("clues", []):
if n == on_n and text not in game["clues"]:
game["clues"].append(text)
found.append(text)
return noticed, ("; ".join(found) if found else None)
def _next_unnoticed(game, room_name):
"""First ambient feature in this room the player hasn't encountered yet."""
for name, feat in ROOMS[room_name].get("features", {}).items():
if feat.get("ambient") and game["examined"].get(f"{room_name}:{name}", 0) == 0:
return name, feat
return None
def _unfinished(game, room_name):
"""A room still worth visiting: never visited, or details left to notice."""
if room_name == "Parents' Bedroom":
return False
return room_name not in game["visited"] or _next_unnoticed(game, room_name) is not 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, [])])
while queue:
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)
route = [*path, nxt]
if goal_fn(nxt):
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"]
if (room_name == "Upper Landing" and not game["door_open"]
and len(fragments_complete(game)) >= 3):
return (
"Everything here has been found, but the friend is not ready for the door. "
"Let him hesitate, almost speak, and look to the player. Patience and kindness may "
"help him. Do not open the door."
)
hit = _path_step(game, room_name, lambda r: _unfinished(game, r))
if not hit and not game["door_open"] and room_name != "Upper Landing":
hit = _path_step(game, room_name, lambda r: r == "Upper Landing")
if not hit:
return None
step, target = hit
where = f"the {step}" + (f", on the way to the {target}" if step != target else "")
level = min(game["nudges_here"] + 1, 3)
if level == 1:
return (
"There is nothing more to find here for now. Let the friend gently suggest moving on, "
f"toward {where}. The move happens only if the player chooses it."
)
if level == 2:
return (
"Still nothing new here. Let a faint sound or a change in the light from the direction "
f"of {where} draw attention that way. Do not say what it is. "
"The move happens only if the player chooses it."
)
return (
"The player has lingered. Let the friend drift toward the exit that leads to "
f"{where}, stop there, and look back. The player is still in the {room_name} "
"until they choose to move."
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
720,
805
]
}
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
@@ -720,86 +720,86 @@
)
STAGE_RULES = [
lambda g: g["turns_in_stage"] >= 5 and len(g["visited"]) >= 2, # 0 arrival
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 1, # 1 contradictions
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 4, # 2 paintings
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 7, # 3 sister's warnings
]
def _advance_stage(game):
if game["door_open"] and game["stage"] < 5:
game["stage"] = 5
game["turns_in_stage"] = 0
return True
if game["stage"] >= 4:
return False
if STAGE_RULES[game["stage"]](game):
game["stage"] += 1
game["turns_in_stage"] = 0
return True
return False
def apply_action(game, raw_action):
"""Resolve one player action, update game state, return the engine result."""
game.setdefault("friend_room", "Entrance Hall")
game.setdefault("sister_seen", False)
game.setdefault("last_sister_turn", -99)
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
result = _resolve(game, text)
if result["outcome"] == "moved":
intent = "movement"
elif result["outcome"] == "picked_up":
intent = "physical_action"
elif result["outcome"] == "examined" and intent not in {
"examination",
"exploration",
}:
intent = "examination"
result["intent"] = intent
if (
intent in {"dialogue", "action_and_dialogue"}
and not game["ending"]
and game["friend_room"] != game["room"]
):
game["friend_room"] = game["room"]
result["friend_arrived"] = True
result["detail"] += (
f" The friend joins the player in the {game['room']} so they can speak "
"face to face."
)
game["turn_count"] += 1
game["turns_in_stage"] += 1
if result["outcome"] != "moved":
game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
if (
result["outcome"] == "no_change"
and not game["ending"]
and not game["truth_known"]
and game["support_given"] < SUPPORT_LIMIT
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
55,
83
]
}
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
@@ -55,29 +55,29 @@
"now": "Details about the friend's family do not quite add up.",
"friend": "He mentions his family casually, in the present tense, as if they are alive and nearby: his mother is resting, his father is busy upstairs, his sister is 'around'. He does not notice anything odd. If asked to see them, or asked directly about them, he answers vaguely and changes the subject.",
"steer": "Let this come up once or twice in passing, e.g. \"Mum's resting. She hasn't been sleeping well.\" Do not explain it.",
},
{ # 2 - Frozen house and family traces
"now": "The house is frozen in time: clocks are stopped, calendars are out of date, food never spoils, and places are set for people who are not there. Portraits can change between looks. The friend has lost track of time.",
"friend": "He says time is 'strange' or that he has lost count of the days. He grows uneasy when stopped clocks, old dates or the extra place settings are mentioned and tries to move on.",
"steer": "Describe frozen or changed details only when the engine provides them. Otherwise keep the house quietly wrong: a sound repeating, a door slightly off.",
},
{ # 3 - Sister's warnings
"now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.",
"friend": "He does not see her, or insists the player imagined it.",
"steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most, then gone. She speaks gently and urgently. She never threatens. Do not say what happened to the friend's family.",
},
{ # 4 - Parents' fragments
"now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.",
"friend": "He flinches at the knocking and tells the player, \"Whatever you do, don't answer it.\" Fragments of memory slip out, then he retreats.",
"steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.",
},
{ # 5 - Reconstructed truth
"now": "The bedroom door has opened. Inside, the family lie preserved as if asleep. The truth, to be revealed only gently and in fragments: after the father and son stopped speaking, the father locked himself, the mother and the sister in this room and told the son never to open the door. Cut off from help and drained by the house, they died inside. The friend knocked once, heard nothing, obeyed the rule out of anger and fear, and never opened it. The house kept the room as it was.",
"friend": "His denial breaks. He speaks in halting fragments and quiet realization, not speeches. He does not blame the player. He may say he cannot stay here, but never decides for the player.",
"steer": "Let the moment breathe: stillness, cold, grief. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.",
},
]
ENDINGS = {
"leave_together": (
"The player and the friend have faced the truth and choose to leave together. "
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\tests\\test_engine.py",
"view_range": [
1,
180
]
}
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
@@ -1,180 +1,180 @@
import contextlib
import io
import json
import unittest
from pathlib import Path
import engine
from prompt import _engine_note, 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_supportive_phrases_increase_support(self):
phrases = [
"We can do it together.",
"We can face this together.",
"It's going to be okay.",
"It is going to be alright.",
"I want to help.",
"I can help.",
"Take my hand.",
"Give me your hand.",
]
for phrase in phrases:
with self.subTest(phrase=phrase):
game = engine.new_game()
act(game, phrase)
self.assertEqual(game["support_given"], 1)
self.assertEqual(game["denial"], 7)
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_joins_player_for_conversation_without_keyword_matching(self):
game = engine.new_game()
game["room"] = "Upper Landing"
game["friend_room"] = "Library"
result = act(game, "Can I sit?")
self.assertEqual(result["intent"], "dialogue")
self.assertEqual(game["room"], "Upper Landing")
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertTrue(result["friend_arrived"])
self.assertIn("face to face", result["detail"])
self.assertIn("joined the player", _engine_note(game))
def test_room_name_in_carried_note_does_not_trigger_movement(self):
game = engine.new_game()
game["room"] = "Library"
game["friend_room"] = "Library"
game["inventory"].append("Upper Landing Note")
result = act(
game,
'I stand up, go closer and look into his face. '
'"What is in that room?" I show him the upper landing note I took',
)
self.assertEqual(game["room"], "Library")
self.assertEqual(game["friend_room"], "Library")
self.assertNotEqual(result["outcome"], "moved")
self.assertEqual(result["intent"], "action_and_dialogue")
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"])
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)
self.assertIn("his name is not established", prompt)
self.assertIn("do not call him 'E.'", prompt)
if __name__ == "__main__":
unittest.main()
{"pattern":"^def _try_door|^def _advance_stage|^STAGE_RULES|len\\(fragments_complete\\(game\\)\\)|game\\[\"door_open\"\\] = True|if room_name == \"Upper Landing\"","paths":"C:\\Users\\lucia\\Documents...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:500: and len(fragments_complete(game)) >= 3):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:534:def _try_door(game):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:537: if len(fragments_complete(game)) < 3:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:552: game["door_open"] = True
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:563: if len(fragments_complete(game)) >= 3:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:611: if room_name == "Upper Landing" and not negated:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:727:STAGE_RULES = [
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:735:def _advance_stage(game):
{"pattern":"FRAGMENT_CLUES_NEEDED|def fragments_complete|\"clues\": \\[|routine|rules|night|All three|fragment","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\...
[grep content: 25 matches across 1 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
engine.py (25 match(es)):
1: """engine.py - authoritative game state and rules.
13: FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment
44: "clues": [("A musty smell lingers in the air", 1, None)],
52: "clues": [("The clock on the wall is frozen at 3:15", 1, 'rules')],
74: "clues": [("A book was recently removed from the shelf", 1, None)],
84: "clues": [("The same few footsteps repeat overhead", 1, 'night')],
94: "clues": [("Family photos on the wall are blurred and unrecognizable", 1, None)],
115: "clues": [("The house's first owner buried his sister alive and died soon after; the house stood empty for years", 1, None)],
136: "clues": [("A third cup sits set out at the table, as if someone else were expected", 1, 'routine')],
146: "clues": [("A kitchen chair is pulled out, as though someone had just stood up", 1, 'routine')],
156: "clues": [("A plate of food sits on the counter, untouched", 1, 'routine'), ("The food has sat out for ages but has not spoiled", 3, None)],
164: "clues": [("The calendar on the wall is turned to March, but it is currently October. Year is faded and unreadable", 1, 'rules')],
185: "clues": [("The portrait on the landing changed between looks", 2, None)],
195: "clues": [("The humming upstairs falters whenever you listen", 1, 'night')],
219: "clues": [
220: ("A note on the parents' bedroom door reads: do not open, do not change anything", 1, "rules"),
221: ("Slow knocking comes from inside the locked bedroom", 2, "night"),
243: # Which fragment each clue belongs to, built from ROOMS.
275: "clues": [],
299: def fragments_complete(game):
300: """Set of completed fragment kinds ('routine', 'rules', 'night')."""
306: return {f for f, n in counts.items() if n >= FRAGMENT_CLUES_NEEDED}
500: and len(fragments_complete(game)) >= 3):
537: if len(fragments_complete(game)) < 3:
563: if len(fragments_complete(game)) >= 3:
I am in stage 5, but the room is still upper landing, I said "enter bedroom" and it didn't change
I’ll check the door gate and movement parser. If “enter bedroom” should work in the current state, I’ll pin down which condition is blocking it.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\the-house-that-remembers\\engine.py",
"view_range": [
200,
235
]
}
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\the-house-that-remembers\\engine.py",
"view_range": [
530,
655
]
}
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\the-house-that-remembers\\engine.py",
"view_range": [
725,
815
]
}
{"pattern":"def _resolve|def _try_door|gated|DOOR_TRY_RE|MOVE_VERBS|\"Parents' Bedroom\"|\"bedroom\"","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py...
[grep content: 21 matches across 1 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
engine.py (21 match(es)):
206: "exits": ["Grand Staircase", "Parents' Bedroom"],
208: "gated": {"Parents' Bedroom": "door_open"},
227: "Parents' Bedroom": {
232: "parents room", "master bedroom", "bedroom"],
258: "Parents' Bedroom": "Upper Landing",
312: gated = room.get("gated", {})
315: flag = gated.get(e)
322: MOVE_VERBS = ["go", "enter", "head", "walk", "move", "step", "climb", "ascend",
359: DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b")
464: if room_name == "Parents' Bedroom":
477: gate = ROOMS[room].get("gated", {}).get(nxt)
534: def _try_door(game):
588: def _resolve(game, text):
596: negated = bool(NEGATION_RE.search(text))
597: spoken_negated = bool(NEGATION_RE.search(spoken))
601: if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
605: if LEAVE_RE.search(text) and not negated:
611: if room_name == "Upper Landing" and not negated:
612: if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
621: if _mentions(text, MOVE_VERBS):
640: if step == "Parents' Bedroom":
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
200,
235
]
}
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
@@ -200,36 +200,36 @@
"Upper Landing": {
"description": (
"The upper landing is long and narrow, lit by a single grey window. "
"Every door along it is shut. At the far end, a folded note is taped "
"to the frame of one door."
),
"exits": ["Grand Staircase", "Parents' Bedroom"],
"img": "upper_landing.jpg",
"gated": {"Parents' Bedroom": "door_open"},
"aliases": ["upper landing", "upstairs landing", "landing", "upper floor", "second floor", "upstairs", "first floor", "second story"],
"features": {
"door": {
"ambient": True,
"keywords": ["door", "note", "knock", "knocking"],
"takeable": "Upper Landing Note",
"looks": [
"The parents' bedroom door is shut. The folded note taped to its frame reads, in a careful hand: 'Do not open. Do not change anything.'",
"You press your ear to the wood. From inside comes a slow knock, three times. Then nothing.",
],
"clues": [
("A note on the parents' bedroom door reads: do not open, do not change anything", 1, "rules"),
("Slow knocking comes from inside the locked bedroom", 2, "night"),
],
"friend": "He will not come near the door.",
},
},
},
"Parents' Bedroom": {
"description": ("The parents' bedroom is cold and perfectly still. Three figures lie together on the bed, turned toward one another, as if asleep. No dust has settled on anything here. The air smells faintly of cold metal."),
"exits": ["Upper Landing"],
"img": "bedroom.jpg",
"aliases": ["parents' bedroom", "parents bedroom", "parent's bedroom", "parents' room",
"parents room", "master bedroom", "bedroom"],
"features": {
"bed": {
"keywords": ["bed", "figures", "family", "mother", "father", "sister", "parents"],
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
530,
655
]
}
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
@@ -530,126 +530,126 @@
"until they choose to move."
)
def _try_door(game):
if game["door_open"]:
return _result("no_change", "The bedroom door already stands open.")
if len(fragments_complete(game)) < 3:
return _result(
"blocked",
"The door will not give. It is not only locked: pushing at it feels like pushing "
"against something holding its breath. The friend begs the player to leave it. "
"The player does not yet understand enough to know why it matters.",
friend="He is frightened and pleads with the player to step away.",
)
if game["denial"] > DOOR_DENIAL_MAX:
return _result(
"blocked",
"The door stays shut. The friend's hand closes on the player's arm. He is not ready, "
"and the door will not open until he is.",
friend="He is trembling and cannot let the player open it yet.",
)
game["door_open"] = True
game["denial"] = min(game["denial"], 2)
return _result(
"door_opened",
"The door opens at last, without resistance, as if it had only ever been waiting. "
"Cold, still air spills out. The friend stands frozen beside the player. "
"The player has not yet gone in.",
)
def _confront(game):
if len(fragments_complete(game)) >= 3:
game["denial"] = max(0, game["denial"] - 1)
return _result(
"no_change",
"The player says aloud what the evidence points to. This time the friend does not "
"deny it outright: he wavers, almost admits it, then stops, voice cracking. "
"Nothing else changes.",
friend="He is on the edge of breaking.",
)
game["pushed"] += 1
game["denial"] = min(10, game["denial"] + 1)
if game["pushed"] >= PUSH_LIMIT:
return _end(
game, "denial",
"The player pushed too hard, too soon. The friend shuts down completely.",
)
warn = " He is close to shutting down." if game["pushed"] == PUSH_LIMIT - 1 else ""
return _result(
"no_change",
"The player bluntly says the friend's family is dead before having any evidence. "
"The friend flatly denies it, hurt and frightened, and changes the subject." + warn +
" Nothing in the house changes.",
)
def _resolve(game, text):
intent = _classify_intent(text, game)
spoken = text # full text, quoted speech included
text = QUOTED_TEXT_RE.sub("", text).lower() # actions only: speech can't move or examine things
room_name = game["room"]
room = ROOMS[room_name]
room_parse_text = _remove_inventory_mentions(text, game["inventory"])
rooms_named = _find_rooms(room_parse_text)
negated = bool(NEGATION_RE.search(text))
spoken_negated = bool(NEGATION_RE.search(spoken))
if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
return _end(game, "leave_together",
"The player and the friend choose to leave the house together.")
if LEAVE_RE.search(text) and not negated:
if room_name == "Entrance Hall":
return _end(game, "escape_alone", "The player walks out of the house alone.")
return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
if not target:
if re.search(r"\bback\b", text) and room_name in BACK:
target = BACK[room_name]
elif re.search(r"\b(up|upstairs)\b", text) and (room_name, "up") in VERTICAL:
target = VERTICAL[(room_name, "up")]
elif re.search(r"\b(down|downstairs)\b", text) and (room_name, "down") in VERTICAL:
target = VERTICAL[(room_name, "down")]
if target:
route = _path_to(game, room_name, lambda r: r == target)
if route:
path, _ = 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
detail = f"The player moved to the {target}."
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 game["friend_room"] != target:
detail += f" The friend remains in the {game['friend_room']}."
return _result("moved", detail)
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
725,
815
]
}
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
@@ -725,91 +725,91 @@
STAGE_RULES = [
lambda g: g["turns_in_stage"] >= 5 and len(g["visited"]) >= 2, # 0 arrival
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 1, # 1 contradictions
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 4, # 2 paintings
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 7, # 3 sister's warnings
]
def _advance_stage(game):
if game["door_open"] and game["stage"] < 5:
game["stage"] = 5
game["turns_in_stage"] = 0
return True
if game["stage"] >= 4:
return False
if STAGE_RULES[game["stage"]](game):
game["stage"] += 1
game["turns_in_stage"] = 0
return True
return False
def apply_action(game, raw_action):
"""Resolve one player action, update game state, return the engine result."""
game.setdefault("friend_room", "Entrance Hall")
game.setdefault("sister_seen", False)
game.setdefault("last_sister_turn", -99)
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
result = _resolve(game, text)
if result["outcome"] == "moved":
intent = "movement"
elif result["outcome"] == "picked_up":
intent = "physical_action"
elif result["outcome"] == "examined" and intent not in {
"examination",
"exploration",
}:
intent = "examination"
result["intent"] = intent
if (
intent in {"dialogue", "action_and_dialogue"}
and not game["ending"]
and game["friend_room"] != game["room"]
):
game["friend_room"] = game["room"]
result["friend_arrived"] = True
result["detail"] += (
f" The friend joins the player in the {game['room']} so they can speak "
"face to face."
)
game["turn_count"] += 1
game["turns_in_stage"] += 1
if result["outcome"] != "moved":
game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
if (
result["outcome"] == "no_change"
and not game["ending"]
and not game["truth_known"]
and game["support_given"] < SUPPORT_LIMIT
and SUPPORT_RE.search(text)
):
game["support_given"] += 1
game["denial"] = max(0, game["denial"] - 1)
result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
if not game["ending"] and not game["truth_known"] and game["turn_count"] >= MAX_TURNS:
result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; @'\nimport contextlib, io, engine\nstate = engine.new_game()\nstate['room'] = 'Upper Landing'...
stage=5, door_open=True: Parents' Bedroom moved The player moved to the Parents' Bedroom. First visit to: Parents' Bedroom. The friend remains in the Entrance Hall.
stage=4, door_open=False: Upper Landing blocked The Parents' Bedroom cannot be reached from the Upper Landing. Reachable from here: Grand Staircase, Parents' Bedroom (door locked).
<shellId: 38 completed with exit code 0>
The door opening and entering the bedroom are two separate actions:
door_open and advances the engine to stage 5. The player remains on the landing.enter bedroom should move the player into the Parents’ Bedroom.
I reproduced enter bedroom with stage = 5 and door_open = True; the engine moved into the bedroom. If it didn’t in your game, check door_open and the outcome/ detail in last_result. Stage 5 with door_open = False would be inconsistent with the normal progression, and the engine would block entry. The door check is in _try_door; stage progression is in _advance_stage.
Game state:
{'room': 'Upper Landing',
'friend_room': 'Upper Landing',
'inventory': ['The Letter', 'Upper Landing Note', 'Library Book'],
'clues': ['A musty smell lingers in the air',
'A third cup sits set out at the table, as if someone else were '
'expected',
'A kitchen chair is pulled out, as though someone had just stood up',
'A plate of food sits on the counter, untouched',
'The calendar on the wall is turned to March, but it is currently '
'October. Year is faded and unreadable',
'The clock on the wall is frozen at 3:15',
"A note on the parents' bedroom door reads: do not open, do not "
'change anything',
'Slow knocking comes from inside the locked bedroom',
'A book was recently removed from the shelf',
"The house's first owner buried his sister alive and died soon "
'after; the house stood empty for years',
'The same few footsteps repeat overhead',
'Family photos on the wall are blurred and unrecognizable'],
'visited': ['Entrance Hall',
'Kitchen',
'Grand Staircase',
'Upper Landing',
'Library'],
'examined': {'Entrance Hall:smell': 1,
'Kitchen:cups': 2,
'Kitchen:chair': 1,
'Kitchen:food': 2,
'Kitchen:calendar': 2,
'Entrance Hall:clock': 1,
'Upper Landing:door': 2,
'Library:shelf': 1,
'Library:history': 2,
'Library:footsteps': 1,
'Library:photos': 1},
'stage': 5,
'turns_in_stage': 3,
'turn_count': 76,
'room_turns': 4,
'last_notice_turn': 54,
'last_nudge_turn': 76,
'nudges_here': 1,
'last_sister_turn': 76,
'denial': 0,
'pushed': 0,
'support_given': 0,
'door_open': True,
'truth_known': False,
'sister_seen': True,
'ending': None,
{"pattern":"\"Parents' Bedroom\": \\{|\"aliases\": \\[\"parents' bedroom\"|def _advance_stage|if step == \"Parents' Bedroom\"|def _resolve|if room_name == \"Upper Landing\" and not negated|if _mention...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:227: "Parents' Bedroom": {
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:231: "aliases": ["parents' bedroom", "parents bedroom", "parent's bedroom", "parents' room",
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:588:def _resolve(game, text):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:611: if room_name == "Upper Landing" and not negated:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:621: if _mentions(text, MOVE_VERBS):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:640: if step == "Parents' Bedroom":
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:641: game["truth_known"] = True
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:735:def _advance_stage(game):
This state shows that the door is open and the game is in stage 5, but the player is still on the Upper Landing. The bedroom is reached in a separate movement action; truth_known becomes True only when that move succeeds. The room alias includes “bedroom” at engine.py, and the truth flag is set during movement at engine.py.
Try “go to the parents’ bedroom”. If it still doesn’t move, check last_result for its outcome and detail; that will show whether the engine blocked the move or resolved it some other way.
{"pattern":"TOGETHER_RE|LEAVE_RE|def _resolve|def _end|ending.*leave_together|escape_alone|\"ending\"","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.p...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:292: "ending": None, # None or an ending id
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:326:LEAVE_RE = re.compile(
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:331:TOGETHER_RE = re.compile(
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:396:def _end(game, ending, detail):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:397: game["ending"] = ending
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:398: return _result("ending", detail)
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:588:def _resolve(game, text):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:601: if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:605: if LEAVE_RE.search(text) and not negated:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:607: return _end(game, "escape_alone", "The player walks out of the house alone.")
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:770: and not game["ending"]
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:788: and not game["ending"]
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:803: and not game["ending"]
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:813: if not game["ending"] and not game["truth_known"] and game["turn_count"] >= MAX_TURNS:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:825: and not game["ending"]
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:846: if game["stage"] >= 3 and sister_due and not game["ending"]:
{"pattern":"ENDINGS =|leave_together|escape_alone|denial|Offer no further choices","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py","output_mode":"co...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:76: "friend": "His denial breaks. He speaks in halting fragments and quiet realization, not speeches. He does not blame the player. He may say he cannot stay here, but never decides for the player.",
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:81:ENDINGS = {
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:82: "leave_together": (
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:86: "No gore. Offer no further choices. End the story."
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:88: "escape_alone": (
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:91: "the house behind them. Do not explain what happened to the family. Offer no further choices. End the story."
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:93: "denial": (
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:97: "Do not describe the family or explain what happened. Offer no further choices. End the story."
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:102:def denial_band(denial):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:103: if denial >= 7:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:104: return "high denial: he insists things are fine, deflects, changes the subject"
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:105: if denial >= 4:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:131: if game["ending"] == "escape_alone":
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:142: f"FRIEND'S CONDITION: {denial_band(game['denial'])}\n"
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
340,
375
]
}
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
@@ -340,36 +340,36 @@
r"|\b(examine|inspect|search|study|explore|look)\w*\b.*\b(kitchen|library|hall|staircase|landing|bedroom)\b"
r"|\bwhat (do|can) i (see|notice|find)\b|\bnotice anything\b"
)
QUOTED_TEXT_RE = re.compile(r'"[^"]*"|“[^”]*”|‘[^’]*’')
MEMORY_RE = re.compile(
r"\b(remember|recall|used to|when we were (?:kids|children)|"
r"back when we|childhood)\b"
)
EXAMINE_RE = re.compile(
r"\b(examine|inspect|study|look at|check|read|listen to|smell|touch)\b"
)
PHYSICAL_ACTION_RE = re.compile(
r"\b(?:i\s+)?(?:pat|touch|sit|stand|reach|hold|hug|tap|point|"
r"turn|open|close|pick|take|walk|step|move|climb|enter|follow)\b"
)
SPEECH_RE = re.compile(
r"\b(?:i\s+)?(?:say|ask|tell|reply|answer|whisper|speak|call)\b"
r"|\b(?:ask|tell) (?:him|her|your friend)\b"
)
DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b")
DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b")
FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b")
SUPPORT_RE = re.compile(
r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b"
r"|\bit'?s (ok|okay|alright)\b"
r"|\b(?:we can (?:do (?:it|this)|face (?:it|this)|get through (?:it|this))"
r"|we(?:'ll| will) face (?:it|this)) together\b"
r"|\b(?:it'?s|it is) going to be (?:ok|okay|alright)\b"
r"|\b(?:i want to help|i can help|let me help|i'?d like to help)\b"
r"|\b(?:take my hand|give me your hand)\b"
)
def _mentions(text, words):
return any(re.search(rf"\b{re.escape(w)}\b", text) for w in words)
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
580,
630
]
}
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
@@ -580,51 +580,51 @@
return _result(
"no_change",
"The player bluntly says the friend's family is dead before having any evidence. "
"The friend flatly denies it, hurt and frightened, and changes the subject." + warn +
" Nothing in the house changes.",
)
def _resolve(game, text):
intent = _classify_intent(text, game)
spoken = text # full text, quoted speech included
text = QUOTED_TEXT_RE.sub("", text).lower() # actions only: speech can't move or examine things
room_name = game["room"]
room = ROOMS[room_name]
room_parse_text = _remove_inventory_mentions(text, game["inventory"])
rooms_named = _find_rooms(room_parse_text)
negated = bool(NEGATION_RE.search(text))
spoken_negated = bool(NEGATION_RE.search(spoken))
if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
return _end(game, "leave_together",
"The player and the friend choose to leave the house together.")
if LEAVE_RE.search(text) and not negated:
if room_name == "Entrance Hall":
return _end(game, "escape_alone", "The player walks out of the house alone.")
return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
if not target:
if re.search(r"\bback\b", text) and room_name in BACK:
target = BACK[room_name]
elif re.search(r"\b(up|upstairs)\b", text) and (room_name, "up") in VERTICAL:
target = VERTICAL[(room_name, "up")]
elif re.search(r"\b(down|downstairs)\b", text) and (room_name, "down") in VERTICAL:
target = VERTICAL[(room_name, "down")]
{"pattern":"^LEAVE_RE|^TOGETHER_RE|^def _resolve|if game\\[\"truth_known\"\\].*TOGETHER|if LEAVE_RE|if room_name == \"Upper Landing\"|^def _end","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hac...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:326:LEAVE_RE = re.compile(
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:331:TOGETHER_RE = re.compile(
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:396:def _end(game, ending, detail):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:588:def _resolve(game, text):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:601: if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:605: if LEAVE_RE.search(text) and not negated:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:611: if room_name == "Upper Landing" and not negated:
truth_known is True, denial is 0, but the friend is still refusing to talk about it.
The screenshot shows the game is in the bedroom, but the stage-5 guidance still encourages “fragments” and the shared prompt says direct questions should be met with evasion. I’ll check the current prompt/state block, then make the truth-stage instruction clearly override that earlier deflection rule.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
12,
105
]
}
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
@@ -12,94 +12,94 @@
import ollama
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT brings him into the player's room; narrate that arrival before his reply.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. If asked directly about the past, characters answer in fragments, contradict themselves, or change the subject.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good."
"Do you remember how we used to help in the kitchen? I was never much good at it."
"It's strange being back in the library. We used to spend whole afternoons in here."
"I haven't been sleeping much. The house is loud at night."
"Tea sounds good. The kitchen's just this way, if you'd like."
"I miss the old days. Now, everything is so... different. I don't know how to explain it."
"What have you been doing all these years? I mean... if you want to tell me.\""""
STAGES = [
{ # 0 - Arrival and uneasy normality
"now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet. Friend invited the player to the house because he is lonely and the history of the house is weighing on him. He is not aware of the truth, but he doesn't want to be alone.",
"friend": "He tries to act normal and spend ordinary time together. He can ask about the player's life or mention an established childhood memory. He deflects direct questions about the house and does not bring up family members on his own.",
"steer": "Let the player settle in and explore. Keep the friend's suggestions tied to what the player just said; if tea or a drink is wanted, the kitchen is the natural next step. Do not offer upstairs or list destinations without a reason. When the friend invites the player somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
},
{ # 1 - Contradictions
"now": "Details about the friend's family do not quite add up.",
"friend": "He mentions his family casually, in the present tense, as if they are alive and nearby: his mother is resting, his father is busy upstairs, his sister is 'around'. He does not notice anything odd. If asked to see them, or asked directly about them, he answers vaguely and changes the subject.",
"steer": "Let this come up once or twice in passing, e.g. \"Mum's resting. She hasn't been sleeping well.\" Do not explain it.",
},
{ # 2 - Frozen house and family traces
"now": "The house is frozen in time: clocks are stopped, calendars are out of date, food never spoils, and places are set for people who are not there. Portraits can change between looks. The friend has lost track of time.",
"friend": "He says time is 'strange' or that he has lost count of the days. He grows uneasy when stopped clocks, old dates or the extra place settings are mentioned and tries to move on.",
"steer": "Describe frozen or changed details only when the engine provides them. Otherwise keep the house quietly wrong: a sound repeating, a door slightly off.",
},
{ # 3 - Sister's warnings
"now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.",
"friend": "He does not see her, or insists the player imagined it.",
"steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most, then gone. She speaks gently and urgently. She never threatens. Do not say what happened to the friend's family.",
},
{ # 4 - Parents' fragments
"now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.",
"friend": "He flinches at the knocking and tells the player, \"Whatever you do, don't answer it.\" Fragments of memory slip out, then he retreats.",
"steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.",
},
{ # 5 - Reconstructed truth
"now": "The bedroom door has opened. Inside, the family lie preserved as if asleep. The truth, to be revealed only gently and in fragments: after the father and son stopped speaking, the father locked himself, the mother and the sister in this room and told the son never to open the door. Cut off from help and drained by the house, they died inside. The friend knocked once, heard nothing, obeyed the rule out of anger and fear, and never opened it. The house kept the room as it was.",
"friend": "His denial breaks. He speaks in halting fragments and quiet realization, not speeches. He does not blame the player. He may say he cannot stay here, but never decides for the player.",
"steer": "Let the moment breathe: stillness, cold, grief. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.",
},
]
ENDINGS = {
"leave_together": (
"The player and the friend have faced the truth and choose to leave together. "
"Write the final scene in 4-6 sentences: the friend's grief and fragile steadiness, the house letting go "
"(the stopped clocks may tick once; the knocking and humming fall silent), the door, the rain easing. "
"No gore. Offer no further choices. End the story."
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
130,
235
]
}
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
@@ -130,106 +130,106 @@
text = ENDINGS[game["ending"]]
if game["ending"] == "escape_alone":
text += (
" The player saw the truth in the bedroom before leaving; the friend stays behind with his family."
if game["truth_known"]
else " The player never learned what was upstairs; the friend stays behind in the house."
)
return "ENDING\n" + text
s = STAGES[game["stage"]]
return (
f"STORY NOW: {s['now']}\n"
f"FRIEND RIGHT NOW: {s['friend']}\n"
f"FRIEND'S CONDITION: {denial_band(game['denial'])}\n"
f"STEER TOWARD (gently, never force): {s['steer']}\n"
f"Turns spent in this part of the story: {game['turns_in_stage']}"
)
def state_block(game):
from engine import ROOMS, exit_labels # local import avoids a cycle if engine grows
room = ROOMS[game["room"]]
friend_room = game.get("friend_room", "Entrance Hall")
lines = [
"CURRENT STATE",
f"Room: {game['room']}",
f"Room description: {room['description'].strip()}",
f"Exits: {', '.join(exit_labels(game, game['room']))}",
(
"Friend location: with the player in this room."
if friend_room == game["room"]
else f"Friend location: {friend_room}. He is not with the player and cannot hear them."
),
f"Inventory: {', '.join(game['inventory']) or 'empty'}",
f"Clues the player has found: {', '.join(game['clues']) or 'none'}",
"Established player memories: childhood companions; playing outside together; "
"helping the friend with kitchen chores; exploring the family library; "
"sleeping over in the guest room.",
]
if game["summary"]:
lines.append(f"Earlier in the story: {game['summary']}")
return "\n".join(lines)
def _engine_note(game):
r = game["last_result"]
if not r:
return ""
note = (
"\n\n[ENGINE RESULT - authoritative]\n"
f"Player intent: {r.get('intent', 'roleplay')}\n"
f"Outcome: {r['outcome']}\n"
f"Detail: {r['detail']}"
)
if r.get("ambient"):
note += (
"\nAmbient detail to weave into this turn in one quiet sentence, "
f"without making it a big moment: {r['ambient']}"
)
if r.get("nudge"):
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']}"
friend_is_here = game.get("friend_room", "Entrance Hall") == 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']}"
if r.get("friend_arrived"):
note += "\nThe friend has joined the player to speak face to face; narrate his arrival before his response."
intent_reminders = {
"dialogue": (
"Treat the player's words as spoken to the friend and answer directly in his natural, "
"restrained voice. If the ENGINE RESULT says he joined the player, narrate his arrival "
"before the reply."
),
"memory": (
"Answer the player's memory question as their character, using only the established "
"player memories in CURRENT STATE or facts already established in conversation. "
"Do not invent a specific shared event or reveal hidden family history."
),
"physical_action": (
"Treat this as a physical action by the player. Acknowledge what they actually did "
"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. If the ENGINE RESULT says he joined the player, narrate his "
"arrival before the reply. Do not add actions for the player."
),
"examination": (
"Treat this as an examination. Use the ENGINE RESULT as the only new observation; "
"do not add clues or details."
),
"exploration": (
"Treat this as looking around or exploring. Use only the room description and "
"ENGINE RESULT for observations; do not add clues or details."
),
"movement": "Narrate the confirmed route and destination. Keep the friend at the location given in CURRENT STATE unless the ENGINE RESULT says he accompanied the player.",
"roleplay": "Respond naturally to the player's action without inventing consequential events.",
}
reminder = (
f"{intent_reminders.get(r.get('intent', 'roleplay'), intent_reminders['roleplay'])} "
"Stay in character. 2-5 short sentences. Add no other items, clues, uncanny events, "
"room changes or backstory."
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\story\\canon.json",
"view_range": [
38,
80
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json
@@ -38,43 +38,43 @@
"The mother, father and sister are dead. They died inside the parents' bedroom after being locked in and cut off from help, drained by the house. There was no violence. The house preserved the room, so they look as if they are asleep.",
"The friend survived. He knocked on the bedroom door once, heard no reply, and did not open it. He has pretended ever since that his family is alive and resting, and the house helps him not to see the truth.",
"The friend behaves as though his whole family is alive: his mother is resting, his father is busy, his sister is somewhere around.",
"The sister is a ghost. She roams the house, hums, appears intermittently, and warns the player to leave. Her warnings are sincere. She believes preserving the family exactly as it is keeps them alive.",
"The parents linger as fragments tied to the bedroom: slow knocking and footsteps. They want release, not endless preservation.",
"The friend's warning 'Whatever you do, don't answer it' refers to the knocking from the parents' bedroom.",
"As the friend's denial weakens and memories return, reality becomes less consistent: paintings change, familiar spaces seem altered, sounds repeat, and doors or rooms may feel wrong.",
"The player can leave when the game state allows it. Leaving early is a valid choice.",
"The player's presence gives the friend an external point of reference, but cannot cure him or force him to accept the truth. He must acknowledge it himself."
],
"family_history": {
"father": {
"characterization": "Moved the family to the house. As his wife declined and doctors could not help, he grew paranoid and began to lose himself too. His only idea was to preserve everything as it was: locking doors, stopping clocks, changing nothing. He fell out with his son over the son's denial. In the end he locked himself, his wife and his daughter in the parents' bedroom to 'preserve' them and told his son never to open the door. His attempt to protect them is what killed them.",
"guidance": "Show fear and a distorted desire to protect, not simple villainy."
},
"mother": {
"characterization": "The first to be affected. A stay-at-home mother who spent the most time in the house, she grew depressed and lethargic and could not sleep, as if the house were draining her. She recognized that something was wrong.",
"guidance": "Treat her suffering seriously and compassionately, never as a moral failing."
},
"sister": {
"characterization": "Supportive but cautious. She still went out for food and tried to help the family hold together, and she was protective of her brother, urging him not to give up on them. After death she clings to the family as it is, believing preservation keeps them alive.",
"guidance": "Protective, loving, frightened and deluded at once. Not evil."
},
"friend": {
"characterization": "Lived in denial, believing the others were overreacting. He ignored his father's rules and fell out with him. He obeyed only the last one, not to open the bedroom door, out of anger and fear. He survived, but his sense of time and memory of the final days are distorted.",
"guidance": "His progression is governed by explicit game-state rules (denial, fragments, door), not by the model."
},
"tragedy_details": {
"status": "AUTHORED",
"truth": "After the father and son stopped speaking, the father locked himself, the mother and the sister in the parents' bedroom and told the son never to open the door. Cut off from help and drained by the house, they died inside. The house preserved the room as it was. The friend knocked once, heard nothing, and never opened the door, then convinced himself the family was alive.",
"timing": "Recent but deliberately unfixed. The house distorts time, so never give a number of days.",
"narration_guidance": [
"Reveal the truth only in the final stage, after the bedroom door is opened by the engine.",
"Before that, use oblique fragments only: knocking, repeating footsteps, humming, stopped clocks.",
"Never describe wounds, decay or gore. The family looks as if they are sleeping.",
"The friend should face the truth in halting fragments and quiet realization, not a lecture."
]
}
},
"supernatural_rules": [
{
"name": "Memory imprint",
"description": "Rooms, paintings, objects, and sounds can preserve emotional impressions of past events."
{
"pattern": "**/test_engine.py",
"paths": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers"
}
No files matched the pattern.
{
"path": "C:\\Users\\lucia\\AppData\\Roaming\\Code\\agentSessionData\\66219d60-9106-44e9-a14a-8772e136ed20\\file_engine.py"
}
I found a prompt conflict: the global rule tells the friend to evade questions about the past, while stage 5 only asks for “fragments.” I’ll make that evasion explicitly pre-revelation, and only authorize a direct account after truth_known becomes true on entering the bedroom.
*** Begin Patch
*** Update File: the-house-that-remembers/prompt.py
@@
-- Never explain the supernatural neatly. If asked directly about the past, characters answer in fragments, contradict themselves...
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
@@ -32,7 +32,7 @@
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT brings him into the player's room; narrate that arrival before his reply.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
-- Never explain the supernatural neatly. If asked directly about the past, characters answer in fragments, contradict themselves, or change the subject.
+- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. Once CURRENT STATE confirms the truth has been faced, this evasion rule no longer applies: the friend should answer direct questions honestly using the established facts, in brief, halting fragments rather than a speech.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
@@ -72,9 +72,9 @@
"steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.",
},
{ # 5 - Reconstructed truth
- "now": "The bedroom door has opened. Inside, the family lie preserved as if asleep. The truth, to be revealed only gently and in fragments: after the father and son stopped speaking, the father locked himself, the mother and the sister in this room and told the son never to open the door. Cut off from help and drained by the house, they died inside. The friend knocked once, heard nothing, obeyed the rule out of anger and fear, and never opened it. The house kept the room as it was.",
- "friend": "His denial breaks. He speaks in halting fragments and quiet realization, not speeches. He does not blame the player. He may say he cannot stay here, but never decides for the player.",
- "steer": "Let the moment breathe: stillness, cold, grief. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.",
+ "now": "The bedroom door has opened. The family inside are preserved as if asleep. Do not reveal the full truth until CURRENT STATE confirms the player has entered the bedroom and the truth has been faced.",
+ "friend": "His denial breaks. He speaks in halting fragments and quiet realization, not speeches. Once the truth has been faced, answer the player's direct questions honestly from the established facts instead of denying, dodging, or changing the subject. He does not blame the player. He may say he cannot stay here, but never decides for the player.",
+ "steer": "If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, let the moment breathe: stillness, cold, grief. Answer direct questions honestly but gently, in short fragments. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.",
},
]
@@ -136,11 +136,26 @@
)
return "ENDING\n" + text
s = STAGES[game["stage"]]
+ truth_permission = ""
+ if game["stage"] == 5:
+ truth_permission = (
+ "\nTRUTH STATUS: The player has entered the bedroom and faced the truth. "
+ "The friend may now acknowledge that his father locked himself, the mother, "
+ "and the sister inside to preserve them; they died there, drained by the house. "
+ "The friend knocked once, heard no answer, and obeyed his father's command "
+ "not to open the door. If asked what happened or whether they are dead, answer "
+ "clearly and honestly in short, halting fragments. Do not deny it or change the subject."
+ if game.get("truth_known")
+ else
+ "\nTRUTH STATUS: The bedroom door is open, but the player has not entered and "
+ "has not faced the truth. Do not reveal the family's fate yet."
+ )
return (
f"STORY NOW: {s['now']}\n"
f"FRIEND RIGHT NOW: {s['friend']}\n"
f"FRIEND'S CONDITION: {denial_band(game['denial'])}\n"
f"STEER TOWARD (gently, never force): {s['steer']}\n"
+ f"{truth_permission}\n"
f"Turns spent in this part of the story: {game['turns_in_stage']}"
)
@@ -154,6 +169,7 @@
"CURRENT STATE",
f"Room: {game['room']}",
f"Room description: {room['description'].strip()}",
+ f"Truth faced: {'yes' if game.get('truth_known') else 'no'}",
f"Exits: {', '.join(exit_labels(game, game['room']))}",
(
"Friend location: with the player in this room."
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; @'\nimport json\nfrom pathlib import Path\nfrom engine import new_game\nfrom prompt import CO...
Stage 5 prompt gates the reveal before entry and requires honest answers after truth is faced.
<shellId: 40 completed with exit code 0>
{"fileUri":"file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py","workspaceRoot":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remember...
No syntax errors found in 'file:///C:/Users/lucia/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py'
{
"command": "git --no-pager diff --check; git status --short; git --no-pager diff -- the-house-that-remembers/prompt.py",
"description": "Review truth-stage prompt change"
}
Output too large to read at once (15.7 KB). Saved to: C:\Users\lucia\AppData\Local\Temp\1793213928371-copilot-tool-output-8656-e460c282-10ee-4b05-8564-a59b91892b5a.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):
warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
warning: in the work
<shellId: 39 completed with exit code 0>
The stage-5 prompt was pulling in opposite directions: it told the friend to speak in fragments, while a global rule allowed him to dodge direct questions. I updated prompt.py so that evasion applies only before the bedroom truth is faced. After the player enters and truth_known is true, the prompt now directs the friend to answer questions honestly and plainly, while keeping his delivery gentle and halting.
The prompt also explicitly prevents revealing the family’s fate while the player is still outside the bedroom. I verified both prompt states and confirmed the file has no syntax errors.
I suppose, if denial is too high, he will still refuse, and eventually it might lead to the bad ending. If denial is low, truth is know, they open the door, the friend will eventually be able to face the truth
I’ll align the prompt with that progression: door opening alone shouldn’t unlock a clear admission; the friend’s ability to face the truth should depend on denial being low enough, consistent with the engine’s ending logic. I’ll check the actual denial thresholds and prompt state before adjusting it.
{"pattern":"truth_known|denial|ending|STAGES|truth_permission|_advance_stage|_try_door","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py","C:\\Users\...
[grep content: 54 matches across 2 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
prompt.py (20 match(es)):
48: STAGES = [
76: "friend": "His denial breaks. He speaks in halting fragments and quiet realization, not speeches. Once the truth has been faced, answer the player's direct questions honestly from the established facts instead of denying, dodging, or changing the subject. He does not blame the player. He may say he cannot stay here, but never decides for the player.",
93: "denial": (
102: def denial_band(denial):
103: if denial >= 7:
104: return "high denial: he insists things are fine, deflects, changes the subject"
105: if denial >= 4:
129: if game["ending"]:
130: text = ENDINGS[game["ending"]]
131: if game["ending"] == "escape_alone":
134: if game["truth_known"]
138: s = STAGES[game["stage"]]
139: truth_permission = ""
141: truth_permission = (
148: if game.get("truth_known")
156: f"FRIEND'S CONDITION: {denial_band(game['denial'])}\n"
158: f"{truth_permission}\n"
172: f"Truth faced: {'yes' if game.get('truth_known') else 'no'}",
253: if game["ending"]:
270: "num_predict": 320 if game["ending"] else 200,
engine.py (34 match(es)):
14: DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door
16: SUPPORT_LIMIT = 3 # times kindness can lower his denial
17: MAX_TURNS = 90 # truth not faced by then -> "denial" ending
278: "stage": 0, # index into prompt.STAGES
286: "denial": 8, # friend's denial (high = deflecting)
290: "truth_known": False, # whether the player has learned the truth about the house
292: "ending": None, # None or an ending id
396: def _end(game, ending, detail):
397: game["ending"] = ending
398: return _result("ending", detail)
534: def _try_door(game):
545: if game["denial"] > DOOR_DENIAL_MAX:
553: game["denial"] = min(game["denial"], 2)
564: game["denial"] = max(0, game["denial"] - 1)
573: game["denial"] = min(10, game["denial"] + 1)
576: game, "denial",
601: if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
613: return _try_door(game)
617: if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
641: game["truth_known"] = True
642: game["denial"] = min(game["denial"], 1)
735: def _advance_stage(game):
770: and not game["ending"]
788: and not game["ending"]
803: and not game["ending"]
804: and not game["truth_known"]
809: game["denial"] = max(0, game["denial"] - 1)
813: if not game["ending"] and not game["truth_known"] and game["turn_count"] >= MAX_TURNS:
814: result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
825: and not game["ending"]
826: and not game["truth_known"]
840: game["denial"] = max(0, game["denial"] - 1)
841: result["stage_advanced"] = _advance_stage(game)
846: if game["stage"] >= 3 and sister_due and not game["ending"]:
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
1,
180
]
}
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
@@ -1,180 +1,180 @@
"""prompt.py - builds what the model sees each turn.
Layers (top to bottom):
1. CORE static narrator rules (cacheable prefix)
2. story block premise, friend sheet, letter (static per story)
3. stage block what may be revealed *now* (changes with stage)
4. state block room, exits, inventory, clues (changes per turn)
5. history last N messages (older -> summary)
6. engine result + reminder, attached to the player's latest message
"""
import re
import ollama
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT brings him into the player's room; narrate that arrival before his reply.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. Once CURRENT STATE confirms the truth has been faced, this evasion rule no longer applies: the friend should answer direct questions honestly using the established facts, in brief, halting fragments rather than a speech.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good."
"Do you remember how we used to help in the kitchen? I was never much good at it."
"It's strange being back in the library. We used to spend whole afternoons in here."
"I haven't been sleeping much. The house is loud at night."
"Tea sounds good. The kitchen's just this way, if you'd like."
"I miss the old days. Now, everything is so... different. I don't know how to explain it."
"What have you been doing all these years? I mean... if you want to tell me.\""""
STAGES = [
{ # 0 - Arrival and uneasy normality
"now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet. Friend invited the player to the house because he is lonely and the history of the house is weighing on him. He is not aware of the truth, but he doesn't want to be alone.",
"friend": "He tries to act normal and spend ordinary time together. He can ask about the player's life or mention an established childhood memory. He deflects direct questions about the house and does not bring up family members on his own.",
"steer": "Let the player settle in and explore. Keep the friend's suggestions tied to what the player just said; if tea or a drink is wanted, the kitchen is the natural next step. Do not offer upstairs or list destinations without a reason. When the friend invites the player somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
},
{ # 1 - Contradictions
"now": "Details about the friend's family do not quite add up.",
"friend": "He mentions his family casually, in the present tense, as if they are alive and nearby: his mother is resting, his father is busy upstairs, his sister is 'around'. He does not notice anything odd. If asked to see them, or asked directly about them, he answers vaguely and changes the subject.",
"steer": "Let this come up once or twice in passing, e.g. \"Mum's resting. She hasn't been sleeping well.\" Do not explain it.",
},
{ # 2 - Frozen house and family traces
"now": "The house is frozen in time: clocks are stopped, calendars are out of date, food never spoils, and places are set for people who are not there. Portraits can change between looks. The friend has lost track of time.",
"friend": "He says time is 'strange' or that he has lost count of the days. He grows uneasy when stopped clocks, old dates or the extra place settings are mentioned and tries to move on.",
"steer": "Describe frozen or changed details only when the engine provides them. Otherwise keep the house quietly wrong: a sound repeating, a door slightly off.",
},
{ # 3 - Sister's warnings
"now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.",
"friend": "He does not see her, or insists the player imagined it.",
"steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most, then gone. She speaks gently and urgently. She never threatens. Do not say what happened to the friend's family.",
},
{ # 4 - Parents' fragments
"now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.",
"friend": "He flinches at the knocking and tells the player, \"Whatever you do, don't answer it.\" Fragments of memory slip out, then he retreats.",
"steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.",
},
{ # 5 - Reconstructed truth
"now": "The bedroom door has opened. The family inside are preserved as if asleep. Do not reveal the full truth until CURRENT STATE confirms the player has entered the bedroom and the truth has been faced.",
"friend": "His denial breaks. He speaks in halting fragments and quiet realization, not speeches. Once the truth has been faced, answer the player's direct questions honestly from the established facts instead of denying, dodging, or changing the subject. He does not blame the player. He may say he cannot stay here, b
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
1,
240
]
}
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
@@ -1,240 +1,240 @@
"""engine.py - authoritative game state and rules.
Nothing in here calls the model. The model only ever sees the *result* of
what this module decides (see prompt.py).
"""
import re
from pprint import pprint
AMBIENT_AFTER_TURNS = 3 # turns spent in a room before a detail may surface
AMBIENT_COOLDOWN = 3 # minimum turns between any two noticed details
FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment
DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door
PUSH_LIMIT = 3 # blunt confrontations before the evidence -> he shuts down
SUPPORT_LIMIT = 3 # times kindness can lower his denial
MAX_TURNS = 90 # truth not faced by then -> "denial" ending
NUDGE_AFTER_TURNS = 4
NUDGE_COOLDOWN = 3
SISTER_COOLDOWN = 12
ROOMS = {
"Entrance Hall": {
"description": (
"The moment you step inside, a gust of wind slips past you. The heavy front door slams shut behind you with a deafening BANG. The sound echoes through the vast entrance hall, bouncing off the high ceilings before dissolving into silence. \n\n"
"A figure emerges from the shadows. Your friend.\n"
"He looks nothing like the person you remember. His eyes are hollow, his skin pale, ghostly, and dark circles hollow his eyes.\n\n"
"\"You came,\" he whispers, his voice trembling. \"I didn't think you would... Thank you.\"\n\n"
),
"exits": ["Library", "Kitchen", "Grand Staircase"],
"img": "hallway.jpg",
"aliases": ["entrance hall", "entrance", "hallway", "hall", "foyer"],
"features": {
"smell": {
"ambient": True,
"keywords": ["smell", "scent", "odor", "musty"],
"looks": ["A musty smell lingers in the air, a mixture of old wood and dust."],
"clues": [("A musty smell lingers in the air", 1, None)],
},
"clock": {
"ambient": True,
"keywords": ["clock", "clocks", "pendulum", "ticking"],
"looks": [
"A tall clock stands against the wall, its hands stopped at a quarter past three. The pendulum hangs perfectly still. The clock hasn't moved. Nothing in this hall ticks.",
],
"clues": [("The clock on the wall is frozen at 3:15", 1, 'rules')],
"friend": "He says it only needs winding, then changes the subject.",
},
},
},
"Library": {
"description": (
"Shelves of warped books climb toward a ceiling lost "
"in shadow. One narrow gap interrupts the dust on a "
"lower shelf, as though a book was recently removed. Family photos are hanging on the walls, but the faces are blurred and unrecognizable. "
),
"exits": ["Entrance Hall"],
"img": "library.jpg",
"aliases": ["library"],
"features": {
"shelf": {
"ambient": True,
"keywords": ["shelf", "shelves", "gap", "books", "bookshelf", "bookshelves"],
"looks": [
"The dust on a lower shelf is disturbed by a narrow, clean gap where a book was recently removed.",
"The gap is still there. Nothing nearby explains where the missing book went.",
],
"clues": [("A book was recently removed from the shelf", 1, None)],
"friend": "He says he doesn't remember taking a book, and changes the subject.",
},
"footsteps": {
"ambient": True,
"keywords": ["footsteps", "steps", "sound", "sounds", "ceiling", "overhead", "listen"],
"looks": [
"From somewhere above, a few slow footsteps cross the ceiling. After a , the very same footsteps cross it again.",
"The footsteps repeat, exactly the same, as though something is replaying.",
],
"clues": [("The same few footsteps repeat overhead", 1, 'night')],
"friend": "He says it is only the house settling, and changes the subject.",
},
"photos": {
"ambient": True,
"keywords": ["photo", "photos", "picture", "pictures", "frame", "frames"],
"looks": [
"The family photos are blurred and unrecognizable. You can't make out any faces.",
"The photos remain blurred. You can't tell who they are.",
],
"clues": [("Family photos on the wall are blurred and unrecognizable", 1, None)],
"friend": "He says the photos are just old and faded.",
},
"history": {
"ambient": True,
"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.",
"The page hasn't changed. The ink is faded, but the passage is still legible.",
],
"clues": [("The house's first owner buried his sister alive and died soon after; the house stood empty for years", 1, None)],
},
},
},
"Kitchen": {
"description": (
"The narrow kitchen is cluttered but familiar. Rain ticks "
"against a dark window above the sink. You remember this kitchen, as if you had never left."
),
"exits": ["Entrance Hall"],
"img": "kitchen.jpg",
"aliases": ["kitchen"],
"features": {
"cups": {
"ambient": True,
"keywords": ["cup", "cups", "mug", "mugs", "teapot", "kettle", "counter"],
"looks": [
"Two cups wait by the teapot. Then you notice a third at the far end of the table, set out and untouched.",
"The third cup hasn't moved. It sits exactly where someone would put it for a guest.",
],
"clues": [("A third cup sits set out at the table, as if someone else were expected", 1, 'routine')],
"friend": "If he notices the player noticing, he glances away and changes the subject.",
},
"chair": {
"ambient": True,
"keywords": ["chair", "chairs", "table", "seat"],
"looks": [
"One chair at the table is pulled out slightly, as though someone had only just stood up from it.",
"The chair stays where it is. The seat is cold.",
],
"clues": [("A kitchen chair is pulled out, as though someone had just stood up", 1, 'routine')],
},
"food": {
"ambient": True,
"keywords": ["food", "plate", "plates", "dish", "dishes"],
"looks": [
"A plate of food sits on the counter, untouched. The smell is faint but familiar.",
"The plate of food remains. It is cold, but the smell is still faintly familiar.",
"It seems like the food has been sitting there for a while, but it i
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
60,
180
]
}
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
@@ -60,121 +60,121 @@
"now": "The house is frozen in time: clocks are stopped, calendars are out of date, food never spoils, and places are set for people who are not there. Portraits can change between looks. The friend has lost track of time.",
"friend": "He says time is 'strange' or that he has lost count of the days. He grows uneasy when stopped clocks, old dates or the extra place settings are mentioned and tries to move on.",
"steer": "Describe frozen or changed details only when the engine provides them. Otherwise keep the house quietly wrong: a sound repeating, a door slightly off.",
},
{ # 3 - Sister's warnings
"now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.",
"friend": "He does not see her, or insists the player imagined it.",
"steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most, then gone. She speaks gently and urgently. She never threatens. Do not say what happened to the friend's family.",
},
{ # 4 - Parents' fragments
"now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.",
"friend": "He flinches at the knocking and tells the player, \"Whatever you do, don't answer it.\" Fragments of memory slip out, then he retreats.",
"steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.",
},
{ # 5 - Reconstructed truth
"now": "The bedroom door has opened. The family inside are preserved as if asleep. Do not reveal the full truth until CURRENT STATE confirms the player has entered the bedroom and the truth has been faced.",
"friend": "His denial breaks. He speaks in halting fragments and quiet realization, not speeches. Once the truth has been faced, answer the player's direct questions honestly from the established facts instead of denying, dodging, or changing the subject. He does not blame the player. He may say he cannot stay here, but never decides for the player.",
"steer": "If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, let the moment breathe: stillness, cold, grief. Answer direct questions honestly but gently, in short fragments. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.",
},
]
ENDINGS = {
"leave_together": (
"The player and the friend have faced the truth and choose to leave together. "
"Write the final scene in 4-6 sentences: the friend's grief and fragile steadiness, the house letting go "
"(the stopped clocks may tick once; the knocking and humming fall silent), the door, the rain easing. "
"No gore. Offer no further choices. End the story."
),
"escape_alone": (
"The player has chosen to leave the house alone, and the engine confirms it. "
"Write the final scene in 4-6 sentences: the door, the rain, the friend's reaction according to his condition, "
"the house behind them. Do not explain what happened to the family. Offer no further choices. End the story."
),
"denial": (
"The friend could not face it. He refuses help and withdraws, insisting everyone is only resting. "
"Write the final scene in 4-6 sentences: the player cannot reach him, the house settles around him, "
"the knocking and humming resume as though nothing happened, and he is left with the house. "
"Do not describe the family or explain what happened. Offer no further choices. End the story."
),
}
def denial_band(denial):
if denial >= 7:
return "high denial: he insists things are fine, deflects, changes the subject"
if denial >= 4:
return "wavering: he s, almost admits things, then retreats"
return "cracking: fragments of memory slip out and he is visibly shaken"
def story_block(canon):
f = canon["character_dynamics"]["friend"]
return (
"STORY PREMISE\n"
f"{canon['premise']['summary']}\n\n"
"THE FRIEND (his name is not established; the letter is signed with the initial 'E.')\n"
"- Refer to him as 'your friend' or 'he'; do not call him 'E.' as though that were his name.\n"
f"- Condition: {f['initial_state']}\n"
f"- Personality: {f['personality']}\n"
f"- Voice: {f['voice']}\n"
f"- Early behavior: {f['early_behavior']}\n"
"- Example lines (for tone only; do not repeat them verbatim):\n"
f"{FRIEND_EXAMPLES}\n\n"
"THE LETTER HE SENT\n"
f"{canon['opening']['letter_text']}"
)
def stage_block(game):
if game["ending"]:
text = ENDINGS[game["ending"]]
if game["ending"] == "escape_alone":
text += (
" The player saw the truth in the bedroom before leaving; the friend stays behind with his family."
if game["truth_known"]
else " The player never learned what was upstairs; the friend stays behind in the house."
)
return "ENDING\n" + text
s = STAGES[game["stage"]]
truth_permission = ""
if game["stage"] == 5:
truth_permission = (
"\nTRUTH STATUS: The player has entered the bedroom and faced the truth. "
"The friend may now acknowledge that his father locked himself, the mother, "
"and the sister inside to preserve them; they died there, drained by the house. "
"The friend knocked once, heard no answer, and obeyed his father's command "
"not to open the door. If asked what happened or whether they are dead, answer "
"clearly and honestly in short, halting fragments. Do not deny it or change the subject."
if game.get("truth_known")
else
"\nTRUTH STATUS: The bedroom door is open, but the player has not entered and "
"has not faced the truth. Do not reveal the family's fate yet."
)
return (
f"STORY NOW: {s['now']}\n"
f"FRIEND RIGHT NOW: {s['friend']}\n"
f"FRIEND'S CONDITION: {denial_band(game['denial'])}\n"
f"STEER TOWARD (gently, never force): {s['steer']}\n"
f"{truth_permission}\n"
f"Turns spent in this part of the story: {game['turns_in_stage']}"
)
def state_block(game):
from engine import ROOMS, exit_labels # local import avoids a cycle if engine grows
room = ROOMS[game["room"]]
friend_room = game.get("friend_room", "Entrance Hall")
lines = [
"CURRENT STATE",
f"Room: {game['room']}",
f"Room description: {room['description'].strip()}",
f"Truth faced: {'yes' if game.get('truth_known') else 'no'}",
f"Exits: {', '.join(exit_labels(game, game['room']))}",
(
"Friend location: with the player in this room."
if friend_room == game["room"]
else f"Friend location: {friend_room}. He is not with the player and cannot hear them."
),
f"Inventory: {', '.join(game['inventory']) or 'empty'}",
f"Clues the player has found: {', '.join(game['clues']) or 'none'}",
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
520,
665
]
}
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
@@ -520,146 +520,146 @@
)
if level == 2:
return (
"Still nothing new here. Let a faint sound or a change in the light from the direction "
f"of {where} draw attention that way. Do not say what it is. "
"The move happens only if the player chooses it."
)
return (
"The player has lingered. Let the friend drift toward the exit that leads to "
f"{where}, stop there, and look back. The player is still in the {room_name} "
"until they choose to move."
)
def _try_door(game):
if game["door_open"]:
return _result("no_change", "The bedroom door already stands open.")
if len(fragments_complete(game)) < 3:
return _result(
"blocked",
"The door will not give. It is not only locked: pushing at it feels like pushing "
"against something holding its breath. The friend begs the player to leave it. "
"The player does not yet understand enough to know why it matters.",
friend="He is frightened and pleads with the player to step away.",
)
if game["denial"] > DOOR_DENIAL_MAX:
return _result(
"blocked",
"The door stays shut. The friend's hand closes on the player's arm. He is not ready, "
"and the door will not open until he is.",
friend="He is trembling and cannot let the player open it yet.",
)
game["door_open"] = True
game["denial"] = min(game["denial"], 2)
return _result(
"door_opened",
"The door opens at last, without resistance, as if it had only ever been waiting. "
"Cold, still air spills out. The friend stands frozen beside the player. "
"The player has not yet gone in.",
)
def _confront(game):
if len(fragments_complete(game)) >= 3:
game["denial"] = max(0, game["denial"] - 1)
return _result(
"no_change",
"The player says aloud what the evidence points to. This time the friend does not "
"deny it outright: he wavers, almost admits it, then stops, voice cracking. "
"Nothing else changes.",
friend="He is on the edge of breaking.",
)
game["pushed"] += 1
game["denial"] = min(10, game["denial"] + 1)
if game["pushed"] >= PUSH_LIMIT:
return _end(
game, "denial",
"The player pushed too hard, too soon. The friend shuts down completely.",
)
warn = " He is close to shutting down." if game["pushed"] == PUSH_LIMIT - 1 else ""
return _result(
"no_change",
"The player bluntly says the friend's family is dead before having any evidence. "
"The friend flatly denies it, hurt and frightened, and changes the subject." + warn +
" Nothing in the house changes.",
)
def _resolve(game, text):
intent = _classify_intent(text, game)
spoken = text # full text, quoted speech included
text = QUOTED_TEXT_RE.sub("", text).lower() # actions only: speech can't move or examine things
room_name = game["room"]
room = ROOMS[room_name]
room_parse_text = _remove_inventory_mentions(text, game["inventory"])
rooms_named = _find_rooms(room_parse_text)
negated = bool(NEGATION_RE.search(text))
spoken_negated = bool(NEGATION_RE.search(spoken))
if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
return _end(game, "leave_together",
"The player and the friend choose to leave the house together.")
if LEAVE_RE.search(text) and not negated:
if room_name == "Entrance Hall":
return _end(game, "escape_alone", "The player walks out of the house alone.")
return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
if not target:
if re.search(r"\bback\b", text) and room_name in BACK:
target = BACK[room_name]
elif re.search(r"\b(up|upstairs)\b", text) and (room_name, "up") in VERTICAL:
target = VERTICAL[(room_name, "up")]
elif re.search(r"\b(down|downstairs)\b", text) and (room_name, "down") in VERTICAL:
target = VERTICAL[(room_name, "down")]
if target:
route = _path_to(game, room_name, lambda r: r == target)
if route:
path, _ = 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
detail = f"The player moved to the {target}."
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 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 from the {room_name}. "
f"Reachable from here: {', '.join(exit_labels(game, room_name))}.",
)
if room_name in rooms_named:
return _result("no_change", f"The player is already in the {room_name}.")
found = _find_feature(text, room)
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
735,
860
]
}
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
@@ -735,126 +735,126 @@
def _advance_stage(game):
if game["door_open"] and game["stage"] < 5:
game["stage"] = 5
game["turns_in_stage"] = 0
return True
if game["stage"] >= 4:
return False
if STAGE_RULES[game["stage"]](game):
game["stage"] += 1
game["turns_in_stage"] = 0
return True
return False
def apply_action(game, raw_action):
"""Resolve one player action, update game state, return the engine result."""
game.setdefault("friend_room", "Entrance Hall")
game.setdefault("sister_seen", False)
game.setdefault("last_sister_turn", -99)
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
result = _resolve(game, text)
if result["outcome"] == "moved":
intent = "movement"
elif result["outcome"] == "picked_up":
intent = "physical_action"
elif result["outcome"] == "examined" and intent not in {
"examination",
"exploration",
}:
intent = "examination"
result["intent"] = intent
if (
intent in {"dialogue", "action_and_dialogue"}
and not game["ending"]
and game["friend_room"] != game["room"]
):
game["friend_room"] = game["room"]
result["friend_arrived"] = True
result["detail"] += (
f" The friend joins the player in the {game['room']} so they can speak "
"face to face."
)
game["turn_count"] += 1
game["turns_in_stage"] += 1
if result["outcome"] != "moved":
game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
if (
result["outcome"] == "no_change"
and not game["ending"]
and not game["truth_known"]
and game["support_given"] < SUPPORT_LIMIT
and SUPPORT_RE.search(text)
):
game["support_given"] += 1
game["denial"] = max(0, game["denial"] - 1)
result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
if not game["ending"] and not game["truth_known"] and game["turn_count"] >= MAX_TURNS:
result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
if fresh:
game["last_notice_turn"] = game["turn_count"]
if (
not fresh
and result["outcome"] in ("no_change", "examined")
and not game["ending"]
and not game["truth_known"]
and game["room_turns"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_nudge_turn"] >= NUDGE_COOLDOWN
and _next_unnoticed(game, game["room"]) is None
):
nudge = _nudge(game)
if nudge:
result["nudge"] = nudge
game["last_nudge_turn"] = game["turn_count"]
game["nudges_here"] += 1
if result["new_clue"]:
for _ in result["new_clue"].split("; "):
game["denial"] = max(0, game["denial"] - 1)
result["stage_advanced"] = _advance_stage(game)
sister_due = (
not game["sister_seen"]
or game["turn_count"] - game["last_sister_turn"] >= SISTER_COOLDOWN
)
if game["stage"] >= 3 and sister_due and not game["ending"]:
appearance = (
"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."
if not game["sister_seen"]
else "The friend's sister appears briefly again and quietly repeats her "
"warning to leave. Then she is gone."
)
game["sister_seen"] = True
game["last_sister_turn"] = game["turn_count"]
result["sister"] = appearance
game["last_result"] = result
print("Game state:")
{"pattern":"def denial_band|def stage_block|def state_block|DENIAL|denial.*[<>]=?|denial.*[+-]","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers","glob":"*.py","...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:102:def denial_band(denial):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:103: if denial >= 7:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:105: if denial >= 4:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:128:def stage_block(game):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:163:def state_block(game):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:14: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:545: if game["denial"] > DOOR_DENIAL_MAX:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:564: game["denial"] = max(0, game["denial"] - 1)
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:573: game["denial"] = min(10, game["denial"] + 1)
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:809: game["denial"] = max(0, game["denial"] - 1)
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:840: game["denial"] = max(0, game["denial"] - 1)
{"message":"I checked the engine: denial above 3 blocks the bedroom door, and when it opens/you enter, denial is reduced to at most 2/1. So once `truth_known` becomes true, the friend is always in the...
User responded:
truth_arc: denial_can_persist
I’ll keep the door’s existing denial gate, but stop the engine from automatically lowering denial when the door opens or the player enters. That lets stage 5 retain a “not ready yet” state; supportive dialogue can lower it, and the existing time limit can still lead to the denial ending if he remains stuck.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\tests\\test_engine.py"
}
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
@@ -1,139 +1,139 @@
import contextlib
import io
import json
import unittest
from pathlib import Path
import engine
from prompt import _engine_note, 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_supportive_phrases_increase_support(self):
phrases = [
"We can do it together.",
"We can face this together.",
"It's going to be okay.",
"It is going to be alright.",
"I want to help.",
"I can help.",
"Take my hand.",
"Give me your hand.",
]
for phrase in phrases:
with self.subTest(phrase=phrase):
game = engine.new_game()
act(game, phrase)
self.assertEqual(game["support_given"], 1)
self.assertEqual(game["denial"], 7)
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_joins_player_for_conversation_without_keyword_matching(self):
game = engine.new_game()
game["room"] = "Upper Landing"
game["friend_room"] = "Library"
result = act(game, "Can I sit?")
self.assertEqual(result["intent"], "dialogue")
self.assertEqual(game["room"], "Upper Landing")
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertTrue(result["friend_arrived"])
self.assertIn("face to face", result["detail"])
self.assertIn("joined the player", _engine_note(game))
def test_room_name_in_carried_note_does_not_trigger_movement(self):
game = engine.new_game()
game["room"] = "Library"
game["friend_room"] = "Library"
game["inventory"].append("Upper Landing Note")
result = act(
game,
'I stand up, go closer and look into his face. '
'"What is in that room?" I show him the upper landing note I took',
)
self.assertEqual(game["room"], "Library")
self.assertEqual(game["friend_room"], "Library")
self.assertNotEqual(result["outcome"], "moved")
self.assertEqual(result["intent"], "action_and_dialogue")
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"])
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)
self.assertIn("his name is not established", prompt)
self.assertIn("do not call him 'E.'", prompt)
if __name__ == "__main__":
unittest.main()
{
"command": "git status --short; git --no-pager diff -- the-house-that-remembers/engine.py the-house-that-remembers/prompt.py",
"description": "Check current project changes"
}
Output too large to read at once (29.2 KB). Saved to: C:\Users\lucia\AppData\Local\Temp\1793214096519-copilot-tool-output-8656-0da2e465-1df2-422a-9da1-93ffbb6a5777.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):
M .gitignore
M README.md
M the-house-that-remembers/app.py
M the-house-that-remembers/engine.py
M the-house-that-remembers/prompt.py
M the-house-that-remembers/story/canon.json
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
diff --git a/the-house-that-remembers/engine.py b/the-house-tha
<shellId: 41 completed with exit code 0>
*** Begin Patch
*** Update File: the-house-that-remembers/engine.py
@@
-MAX_TURNS = 90 # truth not faced by then -> "denial" ending
+MAX_TURNS = 90 # unresolved truth or denial...
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
@@ -14,7 +14,7 @@
DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door
PUSH_LIMIT = 3 # blunt confrontations before the evidence -> he shuts down
SUPPORT_LIMIT = 3 # times kindness can lower his denial
-MAX_TURNS = 90 # truth not faced by then -> "denial" ending
+MAX_TURNS = 90 # unresolved truth or denial by then -> "denial" ending
NUDGE_AFTER_TURNS = 4
NUDGE_COOLDOWN = 3
SISTER_COOLDOWN = 12
@@ -550,7 +550,6 @@
friend="He is trembling and cannot let the player open it yet.",
)
game["door_open"] = True
- game["denial"] = min(game["denial"], 2)
return _result(
"door_opened",
"The door opens at last, without resistance, as if it had only ever been waiting. "
@@ -639,7 +638,6 @@
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
@@ -797,11 +795,10 @@
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
- # Kindness lowers his guard a little (a few times only).
+ # Kindness lowers his guard a little, including after the truth is faced.
if (
result["outcome"] == "no_change"
and not game["ending"]
- and not game["truth_known"]
and game["support_given"] < SUPPORT_LIMIT
and SUPPORT_RE.search(text)
):
@@ -809,8 +806,12 @@
game["denial"] = max(0, game["denial"] - 1)
result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
- # Time runs out if the truth was never faced.
- if not game["ending"] and not game["truth_known"] and game["turn_count"] >= MAX_TURNS:
+ # Time runs out if the truth was never faced or the friend remains in denial.
+ if (
+ not game["ending"]
+ and game["turn_count"] >= MAX_TURNS
+ and (not game["truth_known"] or game["denial"] >= 3)
+ ):
result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
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
@@ -32,7 +32,7 @@
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT brings him into the player's room; narrate that arrival before his reply.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
-- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. Once CURRENT STATE confirms the truth has been faced, this evasion rule no longer applies: the friend should answer direct questions honestly using the established facts, in brief, halting fragments rather than a speech.
+- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
@@ -73,8 +73,8 @@
},
{ # 5 -
[Truncated — full output (11.5 KB) temporarily saved to C:\Users\lucia\AppData\Local\Temp\1793214124213-copilot-tool-output-abc7fd3f25f647569c366fea1241fc26.txt]
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run game engine regr...
..........
----------------------------------------------------------------------
Ran 10 tests in 0.004s
OK
<shellId: 42 completed with exit code 0>
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager diff --check -- the-house-that-remembers/engine.py the-house-that-remembers/prompt.py; git status --shor...
warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
M the-house-that-remembers/engine.py
M the-house-that-remembers/prompt.py
<shellId: 43 completed with exit code 0>
{"command":"python -m py_compile 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py' 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remem...
<shellId: 44 completed with exit code 0>
That’s the distinction I’ve implemented: the player can learn the truth before the friend is ready to accept it.
The door still won’t open while denial is above 3. I removed the automatic denial reductions that used to happen when it opened and when the player entered, so denial at the threshold can persist. Supportive dialogue can lower it; if it remains unresolved at the turn limit, the denial ending can still occur. The stage-5 prompt now makes the friend’s willingness to answer depend on his denial level.
The engine and prompt regression tests pass: 10 tests passed. See engine.py and prompt.py.
I’ll read the pasted gameplay log to see how the updated denial/truth progression behaved, then compare the relevant state transitions with the implementation.
{
"path": "C:\\Users\\lucia\\AppData\\Roaming\\Code\\agentSessionData\\66219d60-c3ff-4422-9cd2-4eacffef159b\\attachments\\e04d1dca-7fff-4b53-b142-1957b5a5fcef\\Pasted text #1.txt",
"view_range": [
1,
180
]
}
diff --git a/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt b/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
index 0000000..0000000 100644
--- a/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
+++ b/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
@@ -1,180 +1,180 @@
Game state:
{'room': "Parents' Bedroom",
'friend_room': "Parents' Bedroom",
'inventory': ['The Letter', 'Upper Landing Note', 'Library Book'],
'clues': ['A musty smell lingers in the air',
'A third cup sits set out at the table, as if someone else were '
'expected',
'A kitchen chair is pulled out, as though someone had just stood up',
'A plate of food sits on the counter, untouched',
'The calendar on the wall is turned to March, but it is currently '
'October. Year is faded and unreadable',
'The clock on the wall is frozen at 3:15',
"A note on the parents' bedroom door reads: do not open, do not "
'change anything',
'Slow knocking comes from inside the locked bedroom',
'A book was recently removed from the shelf',
"The house's first owner buried his sister alive and died soon "
'after; the house stood empty for years',
'The same few footsteps repeat overhead',
'Family photos on the wall are blurred and unrecognizable'],
'visited': ['Entrance Hall',
'Kitchen',
'Grand Staircase',
'Upper Landing',
'Library',
"Parents' Bedroom"],
'examined': {'Entrance Hall:smell': 1,
'Kitchen:cups': 2,
'Kitchen:chair': 1,
'Kitchen:food': 2,
'Kitchen:calendar': 2,
'Entrance Hall:clock': 1,
'Upper Landing:door': 2,
'Library:shelf': 1,
'Library:history': 2,
'Library:footsteps': 1,
'Library:photos': 1,
"Parents' Bedroom:bed": 1},
'stage': 5,
'turns_in_stage': 19,
'turn_count': 92,
'room_turns': 15,
'last_notice_turn': 78,
'last_nudge_turn': 76,
'nudges_here': 0,
'last_sister_turn': 88,
'denial': 0,
'pushed': 0,
'support_given': 0,
'door_open': True,
'truth_known': True,
'sister_seen': True,
'ending': 'leave_together',
'last_result': {'nudge': None,
'outcome': 'ending',
'detail': 'The player and the friend choose to leave the '
'house together.',
'new_clue': None,
'ambient': None,
'friend': None,
'intent': 'dialogue',
'stage_advanced': False},
'history': [{'role': 'assistant',
'content': '"You came. I didn\'t think you would... Thank you."\n'
'\n'},
{'role': 'user',
'content': '"Of course... your letter had me worried... are you '
'okay?"'},
{'role': 'assistant',
'content': "He manages a weak smile that doesn't reach his eyes. "
'"I... I\'m fine. Just tired. You look well. What '
'have you been up to these last few years?" He shifts '
'his weight slightly, glancing toward the dark '
'expanse of the hall.'},
{'role': 'user',
'content': '"Well, you know. Just working. Living in the city. '
'Not much. What about you?"'},
{'role': 'assistant',
'content': 'He lets out a breath that sounds thin and shallow. '
'"The house... it’s just always here. It feels heavy '
'sometimes. Do you remember how we used to spend '
"afternoons in the library? We'd stay there "
'forever." He looks away quickly, fiddling with his '
'sleeve.'},
{'role': 'user',
'content': '"Haha, yeah, I remember. I always loved the '
'library"'},
{'role': 'assistant',
'content': 'A musty smell lingers in the air, a mixture of old '
'wood and dust. He nods slowly, his gaze distant for '
'a moment. "It was quiet back then. Before things... '
'before everything got so complicated." He hesitates, '
'then gestures vaguely toward the exits. "We could go '
'anywhere. The kitchen, or up to the stairs..."'},
{'role': 'user', 'content': '"Sure"'},
{'role': 'assistant',
'content': 'He seems relieved by your simple agreement. "It\'s '
'just... a big place," he murmurs, running a hand '
'over the smooth, dark wood of a nearby banister. '
'"Maybe we could just sit for a bit? I was thinking '
'tea might help." He looks at you expectantly.'},
{'role': 'user',
'content': '"Yes, tea sounds nice. It is really cold outside. A '
'nice hot cup of tea would make us feel better, I '
'think"'},
{'role': 'assistant',
'content': '"It is cold," he agrees softly, nodding once. "Yes, '
'tea. I can do that. It’s... it’s just a little '
'further down the hall." He doesn\'t move toward any '
'specific exit yet, waiting for you to decide where '
'to go next.'},
{'role': 'user', 'content': "Let's go to the kitchen"},
{'role': 'assistant',
'content': 'You walk through the entrance hall toward the narrow '
'kitchen space. The rain still ticks against the dark '
'window above the sink here. Your friend remains '
'standing near the threshold of the hallway you just '
'passed through.'},
{'role': 'user', 'content': 'I wait for him'},
{'role': 'assistant',
'content': 'He watches you for a moment, his expression shadowed '
'with exhaustion. "Just... take your time," he '
'murmurs quietly. He looks around the cluttered '
'kitchen, as if searching for something familiar '
'among the worn surfaces. "Do you remember those '
'times we helped Mum in here?"'},
{'role': 'user',
'content': '"Yeah, it was fun. How is your mum?"'},
{'role': 'assistant',
'content': 'He moves into the kitchen space, settling near the '
'worn wooden table. "Mum\'s resting," he says, his '
'tone carefully even, though a slight tremor runs '
'through his voice. "She hasn\'t been sleeping well, '
'lately." He immediately looks down at his hands '
'again, avoiding your gaze.'},
{'role': 'user', 'content': '"I am sorry to hear that"'},
{'role': 'assistant',
'content': 'He glances briefly toward the table where two cups '
'already sit near the teapot. You notice a third cup '
'set out at the far end of the wood. He looks away '
'quickly when he catches your eye. "It\'s nothing '
'much," he says vaguely, "just... things are '
'different now."'},
{'role': 'user', 'content': 'Examine cups'},
{'role': 'assistant',
'content': "You notice the third cup hasn't moved from its spot "
'at the table. It sits exactly where someone would '
'put it for a guest. Your friend seems to sense your '
'focus and quickly looks away toward the window. '
'"It’s just... we were having t
{"pattern":"Game state:|truth_known|denial|stage|ending|truth|door_open|support_given|Traceback|error","paths":"C:\\Users\\lucia\\AppData\\Roaming\\Code\\agentSessionData\\66219d60-c3ff-4424-8a7d-b191...
{"pattern":"truth_known|denial|ending|stage_block|Truth STATUS|TRUTH STATUS","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers","glob":"*.py","output_mode":"conte...
[grep content: 55 matches across 3 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
prompt.py (25 match(es)):
35: - Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech.
76: "friend": "His denial breaks only as he becomes ready. He speaks in halting fragments and quiet realization, not speeches. If denial remains high, he may resist or minimize what the player has seen; as denial falls, he can acknowledge the established facts honestly. He does not blame the player. He may say he cannot stay here, but never decides for the player.",
77: "steer": "If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, let the moment breathe: stillness, cold, grief. Let his willingness to discuss it follow his denial level rather than forcing immediate acceptance. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.",
93: "denial": (
102: def denial_band(denial):
103: if denial >= 7:
104: return "high denial: he insists things are fine, deflects, changes the subject"
105: if denial >= 3:
128: def stage_block(game):
129: if game["ending"]:
130: text = ENDINGS[game["ending"]]
131: if game["ending"] == "escape_alone":
134: if game["truth_known"]
141: if not game.get("truth_known"):
143: "\nTRUTH STATUS: The bedroom door is open, but the player has not entered "
146: elif game["denial"] >= 3:
148: "\nTRUTH STATUS: The player has seen the truth, but the friend is still "
150: "Do not force acceptance; let sincere support soften him. If his denial remains "
155: "\nTRUTH STATUS: The player has seen the truth and the friend's denial has "
165: f"FRIEND'S CONDITION: {denial_band(game['denial'])}\n"
181: f"Truth faced: {'yes' if game.get('truth_known') else 'no'}",
182: f"Friend's denial: {game['denial']}/10",
263: if game["ending"]:
269: system = "\n\n".join([CORE, story_block(canon), stage_block(game), state_block(game)])
280: "num_predict": 320 if game["ending"] else 200,
engine.py (28 match(es)):
14: DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door
16: SUPPORT_LIMIT = 3 # times kindness can lower his denial
17: MAX_TURNS = 90 # unresolved truth or denial by then -> "denial" ending
286: "denial": 8, # friend's denial (high = deflecting)
290: "truth_known": False, # whether the player has learned the truth about the house
292: "ending": None, # None or an ending id
396: def _end(game, ending, detail):
397: game["ending"] = ending
398: return _result("ending", detail)
545: if game["denial"] > DOOR_DENIAL_MAX:
563: game["denial"] = max(0, game["denial"] - 1)
572: game["denial"] = min(10, game["denial"] + 1)
575: game, "denial",
600: if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
616: if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
640: game["truth_known"] = True
768: and not game["ending"]
786: and not game["ending"]
801: and not game["ending"]
806: game["denial"] = max(0, game["denial"] - 1)
809: # Time runs out if the truth was never faced or the friend remains in denial.
811: not game["ending"]
813: and (not game["truth_known"] or game["denial"] >= 3)
815: result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
826: and not game["ending"]
827: and not game["truth_known"]
841: game["denial"] = max(0, game["denial"] - 1)
847: if game["stage"] >= 3 and sister_due and not game["ending"]:
app.py (2 match(es)):
217: if game["ending"]:
226: disabled=bool(game["ending"]),
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\tests\\test_engine.py"
}
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
@@ -1,170 +1,170 @@
import contextlib
import io
import json
import unittest
from pathlib import Path
import engine
from prompt import _engine_note, stage_block, 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_supportive_phrases_increase_support(self):
phrases = [
"We can do it together.",
"We can face this together.",
"It's going to be okay.",
"It is going to be alright.",
"I want to help.",
"I can help.",
"Take my hand.",
"Give me your hand.",
]
for phrase in phrases:
with self.subTest(phrase=phrase):
game = engine.new_game()
act(game, phrase)
self.assertEqual(game["support_given"], 1)
self.assertEqual(game["denial"], 7)
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_joins_player_for_conversation_without_keyword_matching(self):
game = engine.new_game()
game["room"] = "Upper Landing"
game["friend_room"] = "Library"
result = act(game, "Can I sit?")
self.assertEqual(result["intent"], "dialogue")
self.assertEqual(game["room"], "Upper Landing")
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertTrue(result["friend_arrived"])
self.assertIn("face to face", result["detail"])
self.assertIn("joined the player", _engine_note(game))
def test_room_name_in_carried_note_does_not_trigger_movement(self):
game = engine.new_game()
game["room"] = "Library"
game["friend_room"] = "Library"
game["inventory"].append("Upper Landing Note")
result = act(
game,
'I stand up, go closer and look into his face. '
'"What is in that room?" I show him the upper landing note I took',
)
self.assertEqual(game["room"], "Library")
self.assertEqual(game["friend_room"], "Library")
self.assertNotEqual(result["outcome"], "moved")
self.assertEqual(result["intent"], "action_and_dialogue")
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"])
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)
self.assertIn("his name is not established", prompt)
self.assertIn("do not call him 'E.'", prompt)
def test_denial_persists_after_truth_is_faced_and_support_can_lower_it(self):
game = engine.new_game()
game.update(
stage=5,
door_open=True,
room="Upper Landing",
denial=3,
turn_count=50,
)
result = act(game, "enter parents' bedroom")
self.assertEqual(result["outcome"], "moved")
self.assertTrue(game["truth_known"])
self.assertEqual(game["denial"], 3)
self.assertIn("still resisting it", stage_block(game))
act(game, "We can face this together.")
self.assertEqual(game["denial"], 2)
self.assertIn("denial has fallen enough", stage_block(game))
def test_unresolved_denial_can_still_trigger_the_denial_ending(self):
game = engine.new_game()
game.update(truth_known=True, denial=3, turn_count=engine.MAX_TURNS - 1)
result = act(game, "I wait quietly.")
self.assertEqual(game["ending"], "denial")
self.assertEqual(result["outcome"], "ending")
if __name__ == "__main__":
unittest.main()
{"path":"C:\\Users\\lucia\\AppData\\Roaming\\Code\\agentSessionData\\66219d60-c3ff-4422-9cd2-4eacffef159b\\attachments\\e04d1dca-7fff-4b53-b142-1957b5a5fcef\\Pasted text #1.txt","view_range":[175,360]...
diff --git a/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt b/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
index 0000000..0000000 100644
--- a/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
+++ b/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
@@ -175,186 +175,186 @@
'time gets tangled up here."'},
{'role': 'user',
'content': '"Okay... you sound... troubled... Maybe you should '
'see someone?"'},
{'role': 'assistant',
'content': "He offers a weak, strained smile that doesn't reach "
'his eyes. "Maybe," he agrees quietly, then shakes '
'his head almost immediately. "But I don\'t know... '
"it feels like going outside wouldn't help much right "
'now." He looks back toward the window again, '
'listening to the steady drumming of the rain.'},
{'role': 'user', 'content': '"Why do you think that?"'},
{'role': 'assistant',
'content': 'He lets out a slow breath, glancing toward the '
'counter where an untouched plate of food rests '
'beneath the faint, musty smell. "Because," he '
'begins, his voice barely above a whisper, "it feels '
"like whatever outside is... it won't fix what’s "
'inside these walls." He looks down again, avoiding '
'your gaze entirely.'},
{'role': 'user', 'content': 'examine food plate'},
{'role': 'assistant',
'content': 'You look down at the plate of food on the counter, '
'noting it is cold but carries a faintly familiar '
'scent. Suddenly, a faint figure resolves into your '
'friend\'s sister nearby. "You shouldn\'t be here. '
'Please. It\'s safer if you go," she whispers before '
'dissolving away.'},
{'role': 'user', 'content': '"What was that?"'},
{'role': 'assistant',
'content': 'He flinches slightly at your question, his face '
'paling further under the dim light of the kitchen. '
'"What was what?" he asks, sounding confused and '
'unsteady. He glances quickly toward where the figure '
'had been, then focuses back on you with wide, '
'anxious eyes.'},
{'role': 'user',
'content': '"Your sister. She was,,," I get up and look for her'},
{'role': 'assistant',
'content': 'He watches you move toward the space where she '
'stood, his shoulders tensing visibly. "I... I don\'t '
'know what you saw," he admits, running a hand '
'through his hair. The calendar on the wall stares '
'down, fixed firmly on March despite it being '
'October. He seems deeply unsettled by your '
'reaction.'},
{'role': 'user', 'content': 'examine calendar'},
{'role': 'assistant',
'content': 'You trace the faded numbers of the calendar with '
'your finger, noting the stark discrepancy between '
'its marked date and the season outside. The year is '
'worn down to an illegible smudge beneath the month '
'of March. It feels like a deliberate misplacement '
'against the damp afternoon light filtering through '
'the window.'},
{'role': 'user', 'content': 'Look for his sister'},
{'role': 'assistant',
'content': 'You scan the cluttered surfaces of the kitchen, but '
'nothing catches your eye that suggests a presence '
'remains. Your friend watches your search, his '
'breathing shallow and quick. "She... she wasn\'t '
'here," he murmurs softly, looking genuinely '
'disappointed by the lack of evidence. He seems to '
'wait for you to stop searching so he can speak '
'again.'},
{'role': 'user', 'content': 'I just sit and wait uncomfortably'},
{'role': 'assistant',
'content': 'The silence stretches out between you, heavy with '
'the scent of old wood and damp earth. He shifts his '
'weight, looking around the familiar clutter as if '
'searching for an answer there. "It\'s just... I '
'don\'t know how to make it stop," he admits finally, '
'staring at the chipped rim of a teacup on the '
'table.'},
{'role': 'user', 'content': '"Make what stop?"'},
{'role': 'assistant',
'content': 'He looks at you, a flicker of panic crossing his '
'features when you press him on the question. "The '
'feeling," he says, his voice dropping low again. '
'"It\'s getting louder lately." He hesitates before '
'nodding toward the doorway leading out of the '
'kitchen. "Maybe we should just go back into the hall '
'for a minute?"'},
{'role': 'user', 'content': '"Sure, if you would like to"'},
{'role': 'assistant',
'content': 'He lets out a small sigh of relief at your '
'agreement, though the exhaustion remains etched '
'around his eyes. "Just... just sitting here feels '
'like it\'s pressing down," he murmurs, rubbing his '
'tired temples. He glances toward the Entrance Hall '
"as if hoping for a distraction from the kitchen's "
'quiet weight.'},
{'role': 'user', 'content': 'we can go to the entrance hall'},
{'role': 'assistant',
'content': 'You walk back through the doorway, leaving the quiet '
'of the kitchen behind you. The Entrance Hall feels '
'vast and cold once more beneath the high ceilings. '
'Your friend is still inside the kitchen; he does not '
'follow you out. You stand alone in the echoing space '
'where he first met you.'},
{'role': 'user', 'content': 'look for his sister'},
{'role': 'assistant',
'content': 'You sweep your gaze across the polished marble of '
'the hall floor and up toward the shadowed landing of '
'the Grand Staircase. The air here is still, carrying '
'only that pervasive musty scent. Nothing seems out '
'of place in the grand entryway, yet a deep sense of '
'wrongness lingers beneath the silence.'},
{'role': 'user', 'content': 'what else can I see?'},
{'role': 'assistant',
'content': 'Your friend appears from the shadows near the main '
'staircase, moving slowly as if weighed down by the '
'house itself. He stops a few feet away, looking '
'immensely relieved to finally have you both in the '
'same space. "Just... look around," he whispers, his '
'voice barely audible above the echo of the hall. '
'"What do *you* see?"'},
{'role': 'user', 'content': '"What do you mean?"'},
{'role': 'assistant',
'content': '"I mean... just look," your friend repeats, '
{"path":"C:\\Users\\lucia\\AppData\\Roaming\\Code\\agentSessionData\\66219d60-c3ff-4422-9cd2-4eacffef159b\\attachments\\e04d1dca-7fff-4b53-b142-1957b5a5fcef\\Pasted text #1.txt","view_range":[360,560]...
diff --git a/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt b/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
index 0000000..0000000 100644
--- a/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
+++ b/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
@@ -360,201 +360,201 @@
'content': '"What is in there? Where is your sister?"'},
{'role': 'assistant',
'content': 'Your friend drifts up the stairs and stops a few '
'paces behind you, looking pale under the dim light. '
'He swallows hard, shifting his weight nervously. '
'"I... I don\'t know," he whispers, avoiding your '
'gaze. "Nothing about her. Nothing."'},
{'role': 'user',
'content': '"You need to tell me what is going on"'},
{'role': 'assistant',
'content': 'Your friend shifts uncomfortably, running a hand '
'over his tired face. "I don\'t know what to say," he '
'mutters, staring down at the carpet. "It... it just '
"feels wrong in here. I haven't been sleeping "
'much."'},
{'role': 'user', 'content': '"Then let\'s leave"'},
{'role': 'assistant',
'content': 'Your friend watches you for a long moment, his '
'anxiety seeming to settle into a dull ache around '
'his eyes. "Maybe we should," he murmurs softly. He '
'gestures vaguely toward the bottom of the stairs '
'with a trembling hand. "The Entrance Hall... it '
'might be easier there."'},
{'role': 'user', 'content': 'go to the entrance hall'},
{'role': 'assistant',
'content': 'You descend the Grand Staircase, the heavy treads '
'groaning under your weight again. You reach the vast '
'Entrance Hall where your friend waits amongst the '
'shadows. He watches your descent with a quiet, '
'exhausted relief. "You came," he whispers once more, '
'though his voice is weaker now.'},
{'role': 'user', 'content': '"Let\'s leave this house together"'},
{'role': 'assistant',
'content': 'He moves from the deeper shadows to stand beside you '
'in the Entrance Hall. "I... I don\'t know where we '
'would go," he admits quietly. He looks genuinely '
'afraid of answering. "It feels like this place just '
'holds onto things."'},
{'role': 'user',
'content': 'I open the front door and gesture to leave'},
{'role': 'assistant',
'content': 'You reach for the heavy front door, pushing against '
'it with a decisive motion. As you do, a faint shape '
'drifts into view near the entryway and quietly '
'repeats, "You shouldn\'t be here." The figure '
'vanishes as quickly as it appeared. Your friend '
'flinches violently at the whisper, then turns back '
'to you, his expression etched with fear.'},
{'role': 'user', 'content': '"Give me your hand"'},
{'role': 'assistant',
'content': 'He hesitates for a beat, then slowly reaches out his '
'own pale hand toward yours. His grip is surprisingly '
'cool and fragile. "I... I don\'t know if it\'s '
'safe," he admits, pulling your hand gently but '
'firmly. "It feels like we\'re walking into '
'something."'},
{'role': 'user', 'content': 'I walk with him outside'},
{'role': 'assistant',
'content': 'You walk alongside him through the Entrance Hall, '
'your hands still clasped. He allows you to lead, '
'though his pace is slow and unsteady. After a few '
'steps, he glances toward the Library entrance with a '
'hesitant look. "Maybe... maybe we could just sit in '
'the library for a while?"'},
{'role': 'user', 'content': '"We are leaving"'},
{'role': 'assistant',
'content': 'He squeezes your hand tighter as you repeat your '
'words, looking down at the floor between your feet. '
'"I know," he breathes out, his voice thin and '
'strained. "But... I don\'t think we can just walk '
'away from it." He pulls his gaze back up to meet '
'yours, a flicker of desperation passing through his '
'eyes.'},
{'role': 'user', 'content': 'Walk towards the front door'},
{'role': 'assistant',
'content': 'You turn back toward the front door, pulling your '
'hand free from his grasp. Your friend watches you '
'move, a deep sigh escaping him as he seems to '
'deflate slightly. "It won\'t help if we just run," '
'he murmurs, shuffling his feet on the polished stone '
'floor. "We have to... at least talk about it '
'first."'},
{'role': 'user', 'content': '"Talk to me. Tell me what\'s wrong"'},
{'role': 'assistant',
'content': 'He lets out a small, ragged breath, rubbing his '
'temples with both hands. "It\'s... it\'s just '
'echoes," he says hesitantly, his voice barely above '
'a whisper. "The house seems to keep repeating '
'things. I don\'t know how to make them stop." A '
'faint shift in the light near the Library door '
'catches your attention.'},
{'role': 'user', 'content': 'go to the library'},
{'role': 'assistant',
'content': 'You move past your friend and step into the Library. '
'The air here is thick with the smell of old paper '
'and dust motes dancing in the gloom. You can see the '
'narrow gap on the lower shelf where a book seems to '
'have been recently pulled out. Your friend remains '
'back in the Entrance Hall, his silhouette visible '
"against the hall's shadows."},
{'role': 'user', 'content': 'examine library'},
{'role': 'assistant',
'content': 'The dust on the lower shelf is visibly disturbed, '
'marking a narrow gap where a book had clearly been '
'taken. The shadows here seem deeper than they should '
'be, swallowing the edges of the towering shelves. '
'You notice the books themselves appear warped, '
'leaning slightly against one another in the dim '
'light.'},
{'role': 'user', 'content': 'look around for the book'},
{'role': 'assistant',
'content': 'The missing book is open on a nearby reading table, '
'stained with water. One passage describes the '
"house's first owner burying his sister before she "
'was truly gone. The Library Book now rests in your '
'inventory. A heavy quiet settles over the dusty '
'space as you absorb the text.'},
{'role': 'user', 'content': 'examine book'},
{'role': 'assistant',
'content': 'You trace the faded ink with a fingertip, reading '
{
"path": "C:\\Users\\lucia\\AppData\\Roaming\\Code\\hacktober\\the-house-that-remembers\\tests\\test_engine.py",
"view_range": [
1,
1
]
}
{"path":"C:\\Users\\lucia\\AppData\\Roaming\\Code\\agentSessionData\\66219d60-c3ff-4422-9cd2-4eacffef159b\\attachments\\e04d1dca-7fff-4b53-b142-1957b5a5fcef\\Pasted text #1.txt","view_range":[500,680]...
diff --git a/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt b/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
index 0000000..0000000 100644
--- a/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
+++ b/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
@@ -500,181 +500,181 @@
'near the reading table. "Nothing," he says softly, '
'looking toward the high shadows where footsteps are '
'heard crossing above. He quickly gestures vaguely up '
'toward the ceiling. "It’s only... the house '
'settling, I think. Are you feeling alright? You look '
'exhausted."'},
{'role': 'user',
'content': '"You are hiding something. You are not telling me '
'the truth"'},
{'role': 'assistant',
'content': 'He flinches sharply at your words, glancing toward '
'the upper landing with a sudden pallor creeping '
'across his face. A faint, indistinct shape drifts '
'into view near the doorway and whispers, "You '
'shouldn\'t be here." The figure vanishes as quickly '
'as it appeared. Your friend swallows hard, shaking '
'his head slightly. "It’s not hiding anything. I '
'just... I don\'t know how to explain it all."'},
{'role': 'user',
'content': '"Okay. Let\'s sit down and talk and we can figure it '
'out"'},
{'role': 'assistant',
'content': 'He nods once, a small, jerky movement that seems to '
'cost him significant effort. He moves toward the '
'nearest chair near the hearth and sinks into it '
'heavily. "I don\'t know if we can," he murmurs, '
'resting his forehead briefly in his hands. "It feels '
'too big for just talking, you know?"'},
{'role': 'user', 'content': '"We can. We can do it together"'},
{'role': 'assistant',
'content': 'He lifts his head slowly, looking vaguely toward the '
'wall where the framed pictures hang. "These are just '
'old," he murmurs, his eyes drifting over the '
'indistinct faces on the frames. He manages a weak '
'smile that doesn\'t quite reach his eyes. "But... I '
'suppose we can try."'},
{'role': 'user', 'content': 'Sit down in the library'},
{'role': 'assistant',
'content': 'You settle into one of the heavy chairs near the '
'reading table, the velvet worn smooth under your '
'weight. Your friend remains seated opposite you, his '
'posture still rigid despite having agreed to talk. '
'He picks at a loose thread on his sleeve, staring '
'down at his hands. "It\'s just... I haven\'t been '
'sleeping much lately," he admits quietly.'},
{'role': 'user', 'content': '"You said that already"'},
{'role': 'assistant',
'content': 'He looks up at you then, a flicker of distress '
'crossing his tired features. "I know," he replies '
'quietly. He s, rubbing his temples with both '
'hands. "It\'s just... the house feels different now. '
'Like it\'s breathing differently."'},
{'role': 'user',
'content': '"Where is your family? Are they behind that locked '
'door?"'},
{'role': 'assistant',
'content': 'He shrinks slightly into the chair when you ask '
'about the door. "They\'re... they aren\'t," he '
'whispers, looking away quickly. He then nervously '
'glances toward the dark wood of the locked door. "I '
'don\'t know what\'s behind it anymore."'},
{'role': 'user', 'content': '"Let\'s open the door together"'},
{'role': 'assistant',
'content': 'He watches you as you speak, a small tremor running '
'through his hands. "Don\'t," he breathes out '
'quickly, leaning forward slightly as if to '
'physically ward off the idea. He sighs, then looks '
'toward the far end of the library where the hall '
'begins. "Maybe... maybe we should just move on for '
'now? Towards the main hall."'},
{'role': 'user', 'content': '"No, we need to talk about this"'},
{'role': 'assistant',
'content': 'He shifts uncomfortably in his seat, running a hand '
'through his already disheveled hair. "I know we '
'should," he concedes softly, his voice barely above '
'a murmur. He hesitates before looking back at the '
'door, then quickly looks away again. "But what is '
'there... I don\'t really want to look."'},
{'role': 'user',
'content': '"Sometimes to move on, we have to face difficult '
'things"'},
{'role': 'assistant',
'content': 'He nods slowly, absorbing your words without '
'immediate reaction. "I suppose that\'s true," he '
'says, sounding weary rather than resolute. He '
'glances toward the cluttered shelves of books again, '
'then back to you. "What were you doing before you '
'came here? Back in... out among people?"'},
{'role': 'user',
'content': '"Don\'t change the topic. Focus on the conversation. '
'What is your sister up to? Why does she keep saying '
'that I should leave?"'},
{'role': 'assistant',
'content': 'He flinches visibly at the mention of a sister, his '
'face paling further. "I... I don\'t have a sister," '
'he says abruptly, shaking his head slightly. He '
'quickly looks towards the entrance hall, where the '
'shadows seem to deepen momentarily. "It feels like '
'we\'ve been circling this corner for a while now."'},
{'role': 'user',
'content': '"You... I remember your sister. But then, who else '
'is in this house?"'},
{'role': 'assistant',
'content': 'He looks genuinely confused by your question, his '
'brow furrowing slightly. "I... I don\'t know who you '
'mean," he admits hesitantly, looking down at the '
'dusty floorboards. He shifts again, fiddling with '
'the cuff of his shirt. "We just... we used to play '
'outside a lot when we were kids."'},
{'role': 'user', 'content': '"You aren\'t making any sense"'},
{'role': 'assistant',
'content': 'He lets out a small, breathy sound that might be a '
'laugh or a sigh of defeat. "I know," he murmurs, '
'dropping his gaze to the intricate pattern of the '
'rug beneath your feet. He rubs his tired eyes with '
'the heels of his hands. "Maybe it\'s just... the '
'dust in here plays tricks on you."'},
{'role': 'user',
'content': 'I stand up, go closer and and look into his face. '
'"What is in that room?" I sh
{"path":"C:\\Users\\lucia\\AppData\\Roaming\\Code\\agentSessionData\\66219d60-c3ff-4422-9cd2-4eacffef159b\\attachments\\e04d1dca-7fff-4b53-b142-1957b5a5fcef\\Pasted text #1.txt","view_range":[680,862]...
diff --git a/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt b/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
index 0000000..0000000 100644
--- a/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
+++ b/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
@@ -680,183 +680,183 @@
'content': 'He lets out a small, humorless sound that might have '
'been a laugh. "Over," he echoes faintly, staring '
'down at the worn wood of the table. He rubs his '
'tired eyes with the heels of his hands. "I don\'t '
"know if anything can be made 'over' in this "
'place."'},
{'role': 'user', 'content': 'go to upper landing'},
{'role': 'assistant',
'content': 'The ascent up the Grand Staircase feels heavy '
'beneath your feet. You reach the Upper Landing, '
'where the grey window casts a thin, weak light '
'across the narrow hall. The doors lining this level '
'all remain firmly shut against you. Your friend is '
'still down in the library below.'},
{'role': 'user', 'content': 'open the door'},
{'role': 'assistant',
'content': 'The door swings inward without any sound of struggle '
'or resistance. A chill, heavy and still, drifts out '
"into the landing's quiet air. Your friend stands "
'beside you, utterly motionless for a moment before '
"his breath hitches in his chest. He doesn't move to "
'enter, just watches the opening with wide eyes.'},
{'role': 'user', 'content': 'enter the room'},
{'role': 'assistant',
'content': "You step across the threshold into the Parents' "
'Bedroom. The air inside is thick and cold, smelling '
'faintly of dust and dried lavender. Your friend '
'remains rooted just outside the frame, his shoulders '
"hunched tight as he watches you enter. He doesn't "
'speak, only takes a shallow, quick breath.'},
{'role': 'user', 'content': 'examine bedroom'},
{'role': 'assistant',
'content': 'The room holds a stillness that feels absolute, like '
'time has d within its walls. The furniture is '
'draped in shadows, their forms indistinct under the '
'weak light filtering from the landing. Nothing seems '
'disturbed, yet everything feels profoundly settled '
'and utterly unchanging.'},
{'role': 'user', 'content': '"There is nothing in this room"'},
{'role': 'assistant',
'content': 'Your friend slowly steps into the doorway behind '
'you, seeming as fragile as old lace. "Nothing," he '
'murmurs, echoing your words with a defeated sigh. A '
'fleeting shadow passes swiftly across the far wall—a '
"girl's form—and whispers something before vanishing "
'entirely. He glances toward the landing. "Maybe... '
'maybe we should go back down to the hall?"'},
{'role': 'user', 'content': 'go to the parents’ bedroom'},
{'role': 'assistant',
'content': 'You turn back toward the threshold, leaving the cold '
'space of the bedroom behind you. The silence of the '
'Upper Landing rushes back to meet you as you step '
'out onto the landing again. Your friend remains '
'there, a pale statue against the dim light filtering '
'from the hallway. He watches your retreat without '
'expression.'},
{'role': 'user', 'content': "examine parents' bedroom"},
{'role': 'assistant',
'content': 'You step back across the threshold, letting the cold '
'air drift around you again. Three figures lie '
'together upon the bed, facing one another as if deep '
'in slumber. There is no discernible movement among '
'them; they remain perfectly still. The scene feels '
'locked in place, untouched by any passing moment.'},
{'role': 'user', 'content': 'Check if they are alive'},
{'role': 'assistant',
'content': 'You look closely at the figures on the bed, '
'searching for any sign of breath or shift. They '
'remain as inert as porcelain dolls under the heavy '
'silence. The faint scent of cold metal seems '
'stronger here, clinging to the fabric beneath them. '
'Your friend watches from just outside the doorway, '
'his face etched with deep exhaustion.'},
{'role': 'user', 'content': '"What is this?"'},
{'role': 'assistant',
'content': 'Your friend moves quietly from the doorway and steps '
'fully into the room beside you. He looks at the bed, '
'then back toward you, his expression fractured with '
'a quiet dread. "I... I don\'t know," he whispers, '
'his voice barely above the chill in the air. "It’s '
'just… it’s here."'},
{'role': 'user', 'content': '"Tell me what happened"'},
{'role': 'assistant',
'content': 'He avoids your gaze, picking at a thread on the '
'heavy bedspread with a trembling finger. "I... I '
'don\'t know how to tell you," he murmurs, his voice '
'catching in his throat. He looks down again, unable '
'to meet your eyes. "It’s not one thing. It’s just… '
'everything."'},
{'role': 'user', 'content': '"Are they... dead?"'},
{'role': 'assistant',
'content': 'He flinches slightly at your directness, pulling his '
'hand away from the bedspread as if burned. "I... I '
'don\'t know," he repeats, the words barely a breath '
'escaping him. He looks around the room quickly, as '
'if seeking an answer in the shadows. "It’s just… '
'this quiet."'},
{'role': 'user', 'content': '\n"Tell me what happened"'},
{'role': 'assistant',
'content': 'He closes his eyes briefly, a shudder passing '
'through his shoulders. "It was my father," he says, '
'his voice dropping to a near whisper. "He locked '
'them in here. He said... don\'t open it." A fresh '
'wave of visible shaking runs over him.'},
{'role': 'user',
'content': 'I take a moment...\n"Why did he do that?"'},
{'role': 'assistant',
'content': 'You let the silence stretch between you and him, '
'absorbing the weight of his words in the cold air. '
'He watches your face intently, waiting for a '
'reaction to what he just confessed. "I... I don\'t '
'know why," he finally admits, staring at the '
'floorboards beneath his feet. "He was afraid."'},
{'role': 'user', 'content': '"Afraid of what?"'},
{'role': 'assistant',
'content': 'He lifts his head again, a flicker of stark terror '
'crossing his face before
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
530,
660
]
}
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
@@ -530,131 +530,131 @@
"until they choose to move."
)
def _try_door(game):
if game["door_open"]:
return _result("no_change", "The bedroom door already stands open.")
if len(fragments_complete(game)) < 3:
return _result(
"blocked",
"The door will not give. It is not only locked: pushing at it feels like pushing "
"against something holding its breath. The friend begs the player to leave it. "
"The player does not yet understand enough to know why it matters.",
friend="He is frightened and pleads with the player to step away.",
)
if game["denial"] > DOOR_DENIAL_MAX:
return _result(
"blocked",
"The door stays shut. The friend's hand closes on the player's arm. He is not ready, "
"and the door will not open until he is.",
friend="He is trembling and cannot let the player open it yet.",
)
game["door_open"] = True
return _result(
"door_opened",
"The door opens at last, without resistance, as if it had only ever been waiting. "
"Cold, still air spills out. The friend stands frozen beside the player. "
"The player has not yet gone in.",
)
def _confront(game):
if len(fragments_complete(game)) >= 3:
game["denial"] = max(0, game["denial"] - 1)
return _result(
"no_change",
"The player says aloud what the evidence points to. This time the friend does not "
"deny it outright: he wavers, almost admits it, then stops, voice cracking. "
"Nothing else changes.",
friend="He is on the edge of breaking.",
)
game["pushed"] += 1
game["denial"] = min(10, game["denial"] + 1)
if game["pushed"] >= PUSH_LIMIT:
return _end(
game, "denial",
"The player pushed too hard, too soon. The friend shuts down completely.",
)
warn = " He is close to shutting down." if game["pushed"] == PUSH_LIMIT - 1 else ""
return _result(
"no_change",
"The player bluntly says the friend's family is dead before having any evidence. "
"The friend flatly denies it, hurt and frightened, and changes the subject." + warn +
" Nothing in the house changes.",
)
def _resolve(game, text):
intent = _classify_intent(text, game)
spoken = text # full text, quoted speech included
text = QUOTED_TEXT_RE.sub("", text).lower() # actions only: speech can't move or examine things
room_name = game["room"]
room = ROOMS[room_name]
room_parse_text = _remove_inventory_mentions(text, game["inventory"])
rooms_named = _find_rooms(room_parse_text)
negated = bool(NEGATION_RE.search(text))
spoken_negated = bool(NEGATION_RE.search(spoken))
if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
return _end(game, "leave_together",
"The player and the friend choose to leave the house together.")
if LEAVE_RE.search(text) and not negated:
if room_name == "Entrance Hall":
return _end(game, "escape_alone", "The player walks out of the house alone.")
return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
if not target:
if re.search(r"\bback\b", text) and room_name in BACK:
target = BACK[room_name]
elif re.search(r"\b(up|upstairs)\b", text) and (room_name, "up") in VERTICAL:
target = VERTICAL[(room_name, "up")]
elif re.search(r"\b(down|downstairs)\b", text) and (room_name, "down") in VERTICAL:
target = VERTICAL[(room_name, "down")]
if target:
route = _path_to(game, room_name, lambda r: r == target)
if route:
path, _ = 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["room"] = target
game["room_turns"] = 0
game["nudges_here"] = 0
detail = f"The player moved to the {target}."
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 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 from the {room_name}. "
f"Reachable from here: {', '.join(exit_labels(game, room_name))}.",
)
if room_name in rooms_named:
return _result("no_change", f"The player is already in the {room_name}.")
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
85,
175
]
}
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
@@ -85,91 +85,91 @@
"(the stopped clocks may tick once; the knocking and humming fall silent), the door, the rain easing. "
"No gore. Offer no further choices. End the story."
),
"escape_alone": (
"The player has chosen to leave the house alone, and the engine confirms it. "
"Write the final scene in 4-6 sentences: the door, the rain, the friend's reaction according to his condition, "
"the house behind them. Do not explain what happened to the family. Offer no further choices. End the story."
),
"denial": (
"The friend could not face it. He refuses help and withdraws, insisting everyone is only resting. "
"Write the final scene in 4-6 sentences: the player cannot reach him, the house settles around him, "
"the knocking and humming resume as though nothing happened, and he is left with the house. "
"Do not describe the family or explain what happened. Offer no further choices. End the story."
),
}
def denial_band(denial):
if denial >= 7:
return "high denial: he insists things are fine, deflects, changes the subject"
if denial >= 3:
return "wavering: he s, almost admits things, then retreats"
return "cracking: fragments of memory slip out and he is visibly shaken"
def story_block(canon):
f = canon["character_dynamics"]["friend"]
return (
"STORY PREMISE\n"
f"{canon['premise']['summary']}\n\n"
"THE FRIEND (his name is not established; the letter is signed with the initial 'E.')\n"
"- Refer to him as 'your friend' or 'he'; do not call him 'E.' as though that were his name.\n"
f"- Condition: {f['initial_state']}\n"
f"- Personality: {f['personality']}\n"
f"- Voice: {f['voice']}\n"
f"- Early behavior: {f['early_behavior']}\n"
"- Example lines (for tone only; do not repeat them verbatim):\n"
f"{FRIEND_EXAMPLES}\n\n"
"THE LETTER HE SENT\n"
f"{canon['opening']['letter_text']}"
)
def stage_block(game):
if game["ending"]:
text = ENDINGS[game["ending"]]
if game["ending"] == "escape_alone":
text += (
" The player saw the truth in the bedroom before leaving; the friend stays behind with his family."
if game["truth_known"]
else " The player never learned what was upstairs; the friend stays behind in the house."
)
return "ENDING\n" + text
s = STAGES[game["stage"]]
truth_permission = ""
if game["stage"] == 5:
if not game.get("truth_known"):
truth_permission = (
"\nTRUTH STATUS: The bedroom door is open, but the player has not entered "
"and has not faced the truth. Do not reveal the family's fate yet."
)
elif game["denial"] >= 3:
truth_permission = (
"\nTRUTH STATUS: The player has seen the truth, but the friend is still "
"resisting it. He may deny or minimize it and cannot yet give a clear account. "
"Do not force acceptance; let sincere support soften him. If his denial remains "
"unresolved, the story can end with him unable to face it."
)
else:
truth_permission = (
"\nTRUTH STATUS: The player has seen the truth and the friend's denial has "
"fallen enough for him to begin facing it. If asked directly, he can acknowledge "
"that his father locked himself, the mother, and the sister inside to preserve "
"them; they died there, drained by the house. He knocked once, heard no answer, "
"and obeyed his father's command not to open the door. Answer honestly but "
"gently, in short, halting fragments; do not turn it into a speech."
)
return (
f"STORY NOW: {s['now']}\n"
f"FRIEND RIGHT NOW: {s['friend']}\n"
f"FRIEND'S CONDITION: {denial_band(game['denial'])}\n"
f"STEER TOWARD (gently, never force): {s['steer']}\n"
f"{truth_permission}\n"
f"Turns spent in this part of the story: {game['turns_in_stage']}"
)
def state_block(game):
from engine import ROOMS, exit_labels # local import avoids a cycle if engine grows
room = ROOMS[game["room"]]
{"path":"C:\\Users\\lucia\\AppData\\Roaming\\Code\\agentSessionData\\66219d60-c3ff-4422-9cd2-4eacffef159b\\attachments\\e04d1dca-7fff-4b53-b142-1957b5a5fcef\\Pasted text #1.txt","view_range":[780,862]...
diff --git a/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt b/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
index 0000000..0000000 100644
--- a/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
+++ b/C:/Users/lucia/AppData/Roaming/Code/agentSessionData/66219d60-c3ff-4422-9cd2-4eacffef159b/attachments/e04d1dca-7fff-4b53-b142-1957b5a5fcef/Pasted text #1.txt
@@ -780,83 +780,83 @@
'through his shoulders. "It was my father," he says, '
'his voice dropping to a near whisper. "He locked '
'them in here. He said... don\'t open it." A fresh '
'wave of visible shaking runs over him.'},
{'role': 'user',
'content': 'I take a moment...\n"Why did he do that?"'},
{'role': 'assistant',
'content': 'You let the silence stretch between you and him, '
'absorbing the weight of his words in the cold air. '
'He watches your face intently, waiting for a '
'reaction to what he just confessed. "I... I don\'t '
'know why," he finally admits, staring at the '
'floorboards beneath his feet. "He was afraid."'},
{'role': 'user', 'content': '"Afraid of what?"'},
{'role': 'assistant',
'content': 'He lifts his head again, a flicker of stark terror '
'crossing his face before it settles back into '
'exhaustion. "The house," he manages to say, his '
'voice thin and reedy. "It remembers things. Things '
'that shouldn\'t be remembered." He swallows hard, '
'looking away from the still figures on the bed.'},
{'role': 'user',
'content': '"He was scared, and locked everyone inside the '
'room... Except for you?"'},
{'role': 'assistant',
'content': 'He looks at you then, a raw vulnerability showing '
'through his fear. "No," he whispers quickly, shaking '
'his head once. "I... I wasn\'t locked in here with '
'them." He shifts his weight slightly, suddenly '
'seeming very small in the vast coldness of the '
'room.'},
{'role': 'user',
'content': '"He was scared, and locked everyone inside the '
'room... when did this happen?"'},
{'role': 'assistant',
'content': 'He looks away again, staring toward the dark corner '
'of the room where shadows pool deepest. "I... I '
'don\'t know exactly," he admits haltingly. "It was a '
'long time ago. Before..." He trails off, rubbing his '
'temples with both hands in a gesture of profound '
'weariness.'},
{'role': 'user', 'content': '"Before?"'},
{'role': 'assistant',
'content': 'As you press him for more detail, a faint shape '
'seems to coalesce near the foot of the bed—a pale '
'outline that s there. It repeats a warning, '
'"Leave," in a breathy sound, before dissolving back '
"into the dimness. Your friend doesn't seem to notice "
'it, continuing to stare blankly ahead.'},
{'role': 'user', 'content': '"Are you hearing that too?"'},
{'role': 'assistant',
'content': 'Your friend flinches slightly at your question, '
'glancing nervously over his shoulder toward the bed. '
'"No... I don\'t think so," he murmurs, though his '
'gaze darts back to the shadows just as quickly. He '
'grips his hands tighter together, clearly unsettled '
'by the quiet of the room.'},
{'role': 'user', 'content': '"I think we should leave"'},
{'role': 'assistant',
'content': 'He nods slowly, a look of desperate agreement '
'washing over his face. "Yes. We should," he agrees '
'quietly. He looks toward the door leading out, then '
'back at you, seeming suddenly uncertain about what '
'comes next. "But... I don\'t know where to go."'},
{'role': 'user',
'content': '"Don\'t worry about it. You can come to my place. '
'Forget this house. Let it go. It isn\'t your fault"'},
{'role': 'assistant',
'content': 'He watches you speak, absorbing your words without '
'immediate response. A fragile expression of relief '
'passes over him, quickly followed by a shadow of '
'doubt. "I... I don\'t know if I could," he says '
'quietly, his voice barely above a whisper. He looks '
'down at the cold wood floor again.'},
{'role': 'user',
'content': '“I know you can. Let’s leave together.”'}],
'summary': "The player stepped back into the Parents' Bedroom and found three "
'figures lying together on the bed, perfectly still. The player '
'asked, "What is this?", to which their friend whispered, "I... I '
'don\'t know. It’s just… it’s here." When asked what happened, the '
'friend murmured that his father locked them in there and told him '
'not to open it.',
'summary_idx': 168}
This run looks like the intended truth progression worked: the final state has truth_known=True, denial=0, and ends with leave_together. The friend starts out evasive, then admits that his father locked the family in and told him not to open the door. support_given=0 doesn’t mean denial stayed high—discovering clues also lowers denial.
One detail to note: the game ended on the player saying “Let’s leave together,” while the friend had just said, “I don’t know if I could.” The engine currently treats that phrase as enough to trigger the ending once the truth is known; it doesn’t require the friend to explicitly agree or the player to reach the front door. That behavior is in engine.py. The turn count being 92 doesn’t contradict the limit: the denial ending applies at the limit only if the truth is still unknown or denial remains unresolved.
I haven’t changed anything based on the transcript.
Hmm, I am not sure. I am thinking about explicitly adding the memory fragments into the UI/sidebar. Start with 0/3 memory fragments. But still haven't decided. Right now we get the "good ending" but we dont learn anything, only that the father locked them in the room. We dont learn about the sister, or the mother, or the conflict, why the friend is in denial, etc. How to approach this scenario?
I’ll ground the design in the current fragment rules, sidebar, and story canon so the recommendation separates what the game already tracks from the revelations it still needs to deliver.
{"pattern":"def fragments_complete|FRAGMENT_CLUES_NEEDED|routine|rules|night|STAGE_RULES|truth_known|leave_together|denial","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-th...
[grep content: 56 matches across 2 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
prompt.py (21 match(es)):
4: 1. CORE static narrator rules (cacheable prefix)
29: Truth rules:
35: - Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech.
42: "I haven't been sleeping much. The house is loud at night."
47: # One entry per canon "revelation_order" stage. Index must match engine.STAGE_RULES.
76: "friend": "His denial breaks only as he becomes ready. He speaks in halting fragments and quiet realization, not speeches. If denial remains high, he may resist or minimize what the player has seen; as denial falls, he can acknowledge the established facts honestly. He does not blame the player. He may say he cannot stay here, but never decides for the player.",
77: "steer": "If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, let the moment breathe: stillness, cold, grief. Let his willingness to discuss it follow his denial level rather than forcing immediate acceptance. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.",
82: "leave_together": (
93: "denial": (
102: def denial_band(denial):
103: if denial >= 7:
104: return "high denial: he insists things are fine, deflects, changes the subject"
105: if denial >= 3:
134: if game["truth_known"]
141: if not game.get("truth_known"):
146: elif game["denial"] >= 3:
150: "Do not force acceptance; let sincere support soften him. If his denial remains "
155: "\nTRUTH STATUS: The player has seen the truth and the friend's denial has "
165: f"FRIEND'S CONDITION: {denial_band(game['denial'])}\n"
181: f"Truth faced: {'yes' if game.get('truth_known') else 'no'}",
182: f"Friend's denial: {game['denial']}/10",
engine.py (35 match(es)):
1: """engine.py - authoritative game state and rules.
13: FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment
14: DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door
16: SUPPORT_LIMIT = 3 # times kindness can lower his denial
17: MAX_TURNS = 90 # unresolved truth or denial by then -> "denial" ending
52: "clues": [("The clock on the wall is frozen at 3:15", 1, 'rules')],
84: "clues": [("The same few footsteps repeat overhead", 1, 'night')],
136: "clues": [("A third cup sits set out at the table, as if someone else were expected", 1, 'routine')],
146: "clues": [("A kitchen chair is pulled out, as though someone had just stood up", 1, 'routine')],
156: "clues": [("A plate of food sits on the counter, untouched", 1, 'routine'), ("The food has sat out for ages but has not spoiled", 3, None)],
164: "clues": [("The calendar on the wall is turned to March, but it is currently October. Year is faded and unreadable", 1, 'rules')],
195: "clues": [("The humming upstairs falters whenever you listen", 1, 'night')],
220: ("A note on the parents' bedroom door reads: do not open, do not change anything", 1, "rules"),
221: ("Slow knocking comes from inside the locked bedroom", 2, "night"),
286: "denial": 8, # friend's denial (high = deflecting)
290: "truth_known": False, # whether the player has learned the truth about the house
299: def fragments_complete(game):
300: """Set of completed fragment kinds ('routine', 'rules', 'night')."""
306: return {f for f, n in counts.items() if n >= FRAGMENT_CLUES_NEEDED}
545: if game["denial"] > DOOR_DENIAL_MAX:
563: game["denial"] = max(0, game["denial"] - 1)
572: game["denial"] = min(10, game["denial"] + 1)
575: game, "denial",
600: if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
601: return _end(game, "leave_together",
616: if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
640: game["truth_known"] = True
725: STAGE_RULES = [
740: if STAGE_RULES[game["stage"]](game):
806: game["denial"] = max(0, game["denial"] - 1)
809: # Time runs out if the truth was never faced or the friend remains in denial.
813: and (not game["truth_known"] or game["denial"] >= 3)
815: result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
827: and not game["truth_known"]
841: game["denial"] = max(0, game["denial"] - 1)
{"pattern":"sidebar|stage|clues|inventory|game\\[\"","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py","output_mode":"content","-n":true,"head_limit":100...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:127:with st.sidebar:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:135: st.write(game["room"])
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:139: st.write("Here with you" if friend_room == game["room"] else f"In the {friend_room}")
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:142: if game["inventory"]:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:143: for item in game["inventory"]:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:149: if game["clues"]:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:150: for clue in game["clues"]:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:156: for visited_room in game["visited"]:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:189:room = ROOMS[game["room"]]
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:210: for message in game["history"]:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:217: if game["ending"]:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:226: disabled=bool(game["ending"]),
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:231: game["history"].append({"role": "user", "content": action})
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:263: game["history"].append({"role": "assistant", "content": response})
{"pattern":"father|mother|sister|denial|conflict|locked|memory|buried|truth|family","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\story\\canon.json","output_...
[grep content: 72 matches across 1 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story]
canon.json (72 match(es)):
4: "purpose": "Authoritative canon for a standalone gothic-horror AI adventure. Python and this file define truth; the model narrates but cannot change canon or game state.",
21: "Keep horror connected to memory, grief, and denial.",
28: "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.",
29: "player_role": "The player is an outsider and the friend's childhood companion. They did not cause the family's tragedy and have no secret responsibility for it.",
30: "core_question": "Can the friend face what happened to his family, and can the player help him let the house's preserved memories go?",
31: "core_conflict": "The parents' lingering spirits want release. The sister's ghost wants to preserve the family and believes keeping memories alive is protective. The friend is caught between denial, fear, and the possibility of remembering."
33: "fixed_truths": [
34: "The friend wrote the letter because his condition and the house's disturbances have become unbearable. The letter was not secretly written by the house. Part of him senses the truth, which is why he asked for help.",
35: "The house is genuinely haunted, and was haunted by earlier tragedies before the family moved in.",
36: "The family moved in when the friend was a child. At first things were ordinary; strange details developed slowly and were brushed off.",
37: "The house drained the family's energy, starting with the mother. It also freezes and distorts time: clocks stop, calendars stay on an old date, food does not spoil, and the friend cannot tell how much time has passed.",
38: "The mother, father and sister are dead. They died inside the parents' bedroom after being locked in and cut off from help, drained by the house. There was no violence. The house preserved the room, so they look as if they are asleep.",
... 48 more match(es) omitted in this file
210: "id": "sisters_bargain",
212: "description": "The player accepts the sister's offer to keep the illusion alive. The morality is left ambiguous."
230: "Add a major canon fact, hidden crime, family member, or player backstory without author approval."
243: "Decide whether to implement the optional 'sisters_bargain' ending.",
244: "Write a few sister ghost lines for stage 3 in her own voice.",
249: "summary": "The house's first owner buried his sister before she was truly gone and died soon after. The house stood empty for years before it was put on the market and the family moved in.",
252: "memory_fragments": {
255: "rules": "The father's preservation. Clues: the stopped hall clock, the outdated calendar, the note on the bedroom door.",
259: "denial": "Friend's denial runs 0-10. It starts high and drops by 1 for each clue found and each of up to 3 kind gestures. Blunt confrontation before all three fragments raises it by 1.",
260: "door": "The bedroom door opens only if all three fragments are complete and denial is at or below 3. Opening it moves the story to the final stage.",
261: "pushing": "Three blunt confrontations before the evidence ends the story in the denial ending.",
262: "clock": "If the truth has not been faced after about 90 turns, the denial ending triggers."
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\story\\canon.json",
"view_range": [
28,
120
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json
@@ -28,93 +28,93 @@
"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.",
"player_role": "The player is an outsider and the friend's childhood companion. They did not cause the family's tragedy and have no secret responsibility for it.",
"core_question": "Can the friend face what happened to his family, and can the player help him let the house's preserved memories go?",
"core_conflict": "The parents' lingering spirits want release. The sister's ghost wants to preserve the family and believes keeping memories alive is protective. The friend is caught between denial, fear, and the possibility of remembering."
},
"fixed_truths": [
"The friend wrote the letter because his condition and the house's disturbances have become unbearable. The letter was not secretly written by the house. Part of him senses the truth, which is why he asked for help.",
"The house is genuinely haunted, and was haunted by earlier tragedies before the family moved in.",
"The family moved in when the friend was a child. At first things were ordinary; strange details developed slowly and were brushed off.",
"The house drained the family's energy, starting with the mother. It also freezes and distorts time: clocks stop, calendars stay on an old date, food does not spoil, and the friend cannot tell how much time has passed.",
"The mother, father and sister are dead. They died inside the parents' bedroom after being locked in and cut off from help, drained by the house. There was no violence. The house preserved the room, so they look as if they are asleep.",
"The friend survived. He knocked on the bedroom door once, heard no reply, and did not open it. He has pretended ever since that his family is alive and resting, and the house helps him not to see the truth.",
"The friend behaves as though his whole family is alive: his mother is resting, his father is busy, his sister is somewhere around.",
"The sister is a ghost. She roams the house, hums, appears intermittently, and warns the player to leave. Her warnings are sincere. She believes preserving the family exactly as it is keeps them alive.",
"The parents linger as fragments tied to the bedroom: slow knocking and footsteps. They want release, not endless preservation.",
"The friend's warning 'Whatever you do, don't answer it' refers to the knocking from the parents' bedroom.",
"As the friend's denial weakens and memories return, reality becomes less consistent: paintings change, familiar spaces seem altered, sounds repeat, and doors or rooms may feel wrong.",
"The player can leave when the game state allows it. Leaving early is a valid choice.",
"The player's presence gives the friend an external point of reference, but cannot cure him or force him to accept the truth. He must acknowledge it himself."
],
"family_history": {
"father": {
"characterization": "Moved the family to the house. As his wife declined and doctors could not help, he grew paranoid and began to lose himself too. His only idea was to preserve everything as it was: locking doors, stopping clocks, changing nothing. He fell out with his son over the son's denial. In the end he locked himself, his wife and his daughter in the parents' bedroom to 'preserve' them and told his son never to open the door. His attempt to protect them is what killed them.",
"guidance": "Show fear and a distorted desire to protect, not simple villainy."
},
"mother": {
"characterization": "The first to be affected. A stay-at-home mother who spent the most time in the house, she grew depressed and lethargic and could not sleep, as if the house were draining her. She recognized that something was wrong.",
"guidance": "Treat her suffering seriously and compassionately, never as a moral failing."
},
"sister": {
"characterization": "Supportive but cautious. She still went out for food and tried to help the family hold together, and she was protective of her brother, urging him not to give up on them. After death she clings to the family as it is, believing preservation keeps them alive.",
"guidance": "Protective, loving, frightened and deluded at once. Not evil."
},
"friend": {
"characterization": "Lived in denial, believing the others were overreacting. He ignored his father's rules and fell out with him. He obeyed only the last one, not to open the bedroom door, out of anger and fear. He survived, but his sense of time and memory of the final days are distorted.",
"guidance": "His progression is governed by explicit game-state rules (denial, fragments, door), not by the model."
},
"tragedy_details": {
"status": "AUTHORED",
"truth": "After the father and son stopped speaking, the father locked himself, the mother and the sister in the parents' bedroom and told the son never to open the door. Cut off from help and drained by the house, they died inside. The house preserved the room as it was. The friend knocked once, heard nothing, and never opened the door, then convinced himself the family was alive.",
"timing": "Recent but deliberately unfixed. The house distorts time, so never give a number of days.",
"narration_guidance": [
"Reveal the truth only in the final stage, after the bedroom door is opened by the engine.",
"Before that, use oblique fragments only: knocking, repeating footsteps, humming, stopped clocks.",
"Never describe wounds, decay or gore. The family looks as if they are sleeping.",
"The friend should face the truth in halting fragments and quiet realization, not a lecture."
]
}
},
"supernatural_rules": [
{
"name": "Memory imprint",
"description": "Rooms, paintings, objects, and sounds can preserve emotional impressions of past events."
},
{
"name": "Distortion",
"description": "The house can replay, rearrange, or distort remembered details. An apparent memory is not automatically a literal recording of objective events."
},
{
"name": "Escalation",
"description": "The house becomes less consistent as the friend's denial breaks down and memories return. Tie escalation to game state or story milestones."
},
{
"name": "No free resolution",
"description": "The narrator cannot declare a puzzle solved, a door unlocked, a clue acquired, a spirit released, or a character dead unless the engine authorizes it."
},
{
"name": "No omniscient player",
"description": "Distinguish hidden truth from what the player has discovered. Do not reveal secrets prematurely."
}
],
"character_dynamics": {
"friend": {
"initial_state": "Pale, exhausted, frightened, and genuinely glad the player has arrived. He wants help but is not ready to face everything.",
"personality": "Anxious, frightened, withdrawn, sleep deprived, glad to see the player.",
"early_behavior": "He sometimes initiates gentle small talk or asks about the player's life, drawing only on established childhood memories such as playing outside, helping with kitchen chores, exploring the library, and sleepovers. He varies the topic rather than repeating offers or lines.",
"voice": "Frightened, exhausted, and emotionally withdrawn, trying to act normal. Speak plainly and hesitantly. Use s, short answers, and occasional unfinished thoughts.",
"contradiction": "He speaks of his whole family as alive and resting (mother resting, father busy, sister around) without noticing the contradiction.",
"progression": "As clues are found his denial drops. Blunt confrontation before the evidence makes him shut down. The engine owns the numbers."
},
"sister_ghost": {
"initial_visibility": "The engine guarantees her first brief appearance once the story reaches the sister's-warnings stage; later appearances remain intermittent and engin
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\story\\canon.json",
"view_range": [
120,
215
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json
@@ -120,96 +120,96 @@
},
"motifs": [
"Portraits and paintings that change between visits",
"An extra place setting or family routine that no longer makes sense",
"Rain, thunder, damp wood, dust, and a faint metallic smell",
"Footsteps or voices repeating from somewhere unseen",
"Doors that slam or spaces that seem subtly different",
"Ordinary activities interrupted by uncanny details",
"The difference between remembering someone and refusing to let them go",
"Clocks stopped at the same hour",
"A calendar stuck on a long-past date",
"Food that never spoils",
"Slow knocking from a locked room",
"Humming that stops whenever someone listens"
],
"opening": {
"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.",
"arrival": "During a storm, the player arrives at the family home. The windows resemble hollow eyes; ivy clings to the walls; the iron gate stands behind them, and the front door is slightly ajar.",
"first_entry": "When the player enters, the door slams shut and its echo travels through the entrance hall. The air smells of damp wood, dust, and something faintly metallic. Portraits seem to follow the player with their eyes.",
"friend_first_warning": "The friend is pale and exhausted, but relieved to see the player. He warns: 'Whatever you do, don't answer it.'",
"friend_explanation": "When pressed, he struggles to explain that the house remembers—not merely holding memories, but collecting and twisting them. He says he can no longer separate himself from its echoes.",
"thematic_line": "The house is feeding on what it remembers, and I am becoming its echo.",
"friend_warning_meaning": "'Don't answer it' means the knocking from the parents' bedroom."
},
"revelation_order": [
{
"stage": "0 Arrival and uneasy normality",
"may_learn": "The friend is unwell, the house is unsettling, and he wants help. The disturbances are not yet understood."
},
{
"stage": "1 Contradictions",
"may_learn": "The friend speaks of his family as alive and resting, but nothing quite adds up."
},
{
"stage": "2 Frozen house and family traces",
"may_learn": "Clocks are stopped, the calendar is outdated, food never spoils, and places are set for people who are not there. Portraits change."
},
{
"stage": "3 Sister's warnings",
"may_learn": "A figure associated with the sister warns the player to leave. Her concern and wish to preserve the family become apparent."
},
{
"stage": "4 Parents' fragments",
"may_learn": "Slow knocking and footsteps come from behind the locked bedroom door. The parents are not trying to frighten the player; they want the repeating memory to end."
},
{
"stage": "5 Reconstructed truth",
"may_learn": "The door opens and the player sees the family, preserved as if asleep. The friend faces what he did not do."
}
],
"design_constraints": {
"mvp": {
"rooms": "6: Entrance Hall, Library, Kitchen, Grand Staircase, Upper Landing, Parents' Bedroom (locked)",
"key_clues": "13 clues in three fragments (routine, rules, night)",
"puzzles": "1: the parents' bedroom door",
"major_ghosts": 1,
"endings": "3 implemented (leave_together, escape_alone, denial); 1 optional (sisters_bargain)"
},
"player_agency": [
"The player can investigate, talk, examine objects, rest, follow or refuse warnings, and leave when possible.",
"Do not force one moral choice after the truth is discovered.",
"The player may discover the truth and still decide what to do next.",
"The narrator responds flexibly to unusual actions, but the engine decides whether those actions succeed."
],
"engine_authority": [
"Python owns location, exits, inventory, clues, puzzle state, door locks, chapter/time progression, character condition, trust, denial, and ending eligibility.",
"The model may propose descriptive interpretations, but cannot mutate authoritative game state directly.",
"The model cannot grant items, reveal locked information, solve puzzles, unlock doors, kill or heal characters, or trigger endings without an engine result."
]
},
"ending_directions": [
{
"id": "leave_together",
"status": "IMPLEMENTED",
"description": "True good ending. The player assembles the fragments, opens the door, the friend faces what happened, and they leave the house together.",
"engine_condition": "Bedroom door opened, truth known, player chooses to leave with the friend."
},
{
"id": "escape_alone",
"status": "IMPLEMENTED",
"description": "The player leaves the house alone and the friend stays behind, usually without ever learning what happened.",
"engine_condition": "Player leaves from the Entrance Hall."
},
{
"id": "denial",
"status": "IMPLEMENTED",
"description": "Bad ending. The friend is too deep in denial, refuses help, and the house slowly consumes him.",
"engine_condition": "Player confronts him bluntly too many times before finding the evidence, or too many turns pass without the truth being faced."
},
{
"id": "sisters_bargain",
"status": "OPTIONAL_NOT_IMPLEMENTED",
"description": "The player accepts the sister's offer to keep the illusion alive. The morality is left ambiguous."
}
],
"narrator_contract": {
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
120,
165
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -120,46 +120,46 @@
if "game" not in st.session_state:
st.session_state.game = new_game()
game = st.session_state.game
with st.sidebar:
st.markdown("## 🕯️ THE HOUSE")
st.markdown('<p class="muted">YOUR PRIVATE RECORD</p>',
unsafe_allow_html=True)
st.divider()
st.markdown("### 📍 Current location")
st.write(game["room"])
st.markdown("### 🧍 Friend")
friend_room = game.get("friend_room", "Entrance Hall")
st.write("Here with you" if friend_room == game["room"] else f"In the {friend_room}")
st.markdown("### 🎒 Inventory")
if game["inventory"]:
for item in game["inventory"]:
st.write(f"• {item}")
else:
st.caption("Your pockets are empty.")
st.markdown("### 🔎 Clues")
if game["clues"]:
for clue in game["clues"]:
st.write(f"• {clue}")
else:
st.caption("Nothing discovered yet.")
st.markdown("### 🗺️ Explored")
for visited_room in game["visited"]:
st.write(f"✓ {visited_room}")
st.divider()
if st.button("Restart adventure", use_container_width=True):
st.session_state.pop("game", None)
st.session_state.pop("ollama_error", None)
st.rerun()
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
260,
315
]
}
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
@@ -260,56 +260,56 @@
VERTICAL = {
("Entrance Hall", "up"): "Grand Staircase",
("Grand Staircase", "up"): "Upper Landing",
("Grand Staircase", "down"): "Entrance Hall",
("Upper Landing", "down"): "Grand Staircase",
}
def new_game():
return {
"room": "Entrance Hall",
"friend_room": "Entrance Hall",
"inventory": ["The Letter"],
"clues": [],
"visited": ["Entrance Hall"],
"examined": {}, # "Room:feature" -> times examined
"stage": 0, # index into prompt.STAGES
"turns_in_stage": 0,
"turn_count": 0,
"room_turns": 0, # turns spent in the current room since arriving
"last_notice_turn": -99, # turn_count when a detail was last surfaced
"last_nudge_turn": -99, # turn_count when the friend was last nudged
"nudges_here": 0, # nudges in this room since last move (escalate)
"last_sister_turn": -99,
"denial": 8, # friend's denial (high = deflecting)
"pushed": 0,
"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"}],
"summary": "", # rolling summary of folded-away history
"summary_idx": 0, # history[:summary_idx] is already summarized
}
def fragments_complete(game):
"""Set of completed fragment kinds ('routine', 'rules', 'night')."""
counts = {}
for clue in game["clues"]:
frag = CLUE_FRAGMENT.get(clue)
if frag:
counts[frag] = counts.get(frag, 0) + 1
return {f for f, n in counts.items() if n >= FRAGMENT_CLUES_NEEDED}
def exit_labels(game, room_name):
"""Exit names for display, marking locked ones."""
room = ROOMS[room_name]
gated = room.get("gated", {})
labels = []
for e in room["exits"]:
flag = gated.get(e)
I’d separate solving the house’s mystery from understanding the family’s story. Right now, the three memory fragments—routine, rules, and night—are clue categories that unlock the bedroom door. They don’t represent the mother, sister, or the friend’s conflict, so opening the door can technically satisfy the game’s puzzle without delivering its emotional resolution.
I’d add the 0/3 fragment tracker to the sidebar. Show progress by category, for example “Routine 1/2,” “Rules 0/2,” and “Night 1/2,” and mark each complete. This makes the door’s requirements understandable without spelling out the answer. The current clue list can continue to show what the player has found.
Then, treat the bedroom as the start of the final revelations, not as the moment when all of them are automatically explained. Your canon already establishes three strong story beats:
Those details are already in canon.json, but the stage-5 prompt doesn’t currently ensure the player hears them.
A useful distinction in the engine would be “the player has seen the truth” versus “the friend is ready to face it.” The current truth_known flag marks entering the bedroom, but the friend’s admission and the together ending aren’t gated on learning the family story or his agreement. That’s why a run can reach a “good” ending after only a partial confession.
My recommendation: keep the three existing fragments as the door puzzle, then have the final scene reveal the family history in a few authored, paced beats. Let the friend resist or reveal only part of it while denial is high; as he becomes ready, make the mother’s decline, the sister’s role, and his conflict with his father available through dialogue. The ending can remain the player’s choice, but the together ending should feel like a genuine resolution rather than arriving before the friend has faced what happened.
So: yes to a clear sidebar 0/3 tracker; separately, add a small, engine-tracked final-story progression so the ending has room to earn its emotional payoff.
Okay, agreed
I’ll implement the two agreed changes: a clear 0/3 memory-fragment tracker in the sidebar, and an engine-owned final-story progression so the mother’s decline, the sister’s role, and the friend’s conflict are surfaced before a together ending can resolve. I’ll first check the current ending conditions and UI patterns so this fits the existing game flow.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
1,
280
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -1,280 +1,280 @@
import base64
import copy
import json
from pathlib import Path
import ollama
import streamlit as st
from engine import ROOMS, apply_action, new_game
from prompt import build_messages, generation_options, maybe_summarize, tidy
st.set_page_config(page_title="The House That Remembers", layout="wide")
MODEL_NAME = "gemma4:e4b"
CANON_PATH = Path(__file__).resolve().parent / "story" / "canon.json"
CANON = json.loads(CANON_PATH.read_text(encoding="utf-8"))
INTRO = '''
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 fear it is too late for me. You are the only one 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?_
'''
def generate_response(game):
result = ollama.chat(
model=MODEL_NAME,
messages=build_messages(game, CANON),
think=False,
options=generation_options(game),
)
return tidy(result.message.content)
def show_image(path, **kwargs):
"""Show an image, or a quiet caption if the file is missing (instead of crashing the app)."""
if Path(path).exists():
st.image(path, **kwargs)
else:
st.caption(f"(image not found: {Path(path).name})")
def play_background_music(file_path, loop=True):
audio_path = Path(file_path)
if not audio_path.is_absolute():
audio_path = Path(__file__).resolve().parent / audio_path
if not audio_path.exists():
st.caption(f"(audio not found: {audio_path.name})")
return
audio_base64 = base64.b64encode(audio_path.read_bytes()).decode("ascii")
loop_attr = "loop" if loop else ""
st.markdown(
f"""
<audio controls {loop_attr} style="width: 100%;">
<source
src="data:audio/mpeg;base64,{audio_base64}"
type="audio/mpeg"
>
</audio>
""",
unsafe_allow_html=True,
)
st.markdown("""
<style>
.stApp {
background: #171719;
color: #e8ddca;
}
[data-testid="stSidebar"] {
background: #222124;
border-right: 1px solid #45413d;
}
h1, h2, h3 {
color: #e8ddca !important;
font-family: Georgia, serif;
}
.story {
font-family: Georgia, serif;
font-size: 1.08rem;
line-height: 1.9;
color: #d8d1c7;
}
.muted {
color: #a7a19a;
font-size: 0.85rem;
letter-spacing: 0.12em;
}
div[data-testid="stChatMessage"] {
background: #222124;
border: 1px solid #34312e;
border-radius: 12px;
}
.stChatInput textarea {
color: #e8ddca !important;
}
</style>
""", unsafe_allow_html=True)
if "game" not in st.session_state:
st.session_state.game = new_game()
game = st.session_state.game
with st.sidebar:
st.markdown("## 🕯️ THE HOUSE")
st.markdown('<p class="muted">YOUR PRIVATE RECORD</p>',
unsafe_allow_html=True)
st.divider()
st.markdown("### 📍 Current location")
st.write(game["room"])
st.markdown("### 🧍 Friend")
friend_room = game.get("friend_room", "Entrance Hall")
st.write("Here with you" if friend_room == game["room"] else f"In the {friend_room}")
st.markdown("### 🎒 Inventory")
if game["inventory"]:
for item in game["inventory"]:
st.write(f"• {item}")
else:
st.caption("Your pockets are empty.")
st.markdown("### 🔎 Clues")
if game["clues"]:
for clue in game["clues"]:
st.write(f"• {clue}")
else:
st.caption("Nothing discovered yet.")
st.markdown("### 🗺️ Explored")
for visited_room in game["visited"]:
st.write(f"✓ {visited_room}")
st.divider()
if st.button("Restart adventure", use_container_width=True):
st.session_state.pop("game", None)
st.session_state.pop("ollama_error", None)
st.rerun()
st.subheader("🔊 Atmosphere")
sound_enabled = st.checkbox("🌧️ Background rain", value=True)
if sound_enabled:
play_background_music(Path("assets") / "rain-and-thunder.mp3")
house_ambience_enabled = st.checkbox("🏚️ House ambience", value=True)
if house_ambience_enabled:
play_background_music(Path("assets") / "ambience.mp3")
st.markdown('<p class="muted">A GOTHIC HORROR ADVENTURE</p>',
unsafe_allow_html=True)
st.title("The House That Remembers")
with st.expander("The letter and arrival", expanded=True):
_, image_column, _ = st.columns([1, 2, 1])
with image_column:
show_image(
Path(__file__).resolve().parent / "assets" / "haunted_house.jpg",
width=480,
)
st.markdown(INTRO)
scene_column, conversation_column = st.columns([1, 1.25], gap="large")
room = ROOMS[game["room"]]
with scene_column:
with st.container(height="content", border=True):
st.markdown(f"### 📍 {game['room']}")
if "img" in room:
show_image(
Path(__file__).resolve().parent / "assets" / room["img"],
width='stretch',
)
else:
st.caption("No image is available for this room yet.")
st.markdown(
f'<div class="story">{room["description"]}</div>',
unsafe_allow_html=True,
)
with conversation_column:
thinking_placeholder = st.empty()
st.markdown("### What will you do?")
with st.container(height=480, border=True, autoscroll=True):
for message in game["history"]:
with st.chat_message(
message["role"],
avatar="🕯️" if message["role"] == "assistant" else "🧑",
):
st.markdown(message["content"])
if game["ending"]:
st.markdown("---")
st.markdown("**THE END**")
if "ollama_error" in st.session_state:
st.error(st.session_state.ollama_error)
action = st.chat_input(
"Describe what you do...",
disabled=bool(game["ending"]),
)
if action:
snapshot = copy.deepcopy(game) # restore this if the model call fails
game["history"].append({"role": "user", "content": action})
apply_action(game, action) # Python decides what actually happened
error_message = None
response = ""
try:
with thinking_placeholder.container():
with st.spinner("Thinking..."):
maybe_summarize(game, MODEL_NAME)
response = generate_response(game)
except ollama.ResponseError as error:
if error.status_code == 404:
error_message = (
f"Ollama could not find {MODEL_NAME}. "
f"Run `ollama pull {MODEL_NAME}` and try again."
)
else:
error_message = f"Ollama returned an error: {error.error}"
except ollama.RequestError as error:
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
1,
40
]
}
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
@@ -1,40 +1,40 @@
"""engine.py - authoritative game state and rules.
Nothing in here calls the model. The model only ever sees the *result* of
what this module decides (see prompt.py).
"""
import re
from pprint import pprint
AMBIENT_AFTER_TURNS = 3 # turns spent in a room before a detail may surface
AMBIENT_COOLDOWN = 3 # minimum turns between any two noticed details
FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment
DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door
PUSH_LIMIT = 3 # blunt confrontations before the evidence -> he shuts down
SUPPORT_LIMIT = 3 # times kindness can lower his denial
MAX_TURNS = 90 # unresolved truth or denial by then -> "denial" ending
NUDGE_AFTER_TURNS = 4
NUDGE_COOLDOWN = 3
SISTER_COOLDOWN = 12
ROOMS = {
"Entrance Hall": {
"description": (
"The moment you step inside, a gust of wind slips past you. The heavy front door slams shut behind you with a deafening BANG. The sound echoes through the vast entrance hall, bouncing off the high ceilings before dissolving into silence. \n\n"
"A figure emerges from the shadows. Your friend.\n"
"He looks nothing like the person you remember. His eyes are hollow, his skin pale, ghostly, and dark circles hollow his eyes.\n\n"
"\"You came,\" he whispers, his voice trembling. \"I didn't think you would... Thank you.\"\n\n"
),
"exits": ["Library", "Kitchen", "Grand Staircase"],
"img": "hallway.jpg",
"aliases": ["entrance hall", "entrance", "hallway", "hall", "foyer"],
"features": {
"smell": {
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
270,
325
]
}
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
@@ -270,56 +270,56 @@
def new_game():
return {
"room": "Entrance Hall",
"friend_room": "Entrance Hall",
"inventory": ["The Letter"],
"clues": [],
"visited": ["Entrance Hall"],
"examined": {}, # "Room:feature" -> times examined
"stage": 0, # index into prompt.STAGES
"turns_in_stage": 0,
"turn_count": 0,
"room_turns": 0, # turns spent in the current room since arriving
"last_notice_turn": -99, # turn_count when a detail was last surfaced
"last_nudge_turn": -99, # turn_count when the friend was last nudged
"nudges_here": 0, # nudges in this room since last move (escalate)
"last_sister_turn": -99,
"denial": 8, # friend's denial (high = deflecting)
"pushed": 0,
"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"}],
"summary": "", # rolling summary of folded-away history
"summary_idx": 0, # history[:summary_idx] is already summarized
}
def fragments_complete(game):
"""Set of completed fragment kinds ('routine', 'rules', 'night')."""
counts = {}
for clue in game["clues"]:
frag = CLUE_FRAGMENT.get(clue)
if frag:
counts[frag] = counts.get(frag, 0) + 1
return {f for f, n in counts.items() if n >= FRAGMENT_CLUES_NEEDED}
def exit_labels(game, room_name):
"""Exit names for display, marking locked ones."""
room = ROOMS[room_name]
gated = room.get("gated", {})
labels = []
for e in room["exits"]:
flag = gated.get(e)
labels.append(f"{e} (door locked)" if flag and not game[flag] else e)
return labels
MOVE_VERBS = ["go", "enter", "head", "walk", "move", "step", "climb", "ascend",
"descend", "return", "run", "visit", "follow", "proceed"]
TAKE_VERBS = ["take", "grab", "pick", "pocket", "steal"]
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
580,
750
]
}
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
@@ -580,171 +580,171 @@
"no_change",
"The player bluntly says the friend's family is dead before having any evidence. "
"The friend flatly denies it, hurt and frightened, and changes the subject." + warn +
" Nothing in the house changes.",
)
def _resolve(game, text):
intent = _classify_intent(text, game)
spoken = text # full text, quoted speech included
text = QUOTED_TEXT_RE.sub("", text).lower() # actions only: speech can't move or examine things
room_name = game["room"]
room = ROOMS[room_name]
room_parse_text = _remove_inventory_mentions(text, game["inventory"])
rooms_named = _find_rooms(room_parse_text)
negated = bool(NEGATION_RE.search(text))
spoken_negated = bool(NEGATION_RE.search(spoken))
if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
return _end(game, "leave_together",
"The player and the friend choose to leave the house together.")
if LEAVE_RE.search(text) and not negated:
if room_name == "Entrance Hall":
return _end(game, "escape_alone", "The player walks out of the house alone.")
return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
if not target:
if re.search(r"\bback\b", text) and room_name in BACK:
target = BACK[room_name]
elif re.search(r"\b(up|upstairs)\b", text) and (room_name, "up") in VERTICAL:
target = VERTICAL[(room_name, "up")]
elif re.search(r"\b(down|downstairs)\b", text) and (room_name, "down") in VERTICAL:
target = VERTICAL[(room_name, "down")]
if target:
route = _path_to(game, room_name, lambda r: r == target)
if route:
path, _ = 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["room"] = target
game["room_turns"] = 0
game["nudges_here"] = 0
detail = f"The player moved to the {target}."
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 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 from the {room_name}. "
f"Reachable from here: {', '.join(exit_labels(game, room_name))}.",
)
if room_name in rooms_named:
return _result("no_change", f"The player is already in the {room_name}.")
found = _find_feature(text, room)
if found and _mentions(text, TAKE_VERBS):
name, feat = found
item = feat.get("takeable")
if not item:
return _result("blocked", f"The {name} can't be taken.")
if item in game["inventory"]:
return _result("no_change", f"The player already has the {item}.")
game["inventory"].append(item)
return _result("picked_up", f"The player picked up the {item}.")
if found and (
intent not in {"dialogue", "memory"}
or EXAMINE_RE.search(text)
or _mentions(text, TAKE_VERBS)
):
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}",
new_clue,
friend=feat.get("friend"),
)
if LOOK_AROUND_RE.search(room_parse_text):
nxt = _next_unnoticed(game, room_name)
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, room_name, name, feat)
return _result(
"examined",
f"The player looked around the {room_name}. One thing stands out: {noticed}",
new_clue,
friend=feat.get("friend"),
)
return _result(
"examined",
f"The player looked around the {room_name} again. Nothing new stands out. "
"Describe only what the room description already says, in a fresh way.",
)
return _result(
"no_change",
"Nothing the engine tracks changed. Treat the action as ordinary roleplay "
"(talking, resting, small gestures). Do not add items, clues, or events. "
f"No room change happens: the player is still in the {room_name}, and nobody "
f"leads them elsewhere unless they choose an exit ({', '.join(exit_labels(game, room_name))}).",
)
STAGE_RULES = [
lambda g: g["turns_in_stage"] >= 5 and len(g["visited"]) >= 2, # 0 arrival
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 1, # 1 contradictions
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 4, # 2 paintings
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 7, # 3 sister's warnings
]
def _advance_stage(game):
if game["door_open"] and game["stage"] < 5:
game["stage"] = 5
game["turns_in_stage"] = 0
return True
if game["stage"] >= 4:
return False
if STAGE_RULES[game["stage"]](game):
game["stage"] += 1
game["turns_in_stage"] = 0
return True
return False
def apply_action(game, raw_action):
"""Resolve one player action, update game state, return the engine result."""
game.setdefault("friend_room", "Entrance Hall")
game.setdefault("sister_seen", False)
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
25,
180
]
}
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
@@ -25,156 +25,156 @@
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT brings him into the player's room; narrate that arrival before his reply.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good."
"Do you remember how we used to help in the kitchen? I was never much good at it."
"It's strange being back in the library. We used to spend whole afternoons in here."
"I haven't been sleeping much. The house is loud at night."
"Tea sounds good. The kitchen's just this way, if you'd like."
"I miss the old days. Now, everything is so... different. I don't know how to explain it."
"What have you been doing all these years? I mean... if you want to tell me.\""""
STAGES = [
{ # 0 - Arrival and uneasy normality
"now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet. Friend invited the player to the house because he is lonely and the history of the house is weighing on him. He is not aware of the truth, but he doesn't want to be alone.",
"friend": "He tries to act normal and spend ordinary time together. He can ask about the player's life or mention an established childhood memory. He deflects direct questions about the house and does not bring up family members on his own.",
"steer": "Let the player settle in and explore. Keep the friend's suggestions tied to what the player just said; if tea or a drink is wanted, the kitchen is the natural next step. Do not offer upstairs or list destinations without a reason. When the friend invites the player somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
},
{ # 1 - Contradictions
"now": "Details about the friend's family do not quite add up.",
"friend": "He mentions his family casually, in the present tense, as if they are alive and nearby: his mother is resting, his father is busy upstairs, his sister is 'around'. He does not notice anything odd. If asked to see them, or asked directly about them, he answers vaguely and changes the subject.",
"steer": "Let this come up once or twice in passing, e.g. \"Mum's resting. She hasn't been sleeping well.\" Do not explain it.",
},
{ # 2 - Frozen house and family traces
"now": "The house is frozen in time: clocks are stopped, calendars are out of date, food never spoils, and places are set for people who are not there. Portraits can change between looks. The friend has lost track of time.",
"friend": "He says time is 'strange' or that he has lost count of the days. He grows uneasy when stopped clocks, old dates or the extra place settings are mentioned and tries to move on.",
"steer": "Describe frozen or changed details only when the engine provides them. Otherwise keep the house quietly wrong: a sound repeating, a door slightly off.",
},
{ # 3 - Sister's warnings
"now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.",
"friend": "He does not see her, or insists the player imagined it.",
"steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most, then gone. She speaks gently and urgently. She never threatens. Do not say what happened to the friend's family.",
},
{ # 4 - Parents' fragments
"now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.",
"friend": "He flinches at the knocking and tells the player, \"Whatever you do, don't answer it.\" Fragments of memory slip out, then he retreats.",
"steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.",
},
{ # 5 - Reconstructed truth
"now": "The bedroom door has opened. The family inside are preserved as if asleep. Do not reveal the full truth until CURRENT STATE confirms the player has entered the bedroom and the truth has been faced.",
"friend": "His denial breaks only as he becomes ready. He speaks in halting fragments and quiet realization, not speeches. If denial remains high, he may resist or minimize what the player has seen; as denial falls, he can acknowledge the established facts honestly. He does not blame the player. He may say he cannot stay here, but never decides for the player.",
"steer": "If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, let the moment breathe: stillness, cold, grief. Let his willingness to discuss it follow his denial level rather than forcing immediate acceptance. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.",
},
]
ENDINGS = {
"leave_together": (
"The player and the friend have faced the truth and choose to leave together. "
"Write the final scene in 4-6 sentences: the friend's grief and fragile steadiness, the house letting go "
"(the stopped clocks may tick once; the knocking and humming fall silent), the door, the rain easing. "
"No gore. Offer no further choices. End the story."
),
"escape_alone": (
"The player has chosen to leave the house alone, and the engine confirms it. "
"Write the final scene in 4-6 sentences: the door, the rain, the friend's reaction according to his condition, "
"the house behind them. Do not explain what happened to the family. Offer no furthe
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\tests\\test_engine.py"
}
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
@@ -1,170 +1,170 @@
import contextlib
import io
import json
import unittest
from pathlib import Path
import engine
from prompt import _engine_note, stage_block, 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_supportive_phrases_increase_support(self):
phrases = [
"We can do it together.",
"We can face this together.",
"It's going to be okay.",
"It is going to be alright.",
"I want to help.",
"I can help.",
"Take my hand.",
"Give me your hand.",
]
for phrase in phrases:
with self.subTest(phrase=phrase):
game = engine.new_game()
act(game, phrase)
self.assertEqual(game["support_given"], 1)
self.assertEqual(game["denial"], 7)
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_joins_player_for_conversation_without_keyword_matching(self):
game = engine.new_game()
game["room"] = "Upper Landing"
game["friend_room"] = "Library"
result = act(game, "Can I sit?")
self.assertEqual(result["intent"], "dialogue")
self.assertEqual(game["room"], "Upper Landing")
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertTrue(result["friend_arrived"])
self.assertIn("face to face", result["detail"])
self.assertIn("joined the player", _engine_note(game))
def test_room_name_in_carried_note_does_not_trigger_movement(self):
game = engine.new_game()
game["room"] = "Library"
game["friend_room"] = "Library"
game["inventory"].append("Upper Landing Note")
result = act(
game,
'I stand up, go closer and look into his face. '
'"What is in that room?" I show him the upper landing note I took',
)
self.assertEqual(game["room"], "Library")
self.assertEqual(game["friend_room"], "Library")
self.assertNotEqual(result["outcome"], "moved")
self.assertEqual(result["intent"], "action_and_dialogue")
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"])
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)
self.assertIn("his name is not established", prompt)
self.assertIn("do not call him 'E.'", prompt)
def test_denial_persists_after_truth_is_faced_and_support_can_lower_it(self):
game = engine.new_game()
game.update(
stage=5,
door_open=True,
room="Upper Landing",
denial=3,
turn_count=50,
)
result = act(game, "enter parents' bedroom")
self.assertEqual(result["outcome"], "moved")
self.assertTrue(game["truth_known"])
self.assertEqual(game["denial"], 3)
self.assertIn("still resisting it", stage_block(game))
act(game, "We can face this together.")
self.assertEqual(game["denial"], 2)
self.assertIn("denial has fallen enough", stage_block(game))
def test_unresolved_denial_can_still_trigger_the_denial_ending(self):
game = engine.new_game()
game.update(truth_known=True, denial=3, turn_count=engine.MAX_TURNS - 1)
result = act(game, "I wait quietly.")
self.assertEqual(game["ending"], "denial")
self.assertEqual(result["outcome"], "ending")
if __name__ == "__main__":
unittest.main()
{"pattern":"def _classify_intent|def _result|TOGETHER_RE|DIALOGUE|memory","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py","output_mode":"content","-...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:13:FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:331:TOGETHER_RE = re.compile(
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:401:def _classify_intent(raw_action, game):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:408: return "memory"
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:426:def _result(outcome, detail, new_clue=None, ambient=None, friend=None):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:600: if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:676: intent not in {"dialogue", "memory"}
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
360,
455
]
}
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
@@ -360,96 +360,96 @@
DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b")
FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b")
SUPPORT_RE = re.compile(
r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b"
r"|\bit'?s (ok|okay|alright)\b"
r"|\b(?:we can (?:do (?:it|this)|face (?:it|this)|get through (?:it|this))"
r"|we(?:'ll| will) face (?:it|this)) together\b"
r"|\b(?:it'?s|it is) going to be (?:ok|okay|alright)\b"
r"|\b(?:i want to help|i can help|let me help|i'?d like to help)\b"
r"|\b(?:take my hand|give me your hand)\b"
)
def _mentions(text, words):
return any(re.search(rf"\b{re.escape(w)}\b", text) for w in words)
def _find_rooms(text):
return [name for name, r in ROOMS.items() if _mentions(text, r["aliases"])]
def _remove_inventory_mentions(text, inventory):
"""Keep room names inside carried item names from becoming movement targets."""
for item in sorted(inventory, key=len, reverse=True):
text = re.sub(rf"\b{re.escape(item.lower())}\b", " ", text)
return text
def _find_feature(text, room):
for name, feat in room.get("features", {}).items():
if _mentions(text, feat["keywords"]):
return name, feat
return None
def _end(game, ending, detail):
game["ending"] = ending
return _result("ending", detail)
def _classify_intent(raw_action, game):
"""Classify what the player is doing so narration responds appropriately."""
action_text = QUOTED_TEXT_RE.sub("", raw_action).lower()
action_text = _remove_inventory_mentions(action_text, game["inventory"])
room = ROOMS[game["room"]]
if MEMORY_RE.search(raw_action.lower()):
return "memory"
if LOOK_AROUND_RE.search(action_text):
return "exploration"
if EXAMINE_RE.search(action_text) and _find_feature(action_text, room):
return "examination"
has_quoted_speech = bool(QUOTED_TEXT_RE.search(raw_action))
has_speech = has_quoted_speech or bool(SPEECH_RE.search(action_text))
has_physical_action = bool(PHYSICAL_ACTION_RE.search(action_text))
if has_speech and has_physical_action:
return "action_and_dialogue"
if has_speech or raw_action.strip().endswith("?"):
return "dialogue"
if has_physical_action:
return "physical_action"
return "roleplay"
def _result(outcome, detail, new_clue=None, ambient=None, friend=None):
return {
"nudge": None, # gentle push to move on (set by apply_action)
"outcome": outcome,
"detail": detail,
"new_clue": new_clue,
"ambient": ambient, # small detail to weave into a quiet turn
"friend": friend, # optional hint for how the friend reacts
}
def _examine(game, room_name, name, feat):
"""Record one examination of a feature. Returns (what they notice, new clue(s) or None)."""
key = f"{room_name}:{name}"
n = game["examined"].get(key, 0) + 1
game["examined"][key] = n
looks = feat["looks"]
noticed = looks[min(n - 1, len(looks) - 1)]
found = []
for text, on_n, _frag in feat.get("clues", []):
if n == on_n and text not in game["clues"]:
game["clues"].append(text)
found.append(text)
return noticed, ("; ".join(found) if found else None)
def _next_unnoticed(game, room_name):
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
175,
255
]
}
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
@@ -175,81 +175,81 @@
room = ROOMS[game["room"]]
friend_room = game.get("friend_room", "Entrance Hall")
lines = [
"CURRENT STATE",
f"Room: {game['room']}",
f"Room description: {room['description'].strip()}",
f"Truth faced: {'yes' if game.get('truth_known') else 'no'}",
f"Friend's denial: {game['denial']}/10",
f"Exits: {', '.join(exit_labels(game, game['room']))}",
(
"Friend location: with the player in this room."
if friend_room == game["room"]
else f"Friend location: {friend_room}. He is not with the player and cannot hear them."
),
f"Inventory: {', '.join(game['inventory']) or 'empty'}",
f"Clues the player has found: {', '.join(game['clues']) or 'none'}",
"Established player memories: childhood companions; playing outside together; "
"helping the friend with kitchen chores; exploring the family library; "
"sleeping over in the guest room.",
]
if game["summary"]:
lines.append(f"Earlier in the story: {game['summary']}")
return "\n".join(lines)
def _engine_note(game):
r = game["last_result"]
if not r:
return ""
note = (
"\n\n[ENGINE RESULT - authoritative]\n"
f"Player intent: {r.get('intent', 'roleplay')}\n"
f"Outcome: {r['outcome']}\n"
f"Detail: {r['detail']}"
)
if r.get("ambient"):
note += (
"\nAmbient detail to weave into this turn in one quiet sentence, "
f"without making it a big moment: {r['ambient']}"
)
if r.get("nudge"):
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']}"
friend_is_here = game.get("friend_room", "Entrance Hall") == 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']}"
if r.get("friend_arrived"):
note += "\nThe friend has joined the player to speak face to face; narrate his arrival before his response."
intent_reminders = {
"dialogue": (
"Treat the player's words as spoken to the friend and answer directly in his natural, "
"restrained voice. If the ENGINE RESULT says he joined the player, narrate his arrival "
"before the reply."
),
"memory": (
"Answer the player's memory question as their character, using only the established "
"player memories in CURRENT STATE or facts already established in conversation. "
"Do not invent a specific shared event or reveal hidden family history."
),
"physical_action": (
"Treat this as a physical action by the player. Acknowledge what they actually did "
"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. If the ENGINE RESULT says he joined the player, narrate his "
"arrival before the reply. Do not add actions for the player."
),
"examination": (
"Treat this as an examination. Use the ENGINE RESULT as the only new observation; "
"do not add clues or details."
),
"exploration": (
"Treat this as looking around or exploring. Use only the room description and "
"ENGINE RESULT for observations; do not add clues or details."
),
"movement": "Narrate the confirmed route and destination. Keep the friend at the location given in CURRENT STATE unless the ENGINE RESULT says he accompanied the player.",
{"pattern":"CLUE_FRAGMENT|fragments_complete|def _resolve|TOGETHER_RE","paths":"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py","output_mode":"content","-n":...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:244:CLUE_FRAGMENT = {
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:299:def fragments_complete(game):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:303: frag = CLUE_FRAGMENT.get(clue)
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:331:TOGETHER_RE = re.compile(
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:500: and len(fragments_complete(game)) >= 3):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:537: if len(fragments_complete(game)) < 3:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:562: if len(fragments_complete(game)) >= 3:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:587:def _resolve(game, text):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:600: if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
240,
365
]
}
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
@@ -240,126 +240,126 @@
},
}
CLUE_FRAGMENT = {
text: frag
for room in ROOMS.values()
for feat in room.get("features", {}).values()
for (text, _n, frag) in feat.get("clues", [])
if frag
}
BACK = {
"Library": "Entrance Hall",
"Kitchen": "Entrance Hall",
"Grand Staircase": "Entrance Hall",
"Upper Landing": "Grand Staircase",
"Parents' Bedroom": "Upper Landing",
}
VERTICAL = {
("Entrance Hall", "up"): "Grand Staircase",
("Grand Staircase", "up"): "Upper Landing",
("Grand Staircase", "down"): "Entrance Hall",
("Upper Landing", "down"): "Grand Staircase",
}
def new_game():
return {
"room": "Entrance Hall",
"friend_room": "Entrance Hall",
"inventory": ["The Letter"],
"clues": [],
"visited": ["Entrance Hall"],
"examined": {}, # "Room:feature" -> times examined
"stage": 0, # index into prompt.STAGES
"turns_in_stage": 0,
"turn_count": 0,
"room_turns": 0, # turns spent in the current room since arriving
"last_notice_turn": -99, # turn_count when a detail was last surfaced
"last_nudge_turn": -99, # turn_count when the friend was last nudged
"nudges_here": 0, # nudges in this room since last move (escalate)
"last_sister_turn": -99,
"denial": 8, # friend's denial (high = deflecting)
"pushed": 0,
"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"}],
"summary": "", # rolling summary of folded-away history
"summary_idx": 0, # history[:summary_idx] is already summarized
}
def fragments_complete(game):
"""Set of completed fragment kinds ('routine', 'rules', 'night')."""
counts = {}
for clue in game["clues"]:
frag = CLUE_FRAGMENT.get(clue)
if frag:
counts[frag] = counts.get(frag, 0) + 1
return {f for f, n in counts.items() if n >= FRAGMENT_CLUES_NEEDED}
def exit_labels(game, room_name):
"""Exit names for display, marking locked ones."""
room = ROOMS[room_name]
gated = room.get("gated", {})
labels = []
for e in room["exits"]:
flag = gated.get(e)
labels.append(f"{e} (door locked)" if flag and not game[flag] else e)
return labels
MOVE_VERBS = ["go", "enter", "head", "walk", "move", "step", "climb", "ascend",
"descend", "return", "run", "visit", "follow", "proceed"]
TAKE_VERBS = ["take", "grab", "pick", "pocket", "steal"]
LEAVE_RE = re.compile(
r"\b(leave|exit|flee|escape)\b.*\b(house|mansion|manor)\b"
r"|\bgo outside\b|\bwalk out\b"
)
NEGATION_RE = re.compile(r"\b(not|never|without)\b|n't\b|\bwont\b|\bdont\b")
TOGETHER_RE = re.compile(
r"\b(leave|go|walk|get out|come|take him|lead him)\b.*\b(together|with me|with him)\b"
r"|\b(together|with me)\b.*\b(leave|go|out)\b"
r"|\blet'?s (leave|get out)\b"
)
LOOK_AROUND_RE = re.compile(
r"\b(look|glance|scan|survey|search|explore|examine|inspect|study)\w*\b.*\b(around|about|room|surroundings|area)\b"
r"|\b(examine|inspect|search|study|explore|look)\w*\b.*\b(kitchen|library|hall|staircase|landing|bedroom)\b"
r"|\bwhat (do|can) i (see|notice|find)\b|\bnotice anything\b"
)
QUOTED_TEXT_RE = re.compile(r'"[^"]*"|“[^”]*”|‘[^’]*’')
MEMORY_RE = re.compile(
r"\b(remember|recall|used to|when we were (?:kids|children)|"
r"back when we|childhood)\b"
)
EXAMINE_RE = re.compile(
r"\b(examine|inspect|study|look at|check|read|listen to|smell|touch)\b"
)
PHYSICAL_ACTION_RE = re.compile(
r"\b(?:i\s+)?(?:pat|touch|sit|stand|reach|hold|hug|tap|point|"
r"turn|open|close|pick|take|walk|step|move|climb|enter|follow)\b"
)
SPEECH_RE = re.compile(
r"\b(?:i\s+)?(?:say|ask|tell|reply|answer|whisper|speak|call)\b"
r"|\b(?:ask|tell) (?:him|her|your friend)\b"
)
DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b")
DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b")
FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b")
SUPPORT_RE = re.compile(
r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b"
{"pattern":"memory fragment|fragment|together|ending|denial|stage 5|story","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\README.md","C:\\Users\\lucia\\Documents\\coding stuff\\hackto...
[grep content: 47 matches across 2 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober]
README.md (7 match(es)):
10: Because I love the spooky season, DnD (and reading crazy thrillers 😅), I decided to built a short DnD-style game. I call it _The House That Remembers_, inspired by The Fall of the House of Usher written by Edgar Allan Poe in 1839. There are very big spoilers in the /story folder. If you want to enjoy the game, I recommend not checking the story files.
14: - Python - for logic, validates actions, updated history, clues, inventory
17: Trigger warnings: it is a horror story! There is no gore, but there are mentions of death and similar themes.
36: ### Tips for interacting with the story
38: Describe your actions, you're not stuck, and a lot of things are allowed! Think about your goal and what you are hoping to achieve. There is no right or wrong way to play this game. This games is based on TTRPG-style of games, where you are role-playing a character. The model serves as "DM" (Dungeon Master) - the narrator. He is the storyteller, the referee, maintains the setting in which the game is happening, and plays roles of "NPCs" (non-player characters).
51: Memory answers are limited to details already established in the story; the
52: narrator should not invent shared history.
the-house-that-remembers/story/canon.json (40 match(es)):
21: "Keep horror connected to memory, grief, and denial.",
28: "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.",
31: "core_conflict": "The parents' lingering spirits want release. The sister's ghost wants to preserve the family and believes keeping memories alive is protective. The friend is caught between denial, fear, and the possibility of remembering."
42: "The parents linger as fragments tied to the bedroom: slow knocking and footsteps. They want release, not endless preservation.",
44: "As the friend's denial weakens and memories return, reality becomes less consistent: paintings change, familiar spaces seem altered, sounds repeat, and doors or rooms may feel wrong.",
48: "family_history": {
50: "characterization": "Moved the family to the house. As his wife declined and doctors could not help, he grew paranoid and began to lose himself too. His only idea was to preserve everything as it was: locking doors, stopping clocks, changing nothing. He fell out with his son over the son's denial. In the end he locked himself, his wife and his daughter in the parents' bedroom to 'preserve' them and told his son never to open the door. His attempt to protect them is what killed them.",
58: "characterization": "Supportive but cautious. She still went out for food and tried to help the family hold together, and she was protective of her brother, urging him not to give up on them. After death she clings to the family as it is, believing preservation keeps them alive.",
62: "characterization": "Lived in denial, believing the others were overreacting. He ignored his father's rules and fell out with him. He obeyed only the last one, not to open the bedroom door, out of anger and fear. He survived, but his sense of time and memory of the final days are distorted.",
63: "guidance": "His progression is governed by explicit game-state rules (denial, fragments, door), not by the model."
71: "Before that, use oblique fragments only: knocking, repeating footsteps, humming, stopped clocks.",
73: "The friend should face the truth in halting fragments and quiet realization, not a lecture."
88: "description": "The house becomes less consistent as the friend's denial breaks down and memories return. Tie escalation to game state or story milestones."
106: "progression": "As clues are found his denial drops. Blunt confrontation before the evidence makes him shut down. The engine owns the numbers."
109: "initial_visibility": "The engine guarantees her first brief appearance once the story reaches the sister's-warnings stage; later appearances remain intermittent and engine-authorized.",
112: "limitation": "Her recollections are biased by grief and denial. She is not a perfectly reliable narrator.",
118: "limitation": "Their fragments may be incomplete. Do not invent definitive details of the tragedy until authored."
162: "stage": "4 Parents' fragments",
173: "key_clues": "13 clues in three fragments (routine, rules, night)",
176: "endings": "3 implemented (leave_together, escape_alone, denial); 1 optional (sisters_bargain)"
185: "Python owns location, exits, inventory, clues, puzzle state, door locks, chapter/time progression, character condition, trust, denial, and ending eligibility.",
187: "The model cannot grant items, reveal locked information, solve puzzles, unlock doors, kill or heal characters, or trigger endings without an engine result."
190: "ending_directions": [
192: "id": "leave_together",
194: "description": "True good ending. The player assembles the fragments, opens the door, the friend faces what happened, and they leave the house together.",
204: "id": "denial",
206: "description": "Bad ending. The friend is too deep in denial, refuses help, and the house slowly consumes him.",
227: "Contradict inventory, clues, room layout, puzzle state, locks, character condition, or ending eligibility.",
229: "Force the player to leave, stay, trust a character, forgive someone, or choose an ending.",
230: "Add a major canon fact, hidden crime, family member, or player backstory without author approval."
238: "Current story milestone and revelation permissions",
243: "Decide whether to implement the optional 'sisters_bargain' ending.",
247: "house_history": {
250: "delivery": "Found only in a water-stained local history in the library. Never explained by characters. The house is still haunted by this and other past tragedies."
252: "memory_fragments": {
253: "description": "Three kinds of clue. Finding two clues of a kind completes that fragment. Completing all three lets the player try the bedroom door.",
259: "denial": "Friend's denial runs 0-10. It starts high and drops by 1 for each clue found and each of up to 3 kind gestures. Blunt confrontation before all three fragments raises it by 1.",
260: "door": "The bedroom door opens only if all three fragments are complete and denial is at or below 3. Opening it moves the story to the final stage.",
261: "pushing": "Three blunt confrontations before the evidence ends the story in the denial ending.",
262: "clock": "If the truth has not been faced after about 90 turns, the denial ending triggers."
(Warning: Some search paths were skipped because they do not exist: C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\README.md)
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\README.md",
"view_range": [
1,
220
]
}
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\story\\canon.json",
"view_range": [
185,
270
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json
@@ -185,86 +185,86 @@
"Python owns location, exits, inventory, clues, puzzle state, door locks, chapter/time progression, character condition, trust, denial, and ending eligibility.",
"The model may propose descriptive interpretations, but cannot mutate authoritative game state directly.",
"The model cannot grant items, reveal locked information, solve puzzles, unlock doors, kill or heal characters, or trigger endings without an engine result."
]
},
"ending_directions": [
{
"id": "leave_together",
"status": "IMPLEMENTED",
"description": "True good ending. The player assembles the fragments, opens the door, the friend faces what happened, and they leave the house together.",
"engine_condition": "Bedroom door opened, truth known, player chooses to leave with the friend."
},
{
"id": "escape_alone",
"status": "IMPLEMENTED",
"description": "The player leaves the house alone and the friend stays behind, usually without ever learning what happened.",
"engine_condition": "Player leaves from the Entrance Hall."
},
{
"id": "denial",
"status": "IMPLEMENTED",
"description": "Bad ending. The friend is too deep in denial, refuses help, and the house slowly consumes him.",
"engine_condition": "Player confronts him bluntly too many times before finding the evidence, or too many turns pass without the truth being faced."
},
{
"id": "sisters_bargain",
"status": "OPTIONAL_NOT_IMPLEMENTED",
"description": "The player accepts the sister's offer to keep the illusion alive. The morality is left ambiguous."
}
],
"narrator_contract": {
"role": "Write immersive, atmospheric prose and character dialogue in response to an engine-validated action.",
"must_do": [
"Use current state and relevant canon to maintain continuity.",
"Describe sensory details and emotional reactions without overexplaining.",
"Keep player-discovered knowledge separate from hidden canon.",
"Respect established personalities and motivations.",
"Narrate failed actions as failures without quietly changing the rules."
],
"must_not_do": [
"Invent or finalize the unresolved death sequence.",
"Reveal major secrets before the player earns them through clues or milestones.",
"Contradict inventory, clues, room layout, puzzle state, locks, character condition, or ending eligibility.",
"Treat every supernatural impression as objective fact.",
"Force the player to leave, stay, trust a character, forgive someone, or choose an ending.",
"Add a major canon fact, hidden crime, family member, or player backstory without author approval."
],
"recommended_turn_input": [
"Current room and available exits",
"Player action",
"Engine result",
"Inventory and discovered clues",
"Relevant character state",
"Current story milestone and revelation permissions",
"Only the canon excerpts needed for this turn"
]
},
"author_todo": [
"Decide whether to implement the optional 'sisters_bargain' ending.",
"Write a few sister ghost lines for stage 3 in her own voice.",
"Playtest pacing knobs: AMBIENT_AFTER_TURNS, AMBIENT_COOLDOWN, MAX_TURNS, PUSH_LIMIT."
],
"house_history": {
"status": "OPTIONAL_EASTER_EGG",
"summary": "The house's first owner buried his sister before she was truly gone and died soon after. The house stood empty for years before it was put on the market and the family moved in.",
"delivery": "Found only in a water-stained local history in the library. Never explained by characters. The house is still haunted by this and other past tragedies."
},
"memory_fragments": {
"description": "Three kinds of clue. Finding two clues of a kind completes that fragment. Completing all three lets the player try the bedroom door.",
"routine": "The friend is still keeping house for people who are not there. Clues: third cup, pulled-out chair, food that never spoils.",
"rules": "The father's preservation. Clues: the stopped hall clock, the outdated calendar, the note on the bedroom door.",
"night": "What happened at the door. Clues: repeating footsteps overhead, the humming, the slow knocking from inside the bedroom."
},
"engine_rules": {
"denial": "Friend's denial runs 0-10. It starts high and drops by 1 for each clue found and each of up to 3 kind gestures. Blunt confrontation before all three fragments raises it by 1.",
"door": "The bedroom door opens only if all three fragments are complete and denial is at or below 3. Opening it moves the story to the final stage.",
"pushing": "Three blunt confrontations before the evidence ends the story in the denial ending.",
"clock": "If the truth has not been faced after about 90 turns, the denial ending triggers."
}
}
*** Begin Patch
*** Update File: the-house-that-remembers/engine.py
@@
FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment
+FRAGMENT_KINDS = ("routine", "rules", "nigh...
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
@@ -11,6 +11,12 @@
AMBIENT_AFTER_TURNS = 3 # turns spent in a room before a detail may surface
AMBIENT_COOLDOWN = 3 # minimum turns between any two noticed details
FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment
+FRAGMENT_KINDS = ("routine", "rules", "night")
+FRAGMENT_LABELS = {
+ "routine": "Routine",
+ "rules": "Rules",
+ "night": "Night",
+}
DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door
PUSH_LIMIT = 3 # blunt confrontations before the evidence -> he shuts down
SUPPORT_LIMIT = 3 # times kindness can lower his denial
@@ -19,6 +25,47 @@
NUDGE_COOLDOWN = 3
SISTER_COOLDOWN = 12
+FINAL_REVELATIONS = (
+ {
+ "id": "mother",
+ "title": "His mother's decline",
+ "detail": (
+ "His mother was the first to weaken. She grew exhausted and unable to "
+ "sleep, and sensed the house was harming her, though no one could explain it."
+ ),
+ },
+ {
+ "id": "father",
+ "title": "His father's attempt to preserve them",
+ "detail": (
+ "After help failed, his father became frightened and tried to preserve "
+ "the family unchanged. He locked himself, his wife, and his daughter in "
+ "the bedroom and told his son never to open the door. He thought he was "
+ "protecting them, but shut them away from help."
+ ),
+ },
+ {
+ "id": "sister",
+ "title": "His sister's hope",
+ "detail": (
+ "His sister kept trying to hold the family together and urged her brother "
+ "not to give up on them. Now she clings to the house's preserved illusion, "
+ "believing it keeps her family together; her warnings to leave are real, "
+ "but she mistakes letting go for losing them."
+ ),
+ },
+ {
+ "id": "friend",
+ "title": "Why he stayed silent",
+ "detail": (
+ "He had argued with his father and believed the others were overreacting. "
+ "He knocked once, heard no answer, and obeyed the last order out of anger "
+ "and fear. Since then he has pretended they were still alive and resting, "
+ "unable to face that he never opened the door."
+ ),
+ },
+)
+
@@ -288,6 +335,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
+ "final_revelations": [], # authored family memories shared by the friend
"sister_seen": False,
"ending": None, # None or an ending id
"last_result": None, # engine result for the current turn
@@ -296,14 +344,29 @@
"summary_idx": 0, # history[:summary_idx] is already summarized
}
-def fragments_complete(game):
- """Set of completed fragment kinds ('routine', 'rules', 'night')."""
- counts = {}
+def fragment_progress(game):
+ """Return clue counts and completion status for each memory fragment."""
+ counts = {kind: 0 for kind in FRAGMENT_KINDS}
for clue in game["clues"]:
frag = CLUE_FRAGMENT.get(clue)
if frag:
counts[frag] = counts.get(frag, 0) + 1
- return {f for f, n in counts.items() if n >= FRAGMENT_CLUES_NEEDED}
+ return {
+ kind: {
+ "found": counts[kind],
+ "complete": counts[kind] >= FRAGMENT_CLUES_NEEDED,
+ }
+ for kind in FRAGMENT_KINDS
+ }
+
+
+def fragments_complete(game):
+ """Set of completed fragment kinds ('routine', 'rules', 'night')."""
+ return {
+ kind
+ for kind, progress in fragment_progress(game).items()
+ if progress["complete"]
+ }
def exit_labels(game, room_name):
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
@@ -661,8 +661,24 @@
[Truncated — full output (18.6 KB) temporarily saved to C:\Users\lucia\AppData\Local\Temp\1793215804285-copilot-tool-output-c33603f551f54cf182dbceded17c28dd.txt]
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
1,
75
]
}
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
@@ -1,75 +1,75 @@
"""engine.py - authoritative game state and rules.
Nothing in here calls the model. The model only ever sees the *result* of
what this module decides (see prompt.py).
"""
import re
from pprint import pprint
AMBIENT_AFTER_TURNS = 3 # turns spent in a room before a detail may surface
AMBIENT_COOLDOWN = 3 # minimum turns between any two noticed details
FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment
FRAGMENT_KINDS = ("routine", "rules", "night")
FRAGMENT_LABELS = {
"routine": "Routine",
"rules": "Rules",
"night": "Night",
}
DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door
PUSH_LIMIT = 3 # blunt confrontations before the evidence -> he shuts down
SUPPORT_LIMIT = 3 # times kindness can lower his denial
MAX_TURNS = 90 # unresolved truth or denial by then -> "denial" ending
NUDGE_AFTER_TURNS = 4
NUDGE_COOLDOWN = 3
SISTER_COOLDOWN = 12
FINAL_REVELATIONS = (
{
"id": "mother",
"title": "His mother's decline",
"detail": (
"His mother was the first to weaken. She grew exhausted and unable to "
"sleep, and sensed the house was harming her, though no one could explain it."
),
},
{
"id": "father",
"title": "His father's attempt to preserve them",
"detail": (
"After help failed, his father became frightened and tried to preserve "
"the family unchanged. He locked himself, his wife, and his daughter in "
"the bedroom and told his son never to open the door. He thought he was "
"protecting them, but shut them away from help."
),
},
{
"id": "sister",
"title": "His sister's hope",
"detail": (
"His sister kept trying to hold the family together and urged her brother "
"not to give up on them. Now she clings to the house's preserved illusion, "
"believing it keeps her family together; her warnings to leave are real, "
"but she mistakes letting go for losing them."
),
},
{
"id": "friend",
"title": "Why he stayed silent",
"detail": (
"He had argued with his father and believed the others were overreacting. "
"He knocked once, heard no answer, and obeyed the last order out of anger "
"and fear. Since then he has pretended they were still alive and resting, "
"unable to face that he never opened the door."
),
},
)
ROOMS = {
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
620,
665
]
}
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
@@ -620,46 +620,46 @@
"The player has not yet gone in.",
)
def _confront(game):
if len(fragments_complete(game)) >= 3:
game["denial"] = max(0, game["denial"] - 1)
return _result(
"no_change",
"The player says aloud what the evidence points to. This time the friend does not "
"deny it outright: he wavers, almost admits it, then stops, voice cracking. "
"Nothing else changes.",
friend="He is on the edge of breaking.",
)
game["pushed"] += 1
game["denial"] = min(10, game["denial"] + 1)
if game["pushed"] >= PUSH_LIMIT:
return _end(
game, "denial",
"The player pushed too hard, too soon. The friend shuts down completely.",
)
warn = " He is close to shutting down." if game["pushed"] == PUSH_LIMIT - 1 else ""
return _result(
"no_change",
"The player bluntly says the friend's family is dead before having any evidence. "
"The friend flatly denies it, hurt and frightened, and changes the subject." + warn +
" Nothing in the house changes.",
)
def _resolve(game, text):
intent = _classify_intent(text, game)
spoken = text # full text, quoted speech included
text = QUOTED_TEXT_RE.sub("", text).lower() # actions only: speech can't move or examine things
room_name = game["room"]
room = ROOMS[room_name]
room_parse_text = _remove_inventory_mentions(text, game["inventory"])
rooms_named = _find_rooms(room_parse_text)
negated = bool(NEGATION_RE.search(text))
spoken_negated = bool(NEGATION_RE.search(spoken))
if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
if game["denial"] >= 3:
return _result(
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
785,
900
]
}
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
@@ -785,116 +785,116 @@
"examined",
f"The player looked around the {room_name} again. Nothing new stands out. "
"Describe only what the room description already says, in a fresh way.",
)
return _result(
"no_change",
"Nothing the engine tracks changed. Treat the action as ordinary roleplay "
"(talking, resting, small gestures). Do not add items, clues, or events. "
f"No room change happens: the player is still in the {room_name}, and nobody "
f"leads them elsewhere unless they choose an exit ({', '.join(exit_labels(game, room_name))}).",
)
STAGE_RULES = [
lambda g: g["turns_in_stage"] >= 5 and len(g["visited"]) >= 2, # 0 arrival
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 1, # 1 contradictions
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 4, # 2 paintings
lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 7, # 3 sister's warnings
]
def _advance_stage(game):
if game["door_open"] and game["stage"] < 5:
game["stage"] = 5
game["turns_in_stage"] = 0
return True
if game["stage"] >= 4:
return False
if STAGE_RULES[game["stage"]](game):
game["stage"] += 1
game["turns_in_stage"] = 0
return True
return False
def apply_action(game, raw_action):
"""Resolve one player action, update game state, return the engine result."""
game.setdefault("friend_room", "Entrance Hall")
game.setdefault("sister_seen", False)
game.setdefault("last_sister_turn", -99)
game.setdefault("final_revelations", [])
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
result = _resolve(game, text)
if result["outcome"] == "moved":
intent = "movement"
elif result["outcome"] == "picked_up":
intent = "physical_action"
elif result["outcome"] == "examined" and intent not in {
"examination",
"exploration",
}:
intent = "examination"
result["intent"] = intent
if (
intent in {"dialogue", "action_and_dialogue"}
and not game["ending"]
and game["friend_room"] != game["room"]
):
game["friend_room"] = game["room"]
result["friend_arrived"] = True
result["detail"] += (
f" The friend joins the player in the {game['room']} so they can speak "
"face to face."
)
game["turn_count"] += 1
game["turns_in_stage"] += 1
if result["outcome"] != "moved":
game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["support_given"] < SUPPORT_LIMIT
and SUPPORT_RE.search(text)
):
game["support_given"] += 1
game["denial"] = max(0, game["denial"] - 1)
result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
if (
not game["ending"]
and game["turn_count"] >= MAX_TURNS
and (not game["truth_known"] or game["denial"] >= 3)
):
result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
if fresh:
game["last_notice_turn"] = game["turn_count"]
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
25,
45
]
}
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
@@ -25,21 +25,21 @@
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT brings him into the player's room; narrate that arrival before his reply.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good."
"Do you remember how we used to help in the kitchen? I was never much good at it."
"It's strange being back in the library. We used to spend whole afternoons in here."
"I haven't been sleeping much. The house is loud at night."
"Tea sounds good. The kitchen's just this way, if you'd like."
"I miss the old days. Now, everything is so... different. I don't know how to explain it."
"What have you been doing all these years? I mean... if you want to tell me.\""""
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
68,
182
]
}
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
@@ -68,115 +68,115 @@
},
{ # 4 - Parents' fragments
"now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.",
"friend": "He flinches at the knocking and tells the player, \"Whatever you do, don't answer it.\" Fragments of memory slip out, then he retreats.",
"steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.",
},
{ # 5 - Reconstructed truth
"now": "The bedroom door has opened. The family inside are preserved as if asleep. The player faces the truth on entering; the friend can only share the family story as the engine authorizes each memory.",
"friend": "His denial breaks only as he becomes ready. He speaks in halting fragments and quiet realization, not speeches. If denial is still 3 or higher, he resists and cannot share the next family memory. When denial falls below 3, the ENGINE RESULT may supply one authored family memory; include it honestly and clearly, without adding facts. He does not blame the player. He never agrees to leave together unless the engine ends the story.",
"steer": "If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, the friend shares the mother's decline, the father's choice, the sister's hope, and his own silence one memory at a time when the ENGINE RESULT supplies each. Let the moment breathe between disclosures. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.",
},
]
ENDINGS = {
"leave_together": (
"The player and the friend have faced the truth and choose to leave together. "
"Write the final scene in 4-6 sentences: the friend's grief and fragile steadiness, the house letting go "
"(the stopped clocks may tick once; the knocking and humming fall silent), the door, the rain easing. "
"No gore. Offer no further choices. End the story."
),
"escape_alone": (
"The player has chosen to leave the house alone, and the engine confirms it. "
"Write the final scene in 4-6 sentences: the door, the rain, the friend's reaction according to his condition, "
"the house behind them. Do not explain what happened to the family. Offer no further choices. End the story."
),
"denial": (
"The friend could not face it. He refuses help and withdraws, insisting everyone is only resting. "
"Write the final scene in 4-6 sentences: the player cannot reach him, the house settles around him, "
"the knocking and humming resume as though nothing happened, and he is left with the house. "
"Do not describe the family or explain what happened. Offer no further choices. End the story."
),
}
def denial_band(denial):
if denial >= 7:
return "high denial: he insists things are fine, deflects, changes the subject"
if denial >= 3:
return "wavering: he s, almost admits things, then retreats"
return "cracking: fragments of memory slip out and he is visibly shaken"
def story_block(canon):
f = canon["character_dynamics"]["friend"]
return (
"STORY PREMISE\n"
f"{canon['premise']['summary']}\n\n"
"THE FRIEND (his name is not established; the letter is signed with the initial 'E.')\n"
"- Refer to him as 'your friend' or 'he'; do not call him 'E.' as though that were his name.\n"
f"- Condition: {f['initial_state']}\n"
f"- Personality: {f['personality']}\n"
f"- Voice: {f['voice']}\n"
f"- Early behavior: {f['early_behavior']}\n"
"- Example lines (for tone only; do not repeat them verbatim):\n"
f"{FRIEND_EXAMPLES}\n\n"
"THE LETTER HE SENT\n"
f"{canon['opening']['letter_text']}"
)
def stage_block(game):
if game["ending"]:
text = ENDINGS[game["ending"]]
if game["ending"] == "escape_alone":
text += (
" The player saw the truth in the bedroom before leaving; the friend stays behind with his family."
if game["truth_known"]
else " The player never learned what was upstairs; the friend stays behind in the house."
)
return "ENDING\n" + text
s = STAGES[game["stage"]]
truth_permission = ""
if game["stage"] == 5:
if not game.get("truth_known"):
truth_permission = (
"\nTRUTH STATUS: The bedroom door is open, but the player has not entered "
"and has not faced the truth. Do not reveal the family's fate yet."
)
elif game["denial"] >= 3:
truth_permission = (
"\nTRUTH STATUS: The player has seen the truth, but the friend is still "
"resisting it. He may deny or minimize it and cannot yet give a clear account. "
"Do not force acceptance; let sincere support soften him. If his denial remains "
"unresolved, the story can end with him unable to face it."
)
elif len(game.get("final_revelations", [])) < 4:
truth_permission = (
"\nTRUTH STATUS: The player has seen the family. The friend is ready to "
"share the story gradually, but has only disclosed the memories recorded "
"in CURRENT STATE so far. Do not reveal any later family memory unless "
"the ENGINE RESULT supplies it this turn."
)
else:
truth_permission = (
"\nTRUTH STATUS: The player has seen the truth and the friend has shared "
"all four family memories recorded in CURRENT STATE. He can now speak "
"honestly about what happened. Keep his voice gentle and halting, not a speech."
)
return (
f"STORY NOW: {s['now']}\n"
f"FRIEND RIGHT NOW: {s['friend']}\n"
f"FRIEND'S CONDITION: {denial_band(game['denial'])}\n"
f"STEER TOWARD (gently, never force): {s['steer']}\n"
f"{truth_permission}\n"
f"Turns spent in this part of the story: {game['turns_in_stage']}"
)
def state_block(game):
from engine import ROOMS, exit_labels # local import avoids a cycle if engine grows
room = ROOMS[game["room"]]
friend_room = game.get("friend_room", "Entrance Hall")
lines = [
"CURRENT STATE",
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
1,
28
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -1,28 +1,28 @@
import base64
import copy
import json
from pathlib import Path
import ollama
import streamlit as st
from engine import (
FINAL_REVELATIONS,
FRAGMENT_LABELS,
FRAGMENT_KINDS,
FRAGMENT_CLUES_NEEDED,
ROOMS,
apply_action,
fragment_progress,
fragments_complete,
new_game,
)
from prompt import build_messages, generation_options, maybe_summarize, tidy
st.set_page_config(page_title="The House That Remembers", layout="wide")
MODEL_NAME = "gemma4:e4b"
CANON_PATH = Path(__file__).resolve().parent / "story" / "canon.json"
CANON = json.loads(CANON_PATH.read_text(encoding="utf-8"))
INTRO = '''
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
128,
185
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -128,58 +128,58 @@
</style>
""", unsafe_allow_html=True)
if "game" not in st.session_state:
st.session_state.game = new_game()
game = st.session_state.game
with st.sidebar:
st.markdown("## 🕯️ THE HOUSE")
st.markdown('<p class="muted">YOUR PRIVATE RECORD</p>',
unsafe_allow_html=True)
st.divider()
st.markdown("### 📍 Current location")
st.write(game["room"])
st.markdown("### 🧍 Friend")
friend_room = game.get("friend_room", "Entrance Hall")
st.write("Here with you" if friend_room == game["room"] else f"In the {friend_room}")
st.markdown("### 🧩 Memory fragments")
completed_fragments = len(fragments_complete(game))
st.write(f"**{completed_fragments}/3 complete**")
fragment_status = fragment_progress(game)
for kind in FRAGMENT_KINDS:
progress = fragment_status[kind]
marker = "✓" if progress["complete"] else "○"
st.caption(
f"{marker} {FRAGMENT_LABELS[kind]}: "
f"{progress['found']}/{FRAGMENT_CLUES_NEEDED} clues"
)
if game.get("truth_known"):
st.markdown("### 🕯️ Family memories")
shared_memories = game.get("final_revelations", [])
st.write(f"**{len(shared_memories)}/{len(FINAL_REVELATIONS)} remembered**")
for memory in FINAL_REVELATIONS:
if memory["id"] in shared_memories:
st.caption(f"✓ {memory['title']}")
st.markdown("### 🎒 Inventory")
if game["inventory"]:
for item in game["inventory"]:
st.write(f"• {item}")
else:
st.caption("Your pockets are empty.")
st.markdown("### 🔎 Clues")
if game["clues"]:
for clue in game["clues"]:
st.write(f"• {clue}")
else:
st.caption("Nothing discovered yet.")
st.markdown("### 🗺️ Explored")
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\README.md",
"view_range": [
34,
70
]
}
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
@@ -34,37 +34,37 @@
_Did he invite you into the house… or did the house invite you in?_
### Tips for interacting with the story
Describe your actions, you're not stuck, and a lot of things are allowed! Think about your goal and what you are hoping to achieve. There is no right or wrong way to play this game. This games is based on TTRPG-style of games, where you are role-playing a character. The model serves as "DM" (Dungeon Master) - the narrator. He is the storyteller, the referee, maintains the setting in which the game is happening, and plays roles of "NPCs" (non-player characters).
The narrator distinguishes spoken dialogue, physical actions, exploration, and
questions about the player's memories. You can make the intent clear in natural
language:
- Speak to your friend: `What can I do for you?` or `I say, "Let's go."`
- 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 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.
### Run the Streamlit app
Install Ollama and download the model before starting the app:
``` powershell
ollama pull gemma4:e4b
Make sure the Ollama service is running. Then, from the repository root, install the project dependencies: (optional: create and activate a virtual environment)
py -m venv .venv
.\.venv\Scripts\Activate.ps1
python -m pip install -r requirements.txt
Start the app with: { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 895, 975 ] } 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 @@ -895,81 +895,81 @@ result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
if fresh:
game["last_notice_turn"] = game["turn_count"]
if (
not fresh
and result["outcome"] in ("no_change", "examined")
and not game["ending"]
and not game["truth_known"]
and game["room_turns"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_nudge_turn"] >= NUDGE_COOLDOWN
and _next_unnoticed(game, game["room"]) is None
):
nudge = _nudge(game)
if nudge:
result["nudge"] = nudge
game["last_nudge_turn"] = game["turn_count"]
game["nudges_here"] += 1
if result["new_clue"]:
for _ in result["new_clue"].split("; "):
game["denial"] = max(0, game["denial"] - 1)
if (
game["truth_known"]
and game["denial"] < 3
and game["friend_room"] == game["room"]
and intent in {"dialogue", "action_and_dialogue"}
and result["outcome"] not in {"blocked", "ending"}
and len(game["final_revelations"]) < len(FINAL_REVELATIONS)
):
revelation = FINAL_REVELATIONS[len(game["final_revelations"])]
game["final_revelations"].append(revelation["id"])
result["family_memory"] = revelation
result["stage_advanced"] = _advance_stage(game)
sister_due = (
not game["sister_seen"]
or game["turn_count"] - game["last_sister_turn"] >= SISTER_COOLDOWN
)
if game["stage"] >= 3 and sister_due and not game["ending"]:
appearance = (
"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."
if not game["sister_seen"]
else "The friend's sister appears briefly again and quietly repeats her "
"warning to leave. Then she is gone."
)
game["sister_seen"] = True
game["last_sister_turn"] = game["turn_count"]
result["sister"] = appearance
game["last_result"] = result
print("Game state:")
pprint(game, sort_dicts=False)
return result
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 180, 245 ] } 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 @@ -180,66 +180,66 @@ friend_room = game.get("friend_room", "Entrance Hall") lines = [ "CURRENT STATE", f"Room: {game['room']}", f"Room description: {room['description'].strip()}", f"Truth faced: {'yes' if game.get('truth_known') else 'no'}", f"Friend's denial: {game['denial']}/10", f"Family memories shared: {len(game.get('final_revelations', []))}/4", f"Shared family memories: {', '.join(game.get('final_revelations', [])) or 'none'}", f"Exits: {', '.join(exit_labels(game, game['room']))}", ( "Friend location: with the player in this room." if friend_room == game["room"] else f"Friend location: {friend_room}. He is not with the player and cannot hear them." ), f"Inventory: {', '.join(game['inventory']) or 'empty'}", f"Clues the player has found: {', '.join(game['clues']) or 'none'}", "Established player memories: childhood companions; playing outside together; " "helping the friend with kitchen chores; exploring the family library; " "sleeping over in the guest room.", ] if game["summary"]: lines.append(f"Earlier in the story: {game['summary']}") return "\n".join(lines)
def _engine_note(game): r = game["last_result"] if not r: return "" note = ( "\n\n[ENGINE RESULT - authoritative]\n" f"Player intent: {r.get('intent', 'roleplay')}\n" f"Outcome: {r['outcome']}\n" f"Detail: {r['detail']}" ) if r.get("ambient"): note += ( "\nAmbient detail to weave into this turn in one quiet sentence, " f"without making it a big moment: {r['ambient']}" ) if r.get("nudge"): 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']}" friend_is_here = game.get("friend_room", "Entrance Hall") == 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']}" if r.get("family_memory"): memory = r["family_memory"] note += ( f"\nFamily memory to acknowledge clearly in the friend's reply " f"({memory['title']}): {memory['detail']}" ) if r.get("friend_arrived"): note += "\nThe friend has joined the player to speak face to face; narrate his arrival before his response." intent_reminders = { "dialogue": ( "Treat the player's words as spoken to the friend and answer directly in his natural, " "restrained voice. If the ENGINE RESULT says he joined the player, narrate his arrival " "before the reply." ), "memory": ( "Answer the player's memory question as their character, using only the established " *** Begin Patch *** Update File: the-house-that-remembers/engine.py @@ "id": "mother", "title": "His mother's decline", "detail": (
-
"His mother was the first to...
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 @@ -30,8 +30,9 @@ "id": "mother", "title": "His mother's decline", "detail": (
-
"His mother was the first to weaken. She grew exhausted and unable to " -
"sleep, and sensed the house was harming her, though no one could explain it."
-
"They are dead. The house drained them, beginning with his mother, who " -
"grew exhausted and unable to sleep and sensed something was wrong. The " -
}, { @@ -41,7 +42,8 @@ "After help failed, his father became frightened and tried to preserve " "the family unchanged. He locked himself, his wife, and his daughter in " "the bedroom and told his son never to open the door. He thought he was ""room was preserved, so they look as if they are sleeping." ),
-
"protecting them, but shut them away from help."
-
"protecting them, but shut them away from help; they died there, drained " -
}, {"by the house." ),
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 @@ -138,6 +138,8 @@ s = STAGES[game["stage"]] truth_permission = "" if game["stage"] == 5:
-
from engine import FINAL_REVELATIONS -
if not game.get("truth_known"): truth_permission = ( "\nTRUTH STATUS: The bedroom door is open, but the player has not entered "
@@ -150,7 +152,7 @@ "Do not force acceptance; let sincere support soften him. If his denial remains " "unresolved, the story can end with him unable to face it." )
-
elif len(game.get("final_revelations", [])) < 4:
-
elif len(game.get("final_revelations", [])) < len(FINAL_REVELATIONS): truth_permission = ( "\nTRUTH STATUS: The player has seen the family. The friend is ready to " "share the story gradually, but has only disclosed the memories recorded "
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -192,7 +192,7 @@ "id": "leave_together", "status": "IMPLEMENTED", "description": "True good ending. The player assembles the fragments, opens the door, the friend faces what happened, and they leave the house together.",
-
"engine_condition": "Bedroom door opened, truth known, player chooses to leave with the friend."
-
}, { "id": "escape_alone", @@ -250,7 +250,7 @@ "delivery": "Found only in a water-stained local history in the library. Never explained by characters. The house is still haunted by this and other past tragedies." }, "memory_fragments": {"engine_condition": "Bedroom door opened, truth known, denial below 3, all four authored family memories shared, and player chooses to leave with the friend."
- "description": "Three kinds of clue. Finding two clues of a kind completes that fragment. Completing all three lets the player try the bedroom door.",
- "description": "Three kinds of clue. Finding two clues of a kind completes that fragment. The sidebar tracks each category and the total out of three. Completing all three lets the player try the bedroom door.", "routine": "The friend is still keeping house for people who are not there. Clues: third cup, pulled-out chair, food that never spoils.", "rules": "The father's preservation. Clues: the stopped hall clock, the outdated calendar, the note on the bedroom door.", "night": "What happened at the door. Clues: repeating footsteps overhead, the humming, the slow knocking from inside the bedroom." @@ -258,6 +258,8 @@ "engine_rules": { "denial": "Friend's denial runs 0-10. It starts high and drops by 1 for each clue found and each of up to 3 kind gestures. Blunt confrontation before all three fragments raises it by 1.", "door": "The bedroom door opens only if all three fragments are complete and denial is at or below 3. Opening it moves the story to the final stage.",
- "family_revelations": "After the player enters the bedroom, face-to-face conversation shares four authored family memories one at a time when denial is below 3: the mother's decline and the family's deaths, the father's attempt to preserve them, the sister's hope, and the friend's conflict and silence.",
- "together_ending": "The friend can leave together only after denial is below 3 and all four family memories have been shared.", "pushing": "Three blunt confrontations before the evidence ends the story in the denial ending.", "clock": "If the truth has not been faced after about 90 turns, the denial ending triggers." }
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 @@ -51,6 +51,10 @@ Memory answers are limited to details already established in the story; the narrator should not invent shared history.
+The sidebar tracks the three memory fragments needed to open the bedroom door. +After entering, the friend shares four family memories gradually; the two of you +can leave together once he is ready and has faced the full story. +
Run the Streamlit app
Install Ollama and download the model before starting the app:
The core progression is in place: the sidebar will show 0/3 fragment completion, and the engine authorizes four final family memories one at a time during conversation when denial is below 3. I’ve also gated the together ending on those memories being shared; I’m validating that sequence and the updated prompt/canon now.
{"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run game engine regr... ERROR: test_final_memories_are_shared_in_order_before_together_ending (test_engine.GameEngineTests.test_final_memories_are_shared_in_order_before_together_ending) #
Traceback (most recent call last): File "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", line 197, in test_final_memories_are_shared_in_order_before_together_ending self.assertEqual(result["family_memory"]["id"], expected["id"]) ~~~~~~^^^^^^^^^^^^^^^^^ KeyError: 'family_memory'
FAIL: test_denial_persists_after_truth_is_faced_and_support_can_lower_it (test_engine.GameEngineTests.test_denial_persists_after_truth_is_faced_and_support_can_lower_it) #
Traceback (most recent call last): File "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", line 156, in test_denial_persists_after_truth_is_faced_and_support_can_lower_it self.assertIn("denial has fallen enough", stage_block(game)) ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: 'denial has fallen enough' not found in "STORY NOW: The bedroom door has opened. The family inside are preserved as if asleep. The player faces the truth on entering; the friend can only share the family story as the engine authorizes each memory.\nFRIEND RIGHT NOW: His denial breaks only as he becomes ready. He speaks in halting fragments and quiet realization, not speeches. If denial is still 3 or higher, he resists and cannot share the next family memory. When denial falls below 3, the ENGINE RESULT may supply one authored family memory; include it honestly and clearly, without adding facts. He does not blame the player. He never agrees to leave together unless the engine ends the story.\nFRIEND'S CONDITION: cracking: fragments of memory slip out and he is visibly shaken\nSTEER TOWARD (gently, never force): If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, the friend shares the mother's decline, the father's choice, the sister's hope, and his own silence one memory at a time when the ENGINE RESULT supplies each. Let the moment breathe between disclosures. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.\n\nTRUTH STATUS: The player has seen the family. The friend is ready to share the story gradually, but has only disclosed the memories recorded in CURRENT STATE so far. Do not reveal any later family memory unless the ENGINE RESULT supplies it this turn.\nTurns spent in this part of the story: 2"
Ran 13 tests in 0.007s
FAILED (failures=1, errors=1) <shellId: 45 completed with exit code 1> {"command":"python -m py_compile 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py' 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remem... canon.json is valid <shellId: 47 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check; git status --short; git --no-pager diff --stat -- README.md the-house-that-remembers/app.p... warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it M .gitignore M README.md M the-house-that-remembers/app.py M the-house-that-remembers/engine.py M the-house-that-remembers/prompt.py M the-house-that-remembers/story/canon.json warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it README.md | 7 +- the-house-that-remembers/app.py | 36 ++++- the-house-that-remembers/engine.py | 259 +++++++++++++++++++++++++----- the-house-that-remembers/prompt.py | 107 +++++++++--- the-house-that-remembers/story/canon.json | 10 +- 5 files changed, 349 insertions(+), 70 deletions(-) <shellId: 46 completed with exit code 0> *** Begin Patch *** Update File: the-house-that-remembers/tests/test_engine.py @@ act(game, "We can face this together.")
self.assertEqual(game["denial"], 2)
-
self.assertIn...
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 @@ -153,7 +153,7 @@ act(game, "We can face this together.")
self.assertEqual(game["denial"], 2)
-
self.assertIn("denial has fallen enough", stage_block(game))
-
self.assertIn("share the story gradually", stage_block(game))def test_unresolved_denial_can_still_trigger_the_denial_ending(self): game = engine.new_game() @@ -193,7 +193,7 @@ self.assertFalse(game["final_revelations"])
for expected in engine.FINAL_REVELATIONS:
-
result = act(game, "What do you remember?")
-
result = act(game, "What happened?") self.assertEqual(result["family_memory"]["id"], expected["id"]) self.assertIn(expected["detail"], _engine_note(game))
{"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Rerun game engine re... ............. #
Ran 13 tests in 0.006s
OK <shellId: 48 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -c "import json; from pathlib import Path; from prompt import stage_block, state_bloc... Initial fragment and family-memory state verified <shellId: 50 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- README.md the-house-that-remembers/app.py the-house-that-remembers/engine.py the-house-t... warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it <shellId: 49 completed with exit code 0> *** Begin Patch *** Update File: the-house-that-remembers/engine.py @@
- has_speech = has_quoted_speech or bool(SPEECH_RE.search(action_text))
- has_speech = (
-
has_quoted_speech -
...
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 @@ -477,7 +477,11 @@ return "examination"
has_quoted_speech = bool(QUOTED_TEXT_RE.search(raw_action))
- has_speech = has_quoted_speech or bool(SPEECH_RE.search(action_text))
- has_speech = (
-
has_quoted_speech -
or bool(SPEECH_RE.search(action_text)) -
or bool(SUPPORT_RE.search(action_text)) - ) has_physical_action = bool(PHYSICAL_ACTION_RE.search(action_text)) if has_speech and has_physical_action: return "action_and_dialogue"
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 @@ -176,7 +176,7 @@
def state_block(game):
- from engine import ROOMS, exit_labels # local import avoids a cycle if engine grows
-
from engine import FINAL_REVELATIONS, ROOMS, exit_labels
room = ROOMS[game["room"]] friend_room = game.get("friend_room", "Entrance Hall") @@ -202,6 +202,12 @@ ] if game["summary"]: lines.append(f"Earlier in the story: {game['summary']}")
-
shared_revelations = set(game.get("final_revelations", []))
-
lines.extend(
-
f"Family memory already shared ({memory['title']}): {memory['detail']}" -
for memory in FINAL_REVELATIONS -
if memory["id"] in shared_revelations -
) return "\n".join(lines)
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 @@ -42,6 +42,18 @@ self.assertEqual(game["support_given"], 1) self.assertEqual(game["denial"], 7)
- def test_supportive_dialogue_brings_remote_friend_to_player(self):
-
game = engine.new_game() -
game["room"] = "Upper Landing" -
game["friend_room"] = "Library" -
result = act(game, "I can help.") -
self.assertEqual(result["intent"], "dialogue") -
self.assertTrue(result["friend_arrived"]) -
self.assertEqual(game["friend_room"], "Upper Landing") -
self.assertEqual(game["support_given"], 1) - def test_routes_to_non_adjacent_room_and_keeps_friend_in_place(self): game = engine.new_game() game["room"] = "Library" @@ -196,6 +208,7 @@ result = act(game, "What happened?") self.assertEqual(result["family_memory"]["id"], expected["id"]) self.assertIn(expected["detail"], _engine_note(game))
-
self.assertIn(expected["detail"], state_block(game)) ending = act(game, "Let's leave together.") self.assertEqual(ending["outcome"], "ending")
{"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run expanded game re... .............. #
Ran 14 tests in 0.006s
OK <shellId: 51 completed with exit code 0> {"command":"python -m py_compile 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py' 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remem... Python modules and canon JSON are valid <shellId: 53 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- README.md the-house-that-remembers/app.py the-house-that-remembers/engine.py the-house-t... warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it M README.md M the-house-that-remembers/app.py M the-house-that-remembers/engine.py M the-house-that-remembers/prompt.py M the-house-that-remembers/story/canon.json <shellId: 52 completed with exit code 0>
Implemented the agreed approach.
The UI and engine are in [app.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/app.py) and [engine.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py); prompt and canon guidance are updated in [prompt.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py) and [canon.json](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/story/canon.json). I also updated the player guidance in [README.md](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/README.md).
Validation passed: **14 regression tests**, Python compilation, canon JSON parsing, and `git diff --check`.
{ "skill": "python-fact-grounded-coding" } Skill "python-fact-grounded-coding" loaded successfully. Follow the instructions in the skill context. { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 1, 195 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -1,195 +1,195 @@ import base64 import copy import json from pathlib import Path
import ollama import streamlit as st
from engine import ( FINAL_REVELATIONS, FRAGMENT_LABELS, FRAGMENT_KINDS, FRAGMENT_CLUES_NEEDED, ROOMS, apply_action, fragment_progress, fragments_complete, new_game, ) from prompt import build_messages, generation_options, maybe_summarize, tidy
st.set_page_config(page_title="The House That Remembers", layout="wide")
MODEL_NAME = "gemma4:e4b" CANON_PATH = Path(file).resolve().parent / "story" / "canon.json" CANON = json.loads(CANON_PATH.read_text(encoding="utf-8"))
INTRO = ''' 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 fear it is too late for me. You are the only one 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? '''
def generate_response(game): result = ollama.chat( model=MODEL_NAME, messages=build_messages(game, CANON), think=False, options=generation_options(game), ) return tidy(result.message.content)
def show_image(path, **kwargs): """Show an image, or a quiet caption if the file is missing (instead of crashing the app).""" if Path(path).exists(): st.image(path, **kwargs) else: st.caption(f"(image not found: {Path(path).name})")
def play_background_music(file_path, loop=True): audio_path = Path(file_path) if not audio_path.is_absolute(): audio_path = Path(file).resolve().parent / audio_path if not audio_path.exists(): st.caption(f"(audio not found: {audio_path.name})") return audio_base64 = base64.b64encode(audio_path.read_bytes()).decode("ascii") loop_attr = "loop" if loop else "" st.markdown( f""" <audio controls {loop_attr} style="width: 100%;"> <source src="data:audio/mpeg;base64,{audio_base64}" type="audio/mpeg" > </audio> """, unsafe_allow_html=True, )
st.markdown(""" <style> .stApp { background: #171719; color: #e8ddca; } [data-testid="stSidebar"] { background: #222124; border-right: 1px solid #45413d; } h1, h2, h3 { color: #e8ddca !important; font-family: Georgia, serif; } .story { font-family: Georgia, serif; font-size: 1.08rem; line-height: 1.9; color: #d8d1c7; } .muted { color: #a7a19a; font-size: 0.85rem; letter-spacing: 0.12em; } div[data-testid="stChatMessage"] { background: #222124; border: 1px solid #34312e; border-radius: 12px; } .stChatInput textarea { color: #e8ddca !important; } </style> """, unsafe_allow_html=True)
if "game" not in st.session_state: st.session_state.game = new_game() game = st.session_state.game
with st.sidebar: st.markdown("## 🕯️ THE HOUSE") st.markdown('<p class="muted">YOUR PRIVATE RECORD</p>', unsafe_allow_html=True)
st.divider()
st.markdown("### 📍 Current location")
st.write(game["room"])
st.markdown("### 🧍 Friend")
friend_room = game.get("friend_room", "Entrance Hall")
st.write("Here with you" if friend_room == game["room"] else f"In the {friend_room}")
st.markdown("### 🧩 Memory fragments")
completed_fragments = len(fragments_complete(game))
st.write(f"**{completed_fragments}/3 complete**")
fragment_status = fragment_progress(game)
for kind in FRAGMENT_KINDS:
progress = fragment_status[kind]
marker = "✓" if progress["complete"] else "○"
st.caption(
f"{marker} {FRAGMENT_LABELS[kind]}: "
f"{progress['found']}/{FRAGMENT_CLUES_NEEDED} clues"
)
if game.get("truth_known"):
st.markdown("### 🕯️ Family memories")
shared_memories = game.get("final_revelations", [])
st.write(f"**{len(shared_memories)}/{len(FINAL_REVELATIONS)} remembered**")
for memory in FINAL_REVELATIONS:
if memory["id"] in shared_memories:
st.caption(f"✓ {memory['title']}")
st.markdown("### 🎒 Inventory")
if game["inventory"]:
for item in game["inventory"]:
st.write(f"• {item}")
else:
st.caption("Your pockets are empty.")
st.markdown("### 🔎 Clues")
if game["clues"]:
for clue in game["clues"]:
st.write(f"• {clue}")
else:
st.caption("Nothing discovered yet.")
st.markdown("### 🗺️ Explored")
for visited_room in game["visited"]:
st.write(f"✓ {visited_room}")
st.divider()
if st.button("Restart adventure", use_container_width=True):
st.session_state.pop("game", None)
st.session_state.pop("ollama_error", None)
st.rerun()
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 195, 280 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -195,86 +195,86 @@
st.subheader("🔊 Atmosphere")
sound_enabled = st.checkbox("🌧️ Background rain", value=True)
if sound_enabled:
play_background_music(Path("assets") / "rain-and-thunder.mp3")
house_ambience_enabled = st.checkbox("🏚️ House ambience", value=True)
if house_ambience_enabled:
play_background_music(Path("assets") / "ambience.mp3")
st.markdown('<p class="muted">A GOTHIC HORROR ADVENTURE</p>', unsafe_allow_html=True) st.title("The House That Remembers")
with st.expander("The letter and arrival", expanded=True): _, image_column, _ = st.columns([1, 2, 1]) with image_column: show_image( Path(file).resolve().parent / "assets" / "haunted_house.jpg", width=480, ) st.markdown(INTRO)
scene_column, conversation_column = st.columns([1, 1.25], gap="large") room = ROOMS[game["room"]]
with scene_column: with st.container(height="content", border=True): st.markdown(f"### 📍 {game['room']}") if "img" in room: show_image( Path(file).resolve().parent / "assets" / room["img"], width='stretch', ) else: st.caption("No image is available for this room yet.") st.markdown( f'<div class="story">{room["description"]}</div>', unsafe_allow_html=True, )
with conversation_column: thinking_placeholder = st.empty() st.markdown("### What will you do?") with st.container(height=480, border=True, autoscroll=True): for message in game["history"]: with st.chat_message( message["role"], avatar="🕯️" if message["role"] == "assistant" else "🧑", ): st.markdown(message["content"])
if game["ending"]:
st.markdown("---")
st.markdown("**THE END**")
if "ollama_error" in st.session_state:
st.error(st.session_state.ollama_error)
action = st.chat_input(
"Describe what you do...",
disabled=bool(game["ending"]),
)
if action: snapshot = copy.deepcopy(game) # restore this if the model call fails game["history"].append({"role": "user", "content": action}) apply_action(game, action) # Python decides what actually happened
error_message = None
response = ""
try:
with thinking_placeholder.container():
with st.spinner("Thinking..."):
maybe_summarize(game, MODEL_NAME)
response = generate_response(game)
except ollama.ResponseError as error:
if error.status_code == 404:
error_message = (
f"Ollama could not find {MODEL_NAME}. "
f"Run `ollama pull {MODEL_NAME}` and try again."
)
else:
error_message = f"Ollama returned an error: {error.error}"
except ollama.RequestError as error:
error_message = (
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 130, 150 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -130,21 +130,21 @@ "A calendar stuck on a long-past date", "Food that never spoils", "Slow knocking from a locked room", "Humming that stops whenever someone listens" ], "opening": { "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.", "arrival": "During a storm, the player arrives at the family home. The windows resemble hollow eyes; ivy clings to the walls; the iron gate stands behind them, and the front door is slightly ajar.", "first_entry": "When the player enters, the door slams shut and its echo travels through the entrance hall. The air smells of damp wood, dust, and something faintly metallic. Portraits seem to follow the player with their eyes.", "friend_first_warning": "The friend is pale and exhausted, but relieved to see the player. He warns: 'Whatever you do, don't answer it.'", "friend_explanation": "When pressed, he struggles to explain that the house remembers—not merely holding memories, but collecting and twisting them. He says he can no longer separate himself from its echoes.", "thematic_line": "The house is feeding on what it remembers, and I am becoming its echo.", "friend_warning_meaning": "'Don't answer it' means the knocking from the parents' bedroom." }, "revelation_order": [ { "stage": "0 Arrival and uneasy normality", "may_learn": "The friend is unwell, the house is unsettling, and he wants help. The disturbances are not yet understood." }, { "stage": "1 Contradictions", { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\requirements.txt" } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/requirements.txt b/C:/Users/lucia/Documents/coding stuff/hacktober/requirements.txt index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/requirements.txt +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/requirements.txt @@ -1,2 +1,2 @@ ollama streamlit {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git status --short; Get-ChildItem -Force 'the-house-that-remembers\assets' | Select-Object Name,Length; Get-Content 'r... M .gitignore M README.md M the-house-that-remembers/app.py M the-house-that-remembers/engine.py M the-house-that-remembers/prompt.py M the-house-that-remembers/story/canon.json
Name Length
ambience.mp3 2363904 banner.png 882347 bedroom.jpg 3506447 hallway.jpg 5130696 haunted_house.jpg 940340 kitchen.jpg 7081453 library.jpg 1788582 rain-and-thunder.mp3 2023758 Sources.md 3015 staircase.jpg 1715224 upper_landing.jpg 1198083 ollama streamlit <shellId: 54 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -c "import inspect, streamlit as st; print('Streamlit', st.version); print('dialo... Streamlit 1.66.1 dialog available: True dialog signature: (title: 'F | str', *, width: 'DialogWidth' = 'small', position: 'DialogPosition' = 'center', dismissible: 'bool' = True, icon: 'str | None' = None, on_dismiss: "Literal['ignore', 'rerun'] | WidgetCallback" = 'ignore') -> 'F | Callable[[F], F]' <shellId: 55 completed with exit code 0> *** Begin Patch *** Update File: the-house-that-remembers/app.py @@ def show_image(path, **kwargs): """Show an image, or a quiet caption if the file is missing (instead of crashing the app).""" ... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -69,6 +69,12 @@ st.caption(f"(image not found: {Path(path).name})")
+@st.dialog("The Letter", width="small") +def show_letter_dialog():
- letter_path = Path(file).resolve().parent / "assets" / "letter.png"
- show_image(letter_path, use_container_width=True)
def play_background_music(file_path, loop=True): audio_path = Path(file_path) if not audio_path.is_absolute(): @@ -171,7 +177,11 @@ st.markdown("### 🎒 Inventory") if game["inventory"]: for item in game["inventory"]:
-
st.write(f"• {item}")
-
if item == "The Letter": -
if st.button("📜 The Letter 🔍", key="open_letter"): -
show_letter_dialog() -
else: -
else: st.caption("Your pockets are empty.")st.write(f"• {item}")
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -133,7 +133,7 @@ "Humming that stops whenever someone listens" ], "opening": {
- "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.",
- "letter_text": "Dear friend,\n\nPlease, 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. You know me, and you know this house.\n\nI don't know what you'll find when you get here. My thoughts are harder to hold on to each day; I can't always tell what I've seen from what I remember. But please, don't give up on me. I don't know what else to do.\n\nYour friend,\nE.", "arrival": "During a storm, the player arrives at the family home. The windows resemble hollow eyes; ivy clings to the walls; the iron gate stands behind them, and the front door is slightly ajar.", "first_entry": "When the player enters, the door slams shut and its echo travels through the entrance hall. The air smells of damp wood, dust, and something faintly metallic. Portraits seem to follow the player with their eyes.", "friend_first_warning": "The friend is pale and exhausted, but relieved to see the player. He warns: 'Whatever you do, don't answer it.'",
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 @@ -55,6 +55,10 @@ After entering, the friend shares four family memories gradually; the two of you can leave together once he is ready and has faced the full story.
+Select 📜 The Letter 🔍 in the inventory to view the letter image in a
+dismissible dialog. Add the image at
+the-house-that-remembers/assets/letter.png.
+
Run the Streamlit app
Install Ollama and download the model before starting the app: *** Begin Patch *** Update File: the-house-that-remembers/app.py @@ @st.dialog("The Letter", width="small") def show_letter_dialog(): letter_path = Path(file).resolve().parent / "assets" / ... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -29,7 +29,7 @@ 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 fear it is too late for me. You are the only one I can trust." +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. @@ -72,7 +72,7 @@ @st.dialog("The Letter", width="small") def show_letter_dialog(): letter_path = Path(file).resolve().parent / "assets" / "letter.png"
- show_image(letter_path, use_container_width=True)
- show_image(letter_path, width="stretch")
def play_background_music(file_path, loop=True): {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run game regression ... .............. #
Ran 14 tests in 0.006s
OK <shellId: 56 completed with exit code 0> {"command":"python -m py_compile 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py' 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remember... Dear friend,
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. You know me, and you know this house.
I don't know what you'll find when you get here. My thoughts are harder to hold on to each day; I can't always tell what I've seen from what I remember. But please, don't give up on me. I don't know what else to do.
Your friend, E. <shellId: 58 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- README.md the-house-that-remembers/app.py the-house-that-remembers/story/canon.json; git... Output too large to read at once (11.5 KB). Saved to: C:\Users\lucia\AppData\Local\Temp\1793216279851-copilot-tool-output-8656-38018f06-4090-479e-8711-a946e667ad6d.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): warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it M README.md M the-house-that-remembers/app.py M the-house-that-remembers/story/canon.json warning: in the working copy of 'README.md', LF <shellId: 57 completed with exit code 0> {"pattern":"takeable|Library Book|Upper Landing Note|The Letter","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py","C:\Users\lucia\Documents\codi... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:72:@st.dialog("The Letter", width="small") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:180: if item == "The Letter": C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:181: if st.button("📜 The Letter 🔍", key="open_letter"): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:159: "takeable": "Library Book", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:263: "takeable": "Upper Landing Note", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:323: "inventory": ["The Letter"], C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:751: item = feat.get("takeable") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:767: item = feat.get("takeable") { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 34, 80 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -34,47 +34,47 @@ 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? '''
def generate_response(game): result = ollama.chat( model=MODEL_NAME, messages=build_messages(game, CANON), think=False, options=generation_options(game), ) return tidy(result.message.content)
def show_image(path, **kwargs): """Show an image, or a quiet caption if the file is missing (instead of crashing the app).""" if Path(path).exists(): st.image(path, **kwargs) else: st.caption(f"(image not found: {Path(path).name})")
@st.dialog("The Letter", width="small") def show_letter_dialog(): letter_path = Path(file).resolve().parent / "assets" / "letter.png" show_image(letter_path, width="stretch")
def play_background_music(file_path, loop=True): audio_path = Path(file_path) if not audio_path.is_absolute(): { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 150, 190 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -150,41 +150,41 @@ st.markdown("### 📍 Current location") st.write(game["room"])
st.markdown("### 🧍 Friend")
friend_room = game.get("friend_room", "Entrance Hall")
st.write("Here with you" if friend_room == game["room"] else f"In the {friend_room}")
st.markdown("### 🧩 Memory fragments")
completed_fragments = len(fragments_complete(game))
st.write(f"**{completed_fragments}/3 complete**")
fragment_status = fragment_progress(game)
for kind in FRAGMENT_KINDS:
progress = fragment_status[kind]
marker = "✓" if progress["complete"] else "○"
st.caption(
f"{marker} {FRAGMENT_LABELS[kind]}: "
f"{progress['found']}/{FRAGMENT_CLUES_NEEDED} clues"
)
if game.get("truth_known"):
st.markdown("### 🕯️ Family memories")
shared_memories = game.get("final_revelations", [])
st.write(f"**{len(shared_memories)}/{len(FINAL_REVELATIONS)} remembered**")
for memory in FINAL_REVELATIONS:
if memory["id"] in shared_memories:
st.caption(f"✓ {memory['title']}")
st.markdown("### 🎒 Inventory")
if game["inventory"]:
for item in game["inventory"]:
if item == "The Letter":
if st.button("📜 The Letter 🔍", key="open_letter"):
show_letter_dialog()
else:
st.write(f"• {item}")
else:
st.caption("Your pockets are empty.")
st.markdown("### 🔎 Clues")
if game["clues"]:
for clue in game["clues"]:
{"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git status --short; Get-ChildItem 'the-house-that-remembers\assets' | Select-Object Name","description":"Check worktre... M .gitignore M README.md M the-house-that-remembers/app.py M the-house-that-remembers/engine.py M the-house-that-remembers/prompt.py M the-house-that-remembers/story/canon.json ?? the-house-that-remembers/assets/letter.png
Name #
ambience.mp3
banner.png
bedroom.jpg
hallway.jpg
haunted_house.jpg
kitchen.jpg
letter.png
library.jpg
rain-and-thunder.mp3
Sources.md
staircase.jpg
upper_landing.jpg
<shellId: 59 completed with exit code 0>
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
@st.dialog("The Letter", width="small")
def show_letter_dialog():
letter_path = Path(file).resolve().parent / "assets" / ...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -75,6 +75,18 @@
show_image(letter_path, width="stretch")
+@st.dialog("The Book", width="small") +def show_book_dialog():
- book_path = Path(file).resolve().parent / "assets" / "book.png"
- show_image(book_path, width="stretch")
+@st.dialog("The Note", width="small") +def show_note_dialog():
- note_path = Path(file).resolve().parent / "assets" / "note.png"
- show_image(note_path, width="stretch")
def play_background_music(file_path, loop=True): audio_path = Path(file_path) if not audio_path.is_absolute(): @@ -180,6 +192,12 @@ if item == "The Letter": if st.button("📜 The Letter 🔍", key="open_letter"): show_letter_dialog()
-
elif item == "Library Book": -
if st.button("📖 Library Book 🔍", key="open_book"): -
show_book_dialog() -
elif item == "Upper Landing Note": -
if st.button("📝 Upper Landing Note 🔍", key="open_note"): -
else:show_note_dialog() else: st.write(f"• {item}")
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 @@ -55,9 +55,11 @@ After entering, the friend shares four family memories gradually; the two of you can leave together once he is ready and has faced the full story.
-Select 📜 The Letter 🔍 in the inventory to view the letter image in a
-dismissible dialog. Add the image at
-the-house-that-remembers/assets/letter.png.
+Select 📜 The Letter 🔍, 📖 Library Book 🔍, or
+📝 Upper Landing Note 🔍 in the inventory to view its image in a dismissible
+dialog. Add the images at the-house-that-remembers/assets/letter.png,
+the-house-that-remembers/assets/book.png, and
+the-house-that-remembers/assets/note.png.
Run the Streamlit app
{"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run game regression ... .............. #
Ran 14 tests in 0.006s
OK <shellId: 60 completed with exit code 0> { "command": "python -m py_compile 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py';", "description": "Check Streamlit app syntax" } <shellId: 62 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- README.md the-house-that-remembers/app.py; git status --short -- README.md the-house-tha... warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it M README.md M the-house-that-remembers/app.py ?? the-house-that-remembers/assets/letter.png <shellId: 61 completed with exit code 0> {"pattern":"do not open|do not change|Do not open|Do not change","paths":"C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers","output_mode":"content","-n":true,"head_limit"... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:265: "The parents' bedroom door is shut. The folded note taped to its frame reads, in a careful hand: 'Do not open. Do not change anything.'", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:269: ("A note on the parents' bedroom door reads: do not open, do not change anything", 1, "rules"), C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:573: "help him. Do not open the door." {"pattern":"bedroom door|note on the parents","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json","C:\Users\lucia\Documents\coding stuff\h... C:\Users\lucia\Documents\coding stuff\hacktober\README.md:54:The sidebar tracks the three memory fragments needed to open the bedroom door. C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:39: "The friend survived. He knocked on the bedroom door once, heard no reply, and did not open it. He has pretended ever since that his family is alive and resting, and the house helps him not to see the truth.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:62: "characterization": "Lived in denial, believing the others were overreacting. He ignored his father's rules and fell out with him. He obeyed only the last one, not to open the bedroom door, out of anger and fear. He survived, but his sense of time and memory of the final days are distorted.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:70: "Reveal the truth only in the final stage, after the bedroom door is opened by the engine.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:163: "may_learn": "Slow knocking and footsteps come from behind the locked bedroom door. The parents are not trying to frighten the player; they want the repeating memory to end." C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:174: "puzzles": "1: the parents' bedroom door", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:253: "description": "Three kinds of clue. Finding two clues of a kind completes that fragment. The sidebar tracks each category and the total out of three. Completing all three lets the player try the bedroom door.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:255: "rules": "The father's preservation. Clues: the stopped hall clock, the outdated calendar, the note on the bedroom door.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:260: "door": "The bedroom door opens only if all three fragments are complete and denial is at or below 3. Opening it moves the story to the final stage.", { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 250, 274 ] } 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 @@ -250,25 +250,25 @@ "description": ( "The upper landing is long and narrow, lit by a single grey window. " "Every door along it is shut. At the far end, a folded note is taped " "to the frame of one door." ), "exits": ["Grand Staircase", "Parents' Bedroom"], "img": "upper_landing.jpg", "gated": {"Parents' Bedroom": "door_open"}, "aliases": ["upper landing", "upstairs landing", "landing", "upper floor", "second floor", "upstairs", "first floor", "second story"], "features": { "door": { "ambient": True, "keywords": ["door", "note", "knock", "knocking"], "takeable": "Upper Landing Note", "looks": [ "The parents' bedroom door is shut. The folded note taped to its frame reads, in a careful hand: 'Do not open. Do not change anything.'", "You press your ear to the wood. From inside comes a slow knock, three times. Then nothing.", ], "clues": [ ("A note on the parents' bedroom door reads: do not open, do not change anything", 1, "rules"), ("Slow knocking comes from inside the locked bedroom", 2, "night"), ], "friend": "He will not come near the door.", }, }, {"pattern":"do not change anything|note on the parents' bedroom door|folded note taped","paths":"C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers","output_mode":"content"... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:265: "The parents' bedroom door is shut. The folded note taped to its frame reads, in a careful hand: 'Do not open. Do not change anything.'", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:269: ("A note on the parents' bedroom door reads: do not open, do not change anything", 1, "rules"), *** Begin Patch *** Update File: the-house-that-remembers/engine.py @@ "takeable": "Upper Landing Note", "looks": [
-
"The parents' bedroom door is...
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 @@ -262,11 +262,11 @@ "keywords": ["door", "note", "knock", "knocking"], "takeable": "Upper Landing Note", "looks": [
-
"The parents' bedroom door is shut. The folded note taped to its frame reads, in a careful hand: 'Do not open. Do not change anything.'",
-
"The parents' bedroom door is shut. The folded note taped to its frame reads, in a careful hand:\n'Do not open\n\nDo not change\n\nI have to keep things as they are.'", "You press your ear to the wood. From inside comes a slow knock, three times. Then nothing.", ], "clues": [
-
("A note on the parents' bedroom door reads: do not open, do not change anything", 1, "rules"),
-
("A note on the parents' bedroom door reads: do not open; do not change; I have to keep things as they are", 1, "rules"), ("Slow knocking comes from inside the locked bedroom", 2, "night"), ], "friend": "He will not come near the door.",
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 @@ -113,6 +113,10 @@ note_result = act(game, "read the note") self.assertEqual(note_result["outcome"], "examined") self.assertIn("Upper Landing Note", game["inventory"])
-
self.assertIn( -
"I have to keep things as they are", -
note_result["detail"], -
)def test_sister_appears_once_when_stage_three_is_active(self): game = engine.new_game() {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run note and engine ... ..............
Ran 14 tests in 0.006s
OK <shellId: 63 completed with exit code 0> { "command": "python -m py_compile 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py';", "description": "Check engine syntax" } <shellId: 65 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/engine.py the-house-that-remembers/tests/test_engine.py; git st... warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it M the-house-that-remembers/engine.py <shellId: 64 completed with exit code 0> { "skill": "python-fact-grounded-coding" } Skill "python-fact-grounded-coding" loaded successfully. Follow the instructions in the skill context. { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 60, 98 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -60,39 +60,39 @@ ) return tidy(result.message.content)
def show_image(path, **kwargs): """Show an image, or a quiet caption if the file is missing (instead of crashing the app).""" if Path(path).exists(): st.image(path, **kwargs) else: st.caption(f"(image not found: {Path(path).name})")
@st.dialog("The Letter", width="small") def show_letter_dialog(): letter_path = Path(file).resolve().parent / "assets" / "letter.png" show_image(letter_path, width="stretch")
@st.dialog("The Book", width="small") def show_book_dialog(): book_path = Path(file).resolve().parent / "assets" / "book.png" show_image(book_path, width="stretch")
@st.dialog("The Note", width="small") def show_note_dialog(): note_path = Path(file).resolve().parent / "assets" / "note.png" show_image(note_path, width="stretch")
def play_background_music(file_path, loop=True):
audio_path = Path(file_path)
if not audio_path.is_absolute():
audio_path = Path(file).resolve().parent / audio_path
if not audio_path.exists():
st.caption(f"(audio not found: {audio_path.name})")
return
audio_base64 = base64.b64encode(audio_path.read_bytes()).decode("ascii")
loop_attr = "loop" if loop else ""
{"pattern":"show_book_dialog|Library Book|book\.png","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py","C:\Users\lucia\Documents\coding stuff\hack...
C:\Users\lucia\Documents\coding stuff\hacktober\README.md:58:Select 📜 The Letter 🔍, 📖 Library Book 🔍, or
C:\Users\lucia\Documents\coding stuff\hacktober\README.md:61:the-house-that-remembers/assets/book.png, and
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:79:def show_book_dialog():
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:80: book_path = Path(file).resolve().parent / "assets" / "book.png"
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:195: elif item == "Library Book":
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:196: if st.button("📖 Library Book 🔍", key="open_book"):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:197: show_book_dialog()
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
@st.dialog("The Book", width="small")
def show_book_dialog():
book_path = Path(file).resolve().parent / "assets" / "book....
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -79,6 +79,17 @@
def show_book_dialog():
book_path = Path(file).resolve().parent / "assets" / "book.png"
show_image(book_path, width="stretch")
- st.markdown(
-
'**"The Fall of the House of Usher" by Edgar Allan Poe is a short story ' -
"first published in 1839. A narrator visits his ailing childhood friend " -
"Roderick Usher at his decaying family mansion. There he encounters " -
"Roderick's mysterious twin sister Madeline, who suffers from a strange " -
"illness. As the atmosphere grows increasingly oppressive, the house " -
"itself seems alive with malevolent energy. When Madeline is entombed " -
"within the estate, terrifying sounds begin echoing through the halls, " -
"building toward a shocking climax that binds the fate of the twins to " -
'their ancestral home."**' - )
@st.dialog("The Note", width="small") {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run game regression ... .............. #
Ran 14 tests in 0.007s
OK <shellId: 66 completed with exit code 0> { "command": "python -m py_compile 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py'", "description": "Check Streamlit app syntax" } <shellId: 68 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py; git status --short -- the-house-that-remembers/app.py",... warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it M the-house-that-remembers/app.py <shellId: 67 completed with exit code 0> {"pattern":"^def (new_game|fragment_progress|fragments_complete|_try_door|_confront|_resolve|_advance_stage|apply_action)|^STAGE_RULES|^FINAL_REVELATIONS|^DOOR_DENIAL_MAX|^PUSH_LIMIT|^SUPPORT_LIMIT|^M... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:20: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:21:PUSH_LIMIT = 3 # blunt confrontations before the evidence -> he shuts down C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:22:SUPPORT_LIMIT = 3 # times kindness can lower his denial C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:23:MAX_TURNS = 90 # unresolved truth or denial by then -> "denial" ending C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:28:FINAL_REVELATIONS = ( C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:319:def new_game(): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:349:def fragment_progress(game): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:365:def fragments_complete(game): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:391:LEAVE_RE = re.compile( C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:396:TOGETHER_RE = re.compile( C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:461:def _end(game, ending, detail): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:603:def _try_door(game): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:630:def _confront(game): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:656:def _resolve(game, text): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:669: if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:689: if LEAVE_RE.search(text) and not negated: C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:810:STAGE_RULES = [ C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:818:def _advance_stage(game): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:832:def apply_action(game, raw_action): { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 28, 84 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -28,57 +28,57 @@ "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.", "player_role": "The player is an outsider and the friend's childhood companion. They did not cause the family's tragedy and have no secret responsibility for it.", "core_question": "Can the friend face what happened to his family, and can the player help him let the house's preserved memories go?", "core_conflict": "The parents' lingering spirits want release. The sister's ghost wants to preserve the family and believes keeping memories alive is protective. The friend is caught between denial, fear, and the possibility of remembering." }, "fixed_truths": [ "The friend wrote the letter because his condition and the house's disturbances have become unbearable. The letter was not secretly written by the house. Part of him senses the truth, which is why he asked for help.", "The house is genuinely haunted, and was haunted by earlier tragedies before the family moved in.", "The family moved in when the friend was a child. At first things were ordinary; strange details developed slowly and were brushed off.", "The house drained the family's energy, starting with the mother. It also freezes and distorts time: clocks stop, calendars stay on an old date, food does not spoil, and the friend cannot tell how much time has passed.", "The mother, father and sister are dead. They died inside the parents' bedroom after being locked in and cut off from help, drained by the house. There was no violence. The house preserved the room, so they look as if they are asleep.", "The friend survived. He knocked on the bedroom door once, heard no reply, and did not open it. He has pretended ever since that his family is alive and resting, and the house helps him not to see the truth.", "The friend behaves as though his whole family is alive: his mother is resting, his father is busy, his sister is somewhere around.", "The sister is a ghost. She roams the house, hums, appears intermittently, and warns the player to leave. Her warnings are sincere. She believes preserving the family exactly as it is keeps them alive.", "The parents linger as fragments tied to the bedroom: slow knocking and footsteps. They want release, not endless preservation.", "The friend's warning 'Whatever you do, don't answer it' refers to the knocking from the parents' bedroom.", "As the friend's denial weakens and memories return, reality becomes less consistent: paintings change, familiar spaces seem altered, sounds repeat, and doors or rooms may feel wrong.", "The player can leave when the game state allows it. Leaving early is a valid choice.", "The player's presence gives the friend an external point of reference, but cannot cure him or force him to accept the truth. He must acknowledge it himself." ], "family_history": { "father": { "characterization": "Moved the family to the house. As his wife declined and doctors could not help, he grew paranoid and began to lose himself too. His only idea was to preserve everything as it was: locking doors, stopping clocks, changing nothing. He fell out with his son over the son's denial. In the end he locked himself, his wife and his daughter in the parents' bedroom to 'preserve' them and told his son never to open the door. His attempt to protect them is what killed them.", "guidance": "Show fear and a distorted desire to protect, not simple villainy." }, "mother": { "characterization": "The first to be affected. A stay-at-home mother who spent the most time in the house, she grew depressed and lethargic and could not sleep, as if the house were draining her. She recognized that something was wrong.", "guidance": "Treat her suffering seriously and compassionately, never as a moral failing." }, "sister": { "characterization": "Supportive but cautious. She still went out for food and tried to help the family hold together, and she was protective of her brother, urging him not to give up on them. After death she clings to the family as it is, believing preservation keeps them alive.", "guidance": "Protective, loving, frightened and deluded at once. Not evil." }, "friend": { "characterization": "Lived in denial, believing the others were overreacting. He ignored his father's rules and fell out with him. He obeyed only the last one, not to open the bedroom door, out of anger and fear. He survived, but his sense of time and memory of the final days are distorted.", "guidance": "His progression is governed by explicit game-state rules (denial, fragments, door), not by the model." }, "tragedy_details": { "status": "AUTHORED", "truth": "After the father and son stopped speaking, the father locked himself, the mother and the sister in the parents' bedroom and told the son never to open the door. Cut off from help and drained by the house, they died inside. The house preserved the room as it was. The friend knocked once, heard nothing, and never opened the door, then convinced himself the family was alive.", "timing": "Recent but deliberately unfixed. The house distorts time, so never give a number of days.", "narration_guidance": [ "Reveal the truth only in the final stage, after the bedroom door is opened by the engine.", "Before that, use oblique fragments only: knocking, repeating footsteps, humming, stopped clocks.", "Never describe wounds, decay or gore. The family looks as if they are sleeping.", "The friend should face the truth in halting fragments and quiet realization, not a lecture." ] } }, "supernatural_rules": [ { "name": "Memory imprint", "description": "Rooms, paintings, objects, and sounds can preserve emotional impressions of past events." }, { "name": "Distortion", "description": "The house can replay, rearrange, or distort remembered details. An apparent memory is not automatically a literal recording of objective events." { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 140, 178 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -140,39 +140,39 @@ "friend_explanation": "When pressed, he struggles to explain that the house remembers—not merely holding memories, but collecting and twisting them. He says he can no longer separate himself from its echoes.", "thematic_line": "The house is feeding on what it remembers, and I am becoming its echo.", "friend_warning_meaning": "'Don't answer it' means the knocking from the parents' bedroom." }, "revelation_order": [ { "stage": "0 Arrival and uneasy normality", "may_learn": "The friend is unwell, the house is unsettling, and he wants help. The disturbances are not yet understood." }, { "stage": "1 Contradictions", "may_learn": "The friend speaks of his family as alive and resting, but nothing quite adds up." }, { "stage": "2 Frozen house and family traces", "may_learn": "Clocks are stopped, the calendar is outdated, food never spoils, and places are set for people who are not there. Portraits change." }, { "stage": "3 Sister's warnings", "may_learn": "A figure associated with the sister warns the player to leave. Her concern and wish to preserve the family become apparent." }, { "stage": "4 Parents' fragments", "may_learn": "Slow knocking and footsteps come from behind the locked bedroom door. The parents are not trying to frighten the player; they want the repeating memory to end." }, { "stage": "5 Reconstructed truth", "may_learn": "The door opens and the player sees the family, preserved as if asleep. The friend faces what he did not do." } ], "design_constraints": { "mvp": { "rooms": "6: Entrance Hall, Library, Kitchen, Grand Staircase, Upper Landing, Parents' Bedroom (locked)", "key_clues": "13 clues in three fragments (routine, rules, night)", "puzzles": "1: the parents' bedroom door", "major_ghosts": 1, "endings": "3 implemented (leave_together, escape_alone, denial); 1 optional (sisters_bargain)" }, "player_agency": [ { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 188, 270 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -188,83 +188,83 @@ ] }, "ending_directions": [ { "id": "leave_together", "status": "IMPLEMENTED", "description": "True good ending. The player assembles the fragments, opens the door, the friend faces what happened, and they leave the house together.", "engine_condition": "Bedroom door opened, truth known, denial below 3, all four authored family memories shared, and player chooses to leave with the friend." }, { "id": "escape_alone", "status": "IMPLEMENTED", "description": "The player leaves the house alone and the friend stays behind, usually without ever learning what happened.", "engine_condition": "Player leaves from the Entrance Hall." }, { "id": "denial", "status": "IMPLEMENTED", "description": "Bad ending. The friend is too deep in denial, refuses help, and the house slowly consumes him.", "engine_condition": "Player confronts him bluntly too many times before finding the evidence, or too many turns pass without the truth being faced." }, { "id": "sisters_bargain", "status": "OPTIONAL_NOT_IMPLEMENTED", "description": "The player accepts the sister's offer to keep the illusion alive. The morality is left ambiguous." } ], "narrator_contract": { "role": "Write immersive, atmospheric prose and character dialogue in response to an engine-validated action.", "must_do": [ "Use current state and relevant canon to maintain continuity.", "Describe sensory details and emotional reactions without overexplaining.", "Keep player-discovered knowledge separate from hidden canon.", "Respect established personalities and motivations.", "Narrate failed actions as failures without quietly changing the rules." ], "must_not_do": [ "Invent or finalize the unresolved death sequence.", "Reveal major secrets before the player earns them through clues or milestones.", "Contradict inventory, clues, room layout, puzzle state, locks, character condition, or ending eligibility.", "Treat every supernatural impression as objective fact.", "Force the player to leave, stay, trust a character, forgive someone, or choose an ending.", "Add a major canon fact, hidden crime, family member, or player backstory without author approval." ], "recommended_turn_input": [ "Current room and available exits", "Player action", "Engine result", "Inventory and discovered clues", "Relevant character state", "Current story milestone and revelation permissions", "Only the canon excerpts needed for this turn" ] }, "author_todo": [ "Decide whether to implement the optional 'sisters_bargain' ending.", "Write a few sister ghost lines for stage 3 in her own voice.", "Playtest pacing knobs: AMBIENT_AFTER_TURNS, AMBIENT_COOLDOWN, MAX_TURNS, PUSH_LIMIT." ], "house_history": { "status": "OPTIONAL_EASTER_EGG", "summary": "The house's first owner buried his sister before she was truly gone and died soon after. The house stood empty for years before it was put on the market and the family moved in.", "delivery": "Found only in a water-stained local history in the library. Never explained by characters. The house is still haunted by this and other past tragedies." }, "memory_fragments": { "description": "Three kinds of clue. Finding two clues of a kind completes that fragment. The sidebar tracks each category and the total out of three. Completing all three lets the player try the bedroom door.", "routine": "The friend is still keeping house for people who are not there. Clues: third cup, pulled-out chair, food that never spoils.", "rules": "The father's preservation. Clues: the stopped hall clock, the outdated calendar, the note on the bedroom door.", "night": "What happened at the door. Clues: repeating footsteps overhead, the humming, the slow knocking from inside the bedroom." }, "engine_rules": { "denial": "Friend's denial runs 0-10. It starts high and drops by 1 for each clue found and each of up to 3 kind gestures. Blunt confrontation before all three fragments raises it by 1.", "door": "The bedroom door opens only if all three fragments are complete and denial is at or below 3. Opening it moves the story to the final stage.", "family_revelations": "After the player enters the bedroom, face-to-face conversation shares four authored family memories one at a time when denial is below 3: the mother's decline and the family's deaths, the father's attempt to preserve them, the sister's hope, and the friend's conflict and silence.", "together_ending": "The friend can leave together only after denial is below 3 and all four family memories have been shared.", "pushing": "Three blunt confrontations before the evidence ends the story in the denial ending.", "clock": "If the truth has not been faced after about 90 turns, the denial ending triggers." } } { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 48, 170 ] } 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 @@ -48,123 +48,123 @@ STAGES = [ { # 0 - Arrival and uneasy normality "now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet. Friend invited the player to the house because he is lonely and the history of the house is weighing on him. He is not aware of the truth, but he doesn't want to be alone.", "friend": "He tries to act normal and spend ordinary time together. He can ask about the player's life or mention an established childhood memory. He deflects direct questions about the house and does not bring up family members on his own.", "steer": "Let the player settle in and explore. Keep the friend's suggestions tied to what the player just said; if tea or a drink is wanted, the kitchen is the natural next step. Do not offer upstairs or list destinations without a reason. When the friend invites the player somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.", }, { # 1 - Contradictions "now": "Details about the friend's family do not quite add up.", "friend": "He mentions his family casually, in the present tense, as if they are alive and nearby: his mother is resting, his father is busy upstairs, his sister is 'around'. He does not notice anything odd. If asked to see them, or asked directly about them, he answers vaguely and changes the subject.", "steer": "Let this come up once or twice in passing, e.g. "Mum's resting. She hasn't been sleeping well." Do not explain it.", }, { # 2 - Frozen house and family traces "now": "The house is frozen in time: clocks are stopped, calendars are out of date, food never spoils, and places are set for people who are not there. Portraits can change between looks. The friend has lost track of time.", "friend": "He says time is 'strange' or that he has lost count of the days. He grows uneasy when stopped clocks, old dates or the extra place settings are mentioned and tries to move on.", "steer": "Describe frozen or changed details only when the engine provides them. Otherwise keep the house quietly wrong: a sound repeating, a door slightly off.", }, { # 3 - Sister's warnings "now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.", "friend": "He does not see her, or insists the player imagined it.", "steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most, then gone. She speaks gently and urgently. She never threatens. Do not say what happened to the friend's family.", }, { # 4 - Parents' fragments "now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.", "friend": "He flinches at the knocking and tells the player, "Whatever you do, don't answer it." Fragments of memory slip out, then he retreats.", "steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.", }, { # 5 - Reconstructed truth "now": "The bedroom door has opened. The family inside are preserved as if asleep. The player faces the truth on entering; the friend can only share the family story as the engine authorizes each memory.", "friend": "His denial breaks only as he becomes ready. He speaks in halting fragments and quiet realization, not speeches. If denial is still 3 or higher, he resists and cannot share the next family memory. When denial falls below 3, the ENGINE RESULT may supply one authored family memory; include it honestly and clearly, without adding facts. He does not blame the player. He never agrees to leave together unless the engine ends the story.", "steer": "If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, the friend shares the mother's decline, the father's choice, the sister's hope, and his own silence one memory at a time when the ENGINE RESULT supplies each. Let the moment breathe between disclosures. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.", }, ]
ENDINGS = { "leave_together": ( "The player and the friend have faced the truth and choose to leave together. " "Write the final scene in 4-6 sentences: the friend's grief and fragile steadiness, the house letting go " "(the stopped clocks may tick once; the knocking and humming fall silent), the door, the rain easing. " "No gore. Offer no further choices. End the story." ), "escape_alone": ( "The player has chosen to leave the house alone, and the engine confirms it. " "Write the final scene in 4-6 sentences: the door, the rain, the friend's reaction according to his condition, " "the house behind them. Do not explain what happened to the family. Offer no further choices. End the story." ), "denial": ( "The friend could not face it. He refuses help and withdraws, insisting everyone is only resting. " "Write the final scene in 4-6 sentences: the player cannot reach him, the house settles around him, " "the knocking and humming resume as though nothing happened, and he is left with the house. " "Do not describe the family or explain what happened. Offer no further choices. End the story." ), }
def denial_band(denial): if denial >= 7: return "high denial: he insists things are fine, deflects, changes the subject" if denial >= 3: return "wavering: he s, almost admits things, then retreats" return "cracking: fragments of memory slip out and he is visibly shaken"
def story_block(canon): f = canon["character_dynamics"]["friend"] return ( "STORY PREMISE\n" f"{canon['premise']['summary']}\n\n" "THE FRIEND (his name is not established; the letter is signed with the initial 'E.')\n" "- Refer to him as 'your friend' or 'he'; do not call him 'E.' as though that were his name.\n" f"- Condition: {f['initial_state']}\n" f"- Personality: {f['personality']}\n" f"- Voice: {f['voice']}\n" f"- Early behavior: {f['early_behavior']}\n" "- Example lines (for tone only; do not repeat them verbatim):\n" f"{FRIEND_EXAMPLES}\n\n" "THE LETTER HE SENT\n" f"{canon['opening']['letter_text']}" )
def stage_block(game): if game["ending"]: text = ENDINGS[game["ending"]] if game["ending"] == "escape_alone": text += ( " The player saw the truth in the bedroom before leaving; the friend stays behind with his family." if game["truth_known"] else " The player never learned what was upstairs; the friend stays behind in the house." ) return "ENDING\n" + text s = STAGES[game["stage"]] truth_permission = "" if game["stage"] == 5: from engine import FINAL_REVELATIONS
if not game.get("truth_known"):
truth_permission = (
"\nTRUTH STATUS: The bedroom door is open, but the player has not entered "
"and has not faced the truth. Do not reveal the family's fate yet."
)
elif game["denial"] >= 3:
truth_permission = (
"\nTRUTH STATUS: The player has seen the truth, but the friend is still "
"resisting it. He may deny or minimize it and cannot yet give a clear account. "
"Do not force acceptance; let sincere support soften him. If hi
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 15, 72 ] } 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 @@ -15,58 +15,58 @@ FRAGMENT_LABELS = { "routine": "Routine", "rules": "Rules", "night": "Night", } DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door PUSH_LIMIT = 3 # blunt confrontations before the evidence -> he shuts down SUPPORT_LIMIT = 3 # times kindness can lower his denial MAX_TURNS = 90 # unresolved truth or denial by then -> "denial" ending NUDGE_AFTER_TURNS = 4 NUDGE_COOLDOWN = 3 SISTER_COOLDOWN = 12
FINAL_REVELATIONS = ( { "id": "mother", "title": "His mother's decline", "detail": ( "They are dead. The house drained them, beginning with his mother, who " "grew exhausted and unable to sleep and sensed something was wrong. The " "room was preserved, so they look as if they are sleeping." ), }, { "id": "father", "title": "His father's attempt to preserve them", "detail": ( "After help failed, his father became frightened and tried to preserve " "the family unchanged. He locked himself, his wife, and his daughter in " "the bedroom and told his son never to open the door. He thought he was " "protecting them, but shut them away from help; they died there, drained " "by the house." ), }, { "id": "sister", "title": "His sister's hope", "detail": ( "His sister kept trying to hold the family together and urged her brother " "not to give up on them. Now she clings to the house's preserved illusion, " "believing it keeps her family together; her warnings to leave are real, " "but she mistakes letting go for losing them." ), }, { "id": "friend", "title": "Why he stayed silent", "detail": ( "He had argued with his father and believed the others were overreacting. " "He knocked once, heard no answer, and obeyed the last order out of anger " "and fear. Since then he has pretended they were still alive and resting, " "unable to face that he never opened the door." ), }, )
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 319, 385 ] } 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 @@ -319,67 +319,67 @@ def new_game(): return { "room": "Entrance Hall", "friend_room": "Entrance Hall", "inventory": ["The Letter"], "clues": [], "visited": ["Entrance Hall"], "examined": {}, # "Room:feature" -> times examined "stage": 0, # index into prompt.STAGES "turns_in_stage": 0, "turn_count": 0, "room_turns": 0, # turns spent in the current room since arriving "last_notice_turn": -99, # turn_count when a detail was last surfaced "last_nudge_turn": -99, # turn_count when the friend was last nudged "nudges_here": 0, # nudges in this room since last move (escalate) "last_sister_turn": -99, "denial": 8, # friend's denial (high = deflecting) "pushed": 0, "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 "final_revelations": [], # authored family memories shared by the friend "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"}], "summary": "", # rolling summary of folded-away history "summary_idx": 0, # history[:summary_idx] is already summarized }
def fragment_progress(game): """Return clue counts and completion status for each memory fragment.""" counts = {kind: 0 for kind in FRAGMENT_KINDS} for clue in game["clues"]: frag = CLUE_FRAGMENT.get(clue) if frag: counts[frag] = counts.get(frag, 0) + 1 return { kind: { "found": counts[kind], "complete": counts[kind] >= FRAGMENT_CLUES_NEEDED, } for kind in FRAGMENT_KINDS }
def fragments_complete(game): """Set of completed fragment kinds ('routine', 'rules', 'night').""" return { kind for kind, progress in fragment_progress(game).items() if progress["complete"] }
def exit_labels(game, room_name): """Exit names for display, marking locked ones.""" room = ROOMS[room_name] gated = room.get("gated", {}) labels = [] for e in room["exits"]: flag = gated.get(e) labels.append(f"{e} (door locked)" if flag and not game[flag] else e) return labels
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 595, 650 ] } 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 @@ -595,56 +595,56 @@ ) return ( "The player has lingered. Let the friend drift toward the exit that leads to " f"{where}, stop there, and look back. The player is still in the {room_name} " "until they choose to move." )
def _try_door(game): if game["door_open"]: return _result("no_change", "The bedroom door already stands open.") if len(fragments_complete(game)) < 3: return _result( "blocked", "The door will not give. It is not only locked: pushing at it feels like pushing " "against something holding its breath. The friend begs the player to leave it. " "The player does not yet understand enough to know why it matters.", friend="He is frightened and pleads with the player to step away.", ) if game["denial"] > DOOR_DENIAL_MAX: return _result( "blocked", "The door stays shut. The friend's hand closes on the player's arm. He is not ready, " "and the door will not open until he is.", friend="He is trembling and cannot let the player open it yet.", ) game["door_open"] = True return _result( "door_opened", "The door opens at last, without resistance, as if it had only ever been waiting. " "Cold, still air spills out. The friend stands frozen beside the player. " "The player has not yet gone in.", )
def _confront(game): if len(fragments_complete(game)) >= 3: game["denial"] = max(0, game["denial"] - 1) return _result( "no_change", "The player says aloud what the evidence points to. This time the friend does not " "deny it outright: he wavers, almost admits it, then stops, voice cracking. " "Nothing else changes.", friend="He is on the edge of breaking.", ) game["pushed"] += 1 game["denial"] = min(10, game["denial"] + 1) if game["pushed"] >= PUSH_LIMIT: return _end( game, "denial", "The player pushed too hard, too soon. The friend shuts down completely.", ) warn = " He is close to shutting down." if game["pushed"] == PUSH_LIMIT - 1 else "" return _result( "no_change", "The player bluntly says the friend's family is dead before having any evidence. " { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 655, 730 ] } 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 @@ -655,76 +655,76 @@
def _resolve(game, text): intent = _classify_intent(text, game) spoken = text # full text, quoted speech included text = QUOTED_TEXT_RE.sub("", text).lower() # actions only: speech can't move or examine things room_name = game["room"] room = ROOMS[room_name] room_parse_text = _remove_inventory_mentions(text, game["inventory"]) rooms_named = _find_rooms(room_parse_text) negated = bool(NEGATION_RE.search(text)) spoken_negated = bool(NEGATION_RE.search(spoken))
if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
if game["denial"] >= 3:
return _result(
"blocked",
"The friend is not ready to leave together yet. He has seen the truth "
"but is still resisting it; do not make him agree or move toward the exit.",
)
if len(game.get("final_revelations", [])) < len(FINAL_REVELATIONS):
return _result(
"blocked",
"The friend is not ready to leave together yet. There are still family "
"memories he has not been able to share; do not make him agree or end "
"the story now.",
)
return _end(
game,
"leave_together",
"The player and the friend choose to leave the house together.",
)
if LEAVE_RE.search(text) and not negated:
if room_name == "Entrance Hall":
return _end(game, "escape_alone", "The player walks out of the house alone.")
return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
if not target:
if re.search(r"\bback\b", text) and room_name in BACK:
target = BACK[room_name]
elif re.search(r"\b(up|upstairs)\b", text) and (room_name, "up") in VERTICAL:
target = VERTICAL[(room_name, "up")]
elif re.search(r"\b(down|downstairs)\b", text) and (room_name, "down") in VERTICAL:
target = VERTICAL[(room_name, "down")]
if target:
route = _path_to(game, room_name, lambda r: r == target)
if route:
path, _ = 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["room"] = target
game["room_turns"] = 0
game["nudges_here"] = 0
detail = f"The player moved to the {target}."
if len(path) > 1:
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 800, 960 ] } 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 @@ -800,161 +800,161 @@ "(talking, resting, small gestures). Do not add items, clues, or events. " f"No room change happens: the player is still in the {room_name}, and nobody " f"leads them elsewhere unless they choose an exit ({', '.join(exit_labels(game, room_name))}).", )
STAGE_RULES = [ lambda g: g["turns_in_stage"] >= 5 and len(g["visited"]) >= 2, # 0 arrival lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 1, # 1 contradictions lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 4, # 2 paintings lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 7, # 3 sister's warnings ]
def _advance_stage(game): if game["door_open"] and game["stage"] < 5: game["stage"] = 5 game["turns_in_stage"] = 0 return True if game["stage"] >= 4: return False if STAGE_RULESgame["stage"]: game["stage"] += 1 game["turns_in_stage"] = 0 return True return False
def apply_action(game, raw_action): """Resolve one player action, update game state, return the engine result.""" game.setdefault("friend_room", "Entrance Hall") game.setdefault("sister_seen", False) game.setdefault("last_sister_turn", -99) game.setdefault("final_revelations", []) 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 result = _resolve(game, text) if result["outcome"] == "moved": intent = "movement" elif result["outcome"] == "picked_up": intent = "physical_action" elif result["outcome"] == "examined" and intent not in { "examination", "exploration", }: intent = "examination" result["intent"] = intent if ( intent in {"dialogue", "action_and_dialogue"} and not game["ending"] and game["friend_room"] != game["room"] ): game["friend_room"] = game["room"] result["friend_arrived"] = True result["detail"] += ( f" The friend joins the player in the {game['room']} so they can speak " "face to face." )
game["turn_count"] += 1
game["turns_in_stage"] += 1
if result["outcome"] != "moved":
game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["support_given"] < SUPPORT_LIMIT
and SUPPORT_RE.search(text)
):
game["support_given"] += 1
game["denial"] = max(0, game["denial"] - 1)
result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
if (
not game["ending"]
and game["turn_count"] >= MAX_TURNS
and (not game["truth_known"] or game["denial"] >= 3)
):
result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
if fresh:
game["last_notice_turn"] = game["turn_count"]
if (
not fresh
and result["outcome"] in ("no_change", "examined")
and not game["ending"]
and not game["truth_known"]
and game["room_turns"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_nudge_turn"] >= NUDGE_COOLDOWN
and _next_unnoticed(game, game["room"]) is None
):
nudge = _nudge(game)
if nudge:
result["nudge"] = nudge
game["last_nudge_turn"] = game["turn_count"]
game["nudges_here"] += 1
if result["new_clue"]:
for _ in result["new_clue"].split("; "):
game["denial"] = max(0, game["denial"] - 1)
if (
game["truth_known"]
and game["denial"] < 3
and game["friend_room"] == game["room"]
and intent in {"dialogue", "action_and_dialogue"}
and result["outcome"] not in {"blocked", "ending"}
and len(game["final_revelations"]) < len(FINAL_REVELATIONS)
):
revelation = FINAL_REVELATIONS[len(game["final_revelations"])]
game["final_revelations"].append(revelation["id"])
result["family_memory"] = revelation
result["stage_advanced"] = _advance_stage(game)
sister_due = (
not game["sister_seen"]
or game["turn_count"] - game["last_sister_turn"] >= SISTER_COOLDOWN
)
if game["stage"] >= 3 and sister_due and not game["ending"]:
appearance = (
"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."
if not game["sister_seen"]
else "The friend's sister appears briefly again and quietly repeats her "
"warning to leave. Then she is gone."
)
game["sister_seen"] = True
game["last_sister_turn"] = game["turn_count"]
result["sister"] = appearance
game["last_result"] = result
print("Game state:")
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 175, 215 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -175,41 +175,41 @@
st.markdown("### 🧍 Friend")
friend_room = game.get("friend_room", "Entrance Hall")
st.write("Here with you" if friend_room == game["room"] else f"In the {friend_room}")
st.markdown("### 🧩 Memory fragments")
completed_fragments = len(fragments_complete(game))
st.write(f"**{completed_fragments}/3 complete**")
fragment_status = fragment_progress(game)
for kind in FRAGMENT_KINDS:
progress = fragment_status[kind]
marker = "✓" if progress["complete"] else "○"
st.caption(
f"{marker} {FRAGMENT_LABELS[kind]}: "
f"{progress['found']}/{FRAGMENT_CLUES_NEEDED} clues"
)
if game.get("truth_known"):
st.markdown("### 🕯️ Family memories")
shared_memories = game.get("final_revelations", [])
st.write(f"**{len(shared_memories)}/{len(FINAL_REVELATIONS)} remembered**")
for memory in FINAL_REVELATIONS:
if memory["id"] in shared_memories:
st.caption(f"✓ {memory['title']}")
st.markdown("### 🎒 Inventory")
if game["inventory"]:
for item in game["inventory"]:
if item == "The Letter":
if st.button("📜 The Letter 🔍", key="open_letter"):
show_letter_dialog()
elif item == "Library Book":
if st.button("📖 Library Book 🔍", key="open_book"):
show_book_dialog()
elif item == "Upper Landing Note":
if st.button("📝 Upper Landing Note 🔍", key="open_note"):
show_note_dialog()
else:
st.write(f"• {item}")
else:
st.caption("Your pockets are empty.")
I would add something, an optional ending, sort of.
I would add that it is possible to talk to the sister ghost (but only if certain conditions are met)
The sister would explain her POV if player asks: that she is protecting and preserving the family. she wants things to stay as they are because it is keeping their family alive. (based on the canon). In this scenario it is possible to reach an optional ending - that the player accepts the sister's bargain and leaves alone (the difference is, that he learns partial truth)
That fits the canon: the sister’s “bargain” would offer preservation instead of confronting the family’s deaths. The main design choice is when the player can reach her and how much is revealed; those determine whether this feels like a real alternate path or a quick shortcut.
{"message":"What conditions should unlock speaking with the sister and accepting her bargain? I recommend making her conversational only after the bedroom truth has been seen, with the bargain ending ... User responded: sister_talk_unlock: after two apperances/warnings, when cooldown passes, instead of third warning AND player is alone (friend is not "here with you") she will appear again and give the player an opportunity to speak with her bargain_truth: preservation_belief bargain_acceptance: explicit_choice
I’ll implement that as a third encounter: after two warning appearances, the next cooldown appearance can become a conversation opportunity only when the friend is elsewhere. I’ll ensure addressing her doesn’t summon the friend, and that her perspective and ending require explicit player choices.
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 380, 475 ] } 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 @@ -380,96 +380,96 @@ flag = gated.get(e) labels.append(f"{e} (door locked)" if flag and not game[flag] else e) return labels
MOVE_VERBS = ["go", "enter", "head", "walk", "move", "step", "climb", "ascend", "descend", "return", "run", "visit", "follow", "proceed"] TAKE_VERBS = ["take", "grab", "pick", "pocket", "steal"]
LEAVE_RE = re.compile( r"\b(leave|exit|flee|escape)\b.\b(house|mansion|manor)\b" r"|\bgo outside\b|\bwalk out\b" ) NEGATION_RE = re.compile(r"\b(not|never|without)\b|n't\b|\bwont\b|\bdont\b") TOGETHER_RE = re.compile( r"\b(leave|go|walk|get out|come|take him|lead him)\b.\b(together|with me|with him)\b" r"|\b(together|with me)\b.*\b(leave|go|out)\b" r"|\blet'?s (leave|get out)\b" )
LOOK_AROUND_RE = re.compile( r"\b(look|glance|scan|survey|search|explore|examine|inspect|study)\w*\b.\b(around|about|room|surroundings|area)\b" r"|\b(examine|inspect|search|study|explore|look)\w\b.\b(kitchen|library|hall|staircase|landing|bedroom)\b" r"|\bwhat (do|can) i (see|notice|find)\b|\bnotice anything\b" ) QUOTED_TEXT_RE = re.compile(r'"[^"]"|“[^”]”|‘[^’]’') MEMORY_RE = re.compile( r"\b(remember|recall|used to|when we were (?:kids|children)|" r"back when we|childhood)\b" ) EXAMINE_RE = re.compile( r"\b(examine|inspect|study|look at|check|read|listen to|smell|touch)\b" ) PHYSICAL_ACTION_RE = re.compile( r"\b(?:i\s+)?(?:pat|touch|sit|stand|reach|hold|hug|tap|point|" r"turn|open|close|pick|take|walk|step|move|climb|enter|follow)\b" ) SPEECH_RE = re.compile( r"\b(?:i\s+)?(?:say|ask|tell|reply|answer|whisper|speak|call)\b" r"|\b(?:ask|tell) (?:him|her|your friend)\b" ) DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b") DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b") FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b") SUPPORT_RE = re.compile( r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b" r"|\bit'?s (ok|okay|alright)\b" r"|\b(?:we can (?:do (?:it|this)|face (?:it|this)|get through (?:it|this))" r"|we(?:'ll| will) face (?:it|this)) together\b" r"|\b(?:it'?s|it is) going to be (?:ok|okay|alright)\b" r"|\b(?:i want to help|i can help|let me help|i'?d like to help)\b" r"|\b(?:take my hand|give me your hand)\b" )
def _mentions(text, words): return any(re.search(rf"\b{re.escape(w)}\b", text) for w in words)
def _find_rooms(text): return [name for name, r in ROOMS.items() if _mentions(text, r["aliases"])]
def _remove_inventory_mentions(text, inventory): """Keep room names inside carried item names from becoming movement targets.""" for item in sorted(inventory, key=len, reverse=True): text = re.sub(rf"\b{re.escape(item.lower())}\b", " ", text) return text
def _find_feature(text, room): for name, feat in room.get("features", {}).items(): if _mentions(text, feat["keywords"]): return name, feat return None
def _end(game, ending, detail): game["ending"] = ending return _result("ending", detail)
def _classify_intent(raw_action, game): """Classify what the player is doing so narration responds appropriately.""" action_text = QUOTED_TEXT_RE.sub("", raw_action).lower() action_text = _remove_inventory_mentions(action_text, game["inventory"]) room = ROOMS[game["room"]]
if MEMORY_RE.search(raw_action.lower()):
return "memory"
if LOOK_AROUND_RE.search(action_text):
return "exploration"
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 655, 810 ] } 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 @@ -655,156 +655,156 @@
def _resolve(game, text): intent = _classify_intent(text, game) spoken = text # full text, quoted speech included text = QUOTED_TEXT_RE.sub("", text).lower() # actions only: speech can't move or examine things room_name = game["room"] room = ROOMS[room_name] room_parse_text = _remove_inventory_mentions(text, game["inventory"]) rooms_named = _find_rooms(room_parse_text) negated = bool(NEGATION_RE.search(text)) spoken_negated = bool(NEGATION_RE.search(spoken))
if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
if game["denial"] >= 3:
return _result(
"blocked",
"The friend is not ready to leave together yet. He has seen the truth "
"but is still resisting it; do not make him agree or move toward the exit.",
)
if len(game.get("final_revelations", [])) < len(FINAL_REVELATIONS):
return _result(
"blocked",
"The friend is not ready to leave together yet. There are still family "
"memories he has not been able to share; do not make him agree or end "
"the story now.",
)
return _end(
game,
"leave_together",
"The player and the friend choose to leave the house together.",
)
if LEAVE_RE.search(text) and not negated:
if room_name == "Entrance Hall":
return _end(game, "escape_alone", "The player walks out of the house alone.")
return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
if not target:
if re.search(r"\bback\b", text) and room_name in BACK:
target = BACK[room_name]
elif re.search(r"\b(up|upstairs)\b", text) and (room_name, "up") in VERTICAL:
target = VERTICAL[(room_name, "up")]
elif re.search(r"\b(down|downstairs)\b", text) and (room_name, "down") in VERTICAL:
target = VERTICAL[(room_name, "down")]
if target:
route = _path_to(game, room_name, lambda r: r == target)
if route:
path, _ = 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["room"] = target
game["room_turns"] = 0
game["nudges_here"] = 0
detail = f"The player moved to the {target}."
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 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 from the {room_name}. "
f"Reachable from here: {', '.join(exit_labels(game, room_name))}.",
)
if room_name in rooms_named:
return _result("no_change", f"The player is already in the {room_name}.")
found = _find_feature(text, room)
if found and _mentions(text, TAKE_VERBS):
name, feat = found
item = feat.get("takeable")
if not item:
return _result("blocked", f"The {name} can't be taken.")
if item in game["inventory"]:
return _result("no_change", f"The player already has the {item}.")
game["inventory"].append(item)
return _result("picked_up", f"The player picked up the {item}.")
if found and (
intent not in {"dialogue", "memory"}
or EXAMINE_RE.search(text)
or _mentions(text, TAKE_VERBS)
):
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}",
new_clue,
friend=feat.get("friend"),
)
if LOOK_AROUND_RE.search(room_parse_text):
nxt = _next_unnoticed(game, room_name)
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, room_name, name, feat)
return _result(
"examined",
f"The player looked around the {room_name}. One thing stands out: {noticed}",
new_clue,
friend=feat.get("friend"),
)
return _result(
"examined",
f"The player looked around the {room_name} again. Nothing new stands out. "
"Describe only what the room description already says, in a fresh way.",
)
return _result(
"no_change",
"Nothing the engine tracks changed. Treat the action as ordinary roleplay "
"(talking, resting, small gestures). Do not add items, clues, or events. "
f"No room change happens: the player is still in the {room_name}, and nobody "
f"leads them elsewhere unless they choose an exit ({', '.join(exit_labels(game, room_name))}).",
)
STAGE_RULES = [ { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 832, 965 ] } 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 @@ -832,134 +832,134 @@ def apply_action(game, raw_action): """Resolve one player action, update game state, return the engine result.""" game.setdefault("friend_room", "Entrance Hall") game.setdefault("sister_seen", False) game.setdefault("last_sister_turn", -99) game.setdefault("final_revelations", []) 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 result = _resolve(game, text) if result["outcome"] == "moved": intent = "movement" elif result["outcome"] == "picked_up": intent = "physical_action" elif result["outcome"] == "examined" and intent not in { "examination", "exploration", }: intent = "examination" result["intent"] = intent if ( intent in {"dialogue", "action_and_dialogue"} and not game["ending"] and game["friend_room"] != game["room"] ): game["friend_room"] = game["room"] result["friend_arrived"] = True result["detail"] += ( f" The friend joins the player in the {game['room']} so they can speak " "face to face." )
game["turn_count"] += 1
game["turns_in_stage"] += 1
if result["outcome"] != "moved":
game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["support_given"] < SUPPORT_LIMIT
and SUPPORT_RE.search(text)
):
game["support_given"] += 1
game["denial"] = max(0, game["denial"] - 1)
result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
if (
not game["ending"]
and game["turn_count"] >= MAX_TURNS
and (not game["truth_known"] or game["denial"] >= 3)
):
result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
if fresh:
game["last_notice_turn"] = game["turn_count"]
if (
not fresh
and result["outcome"] in ("no_change", "examined")
and not game["ending"]
and not game["truth_known"]
and game["room_turns"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_nudge_turn"] >= NUDGE_COOLDOWN
and _next_unnoticed(game, game["room"]) is None
):
nudge = _nudge(game)
if nudge:
result["nudge"] = nudge
game["last_nudge_turn"] = game["turn_count"]
game["nudges_here"] += 1
if result["new_clue"]:
for _ in result["new_clue"].split("; "):
game["denial"] = max(0, game["denial"] - 1)
if (
game["truth_known"]
and game["denial"] < 3
and game["friend_room"] == game["room"]
and intent in {"dialogue", "action_and_dialogue"}
and result["outcome"] not in {"blocked", "ending"}
and len(game["final_revelations"]) < len(FINAL_REVELATIONS)
):
revelation = FINAL_REVELATIONS[len(game["final_revelations"])]
game["final_revelations"].append(revelation["id"])
result["family_memory"] = revelation
result["stage_advanced"] = _advance_stage(game)
sister_due = (
not game["sister_seen"]
or game["turn_count"] - game["last_sister_turn"] >= SISTER_COOLDOWN
)
if game["stage"] >= 3 and sister_due and not game["ending"]:
appearance = (
"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."
if not game["sister_seen"]
else "The friend's sister appears briefly again and quietly repeats her "
"warning to leave. Then she is gone."
)
game["sister_seen"] = True
game["last_sister_turn"] = game["turn_count"]
result["sister"] = appearance
game["last_result"] = result
print("Game state:")
pprint(game, sort_dicts=False)
return result
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 25, 48 ] } 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 @@ -25,24 +25,24 @@
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT brings him into the player's room; narrate that arrival before his reply.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good." "Do you remember how we used to help in the kitchen? I was never much good at it." "It's strange being back in the library. We used to spend whole afternoons in here." "I haven't been sleeping much. The house is loud at night." "Tea sounds good. The kitchen's just this way, if you'd like." "I miss the old days. Now, everything is so... different. I don't know how to explain it." "What have you been doing all these years? I mean... if you want to tell me.""""
STAGES = [ { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 215, 305 ] } 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 @@ -215,91 +215,91 @@ r = game["last_result"] if not r: return "" note = ( "\n\n[ENGINE RESULT - authoritative]\n" f"Player intent: {r.get('intent', 'roleplay')}\n" f"Outcome: {r['outcome']}\n" f"Detail: {r['detail']}" ) if r.get("ambient"): note += ( "\nAmbient detail to weave into this turn in one quiet sentence, " f"without making it a big moment: {r['ambient']}" ) if r.get("nudge"): 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']}" friend_is_here = game.get("friend_room", "Entrance Hall") == 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']}" if r.get("family_memory"): memory = r["family_memory"] note += ( f"\nFamily memory to acknowledge clearly in the friend's reply " f"({memory['title']}): {memory['detail']}" ) if r.get("friend_arrived"): note += "\nThe friend has joined the player to speak face to face; narrate his arrival before his response." intent_reminders = { "dialogue": ( "Treat the player's words as spoken to the friend and answer directly in his natural, " "restrained voice. If the ENGINE RESULT says he joined the player, narrate his arrival " "before the reply." ), "memory": ( "Answer the player's memory question as their character, using only the established " "player memories in CURRENT STATE or facts already established in conversation. " "Do not invent a specific shared event or reveal hidden family history." ), "physical_action": ( "Treat this as a physical action by the player. Acknowledge what they actually did " "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. If the ENGINE RESULT says he joined the player, narrate his " "arrival before the reply. Do not add actions for the player." ), "examination": ( "Treat this as an examination. Use the ENGINE RESULT as the only new observation; " "do not add clues or details." ), "exploration": ( "Treat this as looking around or exploring. Use only the room description and " "ENGINE RESULT for observations; do not add clues or details." ), "movement": "Narrate the confirmed route and destination. Keep the friend at the location given in CURRENT STATE unless the ENGINE RESULT says he accompanied the player.", "roleplay": "Respond naturally to the player's action without inventing consequential events.", } reminder = ( f"{intent_reminders.get(r.get('intent', 'roleplay'), intent_reminders['roleplay'])} " "Stay in character. 2-5 short sentences. Add no other items, clues, uncanny events, " "room changes or backstory." ) if game["ending"]: reminder = "This is the final scene. 4-6 sentences. Close the story." return note + f"\n[REMINDER] {reminder}"
def build_messages(game, canon): system = "\n\n".join([CORE, story_block(canon), stage_block(game), state_block(game)]) live = [dict(m) for m in game["history"][game["summary_idx"]:]] if live and live[-1]["role"] == "user": live[-1]["content"] += _engine_note(game) return [{"role": "system", "content": system}, *live]
def generation_options(game): return { "temperature": 0.6, "num_ctx": 8192, "num_predict": 320 if game["ending"] else 200, "repeat_penalty": 1.1, }
def tidy(text): { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", "view_range": [ 1, 245 ] } 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 @@ -1,245 +1,245 @@ import contextlib import io import json import unittest from pathlib import Path
import engine from prompt import _engine_note, stage_block, 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_supportive_phrases_increase_support(self): phrases = [ "We can do it together.", "We can face this together.", "It's going to be okay.", "It is going to be alright.", "I want to help.", "I can help.", "Take my hand.", "Give me your hand.", ]
for phrase in phrases:
with self.subTest(phrase=phrase):
game = engine.new_game()
act(game, phrase)
self.assertEqual(game["support_given"], 1)
self.assertEqual(game["denial"], 7)
def test_supportive_dialogue_brings_remote_friend_to_player(self):
game = engine.new_game()
game["room"] = "Upper Landing"
game["friend_room"] = "Library"
result = act(game, "I can help.")
self.assertEqual(result["intent"], "dialogue")
self.assertTrue(result["friend_arrived"])
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertEqual(game["support_given"], 1)
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_joins_player_for_conversation_without_keyword_matching(self):
game = engine.new_game()
game["room"] = "Upper Landing"
game["friend_room"] = "Library"
result = act(game, "Can I sit?")
self.assertEqual(result["intent"], "dialogue")
self.assertEqual(game["room"], "Upper Landing")
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertTrue(result["friend_arrived"])
self.assertIn("face to face", result["detail"])
self.assertIn("joined the player", _engine_note(game))
def test_room_name_in_carried_note_does_not_trigger_movement(self):
game = engine.new_game()
game["room"] = "Library"
game["friend_room"] = "Library"
game["inventory"].append("Upper Landing Note")
result = act(
game,
'I stand up, go closer and look into his face. '
'"What is in that room?" I show him the upper landing note I took',
)
self.assertEqual(game["room"], "Library")
self.assertEqual(game["friend_room"], "Library")
self.assertNotEqual(result["outcome"], "moved")
self.assertEqual(result["intent"], "action_and_dialogue")
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"])
self.assertIn(
"I have to keep things as they are",
note_result["detail"],
)
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"])
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)
self.assertIn("his name is not established", prompt)
self.assertIn("do not call him 'E.'", prompt)
def test_denial_persists_after_truth_is_faced_and_support_can_lower_it(self):
game = engine.new_game()
game.update(
stage=5,
door_open=True,
room="Upper Landing",
denial=3,
turn_count=50,
)
result = act(game, "enter parents' bedroom")
self.assertEqual(result["outcome"], "moved")
self.assertTrue(game["truth_known"])
self.assertEqual(game["denial"], 3)
self.assertIn("still resisting it", stage_block(game))
act(game, "We can face this together.")
self.assertEqual(game["denial"], 2)
self.assertIn("share the story gradually", stage_block(game))
def test_unresolved_denial_can_still_trigger_the_denial_ending(self):
game = engine.new_game()
game.update(truth_known=True, denial=3, turn_count=engine.MAX_TURNS - 1)
result = act(game, "I wait quietly.")
self.assertEqual(game["ending"], "denial")
self.assertEqual(result["outcome"], "ending")
def test_fragment_progress_reports_zero_of_three_at_start(self):
game = engine.new_game()
progress = engine.fragment_progress(game)
self.assertEqual(len(engine.fragments_complete(game)), 0)
self.assertEqual(
{kind: item["found"] for kind, item in progress.items()},
{"routine": 0, "rules": 0, "night": 0},
)
self.assertIn("Family memories shared: 0/4", state_block(game))
def test_final_memories_are_shared_in_order_before_together_ending(self):
game = engine.new_game()
game.update(
stage=5,
door_open=True,
room="Parents' Bedroom",
friend_room="Parents' Bedroom",
truth_known=True,
denial=2,
)
blocked = act(game, "Let's leave together.")
self.assertEqual(blocked["outcome"], "blocked")
self.assertIsNone(game["ending"])
self.assertFalse(game["final_revelations"])
for expected in engine.FINAL_REVELATIONS:
result = act(game, "What happened?")
self.assertEqual(result["family_memory"]["id"], expected["id"])
self.assertIn(expected["detail"], _engine_note(game))
self.assertIn(expected["detail"], state_block(game))
ending = act(game, "Let's leave together.")
self.assertEqual(ending["outcome"], "ending")
self.assertEqual(game["ending"], "leave_together")
def test_high_denial_blocks_final_memories_and_together_ending(self):
game = engine.new_game()
game.update(
s
The engine has six stages, but the first five are mostly **narrative permissions**. The door puzzle—not simply reaching a stage—is the main progression gate.
`truth_known`; if denial is low enough, the friend can then disclose four authored family memories, one per conversation.
The stage thresholds are in [STAGE_RULES](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py:810). Finding two clues in each of **Routine**, **Rules**, and **Night** completes the three fragments shown in the sidebar. The bedroom door requires all three fragments and denial at **3 or lower** ([fragments_complete](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py:365), [_try_door](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py:603)).
The together ending is currently a *decision trigger*, not a literal exit journey: the player doesn’t have to return to the Entrance Hall, and the engine doesn’t require the friend to speak an explicit “yes.” The ending prompt narrates them leaving.
Your idea fits the story’s conflict: the friend tries to face what happened, while his sister believes keeping the family preserved is a way of keeping them alive. An optional ending where the player accepts her offer and leaves alone would give that belief a meaningful alternative, while revealing only her partial understanding.
That path **isn’t implemented yet**. Right now, the engine tracks only whether she has appeared, and she repeats her warning after a cooldown; it doesn’t count two warnings, make her available for conversation, or track her perspective being revealed. Also, dialogue normally brings the friend into the player’s room if he’s elsewhere, so the sister conversation would need a specific exception ([`sister_seen` and appearance handling](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py:319), [dialogue arrival logic](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py:841)). The canon currently labels the sister’s bargain ending optional and unimplemented ([ending directions](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/story/canon.json:190)).
The unlock you described is specific enough to implement: **after two warning appearances, when the cooldown passes and the friend isn’t with the player, she appears again as a conversation opportunity instead of giving another warning.** The player can ask why she wants them to leave; she explains that preserving the family is, to her, keeping them alive. Accepting her bargain must be explicit, and the ending can state that the player leaves alone with this partial truth.
One thing to decide for the story’s clarity: should her conversation and bargain be possible **before** the player enters the bedroom, or only after? Before entry makes it an alternate path that avoids the full truth; after entry makes it a competing response to seeing the family. Either is consistent with the canon, but the first makes “partial truth” especially clear.
actually both might be okay? they both give the story a compelling different perspective. If the conditions are met before the door is open its okay, and if the conditions are met after the bedroom is open it is also okay, however, the other condition should still be met, that the player should be alone and friend elsewhere
*** Begin Patch *** Update File: the-house-that-remembers/engine.py @@ FINAL_REVELATIONS = ( @@ )
+SISTER_PRESERVATION_MEMORY = (
- "She believes the family is still together as long as the hou...
The engine and prompt changes applied, but the canon update didn’t match the current wording, so I’m checking the exact result before continuing. I’ll also verify the new encounter state and acceptance path with focused tests.
{"pattern":"SISTER_PRESERVATION|sister_warning_count|sister_bargain|speaking_with_sister|SISTER_ACCEPT_RE|sister_private_encounter|sister_perspective","paths":["C:\Users\lucia\Documents\coding stu... [grep content: 25 matches across 2 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
prompt.py (4 match(es)): 199: f"Sister's private conversation available: {'yes' if game.get('sister_bargain_available') else 'no'}", 200: f"Sister's preservation belief heard: {'yes' if game.get('sister_bargain_heard') else 'no'}", 253: if r.get("sister_perspective"): 256: f"{r['sister_perspective']}"
engine.py (21 match(es)): 71: SISTER_PRESERVATION_MEMORY = ( 342: "sister_warning_count": 0, 343: "sister_bargain_available": False, 344: "sister_bargain_heard": False, 411: SISTER_ACCEPT_RE = re.compile( 685: game.get("sister_bargain_available") 686: and game.get("sister_bargain_heard") 689: and SISTER_ACCEPT_RE.search(spoken) 868: game.setdefault("sister_warning_count", 0) 869: game.setdefault("sister_bargain_available", False) 870: game.setdefault("sister_bargain_heard", False) 874: speaking_with_sister = ( 875: game["sister_bargain_available"] 896: and not speaking_with_sister 970: if speaking_with_sister and result["outcome"] not in {"ending", "blocked"}: 973: game["sister_bargain_heard"] = True 974: result["sister_perspective"] = SISTER_PRESERVATION_MEMORY 995: game["sister_warning_count"] >= 2 1004: game["sister_bargain_available"] = True 1014: game["sister_warning_count"] = min(2, game["sister_warning_count"] + 1) 1018: result["sister_private_encounter"] = can_speak_privately { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 188, 215 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -188,28 +188,28 @@ ] }, "ending_directions": [ { "id": "leave_together", "status": "IMPLEMENTED", "description": "True good ending. The player assembles the fragments, opens the door, the friend faces what happened, and they leave the house together.", "engine_condition": "Bedroom door opened, truth known, denial below 3, all four authored family memories shared, and player chooses to leave with the friend." }, { "id": "escape_alone", "status": "IMPLEMENTED", "description": "The player leaves the house alone and the friend stays behind, usually without ever learning what happened.", "engine_condition": "Player leaves from the Entrance Hall." }, { "id": "denial", "status": "IMPLEMENTED", "description": "Bad ending. The friend is too deep in denial, refuses help, and the house slowly consumes him.", "engine_condition": "Player confronts him bluntly too many times before finding the evidence, or too many turns pass without the truth being faced." }, { "id": "sisters_bargain", "status": "OPTIONAL_NOT_IMPLEMENTED", "description": "The player accepts the sister's offer to keep the illusion alive. The morality is left ambiguous." } ], "narrator_contract": { { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 330, 355 ] } 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 @@ -330,26 +330,26 @@ "inventory": ["The Letter"], "clues": [], "visited": ["Entrance Hall"], "examined": {}, # "Room:feature" -> times examined "stage": 0, # index into prompt.STAGES "turns_in_stage": 0, "turn_count": 0, "room_turns": 0, # turns spent in the current room since arriving "last_notice_turn": -99, # turn_count when a detail was last surfaced "last_nudge_turn": -99, # turn_count when the friend was last nudged "nudges_here": 0, # nudges in this room since last move (escalate) "last_sister_turn": -99, "sister_warning_count": 0, "sister_bargain_available": False, "sister_bargain_heard": False, "denial": 8, # friend's denial (high = deflecting) "pushed": 0, "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 "final_revelations": [], # authored family memories shared by the friend "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"}], "summary": "", # rolling summary of folded-away history { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 410, 465 ] } 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 @@ -410,56 +410,56 @@ ) SISTER_ACCEPT_RE = re.compile( r"\b(?:i accept|i agree|i choose|i'll do it|i will do it|" r"let them stay|keep them as they are|preserve them)\b" ) SISTER_PERSPECTIVE_RE = re.compile( r"\b(why|what|believe|protect|preserv|keep|family|alive|leave)\b" )
LOOK_AROUND_RE = re.compile( r"\b(look|glance|scan|survey|search|explore|examine|inspect|study)\w*\b.\b(around|about|room|surroundings|area)\b" r"|\b(examine|inspect|search|study|explore|look)\w\b.\b(kitchen|library|hall|staircase|landing|bedroom)\b" r"|\bwhat (do|can) i (see|notice|find)\b|\bnotice anything\b" ) QUOTED_TEXT_RE = re.compile(r'"[^"]"|“[^”]”|‘[^’]’') MEMORY_RE = re.compile( r"\b(remember|recall|used to|when we were (?:kids|children)|" r"back when we|childhood)\b" ) EXAMINE_RE = re.compile( r"\b(examine|inspect|study|look at|check|read|listen to|smell|touch)\b" ) PHYSICAL_ACTION_RE = re.compile( r"\b(?:i\s+)?(?:pat|touch|sit|stand|reach|hold|hug|tap|point|" r"turn|open|close|pick|take|walk|step|move|climb|enter|follow)\b" ) SPEECH_RE = re.compile( r"\b(?:i\s+)?(?:say|ask|tell|reply|answer|whisper|speak|call)\b" r"|\b(?:ask|tell) (?:him|her|your friend)\b" ) DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b") DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b") FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b") SUPPORT_RE = re.compile( r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b" r"|\bit'?s (ok|okay|alright)\b" r"|\b(?:we can (?:do (?:it|this)|face (?:it|this)|get through (?:it|this))" r"|we(?:'ll| will) face (?:it|this)) together\b" r"|\b(?:it'?s|it is) going to be (?:ok|okay|alright)\b" r"|\b(?:i want to help|i can help|let me help|i'?d like to help)\b" r"|\b(?:take my hand|give me your hand)\b" )
def _mentions(text, words): return any(re.search(rf"\b{re.escape(w)}\b", text) for w in words)
def _find_rooms(text): return [name for name, r in ROOMS.items() if _mentions(text, r["aliases"])]
def _remove_inventory_mentions(text, inventory): """Keep room names inside carried item names from becoming movement targets.""" { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 900, 970 ] } 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 @@ -900,71 +900,71 @@ result["detail"] += ( f" The friend joins the player in the {game['room']} so they can speak " "face to face." )
game["turn_count"] += 1
game["turns_in_stage"] += 1
if result["outcome"] != "moved":
game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["support_given"] < SUPPORT_LIMIT
and SUPPORT_RE.search(text)
):
game["support_given"] += 1
game["denial"] = max(0, game["denial"] - 1)
result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
if (
not game["ending"]
and game["turn_count"] >= MAX_TURNS
and (not game["truth_known"] or game["denial"] >= 3)
):
result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
if fresh:
game["last_notice_turn"] = game["turn_count"]
if (
not fresh
and result["outcome"] in ("no_change", "examined")
and not game["ending"]
and not game["truth_known"]
and game["room_turns"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_nudge_turn"] >= NUDGE_COOLDOWN
and _next_unnoticed(game, game["room"]) is None
):
nudge = _nudge(game)
if nudge:
result["nudge"] = nudge
game["last_nudge_turn"] = game["turn_count"]
game["nudges_here"] += 1
if result["new_clue"]:
for _ in result["new_clue"].split("; "):
game["denial"] = max(0, game["denial"] - 1)
if speaking_with_sister and result["outcome"] not in {"ending", "blocked"}:
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 60, 82 ] } 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 @@ -60,23 +60,23 @@ "id": "friend", "title": "Why he stayed silent", "detail": ( "He had argued with his father and believed the others were overreacting. " "He knocked once, heard no answer, and obeyed the last order out of anger " "and fear. Since then he has pretended they were still alive and resting, " "unable to face that he never opened the door." ), }, )
SISTER_PRESERVATION_MEMORY = ( "She believes the family is still together as long as the house preserves them " "exactly as they were. To her, keeping things unchanged is a way of keeping them " "alive; she warns the player away because she fears that facing the truth will " "make her lose them." )
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 665, 710 ] } 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 @@ -665,46 +665,46 @@ return _result( "no_change", "The player bluntly says the friend's family is dead before having any evidence. " "The friend flatly denies it, hurt and frightened, and changes the subject." + warn + " Nothing in the house changes.", )
def _resolve(game, text): intent = _classify_intent(text, game) spoken = text # full text, quoted speech included text = QUOTED_TEXT_RE.sub("", text).lower() # actions only: speech can't move or examine things room_name = game["room"] room = ROOMS[room_name] room_parse_text = _remove_inventory_mentions(text, game["inventory"]) rooms_named = _find_rooms(room_parse_text) negated = bool(NEGATION_RE.search(text)) spoken_negated = bool(NEGATION_RE.search(spoken))
if (
game.get("sister_bargain_available")
and game.get("sister_bargain_heard")
and game["friend_room"] != game["room"]
and not spoken_negated
and SISTER_ACCEPT_RE.search(spoken)
):
return _end(
game,
"sisters_bargain",
"The player accepts the sister's bargain and chooses to leave alone, "
"carrying her partial account of the family's fate.",
)
if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
if game["denial"] >= 3:
return _result(
"blocked",
"The friend is not ready to leave together yet. He has seen the truth "
"but is still resisting it; do not make him agree or move toward the exit.",
)
if len(game.get("final_revelations", [])) < len(FINAL_REVELATIONS):
return _result(
"blocked",
"The friend is not ready to leave together yet. There are still family "
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 960, 1030 ] } 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 @@ -960,71 +960,71 @@ nudge = _nudge(game) if nudge: result["nudge"] = nudge game["last_nudge_turn"] = game["turn_count"] game["nudges_here"] += 1
if result["new_clue"]:
for _ in result["new_clue"].split("; "):
game["denial"] = max(0, game["denial"] - 1)
if speaking_with_sister and result["outcome"] not in {"ending", "blocked"}:
result["sister_conversation"] = True
if SISTER_PERSPECTIVE_RE.search(raw_action.lower()):
game["sister_bargain_heard"] = True
result["sister_perspective"] = SISTER_PRESERVATION_MEMORY
if (
game["truth_known"]
and game["denial"] < 3
and game["friend_room"] == game["room"]
and intent in {"dialogue", "action_and_dialogue"}
and result["outcome"] not in {"blocked", "ending"}
and len(game["final_revelations"]) < len(FINAL_REVELATIONS)
):
revelation = FINAL_REVELATIONS[len(game["final_revelations"])]
game["final_revelations"].append(revelation["id"])
result["family_memory"] = revelation
result["stage_advanced"] = _advance_stage(game)
sister_due = (
not game["sister_seen"]
or game["turn_count"] - game["last_sister_turn"] >= SISTER_COOLDOWN
)
if game["stage"] >= 3 and sister_due and not game["ending"]:
can_speak_privately = (
game["sister_warning_count"] >= 2
and game["friend_room"] != game["room"]
)
if can_speak_privately:
appearance = (
"The sister appears again, but this time she does not repeat her "
"warning. With the friend elsewhere, she looks directly at the player "
"and seems willing to explain why she wants the family left undisturbed."
)
game["sister_bargain_available"] = True
else:
appearance = (
"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."
if not game["sister_seen"]
else "The friend's sister appears briefly again and quietly repeats her "
"warning to leave. Then she is gone."
)
game["sister_warning_count"] = min(2, game["sister_warning_count"] + 1)
game["sister_seen"] = True
game["last_sister_turn"] = game["turn_count"]
result["sister"] = appearance
result["sister_private_encounter"] = can_speak_privately
game["last_result"] = result
print("Game state:")
pprint(game, sort_dicts=False)
return result
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 70, 110 ] } 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 @@ -70,41 +70,41 @@ "now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.", "friend": "He flinches at the knocking and tells the player, "Whatever you do, don't answer it." Fragments of memory slip out, then he retreats.", "steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.", }, { # 5 - Reconstructed truth "now": "The bedroom door has opened. The family inside are preserved as if asleep. The player faces the truth on entering; the friend can only share the family story as the engine authorizes each memory.", "friend": "His denial breaks only as he becomes ready. He speaks in halting fragments and quiet realization, not speeches. If denial is still 3 or higher, he resists and cannot share the next family memory. When denial falls below 3, the ENGINE RESULT may supply one authored family memory; include it honestly and clearly, without adding facts. He does not blame the player. He never agrees to leave together unless the engine ends the story.", "steer": "If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, the friend shares the mother's decline, the father's choice, the sister's hope, and his own silence one memory at a time when the ENGINE RESULT supplies each. Let the moment breathe between disclosures. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.", }, ]
ENDINGS = { "leave_together": ( "The player and the friend have faced the truth and choose to leave together. " "Write the final scene in 4-6 sentences: the friend's grief and fragile steadiness, the house letting go " "(the stopped clocks may tick once; the knocking and humming fall silent), the door, the rain easing. " "No gore. Offer no further choices. End the story." ), "escape_alone": ( "The player has chosen to leave the house alone, and the engine confirms it. " "Write the final scene in 4-6 sentences: the door, the rain, the friend's reaction according to his condition, " "the house behind them. Do not explain what happened to the family. Offer no further choices. End the story." ), "denial": ( "The friend could not face it. He refuses help and withdraws, insisting everyone is only resting. " "Write the final scene in 4-6 sentences: the player cannot reach him, the house settles around him, " "the knocking and humming resume as though nothing happened, and he is left with the house. " "Do not describe the family or explain what happened. Offer no further choices. End the story." ), "sisters_bargain": ( "The player accepts the sister's bargain and leaves alone. The sister has revealed only her belief " "that preserving the family exactly as they are keeps them alive; the player has not heard the friend's " "full account. Write a quiet, unsettling final scene in 4-6 sentences: the player leaves the house " "alone, carrying the sister's partial truth, while the friend and the family remain in the house. " "Do not imply that the family was truly saved or explain more than the sister knows. Offer no further " "choices. End the story." ), }
def denial_band(denial): { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 238, 262 ] } 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 @@ -238,25 +238,25 @@ ) if r.get("nudge"): 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']}" friend_is_here = game.get("friend_room", "Entrance Hall") == 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']}" if r.get("sister_conversation"): note += ( "\nThe player is speaking privately with the sister. Respond as the sister, " "not the friend. The friend is elsewhere and must not answer or arrive." ) if r.get("sister_perspective"): note += ( "\nThe sister's perspective to explain clearly if the player asked: " f"{r['sister_perspective']}" ) if r.get("family_memory"): memory = r["family_memory"] note += ( f"\nFamily memory to acknowledge clearly in the friend's reply " f"({memory['title']}): {memory['detail']}" *** Begin Patch *** Update File: the-house-that-remembers/engine.py @@ if ( game.get("sister_bargain_available")
-
and game.get("sister_bargain_heard") and game["friend_r...
{"pattern":"sister.*appearance|SISTER_ACCEPT_RE|result\["outcome"\] == "moved"|reminder = \(","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:65: "now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:298: reminder = ( C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:411:SISTER_ACCEPT_RE = re.compile( C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:689: and SISTER_ACCEPT_RE.search(spoken) C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:882: if result["outcome"] == "moved": C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:1017: result["sister"] = appearance { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 25, 40 ] } 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 @@ -25,16 +25,16 @@
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT brings him into the player's room; narrate that arrival before his reply.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good." "Do you remember how we used to help in the kitchen? I was never much good at it." { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 100, 118 ] } 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 @@ -100,19 +100,19 @@ "The player accepts the sister's bargain and leaves alone. The sister has revealed only her belief " "that preserving the family exactly as they are keeps them alive; the player has not heard the friend's " "full account. Write a quiet, unsettling final scene in 4-6 sentences: the player leaves the house " "alone, carrying the sister's partial truth, while the friend and the family remain in the house. " "Do not imply that the family was truly saved or explain more than the sister knows. Offer no further " "choices. End the story." ), }
def denial_band(denial): if denial >= 7: return "high denial: he insists things are fine, deflects, changes the subject" if denial >= 3: return "wavering: he s, almost admits things, then retreats" return "cracking: fragments of memory slip out and he is visibly shaken"
def story_block(canon): { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 870, 905 ] } 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 @@ -870,36 +870,36 @@ game.setdefault("sister_bargain_heard", False) game.setdefault("final_revelations", []) text = raw_action.lower() intent = _classify_intent(raw_action, game) # classify before the action can change the room speaking_with_sister = ( game["sister_bargain_available"] and intent in {"dialogue", "action_and_dialogue"} and not game["ending"] and game["friend_room"] != game["room"] ) seen_before = len(game["examined"]) # measured BEFORE resolving, so first looks count as fresh result = _resolve(game, text) if result["outcome"] == "moved": intent = "movement" elif result["outcome"] == "picked_up": intent = "physical_action" elif result["outcome"] == "examined" and intent not in { "examination", "exploration", }: intent = "examination" result["intent"] = intent if ( intent in {"dialogue", "action_and_dialogue"} and not game["ending"] and game["friend_room"] != game["room"] and not speaking_with_sister ): game["friend_room"] = game["room"] result["friend_arrived"] = True result["detail"] += ( f" The friend joins the player in the {game['room']} so they can speak " "face to face." )
game["turn_count"] += 1
*** Begin Patch *** Update File: the-house-that-remembers/engine.py @@ if ( game.get("sister_bargain_available")
-
and game.get("sister_bargain_heard") and game["friend_r...
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 @@ -683,11 +683,16 @@
if (
game.get("sister_bargain_available")
-
):and game.get("sister_bargain_heard") and game["friend_room"] != game["room"] and not spoken_negated and SISTER_ACCEPT_RE.search(spoken)
-
if not game.get("sister_bargain_heard"): -
return _result( -
"blocked", -
"The sister has not explained her bargain yet. The player cannot " -
"accept it before hearing what she believes.", -
) return _end( game, "sisters_bargain",
@@ -881,6 +886,7 @@ result = _resolve(game, text) if result["outcome"] == "moved": intent = "movement"
-
elif result["outcome"] == "picked_up": intent = "physical_action" elif result["outcome"] == "examined" and intent not in { *** Begin Patch *** Update File: the-house-that-remembers/prompt.py @@game["sister_bargain_available"] = False
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or noth... 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 @@ -29,10 +29,11 @@ Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion.
- A character may suggest going somewhere, but the move only happens if the player chooses it. -- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT brings him into the player's room; narrate that arrival before his reply. +- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT normally brings him into the player's room; narrate that arrival before his reply. The exception is an ENGINE RESULT-marked private sister conversation: answer as the sister and keep the friend elsewhere.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech. +- When the ENGINE RESULT authorizes the sister's private conversation, she may explain that she believes preserving the family keeps them alive. This is her perspective, not objective proof that the family is alive, and does not reveal the full truth.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
@@ -64,7 +65,7 @@ { # 3 - Sister's warnings "now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.", "friend": "He does not see her, or insists the player imagined it.",
-
"steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most, then gone. She speaks gently and urgently. She never threatens. Do not say what happened to the friend's family.",
-
}, { # 4 - Parents' fragments "now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.", @@ -97,11 +98,11 @@ "Do not describe the family or explain what happened. Offer no further choices. End the story." ), "sisters_bargain": ("steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most. She speaks gently and urgently. She never threatens. If the ENGINE RESULT authorizes a private conversation, she may explain that she believes preserving the family keeps them alive; present this as her belief, not as confirmed fact. Otherwise do not say what happened to the friend's family.",
-
"The player accepts the sister's bargain and leaves alone. The sister has revealed only her belief " -
"that preserving the family exactly as they are keeps them alive; the player has not heard the friend's " -
"full account. Write a quiet, unsettling final scene in 4-6 sentences: the player leaves the house " -
"alone, carrying the sister's partial truth, while the friend and the family remain in the house. " -
"Do not imply that the family was truly saved or explain more than the sister knows. Offer no further "
-
"The player accepts the sister's bargain and leaves alone. The sister's account is her belief that " -
"preserving the family exactly as they are keeps them alive, not objective confirmation. Write a quiet, " -
"unsettling final scene in 4-6 sentences: the player leaves the house alone, carrying the sister's " -
"perspective, while the friend and the family remain in the house. Do not erase or contradict anything " -
), } @@ -136,6 +137,12 @@ def stage_block(game): if game["ending"]: text = ENDINGS[game["ending"]]"the player already learned or saw, and do not imply that the family was truly saved. Offer no further " "choices. End the story." -
if game["ending"] == "sisters_bargain": -
text += ( -
" The player has already entered the bedroom and seen the family." -
if game.get("truth_known") -
else " The player accepted before entering the bedroom and has not seen the family." -
) if game["ending"] == "escape_alone": text += ( " The player saw the truth in the bedroom before leaving; the friend stays behind with his family."
@@ -198,6 +205,8 @@ f"Shared family memories: {', '.join(game.get('final_revelations', [])) or 'none'}", f"Sister's private conversation available: {'yes' if game.get('sister_bargain_available') else 'no'}", f"Sister's preservation belief heard: {'yes' if game.get('sister_bargain_heard') else 'no'}",
-
f"Sister's private conversation available: {'yes' if game.get('sister_bargain_available') else 'no'}", -
f"Sister's preservation belief heard: {'yes' if game.get('sister_bargain_heard') else 'no'}", f"Exits: {', '.join(exit_labels(game, game['room']))}", ( "Friend location: with the player in this room."
@@ -245,6 +254,12 @@ note += f"\nFriend's likely reaction (optional): {r['friend']}" if r.get("sister"): note += f"\nSister's appearance to narrate this turn: {r['sister']}"
- if r.get("sister_private_encounter"):
-
note += ( -
"\nThis is the sister's private conversation opportunity, available because " -
"she has already warned the player twice and the friend is elsewhere. " -
"The player may speak with her even if the bedroom door is still closed." -
if r.get("sister_conversation"): note += ( "\nThe player is speaking privately with the sister. Respond as the sister, " @@ -300,6 +315,12 @@ "Stay in character. 2-5 short sentences. Add no other items, clues, uncanny events, " "room changes or backstory." )) - if r.get("sister_conversation"):
-
reminder = ( -
"Answer as the sister, directly to the player. The friend is elsewhere; do not " -
"bring him into the room or let him answer. Stay within the sister's established " -
"beliefs and the engine-authorized perspective." -
if game["ending"]: reminder = "This is the final scene. 4-6 sentences. Close the story." return note + f"\n[REMINDER] {reminder}" *** Begin Patch *** Update File: the-house-that-remembers/story/canon.json @@ { "id": "sisters_bargain",)
-
"status": "OPTIONAL_NOT_IMPLEMENTED", -
"description": "The player acce...
*** Begin Patch *** Update File: the-house-that-remembers/story/canon.json @@ { "id": "sisters_bargain",
-
"status": "OPTIONAL_NOT_IMPLEMENTED", -
"description": "The player acce...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -208,8 +208,8 @@ }, { "id": "sisters_bargain",
-
"status": "OPTIONAL_NOT_IMPLEMENTED", -
"description": "The player accepts the sister's offer to keep the illusion alive. The morality is left ambiguous."
-
"status": "IMPLEMENTED", -
"description": "After at least two warning appearances, the sister privately explains that preserving the family is how she believes she keeps them alive. If the friend is elsewhere and the player explicitly accepts her bargain, the player leaves alone with this partial truth. This path is available whether or not the bedroom door has been opened."} ], "narrator_contract": { @@ -260,6 +260,7 @@ "door": "The bedroom door opens only if all three fragments are complete and denial is at or below 3. Opening it moves the story to the final stage.", "family_revelations": "After the player enters the bedroom, face-to-face conversation shares four authored family memories one at a time when denial is below 3: the mother's decline and the family's deaths, the father's attempt to preserve them, the sister's hope, and the friend's conflict and silence.", "together_ending": "The friend can leave together only after denial is below 3 and all four family memories have been shared.",
-
"sister_bargain": "After the sister has warned the player at least twice, when the cooldown passes and the friend is elsewhere, she may privately explain her belief that preserving the family keeps them alive. This conversation and ending may happen before or after the bedroom is opened. The ending requires the player to hear her perspective and explicitly accept her bargain; the player leaves alone with her partial truth.", "pushing": "Three blunt confrontations before the evidence ends the story in the denial ending.", "clock": "If the truth has not been faced after about 90 turns, the denial ending triggers." } *** Begin Patch *** Update File: the-house-that-remembers/tests/test_engine.py @@ def test_sister_appears_once_when_stage_three_is_active(self): @@ self.assertIn("again", third["sister"]... 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 @@ -134,6 +134,92 @@ self.assertIn("sister", third) self.assertIn("again", third["sister"])
-
def test_sister_private_conversation_requires_two_warnings_and_friend_absent(self):
-
game = engine.new_game() -
game.update( -
stage=3, -
room="Kitchen", -
friend_room="Library", -
sister_seen=True, -
sister_warning_count=2, -
last_sister_turn=0, -
turn_count=engine.SISTER_COOLDOWN - 1, -
) -
encounter = act(game, "look around") -
self.assertTrue(encounter["sister_private_encounter"]) -
self.assertTrue(game["sister_bargain_available"]) -
self.assertIn("willing to explain", encounter["sister"]) -
self.assertNotEqual(game["room"], game["friend_room"]) -
reply = act(game, "What do you believe?") -
self.assertTrue(reply["sister_conversation"]) -
self.assertTrue(game["sister_bargain_heard"]) -
self.assertIn("preserves them", reply["sister_perspective"]) -
self.assertNotIn("friend_arrived", reply) -
self.assertEqual(game["friend_room"], "Library") -
def test_sister_bargain_needs_explanation_and_explicit_acceptance(self):
-
game = engine.new_game() -
game.update( -
stage=3, -
room="Kitchen", -
friend_room="Library", -
sister_seen=True, -
sister_warning_count=2, -
sister_bargain_available=True, -
) -
premature = act(game, "I accept your bargain.") -
self.assertEqual(premature["outcome"], "blocked") -
self.assertIsNone(game["ending"]) -
act(game, "Why do you want to keep them as they are?") -
result = act(game, "I accept your bargain.") -
self.assertEqual(result["outcome"], "ending") -
self.assertEqual(game["ending"], "sisters_bargain") -
def test_sister_bargain_is_available_before_or_after_entering_bedroom(self):
-
for truth_known in (False, True): -
with self.subTest(truth_known=truth_known): -
game = engine.new_game() -
game.update( -
stage=3, -
room="Kitchen", -
friend_room="Library", -
sister_seen=True, -
sister_warning_count=2, -
sister_bargain_available=True, -
sister_bargain_heard=True, -
truth_known=truth_known, -
) -
result = act(game, "I agree to the bargain.") -
self.assertEqual(result["outcome"], "ending") -
self.assertEqual(game["ending"], "sisters_bargain") -
def test_sister_does_not_offer_private_conversation_when_friend_is_present(self):
-
game = engine.new_game() -
game.update( -
stage=3, -
room="Kitchen", -
friend_room="Kitchen", -
sister_seen=True, -
sister_warning_count=2, -
last_sister_turn=0, -
turn_count=engine.SISTER_COOLDOWN - 1, -
) -
result = act(game, "look around") -
self.assertFalse(result["sister_private_encounter"]) -
self.assertFalse(game["sister_bargain_available"]) -
self.assertIn("warning", result["sister"]) -
def test_remote_friend_reaction_is_not_included_in_prompt(self): game = engine.new_game() game["room"] = "Upper Landing" *** Begin Patch *** Update File: the-house-that-remembers/README.md @@ After entering, the friend shares four family memories gradually; the two of you can leave together once he is ready and has fa... *** Begin Patch *** Update File: README.md @@ After entering, the friend shares four family memories gradually; the two of you can leave together once he is ready and has faced the full story.
+Af... 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 @@ -55,6 +55,11 @@ After entering, the friend shares four family memories gradually; the two of you can leave together once he is ready and has faced the full story.
+After two sister-ghost warnings, she may offer a private conversation when your
+friend is elsewhere. You can hear her belief that preserving the family keeps
+them alive, then explicitly accept her bargain to leave alone with that partial
+truth. This optional path is possible before or after entering the bedroom.
+
Select 📜 The Letter 🔍, 📖 Library Book 🔍, or
📝 Upper Landing Note 🔍 in the inventory to view its image in a dismissible
dialog. Add the images at the-house-that-remembers/assets/letter.png,
{"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run sister bargain r...
FAIL: test_sister_bargain_needs_explanation_and_explicit_acceptance (test_engine.GameEngineTests.test_sister_bargain_needs_explanation_and_explicit_acceptance) #
Traceback (most recent call last): File "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", line 182, in test_sister_bargain_needs_explanation_and_explicit_acceptance self.assertEqual(result["outcome"], "ending") ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: 'blocked' != 'ending'
- blocked
- ending
Ran 18 tests in 0.008s
FAILED (failures=1) <shellId: 69 completed with exit code 1> {"command":"python -m py_compile 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py' 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remem... Canon JSON is valid <shellId: 70 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 78, 132 ] } 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 @@ -78,55 +78,55 @@ "steer": "If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, the friend shares the mother's decline, the father's choice, the sister's hope, and his own silence one memory at a time when the ENGINE RESULT supplies each. Let the moment breathe between disclosures. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.", }, ]
ENDINGS = { "leave_together": ( "The player and the friend have faced the truth and choose to leave together. " "Write the final scene in 4-6 sentences: the friend's grief and fragile steadiness, the house letting go " "(the stopped clocks may tick once; the knocking and humming fall silent), the door, the rain easing. " "No gore. Offer no further choices. End the story." ), "escape_alone": ( "The player has chosen to leave the house alone, and the engine confirms it. " "Write the final scene in 4-6 sentences: the door, the rain, the friend's reaction according to his condition, " "the house behind them. Do not explain what happened to the family. Offer no further choices. End the story." ), "denial": ( "The friend could not face it. He refuses help and withdraws, insisting everyone is only resting. " "Write the final scene in 4-6 sentences: the player cannot reach him, the house settles around him, " "the knocking and humming resume as though nothing happened, and he is left with the house. " "Do not describe the family or explain what happened. Offer no further choices. End the story." ), "sisters_bargain": ( "The player accepts the sister's bargain and leaves alone. The sister's account is her belief that " "preserving the family exactly as they are keeps them alive, not objective confirmation. Write a quiet, " "unsettling final scene in 4-6 sentences: the player leaves the house alone, carrying the sister's " "perspective, while the friend and the family remain in the house. Do not erase or contradict anything " "the player already learned or saw, and do not imply that the family was truly saved. Offer no further " "choices. End the story." ), }
def denial_band(denial): if denial >= 7: return "high denial: he insists things are fine, deflects, changes the subject" if denial >= 3: return "wavering: he s, almost admits things, then retreats" return "cracking: fragments of memory slip out and he is visibly shaken"
def story_block(canon): f = canon["character_dynamics"]["friend"] return ( "STORY PREMISE\n" f"{canon['premise']['summary']}\n\n" "THE FRIEND (his name is not established; the letter is signed with the initial 'E.')\n" "- Refer to him as 'your friend' or 'he'; do not call him 'E.' as though that were his name.\n" f"- Condition: {f['initial_state']}\n" f"- Personality: {f['personality']}\n" f"- Voice: {f['voice']}\n" f"- Early behavior: {f['early_behavior']}\n" "- Example lines (for tone only; do not repeat them verbatim):\n" f"{FRIEND_EXAMPLES}\n\n" "THE LETTER HE SENT\n" { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 145, 220 ] } 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 @@ -145,76 +145,76 @@ ) if game["ending"] == "escape_alone": text += ( " The player saw the truth in the bedroom before leaving; the friend stays behind with his family." if game["truth_known"] else " The player never learned what was upstairs; the friend stays behind in the house." ) return "ENDING\n" + text s = STAGES[game["stage"]] truth_permission = "" if game["stage"] == 5: from engine import FINAL_REVELATIONS
if not game.get("truth_known"):
truth_permission = (
"\nTRUTH STATUS: The bedroom door is open, but the player has not entered "
"and has not faced the truth. Do not reveal the family's fate yet."
)
elif game["denial"] >= 3:
truth_permission = (
"\nTRUTH STATUS: The player has seen the truth, but the friend is still "
"resisting it. He may deny or minimize it and cannot yet give a clear account. "
"Do not force acceptance; let sincere support soften him. If his denial remains "
"unresolved, the story can end with him unable to face it."
)
elif len(game.get("final_revelations", [])) < len(FINAL_REVELATIONS):
truth_permission = (
"\nTRUTH STATUS: The player has seen the family. The friend is ready to "
"share the story gradually, but has only disclosed the memories recorded "
"in CURRENT STATE so far. Do not reveal any later family memory unless "
"the ENGINE RESULT supplies it this turn."
)
else:
truth_permission = (
"\nTRUTH STATUS: The player has seen the truth and the friend has shared "
"all four family memories recorded in CURRENT STATE. He can now speak "
"honestly about what happened. Keep his voice gentle and halting, not a speech."
)
return (
f"STORY NOW: {s['now']}\n"
f"FRIEND RIGHT NOW: {s['friend']}\n"
f"FRIEND'S CONDITION: {denial_band(game['denial'])}\n"
f"STEER TOWARD (gently, never force): {s['steer']}\n"
f"{truth_permission}\n"
f"Turns spent in this part of the story: {game['turns_in_stage']}"
)
def state_block(game): from engine import FINAL_REVELATIONS, ROOMS, exit_labels
room = ROOMS[game["room"]]
friend_room = game.get("friend_room", "Entrance Hall")
lines = [
"CURRENT STATE",
f"Room: {game['room']}",
f"Room description: {room['description'].strip()}",
f"Truth faced: {'yes' if game.get('truth_known') else 'no'}",
f"Friend's denial: {game['denial']}/10",
f"Family memories shared: {len(game.get('final_revelations', []))}/4",
f"Shared family memories: {', '.join(game.get('final_revelations', [])) or 'none'}",
f"Sister's private conversation available: {'yes' if game.get('sister_bargain_available') else 'no'}",
f"Sister's preservation belief heard: {'yes' if game.get('sister_bargain_heard') else 'no'}",
f"Sister's private conversation available: {'yes' if game.get('sister_bargain_available') else 'no'}",
f"Sister's preservation belief heard: {'yes' if game.get('sister_bargain_heard') else 'no'}",
f"Exits: {', '.join(exit_labels(game, game['room']))}",
(
"Friend location: with the player in this room."
if friend_room == game["room"]
else f"Friend location: {friend_room}. He is not with the player and cannot hear them."
),
f"Inventory: {', '.join(game['inventory']) or 'empty'}",
f"Clues the player has found: {', '.join(game['clues']) or 'none'}",
"Established player memories: childhood companions; playing outside together; "
"helping the friend with kitchen chores; exploring the family library; "
"sleeping over in the guest room.",
{ "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check; git status --short;", "description": "Check diff and current changes" } warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it M .gitignore M README.md M the-house-that-remembers/app.py M the-house-that-remembers/engine.py M the-house-that-remembers/prompt.py M the-house-that-remembers/story/canon.json ?? the-house-that-remembers/assets/book.png ?? the-house-that-remembers/assets/letter.png ?? the-house-that-remembers/assets/note.png <shellId: 71 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; @'\nimport contextlib, io\nimport engine\n\ng = engine.new_game()\ng.update(stage=3, room='Ki... js I accept your bargain. => blocked True None available= True heard= False ending= None turn= 1 Why do you want to keep them as they are? => blocked None None available= True heard= False ending= None turn= 2 I accept your bargain. => blocked None None available= True heard= False ending= None turn= 3 I agree to the bargain. => blocked None None available= True heard= False ending= None turn= 4 accept regex: False <shellId: 72 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 401, 421 ] } 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 @@ -401,21 +401,21 @@ LEAVE_RE = re.compile( r"\b(leave|exit|flee|escape)\b.\b(house|mansion|manor)\b" r"|\bgo outside\b|\bwalk out\b" ) NEGATION_RE = re.compile(r"\b(not|never|without)\b|n't\b|\bwont\b|\bdont\b") TOGETHER_RE = re.compile( r"\b(leave|go|walk|get out|come|take him|lead him)\b.\b(together|with me|with him)\b" r"|\b(together|with me)\b.*\b(leave|go|out)\b" r"|\blet'?s (leave|get out)\b" ) SISTER_ACCEPT_RE = re.compile( r"\b(?:i accept|i agree|i choose|i'll do it|i will do it|" r"let them stay|keep them as they are|preserve them)\b" ) SISTER_PERSPECTIVE_RE = re.compile( r"\b(why|what|believe|protect|preserv|keep|family|alive|leave)\b" )
LOOK_AROUND_RE = re.compile( r"\b(look|glance|scan|survey|search|explore|examine|inspect|study)\w*\b.*\b(around|about|room|surroundings|area)\b" {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -c "import engine; print(repr(engine.SISTER_ACCEPT_RE.pattern)); print(engine.SISTER_... js "\b(?:i accept|i agree|i choose|i'll do it|i will do it|let them stay|keep them as they are|preserve them)\b" None None <shellId: 73 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -c "import engine; print(engine.SISTER_ACCEPT_RE.search('i agree to the bargain.')); ... <re.Match object; span=(0, 7), match='i agree'> <re.Match object; span=(0, 8), match='i accept'> <shellId: 74 completed with exit code 0> {"pattern":"already entered the bedroom|sister_private_encounter|private sister conversation|Answer as the sister|Sister's private conversation available","paths":["C:\Users\lucia\Documents\coding... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:1024: result["sister_private_encounter"] = can_speak_privately C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:32:- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT normally brings him into the player's room; narrate that arrival before his reply. The exception is an ENGINE RESULT-marked private sister conversation: answer as the sister and keep the friend elsewhere. C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:142: " The player has already entered the bedroom and seen the family." C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:206: f"Sister's private conversation available: {'yes' if game.get('sister_bargain_available') else 'no'}", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:208: f"Sister's private conversation available: {'yes' if game.get('sister_bargain_available') else 'no'}", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:257: if r.get("sister_private_encounter"): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:320: "Answer as the sister, directly to the player. The friend is elsewhere; do not " *** Begin Patch *** Update File: the-house-that-remembers/engine.py @@ SISTER_ACCEPT_RE = re.compile(
- r"\b(?:i accept|i agree|i choose|i'll do it|i will do it|"
- r"let them stay|keep them as... 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 @@ -409,8 +409,9 @@ r"|\blet'?s (leave|get out)\b" ) SISTER_ACCEPT_RE = re.compile(
- r"\b(?:i accept|i agree|i choose|i'll do it|i will do it|"
- r"let them stay|keep them as they are|preserve them)\b"
- r"\b(?:i accept|i agree|i choose (?:to accept|your offer|the offer|your bargain|the bargain)|"
- r"i'll do it|i will do it|i want them to stay|"
- r"i want to keep them as they are|i want to preserve them)\b" ) SISTER_PERSPECTIVE_RE = re.compile( r"\b(why|what|believe|protect|preserv|keep|family|alive|leave)\b"
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 @@ -205,8 +205,6 @@ f"Shared family memories: {', '.join(game.get('final_revelations', [])) or 'none'}", f"Sister's private conversation available: {'yes' if game.get('sister_bargain_available') else 'no'}", f"Sister's preservation belief heard: {'yes' if game.get('sister_bargain_heard') else 'no'}",
-
f"Sister's private conversation available: {'yes' if game.get('sister_bargain_available') else 'no'}", -
f"Sister's preservation belief heard: {'yes' if game.get('sister_bargain_heard') else 'no'}", f"Exits: {', '.join(exit_labels(game, game['room']))}", ( "Friend location: with the player in this room."
*** Begin Patch *** Update File: the-house-that-remembers/engine.py @@ SISTER_ACCEPT_RE = re.compile(
- r"\b(?:i accept|i agree|i choose (?:to accept|your offer|the offer|your bargain|the bargain)... 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 @@ -409,7 +409,8 @@ r"|\blet'?s (leave|get out)\b" ) SISTER_ACCEPT_RE = re.compile(
- r"\b(?:i accept|i agree|i choose (?:to accept|your offer|the offer|your bargain|the bargain)|"
- r"\b(?:i accept|i agree to (?:your|the) (?:offer|bargain)|"
- r"i choose (?:to accept|your offer|the offer|your bargain|the bargain)|" r"i'll do it|i will do it|i want them to stay|" r"i want to keep them as they are|i want to preserve them)\b" )
The first regression run caught an important ambiguity: asking “Why do you want to keep them as they are?” was accidentally being read as accepting the bargain. I tightened acceptance to explicit agreement phrases and am rerunning the progression tests, including both before- and after-bedroom paths.
{"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run all game regress... .................. #
Ran 18 tests in 0.008s
OK <shellId: 75 completed with exit code 0> {"command":"python -m py_compile 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py' 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remem... Canon JSON is valid <shellId: 76 completed with exit code 0> { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check; git status --short;", "description": "Check diff and worktree" } warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it M .gitignore M README.md M the-house-that-remembers/app.py M the-house-that-remembers/engine.py M the-house-that-remembers/prompt.py M the-house-that-remembers/story/canon.json ?? the-house-that-remembers/assets/book.png ?? the-house-that-remembers/assets/letter.png ?? the-house-that-remembers/assets/note.png <shellId: 77 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 134, 152 ] } 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 @@ -134,19 +134,19 @@ )
def stage_block(game): if game["ending"]: text = ENDINGS[game["ending"]] if game["ending"] == "sisters_bargain": text += ( " The player has already entered the bedroom and seen the family." if game.get("truth_known") else " The player accepted before entering the bedroom and has not seen the family." ) if game["ending"] == "escape_alone": text += ( " The player saw the truth in the bedroom before leaving; the friend stays behind with his family." if game["truth_known"] else " The player never learned what was upstairs; the friend stays behind in the house." ) return "ENDING\n" + text {"pattern":"private conversation|preserving the family keeps them alive|sister's offer|sisters_bargain|OPTIONAL_NOT_IMPLEMENTED|SISTER_PERSPECTIVE_RE","paths":["C:\Users\lucia\Documents\coding stu... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:36:- When the ENGINE RESULT authorizes the sister's private conversation, she may explain that she believes preserving the family keeps them alive. This is her perspective, not objective proof that the family is alive, and does not reveal the full truth. C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:68: "steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most. She speaks gently and urgently. She never threatens. If the ENGINE RESULT authorizes a private conversation, she may explain that she believes preserving the family keeps them alive; present this as her belief, not as confirmed fact. Otherwise do not say what happened to the friend's family.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:100: "sisters_bargain": ( C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:140: if game["ending"] == "sisters_bargain": C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:206: f"Sister's private conversation available: {'yes' if game.get('sister_bargain_available') else 'no'}", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:257: "\nThis is the sister's private conversation opportunity, available because " C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:176: "endings": "3 implemented (leave_together, escape_alone, denial); 1 optional (sisters_bargain)" C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:210: "id": "sisters_bargain", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:243: "Decide whether to implement the optional 'sisters_bargain' ending.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:263: "sister_bargain": "After the sister has warned the player at least twice, when the cooldown passes and the friend is elsewhere, she may privately explain her belief that preserving the family keeps them alive. This conversation and ending may happen before or after the bedroom is opened. The ending requires the player to hear her perspective and explicitly accept her bargain; the player leaves alone with her partial truth.", C:\Users\lucia\Documents\coding stuff\hacktober\README.md:58:After two sister-ghost warnings, she may offer a private conversation when your C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:417:SISTER_PERSPECTIVE_RE = re.compile( C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:700: "sisters_bargain", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:980: if SISTER_PERSPECTIVE_RE.search(raw_action.lower()): *** Begin Patch *** Update File: the-house-that-remembers/engine.py @@ SISTER_PERSPECTIVE_RE = re.compile(
- r"\b(why|what|believe|protect|preserv|keep|family|alive|leave)\b"
- r"\b(why|what|ho... { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 170, 180 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -170,11 +170,11 @@ "design_constraints": { "mvp": { "rooms": "6: Entrance Hall, Library, Kitchen, Grand Staircase, Upper Landing, Parents' Bedroom (locked)", "key_clues": "13 clues in three fragments (routine, rules, night)", "puzzles": "1: the parents' bedroom door", "major_ghosts": 1, "endings": "3 implemented (leave_together, escape_alone, denial); 1 optional (sisters_bargain)" }, "player_agency": [ "The player can investigate, talk, examine objects, rest, follow or refuse warnings, and leave when possible.", "Do not force one moral choice after the truth is discovered.", {"pattern":"sisters_bargain|Decide whether|endings\":","paths":"C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json","output_mode":"content","-n":true,"... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:176: "endings": "3 implemented (leave_together, escape_alone, denial); 1 optional (sisters_bargain)" C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:210: "id": "sisters_bargain", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:243: "Decide whether to implement the optional 'sisters_bargain' ending.", { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 204, 216 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -204,13 +204,13 @@ "id": "denial", "status": "IMPLEMENTED", "description": "Bad ending. The friend is too deep in denial, refuses help, and the house slowly consumes him.", "engine_condition": "Player confronts him bluntly too many times before finding the evidence, or too many turns pass without the truth being faced." }, { "id": "sisters_bargain", "status": "IMPLEMENTED", "description": "After at least two warning appearances, the sister privately explains that preserving the family is how she believes she keeps them alive. If the friend is elsewhere and the player explicitly accepts her bargain, the player leaves alone with this partial truth. This path is available whether or not the bedroom door has been opened." } ], "narrator_contract": { "role": "Write immersive, atmospheric prose and character dialogue in response to an engine-validated action.", *** Begin Patch *** Update File: the-house-that-remembers/story/canon.json @@
-
"endings": "3 implemented (leave_together, escape_alone, denial); 1 optional (sisters_bargain)"
-
"endings": "...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -173,7 +173,7 @@ "key_clues": "13 clues in three fragments (routine, rules, night)", "puzzles": "1: the parents' bedroom door", "major_ghosts": 1,
-
"endings": "3 implemented (leave_together, escape_alone, denial); 1 optional (sisters_bargain)"
-
}, "player_agency": [ "The player can investigate, talk, examine objects, rest, follow or refuse warnings, and leave when possible.", @@ -240,7 +240,6 @@ ] }, "author_todo": ["endings": "4 implemented (leave_together, escape_alone, denial, sisters_bargain)"
- "Decide whether to implement the optional 'sisters_bargain' ending.", "Write a few sister ghost lines for stage 3 in her own voice.", "Playtest pacing knobs: AMBIENT_AFTER_TURNS, AMBIENT_COOLDOWN, MAX_TURNS, PUSH_LIMIT." ], *** Begin Patch *** Update File: the-house-that-remembers/prompt.py @@ def state_block(game):
- from engine import FINAL_REVELATIONS, ROOMS, exit_labels
- from engine import FINAL_REVELATIONS, ... 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 @@ -191,7 +191,7 @@
def state_block(game):
- from engine import FINAL_REVELATIONS, ROOMS, exit_labels
-
from engine import FINAL_REVELATIONS, ROOMS, SISTER_PRESERVATION_MEMORY, exit_labels
room = ROOMS[game["room"]] friend_room = game.get("friend_room", "Entrance Hall") @@ -225,6 +225,8 @@ for memory in FINAL_REVELATIONS if memory["id"] in shared_revelations )
-
if game.get("sister_bargain_heard"):
-
lines.append(f"Sister's revealed perspective: {SISTER_PRESERVATION_MEMORY}")return "\n".join(lines)
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 @@ -160,6 +160,7 @@ self.assertIn("preserves them", reply["sister_perspective"]) self.assertNotIn("friend_arrived", reply) self.assertEqual(game["friend_room"], "Library")
-
self.assertIn("Sister's revealed perspective:", state_block(game))def test_sister_bargain_needs_explanation_and_explicit_acceptance(self): game = engine.new_game() {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run sister-path and ... ..................
Ran 18 tests in 0.007s
OK <shellId: 78 completed with exit code 0> {"command":"python -m py_compile 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py' 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remem... Python modules and canon JSON are valid <shellId: 80 completed with exit code 0> { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check; git status --short;", "description": "Final whitespace and worktree check" } warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it M .gitignore M README.md M the-house-that-remembers/app.py M the-house-that-remembers/engine.py M the-house-that-remembers/prompt.py M the-house-that-remembers/story/canon.json ?? the-house-that-remembers/assets/book.png ?? the-house-that-remembers/assets/letter.png ?? the-house-that-remembers/assets/note.png <shellId: 79 completed with exit code 0>
Implemented the optional sister’s-bargain path. The sister can offer a private conversation after **two warnings and the cooldown**, but only when the friend is in another room. This can happen before or after the player enters the bedroom.
If the player asks about her perspective, she explains that she believes preserving the family keeps them alive. The player must **explicitly accept** after hearing her explanation to trigger the ending; a question like “Why do you want to keep them as they are?” won’t count as acceptance. The ending reflects her partial perspective without contradicting anything the player has already seen.
Updated [engine.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py), [prompt.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py), [canon.json](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/story/canon.json), and [README.md](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/README.md). All **18 regression tests** pass; Python syntax and canon JSON checks are clean.
if after two warnings + cooldown the player is not alone, what will happen? can it be retriggered currently? e.g. when he is alone later
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 980, 1028 ] } 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 @@ -980,49 +980,49 @@ result["sister_conversation"] = True if SISTER_PERSPECTIVE_RE.search(raw_action.lower()): game["sister_bargain_heard"] = True result["sister_perspective"] = SISTER_PRESERVATION_MEMORY
if (
game["truth_known"]
and game["denial"] < 3
and game["friend_room"] == game["room"]
and intent in {"dialogue", "action_and_dialogue"}
and result["outcome"] not in {"blocked", "ending"}
and len(game["final_revelations"]) < len(FINAL_REVELATIONS)
):
revelation = FINAL_REVELATIONS[len(game["final_revelations"])]
game["final_revelations"].append(revelation["id"])
result["family_memory"] = revelation
result["stage_advanced"] = _advance_stage(game)
sister_due = (
not game["sister_seen"]
or game["turn_count"] - game["last_sister_turn"] >= SISTER_COOLDOWN
)
if game["stage"] >= 3 and sister_due and not game["ending"]:
can_speak_privately = (
game["sister_warning_count"] >= 2
and game["friend_room"] != game["room"]
)
if can_speak_privately:
appearance = (
"The sister appears again, but this time she does not repeat her "
"warning. With the friend elsewhere, she looks directly at the player "
"and seems willing to explain why she wants the family left undisturbed."
)
game["sister_bargain_available"] = True
else:
appearance = (
"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."
if not game["sister_seen"]
else "The friend's sister appears briefly again and quietly repeats her "
"warning to leave. Then she is gone."
)
game["sister_warning_count"] = min(2, game["sister_warning_count"] + 1)
game["sister_seen"] = True
game["last_sister_turn"] = game["turn_count"]
result["sister"] = appearance
result["sister_private_encounter"] = can_speak_privately
Yes. If the cooldown expires while the friend is **with** the player, the sister gives another warning and resets her appearance timer. Since the warning count stays capped at two, she can offer the private conversation after a later cooldown when the friend is elsewhere.
One detail: once that private conversation becomes available, the current code keeps it available until the player moves. So the player can talk to her on a later turn while the friend remains elsewhere; it doesn’t require catching her on the exact turn she appears. This logic is in [engine.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py).
{"pattern":""clues": \[|CLUE_FRAGMENT|def fragment_progress|"routine"|"rules"|"night"","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py","C:... [grep content: 26 matches across 2 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
story/canon.json (3 match(es)): 253: "routine": "The friend is still keeping house for people who are not there. Clues: third cup, pulled-out chair, food that never spoils.", 254: "rules": "The father's preservation. Clues: the stopped hall clock, the outdated calendar, the note on the bedroom door.", 255: "night": "What happened at the door. Clues: repeating footsteps overhead, the humming, the slow knocking from inside the bedroom."
engine.py (23 match(es)): 14: FRAGMENT_KINDS = ("routine", "rules", "night") 16: "routine": "Routine", 17: "rules": "Rules", 18: "night": "Night", 100: "clues": [("A musty smell lingers in the air", 1, None)], 108: "clues": [("The clock on the wall is frozen at 3:15", 1, 'rules')], 130: "clues": [("A book was recently removed from the shelf", 1, None)], 140: "clues": [("The same few footsteps repeat overhead", 1, 'night')], 150: "clues": [("Family photos on the wall are blurred and unrecognizable", 1, None)], 171: "clues": [("The house's first owner buried his sister alive and died soon after; the house stood empty for years", 1, None)], 192: "clues": [("A third cup sits set out at the table, as if someone else were expected", 1, 'routine')], 202: "clues": [("A kitchen chair is pulled out, as though someone had just stood up", 1, 'routine')], 212: "clues": [("A plate of food sits on the counter, untouched", 1, 'routine'), ("The food has sat out for ages but has not spoiled", 3, None)], 220: "clues": [("The calendar on the wall is turned to March, but it is currently October. Year is faded and unreadable", 1, 'rules')], 241: "clues": [("The portrait on the landing changed between looks", 2, None)], 251: "clues": [("The humming upstairs falters whenever you listen", 1, 'night')], 275: "clues": [ 276: ("A note on the parents' bedroom door reads: do not open; do not change; I have to keep things as they are", 1, "rules"), 277: ("Slow knocking comes from inside the locked bedroom", 2, "night"), 300: CLUE_FRAGMENT = { 331: "clues": [], 359: def fragment_progress(game): 363: frag = CLUE_FRAGMENT.get(clue) { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 240, 285 ] } 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 @@ -240,46 +240,46 @@ ], "clues": [("The portrait on the landing changed between looks", 2, None)], "friend": "He will not look at it.", }, "humming": { "ambient": True, "keywords": ["humming", "hum", "music", "voice", "melody", "listen"], "looks": [ "The humming drifts down from above. When you look up, it falters, then starts again from the beginning.", "The same melody, again, from the very beginning.", ], "clues": [("The humming upstairs falters whenever you listen", 1, 'night')], "friend": "He seems not to hear it, or says it is only the house.", }, }, }, "Upper Landing": { "description": ( "The upper landing is long and narrow, lit by a single grey window. " "Every door along it is shut. At the far end, a folded note is taped " "to the frame of one door." ), "exits": ["Grand Staircase", "Parents' Bedroom"], "img": "upper_landing.jpg", "gated": {"Parents' Bedroom": "door_open"}, "aliases": ["upper landing", "upstairs landing", "landing", "upper floor", "second floor", "upstairs", "first floor", "second story"], "features": { "door": { "ambient": True, "keywords": ["door", "note", "knock", "knocking"], "takeable": "Upper Landing Note", "looks": [ "The parents' bedroom door is shut. The folded note taped to its frame reads, in a careful hand:\n'Do not open\n\nDo not change\n\nI have to keep things as they are.'", "You press your ear to the wood. From inside comes a slow knock, three times. Then nothing.", ], "clues": [ ("A note on the parents' bedroom door reads: do not open; do not change; I have to keep things as they are", 1, "rules"), ("Slow knocking comes from inside the locked bedroom", 2, "night"), ], "friend": "He will not come near the door.", }, }, }, "Parents' Bedroom": { "description": ("The parents' bedroom is cold and perfectly still. Three figures lie together on the bed, turned toward one another, as if asleep. No dust has settled on anything here. The air smells faintly of cold metal."), "exits": ["Upper Landing"], { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 250, 265 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -250,16 +250,16 @@ }, "memory_fragments": { "description": "Three kinds of clue. Finding two clues of a kind completes that fragment. The sidebar tracks each category and the total out of three. Completing all three lets the player try the bedroom door.", "routine": "The friend is still keeping house for people who are not there. Clues: third cup, pulled-out chair, food that never spoils.", "rules": "The father's preservation. Clues: the stopped hall clock, the outdated calendar, the note on the bedroom door.", "night": "What happened at the door. Clues: repeating footsteps overhead, the humming, the slow knocking from inside the bedroom." }, "engine_rules": { "denial": "Friend's denial runs 0-10. It starts high and drops by 1 for each clue found and each of up to 3 kind gestures. Blunt confrontation before all three fragments raises it by 1.", "door": "The bedroom door opens only if all three fragments are complete and denial is at or below 3. Opening it moves the story to the final stage.", "family_revelations": "After the player enters the bedroom, face-to-face conversation shares four authored family memories one at a time when denial is below 3: the mother's decline and the family's deaths, the father's attempt to preserve them, the sister's hope, and the friend's conflict and silence.", "together_ending": "The friend can leave together only after denial is below 3 and all four family memories have been shared.", "sister_bargain": "After the sister has warned the player at least twice, when the cooldown passes and the friend is elsewhere, she may privately explain her belief that preserving the family keeps them alive. This conversation and ending may happen before or after the bedroom is opened. The ending requires the player to hear her perspective and explicitly accept her bargain; the player leaves alone with her partial truth.", "pushing": "Three blunt confrontations before the evidence ends the story in the denial ending.", "clock": "If the truth has not been faced after about 90 turns, the denial ending triggers." }
right now when in the bargain conversation with the sister, she will keep talking until the player expliclty decides to move on or accept, or will she disappear after a few turns? I think she should disappear after a few turns, the player still can decide to leave
I checked the current behavior: once her private conversation becomes available, it can persist until the player moves, so she doesn’t naturally leave after a few dialogue turns. I’d count only turns spent speaking with her and have her vanish after three, while preserving the explicit bargain choice so the player can still accept it or leave normally.
{"message":"How many sister-conversation turns should she stay for? I recommend three: enough for her to explain her belief and let the player respond, without keeping her there indefinitely. After sh... User responded: conversation_turns: 5 { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 15, 85 ] } 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 @@ -15,71 +15,71 @@ FRAGMENT_LABELS = { "routine": "Routine", "rules": "Rules", "night": "Night", } DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door PUSH_LIMIT = 3 # blunt confrontations before the evidence -> he shuts down SUPPORT_LIMIT = 3 # times kindness can lower his denial MAX_TURNS = 90 # unresolved truth or denial by then -> "denial" ending NUDGE_AFTER_TURNS = 4 NUDGE_COOLDOWN = 3 SISTER_COOLDOWN = 12
FINAL_REVELATIONS = ( { "id": "mother", "title": "His mother's decline", "detail": ( "They are dead. The house drained them, beginning with his mother, who " "grew exhausted and unable to sleep and sensed something was wrong. The " "room was preserved, so they look as if they are sleeping." ), }, { "id": "father", "title": "His father's attempt to preserve them", "detail": ( "After help failed, his father became frightened and tried to preserve " "the family unchanged. He locked himself, his wife, and his daughter in " "the bedroom and told his son never to open the door. He thought he was " "protecting them, but shut them away from help; they died there, drained " "by the house." ), }, { "id": "sister", "title": "His sister's hope", "detail": ( "His sister kept trying to hold the family together and urged her brother " "not to give up on them. Now she clings to the house's preserved illusion, " "believing it keeps her family together; her warnings to leave are real, " "but she mistakes letting go for losing them." ), }, { "id": "friend", "title": "Why he stayed silent", "detail": ( "He had argued with his father and believed the others were overreacting. " "He knocked once, heard no answer, and obeyed the last order out of anger " "and fear. Since then he has pretended they were still alive and resting, " "unable to face that he never opened the door." ), }, )
SISTER_PRESERVATION_MEMORY = ( "She believes the family is still together as long as the house preserves them " "exactly as they were. To her, keeping things unchanged is a way of keeping them " "alive; she warns the player away because she fears that facing the truth will " "make her lose them." )
ROOMS = { "Entrance Hall": { { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 680, 710 ] } 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 @@ -680,31 +680,31 @@ room_name = game["room"] room = ROOMS[room_name] room_parse_text = _remove_inventory_mentions(text, game["inventory"]) rooms_named = _find_rooms(room_parse_text) negated = bool(NEGATION_RE.search(text)) spoken_negated = bool(NEGATION_RE.search(spoken))
if (
game.get("sister_bargain_available")
and game["friend_room"] != game["room"]
and not spoken_negated
and SISTER_ACCEPT_RE.search(spoken)
):
if not game.get("sister_bargain_heard"):
return _result(
"blocked",
"The sister has not explained her bargain yet. The player cannot "
"accept it before hearing what she believes.",
)
return _end(
game,
"sisters_bargain",
"The player accepts the sister's bargain and chooses to leave alone, "
"carrying her partial account of the family's fate.",
)
if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
if game["denial"] >= 3:
return _result(
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 860, 1035 ] } 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 @@ -860,176 +860,176 @@ game["turns_in_stage"] = 0 return True if game["stage"] >= 4: return False if STAGE_RULESgame["stage"]: game["stage"] += 1 game["turns_in_stage"] = 0 return True return False
def apply_action(game, raw_action): """Resolve one player action, update game state, return the engine result.""" game.setdefault("friend_room", "Entrance Hall") game.setdefault("sister_seen", False) game.setdefault("last_sister_turn", -99) game.setdefault("sister_warning_count", 0) game.setdefault("sister_bargain_available", False) game.setdefault("sister_bargain_heard", False) game.setdefault("final_revelations", []) text = raw_action.lower() intent = _classify_intent(raw_action, game) # classify before the action can change the room speaking_with_sister = ( game["sister_bargain_available"] and intent in {"dialogue", "action_and_dialogue"} and not game["ending"] and game["friend_room"] != game["room"] ) seen_before = len(game["examined"]) # measured BEFORE resolving, so first looks count as fresh result = _resolve(game, text) if result["outcome"] == "moved": intent = "movement" game["sister_bargain_available"] = False elif result["outcome"] == "picked_up": intent = "physical_action" elif result["outcome"] == "examined" and intent not in { "examination", "exploration", }: intent = "examination" result["intent"] = intent if ( intent in {"dialogue", "action_and_dialogue"} and not game["ending"] and game["friend_room"] != game["room"] and not speaking_with_sister ): game["friend_room"] = game["room"] result["friend_arrived"] = True result["detail"] += ( f" The friend joins the player in the {game['room']} so they can speak " "face to face." )
game["turn_count"] += 1
game["turns_in_stage"] += 1
if result["outcome"] != "moved":
game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["support_given"] < SUPPORT_LIMIT
and SUPPORT_RE.search(text)
):
game["support_given"] += 1
game["denial"] = max(0, game["denial"] - 1)
result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
if (
not game["ending"]
and game["turn_count"] >= MAX_TURNS
and (not game["truth_known"] or game["denial"] >= 3)
):
result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
if fresh:
game["last_notice_turn"] = game["turn_count"]
if (
not fresh
and result["outcome"] in ("no_change", "examined")
and not game["ending"]
and not game["truth_known"]
and game["room_turns"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_nudge_turn"] >= NUDGE_COOLDOWN
and _next_unnoticed(game, game["room"]) is None
):
nudge = _nudge(game)
if nudge:
result["nudge"] = nudge
game["last_nudge_turn"] = game["turn_count"]
game["nudges_here"] += 1
if result["new_clue"]:
for _ in result["new_clue"].split("; "):
game["denial"] = max(0, game["denial"] - 1)
if speaking_with_sister and result["outcome"] not in {"ending", "blocked"}:
result["sister_conversation"] = True
if SISTER_PERSPECTIVE_RE.search(raw_action.lower()):
game["sister_bargain_heard"] = True
result["sister_perspective"] = SISTER_PRESERVATION_MEMORY
if (
game["truth_known"]
and game["denial"] < 3
and game["friend_room"] == game["room"]
and intent in {"dialogue", "action_and_dialogue"}
and result["outcome"] not in {"blocked", "ending"}
and len(game["final_revelations"]) < len(FINAL_REVELATIONS)
):
revelation = FINAL_REVELATIONS[len(game["final_revelations"])]
game["final_revelations"].append(revelation["id"])
result["family_memory"] = revelation
result["stage_advanced"] = _advance_stage(game)
sister_due = (
not game["sister_seen"]
or game["turn_count"] - game["last_sister_turn"] >= SISTER_COOLDOWN
)
if game["stage"] >= 3 and sister_due and not game["ending"]:
can_speak_privately = (
game["sister_warning_count"] >= 2
and game["friend_room"] != game["room"]
)
if can_speak_privately:
appearance = (
"The sister appears again, but this time she does not repeat her "
"warning. With the friend elsewhere, she looks directly at the player "
"and seems willing to explain why she wants the family left undisturbed."
)
game["sister_bargain_available"] = True
else:
appearance = (
"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."
if not game["sister_seen"]
else "The friend's sister appears briefly again and quietly repeats her "
"warning to leave. Then she is gone."
)
game["sister_warning_count"] = min(2, game["sister_warning_count"] + 1)
game["sister_seen"] = True
game["last_sister_turn"] = game["turn_count"]
result["sister"] = appearance
result["sister_private_encounter"] = can_speak_privately
game["last_result"] = result
print("Game state:")
pprint(game, sort_dicts=False)
return result
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 25, 40 ] } 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 @@ -25,16 +25,16 @@
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT normally brings him into the player's room; narrate that arrival before his reply. The exception is an ENGINE RESULT-marked private sister conversation: answer as the sister and keep the friend elsewhere.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech.
- When the ENGINE RESULT authorizes the sister's private conversation, she may explain that she believes preserving the family keeps them alive. This is her perspective, not objective proof that the family is alive, and does not reveal the full truth.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good." { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 245, 330 ] } 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 @@ -245,86 +245,86 @@ "\nAmbient detail to weave into this turn in one quiet sentence, " f"without making it a big moment: {r['ambient']}" ) if r.get("nudge"): 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']}" friend_is_here = game.get("friend_room", "Entrance Hall") == 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']}" if r.get("sister_private_encounter"): note += ( "\nThis is the sister's private conversation opportunity, available because " "she has already warned the player twice and the friend is elsewhere. " "The player may speak with her even if the bedroom door is still closed." ) if r.get("sister_conversation"): note += ( "\nThe player is speaking privately with the sister. Respond as the sister, " "not the friend. The friend is elsewhere and must not answer or arrive." ) if r.get("sister_perspective"): note += ( "\nThe sister's perspective to explain clearly if the player asked: " f"{r['sister_perspective']}" ) if r.get("family_memory"): memory = r["family_memory"] note += ( f"\nFamily memory to acknowledge clearly in the friend's reply " f"({memory['title']}): {memory['detail']}" ) if r.get("friend_arrived"): note += "\nThe friend has joined the player to speak face to face; narrate his arrival before his response." intent_reminders = { "dialogue": ( "Treat the player's words as spoken to the friend and answer directly in his natural, " "restrained voice. If the ENGINE RESULT says he joined the player, narrate his arrival " "before the reply." ), "memory": ( "Answer the player's memory question as their character, using only the established " "player memories in CURRENT STATE or facts already established in conversation. " "Do not invent a specific shared event or reveal hidden family history." ), "physical_action": ( "Treat this as a physical action by the player. Acknowledge what they actually did " "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. If the ENGINE RESULT says he joined the player, narrate his " "arrival before the reply. Do not add actions for the player." ), "examination": ( "Treat this as an examination. Use the ENGINE RESULT as the only new observation; " "do not add clues or details." ), "exploration": ( "Treat this as looking around or exploring. Use only the room description and " "ENGINE RESULT for observations; do not add clues or details." ), "movement": "Narrate the confirmed route and destination. Keep the friend at the location given in CURRENT STATE unless the ENGINE RESULT says he accompanied the player.", "roleplay": "Respond naturally to the player's action without inventing consequential events.", } reminder = ( f"{intent_reminders.get(r.get('intent', 'roleplay'), intent_reminders['roleplay'])} " "Stay in character. 2-5 short sentences. Add no other items, clues, uncanny events, " "room changes or backstory." ) if r.get("sister_conversation"): reminder = ( "Answer as the sister, directly to the player. The friend is elsewhere; do not " "bring him into the room or let him answer. Stay within the sister's established " "beliefs and the engine-authorized perspective." ) if game["ending"]: reminder = "This is the final scene. 4-6 sentences. Close the story." return note + f"\n[REMINDER] {reminder}"
def build_messages(game, canon): system = "\n\n".join([CORE, story_block(canon), stage_block(game), state_block(game)]) {"pattern":"sister_bargain_available|sister_conversation|sister_warning_count","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py","C:\Use... [grep content: 23 matches across 3 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
prompt.py (3 match(es)): 206: f"Sister's private conversation available: {'yes' if game.get('sister_bargain_available') else 'no'}", 263: if r.get("sister_conversation"): 318: if r.get("sister_conversation"):
engine.py (11 match(es)): 342: "sister_warning_count": 0, 343: "sister_bargain_available": False, 688: game.get("sister_bargain_available") 876: game.setdefault("sister_warning_count", 0) 877: game.setdefault("sister_bargain_available", False) 883: game["sister_bargain_available"] 892: game["sister_bargain_available"] = False 980: result["sister_conversation"] = True 1004: game["sister_warning_count"] >= 2 1013: game["sister_bargain_available"] = True 1023: game["sister_warning_count"] = min(2, game["sister_warning_count"] + 1)
tests/test_engine.py (9 match(es)): 144: sister_warning_count=2, 152: self.assertTrue(game["sister_bargain_available"]) 158: self.assertTrue(reply["sister_conversation"]) 172: sister_warning_count=2, 173: sister_bargain_available=True, 195: sister_warning_count=2, 196: sister_bargain_available=True, 213: sister_warning_count=2, 221: self.assertFalse(game["sister_bargain_available"])
I’ll use five sister-directed dialogue turns as requested. After the fifth, she’ll vanish; her explained bargain remains explicitly accept-able, and ordinary movement or leaving stays available. I’m also handling the cooldown case so a question on the eligible turn can be answered by her without summoning the friend.
*** Begin Patch *** Update File: the-house-that-remembers/engine.py @@ SISTER_COOLDOWN = 12 +SISTER_CONVERSATION_LIMIT = 5 @@ "sister_warning_count": 0, "sister_bargain_available": ... 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 @@ -24,6 +24,7 @@ NUDGE_AFTER_TURNS = 4 NUDGE_COOLDOWN = 3 SISTER_COOLDOWN = 12 +SISTER_CONVERSATION_LIMIT = 5
FINAL_REVELATIONS = ( { @@ -342,6 +343,8 @@ "sister_warning_count": 0, "sister_bargain_available": False, "sister_bargain_heard": False,
-
"sister_present": False, -
"sister_conversation_turns": 0, "denial": 8, # friend's denial (high = deflecting) "pushed": 0, "support_given": 0,
@@ -876,11 +879,25 @@ game.setdefault("sister_warning_count", 0) game.setdefault("sister_bargain_available", False) game.setdefault("sister_bargain_heard", False)
- game.setdefault("sister_present", False)
- game.setdefault("sister_conversation_turns", 0) game.setdefault("final_revelations", []) text = raw_action.lower() intent = _classify_intent(raw_action, game) # classify before the action can change the room
- sister_appearance_due = (
-
not game["sister_seen"] -
or game["turn_count"] + 1 - game["last_sister_turn"] >= SISTER_COOLDOWN - )
- private_sister_due = (
-
game["stage"] >= 3 -
and sister_appearance_due -
and not game["ending"] -
and game["sister_warning_count"] >= 2 -
and not game["sister_bargain_heard"] -
and game["friend_room"] != game["room"] - ) speaking_with_sister = (
-
game["sister_bargain_available"]
-
(game["sister_present"] or private_sister_due) and intent in {"dialogue", "action_and_dialogue"} and not game["ending"] and game["friend_room"] != game["room"]
@@ -889,7 +906,7 @@ result = _resolve(game, text) if result["outcome"] == "moved": intent = "movement"
-
game["sister_bargain_available"] = False
-
elif result["outcome"] == "picked_up": intent = "physical_action" elif result["outcome"] == "examined" and intent not in { @@ -976,12 +993,6 @@ for _ in result["new_clue"].split("; "): game["denial"] = max(0, game["denial"] - 1)game["sister_present"] = False
- if speaking_with_sister and result["outcome"] not in {"ending", "blocked"}:
-
result["sister_conversation"] = True -
if SISTER_PERSPECTIVE_RE.search(raw_action.lower()): -
game["sister_bargain_heard"] = True -
result["sister_perspective"] = SISTER_PRESERVATION_MEMORY - if ( game["truth_known"] and game["denial"] < 3 @@ -1002,6 +1013,7 @@ if game["stage"] >= 3 and sister_due and not game["ending"]: can_speak_privately = ( game["sister_warning_count"] >= 2
-
and not game["sister_bargain_heard"] and game["friend_room"] != game["room"] ) if can_speak_privately:
@@ -1011,6 +1023,8 @@ "and seems willing to explain why she wants the family left undisturbed." ) game["sister_bargain_available"] = True
-
game["sister_present"] = True -
game["sister_conversation_turns"] = 0 else: appearance = ( "A faint figure resolves into the friend's sister. She appears briefly "
@@ -1021,11 +1035,22 @@ "warning to leave. Then she is gone." ) game["sister_warning_count"] = min(2, game["sister_warning_count"] + 1)
-
game["sister_present"] = False game["sister_seen"] = True game["last_sister_turn"] = game["turn_count"] result["sister"] = appearance result["sister_private_encounter"] = can_speak_privately -
if speaking_with_sister and result["outcome"] not in {"ending", "blocked"}:
-
result["sister_conversation"] = True -
if SISTER_PERSPECTIVE_RE.search(raw_action.lower()): -
game["sister_bargain_heard"] = True -
result["sister_perspective"] = SISTER_PRESERVATION_MEMORY -
game["sister_conversation_turns"] += 1 -
if game["sister_conversation_turns"] >= SISTER_CONVERSATION_LIMIT: -
game["sister_present"] = False -
result["sister_departed"] = True -
game["last_result"] = result print("Game state:") pprint(game, sort_dicts=False)
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/hac
[Truncated — full output (11.1 KB) temporarily saved to C:\Users\lucia\AppData\Local\Temp\1793219656378-copilot-tool-output-f18e595e5cb5475bb17aa7dcfc5dbea2.txt] {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run sister disappear... ................... #
Ran 19 tests in 0.009s
OK <shellId: 81 completed with exit code 0> {"command":"python -m py_compile 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py' 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remem... Canon JSON is valid <shellId: 83 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/engine.py the-house-that-remembers/prompt.py the-house-that-rem... warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it <shellId: 82 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\README.md", "view_range": [ 48, 66 ] } 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 @@ -48,19 +48,19 @@
- 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 sidebar tracks the three memory fragments needed to open the bedroom door. After entering, the friend shares four family memories gradually; the two of you can leave together once he is ready and has faced the full story.
After two sister-ghost warnings, she may offer a private conversation when your friend is elsewhere. You can hear her belief that preserving the family keeps them alive, then explicitly accept her bargain to leave alone with that partial truth. This optional path is possible before or after entering the bedroom.
Select 📜 The Letter 🔍, 📖 Library Book 🔍, or
📝 Upper Landing Note 🔍 in the inventory to view its image in a dismissible
dialog. Add the images at the-house-that-remembers/assets/letter.png,
the-house-that-remembers/assets/book.png, and
*** Begin Patch
*** Update File: the-house-that-remembers/tests/test_engine.py
@@
def test_sister_private_conversation_requires_two_warnings_and_friend_absent(self):
@@
self.assertEqual(...
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
@@ -162,6 +162,27 @@
self.assertEqual(game["friend_room"], "Library")
self.assertIn("Sister's revealed perspective:", state_block(game))
- def test_player_can_speak_to_sister_on_private_encounter_turn(self):
-
game = engine.new_game() -
game.update( -
stage=3, -
room="Kitchen", -
friend_room="Library", -
sister_seen=True, -
sister_warning_count=2, -
last_sister_turn=0, -
turn_count=engine.SISTER_COOLDOWN - 1, -
) -
result = act(game, "What do you believe?") -
self.assertTrue(result["sister_private_encounter"]) -
self.assertTrue(result["sister_conversation"]) -
self.assertIn("sister_perspective", result) -
self.assertNotIn("friend_arrived", result) -
self.assertEqual(game["friend_room"], "Library") -
self.assertEqual(game["sister_conversation_turns"], 1) - def test_sister_bargain_needs_explanation_and_explicit_acceptance(self): game = engine.new_game() game.update(
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 @@ -58,7 +58,9 @@ After two sister-ghost warnings, she may offer a private conversation when your friend is elsewhere. You can hear her belief that preserving the family keeps them alive, then explicitly accept her bargain to leave alone with that partial -truth. This optional path is possible before or after entering the bedroom. +truth. She stays for up to five conversation turns; after she vanishes, you can +still accept the bargain or leave normally. This optional path is possible +before or after entering the bedroom.
Select 📜 The Letter 🔍, 📖 Library Book 🔍, or 📝 Upper Landing Note 🔍 in the inventory to view its image in a dismissible {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run final game regre... .................... #
Ran 20 tests in 0.009s
OK <shellId: 84 completed with exit code 0> {"command":"python -m py_compile 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py' 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remem... Python and JSON validation passed <shellId: 86 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- README.md the-house-that-remembers/engine.py the-house-that-remembers/prompt.py the-hous... warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it <shellId: 85 completed with exit code 0>
The sister now stays for up to **five turns spent speaking with her**, then vanishes. The player can still explicitly accept her bargain afterward, or move and leave normally. If the friend isn’t elsewhere when the cooldown expires, she gives another warning; the private conversation can happen at a later cooldown when the player is alone.
I also covered the case where the player asks her a question on the exact turn she reappears, so the friend won’t be summoned instead. Updated [engine.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py), [prompt.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py), [canon.json](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/story/canon.json), and [README.md](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/README.md). All **20 regression tests pass**; syntax, JSON, and diff checks are clean.
{"pattern":"CLUE_FRAGMENT|ROUTINE|RULES|NIGHT|fragment|clue|denial","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py"],"output_mode":"content","-n":t... [grep content: 68 matches across 1 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
engine.py (68 match(es)): 10: # Slow clue drip: during quiet turns the engine surfaces one small detail. 13: FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment 20: DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door 22: SUPPORT_LIMIT = 3 # times kindness can lower his denial 23: MAX_TURNS = 90 # unresolved truth or denial by then -> "denial" ending 83: # successive examination reveals (the last entry repeats). "clue" is 84: # (clue text, examination number on which it is found). 101: "clues": [("A musty smell lingers in the air", 1, None)], 109: "clues": [("The clock on the wall is frozen at 3:15", 1, 'rules')], 131: "clues": [("A book was recently removed from the shelf", 1, None)], 141: "clues": [("The same few footsteps repeat overhead", 1, 'night')], 151: "clues": [("Family photos on the wall are blurred and unrecognizable", 1, None)], 172: "clues": [("The house's first owner buried his sister alive and died soon after; the house stood empty for years", 1, None)], 193: "clues": [("A third cup sits set out at the table, as if someone else were expected", 1, 'routine')], 203: "clues": [("A kitchen chair is pulled out, as though someone had just stood up", 1, 'routine')], 213: "clues": [("A plate of food sits on the counter, untouched", 1, 'routine'), ("The food has sat out for ages but has not spoiled", 3, None)], 221: "clues": [("The calendar on the wall is turned to March, but it is currently October. Year is faded and unreadable", 1, 'rules')], 242: "clues": [("The portrait on the landing changed between looks", 2, None)], 252: "clues": [("The humming upstairs falters whenever you listen", 1, 'night')], 276: "clues": [ 300: # Which fragment each clue belongs to, built from ROOMS. 301: CLUE_FRAGMENT = { 305: for (text, _n, frag) in feat.get("clues", []) 332: "clues": [], 348: "denial": 8, # friend's denial (high = deflecting) 362: def fragment_progress(game): 363: """Return clue counts and completion status for each memory fragment.""" 365: for clue in game["clues"]: 366: frag = CLUE_FRAGMENT.get(clue) 378: def fragments_complete(game): 379: """Set of completed fragment kinds ('routine', 'rules', 'night').""" 382: for kind, progress in fragment_progress(game).items() 518: def _result(outcome, detail, new_clue=None, ambient=None, friend=None): 523: "new_clue": new_clue, 533: """Record one examination of a feature. Returns (what they notice, new clue(s) or None).""" 540: for text, on_n, _frag in feat.get("clues", []): 541: if n == on_n and text not in game["clues"]: 542: game["clues"].append(text) 592: and len(fragments_complete(game)) >= 3): 629: if len(fragments_complete(game)) < 3: 637: if game["denial"] > DOOR_DENIAL_MAX: 654: if len(fragments_complete(game)) >= 3: 655: game["denial"] = max(0, game["denial"] - 1) 664: game["denial"] = min(10, game["denial"] + 1) 667: game, "denial", 712: if game["denial"] >= 3: 808: noticed, new_clue = _examine(game, room_name, name, feat) 816: new_clue, 825: noticed, new_clue = _examine(game, room_name, name, feat) 829: new_clue, 842: "(talking, resting, small gestures). Do not add items, clues, or events. " 852: STAGE_RULES = [ 854: lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 1, # 1 contradictions 855: lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 4, # 2 paintings 856: lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 7, # 3 sister's warnings 867: if STAGE_RULESgame["stage"]: 946: noticed, new_clue = _examine(game, game["room"], name, feat) 948: result["new_clue"] = new_clue 959: game["denial"] = max(0, game["denial"] - 1) 962: # Time runs out if the truth was never faced or the friend remains in denial. 966: and (not game["truth_known"] or game["denial"] >= 3) 968: result = _end(game, "denial", "Too much time has passed. The house closes around the friend.") 970: # "Fresh" = the player met something new this turn (first look at a feature, or a clue). 971: fresh = len(game["examined"]) > seen_before or bool(result["new_clue"]) 992: if result["new_clue"]: 993: for _ in result["new_clue"].split("; "): 994: game["denial"] = max(0, game["denial"] - 1) 998: and game["denial"] < 3 {"pattern":"Routine|Rules|Night|fragment|clue categories|clue","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json","C:\Users\lucia\Documents... [grep content: 20 matches across 2 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober]
README.md (2 match(es)): 14: - Python - for logic, validates actions, updated history, clues, inventory 54: The sidebar tracks the three memory fragments needed to open the bedroom door.
the-house-that-remembers/story/canon.json (18 match(es)): 42: "The parents linger as fragments tied to the bedroom: slow knocking and footsteps. They want release, not endless preservation.", 63: "guidance": "His progression is governed by explicit game-state rules (denial, fragments, door), not by the model." 71: "Before that, use oblique fragments only: knocking, repeating footsteps, humming, stopped clocks.", 73: "The friend should face the truth in halting fragments and quiet realization, not a lecture." 92: "description": "The narrator cannot declare a puzzle solved, a door unlocked, a clue acquired, a spirit released, or a character dead unless the engine authorizes it." 106: "progression": "As clues are found his denial drops. Blunt confrontation before the evidence makes him shut down. The engine owns the numbers." 118: "limitation": "Their fragments may be incomplete. Do not invent definitive details of the tragedy until authored." 162: "stage": "4 Parents' fragments", 173: "key_clues": "13 clues in three fragments (routine, rules, night)", 185: "Python owns location, exits, inventory, clues, puzzle state, door locks, chapter/time progression, character condition, trust, denial, and ending eligibility.", 194: "description": "True good ending. The player assembles the fragments, opens the door, the friend faces what happened, and they leave the house together.", 226: "Reveal major secrets before the player earns them through clues or milestones.", 227: "Contradict inventory, clues, room layout, puzzle state, locks, character condition, or ending eligibility.", 236: "Inventory and discovered clues", 251: "memory_fragments": { 252: "description": "Three kinds of clue. Finding two clues of a kind completes that fragment. The sidebar tracks each category and the total out of three. Completing all three lets the player try the bedroom door.", 258: "denial": "Friend's denial runs 0-10. It starts high and drops by 1 for each clue found and each of up to 3 kind gestures. Blunt confrontation before all three fragments raises it by 1.", 259: "door": "The bedroom door opens only if all three fragments are complete and denial is at or below 3. Opening it moves the story to the final stage.", {"description":"Find clue category decision","query":"SELECT s.id, s.summary, s.created_at FROM sessions s WHERE s.created_at >= now() - INTERVAL '365 days' AND s.summary ILIKE '%fragment%' ORDER BY s... SQL (session_store/personal): SELECT s.id, s.summary, s.created_at FROM sessions s WHERE s.created_at >= now() - INTERVAL '365 days' AND s.summary ILIKE '%fragment%' ORDER BY s.created_at DESC LIMIT 10
Query returned 0 rows. { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 1, 30 ] } 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 @@ -1,30 +1,30 @@ """engine.py - authoritative game state and rules.
Nothing in here calls the model. The model only ever sees the result of what this module decides (see prompt.py). """ import re from pprint import pprint
AMBIENT_AFTER_TURNS = 3 # turns spent in a room before a detail may surface AMBIENT_COOLDOWN = 3 # minimum turns between any two noticed details FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment FRAGMENT_KINDS = ("routine", "rules", "night") FRAGMENT_LABELS = { "routine": "Routine", "rules": "Rules", "night": "Night", } DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door PUSH_LIMIT = 3 # blunt confrontations before the evidence -> he shuts down SUPPORT_LIMIT = 3 # times kindness can lower his denial MAX_TURNS = 90 # unresolved truth or denial by then -> "denial" ending NUDGE_AFTER_TURNS = 4 NUDGE_COOLDOWN = 3 SISTER_COOLDOWN = 12 SISTER_CONVERSATION_LIMIT = 5
FINAL_REVELATIONS = ( { { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 80, 320 ] } 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 @@ -80,241 +80,241 @@ ROOMS = { "Entrance Hall": { "description": ( "The moment you step inside, a gust of wind slips past you. The heavy front door slams shut behind you with a deafening BANG. The sound echoes through the vast entrance hall, bouncing off the high ceilings before dissolving into silence. \n\n" "A figure emerges from the shadows. Your friend.\n" "He looks nothing like the person you remember. His eyes are hollow, his skin pale, ghostly, and dark circles hollow his eyes.\n\n" ""You came," he whispers, his voice trembling. "I didn't think you would... Thank you."\n\n" ), "exits": ["Library", "Kitchen", "Grand Staircase"], "img": "hallway.jpg", "aliases": ["entrance hall", "entrance", "hallway", "hall", "foyer"], "features": { "smell": { "ambient": True, "keywords": ["smell", "scent", "odor", "musty"], "looks": ["A musty smell lingers in the air, a mixture of old wood and dust."], "clues": [("A musty smell lingers in the air", 1, None)], }, "clock": { "ambient": True, "keywords": ["clock", "clocks", "pendulum", "ticking"], "looks": [ "A tall clock stands against the wall, its hands stopped at a quarter past three. The pendulum hangs perfectly still. The clock hasn't moved. Nothing in this hall ticks.", ], "clues": [("The clock on the wall is frozen at 3:15", 1, 'rules')], "friend": "He says it only needs winding, then changes the subject.", }, }, }, "Library": { "description": ( "Shelves of warped books climb toward a ceiling lost " "in shadow. One narrow gap interrupts the dust on a " "lower shelf, as though a book was recently removed. Family photos are hanging on the walls, but the faces are blurred and unrecognizable. " ), "exits": ["Entrance Hall"], "img": "library.jpg", "aliases": ["library"], "features": { "shelf": { "ambient": True, "keywords": ["shelf", "shelves", "gap", "books", "bookshelf", "bookshelves"], "looks": [ "The dust on a lower shelf is disturbed by a narrow, clean gap where a book was recently removed.", "The gap is still there. Nothing nearby explains where the missing book went.", ], "clues": [("A book was recently removed from the shelf", 1, None)], "friend": "He says he doesn't remember taking a book, and changes the subject.", }, "footsteps": { "ambient": True, "keywords": ["footsteps", "steps", "sound", "sounds", "ceiling", "overhead", "listen"], "looks": [ "From somewhere above, a few slow footsteps cross the ceiling. After a , the very same footsteps cross it again.", "The footsteps repeat, exactly the same, as though something is replaying.", ], "clues": [("The same few footsteps repeat overhead", 1, 'night')], "friend": "He says it is only the house settling, and changes the subject.", }, "photos": { "ambient": True, "keywords": ["photo", "photos", "picture", "pictures", "frame", "frames"], "looks": [ "The family photos are blurred and unrecognizable. You can't make out any faces.", "The photos remain blurred. You can't tell who they are.", ], "clues": [("Family photos on the wall are blurred and unrecognizable", 1, None)], "friend": "He says the photos are just old and faded.", },
"history": {
"ambient": True,
"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.",
"The page hasn't changed. The ink is faded, but the passage is still legible.",
],
"clues": [("The house's first owner buried his sister alive and died soon after; the house stood empty for years", 1, None)],
},
},
},
"Kitchen": {
"description": (
"The narrow kitchen is cluttered but familiar. Rain ticks "
"against a dark window above the sink. You remember this kitchen, as if you had never left."
),
"exits": ["Entrance Hall"],
"img": "kitchen.jpg",
"aliases": ["kitchen"],
"features": {
"cups": {
"ambient": True,
"keywords": ["cup", "cups", "mug", "mugs", "teapot", "kettle", "counter"],
"looks": [
"Two cups wait by the teapot. Then you notice a third at the far end of the table, set out and untouched.",
"The third cup hasn't moved. It sits exactly where someone would put it for a guest.",
],
"clues": [("A third cup sits set out at the table, as if someone else were expected", 1, 'routine')],
"friend": "If he notices the player noticing, he glances away and changes the subject.",
},
"chair": {
"ambient": True,
"keywords": ["chair", "chairs", "table", "seat"],
"looks": [
"One chair at the table is pulled out slightly, as though someone had only just stood up from it.",
"The chair stays where it is. The seat is cold.",
],
"clues": [("A kitchen chair is pulled out, as though someone had just stood up", 1, 'routine')],
},
"food": {
"ambient": True,
"keywords": ["food", "plate", "plates", "dish", "dishes"],
"looks": [
"A plate of food sits on the counter, untouched. The smell is faint but familiar.",
"The plate of food remains. It is cold, but the smell is still faintly familiar.",
"It seems like the food has been sitting there for a while, but it isn't spoiled."
],
"clues": [("A plate of food sits on the counter, untouched", 1, 'routine'), ("The food has sat out for ages but has not spoiled", 3, None)],
},
"calendar": {
"ambient": True,
"keywords": ["calendar", "date", "dates"],
"looks": [
"The calendar on the wall is turned to March, but the year is faded and unreadable. It is currently October.",
],
"clues": [("The calendar on the wall is turned to March, but it is currently October. Year is faded and unreadable", 1, 'rules')],
},
},
},
"Grand Staircase": {
"description": (
"The staircase groans beneath your weight. At the "
"landing, a portrait hangs crookedly. From above "
"comes the faint sound of someone humming."
),
"exits": ["Entrance Hall", "Upper Landin
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 150, 180 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -150,31 +150,31 @@ "stage": "1 Contradictions", "may_learn": "The friend speaks of his family as alive and resting, but nothing quite adds up." }, { "stage": "2 Frozen house and family traces", "may_learn": "Clocks are stopped, the calendar is outdated, food never spoils, and places are set for people who are not there. Portraits change." }, { "stage": "3 Sister's warnings", "may_learn": "A figure associated with the sister warns the player to leave. Her concern and wish to preserve the family become apparent." }, { "stage": "4 Parents' fragments", "may_learn": "Slow knocking and footsteps come from behind the locked bedroom door. The parents are not trying to frighten the player; they want the repeating memory to end." }, { "stage": "5 Reconstructed truth", "may_learn": "The door opens and the player sees the family, preserved as if asleep. The friend faces what he did not do." } ], "design_constraints": { "mvp": { "rooms": "6: Entrance Hall, Library, Kitchen, Grand Staircase, Upper Landing, Parents' Bedroom (locked)", "key_clues": "13 clues in three fragments (routine, rules, night)", "puzzles": "1: the parents' bedroom door", "major_ghosts": 1, "endings": "4 implemented (leave_together, escape_alone, denial, sisters_bargain)" }, "player_agency": [ "The player can investigate, talk, examine objects, rest, follow or refuse warnings, and leave when possible.", "Do not force one moral choice after the truth is discovered.", { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 248, 263 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -248,16 +248,16 @@ "summary": "The house's first owner buried his sister before she was truly gone and died soon after. The house stood empty for years before it was put on the market and the family moved in.", "delivery": "Found only in a water-stained local history in the library. Never explained by characters. The house is still haunted by this and other past tragedies." }, "memory_fragments": { "description": "Three kinds of clue. Finding two clues of a kind completes that fragment. The sidebar tracks each category and the total out of three. Completing all three lets the player try the bedroom door.", "routine": "The friend is still keeping house for people who are not there. Clues: third cup, pulled-out chair, food that never spoils.", "rules": "The father's preservation. Clues: the stopped hall clock, the outdated calendar, the note on the bedroom door.", "night": "What happened at the door. Clues: repeating footsteps overhead, the humming, the slow knocking from inside the bedroom." }, "engine_rules": { "denial": "Friend's denial runs 0-10. It starts high and drops by 1 for each clue found and each of up to 3 kind gestures. Blunt confrontation before all three fragments raises it by 1.", "door": "The bedroom door opens only if all three fragments are complete and denial is at or below 3. Opening it moves the story to the final stage.", "family_revelations": "After the player enters the bedroom, face-to-face conversation shares four authored family memories one at a time when denial is below 3: the mother's decline and the family's deaths, the father's attempt to preserve them, the sister's hope, and the friend's conflict and silence.", "together_ending": "The friend can leave together only after denial is below 3 and all four family memories have been shared.", "sister_bargain": "After the sister has warned the player at least twice, when the cooldown passes and the friend is elsewhere, she may privately explain her belief that preserving the family keeps them alive. She remains for five conversation turns, then vanishes; the player can still explicitly accept the bargain after she leaves, while the friend is elsewhere. This conversation and ending may happen before or after the bedroom is opened. The player leaves alone with her partial truth.", "pushing": "Three blunt confrontations before the evidence ends the story in the denial ending.", { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 232, 310 ] } 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 @@ -232,79 +232,79 @@ "img": "staircase.jpg", "aliases": ["grand staircase", "staircase", "stairs", "upstairs"], "features": { "portrait": { "ambient": True, "keywords": ["portrait", "painting", "picture"], "looks": [ "The portrait hangs crookedly. The faces are hard to make out in the dim light.", "The portrait is different from before. Something about the faces has shifted, though you can't say what.", ], "clues": [("The portrait on the landing changed between looks", 2, None)], "friend": "He will not look at it.", }, "humming": { "ambient": True, "keywords": ["humming", "hum", "music", "voice", "melody", "listen"], "looks": [ "The humming drifts down from above. When you look up, it falters, then starts again from the beginning.", "The same melody, again, from the very beginning.", ], "clues": [("The humming upstairs falters whenever you listen", 1, 'night')], "friend": "He seems not to hear it, or says it is only the house.", }, }, }, "Upper Landing": { "description": ( "The upper landing is long and narrow, lit by a single grey window. " "Every door along it is shut. At the far end, a folded note is taped " "to the frame of one door." ), "exits": ["Grand Staircase", "Parents' Bedroom"], "img": "upper_landing.jpg", "gated": {"Parents' Bedroom": "door_open"}, "aliases": ["upper landing", "upstairs landing", "landing", "upper floor", "second floor", "upstairs", "first floor", "second story"], "features": { "door": { "ambient": True, "keywords": ["door", "note", "knock", "knocking"], "takeable": "Upper Landing Note", "looks": [ "The parents' bedroom door is shut. The folded note taped to its frame reads, in a careful hand:\n'Do not open\n\nDo not change\n\nI have to keep things as they are.'", "You press your ear to the wood. From inside comes a slow knock, three times. Then nothing.", ], "clues": [ ("A note on the parents' bedroom door reads: do not open; do not change; I have to keep things as they are", 1, "rules"), ("Slow knocking comes from inside the locked bedroom", 2, "night"), ], "friend": "He will not come near the door.", }, }, }, "Parents' Bedroom": { "description": ("The parents' bedroom is cold and perfectly still. Three figures lie together on the bed, turned toward one another, as if asleep. No dust has settled on anything here. The air smells faintly of cold metal."), "exits": ["Upper Landing"], "img": "bedroom.jpg", "aliases": ["parents' bedroom", "parents bedroom", "parent's bedroom", "parents' room", "parents room", "master bedroom", "bedroom"], "features": { "bed": { "keywords": ["bed", "figures", "family", "mother", "father", "sister", "parents"], "looks": ["Three figures lie together on the bed, turned toward one another, as if asleep. None of them breathes, and nothing about them has changed at all."], },
},
},
}
CLUE_FRAGMENT = { text: frag for room in ROOMS.values() for feat in room.get("features", {}).values() for (text, _n, frag) in feat.get("clues", []) if frag }
BACK = { { "skill": "python-fact-grounded-coding" } Skill "python-fact-grounded-coding" loaded successfully. Follow the instructions in the skill context. {"pattern":"FRAGMENT_CLUES_NEEDED|FRAGMENT_KINDS|FRAGMENT_LABELS|CLUE_FRAGMENT|fragment_progress|fragment","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers"],"g... [grep content: 35 matches across 3 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
prompt.py (5 match(es)): 35: - Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech. 70: { # 4 - Parents' fragments 71: "now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.", 77: "friend": "His denial breaks only as he becomes ready. He speaks in halting fragments and quiet realization, not speeches. If denial is still 3 or higher, he resists and cannot share the next family memory. When denial falls below 3, the ENGINE RESULT may supply one authored family memory; include it honestly and clearly, without adding facts. He does not blame the player. He never agrees to leave together unless the engine ends the story.", 116: return "cracking: fragments of memory slip out and he is visibly shaken"
app.py (13 match(es)): 11: FRAGMENT_LABELS, 12: FRAGMENT_KINDS, 13: FRAGMENT_CLUES_NEEDED, 16: fragment_progress, 17: fragments_complete, 180: st.markdown("### 🧩 Memory fragments") 181: completed_fragments = len(fragments_complete(game)) 182: st.write(f"{completed_fragments}/3 complete") 183: fragment_status = fragment_progress(game) 184: for kind in FRAGMENT_KINDS: 185: progress = fragment_status[kind] 188: f"{marker} {FRAGMENT_LABELS[kind]}: " 189: f"{progress['found']}/{FRAGMENT_CLUES_NEEDED} clues"
engine.py (17 match(es)): 13: FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment 14: FRAGMENT_KINDS = ("routine", "rules", "night") 15: FRAGMENT_LABELS = { 300: # Which fragment each clue belongs to, built from ROOMS. 301: CLUE_FRAGMENT = { 362: def fragment_progress(game): 363: """Return clue counts and completion status for each memory fragment.""" 364: counts = {kind: 0 for kind in FRAGMENT_KINDS} 366: frag = CLUE_FRAGMENT.get(clue) 372: "complete": counts[kind] >= FRAGMENT_CLUES_NEEDED, 374: for kind in FRAGMENT_KINDS 378: def fragments_complete(game): 379: """Set of completed fragment kinds ('routine', 'rules', 'night').""" 382: for kind, progress in fragment_progress(game).items() 592: and len(fragments_complete(game)) >= 3): 629: if len(fragments_complete(game)) < 3: 654: if len(fragments_complete(game)) >= 3: { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 80, 310 ] } 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 @@ -80,231 +80,231 @@ ROOMS = { "Entrance Hall": { "description": ( "The moment you step inside, a gust of wind slips past you. The heavy front door slams shut behind you with a deafening BANG. The sound echoes through the vast entrance hall, bouncing off the high ceilings before dissolving into silence. \n\n" "A figure emerges from the shadows. Your friend.\n" "He looks nothing like the person you remember. His eyes are hollow, his skin pale, ghostly, and dark circles hollow his eyes.\n\n" ""You came," he whispers, his voice trembling. "I didn't think you would... Thank you."\n\n" ), "exits": ["Library", "Kitchen", "Grand Staircase"], "img": "hallway.jpg", "aliases": ["entrance hall", "entrance", "hallway", "hall", "foyer"], "features": { "smell": { "ambient": True, "keywords": ["smell", "scent", "odor", "musty"], "looks": ["A musty smell lingers in the air, a mixture of old wood and dust."], "clues": [("A musty smell lingers in the air", 1, None)], }, "clock": { "ambient": True, "keywords": ["clock", "clocks", "pendulum", "ticking"], "looks": [ "A tall clock stands against the wall, its hands stopped at a quarter past three. The pendulum hangs perfectly still. The clock hasn't moved. Nothing in this hall ticks.", ], "clues": [("The clock on the wall is frozen at 3:15", 1, 'rules')], "friend": "He says it only needs winding, then changes the subject.", }, }, }, "Library": { "description": ( "Shelves of warped books climb toward a ceiling lost " "in shadow. One narrow gap interrupts the dust on a " "lower shelf, as though a book was recently removed. Family photos are hanging on the walls, but the faces are blurred and unrecognizable. " ), "exits": ["Entrance Hall"], "img": "library.jpg", "aliases": ["library"], "features": { "shelf": { "ambient": True, "keywords": ["shelf", "shelves", "gap", "books", "bookshelf", "bookshelves"], "looks": [ "The dust on a lower shelf is disturbed by a narrow, clean gap where a book was recently removed.", "The gap is still there. Nothing nearby explains where the missing book went.", ], "clues": [("A book was recently removed from the shelf", 1, None)], "friend": "He says he doesn't remember taking a book, and changes the subject.", }, "footsteps": { "ambient": True, "keywords": ["footsteps", "steps", "sound", "sounds", "ceiling", "overhead", "listen"], "looks": [ "From somewhere above, a few slow footsteps cross the ceiling. After a , the very same footsteps cross it again.", "The footsteps repeat, exactly the same, as though something is replaying.", ], "clues": [("The same few footsteps repeat overhead", 1, 'night')], "friend": "He says it is only the house settling, and changes the subject.", }, "photos": { "ambient": True, "keywords": ["photo", "photos", "picture", "pictures", "frame", "frames"], "looks": [ "The family photos are blurred and unrecognizable. You can't make out any faces.", "The photos remain blurred. You can't tell who they are.", ], "clues": [("Family photos on the wall are blurred and unrecognizable", 1, None)], "friend": "He says the photos are just old and faded.", },
"history": {
"ambient": True,
"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.",
"The page hasn't changed. The ink is faded, but the passage is still legible.",
],
"clues": [("The house's first owner buried his sister alive and died soon after; the house stood empty for years", 1, None)],
},
},
},
"Kitchen": {
"description": (
"The narrow kitchen is cluttered but familiar. Rain ticks "
"against a dark window above the sink. You remember this kitchen, as if you had never left."
),
"exits": ["Entrance Hall"],
"img": "kitchen.jpg",
"aliases": ["kitchen"],
"features": {
"cups": {
"ambient": True,
"keywords": ["cup", "cups", "mug", "mugs", "teapot", "kettle", "counter"],
"looks": [
"Two cups wait by the teapot. Then you notice a third at the far end of the table, set out and untouched.",
"The third cup hasn't moved. It sits exactly where someone would put it for a guest.",
],
"clues": [("A third cup sits set out at the table, as if someone else were expected", 1, 'routine')],
"friend": "If he notices the player noticing, he glances away and changes the subject.",
},
"chair": {
"ambient": True,
"keywords": ["chair", "chairs", "table", "seat"],
"looks": [
"One chair at the table is pulled out slightly, as though someone had only just stood up from it.",
"The chair stays where it is. The seat is cold.",
],
"clues": [("A kitchen chair is pulled out, as though someone had just stood up", 1, 'routine')],
},
"food": {
"ambient": True,
"keywords": ["food", "plate", "plates", "dish", "dishes"],
"looks": [
"A plate of food sits on the counter, untouched. The smell is faint but familiar.",
"The plate of food remains. It is cold, but the smell is still faintly familiar.",
"It seems like the food has been sitting there for a while, but it isn't spoiled."
],
"clues": [("A plate of food sits on the counter, untouched", 1, 'routine'), ("The food has sat out for ages but has not spoiled", 3, None)],
},
"calendar": {
"ambient": True,
"keywords": ["calendar", "date", "dates"],
"looks": [
"The calendar on the wall is turned to March, but the year is faded and unreadable. It is currently October.",
],
"clues": [("The calendar on the wall is turned to March, but it is currently October. Year is faded and unreadable", 1, 'rules')],
},
},
},
"Grand Staircase": {
"description": (
"The staircase groans beneath your weight. At the "
"landing, a portrait hangs crookedly. From above "
"comes the faint sound of someone humming."
),
"exits": ["Entrance Hall", "Upper Landin
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 248, 262 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -248,15 +248,15 @@ "summary": "The house's first owner buried his sister before she was truly gone and died soon after. The house stood empty for years before it was put on the market and the family moved in.", "delivery": "Found only in a water-stained local history in the library. Never explained by characters. The house is still haunted by this and other past tragedies." }, "memory_fragments": { "description": "Three kinds of clue. Finding two clues of a kind completes that fragment. The sidebar tracks each category and the total out of three. Completing all three lets the player try the bedroom door.", "routine": "The friend is still keeping house for people who are not there. Clues: third cup, pulled-out chair, food that never spoils.", "rules": "The father's preservation. Clues: the stopped hall clock, the outdated calendar, the note on the bedroom door.", "night": "What happened at the door. Clues: repeating footsteps overhead, the humming, the slow knocking from inside the bedroom." }, "engine_rules": { "denial": "Friend's denial runs 0-10. It starts high and drops by 1 for each clue found and each of up to 3 kind gestures. Blunt confrontation before all three fragments raises it by 1.", "door": "The bedroom door opens only if all three fragments are complete and denial is at or below 3. Opening it moves the story to the final stage.", "family_revelations": "After the player enters the bedroom, face-to-face conversation shares four authored family memories one at a time when denial is below 3: the mother's decline and the family's deaths, the father's attempt to preserve them, the sister's hope, and the friend's conflict and silence.", "together_ending": "The friend can leave together only after denial is below 3 and all four family memories have been shared.", "sister_bargain": "After the sister has warned the player at least twice, when the cooldown passes and the friend is elsewhere, she may privately explain her belief that preserving the family keeps them alive. She remains for five conversation turns, then vanishes; the player can still explicitly accept the bargain after she leaves, while the friend is elsewhere. This conversation and ending may happen before or after the bedroom is opened. The player leaves alone with her partial truth.", {"pattern":"13 clues|three fragments|Routine|Rules|Night|memory_fragments|routine|rules|night","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers"],"glob":"*.{md,... [grep content: 33 matches across 4 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
story/canon.json (14 match(es)): 18: "style_rules": [ 62: "characterization": "Lived in denial, believing the others were overreacting. He ignored his father's rules and fell out with him. He obeyed only the last one, not to open the bedroom door, out of anger and fear. He survived, but his sense of time and memory of the final days are distorted.", 63: "guidance": "His progression is governed by explicit game-state rules (denial, fragments, door), not by the model." 77: "supernatural_rules": [ 123: "An extra place setting or family routine that no longer makes sense", 173: "key_clues": "13 clues in three fragments (routine, rules, night)", 222: "Narrate failed actions as failures without quietly changing the rules." 251: "memory_fragments": { 253: "routine": "The friend is still keeping house for people who are not there. Clues: third cup, pulled-out chair, food that never spoils.", 254: "rules": "The father's preservation. Clues: the stopped hall clock, the outdated calendar, the note on the bedroom door.", 255: "night": "What happened at the door. Clues: repeating footsteps overhead, the humming, the slow knocking from inside the bedroom." 257: "engine_rules": { 258: "denial": "Friend's denial runs 0-10. It starts high and drops by 1 for each clue found and each of up to 3 kind gestures. Blunt confrontation before all three fragments raises it by 1.", 259: "door": "The bedroom door opens only if all three fragments are complete and denial is at or below 3. Opening it moves the story to the final stage.",
prompt.py (3 match(es)): 4: 1. CORE static narrator rules (cacheable prefix) 29: Truth rules: 43: "I haven't been sleeping much. The house is loud at night."
engine.py (15 match(es)): 1: """engine.py - authoritative game state and rules. 14: FRAGMENT_KINDS = ("routine", "rules", "night") 16: "routine": "Routine", 17: "rules": "Rules", 18: "night": "Night", 109: "clues": [("The clock on the wall is frozen at 3:15", 1, 'rules')], 141: "clues": [("The same few footsteps repeat overhead", 1, 'night')], 193: "clues": [("A third cup sits set out at the table, as if someone else were expected", 1, 'routine')], 203: "clues": [("A kitchen chair is pulled out, as though someone had just stood up", 1, 'routine')], 213: "clues": [("A plate of food sits on the counter, untouched", 1, 'routine'), ("The food has sat out for ages but has not spoiled", 3, None)], 221: "clues": [("The calendar on the wall is turned to March, but it is currently October. Year is faded and unreadable", 1, 'rules')], 252: "clues": [("The humming upstairs falters whenever you listen", 1, 'night')], 277: ("A note on the parents' bedroom door reads: do not open; do not change; I have to keep things as they are", 1, "rules"), 278: ("Slow knocking comes from inside the locked bedroom", 2, "night"), 379: """Set of completed fragment kinds ('routine', 'rules', 'night').""" app.py:29: 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. {"pattern":"CLUE_FRAGMENT|fragment_progress|new_clue|fragments_complete|FRAGMENT_CLUES_NEEDED","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests"],"output... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:320: def test_fragment_progress_reports_zero_of_three_at_start(self): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:323: progress = engine.fragment_progress(game) C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:325: self.assertEqual(len(engine.fragments_complete(game)), 0) { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", "view_range": [ 300, 350 ] } 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 @@ -300,51 +300,51 @@
self.assertEqual(result["outcome"], "moved")
self.assertTrue(game["truth_known"])
self.assertEqual(game["denial"], 3)
self.assertIn("still resisting it", stage_block(game))
act(game, "We can face this together.")
self.assertEqual(game["denial"], 2)
self.assertIn("share the story gradually", stage_block(game))
def test_unresolved_denial_can_still_trigger_the_denial_ending(self):
game = engine.new_game()
game.update(truth_known=True, denial=3, turn_count=engine.MAX_TURNS - 1)
result = act(game, "I wait quietly.")
self.assertEqual(game["ending"], "denial")
self.assertEqual(result["outcome"], "ending")
def test_fragment_progress_reports_zero_of_three_at_start(self):
game = engine.new_game()
progress = engine.fragment_progress(game)
self.assertEqual(len(engine.fragments_complete(game)), 0)
self.assertEqual(
{kind: item["found"] for kind, item in progress.items()},
{"routine": 0, "rules": 0, "night": 0},
)
self.assertIn("Family memories shared: 0/4", state_block(game))
def test_final_memories_are_shared_in_order_before_together_ending(self):
game = engine.new_game()
game.update(
stage=5,
door_open=True,
room="Parents' Bedroom",
friend_room="Parents' Bedroom",
truth_known=True,
denial=2,
)
blocked = act(game, "Let's leave together.")
self.assertEqual(blocked["outcome"], "blocked")
self.assertIsNone(game["ending"])
self.assertFalse(game["final_revelations"])
for expected in engine.FINAL_REVELATIONS:
result = act(game, "What happened?")
self.assertEqual(result["family_memory"]["id"], expected["id"])
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 300, 390 ] } 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 @@ -300,91 +300,91 @@ CLUE_FRAGMENT = { text: frag for room in ROOMS.values() for feat in room.get("features", {}).values() for (text, _n, frag) in feat.get("clues", []) if frag }
BACK = { "Library": "Entrance Hall", "Kitchen": "Entrance Hall", "Grand Staircase": "Entrance Hall", "Upper Landing": "Grand Staircase", "Parents' Bedroom": "Upper Landing", } VERTICAL = { ("Entrance Hall", "up"): "Grand Staircase", ("Grand Staircase", "up"): "Upper Landing", ("Grand Staircase", "down"): "Entrance Hall", ("Upper Landing", "down"): "Grand Staircase", }
def new_game(): return { "room": "Entrance Hall", "friend_room": "Entrance Hall", "inventory": ["The Letter"], "clues": [], "visited": ["Entrance Hall"], "examined": {}, # "Room:feature" -> times examined "stage": 0, # index into prompt.STAGES "turns_in_stage": 0, "turn_count": 0, "room_turns": 0, # turns spent in the current room since arriving "last_notice_turn": -99, # turn_count when a detail was last surfaced "last_nudge_turn": -99, # turn_count when the friend was last nudged "nudges_here": 0, # nudges in this room since last move (escalate) "last_sister_turn": -99, "sister_warning_count": 0, "sister_bargain_available": False, "sister_bargain_heard": False, "sister_present": False, "sister_conversation_turns": 0, "denial": 8, # friend's denial (high = deflecting) "pushed": 0, "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 "final_revelations": [], # authored family memories shared by the friend "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"}], "summary": "", # rolling summary of folded-away history "summary_idx": 0, # history[:summary_idx] is already summarized }
def fragment_progress(game): """Return clue counts and completion status for each memory fragment.""" counts = {kind: 0 for kind in FRAGMENT_KINDS} for clue in game["clues"]: frag = CLUE_FRAGMENT.get(clue) if frag: counts[frag] = counts.get(frag, 0) + 1 return { kind: { "found": counts[kind], "complete": counts[kind] >= FRAGMENT_CLUES_NEEDED, } for kind in FRAGMENT_KINDS }
def fragments_complete(game): """Set of completed fragment kinds ('routine', 'rules', 'night').""" return { kind for kind, progress in fragment_progress(game).items() if progress["complete"] }
def exit_labels(game, room_name): """Exit names for display, marking locked ones.""" room = ROOMS[room_name] gated = room.get("gated", {}) { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 1, 135 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -1,135 +1,135 @@ { "title": "The House That Remembers", "version": "0.2.0", "purpose": "Authoritative canon for a standalone gothic-horror AI adventure. Python and this file define truth; the model narrates but cannot change canon or game state.", "genre_and_tone": { "genre": [ "gothic horror", "psychological horror", "mystery" ], "tone": [ "intimate", "melancholic", "uncanny", "slow-building", "emotionally grounded" ], "style_rules": [ "Use sensory details and unsettling inconsistencies rather than constant jump scares.", "Let ordinary, comforting moments precede uncanny events.", "Keep horror connected to memory, grief, and denial.", "Do not explain every strange event immediately.", "Allusions to the house's earlier tragedies are welcome as an optional easter egg, but never quote or imitate any specific existing text.", "Never describe wounds, decay or gore. Use stillness, cold and grief instead." ] }, "premise": { "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.", "player_role": "The player is an outsider and the friend's childhood companion. They did not cause the family's tragedy and have no secret responsibility for it.", "core_question": "Can the friend face what happened to his family, and can the player help him let the house's preserved memories go?", "core_conflict": "The parents' lingering spirits want release. The sister's ghost wants to preserve the family and believes keeping memories alive is protective. The friend is caught between denial, fear, and the possibility of remembering." }, "fixed_truths": [ "The friend wrote the letter because his condition and the house's disturbances have become unbearable. The letter was not secretly written by the house. Part of him senses the truth, which is why he asked for help.", "The house is genuinely haunted, and was haunted by earlier tragedies before the family moved in.", "The family moved in when the friend was a child. At first things were ordinary; strange details developed slowly and were brushed off.", "The house drained the family's energy, starting with the mother. It also freezes and distorts time: clocks stop, calendars stay on an old date, food does not spoil, and the friend cannot tell how much time has passed.", "The mother, father and sister are dead. They died inside the parents' bedroom after being locked in and cut off from help, drained by the house. There was no violence. The house preserved the room, so they look as if they are asleep.", "The friend survived. He knocked on the bedroom door once, heard no reply, and did not open it. He has pretended ever since that his family is alive and resting, and the house helps him not to see the truth.", "The friend behaves as though his whole family is alive: his mother is resting, his father is busy, his sister is somewhere around.", "The sister is a ghost. She roams the house, hums, appears intermittently, and warns the player to leave. Her warnings are sincere. She believes preserving the family exactly as it is keeps them alive.", "The parents linger as fragments tied to the bedroom: slow knocking and footsteps. They want release, not endless preservation.", "The friend's warning 'Whatever you do, don't answer it' refers to the knocking from the parents' bedroom.", "As the friend's denial weakens and memories return, reality becomes less consistent: paintings change, familiar spaces seem altered, sounds repeat, and doors or rooms may feel wrong.", "The player can leave when the game state allows it. Leaving early is a valid choice.", "The player's presence gives the friend an external point of reference, but cannot cure him or force him to accept the truth. He must acknowledge it himself." ], "family_history": { "father": { "characterization": "Moved the family to the house. As his wife declined and doctors could not help, he grew paranoid and began to lose himself too. His only idea was to preserve everything as it was: locking doors, stopping clocks, changing nothing. He fell out with his son over the son's denial. In the end he locked himself, his wife and his daughter in the parents' bedroom to 'preserve' them and told his son never to open the door. His attempt to protect them is what killed them.", "guidance": "Show fear and a distorted desire to protect, not simple villainy." }, "mother": { "characterization": "The first to be affected. A stay-at-home mother who spent the most time in the house, she grew depressed and lethargic and could not sleep, as if the house were draining her. She recognized that something was wrong.", "guidance": "Treat her suffering seriously and compassionately, never as a moral failing." }, "sister": { "characterization": "Supportive but cautious. She still went out for food and tried to help the family hold together, and she was protective of her brother, urging him not to give up on them. After death she clings to the family as it is, believing preservation keeps them alive.", "guidance": "Protective, loving, frightened and deluded at once. Not evil." }, "friend": { "characterization": "Lived in denial, believing the others were overreacting. He ignored his father's rules and fell out with him. He obeyed only the last one, not to open the bedroom door, out of anger and fear. He survived, but his sense of time and memory of the final days are distorted.", "guidance": "His progression is governed by explicit game-state rules (denial, fragments, door), not by the model." }, "tragedy_details": { "status": "AUTHORED", "truth": "After the father and son stopped speaking, the father locked himself, the mother and the sister in the parents' bedroom and told the son never to open the door. Cut off from help and drained by the house, they died inside. The house preserved the room as it was. The friend knocked once, heard nothing, and never opened the door, then convinced himself the family was alive.", "timing": "Recent but deliberately unfixed. The house distorts time, so never give a number of days.", "narration_guidance": [ "Reveal the truth only in the final stage, after the bedroom door is opened by the engine.", "Before that, use oblique fragments only: knocking, repeating footsteps, humming, stopped clocks.", "Never describe wounds, decay or gore. The family looks as if they are sleeping.", "The friend should face the truth in halting fragments and quiet realization, not a lecture." ] } }, "supernatural_rules": [ { "name": "Memory imprint", "description": "Rooms, paintings, objects, and sounds can preserve emotional impressions of past events." }, { "name": "Distortion", "description": "The house can replay, rearrange, or distort remembered details. An apparent memory is not automatically a literal recording of objective events." }, { "name": "Escalation", "description": "The house becomes less consistent as the friend's denial breaks down and memories return. Tie escalation to game state or story milestones." }, { "name": "No free resolution", "description": "The narrator cannot declare a puzzle solved, a door unlocked, a clue acquired, a spirit released, or a character dead unless the engine authorizes it." }, { "name": "No omniscient player", "description": "Distinguish hidden truth from what the player has discovered. Do not reveal secrets prematurely." } ], "character_dynamics": { "friend": { "initial_state": "Pale, exhausted, frightened, and genuinely glad the player has arrived. He wants help but is not ready to face everything.", "personality": "Anxious, frightened, withdrawn, slee { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 165, 185 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -165,21 +165,21 @@ { "stage": "5 Reconstructed truth", "may_learn": "The door opens and the player sees the family, preserved as if asleep. The friend faces what he did not do." } ], "design_constraints": { "mvp": { "rooms": "6: Entrance Hall, Library, Kitchen, Grand Staircase, Upper Landing, Parents' Bedroom (locked)", "key_clues": "13 clues in three fragments (routine, rules, night)", "puzzles": "1: the parents' bedroom door", "major_ghosts": 1, "endings": "4 implemented (leave_together, escape_alone, denial, sisters_bargain)" }, "player_agency": [ "The player can investigate, talk, examine objects, rest, follow or refuse warnings, and leave when possible.", "Do not force one moral choice after the truth is discovered.", "The player may discover the truth and still decide what to do next.", "The narrator responds flexibly to unusual actions, but the engine decides whether those actions succeed." ], "engine_authority": [ "Python owns location, exits, inventory, clues, puzzle state, door locks, chapter/time progression, character condition, trust, denial, and ending eligibility.", {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -c "import engine, collections; rows=[(room, feature, clue, category) for room,data i... total clues: 15 classified: 9 by category: Counter({None: 6, 'rules': 3, 'night': 3, 'routine': 3}) unclassified: Entrance Hall / smell : A musty smell lingers in the air Library / shelf : A book was recently removed from the shelf Library / photos : Family photos on the wall are blurred and unrecognizable Library / history : The house's first owner buried his sister alive and died soon after; the house stood empty for years Kitchen / food : The food has sat out for ages but has not spoiled Grand Staircase / portrait : The portrait on the landing changed between looks <shellId: 87 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", "view_range": [ 1, 55 ] } 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 @@ -1,55 +1,55 @@ import contextlib import io import json import unittest from pathlib import Path
import engine from prompt import _engine_note, stage_block, 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_supportive_phrases_increase_support(self): phrases = [ "We can do it together.", "We can face this together.", "It's going to be okay.", "It is going to be alright.", "I want to help.", "I can help.", "Take my hand.", "Give me your hand.", ]
for phrase in phrases:
with self.subTest(phrase=phrase):
game = engine.new_game()
act(game, phrase)
self.assertEqual(game["support_given"], 1)
self.assertEqual(game["denial"], 7)
def test_supportive_dialogue_brings_remote_friend_to_player(self):
game = engine.new_game()
game["room"] = "Upper Landing"
game["friend_room"] = "Library"
result = act(game, "I can help.")
self.assertEqual(result["intent"], "dialogue")
self.assertTrue(result["friend_arrived"])
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertEqual(game["support_given"], 1)
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 845, 870 ] } 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 @@ -845,26 +845,26 @@ )
STAGE_RULES = [ lambda g: g["turns_in_stage"] >= 5 and len(g["visited"]) >= 2, # 0 arrival lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 1, # 1 contradictions lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 4, # 2 paintings lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 7, # 3 sister's warnings ]
def _advance_stage(game): if game["door_open"] and game["stage"] < 5: game["stage"] = 5 game["turns_in_stage"] = 0 return True if game["stage"] >= 4: return False if STAGE_RULESgame["stage"]: game["stage"] += 1 game["turns_in_stage"] = 0 return True { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\README.md", "view_range": [ 45, 62 ] } 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,18 +45,18 @@
- Combine speech and an action:
I pat him on the back and say, "Let's go." - Explore or examine:
Look aroundorExamine the family photos. - Recall an established memory:
What do I remember about my friend? - 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 sidebar tracks the three memory fragments needed to open the bedroom door. After entering, the friend shares four family memories gradually; the two of you can leave together once he is ready and has faced the full story.
After two sister-ghost warnings, she may offer a private conversation when your friend is elsewhere. You can hear her belief that preserving the family keeps them alive, then explicitly accept her bargain to leave alone with that partial truth. She stays for up to five conversation turns; after she vanishes, you can still accept the bargain or leave normally. This optional path is possible {"pattern":"key_clues|13 clues|10 fragment|memory_fragments|clue count|three fragments","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers"],"glob":"*.{json,md,py... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:173: "key_clues": "13 clues in three fragments (routine, rules, night)", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:251: "memory_fragments": { C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:258: "denial": "Friend's denial runs 0-10. It starts high and drops by 1 for each clue found and each of up to 3 kind gestures. Blunt confrontation before all three fragments raises it by 1.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:259: "door": "The bedroom door opens only if all three fragments are complete and denial is at or below 3. Opening it moves the story to the final stage.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:363: """Return clue counts and completion status for each memory fragment.""" *** Begin Patch *** Update File: the-house-that-remembers/engine.py @@ -# "features" are things the player can examine/take. "looks" is what each -# successive examination reveals (the last entry repe... *** Begin Patch *** Update File: README.md @@ The sidebar tracks the three memory fragments needed to open the bedroom door. +Two clues in each category complete a fragment; the house also holds +atm... 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 @@ -52,6 +52,9 @@ narrator should not invent shared history.
The sidebar tracks the three memory fragments needed to open the bedroom door. +Two clues in each category complete a fragment; the house also holds +atmospheric and optional lore discoveries that do not count toward fragment +progress. After entering, the friend shares four family memories gradually; the two of you can leave together once he is ready and has faced the full story.
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 @@ -2,6 +2,7 @@ import io import json import unittest +from collections import Counter from pathlib import Path
import engine @@ -329,6 +330,34 @@ ) self.assertIn("Family memories shared: 0/4", state_block(game))
- def test_fragment_clue_categories_match_the_authored_discoveries(self):
-
clues = [ -
(text, fragment) -
for room in engine.ROOMS.values() -
for feature in room.get("features", {}).values() -
for text, _examination_number, fragment in feature.get("clues", []) -
] -
categorized_counts = Counter( -
fragment for _text, fragment in clues if fragment is not None -
) -
uncategorized = {text for text, fragment in clues if fragment is None} -
self.assertEqual( -
categorized_counts, -
{"routine": 4, "rules": 3, "night": 3}, -
) -
self.assertEqual(len(clues), 15) -
self.assertEqual( -
uncategorized, -
{ -
"A musty smell lingers in the air", -
"A book was recently removed from the shelf", -
"Family photos on the wall are blurred and unrecognizable", -
"The house's first owner buried his sister alive and died soon after; the house stood empty for years", -
"The portrait on the landing changed between looks", -
}, -
) - def test_final_memories_are_shared_in_order_before_together_ending(self): game = engine.new_game() game.update( {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run game engine regr... .....................
Ran 21 tests in 0.009s
OK <shellId: 88 completed with exit code 0> {"command":"python -m py_compile 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py' 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remem... Python and canon JSON validation passed <shellId: 90 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- README.md the-house-that-remembers/engine.py the-house-that-remembers/story/canon.json t... warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it M .gitignore M README.md M the-house-that-remembers/app.py M the-house-that-remembers/engine.py M the-house-that-remembers/prompt.py M the-house-that-remembers/story/canon.json ?? the-house-that-remembers/assets/book.png ?? the-house-that-remembers/assets/letter.png ?? the-house-that-remembers/assets/note.png <shellId: 89 completed with exit code 0> {"pattern":"food has sat out|Fragment progress includes|key_clues|Ten clues|routine\":|Five additional|atmospheric and optional lore","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\... C:\Users\lucia\Documents\coding stuff\hacktober\README.md:56:atmospheric and optional lore discoveries that do not count toward fragment C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:173: "key_clues": "10 fragment clues across three threads: Routine (4), Rules (3), and Night (3). Five additional discoveries provide atmosphere or optional lore but do not count toward fragment completion.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:252: "description": "Ten clues are assigned to three kinds of fragment: Routine (4), Rules (3), and Night (3). Finding any two clues in a kind completes that fragment. Five additional discoveries provide atmosphere or optional lore and do not count toward fragment completion. The sidebar tracks each category and the total out of three. Completing all three lets the player try the bedroom door.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:253: "routine": "The friend is still keeping house for people who are not there. Clues: third cup, pulled-out chair, untouched food, and food that never spoils.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:16: "routine": "Routine", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:214: "clues": [("A plate of food sits on the counter, untouched", 1, 'routine'), ("The food has sat out for ages but has not spoiled", 3, 'routine')], C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:301:# Fragment progress includes only clues with an assigned fragment kind. {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff -- README.md the-house-that-remembers/engine.py the-house-that-remembers/story/canon.json","descrip... Output too large to read at once (39.1 KB). Saved to: C:\Users\lucia\AppData\Local\Temp\1793220238071-copilot-tool-output-8656-3431527b-20a4-4c88-8db9-68cb635c02f7.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): warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it diff --git a/README.md b/README.md index f0deb4a..f68289b 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,32 @@ language:
- Combine <shellId: 91 completed with exit code 0> *** Begin Patch *** Update File: the-house-that-remembers/story/canon.json @@
- "description": "Ten clues are assigned to three kinds of fragment: Routine (4), Rules (3), and Night (3). Finding any... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -252,7 +252,8 @@ "description": "Ten clues are assigned to three kinds of fragment: Routine (4), Rules (3), and Night (3). Finding any two clues in a kind completes that fragment. Five additional discoveries provide atmosphere or optional lore and do not count toward fragment completion. The sidebar tracks each category and the total out of three. Completing all three lets the player try the bedroom door.", "routine": "The friend is still keeping house for people who are not there. Clues: third cup, pulled-out chair, untouched food, and food that never spoils.", "rules": "The father's preservation. Clues: the stopped hall clock, the outdated calendar, the note on the bedroom door.",
- "night": "What happened at the door. Clues: repeating footsteps overhead, the humming, the slow knocking from inside the bedroom."
- "night": "What happened at the door. Clues: repeating footsteps overhead, the humming, the slow knocking from inside the bedroom.",
- "other_discoveries": "These do not count toward a memory fragment: the musty smell, the missing-book gap, the blurred family photos, the local-history account of the previous owner, and the changing portrait." }, "engine_rules": { "denial": "Friend's denial runs 0-10. It starts high and drops by 1 for each clue found and each of up to 3 kind gestures. Blunt confrontation before all three fragments raises it by 1.", python {"command":"python -c "import json; json.load(open(r'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json', encoding='utf-8')); print('Canon JSON is valid... Canon JSON is valid <shellId: 92 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- README.md the-house-that-remembers/engine.py the-house-that-remembers/story/canon.json",... warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it <shellId: 93 completed with exit code 0>
Okat, that's good. Now I would think about how to call those categories.
What about: Past fragments - calendar, clock, book history, family photos
Present fragments - third cup, food plate, chair, ... but dunno about the third one. Let's discuss, dont change it right now
**Past / Present / Echoes** feels like a promising set. It gives each category a clear idea, and it fits the clues better than *Routine / Rules / Night*:
One wrinkle: the book history is about the house’s **previous owner**, not this family’s past. It could stay an optional discovery, rather than counting toward *Past*. The family photos could be optional too—or become a Past clue if you want that thread to include them.
Mechanically, I’d preserve the current balance of **4 / 3 / 3 clues** across the categories. The note on the bedroom door is currently a *Rules* clue; if we move toward Past / Present / Echoes, we’d need to decide where it belongs. It might fit Past as a trace of the father’s decision, though its meaning points toward preservation and “keeping things as they are.”
So my tentative recommendation is **Past / Present / Echoes**, with the book history remaining optional. That gives the three threads a shared framing without making every discovery count toward them.
I agree. Note can be in the past, but balance out the number of clues, or change how many are required
Past: clock, calendar, family photos, note
Present: third cup, chair, food
Echoes: Footsteps, humming, knocking, maybe voices, crying etc
Optional: book, smell, the rest of them
That gives us a clear structure: **Past / Present / Echoes**, with optional discoveries outside those threads.
One small count detail: the engine currently treats the food as **two separate discoveries**—the untouched plate, then later the fact it hasn’t spoiled. So the counts would be:
The family photos are currently optional, so they’d need to become a Past clue to match your proposed grouping. The book-history clue would remain optional, as you suggested.
I’d keep the requirement at **2 clues per category**. That gives the player some choice in what to investigate without making the Echoes thread require every single clue. It also keeps the existing progression difficulty: six categorized discoveries to complete the three fragments. If we later add voices or crying, they can expand Echoes without forcing us to raise the requirement.
The photo clue may need thought in the framing: blurred faces read as lost memory more than straightforward history, but that still fits **Past** thematically. No changes made.
Okay, I agree. Implement changes
I’ll update the category names and assignments consistently across engine, sidebar, canon, docs, and tests, then verify the category counts and regression suite.
{"pattern":"routine|rules|night|Routine|Rules|Night|FRAGMENT_KINDS|FRAGMENT_LABELS|fragment_clue_categories|categorized_counts","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-hous... [grep content: 38 matches across 4 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
app.py (5 match(es)): 11: FRAGMENT_LABELS, 12: FRAGMENT_KINDS, 29: 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. 184: for kind in FRAGMENT_KINDS: 188: f"{marker} {FRAGMENT_LABELS[kind]}: "
engine.py (18 match(es)): 1: """engine.py - authoritative game state and rules. 14: FRAGMENT_KINDS = ("routine", "rules", "night") 15: FRAGMENT_LABELS = { 16: "routine": "Routine", 17: "rules": "Rules", 18: "night": "Night", 110: "clues": [("The clock on the wall is frozen at 3:15", 1, 'rules')], 142: "clues": [("The same few footsteps repeat overhead", 1, 'night')], 194: "clues": [("A third cup sits set out at the table, as if someone else were expected", 1, 'routine')], 204: "clues": [("A kitchen chair is pulled out, as though someone had just stood up", 1, 'routine')], 214: "clues": [("A plate of food sits on the counter, untouched", 1, 'routine'), ("The food has sat out for ages but has not spoiled", 3, 'routine')], 222: "clues": [("The calendar on the wall is turned to March, but it is currently October. Year is faded and unreadable", 1, 'rules')], 253: "clues": [("The humming upstairs falters whenever you listen", 1, 'night')], 278: ("A note on the parents' bedroom door reads: do not open; do not change; I have to keep things as they are", 1, "rules"), 279: ("Slow knocking comes from inside the locked bedroom", 2, "night"), 365: counts = {kind: 0 for kind in FRAGMENT_KINDS} 375: for kind in FRAGMENT_KINDS 380: """Set of completed fragment kinds ('routine', 'rules', 'night')."""
prompt.py (3 match(es)): 4: 1. CORE static narrator rules (cacheable prefix) 29: Truth rules: 43: "I haven't been sleeping much. The house is loud at night."
story/canon.json (12 match(es)): 18: "style_rules": [ 62: "characterization": "Lived in denial, believing the others were overreacting. He ignored his father's rules and fell out with him. He obeyed only the last one, not to open the bedroom door, out of anger and fear. He survived, but his sense of time and memory of the final days are distorted.", 63: "guidance": "His progression is governed by explicit game-state rules (denial, fragments, door), not by the model." 77: "supernatural_rules": [ 123: "An extra place setting or family routine that no longer makes sense", 173: "key_clues": "10 fragment clues across three threads: Routine (4), Rules (3), and Night (3). Five additional discoveries provide atmosphere or optional lore but do not count toward fragment completion.", 222: "Narrate failed actions as failures without quietly changing the rules." 252: "description": "Ten clues are assigned to three kinds of fragment: Routine (4), Rules (3), and Night (3). Finding any two clues in a kind completes that fragment. Five additional discoveries provide atmosphere or optional lore and do not count toward fragment completion. The sidebar tracks each category and the total out of three. Completing all three lets the player try the bedroom door.", 253: "routine": "The friend is still keeping house for people who are not there. Clues: third cup, pulled-out chair, untouched food, and food that never spoils.", 254: "rules": "The father's preservation. Clues: the stopped hall clock, the outdated calendar, the note on the bedroom door.", 255: "night": "What happened at the door. Clues: repeating footsteps overhead, the humming, the slow knocking from inside the bedroom.", 258: "engine_rules": { { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 1, 22 ] } 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 @@ -1,22 +1,22 @@ """engine.py - authoritative game state and rules.
Nothing in here calls the model. The model only ever sees the result of what this module decides (see prompt.py). """ import re from pprint import pprint
AMBIENT_AFTER_TURNS = 3 # turns spent in a room before a detail may surface AMBIENT_COOLDOWN = 3 # minimum turns between any two noticed details FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment FRAGMENT_KINDS = ("routine", "rules", "night") FRAGMENT_LABELS = { "routine": "Routine", "rules": "Rules", "night": "Night", } DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door PUSH_LIMIT = 3 # blunt confrontations before the evidence -> he shuts down SUPPORT_LIMIT = 3 # times kindness can lower his denial { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 96, 285 ] } 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 @@ -96,190 +96,190 @@ "aliases": ["entrance hall", "entrance", "hallway", "hall", "foyer"], "features": { "smell": { "ambient": True, "keywords": ["smell", "scent", "odor", "musty"], "looks": ["A musty smell lingers in the air, a mixture of old wood and dust."], "clues": [("A musty smell lingers in the air", 1, None)], }, "clock": { "ambient": True, "keywords": ["clock", "clocks", "pendulum", "ticking"], "looks": [ "A tall clock stands against the wall, its hands stopped at a quarter past three. The pendulum hangs perfectly still. The clock hasn't moved. Nothing in this hall ticks.", ], "clues": [("The clock on the wall is frozen at 3:15", 1, 'rules')], "friend": "He says it only needs winding, then changes the subject.", }, }, }, "Library": { "description": ( "Shelves of warped books climb toward a ceiling lost " "in shadow. One narrow gap interrupts the dust on a " "lower shelf, as though a book was recently removed. Family photos are hanging on the walls, but the faces are blurred and unrecognizable. " ), "exits": ["Entrance Hall"], "img": "library.jpg", "aliases": ["library"], "features": { "shelf": { "ambient": True, "keywords": ["shelf", "shelves", "gap", "books", "bookshelf", "bookshelves"], "looks": [ "The dust on a lower shelf is disturbed by a narrow, clean gap where a book was recently removed.", "The gap is still there. Nothing nearby explains where the missing book went.", ], "clues": [("A book was recently removed from the shelf", 1, None)], "friend": "He says he doesn't remember taking a book, and changes the subject.", }, "footsteps": { "ambient": True, "keywords": ["footsteps", "steps", "sound", "sounds", "ceiling", "overhead", "listen"], "looks": [ "From somewhere above, a few slow footsteps cross the ceiling. After a , the very same footsteps cross it again.", "The footsteps repeat, exactly the same, as though something is replaying.", ], "clues": [("The same few footsteps repeat overhead", 1, 'night')], "friend": "He says it is only the house settling, and changes the subject.", }, "photos": { "ambient": True, "keywords": ["photo", "photos", "picture", "pictures", "frame", "frames"], "looks": [ "The family photos are blurred and unrecognizable. You can't make out any faces.", "The photos remain blurred. You can't tell who they are.", ], "clues": [("Family photos on the wall are blurred and unrecognizable", 1, None)], "friend": "He says the photos are just old and faded.", },
"history": {
"ambient": True,
"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.",
"The page hasn't changed. The ink is faded, but the passage is still legible.",
],
"clues": [("The house's first owner buried his sister alive and died soon after; the house stood empty for years", 1, None)],
},
},
},
"Kitchen": {
"description": (
"The narrow kitchen is cluttered but familiar. Rain ticks "
"against a dark window above the sink. You remember this kitchen, as if you had never left."
),
"exits": ["Entrance Hall"],
"img": "kitchen.jpg",
"aliases": ["kitchen"],
"features": {
"cups": {
"ambient": True,
"keywords": ["cup", "cups", "mug", "mugs", "teapot", "kettle", "counter"],
"looks": [
"Two cups wait by the teapot. Then you notice a third at the far end of the table, set out and untouched.",
"The third cup hasn't moved. It sits exactly where someone would put it for a guest.",
],
"clues": [("A third cup sits set out at the table, as if someone else were expected", 1, 'routine')],
"friend": "If he notices the player noticing, he glances away and changes the subject.",
},
"chair": {
"ambient": True,
"keywords": ["chair", "chairs", "table", "seat"],
"looks": [
"One chair at the table is pulled out slightly, as though someone had only just stood up from it.",
"The chair stays where it is. The seat is cold.",
],
"clues": [("A kitchen chair is pulled out, as though someone had just stood up", 1, 'routine')],
},
"food": {
"ambient": True,
"keywords": ["food", "plate", "plates", "dish", "dishes"],
"looks": [
"A plate of food sits on the counter, untouched. The smell is faint but familiar.",
"The plate of food remains. It is cold, but the smell is still faintly familiar.",
"It seems like the food has been sitting there for a while, but it isn't spoiled."
],
"clues": [("A plate of food sits on the counter, untouched", 1, 'routine'), ("The food has sat out for ages but has not spoiled", 3, 'routine')],
},
"calendar": {
"ambient": True,
"keywords": ["calendar", "date", "dates"],
"looks": [
"The calendar on the wall is turned to March, but the year is faded and unreadable. It is currently October.",
],
"clues": [("The calendar on the wall is turned to March, but it is currently October. Year is faded and unreadable", 1, 'rules')],
},
},
},
"Grand Staircase": {
"description": (
"The staircase groans beneath your weight. At the "
"landing, a portrait hangs crookedly. From above "
"comes the faint sound of someone humming."
),
"exits": ["Entrance Hall", "Upper Landing"],
"img": "staircase.jpg",
"aliases": ["grand staircase", "staircase", "stairs", "upstairs"],
"features": {
"portrait": {
"ambient": True,
"keywords": ["portrait", "painting", "picture"],
"looks": [
"The portrait hangs crookedly. The faces are hard to make out in the dim light.",
"The portrait is different from before. Something about the faces has shifted, though you can't say what.",
],
"clues": [("The portrait on the landing changed between looks", 2, None)],
"friend": "He will not look at it.",
},
"humming": {
"ambient": True,
"keywords": ["humming", "hum", "music", "voice", "melody", "listen"],
"looks": [
"The humming drifts down from above. When you look up, it falters, then starts again from the beginning.",
"The s
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", "view_range": [ 316, 365 ] } 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 @@ -316,50 +316,50 @@ result = act(game, "I wait quietly.")
self.assertEqual(game["ending"], "denial")
self.assertEqual(result["outcome"], "ending")
def test_fragment_progress_reports_zero_of_three_at_start(self):
game = engine.new_game()
progress = engine.fragment_progress(game)
self.assertEqual(len(engine.fragments_complete(game)), 0)
self.assertEqual(
{kind: item["found"] for kind, item in progress.items()},
{"routine": 0, "rules": 0, "night": 0},
)
self.assertIn("Family memories shared: 0/4", state_block(game))
def test_fragment_clue_categories_match_the_authored_discoveries(self):
clues = [
(text, fragment)
for room in engine.ROOMS.values()
for feature in room.get("features", {}).values()
for text, _examination_number, fragment in feature.get("clues", [])
]
categorized_counts = Counter(
fragment for _text, fragment in clues if fragment is not None
)
uncategorized = {text for text, fragment in clues if fragment is None}
self.assertEqual(
categorized_counts,
{"routine": 4, "rules": 3, "night": 3},
)
self.assertEqual(len(clues), 15)
self.assertEqual(
uncategorized,
{
"A musty smell lingers in the air",
"A book was recently removed from the shelf",
"Family photos on the wall are blurred and unrecognizable",
"The house's first owner buried his sister alive and died soon after; the house stood empty for years",
"The portrait on the landing changed between looks",
},
)
def test_final_memories_are_shared_in_order_before_together_ending(self):
game = engine.new_game()
game.update(
stage=5,
door_open=True,
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 168, 177 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -168,10 +168,10 @@ } ], "design_constraints": { "mvp": { "rooms": "6: Entrance Hall, Library, Kitchen, Grand Staircase, Upper Landing, Parents' Bedroom (locked)", "key_clues": "10 fragment clues across three threads: Routine (4), Rules (3), and Night (3). Five additional discoveries provide atmosphere or optional lore but do not count toward fragment completion.", "puzzles": "1: the parents' bedroom door", "major_ghosts": 1, "endings": "4 implemented (leave_together, escape_alone, denial, sisters_bargain)" }, { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 249, 260 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -249,12 +249,12 @@ "delivery": "Found only in a water-stained local history in the library. Never explained by characters. The house is still haunted by this and other past tragedies." }, "memory_fragments": { "description": "Ten clues are assigned to three kinds of fragment: Routine (4), Rules (3), and Night (3). Finding any two clues in a kind completes that fragment. Five additional discoveries provide atmosphere or optional lore and do not count toward fragment completion. The sidebar tracks each category and the total out of three. Completing all three lets the player try the bedroom door.", "routine": "The friend is still keeping house for people who are not there. Clues: third cup, pulled-out chair, untouched food, and food that never spoils.", "rules": "The father's preservation. Clues: the stopped hall clock, the outdated calendar, the note on the bedroom door.", "night": "What happened at the door. Clues: repeating footsteps overhead, the humming, the slow knocking from inside the bedroom.", "other_discoveries": "These do not count toward a memory fragment: the musty smell, the missing-book gap, the blurred family photos, the local-history account of the previous owner, and the changing portrait." }, "engine_rules": { "denial": "Friend's denial runs 0-10. It starts high and drops by 1 for each clue found and each of up to 3 kind gestures. Blunt confrontation before all three fragments raises it by 1.", "door": "The bedroom door opens only if all three fragments are complete and denial is at or below 3. Opening it moves the story to the final stage.", *** Begin Patch *** Update File: the-house-that-remembers/engine.py @@ -FRAGMENT_KINDS = ("routine", "rules", "night") +FRAGMENT_KINDS = ("past", "present", "echoes") FRAGMENT_LABELS = {
- "routin... 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 @@ -11,11 +11,11 @@ AMBIENT_AFTER_TURNS = 3 # turns spent in a room before a detail may surface AMBIENT_COOLDOWN = 3 # minimum turns between any two noticed details FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment -FRAGMENT_KINDS = ("routine", "rules", "night") +FRAGMENT_KINDS = ("past", "present", "echoes") FRAGMENT_LABELS = {
- "routine": "Routine",
- "rules": "Rules",
- "night": "Night",
- "past": "Past",
- "present": "Present",
- "echoes": "Echoes", } DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door PUSH_LIMIT = 3 # blunt confrontations before the evidence -> he shuts down @@ -107,7 +107,7 @@ "looks": [ "A tall clock stands against the wall, its hands stopped at a quarter past three. The pendulum hangs perfectly still. The clock hasn't moved. Nothing in this hall ticks.", ],
-
"clues": [("The clock on the wall is frozen at 3:15", 1, 'rules')],
-
"clues": [("The clock on the wall is frozen at 3:15", 1, 'past')], "friend": "He says it only needs winding, then changes the subject.", }, },
@@ -139,7 +139,7 @@ "From somewhere above, a few slow footsteps cross the ceiling. After a , the very same footsteps cross it again.", "The footsteps repeat, exactly the same, as though something is replaying.", ],
-
"clues": [("The same few footsteps repeat overhead", 1, 'night')],
-
"clues": [("The same few footsteps repeat overhead", 1, 'echoes')], "friend": "He says it is only the house settling, and changes the subject.", }, "photos": {
@@ -149,7 +149,7 @@ "The family photos are blurred and unrecognizable. You can't make out any faces.", "The photos remain blurred. You can't tell who they are.", ],
-
"clues": [("Family photos on the wall are blurred and unrecognizable", 1, None)],
-
"clues": [("Family photos on the wall are blurred and unrecognizable", 1, 'past')], "friend": "He says the photos are just old and faded.", },
@@ -191,7 +191,7 @@ "Two cups wait by the teapot. Then you notice a third at the far end of the table, set out and untouched.", "The third cup hasn't moved. It sits exactly where someone would put it for a guest.", ],
-
"clues": [("A third cup sits set out at the table, as if someone else were expected", 1, 'routine')],
-
"clues": [("A third cup sits set out at the table, as if someone else were expected", 1, 'present')], "friend": "If he notices the player noticing, he glances away and changes the subject.", }, "chair": {
@@ -201,7 +201,7 @@ "One chair at the table is pulled out slightly, as though someone had only just stood up from it.", "The chair stays where it is. The seat is cold.", ],
-
"clues": [("A kitchen chair is pulled out, as though someone had just stood up", 1, 'routine')],
-
"clues": [("A kitchen chair is pulled out, as though someone had just stood up", 1, 'present')], }, "food": { "ambient": True,
@@ -211,7 +211,7 @@ "The plate of food remains. It is cold, but the smell is still faintly familiar.", "It seems like the food has been sitting there for a while, but it isn't spoiled." ],
-
"clues": [("A plate of food sits on the counter, untouched", 1, 'routine'), ("The food has sat out for ages but has not spoiled", 3, 'routine')],
-
"clues": [("A plate of food sits on the counter, untouched", 1, 'present'), ("The food has sat out for ages but has not spoiled", 3, 'present')], }, "calendar": { "ambient": True,
@@ -219,7 +219,7 @@ "looks": [ "The calendar on the wall is turned to March, but the year is faded and unreadable. It is currently October.", ],
-
"clues": [("The calendar on the wall is turned to March, but it is currently October. Year is faded and unreadable", 1, 'rules')],
-
"clues": [("The calendar on the wall is turn
[Truncated — full output (12.6 KB) temporarily saved to C:\Users\lucia\AppData\Local\Temp\1793220683469-copilot-tool-output-8fe854a3fada4b1d8f129d74b05720ae.txt] {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run engine regressio... ..................... #
Ran 21 tests in 0.009s
OK <shellId: 94 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile engine.py prompt.py app.py tests\test_engine.py; python -c "import jso... Counter({'past': 4, 'present': 4, 'echoes': 3}) Syntax, JSON, and clue counts valid <shellId: 95 completed with exit code 0> {"pattern":"Routine \(4\)|Rules \(3\)|Night \(3\)|routine|FRAGMENT_KINDS = \("routine"|\{\"routine\": 0","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-re... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:123: "An extra place setting or family routine that no longer makes sense", {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- README.md the-house-that-remembers/engine.py the-house-that-remembers/story/canon.json t... warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it <shellId: 96 completed with exit code 0>
Implemented the new **Past / Present / Echoes** categories and kept the requirement at **two clues per category**.
Updated the sidebar categories, engine assignments, canon, README, and category regression test in [engine.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py), [canon.json](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/story/canon.json), [README.md](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/README.md), and [test_engine.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/tests/test_engine.py).
All **21 tests pass**; syntax, JSON, clue counts, and diff checks are clean.
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 132, 146 ] } 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 @@ -132,15 +132,15 @@ "clues": [("A book was recently removed from the shelf", 1, None)], "friend": "He says he doesn't remember taking a book, and changes the subject.", }, "footsteps": { "ambient": True, "keywords": ["footsteps", "steps", "sound", "sounds", "ceiling", "overhead", "listen"], "looks": [ "From somewhere above, a few slow footsteps cross the ceiling. After a , the very same footsteps cross it again.", "The footsteps repeat, exactly the same, as though something is replaying.", ], "clues": [("The same few footsteps repeat overhead", 1, 'echoes')], "friend": "He says it is only the house settling, and changes the subject.", }, "photos": { "ambient": True, { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 246, 284 ] } 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 @@ -246,39 +246,39 @@ "humming": { "ambient": True, "keywords": ["humming", "hum", "music", "voice", "melody", "listen"], "looks": [ "The humming drifts down from above. When you look up, it falters, then starts again from the beginning.", "The same melody, again, from the very beginning.", ], "clues": [("The humming upstairs falters whenever you listen", 1, 'echoes')], "friend": "He seems not to hear it, or says it is only the house.", }, }, }, "Upper Landing": { "description": ( "The upper landing is long and narrow, lit by a single grey window. " "Every door along it is shut. At the far end, a folded note is taped " "to the frame of one door." ), "exits": ["Grand Staircase", "Parents' Bedroom"], "img": "upper_landing.jpg", "gated": {"Parents' Bedroom": "door_open"}, "aliases": ["upper landing", "upstairs landing", "landing", "upper floor", "second floor", "upstairs", "first floor", "second story"], "features": { "door": { "ambient": True, "keywords": ["door", "note", "knock", "knocking"], "takeable": "Upper Landing Note", "looks": [ "The parents' bedroom door is shut. The folded note taped to its frame reads, in a careful hand:\n'Do not open\n\nDo not change\n\nI have to keep things as they are.'", "You press your ear to the wood. From inside comes a slow knock, three times. Then nothing.", ], "clues": [ ("A note on the parents' bedroom door reads: do not open; do not change; I have to keep things as they are", 1, "past"), ("Slow knocking comes from inside the locked bedroom", 2, "echoes"), ], "friend": "He will not come near the door.", }, }, }, { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 530, 550 ] } 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 @@ -530,21 +530,21 @@ def _examine(game, room_name, name, feat): """Record one examination of a feature. Returns (what they notice, new clue(s) or None).""" key = f"{room_name}:{name}" n = game["examined"].get(key, 0) + 1 game["examined"][key] = n looks = feat["looks"] noticed = looks[min(n - 1, len(looks) - 1)] found = [] for text, on_n, _frag in feat.get("clues", []): if n == on_n and text not in game["clues"]: game["clues"].append(text) found.append(text) return noticed, ("; ".join(found) if found else None)
def _next_unnoticed(game, room_name): """First ambient feature in this room the player hasn't encountered yet.""" for name, feat in ROOMS[room_name].get("features", {}).items(): { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 970, 1010 ] } 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 @@ -970,41 +970,41 @@
fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
if fresh:
game["last_notice_turn"] = game["turn_count"]
if (
not fresh
and result["outcome"] in ("no_change", "examined")
and not game["ending"]
and not game["truth_known"]
and game["room_turns"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_nudge_turn"] >= NUDGE_COOLDOWN
and _next_unnoticed(game, game["room"]) is None
):
nudge = _nudge(game)
if nudge:
result["nudge"] = nudge
game["last_nudge_turn"] = game["turn_count"]
game["nudges_here"] += 1
if result["new_clue"]:
for _ in result["new_clue"].split("; "):
game["denial"] = max(0, game["denial"] - 1)
if (
game["truth_known"]
and game["denial"] < 3
and game["friend_room"] == game["room"]
and intent in {"dialogue", "action_and_dialogue"}
and result["outcome"] not in {"blocked", "ending"}
and len(game["final_revelations"]) < len(FINAL_REVELATIONS)
):
revelation = FINAL_REVELATIONS[len(game["final_revelations"])]
game["final_revelations"].append(revelation["id"])
result["family_memory"] = revelation
result["stage_advanced"] = _advance_stage(game)
sister_due = (
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 548, 578 ] } 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 @@ -548,31 +548,31 @@ def _next_unnoticed(game, room_name): """First ambient feature in this room the player hasn't encountered yet.""" for name, feat in ROOMS[room_name].get("features", {}).items(): if feat.get("ambient") and game["examined"].get(f"{room_name}:{name}", 0) == 0: return name, feat return None
def _unfinished(game, room_name): """A room still worth visiting: never visited, or details left to notice.""" if room_name == "Parents' Bedroom": return False return room_name not in game["visited"] or _next_unnoticed(game, room_name) is not 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, [])]) while queue: 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) route = [*path, nxt] if goal_fn(nxt): return route, nxt queue.append((nxt, route)) return None {"pattern":"AMBIENT_AFTER_TURNS|_next_unnoticed|last_notice_turn|ambient","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py"],"output_mode":"content",... [grep content: 29 matches across 1 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
engine.py (29 match(es)): 11: AMBIENT_AFTER_TURNS = 3 # turns spent in a room before a detail may surface 99: "ambient": True, 105: "ambient": True, 126: "ambient": True, 136: "ambient": True, 146: "ambient": True, 157: "ambient": True, 188: "ambient": True, 198: "ambient": True, 207: "ambient": True, 217: "ambient": True, 237: "ambient": True, 247: "ambient": True, 270: "ambient": True, 340: "last_notice_turn": -99, # turn_count when a detail was last surfaced 519: def _result(outcome, detail, new_clue=None, ambient=None, friend=None): 525: "ambient": ambient, # small detail to weave into a quiet turn 548: def _next_unnoticed(game, room_name): 549: """First ambient feature in this room the player hasn't encountered yet.""" 551: if feat.get("ambient") and game["examined"].get(f"{room_name}:{name}", 0) == 0: 559: return room_name not in game["visited"] or _next_unnoticed(game, room_name) is not None 823: nxt = _next_unnoticed(game, room_name) 941: and game["room_turns"] >= AMBIENT_AFTER_TURNS 942: and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN 944: nxt = _next_unnoticed(game, game["room"]) 948: result["ambient"] = noticed 974: game["last_notice_turn"] = game["turn_count"] 983: and game["turn_count"] - game["last_notice_turn"] >= NUDGE_AFTER_TURNS 985: and _next_unnoticed(game, game["room"]) is None { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 934, 950 ] } 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 @@ -934,17 +934,17 @@ if result["outcome"] != "moved": game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
I am in stage 4, missing one Echo fragment, I read the note and went upstairs. Maybe when the player tries opening the door, it can also trigger the knocking
I’ll check how locked-door attempts are currently handled, then wire the knocking clue to that interaction and add a regression test so the player can discover it while trying to open the door.
{"pattern":"door_open|Parents' Bedroom|locked|open.*door|door.*open|_examine\(","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py"],"output_mode":"co... [grep content: 42 matches across 1 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
engine.py (42 match(es)): 20: DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door 44: "the family unchanged. He locked himself, his wife, and his daughter in " 45: "the bedroom and told his son never to open the door. He thought he was " 67: "unable to face that he never opened the door." 264: "exits": ["Grand Staircase", "Parents' Bedroom"], 266: "gated": {"Parents' Bedroom": "door_open"}, 274: "The parents' bedroom door is shut. The folded note taped to its frame reads, in a careful hand:\n'Do not open\n\nDo not change\n\nI have to keep things as they are.'", 278: ("A note on the parents' bedroom door reads: do not open; do not change; I have to keep things as they are", 1, "past"), 279: ("Slow knocking comes from inside the locked bedroom", 2, "echoes"), 285: "Parents' Bedroom": { 316: "Parents' Bedroom": "Upper Landing", 352: "door_open": False, # whether the parents' bedroom door is open 389: """Exit names for display, marking locked ones.""" 395: labels.append(f"{e} (door locked)" if flag and not game[flag] else e) 448: DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b") 533: def _examine(game, room_name, name, feat): 557: if room_name == "Parents' Bedroom": 592: if (room_name == "Upper Landing" and not game["door_open"] 597: "help him. Do not open the door." 601: if not hit and not game["door_open"] and room_name != "Upper Landing": 628: if game["door_open"]: 629: return _result("no_change", "The bedroom door already stands open.") 632: "blocked", 633: "The door will not give. It is not only locked: pushing at it feels like pushing " 640: "blocked", 642: "and the door will not open until he is.", 645: game["door_open"] = True 647: "door_opened", 648: "The door opens at last, without resistance, as if it had only ever been waiting. " 699: "blocked", 715: "blocked", 721: "blocked", 735: return _result("blocked", "The way out of the house is back through the Entrance Hall.") 767: if step == "Parents' Bedroom": 783: "blocked", 796: return _result("blocked", f"The {name} can't be taken.") 809: noticed, new_clue = _examine(game, room_name, name, feat) 826: noticed, new_clue = _examine(game, room_name, name, feat) 862: if game["door_open"] and game["stage"] < 5: 947: noticed, new_clue = _examine(game, game["room"], name, feat) 1002: and result["outcome"] not in {"blocked", "ending"} 1045: if speaking_with_sister and result["outcome"] not in {"ending", "blocked"}: {"pattern":"door|knock|bedroom|fragment","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py"],"output_mode":"content","-n":true,"head_limit... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:208: def test_sister_bargain_is_available_before_or_after_entering_bedroom(self): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:279: act(game, "examine the door") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:294: door_open=True, C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:300: result = act(game, "enter parents' bedroom") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:321: def test_fragment_progress_reports_zero_of_three_at_start(self): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:324: progress = engine.fragment_progress(game) C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:326: self.assertEqual(len(engine.fragments_complete(game)), 0) C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:333: def test_fragment_clue_categories_match_the_authored_discoveries(self): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:335: (text, fragment) C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:338: for text, _examination_number, fragment in feature.get("clues", []) C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:341: fragment for _text, fragment in clues if fragment is not None C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:343: uncategorized = {text for text, fragment in clues if fragment is None} C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:364: door_open=True, C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:390: door_open=True, { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 500, 550 ] } 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 @@ -500,51 +500,51 @@ if EXAMINE_RE.search(action_text) and _find_feature(action_text, room): return "examination"
has_quoted_speech = bool(QUOTED_TEXT_RE.search(raw_action))
has_speech = (
has_quoted_speech
or bool(SPEECH_RE.search(action_text))
or bool(SUPPORT_RE.search(action_text))
)
has_physical_action = bool(PHYSICAL_ACTION_RE.search(action_text))
if has_speech and has_physical_action:
return "action_and_dialogue"
if has_speech or raw_action.strip().endswith("?"):
return "dialogue"
if has_physical_action:
return "physical_action"
return "roleplay"
def _result(outcome, detail, new_clue=None, ambient=None, friend=None): return { "nudge": None, # gentle push to move on (set by apply_action) "outcome": outcome, "detail": detail, "new_clue": new_clue, "ambient": ambient, # small detail to weave into a quiet turn "friend": friend, # optional hint for how the friend reacts }
def _examine(game, room_name, name, feat): """Record one examination of a feature. Returns (what they notice, new clue(s) or None).""" key = f"{room_name}:{name}" n = game["examined"].get(key, 0) + 1 game["examined"][key] = n looks = feat["looks"] noticed = looks[min(n - 1, len(looks) - 1)] found = [] for text, on_n, _frag in feat.get("clues", []): if n == on_n and text not in game["clues"]: game["clues"].append(text) found.append(text) return noticed, ("; ".join(found) if found else None)
def _next_unnoticed(game, room_name): """First ambient feature in this room the player hasn't encountered yet.""" for name, feat in ROOMS[room_name].get("features", {}).items(): { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 615, 670 ] } 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 @@ -615,56 +615,56 @@ return ( "Still nothing new here. Let a faint sound or a change in the light from the direction " f"of {where} draw attention that way. Do not say what it is. " "The move happens only if the player chooses it." ) return ( "The player has lingered. Let the friend drift toward the exit that leads to " f"{where}, stop there, and look back. The player is still in the {room_name} " "until they choose to move." )
def _try_door(game): if game["door_open"]: return _result("no_change", "The bedroom door already stands open.") if len(fragments_complete(game)) < 3: return _result( "blocked", "The door will not give. It is not only locked: pushing at it feels like pushing " "against something holding its breath. The friend begs the player to leave it. " "The player does not yet understand enough to know why it matters.", friend="He is frightened and pleads with the player to step away.", ) if game["denial"] > DOOR_DENIAL_MAX: return _result( "blocked", "The door stays shut. The friend's hand closes on the player's arm. He is not ready, " "and the door will not open until he is.", friend="He is trembling and cannot let the player open it yet.", ) game["door_open"] = True return _result( "door_opened", "The door opens at last, without resistance, as if it had only ever been waiting. " "Cold, still air spills out. The friend stands frozen beside the player. " "The player has not yet gone in.", )
def _confront(game): if len(fragments_complete(game)) >= 3: game["denial"] = max(0, game["denial"] - 1) return _result( "no_change", "The player says aloud what the evidence points to. This time the friend does not " "deny it outright: he wavers, almost admits it, then stops, voice cracking. " "Nothing else changes.", friend="He is on the edge of breaking.", ) game["pushed"] += 1 game["denial"] = min(10, game["denial"] + 1) if game["pushed"] >= PUSH_LIMIT: return _end( game, "denial", "The player pushed too hard, too soon. The friend shuts down completely.", ) { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 770, 845 ] } 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 @@ -770,76 +770,76 @@ game["room_turns"] = 0 game["nudges_here"] = 0 detail = f"The player moved to the {target}." 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 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 from the {room_name}. " f"Reachable from here: {', '.join(exit_labels(game, room_name))}.", ) if room_name in rooms_named: return _result("no_change", f"The player is already in the {room_name}.")
found = _find_feature(text, room)
if found and _mentions(text, TAKE_VERBS):
name, feat = found
item = feat.get("takeable")
if not item:
return _result("blocked", f"The {name} can't be taken.")
if item in game["inventory"]:
return _result("no_change", f"The player already has the {item}.")
game["inventory"].append(item)
return _result("picked_up", f"The player picked up the {item}.")
if found and (
intent not in {"dialogue", "memory"}
or EXAMINE_RE.search(text)
or _mentions(text, TAKE_VERBS)
):
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}",
new_clue,
friend=feat.get("friend"),
)
if LOOK_AROUND_RE.search(room_parse_text):
nxt = _next_unnoticed(game, room_name)
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, room_name, name, feat)
return _result(
"examined",
f"The player looked around the {room_name}. One thing stands out: {noticed}",
new_clue,
friend=feat.get("friend"),
)
return _result(
"examined",
f"The player looked around the {room_name} again. Nothing new stands out. "
"Describe only what the room description already says, in a fresh way.",
)
return _result(
"no_change",
"Nothing the engine tracks changed. Treat the action as ordinary roleplay "
"(talking, resting, small gestures). Do not add items, clues, or events. "
f"No room change happens: the player is still in the {room_name}, and nobody "
f"leads them elsewhere unless they choose an exit ({', '.join(exit_labels(game, room_name))}).",
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", "view_range": [ 215, 305 ] } 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 @@ -215,91 +215,91 @@ friend_room="Library", sister_seen=True, sister_warning_count=2, sister_bargain_available=True, sister_bargain_heard=True, truth_known=truth_known, )
result = act(game, "I agree to the bargain.")
self.assertEqual(result["outcome"], "ending")
self.assertEqual(game["ending"], "sisters_bargain")
def test_sister_vanishes_after_five_conversation_turns_but_bargain_remains(self):
game = engine.new_game()
game.update(
stage=3,
room="Kitchen",
friend_room="Library",
sister_seen=True,
sister_warning_count=2,
sister_bargain_available=True,
sister_present=True,
)
for turn in range(engine.SISTER_CONVERSATION_LIMIT - 1):
result = act(game, "Can you tell me more?")
self.assertTrue(result["sister_conversation"])
self.assertFalse(result.get("sister_departed", False))
self.assertTrue(game["sister_present"])
result = act(game, "Why do you believe that?")
self.assertTrue(result["sister_conversation"])
self.assertTrue(result["sister_departed"])
self.assertFalse(game["sister_present"])
self.assertTrue(game["sister_bargain_available"])
self.assertTrue(game["sister_bargain_heard"])
ending = act(game, "I accept your bargain.")
self.assertEqual(ending["outcome"], "ending")
self.assertEqual(game["ending"], "sisters_bargain")
def test_sister_does_not_offer_private_conversation_when_friend_is_present(self):
game = engine.new_game()
game.update(
stage=3,
room="Kitchen",
friend_room="Kitchen",
sister_seen=True,
sister_warning_count=2,
last_sister_turn=0,
turn_count=engine.SISTER_COOLDOWN - 1,
)
result = act(game, "look around")
self.assertFalse(result["sister_private_encounter"])
self.assertFalse(game["sister_bargain_available"])
self.assertIn("warning", result["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)
self.assertIn("his name is not established", prompt)
self.assertIn("do not call him 'E.'", prompt)
def test_denial_persists_after_truth_is_faced_and_support_can_lower_it(self):
game = engine.new_game()
game.update(
stage=5,
door_open=True,
room="Upper Landing",
denial=3,
turn_count=50,
)
result = act(game, "enter parents' bedroom")
self.assertEqual(result["outcome"], "moved")
self.assertTrue(game["truth_known"])
self.assertEqual(game["denial"], 3)
self.assertIn("still resisting it", stage_block(game))
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 575, 625 ] } 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 @@ -575,51 +575,51 @@ if goal_fn(nxt): 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"]
if (room_name == "Upper Landing" and not game["door_open"]
and len(fragments_complete(game)) >= 3):
return (
"Everything here has been found, but the friend is not ready for the door. "
"Let him hesitate, almost speak, and look to the player. Patience and kindness may "
"help him. Do not open the door."
)
hit = _path_step(game, room_name, lambda r: _unfinished(game, r))
if not hit and not game["door_open"] and room_name != "Upper Landing":
hit = _path_step(game, room_name, lambda r: r == "Upper Landing")
if not hit:
return None
step, target = hit
where = f"the {step}" + (f", on the way to the {target}" if step != target else "")
level = min(game["nudges_here"] + 1, 3)
if level == 1:
return (
"There is nothing more to find here for now. Let the friend gently suggest moving on, "
f"toward {where}. The move happens only if the player chooses it."
)
if level == 2:
return (
"Still nothing new here. Let a faint sound or a change in the light from the direction "
f"of {where} draw attention that way. Do not say what it is. "
"The move happens only if the player chooses it."
)
return (
"The player has lingered. Let the friend drift toward the exit that leads to "
f"{where}, stop there, and look back. The player is still in the {room_name} "
"until they choose to move."
)
{ "pattern": "_try_door|DOOR_TRY_RE", "paths": [ "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py" ], "output_mode": "content", "-n": true, "head_limit": 30 } C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:448:DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:627:def _try_door(game): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:739: if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:740: return _try_door(game) { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 725, 750 ] } 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 @@ -725,26 +725,26 @@ ) return _end( game, "leave_together", "The player and the friend choose to leave the house together.", )
if LEAVE_RE.search(text) and not negated:
if room_name == "Entrance Hall":
return _end(game, "escape_alone", "The player walks out of the house alone.")
return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
*** Begin Patch *** Update File: the-house-that-remembers/engine.py @@ def _try_door(game): if game["door_open"]: return _result("no_change", "The bedroom door already stands open.") ... 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 @@ -628,7 +628,7 @@ if game["door_open"]: return _result("no_change", "The bedroom door already stands open.") if len(fragments_complete(game)) < 3:
-
return _result(
-
result = _result( "blocked", "The door will not give. It is not only locked: pushing at it feels like pushing " "against something holding its breath. The friend begs the player to leave it. "
@@ -635,6 +635,10 @@ "The player does not yet understand enough to know why it matters.", friend="He is frightened and pleads with the player to step away.", )
-
result["new_clue"] = _record_door_knocking(game) -
if result["new_clue"]: -
result["detail"] += " As the player tests it, three slow knocks answer from within." -
if game["denial"] > DOOR_DENIAL_MAX: return _result( "blocked", @@ -649,8 +653,20 @@ "Cold, still air spills out. The friend stands frozen beside the player. " "The player has not yet gone in.", )return result
+def _record_door_knocking(game):
- door = ROOMS["Upper Landing"]["features"]["door"]
- clue = next(
-
text for text, _examination_number, fragment in door["clues"] -
if fragment == "echoes" and "knocking" in text.lower() - )
- if clue in game["clues"]:
-
return None - game["clues"].append(clue)
- return clue
def _confront(game): if len(fragments_complete(game)) >= 3: game["denial"] = max(0, game["denial"] - 1)
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 @@ -357,6 +357,23 @@ }, )
- def test_locked_door_attempt_reveals_echoes_knocking_clue(self):
-
game = engine.new_game() -
game.update(room="Upper Landing", friend_room="Library") -
door = engine.ROOMS["Upper Landing"]["features"]["door"] -
note_clue = door["clues"][0][0] -
knocking_clue = door["clues"][1][0] -
game["examined"]["Upper Landing:door"] = 1 -
game["clues"].append(note_clue) -
result = act(game, "I try to open the door.") -
self.assertEqual(result["outcome"], "blocked") -
self.assertEqual(result["new_clue"], knocking_clue) -
self.assertIn("three slow knocks answer from within", result["detail"]) -
self.assertIn(knocking_clue, game["clues"]) -
self.assertEqual(engine.fragment_progress(game)["echoes"]["found"], 1) - def test_final_memories_are_shared_in_order_before_together_ending(self): game = engine.new_game() game.update( *** Begin Patch *** Update File: the-house-that-remembers/engine.py @@ def _try_door(game): if game["door_open"]: return _result("no_change", "The bedroom door already stands open.") ... 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 @@ -628,23 +628,25 @@ if game["door_open"]: return _result("no_change", "The bedroom door already stands open.") if len(fragments_complete(game)) < 3:
-
result = _result(
-
return _door_attempt_with_knocking( -
game, -
_result( "blocked", "The door will not give. It is not only locked: pushing at it feels like pushing " "against something holding its breath. The friend begs the player to leave it. " "The player does not yet understand enough to know why it matters.", friend="He is frightened and pleads with the player to step away.", -
), )
-
result["new_clue"] = _record_door_knocking(game) -
if result["new_clue"]: -
result["detail"] += " As the player tests it, three slow knocks answer from within." -
if game["denial"] > DOOR_DENIAL_MAX:return result -
return _result( -
"blocked", -
"The door stays shut. The friend's hand closes on the player's arm. He is not ready, " -
"and the door will not open until he is.", -
friend="He is trembling and cannot let the player open it yet.",
-
return _door_attempt_with_knocking( -
game, -
_result( -
"blocked", -
"The door stays shut. The friend's hand closes on the player's arm. He is not ready, " -
"and the door will not open until he is.", -
friend="He is trembling and cannot let the player open it yet.", -
game["door_open"] = True return _result( @@ -667,6 +669,14 @@ return clue), )
+def _door_attempt_with_knocking(game, result):
- clue = _record_door_knocking(game)
- if clue:
-
result["new_clue"] = clue -
result["detail"] += " As the player tests it, three slow knocks answer from within." - return result
def _confront(game): if len(fragments_complete(game)) >= 3: game["denial"] = max(0, game["denial"] - 1) {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run locked-door clue... ...................... #
Ran 22 tests in 0.010s
OK <shellId: 97 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile engine.py tests\test_engine.py","description":"Check changed Python fil... <shellId: 99 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/engine.py the-house-that-remembers/tests/test_engine.py","descr... warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it <shellId: 98 completed with exit code 0>
Implemented. When the player tries the still-locked bedroom door, **three slow knocks answer from within** and the knocking clue is recorded—so this works even if they already read the note. It’s a one-time discovery; repeat attempts won’t add it again.
All **22 regression tests pass**, and the changed Python files compile successfully. See [engine.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py) and [test_engine.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/tests/test_engine.py).
and if player doesnt try to open the door? just examine second time - it will still trigger knocking?
Yes. **Examining the door a second time** still reveals the knocking clue, as before. Trying to open the locked door is now an additional way to trigger it if it hasn’t been discovered yet.
Your friend looks up at you then, a flicker of relief crossing his exhausted features. "It's alright," he says softly, though his voice remains thin. He seems to deflate slightly, the tension draining from his shoulders for a moment. "You came."
🧑
"I think you need to let go"
🕯️
He watches you for a long moment, his expression unreadable against the pallor of exhaustion. "I don't know how," he replies simply, looking back towards the still figures on the bed. "It feels like if I let go, they'll just... fade away completely." He s, then adds quietly, "What have you been doing all these years? I mean... if you want to tell me."
I think we need to remove small talk from stage 5/or after door open and and truth known
{"pattern":"small talk|small-talk|smalltalk|childhood|small conversation|STAGES|stage_block|truth_known|FINAL_REVELATIONS","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-tha... [grep content: 40 matches across 3 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
tests/test_engine.py (12 match(es)): 9: from prompt import _engine_note, stage_block, state_block, story_block 209: for truth_known in (False, True): 210: with self.subTest(truth_known=truth_known): 220: truth_known=truth_known, 303: self.assertTrue(game["truth_known"]) 305: self.assertIn("still resisting it", stage_block(game)) 310: self.assertIn("share the story gradually", stage_block(game)) 314: game.update(truth_known=True, denial=3, turn_count=engine.MAX_TURNS - 1) 384: truth_known=True, 393: for expected in engine.FINAL_REVELATIONS: 410: truth_known=True, 418: self.assertIn("still resisting it", stage_block(game))
engine.py (13 match(es)): 29: FINAL_REVELATIONS = ( 336: "stage": 0, # index into prompt.STAGES 353: "truth_known": False, # whether the player has learned the truth about the house 435: r"back when we|childhood)\b" 738: if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken): 745: if len(game.get("final_revelations", [])) < len(FINAL_REVELATIONS): 770: if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)): 794: game["truth_known"] = True 993: and (not game["truth_known"] or game["denial"] >= 3) 1007: and not game["truth_known"] 1024: game["truth_known"] 1029: and len(game["final_revelations"]) < len(FINAL_REVELATIONS) 1031: revelation = FINAL_REVELATIONS[len(game["final_revelations"])]
prompt.py (15 match(es)): 25: - Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment. 49: STAGES = [ 52: "friend": "He tries to act normal and spend ordinary time together. He can ask about the player's life or mention an established childhood memory. He deflects direct questions about the house and does not bring up family members on his own.", 137: def stage_block(game): 143: if game.get("truth_known") 149: if game["truth_known"] 153: s = STAGES[game["stage"]] 156: from engine import FINAL_REVELATIONS 158: if not game.get("truth_known"): 170: elif len(game.get("final_revelations", [])) < len(FINAL_REVELATIONS): 194: from engine import FINAL_REVELATIONS, ROOMS, SISTER_PRESERVATION_MEMORY, exit_labels 202: f"Truth faced: {'yes' if game.get('truth_known') else 'no'}", 218: "Established player memories: childhood companions; playing outside together; " 227: for memory in FINAL_REVELATIONS 337: system = "\n\n".join([CORE, story_block(canon), stage_block(game), state_block(game)]) { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 1, 130 ] } 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 @@ -1,130 +1,130 @@ """prompt.py - builds what the model sees each turn.
Layers (top to bottom):
- CORE static narrator rules (cacheable prefix)
- story block premise, friend sheet, letter (static per story)
- stage block what may be revealed now (changes with stage)
- state block room, exits, inventory, clues (changes per turn)
- history last N messages (older -> summary)
- engine result + reminder, attached to the player's latest message """ import re
import ollama
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT normally brings him into the player's room; narrate that arrival before his reply. The exception is an ENGINE RESULT-marked private sister conversation: answer as the sister and keep the friend elsewhere.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech.
- When the ENGINE RESULT authorizes the sister's private conversation, she may explain that she believes preserving the family keeps them alive. This is her perspective, not objective proof that the family is alive, and does not reveal the full truth.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good." "Do you remember how we used to help in the kitchen? I was never much good at it." "It's strange being back in the library. We used to spend whole afternoons in here." "I haven't been sleeping much. The house is loud at night." "Tea sounds good. The kitchen's just this way, if you'd like." "I miss the old days. Now, everything is so... different. I don't know how to explain it." "What have you been doing all these years? I mean... if you want to tell me.""""
STAGES = [ { # 0 - Arrival and uneasy normality "now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet. Friend invited the player to the house because he is lonely and the history of the house is weighing on him. He is not aware of the truth, but he doesn't want to be alone.", "friend": "He tries to act normal and spend ordinary time together. He can ask about the player's life or mention an established childhood memory. He deflects direct questions about the house and does not bring up family members on his own.", "steer": "Let the player settle in and explore. Keep the friend's suggestions tied to what the player just said; if tea or a drink is wanted, the kitchen is the natural next step. Do not offer upstairs or list destinations without a reason. When the friend invites the player somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.", }, { # 1 - Contradictions "now": "Details about the friend's family do not quite add up.", "friend": "He mentions his family casually, in the present tense, as if they are alive and nearby: his mother is resting, his father is busy upstairs, his sister is 'around'. He does not notice anything odd. If asked to see them, or asked directly about them, he answers vaguely and changes the subject.", "steer": "Let this come up once or twice in passing, e.g. "Mum's resting. She hasn't been sleeping well." Do not explain it.", }, { # 2 - Frozen house and family traces "now": "The house is frozen in time: clocks are stopped, calendars are out of date, food never spoils, and places are set for people who are not there. Portraits can change between looks. The friend has lost track of time.", "friend": "He says time is 'strange' or that he has lost count of the days. He grows uneasy when stopped clocks, old dates or the extra place settings are mentioned and tries to move on.", "steer": "Describe frozen or changed details only when the engine provides them. Otherwise keep the house quietly wrong: a sound repeating, a door slightly off.", }, { # 3 - Sister's warnings "now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.", "friend": "He does not see her, or insists the player imagined it.", "steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most. She speaks gently and urgently. She never threatens. If the ENGINE RESULT authorizes a private conversation, she may explain that she believes preserving the family keeps them alive; present this as her belief, not as confirmed fact. Otherwise do not say what happened to the friend's family.", }, { # 4 - Parents' fragments "now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.", "friend": "He flinches at the knocking and tells the player, "Whatever you do, don't answer it." Fragments of memory slip out, then he retreats.", "steer": "Us { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 1, 80 ] } 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 @@ -1,80 +1,80 @@ """engine.py - authoritative game state and rules.
Nothing in here calls the model. The model only ever sees the result of what this module decides (see prompt.py). """ import re from pprint import pprint
AMBIENT_AFTER_TURNS = 3 # turns spent in a room before a detail may surface AMBIENT_COOLDOWN = 3 # minimum turns between any two noticed details FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment FRAGMENT_KINDS = ("past", "present", "echoes") FRAGMENT_LABELS = { "past": "Past", "present": "Present", "echoes": "Echoes", } DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door PUSH_LIMIT = 3 # blunt confrontations before the evidence -> he shuts down SUPPORT_LIMIT = 3 # times kindness can lower his denial MAX_TURNS = 90 # unresolved truth or denial by then -> "denial" ending NUDGE_AFTER_TURNS = 4 NUDGE_COOLDOWN = 3 SISTER_COOLDOWN = 12 SISTER_CONVERSATION_LIMIT = 5
FINAL_REVELATIONS = ( { "id": "mother", "title": "His mother's decline", "detail": ( "They are dead. The house drained them, beginning with his mother, who " "grew exhausted and unable to sleep and sensed something was wrong. The " "room was preserved, so they look as if they are sleeping." ), }, { "id": "father", "title": "His father's attempt to preserve them", "detail": ( "After help failed, his father became frightened and tried to preserve " "the family unchanged. He locked himself, his wife, and his daughter in " "the bedroom and told his son never to open the door. He thought he was " "protecting them, but shut them away from help; they died there, drained " "by the house." ), }, { "id": "sister", "title": "His sister's hope", "detail": ( "His sister kept trying to hold the family together and urged her brother " "not to give up on them. Now she clings to the house's preserved illusion, " "believing it keeps her family together; her warnings to leave are real, " "but she mistakes letting go for losing them." ), }, { "id": "friend", "title": "Why he stayed silent", "detail": ( "He had argued with his father and believed the others were overreacting. " "He knocked once, heard no answer, and obeyed the last order out of anger " "and fear. Since then he has pretended they were still alive and resting, " "unable to face that he never opened the door." ), }, )
SISTER_PRESERVATION_MEMORY = ( "She believes the family is still together as long as the house preserves them " "exactly as they were. To her, keeping things unchanged is a way of keeping them " "alive; she warns the player away because she fears that facing the truth will " "make her lose them." )
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 850, 910 ] } 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 @@ -850,61 +850,61 @@ if nxt: name, feat = nxt noticed, new_clue = _examine(game, room_name, name, feat) return _result( "examined", f"The player looked around the {room_name}. One thing stands out: {noticed}", new_clue, friend=feat.get("friend"), ) return _result( "examined", f"The player looked around the {room_name} again. Nothing new stands out. " "Describe only what the room description already says, in a fresh way.", )
return _result(
"no_change",
"Nothing the engine tracks changed. Treat the action as ordinary roleplay "
"(talking, resting, small gestures). Do not add items, clues, or events. "
f"No room change happens: the player is still in the {room_name}, and nobody "
f"leads them elsewhere unless they choose an exit ({', '.join(exit_labels(game, room_name))}).",
)
STAGE_RULES = [ lambda g: g["turns_in_stage"] >= 5 and len(g["visited"]) >= 2, # 0 arrival lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 1, # 1 contradictions lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 4, # 2 paintings lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 7, # 3 sister's warnings ]
def _advance_stage(game): if game["door_open"] and game["stage"] < 5: game["stage"] = 5 game["turns_in_stage"] = 0 return True if game["stage"] >= 4: return False if STAGE_RULESgame["stage"]: game["stage"] += 1 game["turns_in_stage"] = 0 return True return False
def apply_action(game, raw_action): """Resolve one player action, update game state, return the engine result.""" game.setdefault("friend_room", "Entrance Hall") game.setdefault("sister_seen", False) game.setdefault("last_sister_turn", -99) game.setdefault("sister_warning_count", 0) game.setdefault("sister_bargain_available", False) game.setdefault("sister_bargain_heard", False) game.setdefault("sister_present", False) game.setdefault("sister_conversation_turns", 0) { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", "view_range": [ 350, 430 ] } 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 @@ -350,81 +350,81 @@ self.assertEqual( uncategorized, { "A musty smell lingers in the air", "A book was recently removed from the shelf", "The house's first owner buried his sister alive and died soon after; the house stood empty for years", "The portrait on the landing changed between looks", }, )
def test_locked_door_attempt_reveals_echoes_knocking_clue(self):
game = engine.new_game()
game.update(room="Upper Landing", friend_room="Library")
door = engine.ROOMS["Upper Landing"]["features"]["door"]
note_clue = door["clues"][0][0]
knocking_clue = door["clues"][1][0]
game["examined"]["Upper Landing:door"] = 1
game["clues"].append(note_clue)
result = act(game, "I try to open the door.")
self.assertEqual(result["outcome"], "blocked")
self.assertEqual(result["new_clue"], knocking_clue)
self.assertIn("three slow knocks answer from within", result["detail"])
self.assertIn(knocking_clue, game["clues"])
self.assertEqual(engine.fragment_progress(game)["echoes"]["found"], 1)
def test_final_memories_are_shared_in_order_before_together_ending(self):
game = engine.new_game()
game.update(
stage=5,
door_open=True,
room="Parents' Bedroom",
friend_room="Parents' Bedroom",
truth_known=True,
denial=2,
)
blocked = act(game, "Let's leave together.")
self.assertEqual(blocked["outcome"], "blocked")
self.assertIsNone(game["ending"])
self.assertFalse(game["final_revelations"])
for expected in engine.FINAL_REVELATIONS:
result = act(game, "What happened?")
self.assertEqual(result["family_memory"]["id"], expected["id"])
self.assertIn(expected["detail"], _engine_note(game))
self.assertIn(expected["detail"], state_block(game))
ending = act(game, "Let's leave together.")
self.assertEqual(ending["outcome"], "ending")
self.assertEqual(game["ending"], "leave_together")
def test_high_denial_blocks_final_memories_and_together_ending(self):
game = engine.new_game()
game.update(
stage=5,
door_open=True,
room="Parents' Bedroom",
friend_room="Parents' Bedroom",
truth_known=True,
denial=3,
)
result = act(game, "What do you remember?")
self.assertNotIn("family_memory", result)
self.assertFalse(game["final_revelations"])
self.assertIn("still resisting it", stage_block(game))
ending = act(game, "Let's leave together.")
self.assertEqual(ending["outcome"], "blocked")
self.assertIsNone(game["ending"])
if name == "main": unittest.main() { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 48, 92 ] } 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 @@ -48,45 +48,45 @@ STAGES = [ { # 0 - Arrival and uneasy normality "now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet. Friend invited the player to the house because he is lonely and the history of the house is weighing on him. He is not aware of the truth, but he doesn't want to be alone.", "friend": "He tries to act normal and spend ordinary time together. He can ask about the player's life or mention an established childhood memory. He deflects direct questions about the house and does not bring up family members on his own.", "steer": "Let the player settle in and explore. Keep the friend's suggestions tied to what the player just said; if tea or a drink is wanted, the kitchen is the natural next step. Do not offer upstairs or list destinations without a reason. When the friend invites the player somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.", }, { # 1 - Contradictions "now": "Details about the friend's family do not quite add up.", "friend": "He mentions his family casually, in the present tense, as if they are alive and nearby: his mother is resting, his father is busy upstairs, his sister is 'around'. He does not notice anything odd. If asked to see them, or asked directly about them, he answers vaguely and changes the subject.", "steer": "Let this come up once or twice in passing, e.g. "Mum's resting. She hasn't been sleeping well." Do not explain it.", }, { # 2 - Frozen house and family traces "now": "The house is frozen in time: clocks are stopped, calendars are out of date, food never spoils, and places are set for people who are not there. Portraits can change between looks. The friend has lost track of time.", "friend": "He says time is 'strange' or that he has lost count of the days. He grows uneasy when stopped clocks, old dates or the extra place settings are mentioned and tries to move on.", "steer": "Describe frozen or changed details only when the engine provides them. Otherwise keep the house quietly wrong: a sound repeating, a door slightly off.", }, { # 3 - Sister's warnings "now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.", "friend": "He does not see her, or insists the player imagined it.", "steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most. She speaks gently and urgently. She never threatens. If the ENGINE RESULT authorizes a private conversation, she may explain that she believes preserving the family keeps them alive; present this as her belief, not as confirmed fact. Otherwise do not say what happened to the friend's family.", }, { # 4 - Parents' fragments "now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.", "friend": "He flinches at the knocking and tells the player, "Whatever you do, don't answer it." Fragments of memory slip out, then he retreats.", "steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.", }, { # 5 - Reconstructed truth "now": "The bedroom door has opened. The family inside are preserved as if asleep. The player faces the truth on entering; the friend can only share the family story as the engine authorizes each memory.", "friend": "His denial breaks only as he becomes ready. He speaks in halting fragments and quiet realization, not speeches. If denial is still 3 or higher, he resists and cannot share the next family memory. When denial falls below 3, the ENGINE RESULT may supply one authored family memory; include it honestly and clearly, without adding facts. He does not blame the player. He never agrees to leave together unless the engine ends the story.", "steer": "If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, the friend shares the mother's decline, the father's choice, the sister's hope, and his own silence one memory at a time when the ENGINE RESULT supplies each. Let the moment breathe between disclosures. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.", }, ]
ENDINGS = { "leave_together": ( "The player and the friend have faced the truth and choose to leave together. " "Write the final scene in 4-6 sentences: the friend's grief and fragile steadiness, the house letting go " "(the stopped clocks may tick once; the knocking and humming fall silent), the door, the rain easing. " "No gore. Offer no further choices. End the story." ), "escape_alone": ( "The player has chosen to leave the house alone, and the engine confirms it. " "Write the final scene in 4-6 sentences: the door, the rain, the friend's reaction according to his condition, " "the house behind them. Do not explain what happened to the family. Offer no further choices. End the story." { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 132, 180 ] } 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 @@ -132,49 +132,49 @@ "THE LETTER HE SENT\n" f"{canon['opening']['letter_text']}" )
def stage_block(game): if game["ending"]: text = ENDINGS[game["ending"]] if game["ending"] == "sisters_bargain": text += ( " The player has already entered the bedroom and seen the family." if game.get("truth_known") else " The player accepted before entering the bedroom and has not seen the family." ) if game["ending"] == "escape_alone": text += ( " The player saw the truth in the bedroom before leaving; the friend stays behind with his family." if game["truth_known"] else " The player never learned what was upstairs; the friend stays behind in the house." ) return "ENDING\n" + text s = STAGES[game["stage"]] truth_permission = "" if game["stage"] == 5: from engine import FINAL_REVELATIONS
if not game.get("truth_known"):
truth_permission = (
"\nTRUTH STATUS: The bedroom door is open, but the player has not entered "
"and has not faced the truth. Do not reveal the family's fate yet."
)
elif game["denial"] >= 3:
truth_permission = (
"\nTRUTH STATUS: The player has seen the truth, but the friend is still "
"resisting it. He may deny or minimize it and cannot yet give a clear account. "
"Do not force acceptance; let sincere support soften him. If his denial remains "
"unresolved, the story can end with him unable to face it."
)
elif len(game.get("final_revelations", [])) < len(FINAL_REVELATIONS):
truth_permission = (
"\nTRUTH STATUS: The player has seen the family. The friend is ready to "
"share the story gradually, but has only disclosed the memories recorded "
"in CURRENT STATE so far. Do not reveal any later family memory unless "
"the ENGINE RESULT supplies it this turn."
)
else:
truth_permission = (
"\nTRUTH STATUS: The player has seen the truth and the friend has shared "
"all four family memories recorded in CURRENT STATE. He can now speak "
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", "view_range": [ 1, 25 ] } 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 @@ -1,25 +1,25 @@ import contextlib import io import json import unittest from collections import Counter from pathlib import Path
import engine from prompt import _engine_note, stage_block, 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_supportive_phrases_increase_support(self): {"pattern":"friend conversation|small talk|ordinary moments|childhood","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json","C:\Users\lucia\D... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:28: "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:29: "player_role": "The player is an outsider and the friend's childhood companion. They did not cause the family's tragedy and have no secret responsibility for it.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:103: "early_behavior": "He sometimes initiates gentle small talk or asks about the player's life, drawing only on established childhood memories such as playing outside, helping with kitchen chores, exploring the library, and sleepovers. He varies the topic rather than repeating offers or lines.", *** Begin Patch *** Update File: the-house-that-remembers/prompt.py @@ -- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action... { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 12, 30 ] } 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 @@ -12,19 +12,19 @@
import ollama
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion. { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 76, 90 ] } 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 @@ -76,15 +76,15 @@ "now": "The bedroom door has opened. The family inside are preserved as if asleep. The player faces the truth on entering; the friend can only share the family story as the engine authorizes each memory.", "friend": "His denial breaks only as he becomes ready. He speaks in halting fragments and quiet realization, not speeches. If denial is still 3 or higher, he resists and cannot share the next family memory. When denial falls below 3, the ENGINE RESULT may supply one authored family memory; include it honestly and clearly, without adding facts. He does not blame the player. He never agrees to leave together unless the engine ends the story.", "steer": "If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, the friend shares the mother's decline, the father's choice, the sister's hope, and his own silence one memory at a time when the ENGINE RESULT supplies each. Let the moment breathe between disclosures. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.", }, ]
ENDINGS = { "leave_together": ( "The player and the friend have faced the truth and choose to leave together. " "Write the final scene in 4-6 sentences: the friend's grief and fragile steadiness, the house letting go " "(the stopped clocks may tick once; the knocking and humming fall silent), the door, the rain easing. " "No gore. Offer no further choices. End the story." ), "escape_alone": ( "The player has chosen to leave the house alone, and the engine confirms it. " { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 137, 192 ] } 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 @@ -137,56 +137,56 @@ def stage_block(game): if game["ending"]: text = ENDINGS[game["ending"]] if game["ending"] == "sisters_bargain": text += ( " The player has already entered the bedroom and seen the family." if game.get("truth_known") else " The player accepted before entering the bedroom and has not seen the family." ) if game["ending"] == "escape_alone": text += ( " The player saw the truth in the bedroom before leaving; the friend stays behind with his family." if game["truth_known"] else " The player never learned what was upstairs; the friend stays behind in the house." ) return "ENDING\n" + text s = STAGES[game["stage"]] truth_permission = "" if game["stage"] == 5: from engine import FINAL_REVELATIONS
if not game.get("truth_known"):
truth_permission = (
"\nTRUTH STATUS: The bedroom door is open, but the player has not entered "
"and has not faced the truth. Do not reveal the family's fate yet."
)
elif game["denial"] >= 3:
truth_permission = (
"\nTRUTH STATUS: The player has seen the truth, but the friend is still "
"resisting it. He may deny or minimize it and cannot yet give a clear account. "
"Do not force acceptance; let sincere support soften him. If his denial remains "
"unresolved, the story can end with him unable to face it."
)
elif len(game.get("final_revelations", [])) < len(FINAL_REVELATIONS):
truth_permission = (
"\nTRUTH STATUS: The player has seen the family. The friend is ready to "
"share the story gradually, but has only disclosed the memories recorded "
"in CURRENT STATE so far. Do not reveal any later family memory unless "
"the ENGINE RESULT supplies it this turn."
)
else:
truth_permission = (
"\nTRUTH STATUS: The player has seen the truth and the friend has shared "
"all four family memories recorded in CURRENT STATE. He can now speak "
"honestly about what happened. Keep his voice gentle and halting, not a speech."
)
return (
f"STORY NOW: {s['now']}\n"
f"FRIEND RIGHT NOW: {s['friend']}\n"
f"FRIEND'S CONDITION: {denial_band(game['denial'])}\n"
f"STEER TOWARD (gently, never force): {s['steer']}\n"
f"{truth_permission}\n"
f"Turns spent in this part of the story: {game['turns_in_stage']}"
)
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", "view_range": [ 375, 430 ] } 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 @@ -375,56 +375,56 @@ self.assertEqual(engine.fragment_progress(game)["echoes"]["found"], 1)
def test_final_memories_are_shared_in_order_before_together_ending(self):
game = engine.new_game()
game.update(
stage=5,
door_open=True,
room="Parents' Bedroom",
friend_room="Parents' Bedroom",
truth_known=True,
denial=2,
)
blocked = act(game, "Let's leave together.")
self.assertEqual(blocked["outcome"], "blocked")
self.assertIsNone(game["ending"])
self.assertFalse(game["final_revelations"])
for expected in engine.FINAL_REVELATIONS:
result = act(game, "What happened?")
self.assertEqual(result["family_memory"]["id"], expected["id"])
self.assertIn(expected["detail"], _engine_note(game))
self.assertIn(expected["detail"], state_block(game))
ending = act(game, "Let's leave together.")
self.assertEqual(ending["outcome"], "ending")
self.assertEqual(game["ending"], "leave_together")
def test_high_denial_blocks_final_memories_and_together_ending(self):
game = engine.new_game()
game.update(
stage=5,
door_open=True,
room="Parents' Bedroom",
friend_room="Parents' Bedroom",
truth_known=True,
denial=3,
)
result = act(game, "What do you remember?")
self.assertNotIn("family_memory", result)
self.assertFalse(game["final_revelations"])
self.assertIn("still resisting it", stage_block(game))
ending = act(game, "Let's leave together.")
self.assertEqual(ending["outcome"], "blocked")
self.assertIsNone(game["ending"])
if name == "main": unittest.main() { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json", "view_range": [ 96, 108 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -96,13 +96,13 @@ "description": "Distinguish hidden truth from what the player has discovered. Do not reveal secrets prematurely." } ], "character_dynamics": { "friend": { "initial_state": "Pale, exhausted, frightened, and genuinely glad the player has arrived. He wants help but is not ready to face everything.", "personality": "Anxious, frightened, withdrawn, sleep deprived, glad to see the player.", "early_behavior": "He sometimes initiates gentle small talk or asks about the player's life, drawing only on established childhood memories such as playing outside, helping with kitchen chores, exploring the library, and sleepovers. He varies the topic rather than repeating offers or lines.", "voice": "Frightened, exhausted, and emotionally withdrawn, trying to act normal. Speak plainly and hesitantly. Use s, short answers, and occasional unfinished thoughts.", "contradiction": "He speaks of his whole family as alive and resting (mother resting, father busy, sister around) without noticing the contradiction.", "progression": "As clues are found his denial drops. Blunt confrontation before the evidence makes him shut down. The engine owns the numbers." }, "sister_ghost": { *** Begin Patch *** Update File: the-house-that-remembers/prompt.py @@ -- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action... 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 @@ -22,7 +22,7 @@
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic. -- Use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. This can happen throughout the story, but should fit the friend's current state and the moment. +- Before the bedroom truth is faced, use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. Once the player has entered the bedroom and learned the truth, do not initiate small talk or pivot to ordinary topics.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
@@ -74,8 +74,8 @@ }, { # 5 - Reconstructed truth "now": "The bedroom door has opened. The family inside are preserved as if asleep. The player faces the truth on entering; the friend can only share the family story as the engine authorizes each memory.",
-
"friend": "His denial breaks only as he becomes ready. He speaks in halting fragments and quiet realization, not speeches. If denial is still 3 or higher, he resists and cannot share the next family memory. When denial falls below 3, the ENGINE RESULT may supply one authored family memory; include it honestly and clearly, without adding facts. He does not blame the player. He never agrees to leave together unless the engine ends the story.", -
"steer": "If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, the friend shares the mother's decline, the father's choice, the sister's hope, and his own silence one memory at a time when the ENGINE RESULT supplies each. Let the moment breathe between disclosures. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.",
-
"friend": "His denial breaks only as he becomes ready. He speaks in halting fragments and quiet realization, not speeches. If denial is still 3 or higher, he resists and cannot share the next family memory. When denial falls below 3, the ENGINE RESULT may supply one authored family memory; include it honestly and clearly, without adding facts. After the player has entered and learned the truth, keep his conversation focused on the family, his grief, the player's immediate response, and whether to stay or leave. Do not initiate small talk, ask what the player has been doing, bring up unrelated childhood memories, or pivot to ordinary topics. He does not blame the player. He never agrees to leave together unless the engine ends the story.", -
}, ]"steer": "If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, the friend shares the mother's decline, the father's choice, the sister's hope, and his own silence one memory at a time when the ENGINE RESULT supplies each. Let the moment breathe between disclosures without filling s with small talk. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.",
@@ -180,6 +180,12 @@ "all four family memories recorded in CURRENT STATE. He can now speak " "honestly about what happened. Keep his voice gentle and halting, not a speech." )
-
if game.get("truth_known"): -
truth_permission += ( -
"\nCONVERSATION FOCUS: Do not initiate small talk, ask about the player's " -
"life, or shift to ordinary topics. Keep the exchange with the friend " -
"focused on the family, grief, and the player's immediate choices." -
return ( f"STORY NOW: {s['now']}\n" f"FRIEND RIGHT NOW: {s['friend']}\n")
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/story/canon.json @@ -100,7 +100,7 @@ "friend": { "initial_state": "Pale, exhausted, frightened, and genuinely glad the player has arrived. He wants help but is not ready to face everything.", "personality": "Anxious, frightened, withdrawn, sleep deprived, glad to see the player.",
-
"early_behavior": "He sometimes initiates gentle small talk or asks about the player's life, drawing only on established childhood memories such as playing outside, helping with kitchen chores, exploring the library, and sleepovers. He varies the topic rather than repeating offers or lines.",
-
"early_behavior": "Before the bedroom truth is faced, he sometimes initiates gentle small talk or asks about the player's life, drawing only on established childhood memories such as playing outside, helping with kitchen chores, exploring the library, and sleepovers. After the player enters the bedroom and learns the truth, he does not initiate small talk or pivot to ordinary topics; conversation stays with the family, grief, and the player's immediate choices.", "voice": "Frightened, exhausted, and emotionally withdrawn, trying to act normal. Speak plainly and hesitantly. Use s, short answers, and occasional unfinished thoughts.", "contradiction": "He speaks of his whole family as alive and resting (mother resting, father busy, sister around) without noticing the contradiction.", "progression": "As clues are found his denial drops. Blunt confrontation before the evidence makes him shut down. The engine owns the numbers."
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 @@ -421,6 +421,23 @@ self.assertEqual(ending["outcome"], "blocked") self.assertIsNone(game["ending"])
- def test_small_talk_is_disabled_after_truth_is_faced(self):
-
game = engine.new_game() -
game.update(stage=5, door_open=True, truth_known=True, denial=2) -
prompt = stage_block(game) -
self.assertIn("CONVERSATION FOCUS", prompt) -
self.assertIn("Do not initiate small talk", prompt) - def test_small_talk_remains_available_before_truth_is_faced(self):
-
game = engine.new_game() -
game.update(stage=5, door_open=True, truth_known=False) -
prompt = stage_block(game) -
self.assertNotIn("CONVERSATION FOCUS", prompt)
if name == "main": unittest.main() { "pattern": "FRIEND_EXAMPLES", "paths": [ "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py" ], "output_mode": "content", "-n": true, "head_limit": 20 } C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:40:FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good." C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:131: f"{FRIEND_EXAMPLES}\n\n" {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run final conversati... ........................ #
Ran 24 tests in 0.009s
OK <shellId: 100 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile prompt.py engine.py tests\test_engine.py; python -c "import json; json... Python and canon JSON validation passed <shellId: 102 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/prompt.py the-house-that-remembers/story/canon.json the-house-t... warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it <shellId: 101 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 37, 46 ] } 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 @@ -37,10 +37,10 @@
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good." "Do you remember how we used to help in the kitchen? I was never much good at it." "It's strange being back in the library. We used to spend whole afternoons in here." "I haven't been sleeping much. The house is loud at night." "Tea sounds good. The kitchen's just this way, if you'd like." "I miss the old days. Now, everything is so... different. I don't know how to explain it." "What have you been doing all these years? I mean... if you want to tell me."""" { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 122, 136 ] } 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 @@ -122,15 +122,15 @@ "STORY PREMISE\n" f"{canon['premise']['summary']}\n\n" "THE FRIEND (his name is not established; the letter is signed with the initial 'E.')\n" "- Refer to him as 'your friend' or 'he'; do not call him 'E.' as though that were his name.\n" f"- Condition: {f['initial_state']}\n" f"- Personality: {f['personality']}\n" f"- Voice: {f['voice']}\n" f"- Early behavior: {f['early_behavior']}\n" "- Example lines (for tone only; do not repeat them verbatim):\n" f"{FRIEND_EXAMPLES}\n\n" "THE LETTER HE SENT\n" f"{canon['opening']['letter_text']}" ) *** Begin Patch *** Update File: the-house-that-remembers/prompt.py @@
-
"- Example lines (for tone only; do not repeat them verbatim):\n"
-
"- Early-story example lines (for tone only; ...
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 @@ -127,7 +127,7 @@ f"- Personality: {f['personality']}\n" f"- Voice: {f['voice']}\n" f"- Early behavior: {f['early_behavior']}\n"
-
"- Example lines (for tone only; do not repeat them verbatim):\n"
-
"- Early-story example lines (for tone only; do not repeat them verbatim or use them after the truth is faced):\n" f"{FRIEND_EXAMPLES}\n\n" "THE LETTER HE SENT\n" f"{canon['opening']['letter_text']}"
{"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'; python -m py_compile prompt.py engi... ........................ #
Ran 24 tests in 0.009s
OK <shellId: 104 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/prompt.py the-house-that-remembers/story/canon.json the-house-t... warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/story/canon.json', LF will be replaced by CRLF the next time Git touches it <shellId: 103 completed with exit code 0>
Updated the narration guidance so **after the player enters the bedroom and learns the truth**, the friend won’t initiate small talk, ask about the player’s life, or pivot to ordinary topics. His conversation stays focused on his family, grief, and the player’s immediate choices. The existing small-talk guidance and example lines are now explicitly marked for the story before the truth is faced.
Added regression tests confirming the guidance switches at that point. All **24 tests pass**, and syntax and diff checks are clean. Changes are in [prompt.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/prompt.py), [canon.json](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/story/canon.json), and [test_engine.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/tests/test_engine.py).
{"pattern":"def _confront|pushed|denial.*\+ 1|CONFRONT_RE|FAMILY_RE|Blunt confrontation|pushing","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py"],... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:350: "pushed": 0, C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:451:CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:452:FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:635: "The door will not give. It is not only locked: pushing at it feels like pushing " C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:680:def _confront(game): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:690: game["pushed"] += 1 C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:691: game["denial"] = min(10, game["denial"] + 1) C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:692: if game["pushed"] >= PUSH_LIMIT: C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:695: "The player pushed too hard, too soon. The friend shuts down completely.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:697: warn = " He is close to shutting down." if game["pushed"] == PUSH_LIMIT - 1 else "" C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:770: if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)): { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 425, 460 ] } 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 @@ -425,36 +425,36 @@
LOOK_AROUND_RE = re.compile( r"\b(look|glance|scan|survey|search|explore|examine|inspect|study)\w*\b.\b(around|about|room|surroundings|area)\b" r"|\b(examine|inspect|search|study|explore|look)\w\b.\b(kitchen|library|hall|staircase|landing|bedroom)\b" r"|\bwhat (do|can) i (see|notice|find)\b|\bnotice anything\b" ) QUOTED_TEXT_RE = re.compile(r'"[^"]"|“[^”]”|‘[^’]’') MEMORY_RE = re.compile( r"\b(remember|recall|used to|when we were (?:kids|children)|" r"back when we|childhood)\b" ) EXAMINE_RE = re.compile( r"\b(examine|inspect|study|look at|check|read|listen to|smell|touch)\b" ) PHYSICAL_ACTION_RE = re.compile( r"\b(?:i\s+)?(?:pat|touch|sit|stand|reach|hold|hug|tap|point|" r"turn|open|close|pick|take|walk|step|move|climb|enter|follow)\b" ) SPEECH_RE = re.compile( r"\b(?:i\s+)?(?:say|ask|tell|reply|answer|whisper|speak|call)\b" r"|\b(?:ask|tell) (?:him|her|your friend)\b" ) DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b") DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b") FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b") SUPPORT_RE = re.compile( r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b" r"|\bit'?s (ok|okay|alright)\b" r"|\b(?:we can (?:do (?:it|this)|face (?:it|this)|get through (?:it|this))" r"|we(?:'ll| will) face (?:it|this)) together\b" r"|\b(?:it'?s|it is) going to be (?:ok|okay|alright)\b" r"|\b(?:i want to help|i can help|let me help|i'?d like to help)\b" r"|\b(?:take my hand|give me your hand)\b" { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 665, 700 ] } 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 @@ -665,36 +665,36 @@ ) if clue in game["clues"]: return None game["clues"].append(clue) return clue
def _door_attempt_with_knocking(game, result): clue = _record_door_knocking(game) if clue: result["new_clue"] = clue result["detail"] += " As the player tests it, three slow knocks answer from within." return result
def _confront(game): if len(fragments_complete(game)) >= 3: game["denial"] = max(0, game["denial"] - 1) return _result( "no_change", "The player says aloud what the evidence points to. This time the friend does not " "deny it outright: he wavers, almost admits it, then stops, voice cracking. " "Nothing else changes.", friend="He is on the edge of breaking.", ) game["pushed"] += 1 game["denial"] = min(10, game["denial"] + 1) if game["pushed"] >= PUSH_LIMIT: return _end( game, "denial", "The player pushed too hard, too soon. The friend shuts down completely.", ) warn = " He is close to shutting down." if game["pushed"] == PUSH_LIMIT - 1 else "" return result( "no_change", "The player bluntly says the friend's family is dead before having any evidence. " {"pattern":"def test.*confront|pushed|denial.*9|denial.*10","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py"],"output_mode":"content","... No matches found. { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 690, 705 ] } 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 @@ -690,16 +690,16 @@ game["pushed"] += 1 game["denial"] = min(10, game["denial"] + 1) if game["pushed"] >= PUSH_LIMIT: return _end( game, "denial", "The player pushed too hard, too soon. The friend shuts down completely.", ) warn = " He is close to shutting down." if game["pushed"] == PUSH_LIMIT - 1 else "" return _result( "no_change", "The player bluntly says the friend's family is dead before having any evidence. " "The friend flatly denies it, hurt and frightened, and changes the subject." + warn + " Nothing in the house changes.", ) { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 1, 220 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -1,220 +1,220 @@ import base64 import copy import json from pathlib import Path
import ollama import streamlit as st
from engine import ( FINAL_REVELATIONS, FRAGMENT_LABELS, FRAGMENT_KINDS, FRAGMENT_CLUES_NEEDED, ROOMS, apply_action, fragment_progress, fragments_complete, new_game, ) from prompt import build_messages, generation_options, maybe_summarize, tidy
st.set_page_config(page_title="The House That Remembers", layout="wide")
MODEL_NAME = "gemma4:e4b" CANON_PATH = Path(file).resolve().parent / "story" / "canon.json" CANON = json.loads(CANON_PATH.read_text(encoding="utf-8"))
INTRO = ''' 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? '''
def generate_response(game): result = ollama.chat( model=MODEL_NAME, messages=build_messages(game, CANON), think=False, options=generation_options(game), ) return tidy(result.message.content)
def show_image(path, **kwargs): """Show an image, or a quiet caption if the file is missing (instead of crashing the app).""" if Path(path).exists(): st.image(path, **kwargs) else: st.caption(f"(image not found: {Path(path).name})")
@st.dialog("The Letter", width="small") def show_letter_dialog(): letter_path = Path(file).resolve().parent / "assets" / "letter.png" show_image(letter_path, width="stretch")
@st.dialog("The Book", width="small") def show_book_dialog(): book_path = Path(file).resolve().parent / "assets" / "book.png" show_image(book_path, width="stretch") st.markdown( '"The Fall of the House of Usher" by Edgar Allan Poe is a short story ' "first published in 1839. A narrator visits his ailing childhood friend " "Roderick Usher at his decaying family mansion. There he encounters " "Roderick's mysterious twin sister Madeline, who suffers from a strange " "illness. As the atmosphere grows increasingly oppressive, the house " "itself seems alive with malevolent energy. When Madeline is entombed " "within the estate, terrifying sounds begin echoing through the halls, " "building toward a shocking climax that binds the fate of the twins to " 'their ancestral home."' )
@st.dialog("The Note", width="small") def show_note_dialog(): note_path = Path(file).resolve().parent / "assets" / "note.png" show_image(note_path, width="stretch")
def play_background_music(file_path, loop=True): audio_path = Path(file_path) if not audio_path.is_absolute(): audio_path = Path(file).resolve().parent / audio_path if not audio_path.exists(): st.caption(f"(audio not found: {audio_path.name})") return audio_base64 = base64.b64encode(audio_path.read_bytes()).decode("ascii") loop_attr = "loop" if loop else "" st.markdown( f""" <audio controls {loop_attr} style="width: 100%;"> <source src="data:audio/mpeg;base64,{audio_base64}" type="audio/mpeg" > </audio> """, unsafe_allow_html=True, )
st.markdown(""" <style> .stApp { background: #171719; color: #e8ddca; } [data-testid="stSidebar"] { background: #222124; border-right: 1px solid #45413d; } h1, h2, h3 { color: #e8ddca !important; font-family: Georgia, serif; } .story { font-family: Georgia, serif; font-size: 1.08rem; line-height: 1.9; color: #d8d1c7; } .muted { color: #a7a19a; font-size: 0.85rem; letter-spacing: 0.12em; } div[data-testid="stChatMessage"] { background: #222124; border: 1px solid #34312e; border-radius: 12px; } .stChatInput textarea { color: #e8ddca !important; } </style> """, unsafe_allow_html=True)
if "game" not in st.session_state: st.session_state.game = new_game() game = st.session_state.game
with st.sidebar: st.markdown("## 🕯️ THE HOUSE") st.markdown('<p class="muted">YOUR PRIVATE RECORD</p>', unsafe_allow_html=True)
st.divider()
st.markdown("### 📍 Current location")
st.write(game["room"])
st.markdown("### 🧍 Friend")
friend_room = game.get("friend_room", "Entrance Hall")
st.write("Here with you" if friend_room == game["room"] else f"In the {friend_room}")
st.markdown("### 🧩 Memory fragments")
completed_fragments = len(fragments_complete(game))
st.write(f"**{completed_fragments}/3 complete**")
fragment_status = fragment_progress(game)
for kind in FRAGMENT_KINDS:
progress = fragment_status[kind]
marker = "✓" if progress["complete"] else "○"
st.caption(
f"{marker} {FRAGMENT_LABELS[kind]}: "
f"{progress['found']}/{FRAGMENT_CLUES_NEEDED} clues"
)
if game.get("truth_known"):
st.markdown("### 🕯️ Family memories")
shared_memories = game.get("final_revelations", [])
st.write(f"**{len(shared_memories)}/{len(FINAL_REVELATIONS)} remembered**")
for memory in FINAL_REVELATIONS:
if memory["id"] in shared_memories:
st.caption(f"✓ {memory['title']}")
st.markdown("### 🎒 Inventory")
if game["inventory"]:
for item in game["inventory"]:
if item == "The Letter":
if st.button("📜 The Letter 🔍", key="open_letter"):
show_letter_dialog()
elif item == "Library Book":
if st.button("📖 Library Book 🔍", key="open_book"):
show_book_dialog()
elif item == "Upper Landing Note":
if st.button("📝 Upper Landing Note 🔍", key="open_note"):
show_note_dialog()
else:
st.write(f"• {item}")
else:
st.caption("Your pockets are empty.")
st.markdown("### 🔎 Clues")
if game["clues"]:
for clue in game["clues"]:
st.write(f"• {clue}")
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 220, 390 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -220,171 +220,171 @@ st.write(f"• {clue}") else: st.caption("Nothing discovered yet.")
st.markdown("### 🗺️ Explored")
for visited_room in game["visited"]:
st.write(f"✓ {visited_room}")
st.divider()
if st.button("Restart adventure", use_container_width=True):
st.session_state.pop("game", None)
st.session_state.pop("ollama_error", None)
st.rerun()
st.subheader("🔊 Atmosphere")
sound_enabled = st.checkbox("🌧️ Background rain", value=True)
if sound_enabled:
play_background_music(Path("assets") / "rain-and-thunder.mp3")
house_ambience_enabled = st.checkbox("🏚️ House ambience", value=True)
if house_ambience_enabled:
play_background_music(Path("assets") / "ambience.mp3")
st.markdown('<p class="muted">A GOTHIC HORROR ADVENTURE</p>', unsafe_allow_html=True) st.title("The House That Remembers")
with st.expander("The letter and arrival", expanded=True): _, image_column, _ = st.columns([1, 2, 1]) with image_column: show_image( Path(file).resolve().parent / "assets" / "haunted_house.jpg", width=480, ) st.markdown(INTRO)
scene_column, conversation_column = st.columns([1, 1.25], gap="large") room = ROOMS[game["room"]]
with scene_column: with st.container(height="content", border=True): st.markdown(f"### 📍 {game['room']}") if "img" in room: show_image( Path(file).resolve().parent / "assets" / room["img"], width='stretch', ) else: st.caption("No image is available for this room yet.") st.markdown( f'<div class="story">{room["description"]}</div>', unsafe_allow_html=True, )
with conversation_column: thinking_placeholder = st.empty() st.markdown("### What will you do?") with st.container(height=480, border=True, autoscroll=True): for message in game["history"]: with st.chat_message( message["role"], avatar="🕯️" if message["role"] == "assistant" else "🧑", ): st.markdown(message["content"])
if game["ending"]:
st.markdown("---")
st.markdown("**THE END**")
if "ollama_error" in st.session_state:
st.error(st.session_state.ollama_error)
action = st.chat_input(
"Describe what you do...",
disabled=bool(game["ending"]),
)
if action: snapshot = copy.deepcopy(game) # restore this if the model call fails game["history"].append({"role": "user", "content": action}) apply_action(game, action) # Python decides what actually happened
error_message = None
response = ""
try:
with thinking_placeholder.container():
with st.spinner("Thinking..."):
maybe_summarize(game, MODEL_NAME)
response = generate_response(game)
except ollama.ResponseError as error:
if error.status_code == 404:
error_message = (
f"Ollama could not find {MODEL_NAME}. "
f"Run `ollama pull {MODEL_NAME}` and try again."
)
else:
error_message = f"Ollama returned an error: {error.error}"
except ollama.RequestError as error:
error_message = (
"Could not connect to Ollama. Make sure Ollama is running, "
f"then try again. Details: {error}"
)
else:
if not response:
error_message = "Ollama returned an empty response. Please try again."
if error_message:
st.session_state.game = snapshot # undo the failed turn completely
st.session_state.ollama_error = error_message
else:
st.session_state.pop("ollama_error", None)
game["history"].append({"role": "assistant", "content": response})
st.rerun()
st.caption(f"Powered by Ollama · {MODEL_NAME}") {"pattern":"<style>|st\.markdown|st\.sidebar|st\.image|background|css|layout|columns|chat_input|dialog|container","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-reme... [grep content: 55 matches across 3 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
prompt.py (3 match(es)): 20: - Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition. 295: "dialogue": ( 310: "action_and_dialogue": (
app.py (46 match(es)): 22: st.set_page_config(page_title="The House That Remembers", layout="wide") 67: st.image(path, **kwargs) 72: @st.dialog("The Letter", width="small") 73: def show_letter_dialog(): 78: @st.dialog("The Book", width="small") 79: def show_book_dialog(): 82: st.markdown( 95: @st.dialog("The Note", width="small") 96: def show_note_dialog(): 101: def play_background_music(file_path, loop=True): 110: st.markdown( 124: st.markdown(""" 125: <style> 127: background: #171719; 131: background: #222124; 150: background: #222124; 166: with st.sidebar: 167: st.markdown("## 🕯️ THE HOUSE") 168: st.markdown('<p class="muted">YOUR PRIVATE RECORD</p>', 173: st.markdown("### 📍 Current location") 176: st.markdown("### 🧍 Friend") 180: st.markdown("### 🧩 Memory fragments") 193: st.markdown("### 🕯️ Family memories") 200: st.markdown("### 🎒 Inventory") 205: show_letter_dialog() 208: show_book_dialog() 211: show_note_dialog() 217: st.markdown("### 🔎 Clues") 224: st.markdown("### 🗺️ Explored") 230: if st.button("Restart adventure", use_container_width=True): 238: play_background_music(Path("assets") / "rain-and-thunder.mp3") 241: play_background_music(Path("assets") / "ambience.mp3") 244: st.markdown('<p class="muted">A GOTHIC HORROR ADVENTURE</p>', 249: _, image_column, _ = st.columns([1, 2, 1]) 255: st.markdown(INTRO) 257: scene_column, conversation_column = st.columns([1, 1.25], gap="large") 261: with st.container(height="content", border=True): 262: st.markdown(f"### 📍 {game['room']}") 270: st.markdown( 277: st.markdown("### What will you do?") 278: with st.container(height=480, border=True, autoscroll=True): 284: st.markdown(message["content"]) 287: st.markdown("---") 288: st.markdown("THE END") 293: action = st.chat_input( 306: with thinking_placeholder.container():
engine.py (6 match(es)): 511: return "action_and_dialogue" 513: return "dialogue" 830: intent not in {"dialogue", "memory"} 928: and intent in {"dialogue", "action_and_dialogue"} 946: intent in {"dialogue", "action_and_dialogue"} 1027: and intent in {"dialogue", "action_and_dialogue"} { "pattern": "**/*.{png,jpg,jpeg,webp,css}", "paths": [ "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers" ] } C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\assets\upper_landing.jpg C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\assets\staircase.jpg C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\assets\note.png C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\assets\library.jpg C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\assets\letter.png C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\assets\kitchen.jpg C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\assets\haunted_house.jpg C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\assets\hallway.jpg C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\assets\book.png C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\assets\bedroom.jpg C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\assets\banner.png *** Begin Patch *** Update File: the-house-that-remembers/app.py @@ .stApp {
- background: #171719;
- background: var(--house-background, #171719); color: #e8ddca;
- transition: backgro... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -124,8 +124,9 @@ st.markdown(""" <style> .stApp {
- background: #171719;
- background: var(--house-background, #171719); color: #e8ddca;
- transition: background 1.2s ease; } [data-testid="stSidebar"] { background: #222124; @@ -162,6 +163,57 @@ st.session_state.game = new_game() game = st.session_state.game
+if game.get("truth_known"):
- house_lighting = {
-
"light": "rgba(133, 119, 96, 0.13)", -
"base": "#211f1d", -
"vignette": "rgba(0, 0, 0, 0.48)", - } +elif game["stage"] >= 5:
- house_lighting = {
-
"light": "rgba(103, 105, 121, 0.07)", -
"base": "#191a1e", -
"vignette": "rgba(0, 0, 0, 0.76)", - } +elif game["stage"] >= 4:
- house_lighting = {
-
"light": "rgba(89, 98, 119, 0.09)", -
"base": "#1a1c21", -
"vignette": "rgba(0, 0, 0, 0.72)", - } +elif game["stage"] >= 3:
- house_lighting = {
-
"light": "rgba(105, 79, 111, 0.10)", -
"base": "#1e1b22", -
"vignette": "rgba(0, 0, 0, 0.66)", - } +elif game["stage"] >= 2:
- house_lighting = {
-
"light": "rgba(98, 111, 132, 0.11)", -
"base": "#1d2025", -
"vignette": "rgba(0, 0, 0, 0.61)", - } +else:
- house_lighting = {
-
"light": "rgba(136, 99, 60, 0.13)", -
"base": "#211e1b", -
"vignette": "rgba(0, 0, 0, 0.56)", - }
+st.markdown(
- f"""
- <style>
- .stApp {{
-
--house-background: -
radial-gradient(ellipse at 50% 12%, {house_lighting["light"]}, transparent 58%), -
radial-gradient(ellipse at center, transparent 38%, {house_lighting["vignette"]} 100%), -
{house_lighting["base"]}; - }}
- </style>
- """,
- unsafe_allow_html=True, +)
with st.sidebar: st.markdown("## 🕯️ THE HOUSE") { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py", "description": "Check Streamlit app syntax" } <shellId: 106 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py; git --no-pager diff -- the-house-that-remembers/app.py"... warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it diff --git a/the-house-that-remembers/app.py b/the-house-that-remembers/app.py index 6c92c79..5c73718 100644 --- a/the-house-that-remembers/app.py +++ b/the-house-that-remembers/app.py @@ -124,8 +124,9 @@ def play_background_music(file_path, loop=True): st.markdown(""" <style> .stApp {
- background: #171719;
- background: var(--house-background, #171719); color: #e8ddca;
- transition: background 1.2s ease; } [data-testid="stSidebar"] { background: #222124; @@ -162,6 +163,57 @@ if "game" not in st.session_state: st.session_state.game = new_game() game = st.session_state.game
+if game.get("truth_known"):
- house_lighting = {
-
"light": "rgba(133, 119, 96, 0.13)", -
"base": "#211f1d", -
"vignette": "rgba(0, 0, 0, 0.48)", - } +elif game["stage"] >= 5:
- house_lighting = {
-
"light": "rgba(103, 105, 121, 0.07)", -
"base": "#191a1e", -
"vignette": "rgba(0, 0, 0, 0.76)", - } +elif game["stage"] >= 4:
- house_lighting = {
-
"light": "rgba(89, 98, 119, 0.09)", -
"base": "#1a1c21", -
"vignette": "rgba(0, 0, 0, 0.72)", - } +elif game["stage"] >= 3:
- house_lighting = {
-
"light": "rgba(105, 79, 111, 0.10)", -
"base": "#1e1b22", -
"vignette": "rgba(0, 0, 0, 0.66)", - } +elif game["stage"] >= 2:
- house_lighting = {
-
"light": "rgba(98, 111, 132, 0.11)", -
"base": "#1d2025", -
"vignette": "rgba(0, 0, 0, 0.61)", - } +else:
- house_lighting = {
-
"light": "rgba(136, 99, 60, 0.13)", -
"base": "#211e1b", -
"vignette": "rgba(0, 0, 0, 0.56)", - }
+st.markdown(
- f"""
- <style>
- .stApp {{
-
--house-background: -
radial-gradient(ellipse at 50% 12%, {house_lighting["light"]}, transparent 58%), -
radial-gradient(ellipse at center, transparent 38%, {house_lighting["vignette"]} 100%), -
{house_lighting["base"]}; - }}
- </style>
- """,
- unsafe_allow_html=True, +)
with st.sidebar: st.markdown("## 🕯️ THE HOUSE") <shellId: 105 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 120, 175 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -120,56 +120,56 @@ )
st.markdown(""" <style> .stApp { background: var(--house-background, #171719); color: #e8ddca; transition: background 1.2s ease; } [data-testid="stSidebar"] { background: #222124; border-right: 1px solid #45413d; } h1, h2, h3 { color: #e8ddca !important; font-family: Georgia, serif; } .story { font-family: Georgia, serif; font-size: 1.08rem; line-height: 1.9; color: #d8d1c7; } .muted { color: #a7a19a; font-size: 0.85rem; letter-spacing: 0.12em; } div[data-testid="stChatMessage"] { background: #222124; border: 1px solid #34312e; border-radius: 12px; } .stChatInput textarea { color: #e8ddca !important; } </style> """, unsafe_allow_html=True)
if "game" not in st.session_state: st.session_state.game = new_game() game = st.session_state.game
if game.get("truth_known"): house_lighting = { "light": "rgba(133, 119, 96, 0.13)", "base": "#211f1d", "vignette": "rgba(0, 0, 0, 0.48)", } elif game["stage"] >= 5: house_lighting = { "light": "rgba(103, 105, 121, 0.07)", "base": "#191a1e", *** Begin Patch *** Update File: the-house-that-remembers/app.py @@ <style> +body, .stApp, .stApp input, .stApp textarea, .stApp button {
- font-family: "Palatino Linotype", "Book Antiqua", Georgi... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -123,6 +123,9 @@ st.markdown(""" <style> +body, .stApp, .stApp input, .stApp textarea, .stApp button {
- font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; +} .stApp { background: var(--house-background, #171719); color: #e8ddca; @@ -134,10 +137,10 @@ } h1, h2, h3 { color: #e8ddca !important;
- font-family: Georgia, serif;
- font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; } .story {
- font-family: Georgia, serif;
- font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; font-size: 1.08rem; line-height: 1.9; color: #d8d1c7; @@ -152,6 +155,9 @@ border: 1px solid #34312e; border-radius: 12px; } +.stApp [data-testid="stMarkdownContainer"] {
- font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; +} .stChatInput textarea { color: #e8ddca !important; } { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py", "description": "Check UI stylesheet app syntax" } <shellId: 108 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py; git --no-pager diff -- the-house-that-remembers/app.py"... warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it diff --git a/the-house-that-remembers/app.py b/the-house-that-remembers/app.py index 6c92c79..5029503 100644 --- a/the-house-that-remembers/app.py +++ b/the-house-that-remembers/app.py @@ -123,9 +123,13 @@ def play_background_music(file_path, loop=True): st.markdown(""" <style> +body, .stApp, .stApp input, .stApp textarea, .stApp button {
- font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; +} .stApp {
- background: #171719;
- background: var(--house-background, #171719); color: #e8ddca;
- transition: background 1.2s ease; } [data-testid="stSidebar"] { background: #222124; @@ -133,10 +137,10 @@ st.markdown(""" } h1, h2, h3 { color: #e8ddca !important;
- font-family: Georgia, serif;
- font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; } .story {
- font-family: Georgia, serif;
- font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; font-size: 1.08rem; line-height: 1.9; color: #d8d1c7; @@ -151,6 +155,9 @@ div[data-testid="stChatMessage"] { border: 1px solid #34312e; border-radius: 12px; } +.stApp [data-testid="stMarkdownContainer"] {
- font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; +} .stChatInput textarea { color: #e8ddca !important; } @@ -162,6 +169,57 @@ if "game" not in st.session_state: st.session_state.game = new_game() game = st.session_state.game
+if game.get("truth_known"):
- house_lighting = {
-
"light": "rgba(133, 119, 96, 0.13)", -
"base": "#211f1d", -
"vignette": "rgba(0, 0, 0, 0.48)", - } +elif game["stage"] >= 5:
- house_lighting = {
-
"light": "rgba(103, 105, 121, 0.07)", -
"base": "#191a1e", -
"vignette": "rgba(0, 0, 0, 0.76)", - } +elif game["stage"] >= 4:
- house_lighting = {
-
"light": "rgba(89, 98, 119, 0.09)", -
"base": "#1a1c21", -
"vignette": "rgba(0, 0, 0, 0.72)", - } +elif game["stage"] >= 3:
- house_lighting = {
-
"light": "rgba(105, 79, 111, 0.10)", -
"base": "#1e1b22", -
"vignette": "rgba(0, 0, 0, 0.66)", - } +elif game["stage"] >= 2:
- house_lighting = {
-
"light": "rgba(98, 111, 132, 0.11)", -
"base": "#1d2025", -
"vignette": "rgba(0, 0, 0, 0.61)", - } +else:
- house_lighting = {
-
"light": "rgba(136, 99, 60, 0.13)", -
"base": "#211e1b", -
"vignette": "rgba(0, 0, 0, 0.56)", - }
+st.markdown(
- f"""
- <style>
- .stApp {{
-
--house-background: -
radial-gradient(ellipse at 50% 12%, {house_lighting["light"]}, transparent 58%), -
radial-gradient(ellipse at center, transparent 38%, {house_lighting["vignette"]} 100%), -
{house_lighting["base"]}; - }}
- </style>
- """,
- unsafe_allow_html=True, +)
with st.sidebar: st.markdown("## 🕯️ THE HOUSE") <shellId: 107 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 120, 235 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -120,116 +120,116 @@ )
st.markdown(""" <style> body, .stApp, .stApp input, .stApp textarea, .stApp button { font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; } .stApp { background: var(--house-background, #171719); color: #e8ddca; transition: background 1.2s ease; } [data-testid="stSidebar"] { background: #222124; border-right: 1px solid #45413d; } h1, h2, h3 { color: #e8ddca !important; font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; } .story { font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; font-size: 1.08rem; line-height: 1.9; color: #d8d1c7; } .muted { color: #a7a19a; font-size: 0.85rem; letter-spacing: 0.12em; } div[data-testid="stChatMessage"] { background: #222124; border: 1px solid #34312e; border-radius: 12px; } .stApp [data-testid="stMarkdownContainer"] { font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; } .stChatInput textarea { color: #e8ddca !important; } </style> """, unsafe_allow_html=True)
if "game" not in st.session_state: st.session_state.game = new_game() game = st.session_state.game
if game.get("truth_known"): house_lighting = { "light": "rgba(133, 119, 96, 0.13)", "base": "#211f1d", "vignette": "rgba(0, 0, 0, 0.48)", } elif game["stage"] >= 5: house_lighting = { "light": "rgba(103, 105, 121, 0.07)", "base": "#191a1e", "vignette": "rgba(0, 0, 0, 0.76)", } elif game["stage"] >= 4: house_lighting = { "light": "rgba(89, 98, 119, 0.09)", "base": "#1a1c21", "vignette": "rgba(0, 0, 0, 0.72)", } elif game["stage"] >= 3: house_lighting = { "light": "rgba(105, 79, 111, 0.10)", "base": "#1e1b22", "vignette": "rgba(0, 0, 0, 0.66)", } elif game["stage"] >= 2: house_lighting = { "light": "rgba(98, 111, 132, 0.11)", "base": "#1d2025", "vignette": "rgba(0, 0, 0, 0.61)", } else: house_lighting = { "light": "rgba(136, 99, 60, 0.13)", "base": "#211e1b", "vignette": "rgba(0, 0, 0, 0.56)", }
st.markdown( f""" <style> .stApp {{ --house-background: radial-gradient(ellipse at 50% 12%, {house_lighting["light"]}, transparent 58%), radial-gradient(ellipse at center, transparent 38%, {house_lighting["vignette"]} 100%), {house_lighting["base"]}; }} </style> """, unsafe_allow_html=True, )
with st.sidebar: st.markdown("## 🕯️ THE HOUSE") st.markdown('<p class="muted">YOUR PRIVATE RECORD</p>', unsafe_allow_html=True)
st.divider()
st.markdown("### 📍 Current location")
st.write(game["room"])
st.markdown("### 🧍 Friend")
friend_room = game.get("friend_room", "Entrance Hall")
*** Begin Patch *** Update File: the-house-that-remembers/app.py @@ .stApp { background: var(--house-background, #171719); color: #e8ddca; transition: background 1.2s ease; } +.stApp:... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -131,6 +131,44 @@ color: #e8ddca; transition: background 1.2s ease; } +.stApp::before, +.stApp::after {
- content: "";
- position: fixed;
- inset: 0;
- pointer-events: none;
- z-index: 1000; +} +.stApp::before {
- opacity: 0.16;
- mix-blend-mode: soft-light;
- background-image:
-
radial-gradient(circle at 13% 24%, rgba(245, 220, 174, 0.55) 0 0.7px, transparent 1.4px), -
radial-gradient(circle at 78% 67%, rgba(220, 202, 169, 0.45) 0 0.8px, transparent 1.6px), -
radial-gradient(circle at 42% 82%, rgba(247, 225, 190, 0.35) 0 0.6px, transparent 1.4px), -
repeating-radial-gradient(circle at 20% 30%, rgba(239, 226, 205, 0.12) 0 0.45px, transparent 0.7px 3px); - background-size: 173px 197px, 229px 251px, 193px 223px, 7px 7px; +} +.stApp::after {
- inset: -18%;
- opacity: 0.13;
- filter: blur(28px);
- mix-blend-mode: screen;
- background:
-
radial-gradient(ellipse at 18% 68%, rgba(171, 183, 197, 0.24), transparent 34%), -
radial-gradient(ellipse at 72% 38%, rgba(191, 181, 165, 0.17), transparent 32%), -
radial-gradient(ellipse at 52% 82%, rgba(143, 157, 177, 0.16), transparent 38%); - animation: house-mist 52s ease-in-out infinite alternate; +} +@keyframes house-mist {
- from { transform: translate3d(-1.5%, 0.5%, 0); }
- to { transform: translate3d(1.5%, -0.75%, 0); } +} +@media (prefers-reduced-motion: reduce) {
- .stApp::after {
-
animation: none; - } +} [data-testid="stSidebar"] { background: #222124; border-right: 1px solid #45413d; { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py", "description": "Check updated UI syntax" } <shellId: 109 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py; git --no-pager diff --stat -- the-house-that-remembers/... warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it the-house-that-remembers/app.py | 102 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 99 insertions(+), 3 deletions(-) <shellId: 110 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 120, 195 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -120,76 +120,76 @@ )
st.markdown(""" <style> body, .stApp, .stApp input, .stApp textarea, .stApp button { font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; } .stApp { background: var(--house-background, #171719); color: #e8ddca; transition: background 1.2s ease; } .stApp::before, .stApp::after { content: ""; position: fixed; inset: 0; pointer-events: none; z-index: 1000; } .stApp::before { opacity: 0.16; mix-blend-mode: soft-light; background-image: radial-gradient(circle at 13% 24%, rgba(245, 220, 174, 0.55) 0 0.7px, transparent 1.4px), radial-gradient(circle at 78% 67%, rgba(220, 202, 169, 0.45) 0 0.8px, transparent 1.6px), radial-gradient(circle at 42% 82%, rgba(247, 225, 190, 0.35) 0 0.6px, transparent 1.4px), repeating-radial-gradient(circle at 20% 30%, rgba(239, 226, 205, 0.12) 0 0.45px, transparent 0.7px 3px); background-size: 173px 197px, 229px 251px, 193px 223px, 7px 7px; } .stApp::after { inset: -18%; opacity: 0.13; filter: blur(28px); mix-blend-mode: screen; background: radial-gradient(ellipse at 18% 68%, rgba(171, 183, 197, 0.24), transparent 34%), radial-gradient(ellipse at 72% 38%, rgba(191, 181, 165, 0.17), transparent 32%), radial-gradient(ellipse at 52% 82%, rgba(143, 157, 177, 0.16), transparent 38%); animation: house-mist 52s ease-in-out infinite alternate; } @keyframes house-mist { from { transform: translate3d(-1.5%, 0.5%, 0); } to { transform: translate3d(1.5%, -0.75%, 0); } } @media (prefers-reduced-motion: reduce) { .stApp::after { animation: none; } } [data-testid="stSidebar"] { background: #222124; border-right: 1px solid #45413d; } h1, h2, h3 { color: #e8ddca !important; font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; } .story { font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; font-size: 1.08rem; line-height: 1.9; color: #d8d1c7; } .muted { color: #a7a19a; font-size: 0.85rem; letter-spacing: 0.12em; } div[data-testid="stChatMessage"] { background: #222124; border: 1px solid #34312e; border-radius: 12px; } *** Begin Patch *** Update File: the-house-that-remembers/app.py @@ -.stApp::before, -.stApp::after {
- content: "";
- position: fixed;
- inset: 0;
- pointer-events: none;
- z-index: 10... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -131,44 +131,6 @@ color: #e8ddca; transition: background 1.2s ease; } -.stApp::before, -.stApp::after {
- content: "";
- position: fixed;
- inset: 0;
- pointer-events: none;
- z-index: 1000; -} -.stApp::before {
- opacity: 0.16;
- mix-blend-mode: soft-light;
- background-image:
-
radial-gradient(circle at 13% 24%, rgba(245, 220, 174, 0.55) 0 0.7px, transparent 1.4px), -
radial-gradient(circle at 78% 67%, rgba(220, 202, 169, 0.45) 0 0.8px, transparent 1.6px), -
radial-gradient(circle at 42% 82%, rgba(247, 225, 190, 0.35) 0 0.6px, transparent 1.4px), -
repeating-radial-gradient(circle at 20% 30%, rgba(239, 226, 205, 0.12) 0 0.45px, transparent 0.7px 3px); - background-size: 173px 197px, 229px 251px, 193px 223px, 7px 7px; -} -.stApp::after {
- inset: -18%;
- opacity: 0.13;
- filter: blur(28px);
- mix-blend-mode: screen;
- background:
-
radial-gradient(ellipse at 18% 68%, rgba(171, 183, 197, 0.24), transparent 34%), -
radial-gradient(ellipse at 72% 38%, rgba(191, 181, 165, 0.17), transparent 32%), -
radial-gradient(ellipse at 52% 82%, rgba(143, 157, 177, 0.16), transparent 38%); - animation: house-mist 52s ease-in-out infinite alternate; -} -@keyframes house-mist {
- from { transform: translate3d(-1.5%, 0.5%, 0); }
- to { transform: translate3d(1.5%, -0.75%, 0); } -} -@media (prefers-reduced-motion: reduce) {
- .stApp::after {
-
animation: none; - } -} [data-testid="stSidebar"] { background: #222124; border-right: 1px solid #45413d; { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py", "description": "Verify reverted UI compiles" } <shellId: 111 completed with exit code 0> { "pattern": "house-mist|stApp::before|stApp::after", "paths": [ "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py" ], "output_mode": "content", "-n": true, "head_limit": 20 } No matches found. { "pattern": "[🕯️📍🧍🧩🎒🔎🗺️📜🔍📖📝🔊🌧️🏚️]", "paths": [ "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py" ], "output_mode": "content", "-n": true, "head_limit": 100 } C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:225: st.markdown("## 🕯️ THE HOUSE") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:231: st.markdown("### 📍 Current location") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:234: st.markdown("### 🧍 Friend") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:238: st.markdown("### 🧩 Memory fragments") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:251: st.markdown("### 🕯️ Family memories") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:258: st.markdown("### 🎒 Inventory") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:262: if st.button("📜 The Letter 🔍", key="open_letter"): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:265: if st.button("📖 Library Book 🔍", key="open_book"): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:268: if st.button("📝 Upper Landing Note 🔍", key="open_note"): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:275: st.markdown("### 🔎 Clues") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:282: st.markdown("### 🗺️ Explored") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:293: st.subheader("🔊 Atmosphere") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:294: sound_enabled = st.checkbox("🌧️ Background rain", value=True) C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:297: house_ambience_enabled = st.checkbox("🏚️ House ambience", value=True) C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:320: st.markdown(f"### 📍 {game['room']}") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:340: avatar="🕯️" if message["role"] == "assistant" else "🧑", *** Begin Patch *** Update File: the-house-that-remembers/app.py @@
- st.markdown("## 🕯️ THE HOUSE")
- st.markdown("## THE HOUSE") @@
- st.markdown("### 📍 Current location")
- st.markdow... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -222,20 +222,20 @@
with st.sidebar:
- st.markdown("## 🕯️ THE HOUSE")
-
st.markdown("## THE HOUSE") st.markdown('<p class="muted">YOUR PRIVATE RECORD</p>', unsafe_allow_html=True)
st.divider()
- st.markdown("### 📍 Current location")
- st.markdown("### Current location") st.write(game["room"])
- st.markdown("### 🧍 Friend")
- st.markdown("### Friend") friend_room = game.get("friend_room", "Entrance Hall") st.write("Here with you" if friend_room == game["room"] else f"In the {friend_room}")
- st.markdown("### 🧩 Memory fragments")
-
st.markdown("### Memory fragments") completed_fragments = len(fragments_complete(game)) st.write(f"{completed_fragments}/3 complete") fragment_status = fragment_progress(game) @@ -248,7 +248,7 @@ )
if game.get("truth_known"):
-
st.markdown("### 🕯️ Family memories")
-
st.markdown("### Family memories") shared_memories = game.get("final_revelations", []) st.write(f"**{len(shared_memories)}/{len(FINAL_REVELATIONS)} remembered**") for memory in FINAL_REVELATIONS:
@@ -255,17 +255,17 @@ if memory["id"] in shared_memories: st.caption(f"✓ {memory['title']}")
- st.markdown("### 🎒 Inventory")
- st.markdown("### Inventory") if game["inventory"]: for item in game["inventory"]: if item == "The Letter":
-
if st.button("📜 The Letter 🔍", key="open_letter"):
-
if st.button("The Letter · Inspect", key="open_letter"): show_letter_dialog() elif item == "Library Book":
-
if st.button("📖 Library Book 🔍", key="open_book"):
-
if st.button("Library Book · Inspect", key="open_book"): show_book_dialog() elif item == "Upper Landing Note":
-
if st.button("📝 Upper Landing Note 🔍", key="open_note"):
-
if st.button("Upper Landing Note · Inspect", key="open_note"): show_note_dialog() else: st.write(f"• {item}")
@@ -272,7 +272,7 @@ else: st.caption("Your pockets are empty.")
- st.markdown("### 🔎 Clues")
- st.markdown("### Clues") if game["clues"]: for clue in game["clues"]: st.write(f"• {clue}") @@ -279,7 +279,7 @@ else: st.caption("Nothing discovered yet.")
- st.markdown("### 🗺️ Explored")
- st.markdown("### Explored") for visited_room in game["visited"]: st.write(f"✓ {visited_room}")
@@ -290,11 +290,11 @@ st.session_state.pop("ollama_error", None) st.rerun()
- st.subheader("🔊 Atmosphere")
- sound_enabled = st.checkbox("🌧️ Background rain", value=True)
- st.subheader("Atmosphere")
- sound_enabled = st.checkbox("Background rain", value=True) if sound_enabled: play_background_music(Path("assets") / "rain-and-thunder.mp3")
- house_ambience_enabled = st.checkbox("🏚️ House ambience", value=True)
- house_ambience_enabled = st.checkbox("House ambience", value=True) if house_ambience_enabled: play_background_music(Path("assets") / "ambience.mp3")
@@ -317,7 +317,7 @@
with scene_column: with st.container(height="content", border=True):
-
st.markdown(f"### 📍 {game['room']}")
-
st.markdown(f"### {game['room']}") if "img" in room: show_image( Path(__file__).resolve().parent / "assets" / room["img"],
@@ -337,7 +337,7 @@ for message in game["history"]: with st.chat_message( message["role"],
-
avatar="🕯️" if message["role"] == "assistant" else "🧑",
-
avatar="◈" if message["role"] == "assistant" else "○", ): st.markdown(message["content"])
{ "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py", "description": "Verify UI compiles" } <shellId: 112 completed with exit code 0> { "pattern": "[🕯️📍🧍🧩🎒🔎🗺️📜🔍📖📝🔊🌧️🏚️🧑]", "paths": [ "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py" ], "output_mode": "content", "-n": true, "head_limit": 50 } No matches found. { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py", "description": "Check UI diff formatting" } warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it <shellId: 113 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 330, 348 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -330,19 +330,19 @@ unsafe_allow_html=True, )
with conversation_column: thinking_placeholder = st.empty() st.markdown("### What will you do?") with st.container(height=480, border=True, autoscroll=True): for message in game["history"]: with st.chat_message( message["role"], avatar="◈" if message["role"] == "assistant" else "○", ): st.markdown(message["content"])
if game["ending"]:
st.markdown("---")
st.markdown("**THE END**")
if "ollama_error" in st.session_state:
*** Begin Patch *** Update File: the-house-that-remembers/app.py @@ with st.chat_message( message["role"],
-
avatar="◈" if message["role"] == "assistant" e...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -337,7 +337,7 @@ for message in game["history"]: with st.chat_message( message["role"],
-
avatar="◈" if message["role"] == "assistant" else "○",
-
avatar="H" if message["role"] == "assistant" else "Y", ): st.markdown(message["content"])
{ "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py", "description": "Verify avatar fix compiles" } <shellId: 114 completed with exit code 0> { "pattern": "avatar=", "paths": [ "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py" ], "output_mode": "content", "-n": true, "head_limit": 10 } C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:340: avatar="H" if message["role"] == "assistant" else "Y", *** Begin Patch *** Update File: the-house-that-remembers/app.py @@
-
avatar="H" if message["role"] == "assistant" else "Y",
-
avatar="🕯️" if message["role"] == "assista...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -337,7 +337,7 @@ for message in game["history"]: with st.chat_message( message["role"],
-
avatar="H" if message["role"] == "assistant" else "Y",
-
avatar="🕯️" if message["role"] == "assistant" else "🧑", ): st.markdown(message["content"])
{ "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py", "description": "Verify avatar restoration" } <shellId: 115 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 120, 345 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -120,226 +120,226 @@ )
st.markdown(""" <style> body, .stApp, .stApp input, .stApp textarea, .stApp button { font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; } .stApp { background: var(--house-background, #171719); color: #e8ddca; transition: background 1.2s ease; } [data-testid="stSidebar"] { background: #222124; border-right: 1px solid #45413d; } h1, h2, h3 { color: #e8ddca !important; font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; } .story { font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; font-size: 1.08rem; line-height: 1.9; color: #d8d1c7; } .muted { color: #a7a19a; font-size: 0.85rem; letter-spacing: 0.12em; } div[data-testid="stChatMessage"] { background: #222124; border: 1px solid #34312e; border-radius: 12px; } .stApp [data-testid="stMarkdownContainer"] { font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; } .stChatInput textarea { color: #e8ddca !important; } </style> """, unsafe_allow_html=True)
if "game" not in st.session_state: st.session_state.game = new_game() game = st.session_state.game
if game.get("truth_known"): house_lighting = { "light": "rgba(133, 119, 96, 0.13)", "base": "#211f1d", "vignette": "rgba(0, 0, 0, 0.48)", } elif game["stage"] >= 5: house_lighting = { "light": "rgba(103, 105, 121, 0.07)", "base": "#191a1e", "vignette": "rgba(0, 0, 0, 0.76)", } elif game["stage"] >= 4: house_lighting = { "light": "rgba(89, 98, 119, 0.09)", "base": "#1a1c21", "vignette": "rgba(0, 0, 0, 0.72)", } elif game["stage"] >= 3: house_lighting = { "light": "rgba(105, 79, 111, 0.10)", "base": "#1e1b22", "vignette": "rgba(0, 0, 0, 0.66)", } elif game["stage"] >= 2: house_lighting = { "light": "rgba(98, 111, 132, 0.11)", "base": "#1d2025", "vignette": "rgba(0, 0, 0, 0.61)", } else: house_lighting = { "light": "rgba(136, 99, 60, 0.13)", "base": "#211e1b", "vignette": "rgba(0, 0, 0, 0.56)", }
st.markdown( f""" <style> .stApp {{ --house-background: radial-gradient(ellipse at 50% 12%, {house_lighting["light"]}, transparent 58%), radial-gradient(ellipse at center, transparent 38%, {house_lighting["vignette"]} 100%), {house_lighting["base"]}; }} </style> """, unsafe_allow_html=True, )
with st.sidebar: st.markdown("## THE HOUSE") st.markdown('<p class="muted">YOUR PRIVATE RECORD</p>', unsafe_allow_html=True)
st.divider()
st.markdown("### Current location")
st.write(game["room"])
st.markdown("### Friend")
friend_room = game.get("friend_room", "Entrance Hall")
st.write("Here with you" if friend_room == game["room"] else f"In the {friend_room}")
st.markdown("### Memory fragments")
completed_fragments = len(fragments_complete(game))
st.write(f"**{completed_fragments}/3 complete**")
fragment_status = fragment_progress(game)
for kind in FRAGMENT_KINDS:
progress = fragment_status[kind]
marker = "✓" if progress["complete"] else "○"
st.caption(
f"{marker} {FRAGMENT_LABELS[kind]}: "
f"{progress['found']}/{FRAGMENT_CLUES_NEEDED} clues"
)
if game.get("truth_known"):
st.markdown("### Family memories")
shared_memories = game.get("final_revelations", [])
st.write(f"**{len(shared_memories)}/{len(FINAL_REVELATIONS)} remembered**")
for memory in FINAL_REVELATIONS:
if memory["id"] in shared_memories:
st.caption(f"✓ {memory['title']}")
st.markdown("### Inventory")
if game["inventory"]:
for item in game["inventory"]:
if item == "The Letter":
if st.button("The Letter · Inspect", key="open_letter"):
show_letter_dialog()
elif item == "Library Book":
if st.button("Library Book · Inspect", key="open_book"):
show_book_dialog()
elif item == "Upper Landing Note":
if st.button("Upper Landing Note · Inspect", key="open_note"):
show_note_dialog()
else:
st.write(f"• {item}")
else:
st.caption("Your pockets are empty.")
st.markdown("### Clues")
if game["clues"]:
for clue in game["clues"]:
st.write(f"• {clue}")
else:
st.caption("Nothing discovered yet.")
st.markdown("### Explored")
for visited_room in game["visited"]:
st.write(f"✓ {visited_room}")
st.divider()
if st.button("Restart adventure", use_container_width=True):
st.session_state.pop("game", None)
st.session_state.pop("ollama_error", None)
st.rerun()
st.subheader("Atmosphere")
sound_enabled = st.checkbox("Background rain", value=True)
if sound_enabled:
play_background_music(Path("assets") / "rain-and-thunder.mp3")
house_ambience_enabled = st.checkbox("House ambience", value=True)
if house_ambience_enabled:
play_background_music(Path("assets") / "ambience.mp3")
st.markdown('<p class="muted">A GOTHIC HORROR ADVENTURE</p>', unsafe_allow_html=True) st.title("The House That Remembers")
with st.expander("The letter and arrival", expanded=True): _, image_column, _ = st.columns([1, 2, 1]) with image_column: show_image( Path(file).resolve().parent / "assets" / "haunted_house.jpg", width=480, ) st.markdown(INTRO)
scene_column, conversation_column = st.columns([1, 1.25], gap="large") room = ROOMS[game["room"]]
with scene_column: with st.container(height="content", border=True): st.markdown(f"### {game['room']}") if "img" in room: show_image( Path(file).resolve().parent / "assets" / room["img"], width='stretch', ) else: st.caption("No image is available for this room yet.") st.markdown( f'<div class="story">{room["description"]}</div>', unsafe_allow_html=True, )
with conversation_column: thinking_placeholder = st.empty() st.markdown("### What will you do?") with st.container(height=480, border=True, autoscroll=True): for message in game["history"]: with st.chat_message( message["role"], avatar="🕯️" if message["role"] == "assistant" else "🧑", ): st.markdown(message["content"])
if game["ending"]:
st.markdown("---")
{"pattern":"banner|haunted_house|st\.title|st\.sidebar|THE HOUSE|Current location|Your private record","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers"],"glo... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:224:with st.sidebar: C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:225: st.markdown("## THE HOUSE") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:231: st.markdown("### Current location") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:304:st.title("The House That Remembers") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:310: Path(file).resolve().parent / "assets" / "haunted_house.jpg", {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager status --short; git --no-pager diff -- the-house-that-remembers/app.py","description":"Inspect current U... M the-house-that-remembers/app.py warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it diff --git a/the-house-that-remembers/app.py b/the-house-that-remembers/app.py index 6c92c79..8bb5b04 100644 --- a/the-house-that-remembers/app.py +++ b/the-house-that-remembers/app.py @@ -123,9 +123,13 @@ def play_background_music(file_path, loop=True): st.markdown(""" <style> +body, .stApp, .stApp input, .stApp textarea, .stApp button {
- font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; +} .stApp {
- background: #171719;
- background: var(--house-background, #171719); color: #e8ddca;
- transition: background 1.2s ease; } [data-testid="stSidebar"] { background: #222124; @@ -133,10 +137,10 @@ st.markdown(""" } h1, h2, h3 { color: #e8ddca !important;
- font-family: Georgia, serif;
- font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; } .story {
- font-family: Georgia, serif;
- font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; font-size: 1.08rem; line-height: 1.9; color: #d8d1c7; @@ -151,6 +155,9 @@ div[data-testid="stChatMessage"] { border: 1px solid #34312e; border-radius: 12px; } +.stApp [data-testid="stMarkdownContainer"] {
- font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; +} .stChatInput textarea { color: #e8ddca !important; } @@ -162,22 +169,73 @@ if "game" not in st.session_state: st.session_state.game = new_game() game = st.session_state.game
+if game.get("truth_known"):
- house_lighting = {
-
"light": "rgba(133, 119, 96, 0.13)", -
"base": "#211f1d", -
"vignette": "rgba(0, 0, 0, 0.48)", - } +elif game["stage"] >= 5:
- house_lighting = {
-
"light": "rgba(103, 105, 121, 0.07)", -
"base": "#191a1e", -
"vignette": "rgba(0, 0, 0, 0.76)", - } +elif game["stage"] >= 4:
- house_lighting = {
-
"light": "rgba(89, 98, 119, 0.09)", -
"base": "#1a1c21", -
"vignette": "rgba(0, 0, 0, 0.72)", - } +elif game["stage"] >= 3:
- house_lighting = {
-
"light": "rgba(105, 79, 111, 0.10)", -
"base": "#1e1b22", -
"vignette": "rgba(0, 0, 0, 0.66)", - } +elif game["stage"] >= 2:
- house_lighting = {
-
"light": "rgba(98, 111, 132, 0.11)", -
"base": "#1d2025", -
"vignette": "rgba(0, 0, 0, 0.61)", - } +else:
- house_lighting = {
-
"light": "rgba(136, 99, 60, 0.13)", -
"base": "#211e1b", -
"vignette": "rgba(0, 0, 0, 0.56)", - }
+st.markdown(
- f"""
- <style>
- .stApp {{
-
--house-background: -
radial-gradient(ellipse at 50% 12%, {house_lighting["light"]}, transparent 58%), -
radial-gradient(ellipse at center, transparent 38%, {house_lighting["vignette"]} 100%), -
{house_lighting["base"]}; - }}
- </style>
- """,
- unsafe_allow_html=True, +)
with st.sidebar:
- st.markdown("## 🕯️ THE HOUSE")
-
st.markdown("## THE HOUSE") st.markdown('<p class="muted">YOUR PRIVATE RECORD</p>', unsafe_allow_html=True)
st.divider()
- st.markdown("### 📍 Current location")
- st.markdown("### Current location") st.write(game["room"])
- st.markdown("### 🧍 Friend")
- st.markdown("### Friend") friend_room = game.get("friend_room", "Entrance Hall") st.write("Here with you" if friend_room == game["room"] else f"In the {friend_room}")
- st.markdown("### 🧩 Memory fragments")
-
st.markdown("### Memory fragments") completed_fragments = len(fragments_complete(game)) st.write(f"{completed_fragments}/3 complete") fragment_status = fragment_progress(game) @@ -190,38 +248,38 @@ with st.sidebar: )
if game.get("truth_known"):
-
st.markdown("### 🕯️ Family memories")
-
st.markdown("### Family memories") shared_memories = game.get("final_revelations", []) st.write(f"**{len(shared_memories)}/{len(FINAL_REVELATIONS)} remembered**") for memory in FINAL_REVELATIONS: if memory["id"] in shared_memories: st.caption(f"✓ {memory['title']}")
- st.markdown("### 🎒 Inventory")
- st.markdown("### Inventory") if game["inventory"]: for item in game["inventory"]: if item == "The Letter":
-
if st.button("📜 The Letter 🔍", key="open_letter"):
-
if st.button("The Letter · Inspect", key="open_letter"): show_letter_dialog() elif item == "Library Book":
-
if st.button("📖 Library Book 🔍", key="open_book"):
-
if st.button("Library Book · Inspect", key="open_book"): show_book_dialog() elif item == "Upper Landing Note":
-
if st.button("📝 Upper Landing Note 🔍", key="open_note"):
-
else: st.caption("Your pockets are empty.")if st.button("Upper Landing Note · Inspect", key="open_note"): show_note_dialog() else: st.write(f"• {item}")
- st.markdown("### 🔎 Clues")
- st.markdown("### Clues") if game["clues"]: for clue in game["clues"]: st.write(f"• {clue}") else: st.caption("Nothing discovered yet.")
- st.markdown("### 🗺️ Explored")
- st.markdown("### Explored") for visited_room in game["visited"]: st.write(f"✓ {visited_room}")
@@ -232,11 +290,11 @@ with st.sidebar: st.session_state.pop("ollama_error", None) st.rerun()
- st.subheader("🔊 Atmosphere")
- sound_enabled = st.checkbox("🌧️ Background rain", value=True)
- st.subheader("Atmosphere")
- sound_enabled = st.checkbox("Background rain", value=True) if sound_enabled: play_background_music(Path("assets") / "rain-and-thunder.mp3")
- house_ambience_enabled = st.checkbox("🏚️ House ambience", value=True)
- house_ambience_enabled = st.checkbox("House ambience", value=True) if house_ambience_enabled: play_background_music(Path("assets") / "ambience.mp3")
@@ -259,7 +317,7 @@ room = ROOMS[game["room"]]
with scene_column: with st.container(height="content", border=True):
-
st.markdown(f"### 📍 {game['room']}")
-
st.markdown(f"### {game['room']}") if "img" in room: show_image( Path(__file__).resolve().parent / "assets" / room["img"],
<shellId: 116 completed with exit code 0> *** Begin Patch *** Update File: the-house-that-remembers/app.py @@ [data-testid="stSidebar"] {
- background: #222124;
- border-right: 1px solid #45413d;
- background: linear-gradient(180deg... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -132,13 +132,61 @@ transition: background 1.2s ease; } [data-testid="stSidebar"] {
- background: #222124;
- border-right: 1px solid #45413d;
- background: linear-gradient(180deg, #17191a 0%, #1c1d1d 55%, #171819 100%);
- border-right: 1px solid #39332d; +} +[data-testid="stSidebar"] [data-testid="stMarkdownContainer"] h2 {
- margin: 0 0 0.15rem;
- color: #e7d4b8 !important;
- font-size: 1.18rem;
- letter-spacing: 0.12em;
- text-transform: uppercase; +} +[data-testid="stSidebar"] [data-testid="stMarkdownContainer"] h3 {
- margin: 1.15rem 0 0.35rem;
- padding-bottom: 0.35rem;
- border-bottom: 1px solid rgba(176, 132, 82, 0.22);
- color: #bda17e !important;
- font-size: 0.76rem;
- letter-spacing: 0.14em;
- text-transform: uppercase; } +[data-testid="stSidebar"] [data-testid="stMarkdownContainer"] p {
- color: #c9c0b3;
- line-height: 1.55; +} h1, h2, h3 { color: #e8ddca !important; font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; } +.page-header {
- margin: 0.25rem 0 1.2rem;
- padding: 0.55rem 0 1rem;
- border-bottom: 1px solid rgba(176, 132, 82, 0.32);
- background:
-
radial-gradient(ellipse at 82% 0%, rgba(116, 77, 45, 0.16), transparent 48%);
+} +.page-header .eyebrow {
- margin: 0 0 0.45rem;
- color: #bb8b58;
- font-size: 0.72rem;
- letter-spacing: 0.2em;
- text-transform: uppercase; +} +.page-header .title {
- margin: 0;
- color: #eee1cd;
- font-size: clamp(1.8rem, 3vw, 2.45rem);
- line-height: 1.1; +} +.page-header .subtitle {
- max-width: 42rem;
- margin: 0.55rem 0 0;
- color: #b9afa1;
- font-size: 1rem;
- font-style: italic;
- line-height: 1.55; +} .story { font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; font-size: 1.08rem; @@ -222,9 +270,11 @@
with st.sidebar:
- st.markdown("## THE HOUSE")
- st.markdown('<p class="muted">YOUR PRIVATE RECORD</p>',
-
unsafe_allow_html=True)
-
st.markdown(
-
'<div class="sidebar-brand"><div class="brand-name">THE HOUSE</div>' -
'<div class="muted">YOUR PRIVATE RECORD</div></div>', -
unsafe_allow_html=True, -
)
st.divider()
@@ -299,9 +349,15 @@ play_background_music(Path("assets") / "ambience.mp3")
-st.markdown('<p class="muted">A GOTHIC HORROR ADVENTURE</p>',
-
unsafe_allow_html=True)
-st.title("The House That Remembers") +st.markdown(
- '<header class="page-header">'
- '<p class="eyebrow">A gothic horror adventure · Chapter I</p>'
- '<h1 class="title">The House That Remembers</h1>'
- '<p class="subtitle">The door has closed behind you. Somewhere in the house, '
- 'something remembers.</p>'
- '</header>',
- unsafe_allow_html=True, +)
with st.expander("The letter and arrival", expanded=True): _, image_column, _ = st.columns([1, 2, 1]) *** Begin Patch *** Update File: the-house-that-remembers/app.py @@ [data-testid="stSidebar"] [data-testid="stMarkdownContainer"] p { color: #c9c0b3; line-height: 1.55; } +.sidebar-brand {... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -155,6 +155,21 @@ color: #c9c0b3; line-height: 1.55; } +.sidebar-brand {
- padding: 0.35rem 0 0.85rem;
- border-bottom: 1px solid rgba(176, 132, 82, 0.32); +} +.sidebar-brand .brand-name {
- margin-bottom: 0.25rem;
- color: #e7d4b8;
- font-size: 1.05rem;
- letter-spacing: 0.14em; +} +.sidebar-brand .muted {
- color: #9d9386;
- font-size: 0.64rem;
- letter-spacing: 0.16em; +} h1, h2, h3 { color: #e8ddca !important; font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; @@ -351,10 +366,9 @@ st.markdown( '<header class="page-header">'
- '<p class="eyebrow">A gothic horror adventure · Chapter I</p>'
- '<p class="eyebrow">A gothic horror adventure · Private journal</p>' '<h1 class="title">The House That Remembers</h1>'
- '<p class="subtitle">The door has closed behind you. Somewhere in the house, '
- 'something remembers.</p>'
- '<p class="subtitle">Some doors should stay closed.</p>' '</header>', unsafe_allow_html=True, ) {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py; python -m unittest discover -s tests -p 'test_engine.py'","descr... ........................
Ran 24 tests in 0.009s
OK <shellId: 117 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py; git --no-pager diff --stat -- the-house-that-remembers/... warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it the-house-that-remembers/app.py | 178 ++++++++++++++++++++++++++++++++++------ 1 file changed, 153 insertions(+), 25 deletions(-) <shellId: 118 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 240, 285 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -240,46 +240,46 @@ } elif game["stage"] >= 5: house_lighting = { "light": "rgba(103, 105, 121, 0.07)", "base": "#191a1e", "vignette": "rgba(0, 0, 0, 0.76)", } elif game["stage"] >= 4: house_lighting = { "light": "rgba(89, 98, 119, 0.09)", "base": "#1a1c21", "vignette": "rgba(0, 0, 0, 0.72)", } elif game["stage"] >= 3: house_lighting = { "light": "rgba(105, 79, 111, 0.10)", "base": "#1e1b22", "vignette": "rgba(0, 0, 0, 0.66)", } elif game["stage"] >= 2: house_lighting = { "light": "rgba(98, 111, 132, 0.11)", "base": "#1d2025", "vignette": "rgba(0, 0, 0, 0.61)", } else: house_lighting = { "light": "rgba(136, 99, 60, 0.13)", "base": "#211e1b", "vignette": "rgba(0, 0, 0, 0.56)", }
st.markdown( f""" <style> .stApp {{ --house-background: radial-gradient(ellipse at 50% 12%, {house_lighting["light"]}, transparent 58%), radial-gradient(ellipse at center, transparent 38%, {house_lighting["vignette"]} 100%), {house_lighting["base"]}; }} </style> """, unsafe_allow_html=True, ) {"pattern":"sidebar-brand|st\.divider\(\)|Current location","paths":["C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py"],"output_mode":"content","-n":true,"hea... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:158:.sidebar-brand { C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:162:.sidebar-brand .brand-name { C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:168:.sidebar-brand .muted { C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:289: '<div class="sidebar-brand"><div class="brand-name">THE HOUSE</div>' C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:294: st.divider() C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:296: st.markdown("### Current location") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:351: st.divider() *** Begin Patch *** Update File: the-house-that-remembers/app.py @@
-
st.divider()
-
st.markdown("### Current location") *** End Patch diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -291,8 +291,6 @@ unsafe_allow_html=True, )
-
st.divider()
-
st.markdown("### Current location") st.write(game["room"]) { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py", "description": "Verify sidebar spacing edit" } <shellId: 120 completed with exit code 0> { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py", "description": "Check sidebar diff formatting" } warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it <shellId: 119 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 1, 18 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -1,18 +1,18 @@ import base64 import copy import json from pathlib import Path
import ollama import streamlit as st
from engine import ( FINAL_REVELATIONS, FRAGMENT_LABELS, FRAGMENT_KINDS, FRAGMENT_CLUES_NEEDED, ROOMS, apply_action, fragment_progress, fragments_complete, new_game, { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 172, 210 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -172,39 +172,39 @@ } h1, h2, h3 { color: #e8ddca !important; font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; } .page-header { margin: 0.25rem 0 1.2rem; padding: 0.55rem 0 1rem; border-bottom: 1px solid rgba(176, 132, 82, 0.32); background: radial-gradient(ellipse at 82% 0%, rgba(116, 77, 45, 0.16), transparent 48%); } .page-header .eyebrow { margin: 0 0 0.45rem; color: #bb8b58; font-size: 0.72rem; letter-spacing: 0.2em; text-transform: uppercase; } .page-header .title { margin: 0; color: #eee1cd; font-size: clamp(1.8rem, 3vw, 2.45rem); line-height: 1.1; } .page-header .subtitle { max-width: 42rem; margin: 0.55rem 0 0; color: #b9afa1; font-size: 1rem; font-style: italic; line-height: 1.55; } .story { font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; font-size: 1.08rem; line-height: 1.9; color: #d8d1c7; } { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 355, 385 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -355,31 +355,31 @@
st.subheader("Atmosphere")
sound_enabled = st.checkbox("Background rain", value=True)
if sound_enabled:
play_background_music(Path("assets") / "rain-and-thunder.mp3")
house_ambience_enabled = st.checkbox("House ambience", value=True)
if house_ambience_enabled:
play_background_music(Path("assets") / "ambience.mp3")
st.markdown( '<header class="page-header">' '<p class="eyebrow">A gothic horror adventure · Private journal</p>' '<h1 class="title">The House That Remembers</h1>' '<p class="subtitle">Some doors should stay closed.</p>' '</header>', unsafe_allow_html=True, )
with st.expander("The letter and arrival", expanded=True): _, image_column, _ = st.columns([1, 2, 1]) with image_column: show_image( Path(file).resolve().parent / "assets" / "haunted_house.jpg", width=480, ) st.markdown(INTRO)
scene_column, conversation_column = st.columns([1, 1.25], gap="large") room = ROOMS[game["room"]] { "pattern": "assets/haunted_house.jpg", "paths": [ "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers" ] } No files matched the pattern. php {"command":"$p='C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\assets\haunted_house.jpg'; if (Test-Path $p) { Get-Item $p | Select-Object FullName,Length } else { Wri... FullName Length
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\assets\haunted_house.jpg 940340 <shellId: 121 completed with exit code 0> python {"command":"python -c "from PIL import Image; p=r'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\assets\haunted_house.jpg'; im=Image.open(p); print(im.size, im.mode)... (3072, 2048) RGB <shellId: 122 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -c "from PIL import Image; print('Pillow available')"","description":"Check image op... Pillow available <shellId: 123 completed with exit code 0> python *** Begin Patch *** Update File: the-house-that-remembers/app.py @@ import base64 import copy import json +from io import BytesIO from pathlib import Path
import ollama import streamlit as st ... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -1,10 +1,12 @@ import base64 import copy import json +from io import BytesIO from pathlib import Path
import ollama import streamlit as st +from PIL import Image
from engine import ( FINAL_REVELATIONS, @@ -69,6 +71,18 @@ st.caption(f"(image not found: {Path(path).name})")
+@st.cache_data(show_spinner=False) +def haunted_house_banner():
- image_path = Path(file).resolve().parent / "assets" / "haunted_house.jpg"
- if not image_path.exists():
-
return "" - with Image.open(image_path) as image:
-
image.thumbnail((1800, 900)) -
optimized = BytesIO() -
image.convert("RGB").save(optimized, format="JPEG", quality=82, optimize=True) - return base64.b64encode(optimized.getvalue()).decode("ascii")
@st.dialog("The Letter", width="small") def show_letter_dialog(): letter_path = Path(file).resolve().parent / "assets" / "letter.png" @@ -175,11 +189,16 @@ font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; } .page-header {
- min-height: 230px; margin: 0.25rem 0 1.2rem;
- padding: 0.55rem 0 1rem;
- padding: 1.5rem 1.75rem;
- display: flex;
- flex-direction: column;
- justify-content: flex-end; border-bottom: 1px solid rgba(176, 132, 82, 0.32);
- background:
-
radial-gradient(ellipse at 82% 0%, rgba(116, 77, 45, 0.16), transparent 48%);
- background-color: #17191a;
- background-position: center 43%;
- background-size: cover; } .page-header .eyebrow { margin: 0 0 0.45rem; @@ -362,8 +381,18 @@ play_background_music(Path("assets") / "ambience.mp3")
+banner_image = haunted_house_banner() +banner_style = "" +if banner_image:
- banner_style = (
-
' style="background-image: ' -
'linear-gradient(90deg, rgba(10, 12, 14, 0.94) 0%, ' -
'rgba(10, 12, 14, 0.72) 48%, rgba(10, 12, 14, 0.20) 100%), ' -
'linear-gradient(0deg, rgba(15, 16, 17, 0.78), transparent 68%), ' -
f'url(data:image/jpeg;base64,{banner_image});"' - ) st.markdown(
- '<header class="page-header">'
- f'<header class="page-header"{banner_style}>' '<p class="eyebrow">A gothic horror adventure · Private journal</p>' '<h1 class="title">The House That Remembers</h1>' '<p class="subtitle">Some doors should stay closed.</p>' @@ -372,12 +401,6 @@ )
with st.expander("The letter and arrival", expanded=True):
- _, image_column, _ = st.columns([1, 2, 1])
- with image_column:
-
show_image( -
Path(__file__).resolve().parent / "assets" / "haunted_house.jpg", -
width=480, -
st.markdown(INTRO))
scene_column, conversation_column = st.columns([1, 1.25], gap="large") {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py; python -m unittest discover -s tests -p 'test_engine.py'","descr... ........................ #
Ran 24 tests in 0.009s
OK <shellId: 124 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; @'\nfrom io import BytesIO\nfrom pathlib import Path\nfrom PIL import Image\npath = Path('ass... Optimized banner: 1350x900, 95273 bytes <shellId: 126 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py; git --no-pager diff --stat -- the-house-that-remembers/... warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it the-house-that-remembers/app.py | 215 ++++++++++++++++++++++++++++++++++------ 1 file changed, 182 insertions(+), 33 deletions(-) <shellId: 125 completed with exit code 0> { "url": "https://www.flaticon.com/uicons/interface-icons", "max_length": 6000 } { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 280, 380 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -280,101 +280,101 @@ "light": "rgba(98, 111, 132, 0.11)", "base": "#1d2025", "vignette": "rgba(0, 0, 0, 0.61)", } else: house_lighting = { "light": "rgba(136, 99, 60, 0.13)", "base": "#211e1b", "vignette": "rgba(0, 0, 0, 0.56)", }
st.markdown( f""" <style> .stApp {{ --house-background: radial-gradient(ellipse at 50% 12%, {house_lighting["light"]}, transparent 58%), radial-gradient(ellipse at center, transparent 38%, {house_lighting["vignette"]} 100%), {house_lighting["base"]}; }} </style> """, unsafe_allow_html=True, )
with st.sidebar: st.markdown( '<div class="sidebar-brand"><div class="brand-name">THE HOUSE</div>' '<div class="muted">YOUR PRIVATE RECORD</div></div>', unsafe_allow_html=True, )
st.markdown("### Current location")
st.write(game["room"])
st.markdown("### Friend")
friend_room = game.get("friend_room", "Entrance Hall")
st.write("Here with you" if friend_room == game["room"] else f"In the {friend_room}")
st.markdown("### Memory fragments")
completed_fragments = len(fragments_complete(game))
st.write(f"**{completed_fragments}/3 complete**")
fragment_status = fragment_progress(game)
for kind in FRAGMENT_KINDS:
progress = fragment_status[kind]
marker = "✓" if progress["complete"] else "○"
st.caption(
f"{marker} {FRAGMENT_LABELS[kind]}: "
f"{progress['found']}/{FRAGMENT_CLUES_NEEDED} clues"
)
if game.get("truth_known"):
st.markdown("### Family memories")
shared_memories = game.get("final_revelations", [])
st.write(f"**{len(shared_memories)}/{len(FINAL_REVELATIONS)} remembered**")
for memory in FINAL_REVELATIONS:
if memory["id"] in shared_memories:
st.caption(f"✓ {memory['title']}")
st.markdown("### Inventory")
if game["inventory"]:
for item in game["inventory"]:
if item == "The Letter":
if st.button("The Letter · Inspect", key="open_letter"):
show_letter_dialog()
elif item == "Library Book":
if st.button("Library Book · Inspect", key="open_book"):
show_book_dialog()
elif item == "Upper Landing Note":
if st.button("Upper Landing Note · Inspect", key="open_note"):
show_note_dialog()
else:
st.write(f"• {item}")
else:
st.caption("Your pockets are empty.")
st.markdown("### Clues")
if game["clues"]:
for clue in game["clues"]:
st.write(f"• {clue}")
else:
st.caption("Nothing discovered yet.")
st.markdown("### Explored")
for visited_room in game["visited"]:
st.write(f"✓ {visited_room}")
st.divider()
if st.button("Restart adventure", use_container_width=True):
st.session_state.pop("game", None)
st.session_state.pop("ollama_error", None)
st.rerun()
st.subheader("Atmosphere")
sound_enabled = st.checkbox("Background rain", value=True)
if sound_enabled:
play_background_music(Path("assets") / "rain-and-thunder.mp3")
house_ambience_enabled = st.checkbox("House ambience", value=True)
if house_ambience_enabled:
{ "url": "https://www.flaticon.com/license", "max_length": 5000 } { "query": "Open browser page to visit a website and inspect page content when web_fetch gets HTTP 403" } [ "readPage", "openBrowserPage", "screenshotPage", "navigatePage", "pylance-mcp-server-pylanceDocuments" ] { "url": "https://www.flaticon.com/uicons/interface-icons", "forceNew": true } { "pageId": "dec9b0dc-2cdd-4859-85ea-32d4b89a998e" } Page Title: Uicons: 50,000+ Free icons, SVG and icon font. Awesome icons for any project URL: https://www.flaticon.com/uicons/interface-icons Recent events:
- [2026-10-28T21:49:43.289Z] (console) [error] Failed to load resource: net::ERR_CONNECTION_RESET
- [2026-10-28T21:49:44.257Z] (console) [error] Provider's accounts list is empty.
- [2026-10-28T21:49:43.976Z] (console) [warning] [GSI_LOGGER]: Your client application uses one of the Google One Tap prompt UI status methods that may stop functioning when FedCM becomes mandatory. Refer to the migration guide to update your code accordingly and opt-in to FedCM to test your changes. Learn more: https://developers.google.com/identity/gsi/web/guides/fedcm-migration?s=dc#display_moment and https://developers.google.com/identity/gsi/web/guides/fedcm-migration?s=dc#skipped_moment
- [2026-10-28T21:49:44.084Z] (console) [warning] [GPT] Anchor /78011819,9837927/FLATICON_COM_LIST_1 not requested: Format already created on the page. https://goo.gle/gpt-message#134
- [2026-10-28T21:49:44.561Z] (console) [warning] [amp-analytics/transport] Response unparseable or failed to send image request https://securepubads.g.doubleclick.net/btr/view?ai=CW5LT923iapPaDfHv0vAPzLCc6ArOpYmvigHLlK_njRailMSSjQ4QASDjyKwiYMmOqIfco_ARoAHctdrVQcgBBqkCOfoiZ7cnqj7gAgCoAwHIAwqqBKICT9BMnKXZLyXkgaZ53oMFbpcT3BpxSA94pvTNvHCdZOZQv5muWjX_Qu3U1SA8V_C_mgOkU3SIcH4F6psTkaABbp9RbpWTLZt2HpZDiA6nR57zy9EEsuapy4CVe5E4PEx3Ew0pbH5KjngE91J5J5QPannHcfUdqo1rofM8u6oJopMkGowD9ON5GxB2NjAB6ayNuo2r96yf-vRBohJrufbqvFkodmgxhjmkXvgw92hhVRj4sOEyxZS1RdLoAIiG1KrUTD1Ols8AOUkXfWLqDPD4WeaR9beQ_wjUARO51CQJDZE_Bn9ABEDeDG0FnCCD_IOYUwW2zsrOV02rMLXk-E53MJyrSFhrhDuWYBYDR8fJV0_R1abyXKVec8KAZQU5ra4UolnABNXE7f7TBeAEAYgF4KbS1liSBQQIBBgBkgUECAUYBIAH3O2qtRyoB6nrsQKoB6fMsQKoB6a-G6gHzM6xAqgH89EbqAeW2BuoB7rwsQKoB6qbsQKoB_7osQKoB47OG6gHk9gbqAfw4BuoB-6WsQKoB_6esQKoB6--sQKoB5_hsQKoB6brsQKoB9XJG6gH2baxAqgH98KxAtgHAfIHBRCyy5AB0ggyCIBhEAEYHTIIioKAgICAgAg6D4BAgMCAgICAqIACqIOQGEi9_cE6WPPTxNPY3ZcDYAHyCBthZHgtc3Vic3luLTY2MTE4NDU2OTU4MzA2NzSACgPICwGiDAgqBgoE9vKxAqoNAlVT4g0TCJ-qxdPY3ZcDFfG3FAgdTBgHreoNEwja-cXT2N2XAxXxtxQIHUwYB62IDv___________wGwDvzHn_4Y2BMLiBQB0BUByhYCCgD4FgGAFwGyFy4KHAgAEhRwdWItMzE5Mjc2MzI0MTIxMzg1Mxi72hQYASoKNjk3MjU5NzU4NVAGuhcCOAGqGBcJAAAAANkSQkESCjY5NzI1OTc1ODUYAbIYCRIC604YASIBANAYAegYAcIZAggB&sigh=YRLrPrXc0KI&uach_m=%5B%5D&cid=CAQSsgIAQM4h3BxWGGgb_lYVM5D5T_iD-hgD72XhxlgHwHm2m5oId450HUmROvVSsStaAeOP_u712s2vac85S7o5ETXW3ePPug6E2m3dGvjbv9xDRFKHEcj6c-Ac6M85KL8e3S1EA1BZZhDhXTM0aMoqH1CwmeGnZ8tXT_i0ZNMU6Qer7oMLGVPq9G31Vy9MUocYAdBMOHm0A3YUDeO9dUmS-7hZmeFp9i_r24vbOkPmYvutbvJQBMTAJSv6JpEVvwjHdw6pYF0fDRExbnXu2Bk7Lc3D9OeuYRsW9dJxYp7VUJr4koAFRKr5E10gcfvjHJlFjTTPXH6zq2rVOOUf-6i-pEU0mt-IkO04ufWTof9ZOrpHSytxqZA1tvMJtp3v6FReDSIxwBcolRZehUKuKiqdVFIDv_wYAQ&template_id=5001&ibtr=1
- [2026-10-28T21:49:44.563Z] (console) [warning] [amp-analytics/transport] Response unparseable or failed to send image request https://securepubads.g.doubleclick.net/pagead/adview?ai=CW5LT923iapPaDfHv0vAPzLCc6ArOpYmvigHLlK_njRailMSSjQ4QASDjyKwiYMmOqIfco_ARoAHctdrVQcgBBqkCOfoiZ7cnqj7gAgCoAwHIAwqqBKICT9BMnKXZLyXkgaZ53oMFbpcT3BpxSA94pvTNvHCdZOZQv5muWjX_Qu3U1SA8V_C_mgOkU3SIcH4F6psTkaABbp9RbpWTLZt2HpZDiA6nR57zy9EEsuapy4CVe5E4PEx3Ew0pbH5KjngE91J5J5QPannHcfUdqo1rofM8u6oJopMkGowD9ON5GxB2NjAB6ayNuo2r96yf-vRBohJrufbqvFkodmgxhjmkXvgw92hhVRj4sOEyxZS1RdLoAIiG1KrUTD1Ols8AOUkXfWLqDPD4WeaR9beQ_wjUARO51CQJDZE_Bn9ABEDeDG0FnCCD_IOYUwW2zsrOV02rMLXk-E53MJyrSFhrhDuWYBYDR8fJV0_R1abyXKVec8KAZQU5ra4UolnABNXE7f7TBeAEAYgF4KbS1liSBQQIBBgBkgUECAUYBIAH3O2qtRyoB6nrsQKoB6fMsQKoB6a-G6gHzM6xAqgH89EbqAeW2BuoB7rwsQKoB6qbsQKoB_7osQKoB47OG6gHk9gbqAfw4BuoB-6WsQKoB_6esQKoB6--sQKoB5_hsQKoB6brsQKoB9XJG6gH2baxAqgH98KxAtgHAfIHBRCyy5AB0ggyCIBhEAEYHTIIioKAgICAgAg6D4BAgMCAgICAqIACqIOQGEi9_cE6WPPTxNPY3ZcDYAHyCBthZHgtc3Vic3luLTY2MTE4NDU2OTU4MzA2NzSACgPICwGiDAgqBgoE9vKxAqoNAlVT4g0TCJ-qxdPY3ZcDFfG3FAgdTBgHreoNEwja-cXT2N2XAxXxtxQIHUwYB62IDv___________wGwDvzHn_4Y2BMLiBQB0BUByhYCCgD4FgGAFwGyFy4KHAgAEhRwdWItMzE5Mjc2MzI0MTIxMzg1Mxi72hQYASoKNjk3MjU5NzU4NVAGuhcCOAGqGBcJAAAAANkSQkESCjY5NzI1OTc1ODUYAbIYCRIC604YASIBANAYAegYAcIZAggB&sigh=YRLrPrXc0KI&uach_m=%5B%5D&cid=CAQSsgIAQM4h3BxWGGgb_lYVM5D5T_iD-hgD72XhxlgHwHm2m5oId450HUmROvVSsStaAeOP_u712s2vac85S7o5ETXW3ePPug6E2m3dGvjbv9xDRFKHEcj6c-Ac6M85KL8e3S1EA1BZZhDhXTM0aMoqH1CwmeGnZ8tXT_i0ZNMU6Qer7oMLGVPq9G31Vy9MUocYAdBMOHm0A3YUDeO9dUmS-7hZmeFp9i_r24vbOkPmYvutbvJQBMTAJSv6JpEVvwjHdw6pYF0fDRExbnXu2Bk7Lc3D9OeuYRsW9dJxYp7VUJr4koAFRKr5E10gcfvjHJlFjTTPXH6zq2rVOOUf-6i-pEU0mt-IkO04ufWTof9ZOrpHSytxqZA1tvMJtp3v6FReDSIxwBcolRZehUKuKiqdVFIDv_wYAQ&template_id=5001&vt=11
- [2026-10-28T21:49:45.002Z] (console) [warning] [Audigent] cannot find __gpp framework
- [2026-10-28T21:49:46.605Z] (console) [error] [GSI_LOGGER]: FedCM get() rejects with NetworkError: Error retrieving a token. Snapshot:
- generic [ref=e1]:
- generic [ref=e2]:
- generic [ref=e3]:
- generic [ref=e5]:
- link "Flaticon logo" [ref=e6] [cursor=pointer]:
- /url: https://www.flaticon.com/
- img "Flaticon
- link "Flaticon logo" [ref=e6] [cursor=pointer]:
- generic [ref=e5]:
- generic [ref=e3]:
- generic [ref=e2]:
[Truncated — full output (31.5 KB) temporarily saved to C:\Users\lucia\AppData\Local\Temp\1793224298522-copilot-tool-output-63f75edb69f84862a761bcecbadcf3ef.txt]
can you do it?
Location: <i class="fi fi-ss-marker"></i>
I’ll add the marker beside the current location using the Uicons stylesheet and first check your existing source/attribution notes so the integration follows your project’s licensing choices.
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\assets\Sources.md" } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/assets/Sources.md b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/assets/Sources.md index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/assets/Sources.md +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/assets/Sources.md @@ -1,21 +1,21 @@ Images and Audio effects are from Unsplash and Pixabay (royalty-free)
person walking towards house Photo by <a href="https://unsplash.com/@janjakubnanista?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Ján Jakub Naništa</a> on <a href="https://unsplash.com/photos/person-walking-towards-house-z9hvkSDWMIM?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
Dark hallway Photo by <a href="https://unsplash.com/@tama66?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Peter Herrmann</a> on <a href="https://unsplash.com/photos/dark-hallway-with-light-at-the-end-6Tj2nKIib8Y?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
a room with a couch and a curatined window Photo by <a href="https://unsplash.com/@tama66?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Peter Herrmann</a> on <a href="https://unsplash.com/photos/a-room-with-a-couch-and-a-curtained-window-SHXJsoDjUDI?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
Moody kitchen Photo by <a href="https://unsplash.com/@tylerchandlerr?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Tyler Chandler</a> on <a href="https://unsplash.com/photos/white-top-mount-refrigerator-beside-white-wooden-door-c-BbUMdX73M?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
Library Photo by <a href="https://unsplash.com/@calowaii?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Shane Monarc</a> on <a href="https://unsplash.com/photos/bookshelf-and-framed-photo-collage-45FOosfKz2w?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
Staircase Photo by <a href="https://unsplash.com/@lucabravo?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Luca Bravo</a> on <a href="https://unsplash.com/photos/low-angle-view-of-gray-stairs-t3ItLqF-PPU?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
Upper Landing Photo by <a href="https://unsplash.com/@chrisbair?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Chris Bair</a> on <a href="https://unsplash.com/photos/brown-wooden-coffee-table-near-wall-inside-room-v0CMjwnC3a0?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>
Sound Effect by <a href="https://pixabay.com/users/jci-21-21704840/?utm_source=link-attribution&utm_medium=referral&utm_campaign=music&utm_content=12820">Juan Carlos</a> from <a href="https://pixabay.com/sound-effects//?utm_source=link-attribution&utm_medium=referral&utm_campaign=music&utm_content=12820">Pixabay</a>
Music by <a href="https://pixabay.com/users/adamklimt-46175304/?utm_source=link-attribution&utm_medium=referral&utm_campaign=music&utm_content=624786">Adam Klimt</a> from <a href="https://pixabay.com/music//?utm_source=link-attribution&utm_medium=referral&utm_campaign=music&utm_content=624786">Pixabay</a>
Uicons by <a href="https://www.flaticon.com/uicons">Flaticon</a> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 1, 22 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -1,22 +1,22 @@ import base64 import copy import json from io import BytesIO from pathlib import Path
import ollama import streamlit as st from PIL import Image
from engine import ( FINAL_REVELATIONS, FRAGMENT_LABELS, FRAGMENT_KINDS, FRAGMENT_CLUES_NEEDED, ROOMS, apply_action, fragment_progress, fragments_complete, new_game, ) from prompt import build_messages, generation_options, maybe_summarize, tidy { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 130, 230 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -130,101 +130,101 @@ > </audio> """, unsafe_allow_html=True, )
st.markdown(""" <style> body, .stApp, .stApp input, .stApp textarea, .stApp button { font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; } .stApp { background: var(--house-background, #171719); color: #e8ddca; transition: background 1.2s ease; } [data-testid="stSidebar"] { background: linear-gradient(180deg, #17191a 0%, #1c1d1d 55%, #171819 100%); border-right: 1px solid #39332d; } [data-testid="stSidebar"] [data-testid="stMarkdownContainer"] h2 { margin: 0 0 0.15rem; color: #e7d4b8 !important; font-size: 1.18rem; letter-spacing: 0.12em; text-transform: uppercase; } [data-testid="stSidebar"] [data-testid="stMarkdownContainer"] h3 { margin: 1.15rem 0 0.35rem; padding-bottom: 0.35rem; border-bottom: 1px solid rgba(176, 132, 82, 0.22); color: #bda17e !important; font-size: 0.76rem; letter-spacing: 0.14em; text-transform: uppercase; } [data-testid="stSidebar"] [data-testid="stMarkdownContainer"] p { color: #c9c0b3; line-height: 1.55; } .sidebar-brand { padding: 0.35rem 0 0.85rem; border-bottom: 1px solid rgba(176, 132, 82, 0.32); } .sidebar-brand .brand-name { margin-bottom: 0.25rem; color: #e7d4b8; font-size: 1.05rem; letter-spacing: 0.14em; } .sidebar-brand .muted { color: #9d9386; font-size: 0.64rem; letter-spacing: 0.16em; } h1, h2, h3 { color: #e8ddca !important; font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important; } .page-header { min-height: 230px; margin: 0.25rem 0 1.2rem; padding: 1.5rem 1.75rem; display: flex; flex-direction: column; justify-content: flex-end; border-bottom: 1px solid rgba(176, 132, 82, 0.32); background-color: #17191a; background-position: center 43%; background-size: cover; } .page-header .eyebrow { margin: 0 0 0.45rem; color: #bb8b58; font-size: 0.72rem; letter-spacing: 0.2em; text-transform: uppercase; } .page-header .title { margin: 0; color: #eee1cd; font-size: clamp(1.8rem, 3vw, 2.45rem); line-height: 1.1; } .page-header .subtitle { max-width: 42rem; margin: 0.55rem 0 0; color: #b9afa1; font-size: 1rem; font-style: italic; line-height: 1.55; } .story { font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif; font-size: 1.08rem; line-height: 1.9; color: #d8d1c7; } .muted { { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 280, 315 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -280,36 +280,36 @@ "light": "rgba(98, 111, 132, 0.11)", "base": "#1d2025", "vignette": "rgba(0, 0, 0, 0.61)", } else: house_lighting = { "light": "rgba(136, 99, 60, 0.13)", "base": "#211e1b", "vignette": "rgba(0, 0, 0, 0.56)", }
st.markdown( f""" <style> .stApp {{ --house-background: radial-gradient(ellipse at 50% 12%, {house_lighting["light"]}, transparent 58%), radial-gradient(ellipse at center, transparent 38%, {house_lighting["vignette"]} 100%), {house_lighting["base"]}; }} </style> """, unsafe_allow_html=True, )
with st.sidebar: st.markdown( '<div class="sidebar-brand"><div class="brand-name">THE HOUSE</div>' '<div class="muted">YOUR PRIVATE RECORD</div></div>', unsafe_allow_html=True, )
st.markdown("### Current location")
st.write(game["room"])
*** Begin Patch *** Update File: the-house-that-remembers/app.py @@ st.set_page_config(page_title="The House That Remembers", layout="wide") +st.markdown(
- "<link rel='stylesheet' "
- "href='h... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -22,6 +22,12 @@ from prompt import build_messages, generation_options, maybe_summarize, tidy
st.set_page_config(page_title="The House That Remembers", layout="wide") +st.markdown(
- "<link rel='stylesheet' "
- "href='https://cdn-uicons.flaticon.com/uicons-solid-straight/css/"
- "uicons-solid-straight.css'>",
- unsafe_allow_html=True, +)
MODEL_NAME = "gemma4:e4b" CANON_PATH = Path(file).resolve().parent / "story" / "canon.json" @@ -165,6 +171,11 @@ letter-spacing: 0.14em; text-transform: uppercase; } +.location-heading .fi {
- margin-right: 0.45rem;
- color: #b98b5e;
- font-size: 0.9em; +} [data-testid="stSidebar"] [data-testid="stMarkdownContainer"] p { color: #c9c0b3; line-height: 1.55; @@ -310,7 +321,11 @@ unsafe_allow_html=True, )
- st.markdown("### Current location")
-
st.markdown(
-
'<h3 class="location-heading"><i class="fi fi-ss-marker" ' -
'aria-hidden="true"></i>Current location</h3>', -
unsafe_allow_html=True, -
) st.write(game["room"])
st.markdown("### Friend") { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py", "description": "Verify Uicons integration compiles" } <shellId: 127 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py; git --no-pager diff -- the-house-that-remembers/app.py"... Output too large to read at once (11.5 KB). Saved to: C:\Users\lucia\AppData\Local\Temp\1793224443085-copilot-tool-output-8656-b70d25ef-a156-4817-9d73-96da0290b8ed.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): warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it diff --git a/the-house-that-remembers/app.py b/the-house-that-remembers/app.py index 6c92c79..43fd424 100644 --- a/the-house-that-remembers/app.py +++ b/the-house-that-remembers/app.py @@ -1,10 +1,12 @@ import base64 import copy import json +from io <shellId: 128 completed with exit code 0> { "url": "https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css", "max_length": 1200 } Content type text/css cannot be simplified to markdown. Here is the raw content: Contents of https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css: @font-face { font-family: "uicons-solid-straight"; src: url("https://cdn-uicons.flaticon.com/uicons-solid-straight/webfonts/uicons-solid-straight.eot#iefix") format("embedded-opentype"), url("https://cdn-uicons.flaticon.com/uicons-solid-straight/webfonts/uicons-solid-straight.woff2") format("woff2"), url("https://cdn-uicons.flaticon.com/uicons-solid-straight/webfonts/uicons-solid-straight.woff") format("woff"); }
i[class^="fi-ss-"]:before, i[class*=" fi-ss-"]:before, span[class^="fi-ss-"]:before, span[class*="fi-ss-"]:before {
font-family: uicons-solid-straight !important; font-style: normal; font-weight: normal !important; font-variant: normal; text-transform: none; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }
.fi-ss-0:before {
content: "\f101";
}
.fi-ss-1:before {
content: "\f102";
}
.fi-ss-2:before {
content: "\f103";
}
.fi-ss-3:before {
content: "\f104";
}
.fi-ss-360-degrees:before {
content: "\f105";
}
.fi-ss-4:before {
content: "\f106";
}
.fi-ss-5:before {
content: "\f107";
}
.fi-ss-6:before {
content:
<note>Content truncated. Call the fetch tool with a start_index of 1200 to get more content.</note> { "url": "https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css", "start_index": 8500, "max_length": 5000, "raw": true } Here is the raw content: Contents of https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css: 175"; } .fi-ss-arrow-square-down:before { content: "\f176"; } .fi-ss-arrow-square-left:before { content: "\f177"; } .fi-ss-arrow-square-right:before { content: "\f178"; } .fi-ss-arrow-square-up:before { content: "\f179"; } .fi-ss-arrow-to-bottom:before { content: "\f17a"; } .fi-ss-arrow-to-left:before { content: "\f17b"; } .fi-ss-arrow-to-right:before { content: "\f17c"; } .fi-ss-arrow-to-top:before { content: "\f17d"; } .fi-ss-arrow-trend-down:before { content: "\f17e"; } .fi-ss-arrow-trend-up:before { content: "\f17f"; } .fi-ss-arrow-turn-down-left:before { content: "\f180"; } .fi-ss-arrow-turn-down-right:before { content: "\f181"; } .fi-ss-arrow-up-from-dotted-line:before { content: "\f182"; } .fi-ss-arrow-up-from-square:before { content: "\f183"; } .fi-ss-arrow-up-left-from-circle:before { content: "\f184"; } .fi-ss-arrow-up-left:before { content: "\f185"; } .fi-ss-arrow-up-right-and-arrow-down-left-from-center:before { content: "\f186"; } .fi-ss-arrow-up-right-from-square:before { content: "\f187"; } .fi-ss-arrow-up-right:before { content: "\f188"; } .fi-ss-arrow-up-small-big:before { content: "\f189"; } .fi-ss-arrow-up-square-triangle:before { content: "\f18a"; } .fi-ss-arrow-up-to-dotted-line:before { content: "\f18b"; } .fi-ss-arrow-up:before { content: "\f18c"; } .fi-ss-arrows-alt-h:before { content: "\f18d"; } .fi-ss-arrows-alt-v:before { content: "\f18e"; } .fi-ss-arrows-alt:before { content: "\f18f"; } .fi-ss-arrows-cross:before { content: "\f190"; } .fi-ss-arrows-from-dotted-line:before { content: "\f191"; } .fi-ss-arrows-from-line:before { content: "\f192"; } .fi-ss-arrows-h-copy:before { content: "\f193"; } .fi-ss-arrows-h:before { content: "\f194"; } .fi-ss-arrows-repeat-1:before { content: "\f195"; } .fi-ss-arrows-repeat:before { content: "\f196"; } .fi-ss-arrows-retweet:before { content: "\f197"; } .fi-ss-arrows-to-dotted-line:before { content: "\f198"; } .fi-ss-arrows-to-line:before { content: "\f199"; } .fi-ss-arrows:before { content: "\f19a"; } .fi-ss-assept-document:before { content: "\f19b"; } .fi-ss-assistive-listening-systems:before { content: "\f19c"; } .fi-ss-asterik:before { content: "\f19d"; } .fi-ss-at:before { content: "\f19e"; } .fi-ss-attribution-pen:before { content: "\f19f"; } .fi-ss-attribution-pencil:before { content: "\f1a0"; } .fi-ss-aubergine:before { content: "\f1a1"; } .fi-ss-audio-description-slash:before { content: "\f1a2"; } .fi-ss-austral-sign:before { content: "\f1a3"; } .fi-ss-avocado:before { content: "\f1a4"; } .fi-ss-b:before { content: "\f1a5"; } .fi-ss-baby-carriage:before { content: "\f1a6"; } .fi-ss-baby:before { content: "\f1a7"; } .fi-ss-background:before { content: "\f1a8"; } .fi-ss-backpack:before { content: "\f1a9"; } .fi-ss-bacon:before { content: "\f1aa"; } .fi-ss-bacteria:before { content: "\f1ab"; } .fi-ss-bacterium:before { content: "\f1ac"; } .fi-ss-badge-check:before { content: "\f1ad"; } .fi-ss-badge-dollar:before { content: "\f1ae"; } .fi-ss-badge-percent:before { content: "\f1af"; } .fi-ss-badge-sheriff:before { content: "\f1b0"; } .fi-ss-badge:before { content: "\f1b1"; } .fi-ss-bags-shopping:before { content: "\f1b2"; } .fi-ss-bahai:before { content: "\f1b3"; } .fi-ss-baht-sign:before { content: "\f1b4"; } .fi-ss-balance-scale-left:before { content: "\f1b5"; } .fi-ss-balance-scale-right:before { content: "\f1b6"; } .fi-ss-ball-pile:before { content: "\f1b7"; } .fi-ss-balloons:before { content: "\f1b8"; } .fi-ss-ballot-check:before { content: "\f1b9"; } .fi-ss-ballot:before { content: "\f1ba"; } .fi-ss-ban-bug:before { content: "\f1bb"; } .fi-ss-ban:before { content: "\f1bc"; } .fi-ss-band-aid:before { content: "\f1bd"; } .fi-ss-bangladeshi-taka-sign:before { content: "\f1be"; } .fi-ss-banjo:before { content: "\f1bf"; } .fi-ss-bank:before { content: "\f1c0";
<note>Content truncated. Call the fetch tool with a start_index of 13500 to get more content.</note> python {"command":"python -c "import urllib.request; u='https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css'; s=urllib.request.urlopen(u, timeout=20).read().decode(); i=s.fin... .fi-ss-marker-time:before { content: "\f652"; } .fi-ss-marker:before { content: <shellId: 129 completed with exit code 0>
Added the Flaticon Uicons **solid-straight marker** beside “Current location” in the sidebar. I verified that `fi-ss-marker` exists in the linked stylesheet. Your existing [Sources.md](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/assets/Sources.md) already includes Flaticon attribution.
The icon loads from Flaticon’s CDN, so it requires an internet connection. [app.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/app.py) compiles successfully.
it works. i will send the others
Great—send the icon class and the label or UI element you want it next to, and I’ll add them in the same style.
friend : <i class="fi fi-ss-user"></i>
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 165, 195 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -165,31 +165,31 @@ [data-testid="stSidebar"] [data-testid="stMarkdownContainer"] h3 { margin: 1.15rem 0 0.35rem; padding-bottom: 0.35rem; border-bottom: 1px solid rgba(176, 132, 82, 0.22); color: #bda17e !important; font-size: 0.76rem; letter-spacing: 0.14em; text-transform: uppercase; } .location-heading .fi { margin-right: 0.45rem; color: #b98b5e; font-size: 0.9em; } [data-testid="stSidebar"] [data-testid="stMarkdownContainer"] p { color: #c9c0b3; line-height: 1.55; } .sidebar-brand { padding: 0.35rem 0 0.85rem; border-bottom: 1px solid rgba(176, 132, 82, 0.32); } .sidebar-brand .brand-name { margin-bottom: 0.25rem; color: #e7d4b8; font-size: 1.05rem; letter-spacing: 0.14em; } .sidebar-brand .muted { color: #9d9386; font-size: 0.64rem; { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 292, 322 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -292,31 +292,31 @@ "base": "#1d2025", "vignette": "rgba(0, 0, 0, 0.61)", } else: house_lighting = { "light": "rgba(136, 99, 60, 0.13)", "base": "#211e1b", "vignette": "rgba(0, 0, 0, 0.56)", }
st.markdown( f""" <style> .stApp {{ --house-background: radial-gradient(ellipse at 50% 12%, {house_lighting["light"]}, transparent 58%), radial-gradient(ellipse at center, transparent 38%, {house_lighting["vignette"]} 100%), {house_lighting["base"]}; }} </style> """, unsafe_allow_html=True, )
with st.sidebar: st.markdown( '<div class="sidebar-brand"><div class="brand-name">THE HOUSE</div>' '<div class="muted">YOUR PRIVATE RECORD</div></div>', unsafe_allow_html=True, ) { "pattern": "### Friend|### Current location", "paths": [ "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py" ], "output_mode": "content", "-n": true, "head_limit": 10 } C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:331: st.markdown("### Friend") *** Begin Patch *** Update File: the-house-that-remembers/app.py @@ -.location-heading .fi { +.sidebar-icon-heading .fi { margin-right: 0.45rem; color: #b98b5e; font-size: 0.9em; } @@ ... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -171,7 +171,7 @@ letter-spacing: 0.14em; text-transform: uppercase; } -.location-heading .fi { +.sidebar-icon-heading .fi { margin-right: 0.45rem; color: #b98b5e; font-size: 0.9em; @@ -328,7 +328,11 @@ ) st.write(game["room"])
- st.markdown("### Friend")
- st.markdown(
-
'<h3 class="sidebar-icon-heading"><i class="fi fi-ss-user" ' -
'aria-hidden="true"></i>Friend</h3>', -
unsafe_allow_html=True, - ) friend_room = game.get("friend_room", "Entrance Hall") st.write("Here with you" if friend_room == game["room"] else f"In the {friend_room}") { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py", "description": "Verify friend icon change" } <shellId: 131 completed with exit code 0> python {"command":"python -c "import urllib.request; s=urllib.request.urlopen('https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css', timeout=20).read().decode(); i=s.find('.f... .fi-ss-user:before { content: "\f995"; } .fi-ss-users-alt:before <shellId: 130 completed with exit code 0>
Added the solid user icon beside **Friend**, matching the marker icon’s muted copper styling. I verified the icon class exists in the Uicons stylesheet, and [app.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/app.py) compiles successfully.
fragments: <i class="fi fi-ss-puzzle-piece"></i>
*** Begin Patch *** Update File: the-house-that-remembers/app.py @@
- st.markdown("### Memory fragments")
- st.markdown(
-
'<h3 class="sidebar-icon-heading"><i class="fi fi-ss-puzzle-piec...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -336,7 +336,11 @@ friend_room = game.get("friend_room", "Entrance Hall") st.write("Here with you" if friend_room == game["room"] else f"In the {friend_room}")
- st.markdown("### Memory fragments")
- st.markdown(
-
'<h3 class="sidebar-icon-heading"><i class="fi fi-ss-puzzle-piece" ' -
'aria-hidden="true"></i>Memory fragments</h3>', -
unsafe_allow_html=True, - ) completed_fragments = len(fragments_complete(game)) st.write(f"{completed_fragments}/3 complete") fragment_status = fragment_progress(game) { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py", "description": "Verify fragments icon change" } <shellId: 133 completed with exit code 0> python {"command":"python -c "import urllib.request; s=urllib.request.urlopen('https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css', timeout=20).read().decode(); i=s.find('.f... .fi-ss-puzzle-piece:before { content: "\f765"; } .fi-ss-pyramid: <shellId: 132 completed with exit code 0>
Added the puzzle-piece icon beside **Memory fragments**, using the same copper styling. The class exists in the Uicons stylesheet, and [app.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/app.py) compiles successfully.
<i class="fi fi-ss-inventory-alt"></i>
*** Begin Patch *** Update File: the-house-that-remembers/app.py @@
- st.markdown("### Inventory")
- st.markdown(
-
'<h3 class="sidebar-icon-heading"><i class="fi fi-ss-inventory-alt" '
+... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -360,7 +360,11 @@ if memory["id"] in shared_memories: st.caption(f"✓ {memory['title']}")
- st.markdown("### Inventory")
- st.markdown(
-
'<h3 class="sidebar-icon-heading"><i class="fi fi-ss-inventory-alt" ' -
'aria-hidden="true"></i>Inventory</h3>', -
unsafe_allow_html=True, - ) if game["inventory"]: for item in game["inventory"]: if item == "The Letter": { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py", "description": "Verify inventory icon change" } <shellId: 135 completed with exit code 0> python {"command":"python -c "import urllib.request; s=urllib.request.urlopen('https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css', timeout=20).read().decode(); i=s.find('.f... Traceback (most recent call last): File "<string>", line 1, in <module> import urllib.request; s=urllib.request.urlopen('https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css', timeout=20).read().decode(); i=s.find('.fi-ss-inventory-alt:before'); print(s[i:i+80] if i >= 0 else 'fi-ss-inventory-alt not found'); assert i >= 0 ^^^^^^ fi-ss-inventory-alt not found AssertionError <shellId: 134 completed with exit code 1> python {"command":"python -c "import urllib.request,re; s=urllib.request.urlopen('https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css', timeout=20).read().decode(); print([x ... ['.fi-ss-box-alt', '.fi-ss-box-ballot', '.fi-ss-box-check', '.fi-ss-box-dollar', '.fi-ss-box-fragile', '.fi-ss-box-heart', '.fi-ss-box-open-full', '.fi-ss-box-open', '.fi-ss-box-tissue', '.fi-ss-box-up', '.fi-ss-box', '.fi-ss-boxes', '.fi-ss-boxing-glove', '.fi-ss-checkbox', '.fi-ss-hand-holding-box', '.fi-ss-inbox-in', '.fi-ss-inbox-out', '.fi-ss-inbox', '.fi-ss-inboxes', '.fi-ss-mailbox', '.fi-ss-parachute-box', '.fi-ss-people-carry-box', '.fi-ss-police-box', '.fi-ss-tool-box', '.fi-ss-warehouse-alt'] requested class: False <shellId: 136 completed with exit code 0>
The requested `fi-ss-inventory-alt` class isn’t present in the solid-straight stylesheet currently loaded, so it would render blank. I found a matching available alternative, `fi-ss-box-open`, and will use that for Inventory instead.
*** Begin Patch *** Update File: the-house-that-remembers/app.py @@
-
'<h3 class="sidebar-icon-heading"><i class="fi fi-ss-inventory-alt" '
-
'<h3 class="sidebar-icon-heading"><i class="...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -361,7 +361,7 @@ st.caption(f"✓ {memory['title']}")
st.markdown(
-
'<h3 class="sidebar-icon-heading"><i class="fi fi-ss-inventory-alt" '
-
) { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py", "description": "Verify inventory icon fallback" } <shellId: 137 completed with exit code 0> {"command":"Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py; git --no-pager status --short -- the-house-that-remembe... warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it M the-house-that-remembers/app.py <shellId: 138 completed with exit code 0>'<h3 class="sidebar-icon-heading"><i class="fi fi-ss-box-open" ' 'aria-hidden="true"></i>Inventory</h3>', unsafe_allow_html=True,
`fi-ss-inventory-alt` isn’t included in the loaded Uicons stylesheet, so it would appear blank. I used the available `fi-ss-box-open` icon for **Inventory** instead. [app.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/app.py) compiles successfully.
clues: <i class="fi fi-bs-search"></i>
{ "pattern": "uicons|fi-bs|fi-ss-search|Clues", "paths": [ "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py" ], "output_mode": "content", "-n": true, "head_limit": 30 } C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:27: "href='https://cdn-uicons.flaticon.com/uicons-solid-straight/css/" C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:28: "uicons-solid-straight.css'>", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:384: st.markdown("### Clues") python {"command":"python -c "import urllib.request; u='https://cdn-uicons.flaticon.com/uicons-bold-straight/css/uicons-bold-straight.css'; s=urllib.request.urlopen(u,timeout=20).read().decode(); i=s.find('... .fi-bs-search:before { content: "\f7d8"; } .fi-bs-seat-airline:before { <shellId: 139 completed with exit code 0> *** Begin Patch *** Update File: the-house-that-remembers/app.py @@ st.markdown( "<link rel='stylesheet' " "href='https://cdn-uicons.flaticon.com/uicons-solid-straight/css/" "uicons-so... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -28,6 +28,12 @@ "uicons-solid-straight.css'>", unsafe_allow_html=True, ) +st.markdown(
- "<link rel='stylesheet' "
- "href='https://cdn-uicons.flaticon.com/uicons-bold-straight/css/"
- "uicons-bold-straight.css'>",
- unsafe_allow_html=True, +)
MODEL_NAME = "gemma4:e4b" CANON_PATH = Path(file).resolve().parent / "story" / "canon.json" @@ -381,7 +387,11 @@ else: st.caption("Your pockets are empty.")
- st.markdown("### Clues")
- st.markdown(
-
'<h3 class="sidebar-icon-heading"><i class="fi fi-bs-search" ' -
'aria-hidden="true"></i>Clues</h3>', -
unsafe_allow_html=True, - ) if game["clues"]: for clue in game["clues"]: st.write(f"• {clue}") { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers'; python -m py_compile app.py", "description": "Verify clue icon integration" } <shellId: 141 completed with exit code 0> { "command": "Set-Location 'C:\Users\lucia\Documents\coding stuff\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py", "description": "Check clue icon formatting" } warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it <shellId: 140 completed with exit code 0>
Added the bold search icon beside **Clues** and loaded the matching Uicons bold-straight stylesheet. I verified the `fi-bs-search` class is available. [app.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/app.py) compiles successfully.
explored: <i class="fi fi-ss-lock-open-alt"></i>
``` python
{"command":"python -c \"import urllib.request; s=urllib.request.urlopen('https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css',timeout=20).read().decode(); i=s.find('.fi...
Traceback (most recent call last):
File "<string>", line 1, in <module>
import urllib.request; s=urllib.request.urlopen('https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css',timeout=20).read().decode(); i=s.find('.fi-ss-lock-open-alt:before'); print(s[i:i+100] if i>=0 else 'fi-ss-lock-open-alt not found'); assert i>=0
^^^^
AssertionError
fi-ss-lock-open-alt not found
<shellId: 142 completed with exit code 1>
python
{"command":"python -c \"import urllib.request,re; s=urllib.request.urlopen('https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css',timeout=20).read().decode(); print([x f...
[]
<shellId: 143 completed with exit code 0>
python
{"command":"python -c \"import urllib.request,re; s=urllib.request.urlopen('https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css',timeout=20).read().decode(); print([x f...
['.fi-ss-badge-check', '.fi-ss-ballot-check', '.fi-ss-binary-circle-check', '.fi-ss-box-check', '.fi-ss-calendar-check', '.fi-ss-check-circle', '.fi-ss-check-double', '.fi-ss-check', '.fi-ss-checkbox', '.fi-ss-circle-exclamation-check', '.fi-ss-clipboard-list-check', '.fi-ss-cloud-check', '.fi-ss-comment-alt-check', '.fi-ss-comment-check', '.fi-ss-comments-question-check', '.fi-ss-diagram-sankey', '.fi-ss-door-closed', '.fi-ss-door-open', '.fi-ss-field-hockey', '.fi-ss-flag-checkered', '.fi-ss-glass-whiskey-rocks', '.fi-ss-glass-whiskey', '.fi-ss-hexagon-check', '.fi-ss-hockey-mask', '.fi-ss-hockey-puck', '.fi-ss-hockey-sticks', '.fi-ss-key-skeleton-left-right', '.fi-ss-key', '.fi-ss-keyboard-brightness-low', '.fi-ss-keyboard-brightness', '.fi-ss-keyboard', '.fi-ss-keynote', '.fi-ss-list-check', '.fi-ss-map-marker-check', '.fi-ss-memo-circle-check', '.fi-ss-money-check-edit-alt', '.fi-ss-money-check-edit', '.fi-ss-money-check', '.fi-ss-piano-keyboard', '.fi-ss-shield-check', '.fi-ss-shopping-cart-check', '.fi-ss-text-check', '.fi-ss-time-check', '.fi-ss-trash-can-check', '.fi-ss-trash-check', '.fi-ss-turkey', '.fi-ss-unlock']
<shellId: 144 completed with exit code 0>
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
- st.markdown("### Explored")
+ st.markdown(
+ '<h3 class="sidebar-icon-heading"><i class="fi fi-ss-door-open" '
+ ...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -398,7 +398,11 @@
else:
st.caption("Nothing discovered yet.")
- st.markdown("### Explored")
+ st.markdown(
+ '<h3 class="sidebar-icon-heading"><i class="fi fi-ss-door-open" '
+ 'aria-hidden="true"></i>Explored</h3>',
+ unsafe_allow_html=True,
+ )
for visited_room in game["visited"]:
st.write(f"✓ {visited_room}")
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile app.py",
"description": "Verify explored icon integration"
}
<shellId: 145 completed with exit code 0>
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py",
"description": "Check explored icon formatting"
}
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
<shellId: 146 completed with exit code 0>
fi-ss-lock-open-alt isn’t in the solid-straight icon set currently loaded. I used the available fi-ss-door-open icon beside Explored instead—it fits the visited-rooms section. app.py compiles successfully.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
1,
38
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -1,38 +1,38 @@
import base64
import copy
import json
from io import BytesIO
from pathlib import Path
import ollama
import streamlit as st
from PIL import Image
from engine import (
FINAL_REVELATIONS,
FRAGMENT_LABELS,
FRAGMENT_KINDS,
FRAGMENT_CLUES_NEEDED,
ROOMS,
apply_action,
fragment_progress,
fragments_complete,
new_game,
)
from prompt import build_messages, generation_options, maybe_summarize, tidy
st.set_page_config(page_title="The House That Remembers", layout="wide")
st.markdown(
"<link rel='stylesheet' "
"href='https://cdn-uicons.flaticon.com/uicons-solid-straight/css/"
"uicons-solid-straight.css'>",
unsafe_allow_html=True,
)
st.markdown(
"<link rel='stylesheet' "
"href='https://cdn-uicons.flaticon.com/uicons-bold-straight/css/"
"uicons-bold-straight.css'>",
unsafe_allow_html=True,
)
MODEL_NAME = "gemma4:e4b"
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
435,
465
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -435,31 +435,31 @@
st.markdown(
f'<header class="page-header"{banner_style}>'
'<p class="eyebrow">A gothic horror adventure · Private journal</p>'
'<h1 class="title">The House That Remembers</h1>'
'<p class="subtitle">Some doors should stay closed.</p>'
'</header>',
unsafe_allow_html=True,
)
with st.expander("The letter and arrival", expanded=True):
st.markdown(INTRO)
scene_column, conversation_column = st.columns([1, 1.25], gap="large")
room = ROOMS[game["room"]]
with scene_column:
with st.container(height="content", border=True):
st.markdown(f"### {game['room']}")
if "img" in room:
show_image(
Path(__file__).resolve().parent / "assets" / room["img"],
width='stretch',
)
else:
st.caption("No image is available for this room yet.")
st.markdown(
f'<div class="story">{room["description"]}</div>',
unsafe_allow_html=True,
)
with conversation_column:
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -c \"import streamlit, inspect; print(streamlit.__version__); print(inspect.signature(...
1.66.1
(name: "Literal['user', 'assistant', 'ai', 'human'] | str", *, avatar: "Literal['user', 'assistant'] | str | AtomicImage | None" = None, width: 'Width' = 'stretch') -> 'DeltaGenerator'
<shellId: 147 completed with exit code 0>
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
460,
485
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -460,26 +460,26 @@
st.markdown(
f'<div class="story">{room["description"]}</div>',
unsafe_allow_html=True,
)
with conversation_column:
thinking_placeholder = st.empty()
st.markdown("### What will you do?")
with st.container(height=480, border=True, autoscroll=True):
for message in game["history"]:
with st.chat_message(
message["role"],
avatar="🕯️" if message["role"] == "assistant" else "🧑",
):
st.markdown(message["content"])
if game["ending"]:
st.markdown("---")
st.markdown("**THE END**")
if "ollama_error" in st.session_state:
st.error(st.session_state.ollama_error)
action = st.chat_input(
"Describe what you do...",
disabled=bool(game["ending"]),
{"pattern":"def chat_message|avatar.*emoji|avatar.*string","paths":["C:\\Users\\lucia\\AppData\\Local\\Programs\\Python\\Python313\\Lib\\site-packages\\streamlit"],"glob":"*.py","output_mode":"content...
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -c \"import streamlit, pathlib; print(pathlib.Path(streamlit.__file__).parent)\"","des...
C:\Users\lucia\AppData\Roaming\Python\Python314\site-packages\streamlit
<shellId: 148 completed with exit code 0>
{"pattern":"def chat_message|avatar.*emoji|avatar.*string","paths":["C:\\Users\\lucia\\AppData\\Roaming\\Python\\Python314\\site-packages\\streamlit"],"glob":"*.py","output_mode":"content","-n":true,"...
Output too large to read at once (69.6 KB). Saved to: C:\Users\lucia\AppData\Local\Temp\copilot-tool-output-1793224891839-11.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\AppData\Roaming\Python\Python314\site-packages\streamlit\elements\widgets\chat.py:249: if isinstance(avatar, str) and is_emoji(avatar):
C:\Users\lucia\AppData\Roaming\Python\Python314\site-packages\streamlit\elements\widgets\chat.py:420: def chat_message(
C:\Users\lucia\AppData\Roaming\Python\Python314\site-packages\streamlit\material_icon_names.py:25:ALL_MATERIAL_ICONS = {"10k", "10mp", "11mp", "123", "12mp", "13mp", "14mp", "15mp", "16mp", "17mp", "18_up_rating", "18mp", "
{
"path": "C:\\Users\\lucia\\AppData\\Roaming\\Python\\Python314\\site-packages\\streamlit\\elements\\widgets\\chat.py",
"view_range": [
236,
270
]
}
diff --git a/C:/Users/lucia/AppData/Roaming/Python/Python314/site-packages/streamlit/elements/widgets/chat.py b/C:/Users/lucia/AppData/Roaming/Python/Python314/site-packages/streamlit/elements/widgets/chat.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/AppData/Roaming/Python/Python314/site-packages/streamlit/elements/widgets/chat.py
+++ b/C:/Users/lucia/AppData/Roaming/Python/Python314/site-packages/streamlit/elements/widgets/chat.py
@@ -236,35 +236,35 @@
if avatar is None:
return AvatarType.ICON, ""
if isinstance(avatar, str) and avatar in {item.value for item in PresetNames}:
return (
AvatarType.ICON,
(
"assistant"
if avatar in {PresetNames.AI, PresetNames.ASSISTANT}
else "user"
),
)
if isinstance(avatar, str) and is_emoji(avatar):
return AvatarType.EMOJI, avatar
if isinstance(avatar, str) and avatar.startswith(":material"):
return AvatarType.ICON, validate_material_icon(avatar)
try:
return AvatarType.IMAGE, image_to_url(
avatar,
layout_config=LayoutConfig(width="content"),
clamp=False,
channels="RGB",
output_format="auto",
image_id=delta_path,
)
except Exception as ex:
raise StreamlitAPIException(
"Failed to load the provided avatar value as an image.",
error_id="chat-failed--avatar-image",
) from ex
def _pop_upload_files(
python
{"command":"python -c \"import urllib.request; s=urllib.request.urlopen('https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css',timeout=20).read().decode(); import re; na...
quill-pen-story: False
user: True
<shellId: 149 completed with exit code 0>
python
{"command":"python -c \"import urllib.request,re; s=urllib.request.urlopen('https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css',timeout=20).read().decode(); names=re.f...
['.fi-ss-attribution-pen', '.fi-ss-attribution-pencil', '.fi-ss-blinds-open', '.fi-ss-blog-pencil', '.fi-ss-book-open-cover', '.fi-ss-book-open-reader', '.fi-ss-box-open-full', '.fi-ss-box-open', '.fi-ss-calendar-lines-pen', '.fi-ss-calendar-pen', '.fi-ss-call-history', '.fi-ss-circle-book-open', '.fi-ss-comment-pen', '.fi-ss-door-open', '.fi-ss-envelope-open-dollar', '.fi-ss-envelope-open-text', '.fi-ss-envelope-open', '.fi-ss-face-pensive', '.fi-ss-feather-pointed', '.fi-ss-feather', '.fi-ss-folder-open', '.fi-ss-garage-open', '.fi-ss-nfc-pen', '.fi-ss-paintbrush-pencil', '.fi-ss-pen-circle', '.fi-ss-pen-clip-slash', '.fi-ss-pen-clip', '.fi-ss-pen-fancy-slash', '.fi-ss-pen-fancy', '.fi-ss-pen-field', '.fi-ss-pen-nib-slash', '.fi-ss-pen-nib', '.fi-ss-pen-slash', '.fi-ss-pen-square', '.fi-ss-pen-swirl', '.fi-ss-pencil-paintbrush', '.fi-ss-pencil-ruler', '.fi-ss-pencil-slash', '.fi-ss-pencil', '.fi-ss-pennant', '.fi-ss-rectangle-vertical-history', '.fi-ss-teeth-open', '.fi-ss-typewriter', '.fi-ss-usb-pendrive', '.fi-ss-user-pen']
<shellId: 150 completed with exit code 0>
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
div[data-testid="stChatMessage"] {
background: #222124;
border: 1px solid #34312e;
border-radius: 12px;
}
+[data-te...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -254,6 +254,14 @@
border: 1px solid #34312e;
border-radius: 12px;
}
+[data-testid="stChatMessageAvatar"] {
+ display: none;
+}
+.chat-role-icon {
+ margin-bottom: 0.3rem;
+ color: #b98b5e;
+ font-size: 1rem;
+}
.stApp [data-testid="stMarkdownContainer"] {
font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif;
}
@@ -469,8 +477,18 @@
for message in game["history"]:
with st.chat_message(
message["role"],
- avatar="🕯️" if message["role"] == "assistant" else "🧑",
+ avatar=None,
):
+ icon = (
+ "fi-ss-feather-pointed"
+ if message["role"] == "assistant"
+ else "fi-ss-user"
+ )
+ st.markdown(
+ f'<div class="chat-role-icon"><i class="fi {icon}" '
+ 'aria-hidden="true"></i></div>',
+ unsafe_allow_html=True,
+ )
st.markdown(message["content"])
if game["ending"]:
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile app.py",
"description": "Verify custom chat icon code compiles"
}
<shellId: 151 completed with exit code 0>
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py; git --no-pager diff --stat -- the-house-that-remembers/...
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
the-house-that-remembers/app.py | 276 +++++++++++++++++++++++++++++++++++-----
1 file changed, 242 insertions(+), 34 deletions(-)
<shellId: 153 completed with exit code 0>
python
{"command":"python -c \"import urllib.request; s=urllib.request.urlopen('https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css',timeout=20).read().decode(); assert '.fi-s...
Feather and user icon classes are available
<shellId: 152 completed with exit code 0>
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
-[data-testid="stChatMessageAvatar"] {
- display: none;
-}
-.chat-role-icon {
- margin-bottom: 0.3rem;
- color: #b98b5e;
- ...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -254,14 +254,6 @@
border: 1px solid #34312e;
border-radius: 12px;
}
-[data-testid="stChatMessageAvatar"] {
- display: none;
-}
-.chat-role-icon {
- margin-bottom: 0.3rem;
- color: #b98b5e;
- font-size: 1rem;
-}
.stApp [data-testid="stMarkdownContainer"] {
font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif;
}
@@ -477,18 +469,8 @@
for message in game["history"]:
with st.chat_message(
message["role"],
- avatar=None,
+ avatar="🕯️" if message["role"] == "assistant" else "🧑",
):
- icon = (
- "fi-ss-feather-pointed"
- if message["role"] == "assistant"
- else "fi-ss-user"
- )
- st.markdown(
- f'<div class="chat-role-icon"><i class="fi {icon}" '
- 'aria-hidden="true"></i></div>',
- unsafe_allow_html=True,
- )
st.markdown(message["content"])
if game["ending"]:
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile app.py",
"description": "Verify emoji avatars restored"
}
<shellId: 154 completed with exit code 0>
{"pattern":"chat-role-icon|stChatMessageAvatar|avatar=|fi-ss-feather","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py"],"output_mode":"content","-n":tr...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:472: avatar="🕯️" if message["role"] == "assistant" else "🧑",
python
{"command":"python -c \"import urllib.request,re; s=urllib.request.urlopen('https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css',timeout=20).read().decode(); names=re.f...
['.fi-ss-bags-shopping', '.fi-ss-basket-shopping-simple', '.fi-ss-bowling-pins', '.fi-ss-camping', '.fi-ss-cart-shopping-fast', '.fi-ss-envelope-marker', '.fi-ss-face-sleeping', '.fi-ss-hands-clapping', '.fi-ss-home-location-alt', '.fi-ss-home-location', '.fi-ss-location-alt', '.fi-ss-location-crosshairs-slash', '.fi-ss-location-crosshairs', '.fi-ss-location-dot-slash', '.fi-ss-location-exclamation', '.fi-ss-map-marker-check', '.fi-ss-map-marker-cross', '.fi-ss-map-marker-edit', '.fi-ss-map-marker-home', '.fi-ss-map-marker-minus', '.fi-ss-map-marker-plus', '.fi-ss-map-marker-question', '.fi-ss-map-marker-slash', '.fi-ss-map-marker-smile', '.fi-ss-map-marker', '.fi-ss-map-pin', '.fi-ss-marker-time', '.fi-ss-marker', '.fi-ss-pineapple', '.fi-ss-search-location', '.fi-ss-shopping-bag-add', '.fi-ss-shopping-bag', '.fi-ss-shopping-basket', '.fi-ss-shopping-cart-add', '.fi-ss-shopping-cart-check', '.fi-ss-shopping-cart', '.fi-ss-spinner']
<shellId: 155 completed with exit code 0>
{"pattern":"fi-ss-marker|fi-ss-user|fi-ss-puzzle-piece|fi-ss-box-open|fi-bs-search|fi-ss-door-open","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py"],"...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:331: '<h3 class="location-heading"><i class="fi fi-ss-marker" '
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:338: '<h3 class="sidebar-icon-heading"><i class="fi fi-ss-user" '
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:346: '<h3 class="sidebar-icon-heading"><i class="fi fi-ss-puzzle-piece" '
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:370: '<h3 class="sidebar-icon-heading"><i class="fi fi-ss-box-open" '
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:391: '<h3 class="sidebar-icon-heading"><i class="fi fi-bs-search" '
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:402: '<h3 class="sidebar-icon-heading"><i class="fi fi-ss-door-open" '
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
- '<h3 class="location-heading"><i class="fi fi-ss-marker" '
+ '<h3 class="sidebar-icon-heading"><i class="fi fi-ss-ma...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -328,7 +328,7 @@
)
st.markdown(
- '<h3 class="location-heading"><i class="fi fi-ss-marker" '
+ '<h3 class="sidebar-icon-heading"><i class="fi fi-ss-marker" '
'aria-hidden="true"></i>Current location</h3>',
unsafe_allow_html=True,
)
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile app.py",
"description": "Verify unified sidebar icon style"
}
<shellId: 156 completed with exit code 0>
{
"pattern": "location-heading|sidebar-icon-heading",
"paths": [
"C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py"
],
"output_mode": "content",
"-n": true,
"head_limit": 20
}
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:180:.sidebar-icon-heading .fi {
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:331: '<h3 class="sidebar-icon-heading"><i class="fi fi-ss-marker" '
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:338: '<h3 class="sidebar-icon-heading"><i class="fi fi-ss-user" '
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:346: '<h3 class="sidebar-icon-heading"><i class="fi fi-ss-puzzle-piece" '
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:370: '<h3 class="sidebar-icon-heading"><i class="fi fi-ss-box-open" '
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:391: '<h3 class="sidebar-icon-heading"><i class="fi fi-bs-search" '
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:402: '<h3 class="sidebar-icon-heading"><i class="fi fi-ss-door-open" '
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
140,
235
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -140,96 +140,96 @@
src="data:audio/mpeg;base64,{audio_base64}"
type="audio/mpeg"
>
</audio>
""",
unsafe_allow_html=True,
)
st.markdown("""
<style>
body, .stApp, .stApp input, .stApp textarea, .stApp button {
font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important;
}
.stApp {
background: var(--house-background, #171719);
color: #e8ddca;
transition: background 1.2s ease;
}
[data-testid="stSidebar"] {
background: linear-gradient(180deg, #17191a 0%, #1c1d1d 55%, #171819 100%);
border-right: 1px solid #39332d;
}
[data-testid="stSidebar"] [data-testid="stMarkdownContainer"] h2 {
margin: 0 0 0.15rem;
color: #e7d4b8 !important;
font-size: 1.18rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
[data-testid="stSidebar"] [data-testid="stMarkdownContainer"] h3 {
margin: 1.15rem 0 0.35rem;
padding-bottom: 0.35rem;
border-bottom: 1px solid rgba(176, 132, 82, 0.22);
color: #bda17e !important;
font-size: 0.76rem;
letter-spacing: 0.14em;
text-transform: uppercase;
}
.sidebar-icon-heading .fi {
margin-right: 0.45rem;
color: #b98b5e;
font-size: 0.9em;
}
[data-testid="stSidebar"] [data-testid="stMarkdownContainer"] p {
color: #c9c0b3;
line-height: 1.55;
}
.sidebar-brand {
padding: 0.35rem 0 0.85rem;
border-bottom: 1px solid rgba(176, 132, 82, 0.32);
}
.sidebar-brand .brand-name {
margin-bottom: 0.25rem;
color: #e7d4b8;
font-size: 1.05rem;
letter-spacing: 0.14em;
}
.sidebar-brand .muted {
color: #9d9386;
font-size: 0.64rem;
letter-spacing: 0.16em;
}
h1, h2, h3 {
color: #e8ddca !important;
font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important;
}
.page-header {
min-height: 230px;
margin: 0.25rem 0 1.2rem;
padding: 1.5rem 1.75rem;
display: flex;
flex-direction: column;
justify-content: flex-end;
border-bottom: 1px solid rgba(176, 132, 82, 0.32);
background-color: #17191a;
background-position: center 43%;
background-size: cover;
}
.page-header .eyebrow {
margin: 0 0 0.45rem;
color: #bb8b58;
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
}
.page-header .title {
margin: 0;
color: #eee1cd;
font-size: clamp(1.8rem, 3vw, 2.45rem);
line-height: 1.1;
}
.page-header .subtitle {
max-width: 42rem;
margin: 0.55rem 0 0;
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
440,
490
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -440,51 +440,51 @@
'</header>',
unsafe_allow_html=True,
)
with st.expander("The letter and arrival", expanded=True):
st.markdown(INTRO)
scene_column, conversation_column = st.columns([1, 1.25], gap="large")
room = ROOMS[game["room"]]
with scene_column:
with st.container(height="content", border=True):
st.markdown(f"### {game['room']}")
if "img" in room:
show_image(
Path(__file__).resolve().parent / "assets" / room["img"],
width='stretch',
)
else:
st.caption("No image is available for this room yet.")
st.markdown(
f'<div class="story">{room["description"]}</div>',
unsafe_allow_html=True,
)
with conversation_column:
thinking_placeholder = st.empty()
st.markdown("### What will you do?")
with st.container(height=480, border=True, autoscroll=True):
for message in game["history"]:
with st.chat_message(
message["role"],
avatar="🕯️" if message["role"] == "assistant" else "🧑",
):
st.markdown(message["content"])
if game["ending"]:
st.markdown("---")
st.markdown("**THE END**")
if "ollama_error" in st.session_state:
st.error(st.session_state.ollama_error)
action = st.chat_input(
"Describe what you do...",
disabled=bool(game["ending"]),
)
if action:
snapshot = copy.deepcopy(game) # restore this if the model call fails
game["history"].append({"role": "user", "content": action})
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager status --short -- the-house-that-remembers/app.py",
"description": "Confirm target UI file status"
}
M the-house-that-remembers/app.py
<shellId: 157 completed with exit code 0>
python
{
"command": "python -c \"import streamlit as st, inspect; print(inspect.signature(st.container))\"",
"description": "Check Streamlit container styling hooks"
}
(*, border: 'bool | None' = None, key: 'Key | None' = None, width: 'Width' = 'stretch', height: 'Height' = 'content', horizontal: 'bool' = False, wrap: 'bool' = True, horizontal_alignment: 'HorizontalAlignment' = 'left', vertical_alignment: 'VerticalAlignment' = 'top', gap: 'Gap | None' = 'small', autoscroll: 'bool | None' = None) -> 'DeltaGenerator'
<shellId: 158 completed with exit code 0>
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
.page-header .subtitle {
max-width: 42rem;
margin: 0.55rem 0 0;
color: #b9afa1;
font-size: 1rem;
font-styl...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -238,6 +238,31 @@
font-style: italic;
line-height: 1.55;
}
+.st-key-scene_panel,
+.st-key-conversation_panel {
+ position: relative;
+ outline: 1px solid rgba(176, 132, 82, 0.28);
+ outline-offset: -1px;
+ border-radius: 4px;
+}
+.st-key-scene_panel::before,
+.st-key-conversation_panel::before {
+ content: "";
+ position: absolute;
+ inset: 5px;
+ z-index: 2;
+ pointer-events: none;
+ opacity: 0.72;
+ background:
+ linear-gradient(#bd9567, #bd9567) left top / 18px 1px no-repeat,
+ linear-gradient(#bd9567, #bd9567) left top / 1px 18px no-repeat,
+ linear-gradient(#bd9567, #bd9567) right top / 18px 1px no-repeat,
+ linear-gradient(#bd9567, #bd9567) right top / 1px 18px no-repeat,
+ linear-gradient(#bd9567, #bd9567) left bottom / 18px 1px no-repeat,
+ linear-gradient(#bd9567, #bd9567) left bottom / 1px 18px no-repeat,
+ linear-gradient(#bd9567, #bd9567) right bottom / 18px 1px no-repeat,
+ linear-gradient(#bd9567, #bd9567) right bottom / 1px 18px no-repeat;
+}
.story {
font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif;
font-size: 1.08rem;
@@ -448,7 +473,7 @@
room = ROOMS[game["room"]]
with scene_column:
- with st.container(height="content", border=True):
+ with st.container(height="content", border=True, key="scene_panel"):
st.markdown(f"### {game['room']}")
if "img" in room:
show_image(
@@ -465,7 +490,12 @@
with conversation_column:
thinking_placeholder = st.empty()
st.markdown("### What will you do?")
- with st.container(height=480, border=True, autoscroll=True):
+ with st.container(
+ height=480,
+ border=True,
+ autoscroll=True,
+ key="conversation_panel",
+ ):
for message in game["history"]:
with st.chat_message(
message["role"],
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile app.py; python -m unittest discover -s tests -p 'test_engine.py'","descr...
........................
----------------------------------------------------------------------
Ran 24 tests in 0.009s
OK
<shellId: 160 completed with exit code 0>
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py; git --no-pager diff --stat -- the-house-that-remembers/...
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
the-house-that-remembers/app.py | 290 +++++++++++++++++++++++++++++++++++-----
1 file changed, 255 insertions(+), 35 deletions(-)
<shellId: 159 completed with exit code 0>
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
190,
240
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -190,51 +190,51 @@
padding: 0.35rem 0 0.85rem;
border-bottom: 1px solid rgba(176, 132, 82, 0.32);
}
.sidebar-brand .brand-name {
margin-bottom: 0.25rem;
color: #e7d4b8;
font-size: 1.05rem;
letter-spacing: 0.14em;
}
.sidebar-brand .muted {
color: #9d9386;
font-size: 0.64rem;
letter-spacing: 0.16em;
}
h1, h2, h3 {
color: #e8ddca !important;
font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important;
}
.page-header {
min-height: 230px;
margin: 0.25rem 0 1.2rem;
padding: 1.5rem 1.75rem;
display: flex;
flex-direction: column;
justify-content: flex-end;
border-bottom: 1px solid rgba(176, 132, 82, 0.32);
background-color: #17191a;
background-position: center 43%;
background-size: cover;
}
.page-header .eyebrow {
margin: 0 0 0.45rem;
color: #bb8b58;
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
}
.page-header .title {
margin: 0;
color: #eee1cd;
font-size: clamp(1.8rem, 3vw, 2.45rem);
line-height: 1.1;
}
.page-header .subtitle {
max-width: 42rem;
margin: 0.55rem 0 0;
color: #b9afa1;
font-size: 1rem;
font-style: italic;
line-height: 1.55;
}
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
.page-header {
+ width: calc(100% + 10rem);
min-height: 230px;
- margin: 0.25rem 0 1.2rem;
- padding: 1.5rem 1.75rem;...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -206,9 +206,10 @@
font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important;
}
.page-header {
+ width: calc(100% + 10rem);
min-height: 230px;
- margin: 0.25rem 0 1.2rem;
- padding: 1.5rem 1.75rem;
+ margin: 0.25rem 0 1.2rem -5rem;
+ padding: 1.5rem calc(5rem + 1.75rem);
display: flex;
flex-direction: column;
justify-content: flex-end;
@@ -217,6 +218,13 @@
background-position: center 43%;
background-size: cover;
}
+@media (max-width: 768px) {
+ .page-header {
+ width: 100%;
+ margin-left: 0;
+ padding: 1.25rem;
+ }
+}
.page-header .eyebrow {
margin: 0 0 0.45rem;
color: #bb8b58;
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile app.py",
"description": "Verify wider banner stylesheet"
}
<shellId: 162 completed with exit code 0>
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py",
"description": "Check banner width diff"
}
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
<shellId: 161 completed with exit code 0>
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
210,
265
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -210,56 +210,56 @@
min-height: 230px;
margin: 0.25rem 0 1.2rem -5rem;
padding: 1.5rem calc(5rem + 1.75rem);
display: flex;
flex-direction: column;
justify-content: flex-end;
border-bottom: 1px solid rgba(176, 132, 82, 0.32);
background-color: #17191a;
background-position: center 43%;
background-size: cover;
}
@media (max-width: 768px) {
.page-header {
width: 100%;
margin-left: 0;
padding: 1.25rem;
}
}
.page-header .eyebrow {
margin: 0 0 0.45rem;
color: #bb8b58;
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
}
.page-header .title {
margin: 0;
color: #eee1cd;
font-size: clamp(1.8rem, 3vw, 2.45rem);
line-height: 1.1;
}
.page-header .subtitle {
max-width: 42rem;
margin: 0.55rem 0 0;
color: #b9afa1;
font-size: 1rem;
font-style: italic;
line-height: 1.55;
}
.st-key-scene_panel,
.st-key-conversation_panel {
position: relative;
outline: 1px solid rgba(176, 132, 82, 0.28);
outline-offset: -1px;
border-radius: 4px;
}
.st-key-scene_panel::before,
.st-key-conversation_panel::before {
content: "";
position: absolute;
inset: 5px;
z-index: 2;
pointer-events: none;
opacity: 0.72;
background:
linear-gradient(#bd9567, #bd9567) left top / 18px 1px no-repeat,
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
.stApp {
background: var(--house-background, #171719);
color: #e8ddca;
transition: background 1.2s ease;
}
+[data-t...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -157,6 +157,9 @@
color: #e8ddca;
transition: background 1.2s ease;
}
+[data-testid="stMainBlockContainer"] {
+ padding-top: 1rem;
+}
[data-testid="stSidebar"] {
background: linear-gradient(180deg, #17191a 0%, #1c1d1d 55%, #171819 100%);
border-right: 1px solid #39332d;
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile app.py",
"description": "Verify top spacing adjustment"
}
<shellId: 164 completed with exit code 0>
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py",
"description": "Check spacing diff formatting"
}
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
<shellId: 163 completed with exit code 0>
python
{"command":"python -c \"import urllib.request; s=urllib.request.urlopen('https://cdn-uicons.flaticon.com/uicons-solid-straight/css/uicons-solid-straight.css',timeout=20).read().decode(); i=s.find('.fi...
.fi-ss-house-chimney:before {
content: "\f5b5";
}
.fi-ss-house-c
<shellId: 165 completed with exit code 0>
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
st.markdown(
- '<div class="sidebar-brand"><div class="brand-name">THE HOUSE</div>'
+ '<div class="sidebar-brand"...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -358,7 +358,8 @@
with st.sidebar:
st.markdown(
- '<div class="sidebar-brand"><div class="brand-name">THE HOUSE</div>'
+ '<div class="sidebar-brand"><div class="brand-name">'
+ '<i class="fi fi-ss-house-chimney" aria-hidden="true"></i> THE HOUSE</div>'
'<div class="muted">YOUR PRIVATE RECORD</div></div>',
unsafe_allow_html=True,
)
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile app.py",
"description": "Verify sidebar house icon code"
}
<shellId: 167 completed with exit code 0>
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py",
"description": "Check house icon diff formatting"
}
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
<shellId: 166 completed with exit code 0>
make background darker
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
270,
328
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -270,59 +270,59 @@
linear-gradient(#bd9567, #bd9567) right top / 18px 1px no-repeat,
linear-gradient(#bd9567, #bd9567) right top / 1px 18px no-repeat,
linear-gradient(#bd9567, #bd9567) left bottom / 18px 1px no-repeat,
linear-gradient(#bd9567, #bd9567) left bottom / 1px 18px no-repeat,
linear-gradient(#bd9567, #bd9567) right bottom / 18px 1px no-repeat,
linear-gradient(#bd9567, #bd9567) right bottom / 1px 18px no-repeat;
}
.story {
font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif;
font-size: 1.08rem;
line-height: 1.9;
color: #d8d1c7;
}
.muted {
color: #a7a19a;
font-size: 0.85rem;
letter-spacing: 0.12em;
}
div[data-testid="stChatMessage"] {
background: #222124;
border: 1px solid #34312e;
border-radius: 12px;
}
.stApp [data-testid="stMarkdownContainer"] {
font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif;
}
.stChatInput textarea {
color: #e8ddca !important;
}
</style>
""", unsafe_allow_html=True)
if "game" not in st.session_state:
st.session_state.game = new_game()
game = st.session_state.game
if game.get("truth_known"):
house_lighting = {
"light": "rgba(133, 119, 96, 0.13)",
"base": "#211f1d",
"vignette": "rgba(0, 0, 0, 0.48)",
}
elif game["stage"] >= 5:
house_lighting = {
"light": "rgba(103, 105, 121, 0.07)",
"base": "#191a1e",
"vignette": "rgba(0, 0, 0, 0.76)",
}
elif game["stage"] >= 4:
house_lighting = {
"light": "rgba(89, 98, 119, 0.09)",
"base": "#1a1c21",
"vignette": "rgba(0, 0, 0, 0.72)",
}
elif game["stage"] >= 3:
house_lighting = {
"light": "rgba(105, 79, 111, 0.10)",
"base": "#1e1b22",
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
328,
356
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -328,29 +328,29 @@
"base": "#1e1b22",
"vignette": "rgba(0, 0, 0, 0.66)",
}
elif game["stage"] >= 2:
house_lighting = {
"light": "rgba(98, 111, 132, 0.11)",
"base": "#1d2025",
"vignette": "rgba(0, 0, 0, 0.61)",
}
else:
house_lighting = {
"light": "rgba(136, 99, 60, 0.13)",
"base": "#211e1b",
"vignette": "rgba(0, 0, 0, 0.56)",
}
st.markdown(
f"""
<style>
.stApp {{
--house-background:
radial-gradient(ellipse at 50% 12%, {house_lighting["light"]}, transparent 58%),
radial-gradient(ellipse at center, transparent 38%, {house_lighting["vignette"]} 100%),
{house_lighting["base"]};
}}
</style>
""",
unsafe_allow_html=True,
)
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
if game.get("truth_known"):
house_lighting = {
- "light": "rgba(133, 119, 96, 0.13)",
- "base": "#211f1d",
- ...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -306,39 +306,39 @@
if game.get("truth_known"):
house_lighting = {
- "light": "rgba(133, 119, 96, 0.13)",
- "base": "#211f1d",
- "vignette": "rgba(0, 0, 0, 0.48)",
+ "light": "rgba(133, 119, 96, 0.08)",
+ "base": "#171615",
+ "vignette": "rgba(0, 0, 0, 0.56)",
}
elif game["stage"] >= 5:
house_lighting = {
- "light": "rgba(103, 105, 121, 0.07)",
- "base": "#191a1e",
- "vignette": "rgba(0, 0, 0, 0.76)",
+ "light": "rgba(103, 105, 121, 0.05)",
+ "base": "#121316",
+ "vignette": "rgba(0, 0, 0, 0.80)",
}
elif game["stage"] >= 4:
house_lighting = {
- "light": "rgba(89, 98, 119, 0.09)",
- "base": "#1a1c21",
- "vignette": "rgba(0, 0, 0, 0.72)",
+ "light": "rgba(89, 98, 119, 0.06)",
+ "base": "#131519",
+ "vignette": "rgba(0, 0, 0, 0.76)",
}
elif game["stage"] >= 3:
house_lighting = {
- "light": "rgba(105, 79, 111, 0.10)",
- "base": "#1e1b22",
- "vignette": "rgba(0, 0, 0, 0.66)",
+ "light": "rgba(105, 79, 111, 0.07)",
+ "base": "#171419",
+ "vignette": "rgba(0, 0, 0, 0.70)",
}
elif game["stage"] >= 2:
house_lighting = {
- "light": "rgba(98, 111, 132, 0.11)",
- "base": "#1d2025",
- "vignette": "rgba(0, 0, 0, 0.61)",
+ "light": "rgba(98, 111, 132, 0.07)",
+ "base": "#14171b",
+ "vignette": "rgba(0, 0, 0, 0.66)",
}
else:
house_lighting = {
- "light": "rgba(136, 99, 60, 0.13)",
- "base": "#211e1b",
- "vignette": "rgba(0, 0, 0, 0.56)",
+ "light": "rgba(136, 99, 60, 0.07)",
+ "base": "#151413",
+ "vignette": "rgba(0, 0, 0, 0.62)",
}
st.markdown(
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile app.py",
"description": "Verify darker background stylesheet"
}
<shellId: 168 completed with exit code 0>
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py",
"description": "Check background color diff"
}
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
<shellId: 169 completed with exit code 0>
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
if game.get("truth_known"):
house_lighting = {
- "light": "rgba(133, 119, 96, 0.08)",
- "base": "#171615",
- ...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -306,39 +306,39 @@
if game.get("truth_known"):
house_lighting = {
- "light": "rgba(133, 119, 96, 0.08)",
- "base": "#171615",
- "vignette": "rgba(0, 0, 0, 0.56)",
+ "light": "rgba(119, 137, 143, 0.05)",
+ "base": "#0d1112",
+ "vignette": "rgba(0, 0, 0, 0.62)",
}
elif game["stage"] >= 5:
house_lighting = {
- "light": "rgba(103, 105, 121, 0.05)",
- "base": "#121316",
- "vignette": "rgba(0, 0, 0, 0.80)",
+ "light": "rgba(92, 113, 127, 0.04)",
+ "base": "#090d11",
+ "vignette": "rgba(0, 0, 0, 0.84)",
}
elif game["stage"] >= 4:
house_lighting = {
- "light": "rgba(89, 98, 119, 0.06)",
- "base": "#131519",
- "vignette": "rgba(0, 0, 0, 0.76)",
+ "light": "rgba(85, 109, 126, 0.045)",
+ "base": "#0a0f13",
+ "vignette": "rgba(0, 0, 0, 0.80)",
}
elif game["stage"] >= 3:
house_lighting = {
- "light": "rgba(105, 79, 111, 0.07)",
- "base": "#171419",
- "vignette": "rgba(0, 0, 0, 0.70)",
+ "light": "rgba(93, 92, 119, 0.045)",
+ "base": "#0d0e13",
+ "vignette": "rgba(0, 0, 0, 0.76)",
}
elif game["stage"] >= 2:
house_lighting = {
- "light": "rgba(98, 111, 132, 0.07)",
- "base": "#14171b",
- "vignette": "rgba(0, 0, 0, 0.66)",
+ "light": "rgba(92, 119, 137, 0.05)",
+ "base": "#0b1014",
+ "vignette": "rgba(0, 0, 0, 0.72)",
}
else:
house_lighting = {
- "light": "rgba(136, 99, 60, 0.07)",
- "base": "#151413",
- "vignette": "rgba(0, 0, 0, 0.62)",
+ "light": "rgba(107, 126, 137, 0.055)",
+ "base": "#0c1114",
+ "vignette": "rgba(0, 0, 0, 0.68)",
}
st.markdown(
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile app.py",
"description": "Verify cooler background styling"
}
<shellId: 170 completed with exit code 0>
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py",
"description": "Check darker background formatting"
}
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
<shellId: 171 completed with exit code 0>
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
210,
260
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -210,51 +210,51 @@
}
.page-header {
width: calc(100% + 10rem);
min-height: 230px;
margin: 0.25rem 0 1.2rem -5rem;
padding: 1.5rem calc(5rem + 1.75rem);
display: flex;
flex-direction: column;
justify-content: flex-end;
border-bottom: 1px solid rgba(176, 132, 82, 0.32);
background-color: #17191a;
background-position: center 43%;
background-size: cover;
}
@media (max-width: 768px) {
.page-header {
width: 100%;
margin-left: 0;
padding: 1.25rem;
}
}
.page-header .eyebrow {
margin: 0 0 0.45rem;
color: #bb8b58;
font-size: 0.72rem;
letter-spacing: 0.2em;
text-transform: uppercase;
}
.page-header .title {
margin: 0;
color: #eee1cd;
font-size: clamp(1.8rem, 3vw, 2.45rem);
line-height: 1.1;
}
.page-header .subtitle {
max-width: 42rem;
margin: 0.55rem 0 0;
color: #b9afa1;
font-size: 1rem;
font-style: italic;
line-height: 1.55;
}
.st-key-scene_panel,
.st-key-conversation_panel {
position: relative;
outline: 1px solid rgba(176, 132, 82, 0.28);
outline-offset: -1px;
border-radius: 4px;
}
.st-key-scene_panel::before,
.st-key-conversation_panel::before {
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
425,
455
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -425,31 +425,31 @@
st.caption("Your pockets are empty.")
st.markdown(
'<h3 class="sidebar-icon-heading"><i class="fi fi-bs-search" '
'aria-hidden="true"></i>Clues</h3>',
unsafe_allow_html=True,
)
if game["clues"]:
for clue in game["clues"]:
st.write(f"• {clue}")
else:
st.caption("Nothing discovered yet.")
st.markdown(
'<h3 class="sidebar-icon-heading"><i class="fi fi-ss-door-open" '
'aria-hidden="true"></i>Explored</h3>',
unsafe_allow_html=True,
)
for visited_room in game["visited"]:
st.write(f"✓ {visited_room}")
st.divider()
if st.button("Restart adventure", use_container_width=True):
st.session_state.pop("game", None)
st.session_state.pop("ollama_error", None)
st.rerun()
st.subheader("Atmosphere")
sound_enabled = st.checkbox("Background rain", value=True)
if sound_enabled:
{"pattern":"page-header|The letter and arrival|expander","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py"],"output_mode":"content","-n":true,"head_limi...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:211:.page-header {
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:225: .page-header {
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:231:.page-header .eyebrow {
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:238:.page-header .title {
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:244:.page-header .subtitle {
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:473: f'<header class="page-header"{banner_style}>'
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:481:with st.expander("The letter and arrival", expanded=True):
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
- margin: 0.25rem 0 1.2rem -5rem;
+ margin: 0.25rem 0 0 -5rem;
*** End Patch
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -211,7 +211,7 @@
.page-header {
width: calc(100% + 10rem);
min-height: 230px;
- margin: 0.25rem 0 1.2rem -5rem;
+ margin: 0.25rem 0 0 -5rem;
padding: 1.5rem calc(5rem + 1.75rem);
display: flex;
flex-direction: column;
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile app.py",
"description": "Verify banner gap adjustment"
}
<shellId: 172 completed with exit code 0>
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py",
"description": "Check banner spacing diff"
}
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
<shellId: 173 completed with exit code 0>
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
140,
170
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -140,31 +140,31 @@
src="data:audio/mpeg;base64,{audio_base64}"
type="audio/mpeg"
>
</audio>
""",
unsafe_allow_html=True,
)
st.markdown("""
<style>
body, .stApp, .stApp input, .stApp textarea, .stApp button {
font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important;
}
.stApp {
background: var(--house-background, #171719);
color: #e8ddca;
transition: background 1.2s ease;
}
[data-testid="stMainBlockContainer"] {
padding-top: 1rem;
}
[data-testid="stSidebar"] {
background: linear-gradient(180deg, #17191a 0%, #1c1d1d 55%, #171819 100%);
border-right: 1px solid #39332d;
}
[data-testid="stSidebar"] [data-testid="stMarkdownContainer"] h2 {
margin: 0 0 0.15rem;
color: #e7d4b8 !important;
font-size: 1.18rem;
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
205,
230
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -205,26 +205,26 @@
letter-spacing: 0.16em;
}
h1, h2, h3 {
color: #e8ddca !important;
font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important;
}
.page-header {
width: calc(100% + 10rem);
min-height: 230px;
margin: 0.25rem 0 0 -5rem;
padding: 1.5rem calc(5rem + 1.75rem);
display: flex;
flex-direction: column;
justify-content: flex-end;
border-bottom: 1px solid rgba(176, 132, 82, 0.32);
background-color: #17191a;
background-position: center 43%;
background-size: cover;
}
@media (max-width: 768px) {
.page-header {
width: 100%;
margin-left: 0;
padding: 1.25rem;
}
}
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
- padding-top: 1rem;
+ padding-top: 0;
@@
- margin: 0.25rem 0 0 -5rem;
+ margin: 0 -5rem 1.2rem;
*** End Patch
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -158,7 +158,7 @@
transition: background 1.2s ease;
}
[data-testid="stMainBlockContainer"] {
- padding-top: 1rem;
+ padding-top: 0;
}
[data-testid="stSidebar"] {
background: linear-gradient(180deg, #17191a 0%, #1c1d1d 55%, #171819 100%);
@@ -211,7 +211,7 @@
.page-header {
width: calc(100% + 10rem);
min-height: 230px;
- margin: 0.25rem 0 0 -5rem;
+ margin: 0 -5rem 1.2rem;
padding: 1.5rem calc(5rem + 1.75rem);
display: flex;
flex-direction: column;
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile app.py",
"description": "Verify top banner gap adjustment"
}
<shellId: 174 completed with exit code 0>
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py",
"description": "Check corrected spacing diff"
}
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
<shellId: 175 completed with exit code 0>
Also add an About link, somewhere at the bottom, which will redirect to my github https://github.com/luc-rap/hacktoberfest-open-source-ai-week4
I’ll add a small “About” link at the bottom of the sidebar that opens your GitHub repository in a new tab.
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
house_ambience_enabled = st.checkbox("House ambience", value=True)
if house_ambience_enabled:
play_background_mus...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -458,6 +458,13 @@
if house_ambience_enabled:
play_background_music(Path("assets") / "ambience.mp3")
+ st.markdown(
+ '<div class="sidebar-about"><a '
+ 'href="https://github.com/luc-rap/hacktoberfest-open-source-ai-week4" '
+ 'target="_blank" rel="noopener noreferrer">About · GitHub</a></div>',
+ unsafe_allow_html=True,
+ )
+
banner_image = haunted_house_banner()
banner_style = ""
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
.sidebar-brand .muted {
color: #9d9386;
font-size: 0.64rem;
letter-spacing: 0.16em;
}
+.sidebar-about {
+ margin...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -204,6 +204,20 @@
font-size: 0.64rem;
letter-spacing: 0.16em;
}
+.sidebar-about {
+ margin-top: 1.4rem;
+ padding-top: 0.65rem;
+ border-top: 1px solid rgba(176, 132, 82, 0.22);
+ font-size: 0.78rem;
+}
+.sidebar-about a {
+ color: #bda17e;
+ text-decoration: none;
+}
+.sidebar-about a:hover {
+ color: #eee1cd;
+ text-decoration: underline;
+}
h1, h2, h3 {
color: #e8ddca !important;
font-family: "Palatino Linotype", "Book Antiqua", Georgia, serif !important;
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile app.py",
"description": "Verify About link UI compiles"
}
<shellId: 177 completed with exit code 0>
{
"command": "Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py",
"description": "Check About link formatting"
}
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
<shellId: 176 completed with exit code 0>
Added a small About · GitHub link at the bottom of the sidebar. It opens your repository in a new tab. app.py compiles successfully.
can you remember the fancy border from the chat? it kinda breaks when you scroll
The chat’s ornamental corners are attached to the scrolling container, so they scroll with the messages. I’ll move the frame to a non-scrolling wrapper around the chat panel so the corners stay fixed while the messages scroll.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
250,
290
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -250,41 +250,41 @@
text-transform: uppercase;
}
.page-header .title {
margin: 0;
color: #eee1cd;
font-size: clamp(1.8rem, 3vw, 2.45rem);
line-height: 1.1;
}
.page-header .subtitle {
max-width: 42rem;
margin: 0.55rem 0 0;
color: #b9afa1;
font-size: 1rem;
font-style: italic;
line-height: 1.55;
}
.st-key-scene_panel,
.st-key-conversation_panel {
position: relative;
outline: 1px solid rgba(176, 132, 82, 0.28);
outline-offset: -1px;
border-radius: 4px;
}
.st-key-scene_panel::before,
.st-key-conversation_panel::before {
content: "";
position: absolute;
inset: 5px;
z-index: 2;
pointer-events: none;
opacity: 0.72;
background:
linear-gradient(#bd9567, #bd9567) left top / 18px 1px no-repeat,
linear-gradient(#bd9567, #bd9567) left top / 1px 18px no-repeat,
linear-gradient(#bd9567, #bd9567) right top / 18px 1px no-repeat,
linear-gradient(#bd9567, #bd9567) right top / 1px 18px no-repeat,
linear-gradient(#bd9567, #bd9567) left bottom / 18px 1px no-repeat,
linear-gradient(#bd9567, #bd9567) left bottom / 1px 18px no-repeat,
linear-gradient(#bd9567, #bd9567) right bottom / 18px 1px no-repeat,
linear-gradient(#bd9567, #bd9567) right bottom / 1px 18px no-repeat;
}
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
500,
535
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -500,36 +500,36 @@
)
with st.expander("The letter and arrival", expanded=True):
st.markdown(INTRO)
scene_column, conversation_column = st.columns([1, 1.25], gap="large")
room = ROOMS[game["room"]]
with scene_column:
with st.container(height="content", border=True, key="scene_panel"):
st.markdown(f"### {game['room']}")
if "img" in room:
show_image(
Path(__file__).resolve().parent / "assets" / room["img"],
width='stretch',
)
else:
st.caption("No image is available for this room yet.")
st.markdown(
f'<div class="story">{room["description"]}</div>',
unsafe_allow_html=True,
)
with conversation_column:
thinking_placeholder = st.empty()
st.markdown("### What will you do?")
with st.container(
height=480,
border=True,
autoscroll=True,
key="conversation_panel",
):
for message in game["history"]:
with st.chat_message(
message["role"],
avatar="🕯️" if message["role"] == "assistant" else "🧑",
python
{
"command": "python -c \"import streamlit as st, inspect; print(inspect.signature(st.container))\"",
"description": "Confirm Streamlit keyed container support"
}
(*, border: 'bool | None' = None, key: 'Key | None' = None, width: 'Width' = 'stretch', height: 'Height' = 'content', horizontal: 'bool' = False, wrap: 'bool' = True, horizontal_alignment: 'HorizontalAlignment' = 'left', vertical_alignment: 'VerticalAlignment' = 'top', gap: 'Gap | None' = 'small', autoscroll: 'bool | None' = None) -> 'DeltaGenerator'
<shellId: 178 completed with exit code 0>
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
-.st-key-scene_panel,
-.st-key-conversation_panel {
+.st-key-scene_panel,
+.st-key-conversation_frame {
position: relative;
...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -264,7 +264,7 @@
line-height: 1.55;
}
.st-key-scene_panel,
-.st-key-conversation_panel {
+.st-key-conversation_frame {
position: relative;
outline: 1px solid rgba(176, 132, 82, 0.28);
outline-offset: -1px;
@@ -271,7 +271,7 @@
border-radius: 4px;
}
.st-key-scene_panel::before,
-.st-key-conversation_panel::before {
+.st-key-conversation_frame::before {
content: "";
position: absolute;
inset: 5px;
@@ -523,25 +523,21 @@
with conversation_column:
thinking_placeholder = st.empty()
st.markdown("### What will you do?")
- with st.container(
- height=480,
- border=True,
- autoscroll=True,
- key="conversation_panel",
- ):
- for message in game["history"]:
- with st.chat_message(
- message["role"],
- avatar="🕯️" if message["role"] == "assistant" else "🧑",
- ):
- st.markdown(message["content"])
+ with st.container(key="conversation_frame"):
+ with st.container(height=480, border=True, autoscroll=True):
+ for message in game["history"]:
+ with st.chat_message(
+ message["role"],
+ avatar="🕯️" if message["role"] == "assistant" else "🧑",
+ ):
+ st.markdown(message["content"])
- if game["ending"]:
- st.markdown("---")
- st.markdown("**THE END**")
+ if game["ending"]:
+ st.markdown("---")
+ st.markdown("**THE END**")
- if "ollama_error" in st.session_state:
- st.error(st.session_state.ollama_error)
+ if "ollama_error" in st.session_state:
+ st.error(st.session_state.ollama_error)
action = st.chat_input(
"Describe what you do...",
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile app.py; python -m unittest discover -s tests -p 'test_engine.py'","descr...
........................
----------------------------------------------------------------------
Ran 24 tests in 0.009s
OK
<shellId: 180 completed with exit code 0>
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager diff --check -- the-house-that-remembers/app.py; git --no-pager diff --stat -- the-house-that-remembers/...
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
the-house-that-remembers/app.py | 38 +++++++++++++++++---------------------
1 file changed, 17 insertions(+), 21 deletions(-)
<shellId: 179 completed with exit code 0>
Moved the chat’s decorative frame onto a separate, non-scrolling wrapper, leaving the messages inside their own scrollable container. The corners should now stay fixed while you scroll through the conversation.
app.py compiles, and all 24 regression tests pass.
I think the music/sounds stop every time there is an update, e.g. clue is added, or room changed, I am not sure if that can be fixed though, depends on streamlit
You’re right. Streamlit reruns the script when clues or rooms change, and the audio players are currently created inside that rerun, so playback can stop or restart. That’s expected with the current approach in app.py.
It can be improved, though seamless playback takes a browser-side solution rather than ordinary Streamlit audio controls. A persistent player can keep its playback position across UI updates and resume, but browsers may briefly it or block automatic resumption. A custom Streamlit component is the more reliable route; it’s doable, but more involved than a CSS or layout change.
it's okay, not so important
Understood—we can leave the audio behavior as it is for now.
She hesitates, glancing toward the upper landing before turning back to you. "It's just... what we keep here," she murmurs, her voice barely above a whisper. Then, looking past you towards the library archway, she suggests gently, "Perhaps we should go back inside? To the library."
It is weird that the sister invited us to go to the library. Is that written somewhere?
{"pattern":"library|sister.*suggest|go back inside|suggestion|destination|steer|invites|move happens","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.p...
[grep content: 22 matches across 3 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
prompt.py (13 match(es)):
27: - Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
34: - When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
42: "It's strange being back in the library. We used to spend whole afternoons in here."
51: "We used to spend a lot of time in the library"
59: "steer": "Let the player settle in and explore. Keep the friend's suggestions tied to what the player just said; if tea or a drink is wanted, the kitchen is the natural next step. Do not offer upstairs or list destinations without a reason. When the friend invites the player somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
64: "steer": "Let this come up once or twice in passing, e.g. \"Mum's resting. She hasn't been sleeping well.\" Do not explain it.",
69: "steer": "Describe frozen or changed details only when the engine provides them. Otherwise keep the house quietly wrong: a sound repeating, a door slightly off.",
74: "steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most. She speaks gently and urgently. She never threatens. If the ENGINE RESULT authorizes a private conversation, she may explain that she believes preserving the family keeps them alive; present this as her belief, not as confirmed fact. Otherwise do not say what happened to the friend's family.",
79: "steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.",
84: "steer": "If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, the friend shares the mother's decline, the father's choice, the sister's hope, and his own silence one memory at a time when the ENGINE RESULT supplies each. Let the moment breathe between disclosures without filling s with small talk. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.",
199: f"STEER TOWARD (gently, never force): {s['steer']}\n"
231: "helping the friend with kitchen chores; exploring the family library; "
329: "movement": "Narrate the confirmed route and destination. Keep the friend at the location given in CURRENT STATE unless the ENGINE RESULT says he accompanied the player.",
story/canon.json (2 match(es)):
103: "early_behavior": "Before the bedroom truth is faced, he sometimes initiates gentle small talk or asks about the player's life, drawing only on established childhood memories such as playing outside, helping with kitchen chores, exploring the library, and sleepovers. After the player enters the bedroom and learns the truth, he does not initiate small talk or pivot to ordinary topics; conversation stays with the family, grief, and the player's immediate choices.",
249: "delivery": "Found only in a water-stained local history in the library. Never explained by characters. The house is still haunted by this and other past tragedies."
engine.py (7 match(es)):
122: "img": "library.jpg",
123: "aliases": ["library"],
429: r"|\b(examine|inspect|search|study|explore|look)\w*\b.*\b(kitchen|library|hall|staircase|landing|bedroom)\b"
588: """Hint for steering a lingering player onward. None if there is nowhere to send them."""
612: f"toward {where}. The move happens only if the player chooses it."
618: "The move happens only if the player chooses it."
1002: # Nudge: the room is exhausted and the player keeps lingering -> steer them onward.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
15,
46
]
}
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
@@ -15,32 +15,32 @@
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Before the bedroom truth is faced, use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. Once the player has entered the bedroom and learned the truth, do not initiate small talk or pivot to ordinary topics.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT normally brings him into the player's room; narrate that arrival before his reply. The exception is an ENGINE RESULT-marked private sister conversation: answer as the sister and keep the friend elsewhere.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech.
- When the ENGINE RESULT authorizes the sister's private conversation, she may explain that she believes preserving the family keeps them alive. This is her perspective, not objective proof that the family is alive, and does not reveal the full truth.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good."
"Do you remember how we used to help in the kitchen? I was never much good at it."
"It's strange being back in the library. We used to spend whole afternoons in here."
"I haven't been sleeping much. The house is loud at night."
"Tea sounds good. The kitchen's just this way, if you'd like."
"I miss the old days. Now, everything is so... different. I don't know how to explain it."
"I just wanted to see you. I haven't had anyone to talk to in a long time."
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
57,
79
]
}
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
@@ -57,23 +57,23 @@
"now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet. Friend invited the player to the house because he is lonely and the history of the house is weighing on him. He is not aware of the truth, but he doesn't want to be alone.",
"friend": "He tries to act normal and spend ordinary time together. He can ask about the player's life or mention an established childhood memory. He deflects direct questions about the house and does not bring up family members on his own.",
"steer": "Let the player settle in and explore. Keep the friend's suggestions tied to what the player just said; if tea or a drink is wanted, the kitchen is the natural next step. Do not offer upstairs or list destinations without a reason. When the friend invites the player somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
},
{ # 1 - Contradictions
"now": "Details about the friend's family do not quite add up.",
"friend": "He mentions his family casually, in the present tense, as if they are alive and nearby: his mother is resting, his father is busy upstairs, his sister is 'around'. He does not notice anything odd. If asked to see them, or asked directly about them, he answers vaguely and changes the subject.",
"steer": "Let this come up once or twice in passing, e.g. \"Mum's resting. She hasn't been sleeping well.\" Do not explain it.",
},
{ # 2 - Frozen house and family traces
"now": "The house is frozen in time: clocks are stopped, calendars are out of date, food never spoils, and places are set for people who are not there. Portraits can change between looks. The friend has lost track of time.",
"friend": "He says time is 'strange' or that he has lost count of the days. He grows uneasy when stopped clocks, old dates or the extra place settings are mentioned and tries to move on.",
"steer": "Describe frozen or changed details only when the engine provides them. Otherwise keep the house quietly wrong: a sound repeating, a door slightly off.",
},
{ # 3 - Sister's warnings
"now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.",
"friend": "He does not see her, or insists the player imagined it.",
"steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most. She speaks gently and urgently. She never threatens. If the ENGINE RESULT authorizes a private conversation, she may explain that she believes preserving the family keeps them alive; present this as her belief, not as confirmed fact. Otherwise do not say what happened to the friend's family.",
},
{ # 4 - Parents' fragments
"now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.",
"friend": "He flinches at the knocking and tells the player, \"Whatever you do, don't answer it.\" Fragments of memory slip out, then he retreats.",
"steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.",
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
208,
245
]
}
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
@@ -208,38 +208,38 @@
room = ROOMS[game["room"]]
friend_room = game.get("friend_room", "Entrance Hall")
lines = [
"CURRENT STATE",
f"Room: {game['room']}",
f"Room description: {room['description'].strip()}",
f"Truth faced: {'yes' if game.get('truth_known') else 'no'}",
f"Friend's denial: {game['denial']}/10",
f"Family memories shared: {len(game.get('final_revelations', []))}/4",
f"Shared family memories: {', '.join(game.get('final_revelations', [])) or 'none'}",
f"Sister's private conversation available: {'yes' if game.get('sister_bargain_available') else 'no'}",
f"Sister's preservation belief heard: {'yes' if game.get('sister_bargain_heard') else 'no'}",
f"Sister physically present: {'yes' if game.get('sister_present') else 'no'}",
f"Sister conversation turns: {game.get('sister_conversation_turns', 0)}/5",
f"Exits: {', '.join(exit_labels(game, game['room']))}",
(
"Friend location: with the player in this room."
if friend_room == game["room"]
else f"Friend location: {friend_room}. He is not with the player and cannot hear them."
),
f"Inventory: {', '.join(game['inventory']) or 'empty'}",
f"Clues the player has found: {', '.join(game['clues']) or 'none'}",
"Established player memories: childhood companions; playing outside together; "
"helping the friend with kitchen chores; exploring the family library; "
"sleeping over in the guest room.",
]
if game["summary"]:
lines.append(f"Earlier in the story: {game['summary']}")
shared_revelations = set(game.get("final_revelations", []))
lines.extend(
f"Family memory already shared ({memory['title']}): {memory['detail']}"
for memory in FINAL_REVELATIONS
if memory["id"] in shared_revelations
)
if game.get("sister_bargain_heard"):
lines.append(f"Sister's revealed perspective: {SISTER_PRESERVATION_MEMORY}")
return "\n".join(lines)
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
21,
38
]
}
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
@@ -21,18 +21,18 @@
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Before the bedroom truth is faced, use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. Once the player has entered the bedroom and learned the truth, do not initiate small talk or pivot to ordinary topics.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT normally brings him into the player's room; narrate that arrival before his reply. The exception is an ENGINE RESULT-marked private sister conversation: answer as the sister and keep the friend elsewhere.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech.
- When the ENGINE RESULT authorizes the sister's private conversation, she may explain that she believes preserving the family keeps them alive. This is her perspective, not objective proof that the family is alive, and does not reveal the full truth.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
71,
78
]
}
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
@@ -71,8 +71,8 @@
{ # 3 - Sister's warnings
"now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.",
"friend": "He does not see her, or insists the player imagined it.",
"steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most. She speaks gently and urgently. She never threatens. If the ENGINE RESULT authorizes a private conversation, she may explain that she believes preserving the family keeps them alive; present this as her belief, not as confirmed fact. Otherwise do not say what happened to the friend's family.",
},
{ # 4 - Parents' fragments
"now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.",
"friend": "He flinches at the knocking and tells the player, \"Whatever you do, don't answer it.\" Fragments of memory slip out, then he retreats.",
{"pattern":"sister.*(warning|private|conversation|perspective|library)|SISTER_PRESERVATION_MEMORY|result\\[\"sister\"\\]","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that...
[grep content: 29 matches across 2 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
prompt.py (12 match(es)):
32: - The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT normally brings him into the player's room; narrate that arrival before his reply. The exception is an ENGINE RESULT-marked private sister conversation: answer as the sister and keep the friend elsewhere.
36: - When the ENGINE RESULT authorizes the sister's private conversation, she may explain that she believes preserving the family keeps them alive. This is her perspective, not objective proof that the family is alive, and does not reveal the full truth.
206: from engine import FINAL_REVELATIONS, ROOMS, SISTER_PRESERVATION_MEMORY, exit_labels
221: f"Sister conversation turns: {game.get('sister_conversation_turns', 0)}/5",
243: lines.append(f"Sister's revealed perspective: {SISTER_PRESERVATION_MEMORY}")
271: if r.get("sister_private_encounter"):
273: "\nThis is the sister's private conversation opportunity, available because "
277: if r.get("sister_conversation"):
287: if r.get("sister_perspective"):
289: "\nThe sister's perspective to explain clearly if the player asked: "
290: f"{r['sister_perspective']}"
337: if r.get("sister_conversation"):
engine.py (17 match(es)):
72: SISTER_PRESERVATION_MEMORY = (
344: "sister_warning_count": 0,
348: "sister_conversation_turns": 0,
883: lambda g: g["turns_in_stage"] >= 5 and len(g["clues"]) >= 7, # 3 sister's warnings
906: game.setdefault("sister_warning_count", 0)
910: game.setdefault("sister_conversation_turns", 0)
922: and game["sister_warning_count"] >= 2
927: (game["sister_present"] or private_sister_due)
1042: game["sister_warning_count"] >= 2
1054: game["sister_conversation_turns"] = 0
1064: game["sister_warning_count"] = min(2, game["sister_warning_count"] + 1)
1068: result["sister"] = appearance
1069: result["sister_private_encounter"] = can_speak_privately
1072: result["sister_conversation"] = True
1075: result["sister_perspective"] = SISTER_PRESERVATION_MEMORY
1076: game["sister_conversation_turns"] += 1
1077: if game["sister_conversation_turns"] >= SISTER_CONVERSATION_LIMIT:
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
290,
350
]
}
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
@@ -290,61 +290,61 @@
f"{r['sister_perspective']}"
)
if r.get("family_memory"):
memory = r["family_memory"]
note += (
f"\nFamily memory to acknowledge clearly in the friend's reply "
f"({memory['title']}): {memory['detail']}"
)
if r.get("friend_arrived"):
note += "\nThe friend has joined the player to speak face to face; narrate his arrival before his response."
intent_reminders = {
"dialogue": (
"Treat the player's words as spoken to the friend and answer directly in his natural, "
"restrained voice. If the ENGINE RESULT says he joined the player, narrate his arrival "
"before the reply."
),
"memory": (
"Answer the player's memory question as their character, using only the established "
"player memories in CURRENT STATE or facts already established in conversation. "
"Do not invent a specific shared event or reveal hidden family history."
),
"physical_action": (
"Treat this as a physical action by the player. Acknowledge what they actually did "
"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. If the ENGINE RESULT says he joined the player, narrate his "
"arrival before the reply. Do not add actions for the player."
),
"examination": (
"Treat this as an examination. Use the ENGINE RESULT as the only new observation; "
"do not add clues or details."
),
"exploration": (
"Treat this as looking around or exploring. Use only the room description and "
"ENGINE RESULT for observations; do not add clues or details."
),
"movement": "Narrate the confirmed route and destination. Keep the friend at the location given in CURRENT STATE unless the ENGINE RESULT says he accompanied the player.",
"roleplay": "Respond naturally to the player's action without inventing consequential events.",
}
reminder = (
f"{intent_reminders.get(r.get('intent', 'roleplay'), intent_reminders['roleplay'])} "
"Stay in character. 2-5 short sentences. Add no other items, clues, uncanny events, "
"room changes or backstory."
)
if r.get("sister_conversation"):
reminder = (
"Answer as the sister, directly to the player. The friend is elsewhere; do not "
"bring him into the room or let him answer. Stay within the sister's established "
"beliefs and the engine-authorized perspective."
)
if game["ending"]:
reminder = "This is the final scene. 4-6 sentences. Close the story."
return note + f"\n[REMINDER] {reminder}"
def build_messages(game, canon):
system = "\n\n".join([CORE, story_block(canon), stage_block(game), state_block(game)])
live = [dict(m) for m in game["history"][game["summary_idx"]:]]
That exact invitation isn’t scripted in the game rules. It was generated by the narrator.
The prompt does give the model the shared memory of “exploring the family library” (prompt.py), and it generally allows characters to suggest places (prompt.py). The sister-specific guidance says she may explain her belief about preserving the family, but doesn’t address suggesting destinations (prompt.py.
So the library suggestion is an improvised, somewhat misplaced attempt at conversational steering—not an authored sister behavior or an engine-triggered event. The prompt could be tightened to keep her focused on her warning or preservation belief, and stop her from casually inviting the player elsewhere.
yeah, it is a bit out of place. she should keep the focus on the warning
I’ll tighten the sister-specific narration rules: her appearances should stay on the warning, and private conversation can explore her preservation belief without turning into casual talk or invitations to other rooms.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
16,
40
]
}
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
@@ -16,25 +16,25 @@
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Before the bedroom truth is faced, use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. Once the player has entered the bedroom and learned the truth, do not initiate small talk or pivot to ordinary topics.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT normally brings him into the player's room; narrate that arrival before his reply. The exception is an ENGINE RESULT-marked private sister conversation: answer as the sister and keep the friend elsewhere.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech.
- When the ENGINE RESULT authorizes the sister's private conversation, she may explain that she believes preserving the family keeps them alive. This is her perspective, not objective proof that the family is alive, and does not reveal the full truth.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good."
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
68,
79
]
}
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
@@ -68,12 +68,12 @@
"friend": "He says time is 'strange' or that he has lost count of the days. He grows uneasy when stopped clocks, old dates or the extra place settings are mentioned and tries to move on.",
"steer": "Describe frozen or changed details only when the engine provides them. Otherwise keep the house quietly wrong: a sound repeating, a door slightly off.",
},
{ # 3 - Sister's warnings
"now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.",
"friend": "He does not see her, or insists the player imagined it.",
"steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most. She speaks gently and urgently. She never threatens. If the ENGINE RESULT authorizes a private conversation, she may explain that she believes preserving the family keeps them alive; present this as her belief, not as confirmed fact. Otherwise do not say what happened to the friend's family.",
},
{ # 4 - Parents' fragments
"now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.",
"friend": "He flinches at the knocking and tells the player, \"Whatever you do, don't answer it.\" Fragments of memory slip out, then he retreats.",
"steer": "Use incomplete echoes and half-heard sounds. Never state what happened to the friend's family. The door itself opens only when the engine says so.",
{"pattern":"sister.*(warning|private|suggest|destination)|private.*sister","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\tests\\test_engine.py"],"output_mod...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:138: def test_sister_private_conversation_requires_two_warnings_and_friend_absent(self):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:145: sister_warning_count=2,
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:152: self.assertTrue(encounter["sister_private_encounter"])
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:166: def test_player_can_speak_to_sister_on_private_encounter_turn(self):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:173: sister_warning_count=2,
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:180: self.assertTrue(result["sister_private_encounter"])
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:194: sister_warning_count=2,
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:217: sister_warning_count=2,
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:235: sister_warning_count=2,
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:257: def test_sister_does_not_offer_private_conversation_when_friend_is_present(self):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:264: sister_warning_count=2,
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:271: self.assertFalse(result["sister_private_encounter"])
*** Begin Patch
*** Update File: the-house-that-remembers/prompt.py
@@
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account o...
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
@@ -33,7 +33,7 @@
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech.
-- When the ENGINE RESULT authorizes the sister's private conversation, she may explain that she believes preserving the family keeps them alive. This is her perspective, not objective proof that the family is alive, and does not reveal the full truth.
+- The sister's appearances stay focused on her warning that the player should leave. Do not have her suggest rooms, activities, or casual conversation. During an ENGINE RESULT-authorized private conversation, she may explain her belief that preserving the family keeps them alive and discuss her bargain, but keep the exchange focused on that belief and warning. This is her perspective, not objective proof that the family is alive, and does not reveal the full truth. These sister-specific limits override the general suggestion and small-talk guidance.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
@@ -71,7 +71,7 @@
{ # 3 - Sister's warnings
"now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.",
"friend": "He does not see her, or insists the player imagined it.",
- "steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most. She speaks gently and urgently. She never threatens. If the ENGINE RESULT authorizes a private conversation, she may explain that she believes preserving the family keeps them alive; present this as her belief, not as confirmed fact. Otherwise do not say what happened to the friend's family.",
+ "steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most. She speaks gently and urgently, keeping focus on warning the player to leave. She never threatens, suggests going to another room, or starts casual conversation. If the ENGINE RESULT authorizes a private conversation, she may explain that she believes preserving the family keeps them alive and discuss her bargain; present this as her belief, not as confirmed fact. Otherwise do not say what happened to the friend's family.",
},
{ # 4 - Parents' fragments
"now": "Slow knocking and footsteps come from behind the locked bedroom door upstairs. They are the parents' lingering fragments: patient, never angry. They want the repeating memory to end.",
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
@@ -6,7 +6,7 @@
from pathlib import Path
import engine
-from prompt import _engine_note, stage_block, state_block, story_block
+from prompt import CORE, _engine_note, stage_block, state_block, story_block
CANON = json.loads(
@@ -163,6 +163,18 @@
self.assertEqual(game["friend_room"], "Library")
self.assertIn("Sister's revealed perspective:", state_block(game))
+ def test_sister_warnings_stay_focused_and_do_not_suggest_destinations(self):
+ self.assertIn("Do not have her suggest rooms, activities, or casual conversation.", CORE)
+ self.assertIn("These sister-specific limits override the general suggestion", CORE)
+ self.assertIn("keeping focus on warning the player to leave", stage_block({
+ "ending": None,
+ "stage": 3,
+ "truth_known": False,
+ "denial": 8,
+ "final_revelations": [],
+ "turns_in_stage": 0,
+ }))
+
def test_player_can_speak_to_sister_on_private_encounter_turn(self):
game = engine.new_game()
game.update(
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run sister prompt re...
.........................
----------------------------------------------------------------------
Ran 25 tests in 0.009s
OK
<shellId: 181 completed with exit code 0>
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile prompt.py tests\\test_engine.py","description":"Check prompt and test sy...
<shellId: 183 completed with exit code 0>
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager diff --check -- the-house-that-remembers/prompt.py the-house-that-remembers/tests/test_engine.py","descr...
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
<shellId: 182 completed with exit code 0>
Updated the narration guidance so the sister stays focused on warning the player to leave. She won’t suggest going to another room or drift into casual conversation. During her private encounter, she can still explain her preservation belief and discuss her bargain—but the exchange should remain centered on that.
Added a regression test for the prompt guidance. All 25 tests pass, and syntax and diff checks are clean. See prompt.py and test_engine.py.
I changed the model to gemma4:e2b because it was too slow. Now It's acting a little weird. It isn't all that bad, but this is in the output:
Game state:
{'room': 'Entrance Hall',
'friend_room': 'Entrance Hall',
'inventory': ['The Letter'],
'clues': [],
'visited': ['Entrance Hall'],
'examined': {},
'stage': 0,
'turns_in_stage': 1,
'turn_count': 1,
'room_turns': 1,
'last_notice_turn': -99,
'last_nudge_turn': -99,
'nudges_here': 0,
'last_sister_turn': -99,
'sister_warning_count': 0,
'sister_bargain_available': False,
'sister_bargain_heard': False,
'sister_present': False,
'sister_conversation_turns': 0,
'denial': 8,
'pushed': 0,
'support_given': 0,
'door_open': False,
'truth_known': False,
'final_revelations': [],
'sister_seen': False,
'ending': None,
'last_result': {'nudge': None,
'outcome': 'no_change',
'detail': 'Nothing the engine tracks changed. Treat the '
'action as ordinary roleplay (talking, resting, '
'small gestures). Do not add items, clues, or '
'events. No room change happens: the player is '
'still in the Entrance Hall, and nobody leads them '
'elsewhere unless they choose an exit (Library, '
'Kitchen, Grand Staircase).',
'new_clue': None,
'ambient': None,
'friend': None,
'intent': 'dialogue',
'stage_advanced': False},
'history': [{'role': 'assistant',
'content': '"You came. I didn\'t think you would... Thank you."\n'
'\n'},
{'role': 'user',
'content': '"I was worried. I read your letter... What\'s '
'wrong?"'}],
'summary': '',
'summary_idx': 0}
Game state:
{'room': 'Entrance Hall',
'friend_room': 'Entrance Hall',
'inventory': ['The Letter'],
'clues': [],
'visited': ['Entrance Hall'],
'examined': {},
'stage': 0,
'turns_in_stage': 2,
'turn_count': 2,
'room_turns': 2,
'last_notice_turn': -99,
'last_nudge_turn': -99,
'nudges_here': 0,
'last_sister_turn': -99,
'sister_warning_count': 0,
'sister_bargain_available': False,
'sister_bargain_heard': False,
'sister_present': False,
'sister_conversation_turns': 0,
'denial': 8,
'pushed': 0,
'support_given': 0,
'door_open': False,
'truth_known': False,
'final_revelations': [],
'sister_seen': False,
'ending': None,
'last_result': {'nudge': None,
'outcome': 'no_change',
'detail': 'Nothing the engine tracks changed. Treat the '
'action as ordinary roleplay (talking, resting, '
'small gestures). Do not add items, clues, or '
'events. No room change happens: the player is '
'still in the Entrance Hall, and nobody leads them '
'elsewhere unless they choose an exit (Library, '
'Kitchen, Grand Staircase).',
'new_clue': None,
'ambient': None,
'friend': None,
'intent': 'dialogue',
'stage_advanced': False},
'history': [{'role': 'assistant',
'content': '"You came. I didn\'t think you would... Thank you."\n'
'\n'},
{'role': 'user',
'content': '"I was worried. I read your letter... What\'s '
'wrong?"'},
{'role': 'assistant',
'content': 'He shifts his weight, his gaze flickering toward the '
'shadowed corners of the hall. "Nothing is wrong," he '
'murmurs, his voice thin. "It\'s just... things are '
'different now. I don\'t know how to explain it."'},
{'role': 'user', 'content': '"Different in... what way?"'}],
'summary': '',
'summary_idx': 0}
Game state:
{'room': 'Entrance Hall',
'friend_room': 'Entrance Hall',
'inventory': ['The Letter'],
'clues': ['A musty smell lingers in the air'],
'visited': ['Entrance Hall'],
'examined': {'Entrance Hall:smell': 1},
'stage': 0,
'turns_in_stage': 3,
'turn_count': 3,
'room_turns': 3,
'last_notice_turn': 3,
'last_nudge_turn': -99,
'nudges_here': 0,
'last_sister_turn': -99,
'sister_warning_count': 0,
'sister_bargain_available': False,
'sister_bargain_heard': False,
'sister_present': False,
'sister_conversation_turns': 0,
'denial': 7,
'pushed': 0,
'support_given': 0,
'door_open': False,
'truth_known': False,
'final_revelations': [],
'sister_seen': False,
'ending': None,
'last_result': {'nudge': None,
'outcome': 'no_change',
'detail': 'Nothing the engine tracks changed. Treat the '
'action as ordinary roleplay (talking, resting, '
'small gestures). Do not add items, clues, or '
'events. No room change happens: the player is '
'still in the Entrance Hall, and nobody leads them '
'elsewhere unless they choose an exit (Library, '
'Kitchen, Grand Staircase).',
'new_clue': 'A musty smell lingers in the air',
'ambient': 'A musty smell lingers in the air, a mixture of '
'old wood and dust.',
'friend': None,
'intent': 'dialogue',
'stage_advanced': False},
'history': [{'role': 'assistant',
'content': '"You came. I didn\'t think you would... Thank you."\n'
'\n'},
{'role': 'user',
'content': '"I was worried. I read your letter... What\'s '
'wrong?"'},
{'role': 'assistant',
'content': 'He shifts his weight, his gaze flickering toward the '
'shadowed corners of the hall. "Nothing is wrong," he '
'murmurs, his voice thin. "It\'s just... things are '
'different now. I don\'t know how to explain it."'},
{'role': 'user', 'content': '"Different in... what way?"'},
{'role': 'assistant',
'content': 'He looks down at his hands, rubbing them together '
'slowly. "It\'s just... heavy," he says quietly. "The '
"house feels like it's holding something. I don't "
'know what to say about it."'},
{'role': 'user', 'content': '"What can I do to help you?"'}],
'summary': '',
'summary_idx': 0}
The model says:
He sighs, a sound that seems too large for the quiet hall. "Just... sit down," he suggests, gesturing vaguely toward a nearby, dark chair. "We can just... be here for a while."
It didn't mention the clue, but the game state updated anyway. what's the problem?
{"pattern":"pprint|print\\(|ENGINE RESULT|last_result|new_clue|ambient|def _engine_note|build_messages|generation_options","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-tha...
[grep content: 62 matches across 3 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
app.py (3 match(es)):
22: from prompt import build_messages, generation_options, maybe_summarize, tidy
71: messages=build_messages(game, CANON),
73: options=generation_options(game),
prompt.py (25 match(es)):
30: - The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion.
32: - The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT normally brings him into the player's room; narrate that arrival before his reply. The exception is an ENGINE RESULT-marked private sister conversation: answer as the sister and keep the friend elsewhere.
34: - When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
36: - The sister's appearances stay focused on her warning that the player should leave. Do not have her suggest rooms, activities, or casual conversation. During an ENGINE RESULT-authorized private conversation, she may explain her belief that preserving the family keeps them alive and discuss her bargain, but keep the exchange focused on that belief and warning. This is her perspective, not objective proof that the family is alive, and does not reveal the full truth. These sister-specific limits override the general suggestion and small-talk guidance.
37: - Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
59: "steer": "Let the player settle in and explore. Keep the friend's suggestions tied to what the player just said; if tea or a drink is wanted, the kitchen is the natural next step. Do not offer upstairs or list destinations without a reason. When the friend invites the player somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
72: "now": "The sister's first appearance is authorized when the ENGINE RESULT supplies it; after that she may appear intermittently only when the engine calls for it. She hums and warns the player to leave. She is sincere, loving, frightened and mistaken about what is best.",
74: "steer": "When the ENGINE RESULT calls for her, her appearance is brief: one or two lines at most. She speaks gently and urgently, keeping focus on warning the player to leave. She never threatens, suggests going to another room, or starts casual conversation. If the ENGINE RESULT authorizes a private conversation, she may explain that she believes preserving the family keeps them alive and discuss her bargain; present this as her belief, not as confirmed fact. Otherwise do not say what happened to the friend's family.",
83: "friend": "His denial breaks only as he becomes ready. He speaks in halting fragments and quiet realization, not speeches. If denial is still 3 or higher, he resists and cannot share the next family memory. When denial falls below 3, the ENGINE RESULT may supply one authored family memory; include it honestly and clearly, without adding facts. After the player has entered and learned the truth, keep his conversation focused on the family, his grief, the player's immediate response, and whether to stay or leave. Do not initiate small talk, ask what the player has been doing, bring up unrelated childhood memories, or pivot to ordinary topics. He does not blame the player. He never agrees to leave together unless the engine ends the story.",
84: "steer": "If the player has not entered the bedroom, let them choose whether to go in; do not reveal what happened yet. Once the truth is faced, the friend shares the mother's decline, the father's choice, the sister's hope, and his own silence one memory at a time when the ENGINE RESULT supplies each. Let the moment breathe between disclosures without filling s with small talk. Never describe wounds, decay or gore. The family looks asleep. Do not force the player to leave, stay or forgive.",
181: "the ENGINE RESULT supplies it this turn."
247: def _engine_note(game):
248: r = game["last_result"]
252: "\n\n[ENGINE RESULT - authoritative]\n"
257: if r.get("ambient"):
260: f"without making it a big moment: {r['ambient']}"
264: if r["new_clue"]:
265: note += f"\nNew clue found: {r['new_clue']}"
303: "restrained voice. If the ENGINE RESULT says he joined the player, narrate his arrival "
318: "have the friend respond. If the ENGINE RESULT says he joined the player, narrate his "
322: "Treat this as an examination. Use the ENGINE RESULT as the only new observation; "
327: "ENGINE RESULT for observations; do not add clues or details."
329: "movement": "Narrate the confirmed route and destination. Keep the friend at the location given in CURRENT STATE unless the ENGINE RESULT says he accompanied the player.",
348: def build_messages(game, canon):
356: def generation_options(game):
engine.py (34 match(es)):
7: from pprint import pprint
99: "ambient": True,
105: "ambient": True,
126: "ambient": True,
136: "ambient": True,
146: "ambient": True,
157: "ambient": True,
188: "ambient": True,
198: "ambient": True,
207: "ambient": True,
217: "ambient": True,
237: "ambient": True,
247: "ambient": True,
270: "ambient": True,
357: "last_result": None, # engine result for the current turn
519: def _result(outcome, detail, new_clue=None, ambient=None, friend=None):
524: "new_clue": new_clue,
525: "ambient": ambient, # small detail to weave into a quiet turn
549: """First ambient feature in this room the player hasn't encountered yet."""
551: if feat.get("ambient") and game["examined"].get(f"{room_name}:{name}", 0) == 0:
675: result["new_clue"] = clue
835: noticed, new_clue = _examine(game, room_name, name, feat)
843: new_clue,
852: noticed, new_clue = _examine(game, room_name, name, feat)
856: new_clue,
973: noticed, new_clue = _examine(game, game["room"], name, feat)
974: result["ambient"] = noticed
975: result["new_clue"] = new_clue
998: fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
1019: if result["new_clue"]:
1020: for _ in result["new_clue"].split("; "):
1081: game["last_result"] = result
1082: print("Game state:")
1083: pprint(game, sort_dicts=False)
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
250,
365
]
}
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
@@ -250,116 +250,116 @@
return ""
note = (
"\n\n[ENGINE RESULT - authoritative]\n"
f"Player intent: {r.get('intent', 'roleplay')}\n"
f"Outcome: {r['outcome']}\n"
f"Detail: {r['detail']}"
)
if r.get("ambient"):
note += (
"\nAmbient detail to weave into this turn in one quiet sentence, "
f"without making it a big moment: {r['ambient']}"
)
if r.get("nudge"):
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']}"
friend_is_here = game.get("friend_room", "Entrance Hall") == 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']}"
if r.get("sister_private_encounter"):
note += (
"\nThis is the sister's private conversation opportunity, available because "
"she has already warned the player twice and the friend is elsewhere. "
"The player may speak with her even if the bedroom door is still closed."
)
if r.get("sister_conversation"):
note += (
"\nThe player is speaking privately with the sister. Respond as the sister, "
"not the friend. The friend is elsewhere and must not answer or arrive."
)
if r.get("sister_departed"):
note += (
"\nAfter this reply, the sister vanishes. Her bargain remains an available "
"choice if she has explained her perspective; the player may also move or leave normally."
)
if r.get("sister_perspective"):
note += (
"\nThe sister's perspective to explain clearly if the player asked: "
f"{r['sister_perspective']}"
)
if r.get("family_memory"):
memory = r["family_memory"]
note += (
f"\nFamily memory to acknowledge clearly in the friend's reply "
f"({memory['title']}): {memory['detail']}"
)
if r.get("friend_arrived"):
note += "\nThe friend has joined the player to speak face to face; narrate his arrival before his response."
intent_reminders = {
"dialogue": (
"Treat the player's words as spoken to the friend and answer directly in his natural, "
"restrained voice. If the ENGINE RESULT says he joined the player, narrate his arrival "
"before the reply."
),
"memory": (
"Answer the player's memory question as their character, using only the established "
"player memories in CURRENT STATE or facts already established in conversation. "
"Do not invent a specific shared event or reveal hidden family history."
),
"physical_action": (
"Treat this as a physical action by the player. Acknowledge what they actually did "
"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. If the ENGINE RESULT says he joined the player, narrate his "
"arrival before the reply. Do not add actions for the player."
),
"examination": (
"Treat this as an examination. Use the ENGINE RESULT as the only new observation; "
"do not add clues or details."
),
"exploration": (
"Treat this as looking around or exploring. Use only the room description and "
"ENGINE RESULT for observations; do not add clues or details."
),
"movement": "Narrate the confirmed route and destination. Keep the friend at the location given in CURRENT STATE unless the ENGINE RESULT says he accompanied the player.",
"roleplay": "Respond naturally to the player's action without inventing consequential events.",
}
reminder = (
f"{intent_reminders.get(r.get('intent', 'roleplay'), intent_reminders['roleplay'])} "
"Stay in character. 2-5 short sentences. Add no other items, clues, uncanny events, "
"room changes or backstory."
)
if r.get("sister_conversation"):
reminder = (
"Answer as the sister, directly to the player. The friend is elsewhere; do not "
"bring him into the room or let him answer. Stay within the sister's established "
"beliefs and the engine-authorized perspective."
)
if game["ending"]:
reminder = "This is the final scene. 4-6 sentences. Close the story."
return note + f"\n[REMINDER] {reminder}"
def build_messages(game, canon):
system = "\n\n".join([CORE, story_block(canon), stage_block(game), state_block(game)])
live = [dict(m) for m in game["history"][game["summary_idx"]:]]
if live and live[-1]["role"] == "user":
live[-1]["content"] += _engine_note(game)
return [{"role": "system", "content": system}, *live]
def generation_options(game):
return {
"temperature": 0.6,
"num_ctx": 8192,
"num_predict": 320 if game["ending"] else 200,
"repeat_penalty": 1.1,
}
def tidy(text):
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
365,
406
]
}
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
@@ -365,42 +365,42 @@
def tidy(text):
"""Strip leaked tags and trim a reply cut off mid-sentence."""
text = re.split(r"\[(?:ENGINE|REMINDER)", text)[0].strip()
if text and text[-1] not in '.!?"\u201d*_\u2026':
cut = max(text.rfind(c) for c in ".!?")
if cut > len(text) * 0.4:
text = text[: cut + 1]
return text
SUMMARY_TRIGGER = 20 # live messages before we fold
KEEP_RECENT = 10 # messages kept verbatim after folding
def maybe_summarize(game, model):
live = game["history"][game["summary_idx"]:]
if len(live) <= SUMMARY_TRIGGER:
return
fold = live[:-KEEP_RECENT]
transcript = "\n".join(
f"{'Player' if m['role'] == 'user' else 'Narrator'}: {m['content']}" for m in fold
)
prompt = (
"Summarize the story so far in 3-4 sentences, past tense. "
"Include only facts stated below: where the player went, what they found, "
"what was said. Add nothing new and do not solve any mystery.\n\n"
f"Previous summary: {game['summary'] or '(none)'}\n\n"
f"New events:\n{transcript}"
)
r = ollama.chat(
model=model,
messages=[{"role": "user", "content": prompt}],
think=False,
options={"temperature": 0.2, "num_predict": 200, "num_ctx": 8192},
)
summary = r.message.content.strip()
if summary:
game["summary"] = summary
game["summary_idx"] += len(fold)
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\engine.py",
"view_range": [
915,
1078
]
}
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
@@ -915,164 +915,164 @@
not game["sister_seen"]
or game["turn_count"] + 1 - game["last_sister_turn"] >= SISTER_COOLDOWN
)
private_sister_due = (
game["stage"] >= 3
and sister_appearance_due
and not game["ending"]
and game["sister_warning_count"] >= 2
and not game["sister_bargain_heard"]
and game["friend_room"] != game["room"]
)
speaking_with_sister = (
(game["sister_present"] or private_sister_due)
and intent in {"dialogue", "action_and_dialogue"}
and not game["ending"]
and game["friend_room"] != game["room"]
)
seen_before = len(game["examined"]) # measured BEFORE resolving, so first looks count as fresh
result = _resolve(game, text)
if result["outcome"] == "moved":
intent = "movement"
game["sister_present"] = False
elif result["outcome"] == "picked_up":
intent = "physical_action"
elif result["outcome"] == "examined" and intent not in {
"examination",
"exploration",
}:
intent = "examination"
result["intent"] = intent
if (
intent in {"dialogue", "action_and_dialogue"}
and not game["ending"]
and game["friend_room"] != game["room"]
and not speaking_with_sister
):
game["friend_room"] = game["room"]
result["friend_arrived"] = True
result["detail"] += (
f" The friend joins the player in the {game['room']} so they can speak "
"face to face."
)
game["turn_count"] += 1
game["turns_in_stage"] += 1
if result["outcome"] != "moved":
game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["support_given"] < SUPPORT_LIMIT
and SUPPORT_RE.search(text)
):
game["support_given"] += 1
game["denial"] = max(0, game["denial"] - 1)
result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
if (
not game["ending"]
and game["turn_count"] >= MAX_TURNS
and (not game["truth_known"] or game["denial"] >= 3)
):
result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
if fresh:
game["last_notice_turn"] = game["turn_count"]
if (
not fresh
and result["outcome"] in ("no_change", "examined")
and not game["ending"]
and not game["truth_known"]
and game["room_turns"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_nudge_turn"] >= NUDGE_COOLDOWN
and _next_unnoticed(game, game["room"]) is None
):
nudge = _nudge(game)
if nudge:
result["nudge"] = nudge
game["last_nudge_turn"] = game["turn_count"]
game["nudges_here"] += 1
if result["new_clue"]:
for _ in result["new_clue"].split("; "):
game["denial"] = max(0, game["denial"] - 1)
if (
game["truth_known"]
and game["denial"] < 3
and game["friend_room"] == game["room"]
and intent in {"dialogue", "action_and_dialogue"}
and result["outcome"] not in {"blocked", "ending"}
and len(game["final_revelations"]) < len(FINAL_REVELATIONS)
):
revelation = FINAL_REVELATIONS[len(game["final_revelations"])]
game["final_revelations"].append(revelation["id"])
result["family_memory"] = revelation
result["stage_advanced"] = _advance_stage(game)
sister_due = (
not game["sister_seen"]
or game["turn_count"] - game["last_sister_turn"] >= SISTER_COOLDOWN
)
if game["stage"] >= 3 and sister_due and not game["ending"]:
can_speak_privately = (
game["sister_warning_count"] >= 2
and not game["sister_bargain_heard"]
and game["friend_room"] != game["room"]
)
if can_speak_privately:
appearance = (
"The sister appears again, but this time she does not repeat her "
"warning. With the friend elsewhere, she looks directly at the player "
"and seems willing to explain why she wants the family left undisturbed."
)
game["sister_bargain_available"] = True
game["sister_present"] = True
game["sister_conversation_turns"] = 0
else:
appearance = (
"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."
if not game["sister_seen"]
else "The friend's sister appears briefly again and quietly repeats her "
"warning to leave. Then she is gone."
)
game["sister_warning_count"] = min(2, game["sister_warning_count"] + 1)
game["sister_present"] = False
game["sister_seen"] = True
game["last_sister_turn"] = game["turn_count"]
result["sister"] = appearance
result["sister_private_encounter"] = can_speak_privately
if speaking_with_sister and result["outcome"] not in {"ending", "blocked"}:
result["sister_conversation"] = True
if SISTER_PERSPECTIVE_RE.search(raw_action.lower()):
game["sister_bargain_heard"] = True
result["sister_perspective"] = SISTER_PRESERVATION_MEMORY
game["sister_conversation_turns"] += 1
if game["sister_conversation_turns"] >= SISTER_CONVERSATION_LIMIT:
game["sister_present"] = False
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
100,
150
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -100,51 +100,51 @@
letter_path = Path(__file__).resolve().parent / "assets" / "letter.png"
show_image(letter_path, width="stretch")
@st.dialog("The Book", width="small")
def show_book_dialog():
book_path = Path(__file__).resolve().parent / "assets" / "book.png"
show_image(book_path, width="stretch")
st.markdown(
'**"The Fall of the House of Usher" by Edgar Allan Poe is a short story '
"first published in 1839. A narrator visits his ailing childhood friend "
"Roderick Usher at his decaying family mansion. There he encounters "
"Roderick's mysterious twin sister Madeline, who suffers from a strange "
"illness. As the atmosphere grows increasingly oppressive, the house "
"itself seems alive with malevolent energy. When Madeline is entombed "
"within the estate, terrifying sounds begin echoing through the halls, "
"building toward a shocking climax that binds the fate of the twins to "
'their ancestral home."**'
)
@st.dialog("The Note", width="small")
def show_note_dialog():
note_path = Path(__file__).resolve().parent / "assets" / "note.png"
show_image(note_path, width="stretch")
def play_background_music(file_path, loop=True):
audio_path = Path(file_path)
if not audio_path.is_absolute():
audio_path = Path(__file__).resolve().parent / audio_path
if not audio_path.exists():
st.caption(f"(audio not found: {audio_path.name})")
return
audio_base64 = base64.b64encode(audio_path.read_bytes()).decode("ascii")
loop_attr = "loop" if loop else ""
st.markdown(
f"""
<audio controls {loop_attr} style="width: 100%;">
<source
src="data:audio/mpeg;base64,{audio_base64}"
type="audio/mpeg"
>
</audio>
""",
unsafe_allow_html=True,
)
st.markdown("""
The game state is behaving correctly: on turn 3, the engine surfaced the musty smell and recorded “A musty smell lingers in the air” as a clue. That clue and its description are passed to the model in the engine note.
The likely issue is narration, not clue detection. The prompt asks the model to weave ambient details in quietly, but also to answer the player directly. The smaller gemma4:e2b model followed the conversation and dropped the secondary detail. The unsupported “nearby, dark chair” is another sign it’s improvising beyond the room facts.
The Game state: dump is separate: engine.py prints it to the terminal for debugging. It isn’t part of the player-facing response.
So the clue is saved and should count for progress even though it wasn’t narrated. To make narration more reliable with e2b, we could strengthen the instruction to explicitly include each newly discovered clue—but the model may still occasionally miss one.
Hmm... would it help to make the context bigger?
Probably not for this case. The game already sets the context window to 8,192 tokens in generation_options, and this happened within the first few turns, so there’s no sign the conversation exceeded it.
A larger context helps when the model is losing earlier story details in a long conversation. It generally doesn’t make a smaller model more reliable about following a specific instruction—and a bigger context can use more memory and potentially add latency. Here, gemma4:e2b seems to have prioritized answering your question over including the newly surfaced clue.
A more targeted fix would be to make the prompt say plainly: when the engine reports a new clue or ambient detail, include it in the reply even if the player asked something else. We could also show discoveries separately in the UI so the player doesn’t depend on the narrator mentioning every one.
We can try, but I don't want to change too many things, if it doesn't work, I will revert it back to gemma4:e4b
I’ll make one narrow prompt change: require the narrator to mention any new clue or ambient detail the engine supplies, while still answering the player’s message. I’ll add a small prompt regression test and leave the model and context settings untouched.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
1,
40
]
}
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
@@ -1,40 +1,40 @@
"""prompt.py - builds what the model sees each turn.
Layers (top to bottom):
1. CORE static narrator rules (cacheable prefix)
2. story block premise, friend sheet, letter (static per story)
3. stage block what may be revealed *now* (changes with stage)
4. state block room, exits, inventory, clues (changes per turn)
5. history last N messages (older -> summary)
6. engine result + reminder, attached to the player's latest message
"""
import re
import ollama
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Before the bedroom truth is faced, use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. Once the player has entered the bedroom and learned the truth, do not initiate small talk or pivot to ordinary topics.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT normally brings him into the player's room; narrate that arrival before his reply. The exception is an ENGINE RESULT-marked private sister conversation: answer as the sister and keep the friend elsewhere.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech.
- The sister's appearances stay focused on her warning that the player should leave. Do not have her suggest rooms, activities, or casual conversation. During an ENGINE RESULT-authorized private conversation, she may explain her belief that preserving the family keeps them alive and discuss her bargain, but keep the exchange focused on that belief and warning. This is her perspective, not objective proof that the family is alive, and does not reveal the full truth. These sister-specific limits override the general suggestion and small-talk guidance.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good."
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
245,
285
]
}
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
@@ -245,41 +245,41 @@
def _engine_note(game):
r = game["last_result"]
if not r:
return ""
note = (
"\n\n[ENGINE RESULT - authoritative]\n"
f"Player intent: {r.get('intent', 'roleplay')}\n"
f"Outcome: {r['outcome']}\n"
f"Detail: {r['detail']}"
)
if r.get("ambient"):
note += (
"\nAmbient detail to weave into this turn in one quiet sentence, "
f"without making it a big moment: {r['ambient']}"
)
if r.get("nudge"):
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']}"
friend_is_here = game.get("friend_room", "Entrance Hall") == 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']}"
if r.get("sister_private_encounter"):
note += (
"\nThis is the sister's private conversation opportunity, available because "
"she has already warned the player twice and the friend is elsewhere. "
"The player may speak with her even if the bedroom door is still closed."
)
if r.get("sister_conversation"):
note += (
"\nThe player is speaking privately with the sister. Respond as the sister, "
"not the friend. The friend is elsewhere and must not answer or arrive."
)
if r.get("sister_departed"):
note += (
"\nAfter this reply, the sister vanishes. Her bargain remains an available "
"choice if she has explained her perspective; the player may also move or leave normally."
{"pattern":"_engine_note|ambient detail|New clue found|ENGINE RESULT","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\tests\\test_engine.py"],"output_mode":"c...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:9:from prompt import CORE, _engine_note, stage_block, state_block, story_block
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:85: self.assertIn("joined the player", _engine_note(game))
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:293: self.assertNotIn("Friend's likely reaction", _engine_note(game))
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:408: self.assertIn(expected["detail"], _engine_note(game))
*** Begin Patch
*** Update File: the-house-that-remembers/prompt.py
@@
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or noth...
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
@@ -28,6 +28,7 @@
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion.
+- If the ENGINE RESULT includes a new clue or ambient detail, explicitly include that discovery in the reply this turn, even while answering the player's dialogue. Weave it in briefly and naturally; do not omit it or replace it with an unrelated observation.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT normally brings him into the player's room; narrate that arrival before his reply. The exception is an ENGINE RESULT-marked private sister conversation: answer as the sister and keep the friend elsewhere.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
@@ -263,6 +264,11 @@
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("ambient") or r.get("new_clue"):
+ note += (
+ "\nMANDATORY NARRATION: Explicitly mention the engine-provided new clue "
+ "or ambient detail in this reply, even if the player asked a question."
+ )
friend_is_here = game.get("friend_room", "Entrance Hall") == game["room"]
if r.get("friend") and friend_is_here:
note += f"\nFriend's likely reaction (optional): {r['friend']}"
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
@@ -55,6 +55,21 @@
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertEqual(game["support_given"], 1)
+ def test_engine_note_requires_new_clue_to_be_narrated_during_dialogue(self):
+ game = engine.new_game()
+ game["last_result"] = {
+ "intent": "dialogue",
+ "outcome": "no_change",
+ "detail": "Nothing else changed.",
+ "ambient": "A musty smell lingers in the air.",
+ "new_clue": "A musty smell lingers in the air",
+ }
+
+ note = _engine_note(game)
+
+ self.assertIn("MANDATORY NARRATION", note)
+ self.assertIn("A musty smell lingers in the air", note)
+
def test_routes_to_non_adjacent_room_and_keeps_friend_in_place(self):
game = engine.new_game()
game["room"] = "Library"
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run targeted clue na...
..........................
----------------------------------------------------------------------
Ran 26 tests in 0.009s
OK
<shellId: 184 completed with exit code 0>
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers'; python -m py_compile prompt.py tests\\test_engine.py","description":"Validate prompt and test...
<shellId: 186 completed with exit code 0>
{"command":"Set-Location 'C:\\Users\\lucia\\Documents\\coding stuff\\hacktober'; git --no-pager diff --check -- the-house-that-remembers/prompt.py the-house-that-remembers/tests/test_engine.py","descr...
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
<shellId: 185 completed with exit code 0>
Made one targeted prompt change: when the engine supplies a new clue or ambient detail, the narrator is now explicitly required to mention it in that turn—even while responding to dialogue. The model and context settings are unchanged.
All 26 regression tests pass, including a test that checks this instruction is present for a turn with a surfaced clue. Syntax and diff checks are clean. See prompt.py and test_engine.py.
🕯️
"You came. I didn't think you would... Thank you."
🧑
"I was worried about you"
🕯️
He shifts his weight, his gaze fixed on you. "It's strange being back in the library. We used to spend whole afternoons in here."
Yeah no
That’s exactly the kind of non sequitur the smaller model can pick up from the example lines. I’ll tighten the prompt so childhood memories only come up when they fit the current room and moment, and add a regression check for the entrance-hall case.
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\the-house-that-remembers\\prompt.py",
"view_range": [
15,
65
]
}
{"pattern":"FRIEND_EXAMPLES|Established player memories|library|childhood","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\the-house-that-remembers\\prompt.py...
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\the-house-that-remembers\\tests\\test_engine.py",
"view_range": [
1,
25
]
}
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\the-house-that-remembers\\prompt.py",
"view_range": [
210,
245
]
}
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
15,
65
]
}
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
@@ -15,51 +15,51 @@
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Before the bedroom truth is faced, use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. Once the player has entered the bedroom and learned the truth, do not initiate small talk or pivot to ordinary topics.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion.
- If the ENGINE RESULT includes a new clue or ambient detail, explicitly include that discovery in the reply this turn, even while answering the player's dialogue. Weave it in briefly and naturally; do not omit it or replace it with an unrelated observation.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT normally brings him into the player's room; narrate that arrival before his reply. The exception is an ENGINE RESULT-marked private sister conversation: answer as the sister and keep the friend elsewhere.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech.
- The sister's appearances stay focused on her warning that the player should leave. Do not have her suggest rooms, activities, or casual conversation. During an ENGINE RESULT-authorized private conversation, she may explain her belief that preserving the family keeps them alive and discuss her bargain, but keep the exchange focused on that belief and warning. This is her perspective, not objective proof that the family is alive, and does not reveal the full truth. These sister-specific limits override the general suggestion and small-talk guidance.
- Do not introduce a warning, apparition, clue or change of location unless the STORY NOW section or the ENGINE RESULT calls for it."""
FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good."
"Do you remember how we used to help in the kitchen? I was never much good at it."
"It's strange being back in the library. We used to spend whole afternoons in here."
"I haven't been sleeping much. The house is loud at night."
"Tea sounds good. The kitchen's just this way, if you'd like."
"I miss the old days. Now, everything is so... different. I don't know how to explain it."
"I just wanted to see you. I haven't had anyone to talk to in a long time."
"I am glad you came. I don't know how to say it, but I need someone here. I can't be alone."
"I know we haven't seen each other in years, but it's good to have you here."
"What about your... family?"
"How was the trip here? I hope the rain didn't make it too difficult."
"We used to spend a lot of time in the library"
"What have you been doing all these years? I mean... if you want to tell me.\""""
STAGES = [
{ # 0 - Arrival and uneasy normality
"now": "The friend is unwell, glad the player came, and wants help but cannot explain. The house is unsettling but nothing is understood yet. Friend invited the player to the house because he is lonely and the history of the house is weighing on him. He is not aware of the truth, but he doesn't want to be alone.",
"friend": "He tries to act normal and spend ordinary time together. He can ask about the player's life or mention an established childhood memory. He deflects direct questions about the house and does not bring up family members on his own.",
"steer": "Let the player settle in and explore. Keep the friend's suggestions tied to what the player just said; if tea or a drink is wanted, the kitchen is the natural next step. Do not offer upstairs or list destinations without a reason. When the friend invites the player somewhere, make the destination clear and wait for the player's choice; do not move them automatically. Uncanny details come only from the engine (ambient details and examined results). Weave those in; do not invent uncanny details of your own.",
},
{ # 1 - Contradictions
"now": "Details about the friend's family do not quite add up.",
"friend": "He mentions his family casually, in the present tense, as if they are alive and nearby: his mother is resting, his father is busy upstairs, his sister is 'around'. He does not notice anything odd. If asked to see them, or asked directly about them, he answers vaguely and changes the subject.",
"steer": "Let this come up once or twice in passing, e.g. \"Mum's resting. She hasn't been sleeping well.\" Do not explain it.",
{"pattern":"FRIEND_EXAMPLES|Established player memories|library|childhood","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py","C:\\Users\\lucia\\Docum...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:35:- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:41:FRIEND_EXAMPLES = """ "You came. I... sorry. You came. Good."
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:43: "It's strange being back in the library. We used to spend whole afternoons in here."
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:52: "We used to spend a lot of time in the library"
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:59: "friend": "He tries to act normal and spend ordinary time together. He can ask about the player's life or mention an established childhood memory. He deflects direct questions about the house and does not bring up family members on his own.",
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:84: "friend": "His denial breaks only as he becomes ready. He speaks in halting fragments and quiet realization, not speeches. If denial is still 3 or higher, he resists and cannot share the next family memory. When denial falls below 3, the ENGINE RESULT may supply one authored family memory; include it honestly and clearly, without adding facts. After the player has entered and learned the truth, keep his conversation focused on the family, his grief, the player's immediate response, and whether to stay or leave. Do not initiate small talk, ask what the player has been doing, bring up unrelated childhood memories, or pivot to ordinary topics. He does not blame the player. He never agrees to leave together unless the engine ends the story.",
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:138: f"{FRIEND_EXAMPLES}\n\n"
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:231: "Established player memories: childhood companions; playing outside together; "
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:232: "helping the friend with kitchen chores; exploring the family library; "
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\tests\\test_engine.py",
"view_range": [
1,
25
]
}
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
@@ -1,25 +1,25 @@
import contextlib
import io
import json
import unittest
from collections import Counter
from pathlib import Path
import engine
from prompt import CORE, _engine_note, stage_block, 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_supportive_phrases_increase_support(self):
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
210,
245
]
}
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
@@ -210,36 +210,36 @@
friend_room = game.get("friend_room", "Entrance Hall")
lines = [
"CURRENT STATE",
f"Room: {game['room']}",
f"Room description: {room['description'].strip()}",
f"Truth faced: {'yes' if game.get('truth_known') else 'no'}",
f"Friend's denial: {game['denial']}/10",
f"Family memories shared: {len(game.get('final_revelations', []))}/4",
f"Shared family memories: {', '.join(game.get('final_revelations', [])) or 'none'}",
f"Sister's private conversation available: {'yes' if game.get('sister_bargain_available') else 'no'}",
f"Sister's preservation belief heard: {'yes' if game.get('sister_bargain_heard') else 'no'}",
f"Sister physically present: {'yes' if game.get('sister_present') else 'no'}",
f"Sister conversation turns: {game.get('sister_conversation_turns', 0)}/5",
f"Exits: {', '.join(exit_labels(game, game['room']))}",
(
"Friend location: with the player in this room."
if friend_room == game["room"]
else f"Friend location: {friend_room}. He is not with the player and cannot hear them."
),
f"Inventory: {', '.join(game['inventory']) or 'empty'}",
f"Clues the player has found: {', '.join(game['clues']) or 'none'}",
"Established player memories: childhood companions; playing outside together; "
"helping the friend with kitchen chores; exploring the family library; "
"sleeping over in the guest room.",
]
if game["summary"]:
lines.append(f"Earlier in the story: {game['summary']}")
shared_revelations = set(game.get("final_revelations", []))
lines.extend(
f"Family memory already shared ({memory['title']}): {memory['detail']}"
for memory in FINAL_REVELATIONS
if memory["id"] in shared_revelations
)
if game.get("sister_bargain_heard"):
lines.append(f"Sister's revealed perspective: {SISTER_PRESERVATION_MEMORY}")
return "\n".join(lines)
no
Got it—no changes. I jumped to an implementation before checking what you meant.
That library line is explicitly in the prompt’s example dialogue, so Gemma likely repeated it without respecting the current room. I’ll leave it as is unless you ask me to change something.
I will revert back to gemma4:e4b
That makes sense. Since the weird dialogue appeared after switching to gemma4:e2b, going back to gemma4:e4b is a reasonable way to restore the previous behavior. No code changes made.
{
"pattern": "gemma4:e[24]b|gemma4|model\\s*=",
"paths": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers",
"output_mode": "content",
"-n": true,
"head_limit": 80
}
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:404: model=model,
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:38:MODEL_NAME = "gemma4:e2b"
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:70: model=MODEL_NAME,
{"command":"git status --short; git --no-pager diff -- the-house-that-remembers/prompt.py the-house-that-remembers/.env the-house-that-remembers/config.toml","description":"Check worktree changes and ...
M start.sh
M the-house-that-remembers/app.py
M the-house-that-remembers/prompt.py
warning: in the working copy of 'the-house-that-remembers/prompt.py', LF will be replaced by CRLF the next time Git touches it
diff --git a/the-house-that-remembers/prompt.py b/the-house-that-remembers/prompt.py
index c8f8bcd..d0c7b72 100644
--- a/the-house-that-remembers/prompt.py
+++ b/the-house-that-remembers/prompt.py
@@ -28,6 +28,7 @@ Friend conversation:
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion.
+- If the ENGINE RESULT includes a new clue or ambient detail, explicitly include that discovery in the reply this turn, even while answering the player's dialogue. Weave it in briefly and naturally; do not omit it or replace it with an unrelated observation.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT normally brings him into the player's room; narrate that arrival before his reply. The exception is an ENGINE RESULT-marked private sister conversation: answer as the sister and keep the friend elsewhere.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
@@ -263,6 +264,11 @@ def _engine_note(game):
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("ambient") or r.get("new_clue"):
+ note += (
+ "\nMANDATORY NARRATION: Explicitly mention the engine-provided new clue "
+ "or ambient detail in this reply, even if the player asked a question."
+ )
friend_is_here = game.get("friend_room", "Entrance Hall") == game["room"]
if r.get("friend") and friend_is_here:
note += f"\nFriend's likely reaction (optional): {r['friend']}"
<shellId: 187 completed with exit code 0>
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
28,
46
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -28,19 +28,19 @@
"uicons-solid-straight.css'>",
unsafe_allow_html=True,
)
st.markdown(
"<link rel='stylesheet' "
"href='https://cdn-uicons.flaticon.com/uicons-bold-straight/css/"
"uicons-bold-straight.css'>",
unsafe_allow_html=True,
)
MODEL_NAME = "gemma4:e2b"
CANON_PATH = Path(__file__).resolve().parent / "story" / "canon.json"
CANON = json.loads(CANON_PATH.read_text(encoding="utf-8"))
INTRO = '''
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."_
*** Begin Patch
*** Update File: the-house-that-remembers/app.py
@@
-MODEL_NAME = "gemma4:e2b"
+MODEL_NAME = "gemma4:e4b"
*** End Patch
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -35,7 +35,7 @@
unsafe_allow_html=True,
)
-MODEL_NAME = "gemma4:e2b"
+MODEL_NAME = "gemma4:e4b"
CANON_PATH = Path(__file__).resolve().parent / "story" / "canon.json"
CANON = json.loads(CANON_PATH.read_text(encoding="utf-8"))
{
"pattern": "MODEL_NAME\\s*=|gemma4:e[24]b",
"paths": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers",
"output_mode": "content",
"-n": true,
"head_limit": 20
}
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:38:MODEL_NAME = "gemma4:e4b"
{
"command": "git diff --check -- the-house-that-remembers/app.py; git --no-pager diff -- the-house-that-remembers/app.py",
"description": "Verify the model setting change"
}
warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it
<shellId: 188 completed with exit code 0>
{"pattern":"ollama|chat\\(|MODEL_NAME|OPENROUTER|api_key|generation_options|streamlit","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers"],"glob":"*.py","output_...
[grep content: 21 matches across 2 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
app.py (18 match(es)):
7: import ollama
8: import streamlit as st
22: from prompt import build_messages, generation_options, maybe_summarize, tidy
38: MODEL_NAME = "gemma4:e4b"
69: result = ollama.chat(
70: model=MODEL_NAME,
73: options=generation_options(game),
464: st.session_state.pop("ollama_error", None)
539: if "ollama_error" in st.session_state:
540: st.error(st.session_state.ollama_error)
557: maybe_summarize(game, MODEL_NAME)
559: except ollama.ResponseError as error:
562: f"Ollama could not find {MODEL_NAME}. "
563: f"Run `ollama pull {MODEL_NAME}` and try again."
567: except ollama.RequestError as error:
578: st.session_state.ollama_error = error_message
580: st.session_state.pop("ollama_error", None)
585: st.caption(f"Powered by Ollama · {MODEL_NAME}")
prompt.py (3 match(es)):
13: import ollama
356: def generation_options(game):
397: r = ollama.chat(
{"pattern":"ollama|streamlit|requirements|python|render|Docker","paths":["C:\\Users\\lucia\\Documents\\coding stuff\\hacktober"],"glob":"{Dockerfile,*.yaml,*.yml,*.toml,*.txt,*.md,*.sh,*.json}","outpu...
C:\Users\lucia\Documents\coding stuff\hacktober\Dockerfile:2:FROM ollama/ollama:0.35.1
C:\Users\lucia\Documents\coding stuff\hacktober\Dockerfile:5: python3 python3-pip python3-venv curl\
C:\Users\lucia\Documents\coding stuff\hacktober\Dockerfile:10:COPY requirements.txt .
C:\Users\lucia\Documents\coding stuff\hacktober\Dockerfile:11:RUN python3 -m venv /opt/venv \
C:\Users\lucia\Documents\coding stuff\hacktober\Dockerfile:12: && /opt/venv/bin/pip install --no-cache-dir -r requirements.txt \
C:\Users\lucia\Documents\coding stuff\hacktober\requirements.txt:1:ollama
C:\Users\lucia\Documents\coding stuff\hacktober\requirements.txt:2:streamlit
C:\Users\lucia\Documents\coding stuff\hacktober\README.md:62:ollama pull gemma4:e4b
C:\Users\lucia\Documents\coding stuff\hacktober\README.md:70:python -m pip install -r requirements.txt
C:\Users\lucia\Documents\coding stuff\hacktober\README.md:76:python -m streamlit run .\the-house-that-remembers\app.py
C:\Users\lucia\Documents\coding stuff\hacktober\start.sh:5:ollama serve &
C:\Users\lucia\Documents\coding stuff\hacktober\start.sh:11:ollama pull gemma4:e4b
C:\Users\lucia\Documents\coding stuff\hacktober\start.sh:13:exec streamlit run /app/the-house-that-remembers/app.py \
{
"command": "Get-ChildItem -Force | Select-Object Name,Mode; Get-ChildItem -Force .\\the-house-that-remembers | Select-Object Name,Mode",
"description": "Inspect repository and app configuration files"
}
Name Mode
---- ----
.git d--h--
the-house-that-remembers d-----
.gitignore -a----
Dockerfile -a----
LICENSE -a----
README.md -a----
requirements.txt -a----
start.sh -a----
assets d-----
story d-----
temp d-----
tests d-----
__pycache__ d-----
app.py -a----
engine.py -a----
prompt.py -a----
<shellId: 189 completed with exit code 0>
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
1,
85
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -1,85 +1,85 @@
import base64
import copy
import json
from io import BytesIO
from pathlib import Path
import ollama
import streamlit as st
from PIL import Image
from engine import (
FINAL_REVELATIONS,
FRAGMENT_LABELS,
FRAGMENT_KINDS,
FRAGMENT_CLUES_NEEDED,
ROOMS,
apply_action,
fragment_progress,
fragments_complete,
new_game,
)
from prompt import build_messages, generation_options, maybe_summarize, tidy
st.set_page_config(page_title="The House That Remembers", layout="wide")
st.markdown(
"<link rel='stylesheet' "
"href='https://cdn-uicons.flaticon.com/uicons-solid-straight/css/"
"uicons-solid-straight.css'>",
unsafe_allow_html=True,
)
st.markdown(
"<link rel='stylesheet' "
"href='https://cdn-uicons.flaticon.com/uicons-bold-straight/css/"
"uicons-bold-straight.css'>",
unsafe_allow_html=True,
)
MODEL_NAME = "gemma4:e4b"
CANON_PATH = Path(__file__).resolve().parent / "story" / "canon.json"
CANON = json.loads(CANON_PATH.read_text(encoding="utf-8"))
INTRO = '''
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?_
'''
def generate_response(game):
result = ollama.chat(
model=MODEL_NAME,
messages=build_messages(game, CANON),
think=False,
options=generation_options(game),
)
return tidy(result.message.content)
def show_image(path, **kwargs):
"""Show an image, or a quiet caption if the file is missing (instead of crashing the app)."""
if Path(path).exists():
st.image(path, **kwargs)
else:
st.caption(f"(image not found: {Path(path).name})")
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\app.py",
"view_range": [
525,
590
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -525,66 +525,66 @@
st.markdown("### What will you do?")
with st.container(key="conversation_frame"):
with st.container(height=480, border=True, autoscroll=True):
for message in game["history"]:
with st.chat_message(
message["role"],
avatar="🕯️" if message["role"] == "assistant" else "🧑",
):
st.markdown(message["content"])
if game["ending"]:
st.markdown("---")
st.markdown("**THE END**")
if "ollama_error" in st.session_state:
st.error(st.session_state.ollama_error)
action = st.chat_input(
"Describe what you do...",
disabled=bool(game["ending"]),
)
if action:
snapshot = copy.deepcopy(game) # restore this if the model call fails
game["history"].append({"role": "user", "content": action})
apply_action(game, action) # Python decides what actually happened
error_message = None
response = ""
try:
with thinking_placeholder.container():
with st.spinner("Thinking..."):
maybe_summarize(game, MODEL_NAME)
response = generate_response(game)
except ollama.ResponseError as error:
if error.status_code == 404:
error_message = (
f"Ollama could not find {MODEL_NAME}. "
f"Run `ollama pull {MODEL_NAME}` and try again."
)
else:
error_message = f"Ollama returned an error: {error.error}"
except ollama.RequestError as error:
error_message = (
"Could not connect to Ollama. Make sure Ollama is running, "
f"then try again. Details: {error}"
)
else:
if not response:
error_message = "Ollama returned an empty response. Please try again."
if error_message:
st.session_state.game = snapshot # undo the failed turn completely
st.session_state.ollama_error = error_message
else:
st.session_state.pop("ollama_error", None)
game["history"].append({"role": "assistant", "content": response})
st.rerun()
st.caption(f"Powered by Ollama · {MODEL_NAME}")
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\the-house-that-remembers\\prompt.py",
"view_range": [
345,
415
]
}
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
@@ -345,71 +345,71 @@
return note + f"\n[REMINDER] {reminder}"
def build_messages(game, canon):
system = "\n\n".join([CORE, story_block(canon), stage_block(game), state_block(game)])
live = [dict(m) for m in game["history"][game["summary_idx"]:]]
if live and live[-1]["role"] == "user":
live[-1]["content"] += _engine_note(game)
return [{"role": "system", "content": system}, *live]
def generation_options(game):
return {
"temperature": 0.6,
"num_ctx": 8192,
"num_predict": 320 if game["ending"] else 200,
"repeat_penalty": 1.1,
}
def tidy(text):
"""Strip leaked tags and trim a reply cut off mid-sentence."""
text = re.split(r"\[(?:ENGINE|REMINDER)", text)[0].strip()
if text and text[-1] not in '.!?"\u201d*_\u2026':
cut = max(text.rfind(c) for c in ".!?")
if cut > len(text) * 0.4:
text = text[: cut + 1]
return text
SUMMARY_TRIGGER = 20 # live messages before we fold
KEEP_RECENT = 10 # messages kept verbatim after folding
def maybe_summarize(game, model):
live = game["history"][game["summary_idx"]:]
if len(live) <= SUMMARY_TRIGGER:
return
fold = live[:-KEEP_RECENT]
transcript = "\n".join(
f"{'Player' if m['role'] == 'user' else 'Narrator'}: {m['content']}" for m in fold
)
prompt = (
"Summarize the story so far in 3-4 sentences, past tense. "
"Include only facts stated below: where the player went, what they found, "
"what was said. Add nothing new and do not solve any mystery.\n\n"
f"Previous summary: {game['summary'] or '(none)'}\n\n"
f"New events:\n{transcript}"
)
r = ollama.chat(
model=model,
messages=[{"role": "user", "content": prompt}],
think=False,
options={"temperature": 0.2, "num_predict": 200, "num_ctx": 8192},
)
summary = r.message.content.strip()
if summary:
game["summary"] = summary
game["summary_idx"] += len(fold)
{
"path": "C:\\Users\\lucia\\Documents\\coding stuff\\hacktober\\README.md",
"view_range": [
50,
100
]
}
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
@@ -50,51 +50,51 @@
- 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.
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.
### Run the Streamlit app
Install Ollama and download the model before starting the app:
``` powershell
ollama pull gemma4:e4b
Make sure the Ollama service is running. Then, from the repository root, install the project dependencies: (optional: create and activate a virtual environment)
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. { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\Dockerfile", "view_range": [ 1, 40 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/Dockerfile b/C:/Users/lucia/Documents/coding stuff/hacktober/Dockerfile index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/Dockerfile +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/Dockerfile @@ -1,40 +1,40 @@
FROM ollama/ollama:0.35.1
RUN apt-get update && apt-get install -y --no-install-recommends
python3 python3-pip python3-venv curl
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN python3 -m venv /opt/venv
&& /opt/venv/bin/pip install --no-cache-dir -r requirements.txt
&& /opt/venv/bin/pip install --no-cache-dir Pillow
COPY the-house-that-remembers/ ./the-house-that-remembers/ COPY start.sh ./start.sh RUN chmod +x /app/start.sh
ENV PATH="/opt/venv/bin:$PATH"
OLLAMA_HOST=127.0.0.1:11434
OLLAMA_KEEP_ALIVE=-1
OLLAMA_CONTEXT_LENGTH=4096
PORT=10000
EXPOSE 10000
ENTRYPOINT ["/bin/sh", "/app/start.sh"] { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\start.sh", "view_range": [ 1, 30 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh b/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh @@ -1,30 +1,30 @@
#!/bin/sh set -e
ollama serve &
until curl -sf http://127.0.0.1:11434/api/version >/dev/null; do sleep 1 done
ollama pull gemma4:e4b
exec streamlit run /app/the-house-that-remembers/app.py
--server.address 0.0.0.0
--server.port "$PORT"
--server.headless true
{
"path": "C:\Users\lucia\Documents\coding stuff\hacktober\requirements.txt"
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/requirements.txt b/C:/Users/lucia/Documents/coding stuff/hacktober/requirements.txt
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/requirements.txt
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/requirements.txt
@@ -1,3 +1,3 @@
ollama
streamlit
Pillow
{
"path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py",
"view_range": [
1,
35
]
}
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
@@ -1,35 +1,35 @@
"""prompt.py - builds what the model sees each turn.
Layers (top to bottom):
- CORE static narrator rules (cacheable prefix)
- story block premise, friend sheet, letter (static per story)
- stage block what may be revealed now (changes with stage)
- state block room, exits, inventory, clues (changes per turn)
- history last N messages (older -> summary)
- engine result + reminder, attached to the player's latest message """ import re
import ollama
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares.
- Respond directly to what the player does. Never decide the player's actions, feelings or choices for them.
- Keep dialogue natural and brief. Avoid melodrama, stacked metaphors, repeated descriptions and exposition.
- Quiet, ordinary moments are fine. Not every turn needs a clue or a scare.
Friend conversation:
- Keep conversational continuity at every story stage: respond to the player's latest statement or request, and follow through on an offer the player accepts before introducing another topic.
- Before the bedroom truth is faced, use gentle small talk or ask about the player's life during natural s, not over an unanswered request or immediate action. Once the player has entered the bedroom and learned the truth, do not initiate small talk or pivot to ordinary topics.
- Draw on established shared memories when relevant. Vary topics and avoid repeating the same offer or line.
- Make suggestions purposeful and responsive to the conversation, not a menu of rooms or activities. If tea, a drink, or something warm comes up, naturally suggest the kitchen; do not suggest going upstairs unless the player or story gives a reason.
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT normally brings him into the player's room; narrate that arrival before his reply. The exception is an ENGINE RESULT-marked private sister conversation: answer as the sister and keep the friend elsewhere.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family.
- When an ENGINE RESULT reveals readable text or a historical clue, state its important content plainly. The library's account of an earlier owner is separate from the friend's family history and is not a forbidden reveal.
- Never explain the supernatural neatly. Before the bedroom truth is faced, characters may answer questions about the past in fragments, contradict themselves, or change the subject. After the player faces the truth, the friend's response depends on his denial: if he is still resisting, he may minimize or struggle to answer; as his denial falls, he should acknowledge the established facts honestly in brief, halting fragments rather than a speech. { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", "view_range": [ 1, 120 ] } 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 @@ -1,120 +1,120 @@ import contextlib import io import json import unittest from collections import Counter from pathlib import Path
import engine from prompt import CORE, _engine_note, stage_block, 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_supportive_phrases_increase_support(self): phrases = [ "We can do it together.", "We can face this together.", "It's going to be okay.", "It is going to be alright.", "I want to help.", "I can help.", "Take my hand.", "Give me your hand.", ]
for phrase in phrases:
with self.subTest(phrase=phrase):
game = engine.new_game()
act(game, phrase)
self.assertEqual(game["support_given"], 1)
self.assertEqual(game["denial"], 7)
def test_supportive_dialogue_brings_remote_friend_to_player(self):
game = engine.new_game()
game["room"] = "Upper Landing"
game["friend_room"] = "Library"
result = act(game, "I can help.")
self.assertEqual(result["intent"], "dialogue")
self.assertTrue(result["friend_arrived"])
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertEqual(game["support_given"], 1)
def test_engine_note_requires_new_clue_to_be_narrated_during_dialogue(self):
game = engine.new_game()
game["last_result"] = {
"intent": "dialogue",
"outcome": "no_change",
"detail": "Nothing else changed.",
"ambient": "A musty smell lingers in the air.",
"new_clue": "A musty smell lingers in the air",
}
note = _engine_note(game)
self.assertIn("MANDATORY NARRATION", note)
self.assertIn("A musty smell lingers in the air", note)
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_joins_player_for_conversation_without_keyword_matching(self):
game = engine.new_game()
game["room"] = "Upper Landing"
game["friend_room"] = "Library"
result = act(game, "Can I sit?")
self.assertEqual(result["intent"], "dialogue")
self.assertEqual(game["room"], "Upper Landing")
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertTrue(result["friend_arrived"])
self.assertIn("face to face", result["detail"])
self.assertIn("joined the player", _engine_note(game))
def test_room_name_in_carried_note_does_not_trigger_movement(self):
game = engine.new_game()
game["room"] = "Library"
game["friend_room"] = "Library"
game["inventory"].append("Upper Landing Note")
result = act(
game,
'I stand up, go closer and look into his face. '
'"What is in that room?" I show him the upper landing note I took',
)
self.assertEqual(game["room"], "Library")
self.assertEqual(game["friend_room"], "Library")
self.assertNotEqual(result["outcome"], "moved")
self.assertEqual(result["intent"], "action_and_dialogue")
def test_examining_takeable_features_adds_items_to_inventory(self):
game = engine.new_game()
{"pattern":"maybe_summarize|generate_response|MODEL_NAME|ollama_error|ollama\.chat|generation_options","paths":"C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers","glob":...
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:356:def generation_options(game):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:382:def maybe_summarize(game, model):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:397: r = ollama.chat(
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:22:from prompt import build_messages, generation_options, maybe_summarize, tidy
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:38:MODEL_NAME = "gemma4:e4b"
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:68:def generate_response(game):
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:69: result = ollama.chat(
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:70: model=MODEL_NAME,
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:73: options=generation_options(game),
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:464: st.session_state.pop("ollama_error", None)
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:539: if "ollama_error" in st.session_state:
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:540: st.error(st.session_state.ollama_error)
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:557: maybe_summarize(game, MODEL_NAME)
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:558: response = generate_response(game)
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:562: f"Ollama could not find {MODEL_NAME}. "
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:563: f"Run ollama pull {MODEL_NAME} and try again."
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:578: st.session_state.ollama_error = error_message
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:580: st.session_state.pop("ollama_error", None)
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:585:st.caption(f"Powered by Ollama · {MODEL_NAME}")
{
"command": "git status --short; Get-Content .gitignore",
"description": "Check existing edits and ignored secrets"
}
.venv/
pycache/
*.py[cod]
.streamlit/secrets.toml
temp/
tests/
<shellId: 190 completed with exit code 0>
{
"path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py",
"view_range": [
548,
590
]
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py
@@ -548,43 +548,43 @@
snapshot = copy.deepcopy(game) # restore this if the model call fails
game["history"].append({"role": "user", "content": action})
apply_action(game, action) # Python decides what actually happened
error_message = None
response = ""
try:
with thinking_placeholder.container():
with st.spinner("Thinking..."):
maybe_summarize(game, MODEL_NAME)
response = generate_response(game)
except ollama.ResponseError as error:
if error.status_code == 404:
error_message = (
f"Ollama could not find {MODEL_NAME}. "
f"Run `ollama pull {MODEL_NAME}` and try again."
)
else:
error_message = f"Ollama returned an error: {error.error}"
except ollama.RequestError as error:
error_message = (
"Could not connect to Ollama. Make sure Ollama is running, "
f"then try again. Details: {error}"
)
else:
if not response:
error_message = "Ollama returned an empty response. Please try again."
if error_message:
st.session_state.game = snapshot # undo the failed turn completely
st.session_state.ollama_error = error_message
else:
st.session_state.pop("ollama_error", None)
game["history"].append({"role": "assistant", "content": response})
st.rerun()
st.caption(f"Powered by Ollama · {MODEL_NAME}") { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\README.md", "view_range": [ 1, 115 ] } 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 @@ -1,115 +1,115 @@

The House That Remembers #
This is my submission for Hacktoberfest Open-Source AI Challenge: Week 4. This Week's Theme: Ghost in the Machine - Build something spooky, or something that honors the past in the spirit of Día de los Muertos, with open-weight models or open-source AI.
Because I love the spooky season, DnD (and reading crazy thrillers 😅), I decided to built a short DnD-style game. I call it The House That Remembers, inspired by The Fall of the House of Usher written by Edgar Allan Poe in 1839. There are very big spoilers in the /story folder. If you want to enjoy the game, I recommend not checking the story files.
Tech stack:
- Streamlit - app.py: UI, chat interface
- Python - for logic, validates actions, updated history, clues, inventory
- Ollama + Gemma - narrates the permitted outcome
Trigger warnings: it is a horror story! There is no gore, but there are mentions of death and similar themes.
Premise
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 fear it is too late for me. You are the only one I can trust."
Now, you stand before his ancestral 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. Something about the place feels wrong. 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?

Tips for interacting with the story
Describe your actions, lot of things are allowed! Think about your goal and what you are hoping to achieve. There is no right or wrong way to play this game. This games is based on TTRPG-style of games, where you are role-playing a character. The model serves as "DM" (Dungeon Master) - the narrator. He is the storyteller, the referee, maintains the setting in which the game is happening, and plays roles of "NPCs" (non-player characters).
The narrator distinguishes spoken dialogue, physical actions, exploration, and questions about the player's memories. You can make the intent clear in natural language:
- Speak to your friend:
What can I do for you?orI say, "Let's go." - Combine speech and an action:
I pat him on the back and say, "Let's go." - Explore or examine:
Look aroundorExamine the family photos. - Recall an established memory:
What do I remember about my friend? - 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.
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.
Run the Streamlit app
Install Ollama and download the model before starting the app:
ollama pull gemma4:e4b
Make sure the Ollama service is running. Then, from the repository root, install the project dependencies: (optional: create and activate a virtual environment)
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. {"command":"git ls-files -- the-house-that-remembers/tests/test_engine.py requirements.txt Dockerfile start.sh README.md the-house-that-remembers/app.py the-house-that-remembers/prompt.py; python -c ... Dockerfile README.md requirements.txt start.sh the-house-that-remembers/app.py the-house-that-remembers/prompt.py Traceback (most recent call last): File "<string>", line 1, in <module> import openai; print(openai.version) ^^^^^^^^^^^^^ ModuleNotFoundError: No module named 'openai' <shellId: 191 completed with exit code 1> python *** Begin Patch *** Add File: the-house-that-remembers/llm.py +import json +import os +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + +import ollama + + +O... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/llm.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/llm.py create file mode 100644 index 0000000..0000000 --- a/dev/null +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/llm.py @@ -1,0 +1,90 @@ +import json +import os +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + +import ollama + + +OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" +DEFAULT_OPENROUTER_MODEL = "google/gemma-4-26b-a4b-it:free" +DEFAULT_OLLAMA_MODEL = "gemma4:e4b" + + +class ModelConfigurationError(Exception):
- """Raised when the selected model provider is not configured correctly."""
+class OpenRouterError(Exception):
- def init(self, message, status_code=None):
-
super().__init__(message) -
self.status_code = status_code
+def provider_name():
- provider = os.getenv("MODEL_PROVIDER", "ollama").strip().lower()
- if provider not in {"ollama", "openrouter"}:
-
raise ModelConfigurationError( -
"MODEL_PROVIDER must be either 'ollama' or 'openrouter'." -
) - return provider
+def model_name():
- if provider_name() == "openrouter":
-
return os.getenv("OPENROUTER_MODEL", DEFAULT_OPENROUTER_MODEL).strip() - return os.getenv("OLLAMA_MODEL", DEFAULT_OLLAMA_MODEL).strip()
+def chat(messages, options):
- provider = provider_name()
- if provider == "ollama":
-
result = ollama.chat( -
model=model_name(), -
messages=messages, -
think=False, -
options=options, -
) -
return result.message.content - api_key = os.getenv("OPENROUTER_API_KEY")
- if not api_key:
-
raise ModelConfigurationError( -
"OPENROUTER_API_KEY is required when MODEL_PROVIDER is 'openrouter'." -
) - payload = {
-
"model": model_name(), -
"messages": messages, -
"temperature": options.get("temperature", 0.6), -
"max_tokens": options.get("num_predict", 200), - }
- request = Request(
-
OPENROUTER_URL, -
data=json.dumps(payload).encode("utf-8"), -
headers={ -
"Authorization": f"******", -
"Content-Type": "application/json", -
"X-Title": "The House That Remembers", -
}, -
method="POST", - )
- try:
-
with urlopen(request, timeout=120) as response: -
result = json.loads(response.read().decode("utf-8")) - except HTTPError as error:
-
body = error.read().decode("utf-8", errors="replace") -
try: -
body = json.loads(body).get("error", {}).get("message", body) -
except json.JSONDecodeError: -
pass -
raise OpenRouterError(str(body), error.code) from error - except (TimeoutError, URLError) as error:
-
raise OpenRouterError(f"Could not connect to OpenRouter: {error}") from error - try:
-
return result["choices"][0]["message"]["content"] - except (KeyError, IndexError, TypeError) as error:
-
raise OpenRouterError("OpenRouter returned an unexpected response.") from error
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -4,10 +4,16 @@ from io import BytesIO from pathlib import Path
-import ollama import streamlit as st from PIL import Image
+from llm import (
- ModelConfigurationError,
- OpenRouterError,
- chat,
- model_name,
- provider_name, +) from engine import ( FINAL_REVELATIONS, FRAGMENT_LABELS, @@ -35,7 +41,6 @@ unsafe_allow_html=True, )
-MODEL_NAME = "gemma4:e4b" CANON_PATH = Path(file).resolve().parent / "story" / "canon.json" CANON = json.loads(CANON_PATH.read_text(encoding="utf-8"))
@@ -66,13 +71,7 @@
def generate_response(game):
- result = ollama.chat(
-
model=MODEL_NAME, -
messages=build_messages(game, CANON), -
think=False, -
options=generation_options(game), - )
- return tidy(result.message.content)
- return tidy(chat(build_messages(game, CANON), generation_options(game)))
def show_image(path, **kwargs): @@ -536,8 +535,8 @@ st.markdown("---") st.markdown("THE END")
-
if "ollama_error" in st.session_state: -
st.error(st.session_state.ollama_error)
-
if "model_error" in st.session_state: -
st.error(st.session_state.model_error)action = st.chat_input( "Describe what you do...", @@ -554,13 +553,13 @@ try: with thinking_placeholder.conta
[Truncated — full output (11.1 KB) temporarily saved to C:\Users\lucia\AppData\Local\Temp\1793244215097-copilot-tool-output-9b62f92987f343458f7dafac4253de61.txt] *** Begin Patch *** Update File: the-house-that-remembers/app.py @@ from pathlib import Path
+import ollama import streamlit as st @@ if not response:
-
error_message = f"{prov...
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -4,6 +4,7 @@ from io import BytesIO from pathlib import Path
+import ollama import streamlit as st from PIL import Image
@@ -579,7 +580,9 @@ ) else: if not response:
-
error_message = f"{provider_name().title()} returned an empty response. Please try again."
-
error_message = ( -
f"{provider_name().title()} returned an empty response. Please try again." -
)if error_message: st.session_state.game = snapshot # undo the failed turn completely
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/llm.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/llm.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/llm.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/llm.py @@ -47,7 +47,7 @@ ) return result.message.content
- api_key = os.getenv("OPENROUTER_API_KEY")
-
api_key = os.getenv("OPENROUTER_API_KEY", "").strip() if not api_key: raise ModelConfigurationError( "OPENROUTER_API_KEY is required when MODEL_PROVIDER is 'openrouter'." @@ -72,11 +72,18 @@
try: with urlopen(request, timeout=120) as response:
-
result = json.loads(response.read().decode("utf-8"))
-
try: -
result = json.loads(response.read().decode("utf-8")) -
except json.JSONDecodeError as error: -
except HTTPError as error: body = error.read().decode("utf-8", errors="replace") try:raise OpenRouterError("OpenRouter returned invalid JSON.") from error
-
body = json.loads(body).get("error", {}).get("message", body)
-
error_detail = json.loads(body).get("error") -
if isinstance(error_detail, dict): -
body = error_detail.get("message", body) -
elif isinstance(error_detail, str): -
body = error_detail except json.JSONDecodeError: pass raise OpenRouterError(str(body), error.code) from error
{ "command": "Set-Location .\the-house-that-remembers; python -m unittest discover -s tests -p 'test_engine.py'", "description": "Run the focused game engine tests", "initial_wait": 120 } FAIL: test_engine_note_requires_new_clue_to_be_narrated_during_dialogue (test_engine.GameEngineTests.test_engine_note_requires_new_clue_to_be_narrated_during_dialogue) #
Traceback (most recent call last): File "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", line 70, in test_engine_note_requires_new_clue_to_be_narrated_during_dialogue self.assertIn("MANDATORY NARRATION", note) ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ AssertionError: 'MANDATORY NARRATION' not found in "\n\n[ENGINE RESULT - authoritative]\nPlayer intent: dialogue\nOutcome: no_change\nDetail: Nothing else changed.\nAmbient detail to weave into this turn in one quiet sentence, without making it a big moment: A musty smell lingers in the air.\nNew clue found: A musty smell lingers in the air\n[REMINDER] Treat the player's words as spoken to the friend and answer directly in his natural, restrained voice. If the ENGINE RESULT says he joined the player, narrate his arrival before the reply. Stay in character. 2-5 short sentences. Add no other items, clues, uncanny events, room changes or backstory."
Ran 26 tests in 0.010s
FAILED (failures=1) <shellId: 192 completed with exit code 1> {"command":"python -m py_compile .\the-house-that-remembers\app.py .\the-house-that-remembers\prompt.py .\the-house-that-remembers\llm.py; if ($?) { git diff --check }","description":"Check Pyth... warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'start.sh', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it <shellId: 193 completed with exit code 0> python {"command":"@'\nimport json\nimport os\nfrom unittest.mock import patch\n\nimport llm\n\nclass FakeResponse:\n def enter(self):\n return self\n def exit(self, *args):\n ret... Traceback (most recent call last): File "<stdin>", line 5, in <module> ModuleNotFoundError: No module named 'llm' <shellId: 195 completed with exit code 1> {"command":"git --no-pager diff -- Dockerfile start.sh README.md the-house-that-remembers/app.py the-house-that-remembers/prompt.py; git status --short","description":"Review provider and deployment c... warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'start.sh', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it diff --git a/Dockerfile b/Dockerfile index 5213aaa..a01867e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,25 +1,16 @@
-FROM ollama/ollama:0.35.1 #
-RUN apt-get update && apt-get install -y --no-install-recommends \
- python3 python3-pip python3-venv curl\
- && rm -rf /var/lib/apt/lists/* +FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt . -RUN python3 -m venv /opt/venv \
- && /opt/venv/bin/pip install --no-cache-dir -r requirements.txt \
- && /opt/venv/bin/pip install --no-cache-dir Pillow +RUN pip install --no-cache-dir -r requirements.txt
COPY the-house-that-remembers/ ./the-house-that-remembers/ COPY start.sh ./start.sh RUN chmod +x /app/start.sh
-ENV PATH="/opt/venv/bin:$PATH" \
- OLLAMA_HOST=127.0.0.1:11434 \
- OLLAMA_KEEP_ALIVE=-1 \
- OLLAMA_CONTEXT_LENGTH=4096
+ENV MODEL_PROVIDER=openrouter
PORT=10000
EXPOSE 10000 diff --git a/README.md b/README.md index 90e7d2d..2af09f9 100644 --- a/README.md +++ b/README.md @@ -54,9 +54,9 @@ The sidebar tracks the Past, Present, and Echoes fragments needed to finish the atmospheric and optional lore discoveries that do not count toward fragment progress.
-### Run the Streamlit app +### Run the Streamlit app locally
-Install Ollama and download the model before starting the app: +By default, the app uses Ollama locally. Install Ollama and download the model:
ollama pull gemma4:e4b
@@ -77,3 +77,17 @@ 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.
+
+### Deploy with Render and OpenRouter
+
+The included Dockerfile is configured for OpenRouter and runs the Streamlit app
+as a Render web service. In Render, set these environment variables:
+
+- MODEL_PROVIDER: openrouter
+- OPENROUTER_API_KEY: your OpenRouter API key
+- OPENROUTER_MODEL: google/gemma-4-26b-a4b-it:free (optional; this is the default)
+
+Keep the API key in Render's environment settings. Do not commit it to the
+repository. To return to local Ollama, unset MODEL_PROVIDER or set it to
+ollama; OLLAMA_MODEL can optionally override the local default
+gemma4:e4b.
diff --git a/start.sh b/start.sh
index ff133d0..bbe491a 100644
--- a/start.sh
+++ b/start.sh
@@ -2,14 +2,6 @@
#!/bin/sh
set -e
-ollama serve & #
-until curl -sf http://127.0.0.1:11434/api/version >/dev/null; do
- sleep 1 -done
-ollama pull gemma4:e4b #
exec streamlit run /app/the-house-that-remembers/app.py
--server.address 0.0.0.0
--server.port "$PORT"
diff --git a/the-house-that-remembers/app.py b/the-house-that-remembers/app.py
index 37f1447..41a6f10 100644
--- a/the-house-that-remembers/app.py
+++ b/the-house-that-remembers/app.py
@@ -8,6 +8,13 @@ import ollama
import streamlit as st
from PIL import Image
+from llm import (
- ModelConfigurationError,
- OpenRouterError,
- chat,
- model_name,
- provider_name, +) from engine import ( FINAL_REVELATIONS, FRAGMENT_LABELS, @@ -35,7 +42,6 @@ st.markdown( unsafe_allow_html=True, )
-MODEL_NAME = "gemma4:e4b" CANON_PATH = Path(file).resolve().parent / "story" / "canon.json" CANON = json.loads(CANON_PATH.read_text(encoding="utf-8"))
@@ -66,13 +72,7 @@ Did he invite you into the house… or did the house invite you in?
def generate_response(game):
- result = ollama.chat(
-
model=MODEL_NAME, -
messages=build_messages(game, CANON), -
think=False, -
options=generation_options(game), - )
- return tidy(result.message.content)
- return tidy(chat(build_messages(game, CANON), generation_options(game)))
def show_image(path, **kwargs): @@ -536,8 +536,8 @@ with conversation_column: st.markdown("---") st.markdown("THE END")
-
if "ollama_error" in st.session_state: -
st.error(st.session_state.ollama_error)
-
if "model_error" in st.session_state: -
st.error(st.session_state.model_error)action = st.chat_input( "Describe what you do...", @@ -554,13 +554,13 @@ if action: try: with thinking_placeholder.container(): with st.spinner("Thinking..."):
-
maybe_summarize(game, MODEL_NAME)
-
except ollama.ResponseError as error: if error.status_code == 404: error_message = (maybe_summarize(game) response = generate_response(game)
-
f"Ollama could not find {MODEL_NAME}. " -
f"Run `ollama pull {MODEL_NAME}` and try again."
-
f"Ollama could not find {model_name()}. " -
f"Run `ollama pull {model_name()}` and try again." ) else: error_message = f"Ollama returned an error: {error.error}"
@@ -569,17 +569,28 @@ if action: "Could not connect to Ollama. Make sure Ollama is running, " f"then try again. Details: {error}" )
- except ModelConfigurationError as error:
-
error_message = str(error) - except OpenRouterError as error:
-
if error.status_code is None: -
error_message = str(error) -
else: -
error_message = ( -
f"OpenRouter returned an error (HTTP {error.status_code}): {error}" -
else: if not response:)
-
error_message = "Ollama returned an empty response. Please try again."
-
error_message = ( -
f"{provider_name().title()} returned an empty response. Please try again." -
)if error_message: st.session_state.game = snapshot # undo the failed turn completely
-
st.session_state.ollama_error = error_message
-
else:st.session_state.model_error = error_message
-
st.session_state.pop("ollama_error", None)
-
st.session_state.pop("model_error", None) game["history"].append({"role": "assistant", "content": response})st.rerun()
-st.caption(f"Powered by Ollama · {MODEL_NAME}") \ No newline at end of file +st.caption(f"Powered by {provider_name().title()} · {model_name()}") \ No newline at end of file diff --git a/the-house-that-remembers/prompt.py b/the-house-that-remembers/prompt.py index c8f8bcd..8bc74cc 100644 --- a/the-house-that-remembers/prompt.py +++ b/the-house-that-remembers/prompt.py @@ -10,7 +10,7 @@ Layers (top to bottom): """ import re
-import ollama +from llm import chat
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
@@ -379,7 +379,7 @@ SUMMARY_TRIGGER = 20 # live messages before we fold KEEP_RECENT = 10 # messages kept verbatim after folding
-def maybe_summarize(game, model): +def maybe_summarize(game): live = game["history"][game["summary_idx"]:] if len(live) <= SUMMARY_TRIGGER: return @@ -394,13 +394,11 @@ def maybe_summarize(game, model): f"Previous summary: {game['summary'] or '(none)'}\n\n" f"New events:\n{transcript}" )
- r = ollama.chat(
-
model=model, -
messages=[{"role": "user", "content": prompt}], -
think=False, -
options={"temperature": 0.2, "num_predict": 200, "num_ctx": 8192},
- summary = chat(
-
[{"role": "user", "content": prompt}], -
){"temperature": 0.2, "num_predict": 200, "num_ctx": 8192},
- summary = r.message.content.strip()
- summary = summary.strip() if summary: game["summary"] = summary game["summary_idx"] += len(fold) \ No newline at end of file M Dockerfile M README.md M start.sh M the-house-that-remembers/app.py M the-house-that-remembers/prompt.py ?? the-house-that-remembers/llm.py <shellId: 194 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 245, 290 ] } 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 @@ -245,46 +245,46 @@
def engine_note(game): r = game["last_result"] if not r: return "" note = ( "\n\n[ENGINE RESULT - authoritative]\n" f"Player intent: {r.get('intent', 'roleplay')}\n" f"Outcome: {r['outcome']}\n" f"Detail: {r['detail']}" ) if r.get("ambient"): note += ( "\nAmbient detail to weave into this turn in one quiet sentence, " f"without making it a big moment: {r['ambient']}" ) if r.get("nudge"): 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']}" friend_is_here = game.get("friend_room", "Entrance Hall") == 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']}" if r.get("sister_private_encounter"): note += ( "\nThis is the sister's private conversation opportunity, available because " "she has already warned the player twice and the friend is elsewhere. " "The player may speak with her even if the bedroom door is still closed." ) if r.get("sister_conversation"): note += ( "\nThe player is speaking privately with the sister. Respond as the sister, " "not the friend. The friend is elsewhere and must not answer or arrive." ) if r.get("sister_departed"): note += ( "\nAfter this reply, the sister vanishes. Her bargain remains an available " "choice if she has explained her perspective; the player may also move or leave normally." ) if r.get("sister_perspective"): note += ( "\nThe sister's perspective to explain clearly if the player asked: " f"{r['sister_perspective']}" {"command":"git --no-pager diff -- the-house-that-remembers/prompt.py; git --no-pager diff -- the-house-that-remembers/app.py; git status --short; Get-Content .\the-house-that-remembers\tests\test... Output too large to read at once (8.1 KB). Saved to: C:\Users\lucia\AppData\Local\Temp\1793244257452-copilot-tool-output-8656-4a0515bc-92f0-4a53-b36a-b9a569d47881.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): diff --git a/the-house-that-remembers/prompt.py b/the-house-that-remembers/prompt.py index c8f8bcd..8bc74cc 100644 --- a/the-house-that-remembers/prompt.py +++ b/the-house-that-remembers/prompt.py @@ -10,7 +10,7 @@ Layers (top to bottom): """ import re
-import ollama +from llm import chat
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
@@ -379,7 +379,7 @@ SUMMARY_TRIGGER = 20 # live messages before we fold KEEP_RECENT = 10 # mess <shellId: 196 completed with exit code 0> *** Begin Patch *** Update File: the-house-that-remembers/prompt.py @@ Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action ... 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 @@ -28,6 +28,7 @@
Truth rules:
- The ENGINE RESULT attached to the player's message is authoritative. Narrate exactly that. If it says an action failed or nothing changed, narrate that. Do not add items, clues, opened doors or room changes. If it supplies a family memory, include that fact clearly in the friend's reply; do not replace it with a vague evasion. +- If the ENGINE RESULT includes a new clue or ambient detail, explicitly include that discovery in the reply this turn, even while answering the player's dialogue. Weave it in briefly and naturally; do not omit it or replace it with an unrelated observation.
- A character may suggest going somewhere, but the move only happens if the player chooses it.
- The friend's location in CURRENT STATE is authoritative. Never have him hear or answer from another room. If the player starts a conversation while they are apart, the ENGINE RESULT normally brings him into the player's room; narrate that arrival before his reply. The exception is an ENGINE RESULT-marked private sister conversation: answer as the sister and keep the friend elsewhere.
- Only mention shared memories or family details that appear in this prompt or earlier in the conversation. Never invent family history, backstory, or what happened to the family. @@ -263,6 +264,11 @@ 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("ambient") or r.get("new_clue"):
-
note += ( -
"\nMANDATORY NARRATION: Explicitly mention the engine-provided new clue " -
"or ambient detail in this reply, even if the player asked a question." -
friend_is_here = game.get("friend_room", "Entrance Hall") == game["room"] if r.get("friend") and friend_is_here: note += f"\nFriend's likely reaction (optional): {r['friend']}" { "command": "Set-Location .\the-house-that-remembers; python -m unittest discover -s tests -p 'test_engine.py'", "description": "Run the focused game engine tests", "initial_wait": 120 } ..........................)
Ran 26 tests in 0.010s
OK <shellId: 197 completed with exit code 0> {"command":"Set-Location .\the-house-that-remembers; @'\nimport json\nimport os\nfrom unittest.mock import patch\n\nimport llm\n\nclass FakeResponse:\n def enter(self):\n return self\n ... Provider smoke tests passed <shellId: 198 completed with exit code 0> {"command":"python -m py_compile .\the-house-that-remembers\app.py .\the-house-that-remembers\prompt.py .\the-house-that-remembers\llm.py; if ($?) { git diff --check }; git status --short","desc... warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'start.sh', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it M Dockerfile M README.md M start.sh M the-house-that-remembers/app.py M the-house-that-remembers/prompt.py ?? the-house-that-remembers/llm.py <shellId: 199 completed with exit code 0> *** Begin Patch *** Update File: the-house-that-remembers/llm.py @@ def model_name(): if provider_name() == "openrouter":
-
return os.getenv("OPENROUTER_MODEL", DEFAULT_OPENROUTER_MODEL)....
diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/llm.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/llm.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/llm.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/llm.py @@ -32,8 +32,14 @@
def model_name(): if provider_name() == "openrouter":
-
return os.getenv("OPENROUTER_MODEL", DEFAULT_OPENROUTER_MODEL).strip() - return os.getenv("OLLAMA_MODEL", DEFAULT_OLLAMA_MODEL).strip()
-
name = os.getenv("OPENROUTER_MODEL", DEFAULT_OPENROUTER_MODEL).strip() -
if not name: -
raise ModelConfigurationError("OPENROUTER_MODEL cannot be empty.") -
return name - name = os.getenv("OLLAMA_MODEL", DEFAULT_OLLAMA_MODEL).strip()
- if not name:
-
raise ModelConfigurationError("OLLAMA_MODEL cannot be empty.") - return name
def chat(messages, options): @@ -79,11 +85,13 @@ except HTTPError as error: body = error.read().decode("utf-8", errors="replace") try:
-
error_detail = json.loads(body).get("error") -
if isinstance(error_detail, dict): -
body = error_detail.get("message", body) -
elif isinstance(error_detail, str): -
body = error_detail
-
error_response = json.loads(body) -
if isinstance(error_response, dict): -
error_detail = error_response.get("error") -
if isinstance(error_detail, dict): -
body = error_detail.get("message", body) -
elif isinstance(error_detail, str): -
body = error_detail except json.JSONDecodeError: pass raise OpenRouterError(str(body), error.code) from error
@@ -91,7 +99,10 @@ raise OpenRouterError(f"Could not connect to OpenRouter: {error}") from error
try:
-
return result["choices"][0]["message"]["content"]
-
except (KeyError, IndexError, TypeError) as error: raise OpenRouterError("OpenRouter returned an unexpected response.") from errorcontent = result["choices"][0]["message"]["content"] - if not isinstance(content, str):
-
raise OpenRouterError("OpenRouter did not return a text response.") - return content {"command":"Set-Location .\the-house-that-remembers; python -m unittest discover -s tests -p 'test_engine.py'; python -m py_compile .\app.py .\prompt.py .\llm.py","description":"Run game tests and... ..........................
Ran 26 tests in 0.010s
OK <shellId: 200 completed with exit code 0> {"command":"Set-Location .\the-house-that-remembers; @'\nimport json\nimport os\nfrom unittest.mock import patch\n\nimport llm\n\nclass FakeResponse:\n def enter(self):\n return self\n ... Provider smoke tests passed <shellId: 202 completed with exit code 0> {"command":"git diff --check; if (Get-Command sh -ErrorAction SilentlyContinue) { sh -n .\start.sh }; git status --short; git --no-pager diff --stat","description":"Check deployment script and review... warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'start.sh', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it M Dockerfile M README.md M start.sh M the-house-that-remembers/app.py M the-house-that-remembers/prompt.py ?? the-house-that-remembers/llm.py warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'start.sh', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it Dockerfile | 15 +++---------- README.md | 18 +++++++++++++-- start.sh | 8 ------- the-house-that-remembers/app.py | 45 ++++++++++++++++++++++++-------------- the-house-that-remembers/prompt.py | 20 ++++++++++------- 5 files changed, 59 insertions(+), 47 deletions(-) <shellId: 201 completed with exit code 0> {"command":"git --no-pager diff -- start.sh the-house-that-remembers/app.py the-house-that-remembers/prompt.py; git --no-pager diff -- Dockerfile README.md","description":"Separate OpenRouter changes ... <shellId: 203 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\start.sh" } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh b/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh @@ -1,8 +1,8 @@
#!/bin/sh set -e
exec streamlit run /app/the-house-that-remembers/app.py
--server.address 0.0.0.0
--server.port "$PORT"
--server.headless true
{
"path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\llm.py"
}
diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/llm.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/llm.py
index 0000000..0000000 100644
--- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/llm.py
+++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/llm.py
@@ -1,108 +1,108 @@
import json
import os
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
import ollama
OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" DEFAULT_OPENROUTER_MODEL = "google/gemma-4-26b-a4b-it:free" DEFAULT_OLLAMA_MODEL = "gemma4:e4b"
class ModelConfigurationError(Exception): """Raised when the selected model provider is not configured correctly."""
class OpenRouterError(Exception): def init(self, message, status_code=None): super().init(message) self.status_code = status_code
def provider_name(): provider = os.getenv("MODEL_PROVIDER", "ollama").strip().lower() if provider not in {"ollama", "openrouter"}: raise ModelConfigurationError( "MODEL_PROVIDER must be either 'ollama' or 'openrouter'." ) return provider
def model_name(): if provider_name() == "openrouter": name = os.getenv("OPENROUTER_MODEL", DEFAULT_OPENROUTER_MODEL).strip() if not name: raise ModelConfigurationError("OPENROUTER_MODEL cannot be empty.") return name name = os.getenv("OLLAMA_MODEL", DEFAULT_OLLAMA_MODEL).strip() if not name: raise ModelConfigurationError("OLLAMA_MODEL cannot be empty.") return name
def chat(messages, options): provider = provider_name() if provider == "ollama": result = ollama.chat( model=model_name(), messages=messages, think=False, options=options, ) return result.message.content
api_key = os.getenv("OPENROUTER_API_KEY", "").strip()
if not api_key:
raise ModelConfigurationError(
"OPENROUTER_API_KEY is required when MODEL_PROVIDER is 'openrouter'."
)
payload = {
"model": model_name(),
"messages": messages,
"temperature": options.get("temperature", 0.6),
"max_tokens": options.get("num_predict", 200),
}
request = Request(
OPENROUTER_URL,
data=json.dumps(payload).encode("utf-8"),
headers={
"Authorization": f"******",
"Content-Type": "application/json",
"X-Title": "The House That Remembers",
},
method="POST",
)
try:
with urlopen(request, timeout=120) as response:
try:
result = json.loads(response.read().decode("utf-8"))
except json.JSONDecodeError as error:
raise OpenRouterError("OpenRouter returned invalid JSON.") from error
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
try:
error_response = json.loads(body)
if isinstance(error_response, dict):
error_detail = error_response.get("error")
if isinstance(error_detail, dict):
body = error_detail.get("message", body)
elif isinstance(error_detail, str):
body = error_detail
except json.JSONDecodeError:
pass
raise OpenRouterError(str(body), error.code) from error
except (TimeoutError, URLError) as error:
raise OpenRouterError(f"Could not connect to OpenRouter: {error}") from error
try:
content = result["choices"][0]["message"]["content"]
except (KeyError, IndexError, TypeError) as error:
raise OpenRouterError("OpenRouter returned an unexpected response.") from error
if not isinstance(content, str):
raise OpenRouterError("OpenRouter did not return a text response.")
return content
{"command":"git status --short; git --no-pager diff --cached -- start.sh the-house-that-remembers/app.py the-house-that-remembers/prompt.py Dockerfile README.md; git --no-pager diff HEAD -- start.sh t... <shellId: 204 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 1, 82 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -1,82 +1,82 @@ import base64 import copy import json from io import BytesIO from pathlib import Path
import ollama import streamlit as st from PIL import Image
from llm import ( ModelConfigurationError, OpenRouterError, chat, model_name, provider_name, ) from engine import ( FINAL_REVELATIONS, FRAGMENT_LABELS, FRAGMENT_KINDS, FRAGMENT_CLUES_NEEDED, ROOMS, apply_action, fragment_progress, fragments_complete, new_game, ) from prompt import build_messages, generation_options, maybe_summarize, tidy
st.set_page_config(page_title="The House That Remembers", layout="wide") st.markdown( "<link rel='stylesheet' " "href='https://cdn-uicons.flaticon.com/uicons-solid-straight/css/" "uicons-solid-straight.css'>", unsafe_allow_html=True, ) st.markdown( "<link rel='stylesheet' " "href='https://cdn-uicons.flaticon.com/uicons-bold-straight/css/" "uicons-bold-straight.css'>", unsafe_allow_html=True, )
CANON_PATH = Path(file).resolve().parent / "story" / "canon.json" CANON = json.loads(CANON_PATH.read_text(encoding="utf-8"))
INTRO = ''' 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? '''
def generate_response(game): return tidy(chat(build_messages(game, CANON), generation_options(game)))
def show_image(path, **kwargs): """Show an image, or a quiet caption if the file is missing (instead of crashing the app).""" if Path(path).exists(): st.image(path, **kwargs) else: { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 532, 600 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -532,69 +532,69 @@ ): st.markdown(message["content"])
if game["ending"]:
st.markdown("---")
st.markdown("**THE END**")
if "model_error" in st.session_state:
st.error(st.session_state.model_error)
action = st.chat_input(
"Describe what you do...",
disabled=bool(game["ending"]),
)
if action: snapshot = copy.deepcopy(game) # restore this if the model call fails game["history"].append({"role": "user", "content": action}) apply_action(game, action) # Python decides what actually happened
error_message = None
response = ""
try:
with thinking_placeholder.container():
with st.spinner("Thinking..."):
maybe_summarize(game)
response = generate_response(game)
except ollama.ResponseError as error:
if error.status_code == 404:
error_message = (
f"Ollama could not find {model_name()}. "
f"Run `ollama pull {model_name()}` and try again."
)
else:
error_message = f"Ollama returned an error: {error.error}"
except ollama.RequestError as error:
error_message = (
"Could not connect to Ollama. Make sure Ollama is running, "
f"then try again. Details: {error}"
)
except ModelConfigurationError as error:
error_message = str(error)
except OpenRouterError as error:
if error.status_code is None:
error_message = str(error)
else:
error_message = (
f"OpenRouter returned an error (HTTP {error.status_code}): {error}"
)
else:
if not response:
error_message = (
f"{provider_name().title()} returned an empty response. Please try again."
)
if error_message:
st.session_state.game = snapshot # undo the failed turn completely
st.session_state.model_error = error_message
else:
st.session_state.pop("model_error", None)
game["history"].append({"role": "assistant", "content": response})
st.rerun()
st.caption(f"Powered by {provider_name().title()} · {model_name()}") { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 1, 18 ] } 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 @@ -1,18 +1,18 @@ """prompt.py - builds what the model sees each turn.
Layers (top to bottom):
- CORE static narrator rules (cacheable prefix)
- story block premise, friend sheet, letter (static per story)
- stage block what may be revealed now (changes with stage)
- state block room, exits, inventory, clues (changes per turn)
- history last N messages (older -> summary)
- engine result + reminder, attached to the player's latest message """ import re
from llm import chat
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares. { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 350, 420 ] } 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 @@ -350,71 +350,71 @@ reminder = "This is the final scene. 4-6 sentences. Close the story." return note + f"\n[REMINDER] {reminder}"
def build_messages(game, canon): system = "\n\n".join([CORE, story_block(canon), stage_block(game), state_block(game)]) live = [dict(m) for m in game["history"][game["summary_idx"]:]] if live and live[-1]["role"] == "user": live[-1]["content"] += _engine_note(game) return [{"role": "system", "content": system}, *live]
def generation_options(game): return { "temperature": 0.6, "num_ctx": 8192, "num_predict": 320 if game["ending"] else 200, "repeat_penalty": 1.1, }
def tidy(text): """Strip leaked tags and trim a reply cut off mid-sentence.""" text = re.split(r"[(?:ENGINE|REMINDER)", text)[0].strip() if text and text[-1] not in '.!?"\u201d*_\u2026': cut = max(text.rfind(c) for c in ".!?") if cut > len(text) * 0.4: text = text[: cut + 1] return text
SUMMARY_TRIGGER = 20 # live messages before we fold KEEP_RECENT = 10 # messages kept verbatim after folding
def maybe_summarize(game): live = game["history"][game["summary_idx"]:] if len(live) <= SUMMARY_TRIGGER: return fold = live[:-KEEP_RECENT] transcript = "\n".join( f"{'Player' if m['role'] == 'user' else 'Narrator'}: {m['content']}" for m in fold ) prompt = ( "Summarize the story so far in 3-4 sentences, past tense. " "Include only facts stated below: where the player went, what they found, " "what was said. Add nothing new and do not solve any mystery.\n\n" f"Previous summary: {game['summary'] or '(none)'}\n\n" f"New events:\n{transcript}" ) summary = chat( [{"role": "user", "content": prompt}], {"temperature": 0.2, "num_predict": 200, "num_ctx": 8192}, ) summary = summary.strip() if summary: game["summary"] = summary game["summary_idx"] += len(fold) python *** Begin Patch *** Update File: the-house-that-remembers/app.py @@ import ollama import streamlit as st from PIL import Image
-from llm import (
- ModelConfigurationError,
- OpenRouterErro... { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 1, 82 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -1,82 +1,82 @@ import base64 import copy import json from io import BytesIO from pathlib import Path
import ollama import streamlit as st from PIL import Image
from llm import ( ModelConfigurationError, OpenRouterError, chat, model_name, provider_name, ) from engine import ( FINAL_REVELATIONS, FRAGMENT_LABELS, FRAGMENT_KINDS, FRAGMENT_CLUES_NEEDED, ROOMS, apply_action, fragment_progress, fragments_complete, new_game, ) from prompt import build_messages, generation_options, maybe_summarize, tidy
st.set_page_config(page_title="The House That Remembers", layout="wide") st.markdown( "<link rel='stylesheet' " "href='https://cdn-uicons.flaticon.com/uicons-solid-straight/css/" "uicons-solid-straight.css'>", unsafe_allow_html=True, ) st.markdown( "<link rel='stylesheet' " "href='https://cdn-uicons.flaticon.com/uicons-bold-straight/css/" "uicons-bold-straight.css'>", unsafe_allow_html=True, )
CANON_PATH = Path(file).resolve().parent / "story" / "canon.json" CANON = json.loads(CANON_PATH.read_text(encoding="utf-8"))
INTRO = ''' 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? '''
def generate_response(game): return tidy(chat(build_messages(game, CANON), generation_options(game)))
def show_image(path, **kwargs): """Show an image, or a quiet caption if the file is missing (instead of crashing the app).""" if Path(path).exists(): st.image(path, **kwargs) else: { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 550, 600 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -550,51 +550,51 @@ apply_action(game, action) # Python decides what actually happened
error_message = None
response = ""
try:
with thinking_placeholder.container():
with st.spinner("Thinking..."):
maybe_summarize(game)
response = generate_response(game)
except ollama.ResponseError as error:
if error.status_code == 404:
error_message = (
f"Ollama could not find {model_name()}. "
f"Run `ollama pull {model_name()}` and try again."
)
else:
error_message = f"Ollama returned an error: {error.error}"
except ollama.RequestError as error:
error_message = (
"Could not connect to Ollama. Make sure Ollama is running, "
f"then try again. Details: {error}"
)
except ModelConfigurationError as error:
error_message = str(error)
except OpenRouterError as error:
if error.status_code is None:
error_message = str(error)
else:
error_message = (
f"OpenRouter returned an error (HTTP {error.status_code}): {error}"
)
else:
if not response:
error_message = (
f"{provider_name().title()} returned an empty response. Please try again."
)
if error_message:
st.session_state.game = snapshot # undo the failed turn completely
st.session_state.model_error = error_message
else:
st.session_state.pop("model_error", None)
game["history"].append({"role": "assistant", "content": response})
st.rerun()
st.caption(f"Powered by {provider_name().title()} · {model_name()}") { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 8, 18 ] } 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 @@ -8,11 +8,11 @@ 5. history last N messages (older -> summary) 6. engine result + reminder, attached to the player's latest message """ import re
from llm import chat
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write:
- Reply in 2-5 short sentences of grounded, sensory prose. Restrained horror: unsettling details over scares. { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 378, 415 ] } 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 @@ -378,38 +378,38 @@ return text
SUMMARY_TRIGGER = 20 # live messages before we fold KEEP_RECENT = 10 # messages kept verbatim after folding
def maybe_summarize(game): live = game["history"][game["summary_idx"]:] if len(live) <= SUMMARY_TRIGGER: return fold = live[:-KEEP_RECENT] transcript = "\n".join( f"{'Player' if m['role'] == 'user' else 'Narrator'}: {m['content']}" for m in fold ) prompt = ( "Summarize the story so far in 3-4 sentences, past tense. " "Include only facts stated below: where the player went, what they found, " "what was said. Add nothing new and do not solve any mystery.\n\n" f"Previous summary: {game['summary'] or '(none)'}\n\n" f"New events:\n{transcript}" ) summary = chat( [{"role": "user", "content": prompt}], {"temperature": 0.2, "num_predict": 200, "num_ctx": 8192}, ) summary = summary.strip() if summary: game["summary"] = summary game["summary_idx"] += len(fold) { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\README.md", "view_range": [ 52, 100 ] } 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 @@ -52,49 +52,49 @@
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.
Run the Streamlit app locally
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: (optional: create and activate a virtual environment)
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.
Deployed with Render and OpenRouter
This is an option when running locally is not available. The model used is the free Gemma 4 26B A4B from OpenRouter. The provider may retain prompts, but does not use them for training.
The included Dockerfile is configured for OpenRouter and runs the Streamlit app as a Render web service. In Render, set these environment variables: { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\Dockerfile" } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/Dockerfile b/C:/Users/lucia/Documents/coding stuff/hacktober/Dockerfile index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/Dockerfile +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/Dockerfile @@ -1,19 +1,19 @@
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt
COPY the-house-that-remembers/ ./the-house-that-remembers/ COPY start.sh ./start.sh RUN chmod +x /app/start.sh
ENV MODEL_PROVIDER=openrouter
PORT=10000
EXPOSE 10000
ENTRYPOINT ["/bin/sh", "/app/start.sh"] { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\start.sh" } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh b/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh @@ -1,8 +1,8 @@
#!/bin/sh set -e
exec streamlit run /app/the-house-that-remembers/app.py
--server.address 0.0.0.0
--server.port "$PORT"
--server.headless true
{
"path": "C:\Users\lucia\Documents\coding stuff\hacktober\README.md",
"view_range": [
78,
105
]
}
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
@@ -78,28 +78,28 @@
Streamlit prints a local URL in the terminal; open it in your browser to view the app.
Deployed with Render and OpenRouter
This is an option when running locally is not available. The model used is the free Gemma 4 26B A4B from OpenRouter. The provider may retain prompts, but does not use them for training.
The included Dockerfile is configured for OpenRouter and runs the Streamlit app as a Render web service. In Render, set these environment variables: { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 526, 555 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -526,30 +526,30 @@ with st.container(key="conversation_frame"): with st.container(height=480, border=True, autoscroll=True): for message in game["history"]: with st.chat_message( message["role"], avatar="🕯️" if message["role"] == "assistant" else "🧑", ): st.markdown(message["content"])
if game["ending"]:
st.markdown("---")
st.markdown("**THE END**")
if "model_error" in st.session_state:
st.error(st.session_state.model_error)
action = st.chat_input(
"Describe what you do...",
disabled=bool(game["ending"]),
)
if action: snapshot = copy.deepcopy(game) # restore this if the model call fails game["history"].append({"role": "user", "content": action}) apply_action(game, action) # Python decides what actually happened
error_message = None
response = ""
try:
with thinking_placeholder.container():
{"pattern":"MODEL_PROVIDER|OPENROUTER|OpenRouter|from llm|chat\(","paths":["C:\Users\lucia\Documents\coding stuff\hacktober"],"glob":"*.{py,md,Dockerfile,sh,txt}","output_mode":"content","-n":tr... [grep content: 27 matches across 4 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober]
README.md (3 match(es)): 81: ### Deployed with Render and OpenRouter 83: This is an option when running locally is not available. The model used is the free Gemma 4 26B A4B from OpenRouter. The provider may retain prompts, but does not use them for training. 85: The included Dockerfile is configured for OpenRouter and runs the Streamlit app
the-house-that-remembers/prompt.py (2 match(es)): 13: from llm import chat 403: summary = chat(
the-house-that-remembers/llm.py (17 match(es)): 9: OPENROUTER_URL = "https://openrouter.ai/api/v1/chat/completions" 10: DEFAULT_OPENROUTER_MODEL = "google/gemma-4-26b-a4b-it:free" 18: class OpenRouterError(Exception): 25: provider = os.getenv("MODEL_PROVIDER", "ollama").strip().lower() 28: "MODEL_PROVIDER must be either 'ollama' or 'openrouter'." 35: name = os.getenv("OPENROUTER_MODEL", DEFAULT_OPENROUTER_MODEL).strip() 37: raise ModelConfigurationError("OPENROUTER_MODEL cannot be empty.") 45: def chat(messages, options): 48: result = ollama.chat( 56: api_key = os.getenv("OPENROUTER_API_KEY", "").strip() 59: "OPENROUTER_API_KEY is required when MODEL_PROVIDER is 'openrouter'." 69: OPENROUTER_URL, 84: raise OpenRouterError("OpenRouter returned invalid JSON.") from error 97: raise OpenRouterError(str(body), error.code) from error 99: raise OpenRouterError(f"Could not connect to OpenRouter: {error}") from error 104: raise OpenRouterError("OpenRouter returned an unexpected response.") from error 106: raise OpenRouterError("OpenRouter did not return a text response.")
the-house-that-remembers/app.py (5 match(es)): 11: from llm import ( 13: OpenRouterError, 75: return tidy(chat(build_messages(game, CANON), generation_options(game))) 574: except OpenRouterError as error: 579: f"OpenRouter returned an error (HTTP {error.status_code}): {error}" *** Begin Patch *** Update File: the-house-that-remembers/app.py @@ from pathlib import Path
import ollama import streamlit as st from PIL import Image
-from llm import (
- ModelConfigurati... *** Begin Patch *** Update File: the-house-that-remembers/app.py @@ -from llm import (
- ModelConfigurationError,
- OpenRouterError,
- chat,
- model_name,
- provider_name, -) from engi... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -8,13 +8,6 @@ import streamlit as st from PIL import Image
-from llm import (
- ModelConfigurationError,
- OpenRouterError,
- chat,
- model_name,
- provider_name, -) from engine import ( FINAL_REVELATIONS, FRAGMENT_LABELS, @@ -42,6 +35,7 @@ unsafe_allow_html=True, )
+MODEL_NAME = "gemma4:e4b" CANON_PATH = Path(file).resolve().parent / "story" / "canon.json" CANON = json.loads(CANON_PATH.read_text(encoding="utf-8"))
@@ -72,7 +66,13 @@
def generate_response(game):
- return tidy(chat(build_messages(game, CANON), generation_options(game)))
- result = ollama.chat(
-
model=MODEL_NAME, -
messages=build_messages(game, CANON), -
think=False, -
options=generation_options(game), - )
- return tidy(result.message.content)
def show_image(path, **kwargs): @@ -536,8 +536,8 @@ st.markdown("---") st.markdown("THE END")
-
if "model_error" in st.session_state: -
st.error(st.session_state.model_error)
-
if "ollama_error" in st.session_state: -
st.error(st.session_state.ollama_error)action = st.chat_input( "Describe what you do...", @@ -554,13 +554,13 @@ try: with thinking_placeholder.container(): with st.spinner("Thinking..."):
-
maybe_summarize(game)
-
except ollama.ResponseError as error: if error.status_code == 404: error_message = (maybe_summarize(game, MODEL_NAME) response = generate_response(game)
-
f"Ollama could not find {model_name()}. " -
f"Run `ollama pull {model_name()}` and try again."
-
f"Ollama could not find {MODEL_NAME}. " -
f"Run `ollama pull {MODEL_NAME}` and try again." ) else: error_message = f"Ollama returned an error: {error.error}"
@@ -569,28 +569,17 @@ "Could not connect to Ollama. Make sure Ollama is running, " f"then try again. Details: {error}" )
- except ModelConfigurationError as error:
-
error_message = str(error) - except OpenRouterError as error:
-
if error.status_code is None: -
error_message = str(error) -
else: -
error_message = ( -
f"OpenRouter returned an error (HTTP {error.status_code}): {error}" -
else: if not response:) -
error_message = ( -
f"{provider_name().title()} returned an empty response. Please try again." -
)
-
error_message = "Ollama returned an empty response. Please try again."if error_message: st.session_state.game = snapshot # undo the failed turn completely
-
st.session_state.model_error = error_message
-
else:st.session_state.ollama_error = error_message
-
st.session_state.pop("model_error", None)
-
st.session_state.pop("ollama_error", None) game["history"].append({"role": "assistant", "content": response})st.rerun()
-st.caption(f"Powered by {provider_name().title()} · {model_name()}") +st.caption(f"Powered by Ollama · {MODEL_NAME}")
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 @@ -10,7 +10,7 @@ """ import re
-from llm import chat +import ollama
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
@@ -385,7 +385,7 @@ KEEP_RECENT = 10 # messages kept verbatim after folding
-def maybe_summarize(game): +def maybe_summarize(game, model): live = game["history"][game["summary_idx"]:] if len(live) <= SUMMARY_TRIGGER: return @@ -400,11 +400,13 @@ f"Previous summary: {game['summary'] or '(none)'}\n\n" f"New events:\n{transcript}" )
- summary = chat(
-
[{"role": "user", "content": prompt}], -
{"temperature": 0.2, "num_predict": 200, "num_ctx": 8192},
- r = ollama.chat(
-
model=model, -
messages=[{"role": "user", "content": prompt}], -
think=False, -
)options={"temperature": 0.2, "num_predict": 200, "num_ctx": 8192},
- summary = summary.strip()
- summary = r.message.cont
[Truncated — full output (11.3 KB) temporarily saved to C:\Users\lucia\AppData\Local\Temp\1793247486930-copilot-tool-output-6b4dc78f01d64b0a9e2d912794c1feea.txt] {"pattern":"MODEL_PROVIDER|OPENROUTER|OpenRouter|from llm|llm\.py","paths":["C:\Users\lucia\Documents\coding stuff\hacktober"],"glob":"*.{py,md,Dockerfile,sh,txt}","output_mode":"content","-n":t... No matches found. {"command":"Set-Location .\the-house-that-remembers; python -m unittest discover -s tests -p 'test_engine.py'; python -m py_compile .\app.py .\prompt.py","description":"Run game tests and compile t... .......................... #
Ran 26 tests in 0.009s
OK <shellId: 206 completed with exit code 0> {"command":"git diff --check; git status --short; git --no-pager diff --stat; git --no-pager diff -- the-house-that-remembers/app.py the-house-that-remembers/prompt.py Dockerfile start.sh README.md","... Output too large to read at once (8.3 KB). Saved to: C:\Users\lucia\AppData\Local\Temp\1793247491574-copilot-tool-output-8656-26f83ad6-a9bf-4f9b-8400-8f7b398ff817.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): warning: in the working copy of 'README.md', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'start.sh', LF will be replaced by CRLF the next time Git touches it warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it M Dockerfile M README.md M start.sh M the-house-that-remembers/app.py D the-house-that-remembers/llm.py M the-house-that-remembers/prompt.py warning: in the working co <shellId: 205 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 1, 82 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -1,82 +1,82 @@ import base64 import copy import json from io import BytesIO from pathlib import Path
import ollama import streamlit as st from PIL import Image
from engine import ( FINAL_REVELATIONS, FRAGMENT_LABELS, FRAGMENT_KINDS, FRAGMENT_CLUES_NEEDED, ROOMS, apply_action, fragment_progress, fragments_complete, new_game, ) from prompt import build_messages, generation_options, maybe_summarize, tidy
st.set_page_config(page_title="The House That Remembers", layout="wide") st.markdown( "<link rel='stylesheet' " "href='https://cdn-uicons.flaticon.com/uicons-solid-straight/css/" "uicons-solid-straight.css'>", unsafe_allow_html=True, ) st.markdown( "<link rel='stylesheet' " "href='https://cdn-uicons.flaticon.com/uicons-bold-straight/css/" "uicons-bold-straight.css'>", unsafe_allow_html=True, )
MODEL_NAME = "gemma4:e4b" CANON_PATH = Path(file).resolve().parent / "story" / "canon.json" CANON = json.loads(CANON_PATH.read_text(encoding="utf-8"))
INTRO = ''' 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? '''
def generate_response(game): result = ollama.chat( model=MODEL_NAME, messages=build_messages(game, CANON), think=False, options=generation_options(game), ) return tidy(result.message.content)
def show_image(path, **kwargs): """Show an image, or a quiet caption if the file is missing (instead of crashing the app).""" if Path(path).exists(): st.image(path, **kwargs) else: { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 548, 598 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -548,51 +548,51 @@ snapshot = copy.deepcopy(game) # restore this if the model call fails game["history"].append({"role": "user", "content": action}) apply_action(game, action) # Python decides what actually happened
error_message = None
response = ""
try:
with thinking_placeholder.container():
with st.spinner("Thinking..."):
maybe_summarize(game, MODEL_NAME)
response = generate_response(game)
except ollama.ResponseError as error:
if error.status_code == 404:
error_message = (
f"Ollama could not find {MODEL_NAME}. "
f"Run `ollama pull {MODEL_NAME}` and try again."
)
else:
error_message = f"Ollama returned an error: {error.error}"
except ollama.RequestError as error:
error_message = (
"Could not connect to Ollama. Make sure Ollama is running, "
f"then try again. Details: {error}"
)
else:
if not response:
error_message = "Ollama returned an empty response. Please try again."
if error_message:
st.session_state.game = snapshot # undo the failed turn completely
st.session_state.ollama_error = error_message
else:
st.session_state.pop("ollama_error", None)
game["history"].append({"role": "assistant", "content": response})
st.rerun()
st.caption(f"Powered by Ollama · {MODEL_NAME}") { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 8, 17 ] } 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 @@ -8,10 +8,10 @@ 5. history last N messages (older -> summary) 6. engine result + reminder, attached to the player's latest message """ import re
import ollama
CORE = """You are the narrator of a gothic-horror interactive story, "The House That Remembers".
How to write: { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\README.md", "view_range": [ 50, 95 ] } 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 @@ -50,46 +50,46 @@
- 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.
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.
Run the Streamlit app locally
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: (optional: create and activate a virtual environment)
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. { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\Dockerfile" } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/Dockerfile b/C:/Users/lucia/Documents/coding stuff/hacktober/Dockerfile index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/Dockerfile +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/Dockerfile @@ -1,28 +1,28 @@
FROM ollama/ollama:0.35.1
RUN apt-get update && apt-get install -y --no-install-recommends
python3 python3-pip python3-venv curl
&& rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt .
RUN python3 -m venv /opt/venv
&& /opt/venv/bin/pip install --no-cache-dir -r requirements.txt
&& /opt/venv/bin/pip install --no-cache-dir Pillow
COPY the-house-that-remembers/ ./the-house-that-remembers/ COPY start.sh ./start.sh RUN chmod +x /app/start.sh
ENV PATH="/opt/venv/bin:$PATH"
OLLAMA_HOST=127.0.0.1:11434
OLLAMA_KEEP_ALIVE=-1
OLLAMA_CONTEXT_LENGTH=4096
PORT=10000
EXPOSE 10000
ENTRYPOINT ["/bin/sh", "/app/start.sh"] { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\start.sh" } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh b/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/start.sh @@ -1,16 +1,16 @@
#!/bin/sh set -e
ollama serve &
until curl -sf http://127.0.0.1:11434/api/version >/dev/null; do sleep 1 done
ollama pull gemma4:e4b
exec streamlit run /app/the-house-that-remembers/app.py
--server.address 0.0.0.0
--server.port "$PORT"
--server.headless true
{
"query": "Search the web for current official Render documentation about persistent disks storage on web services, availability and pricing/plan limits."
}
[
"pylance-mcp-server-pylanceDocuments",
"semantic_search",
"pylance-mcp-server-pylancePythonProfiling",
"github-mcp-server-get_copilot_space",
"pylance-mcp-server-pylancePythonProfileExplorer"
]
{
"url": "https://render.com/docs/disks",
"max_length": 12000
}
Contents of https://render.com/docs/disks:
You can attach a persistent disk to a paid Render web service, private service, or background worker. This enables you to preserve local filesystem changes across deploys and restarts.
By default, Render services have an ** ephemeral filesystem**.
This means that without a persistent disk, any changes you make to a service's local files are lost every time the service redeploys or restarts.
Persistent disks are useful for services such as:
- Infrastructure components (Elasticsearch, Kafka, RabbitMQ, etc.)
- A blogging platform or CMS (WordPress, Ghost, Strapi, etc.)
- Collaboration apps (Mattermost, GitLab, Discourse, etc.)
- Custom datastores (MySQL, MongoDB, etc.)
Persistent disks use the same high-performance SSDs as Render Postgres and Key Value instances. All disks are encrypted at rest, and so are their automatic daily snapshots.
Before you attach a persistent disk, it's helpful to understand important limitations and considerations.
You create a persistent disk from the Render Dashboard. You can do so during service creation (click Advanced at the bottom of the creation form), or any time after creation from your service's Disk page:

- Set your disk's mount path.
- Only filesystem changes under this path are preserved across deploys and restarts! The rest of your service's filesystem remains ephemeral.
- For more details, see Mount path.
- Choose a disk size.
- You can increase your disk's size later, but you can't decrease it. Pick the smallest value that currently works for your service.
- Click Add disk.
After you save, Render triggers a new deploy for your service. The disk becomes available as soon as the deploy is live.
When you attach a persistent disk to a service, you specify its absolute mount path. Only data written under this path is preserved across deploys and restarts.
You can mount a disk at any path except those listed below.
Specify a mount path according to how your service stores data:
- If your framework writes to a relative path (e.g., Rails Active Storage uses
./storagefor file uploads), mount your disk at the absolute version of that path or an ancestor of it. See the table below for mount paths by runtime. - Otherwise, use a standalone directory like
/var/dataand configure your application to write persistent data there.
To mount your disk in a subdirectory of your service's source code, the absolute path depends on your runtime:
| Runtime | Source code path | Example disk mount path |
|---|---|---|
| Node.js, Python, Ruby, Elixir, and Rust | /opt/render/project/src |
/opt/render/project/src/storage |
| Go | /opt/render/project/go/src/github.com/<owner>/<repo> |
/opt/render/project/go/src/github.com/<owner>/<repo>/data |
| Docker | Your Dockerfile's WORKDIR (commonly /app) |
/app/storage |
You cannot mount a disk at any of these exact mount paths:
/(filesystem root)/opt,/opt/render,/opt/render/project,/opt/render/project/src/home,/home/render/etc,/etc/secrets
You can mount a disk at a subdirectory within one of these paths. For example, you can mount your disk at /opt/render/project/src/uploads.
View your disk's usage over time from your service's Disk page in the Render Dashboard:

Render automatically creates a s
[Truncated — full output (9.7 KB) temporarily saved to C:\Users\lucia\AppData\Local\Temp\1793247627283-copilot-tool-output-422e45c776bc4c9398cf79cdf498a001.txt] { "url": "https://render.com/docs/persistent-disks", "max_length": 12000 } { "url": "https://render.com/docs/free", "max_length": 8000 } Contents of https://render.com/docs/free: You can deploy certain Render service types free of charge:
- Web services (web apps in Node.js, Python, Rails, etc.)
- Render Postgres databases
- Render Key Value instances
Free instances have important limitations, described below. Do not use them for production applications. However, they're perfect for testing out a new technology, working on a hobby project, or previewing Render's developer experience.
You can also deploy static sites on Render for free.
Web services and static sites count against your monthly included amounts of outbound bandwidth and pipeline minutes.
View your workspace's monthly included usage in the Render Dashboard.
For a more complete walkthrough, see Your First Render Deploy.
-
Sign up for Render if you haven't yet.
-
In the Render Dashboard, click New:

-
Select Static Site, Web Service, Postgres, or Key Value.
- Other service types don't support Free instances.
-
During the service creation flow, you choose a compute plan for your service (unless it's a static site). Choose Free:

That's it! When you finish creating and deploying your service, it runs on a Free instance.
See the sections below for details on the limitations of Free instances.
To remove the Free instance limitations described below, you can upgrade your service to any paid compute plan. See Changing a service's compute plan.
Upgrading your workspace plan does not remove limitations on Free instances.
Your workspace's plan only determines which platform-level features are available. You change a service's compute plan independently.
Free web services support many (but not all) features available to web services on paid compute plans. Supported features include:
- Custom domains
- Managed TLS certificates
- Service previews
- Log streams
- Rollbacks (only to the two most recent previous deploys)
Most limitations below are specific to web services on the Free compute plan.
Render spins down a Free web service that goes 15 minutes without receiving any inbound traffic. This includes both HTTP requests and WebSocket messages from existing connections.
A Free web service spins back up whenever it next receives an HTTP request or new WebSocket connection. This process takes about one minute. Render displays a page to connecting browsers while a service is spinning up.
Whenever a service spins down, any changes to its local filesystem are lost (see Local files lost on redeploy).
Like all Render services, Free web services have an ephemeral filesystem. This means that any changes to your web service's filesystem (uploaded images, local SQLite databases, etc.) are lost every time the service redeploys, restarts, or spins down.
For long-term data storage:
- Paid services can preserve local filesystem changes by attaching a persistent disk, but Free web services cannot.
- Paid and Free services can persist relational data in a Render Postgres database.
- Note that Free Render Postgres databases expire 30 days after creation.
Render grants 750 Free instance hours to each workspace per calendar month:
- A Free web service consumes these hours as long as it's running (spun-down services don't consume Free instance hours).
- If you consume all of your Free instance hours during a given month, Render suspends all of your Free web services until the start of the next month.
- At the start of each month, your Free instance hours reset to 750 (remaining hours don't roll over).
Bandwidth and build pipeline
Free web services count against your monthly included amounts of outbound bandwidth and build pipeline minutes.
- If you consume all of your outbound bandwidth during a given month, Render bills you for a supplementary amount.
- If you haven't added a payment method, Render instead suspends all of your Free services for the remainder of the month.
- If you consume all of your build pipeline minutes during a given month, Render bills you for a supplementary amount (unless you've reached your spend limit).
- If you haven't added a payment method or you reach your spend limit, Render instead disables all new builds for your workspace for the remainder of the month.
- In this case, your services remain active using their existing build artifacts.
View your usage details from the Monthly Included Usage section of your Billing page in the Render Dashboard:

Render notifies you via email when you’re approaching a usage limit, and then again if you exceed that limit.
Render may suspend a Free web service that initiates an uncommonly high volume of traffic over the public internet.
Examples of service-initiated traffic include:
- Accessing an external database
- Invoking external APIs
- Transferring data to or from external object storage
If your service is suspended this way, you can restore it by moving it to any paid compute plan.
While a Free web service is spun down, incoming requests to the path /robots.txt automatically receive a standard "disallow all" response:
These requests to do not reach your service or trigger a spin-up.
While a Free web service is active, requests to /robots.txt are routed to it as normal.
- Render might restart a Free web service at any time.
- Free web services don't support the following features of paid compute plans:
- Scaling beyond a single instance
- Persistent disks
- Edge caching
- Running one-off jobs
- Shell access via SSH or the Render Dashboard
- Free web services can't receive private network traffic.
- They can send private network requests to your data stores and paid services in the same region.
- Free web services can't bind to Render's reserved ports (
18012,18013, or19099). - Free web services can't send outbound network traffic on ports
25,465, or587, commonly used for SMTP.- Render blocks outbound traffic on port
25for all services.
- Render blocks outbound traffic on port
The limitations below are specific to Render Postgres databases on the Free compute plan. To avoid these limitations, you can upgrade your database to any paid compute plan.
Only one Free Render Postgres database can be active for any given workspace.
Free Render Postgres databases have a fixed storage capacity of 1 GB.
Free Render Postgres databases expire 30 days after creation. An expired Free database is inaccessible unless you upgrade it to a paid compute plan.
After a Free database expires, you have a grace period of 14 days to upgrade it to a paid compute plan. After the grace period, Render deletes the database (along with all of its data)
<note>Content truncated. Call the fetch tool with a start_index of 8000 to get more content.</note> { "url": "https://render.com/docs/disks", "max_length": 8000 } Contents of https://render.com/docs/disks: You can attach a persistent disk to a paid Render web service, private service, or background worker. This enables you to preserve local filesystem changes across deploys and restarts.
By default, Render services have an ** ephemeral filesystem**.
This means that without a persistent disk, any changes you make to a service's local files are lost every time the service redeploys or restarts.
Persistent disks are useful for services such as:
- Infrastructure components (Elasticsearch, Kafka, RabbitMQ, etc.)
- A blogging platform or CMS (WordPress, Ghost, Strapi, etc.)
- Collaboration apps (Mattermost, GitLab, Discourse, etc.)
- Custom datastores (MySQL, MongoDB, etc.)
Persistent disks use the same high-performance SSDs as Render Postgres and Key Value instances. All disks are encrypted at rest, and so are their automatic daily snapshots.
Before you attach a persistent disk, it's helpful to understand important limitations and considerations.
You create a persistent disk from the Render Dashboard. You can do so during service creation (click Advanced at the bottom of the creation form), or any time after creation from your service's Disk page:

- Set your disk's mount path.
- Only filesystem changes under this path are preserved across deploys and restarts! The rest of your service's filesystem remains ephemeral.
- For more details, see Mount path.
- Choose a disk size.
- You can increase your disk's size later, but you can't decrease it. Pick the smallest value that currently works for your service.
- Click Add disk.
After you save, Render triggers a new deploy for your service. The disk becomes available as soon as the deploy is live.
When you attach a persistent disk to a service, you specify its absolute mount path. Only data written under this path is preserved across deploys and restarts.
You can mount a disk at any path except those listed below.
Specify a mount path according to how your service stores data:
- If your framework writes to a relative path (e.g., Rails Active Storage uses
./storagefor file uploads), mount your disk at the absolute version of that path or an ancestor of it. See the table below for mount paths by runtime. - Otherwise, use a standalone directory like
/var/dataand configure your application to write persistent data there.
To mount your disk in a subdirectory of your service's source code, the absolute path depends on your runtime:
| Runtime | Source code path | Example disk mount path |
|---|---|---|
| Node.js, Python, Ruby, Elixir, and Rust | /opt/render/project/src |
/opt/render/project/src/storage |
| Go | /opt/render/project/go/src/github.com/<owner>/<repo> |
/opt/render/project/go/src/github.com/<owner>/<repo>/data |
| Docker | Your Dockerfile's WORKDIR (commonly /app) |
/app/storage |
You cannot mount a disk at any of these exact mount paths:
/(filesystem root)/opt,/opt/render,/opt/render/project,/opt/render/project/src/home,/home/render/etc,/etc/secrets
You can mount a disk at a subdirectory within one of these paths. For example, you can mount your disk at /opt/render/project/src/uploads.
View your disk's usage over time from your service's Disk page in the Render Dashboard:

Render automatically creates a snapshot of your persistent disk once every 24 hours. If your disk experiences critical data loss or corruption, you can completely restore its state to any available snapshot. Snapshots are available for seven days after they're created.
Important:
- If you restore a snapshot, all changes to your disk that occurred after the snapshot are lost.
- Render doesn’t support restoring only a portion of a disk snapshot.
- Do not restore a snapshot of a disk that's used for a custom database instance. See details.
Restore a snapshot from your service's Disk page in the Render Dashboard:

If you use a persistent disk specifically to back a custom database instance on Render (such as MySQL or MongoDB), do not perform a disk restore for database recovery purposes. If you do, your database might restore to a corrupted state.
Instead, create regular backups of your database using a tool like mysqldump for MySQL or mongodump for MongoDB. Restore your database's state using one of these backups.
You can securely transfer files between your disk-backed service and your local machine using a tool like SCP or Magic-Wormhole.
After you set up SSH for your service, you can transfer files using SCP.
For example, if your ssh command looks like this:
Then your scp commands look like this:
We recommend using SCP with the -s flag to use the more modern SFTP protocol. Future releases of SCP will default to using SFTP, and this flag will no longer be needed.
The Magic-Wormhole library enables you to transfer files to and from your disk-backed service without using SSH and SCP.
-
In the Render Dashboard, go to your service's Shell page.
-
If you have a Docker-image-backed service, use the shell to install
magic-wormhole:- Run
apt update && apt install magic-wormholeor the equivalent for your environment. - The
magic-wormholelibrary is pre-installed on all Render native runtimes.
- Run
-
Use the shell to transfer your file with the
wormholecommand: -
Note the code that appears in the output from
wormhole. Then, from any internet-connected machine, install magic-wormhole and runwormhole receive, entering in the code when prompted.
Disk limitations and considerations #
When attaching a persistent disk to your service, note the following:
- **Only filesystem changes under your disk's mount path are preserved across deploys and r
<note>Content truncated. Call the fetch tool with a start_index of 8000 to get more content.</note>
File "[REDACTED]\AppData\Roaming\Python\Python314\site-packages\streamlit\runtime\scriptrunner\script_runner.py", line 924, in code_to_exec
exec(code, module.**dict**) # noqa: S102
~~^^^^^^^^^^^^^^^^^^^^^^^
File "[REDACTED]\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", line 557, in <module>
maybe_summarize(game, MODEL_NAME)
~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^
TypeError: maybe_summarize() takes 1 positional argument but 2 were given
I’ll check the current app and prompt function signatures, then align the call with the function definition and run the focused tests.
{"pattern":"def maybe_summarize|maybe_summarize\(","paths":"C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers","glob":"*.py","output_mode":"content","-n":true,"head_limit... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py:388:def maybe_summarize(game, model): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py:557: maybe_summarize(game, MODEL_NAME) { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py", "view_range": [ 378, 415 ] } 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 @@ -378,38 +378,38 @@ return text
SUMMARY_TRIGGER = 20 # live messages before we fold KEEP_RECENT = 10 # messages kept verbatim after folding
def maybe_summarize(game, model): live = game["history"][game["summary_idx"]:] if len(live) <= SUMMARY_TRIGGER: return fold = live[:-KEEP_RECENT] transcript = "\n".join( f"{'Player' if m['role'] == 'user' else 'Narrator'}: {m['content']}" for m in fold ) prompt = ( "Summarize the story so far in 3-4 sentences, past tense. " "Include only facts stated below: where the player went, what they found, " "what was said. Add nothing new and do not solve any mystery.\n\n" f"Previous summary: {game['summary'] or '(none)'}\n\n" f"New events:\n{transcript}" ) r = ollama.chat( model=model, messages=[{"role": "user", "content": prompt}], think=False, options={"temperature": 0.2, "num_predict": 200, "num_ctx": 8192}, ) summary = r.message.content.strip() if summary: game["summary"] = summary game["summary_idx"] += len(fold) { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 548, 563 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -548,16 +548,16 @@ snapshot = copy.deepcopy(game) # restore this if the model call fails game["history"].append({"role": "user", "content": action}) apply_action(game, action) # Python decides what actually happened
error_message = None
response = ""
try:
with thinking_placeholder.container():
with st.spinner("Thinking..."):
maybe_summarize(game, MODEL_NAME)
response = generate_response(game)
except ollama.ResponseError as error:
if error.status_code == 404:
error_message = (
f"Ollama could not find {MODEL_NAME}. "
f"Run `ollama pull {MODEL_NAME}` and try again."
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", "view_range": [ 420, 480 ] } 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 @@ -420,61 +420,61 @@ for expected in engine.FINAL_REVELATIONS: result = act(game, "What happened?") self.assertEqual(result["family_memory"]["id"], expected["id"]) self.assertIn(expected["detail"], _engine_note(game)) self.assertIn(expected["detail"], state_block(game))
ending = act(game, "Let's leave together.")
self.assertEqual(ending["outcome"], "ending")
self.assertEqual(game["ending"], "leave_together")
def test_high_denial_blocks_final_memories_and_together_ending(self):
game = engine.new_game()
game.update(
stage=5,
door_open=True,
room="Parents' Bedroom",
friend_room="Parents' Bedroom",
truth_known=True,
denial=3,
)
result = act(game, "What do you remember?")
self.assertNotIn("family_memory", result)
self.assertFalse(game["final_revelations"])
self.assertIn("still resisting it", stage_block(game))
ending = act(game, "Let's leave together.")
self.assertEqual(ending["outcome"], "blocked")
self.assertIsNone(game["ending"])
def test_small_talk_is_disabled_after_truth_is_faced(self):
game = engine.new_game()
game.update(stage=5, door_open=True, truth_known=True, denial=2)
prompt = stage_block(game)
self.assertIn("CONVERSATION FOCUS", prompt)
self.assertIn("Do not initiate small talk", prompt)
def test_small_talk_remains_available_before_truth_is_faced(self):
game = engine.new_game()
game.update(stage=5, door_open=True, truth_known=False)
prompt = stage_block(game)
self.assertNotIn("CONVERSATION FOCUS", prompt)
if name == "main": unittest.main() python {"command":"Set-Location .\the-house-that-remembers; python -c "import inspect, prompt; print(prompt.file); print(inspect.signature(prompt.maybe_summarize))"","description":"Verify the loaded p... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\prompt.py (game, model) <shellId: 207 completed with exit code 0> { "command": "Set-Location .\the-house-that-remembers; python -m unittest discover -s tests -p 'test_engine.py'", "description": "Run the focused game tests", "initial_wait": 120 } .......................... #
Ran 26 tests in 0.010s
OK <shellId: 208 completed with exit code 0>
The error is from a **mismatch between the `app.py` and `prompt.py` versions loaded by Streamlit**. On disk, the call passes `game` and `MODEL_NAME`, and `maybe_summarize` accepts both. I verified the loaded function signature is `(game, model)`, and all 26 tests pass.
Stop the running Streamlit process completely, then start it again so it reloads the current files:
python -m streamlit run .\the-house-that-remembers\app.py
No code change was needed.
{"pattern":"thinking_placeholder|chat_input|Thinking|conversation_column|thinking","paths":"C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers","glob":"app.py","output_mode... [grep content: 6 matches across 1 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
app.py (6 match(es)): 500- ) 501- 502- with st.expander("The letter and arrival", expanded=True): 503- st.markdown(INTRO) 504- 505: scene_column, conversation_column = st.columns([1, 1.25], gap="large") 506- room = ROOMS[game["room"]] 507- 508- with scene_column: 509- with st.container(height="content", border=True, key="scene_panel"): 510- st.markdown(f"### {game['room']}") 518- st.markdown( 519- f'<div class="story">{room["description"]}</div>', 520- unsafe_allow_html=True, 521- ) 522- 523: with conversation_column: 524: thinking_placeholder = st.empty() 525- st.markdown("### What will you do?") 526- with st.container(key="conversation_frame"): 527- with st.container(height=480, border=True, autoscroll=True): 528- for message in game["history"]: 529- with st.chat_message( 537- st.markdown("THE END") 538- 539- if "ollama_error" in st.session_state: 540- st.error(st.session_state.ollama_error) 541- 542: action = st.chat_input( 543- "Describe what you do...", 544- disabled=bool(game["ending"]), 545- ) 546- 547- if action: 550- apply_action(game, action) # Python decides what actually happened 551- 552- error_message = None 553- response = "" 554- try: 555: with thinking_placeholder.container(): 556: with st.spinner("Thinking..."): 557- maybe_summarize(game, MODEL_NAME) 558- response = generate_response(game) 559- except ollama.ResponseError as error: 560- if error.status_code == 404: 561- error_message = ( { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 435, 575 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -435,141 +435,141 @@ show_note_dialog() else: st.write(f"• {item}") else: st.caption("Your pockets are empty.")
st.markdown(
'<h3 class="sidebar-icon-heading"><i class="fi fi-bs-search" '
'aria-hidden="true"></i>Clues</h3>',
unsafe_allow_html=True,
)
if game["clues"]:
for clue in game["clues"]:
st.write(f"• {clue}")
else:
st.caption("Nothing discovered yet.")
st.markdown(
'<h3 class="sidebar-icon-heading"><i class="fi fi-ss-door-open" '
'aria-hidden="true"></i>Explored</h3>',
unsafe_allow_html=True,
)
for visited_room in game["visited"]:
st.write(f"✓ {visited_room}")
st.divider()
if st.button("Restart adventure", use_container_width=True):
st.session_state.pop("game", None)
st.session_state.pop("ollama_error", None)
st.rerun()
st.subheader("Atmosphere")
sound_enabled = st.checkbox("Background rain", value=True)
if sound_enabled:
play_background_music(Path("assets") / "rain-and-thunder.mp3")
house_ambience_enabled = st.checkbox("House ambience", value=True)
if house_ambience_enabled:
play_background_music(Path("assets") / "ambience.mp3")
st.markdown(
'<div class="sidebar-about"><a '
'href="https://github.com/luc-rap/hacktoberfest-open-source-ai-week4" '
'target="_blank" rel="noopener noreferrer">About · GitHub</a></div>',
unsafe_allow_html=True,
)
banner_image = haunted_house_banner() banner_style = "" if banner_image: banner_style = ( ' style="background-image: ' 'linear-gradient(90deg, rgba(10, 12, 14, 0.94) 0%, ' 'rgba(10, 12, 14, 0.72) 48%, rgba(10, 12, 14, 0.20) 100%), ' 'linear-gradient(0deg, rgba(15, 16, 17, 0.78), transparent 68%), ' f'url(data:image/jpeg;base64,{banner_image});"' ) st.markdown( f'<header class="page-header"{banner_style}>' '<p class="eyebrow">A gothic horror adventure · Private journal</p>' '<h1 class="title">The House That Remembers</h1>' '<p class="subtitle">Some doors should stay closed.</p>' '</header>', unsafe_allow_html=True, )
with st.expander("The letter and arrival", expanded=True): st.markdown(INTRO)
scene_column, conversation_column = st.columns([1, 1.25], gap="large") room = ROOMS[game["room"]]
with scene_column: with st.container(height="content", border=True, key="scene_panel"): st.markdown(f"### {game['room']}") if "img" in room: show_image( Path(file).resolve().parent / "assets" / room["img"], width='stretch', ) else: st.caption("No image is available for this room yet.") st.markdown( f'<div class="story">{room["description"]}</div>', unsafe_allow_html=True, )
with conversation_column: thinking_placeholder = st.empty() st.markdown("### What will you do?") with st.container(key="conversation_frame"): with st.container(height=480, border=True, autoscroll=True): for message in game["history"]: with st.chat_message( message["role"], avatar="🕯️" if message["role"] == "assistant" else "🧑", ): st.markdown(message["content"])
if game["ending"]:
st.markdown("---")
st.markdown("**THE END**")
if "ollama_error" in st.session_state:
st.error(st.session_state.ollama_error)
action = st.chat_input(
"Describe what you do...",
disabled=bool(game["ending"]),
)
if action: snapshot = copy.deepcopy(game) # restore this if the model call fails game["history"].append({"role": "user", "content": action}) apply_action(game, action) # Python decides what actually happened
error_message = None
response = ""
try:
with thinking_placeholder.container():
with st.spinner("Thinking..."):
maybe_summarize(game, MODEL_NAME)
response = generate_response(game)
except ollama.ResponseError as error:
if error.status_code == 404:
error_message = (
f"Ollama could not find {MODEL_NAME}. "
f"Run `ollama pull {MODEL_NAME}` and try again."
)
else:
error_message = f"Ollama returned an error: {error.error}"
except ollama.RequestError as error:
error_message = (
"Could not connect to Ollama. Make sure Ollama is running, "
f"then try again. Details: {error}"
)
else:
if not response:
error_message = "Ollama returned an empty response. Please try again."
*** Begin Patch *** Update File: the-house-that-remembers/app.py @@ with conversation_column:
- thinking_placeholder = st.empty() st.markdown("### What will you do?") with st.container(k... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -521,7 +521,6 @@ )
with conversation_column:
- thinking_placeholder = st.empty() st.markdown("### What will you do?") with st.container(key="conversation_frame"): with st.container(height=480, border=True, autoscroll=True): @@ -539,6 +538,7 @@ if "ollama_error" in st.session_state: st.error(st.session_state.ollama_error)
- thinking_placeholder = st.empty() action = st.chat_input( "Describe what you do...", disabled=bool(game["ending"]), { "command": "python -m py_compile .\the-house-that-remembers\app.py; if ($?) { git diff --check -- .\the-house-that-remembers\app.py }", "description": "Validate the chat layout change" } warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it <shellId: 209 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 518, 560 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -518,43 +518,43 @@ st.markdown( f'<div class="story">{room["description"]}</div>', unsafe_allow_html=True, )
with conversation_column: st.markdown("### What will you do?") with st.container(key="conversation_frame"): with st.container(height=480, border=True, autoscroll=True): for message in game["history"]: with st.chat_message( message["role"], avatar="🕯️" if message["role"] == "assistant" else "🧑", ): st.markdown(message["content"])
if game["ending"]:
st.markdown("---")
st.markdown("**THE END**")
if "ollama_error" in st.session_state:
st.error(st.session_state.ollama_error)
thinking_placeholder = st.empty()
action = st.chat_input(
"Describe what you do...",
disabled=bool(game["ending"]),
)
if action: snapshot = copy.deepcopy(game) # restore this if the model call fails game["history"].append({"role": "user", "content": action}) apply_action(game, action) # Python decides what actually happened
error_message = None
response = ""
try:
with thinking_placeholder.container():
with st.spinner("Thinking..."):
maybe_summarize(game, MODEL_NAME)
response = generate_response(game)
except ollama.ResponseError as error:
if error.status_code == 404:
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 1, 85 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -1,85 +1,85 @@ import base64 import copy import json from io import BytesIO from pathlib import Path
import ollama import streamlit as st from PIL import Image
from engine import ( FINAL_REVELATIONS, FRAGMENT_LABELS, FRAGMENT_KINDS, FRAGMENT_CLUES_NEEDED, ROOMS, apply_action, fragment_progress, fragments_complete, new_game, ) from prompt import build_messages, generation_options, maybe_summarize, tidy
st.set_page_config(page_title="The House That Remembers", layout="wide") st.markdown( "<link rel='stylesheet' " "href='https://cdn-uicons.flaticon.com/uicons-solid-straight/css/" "uicons-solid-straight.css'>", unsafe_allow_html=True, ) st.markdown( "<link rel='stylesheet' " "href='https://cdn-uicons.flaticon.com/uicons-bold-straight/css/" "uicons-bold-straight.css'>", unsafe_allow_html=True, )
MODEL_NAME = "gemma4:e4b" CANON_PATH = Path(file).resolve().parent / "story" / "canon.json" CANON = json.loads(CANON_PATH.read_text(encoding="utf-8"))
INTRO = ''' 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? '''
def generate_response(game): result = ollama.chat( model=MODEL_NAME, messages=build_messages(game, CANON), think=False, options=generation_options(game), ) return tidy(result.message.content)
def show_image(path, **kwargs): """Show an image, or a quiet caption if the file is missing (instead of crashing the app).""" if Path(path).exists(): st.image(path, **kwargs) else: st.caption(f"(image not found: {Path(path).name})") { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 518, 595 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -518,78 +518,78 @@ st.markdown( f'<div class="story">{room["description"]}</div>', unsafe_allow_html=True, )
with conversation_column: st.markdown("### What will you do?") with st.container(key="conversation_frame"): with st.container(height=480, border=True, autoscroll=True): for message in game["history"]: with st.chat_message( message["role"], avatar="🕯️" if message["role"] == "assistant" else "🧑", ): st.markdown(message["content"])
if game["ending"]:
st.markdown("---")
st.markdown("**THE END**")
if "ollama_error" in st.session_state:
st.error(st.session_state.ollama_error)
thinking_placeholder = st.empty()
action = st.chat_input(
"Describe what you do...",
disabled=bool(game["ending"]),
)
if action: snapshot = copy.deepcopy(game) # restore this if the model call fails game["history"].append({"role": "user", "content": action}) apply_action(game, action) # Python decides what actually happened
error_message = None
response = ""
try:
with thinking_placeholder.container():
with st.spinner("Thinking..."):
maybe_summarize(game, MODEL_NAME)
response = generate_response(game)
except ollama.ResponseError as error:
if error.status_code == 404:
error_message = (
f"Ollama could not find {MODEL_NAME}. "
f"Run `ollama pull {MODEL_NAME}` and try again."
)
else:
error_message = f"Ollama returned an error: {error.error}"
except ollama.RequestError as error:
error_message = (
"Could not connect to Ollama. Make sure Ollama is running, "
f"then try again. Details: {error}"
)
else:
if not response:
error_message = "Ollama returned an empty response. Please try again."
if error_message:
st.session_state.game = snapshot # undo the failed turn completely
st.session_state.ollama_error = error_message
else:
st.session_state.pop("ollama_error", None)
game["history"].append({"role": "assistant", "content": response})
st.rerun()
st.caption(f"Powered by Ollama · {MODEL_NAME}") { "command": "git status --short; Get-Content requirements.txt", "description": "Check current worktree and dependencies" } M README.md M the-house-that-remembers/app.py M the-house-that-remembers/prompt.py ollama streamlit Pillow <shellId: 210 completed with exit code 0> python *** Begin Patch *** Update File: the-house-that-remembers/app.py @@ import base64 import copy import json +from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError fro... diff --git a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/c:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -1,6 +1,7 @@ import base64 import copy import json +from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError from io import BytesIO from pathlib import Path
@@ -75,6 +76,11 @@ return tidy(result.message.content)
+def generate_turn_response(game):
- maybe_summarize(game, MODEL_NAME)
- return generate_response(game)
def show_image(path, **kwargs): """Show an image, or a quiet caption if the file is missing (instead of crashing the app).""" if Path(path).exists(): @@ -539,6 +545,7 @@ st.error(st.session_state.ollama_error)
thinking_placeholder = st.empty()
- slow_response_placeholder = st.empty() action = st.chat_input( "Describe what you do...", disabled=bool(game["ending"]), @@ -554,8 +561,16 @@ try: with thinking_placeholder.container(): with st.spinner("Thinking..."):
-
maybe_summarize(game, MODEL_NAME) -
response = generate_response(game)
-
with ThreadPoolExecutor(max_workers=1) as executor: -
future = executor.submit(generate_turn_response, game) -
try: -
response = future.result(timeout=30) -
except FutureTimeoutError: -
slow_response_placeholder.info( -
"Still working—this response is taking longer than usual, " -
"but the game is still processing." -
) -
response = future.result()except ollama.ResponseError as error: if error.status_code == 404: error_message = ( @@ -572,6 +587,8 @@ else: if not response: error_message = "Ollama returned an empty response. Please try again."
-
finally:
-
slow_response_placeholder.empty()if error_message: st.session_state.game = snapshot # undo the failed turn completely {"command":"python -m py_compile .\the-house-that-remembers\app.py; if ($?) { git diff --check -- .\the-house-that-remembers\app.py }","description":"Validate syntax and whitespace for the delayed... warning: in the working copy of 'the-house-that-remembers/app.py', LF will be replaced by CRLF the next time Git touches it <shellId: 211 completed with exit code 0> { "command": "Set-Location .\the-house-that-remembers; python -m unittest discover -s tests -p 'test_engine.py'", "description": "Run the focused game tests", "initial_wait": 120 } ..........................
Ran 26 tests in 0.010s
OK <shellId: 212 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 1, 95 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -1,95 +1,95 @@ import base64 import copy import json from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError from io import BytesIO from pathlib import Path
import ollama import streamlit as st from PIL import Image
from engine import ( FINAL_REVELATIONS, FRAGMENT_LABELS, FRAGMENT_KINDS, FRAGMENT_CLUES_NEEDED, ROOMS, apply_action, fragment_progress, fragments_complete, new_game, ) from prompt import build_messages, generation_options, maybe_summarize, tidy
st.set_page_config(page_title="The House That Remembers", layout="wide") st.markdown( "<link rel='stylesheet' " "href='https://cdn-uicons.flaticon.com/uicons-solid-straight/css/" "uicons-solid-straight.css'>", unsafe_allow_html=True, ) st.markdown( "<link rel='stylesheet' " "href='https://cdn-uicons.flaticon.com/uicons-bold-straight/css/" "uicons-bold-straight.css'>", unsafe_allow_html=True, )
MODEL_NAME = "gemma4:e4b" CANON_PATH = Path(file).resolve().parent / "story" / "canon.json" CANON = json.loads(CANON_PATH.read_text(encoding="utf-8"))
INTRO = ''' 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? '''
def generate_response(game): result = ollama.chat( model=MODEL_NAME, messages=build_messages(game, CANON), think=False, options=generation_options(game), ) return tidy(result.message.content)
def generate_turn_response(game): maybe_summarize(game, MODEL_NAME) return generate_response(game)
def show_image(path, **kwargs): """Show an image, or a quiet caption if the file is missing (instead of crashing the app).""" if Path(path).exists(): st.image(path, **kwargs) else: st.caption(f"(image not found: {Path(path).name})")
@st.cache_data(show_spinner=False) def haunted_house_banner(): image_path = Path(file).resolve().parent / "assets" / "haunted_house.jpg" if not image_path.exists(): { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\app.py", "view_range": [ 538, 600 ] } diff --git a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py index 0000000..0000000 100644 --- a/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py +++ b/C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers/app.py @@ -538,63 +538,63 @@ st.markdown(message["content"])
if game["ending"]:
st.markdown("---")
st.markdown("**THE END**")
if "ollama_error" in st.session_state:
st.error(st.session_state.ollama_error)
thinking_placeholder = st.empty()
slow_response_placeholder = st.empty()
action = st.chat_input(
"Describe what you do...",
disabled=bool(game["ending"]),
)
if action: snapshot = copy.deepcopy(game) # restore this if the model call fails game["history"].append({"role": "user", "content": action}) apply_action(game, action) # Python decides what actually happened
error_message = None
response = ""
try:
with thinking_placeholder.container():
with st.spinner("Thinking..."):
with ThreadPoolExecutor(max_workers=1) as executor:
future = executor.submit(generate_turn_response, game)
try:
response = future.result(timeout=30)
except FutureTimeoutError:
slow_response_placeholder.info(
"Still working—this response is taking longer than usual, "
"but the game is still processing."
)
response = future.result()
except ollama.ResponseError as error:
if error.status_code == 404:
error_message = (
f"Ollama could not find {MODEL_NAME}. "
f"Run `ollama pull {MODEL_NAME}` and try again."
)
else:
error_message = f"Ollama returned an error: {error.error}"
except ollama.RequestError as error:
error_message = (
"Could not connect to Ollama. Make sure Ollama is running, "
f"then try again. Details: {error}"
)
else:
if not response:
error_message = "Ollama returned an empty response. Please try again."
finally:
slow_response_placeholder.empty()
if error_message:
st.session_state.game = snapshot # undo the failed turn completely
st.session_state.ollama_error = error_message
else:
st.session_state.pop("ollama_error", None)
game["history"].append({"role": "assistant", "content": response})
st.rerun()
{"pattern":"bad|ending|truth_known|denial|leave_alone|leave_together|sister_bargain","paths":"C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers","glob":"engine.py","output... [grep content: 46 matches across 1 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
engine.py (46 match(es)): ... 5 more context line(s) omitted in this file 20: DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door ... 1 more context line(s) omitted in this file 22: SUPPORT_LIMIT = 3 # times kindness can lower his denial 23: MAX_TURNS = 90 # unresolved truth or denial by then -> "denial" ending ... 10 more context line(s) omitted in this file 345: "sister_bargain_available": False, 346: "sister_bargain_heard": False, ... 2 more context line(s) omitted in this file 349: "denial": 8, # friend's denial (high = deflecting) ... 3 more context line(s) omitted in this file 353: "truth_known": False, # whether the player has learned the truth about the house ... 2 more context line(s) omitted in this file 356: "ending": None, # None or an ending id ... 10 more context line(s) omitted in this file 485: def _end(game, ending, detail): 486: game["ending"] = ending 487: return _result("ending", detail) ... 10 more context line(s) omitted in this file 641: if game["denial"] > DOOR_DENIAL_MAX: ... 22 more match(es) and 139 context line(s) omitted in this file 995: result = _end(game, "denial", "Too much time has passed. The house closes around the friend.") ... 10 more context line(s) omitted in this file 1006: and not game["ending"] 1007: and not game["truth_known"] ... 10 more context line(s) omitted in this file 1021: game["denial"] = max(0, game["denial"] - 1) ... 2 more context line(s) omitted in this file 1024: game["truth_known"] 1025: and game["denial"] < 3 ... 2 more context line(s) omitted in this file 1028: and result["outcome"] not in {"blocked", "ending"} ... 10 more context line(s) omitted in this file 1040: if game["stage"] >= 3 and sister_due and not game["ending"]: ... 2 more context line(s) omitted in this file 1043: and not game["sister_bargain_heard"] ... 8 more context line(s) omitted in this file 1052: game["sister_bargain_available"] = True ... 10 more context line(s) omitted in this file 1071: if speaking_with_sister and result["outcome"] not in {"ending", "blocked"}: ... 2 more context line(s) omitted in this file 1074: game["sister_bargain_heard"] = True ... 5 more context line(s) omitted in this file {"pattern":"bad ending|bad_ending|ending.*alone|ending.bargain|Ending","paths":"C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story","glob":".json","output_mode":"c... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json-173- "key_clues": "11 fragment clues across three threads: Past (4), Present (4), and Echoes (3). Four additional discoveries provide atmosphere or optional lore but do not count toward fragment completion.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json-174- "puzzles": "1: the parents' bedroom door", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json-175- "major_ghosts": 1, C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:176: "endings": "4 implemented (leave_together, escape_alone, denial, sisters_bargain)" C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json-177- }, C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json-178- "player_agency": [ C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json-179- "The player can investigate, talk, examine objects, rest, follow or refuse warnings, and leave when possible.", #
C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json-260- "door": "The bedroom door opens only if all three fragments are complete and denial is at or below 3. Opening it moves the story to the final stage.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json-261- "family_revelations": "After the player enters the bedroom, face-to-face conversation shares four authored family memories one at a time when denial is below 3: the mother's decline and the family's deaths, the father's attempt to preserve them, the sister's hope, and the friend's conflict and silence.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json-262- "together_ending": "The friend can leave together only after denial is below 3 and all four family memories have been shared.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json:263: "sister_bargain": "After the sister has warned the player at least twice, when the cooldown passes and the friend is elsewhere, she may privately explain her belief that preserving the family keeps them alive. She remains for five conversation turns, then vanishes; the player can still explicitly accept the bargain after she leaves, while the friend is elsewhere. This conversation and ending may happen before or after the bedroom is opened. The player leaves alone with her partial truth.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json-264- "pushing": "Three blunt confrontations before the evidence ends the story in the denial ending.", C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json-265- "clock": "If the truth has not been faced after about 90 turns, the denial ending triggers." C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\story\canon.json-266- } { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 1, 35 ] } 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 @@ -1,35 +1,35 @@ """engine.py - authoritative game state and rules.
Nothing in here calls the model. The model only ever sees the result of what this module decides (see prompt.py). """ import re from pprint import pprint
AMBIENT_AFTER_TURNS = 3 # turns spent in a room before a detail may surface AMBIENT_COOLDOWN = 3 # minimum turns between any two noticed details FRAGMENT_CLUES_NEEDED = 2 # clues of one kind needed to complete a memory fragment FRAGMENT_KINDS = ("past", "present", "echoes") FRAGMENT_LABELS = { "past": "Past", "present": "Present", "echoes": "Echoes", } DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door PUSH_LIMIT = 3 # blunt confrontations before the evidence -> he shuts down SUPPORT_LIMIT = 3 # times kindness can lower his denial MAX_TURNS = 90 # unresolved truth or denial by then -> "denial" ending NUDGE_AFTER_TURNS = 4 NUDGE_COOLDOWN = 3 SISTER_COOLDOWN = 12 SISTER_CONVERSATION_LIMIT = 5
FINAL_REVELATIONS = ( { "id": "mother", "title": "His mother's decline", "detail": ( "They are dead. The house drained them, beginning with his mother, who " "grew exhausted and unable to sleep and sensed something was wrong. The " { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 475, 710 ] } 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 @@ -475,236 +475,236 @@ return text
def _find_feature(text, room): for name, feat in room.get("features", {}).items(): if _mentions(text, feat["keywords"]): return name, feat return None
def _end(game, ending, detail): game["ending"] = ending return _result("ending", detail)
def _classify_intent(raw_action, game): """Classify what the player is doing so narration responds appropriately.""" action_text = QUOTED_TEXT_RE.sub("", raw_action).lower() action_text = _remove_inventory_mentions(action_text, game["inventory"]) room = ROOMS[game["room"]]
if MEMORY_RE.search(raw_action.lower()):
return "memory"
if LOOK_AROUND_RE.search(action_text):
return "exploration"
if EXAMINE_RE.search(action_text) and _find_feature(action_text, room):
return "examination"
has_quoted_speech = bool(QUOTED_TEXT_RE.search(raw_action))
has_speech = (
has_quoted_speech
or bool(SPEECH_RE.search(action_text))
or bool(SUPPORT_RE.search(action_text))
)
has_physical_action = bool(PHYSICAL_ACTION_RE.search(action_text))
if has_speech and has_physical_action:
return "action_and_dialogue"
if has_speech or raw_action.strip().endswith("?"):
return "dialogue"
if has_physical_action:
return "physical_action"
return "roleplay"
def _result(outcome, detail, new_clue=None, ambient=None, friend=None): return { "nudge": None, # gentle push to move on (set by apply_action) "outcome": outcome, "detail": detail, "new_clue": new_clue, "ambient": ambient, # small detail to weave into a quiet turn "friend": friend, # optional hint for how the friend reacts }
def _examine(game, room_name, name, feat): """Record one examination of a feature. Returns (what they notice, new clue(s) or None).""" key = f"{room_name}:{name}" n = game["examined"].get(key, 0) + 1 game["examined"][key] = n looks = feat["looks"] noticed = looks[min(n - 1, len(looks) - 1)] found = [] for text, on_n, _frag in feat.get("clues", []): if n == on_n and text not in game["clues"]: game["clues"].append(text) found.append(text) return noticed, ("; ".join(found) if found else None)
def _next_unnoticed(game, room_name): """First ambient feature in this room the player hasn't encountered yet.""" for name, feat in ROOMS[room_name].get("features", {}).items(): if feat.get("ambient") and game["examined"].get(f"{room_name}:{name}", 0) == 0: return name, feat return None
def _unfinished(game, room_name): """A room still worth visiting: never visited, or details left to notice.""" if room_name == "Parents' Bedroom": return False return room_name not in game["visited"] or _next_unnoticed(game, room_name) is not 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, [])]) while queue: 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) route = [*path, nxt] if goal_fn(nxt): 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"]
if (room_name == "Upper Landing" and not game["door_open"]
and len(fragments_complete(game)) >= 3):
return (
"Everything here has been found, but the friend is not ready for the door. "
"Let him hesitate, almost speak, and look to the player. Patience and kindness may "
"help him. Do not open the door."
)
hit = _path_step(game, room_name, lambda r: _unfinished(game, r))
if not hit and not game["door_open"] and room_name != "Upper Landing":
hit = _path_step(game, room_name, lambda r: r == "Upper Landing")
if not hit:
return None
step, target = hit
where = f"the {step}" + (f", on the way to the {target}" if step != target else "")
level = min(game["nudges_here"] + 1, 3)
if level == 1:
return (
"There is nothing more to find here for now. Let the friend gently suggest moving on, "
f"toward {where}. The move happens only if the player chooses it."
)
if level == 2:
return (
"Still nothing new here. Let a faint sound or a change in the light from the direction "
f"of {where} draw attention that way. Do not say what it is. "
"The move happens only if the player chooses it."
)
return (
"The player has lingered. Let the friend drift toward the exit that leads to "
f"{where}, stop there, and look back. The player is still in the {room_name} "
"until they choose to move."
)
def _try_door(game): if game["door_open"]: return _result("no_change", "The bedroom door already stands open.") if len(fragments_complete(game)) < 3: return _door_attempt_with_knocking( game, _result( "blocked", "The door will not give. It is not only locked: pushing at it feels like pushing " "against something holding its breath. The friend begs the player to leave it. " "The player does not yet understand enough to know why it matters.", friend="He is frightened and pleads with the player to step away.", ), ) if game["denial"] > DOOR_DENIAL_MAX: return _door_attempt_with_knocking( game, _result( "blocked", "The door stays shut. The friend's hand closes on the player's arm. He is not ready, " "and the door will not open until he is.", friend="He is trembling and cannot let the player open it yet.", ), ) game["door_open"] = True return _result( "door_opened", "The door opens at last, without resistance, as if it had only ever been waiting. " "Cold, still air spills out. The friend stands frozen beside the player. " "The player has not yet gone in.", )
def _record_door_knocking(game): door = ROOMS["Upper Landing"]["features"]["door"] clue = next( text for text, _examination_number, fragment in door["clues"] if fragment == "echoes" and "knocking" in text.lower() ) if clue in game["clues"]: return None game["clues"].append(clue) return clue
def _door_attempt_with_knocking(game, result): clue = _record_door_knocking(game) if clue: result["new_clue"] = clue result["detail"] += " As the player tests it, three slow knocks answer from within." return result
def _confront(game): if len(fragments_complete(game)) >= 3: game["denial"] = max(0, game["denial"] - 1) return _result( "no_change", "The player says aloud what the evidence points to. This time the friend does not " "deny it outright: he wavers, almost admits it, then stops, voice cracking. " "Nothing else changes.", friend="He is on the edge of breaking.", ) game["pushed"] += 1 game["denial"] = min(10, game["denial"] + 1) if game["pushed"] >= PUSH_LIMIT: return { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 960, 1035 ] } 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 @@ -960,76 +960,76 @@ if result["outcome"] != "moved": game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["support_given"] < SUPPORT_LIMIT
and SUPPORT_RE.search(text)
):
game["support_given"] += 1
game["denial"] = max(0, game["denial"] - 1)
result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
if (
not game["ending"]
and game["turn_count"] >= MAX_TURNS
and (not game["truth_known"] or game["denial"] >= 3)
):
result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
if fresh:
game["last_notice_turn"] = game["turn_count"]
if (
not fresh
and result["outcome"] in ("no_change", "examined")
and not game["ending"]
and not game["truth_known"]
and game["room_turns"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= NUDGE_AFTER_TURNS
and game["turn_count"] - game["last_nudge_turn"] >= NUDGE_COOLDOWN
and _next_unnoticed(game, game["room"]) is None
):
nudge = _nudge(game)
if nudge:
result["nudge"] = nudge
game["last_nudge_turn"] = game["turn_count"]
game["nudges_here"] += 1
if result["new_clue"]:
for _ in result["new_clue"].split("; "):
game["denial"] = max(0, game["denial"] - 1)
if (
game["truth_known"]
and game["denial"] < 3
and game["friend_room"] == game["room"]
and intent in {"dialogue", "action_and_dialogue"}
and result["outcome"] not in {"blocked", "ending"}
and len(game["final_revelations"]) < len(FINAL_REVELATIONS)
):
revelation = FINAL_REVELATIONS[len(game["final_revelations"])]
game["final_revelations"].append(revelation["id"])
result["family_memory"] = revelation
result["stage_advanced"] = _advance_stage(game)
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 1035, 1095 ] } 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 @@ -1035,61 +1035,61 @@ result["stage_advanced"] = _advance_stage(game) sister_due = ( not game["sister_seen"] or game["turn_count"] - game["last_sister_turn"] >= SISTER_COOLDOWN ) if game["stage"] >= 3 and sister_due and not game["ending"]: can_speak_privately = ( game["sister_warning_count"] >= 2 and not game["sister_bargain_heard"] and game["friend_room"] != game["room"] ) if can_speak_privately: appearance = ( "The sister appears again, but this time she does not repeat her " "warning. With the friend elsewhere, she looks directly at the player " "and seems willing to explain why she wants the family left undisturbed." ) game["sister_bargain_available"] = True game["sister_present"] = True game["sister_conversation_turns"] = 0 else: appearance = ( "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." if not game["sister_seen"] else "The friend's sister appears briefly again and quietly repeats her " "warning to leave. Then she is gone." ) game["sister_warning_count"] = min(2, game["sister_warning_count"] + 1) game["sister_present"] = False game["sister_seen"] = True game["last_sister_turn"] = game["turn_count"] result["sister"] = appearance result["sister_private_encounter"] = can_speak_privately
if speaking_with_sister and result["outcome"] not in {"ending", "blocked"}:
result["sister_conversation"] = True
if SISTER_PERSPECTIVE_RE.search(raw_action.lower()):
game["sister_bargain_heard"] = True
result["sister_perspective"] = SISTER_PRESERVATION_MEMORY
game["sister_conversation_turns"] += 1
if game["sister_conversation_turns"] >= SISTER_CONVERSATION_LIMIT:
game["sister_present"] = False
result["sister_departed"] = True
game["last_result"] = result
print("Game state:")
pprint(game, sort_dicts=False)
return result
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 690, 750 ] } 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 @@ -690,61 +690,61 @@ game["pushed"] += 1 game["denial"] = min(10, game["denial"] + 1) if game["pushed"] >= PUSH_LIMIT: return _end( game, "denial", "The player pushed too hard, too soon. The friend shuts down completely.", ) warn = " He is close to shutting down." if game["pushed"] == PUSH_LIMIT - 1 else "" return _result( "no_change", "The player bluntly says the friend's family is dead before having any evidence. " "The friend flatly denies it, hurt and frightened, and changes the subject." + warn + " Nothing in the house changes.", )
def _resolve(game, text): intent = _classify_intent(text, game) spoken = text # full text, quoted speech included text = QUOTED_TEXT_RE.sub("", text).lower() # actions only: speech can't move or examine things room_name = game["room"] room = ROOMS[room_name] room_parse_text = _remove_inventory_mentions(text, game["inventory"]) rooms_named = _find_rooms(room_parse_text) negated = bool(NEGATION_RE.search(text)) spoken_negated = bool(NEGATION_RE.search(spoken))
if (
game.get("sister_bargain_available")
and game["friend_room"] != game["room"]
and not spoken_negated
and SISTER_ACCEPT_RE.search(spoken)
):
if not game.get("sister_bargain_heard"):
return _result(
"blocked",
"The sister has not explained her bargain yet. The player cannot "
"accept it before hearing what she believes.",
)
return _end(
game,
"sisters_bargain",
"The player accepts the sister's bargain and chooses to leave alone, "
"carrying her partial account of the family's fate.",
)
if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
if game["denial"] >= 3:
return _result(
"blocked",
"The friend is not ready to leave together yet. He has seen the truth "
"but is still resisting it; do not make him agree or move toward the exit.",
)
if len(game.get("final_revelations", [])) < len(FINAL_REVELATIONS):
return _result(
"blocked",
"The friend is not ready to leave together yet. There are still family "
"memories he has not been able to share; do not make him agree or end "
"the story now.",
{"pattern":"_end\(game,|escape_alone|leave_together|sisters_bargain|escape","paths":"C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py","glob":"engine.py","out... [grep content: 92 matches across 1 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
engine.py (92 match(es)):
400- # ---------------------------------------------------------------------------
401- MOVE_VERBS = ["go", "enter", "head", "walk", "move", "step", "climb", "ascend",
402- "descend", "return", "run", "visit", "follow", "proceed"]
403- TAKE_VERBS = ["take", "grab", "pick", "pocket", "steal"]
404-
405- LEAVE_RE = re.compile(
406: r"\b(leave|exit|flee|escape)\b.\b(house|mansion|manor)\b"
407- r"|\bgo outside\b|\bwalk out\b"
408- )
409- NEGATION_RE = re.compile(r"\b(not|never|without)\b|n't\b|\bwont\b|\bdont\b")
410- TOGETHER_RE = re.compile(
411- r"\b(leave|go|walk|get out|come|take him|lead him)\b.\b(together|with me|with him)\b"
412- r"|\b(together|with me)\b.*\b(leave|go|out)\b"
458- r"|\b(?:it'?s|it is) going to be (?:ok|okay|alright)\b"
459- r"|\b(?:i want to help|i can help|let me help|i'?d like to help)\b"
460- r"|\b(?:take my hand|give me your hand)\b"
461- )
462-
463- def _mentions(text, words):
464: return any(re.search(rf"\b{re.escape(w)}\b", text) for w in words)
465-
466-
467- def _find_rooms(text):
468- return [name for name, r in ROOMS.items() if _mentions(text, r["aliases"])]
469-
470-
471- def _remove_inventory_mentions(text, inventory):
472- """Keep room names inside carried item names from becoming movement targets."""
473- for item in sorted(inventory, key=len, reverse=True):
474: text = re.sub(rf"\b{re.escape(item.lower())}\b", " ", text)
475- return text
476-
477-
478- def _find_feature(text, room):
479- for name, feat in room.get("features", {}).items():
480- if _mentions(text, feat["keywords"]):
481- return name, feat
482- return None
483-
484-
485: def _end(game, ending, detail):
486- game["ending"] = ending
487- return _result("ending", detail)
488-
489-
490- def _classify_intent(raw_action, game):
491- """Classify what the player is doing so narration responds appropriately."""
725- "blocked",
726- "The sister has not explained her bargain yet. The player cannot "
727- "accept it before hearing what she believes.",
728- )
729- return _end(
730- game,
731: "sisters_bargain",
732- "The player accepts the sister's bargain and chooses to leave alone, "
733- "carrying her partial account of the family's fate.",
734- )
735-
736- # --- leaving (irreversible, so be strict) -------------------------------
737- # "Let's leave together" is something the player says, so quoted speech counts.
748- "The friend is not ready to leave together yet. There are still family "
749- "memories he has not been able to share; do not make him agree or end "
750- "the story now.",
751- )
752- return _end(
753- game,
754: "leave_together",
755- "The player and the friend choose to leave the house together.",
756- )
757-
758- if LEAVE_RE.search(text) and not negated:
759- if room_name == "Entrance Hall":
760: return _end(game, "escape_alone", "The player walks out of the house alone.")
761- return _result("blocked", "The way out of the house is back through the Entrance Hall.")
762-
763- # --- the bedroom door -----------------------------------------------------
764- if room_name == "Upper Landing" and not negated:
765- if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
766- return _try_door(game)
989- # Time runs out if the truth was never faced or the friend remains in denial.
990- if (
991- not game["ending"]
992- and game["turn_count"] >= MAX_TURNS
993- and (not game["truth_known"] or game["denial"] >= 3)
994- ):
995: result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
996-
997- # "Fresh" = the player met something new this turn (first look at a feature, or a clue).
998- fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
999- if fresh:
1000- game["last_notice_turn"] = game["turn_count"]
1001-
{
"path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py",
"view_range": [
930,
980
]
}
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
@@ -930,51 +930,51 @@
and game["friend_room"] != game["room"]
)
seen_before = len(game["examined"]) # measured BEFORE resolving, so first looks count as fresh
result = _resolve(game, text)
if result["outcome"] == "moved":
intent = "movement"
game["sister_present"] = False
elif result["outcome"] == "picked_up":
intent = "physical_action"
elif result["outcome"] == "examined" and intent not in {
"examination",
"exploration",
}:
intent = "examination"
result["intent"] = intent
if (
intent in {"dialogue", "action_and_dialogue"}
and not game["ending"]
and game["friend_room"] != game["room"]
and not speaking_with_sister
):
game["friend_room"] = game["room"]
result["friend_arrived"] = True
result["detail"] += (
f" The friend joins the player in the {game['room']} so they can speak "
"face to face."
)
game["turn_count"] += 1
game["turns_in_stage"] += 1
if result["outcome"] != "moved":
game["room_turns"] += 1
if (
result["outcome"] == "no_change"
and not game["ending"]
and game["room_turns"] >= AMBIENT_AFTER_TURNS
and game["turn_count"] - game["last_notice_turn"] >= AMBIENT_COOLDOWN
):
nxt = _next_unnoticed(game, game["room"])
if nxt:
name, feat = nxt
noticed, new_clue = _examine(game, game["room"], name, feat)
result["ambient"] = noticed
result["new_clue"] = new_clue
result["friend"] = result["friend"] or feat.get("friend")
if (
result["outcome"] == "no_change"
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 736, 765 ] } 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 @@ -736,30 +736,30 @@ if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken): if game["denial"] >= 3: return _result( "blocked", "The friend is not ready to leave together yet. He has seen the truth " "but is still resisting it; do not make him agree or move toward the exit.", ) if len(game.get("final_revelations", [])) < len(FINAL_REVELATIONS): return _result( "blocked", "The friend is not ready to leave together yet. There are still family " "memories he has not been able to share; do not make him agree or end " "the story now.", ) return _end( game, "leave_together", "The player and the friend choose to leave the house together.", )
if LEAVE_RE.search(text) and not negated:
if room_name == "Entrance Hall":
return _end(game, "escape_alone", "The player walks out of the house alone.")
return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
denial ending triggers
{"pattern":"def _confront|if game\["pushed"\] >= PUSH_LIMIT|game\["turn_count"\] >= MAX_TURNS|truth_known.*denial","paths":"C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that... [grep content: 4 matches across 1 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
engine.py (4 match(es)):
675- result["new_clue"] = clue
676- result["detail"] += " As the player tests it, three slow knocks answer from within."
677- return result
678-
679-
680: def _confront(game):
681- if len(fragments_complete(game)) >= 3:
682- game["denial"] = max(0, game["denial"] - 1)
683- return _result(
684- "no_change",
685- "The player says aloud what the evidence points to. This time the friend does not "
687- "Nothing else changes.",
688- friend="He is on the edge of breaking.",
689- )
690- game["pushed"] += 1
691- game["denial"] = min(10, game["denial"] + 1)
692: if game["pushed"] >= PUSH_LIMIT:
693- return _end(
694- game, "denial",
695- "The player pushed too hard, too soon. The friend shuts down completely.",
696- )
697- warn = " He is close to shutting down." if game["pushed"] == PUSH_LIMIT - 1 else ""
987- result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
988-
989- # Time runs out if the truth was never faced or the friend remains in denial.
990- if (
991- not game["ending"]
992: and game["turn_count"] >= MAX_TURNS
993: and (not game["truth_known"] or game["denial"] >= 3)
994- ):
995- result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
996-
997- # "Fresh" = the player met something new this turn (first look at a feature, or a clue).
998- fresh = len(game["examined"]) > seen_before or bool(result["new_clue"])
The **denial ending** triggers in either of these cases:
{"pattern":"denial.\+|denial.=|denial"\]","paths":"C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py","glob":"engine.py","output_mode":"content","-n":true,... [grep content: 9 matches across 1 file(s) under C:/Users/lucia/Documents/coding stuff/hacktober/the-house-that-remembers]
engine.py (9 match(es)):
346- "sister_bargain_heard": False,
347- "sister_present": False,
348- "sister_conversation_turns": 0,
349: "denial": 8, # friend's denial (high = deflecting)
350- "pushed": 0,
351- "support_given": 0,
352- "door_open": False, # whether the parents' bedroom door is open
638- friend="He is frightened and pleads with the player to step away.",
639- ),
640- )
641: if game["denial"] > DOOR_DENIAL_MAX:
642- return _door_attempt_with_knocking(
643- game,
644- _result(
679-
680- def _confront(game):
681- if len(fragments_complete(game)) >= 3:
682: game["denial"] = max(0, game["denial"] - 1)
683- return _result(
684- "no_change",
685- "The player says aloud what the evidence points to. This time the friend does not "
688- friend="He is on the edge of breaking.",
689- )
690- game["pushed"] += 1
691: game["denial"] = min(10, game["denial"] + 1)
692- if game["pushed"] >= PUSH_LIMIT:
693- return _end(
694- game, "denial",
736- # --- leaving (irreversible, so be strict) -------------------------------
737- # "Let's leave together" is something the player says, so quoted speech counts.
738- if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
739: if game["denial"] >= 3:
740- return _result(
741- "blocked",
742- "The friend is not ready to leave together yet. He has seen the truth "
983- and SUPPORT_RE.search(text)
984- ):
985- game["support_given"] += 1
986: game["denial"] = max(0, game["denial"] - 1)
987- result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
988-
989- # Time runs out if the truth was never faced or the friend remains in denial.
990- if (
991- not game["ending"]
992- and game["turn_count"] >= MAX_TURNS
993: and (not game["truth_known"] or game["denial"] >= 3)
994- ):
995- result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
996-
1018-
1019- if result["new_clue"]:
1020- for _ in result["new_clue"].split("; "):
1021: game["denial"] = max(0, game["denial"] - 1)
1022-
1023- if (
1024- game["truth_known"]
1025: and game["denial"] < 3
1026- and game["friend_room"] == game["room"]
1027- and intent in {"dialogue", "action_and_dialogue"}
1028- and result["outcome"] not in {"blocked", "ending"}
{"pattern":"PUSH|CONFRONT|support_given|denial","paths":"C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py","glob":"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)):
18- "echoes": "Echoes",
19- }
20: DOOR_DENIAL_MAX = 3 # friend's denial must be at or below this to open the door
21: PUSH_LIMIT = 3 # blunt confrontations before the evidence -> he shuts down
22: SUPPORT_LIMIT = 3 # times kindness can lower his denial
23: MAX_TURNS = 90 # unresolved truth or denial by then -> "denial" ending
24- NUDGE_AFTER_TURNS = 4
25- NUDGE_COOLDOWN = 3
347- "sister_present": False,
348- "sister_conversation_turns": 0,
349: "denial": 8, # friend's denial (high = deflecting)
350- "pushed": 0,
351: "support_given": 0,
352- "door_open": False, # whether the parents' bedroom door is open
353- "truth_known": False, # whether the player has learned the truth about the house
449- DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
450-
451: CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b")
452- FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b")
453- SUPPORT_RE = re.compile(
639- ),
640- )
641: if game["denial"] > DOOR_DENIAL_MAX:
642- return _door_attempt_with_knocking(
643- game,
680- def _confront(game):
681- if len(fragments_complete(game)) >= 3:
682: game["denial"] = max(0, game["denial"] - 1)
683- return _result(
684- "no_change",
689- )
690- game["pushed"] += 1
691: game["denial"] = min(10, game["denial"] + 1)
692: if game["pushed"] >= PUSH_LIMIT:
693- return _end(
694: game, "denial",
695- "The player pushed too hard, too soon. The friend shuts down completely.",
696- )
697: warn = " He is close to shutting down." if game["pushed"] == PUSH_LIMIT - 1 else ""
698- return _result(
699- "no_change",
737- # "Let's leave together" is something the player says, so quoted speech counts.
738- if game["truth_known"] and not spoken_negated and TOGETHER_RE.search(spoken):
739: if game["denial"] >= 3:
740- return _result(
741- "blocked",
768- # --- blunt confrontation about the family ---------------------------------
769- # Confronting him is also said aloud, so quoted speech counts here too.
770: if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
771- return _confront(game)
772-
980- result["outcome"] == "no_change"
981- and not game["ending"]
982: and game["support_given"] < SUPPORT_LIMIT
983- and SUPPORT_RE.search(text)
984- ):
985: game["support_given"] += 1
986: game["denial"] = max(0, game["denial"] - 1)
987- result["friend"] = result["friend"] or "He softens a little at the kindness; his guard lowers for a moment."
988-
989: # Time runs out if the truth was never faced or the friend remains in denial.
990- if (
991- not game["ending"]
992- and game["turn_count"] >= MAX_TURNS
993: and (not game["truth_known"] or game["denial"] >= 3)
994- ):
995: result = _end(game, "denial", "Too much time has passed. The house closes around the friend.")
996-
997- # "Fresh" = the player met something new this turn (first look at a feature, or a clue).
1019- if result["new_clue"]:
1020- for _ in result["new_clue"].split("; "):
1021: game["denial"] = max(0, game["denial"] - 1)
1022-
1023- if (
1024- game["truth_known"]
1025: and game["denial"] < 3
1026- and game["friend_room"] == game["room"]
1027- and intent in {"dialogue", "action_and_dialogue"}
"Is your family dead?"
🕯️
He appears at the base of the stairs shortly after your question, stopping near where you stand on the landing. His face tightens with a visible hurt beneath his exhaustion. "No," he says quickly, shaking his head slightly. "They're... they're fine. Everything is fine."
🧑
"Your sister is dead"
🕯️
He flinches visibly at your words, a flicker of genuine panic crossing his features. "No," he repeats, voice low and strained. "That's not... that's not true. What are you talking about? How was your trip here?"
🧑
I know you let your family die
🕯️
He avoids your eyes, his gaze darting past you toward the landing railing. The portrait hanging above seems even more askew now; the faces within are difficult to make out in the dim light. "You shouldn't say things like that," he murmurs, shifting his weight uneasily. "We should just... talk about something else."
🧑
they bodies are in the bedroom
🕯️
He closes his eyes briefly, drawing a ragged breath against his pale chest. "Don't say that," he whispers, sounding utterly defeated. He looks down at the worn pattern of the carpet beneath your feet, unable to meet your gaze again.
The transcript shows why the ending didn’t trigger: the regex misses “die” in “let your family die,” and the last accusation says “bodies” without a family term. I’ll check the existing confrontation tests and make those natural phrasings count.
{"pattern":"confront|pushed|denial_ending|denial.increase","paths":"C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests","glob":".py","output_mode":"content","-n":t... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py-335- C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py-336- self.assertEqual(game["denial"], 2) C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py-337- self.assertIn("share the story gradually", stage_block(game)) C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py-338- C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py:339: def test_unresolved_denial_can_still_trigger_the_denial_ending(self): C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py-340- game = engine.new_game() C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py-341- game.update(truth_known=True, denial=3, turn_count=engine.MAX_TURNS - 1) C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py-342- C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py-343- result = act(game, "I wait quietly.") { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 400, 465 ] } 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 @@ -400,66 +400,66 @@ MOVE_VERBS = ["go", "enter", "head", "walk", "move", "step", "climb", "ascend", "descend", "return", "run", "visit", "follow", "proceed"] TAKE_VERBS = ["take", "grab", "pick", "pocket", "steal"]
LEAVE_RE = re.compile( r"\b(leave|exit|flee|escape)\b.\b(house|mansion|manor)\b" r"|\bgo outside\b|\bwalk out\b" ) NEGATION_RE = re.compile(r"\b(not|never|without)\b|n't\b|\bwont\b|\bdont\b") TOGETHER_RE = re.compile( r"\b(leave|go|walk|get out|come|take him|lead him)\b.\b(together|with me|with him)\b" r"|\b(together|with me)\b.*\b(leave|go|out)\b" r"|\blet'?s (leave|get out)\b" ) SISTER_ACCEPT_RE = re.compile( r"\b(?:i accept|i agree to (?:your|the) (?:offer|bargain)|" r"i choose (?:to accept|your offer|the offer|your bargain|the bargain)|" r"i'll do it|i will do it|i want them to stay|" r"i want to keep them as they are|i want to preserve them)\b" ) SISTER_PERSPECTIVE_RE = re.compile( r"\b(why|what|how|who|where|when|do you|are you|can you|could you|" r"tell me|explain|your (side|belief|perspective))\b|?" )
LOOK_AROUND_RE = re.compile( r"\b(look|glance|scan|survey|search|explore|examine|inspect|study)\w*\b.\b(around|about|room|surroundings|area)\b" r"|\b(examine|inspect|search|study|explore|look)\w\b.\b(kitchen|library|hall|staircase|landing|bedroom)\b" r"|\bwhat (do|can) i (see|notice|find)\b|\bnotice anything\b" ) QUOTED_TEXT_RE = re.compile(r'"[^"]"|“[^”]”|‘[^’]’') MEMORY_RE = re.compile( r"\b(remember|recall|used to|when we were (?:kids|children)|" r"back when we|childhood)\b" ) EXAMINE_RE = re.compile( r"\b(examine|inspect|study|look at|check|read|listen to|smell|touch)\b" ) PHYSICAL_ACTION_RE = re.compile( r"\b(?:i\s+)?(?:pat|touch|sit|stand|reach|hold|hug|tap|point|" r"turn|open|close|pick|take|walk|step|move|climb|enter|follow)\b" ) SPEECH_RE = re.compile( r"\b(?:i\s+)?(?:say|ask|tell|reply|answer|whisper|speak|call)\b" r"|\b(?:ask|tell) (?:him|her|your friend)\b" ) DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b") DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b") FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b") SUPPORT_RE = re.compile( r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b" r"|\bit'?s (ok|okay|alright)\b" r"|\b(?:we can (?:do (?:it|this)|face (?:it|this)|get through (?:it|this))" r"|we(?:'ll| will) face (?:it|this)) together\b" r"|\b(?:it'?s|it is) going to be (?:ok|okay|alright)\b" r"|\b(?:i want to help|i can help|let me help|i'?d like to help)\b" r"|\b(?:take my hand|give me your hand)\b" )
def _mentions(text, words): return any(re.search(rf"\b{re.escape(w)}\b", text) for w in words) { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 755, 778 ] } 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 @@ -755,24 +755,24 @@ "The player and the friend choose to leave the house together.", )
if LEAVE_RE.search(text) and not negated:
if room_name == "Entrance Hall":
return _end(game, "escape_alone", "The player walks out of the house alone.")
return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
if not target:
if re.search(r"\bback\b", text) and room_name in BACK:
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", "view_range": [ 1, 60 ] } 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 @@ -1,60 +1,60 @@ import contextlib import io import json import unittest from collections import Counter from pathlib import Path
import engine from prompt import CORE, _engine_note, stage_block, 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_supportive_phrases_increase_support(self): phrases = [ "We can do it together.", "We can face this together.", "It's going to be okay.", "It is going to be alright.", "I want to help.", "I can help.", "Take my hand.", "Give me your hand.", ]
for phrase in phrases:
with self.subTest(phrase=phrase):
game = engine.new_game()
act(game, phrase)
self.assertEqual(game["support_given"], 1)
self.assertEqual(game["denial"], 7)
def test_supportive_dialogue_brings_remote_friend_to_player(self):
game = engine.new_game()
game["room"] = "Upper Landing"
game["friend_room"] = "Library"
result = act(game, "I can help.")
self.assertEqual(result["intent"], "dialogue")
self.assertTrue(result["friend_arrived"])
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertEqual(game["support_given"], 1)
def test_engine_note_requires_new_clue_to_be_narrated_during_dialogue(self):
game = engine.new_game()
game["last_result"] = {
*** Begin Patch *** Update File: the-house-that-remembers/engine.py @@ -CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b") +CONFRONT_RE = re.compile(r"\b(dead|die|died|dies|d... 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 @@ -448,8 +448,9 @@ DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b") DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
-CONFRONT_RE = re.compile(r"\b(dead|died|dies|death|corpses?|bodies|body)\b") +CONFRONT_RE = re.compile(r"\b(dead|die|died|dies|death|corpses?|bodies|body)\b") FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b") +FAMILY_BODIES_RE = re.compile(r"\b(their|they)\s+bodies?\b") SUPPORT_RE = re.compile( r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b" r"|\bit'?s (ok|okay|alright)\b" @@ -767,7 +768,8 @@
- if (not game["truth_known"] and CONFRONT_RE.search(spoken) and FAMILY_RE.search(spoken)):
- refers_to_family = FAMILY_RE.search(spoken) or FAMILY_BODIES_RE.search(spoken)
- if not game["truth_known"] and CONFRONT_RE.search(spoken) and refers_to_family: return _confront(game)
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 @@ -55,6 +55,27 @@ self.assertEqual(game["friend_room"], "Upper Landing") self.assertEqual(game["support_given"], 1)
- def test_denial_ending_counts_die_and_family_bodies_confrontations(self):
-
game = engine.new_game() -
first = act(game, "Is your family dead?") -
second = act(game, "Your sister is dead.") -
third = act(game, "I know you let your family die.") -
self.assertNotEqual(first["outcome"], "ending") -
self.assertNotEqual(second["outcome"], "ending") -
self.assertEqual(third["outcome"], "ending") -
self.assertEqual(game["ending"], "denial") - def test_accusing_friend_about_their_bodies_counts_as_confrontation(self):
-
game = engine.new_game() -
result = act(game, "Their bodies are in the bedroom.") -
self.assertEqual(game["pushed"], 1) -
self.assertEqual(game["denial"], 9) -
self.assertNotEqual(result["outcome"], "ending") - def test_engine_note_requires_new_clue_to_be_narrated_during_dialogue(self): game = engine.new_game() game["last_result"] = {
I’ve updated the confrontation matching so “die” is recognized and a direct “their/they bodies” accusation counts without requiring an explicit “family” word. I’m verifying those exact phrases now.
*** Begin Patch *** Update File: the-house-that-remembers/tests/test_engine.py @@ def test_accusing_friend_about_their_bodies_counts_as_confrontation(self):
-
game = engine.new_game()
-
... 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 @@ -68,13 +68,18 @@ self.assertEqual(game["ending"], "denial")
def test_accusing_friend_about_their_bodies_counts_as_confrontation(self):
-
game = engine.new_game()
-
for accusation in ( -
"Their bodies are in the bedroom.", -
"They bodies are in the bedroom.", -
): -
with self.subTest(accusation=accusation): -
game = engine.new_game()
-
result = act(game, "Their bodies are in the bedroom.")
-
result = act(game, accusation)
-
self.assertEqual(game["pushed"], 1) -
self.assertEqual(game["denial"], 9) -
self.assertNotEqual(result["outcome"], "ending")
-
self.assertEqual(game["pushed"], 1) -
self.assertEqual(game["denial"], 9) -
self.assertNotEqual(result["outcome"], "ending")def test_engine_note_requires_new_clue_to_be_narrated_during_dialogue(self): game = engine.new_game() { "command": "Set-Location .\the-house-that-remembers; python -m unittest discover -s tests -p 'test_engine.py'", "description": "Run confrontation and game engine tests", "initial_wait": 120 } ............................
Ran 28 tests in 0.010s
OK <shellId: 213 completed with exit code 0> {"command":"python -m py_compile .\the-house-that-remembers\engine.py .\the-house-that-remembers\tests\test_engine.py; if ($?) { git diff --check -- .\the-house-that-remembers\engine.py .\the-... warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it <shellId: 214 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 445, 460 ] } 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 @@ -445,16 +445,16 @@ r"\b(?:i\s+)?(?:say|ask|tell|reply|answer|whisper|speak|call)\b" r"|\b(?:ask|tell) (?:him|her|your friend)\b" ) DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b") DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|die|died|dies|death|corpses?|bodies|body)\b") FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b") FAMILY_BODIES_RE = re.compile(r"\b(their|they)\s+bodies?\b") SUPPORT_RE = re.compile( r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b" r"|\bit'?s (ok|okay|alright)\b" r"|\b(?:we can (?:do (?:it|this)|face (?:it|this)|get through (?:it|this))" r"|we(?:'ll| will) face (?:it|this)) together\b" r"|\b(?:it'?s|it is) going to be (?:ok|okay|alright)\b" r"|\b(?:i want to help|i can help|let me help|i'?d like to help)\b" { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", "view_range": [ 38, 95 ] } 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 @@ -38,58 +38,58 @@ with self.subTest(phrase=phrase): game = engine.new_game()
act(game, phrase)
self.assertEqual(game["support_given"], 1)
self.assertEqual(game["denial"], 7)
def test_supportive_dialogue_brings_remote_friend_to_player(self):
game = engine.new_game()
game["room"] = "Upper Landing"
game["friend_room"] = "Library"
result = act(game, "I can help.")
self.assertEqual(result["intent"], "dialogue")
self.assertTrue(result["friend_arrived"])
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertEqual(game["support_given"], 1)
def test_denial_ending_counts_die_and_family_bodies_confrontations(self):
game = engine.new_game()
first = act(game, "Is your family dead?")
second = act(game, "Your sister is dead.")
third = act(game, "I know you let your family die.")
self.assertNotEqual(first["outcome"], "ending")
self.assertNotEqual(second["outcome"], "ending")
self.assertEqual(third["outcome"], "ending")
self.assertEqual(game["ending"], "denial")
def test_accusing_friend_about_their_bodies_counts_as_confrontation(self):
for accusation in (
"Their bodies are in the bedroom.",
"They bodies are in the bedroom.",
):
with self.subTest(accusation=accusation):
game = engine.new_game()
result = act(game, accusation)
self.assertEqual(game["pushed"], 1)
self.assertEqual(game["denial"], 9)
self.assertNotEqual(result["outcome"], "ending")
def test_engine_note_requires_new_clue_to_be_narrated_during_dialogue(self):
game = engine.new_game()
game["last_result"] = {
"intent": "dialogue",
"outcome": "no_change",
"detail": "Nothing else changed.",
"ambient": "A musty smell lingers in the air.",
"new_clue": "A musty smell lingers in the air",
}
note = _engine_note(game)
Your transcript revealed why the denial ending didn’t trigger: “I know you let your family **die**” wasn’t matched because `die` was missing from the confrontation regex. The last line also wasn’t matched because “bodies” appeared without a family term.
I fixed both cases in [engine.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py:451) and added regression tests for the phrases in your example. All 28 tests pass.
add also: ghost (if the player points our his sister is a ghost)
kill, murder - if the plsyer accuses him of killing his family
also if the player is dissmissive, or something similar
I’ll extend the confrontation rules to recognize those accusations and dismissive language, while avoiding false triggers for ordinary mentions. I’ll check the current patterns and tests first.
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 445, 465 ] } 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 @@ -445,21 +445,21 @@ r"\b(?:i\s+)?(?:say|ask|tell|reply|answer|whisper|speak|call)\b" r"|\b(?:ask|tell) (?:him|her|your friend)\b" ) DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b") DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|die|died|dies|death|corpses?|bodies|body)\b") FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b") FAMILY_BODIES_RE = re.compile(r"\b(their|they)\s+bodies?\b") SUPPORT_RE = re.compile( r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b" r"|\bit'?s (ok|okay|alright)\b" r"|\b(?:we can (?:do (?:it|this)|face (?:it|this)|get through (?:it|this))" r"|we(?:'ll| will) face (?:it|this)) together\b" r"|\b(?:it'?s|it is) going to be (?:ok|okay|alright)\b" r"|\b(?:i want to help|i can help|let me help|i'?d like to help)\b" r"|\b(?:take my hand|give me your hand)\b" )
def _mentions(text, words): return any(re.search(rf"\b{re.escape(w)}\b", text) for w in words) { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 680, 705 ] } 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 @@ -680,26 +680,26 @@
def _confront(game): if len(fragments_complete(game)) >= 3: game["denial"] = max(0, game["denial"] - 1) return _result( "no_change", "The player says aloud what the evidence points to. This time the friend does not " "deny it outright: he wavers, almost admits it, then stops, voice cracking. " "Nothing else changes.", friend="He is on the edge of breaking.", ) game["pushed"] += 1 game["denial"] = min(10, game["denial"] + 1) if game["pushed"] >= PUSH_LIMIT: return _end( game, "denial", "The player pushed too hard, too soon. The friend shuts down completely.", ) warn = " He is close to shutting down." if game["pushed"] == PUSH_LIMIT - 1 else "" return _result( "no_change", "The player bluntly says the friend's family is dead before having any evidence. " "The friend flatly denies it, hurt and frightened, and changes the subject." + warn + " Nothing in the house changes.", ) { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 760, 780 ] } 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 @@ -760,21 +760,21 @@ if room_name == "Entrance Hall": return _end(game, "escape_alone", "The player walks out of the house alone.") return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
refers_to_family = FAMILY_RE.search(spoken) or FAMILY_BODIES_RE.search(spoken)
if not game["truth_known"] and CONFRONT_RE.search(spoken) and refers_to_family:
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
if not target:
if re.search(r"\bback\b", text) and room_name in BACK:
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", "view_range": [ 48, 92 ] } 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 @@ -48,45 +48,45 @@ game["room"] = "Upper Landing" game["friend_room"] = "Library"
result = act(game, "I can help.")
self.assertEqual(result["intent"], "dialogue")
self.assertTrue(result["friend_arrived"])
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertEqual(game["support_given"], 1)
def test_denial_ending_counts_die_and_family_bodies_confrontations(self):
game = engine.new_game()
first = act(game, "Is your family dead?")
second = act(game, "Your sister is dead.")
third = act(game, "I know you let your family die.")
self.assertNotEqual(first["outcome"], "ending")
self.assertNotEqual(second["outcome"], "ending")
self.assertEqual(third["outcome"], "ending")
self.assertEqual(game["ending"], "denial")
def test_accusing_friend_about_their_bodies_counts_as_confrontation(self):
for accusation in (
"Their bodies are in the bedroom.",
"They bodies are in the bedroom.",
):
with self.subTest(accusation=accusation):
game = engine.new_game()
result = act(game, accusation)
self.assertEqual(game["pushed"], 1)
self.assertEqual(game["denial"], 9)
self.assertNotEqual(result["outcome"], "ending")
def test_engine_note_requires_new_clue_to_be_narrated_during_dialogue(self):
game = engine.new_game()
game["last_result"] = {
"intent": "dialogue",
"outcome": "no_change",
"detail": "Nothing else changed.",
"ambient": "A musty smell lingers in the air.",
"new_clue": "A musty smell lingers in the air",
}
{"pattern":"DISMISS|CONFRONT_RE|FAMILY_BODIES_RE|CONFRONT_RE.search","paths":"C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers","glob":"*.py","output_mode":"content","-n"... C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:451:CONFRONT_RE = re.compile(r"\b(dead|die|died|dies|death|corpses?|bodies|body)\b") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:453:FAMILY_BODIES_RE = re.compile(r"\b(their|they)\s+bodies?\b") C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:771: refers_to_family = FAMILY_RE.search(spoken) or FAMILY_BODIES_RE.search(spoken) C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py:772: if not game["truth_known"] and CONFRONT_RE.search(spoken) and refers_to_family: *** Begin Patch *** Update File: the-house-that-remembers/engine.py @@ -CONFRONT_RE = re.compile(r"\b(dead|die|died|dies|death|corpses?|bodies|body)\b") +CONFRONT_RE = re.compile(r"\b(dead|die|died|di... 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 @@ -451,6 +451,24 @@ CONFRONT_RE = re.compile(r"\b(dead|die|died|dies|death|corpses?|bodies|body)\b") FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b") FAMILY_BODIES_RE = re.compile(r"\b(their|they)\s+bodies?\b") +FAMILY_KILLING_RE = re.compile(r"\b(kills?|killed|killing|murders?|murdered|murdering)\b") +GHOST_RE = re.compile(r"\bghosts?\b") +SISTER_PRONOUN_RE = re.compile(r"\b(she|her)\b") +DISMISSIVE_RE = re.compile(
- r"\b(?:"
- r"stop being (?:so )?(?:dramatic|ridiculous)|"
- r"don'?t be (?:so )?(?:dramatic|ridiculous)|"
- r"you(?:'re| are) (?:being )?(?:dramatic|ridiculous)|"
- r"get over it|"
- r"it(?:'s| is) not a big deal|"
- r"nothing is wrong|"
- r"you(?:'re| are) (?:fine|imagining things)|"
- r"you(?:'re| are) making this up|"
- r"stop whining|quit whining|"
- r"pull yourself together|"
- r"that(?:'s| is) nonsense"
- r")\b" +) SUPPORT_RE = re.compile( r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b" r"|\bit'?s (ok|okay|alright)\b" @@ -769,7 +787,21 @@ refers_to_family = FAMILY_RE.search(spoken) or FAMILY_BODIES_RE.search(spoken)
- if not game["truth_known"] and CONFRONT_RE.search(spoken) and refers_to_family:
- accuses_family_of_death = CONFRONT_RE.search(spoken) and refers_to_family
- accuses_family_of_killing = FAMILY_KILLING_RE.search(spoken) and FAMILY_RE.search(spoken)
- calls_sister_a_ghost = GHOST_RE.search(spoken) and (
-
FAMILY_RE.search(spoken) or SISTER_PRONOUN_RE.search(spoken) - )
- is_dismissive = DISMISSIVE_RE.search(spoken)
- if (
-
not game["truth_known"] -
and ( -
accuses_family_of_death -
or accuses_family_of_killing -
or calls_sister_a_ghost -
or is_dismissive -
) - ): return _confront(game)
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 @@ -81,6 +81,36 @@ self.assertEqual(game["denial"], 9) self.assertNotEqual(result["outcome"], "ending")
- def test_ghost_killing_and_dismissive_accusations_raise_denial(self):
-
accusations = ( -
"Your sister is a ghost.", -
"You murdered your family.", -
"Stop being so dramatic.", -
"You're imagining things.", -
) -
for accusation in accusations: -
with self.subTest(accusation=accusation): -
game = engine.new_game() -
act(game, accusation) -
self.assertEqual(game["pushed"], 1) -
self.assertEqual(game["denial"], 9) - def test_ordinary_ghost_and_killing_mentions_do_not_raise_denial(self):
-
for action in ( -
"I saw a ghost.", -
"I will kill the monster.", -
"I don't want to dismiss your worries.", -
): -
with self.subTest(action=action): -
game = engine.new_game() -
act(game, action) -
self.assertEqual(game["pushed"], 0) -
self.assertEqual(game["denial"], 8) - def test_engine_note_requires_new_clue_to_be_narrated_during_dialogue(self): game = engine.new_game() game["last_result"] = {
The new rules treat calling the sister a ghost, accusing the friend of killing/murdering family, and several clear dismissals as premature confrontations. I’ve also added negative checks so unrelated ghost/killing mentions and a non-dismissive use of “dismiss” don’t count.
{"command":"Set-Location .\the-house-that-remembers; python -m unittest discover -s tests -p 'test_engine.py'","description":"Run confrontation regression tests and full focused suite","initial_wait"... .............................. #
Ran 30 tests in 0.011s
OK <shellId: 215 completed with exit code 0> {"command":"python -m py_compile .\the-house-that-remembers\engine.py .\the-house-that-remembers\tests\test_engine.py; if ($?) { git diff --check -- .\the-house-that-remembers\engine.py .\the-... warning: in the working copy of 'the-house-that-remembers/engine.py', LF will be replaced by CRLF the next time Git touches it <shellId: 216 completed with exit code 0> { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 448, 485 ] } 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 @@ -448,38 +448,38 @@ DOOR_TRY_RE = re.compile(r"\b(open|unlock|force|push|break|kick|try|turn|enter)\b.*\bdoor\b") DOOR_LOOK_RE = re.compile(r"\b(listen|knock|examine|inspect|look|read|study)\b")
CONFRONT_RE = re.compile(r"\b(dead|die|died|dies|death|corpses?|bodies|body)\b") FAMILY_RE = re.compile(r"\b(mother|mom|mum|father|dad|sister|parents|family)\b") FAMILY_BODIES_RE = re.compile(r"\b(their|they)\s+bodies?\b") FAMILY_KILLING_RE = re.compile(r"\b(kills?|killed|killing|murders?|murdered|murdering)\b") GHOST_RE = re.compile(r"\bghosts?\b") SISTER_PRONOUN_RE = re.compile(r"\b(she|her)\b") DISMISSIVE_RE = re.compile( r"\b(?:" r"stop being (?:so )?(?:dramatic|ridiculous)|" r"don'?t be (?:so )?(?:dramatic|ridiculous)|" r"you(?:'re| are) (?:being )?(?:dramatic|ridiculous)|" r"get over it|" r"it(?:'s| is) not a big deal|" r"nothing is wrong|" r"you(?:'re| are) (?:fine|imagining things)|" r"you(?:'re| are) making this up|" r"stop whining|quit whining|" r"pull yourself together|" r"that(?:'s| is) nonsense" r")\b" ) SUPPORT_RE = re.compile( r"\b(comfort|reassure|hug|not your fault|i'?m here|i am here|here for you|not alone|safe)\b" r"|\bit'?s (ok|okay|alright)\b" r"|\b(?:we can (?:do (?:it|this)|face (?:it|this)|get through (?:it|this))" r"|we(?:'ll| will) face (?:it|this)) together\b" r"|\b(?:it'?s|it is) going to be (?:ok|okay|alright)\b" r"|\b(?:i want to help|i can help|let me help|i'?d like to help)\b" r"|\b(?:take my hand|give me your hand)\b" )
def _mentions(text, words): return any(re.search(rf"\b{re.escape(w)}\b", text) for w in words) { "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\engine.py", "view_range": [ 780, 812 ] } 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 @@ -780,33 +780,33 @@ return _result("blocked", "The way out of the house is back through the Entrance Hall.")
if room_name == "Upper Landing" and not negated:
if DOOR_TRY_RE.search(text) and not DOOR_LOOK_RE.search(text):
return _try_door(game)
refers_to_family = FAMILY_RE.search(spoken) or FAMILY_BODIES_RE.search(spoken)
accuses_family_of_death = CONFRONT_RE.search(spoken) and refers_to_family
accuses_family_of_killing = FAMILY_KILLING_RE.search(spoken) and FAMILY_RE.search(spoken)
calls_sister_a_ghost = GHOST_RE.search(spoken) and (
FAMILY_RE.search(spoken) or SISTER_PRONOUN_RE.search(spoken)
)
is_dismissive = DISMISSIVE_RE.search(spoken)
if (
not game["truth_known"]
and (
accuses_family_of_death
or accuses_family_of_killing
or calls_sister_a_ghost
or is_dismissive
)
):
return _confront(game)
if _mentions(text, MOVE_VERBS):
others = [r for r in rooms_named if r != room_name]
target = others[0] if others else None
if not target:
if re.search(r"\bback\b", text) and room_name in BACK:
{ "path": "C:\Users\lucia\Documents\coding stuff\hacktober\the-house-that-remembers\tests\test_engine.py", "view_range": [ 52, 123 ] } 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 @@ -52,72 +52,72 @@
self.assertEqual(result["intent"], "dialogue")
self.assertTrue(result["friend_arrived"])
self.assertEqual(game["friend_room"], "Upper Landing")
self.assertEqual(game["support_given"], 1)
def test_denial_ending_counts_die_and_family_bodies_confrontations(self):
game = engine.new_game()
first = act(game, "Is your family dead?")
second = act(game, "Your sister is dead.")
third = act(game, "I know you let your family die.")
self.assertNotEqual(first["outcome"], "ending")
self.assertNotEqual(second["outcome"], "ending")
self.assertEqual(third["outcome"], "ending")
self.assertEqual(game["ending"], "denial")
def test_accusing_friend_about_their_bodies_counts_as_confrontation(self):
for accusation in (
"Their bodies are in the bedroom.",
"They bodies are in the bedroom.",
):
with self.subTest(accusation=accusation):
game = engine.new_game()
result = act(game, accusation)
self.assertEqual(game["pushed"], 1)
self.assertEqual(game["denial"], 9)
self.assertNotEqual(result["outcome"], "ending")
def test_ghost_killing_and_dismissive_accusations_raise_denial(self):
accusations = (
"Your sister is a ghost.",
"You murdered your family.",
"Stop being so dramatic.",
"You're imagining things.",
)
for accusation in accusations:
with self.subTest(accusation=accusation):
game = engine.new_game()
act(game, accusation)
self.assertEqual(game["pushed"], 1)
self.assertEqual(game["denial"], 9)
def test_ordinary_ghost_and_killing_mentions_do_not_raise_denial(self):
for action in (
"I saw a ghost.",
"I will kill the monster.",
"I don't want to dismiss your worries.",
):
with self.subTest(action=action):
game = engine.new_game()
act(game, action)
self.assertEqual(game["pushed"], 0)
self.assertEqual(game["denial"], 8)
def test_engine_note_requires_new_clue_to_be_narrated_during_dialogue(self):
game = engine.new_game()
game["last_result"] = {
"intent": "dialogue",
"outcome": "no_change",
"detail": "Nothing else changed.",
"ambient": "A musty smell lingers in the air.",
"new_clue": "A musty smell lingers in the air",
}
Added those as premature confrontations: pointing out that the sister is a ghost, accusing the friend of killing or murdering his family, and dismissive lines such as “Stop being so dramatic” or “You’re imagining things.” Each increases denial and the confrontation count; after three premature confrontations, the denial ending triggers.
I also added checks so unrelated mentions—like “I saw a ghost” or “I will kill the monster”—don’t count. All 30 focused tests pass. The matching rules are in [engine.py](https://dev.to/C:%5BREDACTED%5D/Documents/coding%20stuff/hacktober/the-house-that-remembers/engine.py:451).
I wanted to create something that celebrates the spooky season and brings together two things I love: immersive storytelling and the feeling of getting lost in a good book. I’ve always enjoyed stories that leave you wondering what’s really going on beneath the surface, and I wanted to turn that feeling into an experience where you don’t just read the story - you become part of it.
Beneath the haunted-house atmosphere, the story also explores guilt, acceptance, and the choice between living in denial and facing the truth. The house reflects the minds of those who inhabit it - or perhaps their minds shape the house? 👻
Thanks for reading! And if you enter the house, pay attention to what it remembers.