Scrypt / scrypt /ui /interview.py
IMJONEZZ's picture
SCRYPT: initial commit — game, sandbox, Warden, Space web layer
9fca766
Raw
History Blame Contribute Delete
3.74 kB
"""The exit interview: the run is dead, and the Warden keeps records.
No card is forged here. Your statement goes into the crash dump, into the
Warden's persistent memory, and — eventually — gets quoted back to you at
the worst possible moment. Free text reaches the LLM only wrapped inert.
Dismisses with the cleaned statement string.
"""
from __future__ import annotations
from rich.text import Text
from textual import events
from textual.app import ComposeResult
from textual.containers import Vertical
from textual.screen import Screen
from textual.widgets import Input, Static
from scrypt.engine.legacy import clean_statement
from scrypt.warden.guardrails import wrap_player_text
OPENER = (
"Dead. Properly dead — no more ttys. Before the core dump is sealed, "
"procedure allows you one statement for the record. Make it good. "
"I keep these."
)
FILED_LINES = [
"Noted, stamped, archived. The record outlives the process. It always does.",
"Filed under recurring disappointments. You will be quoted at length.",
]
class ExitInterviewScreen(Screen):
CSS = """
#exit-dialogue { height: 5; padding: 1 2 0 2; }
#exit-input { margin: 0 8; }
#exit-prompt { height: 1; dock: bottom; background: $panel; content-align: center middle; }
"""
def __init__(self, voice=None):
super().__init__()
self.voice = voice
self.statement: str | None = None
self._speech_target = ""
self._speech_shown = 0
def compose(self) -> ComposeResult:
with Vertical():
yield Static(id="exit-dialogue")
yield Input(
placeholder="statement for the record",
max_length=120,
id="exit-input",
)
yield Static(id="exit-prompt")
def on_mount(self) -> None:
self.say(OPENER)
self.set_interval(0.025, self._typewriter_tick)
self.query_one("#exit-input", Input).focus()
self.query_one("#exit-prompt", Static).update(Text("[enter] go on the record"))
def say(self, line: str) -> None:
self._speech_target = line
self._speech_shown = 0
def _typewriter_tick(self) -> None:
if self._speech_shown < len(self._speech_target):
self._speech_shown += 2
shown = self._speech_target[: self._speech_shown]
self.query_one("#exit-dialogue", Static).update(
Text(f'"{shown}"', style="italic")
)
def on_input_submitted(self, event: Input.Submitted) -> None:
if self.statement is not None:
return
self.statement = clean_statement(event.value)
inp = self.query_one("#exit-input", Input)
inp.disabled = True
inp.display = False
self.query_one("#exit-prompt", Static).update(Text("[any key] be sealed"))
if self.voice is not None:
self.run_worker(self._react_live(self.statement))
else:
self.say(FILED_LINES[len(self.statement) % len(FILED_LINES)])
async def _react_live(self, statement: str) -> None:
moment = (
"the player is dead for good; their final run just ended. their "
f"exit-interview statement for the record:\n{wrap_player_text(statement)}\n"
"acknowledge it for the file"
)
got_any = False
async for chunk in self.voice.react(moment, taboo=statement):
got_any = True
self.say(chunk)
if not got_any:
self.say(FILED_LINES[len(statement) % len(FILED_LINES)])
def on_key(self, event: events.Key) -> None:
if self.statement is not None and event.key not in ("enter",):
self.dismiss(self.statement)