Pabloler21 Claude Sonnet 4.6 commited on
Commit
03129db
·
1 Parent(s): 8fea751

feat(game): 'what it remembers' weaves the memories it stole from you

Browse files
Files changed (4) hide show
  1. app.py +12 -8
  2. render.py +13 -10
  3. styles.css +9 -0
  4. tests/test_render.py +10 -0
app.py CHANGED
@@ -195,6 +195,10 @@ def _voice_html(b64: str | None) -> str:
195
  return f'<audio autoplay src="data:audio/wav;base64,{b64}"></audio>'
196
 
197
 
 
 
 
 
198
  _SENTENCE_END = re.compile(r'[^.!?…\n]*[.!?…\n]+', re.S)
199
 
200
 
@@ -615,7 +619,7 @@ def _enter_game(mode: str, tone_seed: int = 0):
615
  # never a tier-driven face (the dev HOLLOW_FAST_FINALE seed starts at
616
  # Almost bond, which would otherwise greet you with the possessive grin)
617
  render_entity(20, tone=state["tone"]),
618
- render_recovered(state.get("fragments_told", 0)),
619
  _render_title(state),
620
  )
621
 
@@ -731,7 +735,7 @@ def _play_finale(state: dict, history: list):
731
  input_locked = gr.update(interactive=False)
732
  mutated = False
733
  convulsed = False
734
- recovered = render_recovered(state.get("fragments_told", 0))
735
 
736
  for step in finale_steps(claimed):
737
  role = "assistant" if step["speaker"] == "hollow" else "user"
@@ -787,7 +791,7 @@ def _play_finale_bad(state: dict, history: list):
787
  input_locked = gr.update(interactive=False)
788
  mutated = False
789
  frenzied = False
790
- recovered = render_recovered(state.get("fragments_told", 0))
791
 
792
  for step in finale_steps_bad(wounds):
793
  role = "assistant" if step["speaker"] == "hollow" else "user"
@@ -848,7 +852,7 @@ def _play_finale_good(state: dict, history: list):
848
  mutated = False
849
  convulsed = False
850
  settled = False
851
- recovered = render_recovered(state.get("fragments_told", 0))
852
 
853
  for step in finale_steps_good(OWN_FRAGMENTS):
854
  chat_hist = chat_hist + [{"role": "assistant", "content": step["text"]}]
@@ -920,7 +924,7 @@ def chat(user_msg: str, state: dict, history: list):
920
  render_entity(state["affinity"], entity_mode, seq=state["turn"],
921
  tone=state.get("tone", 0)),
922
  _voice_html(None),
923
- render_recovered(state.get("fragments_told", 0)),
924
  _render_title(state),
925
  )
926
  return
@@ -939,7 +943,7 @@ def chat(user_msg: str, state: dict, history: list):
939
  render_entity(state["affinity"], "idle", seq=state["turn"],
940
  tone=state.get("tone", 0)),
941
  _voice_html(None),
942
- render_recovered(state.get("fragments_told", 0)),
943
  _render_title(state),
944
  )
945
  return
@@ -1015,7 +1019,7 @@ def chat(user_msg: str, state: dict, history: list):
1015
  render_entity(state["affinity"], "idle", seq=state["turn"],
1016
  tone=state.get("tone", 0)),
1017
  voice,
1018
- render_recovered(state.get("fragments_told", 0)),
1019
  _render_title(state),
1020
  )
1021
  except Exception as e:
@@ -1072,7 +1076,7 @@ def chat(user_msg: str, state: dict, history: list):
1072
  render_entity(state["affinity"], mode, seq=state["turn"],
1073
  tone=state.get("tone", 0), cue=cue),
1074
  voice,
1075
- render_recovered(state.get("fragments_told", 0)),
1076
  _render_title(state),
1077
  )
1078
 
 
195
  return f'<audio autoplay src="data:audio/wav;base64,{b64}"></audio>'
196
 
197
 
198
+ def _recovered(state: dict) -> str:
199
+ return render_recovered(state.get("fragments_told", 0), state.get("claimed"))
200
+
201
+
202
  _SENTENCE_END = re.compile(r'[^.!?…\n]*[.!?…\n]+', re.S)
203
 
204
 
 
619
  # never a tier-driven face (the dev HOLLOW_FAST_FINALE seed starts at
620
  # Almost bond, which would otherwise greet you with the possessive grin)
621
  render_entity(20, tone=state["tone"]),
622
+ _recovered(state),
623
  _render_title(state),
624
  )
625
 
 
735
  input_locked = gr.update(interactive=False)
736
  mutated = False
737
  convulsed = False
738
+ recovered = _recovered(state)
739
 
740
  for step in finale_steps(claimed):
741
  role = "assistant" if step["speaker"] == "hollow" else "user"
 
791
  input_locked = gr.update(interactive=False)
792
  mutated = False
793
  frenzied = False
794
+ recovered = _recovered(state)
795
 
796
  for step in finale_steps_bad(wounds):
797
  role = "assistant" if step["speaker"] == "hollow" else "user"
 
852
  mutated = False
853
  convulsed = False
854
  settled = False
855
+ recovered = _recovered(state)
856
 
857
  for step in finale_steps_good(OWN_FRAGMENTS):
858
  chat_hist = chat_hist + [{"role": "assistant", "content": step["text"]}]
 
924
  render_entity(state["affinity"], entity_mode, seq=state["turn"],
925
  tone=state.get("tone", 0)),
926
  _voice_html(None),
927
+ _recovered(state),
928
  _render_title(state),
929
  )
930
  return
 
943
  render_entity(state["affinity"], "idle", seq=state["turn"],
944
  tone=state.get("tone", 0)),
945
  _voice_html(None),
946
+ _recovered(state),
947
  _render_title(state),
948
  )
949
  return
 
1019
  render_entity(state["affinity"], "idle", seq=state["turn"],
1020
  tone=state.get("tone", 0)),
1021
  voice,
1022
+ _recovered(state),
1023
  _render_title(state),
1024
  )
1025
  except Exception as e:
 
1076
  render_entity(state["affinity"], mode, seq=state["turn"],
1077
  tone=state.get("tone", 0), cue=cue),
1078
  voice,
1079
+ _recovered(state),
1080
  _render_title(state),
1081
  )
1082
 
render.py CHANGED
@@ -332,19 +332,22 @@ def _wound_entries(wounds: list[str], revealed: int) -> list[str]:
332
  return parts
333
 
334
 
335
- def render_recovered(fragments_told: int) -> str:
336
- """The child's own recovered memories the right-hand drawer."""
 
337
  n = max(0, min(int(fragments_told), len(OWN_FRAGMENTS)))
338
  head = f'<div class="drawer-head" style="{_HEADER}">✦ What it remembers</div>'
339
- if n == 0:
340
- body = f'<div style="{_EMPTY}">it remembers nothing of itself.</div>'
341
- else:
342
- rows = "".join(
343
- f'<div class="recovered-item" style="{_ITEM}{_CLAIMED}font-style:italic;">'
344
- f'{html.escape(OWN_FRAGMENTS[i], quote=True)}</div>'
345
- for i in range(n)
 
 
346
  )
347
- body = rows
348
  return f'<div class="recovered-panel" style="{_PANEL}">{head}{body}</div>'
349
 
350
 
 
332
  return parts
333
 
334
 
335
+ def render_recovered(fragments_told: int, claimed: list[str] | None = None) -> str:
336
+ """The child's own recovered memories (its real past) + the memories it has
337
+ claimed from the visitor, now worn as its own — the right-hand drawer."""
338
  n = max(0, min(int(fragments_told), len(OWN_FRAGMENTS)))
339
  head = f'<div class="drawer-head" style="{_HEADER}">✦ What it remembers</div>'
340
+ rows = [
341
+ f'<div class="recovered-item" style="{_ITEM}{_CLAIMED}font-style:italic;">'
342
+ f'{html.escape(OWN_FRAGMENTS[i], quote=True)}</div>'
343
+ for i in range(n)
344
+ ]
345
+ for mem in (claimed or []):
346
+ rows.append(
347
+ f'<div class="recovered-item stolen" style="{_ITEM}{_CLAIMED}font-style:italic;">'
348
+ f'{html.escape(mem, quote=True)}</div>'
349
  )
350
+ body = "".join(rows) if rows else f'<div style="{_EMPTY}">it remembers nothing of itself.</div>'
351
  return f'<div class="recovered-panel" style="{_PANEL}">{head}{body}</div>'
352
 
353
 
styles.css CHANGED
@@ -1564,6 +1564,15 @@ footer { display: none !important; }
1564
  border-left-color: rgba(200,130,70,0.5) !important;
1565
  }
1566
 
 
 
 
 
 
 
 
 
 
1567
  /* the child silhouette zone — center, bottom-weighted */
1568
  #game-entity {
1569
  position: fixed;
 
1564
  border-left-color: rgba(200,130,70,0.5) !important;
1565
  }
1566
 
1567
+ /* a memory the child has taken from you, worn as its own */
1568
+ .game-drawer .recovered-item.stolen {
1569
+ border-left-style: dashed !important;
1570
+ opacity: 0.92;
1571
+ }
1572
+ .game-drawer .recovered-item.stolen::before {
1573
+ content: '"'; color: var(--mem-accent); margin-right: 2px;
1574
+ }
1575
+
1576
  /* the child silhouette zone — center, bottom-weighted */
1577
  #game-entity {
1578
  position: fixed;
tests/test_render.py CHANGED
@@ -1,3 +1,4 @@
 
1
  from render import render_entity, render_treasure, render_recovered
2
 
3
 
@@ -35,6 +36,15 @@ class TestRenderRecovered:
35
  out = render_recovered(99)
36
  assert out.count('class="recovered-item') == len(OWN_FRAGMENTS)
37
 
 
 
 
 
 
 
 
 
 
38
 
39
  class TestRenderTreasure:
40
  def test_empty_shows_placeholder(self):
 
1
+ import html as _html_mod
2
  from render import render_entity, render_treasure, render_recovered
3
 
4
 
 
36
  out = render_recovered(99)
37
  assert out.count('class="recovered-item') == len(OWN_FRAGMENTS)
38
 
39
+ def test_recovered_weaves_stolen(self):
40
+ from character import OWN_FRAGMENTS
41
+ html = render_recovered(1, claimed=["a red bicycle, the summer i turned seven"])
42
+ assert _html_mod.escape(OWN_FRAGMENTS[0], quote=True) in html # its real past
43
+ assert "a red bicycle" in html # the stolen memory
44
+ assert "stolen" in html # marked
45
+ # back-compat: no claimed arg still works
46
+ assert "remembers nothing of itself" in render_recovered(0).lower()
47
+
48
 
49
  class TestRenderTreasure:
50
  def test_empty_shows_placeholder(self):