Implements craft command to create items from recipes by consuming ingredients from player inventory. Recipes command lists available recipes or shows details for a specific recipe. Registers commands and loads recipes at server startup.
242 lines
7.3 KiB
Python
242 lines
7.3 KiB
Python
"""Tests for crafting commands."""
|
|
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
|
|
import pytest
|
|
|
|
from mudlib.commands.crafting import cmd_craft, cmd_recipes
|
|
from mudlib.crafting import Recipe, recipes
|
|
from mudlib.player import Player
|
|
from mudlib.things import ThingTemplate, spawn_thing, thing_templates
|
|
from mudlib.zone import Zone
|
|
from mudlib.zones import register_zone, zone_registry
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _clean_registries():
|
|
"""Snapshot and restore registries to prevent test leakage."""
|
|
saved_zones = dict(zone_registry)
|
|
saved_templates = dict(thing_templates)
|
|
saved_recipes = dict(recipes)
|
|
zone_registry.clear()
|
|
thing_templates.clear()
|
|
recipes.clear()
|
|
yield
|
|
zone_registry.clear()
|
|
zone_registry.update(saved_zones)
|
|
thing_templates.clear()
|
|
thing_templates.update(saved_templates)
|
|
recipes.clear()
|
|
recipes.update(saved_recipes)
|
|
|
|
|
|
def _make_zone(name="overworld", width=20, height=20):
|
|
"""Create a test zone."""
|
|
terrain = [["." for _ in range(width)] for _ in range(height)]
|
|
zone = Zone(
|
|
name=name,
|
|
description=name,
|
|
width=width,
|
|
height=height,
|
|
terrain=terrain,
|
|
toroidal=True,
|
|
)
|
|
register_zone(name, zone)
|
|
return zone
|
|
|
|
|
|
def _make_player(name="tester", zone=None, x=5, y=5):
|
|
"""Create a test player with mock writer."""
|
|
mock_writer = MagicMock()
|
|
mock_writer.write = MagicMock()
|
|
mock_writer.drain = AsyncMock()
|
|
return Player(name=name, location=zone, x=x, y=y, writer=mock_writer)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_craft_success():
|
|
"""Craft with ingredients: consumed, result in inventory."""
|
|
zone = _make_zone()
|
|
player = _make_player(zone=zone)
|
|
|
|
# Register templates
|
|
plank_template = ThingTemplate(name="plank", description="A wooden plank")
|
|
nail_template = ThingTemplate(name="nail", description="A small nail")
|
|
table_template = ThingTemplate(name="table", description="A sturdy table")
|
|
thing_templates["plank"] = plank_template
|
|
thing_templates["nail"] = nail_template
|
|
thing_templates["table"] = table_template
|
|
|
|
# Add ingredients to player inventory
|
|
plank1 = spawn_thing(plank_template, player)
|
|
plank2 = spawn_thing(plank_template, player)
|
|
nail1 = spawn_thing(nail_template, player)
|
|
|
|
# Register recipe
|
|
recipe = Recipe(
|
|
name="wooden_table",
|
|
description="Craft a table",
|
|
ingredients=["plank", "plank", "nail"],
|
|
result="table",
|
|
)
|
|
recipes["wooden_table"] = recipe
|
|
|
|
# Craft the item
|
|
await cmd_craft(player, "wooden_table")
|
|
|
|
# Check success message was sent
|
|
assert player.writer.write.called
|
|
output = "".join(call.args[0] for call in player.writer.write.call_args_list)
|
|
assert "table" in output.lower()
|
|
|
|
# Check ingredients were consumed
|
|
inventory = player.contents
|
|
assert plank1 not in inventory
|
|
assert plank2 not in inventory
|
|
assert nail1 not in inventory
|
|
|
|
# Check result was added to inventory
|
|
table_in_inventory = [obj for obj in inventory if obj.name == "table"]
|
|
assert len(table_in_inventory) == 1
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_craft_missing_ingredients():
|
|
"""Error message listing what's missing."""
|
|
zone = _make_zone()
|
|
player = _make_player(zone=zone)
|
|
|
|
# Register templates
|
|
plank_template = ThingTemplate(name="plank", description="A wooden plank")
|
|
table_template = ThingTemplate(name="table", description="A sturdy table")
|
|
thing_templates["plank"] = plank_template
|
|
thing_templates["table"] = table_template
|
|
|
|
# Add only one plank (recipe needs two)
|
|
spawn_thing(plank_template, player)
|
|
|
|
# Register recipe needing two planks
|
|
recipe = Recipe(
|
|
name="wooden_table",
|
|
description="Craft a table",
|
|
ingredients=["plank", "plank"],
|
|
result="table",
|
|
)
|
|
recipes["wooden_table"] = recipe
|
|
|
|
# Try to craft
|
|
await cmd_craft(player, "wooden_table")
|
|
|
|
# Check error message
|
|
assert player.writer.write.called
|
|
output = "".join(call.args[0] for call in player.writer.write.call_args_list)
|
|
assert "missing" in output.lower() or "need" in output.lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_craft_unknown_recipe():
|
|
"""Error for nonexistent recipe."""
|
|
zone = _make_zone()
|
|
player = _make_player(zone=zone)
|
|
|
|
await cmd_craft(player, "nonexistent_recipe")
|
|
|
|
assert player.writer.write.called
|
|
output = "".join(call.args[0] for call in player.writer.write.call_args_list)
|
|
assert "unknown" in output.lower() or "not found" in output.lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_craft_no_args():
|
|
"""Error with usage when no args provided."""
|
|
zone = _make_zone()
|
|
player = _make_player(zone=zone)
|
|
|
|
await cmd_craft(player, "")
|
|
|
|
assert player.writer.write.called
|
|
output = "".join(call.args[0] for call in player.writer.write.call_args_list)
|
|
assert "usage" in output.lower() or "craft" in output.lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_craft_unknown_result_template():
|
|
"""Recipe result template not in thing_templates."""
|
|
zone = _make_zone()
|
|
player = _make_player(zone=zone)
|
|
|
|
# Register template for ingredient
|
|
plank_template = ThingTemplate(name="plank", description="A wooden plank")
|
|
thing_templates["plank"] = plank_template
|
|
|
|
# Add ingredient to inventory
|
|
spawn_thing(plank_template, player)
|
|
|
|
# Register recipe with unknown result template
|
|
recipe = Recipe(
|
|
name="broken_recipe",
|
|
description="This recipe has a broken result",
|
|
ingredients=["plank"],
|
|
result="unknown_item", # This template doesn't exist
|
|
)
|
|
recipes["broken_recipe"] = recipe
|
|
|
|
# Try to craft
|
|
await cmd_craft(player, "broken_recipe")
|
|
|
|
# Check error message
|
|
assert player.writer.write.called
|
|
output = "".join(call.args[0] for call in player.writer.write.call_args_list)
|
|
assert "error" in output.lower() or "unknown" in output.lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_recipes_list():
|
|
"""Shows available recipes."""
|
|
zone = _make_zone()
|
|
player = _make_player(zone=zone)
|
|
|
|
# Register some recipes
|
|
recipes["wooden_table"] = Recipe(
|
|
name="wooden_table",
|
|
description="Craft a table",
|
|
ingredients=["plank", "plank"],
|
|
result="table",
|
|
)
|
|
recipes["wooden_chair"] = Recipe(
|
|
name="wooden_chair",
|
|
description="Craft a chair",
|
|
ingredients=["plank"],
|
|
result="chair",
|
|
)
|
|
|
|
await cmd_recipes(player, "")
|
|
|
|
assert player.writer.write.called
|
|
output = "".join(call.args[0] for call in player.writer.write.call_args_list)
|
|
assert "wooden_table" in output.lower()
|
|
assert "wooden_chair" in output.lower()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_recipes_detail():
|
|
"""Shows specific recipe details."""
|
|
zone = _make_zone()
|
|
player = _make_player(zone=zone)
|
|
|
|
# Register a recipe
|
|
recipes["wooden_table"] = Recipe(
|
|
name="wooden_table",
|
|
description="Craft a sturdy table",
|
|
ingredients=["plank", "plank", "nail", "nail"],
|
|
result="table",
|
|
)
|
|
|
|
await cmd_recipes(player, "wooden_table")
|
|
|
|
assert player.writer.write.called
|
|
output = "".join(call.args[0] for call in player.writer.write.call_args_list)
|
|
assert "wooden_table" in output.lower()
|
|
assert "plank" in output.lower()
|
|
assert "nail" in output.lower()
|
|
assert "table" in output.lower()
|