refactor(app): extract helper functions for manual action updates
Browse files
README.md
CHANGED
|
@@ -10,6 +10,7 @@ pinned: true
|
|
| 10 |
license: mit
|
| 11 |
short_description: Multi-stage MCP-driven advisor fights fires autonomously
|
| 12 |
tags:
|
|
|
|
| 13 |
- mcp-in-action-track-creative
|
| 14 |
- agent
|
| 15 |
- mcp
|
|
@@ -188,6 +189,9 @@ uv run python app.py
|
|
| 188 |
|
| 189 |
# Or plain Python
|
| 190 |
python -m app
|
|
|
|
|
|
|
|
|
|
| 191 |
```
|
| 192 |
|
| 193 |
Visit http://localhost:7860 to start simulating.
|
|
|
|
| 10 |
license: mit
|
| 11 |
short_description: Multi-stage MCP-driven advisor fights fires autonomously
|
| 12 |
tags:
|
| 13 |
+
- mcp-1st-birthday
|
| 14 |
- mcp-in-action-track-creative
|
| 15 |
- agent
|
| 16 |
- mcp
|
|
|
|
| 189 |
|
| 190 |
# Or plain Python
|
| 191 |
python -m app
|
| 192 |
+
|
| 193 |
+
# Or use the helper restart script for local runs
|
| 194 |
+
./restart.sh
|
| 195 |
```
|
| 196 |
|
| 197 |
Visit http://localhost:7860 to start simulating.
|
app.py
CHANGED
|
@@ -775,6 +775,44 @@ def prepare_reset(
|
|
| 775 |
return None, None
|
| 776 |
|
| 777 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 778 |
def deploy_at_cell(
|
| 779 |
x: int,
|
| 780 |
y: int,
|
|
@@ -784,24 +822,12 @@ def deploy_at_cell(
|
|
| 784 |
):
|
| 785 |
"""Deploy unit or fire at specific cell, or remove if unit already exists there."""
|
| 786 |
service = get_or_create_service(service)
|
| 787 |
-
|
| 788 |
-
display_cache = _create_display_cache()
|
| 789 |
-
|
| 790 |
-
# Get thinking state
|
| 791 |
-
is_thinking = service.is_thinking()
|
| 792 |
-
thinking_stage = service.get_thinking_stage()
|
| 793 |
|
| 794 |
# Only allow deployment when simulation is actively running (not paused)
|
| 795 |
if not service.is_running():
|
| 796 |
gr.Warning("โ ๏ธ Please start the simulation first!")
|
| 797 |
-
|
| 798 |
-
updates = get_all_button_updates(state)
|
| 799 |
-
return [
|
| 800 |
-
render_game_result(state.get("status", ""), state.get("after_action_report")),
|
| 801 |
-
_get_combined_advisor_messages(service),
|
| 802 |
-
service.get_event_log_text(),
|
| 803 |
-
render_status_html(state, is_thinking, thinking_stage),
|
| 804 |
-
] + updates + [service, display_cache]
|
| 805 |
|
| 806 |
# Handle fire placement
|
| 807 |
if selection == "๐ฅ Fire":
|
|
@@ -823,19 +849,96 @@ def deploy_at_cell(
|
|
| 823 |
error_msg = result.get("message", "Unknown error")
|
| 824 |
gr.Warning(f"โ ๏ธ {error_msg}")
|
| 825 |
|
| 826 |
-
|
| 827 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 828 |
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
|
| 832 |
|
| 833 |
-
|
| 834 |
-
|
| 835 |
-
|
| 836 |
-
|
| 837 |
-
|
| 838 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 839 |
|
| 840 |
|
| 841 |
def poll_after_action_report(service: Optional[SimulationService]):
|
|
@@ -2021,12 +2124,28 @@ def create_app() -> gr.Blocks:
|
|
| 2021 |
)
|
| 2022 |
grid_buttons.append((x, y, btn))
|
| 2023 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2024 |
# Timers: main simulation refresh + after-action poller
|
| 2025 |
timer = gr.Timer(value=1.0, active=False)
|
| 2026 |
report_timer = gr.Timer(value=1.0, active=False)
|
| 2027 |
|
| 2028 |
# Collect all button outputs for updates
|
| 2029 |
all_buttons = [btn for (_, _, btn) in grid_buttons]
|
|
|
|
| 2030 |
|
| 2031 |
# Event handlers for simulation controls
|
| 2032 |
start_btn.click(
|
|
@@ -2111,6 +2230,38 @@ def create_app() -> gr.Blocks:
|
|
| 2111 |
inputs=[model_selector, service_state],
|
| 2112 |
outputs=[service_state],
|
| 2113 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2114 |
app.load(
|
| 2115 |
fn=_initialize_session_defaults,
|
| 2116 |
inputs=[service_state, display_cache_state],
|
|
|
|
| 775 |
return None, None
|
| 776 |
|
| 777 |
|
| 778 |
+
def _ensure_display_cache_initialized(display_cache: Optional[dict]) -> dict:
|
| 779 |
+
"""Guarantee we always have a mutable display cache."""
|
| 780 |
+
return display_cache if display_cache is not None else _create_display_cache()
|
| 781 |
+
|
| 782 |
+
|
| 783 |
+
def _render_manual_action_update(service: SimulationService, display_cache: dict):
|
| 784 |
+
"""Return the standard UI updates after a manual grid action."""
|
| 785 |
+
state = service.get_state()
|
| 786 |
+
updates = get_all_button_updates(state)
|
| 787 |
+
is_thinking = service.is_thinking()
|
| 788 |
+
thinking_stage = service.get_thinking_stage()
|
| 789 |
+
return [
|
| 790 |
+
render_game_result(state.get("status", ""), state.get("after_action_report")),
|
| 791 |
+
_get_combined_advisor_messages(service),
|
| 792 |
+
service.get_event_log_text(),
|
| 793 |
+
render_status_html(state, is_thinking, thinking_stage),
|
| 794 |
+
] + updates + [service, display_cache]
|
| 795 |
+
|
| 796 |
+
|
| 797 |
+
def _normalize_manual_selection(selection: str) -> str:
|
| 798 |
+
"""Map flexible text inputs to the radio labels used by the UI."""
|
| 799 |
+
if not selection:
|
| 800 |
+
return "๐ Truck"
|
| 801 |
+
normalized = selection.strip().lower()
|
| 802 |
+
mappings = {
|
| 803 |
+
"truck": "๐ Truck",
|
| 804 |
+
"fire_truck": "๐ Truck",
|
| 805 |
+
"firetruck": "๐ Truck",
|
| 806 |
+
"๐": "๐ Truck",
|
| 807 |
+
"heli": "๐ Heli",
|
| 808 |
+
"helicopter": "๐ Heli",
|
| 809 |
+
"๐": "๐ Heli",
|
| 810 |
+
"fire": "๐ฅ Fire",
|
| 811 |
+
"๐ฅ": "๐ฅ Fire",
|
| 812 |
+
}
|
| 813 |
+
return mappings.get(normalized, selection if selection in ["๐ Truck", "๐ Heli", "๐ฅ Fire"] else "๐ Truck")
|
| 814 |
+
|
| 815 |
+
|
| 816 |
def deploy_at_cell(
|
| 817 |
x: int,
|
| 818 |
y: int,
|
|
|
|
| 822 |
):
|
| 823 |
"""Deploy unit or fire at specific cell, or remove if unit already exists there."""
|
| 824 |
service = get_or_create_service(service)
|
| 825 |
+
display_cache = _ensure_display_cache_initialized(display_cache)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 826 |
|
| 827 |
# Only allow deployment when simulation is actively running (not paused)
|
| 828 |
if not service.is_running():
|
| 829 |
gr.Warning("โ ๏ธ Please start the simulation first!")
|
| 830 |
+
return _render_manual_action_update(service, display_cache)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 831 |
|
| 832 |
# Handle fire placement
|
| 833 |
if selection == "๐ฅ Fire":
|
|
|
|
| 849 |
error_msg = result.get("message", "Unknown error")
|
| 850 |
gr.Warning(f"โ ๏ธ {error_msg}")
|
| 851 |
|
| 852 |
+
return _render_manual_action_update(service, display_cache)
|
| 853 |
+
|
| 854 |
+
|
| 855 |
+
def handle_map_deploy(
|
| 856 |
+
selection: str,
|
| 857 |
+
x: int,
|
| 858 |
+
y: int,
|
| 859 |
+
service: Optional[SimulationService],
|
| 860 |
+
display_cache: Optional[dict],
|
| 861 |
+
):
|
| 862 |
+
"""Public API handler to deploy units/fires at a coordinate."""
|
| 863 |
+
normalized = _normalize_manual_selection(selection)
|
| 864 |
+
return deploy_at_cell(
|
| 865 |
+
int(x),
|
| 866 |
+
int(y),
|
| 867 |
+
normalized,
|
| 868 |
+
service,
|
| 869 |
+
display_cache,
|
| 870 |
+
)
|
| 871 |
+
|
| 872 |
+
|
| 873 |
+
def handle_map_remove(
|
| 874 |
+
x: int,
|
| 875 |
+
y: int,
|
| 876 |
+
service: Optional[SimulationService],
|
| 877 |
+
display_cache: Optional[dict],
|
| 878 |
+
):
|
| 879 |
+
"""Public API handler to remove a unit at coordinates."""
|
| 880 |
+
service = get_or_create_service(service)
|
| 881 |
+
display_cache = _ensure_display_cache_initialized(display_cache)
|
| 882 |
|
| 883 |
+
if not service.is_running():
|
| 884 |
+
gr.Warning("โ ๏ธ Please start the simulation first!")
|
| 885 |
+
return _render_manual_action_update(service, display_cache)
|
| 886 |
|
| 887 |
+
x = int(x)
|
| 888 |
+
y = int(y)
|
| 889 |
+
|
| 890 |
+
if not service.has_unit_at(x, y):
|
| 891 |
+
gr.Warning("โ ๏ธ No unit found at the requested coordinates.")
|
| 892 |
+
return _render_manual_action_update(service, display_cache)
|
| 893 |
+
|
| 894 |
+
result = service.remove_unit(x, y)
|
| 895 |
+
if result.get("status") != "ok":
|
| 896 |
+
error_msg = result.get("message", "Unknown error")
|
| 897 |
+
gr.Warning(f"โ ๏ธ {error_msg}")
|
| 898 |
+
|
| 899 |
+
return _render_manual_action_update(service, display_cache)
|
| 900 |
+
|
| 901 |
+
|
| 902 |
+
def handle_map_move(
|
| 903 |
+
source_x: int,
|
| 904 |
+
source_y: int,
|
| 905 |
+
target_x: int,
|
| 906 |
+
target_y: int,
|
| 907 |
+
service: Optional[SimulationService],
|
| 908 |
+
display_cache: Optional[dict],
|
| 909 |
+
):
|
| 910 |
+
"""Public API handler to move a unit between coordinates."""
|
| 911 |
+
service = get_or_create_service(service)
|
| 912 |
+
display_cache = _ensure_display_cache_initialized(display_cache)
|
| 913 |
+
|
| 914 |
+
if not service.is_running():
|
| 915 |
+
gr.Warning("โ ๏ธ Please start the simulation first!")
|
| 916 |
+
return _render_manual_action_update(service, display_cache)
|
| 917 |
+
|
| 918 |
+
sx, sy = int(source_x), int(source_y)
|
| 919 |
+
tx, ty = int(target_x), int(target_y)
|
| 920 |
+
|
| 921 |
+
if not service.has_unit_at(sx, sy):
|
| 922 |
+
gr.Warning("โ ๏ธ No unit found at the source coordinates.")
|
| 923 |
+
return _render_manual_action_update(service, display_cache)
|
| 924 |
+
|
| 925 |
+
removal = service.remove_unit(sx, sy)
|
| 926 |
+
if removal.get("status") != "ok":
|
| 927 |
+
error_msg = removal.get("message", "Failed to remove unit before moving.")
|
| 928 |
+
gr.Warning(f"โ ๏ธ {error_msg}")
|
| 929 |
+
return _render_manual_action_update(service, display_cache)
|
| 930 |
+
|
| 931 |
+
removed_unit = removal.get("unit") or {}
|
| 932 |
+
unit_type = removed_unit.get("type") or removal.get("removed_unit_type") or "fire_truck"
|
| 933 |
+
|
| 934 |
+
deploy_result = service.deploy_unit(unit_type, tx, ty, "player_move")
|
| 935 |
+
if deploy_result.get("status") != "ok":
|
| 936 |
+
# Attempt to restore the original placement
|
| 937 |
+
service.deploy_unit(unit_type, sx, sy, "player_move_rollback")
|
| 938 |
+
error_msg = deploy_result.get("message", "Failed to deploy unit at target location.")
|
| 939 |
+
gr.Warning(f"โ ๏ธ {error_msg}")
|
| 940 |
+
|
| 941 |
+
return _render_manual_action_update(service, display_cache)
|
| 942 |
|
| 943 |
|
| 944 |
def poll_after_action_report(service: Optional[SimulationService]):
|
|
|
|
| 2124 |
)
|
| 2125 |
grid_buttons.append((x, y, btn))
|
| 2126 |
|
| 2127 |
+
# Hidden components that only exist to expose MCP-friendly APIs (kept out of UI)
|
| 2128 |
+
with gr.Column(visible=False):
|
| 2129 |
+
manual_deploy_selection = gr.Textbox(label="deploy_selection", value="๐ Truck")
|
| 2130 |
+
manual_deploy_x = gr.Number(label="deploy_x", value=0, precision=0)
|
| 2131 |
+
manual_deploy_y = gr.Number(label="deploy_y", value=0, precision=0)
|
| 2132 |
+
manual_move_source_x = gr.Number(label="move_source_x", value=0, precision=0)
|
| 2133 |
+
manual_move_source_y = gr.Number(label="move_source_y", value=0, precision=0)
|
| 2134 |
+
manual_move_target_x = gr.Number(label="move_target_x", value=0, precision=0)
|
| 2135 |
+
manual_move_target_y = gr.Number(label="move_target_y", value=0, precision=0)
|
| 2136 |
+
manual_remove_x = gr.Number(label="remove_x", value=0, precision=0)
|
| 2137 |
+
manual_remove_y = gr.Number(label="remove_y", value=0, precision=0)
|
| 2138 |
+
manual_deploy_trigger = gr.Button("manual_deploy_trigger", visible=False)
|
| 2139 |
+
manual_move_trigger = gr.Button("manual_move_trigger", visible=False)
|
| 2140 |
+
manual_remove_trigger = gr.Button("manual_remove_trigger", visible=False)
|
| 2141 |
+
|
| 2142 |
# Timers: main simulation refresh + after-action poller
|
| 2143 |
timer = gr.Timer(value=1.0, active=False)
|
| 2144 |
report_timer = gr.Timer(value=1.0, active=False)
|
| 2145 |
|
| 2146 |
# Collect all button outputs for updates
|
| 2147 |
all_buttons = [btn for (_, _, btn) in grid_buttons]
|
| 2148 |
+
manual_action_outputs = [result_popup, advisor_display, event_log_display, status_display] + all_buttons + [service_state, display_cache_state]
|
| 2149 |
|
| 2150 |
# Event handlers for simulation controls
|
| 2151 |
start_btn.click(
|
|
|
|
| 2230 |
inputs=[model_selector, service_state],
|
| 2231 |
outputs=[service_state],
|
| 2232 |
)
|
| 2233 |
+
|
| 2234 |
+
# Public MCP endpoints for manual deploy/move/remove without exposing all grid buttons
|
| 2235 |
+
manual_deploy_trigger.click(
|
| 2236 |
+
fn=handle_map_deploy,
|
| 2237 |
+
inputs=[manual_deploy_selection, manual_deploy_x, manual_deploy_y, service_state, display_cache_state],
|
| 2238 |
+
outputs=manual_action_outputs,
|
| 2239 |
+
api_name="deploy_unit_manual",
|
| 2240 |
+
api_visibility="public",
|
| 2241 |
+
)
|
| 2242 |
+
|
| 2243 |
+
manual_move_trigger.click(
|
| 2244 |
+
fn=handle_map_move,
|
| 2245 |
+
inputs=[
|
| 2246 |
+
manual_move_source_x,
|
| 2247 |
+
manual_move_source_y,
|
| 2248 |
+
manual_move_target_x,
|
| 2249 |
+
manual_move_target_y,
|
| 2250 |
+
service_state,
|
| 2251 |
+
display_cache_state,
|
| 2252 |
+
],
|
| 2253 |
+
outputs=manual_action_outputs,
|
| 2254 |
+
api_name="move_unit_manual",
|
| 2255 |
+
api_visibility="public",
|
| 2256 |
+
)
|
| 2257 |
+
|
| 2258 |
+
manual_remove_trigger.click(
|
| 2259 |
+
fn=handle_map_remove,
|
| 2260 |
+
inputs=[manual_remove_x, manual_remove_y, service_state, display_cache_state],
|
| 2261 |
+
outputs=manual_action_outputs,
|
| 2262 |
+
api_name="remove_unit_manual",
|
| 2263 |
+
api_visibility="public",
|
| 2264 |
+
)
|
| 2265 |
app.load(
|
| 2266 |
fn=_initialize_session_defaults,
|
| 2267 |
inputs=[service_state, display_cache_state],
|