Files
ai-hell/server/escalation.py
T

122 lines
5.0 KiB
Python

"""Escalation engine — intensity curve, phase params, stochastic timing."""
import math
import random
import time
from server.config import config
class EscalationEngine:
"""Computes intensity and rendering parameters from elapsed time.
Intensity follows a logarithmic curve: intensity = log(1 + elapsed * rate).
Rendering parameters are derived from intensity and clamped to [0, 1].
Timing intervals are randomized within ranges that shrink with intensity.
"""
PALETTES = [
"void_black", "crimson_void", "deep_rot", "sickly_green",
"bruise_purple", "ash_grey", "bile_yellow", "blood_orange",
]
def __init__(self, rate: float | None = None):
self.rate = rate if rate is not None else config.escalation.rate
self.session_start: float | None = None
def start_session(self) -> None:
"""Begin a new escalation session."""
self.session_start = time.monotonic()
def reset(self) -> None:
"""Restart the session from zero."""
self.session_start = time.monotonic()
def get_elapsed(self) -> float:
"""Seconds since session start."""
if self.session_start is None:
return 0.0
return time.monotonic() - self.session_start
def get_intensity(self, elapsed: float | None = None) -> float:
"""Compute intensity from elapsed seconds. Logarithmic, never peaks."""
if elapsed is None:
elapsed = self.get_elapsed()
return math.log(1 + elapsed * self.rate)
def get_phase_params(self, intensity: float | None = None) -> dict:
"""Derive rendering parameters from current intensity.
All values clamped to [0, 1]. Higher intensity = more severe effects.
"""
if intensity is None:
intensity = self.get_intensity()
def _sigmoid(x: float, midpoint: float = 2.0, steepness: float = 1.5) -> float:
"""Smooth 0→1 mapping centered at midpoint."""
return 1.0 / (1.0 + math.exp(-steepness * (x - midpoint)))
morph_speed = _sigmoid(intensity, midpoint=1.5, steepness=1.2)
shader_severity = _sigmoid(intensity, midpoint=2.0, steepness=1.0)
voice_frequency = _sigmoid(intensity, midpoint=2.5, steepness=0.8)
noise_level = _sigmoid(intensity, midpoint=3.0, steepness=1.0)
surprise_chance = min(1.0, _sigmoid(intensity, midpoint=3.5, steepness=0.6))
# Palette shifts to more aggressive colors at higher intensity
palette_index = min(int(intensity), len(self.PALETTES) - 1)
return {
"morph_speed": round(morph_speed, 3),
"shader_severity": round(shader_severity, 3),
"voice_frequency": round(voice_frequency, 3),
"noise_level": round(noise_level, 3),
"surprise_chance": round(surprise_chance, 3),
"palette": self.PALETTES[palette_index],
}
def get_asset_swap_interval(self, intensity: float | None = None) -> float:
"""Random interval until next asset swap. Shrinks with intensity."""
if intensity is None:
intensity = self.get_intensity()
cfg = config.escalation
# Lerp from max to min as intensity increases, with noise
t = min(1.0, intensity / 5.0)
base = cfg.asset_swap_max - t * (cfg.asset_swap_max - cfg.asset_swap_min)
jitter = random.uniform(-base * 0.3, base * 0.3)
return max(cfg.asset_swap_min, base + jitter)
def get_voice_interval(self, intensity: float | None = None) -> float:
"""Poisson-distributed interval until next voice clip. Mean decreases with intensity."""
if intensity is None:
intensity = self.get_intensity()
cfg = config.escalation
# Mean interval decreases from voice_mean_interval toward 3s
mean = max(3.0, cfg.voice_mean_interval / (1 + intensity))
return random.expovariate(1.0 / mean)
def should_fake_calm(self) -> bool:
"""Roll for a fake calm period."""
return random.random() < config.escalation.fake_calm_chance
def get_fake_calm_duration(self) -> float:
"""Duration of a fake calm period."""
cfg = config.escalation
return random.uniform(cfg.fake_calm_duration_min, cfg.fake_calm_duration_max)
def should_cluster_burst(self) -> bool:
"""Roll for a cluster burst (rapid-fire events)."""
return random.random() < config.escalation.cluster_burst_chance
def get_cluster_burst_count(self) -> int:
"""Number of events in a cluster burst."""
cfg = config.escalation
return random.randint(cfg.cluster_burst_count_min, cfg.cluster_burst_count_max)
def select_severity(self, intensity: float | None = None) -> float:
"""Pick a target severity for the next asset. Biased toward current intensity."""
if intensity is None:
intensity = self.get_intensity()
# Normal distribution centered on intensity, clipped to [0, intensity+1]
severity = random.gauss(intensity, 0.5)
return max(0.0, min(severity, intensity + 1.0))