From 085a19a5647a01e762bccd31e8490a32d6706fcc Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Sat, 14 Feb 2026 11:22:08 -0500 Subject: [PATCH] Add score command with stats display Shows PL, stamina, K/D ratio, time played, and unlocked moves. Registered as score/stats/profile, available in all modes. --- src/mudlib/commands/score.py | 58 +++++++++++++++++++++ tests/test_score_command.py | 99 ++++++++++++++++++++++++++++++++++++ 2 files changed, 157 insertions(+) create mode 100644 src/mudlib/commands/score.py create mode 100644 tests/test_score_command.py diff --git a/src/mudlib/commands/score.py b/src/mudlib/commands/score.py new file mode 100644 index 0000000..a87e277 --- /dev/null +++ b/src/mudlib/commands/score.py @@ -0,0 +1,58 @@ +"""Score/stats/profile command.""" + +import time + +from mudlib.commands import CommandDefinition, register +from mudlib.player import Player + + +def format_play_time(seconds: float) -> str: + """Format play time as human-readable string.""" + hours = int(seconds // 3600) + minutes = int((seconds % 3600) // 60) + secs = int(seconds % 60) + parts = [] + if hours: + parts.append(f"{hours}h") + if minutes: + parts.append(f"{minutes}m") + parts.append(f"{secs}s") + return " ".join(parts) + + +async def cmd_score(player: Player, _args: str) -> None: + """Display character sheet with stats.""" + # Calculate K/D ratio + kd_ratio = f"{player.kills / player.deaths:.1f}" if player.deaths > 0 else "N/A" + + # Format play time (accumulate current session first) + total_seconds = player.play_time_seconds + if player.session_start > 0: + total_seconds += time.monotonic() - player.session_start + play_time = format_play_time(total_seconds) + + lines = [ + f"--- {player.name} ---", + f"PL: {player.pl:.0f}/{player.max_pl:.0f}", + f"Stamina: {player.stamina:.0f}/{player.max_stamina:.0f}", + f"Kills: {player.kills} Deaths: {player.deaths} K/D: {kd_ratio}", + f"Time played: {play_time}", + ] + + if player.unlocked_moves: + moves = ", ".join(sorted(player.unlocked_moves)) + lines.append(f"Unlocked moves: {moves}") + + output = "\r\n".join(lines) + "\r\n" + await player.send(output) + + +register( + CommandDefinition( + name="score", + handler=cmd_score, + aliases=["stats", "profile"], + mode="*", + help="Display your character stats and progression", + ) +) diff --git a/tests/test_score_command.py b/tests/test_score_command.py new file mode 100644 index 0000000..6e823ea --- /dev/null +++ b/tests/test_score_command.py @@ -0,0 +1,99 @@ +"""Tests for the score/stats/profile command.""" + +import pytest + +from mudlib.commands.score import cmd_score + + +@pytest.mark.asyncio +async def test_score_shows_player_name(player, mock_writer): + """Score command displays the player's name.""" + player.name = "Goku" + await cmd_score(player, "") + + messages = [call[0][0] for call in player.writer.write.call_args_list] + output = "".join(messages) + assert "Goku" in output + + +@pytest.mark.asyncio +async def test_score_shows_pl_and_stamina(player, mock_writer): + """Score command displays PL and stamina gauges.""" + player.pl = 75 + player.max_pl = 100 + player.stamina = 80 + player.max_stamina = 100 + await cmd_score(player, "") + + messages = [call[0][0] for call in player.writer.write.call_args_list] + output = "".join(messages) + assert "75" in output + assert "100" in output + assert "80" in output + + +@pytest.mark.asyncio +async def test_score_shows_kill_death_stats(player, mock_writer): + """Score command displays kills, deaths, and K/D ratio.""" + player.kills = 10 + player.deaths = 2 + await cmd_score(player, "") + + messages = [call[0][0] for call in player.writer.write.call_args_list] + output = "".join(messages) + assert "10" in output # kills + assert "2" in output # deaths + assert "5.0" in output # K/D ratio + + +@pytest.mark.asyncio +async def test_score_shows_kd_ratio_na_with_zero_deaths(player, mock_writer): + """Score command shows N/A for K/D ratio when deaths is zero.""" + player.kills = 5 + player.deaths = 0 + await cmd_score(player, "") + + messages = [call[0][0] for call in player.writer.write.call_args_list] + output = "".join(messages) + assert "N/A" in output + + +@pytest.mark.asyncio +async def test_score_shows_time_played(player, mock_writer): + """Score command formats and displays play time.""" + player.play_time_seconds = 3661 # 1h 1m 1s + player.session_start = 0 # No active session + await cmd_score(player, "") + + messages = [call[0][0] for call in player.writer.write.call_args_list] + output = "".join(messages) + assert "1h" in output + assert "1m" in output + assert "1s" in output + + +@pytest.mark.asyncio +async def test_score_shows_unlocked_moves(player, mock_writer): + """Score command displays unlocked moves.""" + player.unlocked_moves = {"roundhouse", "sweep"} + await cmd_score(player, "") + + messages = [call[0][0] for call in player.writer.write.call_args_list] + output = "".join(messages) + assert "roundhouse" in output + assert "sweep" in output + + +@pytest.mark.asyncio +async def test_command_registered_with_aliases(player, mock_writer): + """Score command is accessible via score, stats, and profile.""" + from mudlib.commands import CommandDefinition, resolve_prefix + + score_cmd = resolve_prefix("score") + stats_cmd = resolve_prefix("stats") + profile_cmd = resolve_prefix("profile") + + assert isinstance(score_cmd, CommandDefinition) + assert isinstance(stats_cmd, CommandDefinition) + assert isinstance(profile_cmd, CommandDefinition) + assert score_cmd.handler == stats_cmd.handler == profile_cmd.handler