balatrobot 0.7.4__py3-none-any.whl → 1.0.0__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.
@@ -0,0 +1,92 @@
1
+ Metadata-Version: 2.4
2
+ Name: balatrobot
3
+ Version: 1.0.0
4
+ Summary: API for developing Balatro bots
5
+ Project-URL: Homepage, https://github.com/coder/balatrobot
6
+ Project-URL: Issues, https://github.com/coder/balatrobot/issues
7
+ Project-URL: Repository, https://github.com/coder/balatrobot
8
+ Project-URL: Changelog, https://github.com/coder/balatrobot/blob/main/CHANGELOG.md
9
+ Author: stirby, giewev, besteon, phughesion
10
+ Author-email: S1M0N38 <bertolottosimone@gmail.com>
11
+ License-File: LICENSE
12
+ Classifier: Framework :: Pytest
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Requires-Python: >=3.13
17
+ Requires-Dist: httpx>=0.28.1
18
+ Description-Content-Type: text/markdown
19
+
20
+ <div align="center">
21
+ <h1>BalatroBot</h1>
22
+ <p align="center">
23
+ <a href="https://github.com/coder/balatrobot/releases">
24
+ <img alt="GitHub release" src="https://img.shields.io/github/v/release/coder/balatrobot?include_prereleases&sort=semver&style=for-the-badge&logo=github"/>
25
+ </a>
26
+ <a href="https://discord.gg/TPn6FYgGPv">
27
+ <img alt="Discord" src="https://img.shields.io/badge/discord-server?style=for-the-badge&logo=discord&logoColor=%23FFFFFF&color=%235865F2"/>
28
+ </a>
29
+ </p>
30
+ <div><img src="./docs/assets/balatrobot.svg" alt="balatrobot" width="170" height="170"></div>
31
+ <p><em>API for developing Balatro bots</em></p>
32
+ </div>
33
+
34
+ ---
35
+
36
+ BalatroBot is a mod for Balatro that serves a JSON-RPC 2.0 HTTP API, exposing game state and controls for external program interaction. The API provides endpoints for complete game control, including card selection, shop transactions, blind selection, and state management. External clients connect via HTTP POST to execute game actions programmatically.
37
+
38
+ > [!WARNING]
39
+ > **BalatroBot 1.0.0 introduces breaking changes:**
40
+ >
41
+ > - Now a CLI to start Balatro (no longer a Python client)
42
+ > - New JSON-RPC 2.0 protocol over HTTP/1.1
43
+ > - Updated endpoints and API structure
44
+ > - Removed game state logging functionality
45
+ >
46
+ > BalatroBot is now a Lua mod that exposes an API for programmatic game control.
47
+
48
+ ## 📚 Documentation
49
+
50
+ https://coder.github.io/balatrobot/
51
+
52
+ ## 🙏 Acknowledgments
53
+
54
+ This project is a fork of the original [balatrobot](https://github.com/besteon/balatrobot) repository. We would like to acknowledge and thank the original contributors who laid the foundation for this framework:
55
+
56
+ - [@phughesion](https://github.com/phughesion)
57
+ - [@besteon](https://github.com/besteon)
58
+ - [@giewev](https://github.com/giewev)
59
+
60
+ The original repository provided the initial API and botting framework that this project has evolved from. We appreciate their work in creating the foundation for Balatro bot development.
61
+
62
+ ## 🚀 Related Projects
63
+
64
+ <div style="display: flex; justify-content: center; gap: 2rem; flex-wrap: wrap;">
65
+ <figure style="text-align: center; margin: 0;">
66
+ <a href="https://coder.github.io/balatrobot/">
67
+ <img src="docs/assets/balatrobot.svg" alt="BalatroBot" width="92">
68
+ </a>
69
+ <figcaption>
70
+ <a href="https://coder.github.io/balatrobot/">BalatroBot</a><br>
71
+ <small>API for developing Balatro bots</small>
72
+ </figcaption>
73
+ </figure>
74
+ <figure style="text-align: center; margin: 0;">
75
+ <a href="https://coder.github.io/balatrollm/">
76
+ <img src="docs/assets/balatrollm.svg" alt="BalatroLLM" width="92">
77
+ </a>
78
+ <figcaption>
79
+ <a href="https://coder.github.io/balatrollm/">BalatroLLM</a><br>
80
+ <small>Play Balatro with LLMs</small>
81
+ </figcaption>
82
+ </figure>
83
+ <figure style="text-align: center; margin: 0;">
84
+ <a href="https://coder.github.io/balatrobench/">
85
+ <img src="docs/assets/balatrobench.svg" alt="BalatroBench" width="92">
86
+ </a>
87
+ <figcaption>
88
+ <a href="https://coder.github.io/balatrobench/">BalatroBench</a><br>
89
+ <small>Benchmark LLMs playing Balatro</small>
90
+ </figcaption>
91
+ </figure>
92
+ </div>
@@ -0,0 +1,14 @@
1
+ balatrobot/__init__.py,sha256=RctuAtU5oDkld946Zx4LAlCNoE7LKfWgNof_P4gMHQ0,215
2
+ balatrobot/__main__.py,sha256=HujoMPXZ1c2H9ECC6bXY5jmqy4pxlzFGD9U_hnAlSS4,134
3
+ balatrobot/cli.py,sha256=R5xzSWQzX2ONK_B-s6s2d6fY_fUNsH06dX5BEolmUx0,2339
4
+ balatrobot/config.py,sha256=7Ib4HYUEHpAqLn5ua4oRnmXWsFr1D-YILnR0gwxdCkI,2966
5
+ balatrobot/manager.py,sha256=hiMAFN7V16Bt0gTs_6WuBUZtpwHqCi_WSZx1sdCk6_E,4433
6
+ balatrobot/platforms/__init__.py,sha256=EiRON9FKkErzJF1E405D9COD5PrWkPCsG_FJbMOYpr4,1627
7
+ balatrobot/platforms/base.py,sha256=1mW1gwiyY8oXM0mhCDB6qC7e6O-kun-PzaED6a6umRU,1842
8
+ balatrobot/platforms/macos.py,sha256=bYwlpfWKAUpXsGdQFip-hq0D8m6xy6UjiE76FpKyyYY,1601
9
+ balatrobot/platforms/native.py,sha256=f5znPufm49LHKJrpeNBQLR80Vr2KnVN9CuQwjnwJuCY,3896
10
+ balatrobot-1.0.0.dist-info/METADATA,sha256=50SP88g62bOm52q647QoT8P7QGfw5e2ZlJwY-ai8JfM,3898
11
+ balatrobot-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ balatrobot-1.0.0.dist-info/entry_points.txt,sha256=TGBKXiQ57THrsIfLfz9vZgufbuwgJDLJsorJpxadmIM,51
13
+ balatrobot-1.0.0.dist-info/licenses/LICENSE,sha256=71EXhU7CSe-Cihhj_VVxLtgVnSOaavHqVoixPKtE7Bk,1064
14
+ balatrobot-1.0.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ balatrobot = balatrobot.cli:main
balatrobot/client.py DELETED
@@ -1,501 +0,0 @@
1
- """Main BalatroBot client for communicating with the game."""
2
-
3
- import json
4
- import logging
5
- import platform
6
- import re
7
- import shutil
8
- import socket
9
- import time
10
- from pathlib import Path
11
- from typing import Self
12
-
13
- from .enums import ErrorCode
14
- from .exceptions import (
15
- BalatroError,
16
- ConnectionFailedError,
17
- create_exception_from_error_response,
18
- )
19
- from .models import APIRequest
20
-
21
- logger = logging.getLogger(__name__)
22
-
23
-
24
- class BalatroClient:
25
- """Client for communicating with the BalatroBot game API.
26
-
27
- The client provides methods for game control, state management, and development tools
28
- including a checkpointing system for saving and loading game states.
29
-
30
- Attributes:
31
- host: Host address to connect to
32
- port: Port number to connect to
33
- timeout: Socket timeout in seconds
34
- buffer_size: Socket buffer size in bytes
35
- _socket: Socket connection to BalatroBot
36
- """
37
-
38
- host = "127.0.0.1"
39
- timeout = 300.0
40
- buffer_size = 65536
41
-
42
- def __init__(self, port: int = 12346, timeout: float | None = None):
43
- """Initialize BalatroBot client
44
-
45
- Args:
46
- port: Port number to connect to (default: 12346)
47
- timeout: Socket timeout in seconds (default: 300.0)
48
- """
49
- self.port = port
50
- self.timeout = timeout if timeout is not None else self.timeout
51
- self._socket: socket.socket | None = None
52
- self._connected = False
53
- self._message_buffer = b"" # Buffer for incomplete messages
54
-
55
- def _receive_complete_message(self) -> bytes:
56
- """Receive a complete message from the socket, handling message boundaries properly."""
57
- if not self._connected or not self._socket:
58
- raise ConnectionFailedError(
59
- "Socket not connected",
60
- error_code="E008",
61
- context={
62
- "connected": self._connected,
63
- "socket": self._socket is not None,
64
- },
65
- )
66
-
67
- # Check if we already have a complete message in the buffer
68
- while b"\n" not in self._message_buffer:
69
- try:
70
- chunk = self._socket.recv(self.buffer_size)
71
- except socket.timeout:
72
- raise ConnectionFailedError(
73
- "Socket timeout while receiving data",
74
- error_code="E008",
75
- context={
76
- "timeout": self.timeout,
77
- "buffer_size": len(self._message_buffer),
78
- },
79
- )
80
- except socket.error as e:
81
- raise ConnectionFailedError(
82
- f"Socket error while receiving: {e}",
83
- error_code="E008",
84
- context={"error": str(e), "buffer_size": len(self._message_buffer)},
85
- )
86
-
87
- if not chunk:
88
- raise ConnectionFailedError(
89
- "Connection closed by server",
90
- error_code="E008",
91
- context={"buffer_size": len(self._message_buffer)},
92
- )
93
- self._message_buffer += chunk
94
-
95
- # Extract the first complete message
96
- message_end = self._message_buffer.find(b"\n")
97
- complete_message = self._message_buffer[:message_end]
98
-
99
- # Update buffer to remove the processed message
100
- remaining_data = self._message_buffer[message_end + 1 :]
101
- self._message_buffer = remaining_data
102
-
103
- # Log any remaining data for debugging
104
- if remaining_data:
105
- logger.warning(f"Data remaining in buffer: {len(remaining_data)} bytes")
106
- logger.debug(f"Buffer preview: {remaining_data[:100]}...")
107
-
108
- return complete_message
109
-
110
- def __enter__(self) -> Self:
111
- """Enter context manager and connect to the game."""
112
- self.connect()
113
- return self
114
-
115
- def __exit__(self, exc_type, exc_val, exc_tb) -> None:
116
- """Exit context manager and disconnect from the game."""
117
- self.disconnect()
118
-
119
- def connect(self) -> None:
120
- """Connect to Balatro TCP server
121
-
122
- Raises:
123
- ConnectionFailedError: If not connected to the game
124
- """
125
- if self._connected:
126
- return
127
-
128
- logger.info(f"Connecting to BalatroBot API at {self.host}:{self.port}")
129
- try:
130
- self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
131
- self._socket.settimeout(self.timeout)
132
- self._socket.setsockopt(
133
- socket.SOL_SOCKET, socket.SO_RCVBUF, self.buffer_size
134
- )
135
- self._socket.connect((self.host, self.port))
136
- self._connected = True
137
- logger.info(
138
- f"Successfully connected to BalatroBot API at {self.host}:{self.port}"
139
- )
140
- except (socket.error, OSError) as e:
141
- logger.error(f"Failed to connect to {self.host}:{self.port}: {e}")
142
- raise ConnectionFailedError(
143
- f"Failed to connect to {self.host}:{self.port}",
144
- error_code="E008",
145
- context={"host": self.host, "port": self.port, "error": str(e)},
146
- ) from e
147
-
148
- def disconnect(self) -> None:
149
- """Disconnect from the BalatroBot game API."""
150
- if self._socket:
151
- logger.info(f"Disconnecting from BalatroBot API at {self.host}:{self.port}")
152
- self._socket.close()
153
- self._socket = None
154
- self._connected = False
155
- # Clear message buffer on disconnect
156
- self._message_buffer = b""
157
-
158
- def send_message(self, name: str, arguments: dict | None = None) -> dict:
159
- """Send JSON message to Balatro and receive response
160
-
161
- Args:
162
- name: Function name to call
163
- arguments: Function arguments
164
-
165
- Returns:
166
- Response from the game API
167
-
168
- Raises:
169
- ConnectionFailedError: If not connected to the game
170
- BalatroError: If the API returns an error
171
- """
172
- if arguments is None:
173
- arguments = {}
174
-
175
- if not self._connected or not self._socket:
176
- raise ConnectionFailedError(
177
- "Not connected to the game API",
178
- error_code="E008",
179
- context={
180
- "connected": self._connected,
181
- "socket": self._socket is not None,
182
- },
183
- )
184
-
185
- # Create and validate request
186
- request = APIRequest(name=name, arguments=arguments)
187
- logger.debug(f"Sending API request: {name}")
188
-
189
- try:
190
- # Start timing measurement
191
- start_time = time.perf_counter()
192
-
193
- # Send request
194
- message = request.model_dump_json() + "\n"
195
- self._socket.send(message.encode())
196
-
197
- # Receive response using improved message handling
198
- complete_message = self._receive_complete_message()
199
-
200
- # Decode and validate the message
201
- message_str = complete_message.decode().strip()
202
- logger.debug(f"Raw message length: {len(message_str)} characters")
203
- logger.debug(f"Message preview: {message_str[:100]}...")
204
-
205
- # Ensure the message is properly formatted JSON
206
- if not message_str:
207
- raise BalatroError(
208
- "Empty response received from game",
209
- error_code="E001",
210
- context={"raw_data_length": len(complete_message)},
211
- )
212
-
213
- response_data = json.loads(message_str)
214
-
215
- # Check for error response
216
- if "error" in response_data:
217
- logger.error(f"API request {name} failed: {response_data.get('error')}")
218
- raise create_exception_from_error_response(response_data)
219
-
220
- logger.debug(f"API request {name} completed successfully")
221
- return response_data
222
-
223
- except socket.timeout as e:
224
- # Calculate elapsed time and log timeout
225
- elapsed_time = time.perf_counter() - start_time
226
- logger.warning(
227
- f"Timeout on API request {name}: took {elapsed_time:.3f}s, "
228
- f"exceeded timeout of {self.timeout}s (port: {self.port})"
229
- )
230
- raise ConnectionFailedError(
231
- f"Socket timeout during communication: {e}",
232
- error_code="E008",
233
- context={"error": str(e), "elapsed_time": elapsed_time},
234
- ) from e
235
- except socket.error as e:
236
- logger.error(f"Socket error during API request {name}: {e}")
237
- raise ConnectionFailedError(
238
- f"Socket error during communication: {e}",
239
- error_code="E008",
240
- context={"error": str(e)},
241
- ) from e
242
- except json.JSONDecodeError as e:
243
- logger.error(f"Invalid JSON response from API request {name}: {e}")
244
- logger.error(f"Problematic message content: {message_str[:200]}...")
245
- logger.error(
246
- f"Message buffer state: {len(self._message_buffer)} bytes remaining"
247
- )
248
-
249
- # Clear the message buffer to prevent cascading errors
250
- if self._message_buffer:
251
- logger.warning("Clearing message buffer due to JSON parse error")
252
- self._message_buffer = b""
253
-
254
- raise BalatroError(
255
- f"Invalid JSON response from game: {e}",
256
- error_code="E001",
257
- context={"error": str(e), "message_preview": message_str[:100]},
258
- ) from e
259
-
260
- # Checkpoint Management Methods
261
-
262
- def _convert_windows_path_to_linux(self, windows_path: str) -> str:
263
- """Convert Windows path to Linux Steam Proton path if on Linux.
264
-
265
- Args:
266
- windows_path: Windows-style path (e.g., "C:/Users/.../Balatro/3/save.jkr")
267
-
268
- Returns:
269
- Converted path for Linux or original path for other platforms
270
- """
271
-
272
- if platform.system() == "Linux":
273
- # Match Windows drive letter and path (e.g., "C:/...", "D:\\...", "E:...")
274
- match = re.match(r"^([A-Z]):[\\/]*(.*)", windows_path, re.IGNORECASE)
275
- if match:
276
- # Replace drive letter with Linux Steam Proton prefix
277
- linux_prefix = str(
278
- Path(
279
- "~/.steam/steam/steamapps/compatdata/2379780/pfx/drive_c"
280
- ).expanduser()
281
- )
282
- # Normalize slashes and join with prefix
283
- rest_of_path = match.group(2).replace("\\", "/")
284
- return linux_prefix + "/" + rest_of_path
285
-
286
- return windows_path
287
-
288
- def get_save_info(self) -> dict:
289
- """Get the current save file location and profile information.
290
-
291
- Development tool for working with save files and checkpoints.
292
-
293
- Returns:
294
- Dictionary containing:
295
- - profile_path: Current profile path (e.g., "3")
296
- - save_directory: Full path to Love2D save directory
297
- - save_file_path: Full OS-specific path to save.jkr file
298
- - has_active_run: Whether a run is currently active
299
- - save_exists: Whether the save file exists
300
-
301
- Raises:
302
- BalatroError: If request fails
303
-
304
- Note:
305
- This is primarily for development and testing purposes.
306
- """
307
- save_info = self.send_message("get_save_info")
308
-
309
- # Convert Windows paths to Linux Steam Proton paths if needed
310
- if "save_file_path" in save_info and save_info["save_file_path"]:
311
- save_info["save_file_path"] = self._convert_windows_path_to_linux(
312
- save_info["save_file_path"]
313
- )
314
- if "save_directory" in save_info and save_info["save_directory"]:
315
- save_info["save_directory"] = self._convert_windows_path_to_linux(
316
- save_info["save_directory"]
317
- )
318
-
319
- return save_info
320
-
321
- def save_checkpoint(self, checkpoint_name: str | Path) -> Path:
322
- """Save the current save.jkr file as a checkpoint.
323
-
324
- Args:
325
- checkpoint_name: Either:
326
- - A checkpoint name (saved to checkpoints dir)
327
- - A full file path where the checkpoint should be saved
328
- - A directory path (checkpoint will be saved as 'save.jkr' inside it)
329
-
330
- Returns:
331
- Path to the saved checkpoint file
332
-
333
- Raises:
334
- BalatroError: If no save file exists or the destination path is invalid
335
- IOError: If file operations fail
336
- """
337
- # Get current save info
338
- save_info = self.get_save_info()
339
- if not save_info.get("save_exists"):
340
- raise BalatroError(
341
- "No save file exists to checkpoint", ErrorCode.INVALID_GAME_STATE
342
- )
343
-
344
- # Get the full save file path from API (already OS-specific)
345
- save_path = Path(save_info["save_file_path"])
346
- if not save_path.exists():
347
- raise BalatroError(
348
- f"Save file not found: {save_path}", ErrorCode.MISSING_GAME_OBJECT
349
- )
350
-
351
- # Normalize and interpret destination
352
- dest = Path(checkpoint_name).expanduser()
353
- # Treat paths without a .jkr suffix as directories
354
- if dest.suffix.lower() != ".jkr":
355
- raise BalatroError(
356
- f"Invalid checkpoint path provided: {dest}",
357
- ErrorCode.INVALID_PARAMETER,
358
- context={"path": str(dest), "reason": "Path does not end with .jkr"},
359
- )
360
-
361
- # Ensure destination directory exists
362
- try:
363
- dest.parent.mkdir(parents=True, exist_ok=True)
364
- except OSError as e:
365
- raise BalatroError(
366
- f"Invalid checkpoint path provided: {dest}",
367
- ErrorCode.INVALID_PARAMETER,
368
- context={"path": str(dest), "reason": str(e)},
369
- ) from e
370
-
371
- # Copy save file to checkpoint
372
- try:
373
- shutil.copy2(save_path, dest)
374
- except OSError as e:
375
- raise BalatroError(
376
- f"Failed to write checkpoint to: {dest}",
377
- ErrorCode.INVALID_PARAMETER,
378
- context={"path": str(dest), "reason": str(e)},
379
- ) from e
380
-
381
- return dest
382
-
383
- def prepare_save(self, source_path: str | Path) -> str:
384
- """Prepare a test save file for use with load_save.
385
-
386
- This copies a .jkr file from your test directory into Love2D's save directory
387
- in a temporary profile so it can be loaded with load_save().
388
-
389
- Args:
390
- source_path: Path to the .jkr save file to prepare
391
-
392
- Returns:
393
- The Love2D-relative path to use with load_save()
394
- (e.g., "checkpoint/save.jkr")
395
-
396
- Raises:
397
- BalatroError: If source file not found
398
- IOError: If file operations fail
399
- """
400
- source = Path(source_path)
401
- if not source.exists():
402
- raise BalatroError(
403
- f"Source save file not found: {source}", ErrorCode.MISSING_GAME_OBJECT
404
- )
405
-
406
- # Get save directory info
407
- save_info = self.get_save_info()
408
- if not save_info.get("save_directory"):
409
- raise BalatroError(
410
- "Cannot determine Love2D save directory", ErrorCode.INVALID_GAME_STATE
411
- )
412
-
413
- checkpoints_profile = "checkpoint"
414
- save_dir = Path(save_info["save_directory"])
415
- checkpoints_dir = save_dir / checkpoints_profile
416
- checkpoints_dir.mkdir(parents=True, exist_ok=True)
417
-
418
- # Copy the save file to the test profile
419
- dest_path = checkpoints_dir / "save.jkr"
420
- shutil.copy2(source, dest_path)
421
-
422
- # Return the Love2D-relative path
423
- return f"{checkpoints_profile}/save.jkr"
424
-
425
- def load_save(self, save_path: str | Path) -> dict:
426
- """Load a save file directly without requiring a game restart.
427
-
428
- This method loads a save file (in Love2D's save directory format) and starts
429
- a run from that save state. Unlike load_checkpoint which copies to the profile's
430
- save location and requires restart, this directly loads the save into the game.
431
-
432
- This is particularly useful for testing as it allows you to quickly jump to
433
- specific game states without manual setup.
434
-
435
- Args:
436
- save_path: Path to the save file relative to Love2D save directory
437
- (e.g., "3/save.jkr" for profile 3's save)
438
-
439
- Returns:
440
- Game state after loading the save
441
-
442
- Raises:
443
- BalatroError: If save file not found or loading fails
444
-
445
- Note:
446
- This is a development tool that bypasses normal game flow.
447
- Use with caution in production bots.
448
-
449
- Example:
450
- ```python
451
- # Load a profile's save directly
452
- game_state = client.load_save("3/save.jkr")
453
-
454
- # Or use with prepare_save for external files
455
- save_path = client.prepare_save("tests/fixtures/shop_state.jkr")
456
- game_state = client.load_save(save_path)
457
- ```
458
- """
459
- # Convert to string if Path object
460
- if isinstance(save_path, Path):
461
- save_path = str(save_path)
462
-
463
- # Send load_save request to API
464
- return self.send_message("load_save", {"save_path": save_path})
465
-
466
- def load_absolute_save(self, save_path: str | Path) -> dict:
467
- """Load a save from an absolute path. Takes a full path from the OS as a .jkr file and loads it into the game.
468
-
469
- Args:
470
- save_path: Path to the save file relative to Love2D save directory
471
- (e.g., "3/save.jkr" for profile 3's save)
472
-
473
- Returns:
474
- Game state after loading the save
475
- """
476
- love_save_path = self.prepare_save(save_path)
477
- return self.load_save(love_save_path)
478
-
479
- def screenshot(self, path: Path | None = None) -> Path:
480
- """
481
- Take a screenshot and save as both PNG and JPEG formats.
482
-
483
- Args:
484
- path: Optional path for PNG file. If provided, PNG will be moved to this location.
485
-
486
- Returns:
487
- Path to the PNG screenshot. JPEG is saved alongside with .jpg extension.
488
-
489
- Note:
490
- The response now includes both 'path' (PNG) and 'jpeg_path' (JPEG) keys.
491
- This method maintains backward compatibility by returning the PNG path.
492
- """
493
- screenshot_response = self.send_message("screenshot", {})
494
-
495
- if path is None:
496
- return Path(screenshot_response["path"])
497
- else:
498
- source_path = Path(screenshot_response["path"])
499
- dest_path = path
500
- shutil.move(source_path, dest_path)
501
- return dest_path