EmbeddedIFSession runs the hybrid interpreter in a daemon thread, bridged to the async MUD loop via threading.Event synchronization. .z3 files use the embedded path; other formats fall back to dfrotz. - MUD ZUI components: MudScreen (buffered output), MudInputStream (thread-safe input), MudFilesystem (quetzal saves), NullAudio - save/restore via QuetzalWriter/QuetzalParser and :: escape commands - state inspection: get_location_name(), get_room_objects() - error reporting for interpreter crashes - fix quetzal parser bit slice bug: _parse_stks used [0:3] (3 bits, max 7 locals) instead of [0:4] (4 bits, max 15) — Zork uses 15
114 lines
3.8 KiB
Python
114 lines
3.8 KiB
Python
"""Play interactive fiction games."""
|
|
|
|
import pathlib
|
|
|
|
from mudlib.commands import CommandDefinition, register
|
|
from mudlib.embedded_if_session import EmbeddedIFSession
|
|
from mudlib.if_session import IFSession, broadcast_to_spectators
|
|
from mudlib.player import Player
|
|
|
|
# Story files directory
|
|
_stories_dir = pathlib.Path(__file__).resolve().parents[3] / "content" / "stories"
|
|
|
|
# Map of game name -> file extension for lookup
|
|
_STORY_EXTENSIONS = (".z3", ".z5", ".z8", ".zblorb")
|
|
|
|
|
|
def _find_story(name: str) -> pathlib.Path | None:
|
|
"""Find a story file by name in content/stories/."""
|
|
# exact match first
|
|
for ext in _STORY_EXTENSIONS:
|
|
path = _stories_dir / f"{name}{ext}"
|
|
if path.exists():
|
|
return path
|
|
# prefix match (e.g. "zork" matches "zork1.z3")
|
|
if _stories_dir.exists():
|
|
for path in sorted(_stories_dir.iterdir()):
|
|
if path.stem.startswith(name) and path.suffix in _STORY_EXTENSIONS:
|
|
return path
|
|
return None
|
|
|
|
|
|
def _list_stories() -> list[str]:
|
|
"""Return available story names."""
|
|
if not _stories_dir.exists():
|
|
return []
|
|
return sorted(
|
|
p.stem for p in _stories_dir.iterdir() if p.suffix in _STORY_EXTENSIONS
|
|
)
|
|
|
|
|
|
async def cmd_play(player: Player, args: str) -> None:
|
|
"""Start playing an interactive fiction game."""
|
|
game_name = args.strip().lower()
|
|
if not game_name:
|
|
stories = _list_stories()
|
|
if stories:
|
|
names = ", ".join(stories)
|
|
await player.send(f"play what? available: {names}\r\n")
|
|
else:
|
|
await player.send("play what? (no stories installed)\r\n")
|
|
return
|
|
|
|
story_path = _find_story(game_name)
|
|
if not story_path:
|
|
stories = _list_stories()
|
|
if stories:
|
|
names = ", ".join(stories)
|
|
msg = f"no story '{game_name}'. available: {names}\r\n"
|
|
else:
|
|
msg = f"no story found for '{game_name}'.\r\n"
|
|
await player.send(msg)
|
|
return
|
|
|
|
# Ensure story_path is a Path object (for mocking compatibility)
|
|
if not isinstance(story_path, pathlib.Path):
|
|
story_path = pathlib.Path(story_path)
|
|
|
|
# Use embedded interpreter for z3 files, dfrotz for others
|
|
if story_path.suffix == ".z3":
|
|
try:
|
|
session = EmbeddedIFSession(player, str(story_path), game_name)
|
|
except (FileNotFoundError, OSError) as e:
|
|
await player.send(f"error starting game: {e}\r\n")
|
|
return
|
|
else:
|
|
session = IFSession(player, str(story_path), game_name)
|
|
|
|
try:
|
|
intro = await session.start()
|
|
except FileNotFoundError:
|
|
await session.stop()
|
|
await player.send("error: dfrotz not found. cannot play IF games.\r\n")
|
|
return
|
|
except OSError as e:
|
|
await session.stop()
|
|
await player.send(f"error starting game: {e}\r\n")
|
|
return
|
|
|
|
player.if_session = session
|
|
player.mode_stack.append("if")
|
|
|
|
await player.send("(type ::help for escape commands)\r\n")
|
|
|
|
# Check for saved game (both session types now support _do_restore)
|
|
if hasattr(session, "_do_restore") and session.save_path.exists():
|
|
await player.send("restoring saved game...\r\n")
|
|
restored_text = await session._do_restore()
|
|
if restored_text:
|
|
await player.send(restored_text + "\r\n")
|
|
# Broadcast restored text to spectators
|
|
spectator_msg = f"[{player.name}'s terminal]\r\n{restored_text}\r\n"
|
|
await broadcast_to_spectators(player, spectator_msg)
|
|
elif intro:
|
|
await player.send(intro + "\r\n")
|
|
# Broadcast intro to spectators
|
|
spectator_msg = f"[{player.name}'s terminal]\r\n{intro}\r\n"
|
|
await broadcast_to_spectators(player, spectator_msg)
|
|
|
|
|
|
register(
|
|
CommandDefinition(
|
|
"play", cmd_play, mode="normal", help="play an interactive fiction game"
|
|
)
|
|
)
|