247 lines
9.0 KiB
Python
247 lines
9.0 KiB
Python
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)
|