Spaces:
Sleeping
Sleeping
File size: 5,159 Bytes
c9d1b27 | 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 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 | 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
|