File size: 6,445 Bytes
9fca766
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
"""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()