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