Wire kill/death tracking into combat engine

Increment player.kills and player.mob_kills on mob defeat,
player.deaths on player defeat. Session time accumulation
via accumulate_play_time helper.
This commit is contained in:
Jared Miller 2026-02-14 11:20:13 -05:00
parent a398227814
commit e31af53577
Signed by: shmup
GPG key ID: 22B5C6D66A38B06C
4 changed files with 184 additions and 6 deletions

View file

@ -84,6 +84,8 @@ async def process_combat() -> None:
This should be called each game loop tick to advance combat state machines.
"""
from mudlib.player import Player
now = time.monotonic()
for encounter in active_encounters[:]: # Copy list to allow modification
@ -92,8 +94,6 @@ async def process_combat() -> None:
await encounter.attacker.send("Combat has fizzled out.\r\n")
await encounter.defender.send("Combat has fizzled out.\r\n")
from mudlib.player import Player
for entity in (encounter.attacker, encounter.defender):
if isinstance(entity, Player) and entity.mode == "combat":
entity.mode_stack.pop()
@ -149,8 +149,6 @@ async def process_combat() -> None:
await viewer.send(msg + "\r\n")
# Send vitals update after damage resolution
from mudlib.player import Player
for entity in (encounter.attacker, encounter.defender):
if isinstance(entity, Player):
send_char_vitals(entity)
@ -168,6 +166,17 @@ async def process_combat() -> None:
loser = encounter.attacker
winner = encounter.defender
# Track kill/death stats
if isinstance(winner, Player):
winner.kills += 1
if isinstance(loser, Mob):
winner.mob_kills[loser.name] = (
winner.mob_kills.get(loser.name, 0) + 1
)
if isinstance(loser, Player):
loser.deaths += 1
# Despawn mob losers, send victory/defeat messages
if isinstance(loser, Mob):
from mudlib.corpse import create_corpse
@ -191,8 +200,6 @@ async def process_combat() -> None:
)
# Pop combat mode from both entities if they're Players
from mudlib.player import Player
attacker = encounter.attacker
if isinstance(attacker, Player) and attacker.mode == "combat":
attacker.mode_stack.pop()

View file

@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
import time
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any
@ -90,5 +91,16 @@ class Player(Entity):
)
def accumulate_play_time(player: Player) -> None:
"""Accumulate play time since session start and reset timer.
Args:
player: The player to update
"""
if player.session_start > 0:
player.play_time_seconds += time.monotonic() - player.session_start
player.session_start = time.monotonic()
# Global registry of connected players
players: dict[str, Player] = {}

View file

@ -226,6 +226,11 @@ def save_player(player: Player) -> None:
Args:
player: Player instance to save
"""
# Accumulate play time before saving
from mudlib.player import accumulate_play_time
accumulate_play_time(player)
# Serialize inventory as JSON list of thing names
inventory_names = [obj.name for obj in player.contents if isinstance(obj, Thing)]
inventory_json = json.dumps(inventory_names)

154
tests/test_kill_tracking.py Normal file
View file

@ -0,0 +1,154 @@
"""Tests for kill and death tracking in combat."""
import time
import pytest
from mudlib.combat.engine import (
process_combat,
start_encounter,
)
from mudlib.combat.moves import CombatMove
from mudlib.entity import Mob
from mudlib.player import accumulate_play_time
@pytest.fixture
def punch_move():
"""Create a basic punch move for testing."""
return CombatMove(
name="punch right",
move_type="attack",
stamina_cost=5.0,
timing_window_ms=800,
damage_pct=0.15,
countered_by=[],
resolve_hit="{attacker} hits {defender}!",
resolve_miss="{defender} dodges!",
announce="{attacker} punches!",
)
@pytest.mark.asyncio
async def test_player_kills_mob_increments_stats(player, test_zone, punch_move):
"""Player kills mob -> kills incremented, mob_kills tracked."""
# Create a goblin mob
goblin = Mob(name="goblin", x=0, y=0)
goblin.location = test_zone
test_zone._contents.append(goblin)
# Start encounter
encounter = start_encounter(player, goblin)
# Execute attack
encounter.attack(punch_move)
# Advance past telegraph (0.3s) + window (0.8s)
encounter.tick(time.monotonic() + 0.31) # -> WINDOW
encounter.tick(time.monotonic() + 1.2) # -> RESOLVE
# Set defender to very low pl so damage kills them
goblin.pl = 1.0
# Process combat (this will resolve and end encounter)
await process_combat()
# Verify stats
assert player.kills == 1
assert player.mob_kills["goblin"] == 1
@pytest.mark.asyncio
async def test_player_killed_by_mob_increments_deaths(player, test_zone, punch_move):
"""Player killed by mob -> deaths incremented."""
# Create a goblin mob
goblin = Mob(name="goblin", x=0, y=0)
goblin.location = test_zone
test_zone._contents.append(goblin)
# Start encounter with mob as attacker
encounter = start_encounter(goblin, player)
# Execute attack
encounter.attack(punch_move)
# Advance to RESOLVE
encounter.tick(time.monotonic() + 0.31)
encounter.tick(time.monotonic() + 1.2)
# Set player to low pl so they die
player.pl = 1.0
# Process combat
await process_combat()
# Verify deaths incremented
assert player.deaths == 1
@pytest.mark.asyncio
async def test_multiple_kills_accumulate(player, test_zone, punch_move):
"""After killing 3 goblins, player.kills == 3, player.mob_kills["goblin"] == 3."""
for _ in range(3):
# Create goblin
goblin = Mob(name="goblin", x=0, y=0)
goblin.location = test_zone
test_zone._contents.append(goblin)
# Create and resolve encounter
encounter = start_encounter(player, goblin)
encounter.attack(punch_move)
encounter.tick(time.monotonic() + 0.31)
encounter.tick(time.monotonic() + 1.2)
goblin.pl = 1.0
await process_combat()
# Verify accumulated kills
assert player.kills == 3
assert player.mob_kills["goblin"] == 3
def test_session_time_tracking(player):
"""Session time tracking accumulates correctly."""
# Set session start to a known time
start_time = time.monotonic()
player.session_start = start_time
# Simulate 5 seconds passing
time.sleep(0.01) # Small real delay to ensure monotonic() advances
player.session_start = start_time # Reset for predictable test
# Mock time to be 5 seconds later
import unittest.mock
with unittest.mock.patch("time.monotonic", return_value=start_time + 5.0):
accumulate_play_time(player)
# Should have accumulated 5 seconds
assert player.play_time_seconds == 5.0
# Session start should be reset to current time
with unittest.mock.patch("time.monotonic", return_value=start_time + 5.0):
assert player.session_start == start_time + 5.0
def test_accumulate_play_time_multiple_sessions(player):
"""Multiple accumulation calls should add up correctly."""
start_time = time.monotonic()
player.session_start = start_time
import unittest.mock
# First accumulation: 3 seconds
with unittest.mock.patch("time.monotonic", return_value=start_time + 3.0):
accumulate_play_time(player)
assert player.play_time_seconds == 3.0
# Second accumulation: 2 more seconds (from reset point)
with unittest.mock.patch("time.monotonic", return_value=start_time + 5.0):
accumulate_play_time(player)
# Should have 3 + 2 = 5 total
assert player.play_time_seconds == 5.0