rom24-quickmud-python 1.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. mud/__init__.py +0 -0
  2. mud/__main__.py +40 -0
  3. mud/account/__init__.py +20 -0
  4. mud/account/account_manager.py +62 -0
  5. mud/account/account_service.py +80 -0
  6. mud/advancement.py +62 -0
  7. mud/affects/saves.py +123 -0
  8. mud/agent/__init__.py +0 -0
  9. mud/agent/agent_protocol.py +19 -0
  10. mud/agent/character_agent.py +61 -0
  11. mud/combat/__init__.py +3 -0
  12. mud/combat/engine.py +189 -0
  13. mud/commands/__init__.py +3 -0
  14. mud/commands/admin_commands.py +77 -0
  15. mud/commands/advancement.py +36 -0
  16. mud/commands/alias_cmds.py +44 -0
  17. mud/commands/build.py +18 -0
  18. mud/commands/combat.py +16 -0
  19. mud/commands/communication.py +55 -0
  20. mud/commands/decorators.py +11 -0
  21. mud/commands/dispatcher.py +206 -0
  22. mud/commands/healer.py +81 -0
  23. mud/commands/help.py +14 -0
  24. mud/commands/imc.py +19 -0
  25. mud/commands/inspection.py +113 -0
  26. mud/commands/inventory.py +42 -0
  27. mud/commands/movement.py +71 -0
  28. mud/commands/notes.py +44 -0
  29. mud/commands/shop.py +138 -0
  30. mud/commands/socials.py +34 -0
  31. mud/config.py +59 -0
  32. mud/db/__init__.py +0 -0
  33. mud/db/init.py +27 -0
  34. mud/db/migrate_from_files.py +87 -0
  35. mud/db/migrations.py +7 -0
  36. mud/db/models.py +98 -0
  37. mud/db/seed.py +28 -0
  38. mud/db/session.py +11 -0
  39. mud/devtools/__init__.py +0 -0
  40. mud/devtools/agent_demo.py +19 -0
  41. mud/entrypoint.py +34 -0
  42. mud/game_loop.py +117 -0
  43. mud/imc/__init__.py +17 -0
  44. mud/imc/protocol.py +32 -0
  45. mud/loaders/__init__.py +27 -0
  46. mud/loaders/area_loader.py +73 -0
  47. mud/loaders/base_loader.py +38 -0
  48. mud/loaders/help_loader.py +17 -0
  49. mud/loaders/json_area_loader.py +203 -0
  50. mud/loaders/json_loader.py +285 -0
  51. mud/loaders/mob_loader.py +104 -0
  52. mud/loaders/obj_loader.py +76 -0
  53. mud/loaders/reset_loader.py +29 -0
  54. mud/loaders/room_loader.py +63 -0
  55. mud/loaders/shop_loader.py +41 -0
  56. mud/loaders/social_loader.py +16 -0
  57. mud/loaders/specials_loader.py +63 -0
  58. mud/logging/__init__.py +0 -0
  59. mud/logging/admin.py +40 -0
  60. mud/logging/agent_trace.py +9 -0
  61. mud/math/c_compat.py +27 -0
  62. mud/mobprog.py +72 -0
  63. mud/models/__init__.py +106 -0
  64. mud/models/area.py +33 -0
  65. mud/models/area_json.py +27 -0
  66. mud/models/board.py +49 -0
  67. mud/models/board_json.py +16 -0
  68. mud/models/character.py +195 -0
  69. mud/models/character_json.py +46 -0
  70. mud/models/constants.py +423 -0
  71. mud/models/conversion.py +45 -0
  72. mud/models/help.py +28 -0
  73. mud/models/help_json.py +14 -0
  74. mud/models/json_io.py +64 -0
  75. mud/models/mob.py +82 -0
  76. mud/models/note.py +29 -0
  77. mud/models/note_json.py +16 -0
  78. mud/models/obj.py +82 -0
  79. mud/models/object.py +28 -0
  80. mud/models/object_json.py +40 -0
  81. mud/models/player_json.py +29 -0
  82. mud/models/room.py +86 -0
  83. mud/models/room_json.py +46 -0
  84. mud/models/shop.py +21 -0
  85. mud/models/shop_json.py +17 -0
  86. mud/models/skill.py +25 -0
  87. mud/models/skill_json.py +20 -0
  88. mud/models/social.py +78 -0
  89. mud/models/social_json.py +20 -0
  90. mud/net/__init__.py +9 -0
  91. mud/net/ansi.py +27 -0
  92. mud/net/connection.py +174 -0
  93. mud/net/protocol.py +57 -0
  94. mud/net/session.py +21 -0
  95. mud/net/telnet_server.py +31 -0
  96. mud/network/__init__.py +0 -0
  97. mud/network/websocket_server.py +83 -0
  98. mud/network/websocket_session.py +21 -0
  99. mud/notes.py +45 -0
  100. mud/persistence.py +185 -0
  101. mud/registry.py +5 -0
  102. mud/scripts/convert_are_to_json.py +162 -0
  103. mud/scripts/convert_help_are_to_json.py +92 -0
  104. mud/scripts/convert_player_to_json.py +112 -0
  105. mud/scripts/convert_shops_to_json.py +64 -0
  106. mud/scripts/convert_skills_to_json.py +166 -0
  107. mud/scripts/convert_social_are_to_json.py +92 -0
  108. mud/scripts/load_test_data.py +17 -0
  109. mud/security/__init__.py +0 -0
  110. mud/security/bans.py +112 -0
  111. mud/security/hash_utils.py +20 -0
  112. mud/server.py +8 -0
  113. mud/skills/__init__.py +3 -0
  114. mud/skills/handlers.py +795 -0
  115. mud/skills/registry.py +97 -0
  116. mud/spawning/__init__.py +1 -0
  117. mud/spawning/mob_spawner.py +13 -0
  118. mud/spawning/obj_spawner.py +18 -0
  119. mud/spawning/reset_handler.py +222 -0
  120. mud/spawning/templates.py +63 -0
  121. mud/spec_funs.py +57 -0
  122. mud/time.py +48 -0
  123. mud/utils/rng_mm.py +123 -0
  124. mud/wiznet.py +74 -0
  125. mud/world/__init__.py +11 -0
  126. mud/world/linking.py +31 -0
  127. mud/world/look.py +29 -0
  128. mud/world/movement.py +135 -0
  129. mud/world/world_state.py +179 -0
  130. rom24_quickmud_python-1.2.2.dist-info/METADATA +236 -0
  131. rom24_quickmud_python-1.2.2.dist-info/RECORD +135 -0
  132. rom24_quickmud_python-1.2.2.dist-info/WHEEL +5 -0
  133. rom24_quickmud_python-1.2.2.dist-info/entry_points.txt +2 -0
  134. rom24_quickmud_python-1.2.2.dist-info/licenses/LICENSE +21 -0
  135. rom24_quickmud_python-1.2.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,77 @@
1
+ from mud.models.character import Character
2
+ from mud.registry import room_registry
3
+ from mud.spawning.mob_spawner import spawn_mob
4
+ from mud.net.session import SESSIONS
5
+ from mud.security import bans
6
+
7
+
8
+ def cmd_who(char: Character, args: str) -> str:
9
+ lines = ["Online Players:"]
10
+ for sess in SESSIONS.values():
11
+ c = sess.character
12
+ room_vnum = c.room.vnum if getattr(c, "room", None) else "?"
13
+ lines.append(f" - {c.name} in room {room_vnum}")
14
+ return "\n".join(lines)
15
+
16
+
17
+ def cmd_teleport(char: Character, args: str) -> str:
18
+ if not args.isdigit() or int(args) not in room_registry:
19
+ return "Invalid room."
20
+ target = room_registry[int(args)]
21
+ if char.room:
22
+ char.room.remove_character(char)
23
+ target.add_character(char)
24
+ return f"Teleported to room {args}"
25
+
26
+
27
+ def cmd_spawn(char: Character, args: str) -> str:
28
+ if not args.isdigit():
29
+ return "Invalid vnum."
30
+ mob = spawn_mob(int(args))
31
+ if not mob:
32
+ return "NPC not found."
33
+ if not char.room:
34
+ return "Nowhere to spawn."
35
+ char.room.add_mob(mob)
36
+ return f"Spawned {mob.name}."
37
+
38
+
39
+ def cmd_ban(char: Character, args: str) -> str:
40
+ host = args.strip()
41
+ if not host:
42
+ return "Usage: ban <host>"
43
+ bans.add_banned_host(host)
44
+ try:
45
+ bans.save_bans_file()
46
+ except Exception:
47
+ # Persistence errors shouldn't block the action in tests
48
+ pass
49
+ return f"Banned {host}."
50
+
51
+
52
+ def cmd_unban(char: Character, args: str) -> str:
53
+ host = args.strip()
54
+ if not host:
55
+ return "Usage: unban <host>"
56
+ if not bans.is_host_banned(host):
57
+ return "Site is not banned."
58
+ bans.remove_banned_host(host)
59
+ try:
60
+ bans.save_bans_file()
61
+ except Exception:
62
+ pass
63
+ return f"Unbanned {host}."
64
+
65
+
66
+ def cmd_banlist(char: Character, args: str) -> str:
67
+ banned = sorted(list({h for h in list_hosts() for h in [h]}))
68
+ if not banned:
69
+ return "No sites banned."
70
+ lines = ["Banned sites:"] + [f" - {h}" for h in banned]
71
+ return "\n".join(lines)
72
+
73
+
74
+ def list_hosts() -> list[str]:
75
+ # internal helper to read via saving/loading outward if needed later
76
+ # currently directly exposes in-memory set
77
+ return sorted({*bans._banned_hosts}) # type: ignore[attr-defined]
@@ -0,0 +1,36 @@
1
+ from mud.models.character import Character
2
+ from mud.skills.registry import skill_registry
3
+
4
+
5
+ def do_practice(char: Character, args: str) -> str:
6
+ if not args:
7
+ return f"You have {char.practice} practice sessions left."
8
+ if char.practice <= 0:
9
+ return "You have no practice sessions left."
10
+ skill_name = args.lower()
11
+ if skill_name not in skill_registry.skills:
12
+ return "You can't practice that."
13
+ current = char.skills.get(skill_name, 0)
14
+ if current >= 75:
15
+ return f"You are already learned at {skill_name}."
16
+ char.practice -= 1
17
+ char.skills[skill_name] = min(current + 25, 75)
18
+ return f"You practice {skill_name}."
19
+
20
+
21
+ def do_train(char: Character, args: str) -> str:
22
+ if not args:
23
+ return f"You have {char.train} training sessions left."
24
+ if char.train <= 0:
25
+ return "You have no training sessions left."
26
+ stat = args.lower()
27
+ if stat not in {"hp", "mana", "move"}:
28
+ return "Train what?"
29
+ if stat == "hp":
30
+ char.max_hit += 10
31
+ elif stat == "mana":
32
+ char.max_mana += 10
33
+ else:
34
+ char.max_move += 10
35
+ char.train -= 1
36
+ return f"You train your {stat}."
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Dict
4
+
5
+ from mud.models.character import Character
6
+
7
+
8
+ def _format_aliases(aliases: Dict[str, str]) -> str:
9
+ if not aliases:
10
+ return "No aliases defined."
11
+ parts = [f"{k} -> {v}" for k, v in sorted(aliases.items())]
12
+ return "Aliases: " + ", ".join(parts)
13
+
14
+
15
+ def do_alias(char: Character, args: str = "") -> str:
16
+ """Create or list aliases.
17
+
18
+ Usage:
19
+ alias # list
20
+ alias <name> <exp> # set/replace
21
+ """
22
+ if not args.strip():
23
+ return _format_aliases(char.aliases)
24
+ parts = args.split(maxsplit=1)
25
+ if len(parts) < 2:
26
+ return "Usage: alias <name> <expansion>"
27
+ name, expansion = parts[0].strip(), parts[1].strip()
28
+ # Prevent self-referential aliases by simple guard.
29
+ if name.lower() in ("alias", "unalias"):
30
+ return "You cannot alias that command."
31
+ char.aliases[name] = expansion
32
+ return f"Alias set: {name} -> {expansion}"
33
+
34
+
35
+ def do_unalias(char: Character, args: str = "") -> str:
36
+ """Remove an alias by name."""
37
+ name = args.strip()
38
+ if not name:
39
+ return "Usage: unalias <name>"
40
+ if name in char.aliases:
41
+ del char.aliases[name]
42
+ return f"Removed alias: {name}"
43
+ return "No such alias."
44
+
mud/commands/build.py ADDED
@@ -0,0 +1,18 @@
1
+ from mud.models.character import Character
2
+
3
+
4
+ def cmd_redit(char: Character, args: str) -> str:
5
+ """Edit the current room's fields."""
6
+ if not char.room:
7
+ return "You are nowhere."
8
+ parts = args.split(maxsplit=1)
9
+ if len(parts) != 2:
10
+ return "Usage: @redit name|desc <value>"
11
+ field, value = parts
12
+ if field == "name":
13
+ char.room.name = value
14
+ return f"Room name set to {value}"
15
+ if field in {"desc", "description"}:
16
+ char.room.description = value
17
+ return "Room description updated."
18
+ return "Invalid field."
mud/commands/combat.py ADDED
@@ -0,0 +1,16 @@
1
+ from mud.models.character import Character
2
+ from mud.combat import attack_round
3
+
4
+
5
+ def do_kill(char: Character, args: str) -> str:
6
+ if not args:
7
+ return "Kill whom?"
8
+ target_name = args.lower()
9
+ if not getattr(char, "room", None):
10
+ return "You are nowhere."
11
+ for victim in list(char.room.people):
12
+ if victim is char:
13
+ continue
14
+ if victim.name and target_name in victim.name.lower():
15
+ return attack_round(char, victim)
16
+ return "They aren't here."
@@ -0,0 +1,55 @@
1
+ from __future__ import annotations
2
+ import asyncio
3
+
4
+ from mud.models.character import Character, character_registry
5
+ from mud.net.protocol import broadcast_room, broadcast_global, send_to_char
6
+
7
+
8
+ def do_say(char: Character, args: str) -> str:
9
+ if not args:
10
+ return "Say what?"
11
+ message = f"{char.name} says, '{args}'"
12
+ if char.room:
13
+ char.room.broadcast(message, exclude=char)
14
+ broadcast_room(char.room, message, exclude=char)
15
+ return f"You say, '{args}'"
16
+
17
+
18
+ def do_tell(char: Character, args: str) -> str:
19
+ if "tell" in char.banned_channels:
20
+ return "You are banned from tell."
21
+ if not args:
22
+ return "Tell whom what?"
23
+ try:
24
+ target_name, message = args.split(None, 1)
25
+ except ValueError:
26
+ return "Tell whom what?"
27
+ target = next(
28
+ (
29
+ c
30
+ for c in character_registry
31
+ if c.name and c.name.lower() == target_name.lower()
32
+ ),
33
+ None,
34
+ )
35
+ if not target:
36
+ return "They aren't here."
37
+ if "tell" in target.muted_channels:
38
+ return "They aren't listening."
39
+ text = f"{char.name} tells you, '{message}'"
40
+ writer = getattr(target, "connection", None)
41
+ if writer:
42
+ asyncio.create_task(send_to_char(target, text))
43
+ if hasattr(target, "messages"):
44
+ target.messages.append(text)
45
+ return f"You tell {target.name}, '{message}'"
46
+
47
+
48
+ def do_shout(char: Character, args: str) -> str:
49
+ if "shout" in char.banned_channels:
50
+ return "You are banned from shout."
51
+ if not args:
52
+ return "Shout what?"
53
+ message = f"{char.name} shouts, '{args}'"
54
+ broadcast_global(message, channel="shout", exclude=char)
55
+ return f"You shout, '{args}'"
@@ -0,0 +1,11 @@
1
+ from functools import wraps
2
+ from mud.models.character import Character
3
+
4
+
5
+ def admin_only(func):
6
+ @wraps(func)
7
+ def wrapper(char: Character, *args, **kwargs):
8
+ if not getattr(char, "is_admin", False):
9
+ return "You do not have permission to use this command."
10
+ return func(char, *args, **kwargs)
11
+ return wrapper
@@ -0,0 +1,206 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass
3
+ import shlex
4
+ from typing import Callable, Dict, List, Optional
5
+
6
+ from mud.models.character import Character
7
+ from .movement import do_north, do_south, do_east, do_west, do_up, do_down, do_enter
8
+ from .inspection import do_look, do_scan, do_exits
9
+ from .inventory import do_get, do_drop, do_inventory, do_equipment
10
+ from .communication import do_say, do_tell, do_shout
11
+ from .combat import do_kill
12
+ from .admin_commands import (
13
+ cmd_who,
14
+ cmd_teleport,
15
+ cmd_spawn,
16
+ cmd_ban,
17
+ cmd_unban,
18
+ cmd_banlist,
19
+ )
20
+ from .shop import do_list, do_buy, do_sell
21
+ from .healer import do_heal
22
+ from .alias_cmds import do_alias, do_unalias
23
+ from .advancement import do_practice, do_train
24
+ from .notes import do_board, do_note
25
+ from .build import cmd_redit
26
+ from .socials import perform_social
27
+ from .help import do_help
28
+ from .imc import do_imc
29
+ from mud.wiznet import cmd_wiznet
30
+ from mud.logging.admin import log_admin_command
31
+ from mud.models.social import social_registry
32
+ from mud.models.constants import Position
33
+
34
+ CommandFunc = Callable[[Character, str], str]
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class Command:
39
+ name: str
40
+ func: CommandFunc
41
+ aliases: tuple[str, ...] = ()
42
+ admin_only: bool = False
43
+ min_position: Position = Position.DEAD
44
+
45
+
46
+ COMMANDS: List[Command] = [
47
+ # Movement (require standing per ROM)
48
+ Command("north", do_north, aliases=("n",), min_position=Position.STANDING),
49
+ Command("east", do_east, aliases=("e",), min_position=Position.STANDING),
50
+ Command("south", do_south, aliases=("s",), min_position=Position.STANDING),
51
+ Command("west", do_west, aliases=("w",), min_position=Position.STANDING),
52
+ Command("up", do_up, aliases=("u",), min_position=Position.STANDING),
53
+ Command("down", do_down, aliases=("d",), min_position=Position.STANDING),
54
+ Command("enter", do_enter, min_position=Position.STANDING),
55
+
56
+ # Common actions
57
+ Command("look", do_look, aliases=("l",), min_position=Position.RESTING),
58
+ Command("exits", do_exits, aliases=("ex",), min_position=Position.RESTING),
59
+ Command("get", do_get, aliases=("g",), min_position=Position.RESTING),
60
+ Command("drop", do_drop, min_position=Position.RESTING),
61
+ Command("inventory", do_inventory, aliases=("inv",), min_position=Position.DEAD),
62
+ Command("equipment", do_equipment, aliases=("eq",), min_position=Position.DEAD),
63
+
64
+ # Communication
65
+ Command("say", do_say, min_position=Position.RESTING),
66
+ Command("tell", do_tell, min_position=Position.RESTING),
67
+ Command("shout", do_shout, min_position=Position.RESTING),
68
+
69
+ # Combat
70
+ Command("kill", do_kill, aliases=("attack",), min_position=Position.FIGHTING),
71
+
72
+ # Info
73
+ Command("scan", do_scan, min_position=Position.SLEEPING),
74
+
75
+ # Shops
76
+ Command("list", do_list, min_position=Position.RESTING),
77
+ Command("buy", do_buy, min_position=Position.RESTING),
78
+ Command("sell", do_sell, min_position=Position.RESTING),
79
+ Command("heal", do_heal, min_position=Position.RESTING),
80
+
81
+ # Advancement
82
+ Command("practice", do_practice, min_position=Position.SLEEPING),
83
+ Command("train", do_train, min_position=Position.RESTING),
84
+
85
+ # Boards/Notes/Help
86
+ Command("board", do_board, min_position=Position.SLEEPING),
87
+ Command("note", do_note, min_position=Position.DEAD),
88
+ Command("help", do_help, min_position=Position.DEAD),
89
+
90
+ # IMC and aliasing
91
+ Command("imc", do_imc, min_position=Position.DEAD),
92
+ Command("alias", do_alias, min_position=Position.DEAD),
93
+ Command("unalias", do_unalias, min_position=Position.DEAD),
94
+
95
+ # Admin (leave position as DEAD; admin-only gating applies separately)
96
+ Command("@who", cmd_who, admin_only=True),
97
+ Command("@teleport", cmd_teleport, admin_only=True),
98
+ Command("@spawn", cmd_spawn, admin_only=True),
99
+ Command("ban", cmd_ban, admin_only=True),
100
+ Command("unban", cmd_unban, admin_only=True),
101
+ Command("banlist", cmd_banlist, admin_only=True),
102
+ Command("@redit", cmd_redit, admin_only=True),
103
+ Command("wiznet", cmd_wiznet, admin_only=True),
104
+ ]
105
+
106
+
107
+ COMMAND_INDEX: Dict[str, Command] = {}
108
+ for cmd in COMMANDS:
109
+ COMMAND_INDEX[cmd.name] = cmd
110
+ for alias in cmd.aliases:
111
+ COMMAND_INDEX[alias] = cmd
112
+
113
+
114
+ def resolve_command(name: str) -> Optional[Command]:
115
+ name = name.lower()
116
+ if name in COMMAND_INDEX:
117
+ return COMMAND_INDEX[name]
118
+ # ROM str_prefix behavior: choose the first command in table order
119
+ # whose name starts with the provided prefix. If none match, return None.
120
+ matches = [cmd for cmd in COMMANDS if cmd.name.startswith(name)]
121
+ return matches[0] if matches else None
122
+
123
+
124
+ def _expand_aliases(char: Character, input_str: str, *, max_depth: int = 5) -> str:
125
+ """Expand the first token using per-character aliases, up to max_depth."""
126
+ s = input_str
127
+ for _ in range(max_depth):
128
+ try:
129
+ parts = shlex.split(s)
130
+ except ValueError:
131
+ return s
132
+ if not parts:
133
+ return s
134
+ head, tail = parts[0], parts[1:]
135
+ expansion = char.aliases.get(head)
136
+ if not expansion:
137
+ return s
138
+ s = (expansion + (" " + " ".join(tail) if tail else "")).strip()
139
+ return s
140
+
141
+
142
+ def process_command(char: Character, input_str: str) -> str:
143
+ if not input_str.strip():
144
+ return "What?"
145
+ expanded = _expand_aliases(char, input_str)
146
+ try:
147
+ parts = shlex.split(expanded)
148
+ except ValueError:
149
+ return "Huh?"
150
+ if not parts:
151
+ return "What?"
152
+ cmd_name, *args = parts
153
+ command = resolve_command(cmd_name)
154
+ if not command:
155
+ social = social_registry.get(cmd_name.lower())
156
+ if social:
157
+ return perform_social(char, cmd_name, " ".join(args))
158
+ return "Huh?"
159
+ if command.admin_only and not getattr(char, "is_admin", False):
160
+ return "You do not have permission to use this command."
161
+ # Position gating (ROM-compatible messages)
162
+ if char.position < command.min_position:
163
+ pos = char.position
164
+ if pos == Position.DEAD:
165
+ return "Lie still; you are DEAD."
166
+ if pos in (Position.MORTAL, Position.INCAP):
167
+ return "You are hurt far too bad for that."
168
+ if pos == Position.STUNNED:
169
+ return "You are too stunned to do that."
170
+ if pos == Position.SLEEPING:
171
+ return "In your dreams, or what?"
172
+ if pos == Position.RESTING:
173
+ return "Nah... You feel too relaxed..."
174
+ if pos == Position.SITTING:
175
+ return "Better stand up first."
176
+ if pos == Position.FIGHTING:
177
+ return "No way! You are still fighting!"
178
+ # Fallback (should not happen)
179
+ return "You can't do that right now."
180
+ arg_str = " ".join(args)
181
+ # Log admin commands (accepted) to admin log for auditability.
182
+ if command.admin_only and getattr(char, "is_admin", False):
183
+ try:
184
+ log_admin_command(getattr(char, "name", "?"), command.name, arg_str)
185
+ except Exception:
186
+ # Logging must never break command execution.
187
+ pass
188
+ return command.func(char, arg_str)
189
+
190
+
191
+ def run_test_session() -> list[str]:
192
+ from mud.world import initialize_world, create_test_character
193
+ from mud.spawning.obj_spawner import spawn_object
194
+
195
+ initialize_world('area/area.lst')
196
+ char = create_test_character('Tester', 3001)
197
+ # Ensure sufficient movement points for the scripted walk
198
+ char.move = char.max_move = 100
199
+ sword = spawn_object(3022)
200
+ if sword:
201
+ char.room.add_object(sword)
202
+ commands = ["look", "get sword", "north", "say hello"]
203
+ outputs = []
204
+ for line in commands:
205
+ outputs.append(process_command(char, line))
206
+ return outputs
mud/commands/healer.py ADDED
@@ -0,0 +1,81 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Optional
4
+
5
+ from mud.models.character import Character
6
+
7
+
8
+ def _find_healer(char: Character) -> Optional[object]:
9
+ """Find a healer NPC in the room.
10
+
11
+ Heuristic: a mob whose prototype has spec_fun == 'spec_healer',
12
+ or any room occupant with an attribute `is_healer` truthy.
13
+ """
14
+ for mob in getattr(char.room, "people", []):
15
+ if getattr(mob, "is_healer", False):
16
+ return mob
17
+ proto = getattr(mob, "prototype", None)
18
+ if proto:
19
+ spec = getattr(proto, "spec_fun", "") or ""
20
+ if spec.lower() == "spec_healer":
21
+ return mob
22
+ return mob
23
+ return None
24
+
25
+
26
+ PRICE_GOLD = {
27
+ "light": 10,
28
+ "serious": 15,
29
+ "critical": 25,
30
+ "heal": 50,
31
+ "blindness": 20,
32
+ "disease": 15,
33
+ "poison": 25,
34
+ "uncurse": 50,
35
+ "refresh": 5,
36
+ "mana": 10,
37
+ }
38
+
39
+
40
+ def do_heal(char: Character, args: str = "") -> str:
41
+ healer = _find_healer(char)
42
+ if not healer:
43
+ return "You can't do that here."
44
+
45
+ arg = (args or "").strip().lower()
46
+ if not arg:
47
+ # Minimal price list in ROM spirit
48
+ items = "; ".join(f"{k} {v} gold" for k, v in PRICE_GOLD.items())
49
+ return f"Healer offers: {items}"
50
+
51
+ # Normalize common aliases
52
+ if arg.startswith("critic"):
53
+ arg = "critical"
54
+ if arg.startswith("uncurse") or arg == "curse":
55
+ arg = "uncurse"
56
+
57
+ if arg not in PRICE_GOLD:
58
+ return "Type heal for a list of spells."
59
+
60
+ cost = PRICE_GOLD[arg]
61
+ if char.gold < cost:
62
+ return "You do not have enough gold for my services."
63
+
64
+ char.gold -= cost
65
+
66
+ # Apply simple effects sufficient for parity tests
67
+ if arg == "refresh":
68
+ char.move = min(char.max_move, max(char.move, char.max_move))
69
+ return "You feel refreshed."
70
+ if arg == "heal":
71
+ char.hit = min(char.max_hit, max(char.hit, char.max_hit))
72
+ return "Your wounds mend."
73
+ if arg == "mana":
74
+ char.mana = min(char.max_mana, char.mana + 10)
75
+ return "A warm glow passes through you."
76
+ if arg in ("light", "serious", "critical"):
77
+ inc = {"light": 10, "serious": 20, "critical": 30}[arg]
78
+ char.hit = min(char.max_hit, char.hit + inc)
79
+ return "You feel better."
80
+ # For status cures, just acknowledge.
81
+ return "You feel cleansed."
mud/commands/help.py ADDED
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from mud.models.character import Character
4
+ from mud.models.help import help_registry
5
+
6
+
7
+ def do_help(ch: Character, args: str) -> str:
8
+ topic = args.strip().lower()
9
+ if not topic:
10
+ return "Help what?"
11
+ entry = help_registry.get(topic)
12
+ if not entry:
13
+ return "No help on that word."
14
+ return entry.text
mud/commands/imc.py ADDED
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from mud.imc import imc_enabled
4
+
5
+
6
+ def do_imc(char, args: str) -> str:
7
+ """IMC command stub.
8
+
9
+ - Disabled (default): returns a gated message.
10
+ - Enabled: returns basic help/usage; no sockets opened here.
11
+ """
12
+ if not imc_enabled():
13
+ return "IMC is disabled. Set IMC_ENABLED=true to enable."
14
+
15
+ if not args or args.strip().lower() in {"help", "?"}:
16
+ return "IMC is enabled (stub). Usage: imc send <channel> <message>"
17
+
18
+ return "IMC stub: command not implemented."
19
+