mud/tests/test_targeting.py

335 lines
12 KiB
Python

"""Tests for target resolution system."""
from mudlib.entity import Mob
from mudlib.targeting import (
find_entity_on_tile,
find_in_inventory,
find_thing_on_tile,
parse_target,
resolve_target,
)
from mudlib.thing import Thing
class TestParseTarget:
"""Test ordinal prefix parsing."""
def test_plain_name(self):
"""Plain name returns ordinal 1."""
ordinal, name = parse_target("goblin")
assert ordinal == 1
assert name == "goblin"
def test_ordinal_prefix(self):
"""Parse '2.goblin' into (2, 'goblin')."""
ordinal, name = parse_target("2.goblin")
assert ordinal == 2
assert name == "goblin"
def test_first_ordinal(self):
"""Parse '1.sword' into (1, 'sword')."""
ordinal, name = parse_target("1.sword")
assert ordinal == 1
assert name == "sword"
def test_large_ordinal(self):
"""Parse large ordinal numbers."""
ordinal, name = parse_target("10.rat")
assert ordinal == 10
assert name == "rat"
def test_zero_ordinal_invalid(self):
"""Zero ordinal is invalid, treat as plain name."""
ordinal, name = parse_target("0.sword")
assert ordinal == 1
assert name == "0.sword"
def test_negative_ordinal_invalid(self):
"""Negative ordinal is invalid, treat as plain name."""
ordinal, name = parse_target("-1.sword")
assert ordinal == 1
assert name == "-1.sword"
class TestResolveTarget:
"""Test target resolution with name matching."""
def test_exact_match(self):
"""Find exact name match."""
candidates = [Thing(name="sword"), Thing(name="shield")]
result = resolve_target("sword", candidates)
assert result is candidates[0]
def test_exact_match_case_insensitive(self):
"""Exact match is case-insensitive."""
candidates = [Thing(name="Sword")]
result = resolve_target("sword", candidates)
assert result is candidates[0]
def test_prefix_match(self):
"""Find by name prefix."""
candidates = [Thing(name="longsword")]
result = resolve_target("long", candidates)
assert result is candidates[0]
def test_prefix_match_case_insensitive(self):
"""Prefix match is case-insensitive."""
candidates = [Thing(name="LongSword")]
result = resolve_target("long", candidates)
assert result is candidates[0]
def test_alias_exact_match(self):
"""Find by exact alias match."""
candidates = [Thing(name="longsword", aliases=["blade", "weapon"])]
result = resolve_target("blade", candidates)
assert result is candidates[0]
def test_alias_prefix_match(self):
"""Find by alias prefix."""
candidates = [Thing(name="longsword", aliases=["greatsword"])]
result = resolve_target("great", candidates)
assert result is candidates[0]
def test_ordinal_disambiguation(self):
"""Use ordinal to select Nth match."""
candidates = [
Thing(name="goblin"),
Thing(name="goblin"),
Thing(name="goblin"),
]
result = resolve_target("2.goblin", candidates)
assert result is candidates[1]
def test_ordinal_out_of_range(self):
"""Ordinal beyond available matches returns None."""
candidates = [Thing(name="goblin")]
result = resolve_target("2.goblin", candidates)
assert result is None
def test_no_match(self):
"""No matching candidates returns None."""
candidates = [Thing(name="sword")]
result = resolve_target("shield", candidates)
assert result is None
def test_custom_key_function(self):
"""Use custom key function to extract name."""
class CustomObject:
def __init__(self, label):
self.label = label
candidates = [CustomObject("sword"), CustomObject("shield")]
result = resolve_target("sword", candidates, key=lambda obj: obj.label)
assert result is candidates[0]
def test_object_without_aliases(self):
"""Handle objects without aliases attribute."""
class SimpleObject:
def __init__(self, name):
self.name = name
candidates = [SimpleObject("sword")]
result = resolve_target("sword", candidates)
assert result is candidates[0]
def test_exact_match_preferred_over_prefix(self):
"""Exact match takes precedence over prefix."""
candidates = [Thing(name="sword"), Thing(name="swordfish")]
result = resolve_target("sword", candidates)
assert result is candidates[0]
def test_ordinal_with_prefix_match(self):
"""Ordinal works with prefix matching."""
candidates = [
Thing(name="longsword"),
Thing(name="longbow"),
]
result = resolve_target("2.long", candidates)
assert result is candidates[1]
class TestFindEntityOnTile:
"""Test finding entities on the same tile."""
def test_find_nearby_player(self, player, nearby_player, test_zone):
"""Find another player on same tile."""
player.location = test_zone
nearby_player.location = test_zone
result = find_entity_on_tile("vegeta", player)
assert result is nearby_player
def test_find_mob_on_tile(self, player, test_zone):
"""Find mob on same tile."""
player.location = test_zone
goblin = Mob(name="goblin", x=0, y=0, alive=True)
goblin.location = test_zone
test_zone._contents.append(goblin)
result = find_entity_on_tile("goblin", player)
assert result is goblin
def test_skip_dead_mobs(self, player, test_zone):
"""Skip dead mobs."""
player.location = test_zone
dead_goblin = Mob(name="goblin", x=0, y=0, alive=False)
dead_goblin.location = test_zone
test_zone._contents.append(dead_goblin)
result = find_entity_on_tile("goblin", player)
assert result is None
def test_skip_self(self, player, test_zone):
"""Don't return the player themselves."""
player.location = test_zone
result = find_entity_on_tile("goku", player)
assert result is None
def test_z_axis_check_both_grounded(self, player, nearby_player, test_zone):
"""Find entity when both grounded."""
player.location = test_zone
nearby_player.location = test_zone
player.flying = False
nearby_player.flying = False
result = find_entity_on_tile("vegeta", player)
assert result is nearby_player
def test_z_axis_check_both_flying(self, player, nearby_player, test_zone):
"""Find entity when both flying."""
player.location = test_zone
nearby_player.location = test_zone
player.flying = True
nearby_player.flying = True
result = find_entity_on_tile("vegeta", player)
assert result is nearby_player
def test_z_axis_check_mismatch(self, player, nearby_player, test_zone):
"""Don't find entity on different z-axis."""
player.location = test_zone
nearby_player.location = test_zone
player.flying = False
nearby_player.flying = True
result = find_entity_on_tile("vegeta", player)
assert result is None
def test_ordinal_with_multiple_entities(self, player, test_zone):
"""Use ordinal to select specific entity."""
player.location = test_zone
goblin1 = Mob(name="goblin", x=0, y=0, alive=True)
goblin2 = Mob(name="goblin", x=0, y=0, alive=True)
goblin1.location = test_zone
test_zone._contents.append(goblin1)
goblin2.location = test_zone
test_zone._contents.append(goblin2)
result = find_entity_on_tile("2.goblin", player)
assert result is goblin2
def test_prefix_match_entity(self, player, test_zone):
"""Find entity by prefix."""
player.location = test_zone
goblin = Mob(name="goblin", x=0, y=0, alive=True)
goblin.location = test_zone
test_zone._contents.append(goblin)
result = find_entity_on_tile("gob", player)
assert result is goblin
def test_no_match(self, player, test_zone):
"""Return None when no match."""
player.location = test_zone
result = find_entity_on_tile("dragon", player)
assert result is None
class TestFindThingOnTile:
"""Test finding Things on the ground."""
def test_find_by_name(self, test_zone):
"""Find thing by exact name."""
sword = Thing(name="sword", x=5, y=5)
sword.location = test_zone
test_zone._contents.append(sword)
result = find_thing_on_tile("sword", test_zone, 5, 5)
assert result is sword
def test_find_by_alias(self, test_zone):
"""Find thing by alias."""
sword = Thing(name="longsword", aliases=["blade"], x=5, y=5)
sword.location = test_zone
test_zone._contents.append(sword)
result = find_thing_on_tile("blade", test_zone, 5, 5)
assert result is sword
def test_prefix_match(self, test_zone):
"""Find thing by name prefix."""
sword = Thing(name="longsword", x=5, y=5)
sword.location = test_zone
test_zone._contents.append(sword)
result = find_thing_on_tile("long", test_zone, 5, 5)
assert result is sword
def test_ordinal(self, test_zone):
"""Use ordinal to select specific thing."""
rock1 = Thing(name="rock", x=5, y=5)
rock2 = Thing(name="rock", x=5, y=5)
rock1.location = test_zone
test_zone._contents.append(rock1)
rock2.location = test_zone
test_zone._contents.append(rock2)
result = find_thing_on_tile("2.rock", test_zone, 5, 5)
assert result is rock2
def test_skip_non_things(self, test_zone):
"""Only match Thing instances, skip entities."""
mob = Mob(name="sword", x=5, y=5, alive=True)
mob.location = test_zone
test_zone._contents.append(mob)
result = find_thing_on_tile("sword", test_zone, 5, 5)
assert result is None
def test_no_match(self, test_zone):
"""Return None when no match."""
result = find_thing_on_tile("shield", test_zone, 5, 5)
assert result is None
class TestFindInInventory:
"""Test finding Things in player inventory."""
def test_find_by_name(self, player):
"""Find item by exact name."""
sword = Thing(name="sword")
sword.location = player
player._contents.append(sword)
result = find_in_inventory("sword", player)
assert result is sword
def test_find_by_alias(self, player):
"""Find item by alias."""
sword = Thing(name="longsword", aliases=["blade"])
sword.location = player
player._contents.append(sword)
result = find_in_inventory("blade", player)
assert result is sword
def test_prefix_match(self, player):
"""Find item by name prefix."""
sword = Thing(name="longsword")
sword.location = player
player._contents.append(sword)
result = find_in_inventory("long", player)
assert result is sword
def test_ordinal(self, player):
"""Use ordinal to select specific item."""
potion1 = Thing(name="potion")
potion2 = Thing(name="potion")
potion1.location = player
player._contents.append(potion1)
potion2.location = player
player._contents.append(potion2)
result = find_in_inventory("2.potion", player)
assert result is potion2
def test_no_match(self, player):
"""Return None when no match."""
result = find_in_inventory("shield", player)
assert result is None