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,113 @@
1
+ from __future__ import annotations
2
+
3
+ from mud.models.character import Character
4
+ from mud.world.look import look, dir_names
5
+ from mud.models.constants import Direction
6
+
7
+
8
+ def do_scan(char: Character, args: str = "") -> str:
9
+ """ROM-like scan output with distances and optional direction.
10
+
11
+ - No arg: list current room (depth 0) and adjacent rooms (depth 1) in N,E,S,W,Up,Down order.
12
+ - With direction: follow exits up to depth 3 and list visible characters per room.
13
+ """
14
+ if not char.room:
15
+ return "You see nothing."
16
+
17
+ order = [
18
+ Direction.NORTH,
19
+ Direction.EAST,
20
+ Direction.SOUTH,
21
+ Direction.WEST,
22
+ Direction.UP,
23
+ Direction.DOWN,
24
+ ]
25
+ dir_name = {
26
+ Direction.NORTH: "north",
27
+ Direction.EAST: "east",
28
+ Direction.SOUTH: "south",
29
+ Direction.WEST: "west",
30
+ Direction.UP: "up",
31
+ Direction.DOWN: "down",
32
+ }
33
+ distance = [
34
+ "right here.",
35
+ "nearby to the %s.",
36
+ "not far %s.",
37
+ "off in the distance %s.",
38
+ ]
39
+
40
+ def list_room(room, depth: int, door: int) -> list[str]:
41
+ lines: list[str] = []
42
+ if not room:
43
+ return lines
44
+ for p in room.people:
45
+ if p is char:
46
+ continue
47
+ who = p.name or "someone"
48
+ if depth == 0:
49
+ lines.append(f"{who}, {distance[0]}")
50
+ else:
51
+ dn = dir_name[Direction(door)]
52
+ lines.append(f"{who}, {distance[depth] % dn}")
53
+ return lines
54
+
55
+ s = args.strip().lower()
56
+ if not s:
57
+ lines: list[str] = ["Looking around you see:"]
58
+ # current room
59
+ lines += list_room(char.room, 0, -1)
60
+ # each direction at depth 1
61
+ for d in order:
62
+ ex = char.room.exits[int(d)] if char.room.exits and int(d) < len(char.room.exits) else None
63
+ to_room = ex.to_room if ex else None
64
+ lines += list_room(to_room, 1, int(d))
65
+ if len(lines) == 1:
66
+ lines.append("No one is nearby.")
67
+ return "\n".join(lines)
68
+
69
+ # Directional scan up to depth 3
70
+ token_map = {
71
+ "n": Direction.NORTH,
72
+ "north": Direction.NORTH,
73
+ "e": Direction.EAST,
74
+ "east": Direction.EAST,
75
+ "s": Direction.SOUTH,
76
+ "south": Direction.SOUTH,
77
+ "w": Direction.WEST,
78
+ "west": Direction.WEST,
79
+ "u": Direction.UP,
80
+ "up": Direction.UP,
81
+ "d": Direction.DOWN,
82
+ "down": Direction.DOWN,
83
+ }
84
+ if s not in token_map:
85
+ return "Which way do you want to scan?"
86
+ d = token_map[s]
87
+ dir_str = dir_name[d]
88
+ lines = [f"Looking {dir_str} you see:"]
89
+ scan_room = char.room
90
+ for depth in (1, 2, 3):
91
+ ex = scan_room.exits[int(d)] if scan_room and scan_room.exits and int(d) < len(scan_room.exits) else None
92
+ scan_room = ex.to_room if ex else None
93
+ if not scan_room:
94
+ break
95
+ lines += list_room(scan_room, depth, int(d))
96
+ if len(lines) == 1:
97
+ lines.append("Nothing of note.")
98
+ return "\n".join(lines)
99
+
100
+
101
+ def do_look(char: Character, args: str = "") -> str:
102
+ return look(char)
103
+
104
+
105
+ def do_exits(char: Character, args: str = "") -> str:
106
+ """List obvious exits from the current room (ROM-style)."""
107
+ room = char.room
108
+ if not room or not getattr(room, "exits", None):
109
+ return "Obvious exits: none."
110
+ dirs = [dir_names[type(list(dir_names.keys())[0]) (i)] for i, ex in enumerate(room.exits) if ex]
111
+ if not dirs:
112
+ return "Obvious exits: none."
113
+ return f"Obvious exits: {' '.join(dirs)}."
@@ -0,0 +1,42 @@
1
+ from mud.models.character import Character
2
+
3
+
4
+ def do_get(char: Character, args: str) -> str:
5
+ if not args:
6
+ return "Get what?"
7
+ name = args.lower()
8
+ for obj in list(char.room.contents):
9
+ obj_name = (obj.short_descr or obj.name or "").lower()
10
+ if name in obj_name:
11
+ char.room.contents.remove(obj)
12
+ char.add_object(obj)
13
+ return f"You pick up {obj.short_descr or obj.name}."
14
+ return "You don't see that here."
15
+
16
+
17
+ def do_drop(char: Character, args: str) -> str:
18
+ if not args:
19
+ return "Drop what?"
20
+ name = args.lower()
21
+ for obj in list(char.inventory):
22
+ obj_name = (obj.short_descr or obj.name or "").lower()
23
+ if name in obj_name:
24
+ char.inventory.remove(obj)
25
+ char.room.add_object(obj)
26
+ return f"You drop {obj.short_descr or obj.name}."
27
+ return "You aren't carrying that."
28
+
29
+
30
+ def do_inventory(char: Character, args: str = "") -> str:
31
+ if not char.inventory:
32
+ return "You are carrying nothing."
33
+ return "You are carrying: " + ", ".join(obj.short_descr or obj.name or "object" for obj in char.inventory)
34
+
35
+
36
+ def do_equipment(char: Character, args: str = "") -> str:
37
+ if not char.equipment:
38
+ return "You are wearing nothing."
39
+ parts = []
40
+ for slot, obj in char.equipment.items():
41
+ parts.append(f"{slot}: {obj.short_descr or obj.name or 'object'}")
42
+ return "You are using: " + ", ".join(parts)
@@ -0,0 +1,71 @@
1
+ from mud.world.movement import move_character
2
+ from mud.models.character import Character
3
+ from mud.models.constants import ItemType, EX_CLOSED
4
+ from mud.registry import room_registry
5
+
6
+
7
+ def do_north(char: Character, args: str = "") -> str:
8
+ return move_character(char, "north")
9
+
10
+
11
+ def do_south(char: Character, args: str = "") -> str:
12
+ return move_character(char, "south")
13
+
14
+
15
+ def do_east(char: Character, args: str = "") -> str:
16
+ return move_character(char, "east")
17
+
18
+
19
+ def do_west(char: Character, args: str = "") -> str:
20
+ return move_character(char, "west")
21
+
22
+
23
+ def do_up(char: Character, args: str = "") -> str:
24
+ return move_character(char, "up")
25
+
26
+
27
+ def do_down(char: Character, args: str = "") -> str:
28
+ return move_character(char, "down")
29
+
30
+
31
+ def do_enter(char: Character, args: str = "") -> str:
32
+ target = (args or "").strip().lower()
33
+ if not target:
34
+ return "Enter what?"
35
+
36
+ # Find a portal object in the room matching target token
37
+ portal = None
38
+ for obj in getattr(char.room, "contents", []):
39
+ proto = getattr(obj, "prototype", None)
40
+ if not proto or getattr(proto, "item_type", 0) != int(ItemType.PORTAL):
41
+ continue
42
+ name = (getattr(proto, "short_descr", None) or getattr(proto, "name", "") or "").lower()
43
+ if target in name or target == "portal" or target in (getattr(obj, "short_descr", "") or "").lower():
44
+ portal = obj
45
+ break
46
+
47
+ if not portal:
48
+ return f"I see no {target} here."
49
+
50
+ flags = 0
51
+ proto = portal.prototype
52
+ values = getattr(proto, "value", [0, 0, 0, 0, 0])
53
+ if len(values) > 1 and isinstance(values[1], int):
54
+ flags = int(values[1])
55
+
56
+ if flags & EX_CLOSED:
57
+ return "The portal is closed."
58
+
59
+ dest_vnum = values[3] if len(values) > 3 else 0
60
+ dest = room_registry.get(int(dest_vnum))
61
+ if dest is None:
62
+ return "It doesn't seem to go anywhere."
63
+
64
+ # Move character
65
+ old_room = char.room
66
+ if char in old_room.people:
67
+ old_room.people.remove(char)
68
+ dest.people.append(char)
69
+ char.room = dest
70
+ char.wait = max(char.wait, 1)
71
+ return f"You enter the portal and arrive in {dest.name}."
mud/commands/notes.py ADDED
@@ -0,0 +1,44 @@
1
+ from mud.models.character import Character
2
+ from mud.notes import board_registry, get_board, save_board
3
+
4
+
5
+ def do_board(char: Character, args: str) -> str:
6
+ if not args:
7
+ if not board_registry:
8
+ return "No boards."
9
+ return "Boards: " + ", ".join(sorted(board_registry))
10
+ return "Huh?"
11
+
12
+
13
+ def do_note(char: Character, args: str) -> str:
14
+ if not args:
15
+ return "Note what?"
16
+ subcmd, *rest = args.split(None, 1)
17
+ rest_str = rest[0] if rest else ""
18
+ board = get_board("general")
19
+ if subcmd == "post":
20
+ if "|" not in rest_str:
21
+ return "Usage: note post <subject>|<text>"
22
+ subject, text = rest_str.split("|", 1)
23
+ board.post(char.name or "someone", subject.strip(), text.strip())
24
+ save_board(board)
25
+ return "Note posted."
26
+ elif subcmd == "list":
27
+ if not board.notes:
28
+ return "No notes."
29
+ lines = [
30
+ f"{i+1}: {note.subject} ({note.sender})"
31
+ for i, note in enumerate(board.notes)
32
+ ]
33
+ return "\n".join(lines)
34
+ elif subcmd == "read":
35
+ try:
36
+ index = int(rest_str.strip()) - 1
37
+ except ValueError:
38
+ return "Read which note?"
39
+ if index < 0 or index >= len(board.notes):
40
+ return "No such note."
41
+ note = board.notes[index]
42
+ return f"{note.subject}\n{note.text}"
43
+ else:
44
+ return "Huh?"
mud/commands/shop.py ADDED
@@ -0,0 +1,138 @@
1
+ """Shop command handlers."""
2
+
3
+ from mud.registry import shop_registry
4
+ from mud.models.character import Character
5
+ from mud.models.object import Object
6
+ from mud.models.constants import ItemType
7
+ from mud.math.c_compat import c_div
8
+
9
+
10
+ def _find_shopkeeper(char: Character):
11
+ for mob in getattr(char.room, "people", []):
12
+ proto = getattr(mob, "prototype", None)
13
+ if proto and proto.vnum in shop_registry:
14
+ return mob
15
+ return None
16
+
17
+
18
+ def _get_shop(keeper):
19
+ proto = getattr(keeper, "prototype", None)
20
+ if proto:
21
+ return shop_registry.get(proto.vnum)
22
+ return None
23
+
24
+
25
+ def _get_cost(keeper, obj: Object, *, buy: bool) -> int:
26
+ """Compute ROM-like shop price for an object.
27
+
28
+ Mirrors src/act_obj.c:get_cost:
29
+ - buy: base = obj.cost * profit_buy / 100
30
+ - sell: base = obj.cost * profit_sell / 100 if type accepted; otherwise 0
31
+ - inventory discount on sell when keeper already has same item:
32
+ - if existing copy has ITEM_INVENTORY → base /= 2
33
+ - else → base = base * 3 / 4
34
+ - wand/staff charge scaling: value[1]==0 → base/=4; else base = base * value[2] / value[1]
35
+ """
36
+ proto = obj.prototype
37
+ shop = _get_shop(keeper)
38
+ if not shop:
39
+ return 0
40
+ cost = 0
41
+ if buy:
42
+ cost = c_div(getattr(proto, "cost", 0) * shop.profit_buy, 100)
43
+ else:
44
+ # ensure shop buys this type
45
+ item_type = getattr(proto, "item_type", 0)
46
+ if shop.buy_types and item_type not in shop.buy_types:
47
+ return 0
48
+ cost = c_div(getattr(proto, "cost", 0) * shop.profit_sell, 100)
49
+ # inventory discount if keeper already has same item
50
+ for other in getattr(keeper, "inventory", []) or []:
51
+ op = getattr(other, "prototype", None)
52
+ if not op:
53
+ continue
54
+ if op is proto or (
55
+ getattr(op, "vnum", None) == getattr(proto, "vnum", None)
56
+ and (getattr(op, "short_descr", None) or "") == (getattr(proto, "short_descr", None) or "")
57
+ ):
58
+ # treat bit 1<<18 as ITEM_INVENTORY in this port
59
+ ITEM_INVENTORY = 1 << 18
60
+ if getattr(op, "extra_flags", 0) & ITEM_INVENTORY:
61
+ cost = c_div(cost, 2)
62
+ else:
63
+ cost = c_div(cost * 3, 4)
64
+ break
65
+
66
+ # Charge scaling for wand/staff
67
+ if getattr(proto, "item_type", 0) in (int(ItemType.WAND), int(ItemType.STAFF)):
68
+ vals = getattr(proto, "value", [0, 0, 0, 0, 0])
69
+ total = vals[1]
70
+ rem = vals[2]
71
+ if total == 0:
72
+ cost = c_div(cost, 4)
73
+ elif total > 0:
74
+ cost = c_div(cost * rem, total)
75
+ return max(0, int(cost))
76
+
77
+
78
+ def do_list(char: Character, args: str = "") -> str:
79
+ keeper = _find_shopkeeper(char)
80
+ if not keeper:
81
+ return "You can't do that here."
82
+ shop = _get_shop(keeper)
83
+ if not shop:
84
+ return "You can't do that here."
85
+ if not keeper.inventory:
86
+ return "The shop is out of stock."
87
+ items = []
88
+ for obj in keeper.inventory:
89
+ name = obj.short_descr or obj.name or "item"
90
+ price = _get_cost(keeper, obj, buy=True)
91
+ items.append(f"{name} {price} gold")
92
+ return "Items for sale: " + ", ".join(items)
93
+
94
+
95
+ def do_buy(char: Character, args: str) -> str:
96
+ if not args:
97
+ return "Buy what?"
98
+ keeper = _find_shopkeeper(char)
99
+ if not keeper:
100
+ return "You can't do that here."
101
+ shop = _get_shop(keeper)
102
+ if not shop:
103
+ return "You can't do that here."
104
+ name = args.lower()
105
+ for obj in list(keeper.inventory):
106
+ obj_name = (obj.short_descr or obj.name or "").lower()
107
+ if name in obj_name:
108
+ price = _get_cost(keeper, obj, buy=True)
109
+ if char.gold < price:
110
+ return "You can't afford that."
111
+ char.gold -= price
112
+ keeper.inventory.remove(obj)
113
+ char.add_object(obj)
114
+ return f"You buy {obj.short_descr or obj.name} for {price} gold."
115
+ return "The shopkeeper doesn't sell that."
116
+
117
+
118
+ def do_sell(char: Character, args: str) -> str:
119
+ if not args:
120
+ return "Sell what?"
121
+ keeper = _find_shopkeeper(char)
122
+ if not keeper:
123
+ return "You can't do that here."
124
+ shop = _get_shop(keeper)
125
+ if not shop:
126
+ return "You can't do that here."
127
+ name = args.lower()
128
+ for obj in list(char.inventory):
129
+ obj_name = (obj.short_descr or obj.name or "").lower()
130
+ if name in obj_name:
131
+ price = _get_cost(keeper, obj, buy=False)
132
+ if price <= 0:
133
+ return "The shopkeeper doesn't buy that."
134
+ char.gold += price
135
+ char.inventory.remove(obj)
136
+ keeper.inventory.append(obj)
137
+ return f"You sell {obj.short_descr or obj.name} for {price} gold."
138
+ return "You don't have that."
@@ -0,0 +1,34 @@
1
+ from __future__ import annotations
2
+
3
+ from mud.models.character import Character
4
+ from mud.models.social import social_registry, expand_placeholders
5
+
6
+
7
+ def perform_social(char: Character, name: str, arg: str) -> str:
8
+ social = social_registry.get(name.lower())
9
+ if social is None or char.room is None:
10
+ return "Huh?"
11
+ victim = None
12
+ if arg:
13
+ arg_lower = arg.lower()
14
+ for person in char.room.people:
15
+ if person is char:
16
+ continue
17
+ if getattr(person, "name", "").lower().startswith(arg_lower):
18
+ victim = person
19
+ break
20
+ if victim and victim is not char:
21
+ char.messages.append(expand_placeholders(social.char_found, char, victim))
22
+ char.room.broadcast(expand_placeholders(social.others_found, char, victim), exclude=char)
23
+ victim.messages.append(expand_placeholders(social.vict_found, char, victim))
24
+ elif arg and victim is char:
25
+ char.messages.append(expand_placeholders(social.char_auto, char))
26
+ char.room.broadcast(expand_placeholders(social.others_auto, char), exclude=char)
27
+ elif arg and not victim:
28
+ # ROM semantics: if an argument was provided but no victim is found,
29
+ # emit the "not found" message instead of the no-arg variant.
30
+ char.messages.append(expand_placeholders(social.not_found, char))
31
+ else:
32
+ char.messages.append(expand_placeholders(social.char_no_arg, char))
33
+ char.room.broadcast(expand_placeholders(social.others_no_arg, char), exclude=char)
34
+ return ""
mud/config.py ADDED
@@ -0,0 +1,59 @@
1
+ import os
2
+ from dotenv import load_dotenv
3
+
4
+ load_dotenv()
5
+
6
+ # Configuration for servers
7
+ DATABASE_URL = os.getenv("DATABASE_URL", "sqlite:///mud.db")
8
+ PORT = int(os.getenv("PORT", 5000))
9
+ HOST = os.getenv("HOST", "0.0.0.0")
10
+
11
+ # Comma separated list of allowed CORS origins
12
+ CORS_ORIGINS = [origin.strip() for origin in os.getenv("CORS_ORIGINS", "*").split(",")]
13
+
14
+ # ----- ROM tick cadence (PULSE constants) -----
15
+ # ROM defines PULSE_PER_SECOND=4 and PULSE_TICK=60*PULSE_PER_SECOND (see src/merc.h)
16
+ # Keep these values here so engine code can reference parity timings.
17
+ PULSE_PER_SECOND: int = 4
18
+
19
+ def get_pulse_tick() -> int:
20
+ """Return pulses per game tick hour (ROM PULSE_TICK).
21
+
22
+ Matches ROM's PULSE_TICK = 60 * PULSE_PER_SECOND.
23
+ """
24
+ scale = max(1, int(os.getenv("TIME_SCALE", os.getenv("MUD_TIME_SCALE", "1")) or 1))
25
+ # Allow in-test overrides via module variable as well
26
+ try:
27
+ from mud import config as _cfg # local import to avoid cycles
28
+ scale = max(scale, int(getattr(_cfg, "TIME_SCALE", 1)))
29
+ except Exception:
30
+ pass
31
+ base = 60 * PULSE_PER_SECOND
32
+ # Ensure at least 1 pulse per tick when scaled up
33
+ return max(1, base // scale)
34
+
35
+
36
+ def get_pulse_violence() -> int:
37
+ """Return pulses per violence update (ROM PULSE_VIOLENCE).
38
+
39
+ ROM sets PULSE_VIOLENCE = 3 * PULSE_PER_SECOND.
40
+ Honor TIME_SCALE in the same way as ticks by dividing the base.
41
+ """
42
+ scale = max(1, int(os.getenv("TIME_SCALE", os.getenv("MUD_TIME_SCALE", "1")) or 1))
43
+ try:
44
+ from mud import config as _cfg
45
+ scale = max(scale, int(getattr(_cfg, "TIME_SCALE", 1)))
46
+ except Exception:
47
+ pass
48
+ base = 3 * PULSE_PER_SECOND
49
+ return max(1, base // scale)
50
+
51
+ # Feature flags
52
+ COMBAT_USE_THAC0: bool = False
53
+
54
+ # Optional test-only time scaling (1 = real ROM cadence)
55
+ TIME_SCALE: int = 1
56
+
57
+ # When True, schedule weather/reset strictly on point pulses (ROM-like).
58
+ # Default False to preserve existing test expectations.
59
+ GAME_LOOP_STRICT_POINT: bool = False
mud/db/__init__.py ADDED
File without changes
mud/db/init.py ADDED
@@ -0,0 +1,27 @@
1
+ from mud.db.models import Base
2
+ from mud.db.session import engine
3
+ from mud.db.seed import create_test_account
4
+ import os
5
+
6
+
7
+ def initialize_database():
8
+ """Initialize the database with tables and seed data."""
9
+ # Create all tables
10
+ Base.metadata.create_all(engine)
11
+
12
+ # Create test admin account if it doesn't exist
13
+ create_test_account()
14
+
15
+ print(f"Database initialized at: {engine.url}")
16
+
17
+
18
+ def database_exists():
19
+ """Check if the database file exists."""
20
+ if str(engine.url).startswith('sqlite:///'):
21
+ db_path = str(engine.url).replace('sqlite:///', '')
22
+ return os.path.exists(db_path)
23
+ return True # For non-file databases
24
+
25
+
26
+ if __name__ == "__main__":
27
+ initialize_database()
@@ -0,0 +1,87 @@
1
+ """One-time migration script to populate the database from .are files."""
2
+ from mud.loaders import load_all_areas
3
+ from mud.world.linking import link_exits
4
+ from mud.registry import area_registry, room_registry, mob_registry, obj_registry
5
+ from mud.db.session import SessionLocal, engine
6
+ from mud.db import models
7
+
8
+
9
+ def migrate(area_list_path: str = "area/area.lst") -> None:
10
+ load_all_areas(area_list_path)
11
+ link_exits()
12
+ models.Base.metadata.create_all(engine)
13
+ session = SessionLocal()
14
+
15
+ area_map = {}
16
+ for area in area_registry.values():
17
+ db_area = models.Area(
18
+ vnum=area.vnum,
19
+ name=area.name,
20
+ min_vnum=area.min_vnum,
21
+ max_vnum=area.max_vnum,
22
+ )
23
+ session.add(db_area)
24
+ session.flush()
25
+ area_map[area] = db_area
26
+
27
+ room_map = {}
28
+ for room in room_registry.values():
29
+ db_room = models.Room(
30
+ vnum=room.vnum,
31
+ name=room.name,
32
+ description=room.description,
33
+ sector_type=room.sector_type,
34
+ room_flags=room.room_flags,
35
+ area=area_map.get(room.area),
36
+ )
37
+ session.add(db_room)
38
+ session.flush()
39
+ room_map[room] = db_room
40
+
41
+ for room in room_registry.values():
42
+ db_room = room_map[room]
43
+ for direction, exit_obj in enumerate(room.exits):
44
+ if exit_obj and exit_obj.vnum:
45
+ session.add(
46
+ models.Exit(
47
+ room=db_room,
48
+ direction=str(direction),
49
+ to_room_vnum=exit_obj.vnum,
50
+ )
51
+ )
52
+
53
+ for mob in mob_registry.values():
54
+ session.add(
55
+ models.MobPrototype(
56
+ vnum=mob.vnum,
57
+ name=mob.player_name,
58
+ short_desc=mob.short_descr,
59
+ long_desc=mob.long_descr,
60
+ level=mob.level,
61
+ alignment=mob.alignment,
62
+ )
63
+ )
64
+
65
+ for obj in obj_registry.values():
66
+ session.add(
67
+ models.ObjPrototype(
68
+ vnum=obj.vnum,
69
+ name=obj.name,
70
+ short_desc=obj.short_descr,
71
+ long_desc=obj.description,
72
+ item_type=obj.item_type,
73
+ flags=obj.extra_flags,
74
+ value0=obj.value[0] if len(obj.value) > 0 else None,
75
+ value1=obj.value[1] if len(obj.value) > 1 else None,
76
+ value2=obj.value[2] if len(obj.value) > 2 else None,
77
+ value3=obj.value[3] if len(obj.value) > 3 else None,
78
+ )
79
+ )
80
+
81
+ session.commit()
82
+ session.close()
83
+ print("✅ Migration complete")
84
+
85
+
86
+ if __name__ == "__main__":
87
+ migrate()
mud/db/migrations.py ADDED
@@ -0,0 +1,7 @@
1
+ from mud.db.models import Base
2
+ from mud.db.session import engine
3
+
4
+
5
+ def run_migrations() -> None:
6
+ Base.metadata.create_all(bind=engine)
7
+ print("✅ Migrations complete.")