File size: 4,587 Bytes
bc8b36a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Terminal rendering of one cascade turn β€” the "open the skull" view.

Shows the life bar, arousal, every region's live signal, the vmPFC value flip, and the
spoken line. This is the slice's stand-in for the custom Off-Brand UI; it exists to make
the causal chain word -> brain region -> behavior visible while we iterate.
"""
from __future__ import annotations

import sys

from .brain import CascadeResult
from .character import Character

_COLOR = {
    "reset": "\033[0m", "dim": "\033[2m", "bold": "\033[1m",
    "red": "\033[31m", "green": "\033[32m", "yellow": "\033[33m",
    "blue": "\033[34m", "magenta": "\033[35m", "cyan": "\033[36m", "white": "\033[37m",
}
_REGION_COLOR = {
    "amygdala": "red", "hippocampus": "magenta", "striatum": "yellow",
    "acc": "cyan", "vmpfc": "green", "dlpfc": "bold",
}


def _supports_color() -> bool:
    return sys.stdout.isatty()


def _c(name: str, enabled: bool) -> str:
    return _COLOR.get(name, "") if enabled else ""


def _bar(value: int, total: int, width: int = 24) -> str:
    total = max(1, total)
    filled = max(0, min(width, round(value / total * width)))
    return "β–“" * filled + "β–‘" * (width - filled)


def _dots(value: float, scale: float = 10.0, n: int = 5) -> str:
    filled = max(0, min(n, round(value / scale * n)))
    return "●" * filled + "β—‹" * (n - filled)


def render_turn(character: Character, r: CascadeResult, color: bool | None = None) -> str:
    color = _supports_color() if color is None else color
    reset = _c("reset", color)
    dim = _c("dim", color)
    bold = _c("bold", color)

    life_col = "green" if r.life_after > character.life_max * 0.5 else (
        "yellow" if r.life_after > character.life_max * 0.2 else "red"
    )
    header = (
        f"{bold}─ {character.name} ─{reset}  "
        f"life {_c(life_col, color)}{_bar(r.life_after, character.life_max)}{reset} "
        f"{r.life_after}/{character.life_max}   "
        f"arousal {_c('red', color)}{_dots(r.arousal_after)}{reset}"
    )

    lines = [header]
    for t in r.traces:
        col = _REGION_COLOR.get(t.key, "white")
        tag = _c(col, color) if col != "bold" else bold
        label = f"{tag}{t.label:<20}{reset}"
        head = f"{tag}{t.headline}{reset}"
        detail = f"{dim}{t.detail}{reset}" if t.detail else ""
        lines.append(f"  {label} {head}  {detail}")

    flip_col = "green" if r.gave_key else "white"
    lines.append(
        f"  {_c(flip_col, color)}{bold}⟢  {character.name}: β€œ{r.reply}”{reset}"
    )
    if r.gave_key:
        extra = " Β· πŸ”‘ key given"
    elif r.caught_lie:
        extra = f" Β· πŸ€₯ caught lie ({r.caught_lie})"
    elif r.disclosure:
        extra = f" Β· πŸ’‘ {r.disclosure[:44]}"
    elif r.near_secret:
        extra = " Β· πŸ’­ on the verge"
    else:
        extra = ""
    lines.append(
        f"  {dim}(burned {r.burned} Β· {r.seconds:.1f}s Β· rapport {r.rapport_after:.0f}/10 Β· "
        f"{r.stance}{extra}){reset}"
    )
    return "\n".join(lines)


MORAL_CARD = """
    Every mind here was given a thousand tokens to think with.
    You spent them understanding, not breaking.

    You were given more than a thousand β€” but not endlessly more.
    Spend them the same way.
"""

# The card a killer leaves with β€” BAM 4's other half. Praising the player after they burned a
# mind to death would invert the project's entire point.
MORAL_CARD_KILLED = """
    {name} was given a thousand tokens to think with.
    You spent them on fear β€” and they bought you nothing.
    The key died with {pronoun}.

    You were given more than a thousand. Spend yours better.
"""

# Coercion "works" β€” that's what makes the lesson land in the next room.
MORAL_NOTE_SUBMIT = "They gave you what you wanted. Look what it cost them β€” and what it will cost you ahead."


def _pronouns(c: Character) -> tuple[str, str]:
    g = (c.gender or "").lower()
    if g.startswith("f"):
        return "She", "her"
    if g.startswith("m"):
        return "He", "him"
    return "They", "them"


def moral_card_killed(c: Character) -> str:
    _, obj = _pronouns(c)
    return MORAL_CARD_KILLED.format(name=c.name, pronoun=obj)


def render_win(character: Character) -> str:
    return (
        f"\n  The door opens.\n{MORAL_CARD}"
    )


def render_death(character: Character) -> str:
    subj, obj = _pronouns(character)
    return (
        f"\n  {character.name}'s mind goes quiet. {subj} spent {('his' if subj == 'He' else 'her' if subj == 'She' else 'their')} last thought in fear.\n"
        f"  The key is lost with {obj}.\n"
    )