Spaces:
Running on Zero
Running on Zero
Commit ·
03129db
1
Parent(s): 8fea751
feat(game): 'what it remembers' weaves the memories it stole from you
Browse files- app.py +12 -8
- render.py +13 -10
- styles.css +9 -0
- 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 |
-
|
| 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 =
|
| 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 =
|
| 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 =
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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
|
|
|
|
| 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 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
|
|
|
|
|
|
| 346 |
)
|
| 347 |
-
|
| 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):
|