"""Superpower Assignment Algorithm. Assigns superpowers to glyphs based on: 1. Specialized type (if any) 2. Dynamic metrics calculation (5-25 range for G002-G600) 3. Band eligibility 4. Scoring and ranking """ import hashlib from typing import Dict, List, Any, Optional from .superpower_registry import ( load_all_superpowers, get_superpower, get_superpowers_by_band, get_superpowers_by_bands, ) from .specialized_types import get_specialized_type, get_type_config, SPECIALIZED_GLYPH_TYPES def calculate_power_count(metrics: Dict[str, Any], specialized_type: str = "") -> int: """Calculate number of superpowers for a glyph (5-25 range). Formula: power_count = 5 + int((avg_metric / 100) * 20) Args: metrics: Glyph metrics dict (power, complexity, resonance, stability, connectivity, affinity) specialized_type: Optional specialized type name Returns: Power count (5-25) """ # Get metric values values = [ metrics.get("power", 50), metrics.get("complexity", 50), metrics.get("resonance", 50), metrics.get("stability", 50), metrics.get("connectivity", 50), metrics.get("affinity", 50), ] avg_metric = sum(values) / len(values) # Calculate base count (5-25 range) base_count = 5 + int((avg_metric / 100) * 20) # Clamp to range base_count = max(5, min(25, base_count)) # Override for specialized types if specialized_type: type_config = get_type_config(specialized_type) min_powers = type_config.get("min_powers", 5) max_powers = type_config.get("max_powers", 25) # For aether_node, always 152 if specialized_type == "aether_node": return 152 # Clamp to type's range base_count = max(min_powers, min(max_powers, base_count)) return base_count def get_eligible_bands(glyph_id: str, specialized_type: str = "") -> List[str]: """Get eligible superpower bands for a glyph. Args: glyph_id: Glyph ID specialized_type: Optional specialized type Returns: List of band identifiers (e.g., ["A", "B"]) """ # Aether node gets all bands if specialized_type == "aether_node": return ["A", "B", "C", "D"] # Monument grade gets all bands if specialized_type == "monument_grade_equilibrium": return ["A", "B", "C", "D"] # Get type config if specialized_type: type_config = get_type_config(specialized_type) preferred_ids = type_config.get("preferred_superpower_ids", []) # Determine bands from preferred IDs bands = set() for id in preferred_ids: if id <= 15: bands.add("A") elif id <= 45: bands.add("B") elif id <= 76: bands.add("C") else: bands.add("D") return list(bands) # Default: based on glyph ID (family/tier) glyph_num = int(glyph_id[1:]) if glyph_id.startswith("G") else 1 tier = ((glyph_num - 1) // 10) + 1 if tier <= 15: return ["A", "B"] elif tier <= 30: return ["B", "C"] elif tier <= 45: return ["C", "D"] else: return ["D", "C"] def score_superpower( superpower: Dict[str, Any], glyph_id: str, metrics: Dict[str, Any], specialized_type: str = "" ) -> float: """Score a superpower for a glyph. Formula: score = 0.45 × metrics + 0.35 × type_bias + 0.15 × boost% + 0.05 × hash Args: superpower: Superpower dict glyph_id: Glyph ID metrics: Glyph metrics specialized_type: Optional specialized type Returns: Score (0-100) """ # Metrics component (45%) avg_metric = sum(metrics.values()) / len(metrics) if metrics else 50 metrics_score = avg_metric * 0.45 # Type bias component (35%) type_bias = 50 # Default neutral if specialized_type: type_config = get_type_config(specialized_type) preferred_ids = type_config.get("preferred_superpower_ids", []) if superpower["id"] in preferred_ids: type_bias = 100 # Preferred else: type_bias = 25 # Not preferred type_score = type_bias * 0.35 # Boost component (15%) boost = superpower.get("boost_percent", 0) boost_score = (boost / 100) * 0.15 * 100 # Normalize to 0-100 scale # Hash component (5%) - deterministic variety hash_input = f"{glyph_id}_{superpower['id']}" hash_value = int(hashlib.md5(hash_input.encode()).hexdigest()[:8], 16) hash_score = (hash_value / 0xFFFFFFFF) * 100 * 0.05 return metrics_score + type_score + boost_score + hash_score def assign_superpowers( glyph_id: str, metrics: Dict[str, Any], specialized_type: str = "", category: str = "" ) -> List[int]: """Assign superpowers to a glyph. Args: glyph_id: Glyph ID (e.g., "G001") metrics: Glyph metrics dict specialized_type: Optional specialized type category: Optional category Returns: List of superpower IDs """ # Load superpowers if not loaded try: load_all_superpowers() except FileNotFoundError: # If superpowers not loaded, return empty return [] # Special case: G001 / aether_node gets all 152 if glyph_id == "G001" or specialized_type == "aether_node": return list(range(1, 153)) # 1-152 # Determine specialized type if not provided if not specialized_type: specialized_type = get_specialized_type(glyph_id, metrics, category) # Calculate power count power_count = calculate_power_count(metrics, specialized_type) # Get eligible bands eligible_bands = get_eligible_bands(glyph_id, specialized_type) # Get eligible superpowers eligible_powers = get_superpowers_by_bands(eligible_bands) # Score and rank scored = [] for sp in eligible_powers: score = score_superpower(sp, glyph_id, metrics, specialized_type) scored.append((score, sp)) # Sort by score descending scored.sort(key=lambda x: x[0], reverse=True) # Take top N result = [sp["id"] for score, sp in scored[:power_count]] return result def assign_all_glyphs(glyphs: List[Dict[str, Any]]) -> Dict[str, List[int]]: """Assign superpowers to all glyphs. Args: glyphs: List of glyph dicts Returns: Dict mapping glyph_id to superpower IDs """ result = {} for glyph in glyphs: glyph_id = glyph.get("id", "") metrics = glyph.get("originalMetrics", {}) category = glyph.get("category", "") # Get specialized type specialized_type = get_specialized_type(glyph_id, metrics, category) # Assign superpowers superpower_ids = assign_superpowers(glyph_id, metrics, specialized_type, category) result[glyph_id] = superpower_ids return result