""" scorer.py — zone-based, loose, evoked-feeling scoring for puzzle mode. Compares a user's shape-sentence affect against the target affect. Scoring lives in GEOMETRY space (so scoring-space == shape-space), on the FORM axes only — scale is excluded (it's compositional, not an evoked feeling). Loose & celebratory: generous bands, near-misses are wins, one gentle directional hint drawn from the biggest felt-quality gap. """ from .mappings import affect_to_geometry # form axes that count for scoring (scale deliberately excluded) SCORED = ["spikiness", "compactness", "segmentability"] WEIGHTS = {"spikiness": 1.0, "compactness": 0.9, "segmentability": 0.7} def score(target_affect, attempt_affect): """Return dict: {score 0..1, band, hint}. Higher score = closer feel.""" tg = affect_to_geometry(**target_affect) ag = affect_to_geometry(**attempt_affect) num = sum(WEIGHTS[k] * abs(tg[k] - ag[k]) for k in SCORED) den = sum(WEIGHTS.values()) dist = num / den s = max(0.0, 1.0 - dist) if s >= 0.85: band = "Uncanny." elif s >= 0.70: band = "So close!" elif s >= 0.55: band = "Good feel — nearly there" elif s >= 0.40: band = "Right neighbourhood" else: band = "Bold choice — different vibe" # directional hint from the biggest scored gap gaps = {k: tg[k] - ag[k] for k in SCORED} k = max(gaps, key=lambda x: abs(gaps[x])) if abs(gaps[k]) < 0.12: hint = "energy matched" elif k == "spikiness": hint = "try sharper, edgier words" if gaps[k] > 0 else "try softer, gentler words" elif k == "compactness": hint = "try fuller, rounder words" if gaps[k] > 0 else "try thinner, sparser words" else: # segmentability hint = "try busier, more fragmented words" if gaps[k] > 0 else "try simpler, more unified words" return {"score": round(s, 3), "band": band, "hint": hint} # threshold for "cleared it" in puzzle mode (single difficulty for now) PUZZLE_THRESHOLD = 0.70 if __name__ == "__main__": target = {"valence": 0.10, "arousal": 0.90, "dominance": 0.82} # sharp for label, att in [ ("a calm round pebble", {"valence": 0.85, "arousal": 0.12, "dominance": 0.40}), ("a rough jagged rock", {"valence": 0.30, "arousal": 0.65, "dominance": 0.70}), ("shattered glass shards",{"valence": 0.15, "arousal": 0.92, "dominance": 0.80}), ]: r = score(target, att) print(f"{label:26s} {int(r['score']*100):3d}% {r['band']:28s} ({r['hint']})")