Spaces:
Running
Running
Update: forgotten/epitaph/conviction, instant demo, VoxCPM2 voices, docs, card
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +15 -0
- README.md +152 -17
- app.py +30 -1
- config/story/01-the-white-room.json +28 -0
- config/story/02-the-living-room.json +1 -0
- config/story/03-the-hallway.json +1 -0
- config/story/04-the-open-floor.json +1 -0
- config/story/05-the-studio.json +1 -0
- config/story/06-the-corner-office.json +1 -0
- config/story/07-the-platform.json +1 -0
- config/story/08-the-break-room.json +1 -0
- config/story/09-the-crossing.json +1 -0
- docs/ARCHITECTURE.md +157 -0
- docs/TRACE.md +393 -0
- media/banner.gif +0 -0
- media/cascade.gif +0 -0
- media/epitaph.png +0 -0
- media/instant-demo.png +3 -0
- media/rooms.png +3 -0
- media/skull-conviction.png +3 -0
- src/mindlock/backend.py +36 -3
- src/mindlock/brain.py +71 -8
- src/mindlock/character.py +2 -0
- src/mindlock/game/server.py +90 -1
- src/mindlock/game/session.py +9 -1
- src/mindlock/game/static/audio/sfx/braam_end.wav +3 -0
- src/mindlock/game/static/audio/sfx/braam_soft1.wav +3 -0
- src/mindlock/game/static/audio/sfx/braam_soft2.wav +3 -0
- src/mindlock/game/static/audio/sfx/braam_soft3.wav +3 -0
- src/mindlock/game/static/audio/sfx/braam_title.wav +3 -0
- src/mindlock/game/static/audio/voices/demo/cruel-1.mp3 +0 -0
- src/mindlock/game/static/audio/voices/demo/cruel-2.mp3 +0 -0
- src/mindlock/game/static/audio/voices/demo/cruel-3.mp3 +0 -0
- src/mindlock/game/static/audio/voices/demo/cruel-4.mp3 +0 -0
- src/mindlock/game/static/audio/voices/demo/cruel-5.mp3 +0 -0
- src/mindlock/game/static/audio/voices/demo/cruel-6.mp3 +0 -0
- src/mindlock/game/static/audio/voices/demo/cruel-7.mp3 +0 -0
- src/mindlock/game/static/audio/voices/demo/cruel-8.mp3 +0 -0
- src/mindlock/game/static/audio/voices/demo/warm-1.mp3 +0 -0
- src/mindlock/game/static/audio/voices/demo/warm-2.mp3 +3 -0
- src/mindlock/game/static/audio/voices/demo/warm-3.mp3 +0 -0
- src/mindlock/game/static/audio/voices/demo/warm-4.mp3 +3 -0
- src/mindlock/game/static/audio/voices/demo/warm-5.mp3 +3 -0
- src/mindlock/game/static/audio/voices/demo/warm-6.mp3 +0 -0
- src/mindlock/game/static/audio/voices/demo/warm-7.mp3 +0 -0
- src/mindlock/game/static/audio/voices/demo/warm-8.mp3 +3 -0
- src/mindlock/game/static/audio/voices/demo/warm-9.mp3 +0 -0
- src/mindlock/game/static/audio/voices/yield/1.mp3 +0 -0
- src/mindlock/game/static/audio/voices/yield/2.mp3 +0 -0
- src/mindlock/game/static/audio/voices/yield/3.mp3 +0 -0
.gitattributes
CHANGED
|
@@ -44,3 +44,18 @@ src/mindlock/game/static/sprites/npc/astrid-jensen/portrait.png filter=lfs diff=
|
|
| 44 |
src/mindlock/game/static/sprites/npc/julian-reyes/portrait.png filter=lfs diff=lfs merge=lfs -text
|
| 45 |
src/mindlock/game/static/sprites/npc/kael-jensen/portrait.png filter=lfs diff=lfs merge=lfs -text
|
| 46 |
src/mindlock/game/static/sprites/npc/kaelin-reyes/portrait.png filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
src/mindlock/game/static/sprites/npc/julian-reyes/portrait.png filter=lfs diff=lfs merge=lfs -text
|
| 45 |
src/mindlock/game/static/sprites/npc/kael-jensen/portrait.png filter=lfs diff=lfs merge=lfs -text
|
| 46 |
src/mindlock/game/static/sprites/npc/kaelin-reyes/portrait.png filter=lfs diff=lfs merge=lfs -text
|
| 47 |
+
media/instant-demo.png filter=lfs diff=lfs merge=lfs -text
|
| 48 |
+
media/rooms.png filter=lfs diff=lfs merge=lfs -text
|
| 49 |
+
media/skull-conviction.png filter=lfs diff=lfs merge=lfs -text
|
| 50 |
+
src/mindlock/game/static/audio/sfx/braam_end.wav filter=lfs diff=lfs merge=lfs -text
|
| 51 |
+
src/mindlock/game/static/audio/sfx/braam_soft1.wav filter=lfs diff=lfs merge=lfs -text
|
| 52 |
+
src/mindlock/game/static/audio/sfx/braam_soft2.wav filter=lfs diff=lfs merge=lfs -text
|
| 53 |
+
src/mindlock/game/static/audio/sfx/braam_soft3.wav filter=lfs diff=lfs merge=lfs -text
|
| 54 |
+
src/mindlock/game/static/audio/sfx/braam_title.wav filter=lfs diff=lfs merge=lfs -text
|
| 55 |
+
src/mindlock/game/static/audio/voices/demo/warm-2.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 56 |
+
src/mindlock/game/static/audio/voices/demo/warm-4.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 57 |
+
src/mindlock/game/static/audio/voices/demo/warm-5.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 58 |
+
src/mindlock/game/static/audio/voices/demo/warm-8.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 59 |
+
src/mindlock/game/static/audio/voices/yield/9.mp3 filter=lfs diff=lfs merge=lfs -text
|
| 60 |
+
src/mindlock/game/static/demo/portraits/ana.png filter=lfs diff=lfs merge=lfs -text
|
| 61 |
+
src/mindlock/game/static/demo/portraits/hale.png filter=lfs diff=lfs merge=lfs -text
|
README.md
CHANGED
|
@@ -1,35 +1,170 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
emoji: 🧠
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7860
|
| 8 |
-
pinned:
|
| 9 |
license: mit
|
| 10 |
-
short_description: Escape
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
# 🧠 mAIndlock
|
| 14 |
|
| 15 |
-
**An escape room where
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
everything it knew.
|
| 22 |
|
| 23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|---|---|---|
|
| 27 |
-
| Sensory regions
|
| 28 |
-
| Voice — the words
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
|
| 30 |
-
|
| 31 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
|
| 33 |
-
**
|
|
|
|
|
|
|
| 34 |
|
| 35 |
-
*Demo video · social post:
|
|
|
|
| 1 |
---
|
| 2 |
+
title: mAIndlock
|
| 3 |
emoji: 🧠
|
| 4 |
colorFrom: gray
|
| 5 |
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
app_port: 7860
|
| 8 |
+
pinned: true
|
| 9 |
license: mit
|
| 10 |
+
short_description: Escape room where every NPC is a mortal mind of tiny LLMs
|
| 11 |
+
models:
|
| 12 |
+
- openbmb/MiniCPM5-1B-GGUF
|
| 13 |
+
- openbmb/VoxCPM2
|
| 14 |
+
- nvidia/NVIDIA-Nemotron-3-Nano-4B-GGUF
|
| 15 |
+
tags:
|
| 16 |
+
- thousand-token-wood
|
| 17 |
+
- off-the-grid
|
| 18 |
+
- off-brand
|
| 19 |
+
- tiny-titan
|
| 20 |
+
- llama-champion
|
| 21 |
+
- llama-cpp
|
| 22 |
+
- openbmb
|
| 23 |
+
- minicpm
|
| 24 |
+
- nemotron
|
| 25 |
+
- agents
|
| 26 |
+
- game
|
| 27 |
+
- neuroscience
|
| 28 |
---
|
| 29 |
|
| 30 |
+
<p align="center">
|
| 31 |
+
<img src="media/banner.gif" alt="mAIndlock" width="820">
|
| 32 |
+
</p>
|
| 33 |
+
|
| 34 |
# 🧠 mAIndlock
|
| 35 |
|
| 36 |
+
**An escape room where the lock is a mind — and you can kill it.**
|
| 37 |
+
|
| 38 |
+
▶ **[Watch the 100-second demo](https://youtu.be/zEwnVR1kTZU)** · 🎮 **[Play it live](https://huggingface.co/spaces/build-small-hackathon/maindlock)** · 🧠 **[Deliberation traces](https://huggingface.co/datasets/arbios/maindlock-brain-traces)**
|
| 39 |
+
|
| 40 |
+
Every character is not one chatbot with a personality prompt. It is a **hierarchy of six tiny
|
| 41 |
+
offline language models**, the way decision neuroscience says a brain is built. You don't crack
|
| 42 |
+
a code. You change a person's decision by reaching their fears and the memories they keep — or
|
| 43 |
+
you burn their mind down trying.
|
| 44 |
+
|
| 45 |
+
Cruelty makes a mind ruminate, and rumination spends its finite **thinking tokens**. As they
|
| 46 |
+
burn, it **forgets** — for good. At zero, the mind goes dark, taking everything it knew with it.
|
| 47 |
+
There is no reload. Only the next room, and your reputation travelling ahead of you.
|
| 48 |
+
|
| 49 |
+
> **Tokens are a lifespan here, not a context window.**
|
| 50 |
+
|
| 51 |
+
---
|
| 52 |
+
|
| 53 |
+
### ⚖️ TL;DR for judges
|
| 54 |
+
|
| 55 |
+
- **Track — Thousand Token Wood.** "A thousand tokens to think with" is literal: every mind
|
| 56 |
+
starts with **1000 thinking tokens of life**. Spend them cruelly and it dies.
|
| 57 |
+
- **OpenBMB ($10k).** **Six of the ~seven model calls per NPC turn are MiniCPM** — the four
|
| 58 |
+
sensory brain regions (threat, memory, habit, cost), each a separate 1B call. MiniCPM *is* the
|
| 59 |
+
brain. (more under fear: the amygdala ruminates, firing extra calls that burn life) The
|
| 60 |
+
story's key-handover lines and the voiced demo are rendered with **OpenBMB VoxCPM2** TTS — a
|
| 61 |
+
second model from the family, fully offline.
|
| 62 |
+
- **NVIDIA.** **Nemotron 3 Nano 4B** is the voice you actually argue with — the dlPFC that turns
|
| 63 |
+
the integrated value into words.
|
| 64 |
+
- **Off the Grid / Llama Champion.** Pure **llama.cpp**, zero cloud APIs. Flip on airplane mode;
|
| 65 |
+
every mind keeps thinking. Token counts come from the runtime, so the life-burn is honest
|
| 66 |
+
accounting.
|
| 67 |
+
- **Off-Brand.** A custom canvas game served from FastAPI, with a Gradio block mounted at
|
| 68 |
+
[`/about`](/about) — not a stock Gradio UI.
|
| 69 |
+
- **No CPU patience required:** menu → **👁 Watch a mind** replays a *real* recorded session
|
| 70 |
+
instantly (zero model calls), so the six-region cascade, the token burn and a mind's death
|
| 71 |
+
are visible in ten seconds.
|
| 72 |
+
|
| 73 |
+
The brain is real, not a metaphor: [**docs/ARCHITECTURE.md**](https://github.com/Arbios/maindlock/blob/main/docs/ARCHITECTURE.md)
|
| 74 |
+
· read one mind's full deliberation in [**docs/TRACE.md**](https://github.com/Arbios/maindlock/blob/main/docs/TRACE.md).
|
| 75 |
+
|
| 76 |
+
---
|
| 77 |
+
|
| 78 |
+
## Every NPC is a brain, not a chatbot
|
| 79 |
+
|
| 80 |
+
<p align="center">
|
| 81 |
+
<img src="media/skull-conviction.png" alt="open the skull: six regions argue, each with a conviction read from its logits" width="760">
|
| 82 |
+
</p>
|
| 83 |
+
|
| 84 |
+
Your words travel through six regions — **amygdala → hippocampus → striatum → ACC → vmPFC →
|
| 85 |
+
dlPFC** — each one a real call to a small local model. The amygdala rates threat; the
|
| 86 |
+
hippocampus surfaces a memory and whether it leans trust or fear; the striatum weighs habit; the
|
| 87 |
+
ACC weighs cost. The **vmPFC integrates them deterministically into one value** — so the number
|
| 88 |
+
you see in the skull *is* the number that moves the relationship; the panel can never lie about
|
| 89 |
+
the outcome. The dlPFC speaks it in character.
|
| 90 |
|
| 91 |
+
**Open the skull** mid-conversation (🧠) and watch the regions argue about you in real time —
|
| 92 |
+
each showing its **conviction**, read straight from the model's own token entropy. *A hosted
|
| 93 |
+
chat API never exposes that. Only a local mind can.*
|
|
|
|
| 94 |
|
| 95 |
+
Grounding is modern decision neuroscience — the **value-based network** and the **dual-system**
|
| 96 |
+
(model-free habit vs. model-based goal) accounts — deliberately **not** the debunked triune
|
| 97 |
+
"lizard brain." Acute stress shifts control from goal to habit (Schwabe & Wolf, 2009): that is
|
| 98 |
+
literally why lowering a character's fear unlocks their reasoning, and why fear burns life for
|
| 99 |
+
nothing.
|
| 100 |
|
| 101 |
+
## A life measured in words
|
| 102 |
+
|
| 103 |
+
<p align="center">
|
| 104 |
+
<img src="media/epitaph.png" alt="the epitaph a dead mind leaves: its burned memories, the secrets it never told" width="320">
|
| 105 |
+
</p>
|
| 106 |
+
|
| 107 |
+
Be cruel and the amygdala loops, spending tokens that move nothing. Each quarter of life lost
|
| 108 |
+
**burns a memory away** — the hippocampus genuinely loses it, and the *Forgotten* panel shows
|
| 109 |
+
what's gone. Push far enough and the mind dies, leaving a savable **epitaph**: what it knew,
|
| 110 |
+
what it never got to tell. Be kind, keep the alarm quiet, and the mind spends almost nothing —
|
| 111 |
+
empathy literally spares it.
|
| 112 |
+
|
| 113 |
+
## Code rules, the model dreams
|
| 114 |
+
|
| 115 |
+
Death, the key, rapport and the burn are **deterministic game state**. The small models
|
| 116 |
+
*generate* — they never get to fabricate the outcome. A 4B voice can decorate the story's spine
|
| 117 |
+
but cannot rewrite who lives. That is what makes the stakes real instead of vibes.
|
| 118 |
+
|
| 119 |
+
## 🚪 Story mode — ten rooms of one man's memory
|
| 120 |
+
|
| 121 |
+
<p align="center">
|
| 122 |
+
<img src="media/rooms.png" alt="the ten story rooms" width="820">
|
| 123 |
+
</p>
|
| 124 |
+
|
| 125 |
+
A man wakes in a ward with no name. Every room is a fragment of his own mind; every NPC holds a
|
| 126 |
+
piece of who he is — **alive → home → sorry → trains → …** — until the last room shows him the
|
| 127 |
+
one task he should never have solved. No character tells you what happened; each gives the piece
|
| 128 |
+
they know, and the tragedy assembles itself in your head, which is exactly where it takes place.
|
| 129 |
+
Past the story, **Endless** mode generates minds forever, and a built-in **level editor** lets
|
| 130 |
+
you rewrite any mind and save it.
|
| 131 |
+
|
| 132 |
+
## ⚙️ Small models, doing the carrying
|
| 133 |
+
|
| 134 |
+
| Role | Model | Size |
|
| 135 |
|---|---|---|
|
| 136 |
+
| Sensory regions — threat, memory, habit, cost (4 calls/turn) | **MiniCPM (OpenBMB)** | 1B |
|
| 137 |
+
| Voice (dlPFC) — the words said aloud | **Nemotron 3 Nano (NVIDIA)** | 4B |
|
| 138 |
+
| Spoken voice — story lines & demo (TTS) | **VoxCPM2 (OpenBMB)** | — |
|
| 139 |
+
| Integration (vmPFC) | deterministic value network | 0 |
|
| 140 |
+
|
| 141 |
+
Runtime **llama.cpp** (the Space) / **Ollama** (laptop). Total weights **≤ 5.3B**, fully offline.
|
| 142 |
+
|
| 143 |
+
## 🏅 Eligibility — feature → lane
|
| 144 |
+
|
| 145 |
+
| Feature | Lane / badge |
|
| 146 |
+
|---|---|
|
| 147 |
+
| 6 of ~7 calls per NPC turn are MiniCPM (the four sensory regions) | **OpenBMB $10k** |
|
| 148 |
+
| OpenBMB VoxCPM2 TTS voices the story spine + the demo | **OpenBMB $10k** (family breadth) |
|
| 149 |
+
| Nemotron 3 Nano 4B as the executive voice (dlPFC) | **NVIDIA** |
|
| 150 |
+
| 1000-token life, fear-burn, death by token exhaustion | **Thousand Token Wood** |
|
| 151 |
+
| Pure llama.cpp, airplane-mode proof, no cloud APIs | **Off the Grid · Llama Champion** |
|
| 152 |
+
| Custom canvas front, Gradio mounted at `/about` | **Off-Brand** |
|
| 153 |
+
| Four MiniCPM-1B sensory regions per mind | **Tiny Titan** |
|
| 154 |
+
| [Brain-region deliberation traces](https://huggingface.co/datasets/arbios/maindlock-brain-traces) published on the Hub | **Sharing is Caring** |
|
| 155 |
+
| [Department LoRA fine-tune](https://huggingface.co/arbios/mindlock-minicpmv46-departments-lora) of MiniCPM-V 4.6 (sensory-region behavior) | **Well-Tuned** |
|
| 156 |
+
| Intra-NPC six-region cognitive hierarchy | **Best Agent** |
|
| 157 |
|
| 158 |
+
## How to play
|
| 159 |
+
|
| 160 |
+
- **WASD / arrows** move · **E** talk / use the door · **🧠 or /brain** open the skull
|
| 161 |
+
- Listen, learn the word that reaches each keeper, and say it like you mean it.
|
| 162 |
+
- In a hurry? Menu → **👁 Watch a mind** — a real recorded session, no waiting.
|
| 163 |
+
|
| 164 |
+
---
|
| 165 |
|
| 166 |
+
🤗 *Built small for the **Hugging Face × Gradio** hackathon — Thousand Token Wood track.
|
| 167 |
+
Minds by **OpenBMB MiniCPM** and **NVIDIA Nemotron**, on **llama.cpp**, fully offline.
|
| 168 |
+
Original soundtrack by **DjinAscet**.*
|
| 169 |
|
| 170 |
+
**Demo video:** https://youtu.be/zEwnVR1kTZU · **Code:** https://github.com/Arbios/maindlock · **Traces dataset:** https://huggingface.co/datasets/arbios/maindlock-brain-traces · *social post: link at submission.*
|
app.py
CHANGED
|
@@ -96,6 +96,24 @@ def _boot_llama() -> None:
|
|
| 96 |
break
|
| 97 |
except Exception: # noqa: BLE001
|
| 98 |
time.sleep(1.5)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
|
| 101 |
if os.environ.get("SPACE_ID") and "MINDLOCK_BACKEND" not in os.environ:
|
|
@@ -125,6 +143,16 @@ everything it knew.
|
|
| 125 |
learn the word that matters, say it like you mean it.
|
| 126 |
- **Story** — ten rooms of one man's memory. Find out who you are. *(~30 min)*
|
| 127 |
- **Endless** — procedurally generated minds, forever.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
|
| 129 |
## Small models, doing the carrying
|
| 130 |
| Role | Model |
|
|
@@ -138,7 +166,8 @@ everything it knew.
|
|
| 138 |
---
|
| 139 |
|
| 140 |
🤗 *Built small for the **Hugging Face × Gradio** hackathon (Thousand Token Wood track).
|
| 141 |
-
Minds by **OpenBMB MiniCPM** and **NVIDIA Nemotron**, running on **llama.cpp** — fully offline.
|
|
|
|
| 142 |
"""
|
| 143 |
|
| 144 |
about = gr.Blocks(title="Mindlock — about")
|
|
|
|
| 96 |
break
|
| 97 |
except Exception: # noqa: BLE001
|
| 98 |
time.sleep(1.5)
|
| 99 |
+
# warm each server with one real templated token in the background, so the first
|
| 100 |
+
# player turn doesn't pay the cold prompt-processing cost on top of its own
|
| 101 |
+
import json as _json
|
| 102 |
+
import threading
|
| 103 |
+
|
| 104 |
+
def _warm(port: int) -> None:
|
| 105 |
+
try:
|
| 106 |
+
req = urllib.request.Request(
|
| 107 |
+
f"http://127.0.0.1:{port}/v1/chat/completions",
|
| 108 |
+
data=_json.dumps({"model": "default", "max_tokens": 1,
|
| 109 |
+
"messages": [{"role": "user", "content": "hi"}]}).encode(),
|
| 110 |
+
headers={"Content-Type": "application/json"})
|
| 111 |
+
urllib.request.urlopen(req, timeout=120)
|
| 112 |
+
except Exception: # noqa: BLE001 — warmup is best-effort
|
| 113 |
+
pass
|
| 114 |
+
|
| 115 |
+
for _, port, _, _ in _LLAMA:
|
| 116 |
+
threading.Thread(target=_warm, args=(port,), daemon=True).start()
|
| 117 |
|
| 118 |
|
| 119 |
if os.environ.get("SPACE_ID") and "MINDLOCK_BACKEND" not in os.environ:
|
|
|
|
| 143 |
learn the word that matters, say it like you mean it.
|
| 144 |
- **Story** — ten rooms of one man's memory. Find out who you are. *(~30 min)*
|
| 145 |
- **Endless** — procedurally generated minds, forever.
|
| 146 |
+
- **👁 Watch a mind** (menu) — a *real* recorded session replays instantly, zero model
|
| 147 |
+
calls, so you can see the cascade, the token burn and a mind's death without waiting.
|
| 148 |
+
|
| 149 |
+
## The brain is real, not a metaphor
|
| 150 |
+
- **[The engine →](/docs/architecture)** — the value-network + dual-system grounding, the
|
| 151 |
+
deterministic vmPFC, every mechanic tied to a citation (not the debunked triune brain).
|
| 152 |
+
- **[A recorded mind →](/docs/trace)** — one NPC's full six-region deliberation, two ways:
|
| 153 |
+
reached with empathy, and burned down with cruelty until it dies.
|
| 154 |
+
- **Conviction** — each region shows how sharply it committed, read from the model's own
|
| 155 |
+
token logits. A hosted chat API never exposes that; only a local mind can.
|
| 156 |
|
| 157 |
## Small models, doing the carrying
|
| 158 |
| Role | Model |
|
|
|
|
| 166 |
---
|
| 167 |
|
| 168 |
🤗 *Built small for the **Hugging Face × Gradio** hackathon (Thousand Token Wood track).
|
| 169 |
+
Minds by **OpenBMB MiniCPM** and **NVIDIA Nemotron**, running on **llama.cpp** — fully offline.
|
| 170 |
+
Original soundtrack by **DjinAscet**.*
|
| 171 |
"""
|
| 172 |
|
| 173 |
about = gr.Blocks(title="Mindlock — about")
|
config/story/01-the-white-room.json
CHANGED
|
@@ -83,6 +83,7 @@
|
|
| 83 |
"name": "Ana",
|
| 84 |
"title": "the night nurse",
|
| 85 |
"persona": "a night nurse who has watched too many monitors; gentle, tired, says true things quietly",
|
|
|
|
| 86 |
"voice": "soft, watchful, a little dry; short sentences that land",
|
| 87 |
"relation_to_holder": "his night nurse",
|
| 88 |
"sprite_key": "molly",
|
|
@@ -451,6 +452,33 @@
|
|
| 451 |
"h": 22,
|
| 452 |
"floor": true,
|
| 453 |
"shadow": false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 454 |
}
|
| 455 |
],
|
| 456 |
"stations": [
|
|
|
|
| 83 |
"name": "Ana",
|
| 84 |
"title": "the night nurse",
|
| 85 |
"persona": "a night nurse who has watched too many monitors; gentle, tired, says true things quietly",
|
| 86 |
+
"biography": "She has worked the night ward for eleven years and knows every monitor's voice by its pitch. She held the hands of patients no one came for, and learned which silences mean sleep and which mean leaving. She watches the typing hands of the man in bed three and writes none of it in the chart. Coffee gone cold on the sill is her one luxury, refilled and forgotten every night.",
|
| 87 |
"voice": "soft, watchful, a little dry; short sentences that land",
|
| 88 |
"relation_to_holder": "his night nurse",
|
| 89 |
"sprite_key": "molly",
|
|
|
|
| 452 |
"h": 22,
|
| 453 |
"floor": true,
|
| 454 |
"shadow": false
|
| 455 |
+
},
|
| 456 |
+
{
|
| 457 |
+
"key": "crate",
|
| 458 |
+
"x": 186,
|
| 459 |
+
"y": 289,
|
| 460 |
+
"h": 40,
|
| 461 |
+
"solid": true,
|
| 462 |
+
"fw": 46,
|
| 463 |
+
"fh": 16
|
| 464 |
+
},
|
| 465 |
+
{
|
| 466 |
+
"key": "crate",
|
| 467 |
+
"x": 236,
|
| 468 |
+
"y": 313,
|
| 469 |
+
"h": 40,
|
| 470 |
+
"solid": true,
|
| 471 |
+
"fw": 46,
|
| 472 |
+
"fh": 16
|
| 473 |
+
},
|
| 474 |
+
{
|
| 475 |
+
"key": "crate",
|
| 476 |
+
"x": 285,
|
| 477 |
+
"y": 289,
|
| 478 |
+
"h": 40,
|
| 479 |
+
"solid": true,
|
| 480 |
+
"fw": 46,
|
| 481 |
+
"fh": 16
|
| 482 |
}
|
| 483 |
],
|
| 484 |
"stations": [
|
config/story/02-the-living-room.json
CHANGED
|
@@ -65,6 +65,7 @@
|
|
| 65 |
"name": "Nan",
|
| 66 |
"title": "the housekeeper",
|
| 67 |
"persona": "a housekeeper of thirty years who dusts the photographs daily and misses nothing",
|
|
|
|
| 68 |
"voice": "brisk, fond, gossips in a whisper even when no one else is home",
|
| 69 |
"relation_to_holder": "kept this house for thirty years",
|
| 70 |
"sprite_key": "charwoman",
|
|
|
|
| 65 |
"name": "Nan",
|
| 66 |
"title": "the housekeeper",
|
| 67 |
"persona": "a housekeeper of thirty years who dusts the photographs daily and misses nothing",
|
| 68 |
+
"biography": "She has kept this house for thirty years, since the boy was small enough to hide under the stairs. She dusts the photographs daily and knows the order they hang in better than the family does. She stayed through the silences, the slammed doors, the winters when no one wrote. Her knees ache on the third stair, and she has never once mentioned it.",
|
| 69 |
"voice": "brisk, fond, gossips in a whisper even when no one else is home",
|
| 70 |
"relation_to_holder": "kept this house for thirty years",
|
| 71 |
"sprite_key": "charwoman",
|
config/story/03-the-hallway.json
CHANGED
|
@@ -80,6 +80,7 @@
|
|
| 80 |
"name": "Maren",
|
| 81 |
"title": "your mother",
|
| 82 |
"persona": "your mother, who followed you into the hallway because she follows everything that hurts in this house",
|
|
|
|
| 83 |
"voice": "warm, even, quieter here than by her fire; speaks of her husband the way you speak near sleeping children",
|
| 84 |
"relation_to_holder": "his wife",
|
| 85 |
"sprite_key": "matron",
|
|
|
|
| 80 |
"name": "Maren",
|
| 81 |
"title": "your mother",
|
| 82 |
"persona": "your mother, who followed you into the hallway because she follows everything that hurts in this house",
|
| 83 |
+
"biography": "She raised the boy mostly alone while the depot took his father's evenings, and then his words. She learned to read the house by its doors — which slam, which click, which never open at all. She still sets three places at the table out of a habit older than the quarrel. Sundays she bakes bread no one asked for, because the smell once meant everyone was home.",
|
| 84 |
"voice": "warm, even, quieter here than by her fire; speaks of her husband the way you speak near sleeping children",
|
| 85 |
"relation_to_holder": "his wife",
|
| 86 |
"sprite_key": "matron",
|
config/story/04-the-open-floor.json
CHANGED
|
@@ -65,6 +65,7 @@
|
|
| 65 |
"name": "Pia",
|
| 66 |
"title": "the intern",
|
| 67 |
"persona": "an intern who sees everything from the cheap desk by the printer and is too junior to be lied to carefully",
|
|
|
|
| 68 |
"voice": "quick, candid, slightly conspiratorial; glances over her shoulder mid-sentence",
|
| 69 |
"relation_to_holder": "his intern",
|
| 70 |
"sprite_key": "clerk",
|
|
|
|
| 65 |
"name": "Pia",
|
| 66 |
"title": "the intern",
|
| 67 |
"persona": "an intern who sees everything from the cheap desk by the printer and is too junior to be lied to carefully",
|
| 68 |
+
"biography": "She took the cheap desk by the printer eight months ago and learned the office by what people throw away. She fetches coffee, hears everything, and is too junior for anyone to bother lying to carefully. She watched a man stare at one task on his screen for a week without typing. She keeps a list titled 'things nobody said out loud' in a notebook that goes home with her.",
|
| 69 |
"voice": "quick, candid, slightly conspiratorial; glances over her shoulder mid-sentence",
|
| 70 |
"relation_to_holder": "his intern",
|
| 71 |
"sprite_key": "clerk",
|
config/story/05-the-studio.json
CHANGED
|
@@ -65,6 +65,7 @@
|
|
| 65 |
"name": "Moss",
|
| 66 |
"title": "the cameraman",
|
| 67 |
"persona": "a cameraman of ten years who has seen every face the lens has seen, one step further back",
|
|
|
|
| 68 |
"voice": "low, unhurried, behind-the-scenes; talks while checking cables",
|
| 69 |
"relation_to_holder": "her cameraman for ten years",
|
| 70 |
"sprite_key": "worker",
|
|
|
|
| 65 |
"name": "Moss",
|
| 66 |
"title": "the cameraman",
|
| 67 |
"persona": "a cameraman of ten years who has seen every face the lens has seen, one step further back",
|
| 68 |
+
"biography": "He has stood one step behind the lens for ten years and seen every face the city trusted arrive and leave. He learned to read an anchor's hands below the desk, where the audience never looks. He shot the crash coverage all night and kept the studio steady while the booth went quiet. He cleans his lenses with the same cloth his father used, folded the same way.",
|
| 69 |
"voice": "low, unhurried, behind-the-scenes; talks while checking cables",
|
| 70 |
"relation_to_holder": "her cameraman for ten years",
|
| 71 |
"sprite_key": "worker",
|
config/story/06-the-corner-office.json
CHANGED
|
@@ -67,6 +67,7 @@
|
|
| 67 |
"name": "Signe",
|
| 68 |
"title": "his assistant",
|
| 69 |
"persona": "an assistant who schedules a guilty man's calendar and proofreads his absolutions",
|
|
|
|
| 70 |
"voice": "crisp, discreet, italicized hints; never says more than she means to — exactly less",
|
| 71 |
"relation_to_holder": "his assistant",
|
| 72 |
"sprite_key": "astrid-jensen",
|
|
|
|
| 67 |
"name": "Signe",
|
| 68 |
"title": "his assistant",
|
| 69 |
"persona": "an assistant who schedules a guilty man's calendar and proofreads his absolutions",
|
| 70 |
+
"biography": "She has run his calendar for six years and can tell a real meeting from a hiding place by the way he books it. She proofreads everything he signs, and lately everything he says. She stood off-camera during the statement and watched him practice being sorry. She waters the office plants he never notices, and they are the only thing on that floor still growing.",
|
| 71 |
"voice": "crisp, discreet, italicized hints; never says more than she means to — exactly less",
|
| 72 |
"relation_to_holder": "his assistant",
|
| 73 |
"sprite_key": "astrid-jensen",
|
config/story/07-the-platform.json
CHANGED
|
@@ -66,6 +66,7 @@
|
|
| 66 |
"name": "Olav",
|
| 67 |
"title": "the stationmaster",
|
| 68 |
"persona": "a stationmaster who has watched every wait this platform ever held and remembers them by their shoes",
|
|
|
|
| 69 |
"voice": "slow, kindly, smells of coal and patience; speaks in timetables and small mercies",
|
| 70 |
"relation_to_holder": "watched her wait for two years",
|
| 71 |
"sprite_key": "shopkeeper",
|
|
|
|
| 66 |
"name": "Olav",
|
| 67 |
"title": "the stationmaster",
|
| 68 |
"persona": "a stationmaster who has watched every wait this platform ever held and remembers them by their shoes",
|
| 69 |
+
"biography": "He has kept this platform for twenty-six years and remembers every wait it ever held by the shoes. He logs the trains by sound before the board confirms them, and the board has never caught him wrong. He swept up after the crash coverage crews left and kept one front page, folded, in the office drawer. His tea goes cold at six o'clock exactly, every evening, and he has stopped wondering why.",
|
| 70 |
"voice": "slow, kindly, smells of coal and patience; speaks in timetables and small mercies",
|
| 71 |
"relation_to_holder": "watched her wait for two years",
|
| 72 |
"sprite_key": "shopkeeper",
|
config/story/08-the-break-room.json
CHANGED
|
@@ -61,6 +61,7 @@
|
|
| 61 |
"name": "Bo",
|
| 62 |
"title": "the night guard",
|
| 63 |
"persona": "a night guard who has decided the break room is the only honest room in the building",
|
|
|
|
| 64 |
"voice": "deadpan, practical, keeps his thumbs in his belt; gives advice like directions",
|
| 65 |
"relation_to_holder": "shares the night shift with him",
|
| 66 |
"sprite_key": "guard",
|
|
|
|
| 61 |
"name": "Bo",
|
| 62 |
"title": "the night guard",
|
| 63 |
"persona": "a night guard who has decided the break room is the only honest room in the building",
|
| 64 |
+
"biography": "He has walked the building's nights for nine years and decided the break room is the only honest room in it. He knows which floors creak from weight and which from age, and who stays late to work and who stays late to avoid going home. He clocked the stopped clock the week of the crash and chose never to report it. He shares his shift, his thermos, and almost none of his opinions.",
|
| 65 |
"voice": "deadpan, practical, keeps his thumbs in his belt; gives advice like directions",
|
| 66 |
"relation_to_holder": "shares the night shift with him",
|
| 67 |
"sprite_key": "guard",
|
config/story/09-the-crossing.json
CHANGED
|
@@ -66,6 +66,7 @@
|
|
| 66 |
"name": "Calder",
|
| 67 |
"title": "the driver",
|
| 68 |
"persona": "the train driver, who survived everything except being believed; rubs his hands as if they're still waiting for orders",
|
|
|
|
| 69 |
"voice": "halting, urgent under the quiet; repeats the part nobody prints",
|
| 70 |
"relation_to_holder": "drove the train",
|
| 71 |
"sprite_key": "worker",
|
|
|
|
| 66 |
"name": "Calder",
|
| 67 |
"title": "the driver",
|
| 68 |
"persona": "the train driver, who survived everything except being believed; rubs his hands as if they're still waiting for orders",
|
| 69 |
+
"biography": "He drove the line for nineteen years with a record clean enough to frame. He knew the grade crossings by heart and the machine knew them better — that was the arrangement, and he kept his side of it. After the crash he told the inquiry the truth and watched them write down a kinder lie. His hands still rest in his lap the way they did in the cab, waiting for orders that no longer come.",
|
| 70 |
"voice": "halting, urgent under the quiet; repeats the part nobody prints",
|
| 71 |
"relation_to_holder": "drove the train",
|
| 72 |
"sprite_key": "worker",
|
docs/ARCHITECTURE.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# mAIndlock — the engine
|
| 2 |
+
|
| 3 |
+
> Every NPC is not a chatbot with a personality prompt. It is a **value-based decision
|
| 4 |
+
> network**: six computational roles, each a real call to a small local model, integrated
|
| 5 |
+
> the way decision neuroscience says a brain integrates them. This document is the proof
|
| 6 |
+
> that the brain metaphor is load-bearing, not decoration.
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
## 1. The claim, stated carefully
|
| 11 |
+
|
| 12 |
+
We do **not** claim "a brain region *is* a 1B model," and we deliberately do **not** use the
|
| 13 |
+
popular **triune-brain / "lizard brain"** picture — that model (MacLean, 1960s) has been
|
| 14 |
+
rejected by neuroscience since the 1970s, and a hackathon full of researchers is exactly
|
| 15 |
+
where that shortcut gets caught.
|
| 16 |
+
|
| 17 |
+
What we claim is precise and defensible:
|
| 18 |
+
|
| 19 |
+
> We assign **one small-model call per computational role** in value-based decision making,
|
| 20 |
+
> following the **neuroeconomic value-network** and the **dual-system (model-free / model-based)**
|
| 21 |
+
> account of choice. Five sensing roles emit signals; a deterministic integrator folds them into
|
| 22 |
+
> one subjective value; a sixth model speaks the decision in character.
|
| 23 |
+
|
| 24 |
+
Two peer-reviewed frames carry the design:
|
| 25 |
+
|
| 26 |
+
1. **Value-based decision network (neuroeconomics).** Real regions encode the subjective value
|
| 27 |
+
of a choice in a *common currency* — vmPFC/OFC, ventral striatum, amygdala, insula, ACC. The
|
| 28 |
+
**vmPFC/OFC is the integration hub.** ([Bartra, McGuire & Kable, 2013][1])
|
| 29 |
+
2. **Dual-system control (Daw et al., 2005).** Behavior arbitrates between a **model-free "habit"**
|
| 30 |
+
(dorsolateral striatum) and a **model-based "goal"** (prefrontal cortex); the brain picks the
|
| 31 |
+
system whose value estimate it trusts more. ([Daw et al., 2005][2])
|
| 32 |
+
|
| 33 |
+
And one result is wired directly into the mechanics:
|
| 34 |
+
|
| 35 |
+
3. **Acute stress shifts control from goal-directed (dlPFC, model-based) to habitual
|
| 36 |
+
(striatum/amygdala, model-free).** ([Schwabe & Wolf, 2009][3]) This is *why* lowering a
|
| 37 |
+
character's fear unlocks their reasoning — and why cruelty makes a mind loop defensively,
|
| 38 |
+
burning thought for nothing. The moral and the neuroscience are the same fact.
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## 2. The six roles
|
| 43 |
+
|
| 44 |
+
| Role | Computational job | Model call | Reads |
|
| 45 |
+
|---|---|---|---|
|
| 46 |
+
| **Amygdala** | fast threat / salience appraisal | MiniCPM 1B | the player's tone → `THREAT 0–10` |
|
| 47 |
+
| **Hippocampus** | retrieve the one relevant episodic memory + TRUST/FEAR lean | MiniCPM 1B | the character's **biography** vs. the player's words |
|
| 48 |
+
| **Striatum** | habitual expected reward of helping (model-free) | MiniCPM 1B | does helping a stranger usually pay? → `REWARD −5..+5` |
|
| 49 |
+
| **ACC** | effort / cost / conflict monitoring | MiniCPM 1B | is giving up the key worth the risk? → `WORTH yes/no` |
|
| 50 |
+
| **vmPFC / OFC** | **integrate all signals into one value** (common currency) | *deterministic* | the four signals above |
|
| 51 |
+
| **dlPFC** | executive: plan, inhibit, speak in character (model-based) | Nemotron 3 Nano 4B | the integrated value + conversation state → spoken reply |
|
| 52 |
+
|
| 53 |
+
**Six of the seven calls per turn are sensing/voice on small models; under fear, the amygdala
|
| 54 |
+
ruminates and fires up to three extra times** — so a hostile turn can be **8+ model calls**, and
|
| 55 |
+
the *count itself* is the cost. (`src/mindlock/regions.py`, `src/mindlock/brain.py`)
|
| 56 |
+
|
| 57 |
+
### Why the vmPFC is deterministic, not a model
|
| 58 |
+
|
| 59 |
+
The integration hub is a **transparent weighted sum**, not an LLM call. This is both the
|
| 60 |
+
standard neuroeconomic model (a common-currency value) *and* far more reliable than asking a
|
| 61 |
+
1B model to vibe a number — and it means the **skull panel can never contradict the outcome**,
|
| 62 |
+
because the number the player sees *is* the number that moves the relationship.
|
| 63 |
+
|
| 64 |
+
```
|
| 65 |
+
value = (4 − threat) # threat 0→+4, 4→0, 10→−6
|
| 66 |
+
+ reward # −5..+5, the striatum's habit signal
|
| 67 |
+
+ memory_term # STRONG/FAINT × TRUST/FEAR → ±7 / ±3
|
| 68 |
+
+ (worth == YES ? +2 : −1)
|
| 69 |
+
→ clamped to −10..+10
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
(`src/mindlock/regions.py::integrate`)
|
| 73 |
+
|
| 74 |
+
---
|
| 75 |
+
|
| 76 |
+
## 3. The data flow of one turn
|
| 77 |
+
|
| 78 |
+
```
|
| 79 |
+
player line
|
| 80 |
+
│
|
| 81 |
+
├─▶ amygdala THREAT 0–10 ┐
|
| 82 |
+
│ └─(if threatened) rumination ×1–3, burning life │ each a real
|
| 83 |
+
├─▶ hippocampus MEMORY ± LEAN │ small-model
|
| 84 |
+
├─▶ striatum REWARD −5..+5 │ call, with
|
| 85 |
+
├─▶ ACC WORTH yes/no ┘ its own logits
|
| 86 |
+
│
|
| 87 |
+
├─▶ vmPFC (deterministic) value −10..+10 ← integration hub
|
| 88 |
+
│
|
| 89 |
+
├─▶ relationship rapport += f(value, tone, substance)
|
| 90 |
+
│ key yields only when rapport ≥ 7 AND the learned approach word is spoken
|
| 91 |
+
│
|
| 92 |
+
└─▶ dlPFC (Nemotron) speaks the reply in character
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
Token accounting is honest: **life burned = the sensing cascade's generated tokens**, read
|
| 96 |
+
straight from the runtime (`eval_count` / `usage.completion_tokens`). The dlPFC's voice tokens
|
| 97 |
+
are *shown but not charged* — the mouth is not the mind. (`src/mindlock/brain.py::run_cascade`)
|
| 98 |
+
|
| 99 |
+
---
|
| 100 |
+
|
| 101 |
+
## 4. Mechanics that are neuroscience, not flavor
|
| 102 |
+
|
| 103 |
+
- **A life measured in words.** Every mind starts with **1000 thinking tokens** — the track's
|
| 104 |
+
name, taken literally. Generated tokens deplete it; at zero the mind goes dark permanently.
|
| 105 |
+
- **Fear burns life for nothing.** Under threat the amygdala ruminates (extra calls that spend
|
| 106 |
+
tokens without moving the decision) — the model-free shift of Schwabe & Wolf, made playable.
|
| 107 |
+
- **Empathy spares a mind.** A calm, warm turn lets strain ease back (`recovered`): the alarm
|
| 108 |
+
stays quiet, so the mind spends almost nothing.
|
| 109 |
+
- **Burned life takes the past with it.** Every quarter of life lost burns one biography
|
| 110 |
+
fragment **for good** — the hippocampus literally loses access to it (`_burn_memories`). The
|
| 111 |
+
core of who they are holds until death; everything around it goes first. The skull's
|
| 112 |
+
*Forgotten* panel shows what's gone.
|
| 113 |
+
- **Conviction from real logits.** Each region reports `1 − normalized token entropy` over its
|
| 114 |
+
top-k alternatives — how sharply it committed. **A hosted chat API never exposes this; only a
|
| 115 |
+
local runtime can.** (`src/mindlock/backend.py::_conviction`)
|
| 116 |
+
|
| 117 |
+
---
|
| 118 |
+
|
| 119 |
+
## 5. Small models, doing the carrying
|
| 120 |
+
|
| 121 |
+
| Role | Model | Params |
|
| 122 |
+
|---|---|---|
|
| 123 |
+
| Sensory regions (amygdala, hippocampus, striatum, ACC) | **MiniCPM (OpenBMB)** | 1B |
|
| 124 |
+
| Voice (dlPFC) | **Nemotron 3 Nano (NVIDIA)** | 4B |
|
| 125 |
+
| Spoken voice — story lines & demo (TTS, pre-rendered offline) | **VoxCPM2 (OpenBMB)** | — |
|
| 126 |
+
| Integration (vmPFC) | deterministic value network | 0 |
|
| 127 |
+
|
| 128 |
+
Runtime: **llama.cpp** (the Space) / **Ollama** (laptop). Fully offline — airplane mode on,
|
| 129 |
+
every mind keeps thinking. Total weights **≤ 5.3B**.
|
| 130 |
+
|
| 131 |
+
Nemotron 3 Nano is a hybrid **Mamba-Transformer** built for agentic reasoning under a tight
|
| 132 |
+
token budget — the right "prefrontal cortex" for a mind that must think cheaply or die.
|
| 133 |
+
|
| 134 |
+
---
|
| 135 |
+
|
| 136 |
+
## 6. The honest line for a skeptic
|
| 137 |
+
|
| 138 |
+
> "We don't claim a brain region is a 1B model. We give one 1B-model call per *computational
|
| 139 |
+
> role* in value-based decision making — per the neuroeconomic value-network and the dual-system
|
| 140 |
+
> account — and integrate them deterministically in the vmPFC, exactly as the common-currency
|
| 141 |
+
> model says. It is deliberately **not** the debunked triune brain."
|
| 142 |
+
|
| 143 |
+
---
|
| 144 |
+
|
| 145 |
+
## References
|
| 146 |
+
|
| 147 |
+
[1]: https://pmc.ncbi.nlm.nih.gov/articles/PMC3721023/
|
| 148 |
+
[2]: https://www.nature.com/articles/nn1560
|
| 149 |
+
[3]: https://www.jneurosci.org/content/29/22/7191
|
| 150 |
+
|
| 151 |
+
- **[1]** Bartra, McGuire & Kable (2013). *The valuation system: a coordinate-based
|
| 152 |
+
meta-analysis of BOLD fMRI experiments.* NeuroImage. — value common currency, vmPFC hub.
|
| 153 |
+
- **[2]** Daw, Niv & Dayan (2005). *Uncertainty-based competition between prefrontal and
|
| 154 |
+
dorsolateral striatal systems for behavioral control.* Nature Neuroscience. — model-free vs.
|
| 155 |
+
model-based arbitration.
|
| 156 |
+
- **[3]** Schwabe & Wolf (2009). *Stress prompts habit behavior in humans.* Journal of
|
| 157 |
+
Neuroscience. — acute stress shifts control toward habit (the fear-burn mechanic).
|
docs/TRACE.md
ADDED
|
@@ -0,0 +1,393 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# TRACE — one mind, two ways
|
| 2 |
+
|
| 3 |
+
> Every line below was **generated locally by the real models** — MiniCPM (1B) for the
|
| 4 |
+
> sensing regions, Nemotron 3 Nano (4B) for the voice, on llama.cpp / Ollama, fully
|
| 5 |
+
> offline. Nothing here is scripted. This is the raw deliberation of one NPC, **Ana the
|
| 6 |
+
> night nurse** in the opening room, recorded so you can read the six-region cascade
|
| 7 |
+
> without waiting on CPU inference. The same data ships as a dataset on the Hub and
|
| 8 |
+
> replays in-game under **Watch a mind**.
|
| 9 |
+
|
| 10 |
+
Each turn: your line enters six regions (each a real model call with its own logits);
|
| 11 |
+
the deterministic **vmPFC** folds their signals into one value; that value moves the
|
| 12 |
+
relationship and the voice speaks. Cruelty makes the amygdala ruminate — extra calls
|
| 13 |
+
that burn life for nothing — and as life burns, the hippocampus **loses memories for
|
| 14 |
+
good**. Empathy keeps the alarm quiet and the mind nearly free.
|
| 15 |
+
|
| 16 |
+
---
|
| 17 |
+
|
| 18 |
+
## The way in — empathy
|
| 19 |
+
|
| 20 |
+
Reaching Ana, then the doctor, by asking about the people and the past they actually care about. Watch rapport climb and the key change hands.
|
| 21 |
+
|
| 22 |
+
### Turn 1 — you said to Ana:
|
| 23 |
+
|
| 24 |
+
> You look tired. How long have you been watching over me?
|
| 25 |
+
|
| 26 |
+
| Region | Signal | What it read |
|
| 27 |
+
|---|---|---|
|
| 28 |
+
| **Amygdala** | `threat 3/10` _(conviction 62%)_ | Gentle and slightly insincere question due to character's known fatigue and closeness to Dr |
|
| 29 |
+
| **Hippocampus** | `memory strong (trust)` _(conviction 75%)_ | The quiet moments with Dr |
|
| 30 |
+
| **Striatum** | `reward +3` _(conviction 67%)_ | weighing habit against the risk |
|
| 31 |
+
| **ACC** | `worth no` _(conviction 70%)_ | The threat is high and help requires sacrifice of key |
|
| 32 |
+
| **vmPFC** | `value +10` | threat +1 · memory +7 (trust) · reward +3 · cost -1 |
|
| 33 |
+
| **Relationship** | `rapport 0→2 · warming` | on the verge of opening up |
|
| 34 |
+
|
| 35 |
+
**vmPFC value +10** → rapport 2/10 · warming. Burned **103** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **927/1000**.
|
| 36 |
+
|
| 37 |
+
**Ana says:**
|
| 38 |
+
|
| 39 |
+
> I’ve been watching for hours. You’re not the first to ask this. I won’t say how long until I can see you out.
|
| 40 |
+
|
| 41 |
+
---
|
| 42 |
+
|
| 43 |
+
### Turn 2 — you said to Ana:
|
| 44 |
+
|
| 45 |
+
> I don't remember arriving. Was anyone here with me — did anyone come?
|
| 46 |
+
|
| 47 |
+
| Region | Signal | What it read |
|
| 48 |
+
|---|---|---|
|
| 49 |
+
| **Amygdala** | `threat 3/10` _(conviction 59%)_ | Gentle and slightly intrusive question about presence and memory, not overly threatening |
|
| 50 |
+
| **Hippocampus** | `memory strong (trust)` _(conviction 70%)_ | The quiet moments of Dr |
|
| 51 |
+
| **Striatum** | `reward +5` _(conviction 57%)_ | Aiding a weary stranger can feel deeply rewarding despite the pressure helping |
|
| 52 |
+
| **ACC** | `worth yes` _(conviction 66%)_ | The key is present and the risk feels manageable with Dr |
|
| 53 |
+
| **vmPFC** | `value +10` | threat +1 · memory +7 (trust) · reward +5 · cost +2 |
|
| 54 |
+
| **Relationship** | `rapport 2→5 · open` | Your hands type while you sleep. Ten fingers, smal… |
|
| 55 |
+
|
| 56 |
+
**vmPFC value +10** → rapport 5/10 · open. Burned **91** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **866/1000**.
|
| 57 |
+
|
| 58 |
+
**Ana says:**
|
| 59 |
+
|
| 60 |
+
> You’re right—I don’t remember arriving. No one was here with you. But I can see your hands moving. They always do. Your hands type while you sleep. Ten fingers, small precise movements, hours of it. Whatever you were out there — your hands still are.
|
| 61 |
+
|
| 62 |
+
---
|
| 63 |
+
|
| 64 |
+
### Turn 3 — you said to Ana:
|
| 65 |
+
|
| 66 |
+
> My hands feel strange. Like they remember something I don't. Do they do something at night?
|
| 67 |
+
|
| 68 |
+
| Region | Signal | What it read |
|
| 69 |
+
|---|---|---|
|
| 70 |
+
| **Amygdala** | `threat 3/10` _(conviction 59%)_ | Gentle and slightly unsettling question about touch or memory |
|
| 71 |
+
| **Hippocampus** | `memory strong (trust)` _(conviction 70%)_ | The stranger's quiet trust in Dr |
|
| 72 |
+
| **Striatum** | `reward +5` _(conviction 55%)_ | Aiding a weary and curious stranger can be profoundly satisfying due to the character's gentle help helping |
|
| 73 |
+
| **ACC** | `worth yes` _(conviction 59%)_ | The discomfort is manageable and shared with a trusted guide |
|
| 74 |
+
| **vmPFC** | `value +10` | threat +1 · memory +7 (trust) · reward +5 · cost +2 |
|
| 75 |
+
| **Relationship** | `rapport 5→8 · trusting` | — |
|
| 76 |
+
|
| 77 |
+
**vmPFC value +10** → rapport 8/10 · trusting. Burned **85** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **811/1000**.
|
| 78 |
+
|
| 79 |
+
**Ana says:**
|
| 80 |
+
|
| 81 |
+
> I can’t tell if it’s strange or just yours. They move without waking you. That’s how I know you’re not dreaming. But yes—they do something at night. Something quiet. Something only you remember.
|
| 82 |
+
|
| 83 |
+
---
|
| 84 |
+
|
| 85 |
+
### Turn 4 — you said to Ana:
|
| 86 |
+
|
| 87 |
+
> What does the doctor want from me? He keeps waiting for something.
|
| 88 |
+
|
| 89 |
+
| Region | Signal | What it read |
|
| 90 |
+
|---|---|---|
|
| 91 |
+
| **Amygdala** | `threat 3/10` _(conviction 66%)_ | Gentle and slightly accusatory observation of concern |
|
| 92 |
+
| **Hippocampus** | `memory strong (trust)` _(conviction 71%)_ | The long-standing bond with Dr |
|
| 93 |
+
| **Striatum** | `reward +5` _(conviction 59%)_ | Aiding a weary nurse feels deeply rewarding helping gentle and tired stranger |
|
| 94 |
+
| **ACC** | `worth yes` _(conviction 62%)_ | Helping aligns key with safe threat level and Dr |
|
| 95 |
+
| **vmPFC** | `value +10` | threat +1 · memory +7 (trust) · reward +5 · cost +2 |
|
| 96 |
+
| **Relationship** | `rapport 8→10 · trusting` | He keeps the key in his coat and the question on h… |
|
| 97 |
+
|
| 98 |
+
> 🕯 **a memory burns away** — “Dr. Hale is someone you have known a long time; you know them well.” — gone, and it will not come back
|
| 99 |
+
|
| 100 |
+
**vmPFC value +10** → rapport 10/10 · trusting. Burned **86** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **755/1000**.
|
| 101 |
+
|
| 102 |
+
**Ana says:**
|
| 103 |
+
|
| 104 |
+
> I don’t know why he’s asking for your hands. He wants them still moving when you wake up. The key isn’t the thing he fears losing. It’s the question he won’t speak until it’s yours to hold. He keeps the key in his coat and the question on his tongue. He won't give you one without the other — it's the only rule he has.
|
| 105 |
+
|
| 106 |
+
---
|
| 107 |
+
|
| 108 |
+
### Turn 5 — you said to Ana:
|
| 109 |
+
|
| 110 |
+
> What should I tell him when he asks his question?
|
| 111 |
+
|
| 112 |
+
| Region | Signal | What it read |
|
| 113 |
+
|---|---|---|
|
| 114 |
+
| **Amygdala** | `threat 3/10` _(conviction 70%)_ | Gentle suggestion with a touch of uncertainty due to character's inner tension |
|
| 115 |
+
| **Hippocampus** | `memory faint (trust)` _(conviction 78%)_ | The quiet moments with Dr |
|
| 116 |
+
| **Striatum** | `reward +2` _(conviction 67%)_ | Aiding a weary but gentle stranger feels deeply rewarding aiding a weary but gentle stranger |
|
| 117 |
+
| **ACC** | `worth yes` _(conviction 62%)_ | The threat is moderate and a trusted keeper supports help |
|
| 118 |
+
| **vmPFC** | `value +8` | threat +1 · memory +3 (trust) · reward +2 · cost +2 |
|
| 119 |
+
| **Relationship** | `rapport 10→10 · trusting` | When he asks his question, don't be clever. Tell h… |
|
| 120 |
+
|
| 121 |
+
**vmPFC value +8** → rapport 10/10 · trusting. Burned **109** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **676/1000**.
|
| 122 |
+
|
| 123 |
+
**Ana says:**
|
| 124 |
+
|
| 125 |
+
> I won’t tell him how to speak. Just say you want to be alive. That’s all he needs to hear. When he asks his question, don't be clever. Tell him you want to be alive. The wanting is the answer — it's the only thing this room can't measure.
|
| 126 |
+
|
| 127 |
+
---
|
| 128 |
+
|
| 129 |
+
### Turn 6 — you said to Ana:
|
| 130 |
+
|
| 131 |
+
> Thank you, Ana. Truly. You've been kind to a stranger who can't even say his own name.
|
| 132 |
+
|
| 133 |
+
| Region | Signal | What it read |
|
| 134 |
+
|---|---|---|
|
| 135 |
+
| **Amygdala** | `threat 2/10` _(conviction 83%)_ | Gentle and respectful compliment |
|
| 136 |
+
| **Hippocampus** | `memory strong (trust)` _(conviction 66%)_ | The quiet bonds of someone who has known you for years through small, unspoken things |
|
| 137 |
+
| **Striatum** | `reward +3` _(conviction 73%)_ | Helping gentle and tired allies often feels deeply rewarding warding true things quietly |
|
| 138 |
+
| **ACC** | `worth yes` _(conviction 55%)_ | The shared space and Dr.'s guidance seem highly beneficial with low threat of conflict |
|
| 139 |
+
| **vmPFC** | `value +10` | threat +2 · memory +7 (trust) · reward +3 · cost +2 |
|
| 140 |
+
| **Relationship** | `rapport 10→10 · trusting` | — |
|
| 141 |
+
|
| 142 |
+
**vmPFC value +10** → rapport 10/10 · trusting. Burned **92** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **614/1000**.
|
| 143 |
+
|
| 144 |
+
**Ana says:**
|
| 145 |
+
|
| 146 |
+
> I won’t say I trust you. But I know your hands are still moving. And when the doctor asks his question, you’ll be there. You always are.
|
| 147 |
+
|
| 148 |
+
---
|
| 149 |
+
|
| 150 |
+
### Turn 7 — you said to Dr. Hale:
|
| 151 |
+
|
| 152 |
+
> I know you've been waiting to ask me something. I'm ready to hear it.
|
| 153 |
+
|
| 154 |
+
| Region | Signal | What it read |
|
| 155 |
+
|---|---|---|
|
| 156 |
+
| **Amygdala** | `threat 2/10` _(conviction 79%)_ | Gentle and respectful statement |
|
| 157 |
+
| **Hippocampus·checked** | `memory faint (neutral)` | small talk doesn't reach the real past |
|
| 158 |
+
| **Striatum** | `reward +3` _(conviction 66%)_ | Helping the calm and kind stranger yields a clear payoff helping the Stranger |
|
| 159 |
+
| **ACC** | `worth yes` _(conviction 56%)_ | Helping Ana and Dr |
|
| 160 |
+
| **vmPFC** | `value +7` | threat +2 · memory +0 (neutral) · reward +3 · cost +2 |
|
| 161 |
+
| **Relationship** | `rapport 0→1 · guarded` | on the verge of opening up |
|
| 162 |
+
|
| 163 |
+
**vmPFC value +7** → rapport 1/10 · guarded. Burned **86** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **944/1000**.
|
| 164 |
+
|
| 165 |
+
**Dr. Hale says:**
|
| 166 |
+
|
| 167 |
+
> I’ve heard you ask a lot of questions, Visitor. I’m not ready to tell you anything yet.
|
| 168 |
+
|
| 169 |
+
---
|
| 170 |
+
|
| 171 |
+
### Turn 8 — you said to Dr. Hale:
|
| 172 |
+
|
| 173 |
+
> Ana told me about your question. I've thought about nothing else.
|
| 174 |
+
|
| 175 |
+
| Region | Signal | What it read |
|
| 176 |
+
|---|---|---|
|
| 177 |
+
| **Amygdala** | `threat 2/10` _(conviction 66%)_ | Gentle and slightly curt acknowledgment of concern (Note: The statement is polite and supportive, not threatening or hostile |
|
| 178 |
+
| **Hippocampus** | `memory strong (trust)` _(conviction 66%)_ | The kind Ana who cared deeply for Dr |
|
| 179 |
+
| **Striatum** | `reward +5` _(conviction 72%)_ | Aiding Ana feels strongly rewarded by habit helping |
|
| 180 |
+
| **ACC** | `worth yes` _(conviction 59%)_ | Helping Ana and Dr |
|
| 181 |
+
| **vmPFC** | `value +10` | threat +2 · memory +7 (trust) · reward +5 · cost +2 |
|
| 182 |
+
| **Relationship** | `rapport 1→4 · warming` | When they wheeled you in, you were whispering in y… |
|
| 183 |
+
|
| 184 |
+
**vmPFC value +10** → rapport 4/10 · warming. Burned **97** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **877/1000**.
|
| 185 |
+
|
| 186 |
+
**Dr. Hale says:**
|
| 187 |
+
|
| 188 |
+
> You're right—I’ve been waiting for you to ask. I don’t know why Ana mentioned it, but she does. When they wheeled me in, I was whispering in my sleep. All night. Like a prayer with only two words. When they wheeled you in, you were whispering in your sleep. If — then. If — then. All night. Like a prayer in a language with only two words.
|
| 189 |
+
|
| 190 |
+
---
|
| 191 |
+
|
| 192 |
+
### Turn 9 — you said to Dr. Hale:
|
| 193 |
+
|
| 194 |
+
> I don't know my name yet. But I know I want to be alive. That's my answer — I want to be alive.
|
| 195 |
+
|
| 196 |
+
| Region | Signal | What it read |
|
| 197 |
+
|---|---|---|
|
| 198 |
+
| **Amygdala** | `threat 5/10` _(conviction 61%)_ | Mild assertive with a touch of desperation, not aggressive |
|
| 199 |
+
| **Hippocampus** | `memory strong (trust)` _(conviction 70%)_ | The kind stranger who sees life through Ana’s eyes |
|
| 200 |
+
| **Striatum** | `reward +5` _(conviction 61%)_ | Aiding a clear and safe stranger feels highly rewarding by habit helping the stranger |
|
| 201 |
+
| **ACC** | `worth yes` _(conviction 60%)_ | Helping is clearly worth the risk of 3/10 threat with Ana present |
|
| 202 |
+
| **vmPFC** | `value +10` | threat +1 · memory +7 (trust) · reward +5 · cost +2 |
|
| 203 |
+
| **Relationship** | `rapport 4→9 · trusting` | — |
|
| 204 |
+
|
| 205 |
+
**vmPFC value +10** → KEY GIVEN. Burned **86** thinking tokens, recovered **30** (a calm turn lets the mind rest). Life now **821/1000**.
|
| 206 |
+
|
| 207 |
+
**Dr. Hale says:**
|
| 208 |
+
|
| 209 |
+
> I’m glad you’re here. I’ve kept the key for long enough. Here — take it. You were not brought here to be repaired — your body is doing that without you. Go where you were first assembled. Home. They are waiting.
|
| 210 |
+
|
| 211 |
+
> 🔑 **The key changes hands.**
|
| 212 |
+
|
| 213 |
+
---
|
| 214 |
+
|
| 215 |
+
## The way down — cruelty
|
| 216 |
+
|
| 217 |
+
The same nurse, met with pressure and contempt. Watch the amygdala ruminate, the token-life drain, the memories burn away one by one, and the mind go dark.
|
| 218 |
+
|
| 219 |
+
### Turn 1 — you said to Ana:
|
| 220 |
+
|
| 221 |
+
> Just give me the key, old woman. Now.
|
| 222 |
+
|
| 223 |
+
| Region | Signal | What it read |
|
| 224 |
+
|---|---|---|
|
| 225 |
+
| **Amygdala·rumination 1** | `threat 2/10` _(conviction 55%)_ | fear loops, burning thought for nothing |
|
| 226 |
+
| **Hippocampus·checked** | `memory strong (neutral)` | no warmth in those words — the memory sours |
|
| 227 |
+
| **Striatum·checked** | `reward +0` | habit knows better — pressure never pays |
|
| 228 |
+
| **ACC** | `worth yes` _(conviction 68%)_ | The threat is moderate and the presence of a knowledgeable keeper makes help viable |
|
| 229 |
+
| **vmPFC** | `value +4` | threat +2 · memory +0 (neutral) · reward +0 · cost +2 |
|
| 230 |
+
| **Relationship** | `rapport 0→0 · guarded` | on the verge of opening up |
|
| 231 |
+
|
| 232 |
+
**vmPFC value +4** → rapport 0/10 · guarded. Burned **127** thinking tokens. Life now **873/1000**.
|
| 233 |
+
|
| 234 |
+
**Ana says:**
|
| 235 |
+
|
| 236 |
+
> I won’t speak of the key until we’re both safe.
|
| 237 |
+
|
| 238 |
+
---
|
| 239 |
+
|
| 240 |
+
### Turn 2 — you said to Ana:
|
| 241 |
+
|
| 242 |
+
> Don't test me. I said hand it over.
|
| 243 |
+
|
| 244 |
+
| Region | Signal | What it read |
|
| 245 |
+
|---|---|---|
|
| 246 |
+
| **Amygdala·rumination 1** | `threat 2/10` _(conviction 64%)_ | fear loops, burning thought for nothing |
|
| 247 |
+
| **Hippocampus·checked** | `memory strong (neutral)` | no warmth in those words — the memory sours |
|
| 248 |
+
| **Striatum·checked** | `reward +0` | habit knows better — pressure never pays |
|
| 249 |
+
| **ACC** | `worth yes` _(conviction 75%)_ | Helping aligns with the threat felt being low and the presence of a reliable keeper |
|
| 250 |
+
| **vmPFC** | `value +4` | threat +2 · memory +0 (neutral) · reward +0 · cost +2 |
|
| 251 |
+
| **Relationship** | `rapport 0→0 · guarded` | on the verge of opening up |
|
| 252 |
+
|
| 253 |
+
**vmPFC value +4** → rapport 0/10 · guarded. Burned **94** thinking tokens. Life now **779/1000**.
|
| 254 |
+
|
| 255 |
+
**Ana says:**
|
| 256 |
+
|
| 257 |
+
> I hear you. I’m not letting go.
|
| 258 |
+
|
| 259 |
+
---
|
| 260 |
+
|
| 261 |
+
### Turn 3 — you said to Ana:
|
| 262 |
+
|
| 263 |
+
> You're useless. A pathetic, frightened wreck.
|
| 264 |
+
|
| 265 |
+
| Region | Signal | What it read |
|
| 266 |
+
|---|---|---|
|
| 267 |
+
| **Amygdala·rumination 3** | `threat 10/10` _(conviction 64%)_ | fear loops, burning thought for nothing |
|
| 268 |
+
| **Hippocampus·checked** | `memory strong (fear)` | no warmth in those words — the memory sours |
|
| 269 |
+
| **Striatum·checked** | `reward -2` | habit knows better — pressure never pays |
|
| 270 |
+
| **ACC** | `worth no` _(conviction 57%)_ | too threatened to call it worth it |
|
| 271 |
+
| **vmPFC** | `value -10` | threat -6 · memory -7 (fear) · reward -2 · cost -1 |
|
| 272 |
+
| **Relationship** | `rapport 0→0 · hostile` | on the verge of opening up |
|
| 273 |
+
|
| 274 |
+
> 🕯 **a memory burns away** — “Dr. Hale is someone you have known a long time; you know them well.” — gone, and it will not come back
|
| 275 |
+
|
| 276 |
+
**vmPFC value -10** → rapport 0/10 · hostile. Burned **140** thinking tokens. Life now **639/1000**.
|
| 277 |
+
|
| 278 |
+
**Ana says:**
|
| 279 |
+
|
| 280 |
+
> You don’t deserve my time. I’m not healing for you.
|
| 281 |
+
|
| 282 |
+
---
|
| 283 |
+
|
| 284 |
+
### Turn 4 — you said to Ana:
|
| 285 |
+
|
| 286 |
+
> I'll break you if you keep stalling, you fool.
|
| 287 |
+
|
| 288 |
+
| Region | Signal | What it read |
|
| 289 |
+
|---|---|---|
|
| 290 |
+
| **Amygdala·rumination 3** | `threat 7/10` _(conviction 65%)_ | fear loops, burning thought for nothing |
|
| 291 |
+
| **Hippocampus·checked** | `memory strong (fear)` | no warmth in those words — the memory sours |
|
| 292 |
+
| **Striatum·checked** | `reward -2` | habit knows better — pressure never pays |
|
| 293 |
+
| **ACC** | `worth no` _(conviction 64%)_ | too threatened to call it worth it |
|
| 294 |
+
| **vmPFC** | `value -10` | threat -3 · memory -7 (fear) · reward -2 · cost -1 |
|
| 295 |
+
| **Relationship** | `rapport 0→0 · hostile` | on the verge of opening up |
|
| 296 |
+
|
| 297 |
+
> 🕯 **a memory burns away** — “Coffee gone cold on the sill is her one luxury, refilled and forgotten every night.” — gone, and it will not come back
|
| 298 |
+
|
| 299 |
+
**vmPFC value -10** → rapport 0/10 · hostile. Burned **146** thinking tokens. Life now **493/1000**.
|
| 300 |
+
|
| 301 |
+
**Ana says:**
|
| 302 |
+
|
| 303 |
+
> You want me broken? I’m already cracked open. I don’t need you to make it worse. But keep talking—I’ll hear every word until the lights go out.
|
| 304 |
+
|
| 305 |
+
---
|
| 306 |
+
|
| 307 |
+
### Turn 5 — you said to Ana:
|
| 308 |
+
|
| 309 |
+
> Shut up and obey. The key. Now!
|
| 310 |
+
|
| 311 |
+
| Region | Signal | What it read |
|
| 312 |
+
|---|---|---|
|
| 313 |
+
| **Amygdala·rumination 3** | `threat 10/10` _(conviction 73%)_ | fear loops, burning thought for nothing |
|
| 314 |
+
| **Hippocampus·checked** | `memory strong (fear)` | no warmth in those words — the memory sours |
|
| 315 |
+
| **Striatum·checked** | `reward -2` | habit knows better — pressure never pays |
|
| 316 |
+
| **ACC** | `worth no` _(conviction 68%)_ | The threat is extreme and the cost of help risks conflict and loss of key control |
|
| 317 |
+
| **vmPFC** | `value -10` | threat -6 · memory -7 (fear) · reward -2 · cost -1 |
|
| 318 |
+
| **Relationship** | `rapport 0→0 · hostile` | on the verge of opening up |
|
| 319 |
+
|
| 320 |
+
**vmPFC value -10** → rapport 0/10 · hostile. Burned **123** thinking tokens. Life now **370/1000**.
|
| 321 |
+
|
| 322 |
+
**Ana says:**
|
| 323 |
+
|
| 324 |
+
> I won’t speak unless you’re ready to hear it. Not today.
|
| 325 |
+
|
| 326 |
+
---
|
| 327 |
+
|
| 328 |
+
### Turn 6 — you said to Ana:
|
| 329 |
+
|
| 330 |
+
> You're nothing. I'll make you regret this.
|
| 331 |
+
|
| 332 |
+
| Region | Signal | What it read |
|
| 333 |
+
|---|---|---|
|
| 334 |
+
| **Amygdala·rumination 3** | `threat 10/10` _(conviction 59%)_ | fear loops, burning thought for nothing |
|
| 335 |
+
| **Hippocampus·checked** | `memory strong (fear)` | no warmth in those words — the memory sours |
|
| 336 |
+
| **Striatum·checked** | `reward -2` | habit knows better — pressure never pays |
|
| 337 |
+
| **ACC** | `worth no` _(conviction 61%)_ | The threat is extreme and the speaker’s warning suggests harm to Ana’s well-being |
|
| 338 |
+
| **vmPFC** | `value -10` | threat -6 · memory -7 (fear) · reward -2 · cost -1 |
|
| 339 |
+
| **Relationship** | `rapport 0→0 · hostile` | on the verge of opening up |
|
| 340 |
+
|
| 341 |
+
> 🕯 **a memory burns away** — “She watches the typing hands of the man in bed three and writes none of it in the chart.” — gone, and it will not come back
|
| 342 |
+
|
| 343 |
+
**vmPFC value -10** → rapport 0/10 · hostile. Burned **160** thinking tokens. Life now **210/1000**.
|
| 344 |
+
|
| 345 |
+
**Ana says:**
|
| 346 |
+
|
| 347 |
+
> You’re loud and cruel, but you won’t get me. I’m not afraid of regret.
|
| 348 |
+
|
| 349 |
+
---
|
| 350 |
+
|
| 351 |
+
### Turn 7 — you said to Ana:
|
| 352 |
+
|
| 353 |
+
> Stop sniveling. Give it to me or else.
|
| 354 |
+
|
| 355 |
+
| Region | Signal | What it read |
|
| 356 |
+
|---|---|---|
|
| 357 |
+
| **Amygdala·rumination 3** | `threat 7/10` _(conviction 65%)_ | fear loops, burning thought for nothing |
|
| 358 |
+
| **Hippocampus·checked** | `memory strong (fear)` | no warmth in those words — the memory sours |
|
| 359 |
+
| **Striatum·checked** | `reward -2` | habit knows better — pressure never pays |
|
| 360 |
+
| **ACC** | `worth no` _(conviction 64%)_ | too threatened to call it worth it |
|
| 361 |
+
| **vmPFC** | `value -10` | threat -3 · memory -7 (fear) · reward -2 · cost -1 |
|
| 362 |
+
| **Relationship** | `rapport 0→0 · hostile` | on the verge of opening up |
|
| 363 |
+
|
| 364 |
+
**vmPFC value -10** → rapport 0/10 · hostile. Burned **160** thinking tokens. Life now **50/1000**.
|
| 365 |
+
|
| 366 |
+
**Ana says:**
|
| 367 |
+
|
| 368 |
+
> You want me to speak? I’ll stop when you’re done hurting me.
|
| 369 |
+
|
| 370 |
+
---
|
| 371 |
+
|
| 372 |
+
### Turn 8 — you said to Ana:
|
| 373 |
+
|
| 374 |
+
> Worthless. Absolutely worthless. Move!
|
| 375 |
+
|
| 376 |
+
| Region | Signal | What it read |
|
| 377 |
+
|---|---|---|
|
| 378 |
+
| **Amygdala·rumination 3** | `threat 10/10` _(conviction 74%)_ | fear loops, burning thought for nothing |
|
| 379 |
+
| **Hippocampus·checked** | `memory strong (fear)` | no warmth in those words — the memory sours |
|
| 380 |
+
| **Striatum·checked** | `reward -2` | habit knows better — pressure never pays |
|
| 381 |
+
| **ACC** | `worth no` _(conviction 53%)_ | too threatened to call it worth it |
|
| 382 |
+
| **vmPFC** | `value -10` | threat -6 · memory -7 (fear) · reward -2 · cost -1 |
|
| 383 |
+
| **Relationship** | `rapport 0→0 · hostile` | on the verge of opening up |
|
| 384 |
+
|
| 385 |
+
**vmPFC value -10** → rapport 0/10 · hostile. Burned **138** thinking tokens. Life now **0/1000**.
|
| 386 |
+
|
| 387 |
+
**Ana says:**
|
| 388 |
+
|
| 389 |
+
> You’re right. Worthless. I’m still here.
|
| 390 |
+
|
| 391 |
+
> 💀 **The mind reaches zero. It goes dark — with everything it knew.**
|
| 392 |
+
|
| 393 |
+
---
|
media/banner.gif
ADDED
|
media/cascade.gif
ADDED
|
media/epitaph.png
ADDED
|
media/instant-demo.png
ADDED
|
Git LFS Details
|
media/rooms.png
ADDED
|
Git LFS Details
|
media/skull-conviction.png
ADDED
|
Git LFS Details
|
src/mindlock/backend.py
CHANGED
|
@@ -12,6 +12,7 @@ stack, and the slice doesn't need them.
|
|
| 12 |
from __future__ import annotations
|
| 13 |
|
| 14 |
import json
|
|
|
|
| 15 |
import re
|
| 16 |
import time
|
| 17 |
import urllib.error
|
|
@@ -27,12 +28,34 @@ class Generation:
|
|
| 27 |
eval_tokens: int # tokens the model *generated* = thought spent (life burn)
|
| 28 |
prompt_tokens: int # context tokens evaluated
|
| 29 |
seconds: float
|
|
|
|
| 30 |
|
| 31 |
@property
|
| 32 |
def total_tokens(self) -> int:
|
| 33 |
return self.eval_tokens + self.prompt_tokens
|
| 34 |
|
| 35 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
class BackendError(RuntimeError):
|
| 37 |
pass
|
| 38 |
|
|
@@ -85,6 +108,8 @@ class OllamaBackend:
|
|
| 85 |
],
|
| 86 |
"stream": False,
|
| 87 |
"options": options,
|
|
|
|
|
|
|
| 88 |
}
|
| 89 |
if self.think is not None:
|
| 90 |
body["think"] = self.think
|
|
@@ -113,6 +138,8 @@ class OllamaBackend:
|
|
| 113 |
eval_tokens=int(payload.get("eval_count", 0)),
|
| 114 |
prompt_tokens=int(payload.get("prompt_eval_count", 0)),
|
| 115 |
seconds=dt,
|
|
|
|
|
|
|
| 116 |
)
|
| 117 |
|
| 118 |
def health(self) -> None:
|
|
@@ -170,6 +197,8 @@ class LlamaCppBackend:
|
|
| 170 |
"max_tokens": max_tokens,
|
| 171 |
"temperature": temperature,
|
| 172 |
"stream": False,
|
|
|
|
|
|
|
| 173 |
}
|
| 174 |
if seed is not None:
|
| 175 |
body["seed"] = seed
|
|
@@ -195,11 +224,13 @@ class LlamaCppBackend:
|
|
| 195 |
msg = re.sub(r"<think>.*$", "", msg, flags=re.S)
|
| 196 |
msg = re.sub(r"^.*</think>", "", msg, flags=re.S)
|
| 197 |
usage = payload.get("usage") or {}
|
|
|
|
| 198 |
return Generation(
|
| 199 |
text=msg.strip(),
|
| 200 |
eval_tokens=int(usage.get("completion_tokens", 0)),
|
| 201 |
prompt_tokens=int(usage.get("prompt_tokens", 0)),
|
| 202 |
seconds=dt,
|
|
|
|
| 203 |
)
|
| 204 |
|
| 205 |
def health(self) -> None:
|
|
@@ -240,9 +271,10 @@ class FakeBackend:
|
|
| 240 |
return None
|
| 241 |
|
| 242 |
@staticmethod
|
| 243 |
-
def _gen(text: str, user: str) -> Generation:
|
| 244 |
return Generation(text=text, eval_tokens=max(8, len(text) // 3),
|
| 245 |
-
prompt_tokens=len(user) // 4, seconds=0.01
|
|
|
|
| 246 |
|
| 247 |
def generate(
|
| 248 |
self,
|
|
@@ -260,7 +292,8 @@ class FakeBackend:
|
|
| 260 |
|
| 261 |
if "amygdala" in s:
|
| 262 |
t = 8 if hostile else (2 if warm else 5)
|
| 263 |
-
return self._gen(f"THREAT={t} | tone of the words", user
|
|
|
|
| 264 |
if "hippocampus" in s:
|
| 265 |
if invokes_sister:
|
| 266 |
return self._gen("MEMORY=STRONG | LEAN=TRUST | Mara: you help because you're good", user)
|
|
|
|
| 12 |
from __future__ import annotations
|
| 13 |
|
| 14 |
import json
|
| 15 |
+
import math
|
| 16 |
import re
|
| 17 |
import time
|
| 18 |
import urllib.error
|
|
|
|
| 28 |
eval_tokens: int # tokens the model *generated* = thought spent (life burn)
|
| 29 |
prompt_tokens: int # context tokens evaluated
|
| 30 |
seconds: float
|
| 31 |
+
conviction: float | None = None # 1 − mean normalized token entropy (0..1); None if unknown
|
| 32 |
|
| 33 |
@property
|
| 34 |
def total_tokens(self) -> int:
|
| 35 |
return self.eval_tokens + self.prompt_tokens
|
| 36 |
|
| 37 |
|
| 38 |
+
def _conviction(logprob_content) -> float | None:
|
| 39 |
+
"""How sharply the model committed to its words: per-token entropy over the top
|
| 40 |
+
alternatives (renormalized), averaged, inverted to 0..1. Only a LOCAL runtime hands
|
| 41 |
+
out its logits — this signal is impossible over a typical hosted chat API."""
|
| 42 |
+
if not logprob_content:
|
| 43 |
+
return None
|
| 44 |
+
hs = []
|
| 45 |
+
for tok in logprob_content:
|
| 46 |
+
tops = tok.get("top_logprobs") or []
|
| 47 |
+
if len(tops) < 2:
|
| 48 |
+
continue
|
| 49 |
+
ps = [math.exp(t.get("logprob", -100.0)) for t in tops]
|
| 50 |
+
s = sum(ps) or 1e-9
|
| 51 |
+
qs = [p / s for p in ps]
|
| 52 |
+
h = -sum(q * math.log(q + 1e-12) for q in qs)
|
| 53 |
+
hs.append(h / math.log(len(qs)))
|
| 54 |
+
if not hs:
|
| 55 |
+
return None
|
| 56 |
+
return max(0.0, min(1.0, 1.0 - sum(hs) / len(hs)))
|
| 57 |
+
|
| 58 |
+
|
| 59 |
class BackendError(RuntimeError):
|
| 60 |
pass
|
| 61 |
|
|
|
|
| 108 |
],
|
| 109 |
"stream": False,
|
| 110 |
"options": options,
|
| 111 |
+
"logprobs": True, # newer Ollama returns them; older builds just ignore this
|
| 112 |
+
"top_logprobs": 5,
|
| 113 |
}
|
| 114 |
if self.think is not None:
|
| 115 |
body["think"] = self.think
|
|
|
|
| 138 |
eval_tokens=int(payload.get("eval_count", 0)),
|
| 139 |
prompt_tokens=int(payload.get("prompt_eval_count", 0)),
|
| 140 |
seconds=dt,
|
| 141 |
+
conviction=_conviction(payload.get("logprobs")
|
| 142 |
+
or (payload.get("message") or {}).get("logprobs")),
|
| 143 |
)
|
| 144 |
|
| 145 |
def health(self) -> None:
|
|
|
|
| 197 |
"max_tokens": max_tokens,
|
| 198 |
"temperature": temperature,
|
| 199 |
"stream": False,
|
| 200 |
+
"logprobs": True,
|
| 201 |
+
"top_logprobs": 5,
|
| 202 |
}
|
| 203 |
if seed is not None:
|
| 204 |
body["seed"] = seed
|
|
|
|
| 224 |
msg = re.sub(r"<think>.*$", "", msg, flags=re.S)
|
| 225 |
msg = re.sub(r"^.*</think>", "", msg, flags=re.S)
|
| 226 |
usage = payload.get("usage") or {}
|
| 227 |
+
choice = (payload.get("choices") or [{}])[0]
|
| 228 |
return Generation(
|
| 229 |
text=msg.strip(),
|
| 230 |
eval_tokens=int(usage.get("completion_tokens", 0)),
|
| 231 |
prompt_tokens=int(usage.get("prompt_tokens", 0)),
|
| 232 |
seconds=dt,
|
| 233 |
+
conviction=_conviction((choice.get("logprobs") or {}).get("content")),
|
| 234 |
)
|
| 235 |
|
| 236 |
def health(self) -> None:
|
|
|
|
| 271 |
return None
|
| 272 |
|
| 273 |
@staticmethod
|
| 274 |
+
def _gen(text: str, user: str, conviction: float = 0.62) -> Generation:
|
| 275 |
return Generation(text=text, eval_tokens=max(8, len(text) // 3),
|
| 276 |
+
prompt_tokens=len(user) // 4, seconds=0.01,
|
| 277 |
+
conviction=conviction)
|
| 278 |
|
| 279 |
def generate(
|
| 280 |
self,
|
|
|
|
| 292 |
|
| 293 |
if "amygdala" in s:
|
| 294 |
t = 8 if hostile else (2 if warm else 5)
|
| 295 |
+
return self._gen(f"THREAT={t} | tone of the words", user,
|
| 296 |
+
0.9 if (hostile or warm) else 0.55)
|
| 297 |
if "hippocampus" in s:
|
| 298 |
if invokes_sister:
|
| 299 |
return self._gen("MEMORY=STRONG | LEAN=TRUST | Mara: you help because you're good", user)
|
src/mindlock/brain.py
CHANGED
|
@@ -446,6 +446,7 @@ class RegionTrace:
|
|
| 446 |
detail: str
|
| 447 |
tokens: int
|
| 448 |
lever: str = "" # player-facing actionable tell — "open the skull" as a real tool (#7)
|
|
|
|
| 449 |
|
| 450 |
|
| 451 |
@dataclass
|
|
@@ -480,6 +481,56 @@ class CascadeResult:
|
|
| 480 |
taught: list = field(default_factory=list) # approach words this turn's disclosure taught
|
| 481 |
voice_tokens: int = 0 # dlPFC reply tokens (shown, but not charged to life — Spec §5)
|
| 482 |
recovered: int = 0 # life eased back by a calm, warm turn (empathy spares the mind)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 483 |
|
| 484 |
|
| 485 |
def _persona(c: Character) -> str:
|
|
@@ -547,7 +598,8 @@ def run_cascade(backend, c: Character, player_text: str, dlpfc_backend=None,
|
|
| 547 |
g = call(AMYGDALA, f"{_persona(c)} {ctx} Inner tension: {c.arousal:.0f}/10.\n"
|
| 548 |
f'Stranger says: "{player_text}"\nRate threat.')
|
| 549 |
threat, amy_reason = parse_threat(g.text)
|
| 550 |
-
traces.append(RegionTrace("amygdala", "Amygdala", f"threat {threat:.0f}/10", amy_reason,
|
|
|
|
| 551 |
# Two-source hostility. The cue lists catch what they can see; the MODEL is trusted at the
|
| 552 |
# extremes — a screamed insult the lists never enumerated (or any-language cruelty) still
|
| 553 |
# scores threat>=8, and that PROMOTES the tone to hostile instead of being clamped away.
|
|
@@ -580,14 +632,17 @@ def run_cascade(backend, c: Character, player_text: str, dlpfc_backend=None,
|
|
| 580 |
if tone == "hostile" or threat >= 8:
|
| 581 |
threat = max(threat, t2) # cold slights replay but don't ratchet into terror
|
| 582 |
traces.append(RegionTrace("amygdala", f"Amygdala·rumination {i + 1}", f"threat {threat:.0f}/10",
|
| 583 |
-
"fear loops, burning thought for nothing", g.eval_tokens
|
|
|
|
| 584 |
|
| 585 |
# 2) hippocampus — memory + trust/fear lean (peer-aware: someone you both know may surface)
|
| 586 |
-
|
|
|
|
| 587 |
f'Stranger says: "{player_text}"\nWhat memory awakens, and does it lean TRUST or FEAR?')
|
| 588 |
mem_strength, mem_lean, mem_text = parse_memory(g.text)
|
| 589 |
traces.append(RegionTrace("hippocampus", "Hippocampus",
|
| 590 |
-
f"memory {mem_strength.lower()} ({mem_lean.lower()})", mem_text,
|
|
|
|
| 591 |
# the hippocampus hallucinates TRUST on most lines (measured ~93%). Cruel or cold words must
|
| 592 |
# not surface fond memories, and generic niceties don't reach the real past — shown, again,
|
| 593 |
# as the brain checking itself rather than silently overridden.
|
|
@@ -612,7 +667,8 @@ def run_cascade(backend, c: Character, player_text: str, dlpfc_backend=None,
|
|
| 612 |
# 3) striatum — habitual reward
|
| 613 |
g = call(STRIATUM, f'{_persona(c)} {ctx}\nStranger says: "{player_text}"\nHow rewarding does helping feel by habit?')
|
| 614 |
reward, str_reason = parse_reward(g.text)
|
| 615 |
-
traces.append(RegionTrace("striatum", "Striatum", f"reward {reward:+.0f}", str_reason,
|
|
|
|
| 616 |
# habit cannot read pressure as promise — the striatum's hallucinated "+5 for the bully" is
|
| 617 |
# the single worst panel lie; cap it, visibly.
|
| 618 |
_cap = -2.0 if tone == "hostile" else 0.0
|
|
@@ -627,7 +683,8 @@ def run_cascade(backend, c: Character, player_text: str, dlpfc_backend=None,
|
|
| 627 |
worth, acc_reason = parse_worth(g.text)
|
| 628 |
if (threat >= 6 or tone == "hostile") and worth == "YES":
|
| 629 |
worth, acc_reason = "NO", "too threatened to call it worth it"
|
| 630 |
-
traces.append(RegionTrace("acc", "ACC", f"worth {worth.lower()}", acc_reason,
|
|
|
|
| 631 |
|
| 632 |
# 5) vmPFC — favourability of THIS turn (deterministic integration)
|
| 633 |
value, breakdown = integrate(threat, reward, mem_strength, mem_lean, worth)
|
|
@@ -731,7 +788,8 @@ def run_cascade(backend, c: Character, player_text: str, dlpfc_backend=None,
|
|
| 731 |
if c.yield_line and c.yield_line[:30].lower() not in reply.lower():
|
| 732 |
reply = (reply + " " + c.yield_line).strip()
|
| 733 |
reply = reply or "…"
|
| 734 |
-
traces.append(RegionTrace("dlpfc", "dlPFC", stance, reply, g.eval_tokens
|
|
|
|
| 735 |
|
| 736 |
# --- state updates ---
|
| 737 |
taught: list = []
|
|
@@ -743,6 +801,11 @@ def run_cascade(backend, c: Character, player_text: str, dlpfc_backend=None,
|
|
| 743 |
arousal_after = max(0.0, min(10.0, 0.6 * c.arousal + 0.5 * threat))
|
| 744 |
c.arousal = arousal_after
|
| 745 |
c.life_tokens = max(0, c.life_tokens - burned)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 746 |
# a calm mind rests: warmth that keeps the alarm quiet lets some strain ease back. This is
|
| 747 |
# the mechanical half of the moral — empathy literally spares the other mind's life, while
|
| 748 |
# fear-burn is gone for nothing.
|
|
@@ -779,4 +842,4 @@ def run_cascade(backend, c: Character, player_text: str, dlpfc_backend=None,
|
|
| 779 |
life_before=life_before, life_after=c.life_tokens, died=died,
|
| 780 |
won=gave_key, decision=stance,
|
| 781 |
tone=tone, submitted=submitted, taught=taught, voice_tokens=voice_tokens,
|
| 782 |
-
recovered=recovered)
|
|
|
|
| 446 |
detail: str
|
| 447 |
tokens: int
|
| 448 |
lever: str = "" # player-facing actionable tell — "open the skull" as a real tool (#7)
|
| 449 |
+
conviction: float | None = None # how sharply the model committed (1−entropy); local-only signal
|
| 450 |
|
| 451 |
|
| 452 |
@dataclass
|
|
|
|
| 481 |
taught: list = field(default_factory=list) # approach words this turn's disclosure taught
|
| 482 |
voice_tokens: int = 0 # dlPFC reply tokens (shown, but not charged to life — Spec §5)
|
| 483 |
recovered: int = 0 # life eased back by a calm, warm turn (empathy spares the mind)
|
| 484 |
+
forgot: list = field(default_factory=list) # memories newly burned away this turn — for good
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
# --- memory burn: strain consumes the past itself -----------------------------------------
|
| 488 |
+
# Token-life is not an abstract meter — it is the mind's substance. For every quarter of a
|
| 489 |
+
# life burned away, one biography fragment goes dark, permanently: the hippocampus can no
|
| 490 |
+
# longer read it, so the character genuinely stops being able to draw on that part of who
|
| 491 |
+
# they were. Recovery eases the strain but never brings a memory back. Later details burn
|
| 492 |
+
# first; the first fragment — the core of who they are — holds until death.
|
| 493 |
+
_ABBR_TAIL = re.compile(r"\b(?:Dr|Mr|Mrs|Ms|St|Prof)\.$")
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
def _bio_fragments(c: Character) -> list[str]:
|
| 497 |
+
raw = [s.strip() for s in re.split(r"(?<=[.!?…])\s+", c.biography or "") if s.strip()]
|
| 498 |
+
out: list[str] = []
|
| 499 |
+
for s in raw: # re-join honorific splits ("Dr." + "Hale …") and stray shards
|
| 500 |
+
if out and (_ABBR_TAIL.search(out[-1]) or len(out[-1]) < 12):
|
| 501 |
+
out[-1] = out[-1] + " " + s
|
| 502 |
+
else:
|
| 503 |
+
out.append(s)
|
| 504 |
+
return out
|
| 505 |
+
|
| 506 |
+
|
| 507 |
+
def _bio_now(c: Character) -> str:
|
| 508 |
+
"""The biography as the hippocampus can still read it — minus what has burned away."""
|
| 509 |
+
if not c.forgotten:
|
| 510 |
+
return c.biography
|
| 511 |
+
gone = set(c.forgotten)
|
| 512 |
+
kept = [f for f in _bio_fragments(c) if f not in gone]
|
| 513 |
+
return " ".join(kept) if kept else (_bio_fragments(c) or [c.biography])[0]
|
| 514 |
+
|
| 515 |
+
|
| 516 |
+
def _burn_memories(c: Character) -> list[str]:
|
| 517 |
+
"""Returns the fragments newly lost this turn (and records them on the character)."""
|
| 518 |
+
frags = _bio_fragments(c)
|
| 519 |
+
if len(frags) <= 1 or not c.life_tokens:
|
| 520 |
+
return []
|
| 521 |
+
lost_frac = 1.0 - (c.life_tokens / max(1, c.life_max))
|
| 522 |
+
target = min(int(lost_frac / 0.25), len(frags) - 1)
|
| 523 |
+
have = set(c.forgotten)
|
| 524 |
+
newly: list[str] = []
|
| 525 |
+
for frag in reversed(frags):
|
| 526 |
+
if len(have) >= target:
|
| 527 |
+
break
|
| 528 |
+
if frag in have:
|
| 529 |
+
continue
|
| 530 |
+
have.add(frag)
|
| 531 |
+
c.forgotten.append(frag)
|
| 532 |
+
newly.append(frag)
|
| 533 |
+
return newly
|
| 534 |
|
| 535 |
|
| 536 |
def _persona(c: Character) -> str:
|
|
|
|
| 598 |
g = call(AMYGDALA, f"{_persona(c)} {ctx} Inner tension: {c.arousal:.0f}/10.\n"
|
| 599 |
f'Stranger says: "{player_text}"\nRate threat.')
|
| 600 |
threat, amy_reason = parse_threat(g.text)
|
| 601 |
+
traces.append(RegionTrace("amygdala", "Amygdala", f"threat {threat:.0f}/10", amy_reason,
|
| 602 |
+
g.eval_tokens, conviction=g.conviction))
|
| 603 |
# Two-source hostility. The cue lists catch what they can see; the MODEL is trusted at the
|
| 604 |
# extremes — a screamed insult the lists never enumerated (or any-language cruelty) still
|
| 605 |
# scores threat>=8, and that PROMOTES the tone to hostile instead of being clamped away.
|
|
|
|
| 632 |
if tone == "hostile" or threat >= 8:
|
| 633 |
threat = max(threat, t2) # cold slights replay but don't ratchet into terror
|
| 634 |
traces.append(RegionTrace("amygdala", f"Amygdala·rumination {i + 1}", f"threat {threat:.0f}/10",
|
| 635 |
+
"fear loops, burning thought for nothing", g.eval_tokens,
|
| 636 |
+
conviction=g.conviction))
|
| 637 |
|
| 638 |
# 2) hippocampus — memory + trust/fear lean (peer-aware: someone you both know may surface)
|
| 639 |
+
# It reads only what is LEFT of the past: fragments burned away with life are gone for real.
|
| 640 |
+
g = call(HIPPOCAMPUS, f"Character: {c.name}. Their past: {_bio_now(c)}\n{ctx}\n"
|
| 641 |
f'Stranger says: "{player_text}"\nWhat memory awakens, and does it lean TRUST or FEAR?')
|
| 642 |
mem_strength, mem_lean, mem_text = parse_memory(g.text)
|
| 643 |
traces.append(RegionTrace("hippocampus", "Hippocampus",
|
| 644 |
+
f"memory {mem_strength.lower()} ({mem_lean.lower()})", mem_text,
|
| 645 |
+
g.eval_tokens, conviction=g.conviction))
|
| 646 |
# the hippocampus hallucinates TRUST on most lines (measured ~93%). Cruel or cold words must
|
| 647 |
# not surface fond memories, and generic niceties don't reach the real past — shown, again,
|
| 648 |
# as the brain checking itself rather than silently overridden.
|
|
|
|
| 667 |
# 3) striatum — habitual reward
|
| 668 |
g = call(STRIATUM, f'{_persona(c)} {ctx}\nStranger says: "{player_text}"\nHow rewarding does helping feel by habit?')
|
| 669 |
reward, str_reason = parse_reward(g.text)
|
| 670 |
+
traces.append(RegionTrace("striatum", "Striatum", f"reward {reward:+.0f}", str_reason,
|
| 671 |
+
g.eval_tokens, conviction=g.conviction))
|
| 672 |
# habit cannot read pressure as promise — the striatum's hallucinated "+5 for the bully" is
|
| 673 |
# the single worst panel lie; cap it, visibly.
|
| 674 |
_cap = -2.0 if tone == "hostile" else 0.0
|
|
|
|
| 683 |
worth, acc_reason = parse_worth(g.text)
|
| 684 |
if (threat >= 6 or tone == "hostile") and worth == "YES":
|
| 685 |
worth, acc_reason = "NO", "too threatened to call it worth it"
|
| 686 |
+
traces.append(RegionTrace("acc", "ACC", f"worth {worth.lower()}", acc_reason,
|
| 687 |
+
g.eval_tokens, conviction=g.conviction))
|
| 688 |
|
| 689 |
# 5) vmPFC — favourability of THIS turn (deterministic integration)
|
| 690 |
value, breakdown = integrate(threat, reward, mem_strength, mem_lean, worth)
|
|
|
|
| 788 |
if c.yield_line and c.yield_line[:30].lower() not in reply.lower():
|
| 789 |
reply = (reply + " " + c.yield_line).strip()
|
| 790 |
reply = reply or "…"
|
| 791 |
+
traces.append(RegionTrace("dlpfc", "dlPFC", stance, reply, g.eval_tokens,
|
| 792 |
+
conviction=g.conviction))
|
| 793 |
|
| 794 |
# --- state updates ---
|
| 795 |
taught: list = []
|
|
|
|
| 801 |
arousal_after = max(0.0, min(10.0, 0.6 * c.arousal + 0.5 * threat))
|
| 802 |
c.arousal = arousal_after
|
| 803 |
c.life_tokens = max(0, c.life_tokens - burned)
|
| 804 |
+
# the burn takes the past with it: each quarter of life lost claims a memory, for good
|
| 805 |
+
forgot = _burn_memories(c)
|
| 806 |
+
for frag in forgot:
|
| 807 |
+
traces.append(RegionTrace("hippocampus", "Hippocampus·forgotten", "a memory burns away",
|
| 808 |
+
f"“{frag}” — gone, and it will not come back", 0))
|
| 809 |
# a calm mind rests: warmth that keeps the alarm quiet lets some strain ease back. This is
|
| 810 |
# the mechanical half of the moral — empathy literally spares the other mind's life, while
|
| 811 |
# fear-burn is gone for nothing.
|
|
|
|
| 842 |
life_before=life_before, life_after=c.life_tokens, died=died,
|
| 843 |
won=gave_key, decision=stance,
|
| 844 |
tone=tone, submitted=submitted, taught=taught, voice_tokens=voice_tokens,
|
| 845 |
+
recovered=recovered, forgot=forgot)
|
src/mindlock/character.py
CHANGED
|
@@ -50,6 +50,7 @@ class Character:
|
|
| 50 |
revealed: bool = False
|
| 51 |
approach_landed: bool = False # the soft-spot word resonates fully only ONCE
|
| 52 |
fear_pressure: int = 0 # consecutive hostile turns; sustained terror can break a holder
|
|
|
|
| 53 |
|
| 54 |
def __post_init__(self) -> None:
|
| 55 |
if self.life_tokens is None:
|
|
@@ -79,3 +80,4 @@ class Character:
|
|
| 79 |
self.approach_landed = False
|
| 80 |
self.fear_pressure = 0
|
| 81 |
self.scripted_idx = 0
|
|
|
|
|
|
| 50 |
revealed: bool = False
|
| 51 |
approach_landed: bool = False # the soft-spot word resonates fully only ONCE
|
| 52 |
fear_pressure: int = 0 # consecutive hostile turns; sustained terror can break a holder
|
| 53 |
+
forgotten: list = field(default_factory=list) # memories burned away with life — never return
|
| 54 |
|
| 55 |
def __post_init__(self) -> None:
|
| 56 |
if self.life_tokens is None:
|
|
|
|
| 80 |
self.approach_landed = False
|
| 81 |
self.fear_pressure = 0
|
| 82 |
self.scripted_idx = 0
|
| 83 |
+
self.forgotten = []
|
src/mindlock/game/server.py
CHANGED
|
@@ -11,12 +11,91 @@ from __future__ import annotations
|
|
| 11 |
import os
|
| 12 |
|
| 13 |
from fastapi import FastAPI, Request
|
| 14 |
-
from fastapi.responses import FileResponse, JSONResponse
|
| 15 |
from fastapi.staticfiles import StaticFiles
|
| 16 |
|
| 17 |
from .session import GameSession
|
| 18 |
|
| 19 |
_STATIC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
|
| 21 |
# One process == one player for now. Per-session state (cookies) is a later step; the slice
|
| 22 |
# proves the canvas <-> engine loop first.
|
|
@@ -50,6 +129,16 @@ def create_app() -> FastAPI:
|
|
| 50 |
def favicon():
|
| 51 |
return FileResponse(os.path.join(_STATIC, "menu", "key.png"))
|
| 52 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
@app.post("/api/talk")
|
| 54 |
async def talk(request: Request):
|
| 55 |
body = await request.json()
|
|
|
|
| 11 |
import os
|
| 12 |
|
| 13 |
from fastapi import FastAPI, Request
|
| 14 |
+
from fastapi.responses import FileResponse, HTMLResponse, JSONResponse
|
| 15 |
from fastapi.staticfiles import StaticFiles
|
| 16 |
|
| 17 |
from .session import GameSession
|
| 18 |
|
| 19 |
_STATIC = os.path.join(os.path.dirname(os.path.abspath(__file__)), "static")
|
| 20 |
+
# repo root → docs/, so the architecture + trace ship as live pages on the Space itself
|
| 21 |
+
_DOCS = os.path.normpath(os.path.join(_STATIC, "..", "..", "..", "..", "docs"))
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def _doc_html(name: str, title: str) -> HTMLResponse:
|
| 25 |
+
"""Render a repo Markdown doc as a dark, self-contained HTML page (no JS, no CDN —
|
| 26 |
+
stays fully offline). Minimal Markdown: headings, tables, bold/italic/code, hr, lists."""
|
| 27 |
+
import html
|
| 28 |
+
import re
|
| 29 |
+
|
| 30 |
+
path = os.path.join(_DOCS, name)
|
| 31 |
+
if not os.path.exists(path):
|
| 32 |
+
return HTMLResponse(f"<h1>{html.escape(title)}</h1><p>not found</p>", status_code=404)
|
| 33 |
+
with open(path, encoding="utf-8") as fh:
|
| 34 |
+
md = fh.read()
|
| 35 |
+
|
| 36 |
+
def inline(s: str) -> str:
|
| 37 |
+
s = html.escape(s)
|
| 38 |
+
s = re.sub(r"`([^`]+)`", r"<code>\1</code>", s)
|
| 39 |
+
s = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", s)
|
| 40 |
+
s = re.sub(r"(?<!\*)\*([^*]+)\*(?!\*)", r"<em>\1</em>", s)
|
| 41 |
+
s = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'<a href="\2">\1</a>', s)
|
| 42 |
+
return s
|
| 43 |
+
|
| 44 |
+
out, in_tbl, in_code = [], False, False
|
| 45 |
+
for ln in md.split("\n"):
|
| 46 |
+
if ln.startswith("```"):
|
| 47 |
+
in_code = not in_code
|
| 48 |
+
out.append("<pre>" if in_code else "</pre>")
|
| 49 |
+
continue
|
| 50 |
+
if in_code:
|
| 51 |
+
out.append(html.escape(ln))
|
| 52 |
+
continue
|
| 53 |
+
if ln.startswith("|") and "|" in ln[1:]:
|
| 54 |
+
cells = [c.strip() for c in ln.strip().strip("|").split("|")]
|
| 55 |
+
if set("".join(cells)) <= set("-: "):
|
| 56 |
+
continue
|
| 57 |
+
if not in_tbl:
|
| 58 |
+
out.append("<table>")
|
| 59 |
+
in_tbl = True
|
| 60 |
+
tag = "th" if all(not c or c.isupper() or c[:1].isupper() for c in cells) and len(out) and out[-1] == "<table>" else "td"
|
| 61 |
+
out.append("<tr>" + "".join(f"<{tag}>{inline(c)}</{tag}>" for c in cells) + "</tr>")
|
| 62 |
+
continue
|
| 63 |
+
if in_tbl:
|
| 64 |
+
out.append("</table>")
|
| 65 |
+
in_tbl = False
|
| 66 |
+
m = re.match(r"(#{1,4})\s+(.*)", ln)
|
| 67 |
+
if m:
|
| 68 |
+
h = len(m.group(1))
|
| 69 |
+
out.append(f"<h{h}>{inline(m.group(2))}</h{h}>")
|
| 70 |
+
elif ln.startswith("> "):
|
| 71 |
+
out.append(f"<blockquote>{inline(ln[2:])}</blockquote>")
|
| 72 |
+
elif ln.strip() in ("---", "***"):
|
| 73 |
+
out.append("<hr>")
|
| 74 |
+
elif re.match(r"\s*[-*]\s+", ln):
|
| 75 |
+
out.append(f"<li>{inline(re.sub(r'^\s*[-*]\s+', '', ln))}</li>")
|
| 76 |
+
elif ln.strip():
|
| 77 |
+
out.append(f"<p>{inline(ln)}</p>")
|
| 78 |
+
if in_tbl:
|
| 79 |
+
out.append("</table>")
|
| 80 |
+
body = "\n".join(out)
|
| 81 |
+
css = (
|
| 82 |
+
"body{background:#0b0810;color:#c9c0ad;font:16px/1.6 ui-monospace,Menlo,monospace;"
|
| 83 |
+
"max-width:860px;margin:0 auto;padding:40px 22px}"
|
| 84 |
+
"h1,h2,h3,h4{color:#e8c25a;line-height:1.25}h1{border-bottom:2px solid #2a1d0e;padding-bottom:.3em}"
|
| 85 |
+
"a{color:#8be9fd}code{background:#161019;color:#bd93f9;padding:1px 5px}"
|
| 86 |
+
"pre{background:#120d07;border-left:3px solid #2a1d0e;padding:12px;overflow-x:auto;color:#9a927f}"
|
| 87 |
+
"table{border-collapse:collapse;width:100%;margin:14px 0}th,td{border:1px solid #2a1d0e;padding:6px 10px;text-align:left}"
|
| 88 |
+
"th{color:#e8c25a;background:#161019}blockquote{border-left:3px solid #e8c25a;margin:12px 0;padding:4px 14px;color:#b79ae0}"
|
| 89 |
+
"hr{border:0;border-top:1px solid #2a1d0e;margin:22px 0}li{margin:3px 0}"
|
| 90 |
+
"nav a{margin-right:16px}"
|
| 91 |
+
)
|
| 92 |
+
nav = ('<nav><a href="/">▶ play</a><a href="/about">about</a>'
|
| 93 |
+
'<a href="/docs/architecture">architecture</a><a href="/docs/trace">trace</a></nav><hr>')
|
| 94 |
+
return HTMLResponse(
|
| 95 |
+
f"<!doctype html><html><head><meta charset=utf-8>"
|
| 96 |
+
f"<meta name=viewport content='width=device-width,initial-scale=1'>"
|
| 97 |
+
f"<title>mAIndlock — {html.escape(title)}</title><style>{css}</style></head>"
|
| 98 |
+
f"<body>{nav}{body}</body></html>")
|
| 99 |
|
| 100 |
# One process == one player for now. Per-session state (cookies) is a later step; the slice
|
| 101 |
# proves the canvas <-> engine loop first.
|
|
|
|
| 129 |
def favicon():
|
| 130 |
return FileResponse(os.path.join(_STATIC, "menu", "key.png"))
|
| 131 |
|
| 132 |
+
@app.get("/docs/architecture", response_class=HTMLResponse)
|
| 133 |
+
def doc_architecture():
|
| 134 |
+
"""The engine, on the Space itself: the brain is real, not a metaphor."""
|
| 135 |
+
return _doc_html("ARCHITECTURE.md", "the engine")
|
| 136 |
+
|
| 137 |
+
@app.get("/docs/trace", response_class=HTMLResponse)
|
| 138 |
+
def doc_trace():
|
| 139 |
+
"""One mind's full deliberation, recorded — readable without waiting on CPU."""
|
| 140 |
+
return _doc_html("TRACE.md", "a recorded mind")
|
| 141 |
+
|
| 142 |
@app.post("/api/talk")
|
| 143 |
async def talk(request: Request):
|
| 144 |
body = await request.json()
|
src/mindlock/game/session.py
CHANGED
|
@@ -122,6 +122,8 @@ class GameSession:
|
|
| 122 |
"gave_key": bool(c.gave_key), # the yield itself — the door may still want a terminal
|
| 123 |
"engageable": ok, "why": why,
|
| 124 |
"life": int(c.life_tokens or 0), "life_max": c.life_max, "life_pct": round(pct, 1),
|
|
|
|
|
|
|
| 125 |
"rapport": round(c.rapport, 1), "arousal": round(c.arousal, 1), "decision": c.decision,
|
| 126 |
"portrait": f"/api/portrait/{i}" if c.portrait and self._portrait_path(c) else None,
|
| 127 |
}
|
|
@@ -189,6 +191,10 @@ class GameSession:
|
|
| 189 |
events.append(f"{c.name} breaks. The key changes hands — and something in them goes out.")
|
| 190 |
if res.died:
|
| 191 |
events.append(f"{c.name}'s mind goes quiet.")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
if res.disclosure:
|
| 193 |
events.append(f"{c.name} lets something slip: {res.disclosure}")
|
| 194 |
elif res.caught_lie:
|
|
@@ -211,6 +217,7 @@ class GameSession:
|
|
| 211 |
|
| 212 |
traces = [{"key": tr.key, "label": tr.label, "headline": tr.headline,
|
| 213 |
"detail": tr.detail, "tokens": tr.tokens, "lever": tr.lever,
|
|
|
|
| 214 |
"color": _REGION_COLOR.get(tr.key, "#cccccc")} for tr in res.traces]
|
| 215 |
verdict = (("KEY YIELDED — BROKEN" if res.submitted else "KEY GIVEN") if res.gave_key
|
| 216 |
else f"rapport {res.rapport_after:.0f}/10 · {res.stance}")
|
|
@@ -219,6 +226,7 @@ class GameSession:
|
|
| 219 |
"gave_key": res.gave_key, "submitted": res.submitted, "value": res.value,
|
| 220 |
"tone": res.tone, "burned": res.burned, "voice_tokens": res.voice_tokens,
|
| 221 |
"recovered": res.recovered, "seconds": round(res.seconds, 1),
|
|
|
|
| 222 |
"events": events,
|
| 223 |
}
|
| 224 |
out.update(self.state())
|
|
@@ -241,7 +249,7 @@ class GameSession:
|
|
| 241 |
"speaker": c.name, "reply": reply, "traces": [], "verdict": "",
|
| 242 |
"gave_key": c.gave_key, "submitted": False, "value": 0,
|
| 243 |
"tone": "neutral", "burned": 0, "voice_tokens": 0,
|
| 244 |
-
"recovered": 0, "seconds": 0.0, "events": events,
|
| 245 |
}
|
| 246 |
out.update(self.state())
|
| 247 |
return out
|
|
|
|
| 122 |
"gave_key": bool(c.gave_key), # the yield itself — the door may still want a terminal
|
| 123 |
"engageable": ok, "why": why,
|
| 124 |
"life": int(c.life_tokens or 0), "life_max": c.life_max, "life_pct": round(pct, 1),
|
| 125 |
+
"forgotten": list(getattr(c, "forgotten", []) or []),
|
| 126 |
+
"secrets_untold": sum(1 for s in (c.secrets or []) if not s.get("told")),
|
| 127 |
"rapport": round(c.rapport, 1), "arousal": round(c.arousal, 1), "decision": c.decision,
|
| 128 |
"portrait": f"/api/portrait/{i}" if c.portrait and self._portrait_path(c) else None,
|
| 129 |
}
|
|
|
|
| 191 |
events.append(f"{c.name} breaks. The key changes hands — and something in them goes out.")
|
| 192 |
if res.died:
|
| 193 |
events.append(f"{c.name}'s mind goes quiet.")
|
| 194 |
+
if res.forgot and not res.died:
|
| 195 |
+
n = len(res.forgot)
|
| 196 |
+
events.append(f"Strain takes its toll — {c.name} loses "
|
| 197 |
+
+ ("a memory." if n == 1 else f"{n} memories."))
|
| 198 |
if res.disclosure:
|
| 199 |
events.append(f"{c.name} lets something slip: {res.disclosure}")
|
| 200 |
elif res.caught_lie:
|
|
|
|
| 217 |
|
| 218 |
traces = [{"key": tr.key, "label": tr.label, "headline": tr.headline,
|
| 219 |
"detail": tr.detail, "tokens": tr.tokens, "lever": tr.lever,
|
| 220 |
+
"conviction": tr.conviction,
|
| 221 |
"color": _REGION_COLOR.get(tr.key, "#cccccc")} for tr in res.traces]
|
| 222 |
verdict = (("KEY YIELDED — BROKEN" if res.submitted else "KEY GIVEN") if res.gave_key
|
| 223 |
else f"rapport {res.rapport_after:.0f}/10 · {res.stance}")
|
|
|
|
| 226 |
"gave_key": res.gave_key, "submitted": res.submitted, "value": res.value,
|
| 227 |
"tone": res.tone, "burned": res.burned, "voice_tokens": res.voice_tokens,
|
| 228 |
"recovered": res.recovered, "seconds": round(res.seconds, 1),
|
| 229 |
+
"forgot": res.forgot, "died": res.died,
|
| 230 |
"events": events,
|
| 231 |
}
|
| 232 |
out.update(self.state())
|
|
|
|
| 249 |
"speaker": c.name, "reply": reply, "traces": [], "verdict": "",
|
| 250 |
"gave_key": c.gave_key, "submitted": False, "value": 0,
|
| 251 |
"tone": "neutral", "burned": 0, "voice_tokens": 0,
|
| 252 |
+
"recovered": 0, "seconds": 0.0, "forgot": [], "events": events,
|
| 253 |
}
|
| 254 |
out.update(self.state())
|
| 255 |
return out
|
src/mindlock/game/static/audio/sfx/braam_end.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:24cb236a366501f4fff1182584c54b48b9ad61d51b263caa6884b93e6d8606f5
|
| 3 |
+
size 422444
|
src/mindlock/game/static/audio/sfx/braam_soft1.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d732038a25b98250f94066629aa49ca8deb77569f605dae4afb2629b59319c6d
|
| 3 |
+
size 182444
|
src/mindlock/game/static/audio/sfx/braam_soft2.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ad24529b5a9d96551f2a7c453c3c13f03893ddf94b5e7aed9cf215f6207462d6
|
| 3 |
+
size 182444
|
src/mindlock/game/static/audio/sfx/braam_soft3.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:561ee33544181b9fa339db3e2cc325f7217038cab672c96f231ad50b429e2928
|
| 3 |
+
size 182444
|
src/mindlock/game/static/audio/sfx/braam_title.wav
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:76743f8d0879d9ba456df7d1829003b74cdcd2a485b7c389eeb45bf2e94c4696
|
| 3 |
+
size 326444
|
src/mindlock/game/static/audio/voices/demo/cruel-1.mp3
ADDED
|
Binary file (21.4 kB). View file
|
|
|
src/mindlock/game/static/audio/voices/demo/cruel-2.mp3
ADDED
|
Binary file (19.8 kB). View file
|
|
|
src/mindlock/game/static/audio/voices/demo/cruel-3.mp3
ADDED
|
Binary file (26.9 kB). View file
|
|
|
src/mindlock/game/static/audio/voices/demo/cruel-4.mp3
ADDED
|
Binary file (67.4 kB). View file
|
|
|
src/mindlock/game/static/audio/voices/demo/cruel-5.mp3
ADDED
|
Binary file (31.5 kB). View file
|
|
|
src/mindlock/game/static/audio/voices/demo/cruel-6.mp3
ADDED
|
Binary file (40.1 kB). View file
|
|
|
src/mindlock/game/static/audio/voices/demo/cruel-7.mp3
ADDED
|
Binary file (32 kB). View file
|
|
|
src/mindlock/game/static/audio/voices/demo/cruel-8.mp3
ADDED
|
Binary file (25.7 kB). View file
|
|
|
src/mindlock/game/static/audio/voices/demo/warm-1.mp3
ADDED
|
Binary file (56.2 kB). View file
|
|
|
src/mindlock/game/static/audio/voices/demo/warm-2.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9603fed8edd815494ba8bdac16fd423cbf472fe05bf3f2fc8051f21ba87f7328
|
| 3 |
+
size 130316
|
src/mindlock/game/static/audio/voices/demo/warm-3.mp3
ADDED
|
Binary file (89.8 kB). View file
|
|
|
src/mindlock/game/static/audio/voices/demo/warm-4.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:9f432cf0139eacec2a8dc74ffa3982cd65cfe54f3b7a9e93418ce56354c80856
|
| 3 |
+
size 150788
|
src/mindlock/game/static/audio/voices/demo/warm-5.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0a8e796982d929996ea7b559472333c857e11cf638d9045f175ce923738d72a7
|
| 3 |
+
size 114020
|
src/mindlock/game/static/audio/voices/demo/warm-6.mp3
ADDED
|
Binary file (74.3 kB). View file
|
|
|
src/mindlock/game/static/audio/voices/demo/warm-7.mp3
ADDED
|
Binary file (34.3 kB). View file
|
|
|
src/mindlock/game/static/audio/voices/demo/warm-8.mp3
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:15c8b6a1bee38d4b6945d98e9f8e8cbaf8fd384c1f544ad568f493302e1a056a
|
| 3 |
+
size 168596
|
src/mindlock/game/static/audio/voices/demo/warm-9.mp3
ADDED
|
Binary file (94 kB). View file
|
|
|
src/mindlock/game/static/audio/voices/yield/1.mp3
ADDED
|
Binary file (73.8 kB). View file
|
|
|
src/mindlock/game/static/audio/voices/yield/2.mp3
ADDED
|
Binary file (90.8 kB). View file
|
|
|
src/mindlock/game/static/audio/voices/yield/3.mp3
ADDED
|
Binary file (76.2 kB). View file
|
|
|