Project-Mahoraga / env /enemy.py
MridulNegi2005's picture
Initial HF deployment commit
c9d1b27
import random
import utils.constants as const
from utils.constants import (
SUBTYPES, ATTACK_TYPES, BASE_DAMAGE,
PHASE_1_END, PHASE_2_END
)
class CurriculumEnemy:
"""3-phase curriculum enemy for RL training.
Phase 1 (turns 1-5): Always PHYSICAL
Phase 2 (turns 6-15): Cycle PHYSICAL -> CE -> TECHNIQUE, 15% random injection
Phase 3 (turns 16-25): Target lowest resistance category
Difficulty levels:
'easy' — Phase 1 only (always PHYSICAL)
'medium' — Phase 1 + Phase 2 (no adaptive targeting)
'hard' — All 3 phases (default)
"""
def __init__(self, difficulty="hard"):
self.turn = 0
self.pattern = ["PHYSICAL", "CE", "TECHNIQUE"]
self.pattern_index = 0
self.difficulty = difficulty.lower()
def get_attack(self, turn_number=None, resistances=None):
"""Returns dict: {category, subtype, damage, ignore_armor}.
Args:
turn_number: Current turn (1-indexed). Uses internal counter if None.
resistances: Dict of {PHYSICAL, CE, TECHNIQUE} values for Phase 3.
"""
if turn_number is not None:
self.turn = turn_number
else:
self.turn += 1
category = self._select_category(resistances)
subtype = random.choice(SUBTYPES[category])
ignore_armor = (subtype == "PIERCE")
return {
"category": category,
"subtype": subtype,
"damage": BASE_DAMAGE[category],
"ignore_armor": ignore_armor
}
def _select_category(self, resistances=None):
"""Select attack category based on current phase and difficulty."""
# Easy: always Phase 1 behavior
if self.difficulty == "easy":
return "PHYSICAL"
if self.turn <= PHASE_1_END:
# Phase 1: Always PHYSICAL
return "PHYSICAL"
elif self.turn <= PHASE_2_END:
# Phase 2: Cycle with random injection
if random.random() < const.PHASE_2_DEVIATION:
return random.choice(ATTACK_TYPES)
else:
category = self.pattern[self.pattern_index]
self.pattern_index = (self.pattern_index + 1) % len(self.pattern)
return category
else:
# Phase 3: Target lowest resistance (hard only)
if self.difficulty == "medium":
# Medium caps at Phase 2 cycling behavior
if random.random() < const.PHASE_2_DEVIATION:
return random.choice(ATTACK_TYPES)
else:
category = self.pattern[self.pattern_index]
self.pattern_index = (self.pattern_index + 1) % len(self.pattern)
return category
# Hard: full adaptive AI
if resistances is None:
return random.choice(ATTACK_TYPES)
lowest = min(resistances, key=resistances.get)
return lowest
class DifficultyEnemy:
"""Difficulty-based enemy for EVALUATION only.
Modes:
'easy' — Always same category (PHYSICAL). No randomness.
'medium' — Cycles PHYSICAL→CE→TECHNIQUE, 10% random injection.
'hard' — Targets lowest resistance, 20% random injection.
"""
VALID_DIFFICULTIES = ("easy", "medium", "hard")
def __init__(self, difficulty="medium"):
if difficulty not in self.VALID_DIFFICULTIES:
raise ValueError(f"Invalid difficulty: {difficulty}. Must be one of {self.VALID_DIFFICULTIES}")
self.difficulty = difficulty
self.pattern = ["PHYSICAL", "CE", "TECHNIQUE"]
self.pattern_index = 0
def get_attack(self, turn_number=None, resistances=None):
"""Returns dict: {category, subtype, damage, ignore_armor}.
Same interface as CurriculumEnemy for drop-in use.
"""
category = self._select_category(resistances)
subtype = random.choice(SUBTYPES[category])
ignore_armor = (subtype == "PIERCE")
return {
"category": category,
"subtype": subtype,
"damage": BASE_DAMAGE[category],
"ignore_armor": ignore_armor
}
def _select_category(self, resistances=None):
"""Select attack category based on difficulty mode."""
if self.difficulty == "easy":
# Always PHYSICAL — no randomness
return "PHYSICAL"
elif self.difficulty == "medium":
# Cycle with 10% random injection
if random.random() < 0.10:
return random.choice(ATTACK_TYPES)
category = self.pattern[self.pattern_index]
self.pattern_index = (self.pattern_index + 1) % len(self.pattern)
return category
else: # hard
# Target lowest resistance with 20% random injection
if random.random() < 0.20:
return random.choice(ATTACK_TYPES)
if resistances is None:
return random.choice(ATTACK_TYPES)
lowest = min(resistances, key=resistances.get)
return lowest