synth-ai 0.2.13.dev1__py3-none-any.whl → 0.2.14__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/README_verilog_rl.md +77 -0
- examples/multi_step/configs/VERILOG_REWARDS.md +90 -0
- examples/multi_step/configs/VERILOG_RL_CHECKLIST.md +183 -0
- examples/multi_step/configs/crafter_eval_synth_qwen4b.toml +35 -0
- examples/multi_step/configs/crafter_eval_text_only_groq_qwen32b.toml +36 -0
- examples/multi_step/configs/crafter_rl_stepwise_hosted_judge.toml +17 -5
- examples/multi_step/configs/crafter_synth_backend.md +40 -0
- examples/multi_step/configs/verilog_eval_groq_qwen32b.toml +31 -0
- examples/multi_step/configs/verilog_eval_synth_qwen8b.toml +33 -0
- examples/multi_step/configs/verilog_rl_lora.toml +190 -0
- examples/multi_step/judges/crafter_backend_judge.py +220 -0
- examples/multi_step/judges/verilog_backend_judge.py +234 -0
- examples/multi_step/readme.md +48 -0
- examples/multi_step/verilog_rl_lora.md +218 -0
- examples/qwen_coder/configs/coder_lora_30b.toml +1 -1
- examples/sft/evaluate.py +2 -0
- examples/sft/generate_traces.py +2 -0
- examples/swe/task_app/grpo_swe_mini.py +56 -26
- examples/swe/task_app/hosted/rollout.py +42 -0
- examples/swe/task_app/hosted/test_service.py +5 -6
- examples/task_apps/IMAGE_ONLY_EVAL_QUICKSTART.md +258 -0
- examples/task_apps/TESTING.md +275 -0
- examples/task_apps/__init__.py +0 -0
- examples/task_apps/crafter/CREATE_SFT_DATASET.md +273 -0
- examples/task_apps/crafter/EVAL_IMAGE_ONLY_RESULTS.md +152 -0
- examples/task_apps/crafter/FILTER_COMMAND_STATUS.md +174 -0
- examples/task_apps/crafter/FILTER_COMMAND_SUCCESS.md +268 -0
- examples/task_apps/crafter/QUERY_EXAMPLES.md +203 -0
- examples/task_apps/crafter/README_IMAGE_ONLY_EVAL.md +316 -0
- examples/task_apps/crafter/__init__.py +0 -0
- examples/task_apps/crafter/eval_image_only_gpt4o.toml +28 -0
- examples/task_apps/crafter/eval_text_only_groq_llama.toml +36 -0
- examples/task_apps/crafter/filter_sft_dataset.toml +16 -0
- examples/task_apps/crafter/task_app/__init__.py +5 -0
- examples/{warming_up_to_rl → task_apps/crafter}/task_app/grpo_crafter.py +324 -21
- 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/environment.py +10 -0
- examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/policy.py +76 -7
- examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/envs/crafter/react_agent.py +17 -2
- examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/inference/openai_client.py +25 -3
- examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/policy_routes.py +77 -4
- examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/rollout.py +117 -9
- examples/{warming_up_to_rl → task_apps/crafter}/task_app/synth_envs_hosted/test_service.py +5 -6
- examples/task_apps/crafter/task_app/synth_envs_hosted/utils.py +218 -0
- 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/filter_sft.toml +5 -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 +4 -0
- examples/task_apps/enron/tests/conftest.py +115 -0
- examples/task_apps/enron/tests/integration/__init__.py +4 -0
- examples/task_apps/enron/tests/integration/test_enron_eval.py +179 -0
- examples/task_apps/enron/tests/integration/test_enron_rollout.py +135 -0
- examples/task_apps/enron/tests/unit/__init__.py +4 -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/EVAL_IMAGE_ONLY_COMPLETE.md +283 -0
- examples/task_apps/pokemon_red/EVAL_IMAGE_ONLY_STATUS.md +155 -0
- examples/task_apps/pokemon_red/README.md +357 -0
- examples/task_apps/pokemon_red/README_IMAGE_ONLY_EVAL.md +415 -0
- examples/task_apps/pokemon_red/__init__.py +3 -0
- examples/task_apps/pokemon_red/eval_image_only_gpt4o.toml +29 -0
- examples/task_apps/pokemon_red/eval_pokemon_red_policy.py +225 -0
- examples/task_apps/pokemon_red/pallet_town_rl_config.toml +75 -0
- examples/task_apps/pokemon_red/task_app.py +799 -0
- examples/task_apps/pokemon_red/test_pallet_town_rewards.py +193 -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/filter_sft.toml +5 -0
- examples/task_apps/sokoban/task_app.py +1058 -0
- examples/task_apps/sokoban/tests/__init__.py +4 -0
- examples/task_apps/sokoban/tests/conftest.py +113 -0
- examples/task_apps/sokoban/tests/integration/__init__.py +4 -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 +4 -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 +24 -0
- examples/task_apps/verilog/filter_sft.toml +5 -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 +1166 -0
- examples/task_apps/verilog/task_app/grpo_verilog_task_app.py +145 -0
- examples/task_apps/verilog/tests/__init__.py +4 -0
- examples/task_apps/verilog/tests/conftest.py +115 -0
- examples/task_apps/verilog/tests/integration/__init__.py +4 -0
- examples/task_apps/verilog/tests/integration/test_verilog_eval.py +181 -0
- examples/task_apps/verilog/tests/integration/test_verilog_rollout.py +55 -0
- examples/task_apps/verilog/tests/unit/__init__.py +4 -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/groq_test.py +2 -0
- examples/warming_up_to_rl/run_local_rollout.py +2 -0
- examples/warming_up_to_rl/run_local_rollout_modal.py +2 -0
- examples/warming_up_to_rl/run_local_rollout_parallel.py +2 -0
- examples/warming_up_to_rl/run_local_rollout_traced.py +2 -0
- examples/warming_up_to_rl/run_rollout_remote.py +2 -0
- 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 +2 -2
- synth_ai/api/models/supported.py +1 -0
- synth_ai/api/train/builders.py +25 -11
- synth_ai/api/train/cli.py +12 -6
- synth_ai/api/train/configs/__init__.py +10 -10
- synth_ai/api/train/configs/rl.py +5 -4
- synth_ai/api/train/configs/sft.py +4 -3
- 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 +48 -59
- synth_ai/cli/_modal_wrapper.py +3 -2
- 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 +14 -7
- synth_ai/cli/legacy_root_backup.py +1 -1
- synth_ai/cli/recent.py +1 -1
- synth_ai/cli/rl_demo.py +8 -7
- synth_ai/cli/root.py +0 -97
- synth_ai/cli/status.py +1 -1
- synth_ai/cli/task_apps.py +1922 -190
- synth_ai/cli/traces.py +1 -1
- synth_ai/cli/tui.py +57 -0
- synth_ai/cli/turso.py +1 -1
- synth_ai/cli/watch.py +1 -1
- synth_ai/demos/demo_task_apps/crafter/grpo_crafter_task_app.py +29 -17
- synth_ai/environments/examples/crafter_classic/environment.py +1 -1
- 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 +104 -12
- synth_ai/evals/client.py +58 -61
- synth_ai/jobs/client.py +16 -4
- synth_ai/judge_schemas.py +9 -9
- synth_ai/py.typed +0 -0
- synth_ai/task/__init__.py +24 -5
- synth_ai/task/apps/__init__.py +1 -0
- synth_ai/task/config.py +257 -0
- synth_ai/task/contracts.py +138 -39
- synth_ai/task/proxy.py +48 -56
- synth_ai/task/rubrics/__init__.py +56 -0
- synth_ai/task/rubrics/loaders.py +152 -0
- synth_ai/task/rubrics/models.py +57 -0
- synth_ai/task/rubrics/scoring.py +116 -0
- synth_ai/{rubrics/validators.py → task/rubrics/strict.py} +53 -30
- synth_ai/task/server.py +8 -7
- synth_ai/task/trace_correlation_helpers.py +315 -0
- synth_ai/task/validators.py +413 -6
- synth_ai/tracing_v3/abstractions.py +3 -3
- synth_ai/tracing_v3/decorators.py +7 -3
- synth_ai/tracing_v3/llm_call_record_helpers.py +5 -5
- synth_ai/tracing_v3/replica_sync.py +4 -4
- synth_ai/tracing_v3/serialization.py +5 -5
- synth_ai/tracing_v3/session_tracer.py +16 -6
- synth_ai/tracing_v3/storage/base.py +29 -29
- synth_ai/tracing_v3/storage/config.py +3 -3
- synth_ai/tracing_v3/trace_utils.py +317 -0
- synth_ai/tracing_v3/turso/daemon.py +8 -7
- synth_ai/tracing_v3/turso/native_manager.py +66 -43
- synth_ai/tracing_v3/utils.py +3 -3
- synth_ai/tui/__init__.py +5 -0
- synth_ai/tui/__main__.py +13 -0
- synth_ai/tui/cli/__init__.py +1 -0
- synth_ai/tui/cli/query_experiments.py +164 -0
- synth_ai/tui/cli/query_experiments_v3.py +164 -0
- synth_ai/tui/dashboard.py +906 -0
- {synth_ai-0.2.13.dev1.dist-info → synth_ai-0.2.14.dist-info}/METADATA +4 -1
- {synth_ai-0.2.13.dev1.dist-info → synth_ai-0.2.14.dist-info}/RECORD +278 -126
- examples/agora_ex/README_MoE.md +0 -224
- examples/agora_ex/__init__.py +0 -7
- examples/agora_ex/agora_ex.py +0 -65
- examples/agora_ex/agora_ex_task_app.py +0 -590
- examples/agora_ex/configs/rl_lora_qwen3_moe_2xh200.toml +0 -121
- examples/agora_ex/reward_fn_grpo-human.py +0 -129
- examples/agora_ex/system_prompt_CURRENT.md +0 -63
- examples/agora_ex/task_app/agora_ex_task_app.py +0 -590
- examples/agora_ex/task_app/reward_fn_grpo-human.py +0 -129
- examples/agora_ex/task_app/system_prompt_CURRENT.md +0 -63
- examples/warming_up_to_rl/task_app/synth_envs_hosted/utils.py +0 -62
- synth_ai/rubrics/__init__.py +0 -22
- 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/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/{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.13.dev1.dist-info → synth_ai-0.2.14.dist-info}/WHEEL +0 -0
- {synth_ai-0.2.13.dev1.dist-info → synth_ai-0.2.14.dist-info}/entry_points.txt +0 -0
- {synth_ai-0.2.13.dev1.dist-info → synth_ai-0.2.14.dist-info}/licenses/LICENSE +0 -0
- {synth_ai-0.2.13.dev1.dist-info → synth_ai-0.2.14.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1544 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
import threading
|
|
4
|
+
import queue
|
|
5
|
+
import tempfile
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import hashlib
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Dict, Any, Optional, List
|
|
12
|
+
import numpy as np
|
|
13
|
+
from PIL import Image
|
|
14
|
+
|
|
15
|
+
import mgba.core
|
|
16
|
+
import mgba.log
|
|
17
|
+
import mgba.image
|
|
18
|
+
from mgba._pylib import ffi, lib
|
|
19
|
+
|
|
20
|
+
from .memory_reader import PokemonEmeraldReader
|
|
21
|
+
from utils.state_formatter import save_persistent_world_map, load_persistent_world_map
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# some acknowledgement to https://github.com/dvruette/pygba
|
|
26
|
+
|
|
27
|
+
class MilestoneTracker:
|
|
28
|
+
"""Persistent milestone tracking system integrated with emulator"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, filename: str = None):
|
|
31
|
+
# Setup cache directory
|
|
32
|
+
self.cache_dir = ".pokeagent_cache"
|
|
33
|
+
os.makedirs(self.cache_dir, exist_ok=True)
|
|
34
|
+
|
|
35
|
+
# Use cache folder for runtime milestone file
|
|
36
|
+
if filename is None:
|
|
37
|
+
filename = os.path.join(self.cache_dir, "milestones_progress.json")
|
|
38
|
+
self.filename = filename # Runtime cache file (always in cache directory)
|
|
39
|
+
self.loaded_state_milestones_file = None # Track if we loaded from a state-specific file
|
|
40
|
+
self.milestones = {}
|
|
41
|
+
self.latest_milestone = None
|
|
42
|
+
self.latest_split_time = "00:00:00"
|
|
43
|
+
# Don't automatically load from file - only load when explicitly requested
|
|
44
|
+
|
|
45
|
+
def load_from_file(self):
|
|
46
|
+
"""Load milestone progress from file"""
|
|
47
|
+
try:
|
|
48
|
+
if os.path.exists(self.filename):
|
|
49
|
+
with open(self.filename, 'r') as f:
|
|
50
|
+
data = json.load(f)
|
|
51
|
+
self.milestones = data.get('milestones', {})
|
|
52
|
+
|
|
53
|
+
# Determine the latest completed milestone based on timestamps
|
|
54
|
+
latest_timestamp = 0
|
|
55
|
+
latest_milestone_id = None
|
|
56
|
+
for milestone_id, milestone_data in self.milestones.items():
|
|
57
|
+
if milestone_data.get('completed', False):
|
|
58
|
+
timestamp = milestone_data.get('timestamp', 0)
|
|
59
|
+
if timestamp > latest_timestamp:
|
|
60
|
+
latest_timestamp = timestamp
|
|
61
|
+
latest_milestone_id = milestone_id
|
|
62
|
+
|
|
63
|
+
# Set the latest milestone if we found one
|
|
64
|
+
if latest_milestone_id:
|
|
65
|
+
self.latest_milestone = latest_milestone_id
|
|
66
|
+
self.latest_split_time = self.milestones[latest_milestone_id].get('split_formatted', '00:00:00')
|
|
67
|
+
logger.info(f"Latest milestone from file: {latest_milestone_id}")
|
|
68
|
+
|
|
69
|
+
logger.info(f"Loaded {len(self.milestones)} milestone records from {self.filename}")
|
|
70
|
+
else:
|
|
71
|
+
logger.info(f"No existing milestone file found, starting fresh")
|
|
72
|
+
self.milestones = {}
|
|
73
|
+
except Exception as e:
|
|
74
|
+
logger.warning(f"Error loading milestones from file: {e}")
|
|
75
|
+
self.milestones = {}
|
|
76
|
+
|
|
77
|
+
def save_to_file(self):
|
|
78
|
+
"""Save milestone progress to file"""
|
|
79
|
+
try:
|
|
80
|
+
data = {
|
|
81
|
+
'milestones': self.milestones,
|
|
82
|
+
'last_updated': time.time(),
|
|
83
|
+
'version': '1.0'
|
|
84
|
+
}
|
|
85
|
+
with open(self.filename, 'w') as f:
|
|
86
|
+
json.dump(data, f, indent=2)
|
|
87
|
+
logger.debug(f"Saved milestone progress to {self.filename}")
|
|
88
|
+
except Exception as e:
|
|
89
|
+
logger.warning(f"Error saving milestones to file: {e}")
|
|
90
|
+
|
|
91
|
+
def mark_completed(self, milestone_id: str, timestamp: float = None):
|
|
92
|
+
"""Mark a milestone as completed and log split time"""
|
|
93
|
+
if timestamp is None:
|
|
94
|
+
timestamp = time.time()
|
|
95
|
+
|
|
96
|
+
if milestone_id not in self.milestones or not self.milestones[milestone_id].get('completed', False):
|
|
97
|
+
# Calculate split time from previous milestone or start
|
|
98
|
+
split_time = self._calculate_split_time(milestone_id, timestamp)
|
|
99
|
+
|
|
100
|
+
self.milestones[milestone_id] = {
|
|
101
|
+
'completed': True,
|
|
102
|
+
'timestamp': timestamp,
|
|
103
|
+
'first_completed': timestamp,
|
|
104
|
+
'split_time': split_time,
|
|
105
|
+
'split_formatted': self._format_time(split_time),
|
|
106
|
+
'total_time': self._calculate_total_time(timestamp),
|
|
107
|
+
'total_formatted': self._format_time(self._calculate_total_time(timestamp))
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# Store the latest completed milestone for easy access
|
|
111
|
+
self.latest_milestone = milestone_id
|
|
112
|
+
self.latest_split_time = self._format_time(split_time)
|
|
113
|
+
|
|
114
|
+
logger.info(f"Milestone completed: {milestone_id} (Split: {self._format_time(split_time)})")
|
|
115
|
+
self.save_to_file()
|
|
116
|
+
return True
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
def is_completed(self, milestone_id: str) -> bool:
|
|
120
|
+
"""Check if a milestone is completed"""
|
|
121
|
+
return self.milestones.get(milestone_id, {}).get('completed', False)
|
|
122
|
+
|
|
123
|
+
def get_milestone_data(self, milestone_id: str) -> dict:
|
|
124
|
+
"""Get milestone data"""
|
|
125
|
+
return self.milestones.get(milestone_id, {'completed': False, 'timestamp': None})
|
|
126
|
+
|
|
127
|
+
def reset_milestone(self, milestone_id: str):
|
|
128
|
+
"""Reset a milestone (for testing)"""
|
|
129
|
+
if milestone_id in self.milestones:
|
|
130
|
+
del self.milestones[milestone_id]
|
|
131
|
+
self.save_to_file()
|
|
132
|
+
logger.info(f"Reset milestone: {milestone_id}")
|
|
133
|
+
|
|
134
|
+
def _calculate_split_time(self, milestone_id: str, timestamp: float) -> float:
|
|
135
|
+
"""Calculate split time from previous milestone completion or start"""
|
|
136
|
+
# Define milestone order for split calculation
|
|
137
|
+
milestone_order = [
|
|
138
|
+
# Phase 1: Game Initialization
|
|
139
|
+
"GAME_RUNNING", "PLAYER_NAME_SET", "INTRO_CUTSCENE_COMPLETE",
|
|
140
|
+
|
|
141
|
+
# Phase 2: Tutorial & Starting Town
|
|
142
|
+
"LITTLEROOT_TOWN", "PLAYER_HOUSE_ENTERED", "PLAYER_BEDROOM",
|
|
143
|
+
"RIVAL_HOUSE", "RIVAL_BEDROOM",
|
|
144
|
+
|
|
145
|
+
# Phase 3: Professor Birch & Starter
|
|
146
|
+
"ROUTE_101", "STARTER_CHOSEN", "BIRCH_LAB_VISITED",
|
|
147
|
+
|
|
148
|
+
# Phase 4: Rival
|
|
149
|
+
"OLDALE_TOWN", "ROUTE_103", "RECEIVED_POKEDEX",
|
|
150
|
+
|
|
151
|
+
# Phase 5: Route 102 & Petalburg
|
|
152
|
+
"ROUTE_102", "PETALBURG_CITY", "DAD_FIRST_MEETING", "GYM_EXPLANATION",
|
|
153
|
+
|
|
154
|
+
# Phase 6: Road to Rustboro City
|
|
155
|
+
"ROUTE_104_SOUTH", "PETALBURG_WOODS", "TEAM_AQUA_GRUNT_DEFEATED",
|
|
156
|
+
"ROUTE_104_NORTH", "RUSTBORO_CITY",
|
|
157
|
+
|
|
158
|
+
# Phase 7: First Gym Challenge
|
|
159
|
+
"RUSTBORO_GYM_ENTERED", "ROXANNE_DEFEATED", "FIRST_GYM_COMPLETE",
|
|
160
|
+
|
|
161
|
+
# Badge milestones (tracked separately)
|
|
162
|
+
"STONE_BADGE"
|
|
163
|
+
]
|
|
164
|
+
|
|
165
|
+
try:
|
|
166
|
+
# Special case for first milestone - split time is 0
|
|
167
|
+
if milestone_id == "GAME_RUNNING":
|
|
168
|
+
return 0.0
|
|
169
|
+
|
|
170
|
+
if milestone_id not in milestone_order:
|
|
171
|
+
# For unlisted milestones, find the most recent completion
|
|
172
|
+
latest_timestamp = 0
|
|
173
|
+
for _, data in self.milestones.items():
|
|
174
|
+
if data.get('completed', False) and data.get('timestamp', 0) > latest_timestamp:
|
|
175
|
+
latest_timestamp = data.get('timestamp', 0)
|
|
176
|
+
return timestamp - latest_timestamp if latest_timestamp > 0 else 0.0
|
|
177
|
+
|
|
178
|
+
# Find the previous milestone in the order
|
|
179
|
+
current_index = milestone_order.index(milestone_id)
|
|
180
|
+
|
|
181
|
+
# Look backwards for the most recent completed milestone
|
|
182
|
+
for i in range(current_index - 1, -1, -1):
|
|
183
|
+
prev_milestone = milestone_order[i]
|
|
184
|
+
if self.is_completed(prev_milestone):
|
|
185
|
+
prev_timestamp = self.milestones[prev_milestone].get('timestamp', 0)
|
|
186
|
+
return timestamp - prev_timestamp
|
|
187
|
+
|
|
188
|
+
# If no previous milestone found, calculate from start if we have GAME_RUNNING
|
|
189
|
+
if self.is_completed("GAME_RUNNING"):
|
|
190
|
+
start_timestamp = self.milestones["GAME_RUNNING"].get('timestamp', 0)
|
|
191
|
+
return timestamp - start_timestamp
|
|
192
|
+
|
|
193
|
+
# Fallback - no split time available
|
|
194
|
+
return 0.0
|
|
195
|
+
|
|
196
|
+
except Exception as e:
|
|
197
|
+
logger.warning(f"Error calculating split time for {milestone_id}: {e}")
|
|
198
|
+
return 0.0
|
|
199
|
+
|
|
200
|
+
def _format_time(self, seconds: float) -> str:
|
|
201
|
+
"""Format time in HH:MM:SS format"""
|
|
202
|
+
try:
|
|
203
|
+
hours = int(seconds // 3600)
|
|
204
|
+
minutes = int((seconds % 3600) // 60)
|
|
205
|
+
secs = int(seconds % 60)
|
|
206
|
+
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
|
|
207
|
+
except:
|
|
208
|
+
return "00:00:00"
|
|
209
|
+
|
|
210
|
+
def _calculate_total_time(self, timestamp: float) -> float:
|
|
211
|
+
"""Calculate total time from game start"""
|
|
212
|
+
try:
|
|
213
|
+
if self.is_completed("GAME_RUNNING"):
|
|
214
|
+
start_timestamp = self.milestones["GAME_RUNNING"].get('timestamp', timestamp)
|
|
215
|
+
return timestamp - start_timestamp
|
|
216
|
+
return 0.0
|
|
217
|
+
except:
|
|
218
|
+
return 0.0
|
|
219
|
+
|
|
220
|
+
def get_latest_milestone_info(self) -> tuple:
|
|
221
|
+
"""Get the latest milestone information for submission logging
|
|
222
|
+
Returns: (milestone_name, split_time_formatted, total_time_formatted)
|
|
223
|
+
"""
|
|
224
|
+
if self.latest_milestone:
|
|
225
|
+
milestone_data = self.milestones.get(self.latest_milestone, {})
|
|
226
|
+
split_formatted = milestone_data.get('split_formatted', '00:00:00')
|
|
227
|
+
total_formatted = milestone_data.get('total_formatted', '00:00:00')
|
|
228
|
+
return (self.latest_milestone, split_formatted, total_formatted)
|
|
229
|
+
return ("NONE", "00:00:00", "00:00:00")
|
|
230
|
+
|
|
231
|
+
def get_all_completed_milestones(self) -> list:
|
|
232
|
+
"""Get a list of all completed milestones with their times"""
|
|
233
|
+
completed = []
|
|
234
|
+
for milestone_id, data in self.milestones.items():
|
|
235
|
+
if data.get('completed', False):
|
|
236
|
+
completed.append({
|
|
237
|
+
'id': milestone_id,
|
|
238
|
+
'timestamp': data.get('timestamp', 0),
|
|
239
|
+
'split_time': data.get('split_formatted', '00:00:00'),
|
|
240
|
+
'total_time': data.get('total_formatted', '00:00:00')
|
|
241
|
+
})
|
|
242
|
+
return sorted(completed, key=lambda x: x['timestamp'])
|
|
243
|
+
|
|
244
|
+
def reset_all(self):
|
|
245
|
+
"""Reset all milestones (for testing)"""
|
|
246
|
+
self.milestones = {}
|
|
247
|
+
self.save_to_file()
|
|
248
|
+
logger.info("Reset all milestones")
|
|
249
|
+
|
|
250
|
+
def load_milestones_for_state(self, state_filename: str = None):
|
|
251
|
+
"""Load milestones from file, optionally with a specific state filename"""
|
|
252
|
+
if state_filename:
|
|
253
|
+
# If a state filename is provided, try to load milestones from a corresponding file
|
|
254
|
+
# Get the directory and base name of the state file
|
|
255
|
+
state_dir = os.path.dirname(state_filename)
|
|
256
|
+
base_name = os.path.splitext(os.path.basename(state_filename))[0]
|
|
257
|
+
milestone_filename = os.path.join(state_dir, f"{base_name}_milestones.json")
|
|
258
|
+
|
|
259
|
+
# Track that we loaded from a state-specific file
|
|
260
|
+
self.loaded_state_milestones_file = milestone_filename
|
|
261
|
+
logger.info(f"Loading milestones from state-specific file: {milestone_filename}")
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
# Temporarily change filename to load from state file
|
|
265
|
+
original_filename = self.filename
|
|
266
|
+
self.filename = milestone_filename
|
|
267
|
+
self.load_from_file()
|
|
268
|
+
# Restore runtime cache filename (always in main directory)
|
|
269
|
+
self.filename = original_filename
|
|
270
|
+
logger.info(f"Loaded {len(self.milestones)} milestones from state {state_filename}")
|
|
271
|
+
logger.info(f"Runtime milestone cache will be saved to: {self.filename}")
|
|
272
|
+
except FileNotFoundError:
|
|
273
|
+
logger.info(f"Milestone file not found: {milestone_filename}, starting fresh milestones for this state")
|
|
274
|
+
# Start with empty milestones for this state
|
|
275
|
+
self.milestones = {}
|
|
276
|
+
# Don't create the state file, just use runtime cache
|
|
277
|
+
logger.info(f"Runtime milestone cache will be saved to: {self.filename}")
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.error(f"Error loading milestone file {milestone_filename}: {e}")
|
|
280
|
+
# Fall back to default milestone file
|
|
281
|
+
logger.info(f"Using runtime milestone cache: {self.filename}")
|
|
282
|
+
self.load_from_file()
|
|
283
|
+
else:
|
|
284
|
+
# No state filename provided, use default milestone file in cache
|
|
285
|
+
self.loaded_state_milestones_file = None
|
|
286
|
+
self.filename = os.path.join(self.cache_dir, "milestones_progress.json")
|
|
287
|
+
logger.info(f"Loading milestones from default file: {self.filename}")
|
|
288
|
+
self.load_from_file()
|
|
289
|
+
|
|
290
|
+
def save_milestones_for_state(self, state_filename: str = None):
|
|
291
|
+
"""Save milestones to file, optionally with a specific state filename"""
|
|
292
|
+
if state_filename:
|
|
293
|
+
# If a state filename is provided, save milestones to a corresponding file
|
|
294
|
+
# Get the directory and base name of the state file
|
|
295
|
+
state_dir = os.path.dirname(state_filename)
|
|
296
|
+
base_name = os.path.splitext(os.path.basename(state_filename))[0]
|
|
297
|
+
milestone_filename = os.path.join(state_dir, f"{base_name}_milestones.json")
|
|
298
|
+
|
|
299
|
+
original_filename = self.filename
|
|
300
|
+
self.filename = milestone_filename
|
|
301
|
+
logger.info(f"Saving {len(self.milestones)} milestones to state-specific file: {milestone_filename}")
|
|
302
|
+
|
|
303
|
+
try:
|
|
304
|
+
self.save_to_file()
|
|
305
|
+
logger.info(f"Successfully saved milestones to {milestone_filename}")
|
|
306
|
+
except Exception as e:
|
|
307
|
+
logger.error(f"Error saving milestone file {milestone_filename}: {e}")
|
|
308
|
+
# Fall back to default milestone file
|
|
309
|
+
self.filename = original_filename
|
|
310
|
+
self.save_to_file()
|
|
311
|
+
return original_filename
|
|
312
|
+
finally:
|
|
313
|
+
# Restore original filename
|
|
314
|
+
self.filename = original_filename
|
|
315
|
+
|
|
316
|
+
return milestone_filename
|
|
317
|
+
else:
|
|
318
|
+
# Save to default milestone file
|
|
319
|
+
logger.info(f"Saving {len(self.milestones)} milestones to default file: {self.filename}")
|
|
320
|
+
self.save_to_file()
|
|
321
|
+
return self.filename
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
class EmeraldEmulator:
|
|
325
|
+
"""emulator wrapper for Pokémon Emerald with headless frame capture and scripted inputs."""
|
|
326
|
+
|
|
327
|
+
def __init__(self, rom_path: str, headless: bool = True, sound: bool = False):
|
|
328
|
+
self.rom_path = rom_path
|
|
329
|
+
self.headless = headless
|
|
330
|
+
self.sound = sound
|
|
331
|
+
|
|
332
|
+
self.gba = None
|
|
333
|
+
self.core = None
|
|
334
|
+
self.width = 240
|
|
335
|
+
self.height = 160
|
|
336
|
+
self.running = False
|
|
337
|
+
|
|
338
|
+
self.frame_queue = queue.Queue(maxsize=10)
|
|
339
|
+
self.current_frame = None
|
|
340
|
+
self.frame_thread = None
|
|
341
|
+
|
|
342
|
+
# Memory reader for accessing game state
|
|
343
|
+
self.memory_reader = None
|
|
344
|
+
|
|
345
|
+
# Memory cache for efficient reading
|
|
346
|
+
self._mem_cache = {}
|
|
347
|
+
|
|
348
|
+
# Setup cache directory
|
|
349
|
+
self.cache_dir = ".pokeagent_cache"
|
|
350
|
+
os.makedirs(self.cache_dir, exist_ok=True)
|
|
351
|
+
|
|
352
|
+
# Milestone tracker for progress tracking (using cache file)
|
|
353
|
+
self.milestone_tracker = MilestoneTracker(os.path.join(self.cache_dir, "milestones_progress.json"))
|
|
354
|
+
|
|
355
|
+
# Dialog state tracking for FPS adjustment
|
|
356
|
+
self._cached_dialog_state = False
|
|
357
|
+
self._last_dialog_check_time = 0
|
|
358
|
+
self._dialog_check_interval = 0.05 # Check dialog state every 50ms (more responsive)
|
|
359
|
+
|
|
360
|
+
# Track currently loaded state file
|
|
361
|
+
self._current_state_file = None
|
|
362
|
+
|
|
363
|
+
# Define key mapping for mgba
|
|
364
|
+
self.KEY_MAP = {
|
|
365
|
+
"a": lib.GBA_KEY_A,
|
|
366
|
+
"b": lib.GBA_KEY_B,
|
|
367
|
+
"start": lib.GBA_KEY_START,
|
|
368
|
+
"select": lib.GBA_KEY_SELECT,
|
|
369
|
+
"up": lib.GBA_KEY_UP,
|
|
370
|
+
"down": lib.GBA_KEY_DOWN,
|
|
371
|
+
"left": lib.GBA_KEY_LEFT,
|
|
372
|
+
"right": lib.GBA_KEY_RIGHT,
|
|
373
|
+
"l": lib.GBA_KEY_L,
|
|
374
|
+
"r": lib.GBA_KEY_R
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
def initialize(self):
|
|
378
|
+
"""Load ROM and set up emulator"""
|
|
379
|
+
try:
|
|
380
|
+
# Prevents relentless spamming to stdout by libmgba.
|
|
381
|
+
mgba.log.silence()
|
|
382
|
+
|
|
383
|
+
# Create a temporary directory and copy the gba file into it
|
|
384
|
+
# this is necessary to prevent mgba from overwriting the save file (and to prevent crashes)
|
|
385
|
+
tmp_dir = Path(tempfile.mkdtemp())
|
|
386
|
+
tmp_gba = tmp_dir / "rom.gba"
|
|
387
|
+
tmp_gba.write_bytes(Path(self.rom_path).read_bytes())
|
|
388
|
+
|
|
389
|
+
# Load the core
|
|
390
|
+
self.core = mgba.core.load_path(str(tmp_gba))
|
|
391
|
+
if self.core is None:
|
|
392
|
+
raise ValueError(f"Failed to load GBA file: {self.rom_path}")
|
|
393
|
+
|
|
394
|
+
# Auto-load save if it exists
|
|
395
|
+
self.core.autoload_save()
|
|
396
|
+
self.core.reset()
|
|
397
|
+
|
|
398
|
+
# Get dimensions from the core
|
|
399
|
+
self.width, self.height = self.core.desired_video_dimensions()
|
|
400
|
+
logger.info(f"mgba initialized with ROM: {self.rom_path} and dimensions: {self.width}x{self.height}")
|
|
401
|
+
|
|
402
|
+
# Set up video buffer for frame capture using mgba.image.Image
|
|
403
|
+
self.video_buffer = mgba.image.Image(self.width, self.height)
|
|
404
|
+
self.core.set_video_buffer(self.video_buffer)
|
|
405
|
+
self.core.reset() # Reset after setting video buffer
|
|
406
|
+
|
|
407
|
+
# Initialize memory reader
|
|
408
|
+
self.memory_reader = PokemonEmeraldReader(self.core)
|
|
409
|
+
|
|
410
|
+
# Set up callback for memory reader to invalidate emulator cache on area transitions
|
|
411
|
+
def invalidate_emulator_cache():
|
|
412
|
+
if hasattr(self, '_cached_state'):
|
|
413
|
+
delattr(self, '_cached_state')
|
|
414
|
+
if hasattr(self, '_cached_state_time'):
|
|
415
|
+
delattr(self, '_cached_state_time')
|
|
416
|
+
|
|
417
|
+
self.memory_reader._emulator_cache_invalidator = invalidate_emulator_cache
|
|
418
|
+
|
|
419
|
+
# Set up frame callback to invalidate memory cache
|
|
420
|
+
self.core.add_frame_callback(self._invalidate_mem_cache)
|
|
421
|
+
|
|
422
|
+
logger.info(f"mgba initialized with ROM: {self.rom_path}")
|
|
423
|
+
except Exception as e:
|
|
424
|
+
raise RuntimeError(f"Failed to initialize mgba: {e}")
|
|
425
|
+
|
|
426
|
+
def _invalidate_mem_cache(self):
|
|
427
|
+
"""Invalidate memory cache when frame changes"""
|
|
428
|
+
self._mem_cache = {}
|
|
429
|
+
|
|
430
|
+
def _get_memory_region(self, region_id: int):
|
|
431
|
+
"""Get memory region for efficient reading"""
|
|
432
|
+
if region_id not in self._mem_cache:
|
|
433
|
+
mem_core = self.core.memory.u8._core
|
|
434
|
+
size = ffi.new("size_t *")
|
|
435
|
+
ptr = ffi.cast("uint8_t *", mem_core.getMemoryBlock(mem_core, region_id, size))
|
|
436
|
+
self._mem_cache[region_id] = ffi.buffer(ptr, size[0])[:]
|
|
437
|
+
return self._mem_cache[region_id]
|
|
438
|
+
|
|
439
|
+
def read_memory(self, address: int, size: int = 1):
|
|
440
|
+
"""Read memory at given address"""
|
|
441
|
+
region_id = address >> lib.BASE_OFFSET
|
|
442
|
+
mem_region = self._get_memory_region(region_id)
|
|
443
|
+
mask = len(mem_region) - 1
|
|
444
|
+
address &= mask
|
|
445
|
+
return mem_region[address:address + size]
|
|
446
|
+
|
|
447
|
+
def read_u8(self, address: int):
|
|
448
|
+
"""Read unsigned 8-bit value"""
|
|
449
|
+
return int.from_bytes(self.read_memory(address, 1), byteorder='little', signed=False)
|
|
450
|
+
|
|
451
|
+
def read_u16(self, address: int):
|
|
452
|
+
"""Read unsigned 16-bit value"""
|
|
453
|
+
return int.from_bytes(self.read_memory(address, 2), byteorder='little', signed=False)
|
|
454
|
+
|
|
455
|
+
def read_u32(self, address: int):
|
|
456
|
+
"""Read unsigned 32-bit value"""
|
|
457
|
+
return int.from_bytes(self.read_memory(address, 4), byteorder='little', signed=False)
|
|
458
|
+
|
|
459
|
+
def tick(self, frames: int = 1):
|
|
460
|
+
"""Advance emulator by given number of frames"""
|
|
461
|
+
if self.core:
|
|
462
|
+
for _ in range(frames):
|
|
463
|
+
self.core.run_frame()
|
|
464
|
+
|
|
465
|
+
def get_current_fps(self, base_fps: int = 30) -> int:
|
|
466
|
+
"""Get current FPS - quadruples during dialog for faster text progression"""
|
|
467
|
+
# Use cached dialog state for performance
|
|
468
|
+
return base_fps * 4 if self._cached_dialog_state else base_fps
|
|
469
|
+
|
|
470
|
+
def _update_dialog_state_cache(self):
|
|
471
|
+
"""Update cached dialog state (called periodically for performance)"""
|
|
472
|
+
import time
|
|
473
|
+
current_time = time.time()
|
|
474
|
+
|
|
475
|
+
# Only check dialog state periodically to avoid performance issues
|
|
476
|
+
if current_time - self._last_dialog_check_time >= self._dialog_check_interval:
|
|
477
|
+
if self.memory_reader:
|
|
478
|
+
new_dialog_state = self.memory_reader.is_in_dialog()
|
|
479
|
+
if new_dialog_state != self._cached_dialog_state:
|
|
480
|
+
self._cached_dialog_state = new_dialog_state
|
|
481
|
+
if new_dialog_state:
|
|
482
|
+
logger.debug("🎯 Dialog detected - switching to 4x FPS")
|
|
483
|
+
else:
|
|
484
|
+
logger.debug("✅ Dialog ended - reverting to normal FPS")
|
|
485
|
+
self._last_dialog_check_time = current_time
|
|
486
|
+
|
|
487
|
+
def press_key(self, key: str, frames: int = 2):
|
|
488
|
+
"""Press a key for specified number of frames"""
|
|
489
|
+
if key not in self.KEY_MAP:
|
|
490
|
+
raise ValueError(f"Invalid key: {key}")
|
|
491
|
+
if frames < 2:
|
|
492
|
+
raise ValueError("Cannot press a key for less than 2 frames.")
|
|
493
|
+
|
|
494
|
+
key_code = self.KEY_MAP[key]
|
|
495
|
+
self.core.add_keys(key_code)
|
|
496
|
+
self.tick(frames - 1)
|
|
497
|
+
self.core.clear_keys(key_code)
|
|
498
|
+
self.tick(1)
|
|
499
|
+
|
|
500
|
+
def press_buttons(self, buttons: List[str], hold_frames: int = 10, release_frames: int = 10):
|
|
501
|
+
"""Press a sequence of buttons"""
|
|
502
|
+
if not self.core:
|
|
503
|
+
return "Emulator not initialized"
|
|
504
|
+
|
|
505
|
+
for button in buttons:
|
|
506
|
+
if button.lower() not in self.KEY_MAP:
|
|
507
|
+
logger.warning(f"Unknown button: {button}")
|
|
508
|
+
continue
|
|
509
|
+
|
|
510
|
+
self.press_key(button.lower(), hold_frames)
|
|
511
|
+
|
|
512
|
+
self.tick(release_frames)
|
|
513
|
+
return f"Pressed: {'+'.join(buttons)}"
|
|
514
|
+
|
|
515
|
+
def run_frame_with_buttons(self, buttons: List[str]):
|
|
516
|
+
"""Set buttons and advance one frame."""
|
|
517
|
+
if not self.core:
|
|
518
|
+
return
|
|
519
|
+
|
|
520
|
+
# Set all buttons for one frame
|
|
521
|
+
for button in buttons:
|
|
522
|
+
if button.lower() in self.KEY_MAP:
|
|
523
|
+
key_code = self.KEY_MAP[button.lower()]
|
|
524
|
+
self.core.add_keys(key_code)
|
|
525
|
+
|
|
526
|
+
self.core.run_frame()
|
|
527
|
+
|
|
528
|
+
# Clear all buttons
|
|
529
|
+
for button in buttons:
|
|
530
|
+
if button.lower() in self.KEY_MAP:
|
|
531
|
+
key_code = self.KEY_MAP[button.lower()]
|
|
532
|
+
self.core.clear_keys(key_code)
|
|
533
|
+
|
|
534
|
+
# Update dialog state cache for FPS adjustment
|
|
535
|
+
self._update_dialog_state_cache()
|
|
536
|
+
|
|
537
|
+
# Clear dialogue cache if A button was pressed (dismisses dialogue)
|
|
538
|
+
if buttons and any(button.lower() == 'a' for button in buttons):
|
|
539
|
+
if self.memory_reader:
|
|
540
|
+
self.memory_reader.clear_dialogue_cache_on_button_press()
|
|
541
|
+
|
|
542
|
+
# Clear state cache after action to ensure fresh data
|
|
543
|
+
if hasattr(self, '_cached_state'):
|
|
544
|
+
delattr(self, '_cached_state')
|
|
545
|
+
if hasattr(self, '_cached_state_time'):
|
|
546
|
+
delattr(self, '_cached_state_time')
|
|
547
|
+
|
|
548
|
+
def get_screenshot(self) -> Optional[Image.Image]:
|
|
549
|
+
"""Return the current frame as a PIL image"""
|
|
550
|
+
if not self.core or not self.video_buffer:
|
|
551
|
+
return None
|
|
552
|
+
|
|
553
|
+
try:
|
|
554
|
+
# Use the built-in to_pil() method from mgba.image.Image
|
|
555
|
+
if hasattr(self.video_buffer, 'to_pil'):
|
|
556
|
+
screenshot = self.video_buffer.to_pil()
|
|
557
|
+
if screenshot:
|
|
558
|
+
screenshot = screenshot.convert("RGB")
|
|
559
|
+
return screenshot
|
|
560
|
+
else:
|
|
561
|
+
logger.warning("mgba.image.Image does not have to_pil method")
|
|
562
|
+
return None
|
|
563
|
+
else:
|
|
564
|
+
logger.warning("mgba.image.Image does not have to_pil method")
|
|
565
|
+
return None
|
|
566
|
+
except Exception as e:
|
|
567
|
+
logger.error(f"Failed to get screenshot: {e}")
|
|
568
|
+
return None
|
|
569
|
+
|
|
570
|
+
def save_state(self, path: Optional[str] = None) -> Optional[bytes]:
|
|
571
|
+
"""Save current emulator state to file or return as bytes"""
|
|
572
|
+
if not self.core:
|
|
573
|
+
return None
|
|
574
|
+
|
|
575
|
+
try:
|
|
576
|
+
# Get the raw state data
|
|
577
|
+
raw_data = self.core.save_raw_state()
|
|
578
|
+
|
|
579
|
+
# Convert CFFI object to bytes if needed
|
|
580
|
+
if hasattr(raw_data, 'buffer'):
|
|
581
|
+
data = bytes(raw_data.buffer)
|
|
582
|
+
elif hasattr(raw_data, '__len__'):
|
|
583
|
+
data = bytes(raw_data)
|
|
584
|
+
else:
|
|
585
|
+
data = raw_data
|
|
586
|
+
|
|
587
|
+
if path:
|
|
588
|
+
with open(path, 'wb') as f:
|
|
589
|
+
f.write(data)
|
|
590
|
+
logger.info(f"State saved to {path}")
|
|
591
|
+
|
|
592
|
+
# Save corresponding milestones for this state
|
|
593
|
+
milestone_filename = self.milestone_tracker.save_milestones_for_state(path)
|
|
594
|
+
logger.info(f"Milestones saved to {milestone_filename}")
|
|
595
|
+
|
|
596
|
+
# Save the persistent location grids (contains all map data)
|
|
597
|
+
self._save_persistent_grids_for_state(path)
|
|
598
|
+
|
|
599
|
+
return data
|
|
600
|
+
except Exception as e:
|
|
601
|
+
logger.error(f"Failed to save state: {e}")
|
|
602
|
+
return None
|
|
603
|
+
|
|
604
|
+
def load_state(self, path: Optional[str] = None, state_bytes: Optional[bytes] = None):
|
|
605
|
+
"""Load emulator state from file or memory"""
|
|
606
|
+
if not self.core:
|
|
607
|
+
return
|
|
608
|
+
|
|
609
|
+
try:
|
|
610
|
+
if path:
|
|
611
|
+
with open(path, 'rb') as f:
|
|
612
|
+
state_bytes = f.read()
|
|
613
|
+
if state_bytes:
|
|
614
|
+
# Ensure state_bytes is actually bytes
|
|
615
|
+
if not isinstance(state_bytes, bytes):
|
|
616
|
+
state_bytes = bytes(state_bytes)
|
|
617
|
+
self.core.load_raw_state(state_bytes)
|
|
618
|
+
logger.info("State loaded.")
|
|
619
|
+
|
|
620
|
+
# Reset dialog tracking and invalidate map cache when loading new state
|
|
621
|
+
if self.memory_reader:
|
|
622
|
+
self.memory_reader.reset_dialog_tracking()
|
|
623
|
+
# Don't clear buffer address on state load to avoid expensive rescans
|
|
624
|
+
self.memory_reader.invalidate_map_cache(clear_buffer_address=False)
|
|
625
|
+
|
|
626
|
+
# Persistent location maps will be loaded from the state file later
|
|
627
|
+
|
|
628
|
+
# Run a frame to ensure memory is properly loaded
|
|
629
|
+
self.core.run_frame()
|
|
630
|
+
|
|
631
|
+
# Only find map buffer addresses if we don't have them cached
|
|
632
|
+
# This avoids expensive memory scanning on every state load
|
|
633
|
+
if not self.memory_reader._map_buffer_addr:
|
|
634
|
+
if not self.memory_reader._find_map_buffer_addresses():
|
|
635
|
+
logger.warning("Could not find map buffer addresses after state load")
|
|
636
|
+
else:
|
|
637
|
+
logger.info(f"Map buffer found at 0x{self.memory_reader._map_buffer_addr:08X}")
|
|
638
|
+
else:
|
|
639
|
+
logger.debug(f"Using cached map buffer at 0x{self.memory_reader._map_buffer_addr:08X}")
|
|
640
|
+
|
|
641
|
+
# Set the current state file for both emulator and memory reader
|
|
642
|
+
self._current_state_file = path
|
|
643
|
+
if self.memory_reader:
|
|
644
|
+
self.memory_reader._current_state_file = path
|
|
645
|
+
|
|
646
|
+
# Load corresponding milestones for this state
|
|
647
|
+
if path:
|
|
648
|
+
# print( Loading state from path: {path}")
|
|
649
|
+
# Copy state files to cache first
|
|
650
|
+
self._copy_state_files_to_cache(path)
|
|
651
|
+
# Load milestones from cache file
|
|
652
|
+
cache_milestones_file = os.path.join(self.cache_dir, "milestones_progress.json")
|
|
653
|
+
if os.path.exists(cache_milestones_file):
|
|
654
|
+
# Update filename and then load
|
|
655
|
+
self.milestone_tracker.filename = cache_milestones_file
|
|
656
|
+
self.milestone_tracker.load_from_file()
|
|
657
|
+
logger.info(f"Milestones loaded from cache file: {cache_milestones_file}")
|
|
658
|
+
else:
|
|
659
|
+
# Fallback to state-specific file
|
|
660
|
+
self.milestone_tracker.load_milestones_for_state(path)
|
|
661
|
+
logger.info(f"Milestones loaded for state {path}")
|
|
662
|
+
|
|
663
|
+
# Load the persistent location grids (contains all map data)
|
|
664
|
+
# print( About to call _load_persistent_grids_for_state")
|
|
665
|
+
self._load_persistent_grids_for_state(path)
|
|
666
|
+
# print( Completed _load_persistent_grids_for_state")
|
|
667
|
+
except Exception as e:
|
|
668
|
+
logger.error(f"Failed to load state: {e}")
|
|
669
|
+
|
|
670
|
+
def _save_persistent_grids_for_state(self, state_filename: str):
|
|
671
|
+
"""Save persistent location grids for a specific state file"""
|
|
672
|
+
try:
|
|
673
|
+
# Get the directory and base name of the state file
|
|
674
|
+
state_dir = os.path.dirname(state_filename)
|
|
675
|
+
base_name = os.path.splitext(os.path.basename(state_filename))[0]
|
|
676
|
+
|
|
677
|
+
# Only save grids for non-manual saves (splits, checkpoints, etc.)
|
|
678
|
+
# For manual saves, we only need the map_stitcher.json
|
|
679
|
+
if not base_name.startswith("manual_save"):
|
|
680
|
+
grids_filename = os.path.join(state_dir, f"{base_name}_grids.json")
|
|
681
|
+
# Save the persistent grids
|
|
682
|
+
save_persistent_world_map(grids_filename)
|
|
683
|
+
logger.info(f"Persistent grids saved to {grids_filename}")
|
|
684
|
+
|
|
685
|
+
# Always update and save MapStitcher data
|
|
686
|
+
if hasattr(self, 'memory_reader') and self.memory_reader:
|
|
687
|
+
# For manual saves, copy the current map_stitcher.json
|
|
688
|
+
if base_name.startswith("manual_save"):
|
|
689
|
+
# Copy the current map_stitcher_data.json from cache to manual_save_map_stitcher.json
|
|
690
|
+
cache_dir = ".pokeagent_cache"
|
|
691
|
+
current_stitcher_file = os.path.join(cache_dir, "map_stitcher_data.json")
|
|
692
|
+
|
|
693
|
+
# Also check for the old location in case it exists
|
|
694
|
+
if not os.path.exists(current_stitcher_file) and os.path.exists("map_stitcher_data.json"):
|
|
695
|
+
current_stitcher_file = "map_stitcher_data.json"
|
|
696
|
+
|
|
697
|
+
target_stitcher_file = os.path.join(state_dir, f"{base_name}_map_stitcher.json")
|
|
698
|
+
|
|
699
|
+
if os.path.exists(current_stitcher_file):
|
|
700
|
+
shutil.copy2(current_stitcher_file, target_stitcher_file)
|
|
701
|
+
logger.info(f"Map stitcher data copied to {target_stitcher_file}")
|
|
702
|
+
|
|
703
|
+
# Also save current milestones
|
|
704
|
+
if hasattr(self, 'milestone_tracker'):
|
|
705
|
+
milestone_filename = self.milestone_tracker.save_milestones_for_state(state_filename)
|
|
706
|
+
logger.info(f"Milestones saved to {milestone_filename}")
|
|
707
|
+
else:
|
|
708
|
+
# For regular saves, update the map stitcher save file path
|
|
709
|
+
self.memory_reader.update_map_stitcher_save_file(state_filename)
|
|
710
|
+
# Force save the map stitcher data
|
|
711
|
+
if self.memory_reader._map_stitcher:
|
|
712
|
+
self.memory_reader._map_stitcher.save_to_file()
|
|
713
|
+
|
|
714
|
+
except Exception as e:
|
|
715
|
+
logger.error(f"Error saving persistent grids for state: {e}")
|
|
716
|
+
|
|
717
|
+
def _load_persistent_grids_for_state(self, state_filename: str):
|
|
718
|
+
"""Load persistent location grids for a specific state file"""
|
|
719
|
+
try:
|
|
720
|
+
# print( _load_persistent_grids_for_state called with: {state_filename}")
|
|
721
|
+
# Get the directory and base name of the state file
|
|
722
|
+
state_dir = os.path.dirname(state_filename)
|
|
723
|
+
base_name = os.path.splitext(os.path.basename(state_filename))[0]
|
|
724
|
+
grids_filename = os.path.join(state_dir, f"{base_name}_grids.json")
|
|
725
|
+
|
|
726
|
+
# Load persistent grids if they exist
|
|
727
|
+
if os.path.exists(grids_filename):
|
|
728
|
+
# Load the persistent grids
|
|
729
|
+
load_persistent_world_map(grids_filename)
|
|
730
|
+
logger.info(f"Persistent grids loaded from {grids_filename}")
|
|
731
|
+
else:
|
|
732
|
+
logger.info(f"No persistent grids file found for state: {grids_filename}")
|
|
733
|
+
|
|
734
|
+
# # Initialize MapStitcher with cache file
|
|
735
|
+
# if hasattr(self, 'memory_reader') and self.memory_reader:
|
|
736
|
+
# # print( About to initialize MapStitcher for state: {state_filename}")
|
|
737
|
+
# # Use cache file instead of state-specific file
|
|
738
|
+
# self.memory_reader.update_map_stitcher_save_file(state_filename, is_cache_file=True)
|
|
739
|
+
# # print( MapStitcher initialization completed for state: {state_filename}")
|
|
740
|
+
# else:
|
|
741
|
+
# # print( No memory_reader available, cannot initialize MapStitcher")
|
|
742
|
+
|
|
743
|
+
except Exception as e:
|
|
744
|
+
logger.error(f"Error loading persistent grids for state: {e}")
|
|
745
|
+
|
|
746
|
+
def _copy_state_files_to_cache(self, state_filename: str):
|
|
747
|
+
"""Copy state-specific map stitcher and milestones to cache for working storage"""
|
|
748
|
+
import os
|
|
749
|
+
import shutil
|
|
750
|
+
|
|
751
|
+
# Ensure cache directory exists
|
|
752
|
+
cache_dir = ".pokeagent_cache"
|
|
753
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
754
|
+
|
|
755
|
+
# Copy map stitcher file to cache
|
|
756
|
+
state_dir = os.path.dirname(state_filename)
|
|
757
|
+
base_name = os.path.splitext(os.path.basename(state_filename))[0]
|
|
758
|
+
state_map_stitcher_file = os.path.join(state_dir, f"{base_name}_map_stitcher.json")
|
|
759
|
+
cache_map_stitcher_file = os.path.join(cache_dir, "map_stitcher_data.json")
|
|
760
|
+
|
|
761
|
+
if os.path.exists(state_map_stitcher_file):
|
|
762
|
+
# Check if the file has content
|
|
763
|
+
if os.path.getsize(state_map_stitcher_file) > 0:
|
|
764
|
+
shutil.copy2(state_map_stitcher_file, cache_map_stitcher_file)
|
|
765
|
+
# print( Copied map stitcher from {state_map_stitcher_file} to {cache_map_stitcher_file}")
|
|
766
|
+
else:
|
|
767
|
+
# Create a valid empty JSON structure for fresh start
|
|
768
|
+
import json
|
|
769
|
+
empty_data = {"map_areas": {}, "location_connections": {}}
|
|
770
|
+
with open(cache_map_stitcher_file, 'w') as f:
|
|
771
|
+
json.dump(empty_data, f, indent=2)
|
|
772
|
+
# print( State file empty, created fresh map stitcher cache")
|
|
773
|
+
else:
|
|
774
|
+
# Create a valid empty JSON structure for fresh start
|
|
775
|
+
import json
|
|
776
|
+
empty_data = {"map_areas": {}, "location_connections": {}}
|
|
777
|
+
with open(cache_map_stitcher_file, 'w') as f:
|
|
778
|
+
json.dump(empty_data, f, indent=2)
|
|
779
|
+
# print( No state file found, created fresh map stitcher cache")
|
|
780
|
+
|
|
781
|
+
# Copy milestones file to main directory (not cache, as requested)
|
|
782
|
+
state_milestones_file = os.path.join(state_dir, f"{base_name}_milestones.json")
|
|
783
|
+
cache_milestones_file = os.path.join(self.cache_dir, "milestones_progress.json") # Cache directory as requested
|
|
784
|
+
|
|
785
|
+
if os.path.exists(state_milestones_file):
|
|
786
|
+
shutil.copy2(state_milestones_file, cache_milestones_file)
|
|
787
|
+
# # print( Copied milestones from {state_milestones_file} to {cache_milestones_file}")
|
|
788
|
+
# else:
|
|
789
|
+
# # print( No state-specific milestones file found: {state_milestones_file}")
|
|
790
|
+
|
|
791
|
+
def start_frame_capture(self, fps: int = 30):
|
|
792
|
+
"""Start asynchronous frame capture"""
|
|
793
|
+
self.running = True
|
|
794
|
+
self.frame_thread = threading.Thread(target=self._frame_loop, args=(fps,), daemon=True)
|
|
795
|
+
self.frame_thread.start()
|
|
796
|
+
|
|
797
|
+
def _frame_loop(self, fps: int):
|
|
798
|
+
interval = 1.0 / fps
|
|
799
|
+
while self.running:
|
|
800
|
+
start = time.time()
|
|
801
|
+
frame = self.get_screenshot()
|
|
802
|
+
if frame:
|
|
803
|
+
np_frame = np.array(frame)
|
|
804
|
+
if self.frame_queue.full():
|
|
805
|
+
self.frame_queue.get_nowait()
|
|
806
|
+
self.frame_queue.put(np_frame)
|
|
807
|
+
self.current_frame = np_frame
|
|
808
|
+
elapsed = time.time() - start
|
|
809
|
+
time.sleep(max(0.001, interval - elapsed))
|
|
810
|
+
|
|
811
|
+
def get_latest_frame(self) -> Optional[np.ndarray]:
|
|
812
|
+
"""Return last captured frame"""
|
|
813
|
+
return self.current_frame.copy() if self.current_frame is not None else None
|
|
814
|
+
|
|
815
|
+
def process_input(self, input_data: Dict[str, Any]) -> str:
|
|
816
|
+
"""Handle JSON-style input payload"""
|
|
817
|
+
try:
|
|
818
|
+
input_type = input_data.get('type', 'button')
|
|
819
|
+
if input_type == 'button':
|
|
820
|
+
button = input_data.get('button')
|
|
821
|
+
if button:
|
|
822
|
+
return self.press_buttons([button])
|
|
823
|
+
elif input_type == 'sequence':
|
|
824
|
+
buttons = input_data.get('buttons', [])
|
|
825
|
+
return self.press_buttons(buttons)
|
|
826
|
+
elif input_type == 'hold':
|
|
827
|
+
button = input_data.get('button')
|
|
828
|
+
duration = int(input_data.get('duration', 1.0) * 60)
|
|
829
|
+
return self.press_buttons([button], hold_frames=duration)
|
|
830
|
+
return "Invalid input type"
|
|
831
|
+
except Exception as e:
|
|
832
|
+
logger.error(f"Input error: {e}")
|
|
833
|
+
return str(e)
|
|
834
|
+
|
|
835
|
+
def stop(self):
|
|
836
|
+
"""Stop emulator and cleanup"""
|
|
837
|
+
self.running = False
|
|
838
|
+
if self.frame_thread and self.frame_thread.is_alive():
|
|
839
|
+
self.frame_thread.join(timeout=1)
|
|
840
|
+
if self.core:
|
|
841
|
+
self.core = None
|
|
842
|
+
logger.info("Emulator stopped.")
|
|
843
|
+
|
|
844
|
+
def get_info(self) -> Dict[str, Any]:
|
|
845
|
+
"""Return metadata about emulator state"""
|
|
846
|
+
return {
|
|
847
|
+
"rom_path": self.rom_path,
|
|
848
|
+
"dimensions": (self.width, self.height),
|
|
849
|
+
"initialized": self.core is not None,
|
|
850
|
+
"headless": self.headless,
|
|
851
|
+
"sound": self.sound,
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
def get_comprehensive_state(self, screenshot=None) -> Dict[str, Any]:
|
|
855
|
+
"""Get comprehensive game state including visual and memory data using enhanced memory reader
|
|
856
|
+
|
|
857
|
+
Args:
|
|
858
|
+
screenshot: Optional PIL Image screenshot to use. If None, will call get_screenshot()
|
|
859
|
+
"""
|
|
860
|
+
# Simple caching to avoid redundant calls within a short time window
|
|
861
|
+
import time
|
|
862
|
+
current_time = time.time()
|
|
863
|
+
|
|
864
|
+
# Cache state for 100ms to avoid excessive memory reads
|
|
865
|
+
if hasattr(self, '_cached_state') and hasattr(self, '_cached_state_time'):
|
|
866
|
+
if current_time - self._cached_state_time < 0.1: # 100ms cache
|
|
867
|
+
return self._cached_state
|
|
868
|
+
|
|
869
|
+
# Use provided screenshot or get a new one
|
|
870
|
+
if screenshot is None:
|
|
871
|
+
screenshot = self.get_screenshot()
|
|
872
|
+
|
|
873
|
+
# Use the enhanced memory reader's comprehensive state method
|
|
874
|
+
if self.memory_reader:
|
|
875
|
+
state = self.memory_reader.get_comprehensive_state(screenshot)
|
|
876
|
+
else:
|
|
877
|
+
# Fallback to basic state
|
|
878
|
+
state = {
|
|
879
|
+
"visual": {
|
|
880
|
+
"screenshot": None,
|
|
881
|
+
"resolution": [self.width, self.height]
|
|
882
|
+
},
|
|
883
|
+
"player": {
|
|
884
|
+
"position": None,
|
|
885
|
+
"location": None,
|
|
886
|
+
"name": None
|
|
887
|
+
},
|
|
888
|
+
"game": {
|
|
889
|
+
"money": None,
|
|
890
|
+
"party": None,
|
|
891
|
+
"game_state": None,
|
|
892
|
+
"is_in_battle": None,
|
|
893
|
+
"time": None,
|
|
894
|
+
"badges": None,
|
|
895
|
+
"items": None,
|
|
896
|
+
"item_count": None,
|
|
897
|
+
"pokedex_caught": None,
|
|
898
|
+
"pokedex_seen": None
|
|
899
|
+
},
|
|
900
|
+
"map": {
|
|
901
|
+
"tiles": None,
|
|
902
|
+
"tile_names": None,
|
|
903
|
+
"metatile_behaviors": None,
|
|
904
|
+
"metatile_info": None,
|
|
905
|
+
"traversability": None
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
# Use screenshot already captured
|
|
910
|
+
if screenshot is not None and hasattr(screenshot, 'save'):
|
|
911
|
+
state["visual"]["screenshot"] = screenshot
|
|
912
|
+
|
|
913
|
+
# Cache the result
|
|
914
|
+
self._cached_state = state
|
|
915
|
+
self._cached_state_time = current_time
|
|
916
|
+
|
|
917
|
+
return state
|
|
918
|
+
|
|
919
|
+
def _get_tile_passability(self, tile_data) -> bool:
|
|
920
|
+
"""Determine if a tile is passable based on collision bits (like GeminiPlaysPokemonLive)"""
|
|
921
|
+
if not tile_data or len(tile_data) < 3:
|
|
922
|
+
return True # Default to passable if no data
|
|
923
|
+
|
|
924
|
+
# tile_data is (metatile_id, behavior, collision, elevation)
|
|
925
|
+
collision = tile_data[2] if len(tile_data) > 2 else 0
|
|
926
|
+
|
|
927
|
+
# Primary rule: collision == 0 means passable, non-zero means blocked
|
|
928
|
+
return collision == 0
|
|
929
|
+
|
|
930
|
+
def _get_tile_encounter_possible(self, tile_data) -> bool:
|
|
931
|
+
"""Determine if a tile can trigger encounters based on its behavior"""
|
|
932
|
+
if not tile_data or len(tile_data) < 2:
|
|
933
|
+
return False
|
|
934
|
+
|
|
935
|
+
# Import here to avoid circular imports
|
|
936
|
+
from .enums import MetatileBehavior
|
|
937
|
+
|
|
938
|
+
behavior = tile_data[1] if len(tile_data) > 1 else None
|
|
939
|
+
if not behavior:
|
|
940
|
+
return False
|
|
941
|
+
|
|
942
|
+
# Check for encounter tiles
|
|
943
|
+
encounter_behaviors = {
|
|
944
|
+
MetatileBehavior.TALL_GRASS,
|
|
945
|
+
MetatileBehavior.LONG_GRASS,
|
|
946
|
+
MetatileBehavior.UNUSED_05,
|
|
947
|
+
MetatileBehavior.DEEP_SAND,
|
|
948
|
+
MetatileBehavior.CAVE,
|
|
949
|
+
MetatileBehavior.INDOOR_ENCOUNTER,
|
|
950
|
+
MetatileBehavior.POND_WATER,
|
|
951
|
+
MetatileBehavior.INTERIOR_DEEP_WATER,
|
|
952
|
+
MetatileBehavior.DEEP_WATER,
|
|
953
|
+
MetatileBehavior.OCEAN_WATER,
|
|
954
|
+
MetatileBehavior.SEAWEED,
|
|
955
|
+
MetatileBehavior.ASHGRASS,
|
|
956
|
+
MetatileBehavior.FOOTPRINTS,
|
|
957
|
+
MetatileBehavior.SEAWEED_NO_SURFACING
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return behavior in encounter_behaviors
|
|
961
|
+
|
|
962
|
+
def _get_tile_surfable(self, tile_data) -> bool:
|
|
963
|
+
"""Determine if a tile can be surfed on based on its behavior"""
|
|
964
|
+
if not tile_data or len(tile_data) < 2:
|
|
965
|
+
return False
|
|
966
|
+
|
|
967
|
+
# Import here to avoid circular imports
|
|
968
|
+
from .enums import MetatileBehavior
|
|
969
|
+
|
|
970
|
+
behavior = tile_data[1] if len(tile_data) > 1 else None
|
|
971
|
+
if not behavior:
|
|
972
|
+
return False
|
|
973
|
+
|
|
974
|
+
# Check for surfable tiles
|
|
975
|
+
surfable_behaviors = {
|
|
976
|
+
MetatileBehavior.POND_WATER,
|
|
977
|
+
MetatileBehavior.INTERIOR_DEEP_WATER,
|
|
978
|
+
MetatileBehavior.DEEP_WATER,
|
|
979
|
+
MetatileBehavior.SOOTOPOLIS_DEEP_WATER,
|
|
980
|
+
MetatileBehavior.OCEAN_WATER,
|
|
981
|
+
MetatileBehavior.NO_SURFACING,
|
|
982
|
+
MetatileBehavior.SEAWEED,
|
|
983
|
+
MetatileBehavior.SEAWEED_NO_SURFACING
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
return behavior in surfable_behaviors
|
|
987
|
+
|
|
988
|
+
def get_player_position(self) -> Optional[Dict[str, int]]:
|
|
989
|
+
"""Get current player position"""
|
|
990
|
+
if self.memory_reader:
|
|
991
|
+
try:
|
|
992
|
+
coords = self.memory_reader.read_coordinates()
|
|
993
|
+
if coords:
|
|
994
|
+
return {"x": coords[0], "y": coords[1]}
|
|
995
|
+
except Exception as e:
|
|
996
|
+
logger.warning(f"Failed to read player position: {e}")
|
|
997
|
+
return None
|
|
998
|
+
|
|
999
|
+
def get_map_location(self) -> Optional[str]:
|
|
1000
|
+
"""Get current map location name"""
|
|
1001
|
+
if self.memory_reader:
|
|
1002
|
+
try:
|
|
1003
|
+
return self.memory_reader.read_location()
|
|
1004
|
+
except Exception as e:
|
|
1005
|
+
logger.warning(f"Failed to read map location: {e}")
|
|
1006
|
+
return None
|
|
1007
|
+
|
|
1008
|
+
def get_money(self) -> Optional[int]:
|
|
1009
|
+
"""Get current money amount"""
|
|
1010
|
+
if self.memory_reader:
|
|
1011
|
+
try:
|
|
1012
|
+
return self.memory_reader.read_money()
|
|
1013
|
+
except Exception as e:
|
|
1014
|
+
logger.warning(f"Failed to read money: {e}")
|
|
1015
|
+
return None
|
|
1016
|
+
|
|
1017
|
+
def get_party_pokemon(self) -> Optional[List[Dict[str, Any]]]:
|
|
1018
|
+
"""Get current party Pokemon"""
|
|
1019
|
+
if self.memory_reader:
|
|
1020
|
+
try:
|
|
1021
|
+
party = self.memory_reader.read_party_pokemon()
|
|
1022
|
+
if party:
|
|
1023
|
+
return [
|
|
1024
|
+
{
|
|
1025
|
+
"species": pokemon.species_name,
|
|
1026
|
+
"level": pokemon.level,
|
|
1027
|
+
"current_hp": pokemon.current_hp,
|
|
1028
|
+
"max_hp": pokemon.max_hp,
|
|
1029
|
+
"status": pokemon.status.get_status_name() if pokemon.status else "OK",
|
|
1030
|
+
"types": [t for t in [pokemon.type1.name if pokemon.type1 else None,
|
|
1031
|
+
pokemon.type2.name if pokemon.type2 else None] if t is not None]
|
|
1032
|
+
}
|
|
1033
|
+
for pokemon in party
|
|
1034
|
+
]
|
|
1035
|
+
except Exception as e:
|
|
1036
|
+
logger.warning(f"Failed to read party Pokemon: {e}")
|
|
1037
|
+
return None
|
|
1038
|
+
|
|
1039
|
+
def get_map_tiles(self, radius: int = 7) -> Optional[List[List[tuple]]]:
|
|
1040
|
+
"""Get map tiles around player"""
|
|
1041
|
+
if self.memory_reader:
|
|
1042
|
+
try:
|
|
1043
|
+
return self.memory_reader.read_map_around_player(radius=radius)
|
|
1044
|
+
except Exception as e:
|
|
1045
|
+
logger.warning(f"Failed to read map tiles: {e}")
|
|
1046
|
+
return None
|
|
1047
|
+
|
|
1048
|
+
def test_memory_reading(self) -> Dict[str, Any]:
|
|
1049
|
+
"""Test memory reading capabilities and return diagnostic information"""
|
|
1050
|
+
if not self.memory_reader:
|
|
1051
|
+
return {"error": "Memory reader not initialized"}
|
|
1052
|
+
|
|
1053
|
+
try:
|
|
1054
|
+
# Get memory diagnostics
|
|
1055
|
+
diagnostics = self.memory_reader.test_memory_access()
|
|
1056
|
+
|
|
1057
|
+
# Test some basic reads
|
|
1058
|
+
test_results = {
|
|
1059
|
+
"player_name": None,
|
|
1060
|
+
"money": None,
|
|
1061
|
+
"coordinates": None,
|
|
1062
|
+
"party_size": None,
|
|
1063
|
+
"location": None
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
try:
|
|
1067
|
+
test_results["player_name"] = self.memory_reader.read_player_name()
|
|
1068
|
+
except Exception as e:
|
|
1069
|
+
test_results["player_name_error"] = str(e)
|
|
1070
|
+
|
|
1071
|
+
try:
|
|
1072
|
+
test_results["money"] = self.memory_reader.read_money()
|
|
1073
|
+
except Exception as e:
|
|
1074
|
+
test_results["money_error"] = str(e)
|
|
1075
|
+
|
|
1076
|
+
try:
|
|
1077
|
+
test_results["coordinates"] = self.memory_reader.read_coordinates()
|
|
1078
|
+
except Exception as e:
|
|
1079
|
+
test_results["coordinates_error"] = str(e)
|
|
1080
|
+
|
|
1081
|
+
try:
|
|
1082
|
+
test_results["party_size"] = self.memory_reader.read_party_size()
|
|
1083
|
+
except Exception as e:
|
|
1084
|
+
test_results["party_size_error"] = str(e)
|
|
1085
|
+
|
|
1086
|
+
try:
|
|
1087
|
+
test_results["location"] = self.memory_reader.read_location()
|
|
1088
|
+
except Exception as e:
|
|
1089
|
+
test_results["location_error"] = str(e)
|
|
1090
|
+
|
|
1091
|
+
return {
|
|
1092
|
+
"diagnostics": diagnostics,
|
|
1093
|
+
"test_results": test_results
|
|
1094
|
+
}
|
|
1095
|
+
except Exception as e:
|
|
1096
|
+
return {"error": f"Failed to run memory tests: {e}"}
|
|
1097
|
+
|
|
1098
|
+
def check_and_update_milestones(self, game_state: Dict[str, Any]):
|
|
1099
|
+
"""Check current game state and update milestones"""
|
|
1100
|
+
try:
|
|
1101
|
+
# Debug: Show current state
|
|
1102
|
+
location = game_state.get("player", {}).get("location", "Unknown")
|
|
1103
|
+
# print(f"🔍 Checking milestones for location: {location}")
|
|
1104
|
+
# Only check milestones that aren't already completed
|
|
1105
|
+
milestones_to_check = [
|
|
1106
|
+
# Phase 1: Game Initialization
|
|
1107
|
+
"GAME_RUNNING", "PLAYER_NAME_SET", "INTRO_CUTSCENE_COMPLETE",
|
|
1108
|
+
|
|
1109
|
+
# Phase 2: Tutorial & Starting Town
|
|
1110
|
+
"LITTLEROOT_TOWN", "PLAYER_HOUSE_ENTERED", "PLAYER_BEDROOM",
|
|
1111
|
+
"RIVAL_HOUSE", "RIVAL_BEDROOM",
|
|
1112
|
+
|
|
1113
|
+
# Phase 3: Professor Birch & Starter
|
|
1114
|
+
"ROUTE_101", "STARTER_CHOSEN", "BIRCH_LAB_VISITED",
|
|
1115
|
+
|
|
1116
|
+
# Phase 4: Rival
|
|
1117
|
+
"OLDALE_TOWN", "ROUTE_103", "RECEIVED_POKEDEX",
|
|
1118
|
+
|
|
1119
|
+
# Phase 5: Route 102 & Petalburg
|
|
1120
|
+
"ROUTE_102", "PETALBURG_CITY", "DAD_FIRST_MEETING", "GYM_EXPLANATION",
|
|
1121
|
+
|
|
1122
|
+
# Phase 6: Road to Rustboro City
|
|
1123
|
+
"ROUTE_104_SOUTH", "PETALBURG_WOODS", "TEAM_AQUA_GRUNT_DEFEATED",
|
|
1124
|
+
"ROUTE_104_NORTH", "RUSTBORO_CITY",
|
|
1125
|
+
|
|
1126
|
+
# Phase 7: First Gym Challenge
|
|
1127
|
+
"RUSTBORO_GYM_ENTERED", "ROXANNE_DEFEATED", "FIRST_GYM_COMPLETE"
|
|
1128
|
+
]
|
|
1129
|
+
|
|
1130
|
+
for milestone_id in milestones_to_check:
|
|
1131
|
+
if not self.milestone_tracker.is_completed(milestone_id):
|
|
1132
|
+
if self._check_milestone_condition(milestone_id, game_state):
|
|
1133
|
+
print(f"🎯 Milestone detected: {milestone_id}")
|
|
1134
|
+
self.milestone_tracker.mark_completed(milestone_id)
|
|
1135
|
+
except Exception as e:
|
|
1136
|
+
logger.warning(f"Error checking milestones: {e}")
|
|
1137
|
+
|
|
1138
|
+
def _check_milestone_condition(self, milestone_id: str, game_state: Dict[str, Any]) -> bool:
|
|
1139
|
+
"""Check if a specific milestone condition is met based on current game state"""
|
|
1140
|
+
try:
|
|
1141
|
+
# Test milestones (should always work)
|
|
1142
|
+
if milestone_id == "GAME_RUNNING":
|
|
1143
|
+
return True # If we can execute this, game is running
|
|
1144
|
+
elif milestone_id == "HAS_PARTY":
|
|
1145
|
+
if game_state:
|
|
1146
|
+
party = game_state.get("player", {}).get("party", [])
|
|
1147
|
+
return len(party) > 0
|
|
1148
|
+
return False
|
|
1149
|
+
|
|
1150
|
+
# Location-based milestones - check current location
|
|
1151
|
+
elif milestone_id == "LITTLEROOT_TOWN":
|
|
1152
|
+
if game_state:
|
|
1153
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1154
|
+
return "LITTLEROOT" in str(location).upper()
|
|
1155
|
+
return False
|
|
1156
|
+
|
|
1157
|
+
elif milestone_id == "OLDALE_TOWN":
|
|
1158
|
+
if game_state:
|
|
1159
|
+
# Only count Oldale Town if we've already been to Littleroot Town
|
|
1160
|
+
if not self.milestone_tracker.is_completed("LITTLEROOT_TOWN"):
|
|
1161
|
+
return False
|
|
1162
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1163
|
+
return "OLDALE" in str(location).upper()
|
|
1164
|
+
return False
|
|
1165
|
+
elif milestone_id == "RUSTBORO_CITY":
|
|
1166
|
+
if game_state:
|
|
1167
|
+
# Only count Rustboro City if we've already been to Petalburg City
|
|
1168
|
+
if not self.milestone_tracker.is_completed("PETALBURG_CITY"):
|
|
1169
|
+
return False
|
|
1170
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1171
|
+
return "RUSTBORO" in str(location).upper()
|
|
1172
|
+
return False
|
|
1173
|
+
elif milestone_id == "DEWFORD_TOWN":
|
|
1174
|
+
if game_state:
|
|
1175
|
+
# Only count Dewford Town if we've already been to Rustboro City
|
|
1176
|
+
if not self.milestone_tracker.is_completed("RUSTBORO_CITY"):
|
|
1177
|
+
return False
|
|
1178
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1179
|
+
return "DEWFORD" in str(location).upper()
|
|
1180
|
+
return False
|
|
1181
|
+
elif milestone_id == "SLATEPORT_CITY":
|
|
1182
|
+
if game_state:
|
|
1183
|
+
# Only count Slateport City if we've already been to Dewford Town
|
|
1184
|
+
if not self.milestone_tracker.is_completed("DEWFORD_TOWN"):
|
|
1185
|
+
return False
|
|
1186
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1187
|
+
return "SLATEPORT" in str(location).upper()
|
|
1188
|
+
return False
|
|
1189
|
+
elif milestone_id == "MAUVILLE_CITY":
|
|
1190
|
+
if game_state:
|
|
1191
|
+
# Only count Mauville City if we've already been to Slateport City
|
|
1192
|
+
if not self.milestone_tracker.is_completed("SLATEPORT_CITY"):
|
|
1193
|
+
return False
|
|
1194
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1195
|
+
return "MAUVILLE" in str(location).upper()
|
|
1196
|
+
return False
|
|
1197
|
+
|
|
1198
|
+
|
|
1199
|
+
|
|
1200
|
+
# Badge milestones - check badge count/list
|
|
1201
|
+
elif milestone_id == "STONE_BADGE":
|
|
1202
|
+
if game_state:
|
|
1203
|
+
badges = game_state.get("game", {}).get("badges", [])
|
|
1204
|
+
if isinstance(badges, list):
|
|
1205
|
+
return len(badges) >= 1 or any("Stone" in str(b) for b in badges)
|
|
1206
|
+
elif isinstance(badges, int):
|
|
1207
|
+
return badges >= 1
|
|
1208
|
+
return False
|
|
1209
|
+
elif milestone_id == "KNUCKLE_BADGE":
|
|
1210
|
+
if game_state:
|
|
1211
|
+
badges = game_state.get("game", {}).get("badges", [])
|
|
1212
|
+
if isinstance(badges, list):
|
|
1213
|
+
return len(badges) >= 2 or any("Knuckle" in str(b) for b in badges)
|
|
1214
|
+
elif isinstance(badges, int):
|
|
1215
|
+
return badges >= 2
|
|
1216
|
+
return False
|
|
1217
|
+
elif milestone_id == "DYNAMO_BADGE":
|
|
1218
|
+
if game_state:
|
|
1219
|
+
badges = game_state.get("game", {}).get("badges", [])
|
|
1220
|
+
if isinstance(badges, list):
|
|
1221
|
+
return len(badges) >= 3 or any("Dynamo" in str(b) for b in badges)
|
|
1222
|
+
elif isinstance(badges, int):
|
|
1223
|
+
return badges >= 3
|
|
1224
|
+
return False
|
|
1225
|
+
|
|
1226
|
+
# Phase 1: Game Initialization milestones
|
|
1227
|
+
elif milestone_id == "INTRO_CUTSCENE_COMPLETE":
|
|
1228
|
+
if game_state:
|
|
1229
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1230
|
+
return "MOVING_VAN" in str(location).upper()
|
|
1231
|
+
return False
|
|
1232
|
+
elif milestone_id == "PLAYER_NAME_SET":
|
|
1233
|
+
if game_state:
|
|
1234
|
+
player_name = game_state.get("player", {}).get("name", "")
|
|
1235
|
+
# Player name is set if we have a non-empty name that's not the default
|
|
1236
|
+
return (player_name and
|
|
1237
|
+
str(player_name).strip() != "" and
|
|
1238
|
+
str(player_name).strip() not in ["", "UNKNOWN", "PLAYER"])
|
|
1239
|
+
return False
|
|
1240
|
+
|
|
1241
|
+
# Phase 2: Tutorial & Starting Town milestones
|
|
1242
|
+
elif milestone_id == "PLAYER_HOUSE_ENTERED":
|
|
1243
|
+
if game_state:
|
|
1244
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1245
|
+
return "LITTLEROOT TOWN BRENDANS HOUSE 1F" in str(location).upper()
|
|
1246
|
+
return False
|
|
1247
|
+
elif milestone_id == "PLAYER_BEDROOM":
|
|
1248
|
+
if game_state:
|
|
1249
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1250
|
+
return "LITTLEROOT TOWN BRENDANS HOUSE 2F" in str(location).upper()
|
|
1251
|
+
return False
|
|
1252
|
+
elif milestone_id == "RIVAL_HOUSE":
|
|
1253
|
+
if game_state:
|
|
1254
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1255
|
+
return "LITTLEROOT TOWN MAYS HOUSE 1F" in str(location).upper()
|
|
1256
|
+
return False
|
|
1257
|
+
elif milestone_id == "RIVAL_BEDROOM":
|
|
1258
|
+
if game_state:
|
|
1259
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1260
|
+
return "LITTLEROOT TOWN MAYS HOUSE 2F" in str(location).upper()
|
|
1261
|
+
return False
|
|
1262
|
+
|
|
1263
|
+
# Phase 3: Professor Birch & Starter milestones
|
|
1264
|
+
elif milestone_id == "ROUTE_101":
|
|
1265
|
+
if game_state:
|
|
1266
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1267
|
+
return "ROUTE_101" in str(location).upper() or "ROUTE 101" in str(location).upper()
|
|
1268
|
+
return False
|
|
1269
|
+
elif milestone_id == "STARTER_CHOSEN":
|
|
1270
|
+
if game_state:
|
|
1271
|
+
party = game_state.get("player", {}).get("party", [])
|
|
1272
|
+
return len(party) >= 1 and any(p.get("species_name", "").strip() for p in party)
|
|
1273
|
+
return False
|
|
1274
|
+
elif milestone_id == "BIRCH_LAB_VISITED":
|
|
1275
|
+
if game_state:
|
|
1276
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1277
|
+
return "LITTLEROOT TOWN PROFESSOR BIRCHS LAB" in str(location).upper()
|
|
1278
|
+
return False
|
|
1279
|
+
|
|
1280
|
+
# Phase 4: Early Route Progression milestones
|
|
1281
|
+
elif milestone_id == "ROUTE_103":
|
|
1282
|
+
if game_state:
|
|
1283
|
+
# Only count Route 103 if we've already been to Route 101 and have starter
|
|
1284
|
+
if not self.milestone_tracker.is_completed("ROUTE_101"):
|
|
1285
|
+
return False
|
|
1286
|
+
if not self.milestone_tracker.is_completed("STARTER_CHOSEN"):
|
|
1287
|
+
return False
|
|
1288
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1289
|
+
return "ROUTE_103" in str(location).upper() or "ROUTE 103" in str(location).upper()
|
|
1290
|
+
return False
|
|
1291
|
+
# elif milestone_id == "RIVAL_BATTLE_1":
|
|
1292
|
+
# # Check for specific state hash from dialog after the battle (c9086d56)
|
|
1293
|
+
# if game_state:
|
|
1294
|
+
# # Create state hash for comparison
|
|
1295
|
+
# state_str = str(game_state)
|
|
1296
|
+
# state_hash = hashlib.md5(state_str.encode()).hexdigest()[:8]
|
|
1297
|
+
|
|
1298
|
+
# # Check for battle completion state hash or traditional conditions
|
|
1299
|
+
# return (state_hash == "c9086d56" or
|
|
1300
|
+
# (self.milestone_tracker.is_completed("ROUTE_103") and
|
|
1301
|
+
# self.milestone_tracker.is_completed("STARTER_CHOSEN")))
|
|
1302
|
+
# return False
|
|
1303
|
+
elif milestone_id == "RECEIVED_POKEDEX":
|
|
1304
|
+
if game_state:
|
|
1305
|
+
# Check if we're in Birch's lab AND have completed Route 103
|
|
1306
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1307
|
+
return (self.milestone_tracker.is_completed("ROUTE_103") and
|
|
1308
|
+
"LITTLEROOT TOWN PROFESSOR BIRCHS LAB" in str(location).upper())
|
|
1309
|
+
return False
|
|
1310
|
+
## Phase 5: Route 102 & Petalburg
|
|
1311
|
+
elif milestone_id == "ROUTE_102":
|
|
1312
|
+
if game_state:
|
|
1313
|
+
# Only count Route 102 if we've received Pokedex
|
|
1314
|
+
if not self.milestone_tracker.is_completed("RECEIVED_POKEDEX"):
|
|
1315
|
+
return False
|
|
1316
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1317
|
+
return "ROUTE_102" in str(location).upper() or "ROUTE 102" in str(location).upper()
|
|
1318
|
+
return False
|
|
1319
|
+
elif milestone_id == "PETALBURG_CITY":
|
|
1320
|
+
if game_state:
|
|
1321
|
+
# Enforce proper game progression through required towns
|
|
1322
|
+
if not self.milestone_tracker.is_completed("LITTLEROOT_TOWN"):
|
|
1323
|
+
return False
|
|
1324
|
+
if not self.milestone_tracker.is_completed("OLDALE_TOWN"):
|
|
1325
|
+
return False
|
|
1326
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1327
|
+
return "PETALBURG" in str(location).upper()
|
|
1328
|
+
return False
|
|
1329
|
+
elif milestone_id == "DAD_FIRST_MEETING":
|
|
1330
|
+
# Meeting Dad happens in Petalburg Gym
|
|
1331
|
+
if game_state:
|
|
1332
|
+
# Must have visited Petalburg City first
|
|
1333
|
+
if not self.milestone_tracker.is_completed("PETALBURG_CITY"):
|
|
1334
|
+
return False
|
|
1335
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1336
|
+
return "PETALBURG CITY GYM" in str(location).upper() or "PETALBURG_CITY_GYM" in str(location).upper()
|
|
1337
|
+
return False
|
|
1338
|
+
elif milestone_id == "GYM_EXPLANATION":
|
|
1339
|
+
# Gym explanation happens after meeting Dad in the gym
|
|
1340
|
+
if game_state:
|
|
1341
|
+
# Must have met Dad and still be in gym
|
|
1342
|
+
if not self.milestone_tracker.is_completed("DAD_FIRST_MEETING"):
|
|
1343
|
+
return False
|
|
1344
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1345
|
+
return "PETALBURG CITY GYM" in str(location).upper() or "PETALBURG_CITY_GYM" in str(location).upper()
|
|
1346
|
+
return False
|
|
1347
|
+
|
|
1348
|
+
# Phase 6: Pre-Gym Preparation milestones
|
|
1349
|
+
elif milestone_id == "ROUTE_104_SOUTH":
|
|
1350
|
+
if game_state:
|
|
1351
|
+
# Only count if we've been to Petalburg
|
|
1352
|
+
if not self.milestone_tracker.is_completed("PETALBURG_CITY"):
|
|
1353
|
+
return False
|
|
1354
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1355
|
+
return "ROUTE_104" in str(location).upper() or "ROUTE 104" in str(location).upper()
|
|
1356
|
+
return False
|
|
1357
|
+
elif milestone_id == "MR_BRINEY_MET":
|
|
1358
|
+
# Assume meeting Mr. Briney happens on Route 104
|
|
1359
|
+
if game_state:
|
|
1360
|
+
return self.milestone_tracker.is_completed("ROUTE_104_SOUTH")
|
|
1361
|
+
return False
|
|
1362
|
+
elif milestone_id == "PETALBURG_WOODS":
|
|
1363
|
+
if game_state:
|
|
1364
|
+
# Only count if we've been to Route 104
|
|
1365
|
+
if not self.milestone_tracker.is_completed("ROUTE_104_SOUTH"):
|
|
1366
|
+
return False
|
|
1367
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1368
|
+
return "PETALBURG_WOODS" in str(location).upper() or "PETALBURG WOODS" in str(location).upper()
|
|
1369
|
+
return False
|
|
1370
|
+
elif milestone_id == "TEAM_AQUA_GRUNT_DEFEATED":
|
|
1371
|
+
# Team Aqua grunt defeated at specific location in Petalburg Woods
|
|
1372
|
+
if game_state:
|
|
1373
|
+
# Must have visited Petalburg Woods
|
|
1374
|
+
if not self.milestone_tracker.is_completed("PETALBURG_WOODS"):
|
|
1375
|
+
return False
|
|
1376
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1377
|
+
# Check if in Petalburg Woods and at specific coordinates
|
|
1378
|
+
if "PETALBURG_WOODS" in str(location).upper() or "PETALBURG WOODS" in str(location).upper():
|
|
1379
|
+
# Check for Map_18_0B at coords (26,23) or (27,23)
|
|
1380
|
+
pos = game_state.get("player", {}).get("pos", [])
|
|
1381
|
+
if len(pos) >= 2:
|
|
1382
|
+
x, y = pos[0], pos[1]
|
|
1383
|
+
if y == 23 and x in [26, 27]:
|
|
1384
|
+
return True
|
|
1385
|
+
return self.milestone_tracker.is_completed("PETALBURG_WOODS")
|
|
1386
|
+
return False
|
|
1387
|
+
elif milestone_id == "DEVON_GOODS_OBTAINED":
|
|
1388
|
+
# Assume Devon Goods obtained after defeating Team Aqua grunt
|
|
1389
|
+
if game_state:
|
|
1390
|
+
return self.milestone_tracker.is_completed("TEAM_AQUA_GRUNT_DEFEATED")
|
|
1391
|
+
return False
|
|
1392
|
+
|
|
1393
|
+
# Phase 8: Rustboro City Approach milestones
|
|
1394
|
+
elif milestone_id == "ROUTE_104_NORTH":
|
|
1395
|
+
if game_state:
|
|
1396
|
+
# Only count if we've been through Petalburg Woods
|
|
1397
|
+
if not self.milestone_tracker.is_completed("PETALBURG_WOODS"):
|
|
1398
|
+
return False
|
|
1399
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1400
|
+
return (("ROUTE_104" in str(location).upper() or "ROUTE 104" in str(location).upper()) and
|
|
1401
|
+
self.milestone_tracker.is_completed("DEVON_GOODS_OBTAINED"))
|
|
1402
|
+
return False
|
|
1403
|
+
elif milestone_id == "DEVON_CORP_VISITED":
|
|
1404
|
+
if game_state:
|
|
1405
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1406
|
+
return ("DEVON" in str(location).upper() and
|
|
1407
|
+
self.milestone_tracker.is_completed("RUSTBORO_CITY"))
|
|
1408
|
+
return False
|
|
1409
|
+
elif milestone_id == "DEVON_GOODS_DELIVERED":
|
|
1410
|
+
# Assume goods delivered after visiting Devon Corp
|
|
1411
|
+
if game_state:
|
|
1412
|
+
return self.milestone_tracker.is_completed("DEVON_CORP_VISITED")
|
|
1413
|
+
return False
|
|
1414
|
+
elif milestone_id == "LETTER_RECEIVED":
|
|
1415
|
+
# Assume letter received after delivering goods
|
|
1416
|
+
if game_state:
|
|
1417
|
+
return self.milestone_tracker.is_completed("DEVON_GOODS_DELIVERED")
|
|
1418
|
+
return False
|
|
1419
|
+
elif milestone_id == "POKEBALLS_PURCHASED":
|
|
1420
|
+
# Assume Pokeballs purchased in Rustboro City
|
|
1421
|
+
if game_state:
|
|
1422
|
+
return self.milestone_tracker.is_completed("RUSTBORO_CITY")
|
|
1423
|
+
return False
|
|
1424
|
+
|
|
1425
|
+
# Phase 9: Gym Preparation milestones
|
|
1426
|
+
elif milestone_id == "RUSTBORO_GYM_ENTERED":
|
|
1427
|
+
if game_state:
|
|
1428
|
+
# Must have visited Rustboro City first
|
|
1429
|
+
if not self.milestone_tracker.is_completed("RUSTBORO_CITY"):
|
|
1430
|
+
return False
|
|
1431
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1432
|
+
return "RUSTBORO_GYM" in str(location).upper() or "RUSTBORO CITY GYM" in str(location).upper()
|
|
1433
|
+
return False
|
|
1434
|
+
elif milestone_id == "GYM_TRAINERS_DEFEATED":
|
|
1435
|
+
# Assume gym trainers defeated after entering gym
|
|
1436
|
+
if game_state:
|
|
1437
|
+
return self.milestone_tracker.is_completed("RUSTBORO_GYM_ENTERED")
|
|
1438
|
+
return False
|
|
1439
|
+
elif milestone_id == "ROXANNE_BATTLE_STARTED":
|
|
1440
|
+
# Assume Roxanne battle started after defeating gym trainers
|
|
1441
|
+
if game_state:
|
|
1442
|
+
return self.milestone_tracker.is_completed("GYM_TRAINERS_DEFEATED")
|
|
1443
|
+
return False
|
|
1444
|
+
|
|
1445
|
+
# Phase 10: First Gym Victory milestones
|
|
1446
|
+
elif milestone_id == "ROXANNE_DEFEATED":
|
|
1447
|
+
# Roxanne defeated when Stone Badge is obtained
|
|
1448
|
+
if game_state:
|
|
1449
|
+
# Must have Stone Badge
|
|
1450
|
+
return self.milestone_tracker.is_completed("STONE_BADGE")
|
|
1451
|
+
return False
|
|
1452
|
+
elif milestone_id == "TM_ROCK_TOMB_RECEIVED":
|
|
1453
|
+
# Assume TM received after defeating Roxanne
|
|
1454
|
+
if game_state:
|
|
1455
|
+
return self.milestone_tracker.is_completed("ROXANNE_DEFEATED")
|
|
1456
|
+
return False
|
|
1457
|
+
elif milestone_id == "FIRST_GYM_COMPLETE":
|
|
1458
|
+
# Complete after getting Stone Badge and exiting gym
|
|
1459
|
+
if game_state:
|
|
1460
|
+
# Must have Stone Badge and not be in gym
|
|
1461
|
+
if not self.milestone_tracker.is_completed("STONE_BADGE"):
|
|
1462
|
+
return False
|
|
1463
|
+
location = game_state.get("player", {}).get("location", "")
|
|
1464
|
+
# Not in any gym
|
|
1465
|
+
return "GYM" not in str(location).upper()
|
|
1466
|
+
return False
|
|
1467
|
+
|
|
1468
|
+
return False
|
|
1469
|
+
|
|
1470
|
+
except Exception as e:
|
|
1471
|
+
logger.warning(f"Error checking milestone condition {milestone_id}: {e}")
|
|
1472
|
+
return False
|
|
1473
|
+
|
|
1474
|
+
def get_milestones(self) -> Dict[str, Any]:
|
|
1475
|
+
"""Get current milestone data and progress"""
|
|
1476
|
+
try:
|
|
1477
|
+
# Get current game state and update milestones
|
|
1478
|
+
# Use cached state if available to avoid redundant calls
|
|
1479
|
+
game_state = self.get_comprehensive_state()
|
|
1480
|
+
# Only update milestones occasionally to avoid performance issues
|
|
1481
|
+
import time
|
|
1482
|
+
current_time = time.time()
|
|
1483
|
+
if not hasattr(self, '_last_milestone_update') or current_time - self._last_milestone_update > 1.0: # Update at most once per second
|
|
1484
|
+
self.check_and_update_milestones(game_state)
|
|
1485
|
+
self._last_milestone_update = current_time
|
|
1486
|
+
|
|
1487
|
+
# Use loaded milestones from the milestone tracker
|
|
1488
|
+
milestones = []
|
|
1489
|
+
for i, (milestone_id, milestone_data) in enumerate(self.milestone_tracker.milestones.items()):
|
|
1490
|
+
milestones.append({
|
|
1491
|
+
"id": i + 1,
|
|
1492
|
+
"name": milestone_data.get("name", milestone_id),
|
|
1493
|
+
"category": milestone_data.get("category", "unknown"),
|
|
1494
|
+
"completed": milestone_data.get("completed", False),
|
|
1495
|
+
"timestamp": milestone_data.get("timestamp", None)
|
|
1496
|
+
})
|
|
1497
|
+
|
|
1498
|
+
# Calculate summary stats
|
|
1499
|
+
completed_count = sum(1 for m in milestones if m["completed"])
|
|
1500
|
+
total_count = len(milestones)
|
|
1501
|
+
|
|
1502
|
+
# Handle location data properly
|
|
1503
|
+
location_data = game_state.get("player", {}).get("location", "")
|
|
1504
|
+
if isinstance(location_data, dict):
|
|
1505
|
+
current_location = location_data.get("map_name", "UNKNOWN")
|
|
1506
|
+
else:
|
|
1507
|
+
current_location = str(location_data) if location_data else "UNKNOWN"
|
|
1508
|
+
|
|
1509
|
+
# Handle badges data properly
|
|
1510
|
+
badges_data = game_state.get("game", {}).get("badges", 0)
|
|
1511
|
+
if isinstance(badges_data, list):
|
|
1512
|
+
badge_count = sum(1 for b in badges_data if b)
|
|
1513
|
+
else:
|
|
1514
|
+
badge_count = badges_data if isinstance(badges_data, int) else 0
|
|
1515
|
+
|
|
1516
|
+
return {
|
|
1517
|
+
"milestones": milestones,
|
|
1518
|
+
"completed": completed_count,
|
|
1519
|
+
"total": total_count,
|
|
1520
|
+
"progress": completed_count / total_count if total_count > 0 else 0,
|
|
1521
|
+
"current_location": current_location,
|
|
1522
|
+
"badges": badge_count,
|
|
1523
|
+
"pokedex_seen": game_state.get("game", {}).get("pokedex_seen", 0),
|
|
1524
|
+
"pokedex_caught": game_state.get("game", {}).get("pokedex_caught", 0),
|
|
1525
|
+
"party_size": len(game_state.get("player", {}).get("party", [])),
|
|
1526
|
+
"tracking_system": "file_based",
|
|
1527
|
+
"milestone_file": self.milestone_tracker.filename
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
except Exception as e:
|
|
1531
|
+
logger.error(f"Error getting milestones: {e}")
|
|
1532
|
+
# Fallback to basic milestones if memory reading fails
|
|
1533
|
+
basic_milestones = [
|
|
1534
|
+
{"id": 1, "name": "GAME_STARTED", "category": "basic", "completed": True, "timestamp": time.time()},
|
|
1535
|
+
{"id": 2, "name": "EMULATOR_RUNNING", "category": "basic", "completed": True, "timestamp": time.time()},
|
|
1536
|
+
]
|
|
1537
|
+
return {
|
|
1538
|
+
"milestones": basic_milestones,
|
|
1539
|
+
"completed": 2,
|
|
1540
|
+
"total": 2,
|
|
1541
|
+
"progress": 1.0,
|
|
1542
|
+
"tracking_system": "fallback",
|
|
1543
|
+
"error": str(e)
|
|
1544
|
+
}
|