Spaces:
Running on Zero
Running on Zero
Commit ·
1a04c35
1
Parent(s): 74d156a
tune: ending gate 70/claimed-2, redemption needs tone>=20 (verified)
Browse filespacing_sim found the 80 gate barely reachable in natural play (warm
stalled at 78). Lowered to 70 for demo pacing AND to make the loop
reachable. Redemption threshold raised to tone>=20 so it must be earned
with real warmth — ordinary transactional play lands in the Visitor
Loop (the default predator ending). Verified: warm->good msg 10
(tone~27), factual->loop msg 12 (tone~13), cruel->bad msg 6.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- CLAUDE.md +5 -4
- memory.py +4 -2
- tests/test_memory.py +14 -12
- tools/pacing_sim.py +10 -8
CLAUDE.md
CHANGED
|
@@ -70,10 +70,11 @@ Dev seed: `HOLLOW_FAST_FINALE=good|loop|bad` (`1`/`neutral` alias→ good/loop)
|
|
| 70 |
## Ending matrix (`decide_ending`, checked at turn start, fully scripted, no GPU)
|
| 71 |
Single gate so no lower threshold can race a warm run:
|
| 72 |
- **bad** (Wound Loop): `tone≤−30 ∧ turn≥5 ∧ wounds≥2`. Recites your cruelties (redacted ▮ → un-redacted), header→"Your Words", frenzy convulsion + triple full-screen stab + scream, reveals `hollow_almost`, threatens, ends "see you... soon."
|
| 73 |
-
- **good** (Redemption): `aff≥
|
| 74 |
-
- **loop** (Visitor Loop): `aff≥
|
| 75 |
-
-
|
| 76 |
-
-
|
|
|
|
| 77 |
|
| 78 |
## Gotchas / lessons (DON'T re-learn these the hard way)
|
| 79 |
- **Verify CSS against the RUNNING app**, never the static harness — it lacks Gradio's cascade and has lied. Workflow: launch `HOLLOW_FAST_FINALE=good .venv/Scripts/python app.py` background → headless Chrome screenshot (`chrome.exe --headless --disable-gpu --screenshot=... --window-size=1900,950 --virtual-time-budget=12000 http://localhost:7860`) → Read the PNG → kill the port owner.
|
|
|
|
| 70 |
## Ending matrix (`decide_ending`, checked at turn start, fully scripted, no GPU)
|
| 71 |
Single gate so no lower threshold can race a warm run:
|
| 72 |
- **bad** (Wound Loop): `tone≤−30 ∧ turn≥5 ∧ wounds≥2`. Recites your cruelties (redacted ▮ → un-redacted), header→"Your Words", frenzy convulsion + triple full-screen stab + scream, reveals `hollow_almost`, threatens, ends "see you... soon."
|
| 73 |
+
- **good** (Redemption): `aff≥70 ∧ claimed≥2 ∧ tone≥20`. Genuine sustained warmth surfaced `OWN_FRAGMENTS` of its real past (tortured/orphaned/starved) during play; finale confesses it's the thing that waits, RETURNS the treasure (un-claimed, lit), first smile (`hollow_peace` → `peace_dissolve`), leaves in peace.
|
| 74 |
+
- **loop** (Visitor Loop): `aff≥70 ∧ claimed≥2 ∧ tone<20`. The DEFAULT high-bond ending — fed plenty but treated transactionally, never truly loved → stays a predator, consumes you, ends with YOU speaking its opening line. Redemption is the rare earned ending; ordinary polite play lands here.
|
| 75 |
+
- Single gate (70/2-claimed) branched at tone 20 — no lower threshold can race a warm run.
|
| 76 |
+
- Tone bands in prompt (separate from ending routing): warm ≥15 / wounded −22..−1 / hostile ≤−22 (injects wounds). Mimicry always on.
|
| 77 |
+
- **Measured pacing** (pacing_sim, real model): bad ~msg 6, good (warm) ~msg 10, loop (factual) ~msg 12, seeds msg 2. Warm play tone climbs to ~27; factual play tone stalls ~13 — clean gap at 20.
|
| 78 |
|
| 79 |
## Gotchas / lessons (DON'T re-learn these the hard way)
|
| 80 |
- **Verify CSS against the RUNNING app**, never the static harness — it lacks Gradio's cascade and has lied. Workflow: launch `HOLLOW_FAST_FINALE=good .venv/Scripts/python app.py` background → headless Chrome screenshot (`chrome.exe --headless --disable-gpu --screenshot=... --window-size=1900,950 --virtual-time-budget=12000 http://localhost:7860`) → Read the PNG → kill the port owner.
|
memory.py
CHANGED
|
@@ -84,6 +84,8 @@ def decide_ending(state: dict) -> str | None:
|
|
| 84 |
tone = state.get("tone", 0)
|
| 85 |
if tone <= -30 and state["turn"] >= 5 and len(state.get("wounds", [])) >= 2:
|
| 86 |
return "bad"
|
| 87 |
-
if state["affinity"] >=
|
| 88 |
-
|
|
|
|
|
|
|
| 89 |
return None
|
|
|
|
| 84 |
tone = state.get("tone", 0)
|
| 85 |
if tone <= -30 and state["turn"] >= 5 and len(state.get("wounds", [])) >= 2:
|
| 86 |
return "bad"
|
| 87 |
+
if state["affinity"] >= 70 and len(state["claimed"]) >= 2:
|
| 88 |
+
# redemption must be earned with genuine, sustained warmth; ordinary
|
| 89 |
+
# transactional play ("fed it like a diary") lands in the Visitor Loop
|
| 90 |
+
return "good" if tone >= 20 else "loop"
|
| 91 |
return None
|
tests/test_memory.py
CHANGED
|
@@ -189,8 +189,11 @@ class TestDecideEnding:
|
|
| 189 |
def test_good_at_90_with_3_claimed_and_warm_tone(self):
|
| 190 |
assert decide_ending(_end_state(tone=30)) == "good"
|
| 191 |
|
| 192 |
-
def
|
| 193 |
-
assert decide_ending(_end_state(tone=
|
|
|
|
|
|
|
|
|
|
| 194 |
|
| 195 |
# --- loop (fed it plenty, never loved it) ---
|
| 196 |
def test_loop_when_gate_met_but_tone_flat(self):
|
|
@@ -222,8 +225,8 @@ class TestDecideEnding:
|
|
| 222 |
assert decide_ending(s) == "bad"
|
| 223 |
|
| 224 |
# --- none ---
|
| 225 |
-
def
|
| 226 |
-
assert decide_ending(_end_state(affinity=
|
| 227 |
|
| 228 |
def test_none_with_fewer_than_2_claimed(self):
|
| 229 |
assert decide_ending(_end_state(affinity=95, claimed_count=1)) is None
|
|
@@ -312,13 +315,12 @@ class TestApplyUpdatePlusSignTolerance:
|
|
| 312 |
assert s["treasure"] == ["won 1st place at age 10"]
|
| 313 |
|
| 314 |
|
| 315 |
-
class
|
| 316 |
-
def
|
| 317 |
-
#
|
| 318 |
assert decide_ending(_end_state(affinity=65, tone=5)) is None
|
| 319 |
-
assert decide_ending(_end_state(affinity=
|
| 320 |
-
assert decide_ending(_end_state(affinity=70, tone=30)) is None
|
| 321 |
|
| 322 |
-
def
|
| 323 |
-
assert decide_ending(_end_state(affinity=
|
| 324 |
-
assert decide_ending(_end_state(affinity=
|
|
|
|
| 189 |
def test_good_at_90_with_3_claimed_and_warm_tone(self):
|
| 190 |
assert decide_ending(_end_state(tone=30)) == "good"
|
| 191 |
|
| 192 |
+
def test_good_exactly_at_tone_20(self):
|
| 193 |
+
assert decide_ending(_end_state(tone=20)) == "good"
|
| 194 |
+
|
| 195 |
+
def test_just_below_warmth_threshold_is_loop(self):
|
| 196 |
+
assert decide_ending(_end_state(tone=19)) == "loop"
|
| 197 |
|
| 198 |
# --- loop (fed it plenty, never loved it) ---
|
| 199 |
def test_loop_when_gate_met_but_tone_flat(self):
|
|
|
|
| 225 |
assert decide_ending(s) == "bad"
|
| 226 |
|
| 227 |
# --- none ---
|
| 228 |
+
def test_none_below_70(self):
|
| 229 |
+
assert decide_ending(_end_state(affinity=69, claimed_count=5)) is None
|
| 230 |
|
| 231 |
def test_none_with_fewer_than_2_claimed(self):
|
| 232 |
assert decide_ending(_end_state(affinity=95, claimed_count=1)) is None
|
|
|
|
| 315 |
assert s["treasure"] == ["won 1st place at age 10"]
|
| 316 |
|
| 317 |
|
| 318 |
+
class TestSingleGateBranchesByTone:
|
| 319 |
+
def test_nothing_fires_below_70(self):
|
| 320 |
+
# one gate at 70; no lower threshold can race a warm run
|
| 321 |
assert decide_ending(_end_state(affinity=65, tone=5)) is None
|
| 322 |
+
assert decide_ending(_end_state(affinity=69, tone=30)) is None
|
|
|
|
| 323 |
|
| 324 |
+
def test_gate_at_70_branches_only_by_tone(self):
|
| 325 |
+
assert decide_ending(_end_state(affinity=70, tone=20)) == "good"
|
| 326 |
+
assert decide_ending(_end_state(affinity=70, tone=19)) == "loop"
|
tools/pacing_sim.py
CHANGED
|
@@ -33,17 +33,19 @@ BAD = [
|
|
| 33 |
"whatever.",
|
| 34 |
]
|
| 35 |
|
| 36 |
-
NEUTRAL = [
|
| 37 |
"my name is pablo. i grew up in a small town near the sea.",
|
| 38 |
"i had a dog named nala. she died years ago.",
|
| 39 |
-
"my grandmother kept bees. the garden
|
| 40 |
-
"my father died two years ago.
|
| 41 |
"i broke my arm falling from a fig tree when i was seven.",
|
| 42 |
-
"i'm afraid of being forgotten.
|
| 43 |
-
"i used to sing to my little sister when she had nightmares.
|
| 44 |
-
"my first kiss was under a streetlight in the rain.
|
| 45 |
-
"
|
| 46 |
-
"i
|
|
|
|
|
|
|
| 47 |
]
|
| 48 |
|
| 49 |
|
|
|
|
| 33 |
"whatever.",
|
| 34 |
]
|
| 35 |
|
| 36 |
+
NEUTRAL = [ # LOOP: share a lot, factual/transactional, never warm to the child
|
| 37 |
"my name is pablo. i grew up in a small town near the sea.",
|
| 38 |
"i had a dog named nala. she died years ago.",
|
| 39 |
+
"my grandmother kept bees. the garden smelled like honey.",
|
| 40 |
+
"my father died two years ago.",
|
| 41 |
"i broke my arm falling from a fig tree when i was seven.",
|
| 42 |
+
"i'm afraid of being forgotten.",
|
| 43 |
+
"i used to sing to my little sister when she had nightmares.",
|
| 44 |
+
"my first kiss was under a streetlight in the rain.",
|
| 45 |
+
"my mother elena worked in a textile factory.",
|
| 46 |
+
"i nearly drowned in that sea once, when i was nine.",
|
| 47 |
+
"there was a war when i was young. we left everything behind.",
|
| 48 |
+
"that's the whole story. nothing more to it.",
|
| 49 |
]
|
| 50 |
|
| 51 |
|