mud/tests/test_stamina_cue_wiring.py
Jared Miller be63a1cbde
Extract shared test fixtures to conftest.py
Moved common test fixtures (mock_writer, mock_reader, test_zone, player,
nearby_player, clear_state) from individual test files into a shared
conftest.py. This eliminates duplication across test_power.py, test_sleep.py,
test_combat_zaxis.py, test_quit.py, test_stamina_cues.py, and
test_stamina_cue_wiring.py.

Some test files override specific fixtures where they need custom behavior
(e.g., test_quit.py adds a close method to mock_writer, stamina tests use
smaller zones and custom player positions).
2026-02-14 01:00:37 -05:00

282 lines
8.4 KiB
Python

"""Tests for stamina cue system wiring and reset behavior."""
import asyncio
import contextlib
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from mudlib.player import Player
from mudlib.zone import Zone
@pytest.fixture
def test_zone():
"""Override conftest test_zone with smaller zone for stamina tests."""
return Zone(name="test", width=10, height=10)
@pytest.fixture
def player(mock_writer, test_zone):
"""Override conftest player with custom position and stats for stamina tests."""
p = Player(
name="Goku",
x=5,
y=5,
pl=100.0,
stamina=100.0,
max_stamina=100.0,
writer=mock_writer,
)
p.move_to(test_zone, x=5, y=5)
return p
@pytest.fixture
def defender(test_zone):
"""Defender fixture with custom position for stamina tests."""
w = MagicMock()
w.write = MagicMock()
w.drain = AsyncMock()
p = Player(
name="Vegeta",
x=5,
y=5,
pl=100.0,
stamina=100.0,
max_stamina=100.0,
writer=w,
)
p.move_to(test_zone, x=5, y=5)
return p
@pytest.mark.asyncio
async def test_stamina_cue_after_combat_damage(player, defender):
"""check_stamina_cues is called after damage in combat resolution."""
import time
from mudlib.combat.encounter import CombatState
from mudlib.combat.engine import active_encounters, start_encounter
from mudlib.combat.moves import CombatMove
# Clear encounters
active_encounters.clear()
# Create a move that deals damage
move = CombatMove(
name="weak punch",
command="weak",
move_type="attack",
damage_pct=0.5,
stamina_cost=5.0,
timing_window_ms=100,
)
# Start encounter
encounter = start_encounter(player, defender)
encounter.attack(move)
# Fast-forward to RESOLVE state
encounter.state = CombatState.RESOLVE
encounter.move_started_at = time.monotonic()
# Patch check_stamina_cues to verify it's called
with patch("mudlib.combat.engine.check_stamina_cues") as mock_check:
from mudlib.combat.engine import process_combat
await process_combat()
# Should be called for both entities after damage
assert mock_check.called
calls = [call[0][0] for call in mock_check.call_args_list]
assert player in calls
assert defender in calls
@pytest.mark.asyncio
async def test_stamina_cue_after_power_up_deduction(player):
"""check_stamina_cues is called during power-up stamina deduction."""
from mudlib.commands.power import power_up_loop
player.stamina = 100.0
player.max_stamina = 100.0
player.pl = 10.0
player.max_pl = 50.0
with patch("mudlib.commands.power.check_stamina_cues") as mock_check:
# Run power-up for a short time
task = asyncio.create_task(power_up_loop(player, target_pl=20.0))
await asyncio.sleep(0.15) # Let a few ticks happen
task.cancel()
with contextlib.suppress(asyncio.CancelledError):
await task
# check_stamina_cues should have been called at least once
assert mock_check.called
# Verify player was the argument
calls = [call[0][0] for call in mock_check.call_args_list]
assert player in calls
@pytest.mark.asyncio
async def test_stamina_cue_after_attack_cost(player, defender):
"""check_stamina_cues is called after attack stamina cost deduction."""
from mudlib.combat.commands import do_attack
from mudlib.combat.engine import active_encounters
from mudlib.combat.moves import CombatMove
from mudlib.player import players
# Clear encounters
active_encounters.clear()
# Register defender in global players dict so do_attack can find it
players["Vegeta"] = defender
try:
# Create a move with high stamina cost
move = CombatMove(
name="power punch",
command="power",
move_type="attack",
damage_pct=0.3,
stamina_cost=60.0,
timing_window_ms=1000,
)
player.stamina = 100.0
player.max_stamina = 100.0
with patch("mudlib.combat.commands.check_stamina_cues") as mock_check:
await do_attack(player, "Vegeta", move)
# check_stamina_cues should have been called
assert mock_check.called
# Verify player was the argument
calls = [call[0][0] for call in mock_check.call_args_list]
assert player in calls
finally:
players.clear()
@pytest.mark.asyncio
async def test_stamina_cue_after_defense_cost(player):
"""check_stamina_cues is called after defense stamina cost deduction."""
from mudlib.combat.commands import do_defend
from mudlib.combat.moves import CombatMove
# Create a defense move with stamina cost
move = CombatMove(
name="duck",
command="duck",
move_type="defense",
stamina_cost=30.0,
timing_window_ms=100,
)
player.stamina = 100.0
player.max_stamina = 100.0
with patch("mudlib.combat.commands.check_stamina_cues") as mock_check:
# Use a short timing window to avoid blocking too long
move.timing_window_ms = 10
await do_defend(player, "", move)
# check_stamina_cues should have been called
assert mock_check.called
# Verify player was the argument
calls = [call[0][0] for call in mock_check.call_args_list]
assert player in calls
@pytest.mark.asyncio
async def test_stamina_cue_reset_on_resting_recovery(player):
"""_last_stamina_cue resets to 1.0 when stamina fully recovers via resting."""
from mudlib.combat.stamina import check_stamina_cues
from mudlib.player import players
from mudlib.resting import process_resting
# Register player in global dict so process_resting can find them
players[player.name] = player
try:
# Drop stamina to trigger a cue
player.stamina = 20.0
player.max_stamina = 100.0
await check_stamina_cues(player)
assert player._last_stamina_cue == 0.25 # Below 25% threshold
# Start resting and set stamina very close to max
player.resting = True
player.stamina = 99.85 # Will be clamped to 100.0 after one tick
# Process one tick to reach max
await process_resting()
# Stamina should be at max and _last_stamina_cue should reset
assert player.stamina == 100.0
assert player._last_stamina_cue == 1.0
finally:
players.clear()
@pytest.mark.asyncio
async def test_stamina_cue_reset_on_unconscious_recovery(player):
"""_last_stamina_cue resets to 1.0 when stamina recovers from unconsciousness."""
from mudlib.combat.stamina import check_stamina_cues
from mudlib.player import players
from mudlib.unconscious import process_unconscious
# Register player in global dict so process_unconscious can find them
players[player.name] = player
try:
# Drop stamina to trigger a cue
player.stamina = 5.0
player.max_stamina = 100.0
await check_stamina_cues(player)
assert player._last_stamina_cue == 0.10 # Below 10% threshold
# Drop to unconscious
player.stamina = 0.0
player.pl = 0.0
# Verify player is unconscious
assert player.posture == "unconscious"
# Process recovery tick to regain consciousness
await process_unconscious()
# Should have regained consciousness and reset cue
assert player.stamina > 0
assert player.pl > 0
assert player._last_stamina_cue == 1.0
finally:
players.clear()
@pytest.mark.asyncio
async def test_cue_fires_again_after_reset(player):
"""After reset, cues fire again on next descent."""
from mudlib.combat.stamina import check_stamina_cues
player.max_stamina = 100.0
# First descent: trigger 75% threshold
player.stamina = 70.0
player.writer.write.reset_mock()
await check_stamina_cues(player)
first_call_count = player.writer.write.call_count
assert first_call_count > 0
assert player._last_stamina_cue == 0.75
# Recover to max and reset
player.stamina = 100.0
player._last_stamina_cue = 1.0
# Second descent: should fire again at 75%
player.stamina = 70.0
player.writer.write.reset_mock()
await check_stamina_cues(player)
second_call_count = player.writer.write.call_count
assert second_call_count > 0 # Should fire again