77from dataclasses import dataclass
88from typing import TYPE_CHECKING
99
10+ from brain .flee import ( # noqa: F401 -- re-exported for backward compatibility
11+ FLEE_URGENCY_ENTER ,
12+ FLEE_URGENCY_EXIT ,
13+ _fight_winnable ,
14+ _mob_attacking_player ,
15+ compute_flee_urgency ,
16+ )
1017from brain .rule_def import Consideration
1118from brain .rules .skip_log import SkipLog
1219from brain .scoring .curves import inverse_linear , inverse_logistic
@@ -43,9 +50,7 @@ class _SurvivalRuleState:
4350 last_patrol_evade : float = 0.0
4451
4552
46- # -- Flee urgency thresholds (hysteresis) --
47- FLEE_URGENCY_ENTER = 0.65 # start fleeing at this urgency
48- FLEE_URGENCY_EXIT = 0.35 # stop fleeing below this
53+ # Flee urgency thresholds and computation imported from brain.flee
4954
5055
5156def _check_core_safety_floors (ctx : SurvivalView , state : GameState , label : str ) -> bool | None :
@@ -67,145 +72,9 @@ def _check_core_safety_floors(ctx: SurvivalView, state: GameState, label: str) -
6772 return None
6873
6974
70- def _fight_winnable (state : GameState ) -> bool :
71- """True if player can finish the current fight without a pet.
72-
73- Conditions: player HP > 60% AND target npc HP < 50%.
74- At these thresholds a few Lifespikes will finish the npc.
75- """
76- t = state .target
77- if not t or t .hp_max <= 0 :
78- log .debug ("[DECISION] _fight_winnable: no valid target" )
79- return False
80- mob_hp : float = t .hp_current / max (t .hp_max , 1 )
81- winnable : bool = state .hp_pct > 0.60 and mob_hp < 0.50
82- return winnable
83-
84-
85- def _count_damaged_npcs (ctx : SurvivalView , state : GameState ) -> int :
86- """Count damaged NPCs within 40u (for add detection)."""
87- from brain .rules .skip_log import damaged_npcs_near
88-
89- return len (damaged_npcs_near (ctx , state , state .pos , 40 ))
90-
91-
92- def _mob_attacking_player (state : GameState ) -> bool :
93- """Return True if any NPC within 30u is targeting the player."""
94- for sp in state .spawns :
95- if (
96- sp .is_npc
97- and sp .hp_current > 0
98- and sp .target_name == state .name
99- and state .pos .dist_to (sp .pos ) < 30
100- ):
101- return True
102- return False
103-
104-
105- def _urgency_hp (hp_pct : float ) -> float :
106- """HP axis: power curve scaled so ~35% HP yields ~0.65."""
107- result : float = ((1.0 - hp_pct ) ** 1.8 ) * 1.45
108- return result
109-
110-
111- def _urgency_pet_died (ctx : SurvivalView , state : GameState , in_combat : bool ) -> float :
112- """Pet died mid-combat with unwinnable fight: +0.4."""
113- if in_combat and ctx .pet .just_died () and not _fight_winnable (state ):
114- return 0.4
115- return 0.0
116-
117-
118- def _urgency_adds (ctx : SurvivalView , state : GameState , in_combat : bool ) -> float :
119- """Extra damaged NPCs nearby: +0.15 per add."""
120- if not in_combat :
121- return 0.0
122- count = _count_damaged_npcs (ctx , state )
123- return 0.15 * max (0 , count - 1 )
124-
125-
126- def _urgency_target_dying (state : GameState ) -> float :
127- """Target nearly dead (<15% HP): -0.25 to finish the defeat."""
128- t = state .target
129- if t and t .is_npc and t .hp_max > 0 and t .hp_current > 0 :
130- if t .hp_current / t .hp_max < 0.15 :
131- return - 0.25
132- return 0.0
133-
134-
135- def _urgency_learned_danger (ctx : SurvivalView , in_combat : bool ) -> float :
136- """Learned danger from FightHistory (>0.7): +0.2."""
137- fh = ctx .fight_history
138- if not fh or not in_combat :
139- return 0.0
140- name = ctx .defeat_tracker .last_fight_name
141- if not name :
142- return 0.0
143- danger = fh .learned_danger (name )
144- return 0.2 if danger is not None and danger > 0.7 else 0.0
145-
146-
147- def _urgency_pet_hp_low (ctx : SurvivalView ) -> float :
148- """Pet HP below 30%: +0.1."""
149- world = ctx .world
150- if ctx .pet .alive and world :
151- pet_hp = world .pet_hp_pct
152- if 0 <= pet_hp < 0.30 :
153- return 0.1
154- return 0.0
155-
156-
157- def _urgency_red_threat (ctx : SurvivalView ) -> float :
158- """RED imminent threat: +0.5."""
159- if ctx .threat .imminent_threat and ctx .threat .imminent_threat_con == "red" :
160- return 0.5
161- return 0.0
162-
163-
164- def _urgency_mob_on_player (ctx : SurvivalView , state : GameState ) -> float :
165- """No pet + NPC attacking player: +0.5."""
166- if not ctx .pet .alive and not ctx .combat .engaged and _mob_attacking_player (state ):
167- return 0.5
168- return 0.0
169-
170-
171- def compute_flee_urgency (ctx : SurvivalView , state : GameState ) -> float :
172- """Composite flee urgency score (0.0 = safe, 1.0 = critical).
173-
174- Eight axes contribute additively, then clamp to [0, 1].
175- """
176- in_combat = ctx .in_active_combat
177-
178- hp = _urgency_hp (state .hp_pct )
179- pet_died = _urgency_pet_died (ctx , state , in_combat )
180- adds = _urgency_adds (ctx , state , in_combat )
181- target_dying = _urgency_target_dying (state )
182- danger = _urgency_learned_danger (ctx , in_combat )
183- pet_low = _urgency_pet_hp_low (ctx )
184- red = _urgency_red_threat (ctx )
185- mob_on = _urgency_mob_on_player (ctx , state )
186-
187- result : float = max (0.0 , min (1.0 , hp + pet_died + adds + target_dying + danger + pet_low + red + mob_on ))
188-
189- if result >= FLEE_URGENCY_ENTER :
190- factors = []
191- if hp > 0.01 :
192- factors .append (f"hp={ hp :.2f} " )
193- if pet_died > 0 :
194- factors .append ("pet_died" )
195- if adds > 0 :
196- factors .append (f"extra_npcs={ adds / 0.15 :.0f} " )
197- if red > 0 :
198- factors .append ("RED_threat" )
199- if mob_on > 0 :
200- factors .append ("mob_on_player" )
201- log .info (
202- "[DECISION] Flee urgency: %.3f [%s] combat=%s" ,
203- result ,
204- " " .join (factors ) if factors else "hp_only" ,
205- in_combat ,
206- )
207-
208- return result
75+ # _fight_winnable, _count_damaged_npcs, _mob_attacking_player, _urgency_*,
76+ # and compute_flee_urgency are imported from brain.flee at the top of this
77+ # module. They remain importable from here for backward compatibility.
20978
21079
21180def _next_pull_mana_estimate (ctx : SurvivalView ) -> int | None :
0 commit comments