File size: 2,176 Bytes
45e7dfb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
"""
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