File size: 6,988 Bytes
ecca2a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
175
176
177
178
179
180
181
182
183
184
185
186
187
188
"""Bidirectional A+B=C Solver — forward read + backward zone targeting.

Forward:  text -> VADUGWI (wrapper around compute_vadug)
Backward: given A's state + target zone, solve for what B needs to be

The core idea: emotional states are composable. If someone is in state A
and you say B, the resulting state C = weighted_blend(A, B).

A persists at 60% (emotional inertia — you don't forget how you feel).
B has 40% influence (what was just said shifts the state, doesn't replace it).

The solver can also work backwards: given where A is and where you want C
to land (a target zone), sweep B's valence to find valid ranges.
"""

from typing import List, Optional, Tuple

from .shared import VADUG
from .pendulum import compute_vadug
from .zones import ZONES


# ── Forward ────────────────────────────────────────────────────────

def forward(text: str) -> VADUG:
    """Compute VADUGWI for a text string. Wrapper around compute_vadug."""
    result, _ = compute_vadug(text)
    return result


# ── State transition ───────────────────────────────────────────────

def state_transition(
    a_vadug: VADUG,
    b_vadug: VADUG,
    a_weight: float = 0.6,
) -> VADUG:
    """Compute resulting state C from A (receiver state) + B (the force/message).

    Base: C = A * a_weight + B * (1 - a_weight), clamped to 0-255.

    FORCE DIRECTION adjustments:
    - B with CONTROL intent (I>200) + negative V = attack on receiver.
      Receiver's D drops (dominated), W drops (worth under attack).
      The force doesn't blend W/D toward B's values -- it PUSHES them down.
    - B with CONNECT intent (I>155) + positive V = support/healing.
      Receiver's W gets lifted, D stabilized.
    - B with WITHDRAW intent (I<40) = the sender is pulling away.
      Receiver's G increases (heavier), connection fading.
    """
    b_weight = 1.0 - a_weight
    CENTER = 128.0

    # Base blend
    c_v = a_vadug.v * a_weight + b_vadug.v * b_weight
    c_a = a_vadug.a * a_weight + b_vadug.a * b_weight
    c_d = a_vadug.d * a_weight + b_vadug.d * b_weight
    c_u = a_vadug.u * a_weight + b_vadug.u * b_weight
    c_g = a_vadug.g * a_weight + b_vadug.g * b_weight
    c_w = a_vadug.w * a_weight + b_vadug.w * b_weight
    c_i = a_vadug.i * a_weight + b_vadug.i * b_weight

    # Force direction adjustments on the RECEIVER
    b_v = b_vadug.v
    b_i = b_vadug.i

    # Attack: negative V directed outward OR pure CONTROL command
    if b_v < 125 and b_i > 60:
        # Negative force aimed outward = attack on receiver
        attack_strength = (128 - b_v) / 128.0  # 0 to 1
        control_boost = max(0, (b_i - 128)) / 127.0  # 0 to 1
        total = attack_strength * (1.0 + control_boost)
        c_d -= total * 15
        c_w -= total * 15
        c_v -= total * 5

    # Pure CONTROL command (high I) -- even if V is only mildly negative
    # "shut up" has high D force but the RECEIVER loses D (being commanded)
    if b_i > 200:
        control_strength = (b_i - 200) / 55.0  # 0 to 1
        c_d -= control_strength * 12  # being controlled drops YOUR D
        # Override the D blend -- receiver doesn't gain power from being commanded
        if b_vadug.d > CENTER:
            # B has high D (commander's power) but that shouldn't transfer to receiver
            d_excess = (c_d - a_vadug.d * a_weight) * 0.3  # dampen the D transfer
            c_d = a_vadug.d * a_weight + d_excess

    # Healing: CONNECT intent + positive V
    elif b_v > 135 and b_i > 155:
        heal_strength = (b_v - 128) / 127.0
        connect_boost = (b_i - 128) / 127.0
        total = heal_strength * (1.0 + connect_boost * 0.5)
        c_w += total * 12  # W lifted (feeling valued)
        c_d += total * 8   # D stabilized (someone has their back)

    # Withdraw: sender pulling away from receiver
    elif b_i < 40:
        withdraw_strength = (40 - b_i) / 40.0
        c_g -= withdraw_strength * 10  # heavier (weight of abandonment)
        c_d -= withdraw_strength * 8   # loss of relational support
        c_w -= withdraw_strength * 5   # worth hit from being left

    # Deflect/dismiss: mild negative, disengaged
    elif b_i > 60 and b_i < 120 and b_v < 128:
        dismiss_strength = (128 - b_v) / 128.0
        c_w -= dismiss_strength * 8  # mild worth hit from being dismissed

    return VADUG(
        v=int(round(max(0, min(255, c_v)))),
        a=int(round(max(0, min(255, c_a)))),
        d=int(round(max(0, min(255, c_d)))),
        u=int(round(max(0, min(255, c_u)))),
        g=int(round(max(0, min(255, c_g)))),
        w=int(round(max(0, min(255, c_w)))),
        i=int(round(max(0, min(255, c_i)))),
    )


# ── Backward: zone targeting ──────────────────────────────────────

def _in_zone(vadug: VADUG, zone_name: str) -> bool:
    """Check if VADUGWI state falls within a zone's radius on V, D, G."""
    zone = ZONES[zone_name]
    c = zone["center"]
    r = zone["radius"]
    return (
        abs(vadug.v - c["v"]) <= r["v"]
        and abs(vadug.d - c["d"]) <= r["d"]
        and abs(vadug.g - c["g"]) <= r["g"]
    )


def solve_for_b_range(
    a_vadug: VADUG,
    target_zone: str,
    temperature_steps: int = 100,
) -> List[Tuple[int, int]]:
    """Sweep B's valence (0-255), return ranges where C lands in target zone.

    For each candidate B valence, construct a synthetic B with neutral A/D/U/G
    and compute C = state_transition(A, B). If C falls in the target zone,
    include that V value.

    Returns list of (start, end) inclusive ranges of valid B valence values.
    """
    valid = []
    # Use finer steps for better resolution, but always cover 0-255
    step = max(1, 256 // temperature_steps)

    for bv in range(0, 256, step):
        # Synthetic B: only V varies, rest neutral
        b = VADUG(v=bv, a=128, d=128, u=0, g=128, w=128, i=128)
        c = state_transition(a_vadug, b)
        if _in_zone(c, target_zone):
            valid.append(bv)

    # Collapse to contiguous ranges
    if not valid:
        return []

    ranges = []
    start = valid[0]
    prev = valid[0]
    for bv in valid[1:]:
        if bv - prev > step:
            ranges.append((start, prev))
            start = bv
        prev = bv
    ranges.append((start, prev))
    return ranges


def optimal_b_temperature(
    a_vadug: VADUG,
    target_zone: str,
) -> Optional[int]:
    """Find the optimal B valence to reach the target zone from A.

    Returns the midpoint of the widest valid range, or None if unreachable.
    """
    ranges = solve_for_b_range(a_vadug, target_zone, temperature_steps=256)
    if not ranges:
        return None

    # Find widest range
    widest = max(ranges, key=lambda r: r[1] - r[0])
    return (widest[0] + widest[1]) // 2