| from __future__ import annotations | |
| from collections import Counter | |
| from copy import deepcopy | |
| from typing import Any | |
| from .schemas import BehaviorSummary | |
| SCALING_LABELS = { | |
| "str_scaling_dial": "strength", | |
| "agi_scaling_dial": "agility", | |
| "int_scaling_dial": "intelligence", | |
| "def_scaling_dial": "endurance", | |
| } | |
| def summarize_behavior( | |
| state: dict[str, Any], | |
| *, | |
| start_index: int | None = None, | |
| ) -> BehaviorSummary: | |
| checkpoint = int(state.get("behavior_checkpoint_index", 0)) | |
| start = checkpoint if start_index is None else max(0, int(start_index)) | |
| history = [str(entry) for entry in state.get("action_history", [])[start:]] | |
| actions = Counter() | |
| skill_use = Counter() | |
| element_use = Counter() | |
| scaling_use = Counter() | |
| effect_use = Counter() | |
| equipment = Counter() | |
| intent_responses = Counter() | |
| for entry in history: | |
| if entry == "used_strike": | |
| actions["strike"] += 1 | |
| elif entry == "used_defend": | |
| actions["defend"] += 1 | |
| elif entry == "used_item": | |
| actions["item"] += 1 | |
| elif entry.startswith("used_skill:"): | |
| skill_use[entry.split(":", 1)[1]] += 1 | |
| actions["skill"] += 1 | |
| elif entry.startswith("used_signature:"): | |
| continue | |
| elif entry.startswith("equipped_"): | |
| equipment[entry.removeprefix("equipped_")] += 1 | |
| skill_by_id = { | |
| skill.get("id"): skill | |
| for skill in ( | |
| list(state.get("run_skill_registry", {}).values()) | |
| + list(state.get("active_skills", [])) | |
| ) | |
| } | |
| for skill_id, count in skill_use.items(): | |
| skill = skill_by_id.get(skill_id) | |
| if skill: | |
| element_use[str(skill.get("element", "Physical"))] += count | |
| scaling_use[str(skill.get("scaling_stat", "str_scaling_dial"))] += count | |
| effect_use[str(skill.get("effect_id", "damage"))] += count | |
| rider = str(skill.get("rider_effect", "none")) | |
| if rider != "none": | |
| effect_use[rider] += count | |
| events = state.get("combat_events", []) | |
| defensive_reactions = sum( | |
| 1 | |
| for event in events | |
| if event.get("actor") == "Player" | |
| and any(word in str(event.get("text", "")).lower() for word in ("brace", "guard", "fortif")) | |
| ) | |
| for index, event in enumerate(events): | |
| if event.get("actor") != "Enemy": | |
| continue | |
| text = str(event.get("text", "")).lower() | |
| intent = "attack" if "damage" in text else "defend" if "defense up" in text else "tactic" | |
| if index + 1 < len(events) and events[index + 1].get("actor") == "Player": | |
| intent_responses[intent] += 1 | |
| total = sum(actions.values()) | |
| ratios = { | |
| key: round(actions[key] / total, 3) if total else 0.0 | |
| for key in ("strike", "skill", "defend", "item") | |
| } | |
| patterns = [ | |
| f"frequent_{name}" | |
| for name, count in actions.most_common(2) | |
| if total and count / total >= 0.35 | |
| ] | |
| if state.get("heals_refused", 0): | |
| patterns.append("refuses_healing") | |
| affinities = Counter() | |
| affinities["strength"] += actions["strike"] ** 1.25 | |
| affinities["Physical"] += actions["strike"] ** 1.25 | |
| affinities["damage"] += actions["strike"] ** 1.25 | |
| affinities["endurance"] += actions["defend"] ** 1.25 | |
| affinities["guard"] += actions["defend"] ** 1.25 | |
| affinities["armor"] += actions["defend"] ** 1.25 | |
| for scaling, count in scaling_use.items(): | |
| affinities[SCALING_LABELS.get(scaling, scaling)] += count ** 1.25 | |
| for element, count in element_use.items(): | |
| affinities[element] += count ** 1.25 | |
| for effect, count in effect_use.items(): | |
| affinities[effect] += count ** 1.25 | |
| dominant_action = actions.most_common(1)[0][0] if actions else "" | |
| dominant_scaling = scaling_use.most_common(1)[0][0] if scaling_use else ( | |
| "str_scaling_dial" if dominant_action == "strike" | |
| else "def_scaling_dial" if dominant_action == "defend" | |
| else "" | |
| ) | |
| dominant_element = element_use.most_common(1)[0][0] if element_use else ( | |
| "Physical" if dominant_action == "strike" else "" | |
| ) | |
| return BehaviorSummary( | |
| total_actions=total, | |
| action_ratios=ratios, | |
| skill_use=dict(skill_use), | |
| element_use=dict(element_use), | |
| defensive_reactions=defensive_reactions, | |
| intent_responses=dict(intent_responses), | |
| healing_accepted=state.get("healing_decisions", []).count("accepted"), | |
| healing_refused=int(state.get("heals_refused", 0)), | |
| equipment_preferences=dict(equipment), | |
| low_health_turns=sum( | |
| 1 | |
| for event in events | |
| if "hp" in str(event.get("text", "")).lower() | |
| and any(token in str(event.get("text", "")) for token in ("1/", "2/", "3/")) | |
| ), | |
| repeated_patterns=patterns[:6], | |
| affinity_scores={key: round(value, 3) for key, value in affinities.items()}, | |
| dominant_action=dominant_action, | |
| dominant_scaling_stat=dominant_scaling, | |
| dominant_element=dominant_element, | |
| ) | |
| def blend_behavior_summaries( | |
| earlier: BehaviorSummary, | |
| recent: BehaviorSummary, | |
| *, | |
| recent_weight: float = 0.7, | |
| ) -> BehaviorSummary: | |
| earlier_weight = 1.0 - recent_weight | |
| def blend_dict(left: dict[str, float], right: dict[str, float]) -> dict[str, float]: | |
| keys = set(left) | set(right) | |
| return { | |
| key: round(float(left.get(key, 0)) * earlier_weight + float(right.get(key, 0)) * recent_weight, 3) | |
| for key in keys | |
| } | |
| result = deepcopy(recent.model_dump()) | |
| result["total_actions"] = round( | |
| earlier.total_actions * earlier_weight + recent.total_actions * recent_weight | |
| ) | |
| result["action_ratios"] = blend_dict(earlier.action_ratios, recent.action_ratios) | |
| result["skill_use"] = { | |
| key: round(value) | |
| for key, value in blend_dict(earlier.skill_use, recent.skill_use).items() | |
| } | |
| result["element_use"] = { | |
| key: round(value) | |
| for key, value in blend_dict(earlier.element_use, recent.element_use).items() | |
| } | |
| result["affinity_scores"] = blend_dict( | |
| earlier.affinity_scores, recent.affinity_scores | |
| ) | |
| dominant = max(result["affinity_scores"], key=result["affinity_scores"].get, default="") | |
| scaling_by_affinity = { | |
| "strength": "str_scaling_dial", | |
| "agility": "agi_scaling_dial", | |
| "intelligence": "int_scaling_dial", | |
| "endurance": "def_scaling_dial", | |
| } | |
| result["dominant_scaling_stat"] = scaling_by_affinity.get( | |
| dominant, recent.dominant_scaling_stat or earlier.dominant_scaling_stat | |
| ) | |
| result["dominant_element"] = recent.dominant_element or earlier.dominant_element | |
| result["dominant_action"] = recent.dominant_action or earlier.dominant_action | |
| result["repeated_patterns"] = list( | |
| dict.fromkeys([*recent.repeated_patterns, *earlier.repeated_patterns]) | |
| )[:6] | |
| return BehaviorSummary.model_validate(result) | |
| def evolution_behavior_summary( | |
| state: dict[str, Any], | |
| stage: int, | |
| ) -> BehaviorSummary: | |
| recent = summarize_behavior(state) | |
| stored = state.get("behavior_summaries", []) | |
| if stage >= 2 and stored: | |
| earlier = BehaviorSummary.model_validate(stored[0]) | |
| return blend_behavior_summaries(earlier, recent, recent_weight=0.7) | |
| return recent | |