feat(sim): emotions are a right-brain output (Right knob alone)

The affect channel previously gated on BOTH knobs (strength = min(left, right)),
so the analytical Left brain co-controlled the emotion words. Per operator
direction, emotions are now driven by the Right (dreamlike) knob ALONE — Left no
longer gates them. This sharpens the split: Left = measurement (detections +
measurements), Right = the dream AND the feeling it evokes.

- player/alteration.py: `_affect_strength(right) = right` (was min(left,right));
  `_affect_intensity` keyed off Right. AffectOverlay + is_identity docs updated
  (is_identity still holds: right>0 ⇒ dream.strength>0).
- simulator/static/app.js: renderAffect comments (gating is Right via `strength`);
  the client logic already keyed min_level/tier off the passed strength + right.
- Affect design spec: revision note + every min(left,right) → right.
- Tests rewritten: affect is Right-driven (full Right + Left 0 surfaces affect;
  Right 0 + Left 4 surfaces none).

271 passed, 2 skipped. Live API verified: left=0/right=4 → strength 4; left=4/
right=0 → strength 0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BenStullsBets
2026-06-26 04:29:21 -07:00
parent 5bb762ae53
commit c3aa419c53
4 changed files with 71 additions and 56 deletions
+23 -21
View File
@@ -55,42 +55,44 @@ def test_left_and_right_stack_not_cancel():
assert plan.dream.intensity == 1.0
def test_affect_needs_both_knobs_up():
# affect-channel design (session 0013): emotion-words surface only when BOTH
# the Left and Right knobs are up — either at 0 means no affect at all.
def test_affect_is_driven_by_the_right_knob_alone():
# Right-brain affect (session 0018): emotion-words are controlled by the Right
# knob alone — Right at 0 means no affect regardless of Left, and Left no longer
# gates the channel (full Right with Left at 0 still surfaces affect).
assert plan_alteration(_coord(left=4, right=0)).affect.strength == 0
assert plan_alteration(_coord(left=0, right=4)).affect.strength == 0
assert plan_alteration(_coord(left=4, right=0)).affect.intensity == 0.0
assert plan_alteration(_coord(left=0, right=4)).affect.intensity == 0.0
assert plan_alteration(_coord(left=0, right=4)).affect.strength == 4
assert plan_alteration(_coord(left=0, right=4)).affect.intensity == 1.0
def test_affect_strength_is_the_smaller_knob():
# strength = min(left, right)the smaller knob gates the channel.
assert plan_alteration(_coord(left=2, right=4)).affect.strength == 2
def test_affect_strength_is_the_right_knob():
# strength = right — Left is irrelevant to the affect channel.
assert plan_alteration(_coord(left=2, right=4)).affect.strength == 4
assert plan_alteration(_coord(left=4, right=2)).affect.strength == 2
assert plan_alteration(_coord(left=4, right=4)).affect.strength == 4
assert plan_alteration(_coord(left=3, right=1)).affect.strength == 1
assert plan_alteration(_coord(left=0, right=3)).affect.strength == 3
assert plan_alteration(_coord(left=4, right=1)).affect.strength == 1
def test_affect_intensity_scales_with_strength_over_full_scale():
for lo in range(5):
plan = plan_alteration(_coord(left=lo, right=4))
assert plan.affect.intensity == pytest.approx(lo / 4)
assert plan_alteration(_coord(left=4, right=4)).affect.intensity == 1.0
def test_affect_intensity_scales_with_right_over_full_scale():
for r in range(5):
plan = plan_alteration(_coord(left=0, right=r))
assert plan.affect.intensity == pytest.approx(r / 4)
assert plan_alteration(_coord(left=0, right=4)).affect.intensity == 1.0
def test_affect_intensity_honors_overlay_gain():
# affect rides the same overlay_gain as the analytical HUD it belongs to.
# affect text rides the overlay-opacity gain, but is keyed off the Right knob.
cal = Calibration(overlay_gain=0.5)
assert plan_alteration(_coord(left=4, right=4), cal).affect.intensity == pytest.approx(0.5)
assert plan_alteration(_coord(left=0, right=4), cal).affect.intensity == pytest.approx(0.5)
clamp = Calibration(overlay_gain=10.0)
assert plan_alteration(_coord(left=4, right=4), clamp).affect.intensity == 1.0
assert plan_alteration(_coord(left=0, right=4), clamp).affect.intensity == 1.0
def test_affect_active_implies_non_identity():
# min(left,right) > 0 implies left > 0, so an active affect plan is never identity.
plan = plan_alteration(_coord(left=2, right=3))
assert plan.affect.strength == 2
# affect.strength == right, and right > 0 makes dream.strength > 0, so an active
# affect plan (even with Left at 0) is never identity.
plan = plan_alteration(_coord(left=0, right=3))
assert plan.affect.strength == 3
assert not plan.is_identity