synth-ai 0.2.12__py3-none-any.whl → 0.2.13.dev2__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.

Potentially problematic release.


This version of synth-ai might be problematic. Click here for more details.

Files changed (229) hide show
  1. examples/multi_step/configs/crafter_rl_outcome.toml +74 -0
  2. examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +186 -0
  3. examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +83 -0
  4. examples/multi_step/configs/crafter_rl_stepwise_simple.toml +78 -0
  5. examples/multi_step/crafter_rl_lora.md +51 -10
  6. examples/multi_step/sse_metrics_streaming_notes.md +357 -0
  7. examples/multi_step/task_app_config_notes.md +7 -1
  8. examples/swe/task_app/grpo_swe_mini.py +55 -26
  9. examples/swe/task_app/hosted/rollout.py +40 -0
  10. examples/swe/task_app/hosted/test_service.py +5 -6
  11. examples/task_apps/TESTING.md +275 -0
  12. examples/task_apps/__init__.py +0 -0
  13. examples/task_apps/crafter/__init__.py +0 -0
  14. examples/task_apps/crafter/task_app/__init__.py +2 -0
  15. examples/{warming_up_to_rl → task_apps/crafter}/task_app/grpo_crafter.py +21 -46
  16. examples/{warming_up_to_rl → task_apps/crafter}/task_app/grpo_crafter_task_app.py +1 -1
  17. examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/policy.py +60 -4
  18. examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/inference/openai_client.py +109 -45
  19. examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/policy_routes.py +67 -49
  20. examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/rollout.py +242 -193
  21. examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/test_service.py +5 -6
  22. examples/task_apps/dev/pokemon_emerald/__init__.py +2 -0
  23. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/README.md +811 -0
  24. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/__init__.py +120 -0
  25. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/action.py +160 -0
  26. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/memory.py +155 -0
  27. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/perception.py +69 -0
  28. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/planning.py +96 -0
  29. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/simple.py +1502 -0
  30. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/system_prompt.py +4 -0
  31. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/grab_map.py +68 -0
  32. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/manual.py +216 -0
  33. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/__init__.py +35 -0
  34. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emerald_utils.py +631 -0
  35. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emulator.py +1544 -0
  36. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/enums.py +1428 -0
  37. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/memory_reader.py +4848 -0
  38. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/types.py +41 -0
  39. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/utils.py +298 -0
  40. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pyproject.toml +95 -0
  41. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/run.py +204 -0
  42. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/__init__.py +0 -0
  43. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/app.py +2152 -0
  44. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/client.py +429 -0
  45. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/frame_server.py +155 -0
  46. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/README.md +78 -0
  47. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/__init__.py +0 -0
  48. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/run_tests.py +122 -0
  49. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_direct.py +76 -0
  50. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_prompts.py +413 -0
  51. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_battle_state_formatting.py +204 -0
  52. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection.py +133 -0
  53. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection_comprehensive.py +229 -0
  54. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_direct_agent_emulator.py +300 -0
  55. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_fps_adjustment_pytest.py +205 -0
  56. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_direct.py +200 -0
  57. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_transition.py +284 -0
  58. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_map_ground_truth_comparison.py +468 -0
  59. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_memory_map.py +575 -0
  60. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_server_map_validation.py +311 -0
  61. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_torchic_state.py +259 -0
  62. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/__init__.py +0 -0
  63. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/anticheat.py +372 -0
  64. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/checkpoint.py +296 -0
  65. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/error_handler.py +275 -0
  66. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/get_local_ip.py +22 -0
  67. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/helpers.py +44 -0
  68. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/llm_logger.py +514 -0
  69. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_formatter.py +415 -0
  70. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher.py +1763 -0
  71. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher_singleton.py +33 -0
  72. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_trimmer.py +106 -0
  73. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_visualizer.py +334 -0
  74. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/ocr_dialogue.py +1020 -0
  75. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/recording.py +188 -0
  76. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/state_formatter.py +1481 -0
  77. examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/vlm.py +862 -0
  78. examples/task_apps/dev/pokemon_emerald/modal_app.py +114 -0
  79. examples/task_apps/dev/pokemon_emerald/task_app/README.md +81 -0
  80. examples/task_apps/dev/pokemon_emerald/task_app/__init__.py +6 -0
  81. examples/task_apps/dev/pokemon_emerald/task_app/pokemon_emerald.py +685 -0
  82. examples/task_apps/enron/__init__.py +1 -0
  83. examples/task_apps/enron/eval_groq_qwen32.toml +16 -0
  84. examples/task_apps/enron/task_app/README.md +14 -0
  85. examples/task_apps/enron/task_app/__init__.py +1 -0
  86. examples/task_apps/enron/task_app/grpo_enron.py +906 -0
  87. examples/task_apps/enron/task_app/grpo_enron_task_app.py +146 -0
  88. examples/task_apps/enron/tests/__init__.py +2 -0
  89. examples/task_apps/enron/tests/conftest.py +115 -0
  90. examples/task_apps/enron/tests/integration/__init__.py +2 -0
  91. examples/task_apps/enron/tests/integration/test_enron_eval.py +177 -0
  92. examples/task_apps/enron/tests/integration/test_enron_rollout.py +135 -0
  93. examples/task_apps/enron/tests/unit/__init__.py +2 -0
  94. examples/task_apps/enron/tests/unit/test_enron_environment.py +126 -0
  95. examples/task_apps/math/__init__.py +0 -0
  96. examples/{rl/task_app → task_apps/math}/math_single_step.py +19 -10
  97. examples/task_apps/pokemon_battle/__init__.py +2 -0
  98. examples/task_apps/pokemon_battle/modal_app.py +104 -0
  99. examples/task_apps/pokemon_battle/task_app/README.md +68 -0
  100. examples/task_apps/pokemon_battle/task_app/__init__.py +6 -0
  101. examples/task_apps/pokemon_battle/task_app/pokemon_showdown.py +932 -0
  102. examples/task_apps/pokemon_red/README.md +357 -0
  103. examples/task_apps/pokemon_red/__init__.py +3 -0
  104. examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +225 -0
  105. examples/task_apps/pokemon_red/pallet_town_rl_config.toml +73 -0
  106. examples/task_apps/pokemon_red/task_app.py +606 -0
  107. examples/task_apps/pokemon_red/test_pallet_town_rewards.py +191 -0
  108. examples/task_apps/sokoban/README.md +307 -0
  109. examples/task_apps/sokoban/__init__.py +3 -0
  110. examples/task_apps/sokoban/eval_groq_qwen32.toml +16 -0
  111. examples/task_apps/sokoban/eval_openai_gpt5.toml +16 -0
  112. examples/task_apps/sokoban/task_app.py +1058 -0
  113. examples/task_apps/sokoban/tests/__init__.py +2 -0
  114. examples/task_apps/sokoban/tests/conftest.py +113 -0
  115. examples/task_apps/sokoban/tests/integration/__init__.py +2 -0
  116. examples/task_apps/sokoban/tests/integration/test_sokoban_eval.py +57 -0
  117. examples/task_apps/sokoban/tests/integration/test_sokoban_rollout.py +198 -0
  118. examples/task_apps/sokoban/tests/unit/__init__.py +2 -0
  119. examples/task_apps/sokoban/tests/unit/test_sokoban_environment.py +114 -0
  120. examples/task_apps/verilog/__init__.py +1 -0
  121. examples/task_apps/verilog/eval_groq_qwen32b.toml +20 -0
  122. examples/task_apps/verilog/task_app/README.md +12 -0
  123. examples/task_apps/verilog/task_app/__init__.py +1 -0
  124. examples/task_apps/verilog/task_app/grpo_verilog.py +931 -0
  125. examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +145 -0
  126. examples/task_apps/verilog/tests/__init__.py +2 -0
  127. examples/task_apps/verilog/tests/conftest.py +115 -0
  128. examples/task_apps/verilog/tests/integration/__init__.py +2 -0
  129. examples/task_apps/verilog/tests/integration/test_verilog_eval.py +179 -0
  130. examples/task_apps/verilog/tests/integration/test_verilog_rollout.py +55 -0
  131. examples/task_apps/verilog/tests/unit/__init__.py +2 -0
  132. examples/task_apps/verilog/tests/unit/test_verilog_scoring.py +118 -0
  133. examples/vlm/crafter_openai_vlm_agent.py +4 -4
  134. examples/vlm/run_crafter_vlm_benchmark.py +4 -4
  135. examples/warming_up_to_rl/configs/eval_stepwise_complex.toml +4 -2
  136. examples/warming_up_to_rl/configs/eval_stepwise_simple.toml +4 -2
  137. examples/warming_up_to_rl/run_eval.py +127 -18
  138. examples/workflows/__init__.py +0 -0
  139. examples/workflows/math_rl/__init__.py +0 -0
  140. examples/workflows/math_rl/download_dataset.py +80 -0
  141. synth_ai/__init__.py +41 -1
  142. synth_ai/api/train/builders.py +73 -29
  143. synth_ai/api/train/cli.py +12 -6
  144. synth_ai/api/train/configs/__init__.py +44 -0
  145. synth_ai/api/train/configs/rl.py +134 -0
  146. synth_ai/api/train/configs/sft.py +95 -0
  147. synth_ai/api/train/configs/shared.py +24 -0
  148. synth_ai/api/train/env_resolver.py +5 -2
  149. synth_ai/api/train/supported_algos.py +10 -5
  150. synth_ai/api/train/utils.py +7 -4
  151. synth_ai/cli/__init__.py +7 -51
  152. synth_ai/cli/_storage.py +4 -3
  153. synth_ai/cli/_validate_task_app.py +11 -0
  154. synth_ai/cli/balance.py +4 -3
  155. synth_ai/cli/calc.py +2 -2
  156. synth_ai/cli/demo.py +49 -43
  157. synth_ai/cli/legacy_root_backup.py +1 -1
  158. synth_ai/cli/rl_demo.py +86 -106
  159. synth_ai/cli/root.py +0 -97
  160. synth_ai/cli/task_apps.py +1710 -186
  161. synth_ai/demos/core/cli.py +121 -159
  162. synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +28 -16
  163. synth_ai/environments/examples/crafter_classic/environment.py +16 -0
  164. synth_ai/environments/examples/enron/engine.py +7 -2
  165. synth_ai/environments/examples/enron/environment.py +68 -0
  166. synth_ai/environments/examples/red/engine.py +27 -0
  167. synth_ai/environments/examples/red/engine_helpers/memory_map.py +7 -0
  168. synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_progression.py +477 -0
  169. synth_ai/environments/examples/red/engine_helpers/state_extraction.py +32 -0
  170. synth_ai/environments/examples/red/environment.py +60 -0
  171. synth_ai/environments/examples/sokoban/taskset.py +116 -0
  172. synth_ai/environments/examples/verilog/engine.py +30 -4
  173. synth_ai/evals/__init__.py +15 -0
  174. synth_ai/evals/client.py +82 -0
  175. synth_ai/evals/types.py +42 -0
  176. synth_ai/jobs/client.py +16 -4
  177. synth_ai/judge_schemas.py +127 -0
  178. synth_ai/py.typed +0 -0
  179. synth_ai/task/__init__.py +14 -5
  180. synth_ai/task/contracts.py +124 -38
  181. synth_ai/task/proxy.py +48 -56
  182. synth_ai/task/rubrics/__init__.py +53 -0
  183. synth_ai/task/rubrics/loaders.py +133 -0
  184. synth_ai/task/rubrics/models.py +57 -0
  185. synth_ai/task/rubrics/scoring.py +113 -0
  186. synth_ai/task/rubrics/strict.py +149 -0
  187. synth_ai/task/server.py +8 -7
  188. synth_ai/task/validators.py +269 -6
  189. synth_ai/tracing_v3/decorators.py +7 -3
  190. synth_ai/tracing_v3/replica_sync.py +4 -4
  191. synth_ai/tracing_v3/serialization.py +130 -0
  192. synth_ai/tracing_v3/trace_utils.py +317 -0
  193. synth_ai/tracing_v3/turso/native_manager.py +3 -3
  194. {synth_ai-0.2.12.dist-info → synth_ai-0.2.13.dev2.dist-info}/METADATA +4 -1
  195. {synth_ai-0.2.12.dist-info → synth_ai-0.2.13.dev2.dist-info}/RECORD +228 -89
  196. {synth_ai-0.2.12.dist-info → synth_ai-0.2.13.dev2.dist-info}/entry_points.txt +0 -1
  197. synth_ai/task/rubrics.py +0 -219
  198. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/README.md +0 -0
  199. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/README.md +0 -0
  200. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/__init__.py +0 -0
  201. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/branching.py +0 -0
  202. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/environment_routes.py +0 -0
  203. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/__init__.py +0 -0
  204. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/__init__.py +0 -0
  205. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/app.py +0 -0
  206. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/environment.py +0 -0
  207. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/react_agent.py +0 -0
  208. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/shared.py +0 -0
  209. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/tools.py +0 -0
  210. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/hosted_app.py +0 -0
  211. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/inference/__init__.py +0 -0
  212. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/main.py +0 -0
  213. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/registry.py +0 -0
  214. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/storage/__init__.py +0 -0
  215. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/storage/volume.py +0 -0
  216. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/test_agents.py +0 -0
  217. /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/utils.py +0 -0
  218. /examples/{rl/task_app → task_apps/math}/README.md +0 -0
  219. /examples/{rl/task_app → task_apps/math}/math_task_app.py +0 -0
  220. /examples/{rl → workflows/math_rl}/configs/eval_base_qwen.toml +0 -0
  221. /examples/{rl → workflows/math_rl}/configs/eval_rl_qwen.toml +0 -0
  222. /examples/{rl → workflows/math_rl}/configs/rl_from_base_qwen.toml +0 -0
  223. /examples/{rl → workflows/math_rl}/configs/rl_from_base_qwen17.toml +0 -0
  224. /examples/{rl → workflows/math_rl}/configs/rl_from_ft_qwen.toml +0 -0
  225. /examples/{rl → workflows/math_rl}/run_eval.py +0 -0
  226. /examples/{rl → workflows/math_rl}/run_rl_and_save.py +0 -0
  227. {synth_ai-0.2.12.dist-info → synth_ai-0.2.13.dev2.dist-info}/WHEEL +0 -0
  228. {synth_ai-0.2.12.dist-info → synth_ai-0.2.13.dev2.dist-info}/licenses/LICENSE +0 -0
  229. {synth_ai-0.2.12.dist-info → synth_ai-0.2.13.dev2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Singleton manager for MapStitcher to ensure all components use the same instance.
4
+ This prevents multiple MapStitcher instances from being created and ensures
5
+ consistent map data across the application.
6
+ """
7
+
8
+ import logging
9
+ from utils.map_stitcher import MapStitcher
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # The single global MapStitcher instance
14
+ _map_stitcher_instance = None
15
+
16
+ def get_instance():
17
+ """Get the singleton MapStitcher instance."""
18
+ global _map_stitcher_instance
19
+ if _map_stitcher_instance is None:
20
+ _map_stitcher_instance = MapStitcher()
21
+ logger.info(f"Created singleton MapStitcher with {len(_map_stitcher_instance.map_areas)} areas")
22
+ elif len(_map_stitcher_instance.map_areas) == 0:
23
+ # If we have no data, reload from cache file in case it was updated
24
+ _map_stitcher_instance.load_from_file()
25
+ if len(_map_stitcher_instance.map_areas) > 0:
26
+ logger.info(f"Reloaded MapStitcher from cache, now has {len(_map_stitcher_instance.map_areas)} areas")
27
+ return _map_stitcher_instance
28
+
29
+ def reset_instance():
30
+ """Reset the singleton instance (mainly for testing)."""
31
+ global _map_stitcher_instance
32
+ _map_stitcher_instance = None
33
+ logger.info("Reset MapStitcher singleton instance")
@@ -0,0 +1,106 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Map Trimming Utility
4
+
5
+ Removes unnecessary padding from maps - rows/columns that are all walls (#)
6
+ with no meaningful content should be trimmed.
7
+ """
8
+
9
+ def trim_map_padding(grid_dict):
10
+ """
11
+ Trim unnecessary padding from a map grid.
12
+
13
+ Args:
14
+ grid_dict: Dictionary mapping (x, y) to symbols
15
+
16
+ Returns:
17
+ Trimmed grid dictionary
18
+ """
19
+ if not grid_dict:
20
+ return grid_dict
21
+
22
+ # Get bounds
23
+ all_coords = list(grid_dict.keys())
24
+ if not all_coords:
25
+ return grid_dict
26
+
27
+ min_x = min(x for x, y in all_coords)
28
+ max_x = max(x for x, y in all_coords)
29
+ min_y = min(y for x, y in all_coords)
30
+ max_y = max(y for x, y in all_coords)
31
+
32
+ # Check each edge to see if it can be trimmed
33
+ # A row/column can be trimmed if it's all # or empty
34
+
35
+ # Check top rows
36
+ trim_top = 0
37
+ for y in range(min_y, max_y + 1):
38
+ row_values = [grid_dict.get((x, y), ' ') for x in range(min_x, max_x + 1)]
39
+ # Skip if all walls or empty
40
+ if all(v in ['#', ' ', None] for v in row_values):
41
+ trim_top += 1
42
+ else:
43
+ break
44
+
45
+ # Check bottom rows
46
+ trim_bottom = 0
47
+ for y in range(max_y, min_y - 1, -1):
48
+ row_values = [grid_dict.get((x, y), ' ') for x in range(min_x, max_x + 1)]
49
+ if all(v in ['#', ' ', None] for v in row_values):
50
+ trim_bottom += 1
51
+ else:
52
+ break
53
+
54
+ # Check left columns
55
+ trim_left = 0
56
+ for x in range(min_x, max_x + 1):
57
+ col_values = [grid_dict.get((x, y), ' ') for y in range(min_y, max_y + 1)]
58
+ if all(v in ['#', ' ', None] for v in col_values):
59
+ trim_left += 1
60
+ else:
61
+ break
62
+
63
+ # Check right columns
64
+ trim_right = 0
65
+ for x in range(max_x, min_x - 1, -1):
66
+ col_values = [grid_dict.get((x, y), ' ') for y in range(min_y, max_y + 1)]
67
+ if all(v in ['#', ' ', None] for v in col_values):
68
+ trim_right += 1
69
+ else:
70
+ break
71
+
72
+ # But keep at least one row of walls around the actual content
73
+ # Don't trim if it would remove actual room walls
74
+ if trim_top > 1:
75
+ trim_top -= 1 # Keep one row of walls
76
+ if trim_bottom > 1:
77
+ trim_bottom -= 1
78
+ if trim_left > 1:
79
+ trim_left -= 1
80
+ if trim_right > 1:
81
+ trim_right -= 1
82
+
83
+ # Create trimmed grid
84
+ trimmed = {}
85
+ new_min_x = min_x + trim_left
86
+ new_max_x = max_x - trim_right
87
+ new_min_y = min_y + trim_top
88
+ new_max_y = max_y - trim_bottom
89
+
90
+ for y in range(new_min_y, new_max_y + 1):
91
+ for x in range(new_min_x, new_max_x + 1):
92
+ if (x, y) in grid_dict:
93
+ # Adjust coordinates to start from 0
94
+ new_x = x - new_min_x
95
+ new_y = y - new_min_y
96
+ trimmed[(new_x, new_y)] = grid_dict[(x, y)]
97
+
98
+ return trimmed
99
+
100
+
101
+ def is_padding_row(row_values):
102
+ """Check if a row is just padding (all walls with no content)"""
103
+ # A padding row is one that's all # or empty spaces
104
+ # But not if it contains doors, NPCs, items, etc.
105
+ meaningful_symbols = ['.', 'D', 'N', 'T', 'P', 'S', 'W', '?', '~']
106
+ return not any(v in meaningful_symbols for v in row_values)
@@ -0,0 +1,334 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Map Stitching Visualization System
4
+
5
+ Creates visual representations of the stitched world map showing
6
+ connections between different areas, routes, towns, and buildings.
7
+ """
8
+
9
+ import logging
10
+ from typing import Dict, List, Tuple, Optional, Any, Set
11
+ from utils.map_stitcher import MapStitcher, WarpConnection, MapArea
12
+ from utils.map_formatter import format_map_for_display, get_symbol_legend
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ class MapVisualizer:
17
+ """Visualizes stitched map connections and layouts"""
18
+
19
+ def __init__(self, map_stitcher: MapStitcher):
20
+ self.stitcher = map_stitcher
21
+
22
+ def generate_world_map_summary(self) -> str:
23
+ """Generate a text summary of the entire stitched world"""
24
+ stats = self.stitcher.get_stats()
25
+ lines = [
26
+ "=== STITCHED WORLD MAP SUMMARY ===",
27
+ "",
28
+ f"📍 Total Areas Discovered: {stats['total_areas']}",
29
+ f" 🏠 Indoor Areas: {stats['indoor_areas']}",
30
+ f" 🌍 Outdoor Areas: {stats['outdoor_areas']}",
31
+ f"",
32
+ f"🔗 Total Connections: {stats['total_connections']}",
33
+ ]
34
+
35
+ # Show warp type breakdown
36
+ if stats['warp_types']:
37
+ lines.append(" Warp Types:")
38
+ for warp_type, count in stats['warp_types'].items():
39
+ lines.append(f" {warp_type}: {count}")
40
+
41
+ lines.extend([
42
+ "",
43
+ f"⭐ Most Visited: {stats.get('most_visited', 'None')}",
44
+ ""
45
+ ])
46
+
47
+ return "\n".join(lines)
48
+
49
+ def generate_area_connections_map(self, focus_area_id: Optional[int] = None) -> str:
50
+ """Generate a connection map showing how areas link together"""
51
+ lines = ["=== AREA CONNECTIONS MAP ===", ""]
52
+
53
+ if focus_area_id:
54
+ # Show connections for a specific area
55
+ area = self.stitcher.map_areas.get(focus_area_id)
56
+ if not area:
57
+ return "Area not found"
58
+
59
+ lines.append(f"🎯 FOCUS: {area.location_name} (ID: {focus_area_id:04X})")
60
+ lines.append("")
61
+
62
+ connections = self.stitcher.get_connected_areas(focus_area_id)
63
+ if connections:
64
+ lines.append("Connected Areas:")
65
+ for to_id, to_name, direction in connections:
66
+ direction_symbol = self._get_direction_symbol(direction)
67
+ lines.append(f" {direction_symbol} {to_name} (ID: {to_id:04X})")
68
+ else:
69
+ lines.append(" No connections found")
70
+ else:
71
+ # Show all areas and their connection counts
72
+ lines.append("All Areas (with connection counts):")
73
+ lines.append("")
74
+
75
+ for area_id, area in self.stitcher.map_areas.items():
76
+ connections = self.stitcher.get_connected_areas(area_id)
77
+ connection_count = len(connections)
78
+
79
+ area_type = "🏠" if area.location_name and "HOUSE" in area.location_name.upper() else "🌍"
80
+ lines.append(f"{area_type} {area.location_name} ({area_id:04X}) - {connection_count} connections")
81
+
82
+ if connections:
83
+ for to_id, to_name, direction in connections[:3]: # Show first 3
84
+ direction_symbol = self._get_direction_symbol(direction)
85
+ lines.append(f" {direction_symbol} {to_name}")
86
+ if len(connections) > 3:
87
+ lines.append(f" ... and {len(connections) - 3} more")
88
+
89
+ return "\n".join(lines)
90
+
91
+ def generate_route_network_map(self) -> str:
92
+ """Generate a network view of route connections"""
93
+ lines = ["=== ROUTE NETWORK MAP ===", ""]
94
+
95
+ # Group areas by type
96
+ routes = []
97
+ towns = []
98
+ buildings = []
99
+
100
+ for area_id, area in self.stitcher.map_areas.items():
101
+ name = area.location_name.upper() if area.location_name else "UNKNOWN"
102
+ if "ROUTE" in name:
103
+ routes.append((area_id, area))
104
+ elif any(keyword in name for keyword in ["TOWN", "CITY"]):
105
+ towns.append((area_id, area))
106
+ elif any(keyword in name for keyword in ["HOUSE", "ROOM", "CENTER", "MART", "GYM"]):
107
+ buildings.append((area_id, area))
108
+
109
+ if routes:
110
+ lines.append("🛤️ ROUTES:")
111
+ for area_id, area in sorted(routes, key=lambda x: x[1].location_name):
112
+ connections = self.stitcher.get_connected_areas(area_id)
113
+ connection_names = [name for _, name, _ in connections]
114
+ lines.append(f" {area.location_name} → {', '.join(connection_names[:3])}")
115
+
116
+ if towns:
117
+ lines.append("")
118
+ lines.append("🏘️ TOWNS & CITIES:")
119
+ for area_id, area in sorted(towns, key=lambda x: x[1].location_name):
120
+ connections = self.stitcher.get_connected_areas(area_id)
121
+ route_connections = [name for _, name, _ in connections if "ROUTE" in name.upper()]
122
+ if route_connections:
123
+ lines.append(f" {area.location_name} ↔ {', '.join(route_connections)}")
124
+ else:
125
+ lines.append(f" {area.location_name} (isolated)")
126
+
127
+ if buildings:
128
+ lines.append("")
129
+ lines.append("🏢 BUILDINGS:")
130
+ building_count_by_area = {}
131
+ for area_id, area in buildings:
132
+ # Group buildings by their location area
133
+ connections = self.stitcher.get_connected_areas(area_id)
134
+ parent_areas = [name for _, name, _ in connections
135
+ if not any(kw in name.upper() for kw in ["HOUSE", "ROOM", "CENTER"])]
136
+ parent = parent_areas[0] if parent_areas else "Unknown"
137
+ if parent not in building_count_by_area:
138
+ building_count_by_area[parent] = []
139
+ building_count_by_area[parent].append(area.location_name)
140
+
141
+ for parent, buildings_list in building_count_by_area.items():
142
+ lines.append(f" {parent}: {len(buildings_list)} buildings")
143
+ for building in buildings_list[:3]: # Show first 3
144
+ lines.append(f" • {building}")
145
+ if len(buildings_list) > 3:
146
+ lines.append(f" • ... and {len(buildings_list) - 3} more")
147
+
148
+ return "\n".join(lines)
149
+
150
+ def generate_warp_details_report(self) -> str:
151
+ """Generate detailed warp connection information"""
152
+ lines = ["=== WARP CONNECTIONS DETAILS ===", ""]
153
+
154
+ # Group connections by type
155
+ warp_by_type = {}
156
+ for conn in self.stitcher.warp_connections:
157
+ if conn.warp_type not in warp_by_type:
158
+ warp_by_type[conn.warp_type] = []
159
+ warp_by_type[conn.warp_type].append(conn)
160
+
161
+ for warp_type, connections in warp_by_type.items():
162
+ type_symbol = self._get_warp_type_symbol(warp_type)
163
+ lines.append(f"{type_symbol} {warp_type.upper()} CONNECTIONS ({len(connections)}):")
164
+
165
+ for conn in connections[:10]: # Show first 10 of each type
166
+ from_area = self.stitcher.map_areas.get(conn.from_map_id)
167
+ to_area = self.stitcher.map_areas.get(conn.to_map_id)
168
+
169
+ if from_area and to_area:
170
+ direction_symbol = self._get_direction_symbol(conn.direction)
171
+ lines.append(f" {direction_symbol} {from_area.location_name} → {to_area.location_name}")
172
+ lines.append(f" Position: ({conn.from_position[0]}, {conn.from_position[1]}) → ({conn.to_position[0]}, {conn.to_position[1]})")
173
+
174
+ if len(connections) > 10:
175
+ lines.append(f" ... and {len(connections) - 10} more {warp_type} connections")
176
+
177
+ lines.append("")
178
+
179
+ return "\n".join(lines)
180
+
181
+ def generate_navigation_hints(self, current_area_id: int, target_area_name: str) -> str:
182
+ """Generate navigation hints to reach a target area"""
183
+ lines = [f"=== NAVIGATION: TO {target_area_name.upper()} ===", ""]
184
+
185
+ current_area = self.stitcher.map_areas.get(current_area_id)
186
+ if not current_area:
187
+ return "Current area not found in stitched map"
188
+
189
+ # Find target area
190
+ target_area = None
191
+ target_id = None
192
+ for area_id, area in self.stitcher.map_areas.items():
193
+ if area.location_name and target_area_name.upper() in area.location_name.upper():
194
+ target_area = area
195
+ target_id = area_id
196
+ break
197
+
198
+ if not target_area:
199
+ lines.append(f"❌ Target area '{target_area_name}' not found in discovered areas")
200
+ lines.append("")
201
+ lines.append("Available areas:")
202
+ for area in self.stitcher.map_areas.values():
203
+ lines.append(f" • {area.location_name}")
204
+ return "\n".join(lines)
205
+
206
+ lines.append(f"📍 Current: {current_area.location_name}")
207
+ lines.append(f"🎯 Target: {target_area.location_name}")
208
+ lines.append("")
209
+
210
+ # Simple pathfinding - direct connections first
211
+ direct_connections = self.stitcher.get_connected_areas(current_area_id)
212
+ direct_targets = [conn for conn in direct_connections if conn[0] == target_id]
213
+
214
+ if direct_targets:
215
+ conn = direct_targets[0]
216
+ direction_symbol = self._get_direction_symbol(conn[2])
217
+ lines.append(f"🔗 Direct connection available!")
218
+ lines.append(f" {direction_symbol} Go {conn[2]} to reach {target_area.location_name}")
219
+ else:
220
+ # Look for paths through connected areas
221
+ paths = self._find_simple_paths(current_area_id, target_id, max_depth=3)
222
+ if paths:
223
+ lines.append("🗺️ Possible routes:")
224
+ for i, path in enumerate(paths[:3]): # Show first 3 paths
225
+ route_description = self._describe_path(path)
226
+ lines.append(f" Route {i+1}: {route_description}")
227
+ else:
228
+ lines.append("❓ No known path found (areas may not be connected yet)")
229
+
230
+ return "\n".join(lines)
231
+
232
+ def _find_simple_paths(self, start_id: int, target_id: int, max_depth: int = 3) -> List[List[int]]:
233
+ """Find simple paths between areas (basic BFS)"""
234
+ if start_id == target_id:
235
+ return [[start_id]]
236
+
237
+ visited = set()
238
+ queue = [(start_id, [start_id])]
239
+ paths = []
240
+
241
+ while queue and len(paths) < 5: # Limit to 5 paths
242
+ current_id, path = queue.pop(0)
243
+
244
+ if len(path) > max_depth:
245
+ continue
246
+
247
+ if current_id in visited:
248
+ continue
249
+
250
+ visited.add(current_id)
251
+
252
+ connections = self.stitcher.get_connected_areas(current_id)
253
+ for next_id, _, _ in connections:
254
+ if next_id == target_id:
255
+ paths.append(path + [next_id])
256
+ elif next_id not in visited and len(path) < max_depth:
257
+ queue.append((next_id, path + [next_id]))
258
+
259
+ return paths
260
+
261
+ def _describe_path(self, path: List[int]) -> str:
262
+ """Describe a path as a sequence of area names"""
263
+ if len(path) < 2:
264
+ return "No path"
265
+
266
+ descriptions = []
267
+ for i in range(len(path) - 1):
268
+ from_id = path[i]
269
+ to_id = path[i + 1]
270
+
271
+ from_area = self.stitcher.map_areas.get(from_id)
272
+ to_area = self.stitcher.map_areas.get(to_id)
273
+
274
+ if from_area and to_area:
275
+ # Find the connection direction
276
+ connections = self.stitcher.get_connected_areas(from_id)
277
+ direction = "→"
278
+ for conn_id, _, conn_dir in connections:
279
+ if conn_id == to_id:
280
+ direction = self._get_direction_symbol(conn_dir)
281
+ break
282
+
283
+ descriptions.append(f"{from_area.location_name} {direction} {to_area.location_name}")
284
+
285
+ return " → ".join(descriptions)
286
+
287
+ def _get_direction_symbol(self, direction: str) -> str:
288
+ """Get symbol for direction"""
289
+ symbols = {
290
+ "north": "⬆️", "south": "⬇️", "east": "➡️", "west": "⬅️",
291
+ "up": "🔼", "down": "🔽", "northeast": "↗️", "northwest": "↖️",
292
+ "southeast": "↘️", "southwest": "↙️"
293
+ }
294
+ return symbols.get(direction.lower(), "🔄")
295
+
296
+ def _get_warp_type_symbol(self, warp_type: str) -> str:
297
+ """Get symbol for warp type"""
298
+ symbols = {
299
+ "door": "🚪", "stairs": "🪜", "warp": "🌀",
300
+ "route_transition": "🛤️", "exit": "🚪"
301
+ }
302
+ return symbols.get(warp_type, "🔗")
303
+
304
+ def generate_complete_world_overview(self) -> str:
305
+ """Generate a complete overview combining all visualization types"""
306
+ lines = [
307
+ "=" * 60,
308
+ " POKEMON EMERALD WORLD MAP",
309
+ " (Stitched View)",
310
+ "=" * 60,
311
+ "",
312
+ self.generate_world_map_summary(),
313
+ "",
314
+ self.generate_route_network_map(),
315
+ "",
316
+ self.generate_area_connections_map(),
317
+ "",
318
+ self.generate_warp_details_report(),
319
+ "",
320
+ "=" * 60,
321
+ f"Generated from {len(self.stitcher.map_areas)} discovered areas",
322
+ "=" * 60
323
+ ]
324
+
325
+ return "\n".join(lines)
326
+
327
+ def create_map_visualizer(memory_reader) -> MapVisualizer:
328
+ """Create a map visualizer from a memory reader's stitcher"""
329
+ if hasattr(memory_reader, '_map_stitcher'):
330
+ return MapVisualizer(memory_reader._map_stitcher)
331
+ else:
332
+ # Create a standalone stitcher
333
+ stitcher = MapStitcher()
334
+ return MapVisualizer(stitcher)