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
mud/wiznet.py ADDED
@@ -0,0 +1,74 @@
1
+ """Wiznet flags and helpers.
2
+
3
+ Provides flag definitions and broadcast filtering for immortal channels.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ from enum import IntFlag
8
+ from typing import TYPE_CHECKING
9
+
10
+
11
+ class WiznetFlag(IntFlag):
12
+ """Wiznet flags mirroring ROM bit values."""
13
+
14
+ WIZ_ON = 0x00000001
15
+ WIZ_TICKS = 0x00000002
16
+ WIZ_LOGINS = 0x00000004
17
+ WIZ_SITES = 0x00000008
18
+ WIZ_LINKS = 0x00000010
19
+ WIZ_DEATHS = 0x00000020
20
+ WIZ_RESETS = 0x00000040
21
+ WIZ_MOBDEATHS = 0x00000080
22
+ WIZ_FLAGS = 0x00000100
23
+ WIZ_PENALTIES = 0x00000200
24
+ WIZ_SACCING = 0x00000400
25
+ WIZ_LEVELS = 0x00000800
26
+ WIZ_SECURE = 0x00001000
27
+ WIZ_SWITCHES = 0x00002000
28
+ WIZ_SNOOPS = 0x00004000
29
+ WIZ_RESTORE = 0x00008000
30
+ WIZ_LOAD = 0x00010000
31
+ WIZ_NEWBIE = 0x00020000
32
+ WIZ_SPAM = 0x00040000
33
+ WIZ_DEBUG = 0x00080000
34
+ WIZ_MEMORY = 0x00100000
35
+ WIZ_SKILLS = 0x00200000
36
+ WIZ_TESTING = 0x00400000
37
+
38
+
39
+ if TYPE_CHECKING: # pragma: no cover - for type hints only
40
+ pass
41
+
42
+
43
+ def wiznet(message: str, flag: WiznetFlag) -> None:
44
+ """Broadcast *message* to immortals subscribed to *flag*.
45
+
46
+ Immortals must have WIZ_ON and the given *flag* set to receive the message.
47
+ """
48
+ from mud.models.character import character_registry
49
+
50
+ for ch in list(character_registry):
51
+ if not getattr(ch, "is_admin", False):
52
+ continue
53
+ if not getattr(ch, "wiznet", 0) & WiznetFlag.WIZ_ON:
54
+ continue
55
+ if not getattr(ch, "wiznet", 0) & flag:
56
+ continue
57
+ if hasattr(ch, "messages"):
58
+ ch.messages.append(message)
59
+
60
+
61
+ def cmd_wiznet(char, args: str) -> str:
62
+ """Toggle WIZ_ON for immortal *char*.
63
+
64
+ Only immortals may use this command. With no arguments it flips the
65
+ :class:`WiznetFlag.WIZ_ON` bit and reports the new state.
66
+ """
67
+ from mud.models.character import Character # local import to avoid cycle
68
+
69
+ if not isinstance(char, Character) or not getattr(char, "is_admin", False):
70
+ return "Huh?"
71
+
72
+ char.wiznet ^= int(WiznetFlag.WIZ_ON)
73
+ state = "on" if char.wiznet & int(WiznetFlag.WIZ_ON) else "off"
74
+ return f"Wiznet is now {state}."
mud/world/__init__.py ADDED
@@ -0,0 +1,11 @@
1
+ from .world_state import initialize_world, create_test_character, fix_all_exits
2
+ from .movement import move_character
3
+ from .look import look
4
+
5
+ __all__ = [
6
+ "initialize_world",
7
+ "create_test_character",
8
+ "fix_all_exits",
9
+ "move_character",
10
+ "look",
11
+ ]
mud/world/linking.py ADDED
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+ import logging
3
+ from mud.registry import room_registry
4
+ from mud.models.constants import Direction
5
+
6
+
7
+ def link_exits() -> None:
8
+ """Replace exit vnum references with actual Room objects."""
9
+ for room in room_registry.values():
10
+ for idx, exit in enumerate(room.exits):
11
+ if exit is None:
12
+ continue
13
+ if exit.to_room is not None:
14
+ continue
15
+ if exit.vnum <= 0:
16
+ # Negative or zero vnums denote an intentionally unset exit.
17
+ # These should be ignored rather than reported as errors.
18
+ continue
19
+ target = room_registry.get(exit.vnum)
20
+ if target:
21
+ exit.to_room = target
22
+ else:
23
+ logging.warning(
24
+ "Unlinked exit in room %s -> %s (target %s not found)",
25
+ room.vnum,
26
+ Direction(idx).name.lower(),
27
+ exit.vnum,
28
+ )
29
+ if not hasattr(room, "unlinked_exits"):
30
+ room.unlinked_exits = set()
31
+ room.unlinked_exits.add(Direction(idx))
mud/world/look.py ADDED
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+ from mud.models.character import Character
3
+ from mud.models.constants import Direction
4
+
5
+
6
+ dir_names = {
7
+ Direction.NORTH: "north",
8
+ Direction.EAST: "east",
9
+ Direction.SOUTH: "south",
10
+ Direction.WEST: "west",
11
+ Direction.UP: "up",
12
+ Direction.DOWN: "down",
13
+ }
14
+
15
+
16
+ def look(char: Character) -> str:
17
+ room = char.room
18
+ if not room:
19
+ return "You are floating in a void..."
20
+ exit_list = [dir_names[Direction(i)] for i, ex in enumerate(room.exits) if ex]
21
+ lines = [room.name or "", room.description or ""]
22
+ if exit_list:
23
+ lines.append(f"[Exits: {' '.join(exit_list)}]")
24
+ if room.contents:
25
+ lines.append("Objects: " + ", ".join(obj.short_descr or obj.name or "object" for obj in room.contents))
26
+ others = [p.name or "someone" for p in room.people if p is not char]
27
+ if others:
28
+ lines.append("Characters: " + ", ".join(others))
29
+ return "\n".join(lines).strip()
mud/world/movement.py ADDED
@@ -0,0 +1,135 @@
1
+ from __future__ import annotations
2
+ from typing import Dict, Iterable
3
+
4
+ from mud.models.character import Character
5
+ from mud.models.constants import Direction, Sector, AffectFlag, ItemType
6
+ from mud.net.protocol import broadcast_room
7
+
8
+
9
+ dir_map: Dict[str, Direction] = {
10
+ "north": Direction.NORTH,
11
+ "east": Direction.EAST,
12
+ "south": Direction.SOUTH,
13
+ "west": Direction.WEST,
14
+ "up": Direction.UP,
15
+ "down": Direction.DOWN,
16
+ }
17
+
18
+
19
+ # ROM str_app carry table (carry column only) for STR 0..25.
20
+ # Source: src/const.c:str_app (third field), multiplied by 10 in handler.c.
21
+ _STR_CARRY = [
22
+ 0, # 0
23
+ 3, 3, 10, 25, 55, # 1..5
24
+ 80, 90, 100, 100, 115, 115, 130, 130, 140, 150, 165, 180, 200, 225, 250, 300, 350, 400, 450, 500,
25
+ ]
26
+
27
+
28
+ def _get_curr_stat(ch: Character, idx: int) -> int | None:
29
+ stats = getattr(ch, "perm_stat", None) or []
30
+ if idx < len(stats) and stats[idx] > 0:
31
+ val = stats[idx]
32
+ return max(0, min(25, int(val)))
33
+ return None
34
+
35
+
36
+ def can_carry_w(ch: Character) -> int:
37
+ """Carry weight capacity.
38
+
39
+ - If STR stat present: use ROM formula `str_app[STR].carry * 10 + level * 25`.
40
+ - Otherwise: preserve prior fixed cap (100) to avoid changing existing tests.
41
+ """
42
+ s = _get_curr_stat(ch, 0) # STAT_STR
43
+ if s is None:
44
+ return 100
45
+ carry = _STR_CARRY[s]
46
+ return carry * 10 + ch.level * 25
47
+
48
+
49
+ def can_carry_n(ch: Character) -> int:
50
+ """Carry number capacity.
51
+
52
+ - If DEX stat present: use ROM-like `MAX_WEAR + 2*DEX + level` (MAX_WEAR≈19).
53
+ - Otherwise: preserve prior fixed cap (30).
54
+ """
55
+ d = _get_curr_stat(ch, 1) # STAT_DEX
56
+ if d is None:
57
+ return 30
58
+ MAX_WEAR = 19
59
+ return MAX_WEAR + 2 * d + ch.level
60
+
61
+
62
+ def move_character(char: Character, direction: str) -> str:
63
+ dir_key = direction.lower()
64
+ if dir_key not in dir_map:
65
+ return "You cannot go that way."
66
+
67
+ if char.carry_weight > can_carry_w(char) or char.carry_number > can_carry_n(char):
68
+ return "You are too encumbered to move."
69
+
70
+ idx = dir_map[dir_key]
71
+ exit = char.room.exits[idx]
72
+ if exit is None or exit.to_room is None:
73
+ return "You cannot go that way."
74
+
75
+ current_room = char.room
76
+ target_room = exit.to_room
77
+
78
+ # --- Sector-based gating and movement costs (ROM act_move.c) ---
79
+ from_sector = Sector(current_room.sector_type)
80
+ to_sector = Sector(target_room.sector_type)
81
+
82
+ # Air requires flying unless immortal/admin
83
+ if (from_sector == Sector.AIR or to_sector == Sector.AIR):
84
+ if not char.is_admin and not bool(char.affected_by & AffectFlag.FLYING):
85
+ return "You can't fly."
86
+
87
+ # Water (no swim) requires a boat unless flying or immortal
88
+ if (from_sector == Sector.WATER_NOSWIM or to_sector == Sector.WATER_NOSWIM):
89
+ if not char.is_admin and not bool(char.affected_by & AffectFlag.FLYING):
90
+ def has_boat(objs: Iterable):
91
+ for o in objs:
92
+ proto = getattr(o, "prototype", None)
93
+ if proto and getattr(proto, "item_type", None) == int(ItemType.BOAT):
94
+ return True
95
+ return False
96
+
97
+ has_boat_item = has_boat(char.inventory) or has_boat(getattr(char, "equipment", {}).values())
98
+ if not has_boat_item:
99
+ return "You need a boat to go there."
100
+
101
+ movement_loss = {
102
+ Sector.INSIDE: 1,
103
+ Sector.CITY: 2,
104
+ Sector.FIELD: 2,
105
+ Sector.FOREST: 3,
106
+ Sector.HILLS: 4,
107
+ Sector.MOUNTAIN: 6,
108
+ Sector.WATER_SWIM: 4,
109
+ Sector.WATER_NOSWIM: 1,
110
+ Sector.UNUSED: 6,
111
+ Sector.AIR: 10,
112
+ Sector.DESERT: 6,
113
+ }
114
+
115
+ move_cost = (movement_loss.get(from_sector, 2) + movement_loss.get(to_sector, 2)) // 2
116
+ # Conditional effects
117
+ if char.affected_by & AffectFlag.FLYING or char.affected_by & AffectFlag.HASTE:
118
+ move_cost = max(0, move_cost // 2)
119
+ if char.affected_by & AffectFlag.SLOW:
120
+ move_cost *= 2
121
+
122
+ if char.move < move_cost:
123
+ return "You are too exhausted."
124
+
125
+ # Apply short wait-state and deduct movement points
126
+ char.wait = max(char.wait, 1)
127
+ char.move -= move_cost
128
+
129
+ broadcast_room(current_room, f"{char.name} leaves {dir_key}.", exclude=char)
130
+ if char in current_room.people:
131
+ current_room.people.remove(char)
132
+ target_room.people.append(char)
133
+ char.room = target_room
134
+ broadcast_room(target_room, f"{char.name} arrives.", exclude=char)
135
+ return f"You walk {dir_key} to {target_room.name}."
@@ -0,0 +1,179 @@
1
+ from __future__ import annotations
2
+ from mud.loaders import load_all_areas
3
+ from mud.loaders.json_area_loader import load_all_areas_from_json
4
+ from mud.loaders.json_loader import load_all_areas_from_json as load_enhanced_json
5
+ from mud.registry import room_registry, area_registry, mob_registry, obj_registry
6
+ from mud.db.session import SessionLocal
7
+ from mud.db import models
8
+ from mud.models.character import Character, character_registry
9
+ from mud.models.constants import Position
10
+ from mud.spawning.reset_handler import apply_resets
11
+ from .linking import link_exits
12
+ from mud.security import bans
13
+
14
+ # Global skill registry for world initialization
15
+ skill_registry = None
16
+
17
+
18
+ def load_world_from_db() -> bool:
19
+ """Populate registries from the database."""
20
+ session = SessionLocal()
21
+
22
+ db_rooms = session.query(models.Room).all()
23
+ for db_room in db_rooms:
24
+ room = models_to_room(db_room)
25
+ room_registry[room.vnum] = room
26
+
27
+ db_exits = session.query(models.Exit).all()
28
+ for db_exit in db_exits:
29
+ origin_room = session.query(models.Room).get(db_exit.room_id)
30
+ source = room_registry.get(origin_room.vnum) if origin_room else None
31
+ target = room_registry.get(db_exit.to_room_vnum)
32
+ if source and target:
33
+ if len(source.exits) <= int(db_exit.direction):
34
+ source.exits.extend([None] * (int(db_exit.direction) - len(source.exits) + 1))
35
+ source.exits[int(db_exit.direction)] = target
36
+
37
+ for db_mob in session.query(models.MobPrototype).all():
38
+ mob_registry[db_mob.vnum] = models_to_mob(db_mob)
39
+
40
+ for db_obj in session.query(models.ObjPrototype).all():
41
+ obj_registry[db_obj.vnum] = models_to_obj(db_obj)
42
+
43
+ print(
44
+ f"\u2705 Loaded {len(room_registry)} rooms, {len(mob_registry)} mobs, {len(obj_registry)} objects."
45
+ )
46
+ return True
47
+
48
+
49
+ def models_to_room(db_room: models.Room):
50
+ from mud.models.room import Room
51
+
52
+ return Room(
53
+ vnum=db_room.vnum,
54
+ name=db_room.name,
55
+ description=db_room.description,
56
+ sector_type=db_room.sector_type or 0,
57
+ room_flags=db_room.room_flags or 0,
58
+ exits=[None] * 10,
59
+ )
60
+
61
+
62
+ def models_to_mob(db_mob: models.MobPrototype):
63
+ from mud.models.mob import MobIndex
64
+
65
+ return MobIndex(
66
+ vnum=db_mob.vnum,
67
+ player_name=db_mob.name,
68
+ short_descr=db_mob.short_desc,
69
+ long_descr=db_mob.long_desc,
70
+ level=db_mob.level or 0,
71
+ alignment=db_mob.alignment or 0,
72
+ )
73
+
74
+
75
+ def models_to_obj(db_obj: models.ObjPrototype):
76
+ from mud.models.obj import ObjIndex
77
+
78
+ return ObjIndex(
79
+ vnum=db_obj.vnum,
80
+ name=db_obj.name,
81
+ short_descr=db_obj.short_desc,
82
+ description=db_obj.long_desc,
83
+ item_type=db_obj.item_type or 0,
84
+ extra_flags=db_obj.flags or 0,
85
+ value=[db_obj.value0, db_obj.value1, db_obj.value2, db_obj.value3],
86
+ )
87
+
88
+
89
+ def initialize_world(area_list_path: str | None = "area/area.lst", use_json: bool = True) -> None:
90
+ """Initialize world from files or database.
91
+
92
+ Args:
93
+ area_list_path: Path to area.lst file (for legacy .are loading)
94
+ use_json: If True, load from JSON files in data/areas/. If False, use legacy .are files.
95
+ """
96
+ # Tiny fix: ensure a clean ban registry at boot and between tests.
97
+ # ROM loads bans from disk at boot; tests may add bans in-memory.
98
+ # Clearing here avoids leakage across test modules without affecting
99
+ # persistence tests which explicitly save/load.
100
+ bans.clear_all_bans()
101
+
102
+ # Load skills registry from JSON
103
+ from mud.skills.registry import SkillRegistry
104
+ from pathlib import Path
105
+ skills_path = Path("data/skills.json")
106
+ if skills_path.exists():
107
+ try:
108
+ global skill_registry
109
+ skill_registry = SkillRegistry()
110
+ skill_registry.load(skills_path)
111
+ print(f"✅ Loaded {len(skill_registry.skills)} skills from {skills_path}")
112
+ except Exception as e:
113
+ print(f"Warning: Failed to load skills from {skills_path}: {e}")
114
+ skill_registry = None
115
+
116
+ # Load shops from JSON (needed for shopkeeper detection in resets)
117
+ from mud.registry import shop_registry
118
+ from mud.loaders.shop_loader import Shop
119
+ import json
120
+ shops_path = Path("data/shops.json")
121
+ if shops_path.exists():
122
+ try:
123
+ with open(shops_path, 'r') as f:
124
+ shops_data = json.load(f)
125
+ shop_registry.clear()
126
+ for shop_data in shops_data:
127
+ # Convert string buy_types back to int list for compatibility
128
+ buy_types = []
129
+ for bt in shop_data.get('buy_types', []):
130
+ if isinstance(bt, str):
131
+ from mud.models.constants import ItemType
132
+ try:
133
+ buy_types.append(ItemType[bt.upper()].value)
134
+ except (KeyError, AttributeError):
135
+ buy_types.append(0) # unknown type
136
+ else:
137
+ buy_types.append(bt)
138
+
139
+ shop_registry[shop_data['keeper']] = Shop(
140
+ keeper=shop_data['keeper'],
141
+ buy_types=buy_types,
142
+ profit_buy=shop_data.get('profit_buy', 100),
143
+ profit_sell=shop_data.get('profit_sell', 100),
144
+ open_hour=shop_data.get('open_hour', 0),
145
+ close_hour=shop_data.get('close_hour', 23),
146
+ )
147
+ print(f"✅ Loaded {len(shop_registry)} shops from {shops_path}")
148
+ except Exception as e:
149
+ print(f"Warning: Failed to load shops from {shops_path}: {e}")
150
+
151
+ if area_list_path:
152
+ if use_json:
153
+ # Load from JSON files using enhanced field mapping
154
+ from mud.loaders.json_loader import load_all_areas_from_json
155
+ json_areas = load_all_areas_from_json("data/areas")
156
+ # Areas are already registered in area_registry by the JSON loader
157
+ else:
158
+ # Load from legacy .are files
159
+ load_all_areas(area_list_path)
160
+ link_exits()
161
+ for area in area_registry.values():
162
+ apply_resets(area)
163
+ else:
164
+ load_world_from_db()
165
+
166
+
167
+ def fix_all_exits() -> None:
168
+ link_exits()
169
+
170
+
171
+ def create_test_character(name: str, room_vnum: int) -> Character:
172
+ room = room_registry.get(room_vnum)
173
+ char = Character(name=name)
174
+ # ROM default: new players start standing.
175
+ char.position = int(Position.STANDING)
176
+ if room:
177
+ room.add_character(char)
178
+ character_registry.append(char)
179
+ return char
@@ -0,0 +1,236 @@
1
+ Metadata-Version: 2.4
2
+ Name: rom24-quickmud-python
3
+ Version: 1.2.2
4
+ Summary: A modern Python port of the ROM 2.4b6 MUD engine with full telnet server and JSON world loading
5
+ Author-email: Mark Jedrzejczyk <mark.jedrzejczyk@gmail.com>
6
+ Maintainer-email: Mark Jedrzejczyk <mark.jedrzejczyk@gmail.com>
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/Nostoi/rom24-quickmud-python
9
+ Project-URL: Repository, https://github.com/Nostoi/rom24-quickmud-python.git
10
+ Project-URL: Documentation, https://github.com/Nostoi/rom24-quickmud-python/blob/master/README.md
11
+ Project-URL: Bug Tracker, https://github.com/Nostoi/rom24-quickmud-python/issues
12
+ Project-URL: Changelog, https://github.com/Nostoi/rom24-quickmud-python/releases
13
+ Keywords: mud,rom,game,mmo,telnet,server
14
+ Classifier: Development Status :: 4 - Beta
15
+ Classifier: Environment :: Console
16
+ Classifier: Environment :: No Input/Output (Daemon)
17
+ Classifier: Intended Audience :: Developers
18
+ Classifier: Intended Audience :: End Users/Desktop
19
+ Classifier: Operating System :: OS Independent
20
+ Classifier: Programming Language :: Python :: 3
21
+ Classifier: Programming Language :: Python :: 3.10
22
+ Classifier: Programming Language :: Python :: 3.11
23
+ Classifier: Programming Language :: Python :: 3.12
24
+ Classifier: Topic :: Games/Entertainment :: Multi-User Dungeons (MUD)
25
+ Classifier: Topic :: Internet
26
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
27
+ Classifier: Typing :: Typed
28
+ Requires-Python: >=3.10
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: SQLAlchemy<3,>=2.0
32
+ Requires-Dist: typer>=0.9
33
+ Requires-Dist: python-dotenv>=1.0
34
+ Requires-Dist: fastapi>=0.68.0
35
+ Requires-Dist: uvicorn>=0.15.0
36
+ Requires-Dist: pytest-timeout>=2.1.0
37
+ Provides-Extra: dev
38
+ Requires-Dist: pytest>=8.0; extra == "dev"
39
+ Requires-Dist: pytest-cov>=6.0; extra == "dev"
40
+ Requires-Dist: pytest-timeout>=2.1.0; extra == "dev"
41
+ Requires-Dist: mypy>=1.0; extra == "dev"
42
+ Requires-Dist: ruff>=0.1.0; extra == "dev"
43
+ Requires-Dist: black>=22.0; extra == "dev"
44
+ Requires-Dist: jsonschema>=4.0; extra == "dev"
45
+ Requires-Dist: build>=0.10.0; extra == "dev"
46
+ Requires-Dist: twine>=4.0.0; extra == "dev"
47
+ Provides-Extra: test
48
+ Requires-Dist: pytest>=8.0; extra == "test"
49
+ Requires-Dist: pytest-cov>=6.0; extra == "test"
50
+ Requires-Dist: pytest-timeout>=2.1.0; extra == "test"
51
+ Dynamic: license-file
52
+
53
+ # QuickMUD - A Modern ROM 2.4 Python Port
54
+
55
+ [![PyPI version](https://badge.fury.io/py/quickmud.svg)](https://badge.fury.io/py/quickmud)
56
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
57
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
58
+ [![Tests](https://img.shields.io/badge/tests-200%2F200%20passing-brightgreen.svg)](https://github.com/Nostoi/rom24-quickmud-python)
59
+
60
+ **QuickMUD is a modern Python port of the legendary ROM 2.4b6 MUD engine**, derived from ROM 2.4b6, Merc 2.1 and DikuMUD. This is a complete rewrite that brings the classic text-based MMORPG experience to modern Python with async networking, JSON world data, and comprehensive testing.
61
+
62
+ ## 🎮 What is a MUD?
63
+
64
+ A "[Multi-User Dungeon](https://en.wikipedia.org/wiki/MUD)" (MUD) is a text-based MMORPG that runs over telnet. ROM is renowned for its fast-paced combat system and rich player interaction. ROM was also the foundation for [Carrion Fields](http://www.carrionfields.net/), one of the most acclaimed MUDs ever created.
65
+
66
+ ## ✨ Key Features
67
+
68
+ - **🚀 Modern Python Architecture**: Fully async/await networking with SQLAlchemy ORM
69
+ - **📡 Multi-User Telnet Server**: Handle hundreds of concurrent players
70
+ - **🗺️ JSON World Loading**: Easy-to-edit world data with 352+ room resets
71
+ - **🏪 Complete Shop System**: Buy, sell, and list items with working economy
72
+ - **⚔️ ROM Combat System**: Classic ROM combat mechanics and skill system
73
+ - **👥 Social Features**: Say, tell, shout, and 100+ social interactions
74
+ - **🛠️ Admin Commands**: Teleport, spawn, ban management, and OLC building
75
+ - **📊 100% Test Coverage**: 200+ tests ensure reliability and stability
76
+
77
+ ## 📦 Installation
78
+
79
+ ### For Players & Server Operators
80
+
81
+ ```bash
82
+ pip install quickmud
83
+ ```
84
+
85
+ ### Quick Start
86
+
87
+ Run a QuickMUD server:
88
+
89
+ ```bash
90
+ mud runserver
91
+ ```
92
+
93
+ The server will start on `localhost:4000`. Connect with any telnet client:
94
+
95
+ ```bash
96
+ telnet localhost 4000
97
+ ```
98
+
99
+ ## 🏗️ For Developers
100
+
101
+ ## 🏗️ For Developers
102
+
103
+ ### Development Installation
104
+
105
+ ```bash
106
+ git clone https://github.com/Nostoi/rom24-quickmud-python.git
107
+ cd rom24-quickmud-python
108
+ python -m venv venv
109
+ source venv/bin/activate # On Windows: venv\Scripts\activate
110
+ pip install -e .[dev]
111
+ ```
112
+
113
+ ### Running Tests
114
+
115
+ ```bash
116
+ pytest # Run all 200 tests (should complete in ~16 seconds)
117
+ ```
118
+
119
+ ### Development Server
120
+
121
+ ```bash
122
+ python -m mud # Start development server
123
+ ```
124
+
125
+ ## 🎯 Project Status
126
+
127
+ - **Version**: 1.2.0 (Production Ready)
128
+ - **Test Coverage**: 200/200 tests passing (100% success rate)
129
+ - **Performance**: Full test suite completes in ~16 seconds
130
+ - **Compatibility**: Python 3.10+, cross-platform
131
+
132
+ ## 🏛️ Architecture
133
+
134
+ - **Async Networking**: Modern async/await telnet server
135
+ - **SQLAlchemy ORM**: Robust database layer with migrations
136
+ - **JSON World Data**: Human-readable area files with full ROM compatibility
137
+ - **Modular Design**: Clean separation of concerns (commands, world, networking)
138
+ - **Type Safety**: Comprehensive type hints throughout codebase
139
+
140
+ ## 📜 License
141
+
142
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
143
+
144
+ ## 🤝 Contributing
145
+
146
+ Contributions are welcome! Please read our [Contributing Guidelines](CONTRIBUTING.md) and feel free to submit pull requests.
147
+
148
+ ## 📚 Documentation
149
+
150
+ - [Installation Guide](docs/installation.md)
151
+ - [Configuration](docs/configuration.md)
152
+ - [World Building](docs/world-building.md)
153
+ - [API Reference](docs/api.md)
154
+
155
+ ---
156
+
157
+ **Experience the classic MUD gameplay with modern Python reliability!** 🐍✨
158
+
159
+ For a fully reproducible environment, use the pinned requirements files generated with [pip-tools](https://github.com/jazzband/pip-tools):
160
+
161
+ ```bash
162
+ pip install -r requirements-dev.txt
163
+ ```
164
+
165
+ To update the pinned dependencies:
166
+
167
+ ```bash
168
+ pip-compile requirements.in
169
+ pip-compile requirements-dev.in
170
+ ```
171
+
172
+ Tools like [Poetry](https://python-poetry.org/) provide a similar workflow if you prefer that approach.
173
+
174
+ Run tests with:
175
+
176
+ ```bash
177
+ pytest
178
+ ```
179
+
180
+ ### Publishing
181
+
182
+ To release a new version to PyPI:
183
+
184
+ 1. Update the version in `pyproject.toml`.
185
+ 2. Commit and tag:
186
+
187
+ ```bash
188
+ git commit -am "release: v1.2.3"
189
+ git tag v1.2.3
190
+ git push origin main --tags
191
+ ```
192
+
193
+ The GitHub Actions workflow will build and publish the package when the tag is pushed.
194
+
195
+ ## Python Architecture
196
+
197
+ Game systems are implemented in Python modules:
198
+
199
+ - `mud/net` provides asynchronous telnet and websocket servers.
200
+ - `mud/game_loop.py` drives the tick-based update loop.
201
+ - `mud/commands` contains the command dispatcher and handlers.
202
+ - `mud/combat` and `mud/skills` implement combat and abilities.
203
+ - `mud/persistence.py` handles saving characters and world state.
204
+
205
+ Start the server with:
206
+
207
+ ```sh
208
+ python -m mud runserver
209
+ ```
210
+
211
+ ## Docker Image
212
+
213
+ Build and run the Python server with Docker:
214
+
215
+ ```bash
216
+ docker build -t quickmud .
217
+ docker run -p 5000:5000 quickmud
218
+ ```
219
+
220
+ Or use docker-compose to rebuild on changes and mount the repository:
221
+
222
+ ```bash
223
+ docker-compose up
224
+ ```
225
+
226
+ Connect via:
227
+
228
+ ```bash
229
+ telnet localhost 5000
230
+ ```
231
+
232
+ ## Data Models
233
+
234
+ The `mud/models` package defines dataclasses used by the game engine.
235
+ They mirror the JSON schemas in `schemas/` and supply enums and registries
236
+ for loading and manipulating area, room, object, and character data.