Spaces:
Running on Zero
Running on Zero
| """ | |
| features.py -- the single source of truth for what the Modular Mind sees and the | |
| action set it chooses from. Imported by the duel simulator (training env), the | |
| torch model, and the numpy inference brain so all three agree exactly. | |
| """ | |
| from __future__ import annotations | |
| import numpy as np | |
| ACTIONS = ["IDLE", "APPROACH", "RETREAT", "CLEAVE", "BLOCK"] | |
| FEATURES = [ | |
| "abs_dist", # 0 normalised |player - boss|, 0 close .. 1 far | |
| "in_cleave_range", # 1 player within the boss's cleave reach | |
| "in_mid_range", # 2 player in the "step in and swing" band | |
| "boss_hp", # 3 boss hp fraction | |
| "player_hp", # 4 player hp fraction | |
| "boss_ready", # 5 1 = cleave off cooldown, 0 = recovering | |
| "player_pressuring", # 6 player attacking or moving in | |
| "player_open", # 7 player in attack/roll recovery = punishable | |
| "player_blocking", # 8 player is holding block | |
| "player_threat", # 9 player is swinging AT the boss in melee range (block cue) | |
| "bias", # 10 | |
| ] | |
| NF = len(FEATURES) | |
| def extract_features(s: dict) -> np.ndarray: | |
| arena = float(s.get("arenaW", 900)) | |
| dist = abs(float(s["playerX"]) - float(s["bossX"])) | |
| abs_dist = min(dist / (arena * 0.6), 1.0) | |
| cleave_reach = float(s.get("cleaveReach", 160)) | |
| f = np.zeros(NF, dtype=np.float32) | |
| f[0] = abs_dist | |
| f[1] = 1.0 if dist <= cleave_reach else 0.0 | |
| f[2] = 1.0 if cleave_reach < dist <= cleave_reach * 2.2 else 0.0 | |
| f[3] = float(s.get("bossHP", 1.0)) | |
| f[4] = float(s.get("playerHP", 1.0)) | |
| f[5] = 1.0 if float(s.get("bossCooldown", 0.0)) <= 0.0 else 0.0 | |
| f[6] = 1.0 if (s.get("playerAttacking") or s.get("playerApproaching")) else 0.0 | |
| f[7] = 1.0 if s.get("playerRecovering") else 0.0 | |
| f[8] = 1.0 if s.get("playerBlocking") else 0.0 | |
| f[9] = 1.0 if s.get("playerThreat") else 0.0 | |
| f[10] = 1.0 | |
| return f | |
| def legal_mask(s: dict) -> np.ndarray: | |
| """CLEAVE is illegal while on cooldown; everything else always allowed.""" | |
| m = np.ones(len(ACTIONS), dtype=np.float32) | |
| if float(s.get("bossCooldown", 0.0)) > 0.0: | |
| m[ACTIONS.index("CLEAVE")] = 0.0 | |
| return m | |