Spaces:
Running on Zero
Running on Zero
File size: 2,516 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 | """Terminal effects: the small kit every screen's animation draws from.
Honest about the medium β everything here is characters and timing.
SCRYPT_REDUCED_MOTION=1 turns all of it off (accessibility, and the test
suite runs with it set so timing never leaks into assertions).
"""
from __future__ import annotations
import os
import random
from dataclasses import dataclass, field
NOISE_CHARS = "βββββ "
def reduced_motion() -> bool:
return os.environ.get("SCRYPT_REDUCED_MOTION", "") not in ("", "0")
def noise_line(width: int, rng: random.Random | None = None) -> str:
rng = rng or random
return "".join(rng.choice(NOISE_CHARS) for _ in range(width))
def corrupt(text: str, fraction: float = 0.12, rng: random.Random | None = None) -> str:
"""A glitched copy: a few characters replaced with static."""
rng = rng or random
chars = list(text)
for i, ch in enumerate(chars):
if ch.strip() and rng.random() < fraction:
chars[i] = rng.choice("βββββ")
return "".join(chars)
@dataclass
class BoardFX:
"""One frame of combat theater, consumed by render.board().
flash (side, lane) cells whose border burns bright this frame
(side: "foe" | "player" | "queue")
dissolve (side, lane) -> noise intensity 0..2 for dying cards
floats lane -> (text, style) shown in the float row between rows
scale overrides the displayed scale (progressive tipping)
"""
flash: set[tuple[str, int]] = field(default_factory=set)
dissolve: dict[tuple[str, int], int] = field(default_factory=dict)
floats: dict[int, tuple[str, str]] = field(default_factory=dict)
scale: int | None = None
@property
def empty(self) -> bool:
return not (self.flash or self.dissolve or self.floats) and self.scale is None
# Typewriter pacing: ticks to hold after revealing certain characters.
PUNCT_PAUSE = {".": 6, "β": 5, "β¦": 6, "?": 5, "!": 5, ",": 3, ";": 3, ":": 3}
# The eye. It watches. Pupil position follows what the player has selected.
EYE_FRAMES = ["(β )", "( β )", "( β )", "( β )", "( β)"]
EYE_BLINK = "( ββββ )"
EYE_WIDE = "((=β=))"
def eye(selected: int, total: int, *, blink: bool = False, wide: bool = False) -> str:
if wide:
return EYE_WIDE
if blink:
return EYE_BLINK
if total <= 1:
return EYE_FRAMES[2]
pos = round(selected / max(1, total - 1) * (len(EYE_FRAMES) - 1))
return EYE_FRAMES[pos]
|