Pabloler21 Claude Fable 5 commited on
Commit
1a04c35
·
1 Parent(s): 74d156a

tune: ending gate 70/claimed-2, redemption needs tone>=20 (verified)

Browse files

pacing_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>

Files changed (4) hide show
  1. CLAUDE.md +5 -4
  2. memory.py +4 -2
  3. tests/test_memory.py +14 -12
  4. 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≥80 ∧ claimed≥2 ∧ tone≥15`. 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≥80 ∧ claimed≥2 ∧ tone<15`. Fed but never loved → stays a predator, consumes you, ends with YOU speaking its opening line.
75
- - Tone bands in prompt: warm ≥15 / wounded −22..−1 / hostile ≤−22 (injects wounds to echo back). Mimicry always on.
76
- - **Measured pacing** (via pacing_sim): bad ~msg 6, good/loop ~msg 12, seeds msg 2.
 
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"] >= 80 and len(state["claimed"]) >= 2:
88
- return "good" if tone >= 15 else "loop"
 
 
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 test_good_exactly_at_tone_15(self):
193
- assert decide_ending(_end_state(tone=15)) == "good"
 
 
 
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 test_none_below_80(self):
226
- assert decide_ending(_end_state(affinity=79, claimed_count=5)) is None
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 TestNoLowerGateRace:
316
- def test_nothing_fires_below_80_regardless_of_tone(self):
317
- # the old 65 neutral gate stole warm runs whose tone lagged — gone
318
  assert decide_ending(_end_state(affinity=65, tone=5)) is None
319
- assert decide_ending(_end_state(affinity=79, tone=14)) is None
320
- assert decide_ending(_end_state(affinity=70, tone=30)) is None
321
 
322
- def test_gate_at_80_branches_only_by_tone(self):
323
- assert decide_ending(_end_state(affinity=80, tone=15)) == "good"
324
- assert decide_ending(_end_state(affinity=80, tone=14)) == "loop"
 
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 always smelled like honey.",
40
- "my father died two years ago. it is what it is.",
41
  "i broke my arm falling from a fig tree when i was seven.",
42
- "i'm afraid of being forgotten. like everyone, i suppose.",
43
- "i used to sing to my little sister when she had nightmares. long time ago.",
44
- "my first kiss was under a streetlight in the rain. it didn't last.",
45
- "i lost most of the people i cared about. that's just how it goes.",
46
- "i should go soon.",
 
 
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