Spaces:
Sleeping
Sleeping
| 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 | |