246
tests/test_hidden_domain.py
Normal file
246
tests/test_hidden_domain.py
Normal file
@@ -0,0 +1,246 @@
|
||||
import pytest
|
||||
import random
|
||||
from unittest.mock import MagicMock, patch, AsyncMock
|
||||
from src.classes.gathering.hidden_domain import HiddenDomain
|
||||
from src.classes.cultivation import Realm
|
||||
from src.classes.death_reason import DeathReason, DeathType
|
||||
from src.classes.item import Item
|
||||
|
||||
@pytest.fixture
|
||||
def mock_domain_config():
|
||||
"""Mock configuration for hidden domains."""
|
||||
return [
|
||||
{
|
||||
"id": "domain_low",
|
||||
"name": "Low Realm Domain",
|
||||
"desc": "For weaklings",
|
||||
"max_realm": "Qi Refinement",
|
||||
"danger_prob": 0.5,
|
||||
"hp_loss_percent": 0.5,
|
||||
"drop_prob": 0.5,
|
||||
"cd_years": 1,
|
||||
"open_prob": 1.0,
|
||||
},
|
||||
{
|
||||
"id": "domain_high",
|
||||
"name": "High Realm Domain",
|
||||
"desc": "For experts",
|
||||
"max_realm": "Core Formation",
|
||||
"danger_prob": 0.0, # Safe
|
||||
"hp_loss_percent": 0.0,
|
||||
"drop_prob": 0.0,
|
||||
"cd_years": 1,
|
||||
"open_prob": 0.0, # Disabled by prob
|
||||
}
|
||||
]
|
||||
|
||||
@pytest.fixture
|
||||
def hidden_domain(mock_domain_config):
|
||||
"""Instance of HiddenDomain with mocked config."""
|
||||
# Patch game_configs dict directly
|
||||
with patch.dict("src.utils.df.game_configs", {"hidden_domain": mock_domain_config}):
|
||||
domain = HiddenDomain()
|
||||
# Clear static state
|
||||
HiddenDomain._domain_states.clear()
|
||||
yield domain
|
||||
HiddenDomain._domain_states.clear()
|
||||
|
||||
def test_load_configs(hidden_domain):
|
||||
"""Test that configs are loaded correctly from df."""
|
||||
configs = hidden_domain._load_configs()
|
||||
assert len(configs) == 2
|
||||
|
||||
c1 = configs[0]
|
||||
assert c1.id == "domain_low"
|
||||
assert c1.max_realm == Realm.Qi_Refinement
|
||||
assert c1.danger_prob == 0.5
|
||||
|
||||
c2 = configs[1]
|
||||
assert c2.id == "domain_high"
|
||||
assert c2.max_realm == Realm.Core_Formation
|
||||
|
||||
def test_is_start_basic(hidden_domain, base_world):
|
||||
"""Test start condition logic."""
|
||||
# Initial state: Year 1. CD is 1 year.
|
||||
# domain_low: open_prob 1.0
|
||||
# domain_high: open_prob 0.0
|
||||
|
||||
# By default, last_open is -999. Year 1 - (-999) >= 1.
|
||||
|
||||
# Mock random to ensure domain_low opens (though prob is 1.0, good to be safe)
|
||||
# and domain_high stays closed (prob 0.0)
|
||||
with patch("random.random", return_value=0.5):
|
||||
is_started = hidden_domain.is_start(base_world)
|
||||
|
||||
assert is_started is True
|
||||
assert len(hidden_domain._active_domains) == 1
|
||||
assert hidden_domain._active_domains[0].id == "domain_low"
|
||||
|
||||
# Check that state was updated
|
||||
assert HiddenDomain._domain_states["domain_low"] == 1
|
||||
|
||||
def test_is_start_cd_check(hidden_domain, base_world):
|
||||
"""Test that CD prevents opening."""
|
||||
# Mark domain_low as just opened in Year 1
|
||||
HiddenDomain._domain_states["domain_low"] = 1
|
||||
|
||||
# Current world year is 1. Diff is 0. CD is 1. 0 < 1, so shouldn't open.
|
||||
is_started = hidden_domain.is_start(base_world)
|
||||
|
||||
assert is_started is False
|
||||
assert len(hidden_domain._active_domains) == 0
|
||||
|
||||
def test_get_info_formatting(hidden_domain, base_world):
|
||||
"""Test the formatted info string matches the new multi-line format."""
|
||||
# Force activate domain_low
|
||||
configs = hidden_domain._load_configs()
|
||||
hidden_domain._active_domains = [configs[0]] # Low Realm Domain
|
||||
|
||||
info = hidden_domain.get_info(base_world)
|
||||
|
||||
# Expected: "Hidden Domain Low Realm Domain opened! Entry restricted to Qi Refinement and below."
|
||||
# Note: Using 'in' because the exact localized string might vary slightly in tests,
|
||||
# but we look for key parts.
|
||||
assert "Low Realm Domain" in info
|
||||
assert str(Realm.Qi_Refinement) in info
|
||||
# Ensure no "Hidden Domains opened:" prefix if possible, but difficult to test "absence" strictly
|
||||
# without exact string match.
|
||||
# Let's rely on checking the content is roughly correct.
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_entry_restriction(hidden_domain, base_world, dummy_avatar):
|
||||
"""Test that only eligible avatars enter."""
|
||||
# domain_low limits to Qi Refinement.
|
||||
|
||||
# Setup domains
|
||||
configs = hidden_domain._load_configs()
|
||||
hidden_domain._active_domains = [configs[0]]
|
||||
|
||||
# Avatar 1: Qi Refinement (Eligible)
|
||||
dummy_avatar.cultivation_progress.realm = Realm.Qi_Refinement
|
||||
|
||||
# Avatar 2: Foundation Establishment (Too high)
|
||||
av2 = MagicMock(spec=dummy_avatar)
|
||||
av2.id = 1002
|
||||
av2.name = "StrongGuy"
|
||||
av2.cultivation_progress.realm = Realm.Foundation_Establishment
|
||||
av2.personas = []
|
||||
|
||||
# Mock avatar manager
|
||||
base_world.avatar_manager.get_living_avatars = MagicMock(return_value=[dummy_avatar, av2])
|
||||
|
||||
# Execute
|
||||
# Mock process_single_domain internals or random to avoid side effects?
|
||||
# Actually, let's just inspect who gets processed.
|
||||
# Since _process_single_domain does the filtering, we can test that method or spy on it.
|
||||
# But integration testing execute() is better.
|
||||
|
||||
# We need to mock random to avoid death or loot for now
|
||||
with patch("random.random", return_value=0.9): # > danger(0.5) and > drop(0.5) -> Nothing happens
|
||||
events = await hidden_domain.execute(base_world)
|
||||
|
||||
# We expect 1 event for opening
|
||||
# And maybe 0 events for interaction if nothing happened.
|
||||
# But we want to ensure only dummy_avatar was considered.
|
||||
# Let's check logic by spying on logic inside loop?
|
||||
# Hard to spy local var.
|
||||
|
||||
# Alternative: Set danger to 0, drop to 1.0.
|
||||
# Eligible avatar gets loot => Event generated.
|
||||
# Ineligible avatar gets nothing => No event.
|
||||
configs[0].drop_prob = 1.0
|
||||
configs[0].danger_prob = 0.0
|
||||
|
||||
# Mock _generate_loot to return a dummy item
|
||||
mock_item = MagicMock(spec=Item)
|
||||
mock_item.name = "TestTreasure"
|
||||
hidden_domain._generate_loot = MagicMock(return_value=mock_item)
|
||||
|
||||
# Mock story generation to return nothing
|
||||
hidden_domain._generate_story = AsyncMock(return_value=None)
|
||||
|
||||
events = await hidden_domain.execute(base_world)
|
||||
|
||||
# Events should include:
|
||||
# 1. Opening event
|
||||
# 2. Loot event for dummy_avatar
|
||||
# 3. (No event for StrongGuy)
|
||||
|
||||
event_texts = [e.content for e in events]
|
||||
|
||||
# Check opening event
|
||||
assert any("Low Realm Domain" in t for t in event_texts)
|
||||
|
||||
# Check loot event for eligible avatar
|
||||
# Since tests run in zh-CN (forced by fixture), we check for Chinese text
|
||||
# "found a treasure" -> "觅得宝物"
|
||||
assert any("觅得宝物" in t for t in event_texts)
|
||||
assert any(dummy_avatar.name in t for t in event_texts)
|
||||
|
||||
# Check NO event for ineligible avatar
|
||||
assert not any(av2.name in t for t in event_texts)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_danger_death(hidden_domain, base_world, dummy_avatar):
|
||||
"""Test death logic in hidden domain."""
|
||||
# Setup domain
|
||||
configs = hidden_domain._load_configs()
|
||||
domain = configs[0] # Low Realm
|
||||
domain.danger_prob = 1.0 # Certain danger
|
||||
domain.hp_loss_percent = 2.0 # Instant kill (>100% HP)
|
||||
hidden_domain._active_domains = [domain]
|
||||
|
||||
dummy_avatar.cultivation_progress.realm = Realm.Qi_Refinement
|
||||
base_world.avatar_manager.get_living_avatars = MagicMock(return_value=[dummy_avatar])
|
||||
|
||||
# Mock handle_death to avoid complex world logic
|
||||
with patch("src.classes.gathering.hidden_domain.handle_death") as mock_death:
|
||||
events = await hidden_domain.execute(base_world)
|
||||
|
||||
# Verify death handler called
|
||||
mock_death.assert_called_once()
|
||||
args, _ = mock_death.call_args
|
||||
# args[0] is world, args[1] is avatar, args[2] is reason
|
||||
assert args[1] == dummy_avatar
|
||||
assert args[2].death_type == DeathType.HIDDEN_DOMAIN
|
||||
|
||||
# Verify event log
|
||||
event_texts = [e.content for e in events]
|
||||
# "perished" -> "葬身于"
|
||||
assert any("葬身于" in t for t in event_texts)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_loot_drop(hidden_domain, base_world, dummy_avatar):
|
||||
"""Test loot drop logic."""
|
||||
# Setup domain
|
||||
configs = hidden_domain._load_configs()
|
||||
domain = configs[0]
|
||||
domain.danger_prob = 0.0
|
||||
domain.drop_prob = 1.0
|
||||
hidden_domain._active_domains = [domain]
|
||||
|
||||
dummy_avatar.cultivation_progress.realm = Realm.Qi_Refinement
|
||||
base_world.avatar_manager.get_living_avatars = MagicMock(return_value=[dummy_avatar])
|
||||
|
||||
# Mock generation to return a specific item type to trigger specific logic
|
||||
# Let's use Weapon
|
||||
from src.classes.weapon import Weapon
|
||||
mock_weapon = MagicMock(spec=Weapon)
|
||||
mock_weapon.name = "GodSlayer"
|
||||
|
||||
hidden_domain._generate_loot = MagicMock(return_value=mock_weapon)
|
||||
dummy_avatar.change_weapon = MagicMock()
|
||||
|
||||
# Execute
|
||||
events = await hidden_domain.execute(base_world)
|
||||
|
||||
# Check loot generation called with next realm
|
||||
# Current: Qi Refinement -> Next: Foundation Establishment
|
||||
hidden_domain._generate_loot.assert_called_with(dummy_avatar, Realm.Foundation_Establishment)
|
||||
|
||||
# Check weapon equipped
|
||||
dummy_avatar.change_weapon.assert_called_with(mock_weapon)
|
||||
|
||||
# Check event
|
||||
event_texts = [e.content for e in events]
|
||||
assert any("GodSlayer" in t for t in event_texts)
|
||||
Reference in New Issue
Block a user