burtenshaw HF Staff commited on
Commit
cbe0084
·
verified ·
1 Parent(s): 9e17744

Upload folder using huggingface_hub

Browse files
src/envs/wildfire_env/README.md CHANGED
@@ -62,6 +62,8 @@ This makes WildfireEnv a **fast, controllable**, and **open benchmark** for appl
62
  ./run_wildfire_docker.sh
63
  ```
64
 
 
 
65
  Or manually:
66
 
67
  ```bash
@@ -786,34 +788,85 @@ env.close()
786
 
787
  ## 🌐 Web Interface
788
 
789
- The wildfire environment includes a **built-in web interface** for interactive exploration.
790
 
791
  ### Accessing the Web Interface
792
 
793
- 1. **Start the server** (Docker or local)
794
- 2. **Open browser** to: `http://localhost:8000/web`
795
- 3. **Interact** with the environment visually
796
 
797
- ### Web Interface Features
 
 
 
798
 
799
- - **Action form** - Dynamic form to select action type and enter coordinates
800
- - **State observer** - View current observation and state (displayed as JSON)
801
- - **Action history** - Log of all actions taken with timestamps
802
- - **Reset button** - Start new episode
803
- - **WebSocket updates** - Real-time state updates via WebSocket connection
804
- - **Instructions panel** - Environment documentation and usage instructions
805
 
806
- **Note:** The grid is displayed as JSON data. For visual grid rendering, use the matplotlib examples in the [Examples](#-examples) section.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
807
 
808
  ### Using the Web Interface
809
 
810
- 1. Click **"Reset Environment"** to start
811
- 2. Fill in action form:
812
- - Select action: `water`, `break`, or `wait`
813
- - Enter coordinates (x, y) for water/break actions
814
- 3. Click **"Submit Action"**
815
- 4. Observe the grid update and rewards
816
- 5. Monitor resources (water, breaks) in the state panel
 
 
 
 
 
 
 
817
 
818
  ---
819
 
 
62
  ./run_wildfire_docker.sh
63
  ```
64
 
65
+ **Note:** The web interface can be enabled with `ENABLE_WEB_INTERFACE=true`. Access it at `http://localhost:8000/web` when enabled.
66
+
67
  Or manually:
68
 
69
  ```bash
 
788
 
789
  ## 🌐 Web Interface
790
 
791
+ The Wildfire Environment includes a **custom web interface** with visual grid display and wildfire-specific features.
792
 
793
  ### Accessing the Web Interface
794
 
795
+ #### Using Docker
 
 
796
 
797
+ ```bash
798
+ # From the OpenEnv root directory
799
+ ./run_wildfire_docker.sh
800
+ ```
801
 
802
+ Then open: `http://localhost:8000/web`
 
 
 
 
 
803
 
804
+ #### Local Testing (No Docker)
805
+
806
+ ```bash
807
+ # Enable web interface with flag
808
+ ENABLE_WEB_INTERFACE=true PYTHONPATH=src uvicorn src.envs.wildfire_env.server.app:app --reload --host 0.0.0.0 --port 8000
809
+ ```
810
+
811
+ ### Web Interface Features
812
+
813
+ #### Left Pane: Action Interface
814
+ - **Wildfire-specific action form**
815
+ - Action dropdown: Water (Extinguish Fire), Break (Create Firebreak), Wait (Do Nothing)
816
+ - Coordinate inputs (X, Y) - auto-populated when clicking grid cells
817
+ - Coordinates show/hide based on action type
818
+ - **Environment stats display**
819
+ - Step count
820
+ - Water remaining
821
+ - Breaks remaining
822
+ - Burning cells count
823
+ - **Current state display**
824
+ - Status (Reset/Running)
825
+ - Episode ID
826
+ - Wind direction
827
+ - Humidity
828
+ - **Control buttons**
829
+ - Reset Environment
830
+ - Get State
831
+
832
+ #### Right Pane: Visual Grid & Logs
833
+ - **Visual 2D Grid Display** 🔥
834
+ - 16×16 grid rendered as color-coded cells
835
+ - **Color coding:**
836
+ - 🟩 **Green** = Fuel (safe, value 1)
837
+ - 🔥 **Orange/Red** = Burning (fire, value 2)
838
+ - ⬛ **Dark Gray** = Ash (burned, value 0)
839
+ - 🟫 **Brown** = Firebreak (value 3)
840
+ - 🟦 **Blue** = Watered/Damp (value 4)
841
+ - **Interactive:** Click cells to set coordinates for water/break actions
842
+ - **Auto-updates:** Grid refreshes automatically via WebSocket
843
+ - **Legend**
844
+ - Color-coded legend explaining all cell types
845
+ - **Action history**
846
+ - Log of all actions with timestamps
847
+ - Shows action, observation, reward, and done status
848
+
849
+ #### Additional Features
850
+ - **WebSocket connection** - Real-time state updates without page refresh
851
+ - **Instructions panel** - Collapsible environment documentation
852
+ - **Grid status indicator** - Shows grid dimensions and cell count
853
 
854
  ### Using the Web Interface
855
 
856
+ 1. **Start the server** (see above)
857
+ 2. **Open browser** to: `http://localhost:8000/web`
858
+ 3. **Click "Reset Environment"** to initialize and display the grid
859
+ 4. **Interact with the grid:**
860
+ - Click on a cell to set coordinates for water/break actions
861
+ - Or manually enter X, Y coordinates
862
+ 5. **Select action:**
863
+ - Choose `water`, `break`, or `wait` from the dropdown
864
+ 6. **Click "Execute Action"**
865
+ 7. **Watch the grid update in real-time:**
866
+ - Fire spreads automatically
867
+ - Cells change color based on state
868
+ - Stats update automatically
869
+ 8. **Monitor resources** in the stats panel (water, breaks, burning count)
870
 
871
  ---
872
 
src/envs/wildfire_env/server/app.py CHANGED
@@ -1,10 +1,70 @@
1
  # server/app.py
2
  import os
3
- from core.env_server import create_app
 
 
 
 
4
  from ..models import WildfireAction, WildfireObservation
5
  from .wildfire_environment import WildfireEnvironment
 
 
6
 
7
  W = int(os.getenv("WILDFIRE_WIDTH", "16"))
8
  H = int(os.getenv("WILDFIRE_HEIGHT", "16"))
9
  env = WildfireEnvironment(width=W, height=H)
10
- app = create_app(env, WildfireAction, WildfireObservation, env_name='wildfire_env')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # server/app.py
2
  import os
3
+ from fastapi.responses import HTMLResponse
4
+ from fastapi import WebSocket, WebSocketDisconnect
5
+ from core.env_server import create_fastapi_app
6
+ from core.env_server.web_interface import load_environment_metadata, WebInterfaceManager
7
+ from core.env_server.types import Action, Observation
8
  from ..models import WildfireAction, WildfireObservation
9
  from .wildfire_environment import WildfireEnvironment
10
+ from .wildfire_web_interface import get_wildfire_web_interface_html
11
+ from dataclasses import asdict
12
 
13
  W = int(os.getenv("WILDFIRE_WIDTH", "16"))
14
  H = int(os.getenv("WILDFIRE_HEIGHT", "16"))
15
  env = WildfireEnvironment(width=W, height=H)
16
+
17
+ # Create base app without web interface
18
+ app = create_fastapi_app(env, WildfireAction, WildfireObservation)
19
+
20
+ # Check if web interface should be enabled
21
+ # This can be controlled via environment variable
22
+ enable_web = (
23
+ os.getenv("ENABLE_WEB_INTERFACE", "false").lower() in ("true", "1", "yes")
24
+ )
25
+
26
+ if enable_web:
27
+ # Load environment metadata
28
+ metadata = load_environment_metadata(env, 'wildfire_env')
29
+
30
+ # Create web interface manager (needed for /web/reset, /web/step, /ws endpoints)
31
+ web_manager = WebInterfaceManager(env, WildfireAction, WildfireObservation, metadata)
32
+
33
+ # Add our custom wildfire interface route
34
+ @app.get("/web", response_class=HTMLResponse)
35
+ async def wildfire_web_interface():
36
+ """Custom wildfire-specific web interface."""
37
+ return get_wildfire_web_interface_html(metadata)
38
+
39
+ # Add web interface endpoints (these are needed for the interface to work)
40
+ @app.get("/web/metadata")
41
+ async def web_metadata():
42
+ """Get environment metadata."""
43
+ return asdict(metadata)
44
+
45
+ @app.websocket("/ws")
46
+ async def websocket_endpoint(websocket: WebSocket):
47
+ """WebSocket endpoint for real-time updates."""
48
+ await web_manager.connect_websocket(websocket)
49
+ try:
50
+ while True:
51
+ # Keep connection alive
52
+ await websocket.receive_text()
53
+ except WebSocketDisconnect:
54
+ await web_manager.disconnect_websocket(websocket)
55
+
56
+ @app.post("/web/reset")
57
+ async def web_reset():
58
+ """Reset endpoint for web interface."""
59
+ return await web_manager.reset_environment()
60
+
61
+ @app.post("/web/step")
62
+ async def web_step(request: dict):
63
+ """Step endpoint for web interface."""
64
+ action_data = request.get("action", {})
65
+ return await web_manager.step_environment(action_data)
66
+
67
+ @app.get("/web/state")
68
+ async def web_state():
69
+ """State endpoint for web interface."""
70
+ return web_manager.get_state()
src/envs/wildfire_env/server/wildfire_web_interface.py ADDED
@@ -0,0 +1,983 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Custom web interface for Wildfire Environment.
3
+
4
+ This module provides a wildfire-specific web interface with visual grid display
5
+ and wildfire-specific features, without modifying the base web_interface.py.
6
+ """
7
+
8
+ from typing import Optional
9
+ from dataclasses import asdict
10
+ from core.env_server.types import EnvironmentMetadata
11
+ from ..models import WildfireAction
12
+
13
+
14
+ def get_wildfire_web_interface_html(metadata: Optional[EnvironmentMetadata] = None) -> str:
15
+ """Generate custom HTML for the wildfire environment web interface."""
16
+
17
+ # Convert markdown to HTML for instructions
18
+ instructions_html = ""
19
+ if metadata and metadata.readme_content:
20
+ instructions_html = _markdown_to_html_simple(metadata.readme_content)
21
+
22
+ return f"""
23
+ <!DOCTYPE html>
24
+ <html lang="en">
25
+ <head>
26
+ <meta charset="UTF-8">
27
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
28
+ <title>Wildfire Environment - Web Interface</title>
29
+ <style>
30
+ * {{
31
+ margin: 0;
32
+ padding: 0;
33
+ box-sizing: border-box;
34
+ }}
35
+
36
+ body {{
37
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
38
+ background-color: #f5f5f5;
39
+ height: 100vh;
40
+ overflow: hidden;
41
+ }}
42
+
43
+ .container {{
44
+ display: flex;
45
+ height: 100vh;
46
+ }}
47
+
48
+ .left-pane {{
49
+ width: 50%;
50
+ background: white;
51
+ border-right: 1px solid #e0e0e0;
52
+ display: flex;
53
+ flex-direction: column;
54
+ }}
55
+
56
+ .right-pane {{
57
+ width: 50%;
58
+ background: #fafafa;
59
+ display: flex;
60
+ flex-direction: column;
61
+ }}
62
+
63
+ .pane-header {{
64
+ padding: 20px;
65
+ border-bottom: 1px solid #e0e0e0;
66
+ background: #f8f9fa;
67
+ font-weight: 600;
68
+ font-size: 16px;
69
+ }}
70
+
71
+ .pane-content {{
72
+ flex: 1;
73
+ padding: 20px;
74
+ overflow-y: auto;
75
+ }}
76
+
77
+ /* Action Form Styles */
78
+ .action-form {{
79
+ background: white;
80
+ border: 1px solid #e0e0e0;
81
+ border-radius: 8px;
82
+ padding: 20px;
83
+ margin-bottom: 20px;
84
+ }}
85
+
86
+ .form-group {{
87
+ margin-bottom: 15px;
88
+ }}
89
+
90
+ .form-group label {{
91
+ display: block;
92
+ margin-bottom: 5px;
93
+ font-weight: 500;
94
+ color: #333;
95
+ }}
96
+
97
+ .form-group select, .form-group input {{
98
+ width: 100%;
99
+ padding: 8px 12px;
100
+ border: 1px solid #ddd;
101
+ border-radius: 4px;
102
+ font-size: 14px;
103
+ }}
104
+
105
+ .form-group select:focus, .form-group input:focus {{
106
+ outline: none;
107
+ border-color: #007bff;
108
+ box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
109
+ }}
110
+
111
+ /* Buttons */
112
+ .btn {{
113
+ background: #007bff;
114
+ color: white;
115
+ border: none;
116
+ padding: 10px 20px;
117
+ border-radius: 4px;
118
+ cursor: pointer;
119
+ font-size: 14px;
120
+ margin-right: 10px;
121
+ margin-bottom: 10px;
122
+ }}
123
+
124
+ .btn:hover {{
125
+ background: #0056b3;
126
+ }}
127
+
128
+ .btn:disabled {{
129
+ background: #6c757d;
130
+ cursor: not-allowed;
131
+ }}
132
+
133
+ .btn-secondary {{
134
+ background: #6c757d;
135
+ }}
136
+
137
+ .btn-secondary:hover {{
138
+ background: #545b62;
139
+ }}
140
+
141
+ /* Grid Visualization */
142
+ .grid-container {{
143
+ background: white;
144
+ border: 1px solid #e0e0e0;
145
+ border-radius: 8px;
146
+ padding: 20px;
147
+ margin-bottom: 20px;
148
+ }}
149
+
150
+ .grid-display {{
151
+ display: inline-block;
152
+ border: 2px solid #333;
153
+ background: #fff;
154
+ padding: 5px;
155
+ margin: 10px 0;
156
+ }}
157
+
158
+ .grid {{
159
+ display: grid;
160
+ gap: 1px;
161
+ background: #333;
162
+ }}
163
+
164
+ .cell {{
165
+ width: 20px;
166
+ height: 20px;
167
+ display: flex;
168
+ align-items: center;
169
+ justify-content: center;
170
+ font-size: 10px;
171
+ cursor: pointer;
172
+ position: relative;
173
+ }}
174
+
175
+ .cell.ash {{ background-color: #2f2f2f; }}
176
+ .cell.fuel {{ background-color: #228b22; }}
177
+ .cell.burning {{ background-color: #ff4500; }}
178
+ .cell.firebreak {{ background-color: #8b4513; }}
179
+ .cell.watered {{ background-color: #4169e1; }}
180
+
181
+ .cell:hover {{
182
+ opacity: 0.8;
183
+ transform: scale(1.1);
184
+ z-index: 10;
185
+ }}
186
+
187
+ /* Stats Display */
188
+ .stats-display {{
189
+ background: white;
190
+ border: 1px solid #e0e0e0;
191
+ border-radius: 8px;
192
+ padding: 15px;
193
+ margin-bottom: 20px;
194
+ }}
195
+
196
+ .stats-grid {{
197
+ display: grid;
198
+ grid-template-columns: repeat(2, 1fr);
199
+ gap: 15px;
200
+ margin-top: 10px;
201
+ }}
202
+
203
+ .stat-item {{
204
+ display: flex;
205
+ flex-direction: column;
206
+ }}
207
+
208
+ .stat-label {{
209
+ font-size: 12px;
210
+ color: #666;
211
+ margin-bottom: 5px;
212
+ }}
213
+
214
+ .stat-value {{
215
+ font-size: 20px;
216
+ font-weight: bold;
217
+ color: #007bff;
218
+ }}
219
+
220
+ /* Instructions Section */
221
+ .instructions-section {{
222
+ background: white;
223
+ border: 1px solid #e0e0e0;
224
+ border-radius: 8px;
225
+ padding: 20px;
226
+ margin-bottom: 20px;
227
+ }}
228
+
229
+ .instructions-header {{
230
+ display: flex;
231
+ justify-content: space-between;
232
+ align-items: center;
233
+ margin-bottom: 15px;
234
+ }}
235
+
236
+ .instructions-title {{
237
+ font-size: 18px;
238
+ font-weight: 600;
239
+ color: #333;
240
+ margin: 0;
241
+ }}
242
+
243
+ .instructions-toggle {{
244
+ background: #f8f9fa;
245
+ border: 1px solid #dee2e6;
246
+ border-radius: 4px;
247
+ padding: 5px 10px;
248
+ cursor: pointer;
249
+ font-size: 12px;
250
+ color: #6c757d;
251
+ }}
252
+
253
+ .instructions-toggle:hover {{
254
+ background: #e9ecef;
255
+ }}
256
+
257
+ .instructions-content {{
258
+ display: none;
259
+ max-height: 400px;
260
+ overflow-y: auto;
261
+ border-top: 1px solid #e0e0e0;
262
+ padding-top: 15px;
263
+ }}
264
+
265
+ .instructions-content.expanded {{
266
+ display: block;
267
+ }}
268
+
269
+ /* Legend */
270
+ .legend {{
271
+ background: white;
272
+ border: 1px solid #e0e0e0;
273
+ border-radius: 8px;
274
+ padding: 15px;
275
+ margin-bottom: 20px;
276
+ }}
277
+
278
+ .legend-items {{
279
+ display: flex;
280
+ flex-wrap: wrap;
281
+ gap: 15px;
282
+ margin-top: 10px;
283
+ }}
284
+
285
+ .legend-item {{
286
+ display: flex;
287
+ align-items: center;
288
+ gap: 8px;
289
+ }}
290
+
291
+ .legend-color {{
292
+ width: 20px;
293
+ height: 20px;
294
+ border: 1px solid #333;
295
+ }}
296
+
297
+ /* Connection Status */
298
+ .status-indicator {{
299
+ display: inline-block;
300
+ width: 8px;
301
+ height: 8px;
302
+ border-radius: 50%;
303
+ margin-right: 8px;
304
+ }}
305
+
306
+ .status-connected {{
307
+ background: #28a745;
308
+ }}
309
+
310
+ .status-disconnected {{
311
+ background: #dc3545;
312
+ }}
313
+
314
+ /* Action Logs */
315
+ .logs-container {{
316
+ background: white;
317
+ border: 1px solid #e0e0e0;
318
+ border-radius: 8px;
319
+ padding: 15px;
320
+ max-height: 300px;
321
+ overflow-y: auto;
322
+ }}
323
+
324
+ .log-entry {{
325
+ border-bottom: 1px solid #f0f0f0;
326
+ padding: 10px 0;
327
+ }}
328
+
329
+ .log-entry:last-child {{
330
+ border-bottom: none;
331
+ }}
332
+
333
+ .log-timestamp {{
334
+ font-size: 12px;
335
+ color: #666;
336
+ margin-bottom: 5px;
337
+ }}
338
+
339
+ .log-action {{
340
+ background: #e3f2fd;
341
+ padding: 8px;
342
+ border-radius: 4px;
343
+ margin-bottom: 5px;
344
+ font-family: monospace;
345
+ font-size: 12px;
346
+ }}
347
+
348
+ .log-reward {{
349
+ font-weight: 600;
350
+ color: #28a745;
351
+ }}
352
+
353
+ .log-done {{
354
+ font-weight: 600;
355
+ color: #dc3545;
356
+ }}
357
+
358
+ /* State Display */
359
+ .state-display {{
360
+ background: white;
361
+ border: 1px solid #e0e0e0;
362
+ border-radius: 8px;
363
+ padding: 15px;
364
+ margin-bottom: 20px;
365
+ }}
366
+
367
+ .state-item {{
368
+ margin-bottom: 8px;
369
+ }}
370
+
371
+ .state-label {{
372
+ font-weight: 500;
373
+ color: #666;
374
+ }}
375
+
376
+ .state-value {{
377
+ color: #333;
378
+ font-family: monospace;
379
+ }}
380
+ </style>
381
+ </head>
382
+ <body>
383
+ <div class="container">
384
+ <!-- Left Pane: Action Interface -->
385
+ <div class="left-pane">
386
+ <div class="pane-header">
387
+ <span class="status-indicator status-disconnected" id="connection-status"></span>
388
+ Wildfire Containment Interface
389
+ </div>
390
+ <div class="pane-content">
391
+ <!-- Instructions Section -->
392
+ {_generate_instructions_section(instructions_html, metadata)}
393
+
394
+ <!-- Action Form -->
395
+ <div class="action-form">
396
+ <h3>Take Action</h3>
397
+ <form id="action-form">
398
+ <div class="form-group">
399
+ <label for="action">Action Type <span style="color: red;">*</span></label>
400
+ <select name="action" id="action" required>
401
+ <option value="">-- Select Action --</option>
402
+ <option value="water">Water (Extinguish Fire)</option>
403
+ <option value="break">Break (Create Firebreak)</option>
404
+ <option value="wait">Wait (Do Nothing)</option>
405
+ </select>
406
+ <small style="display: block; margin-top: 5px; color: #666;">
407
+ Water: Extinguishes fire at target cell<br>
408
+ Break: Creates firebreak to prevent spread<br>
409
+ Wait: Fire continues spreading
410
+ </small>
411
+ </div>
412
+
413
+ <div class="form-group" id="coordinates-group" style="display: none;">
414
+ <label for="x">X Coordinate</label>
415
+ <input type="number" name="x" id="x" min="0" placeholder="Enter X coordinate">
416
+
417
+ <label for="y" style="margin-top: 10px;">Y Coordinate</label>
418
+ <input type="number" name="y" id="y" min="0" placeholder="Enter Y coordinate">
419
+ <small style="display: block; margin-top: 5px; color: #666;">
420
+ Coordinates are required for water and break actions
421
+ </small>
422
+ </div>
423
+
424
+ <button type="submit" class="btn" id="step-btn">Execute Action</button>
425
+ </form>
426
+ </div>
427
+
428
+ <!-- Control Buttons -->
429
+ <div style="margin-bottom: 20px;">
430
+ <button class="btn btn-secondary" id="reset-btn">Reset Environment</button>
431
+ <button class="btn btn-secondary" id="state-btn">Get State</button>
432
+ </div>
433
+
434
+ <!-- Stats Display -->
435
+ <div class="stats-display">
436
+ <h3>Environment Stats</h3>
437
+ <div class="stats-grid">
438
+ <div class="stat-item">
439
+ <span class="stat-label">Step Count</span>
440
+ <span class="stat-value" id="step-count">0</span>
441
+ </div>
442
+ <div class="stat-item">
443
+ <span class="stat-label">Water Remaining</span>
444
+ <span class="stat-value" id="water-remaining">0</span>
445
+ </div>
446
+ <div class="stat-item">
447
+ <span class="stat-label">Breaks Remaining</span>
448
+ <span class="stat-value" id="breaks-remaining">0</span>
449
+ </div>
450
+ <div class="stat-item">
451
+ <span class="stat-label">Burning Cells</span>
452
+ <span class="stat-value" id="burning-count">0</span>
453
+ </div>
454
+ </div>
455
+ </div>
456
+
457
+ <!-- Current State Display -->
458
+ <div class="state-display">
459
+ <h3>Current State</h3>
460
+ <div id="current-state">
461
+ <div class="state-item">
462
+ <span class="state-label">Status:</span>
463
+ <span class="state-value" id="env-status">Not initialized</span>
464
+ </div>
465
+ <div class="state-item">
466
+ <span class="state-label">Episode ID:</span>
467
+ <span class="state-value" id="episode-id">-</span>
468
+ </div>
469
+ <div class="state-item">
470
+ <span class="state-label">Wind Direction:</span>
471
+ <span class="state-value" id="wind-dir">-</span>
472
+ </div>
473
+ <div class="state-item">
474
+ <span class="state-label">Humidity:</span>
475
+ <span class="state-value" id="humidity">-</span>
476
+ </div>
477
+ </div>
478
+ </div>
479
+ </div>
480
+ </div>
481
+
482
+ <!-- Right Pane: Visual Grid and Logs -->
483
+ <div class="right-pane">
484
+ <div class="pane-header">
485
+ Fire Grid Visualization
486
+ </div>
487
+ <div class="pane-content">
488
+ <!-- Legend -->
489
+ <div class="legend">
490
+ <h3>Legend</h3>
491
+ <div class="legend-items">
492
+ <div class="legend-item">
493
+ <div class="legend-color" style="background-color: #2f2f2f;"></div>
494
+ <span>Ash (Burned)</span>
495
+ </div>
496
+ <div class="legend-item">
497
+ <div class="legend-color" style="background-color: #228b22;"></div>
498
+ <span>Fuel (Safe)</span>
499
+ </div>
500
+ <div class="legend-item">
501
+ <div class="legend-color" style="background-color: #ff4500;"></div>
502
+ <span>Burning (Fire)</span>
503
+ </div>
504
+ <div class="legend-item">
505
+ <div class="legend-color" style="background-color: #8b4513;"></div>
506
+ <span>Firebreak</span>
507
+ </div>
508
+ <div class="legend-item">
509
+ <div class="legend-color" style="background-color: #4169e1;"></div>
510
+ <span>Watered (Damp)</span>
511
+ </div>
512
+ </div>
513
+ </div>
514
+
515
+ <!-- Grid Visualization -->
516
+ <div class="grid-container">
517
+ <h3>Fire Grid</h3>
518
+ <div id="grid-status" style="margin-bottom: 10px; font-size: 12px; color: #666;">
519
+ Waiting for grid data... (Click "Reset Environment" to initialize)
520
+ </div>
521
+ <div class="grid-display">
522
+ <div id="fire-grid" class="grid">
523
+ <!-- Grid will be rendered here -->
524
+ </div>
525
+ </div>
526
+ <p style="margin-top: 10px; font-size: 12px; color: #666;">
527
+ Click on a cell to set coordinates for water/break actions
528
+ </p>
529
+ </div>
530
+
531
+ <!-- Action Logs -->
532
+ <div class="logs-container">
533
+ <h3>Action History</h3>
534
+ <div id="action-logs">
535
+ No actions taken yet
536
+ </div>
537
+ </div>
538
+ </div>
539
+ </div>
540
+ </div>
541
+
542
+ <script>
543
+ class WildfireWebInterface {{
544
+ constructor() {{
545
+ this.ws = null;
546
+ this.isConnected = false;
547
+ this.currentGrid = null;
548
+ this.gridWidth = 0;
549
+ this.gridHeight = 0;
550
+ this.init();
551
+ }}
552
+
553
+ init() {{
554
+ this.connectWebSocket();
555
+ this.setupEventListeners();
556
+ }}
557
+
558
+ connectWebSocket() {{
559
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
560
+ const wsUrl = `${{protocol}}//${{window.location.host}}/ws`;
561
+
562
+ this.ws = new WebSocket(wsUrl);
563
+
564
+ this.ws.onopen = () => {{
565
+ this.isConnected = true;
566
+ this.updateConnectionStatus(true);
567
+ console.log('WebSocket connected');
568
+ // Trigger initial state fetch
569
+ this.fetchInitialState();
570
+ }};
571
+
572
+ this.ws.onmessage = (event) => {{
573
+ const data = JSON.parse(event.data);
574
+ if (data.type === 'state_update') {{
575
+ this.updateUI(data.episode_state);
576
+ }}
577
+ }};
578
+
579
+ this.ws.onclose = () => {{
580
+ this.isConnected = false;
581
+ this.updateConnectionStatus(false);
582
+ console.log('WebSocket disconnected');
583
+ setTimeout(() => this.connectWebSocket(), 3000);
584
+ }};
585
+
586
+ this.ws.onerror = (error) => {{
587
+ console.error('WebSocket error:', error);
588
+ }};
589
+ }}
590
+
591
+ async fetchInitialState() {{
592
+ // Fetch current state on connection to display grid
593
+ try {{
594
+ // Try to get current observation from state
595
+ const stateResponse = await fetch('/web/state');
596
+ const state = await stateResponse.json();
597
+
598
+ // If we have grid data in state, render it
599
+ if (state.grid && Array.isArray(state.grid) && state.width && state.height) {{
600
+ console.log('Rendering grid from state');
601
+ this.renderGrid(state.grid, state.width, state.height);
602
+ return;
603
+ }}
604
+
605
+ // If no grid in state, try to get it from the current episode state
606
+ // The WebSocket will send the current observation shortly
607
+ console.log('No grid in state, waiting for WebSocket update...');
608
+ }} catch (error) {{
609
+ console.error('Error fetching initial state:', error);
610
+ }}
611
+ }}
612
+
613
+ setupEventListeners() {{
614
+ // Instructions toggle
615
+ const instructionsToggle = document.getElementById('instructions-toggle');
616
+ const instructionsContent = document.getElementById('instructions-content');
617
+ if (instructionsToggle && instructionsContent) {{
618
+ instructionsToggle.addEventListener('click', () => {{
619
+ instructionsContent.classList.toggle('expanded');
620
+ instructionsToggle.textContent = instructionsContent.classList.contains('expanded')
621
+ ? 'Hide Instructions' : 'Show Instructions';
622
+ }});
623
+ }}
624
+
625
+ // Action type change - show/hide coordinates
626
+ document.getElementById('action').addEventListener('change', (e) => {{
627
+ const coordsGroup = document.getElementById('coordinates-group');
628
+ if (e.target.value === 'water' || e.target.value === 'break') {{
629
+ coordsGroup.style.display = 'block';
630
+ document.getElementById('x').required = true;
631
+ document.getElementById('y').required = true;
632
+ }} else {{
633
+ coordsGroup.style.display = 'none';
634
+ document.getElementById('x').required = false;
635
+ document.getElementById('y').required = false;
636
+ }}
637
+ }});
638
+
639
+ // Form submission
640
+ document.getElementById('action-form').addEventListener('submit', (e) => {{
641
+ e.preventDefault();
642
+ this.submitAction();
643
+ }});
644
+
645
+ // Reset button
646
+ document.getElementById('reset-btn').addEventListener('click', () => {{
647
+ this.resetEnvironment();
648
+ }});
649
+
650
+ // State button
651
+ document.getElementById('state-btn').addEventListener('click', () => {{
652
+ this.getState();
653
+ }});
654
+ }}
655
+
656
+ async submitAction() {{
657
+ const formData = new FormData(document.getElementById('action-form'));
658
+ const action = {{}};
659
+
660
+ for (const [key, value] of formData.entries()) {{
661
+ if (value !== '') {{
662
+ if (key === 'x' || key === 'y') {{
663
+ action[key] = parseInt(value);
664
+ }} else {{
665
+ action[key] = value;
666
+ }}
667
+ }}
668
+ }}
669
+
670
+ // Remove x/y if action is 'wait'
671
+ if (action.action === 'wait') {{
672
+ delete action.x;
673
+ delete action.y;
674
+ }}
675
+
676
+ try {{
677
+ const response = await fetch('/web/step', {{
678
+ method: 'POST',
679
+ headers: {{ 'Content-Type': 'application/json' }},
680
+ body: JSON.stringify({{ action }})
681
+ }});
682
+
683
+ if (!response.ok) {{
684
+ throw new Error(`HTTP error! status: ${{response.status}}`);
685
+ }}
686
+
687
+ const result = await response.json();
688
+ console.log('Step result:', result);
689
+ }} catch (error) {{
690
+ console.error('Error submitting action:', error);
691
+ alert('Error submitting action: ' + error.message);
692
+ }}
693
+ }}
694
+
695
+ async resetEnvironment() {{
696
+ try {{
697
+ const response = await fetch('/web/reset', {{
698
+ method: 'POST',
699
+ headers: {{ 'Content-Type': 'application/json' }}
700
+ }});
701
+
702
+ if (!response.ok) {{
703
+ throw new Error(`HTTP error! status: ${{response.status}}`);
704
+ }}
705
+
706
+ const result = await response.json();
707
+ console.log('Reset result:', result);
708
+ console.log('Reset observation:', result.observation);
709
+
710
+ // Render grid immediately after reset
711
+ if (result.observation && result.observation.grid) {{
712
+ const obs = result.observation;
713
+ console.log('Grid data:', obs.grid);
714
+ console.log('Grid dimensions:', obs.width, 'x', obs.height);
715
+ if (obs.grid && Array.isArray(obs.grid) && obs.width && obs.height) {{
716
+ console.log('Rendering grid from reset...');
717
+ this.renderGrid(obs.grid, obs.width, obs.height);
718
+ }} else {{
719
+ console.warn('Grid data invalid:', {{
720
+ gridIsArray: Array.isArray(obs.grid),
721
+ width: obs.width,
722
+ height: obs.height
723
+ }});
724
+ }}
725
+ }} else {{
726
+ console.warn('No grid data in reset result:', result);
727
+ }}
728
+ }} catch (error) {{
729
+ console.error('Error resetting environment:', error);
730
+ alert('Error resetting environment: ' + error.message);
731
+ }}
732
+ }}
733
+
734
+ async getState() {{
735
+ try {{
736
+ const response = await fetch('/web/state');
737
+ const state = await response.json();
738
+ console.log('Current state:', state);
739
+ alert('Current state: ' + JSON.stringify(state, null, 2));
740
+ }} catch (error) {{
741
+ console.error('Error getting state:', error);
742
+ alert('Error getting state: ' + error.message);
743
+ }}
744
+ }}
745
+
746
+ updateConnectionStatus(connected) {{
747
+ const indicator = document.getElementById('connection-status');
748
+ if (connected) {{
749
+ indicator.className = 'status-indicator status-connected';
750
+ }} else {{
751
+ indicator.className = 'status-indicator status-disconnected';
752
+ }}
753
+ }}
754
+
755
+ updateUI(episodeState) {{
756
+ // Update state display
757
+ document.getElementById('env-status').textContent =
758
+ episodeState.is_reset ? 'Reset' : 'Running';
759
+ document.getElementById('episode-id').textContent =
760
+ episodeState.episode_id || '-';
761
+ document.getElementById('step-count').textContent =
762
+ episodeState.step_count.toString();
763
+
764
+ // Update observation if available
765
+ if (episodeState.current_observation) {{
766
+ const obs = episodeState.current_observation;
767
+
768
+ // Update stats
769
+ document.getElementById('water-remaining').textContent =
770
+ obs.remaining_water !== undefined ? obs.remaining_water : '-';
771
+ document.getElementById('breaks-remaining').textContent =
772
+ obs.remaining_breaks !== undefined ? obs.remaining_breaks : '-';
773
+ document.getElementById('burning-count').textContent =
774
+ obs.burning_count !== undefined ? obs.burning_count : '-';
775
+ document.getElementById('wind-dir').textContent =
776
+ obs.wind_dir || '-';
777
+ document.getElementById('humidity').textContent =
778
+ obs.humidity !== undefined ? obs.humidity.toFixed(2) : '-';
779
+
780
+ // Update grid visualization - handle both array and list formats
781
+ let gridData = obs.grid;
782
+ let gridWidth = obs.width;
783
+ let gridHeight = obs.height;
784
+
785
+ console.log('Updating grid from observation:', {{
786
+ hasGrid: !!gridData,
787
+ gridType: typeof gridData,
788
+ isArray: Array.isArray(gridData),
789
+ width: gridWidth,
790
+ height: gridHeight
791
+ }});
792
+
793
+ // Convert grid to array if it's not already
794
+ if (gridData && !Array.isArray(gridData)) {{
795
+ if (typeof gridData === 'string') {{
796
+ try {{
797
+ gridData = JSON.parse(gridData);
798
+ console.log('Parsed grid from string');
799
+ }} catch (e) {{
800
+ console.error('Error parsing grid data:', e);
801
+ gridData = null;
802
+ }}
803
+ }}
804
+ }}
805
+
806
+ // Ensure we have valid grid data
807
+ if (gridData && Array.isArray(gridData) && gridWidth && gridHeight) {{
808
+ console.log('Rendering grid from WebSocket update:', gridWidth, 'x', gridHeight, 'cells:', gridData.length);
809
+ this.renderGrid(gridData, gridWidth, gridHeight);
810
+ }} else {{
811
+ console.warn('Invalid grid data in WebSocket update:', {{
812
+ grid: gridData,
813
+ gridLength: gridData ? (Array.isArray(gridData) ? gridData.length : 'not array') : 'null',
814
+ width: gridWidth,
815
+ height: gridHeight
816
+ }});
817
+ }}
818
+ }}
819
+
820
+ // Update action logs
821
+ const logsDiv = document.getElementById('action-logs');
822
+ if (episodeState.action_logs.length === 0) {{
823
+ logsDiv.innerHTML = 'No actions taken yet';
824
+ }} else {{
825
+ logsDiv.innerHTML = episodeState.action_logs.map(log => `
826
+ <div class="log-entry">
827
+ <div class="log-timestamp">${{log.timestamp}} (Step ${{log.step_count}})</div>
828
+ <div class="log-action">Action: ${{JSON.stringify(log.action, null, 2)}}</div>
829
+ <div>
830
+ <span class="log-reward">Reward: ${{log.reward !== null ? log.reward.toFixed(2) : 'None'}}</span>
831
+ ${{log.done ? '<span class="log-done">DONE</span>' : ''}}
832
+ </div>
833
+ </div>
834
+ `).join('');
835
+ }}
836
+ }}
837
+
838
+ renderGrid(grid, width, height) {{
839
+ this.gridWidth = width;
840
+ this.gridHeight = height;
841
+ this.currentGrid = grid;
842
+
843
+ const gridContainer = document.getElementById('fire-grid');
844
+ const gridStatus = document.getElementById('grid-status');
845
+
846
+ if (!gridContainer) {{
847
+ console.error('Grid container not found!');
848
+ return;
849
+ }}
850
+
851
+ // Validate grid dimensions
852
+ if (!width || !height || !grid || !Array.isArray(grid)) {{
853
+ console.error('Invalid grid parameters:', {{ width, height, grid }});
854
+ if (gridStatus) {{
855
+ gridStatus.innerHTML = '<span style="color: red;">Error: Invalid grid data</span>';
856
+ }}
857
+ gridContainer.innerHTML = '<p style="color: red;">Error: Invalid grid data</p>';
858
+ return;
859
+ }}
860
+
861
+ // Calculate grid size once
862
+ const gridSize = grid.length;
863
+ const expectedSize = width * height;
864
+
865
+ // Update status
866
+ if (gridStatus) {{
867
+ gridStatus.innerHTML = `Grid: ${{width}}×${{height}} (${{gridSize}} cells)`;
868
+ }}
869
+
870
+ // Check if grid size matches expected dimensions
871
+ if (gridSize !== expectedSize) {{
872
+ console.warn(`Grid size mismatch: expected ${{expectedSize}}, got ${{gridSize}}`);
873
+ }}
874
+
875
+ gridContainer.style.gridTemplateColumns = `repeat(${{width}}, 20px)`;
876
+ gridContainer.innerHTML = '';
877
+
878
+ // Grid encoding: 0=ash, 1=fuel, 2=burning, 3=firebreak, 4=watered
879
+ const cellClasses = ['ash', 'fuel', 'burning', 'firebreak', 'watered'];
880
+ const cellLabels = ['Ash', 'Fuel', 'Burning', 'Firebreak', 'Watered'];
881
+
882
+ console.log(`Rendering grid: ${{width}}x${{height}}, ${{gridSize}} cells`);
883
+
884
+ let renderedCells = 0;
885
+ for (let y = 0; y < height; y++) {{
886
+ for (let x = 0; x < width; x++) {{
887
+ const index = y * width + x;
888
+ const cellValue = (grid[index] !== undefined && grid[index] !== null) ? grid[index] : 0;
889
+ const cellClass = cellClasses[cellValue] || 'ash';
890
+ const cellLabel = cellLabels[cellValue] || 'Unknown';
891
+
892
+ const cell = document.createElement('div');
893
+ cell.className = `cell ${{cellClass}}`;
894
+ cell.title = `(${{x}}, ${{y}}): ${{cellLabel}} (value: ${{cellValue}})`;
895
+ cell.dataset.x = x;
896
+ cell.dataset.y = y;
897
+ cell.dataset.value = cellValue;
898
+
899
+ // Click to set coordinates
900
+ cell.addEventListener('click', () => {{
901
+ const xInput = document.getElementById('x');
902
+ const yInput = document.getElementById('y');
903
+ if (xInput) xInput.value = x;
904
+ if (yInput) yInput.value = y;
905
+ }});
906
+
907
+ gridContainer.appendChild(cell);
908
+ renderedCells++;
909
+ }}
910
+ }}
911
+
912
+ console.log(`Grid rendered: ${{width}}x${{height}} = ${{renderedCells}} cells`);
913
+
914
+ // Verify grid is visible
915
+ if (gridStatus) {{
916
+ gridStatus.innerHTML = `Grid: ${{width}}×${{height}} (${{renderedCells}} cells rendered) ✅`;
917
+ gridStatus.style.color = '#28a745';
918
+ }}
919
+ }}
920
+ }}
921
+
922
+ // Initialize the web interface when the page loads
923
+ document.addEventListener('DOMContentLoaded', () => {{
924
+ new WildfireWebInterface();
925
+ }});
926
+ </script>
927
+ </body>
928
+ </html>
929
+ """.replace('{_generate_instructions_section(instructions_html, metadata)}',
930
+ _generate_instructions_section(instructions_html, metadata))
931
+
932
+
933
+ def _generate_instructions_section(instructions_html: str, metadata: Optional[EnvironmentMetadata]) -> str:
934
+ """Generate the instructions section."""
935
+ if not instructions_html or not metadata:
936
+ return ''
937
+
938
+ return f'''
939
+ <!-- Instructions Section -->
940
+ <div class="instructions-section">
941
+ <div class="instructions-header">
942
+ <h3 class="instructions-title">{metadata.name if metadata else "Wildfire Environment"}</h3>
943
+ <button class="instructions-toggle" id="instructions-toggle">Show Instructions</button>
944
+ </div>
945
+ <div class="instructions-content" id="instructions-content">
946
+ <div class="instructions-readme">
947
+ {instructions_html}
948
+ </div>
949
+ </div>
950
+ </div>
951
+ '''
952
+
953
+
954
+ def _markdown_to_html_simple(markdown: str) -> str:
955
+ """Convert basic markdown to HTML."""
956
+ import html
957
+ import re
958
+
959
+ # Escape HTML first
960
+ html_content = html.escape(markdown)
961
+
962
+ # Convert headers
963
+ html_content = re.sub(r'^# (.*?)$', r'<h1>\1</h1>', html_content, flags=re.MULTILINE)
964
+ html_content = re.sub(r'^## (.*?)$', r'<h2>\1</h2>', html_content, flags=re.MULTILINE)
965
+ html_content = re.sub(r'^### (.*?)$', r'<h3>\1</h3>', html_content, flags=re.MULTILINE)
966
+
967
+ # Convert code blocks
968
+ html_content = re.sub(r'```(.*?)\n(.*?)\n```', r'<pre><code>\2</code></pre>', html_content, flags=re.DOTALL)
969
+ html_content = re.sub(r'`([^`]+)`', r'<code>\1</code>', html_content)
970
+
971
+ # Convert bold and italic
972
+ html_content = re.sub(r'\*\*(.*?)\*\*', r'<strong>\1</strong>', html_content)
973
+ html_content = re.sub(r'\*(.*?)\*', r'<em>\1</em>', html_content)
974
+
975
+ # Convert lists
976
+ html_content = re.sub(r'^- (.*?)$', r'<li>\1</li>', html_content, flags=re.MULTILINE)
977
+ html_content = re.sub(r'(<li>.*</li>)', r'<ul>\1</ul>', html_content, flags=re.DOTALL)
978
+
979
+ # Convert line breaks
980
+ html_content = html_content.replace('\n', '<br>')
981
+
982
+ return html_content
983
+