335 lines
12 KiB
Python
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
|