Spaces:
Running on Zero
Running on Zero
| """The altar: the Warden offers power for your most-used command. | |
| If you walked in carrying contraband (a card smuggled out of a won run), | |
| the Warden offers to take THAT instead — it knows. Refusal has a price | |
| too: the Warden deletes something personal from the sandbox. (Duplicates. | |
| Always duplicates. The player doesn't know that.) | |
| Dismisses with {"card": offered_id | None, "paid": "command" | "contraband" | None}. | |
| """ | |
| from __future__ import annotations | |
| import random | |
| 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 Static | |
| from scrypt.engine.cards import Card, CardInstance | |
| from scrypt.sandbox.shell import Shell | |
| from scrypt.sandbox.vfs import DirNode | |
| from scrypt.ui import fx as fxmod | |
| from scrypt.ui import palette as pal | |
| from scrypt.ui.render import card_info, card_panel | |
| from scrypt.warden import moments | |
| from scrypt.warden.presence import REACTION | |
| EPITAPHS = { | |
| "grep": "no more needles. enjoy the haystacks.", | |
| "ls": "you will walk your directories blind.", | |
| "cat": "the files keep their secrets now.", | |
| "find": "lost things stay lost.", | |
| "tree": "the forest, missed for the trees.", | |
| "cd": "you live here now.", | |
| } | |
| class AltarScreen(Screen): | |
| CSS = """ | |
| #altar-dialogue { height: 4; padding: 1 2 0 2; text-style: italic; } | |
| #altar-offer { height: 12; content-align: center middle; } | |
| #altar-info { height: 3; content-align: center top; } | |
| #altar-prompt { height: 1; dock: bottom; background: $panel; content-align: center middle; } | |
| """ | |
| def __init__( | |
| self, | |
| shell: Shell, | |
| offered: Card, | |
| rng: random.Random, | |
| contraband: Card | None = None, | |
| presence=None, | |
| ): | |
| super().__init__() | |
| self.shell = shell | |
| self.offered = offered | |
| self.rng = rng | |
| self.contraband = contraband | |
| self.presence = presence | |
| self.demand = shell.most_used() or "echo" | |
| self.resolved = False | |
| def compose(self) -> ComposeResult: | |
| with Vertical(): | |
| yield Static(id="altar-dialogue") | |
| yield Static(id="altar-offer") | |
| yield Static(id="altar-info") | |
| yield Static(id="altar-prompt") | |
| def on_mount(self) -> None: | |
| opener = ( | |
| f'"You lean on `{self.demand}`. I have counted. {self.shell.usage[self.demand]} times.\n' | |
| f' Give it to me — for good — and take this."' | |
| ) | |
| keys = f"[a] surrender `{self.demand}` [r] refuse" | |
| if self.contraband is not None: | |
| opener = ( | |
| f'"You brought {self.contraband.name} in here. Stolen property. I want it back —\n' | |
| f' or, if you are attached, `{self.demand}` will do. I have counted your uses."' | |
| ) | |
| keys = f"[t] hand over {self.contraband.name} [a] surrender `{self.demand}` [r] refuse" | |
| self.query_one("#altar-dialogue", Static).update(Text(opener, style="italic")) | |
| self.query_one("#altar-offer", Static).update( | |
| card_panel(CardInstance(spec=self.offered), show_cost=True, selected=True, art=True) | |
| ) | |
| self.query_one("#altar-info", Static).update(card_info(CardInstance(spec=self.offered))) | |
| self.query_one("#altar-prompt", Static).update(Text(keys)) | |
| if self.presence is not None: | |
| self.presence.attach(self._on_warden_line) | |
| # The player is deciding: cook every ending while they hesitate. | |
| self.presence.prefetch( | |
| "altar_accept", moments.altar_accept(self.demand, self.offered.name) | |
| ) | |
| self.presence.prefetch("altar_refuse", moments.altar_refuse(self.demand)) | |
| if self.contraband is not None: | |
| self.presence.prefetch( | |
| "altar_contraband", | |
| moments.altar_contraband(self.contraband.name, self.demand), | |
| ) | |
| def on_unmount(self) -> None: | |
| if self.presence is not None: | |
| self.presence.detach(self._on_warden_line) | |
| def _on_warden_line(self, line: str, priority: int) -> None: | |
| self.query_one("#altar-dialogue", Static).update(Text(f'"{line}"', style="italic")) | |
| def _speak(self, moment: str, key: str) -> None: | |
| if self.presence is not None: | |
| # fallback="" — the scripted farewell is already on screen. | |
| self.presence.submit(moment, key=key, priority=REACTION) | |
| def _personal_target(self) -> str | None: | |
| """Pick a personal directory worth grieving over.""" | |
| home = self.shell.vfs.resolve("/home/drifter") | |
| if not isinstance(home, DirNode): | |
| return None | |
| dirs = [ | |
| name for name, node in home.children.items() | |
| if isinstance(node, DirNode) and not name.startswith(".") and node.children | |
| ] | |
| return self.rng.choice(sorted(dirs)) if dirs else None | |
| def on_key(self, event: events.Key) -> None: | |
| if self.resolved: | |
| self.dismiss(self._outcome) | |
| return | |
| if event.key == "a": | |
| self.shell.revoke(self.demand, EPITAPHS.get(self.demand, "")) | |
| self._outcome = {"card": self.offered.id, "paid": "command"} | |
| self._farewell( | |
| f'"Done. `{self.demand}` is mine now. Try not to think about ' | |
| f'how often your fingers will reach for it."', | |
| detail=f"`{self.demand}` revoked for the rest of the run", | |
| ) | |
| self._speak(moments.altar_accept(self.demand, self.offered.name), "altar_accept") | |
| elif event.key == "t" and self.contraband is not None: | |
| self._outcome = {"card": self.offered.id, "paid": "contraband"} | |
| self._farewell( | |
| f'"Recovered. {self.contraband.name} goes back in evidence. ' | |
| f'Keep your `{self.demand}` — and take the offer. We are square."', | |
| detail=f"{self.contraband.name} removed from your deck", | |
| ) | |
| self._speak( | |
| moments.altar_contraband(self.contraband.name, self.demand), | |
| "altar_contraband", | |
| ) | |
| elif event.key == "r": | |
| self._outcome = {"card": None, "paid": None} | |
| target = self._personal_target() | |
| if target: | |
| node = self.shell.vfs.resolve(f"/home/drifter/{target}") | |
| doomed = [ | |
| path for path, _ in self.shell.vfs.iter_files(node, prefix=f"~/{target}") | |
| ] | |
| count = self.shell.vfs.remove(f"/home/drifter/{target}", recursive=True) | |
| self._farewell( | |
| '"Refusal has a price. Refuse me again sometime."', | |
| detail=f"rm -rf ~/{target} — {count} of your files, unlinked", | |
| ) | |
| if not fxmod.reduced_motion(): | |
| self.run_worker(self._unlink_scroll(doomed)) | |
| else: | |
| self._farewell('"Refusal noted. There is so little left of yours to take."') | |
| self._speak(moments.altar_refuse(self.demand), "altar_refuse") | |
| async def _unlink_scroll(self, paths: list[str]) -> None: | |
| """Watch them go. Each name appears already struck through.""" | |
| import asyncio | |
| offer = self.query_one("#altar-offer", Static) | |
| shown: list[str] = [] | |
| for path in paths[:8]: | |
| shown.append(path) | |
| t = Text(justify="center") | |
| for s in shown: | |
| t.append(s + "\n", style=f"{pal.DANGER} strike") | |
| if len(paths) > 8 and len(shown) == 8: | |
| t.append(f"…and {len(paths) - 8} more", style=f"{pal.DANGER} dim") | |
| offer.update(t) | |
| await asyncio.sleep(0.13) | |
| def _farewell(self, line: str, detail: str = "") -> None: | |
| """The scripted goodbye, with the deal's mechanical terms kept | |
| deterministic and visible even if a live line replaces the dialogue.""" | |
| self.resolved = True | |
| self.query_one("#altar-dialogue", Static).update(Text(line, style="italic")) | |
| self.query_one("#altar-offer", Static).update(Text("")) | |
| self.query_one("#altar-info", Static).update(Text(detail, style=pal.MUTED, justify="center")) | |
| self.query_one("#altar-prompt", Static).update(Text("[any key] step away")) | |