Add save functionality to IF sessions
This commit is contained in:
parent
6ea82a8496
commit
e2bd260538
2 changed files with 110 additions and 0 deletions
|
|
@ -85,6 +85,30 @@ class IFSession:
|
||||||
self.process.terminate()
|
self.process.terminate()
|
||||||
await self.process.wait()
|
await self.process.wait()
|
||||||
|
|
||||||
|
async def _do_save(self) -> str:
|
||||||
|
"""Save game state to disk. Returns confirmation message."""
|
||||||
|
if not self.process or not self.process.stdin:
|
||||||
|
return "error: game not running"
|
||||||
|
|
||||||
|
# Ensure save directory exists
|
||||||
|
self._ensure_save_dir()
|
||||||
|
|
||||||
|
# Send "save" command to dfrotz
|
||||||
|
self.process.stdin.write(b"save\n")
|
||||||
|
await self.process.stdin.drain()
|
||||||
|
|
||||||
|
# Read filename prompt
|
||||||
|
await self._read_response()
|
||||||
|
|
||||||
|
# Send save file path
|
||||||
|
save_path_str = str(self.save_path)
|
||||||
|
self.process.stdin.write(f"{save_path_str}\n".encode())
|
||||||
|
await self.process.stdin.drain()
|
||||||
|
|
||||||
|
# Read confirmation
|
||||||
|
confirmation = await self._read_response()
|
||||||
|
return confirmation
|
||||||
|
|
||||||
async def _read_response(self) -> str:
|
async def _read_response(self) -> str:
|
||||||
"""Read dfrotz output until the '>' prompt appears."""
|
"""Read dfrotz output until the '>' prompt appears."""
|
||||||
if not self.process or not self.process.stdout:
|
if not self.process or not self.process.stdout:
|
||||||
|
|
|
||||||
|
|
@ -324,3 +324,89 @@ def test_ensure_save_dir_creates_directories(tmp_path):
|
||||||
# Now it should exist
|
# Now it should exist
|
||||||
assert expected_dir.exists()
|
assert expected_dir.exists()
|
||||||
assert expected_dir.is_dir()
|
assert expected_dir.is_dir()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_do_save_sends_save_command(tmp_path):
|
||||||
|
"""_do_save() sends save command and filepath to dfrotz."""
|
||||||
|
player = MagicMock()
|
||||||
|
player.name = "tester"
|
||||||
|
session = IFSession(player, "/path/to/zork.z5", "zork")
|
||||||
|
session._data_dir = tmp_path
|
||||||
|
|
||||||
|
# Mock process
|
||||||
|
mock_process = MagicMock()
|
||||||
|
mock_process.stdin = MagicMock()
|
||||||
|
mock_process.stdin.write = MagicMock()
|
||||||
|
mock_process.stdin.drain = AsyncMock()
|
||||||
|
mock_process.stdout = AsyncMock()
|
||||||
|
session.process = mock_process
|
||||||
|
|
||||||
|
# Simulate dfrotz responses: first the filename prompt, then confirmation
|
||||||
|
responses = [
|
||||||
|
b"Enter saved game to store: \n>",
|
||||||
|
b"Ok.\n>",
|
||||||
|
]
|
||||||
|
response_data = b"".join(responses)
|
||||||
|
|
||||||
|
async def read_side_effect(n):
|
||||||
|
nonlocal response_data
|
||||||
|
if response_data:
|
||||||
|
byte = response_data[:1]
|
||||||
|
response_data = response_data[1:]
|
||||||
|
return byte
|
||||||
|
return b""
|
||||||
|
|
||||||
|
mock_process.stdout.read = AsyncMock(side_effect=read_side_effect)
|
||||||
|
|
||||||
|
result = await session._do_save()
|
||||||
|
|
||||||
|
# Should have written "save\n" then the filepath
|
||||||
|
calls = mock_process.stdin.write.call_args_list
|
||||||
|
assert len(calls) == 2
|
||||||
|
assert calls[0][0][0] == b"save\n"
|
||||||
|
assert str(session.save_path) in calls[1][0][0].decode()
|
||||||
|
assert b"\n" in calls[1][0][0]
|
||||||
|
|
||||||
|
# Result should contain confirmation
|
||||||
|
assert "Ok" in result or "ok" in result.lower()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_do_save_creates_save_directory(tmp_path):
|
||||||
|
"""_do_save() ensures save directory exists."""
|
||||||
|
player = MagicMock()
|
||||||
|
player.name = "alice"
|
||||||
|
session = IFSession(player, "/path/to/story.z5", "story")
|
||||||
|
session._data_dir = tmp_path
|
||||||
|
|
||||||
|
# Mock process
|
||||||
|
mock_process = MagicMock()
|
||||||
|
mock_process.stdin = MagicMock()
|
||||||
|
mock_process.stdin.write = MagicMock()
|
||||||
|
mock_process.stdin.drain = AsyncMock()
|
||||||
|
mock_process.stdout = AsyncMock()
|
||||||
|
session.process = mock_process
|
||||||
|
|
||||||
|
# Simulate dfrotz responses
|
||||||
|
responses = [b"Enter saved game: \n>", b"Ok.\n>"]
|
||||||
|
response_data = b"".join(responses)
|
||||||
|
|
||||||
|
async def read_side_effect(n):
|
||||||
|
nonlocal response_data
|
||||||
|
if response_data:
|
||||||
|
byte = response_data[:1]
|
||||||
|
response_data = response_data[1:]
|
||||||
|
return byte
|
||||||
|
return b""
|
||||||
|
|
||||||
|
mock_process.stdout.read = AsyncMock(side_effect=read_side_effect)
|
||||||
|
|
||||||
|
# Directory shouldn't exist yet
|
||||||
|
expected_dir = tmp_path / "if_saves" / "alice"
|
||||||
|
assert not expected_dir.exists()
|
||||||
|
|
||||||
|
await session._do_save()
|
||||||
|
|
||||||
|
# Now it should exist
|
||||||
|
assert expected_dir.exists()
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue