"""Orientation: shown once per installation, the first time a game begins. Five pages: who the Warden is, the way out, the table, the economy of death, and the world between fights. Skippable, never repeated (the `initiated` flag in legacy.json), and everything it teaches stays reachable afterwards — [?] in a fight, [h] on the menu. """ from __future__ import annotations from rich import box from rich.panel import Panel 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 TABLE_DIAGRAM = """\ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │queued│ │ │ │queued│ │ │ arriving next turn └──────┘ └──────┘ └──────┘ └──────┘ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │ foe │ │ │ │ foe │ │ │ the Warden's row └──────┘ └──────┘ └──────┘ └──────┘ ───── you ██ ·· ·· ·· ·· ⚖ ·· ·· ·· ·· ·· foe ───── ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ │yours │ │ │ │yours │ │ │ your row └──────┘ └──────┘ └──────┘ └──────┘ [1] [2] [3] [4] """ PAGES: list[tuple[str, str]] = [ ( "WHERE YOU ARE", """\ You are a process. A small one, freshly spawned, inside a machine that belongs to something called the WARDEN. The Warden is not a script wearing a villain mask. It is a language model running on this computer. It watches what you type, remembers you between runs, rewrites encounters while you explore, and keeps files on everyone who has died here. It has noticed you. That is rarely good news.""", ), ( "THE WAY OUT", """\ Objective: escape the machine. A run is a short gauntlet — the path reads left to right: ⚔ fight $ shell † altar + draft Win every fight and the last door opens: you walk out with root. You have 2 ttys. Lose a fight and one burns — you retry the same fight. Lose both and you are reaped: the run ends, the Warden files a report on your corpse, and your next run inherits the wreckage.""", ), ( "THE TABLE", TABLE_DIAGRAM + """ Your processes hit the lane across from them — or the Warden's face when that lane is empty. Face damage tips THE SCALE. Tip it +5 your way and you win the fight; let it reach −5 and you are reaped. [b] rings the bell to end your turn. Then everything attacks.""", ), ( "THE ECONOMY OF DEATH", """\ Each turn you draw: [d] from your deck, or [s] a bit. A bit is a free 0/1 process whose entire purpose is to die. ♦ cards are paid in blood — mark processes you already control and they are killed to summon it. ⊙ cards cost core dumps: you bank one each time a process of yours dies. Death is your economy. Spend it. Your two piles stand at the right of the table. When a pile says EMPTY, there is no more drawing from it. Watch them. Select any card and its exact rules appear under your hand. [tab] does the same for the Warden's cards. No mystery mechanics.""", ), ( "BETWEEN FIGHTS", """\ After a fight you get a shell. It is small, it is fake, and it is the most honest part of this machine. `ls`, `cat`, `cd`, `grep` your way around — [tab] completes, [↑] recalls — some files pay cycles, some hide cards, and one is a schedule you really should do something about. The altar sells power. The price is always one of YOUR commands — whichever you lean on most. Sold means gone for the rest of the run, and the Warden notices every time you reach for it anyway. Hints will follow you through your first fight. [?] reopens the rules any time. The Warden is listening — `say` something if you must. It will not be kind about it.""", ), ] class OrientationScreen(Screen): """Dismisses with True when the player has seen (or skipped) it all.""" CSS = """ #orient { height: 1fr; align: center middle; } #orient-page { width: 76; height: auto; } #orient-prompt { height: 1; dock: bottom; background: $panel; content-align: center middle; } """ def __init__(self) -> None: super().__init__() self.page = 0 def compose(self) -> ComposeResult: with Vertical(id="orient"): yield Static(id="orient-page") yield Static(id="orient-prompt") def on_mount(self) -> None: self._show() def _dots(self) -> Text: from scrypt.ui import palette as pal t = Text() for i in range(len(PAGES)): t.append("● " if i <= self.page else "○ ", style=pal.WARDEN if i == self.page else pal.GHOST) return t def _show(self) -> None: from scrypt.ui import palette as pal title, body = PAGES[self.page] self.query_one("#orient-page", Static).update( Panel( Text(body, style=pal.FG), box=box.HEAVY, border_style=pal.BORDER_BRIGHT, title=Text(f"⟪ orientation — {title} ⟫", style=f"bold {pal.WARDEN}"), subtitle=self._dots(), subtitle_align="center", padding=(1, 3), ) ) last = self.page == len(PAGES) - 1 prompt = ( "[enter] the Warden is waiting" if last else "[enter] next [backspace] back [s] skip orientation" ) self.query_one("#orient-prompt", Static).update(Text(prompt)) def on_key(self, event: events.Key) -> None: if event.key == "s": self.dismiss(True) elif event.key == "backspace" and self.page > 0: self.page -= 1 self._show() elif event.key in ("enter", "space", "right"): if self.page == len(PAGES) - 1: self.dismiss(True) else: self.page += 1 self._show()