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.
- synth_ai/environments/examples/__init__.py +1 -0
- synth_ai/environments/examples/crafter_classic/__init__.py +8 -0
- synth_ai/environments/examples/crafter_classic/config_logging.py +111 -0
- synth_ai/environments/examples/crafter_classic/debug_translation.py +0 -0
- synth_ai/environments/examples/crafter_classic/engine.py +575 -0
- synth_ai/environments/examples/crafter_classic/engine_deterministic_patch.py +63 -0
- synth_ai/environments/examples/crafter_classic/engine_helpers/action_map.py +5 -0
- synth_ai/environments/examples/crafter_classic/engine_helpers/serialization.py +74 -0
- synth_ai/environments/examples/crafter_classic/engine_serialization_patch_v3.py +266 -0
- synth_ai/environments/examples/crafter_classic/environment.py +364 -0
- synth_ai/environments/examples/crafter_classic/taskset.py +233 -0
- synth_ai/environments/examples/crafter_classic/trace_hooks_v3.py +229 -0
- synth_ai/environments/examples/crafter_classic/world_config_patch_simple.py +298 -0
- synth_ai/environments/examples/crafter_custom/__init__.py +4 -0
- synth_ai/environments/examples/crafter_custom/crafter/__init__.py +7 -0
- synth_ai/environments/examples/crafter_custom/crafter/config.py +182 -0
- synth_ai/environments/examples/crafter_custom/crafter/constants.py +8 -0
- synth_ai/environments/examples/crafter_custom/crafter/engine.py +269 -0
- synth_ai/environments/examples/crafter_custom/crafter/env.py +266 -0
- synth_ai/environments/examples/crafter_custom/crafter/objects.py +418 -0
- synth_ai/environments/examples/crafter_custom/crafter/recorder.py +187 -0
- synth_ai/environments/examples/crafter_custom/crafter/worldgen.py +119 -0
- synth_ai/environments/examples/crafter_custom/dataset_builder.py +373 -0
- synth_ai/environments/examples/crafter_custom/environment.py +312 -0
- synth_ai/environments/examples/crafter_custom/run_dataset.py +305 -0
- synth_ai/environments/examples/enron/art_helpers/email_search_tools.py +156 -0
- synth_ai/environments/examples/enron/art_helpers/local_email_db.py +280 -0
- synth_ai/environments/examples/enron/art_helpers/types_enron.py +24 -0
- synth_ai/environments/examples/enron/engine.py +291 -0
- synth_ai/environments/examples/enron/environment.py +165 -0
- synth_ai/environments/examples/enron/taskset.py +112 -0
- synth_ai/environments/examples/minigrid/__init__.py +48 -0
- synth_ai/environments/examples/minigrid/engine.py +589 -0
- synth_ai/environments/examples/minigrid/environment.py +274 -0
- synth_ai/environments/examples/minigrid/environment_mapping.py +242 -0
- synth_ai/environments/examples/minigrid/puzzle_loader.py +416 -0
- synth_ai/environments/examples/minigrid/taskset.py +583 -0
- synth_ai/environments/examples/nethack/__init__.py +7 -0
- synth_ai/environments/examples/nethack/achievements.py +337 -0
- synth_ai/environments/examples/nethack/engine.py +738 -0
- synth_ai/environments/examples/nethack/environment.py +255 -0
- synth_ai/environments/examples/nethack/helpers/__init__.py +42 -0
- synth_ai/environments/examples/nethack/helpers/action_mapping.py +301 -0
- synth_ai/environments/examples/nethack/helpers/nle_wrapper.py +401 -0
- synth_ai/environments/examples/nethack/helpers/observation_utils.py +433 -0
- synth_ai/environments/examples/nethack/helpers/recording_wrapper.py +201 -0
- synth_ai/environments/examples/nethack/helpers/trajectory_recorder.py +268 -0
- synth_ai/environments/examples/nethack/helpers/visualization/replay_viewer.py +308 -0
- synth_ai/environments/examples/nethack/helpers/visualization/visualizer.py +430 -0
- synth_ai/environments/examples/nethack/taskset.py +323 -0
- synth_ai/environments/examples/red/__init__.py +7 -0
- synth_ai/environments/examples/red/config_logging.py +110 -0
- synth_ai/environments/examples/red/engine.py +693 -0
- synth_ai/environments/examples/red/engine_helpers/__init__.py +1 -0
- synth_ai/environments/examples/red/engine_helpers/memory_map.py +28 -0
- synth_ai/environments/examples/red/engine_helpers/reward_components.py +275 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/__init__.py +142 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/adaptive_rewards.py +56 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/battle_rewards.py +283 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/composite_rewards.py +149 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/economy_rewards.py +137 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/efficiency_rewards.py +56 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/exploration_rewards.py +330 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/novelty_rewards.py +120 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_rewards.py +558 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/pokemon_rewards.py +312 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/social_rewards.py +147 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/story_rewards.py +246 -0
- synth_ai/environments/examples/red/engine_helpers/screen_analysis.py +367 -0
- synth_ai/environments/examples/red/engine_helpers/state_extraction.py +139 -0
- synth_ai/environments/examples/red/environment.py +235 -0
- synth_ai/environments/examples/red/taskset.py +77 -0
- synth_ai/environments/examples/sokoban/__init__.py +1 -0
- synth_ai/environments/examples/sokoban/engine.py +675 -0
- synth_ai/environments/examples/sokoban/engine_helpers/__init__.py +1 -0
- synth_ai/environments/examples/sokoban/engine_helpers/room_utils.py +656 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/__init__.py +17 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/__init__.py +3 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/boxoban_env.py +129 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/render_utils.py +370 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/room_utils.py +331 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env.py +305 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_fixed_targets.py +66 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_pull.py +114 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_two_player.py +122 -0
- synth_ai/environments/examples/sokoban/engine_helpers/vendored/envs/sokoban_env_variations.py +394 -0
- synth_ai/environments/examples/sokoban/environment.py +228 -0
- synth_ai/environments/examples/sokoban/generate_verified_puzzles.py +438 -0
- synth_ai/environments/examples/sokoban/puzzle_loader.py +311 -0
- synth_ai/environments/examples/sokoban/taskset.py +425 -0
- synth_ai/environments/examples/tictactoe/__init__.py +1 -0
- synth_ai/environments/examples/tictactoe/engine.py +368 -0
- synth_ai/environments/examples/tictactoe/environment.py +239 -0
- synth_ai/environments/examples/tictactoe/taskset.py +214 -0
- synth_ai/environments/examples/verilog/__init__.py +10 -0
- synth_ai/environments/examples/verilog/engine.py +328 -0
- synth_ai/environments/examples/verilog/environment.py +349 -0
- synth_ai/environments/examples/verilog/taskset.py +418 -0
- {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/METADATA +1 -1
- {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/RECORD +104 -6
- {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.4.dev4.dist-info → synth_ai-0.2.4.dev5.dist-info}/licenses/LICENSE +0 -0
- {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}%)"
|