Spaces:
Sleeping
Sleeping
| import sys | |
| import os | |
| sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) | |
| from env.mahoraga_env import MahoragaEnv | |
| from env.mechanics import ( | |
| new_resistances, apply_resistance_change, compute_enemy_damage, | |
| compute_judgment_damage, check_correct_adaptation | |
| ) | |
| from env.enemy import CurriculumEnemy | |
| from env.rewards import compute_rewards | |
| from utils.constants import SUBTYPES, ATTACK_TYPES, MAX_HP, ENEMY_HP | |
| PASS = 0 | |
| FAIL = 0 | |
| def check(name, condition): | |
| global PASS, FAIL | |
| if condition: | |
| PASS += 1 | |
| print(f" [PASS] {name}") | |
| else: | |
| FAIL += 1 | |
| print(f" [FAIL] {name}") | |
| # ========== PHASE 1 CORE TESTS ========== | |
| def test_resistance_update(): | |
| print("\n--- Test: Resistance Update ---") | |
| res = new_resistances() | |
| res = apply_resistance_change(res, "PHYSICAL") | |
| check("PHYSICAL +40", res["PHYSICAL"] == 40) | |
| check("CE -20 clamped to 0", res["CE"] == 0) | |
| check("TECHNIQUE -20 clamped to 0", res["TECHNIQUE"] == 0) | |
| res = apply_resistance_change(res, "PHYSICAL") | |
| check("PHYSICAL +40 again = 80", res["PHYSICAL"] == 80) | |
| check("CE stays 0", res["CE"] == 0) | |
| check("TECHNIQUE stays 0", res["TECHNIQUE"] == 0) | |
| def test_clamp_logic(): | |
| print("\n--- Test: Clamp Logic ---") | |
| res = {"PHYSICAL": 70, "CE": 10, "TECHNIQUE": 10} | |
| res = apply_resistance_change(res, "PHYSICAL") | |
| check("PHYSICAL clamped to 80 (70+40)", res["PHYSICAL"] == 80) | |
| check("CE clamped to 0 (10-20)", res["CE"] == 0) | |
| check("TECHNIQUE clamped to 0 (10-20)", res["TECHNIQUE"] == 0) | |
| def test_damage_formula(): | |
| print("\n--- Test: Damage Formula ---") | |
| res = new_resistances() | |
| dmg = compute_enemy_damage("PHYSICAL", res) | |
| check("PHYSICAL full damage = 120", dmg == 120) | |
| dmg = compute_enemy_damage("CE", res) | |
| check("CE full damage = 150", dmg == 150) | |
| dmg = compute_enemy_damage("TECHNIQUE", res) | |
| check("TECHNIQUE full damage = 220", dmg == 220) | |
| res["PHYSICAL"] = 50 | |
| dmg = compute_enemy_damage("PHYSICAL", res) | |
| check("PHYSICAL with 50 res = 60", dmg == 60) | |
| def test_judgment_damage(): | |
| print("\n--- Test: Judgment Damage (Adaptation-Match) ---") | |
| # No adaptation — base damage | |
| dmg = compute_judgment_damage(None, "PHYSICAL") | |
| check("Base judgment (no adapt) = 100", dmg == 100) | |
| # Matching adaptation — burst | |
| dmg = compute_judgment_damage("PHYSICAL", "PHYSICAL") | |
| check("Burst judgment (matching adapt) = 350", dmg == 350) | |
| # Non-matching adaptation — no burst | |
| dmg = compute_judgment_damage("CE", "PHYSICAL") | |
| check("No burst when non-matching adapt = 100", dmg == 100) | |
| def test_episode_termination(): | |
| print("\n--- Test: Episode Termination ---") | |
| env = MahoragaEnv() | |
| env.reset() | |
| # Ensure agent survives all 25 turns by giving it massive HP | |
| env.agent_hp = 99999 | |
| # Also prevent accidental enemy death by giving enemy massive HP | |
| env.enemy_hp = 99999 | |
| done = False | |
| info = {} | |
| for i in range(25): | |
| if not done: | |
| _, _, done, info = env.step(0) # Only adapt, never attack | |
| check("Episode ends at turn 25", done is True) | |
| check("Reason is turn limit", info.get("reason") == "Turn limit reached") | |
| def test_actions_behave_correctly(): | |
| print("\n--- Test: Action Behavior ---") | |
| env = MahoragaEnv() | |
| env.reset() | |
| state, _, _, _ = env.step(0) | |
| check("Adapt PHYSICAL sets res to 40", state["resistances"]["physical"] == 40) | |
| # Judgment Strike resets resistances | |
| env.reset() | |
| env.step(0) | |
| env.step(0) | |
| state, _, _, _ = env.step(3) | |
| check("Judgment resets resistances", state["resistances"]["physical"] == 0) | |
| check("Judgment damages enemy", state["enemy_hp"] < ENEMY_HP) | |
| # Regeneration heals but does NOT reset resistances | |
| env.reset() | |
| env.step(0) | |
| hp_before = env.agent_hp | |
| res_before = env.resistances["PHYSICAL"] | |
| state, _, _, _ = env.step(4) | |
| check("Regeneration heals agent", state["agent_hp"] > hp_before - 120) | |
| check("Regeneration does NOT reset resistances", state["resistances"]["physical"] == res_before) | |
| def test_adaptation_stack(): | |
| print("\n--- Test: Adaptation Stack ---") | |
| env = MahoragaEnv() | |
| env.reset() | |
| # Phase 1 enemy always attacks PHYSICAL | |
| env.step(0) | |
| check("Stack +1 on correct adapt", env.adaptation_stack == 1) | |
| env.step(0) | |
| check("Stack +2 on second correct adapt", env.adaptation_stack == 2) | |
| env.reset() | |
| env.step(1) # CE adapt vs PHYSICAL enemy | |
| check("Stack stays 0 on wrong adapt", env.adaptation_stack == 0) | |
| env.reset() | |
| env.step(0) | |
| env.step(0) | |
| env.step(3) | |
| check("Stack reset to 0 after Judgment", env.adaptation_stack == 0) | |
| def test_invalid_action(): | |
| print("\n--- Test: Invalid Action ---") | |
| env = MahoragaEnv() | |
| env.reset() | |
| try: | |
| env.step(5) | |
| check("Rejects action 5", False) | |
| except ValueError: | |
| check("Rejects action 5", True) | |
| try: | |
| env.step(-1) | |
| check("Rejects action -1", False) | |
| except ValueError: | |
| check("Rejects action -1", True) | |
| def test_heal_cooldown(): | |
| print("\n--- Test: Heal Cooldown ---") | |
| env = MahoragaEnv() | |
| env.reset() | |
| state, _, _, info = env.step(4) | |
| check("First heal works", info["heal_on_cooldown"] is False) | |
| check("Cooldown set to 3", env.heal_cooldown_counter == 3) | |
| _, _, _, info2 = env.step(4) | |
| check("Heal blocked on turn 2 (cooldown)", info2["heal_on_cooldown"] is True) | |
| _, _, _, info3 = env.step(4) | |
| check("Heal blocked on turn 3 (cooldown)", info3["heal_on_cooldown"] is True) | |
| _, _, _, info4 = env.step(4) | |
| check("Heal available on turn 4 (cooldown expired)", info4["heal_on_cooldown"] is False) | |
| def test_heal_preserves_resistances(): | |
| print("\n--- Test: Heal Preserves Resistances ---") | |
| env = MahoragaEnv() | |
| env.reset() | |
| env.step(0) | |
| env.step(0) | |
| res_before = env.resistances.copy() | |
| env.step(4) | |
| check("PHYSICAL res preserved after heal", env.resistances["PHYSICAL"] == res_before["PHYSICAL"]) | |
| check("CE res preserved after heal", env.resistances["CE"] == res_before["CE"]) | |
| check("TECHNIQUE res preserved after heal", env.resistances["TECHNIQUE"] == res_before["TECHNIQUE"]) | |
| def test_judgment_burst_adaptation_match(): | |
| print("\n--- Test: Judgment Burst Via Adaptation Match ---") | |
| env = MahoragaEnv() | |
| env.reset() | |
| # Adapt PHYSICAL (correct for Phase 1 enemy) then Judgment | |
| env.step(0) # Adapt PHYSICAL, correct, stack=1, last_adapted=PHYSICAL | |
| env.step(0) # Adapt PHYSICAL, correct, stack=2, last_adapted=PHYSICAL | |
| enemy_hp_before = env.enemy_hp | |
| env.step(3) # Judgment — last_adapted=PHYSICAL, enemy=PHYSICAL → BURST | |
| damage_dealt = enemy_hp_before - env.enemy_hp | |
| # burst 350 + stack 2*50 = 450 | |
| check("Burst when adapted to matching category (dmg=450)", damage_dealt == 450) | |
| # Wrong adaptation — should NOT burst | |
| env.reset() | |
| env.step(1) # Adapt CE, but enemy is PHYSICAL | |
| env.step(1) # Adapt CE again | |
| enemy_hp_before = env.enemy_hp | |
| env.step(3) # Judgment — last_adapted=CE, enemy=PHYSICAL → NO BURST | |
| damage_dealt = enemy_hp_before - env.enemy_hp | |
| # base 100 + stack 0*50 = 100 | |
| check("No burst when adapted to wrong category (dmg=100)", damage_dealt == 100) | |
| def test_info_dict_fields(): | |
| print("\n--- Test: Info Dict Fields ---") | |
| env = MahoragaEnv() | |
| env.reset() | |
| _, _, _, info = env.step(0) | |
| required_keys = ["damage_taken", "damage_dealt", "correct_adaptation", "adaptation_stack", "heal_on_cooldown"] | |
| all_present = all(k in info for k in required_keys) | |
| check("Info dict has all required fields", all_present) | |
| check("correct_adaptation is True for matching adapt", info["correct_adaptation"] is True) | |
| check("damage_taken > 0", info["damage_taken"] > 0) | |
| check("damage_dealt = 0 for adapt action", info["damage_dealt"] == 0) | |
| def test_hp_configuration(): | |
| print("\n--- Test: HP Configuration ---") | |
| env = MahoragaEnv() | |
| state = env.reset() | |
| check("Agent HP = 1200", state["agent_hp"] == 1200) | |
| check("Enemy HP = 1000", state["enemy_hp"] == 1000) | |
| # ========== ENEMY TESTS ========== | |
| def test_subtype_mapping(): | |
| print("\n--- Test: Subtype Mapping ---") | |
| for attack_type in ATTACK_TYPES: | |
| check(f"{attack_type} has subtypes", attack_type in SUBTYPES) | |
| check(f"{attack_type} has 3 subtypes", len(SUBTYPES[attack_type]) == 3) | |
| check("PHYSICAL has PIERCE", "PIERCE" in SUBTYPES["PHYSICAL"]) | |
| check("CE has BEAM", "BEAM" in SUBTYPES["CE"]) | |
| check("TECHNIQUE has DELAYED", "DELAYED" in SUBTYPES["TECHNIQUE"]) | |
| def test_ignore_armor_bypass(): | |
| print("\n--- Test: Ignore Armor Bypass ---") | |
| res = new_resistances() | |
| res["PHYSICAL"] = 50 | |
| normal_dmg = compute_enemy_damage("PHYSICAL", res, ignore_armor=False) | |
| pierce_dmg = compute_enemy_damage("PHYSICAL", res, ignore_armor=True) | |
| check("Ignore armor does more damage than normal at 50 res", pierce_dmg > normal_dmg) | |
| check("Normal damage = 60", normal_dmg == 60) | |
| check("Armor bypass damage = 72", pierce_dmg == 72) | |
| res_zero = new_resistances() | |
| normal_zero = compute_enemy_damage("PHYSICAL", res_zero, ignore_armor=False) | |
| pierce_zero = compute_enemy_damage("PHYSICAL", res_zero, ignore_armor=True) | |
| check("Same damage at 0 resistance", normal_zero == pierce_zero) | |
| def test_curriculum_phase_1(): | |
| print("\n--- Test: Curriculum Phase 1 (Turns 1-5) ---") | |
| enemy = CurriculumEnemy() | |
| for turn in range(1, 6): | |
| attack = enemy.get_attack(turn_number=turn) | |
| check(f"Turn {turn} is PHYSICAL", attack["category"] == "PHYSICAL") | |
| check(f"Turn {turn} has damage", attack["damage"] == 120) | |
| check(f"Turn {turn} has ignore_armor field", "ignore_armor" in attack) | |
| check(f"Turn {turn} subtype valid", attack["subtype"] in SUBTYPES["PHYSICAL"]) | |
| def test_curriculum_phase_2(): | |
| print("\n--- Test: Curriculum Phase 2 (Cycle, No Deviation) ---") | |
| # Use a fresh enemy and go directly to Phase 2 turns | |
| enemy = CurriculumEnemy() | |
| import utils.constants as c | |
| old_dev = c.PHASE_2_DEVIATION | |
| c.PHASE_2_DEVIATION = 0 # Temporarily disable randomness | |
| a6 = enemy.get_attack(turn_number=6) | |
| check("Phase 2 step 1 = PHYSICAL", a6["category"] == "PHYSICAL") | |
| a7 = enemy.get_attack(turn_number=7) | |
| check("Phase 2 step 2 = CE", a7["category"] == "CE") | |
| a8 = enemy.get_attack(turn_number=8) | |
| check("Phase 2 step 3 = TECHNIQUE", a8["category"] == "TECHNIQUE") | |
| a9 = enemy.get_attack(turn_number=9) | |
| check("Phase 2 step 4 = PHYSICAL (cycle)", a9["category"] == "PHYSICAL") | |
| c.PHASE_2_DEVIATION = old_dev # Restore | |
| def test_curriculum_phase_3(): | |
| print("\n--- Test: Curriculum Phase 3 (Target Lowest) ---") | |
| enemy = CurriculumEnemy() | |
| # Phase 3 starts at turn 16 | |
| resistances = {"PHYSICAL": 80, "CE": 40, "TECHNIQUE": 0} | |
| attack = enemy.get_attack(turn_number=16, resistances=resistances) | |
| check("Phase 3 targets lowest resistance (TECHNIQUE)", attack["category"] == "TECHNIQUE") | |
| resistances2 = {"PHYSICAL": 0, "CE": 80, "TECHNIQUE": 40} | |
| attack2 = enemy.get_attack(turn_number=17, resistances=resistances2) | |
| check("Phase 3 targets lowest (PHYSICAL)", attack2["category"] == "PHYSICAL") | |
| def test_enemy_dict_format(): | |
| print("\n--- Test: Enemy Dict Format ---") | |
| enemy = CurriculumEnemy() | |
| attack = enemy.get_attack(turn_number=1) | |
| check("Returns dict", isinstance(attack, dict)) | |
| check("Has 'category'", "category" in attack) | |
| check("Has 'subtype'", "subtype" in attack) | |
| check("Has 'damage'", "damage" in attack) | |
| check("Has 'ignore_armor'", "ignore_armor" in attack) | |
| check("category is PHYSICAL", attack["category"] == "PHYSICAL") | |
| check("subtype is valid", attack["subtype"] in SUBTYPES["PHYSICAL"]) | |
| def test_rl_observation_uses_3_types_only(): | |
| print("\n--- Test: RL Observation Uses Only 3 Types ---") | |
| env = MahoragaEnv() | |
| env.reset() | |
| state, _, _, _ = env.step(0) | |
| check("last_enemy_attack_type is a valid RL type", | |
| state["last_enemy_attack_type"] in ATTACK_TYPES) | |
| check("Resistances use 3 keys only", | |
| set(state["resistances"].keys()) == {"physical", "ce", "technique"}) | |
| # ========== REWARD TESTS ========== | |
| def test_adaptation_reward(): | |
| print("\n--- Test: Adaptation Reward ---") | |
| env = MahoragaEnv() | |
| env.reset() | |
| _, reward, _, info = env.step(0) | |
| breakdown = info["reward_breakdown"] | |
| check("Correct adapt gives +0.8", breakdown["adaptation"] == 0.8) | |
| env.reset() | |
| _, reward, _, info = env.step(1) | |
| breakdown = info["reward_breakdown"] | |
| check("Wrong adapt gives 0.0", breakdown["adaptation"] == 0.0) | |
| def test_damage_rewards(): | |
| print("\n--- Test: Damage Rewards ---") | |
| env = MahoragaEnv() | |
| env.reset() | |
| _, _, _, info = env.step(0) | |
| breakdown = info["reward_breakdown"] | |
| check("Survival reward is negative (took damage)", breakdown["survival"] < 0) | |
| env.reset() | |
| _, _, _, info = env.step(3) | |
| breakdown = info["reward_breakdown"] | |
| check("Combat reward is positive (dealt damage)", breakdown["combat"] > 0) | |
| env.reset() | |
| _, _, _, info = env.step(0) | |
| breakdown = info["reward_breakdown"] | |
| check("Combat reward is 0 for adapt action", breakdown["combat"] == 0.0) | |
| def test_heal_penalty(): | |
| print("\n--- Test: Heal Penalty (Anti-Cowardice) ---") | |
| env = MahoragaEnv() | |
| env.reset() | |
| _, _, _, info = env.step(4) | |
| breakdown = info["reward_breakdown"] | |
| check("Heal at high HP penalized (-1.0)", breakdown["anti_cowardice"] == -1.0) | |
| env.reset() | |
| for _ in range(7): | |
| env.step(1) | |
| check("Agent HP is low enough", env.agent_hp < 0.7 * MAX_HP) | |
| _, _, _, info = env.step(4) | |
| breakdown = info["reward_breakdown"] | |
| check("Heal at low HP NOT penalized (0.0)", breakdown["anti_cowardice"] == 0.0) | |
| def test_terminal_reward(): | |
| print("\n--- Test: Terminal Reward ---") | |
| info_win = {"damage_taken": 0, "damage_dealt": 100, "correct_adaptation": False} | |
| state_win = {"agent_hp": 500, "enemy_hp": 0} | |
| rewards = compute_rewards(info_win, state_win, 3, done=True) | |
| check("Win terminal = +10.0", rewards["terminal"] == 10.0) | |
| info_loss = {"damage_taken": 100, "damage_dealt": 0, "correct_adaptation": False} | |
| state_loss = {"agent_hp": 0, "enemy_hp": 500} | |
| rewards = compute_rewards(info_loss, state_loss, 0, done=True) | |
| check("Loss terminal = -8.0", rewards["terminal"] == -8.0) | |
| rewards_nd = compute_rewards(info_win, state_win, 0, done=False) | |
| check("Not done terminal = 0.0", rewards_nd["terminal"] == 0.0) | |
| def test_reward_breakdown_in_info(): | |
| print("\n--- Test: Reward Breakdown In Info ---") | |
| env = MahoragaEnv() | |
| env.reset() | |
| _, reward, _, info = env.step(0) | |
| check("Info has reward_breakdown", "reward_breakdown" in info) | |
| breakdown = info["reward_breakdown"] | |
| expected_keys = {"survival", "combat", "adaptation", "anti_cowardice", "efficiency", "terminal", "opportunity"} | |
| check("Breakdown has all 7 components", set(breakdown.keys()) == expected_keys) | |
| total = sum(breakdown.values()) | |
| check("Total reward = sum of components", abs(reward - total) < 1e-9) | |
| def test_opportunity_penalty(): | |
| print("\n--- Test: Opportunity Penalty ---") | |
| env = MahoragaEnv() | |
| env.reset() | |
| # Build 2 stacks via correct adaptation | |
| env.step(0) # stack=1 | |
| _, _, _, info = env.step(0) # stack=2 | |
| breakdown = info["reward_breakdown"] | |
| check("Opportunity penalty when stack>=2 and not striking (-0.5)", breakdown["opportunity"] == -0.5) | |
| # Now strike — no penalty | |
| env.reset() | |
| env.step(0) # stack=1 | |
| env.step(0) # stack=2 | |
| _, _, _, info3 = env.step(3) # Judgment Strike | |
| breakdown3 = info3["reward_breakdown"] | |
| check("No opportunity penalty when striking", breakdown3["opportunity"] == 0.0) | |
| def test_no_reward_for_nothing(): | |
| print("\n--- Test: No Free Rewards ---") | |
| env = MahoragaEnv() | |
| env.reset() | |
| env.step(4) | |
| _, reward, _, info = env.step(4) | |
| breakdown = info["reward_breakdown"] | |
| check("No adaptation reward on wasted turn", breakdown["adaptation"] == 0.0) | |
| check("No combat reward on wasted turn", breakdown["combat"] == 0.0) | |
| check("No efficiency bonus on wasted turn", breakdown["efficiency"] == 0.0) | |
| if __name__ == "__main__": | |
| print("=" * 50) | |
| print(" MahoragaEnv Merged System Tests") | |
| print("=" * 50) | |
| # Core mechanics | |
| test_resistance_update() | |
| test_clamp_logic() | |
| test_damage_formula() | |
| test_judgment_damage() | |
| test_episode_termination() | |
| test_actions_behave_correctly() | |
| test_adaptation_stack() | |
| test_invalid_action() | |
| test_heal_cooldown() | |
| test_heal_preserves_resistances() | |
| test_judgment_burst_adaptation_match() | |
| test_info_dict_fields() | |
| test_hp_configuration() | |
| # Enemy system | |
| test_subtype_mapping() | |
| test_ignore_armor_bypass() | |
| test_curriculum_phase_1() | |
| test_curriculum_phase_2() | |
| test_curriculum_phase_3() | |
| test_enemy_dict_format() | |
| test_rl_observation_uses_3_types_only() | |
| # Rewards | |
| test_adaptation_reward() | |
| test_damage_rewards() | |
| test_heal_penalty() | |
| test_terminal_reward() | |
| test_reward_breakdown_in_info() | |
| test_opportunity_penalty() | |
| test_no_reward_for_nothing() | |
| print("\n" + "=" * 50) | |
| print(f" Results: {PASS} passed, {FAIL} failed") | |
| print("=" * 50) | |
| if FAIL > 0: | |
| sys.exit(1) | |