synth-ai 0.2.4.dev4__py3-none-any.whl → 0.2.4.dev5__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 (104) hide show
  1. synth_ai/environments/examples/__init__.py +1 -0
  2. synth_ai/environments/examples/crafter_classic/__init__.py +8 -0
  3. synth_ai/environments/examples/crafter_classic/config_logging.py +111 -0
  4. synth_ai/environments/examples/crafter_classic/debug_translation.py +0 -0
  5. synth_ai/environments/examples/crafter_classic/engine.py +575 -0
  6. synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +63 -0
  7. synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +5 -0
  8. synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +74 -0
  9. synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +266 -0
  10. synth_ai/environments/examples/crafter_classic/environment.py +364 -0
  11. synth_ai/environments/examples/crafter_classic/taskset.py +233 -0
  12. synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +229 -0
  13. synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +298 -0
  14. synth_ai/environments/examples/crafter_custom/__init__.py +4 -0
  15. synth_ai/environments/examples/crafter_custom/crafter/__init__.py +7 -0
  16. synth_ai/environments/examples/crafter_custom/crafter/config.py +182 -0
  17. synth_ai/environments/examples/crafter_custom/crafter/constants.py +8 -0
  18. synth_ai/environments/examples/crafter_custom/crafter/engine.py +269 -0
  19. synth_ai/environments/examples/crafter_custom/crafter/env.py +266 -0
  20. synth_ai/environments/examples/crafter_custom/crafter/objects.py +418 -0
  21. synth_ai/environments/examples/crafter_custom/crafter/recorder.py +187 -0
  22. synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +119 -0
  23. synth_ai/environments/examples/crafter_custom/dataset_builder.py +373 -0
  24. synth_ai/environments/examples/crafter_custom/environment.py +312 -0
  25. synth_ai/environments/examples/crafter_custom/run_dataset.py +305 -0
  26. synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +156 -0
  27. synth_ai/environments/examples/enron/art_helpers/local_email_db.py +280 -0
  28. synth_ai/environments/examples/enron/art_helpers/types_enron.py +24 -0
  29. synth_ai/environments/examples/enron/engine.py +291 -0
  30. synth_ai/environments/examples/enron/environment.py +165 -0
  31. synth_ai/environments/examples/enron/taskset.py +112 -0
  32. synth_ai/environments/examples/minigrid/__init__.py +48 -0
  33. synth_ai/environments/examples/minigrid/engine.py +589 -0
  34. synth_ai/environments/examples/minigrid/environment.py +274 -0
  35. synth_ai/environments/examples/minigrid/environment_mapping.py +242 -0
  36. synth_ai/environments/examples/minigrid/puzzle_loader.py +416 -0
  37. synth_ai/environments/examples/minigrid/taskset.py +583 -0
  38. synth_ai/environments/examples/nethack/__init__.py +7 -0
  39. synth_ai/environments/examples/nethack/achievements.py +337 -0
  40. synth_ai/environments/examples/nethack/engine.py +738 -0
  41. synth_ai/environments/examples/nethack/environment.py +255 -0
  42. synth_ai/environments/examples/nethack/helpers/__init__.py +42 -0
  43. synth_ai/environments/examples/nethack/helpers/action_mapping.py +301 -0
  44. synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +401 -0
  45. synth_ai/environments/examples/nethack/helpers/observation_utils.py +433 -0
  46. synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +201 -0
  47. synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +268 -0
  48. synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +308 -0
  49. synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +430 -0
  50. synth_ai/environments/examples/nethack/taskset.py +323 -0
  51. synth_ai/environments/examples/red/__init__.py +7 -0
  52. synth_ai/environments/examples/red/config_logging.py +110 -0
  53. synth_ai/environments/examples/red/engine.py +693 -0
  54. synth_ai/environments/examples/red/engine_helpers/__init__.py +1 -0
  55. synth_ai/environments/examples/red/engine_helpers/memory_map.py +28 -0
  56. synth_ai/environments/examples/red/engine_helpers/reward_components.py +275 -0
  57. synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +142 -0
  58. synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +56 -0
  59. synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +283 -0
  60. synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +149 -0
  61. synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +137 -0
  62. synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +56 -0
  63. synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +330 -0
  64. synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +120 -0
  65. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +558 -0
  66. synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +312 -0
  67. synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +147 -0
  68. synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +246 -0
  69. synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +367 -0
  70. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +139 -0
  71. synth_ai/environments/examples/red/environment.py +235 -0
  72. synth_ai/environments/examples/red/taskset.py +77 -0
  73. synth_ai/environments/examples/sokoban/__init__.py +1 -0
  74. synth_ai/environments/examples/sokoban/engine.py +675 -0
  75. synth_ai/environments/examples/sokoban/engine_helpers/__init__.py +1 -0
  76. synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +656 -0
  77. synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +17 -0
  78. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +3 -0
  79. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +129 -0
  80. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +370 -0
  81. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +331 -0
  82. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +305 -0
  83. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +66 -0
  84. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +114 -0
  85. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +122 -0
  86. synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +394 -0
  87. synth_ai/environments/examples/sokoban/environment.py +228 -0
  88. synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +438 -0
  89. synth_ai/environments/examples/sokoban/puzzle_loader.py +311 -0
  90. synth_ai/environments/examples/sokoban/taskset.py +425 -0
  91. synth_ai/environments/examples/tictactoe/__init__.py +1 -0
  92. synth_ai/environments/examples/tictactoe/engine.py +368 -0
  93. synth_ai/environments/examples/tictactoe/environment.py +239 -0
  94. synth_ai/environments/examples/tictactoe/taskset.py +214 -0
  95. synth_ai/environments/examples/verilog/__init__.py +10 -0
  96. synth_ai/environments/examples/verilog/engine.py +328 -0
  97. synth_ai/environments/examples/verilog/environment.py +349 -0
  98. synth_ai/environments/examples/verilog/taskset.py +418 -0
  99. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/METADATA +1 -1
  100. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/RECORD +104 -6
  101. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/WHEEL +0 -0
  102. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/entry_points.txt +0 -0
  103. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/licenses/LICENSE +0 -0
  104. {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,367 @@
1
+ """
2
+ Screen analysis functions for Pokemon Red to provide rich textual descriptions
3
+ of what's actually visible on the game screen.
4
+ """
5
+
6
+ import numpy as np
7
+ from typing import Dict, Any, List, Tuple
8
+ import hashlib
9
+
10
+ # Define some common Pokemon Red screen colors (RGB values)
11
+ POKEMON_RED_COLORS = {
12
+ (255, 255, 255): "WHITE", # White/light backgrounds
13
+ (192, 192, 192): "LIGHT_GRAY", # Light gray
14
+ (128, 128, 128): "GRAY", # Gray
15
+ (64, 64, 64): "DARK_GRAY", # Dark gray
16
+ (0, 0, 0): "BLACK", # Black/dark
17
+ (248, 248, 248): "OFF_WHITE", # Slightly off white
18
+ (200, 200, 200): "SILVER", # Silver-ish
19
+ }
20
+
21
+
22
+ def analyze_screen_buffer(screen_buffer: np.ndarray) -> Dict[str, Any]:
23
+ """
24
+ Analyze the Pokemon Red screen buffer to extract meaningful information
25
+ about what's currently visible on screen.
26
+
27
+ Args:
28
+ screen_buffer: numpy array of shape (144, 160, 3) or (144, 160, 4) representing the screen
29
+
30
+ Returns:
31
+ Dictionary containing analysis of the screen content
32
+ """
33
+ if screen_buffer is None:
34
+ return {"error": "No screen buffer provided"}
35
+
36
+ # Convert RGBA to RGB if needed
37
+ if screen_buffer.shape[2] == 4:
38
+ rgb_buffer = screen_buffer[:, :, :3]
39
+ else:
40
+ rgb_buffer = screen_buffer
41
+
42
+ analysis = {
43
+ "screen_hash": hashlib.md5(screen_buffer.tobytes()).hexdigest()[:8],
44
+ "screen_type": detect_screen_type(rgb_buffer),
45
+ "text_content": extract_text_content(rgb_buffer),
46
+ "entities": detect_entities(rgb_buffer),
47
+ "ui_elements": detect_ui_elements(rgb_buffer),
48
+ "color_analysis": analyze_colors(rgb_buffer),
49
+ "ascii_representation": create_ascii_representation(rgb_buffer),
50
+ }
51
+
52
+ return analysis
53
+
54
+
55
+ def detect_screen_type(rgb_buffer: np.ndarray) -> str:
56
+ """
57
+ Determine what type of screen is being displayed based on visual patterns.
58
+ """
59
+ height, width = rgb_buffer.shape[:2]
60
+
61
+ # Check for common screen types based on color patterns and layout
62
+
63
+ # Check if mostly one color (like a menu background)
64
+ unique_colors = len(np.unique(rgb_buffer.reshape(-1, 3), axis=0))
65
+
66
+ if unique_colors < 8:
67
+ # Very few colors - could be simple UI but be more conservative about "MENU"
68
+ if is_text_box_screen(rgb_buffer):
69
+ return "TEXT_BOX"
70
+ else:
71
+ return "SIMPLE_UI" # Don't assume MENU, use neutral term
72
+
73
+ elif unique_colors > 50:
74
+ # Many colors - likely overworld or complex scene
75
+ return "OVERWORLD"
76
+
77
+ else:
78
+ # Medium complexity
79
+ if is_battle_screen(rgb_buffer):
80
+ return "BATTLE"
81
+ else:
82
+ return "GAME_SCREEN" # Use neutral term instead of assuming menu
83
+
84
+
85
+ def is_menu_screen(rgb_buffer: np.ndarray) -> bool:
86
+ """Check if this looks like a menu screen - made more conservative."""
87
+ # This function is now unused but keeping for potential future use
88
+ # The logic was too aggressive in detecting menus
89
+ height, width = rgb_buffer.shape[:2]
90
+
91
+ # Much more strict criteria for what constitutes a menu
92
+ # Look for very specific menu patterns like uniform background with text boxes
93
+ horizontal_consistency = 0
94
+ for row in range(height):
95
+ row_colors = rgb_buffer[row, :, :]
96
+ unique_in_row = len(np.unique(row_colors.reshape(-1, 3), axis=0))
97
+ if unique_in_row < 3: # Very consistent row color (stricter than before)
98
+ horizontal_consistency += 1
99
+
100
+ # Much higher threshold - only classify as menu if almost the entire screen is uniform
101
+ return horizontal_consistency > height * 0.8
102
+
103
+
104
+ def is_text_box_screen(rgb_buffer: np.ndarray) -> bool:
105
+ """Check if this looks like a text box is displayed."""
106
+ # Look for typical text box patterns - usually bottom portion has different colors
107
+ height, width = rgb_buffer.shape[:2]
108
+ bottom_quarter = rgb_buffer[height * 3 // 4 :, :, :]
109
+ top_quarter = rgb_buffer[: height // 4, :, :]
110
+
111
+ bottom_colors = len(np.unique(bottom_quarter.reshape(-1, 3), axis=0))
112
+ top_colors = len(np.unique(top_quarter.reshape(-1, 3), axis=0))
113
+
114
+ return bottom_colors > top_colors * 1.5
115
+
116
+
117
+ def is_battle_screen(rgb_buffer: np.ndarray) -> bool:
118
+ """Check if this looks like a battle screen."""
119
+ # Battle screens typically have more color variation
120
+ height, width = rgb_buffer.shape[:2]
121
+ unique_colors = len(np.unique(rgb_buffer.reshape(-1, 3), axis=0))
122
+ return unique_colors > 30
123
+
124
+
125
+ def extract_text_content(rgb_buffer: np.ndarray) -> List[str]:
126
+ """
127
+ Attempt to extract readable text from the screen.
128
+ This is simplified - real OCR would be more complex.
129
+ """
130
+ # For now, just analyze patterns that might indicate text
131
+ text_lines = []
132
+
133
+ height, width = rgb_buffer.shape[:2]
134
+
135
+ # Look for text-like patterns in bottom area (common location for text boxes)
136
+ text_area = rgb_buffer[height * 2 // 3 :, :, :]
137
+
138
+ # Simple heuristic: if there are alternating light/dark patterns, might be text
139
+ for row_idx in range(text_area.shape[0]):
140
+ row = text_area[row_idx, :, :]
141
+ # Check for patterns that might indicate text
142
+ brightness = np.mean(row, axis=1)
143
+ changes = np.diff(brightness > np.mean(brightness))
144
+ if np.sum(changes) > width // 10: # Lots of brightness changes might be text
145
+ text_lines.append(f"Text detected at row {height * 2 // 3 + row_idx}")
146
+
147
+ return text_lines
148
+
149
+
150
+ def detect_entities(rgb_buffer: np.ndarray) -> List[Dict[str, Any]]:
151
+ """
152
+ Detect entities like player character, NPCs, objects on screen.
153
+ """
154
+ entities = []
155
+
156
+ # This is a simplified approach - real entity detection would be more sophisticated
157
+ height, width = rgb_buffer.shape[:2]
158
+
159
+ # Look for small moving/distinct colored regions that might be characters
160
+ # Scan in a grid pattern
161
+ block_size = 16 # Pokemon Red typically uses 16x16 sprites
162
+
163
+ for y in range(0, height - block_size, block_size):
164
+ for x in range(0, width - block_size, block_size):
165
+ block = rgb_buffer[y : y + block_size, x : x + block_size, :]
166
+
167
+ # Check if this block has interesting characteristics
168
+ unique_colors_in_block = len(np.unique(block.reshape(-1, 3), axis=0))
169
+
170
+ if 3 <= unique_colors_in_block <= 8: # Sprite-like color count
171
+ avg_color = np.mean(block, axis=(0, 1))
172
+ entities.append(
173
+ {
174
+ "type": "SPRITE_LIKE",
175
+ "position": (x, y),
176
+ "avg_color": tuple(avg_color.astype(int)),
177
+ "color_count": unique_colors_in_block,
178
+ }
179
+ )
180
+
181
+ return entities
182
+
183
+
184
+ def detect_ui_elements(rgb_buffer: np.ndarray) -> Dict[str, Any]:
185
+ """
186
+ Detect UI elements like borders, windows, buttons.
187
+ """
188
+ height, width = rgb_buffer.shape[:2]
189
+
190
+ ui_elements = {"has_border": False, "windows": [], "button_like_areas": []}
191
+
192
+ # Check for borders (consistent colors around edges)
193
+ edges = [
194
+ rgb_buffer[0, :, :], # top edge
195
+ rgb_buffer[-1, :, :], # bottom edge
196
+ rgb_buffer[:, 0, :], # left edge
197
+ rgb_buffer[:, -1, :], # right edge
198
+ ]
199
+
200
+ for edge in edges:
201
+ unique_colors = len(np.unique(edge.reshape(-1, 3), axis=0))
202
+ if unique_colors < 3: # Very consistent edge color
203
+ ui_elements["has_border"] = True
204
+ break
205
+
206
+ return ui_elements
207
+
208
+
209
+ def analyze_colors(rgb_buffer: np.ndarray) -> Dict[str, Any]:
210
+ """
211
+ Analyze the color composition of the screen.
212
+ """
213
+ unique_colors, counts = np.unique(rgb_buffer.reshape(-1, 3), axis=0, return_counts=True)
214
+
215
+ # Sort by frequency
216
+ sorted_indices = np.argsort(counts)[::-1]
217
+ top_colors = unique_colors[sorted_indices[:10]] # Top 10 colors
218
+ top_counts = counts[sorted_indices[:10]]
219
+
220
+ total_pixels = rgb_buffer.shape[0] * rgb_buffer.shape[1]
221
+
222
+ color_analysis = {
223
+ "total_unique_colors": len(unique_colors),
224
+ "dominant_colors": [],
225
+ "color_complexity": "HIGH"
226
+ if len(unique_colors) > 50
227
+ else "MEDIUM"
228
+ if len(unique_colors) > 20
229
+ else "LOW",
230
+ }
231
+
232
+ for i, (color, count) in enumerate(zip(top_colors, top_counts)):
233
+ percentage = (count / total_pixels) * 100
234
+ color_analysis["dominant_colors"].append(
235
+ {
236
+ "rgb": tuple(color),
237
+ "percentage": round(percentage, 1),
238
+ "name": get_color_name(tuple(color)),
239
+ }
240
+ )
241
+
242
+ return color_analysis
243
+
244
+
245
+ def get_color_name(rgb_tuple: Tuple[int, int, int]) -> str:
246
+ """
247
+ Get a human-readable name for an RGB color.
248
+ """
249
+ # Find closest match in our color dictionary
250
+ min_distance = float("inf")
251
+ closest_name = "UNKNOWN"
252
+
253
+ for known_rgb, name in POKEMON_RED_COLORS.items():
254
+ distance = sum((a - b) ** 2 for a, b in zip(rgb_tuple, known_rgb))
255
+ if distance < min_distance:
256
+ min_distance = distance
257
+ closest_name = name
258
+
259
+ # If no close match, generate a descriptive name
260
+ if min_distance > 10000: # Threshold for "close enough"
261
+ r, g, b = rgb_tuple
262
+ if r > 200 and g > 200 and b > 200:
263
+ return "LIGHT"
264
+ elif r < 50 and g < 50 and b < 50:
265
+ return "DARK"
266
+ else:
267
+ return "MEDIUM"
268
+
269
+ return closest_name
270
+
271
+
272
+ def create_ascii_representation(rgb_buffer: np.ndarray) -> str:
273
+ """
274
+ Create a simplified ASCII representation of the screen.
275
+ """
276
+ height, width = rgb_buffer.shape[:2]
277
+
278
+ # Downsample to a reasonable ASCII size (e.g., 40x20)
279
+ ascii_height = 20
280
+ ascii_width = 40
281
+
282
+ ascii_chars = []
283
+
284
+ for row in range(ascii_height):
285
+ ascii_row = ""
286
+ for col in range(ascii_width):
287
+ # Map to original coordinates
288
+ orig_row = int((row / ascii_height) * height)
289
+ orig_col = int((col / ascii_width) * width)
290
+
291
+ # Get average brightness of this region
292
+ region = rgb_buffer[
293
+ orig_row : orig_row + height // ascii_height,
294
+ orig_col : orig_col + width // ascii_width,
295
+ :,
296
+ ]
297
+
298
+ brightness = np.mean(region)
299
+
300
+ # Convert brightness to ASCII character
301
+ if brightness > 200:
302
+ ascii_row += " " # Bright = space
303
+ elif brightness > 150:
304
+ ascii_row += "." # Medium-bright = dot
305
+ elif brightness > 100:
306
+ ascii_row += ":" # Medium = colon
307
+ elif brightness > 50:
308
+ ascii_row += "x" # Dark = x
309
+ else:
310
+ ascii_row += "#" # Very dark = hash
311
+
312
+ ascii_chars.append(ascii_row)
313
+
314
+ return "\n".join(ascii_chars)
315
+
316
+
317
+ def create_detailed_screen_description(screen_analysis: Dict[str, Any]) -> str:
318
+ """
319
+ Create a detailed text description of what's on screen based on the analysis.
320
+ """
321
+ description_lines = []
322
+
323
+ # Screen type and basic info
324
+ screen_type = screen_analysis.get("screen_type", "UNKNOWN")
325
+ description_lines.append(f"SCREEN TYPE: {screen_type}")
326
+
327
+ # Color analysis
328
+ color_info = screen_analysis.get("color_analysis", {})
329
+ complexity = color_info.get("color_complexity", "UNKNOWN")
330
+ description_lines.append(f"VISUAL COMPLEXITY: {complexity}")
331
+
332
+ # Dominant colors
333
+ if "dominant_colors" in color_info:
334
+ color_desc = "DOMINANT COLORS: "
335
+ top_3_colors = color_info["dominant_colors"][:3]
336
+ color_names = [f"{c['name']}({c['percentage']:.0f}%)" for c in top_3_colors]
337
+ color_desc += ", ".join(color_names)
338
+ description_lines.append(color_desc)
339
+
340
+ # Entities
341
+ entities = screen_analysis.get("entities", [])
342
+ if entities:
343
+ description_lines.append(f"DETECTED ENTITIES: {len(entities)} sprite-like objects")
344
+ # Describe a few entities
345
+ for i, entity in enumerate(entities[:3]):
346
+ pos = entity["position"]
347
+ description_lines.append(f" Entity {i + 1}: {entity['type']} at ({pos[0]}, {pos[1]})")
348
+
349
+ # Text content
350
+ text_content = screen_analysis.get("text_content", [])
351
+ if text_content:
352
+ description_lines.append("TEXT DETECTED:")
353
+ for text_line in text_content[:3]: # Show first 3 text detections
354
+ description_lines.append(f" {text_line}")
355
+
356
+ # UI elements
357
+ ui_elements = screen_analysis.get("ui_elements", {})
358
+ if ui_elements.get("has_border"):
359
+ description_lines.append("UI: Window/border detected")
360
+
361
+ # ASCII representation
362
+ ascii_repr = screen_analysis.get("ascii_representation", "")
363
+ if ascii_repr:
364
+ description_lines.append("\nASCII REPRESENTATION:")
365
+ description_lines.append(ascii_repr)
366
+
367
+ return "\n".join(description_lines)
@@ -0,0 +1,139 @@
1
+ from .memory_map import *
2
+ from typing import Dict, Any, List
3
+
4
+
5
+ def get_byte(memory, addr: int) -> int:
6
+ """Get single byte from memory"""
7
+ try:
8
+ return memory[addr]
9
+ except (IndexError, TypeError):
10
+ return 0
11
+
12
+
13
+ def get_word(memory, addr: int) -> int:
14
+ """Get 16-bit word from memory (little endian)"""
15
+ return get_byte(memory, addr) | (get_byte(memory, addr + 1) << 8)
16
+
17
+
18
+ def get_3byte_int(memory, addr: int) -> int:
19
+ """Get 24-bit integer from memory for XP values"""
20
+ return (
21
+ get_byte(memory, addr)
22
+ | (get_byte(memory, addr + 1) << 8)
23
+ | (get_byte(memory, addr + 2) << 16)
24
+ )
25
+
26
+
27
+ def get_bcd_3byte(memory, addr: int) -> int:
28
+ """Get 3-byte BCD (Binary Coded Decimal) value for money"""
29
+ byte1 = get_byte(memory, addr)
30
+ byte2 = get_byte(memory, addr + 1)
31
+ byte3 = get_byte(memory, addr + 2)
32
+ return (
33
+ (byte1 >> 4) * 100000
34
+ + (byte1 & 0xF) * 10000
35
+ + (byte2 >> 4) * 1000
36
+ + (byte2 & 0xF) * 100
37
+ + (byte3 >> 4) * 10
38
+ + (byte3 & 0xF)
39
+ )
40
+
41
+
42
+ def extract_party_pokemon(memory) -> List[Dict[str, Any]]:
43
+ """Extract detailed information for each Pokemon in party"""
44
+ party_count = get_byte(memory, PARTY_COUNT)
45
+ party = []
46
+
47
+ for i in range(party_count):
48
+ # Basic Pokemon info
49
+ species = get_byte(memory, PARTY_SPECIES + i)
50
+ level = get_byte(memory, PARTY_LEVELS + i)
51
+
52
+ # HP (2 bytes each Pokemon)
53
+ hp_current = get_word(memory, PARTY_HP_CURRENT + (i * 2))
54
+ hp_max = get_word(memory, PARTY_HP_MAX + (i * 2))
55
+
56
+ # XP (3 bytes each Pokemon)
57
+ xp = get_3byte_int(memory, PARTY_XP + (i * 3))
58
+
59
+ pokemon = {
60
+ "species_id": species,
61
+ "level": level,
62
+ "hp_current": hp_current,
63
+ "hp_max": hp_max,
64
+ "xp": xp,
65
+ "hp_percentage": round((hp_current / hp_max * 100) if hp_max > 0 else 0, 1),
66
+ }
67
+ party.append(pokemon)
68
+
69
+ return party
70
+
71
+
72
+ def extract_inventory(memory) -> List[Dict[str, Any]]:
73
+ """Extract inventory items"""
74
+ item_count = get_byte(memory, INVENTORY_COUNT)
75
+ inventory = []
76
+
77
+ for i in range(min(item_count, 20)): # Max 20 items
78
+ item_id = get_byte(memory, INVENTORY_START + (i * 2))
79
+ quantity = get_byte(memory, INVENTORY_START + (i * 2) + 1)
80
+
81
+ if item_id > 0: # Valid item
82
+ inventory.append({"item_id": item_id, "quantity": quantity})
83
+
84
+ return inventory
85
+
86
+
87
+ def extract_game_state(memory) -> Dict[str, Any]:
88
+ """Extract comprehensive game state from Game Boy memory"""
89
+ # Get party and inventory details
90
+ party = extract_party_pokemon(memory)
91
+ inventory = extract_inventory(memory)
92
+
93
+ # Get money
94
+ money = get_bcd_3byte(memory, MONEY)
95
+
96
+ # Basic game state
97
+ state = {
98
+ "map_id": get_byte(memory, MAP_ID),
99
+ "player_x": get_byte(memory, PLAYER_X),
100
+ "player_y": get_byte(memory, PLAYER_Y),
101
+ "badges": get_byte(memory, BADGE_FLAGS),
102
+ "in_battle": get_byte(memory, IN_BATTLE_FLAG) > 0,
103
+ "battle_outcome": get_byte(memory, BATTLE_OUTCOME),
104
+ "money": money,
105
+ "menu_state": get_byte(memory, MENU_STATE),
106
+ "warp_flag": get_byte(memory, WARP_FLAG),
107
+ "text_box_active": get_byte(memory, TEXT_BOX_ACTIVE) > 0,
108
+ # Detailed party and inventory
109
+ "party_count": len(party),
110
+ "party_pokemon": party,
111
+ "inventory_count": len(inventory),
112
+ "inventory_items": inventory,
113
+ # Legacy fields for compatibility (use first Pokemon if available)
114
+ "party_level": party[0]["level"] if party else 0,
115
+ "party_hp_current": party[0]["hp_current"] if party else 0,
116
+ "party_hp_max": party[0]["hp_max"] if party else 0,
117
+ "party_xp": party[0]["xp"] if party else 0,
118
+ "inventory_count_legacy": get_byte(memory, INVENTORY_COUNT),
119
+ }
120
+
121
+ return state
122
+
123
+
124
+ def get_badge_count(badges_byte: int) -> int:
125
+ """Count number of badges from bitfield"""
126
+ return bin(badges_byte).count("1")
127
+
128
+
129
+ def format_position(x: int, y: int, map_id: int) -> str:
130
+ """Format position as readable string"""
131
+ return f"Map{map_id:02X}:({x},{y})"
132
+
133
+
134
+ def format_hp_status(current: int, max_hp: int) -> str:
135
+ """Format HP as readable string"""
136
+ if max_hp == 0:
137
+ return "HP: Unknown"
138
+ percentage = (current / max_hp) * 100
139
+ return f"HP: {current}/{max_hp} ({percentage:.0f}%)"