mud/tests/test_command_craft.py
Jared Miller 11636e073a
Add craft and recipes commands
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.
2026-02-14 17:58:59 -05:00

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()