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.
- examples/multi_step/configs/crafter_rl_outcome.toml +74 -0
- examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +186 -0
- examples/multi_step/configs/crafter_rl_stepwise_shaped.toml +83 -0
- examples/multi_step/configs/crafter_rl_stepwise_simple.toml +78 -0
- examples/multi_step/crafter_rl_lora.md +51 -10
- examples/multi_step/sse_metrics_streaming_notes.md +357 -0
- examples/multi_step/task_app_config_notes.md +7 -1
- examples/swe/task_app/grpo_swe_mini.py +55 -26
- examples/swe/task_app/hosted/rollout.py +40 -0
- examples/swe/task_app/hosted/test_service.py +5 -6
- examples/task_apps/TESTING.md +275 -0
- examples/task_apps/__init__.py +0 -0
- examples/task_apps/crafter/__init__.py +0 -0
- examples/task_apps/crafter/task_app/__init__.py +2 -0
- examples/{warming_up_to_rl → task_apps/crafter}/task_app/grpo_crafter.py +21 -46
- examples/{warming_up_to_rl → task_apps/crafter}/task_app/grpo_crafter_task_app.py +1 -1
- examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/policy.py +60 -4
- examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/inference/openai_client.py +109 -45
- examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/policy_routes.py +67 -49
- examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/rollout.py +242 -193
- examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/test_service.py +5 -6
- examples/task_apps/dev/pokemon_emerald/__init__.py +2 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/README.md +811 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/__init__.py +120 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/action.py +160 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/memory.py +155 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/perception.py +69 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/planning.py +96 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/simple.py +1502 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/agent/system_prompt.py +4 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/grab_map.py +68 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/manual.py +216 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/__init__.py +35 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emerald_utils.py +631 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/emulator.py +1544 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/enums.py +1428 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/memory_reader.py +4848 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/types.py +41 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pokemon_env/utils.py +298 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/pyproject.toml +95 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/run.py +204 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/__init__.py +0 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/app.py +2152 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/client.py +429 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/server/frame_server.py +155 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/README.md +78 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/__init__.py +0 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/run_tests.py +122 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_direct.py +76 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_agent_prompts.py +413 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_battle_state_formatting.py +204 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection.py +133 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_dialogue_detection_comprehensive.py +229 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_direct_agent_emulator.py +300 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_fps_adjustment_pytest.py +205 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_direct.py +200 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_house_to_outside_transition.py +284 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_map_ground_truth_comparison.py +468 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_memory_map.py +575 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_server_map_validation.py +311 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/tests/test_torchic_state.py +259 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/__init__.py +0 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/anticheat.py +372 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/checkpoint.py +296 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/error_handler.py +275 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/get_local_ip.py +22 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/helpers.py +44 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/llm_logger.py +514 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_formatter.py +415 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher.py +1763 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_stitcher_singleton.py +33 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_trimmer.py +106 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/map_visualizer.py +334 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/ocr_dialogue.py +1020 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/recording.py +188 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/state_formatter.py +1481 -0
- examples/task_apps/dev/pokemon_emerald/external/pokeagent-speedrun/utils/vlm.py +862 -0
- examples/task_apps/dev/pokemon_emerald/modal_app.py +114 -0
- examples/task_apps/dev/pokemon_emerald/task_app/README.md +81 -0
- examples/task_apps/dev/pokemon_emerald/task_app/__init__.py +6 -0
- examples/task_apps/dev/pokemon_emerald/task_app/pokemon_emerald.py +685 -0
- examples/task_apps/enron/__init__.py +1 -0
- examples/task_apps/enron/eval_groq_qwen32.toml +16 -0
- examples/task_apps/enron/task_app/README.md +14 -0
- examples/task_apps/enron/task_app/__init__.py +1 -0
- examples/task_apps/enron/task_app/grpo_enron.py +906 -0
- examples/task_apps/enron/task_app/grpo_enron_task_app.py +146 -0
- examples/task_apps/enron/tests/__init__.py +2 -0
- examples/task_apps/enron/tests/conftest.py +115 -0
- examples/task_apps/enron/tests/integration/__init__.py +2 -0
- examples/task_apps/enron/tests/integration/test_enron_eval.py +177 -0
- examples/task_apps/enron/tests/integration/test_enron_rollout.py +135 -0
- examples/task_apps/enron/tests/unit/__init__.py +2 -0
- examples/task_apps/enron/tests/unit/test_enron_environment.py +126 -0
- examples/task_apps/math/__init__.py +0 -0
- examples/{rl/task_app → task_apps/math}/math_single_step.py +19 -10
- examples/task_apps/pokemon_battle/__init__.py +2 -0
- examples/task_apps/pokemon_battle/modal_app.py +104 -0
- examples/task_apps/pokemon_battle/task_app/README.md +68 -0
- examples/task_apps/pokemon_battle/task_app/__init__.py +6 -0
- examples/task_apps/pokemon_battle/task_app/pokemon_showdown.py +932 -0
- examples/task_apps/pokemon_red/README.md +357 -0
- examples/task_apps/pokemon_red/__init__.py +3 -0
- examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +225 -0
- examples/task_apps/pokemon_red/pallet_town_rl_config.toml +73 -0
- examples/task_apps/pokemon_red/task_app.py +606 -0
- examples/task_apps/pokemon_red/test_pallet_town_rewards.py +191 -0
- examples/task_apps/sokoban/README.md +307 -0
- examples/task_apps/sokoban/__init__.py +3 -0
- examples/task_apps/sokoban/eval_groq_qwen32.toml +16 -0
- examples/task_apps/sokoban/eval_openai_gpt5.toml +16 -0
- examples/task_apps/sokoban/task_app.py +1058 -0
- examples/task_apps/sokoban/tests/__init__.py +2 -0
- examples/task_apps/sokoban/tests/conftest.py +113 -0
- examples/task_apps/sokoban/tests/integration/__init__.py +2 -0
- examples/task_apps/sokoban/tests/integration/test_sokoban_eval.py +57 -0
- examples/task_apps/sokoban/tests/integration/test_sokoban_rollout.py +198 -0
- examples/task_apps/sokoban/tests/unit/__init__.py +2 -0
- examples/task_apps/sokoban/tests/unit/test_sokoban_environment.py +114 -0
- examples/task_apps/verilog/__init__.py +1 -0
- examples/task_apps/verilog/eval_groq_qwen32b.toml +20 -0
- examples/task_apps/verilog/task_app/README.md +12 -0
- examples/task_apps/verilog/task_app/__init__.py +1 -0
- examples/task_apps/verilog/task_app/grpo_verilog.py +931 -0
- examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +145 -0
- examples/task_apps/verilog/tests/__init__.py +2 -0
- examples/task_apps/verilog/tests/conftest.py +115 -0
- examples/task_apps/verilog/tests/integration/__init__.py +2 -0
- examples/task_apps/verilog/tests/integration/test_verilog_eval.py +179 -0
- examples/task_apps/verilog/tests/integration/test_verilog_rollout.py +55 -0
- examples/task_apps/verilog/tests/unit/__init__.py +2 -0
- examples/task_apps/verilog/tests/unit/test_verilog_scoring.py +118 -0
- examples/vlm/crafter_openai_vlm_agent.py +4 -4
- examples/vlm/run_crafter_vlm_benchmark.py +4 -4
- examples/warming_up_to_rl/configs/eval_stepwise_complex.toml +4 -2
- examples/warming_up_to_rl/configs/eval_stepwise_simple.toml +4 -2
- examples/warming_up_to_rl/run_eval.py +127 -18
- examples/workflows/__init__.py +0 -0
- examples/workflows/math_rl/__init__.py +0 -0
- examples/workflows/math_rl/download_dataset.py +80 -0
- synth_ai/__init__.py +41 -1
- synth_ai/api/train/builders.py +73 -29
- synth_ai/api/train/cli.py +12 -6
- synth_ai/api/train/configs/__init__.py +44 -0
- synth_ai/api/train/configs/rl.py +134 -0
- synth_ai/api/train/configs/sft.py +95 -0
- synth_ai/api/train/configs/shared.py +24 -0
- synth_ai/api/train/env_resolver.py +5 -2
- synth_ai/api/train/supported_algos.py +10 -5
- synth_ai/api/train/utils.py +7 -4
- synth_ai/cli/__init__.py +7 -51
- synth_ai/cli/_storage.py +4 -3
- synth_ai/cli/_validate_task_app.py +11 -0
- synth_ai/cli/balance.py +4 -3
- synth_ai/cli/calc.py +2 -2
- synth_ai/cli/demo.py +49 -43
- synth_ai/cli/legacy_root_backup.py +1 -1
- synth_ai/cli/rl_demo.py +86 -106
- synth_ai/cli/root.py +0 -97
- synth_ai/cli/task_apps.py +1710 -186
- synth_ai/demos/core/cli.py +121 -159
- synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +28 -16
- synth_ai/environments/examples/crafter_classic/environment.py +16 -0
- synth_ai/environments/examples/enron/engine.py +7 -2
- synth_ai/environments/examples/enron/environment.py +68 -0
- synth_ai/environments/examples/red/engine.py +27 -0
- synth_ai/environments/examples/red/engine_helpers/memory_map.py +7 -0
- synth_ai/environments/examples/red/engine_helpers/reward_library/pallet_town_progression.py +477 -0
- synth_ai/environments/examples/red/engine_helpers/state_extraction.py +32 -0
- synth_ai/environments/examples/red/environment.py +60 -0
- synth_ai/environments/examples/sokoban/taskset.py +116 -0
- synth_ai/environments/examples/verilog/engine.py +30 -4
- synth_ai/evals/__init__.py +15 -0
- synth_ai/evals/client.py +82 -0
- synth_ai/evals/types.py +42 -0
- synth_ai/jobs/client.py +16 -4
- synth_ai/judge_schemas.py +127 -0
- synth_ai/py.typed +0 -0
- synth_ai/task/__init__.py +14 -5
- synth_ai/task/contracts.py +124 -38
- synth_ai/task/proxy.py +48 -56
- synth_ai/task/rubrics/__init__.py +53 -0
- synth_ai/task/rubrics/loaders.py +133 -0
- synth_ai/task/rubrics/models.py +57 -0
- synth_ai/task/rubrics/scoring.py +113 -0
- synth_ai/task/rubrics/strict.py +149 -0
- synth_ai/task/server.py +8 -7
- synth_ai/task/validators.py +269 -6
- synth_ai/tracing_v3/decorators.py +7 -3
- synth_ai/tracing_v3/replica_sync.py +4 -4
- synth_ai/tracing_v3/serialization.py +130 -0
- synth_ai/tracing_v3/trace_utils.py +317 -0
- synth_ai/tracing_v3/turso/native_manager.py +3 -3
- {synth_ai-0.2.12.dist-info → synth_ai-0.2.13.dev2.dist-info}/METADATA +4 -1
- {synth_ai-0.2.12.dist-info → synth_ai-0.2.13.dev2.dist-info}/RECORD +228 -89
- {synth_ai-0.2.12.dist-info → synth_ai-0.2.13.dev2.dist-info}/entry_points.txt +0 -1
- synth_ai/task/rubrics.py +0 -219
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/README.md +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/README.md +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/__init__.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/branching.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/environment_routes.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/__init__.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/__init__.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/app.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/environment.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/react_agent.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/shared.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/tools.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/hosted_app.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/inference/__init__.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/main.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/registry.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/storage/__init__.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/storage/volume.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/test_agents.py +0 -0
- /examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/utils.py +0 -0
- /examples/{rl/task_app → task_apps/math}/README.md +0 -0
- /examples/{rl/task_app → task_apps/math}/math_task_app.py +0 -0
- /examples/{rl → workflows/math_rl}/configs/eval_base_qwen.toml +0 -0
- /examples/{rl → workflows/math_rl}/configs/eval_rl_qwen.toml +0 -0
- /examples/{rl → workflows/math_rl}/configs/rl_from_base_qwen.toml +0 -0
- /examples/{rl → workflows/math_rl}/configs/rl_from_base_qwen17.toml +0 -0
- /examples/{rl → workflows/math_rl}/configs/rl_from_ft_qwen.toml +0 -0
- /examples/{rl → workflows/math_rl}/run_eval.py +0 -0
- /examples/{rl → workflows/math_rl}/run_rl_and_save.py +0 -0
- {synth_ai-0.2.12.dist-info → synth_ai-0.2.13.dev2.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.12.dist-info → synth_ai-0.2.13.dev2.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.12.dist-info → synth_ai-0.2.13.dev2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1481 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
State Formatter Utility
|
|
4
|
+
|
|
5
|
+
Converts comprehensive game state objects to formatted text for LLM prompts and debugging.
|
|
6
|
+
Centralizes all state formatting logic for consistency across agent modules.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import numpy as np
|
|
12
|
+
from PIL import Image
|
|
13
|
+
from utils.map_formatter import format_map_grid, format_map_for_llm, generate_dynamic_legend, format_tile_to_symbol
|
|
14
|
+
import base64
|
|
15
|
+
import io
|
|
16
|
+
import os, sys
|
|
17
|
+
from pokemon_env.enums import MetatileBehavior
|
|
18
|
+
from utils import state_formatter as sf
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger(__name__)
|
|
21
|
+
|
|
22
|
+
# Global location tracking - MapStitcher handles all persistent storage
|
|
23
|
+
CURRENT_LOCATION = None
|
|
24
|
+
LAST_LOCATION = None
|
|
25
|
+
LAST_TRANSITION = None # Stores transition coordinates
|
|
26
|
+
MAP_STITCHER_SAVE_CALLBACK = None # Callback to save map stitcher when location connections change
|
|
27
|
+
MAP_STITCHER_INSTANCE = None # Reference to the MapStitcher instance
|
|
28
|
+
|
|
29
|
+
def _get_location_connections_from_cache():
|
|
30
|
+
"""Read location connections from MapStitcher's cache file"""
|
|
31
|
+
try:
|
|
32
|
+
cache_file = '.pokeagent_cache/map_stitcher_data.json'
|
|
33
|
+
if os.path.exists(cache_file):
|
|
34
|
+
with open(cache_file, 'r') as f:
|
|
35
|
+
data = json.load(f)
|
|
36
|
+
return data.get('location_connections', {})
|
|
37
|
+
except Exception as e:
|
|
38
|
+
print(f"Failed to read location connections from cache: {e}")
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
def detect_dialogue_on_frame(screenshot_base64=None, frame_array=None):
|
|
42
|
+
"""
|
|
43
|
+
Detect if dialogue is visible on the game frame by analyzing the lower portion.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
screenshot_base64: Base64 encoded screenshot string
|
|
47
|
+
frame_array: numpy array of the frame (240x160 for GBA)
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
dict: {
|
|
51
|
+
'has_dialogue': bool,
|
|
52
|
+
'confidence': float (0-1),
|
|
53
|
+
'reason': str
|
|
54
|
+
}
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
# Convert base64 to image if needed
|
|
58
|
+
if screenshot_base64 and not frame_array:
|
|
59
|
+
image_data = base64.b64decode(screenshot_base64)
|
|
60
|
+
image = Image.open(io.BytesIO(image_data))
|
|
61
|
+
frame_array = np.array(image)
|
|
62
|
+
|
|
63
|
+
if frame_array is None:
|
|
64
|
+
return {'has_dialogue': False, 'confidence': 0.0, 'reason': 'No frame data'}
|
|
65
|
+
|
|
66
|
+
# GBA resolution is 240x160
|
|
67
|
+
height, width = frame_array.shape[:2]
|
|
68
|
+
|
|
69
|
+
# Dialogue typically appears in the bottom 40-50 pixels
|
|
70
|
+
dialogue_region = frame_array[height-50:, :] # Bottom 50 pixels
|
|
71
|
+
|
|
72
|
+
# Convert to grayscale for analysis
|
|
73
|
+
if len(dialogue_region.shape) == 3:
|
|
74
|
+
# Convert RGB to grayscale
|
|
75
|
+
gray = np.dot(dialogue_region[...,:3], [0.299, 0.587, 0.114]).astype(np.uint8)
|
|
76
|
+
else:
|
|
77
|
+
gray = dialogue_region
|
|
78
|
+
|
|
79
|
+
# Dialogue boxes in Pokemon are typically:
|
|
80
|
+
# 1. Have a distinct blue/white color scheme
|
|
81
|
+
# 2. Have high contrast text on background
|
|
82
|
+
# 3. Have consistent borders
|
|
83
|
+
|
|
84
|
+
# Check for dialogue box characteristics
|
|
85
|
+
# 1. Check for blue dialogue box (typical color range)
|
|
86
|
+
if len(dialogue_region.shape) == 3:
|
|
87
|
+
# Blue dialogue box detection (Pokemon dialogue boxes are often blue-ish)
|
|
88
|
+
blue_mask = (
|
|
89
|
+
(dialogue_region[:,:,2] > 100) & # High blue channel
|
|
90
|
+
(dialogue_region[:,:,2] > dialogue_region[:,:,0] * 1.2) & # More blue than red
|
|
91
|
+
(dialogue_region[:,:,2] > dialogue_region[:,:,1] * 1.2) # More blue than green
|
|
92
|
+
)
|
|
93
|
+
blue_percentage = np.sum(blue_mask) / blue_mask.size
|
|
94
|
+
|
|
95
|
+
# White/light regions (text areas)
|
|
96
|
+
white_mask = (
|
|
97
|
+
(dialogue_region[:,:,0] > 200) &
|
|
98
|
+
(dialogue_region[:,:,1] > 200) &
|
|
99
|
+
(dialogue_region[:,:,2] > 200)
|
|
100
|
+
)
|
|
101
|
+
white_percentage = np.sum(white_mask) / white_mask.size
|
|
102
|
+
else:
|
|
103
|
+
blue_percentage = 0
|
|
104
|
+
white_percentage = 0
|
|
105
|
+
|
|
106
|
+
# 2. Check for high contrast (text on background)
|
|
107
|
+
std_dev = np.std(gray)
|
|
108
|
+
|
|
109
|
+
# 3. Check for horizontal lines (dialogue box borders)
|
|
110
|
+
# Detect horizontal edges
|
|
111
|
+
vertical_diff = np.abs(np.diff(gray, axis=0))
|
|
112
|
+
horizontal_edges = np.sum(vertical_diff > 50) / vertical_diff.size
|
|
113
|
+
|
|
114
|
+
# 4. Check for consistent patterns (not random pixels)
|
|
115
|
+
# Calculate local variance to detect structured content
|
|
116
|
+
local_variance = []
|
|
117
|
+
for i in range(0, gray.shape[0]-5, 5):
|
|
118
|
+
for j in range(0, gray.shape[1]-5, 5):
|
|
119
|
+
patch = gray[i:i+5, j:j+5]
|
|
120
|
+
local_variance.append(np.var(patch))
|
|
121
|
+
|
|
122
|
+
avg_local_variance = np.mean(local_variance) if local_variance else 0
|
|
123
|
+
|
|
124
|
+
# Scoring system
|
|
125
|
+
confidence = 0.0
|
|
126
|
+
reasons = []
|
|
127
|
+
|
|
128
|
+
# Blue/white dialogue box detection
|
|
129
|
+
if blue_percentage > 0.3:
|
|
130
|
+
confidence += 0.3
|
|
131
|
+
reasons.append("blue dialogue box detected")
|
|
132
|
+
|
|
133
|
+
if white_percentage > 0.1 and white_percentage < 0.5:
|
|
134
|
+
confidence += 0.2
|
|
135
|
+
reasons.append("text area detected")
|
|
136
|
+
|
|
137
|
+
# High contrast for text
|
|
138
|
+
if std_dev > 30 and std_dev < 100:
|
|
139
|
+
confidence += 0.2
|
|
140
|
+
reasons.append("text contrast detected")
|
|
141
|
+
|
|
142
|
+
# Horizontal edges (box borders)
|
|
143
|
+
if horizontal_edges > 0.01 and horizontal_edges < 0.1:
|
|
144
|
+
confidence += 0.2
|
|
145
|
+
reasons.append("dialogue box borders detected")
|
|
146
|
+
|
|
147
|
+
# Structured content (not random)
|
|
148
|
+
if avg_local_variance > 100 and avg_local_variance < 2000:
|
|
149
|
+
confidence += 0.1
|
|
150
|
+
reasons.append("structured content")
|
|
151
|
+
|
|
152
|
+
# Determine if dialogue is present
|
|
153
|
+
has_dialogue = confidence >= 0.5
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
'has_dialogue': has_dialogue,
|
|
157
|
+
'confidence': min(confidence, 1.0),
|
|
158
|
+
'reason': ', '.join(reasons) if reasons else 'no dialogue indicators'
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
except Exception as e:
|
|
162
|
+
logger.warning(f"Failed to detect dialogue on frame: {e}")
|
|
163
|
+
return {'has_dialogue': False, 'confidence': 0.0, 'reason': f'error: {e}'}
|
|
164
|
+
|
|
165
|
+
def format_state(state_data, format_type="summary", include_debug_info=False, include_npcs=True):
|
|
166
|
+
"""
|
|
167
|
+
Format comprehensive state data into readable text.
|
|
168
|
+
|
|
169
|
+
Args:
|
|
170
|
+
state_data (dict): The comprehensive state from /state endpoint
|
|
171
|
+
format_type (str): "summary" for one-line summary, "detailed" for multi-line LLM format
|
|
172
|
+
include_debug_info (bool): Whether to include extra debug information (for detailed format)
|
|
173
|
+
include_npcs (bool): Whether to include NPC information in the state
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
str: Formatted state text
|
|
177
|
+
"""
|
|
178
|
+
if format_type == "summary":
|
|
179
|
+
return _format_state_summary(state_data)
|
|
180
|
+
elif format_type == "detailed":
|
|
181
|
+
return _format_state_detailed(state_data, include_debug_info, include_npcs)
|
|
182
|
+
else:
|
|
183
|
+
raise ValueError(f"Unknown format_type: {format_type}. Use 'summary' or 'detailed'")
|
|
184
|
+
|
|
185
|
+
def format_state_for_llm(state_data, include_debug_info=False, include_npcs=True):
|
|
186
|
+
"""
|
|
187
|
+
Format comprehensive state data into a readable context for the VLM.
|
|
188
|
+
|
|
189
|
+
Args:
|
|
190
|
+
state_data (dict): The comprehensive state from /state endpoint
|
|
191
|
+
include_debug_info (bool): Whether to include extra debug information
|
|
192
|
+
include_npcs (bool): Whether to include NPC information in the state
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
str: Formatted state context for LLM prompts
|
|
196
|
+
"""
|
|
197
|
+
return format_state(state_data, format_type="detailed", include_debug_info=include_debug_info, include_npcs=include_npcs)
|
|
198
|
+
|
|
199
|
+
def format_state_summary(state_data):
|
|
200
|
+
"""
|
|
201
|
+
Create a concise one-line summary of the current state for logging.
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
state_data (dict): The comprehensive state from /state endpoint
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
str: Concise state summary
|
|
208
|
+
"""
|
|
209
|
+
return format_state(state_data, format_type="summary")
|
|
210
|
+
|
|
211
|
+
def _format_state_summary(state_data):
|
|
212
|
+
"""
|
|
213
|
+
Internal function to create a concise one-line summary of the current state.
|
|
214
|
+
"""
|
|
215
|
+
player_data = state_data.get('player', {})
|
|
216
|
+
game_data = state_data.get('game', {})
|
|
217
|
+
|
|
218
|
+
summary_parts = []
|
|
219
|
+
|
|
220
|
+
# Player name (don't show during title sequence)
|
|
221
|
+
player_location = player_data.get('location', '')
|
|
222
|
+
if player_data.get('name') and player_location != 'TITLE_SEQUENCE':
|
|
223
|
+
summary_parts.append(f"Player: {player_data['name']}")
|
|
224
|
+
|
|
225
|
+
# Location
|
|
226
|
+
location = player_data.get('location')
|
|
227
|
+
if location:
|
|
228
|
+
summary_parts.append(f"Location: {location}")
|
|
229
|
+
|
|
230
|
+
# Position
|
|
231
|
+
position = player_data.get('position')
|
|
232
|
+
if position and isinstance(position, dict):
|
|
233
|
+
summary_parts.append(f"Pos: ({position.get('x', '?')}, {position.get('y', '?')})")
|
|
234
|
+
|
|
235
|
+
# Facing direction - removed as it's often unreliable
|
|
236
|
+
# facing = player_data.get('facing')
|
|
237
|
+
# if facing:
|
|
238
|
+
# summary_parts.append(f"Facing: {facing}")
|
|
239
|
+
|
|
240
|
+
# Game state
|
|
241
|
+
game_state = game_data.get('game_state')
|
|
242
|
+
if game_state:
|
|
243
|
+
summary_parts.append(f"State: {game_state}")
|
|
244
|
+
|
|
245
|
+
# Battle status
|
|
246
|
+
if game_data.get('is_in_battle'):
|
|
247
|
+
summary_parts.append("In Battle")
|
|
248
|
+
|
|
249
|
+
# Money
|
|
250
|
+
money = game_data.get('money')
|
|
251
|
+
if money is not None:
|
|
252
|
+
summary_parts.append(f"Money: ${money}")
|
|
253
|
+
|
|
254
|
+
# Party information
|
|
255
|
+
party_data = player_data.get('party')
|
|
256
|
+
if party_data:
|
|
257
|
+
party_size = len(party_data)
|
|
258
|
+
if party_size > 0:
|
|
259
|
+
# Get first Pokemon details
|
|
260
|
+
first_pokemon = party_data[0]
|
|
261
|
+
species = first_pokemon.get('species_name', 'Unknown')
|
|
262
|
+
level = first_pokemon.get('level', '?')
|
|
263
|
+
hp = first_pokemon.get('current_hp', '?')
|
|
264
|
+
max_hp = first_pokemon.get('max_hp', '?')
|
|
265
|
+
status = first_pokemon.get('status', 'OK')
|
|
266
|
+
|
|
267
|
+
summary_parts.append(f"Party: {party_size} pokemon")
|
|
268
|
+
summary_parts.append(f"Lead: {species} Lv{level} HP:{hp}/{max_hp} {status}")
|
|
269
|
+
|
|
270
|
+
# Pokedex information
|
|
271
|
+
pokedex_seen = game_data.get('pokedex_seen')
|
|
272
|
+
pokedex_caught = game_data.get('pokedex_caught')
|
|
273
|
+
if pokedex_seen is not None:
|
|
274
|
+
summary_parts.append(f"Pokedex: {pokedex_caught or 0} caught, {pokedex_seen} seen")
|
|
275
|
+
|
|
276
|
+
# Badges
|
|
277
|
+
badges = game_data.get('badges')
|
|
278
|
+
if badges:
|
|
279
|
+
if isinstance(badges, list):
|
|
280
|
+
badge_count = len(badges)
|
|
281
|
+
else:
|
|
282
|
+
badge_count = badges
|
|
283
|
+
summary_parts.append(f"Badges: {badge_count}")
|
|
284
|
+
|
|
285
|
+
# Items
|
|
286
|
+
item_count = game_data.get('item_count')
|
|
287
|
+
if item_count is not None:
|
|
288
|
+
summary_parts.append(f"Items: {item_count}")
|
|
289
|
+
|
|
290
|
+
# Game time
|
|
291
|
+
time_data = game_data.get('time')
|
|
292
|
+
if time_data and isinstance(time_data, (list, tuple)) and len(time_data) >= 3:
|
|
293
|
+
hours, minutes, seconds = time_data[:3]
|
|
294
|
+
summary_parts.append(f"Time: {hours:02d}:{minutes:02d}:{seconds:02d}")
|
|
295
|
+
|
|
296
|
+
# Dialog text (if any)
|
|
297
|
+
dialog_text = game_data.get('dialog_text')
|
|
298
|
+
dialogue_detected = game_data.get('dialogue_detected', {})
|
|
299
|
+
if dialog_text and dialogue_detected.get('has_dialogue', True):
|
|
300
|
+
# Only show dialogue if frame detection confirms it (or if detection wasn't run)
|
|
301
|
+
# Truncate dialog text to first 50 characters
|
|
302
|
+
dialog_preview = dialog_text[:50].replace('\n', ' ').strip()
|
|
303
|
+
if len(dialog_text) > 50:
|
|
304
|
+
dialog_preview += "..."
|
|
305
|
+
summary_parts.append(f"Dialog: {dialog_preview}")
|
|
306
|
+
|
|
307
|
+
# Progress context (if available)
|
|
308
|
+
progress_context = game_data.get('progress_context')
|
|
309
|
+
if progress_context:
|
|
310
|
+
badges_obtained = progress_context.get('badges_obtained', 0)
|
|
311
|
+
visited_locations = progress_context.get('visited_locations', [])
|
|
312
|
+
if badges_obtained > 0:
|
|
313
|
+
summary_parts.append(f"Progress: {badges_obtained} badges, {len(visited_locations)} locations")
|
|
314
|
+
|
|
315
|
+
return " | ".join(summary_parts) if summary_parts else "No state data"
|
|
316
|
+
|
|
317
|
+
def _format_state_detailed(state_data, include_debug_info=False, include_npcs=True):
|
|
318
|
+
"""
|
|
319
|
+
Internal function to create detailed multi-line state format for LLM prompts.
|
|
320
|
+
"""
|
|
321
|
+
context_parts = []
|
|
322
|
+
|
|
323
|
+
# Check both player and game sections for data
|
|
324
|
+
player_data = state_data.get('player', {})
|
|
325
|
+
game_data = state_data.get('game', {})
|
|
326
|
+
|
|
327
|
+
# Check if we're in battle to determine formatting mode
|
|
328
|
+
is_in_battle = game_data.get('is_in_battle', False) or game_data.get('in_battle', False)
|
|
329
|
+
|
|
330
|
+
if is_in_battle:
|
|
331
|
+
# BATTLE MODE: Focus on battle-relevant information
|
|
332
|
+
context_parts.append("=== BATTLE MODE ===")
|
|
333
|
+
context_parts.append("Currently in battle - map and dialogue information hidden")
|
|
334
|
+
|
|
335
|
+
# Battle information first
|
|
336
|
+
if 'battle_info' in game_data and game_data['battle_info']:
|
|
337
|
+
battle = game_data['battle_info']
|
|
338
|
+
context_parts.append("\n=== BATTLE STATUS ===")
|
|
339
|
+
|
|
340
|
+
# Battle type and context
|
|
341
|
+
battle_type = battle.get('battle_type', 'unknown')
|
|
342
|
+
context_parts.append(f"Battle Type: {battle_type.title()}")
|
|
343
|
+
if battle.get('is_capturable'):
|
|
344
|
+
context_parts.append("🟢 Wild Pokémon - CAN BE CAPTURED")
|
|
345
|
+
if battle.get('can_escape'):
|
|
346
|
+
context_parts.append("🟡 Can escape from battle")
|
|
347
|
+
|
|
348
|
+
# Player's active Pokémon
|
|
349
|
+
if 'player_pokemon' in battle and battle['player_pokemon']:
|
|
350
|
+
player_pkmn = battle['player_pokemon']
|
|
351
|
+
context_parts.append(f"\n--- YOUR POKÉMON ---")
|
|
352
|
+
context_parts.append(f"{player_pkmn.get('nickname', player_pkmn.get('species', 'Unknown'))} (Lv.{player_pkmn.get('level', '?')})")
|
|
353
|
+
|
|
354
|
+
# Health display with percentage
|
|
355
|
+
current_hp = player_pkmn.get('current_hp', 0)
|
|
356
|
+
max_hp = player_pkmn.get('max_hp', 1)
|
|
357
|
+
hp_pct = player_pkmn.get('hp_percentage', 0)
|
|
358
|
+
health_bar = "🟢" if hp_pct > 50 else "🟡" if hp_pct > 25 else "🔴"
|
|
359
|
+
context_parts.append(f" HP: {current_hp}/{max_hp} ({hp_pct}%) {health_bar}")
|
|
360
|
+
|
|
361
|
+
# Status condition
|
|
362
|
+
status = player_pkmn.get('status', 'Normal')
|
|
363
|
+
if status != 'Normal':
|
|
364
|
+
context_parts.append(f" Status: {status}")
|
|
365
|
+
|
|
366
|
+
# Types
|
|
367
|
+
types = player_pkmn.get('types', [])
|
|
368
|
+
if types:
|
|
369
|
+
context_parts.append(f" Type: {'/'.join(types)}")
|
|
370
|
+
|
|
371
|
+
# Available moves with PP
|
|
372
|
+
moves = player_pkmn.get('moves', [])
|
|
373
|
+
move_pp = player_pkmn.get('move_pp', [])
|
|
374
|
+
if moves:
|
|
375
|
+
context_parts.append(f" Moves:")
|
|
376
|
+
for i, move in enumerate(moves):
|
|
377
|
+
if move and move.strip():
|
|
378
|
+
pp = move_pp[i] if i < len(move_pp) else '?'
|
|
379
|
+
context_parts.append(f" {i+1}. {move} (PP: {pp})")
|
|
380
|
+
|
|
381
|
+
# Opponent Pokémon
|
|
382
|
+
if 'opponent_pokemon' in battle:
|
|
383
|
+
if battle['opponent_pokemon']:
|
|
384
|
+
opp_pkmn = battle['opponent_pokemon']
|
|
385
|
+
context_parts.append(f"\n--- OPPONENT POKÉMON ---")
|
|
386
|
+
context_parts.append(f"{opp_pkmn.get('species', 'Unknown')} (Lv.{opp_pkmn.get('level', '?')})")
|
|
387
|
+
|
|
388
|
+
# Health display with percentage
|
|
389
|
+
current_hp = opp_pkmn.get('current_hp', 0)
|
|
390
|
+
max_hp = opp_pkmn.get('max_hp', 1)
|
|
391
|
+
hp_pct = opp_pkmn.get('hp_percentage', 0)
|
|
392
|
+
health_bar = "🟢" if hp_pct > 50 else "🟡" if hp_pct > 25 else "🔴"
|
|
393
|
+
context_parts.append(f" HP: {current_hp}/{max_hp} ({hp_pct}%) {health_bar}")
|
|
394
|
+
|
|
395
|
+
# Status condition
|
|
396
|
+
status = opp_pkmn.get('status', 'Normal')
|
|
397
|
+
if status != 'Normal':
|
|
398
|
+
context_parts.append(f" Status: {status}")
|
|
399
|
+
|
|
400
|
+
# Types
|
|
401
|
+
types = opp_pkmn.get('types', [])
|
|
402
|
+
if types:
|
|
403
|
+
context_parts.append(f" Type: {'/'.join(types)}")
|
|
404
|
+
|
|
405
|
+
# Moves (for wild Pokémon, showing moves can help with strategy)
|
|
406
|
+
moves = opp_pkmn.get('moves', [])
|
|
407
|
+
if moves and any(move.strip() for move in moves):
|
|
408
|
+
context_parts.append(f" Known Moves:")
|
|
409
|
+
for i, move in enumerate(moves):
|
|
410
|
+
if move and move.strip():
|
|
411
|
+
context_parts.append(f" • {move}")
|
|
412
|
+
|
|
413
|
+
# Stats (helpful for battle strategy)
|
|
414
|
+
stats = opp_pkmn.get('stats', {})
|
|
415
|
+
if stats:
|
|
416
|
+
context_parts.append(f" Battle Stats: ATK:{stats.get('attack', '?')} DEF:{stats.get('defense', '?')} SPD:{stats.get('speed', '?')}")
|
|
417
|
+
|
|
418
|
+
# Special indicators
|
|
419
|
+
if opp_pkmn.get('is_shiny'):
|
|
420
|
+
context_parts.append(f" ✨ SHINY POKÉMON!")
|
|
421
|
+
else:
|
|
422
|
+
# Opponent data not ready
|
|
423
|
+
context_parts.append(f"\n--- OPPONENT POKÉMON ---")
|
|
424
|
+
opponent_status = battle.get('opponent_status', 'Opponent data not available')
|
|
425
|
+
context_parts.append(f"⏳ {opponent_status}")
|
|
426
|
+
context_parts.append(" (Battle may be in initialization phase)")
|
|
427
|
+
|
|
428
|
+
# Battle interface info
|
|
429
|
+
interface = battle.get('battle_interface', {})
|
|
430
|
+
available_actions = interface.get('available_actions', [])
|
|
431
|
+
if available_actions:
|
|
432
|
+
context_parts.append(f"\n--- AVAILABLE ACTIONS ---")
|
|
433
|
+
context_parts.append(f"Options: {', '.join(available_actions)}")
|
|
434
|
+
|
|
435
|
+
# Trainer battle specific info
|
|
436
|
+
if battle.get('is_trainer_battle'):
|
|
437
|
+
remaining = battle.get('opponent_team_remaining', 1)
|
|
438
|
+
if remaining > 1:
|
|
439
|
+
context_parts.append(f"\nTrainer has {remaining} Pokémon remaining")
|
|
440
|
+
|
|
441
|
+
# Battle phase info
|
|
442
|
+
battle_phase = battle.get('battle_phase_name')
|
|
443
|
+
if battle_phase:
|
|
444
|
+
context_parts.append(f"\nBattle Phase: {battle_phase}")
|
|
445
|
+
|
|
446
|
+
# Party information (important for switching decisions)
|
|
447
|
+
context_parts.append("\n=== PARTY STATUS ===")
|
|
448
|
+
party_context = _format_party_info(player_data, game_data)
|
|
449
|
+
context_parts.extend(party_context)
|
|
450
|
+
|
|
451
|
+
# Trainer info if available
|
|
452
|
+
if 'name' in player_data and player_data['name']:
|
|
453
|
+
context_parts.append(f"\nTrainer: {player_data['name']}")
|
|
454
|
+
|
|
455
|
+
# Money/badges might be relevant
|
|
456
|
+
money = player_data.get('money') or game_data.get('money')
|
|
457
|
+
if money is not None:
|
|
458
|
+
context_parts.append(f"Money: ${money}")
|
|
459
|
+
|
|
460
|
+
else:
|
|
461
|
+
# NORMAL MODE: Full state information
|
|
462
|
+
context_parts.append("=== PLAYER INFO ===")
|
|
463
|
+
|
|
464
|
+
# Player name and basic info (don't show during title sequence as it hasn't been set yet)
|
|
465
|
+
player_location = player_data.get('location', '')
|
|
466
|
+
if 'name' in player_data and player_data['name'] and player_location != 'TITLE_SEQUENCE':
|
|
467
|
+
context_parts.append(f"Player Name: {player_data['name']}")
|
|
468
|
+
|
|
469
|
+
# Position information
|
|
470
|
+
position = _get_player_position(player_data)
|
|
471
|
+
if position:
|
|
472
|
+
context_parts.append(f"Position: X={position.get('x', 'unknown')}, Y={position.get('y', 'unknown')}")
|
|
473
|
+
|
|
474
|
+
# Facing direction - removed as it's often unreliable
|
|
475
|
+
# if 'facing' in player_data and player_data['facing']:
|
|
476
|
+
# context_parts.append(f"Facing: {player_data['facing']}")
|
|
477
|
+
|
|
478
|
+
# Money (check both player and game sections)
|
|
479
|
+
money = player_data.get('money') or game_data.get('money')
|
|
480
|
+
if money is not None:
|
|
481
|
+
context_parts.append(f"Money: ${money}")
|
|
482
|
+
|
|
483
|
+
# Pokemon Party (check both player and game sections)
|
|
484
|
+
party_context = _format_party_info(player_data, game_data)
|
|
485
|
+
context_parts.extend(party_context)
|
|
486
|
+
|
|
487
|
+
# Map/Location information with traversability (NOT shown in battle)
|
|
488
|
+
map_context = _format_map_info(state_data.get('map', {}), player_data, include_debug_info, include_npcs, state_data)
|
|
489
|
+
context_parts.extend(map_context)
|
|
490
|
+
|
|
491
|
+
# Game state information (including dialogue if not in battle)
|
|
492
|
+
game_context = _format_game_state(game_data, state_data)
|
|
493
|
+
context_parts.extend(game_context)
|
|
494
|
+
|
|
495
|
+
# Debug information if requested (shown in both modes)
|
|
496
|
+
if include_debug_info:
|
|
497
|
+
debug_context = _format_debug_info(state_data)
|
|
498
|
+
context_parts.extend(debug_context)
|
|
499
|
+
|
|
500
|
+
return "\n".join(context_parts)
|
|
501
|
+
|
|
502
|
+
def format_state_for_debug(state_data):
|
|
503
|
+
"""
|
|
504
|
+
Format state data for detailed debugging output.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
state_data (dict): The comprehensive state from /state endpoint
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
str: Detailed debug information
|
|
511
|
+
"""
|
|
512
|
+
debug_parts = []
|
|
513
|
+
debug_parts.append("=" * 60)
|
|
514
|
+
debug_parts.append("COMPREHENSIVE STATE DEBUG")
|
|
515
|
+
debug_parts.append("=" * 60)
|
|
516
|
+
|
|
517
|
+
# Raw structure overview
|
|
518
|
+
debug_parts.append("\n--- STRUCTURE OVERVIEW ---")
|
|
519
|
+
for key, value in state_data.items():
|
|
520
|
+
if isinstance(value, dict):
|
|
521
|
+
debug_parts.append(f"{key}: dict with {len(value)} keys")
|
|
522
|
+
elif isinstance(value, list):
|
|
523
|
+
debug_parts.append(f"{key}: list with {len(value)} items")
|
|
524
|
+
else:
|
|
525
|
+
debug_parts.append(f"{key}: {type(value).__name__} = {value}")
|
|
526
|
+
|
|
527
|
+
# Detailed formatted state
|
|
528
|
+
debug_parts.append("\n--- FORMATTED STATE ---")
|
|
529
|
+
debug_parts.append(format_state_for_llm(state_data, include_debug_info=True))
|
|
530
|
+
|
|
531
|
+
# Raw JSON (truncated if too long)
|
|
532
|
+
debug_parts.append("\n--- RAW JSON (truncated) ---")
|
|
533
|
+
raw_json = json.dumps(state_data, indent=2)
|
|
534
|
+
if len(raw_json) > 2000:
|
|
535
|
+
debug_parts.append(raw_json[:2000] + "\n... (truncated)")
|
|
536
|
+
else:
|
|
537
|
+
debug_parts.append(raw_json)
|
|
538
|
+
|
|
539
|
+
debug_parts.append("=" * 60)
|
|
540
|
+
return "\n".join(debug_parts)
|
|
541
|
+
|
|
542
|
+
# Helper functions for state formatting
|
|
543
|
+
|
|
544
|
+
def _get_player_position(player_data):
|
|
545
|
+
"""Extract player position from various possible locations in player data."""
|
|
546
|
+
if 'coordinates' in player_data:
|
|
547
|
+
return player_data['coordinates']
|
|
548
|
+
elif 'position' in player_data:
|
|
549
|
+
pos = player_data['position']
|
|
550
|
+
# print( _get_player_position found position: {pos}, type: {type(pos)}")
|
|
551
|
+
# Check if it's a valid position dict with x,y keys
|
|
552
|
+
if pos and isinstance(pos, dict) and 'x' in pos and 'y' in pos:
|
|
553
|
+
return pos
|
|
554
|
+
# print( position invalid - missing x,y or not dict")
|
|
555
|
+
return None
|
|
556
|
+
|
|
557
|
+
def _get_party_size(party_data):
|
|
558
|
+
"""Get party size from party data regardless of format."""
|
|
559
|
+
if isinstance(party_data, dict):
|
|
560
|
+
return party_data.get('size', len(party_data.get('pokemon', [])))
|
|
561
|
+
elif isinstance(party_data, list):
|
|
562
|
+
return len(party_data)
|
|
563
|
+
return 0
|
|
564
|
+
|
|
565
|
+
def _format_party_info(player_data, game_data):
|
|
566
|
+
"""Format pokemon party information."""
|
|
567
|
+
context_parts = []
|
|
568
|
+
|
|
569
|
+
# Pokemon Party (check both player and game sections)
|
|
570
|
+
party_data = player_data.get('party') or game_data.get('party')
|
|
571
|
+
if party_data:
|
|
572
|
+
pokemon_list = []
|
|
573
|
+
if isinstance(party_data, dict) and party_data.get('pokemon'):
|
|
574
|
+
# Format: {"size": X, "pokemon": [...]}
|
|
575
|
+
pokemon_list = party_data.get('pokemon', [])
|
|
576
|
+
party_size = party_data.get('size', len(pokemon_list))
|
|
577
|
+
elif isinstance(party_data, list):
|
|
578
|
+
# Format: [pokemon1, pokemon2, ...]
|
|
579
|
+
pokemon_list = party_data
|
|
580
|
+
party_size = len(pokemon_list)
|
|
581
|
+
else:
|
|
582
|
+
party_size = 0
|
|
583
|
+
|
|
584
|
+
if party_size > 0:
|
|
585
|
+
context_parts.append(f"Pokemon Party ({party_size} pokemon):")
|
|
586
|
+
for i, pokemon in enumerate(pokemon_list[:6]):
|
|
587
|
+
if pokemon:
|
|
588
|
+
species = pokemon.get('species_name', pokemon.get('species', 'Unknown'))
|
|
589
|
+
level = pokemon.get('level', '?')
|
|
590
|
+
hp = pokemon.get('current_hp', '?')
|
|
591
|
+
max_hp = pokemon.get('max_hp', '?')
|
|
592
|
+
status = pokemon.get('status', 'Normal')
|
|
593
|
+
context_parts.append(f" {i+1}. {species} (Lv.{level}) HP: {hp}/{max_hp} Status: {status}")
|
|
594
|
+
else:
|
|
595
|
+
context_parts.append("No Pokemon in party")
|
|
596
|
+
else:
|
|
597
|
+
context_parts.append("No Pokemon in party")
|
|
598
|
+
|
|
599
|
+
return context_parts
|
|
600
|
+
|
|
601
|
+
def _format_map_info(map_info, player_data=None, include_debug_info=False, include_npcs=True, full_state_data=None):
|
|
602
|
+
"""Format map and traversability information using MapStitcher."""
|
|
603
|
+
context_parts = []
|
|
604
|
+
|
|
605
|
+
if not map_info:
|
|
606
|
+
return context_parts
|
|
607
|
+
|
|
608
|
+
# Get location name from player data
|
|
609
|
+
location_name = None
|
|
610
|
+
if player_data and 'location' in player_data and player_data['location']:
|
|
611
|
+
location = player_data['location']
|
|
612
|
+
if isinstance(location, dict):
|
|
613
|
+
location_name = location.get('map_name', str(location))
|
|
614
|
+
else:
|
|
615
|
+
location_name = str(location)
|
|
616
|
+
|
|
617
|
+
# Special handling for title sequence - don't show map
|
|
618
|
+
if location_name == 'TITLE_SEQUENCE':
|
|
619
|
+
context_parts.append("\n=== LOCATION INFO ===")
|
|
620
|
+
context_parts.append(f"Current Location: {location_name}")
|
|
621
|
+
context_parts.append("No map available during title sequence")
|
|
622
|
+
return context_parts
|
|
623
|
+
|
|
624
|
+
context_parts.append("\n=== LOCATION & MAP INFO ===")
|
|
625
|
+
if location_name:
|
|
626
|
+
context_parts.append(f"Current Location: {location_name}")
|
|
627
|
+
|
|
628
|
+
# Also add current map if different
|
|
629
|
+
if 'current_map' in map_info and map_info['current_map'] != location_name:
|
|
630
|
+
context_parts.append(f"Current Map: {map_info['current_map']}")
|
|
631
|
+
|
|
632
|
+
# Get player coordinates
|
|
633
|
+
player_coords = None
|
|
634
|
+
player_coords_dict = map_info.get('player_coords', {})
|
|
635
|
+
if isinstance(player_coords_dict, dict) and player_coords_dict.get('x') is not None:
|
|
636
|
+
player_coords = (player_coords_dict.get('x', 0), player_coords_dict.get('y', 0))
|
|
637
|
+
elif player_coords_dict and not isinstance(player_coords_dict, dict):
|
|
638
|
+
player_coords = player_coords_dict
|
|
639
|
+
elif player_data and 'position' in player_data:
|
|
640
|
+
pos = player_data['position']
|
|
641
|
+
player_coords = (pos.get('x', 0), pos.get('y', 0))
|
|
642
|
+
|
|
643
|
+
# Get MapStitcher instance - prefer the one from memory_reader if available
|
|
644
|
+
# This ensures we use the instance that has the actual map data
|
|
645
|
+
map_stitcher = map_info.get('_map_stitcher_instance') if map_info else None
|
|
646
|
+
if not map_stitcher:
|
|
647
|
+
map_stitcher = _get_map_stitcher_instance()
|
|
648
|
+
|
|
649
|
+
# Get NPCs if available
|
|
650
|
+
npcs = []
|
|
651
|
+
if include_npcs and 'object_events' in map_info:
|
|
652
|
+
npcs = map_info.get('object_events', [])
|
|
653
|
+
|
|
654
|
+
# Get connections from current area
|
|
655
|
+
connections = []
|
|
656
|
+
if map_info.get('stitched_map_info'):
|
|
657
|
+
current_area = map_info['stitched_map_info'].get('current_area', {})
|
|
658
|
+
connections = current_area.get('connections', [])
|
|
659
|
+
|
|
660
|
+
# Check for pre-generated visual map first (from memory_reader)
|
|
661
|
+
if map_info.get('visual_map'):
|
|
662
|
+
# Use the pre-generated map visualization
|
|
663
|
+
context_parts.append(map_info['visual_map'])
|
|
664
|
+
elif location_name:
|
|
665
|
+
# Generate map display using MapStitcher
|
|
666
|
+
# print( Attempting to generate map for location: '{location_name}'")
|
|
667
|
+
# print( MapStitcher exists: {map_stitcher is not None}")
|
|
668
|
+
if map_stitcher:
|
|
669
|
+
# print( MapStitcher has {len(map_stitcher.map_areas)} areas")
|
|
670
|
+
for map_id in list(map_stitcher.map_areas.keys())[:3]:
|
|
671
|
+
area = map_stitcher.map_areas[map_id]
|
|
672
|
+
# print( Area {map_id}: '{area.location_name}'")
|
|
673
|
+
|
|
674
|
+
map_lines = map_stitcher.generate_location_map_display(
|
|
675
|
+
location_name=location_name,
|
|
676
|
+
player_pos=player_coords,
|
|
677
|
+
npcs=npcs,
|
|
678
|
+
connections=connections
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
if map_lines:
|
|
682
|
+
# print( Generated {len(map_lines)} map lines from MapStitcher")
|
|
683
|
+
context_parts.extend(map_lines)
|
|
684
|
+
# Add exploration statistics
|
|
685
|
+
location_grid = map_stitcher.get_location_grid(location_name)
|
|
686
|
+
if location_grid:
|
|
687
|
+
total_tiles = len(location_grid)
|
|
688
|
+
context_parts.append("")
|
|
689
|
+
context_parts.append(f"Total explored: {total_tiles} tiles")
|
|
690
|
+
else:
|
|
691
|
+
# print( MapStitcher returned empty, falling back to memory tiles")
|
|
692
|
+
# Fallback if MapStitcher doesn't have data for this location - use memory tiles
|
|
693
|
+
pass
|
|
694
|
+
if 'tiles' in map_info and map_info['tiles']:
|
|
695
|
+
context_parts.append(f"\n--- MAP: {location_name.upper()} (from memory) ---")
|
|
696
|
+
_add_local_map_fallback(context_parts, map_info, include_npcs)
|
|
697
|
+
else:
|
|
698
|
+
context_parts.append(f"\n--- MAP: {location_name.upper()} ---")
|
|
699
|
+
context_parts.append("No map data available")
|
|
700
|
+
else:
|
|
701
|
+
# No location name - use local map fallback
|
|
702
|
+
context_parts.append("\n--- LOCAL MAP (Location unknown) ---")
|
|
703
|
+
if 'tiles' in map_info and map_info['tiles']:
|
|
704
|
+
_add_local_map_fallback(context_parts, map_info, include_npcs)
|
|
705
|
+
|
|
706
|
+
# NPC information removed - unreliable detection with incorrect positions
|
|
707
|
+
|
|
708
|
+
# Add stitched map information if available
|
|
709
|
+
stitched_info = _format_stitched_map_info(map_info)
|
|
710
|
+
if stitched_info:
|
|
711
|
+
context_parts.extend(stitched_info)
|
|
712
|
+
|
|
713
|
+
return context_parts
|
|
714
|
+
|
|
715
|
+
def _add_local_map_fallback(context_parts, map_info, include_npcs):
|
|
716
|
+
"""Helper function to add local map display as fallback"""
|
|
717
|
+
if 'tiles' in map_info and map_info['tiles']:
|
|
718
|
+
raw_tiles = map_info['tiles']
|
|
719
|
+
# Use default facing direction since memory-based facing is unreliable
|
|
720
|
+
facing = "South" # default
|
|
721
|
+
|
|
722
|
+
# Get player coordinates
|
|
723
|
+
player_coords = map_info.get('player_coords')
|
|
724
|
+
|
|
725
|
+
# Get NPCs if available and include_npcs is True
|
|
726
|
+
npcs = []
|
|
727
|
+
if include_npcs and 'object_events' in map_info:
|
|
728
|
+
npcs = map_info.get('object_events', [])
|
|
729
|
+
|
|
730
|
+
# Use unified LLM formatter for consistency with NPCs if available
|
|
731
|
+
map_display = format_map_for_llm(raw_tiles, facing, npcs, player_coords)
|
|
732
|
+
context_parts.append(map_display)
|
|
733
|
+
|
|
734
|
+
# Add dynamic legend based on symbols in the map
|
|
735
|
+
grid = format_map_grid(raw_tiles, facing, npcs, player_coords)
|
|
736
|
+
legend = generate_dynamic_legend(grid)
|
|
737
|
+
context_parts.append(f"\n{legend}")
|
|
738
|
+
|
|
739
|
+
def _format_world_map_display(stitched_data, full_state_data=None):
|
|
740
|
+
"""Format location-specific map display"""
|
|
741
|
+
try:
|
|
742
|
+
# Build separate map for each location
|
|
743
|
+
return _build_stitched_world_map(stitched_data, full_state_data)
|
|
744
|
+
|
|
745
|
+
except Exception as e:
|
|
746
|
+
logger.warning(f"World map generation failed: {e}")
|
|
747
|
+
return []
|
|
748
|
+
|
|
749
|
+
def _get_map_stitcher_instance():
|
|
750
|
+
"""Get the MapStitcher instance - always reload from cache for multiprocess compatibility"""
|
|
751
|
+
from utils.map_stitcher import MapStitcher
|
|
752
|
+
# Always create fresh instance to read latest cache
|
|
753
|
+
# This is needed because server and client run in different processes
|
|
754
|
+
return MapStitcher()
|
|
755
|
+
|
|
756
|
+
def save_persistent_world_map(file_path=None):
|
|
757
|
+
"""Deprecated - MapStitcher handles all persistence now"""
|
|
758
|
+
# MapStitcher auto-saves, nothing to do here
|
|
759
|
+
pass
|
|
760
|
+
|
|
761
|
+
def load_persistent_world_map(file_path=None):
|
|
762
|
+
"""Deprecated - MapStitcher handles all persistence now"""
|
|
763
|
+
# MapStitcher auto-loads, nothing to do here
|
|
764
|
+
pass
|
|
765
|
+
|
|
766
|
+
def clear_persistent_world_map():
|
|
767
|
+
"""Clear the MapStitcher's data for testing"""
|
|
768
|
+
global CURRENT_LOCATION, LAST_LOCATION, LAST_TRANSITION
|
|
769
|
+
CURRENT_LOCATION = None
|
|
770
|
+
LAST_LOCATION = None
|
|
771
|
+
LAST_TRANSITION = None
|
|
772
|
+
# Clear MapStitcher data if instance exists
|
|
773
|
+
if MAP_STITCHER_INSTANCE:
|
|
774
|
+
MAP_STITCHER_INSTANCE.map_areas.clear()
|
|
775
|
+
MAP_STITCHER_INSTANCE.warp_connections.clear()
|
|
776
|
+
MAP_STITCHER_INSTANCE.save_to_file()
|
|
777
|
+
# print( Cleared map stitcher data")
|
|
778
|
+
|
|
779
|
+
# Helper function removed - now handled by MapStitcher._is_explorable_edge()
|
|
780
|
+
|
|
781
|
+
|
|
782
|
+
def _build_stitched_world_map(stitched_data, full_state_data=None):
|
|
783
|
+
"""Build map display using MapStitcher"""
|
|
784
|
+
|
|
785
|
+
# Get the current area and location info
|
|
786
|
+
current_area = stitched_data.get('current_area', {})
|
|
787
|
+
location_name = current_area.get('name')
|
|
788
|
+
|
|
789
|
+
# Get NPCs if available
|
|
790
|
+
npcs = stitched_data.get('object_events', [])
|
|
791
|
+
|
|
792
|
+
# Use fallback location name if current_area has 'Unknown' or empty name
|
|
793
|
+
fallback_name = stitched_data.get('location_name_fallback')
|
|
794
|
+
if not location_name or location_name == 'Unknown':
|
|
795
|
+
if fallback_name and fallback_name != 'Unknown':
|
|
796
|
+
location_name = fallback_name
|
|
797
|
+
else:
|
|
798
|
+
location_name = 'Unknown'
|
|
799
|
+
elif fallback_name and fallback_name != location_name and fallback_name != 'Unknown':
|
|
800
|
+
location_name = fallback_name
|
|
801
|
+
|
|
802
|
+
# Track location changes and transition coordinates
|
|
803
|
+
global CURRENT_LOCATION, LAST_LOCATION, LAST_TRANSITION
|
|
804
|
+
LAST_TRANSITION = None # Will store transition info
|
|
805
|
+
|
|
806
|
+
# Get MapStitcher instance - prefer the one from memory_reader if available
|
|
807
|
+
# This ensures we use the instance that has the actual map data
|
|
808
|
+
map_stitcher = map_info.get('_map_stitcher_instance') if map_info else None
|
|
809
|
+
if not map_stitcher:
|
|
810
|
+
map_stitcher = _get_map_stitcher_instance()
|
|
811
|
+
|
|
812
|
+
# Get player position for transition tracking
|
|
813
|
+
player_local_pos = stitched_data.get('player_local_pos', (0, 0))
|
|
814
|
+
|
|
815
|
+
# Check if we've changed locations
|
|
816
|
+
if CURRENT_LOCATION != location_name:
|
|
817
|
+
# Store transition information
|
|
818
|
+
if CURRENT_LOCATION is not None and CURRENT_LOCATION != location_name:
|
|
819
|
+
LAST_TRANSITION = {
|
|
820
|
+
'from_location': CURRENT_LOCATION,
|
|
821
|
+
'from_coords': player_local_pos, # Use current position as it's the exit point
|
|
822
|
+
'to_location': location_name,
|
|
823
|
+
'to_coords': player_local_pos
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
# Trigger MapStitcher save if callback is available
|
|
827
|
+
if MAP_STITCHER_SAVE_CALLBACK:
|
|
828
|
+
try:
|
|
829
|
+
# print( Location transition detected, triggering MapStitcher save...")
|
|
830
|
+
MAP_STITCHER_SAVE_CALLBACK()
|
|
831
|
+
# print( MapStitcher save completed")
|
|
832
|
+
except Exception as e:
|
|
833
|
+
print("Failed to save via MapStitcher callback: {e}")
|
|
834
|
+
|
|
835
|
+
# print( Location transition: {CURRENT_LOCATION} → {location_name} at position {player_local_pos}")
|
|
836
|
+
|
|
837
|
+
LAST_LOCATION = CURRENT_LOCATION
|
|
838
|
+
CURRENT_LOCATION = location_name
|
|
839
|
+
|
|
840
|
+
# Get player position
|
|
841
|
+
player_local_pos = stitched_data.get('player_local_pos')
|
|
842
|
+
if not player_local_pos:
|
|
843
|
+
# Fallback to extracting from current area
|
|
844
|
+
player_local_pos = current_area.get('player_pos', (5, 5)) # Default to center
|
|
845
|
+
|
|
846
|
+
# Get connection info from current_area
|
|
847
|
+
connections = current_area.get('connections', [])
|
|
848
|
+
|
|
849
|
+
# Build the display for this location
|
|
850
|
+
lines = []
|
|
851
|
+
|
|
852
|
+
# Add location transition indicator if we just changed locations
|
|
853
|
+
if LAST_LOCATION and LAST_LOCATION != location_name:
|
|
854
|
+
lines.append(f"\n⚡ LOCATION TRANSITION: {LAST_LOCATION} → {location_name}")
|
|
855
|
+
# Add transition coordinates if available
|
|
856
|
+
if LAST_TRANSITION and LAST_TRANSITION['to_location'] == location_name:
|
|
857
|
+
from_coords = LAST_TRANSITION['from_coords']
|
|
858
|
+
to_coords = LAST_TRANSITION['to_coords']
|
|
859
|
+
from_location = LAST_TRANSITION['from_location']
|
|
860
|
+
lines.append(f"📍 Exited {from_location} at ({from_coords[0]}, {from_coords[1]})")
|
|
861
|
+
lines.append(f"📍 Entered {location_name} at ({to_coords[0]}, {to_coords[1]})")
|
|
862
|
+
# Clear the LAST_LOCATION after showing transition
|
|
863
|
+
LAST_LOCATION = None
|
|
864
|
+
lines.append("")
|
|
865
|
+
|
|
866
|
+
# Use MapStitcher to generate the map display
|
|
867
|
+
map_stitcher = full_state_data.get('map_stitcher') if full_state_data else None
|
|
868
|
+
if not map_stitcher and MAP_STITCHER_INSTANCE:
|
|
869
|
+
map_stitcher = MAP_STITCHER_INSTANCE
|
|
870
|
+
|
|
871
|
+
if map_stitcher:
|
|
872
|
+
# Generate map display using MapStitcher
|
|
873
|
+
map_lines = map_stitcher.generate_location_map_display(
|
|
874
|
+
location_name=location_name,
|
|
875
|
+
player_pos=player_local_pos,
|
|
876
|
+
npcs=npcs,
|
|
877
|
+
connections=connections
|
|
878
|
+
)
|
|
879
|
+
lines.extend(map_lines)
|
|
880
|
+
|
|
881
|
+
# Add exploration statistics
|
|
882
|
+
location_grid = map_stitcher.get_location_grid(location_name)
|
|
883
|
+
if location_grid:
|
|
884
|
+
total_tiles = len(location_grid)
|
|
885
|
+
lines.append("")
|
|
886
|
+
lines.append(f"Total explored in {location_name}: {total_tiles} tiles")
|
|
887
|
+
else:
|
|
888
|
+
# Fallback if no MapStitcher available
|
|
889
|
+
lines.append(f"\n--- MAP: {location_name.upper()} ---")
|
|
890
|
+
lines.append("Map data not available")
|
|
891
|
+
|
|
892
|
+
# Add discovered connection points from our transition tracking
|
|
893
|
+
# print( Checking portal coordinates for location: {location_name}")
|
|
894
|
+
portal_connections_found = False
|
|
895
|
+
|
|
896
|
+
# Check location connections from MapStitcher cache first
|
|
897
|
+
location_connections = _get_location_connections_from_cache()
|
|
898
|
+
if location_connections and location_name in location_connections:
|
|
899
|
+
if not portal_connections_found:
|
|
900
|
+
lines.append("")
|
|
901
|
+
lines.append("Known Portal Coordinates:")
|
|
902
|
+
portal_connections_found = True
|
|
903
|
+
for other_loc, my_coords, their_coords in location_connections[location_name]:
|
|
904
|
+
lines.append(f" At ({my_coords[0]}, {my_coords[1]}) → {other_loc} ({their_coords[0]}, {their_coords[1]})")
|
|
905
|
+
|
|
906
|
+
# Also check MAP_ID_CONNECTIONS if available (from loaded MapStitcher data or HTTP response)
|
|
907
|
+
# print( About to check MAP_ID_CONNECTIONS...")
|
|
908
|
+
# print( full_state_data is: {type(full_state_data)} with keys: {list(full_state_data.keys()) if full_state_data else 'None'}")
|
|
909
|
+
try:
|
|
910
|
+
# Try multiple ways to find MAP_ID_CONNECTIONS
|
|
911
|
+
map_id_connections = None
|
|
912
|
+
|
|
913
|
+
# Method 0: Check if passed via HTTP response (NEW - preferred method)
|
|
914
|
+
# First try location_connections (more accurate), then fall back to portal_connections
|
|
915
|
+
if full_state_data and 'location_connections' in full_state_data:
|
|
916
|
+
location_connections = full_state_data['location_connections']
|
|
917
|
+
# print( Found location_connections in HTTP response: {location_connections}")
|
|
918
|
+
|
|
919
|
+
# Use the location connections display logic
|
|
920
|
+
if location_name in location_connections:
|
|
921
|
+
if not portal_connections_found:
|
|
922
|
+
lines.append("")
|
|
923
|
+
lines.append("Known Portal Coordinates:")
|
|
924
|
+
portal_connections_found = True
|
|
925
|
+
for other_loc, my_coords, their_coords in location_connections[location_name]:
|
|
926
|
+
lines.append(f" At ({my_coords[0]}, {my_coords[1]}) → {other_loc} ({their_coords[0]}, {their_coords[1]})")
|
|
927
|
+
# print( Added location connection: At ({my_coords[0]}, {my_coords[1]}) → {other_loc} ({their_coords[0]}, {their_coords[1]})")
|
|
928
|
+
|
|
929
|
+
elif full_state_data and 'portal_connections' in full_state_data:
|
|
930
|
+
map_id_connections = full_state_data['portal_connections']
|
|
931
|
+
# print( Found MAP_ID_CONNECTIONS in HTTP response: {map_id_connections}")
|
|
932
|
+
|
|
933
|
+
# Get current map ID to find relevant portals
|
|
934
|
+
current_map_id = None
|
|
935
|
+
if stitched_data and 'current_area' in stitched_data:
|
|
936
|
+
area_id = stitched_data['current_area'].get('id', '')
|
|
937
|
+
if area_id:
|
|
938
|
+
try:
|
|
939
|
+
current_map_id = int(area_id, 16) if isinstance(area_id, str) else area_id
|
|
940
|
+
# print( Current map ID: {current_map_id}")
|
|
941
|
+
except ValueError:
|
|
942
|
+
print("Could not parse map ID from: {area_id}")
|
|
943
|
+
|
|
944
|
+
# Try to find current map ID by matching current location name
|
|
945
|
+
if not current_map_id:
|
|
946
|
+
current_location = location_name # Use the location_name parameter
|
|
947
|
+
# print( Trying to find map ID for location: {current_location}")
|
|
948
|
+
|
|
949
|
+
# Look through server's map data to find current map ID
|
|
950
|
+
if 'map' in full_state_data:
|
|
951
|
+
map_info = full_state_data.get('map', {})
|
|
952
|
+
map_location = map_info.get('location_name', '')
|
|
953
|
+
if map_location:
|
|
954
|
+
current_location = map_location
|
|
955
|
+
# print( Using map location: {current_location}")
|
|
956
|
+
|
|
957
|
+
# Match location with portal connections - try all map IDs
|
|
958
|
+
for map_id in map_id_connections.keys():
|
|
959
|
+
# Convert string keys to int if needed
|
|
960
|
+
try:
|
|
961
|
+
test_map_id = int(map_id) if isinstance(map_id, str) else map_id
|
|
962
|
+
# print( Testing map ID {test_map_id} for location '{current_location}'")
|
|
963
|
+
|
|
964
|
+
# For LITTLEROOT TOWN MAYS HOUSE 2F, map_id should be 259
|
|
965
|
+
if current_location == "LITTLEROOT TOWN MAYS HOUSE 2F" and test_map_id == 259:
|
|
966
|
+
current_map_id = test_map_id
|
|
967
|
+
# print( Found matching map ID: {current_map_id}")
|
|
968
|
+
break
|
|
969
|
+
elif current_location == "LITTLEROOT TOWN MAYS HOUSE 1F" and test_map_id == 258:
|
|
970
|
+
current_map_id = test_map_id
|
|
971
|
+
# print( Found matching map ID: {current_map_id}")
|
|
972
|
+
break
|
|
973
|
+
except (ValueError, TypeError):
|
|
974
|
+
continue
|
|
975
|
+
|
|
976
|
+
# Display portal coordinates if we found them
|
|
977
|
+
if current_map_id and current_map_id in map_id_connections:
|
|
978
|
+
if not portal_connections_found:
|
|
979
|
+
lines.append("")
|
|
980
|
+
lines.append("Known Portal Coordinates:")
|
|
981
|
+
portal_connections_found = True
|
|
982
|
+
|
|
983
|
+
for conn in map_id_connections[current_map_id]:
|
|
984
|
+
to_name = conn.get('to_name', 'Unknown Location')
|
|
985
|
+
from_pos = conn.get('from_pos', [0, 0])
|
|
986
|
+
to_pos = conn.get('to_pos', [0, 0])
|
|
987
|
+
lines.append(f" At ({from_pos[0]}, {from_pos[1]}) → {to_name} ({to_pos[0]}, {to_pos[1]})")
|
|
988
|
+
# print( Added portal: At ({from_pos[0]}, {from_pos[1]}) → {to_name} ({to_pos[0]}, {to_pos[1]})")
|
|
989
|
+
|
|
990
|
+
elif stitched_data and 'portal_connections' in stitched_data:
|
|
991
|
+
map_id_connections = stitched_data['portal_connections']
|
|
992
|
+
# print( Found MAP_ID_CONNECTIONS in stitched_data: {map_id_connections}")
|
|
993
|
+
|
|
994
|
+
# Method 1: Check current module (fallback)
|
|
995
|
+
if not map_id_connections:
|
|
996
|
+
current_module = sys.modules[__name__]
|
|
997
|
+
# print( Checking current module for MAP_ID_CONNECTIONS attribute...")
|
|
998
|
+
# print( hasattr(current_module, 'MAP_ID_CONNECTIONS'): {hasattr(current_module, 'MAP_ID_CONNECTIONS')}")
|
|
999
|
+
if hasattr(current_module, 'MAP_ID_CONNECTIONS') and current_module.MAP_ID_CONNECTIONS:
|
|
1000
|
+
map_id_connections = current_module.MAP_ID_CONNECTIONS
|
|
1001
|
+
# print( Found MAP_ID_CONNECTIONS in current module: {map_id_connections}")
|
|
1002
|
+
|
|
1003
|
+
# Method 2: Check global variable (fallback)
|
|
1004
|
+
if not map_id_connections:
|
|
1005
|
+
# print( Checking globals for MAP_ID_CONNECTIONS...")
|
|
1006
|
+
# print( 'MAP_ID_CONNECTIONS' in globals(): {'MAP_ID_CONNECTIONS' in globals()}")
|
|
1007
|
+
try:
|
|
1008
|
+
if 'MAP_ID_CONNECTIONS' in globals():
|
|
1009
|
+
# print( globals()['MAP_ID_CONNECTIONS']: {globals()['MAP_ID_CONNECTIONS']}")
|
|
1010
|
+
if 'MAP_ID_CONNECTIONS' in globals() and globals()['MAP_ID_CONNECTIONS']:
|
|
1011
|
+
map_id_connections = globals()['MAP_ID_CONNECTIONS']
|
|
1012
|
+
# print( Found MAP_ID_CONNECTIONS in globals: {map_id_connections}")
|
|
1013
|
+
except Exception as e:
|
|
1014
|
+
print("Error checking globals: {e}")
|
|
1015
|
+
|
|
1016
|
+
# Method 3: Re-import state_formatter and check (fallback)
|
|
1017
|
+
if not map_id_connections:
|
|
1018
|
+
try:
|
|
1019
|
+
# print( Attempting to re-import state_formatter...")
|
|
1020
|
+
# print( hasattr(sf, 'MAP_ID_CONNECTIONS'): {hasattr(sf, 'MAP_ID_CONNECTIONS')}")
|
|
1021
|
+
if hasattr(sf, 'MAP_ID_CONNECTIONS'):
|
|
1022
|
+
# print( sf.MAP_ID_CONNECTIONS: {sf.MAP_ID_CONNECTIONS}")
|
|
1023
|
+
if hasattr(sf, 'MAP_ID_CONNECTIONS') and sf.MAP_ID_CONNECTIONS:
|
|
1024
|
+
map_id_connections = sf.MAP_ID_CONNECTIONS
|
|
1025
|
+
# print( Found MAP_ID_CONNECTIONS in imported module: {map_id_connections}")
|
|
1026
|
+
except Exception as e:
|
|
1027
|
+
print(f" Error re-importing state_formatter: {e}")
|
|
1028
|
+
|
|
1029
|
+
if map_id_connections:
|
|
1030
|
+
# print( MAP_ID_CONNECTIONS available with {len(map_id_connections)} maps")
|
|
1031
|
+
# print( Available map IDs: {list(map_id_connections.keys())}")
|
|
1032
|
+
|
|
1033
|
+
# Get current map ID from stitched data
|
|
1034
|
+
current_map_id = None
|
|
1035
|
+
# print( stitched_data structure: {stitched_data}")
|
|
1036
|
+
if stitched_data and stitched_data.get('available'):
|
|
1037
|
+
# Try to get map ID from current area
|
|
1038
|
+
current_area = stitched_data.get('current_area', {})
|
|
1039
|
+
map_id_str = current_area.get('id')
|
|
1040
|
+
# print( Current area ID from stitched_data: {map_id_str}")
|
|
1041
|
+
# print( Full current_area: {current_area}")
|
|
1042
|
+
if map_id_str:
|
|
1043
|
+
try:
|
|
1044
|
+
current_map_id = int(map_id_str, 16) # Convert from hex string
|
|
1045
|
+
print(f" Converted to map ID: {current_map_id}")
|
|
1046
|
+
except ValueError:
|
|
1047
|
+
print(f" Failed to convert map ID: {map_id_str}")
|
|
1048
|
+
else:
|
|
1049
|
+
print("No map ID found in current_area: {current_area}")
|
|
1050
|
+
# else:
|
|
1051
|
+
# print( No stitched_data available or not available")
|
|
1052
|
+
# print( stitched_data type: {type(stitched_data)}")
|
|
1053
|
+
# if stitched_data:
|
|
1054
|
+
# print( stitched_data keys: {list(stitched_data.keys()) if isinstance(stitched_data, dict) else 'not a dict'}")
|
|
1055
|
+
# print( stitched_data.get('available'): {stitched_data.get('available') if isinstance(stitched_data, dict) else 'N/A'}")
|
|
1056
|
+
|
|
1057
|
+
if current_map_id and current_map_id in map_id_connections:
|
|
1058
|
+
# print( Found connections for map ID {current_map_id}")
|
|
1059
|
+
if not portal_connections_found:
|
|
1060
|
+
lines.append("")
|
|
1061
|
+
lines.append("Known Portal Coordinates:")
|
|
1062
|
+
portal_connections_found = True
|
|
1063
|
+
for conn in map_id_connections[current_map_id]:
|
|
1064
|
+
to_name = conn['to_name']
|
|
1065
|
+
from_pos = conn['from_pos']
|
|
1066
|
+
to_pos = conn['to_pos']
|
|
1067
|
+
lines.append(f" At ({from_pos[0]}, {from_pos[1]}) → {to_name} ({to_pos[0]}, {to_pos[1]})")
|
|
1068
|
+
else:
|
|
1069
|
+
print("No connections found for map ID {current_map_id} (available: {list(map_id_connections.keys()) if map_id_connections else 'None'})")
|
|
1070
|
+
else:
|
|
1071
|
+
print("MAP_ID_CONNECTIONS not found in any location")
|
|
1072
|
+
except Exception as e:
|
|
1073
|
+
print(f" Error checking MAP_ID_CONNECTIONS: {e}")
|
|
1074
|
+
|
|
1075
|
+
return lines
|
|
1076
|
+
|
|
1077
|
+
|
|
1078
|
+
def _format_stitched_map_info(map_info):
|
|
1079
|
+
"""Format stitched map information for the agent"""
|
|
1080
|
+
context_parts = []
|
|
1081
|
+
|
|
1082
|
+
# Check if stitched map info is available
|
|
1083
|
+
stitched_data = map_info.get('stitched_map_info')
|
|
1084
|
+
if not stitched_data or not stitched_data.get('available'):
|
|
1085
|
+
return context_parts
|
|
1086
|
+
|
|
1087
|
+
# Check if world map display with terrain was already shown
|
|
1088
|
+
# Old world map knowledge system removed - replaced by location-based maps with portal coordinates
|
|
1089
|
+
return context_parts
|
|
1090
|
+
|
|
1091
|
+
def _format_game_state(game_data, state_data=None):
|
|
1092
|
+
"""Format game state information (for non-battle mode)."""
|
|
1093
|
+
context_parts = []
|
|
1094
|
+
|
|
1095
|
+
if not game_data:
|
|
1096
|
+
return context_parts
|
|
1097
|
+
|
|
1098
|
+
context_parts.append("\n=== GAME STATE ===")
|
|
1099
|
+
|
|
1100
|
+
# Note: Battle info is handled separately in battle mode
|
|
1101
|
+
# This is for showing game state when NOT in battle
|
|
1102
|
+
|
|
1103
|
+
# Dialogue detection and validation (only show when not in battle)
|
|
1104
|
+
is_in_battle = game_data.get('is_in_battle', False) or game_data.get('in_battle', False)
|
|
1105
|
+
|
|
1106
|
+
if not is_in_battle:
|
|
1107
|
+
dialog_text = game_data.get('dialog_text')
|
|
1108
|
+
dialogue_detected = game_data.get('dialogue_detected', {})
|
|
1109
|
+
|
|
1110
|
+
if dialog_text and dialogue_detected.get('has_dialogue', False):
|
|
1111
|
+
# Only show dialogue if it's actually visible and active
|
|
1112
|
+
context_parts.append(f"\n--- DIALOGUE ---")
|
|
1113
|
+
if dialogue_detected.get('confidence') is not None:
|
|
1114
|
+
context_parts.append(f"Detection confidence: {dialogue_detected['confidence']:.1%}")
|
|
1115
|
+
context_parts.append(f"Text: {dialog_text}")
|
|
1116
|
+
# Note: Residual/invisible dialogue text is completely hidden from agent
|
|
1117
|
+
|
|
1118
|
+
# Check if we're in title sequence and override game state
|
|
1119
|
+
player_location = state_data.get('player', {}).get('location', '') if state_data else ''
|
|
1120
|
+
if player_location == 'TITLE_SEQUENCE':
|
|
1121
|
+
context_parts.append(f"Game State: title")
|
|
1122
|
+
elif 'game_state' in game_data:
|
|
1123
|
+
context_parts.append(f"Game State: {game_data['game_state']}")
|
|
1124
|
+
|
|
1125
|
+
# Get player data from state_data if provided
|
|
1126
|
+
player_data = state_data.get('player', {}) if state_data else {}
|
|
1127
|
+
|
|
1128
|
+
# Add helpful prompt for title sequence
|
|
1129
|
+
player_location = player_data.get('location', '')
|
|
1130
|
+
if player_location == 'TITLE_SEQUENCE':
|
|
1131
|
+
context_parts.append("")
|
|
1132
|
+
context_parts.append("💡 TIP: Make sure to choose a fun name for your character!")
|
|
1133
|
+
context_parts.append("Be creative and have fun with the naming!")
|
|
1134
|
+
|
|
1135
|
+
# Add movement preview for overworld navigation (but not during title sequence)
|
|
1136
|
+
if (state_data and not is_in_battle and
|
|
1137
|
+
game_data.get('game_state') == 'overworld' and
|
|
1138
|
+
player_location != 'TITLE_SEQUENCE'):
|
|
1139
|
+
movement_preview = format_movement_preview_for_llm(state_data)
|
|
1140
|
+
if movement_preview:
|
|
1141
|
+
context_parts.append("")
|
|
1142
|
+
context_parts.append(movement_preview)
|
|
1143
|
+
|
|
1144
|
+
return context_parts
|
|
1145
|
+
|
|
1146
|
+
def _format_debug_info(state_data):
|
|
1147
|
+
"""Format additional debug information."""
|
|
1148
|
+
context_parts = []
|
|
1149
|
+
|
|
1150
|
+
context_parts.append("\n=== DEBUG INFO ===")
|
|
1151
|
+
|
|
1152
|
+
# Step information
|
|
1153
|
+
if 'step_number' in state_data:
|
|
1154
|
+
context_parts.append(f"Step Number: {state_data['step_number']}")
|
|
1155
|
+
|
|
1156
|
+
if 'status' in state_data:
|
|
1157
|
+
context_parts.append(f"Status: {state_data['status']}")
|
|
1158
|
+
|
|
1159
|
+
# Visual data info
|
|
1160
|
+
if 'visual' in state_data:
|
|
1161
|
+
visual = state_data['visual']
|
|
1162
|
+
if 'resolution' in visual:
|
|
1163
|
+
context_parts.append(f"Resolution: {visual['resolution']}")
|
|
1164
|
+
if 'screenshot_base64' in visual:
|
|
1165
|
+
context_parts.append(f"Screenshot: Available ({len(visual['screenshot_base64'])} chars)")
|
|
1166
|
+
|
|
1167
|
+
return context_parts
|
|
1168
|
+
|
|
1169
|
+
# Convenience functions for specific use cases
|
|
1170
|
+
|
|
1171
|
+
def get_movement_options(state_data):
|
|
1172
|
+
"""
|
|
1173
|
+
Extract movement options from traversability data.
|
|
1174
|
+
|
|
1175
|
+
Returns:
|
|
1176
|
+
dict: Direction -> description mapping
|
|
1177
|
+
"""
|
|
1178
|
+
map_info = state_data.get('map', {})
|
|
1179
|
+
if 'traversability' not in map_info or not map_info['traversability']:
|
|
1180
|
+
return {}
|
|
1181
|
+
|
|
1182
|
+
traversability = map_info['traversability']
|
|
1183
|
+
center_y = len(traversability) // 2
|
|
1184
|
+
center_x = len(traversability[0]) // 2
|
|
1185
|
+
|
|
1186
|
+
directions = {
|
|
1187
|
+
'UP': (0, -1), 'DOWN': (0, 1),
|
|
1188
|
+
'LEFT': (-1, 0), 'RIGHT': (1, 0)
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
movement_options = {}
|
|
1192
|
+
for direction, (dx, dy) in directions.items():
|
|
1193
|
+
new_x, new_y = center_x + dx, center_y + dy
|
|
1194
|
+
if 0 <= new_y < len(traversability) and 0 <= new_x < len(traversability[new_y]):
|
|
1195
|
+
cell = str(traversability[new_y][new_x])
|
|
1196
|
+
if cell == "0":
|
|
1197
|
+
movement_options[direction] = "BLOCKED"
|
|
1198
|
+
elif cell == ".":
|
|
1199
|
+
movement_options[direction] = "Normal path"
|
|
1200
|
+
elif "TALL" in cell:
|
|
1201
|
+
movement_options[direction] = "Tall grass (wild encounters)"
|
|
1202
|
+
elif "WATER" in cell:
|
|
1203
|
+
movement_options[direction] = "Water (need Surf)"
|
|
1204
|
+
else:
|
|
1205
|
+
movement_options[direction] = cell
|
|
1206
|
+
else:
|
|
1207
|
+
movement_options[direction] = "Out of bounds"
|
|
1208
|
+
|
|
1209
|
+
return movement_options
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
def get_movement_preview(state_data):
|
|
1213
|
+
"""
|
|
1214
|
+
Get detailed preview of what happens with each directional movement.
|
|
1215
|
+
Shows new coordinates and tile information for each direction.
|
|
1216
|
+
|
|
1217
|
+
Args:
|
|
1218
|
+
state_data: Complete game state data
|
|
1219
|
+
|
|
1220
|
+
Returns:
|
|
1221
|
+
dict: Direction -> preview info mapping
|
|
1222
|
+
"""
|
|
1223
|
+
# Get current player position
|
|
1224
|
+
player_data = state_data.get('player', {})
|
|
1225
|
+
player_position = _get_player_position(player_data)
|
|
1226
|
+
|
|
1227
|
+
if not player_position or 'x' not in player_position or 'y' not in player_position:
|
|
1228
|
+
# print( Movement preview - No player position. player_position={player_position}")
|
|
1229
|
+
return {}
|
|
1230
|
+
|
|
1231
|
+
current_x = int(player_position['x'])
|
|
1232
|
+
current_y = int(player_position['y'])
|
|
1233
|
+
|
|
1234
|
+
# Get map and tile data
|
|
1235
|
+
map_info = state_data.get('map', {})
|
|
1236
|
+
raw_tiles = map_info.get('tiles', [])
|
|
1237
|
+
|
|
1238
|
+
if not raw_tiles:
|
|
1239
|
+
# print( Movement preview - No tiles. map_info keys: {list(map_info.keys()) if map_info else 'None'}")
|
|
1240
|
+
return {}
|
|
1241
|
+
|
|
1242
|
+
directions = {
|
|
1243
|
+
'UP': (0, -1),
|
|
1244
|
+
'DOWN': (0, 1),
|
|
1245
|
+
'LEFT': (-1, 0),
|
|
1246
|
+
'RIGHT': (1, 0)
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
movement_preview = {}
|
|
1250
|
+
|
|
1251
|
+
# Player is at center of the 15x15 grid
|
|
1252
|
+
center_x = len(raw_tiles[0]) // 2 if raw_tiles and raw_tiles[0] else 7
|
|
1253
|
+
center_y = len(raw_tiles) // 2 if raw_tiles else 7
|
|
1254
|
+
|
|
1255
|
+
# Get the tile the player is currently standing on
|
|
1256
|
+
current_tile_symbol = None
|
|
1257
|
+
if (0 <= center_y < len(raw_tiles) and
|
|
1258
|
+
0 <= center_x < len(raw_tiles[center_y]) and
|
|
1259
|
+
raw_tiles[center_y] and raw_tiles[center_y][center_x]):
|
|
1260
|
+
current_tile = raw_tiles[center_y][center_x]
|
|
1261
|
+
current_tile_symbol = format_tile_to_symbol(current_tile)
|
|
1262
|
+
|
|
1263
|
+
for direction, (dx, dy) in directions.items():
|
|
1264
|
+
# Calculate new world coordinates
|
|
1265
|
+
new_world_x = current_x + dx
|
|
1266
|
+
new_world_y = current_y + dy
|
|
1267
|
+
|
|
1268
|
+
# Calculate grid position in the tile array
|
|
1269
|
+
grid_x = center_x + dx
|
|
1270
|
+
grid_y = center_y + dy
|
|
1271
|
+
|
|
1272
|
+
preview_info = {
|
|
1273
|
+
'new_coords': (new_world_x, new_world_y),
|
|
1274
|
+
'blocked': True,
|
|
1275
|
+
'tile_symbol': '#',
|
|
1276
|
+
'tile_description': 'BLOCKED - Out of bounds'
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
# Check if the target position is within the grid bounds
|
|
1280
|
+
if (0 <= grid_y < len(raw_tiles) and
|
|
1281
|
+
0 <= grid_x < len(raw_tiles[grid_y]) and
|
|
1282
|
+
raw_tiles[grid_y]):
|
|
1283
|
+
|
|
1284
|
+
try:
|
|
1285
|
+
# Get the tile at the target position
|
|
1286
|
+
target_tile = raw_tiles[grid_y][grid_x]
|
|
1287
|
+
|
|
1288
|
+
# Get tile symbol and check if walkable
|
|
1289
|
+
tile_symbol = format_tile_to_symbol(target_tile)
|
|
1290
|
+
|
|
1291
|
+
# Determine if movement is blocked
|
|
1292
|
+
is_blocked = tile_symbol in ['#', 'W'] # Walls and water block movement
|
|
1293
|
+
|
|
1294
|
+
# SPECIAL CASE: If player is standing on stairs/door, don't block the warp direction
|
|
1295
|
+
# Stairs and doors often require moving in a specific direction to activate
|
|
1296
|
+
if current_tile_symbol in ['S', 'D']:
|
|
1297
|
+
# When on stairs/doors, typically you need to move forward to activate them
|
|
1298
|
+
# Don't block any direction when on these tiles to allow proper navigation
|
|
1299
|
+
# This ensures the agent can properly use warps/doors even if the destination
|
|
1300
|
+
# tile might normally be considered blocked
|
|
1301
|
+
if tile_symbol in ['#', 'W']:
|
|
1302
|
+
# Override the blocking for navigation tiles but KEEP original symbol
|
|
1303
|
+
is_blocked = False
|
|
1304
|
+
# DO NOT change tile_symbol - preserve S, D, #, W, etc.
|
|
1305
|
+
|
|
1306
|
+
# Special handling for jump ledges - they're only walkable in their direction
|
|
1307
|
+
if tile_symbol in ['↓', '↑', '←', '→', '↗', '↖', '↘', '↙']:
|
|
1308
|
+
# Map directions to tile symbols
|
|
1309
|
+
ledge_direction_map = {
|
|
1310
|
+
'UP': '↑',
|
|
1311
|
+
'DOWN': '↓',
|
|
1312
|
+
'LEFT': '←',
|
|
1313
|
+
'RIGHT': '→'
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
# Only allow movement if we're going in the direction the ledge points
|
|
1317
|
+
if direction in ledge_direction_map:
|
|
1318
|
+
allowed_symbol = ledge_direction_map[direction]
|
|
1319
|
+
if tile_symbol != allowed_symbol:
|
|
1320
|
+
is_blocked = True # Block movement in wrong direction
|
|
1321
|
+
else:
|
|
1322
|
+
is_blocked = True # Block diagonal movements for basic directional ledges
|
|
1323
|
+
|
|
1324
|
+
# Get tile description
|
|
1325
|
+
if len(target_tile) >= 2:
|
|
1326
|
+
tile_id, behavior = target_tile[:2]
|
|
1327
|
+
|
|
1328
|
+
# Convert behavior to readable description
|
|
1329
|
+
if hasattr(behavior, 'name'):
|
|
1330
|
+
behavior_name = behavior.name
|
|
1331
|
+
elif isinstance(behavior, int):
|
|
1332
|
+
try:
|
|
1333
|
+
behavior_enum = MetatileBehavior(behavior)
|
|
1334
|
+
behavior_name = behavior_enum.name
|
|
1335
|
+
except (ValueError, ImportError):
|
|
1336
|
+
behavior_name = f"BEHAVIOR_{behavior}"
|
|
1337
|
+
else:
|
|
1338
|
+
behavior_name = str(behavior)
|
|
1339
|
+
|
|
1340
|
+
# Create human-readable description
|
|
1341
|
+
# Check if we're overriding blocking due to being on stairs/door
|
|
1342
|
+
is_override = current_tile_symbol in ['S', 'D'] and not is_blocked and tile_symbol in ['#', 'W']
|
|
1343
|
+
|
|
1344
|
+
if is_override:
|
|
1345
|
+
# We're on stairs/door and this normally blocked tile is walkable
|
|
1346
|
+
if tile_symbol == '#':
|
|
1347
|
+
tile_description = f"Walkable - Warp/Door exit (normally blocked) (ID: {tile_id})"
|
|
1348
|
+
elif tile_symbol == 'W':
|
|
1349
|
+
tile_description = f"Walkable - Warp/Door exit over water (ID: {tile_id})"
|
|
1350
|
+
elif tile_symbol == '.':
|
|
1351
|
+
tile_description = f"Walkable path (ID: {tile_id})"
|
|
1352
|
+
elif tile_symbol == '#':
|
|
1353
|
+
tile_description = f"BLOCKED - Wall/Obstacle (ID: {tile_id}, {behavior_name})"
|
|
1354
|
+
elif tile_symbol == 'W':
|
|
1355
|
+
tile_description = f"BLOCKED - Water (need Surf) (ID: {tile_id})"
|
|
1356
|
+
elif tile_symbol == '~':
|
|
1357
|
+
tile_description = f"Walkable - Tall grass (wild encounters) (ID: {tile_id})"
|
|
1358
|
+
elif tile_symbol == 'D':
|
|
1359
|
+
tile_description = f"Walkable - Door/Entrance (ID: {tile_id})"
|
|
1360
|
+
elif tile_symbol == 'S':
|
|
1361
|
+
tile_description = f"Walkable - Stairs/Warp (ID: {tile_id})"
|
|
1362
|
+
elif tile_symbol in ['↓', '↑', '←', '→', '↗', '↖', '↘', '↙']:
|
|
1363
|
+
# Ledge description based on whether movement is allowed
|
|
1364
|
+
if is_blocked:
|
|
1365
|
+
tile_description = f"BLOCKED - Jump ledge {tile_symbol} (wrong direction) (ID: {tile_id})"
|
|
1366
|
+
else:
|
|
1367
|
+
tile_description = f"Walkable - Jump ledge {tile_symbol} (correct direction) (ID: {tile_id})"
|
|
1368
|
+
else:
|
|
1369
|
+
tile_description = f"Walkable - {behavior_name} (ID: {tile_id})"
|
|
1370
|
+
else:
|
|
1371
|
+
tile_description = "Unknown tile"
|
|
1372
|
+
|
|
1373
|
+
preview_info.update({
|
|
1374
|
+
'blocked': is_blocked,
|
|
1375
|
+
'tile_symbol': tile_symbol,
|
|
1376
|
+
'tile_description': tile_description
|
|
1377
|
+
})
|
|
1378
|
+
|
|
1379
|
+
except (IndexError, TypeError) as e:
|
|
1380
|
+
logger.warning(f"Error analyzing tile at {grid_x}, {grid_y}: {e}")
|
|
1381
|
+
# Keep default blocked values
|
|
1382
|
+
pass
|
|
1383
|
+
|
|
1384
|
+
movement_preview[direction] = preview_info
|
|
1385
|
+
|
|
1386
|
+
return movement_preview
|
|
1387
|
+
|
|
1388
|
+
|
|
1389
|
+
def format_movement_preview_for_llm(state_data):
|
|
1390
|
+
"""
|
|
1391
|
+
Format movement preview in a concise format suitable for LLM prompts.
|
|
1392
|
+
|
|
1393
|
+
Args:
|
|
1394
|
+
state_data: Complete game state data
|
|
1395
|
+
|
|
1396
|
+
Returns:
|
|
1397
|
+
str: Formatted movement preview text
|
|
1398
|
+
"""
|
|
1399
|
+
preview = get_movement_preview(state_data)
|
|
1400
|
+
|
|
1401
|
+
if not preview:
|
|
1402
|
+
return "Movement preview: Not available"
|
|
1403
|
+
|
|
1404
|
+
lines = ["MOVEMENT PREVIEW:"]
|
|
1405
|
+
|
|
1406
|
+
for direction in ['UP', 'DOWN', 'LEFT', 'RIGHT']:
|
|
1407
|
+
if direction in preview:
|
|
1408
|
+
info = preview[direction]
|
|
1409
|
+
new_x, new_y = info['new_coords']
|
|
1410
|
+
symbol = info['tile_symbol']
|
|
1411
|
+
status = "BLOCKED" if info['blocked'] else "WALKABLE"
|
|
1412
|
+
|
|
1413
|
+
lines.append(f" {direction:5}: ({new_x:3},{new_y:3}) [{symbol}] {status}")
|
|
1414
|
+
# Add brief description for tiles
|
|
1415
|
+
desc = info['tile_description']
|
|
1416
|
+
if info['blocked']:
|
|
1417
|
+
# Special messages for blocked tiles
|
|
1418
|
+
if 'Jump ledge' in desc and 'wrong direction' in desc:
|
|
1419
|
+
lines[-1] += " - Can only jump in arrow direction"
|
|
1420
|
+
elif 'Water' in desc:
|
|
1421
|
+
lines[-1] += " - Need Surf to cross"
|
|
1422
|
+
elif 'Wall' in desc or 'Obstacle' in desc:
|
|
1423
|
+
lines[-1] += " - Impassable"
|
|
1424
|
+
else:
|
|
1425
|
+
# Add brief description for walkable tiles
|
|
1426
|
+
if 'Tall grass' in desc:
|
|
1427
|
+
lines[-1] += " - Tall grass (wild encounters)"
|
|
1428
|
+
elif 'Stairs' in desc or 'Warp' in desc:
|
|
1429
|
+
lines[-1] += " - Stairs/Warp"
|
|
1430
|
+
elif 'Door' in desc or 'Entrance' in desc:
|
|
1431
|
+
lines[-1] += " - Door/Entrance"
|
|
1432
|
+
elif 'Jump ledge' in desc and 'correct direction' in desc:
|
|
1433
|
+
lines[-1] += " - Jump ledge (can jump this way)"
|
|
1434
|
+
|
|
1435
|
+
return "\n".join(lines)
|
|
1436
|
+
|
|
1437
|
+
|
|
1438
|
+
def get_party_health_summary(state_data):
|
|
1439
|
+
"""
|
|
1440
|
+
Get a summary of party health status.
|
|
1441
|
+
|
|
1442
|
+
Returns:
|
|
1443
|
+
dict: Summary with healthy_count, total_count, critical_pokemon
|
|
1444
|
+
"""
|
|
1445
|
+
player_data = state_data.get('player', {})
|
|
1446
|
+
game_data = state_data.get('game', {})
|
|
1447
|
+
party_data = player_data.get('party') or game_data.get('party')
|
|
1448
|
+
|
|
1449
|
+
if not party_data:
|
|
1450
|
+
return {"healthy_count": 0, "total_count": 0, "critical_pokemon": []}
|
|
1451
|
+
|
|
1452
|
+
pokemon_list = []
|
|
1453
|
+
if isinstance(party_data, dict) and party_data.get('pokemon'):
|
|
1454
|
+
pokemon_list = party_data.get('pokemon', [])
|
|
1455
|
+
elif isinstance(party_data, list):
|
|
1456
|
+
pokemon_list = party_data
|
|
1457
|
+
|
|
1458
|
+
healthy_count = 0
|
|
1459
|
+
critical_pokemon = []
|
|
1460
|
+
|
|
1461
|
+
for i, pokemon in enumerate(pokemon_list[:6]):
|
|
1462
|
+
if pokemon:
|
|
1463
|
+
hp = pokemon.get('current_hp', 0)
|
|
1464
|
+
max_hp = pokemon.get('max_hp', 1)
|
|
1465
|
+
status = pokemon.get('status', 'OK')
|
|
1466
|
+
species = pokemon.get('species_name', pokemon.get('species', 'Unknown Pokemon'))
|
|
1467
|
+
|
|
1468
|
+
# Check if healthy: has HP and no negative status (OK or Normal are both healthy)
|
|
1469
|
+
if hp > 0 and status in ['OK', 'Normal']:
|
|
1470
|
+
healthy_count += 1
|
|
1471
|
+
|
|
1472
|
+
hp_percent = (hp / max_hp * 100) if max_hp > 0 else 0
|
|
1473
|
+
# Mark as critical if low HP or has a status condition
|
|
1474
|
+
if hp_percent < 25 or status not in ['OK', 'Normal']:
|
|
1475
|
+
critical_pokemon.append(f"{species} ({hp_percent:.0f}% HP, {status})")
|
|
1476
|
+
|
|
1477
|
+
return {
|
|
1478
|
+
"healthy_count": healthy_count,
|
|
1479
|
+
"total_count": len(pokemon_list),
|
|
1480
|
+
"critical_pokemon": critical_pokemon
|
|
1481
|
+
}
|