arkai2025 commited on
Commit
85cbecd
·
1 Parent(s): c7829ce

refactor(config): centralize scenario configuration defaults

Browse files
Files changed (7) hide show
  1. README.md +0 -13
  2. app.py +36 -14
  3. config.py +45 -0
  4. fire_rescue_mcp/mcp_server.py +10 -5
  5. models.py +11 -6
  6. service.py +7 -3
  7. simulation.py +15 -10
README.md CHANGED
@@ -165,19 +165,6 @@ Prompts for every stage live in `prompts.yaml`, making it easy to retune instruc
165
  - [uv](https://github.com/astral-sh/uv) (optional but fastest)
166
  - HuggingFace API token with access to the OpenAI-compatible inference endpoint (set as `HF_TOKEN`)
167
 
168
- ### Installation
169
-
170
- ```bash
171
- # Clone
172
- git clone https://github.com/ArkaiAriza/fire-rescue-mcp.git
173
- cd fire-rescue-mcp
174
-
175
- # Install deps (recommended)
176
- uv sync
177
- # or fall back to pip
178
- pip install -e .
179
- ```
180
-
181
  ### Environment Variables
182
 
183
  ```bash
 
165
  - [uv](https://github.com/astral-sh/uv) (optional but fastest)
166
  - HuggingFace API token with access to the OpenAI-compatible inference endpoint (set as `HF_TOKEN`)
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  ### Environment Variables
169
 
170
  ```bash
app.py CHANGED
@@ -11,6 +11,7 @@ import uuid
11
  import gradio as gr
12
  from typing import Optional
13
 
 
14
  from service import (
15
  ADVISOR_MODEL_CHOICES,
16
  DEFAULT_ADVISOR_MODEL_CHOICE,
@@ -800,7 +801,7 @@ def deploy_at_cell(
800
  _get_combined_advisor_messages(service),
801
  service.get_event_log_text(),
802
  render_status_html(state, is_thinking, thinking_stage),
803
- ] + updates
804
 
805
  # Handle fire placement
806
  if selection == "🔥 Fire":
@@ -890,14 +891,19 @@ def refresh_display(
890
  thinking_stage = service.get_thinking_stage()
891
 
892
  # Freeze background UI once a win/lose overlay has already been shown
 
 
893
  overlay_state = changes.get("result_state", "")
894
- freeze_background = bool(overlay_state)
895
- if freeze_background:
896
- if display_cache["result_freeze_state"] != overlay_state:
897
- display_cache["result_freeze_state"] = overlay_state
898
- elif display_cache["result_freeze_state"]:
 
899
  display_cache["result_freeze_state"] = ""
900
 
 
 
901
  # Timer control - stop when game ends
902
  timer_update = gr.update()
903
  if status in ["success", "fail"]:
@@ -1847,29 +1853,45 @@ def create_app() -> gr.Blocks:
1847
  with gr.Accordion("⚙️ Settings & Controls", open=False):
1848
  with gr.Row():
1849
  with gr.Column(scale=1):
 
1850
  fire_count = gr.Slider(
1851
- minimum=1, maximum=40, value=20, step=1,
 
 
 
1852
  label="🔥 Initial Fire Count",
1853
- info="Number of fire starting points (1-25)"
1854
  )
1855
  with gr.Column(scale=1):
 
1856
  fire_intensity = gr.Slider(
1857
- minimum=0.2, maximum=0.9, value=0.6, step=0.05,
 
 
 
1858
  label="🌡️ Fire Intensity",
1859
- info="Initial fire strength (0.2-0.9)"
1860
  )
1861
  with gr.Row():
1862
  with gr.Column(scale=1):
 
1863
  building_count = gr.Slider(
1864
- minimum=1, maximum=35, value=20, step=1,
 
 
 
1865
  label="🏢 Building Count",
1866
  info="Number of buildings (connected cluster)"
1867
  )
1868
  with gr.Column(scale=1):
 
1869
  max_units = gr.Slider(
1870
- minimum=1, maximum=20, value=10, step=1,
 
 
 
1871
  label="🚒 Max Units",
1872
- info="Maximum deployable units (1-20)"
1873
  )
1874
  with gr.Row():
1875
  with gr.Column(scale=1):
@@ -2044,7 +2066,7 @@ def create_app() -> gr.Blocks:
2044
  # Event handlers for grid buttons (click to place)
2045
  for x, y, btn in grid_buttons:
2046
  btn.click(
2047
- fn=lambda sel, _x=x, _y=y, svc=None, cache=None: deploy_at_cell(_x, _y, sel, svc, cache),
2048
  inputs=[place_selector, service_state, display_cache_state],
2049
  outputs=[result_popup, advisor_display, event_log_display, status_display] + all_buttons + [service_state, display_cache_state],
2050
  )
 
11
  import gradio as gr
12
  from typing import Optional
13
 
14
+ from config import SCENARIO_DEFAULTS
15
  from service import (
16
  ADVISOR_MODEL_CHOICES,
17
  DEFAULT_ADVISOR_MODEL_CHOICE,
 
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":
 
891
  thinking_stage = service.get_thinking_stage()
892
 
893
  # Freeze background UI once a win/lose overlay has already been shown
894
+ # but still allow the final tick (when the overlay first appears) to push
895
+ # its grid updates so the user sees the true end state.
896
  overlay_state = changes.get("result_state", "")
897
+ cached_overlay_state = display_cache["result_freeze_state"]
898
+ overlay_first_tick = bool(overlay_state) and not cached_overlay_state
899
+
900
+ if overlay_state:
901
+ display_cache["result_freeze_state"] = overlay_state
902
+ elif cached_overlay_state:
903
  display_cache["result_freeze_state"] = ""
904
 
905
+ freeze_background = bool(display_cache["result_freeze_state"]) and not overlay_first_tick
906
+
907
  # Timer control - stop when game ends
908
  timer_update = gr.update()
909
  if status in ["success", "fail"]:
 
1853
  with gr.Accordion("⚙️ Settings & Controls", open=False):
1854
  with gr.Row():
1855
  with gr.Column(scale=1):
1856
+ fire_count_defaults = SCENARIO_DEFAULTS["fire_count"]
1857
  fire_count = gr.Slider(
1858
+ minimum=fire_count_defaults.minimum,
1859
+ maximum=fire_count_defaults.maximum,
1860
+ value=fire_count_defaults.value,
1861
+ step=fire_count_defaults.step,
1862
  label="🔥 Initial Fire Count",
1863
+ info="Number of fire starting points"
1864
  )
1865
  with gr.Column(scale=1):
1866
+ fire_intensity_defaults = SCENARIO_DEFAULTS["fire_intensity"]
1867
  fire_intensity = gr.Slider(
1868
+ minimum=fire_intensity_defaults.minimum,
1869
+ maximum=fire_intensity_defaults.maximum,
1870
+ value=fire_intensity_defaults.value,
1871
+ step=fire_intensity_defaults.step,
1872
  label="🌡️ Fire Intensity",
1873
+ info="Initial fire strength"
1874
  )
1875
  with gr.Row():
1876
  with gr.Column(scale=1):
1877
+ building_count_defaults = SCENARIO_DEFAULTS["building_count"]
1878
  building_count = gr.Slider(
1879
+ minimum=building_count_defaults.minimum,
1880
+ maximum=building_count_defaults.maximum,
1881
+ value=building_count_defaults.value,
1882
+ step=building_count_defaults.step,
1883
  label="🏢 Building Count",
1884
  info="Number of buildings (connected cluster)"
1885
  )
1886
  with gr.Column(scale=1):
1887
+ max_units_defaults = SCENARIO_DEFAULTS["max_units"]
1888
  max_units = gr.Slider(
1889
+ minimum=max_units_defaults.minimum,
1890
+ maximum=max_units_defaults.maximum,
1891
+ value=max_units_defaults.value,
1892
+ step=max_units_defaults.step,
1893
  label="🚒 Max Units",
1894
+ info="Maximum deployable units"
1895
  )
1896
  with gr.Row():
1897
  with gr.Column(scale=1):
 
2066
  # Event handlers for grid buttons (click to place)
2067
  for x, y, btn in grid_buttons:
2068
  btn.click(
2069
+ fn=lambda sel, svc, cache, _x=x, _y=y: deploy_at_cell(_x, _y, sel, svc, cache),
2070
  inputs=[place_selector, service_state, display_cache_state],
2071
  outputs=[result_popup, advisor_display, event_log_display, status_display] + all_buttons + [service_state, display_cache_state],
2072
  )
config.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Centralized configuration defaults for scenario controls."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Final
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class SliderDefaults:
11
+ """Slider configuration along with a helper to clamp values."""
12
+
13
+ minimum: float
14
+ maximum: float
15
+ value: float
16
+ step: float
17
+
18
+ def clamp(self, candidate: float) -> float:
19
+ """Clamp ``candidate`` to this slider's min/max bounds."""
20
+ return max(self.minimum, min(self.maximum, candidate))
21
+
22
+
23
+ SCENARIO_DEFAULTS: Final[dict[str, SliderDefaults]] = {
24
+ "fire_count": SliderDefaults(minimum=1, maximum=40, value=20, step=1),
25
+ "fire_intensity": SliderDefaults(minimum=0.2, maximum=0.9, value=0.6, step=0.05),
26
+ "building_count": SliderDefaults(minimum=1, maximum=35, value=20, step=1),
27
+ "max_units": SliderDefaults(minimum=1, maximum=20, value=10, step=1),
28
+ }
29
+
30
+
31
+ def range_text(key: str) -> str:
32
+ """
33
+ Return a human-readable ``min-max`` string for any configured scenario slider.
34
+ Primarily used in docstrings / README notes to avoid diverging documentation.
35
+ """
36
+ defaults = SCENARIO_DEFAULTS[key]
37
+ step_value = float(defaults.step)
38
+ if step_value.is_integer():
39
+ min_val = int(defaults.minimum)
40
+ max_val = int(defaults.maximum)
41
+ else:
42
+ min_val = defaults.minimum
43
+ max_val = defaults.maximum
44
+ return f"{min_val}-{max_val}"
45
+
fire_rescue_mcp/mcp_server.py CHANGED
@@ -28,6 +28,7 @@ from mcp.server.fastmcp import FastMCP
28
  # Add parent directory to path for imports
29
  sys.path.insert(0, str(Path(__file__).parent.parent))
30
 
 
31
  from models import CellType, UnitType
32
  from simulation import SimulationEngine, SimulationConfig
33
 
@@ -49,6 +50,10 @@ def attach_engine(engine: SimulationEngine, session_id: str) -> None:
49
  # Unit effective ranges (matching SimulationConfig, Chebyshev/square distance)
50
  FIRE_TRUCK_RANGE = 1 # Square coverage radius (includes 8 neighbors)
51
  HELICOPTER_RANGE = 2 # Square coverage radius (extends two cells in all directions)
 
 
 
 
52
 
53
 
54
  def detach_engine(session_id: str) -> None:
@@ -175,15 +180,15 @@ def reset_scenario(
175
  max_units: int = 10,
176
  session_id: Optional[str] = None,
177
  ) -> dict:
178
- """
179
  Reset and initialize a new fire rescue simulation scenario.
180
 
181
  Args:
182
  seed: Random seed for reproducibility (optional)
183
- fire_count: Number of initial fire points (1-25, default: 10)
184
- fire_intensity: Initial fire intensity (0.2-0.9, default: 0.5)
185
- building_count: Number of buildings to place (1-35, default: 20)
186
- max_units: Maximum deployable units (1-20, default: 10)
187
 
188
  Returns:
189
  Status, summary, and emoji map of the initial state
 
28
  # Add parent directory to path for imports
29
  sys.path.insert(0, str(Path(__file__).parent.parent))
30
 
31
+ from config import range_text
32
  from models import CellType, UnitType
33
  from simulation import SimulationEngine, SimulationConfig
34
 
 
50
  # Unit effective ranges (matching SimulationConfig, Chebyshev/square distance)
51
  FIRE_TRUCK_RANGE = 1 # Square coverage radius (includes 8 neighbors)
52
  HELICOPTER_RANGE = 2 # Square coverage radius (extends two cells in all directions)
53
+ FIRE_COUNT_RANGE_TEXT = range_text("fire_count")
54
+ FIRE_INTENSITY_RANGE_TEXT = range_text("fire_intensity")
55
+ BUILDING_COUNT_RANGE_TEXT = range_text("building_count")
56
+ MAX_UNITS_RANGE_TEXT = range_text("max_units")
57
 
58
 
59
  def detach_engine(session_id: str) -> None:
 
180
  max_units: int = 10,
181
  session_id: Optional[str] = None,
182
  ) -> dict:
183
+ f"""
184
  Reset and initialize a new fire rescue simulation scenario.
185
 
186
  Args:
187
  seed: Random seed for reproducibility (optional)
188
+ fire_count: Number of initial fire points ({FIRE_COUNT_RANGE_TEXT}, default: 10)
189
+ fire_intensity: Initial fire intensity ({FIRE_INTENSITY_RANGE_TEXT}, default: 0.5)
190
+ building_count: Number of buildings to place ({BUILDING_COUNT_RANGE_TEXT}, default: 20)
191
+ max_units: Maximum deployable units ({MAX_UNITS_RANGE_TEXT}, default: 10)
192
 
193
  Returns:
194
  Status, summary, and emoji map of the initial state
models.py CHANGED
@@ -9,6 +9,11 @@ from enum import Enum
9
  from typing import Optional
10
  import random
11
 
 
 
 
 
 
12
 
13
  class UnitType(str, Enum):
14
  """Types of firefighting units available."""
@@ -146,23 +151,23 @@ class WorldState:
146
  fire_intensity: float = 0.6,
147
  building_count: int = 16
148
  ):
149
- """
150
  Initialize the grid with terrain and initial fires.
151
 
152
  Args:
153
  seed: Random seed for reproducibility
154
- fire_count: Number of initial fire points (1-25)
155
  fire_intensity: Initial fire intensity (0.0-1.0)
156
- building_count: Number of buildings to place (1-25)
157
  """
158
  if seed is not None:
159
  random.seed(seed)
160
  self.seed = seed
161
 
162
- # Clamp values to valid ranges
163
- fire_count = max(1, min(25, fire_count))
164
  fire_intensity = max(0.0, min(1.0, fire_intensity))
165
- building_count = max(1, min(25, building_count))
166
 
167
  # Generate random building positions (connected cluster)
168
  self.building_positions = self._generate_building_positions(building_count)
 
9
  from typing import Optional
10
  import random
11
 
12
+ from config import SCENARIO_DEFAULTS
13
+
14
+ FIRE_COUNT_DEFAULTS = SCENARIO_DEFAULTS["fire_count"]
15
+ BUILDING_COUNT_DEFAULTS = SCENARIO_DEFAULTS["building_count"]
16
+
17
 
18
  class UnitType(str, Enum):
19
  """Types of firefighting units available."""
 
151
  fire_intensity: float = 0.6,
152
  building_count: int = 16
153
  ):
154
+ f"""
155
  Initialize the grid with terrain and initial fires.
156
 
157
  Args:
158
  seed: Random seed for reproducibility
159
+ fire_count: Number of initial fire points ({int(FIRE_COUNT_DEFAULTS.minimum)}-{int(FIRE_COUNT_DEFAULTS.maximum)})
160
  fire_intensity: Initial fire intensity (0.0-1.0)
161
+ building_count: Number of buildings to place ({int(BUILDING_COUNT_DEFAULTS.minimum)}-{int(BUILDING_COUNT_DEFAULTS.maximum)})
162
  """
163
  if seed is not None:
164
  random.seed(seed)
165
  self.seed = seed
166
 
167
+ # Clamp values using shared configuration
168
+ fire_count = int(FIRE_COUNT_DEFAULTS.clamp(fire_count))
169
  fire_intensity = max(0.0, min(1.0, fire_intensity))
170
+ building_count = int(BUILDING_COUNT_DEFAULTS.clamp(building_count))
171
 
172
  # Generate random building positions (connected cluster)
173
  self.building_positions = self._generate_building_positions(building_count)
service.py CHANGED
@@ -24,11 +24,15 @@ from agent import (
24
  PlanResult,
25
  CycleSummary,
26
  )
 
27
  from fire_rescue_mcp.mcp_client import LocalFastMCPClient
28
  from fire_rescue_mcp.mcp_server import attach_engine, detach_engine, mcp as fastmcp_server
29
  from models import SimulationStatus, CellType
30
  from simulation import SimulationEngine
31
 
 
 
 
32
 
33
  ADVISOR_MODEL_CHOICES = {
34
  "GPT-OSS · HuggingFace (openai/gpt-oss-120b)": {
@@ -256,14 +260,14 @@ class SimulationService:
256
  session_id: Optional[str] = None,
257
  on_update: Optional[Callable] = None
258
  ) -> dict:
259
- """
260
  Start a new simulation.
261
 
262
  Args:
263
  seed: Random seed for reproducibility
264
- fire_count: Number of initial fire points (1-25)
265
  fire_intensity: Initial fire intensity (0.0-1.0)
266
- building_count: Number of buildings to place (1-25)
267
  on_update: Callback function called on state changes
268
 
269
  Returns:
 
24
  PlanResult,
25
  CycleSummary,
26
  )
27
+ from config import range_text
28
  from fire_rescue_mcp.mcp_client import LocalFastMCPClient
29
  from fire_rescue_mcp.mcp_server import attach_engine, detach_engine, mcp as fastmcp_server
30
  from models import SimulationStatus, CellType
31
  from simulation import SimulationEngine
32
 
33
+ FIRE_COUNT_RANGE_TEXT = range_text("fire_count")
34
+ BUILDING_COUNT_RANGE_TEXT = range_text("building_count")
35
+
36
 
37
  ADVISOR_MODEL_CHOICES = {
38
  "GPT-OSS · HuggingFace (openai/gpt-oss-120b)": {
 
260
  session_id: Optional[str] = None,
261
  on_update: Optional[Callable] = None
262
  ) -> dict:
263
+ f"""
264
  Start a new simulation.
265
 
266
  Args:
267
  seed: Random seed for reproducibility
268
+ fire_count: Number of initial fire points ({FIRE_COUNT_RANGE_TEXT})
269
  fire_intensity: Initial fire intensity (0.0-1.0)
270
+ building_count: Number of buildings to place ({BUILDING_COUNT_RANGE_TEXT})
271
  on_update: Callback function called on state changes
272
 
273
  Returns:
simulation.py CHANGED
@@ -7,16 +7,21 @@ Handles fire spread, unit behavior, and win/lose conditions.
7
  import random
8
  from typing import Optional
9
 
 
10
  from models import (
11
- WorldState,
12
- Cell,
13
- CellType,
14
- Unit,
15
- UnitType,
16
  SimulationStatus,
17
- Event
18
  )
19
 
 
 
 
 
20
 
21
  class SimulationConfig:
22
  """Configuration parameters for the simulation."""
@@ -64,15 +69,15 @@ class SimulationEngine:
64
  building_count: int = 16,
65
  max_units: int = 10
66
  ) -> WorldState:
67
- """
68
  Reset and initialize a new simulation.
69
 
70
  Args:
71
  seed: Random seed for reproducibility
72
- fire_count: Number of initial fire points (1-25)
73
  fire_intensity: Initial fire intensity (0.0-1.0)
74
- building_count: Number of buildings to place (1-25)
75
- max_units: Maximum number of deployable units (1-20)
76
  """
77
  self.world = WorldState(
78
  width=self.config.GRID_WIDTH,
 
7
  import random
8
  from typing import Optional
9
 
10
+ from config import range_text
11
  from models import (
12
+ WorldState,
13
+ Cell,
14
+ CellType,
15
+ Unit,
16
+ UnitType,
17
  SimulationStatus,
18
+ Event,
19
  )
20
 
21
+ FIRE_COUNT_RANGE_TEXT = range_text("fire_count")
22
+ BUILDING_COUNT_RANGE_TEXT = range_text("building_count")
23
+ MAX_UNITS_RANGE_TEXT = range_text("max_units")
24
+
25
 
26
  class SimulationConfig:
27
  """Configuration parameters for the simulation."""
 
69
  building_count: int = 16,
70
  max_units: int = 10
71
  ) -> WorldState:
72
+ f"""
73
  Reset and initialize a new simulation.
74
 
75
  Args:
76
  seed: Random seed for reproducibility
77
+ fire_count: Number of initial fire points ({FIRE_COUNT_RANGE_TEXT})
78
  fire_intensity: Initial fire intensity (0.0-1.0)
79
+ building_count: Number of buildings to place ({BUILDING_COUNT_RANGE_TEXT})
80
+ max_units: Maximum number of deployable units ({MAX_UNITS_RANGE_TEXT})
81
  """
82
  self.world = WorldState(
83
  width=self.config.GRID_WIDTH,