Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- Dockerfile +0 -1
- README.md +0 -1
- evals_hf.ipynb +114 -20
- examples/train_rl_agent.py +984 -0
- examples/train_sb3_agent.py +285 -0
- examples/train_torch_ppo.py +1278 -0
- examples/train_torch_ppo_http.py +492 -0
- frontend/README.md +93 -0
- frontend/eslint.config.js +22 -0
- frontend/index.html +16 -0
- frontend/package-lock.json +2772 -0
- frontend/package.json +30 -0
- frontend/public/favicon.svg +1 -0
- frontend/public/icons.svg +24 -0
- frontend/src/App.css +703 -0
- frontend/src/App.tsx +460 -0
- frontend/src/assets/hero.png +0 -0
- frontend/src/assets/react.svg +1 -0
- frontend/src/assets/vite.svg +1 -0
- frontend/src/components/APIReport.tsx +33 -0
- frontend/src/components/ControlPanel.tsx +90 -0
- frontend/src/components/HUD.tsx +169 -0
- frontend/src/components/Map2D.tsx +600 -0
- frontend/src/components/StatusCard.tsx +26 -0
- frontend/src/index.css +1 -0
- frontend/src/main.tsx +10 -0
- frontend/src/types.ts +112 -0
- frontend/tsconfig.app.json +25 -0
- frontend/tsconfig.json +7 -0
- frontend/tsconfig.node.json +24 -0
- frontend/vite.config.ts +20 -0
- openenv_pyre_env.egg-info/PKG-INFO +13 -0
- openenv_pyre_env.egg-info/SOURCES.txt +2 -0
- openenv_pyre_env.egg-info/requires.txt +14 -0
- outputs/20260425_154907_Qwen-Qwen3-06B/error.txt +57 -0
- outputs/20260425_154907_Qwen-Qwen3-06B/output.txt +46 -0
- outputs/20260425_154915_Qwen-Qwen3-17B/README.md +67 -0
- outputs/20260425_154915_Qwen-Qwen3-17B/checkpoint-10/config.json +60 -0
- outputs/20260425_154915_Qwen-Qwen3-17B/checkpoint-10/generation_config.json +12 -0
- outputs/20260425_154915_Qwen-Qwen3-17B/checkpoint-10/model-00001-of-00002.safetensors +3 -0
- outputs/20260425_154915_Qwen-Qwen3-17B/checkpoint-10/model-00002-of-00002.safetensors +3 -0
- outputs/20260425_154915_Qwen-Qwen3-17B/error.txt +220 -0
- outputs/20260425_154915_Qwen-Qwen3-17B/output.txt +46 -0
- outputs/20260425_154915_Qwen-Qwen3-17B/runs/Apr25_16-01-06_cccxc590/events.out.tfevents.1777147555.cccxc590.2920434.0 +3 -0
- pyproject.toml +28 -0
- run_training_openenv.sh +4 -7
- run_training_unsloth.sh +55 -0
- server/app.py +144 -6
- server/floor_plan.py +1 -0
- server/pyre_env_environment.py +1 -0
Dockerfile
CHANGED
|
@@ -77,5 +77,4 @@ HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
| 77 |
|
| 78 |
# Run the FastAPI server
|
| 79 |
# The module path is constructed to work with the /app/env structure
|
| 80 |
-
ENV ENABLE_WEB_INTERFACE=true
|
| 81 |
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
|
|
|
|
| 77 |
|
| 78 |
# Run the FastAPI server
|
| 79 |
# The module path is constructed to work with the /app/env structure
|
|
|
|
| 80 |
CMD ["sh", "-c", "cd /app/env && uvicorn server.app:app --host 0.0.0.0 --port 8000"]
|
README.md
CHANGED
|
@@ -6,7 +6,6 @@ colorTo: yellow
|
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
app_port: 8000
|
| 9 |
-
base_path: /web
|
| 10 |
tags:
|
| 11 |
- openenv
|
| 12 |
---
|
|
|
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
app_port: 8000
|
|
|
|
| 9 |
tags:
|
| 10 |
- openenv
|
| 11 |
---
|
evals_hf.ipynb
CHANGED
|
@@ -26,10 +26,18 @@
|
|
| 26 |
},
|
| 27 |
{
|
| 28 |
"cell_type": "code",
|
| 29 |
-
"execution_count":
|
| 30 |
"id": "c3d4e5f6",
|
| 31 |
"metadata": {},
|
| 32 |
-
"outputs": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
"source": [
|
| 34 |
"import csv\n",
|
| 35 |
"import json\n",
|
|
@@ -65,7 +73,7 @@
|
|
| 65 |
},
|
| 66 |
{
|
| 67 |
"cell_type": "markdown",
|
| 68 |
-
"id": "
|
| 69 |
"metadata": {},
|
| 70 |
"source": [
|
| 71 |
"## Cell 2 — Config ✏️ Edit this cell to change eval settings"
|
|
@@ -73,10 +81,23 @@
|
|
| 73 |
},
|
| 74 |
{
|
| 75 |
"cell_type": "code",
|
| 76 |
-
"execution_count":
|
| 77 |
"id": "e5f6a7b8",
|
| 78 |
"metadata": {},
|
| 79 |
-
"outputs": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 80 |
"source": [
|
| 81 |
"# ── Model ─────────────────────────────────────────────────────────────────────\n",
|
| 82 |
"MODEL_ID = \"Qwen/Qwen3-1.7B\" # HF model ID or local path / adapter dir\n",
|
|
@@ -123,10 +144,19 @@
|
|
| 123 |
},
|
| 124 |
{
|
| 125 |
"cell_type": "code",
|
| 126 |
-
"execution_count":
|
| 127 |
"id": "a7b8c9d0",
|
| 128 |
"metadata": {},
|
| 129 |
-
"outputs": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
"source": [
|
| 131 |
"ALL_DIFFICULTIES: List[Dict[str, Any]] = [\n",
|
| 132 |
" {\"difficulty\": \"easy\", \"max_steps\": 200, \"description\": \"1 fire source · slow spread · calm wind · high humidity\"},\n",
|
|
@@ -195,10 +225,18 @@
|
|
| 195 |
},
|
| 196 |
{
|
| 197 |
"cell_type": "code",
|
| 198 |
-
"execution_count":
|
| 199 |
"id": "c9d0e1f2",
|
| 200 |
"metadata": {},
|
| 201 |
-
"outputs": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
"source": [
|
| 203 |
"class HFChatModel(SimpleChatModel):\n",
|
| 204 |
" \"\"\"\n",
|
|
@@ -308,10 +346,18 @@
|
|
| 308 |
},
|
| 309 |
{
|
| 310 |
"cell_type": "code",
|
| 311 |
-
"execution_count":
|
| 312 |
"id": "e1f2a3b4",
|
| 313 |
"metadata": {},
|
| 314 |
-
"outputs": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
"source": [
|
| 316 |
"def _build_user_message(obs: Dict[str, Any], history: List[str]) -> str:\n",
|
| 317 |
" \"\"\"Convert a raw observation dict + history into the LLM user message.\"\"\"\n",
|
|
@@ -466,10 +512,18 @@
|
|
| 466 |
},
|
| 467 |
{
|
| 468 |
"cell_type": "code",
|
| 469 |
-
"execution_count":
|
| 470 |
"id": "a3b4c5d6",
|
| 471 |
"metadata": {},
|
| 472 |
-
"outputs": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 473 |
"source": [
|
| 474 |
"def run_episode(\n",
|
| 475 |
" llm: HFChatModel,\n",
|
|
@@ -637,10 +691,54 @@
|
|
| 637 |
},
|
| 638 |
{
|
| 639 |
"cell_type": "code",
|
| 640 |
-
"execution_count":
|
| 641 |
"id": "c5d6e7f8",
|
| 642 |
"metadata": {},
|
| 643 |
-
"outputs": [
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 644 |
"source": [
|
| 645 |
"# Health check first — fail fast before waiting for the large model to load\n",
|
| 646 |
"try:\n",
|
|
@@ -855,13 +953,9 @@
|
|
| 855 |
],
|
| 856 |
"metadata": {
|
| 857 |
"kernelspec": {
|
| 858 |
-
"display_name": "Python 3",
|
| 859 |
"language": "python",
|
| 860 |
"name": "python3"
|
| 861 |
-
},
|
| 862 |
-
"language_info": {
|
| 863 |
-
"name": "python",
|
| 864 |
-
"version": "3.10.0"
|
| 865 |
}
|
| 866 |
},
|
| 867 |
"nbformat": 4,
|
|
|
|
| 26 |
},
|
| 27 |
{
|
| 28 |
"cell_type": "code",
|
| 29 |
+
"execution_count": 1,
|
| 30 |
"id": "c3d4e5f6",
|
| 31 |
"metadata": {},
|
| 32 |
+
"outputs": [
|
| 33 |
+
{
|
| 34 |
+
"name": "stdout",
|
| 35 |
+
"output_type": "stream",
|
| 36 |
+
"text": [
|
| 37 |
+
"Imports OK\n"
|
| 38 |
+
]
|
| 39 |
+
}
|
| 40 |
+
],
|
| 41 |
"source": [
|
| 42 |
"import csv\n",
|
| 43 |
"import json\n",
|
|
|
|
| 73 |
},
|
| 74 |
{
|
| 75 |
"cell_type": "markdown",
|
| 76 |
+
"id": "1f1d9271",
|
| 77 |
"metadata": {},
|
| 78 |
"source": [
|
| 79 |
"## Cell 2 — Config ✏️ Edit this cell to change eval settings"
|
|
|
|
| 81 |
},
|
| 82 |
{
|
| 83 |
"cell_type": "code",
|
| 84 |
+
"execution_count": 2,
|
| 85 |
"id": "e5f6a7b8",
|
| 86 |
"metadata": {},
|
| 87 |
+
"outputs": [
|
| 88 |
+
{
|
| 89 |
+
"name": "stdout",
|
| 90 |
+
"output_type": "stream",
|
| 91 |
+
"text": [
|
| 92 |
+
"Model : Qwen/Qwen3-1.7B\n",
|
| 93 |
+
"4-bit : False\n",
|
| 94 |
+
"Temperature: 0.3\n",
|
| 95 |
+
"Levels : all 3 (easy, medium, hard)\n",
|
| 96 |
+
"Seeds : [1, 2, 3]\n",
|
| 97 |
+
"Output dir : ./outputs/hf_evals\n"
|
| 98 |
+
]
|
| 99 |
+
}
|
| 100 |
+
],
|
| 101 |
"source": [
|
| 102 |
"# ── Model ─────────────────────────────────────────────────────────────────────\n",
|
| 103 |
"MODEL_ID = \"Qwen/Qwen3-1.7B\" # HF model ID or local path / adapter dir\n",
|
|
|
|
| 144 |
},
|
| 145 |
{
|
| 146 |
"cell_type": "code",
|
| 147 |
+
"execution_count": 3,
|
| 148 |
"id": "a7b8c9d0",
|
| 149 |
"metadata": {},
|
| 150 |
+
"outputs": [
|
| 151 |
+
{
|
| 152 |
+
"name": "stdout",
|
| 153 |
+
"output_type": "stream",
|
| 154 |
+
"text": [
|
| 155 |
+
"Difficulties to evaluate : ['easy', 'medium', 'hard']\n",
|
| 156 |
+
"Total episodes : 9\n"
|
| 157 |
+
]
|
| 158 |
+
}
|
| 159 |
+
],
|
| 160 |
"source": [
|
| 161 |
"ALL_DIFFICULTIES: List[Dict[str, Any]] = [\n",
|
| 162 |
" {\"difficulty\": \"easy\", \"max_steps\": 200, \"description\": \"1 fire source · slow spread · calm wind · high humidity\"},\n",
|
|
|
|
| 225 |
},
|
| 226 |
{
|
| 227 |
"cell_type": "code",
|
| 228 |
+
"execution_count": 4,
|
| 229 |
"id": "c9d0e1f2",
|
| 230 |
"metadata": {},
|
| 231 |
+
"outputs": [
|
| 232 |
+
{
|
| 233 |
+
"name": "stdout",
|
| 234 |
+
"output_type": "stream",
|
| 235 |
+
"text": [
|
| 236 |
+
"HFChatModel class defined.\n"
|
| 237 |
+
]
|
| 238 |
+
}
|
| 239 |
+
],
|
| 240 |
"source": [
|
| 241 |
"class HFChatModel(SimpleChatModel):\n",
|
| 242 |
" \"\"\"\n",
|
|
|
|
| 346 |
},
|
| 347 |
{
|
| 348 |
"cell_type": "code",
|
| 349 |
+
"execution_count": 5,
|
| 350 |
"id": "e1f2a3b4",
|
| 351 |
"metadata": {},
|
| 352 |
+
"outputs": [
|
| 353 |
+
{
|
| 354 |
+
"name": "stdout",
|
| 355 |
+
"output_type": "stream",
|
| 356 |
+
"text": [
|
| 357 |
+
"Prompt builder and action parser defined.\n"
|
| 358 |
+
]
|
| 359 |
+
}
|
| 360 |
+
],
|
| 361 |
"source": [
|
| 362 |
"def _build_user_message(obs: Dict[str, Any], history: List[str]) -> str:\n",
|
| 363 |
" \"\"\"Convert a raw observation dict + history into the LLM user message.\"\"\"\n",
|
|
|
|
| 512 |
},
|
| 513 |
{
|
| 514 |
"cell_type": "code",
|
| 515 |
+
"execution_count": 6,
|
| 516 |
"id": "a3b4c5d6",
|
| 517 |
"metadata": {},
|
| 518 |
+
"outputs": [
|
| 519 |
+
{
|
| 520 |
+
"name": "stdout",
|
| 521 |
+
"output_type": "stream",
|
| 522 |
+
"text": [
|
| 523 |
+
"Episode runner defined.\n"
|
| 524 |
+
]
|
| 525 |
+
}
|
| 526 |
+
],
|
| 527 |
"source": [
|
| 528 |
"def run_episode(\n",
|
| 529 |
" llm: HFChatModel,\n",
|
|
|
|
| 691 |
},
|
| 692 |
{
|
| 693 |
"cell_type": "code",
|
| 694 |
+
"execution_count": 7,
|
| 695 |
"id": "c5d6e7f8",
|
| 696 |
"metadata": {},
|
| 697 |
+
"outputs": [
|
| 698 |
+
{
|
| 699 |
+
"ename": "RuntimeError",
|
| 700 |
+
"evalue": "Server not reachable at http://localhost:8000: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /health (Caused by NewConnectionError(\"HTTPConnection(host='localhost', port=8000): Failed to establish a new connection: [Errno 111] Connection refused\"))",
|
| 701 |
+
"output_type": "error",
|
| 702 |
+
"traceback": [
|
| 703 |
+
"\u001b[31m---------------------------------------------------------------------------\u001b[39m",
|
| 704 |
+
"\u001b[31mConnectionRefusedError\u001b[39m Traceback (most recent call last)",
|
| 705 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/urllib3/connection.py:204\u001b[39m, in \u001b[36mHTTPConnection._new_conn\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 203\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m204\u001b[39m sock = \u001b[30;43mconnection\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mcreate_connection\u001b[39;49m\u001b[30;43m(\u001b[39;49m\n\u001b[32m 205\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_dns_host\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mport\u001b[39;49m\u001b[30;43m)\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 206\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mtimeout\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 207\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43msource_address\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43msource_address\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 208\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43msocket_options\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43msocket_options\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 209\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 210\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m socket.gaierror \u001b[38;5;28;01mas\u001b[39;00m e:\n",
|
| 706 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/urllib3/util/connection.py:85\u001b[39m, in \u001b[36mcreate_connection\u001b[39m\u001b[34m(address, timeout, source_address, socket_options)\u001b[39m\n\u001b[32m 84\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m---> \u001b[39m\u001b[32m85\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m err\n\u001b[32m 86\u001b[39m \u001b[38;5;28;01mfinally\u001b[39;00m:\n\u001b[32m 87\u001b[39m \u001b[38;5;66;03m# Break explicitly a reference cycle\u001b[39;00m\n",
|
| 707 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/urllib3/util/connection.py:73\u001b[39m, in \u001b[36mcreate_connection\u001b[39m\u001b[34m(address, timeout, source_address, socket_options)\u001b[39m\n\u001b[32m 72\u001b[39m sock.bind(source_address)\n\u001b[32m---> \u001b[39m\u001b[32m73\u001b[39m \u001b[30;43msock\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mconnect\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43msa\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 74\u001b[39m \u001b[38;5;66;03m# Break explicitly a reference cycle\u001b[39;00m\n",
|
| 708 |
+
"\u001b[31mConnectionRefusedError\u001b[39m: [Errno 111] Connection refused",
|
| 709 |
+
"\nThe above exception was the direct cause of the following exception:\n",
|
| 710 |
+
"\u001b[31mNewConnectionError\u001b[39m Traceback (most recent call last)",
|
| 711 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/urllib3/connectionpool.py:787\u001b[39m, in \u001b[36mHTTPConnectionPool.urlopen\u001b[39m\u001b[34m(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)\u001b[39m\n\u001b[32m 786\u001b[39m \u001b[38;5;66;03m# Make the request on the HTTPConnection object\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m787\u001b[39m response = \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_make_request\u001b[39;49m\u001b[30;43m(\u001b[39;49m\n\u001b[32m 788\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mconn\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 789\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mmethod\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 790\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43murl\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 791\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mtimeout\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mtimeout_obj\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 792\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mbody\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mbody\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 793\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mheaders\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mheaders\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 794\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mchunked\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mchunked\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 795\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mretries\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mretries\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 796\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mresponse_conn\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mresponse_conn\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 797\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mpreload_content\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mpreload_content\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 798\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mdecode_content\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mdecode_content\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 799\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43mresponse_kw\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 800\u001b[39m \u001b[30;43m\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 802\u001b[39m \u001b[38;5;66;03m# Everything went great!\u001b[39;00m\n",
|
| 712 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/urllib3/connectionpool.py:493\u001b[39m, in \u001b[36mHTTPConnectionPool._make_request\u001b[39m\u001b[34m(self, conn, method, url, body, headers, retries, timeout, chunked, response_conn, preload_content, decode_content, enforce_content_length)\u001b[39m\n\u001b[32m 492\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m493\u001b[39m \u001b[30;43mconn\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mrequest\u001b[39;49m\u001b[30;43m(\u001b[39;49m\n\u001b[32m 494\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mmethod\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 495\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43murl\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 496\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mbody\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mbody\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 497\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mheaders\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mheaders\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 498\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mchunked\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mchunked\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 499\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mpreload_content\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mpreload_content\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 500\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mdecode_content\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mdecode_content\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 501\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43menforce_content_length\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43menforce_content_length\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 502\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 504\u001b[39m \u001b[38;5;66;03m# We are swallowing BrokenPipeError (errno.EPIPE) since the server is\u001b[39;00m\n\u001b[32m 505\u001b[39m \u001b[38;5;66;03m# legitimately able to close the connection after sending a valid response.\u001b[39;00m\n\u001b[32m 506\u001b[39m \u001b[38;5;66;03m# With this behaviour, the received response is still readable.\u001b[39;00m\n",
|
| 713 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/urllib3/connection.py:500\u001b[39m, in \u001b[36mHTTPConnection.request\u001b[39m\u001b[34m(self, method, url, body, headers, chunked, preload_content, decode_content, enforce_content_length)\u001b[39m\n\u001b[32m 499\u001b[39m \u001b[38;5;28mself\u001b[39m.putheader(header, value)\n\u001b[32m--> \u001b[39m\u001b[32m500\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mendheaders\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 502\u001b[39m \u001b[38;5;66;03m# If we're given a body we start sending that in chunks.\u001b[39;00m\n",
|
| 714 |
+
"\u001b[36mFile \u001b[39m\u001b[32m~/.local/share/uv/python/cpython-3.12.12-linux-x86_64-gnu/lib/python3.12/http/client.py:1333\u001b[39m, in \u001b[36mHTTPConnection.endheaders\u001b[39m\u001b[34m(self, message_body, encode_chunked)\u001b[39m\n\u001b[32m 1332\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m CannotSendHeader()\n\u001b[32m-> \u001b[39m\u001b[32m1333\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_send_output\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mmessage_body\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mencode_chunked\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mencode_chunked\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n",
|
| 715 |
+
"\u001b[36mFile \u001b[39m\u001b[32m~/.local/share/uv/python/cpython-3.12.12-linux-x86_64-gnu/lib/python3.12/http/client.py:1093\u001b[39m, in \u001b[36mHTTPConnection._send_output\u001b[39m\u001b[34m(self, message_body, encode_chunked)\u001b[39m\n\u001b[32m 1092\u001b[39m \u001b[38;5;28;01mdel\u001b[39;00m \u001b[38;5;28mself\u001b[39m._buffer[:]\n\u001b[32m-> \u001b[39m\u001b[32m1093\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43msend\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mmsg\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 1095\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m message_body \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m 1096\u001b[39m \n\u001b[32m 1097\u001b[39m \u001b[38;5;66;03m# create a consistent interface to message_body\u001b[39;00m\n",
|
| 716 |
+
"\u001b[36mFile \u001b[39m\u001b[32m~/.local/share/uv/python/cpython-3.12.12-linux-x86_64-gnu/lib/python3.12/http/client.py:1037\u001b[39m, in \u001b[36mHTTPConnection.send\u001b[39m\u001b[34m(self, data)\u001b[39m\n\u001b[32m 1036\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m.auto_open:\n\u001b[32m-> \u001b[39m\u001b[32m1037\u001b[39m \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mconnect\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 1038\u001b[39m \u001b[38;5;28;01melse\u001b[39;00m:\n",
|
| 717 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/urllib3/connection.py:331\u001b[39m, in \u001b[36mHTTPConnection.connect\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 330\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mconnect\u001b[39m(\u001b[38;5;28mself\u001b[39m) -> \u001b[38;5;28;01mNone\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m331\u001b[39m \u001b[38;5;28mself\u001b[39m.sock = \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43m_new_conn\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 332\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m._tunnel_host:\n\u001b[32m 333\u001b[39m \u001b[38;5;66;03m# If we're tunneling it means we're connected to our proxy.\u001b[39;00m\n",
|
| 718 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/urllib3/connection.py:219\u001b[39m, in \u001b[36mHTTPConnection._new_conn\u001b[39m\u001b[34m(self)\u001b[39m\n\u001b[32m 218\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m \u001b[38;5;167;01mOSError\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m e:\n\u001b[32m--> \u001b[39m\u001b[32m219\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m NewConnectionError(\n\u001b[32m 220\u001b[39m \u001b[38;5;28mself\u001b[39m, \u001b[33mf\u001b[39m\u001b[33m\"\u001b[39m\u001b[33mFailed to establish a new connection: \u001b[39m\u001b[38;5;132;01m{\u001b[39;00me\u001b[38;5;132;01m}\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 221\u001b[39m ) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01me\u001b[39;00m\n\u001b[32m 223\u001b[39m sys.audit(\u001b[33m\"\u001b[39m\u001b[33mhttp.client.connect\u001b[39m\u001b[33m\"\u001b[39m, \u001b[38;5;28mself\u001b[39m, \u001b[38;5;28mself\u001b[39m.host, \u001b[38;5;28mself\u001b[39m.port)\n",
|
| 719 |
+
"\u001b[31mNewConnectionError\u001b[39m: HTTPConnection(host='localhost', port=8000): Failed to establish a new connection: [Errno 111] Connection refused",
|
| 720 |
+
"\nThe above exception was the direct cause of the following exception:\n",
|
| 721 |
+
"\u001b[31mMaxRetryError\u001b[39m Traceback (most recent call last)",
|
| 722 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/requests/adapters.py:645\u001b[39m, in \u001b[36mHTTPAdapter.send\u001b[39m\u001b[34m(self, request, stream, timeout, verify, cert, proxies)\u001b[39m\n\u001b[32m 644\u001b[39m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[32m--> \u001b[39m\u001b[32m645\u001b[39m resp = \u001b[30;43mconn\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43murlopen\u001b[39;49m\u001b[30;43m(\u001b[39;49m\n\u001b[32m 646\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mmethod\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mrequest\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mmethod\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 647\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43murl\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43murl\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 648\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mbody\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mrequest\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mbody\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 649\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mheaders\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mrequest\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mheaders\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 650\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mredirect\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43;01mFalse\u001b[39;49;00m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 651\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43massert_same_host\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43;01mFalse\u001b[39;49;00m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 652\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mpreload_content\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43;01mFalse\u001b[39;49;00m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 653\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mdecode_content\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43;01mFalse\u001b[39;49;00m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 654\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mretries\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mmax_retries\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 655\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mtimeout\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mtimeout\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 656\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mchunked\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mchunked\u001b[39;49m\u001b[30;43m,\u001b[39;49m\n\u001b[32m 657\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 659\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m (ProtocolError, \u001b[38;5;167;01mOSError\u001b[39;00m) \u001b[38;5;28;01mas\u001b[39;00m err:\n",
|
| 723 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/urllib3/connectionpool.py:841\u001b[39m, in \u001b[36mHTTPConnectionPool.urlopen\u001b[39m\u001b[34m(self, method, url, body, headers, retries, redirect, assert_same_host, timeout, pool_timeout, release_conn, chunked, body_pos, preload_content, decode_content, **response_kw)\u001b[39m\n\u001b[32m 839\u001b[39m new_e = ProtocolError(\u001b[33m\"\u001b[39m\u001b[33mConnection aborted.\u001b[39m\u001b[33m\"\u001b[39m, new_e)\n\u001b[32m--> \u001b[39m\u001b[32m841\u001b[39m retries = \u001b[30;43mretries\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mincrement\u001b[39;49m\u001b[30;43m(\u001b[39;49m\n\u001b[32m 842\u001b[39m \u001b[30;43m \u001b[39;49m\u001b[30;43mmethod\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43murl\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43merror\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mnew_e\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m_pool\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mself\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m_stacktrace\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43msys\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mexc_info\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m)\u001b[39;49m\u001b[30;43m[\u001b[39;49m\u001b[30;43m2\u001b[39;49m\u001b[30;43m]\u001b[39;49m\n\u001b[32m 843\u001b[39m \u001b[30;43m\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 844\u001b[39m retries.sleep()\n",
|
| 724 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/urllib3/util/retry.py:535\u001b[39m, in \u001b[36mRetry.increment\u001b[39m\u001b[34m(self, method, url, response, error, _pool, _stacktrace)\u001b[39m\n\u001b[32m 534\u001b[39m reason = error \u001b[38;5;129;01mor\u001b[39;00m ResponseError(cause)\n\u001b[32m--> \u001b[39m\u001b[32m535\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m MaxRetryError(_pool, url, reason) \u001b[38;5;28;01mfrom\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34;01mreason\u001b[39;00m \u001b[38;5;66;03m# type: ignore[arg-type]\u001b[39;00m\n\u001b[32m 537\u001b[39m log.debug(\u001b[33m\"\u001b[39m\u001b[33mIncremented Retry for (url=\u001b[39m\u001b[33m'\u001b[39m\u001b[38;5;132;01m%s\u001b[39;00m\u001b[33m'\u001b[39m\u001b[33m): \u001b[39m\u001b[38;5;132;01m%r\u001b[39;00m\u001b[33m\"\u001b[39m, url, new_retry)\n",
|
| 725 |
+
"\u001b[31mMaxRetryError\u001b[39m: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /health (Caused by NewConnectionError(\"HTTPConnection(host='localhost', port=8000): Failed to establish a new connection: [Errno 111] Connection refused\"))",
|
| 726 |
+
"\nDuring handling of the above exception, another exception occurred:\n",
|
| 727 |
+
"\u001b[31mConnectionError\u001b[39m Traceback (most recent call last)",
|
| 728 |
+
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 7\u001b[39m\n\u001b[32m 5\u001b[39m print(f\"Server health check PASSED ({ENV_URL})\")\n\u001b[32m 6\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m Exception \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[32m----> \u001b[39m\u001b[32m7\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m RuntimeError(f\"Server not reachable at {ENV_URL}: {exc}\")\n\u001b[32m 8\u001b[39m \n",
|
| 729 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/requests/api.py:73\u001b[39m, in \u001b[36mget\u001b[39m\u001b[34m(url, params, **kwargs)\u001b[39m\n\u001b[32m 63\u001b[39m \u001b[38;5;250m\u001b[39m\u001b[33mr\u001b[39m\u001b[33;03m\"\"\"Sends a GET request.\u001b[39;00m\n\u001b[32m 64\u001b[39m \n\u001b[32m 65\u001b[39m \u001b[33;03m:param url: URL for the new :class:`Request` object.\u001b[39;00m\n\u001b[32m (...)\u001b[39m\u001b[32m 70\u001b[39m \u001b[33;03m:rtype: requests.Response\u001b[39;00m\n\u001b[32m 71\u001b[39m \u001b[33;03m\"\"\"\u001b[39;00m\n\u001b[32m---> \u001b[39m\u001b[32m73\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43mrequest\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43m\"\u001b[39;49m\u001b[30;43mget\u001b[39;49m\u001b[30;43m\"\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43murl\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43mparams\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mparams\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43mkwargs\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n",
|
| 730 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/requests/api.py:59\u001b[39m, in \u001b[36mrequest\u001b[39m\u001b[34m(method, url, **kwargs)\u001b[39m\n\u001b[32m 58\u001b[39m \u001b[38;5;28;01mwith\u001b[39;00m sessions.Session() \u001b[38;5;28;01mas\u001b[39;00m session:\n\u001b[32m---> \u001b[39m\u001b[32m59\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[30;43msession\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43mrequest\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mmethod\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43mmethod\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43murl\u001b[39;49m\u001b[30;43m=\u001b[39;49m\u001b[30;43murl\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43mkwargs\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n",
|
| 731 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/requests/sessions.py:592\u001b[39m, in \u001b[36mSession.request\u001b[39m\u001b[34m(self, method, url, params, data, headers, cookies, files, auth, timeout, allow_redirects, proxies, hooks, stream, verify, cert, json)\u001b[39m\n\u001b[32m 591\u001b[39m send_kwargs.update(settings)\n\u001b[32m--> \u001b[39m\u001b[32m592\u001b[39m resp = \u001b[30;43mself\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43msend\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mprep\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43msend_kwargs\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 594\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m resp\n",
|
| 732 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/requests/sessions.py:706\u001b[39m, in \u001b[36mSession.send\u001b[39m\u001b[34m(self, request, **kwargs)\u001b[39m\n\u001b[32m 705\u001b[39m \u001b[38;5;66;03m# Send the request\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m706\u001b[39m r = \u001b[30;43madapter\u001b[39;49m\u001b[30;43m.\u001b[39;49m\u001b[30;43msend\u001b[39;49m\u001b[30;43m(\u001b[39;49m\u001b[30;43mrequest\u001b[39;49m\u001b[30;43m,\u001b[39;49m\u001b[30;43m \u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43m*\u001b[39;49m\u001b[30;43mkwargs\u001b[39;49m\u001b[30;43m)\u001b[39;49m\n\u001b[32m 708\u001b[39m \u001b[38;5;66;03m# Total elapsed time of the request (approximately)\u001b[39;00m\n",
|
| 733 |
+
"\u001b[36mFile \u001b[39m\u001b[32m/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/requests/adapters.py:678\u001b[39m, in \u001b[36mHTTPAdapter.send\u001b[39m\u001b[34m(self, request, stream, timeout, verify, cert, proxies)\u001b[39m\n\u001b[32m 676\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m SSLError(e, request=request)\n\u001b[32m--> \u001b[39m\u001b[32m678\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mConnectionError\u001b[39;00m(e, request=request)\n\u001b[32m 680\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m ClosedPoolError \u001b[38;5;28;01mas\u001b[39;00m e:\n",
|
| 734 |
+
"\u001b[31mConnectionError\u001b[39m: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /health (Caused by NewConnectionError(\"HTTPConnection(host='localhost', port=8000): Failed to establish a new connection: [Errno 111] Connection refused\"))",
|
| 735 |
+
"\nDuring handling of the above exception, another exception occurred:\n",
|
| 736 |
+
"\u001b[31mRuntimeError\u001b[39m Traceback (most recent call last)",
|
| 737 |
+
"\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[7]\u001b[39m\u001b[32m, line 7\u001b[39m\n\u001b[32m 3\u001b[39m health = requests.get(f\"{ENV_URL}/health\", timeout=\u001b[32m5\u001b[39m)\n\u001b[32m 4\u001b[39m health.raise_for_status()\n\u001b[32m 5\u001b[39m print(f\"Server health check PASSED ({ENV_URL})\")\n\u001b[32m 6\u001b[39m \u001b[38;5;28;01mexcept\u001b[39;00m Exception \u001b[38;5;28;01mas\u001b[39;00m exc:\n\u001b[32m----> \u001b[39m\u001b[32m7\u001b[39m \u001b[38;5;28;01mraise\u001b[39;00m RuntimeError(f\"Server not reachable at {ENV_URL}: {exc}\")\n\u001b[32m 8\u001b[39m \n\u001b[32m 9\u001b[39m \u001b[38;5;66;03m# Build and load the HF model — this is the expensive step\u001b[39;00m\n\u001b[32m 10\u001b[39m llm = HFChatModel(\n",
|
| 738 |
+
"\u001b[31mRuntimeError\u001b[39m: Server not reachable at http://localhost:8000: HTTPConnectionPool(host='localhost', port=8000): Max retries exceeded with url: /health (Caused by NewConnectionError(\"HTTPConnection(host='localhost', port=8000): Failed to establish a new connection: [Errno 111] Connection refused\"))"
|
| 739 |
+
]
|
| 740 |
+
}
|
| 741 |
+
],
|
| 742 |
"source": [
|
| 743 |
"# Health check first — fail fast before waiting for the large model to load\n",
|
| 744 |
"try:\n",
|
|
|
|
| 953 |
],
|
| 954 |
"metadata": {
|
| 955 |
"kernelspec": {
|
| 956 |
+
"display_name": "Python 3 (ipykernel)",
|
| 957 |
"language": "python",
|
| 958 |
"name": "python3"
|
|
|
|
|
|
|
|
|
|
|
|
|
| 959 |
}
|
| 960 |
},
|
| 961 |
"nbformat": 4,
|
examples/train_rl_agent.py
ADDED
|
@@ -0,0 +1,984 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Train a deep RL baseline directly against the local Pyre environment.
|
| 2 |
+
|
| 3 |
+
This script makes the environment contract explicit:
|
| 4 |
+
- Observation: encoded from `PyreObservation.map_state` into a fixed-length vector
|
| 5 |
+
- Action: fixed discrete action table with a runtime validity mask from `available_actions_hint`
|
| 6 |
+
- Reward: the environment's composite reward returned by `PyreEnvironment.step()`
|
| 7 |
+
|
| 8 |
+
It uses a self-contained NumPy actor-critic implementation so it can run in
|
| 9 |
+
this repository without external ML dependencies.
|
| 10 |
+
|
| 11 |
+
Examples:
|
| 12 |
+
python examples/train_rl_agent.py --episodes 150 --difficulty easy
|
| 13 |
+
python examples/train_rl_agent.py --episodes 300 --difficulty-schedule easy,medium
|
| 14 |
+
python examples/train_rl_agent.py --episodes 200 --difficulty easy,medium,hard --observation-mode full
|
| 15 |
+
python examples/train_rl_agent.py --describe-only
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
import argparse
|
| 21 |
+
import csv
|
| 22 |
+
import json
|
| 23 |
+
import math
|
| 24 |
+
import re
|
| 25 |
+
from collections import deque
|
| 26 |
+
from dataclasses import dataclass
|
| 27 |
+
from pathlib import Path
|
| 28 |
+
from typing import Dict, Iterable, List, Sequence
|
| 29 |
+
|
| 30 |
+
import numpy as np
|
| 31 |
+
|
| 32 |
+
from pyre_env.models import PyreAction, PyreObservation
|
| 33 |
+
from pyre_env.server.pyre_env_environment import PyreEnvironment
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
MAX_GRID_W = 24
|
| 37 |
+
MAX_GRID_H = 24
|
| 38 |
+
MAX_DOORS = 16
|
| 39 |
+
DIRECTIONS = ("north", "south", "west", "east")
|
| 40 |
+
WINDS = ("CALM", "NORTH", "SOUTH", "WEST", "EAST")
|
| 41 |
+
DIFFICULTIES = ("easy", "medium", "hard")
|
| 42 |
+
|
| 43 |
+
MOVE_KEYS = [f"move(direction='{d}')" for d in DIRECTIONS]
|
| 44 |
+
LOOK_KEYS = [f"look(direction='{d}')" for d in DIRECTIONS]
|
| 45 |
+
WAIT_KEY = "wait()"
|
| 46 |
+
OPEN_KEYS = [f"door(target_id='door_{i}', door_state='open')" for i in range(1, MAX_DOORS + 1)]
|
| 47 |
+
CLOSE_KEYS = [f"door(target_id='door_{i}', door_state='close')" for i in range(1, MAX_DOORS + 1)]
|
| 48 |
+
ACTION_KEYS = MOVE_KEYS + LOOK_KEYS + [WAIT_KEY] + OPEN_KEYS + CLOSE_KEYS
|
| 49 |
+
ACTION_DIM = len(ACTION_KEYS)
|
| 50 |
+
ACTION_TO_INDEX = {key: idx for idx, key in enumerate(ACTION_KEYS)}
|
| 51 |
+
|
| 52 |
+
_MOVE_RE = re.compile(r"move\(direction='(north|south|west|east)'\)")
|
| 53 |
+
_LOOK_RE = re.compile(r"look\(direction='(north|south|west|east)'\)")
|
| 54 |
+
_DOOR_RE = re.compile(r"door\(target_id='(door_(\d+))', door_state='(open|close)'\)")
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def _one_hot(index: int, size: int) -> np.ndarray:
|
| 58 |
+
arr = np.zeros(size, dtype=np.float32)
|
| 59 |
+
if 0 <= index < size:
|
| 60 |
+
arr[index] = 1.0
|
| 61 |
+
return arr
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def action_index_to_env_action(index: int) -> PyreAction:
|
| 65 |
+
if 0 <= index < 4:
|
| 66 |
+
return PyreAction(action="move", direction=DIRECTIONS[index])
|
| 67 |
+
if 4 <= index < 8:
|
| 68 |
+
return PyreAction(action="look", direction=DIRECTIONS[index - 4])
|
| 69 |
+
if index == 8:
|
| 70 |
+
return PyreAction(action="wait")
|
| 71 |
+
if 9 <= index < 9 + MAX_DOORS:
|
| 72 |
+
door_id = f"door_{index - 8}"
|
| 73 |
+
return PyreAction(action="door", target_id=door_id, door_state="open")
|
| 74 |
+
door_slot = index - (9 + MAX_DOORS)
|
| 75 |
+
door_id = f"door_{door_slot + 1}"
|
| 76 |
+
return PyreAction(action="door", target_id=door_id, door_state="close")
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def build_action_mask(observation: PyreObservation) -> np.ndarray:
|
| 80 |
+
mask = np.zeros(ACTION_DIM, dtype=np.float32)
|
| 81 |
+
for hint in observation.available_actions_hint:
|
| 82 |
+
idx = ACTION_TO_INDEX.get(hint)
|
| 83 |
+
if idx is not None:
|
| 84 |
+
mask[idx] = 1.0
|
| 85 |
+
continue
|
| 86 |
+
|
| 87 |
+
match = _MOVE_RE.fullmatch(hint)
|
| 88 |
+
if match:
|
| 89 |
+
mask[ACTION_TO_INDEX[f"move(direction='{match.group(1)}')"]] = 1.0
|
| 90 |
+
continue
|
| 91 |
+
|
| 92 |
+
match = _LOOK_RE.fullmatch(hint)
|
| 93 |
+
if match:
|
| 94 |
+
mask[ACTION_TO_INDEX[f"look(direction='{match.group(1)}')"]] = 1.0
|
| 95 |
+
continue
|
| 96 |
+
|
| 97 |
+
match = _DOOR_RE.fullmatch(hint)
|
| 98 |
+
if match:
|
| 99 |
+
door_id = match.group(1)
|
| 100 |
+
door_num = int(match.group(2))
|
| 101 |
+
state = match.group(3)
|
| 102 |
+
if 1 <= door_num <= MAX_DOORS:
|
| 103 |
+
mask[ACTION_TO_INDEX[f"door(target_id='{door_id}', door_state='{state}')"]] = 1.0
|
| 104 |
+
|
| 105 |
+
if mask.sum() == 0:
|
| 106 |
+
mask[ACTION_TO_INDEX[WAIT_KEY]] = 1.0
|
| 107 |
+
return mask
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
class ObservationEncoder:
|
| 111 |
+
"""Encode Pyre observations into a fixed-size float vector."""
|
| 112 |
+
|
| 113 |
+
def __init__(self, mode: str = "visible"):
|
| 114 |
+
if mode not in {"visible", "full"}:
|
| 115 |
+
raise ValueError(f"Unsupported observation mode: {mode}")
|
| 116 |
+
self.mode = mode
|
| 117 |
+
self.base_dim = MAX_GRID_W * MAX_GRID_H * 10 + 22
|
| 118 |
+
|
| 119 |
+
def encode(self, observation: PyreObservation) -> np.ndarray:
|
| 120 |
+
map_state = observation.map_state
|
| 121 |
+
if map_state is None:
|
| 122 |
+
raise ValueError("PyreObservation.map_state is required for RL training.")
|
| 123 |
+
|
| 124 |
+
cell_one_hot = np.zeros((MAX_GRID_H, MAX_GRID_W, 6), dtype=np.float32)
|
| 125 |
+
fire_channel = np.zeros((MAX_GRID_H, MAX_GRID_W), dtype=np.float32)
|
| 126 |
+
smoke_channel = np.zeros((MAX_GRID_H, MAX_GRID_W), dtype=np.float32)
|
| 127 |
+
visible_channel = np.zeros((MAX_GRID_H, MAX_GRID_W), dtype=np.float32)
|
| 128 |
+
agent_channel = np.zeros((MAX_GRID_H, MAX_GRID_W), dtype=np.float32)
|
| 129 |
+
|
| 130 |
+
visible = {(x, y) for x, y in map_state.visible_cells}
|
| 131 |
+
for y in range(map_state.grid_h):
|
| 132 |
+
for x in range(map_state.grid_w):
|
| 133 |
+
if self.mode == "visible" and (x, y) not in visible and (x, y) != (map_state.agent_x, map_state.agent_y):
|
| 134 |
+
continue
|
| 135 |
+
i = y * map_state.grid_w + x
|
| 136 |
+
cell_type = int(map_state.cell_grid[i])
|
| 137 |
+
if 0 <= cell_type <= 5:
|
| 138 |
+
cell_one_hot[y, x, cell_type] = 1.0
|
| 139 |
+
fire_channel[y, x] = float(map_state.fire_grid[i])
|
| 140 |
+
smoke_channel[y, x] = float(map_state.smoke_grid[i])
|
| 141 |
+
visible_channel[y, x] = 1.0 if (x, y) in visible else 0.0
|
| 142 |
+
|
| 143 |
+
if 0 <= map_state.agent_x < MAX_GRID_W and 0 <= map_state.agent_y < MAX_GRID_H:
|
| 144 |
+
agent_channel[map_state.agent_y, map_state.agent_x] = 1.0
|
| 145 |
+
|
| 146 |
+
grid_features = np.concatenate(
|
| 147 |
+
[
|
| 148 |
+
cell_one_hot.reshape(-1),
|
| 149 |
+
fire_channel.reshape(-1),
|
| 150 |
+
smoke_channel.reshape(-1),
|
| 151 |
+
visible_channel.reshape(-1),
|
| 152 |
+
agent_channel.reshape(-1),
|
| 153 |
+
]
|
| 154 |
+
)
|
| 155 |
+
|
| 156 |
+
metadata = observation.metadata or {}
|
| 157 |
+
wind_dir = str(metadata.get("wind_dir", map_state.wind_dir or "CALM")).upper()
|
| 158 |
+
difficulty = str(metadata.get("difficulty", "medium")).lower()
|
| 159 |
+
wind_index = WINDS.index(wind_dir) if wind_dir in WINDS else 0
|
| 160 |
+
difficulty_index = DIFFICULTIES.index(difficulty) if difficulty in DIFFICULTIES else 1
|
| 161 |
+
|
| 162 |
+
global_features = np.concatenate(
|
| 163 |
+
[
|
| 164 |
+
np.array(
|
| 165 |
+
[
|
| 166 |
+
float(observation.agent_health) / 100.0,
|
| 167 |
+
float(map_state.agent_health) / 100.0,
|
| 168 |
+
float(map_state.step_count) / max(1, map_state.max_steps),
|
| 169 |
+
float(map_state.fire_spread_rate),
|
| 170 |
+
float(map_state.humidity),
|
| 171 |
+
float(map_state.agent_x) / max(1, map_state.grid_w - 1),
|
| 172 |
+
float(map_state.agent_y) / max(1, map_state.grid_h - 1),
|
| 173 |
+
float(metadata.get("nearest_exit_distance", MAX_GRID_W + MAX_GRID_H) or 0.0) / float(MAX_GRID_W + MAX_GRID_H),
|
| 174 |
+
float(metadata.get("reachable_exit_count", 0.0)) / 4.0,
|
| 175 |
+
float(metadata.get("visible_cell_count", 0.0)) / float(MAX_GRID_W * MAX_GRID_H),
|
| 176 |
+
float(metadata.get("fire_sources", 0.0)) / 5.0,
|
| 177 |
+
{"none": 0.0, "light": 0.33, "moderate": 0.66, "heavy": 1.0}.get(observation.smoke_level, 0.0),
|
| 178 |
+
1.0 if map_state.agent_alive else 0.0,
|
| 179 |
+
1.0 if map_state.agent_evacuated else 0.0,
|
| 180 |
+
],
|
| 181 |
+
dtype=np.float32,
|
| 182 |
+
),
|
| 183 |
+
_one_hot(wind_index, len(WINDS)),
|
| 184 |
+
_one_hot(difficulty_index, len(DIFFICULTIES)),
|
| 185 |
+
]
|
| 186 |
+
)
|
| 187 |
+
|
| 188 |
+
return np.concatenate([grid_features, global_features]).astype(np.float32)
|
| 189 |
+
|
| 190 |
+
def describe(self, history_length: int) -> str:
|
| 191 |
+
grid_text = (
|
| 192 |
+
f"Observation mode `{self.mode}` encodes a {MAX_GRID_W}x{MAX_GRID_H} padded map with "
|
| 193 |
+
"10 channels per cell: 6-way cell type one-hot, fire intensity, smoke intensity, visible mask, and agent mask."
|
| 194 |
+
)
|
| 195 |
+
if self.mode == "visible":
|
| 196 |
+
visibility_text = "Only currently visible cells are populated; unseen cells stay zeroed."
|
| 197 |
+
else:
|
| 198 |
+
visibility_text = "The full ground-truth map is exposed for curriculum/debug use."
|
| 199 |
+
return (
|
| 200 |
+
f"{grid_text} {visibility_text} "
|
| 201 |
+
f"Global features add health, step progress, fire parameters, position, exit-distance metadata, smoke severity, wind, and difficulty. "
|
| 202 |
+
f"{history_length} encoded frames are stacked, so the network input dimension is {self.base_dim * history_length}."
|
| 203 |
+
)
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def softmax_with_mask(logits: np.ndarray, mask: np.ndarray) -> np.ndarray:
|
| 207 |
+
masked_logits = np.where(mask > 0.0, logits, -1e9)
|
| 208 |
+
max_logits = np.max(masked_logits, axis=1, keepdims=True)
|
| 209 |
+
exps = np.exp(masked_logits - max_logits) * mask
|
| 210 |
+
denom = np.sum(exps, axis=1, keepdims=True)
|
| 211 |
+
denom = np.where(denom <= 0.0, 1.0, denom)
|
| 212 |
+
return exps / denom
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
class AdamOptimizer:
|
| 216 |
+
def __init__(self, params: Dict[str, np.ndarray], lr: float = 3e-4, beta1: float = 0.9, beta2: float = 0.999):
|
| 217 |
+
self.lr = lr
|
| 218 |
+
self.beta1 = beta1
|
| 219 |
+
self.beta2 = beta2
|
| 220 |
+
self.eps = 1e-8
|
| 221 |
+
self.t = 0
|
| 222 |
+
self.m = {k: np.zeros_like(v) for k, v in params.items()}
|
| 223 |
+
self.v = {k: np.zeros_like(v) for k, v in params.items()}
|
| 224 |
+
|
| 225 |
+
def step(self, params: Dict[str, np.ndarray], grads: Dict[str, np.ndarray], clip_norm: float = 1.0) -> None:
|
| 226 |
+
total_norm_sq = 0.0
|
| 227 |
+
for grad in grads.values():
|
| 228 |
+
total_norm_sq += float(np.sum(grad * grad))
|
| 229 |
+
total_norm = math.sqrt(total_norm_sq)
|
| 230 |
+
scale = 1.0
|
| 231 |
+
if total_norm > clip_norm:
|
| 232 |
+
scale = clip_norm / (total_norm + 1e-8)
|
| 233 |
+
|
| 234 |
+
self.t += 1
|
| 235 |
+
for name, param in params.items():
|
| 236 |
+
grad = grads[name] * scale
|
| 237 |
+
self.m[name] = self.beta1 * self.m[name] + (1.0 - self.beta1) * grad
|
| 238 |
+
self.v[name] = self.beta2 * self.v[name] + (1.0 - self.beta2) * (grad * grad)
|
| 239 |
+
m_hat = self.m[name] / (1.0 - self.beta1 ** self.t)
|
| 240 |
+
v_hat = self.v[name] / (1.0 - self.beta2 ** self.t)
|
| 241 |
+
params[name] -= self.lr * m_hat / (np.sqrt(v_hat) + self.eps)
|
| 242 |
+
|
| 243 |
+
|
| 244 |
+
class PolicyValueNetwork:
|
| 245 |
+
def __init__(self, input_dim: int, action_dim: int, rng: np.random.Generator, hidden_sizes: Sequence[int] = (256, 128)):
|
| 246 |
+
h1, h2 = hidden_sizes
|
| 247 |
+
self.params: Dict[str, np.ndarray] = {
|
| 248 |
+
"w1": self._init_weight(rng, input_dim, h1),
|
| 249 |
+
"b1": np.zeros(h1, dtype=np.float32),
|
| 250 |
+
"w2": self._init_weight(rng, h1, h2),
|
| 251 |
+
"b2": np.zeros(h2, dtype=np.float32),
|
| 252 |
+
"wp": self._init_weight(rng, h2, action_dim),
|
| 253 |
+
"bp": np.zeros(action_dim, dtype=np.float32),
|
| 254 |
+
"wv": self._init_weight(rng, h2, 1),
|
| 255 |
+
"bv": np.zeros(1, dtype=np.float32),
|
| 256 |
+
}
|
| 257 |
+
self.optimizer = AdamOptimizer(self.params)
|
| 258 |
+
|
| 259 |
+
@staticmethod
|
| 260 |
+
def _init_weight(rng: np.random.Generator, in_dim: int, out_dim: int) -> np.ndarray:
|
| 261 |
+
scale = math.sqrt(2.0 / max(1, in_dim + out_dim))
|
| 262 |
+
return (rng.standard_normal((in_dim, out_dim)) * scale).astype(np.float32)
|
| 263 |
+
|
| 264 |
+
def forward(self, x: np.ndarray) -> tuple[np.ndarray, np.ndarray, Dict[str, np.ndarray]]:
|
| 265 |
+
z1 = x @ self.params["w1"] + self.params["b1"]
|
| 266 |
+
h1 = np.tanh(z1)
|
| 267 |
+
z2 = h1 @ self.params["w2"] + self.params["b2"]
|
| 268 |
+
h2 = np.tanh(z2)
|
| 269 |
+
logits = h2 @ self.params["wp"] + self.params["bp"]
|
| 270 |
+
values = (h2 @ self.params["wv"] + self.params["bv"]).reshape(-1)
|
| 271 |
+
cache = {"x": x, "h1": h1, "h2": h2}
|
| 272 |
+
return logits, values, cache
|
| 273 |
+
|
| 274 |
+
def predict(self, x: np.ndarray, mask: np.ndarray) -> tuple[np.ndarray, float]:
|
| 275 |
+
logits, values, _ = self.forward(x[None, :])
|
| 276 |
+
probs = softmax_with_mask(logits, mask[None, :])[0]
|
| 277 |
+
return probs, float(values[0])
|
| 278 |
+
|
| 279 |
+
def update(
|
| 280 |
+
self,
|
| 281 |
+
states: np.ndarray,
|
| 282 |
+
masks: np.ndarray,
|
| 283 |
+
actions: np.ndarray,
|
| 284 |
+
returns: np.ndarray,
|
| 285 |
+
advantages: np.ndarray,
|
| 286 |
+
value_coef: float = 0.5,
|
| 287 |
+
) -> Dict[str, float]:
|
| 288 |
+
logits, values, cache = self.forward(states)
|
| 289 |
+
probs = softmax_with_mask(logits, masks)
|
| 290 |
+
|
| 291 |
+
batch_size = max(1, states.shape[0])
|
| 292 |
+
grad_logits = probs.copy()
|
| 293 |
+
grad_logits[np.arange(batch_size), actions] -= 1.0
|
| 294 |
+
grad_logits *= advantages[:, None] / batch_size
|
| 295 |
+
grad_logits *= masks
|
| 296 |
+
|
| 297 |
+
grad_values = ((values - returns)[:, None] * value_coef) / batch_size
|
| 298 |
+
|
| 299 |
+
grads: Dict[str, np.ndarray] = {}
|
| 300 |
+
grads["wp"] = cache["h2"].T @ grad_logits
|
| 301 |
+
grads["bp"] = np.sum(grad_logits, axis=0)
|
| 302 |
+
grads["wv"] = cache["h2"].T @ grad_values
|
| 303 |
+
grads["bv"] = np.sum(grad_values, axis=0)
|
| 304 |
+
|
| 305 |
+
dh2 = grad_logits @ self.params["wp"].T + grad_values @ self.params["wv"].T
|
| 306 |
+
dz2 = dh2 * (1.0 - cache["h2"] ** 2)
|
| 307 |
+
grads["w2"] = cache["h1"].T @ dz2
|
| 308 |
+
grads["b2"] = np.sum(dz2, axis=0)
|
| 309 |
+
|
| 310 |
+
dh1 = dz2 @ self.params["w2"].T
|
| 311 |
+
dz1 = dh1 * (1.0 - cache["h1"] ** 2)
|
| 312 |
+
grads["w1"] = cache["x"].T @ dz1
|
| 313 |
+
grads["b1"] = np.sum(dz1, axis=0)
|
| 314 |
+
|
| 315 |
+
self.optimizer.step(self.params, grads, clip_norm=1.0)
|
| 316 |
+
|
| 317 |
+
chosen_probs = np.clip(probs[np.arange(batch_size), actions], 1e-8, 1.0)
|
| 318 |
+
policy_loss = float(-np.mean(advantages * np.log(chosen_probs)))
|
| 319 |
+
value_loss = float(0.5 * np.mean((values - returns) ** 2))
|
| 320 |
+
entropy = float(-np.mean(np.sum(np.where(probs > 0.0, probs * np.log(np.clip(probs, 1e-8, 1.0)), 0.0), axis=1)))
|
| 321 |
+
return {
|
| 322 |
+
"policy_loss": policy_loss,
|
| 323 |
+
"value_loss": value_loss,
|
| 324 |
+
"entropy": entropy,
|
| 325 |
+
"mean_value": float(np.mean(values)),
|
| 326 |
+
}
|
| 327 |
+
|
| 328 |
+
def save(self, path: Path, metadata: Dict[str, object]) -> None:
|
| 329 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 330 |
+
arrays = {name: value for name, value in self.params.items()}
|
| 331 |
+
arrays["metadata_json"] = np.array(json.dumps(metadata))
|
| 332 |
+
np.savez(path, **arrays)
|
| 333 |
+
|
| 334 |
+
|
| 335 |
+
@dataclass
|
| 336 |
+
class Trajectory:
|
| 337 |
+
states: List[np.ndarray]
|
| 338 |
+
masks: List[np.ndarray]
|
| 339 |
+
actions: List[int]
|
| 340 |
+
rewards: List[float]
|
| 341 |
+
values: List[float]
|
| 342 |
+
evacuated: bool
|
| 343 |
+
final_health: float
|
| 344 |
+
steps: int
|
| 345 |
+
total_reward: float
|
| 346 |
+
|
| 347 |
+
|
| 348 |
+
def compute_gae(
|
| 349 |
+
rewards: Sequence[float],
|
| 350 |
+
values: Sequence[float],
|
| 351 |
+
gamma: float,
|
| 352 |
+
gae_lambda: float,
|
| 353 |
+
) -> tuple[np.ndarray, np.ndarray]:
|
| 354 |
+
rewards_arr = np.asarray(rewards, dtype=np.float32)
|
| 355 |
+
values_arr = np.asarray(values, dtype=np.float32)
|
| 356 |
+
advantages = np.zeros(len(rewards_arr), dtype=np.float32)
|
| 357 |
+
gae = 0.0
|
| 358 |
+
next_value = 0.0
|
| 359 |
+
for i in range(len(rewards_arr) - 1, -1, -1):
|
| 360 |
+
delta = rewards_arr[i] + gamma * next_value - values_arr[i]
|
| 361 |
+
gae = delta + gamma * gae_lambda * gae
|
| 362 |
+
advantages[i] = gae
|
| 363 |
+
next_value = values_arr[i]
|
| 364 |
+
returns = advantages + values_arr
|
| 365 |
+
return returns.astype(np.float32), advantages.astype(np.float32)
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def select_action(
|
| 369 |
+
network: PolicyValueNetwork,
|
| 370 |
+
state_vec: np.ndarray,
|
| 371 |
+
mask: np.ndarray,
|
| 372 |
+
rng: np.random.Generator,
|
| 373 |
+
greedy: bool = False,
|
| 374 |
+
) -> tuple[int, float]:
|
| 375 |
+
probs, value = network.predict(state_vec, mask)
|
| 376 |
+
valid_indices = np.flatnonzero(mask > 0.0)
|
| 377 |
+
if len(valid_indices) == 0:
|
| 378 |
+
return ACTION_TO_INDEX[WAIT_KEY], value
|
| 379 |
+
if greedy:
|
| 380 |
+
best_local = int(np.argmax(probs[valid_indices]))
|
| 381 |
+
return int(valid_indices[best_local]), value
|
| 382 |
+
return int(rng.choice(np.arange(len(probs)), p=probs)), value
|
| 383 |
+
|
| 384 |
+
|
| 385 |
+
def build_stacked_state(frames: deque[np.ndarray]) -> np.ndarray:
|
| 386 |
+
return np.concatenate(list(frames), dtype=np.float32)
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
def run_episode(
|
| 390 |
+
env: PyreEnvironment,
|
| 391 |
+
network: PolicyValueNetwork,
|
| 392 |
+
encoder: ObservationEncoder,
|
| 393 |
+
rng: np.random.Generator,
|
| 394 |
+
difficulty: str,
|
| 395 |
+
history_length: int,
|
| 396 |
+
greedy: bool = False,
|
| 397 |
+
) -> Trajectory:
|
| 398 |
+
observation = env.reset(difficulty=difficulty)
|
| 399 |
+
zero_frame = np.zeros(encoder.base_dim, dtype=np.float32)
|
| 400 |
+
frames: deque[np.ndarray] = deque([zero_frame.copy() for _ in range(history_length)], maxlen=history_length)
|
| 401 |
+
frames.append(encoder.encode(observation))
|
| 402 |
+
|
| 403 |
+
states: List[np.ndarray] = []
|
| 404 |
+
masks: List[np.ndarray] = []
|
| 405 |
+
actions: List[int] = []
|
| 406 |
+
rewards: List[float] = []
|
| 407 |
+
values: List[float] = []
|
| 408 |
+
|
| 409 |
+
total_reward = 0.0
|
| 410 |
+
final_health = observation.agent_health
|
| 411 |
+
evacuated = False
|
| 412 |
+
steps = 0
|
| 413 |
+
|
| 414 |
+
while True:
|
| 415 |
+
state_vec = build_stacked_state(frames)
|
| 416 |
+
mask = build_action_mask(observation)
|
| 417 |
+
action_idx, value = select_action(network, state_vec, mask, rng, greedy=greedy)
|
| 418 |
+
action = action_index_to_env_action(action_idx)
|
| 419 |
+
|
| 420 |
+
next_obs = env.step(action)
|
| 421 |
+
reward = float(next_obs.reward or 0.0)
|
| 422 |
+
|
| 423 |
+
states.append(state_vec)
|
| 424 |
+
masks.append(mask)
|
| 425 |
+
actions.append(action_idx)
|
| 426 |
+
rewards.append(reward)
|
| 427 |
+
values.append(value)
|
| 428 |
+
|
| 429 |
+
total_reward += reward
|
| 430 |
+
steps += 1
|
| 431 |
+
final_health = next_obs.agent_health
|
| 432 |
+
evacuated = next_obs.agent_evacuated
|
| 433 |
+
|
| 434 |
+
frames.append(encoder.encode(next_obs))
|
| 435 |
+
observation = next_obs
|
| 436 |
+
if next_obs.done:
|
| 437 |
+
break
|
| 438 |
+
|
| 439 |
+
return Trajectory(
|
| 440 |
+
states=states,
|
| 441 |
+
masks=masks,
|
| 442 |
+
actions=actions,
|
| 443 |
+
rewards=rewards,
|
| 444 |
+
values=values,
|
| 445 |
+
evacuated=evacuated,
|
| 446 |
+
final_health=final_health,
|
| 447 |
+
steps=steps,
|
| 448 |
+
total_reward=total_reward,
|
| 449 |
+
)
|
| 450 |
+
|
| 451 |
+
|
| 452 |
+
def evaluate_policy(
|
| 453 |
+
env: PyreEnvironment,
|
| 454 |
+
network: PolicyValueNetwork,
|
| 455 |
+
encoder: ObservationEncoder,
|
| 456 |
+
rng: np.random.Generator,
|
| 457 |
+
difficulty: str,
|
| 458 |
+
history_length: int,
|
| 459 |
+
episodes: int,
|
| 460 |
+
) -> Dict[str, float]:
|
| 461 |
+
rewards = []
|
| 462 |
+
evacuations = 0
|
| 463 |
+
lengths = []
|
| 464 |
+
for _ in range(episodes):
|
| 465 |
+
traj = run_episode(env, network, encoder, rng, difficulty, history_length, greedy=True)
|
| 466 |
+
rewards.append(traj.total_reward)
|
| 467 |
+
lengths.append(traj.steps)
|
| 468 |
+
evacuations += int(traj.evacuated)
|
| 469 |
+
return {
|
| 470 |
+
"eval_reward_mean": float(np.mean(rewards)) if rewards else 0.0,
|
| 471 |
+
"eval_reward_max": float(np.max(rewards)) if rewards else 0.0,
|
| 472 |
+
"eval_success_rate": float(evacuations / max(1, episodes)),
|
| 473 |
+
"eval_steps_mean": float(np.mean(lengths)) if lengths else 0.0,
|
| 474 |
+
}
|
| 475 |
+
|
| 476 |
+
|
| 477 |
+
def expand_difficulty_schedule(schedule_text: str, episodes: int) -> List[str]:
|
| 478 |
+
stages = [part.strip().lower() for part in schedule_text.split(",") if part.strip()]
|
| 479 |
+
if not stages:
|
| 480 |
+
stages = ["medium"]
|
| 481 |
+
for stage in stages:
|
| 482 |
+
if stage not in DIFFICULTIES:
|
| 483 |
+
raise ValueError(f"Invalid difficulty in schedule: {stage}")
|
| 484 |
+
segment = max(1, episodes // len(stages))
|
| 485 |
+
expanded: List[str] = []
|
| 486 |
+
for stage in stages:
|
| 487 |
+
expanded.extend([stage] * segment)
|
| 488 |
+
while len(expanded) < episodes:
|
| 489 |
+
expanded.append(stages[-1])
|
| 490 |
+
return expanded[:episodes]
|
| 491 |
+
|
| 492 |
+
|
| 493 |
+
def describe_environment_contract(encoder: ObservationEncoder, history_length: int) -> str:
|
| 494 |
+
action_text = (
|
| 495 |
+
f"Action space has {ACTION_DIM} fixed discrete actions: 4 moves, 4 looks, wait, "
|
| 496 |
+
f"{MAX_DOORS} door-open slots, and {MAX_DOORS} door-close slots. "
|
| 497 |
+
"A per-step mask from `available_actions_hint` prevents invalid actions."
|
| 498 |
+
)
|
| 499 |
+
reward_text = (
|
| 500 |
+
"Reward comes directly from the environment's composite rubric: time penalty, exit progress, "
|
| 501 |
+
"progress regression penalty, safe-progress bonus, danger penalty, health-drain penalty, "
|
| 502 |
+
"strategic door bonus, exploration bonus, plus terminal evacuation/death/timeout/near-miss/time bonuses."
|
| 503 |
+
)
|
| 504 |
+
return "\n".join(
|
| 505 |
+
[
|
| 506 |
+
"Pyre RL contract",
|
| 507 |
+
encoder.describe(history_length),
|
| 508 |
+
action_text,
|
| 509 |
+
reward_text,
|
| 510 |
+
]
|
| 511 |
+
)
|
| 512 |
+
|
| 513 |
+
|
| 514 |
+
def _moving_average(values: Sequence[float], window: int) -> List[float]:
|
| 515 |
+
if not values:
|
| 516 |
+
return []
|
| 517 |
+
out: List[float] = []
|
| 518 |
+
run = 0.0
|
| 519 |
+
q: deque[float] = deque()
|
| 520 |
+
for value in values:
|
| 521 |
+
q.append(float(value))
|
| 522 |
+
run += float(value)
|
| 523 |
+
if len(q) > window:
|
| 524 |
+
run -= q.popleft()
|
| 525 |
+
out.append(run / len(q))
|
| 526 |
+
return out
|
| 527 |
+
|
| 528 |
+
|
| 529 |
+
def save_metrics_csv(path: Path, rows: List[Dict[str, float | int | str]]) -> None:
|
| 530 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 531 |
+
if not rows:
|
| 532 |
+
return
|
| 533 |
+
with path.open("w", newline="", encoding="utf-8") as f:
|
| 534 |
+
writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
|
| 535 |
+
writer.writeheader()
|
| 536 |
+
writer.writerows(rows)
|
| 537 |
+
|
| 538 |
+
|
| 539 |
+
def save_training_graph(path: Path, episode_rows: List[Dict[str, float | int | str]], eval_rows: List[Dict[str, float | int | str]]) -> None:
|
| 540 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 541 |
+
if not episode_rows:
|
| 542 |
+
return
|
| 543 |
+
|
| 544 |
+
width = 1260
|
| 545 |
+
height = 780
|
| 546 |
+
margin_left = 100 # extra room for rotated Y-axis label + tick values
|
| 547 |
+
margin_right = 110 # extra room for right axis label + tick values
|
| 548 |
+
margin_top = 70 # room for title
|
| 549 |
+
margin_bottom = 90 # room for X-axis label + tick values + legend
|
| 550 |
+
plot_w = width - margin_left - margin_right
|
| 551 |
+
plot_h = height - margin_top - margin_bottom
|
| 552 |
+
|
| 553 |
+
# X: plot_left=100, plot_right=1150 Y: plot_top=70, plot_bottom=690
|
| 554 |
+
|
| 555 |
+
episodes = [int(r["episode"]) for r in episode_rows]
|
| 556 |
+
rewards = [float(r["reward"]) for r in episode_rows]
|
| 557 |
+
reward_ma = _moving_average(rewards, 20)
|
| 558 |
+
success_ma = _moving_average([float(r["evacuated"]) for r in episode_rows], 20)
|
| 559 |
+
|
| 560 |
+
all_reward_values = rewards + reward_ma + [float(r["reward_mean"]) for r in eval_rows] + [float(r["reward_max"]) for r in eval_rows]
|
| 561 |
+
y_min = min(all_reward_values) if all_reward_values else -1.0
|
| 562 |
+
y_max = max(all_reward_values) if all_reward_values else 1.0
|
| 563 |
+
if abs(y_max - y_min) < 1e-6:
|
| 564 |
+
y_min -= 1.0
|
| 565 |
+
y_max += 1.0
|
| 566 |
+
y_pad = 0.1 * (y_max - y_min)
|
| 567 |
+
y_min -= y_pad
|
| 568 |
+
y_max += y_pad
|
| 569 |
+
|
| 570 |
+
max_episode = max(episodes) if episodes else 1
|
| 571 |
+
|
| 572 |
+
plot_left = margin_left
|
| 573 |
+
plot_right = margin_left + plot_w
|
| 574 |
+
plot_top = margin_top
|
| 575 |
+
plot_bottom = margin_top + plot_h
|
| 576 |
+
|
| 577 |
+
def x_pos(ep: float) -> float:
|
| 578 |
+
return plot_left + (float(ep) - 1.0) / max(1.0, max_episode - 1.0) * plot_w
|
| 579 |
+
|
| 580 |
+
def y_pos_reward(value: float) -> float:
|
| 581 |
+
return plot_top + (y_max - float(value)) / max(1e-6, (y_max - y_min)) * plot_h
|
| 582 |
+
|
| 583 |
+
def y_pos_success(value: float) -> float:
|
| 584 |
+
return plot_top + (1.0 - float(value)) * plot_h
|
| 585 |
+
|
| 586 |
+
def polyline(points: List[tuple[float, float]]) -> str:
|
| 587 |
+
return " ".join(f"{x:.1f},{y:.1f}" for x, y in points)
|
| 588 |
+
|
| 589 |
+
reward_points = [(x_pos(ep), y_pos_reward(val)) for ep, val in zip(episodes, rewards)]
|
| 590 |
+
reward_ma_points = [(x_pos(ep), y_pos_reward(val)) for ep, val in zip(episodes, reward_ma)]
|
| 591 |
+
success_points = [(x_pos(ep), y_pos_success(val)) for ep, val in zip(episodes, success_ma)]
|
| 592 |
+
eval_points = [(x_pos(float(r["episode"])), y_pos_success(float(r["success_rate"]))) for r in eval_rows]
|
| 593 |
+
|
| 594 |
+
n_x_ticks = 8
|
| 595 |
+
episode_ticks = sorted(set(
|
| 596 |
+
max(1, round(1 + i * (max_episode - 1) / n_x_ticks))
|
| 597 |
+
for i in range(n_x_ticks + 1)
|
| 598 |
+
))
|
| 599 |
+
n_y_ticks = 6
|
| 600 |
+
reward_ticks = [y_min + (y_max - y_min) * i / n_y_ticks for i in range(n_y_ticks + 1)]
|
| 601 |
+
success_ticks = [0.0, 0.2, 0.4, 0.6, 0.8, 1.0]
|
| 602 |
+
|
| 603 |
+
svg = []
|
| 604 |
+
svg.append(f'<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="{height}" viewBox="0 0 {width} {height}">')
|
| 605 |
+
|
| 606 |
+
# Background
|
| 607 |
+
svg.append('<rect width="100%" height="100%" fill="#f7f5ef"/>')
|
| 608 |
+
|
| 609 |
+
# Title + subtitle
|
| 610 |
+
svg.append(f'<text x="{plot_left}" y="28" font-family="Georgia, serif" font-size="22" font-weight="bold" fill="#1d2a38">Pyre RL Training</text>')
|
| 611 |
+
svg.append(f'<text x="{plot_left}" y="50" font-family="Georgia, serif" font-size="13" fill="#5b6770">Left axis: Reward | Right axis: Success Rate (0–1)</text>')
|
| 612 |
+
|
| 613 |
+
# Plot area background + border
|
| 614 |
+
svg.append(f'<rect x="{plot_left}" y="{plot_top}" width="{plot_w}" height="{plot_h}" fill="#fffdf8" stroke="#b8b0a2" stroke-width="1.5"/>')
|
| 615 |
+
|
| 616 |
+
# ── Vertical grid lines + X-axis ticks ──────────────────────────────────
|
| 617 |
+
for tick in episode_ticks:
|
| 618 |
+
x = x_pos(float(tick))
|
| 619 |
+
# dashed grid line
|
| 620 |
+
svg.append(f'<line x1="{x:.1f}" y1="{plot_top}" x2="{x:.1f}" y2="{plot_bottom}" '
|
| 621 |
+
f'stroke="#d8d2c8" stroke-width="1" stroke-dasharray="4,4"/>')
|
| 622 |
+
# solid tick mark on bottom axis
|
| 623 |
+
svg.append(f'<line x1="{x:.1f}" y1="{plot_bottom}" x2="{x:.1f}" y2="{plot_bottom + 6}" '
|
| 624 |
+
f'stroke="#6b6460" stroke-width="1.5"/>')
|
| 625 |
+
# tick label
|
| 626 |
+
svg.append(f'<text x="{x:.1f}" y="{plot_bottom + 20}" text-anchor="middle" '
|
| 627 |
+
f'font-family="Georgia, serif" font-size="12" fill="#4a4540">{tick}</text>')
|
| 628 |
+
|
| 629 |
+
# X-axis title
|
| 630 |
+
x_title_x = plot_left + plot_w / 2
|
| 631 |
+
x_title_y = plot_bottom + 50
|
| 632 |
+
svg.append(f'<text x="{x_title_x:.1f}" y="{x_title_y}" text-anchor="middle" '
|
| 633 |
+
f'font-family="Georgia, serif" font-size="14" font-weight="bold" fill="#1d2a38">Episode</text>')
|
| 634 |
+
|
| 635 |
+
# ── Horizontal grid lines + Left Y-axis ticks (Reward) ──────────────────
|
| 636 |
+
for tick in reward_ticks:
|
| 637 |
+
y = y_pos_reward(tick)
|
| 638 |
+
# dashed grid line
|
| 639 |
+
svg.append(f'<line x1="{plot_left}" y1="{y:.1f}" x2="{plot_right}" y2="{y:.1f}" '
|
| 640 |
+
f'stroke="#d8d2c8" stroke-width="1" stroke-dasharray="4,4"/>')
|
| 641 |
+
# solid tick mark on left axis
|
| 642 |
+
svg.append(f'<line x1="{plot_left - 6}" y1="{y:.1f}" x2="{plot_left}" y2="{y:.1f}" '
|
| 643 |
+
f'stroke="#6b6460" stroke-width="1.5"/>')
|
| 644 |
+
# tick label
|
| 645 |
+
svg.append(f'<text x="{plot_left - 10}" y="{y + 4:.1f}" text-anchor="end" '
|
| 646 |
+
f'font-family="Georgia, serif" font-size="12" fill="#8a4b08">{tick:.1f}</text>')
|
| 647 |
+
|
| 648 |
+
# Left Y-axis title (rotated) — centered on plot height
|
| 649 |
+
ly_cx = plot_left - 70
|
| 650 |
+
ly_cy = plot_top + plot_h / 2
|
| 651 |
+
svg.append(f'<text transform="rotate(-90, {ly_cx:.1f}, {ly_cy:.1f})" '
|
| 652 |
+
f'x="{ly_cx:.1f}" y="{ly_cy:.1f}" text-anchor="middle" '
|
| 653 |
+
f'font-family="Georgia, serif" font-size="14" font-weight="bold" fill="#8a4b08">Reward</text>')
|
| 654 |
+
|
| 655 |
+
# ── Right Y-axis ticks (Success Rate) ───────────────────────────────────
|
| 656 |
+
for tick in success_ticks:
|
| 657 |
+
y = y_pos_success(tick)
|
| 658 |
+
# solid tick mark on right axis
|
| 659 |
+
svg.append(f'<line x1="{plot_right}" y1="{y:.1f}" x2="{plot_right + 6}" y2="{y:.1f}" '
|
| 660 |
+
f'stroke="#6b6460" stroke-width="1.5"/>')
|
| 661 |
+
# tick label
|
| 662 |
+
svg.append(f'<text x="{plot_right + 12}" y="{y + 4:.1f}" '
|
| 663 |
+
f'font-family="Georgia, serif" font-size="12" fill="#0d5b6b">{tick:.2f}</text>')
|
| 664 |
+
|
| 665 |
+
# Right Y-axis title (rotated)
|
| 666 |
+
ry_cx = plot_right + 85
|
| 667 |
+
ry_cy = plot_top + plot_h / 2
|
| 668 |
+
svg.append(f'<text transform="rotate(90, {ry_cx:.1f}, {ry_cy:.1f})" '
|
| 669 |
+
f'x="{ry_cx:.1f}" y="{ry_cy:.1f}" text-anchor="middle" '
|
| 670 |
+
f'font-family="Georgia, serif" font-size="14" font-weight="bold" fill="#0d5b6b">Success Rate</text>')
|
| 671 |
+
|
| 672 |
+
# ── Axis border lines (solid, on top of grid) ────────────────────────────
|
| 673 |
+
# Bottom axis
|
| 674 |
+
svg.append(f'<line x1="{plot_left}" y1="{plot_bottom}" x2="{plot_right}" y2="{plot_bottom}" '
|
| 675 |
+
f'stroke="#6b6460" stroke-width="2"/>')
|
| 676 |
+
# Left axis
|
| 677 |
+
svg.append(f'<line x1="{plot_left}" y1="{plot_top}" x2="{plot_left}" y2="{plot_bottom}" '
|
| 678 |
+
f'stroke="#6b6460" stroke-width="2"/>')
|
| 679 |
+
# Right axis
|
| 680 |
+
svg.append(f'<line x1="{plot_right}" y1="{plot_top}" x2="{plot_right}" y2="{plot_bottom}" '
|
| 681 |
+
f'stroke="#6b6460" stroke-width="2"/>')
|
| 682 |
+
|
| 683 |
+
# ── Data series ─────────────────────────────────────────────────────────
|
| 684 |
+
# Raw episode reward (faint)
|
| 685 |
+
svg.append(f'<polyline fill="none" stroke="#c5bfb1" stroke-width="1.5" points="{polyline(reward_points)}"/>')
|
| 686 |
+
# Reward moving average
|
| 687 |
+
svg.append(f'<polyline fill="none" stroke="#c1661c" stroke-width="3" stroke-linejoin="round" points="{polyline(reward_ma_points)}"/>')
|
| 688 |
+
# Success moving average
|
| 689 |
+
svg.append(f'<polyline fill="none" stroke="#127a8a" stroke-width="3" stroke-linejoin="round" points="{polyline(success_points)}"/>')
|
| 690 |
+
# Eval checkpoints
|
| 691 |
+
for x, y in eval_points:
|
| 692 |
+
svg.append(f'<circle cx="{x:.1f}" cy="{y:.1f}" r="5" fill="#0d5b6b" stroke="#ffffff" stroke-width="2"/>')
|
| 693 |
+
|
| 694 |
+
# ── Legend ───────────────────────────────────────────────────────────────
|
| 695 |
+
legend_y = plot_bottom + 72
|
| 696 |
+
items = [
|
| 697 |
+
("#c1661c", 3, False, "Reward (moving avg)"),
|
| 698 |
+
("#127a8a", 3, False, "Success rate (moving avg)"),
|
| 699 |
+
("#c5bfb1", 1.5, False, "Episode reward"),
|
| 700 |
+
("#0d5b6b", 0, True, "Eval success checkpoint"),
|
| 701 |
+
]
|
| 702 |
+
lx = plot_left
|
| 703 |
+
for color, sw, is_dot, label in items:
|
| 704 |
+
if is_dot:
|
| 705 |
+
svg.append(f'<circle cx="{lx + 15}" cy="{legend_y - 4}" r="5" fill="{color}" stroke="#ffffff" stroke-width="2"/>')
|
| 706 |
+
else:
|
| 707 |
+
svg.append(f'<line x1="{lx}" y1="{legend_y - 4}" x2="{lx + 30}" y2="{legend_y - 4}" stroke="{color}" stroke-width="{sw}"/>')
|
| 708 |
+
svg.append(f'<text x="{lx + 36}" y="{legend_y}" font-family="Georgia, serif" font-size="12" fill="#1d2a38">{label}</text>')
|
| 709 |
+
lx += 230
|
| 710 |
+
|
| 711 |
+
svg.append("</svg>")
|
| 712 |
+
path.write_text("\n".join(svg), encoding="utf-8")
|
| 713 |
+
|
| 714 |
+
|
| 715 |
+
def train(args: argparse.Namespace) -> None:
|
| 716 |
+
rng = np.random.default_rng(args.seed)
|
| 717 |
+
encoder = ObservationEncoder(mode=args.observation_mode)
|
| 718 |
+
difficulty_schedule = expand_difficulty_schedule(args.difficulty_schedule, args.episodes)
|
| 719 |
+
input_dim = encoder.base_dim * args.history_length
|
| 720 |
+
network = PolicyValueNetwork(input_dim=input_dim, action_dim=ACTION_DIM, rng=rng)
|
| 721 |
+
env = PyreEnvironment(max_steps=args.max_steps)
|
| 722 |
+
|
| 723 |
+
print(describe_environment_contract(encoder, args.history_length))
|
| 724 |
+
print("")
|
| 725 |
+
|
| 726 |
+
batch_states: List[np.ndarray] = []
|
| 727 |
+
batch_masks: List[np.ndarray] = []
|
| 728 |
+
batch_actions: List[int] = []
|
| 729 |
+
batch_returns: List[np.ndarray] = []
|
| 730 |
+
batch_advantages: List[np.ndarray] = []
|
| 731 |
+
|
| 732 |
+
reward_window: deque[float] = deque(maxlen=20)
|
| 733 |
+
success_window: deque[float] = deque(maxlen=20)
|
| 734 |
+
episode_metrics: List[Dict[str, float | int | str]] = []
|
| 735 |
+
eval_metrics_rows: List[Dict[str, float | int | str]] = []
|
| 736 |
+
|
| 737 |
+
for episode_idx in range(args.episodes):
|
| 738 |
+
difficulty = difficulty_schedule[episode_idx] if args.difficulty_schedule else args.difficulty
|
| 739 |
+
traj = run_episode(
|
| 740 |
+
env=env,
|
| 741 |
+
network=network,
|
| 742 |
+
encoder=encoder,
|
| 743 |
+
rng=rng,
|
| 744 |
+
difficulty=difficulty,
|
| 745 |
+
history_length=args.history_length,
|
| 746 |
+
greedy=False,
|
| 747 |
+
)
|
| 748 |
+
|
| 749 |
+
returns, advantages = compute_gae(traj.rewards, traj.values, args.gamma, args.gae_lambda)
|
| 750 |
+
batch_states.extend(traj.states)
|
| 751 |
+
batch_masks.extend(traj.masks)
|
| 752 |
+
batch_actions.extend(traj.actions)
|
| 753 |
+
batch_returns.append(returns)
|
| 754 |
+
batch_advantages.append(advantages)
|
| 755 |
+
|
| 756 |
+
reward_window.append(traj.total_reward)
|
| 757 |
+
success_window.append(float(traj.evacuated))
|
| 758 |
+
episode_metrics.append(
|
| 759 |
+
{
|
| 760 |
+
"episode": episode_idx + 1,
|
| 761 |
+
"difficulty": difficulty,
|
| 762 |
+
"reward": round(traj.total_reward, 4),
|
| 763 |
+
"evacuated": int(traj.evacuated),
|
| 764 |
+
"steps": traj.steps,
|
| 765 |
+
"final_health": round(traj.final_health, 2),
|
| 766 |
+
"reward_mean_20": round(float(np.mean(reward_window)), 4),
|
| 767 |
+
"success_rate_20": round(float(np.mean(success_window)), 4),
|
| 768 |
+
}
|
| 769 |
+
)
|
| 770 |
+
|
| 771 |
+
print(
|
| 772 |
+
f"episode={episode_idx + 1:04d} difficulty={difficulty:<6} "
|
| 773 |
+
f"steps={traj.steps:03d} reward={traj.total_reward:+8.3f} "
|
| 774 |
+
f"evacuated={int(traj.evacuated)} health={traj.final_health:6.1f}"
|
| 775 |
+
)
|
| 776 |
+
|
| 777 |
+
should_update = (episode_idx + 1) % args.update_every == 0 or (episode_idx + 1) == args.episodes
|
| 778 |
+
if should_update and batch_states:
|
| 779 |
+
states_arr = np.asarray(batch_states, dtype=np.float32)
|
| 780 |
+
masks_arr = np.asarray(batch_masks, dtype=np.float32)
|
| 781 |
+
actions_arr = np.asarray(batch_actions, dtype=np.int64)
|
| 782 |
+
returns_arr = np.concatenate(batch_returns).astype(np.float32)
|
| 783 |
+
advantages_arr = np.concatenate(batch_advantages).astype(np.float32)
|
| 784 |
+
advantages_arr = (advantages_arr - advantages_arr.mean()) / (advantages_arr.std() + 1e-8)
|
| 785 |
+
|
| 786 |
+
network.optimizer.lr = args.learning_rate
|
| 787 |
+
metrics = {}
|
| 788 |
+
for _ in range(args.update_epochs):
|
| 789 |
+
order = rng.permutation(len(states_arr))
|
| 790 |
+
for start in range(0, len(states_arr), args.minibatch_size):
|
| 791 |
+
idx = order[start:start + args.minibatch_size]
|
| 792 |
+
metrics = network.update(
|
| 793 |
+
states=states_arr[idx],
|
| 794 |
+
masks=masks_arr[idx],
|
| 795 |
+
actions=actions_arr[idx],
|
| 796 |
+
returns=returns_arr[idx],
|
| 797 |
+
advantages=advantages_arr[idx],
|
| 798 |
+
value_coef=args.value_coef,
|
| 799 |
+
)
|
| 800 |
+
|
| 801 |
+
print(
|
| 802 |
+
f"update episodes={episode_idx + 1:04d} samples={len(states_arr):05d} "
|
| 803 |
+
f"reward_mean20={np.mean(reward_window):+8.3f} success20={np.mean(success_window):.2f} "
|
| 804 |
+
f"policy_loss={metrics['policy_loss']:+.4f} value_loss={metrics['value_loss']:.4f} "
|
| 805 |
+
f"entropy={metrics['entropy']:.4f}"
|
| 806 |
+
)
|
| 807 |
+
|
| 808 |
+
batch_states.clear()
|
| 809 |
+
batch_masks.clear()
|
| 810 |
+
batch_actions.clear()
|
| 811 |
+
batch_returns.clear()
|
| 812 |
+
batch_advantages.clear()
|
| 813 |
+
|
| 814 |
+
should_eval = args.eval_every > 0 and ((episode_idx + 1) % args.eval_every == 0 or (episode_idx + 1) == args.episodes)
|
| 815 |
+
if should_eval:
|
| 816 |
+
eval_metrics = evaluate_policy(
|
| 817 |
+
env=env,
|
| 818 |
+
network=network,
|
| 819 |
+
encoder=encoder,
|
| 820 |
+
rng=rng,
|
| 821 |
+
difficulty=args.eval_difficulty,
|
| 822 |
+
history_length=args.history_length,
|
| 823 |
+
episodes=args.eval_episodes,
|
| 824 |
+
)
|
| 825 |
+
print(
|
| 826 |
+
f"eval episodes={episode_idx + 1:04d} difficulty={args.eval_difficulty:<6} "
|
| 827 |
+
f"reward_mean={eval_metrics['eval_reward_mean']:+8.3f} "
|
| 828 |
+
f"reward_max={eval_metrics['eval_reward_max']:+8.3f} "
|
| 829 |
+
f"success={eval_metrics['eval_success_rate']:.2f} "
|
| 830 |
+
f"steps={eval_metrics['eval_steps_mean']:.1f}"
|
| 831 |
+
)
|
| 832 |
+
eval_metrics_rows.append(
|
| 833 |
+
{
|
| 834 |
+
"episode": episode_idx + 1,
|
| 835 |
+
"difficulty": args.eval_difficulty,
|
| 836 |
+
"reward_mean": round(eval_metrics["eval_reward_mean"], 4),
|
| 837 |
+
"reward_max": round(eval_metrics["eval_reward_max"], 4),
|
| 838 |
+
"success_rate": round(eval_metrics["eval_success_rate"], 4),
|
| 839 |
+
"steps_mean": round(eval_metrics["eval_steps_mean"], 4),
|
| 840 |
+
}
|
| 841 |
+
)
|
| 842 |
+
|
| 843 |
+
if args.output:
|
| 844 |
+
output_path = Path(args.output)
|
| 845 |
+
network.save(
|
| 846 |
+
output_path,
|
| 847 |
+
metadata={
|
| 848 |
+
"observation_mode": args.observation_mode,
|
| 849 |
+
"history_length": args.history_length,
|
| 850 |
+
"episodes": args.episodes,
|
| 851 |
+
"difficulty": args.difficulty,
|
| 852 |
+
"difficulty_schedule": args.difficulty_schedule,
|
| 853 |
+
"gamma": args.gamma,
|
| 854 |
+
"gae_lambda": args.gae_lambda,
|
| 855 |
+
"learning_rate": args.learning_rate,
|
| 856 |
+
"update_epochs": args.update_epochs,
|
| 857 |
+
"minibatch_size": args.minibatch_size,
|
| 858 |
+
"action_dim": ACTION_DIM,
|
| 859 |
+
"input_dim": input_dim,
|
| 860 |
+
},
|
| 861 |
+
)
|
| 862 |
+
print(f"saved model={output_path}")
|
| 863 |
+
if args.save_metrics:
|
| 864 |
+
metrics_path = output_path.with_suffix(".csv")
|
| 865 |
+
save_metrics_csv(metrics_path, episode_metrics)
|
| 866 |
+
print(f"saved metrics={metrics_path}")
|
| 867 |
+
if args.save_graph:
|
| 868 |
+
graph_path = output_path.with_suffix(".svg")
|
| 869 |
+
save_training_graph(graph_path, episode_metrics, eval_metrics_rows)
|
| 870 |
+
print(f"saved graph={graph_path}")
|
| 871 |
+
# Also save PNG
|
| 872 |
+
try:
|
| 873 |
+
import matplotlib
|
| 874 |
+
matplotlib.use("Agg")
|
| 875 |
+
import matplotlib.pyplot as plt
|
| 876 |
+
import matplotlib.ticker as mticker
|
| 877 |
+
import matplotlib.patches as mpatches
|
| 878 |
+
|
| 879 |
+
episodes_list = [int(r["episode"]) for r in episode_metrics]
|
| 880 |
+
rewards_list = [float(r["reward"]) for r in episode_metrics]
|
| 881 |
+
evacuated_list = [float(r["evacuated"]) for r in episode_metrics]
|
| 882 |
+
diff_list = [str(r["difficulty"]) for r in episode_metrics]
|
| 883 |
+
|
| 884 |
+
def _ma(vals, w=20):
|
| 885 |
+
out, run, q = [], 0.0, []
|
| 886 |
+
for v in vals:
|
| 887 |
+
q.append(v); run += v
|
| 888 |
+
if len(q) > w: run -= q.pop(0)
|
| 889 |
+
out.append(run / len(q))
|
| 890 |
+
return out
|
| 891 |
+
|
| 892 |
+
reward_ma = _ma(rewards_list)
|
| 893 |
+
success_ma = _ma(evacuated_list)
|
| 894 |
+
eval_eps = [int(r["episode"]) for r in eval_metrics_rows]
|
| 895 |
+
eval_succ = [float(r["success_rate"]) for r in eval_metrics_rows]
|
| 896 |
+
|
| 897 |
+
diff_colors = {"easy": "#d4edda", "medium": "#fff3cd", "hard": "#f8d7da"}
|
| 898 |
+
regions = []
|
| 899 |
+
if diff_list:
|
| 900 |
+
cur, start = diff_list[0], episodes_list[0]
|
| 901 |
+
for ep, d in zip(episodes_list[1:], diff_list[1:]):
|
| 902 |
+
if d != cur:
|
| 903 |
+
regions.append((start, ep, cur)); cur, start = d, ep
|
| 904 |
+
regions.append((start, episodes_list[-1], cur))
|
| 905 |
+
|
| 906 |
+
fig, ax1 = plt.subplots(figsize=(14, 6))
|
| 907 |
+
ax2 = ax1.twinx()
|
| 908 |
+
for x0, x1, diff in regions:
|
| 909 |
+
ax1.axvspan(x0, x1, color=diff_colors.get(diff, "#eeeeee"), alpha=0.35, zorder=0)
|
| 910 |
+
ax1.axhline(0, color="#aaaaaa", linewidth=0.8, linestyle="--", zorder=1)
|
| 911 |
+
ax1.plot(episodes_list, rewards_list, color="#d1c7bc", linewidth=0.8, alpha=0.6, label="Episode reward", zorder=2)
|
| 912 |
+
ax1.plot(episodes_list, reward_ma, color="#c1661c", linewidth=2.5, label="Reward (MA-20)", zorder=3)
|
| 913 |
+
ax2.plot(episodes_list, success_ma, color="#1a7a8a", linewidth=2.5, label="Success rate (MA-20)", zorder=3)
|
| 914 |
+
if eval_eps:
|
| 915 |
+
ax2.scatter(eval_eps, eval_succ, color="#0d5b6b", s=60, zorder=5, marker="D", edgecolors="white", linewidths=1.2, label="Eval success")
|
| 916 |
+
ax1.set_xlabel("Episode", fontsize=13, fontweight="bold", labelpad=8)
|
| 917 |
+
ax1.set_ylabel("Reward", fontsize=13, fontweight="bold", color="#c1661c", labelpad=8)
|
| 918 |
+
ax2.set_ylabel("Success Rate", fontsize=13, fontweight="bold", color="#1a7a8a", labelpad=8)
|
| 919 |
+
ax1.tick_params(axis="y", labelcolor="#c1661c")
|
| 920 |
+
ax2.tick_params(axis="y", labelcolor="#1a7a8a")
|
| 921 |
+
ax2.set_ylim(-0.05, 1.05)
|
| 922 |
+
ax2.yaxis.set_major_formatter(mticker.PercentFormatter(xmax=1.0, decimals=0))
|
| 923 |
+
ax1.grid(True, linestyle="--", linewidth=0.6, color="#dddddd", alpha=0.8)
|
| 924 |
+
ax1.set_xlim(episodes_list[0], episodes_list[-1])
|
| 925 |
+
diff_patches = [mpatches.Patch(color=diff_colors[d], alpha=0.6, label=d.capitalize())
|
| 926 |
+
for d in ["easy", "medium", "hard"] if d in diff_list]
|
| 927 |
+
h1, l1 = ax1.get_legend_handles_labels()
|
| 928 |
+
h2, l2 = ax2.get_legend_handles_labels()
|
| 929 |
+
ax1.legend(h1 + h2 + diff_patches, l1 + l2 + [p.get_label() for p in diff_patches],
|
| 930 |
+
loc="upper left", fontsize=9, framealpha=0.85)
|
| 931 |
+
final_sr = success_ma[-1] if success_ma else 0.0
|
| 932 |
+
fig.suptitle(f"Pyre NumPy A2C Training — {episodes_list[-1]} episodes | final success: {final_sr:.0%}",
|
| 933 |
+
fontsize=14, fontweight="bold", y=1.01)
|
| 934 |
+
fig.tight_layout()
|
| 935 |
+
png_path = output_path.with_suffix(".png")
|
| 936 |
+
fig.savefig(png_path, dpi=150, bbox_inches="tight")
|
| 937 |
+
plt.close(fig)
|
| 938 |
+
print(f"saved graph_png={png_path}")
|
| 939 |
+
except ImportError:
|
| 940 |
+
pass
|
| 941 |
+
|
| 942 |
+
|
| 943 |
+
def parse_args() -> argparse.Namespace:
|
| 944 |
+
parser = argparse.ArgumentParser(description="Train a NumPy actor-critic baseline for Pyre.")
|
| 945 |
+
parser.add_argument("--episodes", type=int, default=120, help="Training episodes.")
|
| 946 |
+
parser.add_argument("--difficulty", type=str, default="easy", choices=DIFFICULTIES)
|
| 947 |
+
parser.add_argument(
|
| 948 |
+
"--difficulty-schedule",
|
| 949 |
+
type=str,
|
| 950 |
+
default="easy,medium",
|
| 951 |
+
help="Comma-separated curriculum, expanded evenly across episodes.",
|
| 952 |
+
)
|
| 953 |
+
parser.add_argument("--eval-difficulty", type=str, default="medium", choices=DIFFICULTIES)
|
| 954 |
+
parser.add_argument("--eval-episodes", type=int, default=5)
|
| 955 |
+
parser.add_argument("--eval-every", type=int, default=20)
|
| 956 |
+
parser.add_argument("--update-every", type=int, default=5, help="Episodes per policy update.")
|
| 957 |
+
parser.add_argument("--update-epochs", type=int, default=3, help="Gradient passes over each on-policy batch.")
|
| 958 |
+
parser.add_argument("--minibatch-size", type=int, default=256, help="Samples per gradient step.")
|
| 959 |
+
parser.add_argument("--gamma", type=float, default=0.99)
|
| 960 |
+
parser.add_argument("--gae-lambda", type=float, default=0.95)
|
| 961 |
+
parser.add_argument("--learning-rate", type=float, default=3e-4)
|
| 962 |
+
parser.add_argument("--value-coef", type=float, default=0.5)
|
| 963 |
+
parser.add_argument("--history-length", type=int, default=4)
|
| 964 |
+
parser.add_argument("--max-steps", type=int, default=150)
|
| 965 |
+
parser.add_argument("--seed", type=int, default=7)
|
| 966 |
+
parser.add_argument("--observation-mode", type=str, default="visible", choices=("visible", "full"))
|
| 967 |
+
parser.add_argument("--output", type=str, default="artifacts/pyre_actor_critic.npz")
|
| 968 |
+
parser.add_argument("--save-metrics", action="store_true", help="Save per-episode metrics as CSV beside the model.")
|
| 969 |
+
parser.add_argument("--save-graph", action="store_true", help="Save an SVG training graph beside the model.")
|
| 970 |
+
parser.add_argument("--describe-only", action="store_true", help="Print observation/action/reward definitions and exit.")
|
| 971 |
+
return parser.parse_args()
|
| 972 |
+
|
| 973 |
+
|
| 974 |
+
def main() -> None:
|
| 975 |
+
args = parse_args()
|
| 976 |
+
encoder = ObservationEncoder(mode=args.observation_mode)
|
| 977 |
+
if args.describe_only:
|
| 978 |
+
print(describe_environment_contract(encoder, args.history_length))
|
| 979 |
+
return
|
| 980 |
+
train(args)
|
| 981 |
+
|
| 982 |
+
|
| 983 |
+
if __name__ == "__main__":
|
| 984 |
+
main()
|
examples/train_sb3_agent.py
ADDED
|
@@ -0,0 +1,285 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import sys
|
| 2 |
+
import os
|
| 3 |
+
import types
|
| 4 |
+
|
| 5 |
+
# Windows AppControl can block matplotlib's compiled C extensions.
|
| 6 |
+
# Stub the minimal surface that stable_baselines3.common.logger imports
|
| 7 |
+
# at module level so SB3 loads cleanly even without a working matplotlib.
|
| 8 |
+
def _stub_matplotlib():
|
| 9 |
+
if "matplotlib" in sys.modules:
|
| 10 |
+
return
|
| 11 |
+
_mpl = types.ModuleType("matplotlib")
|
| 12 |
+
_mpl.figure = types.ModuleType("matplotlib.figure")
|
| 13 |
+
_mpl.figure.Figure = object
|
| 14 |
+
_mpl.use = lambda *a, **kw: None
|
| 15 |
+
_mpl.__version__ = "0.0.0"
|
| 16 |
+
sys.modules["matplotlib"] = _mpl
|
| 17 |
+
sys.modules["matplotlib.figure"] = _mpl.figure
|
| 18 |
+
for sub in ("matplotlib.pyplot", "matplotlib.ticker", "matplotlib.patches",
|
| 19 |
+
"matplotlib.gridspec", "matplotlib.colors", "matplotlib.cm",
|
| 20 |
+
"matplotlib.backend_bases", "matplotlib.backends",
|
| 21 |
+
"matplotlib.backends.backend_agg"):
|
| 22 |
+
m = types.ModuleType(sub)
|
| 23 |
+
sys.modules[sub] = m
|
| 24 |
+
|
| 25 |
+
_stub_matplotlib()
|
| 26 |
+
|
| 27 |
+
import gymnasium as gym
|
| 28 |
+
import numpy as np
|
| 29 |
+
from gymnasium import spaces
|
| 30 |
+
from pyre_env.models import PyreAction, PyreObservation
|
| 31 |
+
from pyre_env.server.pyre_env_environment import PyreEnvironment
|
| 32 |
+
import torch as th
|
| 33 |
+
sys.path.append(os.getcwd())
|
| 34 |
+
|
| 35 |
+
class PyreGymEnv(gym.Env):
|
| 36 |
+
"""Gymnasium wrapper for PyreEnvironment."""
|
| 37 |
+
|
| 38 |
+
def __init__(self, difficulty="easy", max_steps=150, observation_mode="visible"):
|
| 39 |
+
super().__init__()
|
| 40 |
+
self.env = PyreEnvironment(max_steps=max_steps)
|
| 41 |
+
self.difficulty = difficulty
|
| 42 |
+
self.observation_mode = observation_mode
|
| 43 |
+
|
| 44 |
+
# Action space:
|
| 45 |
+
# 0-3: Move (N, S, W, E)
|
| 46 |
+
# 4-7: Look (N, S, W, E)
|
| 47 |
+
# 8: Wait
|
| 48 |
+
# 9-24: Open Door 1-16
|
| 49 |
+
# 25-40: Close Door 1-16
|
| 50 |
+
self.action_space = spaces.Discrete(41)
|
| 51 |
+
|
| 52 |
+
# Observation space: Multi-input
|
| 53 |
+
# 1. Grid: 24x24x7 (Floor, Wall, Door_Open, Door_Closed, Exit, Obstacle, Fire, Smoke)
|
| 54 |
+
# 2. Global: [health, oxygen, step_progress, fire_spread, humidity, agent_x, agent_y, nearest_exit_dist, is_coughing]
|
| 55 |
+
# 3. Heat Sensor: 3x3
|
| 56 |
+
self.observation_space = spaces.Dict({
|
| 57 |
+
"grid": spaces.Box(low=0, high=1, shape=(7, 24, 24), dtype=np.float32),
|
| 58 |
+
"global": spaces.Box(low=0, high=1, shape=(9,), dtype=np.float32),
|
| 59 |
+
"heat": spaces.Box(low=0, high=1, shape=(1, 3, 3), dtype=np.float32)
|
| 60 |
+
})
|
| 61 |
+
|
| 62 |
+
def _get_obs(self, pyre_obs: PyreObservation):
|
| 63 |
+
map_state = pyre_obs.map_state
|
| 64 |
+
w, h = map_state.grid_w, map_state.grid_h
|
| 65 |
+
|
| 66 |
+
# Build 7-channel grid
|
| 67 |
+
# Channels: 0:Wall, 1:Door_Open, 2:Door_Closed, 3:Exit, 4:Obstacle, 5:Fire, 6:Smoke
|
| 68 |
+
# (Floor is implicit as all zeros in other channels)
|
| 69 |
+
grid = np.zeros((7, 24, 24), dtype=np.float32)
|
| 70 |
+
|
| 71 |
+
visible = {(x, y) for x, y in map_state.visible_cells}
|
| 72 |
+
for y in range(h):
|
| 73 |
+
for x in range(w):
|
| 74 |
+
if self.observation_mode == "visible" and (x, y) not in visible and (x, y) != (map_state.agent_x, map_state.agent_y):
|
| 75 |
+
continue
|
| 76 |
+
|
| 77 |
+
i = y * w + x
|
| 78 |
+
ct = map_state.cell_grid[i]
|
| 79 |
+
if ct == 1: grid[0, y, x] = 1.0 # Wall
|
| 80 |
+
elif ct == 2: grid[1, y, x] = 1.0 # Door Open
|
| 81 |
+
elif ct == 3: grid[2, y, x] = 1.0 # Door Closed
|
| 82 |
+
elif ct == 4: grid[3, y, x] = 1.0 # Exit
|
| 83 |
+
elif ct == 5: grid[4, y, x] = 1.0 # Obstacle
|
| 84 |
+
|
| 85 |
+
grid[5, y, x] = float(map_state.fire_grid[i])
|
| 86 |
+
grid[6, y, x] = float(map_state.smoke_grid[i])
|
| 87 |
+
|
| 88 |
+
# Global features
|
| 89 |
+
metadata = pyre_obs.metadata or {}
|
| 90 |
+
nearest_exit = float(metadata.get("nearest_exit_distance", 48) or 48.0) / 48.0
|
| 91 |
+
# smoke_level → is_coughing proxy (moderate/heavy smoke = coughing)
|
| 92 |
+
smoke = getattr(pyre_obs, "smoke_level", "none") or "none"
|
| 93 |
+
is_coughing = 1.0 if smoke in ("moderate", "heavy") else 0.0
|
| 94 |
+
|
| 95 |
+
global_feats = np.array([
|
| 96 |
+
float(pyre_obs.agent_health) / 100.0,
|
| 97 |
+
float(pyre_obs.agent_health) / 100.0, # oxygen_level proxy
|
| 98 |
+
float(map_state.step_count) / float(map_state.max_steps),
|
| 99 |
+
float(map_state.fire_spread_rate),
|
| 100 |
+
float(map_state.humidity),
|
| 101 |
+
float(map_state.agent_x) / 24.0,
|
| 102 |
+
float(map_state.agent_y) / 24.0,
|
| 103 |
+
nearest_exit,
|
| 104 |
+
is_coughing,
|
| 105 |
+
], dtype=np.float32)
|
| 106 |
+
|
| 107 |
+
# Heat sensor — derive 3×3 fire neighbourhood around agent from the fire grid
|
| 108 |
+
ax, ay = map_state.agent_x, map_state.agent_y
|
| 109 |
+
gw, gh = map_state.grid_w, map_state.grid_h
|
| 110 |
+
heat_vals = []
|
| 111 |
+
for dy in (-1, 0, 1):
|
| 112 |
+
for dx in (-1, 0, 1):
|
| 113 |
+
nx, ny = ax + dx, ay + dy
|
| 114 |
+
if 0 <= nx < gw and 0 <= ny < gh:
|
| 115 |
+
heat_vals.append(float(map_state.fire_grid[ny * gw + nx]))
|
| 116 |
+
else:
|
| 117 |
+
heat_vals.append(0.0)
|
| 118 |
+
heat = np.array(heat_vals, dtype=np.float32).reshape(1, 3, 3)
|
| 119 |
+
|
| 120 |
+
return {
|
| 121 |
+
"grid": grid,
|
| 122 |
+
"global": global_feats,
|
| 123 |
+
"heat": heat
|
| 124 |
+
}
|
| 125 |
+
|
| 126 |
+
def reset(self, seed=None, options=None):
|
| 127 |
+
super().reset(seed=seed)
|
| 128 |
+
difficulty = options.get("difficulty", self.difficulty) if options else self.difficulty
|
| 129 |
+
pyre_obs = self.env.reset(seed=seed, difficulty=difficulty)
|
| 130 |
+
return self._get_obs(pyre_obs), {}
|
| 131 |
+
|
| 132 |
+
def step(self, action_idx):
|
| 133 |
+
# Map Discrete action to PyreAction
|
| 134 |
+
if action_idx < 4:
|
| 135 |
+
dirs = ["north", "south", "west", "east"]
|
| 136 |
+
action = PyreAction(action="move", direction=dirs[action_idx])
|
| 137 |
+
elif action_idx < 8:
|
| 138 |
+
dirs = ["north", "south", "west", "east"]
|
| 139 |
+
action = PyreAction(action="look", direction=dirs[action_idx - 4])
|
| 140 |
+
elif action_idx == 8:
|
| 141 |
+
action = PyreAction(action="wait")
|
| 142 |
+
elif action_idx < 9 + 16:
|
| 143 |
+
action = PyreAction(action="door", target_id=f"door_{action_idx - 8}", door_state="open")
|
| 144 |
+
else:
|
| 145 |
+
action = PyreAction(action="door", target_id=f"door_{action_idx - 24}", door_state="close")
|
| 146 |
+
|
| 147 |
+
pyre_obs = self.env.step(action)
|
| 148 |
+
|
| 149 |
+
obs = self._get_obs(pyre_obs)
|
| 150 |
+
reward = pyre_obs.reward
|
| 151 |
+
terminated = pyre_obs.done
|
| 152 |
+
truncated = False # Step limit handled by env.done
|
| 153 |
+
|
| 154 |
+
return obs, reward, terminated, truncated, {"pyre_obs": pyre_obs}
|
| 155 |
+
|
| 156 |
+
if __name__ == "__main__":
|
| 157 |
+
from stable_baselines3 import PPO
|
| 158 |
+
from stable_baselines3.common.callbacks import CheckpointCallback
|
| 159 |
+
import argparse
|
| 160 |
+
|
| 161 |
+
parser = argparse.ArgumentParser()
|
| 162 |
+
parser.add_argument("--episodes", type=int, default=1500, help="Total episodes to train across all levels")
|
| 163 |
+
parser.add_argument("--difficulty", type=str, default="curriculum", help="easy, medium, hard, random, or curriculum")
|
| 164 |
+
parser.add_argument("--output", type=str, default="artifacts/ppo_pyre_multilevel")
|
| 165 |
+
args = parser.parse_args()
|
| 166 |
+
|
| 167 |
+
from gymnasium.wrappers import RecordEpisodeStatistics
|
| 168 |
+
|
| 169 |
+
# Custom wrapper to handle difficulty changes
|
| 170 |
+
class MultiLevelWrapper(gym.Wrapper):
|
| 171 |
+
def __init__(self, env, mode="curriculum"):
|
| 172 |
+
super().__init__(env)
|
| 173 |
+
self.mode = mode
|
| 174 |
+
self.current_difficulty = "easy"
|
| 175 |
+
self.step_count = 0
|
| 176 |
+
self.total_steps = 0
|
| 177 |
+
|
| 178 |
+
def reset(self, **kwargs):
|
| 179 |
+
if self.mode == "random":
|
| 180 |
+
self.current_difficulty = np.random.choice(["easy", "medium", "hard"])
|
| 181 |
+
elif self.mode == "curriculum":
|
| 182 |
+
if self.total_steps < 0.33 * total_training_steps:
|
| 183 |
+
self.current_difficulty = "easy"
|
| 184 |
+
elif self.total_steps < 0.66 * total_training_steps:
|
| 185 |
+
self.current_difficulty = "medium"
|
| 186 |
+
else:
|
| 187 |
+
self.current_difficulty = "hard"
|
| 188 |
+
else:
|
| 189 |
+
self.current_difficulty = self.mode
|
| 190 |
+
|
| 191 |
+
# Extract options from kwargs if present, or create new
|
| 192 |
+
options = kwargs.get("options")
|
| 193 |
+
if options is None:
|
| 194 |
+
options = {}
|
| 195 |
+
options["difficulty"] = self.current_difficulty
|
| 196 |
+
kwargs["options"] = options
|
| 197 |
+
|
| 198 |
+
return self.env.reset(**kwargs)
|
| 199 |
+
|
| 200 |
+
def step(self, action):
|
| 201 |
+
obs, reward, term, trunc, info = self.env.step(action)
|
| 202 |
+
self.total_steps += 1
|
| 203 |
+
info["difficulty"] = self.current_difficulty
|
| 204 |
+
return obs, reward, term, trunc, info
|
| 205 |
+
|
| 206 |
+
total_training_steps = args.episodes * 60
|
| 207 |
+
|
| 208 |
+
env = PyreGymEnv(difficulty="easy") # Base difficulty
|
| 209 |
+
env = MultiLevelWrapper(env, mode=args.difficulty)
|
| 210 |
+
env = RecordEpisodeStatistics(env)
|
| 211 |
+
|
| 212 |
+
# Custom CNN policy for the grid
|
| 213 |
+
# Increased network capacity for multiple levels
|
| 214 |
+
policy_kwargs = dict(
|
| 215 |
+
activation_fn=th.nn.ReLU,
|
| 216 |
+
net_arch=dict(pi=[256, 128], qf=[256, 128])
|
| 217 |
+
)
|
| 218 |
+
|
| 219 |
+
model = PPO(
|
| 220 |
+
"MultiInputPolicy",
|
| 221 |
+
env,
|
| 222 |
+
verbose=1,
|
| 223 |
+
learning_rate=2e-4, # Slightly lower LR for stability across levels
|
| 224 |
+
n_steps=2048,
|
| 225 |
+
batch_size=128,
|
| 226 |
+
n_epochs=10,
|
| 227 |
+
gamma=0.99,
|
| 228 |
+
gae_lambda=0.95,
|
| 229 |
+
clip_range=0.2,
|
| 230 |
+
ent_coef=0.02, # Higher entropy to encourage exploration in procedural maps
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
print(f"Starting multi-level training (mode: {args.difficulty})...")
|
| 234 |
+
|
| 235 |
+
# Add a simple callback to log episode rewards to a CSV
|
| 236 |
+
from stable_baselines3.common.callbacks import BaseCallback
|
| 237 |
+
import csv
|
| 238 |
+
from pathlib import Path
|
| 239 |
+
|
| 240 |
+
class CSVLogCallback(BaseCallback):
|
| 241 |
+
def __init__(self, filename):
|
| 242 |
+
super().__init__()
|
| 243 |
+
self.filename = filename
|
| 244 |
+
self.results = []
|
| 245 |
+
def _on_step(self):
|
| 246 |
+
# Check every step for finished episodes
|
| 247 |
+
for info in self.locals.get("infos", []):
|
| 248 |
+
if "episode" in info:
|
| 249 |
+
self.results.append({
|
| 250 |
+
"step": self.num_timesteps,
|
| 251 |
+
"reward": info["episode"]["r"],
|
| 252 |
+
"length": info["episode"]["l"]
|
| 253 |
+
})
|
| 254 |
+
return True
|
| 255 |
+
def _on_rollout_end(self):
|
| 256 |
+
# Save every rollout
|
| 257 |
+
if self.results:
|
| 258 |
+
with open(self.filename, "w", newline="") as f:
|
| 259 |
+
writer = csv.DictWriter(f, fieldnames=["step", "reward", "length"])
|
| 260 |
+
writer.writeheader()
|
| 261 |
+
writer.writerows(self.results)
|
| 262 |
+
return True
|
| 263 |
+
|
| 264 |
+
csv_path = args.output + ".csv"
|
| 265 |
+
callback = CSVLogCallback(csv_path)
|
| 266 |
+
|
| 267 |
+
# CNN MultiInputPolicy needs far more steps than a flat MLP to warm up.
|
| 268 |
+
# episodes * 50 ≈ 15k steps (too few). Use episodes * 500 for meaningful learning.
|
| 269 |
+
model.learn(total_timesteps=args.episodes * 500, callback=callback)
|
| 270 |
+
|
| 271 |
+
model.save(args.output)
|
| 272 |
+
print(f"Model saved to {args.output}")
|
| 273 |
+
print(f"Metrics saved to {csv_path}")
|
| 274 |
+
|
| 275 |
+
# Generate a quick SVG graph if we have results
|
| 276 |
+
if callback.results:
|
| 277 |
+
try:
|
| 278 |
+
from examples.train_rl_agent import save_training_graph
|
| 279 |
+
# Mocking the row format expected by the baseline plotter
|
| 280 |
+
rows = [{"episode": i, "reward": r["reward"], "evacuated": 0} for i, r in enumerate(callback.results)]
|
| 281 |
+
save_training_graph(Path(args.output + ".svg"), rows, [])
|
| 282 |
+
print(f"Graph saved to {args.output}.svg")
|
| 283 |
+
except Exception as e:
|
| 284 |
+
print(f"Could not generate SVG automatically: {e}")
|
| 285 |
+
print("CSV is available at " + csv_path)
|
examples/train_torch_ppo.py
ADDED
|
@@ -0,0 +1,1278 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
PyTorch PPO Agent for Pyre — Fire Evacuation RL Training Script.
|
| 3 |
+
|
| 4 |
+
=== ENVIRONMENT SUMMARY ===
|
| 5 |
+
Pyre is a partial-observability crisis navigation environment:
|
| 6 |
+
- Grid: 16×16 (easy/medium) or 20×24 (hard, procedural)
|
| 7 |
+
- Agent: Spawns inside a burning building, must evacuate before dying
|
| 8 |
+
- Fire: Spreads via cellular automaton — wind, humidity, fuel vary per episode
|
| 9 |
+
- Partial observability: visibility radius (2–5 cells) shrinks in heavy smoke
|
| 10 |
+
- Doors: Can be opened/closed to slow fire spread (+0.5 strategic door bonus)
|
| 11 |
+
- Health: 100 HP, drains from smoke (0.5–5/step) and fire (10/step)
|
| 12 |
+
|
| 13 |
+
=== ACTION SPACE (41 discrete) ===
|
| 14 |
+
0–3 : move(north|south|west|east)
|
| 15 |
+
4–7 : look(north|south|west|east) — scan without moving, still costs a step
|
| 16 |
+
8 : wait()
|
| 17 |
+
9–24 : door(door_1..16, open)
|
| 18 |
+
25–40 : door(door_1..16, close)
|
| 19 |
+
Runtime action masking via `available_actions_hint` prevents invalid moves.
|
| 20 |
+
|
| 21 |
+
=== OBSERVATION ENCODING ===
|
| 22 |
+
Per-step grid: 24×24 padded map × 10 channels
|
| 23 |
+
• 6 one-hot cell type (floor/wall/door_open/door_closed/exit/obstacle)
|
| 24 |
+
• fire intensity [0, 1]
|
| 25 |
+
• smoke density [0, 1]
|
| 26 |
+
• visibility mask (1=visible, 0=unseen)
|
| 27 |
+
• agent position mask
|
| 28 |
+
Global scalars (22): health, step_progress, fire_spread, humidity,
|
| 29 |
+
agent_x, agent_y, exit_distance, reachable_exits, visible_cells,
|
| 30 |
+
fire_sources, smoke_severity, alive, evacuated, wind (one-hot 5), difficulty (one-hot 3)
|
| 31 |
+
Frame stacking: 4 consecutive frames → input_dim = 5782 × 4 = 23128
|
| 32 |
+
|
| 33 |
+
=== REWARD STRUCTURE ===
|
| 34 |
+
Per-step:
|
| 35 |
+
-0.01 time penalty (urgency)
|
| 36 |
+
+0.10 BFS progress toward nearest unblocked exit
|
| 37 |
+
-0.05 regression (moved farther from exit)
|
| 38 |
+
+0.05 safe-progress bonus (progress through smoke-free cell)
|
| 39 |
+
-0.50 danger penalty (moved into smoke≥moderate or fire-adjacent)
|
| 40 |
+
-0.02×dmg health drain penalty
|
| 41 |
+
+0.50 strategic door close (adjacent to fire, once per door per episode)
|
| 42 |
+
+0.02 exploration bonus (first visit to cell)
|
| 43 |
+
Terminal:
|
| 44 |
+
+5.00 evacuation success
|
| 45 |
+
+1.50×(hp/100) health survival bonus (max +1.5)
|
| 46 |
+
-10.0 death
|
| 47 |
+
-5.00 timeout
|
| 48 |
+
0→+3.0 near-miss partial credit (based on closest exit approach)
|
| 49 |
+
+0.05×remaining_steps time bonus
|
| 50 |
+
|
| 51 |
+
=== ALGORITHM: PPO (Proximal Policy Optimization) ===
|
| 52 |
+
WHY PPO over alternatives:
|
| 53 |
+
• DQN — Off-policy, harder credit assignment for sparse terminal rewards; no clean action masking
|
| 54 |
+
• A2C — Simpler but no clipping → unstable on hard stochastic episodes
|
| 55 |
+
• SAC — Designed for continuous spaces; discrete SAC works but adds complexity
|
| 56 |
+
• LSTM-PPO — Better for fully text-only obs; grid map_state already encodes spatial state
|
| 57 |
+
→ PPO + frame-stack + action-mask hits the sweet spot for this env
|
| 58 |
+
|
| 59 |
+
Key PPO improvements over the existing NumPy A2C (train_rl_agent.py):
|
| 60 |
+
✓ PPO clip (ε=0.2) prevents catastrophic updates
|
| 61 |
+
✓ Entropy regularization sustains exploration in smoke-obscured corridors
|
| 62 |
+
✓ Value function clipping stabilises critic under sparse terminal rewards
|
| 63 |
+
✓ GPU acceleration 10–20× faster than NumPy baseline
|
| 64 |
+
✓ LayerNorm in network improves gradient flow for large input dims
|
| 65 |
+
✓ Linear LR decay stabilises late-stage convergence
|
| 66 |
+
✓ Better curriculum 3-stage easy→medium→hard with patience gating
|
| 67 |
+
|
| 68 |
+
Usage:
|
| 69 |
+
python examples/train_torch_ppo.py --episodes 500 --device cuda
|
| 70 |
+
python examples/train_torch_ppo.py --episodes 300 --difficulty-schedule easy,medium,hard
|
| 71 |
+
python examples/train_torch_ppo.py --resume artifacts/pyre_ppo_checkpoint.pt
|
| 72 |
+
python examples/train_torch_ppo.py --describe-only
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
from __future__ import annotations
|
| 76 |
+
|
| 77 |
+
import argparse
|
| 78 |
+
import csv
|
| 79 |
+
import json
|
| 80 |
+
import os
|
| 81 |
+
import sys
|
| 82 |
+
import time
|
| 83 |
+
from collections import deque
|
| 84 |
+
from dataclasses import dataclass, field
|
| 85 |
+
from pathlib import Path
|
| 86 |
+
from typing import Dict, List, Optional, Sequence, Tuple
|
| 87 |
+
|
| 88 |
+
import numpy as np
|
| 89 |
+
|
| 90 |
+
# ---------------------------------------------------------------------------
|
| 91 |
+
# Optional torch import — fail fast with a helpful message
|
| 92 |
+
# ---------------------------------------------------------------------------
|
| 93 |
+
try:
|
| 94 |
+
import torch
|
| 95 |
+
import torch.nn as nn
|
| 96 |
+
import torch.nn.functional as F
|
| 97 |
+
from torch.optim import Adam
|
| 98 |
+
from torch.optim.lr_scheduler import LinearLR
|
| 99 |
+
except ImportError:
|
| 100 |
+
sys.exit(
|
| 101 |
+
"PyTorch not found. Install with:\n"
|
| 102 |
+
" pip install torch --index-url https://download.pytorch.org/whl/cu121\n"
|
| 103 |
+
"or for CPU only:\n"
|
| 104 |
+
" pip install torch"
|
| 105 |
+
)
|
| 106 |
+
|
| 107 |
+
# ---------------------------------------------------------------------------
|
| 108 |
+
# Project imports — support both package install and direct run from root
|
| 109 |
+
# ---------------------------------------------------------------------------
|
| 110 |
+
_ROOT = Path(__file__).resolve().parent.parent
|
| 111 |
+
if str(_ROOT) not in sys.path:
|
| 112 |
+
sys.path.insert(0, str(_ROOT))
|
| 113 |
+
|
| 114 |
+
try:
|
| 115 |
+
from pyre_env.models import PyreAction, PyreObservation
|
| 116 |
+
from pyre_env.server.pyre_env_environment import PyreEnvironment
|
| 117 |
+
except ModuleNotFoundError:
|
| 118 |
+
try:
|
| 119 |
+
from models import PyreAction, PyreObservation
|
| 120 |
+
from server.pyre_env_environment import PyreEnvironment
|
| 121 |
+
except ModuleNotFoundError:
|
| 122 |
+
sys.exit(
|
| 123 |
+
"Cannot import Pyre modules. Run this script from the openenv-pyre root:\n"
|
| 124 |
+
" python examples/train_torch_ppo.py"
|
| 125 |
+
)
|
| 126 |
+
|
| 127 |
+
# ---------------------------------------------------------------------------
|
| 128 |
+
# Reuse the established observation/action interface from train_rl_agent.py
|
| 129 |
+
# These are the canonical definitions for this environment.
|
| 130 |
+
# ---------------------------------------------------------------------------
|
| 131 |
+
MAX_GRID_W = 24
|
| 132 |
+
MAX_GRID_H = 24
|
| 133 |
+
MAX_DOORS = 16
|
| 134 |
+
DIRECTIONS = ("north", "south", "west", "east")
|
| 135 |
+
WINDS = ("CALM", "NORTH", "SOUTH", "WEST", "EAST")
|
| 136 |
+
DIFFICULTIES = ("easy", "medium", "hard")
|
| 137 |
+
|
| 138 |
+
MOVE_KEYS = [f"move(direction='{d}')" for d in DIRECTIONS]
|
| 139 |
+
LOOK_KEYS = [f"look(direction='{d}')" for d in DIRECTIONS]
|
| 140 |
+
WAIT_KEY = "wait()"
|
| 141 |
+
OPEN_KEYS = [f"door(target_id='door_{i}', door_state='open')" for i in range(1, MAX_DOORS + 1)]
|
| 142 |
+
CLOSE_KEYS = [f"door(target_id='door_{i}', door_state='close')" for i in range(1, MAX_DOORS + 1)]
|
| 143 |
+
ACTION_KEYS = MOVE_KEYS + LOOK_KEYS + [WAIT_KEY] + OPEN_KEYS + CLOSE_KEYS
|
| 144 |
+
ACTION_DIM = len(ACTION_KEYS) # 41
|
| 145 |
+
ACTION_TO_INDEX = {key: idx for idx, key in enumerate(ACTION_KEYS)}
|
| 146 |
+
|
| 147 |
+
import re
|
| 148 |
+
_MOVE_RE = re.compile(r"move\(direction='(north|south|west|east)'\)")
|
| 149 |
+
_LOOK_RE = re.compile(r"look\(direction='(north|south|west|east)'\)")
|
| 150 |
+
_DOOR_RE = re.compile(r"door\(target_id='(door_(\d+))', door_state='(open|close)'\)")
|
| 151 |
+
|
| 152 |
+
|
| 153 |
+
def action_index_to_env_action(index: int) -> PyreAction:
|
| 154 |
+
if 0 <= index < 4:
|
| 155 |
+
return PyreAction(action="move", direction=DIRECTIONS[index])
|
| 156 |
+
if 4 <= index < 8:
|
| 157 |
+
return PyreAction(action="look", direction=DIRECTIONS[index - 4])
|
| 158 |
+
if index == 8:
|
| 159 |
+
return PyreAction(action="wait")
|
| 160 |
+
if 9 <= index < 9 + MAX_DOORS:
|
| 161 |
+
door_id = f"door_{index - 8}"
|
| 162 |
+
return PyreAction(action="door", target_id=door_id, door_state="open")
|
| 163 |
+
door_slot = index - (9 + MAX_DOORS)
|
| 164 |
+
door_id = f"door_{door_slot + 1}"
|
| 165 |
+
return PyreAction(action="door", target_id=door_id, door_state="close")
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def build_action_mask(observation: PyreObservation, exclude_look: bool = True) -> np.ndarray:
|
| 169 |
+
"""Build a binary validity mask over the 41-action space.
|
| 170 |
+
|
| 171 |
+
exclude_look=True (default for RL):
|
| 172 |
+
Suppresses all 4 'look' actions. The RL agent already receives the full
|
| 173 |
+
grid via map_state — look gives zero new information but wastes a step
|
| 174 |
+
and earns no reward. Excluding it concentrates the policy on moves and
|
| 175 |
+
doors, which are the only actions that can improve the agent's position.
|
| 176 |
+
"""
|
| 177 |
+
mask = np.zeros(ACTION_DIM, dtype=np.float32)
|
| 178 |
+
for hint in observation.available_actions_hint:
|
| 179 |
+
idx = ACTION_TO_INDEX.get(hint)
|
| 180 |
+
if idx is not None:
|
| 181 |
+
mask[idx] = 1.0
|
| 182 |
+
continue
|
| 183 |
+
m = _MOVE_RE.fullmatch(hint)
|
| 184 |
+
if m:
|
| 185 |
+
mask[ACTION_TO_INDEX[f"move(direction='{m.group(1)}')"]] = 1.0
|
| 186 |
+
continue
|
| 187 |
+
m = _LOOK_RE.fullmatch(hint)
|
| 188 |
+
if m:
|
| 189 |
+
if not exclude_look:
|
| 190 |
+
mask[ACTION_TO_INDEX[f"look(direction='{m.group(1)}')"]] = 1.0
|
| 191 |
+
continue
|
| 192 |
+
m = _DOOR_RE.fullmatch(hint)
|
| 193 |
+
if m:
|
| 194 |
+
door_id, door_num, state = m.group(1), int(m.group(2)), m.group(3)
|
| 195 |
+
if 1 <= door_num <= MAX_DOORS:
|
| 196 |
+
mask[ACTION_TO_INDEX[f"door(target_id='{door_id}', door_state='{state}')"]] = 1.0
|
| 197 |
+
if mask.sum() == 0:
|
| 198 |
+
mask[ACTION_TO_INDEX[WAIT_KEY]] = 1.0
|
| 199 |
+
return mask
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
class ObservationEncoder:
|
| 203 |
+
"""Encode PyreObservation into a fixed-length float32 vector.
|
| 204 |
+
|
| 205 |
+
Mode 'visible': only populate cells within the agent's sight radius —
|
| 206 |
+
mimics true partial observability; preferred for training.
|
| 207 |
+
Mode 'full': expose complete ground-truth grid — useful for debugging
|
| 208 |
+
or oracle upper-bound experiments.
|
| 209 |
+
|
| 210 |
+
Output shape: (base_dim,) = (MAX_GRID_W × MAX_GRID_H × 10 + 25,) = (5785,)
|
| 211 |
+
With history stacking of k frames: (5785 × k,)
|
| 212 |
+
|
| 213 |
+
The 3 extra scalars over the v1 baseline are map-agnostic exit-compass
|
| 214 |
+
features (Fix 3): exit_dx_norm, exit_dy_norm, exit_manhattan_norm.
|
| 215 |
+
These allow the agent to locate the nearest exit on procedurally generated
|
| 216 |
+
maps without having to memorise layout-specific coordinates.
|
| 217 |
+
"""
|
| 218 |
+
|
| 219 |
+
base_dim = MAX_GRID_W * MAX_GRID_H * 10 + 25
|
| 220 |
+
|
| 221 |
+
def __init__(self, mode: str = "visible"):
|
| 222 |
+
if mode not in {"visible", "full"}:
|
| 223 |
+
raise ValueError(f"mode must be 'visible' or 'full', got '{mode}'")
|
| 224 |
+
self.mode = mode
|
| 225 |
+
|
| 226 |
+
def encode(self, observation: PyreObservation) -> np.ndarray:
|
| 227 |
+
ms = observation.map_state
|
| 228 |
+
if ms is None:
|
| 229 |
+
raise ValueError("map_state is required for encoding.")
|
| 230 |
+
|
| 231 |
+
cell_one_hot = np.zeros((MAX_GRID_H, MAX_GRID_W, 6), dtype=np.float32)
|
| 232 |
+
fire_ch = np.zeros((MAX_GRID_H, MAX_GRID_W), dtype=np.float32)
|
| 233 |
+
smoke_ch = np.zeros((MAX_GRID_H, MAX_GRID_W), dtype=np.float32)
|
| 234 |
+
vis_ch = np.zeros((MAX_GRID_H, MAX_GRID_W), dtype=np.float32)
|
| 235 |
+
agent_ch = np.zeros((MAX_GRID_H, MAX_GRID_W), dtype=np.float32)
|
| 236 |
+
|
| 237 |
+
visible = {(x, y) for x, y in ms.visible_cells}
|
| 238 |
+
for y in range(ms.grid_h):
|
| 239 |
+
for x in range(ms.grid_w):
|
| 240 |
+
if self.mode == "visible" and (x, y) not in visible and (x, y) != (ms.agent_x, ms.agent_y):
|
| 241 |
+
continue
|
| 242 |
+
i = y * ms.grid_w + x
|
| 243 |
+
ct = int(ms.cell_grid[i])
|
| 244 |
+
if 0 <= ct <= 5:
|
| 245 |
+
cell_one_hot[y, x, ct] = 1.0
|
| 246 |
+
fire_ch[y, x] = float(ms.fire_grid[i])
|
| 247 |
+
smoke_ch[y, x] = float(ms.smoke_grid[i])
|
| 248 |
+
vis_ch[y, x] = 1.0 if (x, y) in visible else 0.0
|
| 249 |
+
|
| 250 |
+
if 0 <= ms.agent_x < MAX_GRID_W and 0 <= ms.agent_y < MAX_GRID_H:
|
| 251 |
+
agent_ch[ms.agent_y, ms.agent_x] = 1.0
|
| 252 |
+
|
| 253 |
+
grid_features = np.concatenate([
|
| 254 |
+
cell_one_hot.reshape(-1),
|
| 255 |
+
fire_ch.reshape(-1),
|
| 256 |
+
smoke_ch.reshape(-1),
|
| 257 |
+
vis_ch.reshape(-1),
|
| 258 |
+
agent_ch.reshape(-1),
|
| 259 |
+
])
|
| 260 |
+
|
| 261 |
+
meta = observation.metadata or {}
|
| 262 |
+
wind = str(meta.get("wind_dir", ms.wind_dir or "CALM")).upper()
|
| 263 |
+
diff = str(meta.get("difficulty", "medium")).lower()
|
| 264 |
+
wi = WINDS.index(wind) if wind in WINDS else 0
|
| 265 |
+
di = DIFFICULTIES.index(diff) if diff in DIFFICULTIES else 1
|
| 266 |
+
|
| 267 |
+
wind_oh = np.zeros(len(WINDS), dtype=np.float32); wind_oh[wi] = 1.0
|
| 268 |
+
diff_oh = np.zeros(len(DIFFICULTIES), dtype=np.float32); diff_oh[di] = 1.0
|
| 269 |
+
|
| 270 |
+
# Fix 3 — map-agnostic exit compass features.
|
| 271 |
+
# Compute the direction vector and normalised Manhattan distance to the
|
| 272 |
+
# nearest exit cell (cell_type == 4) directly from the live grid.
|
| 273 |
+
# This gives the agent an exit "compass" that works on procedurally
|
| 274 |
+
# generated maps without memorising any layout.
|
| 275 |
+
EXIT_CELL_TYPE = 4
|
| 276 |
+
ax, ay = ms.agent_x, ms.agent_y
|
| 277 |
+
gw, gh = ms.grid_w, ms.grid_h
|
| 278 |
+
best_dist = float(gw + gh)
|
| 279 |
+
best_dx = 0.0
|
| 280 |
+
best_dy = 0.0
|
| 281 |
+
for cy in range(gh):
|
| 282 |
+
for cx in range(gw):
|
| 283 |
+
if int(ms.cell_grid[cy * gw + cx]) == EXIT_CELL_TYPE:
|
| 284 |
+
d = abs(cx - ax) + abs(cy - ay)
|
| 285 |
+
if d < best_dist:
|
| 286 |
+
best_dist = d
|
| 287 |
+
best_dx = float(cx - ax) / max(1, gw - 1)
|
| 288 |
+
best_dy = float(cy - ay) / max(1, gh - 1)
|
| 289 |
+
exit_manhattan_norm = best_dist / float(gw + gh)
|
| 290 |
+
|
| 291 |
+
global_features = np.array([
|
| 292 |
+
float(observation.agent_health) / 100.0,
|
| 293 |
+
float(ms.agent_health) / 100.0,
|
| 294 |
+
float(ms.step_count) / max(1, ms.max_steps),
|
| 295 |
+
float(ms.fire_spread_rate),
|
| 296 |
+
float(ms.humidity),
|
| 297 |
+
float(ms.agent_x) / max(1, ms.grid_w - 1),
|
| 298 |
+
float(ms.agent_y) / max(1, ms.grid_h - 1),
|
| 299 |
+
float(meta.get("nearest_exit_distance", MAX_GRID_W + MAX_GRID_H) or 0.0) / float(MAX_GRID_W + MAX_GRID_H),
|
| 300 |
+
float(meta.get("reachable_exit_count", 0.0)) / 4.0,
|
| 301 |
+
float(meta.get("visible_cell_count", 0.0)) / float(MAX_GRID_W * MAX_GRID_H),
|
| 302 |
+
float(meta.get("fire_sources", 0.0)) / 5.0,
|
| 303 |
+
{"none": 0.0, "light": 0.33, "moderate": 0.66, "heavy": 1.0}.get(observation.smoke_level, 0.0),
|
| 304 |
+
1.0 if ms.agent_alive else 0.0,
|
| 305 |
+
1.0 if ms.agent_evacuated else 0.0,
|
| 306 |
+
# Fix 3: exit-compass (3 new scalars — map-agnostic, layout-independent)
|
| 307 |
+
best_dx, # signed x-direction toward nearest exit
|
| 308 |
+
best_dy, # signed y-direction toward nearest exit
|
| 309 |
+
exit_manhattan_norm, # how far away the exit is (0 = here, 1 = max)
|
| 310 |
+
], dtype=np.float32)
|
| 311 |
+
|
| 312 |
+
return np.concatenate([grid_features, global_features, wind_oh, diff_oh]).astype(np.float32)
|
| 313 |
+
|
| 314 |
+
|
| 315 |
+
# ---------------------------------------------------------------------------
|
| 316 |
+
# Neural Network
|
| 317 |
+
# ---------------------------------------------------------------------------
|
| 318 |
+
|
| 319 |
+
class ActorCritic(nn.Module):
|
| 320 |
+
"""Shared-backbone Actor-Critic network for PPO.
|
| 321 |
+
|
| 322 |
+
Architecture:
|
| 323 |
+
Input → LayerNorm → FC(512) → LayerNorm → ReLU
|
| 324 |
+
→ FC(256) → LayerNorm → ReLU
|
| 325 |
+
→ FC(128) → ReLU
|
| 326 |
+
┌──────────────┴──────────────┐
|
| 327 |
+
Policy head (→ logits) Value head (→ scalar)
|
| 328 |
+
|
| 329 |
+
LayerNorm before activations improves gradient flow for the large
|
| 330 |
+
(23128-dim) flat input without requiring feature normalization.
|
| 331 |
+
"""
|
| 332 |
+
|
| 333 |
+
def __init__(self, input_dim: int, action_dim: int, hidden_sizes: Tuple[int, ...] = (512, 256, 128)):
|
| 334 |
+
super().__init__()
|
| 335 |
+
h1, h2, h3 = hidden_sizes
|
| 336 |
+
|
| 337 |
+
self.shared = nn.Sequential(
|
| 338 |
+
nn.LayerNorm(input_dim),
|
| 339 |
+
nn.Linear(input_dim, h1),
|
| 340 |
+
nn.LayerNorm(h1),
|
| 341 |
+
nn.ReLU(),
|
| 342 |
+
nn.Linear(h1, h2),
|
| 343 |
+
nn.LayerNorm(h2),
|
| 344 |
+
nn.ReLU(),
|
| 345 |
+
nn.Linear(h2, h3),
|
| 346 |
+
nn.ReLU(),
|
| 347 |
+
)
|
| 348 |
+
|
| 349 |
+
# Orthogonal init — standard for PPO (improves early convergence)
|
| 350 |
+
self._init_orthogonal()
|
| 351 |
+
|
| 352 |
+
self.policy_head = nn.Linear(h3, action_dim)
|
| 353 |
+
self.value_head = nn.Linear(h3, 1)
|
| 354 |
+
|
| 355 |
+
# Small init for output heads prevents saturated softmax early on
|
| 356 |
+
nn.init.orthogonal_(self.policy_head.weight, gain=0.01)
|
| 357 |
+
nn.init.zeros_(self.policy_head.bias)
|
| 358 |
+
nn.init.orthogonal_(self.value_head.weight, gain=1.0)
|
| 359 |
+
nn.init.zeros_(self.value_head.bias)
|
| 360 |
+
|
| 361 |
+
def _init_orthogonal(self) -> None:
|
| 362 |
+
for layer in self.shared:
|
| 363 |
+
if isinstance(layer, nn.Linear):
|
| 364 |
+
nn.init.orthogonal_(layer.weight, gain=np.sqrt(2))
|
| 365 |
+
nn.init.zeros_(layer.bias)
|
| 366 |
+
|
| 367 |
+
def forward(
|
| 368 |
+
self,
|
| 369 |
+
obs: torch.Tensor,
|
| 370 |
+
mask: torch.Tensor,
|
| 371 |
+
) -> Tuple[torch.distributions.Categorical, torch.Tensor]:
|
| 372 |
+
"""
|
| 373 |
+
Args:
|
| 374 |
+
obs: (B, input_dim) float32
|
| 375 |
+
mask: (B, action_dim) float32 — 1.0 = valid, 0.0 = invalid
|
| 376 |
+
Returns:
|
| 377 |
+
dist: Categorical distribution (action masking applied as -inf)
|
| 378 |
+
values: (B,) float32
|
| 379 |
+
"""
|
| 380 |
+
features = self.shared(obs)
|
| 381 |
+
logits = self.policy_head(features)
|
| 382 |
+
|
| 383 |
+
# Mask invalid actions with -inf before softmax (numerically stable)
|
| 384 |
+
logits = torch.where(mask.bool(), logits, torch.full_like(logits, -1e9))
|
| 385 |
+
|
| 386 |
+
dist = torch.distributions.Categorical(logits=logits)
|
| 387 |
+
values = self.value_head(features).squeeze(-1)
|
| 388 |
+
return dist, values
|
| 389 |
+
|
| 390 |
+
def act(
|
| 391 |
+
self,
|
| 392 |
+
obs: torch.Tensor,
|
| 393 |
+
mask: torch.Tensor,
|
| 394 |
+
deterministic: bool = False,
|
| 395 |
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 396 |
+
"""Sample (or take greedy) action. Returns (action, log_prob, value)."""
|
| 397 |
+
dist, values = self(obs, mask)
|
| 398 |
+
action = dist.mode if deterministic else dist.sample()
|
| 399 |
+
log_prob = dist.log_prob(action)
|
| 400 |
+
return action, log_prob, values
|
| 401 |
+
|
| 402 |
+
def evaluate(
|
| 403 |
+
self,
|
| 404 |
+
obs: torch.Tensor,
|
| 405 |
+
mask: torch.Tensor,
|
| 406 |
+
action: torch.Tensor,
|
| 407 |
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 408 |
+
"""Evaluate stored actions during PPO update. Returns (log_prob, value, entropy)."""
|
| 409 |
+
dist, values = self(obs, mask)
|
| 410 |
+
log_prob = dist.log_prob(action)
|
| 411 |
+
entropy = dist.entropy()
|
| 412 |
+
return log_prob, values, entropy
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
# ---------------------------------------------------------------------------
|
| 416 |
+
# Rollout buffer
|
| 417 |
+
# ---------------------------------------------------------------------------
|
| 418 |
+
|
| 419 |
+
@dataclass
|
| 420 |
+
class RolloutBuffer:
|
| 421 |
+
"""Stores transitions for a batch of episodes before PPO update."""
|
| 422 |
+
obs: List[np.ndarray] = field(default_factory=list)
|
| 423 |
+
masks: List[np.ndarray] = field(default_factory=list)
|
| 424 |
+
actions: List[int] = field(default_factory=list)
|
| 425 |
+
rewards: List[float] = field(default_factory=list)
|
| 426 |
+
log_probs: List[float] = field(default_factory=list)
|
| 427 |
+
values: List[float] = field(default_factory=list)
|
| 428 |
+
dones: List[bool] = field(default_factory=list)
|
| 429 |
+
|
| 430 |
+
def clear(self) -> None:
|
| 431 |
+
self.obs.clear()
|
| 432 |
+
self.masks.clear()
|
| 433 |
+
self.actions.clear()
|
| 434 |
+
self.rewards.clear()
|
| 435 |
+
self.log_probs.clear()
|
| 436 |
+
self.values.clear()
|
| 437 |
+
self.dones.clear()
|
| 438 |
+
|
| 439 |
+
def __len__(self) -> int:
|
| 440 |
+
return len(self.rewards)
|
| 441 |
+
|
| 442 |
+
|
| 443 |
+
# ---------------------------------------------------------------------------
|
| 444 |
+
# GAE computation
|
| 445 |
+
# ---------------------------------------------------------------------------
|
| 446 |
+
|
| 447 |
+
def compute_gae(
|
| 448 |
+
rewards: np.ndarray,
|
| 449 |
+
values: np.ndarray,
|
| 450 |
+
dones: np.ndarray,
|
| 451 |
+
gamma: float,
|
| 452 |
+
gae_lambda: float,
|
| 453 |
+
) -> Tuple[np.ndarray, np.ndarray]:
|
| 454 |
+
"""Generalized Advantage Estimation.
|
| 455 |
+
|
| 456 |
+
Returns (returns, advantages) — both shape (T,).
|
| 457 |
+
Episode boundaries (done=True) reset the GAE accumulator so advantages
|
| 458 |
+
don't bleed across episodes within a mixed batch.
|
| 459 |
+
"""
|
| 460 |
+
T = len(rewards)
|
| 461 |
+
advantages = np.zeros(T, dtype=np.float32)
|
| 462 |
+
gae = 0.0
|
| 463 |
+
next_value = 0.0
|
| 464 |
+
for t in reversed(range(T)):
|
| 465 |
+
if dones[t]:
|
| 466 |
+
next_value = 0.0
|
| 467 |
+
gae = 0.0
|
| 468 |
+
delta = rewards[t] + gamma * next_value * (1.0 - dones[t]) - values[t]
|
| 469 |
+
gae = delta + gamma * gae_lambda * (1.0 - dones[t]) * gae
|
| 470 |
+
advantages[t] = gae
|
| 471 |
+
next_value = values[t]
|
| 472 |
+
returns = advantages + values
|
| 473 |
+
return returns, advantages
|
| 474 |
+
|
| 475 |
+
|
| 476 |
+
# ---------------------------------------------------------------------------
|
| 477 |
+
# Episode runner
|
| 478 |
+
# ---------------------------------------------------------------------------
|
| 479 |
+
|
| 480 |
+
@dataclass
|
| 481 |
+
class EpisodeResult:
|
| 482 |
+
total_reward: float
|
| 483 |
+
steps: int
|
| 484 |
+
evacuated: bool
|
| 485 |
+
final_health: float
|
| 486 |
+
difficulty: str
|
| 487 |
+
|
| 488 |
+
|
| 489 |
+
def run_episode(
|
| 490 |
+
env: PyreEnvironment,
|
| 491 |
+
network: ActorCritic,
|
| 492 |
+
encoder: ObservationEncoder,
|
| 493 |
+
device: torch.device,
|
| 494 |
+
difficulty: str,
|
| 495 |
+
history_length: int,
|
| 496 |
+
buffer: RolloutBuffer,
|
| 497 |
+
deterministic: bool = False,
|
| 498 |
+
) -> EpisodeResult:
|
| 499 |
+
"""Run one episode, appending transitions to *buffer*."""
|
| 500 |
+
observation = env.reset(difficulty=difficulty)
|
| 501 |
+
zero_frame = np.zeros(encoder.base_dim, dtype=np.float32)
|
| 502 |
+
frames: deque = deque([zero_frame.copy() for _ in range(history_length)], maxlen=history_length)
|
| 503 |
+
frames.append(encoder.encode(observation))
|
| 504 |
+
|
| 505 |
+
total_reward = 0.0
|
| 506 |
+
final_health = observation.agent_health
|
| 507 |
+
evacuated = False
|
| 508 |
+
steps = 0
|
| 509 |
+
# Anti-loop tracking: remember the last LOOP_WINDOW positions this episode.
|
| 510 |
+
# Revisiting any of them means the agent is circling, not exploring.
|
| 511 |
+
LOOP_WINDOW = 12
|
| 512 |
+
recent_positions: deque = deque(maxlen=LOOP_WINDOW)
|
| 513 |
+
|
| 514 |
+
network.eval()
|
| 515 |
+
with torch.no_grad():
|
| 516 |
+
while True:
|
| 517 |
+
state_vec = np.concatenate(list(frames), dtype=np.float32)
|
| 518 |
+
# exclude_look=True: RL agent sees full grid — look wastes steps
|
| 519 |
+
action_mask = build_action_mask(observation, exclude_look=True)
|
| 520 |
+
|
| 521 |
+
obs_t = torch.tensor(state_vec, dtype=torch.float32, device=device).unsqueeze(0)
|
| 522 |
+
mask_t = torch.tensor(action_mask, dtype=torch.float32, device=device).unsqueeze(0)
|
| 523 |
+
|
| 524 |
+
action_t, log_prob_t, value_t = network.act(obs_t, mask_t, deterministic=deterministic)
|
| 525 |
+
|
| 526 |
+
action_idx = int(action_t.item())
|
| 527 |
+
env_action = action_index_to_env_action(action_idx)
|
| 528 |
+
next_obs = env.step(env_action)
|
| 529 |
+
|
| 530 |
+
reward = float(next_obs.reward or 0.0)
|
| 531 |
+
|
| 532 |
+
# ----------------------------------------------------------------
|
| 533 |
+
# Reward shaping 1 — idle penalty
|
| 534 |
+
# The env's -0.01/step is too weak; make waiting explicitly costly.
|
| 535 |
+
# ----------------------------------------------------------------
|
| 536 |
+
chosen_action = env_action.action
|
| 537 |
+
if chosen_action == "wait":
|
| 538 |
+
reward -= 0.05
|
| 539 |
+
|
| 540 |
+
# ----------------------------------------------------------------
|
| 541 |
+
# Reward shaping 2 — fire-approach penalty (Fix 2)
|
| 542 |
+
# Penalise landing on (or moving next to) a cell with active fire.
|
| 543 |
+
# This is stronger than the env's DangerPenalty and fires *before*
|
| 544 |
+
# health drain accumulates, teaching the agent to predict spread.
|
| 545 |
+
# We look at the NEW observation's map to catch the current step.
|
| 546 |
+
# ----------------------------------------------------------------
|
| 547 |
+
ms_next = next_obs.map_state
|
| 548 |
+
if ms_next is not None and chosen_action.startswith("move"):
|
| 549 |
+
ax, ay = ms_next.agent_x, ms_next.agent_y
|
| 550 |
+
gw, gh = ms_next.grid_w, ms_next.grid_h
|
| 551 |
+
fire_grid = ms_next.fire_grid
|
| 552 |
+
for dx, dy in ((0, 1), (0, -1), (1, 0), (-1, 0)):
|
| 553 |
+
nx, ny = ax + dx, ay + dy
|
| 554 |
+
if 0 <= nx < gw and 0 <= ny < gh:
|
| 555 |
+
if float(fire_grid[ny * gw + nx]) > 0.15:
|
| 556 |
+
reward -= 0.15 # early fire-proximity warning
|
| 557 |
+
break
|
| 558 |
+
|
| 559 |
+
# ----------------------------------------------------------------
|
| 560 |
+
# Reward shaping 3 — anti-loop penalty
|
| 561 |
+
# If the agent steps onto a cell it occupied in the last LOOP_WINDOW
|
| 562 |
+
# steps, it is circling. Penalise to force forward exploration.
|
| 563 |
+
# Fires only on move actions — wait is already penalised above.
|
| 564 |
+
# ----------------------------------------------------------------
|
| 565 |
+
if ms_next is not None and chosen_action.startswith("move"):
|
| 566 |
+
cur_pos = (ms_next.agent_x, ms_next.agent_y)
|
| 567 |
+
if cur_pos in recent_positions:
|
| 568 |
+
reward -= 0.2 # break the loop
|
| 569 |
+
recent_positions.append(cur_pos)
|
| 570 |
+
|
| 571 |
+
done = bool(next_obs.done)
|
| 572 |
+
|
| 573 |
+
buffer.obs.append(state_vec)
|
| 574 |
+
buffer.masks.append(action_mask)
|
| 575 |
+
buffer.actions.append(action_idx)
|
| 576 |
+
buffer.rewards.append(reward)
|
| 577 |
+
buffer.log_probs.append(float(log_prob_t.item()))
|
| 578 |
+
buffer.values.append(float(value_t.item()))
|
| 579 |
+
buffer.dones.append(done)
|
| 580 |
+
|
| 581 |
+
total_reward += reward
|
| 582 |
+
steps += 1
|
| 583 |
+
final_health = next_obs.agent_health
|
| 584 |
+
evacuated = next_obs.agent_evacuated
|
| 585 |
+
|
| 586 |
+
frames.append(encoder.encode(next_obs))
|
| 587 |
+
observation = next_obs
|
| 588 |
+
if done:
|
| 589 |
+
break
|
| 590 |
+
|
| 591 |
+
return EpisodeResult(
|
| 592 |
+
total_reward=total_reward,
|
| 593 |
+
steps=steps,
|
| 594 |
+
evacuated=evacuated,
|
| 595 |
+
final_health=final_health,
|
| 596 |
+
difficulty=difficulty,
|
| 597 |
+
)
|
| 598 |
+
|
| 599 |
+
|
| 600 |
+
# ---------------------------------------------------------------------------
|
| 601 |
+
# PPO update
|
| 602 |
+
# ---------------------------------------------------------------------------
|
| 603 |
+
|
| 604 |
+
def ppo_update(
|
| 605 |
+
network: ActorCritic,
|
| 606 |
+
optimizer: Adam,
|
| 607 |
+
buffer: RolloutBuffer,
|
| 608 |
+
device: torch.device,
|
| 609 |
+
clip_eps: float,
|
| 610 |
+
value_clip_eps: float,
|
| 611 |
+
entropy_coef: float,
|
| 612 |
+
value_coef: float,
|
| 613 |
+
n_epochs: int,
|
| 614 |
+
minibatch_size: int,
|
| 615 |
+
gamma: float,
|
| 616 |
+
gae_lambda: float,
|
| 617 |
+
max_grad_norm: float,
|
| 618 |
+
) -> Dict[str, float]:
|
| 619 |
+
"""Full PPO update over the collected rollout buffer."""
|
| 620 |
+
rewards = np.array(buffer.rewards, dtype=np.float32)
|
| 621 |
+
values = np.array(buffer.values, dtype=np.float32)
|
| 622 |
+
dones = np.array(buffer.dones, dtype=np.float32)
|
| 623 |
+
|
| 624 |
+
returns, advantages = compute_gae(rewards, values, dones, gamma, gae_lambda)
|
| 625 |
+
|
| 626 |
+
# Normalize advantages across the whole batch (reduces variance)
|
| 627 |
+
advantages = (advantages - advantages.mean()) / (advantages.std() + 1e-8)
|
| 628 |
+
|
| 629 |
+
obs_arr = torch.tensor(np.stack(buffer.obs), dtype=torch.float32, device=device)
|
| 630 |
+
mask_arr = torch.tensor(np.stack(buffer.masks), dtype=torch.float32, device=device)
|
| 631 |
+
action_arr = torch.tensor(buffer.actions, dtype=torch.long, device=device)
|
| 632 |
+
old_logp_arr = torch.tensor(buffer.log_probs, dtype=torch.float32, device=device)
|
| 633 |
+
return_arr = torch.tensor(returns, dtype=torch.float32, device=device)
|
| 634 |
+
adv_arr = torch.tensor(advantages, dtype=torch.float32, device=device)
|
| 635 |
+
old_value_arr = torch.tensor(values, dtype=torch.float32, device=device)
|
| 636 |
+
|
| 637 |
+
T = len(buffer)
|
| 638 |
+
metrics = {"policy_loss": 0.0, "value_loss": 0.0, "entropy": 0.0, "approx_kl": 0.0, "clip_frac": 0.0}
|
| 639 |
+
n_updates = 0
|
| 640 |
+
|
| 641 |
+
network.train()
|
| 642 |
+
for _ in range(n_epochs):
|
| 643 |
+
perm = torch.randperm(T, device=device)
|
| 644 |
+
for start in range(0, T, minibatch_size):
|
| 645 |
+
idx = perm[start:start + minibatch_size]
|
| 646 |
+
if len(idx) < 2:
|
| 647 |
+
continue
|
| 648 |
+
|
| 649 |
+
log_prob, value, entropy = network.evaluate(obs_arr[idx], mask_arr[idx], action_arr[idx])
|
| 650 |
+
|
| 651 |
+
# PPO ratio and clipped surrogate loss
|
| 652 |
+
ratio = torch.exp(log_prob - old_logp_arr[idx])
|
| 653 |
+
adv_mb = adv_arr[idx]
|
| 654 |
+
surr1 = ratio * adv_mb
|
| 655 |
+
surr2 = torch.clamp(ratio, 1.0 - clip_eps, 1.0 + clip_eps) * adv_mb
|
| 656 |
+
policy_loss = -torch.min(surr1, surr2).mean()
|
| 657 |
+
|
| 658 |
+
# Value loss with optional clipping (stabilises critic)
|
| 659 |
+
ret_mb = return_arr[idx]
|
| 660 |
+
old_val_mb = old_value_arr[idx]
|
| 661 |
+
value_pred_clipped = old_val_mb + torch.clamp(value - old_val_mb, -value_clip_eps, value_clip_eps)
|
| 662 |
+
value_loss = torch.max(
|
| 663 |
+
F.mse_loss(value, ret_mb),
|
| 664 |
+
F.mse_loss(value_pred_clipped, ret_mb),
|
| 665 |
+
)
|
| 666 |
+
|
| 667 |
+
entropy_loss = -entropy.mean()
|
| 668 |
+
|
| 669 |
+
loss = policy_loss + value_coef * value_loss + entropy_coef * entropy_loss
|
| 670 |
+
|
| 671 |
+
optimizer.zero_grad()
|
| 672 |
+
loss.backward()
|
| 673 |
+
nn.utils.clip_grad_norm_(network.parameters(), max_grad_norm)
|
| 674 |
+
optimizer.step()
|
| 675 |
+
|
| 676 |
+
with torch.no_grad():
|
| 677 |
+
approx_kl = ((ratio - 1) - (log_prob - old_logp_arr[idx])).mean().item()
|
| 678 |
+
clip_frac = ((ratio - 1.0).abs() > clip_eps).float().mean().item()
|
| 679 |
+
|
| 680 |
+
metrics["policy_loss"] += policy_loss.item()
|
| 681 |
+
metrics["value_loss"] += value_loss.item()
|
| 682 |
+
metrics["entropy"] += entropy.mean().item()
|
| 683 |
+
metrics["approx_kl"] += approx_kl
|
| 684 |
+
metrics["clip_frac"] += clip_frac
|
| 685 |
+
n_updates += 1
|
| 686 |
+
|
| 687 |
+
if n_updates > 0:
|
| 688 |
+
for k in metrics:
|
| 689 |
+
metrics[k] /= n_updates
|
| 690 |
+
return metrics
|
| 691 |
+
|
| 692 |
+
|
| 693 |
+
# ---------------------------------------------------------------------------
|
| 694 |
+
# Evaluation
|
| 695 |
+
# ---------------------------------------------------------------------------
|
| 696 |
+
|
| 697 |
+
def evaluate_policy(
|
| 698 |
+
env: PyreEnvironment,
|
| 699 |
+
network: ActorCritic,
|
| 700 |
+
encoder: ObservationEncoder,
|
| 701 |
+
device: torch.device,
|
| 702 |
+
difficulty: str,
|
| 703 |
+
history_length: int,
|
| 704 |
+
n_episodes: int,
|
| 705 |
+
) -> Dict[str, float]:
|
| 706 |
+
rewards, successes, steps = [], [], []
|
| 707 |
+
dummy_buffer = RolloutBuffer()
|
| 708 |
+
for _ in range(n_episodes):
|
| 709 |
+
result = run_episode(
|
| 710 |
+
env=env, network=network, encoder=encoder, device=device,
|
| 711 |
+
difficulty=difficulty, history_length=history_length,
|
| 712 |
+
buffer=dummy_buffer, deterministic=True,
|
| 713 |
+
)
|
| 714 |
+
dummy_buffer.clear()
|
| 715 |
+
rewards.append(result.total_reward)
|
| 716 |
+
successes.append(float(result.evacuated))
|
| 717 |
+
steps.append(result.steps)
|
| 718 |
+
return {
|
| 719 |
+
"reward_mean": float(np.mean(rewards)),
|
| 720 |
+
"reward_max": float(np.max(rewards)),
|
| 721 |
+
"success_rate": float(np.mean(successes)),
|
| 722 |
+
"steps_mean": float(np.mean(steps)),
|
| 723 |
+
}
|
| 724 |
+
|
| 725 |
+
|
| 726 |
+
# ---------------------------------------------------------------------------
|
| 727 |
+
# PNG graph (matplotlib)
|
| 728 |
+
# ---------------------------------------------------------------------------
|
| 729 |
+
|
| 730 |
+
def save_training_graph_png(
|
| 731 |
+
path: Path,
|
| 732 |
+
episode_rows: List[Dict],
|
| 733 |
+
eval_rows: List[Dict],
|
| 734 |
+
window: int = 20,
|
| 735 |
+
) -> None:
|
| 736 |
+
"""Save a publication-quality PNG training graph with dual Y-axes."""
|
| 737 |
+
try:
|
| 738 |
+
import matplotlib
|
| 739 |
+
matplotlib.use("Agg") # non-interactive backend — no display needed
|
| 740 |
+
import matplotlib.pyplot as plt
|
| 741 |
+
import matplotlib.ticker as mticker
|
| 742 |
+
except ImportError:
|
| 743 |
+
print("[warn] matplotlib not installed — skipping PNG graph. Run: uv pip install matplotlib")
|
| 744 |
+
return
|
| 745 |
+
|
| 746 |
+
if not episode_rows:
|
| 747 |
+
return
|
| 748 |
+
|
| 749 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 750 |
+
|
| 751 |
+
episodes = [int(r["episode"]) for r in episode_rows]
|
| 752 |
+
rewards = [float(r["reward"]) for r in episode_rows]
|
| 753 |
+
evacuated = [float(r["evacuated"]) for r in episode_rows]
|
| 754 |
+
difficulty = [str(r["difficulty"]) for r in episode_rows]
|
| 755 |
+
|
| 756 |
+
# Moving average helper
|
| 757 |
+
def ma(values: list, w: int) -> list:
|
| 758 |
+
out, run, q = [], 0.0, []
|
| 759 |
+
for v in values:
|
| 760 |
+
q.append(v); run += v
|
| 761 |
+
if len(q) > w: run -= q.pop(0)
|
| 762 |
+
out.append(run / len(q))
|
| 763 |
+
return out
|
| 764 |
+
|
| 765 |
+
reward_ma = ma(rewards, window)
|
| 766 |
+
success_ma = ma(evacuated, window)
|
| 767 |
+
|
| 768 |
+
eval_eps = [int(r["episode"]) for r in eval_rows]
|
| 769 |
+
eval_succ = [float(r["success_rate"]) for r in eval_rows]
|
| 770 |
+
|
| 771 |
+
# Difficulty shading regions
|
| 772 |
+
diff_colors = {"easy": "#d4edda", "medium": "#fff3cd", "hard": "#f8d7da"}
|
| 773 |
+
regions: List[tuple] = []
|
| 774 |
+
if difficulty:
|
| 775 |
+
cur, start = difficulty[0], episodes[0]
|
| 776 |
+
for ep, d in zip(episodes[1:], difficulty[1:]):
|
| 777 |
+
if d != cur:
|
| 778 |
+
regions.append((start, ep, cur))
|
| 779 |
+
cur, start = d, ep
|
| 780 |
+
regions.append((start, episodes[-1], cur))
|
| 781 |
+
|
| 782 |
+
fig, ax1 = plt.subplots(figsize=(14, 6))
|
| 783 |
+
ax2 = ax1.twinx()
|
| 784 |
+
|
| 785 |
+
# Shade difficulty regions
|
| 786 |
+
for x0, x1, diff in regions:
|
| 787 |
+
ax1.axvspan(x0, x1, color=diff_colors.get(diff, "#eeeeee"), alpha=0.35, zorder=0)
|
| 788 |
+
|
| 789 |
+
# Zero line
|
| 790 |
+
ax1.axhline(0, color="#aaaaaa", linewidth=0.8, linestyle="--", zorder=1)
|
| 791 |
+
|
| 792 |
+
# Raw reward (faint)
|
| 793 |
+
ax1.plot(episodes, rewards, color="#d1c7bc", linewidth=0.8,
|
| 794 |
+
alpha=0.6, label="Episode reward", zorder=2)
|
| 795 |
+
|
| 796 |
+
# Reward moving average
|
| 797 |
+
ax1.plot(episodes, reward_ma, color="#c1661c", linewidth=2.5,
|
| 798 |
+
label=f"Reward (MA-{window})", zorder=3)
|
| 799 |
+
|
| 800 |
+
# Success moving average (right axis)
|
| 801 |
+
ax2.plot(episodes, success_ma, color="#1a7a8a", linewidth=2.5,
|
| 802 |
+
linestyle="-", label=f"Success rate (MA-{window})", zorder=3)
|
| 803 |
+
|
| 804 |
+
# Eval checkpoints
|
| 805 |
+
if eval_eps:
|
| 806 |
+
ax2.scatter(eval_eps, eval_succ, color="#0d5b6b", s=60, zorder=5,
|
| 807 |
+
marker="D", label="Eval success", edgecolors="white", linewidths=1.2)
|
| 808 |
+
|
| 809 |
+
# Axes labels & formatting
|
| 810 |
+
ax1.set_xlabel("Episode", fontsize=13, fontweight="bold", labelpad=8)
|
| 811 |
+
ax1.set_ylabel("Reward", fontsize=13, fontweight="bold", color="#c1661c", labelpad=8)
|
| 812 |
+
ax2.set_ylabel("Success Rate", fontsize=13, fontweight="bold", color="#1a7a8a", labelpad=8)
|
| 813 |
+
|
| 814 |
+
ax1.tick_params(axis="y", labelcolor="#c1661c")
|
| 815 |
+
ax2.tick_params(axis="y", labelcolor="#1a7a8a")
|
| 816 |
+
ax2.set_ylim(-0.05, 1.05)
|
| 817 |
+
ax2.yaxis.set_major_formatter(mticker.PercentFormatter(xmax=1.0, decimals=0))
|
| 818 |
+
|
| 819 |
+
ax1.grid(True, which="major", linestyle="--", linewidth=0.6,
|
| 820 |
+
color="#dddddd", alpha=0.8, zorder=0)
|
| 821 |
+
ax1.set_xlim(episodes[0], episodes[-1])
|
| 822 |
+
|
| 823 |
+
ax1.tick_params(axis="x", labelsize=10)
|
| 824 |
+
ax1.tick_params(axis="y", labelsize=10)
|
| 825 |
+
ax2.tick_params(axis="y", labelsize=10)
|
| 826 |
+
|
| 827 |
+
# Title
|
| 828 |
+
total_eps = episodes[-1]
|
| 829 |
+
final_sr = success_ma[-1] if success_ma else 0.0
|
| 830 |
+
fig.suptitle(
|
| 831 |
+
f"Pyre PPO Training — {total_eps} episodes | final success rate: {final_sr:.0%}",
|
| 832 |
+
fontsize=14, fontweight="bold", y=1.01,
|
| 833 |
+
)
|
| 834 |
+
|
| 835 |
+
# Difficulty legend patches
|
| 836 |
+
import matplotlib.patches as mpatches
|
| 837 |
+
diff_patches = [
|
| 838 |
+
mpatches.Patch(color=diff_colors[d], alpha=0.6, label=d.capitalize())
|
| 839 |
+
for d in ["easy", "medium", "hard"] if any(r == d for r in difficulty)
|
| 840 |
+
]
|
| 841 |
+
|
| 842 |
+
# Combine legends from both axes
|
| 843 |
+
h1, l1 = ax1.get_legend_handles_labels()
|
| 844 |
+
h2, l2 = ax2.get_legend_handles_labels()
|
| 845 |
+
ax1.legend(h1 + h2 + diff_patches, l1 + l2 + [p.get_label() for p in diff_patches],
|
| 846 |
+
loc="upper left", fontsize=9, framealpha=0.85)
|
| 847 |
+
|
| 848 |
+
fig.tight_layout()
|
| 849 |
+
fig.savefig(path, dpi=150, bbox_inches="tight")
|
| 850 |
+
plt.close(fig)
|
| 851 |
+
|
| 852 |
+
|
| 853 |
+
# ---------------------------------------------------------------------------
|
| 854 |
+
# Curriculum scheduling
|
| 855 |
+
# ---------------------------------------------------------------------------
|
| 856 |
+
|
| 857 |
+
def build_curriculum(schedule_str: str, n_episodes: int) -> List[str]:
|
| 858 |
+
"""Expand comma-separated difficulty stages evenly over n_episodes.
|
| 859 |
+
|
| 860 |
+
Example: 'easy,medium,hard' with 300 episodes → 100 each.
|
| 861 |
+
Used only when patience_threshold=0 (static schedule).
|
| 862 |
+
"""
|
| 863 |
+
stages = [s.strip().lower() for s in schedule_str.split(",") if s.strip()]
|
| 864 |
+
if not stages:
|
| 865 |
+
stages = ["medium"]
|
| 866 |
+
for s in stages:
|
| 867 |
+
if s not in DIFFICULTIES:
|
| 868 |
+
raise ValueError(f"Unknown difficulty '{s}'. Choose from {DIFFICULTIES}.")
|
| 869 |
+
seg = max(1, n_episodes // len(stages))
|
| 870 |
+
schedule = []
|
| 871 |
+
for s in stages:
|
| 872 |
+
schedule.extend([s] * seg)
|
| 873 |
+
while len(schedule) < n_episodes:
|
| 874 |
+
schedule.append(stages[-1])
|
| 875 |
+
return schedule[:n_episodes]
|
| 876 |
+
|
| 877 |
+
|
| 878 |
+
class PatienceCurriculum:
|
| 879 |
+
"""Dynamic difficulty scheduler that gates advancement on sustained success rate.
|
| 880 |
+
|
| 881 |
+
Stays on current difficulty until success_rate_30 >= threshold for
|
| 882 |
+
patience_window consecutive episodes, then advances to the next stage.
|
| 883 |
+
During the hard phase an optional mix_ratio fraction of episodes are
|
| 884 |
+
replayed on the previous (medium) difficulty to prevent catastrophic
|
| 885 |
+
forgetting of the medium policy.
|
| 886 |
+
|
| 887 |
+
Args:
|
| 888 |
+
stages: ordered list of difficulty strings, e.g. ['easy','medium','hard']
|
| 889 |
+
threshold: minimum success rate (0–1) required before advancing
|
| 890 |
+
patience_window: number of consecutive episodes that must meet threshold
|
| 891 |
+
mix_ratio: fraction of hard-phase episodes to run on medium instead (0–1)
|
| 892 |
+
"""
|
| 893 |
+
|
| 894 |
+
def __init__(
|
| 895 |
+
self,
|
| 896 |
+
stages: List[str],
|
| 897 |
+
threshold: float,
|
| 898 |
+
patience_window: int,
|
| 899 |
+
mix_ratio: float = 0.0,
|
| 900 |
+
) -> None:
|
| 901 |
+
self.stages = stages
|
| 902 |
+
self.threshold = threshold
|
| 903 |
+
self.patience_window = patience_window
|
| 904 |
+
self.mix_ratio = mix_ratio
|
| 905 |
+
self.stage_idx = 0
|
| 906 |
+
self._streak = 0
|
| 907 |
+
|
| 908 |
+
@property
|
| 909 |
+
def current(self) -> str:
|
| 910 |
+
return self.stages[self.stage_idx]
|
| 911 |
+
|
| 912 |
+
def step(self, success_rate_30: float) -> str:
|
| 913 |
+
"""Call once per episode *after* appending to success_window.
|
| 914 |
+
|
| 915 |
+
Returns the difficulty to use for the *next* episode.
|
| 916 |
+
Also handles the hard-phase medium-mix injection.
|
| 917 |
+
"""
|
| 918 |
+
if self.stage_idx < len(self.stages) - 1:
|
| 919 |
+
if success_rate_30 >= self.threshold:
|
| 920 |
+
self._streak += 1
|
| 921 |
+
else:
|
| 922 |
+
self._streak = 0
|
| 923 |
+
if self._streak >= self.patience_window:
|
| 924 |
+
self.stage_idx += 1
|
| 925 |
+
self._streak = 0
|
| 926 |
+
print(
|
| 927 |
+
f" [curriculum] Advanced to '{self.current}' "
|
| 928 |
+
f"(success_rate_30={success_rate_30:.2f} >= {self.threshold} "
|
| 929 |
+
f"for {self.patience_window} eps)"
|
| 930 |
+
)
|
| 931 |
+
|
| 932 |
+
# Hard-phase mix: occasionally replay medium to prevent forgetting
|
| 933 |
+
if self.current == "hard" and self.mix_ratio > 0.0 and len(self.stages) >= 2:
|
| 934 |
+
prev = self.stages[self.stage_idx - 1]
|
| 935 |
+
if np.random.rand() < self.mix_ratio:
|
| 936 |
+
return prev # medium replay episode
|
| 937 |
+
return self.current
|
| 938 |
+
|
| 939 |
+
|
| 940 |
+
# ---------------------------------------------------------------------------
|
| 941 |
+
# Checkpoint
|
| 942 |
+
# ---------------------------------------------------------------------------
|
| 943 |
+
|
| 944 |
+
def save_checkpoint(
|
| 945 |
+
path: Path,
|
| 946 |
+
network: ActorCritic,
|
| 947 |
+
optimizer: Adam,
|
| 948 |
+
scheduler,
|
| 949 |
+
episode: int,
|
| 950 |
+
args: argparse.Namespace,
|
| 951 |
+
) -> None:
|
| 952 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 953 |
+
torch.save({
|
| 954 |
+
"episode": episode,
|
| 955 |
+
"network_state": network.state_dict(),
|
| 956 |
+
"optimizer_state": optimizer.state_dict(),
|
| 957 |
+
"scheduler_state": scheduler.state_dict() if scheduler else None,
|
| 958 |
+
"args": vars(args),
|
| 959 |
+
}, path)
|
| 960 |
+
|
| 961 |
+
|
| 962 |
+
def load_checkpoint(
|
| 963 |
+
path: Path,
|
| 964 |
+
network: ActorCritic,
|
| 965 |
+
optimizer: Adam,
|
| 966 |
+
scheduler,
|
| 967 |
+
) -> int:
|
| 968 |
+
ckpt = torch.load(path, map_location="cpu", weights_only=False)
|
| 969 |
+
network.load_state_dict(ckpt["network_state"])
|
| 970 |
+
optimizer.load_state_dict(ckpt["optimizer_state"])
|
| 971 |
+
if scheduler and ckpt.get("scheduler_state"):
|
| 972 |
+
scheduler.load_state_dict(ckpt["scheduler_state"])
|
| 973 |
+
start_episode = int(ckpt.get("episode", 0))
|
| 974 |
+
print(f"[resume] Loaded checkpoint from episode {start_episode}: {path}")
|
| 975 |
+
return start_episode
|
| 976 |
+
|
| 977 |
+
|
| 978 |
+
# ---------------------------------------------------------------------------
|
| 979 |
+
# CSV logging
|
| 980 |
+
# ---------------------------------------------------------------------------
|
| 981 |
+
|
| 982 |
+
def save_csv(path: Path, rows: List[Dict]) -> None:
|
| 983 |
+
path.parent.mkdir(parents=True, exist_ok=True)
|
| 984 |
+
if not rows:
|
| 985 |
+
return
|
| 986 |
+
with path.open("w", newline="", encoding="utf-8") as f:
|
| 987 |
+
writer = csv.DictWriter(f, fieldnames=list(rows[0].keys()))
|
| 988 |
+
writer.writeheader()
|
| 989 |
+
writer.writerows(rows)
|
| 990 |
+
|
| 991 |
+
|
| 992 |
+
# ---------------------------------------------------------------------------
|
| 993 |
+
# Main training loop
|
| 994 |
+
# ---------------------------------------------------------------------------
|
| 995 |
+
|
| 996 |
+
def train(args: argparse.Namespace) -> None:
|
| 997 |
+
device = torch.device(args.device if torch.cuda.is_available() or args.device == "cpu" else "cpu")
|
| 998 |
+
if args.device == "cuda" and not torch.cuda.is_available():
|
| 999 |
+
print("[warn] CUDA not available - falling back to CPU.")
|
| 1000 |
+
|
| 1001 |
+
print(f"[config] device={device} episodes={args.episodes} batch={args.update_every} eps "
|
| 1002 |
+
f"hidden={args.hidden_sizes} frames={args.history_length}")
|
| 1003 |
+
print(f"[config] curriculum: {args.difficulty_schedule}")
|
| 1004 |
+
print(f"[config] PPO clip_eps={args.clip_eps} entropy={args.entropy_coef} lr={args.learning_rate}\n")
|
| 1005 |
+
|
| 1006 |
+
encoder = ObservationEncoder(mode=args.observation_mode)
|
| 1007 |
+
input_dim = encoder.base_dim * args.history_length
|
| 1008 |
+
|
| 1009 |
+
hidden_sizes = tuple(int(h) for h in args.hidden_sizes.split(","))
|
| 1010 |
+
network = ActorCritic(input_dim=input_dim, action_dim=ACTION_DIM, hidden_sizes=hidden_sizes).to(device)
|
| 1011 |
+
optimizer = Adam(network.parameters(), lr=args.learning_rate, eps=1e-5)
|
| 1012 |
+
|
| 1013 |
+
total_steps_for_scheduler = args.episodes // args.update_every
|
| 1014 |
+
scheduler = LinearLR(optimizer, start_factor=1.0, end_factor=args.lr_end_factor,
|
| 1015 |
+
total_iters=max(1, total_steps_for_scheduler)) if args.lr_decay else None
|
| 1016 |
+
|
| 1017 |
+
env = PyreEnvironment(max_steps=args.max_steps)
|
| 1018 |
+
|
| 1019 |
+
# Build curriculum — patience-gated (dynamic) or static
|
| 1020 |
+
stages = [s.strip().lower() for s in args.difficulty_schedule.split(",") if s.strip()]
|
| 1021 |
+
if args.patience_threshold > 0:
|
| 1022 |
+
patience_curriculum = PatienceCurriculum(
|
| 1023 |
+
stages=stages,
|
| 1024 |
+
threshold=args.patience_threshold,
|
| 1025 |
+
patience_window=args.patience_window,
|
| 1026 |
+
mix_ratio=args.hard_mix_ratio,
|
| 1027 |
+
)
|
| 1028 |
+
static_curriculum: Optional[List[str]] = None
|
| 1029 |
+
print(f"[curriculum] patience-gated: threshold={args.patience_threshold} "
|
| 1030 |
+
f"window={args.patience_window} mix={args.hard_mix_ratio}")
|
| 1031 |
+
else:
|
| 1032 |
+
patience_curriculum = None
|
| 1033 |
+
static_curriculum = build_curriculum(args.difficulty_schedule, args.episodes)
|
| 1034 |
+
print(f"[curriculum] static: {args.difficulty_schedule}")
|
| 1035 |
+
|
| 1036 |
+
start_episode = 0
|
| 1037 |
+
if args.resume:
|
| 1038 |
+
resume_path = Path(args.resume)
|
| 1039 |
+
if resume_path.exists():
|
| 1040 |
+
start_episode = load_checkpoint(resume_path, network, optimizer, scheduler)
|
| 1041 |
+
|
| 1042 |
+
# Tracking
|
| 1043 |
+
buffer = RolloutBuffer()
|
| 1044 |
+
episode_rows: List[Dict] = []
|
| 1045 |
+
eval_rows: List[Dict] = []
|
| 1046 |
+
reward_window: deque = deque(maxlen=30)
|
| 1047 |
+
success_window: deque = deque(maxlen=30)
|
| 1048 |
+
|
| 1049 |
+
n_params = sum(p.numel() for p in network.parameters())
|
| 1050 |
+
print(f"[network] Parameters: {n_params:,}")
|
| 1051 |
+
print(f"[network] Input dim: {input_dim:,} (encoder.base_dim={encoder.base_dim} x {args.history_length} frames)")
|
| 1052 |
+
print(f"[network] Action dim: {ACTION_DIM} (4 move + 4 look + 1 wait + {MAX_DOORS} open + {MAX_DOORS} close)")
|
| 1053 |
+
print()
|
| 1054 |
+
|
| 1055 |
+
t_start = time.time()
|
| 1056 |
+
|
| 1057 |
+
for ep_idx in range(start_episode, args.episodes):
|
| 1058 |
+
# Determine difficulty for this episode
|
| 1059 |
+
if patience_curriculum is not None:
|
| 1060 |
+
difficulty = patience_curriculum.current
|
| 1061 |
+
else:
|
| 1062 |
+
difficulty = static_curriculum[ep_idx] # type: ignore[index]
|
| 1063 |
+
|
| 1064 |
+
result = run_episode(
|
| 1065 |
+
env=env, network=network, encoder=encoder, device=device,
|
| 1066 |
+
difficulty=difficulty, history_length=args.history_length,
|
| 1067 |
+
buffer=buffer, deterministic=False,
|
| 1068 |
+
)
|
| 1069 |
+
|
| 1070 |
+
reward_window.append(result.total_reward)
|
| 1071 |
+
success_window.append(float(result.evacuated))
|
| 1072 |
+
|
| 1073 |
+
# Advance patience curriculum *after* updating success_window
|
| 1074 |
+
if patience_curriculum is not None:
|
| 1075 |
+
difficulty = patience_curriculum.step(float(np.mean(success_window)))
|
| 1076 |
+
|
| 1077 |
+
ep_num = ep_idx + 1
|
| 1078 |
+
episode_rows.append({
|
| 1079 |
+
"episode": ep_num,
|
| 1080 |
+
"difficulty": difficulty,
|
| 1081 |
+
"reward": round(result.total_reward, 4),
|
| 1082 |
+
"evacuated": int(result.evacuated),
|
| 1083 |
+
"steps": result.steps,
|
| 1084 |
+
"final_health": round(result.final_health, 2),
|
| 1085 |
+
"reward_mean_30": round(float(np.mean(reward_window)), 4),
|
| 1086 |
+
"success_rate_30": round(float(np.mean(success_window)), 4),
|
| 1087 |
+
})
|
| 1088 |
+
|
| 1089 |
+
elapsed = time.time() - t_start
|
| 1090 |
+
print(
|
| 1091 |
+
f"ep={ep_num:04d} [{difficulty:<6}] "
|
| 1092 |
+
f"steps={result.steps:03d} "
|
| 1093 |
+
f"reward={result.total_reward:+8.3f} "
|
| 1094 |
+
f"evac={int(result.evacuated)} "
|
| 1095 |
+
f"hp={result.final_health:5.1f} "
|
| 1096 |
+
f"suc30={float(np.mean(success_window)):.2f} "
|
| 1097 |
+
f"r30={float(np.mean(reward_window)):+7.2f} "
|
| 1098 |
+
f"t={elapsed:.0f}s"
|
| 1099 |
+
)
|
| 1100 |
+
|
| 1101 |
+
# PPO update every N episodes
|
| 1102 |
+
should_update = (ep_num % args.update_every == 0) or (ep_num == args.episodes)
|
| 1103 |
+
if should_update and len(buffer) > 0:
|
| 1104 |
+
ppo_metrics = ppo_update(
|
| 1105 |
+
network=network, optimizer=optimizer, buffer=buffer, device=device,
|
| 1106 |
+
clip_eps=args.clip_eps, value_clip_eps=args.clip_eps,
|
| 1107 |
+
entropy_coef=args.entropy_coef, value_coef=args.value_coef,
|
| 1108 |
+
n_epochs=args.update_epochs, minibatch_size=args.minibatch_size,
|
| 1109 |
+
gamma=args.gamma, gae_lambda=args.gae_lambda,
|
| 1110 |
+
max_grad_norm=args.max_grad_norm,
|
| 1111 |
+
)
|
| 1112 |
+
if scheduler:
|
| 1113 |
+
scheduler.step()
|
| 1114 |
+
buffer.clear()
|
| 1115 |
+
|
| 1116 |
+
cur_lr = optimizer.param_groups[0]["lr"]
|
| 1117 |
+
print(
|
| 1118 |
+
f" >> PPO update samples={len(buffer) if len(buffer) > 0 else 'flushed'} "
|
| 1119 |
+
f"pi_loss={ppo_metrics['policy_loss']:+.4f} "
|
| 1120 |
+
f"v_loss={ppo_metrics['value_loss']:.4f} "
|
| 1121 |
+
f"entropy={ppo_metrics['entropy']:.4f} "
|
| 1122 |
+
f"kl={ppo_metrics['approx_kl']:.4f} "
|
| 1123 |
+
f"clip%={ppo_metrics['clip_frac']:.2f} "
|
| 1124 |
+
f"lr={cur_lr:.2e}"
|
| 1125 |
+
)
|
| 1126 |
+
|
| 1127 |
+
# Periodic evaluation
|
| 1128 |
+
if args.eval_every > 0 and (ep_num % args.eval_every == 0 or ep_num == args.episodes):
|
| 1129 |
+
eval_m = evaluate_policy(
|
| 1130 |
+
env=env, network=network, encoder=encoder, device=device,
|
| 1131 |
+
difficulty=args.eval_difficulty, history_length=args.history_length,
|
| 1132 |
+
n_episodes=args.eval_episodes,
|
| 1133 |
+
)
|
| 1134 |
+
eval_rows.append({"episode": ep_num, "difficulty": args.eval_difficulty, **{k: round(v, 4) for k, v in eval_m.items()}})
|
| 1135 |
+
print(
|
| 1136 |
+
f" ** EVAL [{args.eval_difficulty}] "
|
| 1137 |
+
f"reward={eval_m['reward_mean']:+.3f} "
|
| 1138 |
+
f"success={eval_m['success_rate']:.2f} "
|
| 1139 |
+
f"steps={eval_m['steps_mean']:.1f}"
|
| 1140 |
+
)
|
| 1141 |
+
|
| 1142 |
+
# Periodic checkpoint
|
| 1143 |
+
if args.checkpoint and args.checkpoint_every > 0 and ep_num % args.checkpoint_every == 0:
|
| 1144 |
+
save_checkpoint(Path(args.checkpoint), network, optimizer, scheduler, ep_num, args)
|
| 1145 |
+
print(f" [ckpt] saved -> {args.checkpoint}")
|
| 1146 |
+
|
| 1147 |
+
# Final save
|
| 1148 |
+
if args.output:
|
| 1149 |
+
out = Path(args.output)
|
| 1150 |
+
save_checkpoint(out, network, optimizer, scheduler, args.episodes, args)
|
| 1151 |
+
print(f"\n[done] Model saved -> {out}")
|
| 1152 |
+
|
| 1153 |
+
if args.save_metrics:
|
| 1154 |
+
csv_path = out.with_suffix(".csv")
|
| 1155 |
+
save_csv(csv_path, episode_rows)
|
| 1156 |
+
print(f"[done] Metrics CSV -> {csv_path}")
|
| 1157 |
+
|
| 1158 |
+
if eval_rows:
|
| 1159 |
+
eval_csv = out.parent / (out.stem + "_eval.csv")
|
| 1160 |
+
save_csv(eval_csv, eval_rows)
|
| 1161 |
+
print(f"[done] Eval CSV -> {eval_csv}")
|
| 1162 |
+
|
| 1163 |
+
if args.save_graph:
|
| 1164 |
+
png_path = out.with_suffix(".png")
|
| 1165 |
+
save_training_graph_png(png_path, episode_rows, eval_rows)
|
| 1166 |
+
print(f"[done] Graph PNG -> {png_path}")
|
| 1167 |
+
|
| 1168 |
+
total_time = time.time() - t_start
|
| 1169 |
+
print(f"\n[summary] {args.episodes - start_episode} episodes in {total_time:.1f}s "
|
| 1170 |
+
f"({(args.episodes - start_episode) / max(1, total_time):.1f} eps/s)")
|
| 1171 |
+
print(f"[summary] Final success rate (last 30): {float(np.mean(success_window)):.2f}")
|
| 1172 |
+
print(f"[summary] Final reward mean (last 30): {float(np.mean(reward_window)):+.3f}")
|
| 1173 |
+
|
| 1174 |
+
|
| 1175 |
+
# ---------------------------------------------------------------------------
|
| 1176 |
+
# CLI
|
| 1177 |
+
# ---------------------------------------------------------------------------
|
| 1178 |
+
|
| 1179 |
+
def describe_env() -> None:
|
| 1180 |
+
print(__doc__)
|
| 1181 |
+
|
| 1182 |
+
|
| 1183 |
+
def parse_args() -> argparse.Namespace:
|
| 1184 |
+
p = argparse.ArgumentParser(
|
| 1185 |
+
description="PPO training for Pyre fire-evacuation environment",
|
| 1186 |
+
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
| 1187 |
+
)
|
| 1188 |
+
|
| 1189 |
+
# Training scale
|
| 1190 |
+
p.add_argument("--episodes", type=int, default=400, help="Total training episodes")
|
| 1191 |
+
p.add_argument("--max-steps", type=int, default=150, help="Max steps per episode")
|
| 1192 |
+
p.add_argument("--device", type=str, default="cuda", choices=("cuda", "cpu"), help="Torch device")
|
| 1193 |
+
|
| 1194 |
+
# Curriculum
|
| 1195 |
+
p.add_argument("--difficulty", type=str, default="easy", choices=DIFFICULTIES,
|
| 1196 |
+
help="Single difficulty (overridden by --difficulty-schedule if set)")
|
| 1197 |
+
p.add_argument("--difficulty-schedule", type=str, default="easy,medium,hard",
|
| 1198 |
+
help="Comma-separated curriculum stages. With --patience-threshold>0 these "
|
| 1199 |
+
"become gated stages; otherwise split evenly across episodes.")
|
| 1200 |
+
p.add_argument("--patience-threshold", type=float, default=0.65,
|
| 1201 |
+
help="Success-rate threshold (30-ep window) required before advancing to next "
|
| 1202 |
+
"difficulty. Set 0 to use static even-split schedule.")
|
| 1203 |
+
p.add_argument("--patience-window", type=int, default=15,
|
| 1204 |
+
help="Episodes that must sustain >= patience-threshold before advancing.")
|
| 1205 |
+
p.add_argument("--hard-mix-ratio", type=float, default=0.25,
|
| 1206 |
+
help="Fraction of hard-phase episodes to replay on medium (0=pure hard). "
|
| 1207 |
+
"Prevents catastrophic forgetting of the medium policy.")
|
| 1208 |
+
p.add_argument("--eval-difficulty", type=str, default="medium", choices=DIFFICULTIES)
|
| 1209 |
+
p.add_argument("--eval-episodes", type=int, default=10)
|
| 1210 |
+
p.add_argument("--eval-every", type=int, default=50)
|
| 1211 |
+
|
| 1212 |
+
# Observation
|
| 1213 |
+
p.add_argument("--observation-mode", type=str, default="visible", choices=("visible", "full"),
|
| 1214 |
+
help="'visible': partial obs (realistic); 'full': oracle grid (debug)")
|
| 1215 |
+
p.add_argument("--history-length", type=int, default=4,
|
| 1216 |
+
help="Frames stacked per observation (temporal context for partial obs)")
|
| 1217 |
+
|
| 1218 |
+
# Network
|
| 1219 |
+
p.add_argument("--hidden-sizes", type=str, default="512,256,128",
|
| 1220 |
+
help="Comma-separated MLP hidden layer sizes")
|
| 1221 |
+
|
| 1222 |
+
# PPO hyperparameters
|
| 1223 |
+
p.add_argument("--update-every", type=int, default=5,
|
| 1224 |
+
help="Episodes between PPO updates (smaller = faster feedback loop early in training)")
|
| 1225 |
+
p.add_argument("--update-epochs", type=int, default=4,
|
| 1226 |
+
help="Gradient passes over each collected batch (PPO allows >1)")
|
| 1227 |
+
p.add_argument("--minibatch-size", type=int, default=256)
|
| 1228 |
+
p.add_argument("--clip-eps", type=float, default=0.2, help="PPO surrogate clip ε")
|
| 1229 |
+
p.add_argument("--entropy-coef", type=float, default=0.03,
|
| 1230 |
+
help="Entropy bonus coefficient — higher = more exploration (0.03 default encourages early exit-seeking)")
|
| 1231 |
+
p.add_argument("--value-coef", type=float, default=0.5)
|
| 1232 |
+
p.add_argument("--gamma", type=float, default=0.99)
|
| 1233 |
+
p.add_argument("--gae-lambda", type=float, default=0.95)
|
| 1234 |
+
p.add_argument("--max-grad-norm", type=float, default=0.5)
|
| 1235 |
+
|
| 1236 |
+
# Optimizer / LR schedule
|
| 1237 |
+
p.add_argument("--learning-rate", type=float, default=3e-4)
|
| 1238 |
+
p.add_argument("--lr-decay", action="store_true", default=True,
|
| 1239 |
+
help="Linear LR decay to lr_end_factor × initial_lr over training")
|
| 1240 |
+
p.add_argument("--lr-end-factor", type=float, default=0.1,
|
| 1241 |
+
help="LR at end of training = initial_lr × this value")
|
| 1242 |
+
|
| 1243 |
+
# Persistence
|
| 1244 |
+
p.add_argument("--output", type=str, default="artifacts/pyre_ppo.pt",
|
| 1245 |
+
help="Path to save final model checkpoint")
|
| 1246 |
+
p.add_argument("--checkpoint", type=str, default="artifacts/pyre_ppo_checkpoint.pt",
|
| 1247 |
+
help="Path for periodic checkpoints (also used by --resume)")
|
| 1248 |
+
p.add_argument("--checkpoint-every", type=int, default=50)
|
| 1249 |
+
p.add_argument("--resume", type=str, default=None,
|
| 1250 |
+
help="Path to checkpoint to resume training from")
|
| 1251 |
+
p.add_argument("--save-metrics", action="store_true", default=True,
|
| 1252 |
+
help="Save per-episode metrics as CSV alongside the model")
|
| 1253 |
+
p.add_argument("--save-graph", action="store_true", default=True,
|
| 1254 |
+
help="Save a PNG training graph alongside the model (requires matplotlib)")
|
| 1255 |
+
|
| 1256 |
+
# Misc
|
| 1257 |
+
p.add_argument("--seed", type=int, default=42)
|
| 1258 |
+
p.add_argument("--describe-only", action="store_true",
|
| 1259 |
+
help="Print environment/algorithm description and exit")
|
| 1260 |
+
|
| 1261 |
+
return p.parse_args()
|
| 1262 |
+
|
| 1263 |
+
|
| 1264 |
+
def main() -> None:
|
| 1265 |
+
args = parse_args()
|
| 1266 |
+
|
| 1267 |
+
if args.describe_only:
|
| 1268 |
+
describe_env()
|
| 1269 |
+
return
|
| 1270 |
+
|
| 1271 |
+
torch.manual_seed(args.seed)
|
| 1272 |
+
np.random.seed(args.seed)
|
| 1273 |
+
|
| 1274 |
+
train(args)
|
| 1275 |
+
|
| 1276 |
+
|
| 1277 |
+
if __name__ == "__main__":
|
| 1278 |
+
main()
|
examples/train_torch_ppo_http.py
ADDED
|
@@ -0,0 +1,492 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""PPO trainer that talks to the Pyre env via HTTP (localhost:8000).
|
| 2 |
+
|
| 3 |
+
Identical training logic to train_torch_ppo.py, but the environment is
|
| 4 |
+
accessed through the REST API instead of a direct Python import. This
|
| 5 |
+
lets you run the server once and connect any number of training scripts,
|
| 6 |
+
remote notebooks, or evaluation tools to the same live instance.
|
| 7 |
+
|
| 8 |
+
Usage
|
| 9 |
+
-----
|
| 10 |
+
1. Start the server (in a separate terminal):
|
| 11 |
+
cd openenv-pyre
|
| 12 |
+
.venv/Scripts/python.exe server/app.py
|
| 13 |
+
|
| 14 |
+
2. Run this script:
|
| 15 |
+
.venv/Scripts/python.exe examples/train_torch_ppo_http.py
|
| 16 |
+
|
| 17 |
+
Optional flags (identical to train_torch_ppo.py):
|
| 18 |
+
--server Base URL of the Pyre server [default: http://localhost:8000]
|
| 19 |
+
--episodes Total training episodes [default: 400]
|
| 20 |
+
--difficulty-schedule Curriculum [default: easy,easy,easy,medium,medium]
|
| 21 |
+
--output Where to save the model .pt [default: artifacts/pyre_ppo_http.pt]
|
| 22 |
+
... (all other flags are the same as train_torch_ppo.py)
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
from __future__ import annotations
|
| 26 |
+
|
| 27 |
+
import argparse
|
| 28 |
+
import csv
|
| 29 |
+
import os
|
| 30 |
+
import sys
|
| 31 |
+
import time
|
| 32 |
+
from collections import deque
|
| 33 |
+
from dataclasses import dataclass, field
|
| 34 |
+
from pathlib import Path
|
| 35 |
+
from typing import Any, Dict, List, Optional
|
| 36 |
+
|
| 37 |
+
import numpy as np
|
| 38 |
+
import requests
|
| 39 |
+
import torch
|
| 40 |
+
import torch.nn as nn
|
| 41 |
+
import torch.optim as optim
|
| 42 |
+
from torch.distributions import Categorical
|
| 43 |
+
|
| 44 |
+
# ---------------------------------------------------------------------------
|
| 45 |
+
# Resolve project root so we can import shared models regardless of CWD
|
| 46 |
+
# ---------------------------------------------------------------------------
|
| 47 |
+
_HERE = Path(__file__).resolve().parent
|
| 48 |
+
_ROOT = _HERE.parent
|
| 49 |
+
if str(_ROOT) not in sys.path:
|
| 50 |
+
sys.path.insert(0, str(_ROOT))
|
| 51 |
+
|
| 52 |
+
try:
|
| 53 |
+
from models import PyreAction, PyreMapState, PyreObservation
|
| 54 |
+
except ImportError:
|
| 55 |
+
from openenv_pyre.models import PyreAction, PyreMapState, PyreObservation
|
| 56 |
+
|
| 57 |
+
# Reuse all shared utilities from the direct-import trainer
|
| 58 |
+
from examples.train_torch_ppo import (
|
| 59 |
+
ACTION_KEYS,
|
| 60 |
+
ACTION_DIM,
|
| 61 |
+
ACTION_TO_INDEX,
|
| 62 |
+
DIFFICULTIES,
|
| 63 |
+
MAX_DOORS,
|
| 64 |
+
MAX_GRID_H,
|
| 65 |
+
MAX_GRID_W,
|
| 66 |
+
WAIT_KEY,
|
| 67 |
+
WINDS,
|
| 68 |
+
ActorCritic,
|
| 69 |
+
ObservationEncoder,
|
| 70 |
+
RolloutBuffer,
|
| 71 |
+
action_index_to_env_action,
|
| 72 |
+
build_action_mask,
|
| 73 |
+
compute_gae,
|
| 74 |
+
ppo_update,
|
| 75 |
+
save_training_graph_png,
|
| 76 |
+
)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
# ---------------------------------------------------------------------------
|
| 80 |
+
# HTTP environment wrapper
|
| 81 |
+
# ---------------------------------------------------------------------------
|
| 82 |
+
|
| 83 |
+
class HttpPyreEnv:
|
| 84 |
+
"""Thin wrapper around the Pyre REST API.
|
| 85 |
+
|
| 86 |
+
Exposes the same ``reset()`` / ``step()`` interface as ``PyreEnvironment``
|
| 87 |
+
so the episode runner needs no changes.
|
| 88 |
+
|
| 89 |
+
POST /reset → {"difficulty": str, "seed"?: int}
|
| 90 |
+
POST /step → {"action": str, "direction"?: str,
|
| 91 |
+
"target_id"?: str, "door_state"?: str}
|
| 92 |
+
Both return → {"observation": {...}, "reward": float,
|
| 93 |
+
"done": bool, "metadata": {...}}
|
| 94 |
+
"""
|
| 95 |
+
|
| 96 |
+
def __init__(self, base_url: str = "http://localhost:8000", timeout: int = 15):
|
| 97 |
+
self.base_url = base_url.rstrip("/")
|
| 98 |
+
self.timeout = timeout
|
| 99 |
+
self.session = requests.Session()
|
| 100 |
+
self.session.headers.update({"Content-Type": "application/json"})
|
| 101 |
+
|
| 102 |
+
# ------------------------------------------------------------------
|
| 103 |
+
def _parse(self, data: Dict[str, Any]) -> PyreObservation:
|
| 104 |
+
"""Convert a raw JSON response dict into a PyreObservation."""
|
| 105 |
+
obs_raw = data.get("observation", data)
|
| 106 |
+
|
| 107 |
+
map_state: Optional[PyreMapState] = None
|
| 108 |
+
ms_raw = obs_raw.get("map_state")
|
| 109 |
+
if ms_raw:
|
| 110 |
+
map_state = PyreMapState(**ms_raw)
|
| 111 |
+
|
| 112 |
+
return PyreObservation(
|
| 113 |
+
narrative=obs_raw.get("narrative", ""),
|
| 114 |
+
agent_evacuated=obs_raw.get("agent_evacuated", False),
|
| 115 |
+
location_label=obs_raw.get("location_label", ""),
|
| 116 |
+
smoke_level=obs_raw.get("smoke_level", "none"),
|
| 117 |
+
fire_visible=obs_raw.get("fire_visible", False),
|
| 118 |
+
fire_direction=obs_raw.get("fire_direction"),
|
| 119 |
+
agent_health=float(obs_raw.get("agent_health", 100.0)),
|
| 120 |
+
health_status=obs_raw.get("health_status", "Good"),
|
| 121 |
+
wind_dir=obs_raw.get("wind_dir", "CALM"),
|
| 122 |
+
visible_objects=obs_raw.get("visible_objects", []),
|
| 123 |
+
blocked_exit_ids=obs_raw.get("blocked_exit_ids", []),
|
| 124 |
+
audible_signals=obs_raw.get("audible_signals", []),
|
| 125 |
+
elapsed_steps=obs_raw.get("elapsed_steps", 0),
|
| 126 |
+
last_action_feedback=obs_raw.get("last_action_feedback", ""),
|
| 127 |
+
available_actions_hint=obs_raw.get("available_actions_hint", []),
|
| 128 |
+
map_state=map_state,
|
| 129 |
+
reward=float(data.get("reward", 0.0)),
|
| 130 |
+
done=bool(data.get("done", False)),
|
| 131 |
+
metadata=data.get("metadata", {}),
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
# ------------------------------------------------------------------
|
| 135 |
+
def reset(self, difficulty: str = "easy", seed: Optional[int] = None) -> PyreObservation:
|
| 136 |
+
payload: Dict[str, Any] = {"difficulty": difficulty}
|
| 137 |
+
if seed is not None:
|
| 138 |
+
payload["seed"] = seed
|
| 139 |
+
resp = self.session.post(
|
| 140 |
+
f"{self.base_url}/reset", json=payload, timeout=self.timeout
|
| 141 |
+
)
|
| 142 |
+
resp.raise_for_status()
|
| 143 |
+
return self._parse(resp.json())
|
| 144 |
+
|
| 145 |
+
# ------------------------------------------------------------------
|
| 146 |
+
def step(self, action: PyreAction) -> PyreObservation:
|
| 147 |
+
payload: Dict[str, Any] = {"action": action.action}
|
| 148 |
+
if action.direction is not None:
|
| 149 |
+
payload["direction"] = action.direction
|
| 150 |
+
if action.target_id is not None:
|
| 151 |
+
payload["target_id"] = action.target_id
|
| 152 |
+
if action.door_state is not None:
|
| 153 |
+
payload["door_state"] = action.door_state
|
| 154 |
+
resp = self.session.post(
|
| 155 |
+
f"{self.base_url}/step", json=payload, timeout=self.timeout
|
| 156 |
+
)
|
| 157 |
+
resp.raise_for_status()
|
| 158 |
+
return self._parse(resp.json())
|
| 159 |
+
|
| 160 |
+
# ------------------------------------------------------------------
|
| 161 |
+
def health_check(self) -> bool:
|
| 162 |
+
"""Return True if the server is reachable."""
|
| 163 |
+
try:
|
| 164 |
+
r = self.session.get(f"{self.base_url}/state", timeout=5)
|
| 165 |
+
return r.status_code < 500
|
| 166 |
+
except requests.exceptions.RequestException:
|
| 167 |
+
return False
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
# ---------------------------------------------------------------------------
|
| 171 |
+
# Episode runner (identical reward shaping as train_torch_ppo.py)
|
| 172 |
+
# ---------------------------------------------------------------------------
|
| 173 |
+
|
| 174 |
+
@dataclass
|
| 175 |
+
class EpisodeResult:
|
| 176 |
+
total_reward: float
|
| 177 |
+
steps: int
|
| 178 |
+
evacuated: bool
|
| 179 |
+
final_health: float
|
| 180 |
+
difficulty: str
|
| 181 |
+
|
| 182 |
+
|
| 183 |
+
def run_episode(
|
| 184 |
+
env: HttpPyreEnv,
|
| 185 |
+
network: ActorCritic,
|
| 186 |
+
encoder: ObservationEncoder,
|
| 187 |
+
device: torch.device,
|
| 188 |
+
difficulty: str,
|
| 189 |
+
history_length: int,
|
| 190 |
+
buffer: RolloutBuffer,
|
| 191 |
+
deterministic: bool = False,
|
| 192 |
+
) -> EpisodeResult:
|
| 193 |
+
observation = env.reset(difficulty=difficulty)
|
| 194 |
+
zero_frame = np.zeros(encoder.base_dim, dtype=np.float32)
|
| 195 |
+
frames: deque = deque([zero_frame.copy() for _ in range(history_length)], maxlen=history_length)
|
| 196 |
+
frames.append(encoder.encode(observation))
|
| 197 |
+
|
| 198 |
+
total_reward = 0.0
|
| 199 |
+
final_health = observation.agent_health
|
| 200 |
+
evacuated = False
|
| 201 |
+
steps = 0
|
| 202 |
+
LOOP_WINDOW = 12
|
| 203 |
+
recent_positions: deque = deque(maxlen=LOOP_WINDOW)
|
| 204 |
+
|
| 205 |
+
network.eval()
|
| 206 |
+
with torch.no_grad():
|
| 207 |
+
while True:
|
| 208 |
+
state_vec = np.concatenate(list(frames), dtype=np.float32)
|
| 209 |
+
action_mask = build_action_mask(observation, exclude_look=True)
|
| 210 |
+
|
| 211 |
+
obs_t = torch.tensor(state_vec, dtype=torch.float32, device=device).unsqueeze(0)
|
| 212 |
+
mask_t = torch.tensor(action_mask, dtype=torch.float32, device=device).unsqueeze(0)
|
| 213 |
+
|
| 214 |
+
action_t, log_prob_t, value_t = network.act(obs_t, mask_t, deterministic=deterministic)
|
| 215 |
+
|
| 216 |
+
action_idx = int(action_t.item())
|
| 217 |
+
env_action = action_index_to_env_action(action_idx)
|
| 218 |
+
next_obs = env.step(env_action)
|
| 219 |
+
|
| 220 |
+
reward = float(next_obs.reward or 0.0)
|
| 221 |
+
chosen_action = env_action.action
|
| 222 |
+
|
| 223 |
+
# Shaping 1 — idle penalty
|
| 224 |
+
if chosen_action == "wait":
|
| 225 |
+
reward -= 0.05
|
| 226 |
+
|
| 227 |
+
# Shaping 2 — fire-approach penalty
|
| 228 |
+
ms_next = next_obs.map_state
|
| 229 |
+
if ms_next is not None and chosen_action.startswith("move"):
|
| 230 |
+
ax, ay = ms_next.agent_x, ms_next.agent_y
|
| 231 |
+
gw, gh = ms_next.grid_w, ms_next.grid_h
|
| 232 |
+
for dx, dy in ((0, 1), (0, -1), (1, 0), (-1, 0)):
|
| 233 |
+
nx, ny = ax + dx, ay + dy
|
| 234 |
+
if 0 <= nx < gw and 0 <= ny < gh:
|
| 235 |
+
if float(ms_next.fire_grid[ny * gw + nx]) > 0.15:
|
| 236 |
+
reward -= 0.15
|
| 237 |
+
break
|
| 238 |
+
|
| 239 |
+
# Shaping 3 — anti-loop penalty
|
| 240 |
+
if ms_next is not None and chosen_action.startswith("move"):
|
| 241 |
+
cur_pos = (ms_next.agent_x, ms_next.agent_y)
|
| 242 |
+
if cur_pos in recent_positions:
|
| 243 |
+
reward -= 0.2
|
| 244 |
+
recent_positions.append(cur_pos)
|
| 245 |
+
|
| 246 |
+
done = bool(next_obs.done)
|
| 247 |
+
|
| 248 |
+
buffer.obs.append(state_vec)
|
| 249 |
+
buffer.masks.append(action_mask)
|
| 250 |
+
buffer.actions.append(action_idx)
|
| 251 |
+
buffer.rewards.append(reward)
|
| 252 |
+
buffer.log_probs.append(float(log_prob_t.item()))
|
| 253 |
+
buffer.values.append(float(value_t.item()))
|
| 254 |
+
buffer.dones.append(done)
|
| 255 |
+
|
| 256 |
+
total_reward += reward
|
| 257 |
+
steps += 1
|
| 258 |
+
final_health = next_obs.agent_health
|
| 259 |
+
evacuated = next_obs.agent_evacuated
|
| 260 |
+
|
| 261 |
+
frames.append(encoder.encode(next_obs))
|
| 262 |
+
observation = next_obs
|
| 263 |
+
if done:
|
| 264 |
+
break
|
| 265 |
+
|
| 266 |
+
return EpisodeResult(
|
| 267 |
+
total_reward=total_reward,
|
| 268 |
+
steps=steps,
|
| 269 |
+
evacuated=evacuated,
|
| 270 |
+
final_health=final_health,
|
| 271 |
+
difficulty=difficulty,
|
| 272 |
+
)
|
| 273 |
+
|
| 274 |
+
|
| 275 |
+
# ---------------------------------------------------------------------------
|
| 276 |
+
# Training loop
|
| 277 |
+
# ---------------------------------------------------------------------------
|
| 278 |
+
|
| 279 |
+
def train(args: argparse.Namespace) -> None:
|
| 280 |
+
device = torch.device("cuda" if args.device == "cuda" and torch.cuda.is_available() else "cpu")
|
| 281 |
+
encoder = ObservationEncoder(mode=args.observation_mode)
|
| 282 |
+
input_dim = encoder.base_dim * args.history_length
|
| 283 |
+
hidden_sizes = [int(x) for x in args.hidden_sizes.split(",")]
|
| 284 |
+
action_dim = ACTION_DIM
|
| 285 |
+
|
| 286 |
+
# Connect to server
|
| 287 |
+
env = HttpPyreEnv(base_url=args.server)
|
| 288 |
+
print(f"[server] Connecting to {args.server} ...", end=" ", flush=True)
|
| 289 |
+
if not env.health_check():
|
| 290 |
+
print("FAILED\n[error] Server not reachable. Start it with: python server/app.py")
|
| 291 |
+
sys.exit(1)
|
| 292 |
+
print("OK")
|
| 293 |
+
|
| 294 |
+
# Network
|
| 295 |
+
network = ActorCritic(input_dim, action_dim, hidden_sizes).to(device)
|
| 296 |
+
optimizer = optim.Adam(network.parameters(), lr=args.lr)
|
| 297 |
+
|
| 298 |
+
total_params = sum(p.numel() for p in network.parameters())
|
| 299 |
+
print(f"\n[config] server={args.server}")
|
| 300 |
+
print(f"[config] device={device} episodes={args.episodes} batch={args.update_every} eps")
|
| 301 |
+
print(f"[config] curriculum: {args.difficulty_schedule}")
|
| 302 |
+
print(f"[config] PPO clip_eps={args.clip_eps} entropy={args.entropy_coef} lr={args.lr}")
|
| 303 |
+
print(f"\n[network] Parameters: {total_params:,}")
|
| 304 |
+
print(f"[network] Input dim: {input_dim:,} (encoder.base_dim={encoder.base_dim} x {args.history_length} frames)")
|
| 305 |
+
print(f"[network] Action dim: {action_dim} (4 move + 4 look + 1 wait + {MAX_DOORS} open + {MAX_DOORS} close)\n", flush=True)
|
| 306 |
+
|
| 307 |
+
schedule = args.difficulty_schedule.split(",")
|
| 308 |
+
buffer = RolloutBuffer()
|
| 309 |
+
metrics: list = []
|
| 310 |
+
eval_metrics: list = []
|
| 311 |
+
success_window: deque = deque(maxlen=30)
|
| 312 |
+
reward_window: deque = deque(maxlen=30)
|
| 313 |
+
t0 = time.time()
|
| 314 |
+
lr_scheduler = optim.lr_scheduler.LinearLR(
|
| 315 |
+
optimizer, start_factor=1.0, end_factor=0.1, total_iters=args.episodes
|
| 316 |
+
)
|
| 317 |
+
|
| 318 |
+
for ep in range(1, args.episodes + 1):
|
| 319 |
+
stage_idx = min(int((ep - 1) / args.episodes * len(schedule)), len(schedule) - 1)
|
| 320 |
+
difficulty = schedule[stage_idx]
|
| 321 |
+
|
| 322 |
+
result = run_episode(env, network, encoder, device, difficulty, args.history_length, buffer)
|
| 323 |
+
success_window.append(1 if result.evacuated else 0)
|
| 324 |
+
reward_window.append(result.total_reward)
|
| 325 |
+
suc30 = sum(success_window) / len(success_window)
|
| 326 |
+
r30 = sum(reward_window) / len(reward_window)
|
| 327 |
+
elapsed = int(time.time() - t0)
|
| 328 |
+
|
| 329 |
+
evac_sym = "1" if result.evacuated else "0"
|
| 330 |
+
print(
|
| 331 |
+
f"ep={ep:04d} [{difficulty:<6}] steps={result.steps:03d} "
|
| 332 |
+
f"reward={result.total_reward:+8.3f} evac={evac_sym} "
|
| 333 |
+
f"hp={result.final_health:5.1f} suc30={suc30:.2f} "
|
| 334 |
+
f"r30={r30:+7.2f} t={elapsed}s"
|
| 335 |
+
)
|
| 336 |
+
|
| 337 |
+
metrics.append({
|
| 338 |
+
"episode": ep, "difficulty": difficulty, "steps": result.steps,
|
| 339 |
+
"reward": round(result.total_reward, 4), "evacuated": int(result.evacuated),
|
| 340 |
+
"final_health": result.final_health, "suc30": round(suc30, 3), "r30": round(r30, 3),
|
| 341 |
+
})
|
| 342 |
+
|
| 343 |
+
# PPO update
|
| 344 |
+
if ep % args.update_every == 0 and len(buffer.obs) > 0:
|
| 345 |
+
network.train()
|
| 346 |
+
stats = ppo_update(
|
| 347 |
+
network=network, optimizer=optimizer, buffer=buffer, device=device,
|
| 348 |
+
clip_eps=args.clip_eps, value_clip_eps=args.clip_eps,
|
| 349 |
+
entropy_coef=args.entropy_coef, value_coef=args.value_coef,
|
| 350 |
+
n_epochs=args.update_epochs, minibatch_size=args.minibatch_size,
|
| 351 |
+
gamma=args.gamma, gae_lambda=args.gae_lambda,
|
| 352 |
+
max_grad_norm=args.max_grad_norm,
|
| 353 |
+
)
|
| 354 |
+
lr_scheduler.step()
|
| 355 |
+
cur_lr = optimizer.param_groups[0]["lr"]
|
| 356 |
+
print(
|
| 357 |
+
f" >> PPO update samples=flushed "
|
| 358 |
+
f"pi_loss={stats['policy_loss']:+.4f} v_loss={stats['value_loss']:.4f} "
|
| 359 |
+
f"entropy={stats['entropy']:.4f} kl={stats['approx_kl']:.4f} "
|
| 360 |
+
f"clip%={stats['clip_frac']:.2f} lr={cur_lr:.2e}"
|
| 361 |
+
)
|
| 362 |
+
buffer.clear()
|
| 363 |
+
network.eval()
|
| 364 |
+
|
| 365 |
+
# Evaluation
|
| 366 |
+
if ep % args.eval_every == 0:
|
| 367 |
+
eval_rewards, eval_success, eval_steps_list = [], [], []
|
| 368 |
+
eval_buf = RolloutBuffer()
|
| 369 |
+
for _ in range(args.eval_episodes):
|
| 370 |
+
er = run_episode(
|
| 371 |
+
env, network, encoder, device,
|
| 372 |
+
args.eval_difficulty, args.history_length,
|
| 373 |
+
eval_buf, deterministic=True,
|
| 374 |
+
)
|
| 375 |
+
eval_rewards.append(er.total_reward)
|
| 376 |
+
eval_success.append(1 if er.evacuated else 0)
|
| 377 |
+
eval_steps_list.append(er.steps)
|
| 378 |
+
avg_r = sum(eval_rewards) / len(eval_rewards)
|
| 379 |
+
avg_s = sum(eval_success) / len(eval_success)
|
| 380 |
+
avg_st = sum(eval_steps_list) / len(eval_steps_list)
|
| 381 |
+
print(f" ** EVAL [{args.eval_difficulty}] reward={avg_r:+.3f} success={avg_s:.2f} steps={avg_st:.1f}")
|
| 382 |
+
eval_metrics.append({
|
| 383 |
+
"episode": ep, "eval_difficulty": args.eval_difficulty,
|
| 384 |
+
"avg_reward": round(avg_r, 4), "success_rate": round(avg_s, 3),
|
| 385 |
+
"avg_steps": round(avg_st, 1),
|
| 386 |
+
})
|
| 387 |
+
|
| 388 |
+
# Checkpoint
|
| 389 |
+
if args.checkpoint and ep % args.checkpoint_every == 0:
|
| 390 |
+
torch.save(network.state_dict(), args.checkpoint)
|
| 391 |
+
print(f" [ckpt] saved -> {args.checkpoint}")
|
| 392 |
+
|
| 393 |
+
# --- Save artefacts ---
|
| 394 |
+
out = Path(args.output)
|
| 395 |
+
out.parent.mkdir(parents=True, exist_ok=True)
|
| 396 |
+
torch.save(network.state_dict(), out)
|
| 397 |
+
print(f"\n[done] Model saved -> {out}")
|
| 398 |
+
|
| 399 |
+
if args.save_metrics and metrics:
|
| 400 |
+
csv_path = out.with_suffix(".csv")
|
| 401 |
+
with open(csv_path, "w", newline="") as f:
|
| 402 |
+
writer = csv.DictWriter(f, fieldnames=metrics[0].keys())
|
| 403 |
+
writer.writeheader()
|
| 404 |
+
writer.writerows(metrics)
|
| 405 |
+
print(f"[done] Metrics CSV -> {csv_path}")
|
| 406 |
+
|
| 407 |
+
if eval_metrics:
|
| 408 |
+
eval_csv = out.with_stem(out.stem + "_eval").with_suffix(".csv")
|
| 409 |
+
with open(eval_csv, "w", newline="") as f:
|
| 410 |
+
writer = csv.DictWriter(f, fieldnames=eval_metrics[0].keys())
|
| 411 |
+
writer.writeheader()
|
| 412 |
+
writer.writerows(eval_metrics)
|
| 413 |
+
print(f"[done] Eval CSV -> {eval_csv}")
|
| 414 |
+
|
| 415 |
+
if args.save_graph:
|
| 416 |
+
try:
|
| 417 |
+
png_path = out.with_suffix(".png")
|
| 418 |
+
save_training_graph_png(metrics, eval_metrics, str(png_path))
|
| 419 |
+
print(f"[done] Graph PNG -> {png_path}")
|
| 420 |
+
except Exception as e:
|
| 421 |
+
print(f"[warn] Graph skipped: {e}")
|
| 422 |
+
|
| 423 |
+
suc_final = sum(success_window) / max(1, len(success_window))
|
| 424 |
+
r_final = sum(reward_window) / max(1, len(reward_window))
|
| 425 |
+
elapsed_total = time.time() - t0
|
| 426 |
+
print(f"\n[summary] {args.episodes} episodes in {elapsed_total:.1f}s ({args.episodes / elapsed_total:.1f} eps/s)")
|
| 427 |
+
print(f"[summary] Final success rate (last 30): {suc_final:.2f}")
|
| 428 |
+
print(f"[summary] Final reward mean (last 30): {r_final:+.3f}")
|
| 429 |
+
|
| 430 |
+
|
| 431 |
+
# ---------------------------------------------------------------------------
|
| 432 |
+
# CLI
|
| 433 |
+
# ---------------------------------------------------------------------------
|
| 434 |
+
|
| 435 |
+
def parse_args() -> argparse.Namespace:
|
| 436 |
+
p = argparse.ArgumentParser(
|
| 437 |
+
description="PPO trainer using the Pyre HTTP server (localhost:8000)"
|
| 438 |
+
)
|
| 439 |
+
|
| 440 |
+
# Server
|
| 441 |
+
p.add_argument("--server", type=str, default="http://localhost:8000",
|
| 442 |
+
help="Base URL of the running Pyre env server")
|
| 443 |
+
|
| 444 |
+
# Training
|
| 445 |
+
p.add_argument("--episodes", type=int, default=400)
|
| 446 |
+
p.add_argument("--device", type=str, default="cpu", choices=("cuda", "cpu"))
|
| 447 |
+
|
| 448 |
+
# Curriculum
|
| 449 |
+
p.add_argument("--difficulty-schedule", type=str, default="easy,easy,easy,medium,medium")
|
| 450 |
+
p.add_argument("--eval-difficulty", type=str, default="medium", choices=DIFFICULTIES)
|
| 451 |
+
p.add_argument("--eval-episodes", type=int, default=10)
|
| 452 |
+
p.add_argument("--eval-every", type=int, default=50)
|
| 453 |
+
|
| 454 |
+
# Observation
|
| 455 |
+
p.add_argument("--observation-mode", type=str, default="visible", choices=("visible", "full"))
|
| 456 |
+
p.add_argument("--history-length", type=int, default=4)
|
| 457 |
+
|
| 458 |
+
# Network
|
| 459 |
+
p.add_argument("--hidden-sizes", type=str, default="256,128,64")
|
| 460 |
+
|
| 461 |
+
# PPO
|
| 462 |
+
p.add_argument("--lr", type=float, default=3e-4)
|
| 463 |
+
p.add_argument("--gamma", type=float, default=0.99)
|
| 464 |
+
p.add_argument("--gae-lambda", type=float, default=0.95)
|
| 465 |
+
p.add_argument("--clip-eps", type=float, default=0.2)
|
| 466 |
+
p.add_argument("--value-coef", type=float, default=0.5)
|
| 467 |
+
p.add_argument("--entropy-coef", type=float, default=0.03)
|
| 468 |
+
p.add_argument("--update-every", type=int, default=5)
|
| 469 |
+
p.add_argument("--update-epochs", type=int, default=4)
|
| 470 |
+
p.add_argument("--minibatch-size", type=int, default=256)
|
| 471 |
+
p.add_argument("--max-grad-norm", type=float, default=0.5)
|
| 472 |
+
|
| 473 |
+
# Output
|
| 474 |
+
p.add_argument("--output", type=str, default="artifacts/pyre_ppo_http.pt")
|
| 475 |
+
p.add_argument("--checkpoint", type=str, default="artifacts/pyre_ppo_http_ckpt.pt")
|
| 476 |
+
p.add_argument("--checkpoint-every", type=int, default=50)
|
| 477 |
+
p.add_argument("--save-metrics", action="store_true", default=True)
|
| 478 |
+
p.add_argument("--save-graph", action="store_true", default=True)
|
| 479 |
+
p.add_argument("--seed", type=int, default=42)
|
| 480 |
+
|
| 481 |
+
return p.parse_args()
|
| 482 |
+
|
| 483 |
+
|
| 484 |
+
def main() -> None:
|
| 485 |
+
args = parse_args()
|
| 486 |
+
torch.manual_seed(args.seed)
|
| 487 |
+
np.random.seed(args.seed)
|
| 488 |
+
train(args)
|
| 489 |
+
|
| 490 |
+
|
| 491 |
+
if __name__ == "__main__":
|
| 492 |
+
main()
|
frontend/README.md
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Pyre — Frontend Visualization
|
| 2 |
+
|
| 3 |
+
A cinematic real-time visualization for the **Pyre Crisis Navigation Environment** — a reinforcement learning environment where an LLM agent navigates a burning building.
|
| 4 |
+
|
| 5 |
+
## Quick start
|
| 6 |
+
|
| 7 |
+
```bash
|
| 8 |
+
# Open directly in a browser — no build step needed
|
| 9 |
+
open frontend/index.html
|
| 10 |
+
```
|
| 11 |
+
|
| 12 |
+
The app runs entirely in-browser. **Demo mode** simulates the fire physics in JavaScript (no server required). **Live mode** connects to the deployed environment.
|
| 13 |
+
|
| 14 |
+
---
|
| 15 |
+
|
| 16 |
+
## Demo mode vs Live mode
|
| 17 |
+
|
| 18 |
+
| | Demo | Live |
|
| 19 |
+
|---|---|---|
|
| 20 |
+
| Server needed | ✗ | ✓ |
|
| 21 |
+
| Fire physics | JS port (exact match) | Python server |
|
| 22 |
+
| Full reward rubric | Simplified | Complete |
|
| 23 |
+
| Toggle | Default | Click "Live" in topbar |
|
| 24 |
+
|
| 25 |
+
**Live server:** `https://krooz-pyre-env.hf.space`
|
| 26 |
+
|
| 27 |
+
---
|
| 28 |
+
|
| 29 |
+
## Controls
|
| 30 |
+
|
| 31 |
+
| Key | Action |
|
| 32 |
+
|---|---|
|
| 33 |
+
| `Space` | Play / pause |
|
| 34 |
+
| `→` | Single step |
|
| 35 |
+
| `R` | New episode |
|
| 36 |
+
| `1`–`5` | Speed ½× / 1× / 2× / 4× / 8× |
|
| 37 |
+
|
| 38 |
+
Bottom bar: difficulty selector, seed input, speed control, reset.
|
| 39 |
+
|
| 40 |
+
---
|
| 41 |
+
|
| 42 |
+
## Recording episodes (Python)
|
| 43 |
+
|
| 44 |
+
```bash
|
| 45 |
+
pip install requests # only stdlib used, no install needed
|
| 46 |
+
|
| 47 |
+
python bridge/recorder.py \
|
| 48 |
+
--url https://krooz-pyre-env.hf.space/web \
|
| 49 |
+
--episodes 10 \
|
| 50 |
+
--difficulty medium \
|
| 51 |
+
--out episodes/
|
| 52 |
+
```
|
| 53 |
+
|
| 54 |
+
Episodes are saved as JSON files to `episodes/`. Each file contains full frame-by-frame grid data (cell, fire, smoke grids + agent position + visible cells).
|
| 55 |
+
|
| 56 |
+
---
|
| 57 |
+
|
| 58 |
+
## File structure
|
| 59 |
+
|
| 60 |
+
```
|
| 61 |
+
frontend/
|
| 62 |
+
├── index.html Main app — open this
|
| 63 |
+
└── js/
|
| 64 |
+
├── sim.js JS port of pyre_env fire simulation + floor plans
|
| 65 |
+
├── renderer.js Canvas2D rendering (fire particles, fog-of-war, agent trail)
|
| 66 |
+
└── app.js App controller, charts, HUD, live/demo modes
|
| 67 |
+
|
| 68 |
+
bridge/
|
| 69 |
+
└── recorder.py Record live episodes to JSON for replay
|
| 70 |
+
```
|
| 71 |
+
|
| 72 |
+
---
|
| 73 |
+
|
| 74 |
+
## Architecture notes
|
| 75 |
+
|
| 76 |
+
**Rendering:** HTML5 Canvas 2D — sufficient at 60fps for 16×16 grids; additive blending (`globalCompositeOperation: lighter`) for fire glow; ember particle pool (200 max); fog-of-war via per-cell alpha overlay.
|
| 77 |
+
|
| 78 |
+
**Demo agent:** BFS toward nearest unblocked exit, 15% random exploration, avoids fire cells > 0.4 intensity.
|
| 79 |
+
|
| 80 |
+
**Live bridge:** Polls `/web/scene` every 800ms; applies grid state to the same rendering pipeline.
|
| 81 |
+
|
| 82 |
+
---
|
| 83 |
+
|
| 84 |
+
## Demo script (30-second stage walkthrough)
|
| 85 |
+
|
| 86 |
+
1. **Open** `frontend/index.html` — fire simulation starts automatically at 1×
|
| 87 |
+
2. **Point out** the dark floor plan canvas with glowing fire cells, fog-of-war, and cyan agent dot
|
| 88 |
+
3. **Slow to ½×** to show per-step fire propagation and smoke spread
|
| 89 |
+
4. **Speed to 4×** — show agent navigating toward exits (green glow), closing doors (blue bars) to slow fire
|
| 90 |
+
5. **Highlight** the side panel: cumulative reward curve dipping on smoke exposure, fire cell count climbing, action histogram
|
| 91 |
+
6. **Describe partial observability** — the dark unexplored cells vs. visible corridor
|
| 92 |
+
7. **Reset (R)** with a different seed to show episode variety
|
| 93 |
+
8. If server is available: click **Live** — "Connected" chip turns green, real Python environment takes over
|
frontend/eslint.config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import js from '@eslint/js'
|
| 2 |
+
import globals from 'globals'
|
| 3 |
+
import reactHooks from 'eslint-plugin-react-hooks'
|
| 4 |
+
import reactRefresh from 'eslint-plugin-react-refresh'
|
| 5 |
+
import tseslint from 'typescript-eslint'
|
| 6 |
+
import { defineConfig, globalIgnores } from 'eslint/config'
|
| 7 |
+
|
| 8 |
+
export default defineConfig([
|
| 9 |
+
globalIgnores(['dist']),
|
| 10 |
+
{
|
| 11 |
+
files: ['**/*.{ts,tsx}'],
|
| 12 |
+
extends: [
|
| 13 |
+
js.configs.recommended,
|
| 14 |
+
tseslint.configs.recommended,
|
| 15 |
+
reactHooks.configs.flat.recommended,
|
| 16 |
+
reactRefresh.configs.vite,
|
| 17 |
+
],
|
| 18 |
+
languageOptions: {
|
| 19 |
+
globals: globals.browser,
|
| 20 |
+
},
|
| 21 |
+
},
|
| 22 |
+
])
|
frontend/index.html
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Pyre — Crisis Navigation</title>
|
| 8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
| 9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
| 10 |
+
<link href="https://fonts.googleapis.com/css2?family=DM+Mono:wght@400;500&family=DM+Sans:wght@400;500;600&display=swap" rel="stylesheet" />
|
| 11 |
+
</head>
|
| 12 |
+
<body>
|
| 13 |
+
<div id="root"></div>
|
| 14 |
+
<script type="module" src="/src/main.tsx"></script>
|
| 15 |
+
</body>
|
| 16 |
+
</html>
|
frontend/package-lock.json
ADDED
|
@@ -0,0 +1,2772 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "frontend",
|
| 3 |
+
"version": "0.0.0",
|
| 4 |
+
"lockfileVersion": 3,
|
| 5 |
+
"requires": true,
|
| 6 |
+
"packages": {
|
| 7 |
+
"": {
|
| 8 |
+
"name": "frontend",
|
| 9 |
+
"version": "0.0.0",
|
| 10 |
+
"dependencies": {
|
| 11 |
+
"react": "^19.2.5",
|
| 12 |
+
"react-dom": "^19.2.5"
|
| 13 |
+
},
|
| 14 |
+
"devDependencies": {
|
| 15 |
+
"@eslint/js": "^10.0.1",
|
| 16 |
+
"@types/node": "^24.12.2",
|
| 17 |
+
"@types/react": "^19.2.14",
|
| 18 |
+
"@types/react-dom": "^19.2.3",
|
| 19 |
+
"@vitejs/plugin-react": "^6.0.1",
|
| 20 |
+
"eslint": "^10.2.1",
|
| 21 |
+
"eslint-plugin-react-hooks": "^7.1.1",
|
| 22 |
+
"eslint-plugin-react-refresh": "^0.5.2",
|
| 23 |
+
"globals": "^17.5.0",
|
| 24 |
+
"typescript": "~6.0.2",
|
| 25 |
+
"typescript-eslint": "^8.58.2",
|
| 26 |
+
"vite": "^8.0.10"
|
| 27 |
+
}
|
| 28 |
+
},
|
| 29 |
+
"node_modules/@babel/code-frame": {
|
| 30 |
+
"version": "7.29.0",
|
| 31 |
+
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
|
| 32 |
+
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
|
| 33 |
+
"dev": true,
|
| 34 |
+
"license": "MIT",
|
| 35 |
+
"dependencies": {
|
| 36 |
+
"@babel/helper-validator-identifier": "^7.28.5",
|
| 37 |
+
"js-tokens": "^4.0.0",
|
| 38 |
+
"picocolors": "^1.1.1"
|
| 39 |
+
},
|
| 40 |
+
"engines": {
|
| 41 |
+
"node": ">=6.9.0"
|
| 42 |
+
}
|
| 43 |
+
},
|
| 44 |
+
"node_modules/@babel/compat-data": {
|
| 45 |
+
"version": "7.29.0",
|
| 46 |
+
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
|
| 47 |
+
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
|
| 48 |
+
"dev": true,
|
| 49 |
+
"license": "MIT",
|
| 50 |
+
"engines": {
|
| 51 |
+
"node": ">=6.9.0"
|
| 52 |
+
}
|
| 53 |
+
},
|
| 54 |
+
"node_modules/@babel/core": {
|
| 55 |
+
"version": "7.29.0",
|
| 56 |
+
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
|
| 57 |
+
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
| 58 |
+
"dev": true,
|
| 59 |
+
"license": "MIT",
|
| 60 |
+
"dependencies": {
|
| 61 |
+
"@babel/code-frame": "^7.29.0",
|
| 62 |
+
"@babel/generator": "^7.29.0",
|
| 63 |
+
"@babel/helper-compilation-targets": "^7.28.6",
|
| 64 |
+
"@babel/helper-module-transforms": "^7.28.6",
|
| 65 |
+
"@babel/helpers": "^7.28.6",
|
| 66 |
+
"@babel/parser": "^7.29.0",
|
| 67 |
+
"@babel/template": "^7.28.6",
|
| 68 |
+
"@babel/traverse": "^7.29.0",
|
| 69 |
+
"@babel/types": "^7.29.0",
|
| 70 |
+
"@jridgewell/remapping": "^2.3.5",
|
| 71 |
+
"convert-source-map": "^2.0.0",
|
| 72 |
+
"debug": "^4.1.0",
|
| 73 |
+
"gensync": "^1.0.0-beta.2",
|
| 74 |
+
"json5": "^2.2.3",
|
| 75 |
+
"semver": "^6.3.1"
|
| 76 |
+
},
|
| 77 |
+
"engines": {
|
| 78 |
+
"node": ">=6.9.0"
|
| 79 |
+
},
|
| 80 |
+
"funding": {
|
| 81 |
+
"type": "opencollective",
|
| 82 |
+
"url": "https://opencollective.com/babel"
|
| 83 |
+
}
|
| 84 |
+
},
|
| 85 |
+
"node_modules/@babel/generator": {
|
| 86 |
+
"version": "7.29.1",
|
| 87 |
+
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
|
| 88 |
+
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
|
| 89 |
+
"dev": true,
|
| 90 |
+
"license": "MIT",
|
| 91 |
+
"dependencies": {
|
| 92 |
+
"@babel/parser": "^7.29.0",
|
| 93 |
+
"@babel/types": "^7.29.0",
|
| 94 |
+
"@jridgewell/gen-mapping": "^0.3.12",
|
| 95 |
+
"@jridgewell/trace-mapping": "^0.3.28",
|
| 96 |
+
"jsesc": "^3.0.2"
|
| 97 |
+
},
|
| 98 |
+
"engines": {
|
| 99 |
+
"node": ">=6.9.0"
|
| 100 |
+
}
|
| 101 |
+
},
|
| 102 |
+
"node_modules/@babel/helper-compilation-targets": {
|
| 103 |
+
"version": "7.28.6",
|
| 104 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
|
| 105 |
+
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
|
| 106 |
+
"dev": true,
|
| 107 |
+
"license": "MIT",
|
| 108 |
+
"dependencies": {
|
| 109 |
+
"@babel/compat-data": "^7.28.6",
|
| 110 |
+
"@babel/helper-validator-option": "^7.27.1",
|
| 111 |
+
"browserslist": "^4.24.0",
|
| 112 |
+
"lru-cache": "^5.1.1",
|
| 113 |
+
"semver": "^6.3.1"
|
| 114 |
+
},
|
| 115 |
+
"engines": {
|
| 116 |
+
"node": ">=6.9.0"
|
| 117 |
+
}
|
| 118 |
+
},
|
| 119 |
+
"node_modules/@babel/helper-globals": {
|
| 120 |
+
"version": "7.28.0",
|
| 121 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
|
| 122 |
+
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
|
| 123 |
+
"dev": true,
|
| 124 |
+
"license": "MIT",
|
| 125 |
+
"engines": {
|
| 126 |
+
"node": ">=6.9.0"
|
| 127 |
+
}
|
| 128 |
+
},
|
| 129 |
+
"node_modules/@babel/helper-module-imports": {
|
| 130 |
+
"version": "7.28.6",
|
| 131 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
|
| 132 |
+
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
|
| 133 |
+
"dev": true,
|
| 134 |
+
"license": "MIT",
|
| 135 |
+
"dependencies": {
|
| 136 |
+
"@babel/traverse": "^7.28.6",
|
| 137 |
+
"@babel/types": "^7.28.6"
|
| 138 |
+
},
|
| 139 |
+
"engines": {
|
| 140 |
+
"node": ">=6.9.0"
|
| 141 |
+
}
|
| 142 |
+
},
|
| 143 |
+
"node_modules/@babel/helper-module-transforms": {
|
| 144 |
+
"version": "7.28.6",
|
| 145 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
|
| 146 |
+
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
|
| 147 |
+
"dev": true,
|
| 148 |
+
"license": "MIT",
|
| 149 |
+
"dependencies": {
|
| 150 |
+
"@babel/helper-module-imports": "^7.28.6",
|
| 151 |
+
"@babel/helper-validator-identifier": "^7.28.5",
|
| 152 |
+
"@babel/traverse": "^7.28.6"
|
| 153 |
+
},
|
| 154 |
+
"engines": {
|
| 155 |
+
"node": ">=6.9.0"
|
| 156 |
+
},
|
| 157 |
+
"peerDependencies": {
|
| 158 |
+
"@babel/core": "^7.0.0"
|
| 159 |
+
}
|
| 160 |
+
},
|
| 161 |
+
"node_modules/@babel/helper-string-parser": {
|
| 162 |
+
"version": "7.27.1",
|
| 163 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
|
| 164 |
+
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
|
| 165 |
+
"dev": true,
|
| 166 |
+
"license": "MIT",
|
| 167 |
+
"engines": {
|
| 168 |
+
"node": ">=6.9.0"
|
| 169 |
+
}
|
| 170 |
+
},
|
| 171 |
+
"node_modules/@babel/helper-validator-identifier": {
|
| 172 |
+
"version": "7.28.5",
|
| 173 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
|
| 174 |
+
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
|
| 175 |
+
"dev": true,
|
| 176 |
+
"license": "MIT",
|
| 177 |
+
"engines": {
|
| 178 |
+
"node": ">=6.9.0"
|
| 179 |
+
}
|
| 180 |
+
},
|
| 181 |
+
"node_modules/@babel/helper-validator-option": {
|
| 182 |
+
"version": "7.27.1",
|
| 183 |
+
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
|
| 184 |
+
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
|
| 185 |
+
"dev": true,
|
| 186 |
+
"license": "MIT",
|
| 187 |
+
"engines": {
|
| 188 |
+
"node": ">=6.9.0"
|
| 189 |
+
}
|
| 190 |
+
},
|
| 191 |
+
"node_modules/@babel/helpers": {
|
| 192 |
+
"version": "7.29.2",
|
| 193 |
+
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
|
| 194 |
+
"integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
|
| 195 |
+
"dev": true,
|
| 196 |
+
"license": "MIT",
|
| 197 |
+
"dependencies": {
|
| 198 |
+
"@babel/template": "^7.28.6",
|
| 199 |
+
"@babel/types": "^7.29.0"
|
| 200 |
+
},
|
| 201 |
+
"engines": {
|
| 202 |
+
"node": ">=6.9.0"
|
| 203 |
+
}
|
| 204 |
+
},
|
| 205 |
+
"node_modules/@babel/parser": {
|
| 206 |
+
"version": "7.29.2",
|
| 207 |
+
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz",
|
| 208 |
+
"integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
|
| 209 |
+
"dev": true,
|
| 210 |
+
"license": "MIT",
|
| 211 |
+
"dependencies": {
|
| 212 |
+
"@babel/types": "^7.29.0"
|
| 213 |
+
},
|
| 214 |
+
"bin": {
|
| 215 |
+
"parser": "bin/babel-parser.js"
|
| 216 |
+
},
|
| 217 |
+
"engines": {
|
| 218 |
+
"node": ">=6.0.0"
|
| 219 |
+
}
|
| 220 |
+
},
|
| 221 |
+
"node_modules/@babel/template": {
|
| 222 |
+
"version": "7.28.6",
|
| 223 |
+
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
|
| 224 |
+
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
|
| 225 |
+
"dev": true,
|
| 226 |
+
"license": "MIT",
|
| 227 |
+
"dependencies": {
|
| 228 |
+
"@babel/code-frame": "^7.28.6",
|
| 229 |
+
"@babel/parser": "^7.28.6",
|
| 230 |
+
"@babel/types": "^7.28.6"
|
| 231 |
+
},
|
| 232 |
+
"engines": {
|
| 233 |
+
"node": ">=6.9.0"
|
| 234 |
+
}
|
| 235 |
+
},
|
| 236 |
+
"node_modules/@babel/traverse": {
|
| 237 |
+
"version": "7.29.0",
|
| 238 |
+
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
|
| 239 |
+
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
|
| 240 |
+
"dev": true,
|
| 241 |
+
"license": "MIT",
|
| 242 |
+
"dependencies": {
|
| 243 |
+
"@babel/code-frame": "^7.29.0",
|
| 244 |
+
"@babel/generator": "^7.29.0",
|
| 245 |
+
"@babel/helper-globals": "^7.28.0",
|
| 246 |
+
"@babel/parser": "^7.29.0",
|
| 247 |
+
"@babel/template": "^7.28.6",
|
| 248 |
+
"@babel/types": "^7.29.0",
|
| 249 |
+
"debug": "^4.3.1"
|
| 250 |
+
},
|
| 251 |
+
"engines": {
|
| 252 |
+
"node": ">=6.9.0"
|
| 253 |
+
}
|
| 254 |
+
},
|
| 255 |
+
"node_modules/@babel/types": {
|
| 256 |
+
"version": "7.29.0",
|
| 257 |
+
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
|
| 258 |
+
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
|
| 259 |
+
"dev": true,
|
| 260 |
+
"license": "MIT",
|
| 261 |
+
"dependencies": {
|
| 262 |
+
"@babel/helper-string-parser": "^7.27.1",
|
| 263 |
+
"@babel/helper-validator-identifier": "^7.28.5"
|
| 264 |
+
},
|
| 265 |
+
"engines": {
|
| 266 |
+
"node": ">=6.9.0"
|
| 267 |
+
}
|
| 268 |
+
},
|
| 269 |
+
"node_modules/@emnapi/core": {
|
| 270 |
+
"version": "1.10.0",
|
| 271 |
+
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz",
|
| 272 |
+
"integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==",
|
| 273 |
+
"dev": true,
|
| 274 |
+
"license": "MIT",
|
| 275 |
+
"optional": true,
|
| 276 |
+
"dependencies": {
|
| 277 |
+
"@emnapi/wasi-threads": "1.2.1",
|
| 278 |
+
"tslib": "^2.4.0"
|
| 279 |
+
}
|
| 280 |
+
},
|
| 281 |
+
"node_modules/@emnapi/runtime": {
|
| 282 |
+
"version": "1.10.0",
|
| 283 |
+
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz",
|
| 284 |
+
"integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==",
|
| 285 |
+
"dev": true,
|
| 286 |
+
"license": "MIT",
|
| 287 |
+
"optional": true,
|
| 288 |
+
"dependencies": {
|
| 289 |
+
"tslib": "^2.4.0"
|
| 290 |
+
}
|
| 291 |
+
},
|
| 292 |
+
"node_modules/@emnapi/wasi-threads": {
|
| 293 |
+
"version": "1.2.1",
|
| 294 |
+
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz",
|
| 295 |
+
"integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==",
|
| 296 |
+
"dev": true,
|
| 297 |
+
"license": "MIT",
|
| 298 |
+
"optional": true,
|
| 299 |
+
"dependencies": {
|
| 300 |
+
"tslib": "^2.4.0"
|
| 301 |
+
}
|
| 302 |
+
},
|
| 303 |
+
"node_modules/@eslint-community/eslint-utils": {
|
| 304 |
+
"version": "4.9.1",
|
| 305 |
+
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
| 306 |
+
"integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
|
| 307 |
+
"dev": true,
|
| 308 |
+
"license": "MIT",
|
| 309 |
+
"dependencies": {
|
| 310 |
+
"eslint-visitor-keys": "^3.4.3"
|
| 311 |
+
},
|
| 312 |
+
"engines": {
|
| 313 |
+
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
| 314 |
+
},
|
| 315 |
+
"funding": {
|
| 316 |
+
"url": "https://opencollective.com/eslint"
|
| 317 |
+
},
|
| 318 |
+
"peerDependencies": {
|
| 319 |
+
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
|
| 320 |
+
}
|
| 321 |
+
},
|
| 322 |
+
"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
|
| 323 |
+
"version": "3.4.3",
|
| 324 |
+
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
|
| 325 |
+
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
|
| 326 |
+
"dev": true,
|
| 327 |
+
"license": "Apache-2.0",
|
| 328 |
+
"engines": {
|
| 329 |
+
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
| 330 |
+
},
|
| 331 |
+
"funding": {
|
| 332 |
+
"url": "https://opencollective.com/eslint"
|
| 333 |
+
}
|
| 334 |
+
},
|
| 335 |
+
"node_modules/@eslint-community/regexpp": {
|
| 336 |
+
"version": "4.12.2",
|
| 337 |
+
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
|
| 338 |
+
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
|
| 339 |
+
"dev": true,
|
| 340 |
+
"license": "MIT",
|
| 341 |
+
"engines": {
|
| 342 |
+
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
|
| 343 |
+
}
|
| 344 |
+
},
|
| 345 |
+
"node_modules/@eslint/config-array": {
|
| 346 |
+
"version": "0.23.5",
|
| 347 |
+
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz",
|
| 348 |
+
"integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==",
|
| 349 |
+
"dev": true,
|
| 350 |
+
"license": "Apache-2.0",
|
| 351 |
+
"dependencies": {
|
| 352 |
+
"@eslint/object-schema": "^3.0.5",
|
| 353 |
+
"debug": "^4.3.1",
|
| 354 |
+
"minimatch": "^10.2.4"
|
| 355 |
+
},
|
| 356 |
+
"engines": {
|
| 357 |
+
"node": "^20.19.0 || ^22.13.0 || >=24"
|
| 358 |
+
}
|
| 359 |
+
},
|
| 360 |
+
"node_modules/@eslint/config-helpers": {
|
| 361 |
+
"version": "0.5.5",
|
| 362 |
+
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz",
|
| 363 |
+
"integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==",
|
| 364 |
+
"dev": true,
|
| 365 |
+
"license": "Apache-2.0",
|
| 366 |
+
"dependencies": {
|
| 367 |
+
"@eslint/core": "^1.2.1"
|
| 368 |
+
},
|
| 369 |
+
"engines": {
|
| 370 |
+
"node": "^20.19.0 || ^22.13.0 || >=24"
|
| 371 |
+
}
|
| 372 |
+
},
|
| 373 |
+
"node_modules/@eslint/core": {
|
| 374 |
+
"version": "1.2.1",
|
| 375 |
+
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz",
|
| 376 |
+
"integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==",
|
| 377 |
+
"dev": true,
|
| 378 |
+
"license": "Apache-2.0",
|
| 379 |
+
"dependencies": {
|
| 380 |
+
"@types/json-schema": "^7.0.15"
|
| 381 |
+
},
|
| 382 |
+
"engines": {
|
| 383 |
+
"node": "^20.19.0 || ^22.13.0 || >=24"
|
| 384 |
+
}
|
| 385 |
+
},
|
| 386 |
+
"node_modules/@eslint/js": {
|
| 387 |
+
"version": "10.0.1",
|
| 388 |
+
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz",
|
| 389 |
+
"integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==",
|
| 390 |
+
"dev": true,
|
| 391 |
+
"license": "MIT",
|
| 392 |
+
"engines": {
|
| 393 |
+
"node": "^20.19.0 || ^22.13.0 || >=24"
|
| 394 |
+
},
|
| 395 |
+
"funding": {
|
| 396 |
+
"url": "https://eslint.org/donate"
|
| 397 |
+
},
|
| 398 |
+
"peerDependencies": {
|
| 399 |
+
"eslint": "^10.0.0"
|
| 400 |
+
},
|
| 401 |
+
"peerDependenciesMeta": {
|
| 402 |
+
"eslint": {
|
| 403 |
+
"optional": true
|
| 404 |
+
}
|
| 405 |
+
}
|
| 406 |
+
},
|
| 407 |
+
"node_modules/@eslint/object-schema": {
|
| 408 |
+
"version": "3.0.5",
|
| 409 |
+
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz",
|
| 410 |
+
"integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==",
|
| 411 |
+
"dev": true,
|
| 412 |
+
"license": "Apache-2.0",
|
| 413 |
+
"engines": {
|
| 414 |
+
"node": "^20.19.0 || ^22.13.0 || >=24"
|
| 415 |
+
}
|
| 416 |
+
},
|
| 417 |
+
"node_modules/@eslint/plugin-kit": {
|
| 418 |
+
"version": "0.7.1",
|
| 419 |
+
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz",
|
| 420 |
+
"integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==",
|
| 421 |
+
"dev": true,
|
| 422 |
+
"license": "Apache-2.0",
|
| 423 |
+
"dependencies": {
|
| 424 |
+
"@eslint/core": "^1.2.1",
|
| 425 |
+
"levn": "^0.4.1"
|
| 426 |
+
},
|
| 427 |
+
"engines": {
|
| 428 |
+
"node": "^20.19.0 || ^22.13.0 || >=24"
|
| 429 |
+
}
|
| 430 |
+
},
|
| 431 |
+
"node_modules/@humanfs/core": {
|
| 432 |
+
"version": "0.19.2",
|
| 433 |
+
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
| 434 |
+
"integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==",
|
| 435 |
+
"dev": true,
|
| 436 |
+
"license": "Apache-2.0",
|
| 437 |
+
"dependencies": {
|
| 438 |
+
"@humanfs/types": "^0.15.0"
|
| 439 |
+
},
|
| 440 |
+
"engines": {
|
| 441 |
+
"node": ">=18.18.0"
|
| 442 |
+
}
|
| 443 |
+
},
|
| 444 |
+
"node_modules/@humanfs/node": {
|
| 445 |
+
"version": "0.16.8",
|
| 446 |
+
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz",
|
| 447 |
+
"integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==",
|
| 448 |
+
"dev": true,
|
| 449 |
+
"license": "Apache-2.0",
|
| 450 |
+
"dependencies": {
|
| 451 |
+
"@humanfs/core": "^0.19.2",
|
| 452 |
+
"@humanfs/types": "^0.15.0",
|
| 453 |
+
"@humanwhocodes/retry": "^0.4.0"
|
| 454 |
+
},
|
| 455 |
+
"engines": {
|
| 456 |
+
"node": ">=18.18.0"
|
| 457 |
+
}
|
| 458 |
+
},
|
| 459 |
+
"node_modules/@humanfs/types": {
|
| 460 |
+
"version": "0.15.0",
|
| 461 |
+
"resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz",
|
| 462 |
+
"integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==",
|
| 463 |
+
"dev": true,
|
| 464 |
+
"license": "Apache-2.0",
|
| 465 |
+
"engines": {
|
| 466 |
+
"node": ">=18.18.0"
|
| 467 |
+
}
|
| 468 |
+
},
|
| 469 |
+
"node_modules/@humanwhocodes/module-importer": {
|
| 470 |
+
"version": "1.0.1",
|
| 471 |
+
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
|
| 472 |
+
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
|
| 473 |
+
"dev": true,
|
| 474 |
+
"license": "Apache-2.0",
|
| 475 |
+
"engines": {
|
| 476 |
+
"node": ">=12.22"
|
| 477 |
+
},
|
| 478 |
+
"funding": {
|
| 479 |
+
"type": "github",
|
| 480 |
+
"url": "https://github.com/sponsors/nzakas"
|
| 481 |
+
}
|
| 482 |
+
},
|
| 483 |
+
"node_modules/@humanwhocodes/retry": {
|
| 484 |
+
"version": "0.4.3",
|
| 485 |
+
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
|
| 486 |
+
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
|
| 487 |
+
"dev": true,
|
| 488 |
+
"license": "Apache-2.0",
|
| 489 |
+
"engines": {
|
| 490 |
+
"node": ">=18.18"
|
| 491 |
+
},
|
| 492 |
+
"funding": {
|
| 493 |
+
"type": "github",
|
| 494 |
+
"url": "https://github.com/sponsors/nzakas"
|
| 495 |
+
}
|
| 496 |
+
},
|
| 497 |
+
"node_modules/@jridgewell/gen-mapping": {
|
| 498 |
+
"version": "0.3.13",
|
| 499 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
| 500 |
+
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
| 501 |
+
"dev": true,
|
| 502 |
+
"license": "MIT",
|
| 503 |
+
"dependencies": {
|
| 504 |
+
"@jridgewell/sourcemap-codec": "^1.5.0",
|
| 505 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 506 |
+
}
|
| 507 |
+
},
|
| 508 |
+
"node_modules/@jridgewell/remapping": {
|
| 509 |
+
"version": "2.3.5",
|
| 510 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
|
| 511 |
+
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
|
| 512 |
+
"dev": true,
|
| 513 |
+
"license": "MIT",
|
| 514 |
+
"dependencies": {
|
| 515 |
+
"@jridgewell/gen-mapping": "^0.3.5",
|
| 516 |
+
"@jridgewell/trace-mapping": "^0.3.24"
|
| 517 |
+
}
|
| 518 |
+
},
|
| 519 |
+
"node_modules/@jridgewell/resolve-uri": {
|
| 520 |
+
"version": "3.1.2",
|
| 521 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
| 522 |
+
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
| 523 |
+
"dev": true,
|
| 524 |
+
"license": "MIT",
|
| 525 |
+
"engines": {
|
| 526 |
+
"node": ">=6.0.0"
|
| 527 |
+
}
|
| 528 |
+
},
|
| 529 |
+
"node_modules/@jridgewell/sourcemap-codec": {
|
| 530 |
+
"version": "1.5.5",
|
| 531 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
| 532 |
+
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
| 533 |
+
"dev": true,
|
| 534 |
+
"license": "MIT"
|
| 535 |
+
},
|
| 536 |
+
"node_modules/@jridgewell/trace-mapping": {
|
| 537 |
+
"version": "0.3.31",
|
| 538 |
+
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
| 539 |
+
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
| 540 |
+
"dev": true,
|
| 541 |
+
"license": "MIT",
|
| 542 |
+
"dependencies": {
|
| 543 |
+
"@jridgewell/resolve-uri": "^3.1.0",
|
| 544 |
+
"@jridgewell/sourcemap-codec": "^1.4.14"
|
| 545 |
+
}
|
| 546 |
+
},
|
| 547 |
+
"node_modules/@napi-rs/wasm-runtime": {
|
| 548 |
+
"version": "1.1.4",
|
| 549 |
+
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz",
|
| 550 |
+
"integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==",
|
| 551 |
+
"dev": true,
|
| 552 |
+
"license": "MIT",
|
| 553 |
+
"optional": true,
|
| 554 |
+
"dependencies": {
|
| 555 |
+
"@tybys/wasm-util": "^0.10.1"
|
| 556 |
+
},
|
| 557 |
+
"funding": {
|
| 558 |
+
"type": "github",
|
| 559 |
+
"url": "https://github.com/sponsors/Brooooooklyn"
|
| 560 |
+
},
|
| 561 |
+
"peerDependencies": {
|
| 562 |
+
"@emnapi/core": "^1.7.1",
|
| 563 |
+
"@emnapi/runtime": "^1.7.1"
|
| 564 |
+
}
|
| 565 |
+
},
|
| 566 |
+
"node_modules/@oxc-project/types": {
|
| 567 |
+
"version": "0.127.0",
|
| 568 |
+
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz",
|
| 569 |
+
"integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==",
|
| 570 |
+
"dev": true,
|
| 571 |
+
"license": "MIT",
|
| 572 |
+
"funding": {
|
| 573 |
+
"url": "https://github.com/sponsors/Boshen"
|
| 574 |
+
}
|
| 575 |
+
},
|
| 576 |
+
"node_modules/@rolldown/binding-android-arm64": {
|
| 577 |
+
"version": "1.0.0-rc.17",
|
| 578 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz",
|
| 579 |
+
"integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==",
|
| 580 |
+
"cpu": [
|
| 581 |
+
"arm64"
|
| 582 |
+
],
|
| 583 |
+
"dev": true,
|
| 584 |
+
"license": "MIT",
|
| 585 |
+
"optional": true,
|
| 586 |
+
"os": [
|
| 587 |
+
"android"
|
| 588 |
+
],
|
| 589 |
+
"engines": {
|
| 590 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 591 |
+
}
|
| 592 |
+
},
|
| 593 |
+
"node_modules/@rolldown/binding-darwin-arm64": {
|
| 594 |
+
"version": "1.0.0-rc.17",
|
| 595 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz",
|
| 596 |
+
"integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==",
|
| 597 |
+
"cpu": [
|
| 598 |
+
"arm64"
|
| 599 |
+
],
|
| 600 |
+
"dev": true,
|
| 601 |
+
"license": "MIT",
|
| 602 |
+
"optional": true,
|
| 603 |
+
"os": [
|
| 604 |
+
"darwin"
|
| 605 |
+
],
|
| 606 |
+
"engines": {
|
| 607 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 608 |
+
}
|
| 609 |
+
},
|
| 610 |
+
"node_modules/@rolldown/binding-darwin-x64": {
|
| 611 |
+
"version": "1.0.0-rc.17",
|
| 612 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz",
|
| 613 |
+
"integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==",
|
| 614 |
+
"cpu": [
|
| 615 |
+
"x64"
|
| 616 |
+
],
|
| 617 |
+
"dev": true,
|
| 618 |
+
"license": "MIT",
|
| 619 |
+
"optional": true,
|
| 620 |
+
"os": [
|
| 621 |
+
"darwin"
|
| 622 |
+
],
|
| 623 |
+
"engines": {
|
| 624 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 625 |
+
}
|
| 626 |
+
},
|
| 627 |
+
"node_modules/@rolldown/binding-freebsd-x64": {
|
| 628 |
+
"version": "1.0.0-rc.17",
|
| 629 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz",
|
| 630 |
+
"integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==",
|
| 631 |
+
"cpu": [
|
| 632 |
+
"x64"
|
| 633 |
+
],
|
| 634 |
+
"dev": true,
|
| 635 |
+
"license": "MIT",
|
| 636 |
+
"optional": true,
|
| 637 |
+
"os": [
|
| 638 |
+
"freebsd"
|
| 639 |
+
],
|
| 640 |
+
"engines": {
|
| 641 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 642 |
+
}
|
| 643 |
+
},
|
| 644 |
+
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
|
| 645 |
+
"version": "1.0.0-rc.17",
|
| 646 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz",
|
| 647 |
+
"integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==",
|
| 648 |
+
"cpu": [
|
| 649 |
+
"arm"
|
| 650 |
+
],
|
| 651 |
+
"dev": true,
|
| 652 |
+
"license": "MIT",
|
| 653 |
+
"optional": true,
|
| 654 |
+
"os": [
|
| 655 |
+
"linux"
|
| 656 |
+
],
|
| 657 |
+
"engines": {
|
| 658 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 659 |
+
}
|
| 660 |
+
},
|
| 661 |
+
"node_modules/@rolldown/binding-linux-arm64-gnu": {
|
| 662 |
+
"version": "1.0.0-rc.17",
|
| 663 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz",
|
| 664 |
+
"integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==",
|
| 665 |
+
"cpu": [
|
| 666 |
+
"arm64"
|
| 667 |
+
],
|
| 668 |
+
"dev": true,
|
| 669 |
+
"libc": [
|
| 670 |
+
"glibc"
|
| 671 |
+
],
|
| 672 |
+
"license": "MIT",
|
| 673 |
+
"optional": true,
|
| 674 |
+
"os": [
|
| 675 |
+
"linux"
|
| 676 |
+
],
|
| 677 |
+
"engines": {
|
| 678 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 679 |
+
}
|
| 680 |
+
},
|
| 681 |
+
"node_modules/@rolldown/binding-linux-arm64-musl": {
|
| 682 |
+
"version": "1.0.0-rc.17",
|
| 683 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz",
|
| 684 |
+
"integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==",
|
| 685 |
+
"cpu": [
|
| 686 |
+
"arm64"
|
| 687 |
+
],
|
| 688 |
+
"dev": true,
|
| 689 |
+
"libc": [
|
| 690 |
+
"musl"
|
| 691 |
+
],
|
| 692 |
+
"license": "MIT",
|
| 693 |
+
"optional": true,
|
| 694 |
+
"os": [
|
| 695 |
+
"linux"
|
| 696 |
+
],
|
| 697 |
+
"engines": {
|
| 698 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 699 |
+
}
|
| 700 |
+
},
|
| 701 |
+
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
|
| 702 |
+
"version": "1.0.0-rc.17",
|
| 703 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz",
|
| 704 |
+
"integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==",
|
| 705 |
+
"cpu": [
|
| 706 |
+
"ppc64"
|
| 707 |
+
],
|
| 708 |
+
"dev": true,
|
| 709 |
+
"libc": [
|
| 710 |
+
"glibc"
|
| 711 |
+
],
|
| 712 |
+
"license": "MIT",
|
| 713 |
+
"optional": true,
|
| 714 |
+
"os": [
|
| 715 |
+
"linux"
|
| 716 |
+
],
|
| 717 |
+
"engines": {
|
| 718 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 719 |
+
}
|
| 720 |
+
},
|
| 721 |
+
"node_modules/@rolldown/binding-linux-s390x-gnu": {
|
| 722 |
+
"version": "1.0.0-rc.17",
|
| 723 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz",
|
| 724 |
+
"integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==",
|
| 725 |
+
"cpu": [
|
| 726 |
+
"s390x"
|
| 727 |
+
],
|
| 728 |
+
"dev": true,
|
| 729 |
+
"libc": [
|
| 730 |
+
"glibc"
|
| 731 |
+
],
|
| 732 |
+
"license": "MIT",
|
| 733 |
+
"optional": true,
|
| 734 |
+
"os": [
|
| 735 |
+
"linux"
|
| 736 |
+
],
|
| 737 |
+
"engines": {
|
| 738 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 739 |
+
}
|
| 740 |
+
},
|
| 741 |
+
"node_modules/@rolldown/binding-linux-x64-gnu": {
|
| 742 |
+
"version": "1.0.0-rc.17",
|
| 743 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz",
|
| 744 |
+
"integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==",
|
| 745 |
+
"cpu": [
|
| 746 |
+
"x64"
|
| 747 |
+
],
|
| 748 |
+
"dev": true,
|
| 749 |
+
"libc": [
|
| 750 |
+
"glibc"
|
| 751 |
+
],
|
| 752 |
+
"license": "MIT",
|
| 753 |
+
"optional": true,
|
| 754 |
+
"os": [
|
| 755 |
+
"linux"
|
| 756 |
+
],
|
| 757 |
+
"engines": {
|
| 758 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 759 |
+
}
|
| 760 |
+
},
|
| 761 |
+
"node_modules/@rolldown/binding-linux-x64-musl": {
|
| 762 |
+
"version": "1.0.0-rc.17",
|
| 763 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz",
|
| 764 |
+
"integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==",
|
| 765 |
+
"cpu": [
|
| 766 |
+
"x64"
|
| 767 |
+
],
|
| 768 |
+
"dev": true,
|
| 769 |
+
"libc": [
|
| 770 |
+
"musl"
|
| 771 |
+
],
|
| 772 |
+
"license": "MIT",
|
| 773 |
+
"optional": true,
|
| 774 |
+
"os": [
|
| 775 |
+
"linux"
|
| 776 |
+
],
|
| 777 |
+
"engines": {
|
| 778 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 779 |
+
}
|
| 780 |
+
},
|
| 781 |
+
"node_modules/@rolldown/binding-openharmony-arm64": {
|
| 782 |
+
"version": "1.0.0-rc.17",
|
| 783 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz",
|
| 784 |
+
"integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==",
|
| 785 |
+
"cpu": [
|
| 786 |
+
"arm64"
|
| 787 |
+
],
|
| 788 |
+
"dev": true,
|
| 789 |
+
"license": "MIT",
|
| 790 |
+
"optional": true,
|
| 791 |
+
"os": [
|
| 792 |
+
"openharmony"
|
| 793 |
+
],
|
| 794 |
+
"engines": {
|
| 795 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 796 |
+
}
|
| 797 |
+
},
|
| 798 |
+
"node_modules/@rolldown/binding-wasm32-wasi": {
|
| 799 |
+
"version": "1.0.0-rc.17",
|
| 800 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz",
|
| 801 |
+
"integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==",
|
| 802 |
+
"cpu": [
|
| 803 |
+
"wasm32"
|
| 804 |
+
],
|
| 805 |
+
"dev": true,
|
| 806 |
+
"license": "MIT",
|
| 807 |
+
"optional": true,
|
| 808 |
+
"dependencies": {
|
| 809 |
+
"@emnapi/core": "1.10.0",
|
| 810 |
+
"@emnapi/runtime": "1.10.0",
|
| 811 |
+
"@napi-rs/wasm-runtime": "^1.1.4"
|
| 812 |
+
},
|
| 813 |
+
"engines": {
|
| 814 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 815 |
+
}
|
| 816 |
+
},
|
| 817 |
+
"node_modules/@rolldown/binding-win32-arm64-msvc": {
|
| 818 |
+
"version": "1.0.0-rc.17",
|
| 819 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz",
|
| 820 |
+
"integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==",
|
| 821 |
+
"cpu": [
|
| 822 |
+
"arm64"
|
| 823 |
+
],
|
| 824 |
+
"dev": true,
|
| 825 |
+
"license": "MIT",
|
| 826 |
+
"optional": true,
|
| 827 |
+
"os": [
|
| 828 |
+
"win32"
|
| 829 |
+
],
|
| 830 |
+
"engines": {
|
| 831 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 832 |
+
}
|
| 833 |
+
},
|
| 834 |
+
"node_modules/@rolldown/binding-win32-x64-msvc": {
|
| 835 |
+
"version": "1.0.0-rc.17",
|
| 836 |
+
"resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz",
|
| 837 |
+
"integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==",
|
| 838 |
+
"cpu": [
|
| 839 |
+
"x64"
|
| 840 |
+
],
|
| 841 |
+
"dev": true,
|
| 842 |
+
"license": "MIT",
|
| 843 |
+
"optional": true,
|
| 844 |
+
"os": [
|
| 845 |
+
"win32"
|
| 846 |
+
],
|
| 847 |
+
"engines": {
|
| 848 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 849 |
+
}
|
| 850 |
+
},
|
| 851 |
+
"node_modules/@rolldown/pluginutils": {
|
| 852 |
+
"version": "1.0.0-rc.7",
|
| 853 |
+
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
|
| 854 |
+
"integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
|
| 855 |
+
"dev": true,
|
| 856 |
+
"license": "MIT"
|
| 857 |
+
},
|
| 858 |
+
"node_modules/@tybys/wasm-util": {
|
| 859 |
+
"version": "0.10.1",
|
| 860 |
+
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
|
| 861 |
+
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
|
| 862 |
+
"dev": true,
|
| 863 |
+
"license": "MIT",
|
| 864 |
+
"optional": true,
|
| 865 |
+
"dependencies": {
|
| 866 |
+
"tslib": "^2.4.0"
|
| 867 |
+
}
|
| 868 |
+
},
|
| 869 |
+
"node_modules/@types/esrecurse": {
|
| 870 |
+
"version": "4.3.1",
|
| 871 |
+
"resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz",
|
| 872 |
+
"integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==",
|
| 873 |
+
"dev": true,
|
| 874 |
+
"license": "MIT"
|
| 875 |
+
},
|
| 876 |
+
"node_modules/@types/estree": {
|
| 877 |
+
"version": "1.0.8",
|
| 878 |
+
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
| 879 |
+
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
|
| 880 |
+
"dev": true,
|
| 881 |
+
"license": "MIT"
|
| 882 |
+
},
|
| 883 |
+
"node_modules/@types/json-schema": {
|
| 884 |
+
"version": "7.0.15",
|
| 885 |
+
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
| 886 |
+
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
| 887 |
+
"dev": true,
|
| 888 |
+
"license": "MIT"
|
| 889 |
+
},
|
| 890 |
+
"node_modules/@types/node": {
|
| 891 |
+
"version": "24.12.2",
|
| 892 |
+
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.2.tgz",
|
| 893 |
+
"integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==",
|
| 894 |
+
"dev": true,
|
| 895 |
+
"license": "MIT",
|
| 896 |
+
"dependencies": {
|
| 897 |
+
"undici-types": "~7.16.0"
|
| 898 |
+
}
|
| 899 |
+
},
|
| 900 |
+
"node_modules/@types/react": {
|
| 901 |
+
"version": "19.2.14",
|
| 902 |
+
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
| 903 |
+
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
| 904 |
+
"dev": true,
|
| 905 |
+
"license": "MIT",
|
| 906 |
+
"dependencies": {
|
| 907 |
+
"csstype": "^3.2.2"
|
| 908 |
+
}
|
| 909 |
+
},
|
| 910 |
+
"node_modules/@types/react-dom": {
|
| 911 |
+
"version": "19.2.3",
|
| 912 |
+
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
|
| 913 |
+
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
|
| 914 |
+
"dev": true,
|
| 915 |
+
"license": "MIT",
|
| 916 |
+
"peerDependencies": {
|
| 917 |
+
"@types/react": "^19.2.0"
|
| 918 |
+
}
|
| 919 |
+
},
|
| 920 |
+
"node_modules/@typescript-eslint/eslint-plugin": {
|
| 921 |
+
"version": "8.59.0",
|
| 922 |
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.0.tgz",
|
| 923 |
+
"integrity": "sha512-HyAZtpdkgZwpq8Sz3FSUvCR4c+ScbuWa9AksK2Jweub7w4M3yTz4O11AqVJzLYjy/B9ZWPyc81I+mOdJU/bDQw==",
|
| 924 |
+
"dev": true,
|
| 925 |
+
"license": "MIT",
|
| 926 |
+
"dependencies": {
|
| 927 |
+
"@eslint-community/regexpp": "^4.12.2",
|
| 928 |
+
"@typescript-eslint/scope-manager": "8.59.0",
|
| 929 |
+
"@typescript-eslint/type-utils": "8.59.0",
|
| 930 |
+
"@typescript-eslint/utils": "8.59.0",
|
| 931 |
+
"@typescript-eslint/visitor-keys": "8.59.0",
|
| 932 |
+
"ignore": "^7.0.5",
|
| 933 |
+
"natural-compare": "^1.4.0",
|
| 934 |
+
"ts-api-utils": "^2.5.0"
|
| 935 |
+
},
|
| 936 |
+
"engines": {
|
| 937 |
+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 938 |
+
},
|
| 939 |
+
"funding": {
|
| 940 |
+
"type": "opencollective",
|
| 941 |
+
"url": "https://opencollective.com/typescript-eslint"
|
| 942 |
+
},
|
| 943 |
+
"peerDependencies": {
|
| 944 |
+
"@typescript-eslint/parser": "^8.59.0",
|
| 945 |
+
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
| 946 |
+
"typescript": ">=4.8.4 <6.1.0"
|
| 947 |
+
}
|
| 948 |
+
},
|
| 949 |
+
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
| 950 |
+
"version": "7.0.5",
|
| 951 |
+
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
|
| 952 |
+
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
|
| 953 |
+
"dev": true,
|
| 954 |
+
"license": "MIT",
|
| 955 |
+
"engines": {
|
| 956 |
+
"node": ">= 4"
|
| 957 |
+
}
|
| 958 |
+
},
|
| 959 |
+
"node_modules/@typescript-eslint/parser": {
|
| 960 |
+
"version": "8.59.0",
|
| 961 |
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.59.0.tgz",
|
| 962 |
+
"integrity": "sha512-TI1XGwKbDpo9tRW8UDIXCOeLk55qe9ZFGs8MTKU6/M08HWTw52DD/IYhfQtOEhEdPhLMT26Ka/x7p70nd3dzDg==",
|
| 963 |
+
"dev": true,
|
| 964 |
+
"license": "MIT",
|
| 965 |
+
"dependencies": {
|
| 966 |
+
"@typescript-eslint/scope-manager": "8.59.0",
|
| 967 |
+
"@typescript-eslint/types": "8.59.0",
|
| 968 |
+
"@typescript-eslint/typescript-estree": "8.59.0",
|
| 969 |
+
"@typescript-eslint/visitor-keys": "8.59.0",
|
| 970 |
+
"debug": "^4.4.3"
|
| 971 |
+
},
|
| 972 |
+
"engines": {
|
| 973 |
+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 974 |
+
},
|
| 975 |
+
"funding": {
|
| 976 |
+
"type": "opencollective",
|
| 977 |
+
"url": "https://opencollective.com/typescript-eslint"
|
| 978 |
+
},
|
| 979 |
+
"peerDependencies": {
|
| 980 |
+
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
| 981 |
+
"typescript": ">=4.8.4 <6.1.0"
|
| 982 |
+
}
|
| 983 |
+
},
|
| 984 |
+
"node_modules/@typescript-eslint/project-service": {
|
| 985 |
+
"version": "8.59.0",
|
| 986 |
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.59.0.tgz",
|
| 987 |
+
"integrity": "sha512-Lw5ITrR5s5TbC19YSvlr63ZfLaJoU6vtKTHyB0GQOpX0W7d5/Ir6vUahWi/8Sps/nOukZQ0IB3SmlxZnjaKVnw==",
|
| 988 |
+
"dev": true,
|
| 989 |
+
"license": "MIT",
|
| 990 |
+
"dependencies": {
|
| 991 |
+
"@typescript-eslint/tsconfig-utils": "^8.59.0",
|
| 992 |
+
"@typescript-eslint/types": "^8.59.0",
|
| 993 |
+
"debug": "^4.4.3"
|
| 994 |
+
},
|
| 995 |
+
"engines": {
|
| 996 |
+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 997 |
+
},
|
| 998 |
+
"funding": {
|
| 999 |
+
"type": "opencollective",
|
| 1000 |
+
"url": "https://opencollective.com/typescript-eslint"
|
| 1001 |
+
},
|
| 1002 |
+
"peerDependencies": {
|
| 1003 |
+
"typescript": ">=4.8.4 <6.1.0"
|
| 1004 |
+
}
|
| 1005 |
+
},
|
| 1006 |
+
"node_modules/@typescript-eslint/scope-manager": {
|
| 1007 |
+
"version": "8.59.0",
|
| 1008 |
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.59.0.tgz",
|
| 1009 |
+
"integrity": "sha512-UzR16Ut8IpA3Mc4DbgAShlPPkVm8xXMWafXxB0BocaVRHs8ZGakAxGRskF7FId3sdk9lgGD73GSFaWmWFDE4dg==",
|
| 1010 |
+
"dev": true,
|
| 1011 |
+
"license": "MIT",
|
| 1012 |
+
"dependencies": {
|
| 1013 |
+
"@typescript-eslint/types": "8.59.0",
|
| 1014 |
+
"@typescript-eslint/visitor-keys": "8.59.0"
|
| 1015 |
+
},
|
| 1016 |
+
"engines": {
|
| 1017 |
+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 1018 |
+
},
|
| 1019 |
+
"funding": {
|
| 1020 |
+
"type": "opencollective",
|
| 1021 |
+
"url": "https://opencollective.com/typescript-eslint"
|
| 1022 |
+
}
|
| 1023 |
+
},
|
| 1024 |
+
"node_modules/@typescript-eslint/tsconfig-utils": {
|
| 1025 |
+
"version": "8.59.0",
|
| 1026 |
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.0.tgz",
|
| 1027 |
+
"integrity": "sha512-91Sbl3s4Kb3SybliIY6muFBmHVv+pYXfybC4Oolp3dvk8BvIE3wOPc+403CWIT7mJNkfQRGtdqghzs2+Z91Tqg==",
|
| 1028 |
+
"dev": true,
|
| 1029 |
+
"license": "MIT",
|
| 1030 |
+
"engines": {
|
| 1031 |
+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 1032 |
+
},
|
| 1033 |
+
"funding": {
|
| 1034 |
+
"type": "opencollective",
|
| 1035 |
+
"url": "https://opencollective.com/typescript-eslint"
|
| 1036 |
+
},
|
| 1037 |
+
"peerDependencies": {
|
| 1038 |
+
"typescript": ">=4.8.4 <6.1.0"
|
| 1039 |
+
}
|
| 1040 |
+
},
|
| 1041 |
+
"node_modules/@typescript-eslint/type-utils": {
|
| 1042 |
+
"version": "8.59.0",
|
| 1043 |
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.59.0.tgz",
|
| 1044 |
+
"integrity": "sha512-3TRiZaQSltGqGeNrJzzr1+8YcEobKH9rHnqIp/1psfKFmhRQDNMGP5hBufanYTGznwShzVLs3Mz+gDN7HkWfXg==",
|
| 1045 |
+
"dev": true,
|
| 1046 |
+
"license": "MIT",
|
| 1047 |
+
"dependencies": {
|
| 1048 |
+
"@typescript-eslint/types": "8.59.0",
|
| 1049 |
+
"@typescript-eslint/typescript-estree": "8.59.0",
|
| 1050 |
+
"@typescript-eslint/utils": "8.59.0",
|
| 1051 |
+
"debug": "^4.4.3",
|
| 1052 |
+
"ts-api-utils": "^2.5.0"
|
| 1053 |
+
},
|
| 1054 |
+
"engines": {
|
| 1055 |
+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 1056 |
+
},
|
| 1057 |
+
"funding": {
|
| 1058 |
+
"type": "opencollective",
|
| 1059 |
+
"url": "https://opencollective.com/typescript-eslint"
|
| 1060 |
+
},
|
| 1061 |
+
"peerDependencies": {
|
| 1062 |
+
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
| 1063 |
+
"typescript": ">=4.8.4 <6.1.0"
|
| 1064 |
+
}
|
| 1065 |
+
},
|
| 1066 |
+
"node_modules/@typescript-eslint/types": {
|
| 1067 |
+
"version": "8.59.0",
|
| 1068 |
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.59.0.tgz",
|
| 1069 |
+
"integrity": "sha512-nLzdsT1gdOgFxxxwrlNVUBzSNBEEHJ86bblmk4QAS6stfig7rcJzWKqCyxFy3YRRHXDWEkb2NralA1nOYkkm/A==",
|
| 1070 |
+
"dev": true,
|
| 1071 |
+
"license": "MIT",
|
| 1072 |
+
"engines": {
|
| 1073 |
+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 1074 |
+
},
|
| 1075 |
+
"funding": {
|
| 1076 |
+
"type": "opencollective",
|
| 1077 |
+
"url": "https://opencollective.com/typescript-eslint"
|
| 1078 |
+
}
|
| 1079 |
+
},
|
| 1080 |
+
"node_modules/@typescript-eslint/typescript-estree": {
|
| 1081 |
+
"version": "8.59.0",
|
| 1082 |
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.0.tgz",
|
| 1083 |
+
"integrity": "sha512-O9Re9P1BmBLFJyikRbQpLku/QA3/AueZNO9WePLBwQrvkixTmDe8u76B6CYUAITRl/rHawggEqUGn5QIkVRLMw==",
|
| 1084 |
+
"dev": true,
|
| 1085 |
+
"license": "MIT",
|
| 1086 |
+
"dependencies": {
|
| 1087 |
+
"@typescript-eslint/project-service": "8.59.0",
|
| 1088 |
+
"@typescript-eslint/tsconfig-utils": "8.59.0",
|
| 1089 |
+
"@typescript-eslint/types": "8.59.0",
|
| 1090 |
+
"@typescript-eslint/visitor-keys": "8.59.0",
|
| 1091 |
+
"debug": "^4.4.3",
|
| 1092 |
+
"minimatch": "^10.2.2",
|
| 1093 |
+
"semver": "^7.7.3",
|
| 1094 |
+
"tinyglobby": "^0.2.15",
|
| 1095 |
+
"ts-api-utils": "^2.5.0"
|
| 1096 |
+
},
|
| 1097 |
+
"engines": {
|
| 1098 |
+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 1099 |
+
},
|
| 1100 |
+
"funding": {
|
| 1101 |
+
"type": "opencollective",
|
| 1102 |
+
"url": "https://opencollective.com/typescript-eslint"
|
| 1103 |
+
},
|
| 1104 |
+
"peerDependencies": {
|
| 1105 |
+
"typescript": ">=4.8.4 <6.1.0"
|
| 1106 |
+
}
|
| 1107 |
+
},
|
| 1108 |
+
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
| 1109 |
+
"version": "7.7.4",
|
| 1110 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
|
| 1111 |
+
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
|
| 1112 |
+
"dev": true,
|
| 1113 |
+
"license": "ISC",
|
| 1114 |
+
"bin": {
|
| 1115 |
+
"semver": "bin/semver.js"
|
| 1116 |
+
},
|
| 1117 |
+
"engines": {
|
| 1118 |
+
"node": ">=10"
|
| 1119 |
+
}
|
| 1120 |
+
},
|
| 1121 |
+
"node_modules/@typescript-eslint/utils": {
|
| 1122 |
+
"version": "8.59.0",
|
| 1123 |
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.59.0.tgz",
|
| 1124 |
+
"integrity": "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g==",
|
| 1125 |
+
"dev": true,
|
| 1126 |
+
"license": "MIT",
|
| 1127 |
+
"dependencies": {
|
| 1128 |
+
"@eslint-community/eslint-utils": "^4.9.1",
|
| 1129 |
+
"@typescript-eslint/scope-manager": "8.59.0",
|
| 1130 |
+
"@typescript-eslint/types": "8.59.0",
|
| 1131 |
+
"@typescript-eslint/typescript-estree": "8.59.0"
|
| 1132 |
+
},
|
| 1133 |
+
"engines": {
|
| 1134 |
+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 1135 |
+
},
|
| 1136 |
+
"funding": {
|
| 1137 |
+
"type": "opencollective",
|
| 1138 |
+
"url": "https://opencollective.com/typescript-eslint"
|
| 1139 |
+
},
|
| 1140 |
+
"peerDependencies": {
|
| 1141 |
+
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
| 1142 |
+
"typescript": ">=4.8.4 <6.1.0"
|
| 1143 |
+
}
|
| 1144 |
+
},
|
| 1145 |
+
"node_modules/@typescript-eslint/visitor-keys": {
|
| 1146 |
+
"version": "8.59.0",
|
| 1147 |
+
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.0.tgz",
|
| 1148 |
+
"integrity": "sha512-/uejZt4dSere1bx12WLlPfv8GktzcaDtuJ7s42/HEZ5zGj9oxRaD4bj7qwSunXkf+pbAhFt2zjpHYUiT5lHf0Q==",
|
| 1149 |
+
"dev": true,
|
| 1150 |
+
"license": "MIT",
|
| 1151 |
+
"dependencies": {
|
| 1152 |
+
"@typescript-eslint/types": "8.59.0",
|
| 1153 |
+
"eslint-visitor-keys": "^5.0.0"
|
| 1154 |
+
},
|
| 1155 |
+
"engines": {
|
| 1156 |
+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 1157 |
+
},
|
| 1158 |
+
"funding": {
|
| 1159 |
+
"type": "opencollective",
|
| 1160 |
+
"url": "https://opencollective.com/typescript-eslint"
|
| 1161 |
+
}
|
| 1162 |
+
},
|
| 1163 |
+
"node_modules/@vitejs/plugin-react": {
|
| 1164 |
+
"version": "6.0.1",
|
| 1165 |
+
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
|
| 1166 |
+
"integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
|
| 1167 |
+
"dev": true,
|
| 1168 |
+
"license": "MIT",
|
| 1169 |
+
"dependencies": {
|
| 1170 |
+
"@rolldown/pluginutils": "1.0.0-rc.7"
|
| 1171 |
+
},
|
| 1172 |
+
"engines": {
|
| 1173 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 1174 |
+
},
|
| 1175 |
+
"peerDependencies": {
|
| 1176 |
+
"@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
|
| 1177 |
+
"babel-plugin-react-compiler": "^1.0.0",
|
| 1178 |
+
"vite": "^8.0.0"
|
| 1179 |
+
},
|
| 1180 |
+
"peerDependenciesMeta": {
|
| 1181 |
+
"@rolldown/plugin-babel": {
|
| 1182 |
+
"optional": true
|
| 1183 |
+
},
|
| 1184 |
+
"babel-plugin-react-compiler": {
|
| 1185 |
+
"optional": true
|
| 1186 |
+
}
|
| 1187 |
+
}
|
| 1188 |
+
},
|
| 1189 |
+
"node_modules/acorn": {
|
| 1190 |
+
"version": "8.16.0",
|
| 1191 |
+
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
| 1192 |
+
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
| 1193 |
+
"dev": true,
|
| 1194 |
+
"license": "MIT",
|
| 1195 |
+
"bin": {
|
| 1196 |
+
"acorn": "bin/acorn"
|
| 1197 |
+
},
|
| 1198 |
+
"engines": {
|
| 1199 |
+
"node": ">=0.4.0"
|
| 1200 |
+
}
|
| 1201 |
+
},
|
| 1202 |
+
"node_modules/acorn-jsx": {
|
| 1203 |
+
"version": "5.3.2",
|
| 1204 |
+
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
|
| 1205 |
+
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
|
| 1206 |
+
"dev": true,
|
| 1207 |
+
"license": "MIT",
|
| 1208 |
+
"peerDependencies": {
|
| 1209 |
+
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
| 1210 |
+
}
|
| 1211 |
+
},
|
| 1212 |
+
"node_modules/ajv": {
|
| 1213 |
+
"version": "6.15.0",
|
| 1214 |
+
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz",
|
| 1215 |
+
"integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==",
|
| 1216 |
+
"dev": true,
|
| 1217 |
+
"license": "MIT",
|
| 1218 |
+
"dependencies": {
|
| 1219 |
+
"fast-deep-equal": "^3.1.1",
|
| 1220 |
+
"fast-json-stable-stringify": "^2.0.0",
|
| 1221 |
+
"json-schema-traverse": "^0.4.1",
|
| 1222 |
+
"uri-js": "^4.2.2"
|
| 1223 |
+
},
|
| 1224 |
+
"funding": {
|
| 1225 |
+
"type": "github",
|
| 1226 |
+
"url": "https://github.com/sponsors/epoberezkin"
|
| 1227 |
+
}
|
| 1228 |
+
},
|
| 1229 |
+
"node_modules/balanced-match": {
|
| 1230 |
+
"version": "4.0.4",
|
| 1231 |
+
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
| 1232 |
+
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
| 1233 |
+
"dev": true,
|
| 1234 |
+
"license": "MIT",
|
| 1235 |
+
"engines": {
|
| 1236 |
+
"node": "18 || 20 || >=22"
|
| 1237 |
+
}
|
| 1238 |
+
},
|
| 1239 |
+
"node_modules/baseline-browser-mapping": {
|
| 1240 |
+
"version": "2.10.22",
|
| 1241 |
+
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.22.tgz",
|
| 1242 |
+
"integrity": "sha512-6qruVrb5rse6WylFkU0FhBKKGuecWseqdpQfhkawn6ztyk2QlfwSRjsDxMCLJrkfmfN21qvhl9ABgaMeRkuwww==",
|
| 1243 |
+
"dev": true,
|
| 1244 |
+
"license": "Apache-2.0",
|
| 1245 |
+
"bin": {
|
| 1246 |
+
"baseline-browser-mapping": "dist/cli.cjs"
|
| 1247 |
+
},
|
| 1248 |
+
"engines": {
|
| 1249 |
+
"node": ">=6.0.0"
|
| 1250 |
+
}
|
| 1251 |
+
},
|
| 1252 |
+
"node_modules/brace-expansion": {
|
| 1253 |
+
"version": "5.0.5",
|
| 1254 |
+
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
| 1255 |
+
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
| 1256 |
+
"dev": true,
|
| 1257 |
+
"license": "MIT",
|
| 1258 |
+
"dependencies": {
|
| 1259 |
+
"balanced-match": "^4.0.2"
|
| 1260 |
+
},
|
| 1261 |
+
"engines": {
|
| 1262 |
+
"node": "18 || 20 || >=22"
|
| 1263 |
+
}
|
| 1264 |
+
},
|
| 1265 |
+
"node_modules/browserslist": {
|
| 1266 |
+
"version": "4.28.2",
|
| 1267 |
+
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
|
| 1268 |
+
"integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
|
| 1269 |
+
"dev": true,
|
| 1270 |
+
"funding": [
|
| 1271 |
+
{
|
| 1272 |
+
"type": "opencollective",
|
| 1273 |
+
"url": "https://opencollective.com/browserslist"
|
| 1274 |
+
},
|
| 1275 |
+
{
|
| 1276 |
+
"type": "tidelift",
|
| 1277 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 1278 |
+
},
|
| 1279 |
+
{
|
| 1280 |
+
"type": "github",
|
| 1281 |
+
"url": "https://github.com/sponsors/ai"
|
| 1282 |
+
}
|
| 1283 |
+
],
|
| 1284 |
+
"license": "MIT",
|
| 1285 |
+
"dependencies": {
|
| 1286 |
+
"baseline-browser-mapping": "^2.10.12",
|
| 1287 |
+
"caniuse-lite": "^1.0.30001782",
|
| 1288 |
+
"electron-to-chromium": "^1.5.328",
|
| 1289 |
+
"node-releases": "^2.0.36",
|
| 1290 |
+
"update-browserslist-db": "^1.2.3"
|
| 1291 |
+
},
|
| 1292 |
+
"bin": {
|
| 1293 |
+
"browserslist": "cli.js"
|
| 1294 |
+
},
|
| 1295 |
+
"engines": {
|
| 1296 |
+
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
|
| 1297 |
+
}
|
| 1298 |
+
},
|
| 1299 |
+
"node_modules/caniuse-lite": {
|
| 1300 |
+
"version": "1.0.30001790",
|
| 1301 |
+
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001790.tgz",
|
| 1302 |
+
"integrity": "sha512-bOoxfJPyYo+ds6W0YfptaCWbFnJYjh2Y1Eow5lRv+vI2u8ganPZqNm1JwNh0t2ELQCqIWg4B3dWEusgAmsoyOw==",
|
| 1303 |
+
"dev": true,
|
| 1304 |
+
"funding": [
|
| 1305 |
+
{
|
| 1306 |
+
"type": "opencollective",
|
| 1307 |
+
"url": "https://opencollective.com/browserslist"
|
| 1308 |
+
},
|
| 1309 |
+
{
|
| 1310 |
+
"type": "tidelift",
|
| 1311 |
+
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
|
| 1312 |
+
},
|
| 1313 |
+
{
|
| 1314 |
+
"type": "github",
|
| 1315 |
+
"url": "https://github.com/sponsors/ai"
|
| 1316 |
+
}
|
| 1317 |
+
],
|
| 1318 |
+
"license": "CC-BY-4.0"
|
| 1319 |
+
},
|
| 1320 |
+
"node_modules/convert-source-map": {
|
| 1321 |
+
"version": "2.0.0",
|
| 1322 |
+
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
| 1323 |
+
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
| 1324 |
+
"dev": true,
|
| 1325 |
+
"license": "MIT"
|
| 1326 |
+
},
|
| 1327 |
+
"node_modules/cross-spawn": {
|
| 1328 |
+
"version": "7.0.6",
|
| 1329 |
+
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
| 1330 |
+
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
| 1331 |
+
"dev": true,
|
| 1332 |
+
"license": "MIT",
|
| 1333 |
+
"dependencies": {
|
| 1334 |
+
"path-key": "^3.1.0",
|
| 1335 |
+
"shebang-command": "^2.0.0",
|
| 1336 |
+
"which": "^2.0.1"
|
| 1337 |
+
},
|
| 1338 |
+
"engines": {
|
| 1339 |
+
"node": ">= 8"
|
| 1340 |
+
}
|
| 1341 |
+
},
|
| 1342 |
+
"node_modules/csstype": {
|
| 1343 |
+
"version": "3.2.3",
|
| 1344 |
+
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
|
| 1345 |
+
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
| 1346 |
+
"dev": true,
|
| 1347 |
+
"license": "MIT"
|
| 1348 |
+
},
|
| 1349 |
+
"node_modules/debug": {
|
| 1350 |
+
"version": "4.4.3",
|
| 1351 |
+
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
|
| 1352 |
+
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
|
| 1353 |
+
"dev": true,
|
| 1354 |
+
"license": "MIT",
|
| 1355 |
+
"dependencies": {
|
| 1356 |
+
"ms": "^2.1.3"
|
| 1357 |
+
},
|
| 1358 |
+
"engines": {
|
| 1359 |
+
"node": ">=6.0"
|
| 1360 |
+
},
|
| 1361 |
+
"peerDependenciesMeta": {
|
| 1362 |
+
"supports-color": {
|
| 1363 |
+
"optional": true
|
| 1364 |
+
}
|
| 1365 |
+
}
|
| 1366 |
+
},
|
| 1367 |
+
"node_modules/deep-is": {
|
| 1368 |
+
"version": "0.1.4",
|
| 1369 |
+
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
| 1370 |
+
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
|
| 1371 |
+
"dev": true,
|
| 1372 |
+
"license": "MIT"
|
| 1373 |
+
},
|
| 1374 |
+
"node_modules/detect-libc": {
|
| 1375 |
+
"version": "2.1.2",
|
| 1376 |
+
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
| 1377 |
+
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
| 1378 |
+
"dev": true,
|
| 1379 |
+
"license": "Apache-2.0",
|
| 1380 |
+
"engines": {
|
| 1381 |
+
"node": ">=8"
|
| 1382 |
+
}
|
| 1383 |
+
},
|
| 1384 |
+
"node_modules/electron-to-chromium": {
|
| 1385 |
+
"version": "1.5.344",
|
| 1386 |
+
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.344.tgz",
|
| 1387 |
+
"integrity": "sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==",
|
| 1388 |
+
"dev": true,
|
| 1389 |
+
"license": "ISC"
|
| 1390 |
+
},
|
| 1391 |
+
"node_modules/escalade": {
|
| 1392 |
+
"version": "3.2.0",
|
| 1393 |
+
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
| 1394 |
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
| 1395 |
+
"dev": true,
|
| 1396 |
+
"license": "MIT",
|
| 1397 |
+
"engines": {
|
| 1398 |
+
"node": ">=6"
|
| 1399 |
+
}
|
| 1400 |
+
},
|
| 1401 |
+
"node_modules/escape-string-regexp": {
|
| 1402 |
+
"version": "4.0.0",
|
| 1403 |
+
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
|
| 1404 |
+
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
|
| 1405 |
+
"dev": true,
|
| 1406 |
+
"license": "MIT",
|
| 1407 |
+
"engines": {
|
| 1408 |
+
"node": ">=10"
|
| 1409 |
+
},
|
| 1410 |
+
"funding": {
|
| 1411 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1412 |
+
}
|
| 1413 |
+
},
|
| 1414 |
+
"node_modules/eslint": {
|
| 1415 |
+
"version": "10.2.1",
|
| 1416 |
+
"resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.1.tgz",
|
| 1417 |
+
"integrity": "sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==",
|
| 1418 |
+
"dev": true,
|
| 1419 |
+
"license": "MIT",
|
| 1420 |
+
"dependencies": {
|
| 1421 |
+
"@eslint-community/eslint-utils": "^4.8.0",
|
| 1422 |
+
"@eslint-community/regexpp": "^4.12.2",
|
| 1423 |
+
"@eslint/config-array": "^0.23.5",
|
| 1424 |
+
"@eslint/config-helpers": "^0.5.5",
|
| 1425 |
+
"@eslint/core": "^1.2.1",
|
| 1426 |
+
"@eslint/plugin-kit": "^0.7.1",
|
| 1427 |
+
"@humanfs/node": "^0.16.6",
|
| 1428 |
+
"@humanwhocodes/module-importer": "^1.0.1",
|
| 1429 |
+
"@humanwhocodes/retry": "^0.4.2",
|
| 1430 |
+
"@types/estree": "^1.0.6",
|
| 1431 |
+
"ajv": "^6.14.0",
|
| 1432 |
+
"cross-spawn": "^7.0.6",
|
| 1433 |
+
"debug": "^4.3.2",
|
| 1434 |
+
"escape-string-regexp": "^4.0.0",
|
| 1435 |
+
"eslint-scope": "^9.1.2",
|
| 1436 |
+
"eslint-visitor-keys": "^5.0.1",
|
| 1437 |
+
"espree": "^11.2.0",
|
| 1438 |
+
"esquery": "^1.7.0",
|
| 1439 |
+
"esutils": "^2.0.2",
|
| 1440 |
+
"fast-deep-equal": "^3.1.3",
|
| 1441 |
+
"file-entry-cache": "^8.0.0",
|
| 1442 |
+
"find-up": "^5.0.0",
|
| 1443 |
+
"glob-parent": "^6.0.2",
|
| 1444 |
+
"ignore": "^5.2.0",
|
| 1445 |
+
"imurmurhash": "^0.1.4",
|
| 1446 |
+
"is-glob": "^4.0.0",
|
| 1447 |
+
"json-stable-stringify-without-jsonify": "^1.0.1",
|
| 1448 |
+
"minimatch": "^10.2.4",
|
| 1449 |
+
"natural-compare": "^1.4.0",
|
| 1450 |
+
"optionator": "^0.9.3"
|
| 1451 |
+
},
|
| 1452 |
+
"bin": {
|
| 1453 |
+
"eslint": "bin/eslint.js"
|
| 1454 |
+
},
|
| 1455 |
+
"engines": {
|
| 1456 |
+
"node": "^20.19.0 || ^22.13.0 || >=24"
|
| 1457 |
+
},
|
| 1458 |
+
"funding": {
|
| 1459 |
+
"url": "https://eslint.org/donate"
|
| 1460 |
+
},
|
| 1461 |
+
"peerDependencies": {
|
| 1462 |
+
"jiti": "*"
|
| 1463 |
+
},
|
| 1464 |
+
"peerDependenciesMeta": {
|
| 1465 |
+
"jiti": {
|
| 1466 |
+
"optional": true
|
| 1467 |
+
}
|
| 1468 |
+
}
|
| 1469 |
+
},
|
| 1470 |
+
"node_modules/eslint-plugin-react-hooks": {
|
| 1471 |
+
"version": "7.1.1",
|
| 1472 |
+
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.1.1.tgz",
|
| 1473 |
+
"integrity": "sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==",
|
| 1474 |
+
"dev": true,
|
| 1475 |
+
"license": "MIT",
|
| 1476 |
+
"dependencies": {
|
| 1477 |
+
"@babel/core": "^7.24.4",
|
| 1478 |
+
"@babel/parser": "^7.24.4",
|
| 1479 |
+
"hermes-parser": "^0.25.1",
|
| 1480 |
+
"zod": "^3.25.0 || ^4.0.0",
|
| 1481 |
+
"zod-validation-error": "^3.5.0 || ^4.0.0"
|
| 1482 |
+
},
|
| 1483 |
+
"engines": {
|
| 1484 |
+
"node": ">=18"
|
| 1485 |
+
},
|
| 1486 |
+
"peerDependencies": {
|
| 1487 |
+
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0"
|
| 1488 |
+
}
|
| 1489 |
+
},
|
| 1490 |
+
"node_modules/eslint-plugin-react-refresh": {
|
| 1491 |
+
"version": "0.5.2",
|
| 1492 |
+
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz",
|
| 1493 |
+
"integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
|
| 1494 |
+
"dev": true,
|
| 1495 |
+
"license": "MIT",
|
| 1496 |
+
"peerDependencies": {
|
| 1497 |
+
"eslint": "^9 || ^10"
|
| 1498 |
+
}
|
| 1499 |
+
},
|
| 1500 |
+
"node_modules/eslint-scope": {
|
| 1501 |
+
"version": "9.1.2",
|
| 1502 |
+
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",
|
| 1503 |
+
"integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==",
|
| 1504 |
+
"dev": true,
|
| 1505 |
+
"license": "BSD-2-Clause",
|
| 1506 |
+
"dependencies": {
|
| 1507 |
+
"@types/esrecurse": "^4.3.1",
|
| 1508 |
+
"@types/estree": "^1.0.8",
|
| 1509 |
+
"esrecurse": "^4.3.0",
|
| 1510 |
+
"estraverse": "^5.2.0"
|
| 1511 |
+
},
|
| 1512 |
+
"engines": {
|
| 1513 |
+
"node": "^20.19.0 || ^22.13.0 || >=24"
|
| 1514 |
+
},
|
| 1515 |
+
"funding": {
|
| 1516 |
+
"url": "https://opencollective.com/eslint"
|
| 1517 |
+
}
|
| 1518 |
+
},
|
| 1519 |
+
"node_modules/eslint-visitor-keys": {
|
| 1520 |
+
"version": "5.0.1",
|
| 1521 |
+
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
| 1522 |
+
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
| 1523 |
+
"dev": true,
|
| 1524 |
+
"license": "Apache-2.0",
|
| 1525 |
+
"engines": {
|
| 1526 |
+
"node": "^20.19.0 || ^22.13.0 || >=24"
|
| 1527 |
+
},
|
| 1528 |
+
"funding": {
|
| 1529 |
+
"url": "https://opencollective.com/eslint"
|
| 1530 |
+
}
|
| 1531 |
+
},
|
| 1532 |
+
"node_modules/espree": {
|
| 1533 |
+
"version": "11.2.0",
|
| 1534 |
+
"resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz",
|
| 1535 |
+
"integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==",
|
| 1536 |
+
"dev": true,
|
| 1537 |
+
"license": "BSD-2-Clause",
|
| 1538 |
+
"dependencies": {
|
| 1539 |
+
"acorn": "^8.16.0",
|
| 1540 |
+
"acorn-jsx": "^5.3.2",
|
| 1541 |
+
"eslint-visitor-keys": "^5.0.1"
|
| 1542 |
+
},
|
| 1543 |
+
"engines": {
|
| 1544 |
+
"node": "^20.19.0 || ^22.13.0 || >=24"
|
| 1545 |
+
},
|
| 1546 |
+
"funding": {
|
| 1547 |
+
"url": "https://opencollective.com/eslint"
|
| 1548 |
+
}
|
| 1549 |
+
},
|
| 1550 |
+
"node_modules/esquery": {
|
| 1551 |
+
"version": "1.7.0",
|
| 1552 |
+
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz",
|
| 1553 |
+
"integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
|
| 1554 |
+
"dev": true,
|
| 1555 |
+
"license": "BSD-3-Clause",
|
| 1556 |
+
"dependencies": {
|
| 1557 |
+
"estraverse": "^5.1.0"
|
| 1558 |
+
},
|
| 1559 |
+
"engines": {
|
| 1560 |
+
"node": ">=0.10"
|
| 1561 |
+
}
|
| 1562 |
+
},
|
| 1563 |
+
"node_modules/esrecurse": {
|
| 1564 |
+
"version": "4.3.0",
|
| 1565 |
+
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
| 1566 |
+
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
|
| 1567 |
+
"dev": true,
|
| 1568 |
+
"license": "BSD-2-Clause",
|
| 1569 |
+
"dependencies": {
|
| 1570 |
+
"estraverse": "^5.2.0"
|
| 1571 |
+
},
|
| 1572 |
+
"engines": {
|
| 1573 |
+
"node": ">=4.0"
|
| 1574 |
+
}
|
| 1575 |
+
},
|
| 1576 |
+
"node_modules/estraverse": {
|
| 1577 |
+
"version": "5.3.0",
|
| 1578 |
+
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
|
| 1579 |
+
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
|
| 1580 |
+
"dev": true,
|
| 1581 |
+
"license": "BSD-2-Clause",
|
| 1582 |
+
"engines": {
|
| 1583 |
+
"node": ">=4.0"
|
| 1584 |
+
}
|
| 1585 |
+
},
|
| 1586 |
+
"node_modules/esutils": {
|
| 1587 |
+
"version": "2.0.3",
|
| 1588 |
+
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
|
| 1589 |
+
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
|
| 1590 |
+
"dev": true,
|
| 1591 |
+
"license": "BSD-2-Clause",
|
| 1592 |
+
"engines": {
|
| 1593 |
+
"node": ">=0.10.0"
|
| 1594 |
+
}
|
| 1595 |
+
},
|
| 1596 |
+
"node_modules/fast-deep-equal": {
|
| 1597 |
+
"version": "3.1.3",
|
| 1598 |
+
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
| 1599 |
+
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
| 1600 |
+
"dev": true,
|
| 1601 |
+
"license": "MIT"
|
| 1602 |
+
},
|
| 1603 |
+
"node_modules/fast-json-stable-stringify": {
|
| 1604 |
+
"version": "2.1.0",
|
| 1605 |
+
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
|
| 1606 |
+
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
|
| 1607 |
+
"dev": true,
|
| 1608 |
+
"license": "MIT"
|
| 1609 |
+
},
|
| 1610 |
+
"node_modules/fast-levenshtein": {
|
| 1611 |
+
"version": "2.0.6",
|
| 1612 |
+
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
|
| 1613 |
+
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
|
| 1614 |
+
"dev": true,
|
| 1615 |
+
"license": "MIT"
|
| 1616 |
+
},
|
| 1617 |
+
"node_modules/fdir": {
|
| 1618 |
+
"version": "6.5.0",
|
| 1619 |
+
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
| 1620 |
+
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
| 1621 |
+
"dev": true,
|
| 1622 |
+
"license": "MIT",
|
| 1623 |
+
"engines": {
|
| 1624 |
+
"node": ">=12.0.0"
|
| 1625 |
+
},
|
| 1626 |
+
"peerDependencies": {
|
| 1627 |
+
"picomatch": "^3 || ^4"
|
| 1628 |
+
},
|
| 1629 |
+
"peerDependenciesMeta": {
|
| 1630 |
+
"picomatch": {
|
| 1631 |
+
"optional": true
|
| 1632 |
+
}
|
| 1633 |
+
}
|
| 1634 |
+
},
|
| 1635 |
+
"node_modules/file-entry-cache": {
|
| 1636 |
+
"version": "8.0.0",
|
| 1637 |
+
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
|
| 1638 |
+
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
|
| 1639 |
+
"dev": true,
|
| 1640 |
+
"license": "MIT",
|
| 1641 |
+
"dependencies": {
|
| 1642 |
+
"flat-cache": "^4.0.0"
|
| 1643 |
+
},
|
| 1644 |
+
"engines": {
|
| 1645 |
+
"node": ">=16.0.0"
|
| 1646 |
+
}
|
| 1647 |
+
},
|
| 1648 |
+
"node_modules/find-up": {
|
| 1649 |
+
"version": "5.0.0",
|
| 1650 |
+
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
|
| 1651 |
+
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
|
| 1652 |
+
"dev": true,
|
| 1653 |
+
"license": "MIT",
|
| 1654 |
+
"dependencies": {
|
| 1655 |
+
"locate-path": "^6.0.0",
|
| 1656 |
+
"path-exists": "^4.0.0"
|
| 1657 |
+
},
|
| 1658 |
+
"engines": {
|
| 1659 |
+
"node": ">=10"
|
| 1660 |
+
},
|
| 1661 |
+
"funding": {
|
| 1662 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1663 |
+
}
|
| 1664 |
+
},
|
| 1665 |
+
"node_modules/flat-cache": {
|
| 1666 |
+
"version": "4.0.1",
|
| 1667 |
+
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
|
| 1668 |
+
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
|
| 1669 |
+
"dev": true,
|
| 1670 |
+
"license": "MIT",
|
| 1671 |
+
"dependencies": {
|
| 1672 |
+
"flatted": "^3.2.9",
|
| 1673 |
+
"keyv": "^4.5.4"
|
| 1674 |
+
},
|
| 1675 |
+
"engines": {
|
| 1676 |
+
"node": ">=16"
|
| 1677 |
+
}
|
| 1678 |
+
},
|
| 1679 |
+
"node_modules/flatted": {
|
| 1680 |
+
"version": "3.4.2",
|
| 1681 |
+
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
| 1682 |
+
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
| 1683 |
+
"dev": true,
|
| 1684 |
+
"license": "ISC"
|
| 1685 |
+
},
|
| 1686 |
+
"node_modules/fsevents": {
|
| 1687 |
+
"version": "2.3.3",
|
| 1688 |
+
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
| 1689 |
+
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
| 1690 |
+
"dev": true,
|
| 1691 |
+
"hasInstallScript": true,
|
| 1692 |
+
"license": "MIT",
|
| 1693 |
+
"optional": true,
|
| 1694 |
+
"os": [
|
| 1695 |
+
"darwin"
|
| 1696 |
+
],
|
| 1697 |
+
"engines": {
|
| 1698 |
+
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
| 1699 |
+
}
|
| 1700 |
+
},
|
| 1701 |
+
"node_modules/gensync": {
|
| 1702 |
+
"version": "1.0.0-beta.2",
|
| 1703 |
+
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
|
| 1704 |
+
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
|
| 1705 |
+
"dev": true,
|
| 1706 |
+
"license": "MIT",
|
| 1707 |
+
"engines": {
|
| 1708 |
+
"node": ">=6.9.0"
|
| 1709 |
+
}
|
| 1710 |
+
},
|
| 1711 |
+
"node_modules/glob-parent": {
|
| 1712 |
+
"version": "6.0.2",
|
| 1713 |
+
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
|
| 1714 |
+
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
|
| 1715 |
+
"dev": true,
|
| 1716 |
+
"license": "ISC",
|
| 1717 |
+
"dependencies": {
|
| 1718 |
+
"is-glob": "^4.0.3"
|
| 1719 |
+
},
|
| 1720 |
+
"engines": {
|
| 1721 |
+
"node": ">=10.13.0"
|
| 1722 |
+
}
|
| 1723 |
+
},
|
| 1724 |
+
"node_modules/globals": {
|
| 1725 |
+
"version": "17.5.0",
|
| 1726 |
+
"resolved": "https://registry.npmjs.org/globals/-/globals-17.5.0.tgz",
|
| 1727 |
+
"integrity": "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==",
|
| 1728 |
+
"dev": true,
|
| 1729 |
+
"license": "MIT",
|
| 1730 |
+
"engines": {
|
| 1731 |
+
"node": ">=18"
|
| 1732 |
+
},
|
| 1733 |
+
"funding": {
|
| 1734 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 1735 |
+
}
|
| 1736 |
+
},
|
| 1737 |
+
"node_modules/hermes-estree": {
|
| 1738 |
+
"version": "0.25.1",
|
| 1739 |
+
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
|
| 1740 |
+
"integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
|
| 1741 |
+
"dev": true,
|
| 1742 |
+
"license": "MIT"
|
| 1743 |
+
},
|
| 1744 |
+
"node_modules/hermes-parser": {
|
| 1745 |
+
"version": "0.25.1",
|
| 1746 |
+
"resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
|
| 1747 |
+
"integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
|
| 1748 |
+
"dev": true,
|
| 1749 |
+
"license": "MIT",
|
| 1750 |
+
"dependencies": {
|
| 1751 |
+
"hermes-estree": "0.25.1"
|
| 1752 |
+
}
|
| 1753 |
+
},
|
| 1754 |
+
"node_modules/ignore": {
|
| 1755 |
+
"version": "5.3.2",
|
| 1756 |
+
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
| 1757 |
+
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
|
| 1758 |
+
"dev": true,
|
| 1759 |
+
"license": "MIT",
|
| 1760 |
+
"engines": {
|
| 1761 |
+
"node": ">= 4"
|
| 1762 |
+
}
|
| 1763 |
+
},
|
| 1764 |
+
"node_modules/imurmurhash": {
|
| 1765 |
+
"version": "0.1.4",
|
| 1766 |
+
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
|
| 1767 |
+
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
| 1768 |
+
"dev": true,
|
| 1769 |
+
"license": "MIT",
|
| 1770 |
+
"engines": {
|
| 1771 |
+
"node": ">=0.8.19"
|
| 1772 |
+
}
|
| 1773 |
+
},
|
| 1774 |
+
"node_modules/is-extglob": {
|
| 1775 |
+
"version": "2.1.1",
|
| 1776 |
+
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
| 1777 |
+
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
| 1778 |
+
"dev": true,
|
| 1779 |
+
"license": "MIT",
|
| 1780 |
+
"engines": {
|
| 1781 |
+
"node": ">=0.10.0"
|
| 1782 |
+
}
|
| 1783 |
+
},
|
| 1784 |
+
"node_modules/is-glob": {
|
| 1785 |
+
"version": "4.0.3",
|
| 1786 |
+
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
| 1787 |
+
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
| 1788 |
+
"dev": true,
|
| 1789 |
+
"license": "MIT",
|
| 1790 |
+
"dependencies": {
|
| 1791 |
+
"is-extglob": "^2.1.1"
|
| 1792 |
+
},
|
| 1793 |
+
"engines": {
|
| 1794 |
+
"node": ">=0.10.0"
|
| 1795 |
+
}
|
| 1796 |
+
},
|
| 1797 |
+
"node_modules/isexe": {
|
| 1798 |
+
"version": "2.0.0",
|
| 1799 |
+
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
| 1800 |
+
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
| 1801 |
+
"dev": true,
|
| 1802 |
+
"license": "ISC"
|
| 1803 |
+
},
|
| 1804 |
+
"node_modules/js-tokens": {
|
| 1805 |
+
"version": "4.0.0",
|
| 1806 |
+
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
| 1807 |
+
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
| 1808 |
+
"dev": true,
|
| 1809 |
+
"license": "MIT"
|
| 1810 |
+
},
|
| 1811 |
+
"node_modules/jsesc": {
|
| 1812 |
+
"version": "3.1.0",
|
| 1813 |
+
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
|
| 1814 |
+
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
|
| 1815 |
+
"dev": true,
|
| 1816 |
+
"license": "MIT",
|
| 1817 |
+
"bin": {
|
| 1818 |
+
"jsesc": "bin/jsesc"
|
| 1819 |
+
},
|
| 1820 |
+
"engines": {
|
| 1821 |
+
"node": ">=6"
|
| 1822 |
+
}
|
| 1823 |
+
},
|
| 1824 |
+
"node_modules/json-buffer": {
|
| 1825 |
+
"version": "3.0.1",
|
| 1826 |
+
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
|
| 1827 |
+
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
|
| 1828 |
+
"dev": true,
|
| 1829 |
+
"license": "MIT"
|
| 1830 |
+
},
|
| 1831 |
+
"node_modules/json-schema-traverse": {
|
| 1832 |
+
"version": "0.4.1",
|
| 1833 |
+
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
|
| 1834 |
+
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
|
| 1835 |
+
"dev": true,
|
| 1836 |
+
"license": "MIT"
|
| 1837 |
+
},
|
| 1838 |
+
"node_modules/json-stable-stringify-without-jsonify": {
|
| 1839 |
+
"version": "1.0.1",
|
| 1840 |
+
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
|
| 1841 |
+
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
|
| 1842 |
+
"dev": true,
|
| 1843 |
+
"license": "MIT"
|
| 1844 |
+
},
|
| 1845 |
+
"node_modules/json5": {
|
| 1846 |
+
"version": "2.2.3",
|
| 1847 |
+
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
|
| 1848 |
+
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
|
| 1849 |
+
"dev": true,
|
| 1850 |
+
"license": "MIT",
|
| 1851 |
+
"bin": {
|
| 1852 |
+
"json5": "lib/cli.js"
|
| 1853 |
+
},
|
| 1854 |
+
"engines": {
|
| 1855 |
+
"node": ">=6"
|
| 1856 |
+
}
|
| 1857 |
+
},
|
| 1858 |
+
"node_modules/keyv": {
|
| 1859 |
+
"version": "4.5.4",
|
| 1860 |
+
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
| 1861 |
+
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
|
| 1862 |
+
"dev": true,
|
| 1863 |
+
"license": "MIT",
|
| 1864 |
+
"dependencies": {
|
| 1865 |
+
"json-buffer": "3.0.1"
|
| 1866 |
+
}
|
| 1867 |
+
},
|
| 1868 |
+
"node_modules/levn": {
|
| 1869 |
+
"version": "0.4.1",
|
| 1870 |
+
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
|
| 1871 |
+
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
|
| 1872 |
+
"dev": true,
|
| 1873 |
+
"license": "MIT",
|
| 1874 |
+
"dependencies": {
|
| 1875 |
+
"prelude-ls": "^1.2.1",
|
| 1876 |
+
"type-check": "~0.4.0"
|
| 1877 |
+
},
|
| 1878 |
+
"engines": {
|
| 1879 |
+
"node": ">= 0.8.0"
|
| 1880 |
+
}
|
| 1881 |
+
},
|
| 1882 |
+
"node_modules/lightningcss": {
|
| 1883 |
+
"version": "1.32.0",
|
| 1884 |
+
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
| 1885 |
+
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
| 1886 |
+
"dev": true,
|
| 1887 |
+
"license": "MPL-2.0",
|
| 1888 |
+
"dependencies": {
|
| 1889 |
+
"detect-libc": "^2.0.3"
|
| 1890 |
+
},
|
| 1891 |
+
"engines": {
|
| 1892 |
+
"node": ">= 12.0.0"
|
| 1893 |
+
},
|
| 1894 |
+
"funding": {
|
| 1895 |
+
"type": "opencollective",
|
| 1896 |
+
"url": "https://opencollective.com/parcel"
|
| 1897 |
+
},
|
| 1898 |
+
"optionalDependencies": {
|
| 1899 |
+
"lightningcss-android-arm64": "1.32.0",
|
| 1900 |
+
"lightningcss-darwin-arm64": "1.32.0",
|
| 1901 |
+
"lightningcss-darwin-x64": "1.32.0",
|
| 1902 |
+
"lightningcss-freebsd-x64": "1.32.0",
|
| 1903 |
+
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
| 1904 |
+
"lightningcss-linux-arm64-gnu": "1.32.0",
|
| 1905 |
+
"lightningcss-linux-arm64-musl": "1.32.0",
|
| 1906 |
+
"lightningcss-linux-x64-gnu": "1.32.0",
|
| 1907 |
+
"lightningcss-linux-x64-musl": "1.32.0",
|
| 1908 |
+
"lightningcss-win32-arm64-msvc": "1.32.0",
|
| 1909 |
+
"lightningcss-win32-x64-msvc": "1.32.0"
|
| 1910 |
+
}
|
| 1911 |
+
},
|
| 1912 |
+
"node_modules/lightningcss-android-arm64": {
|
| 1913 |
+
"version": "1.32.0",
|
| 1914 |
+
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
| 1915 |
+
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
| 1916 |
+
"cpu": [
|
| 1917 |
+
"arm64"
|
| 1918 |
+
],
|
| 1919 |
+
"dev": true,
|
| 1920 |
+
"license": "MPL-2.0",
|
| 1921 |
+
"optional": true,
|
| 1922 |
+
"os": [
|
| 1923 |
+
"android"
|
| 1924 |
+
],
|
| 1925 |
+
"engines": {
|
| 1926 |
+
"node": ">= 12.0.0"
|
| 1927 |
+
},
|
| 1928 |
+
"funding": {
|
| 1929 |
+
"type": "opencollective",
|
| 1930 |
+
"url": "https://opencollective.com/parcel"
|
| 1931 |
+
}
|
| 1932 |
+
},
|
| 1933 |
+
"node_modules/lightningcss-darwin-arm64": {
|
| 1934 |
+
"version": "1.32.0",
|
| 1935 |
+
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
| 1936 |
+
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
| 1937 |
+
"cpu": [
|
| 1938 |
+
"arm64"
|
| 1939 |
+
],
|
| 1940 |
+
"dev": true,
|
| 1941 |
+
"license": "MPL-2.0",
|
| 1942 |
+
"optional": true,
|
| 1943 |
+
"os": [
|
| 1944 |
+
"darwin"
|
| 1945 |
+
],
|
| 1946 |
+
"engines": {
|
| 1947 |
+
"node": ">= 12.0.0"
|
| 1948 |
+
},
|
| 1949 |
+
"funding": {
|
| 1950 |
+
"type": "opencollective",
|
| 1951 |
+
"url": "https://opencollective.com/parcel"
|
| 1952 |
+
}
|
| 1953 |
+
},
|
| 1954 |
+
"node_modules/lightningcss-darwin-x64": {
|
| 1955 |
+
"version": "1.32.0",
|
| 1956 |
+
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
| 1957 |
+
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
| 1958 |
+
"cpu": [
|
| 1959 |
+
"x64"
|
| 1960 |
+
],
|
| 1961 |
+
"dev": true,
|
| 1962 |
+
"license": "MPL-2.0",
|
| 1963 |
+
"optional": true,
|
| 1964 |
+
"os": [
|
| 1965 |
+
"darwin"
|
| 1966 |
+
],
|
| 1967 |
+
"engines": {
|
| 1968 |
+
"node": ">= 12.0.0"
|
| 1969 |
+
},
|
| 1970 |
+
"funding": {
|
| 1971 |
+
"type": "opencollective",
|
| 1972 |
+
"url": "https://opencollective.com/parcel"
|
| 1973 |
+
}
|
| 1974 |
+
},
|
| 1975 |
+
"node_modules/lightningcss-freebsd-x64": {
|
| 1976 |
+
"version": "1.32.0",
|
| 1977 |
+
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
| 1978 |
+
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
| 1979 |
+
"cpu": [
|
| 1980 |
+
"x64"
|
| 1981 |
+
],
|
| 1982 |
+
"dev": true,
|
| 1983 |
+
"license": "MPL-2.0",
|
| 1984 |
+
"optional": true,
|
| 1985 |
+
"os": [
|
| 1986 |
+
"freebsd"
|
| 1987 |
+
],
|
| 1988 |
+
"engines": {
|
| 1989 |
+
"node": ">= 12.0.0"
|
| 1990 |
+
},
|
| 1991 |
+
"funding": {
|
| 1992 |
+
"type": "opencollective",
|
| 1993 |
+
"url": "https://opencollective.com/parcel"
|
| 1994 |
+
}
|
| 1995 |
+
},
|
| 1996 |
+
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
| 1997 |
+
"version": "1.32.0",
|
| 1998 |
+
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
| 1999 |
+
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
| 2000 |
+
"cpu": [
|
| 2001 |
+
"arm"
|
| 2002 |
+
],
|
| 2003 |
+
"dev": true,
|
| 2004 |
+
"license": "MPL-2.0",
|
| 2005 |
+
"optional": true,
|
| 2006 |
+
"os": [
|
| 2007 |
+
"linux"
|
| 2008 |
+
],
|
| 2009 |
+
"engines": {
|
| 2010 |
+
"node": ">= 12.0.0"
|
| 2011 |
+
},
|
| 2012 |
+
"funding": {
|
| 2013 |
+
"type": "opencollective",
|
| 2014 |
+
"url": "https://opencollective.com/parcel"
|
| 2015 |
+
}
|
| 2016 |
+
},
|
| 2017 |
+
"node_modules/lightningcss-linux-arm64-gnu": {
|
| 2018 |
+
"version": "1.32.0",
|
| 2019 |
+
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
| 2020 |
+
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
| 2021 |
+
"cpu": [
|
| 2022 |
+
"arm64"
|
| 2023 |
+
],
|
| 2024 |
+
"dev": true,
|
| 2025 |
+
"libc": [
|
| 2026 |
+
"glibc"
|
| 2027 |
+
],
|
| 2028 |
+
"license": "MPL-2.0",
|
| 2029 |
+
"optional": true,
|
| 2030 |
+
"os": [
|
| 2031 |
+
"linux"
|
| 2032 |
+
],
|
| 2033 |
+
"engines": {
|
| 2034 |
+
"node": ">= 12.0.0"
|
| 2035 |
+
},
|
| 2036 |
+
"funding": {
|
| 2037 |
+
"type": "opencollective",
|
| 2038 |
+
"url": "https://opencollective.com/parcel"
|
| 2039 |
+
}
|
| 2040 |
+
},
|
| 2041 |
+
"node_modules/lightningcss-linux-arm64-musl": {
|
| 2042 |
+
"version": "1.32.0",
|
| 2043 |
+
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
| 2044 |
+
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
| 2045 |
+
"cpu": [
|
| 2046 |
+
"arm64"
|
| 2047 |
+
],
|
| 2048 |
+
"dev": true,
|
| 2049 |
+
"libc": [
|
| 2050 |
+
"musl"
|
| 2051 |
+
],
|
| 2052 |
+
"license": "MPL-2.0",
|
| 2053 |
+
"optional": true,
|
| 2054 |
+
"os": [
|
| 2055 |
+
"linux"
|
| 2056 |
+
],
|
| 2057 |
+
"engines": {
|
| 2058 |
+
"node": ">= 12.0.0"
|
| 2059 |
+
},
|
| 2060 |
+
"funding": {
|
| 2061 |
+
"type": "opencollective",
|
| 2062 |
+
"url": "https://opencollective.com/parcel"
|
| 2063 |
+
}
|
| 2064 |
+
},
|
| 2065 |
+
"node_modules/lightningcss-linux-x64-gnu": {
|
| 2066 |
+
"version": "1.32.0",
|
| 2067 |
+
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
| 2068 |
+
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
| 2069 |
+
"cpu": [
|
| 2070 |
+
"x64"
|
| 2071 |
+
],
|
| 2072 |
+
"dev": true,
|
| 2073 |
+
"libc": [
|
| 2074 |
+
"glibc"
|
| 2075 |
+
],
|
| 2076 |
+
"license": "MPL-2.0",
|
| 2077 |
+
"optional": true,
|
| 2078 |
+
"os": [
|
| 2079 |
+
"linux"
|
| 2080 |
+
],
|
| 2081 |
+
"engines": {
|
| 2082 |
+
"node": ">= 12.0.0"
|
| 2083 |
+
},
|
| 2084 |
+
"funding": {
|
| 2085 |
+
"type": "opencollective",
|
| 2086 |
+
"url": "https://opencollective.com/parcel"
|
| 2087 |
+
}
|
| 2088 |
+
},
|
| 2089 |
+
"node_modules/lightningcss-linux-x64-musl": {
|
| 2090 |
+
"version": "1.32.0",
|
| 2091 |
+
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
| 2092 |
+
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
| 2093 |
+
"cpu": [
|
| 2094 |
+
"x64"
|
| 2095 |
+
],
|
| 2096 |
+
"dev": true,
|
| 2097 |
+
"libc": [
|
| 2098 |
+
"musl"
|
| 2099 |
+
],
|
| 2100 |
+
"license": "MPL-2.0",
|
| 2101 |
+
"optional": true,
|
| 2102 |
+
"os": [
|
| 2103 |
+
"linux"
|
| 2104 |
+
],
|
| 2105 |
+
"engines": {
|
| 2106 |
+
"node": ">= 12.0.0"
|
| 2107 |
+
},
|
| 2108 |
+
"funding": {
|
| 2109 |
+
"type": "opencollective",
|
| 2110 |
+
"url": "https://opencollective.com/parcel"
|
| 2111 |
+
}
|
| 2112 |
+
},
|
| 2113 |
+
"node_modules/lightningcss-win32-arm64-msvc": {
|
| 2114 |
+
"version": "1.32.0",
|
| 2115 |
+
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
| 2116 |
+
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
| 2117 |
+
"cpu": [
|
| 2118 |
+
"arm64"
|
| 2119 |
+
],
|
| 2120 |
+
"dev": true,
|
| 2121 |
+
"license": "MPL-2.0",
|
| 2122 |
+
"optional": true,
|
| 2123 |
+
"os": [
|
| 2124 |
+
"win32"
|
| 2125 |
+
],
|
| 2126 |
+
"engines": {
|
| 2127 |
+
"node": ">= 12.0.0"
|
| 2128 |
+
},
|
| 2129 |
+
"funding": {
|
| 2130 |
+
"type": "opencollective",
|
| 2131 |
+
"url": "https://opencollective.com/parcel"
|
| 2132 |
+
}
|
| 2133 |
+
},
|
| 2134 |
+
"node_modules/lightningcss-win32-x64-msvc": {
|
| 2135 |
+
"version": "1.32.0",
|
| 2136 |
+
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
| 2137 |
+
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
| 2138 |
+
"cpu": [
|
| 2139 |
+
"x64"
|
| 2140 |
+
],
|
| 2141 |
+
"dev": true,
|
| 2142 |
+
"license": "MPL-2.0",
|
| 2143 |
+
"optional": true,
|
| 2144 |
+
"os": [
|
| 2145 |
+
"win32"
|
| 2146 |
+
],
|
| 2147 |
+
"engines": {
|
| 2148 |
+
"node": ">= 12.0.0"
|
| 2149 |
+
},
|
| 2150 |
+
"funding": {
|
| 2151 |
+
"type": "opencollective",
|
| 2152 |
+
"url": "https://opencollective.com/parcel"
|
| 2153 |
+
}
|
| 2154 |
+
},
|
| 2155 |
+
"node_modules/locate-path": {
|
| 2156 |
+
"version": "6.0.0",
|
| 2157 |
+
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
|
| 2158 |
+
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
|
| 2159 |
+
"dev": true,
|
| 2160 |
+
"license": "MIT",
|
| 2161 |
+
"dependencies": {
|
| 2162 |
+
"p-locate": "^5.0.0"
|
| 2163 |
+
},
|
| 2164 |
+
"engines": {
|
| 2165 |
+
"node": ">=10"
|
| 2166 |
+
},
|
| 2167 |
+
"funding": {
|
| 2168 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2169 |
+
}
|
| 2170 |
+
},
|
| 2171 |
+
"node_modules/lru-cache": {
|
| 2172 |
+
"version": "5.1.1",
|
| 2173 |
+
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
|
| 2174 |
+
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
|
| 2175 |
+
"dev": true,
|
| 2176 |
+
"license": "ISC",
|
| 2177 |
+
"dependencies": {
|
| 2178 |
+
"yallist": "^3.0.2"
|
| 2179 |
+
}
|
| 2180 |
+
},
|
| 2181 |
+
"node_modules/minimatch": {
|
| 2182 |
+
"version": "10.2.5",
|
| 2183 |
+
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
|
| 2184 |
+
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
| 2185 |
+
"dev": true,
|
| 2186 |
+
"license": "BlueOak-1.0.0",
|
| 2187 |
+
"dependencies": {
|
| 2188 |
+
"brace-expansion": "^5.0.5"
|
| 2189 |
+
},
|
| 2190 |
+
"engines": {
|
| 2191 |
+
"node": "18 || 20 || >=22"
|
| 2192 |
+
},
|
| 2193 |
+
"funding": {
|
| 2194 |
+
"url": "https://github.com/sponsors/isaacs"
|
| 2195 |
+
}
|
| 2196 |
+
},
|
| 2197 |
+
"node_modules/ms": {
|
| 2198 |
+
"version": "2.1.3",
|
| 2199 |
+
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
| 2200 |
+
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
| 2201 |
+
"dev": true,
|
| 2202 |
+
"license": "MIT"
|
| 2203 |
+
},
|
| 2204 |
+
"node_modules/nanoid": {
|
| 2205 |
+
"version": "3.3.11",
|
| 2206 |
+
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
| 2207 |
+
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
| 2208 |
+
"dev": true,
|
| 2209 |
+
"funding": [
|
| 2210 |
+
{
|
| 2211 |
+
"type": "github",
|
| 2212 |
+
"url": "https://github.com/sponsors/ai"
|
| 2213 |
+
}
|
| 2214 |
+
],
|
| 2215 |
+
"license": "MIT",
|
| 2216 |
+
"bin": {
|
| 2217 |
+
"nanoid": "bin/nanoid.cjs"
|
| 2218 |
+
},
|
| 2219 |
+
"engines": {
|
| 2220 |
+
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
| 2221 |
+
}
|
| 2222 |
+
},
|
| 2223 |
+
"node_modules/natural-compare": {
|
| 2224 |
+
"version": "1.4.0",
|
| 2225 |
+
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
| 2226 |
+
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
|
| 2227 |
+
"dev": true,
|
| 2228 |
+
"license": "MIT"
|
| 2229 |
+
},
|
| 2230 |
+
"node_modules/node-releases": {
|
| 2231 |
+
"version": "2.0.38",
|
| 2232 |
+
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.38.tgz",
|
| 2233 |
+
"integrity": "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==",
|
| 2234 |
+
"dev": true,
|
| 2235 |
+
"license": "MIT"
|
| 2236 |
+
},
|
| 2237 |
+
"node_modules/optionator": {
|
| 2238 |
+
"version": "0.9.4",
|
| 2239 |
+
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
| 2240 |
+
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
|
| 2241 |
+
"dev": true,
|
| 2242 |
+
"license": "MIT",
|
| 2243 |
+
"dependencies": {
|
| 2244 |
+
"deep-is": "^0.1.3",
|
| 2245 |
+
"fast-levenshtein": "^2.0.6",
|
| 2246 |
+
"levn": "^0.4.1",
|
| 2247 |
+
"prelude-ls": "^1.2.1",
|
| 2248 |
+
"type-check": "^0.4.0",
|
| 2249 |
+
"word-wrap": "^1.2.5"
|
| 2250 |
+
},
|
| 2251 |
+
"engines": {
|
| 2252 |
+
"node": ">= 0.8.0"
|
| 2253 |
+
}
|
| 2254 |
+
},
|
| 2255 |
+
"node_modules/p-limit": {
|
| 2256 |
+
"version": "3.1.0",
|
| 2257 |
+
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
|
| 2258 |
+
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
|
| 2259 |
+
"dev": true,
|
| 2260 |
+
"license": "MIT",
|
| 2261 |
+
"dependencies": {
|
| 2262 |
+
"yocto-queue": "^0.1.0"
|
| 2263 |
+
},
|
| 2264 |
+
"engines": {
|
| 2265 |
+
"node": ">=10"
|
| 2266 |
+
},
|
| 2267 |
+
"funding": {
|
| 2268 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2269 |
+
}
|
| 2270 |
+
},
|
| 2271 |
+
"node_modules/p-locate": {
|
| 2272 |
+
"version": "5.0.0",
|
| 2273 |
+
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
|
| 2274 |
+
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
|
| 2275 |
+
"dev": true,
|
| 2276 |
+
"license": "MIT",
|
| 2277 |
+
"dependencies": {
|
| 2278 |
+
"p-limit": "^3.0.2"
|
| 2279 |
+
},
|
| 2280 |
+
"engines": {
|
| 2281 |
+
"node": ">=10"
|
| 2282 |
+
},
|
| 2283 |
+
"funding": {
|
| 2284 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2285 |
+
}
|
| 2286 |
+
},
|
| 2287 |
+
"node_modules/path-exists": {
|
| 2288 |
+
"version": "4.0.0",
|
| 2289 |
+
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
| 2290 |
+
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
|
| 2291 |
+
"dev": true,
|
| 2292 |
+
"license": "MIT",
|
| 2293 |
+
"engines": {
|
| 2294 |
+
"node": ">=8"
|
| 2295 |
+
}
|
| 2296 |
+
},
|
| 2297 |
+
"node_modules/path-key": {
|
| 2298 |
+
"version": "3.1.1",
|
| 2299 |
+
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
| 2300 |
+
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
| 2301 |
+
"dev": true,
|
| 2302 |
+
"license": "MIT",
|
| 2303 |
+
"engines": {
|
| 2304 |
+
"node": ">=8"
|
| 2305 |
+
}
|
| 2306 |
+
},
|
| 2307 |
+
"node_modules/picocolors": {
|
| 2308 |
+
"version": "1.1.1",
|
| 2309 |
+
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
| 2310 |
+
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
| 2311 |
+
"dev": true,
|
| 2312 |
+
"license": "ISC"
|
| 2313 |
+
},
|
| 2314 |
+
"node_modules/picomatch": {
|
| 2315 |
+
"version": "4.0.4",
|
| 2316 |
+
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
| 2317 |
+
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
| 2318 |
+
"dev": true,
|
| 2319 |
+
"license": "MIT",
|
| 2320 |
+
"engines": {
|
| 2321 |
+
"node": ">=12"
|
| 2322 |
+
},
|
| 2323 |
+
"funding": {
|
| 2324 |
+
"url": "https://github.com/sponsors/jonschlinkert"
|
| 2325 |
+
}
|
| 2326 |
+
},
|
| 2327 |
+
"node_modules/postcss": {
|
| 2328 |
+
"version": "8.5.10",
|
| 2329 |
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
|
| 2330 |
+
"integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
|
| 2331 |
+
"dev": true,
|
| 2332 |
+
"funding": [
|
| 2333 |
+
{
|
| 2334 |
+
"type": "opencollective",
|
| 2335 |
+
"url": "https://opencollective.com/postcss/"
|
| 2336 |
+
},
|
| 2337 |
+
{
|
| 2338 |
+
"type": "tidelift",
|
| 2339 |
+
"url": "https://tidelift.com/funding/github/npm/postcss"
|
| 2340 |
+
},
|
| 2341 |
+
{
|
| 2342 |
+
"type": "github",
|
| 2343 |
+
"url": "https://github.com/sponsors/ai"
|
| 2344 |
+
}
|
| 2345 |
+
],
|
| 2346 |
+
"license": "MIT",
|
| 2347 |
+
"dependencies": {
|
| 2348 |
+
"nanoid": "^3.3.11",
|
| 2349 |
+
"picocolors": "^1.1.1",
|
| 2350 |
+
"source-map-js": "^1.2.1"
|
| 2351 |
+
},
|
| 2352 |
+
"engines": {
|
| 2353 |
+
"node": "^10 || ^12 || >=14"
|
| 2354 |
+
}
|
| 2355 |
+
},
|
| 2356 |
+
"node_modules/prelude-ls": {
|
| 2357 |
+
"version": "1.2.1",
|
| 2358 |
+
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
| 2359 |
+
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
|
| 2360 |
+
"dev": true,
|
| 2361 |
+
"license": "MIT",
|
| 2362 |
+
"engines": {
|
| 2363 |
+
"node": ">= 0.8.0"
|
| 2364 |
+
}
|
| 2365 |
+
},
|
| 2366 |
+
"node_modules/punycode": {
|
| 2367 |
+
"version": "2.3.1",
|
| 2368 |
+
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
| 2369 |
+
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
|
| 2370 |
+
"dev": true,
|
| 2371 |
+
"license": "MIT",
|
| 2372 |
+
"engines": {
|
| 2373 |
+
"node": ">=6"
|
| 2374 |
+
}
|
| 2375 |
+
},
|
| 2376 |
+
"node_modules/react": {
|
| 2377 |
+
"version": "19.2.5",
|
| 2378 |
+
"resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
|
| 2379 |
+
"integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
|
| 2380 |
+
"license": "MIT",
|
| 2381 |
+
"engines": {
|
| 2382 |
+
"node": ">=0.10.0"
|
| 2383 |
+
}
|
| 2384 |
+
},
|
| 2385 |
+
"node_modules/react-dom": {
|
| 2386 |
+
"version": "19.2.5",
|
| 2387 |
+
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
|
| 2388 |
+
"integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
|
| 2389 |
+
"license": "MIT",
|
| 2390 |
+
"dependencies": {
|
| 2391 |
+
"scheduler": "^0.27.0"
|
| 2392 |
+
},
|
| 2393 |
+
"peerDependencies": {
|
| 2394 |
+
"react": "^19.2.5"
|
| 2395 |
+
}
|
| 2396 |
+
},
|
| 2397 |
+
"node_modules/rolldown": {
|
| 2398 |
+
"version": "1.0.0-rc.17",
|
| 2399 |
+
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz",
|
| 2400 |
+
"integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==",
|
| 2401 |
+
"dev": true,
|
| 2402 |
+
"license": "MIT",
|
| 2403 |
+
"dependencies": {
|
| 2404 |
+
"@oxc-project/types": "=0.127.0",
|
| 2405 |
+
"@rolldown/pluginutils": "1.0.0-rc.17"
|
| 2406 |
+
},
|
| 2407 |
+
"bin": {
|
| 2408 |
+
"rolldown": "bin/cli.mjs"
|
| 2409 |
+
},
|
| 2410 |
+
"engines": {
|
| 2411 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 2412 |
+
},
|
| 2413 |
+
"optionalDependencies": {
|
| 2414 |
+
"@rolldown/binding-android-arm64": "1.0.0-rc.17",
|
| 2415 |
+
"@rolldown/binding-darwin-arm64": "1.0.0-rc.17",
|
| 2416 |
+
"@rolldown/binding-darwin-x64": "1.0.0-rc.17",
|
| 2417 |
+
"@rolldown/binding-freebsd-x64": "1.0.0-rc.17",
|
| 2418 |
+
"@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17",
|
| 2419 |
+
"@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17",
|
| 2420 |
+
"@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17",
|
| 2421 |
+
"@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17",
|
| 2422 |
+
"@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17",
|
| 2423 |
+
"@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17",
|
| 2424 |
+
"@rolldown/binding-linux-x64-musl": "1.0.0-rc.17",
|
| 2425 |
+
"@rolldown/binding-openharmony-arm64": "1.0.0-rc.17",
|
| 2426 |
+
"@rolldown/binding-wasm32-wasi": "1.0.0-rc.17",
|
| 2427 |
+
"@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17",
|
| 2428 |
+
"@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17"
|
| 2429 |
+
}
|
| 2430 |
+
},
|
| 2431 |
+
"node_modules/rolldown/node_modules/@rolldown/pluginutils": {
|
| 2432 |
+
"version": "1.0.0-rc.17",
|
| 2433 |
+
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz",
|
| 2434 |
+
"integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==",
|
| 2435 |
+
"dev": true,
|
| 2436 |
+
"license": "MIT"
|
| 2437 |
+
},
|
| 2438 |
+
"node_modules/scheduler": {
|
| 2439 |
+
"version": "0.27.0",
|
| 2440 |
+
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
|
| 2441 |
+
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
|
| 2442 |
+
"license": "MIT"
|
| 2443 |
+
},
|
| 2444 |
+
"node_modules/semver": {
|
| 2445 |
+
"version": "6.3.1",
|
| 2446 |
+
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
|
| 2447 |
+
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
|
| 2448 |
+
"dev": true,
|
| 2449 |
+
"license": "ISC",
|
| 2450 |
+
"bin": {
|
| 2451 |
+
"semver": "bin/semver.js"
|
| 2452 |
+
}
|
| 2453 |
+
},
|
| 2454 |
+
"node_modules/shebang-command": {
|
| 2455 |
+
"version": "2.0.0",
|
| 2456 |
+
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
| 2457 |
+
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
| 2458 |
+
"dev": true,
|
| 2459 |
+
"license": "MIT",
|
| 2460 |
+
"dependencies": {
|
| 2461 |
+
"shebang-regex": "^3.0.0"
|
| 2462 |
+
},
|
| 2463 |
+
"engines": {
|
| 2464 |
+
"node": ">=8"
|
| 2465 |
+
}
|
| 2466 |
+
},
|
| 2467 |
+
"node_modules/shebang-regex": {
|
| 2468 |
+
"version": "3.0.0",
|
| 2469 |
+
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
| 2470 |
+
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
| 2471 |
+
"dev": true,
|
| 2472 |
+
"license": "MIT",
|
| 2473 |
+
"engines": {
|
| 2474 |
+
"node": ">=8"
|
| 2475 |
+
}
|
| 2476 |
+
},
|
| 2477 |
+
"node_modules/source-map-js": {
|
| 2478 |
+
"version": "1.2.1",
|
| 2479 |
+
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
| 2480 |
+
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
| 2481 |
+
"dev": true,
|
| 2482 |
+
"license": "BSD-3-Clause",
|
| 2483 |
+
"engines": {
|
| 2484 |
+
"node": ">=0.10.0"
|
| 2485 |
+
}
|
| 2486 |
+
},
|
| 2487 |
+
"node_modules/tinyglobby": {
|
| 2488 |
+
"version": "0.2.16",
|
| 2489 |
+
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
|
| 2490 |
+
"integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
|
| 2491 |
+
"dev": true,
|
| 2492 |
+
"license": "MIT",
|
| 2493 |
+
"dependencies": {
|
| 2494 |
+
"fdir": "^6.5.0",
|
| 2495 |
+
"picomatch": "^4.0.4"
|
| 2496 |
+
},
|
| 2497 |
+
"engines": {
|
| 2498 |
+
"node": ">=12.0.0"
|
| 2499 |
+
},
|
| 2500 |
+
"funding": {
|
| 2501 |
+
"url": "https://github.com/sponsors/SuperchupuDev"
|
| 2502 |
+
}
|
| 2503 |
+
},
|
| 2504 |
+
"node_modules/ts-api-utils": {
|
| 2505 |
+
"version": "2.5.0",
|
| 2506 |
+
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
| 2507 |
+
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
|
| 2508 |
+
"dev": true,
|
| 2509 |
+
"license": "MIT",
|
| 2510 |
+
"engines": {
|
| 2511 |
+
"node": ">=18.12"
|
| 2512 |
+
},
|
| 2513 |
+
"peerDependencies": {
|
| 2514 |
+
"typescript": ">=4.8.4"
|
| 2515 |
+
}
|
| 2516 |
+
},
|
| 2517 |
+
"node_modules/tslib": {
|
| 2518 |
+
"version": "2.8.1",
|
| 2519 |
+
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
| 2520 |
+
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
| 2521 |
+
"dev": true,
|
| 2522 |
+
"license": "0BSD",
|
| 2523 |
+
"optional": true
|
| 2524 |
+
},
|
| 2525 |
+
"node_modules/type-check": {
|
| 2526 |
+
"version": "0.4.0",
|
| 2527 |
+
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
| 2528 |
+
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
|
| 2529 |
+
"dev": true,
|
| 2530 |
+
"license": "MIT",
|
| 2531 |
+
"dependencies": {
|
| 2532 |
+
"prelude-ls": "^1.2.1"
|
| 2533 |
+
},
|
| 2534 |
+
"engines": {
|
| 2535 |
+
"node": ">= 0.8.0"
|
| 2536 |
+
}
|
| 2537 |
+
},
|
| 2538 |
+
"node_modules/typescript": {
|
| 2539 |
+
"version": "6.0.3",
|
| 2540 |
+
"resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
|
| 2541 |
+
"integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
|
| 2542 |
+
"dev": true,
|
| 2543 |
+
"license": "Apache-2.0",
|
| 2544 |
+
"bin": {
|
| 2545 |
+
"tsc": "bin/tsc",
|
| 2546 |
+
"tsserver": "bin/tsserver"
|
| 2547 |
+
},
|
| 2548 |
+
"engines": {
|
| 2549 |
+
"node": ">=14.17"
|
| 2550 |
+
}
|
| 2551 |
+
},
|
| 2552 |
+
"node_modules/typescript-eslint": {
|
| 2553 |
+
"version": "8.59.0",
|
| 2554 |
+
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.59.0.tgz",
|
| 2555 |
+
"integrity": "sha512-BU3ONW9X+v90EcCH9ZS6LMackcVtxRLlI3XrYyqZIwVSHIk7Qf7bFw1z0M9Q0IUxhTMZCf8piY9hTYaNEIASrw==",
|
| 2556 |
+
"dev": true,
|
| 2557 |
+
"license": "MIT",
|
| 2558 |
+
"dependencies": {
|
| 2559 |
+
"@typescript-eslint/eslint-plugin": "8.59.0",
|
| 2560 |
+
"@typescript-eslint/parser": "8.59.0",
|
| 2561 |
+
"@typescript-eslint/typescript-estree": "8.59.0",
|
| 2562 |
+
"@typescript-eslint/utils": "8.59.0"
|
| 2563 |
+
},
|
| 2564 |
+
"engines": {
|
| 2565 |
+
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
| 2566 |
+
},
|
| 2567 |
+
"funding": {
|
| 2568 |
+
"type": "opencollective",
|
| 2569 |
+
"url": "https://opencollective.com/typescript-eslint"
|
| 2570 |
+
},
|
| 2571 |
+
"peerDependencies": {
|
| 2572 |
+
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
| 2573 |
+
"typescript": ">=4.8.4 <6.1.0"
|
| 2574 |
+
}
|
| 2575 |
+
},
|
| 2576 |
+
"node_modules/undici-types": {
|
| 2577 |
+
"version": "7.16.0",
|
| 2578 |
+
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
|
| 2579 |
+
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
|
| 2580 |
+
"dev": true,
|
| 2581 |
+
"license": "MIT"
|
| 2582 |
+
},
|
| 2583 |
+
"node_modules/update-browserslist-db": {
|
| 2584 |
+
"version": "1.2.3",
|
| 2585 |
+
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
|
| 2586 |
+
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
|
| 2587 |
+
"dev": true,
|
| 2588 |
+
"funding": [
|
| 2589 |
+
{
|
| 2590 |
+
"type": "opencollective",
|
| 2591 |
+
"url": "https://opencollective.com/browserslist"
|
| 2592 |
+
},
|
| 2593 |
+
{
|
| 2594 |
+
"type": "tidelift",
|
| 2595 |
+
"url": "https://tidelift.com/funding/github/npm/browserslist"
|
| 2596 |
+
},
|
| 2597 |
+
{
|
| 2598 |
+
"type": "github",
|
| 2599 |
+
"url": "https://github.com/sponsors/ai"
|
| 2600 |
+
}
|
| 2601 |
+
],
|
| 2602 |
+
"license": "MIT",
|
| 2603 |
+
"dependencies": {
|
| 2604 |
+
"escalade": "^3.2.0",
|
| 2605 |
+
"picocolors": "^1.1.1"
|
| 2606 |
+
},
|
| 2607 |
+
"bin": {
|
| 2608 |
+
"update-browserslist-db": "cli.js"
|
| 2609 |
+
},
|
| 2610 |
+
"peerDependencies": {
|
| 2611 |
+
"browserslist": ">= 4.21.0"
|
| 2612 |
+
}
|
| 2613 |
+
},
|
| 2614 |
+
"node_modules/uri-js": {
|
| 2615 |
+
"version": "4.4.1",
|
| 2616 |
+
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
| 2617 |
+
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
|
| 2618 |
+
"dev": true,
|
| 2619 |
+
"license": "BSD-2-Clause",
|
| 2620 |
+
"dependencies": {
|
| 2621 |
+
"punycode": "^2.1.0"
|
| 2622 |
+
}
|
| 2623 |
+
},
|
| 2624 |
+
"node_modules/vite": {
|
| 2625 |
+
"version": "8.0.10",
|
| 2626 |
+
"resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz",
|
| 2627 |
+
"integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==",
|
| 2628 |
+
"dev": true,
|
| 2629 |
+
"license": "MIT",
|
| 2630 |
+
"dependencies": {
|
| 2631 |
+
"lightningcss": "^1.32.0",
|
| 2632 |
+
"picomatch": "^4.0.4",
|
| 2633 |
+
"postcss": "^8.5.10",
|
| 2634 |
+
"rolldown": "1.0.0-rc.17",
|
| 2635 |
+
"tinyglobby": "^0.2.16"
|
| 2636 |
+
},
|
| 2637 |
+
"bin": {
|
| 2638 |
+
"vite": "bin/vite.js"
|
| 2639 |
+
},
|
| 2640 |
+
"engines": {
|
| 2641 |
+
"node": "^20.19.0 || >=22.12.0"
|
| 2642 |
+
},
|
| 2643 |
+
"funding": {
|
| 2644 |
+
"url": "https://github.com/vitejs/vite?sponsor=1"
|
| 2645 |
+
},
|
| 2646 |
+
"optionalDependencies": {
|
| 2647 |
+
"fsevents": "~2.3.3"
|
| 2648 |
+
},
|
| 2649 |
+
"peerDependencies": {
|
| 2650 |
+
"@types/node": "^20.19.0 || >=22.12.0",
|
| 2651 |
+
"@vitejs/devtools": "^0.1.0",
|
| 2652 |
+
"esbuild": "^0.27.0 || ^0.28.0",
|
| 2653 |
+
"jiti": ">=1.21.0",
|
| 2654 |
+
"less": "^4.0.0",
|
| 2655 |
+
"sass": "^1.70.0",
|
| 2656 |
+
"sass-embedded": "^1.70.0",
|
| 2657 |
+
"stylus": ">=0.54.8",
|
| 2658 |
+
"sugarss": "^5.0.0",
|
| 2659 |
+
"terser": "^5.16.0",
|
| 2660 |
+
"tsx": "^4.8.1",
|
| 2661 |
+
"yaml": "^2.4.2"
|
| 2662 |
+
},
|
| 2663 |
+
"peerDependenciesMeta": {
|
| 2664 |
+
"@types/node": {
|
| 2665 |
+
"optional": true
|
| 2666 |
+
},
|
| 2667 |
+
"@vitejs/devtools": {
|
| 2668 |
+
"optional": true
|
| 2669 |
+
},
|
| 2670 |
+
"esbuild": {
|
| 2671 |
+
"optional": true
|
| 2672 |
+
},
|
| 2673 |
+
"jiti": {
|
| 2674 |
+
"optional": true
|
| 2675 |
+
},
|
| 2676 |
+
"less": {
|
| 2677 |
+
"optional": true
|
| 2678 |
+
},
|
| 2679 |
+
"sass": {
|
| 2680 |
+
"optional": true
|
| 2681 |
+
},
|
| 2682 |
+
"sass-embedded": {
|
| 2683 |
+
"optional": true
|
| 2684 |
+
},
|
| 2685 |
+
"stylus": {
|
| 2686 |
+
"optional": true
|
| 2687 |
+
},
|
| 2688 |
+
"sugarss": {
|
| 2689 |
+
"optional": true
|
| 2690 |
+
},
|
| 2691 |
+
"terser": {
|
| 2692 |
+
"optional": true
|
| 2693 |
+
},
|
| 2694 |
+
"tsx": {
|
| 2695 |
+
"optional": true
|
| 2696 |
+
},
|
| 2697 |
+
"yaml": {
|
| 2698 |
+
"optional": true
|
| 2699 |
+
}
|
| 2700 |
+
}
|
| 2701 |
+
},
|
| 2702 |
+
"node_modules/which": {
|
| 2703 |
+
"version": "2.0.2",
|
| 2704 |
+
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
| 2705 |
+
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
| 2706 |
+
"dev": true,
|
| 2707 |
+
"license": "ISC",
|
| 2708 |
+
"dependencies": {
|
| 2709 |
+
"isexe": "^2.0.0"
|
| 2710 |
+
},
|
| 2711 |
+
"bin": {
|
| 2712 |
+
"node-which": "bin/node-which"
|
| 2713 |
+
},
|
| 2714 |
+
"engines": {
|
| 2715 |
+
"node": ">= 8"
|
| 2716 |
+
}
|
| 2717 |
+
},
|
| 2718 |
+
"node_modules/word-wrap": {
|
| 2719 |
+
"version": "1.2.5",
|
| 2720 |
+
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
|
| 2721 |
+
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
|
| 2722 |
+
"dev": true,
|
| 2723 |
+
"license": "MIT",
|
| 2724 |
+
"engines": {
|
| 2725 |
+
"node": ">=0.10.0"
|
| 2726 |
+
}
|
| 2727 |
+
},
|
| 2728 |
+
"node_modules/yallist": {
|
| 2729 |
+
"version": "3.1.1",
|
| 2730 |
+
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
| 2731 |
+
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
|
| 2732 |
+
"dev": true,
|
| 2733 |
+
"license": "ISC"
|
| 2734 |
+
},
|
| 2735 |
+
"node_modules/yocto-queue": {
|
| 2736 |
+
"version": "0.1.0",
|
| 2737 |
+
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
|
| 2738 |
+
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
|
| 2739 |
+
"dev": true,
|
| 2740 |
+
"license": "MIT",
|
| 2741 |
+
"engines": {
|
| 2742 |
+
"node": ">=10"
|
| 2743 |
+
},
|
| 2744 |
+
"funding": {
|
| 2745 |
+
"url": "https://github.com/sponsors/sindresorhus"
|
| 2746 |
+
}
|
| 2747 |
+
},
|
| 2748 |
+
"node_modules/zod": {
|
| 2749 |
+
"version": "4.3.6",
|
| 2750 |
+
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
| 2751 |
+
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
| 2752 |
+
"dev": true,
|
| 2753 |
+
"license": "MIT",
|
| 2754 |
+
"funding": {
|
| 2755 |
+
"url": "https://github.com/sponsors/colinhacks"
|
| 2756 |
+
}
|
| 2757 |
+
},
|
| 2758 |
+
"node_modules/zod-validation-error": {
|
| 2759 |
+
"version": "4.0.2",
|
| 2760 |
+
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
|
| 2761 |
+
"integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
|
| 2762 |
+
"dev": true,
|
| 2763 |
+
"license": "MIT",
|
| 2764 |
+
"engines": {
|
| 2765 |
+
"node": ">=18.0.0"
|
| 2766 |
+
},
|
| 2767 |
+
"peerDependencies": {
|
| 2768 |
+
"zod": "^3.25.0 || ^4.0.0"
|
| 2769 |
+
}
|
| 2770 |
+
}
|
| 2771 |
+
}
|
| 2772 |
+
}
|
frontend/package.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "frontend",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "tsc -b && vite build",
|
| 9 |
+
"lint": "eslint .",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"react": "^19.2.5",
|
| 14 |
+
"react-dom": "^19.2.5"
|
| 15 |
+
},
|
| 16 |
+
"devDependencies": {
|
| 17 |
+
"@eslint/js": "^10.0.1",
|
| 18 |
+
"@types/node": "^24.12.2",
|
| 19 |
+
"@types/react": "^19.2.14",
|
| 20 |
+
"@types/react-dom": "^19.2.3",
|
| 21 |
+
"@vitejs/plugin-react": "^6.0.1",
|
| 22 |
+
"eslint": "^10.2.1",
|
| 23 |
+
"eslint-plugin-react-hooks": "^7.1.1",
|
| 24 |
+
"eslint-plugin-react-refresh": "^0.5.2",
|
| 25 |
+
"globals": "^17.5.0",
|
| 26 |
+
"typescript": "~6.0.2",
|
| 27 |
+
"typescript-eslint": "^8.58.2",
|
| 28 |
+
"vite": "^8.0.10"
|
| 29 |
+
}
|
| 30 |
+
}
|
frontend/public/favicon.svg
ADDED
|
|
frontend/public/icons.svg
ADDED
|
|
frontend/src/App.css
ADDED
|
@@ -0,0 +1,703 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* ── Reset ── */
|
| 2 |
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
| 3 |
+
|
| 4 |
+
/* ── Design Tokens ── */
|
| 5 |
+
:root {
|
| 6 |
+
--bg: #f5f2ed;
|
| 7 |
+
--s1: #ffffff;
|
| 8 |
+
--s2: #faf9f6;
|
| 9 |
+
--s3: #f0ede8;
|
| 10 |
+
--bd: #e4dfd7;
|
| 11 |
+
--bd2: #cdc7be;
|
| 12 |
+
--t1: #1c1712;
|
| 13 |
+
--t2: #6b6460;
|
| 14 |
+
--t3: #a8a29e;
|
| 15 |
+
--fire: #c2410c;
|
| 16 |
+
--fire2: rgba(194,65,12,0.08);
|
| 17 |
+
--blue: #1d4ed8;
|
| 18 |
+
--blue2: rgba(29,78,216,0.08);
|
| 19 |
+
--green: #166534;
|
| 20 |
+
--green2:rgba(22,101,52,0.08);
|
| 21 |
+
--amber: #92400e;
|
| 22 |
+
--amber2:rgba(146,64,14,0.08);
|
| 23 |
+
--red: #b91c1c;
|
| 24 |
+
--red2: rgba(185,28,28,0.08);
|
| 25 |
+
--gold: #d97706;
|
| 26 |
+
--gold2: rgba(217,119,6,0.12);
|
| 27 |
+
--mono: 'DM Mono', 'JetBrains Mono', monospace;
|
| 28 |
+
--sans: 'DM Sans', 'Inter', system-ui, sans-serif;
|
| 29 |
+
--r: 8px;
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
body {
|
| 33 |
+
background: var(--bg);
|
| 34 |
+
color: var(--t1);
|
| 35 |
+
font-family: var(--sans);
|
| 36 |
+
font-size: 13px;
|
| 37 |
+
line-height: 1.5;
|
| 38 |
+
overflow: hidden;
|
| 39 |
+
}
|
| 40 |
+
|
| 41 |
+
/* ── Shell ── */
|
| 42 |
+
.shell {
|
| 43 |
+
display: grid;
|
| 44 |
+
grid-template-rows: 48px 1fr;
|
| 45 |
+
height: 100vh;
|
| 46 |
+
overflow: hidden;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
/* ── Topbar ── */
|
| 50 |
+
.topbar {
|
| 51 |
+
background: var(--s1);
|
| 52 |
+
border-bottom: 1px solid var(--bd);
|
| 53 |
+
display: flex;
|
| 54 |
+
align-items: center;
|
| 55 |
+
padding: 0 20px;
|
| 56 |
+
gap: 16px;
|
| 57 |
+
z-index: 20;
|
| 58 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.04);
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.brand {
|
| 62 |
+
display: flex;
|
| 63 |
+
align-items: center;
|
| 64 |
+
gap: 10px;
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
.brand-icon {
|
| 68 |
+
width: 28px;
|
| 69 |
+
height: 28px;
|
| 70 |
+
background: linear-gradient(135deg, #c2410c, #f97316);
|
| 71 |
+
border-radius: 6px;
|
| 72 |
+
display: flex;
|
| 73 |
+
align-items: center;
|
| 74 |
+
justify-content: center;
|
| 75 |
+
font-size: 14px;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
.brand-name {
|
| 79 |
+
font-family: var(--mono);
|
| 80 |
+
font-size: 15px;
|
| 81 |
+
font-weight: 500;
|
| 82 |
+
color: var(--t1);
|
| 83 |
+
letter-spacing: 0.06em;
|
| 84 |
+
text-transform: uppercase;
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
.brand-sep {
|
| 88 |
+
color: var(--bd2);
|
| 89 |
+
margin: 0 2px;
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
.brand-sub {
|
| 93 |
+
font-size: 11px;
|
| 94 |
+
color: var(--t3);
|
| 95 |
+
letter-spacing: 0.08em;
|
| 96 |
+
font-family: var(--mono);
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
.topbar-right {
|
| 100 |
+
margin-left: auto;
|
| 101 |
+
display: flex;
|
| 102 |
+
align-items: center;
|
| 103 |
+
gap: 12px;
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
.live-chip {
|
| 107 |
+
display: flex;
|
| 108 |
+
align-items: center;
|
| 109 |
+
gap: 5px;
|
| 110 |
+
font-family: var(--mono);
|
| 111 |
+
font-size: 10px;
|
| 112 |
+
font-weight: 500;
|
| 113 |
+
letter-spacing: 0.08em;
|
| 114 |
+
text-transform: uppercase;
|
| 115 |
+
padding: 4px 10px;
|
| 116 |
+
border-radius: 20px;
|
| 117 |
+
border: 1px solid var(--bd);
|
| 118 |
+
color: var(--t3);
|
| 119 |
+
background: var(--s2);
|
| 120 |
+
}
|
| 121 |
+
|
| 122 |
+
.live-chip::before {
|
| 123 |
+
content: '';
|
| 124 |
+
width: 6px;
|
| 125 |
+
height: 6px;
|
| 126 |
+
border-radius: 50%;
|
| 127 |
+
background: var(--bd2);
|
| 128 |
+
}
|
| 129 |
+
|
| 130 |
+
.live-chip.online {
|
| 131 |
+
color: var(--green);
|
| 132 |
+
background: var(--green2);
|
| 133 |
+
border-color: rgba(22,101,52,0.2);
|
| 134 |
+
}
|
| 135 |
+
|
| 136 |
+
.live-chip.online::before { background: #22c55e; animation: blink 1.2s infinite; }
|
| 137 |
+
|
| 138 |
+
.live-chip.offline {
|
| 139 |
+
color: var(--t3);
|
| 140 |
+
background: var(--s2);
|
| 141 |
+
}
|
| 142 |
+
|
| 143 |
+
.live-chip.error {
|
| 144 |
+
color: var(--red);
|
| 145 |
+
background: var(--red2);
|
| 146 |
+
border-color: rgba(185,28,28,0.2);
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
.live-chip.error::before { background: var(--red); }
|
| 150 |
+
|
| 151 |
+
@keyframes blink { 0%,100% { opacity:1 } 50% { opacity:0.3 } }
|
| 152 |
+
|
| 153 |
+
.topbar-ep {
|
| 154 |
+
font-family: var(--mono);
|
| 155 |
+
font-size: 10px;
|
| 156 |
+
color: var(--t3);
|
| 157 |
+
display: flex;
|
| 158 |
+
align-items: center;
|
| 159 |
+
gap: 6px;
|
| 160 |
+
}
|
| 161 |
+
|
| 162 |
+
.topbar-ep .ep-id {
|
| 163 |
+
color: var(--blue);
|
| 164 |
+
font-weight: 500;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
/* ── Content Layout ── */
|
| 168 |
+
.content {
|
| 169 |
+
display: grid;
|
| 170 |
+
grid-template-columns: 1fr 300px;
|
| 171 |
+
overflow: hidden;
|
| 172 |
+
height: 100%;
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
/* ── Canvas Zone (left) ── */
|
| 176 |
+
.canvas-zone {
|
| 177 |
+
display: flex;
|
| 178 |
+
flex-direction: column;
|
| 179 |
+
align-items: center;
|
| 180 |
+
justify-content: flex-start;
|
| 181 |
+
padding: 20px;
|
| 182 |
+
overflow-y: auto;
|
| 183 |
+
gap: 16px;
|
| 184 |
+
background: var(--bg);
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
.canvas-frame {
|
| 188 |
+
position: relative;
|
| 189 |
+
border-radius: 10px;
|
| 190 |
+
overflow: hidden;
|
| 191 |
+
box-shadow:
|
| 192 |
+
0 0 0 1px var(--bd2),
|
| 193 |
+
0 4px 24px rgba(0,0,0,0.10),
|
| 194 |
+
0 1px 4px rgba(0,0,0,0.06);
|
| 195 |
+
background: #d4c9a8;
|
| 196 |
+
line-height: 0;
|
| 197 |
+
flex-shrink: 0;
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
#map-canvas {
|
| 201 |
+
display: block;
|
| 202 |
+
max-width: 100%;
|
| 203 |
+
max-height: calc(100vh - 48px - 120px);
|
| 204 |
+
image-rendering: pixelated;
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
/* ── HUD Overlays on Canvas ── */
|
| 208 |
+
.hud-overlay {
|
| 209 |
+
position: absolute;
|
| 210 |
+
top: 0; left: 0; right: 0;
|
| 211 |
+
display: flex;
|
| 212 |
+
justify-content: space-between;
|
| 213 |
+
pointer-events: none;
|
| 214 |
+
z-index: 10;
|
| 215 |
+
padding: 10px;
|
| 216 |
+
gap: 8px;
|
| 217 |
+
}
|
| 218 |
+
|
| 219 |
+
.hud-card {
|
| 220 |
+
background: rgba(12, 8, 5, 0.82);
|
| 221 |
+
backdrop-filter: blur(10px);
|
| 222 |
+
border: 1px solid rgba(255,255,255,0.10);
|
| 223 |
+
border-radius: 6px;
|
| 224 |
+
padding: 7px 10px;
|
| 225 |
+
color: #ede6dc;
|
| 226 |
+
display: flex;
|
| 227 |
+
flex-direction: column;
|
| 228 |
+
gap: 3px;
|
| 229 |
+
min-width: 130px;
|
| 230 |
+
}
|
| 231 |
+
|
| 232 |
+
.hud-card-center {
|
| 233 |
+
min-width: 160px;
|
| 234 |
+
border-color: rgba(251,191,36,0.18);
|
| 235 |
+
}
|
| 236 |
+
|
| 237 |
+
.hud-r {
|
| 238 |
+
display: flex;
|
| 239 |
+
align-items: center;
|
| 240 |
+
gap: 6px;
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
.hlbl {
|
| 244 |
+
font-family: var(--mono);
|
| 245 |
+
font-size: 9px;
|
| 246 |
+
color: rgba(168,162,158,0.7);
|
| 247 |
+
letter-spacing: 0.10em;
|
| 248 |
+
text-transform: uppercase;
|
| 249 |
+
flex-shrink: 0;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
.hval {
|
| 253 |
+
font-family: var(--mono);
|
| 254 |
+
font-size: 13px;
|
| 255 |
+
font-weight: 500;
|
| 256 |
+
color: #f0e8e0;
|
| 257 |
+
margin-left: auto;
|
| 258 |
+
min-width: 26px;
|
| 259 |
+
text-align: right;
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
.hbar-bg {
|
| 263 |
+
flex: 1;
|
| 264 |
+
height: 5px;
|
| 265 |
+
background: rgba(255,255,255,0.10);
|
| 266 |
+
border-radius: 3px;
|
| 267 |
+
overflow: hidden;
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
.hbar-fill {
|
| 271 |
+
height: 100%;
|
| 272 |
+
border-radius: 3px;
|
| 273 |
+
transition: width 0.35s ease;
|
| 274 |
+
}
|
| 275 |
+
|
| 276 |
+
.hbar-fill.g { background: linear-gradient(90deg, #22c55e, #4ade80); }
|
| 277 |
+
.hbar-fill.m { background: linear-gradient(90deg, #f59e0b, #fbbf24); }
|
| 278 |
+
.hbar-fill.c { background: linear-gradient(90deg, #ef4444, #f87171); }
|
| 279 |
+
|
| 280 |
+
.hstatus {
|
| 281 |
+
font-family: var(--mono);
|
| 282 |
+
font-size: 9px;
|
| 283 |
+
font-weight: 500;
|
| 284 |
+
letter-spacing: 0.06em;
|
| 285 |
+
text-transform: uppercase;
|
| 286 |
+
padding: 2px 6px;
|
| 287 |
+
border-radius: 3px;
|
| 288 |
+
}
|
| 289 |
+
|
| 290 |
+
.hstatus.good { color: #4ade80; background: rgba(74,222,128,0.12); }
|
| 291 |
+
.hstatus.moderate { color: #fbbf24; background: rgba(251,191,36,0.12); }
|
| 292 |
+
.hstatus.low { color: #fb923c; background: rgba(251,146,60,0.12); }
|
| 293 |
+
.hstatus.critical { color: #f87171; background: rgba(248,113,113,0.12); animation: pulse-red 1s infinite; }
|
| 294 |
+
|
| 295 |
+
@keyframes pulse-red { 0%,100%{opacity:1} 50%{opacity:0.5} }
|
| 296 |
+
|
| 297 |
+
.step-val {
|
| 298 |
+
font-family: var(--mono);
|
| 299 |
+
font-size: 12px;
|
| 300 |
+
font-weight: 500;
|
| 301 |
+
color: #f0e8e0;
|
| 302 |
+
}
|
| 303 |
+
|
| 304 |
+
.sbar-bg {
|
| 305 |
+
height: 3px;
|
| 306 |
+
background: rgba(255,255,255,0.10);
|
| 307 |
+
border-radius: 2px;
|
| 308 |
+
overflow: hidden;
|
| 309 |
+
width: 100%;
|
| 310 |
+
margin-top: 1px;
|
| 311 |
+
}
|
| 312 |
+
|
| 313 |
+
.sbar-fill {
|
| 314 |
+
height: 100%;
|
| 315 |
+
background: linear-gradient(90deg, var(--blue), #60a5fa);
|
| 316 |
+
border-radius: 2px;
|
| 317 |
+
transition: width 0.35s ease;
|
| 318 |
+
}
|
| 319 |
+
|
| 320 |
+
.step-meta {
|
| 321 |
+
font-family: var(--mono);
|
| 322 |
+
font-size: 8px;
|
| 323 |
+
color: rgba(168,162,158,0.55);
|
| 324 |
+
letter-spacing: 0.06em;
|
| 325 |
+
text-transform: uppercase;
|
| 326 |
+
margin-top: 1px;
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
/* ── Legend below canvas ── */
|
| 330 |
+
.legend {
|
| 331 |
+
display: flex;
|
| 332 |
+
gap: 14px;
|
| 333 |
+
flex-wrap: wrap;
|
| 334 |
+
align-items: center;
|
| 335 |
+
padding: 8px 12px;
|
| 336 |
+
background: var(--s1);
|
| 337 |
+
border: 1px solid var(--bd);
|
| 338 |
+
border-radius: 6px;
|
| 339 |
+
width: 100%;
|
| 340 |
+
max-width: 640px;
|
| 341 |
+
}
|
| 342 |
+
|
| 343 |
+
.legend-item {
|
| 344 |
+
display: flex;
|
| 345 |
+
align-items: center;
|
| 346 |
+
gap: 5px;
|
| 347 |
+
font-family: var(--mono);
|
| 348 |
+
font-size: 9px;
|
| 349 |
+
color: var(--t2);
|
| 350 |
+
letter-spacing: 0.05em;
|
| 351 |
+
text-transform: uppercase;
|
| 352 |
+
}
|
| 353 |
+
|
| 354 |
+
.leg-swatch {
|
| 355 |
+
width: 12px;
|
| 356 |
+
height: 12px;
|
| 357 |
+
border-radius: 2px;
|
| 358 |
+
flex-shrink: 0;
|
| 359 |
+
}
|
| 360 |
+
|
| 361 |
+
/* ── Dialog / Field Report ── */
|
| 362 |
+
.dialog {
|
| 363 |
+
background: var(--s1);
|
| 364 |
+
border: 1px solid var(--bd);
|
| 365 |
+
border-left: 3px solid var(--fire);
|
| 366 |
+
border-radius: var(--r);
|
| 367 |
+
padding: 10px 14px;
|
| 368 |
+
font-family: var(--mono);
|
| 369 |
+
font-size: 12px;
|
| 370 |
+
color: var(--t2);
|
| 371 |
+
width: 100%;
|
| 372 |
+
max-width: 640px;
|
| 373 |
+
line-height: 1.5;
|
| 374 |
+
}
|
| 375 |
+
|
| 376 |
+
.dialog-who {
|
| 377 |
+
font-size: 9px;
|
| 378 |
+
color: var(--t3);
|
| 379 |
+
text-transform: uppercase;
|
| 380 |
+
letter-spacing: 0.10em;
|
| 381 |
+
display: block;
|
| 382 |
+
margin-bottom: 3px;
|
| 383 |
+
}
|
| 384 |
+
|
| 385 |
+
/* ── Side Panel (right) ── */
|
| 386 |
+
.side {
|
| 387 |
+
background: var(--s1);
|
| 388 |
+
border-left: 1px solid var(--bd);
|
| 389 |
+
display: flex;
|
| 390 |
+
flex-direction: column;
|
| 391 |
+
overflow-y: auto;
|
| 392 |
+
overflow-x: hidden;
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
.side-sec {
|
| 396 |
+
border-bottom: 1px solid var(--bd);
|
| 397 |
+
padding: 14px 16px;
|
| 398 |
+
}
|
| 399 |
+
|
| 400 |
+
.sec-hd {
|
| 401 |
+
font-family: var(--mono);
|
| 402 |
+
font-size: 9px;
|
| 403 |
+
font-weight: 600;
|
| 404 |
+
color: var(--t3);
|
| 405 |
+
text-transform: uppercase;
|
| 406 |
+
letter-spacing: 0.12em;
|
| 407 |
+
margin-bottom: 10px;
|
| 408 |
+
display: flex;
|
| 409 |
+
align-items: center;
|
| 410 |
+
justify-content: space-between;
|
| 411 |
+
}
|
| 412 |
+
|
| 413 |
+
/* ── Stat Grid (2×N) ── */
|
| 414 |
+
.sg {
|
| 415 |
+
display: grid;
|
| 416 |
+
grid-template-columns: 1fr 1fr;
|
| 417 |
+
gap: 6px;
|
| 418 |
+
}
|
| 419 |
+
|
| 420 |
+
.sc {
|
| 421 |
+
background: var(--s2);
|
| 422 |
+
border: 1px solid var(--bd);
|
| 423 |
+
border-radius: 6px;
|
| 424 |
+
padding: 8px 10px;
|
| 425 |
+
}
|
| 426 |
+
|
| 427 |
+
.sc-l {
|
| 428 |
+
font-family: var(--mono);
|
| 429 |
+
font-size: 9px;
|
| 430 |
+
color: var(--t3);
|
| 431 |
+
text-transform: uppercase;
|
| 432 |
+
letter-spacing: 0.08em;
|
| 433 |
+
margin-bottom: 2px;
|
| 434 |
+
}
|
| 435 |
+
|
| 436 |
+
.sc-v {
|
| 437 |
+
font-family: var(--mono);
|
| 438 |
+
font-size: 14px;
|
| 439 |
+
font-weight: 500;
|
| 440 |
+
color: var(--t1);
|
| 441 |
+
}
|
| 442 |
+
|
| 443 |
+
.sc-v.fire { color: var(--fire); }
|
| 444 |
+
.sc-v.blue { color: var(--blue); }
|
| 445 |
+
.sc-v.green { color: var(--green); }
|
| 446 |
+
.sc-v.amber { color: var(--amber); }
|
| 447 |
+
.sc-v.gold { color: var(--gold); }
|
| 448 |
+
|
| 449 |
+
/* ── Status Rows ── */
|
| 450 |
+
.srow {
|
| 451 |
+
display: flex;
|
| 452 |
+
justify-content: space-between;
|
| 453 |
+
align-items: center;
|
| 454 |
+
padding: 5px 0;
|
| 455 |
+
font-size: 12px;
|
| 456 |
+
border-bottom: 1px solid rgba(0,0,0,0.03);
|
| 457 |
+
}
|
| 458 |
+
|
| 459 |
+
.srow:last-child { border-bottom: none; }
|
| 460 |
+
|
| 461 |
+
.sv { font-family: var(--mono); font-weight: 500; }
|
| 462 |
+
.sv.blue { color: var(--blue); }
|
| 463 |
+
.sv.green { color: var(--green); }
|
| 464 |
+
.sv.hot { color: var(--fire); }
|
| 465 |
+
.sv.danger{ color: var(--red); }
|
| 466 |
+
.sv.warn { color: var(--amber); }
|
| 467 |
+
.sv.gold { color: var(--gold); }
|
| 468 |
+
|
| 469 |
+
/* ── Health bar in side panel ── */
|
| 470 |
+
.bar-w { margin-top: 10px; }
|
| 471 |
+
|
| 472 |
+
.bar-lbl {
|
| 473 |
+
font-family: var(--mono);
|
| 474 |
+
font-size: 9px;
|
| 475 |
+
color: var(--t3);
|
| 476 |
+
display: flex;
|
| 477 |
+
justify-content: space-between;
|
| 478 |
+
margin-bottom: 5px;
|
| 479 |
+
text-transform: uppercase;
|
| 480 |
+
letter-spacing: 0.06em;
|
| 481 |
+
}
|
| 482 |
+
|
| 483 |
+
.bar-bg {
|
| 484 |
+
background: var(--s2);
|
| 485 |
+
border: 1px solid var(--bd);
|
| 486 |
+
border-radius: 4px;
|
| 487 |
+
height: 6px;
|
| 488 |
+
overflow: hidden;
|
| 489 |
+
}
|
| 490 |
+
|
| 491 |
+
.bar-fill {
|
| 492 |
+
height: 100%;
|
| 493 |
+
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
| 494 |
+
border-radius: 4px;
|
| 495 |
+
}
|
| 496 |
+
|
| 497 |
+
/* ── Event Log ── */
|
| 498 |
+
.elog {
|
| 499 |
+
display: flex;
|
| 500 |
+
flex-direction: column;
|
| 501 |
+
gap: 2px;
|
| 502 |
+
max-height: 160px;
|
| 503 |
+
overflow-y: auto;
|
| 504 |
+
}
|
| 505 |
+
|
| 506 |
+
.erow {
|
| 507 |
+
display: flex;
|
| 508 |
+
align-items: flex-start;
|
| 509 |
+
gap: 6px;
|
| 510 |
+
padding: 4px 6px;
|
| 511 |
+
border-radius: 4px;
|
| 512 |
+
font-size: 11px;
|
| 513 |
+
background: transparent;
|
| 514 |
+
}
|
| 515 |
+
|
| 516 |
+
.erow.alarm {
|
| 517 |
+
background: var(--red2);
|
| 518 |
+
border-left: 2px solid var(--red);
|
| 519 |
+
}
|
| 520 |
+
|
| 521 |
+
.erow.warn-row {
|
| 522 |
+
background: var(--amber2);
|
| 523 |
+
border-left: 2px solid var(--amber);
|
| 524 |
+
}
|
| 525 |
+
|
| 526 |
+
.estep {
|
| 527 |
+
font-family: var(--mono);
|
| 528 |
+
font-size: 9px;
|
| 529 |
+
color: var(--t3);
|
| 530 |
+
min-width: 26px;
|
| 531 |
+
padding-top: 1px;
|
| 532 |
+
flex-shrink: 0;
|
| 533 |
+
}
|
| 534 |
+
|
| 535 |
+
.etext {
|
| 536 |
+
color: var(--t2);
|
| 537 |
+
line-height: 1.4;
|
| 538 |
+
flex: 1;
|
| 539 |
+
font-size: 11px;
|
| 540 |
+
}
|
| 541 |
+
|
| 542 |
+
.erwd {
|
| 543 |
+
font-family: var(--mono);
|
| 544 |
+
font-size: 9px;
|
| 545 |
+
font-weight: 500;
|
| 546 |
+
flex-shrink: 0;
|
| 547 |
+
padding-top: 1px;
|
| 548 |
+
}
|
| 549 |
+
|
| 550 |
+
.erwd.p { color: var(--green); }
|
| 551 |
+
.erwd.n { color: var(--red); }
|
| 552 |
+
|
| 553 |
+
/* ── Direction Pad ── */
|
| 554 |
+
.dpad {
|
| 555 |
+
display: grid;
|
| 556 |
+
grid-template-columns: repeat(3, 1fr);
|
| 557 |
+
gap: 5px;
|
| 558 |
+
max-width: 140px;
|
| 559 |
+
margin: 0 auto;
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
.dpad-center {
|
| 563 |
+
display: flex;
|
| 564 |
+
align-items: center;
|
| 565 |
+
justify-content: center;
|
| 566 |
+
}
|
| 567 |
+
|
| 568 |
+
/* ── Control Buttons ── */
|
| 569 |
+
.ctrl-grid {
|
| 570 |
+
display: grid;
|
| 571 |
+
grid-template-columns: repeat(3, 1fr);
|
| 572 |
+
gap: 5px;
|
| 573 |
+
}
|
| 574 |
+
|
| 575 |
+
.ctrl-row {
|
| 576 |
+
display: flex;
|
| 577 |
+
gap: 5px;
|
| 578 |
+
margin-top: 8px;
|
| 579 |
+
}
|
| 580 |
+
|
| 581 |
+
.ctrl-btn {
|
| 582 |
+
background: var(--s2);
|
| 583 |
+
color: var(--t2);
|
| 584 |
+
border: 1px solid var(--bd);
|
| 585 |
+
border-radius: 5px;
|
| 586 |
+
padding: 7px 6px;
|
| 587 |
+
font-family: var(--mono);
|
| 588 |
+
font-size: 10px;
|
| 589 |
+
font-weight: 500;
|
| 590 |
+
letter-spacing: 0.04em;
|
| 591 |
+
cursor: pointer;
|
| 592 |
+
transition: all 0.1s;
|
| 593 |
+
text-align: center;
|
| 594 |
+
flex: 1;
|
| 595 |
+
}
|
| 596 |
+
|
| 597 |
+
.ctrl-btn:hover:not(:disabled) {
|
| 598 |
+
background: var(--s3);
|
| 599 |
+
color: var(--t1);
|
| 600 |
+
border-color: var(--bd2);
|
| 601 |
+
}
|
| 602 |
+
|
| 603 |
+
.ctrl-btn:disabled {
|
| 604 |
+
opacity: 0.4;
|
| 605 |
+
cursor: not-allowed;
|
| 606 |
+
}
|
| 607 |
+
|
| 608 |
+
.ctrl-btn.accent {
|
| 609 |
+
background: var(--fire2);
|
| 610 |
+
color: var(--fire);
|
| 611 |
+
border-color: rgba(194,65,12,0.25);
|
| 612 |
+
}
|
| 613 |
+
|
| 614 |
+
.ctrl-btn.accent:hover {
|
| 615 |
+
background: rgba(194,65,12,0.15);
|
| 616 |
+
color: var(--fire);
|
| 617 |
+
}
|
| 618 |
+
|
| 619 |
+
.ctrl-btn.active {
|
| 620 |
+
background: var(--blue2);
|
| 621 |
+
color: var(--blue);
|
| 622 |
+
border-color: rgba(29,78,216,0.25);
|
| 623 |
+
}
|
| 624 |
+
|
| 625 |
+
.ctrl-btn.active:hover {
|
| 626 |
+
background: rgba(29,78,216,0.15);
|
| 627 |
+
}
|
| 628 |
+
|
| 629 |
+
.ctrl-btn.play {
|
| 630 |
+
background: linear-gradient(135deg, #c2410c, #f97316);
|
| 631 |
+
color: white;
|
| 632 |
+
border: none;
|
| 633 |
+
font-size: 11px;
|
| 634 |
+
box-shadow: 0 2px 8px rgba(194,65,12,0.30);
|
| 635 |
+
}
|
| 636 |
+
|
| 637 |
+
.ctrl-btn.play:hover {
|
| 638 |
+
box-shadow: 0 3px 12px rgba(194,65,12,0.45);
|
| 639 |
+
filter: brightness(1.05);
|
| 640 |
+
}
|
| 641 |
+
|
| 642 |
+
.ctrl-status {
|
| 643 |
+
margin-top: 10px;
|
| 644 |
+
font-family: var(--mono);
|
| 645 |
+
font-size: 10px;
|
| 646 |
+
color: var(--t3);
|
| 647 |
+
padding: 7px 10px;
|
| 648 |
+
background: var(--s2);
|
| 649 |
+
border-radius: 5px;
|
| 650 |
+
border: 1px solid var(--bd);
|
| 651 |
+
border-left: 3px solid var(--bd2);
|
| 652 |
+
}
|
| 653 |
+
|
| 654 |
+
.ctrl-status.error {
|
| 655 |
+
color: var(--red);
|
| 656 |
+
background: var(--red2);
|
| 657 |
+
border-left-color: var(--red);
|
| 658 |
+
}
|
| 659 |
+
|
| 660 |
+
/* ── Door List ── */
|
| 661 |
+
.door-grid {
|
| 662 |
+
display: grid;
|
| 663 |
+
grid-template-columns: 1fr 1fr;
|
| 664 |
+
gap: 4px;
|
| 665 |
+
}
|
| 666 |
+
|
| 667 |
+
.door-btn {
|
| 668 |
+
background: var(--s2);
|
| 669 |
+
border: 1px solid var(--bd);
|
| 670 |
+
border-radius: 5px;
|
| 671 |
+
padding: 5px 4px;
|
| 672 |
+
font-family: var(--mono);
|
| 673 |
+
font-size: 9px;
|
| 674 |
+
cursor: pointer;
|
| 675 |
+
color: var(--t2);
|
| 676 |
+
text-align: center;
|
| 677 |
+
transition: all 0.1s;
|
| 678 |
+
}
|
| 679 |
+
|
| 680 |
+
.door-btn:hover { background: var(--s3); color: var(--t1); }
|
| 681 |
+
.door-btn.open { color: var(--green); border-color: rgba(22,101,52,0.25); background: var(--green2); }
|
| 682 |
+
.door-btn.closed { color: var(--amber); border-color: rgba(146,64,14,0.25); background: var(--amber2); }
|
| 683 |
+
.door-btn.failed { color: var(--red); border-color: rgba(185,28,28,0.25); background: var(--red2); }
|
| 684 |
+
|
| 685 |
+
/* ── API Report ── */
|
| 686 |
+
.report-box {
|
| 687 |
+
background: var(--s2);
|
| 688 |
+
border: 1px solid var(--bd);
|
| 689 |
+
border-radius: 5px;
|
| 690 |
+
padding: 10px;
|
| 691 |
+
max-height: 160px;
|
| 692 |
+
overflow-y: auto;
|
| 693 |
+
font-family: var(--mono);
|
| 694 |
+
font-size: 9px;
|
| 695 |
+
color: var(--t2);
|
| 696 |
+
white-space: pre-wrap;
|
| 697 |
+
line-height: 1.5;
|
| 698 |
+
}
|
| 699 |
+
|
| 700 |
+
/* ── Scrollbar ── */
|
| 701 |
+
::-webkit-scrollbar { width: 4px; height: 4px; }
|
| 702 |
+
::-webkit-scrollbar-track { background: transparent; }
|
| 703 |
+
::-webkit-scrollbar-thumb { background: var(--bd2); border-radius: 2px; }
|
frontend/src/App.tsx
ADDED
|
@@ -0,0 +1,460 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect, useCallback, useRef } from 'react';
|
| 2 |
+
import './App.css';
|
| 3 |
+
import type { Observation, Door, ApiReport, SceneResponse } from './types';
|
| 4 |
+
import Map2D from './components/Map2D';
|
| 5 |
+
import HUD from './components/HUD';
|
| 6 |
+
import ControlPanel from './components/ControlPanel';
|
| 7 |
+
import APIReport from './components/APIReport';
|
| 8 |
+
|
| 9 |
+
const DOOR_CLOSED = 3;
|
| 10 |
+
const OBSTACLE = 5;
|
| 11 |
+
|
| 12 |
+
interface EventEntry {
|
| 13 |
+
step: number;
|
| 14 |
+
text: string;
|
| 15 |
+
reward: number;
|
| 16 |
+
isAlarm?: boolean;
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
function App() {
|
| 20 |
+
const [observation, setObservation] = useState<Observation | null>(null);
|
| 21 |
+
const [sceneData, setSceneData] = useState<SceneResponse | null>(null);
|
| 22 |
+
const [isAutoWait, setIsAutoWait] = useState(false);
|
| 23 |
+
const [isPolling, setIsPolling] = useState(true);
|
| 24 |
+
const [status, setStatus] = useState('Idle — waiting for connection');
|
| 25 |
+
const [isError, setIsError] = useState(false);
|
| 26 |
+
const [apiReport, setApiReport] = useState<ApiReport | null>(null);
|
| 27 |
+
const [agentMoveCount, setAgentMoveCount] = useState(0);
|
| 28 |
+
const [agentMoveFlash, setAgentMoveFlash] = useState(0);
|
| 29 |
+
const [eventLog, setEventLog] = useState<EventEntry[]>([]);
|
| 30 |
+
|
| 31 |
+
const prevAgentPos = useRef({ x: -1, y: -1 });
|
| 32 |
+
const autoWaitTimer = useRef<number | null>(null);
|
| 33 |
+
const pollTimer = useRef<number | null>(null);
|
| 34 |
+
const logEndRef = useRef<HTMLDivElement | null>(null);
|
| 35 |
+
|
| 36 |
+
/* scroll event log to bottom */
|
| 37 |
+
useEffect(() => {
|
| 38 |
+
logEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
| 39 |
+
}, [eventLog]);
|
| 40 |
+
|
| 41 |
+
const setStatusMsg = (msg: string, error = false) => {
|
| 42 |
+
setStatus(msg);
|
| 43 |
+
setIsError(error);
|
| 44 |
+
};
|
| 45 |
+
|
| 46 |
+
const pushLog = (text: string, step: number, reward: number, isAlarm = false) => {
|
| 47 |
+
setEventLog(prev => [...prev.slice(-49), { step, text, reward, isAlarm }]);
|
| 48 |
+
};
|
| 49 |
+
|
| 50 |
+
const applyObservation = useCallback((obs: Observation) => {
|
| 51 |
+
const newX = obs.map_state.agent_x;
|
| 52 |
+
const newY = obs.map_state.agent_y;
|
| 53 |
+
if (prevAgentPos.current.x !== -1 &&
|
| 54 |
+
(newX !== prevAgentPos.current.x || newY !== prevAgentPos.current.y)) {
|
| 55 |
+
setAgentMoveFlash(18);
|
| 56 |
+
setAgentMoveCount(c => c + 1);
|
| 57 |
+
}
|
| 58 |
+
prevAgentPos.current = { x: newX, y: newY };
|
| 59 |
+
setObservation(obs);
|
| 60 |
+
}, []);
|
| 61 |
+
|
| 62 |
+
const updateReport = (kind: string, request: unknown, response: any) => {
|
| 63 |
+
const mapState = response?.observation?.map_state || response?.map_state || response?.graph;
|
| 64 |
+
const template = mapState?.template_name || response?.labels?.episode?.template || 'unknown';
|
| 65 |
+
const step = mapState?.step_count ?? response?.observation?.elapsed_steps ?? response?.labels?.episode?.step ?? '-';
|
| 66 |
+
const reward = Number(response?.reward ?? 0).toFixed(3);
|
| 67 |
+
const done = Boolean(response?.done);
|
| 68 |
+
setApiReport({
|
| 69 |
+
call_type: kind,
|
| 70 |
+
request,
|
| 71 |
+
response,
|
| 72 |
+
meta: `${kind.toUpperCase()} | template=${template} | step=${step} | reward=${reward} | done=${done}`,
|
| 73 |
+
});
|
| 74 |
+
};
|
| 75 |
+
|
| 76 |
+
const apiCall = async (path: string, payload: unknown) => {
|
| 77 |
+
const res = await fetch(path, {
|
| 78 |
+
method: 'POST',
|
| 79 |
+
headers: { 'Content-Type': 'application/json' },
|
| 80 |
+
body: JSON.stringify(payload || {}),
|
| 81 |
+
});
|
| 82 |
+
if (!res.ok) {
|
| 83 |
+
const text = await res.text();
|
| 84 |
+
throw new Error(`${res.status} ${res.statusText}: ${text}`);
|
| 85 |
+
}
|
| 86 |
+
return res.json();
|
| 87 |
+
};
|
| 88 |
+
|
| 89 |
+
const resetLive = async (difficulty = 'medium') => {
|
| 90 |
+
try {
|
| 91 |
+
setStatusMsg('Initiating Reset...');
|
| 92 |
+
const payload = { difficulty };
|
| 93 |
+
const data = await apiCall('/reset', payload);
|
| 94 |
+
const obs: Observation = data.observation;
|
| 95 |
+
if (data.observation?.map_state) {
|
| 96 |
+
obs.metadata = {
|
| 97 |
+
fire_sources: data.observation.fire_sources_count ?? 0,
|
| 98 |
+
fire_spread_rate:data.observation.fire_spread_rate ?? 0,
|
| 99 |
+
humidity: data.observation.humidity ?? 0,
|
| 100 |
+
difficulty,
|
| 101 |
+
};
|
| 102 |
+
}
|
| 103 |
+
applyObservation(obs);
|
| 104 |
+
updateReport('reset', payload, data);
|
| 105 |
+
setStatusMsg(`Ready. Reward: ${Number(data.reward || 0).toFixed(2)}`);
|
| 106 |
+
pushLog('Episode reset. Assess surroundings.', obs.map_state.step_count, data.reward ?? 0);
|
| 107 |
+
} catch (err: any) {
|
| 108 |
+
setStatusMsg(`Reset Failed: ${err.message}`, true);
|
| 109 |
+
}
|
| 110 |
+
};
|
| 111 |
+
|
| 112 |
+
const resetUntilDoors = async () => {
|
| 113 |
+
try {
|
| 114 |
+
setStatusMsg('Searching for layout with doors...');
|
| 115 |
+
for (let i = 1; i <= 8; i++) {
|
| 116 |
+
const payload = { difficulty: 'medium' };
|
| 117 |
+
const data = await apiCall('/reset', payload);
|
| 118 |
+
const doorCount = Object.keys(data?.observation?.map_state?.door_registry || {}).length;
|
| 119 |
+
if (doorCount > 0) {
|
| 120 |
+
const obs: Observation = data.observation;
|
| 121 |
+
obs.metadata = {
|
| 122 |
+
fire_sources: data.observation.fire_sources_count ?? 0,
|
| 123 |
+
fire_spread_rate:data.observation.fire_spread_rate ?? 0,
|
| 124 |
+
humidity: data.observation.humidity ?? 0,
|
| 125 |
+
difficulty: 'medium',
|
| 126 |
+
};
|
| 127 |
+
applyObservation(obs);
|
| 128 |
+
updateReport('reset', payload, data);
|
| 129 |
+
setStatusMsg(`System Ready — ${doorCount} door(s) detected.`);
|
| 130 |
+
pushLog(`Layout found (${doorCount} doors). Doors detected.`, obs.map_state.step_count, 0);
|
| 131 |
+
return;
|
| 132 |
+
}
|
| 133 |
+
}
|
| 134 |
+
setStatusMsg('Optimal layout not found after 8 attempts.', true);
|
| 135 |
+
} catch (err: any) {
|
| 136 |
+
setStatusMsg(`Search Failed: ${err.message}`, true);
|
| 137 |
+
}
|
| 138 |
+
};
|
| 139 |
+
|
| 140 |
+
const runAction = async (actionObj: unknown, label: string) => {
|
| 141 |
+
try {
|
| 142 |
+
setStatusMsg(`Action: ${label}`);
|
| 143 |
+
const payload = actionObj;
|
| 144 |
+
const data = await apiCall('/step', payload);
|
| 145 |
+
const obs: Observation = data.observation;
|
| 146 |
+
obs.metadata = observation?.metadata;
|
| 147 |
+
applyObservation(obs);
|
| 148 |
+
updateReport('step', payload, data);
|
| 149 |
+
const rwd = Number(data.reward || 0);
|
| 150 |
+
setStatusMsg(`Executed. Reward: ${rwd.toFixed(2)}`);
|
| 151 |
+
pushLog(
|
| 152 |
+
obs.last_action_feedback || label,
|
| 153 |
+
obs.map_state.step_count,
|
| 154 |
+
rwd,
|
| 155 |
+
rwd < -0.5,
|
| 156 |
+
);
|
| 157 |
+
if (data.done) setIsAutoWait(false);
|
| 158 |
+
} catch (err: any) {
|
| 159 |
+
setStatusMsg(`Error: ${err.message}`, true);
|
| 160 |
+
}
|
| 161 |
+
};
|
| 162 |
+
|
| 163 |
+
const fetchAndApplyScene = useCallback(async () => {
|
| 164 |
+
try {
|
| 165 |
+
const res = await fetch('/scene');
|
| 166 |
+
if (!res.ok) throw new Error(`${res.status} ${res.statusText}`);
|
| 167 |
+
const scene: SceneResponse = await res.json();
|
| 168 |
+
setSceneData(scene);
|
| 169 |
+
updateReport('scene', {}, scene);
|
| 170 |
+
|
| 171 |
+
const { labels, graph } = scene;
|
| 172 |
+
const cell_grid: number[] = [];
|
| 173 |
+
const fire_grid: number[] = [];
|
| 174 |
+
const smoke_grid: number[] = [];
|
| 175 |
+
|
| 176 |
+
for (let y = 0; y < graph.height; y++) {
|
| 177 |
+
for (let x = 0; x < graph.width; x++) {
|
| 178 |
+
const [type, fire, smoke] = graph.grid[y][x];
|
| 179 |
+
cell_grid.push(type);
|
| 180 |
+
fire_grid.push(fire);
|
| 181 |
+
smoke_grid.push(smoke);
|
| 182 |
+
}
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
const visible_cells: [number, number][] = [];
|
| 186 |
+
for (let y = 0; y < graph.height; y++) {
|
| 187 |
+
for (let x = 0; x < graph.width; x++) {
|
| 188 |
+
if (graph.grid[y][x][4] === 1.0) visible_cells.push([x, y]);
|
| 189 |
+
}
|
| 190 |
+
}
|
| 191 |
+
|
| 192 |
+
const pseudoObs: Observation = {
|
| 193 |
+
map_state: {
|
| 194 |
+
cell_grid, fire_grid, smoke_grid,
|
| 195 |
+
agent_x: labels.agent.x,
|
| 196 |
+
agent_y: labels.agent.y,
|
| 197 |
+
visible_cells,
|
| 198 |
+
door_registry: labels.map.door_registry,
|
| 199 |
+
exit_positions: labels.map.exit_positions,
|
| 200 |
+
step_count: labels.episode.step,
|
| 201 |
+
max_steps: labels.episode.max_steps,
|
| 202 |
+
grid_w: graph.width,
|
| 203 |
+
grid_h: graph.height,
|
| 204 |
+
template_name: labels.episode.template,
|
| 205 |
+
},
|
| 206 |
+
agent_health: labels.agent.health,
|
| 207 |
+
location_label: labels.agent.location,
|
| 208 |
+
smoke_level: labels.agent.smoke_level,
|
| 209 |
+
wind_dir: labels.episode.wind_dir,
|
| 210 |
+
fire_visible: labels.agent.fire_visible,
|
| 211 |
+
fire_direction: labels.agent.fire_direction,
|
| 212 |
+
last_action_feedback: labels.agent.last_action_feedback,
|
| 213 |
+
narrative: '',
|
| 214 |
+
metadata: {
|
| 215 |
+
fire_sources: labels.episode.fire_sources,
|
| 216 |
+
fire_spread_rate:labels.episode.fire_spread_rate,
|
| 217 |
+
humidity: labels.episode.humidity,
|
| 218 |
+
difficulty: labels.episode.difficulty,
|
| 219 |
+
},
|
| 220 |
+
};
|
| 221 |
+
applyObservation(pseudoObs);
|
| 222 |
+
} catch (err: any) {
|
| 223 |
+
setStatusMsg(`Sync Error: ${err.message}`, true);
|
| 224 |
+
}
|
| 225 |
+
}, [applyObservation, observation?.metadata]);
|
| 226 |
+
|
| 227 |
+
useEffect(() => {
|
| 228 |
+
if (isAutoWait) {
|
| 229 |
+
autoWaitTimer.current = window.setInterval(() => runAction({ action: 'wait' }, 'AUTO WAIT'), 900);
|
| 230 |
+
} else {
|
| 231 |
+
if (autoWaitTimer.current) clearInterval(autoWaitTimer.current);
|
| 232 |
+
}
|
| 233 |
+
return () => { if (autoWaitTimer.current) clearInterval(autoWaitTimer.current); };
|
| 234 |
+
}, [isAutoWait]);
|
| 235 |
+
|
| 236 |
+
useEffect(() => {
|
| 237 |
+
if (isPolling) {
|
| 238 |
+
fetchAndApplyScene();
|
| 239 |
+
pollTimer.current = window.setInterval(fetchAndApplyScene, 500);
|
| 240 |
+
} else {
|
| 241 |
+
if (pollTimer.current) clearInterval(pollTimer.current);
|
| 242 |
+
}
|
| 243 |
+
return () => { if (pollTimer.current) clearInterval(pollTimer.current); };
|
| 244 |
+
}, [isPolling, fetchAndApplyScene]);
|
| 245 |
+
|
| 246 |
+
useEffect(() => {
|
| 247 |
+
if (agentMoveFlash > 0) {
|
| 248 |
+
const timer = setTimeout(() => setAgentMoveFlash(f => f - 1), 50);
|
| 249 |
+
return () => clearTimeout(timer);
|
| 250 |
+
}
|
| 251 |
+
}, [agentMoveFlash]);
|
| 252 |
+
|
| 253 |
+
const setup = async () => {
|
| 254 |
+
setIsPolling(false);
|
| 255 |
+
setAgentMoveCount(0);
|
| 256 |
+
setAgentMoveFlash(0);
|
| 257 |
+
setEventLog([]);
|
| 258 |
+
prevAgentPos.current = { x: -1, y: -1 };
|
| 259 |
+
await resetLive();
|
| 260 |
+
setIsPolling(true);
|
| 261 |
+
};
|
| 262 |
+
|
| 263 |
+
/* Derived state */
|
| 264 |
+
const doors: Door[] = Object.entries(observation?.map_state.door_registry || {})
|
| 265 |
+
.map(([id, [x, y]]) => {
|
| 266 |
+
const ct = observation?.map_state.cell_grid[y * (observation?.map_state.grid_w ?? 16) + x];
|
| 267 |
+
let state: 'open' | 'closed' | 'failed' = 'open';
|
| 268 |
+
if (ct === DOOR_CLOSED) state = 'closed';
|
| 269 |
+
if (ct === OBSTACLE) state = 'failed';
|
| 270 |
+
return { id, x, y, state };
|
| 271 |
+
})
|
| 272 |
+
.sort((a, b) => a.id.localeCompare(b.id, undefined, { numeric: true }));
|
| 273 |
+
|
| 274 |
+
const fireCells = observation?.map_state.fire_grid.filter(v => v > 0.05).length ?? 0;
|
| 275 |
+
const exploredPct = observation
|
| 276 |
+
? Math.round((new Set(observation.map_state.visible_cells.map(([vx, vy]) => `${vx},${vy}`)).size
|
| 277 |
+
/ observation.map_state.cell_grid.length) * 100)
|
| 278 |
+
: 0;
|
| 279 |
+
|
| 280 |
+
const hp = Math.round(observation?.agent_health ?? 0);
|
| 281 |
+
const hpColor = hp >= 60 ? 'var(--green)' : hp >= 30 ? 'var(--amber)' : 'var(--red)';
|
| 282 |
+
const isOnline = isPolling && !isError;
|
| 283 |
+
const epId = sceneData?.labels.episode.id?.slice(0, 8) ?? '—';
|
| 284 |
+
|
| 285 |
+
return (
|
| 286 |
+
<div className="shell">
|
| 287 |
+
{/* ── Topbar ── */}
|
| 288 |
+
<header className="topbar">
|
| 289 |
+
<div className="brand">
|
| 290 |
+
<div className="brand-icon">🔥</div>
|
| 291 |
+
<span className="brand-name">Pyre</span>
|
| 292 |
+
<span className="brand-sep">/</span>
|
| 293 |
+
<span className="brand-sub">Crisis Navigation</span>
|
| 294 |
+
</div>
|
| 295 |
+
|
| 296 |
+
<div className="topbar-right">
|
| 297 |
+
<div className="topbar-ep">
|
| 298 |
+
Episode <span className="ep-id">{epId}</span>
|
| 299 |
+
</div>
|
| 300 |
+
<span className={`live-chip ${isError ? 'error' : isOnline ? 'online' : 'offline'}`}>
|
| 301 |
+
{isError ? 'Error' : isOnline ? 'Live' : 'Idle'}
|
| 302 |
+
</span>
|
| 303 |
+
</div>
|
| 304 |
+
</header>
|
| 305 |
+
|
| 306 |
+
{/* ── Body ── */}
|
| 307 |
+
<div className="content">
|
| 308 |
+
|
| 309 |
+
{/* ── Left: Canvas Zone ── */}
|
| 310 |
+
<div className="canvas-zone">
|
| 311 |
+
<div className="canvas-frame">
|
| 312 |
+
<Map2D observation={observation} agentMoveFlash={agentMoveFlash} />
|
| 313 |
+
<HUD observation={observation} agentMoveCount={agentMoveCount} />
|
| 314 |
+
</div>
|
| 315 |
+
|
| 316 |
+
{/* Legend */}
|
| 317 |
+
<div className="legend">
|
| 318 |
+
{[
|
| 319 |
+
{ color:'#5e5850', label:'Wall' },
|
| 320 |
+
{ color:'#3a3530', label:'Obstacle' },
|
| 321 |
+
{ color:'#e6f4ec', label:'Exit' },
|
| 322 |
+
{ color:'#7c5c3c', label:'Door' },
|
| 323 |
+
{ color:'#f97316', label:'Fire' },
|
| 324 |
+
{ color:'rgba(72,82,96,0.7)', label:'Smoke' },
|
| 325 |
+
{ color:'#3b82f6', label:'Agent' },
|
| 326 |
+
{ color:'rgba(2,132,199,0.6)', label:'Trail'},
|
| 327 |
+
].map(({ color, label }) => (
|
| 328 |
+
<div key={label} className="legend-item">
|
| 329 |
+
<div className="leg-swatch" style={{ background: color, border:'1px solid rgba(0,0,0,0.1)' }} />
|
| 330 |
+
{label}
|
| 331 |
+
</div>
|
| 332 |
+
))}
|
| 333 |
+
</div>
|
| 334 |
+
|
| 335 |
+
{/* Field Report */}
|
| 336 |
+
<div className="dialog">
|
| 337 |
+
<span className="dialog-who">Field Report</span>
|
| 338 |
+
{observation?.last_action_feedback || 'Establishing link to field systems...'}
|
| 339 |
+
</div>
|
| 340 |
+
</div>
|
| 341 |
+
|
| 342 |
+
{/* ── Right: Side Panel ── */}
|
| 343 |
+
<aside className="side">
|
| 344 |
+
|
| 345 |
+
{/* Controls */}
|
| 346 |
+
<ControlPanel
|
| 347 |
+
onAction={runAction}
|
| 348 |
+
onReset={resetLive}
|
| 349 |
+
onResetDoors={resetUntilDoors}
|
| 350 |
+
onSetup={setup}
|
| 351 |
+
doors={doors}
|
| 352 |
+
isAutoWait={isAutoWait}
|
| 353 |
+
toggleAutoWait={() => setIsAutoWait(!isAutoWait)}
|
| 354 |
+
isPolling={isPolling}
|
| 355 |
+
togglePolling={() => setIsPolling(!isPolling)}
|
| 356 |
+
status={status}
|
| 357 |
+
isError={isError}
|
| 358 |
+
/>
|
| 359 |
+
|
| 360 |
+
{/* Agent Biometrics */}
|
| 361 |
+
<div className="side-sec">
|
| 362 |
+
<div className="sec-hd">Agent Biometrics</div>
|
| 363 |
+
<div className="sg">
|
| 364 |
+
<div className="sc">
|
| 365 |
+
<div className="sc-l">Health</div>
|
| 366 |
+
<div className="sc-v" style={{ color: hpColor }}>{hp}%</div>
|
| 367 |
+
</div>
|
| 368 |
+
<div className="sc">
|
| 369 |
+
<div className="sc-l">Status</div>
|
| 370 |
+
<div className="sc-v" style={{ color: hpColor }}>
|
| 371 |
+
{sceneData?.labels.agent.health_status ?? 'NOMINAL'}
|
| 372 |
+
</div>
|
| 373 |
+
</div>
|
| 374 |
+
<div className="sc">
|
| 375 |
+
<div className="sc-l">Position</div>
|
| 376 |
+
<div className="sc-v blue">
|
| 377 |
+
({observation?.map_state.agent_x ?? '—'},{observation?.map_state.agent_y ?? '—'})
|
| 378 |
+
</div>
|
| 379 |
+
</div>
|
| 380 |
+
<div className="sc">
|
| 381 |
+
<div className="sc-l">Sector</div>
|
| 382 |
+
<div className="sc-v">{observation?.location_label ?? 'Unknown'}</div>
|
| 383 |
+
</div>
|
| 384 |
+
</div>
|
| 385 |
+
<div className="bar-w">
|
| 386 |
+
<div className="bar-lbl">
|
| 387 |
+
<span>System Integrity</span>
|
| 388 |
+
<span>{hp}%</span>
|
| 389 |
+
</div>
|
| 390 |
+
<div className="bar-bg">
|
| 391 |
+
<div className="bar-fill" style={{ width: `${hp}%`, background: hpColor }} />
|
| 392 |
+
</div>
|
| 393 |
+
</div>
|
| 394 |
+
</div>
|
| 395 |
+
|
| 396 |
+
{/* Environment */}
|
| 397 |
+
<div className="side-sec">
|
| 398 |
+
<div className="sec-hd">Environment</div>
|
| 399 |
+
<div className="sg">
|
| 400 |
+
<div className="sc">
|
| 401 |
+
<div className="sc-l">Hazard Cells</div>
|
| 402 |
+
<div className="sc-v fire">{fireCells}</div>
|
| 403 |
+
</div>
|
| 404 |
+
<div className="sc">
|
| 405 |
+
<div className="sc-l">Explored</div>
|
| 406 |
+
<div className="sc-v blue">{exploredPct}%</div>
|
| 407 |
+
</div>
|
| 408 |
+
<div className="sc">
|
| 409 |
+
<div className="sc-l">Wind</div>
|
| 410 |
+
<div className="sc-v">{observation?.wind_dir ?? 'CALM'}</div>
|
| 411 |
+
</div>
|
| 412 |
+
<div className="sc">
|
| 413 |
+
<div className="sc-l">Humidity</div>
|
| 414 |
+
<div className="sc-v amber">
|
| 415 |
+
{Math.round((observation?.metadata?.humidity ?? 0) * 100)}%
|
| 416 |
+
</div>
|
| 417 |
+
</div>
|
| 418 |
+
</div>
|
| 419 |
+
</div>
|
| 420 |
+
|
| 421 |
+
{/* Event Log */}
|
| 422 |
+
<div className="side-sec" style={{ flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
|
| 423 |
+
<div className="sec-hd">
|
| 424 |
+
Event Log
|
| 425 |
+
<span style={{ fontFamily: 'var(--mono)', fontSize: '9px', color: 'var(--t3)' }}>
|
| 426 |
+
{eventLog.length} events
|
| 427 |
+
</span>
|
| 428 |
+
</div>
|
| 429 |
+
<div className="elog">
|
| 430 |
+
{eventLog.length === 0 && (
|
| 431 |
+
<div style={{ color: 'var(--t3)', fontFamily: 'var(--mono)', fontSize: '10px', padding: '4px' }}>
|
| 432 |
+
No events yet…
|
| 433 |
+
</div>
|
| 434 |
+
)}
|
| 435 |
+
{eventLog.map((e, i) => (
|
| 436 |
+
<div key={i} className={`erow ${e.isAlarm ? 'alarm' : ''}`}>
|
| 437 |
+
<span className="estep">T{e.step}</span>
|
| 438 |
+
<span className="etext">{e.text}</span>
|
| 439 |
+
<span className={`erwd ${e.reward >= 0 ? 'p' : 'n'}`}>
|
| 440 |
+
{e.reward >= 0 ? '+' : ''}{e.reward.toFixed(2)}
|
| 441 |
+
</span>
|
| 442 |
+
</div>
|
| 443 |
+
))}
|
| 444 |
+
<div ref={logEndRef} />
|
| 445 |
+
</div>
|
| 446 |
+
</div>
|
| 447 |
+
|
| 448 |
+
{/* API Report */}
|
| 449 |
+
<div className="side-sec">
|
| 450 |
+
<div className="sec-hd">Network Activity</div>
|
| 451 |
+
<APIReport report={apiReport} onCopyReset={() => {}} onCopyStep={() => {}} onCopyScene={() => {}} />
|
| 452 |
+
</div>
|
| 453 |
+
|
| 454 |
+
</aside>
|
| 455 |
+
</div>
|
| 456 |
+
</div>
|
| 457 |
+
);
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
export default App;
|
frontend/src/assets/hero.png
ADDED
|
frontend/src/assets/react.svg
ADDED
|
|
frontend/src/assets/vite.svg
ADDED
|
|
frontend/src/components/APIReport.tsx
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import type { ApiReport } from '../types';
|
| 3 |
+
|
| 4 |
+
interface APIReportProps {
|
| 5 |
+
report: ApiReport | null;
|
| 6 |
+
onCopyReset: () => void;
|
| 7 |
+
onCopyStep: () => void;
|
| 8 |
+
onCopyScene: () => void;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
const APIReport: React.FC<APIReportProps> = ({ report, onCopyReset, onCopyStep, onCopyScene }) => {
|
| 12 |
+
return (
|
| 13 |
+
<div>
|
| 14 |
+
<div className="report-meta" style={{ marginBottom: '8px' }}>
|
| 15 |
+
{report ? report.meta : 'Awaiting telemetry...'}
|
| 16 |
+
</div>
|
| 17 |
+
<div className="report-box">
|
| 18 |
+
{report ? JSON.stringify({
|
| 19 |
+
call_type: report.call_type,
|
| 20 |
+
request: report.request,
|
| 21 |
+
response: report.response
|
| 22 |
+
}, null, 2) : '{}'}
|
| 23 |
+
</div>
|
| 24 |
+
<div className="ctrl-grid" style={{ marginTop: '12px' }}>
|
| 25 |
+
<button className="ctrl-btn" onClick={onCopyScene}>Scene</button>
|
| 26 |
+
<button className="ctrl-btn" onClick={onCopyReset}>Reset</button>
|
| 27 |
+
<button className="ctrl-btn" onClick={onCopyStep}>Step</button>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
);
|
| 31 |
+
};
|
| 32 |
+
|
| 33 |
+
export default APIReport;
|
frontend/src/components/ControlPanel.tsx
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
import type { Door } from '../types';
|
| 3 |
+
|
| 4 |
+
interface ControlPanelProps {
|
| 5 |
+
onAction: (action: unknown, label: string) => void;
|
| 6 |
+
onReset: (difficulty: string) => void;
|
| 7 |
+
onResetDoors: () => void;
|
| 8 |
+
onSetup: () => void;
|
| 9 |
+
doors: Door[];
|
| 10 |
+
isAutoWait: boolean;
|
| 11 |
+
toggleAutoWait: () => void;
|
| 12 |
+
isPolling: boolean;
|
| 13 |
+
togglePolling: () => void;
|
| 14 |
+
status: string;
|
| 15 |
+
isError: boolean;
|
| 16 |
+
}
|
| 17 |
+
|
| 18 |
+
const ControlPanel: React.FC<ControlPanelProps> = ({
|
| 19 |
+
onAction, onSetup, doors,
|
| 20 |
+
isAutoWait, toggleAutoWait,
|
| 21 |
+
isPolling, togglePolling,
|
| 22 |
+
status, isError,
|
| 23 |
+
}) => {
|
| 24 |
+
const mv = (dir: string) => onAction({ action: 'move', direction: dir.toLowerCase() }, `MOVE ${dir.toUpperCase()}`);
|
| 25 |
+
|
| 26 |
+
return (
|
| 27 |
+
<div className="side-sec">
|
| 28 |
+
<div className="sec-hd">Tactical Controls</div>
|
| 29 |
+
|
| 30 |
+
{/* D-Pad */}
|
| 31 |
+
<div className="ctrl-grid" style={{ maxWidth: 160, margin: '0 auto', marginBottom: 8 }}>
|
| 32 |
+
{/* Row 1 */}
|
| 33 |
+
<div />
|
| 34 |
+
<button className="ctrl-btn" onClick={() => mv('north')}>▲</button>
|
| 35 |
+
<div />
|
| 36 |
+
{/* Row 2 */}
|
| 37 |
+
<button className="ctrl-btn" onClick={() => mv('west')}>◀</button>
|
| 38 |
+
<button className="ctrl-btn accent" onClick={() => onAction({ action: 'wait' }, 'WAIT')} title="Wait in place">●</button>
|
| 39 |
+
<button className="ctrl-btn" onClick={() => mv('east')}>▶</button>
|
| 40 |
+
{/* Row 3 */}
|
| 41 |
+
<div />
|
| 42 |
+
<button className="ctrl-btn" onClick={() => mv('south')}>▼</button>
|
| 43 |
+
<div />
|
| 44 |
+
</div>
|
| 45 |
+
|
| 46 |
+
{/* Secondary actions */}
|
| 47 |
+
<div className="ctrl-row">
|
| 48 |
+
<button className="ctrl-btn" onClick={() => onAction({ action: 'look' }, 'LOOK')}>SCAN</button>
|
| 49 |
+
<button className={`ctrl-btn ${isAutoWait ? 'active' : ''}`} onClick={toggleAutoWait}>
|
| 50 |
+
{isAutoWait ? 'STOP AUTO' : 'AUTO'}
|
| 51 |
+
</button>
|
| 52 |
+
<button className={`ctrl-btn ${isPolling ? 'active' : ''}`} onClick={togglePolling}>
|
| 53 |
+
{isPolling ? 'LIVE ●' : 'LIVE ○'}
|
| 54 |
+
</button>
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
{/* Reboot */}
|
| 58 |
+
<div className="ctrl-row" style={{ marginTop: 6 }}>
|
| 59 |
+
<button className="ctrl-btn play" onClick={onSetup} style={{ flex: 'none', width: '100%' }}>
|
| 60 |
+
↺ REBOOT EPISODE
|
| 61 |
+
</button>
|
| 62 |
+
</div>
|
| 63 |
+
|
| 64 |
+
{/* Status line */}
|
| 65 |
+
<div className={`ctrl-status ${isError ? 'error' : ''}`}>
|
| 66 |
+
{status}
|
| 67 |
+
</div>
|
| 68 |
+
|
| 69 |
+
{/* Doors */}
|
| 70 |
+
{doors.length > 0 && (
|
| 71 |
+
<div style={{ marginTop: 12 }}>
|
| 72 |
+
<div className="sec-hd" style={{ marginBottom: 6 }}>Proximity Doors</div>
|
| 73 |
+
<div className="door-grid">
|
| 74 |
+
{doors.map(d => (
|
| 75 |
+
<button
|
| 76 |
+
key={d.id}
|
| 77 |
+
className={`door-btn ${d.state}`}
|
| 78 |
+
onClick={() => onAction({ action: 'door', target_id: d.id, door_state: d.state === 'closed' ? 'open' : 'close' }, `DOOR ${d.id}`)}
|
| 79 |
+
>
|
| 80 |
+
{d.id} [{d.state}]
|
| 81 |
+
</button>
|
| 82 |
+
))}
|
| 83 |
+
</div>
|
| 84 |
+
</div>
|
| 85 |
+
)}
|
| 86 |
+
</div>
|
| 87 |
+
);
|
| 88 |
+
};
|
| 89 |
+
|
| 90 |
+
export default ControlPanel;
|
frontend/src/components/HUD.tsx
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useEffect, useRef, useState } from 'react';
|
| 2 |
+
import type { Observation } from '../types';
|
| 3 |
+
|
| 4 |
+
interface HUDProps {
|
| 5 |
+
observation: Observation | null;
|
| 6 |
+
agentMoveCount?: number;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
/* Unicode compass arrows for each wind direction */
|
| 10 |
+
const WIND_ARROW: Record<string, string> = {
|
| 11 |
+
N: '↑', S: '↓', E: '→', W: '←',
|
| 12 |
+
NE: '↗', NW: '↖', SE: '↘', SW: '↙',
|
| 13 |
+
CALM: '·',
|
| 14 |
+
};
|
| 15 |
+
|
| 16 |
+
/* Rotation degrees so the arrow visually points in the right direction */
|
| 17 |
+
const WIND_DEG: Record<string, number> = {
|
| 18 |
+
N: 0, NE: 45, E: 90, SE: 135,
|
| 19 |
+
S: 180, SW: 225, W: 270, NW: 315, CALM: 0,
|
| 20 |
+
};
|
| 21 |
+
|
| 22 |
+
const HUD: React.FC<HUDProps> = ({ observation, agentMoveCount = 0 }) => {
|
| 23 |
+
const [firePulse, setFirePulse] = useState(0);
|
| 24 |
+
const pulseRef = useRef(0);
|
| 25 |
+
|
| 26 |
+
useEffect(() => {
|
| 27 |
+
let raf: number;
|
| 28 |
+
const tick = () => {
|
| 29 |
+
pulseRef.current += 0.05;
|
| 30 |
+
setFirePulse(Math.sin(pulseRef.current * 4) * 0.5 + 0.5);
|
| 31 |
+
raf = requestAnimationFrame(tick);
|
| 32 |
+
};
|
| 33 |
+
raf = requestAnimationFrame(tick);
|
| 34 |
+
return () => cancelAnimationFrame(raf);
|
| 35 |
+
}, []);
|
| 36 |
+
|
| 37 |
+
if (!observation) return null;
|
| 38 |
+
|
| 39 |
+
const { map_state, agent_health, smoke_level, wind_dir, fire_visible, metadata } = observation;
|
| 40 |
+
const hPct = Math.round(agent_health);
|
| 41 |
+
const sPct = Math.round((map_state.step_count / map_state.max_steps) * 100);
|
| 42 |
+
const totalFireCells = map_state.fire_grid.filter(v => v > 0.05).length;
|
| 43 |
+
|
| 44 |
+
let hBarClass = 'g';
|
| 45 |
+
let hStatusLabel = 'Nominal';
|
| 46 |
+
let hStatusClass = 'good';
|
| 47 |
+
|
| 48 |
+
if (hPct < 30) { hBarClass = 'c'; hStatusLabel = 'Critical'; hStatusClass = 'critical'; }
|
| 49 |
+
else if (hPct < 60) { hBarClass = 'm'; hStatusLabel = 'Moderate'; hStatusClass = 'moderate'; }
|
| 50 |
+
|
| 51 |
+
const windDir = wind_dir || 'CALM';
|
| 52 |
+
const windArrow = WIND_ARROW[windDir] ?? '?';
|
| 53 |
+
const windDeg = WIND_DEG[windDir] ?? 0;
|
| 54 |
+
const spreadRate = metadata?.fire_spread_rate ?? 0;
|
| 55 |
+
const humidity = metadata?.humidity ?? 0;
|
| 56 |
+
|
| 57 |
+
return (
|
| 58 |
+
<div className="hud-overlay">
|
| 59 |
+
{/* ── Left: Health ── */}
|
| 60 |
+
<div className="hud-card">
|
| 61 |
+
<div className="hud-r">
|
| 62 |
+
<span className="hlbl">HP</span>
|
| 63 |
+
<div className="hbar-bg">
|
| 64 |
+
<div className={`hbar-fill ${hBarClass}`} style={{ width: `${hPct}%` }} />
|
| 65 |
+
</div>
|
| 66 |
+
<span className="hval">{hPct}</span>
|
| 67 |
+
</div>
|
| 68 |
+
<div className="hud-r" style={{ gap: '8px', marginTop: '2px' }}>
|
| 69 |
+
<span className={`hstatus ${hStatusClass}`}>{hStatusLabel}</span>
|
| 70 |
+
<span style={{ fontFamily: 'var(--mono)', fontSize: '9px', color: 'rgba(168,162,158,.55)', marginLeft: 'auto' }}>
|
| 71 |
+
💨 {smoke_level || 'clear'}
|
| 72 |
+
</span>
|
| 73 |
+
</div>
|
| 74 |
+
{agentMoveCount > 0 && (
|
| 75 |
+
<div style={{ fontFamily: 'var(--mono)', fontSize: '8px', color: 'rgba(168,162,158,.4)', marginTop: '2px' }}>
|
| 76 |
+
moves: {agentMoveCount}
|
| 77 |
+
</div>
|
| 78 |
+
)}
|
| 79 |
+
</div>
|
| 80 |
+
|
| 81 |
+
{/* ── Center: Wind & Hazard ── */}
|
| 82 |
+
<div className="hud-card hud-card-center">
|
| 83 |
+
{/* Compass rose */}
|
| 84 |
+
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
| 85 |
+
<div style={{ position: 'relative', width: 32, height: 32, flexShrink: 0 }}>
|
| 86 |
+
{/* compass ring */}
|
| 87 |
+
<svg width="32" height="32" viewBox="0 0 32 32" style={{ position: 'absolute', top: 0, left: 0 }}>
|
| 88 |
+
<circle cx="16" cy="16" r="14" fill="none" stroke="rgba(255,255,255,0.12)" strokeWidth="1.5" />
|
| 89 |
+
<text x="16" y="6" textAnchor="middle" fontSize="5" fill="rgba(255,255,255,0.35)" dominantBaseline="middle">N</text>
|
| 90 |
+
<text x="16" y="28" textAnchor="middle" fontSize="5" fill="rgba(255,255,255,0.35)" dominantBaseline="middle">S</text>
|
| 91 |
+
<text x="4" y="17" textAnchor="middle" fontSize="5" fill="rgba(255,255,255,0.35)" dominantBaseline="middle">W</text>
|
| 92 |
+
<text x="28" y="17" textAnchor="middle" fontSize="5" fill="rgba(255,255,255,0.35)" dominantBaseline="middle">E</text>
|
| 93 |
+
</svg>
|
| 94 |
+
{/* direction arrow */}
|
| 95 |
+
<div style={{
|
| 96 |
+
position: 'absolute', top: '50%', left: '50%',
|
| 97 |
+
transform: `translate(-50%,-50%) rotate(${windDeg}deg)`,
|
| 98 |
+
fontSize: windDir === 'CALM' ? '10px' : '14px',
|
| 99 |
+
color: windDir === 'CALM' ? 'rgba(168,162,158,0.6)' : '#fbbf24',
|
| 100 |
+
lineHeight: 1,
|
| 101 |
+
transition: 'transform 0.6s ease',
|
| 102 |
+
}}>
|
| 103 |
+
{windArrow}
|
| 104 |
+
</div>
|
| 105 |
+
</div>
|
| 106 |
+
|
| 107 |
+
<div>
|
| 108 |
+
<div className="hlbl">Wind</div>
|
| 109 |
+
<div style={{ fontFamily: 'var(--mono)', fontSize: '13px', fontWeight: 500, color: '#f0e8e0', lineHeight: 1.2 }}>
|
| 110 |
+
{windDir}
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
</div>
|
| 114 |
+
|
| 115 |
+
<div style={{ display: 'flex', gap: '10px', marginTop: '4px' }}>
|
| 116 |
+
<div>
|
| 117 |
+
<div className="hlbl">Spread</div>
|
| 118 |
+
<div style={{ fontFamily: 'var(--mono)', fontSize: '11px', color: spreadRate > 0.5 ? '#f87171' : '#fbbf24' }}>
|
| 119 |
+
{(spreadRate * 100).toFixed(0)}%
|
| 120 |
+
</div>
|
| 121 |
+
</div>
|
| 122 |
+
<div>
|
| 123 |
+
<div className="hlbl">Humidity</div>
|
| 124 |
+
<div style={{ fontFamily: 'var(--mono)', fontSize: '11px', color: humidity > 0.6 ? '#60a5fa' : '#a8a29e' }}>
|
| 125 |
+
{(humidity * 100).toFixed(0)}%
|
| 126 |
+
</div>
|
| 127 |
+
</div>
|
| 128 |
+
{totalFireCells > 0 && (
|
| 129 |
+
<div style={{ alignSelf: 'center' }}>
|
| 130 |
+
<span style={{
|
| 131 |
+
fontFamily: 'var(--mono)', fontSize: '9px', fontWeight: 700,
|
| 132 |
+
color: fire_visible ? '#fff' : '#fbbf24',
|
| 133 |
+
background: fire_visible
|
| 134 |
+
? `rgba(239,${Math.floor(30 + firePulse * 40)},0,${0.75 + firePulse * 0.25})`
|
| 135 |
+
: `rgba(180,60,0,${0.55 + firePulse * 0.3})`,
|
| 136 |
+
border: fire_visible
|
| 137 |
+
? `1px solid rgba(255,${Math.floor(60 + firePulse * 80)},0,0.8)`
|
| 138 |
+
: '1px solid rgba(251,191,36,0.5)',
|
| 139 |
+
padding: '2px 6px', borderRadius: '3px', letterSpacing: '0.06em',
|
| 140 |
+
boxShadow: fire_visible
|
| 141 |
+
? `0 0 ${6 + firePulse * 8}px rgba(255,80,0,0.7)`
|
| 142 |
+
: `0 0 ${3 + firePulse * 4}px rgba(200,80,0,0.4)`,
|
| 143 |
+
display: 'flex', alignItems: 'center', gap: '3px',
|
| 144 |
+
transition: 'box-shadow 0.1s',
|
| 145 |
+
}}>
|
| 146 |
+
🔥 {fire_visible ? 'IN RANGE' : 'ACTIVE'} · {totalFireCells}
|
| 147 |
+
</span>
|
| 148 |
+
</div>
|
| 149 |
+
)}
|
| 150 |
+
</div>
|
| 151 |
+
</div>
|
| 152 |
+
|
| 153 |
+
{/* ── Right: Steps ── */}
|
| 154 |
+
<div className="hud-card" style={{ textAlign: 'right', minWidth: 'auto' }}>
|
| 155 |
+
<div className="hud-r" style={{ justifyContent: 'flex-end' }}>
|
| 156 |
+
<span className="step-val">{map_state.step_count} / {map_state.max_steps}</span>
|
| 157 |
+
</div>
|
| 158 |
+
<div className="sbar-bg">
|
| 159 |
+
<div className="sbar-fill" style={{ width: `${sPct}%` }} />
|
| 160 |
+
</div>
|
| 161 |
+
<div className="step-meta">
|
| 162 |
+
{metadata?.difficulty ?? 'medium'} · {map_state.template_name}
|
| 163 |
+
</div>
|
| 164 |
+
</div>
|
| 165 |
+
</div>
|
| 166 |
+
);
|
| 167 |
+
};
|
| 168 |
+
|
| 169 |
+
export default HUD;
|
frontend/src/components/Map2D.tsx
ADDED
|
@@ -0,0 +1,600 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React, { useRef, useEffect } from 'react';
|
| 2 |
+
import type { Observation } from '../types';
|
| 3 |
+
|
| 4 |
+
interface Map2DProps {
|
| 5 |
+
observation: Observation | null;
|
| 6 |
+
agentMoveFlash: number;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
/* ── Cell type constants ── */
|
| 10 |
+
const WALL = 1;
|
| 11 |
+
const DOOR_OPEN = 2;
|
| 12 |
+
const DOOR_CLOSED = 3;
|
| 13 |
+
const EXIT = 4;
|
| 14 |
+
const OBSTACLE = 5;
|
| 15 |
+
|
| 16 |
+
/* ── Wind direction vectors ── */
|
| 17 |
+
const WIND_DIRS: Record<string, [number, number]> = {
|
| 18 |
+
N: [0, -1], S: [0, 1], E: [1, 0], W: [-1, 0],
|
| 19 |
+
NW: [-0.7, -0.7], NE: [0.7, -0.7], SW: [-0.7, 0.7], SE: [0.7, 0.7],
|
| 20 |
+
CALM: [0, 0],
|
| 21 |
+
};
|
| 22 |
+
|
| 23 |
+
/* ── Agent appearance per health tier ── */
|
| 24 |
+
const AGENT_THEMES = {
|
| 25 |
+
healthy: { body: '#3b82f6', dark: '#1d4ed8', arm: '#2563eb', ring: '#fbbf24', ringGlow: 'rgba(251,191,36,0.5)' },
|
| 26 |
+
moderate: { body: '#f97316', dark: '#c2410c', arm: '#ea580c', ring: '#fb923c', ringGlow: 'rgba(251,146,60,0.5)' },
|
| 27 |
+
low: { body: '#dc2626', dark: '#991b1b', arm: '#b91c1c', ring: '#f87171', ringGlow: 'rgba(248,113,113,0.5)' },
|
| 28 |
+
critical: { body: '#7c3aed', dark: '#5b21b6', arm: '#6d28d9', ring: '#c4b5fd', ringGlow: 'rgba(196,181,253,0.5)' },
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
/* ── Ember particle ── */
|
| 32 |
+
class Ember {
|
| 33 |
+
x: number; y: number; vx: number; vy: number;
|
| 34 |
+
life: number; decay: number; size: number;
|
| 35 |
+
type: 'ember' | 'spark';
|
| 36 |
+
|
| 37 |
+
constructor(x: number, y: number, windX: number) {
|
| 38 |
+
const speed = 0.4 + Math.random() * 1.0;
|
| 39 |
+
const angle = -Math.PI / 2 + (Math.random() - 0.5) * 1.6;
|
| 40 |
+
this.x = x + (Math.random() - 0.5) * 3;
|
| 41 |
+
this.y = y + (Math.random() - 0.5) * 3;
|
| 42 |
+
this.vx = Math.cos(angle) * speed + windX * 0.7;
|
| 43 |
+
this.vy = Math.sin(angle) * speed - 0.22;
|
| 44 |
+
this.life = 1.0;
|
| 45 |
+
this.decay = 0.012 + Math.random() * 0.015;
|
| 46 |
+
this.size = 1.2 + Math.random() * 2.2;
|
| 47 |
+
this.type = Math.random() > 0.4 ? 'ember' : 'spark';
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
update() {
|
| 51 |
+
this.x += this.vx;
|
| 52 |
+
this.y += this.vy;
|
| 53 |
+
this.vy -= 0.012;
|
| 54 |
+
this.vx *= 0.97;
|
| 55 |
+
this.life -= this.decay;
|
| 56 |
+
}
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
/* ── Minecraft pixel-art character ── */
|
| 61 |
+
function drawMinecraftAgent(
|
| 62 |
+
ctx: CanvasRenderingContext2D,
|
| 63 |
+
cx: number, cy: number, cs: number,
|
| 64 |
+
theme: typeof AGENT_THEMES.healthy
|
| 65 |
+
) {
|
| 66 |
+
const u = cs / 18;
|
| 67 |
+
const left = cx - 5 * u;
|
| 68 |
+
const top = cy - 8.5 * u;
|
| 69 |
+
|
| 70 |
+
const px = (rx: number, ry: number, rw: number, rh: number, color: string) => {
|
| 71 |
+
ctx.fillStyle = color;
|
| 72 |
+
ctx.fillRect(left + rx * u, top + ry * u, rw * u, rh * u);
|
| 73 |
+
};
|
| 74 |
+
|
| 75 |
+
/* Helmet */
|
| 76 |
+
px(2, 0, 6, 1, '#5c4a3d');
|
| 77 |
+
/* Head */
|
| 78 |
+
px(2, 1, 6, 5, '#f5d5a0');
|
| 79 |
+
/* Face features */
|
| 80 |
+
px(3, 3, 1, 1, '#3d2b1a'); /* left eye */
|
| 81 |
+
px(6, 3, 1, 1, '#3d2b1a'); /* right eye */
|
| 82 |
+
px(4, 5, 2, 1, '#c8937a'); /* mouth */
|
| 83 |
+
/* Hair accent */
|
| 84 |
+
px(2, 1, 6, 1, '#7a5c3e');
|
| 85 |
+
|
| 86 |
+
/* Body */
|
| 87 |
+
px(3, 6, 4, 4, theme.body);
|
| 88 |
+
px(3, 6, 4, 1, theme.dark);
|
| 89 |
+
|
| 90 |
+
/* Arms */
|
| 91 |
+
px(1, 6, 2, 4, theme.arm);
|
| 92 |
+
px(7, 6, 2, 4, theme.arm);
|
| 93 |
+
|
| 94 |
+
/* Legs */
|
| 95 |
+
px(3, 10, 2, 4, '#1e40af');
|
| 96 |
+
px(5, 10, 2, 4, '#1e3a8a');
|
| 97 |
+
|
| 98 |
+
/* Boots */
|
| 99 |
+
px(3, 14, 2, 2, '#3a2e26');
|
| 100 |
+
px(5, 14, 2, 2, '#2e2420');
|
| 101 |
+
}
|
| 102 |
+
|
| 103 |
+
/* ── Main canvas component ── */
|
| 104 |
+
const Map2D: React.FC<Map2DProps> = ({ observation, agentMoveFlash }) => {
|
| 105 |
+
const canvasRef = useRef<HTMLCanvasElement>(null);
|
| 106 |
+
const embersRef = useRef<Ember[]>([]);
|
| 107 |
+
const trailRef = useRef<{ x: number; y: number; t: number }[]>([]);
|
| 108 |
+
const timeRef = useRef(0);
|
| 109 |
+
const rafRef = useRef(0);
|
| 110 |
+
|
| 111 |
+
const CS = 40;
|
| 112 |
+
|
| 113 |
+
const animate = () => {
|
| 114 |
+
const canvas = canvasRef.current;
|
| 115 |
+
if (!canvas || !observation) { rafRef.current = requestAnimationFrame(animate); return; }
|
| 116 |
+
const ctx = canvas.getContext('2d');
|
| 117 |
+
if (!ctx) return;
|
| 118 |
+
|
| 119 |
+
timeRef.current += 0.016;
|
| 120 |
+
const t = timeRef.current;
|
| 121 |
+
|
| 122 |
+
const { map_state, agent_health, wind_dir } = observation;
|
| 123 |
+
const { grid_w: W, grid_h: H, cell_grid, fire_grid, smoke_grid, agent_x, agent_y } = map_state;
|
| 124 |
+
const cs = CS;
|
| 125 |
+
const wv = WIND_DIRS[wind_dir] ?? [0, 0];
|
| 126 |
+
const idx = (x: number, y: number) => y * W + x;
|
| 127 |
+
const visible = new Set(map_state.visible_cells.map(([vx, vy]) => `${vx},${vy}`));
|
| 128 |
+
|
| 129 |
+
/* ── Canvas bg ── */
|
| 130 |
+
ctx.fillStyle = '#c8b890';
|
| 131 |
+
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
| 132 |
+
|
| 133 |
+
/* ── Base layer ── */
|
| 134 |
+
for (let y = 0; y < H; y++) {
|
| 135 |
+
for (let x = 0; x < W; x++) {
|
| 136 |
+
const ct = cell_grid[idx(x, y)];
|
| 137 |
+
const px = x * cs, py = y * cs;
|
| 138 |
+
|
| 139 |
+
switch (ct) {
|
| 140 |
+
case WALL: {
|
| 141 |
+
/* Animated heat tint — walls near fire glow ember-red */
|
| 142 |
+
const nearFire = fire_grid[idx(x, y)] > 0.05
|
| 143 |
+
? fire_grid[idx(x, y)]
|
| 144 |
+
: (
|
| 145 |
+
(x > 0 && fire_grid[idx(x-1, y)] > 0.05 ? fire_grid[idx(x-1, y)] : 0) +
|
| 146 |
+
(x < W-1 && fire_grid[idx(x+1, y)] > 0.05 ? fire_grid[idx(x+1, y)] : 0) +
|
| 147 |
+
(y > 0 && fire_grid[idx(x, y-1)] > 0.05 ? fire_grid[idx(x, y-1)] : 0) +
|
| 148 |
+
(y < H-1 && fire_grid[idx(x, y+1)] > 0.05 ? fire_grid[idx(x, y+1)] : 0)
|
| 149 |
+
) * 0.28;
|
| 150 |
+
|
| 151 |
+
const heatShift = Math.min(1, nearFire * 2.2);
|
| 152 |
+
const wallFlicker = 0.88 + 0.12 * Math.sin(t * 7.3 + x * 2.1 + y * 3.7);
|
| 153 |
+
|
| 154 |
+
/* Base stone colour, heat-shifted toward deep orange-red */
|
| 155 |
+
const br = Math.round(94 + heatShift * 100 * wallFlicker);
|
| 156 |
+
const bg = Math.round(88 - heatShift * 52);
|
| 157 |
+
const bb = Math.round(80 - heatShift * 70);
|
| 158 |
+
ctx.fillStyle = `rgb(${br},${bg},${bb})`;
|
| 159 |
+
ctx.fillRect(px, py, cs, cs);
|
| 160 |
+
|
| 161 |
+
/* Brick rows — two horizontal bands */
|
| 162 |
+
const brickH = cs / 2;
|
| 163 |
+
for (let row = 0; row < 2; row++) {
|
| 164 |
+
const by = py + row * brickH;
|
| 165 |
+
/* mortar gap between rows */
|
| 166 |
+
ctx.fillStyle = `rgba(0,0,0,${0.28 + heatShift * 0.12})`;
|
| 167 |
+
ctx.fillRect(px, by + brickH - 1, cs, 1);
|
| 168 |
+
/* vertical mortar — staggered per row */
|
| 169 |
+
const mortarX = px + ((x + row) % 2 === 0 ? cs / 2 : cs / 4);
|
| 170 |
+
ctx.fillStyle = `rgba(0,0,0,${0.22 + heatShift * 0.10})`;
|
| 171 |
+
ctx.fillRect(mortarX, by, 1, brickH - 1);
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
/* Top-left highlight bevel */
|
| 175 |
+
ctx.fillStyle = `rgba(255,${200 - Math.round(heatShift * 80)},${160 - Math.round(heatShift * 140)},${0.32 + heatShift * 0.15})`;
|
| 176 |
+
ctx.fillRect(px, py, cs, 2);
|
| 177 |
+
ctx.fillRect(px, py + 2, 2, cs - 2);
|
| 178 |
+
|
| 179 |
+
/* Bottom-right shadow bevel */
|
| 180 |
+
ctx.fillStyle = `rgba(0,0,0,${0.50 + heatShift * 0.20})`;
|
| 181 |
+
ctx.fillRect(px, py + cs - 2, cs, 2);
|
| 182 |
+
ctx.fillRect(px + cs - 2, py, 2, cs - 2);
|
| 183 |
+
|
| 184 |
+
/* Heat glow overlay on wall face */
|
| 185 |
+
if (heatShift > 0.05) {
|
| 186 |
+
const glowA = heatShift * 0.35 * wallFlicker;
|
| 187 |
+
ctx.fillStyle = `rgba(255,${Math.round(80 - heatShift * 60)},0,${glowA})`;
|
| 188 |
+
ctx.fillRect(px + 2, py + 2, cs - 4, cs - 4);
|
| 189 |
+
|
| 190 |
+
/* Hot crack lines radiating from fire side */
|
| 191 |
+
ctx.strokeStyle = `rgba(255,${Math.round(160 - heatShift * 120)},0,${heatShift * 0.6 * wallFlicker})`;
|
| 192 |
+
ctx.lineWidth = 1;
|
| 193 |
+
ctx.beginPath();
|
| 194 |
+
ctx.moveTo(px + cs * 0.3, py + cs * 0.2);
|
| 195 |
+
ctx.lineTo(px + cs * 0.5, py + cs * 0.55);
|
| 196 |
+
ctx.lineTo(px + cs * 0.7, py + cs * 0.4);
|
| 197 |
+
ctx.stroke();
|
| 198 |
+
if (heatShift > 0.4) {
|
| 199 |
+
ctx.beginPath();
|
| 200 |
+
ctx.moveTo(px + cs * 0.2, py + cs * 0.7);
|
| 201 |
+
ctx.lineTo(px + cs * 0.45, py + cs * 0.85);
|
| 202 |
+
ctx.stroke();
|
| 203 |
+
}
|
| 204 |
+
}
|
| 205 |
+
break;
|
| 206 |
+
}
|
| 207 |
+
case OBSTACLE: {
|
| 208 |
+
/* Charred debris — dark with ember glow */
|
| 209 |
+
const obsNearFire = (
|
| 210 |
+
(x > 0 && fire_grid[idx(x-1, y)] > 0.05 ? fire_grid[idx(x-1, y)] : 0) +
|
| 211 |
+
(x < W-1 && fire_grid[idx(x+1, y)] > 0.05 ? fire_grid[idx(x+1, y)] : 0) +
|
| 212 |
+
(y > 0 && fire_grid[idx(x, y-1)] > 0.05 ? fire_grid[idx(x, y-1)] : 0) +
|
| 213 |
+
(y < H-1 && fire_grid[idx(x, y+1)] > 0.05 ? fire_grid[idx(x, y+1)] : 0)
|
| 214 |
+
) * 0.4 + fire_grid[idx(x, y)] * 0.8;
|
| 215 |
+
const obsHeat = Math.min(1, obsNearFire);
|
| 216 |
+
const obsFlicker = 0.82 + 0.18 * Math.sin(t * 9.1 + x * 1.9 + y * 2.5);
|
| 217 |
+
|
| 218 |
+
ctx.fillStyle = '#2a2520';
|
| 219 |
+
ctx.fillRect(px, py, cs, cs);
|
| 220 |
+
|
| 221 |
+
/* Rubble texture patches */
|
| 222 |
+
ctx.fillStyle = 'rgba(60,50,40,0.7)';
|
| 223 |
+
ctx.fillRect(px + 3, py + 3, cs * 0.4, cs * 0.35);
|
| 224 |
+
ctx.fillRect(px + cs * 0.55, py + cs * 0.5, cs * 0.35, cs * 0.4);
|
| 225 |
+
ctx.fillStyle = 'rgba(80,65,50,0.5)';
|
| 226 |
+
ctx.fillRect(px + cs * 0.25, py + cs * 0.6, cs * 0.45, cs * 0.3);
|
| 227 |
+
|
| 228 |
+
/* Ember glow if near fire */
|
| 229 |
+
if (obsHeat > 0.05) {
|
| 230 |
+
const eg = Math.round(40 + obsHeat * 90 * obsFlicker);
|
| 231 |
+
ctx.fillStyle = `rgba(255,${eg},0,${obsHeat * 0.55 * obsFlicker})`;
|
| 232 |
+
ctx.fillRect(px + 2, py + 2, cs - 4, cs - 4);
|
| 233 |
+
|
| 234 |
+
/* Glowing edge cracks */
|
| 235 |
+
ctx.strokeStyle = `rgba(255,${Math.round(120 * obsHeat * obsFlicker)},0,${obsHeat * 0.8})`;
|
| 236 |
+
ctx.lineWidth = 1.5;
|
| 237 |
+
ctx.beginPath();
|
| 238 |
+
ctx.moveTo(px + cs * 0.1, py + cs * 0.5);
|
| 239 |
+
ctx.lineTo(px + cs * 0.4, py + cs * 0.3);
|
| 240 |
+
ctx.lineTo(px + cs * 0.6, py + cs * 0.7);
|
| 241 |
+
ctx.lineTo(px + cs * 0.9, py + cs * 0.4);
|
| 242 |
+
ctx.stroke();
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
/* Orange danger frame */
|
| 246 |
+
ctx.strokeStyle = `rgba(255,${Math.round(80 + obsHeat * 60)},0,${0.55 + obsHeat * 0.35})`;
|
| 247 |
+
ctx.lineWidth = 2;
|
| 248 |
+
ctx.strokeRect(px + 1, py + 1, cs - 2, cs - 2);
|
| 249 |
+
|
| 250 |
+
/* Corner bolts */
|
| 251 |
+
ctx.fillStyle = `rgba(255,${Math.round(100 + obsHeat * 80)},0,${0.7 + obsHeat * 0.3})`;
|
| 252 |
+
[[4,4],[cs-6,4],[4,cs-6],[cs-6,cs-6]].forEach(([bx, by]) => {
|
| 253 |
+
ctx.beginPath(); ctx.arc(px+bx, py+by, 2, 0, Math.PI*2); ctx.fill();
|
| 254 |
+
});
|
| 255 |
+
break;
|
| 256 |
+
}
|
| 257 |
+
default: {
|
| 258 |
+
/* Checkerboard floor with warm heat tint near fire */
|
| 259 |
+
const floorFire = Math.min(1,
|
| 260 |
+
fire_grid[idx(x, y)] * 1.5 +
|
| 261 |
+
(x > 0 ? fire_grid[idx(x-1, y)] : 0) * 0.3 +
|
| 262 |
+
(x < W-1 ? fire_grid[idx(x+1, y)] : 0) * 0.3 +
|
| 263 |
+
(y > 0 ? fire_grid[idx(x, y-1)] : 0) * 0.3 +
|
| 264 |
+
(y < H-1 ? fire_grid[idx(x, y+1)] : 0) * 0.3
|
| 265 |
+
);
|
| 266 |
+
const base = (x + y) % 2 === 0;
|
| 267 |
+
const fr = Math.round((base ? 232 : 208) + floorFire * 23);
|
| 268 |
+
const fg = Math.round((base ? 216 : 190) - floorFire * 40);
|
| 269 |
+
const fb = Math.round((base ? 184 : 152) - floorFire * 80);
|
| 270 |
+
ctx.fillStyle = `rgb(${fr},${fg},${fb})`;
|
| 271 |
+
ctx.fillRect(px, py, cs, cs);
|
| 272 |
+
/* tile bevel */
|
| 273 |
+
ctx.fillStyle = 'rgba(255,255,255,0.20)';
|
| 274 |
+
ctx.fillRect(px, py, cs, 2);
|
| 275 |
+
ctx.fillRect(px, py + 2, 2, cs - 2);
|
| 276 |
+
ctx.fillStyle = 'rgba(0,0,0,0.18)';
|
| 277 |
+
ctx.fillRect(px, py + cs - 2, cs, 2);
|
| 278 |
+
ctx.fillRect(px + cs - 2, py, 2, cs - 2);
|
| 279 |
+
}
|
| 280 |
+
}
|
| 281 |
+
}
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
/* ── Fire ambient: multiply scorches the floor tiles (under fog) ── */
|
| 285 |
+
ctx.save();
|
| 286 |
+
ctx.globalCompositeOperation = 'multiply';
|
| 287 |
+
for (let y = 0; y < H; y++) {
|
| 288 |
+
for (let x = 0; x < W; x++) {
|
| 289 |
+
const fire = fire_grid[idx(x, y)];
|
| 290 |
+
if (fire < 0.1) continue;
|
| 291 |
+
const px = x * cs + cs / 2, py = y * cs + cs / 2;
|
| 292 |
+
const radius = cs * (1.2 + fire * 1.8);
|
| 293 |
+
const a = Math.min(0.85, fire * 0.9);
|
| 294 |
+
const gr = ctx.createRadialGradient(px, py, 0, px, py, radius);
|
| 295 |
+
gr.addColorStop(0, `rgba(255,80,0,${a})`);
|
| 296 |
+
gr.addColorStop(0.4, `rgba(220,40,0,${a * 0.5})`);
|
| 297 |
+
gr.addColorStop(1, 'rgba(0,0,0,0)');
|
| 298 |
+
ctx.fillStyle = gr;
|
| 299 |
+
ctx.fillRect(px - radius, py - radius, radius * 2, radius * 2);
|
| 300 |
+
}
|
| 301 |
+
}
|
| 302 |
+
ctx.restore();
|
| 303 |
+
|
| 304 |
+
/* ── Smoke (dark on light bg, under fog) ── */
|
| 305 |
+
for (let y = 0; y < H; y++) {
|
| 306 |
+
for (let x = 0; x < W; x++) {
|
| 307 |
+
const smoke = smoke_grid[idx(x, y)];
|
| 308 |
+
if (smoke < 0.1) continue;
|
| 309 |
+
const px = x * cs + cs / 2, py = y * cs + cs / 2;
|
| 310 |
+
const offX = Math.sin(t * 0.5 + x) * 2;
|
| 311 |
+
const offY = Math.cos(t * 0.4 + y) * 2;
|
| 312 |
+
const alpha = Math.min(0.68, smoke * 0.8);
|
| 313 |
+
const gr = ctx.createRadialGradient(px + offX, py + offY, 0, px + offX, py + offY, cs * 0.82);
|
| 314 |
+
gr.addColorStop(0, `rgba(72,82,96,${alpha})`);
|
| 315 |
+
gr.addColorStop(1, 'rgba(72,82,96,0)');
|
| 316 |
+
ctx.fillStyle = gr;
|
| 317 |
+
ctx.beginPath(); ctx.arc(px + offX, py + offY, cs * 0.82, 0, Math.PI * 2); ctx.fill();
|
| 318 |
+
}
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
/* ── Exits & Doors ── */
|
| 322 |
+
for (let y = 0; y < H; y++) {
|
| 323 |
+
for (let x = 0; x < W; x++) {
|
| 324 |
+
const ct = cell_grid[idx(x, y)];
|
| 325 |
+
const px = x * cs, py = y * cs;
|
| 326 |
+
const pulse = 0.7 + 0.3 * Math.sin(t * 3);
|
| 327 |
+
|
| 328 |
+
if (ct === EXIT) {
|
| 329 |
+
ctx.fillStyle = '#e6f4ec';
|
| 330 |
+
ctx.fillRect(px + 2, py + 2, cs - 4, cs - 4);
|
| 331 |
+
ctx.strokeStyle = `rgba(22,163,74,${0.7 + 0.3 * pulse})`;
|
| 332 |
+
ctx.lineWidth = 2 * pulse;
|
| 333 |
+
ctx.strokeRect(px + 5, py + 5, cs - 10, cs - 10);
|
| 334 |
+
/* EXIT symbol */
|
| 335 |
+
ctx.fillStyle = `rgba(22,163,74,${0.85 + 0.15 * pulse})`;
|
| 336 |
+
ctx.font = `bold ${cs * 0.26}px var(--mono, monospace)`;
|
| 337 |
+
ctx.textAlign = 'center';
|
| 338 |
+
ctx.textBaseline = 'middle';
|
| 339 |
+
ctx.fillText('EXIT', px + cs / 2, py + cs / 2);
|
| 340 |
+
} else if (ct === DOOR_CLOSED) {
|
| 341 |
+
ctx.fillStyle = '#7c5c3c';
|
| 342 |
+
ctx.fillRect(px + 4, py + 2, cs - 8, cs - 4);
|
| 343 |
+
ctx.fillStyle = '#4a3020';
|
| 344 |
+
ctx.fillRect(px + 2, py, cs - 4, 2);
|
| 345 |
+
ctx.fillRect(px + 2, py + cs - 2, cs - 4, 2);
|
| 346 |
+
/* handle */
|
| 347 |
+
ctx.fillStyle = '#f0b030';
|
| 348 |
+
ctx.beginPath();
|
| 349 |
+
ctx.arc(px + cs - 10, py + cs / 2, 2.5, 0, Math.PI * 2);
|
| 350 |
+
ctx.fill();
|
| 351 |
+
} else if (ct === DOOR_OPEN) {
|
| 352 |
+
ctx.fillStyle = '#4a3020';
|
| 353 |
+
ctx.fillRect(px + 2, py, 4, cs);
|
| 354 |
+
ctx.fillRect(px + cs - 6, py, 4, cs);
|
| 355 |
+
}
|
| 356 |
+
}
|
| 357 |
+
}
|
| 358 |
+
|
| 359 |
+
/* ── Fog of War (dim only — fire still punches through above) ── */
|
| 360 |
+
for (let y = 0; y < H; y++) {
|
| 361 |
+
for (let x = 0; x < W; x++) {
|
| 362 |
+
const key = `${x},${y}`;
|
| 363 |
+
if (!visible.has(key)) {
|
| 364 |
+
ctx.fillStyle = 'rgba(140,134,126,0.55)';
|
| 365 |
+
ctx.fillRect(x * cs, y * cs, cs, cs);
|
| 366 |
+
}
|
| 367 |
+
}
|
| 368 |
+
}
|
| 369 |
+
|
| 370 |
+
/* ── Fire volumetric: drawn ABOVE fog — always visible ── */
|
| 371 |
+
for (let y = 0; y < H; y++) {
|
| 372 |
+
for (let x = 0; x < W; x++) {
|
| 373 |
+
const fire = fire_grid[idx(x, y)];
|
| 374 |
+
if (fire < 0.05) continue;
|
| 375 |
+
const px = x * cs + cs / 2, py = y * cs + cs / 2;
|
| 376 |
+
const flicker = 0.80 + 0.20 * Math.sin(t * 11.0 + x * 3.1 + y * 2.7);
|
| 377 |
+
const eff = Math.min(1, fire * flicker);
|
| 378 |
+
const isVisible = visible.has(`${x},${y}`);
|
| 379 |
+
|
| 380 |
+
const windDx = wv[0] * cs * 0.25 * eff;
|
| 381 |
+
const windDy = wv[1] * cs * 0.25 * eff - cs * 0.06;
|
| 382 |
+
|
| 383 |
+
/* Wide warning beacon glow for fire in fog — always shown */
|
| 384 |
+
if (!isVisible) {
|
| 385 |
+
const beaconPulse = 0.6 + 0.4 * Math.sin(t * 6.0 + x * 1.7 + y * 2.3);
|
| 386 |
+
const beaconR = cs * (1.4 + beaconPulse * 0.6);
|
| 387 |
+
const beaconGr = ctx.createRadialGradient(px, py, 0, px, py, beaconR);
|
| 388 |
+
beaconGr.addColorStop(0, `rgba(255,100,0,${eff * beaconPulse * 0.85})`);
|
| 389 |
+
beaconGr.addColorStop(0.3, `rgba(255,60,0,${eff * beaconPulse * 0.50})`);
|
| 390 |
+
beaconGr.addColorStop(1, 'rgba(220,30,0,0)');
|
| 391 |
+
ctx.fillStyle = beaconGr;
|
| 392 |
+
ctx.beginPath(); ctx.arc(px, py, beaconR, 0, Math.PI * 2); ctx.fill();
|
| 393 |
+
}
|
| 394 |
+
|
| 395 |
+
/* Outer dark-red base */
|
| 396 |
+
{
|
| 397 |
+
const r = cs * 0.70 * (0.7 + eff * 0.3);
|
| 398 |
+
const cx2 = px + windDx * 0.5, cy2 = py + windDy * 0.5;
|
| 399 |
+
const gr = ctx.createRadialGradient(cx2, cy2, 0, cx2, cy2, r);
|
| 400 |
+
gr.addColorStop(0, `rgba(200,20,0,${eff * 0.65})`);
|
| 401 |
+
gr.addColorStop(0.55,`rgba(170,10,0,${eff * 0.35})`);
|
| 402 |
+
gr.addColorStop(1, 'rgba(80,0,0,0)');
|
| 403 |
+
ctx.fillStyle = gr;
|
| 404 |
+
ctx.beginPath(); ctx.arc(cx2, cy2, r, 0, Math.PI * 2); ctx.fill();
|
| 405 |
+
}
|
| 406 |
+
|
| 407 |
+
/* Mid vivid-orange body */
|
| 408 |
+
{
|
| 409 |
+
const r = cs * 0.46 * (0.8 + eff * 0.2);
|
| 410 |
+
const cx2 = px + windDx * 0.35, cy2 = py + windDy * 0.35 - cs * 0.04 * eff;
|
| 411 |
+
const gr = ctx.createRadialGradient(cx2, cy2, 0, cx2, cy2, r);
|
| 412 |
+
gr.addColorStop(0, `rgba(255,110,0,${eff * 0.92})`);
|
| 413 |
+
gr.addColorStop(0.45,`rgba(255,60,0,${eff * 0.62})`);
|
| 414 |
+
gr.addColorStop(1, 'rgba(220,20,0,0)');
|
| 415 |
+
ctx.fillStyle = gr;
|
| 416 |
+
ctx.beginPath(); ctx.arc(cx2, cy2, r, 0, Math.PI * 2); ctx.fill();
|
| 417 |
+
}
|
| 418 |
+
|
| 419 |
+
/* Inner bright-yellow core */
|
| 420 |
+
{
|
| 421 |
+
const r = cs * 0.28 * eff;
|
| 422 |
+
const cx2 = px + windDx * 0.15, cy2 = py + windDy * 0.15 - cs * 0.10 * eff;
|
| 423 |
+
const gr = ctx.createRadialGradient(cx2, cy2, 0, cx2, cy2, r);
|
| 424 |
+
gr.addColorStop(0, `rgba(255,235,90,${eff * 0.97})`);
|
| 425 |
+
gr.addColorStop(0.35,`rgba(255,175,25,${eff * 0.78})`);
|
| 426 |
+
gr.addColorStop(1, 'rgba(255,80,0,0)');
|
| 427 |
+
ctx.fillStyle = gr;
|
| 428 |
+
ctx.beginPath(); ctx.arc(cx2, cy2, r, 0, Math.PI * 2); ctx.fill();
|
| 429 |
+
}
|
| 430 |
+
|
| 431 |
+
/* White-hot tip (only for intense fire) */
|
| 432 |
+
if (eff > 0.55) {
|
| 433 |
+
const r = cs * 0.14 * eff;
|
| 434 |
+
const cx2 = px + windDx * 0.1, cy2 = py + windDy * 0.1 - cs * 0.18 * eff;
|
| 435 |
+
const gr = ctx.createRadialGradient(cx2, cy2, 0, cx2, cy2, r);
|
| 436 |
+
gr.addColorStop(0, `rgba(255,255,230,${eff * 0.95})`);
|
| 437 |
+
gr.addColorStop(1, 'rgba(255,220,60,0)');
|
| 438 |
+
ctx.fillStyle = gr;
|
| 439 |
+
ctx.beginPath(); ctx.arc(cx2, cy2, r, 0, Math.PI * 2); ctx.fill();
|
| 440 |
+
}
|
| 441 |
+
|
| 442 |
+
/* Wind-carried plume tip */
|
| 443 |
+
if (fire > 0.35) {
|
| 444 |
+
const r = cs * 0.30 * eff;
|
| 445 |
+
const cx2 = px + windDx, cy2 = py + windDy - cs * 0.22 * eff;
|
| 446 |
+
const gr = ctx.createRadialGradient(cx2, cy2, 0, cx2, cy2, r);
|
| 447 |
+
gr.addColorStop(0, `rgba(255,165,10,${eff * 0.68})`);
|
| 448 |
+
gr.addColorStop(1, 'rgba(255,60,0,0)');
|
| 449 |
+
ctx.fillStyle = gr;
|
| 450 |
+
ctx.beginPath(); ctx.arc(cx2, cy2, r, 0, Math.PI * 2); ctx.fill();
|
| 451 |
+
}
|
| 452 |
+
|
| 453 |
+
/* Outer visible bloom ring (makes fire pop even in fog) */
|
| 454 |
+
{
|
| 455 |
+
const bloomR = cs * (0.85 + eff * 0.35);
|
| 456 |
+
const bloomGr = ctx.createRadialGradient(px, py, cs * 0.2, px, py, bloomR);
|
| 457 |
+
bloomGr.addColorStop(0, 'rgba(255,120,0,0)');
|
| 458 |
+
bloomGr.addColorStop(0.6, `rgba(255,80,0,${eff * 0.22})`);
|
| 459 |
+
bloomGr.addColorStop(1, 'rgba(200,30,0,0)');
|
| 460 |
+
ctx.fillStyle = bloomGr;
|
| 461 |
+
ctx.beginPath(); ctx.arc(px, py, bloomR, 0, Math.PI * 2); ctx.fill();
|
| 462 |
+
}
|
| 463 |
+
|
| 464 |
+
if (fire > 0.45 && Math.random() < 0.09 && embersRef.current.length < 120) {
|
| 465 |
+
embersRef.current.push(new Ember(px, py, wv[0]));
|
| 466 |
+
}
|
| 467 |
+
}
|
| 468 |
+
}
|
| 469 |
+
|
| 470 |
+
/* ── Vision lantern glow around agent ── */
|
| 471 |
+
const apx = agent_x * cs + cs / 2, apy = agent_y * cs + cs / 2;
|
| 472 |
+
const lanternR = cs * 3.5;
|
| 473 |
+
const lanternGr = ctx.createRadialGradient(apx, apy, 0, apx, apy, lanternR);
|
| 474 |
+
lanternGr.addColorStop(0, 'rgba(255,240,190,0.18)');
|
| 475 |
+
lanternGr.addColorStop(0.5, 'rgba(255,220,140,0.08)');
|
| 476 |
+
lanternGr.addColorStop(1, 'rgba(0,0,0,0)');
|
| 477 |
+
ctx.fillStyle = lanternGr;
|
| 478 |
+
ctx.fillRect(apx - lanternR, apy - lanternR, lanternR * 2, lanternR * 2);
|
| 479 |
+
|
| 480 |
+
/* ── Agent trail ── */
|
| 481 |
+
const now = timeRef.current;
|
| 482 |
+
if (
|
| 483 |
+
trailRef.current.length === 0 ||
|
| 484 |
+
Math.abs(trailRef.current[0].x - apx) > 1 ||
|
| 485 |
+
Math.abs(trailRef.current[0].y - apy) > 1
|
| 486 |
+
) {
|
| 487 |
+
trailRef.current.unshift({ x: apx, y: apy, t: now });
|
| 488 |
+
}
|
| 489 |
+
if (trailRef.current.length > 20) trailRef.current.pop();
|
| 490 |
+
|
| 491 |
+
trailRef.current.forEach((p, i) => {
|
| 492 |
+
const alpha = (1 - i / trailRef.current.length) * 0.70;
|
| 493 |
+
ctx.fillStyle = `rgba(2,132,199,${alpha})`;
|
| 494 |
+
ctx.beginPath();
|
| 495 |
+
ctx.arc(p.x, p.y, cs * 0.12 * (1 - i / 22), 0, Math.PI * 2);
|
| 496 |
+
ctx.fill();
|
| 497 |
+
});
|
| 498 |
+
|
| 499 |
+
/* ── Agent rendering ── */
|
| 500 |
+
const theme =
|
| 501 |
+
agent_health >= 60 ? AGENT_THEMES.healthy :
|
| 502 |
+
agent_health >= 30 ? AGENT_THEMES.moderate :
|
| 503 |
+
agent_health > 0 ? AGENT_THEMES.low :
|
| 504 |
+
AGENT_THEMES.critical;
|
| 505 |
+
|
| 506 |
+
const pulse = 0.85 + 0.15 * Math.sin(t * 4);
|
| 507 |
+
const ringR = cs * 0.48;
|
| 508 |
+
|
| 509 |
+
/* pulsing gold aura */
|
| 510 |
+
const auraR = ringR * (1.5 + 0.2 * pulse);
|
| 511 |
+
const auraGr = ctx.createRadialGradient(apx, apy, ringR * 0.7, apx, apy, auraR);
|
| 512 |
+
auraGr.addColorStop(0, `rgba(251,191,36,${0.28 * pulse})`);
|
| 513 |
+
auraGr.addColorStop(1, 'rgba(251,191,36,0)');
|
| 514 |
+
ctx.fillStyle = auraGr;
|
| 515 |
+
ctx.beginPath(); ctx.arc(apx, apy, auraR, 0, Math.PI * 2); ctx.fill();
|
| 516 |
+
|
| 517 |
+
/* ground shadow */
|
| 518 |
+
ctx.save();
|
| 519 |
+
ctx.globalAlpha = 0.22;
|
| 520 |
+
const shadowGr = ctx.createRadialGradient(apx, apy + cs * 0.32, 0, apx, apy + cs * 0.32, cs * 0.38);
|
| 521 |
+
shadowGr.addColorStop(0, 'rgba(0,0,0,0.6)');
|
| 522 |
+
shadowGr.addColorStop(1, 'rgba(0,0,0,0)');
|
| 523 |
+
ctx.fillStyle = shadowGr;
|
| 524 |
+
ctx.beginPath(); ctx.ellipse(apx, apy + cs * 0.32, cs * 0.38, cs * 0.14, 0, 0, Math.PI * 2); ctx.fill();
|
| 525 |
+
ctx.restore();
|
| 526 |
+
|
| 527 |
+
/* Minecraft character */
|
| 528 |
+
drawMinecraftAgent(ctx, apx, apy, cs, theme);
|
| 529 |
+
|
| 530 |
+
/* health arc ring — gold ring + colored fill */
|
| 531 |
+
const hRatio = Math.max(0, Math.min(1, agent_health / 100));
|
| 532 |
+
/* ring track */
|
| 533 |
+
ctx.beginPath();
|
| 534 |
+
ctx.arc(apx, apy, ringR, 0, Math.PI * 2);
|
| 535 |
+
ctx.strokeStyle = 'rgba(0,0,0,0.12)';
|
| 536 |
+
ctx.lineWidth = 4;
|
| 537 |
+
ctx.lineCap = 'round';
|
| 538 |
+
ctx.stroke();
|
| 539 |
+
/* gold base ring */
|
| 540 |
+
ctx.beginPath();
|
| 541 |
+
ctx.arc(apx, apy, ringR, 0, Math.PI * 2);
|
| 542 |
+
ctx.strokeStyle = 'rgba(251,191,36,0.25)';
|
| 543 |
+
ctx.lineWidth = 3.5;
|
| 544 |
+
ctx.stroke();
|
| 545 |
+
/* health fill */
|
| 546 |
+
ctx.beginPath();
|
| 547 |
+
ctx.arc(apx, apy, ringR, -Math.PI / 2, -Math.PI / 2 + hRatio * Math.PI * 2);
|
| 548 |
+
ctx.strokeStyle = theme.ring;
|
| 549 |
+
ctx.lineWidth = 3.5;
|
| 550 |
+
ctx.lineCap = 'round';
|
| 551 |
+
ctx.stroke();
|
| 552 |
+
/* ring glow */
|
| 553 |
+
ctx.beginPath();
|
| 554 |
+
ctx.arc(apx, apy, ringR, -Math.PI / 2, -Math.PI / 2 + hRatio * Math.PI * 2);
|
| 555 |
+
ctx.strokeStyle = theme.ringGlow;
|
| 556 |
+
ctx.lineWidth = 6;
|
| 557 |
+
ctx.stroke();
|
| 558 |
+
|
| 559 |
+
/* move flash */
|
| 560 |
+
if (agentMoveFlash > 0) {
|
| 561 |
+
const fa = agentMoveFlash / 18;
|
| 562 |
+
ctx.strokeStyle = `rgba(255,255,255,${fa * 0.8})`;
|
| 563 |
+
ctx.lineWidth = 3;
|
| 564 |
+
ctx.beginPath();
|
| 565 |
+
ctx.arc(apx, apy, ringR * (1.8 + (1 - fa) * 0.6), 0, Math.PI * 2);
|
| 566 |
+
ctx.stroke();
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
/* ── Embers ── */
|
| 570 |
+
for (let i = embersRef.current.length - 1; i >= 0; i--) {
|
| 571 |
+
const e = embersRef.current[i];
|
| 572 |
+
e.update();
|
| 573 |
+
if (e.life <= 0) { embersRef.current.splice(i, 1); continue; }
|
| 574 |
+
ctx.fillStyle = `rgba(255,${Math.floor(80 + 175 * e.life)},0,${e.life})`;
|
| 575 |
+
ctx.beginPath(); ctx.arc(e.x, e.y, e.size * e.life, 0, Math.PI * 2); ctx.fill();
|
| 576 |
+
}
|
| 577 |
+
|
| 578 |
+
rafRef.current = requestAnimationFrame(animate);
|
| 579 |
+
};
|
| 580 |
+
|
| 581 |
+
useEffect(() => {
|
| 582 |
+
rafRef.current = requestAnimationFrame(animate);
|
| 583 |
+
return () => { if (rafRef.current) cancelAnimationFrame(rafRef.current); };
|
| 584 |
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
| 585 |
+
}, [observation, agentMoveFlash]);
|
| 586 |
+
|
| 587 |
+
const W = observation?.map_state.grid_w ?? 16;
|
| 588 |
+
const H = observation?.map_state.grid_h ?? 16;
|
| 589 |
+
|
| 590 |
+
return (
|
| 591 |
+
<canvas
|
| 592 |
+
ref={canvasRef}
|
| 593 |
+
id="map-canvas"
|
| 594 |
+
width={W * CS}
|
| 595 |
+
height={H * CS}
|
| 596 |
+
/>
|
| 597 |
+
);
|
| 598 |
+
};
|
| 599 |
+
|
| 600 |
+
export default Map2D;
|
frontend/src/components/StatusCard.tsx
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react';
|
| 2 |
+
|
| 3 |
+
interface StatusRowProps {
|
| 4 |
+
label: string;
|
| 5 |
+
value: string | number;
|
| 6 |
+
className?: string;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
export const StatusRow: React.FC<StatusRowProps> = ({ label, value, className = '' }) => (
|
| 10 |
+
<div className="srow">
|
| 11 |
+
<span>{label}</span>
|
| 12 |
+
<span className={`sv ${className}`}>{value}</span>
|
| 13 |
+
</div>
|
| 14 |
+
);
|
| 15 |
+
|
| 16 |
+
interface StatusCardProps {
|
| 17 |
+
title: string;
|
| 18 |
+
children: React.ReactNode;
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
export const StatusCard: React.FC<StatusCardProps> = ({ title, children }) => (
|
| 22 |
+
<div className="card">
|
| 23 |
+
<div className="card-title">{title}</div>
|
| 24 |
+
{children}
|
| 25 |
+
</div>
|
| 26 |
+
);
|
frontend/src/index.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
/* index.css cleared to use App.css exclusively for project styling */
|
frontend/src/main.tsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { StrictMode } from 'react'
|
| 2 |
+
import { createRoot } from 'react-dom/client'
|
| 3 |
+
import './index.css'
|
| 4 |
+
import App from './App.tsx'
|
| 5 |
+
|
| 6 |
+
createRoot(document.getElementById('root')!).render(
|
| 7 |
+
<StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</StrictMode>,
|
| 10 |
+
)
|
frontend/src/types.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export interface SceneLabels {
|
| 2 |
+
agent: {
|
| 3 |
+
x: number;
|
| 4 |
+
y: number;
|
| 5 |
+
health: number;
|
| 6 |
+
health_status: string;
|
| 7 |
+
alive: boolean;
|
| 8 |
+
evacuated: boolean;
|
| 9 |
+
location: string;
|
| 10 |
+
smoke_level: string;
|
| 11 |
+
fire_visible: boolean;
|
| 12 |
+
fire_direction: string | null;
|
| 13 |
+
last_action_feedback: string;
|
| 14 |
+
};
|
| 15 |
+
episode: {
|
| 16 |
+
id: string;
|
| 17 |
+
step: number;
|
| 18 |
+
max_steps: number;
|
| 19 |
+
template: string;
|
| 20 |
+
difficulty: string;
|
| 21 |
+
wind_dir: string;
|
| 22 |
+
fire_spread_rate: number;
|
| 23 |
+
humidity: number;
|
| 24 |
+
fire_sources: number;
|
| 25 |
+
};
|
| 26 |
+
map: {
|
| 27 |
+
width: number;
|
| 28 |
+
height: number;
|
| 29 |
+
exit_positions: [number, number][];
|
| 30 |
+
door_registry: Record<string, [number, number]>;
|
| 31 |
+
};
|
| 32 |
+
surroundings: {
|
| 33 |
+
visible_objects: {
|
| 34 |
+
id: string;
|
| 35 |
+
type: string;
|
| 36 |
+
relative_pos: string;
|
| 37 |
+
state: string;
|
| 38 |
+
}[];
|
| 39 |
+
blocked_exit_ids: string[];
|
| 40 |
+
audible_signals: string[];
|
| 41 |
+
available_actions: string[];
|
| 42 |
+
};
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
export interface SceneGraph {
|
| 46 |
+
channels: string[];
|
| 47 |
+
channel_info: Record<string, string>;
|
| 48 |
+
width: number;
|
| 49 |
+
height: number;
|
| 50 |
+
grid: number[][][]; // grid[y][x] = [cell_type, fire, smoke, is_agent, is_visible]
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
export interface SceneResponse {
|
| 54 |
+
labels: SceneLabels;
|
| 55 |
+
graph: SceneGraph;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
export interface Observation {
|
| 59 |
+
map_state: {
|
| 60 |
+
cell_grid: number[];
|
| 61 |
+
fire_grid: number[];
|
| 62 |
+
smoke_grid: number[];
|
| 63 |
+
agent_x: number;
|
| 64 |
+
agent_y: number;
|
| 65 |
+
visible_cells: [number, number][];
|
| 66 |
+
door_registry: Record<string, [number, number]>;
|
| 67 |
+
exit_positions: [number, number][];
|
| 68 |
+
step_count: number;
|
| 69 |
+
max_steps: number;
|
| 70 |
+
grid_w: number;
|
| 71 |
+
grid_h: number;
|
| 72 |
+
template_name: string;
|
| 73 |
+
};
|
| 74 |
+
agent_health: number;
|
| 75 |
+
location_label: string;
|
| 76 |
+
smoke_level: string;
|
| 77 |
+
wind_dir: string;
|
| 78 |
+
fire_visible: boolean;
|
| 79 |
+
fire_direction: string | null;
|
| 80 |
+
last_action_feedback: string;
|
| 81 |
+
narrative: string;
|
| 82 |
+
reward?: number;
|
| 83 |
+
done?: boolean;
|
| 84 |
+
metadata?: {
|
| 85 |
+
fire_sources: number;
|
| 86 |
+
fire_spread_rate: number;
|
| 87 |
+
humidity: number;
|
| 88 |
+
difficulty: string;
|
| 89 |
+
};
|
| 90 |
+
}
|
| 91 |
+
|
| 92 |
+
export interface Door {
|
| 93 |
+
id: string;
|
| 94 |
+
x: number;
|
| 95 |
+
y: number;
|
| 96 |
+
state: 'open' | 'closed' | 'failed';
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
export interface StaffMember {
|
| 100 |
+
id: string;
|
| 101 |
+
x: number;
|
| 102 |
+
y: number;
|
| 103 |
+
phase: number;
|
| 104 |
+
mood: 'calm' | 'panicked';
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
export interface ApiReport {
|
| 108 |
+
call_type: string;
|
| 109 |
+
request: any;
|
| 110 |
+
response: any;
|
| 111 |
+
meta: string;
|
| 112 |
+
}
|
frontend/tsconfig.app.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
| 4 |
+
"target": "es2023",
|
| 5 |
+
"lib": ["ES2023", "DOM"],
|
| 6 |
+
"module": "esnext",
|
| 7 |
+
"types": ["vite/client"],
|
| 8 |
+
"skipLibCheck": true,
|
| 9 |
+
|
| 10 |
+
/* Bundler mode */
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"allowImportingTsExtensions": true,
|
| 13 |
+
"verbatimModuleSyntax": true,
|
| 14 |
+
"moduleDetection": "force",
|
| 15 |
+
"noEmit": true,
|
| 16 |
+
"jsx": "react-jsx",
|
| 17 |
+
|
| 18 |
+
/* Linting */
|
| 19 |
+
"noUnusedLocals": true,
|
| 20 |
+
"noUnusedParameters": true,
|
| 21 |
+
"erasableSyntaxOnly": true,
|
| 22 |
+
"noFallthroughCasesInSwitch": true
|
| 23 |
+
},
|
| 24 |
+
"include": ["src"]
|
| 25 |
+
}
|
frontend/tsconfig.json
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"files": [],
|
| 3 |
+
"references": [
|
| 4 |
+
{ "path": "./tsconfig.app.json" },
|
| 5 |
+
{ "path": "./tsconfig.node.json" }
|
| 6 |
+
]
|
| 7 |
+
}
|
frontend/tsconfig.node.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"compilerOptions": {
|
| 3 |
+
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
| 4 |
+
"target": "es2023",
|
| 5 |
+
"lib": ["ES2023"],
|
| 6 |
+
"module": "esnext",
|
| 7 |
+
"types": ["node"],
|
| 8 |
+
"skipLibCheck": true,
|
| 9 |
+
|
| 10 |
+
/* Bundler mode */
|
| 11 |
+
"moduleResolution": "bundler",
|
| 12 |
+
"allowImportingTsExtensions": true,
|
| 13 |
+
"verbatimModuleSyntax": true,
|
| 14 |
+
"moduleDetection": "force",
|
| 15 |
+
"noEmit": true,
|
| 16 |
+
|
| 17 |
+
/* Linting */
|
| 18 |
+
"noUnusedLocals": true,
|
| 19 |
+
"noUnusedParameters": true,
|
| 20 |
+
"erasableSyntaxOnly": true,
|
| 21 |
+
"noFallthroughCasesInSwitch": true
|
| 22 |
+
},
|
| 23 |
+
"include": ["vite.config.ts"]
|
| 24 |
+
}
|
frontend/vite.config.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
import path from 'path'
|
| 4 |
+
|
| 5 |
+
// https://vite.dev/config/
|
| 6 |
+
export default defineConfig({
|
| 7 |
+
plugins: [react()],
|
| 8 |
+
build: {
|
| 9 |
+
outDir: path.resolve(__dirname, '../server/static'),
|
| 10 |
+
emptyOutDir: true,
|
| 11 |
+
},
|
| 12 |
+
server: {
|
| 13 |
+
proxy: {
|
| 14 |
+
'/state': 'http://127.0.0.1:8000',
|
| 15 |
+
'/reset': 'http://127.0.0.1:8000',
|
| 16 |
+
'/step': 'http://127.0.0.1:8000',
|
| 17 |
+
'/scene': 'http://127.0.0.1:8000',
|
| 18 |
+
}
|
| 19 |
+
}
|
| 20 |
+
})
|
openenv_pyre_env.egg-info/PKG-INFO
CHANGED
|
@@ -13,3 +13,16 @@ Requires-Dist: langchain-openai>=1.2.1
|
|
| 13 |
Provides-Extra: dev
|
| 14 |
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
| 15 |
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
Provides-Extra: dev
|
| 14 |
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
| 15 |
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
| 16 |
+
Provides-Extra: train
|
| 17 |
+
Requires-Dist: datasets>=4.8.4; extra == "train"
|
| 18 |
+
Requires-Dist: peft>=0.15.2; extra == "train"
|
| 19 |
+
Requires-Dist: tensorboard>=2.20.0; extra == "train"
|
| 20 |
+
Requires-Dist: torch>=2.9.0; extra == "train"
|
| 21 |
+
Requires-Dist: transformers>=4.57.6; extra == "train"
|
| 22 |
+
Requires-Dist: trl>=1.2.0; extra == "train"
|
| 23 |
+
Requires-Dist: tornado>=6.5.5; extra == "train"
|
| 24 |
+
Requires-Dist: vllm<=0.18.0,>=0.11.0; extra == "train"
|
| 25 |
+
Requires-Dist: flashinfer-python==0.6.3; extra == "train"
|
| 26 |
+
Requires-Dist: flashinfer-cubin==0.6.3; extra == "train"
|
| 27 |
+
Requires-Dist: jupyter>=1.1.1; extra == "train"
|
| 28 |
+
Requires-Dist: flash-attn>=2.7.0; extra == "train"
|
openenv_pyre_env.egg-info/SOURCES.txt
CHANGED
|
@@ -4,6 +4,7 @@ client.py
|
|
| 4 |
evals.py
|
| 5 |
models.py
|
| 6 |
pyproject.toml
|
|
|
|
| 7 |
./__init__.py
|
| 8 |
./client.py
|
| 9 |
./evals.py
|
|
@@ -20,5 +21,6 @@ server/app.py
|
|
| 20 |
server/fire_sim.py
|
| 21 |
server/floor_plan.py
|
| 22 |
server/narrative.py
|
|
|
|
| 23 |
server/pyre_env_environment.py
|
| 24 |
server/rubrics.py
|
|
|
|
| 4 |
evals.py
|
| 5 |
models.py
|
| 6 |
pyproject.toml
|
| 7 |
+
train_grpo_openenv.py
|
| 8 |
./__init__.py
|
| 9 |
./client.py
|
| 10 |
./evals.py
|
|
|
|
| 21 |
server/fire_sim.py
|
| 22 |
server/floor_plan.py
|
| 23 |
server/narrative.py
|
| 24 |
+
server/npc_model.py
|
| 25 |
server/pyre_env_environment.py
|
| 26 |
server/rubrics.py
|
openenv_pyre_env.egg-info/requires.txt
CHANGED
|
@@ -9,3 +9,17 @@ langchain-openai>=1.2.1
|
|
| 9 |
[dev]
|
| 10 |
pytest>=8.0.0
|
| 11 |
pytest-cov>=4.0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
[dev]
|
| 10 |
pytest>=8.0.0
|
| 11 |
pytest-cov>=4.0.0
|
| 12 |
+
|
| 13 |
+
[train]
|
| 14 |
+
datasets>=4.8.4
|
| 15 |
+
peft>=0.15.2
|
| 16 |
+
tensorboard>=2.20.0
|
| 17 |
+
torch>=2.9.0
|
| 18 |
+
transformers>=4.57.6
|
| 19 |
+
trl>=1.2.0
|
| 20 |
+
tornado>=6.5.5
|
| 21 |
+
vllm<=0.18.0,>=0.11.0
|
| 22 |
+
flashinfer-python==0.6.3
|
| 23 |
+
flashinfer-cubin==0.6.3
|
| 24 |
+
jupyter>=1.1.1
|
| 25 |
+
flash-attn>=2.7.0
|
outputs/20260425_154907_Qwen-Qwen3-06B/error.txt
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
warning: Failed to uninstall package at .venv/lib/python3.12/site-packages/numpy-2.4.4.dist-info due to missing `RECORD` file. Installation may result in an incomplete environment.
|
| 2 |
+
Uninstalled 2 packages in 6.94s
|
| 3 |
+
warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance.
|
| 4 |
+
If the cache and target directories are on different filesystems, hardlinking may not be supported.
|
| 5 |
+
If this is intentional, set `export UV_LINK_MODE=copy` or use `--link-mode=copy` to suppress this warning.
|
| 6 |
+
Installed 1 package in 1.15s
|
| 7 |
+
[2026-04-25 16:01:06] INFO train_grpo_openenv.py:567: Model: Qwen/Qwen3-0.6B
|
| 8 |
+
[2026-04-25 16:01:06] INFO train_grpo_openenv.py:573: Dataset: 1000 prompts
|
| 9 |
+
[2026-04-25 16:01:07] INFO train_grpo_openenv.py:608: Output: ./outputs/20260425_154907_Qwen-Qwen3-06B | vLLM mode: colocate
|
| 10 |
+
`torch_dtype` is deprecated! Use `dtype` instead!
|
| 11 |
+
Flash Attention 2 only supports torch.float16 and torch.bfloat16 dtypes, but the current dype in Qwen3ForCausalLM is torch.float32. You should run training or inference using Automatic Mixed-Precision via the `with torch.autocast(device_type='torch_device'):` decorator, or load the model with the `dtype` argument. Example: `model = AutoModel.from_pretrained("openai/whisper-tiny", attn_implementation="flash_attention_2", dtype=torch.float16)`
|
| 12 |
+
Flash Attention 2 only supports torch.float16 and torch.bfloat16 dtypes, but the current dype in Qwen3Model is torch.float32. You should run training or inference using Automatic Mixed-Precision via the `with torch.autocast(device_type='torch_device'):` decorator, or load the model with the `dtype` argument. Example: `model = AutoModel.from_pretrained("openai/whisper-tiny", attn_implementation="flash_attention_2", dtype=torch.float16)`
|
| 13 |
+
<frozen importlib._bootstrap_external>:1301: FutureWarning: The cuda.cudart module is deprecated and will be removed in a future release, please switch to use the cuda.bindings.runtime module instead.
|
| 14 |
+
<frozen importlib._bootstrap_external>:1301: FutureWarning: The cuda.nvrtc module is deprecated and will be removed in a future release, please switch to use the cuda.bindings.nvrtc module instead.
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
[rank0]: Traceback (most recent call last):
|
| 20 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/train_grpo_openenv.py", line 611, in <module>
|
| 21 |
+
[rank0]: trainer = GRPOTrainer(
|
| 22 |
+
[rank0]: ^^^^^^^^^^^^
|
| 23 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/trl/trainer/grpo_trainer.py", line 750, in __init__
|
| 24 |
+
[rank0]: self.vllm_generation = VLLMGeneration(
|
| 25 |
+
[rank0]: ^^^^^^^^^^^^^^^
|
| 26 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/trl/generation/vllm_generation.py", line 282, in __init__
|
| 27 |
+
[rank0]: self._init_vllm()
|
| 28 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/trl/generation/vllm_generation.py", line 342, in _init_vllm
|
| 29 |
+
[rank0]: self.llm = LLM(
|
| 30 |
+
[rank0]: ^^^^
|
| 31 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/vllm/entrypoints/llm.py", line 346, in __init__
|
| 32 |
+
[rank0]: self.llm_engine = LLMEngine.from_engine_args(
|
| 33 |
+
[rank0]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 34 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/vllm/v1/engine/llm_engine.py", line 174, in from_engine_args
|
| 35 |
+
[rank0]: return cls(
|
| 36 |
+
[rank0]: ^^^^
|
| 37 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/vllm/v1/engine/llm_engine.py", line 108, in __init__
|
| 38 |
+
[rank0]: self.engine_core = EngineCoreClient.make_client(
|
| 39 |
+
[rank0]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 40 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/vllm/v1/engine/core_client.py", line 97, in make_client
|
| 41 |
+
[rank0]: return InprocClient(vllm_config, executor_class, log_stats)
|
| 42 |
+
[rank0]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 43 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/vllm/v1/engine/core_client.py", line 277, in __init__
|
| 44 |
+
[rank0]: self.engine_core = EngineCore(*args, **kwargs)
|
| 45 |
+
[rank0]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 46 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/vllm/v1/engine/core.py", line 113, in __init__
|
| 47 |
+
[rank0]: num_gpu_blocks, num_cpu_blocks, kv_cache_config = self._initialize_kv_caches(
|
| 48 |
+
[rank0]: ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
| 49 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/vllm/v1/engine/core.py", line 259, in _initialize_kv_caches
|
| 50 |
+
[rank0]: kv_cache_configs = get_kv_cache_configs(
|
| 51 |
+
[rank0]: ^^^^^^^^^^^^^^^^^^^^^
|
| 52 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/vllm/v1/core/kv_cache_utils.py", line 1516, in get_kv_cache_configs
|
| 53 |
+
[rank0]: _check_enough_kv_cache_memory(
|
| 54 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/vllm/v1/core/kv_cache_utils.py", line 634, in _check_enough_kv_cache_memory
|
| 55 |
+
[rank0]: raise ValueError(
|
| 56 |
+
[rank0]: ValueError: To serve at least one request with the models's max seq len (8192), (0.88 GiB KV cache is needed, which is larger than the available KV cache memory (0.12 GiB). Based on the available memory, the estimated maximum model length is 1104. Try increasing `gpu_memory_utilization` or decreasing `max_model_len` when initializing the engine. See https://docs.vllm.ai/en/latest/configuration/conserving_memory/ for more details.
|
| 57 |
+
[rank0]:[W425 16:05:53.916115210 ProcessGroupNCCL.cpp:1524] Warning: WARNING: destroy_process_group() was not called before program exit, which can leak resources. For more info, please see https://pytorch.org/docs/stable/distributed.html#shutdown (function operator())
|
outputs/20260425_154907_Qwen-Qwen3-06B/output.txt
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Helper functions defined.
|
| 2 |
+
Rollout functions defined.
|
| 3 |
+
Reward functions: evacuated, step_efficiency, format, efficiency
|
| 4 |
+
|
| 5 |
+
------------------------------------------------------------
|
| 6 |
+
Sender: LSF System <lsfadmin@cccxc590>
|
| 7 |
+
Subject: Job 1281121: <20260425_154907_Qwen-Qwen3-06B> in cluster <CCCFP15Cluster> Exited
|
| 8 |
+
|
| 9 |
+
Job <20260425_154907_Qwen-Qwen3-06B> was submitted from host <ccc-login3> by user <kirushi> in cluster <CCCFP15Cluster> at Sat Apr 25 15:50:13 2026
|
| 10 |
+
Job was executed on host(s) <cccxc590>, in queue <normal>, as user <kirushi> in cluster <CCCFP15Cluster> at Sat Apr 25 15:49:50 2026
|
| 11 |
+
</u/kirushi> was used as the home directory.
|
| 12 |
+
</dccstor/kirushikesh/personal-projects/openenv-pyre> was used as the working directory.
|
| 13 |
+
Started at Sat Apr 25 15:49:50 2026
|
| 14 |
+
Terminated at Sat Apr 25 16:05:56 2026
|
| 15 |
+
Results reported at Sat Apr 25 16:05:56 2026
|
| 16 |
+
|
| 17 |
+
Your job looked like:
|
| 18 |
+
|
| 19 |
+
------------------------------------------------------------
|
| 20 |
+
# LSBATCH: User input
|
| 21 |
+
/u/kirushi/.local/bin/uv run python train_grpo_openenv.py --model-id Qwen/Qwen3-0.6B --dataset-size 1000 --output-dir ./outputs/20260425_154907_Qwen-Qwen3-06B --report-to tensorboard --seed 42
|
| 22 |
+
------------------------------------------------------------
|
| 23 |
+
|
| 24 |
+
Exited with exit code 1.
|
| 25 |
+
|
| 26 |
+
Resource usage summary:
|
| 27 |
+
|
| 28 |
+
CPU time : 374.61 sec.
|
| 29 |
+
Max Memory : 3 GB
|
| 30 |
+
Average Memory : 1.39 GB
|
| 31 |
+
Total Requested Memory : -
|
| 32 |
+
Delta Memory : -
|
| 33 |
+
Max Swap : -
|
| 34 |
+
Max Processes : 5
|
| 35 |
+
Max Threads : 494
|
| 36 |
+
Run time : 966 sec.
|
| 37 |
+
Turnaround time : 943 sec.
|
| 38 |
+
|
| 39 |
+
The output (if any) is above this job summary.
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
PS:
|
| 44 |
+
|
| 45 |
+
Read file <./outputs/20260425_154907_Qwen-Qwen3-06B/error.txt> for stderr output of this job.
|
| 46 |
+
|
outputs/20260425_154915_Qwen-Qwen3-17B/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
base_model: Qwen/Qwen3-1.7B
|
| 3 |
+
library_name: transformers
|
| 4 |
+
model_name: 20260425_154915_Qwen-Qwen3-17B
|
| 5 |
+
tags:
|
| 6 |
+
- generated_from_trainer
|
| 7 |
+
- trl
|
| 8 |
+
- grpo
|
| 9 |
+
licence: license
|
| 10 |
+
---
|
| 11 |
+
|
| 12 |
+
# Model Card for 20260425_154915_Qwen-Qwen3-17B
|
| 13 |
+
|
| 14 |
+
This model is a fine-tuned version of [Qwen/Qwen3-1.7B](https://huggingface.co/Qwen/Qwen3-1.7B).
|
| 15 |
+
It has been trained using [TRL](https://github.com/huggingface/trl).
|
| 16 |
+
|
| 17 |
+
## Quick start
|
| 18 |
+
|
| 19 |
+
```python
|
| 20 |
+
from transformers import pipeline
|
| 21 |
+
|
| 22 |
+
question = "If you had a time machine, but could only go to the past or the future once and never return, which would you choose and why?"
|
| 23 |
+
generator = pipeline("text-generation", model="None", device="cuda")
|
| 24 |
+
output = generator([{"role": "user", "content": question}], max_new_tokens=128, return_full_text=False)[0]
|
| 25 |
+
print(output["generated_text"])
|
| 26 |
+
```
|
| 27 |
+
|
| 28 |
+
## Training procedure
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
This model was trained with GRPO, a method introduced in [DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models](https://huggingface.co/papers/2402.03300).
|
| 35 |
+
|
| 36 |
+
### Framework versions
|
| 37 |
+
|
| 38 |
+
- TRL: 1.2.0
|
| 39 |
+
- Transformers: 4.57.6
|
| 40 |
+
- Pytorch: 2.9.1
|
| 41 |
+
- Datasets: 4.8.4
|
| 42 |
+
- Tokenizers: 0.22.2
|
| 43 |
+
|
| 44 |
+
## Citations
|
| 45 |
+
|
| 46 |
+
Cite GRPO as:
|
| 47 |
+
|
| 48 |
+
```bibtex
|
| 49 |
+
@article{shao2024deepseekmath,
|
| 50 |
+
title = {{DeepSeekMath: Pushing the Limits of Mathematical Reasoning in Open Language Models}},
|
| 51 |
+
author = {Zhihong Shao and Peiyi Wang and Qihao Zhu and Runxin Xu and Junxiao Song and Mingchuan Zhang and Y. K. Li and Y. Wu and Daya Guo},
|
| 52 |
+
year = 2024,
|
| 53 |
+
eprint = {arXiv:2402.03300},
|
| 54 |
+
}
|
| 55 |
+
```
|
| 56 |
+
|
| 57 |
+
Cite TRL as:
|
| 58 |
+
|
| 59 |
+
```bibtex
|
| 60 |
+
@software{vonwerra2020trl,
|
| 61 |
+
title = {{TRL: Transformers Reinforcement Learning}},
|
| 62 |
+
author = {von Werra, Leandro and Belkada, Younes and Tunstall, Lewis and Beeching, Edward and Thrush, Tristan and Lambert, Nathan and Huang, Shengyi and Rasul, Kashif and Gallouédec, Quentin},
|
| 63 |
+
license = {Apache-2.0},
|
| 64 |
+
url = {https://github.com/huggingface/trl},
|
| 65 |
+
year = {2020}
|
| 66 |
+
}
|
| 67 |
+
```
|
outputs/20260425_154915_Qwen-Qwen3-17B/checkpoint-10/config.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"architectures": [
|
| 3 |
+
"Qwen3ForCausalLM"
|
| 4 |
+
],
|
| 5 |
+
"attention_bias": false,
|
| 6 |
+
"attention_dropout": 0.0,
|
| 7 |
+
"dtype": "float32",
|
| 8 |
+
"eos_token_id": 151645,
|
| 9 |
+
"head_dim": 128,
|
| 10 |
+
"hidden_act": "silu",
|
| 11 |
+
"hidden_size": 2048,
|
| 12 |
+
"initializer_range": 0.02,
|
| 13 |
+
"intermediate_size": 6144,
|
| 14 |
+
"layer_types": [
|
| 15 |
+
"full_attention",
|
| 16 |
+
"full_attention",
|
| 17 |
+
"full_attention",
|
| 18 |
+
"full_attention",
|
| 19 |
+
"full_attention",
|
| 20 |
+
"full_attention",
|
| 21 |
+
"full_attention",
|
| 22 |
+
"full_attention",
|
| 23 |
+
"full_attention",
|
| 24 |
+
"full_attention",
|
| 25 |
+
"full_attention",
|
| 26 |
+
"full_attention",
|
| 27 |
+
"full_attention",
|
| 28 |
+
"full_attention",
|
| 29 |
+
"full_attention",
|
| 30 |
+
"full_attention",
|
| 31 |
+
"full_attention",
|
| 32 |
+
"full_attention",
|
| 33 |
+
"full_attention",
|
| 34 |
+
"full_attention",
|
| 35 |
+
"full_attention",
|
| 36 |
+
"full_attention",
|
| 37 |
+
"full_attention",
|
| 38 |
+
"full_attention",
|
| 39 |
+
"full_attention",
|
| 40 |
+
"full_attention",
|
| 41 |
+
"full_attention",
|
| 42 |
+
"full_attention"
|
| 43 |
+
],
|
| 44 |
+
"max_position_embeddings": 40960,
|
| 45 |
+
"max_window_layers": 28,
|
| 46 |
+
"model_type": "qwen3",
|
| 47 |
+
"num_attention_heads": 16,
|
| 48 |
+
"num_hidden_layers": 28,
|
| 49 |
+
"num_key_value_heads": 8,
|
| 50 |
+
"pad_token_id": 151643,
|
| 51 |
+
"rms_norm_eps": 1e-06,
|
| 52 |
+
"rope_scaling": null,
|
| 53 |
+
"rope_theta": 1000000,
|
| 54 |
+
"sliding_window": null,
|
| 55 |
+
"tie_word_embeddings": true,
|
| 56 |
+
"transformers_version": "4.57.6",
|
| 57 |
+
"use_cache": true,
|
| 58 |
+
"use_sliding_window": false,
|
| 59 |
+
"vocab_size": 151936
|
| 60 |
+
}
|
outputs/20260425_154915_Qwen-Qwen3-17B/checkpoint-10/generation_config.json
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"do_sample": true,
|
| 3 |
+
"eos_token_id": [
|
| 4 |
+
151645,
|
| 5 |
+
151643
|
| 6 |
+
],
|
| 7 |
+
"pad_token_id": 151643,
|
| 8 |
+
"temperature": 0.6,
|
| 9 |
+
"top_k": 20,
|
| 10 |
+
"top_p": 0.95,
|
| 11 |
+
"transformers_version": "4.57.6"
|
| 12 |
+
}
|
outputs/20260425_154915_Qwen-Qwen3-17B/checkpoint-10/model-00001-of-00002.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:69a3cc129138e8190c7a058ddd4346f9c540ff3b4f110490e8a796ffc4457942
|
| 3 |
+
size 5242880
|
outputs/20260425_154915_Qwen-Qwen3-17B/checkpoint-10/model-00002-of-00002.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a66c56b9b128725068ad1913324cd9d69e411d41c81e627caa41d68c7a5db527
|
| 3 |
+
size 5242880
|
outputs/20260425_154915_Qwen-Qwen3-17B/error.txt
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 0 |
0%| | 0/500 [00:00<?, ?it/s][2026-04-25 16:05:56] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=794772
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
0%| | 1/500 [04:42<39:13:06, 282.94s/it]
|
| 2 |
|
| 3 |
0%| | 1/500 [04:43<39:13:06, 282.94s/it][2026-04-25 16:10:39] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=792518
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
0%| | 2/500 [09:56<41:39:36, 301.16s/it]
|
| 5 |
|
| 6 |
0%| | 2/500 [09:57<41:39:36, 301.16s/it][2026-04-25 16:15:53] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=105592
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
1%| | 3/500 [15:20<42:57:56, 311.22s/it]
|
| 8 |
|
| 9 |
1%| | 3/500 [15:20<42:57:56, 311.22s/it][2026-04-25 16:21:16] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=418196
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
1%| | 4/500 [20:11<41:48:51, 303.49s/it]
|
| 11 |
|
| 12 |
1%| | 4/500 [20:11<41:48:51, 303.49s/it][2026-04-25 16:26:07] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=149416
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
1%| | 5/500 [25:32<42:35:25, 309.75s/it]
|
| 14 |
|
| 15 |
1%| | 5/500 [25:32<42:35:25, 309.75s/it][2026-04-25 16:31:28] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=838260
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 16 |
1%| | 6/500 [30:28<41:51:02, 304.98s/it]
|
| 17 |
|
| 18 |
1%| | 6/500 [30:28<41:51:02, 304.98s/it][2026-04-25 16:36:24] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=72441
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
1%|▏ | 7/500 [35:36<41:54:16, 306.00s/it]
|
| 20 |
|
| 21 |
1%|▏ | 7/500 [35:36<41:54:16, 306.00s/it][2026-04-25 16:41:32] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=246941
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
2%|▏ | 8/500 [40:45<41:57:45, 307.04s/it]
|
| 23 |
|
| 24 |
2%|▏ | 8/500 [40:45<41:57:45, 307.04s/it][2026-04-25 16:46:42] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=510073
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
2%|▏ | 9/500 [45:33<41:03:58, 301.10s/it]
|
| 26 |
|
| 27 |
2%|▏ | 9/500 [45:33<41:03:58, 301.10s/it][2026-04-25 16:51:29] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=731560
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
2%|▏ | 10/500 [50:50<41:38:33, 305.95s/it]
|
| 29 |
|
| 30 |
2%|▏ | 10/500 [50:50<41:38:33, 305.95s/it][rank0]: Traceback (most recent call last):
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
warning: Failed to uninstall package at .venv/lib/python3.12/site-packages/numpy-2.4.4.dist-info due to missing `RECORD` file. Installation may result in an incomplete environment.
|
| 2 |
+
Uninstalled 2 packages in 1.65s
|
| 3 |
+
warning: Failed to hardlink files; falling back to full copy. This may lead to degraded performance.
|
| 4 |
+
If the cache and target directories are on different filesystems, hardlinking may not be supported.
|
| 5 |
+
If this is intentional, set `export UV_LINK_MODE=copy` or use `--link-mode=copy` to suppress this warning.
|
| 6 |
+
Installed 1 package in 1.24s
|
| 7 |
+
[2026-04-25 16:01:06] INFO train_grpo_openenv.py:567: Model: Qwen/Qwen3-1.7B
|
| 8 |
+
[2026-04-25 16:01:06] INFO train_grpo_openenv.py:573: Dataset: 1000 prompts
|
| 9 |
+
[2026-04-25 16:01:07] INFO train_grpo_openenv.py:608: Output: ./outputs/20260425_154915_Qwen-Qwen3-17B | vLLM mode: colocate
|
| 10 |
+
`torch_dtype` is deprecated! Use `dtype` instead!
|
| 11 |
+
Flash Attention 2 only supports torch.float16 and torch.bfloat16 dtypes, but the current dype in Qwen3ForCausalLM is torch.float32. You should run training or inference using Automatic Mixed-Precision via the `with torch.autocast(device_type='torch_device'):` decorator, or load the model with the `dtype` argument. Example: `model = AutoModel.from_pretrained("openai/whisper-tiny", attn_implementation="flash_attention_2", dtype=torch.float16)`
|
| 12 |
+
Flash Attention 2 only supports torch.float16 and torch.bfloat16 dtypes, but the current dype in Qwen3Model is torch.float32. You should run training or inference using Automatic Mixed-Precision via the `with torch.autocast(device_type='torch_device'):` decorator, or load the model with the `dtype` argument. Example: `model = AutoModel.from_pretrained("openai/whisper-tiny", attn_implementation="flash_attention_2", dtype=torch.float16)`
|
| 13 |
+
|
| 14 |
+
<frozen importlib._bootstrap_external>:1301: FutureWarning: The cuda.cudart module is deprecated and will be removed in a future release, please switch to use the cuda.bindings.runtime module instead.
|
| 15 |
+
<frozen importlib._bootstrap_external>:1301: FutureWarning: The cuda.nvrtc module is deprecated and will be removed in a future release, please switch to use the cuda.bindings.nvrtc module instead.
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
[2026-04-25 16:05:55] INFO train_grpo_openenv.py:630: GPU: NVIDIA A100-SXM4-80GB — 79.251 GB total, 25.477 GB reserved
|
| 23 |
+
The tokenizer has new PAD/BOS/EOS tokens that differ from the model config and generation config. The model config and generation config were aligned accordingly, being updated with the tokenizer's values. Updated tokens: {'bos_token_id': None, 'pad_token_id': 151643}.
|
| 24 |
+
|
| 25 |
0%| | 0/500 [00:00<?, ?it/s][2026-04-25 16:05:56] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=794772
|
| 26 |
+
[2026-04-25 16:06:33] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.030 | steps=15 | fmt=87%
|
| 27 |
+
[2026-04-25 16:06:33] INFO train_grpo_openenv.py:444: Episode 2 | difficulty=easy | seed=42450
|
| 28 |
+
[2026-04-25 16:07:07] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.040 | steps=14 | fmt=86%
|
| 29 |
+
[2026-04-25 16:07:07] INFO train_grpo_openenv.py:444: Episode 3 | difficulty=easy | seed=536110
|
| 30 |
+
[2026-04-25 16:07:41] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.021 | steps=13 | fmt=92%
|
| 31 |
+
[2026-04-25 16:07:41] INFO train_grpo_openenv.py:444: Episode 4 | difficulty=easy | seed=962838
|
| 32 |
+
[2026-04-25 16:08:15] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.009 | steps=13 | fmt=85%
|
| 33 |
+
[2026-04-25 16:08:15] INFO train_grpo_openenv.py:444: Episode 5 | difficulty=easy | seed=318046
|
| 34 |
+
[2026-04-25 16:08:48] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.010 | steps=11 | fmt=82%
|
| 35 |
+
[2026-04-25 16:08:48] INFO train_grpo_openenv.py:444: Episode 6 | difficulty=easy | seed=375441
|
| 36 |
+
[2026-04-25 16:09:24] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.014 | steps=14 | fmt=86%
|
| 37 |
+
[2026-04-25 16:09:24] INFO train_grpo_openenv.py:444: Episode 7 | difficulty=easy | seed=952225
|
| 38 |
+
[2026-04-25 16:09:58] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.023 | steps=14 | fmt=86%
|
| 39 |
+
[2026-04-25 16:09:58] INFO train_grpo_openenv.py:444: Episode 8 | difficulty=easy | seed=146039
|
| 40 |
+
[2026-04-25 16:10:06] INFO train_grpo_openenv.py:466: → EVACUATED | health=100.0 | mean_step=2.822 | steps=6 | fmt=100%
|
| 41 |
+
Casting fp32 inputs back to torch.bfloat16 for flash-attn compatibility.
|
| 42 |
+
Could not estimate the number of tokens of the input, floating-point operations will not be computed
|
| 43 |
+
|
| 44 |
0%| | 1/500 [04:42<39:13:06, 282.94s/it]
|
| 45 |
|
| 46 |
0%| | 1/500 [04:43<39:13:06, 282.94s/it][2026-04-25 16:10:39] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=792518
|
| 47 |
+
[2026-04-25 16:11:15] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.002 | steps=15 | fmt=80%
|
| 48 |
+
[2026-04-25 16:11:15] INFO train_grpo_openenv.py:444: Episode 2 | difficulty=easy | seed=838234
|
| 49 |
+
[2026-04-25 16:11:49] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.019 | steps=15 | fmt=93%
|
| 50 |
+
[2026-04-25 16:11:49] INFO train_grpo_openenv.py:444: Episode 3 | difficulty=easy | seed=953938
|
| 51 |
+
[2026-04-25 16:12:26] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.001 | steps=16 | fmt=88%
|
| 52 |
+
[2026-04-25 16:12:26] INFO train_grpo_openenv.py:444: Episode 4 | difficulty=easy | seed=739426
|
| 53 |
+
[2026-04-25 16:13:00] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.017 | steps=16 | fmt=94%
|
| 54 |
+
[2026-04-25 16:13:00] INFO train_grpo_openenv.py:444: Episode 5 | difficulty=easy | seed=945989
|
| 55 |
+
[2026-04-25 16:13:34] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.019 | steps=14 | fmt=93%
|
| 56 |
+
[2026-04-25 16:13:34] INFO train_grpo_openenv.py:444: Episode 6 | difficulty=easy | seed=103560
|
| 57 |
+
[2026-04-25 16:14:07] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.018 | steps=16 | fmt=100%
|
| 58 |
+
[2026-04-25 16:14:07] INFO train_grpo_openenv.py:444: Episode 7 | difficulty=easy | seed=942500
|
| 59 |
+
[2026-04-25 16:14:42] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.015 | steps=18 | fmt=94%
|
| 60 |
+
[2026-04-25 16:14:42] INFO train_grpo_openenv.py:444: Episode 8 | difficulty=easy | seed=346236
|
| 61 |
+
[2026-04-25 16:15:15] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.004 | steps=16 | fmt=88%
|
| 62 |
+
|
| 63 |
0%| | 2/500 [09:56<41:39:36, 301.16s/it]
|
| 64 |
|
| 65 |
0%| | 2/500 [09:57<41:39:36, 301.16s/it][2026-04-25 16:15:53] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=105592
|
| 66 |
+
[2026-04-25 16:16:29] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.025 | steps=16 | fmt=88%
|
| 67 |
+
[2026-04-25 16:16:29] INFO train_grpo_openenv.py:444: Episode 2 | difficulty=easy | seed=331556
|
| 68 |
+
[2026-04-25 16:17:03] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.017 | steps=13 | fmt=85%
|
| 69 |
+
[2026-04-25 16:17:03] INFO train_grpo_openenv.py:444: Episode 3 | difficulty=easy | seed=957361
|
| 70 |
+
[2026-04-25 16:17:40] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.059 | steps=12 | fmt=58%
|
| 71 |
+
[2026-04-25 16:17:40] INFO train_grpo_openenv.py:444: Episode 4 | difficulty=easy | seed=579363
|
| 72 |
+
[2026-04-25 16:18:16] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.017 | steps=15 | fmt=100%
|
| 73 |
+
[2026-04-25 16:18:16] INFO train_grpo_openenv.py:444: Episode 5 | difficulty=easy | seed=907343
|
| 74 |
+
[2026-04-25 16:18:55] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.011 | steps=15 | fmt=93%
|
| 75 |
+
[2026-04-25 16:18:55] INFO train_grpo_openenv.py:444: Episode 6 | difficulty=easy | seed=65304
|
| 76 |
+
[2026-04-25 16:19:30] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.001 | steps=15 | fmt=93%
|
| 77 |
+
[2026-04-25 16:19:30] INFO train_grpo_openenv.py:444: Episode 7 | difficulty=easy | seed=575352
|
| 78 |
+
[2026-04-25 16:20:06] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.035 | steps=15 | fmt=93%
|
| 79 |
+
[2026-04-25 16:20:06] INFO train_grpo_openenv.py:444: Episode 8 | difficulty=easy | seed=97802
|
| 80 |
+
[2026-04-25 16:20:43] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.013 | steps=14 | fmt=100%
|
| 81 |
+
|
| 82 |
1%| | 3/500 [15:20<42:57:56, 311.22s/it]
|
| 83 |
|
| 84 |
1%| | 3/500 [15:20<42:57:56, 311.22s/it][2026-04-25 16:21:16] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=418196
|
| 85 |
+
[2026-04-25 16:21:25] INFO train_grpo_openenv.py:466: → EVACUATED | health=100.0 | mean_step=2.803 | steps=6 | fmt=100%
|
| 86 |
+
[2026-04-25 16:21:25] INFO train_grpo_openenv.py:444: Episode 2 | difficulty=easy | seed=823182
|
| 87 |
+
[2026-04-25 16:22:01] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.045 | steps=15 | fmt=100%
|
| 88 |
+
[2026-04-25 16:22:01] INFO train_grpo_openenv.py:444: Episode 3 | difficulty=easy | seed=1198
|
| 89 |
+
[2026-04-25 16:22:37] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.024 | steps=14 | fmt=93%
|
| 90 |
+
[2026-04-25 16:22:37] INFO train_grpo_openenv.py:444: Episode 4 | difficulty=easy | seed=868287
|
| 91 |
+
[2026-04-25 16:23:13] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.028 | steps=12 | fmt=92%
|
| 92 |
+
[2026-04-25 16:23:13] INFO train_grpo_openenv.py:444: Episode 5 | difficulty=easy | seed=255759
|
| 93 |
+
[2026-04-25 16:23:47] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.010 | steps=15 | fmt=87%
|
| 94 |
+
[2026-04-25 16:23:47] INFO train_grpo_openenv.py:444: Episode 6 | difficulty=easy | seed=737822
|
| 95 |
+
[2026-04-25 16:24:22] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.032 | steps=13 | fmt=85%
|
| 96 |
+
[2026-04-25 16:24:22] INFO train_grpo_openenv.py:444: Episode 7 | difficulty=easy | seed=200348
|
| 97 |
+
[2026-04-25 16:25:00] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.027 | steps=14 | fmt=86%
|
| 98 |
+
[2026-04-25 16:25:00] INFO train_grpo_openenv.py:444: Episode 8 | difficulty=easy | seed=232473
|
| 99 |
+
[2026-04-25 16:25:36] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.011 | steps=15 | fmt=100%
|
| 100 |
+
|
| 101 |
1%| | 4/500 [20:11<41:48:51, 303.49s/it]
|
| 102 |
|
| 103 |
1%| | 4/500 [20:11<41:48:51, 303.49s/it][2026-04-25 16:26:07] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=149416
|
| 104 |
+
[2026-04-25 16:26:43] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.028 | steps=16 | fmt=88%
|
| 105 |
+
[2026-04-25 16:26:43] INFO train_grpo_openenv.py:444: Episode 2 | difficulty=easy | seed=469730
|
| 106 |
+
[2026-04-25 16:27:19] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.007 | steps=15 | fmt=100%
|
| 107 |
+
[2026-04-25 16:27:19] INFO train_grpo_openenv.py:444: Episode 3 | difficulty=easy | seed=335601
|
| 108 |
+
[2026-04-25 16:27:54] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.005 | steps=14 | fmt=86%
|
| 109 |
+
[2026-04-25 16:27:54] INFO train_grpo_openenv.py:444: Episode 4 | difficulty=easy | seed=978147
|
| 110 |
+
[2026-04-25 16:28:30] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.015 | steps=15 | fmt=87%
|
| 111 |
+
[2026-04-25 16:28:30] INFO train_grpo_openenv.py:444: Episode 5 | difficulty=easy | seed=316089
|
| 112 |
+
[2026-04-25 16:29:06] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.016 | steps=17 | fmt=94%
|
| 113 |
+
[2026-04-25 16:29:06] INFO train_grpo_openenv.py:444: Episode 6 | difficulty=easy | seed=740883
|
| 114 |
+
[2026-04-25 16:29:43] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.005 | steps=15 | fmt=87%
|
| 115 |
+
[2026-04-25 16:29:43] INFO train_grpo_openenv.py:444: Episode 7 | difficulty=easy | seed=348914
|
| 116 |
+
[2026-04-25 16:30:19] INFO train_grpo_openenv.py:466: → failed | health=50.0 | mean_step=-0.159 | steps=13 | fmt=85%
|
| 117 |
+
[2026-04-25 16:30:19] INFO train_grpo_openenv.py:444: Episode 8 | difficulty=easy | seed=566528
|
| 118 |
+
[2026-04-25 16:30:54] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.001 | steps=13 | fmt=77%
|
| 119 |
+
|
| 120 |
1%| | 5/500 [25:32<42:35:25, 309.75s/it]
|
| 121 |
|
| 122 |
1%| | 5/500 [25:32<42:35:25, 309.75s/it][2026-04-25 16:31:28] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=838260
|
| 123 |
+
[2026-04-25 16:32:05] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.004 | steps=12 | fmt=83%
|
| 124 |
+
[2026-04-25 16:32:05] INFO train_grpo_openenv.py:444: Episode 2 | difficulty=easy | seed=616161
|
| 125 |
+
[2026-04-25 16:32:40] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.038 | steps=12 | fmt=75%
|
| 126 |
+
[2026-04-25 16:32:40] INFO train_grpo_openenv.py:444: Episode 3 | difficulty=easy | seed=96083
|
| 127 |
+
[2026-04-25 16:32:55] INFO train_grpo_openenv.py:466: → EVACUATED | health=100.0 | mean_step=1.823 | steps=9 | fmt=100%
|
| 128 |
+
[2026-04-25 16:32:55] INFO train_grpo_openenv.py:444: Episode 4 | difficulty=easy | seed=403598
|
| 129 |
+
[2026-04-25 16:33:30] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.044 | steps=14 | fmt=79%
|
| 130 |
+
[2026-04-25 16:33:30] INFO train_grpo_openenv.py:444: Episode 5 | difficulty=easy | seed=253867
|
| 131 |
+
[2026-04-25 16:34:08] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.018 | steps=13 | fmt=77%
|
| 132 |
+
[2026-04-25 16:34:08] INFO train_grpo_openenv.py:444: Episode 6 | difficulty=easy | seed=198591
|
| 133 |
+
[2026-04-25 16:34:43] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.009 | steps=13 | fmt=85%
|
| 134 |
+
[2026-04-25 16:34:43] INFO train_grpo_openenv.py:444: Episode 7 | difficulty=easy | seed=34574
|
| 135 |
+
[2026-04-25 16:35:19] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.027 | steps=15 | fmt=93%
|
| 136 |
+
[2026-04-25 16:35:19] INFO train_grpo_openenv.py:444: Episode 8 | difficulty=easy | seed=688557
|
| 137 |
+
[2026-04-25 16:35:53] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.034 | steps=14 | fmt=93%
|
| 138 |
+
|
| 139 |
1%| | 6/500 [30:28<41:51:02, 304.98s/it]
|
| 140 |
|
| 141 |
1%| | 6/500 [30:28<41:51:02, 304.98s/it][2026-04-25 16:36:24] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=72441
|
| 142 |
+
[2026-04-25 16:36:57] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.019 | steps=14 | fmt=93%
|
| 143 |
+
[2026-04-25 16:36:57] INFO train_grpo_openenv.py:444: Episode 2 | difficulty=easy | seed=794405
|
| 144 |
+
[2026-04-25 16:37:32] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.000 | steps=15 | fmt=93%
|
| 145 |
+
[2026-04-25 16:37:32] INFO train_grpo_openenv.py:444: Episode 3 | difficulty=easy | seed=156814
|
| 146 |
+
[2026-04-25 16:38:07] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.004 | steps=14 | fmt=71%
|
| 147 |
+
[2026-04-25 16:38:07] INFO train_grpo_openenv.py:444: Episode 4 | difficulty=easy | seed=883383
|
| 148 |
+
[2026-04-25 16:38:44] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.005 | steps=13 | fmt=92%
|
| 149 |
+
[2026-04-25 16:38:44] INFO train_grpo_openenv.py:444: Episode 5 | difficulty=easy | seed=733293
|
| 150 |
+
[2026-04-25 16:39:16] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.058 | steps=12 | fmt=75%
|
| 151 |
+
[2026-04-25 16:39:16] INFO train_grpo_openenv.py:444: Episode 6 | difficulty=easy | seed=566860
|
| 152 |
+
[2026-04-25 16:39:51] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.031 | steps=13 | fmt=92%
|
| 153 |
+
[2026-04-25 16:39:51] INFO train_grpo_openenv.py:444: Episode 7 | difficulty=easy | seed=878565
|
| 154 |
+
[2026-04-25 16:40:26] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.045 | steps=13 | fmt=85%
|
| 155 |
+
[2026-04-25 16:40:26] INFO train_grpo_openenv.py:444: Episode 8 | difficulty=easy | seed=289023
|
| 156 |
+
[2026-04-25 16:41:01] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.032 | steps=13 | fmt=69%
|
| 157 |
+
|
| 158 |
1%|▏ | 7/500 [35:36<41:54:16, 306.00s/it]
|
| 159 |
|
| 160 |
1%|▏ | 7/500 [35:36<41:54:16, 306.00s/it][2026-04-25 16:41:32] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=246941
|
| 161 |
+
[2026-04-25 16:42:07] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.031 | steps=13 | fmt=77%
|
| 162 |
+
[2026-04-25 16:42:07] INFO train_grpo_openenv.py:444: Episode 2 | difficulty=easy | seed=938516
|
| 163 |
+
[2026-04-25 16:42:43] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.034 | steps=16 | fmt=94%
|
| 164 |
+
[2026-04-25 16:42:43] INFO train_grpo_openenv.py:444: Episode 3 | difficulty=easy | seed=865351
|
| 165 |
+
[2026-04-25 16:43:16] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.056 | steps=13 | fmt=77%
|
| 166 |
+
[2026-04-25 16:43:16] INFO train_grpo_openenv.py:444: Episode 4 | difficulty=easy | seed=607854
|
| 167 |
+
[2026-04-25 16:43:49] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.019 | steps=15 | fmt=93%
|
| 168 |
+
[2026-04-25 16:43:49] INFO train_grpo_openenv.py:444: Episode 5 | difficulty=easy | seed=516586
|
| 169 |
+
[2026-04-25 16:44:25] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.031 | steps=15 | fmt=87%
|
| 170 |
+
[2026-04-25 16:44:25] INFO train_grpo_openenv.py:444: Episode 6 | difficulty=easy | seed=734239
|
| 171 |
+
[2026-04-25 16:45:00] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.016 | steps=14 | fmt=93%
|
| 172 |
+
[2026-04-25 16:45:00] INFO train_grpo_openenv.py:444: Episode 7 | difficulty=easy | seed=831861
|
| 173 |
+
[2026-04-25 16:45:34] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.017 | steps=15 | fmt=100%
|
| 174 |
+
[2026-04-25 16:45:34] INFO train_grpo_openenv.py:444: Episode 8 | difficulty=easy | seed=340079
|
| 175 |
+
[2026-04-25 16:46:08] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.007 | steps=13 | fmt=77%
|
| 176 |
+
|
| 177 |
2%|▏ | 8/500 [40:45<41:57:45, 307.04s/it]
|
| 178 |
|
| 179 |
2%|▏ | 8/500 [40:45<41:57:45, 307.04s/it][2026-04-25 16:46:42] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=510073
|
| 180 |
+
[2026-04-25 16:47:17] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.033 | steps=13 | fmt=100%
|
| 181 |
+
[2026-04-25 16:47:17] INFO train_grpo_openenv.py:444: Episode 2 | difficulty=easy | seed=351556
|
| 182 |
+
[2026-04-25 16:47:54] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.004 | steps=13 | fmt=77%
|
| 183 |
+
[2026-04-25 16:47:54] INFO train_grpo_openenv.py:444: Episode 3 | difficulty=easy | seed=254841
|
| 184 |
+
[2026-04-25 16:48:29] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.017 | steps=18 | fmt=94%
|
| 185 |
+
[2026-04-25 16:48:29] INFO train_grpo_openenv.py:444: Episode 4 | difficulty=easy | seed=284203
|
| 186 |
+
[2026-04-25 16:48:36] INFO train_grpo_openenv.py:466: → EVACUATED | health=100.0 | mean_step=4.190 | steps=4 | fmt=100%
|
| 187 |
+
[2026-04-25 16:48:36] INFO train_grpo_openenv.py:444: Episode 5 | difficulty=easy | seed=231169
|
| 188 |
+
[2026-04-25 16:49:10] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.021 | steps=14 | fmt=93%
|
| 189 |
+
[2026-04-25 16:49:10] INFO train_grpo_openenv.py:444: Episode 6 | difficulty=easy | seed=178763
|
| 190 |
+
[2026-04-25 16:49:44] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.021 | steps=15 | fmt=100%
|
| 191 |
+
[2026-04-25 16:49:44] INFO train_grpo_openenv.py:444: Episode 7 | difficulty=easy | seed=855546
|
| 192 |
+
[2026-04-25 16:50:19] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.014 | steps=13 | fmt=92%
|
| 193 |
+
[2026-04-25 16:50:19] INFO train_grpo_openenv.py:444: Episode 8 | difficulty=easy | seed=821159
|
| 194 |
+
[2026-04-25 16:50:55] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.022 | steps=14 | fmt=86%
|
| 195 |
+
|
| 196 |
2%|▏ | 9/500 [45:33<41:03:58, 301.10s/it]
|
| 197 |
|
| 198 |
2%|▏ | 9/500 [45:33<41:03:58, 301.10s/it][2026-04-25 16:51:29] INFO train_grpo_openenv.py:444: Episode 1 | difficulty=easy | seed=731560
|
| 199 |
+
[2026-04-25 16:52:07] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.021 | steps=13 | fmt=85%
|
| 200 |
+
[2026-04-25 16:52:07] INFO train_grpo_openenv.py:444: Episode 2 | difficulty=easy | seed=856812
|
| 201 |
+
[2026-04-25 16:52:40] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.025 | steps=13 | fmt=69%
|
| 202 |
+
[2026-04-25 16:52:40] INFO train_grpo_openenv.py:444: Episode 3 | difficulty=easy | seed=954220
|
| 203 |
+
[2026-04-25 16:53:15] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.001 | steps=13 | fmt=77%
|
| 204 |
+
[2026-04-25 16:53:15] INFO train_grpo_openenv.py:444: Episode 4 | difficulty=easy | seed=631421
|
| 205 |
+
[2026-04-25 16:53:50] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.021 | steps=11 | fmt=55%
|
| 206 |
+
[2026-04-25 16:53:50] INFO train_grpo_openenv.py:444: Episode 5 | difficulty=easy | seed=27993
|
| 207 |
+
[2026-04-25 16:54:25] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.015 | steps=19 | fmt=100%
|
| 208 |
+
[2026-04-25 16:54:25] INFO train_grpo_openenv.py:444: Episode 6 | difficulty=easy | seed=197678
|
| 209 |
+
[2026-04-25 16:55:01] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=0.019 | steps=14 | fmt=93%
|
| 210 |
+
[2026-04-25 16:55:01] INFO train_grpo_openenv.py:444: Episode 7 | difficulty=easy | seed=603930
|
| 211 |
+
[2026-04-25 16:55:35] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.026 | steps=11 | fmt=73%
|
| 212 |
+
[2026-04-25 16:55:35] INFO train_grpo_openenv.py:444: Episode 8 | difficulty=easy | seed=95978
|
| 213 |
+
[2026-04-25 16:56:11] INFO train_grpo_openenv.py:466: → failed | health=100.0 | mean_step=-0.045 | steps=12 | fmt=83%
|
| 214 |
+
|
| 215 |
2%|▏ | 10/500 [50:50<41:38:33, 305.95s/it]
|
| 216 |
|
| 217 |
2%|▏ | 10/500 [50:50<41:38:33, 305.95s/it][rank0]: Traceback (most recent call last):
|
| 218 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/train_grpo_openenv.py", line 633, in <module>
|
| 219 |
+
[rank0]: trainer_stats = trainer.train()
|
| 220 |
+
[rank0]: ^^^^^^^^^^^^^^^
|
| 221 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/transformers/trainer.py", line 2325, in train
|
| 222 |
+
[rank0]: return inner_training_loop(
|
| 223 |
+
[rank0]: ^^^^^^^^^^^^^^^^^^^^
|
| 224 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/transformers/trainer.py", line 2756, in _inner_training_loop
|
| 225 |
+
[rank0]: self._maybe_log_save_evaluate(
|
| 226 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/transformers/trainer.py", line 3228, in _maybe_log_save_evaluate
|
| 227 |
+
[rank0]: self._save_checkpoint(model, trial)
|
| 228 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/trl/trainer/grpo_trainer.py", line 2719, in _save_checkpoint
|
| 229 |
+
[rank0]: super()._save_checkpoint(model, trial)
|
| 230 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/transformers/trainer.py", line 3325, in _save_checkpoint
|
| 231 |
+
[rank0]: self.save_model(output_dir, _internal_call=True)
|
| 232 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/transformers/trainer.py", line 4227, in save_model
|
| 233 |
+
[rank0]: self._save(output_dir)
|
| 234 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/transformers/trainer.py", line 4331, in _save
|
| 235 |
+
[rank0]: self.model.save_pretrained(
|
| 236 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/transformers/modeling_utils.py", line 4173, in save_pretrained
|
| 237 |
+
[rank0]: safe_save_file(shard, os.path.join(save_directory, shard_file), metadata=metadata)
|
| 238 |
+
[rank0]: File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/safetensors/torch.py", line 307, in save_file
|
| 239 |
+
[rank0]: serialize_file(_flatten(tensors), filename, metadata=metadata)
|
| 240 |
+
[rank0]: safetensors_rust.SafetensorError: Error while serializing: I/O error: Disk quota exceeded (os error 122)
|
| 241 |
+
Exception ignored in: <_io.TextIOWrapper name='<stdout>' mode='w' encoding='utf-8'>
|
| 242 |
+
OSError: [Errno 122] Disk quota exceeded
|
| 243 |
+
[rank0]:[W425 16:56:59.402981600 ProcessGroupNCCL.cpp:1524] Warning: WARNING: destroy_process_group() was not called before program exit, which can leak resources. For more info, please see https://pytorch.org/docs/stable/distributed.html#shutdown (function operator())
|
| 244 |
+
Exception ignored in: <function tqdm.__del__ at 0x14992c829f80>
|
| 245 |
+
Traceback (most recent call last):
|
| 246 |
+
File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/tqdm/std.py", line 1148, in __del__
|
| 247 |
+
File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/tqdm/std.py", line 1302, in close
|
| 248 |
+
File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/tqdm/std.py", line 1495, in display
|
| 249 |
+
File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/tqdm/std.py", line 1151, in __str__
|
| 250 |
+
File "/dccstor/kirushikesh/personal-projects/openenv-pyre/.venv/lib/python3.12/site-packages/tqdm/std.py", line 546, in format_meter
|
| 251 |
+
AttributeError: 'NoneType' object has no attribute 'format_interval'
|
outputs/20260425_154915_Qwen-Qwen3-17B/output.txt
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Helper functions defined.
|
| 2 |
+
Rollout functions defined.
|
| 3 |
+
Reward functions: evacuated, step_efficiency, format, efficiency
|
| 4 |
+
|
| 5 |
+
------------------------------------------------------------
|
| 6 |
+
Sender: LSF System <lsfadmin@cccxc590>
|
| 7 |
+
Subject: Job 1281122: <20260425_154915_Qwen-Qwen3-17B> in cluster <CCCFP15Cluster> Exited
|
| 8 |
+
|
| 9 |
+
Job <20260425_154915_Qwen-Qwen3-17B> was submitted from host <ccc-login3> by user <kirushi> in cluster <CCCFP15Cluster> at Sat Apr 25 15:50:20 2026
|
| 10 |
+
Job was executed on host(s) <cccxc590>, in queue <normal>, as user <kirushi> in cluster <CCCFP15Cluster> at Sat Apr 25 15:49:58 2026
|
| 11 |
+
</u/kirushi> was used as the home directory.
|
| 12 |
+
</dccstor/kirushikesh/personal-projects/openenv-pyre> was used as the working directory.
|
| 13 |
+
Started at Sat Apr 25 15:49:58 2026
|
| 14 |
+
Terminated at Sat Apr 25 16:57:00 2026
|
| 15 |
+
Results reported at Sat Apr 25 16:57:00 2026
|
| 16 |
+
|
| 17 |
+
Your job looked like:
|
| 18 |
+
|
| 19 |
+
------------------------------------------------------------
|
| 20 |
+
# LSBATCH: User input
|
| 21 |
+
/u/kirushi/.local/bin/uv run python train_grpo_openenv.py --model-id Qwen/Qwen3-1.7B --dataset-size 1000 --output-dir ./outputs/20260425_154915_Qwen-Qwen3-17B --report-to tensorboard --seed 42
|
| 22 |
+
------------------------------------------------------------
|
| 23 |
+
|
| 24 |
+
Exited with exit code 120.
|
| 25 |
+
|
| 26 |
+
Resource usage summary:
|
| 27 |
+
|
| 28 |
+
CPU time : 3999.24 sec.
|
| 29 |
+
Max Memory : 8 GB
|
| 30 |
+
Average Memory : 2.66 GB
|
| 31 |
+
Total Requested Memory : -
|
| 32 |
+
Delta Memory : -
|
| 33 |
+
Max Swap : -
|
| 34 |
+
Max Processes : 5
|
| 35 |
+
Max Threads : 625
|
| 36 |
+
Run time : 4022 sec.
|
| 37 |
+
Turnaround time : 4000 sec.
|
| 38 |
+
|
| 39 |
+
The output (if any) is above this job summary.
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
PS:
|
| 44 |
+
|
| 45 |
+
Read file <./outputs/20260425_154915_Qwen-Qwen3-17B/error.txt> for stderr output of this job.
|
| 46 |
+
|
outputs/20260425_154915_Qwen-Qwen3-17B/runs/Apr25_16-01-06_cccxc590/events.out.tfevents.1777147555.cccxc590.2920434.0
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:8c8afb63bb7a46085a82d14e6a8f0c988a3f2f3c45d8969a4294f653573f645b
|
| 3 |
+
size 31316
|
pyproject.toml
CHANGED
|
@@ -28,6 +28,31 @@ dev = [
|
|
| 28 |
"pytest>=8.0.0",
|
| 29 |
"pytest-cov>=4.0.0",
|
| 30 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
[project.scripts]
|
| 33 |
# Server entry point - enables running via: uv run --project . server
|
|
@@ -38,3 +63,6 @@ server = "pyre_env.server.app:main"
|
|
| 38 |
include-package-data = true
|
| 39 |
packages = ["pyre_env", "pyre_env.server"]
|
| 40 |
package-dir = { "pyre_env" = ".", "pyre_env.server" = "server" }
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
"pytest>=8.0.0",
|
| 29 |
"pytest-cov>=4.0.0",
|
| 30 |
]
|
| 31 |
+
train = [
|
| 32 |
+
"datasets>=4.8.4",
|
| 33 |
+
"peft>=0.15.2",
|
| 34 |
+
"tensorboard>=2.20.0",
|
| 35 |
+
"torch>=2.9.0",
|
| 36 |
+
"transformers>=4.57.6",
|
| 37 |
+
"trl>=1.2.0",
|
| 38 |
+
"tornado>=6.5.5",
|
| 39 |
+
"vllm>=0.11.0,<=0.18.0",
|
| 40 |
+
"flashinfer-python==0.6.3",
|
| 41 |
+
"flashinfer-cubin==0.6.3",
|
| 42 |
+
"jupyter>=1.1.1",
|
| 43 |
+
"flash-attn>=2.7.0",
|
| 44 |
+
]
|
| 45 |
+
# Unsloth-backed training (replaces raw TRL + vLLM stack for train_grpo_unsloth.py)
|
| 46 |
+
# Variant is fixed: A100 (Ampere) + CUDA 12.4, inferred from flashinfer==0.6.3 in train extra.
|
| 47 |
+
# Unsloth bundles its own vLLM & transformers builds.
|
| 48 |
+
train-unsloth = [
|
| 49 |
+
"unsloth[cu124-ampere-torch260]",
|
| 50 |
+
"trl==0.22.2",
|
| 51 |
+
"datasets>=4.8.4",
|
| 52 |
+
"tensorboard>=2.20.0",
|
| 53 |
+
"tornado>=6.5.5",
|
| 54 |
+
"jupyter>=1.1.1",
|
| 55 |
+
]
|
| 56 |
|
| 57 |
[project.scripts]
|
| 58 |
# Server entry point - enables running via: uv run --project . server
|
|
|
|
| 63 |
include-package-data = true
|
| 64 |
packages = ["pyre_env", "pyre_env.server"]
|
| 65 |
package-dir = { "pyre_env" = ".", "pyre_env.server" = "server" }
|
| 66 |
+
|
| 67 |
+
[tool.uv]
|
| 68 |
+
no-build-isolation-package = ["flash-attn"]
|
run_training_openenv.sh
CHANGED
|
@@ -25,18 +25,15 @@ echo "Job name : ${RUN_ID}"
|
|
| 25 |
echo "Port : ${MASTER_PORT}"
|
| 26 |
echo "uv : ${UV_BIN}"
|
| 27 |
|
| 28 |
-
export CUDA_LAUNCH_BLOCKING=1
|
| 29 |
-
|
| 30 |
bsub -q normal -M 128 -n 1 \
|
| 31 |
-
-gpu "num=
|
| 32 |
-o "${OUTPUT_DIR}/output.txt" \
|
| 33 |
-e "${OUTPUT_DIR}/error.txt" \
|
| 34 |
-J "${RUN_ID}" \
|
| 35 |
-env "MASTER_PORT=${MASTER_PORT},VLLM_USE_V1=0" \
|
| 36 |
-
"${UV_BIN}" run python
|
| 37 |
--model-id "${MODEL_NAME}" \
|
| 38 |
-
--dataset-size
|
| 39 |
--output-dir "${OUTPUT_DIR}" \
|
| 40 |
--report-to "tensorboard" \
|
| 41 |
-
--seed 42
|
| 42 |
-
--debug
|
|
|
|
| 25 |
echo "Port : ${MASTER_PORT}"
|
| 26 |
echo "uv : ${UV_BIN}"
|
| 27 |
|
|
|
|
|
|
|
| 28 |
bsub -q normal -M 128 -n 1 \
|
| 29 |
+
-gpu "num=1:gmodel=NVIDIAA100_SXM4_80GB" \
|
| 30 |
-o "${OUTPUT_DIR}/output.txt" \
|
| 31 |
-e "${OUTPUT_DIR}/error.txt" \
|
| 32 |
-J "${RUN_ID}" \
|
| 33 |
-env "MASTER_PORT=${MASTER_PORT},VLLM_USE_V1=0" \
|
| 34 |
+
"${UV_BIN}" run python train_grpo_openenv.py \
|
| 35 |
--model-id "${MODEL_NAME}" \
|
| 36 |
+
--dataset-size 200 \
|
| 37 |
--output-dir "${OUTPUT_DIR}" \
|
| 38 |
--report-to "tensorboard" \
|
| 39 |
+
--seed 42
|
|
|
run_training_unsloth.sh
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
# run_training_unsloth.sh — LSF launcher for train_grpo_unsloth.py
|
| 3 |
+
#
|
| 4 |
+
# Usage:
|
| 5 |
+
# ./run_training_unsloth.sh <model-name>
|
| 6 |
+
# ./run_training_unsloth.sh unsloth/Qwen3-1.7B
|
| 7 |
+
# ./run_training_unsloth.sh unsloth/Qwen3-4B --lora-rank 32 --save-merged
|
| 8 |
+
#
|
| 9 |
+
# All arguments after the model name are forwarded verbatim to the Python
|
| 10 |
+
# script, so any train_grpo_unsloth.py flag can be passed here:
|
| 11 |
+
# ./run_training_unsloth.sh unsloth/Qwen3-4B --dataset-size 500 --save-merged
|
| 12 |
+
set -e
|
| 13 |
+
|
| 14 |
+
if [ -z "$1" ]; then
|
| 15 |
+
echo "Usage: $0 <model-name> [extra python args...]"
|
| 16 |
+
echo " e.g.: $0 unsloth/Qwen3-1.7B"
|
| 17 |
+
echo " e.g.: $0 unsloth/Qwen3-4B --lora-rank 32 --save-merged"
|
| 18 |
+
exit 1
|
| 19 |
+
fi
|
| 20 |
+
|
| 21 |
+
MODEL_NAME="$1"
|
| 22 |
+
shift # remaining args forwarded to the python script
|
| 23 |
+
EXTRA_ARGS="$*"
|
| 24 |
+
|
| 25 |
+
MODEL_SAFE=$(echo "$MODEL_NAME" | tr '/:' '--' | tr -cd '[:alnum:]_-')
|
| 26 |
+
TIMESTAMP=$(date +"%Y%m%d_%H%M%S")
|
| 27 |
+
RUN_ID="${TIMESTAMP}_${MODEL_SAFE}"
|
| 28 |
+
|
| 29 |
+
OUTPUT_DIR="./outputs/${RUN_ID}"
|
| 30 |
+
mkdir -p "${OUTPUT_DIR}"
|
| 31 |
+
|
| 32 |
+
MASTER_PORT=$(( 29500 + RANDOM % 1000 ))
|
| 33 |
+
UV_BIN=$(which uv)
|
| 34 |
+
|
| 35 |
+
echo "Model : ${MODEL_NAME}"
|
| 36 |
+
echo "Output dir: ${OUTPUT_DIR}"
|
| 37 |
+
echo "Job name : ${RUN_ID}"
|
| 38 |
+
echo "Port : ${MASTER_PORT}"
|
| 39 |
+
echo "uv : ${UV_BIN}"
|
| 40 |
+
echo "Extra args: ${EXTRA_ARGS}"
|
| 41 |
+
|
| 42 |
+
bsub -q normal -M 128 -n 1 \
|
| 43 |
+
-gpu "num=1:gmodel=NVIDIAA100_SXM4_80GB" \
|
| 44 |
+
-o "${OUTPUT_DIR}/output.txt" \
|
| 45 |
+
-e "${OUTPUT_DIR}/error.txt" \
|
| 46 |
+
-J "${RUN_ID}" \
|
| 47 |
+
-env "MASTER_PORT=${MASTER_PORT}" \
|
| 48 |
+
"${UV_BIN}" run python train_grpo_unsloth.py \
|
| 49 |
+
--model-id "${MODEL_NAME}" \
|
| 50 |
+
--dataset-size 200 \
|
| 51 |
+
--lora-rank 32 \
|
| 52 |
+
--output-dir "${OUTPUT_DIR}" \
|
| 53 |
+
--report-to "tensorboard" \
|
| 54 |
+
--seed 42 \
|
| 55 |
+
${EXTRA_ARGS}
|
server/app.py
CHANGED
|
@@ -10,20 +10,22 @@ Configuration via environment variables:
|
|
| 10 |
|
| 11 |
import os
|
| 12 |
from pathlib import Path
|
| 13 |
-
from typing import Any, Dict, Optional
|
| 14 |
-
from pydantic import Field
|
| 15 |
from fastapi import HTTPException
|
| 16 |
from fastapi.responses import FileResponse
|
|
|
|
| 17 |
from openenv.core.env_server.http_server import create_app
|
| 18 |
-
from pydantic import BaseModel
|
| 19 |
from starlette.routing import Route
|
| 20 |
|
| 21 |
try:
|
| 22 |
from ..models import PyreAction, PyreObservation
|
| 23 |
from .pyre_env_environment import PyreEnvironment
|
|
|
|
| 24 |
except (ImportError, ModuleNotFoundError):
|
| 25 |
from models import PyreAction, PyreObservation
|
| 26 |
from server.pyre_env_environment import PyreEnvironment
|
|
|
|
| 27 |
|
| 28 |
MAX_STEPS = int(os.getenv("PYRE_MAX_STEPS", "150"))
|
| 29 |
BASE_SEED = int(os.getenv("PYRE_SEED", "42"))
|
|
@@ -83,13 +85,22 @@ STATIC_DIR = Path(__file__).resolve().parent / "static"
|
|
| 83 |
|
| 84 |
@app.get("/")
|
| 85 |
def serve_frontend() -> FileResponse:
|
| 86 |
-
"""Serve the
|
| 87 |
-
html_path = STATIC_DIR / "
|
| 88 |
if not html_path.exists():
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
return FileResponse(str(html_path))
|
| 91 |
|
| 92 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
@app.post("/reset")
|
| 94 |
def reset_episode(body: ResetRequest = ResetRequest()) -> Dict[str, Any]:
|
| 95 |
env = get_stateful_env()
|
|
@@ -127,6 +138,133 @@ def get_state() -> Dict[str, Any]:
|
|
| 127 |
return env.state.model_dump()
|
| 128 |
|
| 129 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 130 |
def main(host: str = "0.0.0.0", port: int = 8000):
|
| 131 |
import uvicorn
|
| 132 |
port = int(os.getenv("PORT", port))
|
|
|
|
| 10 |
|
| 11 |
import os
|
| 12 |
from pathlib import Path
|
| 13 |
+
from typing import Any, Dict, List, Optional
|
| 14 |
+
from pydantic import Field, BaseModel
|
| 15 |
from fastapi import HTTPException
|
| 16 |
from fastapi.responses import FileResponse
|
| 17 |
+
from fastapi.staticfiles import StaticFiles
|
| 18 |
from openenv.core.env_server.http_server import create_app
|
|
|
|
| 19 |
from starlette.routing import Route
|
| 20 |
|
| 21 |
try:
|
| 22 |
from ..models import PyreAction, PyreObservation
|
| 23 |
from .pyre_env_environment import PyreEnvironment
|
| 24 |
+
from .narrative import build_narrative_observation, compute_visible_cells
|
| 25 |
except (ImportError, ModuleNotFoundError):
|
| 26 |
from models import PyreAction, PyreObservation
|
| 27 |
from server.pyre_env_environment import PyreEnvironment
|
| 28 |
+
from server.narrative import build_narrative_observation, compute_visible_cells
|
| 29 |
|
| 30 |
MAX_STEPS = int(os.getenv("PYRE_MAX_STEPS", "150"))
|
| 31 |
BASE_SEED = int(os.getenv("PYRE_SEED", "42"))
|
|
|
|
| 85 |
|
| 86 |
@app.get("/")
|
| 87 |
def serve_frontend() -> FileResponse:
|
| 88 |
+
"""Serve the React frontend from server/static/index.html."""
|
| 89 |
+
html_path = STATIC_DIR / "index.html"
|
| 90 |
if not html_path.exists():
|
| 91 |
+
# Fallback to the RPG viewer if index.html is missing
|
| 92 |
+
rpg_path = STATIC_DIR / "viewer_rpg.html"
|
| 93 |
+
if rpg_path.exists():
|
| 94 |
+
return FileResponse(str(rpg_path))
|
| 95 |
+
raise HTTPException(status_code=404, detail="Frontend file not found.")
|
| 96 |
return FileResponse(str(html_path))
|
| 97 |
|
| 98 |
|
| 99 |
+
# Mount the static directory for assets (CSS, JS, etc.)
|
| 100 |
+
if (STATIC_DIR / "assets").exists():
|
| 101 |
+
app.mount("/assets", StaticFiles(directory=str(STATIC_DIR / "assets")), name="assets")
|
| 102 |
+
|
| 103 |
+
|
| 104 |
@app.post("/reset")
|
| 105 |
def reset_episode(body: ResetRequest = ResetRequest()) -> Dict[str, Any]:
|
| 106 |
env = get_stateful_env()
|
|
|
|
| 138 |
return env.state.model_dump()
|
| 139 |
|
| 140 |
|
| 141 |
+
@app.get("/scene")
|
| 142 |
+
def get_scene() -> Dict[str, Any]:
|
| 143 |
+
"""Return a compact scene snapshot for external frontends.
|
| 144 |
+
|
| 145 |
+
Response shape
|
| 146 |
+
--------------
|
| 147 |
+
labels
|
| 148 |
+
agent — position, health, status flags, perception summary
|
| 149 |
+
episode — fire parameters, step counters, difficulty
|
| 150 |
+
map — grid dimensions, exit positions, door registry
|
| 151 |
+
surroundings — visible objects, blocked exits, audible signals,
|
| 152 |
+
available action hints
|
| 153 |
+
graph
|
| 154 |
+
channels — ordered list of channel names (index guide)
|
| 155 |
+
channel_info — human-readable description of each channel
|
| 156 |
+
width / height
|
| 157 |
+
grid — grid[y][x] = [cell_type, fire, smoke, is_agent, is_visible]
|
| 158 |
+
cell_type: 0=floor 1=wall 2=door_open 3=door_closed
|
| 159 |
+
4=exit 5=obstacle
|
| 160 |
+
fire / smoke: 0.0 (none) → 1.0 (max)
|
| 161 |
+
is_agent / is_visible: 0 or 1
|
| 162 |
+
"""
|
| 163 |
+
env = get_stateful_env()
|
| 164 |
+
st = env.state
|
| 165 |
+
|
| 166 |
+
# --- Build structured observation fields (no narrative) ---
|
| 167 |
+
obs_data = build_narrative_observation(
|
| 168 |
+
step_count=st.step_count,
|
| 169 |
+
agent_x=st.agent_x,
|
| 170 |
+
agent_y=st.agent_y,
|
| 171 |
+
agent_alive=st.agent_alive,
|
| 172 |
+
agent_evacuated=st.agent_evacuated,
|
| 173 |
+
agent_health=st.agent_health,
|
| 174 |
+
cell_grid=st.cell_grid,
|
| 175 |
+
fire_grid=st.fire_grid,
|
| 176 |
+
smoke_grid=st.smoke_grid,
|
| 177 |
+
exit_positions=st.exit_positions,
|
| 178 |
+
door_registry=st.door_registry,
|
| 179 |
+
zone_map=st.zone_map,
|
| 180 |
+
last_action_feedback=getattr(env, "_last_feedback", ""),
|
| 181 |
+
wind_dir=st.wind_dir,
|
| 182 |
+
w=st.grid_w,
|
| 183 |
+
h=st.grid_h,
|
| 184 |
+
)
|
| 185 |
+
|
| 186 |
+
# --- Visibility set for the graph layer ---
|
| 187 |
+
if st.agent_alive and not st.agent_evacuated:
|
| 188 |
+
visible_set = compute_visible_cells(
|
| 189 |
+
st.agent_x, st.agent_y,
|
| 190 |
+
st.cell_grid, st.smoke_grid,
|
| 191 |
+
st.grid_w, st.grid_h,
|
| 192 |
+
)
|
| 193 |
+
else:
|
| 194 |
+
visible_set = set()
|
| 195 |
+
|
| 196 |
+
# --- Labels ---
|
| 197 |
+
labels: Dict[str, Any] = {
|
| 198 |
+
"agent": {
|
| 199 |
+
"x": st.agent_x,
|
| 200 |
+
"y": st.agent_y,
|
| 201 |
+
"health": st.agent_health,
|
| 202 |
+
"health_status": obs_data.get("health_status", "Good"),
|
| 203 |
+
"alive": st.agent_alive,
|
| 204 |
+
"evacuated": st.agent_evacuated,
|
| 205 |
+
"location": obs_data.get("location_label", ""),
|
| 206 |
+
"smoke_level": obs_data.get("smoke_level", "none"),
|
| 207 |
+
"fire_visible": obs_data.get("fire_visible", False),
|
| 208 |
+
"fire_direction": obs_data.get("fire_direction", None),
|
| 209 |
+
"last_action_feedback": obs_data.get("last_action_feedback", ""),
|
| 210 |
+
},
|
| 211 |
+
"episode": {
|
| 212 |
+
"id": st.episode_id,
|
| 213 |
+
"step": st.step_count,
|
| 214 |
+
"max_steps": st.max_steps,
|
| 215 |
+
"template": st.template_name,
|
| 216 |
+
"difficulty": getattr(env, "_difficulty", "medium"),
|
| 217 |
+
"wind_dir": st.wind_dir,
|
| 218 |
+
"fire_spread_rate": st.fire_spread_rate,
|
| 219 |
+
"humidity": st.humidity,
|
| 220 |
+
"fire_sources": st.fire_sources_count,
|
| 221 |
+
},
|
| 222 |
+
"map": {
|
| 223 |
+
"width": st.grid_w,
|
| 224 |
+
"height": st.grid_h,
|
| 225 |
+
"exit_positions": st.exit_positions,
|
| 226 |
+
"door_registry": st.door_registry,
|
| 227 |
+
},
|
| 228 |
+
"surroundings": {
|
| 229 |
+
"visible_objects": obs_data.get("visible_objects", []),
|
| 230 |
+
"blocked_exit_ids": obs_data.get("blocked_exit_ids", []),
|
| 231 |
+
"audible_signals": obs_data.get("audible_signals", []),
|
| 232 |
+
"available_actions": obs_data.get("available_actions_hint", []),
|
| 233 |
+
},
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
# --- 2-D multi-channel grid ---
|
| 237 |
+
w, h = st.grid_w, st.grid_h
|
| 238 |
+
grid: List[List[List[float]]] = []
|
| 239 |
+
for y in range(h):
|
| 240 |
+
row: List[List[float]] = []
|
| 241 |
+
for x in range(w):
|
| 242 |
+
idx = y * w + x
|
| 243 |
+
cell_type = float(st.cell_grid[idx])
|
| 244 |
+
fire = round(st.fire_grid[idx], 4)
|
| 245 |
+
smoke = round(st.smoke_grid[idx], 4)
|
| 246 |
+
is_agent = 1.0 if (x == st.agent_x and y == st.agent_y) else 0.0
|
| 247 |
+
is_visible = 1.0 if (x, y) in visible_set else 0.0
|
| 248 |
+
row.append([cell_type, fire, smoke, is_agent, is_visible])
|
| 249 |
+
grid.append(row)
|
| 250 |
+
|
| 251 |
+
graph: Dict[str, Any] = {
|
| 252 |
+
"channels": ["cell_type", "fire", "smoke", "is_agent", "is_visible"],
|
| 253 |
+
"channel_info": {
|
| 254 |
+
"cell_type": "0=floor 1=wall 2=door_open 3=door_closed 4=exit 5=obstacle",
|
| 255 |
+
"fire": "0.0=none to 1.0=fully burning",
|
| 256 |
+
"smoke": "0.0=clear to 1.0=dense smoke",
|
| 257 |
+
"is_agent": "1 if agent occupies this cell, else 0",
|
| 258 |
+
"is_visible": "1 if within agent line-of-sight, else 0",
|
| 259 |
+
},
|
| 260 |
+
"width": w,
|
| 261 |
+
"height": h,
|
| 262 |
+
"grid": grid,
|
| 263 |
+
}
|
| 264 |
+
|
| 265 |
+
return {"labels": labels, "graph": graph}
|
| 266 |
+
|
| 267 |
+
|
| 268 |
def main(host: str = "0.0.0.0", port: int = 8000):
|
| 269 |
import uvicorn
|
| 270 |
port = int(os.getenv("PORT", port))
|
server/floor_plan.py
CHANGED
|
@@ -47,6 +47,7 @@ class FloorPlan:
|
|
| 47 |
spawn_zones: List[Tuple[int, int]] # valid NPC spawn cells
|
| 48 |
agent_spawn_options: List[Tuple[int, int]]
|
| 49 |
zone_map: Dict[str, str] # "{x},{y}" → zone_label
|
|
|
|
| 50 |
fire_min_exit_dist: int = 5 # fire ignition at least this far from any exit
|
| 51 |
fuel_map: List[float] = field(default_factory=list) # fire fuel per cell
|
| 52 |
ventilation_map: List[float] = field(default_factory=list) # smoke decay per cell
|
|
|
|
| 47 |
spawn_zones: List[Tuple[int, int]] # valid NPC spawn cells
|
| 48 |
agent_spawn_options: List[Tuple[int, int]]
|
| 49 |
zone_map: Dict[str, str] # "{x},{y}" → zone_label
|
| 50 |
+
static_objects: Dict[str, str] = field(default_factory=dict) # "{x},{y}" → item_type
|
| 51 |
fire_min_exit_dist: int = 5 # fire ignition at least this far from any exit
|
| 52 |
fuel_map: List[float] = field(default_factory=list) # fire fuel per cell
|
| 53 |
ventilation_map: List[float] = field(default_factory=list) # smoke decay per cell
|
server/pyre_env_environment.py
CHANGED
|
@@ -572,6 +572,7 @@ class PyreEnvironment(Environment):
|
|
| 572 |
is_new_cell=is_new_cell,
|
| 573 |
min_exit_dist_reached=self._min_exit_dist_reached,
|
| 574 |
rewarded_doors=self._rewarded_doors,
|
|
|
|
| 575 |
)
|
| 576 |
|
| 577 |
total = 0.0
|
|
|
|
| 572 |
is_new_cell=is_new_cell,
|
| 573 |
min_exit_dist_reached=self._min_exit_dist_reached,
|
| 574 |
rewarded_doors=self._rewarded_doors,
|
| 575 |
+
reachable_exit_count=len(unblocked_exits(st.exit_positions, st.fire_grid, st.grid_w)),
|
| 576 |
)
|
| 577 |
|
| 578 |
total = 0.0
|