563 lines
14 KiB
Python
563 lines
14 KiB
Python
"""Tests for combat move definitions and TOML loading."""
|
|
|
|
import pytest
|
|
|
|
from mudlib.combat.moves import CombatMove, load_move, load_moves
|
|
|
|
|
|
def test_combat_move_dataclass():
|
|
"""Test CombatMove can be instantiated with all fields."""
|
|
move = CombatMove(
|
|
name="punch right",
|
|
move_type="attack",
|
|
aliases=["pr"],
|
|
stamina_cost=5.0,
|
|
telegraph="{attacker} winds up a right hook!",
|
|
hit_time_ms=800,
|
|
damage_pct=0.15,
|
|
countered_by=["dodge left", "parry high"],
|
|
command="punch",
|
|
variant="right",
|
|
)
|
|
assert move.name == "punch right"
|
|
assert move.move_type == "attack"
|
|
assert move.aliases == ["pr"]
|
|
assert move.stamina_cost == 5.0
|
|
assert move.telegraph == "{attacker} winds up a right hook!"
|
|
assert move.hit_time_ms == 800
|
|
assert move.damage_pct == 0.15
|
|
assert move.countered_by == ["dodge left", "parry high"]
|
|
assert move.handler is None
|
|
assert move.command == "punch"
|
|
assert move.variant == "right"
|
|
|
|
|
|
def test_combat_move_minimal():
|
|
"""Test CombatMove with minimal required fields."""
|
|
move = CombatMove(
|
|
name="test move",
|
|
move_type="attack",
|
|
stamina_cost=10.0,
|
|
hit_time_ms=500,
|
|
)
|
|
assert move.name == "test move"
|
|
assert move.move_type == "attack"
|
|
assert move.aliases == []
|
|
assert move.stamina_cost == 10.0
|
|
assert move.telegraph == ""
|
|
assert move.hit_time_ms == 500
|
|
assert move.damage_pct == 0.0
|
|
assert move.countered_by == []
|
|
assert move.command == ""
|
|
assert move.variant == ""
|
|
|
|
|
|
def test_load_simple_move_from_toml(tmp_path):
|
|
"""Test loading a simple combat move from TOML file."""
|
|
toml_content = """
|
|
name = "roundhouse"
|
|
aliases = ["rh"]
|
|
move_type = "attack"
|
|
stamina_cost = 8.0
|
|
telegraph = "{attacker} spins into a roundhouse kick!"
|
|
hit_time_ms = 600
|
|
damage_pct = 0.25
|
|
countered_by = ["duck", "parry high", "parry low"]
|
|
"""
|
|
toml_file = tmp_path / "roundhouse.toml"
|
|
toml_file.write_text(toml_content)
|
|
|
|
moves = load_move(toml_file)
|
|
assert len(moves) == 1
|
|
move = moves[0]
|
|
assert move.name == "roundhouse"
|
|
assert move.command == "roundhouse"
|
|
assert move.variant == ""
|
|
assert move.aliases == ["rh"]
|
|
assert move.damage_pct == 0.25
|
|
|
|
|
|
def test_load_variant_move_from_toml(tmp_path):
|
|
"""Test loading a variant combat move from TOML file."""
|
|
toml_content = """
|
|
name = "punch"
|
|
move_type = "attack"
|
|
stamina_cost = 5.0
|
|
hit_time_ms = 800
|
|
damage_pct = 0.15
|
|
|
|
[variants.left]
|
|
aliases = ["pl"]
|
|
telegraph = "{attacker} winds up a left hook!"
|
|
countered_by = ["dodge right", "parry high"]
|
|
|
|
[variants.right]
|
|
aliases = ["pr"]
|
|
telegraph = "{attacker} winds up a right hook!"
|
|
countered_by = ["dodge left", "parry high"]
|
|
"""
|
|
toml_file = tmp_path / "punch.toml"
|
|
toml_file.write_text(toml_content)
|
|
|
|
moves = load_move(toml_file)
|
|
assert len(moves) == 2
|
|
|
|
by_name = {m.name: m for m in moves}
|
|
assert "punch left" in by_name
|
|
assert "punch right" in by_name
|
|
|
|
left = by_name["punch left"]
|
|
assert left.command == "punch"
|
|
assert left.variant == "left"
|
|
assert left.aliases == ["pl"]
|
|
assert left.telegraph == "{attacker} winds up a left hook!"
|
|
assert left.countered_by == ["dodge right", "parry high"]
|
|
# Inherited from parent
|
|
assert left.stamina_cost == 5.0
|
|
assert left.hit_time_ms == 800
|
|
assert left.damage_pct == 0.15
|
|
|
|
right = by_name["punch right"]
|
|
assert right.command == "punch"
|
|
assert right.variant == "right"
|
|
assert right.aliases == ["pr"]
|
|
assert right.countered_by == ["dodge left", "parry high"]
|
|
|
|
|
|
def test_variant_inherits_shared_properties(tmp_path):
|
|
"""Test that variants inherit shared properties and can override them."""
|
|
toml_content = """
|
|
name = "kick"
|
|
move_type = "attack"
|
|
stamina_cost = 5.0
|
|
hit_time_ms = 800
|
|
damage_pct = 0.10
|
|
|
|
[variants.low]
|
|
aliases = ["kl"]
|
|
damage_pct = 0.08
|
|
hit_time_ms = 600
|
|
|
|
[variants.high]
|
|
aliases = ["kh"]
|
|
damage_pct = 0.15
|
|
"""
|
|
toml_file = tmp_path / "kick.toml"
|
|
toml_file.write_text(toml_content)
|
|
|
|
moves = load_move(toml_file)
|
|
by_name = {m.name: m for m in moves}
|
|
|
|
low = by_name["kick low"]
|
|
assert low.damage_pct == 0.08
|
|
assert low.hit_time_ms == 600 # overridden
|
|
assert low.stamina_cost == 5.0 # inherited
|
|
|
|
high = by_name["kick high"]
|
|
assert high.damage_pct == 0.15
|
|
assert high.hit_time_ms == 800 # inherited
|
|
assert high.stamina_cost == 5.0 # inherited
|
|
|
|
|
|
def test_load_move_with_defaults(tmp_path):
|
|
"""Test loading move with only required fields uses defaults."""
|
|
toml_content = """
|
|
name = "basic move"
|
|
move_type = "defense"
|
|
stamina_cost = 3.0
|
|
active_ms = 600
|
|
recovery_ms = 2700
|
|
"""
|
|
toml_file = tmp_path / "basic.toml"
|
|
toml_file.write_text(toml_content)
|
|
|
|
moves = load_move(toml_file)
|
|
assert len(moves) == 1
|
|
move = moves[0]
|
|
assert move.name == "basic move"
|
|
assert move.aliases == []
|
|
assert move.telegraph == ""
|
|
assert move.damage_pct == 0.0
|
|
assert move.countered_by == []
|
|
|
|
|
|
def test_load_move_missing_name(tmp_path):
|
|
"""Test loading move without name raises error."""
|
|
toml_content = """
|
|
move_type = "attack"
|
|
stamina_cost = 5.0
|
|
hit_time_ms = 800
|
|
"""
|
|
toml_file = tmp_path / "bad.toml"
|
|
toml_file.write_text(toml_content)
|
|
|
|
with pytest.raises(ValueError, match="missing required field.*name"):
|
|
load_move(toml_file)
|
|
|
|
|
|
def test_load_move_missing_move_type(tmp_path):
|
|
"""Test loading move without move_type raises error."""
|
|
toml_content = """
|
|
name = "test"
|
|
stamina_cost = 5.0
|
|
timing_window_ms = 800
|
|
"""
|
|
toml_file = tmp_path / "bad.toml"
|
|
toml_file.write_text(toml_content)
|
|
|
|
with pytest.raises(ValueError, match="missing required field.*move_type"):
|
|
load_move(toml_file)
|
|
|
|
|
|
def test_load_move_missing_stamina_cost(tmp_path):
|
|
"""Test loading move without stamina_cost raises error."""
|
|
toml_content = """
|
|
name = "test"
|
|
move_type = "attack"
|
|
hit_time_ms = 800
|
|
"""
|
|
toml_file = tmp_path / "bad.toml"
|
|
toml_file.write_text(toml_content)
|
|
|
|
with pytest.raises(ValueError, match="missing required field.*stamina_cost"):
|
|
load_move(toml_file)
|
|
|
|
|
|
def test_load_attack_missing_hit_time_raises(tmp_path):
|
|
"""Test loading attack without hit_time_ms raises error."""
|
|
toml_content = """
|
|
name = "test"
|
|
move_type = "attack"
|
|
stamina_cost = 5.0
|
|
"""
|
|
toml_file = tmp_path / "bad.toml"
|
|
toml_file.write_text(toml_content)
|
|
|
|
with pytest.raises(ValueError, match="hit_time_ms"):
|
|
load_move(toml_file)
|
|
|
|
|
|
def test_load_attack_zero_hit_time_raises(tmp_path):
|
|
"""Test loading attack with hit_time_ms = 0 raises error."""
|
|
toml_content = """
|
|
name = "test"
|
|
move_type = "attack"
|
|
stamina_cost = 5.0
|
|
hit_time_ms = 0
|
|
"""
|
|
toml_file = tmp_path / "bad.toml"
|
|
toml_file.write_text(toml_content)
|
|
|
|
with pytest.raises(ValueError, match="hit_time_ms"):
|
|
load_move(toml_file)
|
|
|
|
|
|
def test_load_defense_missing_active_ms_raises(tmp_path):
|
|
"""Test loading defense without active_ms raises error."""
|
|
toml_content = """
|
|
name = "test"
|
|
move_type = "defense"
|
|
stamina_cost = 3.0
|
|
recovery_ms = 2000
|
|
"""
|
|
toml_file = tmp_path / "bad.toml"
|
|
toml_file.write_text(toml_content)
|
|
|
|
with pytest.raises(ValueError, match="active_ms"):
|
|
load_move(toml_file)
|
|
|
|
|
|
def test_load_defense_zero_active_ms_raises(tmp_path):
|
|
"""Test loading defense with active_ms = 0 raises error."""
|
|
toml_content = """
|
|
name = "test"
|
|
move_type = "defense"
|
|
stamina_cost = 3.0
|
|
active_ms = 0
|
|
recovery_ms = 2000
|
|
"""
|
|
toml_file = tmp_path / "bad.toml"
|
|
toml_file.write_text(toml_content)
|
|
|
|
with pytest.raises(ValueError, match="active_ms"):
|
|
load_move(toml_file)
|
|
|
|
|
|
def test_load_attack_valid_passes(tmp_path):
|
|
"""Test loading attack with valid hit_time_ms passes."""
|
|
toml_content = """
|
|
name = "test"
|
|
move_type = "attack"
|
|
stamina_cost = 5.0
|
|
hit_time_ms = 500
|
|
"""
|
|
toml_file = tmp_path / "valid.toml"
|
|
toml_file.write_text(toml_content)
|
|
|
|
moves = load_move(toml_file)
|
|
assert len(moves) == 1
|
|
assert moves[0].hit_time_ms == 500
|
|
|
|
|
|
def test_load_defense_valid_passes(tmp_path):
|
|
"""Test loading defense with valid active_ms passes."""
|
|
toml_content = """
|
|
name = "test"
|
|
move_type = "defense"
|
|
stamina_cost = 3.0
|
|
active_ms = 500
|
|
recovery_ms = 2000
|
|
"""
|
|
toml_file = tmp_path / "valid.toml"
|
|
toml_file.write_text(toml_content)
|
|
|
|
moves = load_move(toml_file)
|
|
assert len(moves) == 1
|
|
assert moves[0].active_ms == 500
|
|
|
|
|
|
def test_load_moves_from_directory(tmp_path):
|
|
"""Test loading all moves from a directory."""
|
|
# Create a variant move
|
|
punch_toml = tmp_path / "punch.toml"
|
|
punch_toml.write_text(
|
|
"""
|
|
name = "punch"
|
|
move_type = "attack"
|
|
stamina_cost = 5.0
|
|
hit_time_ms = 800
|
|
damage_pct = 0.15
|
|
|
|
[variants.right]
|
|
aliases = ["pr"]
|
|
telegraph = "{attacker} winds up a right hook!"
|
|
countered_by = ["dodge left"]
|
|
"""
|
|
)
|
|
|
|
# Create a simple move
|
|
dodge_toml = tmp_path / "dodge.toml"
|
|
dodge_toml.write_text(
|
|
"""
|
|
name = "duck"
|
|
move_type = "defense"
|
|
stamina_cost = 3.0
|
|
active_ms = 500
|
|
recovery_ms = 2700
|
|
"""
|
|
)
|
|
|
|
# Create a non-TOML file that should be ignored
|
|
other_file = tmp_path / "readme.txt"
|
|
other_file.write_text("This should be ignored")
|
|
|
|
moves = load_moves(tmp_path)
|
|
|
|
# Should have entries for variant qualified names, aliases, and simple moves
|
|
assert "punch right" in moves
|
|
assert "pr" in moves
|
|
assert "duck" in moves
|
|
|
|
# Aliases should point to the same object
|
|
assert moves["punch right"] is moves["pr"]
|
|
|
|
# Check variant properties
|
|
assert moves["punch right"].command == "punch"
|
|
assert moves["punch right"].variant == "right"
|
|
assert moves["duck"].command == "duck"
|
|
assert moves["duck"].variant == ""
|
|
|
|
|
|
def test_load_moves_empty_directory(tmp_path):
|
|
"""Test loading from empty directory returns empty dict."""
|
|
moves = load_moves(tmp_path)
|
|
assert moves == {}
|
|
|
|
|
|
def test_load_moves_alias_collision(tmp_path):
|
|
"""Test that duplicate aliases are detected."""
|
|
# Create two moves with same alias
|
|
move1_toml = tmp_path / "move1.toml"
|
|
move1_toml.write_text(
|
|
"""
|
|
name = "move one"
|
|
aliases = ["m"]
|
|
move_type = "attack"
|
|
stamina_cost = 5.0
|
|
hit_time_ms = 800
|
|
"""
|
|
)
|
|
|
|
move2_toml = tmp_path / "move2.toml"
|
|
move2_toml.write_text(
|
|
"""
|
|
name = "move two"
|
|
aliases = ["m"]
|
|
move_type = "defense"
|
|
stamina_cost = 3.0
|
|
active_ms = 500
|
|
recovery_ms = 2700
|
|
"""
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="duplicate.*alias.*m"):
|
|
load_moves(tmp_path)
|
|
|
|
|
|
def test_load_moves_name_collision(tmp_path):
|
|
"""Test that duplicate move names are detected."""
|
|
move1_toml = tmp_path / "move1.toml"
|
|
move1_toml.write_text(
|
|
"""
|
|
name = "punch"
|
|
move_type = "attack"
|
|
stamina_cost = 5.0
|
|
hit_time_ms = 800
|
|
"""
|
|
)
|
|
|
|
move2_toml = tmp_path / "move2.toml"
|
|
move2_toml.write_text(
|
|
"""
|
|
name = "punch"
|
|
move_type = "attack"
|
|
stamina_cost = 5.0
|
|
hit_time_ms = 800
|
|
"""
|
|
)
|
|
|
|
with pytest.raises(ValueError, match="duplicate.*name.*punch"):
|
|
load_moves(tmp_path)
|
|
|
|
|
|
def test_load_moves_validates_countered_by_refs(tmp_path, caplog):
|
|
"""Test that invalid countered_by references log warnings."""
|
|
import logging
|
|
|
|
punch_toml = tmp_path / "punch.toml"
|
|
punch_toml.write_text(
|
|
"""
|
|
name = "punch"
|
|
move_type = "attack"
|
|
stamina_cost = 5.0
|
|
hit_time_ms = 800
|
|
damage_pct = 0.15
|
|
|
|
[variants.right]
|
|
countered_by = ["dodge left", "nonexistent move"]
|
|
"""
|
|
)
|
|
|
|
dodge_toml = tmp_path / "dodge.toml"
|
|
dodge_toml.write_text(
|
|
"""
|
|
name = "dodge"
|
|
move_type = "defense"
|
|
stamina_cost = 3.0
|
|
active_ms = 500
|
|
recovery_ms = 2700
|
|
|
|
[variants.left]
|
|
aliases = ["dl"]
|
|
"""
|
|
)
|
|
|
|
with caplog.at_level(logging.WARNING):
|
|
moves = load_moves(tmp_path)
|
|
|
|
# Should still load successfully
|
|
assert "punch right" in moves
|
|
assert "dodge left" in moves
|
|
|
|
# Should have logged a warning about the invalid reference
|
|
assert any("nonexistent move" in record.message for record in caplog.records)
|
|
assert any("punch right" in record.message for record in caplog.records)
|
|
|
|
|
|
def test_load_moves_valid_countered_by_refs_no_warning(tmp_path, caplog):
|
|
"""Test that valid countered_by references don't log warnings."""
|
|
import logging
|
|
|
|
punch_toml = tmp_path / "punch.toml"
|
|
punch_toml.write_text(
|
|
"""
|
|
name = "punch"
|
|
move_type = "attack"
|
|
stamina_cost = 5.0
|
|
hit_time_ms = 800
|
|
damage_pct = 0.15
|
|
|
|
[variants.right]
|
|
countered_by = ["dodge left", "parry high"]
|
|
"""
|
|
)
|
|
|
|
dodge_toml = tmp_path / "dodge.toml"
|
|
dodge_toml.write_text(
|
|
"""
|
|
name = "dodge"
|
|
move_type = "defense"
|
|
stamina_cost = 3.0
|
|
active_ms = 500
|
|
recovery_ms = 2700
|
|
|
|
[variants.left]
|
|
aliases = ["dl"]
|
|
"""
|
|
)
|
|
|
|
parry_toml = tmp_path / "parry.toml"
|
|
parry_toml.write_text(
|
|
"""
|
|
name = "parry"
|
|
move_type = "defense"
|
|
stamina_cost = 3.0
|
|
active_ms = 500
|
|
recovery_ms = 2700
|
|
|
|
[variants.high]
|
|
aliases = ["f"]
|
|
"""
|
|
)
|
|
|
|
with caplog.at_level(logging.WARNING):
|
|
moves = load_moves(tmp_path)
|
|
|
|
# Should load successfully
|
|
assert "punch right" in moves
|
|
assert "dodge left" in moves
|
|
assert "parry high" in moves
|
|
|
|
# Should have no warnings
|
|
assert len(caplog.records) == 0
|
|
|
|
|
|
def test_load_content_combat_directory():
|
|
"""Test loading the actual content/combat directory."""
|
|
from pathlib import Path
|
|
|
|
content_dir = Path(__file__).parent.parent / "content" / "combat"
|
|
moves = load_moves(content_dir)
|
|
|
|
# Verify variant moves have correct structure
|
|
assert "punch left" in moves
|
|
assert "punch right" in moves
|
|
assert moves["punch left"].command == "punch"
|
|
assert moves["punch left"].variant == "left"
|
|
|
|
assert "dodge left" in moves
|
|
assert "dodge right" in moves
|
|
assert moves["dodge left"].command == "dodge"
|
|
|
|
assert "parry high" in moves
|
|
assert "parry low" in moves
|
|
assert moves["parry high"].command == "parry"
|
|
|
|
# Verify simple moves
|
|
assert "roundhouse" in moves
|
|
assert moves["roundhouse"].command == "roundhouse"
|
|
assert moves["roundhouse"].variant == ""
|
|
|
|
assert "sweep" in moves
|
|
assert "duck" in moves
|
|
assert "jump" in moves
|