"""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!", timing_window_ms=800, damage_pct=0.15, countered_by=["dodge left", "parry high"], ) 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.timing_window_ms == 800 assert move.damage_pct == 0.15 assert move.countered_by == ["dodge left", "parry high"] assert move.handler is None def test_combat_move_minimal(): """Test CombatMove with minimal required fields.""" move = CombatMove( name="test move", move_type="attack", stamina_cost=10.0, timing_window_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.timing_window_ms == 500 assert move.damage_pct == 0.0 assert move.countered_by == [] def test_load_move_from_toml(tmp_path): """Test loading a combat move from TOML file.""" toml_content = """ name = "punch right" aliases = ["pr"] move_type = "attack" stamina_cost = 5.0 telegraph = "{attacker} winds up a right hook!" timing_window_ms = 800 damage_pct = 0.15 countered_by = ["dodge left", "parry high"] """ toml_file = tmp_path / "punch_right.toml" toml_file.write_text(toml_content) move = load_move(toml_file) 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.timing_window_ms == 800 assert move.damage_pct == 0.15 assert move.countered_by == ["dodge left", "parry high"] 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 timing_window_ms = 600 """ toml_file = tmp_path / "basic.toml" toml_file.write_text(toml_content) move = load_move(toml_file) 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 timing_window_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" timing_window_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_timing_window(tmp_path): """Test loading move without timing_window_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="missing required field.*timing_window_ms"): load_move(toml_file) def test_load_moves_from_directory(tmp_path): """Test loading all moves from a directory.""" # Create multiple TOML files punch_toml = tmp_path / "punch_right.toml" punch_toml.write_text( """ name = "punch right" aliases = ["pr"] move_type = "attack" stamina_cost = 5.0 telegraph = "{attacker} winds up a right hook!" timing_window_ms = 800 damage_pct = 0.15 countered_by = ["dodge left", "parry high"] """ ) dodge_toml = tmp_path / "dodge_left.toml" dodge_toml.write_text( """ name = "dodge left" aliases = ["dl"] move_type = "defense" stamina_cost = 3.0 telegraph = "" timing_window_ms = 500 damage_pct = 0.0 countered_by = [] """ ) # 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 both names and all aliases assert "punch right" in moves assert "pr" in moves assert "dodge left" in moves assert "dl" in moves # All aliases should point to the same object assert moves["punch right"] is moves["pr"] assert moves["dodge left"] is moves["dl"] # Check move properties assert moves["punch right"].name == "punch right" assert moves["dodge left"].name == "dodge left" 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 timing_window_ms = 800 """ ) move2_toml = tmp_path / "move2.toml" move2_toml.write_text( """ name = "move two" aliases = ["m"] move_type = "defense" stamina_cost = 3.0 timing_window_ms = 500 """ ) 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 timing_window_ms = 800 """ ) move2_toml = tmp_path / "move2.toml" move2_toml.write_text( """ name = "punch" move_type = "attack" stamina_cost = 5.0 timing_window_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 # Create a move with an invalid counter reference punch_toml = tmp_path / "punch_right.toml" punch_toml.write_text( """ name = "punch right" move_type = "attack" stamina_cost = 5.0 timing_window_ms = 800 damage_pct = 0.15 countered_by = ["dodge left", "nonexistent move"] """ ) dodge_toml = tmp_path / "dodge_left.toml" dodge_toml.write_text( """ name = "dodge left" move_type = "defense" stamina_cost = 3.0 timing_window_ms = 500 """ ) 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 # Create moves with valid counter references punch_toml = tmp_path / "punch_right.toml" punch_toml.write_text( """ name = "punch right" move_type = "attack" stamina_cost = 5.0 timing_window_ms = 800 damage_pct = 0.15 countered_by = ["dodge left", "parry high"] """ ) dodge_toml = tmp_path / "dodge_left.toml" dodge_toml.write_text( """ name = "dodge left" move_type = "defense" stamina_cost = 3.0 timing_window_ms = 500 """ ) parry_toml = tmp_path / "parry_high.toml" parry_toml.write_text( """ name = "parry high" move_type = "defense" stamina_cost = 3.0 timing_window_ms = 500 """ ) 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