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,2152 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Fixed Simple Pokemon Emerald server - headless FastAPI server
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
# Standard library imports
|
|
7
|
+
import base64
|
|
8
|
+
import datetime
|
|
9
|
+
import glob
|
|
10
|
+
import io
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
import os
|
|
14
|
+
import signal
|
|
15
|
+
import sys
|
|
16
|
+
import threading
|
|
17
|
+
import time
|
|
18
|
+
|
|
19
|
+
# Third-party imports
|
|
20
|
+
import cv2
|
|
21
|
+
import numpy as np
|
|
22
|
+
import uvicorn
|
|
23
|
+
from fastapi import FastAPI, HTTPException, Request
|
|
24
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
25
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
26
|
+
from PIL import Image
|
|
27
|
+
from pydantic import BaseModel
|
|
28
|
+
|
|
29
|
+
# Add parent directory to path for local modules
|
|
30
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
31
|
+
|
|
32
|
+
# Local application imports
|
|
33
|
+
from pokemon_env.emulator import EmeraldEmulator
|
|
34
|
+
from utils.anticheat import AntiCheatTracker
|
|
35
|
+
|
|
36
|
+
# Set up logging - reduced verbosity for multiprocess mode
|
|
37
|
+
logging.basicConfig(level=logging.WARNING)
|
|
38
|
+
logger = logging.getLogger(__name__)
|
|
39
|
+
|
|
40
|
+
# Global state
|
|
41
|
+
env = None
|
|
42
|
+
anticheat_tracker = None # AntiCheat tracker for submission logging
|
|
43
|
+
step_counter = 0 # Track steps for submission logging
|
|
44
|
+
last_action_time = None # Track time of last action for decision time calculation
|
|
45
|
+
running = True
|
|
46
|
+
step_count = 0
|
|
47
|
+
agent_step_count = 0 # Track agent steps separately from frame steps
|
|
48
|
+
current_obs = None
|
|
49
|
+
fps = 80
|
|
50
|
+
|
|
51
|
+
# Performance monitoring
|
|
52
|
+
last_fps_log = time.time()
|
|
53
|
+
frame_count_since_log = 0
|
|
54
|
+
action_queue = [] # Queue for multi-action sequences
|
|
55
|
+
current_action = None # Current action being held
|
|
56
|
+
action_frames_remaining = 0 # Frames left to hold current action
|
|
57
|
+
release_frames_remaining = 0 # Frames left to wait after release
|
|
58
|
+
|
|
59
|
+
### IMPORTANT: DO NOT REDUCE THESE OR BUTTONS MAY NOT WORK! ###
|
|
60
|
+
ACTION_HOLD_FRAMES = 12 # Hold each action for 12 frames
|
|
61
|
+
ACTION_RELEASE_DELAY = 24 # Delay between actions for processing
|
|
62
|
+
|
|
63
|
+
# Video recording state
|
|
64
|
+
video_writer = None
|
|
65
|
+
video_recording = False
|
|
66
|
+
video_filename = ""
|
|
67
|
+
video_frame_counter = 0
|
|
68
|
+
video_frame_skip = 4 # Record every 4th frame (120/4 = 30 FPS)
|
|
69
|
+
|
|
70
|
+
# Frame cache for separate frame server
|
|
71
|
+
# Use cache directory instead of /tmp
|
|
72
|
+
CACHE_DIR = ".pokeagent_cache"
|
|
73
|
+
os.makedirs(CACHE_DIR, exist_ok=True)
|
|
74
|
+
FRAME_CACHE_FILE = os.path.join(CACHE_DIR, "frame_cache.json")
|
|
75
|
+
frame_cache_counter = 0
|
|
76
|
+
|
|
77
|
+
# Server runs headless - display handled by client
|
|
78
|
+
|
|
79
|
+
# Threading locks for thread safety
|
|
80
|
+
obs_lock = threading.Lock()
|
|
81
|
+
step_lock = threading.Lock()
|
|
82
|
+
memory_lock = threading.Lock() # New lock for memory operations to prevent race conditions
|
|
83
|
+
|
|
84
|
+
# Background milestone processing
|
|
85
|
+
state_update_thread = None
|
|
86
|
+
state_update_running = False
|
|
87
|
+
|
|
88
|
+
# Button mapping removed - handled by client
|
|
89
|
+
|
|
90
|
+
# Video recording functions
|
|
91
|
+
def init_video_recording(record_enabled=False):
|
|
92
|
+
"""Initialize video recording if enabled"""
|
|
93
|
+
global video_writer, video_recording, video_filename, fps, video_frame_skip
|
|
94
|
+
|
|
95
|
+
if not record_enabled:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
# Create video filename with timestamp
|
|
100
|
+
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
101
|
+
video_filename = f"pokegent_recording_{timestamp}.mp4"
|
|
102
|
+
|
|
103
|
+
# Video settings (GBA resolution is 240x160)
|
|
104
|
+
# Record at 30 FPS (skip every 4th frame from 120 FPS emulator)
|
|
105
|
+
recording_fps = fps / video_frame_skip # 120 / 4 = 30 FPS
|
|
106
|
+
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
|
|
107
|
+
video_writer = cv2.VideoWriter(video_filename, fourcc, float(recording_fps), (240, 160))
|
|
108
|
+
|
|
109
|
+
if video_writer.isOpened():
|
|
110
|
+
video_recording = True
|
|
111
|
+
print(f"📹 Video recording started: {video_filename} at {recording_fps:.0f} FPS (recording every {video_frame_skip} frames)")
|
|
112
|
+
else:
|
|
113
|
+
print("❌ Failed to initialize video recording")
|
|
114
|
+
video_writer = None
|
|
115
|
+
|
|
116
|
+
except Exception as e:
|
|
117
|
+
print(f"❌ Video recording initialization error: {e}")
|
|
118
|
+
video_writer = None
|
|
119
|
+
|
|
120
|
+
def update_frame_cache(screenshot):
|
|
121
|
+
"""Update the frame cache file for the separate frame server"""
|
|
122
|
+
global frame_cache_counter, FRAME_CACHE_FILE
|
|
123
|
+
|
|
124
|
+
if screenshot is None:
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
try:
|
|
128
|
+
# Convert screenshot to base64
|
|
129
|
+
if hasattr(screenshot, 'save'): # PIL image
|
|
130
|
+
buffer = io.BytesIO()
|
|
131
|
+
screenshot.save(buffer, format='PNG')
|
|
132
|
+
img_str = base64.b64encode(buffer.getvalue()).decode()
|
|
133
|
+
elif isinstance(screenshot, np.ndarray): # Numpy array
|
|
134
|
+
pil_image = Image.fromarray(screenshot)
|
|
135
|
+
buffer = io.BytesIO()
|
|
136
|
+
pil_image.save(buffer, format='PNG')
|
|
137
|
+
img_str = base64.b64encode(buffer.getvalue()).decode()
|
|
138
|
+
else:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
frame_cache_counter += 1
|
|
142
|
+
|
|
143
|
+
# Write to cache file atomically
|
|
144
|
+
cache_data = {
|
|
145
|
+
"frame_data": img_str,
|
|
146
|
+
"frame_counter": frame_cache_counter,
|
|
147
|
+
"timestamp": time.time()
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
# Write to temporary file first, then move (atomic operation)
|
|
151
|
+
temp_file = FRAME_CACHE_FILE + ".tmp"
|
|
152
|
+
with open(temp_file, 'w') as f:
|
|
153
|
+
json.dump(cache_data, f)
|
|
154
|
+
os.rename(temp_file, FRAME_CACHE_FILE)
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
pass # Silently handle cache write errors
|
|
158
|
+
|
|
159
|
+
def record_frame(screenshot):
|
|
160
|
+
"""Record frame to video if recording is enabled with frame skipping"""
|
|
161
|
+
global video_writer, video_recording, video_frame_counter, video_frame_skip
|
|
162
|
+
|
|
163
|
+
if not video_recording or video_writer is None or screenshot is None:
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
# Increment frame counter
|
|
167
|
+
video_frame_counter += 1
|
|
168
|
+
|
|
169
|
+
# Only record every Nth frame based on frame skip
|
|
170
|
+
if video_frame_counter % video_frame_skip != 0:
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
# Convert PIL image to OpenCV format
|
|
175
|
+
if hasattr(screenshot, 'save'): # PIL image
|
|
176
|
+
# Convert PIL to numpy array
|
|
177
|
+
frame_array = np.array(screenshot)
|
|
178
|
+
# Convert RGB to BGR for OpenCV
|
|
179
|
+
frame_bgr = cv2.cvtColor(frame_array, cv2.COLOR_RGB2BGR)
|
|
180
|
+
video_writer.write(frame_bgr)
|
|
181
|
+
elif isinstance(screenshot, np.ndarray): # Already numpy array
|
|
182
|
+
# Convert RGB to BGR for OpenCV if needed
|
|
183
|
+
if screenshot.shape[2] == 3: # RGB
|
|
184
|
+
frame_bgr = cv2.cvtColor(screenshot, cv2.COLOR_RGB2BGR)
|
|
185
|
+
else:
|
|
186
|
+
frame_bgr = screenshot
|
|
187
|
+
video_writer.write(frame_bgr)
|
|
188
|
+
|
|
189
|
+
except Exception as e:
|
|
190
|
+
import logging
|
|
191
|
+
logger = logging.getLogger(__name__)
|
|
192
|
+
logger.debug(f"Video recording frame error: {e}")
|
|
193
|
+
|
|
194
|
+
def cleanup_video_recording():
|
|
195
|
+
"""Clean up video recording resources"""
|
|
196
|
+
global video_writer, video_recording
|
|
197
|
+
|
|
198
|
+
if video_recording and video_writer is not None:
|
|
199
|
+
try:
|
|
200
|
+
video_writer.release()
|
|
201
|
+
print(f"📹 Video recording saved: {video_filename}")
|
|
202
|
+
except Exception as e:
|
|
203
|
+
print(f"❌ Error saving video recording: {e}")
|
|
204
|
+
finally:
|
|
205
|
+
video_writer = None
|
|
206
|
+
video_recording = False
|
|
207
|
+
|
|
208
|
+
# Milestone tracking is now handled by the emulator
|
|
209
|
+
|
|
210
|
+
# FastAPI app
|
|
211
|
+
app = FastAPI(
|
|
212
|
+
title="PokeAgent Challenge",
|
|
213
|
+
description="Streamer display FastAPI endpoints",
|
|
214
|
+
version="3.0.0-preview",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Add CORS middleware
|
|
218
|
+
app.add_middleware(
|
|
219
|
+
CORSMiddleware,
|
|
220
|
+
allow_origins=["*"],
|
|
221
|
+
allow_credentials=True,
|
|
222
|
+
allow_methods=["*"],
|
|
223
|
+
allow_headers=["*"],
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Models for API requests and responses
|
|
227
|
+
class ActionRequest(BaseModel):
|
|
228
|
+
buttons: list = [] # List of button names: A, B, SELECT, START, UP, DOWN, LEFT, RIGHT
|
|
229
|
+
|
|
230
|
+
class GameStateResponse(BaseModel):
|
|
231
|
+
screenshot_base64: str
|
|
232
|
+
step_number: int
|
|
233
|
+
resolution: list # [width, height]
|
|
234
|
+
status: str
|
|
235
|
+
|
|
236
|
+
class ComprehensiveStateResponse(BaseModel):
|
|
237
|
+
visual: dict
|
|
238
|
+
player: dict
|
|
239
|
+
game: dict
|
|
240
|
+
map: dict
|
|
241
|
+
milestones: dict = {}
|
|
242
|
+
location_connections: dict = {} # Add location connections for portal display
|
|
243
|
+
step_number: int
|
|
244
|
+
status: str
|
|
245
|
+
action_queue_length: int = 0
|
|
246
|
+
|
|
247
|
+
def periodic_milestone_updater():
|
|
248
|
+
"""Lightweight background thread that only updates milestones occasionally"""
|
|
249
|
+
global state_update_running
|
|
250
|
+
|
|
251
|
+
last_milestone_update = 0
|
|
252
|
+
|
|
253
|
+
while state_update_running and running:
|
|
254
|
+
try:
|
|
255
|
+
current_time = time.time()
|
|
256
|
+
|
|
257
|
+
# Update milestones only every 5 seconds (much less frequent)
|
|
258
|
+
if current_time - last_milestone_update >= 5.0:
|
|
259
|
+
if env and env.memory_reader:
|
|
260
|
+
try:
|
|
261
|
+
# Use lightweight state for milestone updates only
|
|
262
|
+
basic_state = {
|
|
263
|
+
"player": {
|
|
264
|
+
"money": env.get_money(),
|
|
265
|
+
"party_size": len(env.get_party_pokemon() or []),
|
|
266
|
+
"position": env.get_coordinates()
|
|
267
|
+
},
|
|
268
|
+
"map": {
|
|
269
|
+
"location": env.get_location()
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
env.check_and_update_milestones(basic_state)
|
|
273
|
+
last_milestone_update = current_time
|
|
274
|
+
logger.debug("Lightweight milestone update completed")
|
|
275
|
+
except Exception as e:
|
|
276
|
+
logger.debug(f"Milestone update failed: {e}")
|
|
277
|
+
|
|
278
|
+
# Sleep for 1 second between checks
|
|
279
|
+
time.sleep(1.0)
|
|
280
|
+
|
|
281
|
+
except Exception as e:
|
|
282
|
+
logger.error(f"Error in milestone updater: {e}")
|
|
283
|
+
time.sleep(5.0) # Wait longer on error
|
|
284
|
+
|
|
285
|
+
def signal_handler(signum, frame):
|
|
286
|
+
"""Handle shutdown signals gracefully"""
|
|
287
|
+
global running, state_update_running
|
|
288
|
+
print(f"\nReceived signal {signum}, shutting down gracefully...")
|
|
289
|
+
running = False
|
|
290
|
+
state_update_running = False
|
|
291
|
+
cleanup_video_recording()
|
|
292
|
+
if env:
|
|
293
|
+
env.stop()
|
|
294
|
+
sys.exit(0)
|
|
295
|
+
|
|
296
|
+
def setup_environment(skip_initial_state=False):
|
|
297
|
+
"""Initialize the emulator"""
|
|
298
|
+
global env, current_obs, anticheat_tracker
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
rom_path = "Emerald-GBAdvance/rom.gba"
|
|
302
|
+
if not os.path.exists(rom_path):
|
|
303
|
+
raise RuntimeError(f"ROM not found at {rom_path}")
|
|
304
|
+
|
|
305
|
+
env = EmeraldEmulator(rom_path=rom_path)
|
|
306
|
+
env.initialize()
|
|
307
|
+
|
|
308
|
+
# Initialize AntiCheat tracker for submission logging
|
|
309
|
+
anticheat_tracker = AntiCheatTracker()
|
|
310
|
+
anticheat_tracker.initialize_submission_log("SERVER_MODE")
|
|
311
|
+
print("AntiCheat tracker initialized for submission logging")
|
|
312
|
+
|
|
313
|
+
# Log initial GAME_RUNNING milestone at startup (STEP=0, time=0)
|
|
314
|
+
# Skip this if we're going to load a state anyway
|
|
315
|
+
if not skip_initial_state:
|
|
316
|
+
try:
|
|
317
|
+
# Mark GAME_RUNNING milestone as completed immediately
|
|
318
|
+
env.milestone_tracker.mark_completed("GAME_RUNNING")
|
|
319
|
+
|
|
320
|
+
# Get initial game state for logging
|
|
321
|
+
initial_state = env.get_comprehensive_state()
|
|
322
|
+
|
|
323
|
+
# Create state hash
|
|
324
|
+
import hashlib
|
|
325
|
+
state_str = str(initial_state)
|
|
326
|
+
state_hash = hashlib.md5(state_str.encode()).hexdigest()[:8]
|
|
327
|
+
|
|
328
|
+
# Log initial entry with GAME_RUNNING milestone
|
|
329
|
+
anticheat_tracker.log_submission_data(
|
|
330
|
+
step=0,
|
|
331
|
+
state_data=initial_state,
|
|
332
|
+
action_taken="INIT",
|
|
333
|
+
decision_time=0.0,
|
|
334
|
+
state_hash=state_hash,
|
|
335
|
+
manual_mode=True,
|
|
336
|
+
milestone_override="GAME_RUNNING"
|
|
337
|
+
)
|
|
338
|
+
print("Initial GAME_RUNNING milestone logged at startup")
|
|
339
|
+
|
|
340
|
+
except Exception as e:
|
|
341
|
+
print(f"Warning: Could not log initial milestone: {e}")
|
|
342
|
+
|
|
343
|
+
screenshot = env.get_screenshot()
|
|
344
|
+
if screenshot:
|
|
345
|
+
with obs_lock:
|
|
346
|
+
current_obs = np.array(screenshot)
|
|
347
|
+
else:
|
|
348
|
+
with obs_lock:
|
|
349
|
+
current_obs = np.zeros((env.height, env.width, 3), dtype=np.uint8)
|
|
350
|
+
|
|
351
|
+
print("Emulator initialized successfully!")
|
|
352
|
+
return True
|
|
353
|
+
|
|
354
|
+
except Exception as e:
|
|
355
|
+
print(f"Failed to initialize emulator: {e}")
|
|
356
|
+
return False
|
|
357
|
+
|
|
358
|
+
def handle_input(manual_mode=False):
|
|
359
|
+
"""Handle input - server runs headless, no input handling needed"""
|
|
360
|
+
# Server always runs headless - input handled by client via HTTP API
|
|
361
|
+
return True, []
|
|
362
|
+
|
|
363
|
+
def step_environment(actions_pressed):
|
|
364
|
+
"""Take a step in the environment with optimized locking for better performance"""
|
|
365
|
+
global current_obs
|
|
366
|
+
|
|
367
|
+
# Debug: print what actions are being sent to emulator
|
|
368
|
+
# if actions_pressed:
|
|
369
|
+
# print( Stepping emulator with actions: {actions_pressed}")
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
# Only use memory_lock for the essential emulator step
|
|
373
|
+
with memory_lock:
|
|
374
|
+
env.run_frame_with_buttons(actions_pressed)
|
|
375
|
+
|
|
376
|
+
# Do lightweight area transition detection inside the lock
|
|
377
|
+
if hasattr(env, 'memory_reader') and env.memory_reader:
|
|
378
|
+
try:
|
|
379
|
+
transition_detected = env.memory_reader._check_area_transition()
|
|
380
|
+
if transition_detected:
|
|
381
|
+
logger.info("Area transition detected")
|
|
382
|
+
env.memory_reader.invalidate_map_cache()
|
|
383
|
+
if hasattr(env.memory_reader, '_cached_behaviors'):
|
|
384
|
+
env.memory_reader._cached_behaviors = None
|
|
385
|
+
if hasattr(env.memory_reader, '_cached_behaviors_map_key'):
|
|
386
|
+
env.memory_reader._cached_behaviors_map_key = None
|
|
387
|
+
# Set flag to trigger map stitcher update outside the lock
|
|
388
|
+
env.memory_reader._area_transition_detected = True
|
|
389
|
+
except Exception as e:
|
|
390
|
+
logger.warning(f"Area transition check failed: {e}")
|
|
391
|
+
|
|
392
|
+
# Update screenshot outside the memory lock to reduce contention
|
|
393
|
+
try:
|
|
394
|
+
screenshot = env.get_screenshot()
|
|
395
|
+
if screenshot:
|
|
396
|
+
record_frame(screenshot)
|
|
397
|
+
update_frame_cache(screenshot) # Update frame cache for separate frame server
|
|
398
|
+
with obs_lock:
|
|
399
|
+
current_obs = np.array(screenshot)
|
|
400
|
+
|
|
401
|
+
# Update map stitcher on position changes (lightweight approach)
|
|
402
|
+
# This ensures map data stays current as player moves
|
|
403
|
+
if hasattr(env, 'memory_reader') and env.memory_reader:
|
|
404
|
+
try:
|
|
405
|
+
# Check if player position has changed
|
|
406
|
+
should_update = False
|
|
407
|
+
|
|
408
|
+
# Get current player coordinates and map info
|
|
409
|
+
current_coords = env.memory_reader.read_coordinates()
|
|
410
|
+
current_map_bank = env.memory_reader._read_u8(env.memory_reader.addresses.MAP_BANK)
|
|
411
|
+
current_map_number = env.memory_reader._read_u8(env.memory_reader.addresses.MAP_NUMBER)
|
|
412
|
+
current_map_info = (current_map_bank, current_map_number)
|
|
413
|
+
|
|
414
|
+
# Initialize tracking variables if needed
|
|
415
|
+
if not hasattr(env, '_last_player_coords'):
|
|
416
|
+
env._last_player_coords = None
|
|
417
|
+
env._last_map_info = None
|
|
418
|
+
|
|
419
|
+
# Check for position changes
|
|
420
|
+
if current_coords != env._last_player_coords or current_map_info != env._last_map_info:
|
|
421
|
+
should_update = True
|
|
422
|
+
env._last_player_coords = current_coords
|
|
423
|
+
env._last_map_info = current_map_info
|
|
424
|
+
print(f"📍 Position change detected: {current_coords}, map: {current_map_info}")
|
|
425
|
+
logger.debug(f"Map stitcher update triggered by position change: {current_coords}, map: {current_map_info}")
|
|
426
|
+
|
|
427
|
+
# Always update on area transitions (already detected above)
|
|
428
|
+
if hasattr(env.memory_reader, '_area_transition_detected') and env.memory_reader._area_transition_detected:
|
|
429
|
+
should_update = True
|
|
430
|
+
env.memory_reader._area_transition_detected = False # Reset flag
|
|
431
|
+
logger.debug("Map stitcher update triggered by area transition")
|
|
432
|
+
|
|
433
|
+
# Update map stitcher directly when position changes
|
|
434
|
+
if should_update:
|
|
435
|
+
# @TODO should do location change warps here too
|
|
436
|
+
print(f"🗺️ Triggering map stitcher update for position change")
|
|
437
|
+
# Call map stitcher update directly without full map reading
|
|
438
|
+
tiles = env.memory_reader.read_map_around_player(radius=7)
|
|
439
|
+
if tiles:
|
|
440
|
+
print(f"🗺️ Got {len(tiles)} tiles, updating map stitcher")
|
|
441
|
+
state = {"map": {}} # Basic state for stitcher
|
|
442
|
+
env.memory_reader._update_map_stitcher(tiles, state)
|
|
443
|
+
logger.debug("Map stitcher updated for position change")
|
|
444
|
+
print(f"✅ Map stitcher update completed")
|
|
445
|
+
else:
|
|
446
|
+
print(f"❌ No tiles found for map stitcher update")
|
|
447
|
+
|
|
448
|
+
except Exception as e:
|
|
449
|
+
logger.error(f"Failed to update map stitcher during movement: {e}")
|
|
450
|
+
print(f"❌ Map stitcher update failed: {e}")
|
|
451
|
+
except Exception as e:
|
|
452
|
+
logger.warning(f"Error updating screenshot: {e}")
|
|
453
|
+
|
|
454
|
+
def update_display(manual_mode=False):
|
|
455
|
+
"""Update display - server runs headless, no display update needed"""
|
|
456
|
+
# Server runs headless - display handled by client
|
|
457
|
+
pass
|
|
458
|
+
|
|
459
|
+
def draw_info_overlay():
|
|
460
|
+
"""Draw info overlay - server runs headless, no overlay needed"""
|
|
461
|
+
# Server runs headless - overlay handled by client
|
|
462
|
+
pass
|
|
463
|
+
|
|
464
|
+
def save_screenshot():
|
|
465
|
+
"""Save current screenshot"""
|
|
466
|
+
global current_obs
|
|
467
|
+
|
|
468
|
+
with obs_lock:
|
|
469
|
+
obs_copy = current_obs.copy() if current_obs is not None else None
|
|
470
|
+
|
|
471
|
+
if obs_copy is not None:
|
|
472
|
+
timestamp = int(time.time())
|
|
473
|
+
filename = f"simple_test_screenshot_{timestamp}.png"
|
|
474
|
+
img = Image.fromarray(obs_copy)
|
|
475
|
+
img.save(filename)
|
|
476
|
+
print(f"Screenshot saved: {filename}")
|
|
477
|
+
|
|
478
|
+
def reset_game():
|
|
479
|
+
"""Reset the game and all milestones"""
|
|
480
|
+
global env, step_count
|
|
481
|
+
|
|
482
|
+
print("Resetting game and milestones...")
|
|
483
|
+
with step_lock:
|
|
484
|
+
env.initialize()
|
|
485
|
+
env.milestone_tracker.reset_all() # Reset all milestones
|
|
486
|
+
step_count = 0
|
|
487
|
+
print("Game and milestone reset complete")
|
|
488
|
+
|
|
489
|
+
def game_loop(manual_mode=False):
|
|
490
|
+
"""Main game loop - runs in main thread, always headless"""
|
|
491
|
+
global running, step_count
|
|
492
|
+
|
|
493
|
+
print("Starting headless game loop...")
|
|
494
|
+
|
|
495
|
+
while running:
|
|
496
|
+
# Handle input
|
|
497
|
+
should_continue, actions_pressed = handle_input(manual_mode)
|
|
498
|
+
if not should_continue:
|
|
499
|
+
break
|
|
500
|
+
|
|
501
|
+
# In server mode, handle action queue with proper button hold timing
|
|
502
|
+
action_completed = False
|
|
503
|
+
if not manual_mode:
|
|
504
|
+
global current_action, action_frames_remaining, release_frames_remaining
|
|
505
|
+
|
|
506
|
+
if current_action and action_frames_remaining > 0:
|
|
507
|
+
# Continue holding the current action
|
|
508
|
+
actions_pressed = [current_action]
|
|
509
|
+
action_frames_remaining -= 1
|
|
510
|
+
if action_frames_remaining == 0:
|
|
511
|
+
# Action finished, start release delay
|
|
512
|
+
current_action = None
|
|
513
|
+
release_frames_remaining = ACTION_RELEASE_DELAY
|
|
514
|
+
action_completed = True # Mark action as completed
|
|
515
|
+
print(f"✅ Action completed: step_count will increment")
|
|
516
|
+
elif release_frames_remaining > 0:
|
|
517
|
+
# Release delay (no button pressed)
|
|
518
|
+
actions_pressed = []
|
|
519
|
+
release_frames_remaining -= 1
|
|
520
|
+
elif action_queue:
|
|
521
|
+
# Start a new action from the queue
|
|
522
|
+
current_action = action_queue.pop(0)
|
|
523
|
+
action_frames_remaining = ACTION_HOLD_FRAMES
|
|
524
|
+
actions_pressed = [current_action]
|
|
525
|
+
queue_len = len(action_queue)
|
|
526
|
+
# Get current FPS for estimation
|
|
527
|
+
current_fps_for_calc = env.get_current_fps(fps) if env else fps
|
|
528
|
+
estimated_time = queue_len * (ACTION_HOLD_FRAMES + ACTION_RELEASE_DELAY) / current_fps_for_calc
|
|
529
|
+
print(f"🎮 Server processing action: {current_action}, Queue remaining: {queue_len} actions (~{estimated_time:.1f}s)")
|
|
530
|
+
else:
|
|
531
|
+
# No action to process
|
|
532
|
+
actions_pressed = []
|
|
533
|
+
|
|
534
|
+
# Step environment
|
|
535
|
+
step_environment(actions_pressed)
|
|
536
|
+
|
|
537
|
+
# Milestones are now updated in background thread
|
|
538
|
+
|
|
539
|
+
# Server runs headless - no display update needed
|
|
540
|
+
update_display(manual_mode)
|
|
541
|
+
|
|
542
|
+
# Only increment step count when an action is completed
|
|
543
|
+
if action_completed:
|
|
544
|
+
with step_lock:
|
|
545
|
+
step_count += 1
|
|
546
|
+
print(f"📈 Step count incremented to: {step_count}")
|
|
547
|
+
|
|
548
|
+
# Performance monitoring - log actual FPS every 5 seconds
|
|
549
|
+
global last_fps_log, frame_count_since_log
|
|
550
|
+
frame_count_since_log += 1
|
|
551
|
+
current_time = time.time()
|
|
552
|
+
if current_time - last_fps_log >= 5.0: # Log every 5 seconds
|
|
553
|
+
actual_fps = frame_count_since_log / (current_time - last_fps_log)
|
|
554
|
+
queue_len = len(action_queue)
|
|
555
|
+
print(f"📊 Server FPS: {actual_fps:.1f} (target: {fps}), Queue: {queue_len} actions")
|
|
556
|
+
last_fps_log = current_time
|
|
557
|
+
frame_count_since_log = 0
|
|
558
|
+
|
|
559
|
+
# Use dynamic FPS - 2x speed during dialog
|
|
560
|
+
current_fps = env.get_current_fps(fps) if env else fps
|
|
561
|
+
# Server runs headless - always use sleep for timing
|
|
562
|
+
time.sleep(1.0 / current_fps)
|
|
563
|
+
|
|
564
|
+
def run_fastapi_server(port):
|
|
565
|
+
"""Run FastAPI server in background thread"""
|
|
566
|
+
uvicorn.run(
|
|
567
|
+
app,
|
|
568
|
+
host="0.0.0.0",
|
|
569
|
+
port=port,
|
|
570
|
+
log_level="error",
|
|
571
|
+
access_log=False,
|
|
572
|
+
timeout_keep_alive=60, # Keep connections alive longer
|
|
573
|
+
timeout_graceful_shutdown=30 # More time for graceful shutdown
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
# Serve stream.html
|
|
577
|
+
@app.get("/stream")
|
|
578
|
+
async def get_stream():
|
|
579
|
+
"""Serve the stream.html interface"""
|
|
580
|
+
try:
|
|
581
|
+
with open("server/stream.html", "r") as f:
|
|
582
|
+
content = f.read()
|
|
583
|
+
return HTMLResponse(content=content)
|
|
584
|
+
except FileNotFoundError:
|
|
585
|
+
raise HTTPException(status_code=404, detail="Stream interface not found")
|
|
586
|
+
|
|
587
|
+
# FastAPI endpoints
|
|
588
|
+
@app.get("/health")
|
|
589
|
+
async def get_health():
|
|
590
|
+
"""Health check endpoint for server monitoring"""
|
|
591
|
+
return {"status": "healthy", "timestamp": time.time()}
|
|
592
|
+
|
|
593
|
+
@app.get("/status")
|
|
594
|
+
async def get_status():
|
|
595
|
+
"""Get server status"""
|
|
596
|
+
with step_lock:
|
|
597
|
+
current_step = step_count
|
|
598
|
+
|
|
599
|
+
# Get current FPS (may be 4x during dialog)
|
|
600
|
+
current_fps = env.get_current_fps(fps) if env else fps
|
|
601
|
+
# Use cached dialog state for consistency with FPS calculation
|
|
602
|
+
is_dialog = env._cached_dialog_state if env else False
|
|
603
|
+
|
|
604
|
+
return {
|
|
605
|
+
"status": "running",
|
|
606
|
+
"step_count": current_step,
|
|
607
|
+
"base_fps": fps,
|
|
608
|
+
"current_fps": current_fps,
|
|
609
|
+
"is_dialog": is_dialog,
|
|
610
|
+
"fps_multiplier": 2 if is_dialog else 1
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
@app.get("/screenshot")
|
|
614
|
+
async def get_screenshot():
|
|
615
|
+
"""Get current screenshot"""
|
|
616
|
+
global current_obs, step_count
|
|
617
|
+
|
|
618
|
+
if env is None:
|
|
619
|
+
raise HTTPException(status_code=400, detail="Emulator not initialized")
|
|
620
|
+
|
|
621
|
+
with obs_lock:
|
|
622
|
+
obs_copy = current_obs.copy() if current_obs is not None else None
|
|
623
|
+
|
|
624
|
+
if obs_copy is None:
|
|
625
|
+
raise HTTPException(status_code=500, detail="No screenshot available")
|
|
626
|
+
|
|
627
|
+
try:
|
|
628
|
+
# Convert numpy array to PIL image
|
|
629
|
+
pil_image = Image.fromarray(obs_copy)
|
|
630
|
+
|
|
631
|
+
# Convert to base64
|
|
632
|
+
buffer = io.BytesIO()
|
|
633
|
+
pil_image.save(buffer, format='PNG')
|
|
634
|
+
img_str = base64.b64encode(buffer.getvalue()).decode()
|
|
635
|
+
|
|
636
|
+
with step_lock:
|
|
637
|
+
current_step = step_count
|
|
638
|
+
|
|
639
|
+
return GameStateResponse(
|
|
640
|
+
screenshot_base64=img_str,
|
|
641
|
+
step_number=current_step,
|
|
642
|
+
resolution=[obs_copy.shape[1], obs_copy.shape[0]],
|
|
643
|
+
status="running"
|
|
644
|
+
)
|
|
645
|
+
|
|
646
|
+
except Exception as e:
|
|
647
|
+
logger.error(f"Error getting screenshot: {e}")
|
|
648
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
649
|
+
|
|
650
|
+
@app.get("/api/frame")
|
|
651
|
+
async def get_latest_frame():
|
|
652
|
+
"""Get latest game frame in same format as single-process mode"""
|
|
653
|
+
global current_obs, env
|
|
654
|
+
|
|
655
|
+
with obs_lock:
|
|
656
|
+
obs_copy = current_obs.copy() if current_obs is not None else None
|
|
657
|
+
|
|
658
|
+
# If current_obs is None (e.g., after server restart), try to get a fresh screenshot
|
|
659
|
+
if obs_copy is None and env:
|
|
660
|
+
try:
|
|
661
|
+
screenshot = env.get_screenshot()
|
|
662
|
+
if screenshot:
|
|
663
|
+
obs_copy = np.array(screenshot)
|
|
664
|
+
# Update current_obs for future requests
|
|
665
|
+
with obs_lock:
|
|
666
|
+
current_obs = obs_copy.copy()
|
|
667
|
+
logger.debug("Frame endpoint: Retrieved fresh screenshot after restart")
|
|
668
|
+
except Exception as e:
|
|
669
|
+
logger.warning(f"Frame endpoint: Failed to get fresh screenshot: {e}")
|
|
670
|
+
|
|
671
|
+
if obs_copy is None:
|
|
672
|
+
return {"frame": ""}
|
|
673
|
+
|
|
674
|
+
try:
|
|
675
|
+
# Convert to base64
|
|
676
|
+
pil_image = Image.fromarray(obs_copy)
|
|
677
|
+
buffer = io.BytesIO()
|
|
678
|
+
pil_image.save(buffer, format='PNG')
|
|
679
|
+
img_str = base64.b64encode(buffer.getvalue()).decode()
|
|
680
|
+
|
|
681
|
+
return {"frame": img_str}
|
|
682
|
+
except Exception as e:
|
|
683
|
+
logger.warning(f"Frame endpoint: Error encoding frame: {e}")
|
|
684
|
+
return {"frame": ""}
|
|
685
|
+
|
|
686
|
+
@app.post("/action")
|
|
687
|
+
async def take_action(request: ActionRequest):
|
|
688
|
+
"""Take an action"""
|
|
689
|
+
global current_obs, step_count, recent_button_presses, action_queue, anticheat_tracker, step_counter, last_action_time
|
|
690
|
+
|
|
691
|
+
# print( Action endpoint called with request: {request}")
|
|
692
|
+
# print( Request buttons: {request.buttons}")
|
|
693
|
+
|
|
694
|
+
if env is None:
|
|
695
|
+
# print( Emulator not initialized")
|
|
696
|
+
raise HTTPException(status_code=400, detail="Emulator not initialized")
|
|
697
|
+
|
|
698
|
+
try:
|
|
699
|
+
# Add all actions to the queue (handle both single actions and lists)
|
|
700
|
+
if request.buttons:
|
|
701
|
+
# Add ALL actions to the queue - let the game loop handle execution
|
|
702
|
+
print(f"📡 Server received actions: {request.buttons}")
|
|
703
|
+
print(f"📋 Action queue before extend: {action_queue}")
|
|
704
|
+
action_queue.extend(request.buttons)
|
|
705
|
+
print(f"📋 Action queue after extend: {action_queue}")
|
|
706
|
+
|
|
707
|
+
# Track button presses for recent actions display
|
|
708
|
+
current_time = time.time()
|
|
709
|
+
for button in request.buttons:
|
|
710
|
+
# Add all buttons to recent actions (removed duplicate filtering for debugging)
|
|
711
|
+
recent_button_presses.append({
|
|
712
|
+
"button": button,
|
|
713
|
+
"timestamp": current_time
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
# Update total actions count in metrics
|
|
717
|
+
with step_lock:
|
|
718
|
+
latest_metrics["total_actions"] = latest_metrics.get("total_actions", 0) + len(request.buttons)
|
|
719
|
+
|
|
720
|
+
# Also update the LLM logger's action count for checkpoint persistence
|
|
721
|
+
try:
|
|
722
|
+
from utils.llm_logger import get_llm_logger
|
|
723
|
+
llm_logger = get_llm_logger()
|
|
724
|
+
if llm_logger:
|
|
725
|
+
llm_logger.cumulative_metrics["total_actions"] = latest_metrics["total_actions"]
|
|
726
|
+
|
|
727
|
+
# Sync LLM logger's cumulative metrics back to latest_metrics
|
|
728
|
+
# This ensures token usage and costs from LLM interactions are displayed
|
|
729
|
+
cumulative_metrics_to_sync = ["total_tokens", "prompt_tokens", "completion_tokens", "total_cost", "total_llm_calls", "total_run_time"]
|
|
730
|
+
for metric_key in cumulative_metrics_to_sync:
|
|
731
|
+
if metric_key in llm_logger.cumulative_metrics:
|
|
732
|
+
latest_metrics[metric_key] = llm_logger.cumulative_metrics[metric_key]
|
|
733
|
+
except Exception as e:
|
|
734
|
+
logger.debug(f"Failed to sync metrics with LLM logger: {e}")
|
|
735
|
+
|
|
736
|
+
# Keep only last 50 button presses to avoid memory issues
|
|
737
|
+
if len(recent_button_presses) > 50:
|
|
738
|
+
recent_button_presses = recent_button_presses[-50:]
|
|
739
|
+
else:
|
|
740
|
+
print(f" No buttons in request")
|
|
741
|
+
|
|
742
|
+
# DON'T execute action here - let the game loop handle it from the queue
|
|
743
|
+
# This prevents conflicts between the API thread and pygame thread
|
|
744
|
+
|
|
745
|
+
# Return immediate success - avoid all locks to prevent deadlocks
|
|
746
|
+
actions_added = len(request.buttons) if request.buttons else 0
|
|
747
|
+
|
|
748
|
+
# print( Returning success, actions_added: {actions_added}, queue_length: {len(action_queue)}")
|
|
749
|
+
|
|
750
|
+
# Log action to submission.log if anticheat tracker is available
|
|
751
|
+
if anticheat_tracker and request.buttons:
|
|
752
|
+
try:
|
|
753
|
+
# Calculate decision time
|
|
754
|
+
current_time = time.time()
|
|
755
|
+
if last_action_time is not None:
|
|
756
|
+
decision_time = current_time - last_action_time
|
|
757
|
+
else:
|
|
758
|
+
decision_time = 0.0 # First action
|
|
759
|
+
last_action_time = current_time
|
|
760
|
+
|
|
761
|
+
# Get current game state for logging
|
|
762
|
+
game_state = env.get_comprehensive_state()
|
|
763
|
+
action_taken = request.buttons[0] if request.buttons else "NONE" # Log first action
|
|
764
|
+
|
|
765
|
+
# Create simple state hash
|
|
766
|
+
import hashlib
|
|
767
|
+
state_str = str(game_state)
|
|
768
|
+
state_hash = hashlib.md5(state_str.encode()).hexdigest()[:8]
|
|
769
|
+
|
|
770
|
+
# Determine if this is manual mode (from client) or agent mode
|
|
771
|
+
# For now, assume manual mode if coming through API
|
|
772
|
+
manual_mode = request.source == "manual" if hasattr(request, 'source') else True
|
|
773
|
+
|
|
774
|
+
# Get the latest milestone from the emulator's milestone tracker
|
|
775
|
+
# First, trigger an immediate milestone check to ensure current state is detected
|
|
776
|
+
latest_milestone = "NONE"
|
|
777
|
+
if env and hasattr(env, 'milestone_tracker'):
|
|
778
|
+
try:
|
|
779
|
+
# Force an immediate milestone check before logging
|
|
780
|
+
env.check_and_update_milestones(game_state)
|
|
781
|
+
except Exception as e:
|
|
782
|
+
logger.debug(f"Error during immediate milestone check: {e}")
|
|
783
|
+
|
|
784
|
+
milestone_name, split_time, total_time = env.milestone_tracker.get_latest_milestone_info()
|
|
785
|
+
latest_milestone = milestone_name if milestone_name != "NONE" else "NONE"
|
|
786
|
+
|
|
787
|
+
# Log the action
|
|
788
|
+
step_counter += 1
|
|
789
|
+
anticheat_tracker.log_submission_data(
|
|
790
|
+
step=step_counter,
|
|
791
|
+
state_data=game_state,
|
|
792
|
+
action_taken=action_taken,
|
|
793
|
+
decision_time=decision_time,
|
|
794
|
+
state_hash=state_hash,
|
|
795
|
+
manual_mode=manual_mode,
|
|
796
|
+
milestone_override=latest_milestone
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
except Exception as e:
|
|
800
|
+
logger.warning(f"Error logging to submission.log: {e}")
|
|
801
|
+
|
|
802
|
+
# Return lightweight response without any lock acquisition
|
|
803
|
+
return {
|
|
804
|
+
"status": "success",
|
|
805
|
+
"actions_queued": actions_added,
|
|
806
|
+
"queue_length": len(action_queue), # action_queue access is atomic for lists
|
|
807
|
+
"message": f"Added {actions_added} actions to queue"
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
except Exception as e:
|
|
811
|
+
# print( Exception in action endpoint: {e}")
|
|
812
|
+
logger.error(f"Error taking action: {e}")
|
|
813
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
814
|
+
|
|
815
|
+
|
|
816
|
+
@app.get("/queue_status")
|
|
817
|
+
async def get_queue_status():
|
|
818
|
+
"""Get action queue status"""
|
|
819
|
+
global action_queue, current_action, action_frames_remaining, release_frames_remaining
|
|
820
|
+
|
|
821
|
+
queue_empty = (len(action_queue) == 0 and
|
|
822
|
+
current_action is None and
|
|
823
|
+
action_frames_remaining == 0 and
|
|
824
|
+
release_frames_remaining == 0)
|
|
825
|
+
|
|
826
|
+
return {
|
|
827
|
+
"queue_empty": queue_empty,
|
|
828
|
+
"queue_length": len(action_queue),
|
|
829
|
+
"current_action": current_action,
|
|
830
|
+
"action_frames_remaining": action_frames_remaining,
|
|
831
|
+
"release_frames_remaining": release_frames_remaining
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
@app.get("/state")
|
|
835
|
+
async def get_comprehensive_state():
|
|
836
|
+
"""Get comprehensive game state including visual and memory data"""
|
|
837
|
+
if env is None:
|
|
838
|
+
raise HTTPException(status_code=400, detail="Emulator not initialized")
|
|
839
|
+
|
|
840
|
+
try:
|
|
841
|
+
# Use the emulator's built-in caching (100ms cache)
|
|
842
|
+
# This avoids expensive operations on rapid requests
|
|
843
|
+
state = env.get_comprehensive_state()
|
|
844
|
+
|
|
845
|
+
# Ensure game state is consistent with cached dialog state
|
|
846
|
+
# Use the same cached dialog state as the status endpoint
|
|
847
|
+
is_dialog = env._cached_dialog_state if env else False
|
|
848
|
+
if is_dialog:
|
|
849
|
+
state["game"]["game_state"] = "dialog"
|
|
850
|
+
else:
|
|
851
|
+
# Force overworld if not in dialog (respect 5-second timeout)
|
|
852
|
+
state["game"]["game_state"] = "overworld"
|
|
853
|
+
|
|
854
|
+
# Include milestones for storyline objective auto-completion
|
|
855
|
+
if env.milestone_tracker:
|
|
856
|
+
state["milestones"] = env.milestone_tracker.milestones
|
|
857
|
+
|
|
858
|
+
# Get map stitcher data for enhanced map display
|
|
859
|
+
# Use the memory_reader's MapStitcher instance which has the accumulated data
|
|
860
|
+
map_stitcher = None
|
|
861
|
+
if env and env.memory_reader and hasattr(env.memory_reader, '_map_stitcher'):
|
|
862
|
+
map_stitcher = env.memory_reader._map_stitcher
|
|
863
|
+
num_areas = len(map_stitcher.map_areas) if map_stitcher and hasattr(map_stitcher, 'map_areas') else 0
|
|
864
|
+
logger.debug(f"Using memory_reader's MapStitcher with {num_areas} areas")
|
|
865
|
+
else:
|
|
866
|
+
logger.debug("No MapStitcher available from memory_reader")
|
|
867
|
+
|
|
868
|
+
# Get current location name
|
|
869
|
+
current_location = state.get("player", {}).get("location", "Unknown")
|
|
870
|
+
player_pos = state.get("player", {}).get("position")
|
|
871
|
+
if player_pos:
|
|
872
|
+
player_coords = (player_pos.get("x", 0), player_pos.get("y", 0))
|
|
873
|
+
else:
|
|
874
|
+
player_coords = None
|
|
875
|
+
|
|
876
|
+
# Add stitched map info to the map section
|
|
877
|
+
if not "map" in state:
|
|
878
|
+
state["map"] = {}
|
|
879
|
+
|
|
880
|
+
# Check if visual_map was already generated by memory_reader
|
|
881
|
+
# If so, preserve it as it has the proper accumulated map data
|
|
882
|
+
visual_map_from_memory_reader = state.get("map", {}).get("visual_map")
|
|
883
|
+
if visual_map_from_memory_reader:
|
|
884
|
+
logger.debug("Using visual_map generated by memory_reader")
|
|
885
|
+
# Keep the visual_map as-is
|
|
886
|
+
elif map_stitcher:
|
|
887
|
+
# Generate visual map if not already present
|
|
888
|
+
try:
|
|
889
|
+
# Get NPCs from state if available
|
|
890
|
+
npcs = state.get("map", {}).get("object_events", [])
|
|
891
|
+
|
|
892
|
+
# Get connections for this location
|
|
893
|
+
connections_with_coords = []
|
|
894
|
+
if current_location and current_location != "Unknown":
|
|
895
|
+
location_connections = map_stitcher.get_location_connections(current_location)
|
|
896
|
+
for conn in location_connections:
|
|
897
|
+
if len(conn) >= 3:
|
|
898
|
+
other_loc, my_coords, their_coords = conn[0], conn[1], conn[2]
|
|
899
|
+
connections_with_coords.append({
|
|
900
|
+
"to": other_loc,
|
|
901
|
+
"from_pos": list(my_coords) if my_coords else [],
|
|
902
|
+
"to_pos": list(their_coords) if their_coords else []
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
# Generate the map display
|
|
906
|
+
map_lines = map_stitcher.generate_location_map_display(
|
|
907
|
+
location_name=current_location,
|
|
908
|
+
player_pos=player_coords,
|
|
909
|
+
npcs=npcs,
|
|
910
|
+
connections=connections_with_coords
|
|
911
|
+
)
|
|
912
|
+
|
|
913
|
+
# Store as formatted text
|
|
914
|
+
if map_lines:
|
|
915
|
+
state["map"]["visual_map"] = "\n".join(map_lines)
|
|
916
|
+
logger.debug(f"Generated visual_map with {len(map_lines)} lines")
|
|
917
|
+
except Exception as e:
|
|
918
|
+
logger.error(f"Failed to generate visual_map: {e}")
|
|
919
|
+
|
|
920
|
+
# Add stitched map info for the client/frontend
|
|
921
|
+
if map_stitcher:
|
|
922
|
+
# Get the location grid and connections
|
|
923
|
+
if current_location and current_location != "Unknown":
|
|
924
|
+
location_grid = map_stitcher.get_location_grid(current_location)
|
|
925
|
+
connections = []
|
|
926
|
+
|
|
927
|
+
# Get connections for this location
|
|
928
|
+
for other_loc, my_coords, their_coords in map_stitcher.get_location_connections(current_location):
|
|
929
|
+
connections.append({
|
|
930
|
+
"to": other_loc,
|
|
931
|
+
"from_pos": list(my_coords),
|
|
932
|
+
"to_pos": list(their_coords)
|
|
933
|
+
})
|
|
934
|
+
|
|
935
|
+
state["map"]["stitched_map_info"] = {
|
|
936
|
+
"available": True,
|
|
937
|
+
"current_area": {
|
|
938
|
+
"name": current_location,
|
|
939
|
+
"connections": connections,
|
|
940
|
+
"player_pos": player_coords
|
|
941
|
+
},
|
|
942
|
+
"player_local_pos": player_coords
|
|
943
|
+
}
|
|
944
|
+
else:
|
|
945
|
+
state["map"]["stitched_map_info"] = {
|
|
946
|
+
"available": False,
|
|
947
|
+
"reason": "Unknown location"
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
# Also include location connections directly for backward compatibility
|
|
951
|
+
try:
|
|
952
|
+
cache_file = ".pokeagent_cache/map_stitcher_data.json"
|
|
953
|
+
if os.path.exists(cache_file):
|
|
954
|
+
with open(cache_file, 'r') as f:
|
|
955
|
+
map_data = json.load(f)
|
|
956
|
+
if 'location_connections' in map_data and map_data['location_connections']:
|
|
957
|
+
location_connections = map_data['location_connections']
|
|
958
|
+
state["location_connections"] = location_connections
|
|
959
|
+
logger.debug(f"Loaded location connections for {len(location_connections) if location_connections else 0} locations")
|
|
960
|
+
elif 'warp_connections' in map_data and map_data['warp_connections']:
|
|
961
|
+
# Convert warp_connections to portal_connections format for LLM display
|
|
962
|
+
map_id_connections = {}
|
|
963
|
+
for conn in map_data['warp_connections']:
|
|
964
|
+
from_map = conn['from_map_id']
|
|
965
|
+
if from_map not in map_id_connections:
|
|
966
|
+
map_id_connections[from_map] = []
|
|
967
|
+
|
|
968
|
+
# Find the location name for the destination map
|
|
969
|
+
to_map_name = "Unknown Location"
|
|
970
|
+
if str(conn['to_map_id']) in map_data.get('map_areas', {}):
|
|
971
|
+
to_map_name = map_data['map_areas'][str(conn['to_map_id'])]['location_name']
|
|
972
|
+
|
|
973
|
+
map_id_connections[from_map].append({
|
|
974
|
+
'to_name': to_map_name,
|
|
975
|
+
'from_pos': conn['from_position'], # Keep as list for JSON serialization
|
|
976
|
+
'to_pos': conn['to_position'] # Keep as list for JSON serialization
|
|
977
|
+
})
|
|
978
|
+
|
|
979
|
+
state["portal_connections"] = map_id_connections
|
|
980
|
+
print(f"🗺️ SERVER: Added portal connections to state: {map_id_connections}")
|
|
981
|
+
print(f"🗺️ SERVER: State now has keys: {list(state.keys())}")
|
|
982
|
+
logger.debug(f"Loaded portal connections for {len(map_id_connections) if map_id_connections else 0} maps from persistent storage")
|
|
983
|
+
else:
|
|
984
|
+
print(f"🗺️ SERVER: No warp connections found in map data")
|
|
985
|
+
logger.debug("No warp connections found in map stitcher data")
|
|
986
|
+
else:
|
|
987
|
+
print(f"🗺️ SERVER: Cache file not found at {cache_file}")
|
|
988
|
+
logger.debug(f"Map stitcher cache file not found: {cache_file}")
|
|
989
|
+
except Exception as e:
|
|
990
|
+
import traceback
|
|
991
|
+
print(f"🗺️ SERVER: Error loading portal connections: {e}")
|
|
992
|
+
print(f"🗺️ SERVER: Full traceback: {traceback.format_exc()}")
|
|
993
|
+
logger.debug(f"Could not load portal connections from persistent storage: {e}")
|
|
994
|
+
|
|
995
|
+
# The battle information already contains all necessary data
|
|
996
|
+
# No additional analysis needed - keep it clean
|
|
997
|
+
|
|
998
|
+
# Remove MapStitcher instance to avoid serialization issues
|
|
999
|
+
# The instance is only for internal use by state_formatter
|
|
1000
|
+
if "_map_stitcher_instance" in state.get("map", {}):
|
|
1001
|
+
del state["map"]["_map_stitcher_instance"]
|
|
1002
|
+
|
|
1003
|
+
# Convert screenshot to base64 if available
|
|
1004
|
+
if state["visual"]["screenshot"]:
|
|
1005
|
+
buffer = io.BytesIO()
|
|
1006
|
+
state["visual"]["screenshot"].save(buffer, format='PNG')
|
|
1007
|
+
img_str = base64.b64encode(buffer.getvalue()).decode()
|
|
1008
|
+
state["visual"]["screenshot_base64"] = img_str
|
|
1009
|
+
# Remove the PIL image object to avoid serialization issues
|
|
1010
|
+
del state["visual"]["screenshot"]
|
|
1011
|
+
|
|
1012
|
+
with step_lock:
|
|
1013
|
+
current_step = step_count
|
|
1014
|
+
|
|
1015
|
+
# Include action queue info for multiprocess coordination
|
|
1016
|
+
queue_length = len(action_queue) # Action queue access is atomic for len()
|
|
1017
|
+
|
|
1018
|
+
return ComprehensiveStateResponse(
|
|
1019
|
+
visual=state["visual"],
|
|
1020
|
+
player=state["player"],
|
|
1021
|
+
game=state["game"],
|
|
1022
|
+
map=state["map"],
|
|
1023
|
+
milestones=state.get("milestones", {}),
|
|
1024
|
+
location_connections=state.get("location_connections", {}),
|
|
1025
|
+
step_number=current_step,
|
|
1026
|
+
status="running",
|
|
1027
|
+
action_queue_length=queue_length
|
|
1028
|
+
)
|
|
1029
|
+
|
|
1030
|
+
except Exception as e:
|
|
1031
|
+
logger.error(f"Error getting comprehensive state: {e}")
|
|
1032
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
1033
|
+
|
|
1034
|
+
@app.get("/debug/memory")
|
|
1035
|
+
async def debug_memory():
|
|
1036
|
+
"""Debug memory reading (basic version)"""
|
|
1037
|
+
if env is None:
|
|
1038
|
+
raise HTTPException(status_code=400, detail="Emulator not initialized")
|
|
1039
|
+
|
|
1040
|
+
try:
|
|
1041
|
+
if not env.memory_reader:
|
|
1042
|
+
return {"error": "Memory reader not initialized"}
|
|
1043
|
+
|
|
1044
|
+
# Test basic memory access
|
|
1045
|
+
diagnostics = env.memory_reader.test_memory_access()
|
|
1046
|
+
|
|
1047
|
+
# Try to read some basic data
|
|
1048
|
+
try:
|
|
1049
|
+
party_size = env.memory_reader.read_party_size()
|
|
1050
|
+
coordinates = env.memory_reader.read_coordinates()
|
|
1051
|
+
money = env.memory_reader.read_money()
|
|
1052
|
+
|
|
1053
|
+
# Add new debugging info
|
|
1054
|
+
is_in_battle = env.memory_reader.is_in_battle()
|
|
1055
|
+
game_state = env.memory_reader.get_game_state()
|
|
1056
|
+
player_name = env.memory_reader.read_player_name()
|
|
1057
|
+
|
|
1058
|
+
# Add battle detection debugging
|
|
1059
|
+
try:
|
|
1060
|
+
battle_addr = env.memory_reader.IN_BATTLE_BIT_ADDR
|
|
1061
|
+
battle_raw_value = env.memory_reader._read_u8(battle_addr)
|
|
1062
|
+
battle_mask = env.memory_reader.IN_BATTLE_BITMASK
|
|
1063
|
+
battle_result = (battle_raw_value & battle_mask) != 0
|
|
1064
|
+
except Exception as e:
|
|
1065
|
+
battle_raw_value = None
|
|
1066
|
+
battle_mask = None
|
|
1067
|
+
battle_result = None
|
|
1068
|
+
|
|
1069
|
+
diagnostics.update({
|
|
1070
|
+
'party_size': party_size,
|
|
1071
|
+
'coordinates': coordinates,
|
|
1072
|
+
'money': money,
|
|
1073
|
+
'is_in_battle': is_in_battle,
|
|
1074
|
+
'game_state': game_state,
|
|
1075
|
+
'player_name': player_name,
|
|
1076
|
+
'battle_detection': {
|
|
1077
|
+
'address': f'0x{battle_addr:08x}' if 'battle_addr' in locals() else 'unknown',
|
|
1078
|
+
'raw_value': f'0x{battle_raw_value:02x}' if battle_raw_value is not None else 'error',
|
|
1079
|
+
'mask': f'0x{battle_mask:02x}' if battle_mask is not None else 'unknown',
|
|
1080
|
+
'result': battle_result
|
|
1081
|
+
},
|
|
1082
|
+
'working_reads': True
|
|
1083
|
+
})
|
|
1084
|
+
except Exception as read_error:
|
|
1085
|
+
diagnostics['read_error'] = str(read_error)
|
|
1086
|
+
diagnostics['working_reads'] = False
|
|
1087
|
+
|
|
1088
|
+
return diagnostics
|
|
1089
|
+
|
|
1090
|
+
except Exception as e:
|
|
1091
|
+
logger.error(f"Error debugging memory: {e}")
|
|
1092
|
+
return {"error": str(e)}
|
|
1093
|
+
|
|
1094
|
+
@app.get("/debug/memory/comprehensive")
|
|
1095
|
+
async def debug_memory_comprehensive():
|
|
1096
|
+
"""Comprehensive memory reading test with detailed diagnostics"""
|
|
1097
|
+
if env is None:
|
|
1098
|
+
raise HTTPException(status_code=400, detail="Emulator not initialized")
|
|
1099
|
+
|
|
1100
|
+
try:
|
|
1101
|
+
# Use the comprehensive memory testing method
|
|
1102
|
+
test_results = env.test_memory_reading()
|
|
1103
|
+
return test_results
|
|
1104
|
+
|
|
1105
|
+
except Exception as e:
|
|
1106
|
+
logger.error(f"Error running comprehensive memory test: {e}")
|
|
1107
|
+
return {"error": str(e)}
|
|
1108
|
+
|
|
1109
|
+
@app.get("/debug/memory/dump")
|
|
1110
|
+
async def debug_memory_dump(start: int = 0x02000000, length: int = 0x1000):
|
|
1111
|
+
"""Dump raw memory from the emulator"""
|
|
1112
|
+
if env is None:
|
|
1113
|
+
raise HTTPException(status_code=400, detail="Emulator not initialized")
|
|
1114
|
+
|
|
1115
|
+
try:
|
|
1116
|
+
if not env.memory_reader:
|
|
1117
|
+
return {"error": "Memory reader not initialized"}
|
|
1118
|
+
|
|
1119
|
+
# Read raw memory bytes
|
|
1120
|
+
memory_bytes = env.memory_reader._read_bytes(start, length)
|
|
1121
|
+
|
|
1122
|
+
# Convert to hex string for easy viewing
|
|
1123
|
+
hex_data = memory_bytes.hex()
|
|
1124
|
+
|
|
1125
|
+
# Also try to decode as text using Pokemon Emerald character mapping
|
|
1126
|
+
try:
|
|
1127
|
+
from pokemon_env.emerald_utils import EmeraldCharmap
|
|
1128
|
+
charmap = EmeraldCharmap()
|
|
1129
|
+
decoded_text = charmap.decode(memory_bytes)
|
|
1130
|
+
except:
|
|
1131
|
+
decoded_text = "Could not decode as text"
|
|
1132
|
+
|
|
1133
|
+
return {
|
|
1134
|
+
"start_address": f"0x{start:08X}",
|
|
1135
|
+
"length": length,
|
|
1136
|
+
"hex_data": hex_data,
|
|
1137
|
+
"decoded_text": decoded_text,
|
|
1138
|
+
"raw_bytes": [b for b in memory_bytes[:100]] # First 100 bytes as numbers
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
except Exception as e:
|
|
1142
|
+
logger.error(f"Error dumping memory: {e}")
|
|
1143
|
+
return {"error": str(e)}
|
|
1144
|
+
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
@app.get("/test_stream")
|
|
1148
|
+
async def test_stream():
|
|
1149
|
+
"""Simple test stream to verify SSE works"""
|
|
1150
|
+
from fastapi.responses import StreamingResponse
|
|
1151
|
+
import asyncio
|
|
1152
|
+
|
|
1153
|
+
async def simple_stream():
|
|
1154
|
+
for i in range(5):
|
|
1155
|
+
yield f"data: {{'test': {i}, 'timestamp': {time.time()}}}\n\n"
|
|
1156
|
+
await asyncio.sleep(1)
|
|
1157
|
+
yield f"data: {{'done': true}}\n\n"
|
|
1158
|
+
|
|
1159
|
+
return StreamingResponse(simple_stream(), media_type="text/event-stream")
|
|
1160
|
+
|
|
1161
|
+
@app.get("/agent_stream")
|
|
1162
|
+
async def stream_agent_thinking():
|
|
1163
|
+
"""Stream agent thinking in real-time using Server-Sent Events"""
|
|
1164
|
+
from fastapi.responses import StreamingResponse
|
|
1165
|
+
import asyncio
|
|
1166
|
+
|
|
1167
|
+
async def event_stream():
|
|
1168
|
+
"""Generate server-sent events for agent thinking"""
|
|
1169
|
+
logger.info("SSE: Starting event stream")
|
|
1170
|
+
last_timestamp = "" # Track last seen timestamp instead of count
|
|
1171
|
+
sent_timestamps = set() # Track all sent timestamps to avoid duplicates
|
|
1172
|
+
heartbeat_counter = 0
|
|
1173
|
+
|
|
1174
|
+
try:
|
|
1175
|
+
# Send initial connection message
|
|
1176
|
+
yield f"data: {json.dumps({'status': 'connected', 'timestamp': time.time()})}\n\n"
|
|
1177
|
+
|
|
1178
|
+
# On startup, mark all existing interactions as "sent" to avoid flooding with old messages
|
|
1179
|
+
# We only want to stream NEW interactions from this point forward
|
|
1180
|
+
try:
|
|
1181
|
+
log_files = sorted(glob.glob("llm_logs/llm_log_*.jsonl"))
|
|
1182
|
+
for log_file in log_files:
|
|
1183
|
+
if os.path.exists(log_file):
|
|
1184
|
+
with open(log_file, 'r', encoding='utf-8') as f:
|
|
1185
|
+
for line in f:
|
|
1186
|
+
try:
|
|
1187
|
+
entry = json.loads(line.strip())
|
|
1188
|
+
if entry.get("type") == "interaction":
|
|
1189
|
+
timestamp = entry.get("timestamp", "")
|
|
1190
|
+
if timestamp:
|
|
1191
|
+
sent_timestamps.add(timestamp)
|
|
1192
|
+
except:
|
|
1193
|
+
continue
|
|
1194
|
+
logger.info(f"SSE: Marked {len(sent_timestamps)} existing interactions as already sent")
|
|
1195
|
+
except Exception as init_e:
|
|
1196
|
+
logger.warning(f"SSE: Error initializing sent timestamps: {init_e}")
|
|
1197
|
+
|
|
1198
|
+
while True:
|
|
1199
|
+
try:
|
|
1200
|
+
heartbeat_counter += 1
|
|
1201
|
+
|
|
1202
|
+
# Use simple file reading instead of complex get_agent_thinking()
|
|
1203
|
+
current_step = 0
|
|
1204
|
+
|
|
1205
|
+
with step_lock:
|
|
1206
|
+
current_step = agent_step_count
|
|
1207
|
+
|
|
1208
|
+
new_interactions = []
|
|
1209
|
+
try:
|
|
1210
|
+
# Read LLM log files directly (same as working /agent endpoint)
|
|
1211
|
+
log_files = sorted(glob.glob("llm_logs/llm_log_*.jsonl"))
|
|
1212
|
+
|
|
1213
|
+
# Check all recent log files for new entries
|
|
1214
|
+
for log_file in log_files[-2:]: # Check last 2 files to catch session changes
|
|
1215
|
+
if os.path.exists(log_file):
|
|
1216
|
+
with open(log_file, 'r', encoding='utf-8') as f:
|
|
1217
|
+
lines = f.readlines()
|
|
1218
|
+
# Check all lines, not just last 5
|
|
1219
|
+
for line in lines:
|
|
1220
|
+
try:
|
|
1221
|
+
entry = json.loads(line.strip())
|
|
1222
|
+
if entry.get("type") == "interaction":
|
|
1223
|
+
timestamp = entry.get("timestamp", "")
|
|
1224
|
+
# Only add if we haven't sent this timestamp before
|
|
1225
|
+
if timestamp and timestamp not in sent_timestamps:
|
|
1226
|
+
new_interactions.append({
|
|
1227
|
+
"type": entry.get("interaction_type", "unknown"),
|
|
1228
|
+
"response": entry.get("response", ""),
|
|
1229
|
+
"duration": entry.get("duration", 0),
|
|
1230
|
+
"timestamp": timestamp
|
|
1231
|
+
})
|
|
1232
|
+
except:
|
|
1233
|
+
continue
|
|
1234
|
+
except Exception as file_e:
|
|
1235
|
+
logger.warning(f"SSE: File reading error: {file_e}")
|
|
1236
|
+
|
|
1237
|
+
# Sort by timestamp to ensure chronological order
|
|
1238
|
+
new_interactions.sort(key=lambda x: x.get("timestamp", ""))
|
|
1239
|
+
|
|
1240
|
+
# Check if there are new interactions
|
|
1241
|
+
if new_interactions:
|
|
1242
|
+
logger.info(f"SSE: Found {len(new_interactions)} new interactions to send")
|
|
1243
|
+
# Send new interactions
|
|
1244
|
+
for interaction in new_interactions:
|
|
1245
|
+
|
|
1246
|
+
event_data = {
|
|
1247
|
+
"step": current_step,
|
|
1248
|
+
"type": interaction.get("type", "unknown"),
|
|
1249
|
+
"response": interaction.get("response", ""),
|
|
1250
|
+
"duration": interaction.get("duration", 0),
|
|
1251
|
+
"timestamp": interaction.get("timestamp", ""),
|
|
1252
|
+
"is_new": True
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
yield f"data: {json.dumps(event_data)}\n\n"
|
|
1256
|
+
# Mark this timestamp as sent
|
|
1257
|
+
sent_timestamps.add(interaction.get("timestamp", ""))
|
|
1258
|
+
|
|
1259
|
+
# Send periodic heartbeat to keep connection alive (every 10 cycles = 5 seconds)
|
|
1260
|
+
elif heartbeat_counter % 10 == 0:
|
|
1261
|
+
yield f"data: {json.dumps({'heartbeat': True, 'timestamp': time.time(), 'step': current_step})}\n\n"
|
|
1262
|
+
|
|
1263
|
+
# Wait before checking again
|
|
1264
|
+
await asyncio.sleep(0.5)
|
|
1265
|
+
|
|
1266
|
+
except Exception as e:
|
|
1267
|
+
logger.error(f"SSE: Error in stream loop: {e}")
|
|
1268
|
+
yield f"data: {json.dumps({'error': str(e), 'timestamp': time.time()})}\n\n"
|
|
1269
|
+
await asyncio.sleep(2)
|
|
1270
|
+
|
|
1271
|
+
except Exception as outer_e:
|
|
1272
|
+
logger.error(f"SSE: Fatal error in event stream: {outer_e}")
|
|
1273
|
+
yield f"data: {json.dumps({'fatal_error': str(outer_e), 'timestamp': time.time()})}\n\n"
|
|
1274
|
+
|
|
1275
|
+
return StreamingResponse(event_stream(), media_type="text/event-stream", headers={"Cache-Control": "no-cache", "Connection": "keep-alive"})
|
|
1276
|
+
|
|
1277
|
+
@app.get("/agent")
|
|
1278
|
+
async def get_agent_thinking():
|
|
1279
|
+
"""Get current agent thinking status and recent LLM interactions"""
|
|
1280
|
+
try:
|
|
1281
|
+
# Get the most recent LLM log file
|
|
1282
|
+
from utils.llm_logger import get_llm_logger
|
|
1283
|
+
|
|
1284
|
+
# Get recent LLM interactions
|
|
1285
|
+
llm_logger = get_llm_logger()
|
|
1286
|
+
session_summary = llm_logger.get_session_summary()
|
|
1287
|
+
|
|
1288
|
+
# Find all LLM log files and get interactions from all of them
|
|
1289
|
+
import glob
|
|
1290
|
+
log_files = glob.glob("llm_logs/llm_log_*.jsonl")
|
|
1291
|
+
logger.info(f"Found {len(log_files)} log files: {log_files}")
|
|
1292
|
+
|
|
1293
|
+
# Get recent interactions from all log files
|
|
1294
|
+
recent_interactions = []
|
|
1295
|
+
for log_file in log_files:
|
|
1296
|
+
if os.path.exists(log_file):
|
|
1297
|
+
try:
|
|
1298
|
+
with open(log_file, 'r', encoding='utf-8') as f:
|
|
1299
|
+
lines = f.readlines()
|
|
1300
|
+
# Get interactions from this file
|
|
1301
|
+
for line in lines:
|
|
1302
|
+
try:
|
|
1303
|
+
entry = json.loads(line.strip())
|
|
1304
|
+
if entry.get("type") == "interaction":
|
|
1305
|
+
recent_interactions.append({
|
|
1306
|
+
"type": entry.get("interaction_type", "unknown"),
|
|
1307
|
+
"prompt": entry.get("prompt", ""),
|
|
1308
|
+
"response": entry.get("response", ""),
|
|
1309
|
+
"duration": entry.get("duration", 0),
|
|
1310
|
+
"timestamp": entry.get("timestamp", "")
|
|
1311
|
+
})
|
|
1312
|
+
except json.JSONDecodeError:
|
|
1313
|
+
continue
|
|
1314
|
+
except Exception as e:
|
|
1315
|
+
logger.error(f"Error reading LLM log {log_file}: {e}")
|
|
1316
|
+
|
|
1317
|
+
# Sort by timestamp and keep only the most recent interaction (current step)
|
|
1318
|
+
recent_interactions.sort(key=lambda x: x.get("timestamp", ""))
|
|
1319
|
+
recent_interactions = recent_interactions[-1:] if recent_interactions else []
|
|
1320
|
+
logger.info(f"Found {len(recent_interactions)} recent interactions (showing current step only)")
|
|
1321
|
+
|
|
1322
|
+
# Format the agent thinking display
|
|
1323
|
+
if recent_interactions:
|
|
1324
|
+
interaction = recent_interactions[-1] # Get the most recent interaction
|
|
1325
|
+
current_thought = f"Current step LLM output:\n"
|
|
1326
|
+
current_thought += f"{interaction['type'].upper()} ({interaction['duration']:.2f}s)\n"
|
|
1327
|
+
current_thought += f"Response: {interaction['response']}"
|
|
1328
|
+
else:
|
|
1329
|
+
current_thought = "No recent LLM interactions. Agent is ready to process game state."
|
|
1330
|
+
|
|
1331
|
+
with step_lock:
|
|
1332
|
+
current_step = agent_step_count # Use agent step count instead of frame step count
|
|
1333
|
+
|
|
1334
|
+
return {
|
|
1335
|
+
"status": "thinking",
|
|
1336
|
+
"current_thought": current_thought,
|
|
1337
|
+
"confidence": 0.85,
|
|
1338
|
+
"timestamp": time.time(),
|
|
1339
|
+
"llm_session": session_summary,
|
|
1340
|
+
"recent_interactions": recent_interactions,
|
|
1341
|
+
"current_step": current_step
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
except Exception as e:
|
|
1345
|
+
logger.error(f"Error in agent thinking: {e}")
|
|
1346
|
+
return {
|
|
1347
|
+
"status": "error",
|
|
1348
|
+
"current_thought": f"Error getting agent thinking: {str(e)}",
|
|
1349
|
+
"confidence": 0.0,
|
|
1350
|
+
"timestamp": time.time()
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
@app.get("/metrics")
|
|
1354
|
+
async def get_metrics():
|
|
1355
|
+
"""Get cumulative metrics for the run"""
|
|
1356
|
+
global latest_metrics
|
|
1357
|
+
|
|
1358
|
+
try:
|
|
1359
|
+
# Return the latest metrics received from client (with thread safety)
|
|
1360
|
+
with step_lock:
|
|
1361
|
+
metrics = latest_metrics.copy()
|
|
1362
|
+
metrics["agent_step_count"] = agent_step_count
|
|
1363
|
+
|
|
1364
|
+
# If metrics haven't been initialized by client yet, try to load from checkpoint
|
|
1365
|
+
# BUT only if checkpoint loading is enabled (not for fresh starts with --load-state)
|
|
1366
|
+
if metrics.get("total_llm_calls", 0) == 0 and checkpoint_loading_enabled:
|
|
1367
|
+
# Check cache folder first, then fall back to old location
|
|
1368
|
+
cache_dir = ".pokeagent_cache"
|
|
1369
|
+
checkpoint_file = os.path.join(cache_dir, "checkpoint_llm.txt") if os.path.exists(cache_dir) else "checkpoint_llm.txt"
|
|
1370
|
+
if not os.path.exists(checkpoint_file) and os.path.exists("checkpoint_llm.txt"):
|
|
1371
|
+
checkpoint_file = "checkpoint_llm.txt"
|
|
1372
|
+
if os.path.exists(checkpoint_file):
|
|
1373
|
+
try:
|
|
1374
|
+
with open(checkpoint_file, 'r', encoding='utf-8') as f:
|
|
1375
|
+
checkpoint_data = json.load(f)
|
|
1376
|
+
if "cumulative_metrics" in checkpoint_data:
|
|
1377
|
+
checkpoint_metrics = checkpoint_data["cumulative_metrics"]
|
|
1378
|
+
metrics.update(checkpoint_metrics)
|
|
1379
|
+
|
|
1380
|
+
# Recalculate total_run_time based on original start_time
|
|
1381
|
+
if "start_time" in checkpoint_metrics:
|
|
1382
|
+
metrics["total_run_time"] = time.time() - checkpoint_metrics["start_time"]
|
|
1383
|
+
|
|
1384
|
+
# Update agent step count from checkpoint
|
|
1385
|
+
if "agent_step_count" in checkpoint_data:
|
|
1386
|
+
metrics["agent_step_count"] = checkpoint_data["agent_step_count"]
|
|
1387
|
+
except:
|
|
1388
|
+
pass
|
|
1389
|
+
|
|
1390
|
+
return metrics
|
|
1391
|
+
|
|
1392
|
+
except Exception as e:
|
|
1393
|
+
logger.error(f"Error getting metrics: {e}")
|
|
1394
|
+
return {
|
|
1395
|
+
"total_tokens": 0,
|
|
1396
|
+
"prompt_tokens": 0,
|
|
1397
|
+
"completion_tokens": 0,
|
|
1398
|
+
"total_cost": 0.0,
|
|
1399
|
+
"total_actions": 0,
|
|
1400
|
+
"total_run_time": 0,
|
|
1401
|
+
"total_llm_calls": 0,
|
|
1402
|
+
"agent_step_count": agent_step_count
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
# Store latest metrics from client
|
|
1406
|
+
latest_metrics = {
|
|
1407
|
+
"total_tokens": 0,
|
|
1408
|
+
"prompt_tokens": 0,
|
|
1409
|
+
"completion_tokens": 0,
|
|
1410
|
+
"total_cost": 0.0,
|
|
1411
|
+
"total_actions": 0,
|
|
1412
|
+
"total_run_time": 0,
|
|
1413
|
+
"total_llm_calls": 0,
|
|
1414
|
+
"start_time": time.time() # Will be overwritten if checkpoint is loaded
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
# Flag to track whether checkpoint loading should be enabled
|
|
1418
|
+
checkpoint_loading_enabled = True # Will be set based on startup args
|
|
1419
|
+
|
|
1420
|
+
@app.post("/reset_metrics")
|
|
1421
|
+
async def reset_metrics():
|
|
1422
|
+
"""Reset all metrics to zero for fresh start"""
|
|
1423
|
+
global latest_metrics, agent_step_count, checkpoint_loading_enabled
|
|
1424
|
+
|
|
1425
|
+
with step_lock:
|
|
1426
|
+
latest_metrics.update({
|
|
1427
|
+
"total_tokens": 0,
|
|
1428
|
+
"prompt_tokens": 0,
|
|
1429
|
+
"completion_tokens": 0,
|
|
1430
|
+
"total_cost": 0.0,
|
|
1431
|
+
"total_actions": 0,
|
|
1432
|
+
"total_run_time": 0,
|
|
1433
|
+
"total_llm_calls": 0,
|
|
1434
|
+
"start_time": time.time()
|
|
1435
|
+
})
|
|
1436
|
+
agent_step_count = 0
|
|
1437
|
+
# Disable checkpoint loading to prevent loading from checkpoint_llm.txt
|
|
1438
|
+
checkpoint_loading_enabled = False
|
|
1439
|
+
|
|
1440
|
+
print("🔄 Server metrics reset for fresh start - checkpoint loading disabled")
|
|
1441
|
+
return {"status": "reset", "timestamp": time.time()}
|
|
1442
|
+
|
|
1443
|
+
@app.post("/agent_step")
|
|
1444
|
+
async def update_agent_step(request: Request = None):
|
|
1445
|
+
"""Update the agent step count and metrics (called by agent.py)"""
|
|
1446
|
+
global agent_step_count, latest_metrics
|
|
1447
|
+
|
|
1448
|
+
try:
|
|
1449
|
+
# Check if this is a direct set operation or has metrics
|
|
1450
|
+
if request:
|
|
1451
|
+
try:
|
|
1452
|
+
request_data = await request.json()
|
|
1453
|
+
|
|
1454
|
+
# Update metrics if provided (with thread safety)
|
|
1455
|
+
if "metrics" in request_data and isinstance(request_data["metrics"], dict):
|
|
1456
|
+
with step_lock: # Use existing lock for thread safety
|
|
1457
|
+
# Safely update each metric individually to avoid race conditions
|
|
1458
|
+
for key, value in request_data["metrics"].items():
|
|
1459
|
+
if key in latest_metrics:
|
|
1460
|
+
# Always protect total_actions as it's managed by server
|
|
1461
|
+
if key == "total_actions":
|
|
1462
|
+
continue
|
|
1463
|
+
else:
|
|
1464
|
+
latest_metrics[key] = value
|
|
1465
|
+
|
|
1466
|
+
# Handle set_step for initialization
|
|
1467
|
+
if "set_step" in request_data:
|
|
1468
|
+
with step_lock:
|
|
1469
|
+
agent_step_count = request_data["set_step"]
|
|
1470
|
+
return {"status": "set", "agent_step": agent_step_count}
|
|
1471
|
+
except Exception as e:
|
|
1472
|
+
logger.error(f"Error processing agent_step request: {e}")
|
|
1473
|
+
# Continue with default increment behavior
|
|
1474
|
+
except Exception as e:
|
|
1475
|
+
logger.error(f"Error in agent_step endpoint: {e}")
|
|
1476
|
+
# Continue with default increment behavior
|
|
1477
|
+
|
|
1478
|
+
# Default increment behavior
|
|
1479
|
+
with step_lock:
|
|
1480
|
+
agent_step_count += 1
|
|
1481
|
+
|
|
1482
|
+
return {"status": "updated", "agent_step": agent_step_count}
|
|
1483
|
+
|
|
1484
|
+
@app.get("/llm_logs")
|
|
1485
|
+
async def get_llm_logs():
|
|
1486
|
+
"""Get recent LLM log entries"""
|
|
1487
|
+
try:
|
|
1488
|
+
from utils.llm_logger import get_llm_logger
|
|
1489
|
+
|
|
1490
|
+
llm_logger = get_llm_logger()
|
|
1491
|
+
session_summary = llm_logger.get_session_summary()
|
|
1492
|
+
|
|
1493
|
+
# Get recent log entries
|
|
1494
|
+
recent_entries = []
|
|
1495
|
+
if os.path.exists(llm_logger.log_file):
|
|
1496
|
+
try:
|
|
1497
|
+
with open(llm_logger.log_file, 'r', encoding='utf-8') as f:
|
|
1498
|
+
lines = f.readlines()
|
|
1499
|
+
# Get the last 20 entries
|
|
1500
|
+
for line in lines[-20:]:
|
|
1501
|
+
try:
|
|
1502
|
+
entry = json.loads(line.strip())
|
|
1503
|
+
recent_entries.append(entry)
|
|
1504
|
+
except json.JSONDecodeError:
|
|
1505
|
+
continue
|
|
1506
|
+
except Exception as e:
|
|
1507
|
+
logger.error(f"Error reading LLM log: {e}")
|
|
1508
|
+
|
|
1509
|
+
return {
|
|
1510
|
+
"session_summary": session_summary,
|
|
1511
|
+
"recent_entries": recent_entries,
|
|
1512
|
+
"log_file": llm_logger.log_file
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1515
|
+
except Exception as e:
|
|
1516
|
+
logger.error(f"Error getting LLM logs: {e}")
|
|
1517
|
+
return {"error": str(e)}
|
|
1518
|
+
|
|
1519
|
+
# Milestone checking is now handled by the emulator
|
|
1520
|
+
|
|
1521
|
+
@app.get("/milestones")
|
|
1522
|
+
async def get_milestones():
|
|
1523
|
+
"""Get current milestones achieved based on persistent tracking"""
|
|
1524
|
+
if env is None:
|
|
1525
|
+
raise HTTPException(status_code=400, detail="Emulator not initialized")
|
|
1526
|
+
|
|
1527
|
+
try:
|
|
1528
|
+
# Get milestones directly from emulator
|
|
1529
|
+
return env.get_milestones()
|
|
1530
|
+
|
|
1531
|
+
except Exception as e:
|
|
1532
|
+
logger.error(f"Error getting milestones: {e}")
|
|
1533
|
+
# Fallback to basic milestones if memory reading fails
|
|
1534
|
+
basic_milestones = [
|
|
1535
|
+
{"id": 1, "name": "GAME_STARTED", "category": "basic", "completed": True, "timestamp": time.time()},
|
|
1536
|
+
{"id": 2, "name": "EMULATOR_RUNNING", "category": "basic", "completed": True, "timestamp": time.time()},
|
|
1537
|
+
]
|
|
1538
|
+
return {
|
|
1539
|
+
"milestones": basic_milestones,
|
|
1540
|
+
"completed": 2,
|
|
1541
|
+
"total": 2,
|
|
1542
|
+
"progress": 1.0,
|
|
1543
|
+
"tracking_system": "fallback",
|
|
1544
|
+
"error": str(e)
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
# Global list to track recent button presses
|
|
1548
|
+
recent_button_presses = []
|
|
1549
|
+
|
|
1550
|
+
@app.get("/recent_actions")
|
|
1551
|
+
async def get_recent_actions():
|
|
1552
|
+
"""Get recently pressed buttons"""
|
|
1553
|
+
global recent_button_presses
|
|
1554
|
+
return {
|
|
1555
|
+
"recent_buttons": recent_button_presses[-10:], # Last 10 button presses
|
|
1556
|
+
"timestamp": time.time()
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1559
|
+
@app.get("/debug/milestones")
|
|
1560
|
+
async def debug_milestones():
|
|
1561
|
+
"""Debug milestone tracking system"""
|
|
1562
|
+
if env is None:
|
|
1563
|
+
raise HTTPException(status_code=400, detail="Emulator not initialized")
|
|
1564
|
+
|
|
1565
|
+
try:
|
|
1566
|
+
# Get current working directory and list milestone files
|
|
1567
|
+
import glob
|
|
1568
|
+
current_dir = os.getcwd()
|
|
1569
|
+
milestone_files = glob.glob("*milestones*.json")
|
|
1570
|
+
|
|
1571
|
+
# Check if default milestone file exists and get its info
|
|
1572
|
+
default_file_info = None
|
|
1573
|
+
if os.path.exists(env.milestone_tracker.filename):
|
|
1574
|
+
try:
|
|
1575
|
+
with open(env.milestone_tracker.filename, 'r') as f:
|
|
1576
|
+
default_data = json.load(f)
|
|
1577
|
+
default_file_info = {
|
|
1578
|
+
"exists": True,
|
|
1579
|
+
"size": os.path.getsize(env.milestone_tracker.filename),
|
|
1580
|
+
"last_modified": time.ctime(os.path.getmtime(env.milestone_tracker.filename)),
|
|
1581
|
+
"milestone_count": len(default_data.get('milestones', {})),
|
|
1582
|
+
"last_updated": default_data.get('last_updated', 'unknown')
|
|
1583
|
+
}
|
|
1584
|
+
except Exception as e:
|
|
1585
|
+
default_file_info = {"exists": True, "error": str(e)}
|
|
1586
|
+
else:
|
|
1587
|
+
default_file_info = {"exists": False}
|
|
1588
|
+
|
|
1589
|
+
return {
|
|
1590
|
+
"tracking_system": "file_based",
|
|
1591
|
+
"current_filename": env.milestone_tracker.filename,
|
|
1592
|
+
"current_milestones": len(env.milestone_tracker.milestones),
|
|
1593
|
+
"completed_milestones": sum(1 for m in env.milestone_tracker.milestones.values() if m.get("completed", False)),
|
|
1594
|
+
"default_file_info": default_file_info,
|
|
1595
|
+
"milestone_files_in_directory": milestone_files,
|
|
1596
|
+
"working_directory": current_dir,
|
|
1597
|
+
"milestone_details": env.milestone_tracker.milestones
|
|
1598
|
+
}
|
|
1599
|
+
except Exception as e:
|
|
1600
|
+
logger.error(f"Error in milestone debug: {e}")
|
|
1601
|
+
return {"error": str(e)}
|
|
1602
|
+
|
|
1603
|
+
@app.post("/debug/reset_milestones")
|
|
1604
|
+
async def reset_milestones():
|
|
1605
|
+
"""Reset all milestones (for testing)"""
|
|
1606
|
+
if env is None:
|
|
1607
|
+
raise HTTPException(status_code=400, detail="Emulator not initialized")
|
|
1608
|
+
|
|
1609
|
+
try:
|
|
1610
|
+
env.milestone_tracker.reset_all()
|
|
1611
|
+
return {
|
|
1612
|
+
"status": "reset",
|
|
1613
|
+
"milestone_file": env.milestone_tracker.filename,
|
|
1614
|
+
"remaining_milestones": len(env.milestone_tracker.milestones)
|
|
1615
|
+
}
|
|
1616
|
+
except Exception as e:
|
|
1617
|
+
logger.error(f"Error resetting milestones: {e}")
|
|
1618
|
+
return {"error": str(e)}
|
|
1619
|
+
|
|
1620
|
+
@app.post("/debug/test_milestone_operations")
|
|
1621
|
+
async def test_milestone_operations():
|
|
1622
|
+
"""Test milestone loading and saving operations"""
|
|
1623
|
+
if env is None:
|
|
1624
|
+
raise HTTPException(status_code=400, detail="Emulator not initialized")
|
|
1625
|
+
|
|
1626
|
+
try:
|
|
1627
|
+
# Test data
|
|
1628
|
+
test_milestones = {
|
|
1629
|
+
"TEST_MILESTONE_1": {
|
|
1630
|
+
"completed": True,
|
|
1631
|
+
"timestamp": time.time(),
|
|
1632
|
+
"first_completed": time.time()
|
|
1633
|
+
},
|
|
1634
|
+
"TEST_MILESTONE_2": {
|
|
1635
|
+
"completed": False,
|
|
1636
|
+
"timestamp": None
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
# Save current state
|
|
1641
|
+
original_milestones = env.milestone_tracker.milestones.copy()
|
|
1642
|
+
original_filename = env.milestone_tracker.filename
|
|
1643
|
+
|
|
1644
|
+
# Test 1: Save milestones with state filename
|
|
1645
|
+
test_state_filename = "test_state_123.sav"
|
|
1646
|
+
env.milestone_tracker.milestones = test_milestones.copy()
|
|
1647
|
+
saved_filename = env.milestone_tracker.save_milestones_for_state(test_state_filename)
|
|
1648
|
+
|
|
1649
|
+
# Test 2: Load milestones for state
|
|
1650
|
+
env.milestone_tracker.milestones = {} # Clear current milestones
|
|
1651
|
+
env.milestone_tracker.load_milestones_for_state(test_state_filename)
|
|
1652
|
+
loaded_milestones = env.milestone_tracker.milestones.copy()
|
|
1653
|
+
|
|
1654
|
+
# Test 3: Check if file was created
|
|
1655
|
+
file_exists = os.path.exists(saved_filename)
|
|
1656
|
+
file_size = os.path.getsize(saved_filename) if file_exists else 0
|
|
1657
|
+
|
|
1658
|
+
# Restore original state
|
|
1659
|
+
env.milestone_tracker.milestones = original_milestones
|
|
1660
|
+
env.milestone_tracker.filename = original_filename
|
|
1661
|
+
|
|
1662
|
+
return {
|
|
1663
|
+
"test_results": {
|
|
1664
|
+
"save_operation": {
|
|
1665
|
+
"filename": saved_filename,
|
|
1666
|
+
"file_exists": file_exists,
|
|
1667
|
+
"file_size": file_size,
|
|
1668
|
+
"milestones_saved": len(test_milestones)
|
|
1669
|
+
},
|
|
1670
|
+
"load_operation": {
|
|
1671
|
+
"milestones_loaded": len(loaded_milestones),
|
|
1672
|
+
"milestones_match": loaded_milestones == test_milestones,
|
|
1673
|
+
"loaded_milestones": loaded_milestones
|
|
1674
|
+
}
|
|
1675
|
+
},
|
|
1676
|
+
"original_state_restored": True
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
except Exception as e:
|
|
1680
|
+
logger.error(f"Error testing milestone operations: {e}")
|
|
1681
|
+
return {"error": str(e)}
|
|
1682
|
+
|
|
1683
|
+
@app.post("/stop")
|
|
1684
|
+
async def stop_server():
|
|
1685
|
+
"""Stop the server"""
|
|
1686
|
+
global running
|
|
1687
|
+
running = False
|
|
1688
|
+
return {"status": "stopping"}
|
|
1689
|
+
|
|
1690
|
+
@app.post("/save_state")
|
|
1691
|
+
async def save_state_endpoint(request: dict):
|
|
1692
|
+
"""Save the current emulator state to a file"""
|
|
1693
|
+
try:
|
|
1694
|
+
os.makedirs(".pokeagent_cache", exist_ok=True)
|
|
1695
|
+
filepath = request.get("filepath", ".pokeagent_cache/manual_save.state")
|
|
1696
|
+
if env:
|
|
1697
|
+
env.save_state(filepath)
|
|
1698
|
+
logger.info(f"💾 State saved to: {filepath}")
|
|
1699
|
+
return {"status": "success", "message": f"State saved to {filepath}"}
|
|
1700
|
+
else:
|
|
1701
|
+
return JSONResponse(status_code=500, content={"error": "Emulator not initialized"})
|
|
1702
|
+
except Exception as e:
|
|
1703
|
+
logger.error(f"Error saving state: {e}")
|
|
1704
|
+
return JSONResponse(status_code=500, content={"error": str(e)})
|
|
1705
|
+
|
|
1706
|
+
@app.post("/load_state")
|
|
1707
|
+
async def load_state_endpoint(request: dict):
|
|
1708
|
+
"""Load an emulator state from a file"""
|
|
1709
|
+
try:
|
|
1710
|
+
os.makedirs(".pokeagent_cache", exist_ok=True)
|
|
1711
|
+
filepath = request.get("filepath", ".pokeagent_cache/manual_save.state")
|
|
1712
|
+
if env:
|
|
1713
|
+
if not os.path.exists(filepath):
|
|
1714
|
+
return JSONResponse(status_code=404, content={"error": f"State file not found: {filepath}"})
|
|
1715
|
+
env.load_state(filepath)
|
|
1716
|
+
logger.info(f"📂 State loaded from: {filepath}")
|
|
1717
|
+
return {"status": "success", "message": f"State loaded from {filepath}"}
|
|
1718
|
+
else:
|
|
1719
|
+
return JSONResponse(status_code=500, content={"error": "Emulator not initialized"})
|
|
1720
|
+
except Exception as e:
|
|
1721
|
+
logger.error(f"Error loading state: {e}")
|
|
1722
|
+
return JSONResponse(status_code=500, content={"error": str(e)})
|
|
1723
|
+
|
|
1724
|
+
@app.post("/checkpoint")
|
|
1725
|
+
async def save_checkpoint(request_data: dict = None):
|
|
1726
|
+
"""Save checkpoint - called by client when step count reaches checkpoint interval"""
|
|
1727
|
+
try:
|
|
1728
|
+
step_count = request_data.get("step_count", 0) if request_data else 0
|
|
1729
|
+
|
|
1730
|
+
# Save emulator state
|
|
1731
|
+
os.makedirs(".pokeagent_cache", exist_ok=True)
|
|
1732
|
+
checkpoint_state = ".pokeagent_cache/checkpoint.state"
|
|
1733
|
+
if env:
|
|
1734
|
+
env.save_state(checkpoint_state)
|
|
1735
|
+
logger.info(f"💾 Server: Saved checkpoint state at step {step_count}")
|
|
1736
|
+
|
|
1737
|
+
# Save milestones
|
|
1738
|
+
if env.milestone_tracker:
|
|
1739
|
+
milestone_file = env.milestone_tracker.save_milestones_for_state(checkpoint_state)
|
|
1740
|
+
logger.info(f"💾 Server: Saved checkpoint milestones")
|
|
1741
|
+
|
|
1742
|
+
return {
|
|
1743
|
+
"status": "checkpoint_saved",
|
|
1744
|
+
"step_count": step_count,
|
|
1745
|
+
"files": {
|
|
1746
|
+
"state": checkpoint_state,
|
|
1747
|
+
"milestones": f".pokeagent_cache/checkpoint_milestones.json",
|
|
1748
|
+
"map": f".pokeagent_cache/checkpoint_grids.json"
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
else:
|
|
1752
|
+
return {"status": "error", "message": "No emulator available"}
|
|
1753
|
+
|
|
1754
|
+
except Exception as e:
|
|
1755
|
+
logger.error(f"Failed to save checkpoint: {e}")
|
|
1756
|
+
return {"status": "error", "message": str(e)}
|
|
1757
|
+
|
|
1758
|
+
@app.post("/sync_llm_metrics")
|
|
1759
|
+
async def sync_llm_metrics(request: Request):
|
|
1760
|
+
"""Sync LLM cumulative metrics from client to server"""
|
|
1761
|
+
try:
|
|
1762
|
+
request_data = await request.json()
|
|
1763
|
+
cumulative_metrics = request_data.get("cumulative_metrics", {})
|
|
1764
|
+
|
|
1765
|
+
if not cumulative_metrics:
|
|
1766
|
+
return {"status": "error", "message": "No metrics provided"}, 400
|
|
1767
|
+
|
|
1768
|
+
# Update server's LLM logger with client's cumulative metrics
|
|
1769
|
+
from utils.llm_logger import get_llm_logger
|
|
1770
|
+
llm_logger = get_llm_logger()
|
|
1771
|
+
if llm_logger is not None:
|
|
1772
|
+
# Update cumulative metrics (but preserve server-managed metrics like start_time and total_actions)
|
|
1773
|
+
server_start_time = llm_logger.cumulative_metrics.get("start_time")
|
|
1774
|
+
server_total_actions = llm_logger.cumulative_metrics.get("total_actions")
|
|
1775
|
+
|
|
1776
|
+
llm_logger.cumulative_metrics.update(cumulative_metrics)
|
|
1777
|
+
|
|
1778
|
+
# Restore server-managed metrics
|
|
1779
|
+
if server_start_time:
|
|
1780
|
+
llm_logger.cumulative_metrics["start_time"] = server_start_time
|
|
1781
|
+
if server_total_actions is not None:
|
|
1782
|
+
llm_logger.cumulative_metrics["total_actions"] = server_total_actions
|
|
1783
|
+
|
|
1784
|
+
# Also sync to latest_metrics for stream.html display (excluding server-managed metrics)
|
|
1785
|
+
global latest_metrics
|
|
1786
|
+
with step_lock:
|
|
1787
|
+
for key, value in cumulative_metrics.items():
|
|
1788
|
+
if key in latest_metrics and key not in ["total_actions", "start_time"]:
|
|
1789
|
+
latest_metrics[key] = value
|
|
1790
|
+
|
|
1791
|
+
logger.info(f"🔄 Synced LLM metrics: {cumulative_metrics.get('total_llm_calls', 0)} calls, {cumulative_metrics.get('total_tokens', 0)} tokens, ${cumulative_metrics.get('total_cost', 0):.6f}")
|
|
1792
|
+
return {"status": "metrics_synced"}
|
|
1793
|
+
else:
|
|
1794
|
+
logger.error("No LLM logger available for sync")
|
|
1795
|
+
return {"status": "error", "message": "No LLM logger available"}, 500
|
|
1796
|
+
except Exception as e:
|
|
1797
|
+
logger.error(f"Error syncing LLM metrics: {e}")
|
|
1798
|
+
return {"status": "error", "message": str(e)}, 500
|
|
1799
|
+
|
|
1800
|
+
@app.post("/save_agent_history")
|
|
1801
|
+
async def save_agent_history():
|
|
1802
|
+
"""Save agent history to checkpoint_llm.txt (called by client after each step)"""
|
|
1803
|
+
try:
|
|
1804
|
+
# Use server-side LLM logger to save checkpoint
|
|
1805
|
+
from utils.llm_logger import get_llm_logger
|
|
1806
|
+
|
|
1807
|
+
llm_logger = get_llm_logger()
|
|
1808
|
+
if llm_logger is not None:
|
|
1809
|
+
# Save checkpoint using current agent step count
|
|
1810
|
+
global agent_step_count
|
|
1811
|
+
# Save to cache folder (llm_logger handles path internally now)
|
|
1812
|
+
llm_logger.save_checkpoint(agent_step_count=agent_step_count)
|
|
1813
|
+
logger.info(f"💾 Saved LLM checkpoint at step {agent_step_count}")
|
|
1814
|
+
return {"status": "agent_history_saved", "step_count": agent_step_count}
|
|
1815
|
+
else:
|
|
1816
|
+
return {"status": "no_logger", "message": "No LLM logger available"}
|
|
1817
|
+
|
|
1818
|
+
except Exception as e:
|
|
1819
|
+
logger.error(f"Failed to save agent history: {e}")
|
|
1820
|
+
return {"status": "error", "message": str(e)}
|
|
1821
|
+
|
|
1822
|
+
@app.post("/load_checkpoint")
|
|
1823
|
+
async def load_checkpoint():
|
|
1824
|
+
"""Load checkpoint state - called by client on startup if --load-checkpoint flag is used"""
|
|
1825
|
+
try:
|
|
1826
|
+
checkpoint_state = ".pokeagent_cache/checkpoint.state"
|
|
1827
|
+
|
|
1828
|
+
if not os.path.exists(checkpoint_state):
|
|
1829
|
+
return {"status": "no_checkpoint", "message": "No .pokeagent_cache/checkpoint.state file found"}
|
|
1830
|
+
|
|
1831
|
+
if env:
|
|
1832
|
+
env.load_state(checkpoint_state)
|
|
1833
|
+
logger.info(f"📂 Server: Loaded checkpoint state")
|
|
1834
|
+
|
|
1835
|
+
# Load milestones if available
|
|
1836
|
+
if env.milestone_tracker:
|
|
1837
|
+
try:
|
|
1838
|
+
env.milestone_tracker.load_milestones_for_state(checkpoint_state)
|
|
1839
|
+
logger.info(f"📂 Server: Loaded checkpoint milestones")
|
|
1840
|
+
except:
|
|
1841
|
+
logger.warning(f"Could not load checkpoint milestones")
|
|
1842
|
+
|
|
1843
|
+
return {
|
|
1844
|
+
"status": "checkpoint_loaded",
|
|
1845
|
+
"files": {
|
|
1846
|
+
"state": checkpoint_state,
|
|
1847
|
+
"milestones": f".pokeagent_cache/checkpoint_milestones.json",
|
|
1848
|
+
"map": f".pokeagent_cache/checkpoint_grids.json"
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
else:
|
|
1852
|
+
return {"status": "error", "message": "No emulator available"}
|
|
1853
|
+
|
|
1854
|
+
except Exception as e:
|
|
1855
|
+
logger.error(f"Failed to load checkpoint: {e}")
|
|
1856
|
+
return {"status": "error", "message": str(e)}
|
|
1857
|
+
|
|
1858
|
+
def main():
|
|
1859
|
+
"""Main function"""
|
|
1860
|
+
import argparse
|
|
1861
|
+
|
|
1862
|
+
global state_update_running, state_update_thread
|
|
1863
|
+
|
|
1864
|
+
# Set up signal handlers
|
|
1865
|
+
signal.signal(signal.SIGINT, signal_handler)
|
|
1866
|
+
signal.signal(signal.SIGTERM, signal_handler)
|
|
1867
|
+
|
|
1868
|
+
parser = argparse.ArgumentParser(description="Simple Pokemon Emerald Server")
|
|
1869
|
+
parser.add_argument("--port", type=int, default=8000, help="Port for FastAPI server")
|
|
1870
|
+
parser.add_argument("--manual", action="store_true", help="Enable manual mode with keyboard input and overlay")
|
|
1871
|
+
parser.add_argument("--load-state", type=str, help="Load a saved state file on startup")
|
|
1872
|
+
parser.add_argument("--record", action="store_true", help="Record video of the gameplay")
|
|
1873
|
+
parser.add_argument("--no-ocr", action="store_true", help="Disable OCR dialogue detection")
|
|
1874
|
+
# Server always runs headless - display handled by client
|
|
1875
|
+
|
|
1876
|
+
args = parser.parse_args()
|
|
1877
|
+
|
|
1878
|
+
# Check for environment variables from multiprocess mode
|
|
1879
|
+
env_load_state = os.environ.get("LOAD_STATE")
|
|
1880
|
+
if env_load_state and not args.load_state:
|
|
1881
|
+
args.load_state = env_load_state
|
|
1882
|
+
print(f"📂 Using load state from environment: {env_load_state}")
|
|
1883
|
+
if env_load_state == ".pokeagent_cache/checkpoint.state":
|
|
1884
|
+
if os.path.exists(".pokeagent_cache/checkpoint.state"):
|
|
1885
|
+
print(f"✅ Server startup: .pokeagent_cache/checkpoint.state file exists")
|
|
1886
|
+
else:
|
|
1887
|
+
print(f"❌ Server startup: .pokeagent_cache/checkpoint.state file MISSING!")
|
|
1888
|
+
|
|
1889
|
+
# Set checkpoint loading flag based on whether this is a true checkpoint load
|
|
1890
|
+
global checkpoint_loading_enabled
|
|
1891
|
+
env_load_checkpoint_mode = os.environ.get("LOAD_CHECKPOINT_MODE")
|
|
1892
|
+
|
|
1893
|
+
if env_load_checkpoint_mode == "true":
|
|
1894
|
+
checkpoint_loading_enabled = True
|
|
1895
|
+
print("🔄 Checkpoint loading enabled - will restore LLM metrics from checkpoint_llm.txt")
|
|
1896
|
+
|
|
1897
|
+
# Initialize LLM logger and load checkpoint immediately during server startup
|
|
1898
|
+
from utils.llm_logger import get_llm_logger
|
|
1899
|
+
llm_logger = get_llm_logger()
|
|
1900
|
+
# Check both cache folder and old location
|
|
1901
|
+
cache_dir = ".pokeagent_cache"
|
|
1902
|
+
checkpoint_file = os.path.join(cache_dir, "checkpoint_llm.txt") if os.path.exists(cache_dir) else "checkpoint_llm.txt"
|
|
1903
|
+
if not os.path.exists(checkpoint_file) and os.path.exists("checkpoint_llm.txt"):
|
|
1904
|
+
checkpoint_file = "checkpoint_llm.txt"
|
|
1905
|
+
|
|
1906
|
+
if llm_logger and os.path.exists(checkpoint_file):
|
|
1907
|
+
restored_step_count = llm_logger.load_checkpoint(checkpoint_file)
|
|
1908
|
+
if restored_step_count is not None:
|
|
1909
|
+
global agent_step_count
|
|
1910
|
+
agent_step_count = restored_step_count
|
|
1911
|
+
print(f"✅ Server startup: restored LLM checkpoint with step count {restored_step_count}")
|
|
1912
|
+
|
|
1913
|
+
# Sync latest_metrics with loaded cumulative metrics
|
|
1914
|
+
global latest_metrics
|
|
1915
|
+
latest_metrics.update(llm_logger.cumulative_metrics)
|
|
1916
|
+
print(f"✅ Server startup: synced metrics - actions: {latest_metrics.get('total_actions', 0)}, cost: {latest_metrics.get('total_cost', 0)}")
|
|
1917
|
+
else:
|
|
1918
|
+
print("❌ Server startup: failed to load LLM checkpoint")
|
|
1919
|
+
else:
|
|
1920
|
+
print("ℹ️ Server startup: no checkpoint_llm.txt file found")
|
|
1921
|
+
elif env_load_checkpoint_mode == "false":
|
|
1922
|
+
checkpoint_loading_enabled = False
|
|
1923
|
+
print("✨ Fresh start mode - will NOT load LLM metrics from checkpoint_llm.txt")
|
|
1924
|
+
else:
|
|
1925
|
+
# Default behavior: allow checkpoint loading unless explicitly disabled
|
|
1926
|
+
checkpoint_loading_enabled = True
|
|
1927
|
+
print("🔄 Checkpoint loading enabled by default - will restore LLM metrics from checkpoint_llm.txt if available")
|
|
1928
|
+
|
|
1929
|
+
print("Starting Fixed Simple Pokemon Emerald Server")
|
|
1930
|
+
# Initialize video recording if requested
|
|
1931
|
+
init_video_recording(args.record)
|
|
1932
|
+
print("Server mode - headless operation, display handled by client")
|
|
1933
|
+
if args.no_ocr:
|
|
1934
|
+
print("OCR dialogue detection disabled")
|
|
1935
|
+
print("Press Ctrl+C to stop")
|
|
1936
|
+
|
|
1937
|
+
# Initialize emulator
|
|
1938
|
+
# Skip initial state reading if we're going to load a state
|
|
1939
|
+
if not setup_environment(skip_initial_state=(args.load_state is not None)):
|
|
1940
|
+
print("Failed to initialize emulator")
|
|
1941
|
+
return
|
|
1942
|
+
|
|
1943
|
+
# Disable dialogue detection if --no-ocr flag is set
|
|
1944
|
+
if args.no_ocr:
|
|
1945
|
+
if env and env.memory_reader:
|
|
1946
|
+
env.memory_reader._dialog_detection_enabled = False
|
|
1947
|
+
print("🚫 All dialogue detection disabled (--no-ocr flag)")
|
|
1948
|
+
|
|
1949
|
+
# Load state if specified
|
|
1950
|
+
if args.load_state:
|
|
1951
|
+
try:
|
|
1952
|
+
env.load_state(args.load_state)
|
|
1953
|
+
print(f"Loaded state from: {args.load_state}")
|
|
1954
|
+
|
|
1955
|
+
# Milestones and map data are automatically loaded by env.load_state()
|
|
1956
|
+
# Check what was loaded
|
|
1957
|
+
state_dir = os.path.dirname(args.load_state)
|
|
1958
|
+
base_name = os.path.splitext(os.path.basename(args.load_state))[0]
|
|
1959
|
+
|
|
1960
|
+
milestone_file = os.path.join(state_dir, f"{base_name}_milestones.json")
|
|
1961
|
+
if os.path.exists(milestone_file):
|
|
1962
|
+
print(f"📂 Loaded milestones from: {milestone_file}")
|
|
1963
|
+
|
|
1964
|
+
grids_file = os.path.join(state_dir, f"{base_name}_grids.json")
|
|
1965
|
+
if os.path.exists(grids_file):
|
|
1966
|
+
print(f"🗺️ Loaded map grids from: {grids_file}")
|
|
1967
|
+
|
|
1968
|
+
# Map buffer should already be found by emulator.load_state()
|
|
1969
|
+
if env.memory_reader and env.memory_reader._map_buffer_addr:
|
|
1970
|
+
print(f"Map buffer already initialized at 0x{env.memory_reader._map_buffer_addr:08X}")
|
|
1971
|
+
|
|
1972
|
+
# Now log the initial GAME_RUNNING milestone after state is loaded
|
|
1973
|
+
try:
|
|
1974
|
+
env.milestone_tracker.mark_completed("GAME_RUNNING")
|
|
1975
|
+
initial_state = env.get_comprehensive_state()
|
|
1976
|
+
|
|
1977
|
+
import hashlib
|
|
1978
|
+
state_str = str(initial_state)
|
|
1979
|
+
state_hash = hashlib.md5(state_str.encode()).hexdigest()[:8]
|
|
1980
|
+
|
|
1981
|
+
anticheat_tracker.log_submission_data(
|
|
1982
|
+
step=0,
|
|
1983
|
+
state_data=initial_state,
|
|
1984
|
+
action_taken="INIT",
|
|
1985
|
+
decision_time=0.0,
|
|
1986
|
+
state_hash=state_hash,
|
|
1987
|
+
manual_mode=True,
|
|
1988
|
+
milestone_override="GAME_RUNNING"
|
|
1989
|
+
)
|
|
1990
|
+
print("Initial GAME_RUNNING milestone logged after state load")
|
|
1991
|
+
|
|
1992
|
+
# Trigger a map stitcher update to ensure visual map is ready
|
|
1993
|
+
try:
|
|
1994
|
+
if env.memory_reader and env.memory_reader._map_stitcher:
|
|
1995
|
+
# Check if map stitcher is empty and collect initial map data if needed
|
|
1996
|
+
map_areas = env.memory_reader._map_stitcher.map_areas
|
|
1997
|
+
if not map_areas:
|
|
1998
|
+
print("🗺️ Map stitcher is empty, collecting initial map data...")
|
|
1999
|
+
# Collect initial map data
|
|
2000
|
+
tiles = env.memory_reader.read_map_around_player(radius=7)
|
|
2001
|
+
if tiles:
|
|
2002
|
+
print(f"🗺️ Collected {len(tiles)} tiles, updating map stitcher")
|
|
2003
|
+
# Create minimal state for stitcher update
|
|
2004
|
+
initial_state = {"map": {}}
|
|
2005
|
+
env.memory_reader._update_map_stitcher(tiles, initial_state)
|
|
2006
|
+
print("✅ Initial map data collection completed")
|
|
2007
|
+
else:
|
|
2008
|
+
print("❌ Could not collect initial map data")
|
|
2009
|
+
|
|
2010
|
+
# Get current state for map stitcher update
|
|
2011
|
+
current_state = env.get_comprehensive_state()
|
|
2012
|
+
# The map stitcher should now have data
|
|
2013
|
+
# This just ensures the visual_map is generated
|
|
2014
|
+
print("Ensuring map stitcher visual data is ready after state load")
|
|
2015
|
+
except Exception as e:
|
|
2016
|
+
print(f"Note: Could not update map stitcher after state load: {e}")
|
|
2017
|
+
except Exception as e:
|
|
2018
|
+
print(f"Warning: Could not log initial milestone: {e}")
|
|
2019
|
+
except Exception as e:
|
|
2020
|
+
print(f"Failed to load state from {args.load_state}: {e}")
|
|
2021
|
+
print("Continuing with fresh game state...")
|
|
2022
|
+
|
|
2023
|
+
# Start lightweight milestone updater thread
|
|
2024
|
+
state_update_running = True
|
|
2025
|
+
state_update_thread = threading.Thread(target=periodic_milestone_updater, daemon=True)
|
|
2026
|
+
state_update_thread.start()
|
|
2027
|
+
|
|
2028
|
+
# Start FastAPI server in background thread
|
|
2029
|
+
server_thread = threading.Thread(target=run_fastapi_server, args=(args.port,), daemon=True)
|
|
2030
|
+
server_thread.start()
|
|
2031
|
+
|
|
2032
|
+
# Get local IP for network access
|
|
2033
|
+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
2034
|
+
from utils.get_local_ip import get_local_ip
|
|
2035
|
+
local_ip = get_local_ip()
|
|
2036
|
+
|
|
2037
|
+
print(f"🌐 FastAPI server running:")
|
|
2038
|
+
print(f" Local: http://localhost:{args.port}")
|
|
2039
|
+
print(f" Network: http://{local_ip}:{args.port}")
|
|
2040
|
+
print(f"📺 Stream interface: http://{local_ip}:{args.port}/stream")
|
|
2041
|
+
print("Available endpoints:")
|
|
2042
|
+
print(" /status - Server status")
|
|
2043
|
+
print(" /screenshot - Current screenshot")
|
|
2044
|
+
print(" /action - Take action (POST)")
|
|
2045
|
+
print(" /state - Comprehensive game state (visual + memory data)")
|
|
2046
|
+
print(" /agent - Agent thinking status")
|
|
2047
|
+
print(" /milestones - Current milestones achieved")
|
|
2048
|
+
print(" /recent_actions - Recently pressed buttons")
|
|
2049
|
+
print(" /debug/memory - Debug memory reading (basic)")
|
|
2050
|
+
print(" /debug/memory/comprehensive - Comprehensive memory diagnostics")
|
|
2051
|
+
print(" /debug/milestones - Debug milestone tracking system")
|
|
2052
|
+
print(" /debug/reset_milestones - Reset all milestones (POST)")
|
|
2053
|
+
print(" /debug/test_milestone_operations - Test milestone save/load (POST)")
|
|
2054
|
+
print(" /stop - Stop server")
|
|
2055
|
+
|
|
2056
|
+
try:
|
|
2057
|
+
# Run headless game loop in main thread
|
|
2058
|
+
game_loop(manual_mode=False) # Server always runs in server mode
|
|
2059
|
+
except KeyboardInterrupt:
|
|
2060
|
+
print("Interrupted by user")
|
|
2061
|
+
except Exception as e:
|
|
2062
|
+
print(f"Error: {e}")
|
|
2063
|
+
finally:
|
|
2064
|
+
# Cleanup
|
|
2065
|
+
global running
|
|
2066
|
+
running = False
|
|
2067
|
+
state_update_running = False
|
|
2068
|
+
if env:
|
|
2069
|
+
env.stop()
|
|
2070
|
+
print("Server stopped")
|
|
2071
|
+
|
|
2072
|
+
# Initialize emulator when imported for multiprocess mode
|
|
2073
|
+
def init_for_multiprocess():
|
|
2074
|
+
"""Initialize emulator when server is imported for multiprocess mode"""
|
|
2075
|
+
global env
|
|
2076
|
+
|
|
2077
|
+
if env is None: # Only initialize once
|
|
2078
|
+
# Check for environment variables set by agent.py multiprocess mode
|
|
2079
|
+
rom_path = os.environ.get("ROM_PATH", "Emerald-GBAdvance/rom.gba")
|
|
2080
|
+
load_state = os.environ.get("LOAD_STATE")
|
|
2081
|
+
record_video = os.environ.get("RECORD_VIDEO") == "1"
|
|
2082
|
+
no_ocr = os.environ.get("NO_OCR") == "1"
|
|
2083
|
+
|
|
2084
|
+
print(f"🔧 Initializing server for multiprocess mode...")
|
|
2085
|
+
print(f" ROM: {rom_path}")
|
|
2086
|
+
if load_state:
|
|
2087
|
+
print(f" Load state: {load_state}")
|
|
2088
|
+
|
|
2089
|
+
# Initialize emulator
|
|
2090
|
+
try:
|
|
2091
|
+
if not os.path.exists(rom_path):
|
|
2092
|
+
raise RuntimeError(f"ROM not found at {rom_path}")
|
|
2093
|
+
|
|
2094
|
+
env = EmeraldEmulator(rom_path=rom_path)
|
|
2095
|
+
env.initialize()
|
|
2096
|
+
|
|
2097
|
+
# Initialize video recording if requested
|
|
2098
|
+
init_video_recording(record_video)
|
|
2099
|
+
|
|
2100
|
+
# Disable OCR if requested
|
|
2101
|
+
if no_ocr and env and env.memory_reader:
|
|
2102
|
+
env.memory_reader._dialog_detection_enabled = False
|
|
2103
|
+
print("🚫 All dialogue detection disabled (--no-ocr flag)")
|
|
2104
|
+
|
|
2105
|
+
# Load state if specified
|
|
2106
|
+
if load_state:
|
|
2107
|
+
try:
|
|
2108
|
+
print(f"🔄 Attempting to load state from: {load_state}")
|
|
2109
|
+
env.load_state(load_state)
|
|
2110
|
+
print(f"📂 Successfully loaded state from: {load_state}")
|
|
2111
|
+
|
|
2112
|
+
# Milestones and map data are automatically loaded by env.load_state()
|
|
2113
|
+
# Check what was loaded
|
|
2114
|
+
state_dir = os.path.dirname(load_state)
|
|
2115
|
+
base_name = os.path.splitext(os.path.basename(load_state))[0]
|
|
2116
|
+
|
|
2117
|
+
milestone_file = os.path.join(state_dir, f"{base_name}_milestones.json")
|
|
2118
|
+
if os.path.exists(milestone_file):
|
|
2119
|
+
print(f"📋 Loaded milestones from: {milestone_file}")
|
|
2120
|
+
|
|
2121
|
+
grids_file = os.path.join(state_dir, f"{base_name}_grids.json")
|
|
2122
|
+
if os.path.exists(grids_file):
|
|
2123
|
+
print(f"🗺️ Loaded map grids from: {grids_file}")
|
|
2124
|
+
|
|
2125
|
+
# Map buffer should already be found by emulator.load_state()
|
|
2126
|
+
if env.memory_reader and env.memory_reader._map_buffer_addr:
|
|
2127
|
+
print(f"📍 Map buffer initialized at 0x{env.memory_reader._map_buffer_addr:08X}")
|
|
2128
|
+
|
|
2129
|
+
print(f"✅ State loading complete for {load_state}")
|
|
2130
|
+
|
|
2131
|
+
except Exception as e:
|
|
2132
|
+
print(f"❌ Failed to load state from {load_state}: {e}")
|
|
2133
|
+
print(" Continuing with fresh game state...")
|
|
2134
|
+
|
|
2135
|
+
# Start lightweight milestone updater thread
|
|
2136
|
+
global state_update_running, state_update_thread
|
|
2137
|
+
state_update_running = True
|
|
2138
|
+
state_update_thread = threading.Thread(target=periodic_milestone_updater, daemon=True)
|
|
2139
|
+
state_update_thread.start()
|
|
2140
|
+
|
|
2141
|
+
print("✅ Server initialized successfully for multiprocess mode")
|
|
2142
|
+
|
|
2143
|
+
except Exception as e:
|
|
2144
|
+
print(f"❌ Failed to initialize server for multiprocess mode: {e}")
|
|
2145
|
+
raise
|
|
2146
|
+
|
|
2147
|
+
# Auto-initialize when imported for multiprocess mode (when ROM_PATH env var is set)
|
|
2148
|
+
if os.environ.get("ROM_PATH") and __name__ != "__main__":
|
|
2149
|
+
init_for_multiprocess()
|
|
2150
|
+
|
|
2151
|
+
if __name__ == "__main__":
|
|
2152
|
+
main()
|