"""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_move_missing_hit_time(tmp_path): """Test loading move without hit_time_ms defaults to 0.""" toml_content = """ name = "test" move_type = "attack" stamina_cost = 5.0 """ toml_file = tmp_path / "bad.toml" toml_file.write_text(toml_content) moves = load_move(toml_file) assert len(moves) == 1 assert moves[0].hit_time_ms == 0 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