Spaces:
Running
Running
feat: add new units/buildings/map assets, admin routes, and frontend build
Browse files- Add new sprites (goliath, medic, tank, wraith, armory, barracks, etc.)
- Add MAP_half/quarter LOD variants via generate_map_lod script
- Add compiled_map.json and game_positions.json static assets
- Add cover.jpg and HF Spaces logos (LFS-tracked)
- Add admin compiled-map route and layout
- Add map_compiler.py and safe_name.py modules
- Track *.jpg/*.jpeg in Git LFS; remove frontend/build from .gitignore
- Update game engine, pathfinding, tech tree, bot, buildings, commands, units
- Refresh SvelteKit build and type definitions
Made-with: Cursor
This view is limited to 50 files because it contains too many changes. See raw diff
- .env.example +5 -0
- .gitattributes +2 -0
- .gitignore +1 -2
- TODO.md +7 -4
- backend/config.py +5 -0
- backend/game/bot.py +1 -1
- backend/game/buildings.py +19 -8
- backend/game/commands.py +7 -6
- backend/game/engine.py +469 -147
- backend/game/map.py +271 -40
- backend/game/map_compiler.py +157 -0
- backend/game/pathfinding.py +308 -46
- backend/game/state.py +35 -18
- backend/game/tech_tree.py +1 -1
- backend/game/units.py +5 -5
- backend/lobby/manager.py +11 -0
- backend/lobby/safe_name.py +75 -0
- backend/main.py +227 -55
- backend/requirements.txt +2 -0
- backend/scripts/generate_map_lod.py +55 -0
- backend/scripts/generate_sprites.py +276 -58
- backend/scripts/upscale_cover.py +124 -0
- backend/scripts/upscale_map.py +103 -0
- backend/static/MAP.png +2 -2
- backend/static/MAP_half.png +3 -0
- backend/static/MAP_quarter.png +3 -0
- backend/static/compiled_map.json +4211 -0
- backend/static/game_positions.json +362 -0
- backend/static/sprites/buildings/armory.png +3 -0
- backend/static/sprites/buildings/barracks.png +3 -0
- backend/static/sprites/buildings/command_center.png +2 -2
- backend/static/sprites/buildings/engineering_bay.png +3 -0
- backend/static/sprites/buildings/factory.png +3 -0
- backend/static/sprites/buildings/refinery.png +3 -0
- backend/static/sprites/buildings/starport.png +3 -0
- backend/static/sprites/buildings/supply_depot.png +3 -0
- backend/static/sprites/icons/gas.png +3 -0
- backend/static/sprites/icons/mineral.png +3 -0
- backend/static/sprites/icons/supply.png +3 -0
- backend/static/sprites/resources/geyser.png +3 -0
- backend/static/sprites/resources/mineral.png +3 -0
- backend/static/sprites/units/goliath.png +3 -0
- backend/static/sprites/units/marine.png +2 -2
- backend/static/sprites/units/medic.png +3 -0
- backend/static/sprites/units/scv.png +2 -2
- backend/static/sprites/units/tank.png +3 -0
- backend/static/sprites/units/wraith.png +3 -0
- backend/static/walkable.json +34 -34
- backend/voice/command_parser.py +78 -12
- frontend/.svelte-kit/non-ambient.d.ts +4 -3
.env.example
CHANGED
|
@@ -1,2 +1,7 @@
|
|
| 1 |
MISTRAL_API_KEY=your_mistral_api_key_here
|
| 2 |
SECRET_KEY=change-me-in-production
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
MISTRAL_API_KEY=your_mistral_api_key_here
|
| 2 |
SECRET_KEY=change-me-in-production
|
| 3 |
+
|
| 4 |
+
# Optionnel : génération sprites via Vertex AI Imagen (ex. project GCP temporaire)
|
| 5 |
+
# Si défini, le script generate_sprites utilise Imagen au lieu de Mistral. À retirer en prod.
|
| 6 |
+
# GCP_PROJECT_ID=mtgbinder
|
| 7 |
+
# GCP_LOCATION=us-central1
|
.gitattributes
CHANGED
|
@@ -1,2 +1,4 @@
|
|
| 1 |
*.png filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.PNG filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 1 |
*.png filter=lfs diff=lfs merge=lfs -text
|
| 2 |
*.PNG filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.jpeg filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.jpg filter=lfs diff=lfs merge=lfs -text
|
.gitignore
CHANGED
|
@@ -6,5 +6,4 @@ __pycache__/
|
|
| 6 |
**/*.pyc
|
| 7 |
**/__pycache__/
|
| 8 |
frontend/.svelte-kit/generated/
|
| 9 |
-
frontend/.svelte-kit/output/
|
| 10 |
-
frontend/build/
|
|
|
|
| 6 |
**/*.pyc
|
| 7 |
**/__pycache__/
|
| 8 |
frontend/.svelte-kit/generated/
|
| 9 |
+
frontend/.svelte-kit/output/
|
|
|
TODO.md
CHANGED
|
@@ -1,4 +1,7 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
Ajouter PVs unités
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
+ Refaire la landing page pour etre plus précis engageant design plus cool etc... expliquer dans la peau d'un général
|
| 2 |
+
+ AJouter detection safename
|
| 3 |
+
+ Ajouter observing des matches existants si trop de matches en cours.
|
| 4 |
+
Ajouter PVs unités
|
| 5 |
+
utiliser poisiotns de départ et placement des minéraux
|
| 6 |
+
pathfinding
|
| 7 |
+
shoot animation
|
backend/config.py
CHANGED
|
@@ -17,6 +17,11 @@ VOXTRAL_REALTIME_MODEL: str = "voxtral-mini-transcribe-realtime-2602" # streami
|
|
| 17 |
# Mistral LLM model for command parsing
|
| 18 |
MISTRAL_CHAT_MODEL: str = "mistral-large-latest"
|
| 19 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 20 |
# Game constants
|
| 21 |
TICK_INTERVAL: float = 0.25 # seconds per game tick
|
| 22 |
TICKS_PER_SECOND: int = 4
|
|
|
|
| 17 |
# Mistral LLM model for command parsing
|
| 18 |
MISTRAL_CHAT_MODEL: str = "mistral-large-latest"
|
| 19 |
|
| 20 |
+
# GCP Vertex AI Imagen (optionnel, pour génération sprites si Mistral restreint)
|
| 21 |
+
# Ex: project mtgbinder — à retirer en prod
|
| 22 |
+
GCP_PROJECT_ID: str = os.getenv("GCP_PROJECT_ID", "")
|
| 23 |
+
GCP_LOCATION: str = os.getenv("GCP_LOCATION", "us-central1")
|
| 24 |
+
|
| 25 |
# Game constants
|
| 26 |
TICK_INTERVAL: float = 0.25 # seconds per game tick
|
| 27 |
TICKS_PER_SECOND: int = 4
|
backend/game/bot.py
CHANGED
|
@@ -31,7 +31,7 @@ BOT_TICK_INTERVAL = 8
|
|
| 31 |
|
| 32 |
|
| 33 |
def _cmd(*actions: GameAction) -> ParsedCommand:
|
| 34 |
-
return ParsedCommand(actions=list(actions),
|
| 35 |
|
| 36 |
|
| 37 |
class BotPlayer:
|
|
|
|
| 31 |
|
| 32 |
|
| 33 |
def _cmd(*actions: GameAction) -> ParsedCommand:
|
| 34 |
+
return ParsedCommand(actions=list(actions), feedback_template="")
|
| 35 |
|
| 36 |
|
| 37 |
class BotPlayer:
|
backend/game/buildings.py
CHANGED
|
@@ -33,6 +33,17 @@ class BuildingDef(BaseModel):
|
|
| 33 |
width: int = 2
|
| 34 |
height: int = 2
|
| 35 |
supply_provided: int = 0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
|
| 37 |
|
| 38 |
BUILDING_DEFS: dict[BuildingType, BuildingDef] = {
|
|
@@ -54,7 +65,7 @@ BUILDING_DEFS: dict[BuildingType, BuildingDef] = {
|
|
| 54 |
),
|
| 55 |
BuildingType.REFINERY: BuildingDef(
|
| 56 |
max_hp=500, mineral_cost=100, gas_cost=0, build_time_ticks=40,
|
| 57 |
-
width=2, height=2,
|
| 58 |
),
|
| 59 |
BuildingType.FACTORY: BuildingDef(
|
| 60 |
max_hp=1250, mineral_cost=200, gas_cost=100, build_time_ticks=80,
|
|
@@ -81,8 +92,8 @@ class Building(BaseModel):
|
|
| 81 |
id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
|
| 82 |
building_type: BuildingType
|
| 83 |
owner: str
|
| 84 |
-
x:
|
| 85 |
-
y:
|
| 86 |
hp: float
|
| 87 |
max_hp: int
|
| 88 |
status: BuildingStatus = BuildingStatus.CONSTRUCTING
|
|
@@ -93,20 +104,20 @@ class Building(BaseModel):
|
|
| 93 |
rally_y: Optional[float] = None
|
| 94 |
|
| 95 |
@classmethod
|
| 96 |
-
def create(cls, bt: BuildingType, owner: str, x:
|
| 97 |
defn = BUILDING_DEFS[bt]
|
| 98 |
return cls(
|
| 99 |
building_type=bt,
|
| 100 |
owner=owner,
|
| 101 |
x=x,
|
| 102 |
y=y,
|
| 103 |
-
hp=float(defn.max_hp),
|
| 104 |
max_hp=defn.max_hp,
|
| 105 |
construction_ticks_remaining=defn.build_time_ticks,
|
| 106 |
-
|
| 107 |
)
|
| 108 |
|
| 109 |
def spawn_point(self) -> tuple[float, float]:
|
| 110 |
-
"""Position where units appear when produced."""
|
| 111 |
defn = BUILDING_DEFS[self.building_type]
|
| 112 |
-
return (
|
|
|
|
| 33 |
width: int = 2
|
| 34 |
height: int = 2
|
| 35 |
supply_provided: int = 0
|
| 36 |
+
# Collision box is shrunk by this amount on each side vs. the visual box,
|
| 37 |
+
# so units can squeeze between buildings that have a small visual gap.
|
| 38 |
+
collision_shrink: float = 0.4
|
| 39 |
+
|
| 40 |
+
def col_hw(self) -> float:
|
| 41 |
+
"""Half collision width."""
|
| 42 |
+
return max(0.5, self.width / 2 - self.collision_shrink)
|
| 43 |
+
|
| 44 |
+
def col_hh(self) -> float:
|
| 45 |
+
"""Half collision height."""
|
| 46 |
+
return max(0.5, self.height / 2 - self.collision_shrink)
|
| 47 |
|
| 48 |
|
| 49 |
BUILDING_DEFS: dict[BuildingType, BuildingDef] = {
|
|
|
|
| 65 |
),
|
| 66 |
BuildingType.REFINERY: BuildingDef(
|
| 67 |
max_hp=500, mineral_cost=100, gas_cost=0, build_time_ticks=40,
|
| 68 |
+
width=2, height=2, collision_shrink=0.2,
|
| 69 |
),
|
| 70 |
BuildingType.FACTORY: BuildingDef(
|
| 71 |
max_hp=1250, mineral_cost=200, gas_cost=100, build_time_ticks=80,
|
|
|
|
| 92 |
id: str = Field(default_factory=lambda: str(uuid.uuid4())[:8])
|
| 93 |
building_type: BuildingType
|
| 94 |
owner: str
|
| 95 |
+
x: float # center x
|
| 96 |
+
y: float # center y
|
| 97 |
hp: float
|
| 98 |
max_hp: int
|
| 99 |
status: BuildingStatus = BuildingStatus.CONSTRUCTING
|
|
|
|
| 104 |
rally_y: Optional[float] = None
|
| 105 |
|
| 106 |
@classmethod
|
| 107 |
+
def create(cls, bt: BuildingType, owner: str, x: float, y: float) -> "Building":
|
| 108 |
defn = BUILDING_DEFS[bt]
|
| 109 |
return cls(
|
| 110 |
building_type=bt,
|
| 111 |
owner=owner,
|
| 112 |
x=x,
|
| 113 |
y=y,
|
| 114 |
+
hp=float(defn.max_hp) * 0.15,
|
| 115 |
max_hp=defn.max_hp,
|
| 116 |
construction_ticks_remaining=defn.build_time_ticks,
|
| 117 |
+
construction_max_ticks=defn.build_time_ticks,
|
| 118 |
)
|
| 119 |
|
| 120 |
def spawn_point(self) -> tuple[float, float]:
|
| 121 |
+
"""Position where units appear when produced (below bottom edge, horizontally centered)."""
|
| 122 |
defn = BUILDING_DEFS[self.building_type]
|
| 123 |
+
return (self.x, self.y + defn.height / 2 + 1.0)
|
backend/game/commands.py
CHANGED
|
@@ -6,9 +6,9 @@ given to Mistral in voice/command_parser.py.
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
from enum import Enum
|
| 9 |
-
from typing import Optional
|
| 10 |
|
| 11 |
-
from pydantic import BaseModel
|
| 12 |
|
| 13 |
|
| 14 |
class ActionType(str, Enum):
|
|
@@ -68,19 +68,20 @@ class GameAction(BaseModel):
|
|
| 68 |
|
| 69 |
|
| 70 |
class ParsedCommand(BaseModel):
|
| 71 |
-
"""Full Mistral response: one or more actions +
|
| 72 |
actions: list[GameAction]
|
| 73 |
-
|
|
|
|
| 74 |
|
| 75 |
|
| 76 |
class ActionResult(BaseModel):
|
| 77 |
action_type: str
|
| 78 |
success: bool
|
| 79 |
-
|
| 80 |
sound_events: list[dict] = [] # e.g. [{"kind": "move_ack", "unit_type": "marine"}]
|
| 81 |
unit_ids: list[str] = [] # for query_units: list of unit IDs matching the filter
|
| 82 |
|
| 83 |
|
| 84 |
class CommandResult(BaseModel):
|
| 85 |
results: list[ActionResult]
|
| 86 |
-
feedback_override: Optional[str] = None #
|
|
|
|
| 6 |
"""
|
| 7 |
|
| 8 |
from enum import Enum
|
| 9 |
+
from typing import Any, Optional
|
| 10 |
|
| 11 |
+
from pydantic import BaseModel, Field
|
| 12 |
|
| 13 |
|
| 14 |
class ActionType(str, Enum):
|
|
|
|
| 68 |
|
| 69 |
|
| 70 |
class ParsedCommand(BaseModel):
|
| 71 |
+
"""Full Mistral response: one or more actions + feedback template to fill with game data."""
|
| 72 |
actions: list[GameAction]
|
| 73 |
+
feedback_template: str # sentence in command language with placeholders {n}, {zone}, {building}, etc.
|
| 74 |
+
language: str = "fr" # ISO code of the user's command language (fr, en, ...)
|
| 75 |
|
| 76 |
|
| 77 |
class ActionResult(BaseModel):
|
| 78 |
action_type: str
|
| 79 |
success: bool
|
| 80 |
+
data: dict[str, Any] = Field(default_factory=dict) # template variables: n, zone, building, count, summary, ...
|
| 81 |
sound_events: list[dict] = [] # e.g. [{"kind": "move_ack", "unit_type": "marine"}]
|
| 82 |
unit_ids: list[str] = [] # for query_units: list of unit IDs matching the filter
|
| 83 |
|
| 84 |
|
| 85 |
class CommandResult(BaseModel):
|
| 86 |
results: list[ActionResult]
|
| 87 |
+
feedback_override: Optional[str] = None # error key for API to generate message (e.g. "game_not_in_progress")
|
backend/game/engine.py
CHANGED
|
@@ -22,7 +22,7 @@ from .bot import BOT_TICK_INTERVAL, BotPlayer
|
|
| 22 |
from .buildings import Building, BuildingDef, BuildingStatus, BuildingType, BUILDING_DEFS
|
| 23 |
from .commands import ActionResult, ActionType, CommandResult, GameAction, ParsedCommand
|
| 24 |
from .map import MAP_HEIGHT, MAP_WIDTH, ResourceType
|
| 25 |
-
from .pathfinding import find_path, is_walkable, snap_to_walkable
|
| 26 |
from .state import GamePhase, GameState, PlayerState
|
| 27 |
from .tech_tree import can_build, can_train, get_producer, missing_for_build, missing_for_train
|
| 28 |
from .units import Unit, UnitDef, UnitStatus, UnitType, UNIT_DEFS
|
|
@@ -47,6 +47,7 @@ class GameEngine:
|
|
| 47 |
self._task: Optional[asyncio.Task] = None # type: ignore[type-arg]
|
| 48 |
self.bot: Optional[BotPlayer] = None
|
| 49 |
self._sound_events: list[dict] = [] # fire/death per tick, sent in game_update
|
|
|
|
| 50 |
|
| 51 |
# ------------------------------------------------------------------
|
| 52 |
# Lifecycle
|
|
@@ -68,9 +69,10 @@ class GameEngine:
|
|
| 68 |
# ------------------------------------------------------------------
|
| 69 |
|
| 70 |
def apply_command(self, player_id: str, parsed: ParsedCommand) -> CommandResult:
|
|
|
|
| 71 |
player = self.state.players.get(player_id)
|
| 72 |
if not player or self.state.phase != GamePhase.PLAYING:
|
| 73 |
-
return CommandResult(results=[], feedback_override="
|
| 74 |
|
| 75 |
results: list[ActionResult] = []
|
| 76 |
last_query_unit_ids: Optional[list[str]] = None
|
|
@@ -137,20 +139,44 @@ class GameEngine:
|
|
| 137 |
for building in player.buildings.values():
|
| 138 |
if building.status != BuildingStatus.CONSTRUCTING:
|
| 139 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
building.construction_ticks_remaining -= 1
|
|
|
|
|
|
|
|
|
|
| 141 |
if building.construction_ticks_remaining <= 0:
|
| 142 |
building.status = BuildingStatus.ACTIVE
|
| 143 |
building.construction_ticks_remaining = 0
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 150 |
|
| 151 |
def _tick_production(self, player: PlayerState) -> None:
|
| 152 |
"""Tick building production queues and spawn units."""
|
| 153 |
for building in player.buildings.values():
|
|
|
|
|
|
|
| 154 |
if not building.production_queue:
|
| 155 |
building.status = BuildingStatus.ACTIVE
|
| 156 |
continue
|
|
@@ -166,13 +192,19 @@ class GameEngine:
|
|
| 166 |
self._spawn_unit(player, building, UnitType(item.unit_type))
|
| 167 |
|
| 168 |
def _spawn_unit(self, player: PlayerState, building: Building, ut: UnitType) -> None:
|
| 169 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 170 |
tx = building.rally_x if building.rally_x is not None else sx
|
| 171 |
ty = building.rally_y if building.rally_y is not None else sy
|
| 172 |
unit = Unit.create(ut, player.player_id, sx, sy)
|
| 173 |
if tx != sx or ty != sy:
|
| 174 |
unit.status = UnitStatus.MOVING
|
| 175 |
-
self._set_unit_destination(unit, tx, ty, is_flying=
|
| 176 |
player.units[unit.id] = unit
|
| 177 |
|
| 178 |
def _tick_mining(self, player: PlayerState) -> None:
|
|
@@ -193,36 +225,47 @@ class GameEngine:
|
|
| 193 |
unit.harvest_carry = False
|
| 194 |
return
|
| 195 |
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
arrive_dist = 1.5
|
| 199 |
|
| 200 |
for unit in player.units.values():
|
| 201 |
if unit.status == UnitStatus.MINING_MINERALS:
|
| 202 |
resource = self.state.game_map.get_resource(unit.assigned_resource_id or "")
|
| 203 |
if not resource or resource.is_depleted:
|
|
|
|
|
|
|
|
|
|
| 204 |
unit.assigned_resource_id = None
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
continue
|
| 210 |
|
| 211 |
rx, ry = float(resource.x), float(resource.y)
|
| 212 |
|
| 213 |
if not unit.harvest_carry:
|
| 214 |
-
if unit.target_x
|
| 215 |
self._set_unit_destination(unit, rx, ry, is_flying=False)
|
| 216 |
-
if unit.dist_to(rx, ry) <=
|
| 217 |
gathered = min(MINERAL_PER_HARVEST, resource.amount)
|
| 218 |
resource.amount -= gathered
|
| 219 |
unit.harvest_carry = True
|
| 220 |
unit.harvest_amount = gathered
|
| 221 |
-
self.
|
|
|
|
| 222 |
else:
|
| 223 |
-
if unit.target_x
|
| 224 |
-
self.
|
| 225 |
-
|
|
|
|
| 226 |
player.minerals += unit.harvest_amount
|
| 227 |
unit.harvest_carry = False
|
| 228 |
unit.harvest_amount = 0
|
|
@@ -241,16 +284,18 @@ class GameEngine:
|
|
| 241 |
rx, ry = float(resource.x), float(resource.y)
|
| 242 |
|
| 243 |
if not unit.harvest_carry:
|
| 244 |
-
if unit.target_x
|
| 245 |
self._set_unit_destination(unit, rx, ry, is_flying=False)
|
| 246 |
-
if unit.dist_to(rx, ry) <=
|
| 247 |
unit.harvest_carry = True
|
| 248 |
unit.harvest_amount = GAS_PER_HARVEST
|
| 249 |
-
self.
|
|
|
|
| 250 |
else:
|
| 251 |
-
if unit.target_x
|
| 252 |
-
self.
|
| 253 |
-
|
|
|
|
| 254 |
player.gas += unit.harvest_amount
|
| 255 |
unit.harvest_carry = False
|
| 256 |
unit.harvest_amount = 0
|
|
@@ -280,6 +325,10 @@ class GameEngine:
|
|
| 280 |
# Building SCVs stay put; mining SCVs move but skip combat
|
| 281 |
if unit.status == UnitStatus.BUILDING:
|
| 282 |
continue
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
if unit.status in (UnitStatus.MINING_MINERALS, UnitStatus.MINING_GAS):
|
| 284 |
if unit.target_x is not None and unit.target_y is not None:
|
| 285 |
self._move_toward(unit, defn, unit.target_x, unit.target_y)
|
|
@@ -325,6 +374,22 @@ class GameEngine:
|
|
| 325 |
if unit.attack_target_id or unit.attack_target_building_id:
|
| 326 |
self._combat_attack(unit, defn, all_units, player, enemy, sieged=False)
|
| 327 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 328 |
def _set_unit_destination(
|
| 329 |
self, unit: Unit, tx: float, ty: float, *, is_flying: bool
|
| 330 |
) -> None:
|
|
@@ -334,40 +399,157 @@ class GameEngine:
|
|
| 334 |
unit.path_waypoints = []
|
| 335 |
if is_flying:
|
| 336 |
return
|
| 337 |
-
|
|
|
|
|
|
|
| 338 |
if path is None:
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 342 |
if not path:
|
| 343 |
return
|
| 344 |
unit.target_x, unit.target_y = path[0][0], path[0][1]
|
| 345 |
unit.path_waypoints = [[p[0], p[1]] for p in path[1:]]
|
| 346 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 347 |
def _move_toward(self, unit: Unit, defn: UnitDef, tx: float, ty: float) -> None:
|
| 348 |
-
|
| 349 |
-
dy = ty - unit.y
|
| 350 |
-
dist = math.sqrt(dx * dx + dy * dy)
|
| 351 |
step = defn.move_speed * TICK_INTERVAL
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 368 |
else:
|
| 369 |
-
unit.
|
| 370 |
-
unit.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 371 |
|
| 372 |
def _combat_attack(
|
| 373 |
self,
|
|
@@ -517,18 +699,38 @@ class GameEngine:
|
|
| 517 |
# ------------------------------------------------------------------
|
| 518 |
|
| 519 |
def _building_center(self, b: Building) -> tuple[float, float]:
|
| 520 |
-
|
| 521 |
-
return (float(b.x) + defn.width / 2, float(b.y) + defn.height / 2)
|
| 522 |
|
| 523 |
def _dist_unit_to_building(self, unit: Unit, building: Building) -> float:
|
| 524 |
-
"""Distance from unit center to nearest point on building
|
| 525 |
defn = BUILDING_DEFS[building.building_type]
|
| 526 |
-
|
| 527 |
-
|
|
|
|
| 528 |
px = max(x0, min(x1, unit.x))
|
| 529 |
py = max(y0, min(y1, unit.y))
|
| 530 |
return math.sqrt((unit.x - px) ** 2 + (unit.y - py) ** 2)
|
| 531 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
def _nearest_enemy_in_range(
|
| 533 |
self,
|
| 534 |
unit: Unit,
|
|
@@ -557,17 +759,18 @@ class GameEngine:
|
|
| 557 |
return best[1] if best else None
|
| 558 |
|
| 559 |
def _resolve_zone(self, player_id: str, zone: str) -> tuple[float, float]:
|
|
|
|
| 560 |
player = self.state.players[player_id]
|
| 561 |
enemy = self.state.enemy_of(player_id)
|
| 562 |
cc = player.command_center()
|
| 563 |
-
base_x =
|
| 564 |
-
base_y =
|
| 565 |
|
| 566 |
if zone == "my_base":
|
| 567 |
return (base_x, base_y)
|
| 568 |
if zone == "enemy_base" and enemy:
|
| 569 |
ecc = enemy.command_center()
|
| 570 |
-
return (
|
| 571 |
if zone == "center":
|
| 572 |
return (MAP_WIDTH / 2, MAP_HEIGHT / 2)
|
| 573 |
if zone == "top_left":
|
|
@@ -587,6 +790,29 @@ class GameEngine:
|
|
| 587 |
avg_x = sum(u.x for u in military) / len(military)
|
| 588 |
avg_y = sum(u.y for u in military) / len(military)
|
| 589 |
return (avg_x, avg_y)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 590 |
# Fallback: enemy base
|
| 591 |
if enemy:
|
| 592 |
ecc = enemy.command_center()
|
|
@@ -635,38 +861,71 @@ class GameEngine:
|
|
| 635 |
pass
|
| 636 |
return [u.id for u in units]
|
| 637 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 638 |
def _find_build_position(
|
| 639 |
-
self, player: PlayerState, bt: BuildingType
|
| 640 |
-
) -> Optional[tuple[
|
| 641 |
-
|
| 642 |
-
if not cc:
|
| 643 |
-
return None
|
| 644 |
-
origin_x, origin_y = cc.x, cc.y
|
| 645 |
defn = BUILDING_DEFS[bt]
|
|
|
|
| 646 |
|
| 647 |
-
for radius in range(
|
| 648 |
for dx in range(-radius, radius + 1):
|
| 649 |
for dy in range(-radius, radius + 1):
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 653 |
return None
|
| 654 |
|
| 655 |
-
def _can_place(self,
|
| 656 |
-
if
|
|
|
|
| 657 |
return False
|
| 658 |
-
# Check overlap with
|
| 659 |
for player in self.state.players.values():
|
| 660 |
for b in player.buildings.values():
|
| 661 |
if b.status == BuildingStatus.DESTROYED:
|
| 662 |
continue
|
| 663 |
bd = BUILDING_DEFS[b.building_type]
|
| 664 |
-
if
|
| 665 |
-
and
|
| 666 |
return False
|
| 667 |
# Check overlap with resources
|
| 668 |
for res in self.state.game_map.resources:
|
| 669 |
-
if
|
| 670 |
return False
|
| 671 |
return True
|
| 672 |
|
|
@@ -705,30 +964,37 @@ class GameEngine:
|
|
| 705 |
return self._cmd_query_units(player, action)
|
| 706 |
if t == ActionType.ASSIGN_TO_GROUP:
|
| 707 |
return self._cmd_assign_to_group(player, action)
|
| 708 |
-
return ActionResult(
|
|
|
|
|
|
|
| 709 |
except Exception as exc:
|
| 710 |
log.exception("Error applying action %s", action.type)
|
| 711 |
-
return ActionResult(
|
|
|
|
|
|
|
| 712 |
|
| 713 |
def _cmd_build(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 714 |
raw = action.building_type
|
| 715 |
if not raw:
|
| 716 |
-
return ActionResult(action_type="build", success=False,
|
| 717 |
try:
|
| 718 |
bt = BuildingType(raw)
|
| 719 |
except ValueError:
|
| 720 |
-
return ActionResult(action_type="build", success=False,
|
| 721 |
|
| 722 |
if not can_build(bt, player):
|
| 723 |
missing = missing_for_build(bt, player)
|
| 724 |
names = ", ".join(m.value for m in missing)
|
| 725 |
-
return ActionResult(
|
| 726 |
-
|
|
|
|
| 727 |
|
| 728 |
defn = BUILDING_DEFS[bt]
|
| 729 |
if player.minerals < defn.mineral_cost or player.gas < defn.gas_cost:
|
| 730 |
-
return ActionResult(
|
| 731 |
-
|
|
|
|
|
|
|
| 732 |
|
| 733 |
# Clamp count to what resources and SCVs allow
|
| 734 |
count = max(1, min(action.count, 5))
|
|
@@ -745,27 +1011,31 @@ class GameEngine:
|
|
| 745 |
)
|
| 746 |
count = min(count, len(available_scvs))
|
| 747 |
if count == 0:
|
| 748 |
-
return ActionResult(action_type="build", success=False,
|
| 749 |
|
| 750 |
cc = player.command_center()
|
| 751 |
cx, cy = (float(cc.x), float(cc.y)) if cc else (0.0, 0.0)
|
| 752 |
|
| 753 |
built = 0
|
| 754 |
for i in range(count):
|
| 755 |
-
|
|
|
|
|
|
|
| 756 |
if bt == BuildingType.REFINERY:
|
| 757 |
geyser = self.state.game_map.nearest_geyser_without_refinery(cx, cy)
|
| 758 |
if not geyser:
|
| 759 |
break
|
| 760 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 761 |
geyser.has_refinery = True
|
| 762 |
else:
|
| 763 |
-
pos_opt = self._find_build_position(player, bt)
|
| 764 |
if not pos_opt:
|
| 765 |
break
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
scv = available_scvs[i]
|
| 769 |
# Unassign from resource if mining
|
| 770 |
if scv.assigned_resource_id:
|
| 771 |
res = self.state.game_map.get_resource(scv.assigned_resource_id)
|
|
@@ -778,62 +1048,80 @@ class GameEngine:
|
|
| 778 |
player.minerals -= defn.mineral_cost
|
| 779 |
player.gas -= defn.gas_cost
|
| 780 |
|
| 781 |
-
building = Building.create(bt, player.player_id,
|
| 782 |
player.buildings[building.id] = building
|
| 783 |
|
| 784 |
-
scv.status = UnitStatus.
|
| 785 |
scv.building_target_id = building.id
|
| 786 |
-
|
| 787 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 788 |
built += 1
|
| 789 |
|
| 790 |
if built == 0:
|
| 791 |
-
return ActionResult(action_type="build", success=False,
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
|
|
|
| 795 |
|
| 796 |
def _cmd_train(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 797 |
raw = action.unit_type
|
| 798 |
if not raw:
|
| 799 |
-
return ActionResult(action_type="train", success=False,
|
| 800 |
try:
|
| 801 |
ut = UnitType(raw)
|
| 802 |
except ValueError:
|
| 803 |
-
return ActionResult(action_type="train", success=False,
|
| 804 |
|
| 805 |
if not can_train(ut, player):
|
| 806 |
missing = missing_for_train(ut, player)
|
| 807 |
names = ", ".join(m.value for m in missing)
|
| 808 |
-
return ActionResult(
|
| 809 |
-
|
|
|
|
| 810 |
|
| 811 |
defn = UNIT_DEFS[ut]
|
| 812 |
producer_type = get_producer(ut)
|
| 813 |
producers = player.active_buildings_of(producer_type)
|
| 814 |
if not producers:
|
| 815 |
-
return ActionResult(
|
| 816 |
-
|
|
|
|
|
|
|
| 817 |
|
| 818 |
count = max(1, min(action.count, 20))
|
| 819 |
num_producers = len(producers)
|
| 820 |
|
| 821 |
-
# Resources are checked for the full requested count
|
| 822 |
total_minerals = defn.mineral_cost * count
|
| 823 |
total_gas = defn.gas_cost * count
|
| 824 |
if player.minerals < total_minerals or player.gas < total_gas:
|
| 825 |
-
return ActionResult(
|
| 826 |
-
|
|
|
|
| 827 |
|
| 828 |
-
# Supply check: account for actual units + already-queued units + new batch
|
| 829 |
queued_supply = sum(
|
| 830 |
UNIT_DEFS[UnitType(item.unit_type)].supply_cost
|
| 831 |
for b in player.buildings.values()
|
| 832 |
for item in b.production_queue
|
| 833 |
)
|
| 834 |
if player.supply_used + queued_supply + defn.supply_cost * count > player.supply_max:
|
| 835 |
-
return ActionResult(
|
| 836 |
-
|
|
|
|
| 837 |
|
| 838 |
from .buildings import ProductionItem # local import to avoid cycle
|
| 839 |
for i in range(count):
|
|
@@ -849,13 +1137,15 @@ class GameEngine:
|
|
| 849 |
player.minerals -= total_minerals
|
| 850 |
player.gas -= total_gas
|
| 851 |
|
| 852 |
-
return ActionResult(
|
| 853 |
-
|
|
|
|
|
|
|
| 854 |
|
| 855 |
def _cmd_move(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 856 |
units = self._resolve_selector(player, action.unit_selector or "all_military")
|
| 857 |
if not units:
|
| 858 |
-
return ActionResult(action_type="move", success=False,
|
| 859 |
tx, ty = self._resolve_zone(player.player_id, action.target_zone or "center")
|
| 860 |
for unit in units:
|
| 861 |
if unit.is_sieged:
|
|
@@ -865,14 +1155,16 @@ class GameEngine:
|
|
| 865 |
unit.attack_target_building_id = None
|
| 866 |
self._set_unit_destination(unit, tx, ty, is_flying=UNIT_DEFS[unit.unit_type].is_flying)
|
| 867 |
move_ack = [{"kind": "move_ack", "unit_type": units[0].unit_type.value}]
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
|
|
|
|
|
|
| 871 |
|
| 872 |
def _cmd_attack(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 873 |
units = self._resolve_selector(player, action.unit_selector or "all_military")
|
| 874 |
if not units:
|
| 875 |
-
return ActionResult(action_type="attack", success=False,
|
| 876 |
tx, ty = self._resolve_zone(player.player_id, action.target_zone or "enemy_base")
|
| 877 |
for unit in units:
|
| 878 |
if unit.is_sieged:
|
|
@@ -882,34 +1174,40 @@ class GameEngine:
|
|
| 882 |
unit.attack_target_building_id = None
|
| 883 |
self._set_unit_destination(unit, tx, ty, is_flying=UNIT_DEFS[unit.unit_type].is_flying)
|
| 884 |
move_ack = [{"kind": "move_ack", "unit_type": units[0].unit_type.value}]
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
|
|
|
|
|
|
| 888 |
|
| 889 |
def _cmd_siege(self, player: PlayerState, action: GameAction, siege: bool) -> ActionResult:
|
| 890 |
tanks = self._resolve_selector(player, action.unit_selector or "all_tanks")
|
| 891 |
tanks = [u for u in tanks if u.unit_type == UnitType.TANK]
|
| 892 |
if not tanks:
|
| 893 |
-
return ActionResult(action_type="siege", success=False,
|
| 894 |
for tank in tanks:
|
| 895 |
tank.is_sieged = siege
|
| 896 |
tank.status = UnitStatus.SIEGED if siege else UnitStatus.IDLE
|
| 897 |
if siege:
|
| 898 |
tank.target_x = tank.target_y = None
|
| 899 |
-
mode = "
|
| 900 |
-
return ActionResult(
|
| 901 |
-
|
|
|
|
|
|
|
| 902 |
|
| 903 |
def _cmd_cloak(self, player: PlayerState, action: GameAction, cloak: bool) -> ActionResult:
|
| 904 |
wraiths = self._resolve_selector(player, action.unit_selector or "all_wraiths")
|
| 905 |
wraiths = [u for u in wraiths if u.unit_type == UnitType.WRAITH]
|
| 906 |
if not wraiths:
|
| 907 |
-
return ActionResult(action_type="cloak", success=False,
|
| 908 |
for wraith in wraiths:
|
| 909 |
wraith.is_cloaked = cloak
|
| 910 |
-
state = "
|
| 911 |
-
return ActionResult(
|
| 912 |
-
|
|
|
|
|
|
|
| 913 |
|
| 914 |
def _cmd_gather(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 915 |
resource_type = (action.resource_type or "minerals").lower()
|
|
@@ -919,7 +1217,7 @@ class GameEngine:
|
|
| 919 |
scvs = self._resolve_selector(player, action.unit_selector or "idle_scv")
|
| 920 |
scvs = [u for u in scvs if u.unit_type == UnitType.SCV]
|
| 921 |
if not scvs:
|
| 922 |
-
return ActionResult(action_type="gather", success=False,
|
| 923 |
|
| 924 |
assigned = 0
|
| 925 |
if resource_type == "gas":
|
|
@@ -940,8 +1238,23 @@ class GameEngine:
|
|
| 940 |
geyser.assigned_scv_ids.append(scv.id)
|
| 941 |
assigned += 1
|
| 942 |
else:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 943 |
for scv in scvs:
|
| 944 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 945 |
if not patch:
|
| 946 |
break
|
| 947 |
if scv.assigned_resource_id:
|
|
@@ -955,12 +1268,17 @@ class GameEngine:
|
|
| 955 |
self._set_unit_destination(scv, float(patch.x), float(patch.y), is_flying=False)
|
| 956 |
patch.assigned_scv_ids.append(scv.id)
|
| 957 |
assigned += 1
|
|
|
|
|
|
|
| 958 |
|
| 959 |
if assigned == 0:
|
| 960 |
-
return ActionResult(
|
| 961 |
-
|
| 962 |
-
|
| 963 |
-
|
|
|
|
|
|
|
|
|
|
| 964 |
|
| 965 |
def _cmd_stop(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 966 |
units = self._resolve_selector(player, action.unit_selector or "all_military")
|
|
@@ -970,13 +1288,12 @@ class GameEngine:
|
|
| 970 |
unit.path_waypoints = []
|
| 971 |
unit.attack_target_id = None
|
| 972 |
unit.attack_target_building_id = None
|
| 973 |
-
return ActionResult(action_type="stop", success=True,
|
| 974 |
-
message=f"{len(units)} unité(s) stoppées.")
|
| 975 |
|
| 976 |
def _cmd_patrol(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 977 |
units = self._resolve_selector(player, action.unit_selector or "all_military")
|
| 978 |
if not units:
|
| 979 |
-
return ActionResult(action_type="patrol", success=False,
|
| 980 |
tx, ty = self._resolve_zone(player.player_id, action.target_zone or "center")
|
| 981 |
for unit in units:
|
| 982 |
if unit.is_sieged:
|
|
@@ -986,12 +1303,17 @@ class GameEngine:
|
|
| 986 |
unit.status = UnitStatus.PATROLLING
|
| 987 |
self._set_unit_destination(unit, tx, ty, is_flying=UNIT_DEFS[unit.unit_type].is_flying)
|
| 988 |
move_ack = [{"kind": "move_ack", "unit_type": units[0].unit_type.value}]
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
|
|
|
|
|
|
| 992 |
|
| 993 |
def _cmd_query(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 994 |
-
return ActionResult(
|
|
|
|
|
|
|
|
|
|
| 995 |
|
| 996 |
def _cmd_query_units(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 997 |
"""Query units by zone and/or type; return their IDs in result.unit_ids."""
|
|
@@ -1001,7 +1323,7 @@ class GameEngine:
|
|
| 1001 |
return ActionResult(
|
| 1002 |
action_type="query_units",
|
| 1003 |
success=True,
|
| 1004 |
-
|
| 1005 |
unit_ids=ids,
|
| 1006 |
)
|
| 1007 |
|
|
@@ -1012,7 +1334,7 @@ class GameEngine:
|
|
| 1012 |
return ActionResult(
|
| 1013 |
action_type="assign_to_group",
|
| 1014 |
success=False,
|
| 1015 |
-
|
| 1016 |
)
|
| 1017 |
ids = list(action.unit_ids) if action.unit_ids else []
|
| 1018 |
valid_ids = [uid for uid in ids if uid in player.units]
|
|
@@ -1020,7 +1342,7 @@ class GameEngine:
|
|
| 1020 |
return ActionResult(
|
| 1021 |
action_type="assign_to_group",
|
| 1022 |
success=True,
|
| 1023 |
-
|
| 1024 |
)
|
| 1025 |
|
| 1026 |
# ------------------------------------------------------------------
|
|
|
|
| 22 |
from .buildings import Building, BuildingDef, BuildingStatus, BuildingType, BUILDING_DEFS
|
| 23 |
from .commands import ActionResult, ActionType, CommandResult, GameAction, ParsedCommand
|
| 24 |
from .map import MAP_HEIGHT, MAP_WIDTH, ResourceType
|
| 25 |
+
from .pathfinding import find_path, is_walkable, nearest_walkable_navpoint, snap_to_walkable
|
| 26 |
from .state import GamePhase, GameState, PlayerState
|
| 27 |
from .tech_tree import can_build, can_train, get_producer, missing_for_build, missing_for_train
|
| 28 |
from .units import Unit, UnitDef, UnitStatus, UnitType, UNIT_DEFS
|
|
|
|
| 47 |
self._task: Optional[asyncio.Task] = None # type: ignore[type-arg]
|
| 48 |
self.bot: Optional[BotPlayer] = None
|
| 49 |
self._sound_events: list[dict] = [] # fire/death per tick, sent in game_update
|
| 50 |
+
self._cmd_lang: str = "fr"
|
| 51 |
|
| 52 |
# ------------------------------------------------------------------
|
| 53 |
# Lifecycle
|
|
|
|
| 69 |
# ------------------------------------------------------------------
|
| 70 |
|
| 71 |
def apply_command(self, player_id: str, parsed: ParsedCommand) -> CommandResult:
|
| 72 |
+
self._cmd_lang = getattr(parsed, "language", "fr") or "fr"
|
| 73 |
player = self.state.players.get(player_id)
|
| 74 |
if not player or self.state.phase != GamePhase.PLAYING:
|
| 75 |
+
return CommandResult(results=[], feedback_override="game_not_in_progress")
|
| 76 |
|
| 77 |
results: list[ActionResult] = []
|
| 78 |
last_query_unit_ids: Optional[list[str]] = None
|
|
|
|
| 139 |
for building in player.buildings.values():
|
| 140 |
if building.status != BuildingStatus.CONSTRUCTING:
|
| 141 |
continue
|
| 142 |
+
# Only progress if the assigned SCV has arrived (status == BUILDING)
|
| 143 |
+
scv = next(
|
| 144 |
+
(u for u in player.units.values()
|
| 145 |
+
if u.building_target_id == building.id and u.status == UnitStatus.BUILDING),
|
| 146 |
+
None,
|
| 147 |
+
)
|
| 148 |
+
if not scv:
|
| 149 |
+
continue
|
| 150 |
building.construction_ticks_remaining -= 1
|
| 151 |
+
# HP grows linearly from 15% to 100% over build time
|
| 152 |
+
hp_gain = building.max_hp * 0.85 / max(1, building.construction_max_ticks)
|
| 153 |
+
building.hp = min(building.hp + hp_gain, float(building.max_hp))
|
| 154 |
if building.construction_ticks_remaining <= 0:
|
| 155 |
building.status = BuildingStatus.ACTIVE
|
| 156 |
building.construction_ticks_remaining = 0
|
| 157 |
+
building.hp = float(building.max_hp)
|
| 158 |
+
scv.building_target_id = None
|
| 159 |
+
scv.target_x = scv.target_y = None
|
| 160 |
+
# Auto-return SCV to nearest mineral patch
|
| 161 |
+
cc = player.command_center()
|
| 162 |
+
cx = float(cc.x) + 2.0 if cc else scv.x
|
| 163 |
+
cy = float(cc.y) + 1.5 if cc else scv.y
|
| 164 |
+
patch = self.state.game_map.nearest_mineral(cx, cy)
|
| 165 |
+
if patch:
|
| 166 |
+
scv.status = UnitStatus.MINING_MINERALS
|
| 167 |
+
scv.assigned_resource_id = patch.id
|
| 168 |
+
scv.harvest_carry = False
|
| 169 |
+
scv.harvest_amount = 0
|
| 170 |
+
patch.assigned_scv_ids.append(scv.id)
|
| 171 |
+
self._set_unit_destination(scv, float(patch.x), float(patch.y), is_flying=False)
|
| 172 |
+
else:
|
| 173 |
+
scv.status = UnitStatus.IDLE
|
| 174 |
|
| 175 |
def _tick_production(self, player: PlayerState) -> None:
|
| 176 |
"""Tick building production queues and spawn units."""
|
| 177 |
for building in player.buildings.values():
|
| 178 |
+
if building.status == BuildingStatus.CONSTRUCTING:
|
| 179 |
+
continue
|
| 180 |
if not building.production_queue:
|
| 181 |
building.status = BuildingStatus.ACTIVE
|
| 182 |
continue
|
|
|
|
| 192 |
self._spawn_unit(player, building, UnitType(item.unit_type))
|
| 193 |
|
| 194 |
def _spawn_unit(self, player: PlayerState, building: Building, ut: UnitType) -> None:
|
| 195 |
+
raw_sx, raw_sy = building.spawn_point()
|
| 196 |
+
is_flying = UNIT_DEFS[ut].is_flying
|
| 197 |
+
if is_flying:
|
| 198 |
+
sx, sy = raw_sx, raw_sy
|
| 199 |
+
else:
|
| 200 |
+
blocked = self._building_blocked_rects()
|
| 201 |
+
sx, sy = nearest_walkable_navpoint(raw_sx, raw_sy, blocked_rects=blocked)
|
| 202 |
tx = building.rally_x if building.rally_x is not None else sx
|
| 203 |
ty = building.rally_y if building.rally_y is not None else sy
|
| 204 |
unit = Unit.create(ut, player.player_id, sx, sy)
|
| 205 |
if tx != sx or ty != sy:
|
| 206 |
unit.status = UnitStatus.MOVING
|
| 207 |
+
self._set_unit_destination(unit, tx, ty, is_flying=is_flying)
|
| 208 |
player.units[unit.id] = unit
|
| 209 |
|
| 210 |
def _tick_mining(self, player: PlayerState) -> None:
|
|
|
|
| 225 |
unit.harvest_carry = False
|
| 226 |
return
|
| 227 |
|
| 228 |
+
mineral_arrive = 1.2 # SCV stops right next to the patch
|
| 229 |
+
cc_edge_arrive = 1.0 # SCV triggers deposit when within 1 tile of any CC edge
|
|
|
|
| 230 |
|
| 231 |
for unit in player.units.values():
|
| 232 |
if unit.status == UnitStatus.MINING_MINERALS:
|
| 233 |
resource = self.state.game_map.get_resource(unit.assigned_resource_id or "")
|
| 234 |
if not resource or resource.is_depleted:
|
| 235 |
+
# Remove from old patch assignment
|
| 236 |
+
if resource and unit.id in resource.assigned_scv_ids:
|
| 237 |
+
resource.assigned_scv_ids.remove(unit.id)
|
| 238 |
unit.assigned_resource_id = None
|
| 239 |
+
# Auto-reassign to next available mineral instead of going idle
|
| 240 |
+
next_patch = self.state.game_map.nearest_mineral(cc.x, cc.y)
|
| 241 |
+
if next_patch:
|
| 242 |
+
unit.assigned_resource_id = next_patch.id
|
| 243 |
+
next_patch.assigned_scv_ids.append(unit.id)
|
| 244 |
+
self._set_unit_destination(unit, float(next_patch.x), float(next_patch.y), is_flying=False)
|
| 245 |
+
else:
|
| 246 |
+
unit.status = UnitStatus.IDLE
|
| 247 |
+
unit.target_x = unit.target_y = None
|
| 248 |
+
unit.path_waypoints = []
|
| 249 |
+
unit.harvest_carry = False
|
| 250 |
continue
|
| 251 |
|
| 252 |
rx, ry = float(resource.x), float(resource.y)
|
| 253 |
|
| 254 |
if not unit.harvest_carry:
|
| 255 |
+
if unit.target_x is None:
|
| 256 |
self._set_unit_destination(unit, rx, ry, is_flying=False)
|
| 257 |
+
if unit.dist_to(rx, ry) <= mineral_arrive:
|
| 258 |
gathered = min(MINERAL_PER_HARVEST, resource.amount)
|
| 259 |
resource.amount -= gathered
|
| 260 |
unit.harvest_carry = True
|
| 261 |
unit.harvest_amount = gathered
|
| 262 |
+
tx, ty = self._nearest_building_entry(unit, cc)
|
| 263 |
+
self._set_unit_destination(unit, tx, ty, is_flying=False)
|
| 264 |
else:
|
| 265 |
+
if unit.target_x is None:
|
| 266 |
+
tx, ty = self._nearest_building_entry(unit, cc)
|
| 267 |
+
self._set_unit_destination(unit, tx, ty, is_flying=False)
|
| 268 |
+
if self._dist_unit_to_building(unit, cc) <= cc_edge_arrive:
|
| 269 |
player.minerals += unit.harvest_amount
|
| 270 |
unit.harvest_carry = False
|
| 271 |
unit.harvest_amount = 0
|
|
|
|
| 284 |
rx, ry = float(resource.x), float(resource.y)
|
| 285 |
|
| 286 |
if not unit.harvest_carry:
|
| 287 |
+
if unit.target_x is None:
|
| 288 |
self._set_unit_destination(unit, rx, ry, is_flying=False)
|
| 289 |
+
if unit.dist_to(rx, ry) <= mineral_arrive:
|
| 290 |
unit.harvest_carry = True
|
| 291 |
unit.harvest_amount = GAS_PER_HARVEST
|
| 292 |
+
tx, ty = self._nearest_building_entry(unit, cc)
|
| 293 |
+
self._set_unit_destination(unit, tx, ty, is_flying=False)
|
| 294 |
else:
|
| 295 |
+
if unit.target_x is None:
|
| 296 |
+
tx, ty = self._nearest_building_entry(unit, cc)
|
| 297 |
+
self._set_unit_destination(unit, tx, ty, is_flying=False)
|
| 298 |
+
if self._dist_unit_to_building(unit, cc) <= cc_edge_arrive:
|
| 299 |
player.gas += unit.harvest_amount
|
| 300 |
unit.harvest_carry = False
|
| 301 |
unit.harvest_amount = 0
|
|
|
|
| 325 |
# Building SCVs stay put; mining SCVs move but skip combat
|
| 326 |
if unit.status == UnitStatus.BUILDING:
|
| 327 |
continue
|
| 328 |
+
if unit.status == UnitStatus.MOVING_TO_BUILD:
|
| 329 |
+
if unit.target_x is not None and unit.target_y is not None:
|
| 330 |
+
self._move_toward(unit, defn, unit.target_x, unit.target_y)
|
| 331 |
+
continue
|
| 332 |
if unit.status in (UnitStatus.MINING_MINERALS, UnitStatus.MINING_GAS):
|
| 333 |
if unit.target_x is not None and unit.target_y is not None:
|
| 334 |
self._move_toward(unit, defn, unit.target_x, unit.target_y)
|
|
|
|
| 374 |
if unit.attack_target_id or unit.attack_target_building_id:
|
| 375 |
self._combat_attack(unit, defn, all_units, player, enemy, sieged=False)
|
| 376 |
|
| 377 |
+
def _building_blocked_rects(self) -> list[tuple[float, float, float, float]]:
|
| 378 |
+
"""Collision footprints (x, y, w, h) used for pathfinding and unit collision.
|
| 379 |
+
|
| 380 |
+
Uses the shrunk collision box so units can pass between buildings
|
| 381 |
+
that have a small visual gap.
|
| 382 |
+
"""
|
| 383 |
+
rects: list[tuple[float, float, float, float]] = []
|
| 384 |
+
for player in self.state.players.values():
|
| 385 |
+
for b in player.buildings.values():
|
| 386 |
+
if b.status == BuildingStatus.DESTROYED:
|
| 387 |
+
continue
|
| 388 |
+
defn = BUILDING_DEFS[b.building_type]
|
| 389 |
+
chw, chh = defn.col_hw(), defn.col_hh()
|
| 390 |
+
rects.append((b.x - chw, b.y - chh, chw * 2, chh * 2))
|
| 391 |
+
return rects
|
| 392 |
+
|
| 393 |
def _set_unit_destination(
|
| 394 |
self, unit: Unit, tx: float, ty: float, *, is_flying: bool
|
| 395 |
) -> None:
|
|
|
|
| 399 |
unit.path_waypoints = []
|
| 400 |
if is_flying:
|
| 401 |
return
|
| 402 |
+
blocked = self._building_blocked_rects()
|
| 403 |
+
sx, sy = unit.x, unit.y
|
| 404 |
+
path = find_path(sx, sy, tx, ty, blocked_rects=blocked)
|
| 405 |
if path is None:
|
| 406 |
+
# Start is inside a building footprint — snap start outside first
|
| 407 |
+
snapped_start = snap_to_walkable(sx, sy, blocked_rects=blocked)
|
| 408 |
+
if snapped_start != (sx, sy):
|
| 409 |
+
path = find_path(snapped_start[0], snapped_start[1], tx, ty, blocked_rects=blocked)
|
| 410 |
+
if path is None:
|
| 411 |
+
snapped_dst = snap_to_walkable(tx, ty, blocked_rects=blocked)
|
| 412 |
+
unit.target_x, unit.target_y = snapped_dst[0], snapped_dst[1]
|
| 413 |
+
return
|
| 414 |
if not path:
|
| 415 |
return
|
| 416 |
unit.target_x, unit.target_y = path[0][0], path[0][1]
|
| 417 |
unit.path_waypoints = [[p[0], p[1]] for p in path[1:]]
|
| 418 |
|
| 419 |
+
def _would_overlap(
|
| 420 |
+
self, unit: Unit, new_x: float, new_y: float, *, exclude_unit_id: Optional[str] = None
|
| 421 |
+
) -> bool:
|
| 422 |
+
"""True if (new_x, new_y) would overlap another unit or a building.
|
| 423 |
+
|
| 424 |
+
Mining SCVs use soft collision (they can pass through each other, like in SC).
|
| 425 |
+
For other units, if two already overlap we allow moves that increase separation.
|
| 426 |
+
"""
|
| 427 |
+
_mining = (UnitStatus.MINING_MINERALS, UnitStatus.MINING_GAS)
|
| 428 |
+
unit_is_miner = unit.status in _mining
|
| 429 |
+
for player in self.state.players.values():
|
| 430 |
+
for u in player.units.values():
|
| 431 |
+
if u.id == unit.id or (exclude_unit_id and u.id == exclude_unit_id):
|
| 432 |
+
continue
|
| 433 |
+
# Miners pass through other miners (soft collision, matching SC behaviour)
|
| 434 |
+
if unit_is_miner and u.status in _mining:
|
| 435 |
+
continue
|
| 436 |
+
min_dist = 2 * UNIT_RADIUS
|
| 437 |
+
new_dist = math.hypot(new_x - u.x, new_y - u.y)
|
| 438 |
+
if new_dist >= min_dist:
|
| 439 |
+
continue
|
| 440 |
+
# Already overlapping: allow the move only if it increases separation
|
| 441 |
+
cur_dist = math.hypot(unit.x - u.x, unit.y - u.y)
|
| 442 |
+
if cur_dist < min_dist and new_dist >= cur_dist:
|
| 443 |
+
continue # moving away from an already-overlapping unit — allow
|
| 444 |
+
return True
|
| 445 |
+
for rx, ry, w, h in self._building_blocked_rects():
|
| 446 |
+
px = max(rx, min(rx + w, new_x))
|
| 447 |
+
py = max(ry, min(ry + h, new_y))
|
| 448 |
+
if math.hypot(new_x - px, new_y - py) < UNIT_RADIUS:
|
| 449 |
+
return True
|
| 450 |
+
return False
|
| 451 |
+
|
| 452 |
def _move_toward(self, unit: Unit, defn: UnitDef, tx: float, ty: float) -> None:
|
| 453 |
+
"""Move unit up to one full step toward target, consuming intermediate waypoints smoothly."""
|
|
|
|
|
|
|
| 454 |
step = defn.move_speed * TICK_INTERVAL
|
| 455 |
+
remaining = step
|
| 456 |
+
cur_tx, cur_ty = tx, ty
|
| 457 |
+
moved = False
|
| 458 |
+
arrived = False
|
| 459 |
+
|
| 460 |
+
while remaining > 1e-6:
|
| 461 |
+
dx = cur_tx - unit.x
|
| 462 |
+
dy = cur_ty - unit.y
|
| 463 |
+
dist = math.sqrt(dx * dx + dy * dy)
|
| 464 |
+
|
| 465 |
+
if dist < 1e-6:
|
| 466 |
+
# Already on this waypoint — advance immediately
|
| 467 |
+
if unit.path_waypoints:
|
| 468 |
+
nw = unit.path_waypoints.pop(0)
|
| 469 |
+
cur_tx = nw[0]; cur_ty = nw[1]
|
| 470 |
+
unit.target_x = cur_tx; unit.target_y = cur_ty
|
| 471 |
+
else:
|
| 472 |
+
arrived = True
|
| 473 |
+
break
|
| 474 |
+
|
| 475 |
+
if dist <= remaining:
|
| 476 |
+
# Can reach this waypoint within remaining budget — snap and continue
|
| 477 |
+
unit.x = cur_tx
|
| 478 |
+
unit.y = cur_ty
|
| 479 |
+
remaining -= dist
|
| 480 |
+
moved = True
|
| 481 |
+
if unit.path_waypoints:
|
| 482 |
+
nw = unit.path_waypoints.pop(0)
|
| 483 |
+
cur_tx = nw[0]; cur_ty = nw[1]
|
| 484 |
+
unit.target_x = cur_tx; unit.target_y = cur_ty
|
| 485 |
+
else:
|
| 486 |
+
arrived = True
|
| 487 |
+
break
|
| 488 |
+
else:
|
| 489 |
+
# Partial move — try full remaining, then half, then slide
|
| 490 |
+
nx = dx / dist
|
| 491 |
+
ny = dy / dist
|
| 492 |
+
new_x = unit.x + nx * remaining
|
| 493 |
+
new_y = unit.y + ny * remaining
|
| 494 |
+
|
| 495 |
+
if not self._would_overlap(unit, new_x, new_y):
|
| 496 |
+
unit.x = new_x
|
| 497 |
+
unit.y = new_y
|
| 498 |
+
moved = True
|
| 499 |
+
else:
|
| 500 |
+
hx = unit.x + nx * remaining * 0.5
|
| 501 |
+
hy = unit.y + ny * remaining * 0.5
|
| 502 |
+
if not self._would_overlap(unit, hx, hy):
|
| 503 |
+
unit.x = hx
|
| 504 |
+
unit.y = hy
|
| 505 |
+
moved = True
|
| 506 |
+
elif abs(nx) >= abs(ny):
|
| 507 |
+
if abs(nx) > 0.05 and not self._would_overlap(unit, unit.x + nx * remaining, unit.y):
|
| 508 |
+
unit.x += nx * remaining
|
| 509 |
+
moved = True
|
| 510 |
+
elif abs(ny) > 0.05 and not self._would_overlap(unit, unit.x, unit.y + ny * remaining):
|
| 511 |
+
unit.y += ny * remaining
|
| 512 |
+
moved = True
|
| 513 |
+
else:
|
| 514 |
+
if abs(ny) > 0.05 and not self._would_overlap(unit, unit.x, unit.y + ny * remaining):
|
| 515 |
+
unit.y += ny * remaining
|
| 516 |
+
moved = True
|
| 517 |
+
elif abs(nx) > 0.05 and not self._would_overlap(unit, unit.x + nx * remaining, unit.y):
|
| 518 |
+
unit.x += nx * remaining
|
| 519 |
+
moved = True
|
| 520 |
+
break
|
| 521 |
+
|
| 522 |
+
unit.target_x = cur_tx
|
| 523 |
+
unit.target_y = cur_ty
|
| 524 |
+
|
| 525 |
+
if moved or arrived:
|
| 526 |
+
unit.stuck_ticks = 0
|
| 527 |
else:
|
| 528 |
+
unit.stuck_ticks += 1
|
| 529 |
+
if unit.stuck_ticks >= 4:
|
| 530 |
+
unit.stuck_ticks = 0
|
| 531 |
+
if unit.path_waypoints:
|
| 532 |
+
nw = unit.path_waypoints.pop(0)
|
| 533 |
+
unit.target_x, unit.target_y = nw[0], nw[1]
|
| 534 |
+
else:
|
| 535 |
+
arrived = True
|
| 536 |
+
else:
|
| 537 |
+
return
|
| 538 |
+
|
| 539 |
+
if not arrived:
|
| 540 |
+
return
|
| 541 |
+
if unit.status == UnitStatus.MOVING:
|
| 542 |
+
unit.status = UnitStatus.IDLE
|
| 543 |
+
unit.target_x = unit.target_y = None
|
| 544 |
+
elif unit.status == UnitStatus.ATTACKING:
|
| 545 |
+
unit.status = UnitStatus.IDLE
|
| 546 |
+
unit.target_x = unit.target_y = None
|
| 547 |
+
elif unit.status == UnitStatus.MOVING_TO_BUILD:
|
| 548 |
+
unit.status = UnitStatus.BUILDING
|
| 549 |
+
unit.target_x = unit.target_y = None
|
| 550 |
+
elif unit.status == UnitStatus.PATROLLING:
|
| 551 |
+
unit.target_x, unit.patrol_x = unit.patrol_x, unit.target_x
|
| 552 |
+
unit.target_y, unit.patrol_y = unit.patrol_y, unit.target_y
|
| 553 |
|
| 554 |
def _combat_attack(
|
| 555 |
self,
|
|
|
|
| 699 |
# ------------------------------------------------------------------
|
| 700 |
|
| 701 |
def _building_center(self, b: Building) -> tuple[float, float]:
|
| 702 |
+
return (b.x, b.y)
|
|
|
|
| 703 |
|
| 704 |
def _dist_unit_to_building(self, unit: Unit, building: Building) -> float:
|
| 705 |
+
"""Distance from unit center to nearest point on the building collision box."""
|
| 706 |
defn = BUILDING_DEFS[building.building_type]
|
| 707 |
+
chw, chh = defn.col_hw(), defn.col_hh()
|
| 708 |
+
x0, x1 = building.x - chw, building.x + chw
|
| 709 |
+
y0, y1 = building.y - chh, building.y + chh
|
| 710 |
px = max(x0, min(x1, unit.x))
|
| 711 |
py = max(y0, min(y1, unit.y))
|
| 712 |
return math.sqrt((unit.x - px) ** 2 + (unit.y - py) ** 2)
|
| 713 |
|
| 714 |
+
def _nearest_building_entry(self, unit: Unit, building: Building) -> tuple[float, float]:
|
| 715 |
+
"""Return a point just outside the nearest visual edge of a building, on the unit's side.
|
| 716 |
+
|
| 717 |
+
SCVs use this to approach from whichever direction they're coming from,
|
| 718 |
+
so they can deposit/interact from any side instead of always queuing at one point.
|
| 719 |
+
Uses the visual box (not the collision box) so the SCV visually touches the building.
|
| 720 |
+
"""
|
| 721 |
+
defn = BUILDING_DEFS[building.building_type]
|
| 722 |
+
hw = defn.width / 2
|
| 723 |
+
hh = defn.height / 2
|
| 724 |
+
px = max(building.x - hw, min(building.x + hw, unit.x))
|
| 725 |
+
py = max(building.y - hh, min(building.y + hh, unit.y))
|
| 726 |
+
dx = unit.x - px
|
| 727 |
+
dy = unit.y - py
|
| 728 |
+
d = math.hypot(dx, dy)
|
| 729 |
+
margin = UNIT_RADIUS + 0.3
|
| 730 |
+
if d > 0:
|
| 731 |
+
return (px + dx / d * margin, py + dy / d * margin)
|
| 732 |
+
return (building.x, building.y + hh + margin)
|
| 733 |
+
|
| 734 |
def _nearest_enemy_in_range(
|
| 735 |
self,
|
| 736 |
unit: Unit,
|
|
|
|
| 759 |
return best[1] if best else None
|
| 760 |
|
| 761 |
def _resolve_zone(self, player_id: str, zone: str) -> tuple[float, float]:
|
| 762 |
+
import re as _re
|
| 763 |
player = self.state.players[player_id]
|
| 764 |
enemy = self.state.enemy_of(player_id)
|
| 765 |
cc = player.command_center()
|
| 766 |
+
base_x = cc.x if cc else float(MAP_WIDTH) / 2
|
| 767 |
+
base_y = cc.y if cc else float(MAP_HEIGHT) / 2
|
| 768 |
|
| 769 |
if zone == "my_base":
|
| 770 |
return (base_x, base_y)
|
| 771 |
if zone == "enemy_base" and enemy:
|
| 772 |
ecc = enemy.command_center()
|
| 773 |
+
return (ecc.x, ecc.y) if ecc else (MAP_WIDTH - 5, MAP_HEIGHT - 5)
|
| 774 |
if zone == "center":
|
| 775 |
return (MAP_WIDTH / 2, MAP_HEIGHT / 2)
|
| 776 |
if zone == "top_left":
|
|
|
|
| 790 |
avg_x = sum(u.x for u in military) / len(military)
|
| 791 |
avg_y = sum(u.y for u in military) / len(military)
|
| 792 |
return (avg_x, avg_y)
|
| 793 |
+
|
| 794 |
+
m = _re.match(r'^mineral_(\d+)$', zone)
|
| 795 |
+
if m:
|
| 796 |
+
idx = int(m.group(1)) - 1
|
| 797 |
+
minerals = [
|
| 798 |
+
r for r in self.state.game_map.resources
|
| 799 |
+
if r.resource_type == ResourceType.MINERAL and not r.is_depleted
|
| 800 |
+
]
|
| 801 |
+
minerals.sort(key=lambda r: (r.x - base_x) ** 2 + (r.y - base_y) ** 2)
|
| 802 |
+
if 0 <= idx < len(minerals):
|
| 803 |
+
return (float(minerals[idx].x), float(minerals[idx].y))
|
| 804 |
+
|
| 805 |
+
m = _re.match(r'^geyser_(\d+)$', zone)
|
| 806 |
+
if m:
|
| 807 |
+
idx = int(m.group(1)) - 1
|
| 808 |
+
geysers = [
|
| 809 |
+
r for r in self.state.game_map.resources
|
| 810 |
+
if r.resource_type == ResourceType.GEYSER
|
| 811 |
+
]
|
| 812 |
+
geysers.sort(key=lambda r: (r.x - base_x) ** 2 + (r.y - base_y) ** 2)
|
| 813 |
+
if 0 <= idx < len(geysers):
|
| 814 |
+
return (float(geysers[idx].x), float(geysers[idx].y))
|
| 815 |
+
|
| 816 |
# Fallback: enemy base
|
| 817 |
if enemy:
|
| 818 |
ecc = enemy.command_center()
|
|
|
|
| 861 |
pass
|
| 862 |
return [u.id for u in units]
|
| 863 |
|
| 864 |
+
# Vision radii (in cells) — must match frontend constants
|
| 865 |
+
_UNIT_VISION: dict[UnitType, float] = {
|
| 866 |
+
UnitType.SCV: 6, UnitType.MARINE: 6, UnitType.MEDIC: 6,
|
| 867 |
+
UnitType.GOLIATH: 8, UnitType.TANK: 8, UnitType.WRAITH: 9,
|
| 868 |
+
}
|
| 869 |
+
_BUILDING_VISION: dict[BuildingType, float] = {
|
| 870 |
+
BuildingType.COMMAND_CENTER: 10, BuildingType.SUPPLY_DEPOT: 7,
|
| 871 |
+
BuildingType.BARRACKS: 7, BuildingType.ENGINEERING_BAY: 7,
|
| 872 |
+
BuildingType.REFINERY: 7, BuildingType.FACTORY: 7,
|
| 873 |
+
BuildingType.ARMORY: 7, BuildingType.STARPORT: 7,
|
| 874 |
+
}
|
| 875 |
+
_SCV_BUILD_RANGE: float = 6.0
|
| 876 |
+
|
| 877 |
+
def _is_visible(self, player: PlayerState, x: float, y: float) -> bool:
|
| 878 |
+
"""Return True if tile (x, y) is within vision of any own unit or building."""
|
| 879 |
+
for u in player.units.values():
|
| 880 |
+
r = self._UNIT_VISION.get(u.unit_type, 6.0)
|
| 881 |
+
if (u.x - x) ** 2 + (u.y - y) ** 2 <= r * r:
|
| 882 |
+
return True
|
| 883 |
+
for b in player.buildings.values():
|
| 884 |
+
if b.status == BuildingStatus.DESTROYED:
|
| 885 |
+
continue
|
| 886 |
+
r = self._BUILDING_VISION.get(b.building_type, 7.0)
|
| 887 |
+
if (b.x - x) ** 2 + (b.y - y) ** 2 <= r * r:
|
| 888 |
+
return True
|
| 889 |
+
return False
|
| 890 |
+
|
| 891 |
def _find_build_position(
|
| 892 |
+
self, player: PlayerState, bt: BuildingType, near_scv: Unit
|
| 893 |
+
) -> Optional[tuple[float, float]]:
|
| 894 |
+
"""Return CENTER coordinates for a new building, or None if no valid spot found."""
|
|
|
|
|
|
|
|
|
|
| 895 |
defn = BUILDING_DEFS[bt]
|
| 896 |
+
cx, cy = near_scv.x, near_scv.y
|
| 897 |
|
| 898 |
+
for radius in range(1, int(self._SCV_BUILD_RANGE) + 2):
|
| 899 |
for dx in range(-radius, radius + 1):
|
| 900 |
for dy in range(-radius, radius + 1):
|
| 901 |
+
tl_x, tl_y = int(cx) + dx, int(cy) + dy
|
| 902 |
+
tile_cx = tl_x + defn.width / 2.0
|
| 903 |
+
tile_cy = tl_y + defn.height / 2.0
|
| 904 |
+
if (tile_cx - cx) ** 2 + (tile_cy - cy) ** 2 > self._SCV_BUILD_RANGE ** 2:
|
| 905 |
+
continue
|
| 906 |
+
if not self._can_place(tl_x, tl_y, defn):
|
| 907 |
+
continue
|
| 908 |
+
if not self._is_visible(player, tile_cx, tile_cy):
|
| 909 |
+
continue
|
| 910 |
+
return (tile_cx, tile_cy)
|
| 911 |
return None
|
| 912 |
|
| 913 |
+
def _can_place(self, tl_x: int, tl_y: int, defn: BuildingDef) -> bool:
|
| 914 |
+
"""Check if a building with given top-left corner can be placed (no overlap, in bounds)."""
|
| 915 |
+
if tl_x < 0 or tl_y < 0 or tl_x + defn.width > MAP_WIDTH or tl_y + defn.height > MAP_HEIGHT:
|
| 916 |
return False
|
| 917 |
+
# Check overlap with existing buildings (stored as center coords)
|
| 918 |
for player in self.state.players.values():
|
| 919 |
for b in player.buildings.values():
|
| 920 |
if b.status == BuildingStatus.DESTROYED:
|
| 921 |
continue
|
| 922 |
bd = BUILDING_DEFS[b.building_type]
|
| 923 |
+
if tl_x < b.x + bd.width / 2 and tl_x + defn.width > b.x - bd.width / 2 \
|
| 924 |
+
and tl_y < b.y + bd.height / 2 and tl_y + defn.height > b.y - bd.height / 2:
|
| 925 |
return False
|
| 926 |
# Check overlap with resources
|
| 927 |
for res in self.state.game_map.resources:
|
| 928 |
+
if tl_x <= res.x < tl_x + defn.width and tl_y <= res.y < tl_y + defn.height:
|
| 929 |
return False
|
| 930 |
return True
|
| 931 |
|
|
|
|
| 964 |
return self._cmd_query_units(player, action)
|
| 965 |
if t == ActionType.ASSIGN_TO_GROUP:
|
| 966 |
return self._cmd_assign_to_group(player, action)
|
| 967 |
+
return ActionResult(
|
| 968 |
+
action_type=t, success=False, data={"error": "unknown_action"}
|
| 969 |
+
)
|
| 970 |
except Exception as exc:
|
| 971 |
log.exception("Error applying action %s", action.type)
|
| 972 |
+
return ActionResult(
|
| 973 |
+
action_type=str(action.type), success=False, data={"error": "exception", "detail": str(exc)}
|
| 974 |
+
)
|
| 975 |
|
| 976 |
def _cmd_build(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 977 |
raw = action.building_type
|
| 978 |
if not raw:
|
| 979 |
+
return ActionResult(action_type="build", success=False, data={"error": "build_missing_type"})
|
| 980 |
try:
|
| 981 |
bt = BuildingType(raw)
|
| 982 |
except ValueError:
|
| 983 |
+
return ActionResult(action_type="build", success=False, data={"error": "build_unknown", "raw": raw})
|
| 984 |
|
| 985 |
if not can_build(bt, player):
|
| 986 |
missing = missing_for_build(bt, player)
|
| 987 |
names = ", ".join(m.value for m in missing)
|
| 988 |
+
return ActionResult(
|
| 989 |
+
action_type="build", success=False, data={"error": "build_missing_prereq", "names": names}
|
| 990 |
+
)
|
| 991 |
|
| 992 |
defn = BUILDING_DEFS[bt]
|
| 993 |
if player.minerals < defn.mineral_cost or player.gas < defn.gas_cost:
|
| 994 |
+
return ActionResult(
|
| 995 |
+
action_type="build", success=False,
|
| 996 |
+
data={"error": "build_insufficient_resources", "mineral": defn.mineral_cost, "gas": defn.gas_cost},
|
| 997 |
+
)
|
| 998 |
|
| 999 |
# Clamp count to what resources and SCVs allow
|
| 1000 |
count = max(1, min(action.count, 5))
|
|
|
|
| 1011 |
)
|
| 1012 |
count = min(count, len(available_scvs))
|
| 1013 |
if count == 0:
|
| 1014 |
+
return ActionResult(action_type="build", success=False, data={"error": "build_no_scv"})
|
| 1015 |
|
| 1016 |
cc = player.command_center()
|
| 1017 |
cx, cy = (float(cc.x), float(cc.y)) if cc else (0.0, 0.0)
|
| 1018 |
|
| 1019 |
built = 0
|
| 1020 |
for i in range(count):
|
| 1021 |
+
scv = available_scvs[i]
|
| 1022 |
+
|
| 1023 |
+
# Find center position for this building
|
| 1024 |
if bt == BuildingType.REFINERY:
|
| 1025 |
geyser = self.state.game_map.nearest_geyser_without_refinery(cx, cy)
|
| 1026 |
if not geyser:
|
| 1027 |
break
|
| 1028 |
+
# Refinery (2×2) centered on geyser tile (+1 from integer geyser coord)
|
| 1029 |
+
pos_cx: float = geyser.x + 1.0
|
| 1030 |
+
pos_cy: float = geyser.y + 1.0
|
| 1031 |
+
if not self._is_visible(player, pos_cx, pos_cy):
|
| 1032 |
+
break
|
| 1033 |
geyser.has_refinery = True
|
| 1034 |
else:
|
| 1035 |
+
pos_opt = self._find_build_position(player, bt, scv)
|
| 1036 |
if not pos_opt:
|
| 1037 |
break
|
| 1038 |
+
pos_cx, pos_cy = pos_opt
|
|
|
|
|
|
|
| 1039 |
# Unassign from resource if mining
|
| 1040 |
if scv.assigned_resource_id:
|
| 1041 |
res = self.state.game_map.get_resource(scv.assigned_resource_id)
|
|
|
|
| 1048 |
player.minerals -= defn.mineral_cost
|
| 1049 |
player.gas -= defn.gas_cost
|
| 1050 |
|
| 1051 |
+
building = Building.create(bt, player.player_id, pos_cx, pos_cy)
|
| 1052 |
player.buildings[building.id] = building
|
| 1053 |
|
| 1054 |
+
scv.status = UnitStatus.MOVING_TO_BUILD
|
| 1055 |
scv.building_target_id = building.id
|
| 1056 |
+
# Navigate to the nearest point just outside the building edge
|
| 1057 |
+
bw = float(defn.width)
|
| 1058 |
+
bh = float(defn.height)
|
| 1059 |
+
edge_x = max(pos_cx - bw / 2, min(pos_cx + bw / 2, scv.x))
|
| 1060 |
+
edge_y = max(pos_cy - bh / 2, min(pos_cy + bh / 2, scv.y))
|
| 1061 |
+
dx = scv.x - edge_x
|
| 1062 |
+
dy = scv.y - edge_y
|
| 1063 |
+
edge_dist = math.hypot(dx, dy)
|
| 1064 |
+
approach_margin = UNIT_RADIUS + 0.6
|
| 1065 |
+
if edge_dist > 0:
|
| 1066 |
+
dest_x = edge_x + dx / edge_dist * approach_margin
|
| 1067 |
+
dest_y = edge_y + dy / edge_dist * approach_margin
|
| 1068 |
+
else:
|
| 1069 |
+
dest_x = pos_cx + bw / 2 + approach_margin
|
| 1070 |
+
dest_y = pos_cy
|
| 1071 |
+
self._set_unit_destination(scv, dest_x, dest_y, is_flying=False)
|
| 1072 |
built += 1
|
| 1073 |
|
| 1074 |
if built == 0:
|
| 1075 |
+
return ActionResult(action_type="build", success=False, data={"error": "build_no_placement"})
|
| 1076 |
+
return ActionResult(
|
| 1077 |
+
action_type="build", success=True,
|
| 1078 |
+
data={"built": built, "building": bt.value},
|
| 1079 |
+
)
|
| 1080 |
|
| 1081 |
def _cmd_train(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 1082 |
raw = action.unit_type
|
| 1083 |
if not raw:
|
| 1084 |
+
return ActionResult(action_type="train", success=False, data={"error": "train_missing_type"})
|
| 1085 |
try:
|
| 1086 |
ut = UnitType(raw)
|
| 1087 |
except ValueError:
|
| 1088 |
+
return ActionResult(action_type="train", success=False, data={"error": "train_unknown", "raw": raw})
|
| 1089 |
|
| 1090 |
if not can_train(ut, player):
|
| 1091 |
missing = missing_for_train(ut, player)
|
| 1092 |
names = ", ".join(m.value for m in missing)
|
| 1093 |
+
return ActionResult(
|
| 1094 |
+
action_type="train", success=False, data={"error": "train_missing_prereq", "names": names}
|
| 1095 |
+
)
|
| 1096 |
|
| 1097 |
defn = UNIT_DEFS[ut]
|
| 1098 |
producer_type = get_producer(ut)
|
| 1099 |
producers = player.active_buildings_of(producer_type)
|
| 1100 |
if not producers:
|
| 1101 |
+
return ActionResult(
|
| 1102 |
+
action_type="train", success=False,
|
| 1103 |
+
data={"error": "train_no_producer", "producer": producer_type.value},
|
| 1104 |
+
)
|
| 1105 |
|
| 1106 |
count = max(1, min(action.count, 20))
|
| 1107 |
num_producers = len(producers)
|
| 1108 |
|
|
|
|
| 1109 |
total_minerals = defn.mineral_cost * count
|
| 1110 |
total_gas = defn.gas_cost * count
|
| 1111 |
if player.minerals < total_minerals or player.gas < total_gas:
|
| 1112 |
+
return ActionResult(
|
| 1113 |
+
action_type="train", success=False, data={"error": "train_insufficient_resources"}
|
| 1114 |
+
)
|
| 1115 |
|
|
|
|
| 1116 |
queued_supply = sum(
|
| 1117 |
UNIT_DEFS[UnitType(item.unit_type)].supply_cost
|
| 1118 |
for b in player.buildings.values()
|
| 1119 |
for item in b.production_queue
|
| 1120 |
)
|
| 1121 |
if player.supply_used + queued_supply + defn.supply_cost * count > player.supply_max:
|
| 1122 |
+
return ActionResult(
|
| 1123 |
+
action_type="train", success=False, data={"error": "train_insufficient_supply"}
|
| 1124 |
+
)
|
| 1125 |
|
| 1126 |
from .buildings import ProductionItem # local import to avoid cycle
|
| 1127 |
for i in range(count):
|
|
|
|
| 1137 |
player.minerals -= total_minerals
|
| 1138 |
player.gas -= total_gas
|
| 1139 |
|
| 1140 |
+
return ActionResult(
|
| 1141 |
+
action_type="train", success=True,
|
| 1142 |
+
data={"count": count, "unit": ut.value},
|
| 1143 |
+
)
|
| 1144 |
|
| 1145 |
def _cmd_move(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 1146 |
units = self._resolve_selector(player, action.unit_selector or "all_military")
|
| 1147 |
if not units:
|
| 1148 |
+
return ActionResult(action_type="move", success=False, data={"error": "no_units_selected"})
|
| 1149 |
tx, ty = self._resolve_zone(player.player_id, action.target_zone or "center")
|
| 1150 |
for unit in units:
|
| 1151 |
if unit.is_sieged:
|
|
|
|
| 1155 |
unit.attack_target_building_id = None
|
| 1156 |
self._set_unit_destination(unit, tx, ty, is_flying=UNIT_DEFS[unit.unit_type].is_flying)
|
| 1157 |
move_ack = [{"kind": "move_ack", "unit_type": units[0].unit_type.value}]
|
| 1158 |
+
zone = action.target_zone or "center"
|
| 1159 |
+
return ActionResult(
|
| 1160 |
+
action_type="move", success=True,
|
| 1161 |
+
data={"n": len(units), "zone": zone}, sound_events=move_ack,
|
| 1162 |
+
)
|
| 1163 |
|
| 1164 |
def _cmd_attack(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 1165 |
units = self._resolve_selector(player, action.unit_selector or "all_military")
|
| 1166 |
if not units:
|
| 1167 |
+
return ActionResult(action_type="attack", success=False, data={"error": "no_units_selected"})
|
| 1168 |
tx, ty = self._resolve_zone(player.player_id, action.target_zone or "enemy_base")
|
| 1169 |
for unit in units:
|
| 1170 |
if unit.is_sieged:
|
|
|
|
| 1174 |
unit.attack_target_building_id = None
|
| 1175 |
self._set_unit_destination(unit, tx, ty, is_flying=UNIT_DEFS[unit.unit_type].is_flying)
|
| 1176 |
move_ack = [{"kind": "move_ack", "unit_type": units[0].unit_type.value}]
|
| 1177 |
+
zone = action.target_zone or "enemy_base"
|
| 1178 |
+
return ActionResult(
|
| 1179 |
+
action_type="attack", success=True,
|
| 1180 |
+
data={"n": len(units), "zone": zone}, sound_events=move_ack,
|
| 1181 |
+
)
|
| 1182 |
|
| 1183 |
def _cmd_siege(self, player: PlayerState, action: GameAction, siege: bool) -> ActionResult:
|
| 1184 |
tanks = self._resolve_selector(player, action.unit_selector or "all_tanks")
|
| 1185 |
tanks = [u for u in tanks if u.unit_type == UnitType.TANK]
|
| 1186 |
if not tanks:
|
| 1187 |
+
return ActionResult(action_type="siege", success=False, data={"error": "siege_no_tanks"})
|
| 1188 |
for tank in tanks:
|
| 1189 |
tank.is_sieged = siege
|
| 1190 |
tank.status = UnitStatus.SIEGED if siege else UnitStatus.IDLE
|
| 1191 |
if siege:
|
| 1192 |
tank.target_x = tank.target_y = None
|
| 1193 |
+
mode = "siege" if siege else "mobile"
|
| 1194 |
+
return ActionResult(
|
| 1195 |
+
action_type="siege", success=True,
|
| 1196 |
+
data={"n": len(tanks), "mode": mode},
|
| 1197 |
+
)
|
| 1198 |
|
| 1199 |
def _cmd_cloak(self, player: PlayerState, action: GameAction, cloak: bool) -> ActionResult:
|
| 1200 |
wraiths = self._resolve_selector(player, action.unit_selector or "all_wraiths")
|
| 1201 |
wraiths = [u for u in wraiths if u.unit_type == UnitType.WRAITH]
|
| 1202 |
if not wraiths:
|
| 1203 |
+
return ActionResult(action_type="cloak", success=False, data={"error": "cloak_no_wraiths"})
|
| 1204 |
for wraith in wraiths:
|
| 1205 |
wraith.is_cloaked = cloak
|
| 1206 |
+
state = "on" if cloak else "off"
|
| 1207 |
+
return ActionResult(
|
| 1208 |
+
action_type="cloak", success=True,
|
| 1209 |
+
data={"n": len(wraiths), "state": state},
|
| 1210 |
+
)
|
| 1211 |
|
| 1212 |
def _cmd_gather(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 1213 |
resource_type = (action.resource_type or "minerals").lower()
|
|
|
|
| 1217 |
scvs = self._resolve_selector(player, action.unit_selector or "idle_scv")
|
| 1218 |
scvs = [u for u in scvs if u.unit_type == UnitType.SCV]
|
| 1219 |
if not scvs:
|
| 1220 |
+
return ActionResult(action_type="gather", success=False, data={"error": "gather_no_scv"})
|
| 1221 |
|
| 1222 |
assigned = 0
|
| 1223 |
if resource_type == "gas":
|
|
|
|
| 1238 |
geyser.assigned_scv_ids.append(scv.id)
|
| 1239 |
assigned += 1
|
| 1240 |
else:
|
| 1241 |
+
# Distribute SCVs across patches evenly: pick the patch with fewest assigned SCVs
|
| 1242 |
+
# (avoids funnelling all 5 SCVs to the same nearest patch)
|
| 1243 |
+
mineral_patches = [
|
| 1244 |
+
r for r in self.state.game_map.resources
|
| 1245 |
+
if r.resource_type.value == "mineral" and not r.is_depleted and r.has_capacity
|
| 1246 |
+
]
|
| 1247 |
+
if not mineral_patches:
|
| 1248 |
+
return ActionResult(action_type="gather", success=False, data={"error": "gather_no_resource"})
|
| 1249 |
+
|
| 1250 |
for scv in scvs:
|
| 1251 |
+
if not mineral_patches:
|
| 1252 |
+
break
|
| 1253 |
+
# Choose patch with fewest assigned SCVs (tie-break: nearest to CC)
|
| 1254 |
+
patch = min(
|
| 1255 |
+
mineral_patches,
|
| 1256 |
+
key=lambda r: (len(r.assigned_scv_ids), (r.x - cx) ** 2 + (r.y - cy) ** 2)
|
| 1257 |
+
)
|
| 1258 |
if not patch:
|
| 1259 |
break
|
| 1260 |
if scv.assigned_resource_id:
|
|
|
|
| 1268 |
self._set_unit_destination(scv, float(patch.x), float(patch.y), is_flying=False)
|
| 1269 |
patch.assigned_scv_ids.append(scv.id)
|
| 1270 |
assigned += 1
|
| 1271 |
+
# Remove full patches from candidates
|
| 1272 |
+
mineral_patches = [r for r in mineral_patches if r.has_capacity]
|
| 1273 |
|
| 1274 |
if assigned == 0:
|
| 1275 |
+
return ActionResult(
|
| 1276 |
+
action_type="gather", success=False, data={"error": "gather_no_resource"}
|
| 1277 |
+
)
|
| 1278 |
+
return ActionResult(
|
| 1279 |
+
action_type="gather", success=True,
|
| 1280 |
+
data={"n": assigned, "resource": resource_type},
|
| 1281 |
+
)
|
| 1282 |
|
| 1283 |
def _cmd_stop(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 1284 |
units = self._resolve_selector(player, action.unit_selector or "all_military")
|
|
|
|
| 1288 |
unit.path_waypoints = []
|
| 1289 |
unit.attack_target_id = None
|
| 1290 |
unit.attack_target_building_id = None
|
| 1291 |
+
return ActionResult(action_type="stop", success=True, data={"n": len(units)})
|
|
|
|
| 1292 |
|
| 1293 |
def _cmd_patrol(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 1294 |
units = self._resolve_selector(player, action.unit_selector or "all_military")
|
| 1295 |
if not units:
|
| 1296 |
+
return ActionResult(action_type="patrol", success=False, data={"error": "no_units_selected"})
|
| 1297 |
tx, ty = self._resolve_zone(player.player_id, action.target_zone or "center")
|
| 1298 |
for unit in units:
|
| 1299 |
if unit.is_sieged:
|
|
|
|
| 1303 |
unit.status = UnitStatus.PATROLLING
|
| 1304 |
self._set_unit_destination(unit, tx, ty, is_flying=UNIT_DEFS[unit.unit_type].is_flying)
|
| 1305 |
move_ack = [{"kind": "move_ack", "unit_type": units[0].unit_type.value}]
|
| 1306 |
+
zone = action.target_zone or "center"
|
| 1307 |
+
return ActionResult(
|
| 1308 |
+
action_type="patrol", success=True,
|
| 1309 |
+
data={"n": len(units), "zone": zone}, sound_events=move_ack,
|
| 1310 |
+
)
|
| 1311 |
|
| 1312 |
def _cmd_query(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 1313 |
+
return ActionResult(
|
| 1314 |
+
action_type="query", success=True,
|
| 1315 |
+
data={"summary": player.summary(self._cmd_lang)},
|
| 1316 |
+
)
|
| 1317 |
|
| 1318 |
def _cmd_query_units(self, player: PlayerState, action: GameAction) -> ActionResult:
|
| 1319 |
"""Query units by zone and/or type; return their IDs in result.unit_ids."""
|
|
|
|
| 1323 |
return ActionResult(
|
| 1324 |
action_type="query_units",
|
| 1325 |
success=True,
|
| 1326 |
+
data={"n": len(ids)},
|
| 1327 |
unit_ids=ids,
|
| 1328 |
)
|
| 1329 |
|
|
|
|
| 1334 |
return ActionResult(
|
| 1335 |
action_type="assign_to_group",
|
| 1336 |
success=False,
|
| 1337 |
+
data={"error": "group_invalid"},
|
| 1338 |
)
|
| 1339 |
ids = list(action.unit_ids) if action.unit_ids else []
|
| 1340 |
valid_ids = [uid for uid in ids if uid in player.units]
|
|
|
|
| 1342 |
return ActionResult(
|
| 1343 |
action_type="assign_to_group",
|
| 1344 |
success=True,
|
| 1345 |
+
data={"gi": gi, "n": len(valid_ids)},
|
| 1346 |
)
|
| 1347 |
|
| 1348 |
# ------------------------------------------------------------------
|
backend/game/map.py
CHANGED
|
@@ -7,25 +7,25 @@ from enum import Enum
|
|
| 7 |
|
| 8 |
from pydantic import BaseModel, Field
|
| 9 |
|
| 10 |
-
MAP_WIDTH =
|
| 11 |
-
MAP_HEIGHT =
|
| 12 |
|
| 13 |
# Starting positions (top-left corner of Command Center footprint) — fallback if no game_positions.json
|
| 14 |
-
PLAYER1_START: tuple[int, int] = (
|
| 15 |
-
PLAYER2_START: tuple[int, int] = (
|
| 16 |
|
| 17 |
# Absolute resource positions (fallback)
|
| 18 |
_P1_MINERALS: list[tuple[int, int]] = [
|
| 19 |
-
(
|
| 20 |
-
(
|
| 21 |
]
|
| 22 |
-
_P1_GEYSERS: list[tuple[int, int]] = [(
|
| 23 |
|
| 24 |
_P2_MINERALS: list[tuple[int, int]] = [
|
| 25 |
-
(
|
| 26 |
-
(
|
| 27 |
]
|
| 28 |
-
_P2_GEYSERS: list[tuple[int, int]] = [(
|
| 29 |
|
| 30 |
|
| 31 |
def _game_positions_path() -> Path | None:
|
|
@@ -33,32 +33,266 @@ def _game_positions_path() -> Path | None:
|
|
| 33 |
return p if p.exists() else None
|
| 34 |
|
| 35 |
|
| 36 |
-
def
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 38 |
import random
|
| 39 |
path = _game_positions_path()
|
| 40 |
if not path:
|
| 41 |
-
return
|
| 42 |
try:
|
| 43 |
with open(path, encoding="utf-8") as f:
|
| 44 |
data = json.load(f)
|
| 45 |
starts = data.get("starting_positions") or []
|
| 46 |
if len(starts) >= 3:
|
| 47 |
-
def to_game(p: dict) -> tuple[int, int]:
|
| 48 |
-
x = p.get("x", 0) * MAP_WIDTH / 100.0
|
| 49 |
-
y = p.get("y", 0) * MAP_HEIGHT / 100.0
|
| 50 |
-
return (max(0, min(MAP_WIDTH - 1, int(round(x)))), max(0, min(MAP_HEIGHT - 1, int(round(y)))))
|
| 51 |
chosen = random.sample(starts, 2)
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
if len(starts) >= 2:
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
|
|
|
| 59 |
except (OSError, json.JSONDecodeError, KeyError):
|
| 60 |
pass
|
| 61 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
|
| 63 |
# Named map zones resolved to (x, y) center coordinates
|
| 64 |
# Zone values depend on player — resolved at engine level using player start positions
|
|
@@ -99,35 +333,32 @@ class GameMap(BaseModel):
|
|
| 99 |
resources: list[Resource] = Field(default_factory=list)
|
| 100 |
|
| 101 |
@classmethod
|
| 102 |
-
def create_default(cls) -> "GameMap":
|
|
|
|
|
|
|
|
|
|
| 103 |
path = _game_positions_path()
|
| 104 |
if path:
|
| 105 |
try:
|
| 106 |
with open(path, encoding="utf-8") as f:
|
| 107 |
data = json.load(f)
|
| 108 |
-
|
|
|
|
| 109 |
for m in data.get("minerals") or []:
|
| 110 |
x, y = int(m.get("x", 0)), int(m.get("y", 0))
|
| 111 |
if 0 <= x < MAP_WIDTH and 0 <= y < MAP_HEIGHT:
|
| 112 |
-
|
| 113 |
for g in data.get("geysers") or []:
|
| 114 |
x, y = int(g.get("x", 0)), int(g.get("y", 0))
|
| 115 |
if 0 <= x < MAP_WIDTH and 0 <= y < MAP_HEIGHT:
|
| 116 |
-
|
| 117 |
-
if
|
| 118 |
-
return cls(resources=
|
| 119 |
except (OSError, json.JSONDecodeError, KeyError):
|
| 120 |
pass
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
for x, y in _P1_GEYSERS:
|
| 125 |
-
resources.append(Resource(resource_type=ResourceType.GEYSER, x=x, y=y))
|
| 126 |
-
for x, y in _P2_MINERALS:
|
| 127 |
-
resources.append(Resource(resource_type=ResourceType.MINERAL, x=x, y=y))
|
| 128 |
-
for x, y in _P2_GEYSERS:
|
| 129 |
-
resources.append(Resource(resource_type=ResourceType.GEYSER, x=x, y=y))
|
| 130 |
-
return cls(resources=resources)
|
| 131 |
|
| 132 |
def get_resource(self, resource_id: str) -> Resource | None:
|
| 133 |
return next((r for r in self.resources if r.id == resource_id), None)
|
|
|
|
| 7 |
|
| 8 |
from pydantic import BaseModel, Field
|
| 9 |
|
| 10 |
+
MAP_WIDTH = 80
|
| 11 |
+
MAP_HEIGHT = 80
|
| 12 |
|
| 13 |
# Starting positions (top-left corner of Command Center footprint) — fallback if no game_positions.json
|
| 14 |
+
PLAYER1_START: tuple[int, int] = (8, 10)
|
| 15 |
+
PLAYER2_START: tuple[int, int] = (64, 64)
|
| 16 |
|
| 17 |
# Absolute resource positions (fallback)
|
| 18 |
_P1_MINERALS: list[tuple[int, int]] = [
|
| 19 |
+
(4, 4), (6, 4), (8, 4), (10, 4), (12, 4),
|
| 20 |
+
(4, 6), (12, 6), (6, 18),
|
| 21 |
]
|
| 22 |
+
_P1_GEYSERS: list[tuple[int, int]] = [(4, 18), (14, 18)]
|
| 23 |
|
| 24 |
_P2_MINERALS: list[tuple[int, int]] = [
|
| 25 |
+
(66, 74), (68, 74), (70, 74), (72, 74), (74, 74),
|
| 26 |
+
(66, 72), (74, 72), (68, 60),
|
| 27 |
]
|
| 28 |
+
_P2_GEYSERS: list[tuple[int, int]] = [(64, 60), (74, 60)]
|
| 29 |
|
| 30 |
|
| 31 |
def _game_positions_path() -> Path | None:
|
|
|
|
| 33 |
return p if p.exists() else None
|
| 34 |
|
| 35 |
|
| 36 |
+
def _to_game_coords(x: float, y: float) -> tuple[int, int]:
|
| 37 |
+
gx = max(0, min(MAP_WIDTH - 1, int(round(x * MAP_WIDTH / 100.0))))
|
| 38 |
+
gy = max(0, min(MAP_HEIGHT - 1, int(round(y * MAP_HEIGHT / 100.0))))
|
| 39 |
+
return (gx, gy)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def _resources_from_start_entry(entry: dict) -> list["Resource"]:
|
| 43 |
+
"""Build list of Resource from a starting_position entry that has nested minerals/geysers."""
|
| 44 |
+
resources: list[Resource] = []
|
| 45 |
+
for m in entry.get("minerals") or []:
|
| 46 |
+
x, y = int(m.get("x", 0)), int(m.get("y", 0))
|
| 47 |
+
if 0 <= x < MAP_WIDTH and 0 <= y < MAP_HEIGHT:
|
| 48 |
+
resources.append(Resource(resource_type=ResourceType.MINERAL, x=x, y=y))
|
| 49 |
+
for g in entry.get("geysers") or []:
|
| 50 |
+
x, y = int(g.get("x", 0)), int(g.get("y", 0))
|
| 51 |
+
if 0 <= x < MAP_WIDTH and 0 <= y < MAP_HEIGHT:
|
| 52 |
+
resources.append(Resource(resource_type=ResourceType.GEYSER, x=x, y=y))
|
| 53 |
+
return resources
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
# 8 minerals in a ring at ~6 tiles from center, 2 geysers at ~8 tiles.
|
| 57 |
+
# All distances are deterministic so every base gets the same layout.
|
| 58 |
+
_RESOURCE_OFFSETS_MINERAL = [
|
| 59 |
+
(6, 0), (5, 3), (0, 6), (-5, 3), (-6, 0), (-5, -3), (0, -6), (5, -3),
|
| 60 |
+
]
|
| 61 |
+
_RESOURCE_OFFSETS_GEYSER = [(7, 4), (-7, 4)]
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def _generate_resources_at(gx: int, gy: int) -> list["Resource"]:
|
| 65 |
+
"""Generate 8 mineral patches and 2 geysers at fixed distances around a game coordinate.
|
| 66 |
+
|
| 67 |
+
Every base always gets the same symmetric layout so distances are consistent.
|
| 68 |
+
"""
|
| 69 |
+
resources: list[Resource] = []
|
| 70 |
+
seen: set[tuple[int, int]] = set()
|
| 71 |
+
for dx, dy in _RESOURCE_OFFSETS_MINERAL:
|
| 72 |
+
x = max(0, min(MAP_WIDTH - 1, gx + dx))
|
| 73 |
+
y = max(0, min(MAP_HEIGHT - 1, gy + dy))
|
| 74 |
+
if (x, y) not in seen:
|
| 75 |
+
seen.add((x, y))
|
| 76 |
+
resources.append(Resource(resource_type=ResourceType.MINERAL, x=x, y=y))
|
| 77 |
+
for dx, dy in _RESOURCE_OFFSETS_GEYSER:
|
| 78 |
+
x = max(0, min(MAP_WIDTH - 1, gx + dx))
|
| 79 |
+
y = max(0, min(MAP_HEIGHT - 1, gy + dy))
|
| 80 |
+
if (x, y) not in seen:
|
| 81 |
+
seen.add((x, y))
|
| 82 |
+
resources.append(Resource(resource_type=ResourceType.GEYSER, x=x, y=y))
|
| 83 |
+
return resources
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
def get_start_positions() -> tuple[tuple[float, float], tuple[float, float]]:
|
| 87 |
+
"""Return (player1_start, player2_start) in game coords. With 3 positions in file, 2 are chosen at random."""
|
| 88 |
+
start1, _, start2, _ = get_start_data()
|
| 89 |
+
return start1, start2
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def get_all_map_resources() -> list["Resource"]:
|
| 93 |
+
"""Return resources for ALL starting positions AND expansion positions.
|
| 94 |
+
|
| 95 |
+
Resources are ALWAYS generated via _generate_resources_at() using fixed offsets so that
|
| 96 |
+
every base — regardless of origin — has minerals and geysers at a consistent distance
|
| 97 |
+
from its Command Center. Embedded minerals in game_positions.json are intentionally
|
| 98 |
+
ignored in favour of this deterministic layout.
|
| 99 |
+
"""
|
| 100 |
+
path = _game_positions_path()
|
| 101 |
+
if path:
|
| 102 |
+
try:
|
| 103 |
+
with open(path, encoding="utf-8") as f:
|
| 104 |
+
data = json.load(f)
|
| 105 |
+
all_entries = (
|
| 106 |
+
list(data.get("starting_positions") or [])
|
| 107 |
+
+ list(data.get("expansion_positions") or [])
|
| 108 |
+
)
|
| 109 |
+
if all_entries:
|
| 110 |
+
resources: list[Resource] = []
|
| 111 |
+
for entry in all_entries:
|
| 112 |
+
gx, gy = _to_game_coords(float(entry.get("x", 0)), float(entry.get("y", 0)))
|
| 113 |
+
resources.extend(_generate_resources_at(gx, gy))
|
| 114 |
+
return resources
|
| 115 |
+
except (OSError, json.JSONDecodeError, KeyError):
|
| 116 |
+
pass
|
| 117 |
+
# Fallback: derive starts + expansions from nav_points
|
| 118 |
+
compiled_path = Path(__file__).resolve().parent.parent / "static" / "compiled_map.json"
|
| 119 |
+
if compiled_path.exists():
|
| 120 |
+
try:
|
| 121 |
+
with open(compiled_path, encoding="utf-8") as f:
|
| 122 |
+
nav_data = json.load(f)
|
| 123 |
+
pts = nav_data.get("nav_points") or []
|
| 124 |
+
if len(pts) >= 4:
|
| 125 |
+
coords = [(float(p[0]), float(p[1])) for p in pts]
|
| 126 |
+
starts, expansions, _ = _fallback_all_resources(coords)
|
| 127 |
+
resources = []
|
| 128 |
+
for pos in starts + expansions:
|
| 129 |
+
gx, gy = int(round(pos[0])), int(round(pos[1]))
|
| 130 |
+
resources.extend(_generate_resources_at(gx, gy))
|
| 131 |
+
return resources
|
| 132 |
+
except (OSError, json.JSONDecodeError, KeyError, ValueError):
|
| 133 |
+
pass
|
| 134 |
+
_, r1, _, r2 = _fallback_from_nav()
|
| 135 |
+
return r1 + r2
|
| 136 |
+
|
| 137 |
+
|
| 138 |
+
def get_start_data() -> tuple[
|
| 139 |
+
tuple[float, float], list[Resource], tuple[float, float], list[Resource]
|
| 140 |
+
]:
|
| 141 |
+
"""Return (start1, resources1, start2, resources2) for the 2 chosen bases. With 3 positions, 2 are chosen at random; resources are only those for the chosen bases."""
|
| 142 |
import random
|
| 143 |
path = _game_positions_path()
|
| 144 |
if not path:
|
| 145 |
+
return _fallback_from_nav()
|
| 146 |
try:
|
| 147 |
with open(path, encoding="utf-8") as f:
|
| 148 |
data = json.load(f)
|
| 149 |
starts = data.get("starting_positions") or []
|
| 150 |
if len(starts) >= 3:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 151 |
chosen = random.sample(starts, 2)
|
| 152 |
+
s1, s2 = chosen[0], chosen[1]
|
| 153 |
+
start1 = _to_game_coords(s1.get("x", 0), s1.get("y", 0))
|
| 154 |
+
start2 = _to_game_coords(s2.get("x", 0), s2.get("y", 0))
|
| 155 |
+
res1 = _resources_from_start_entry(s1) if "minerals" in s1 else []
|
| 156 |
+
res2 = _resources_from_start_entry(s2) if "minerals" in s2 else []
|
| 157 |
+
return (start1, res1, start2, res2)
|
| 158 |
if len(starts) >= 2:
|
| 159 |
+
s1, s2 = starts[0], starts[1]
|
| 160 |
+
start1 = _to_game_coords(s1.get("x", 0), s1.get("y", 0))
|
| 161 |
+
start2 = _to_game_coords(s2.get("x", 0), s2.get("y", 0))
|
| 162 |
+
res1 = _resources_from_start_entry(s1) if "minerals" in s1 else []
|
| 163 |
+
res2 = _resources_from_start_entry(s2) if "minerals" in s2 else []
|
| 164 |
+
return (start1, res1, start2, res2)
|
| 165 |
except (OSError, json.JSONDecodeError, KeyError):
|
| 166 |
pass
|
| 167 |
+
return _fallback_from_nav()
|
| 168 |
+
|
| 169 |
+
|
| 170 |
+
def _pick_nav_corners(coords: list[tuple[float, float]]) -> tuple[tuple[float, float], tuple[float, float]]:
|
| 171 |
+
"""Pick the 2 most separated nav_points (opposing corners)."""
|
| 172 |
+
pa1 = min(coords, key=lambda p: p[0] + p[1])
|
| 173 |
+
pa2 = max(coords, key=lambda p: p[0] + p[1])
|
| 174 |
+
pb1 = min(coords, key=lambda p: p[0] - p[1])
|
| 175 |
+
pb2 = max(coords, key=lambda p: p[0] - p[1])
|
| 176 |
+
d_a = (pa2[0] - pa1[0]) ** 2 + (pa2[1] - pa1[1]) ** 2
|
| 177 |
+
d_b = (pb2[0] - pb1[0]) ** 2 + (pb2[1] - pb1[1]) ** 2
|
| 178 |
+
return (pa1, pa2) if d_a >= d_b else (pb1, pb2)
|
| 179 |
+
|
| 180 |
+
|
| 181 |
+
def _pick_expansion_nav_positions(
|
| 182 |
+
coords: list[tuple[float, float]],
|
| 183 |
+
start_positions: list[tuple[float, float]],
|
| 184 |
+
count: int = 3,
|
| 185 |
+
min_dist_from_start: float = 12.0,
|
| 186 |
+
) -> list[tuple[float, float]]:
|
| 187 |
+
"""Pick expansion positions: closest nav_point to each pair midpoint, at least min_dist from any start."""
|
| 188 |
+
def dist2(a: tuple[float, float], b: tuple[float, float]) -> float:
|
| 189 |
+
return (a[0] - b[0]) ** 2 + (a[1] - b[1]) ** 2
|
| 190 |
+
|
| 191 |
+
candidates: list[tuple[float, float]] = []
|
| 192 |
+
n = len(start_positions)
|
| 193 |
+
for i in range(n):
|
| 194 |
+
for j in range(i + 1, n):
|
| 195 |
+
mx = (start_positions[i][0] + start_positions[j][0]) / 2
|
| 196 |
+
my = (start_positions[i][1] + start_positions[j][1]) / 2
|
| 197 |
+
# Find closest nav_point to midpoint that is far enough from all starts
|
| 198 |
+
nearby = [
|
| 199 |
+
p for p in coords
|
| 200 |
+
if all(dist2(p, s) >= min_dist_from_start ** 2 for s in start_positions)
|
| 201 |
+
]
|
| 202 |
+
if nearby:
|
| 203 |
+
best = min(nearby, key=lambda p: dist2(p, (mx, my)))
|
| 204 |
+
# Avoid duplicates
|
| 205 |
+
if all(dist2(best, c) > 4.0 for c in candidates):
|
| 206 |
+
candidates.append(best)
|
| 207 |
+
|
| 208 |
+
return candidates[:count]
|
| 209 |
+
|
| 210 |
+
|
| 211 |
+
def _fallback_all_resources(
|
| 212 |
+
coords: list[tuple[float, float]],
|
| 213 |
+
) -> tuple[list[tuple[float, float]], list[tuple[float, float]], list[list[Resource]]]:
|
| 214 |
+
"""Pick start and expansion positions from nav_points and generate resources at each.
|
| 215 |
+
|
| 216 |
+
Returns (start_positions, expansion_positions, all_resource_lists).
|
| 217 |
+
Resources are generated via _generate_resources_at for consistent fixed-distance placement.
|
| 218 |
+
"""
|
| 219 |
+
s1_f, s2_f = _pick_nav_corners(coords)
|
| 220 |
+
def min_dist2(p: tuple[float, float]) -> float:
|
| 221 |
+
return min((p[0]-s1_f[0])**2+(p[1]-s1_f[1])**2, (p[0]-s2_f[0])**2+(p[1]-s2_f[1])**2)
|
| 222 |
+
s3_f = max(coords, key=min_dist2)
|
| 223 |
+
|
| 224 |
+
starts = [s1_f, s2_f, s3_f]
|
| 225 |
+
expansions = _pick_expansion_nav_positions(coords, starts, count=3)
|
| 226 |
+
|
| 227 |
+
all_resources: list[list[Resource]] = []
|
| 228 |
+
for pos in starts + expansions:
|
| 229 |
+
gx, gy = int(round(pos[0])), int(round(pos[1]))
|
| 230 |
+
all_resources.append(_generate_resources_at(gx, gy))
|
| 231 |
+
|
| 232 |
+
return starts, expansions, all_resources
|
| 233 |
+
|
| 234 |
+
|
| 235 |
+
def _fallback_from_nav() -> tuple[
|
| 236 |
+
tuple[int, int], list[Resource], tuple[int, int], list[Resource]
|
| 237 |
+
]:
|
| 238 |
+
"""Pick 2 well-separated start positions from compiled nav_points and generate minerals near each."""
|
| 239 |
+
compiled_path = Path(__file__).resolve().parent.parent / "static" / "compiled_map.json"
|
| 240 |
+
if compiled_path.exists():
|
| 241 |
+
try:
|
| 242 |
+
with open(compiled_path, encoding="utf-8") as f:
|
| 243 |
+
data = json.load(f)
|
| 244 |
+
pts = data.get("nav_points") or []
|
| 245 |
+
if len(pts) >= 4:
|
| 246 |
+
coords = [(float(p[0]), float(p[1])) for p in pts]
|
| 247 |
+
s1_f, s2_f = _pick_nav_corners(coords)
|
| 248 |
+
res1 = _nav_minerals_around(s1_f, coords)
|
| 249 |
+
res2 = _nav_minerals_around(s2_f, coords)
|
| 250 |
+
return (s1_f, res1, s2_f, res2)
|
| 251 |
+
except (OSError, json.JSONDecodeError, KeyError, ValueError):
|
| 252 |
+
pass
|
| 253 |
+
# Last resort: use original hardcoded values (may be outside walkable area on this map)
|
| 254 |
+
resources: list[Resource] = []
|
| 255 |
+
for x, y in _P1_MINERALS:
|
| 256 |
+
resources.append(Resource(resource_type=ResourceType.MINERAL, x=x, y=y))
|
| 257 |
+
for x, y in _P1_GEYSERS:
|
| 258 |
+
resources.append(Resource(resource_type=ResourceType.GEYSER, x=x, y=y))
|
| 259 |
+
r2: list[Resource] = []
|
| 260 |
+
for x, y in _P2_MINERALS:
|
| 261 |
+
r2.append(Resource(resource_type=ResourceType.MINERAL, x=x, y=y))
|
| 262 |
+
for x, y in _P2_GEYSERS:
|
| 263 |
+
r2.append(Resource(resource_type=ResourceType.GEYSER, x=x, y=y))
|
| 264 |
+
return (
|
| 265 |
+
(float(PLAYER1_START[0]), float(PLAYER1_START[1])),
|
| 266 |
+
resources,
|
| 267 |
+
(float(PLAYER2_START[0]), float(PLAYER2_START[1])),
|
| 268 |
+
r2,
|
| 269 |
+
)
|
| 270 |
+
|
| 271 |
+
|
| 272 |
+
def _nav_minerals_around(
|
| 273 |
+
center: tuple[float, float],
|
| 274 |
+
all_nav: list[tuple[float, float]],
|
| 275 |
+
mineral_count: int = 7,
|
| 276 |
+
geyser_count: int = 1,
|
| 277 |
+
mineral_radius: float = 6.0,
|
| 278 |
+
geyser_radius: float = 8.0,
|
| 279 |
+
) -> list[Resource]:
|
| 280 |
+
"""Generate minerals and geysers at nav_points near a start position."""
|
| 281 |
+
cx, cy = center
|
| 282 |
+
nearby = sorted(
|
| 283 |
+
[p for p in all_nav if p != center and (p[0]-cx)**2 + (p[1]-cy)**2 <= mineral_radius**2],
|
| 284 |
+
key=lambda p: (p[0]-cx)**2 + (p[1]-cy)**2,
|
| 285 |
+
)
|
| 286 |
+
resources: list[Resource] = []
|
| 287 |
+
for p in nearby[:mineral_count]:
|
| 288 |
+
resources.append(Resource(resource_type=ResourceType.MINERAL, x=int(round(p[0])), y=int(round(p[1]))))
|
| 289 |
+
geyser_nearby = sorted(
|
| 290 |
+
[p for p in all_nav if p != center and (p[0]-cx)**2 + (p[1]-cy)**2 <= geyser_radius**2],
|
| 291 |
+
key=lambda p: -((p[0]-cx)**2 + (p[1]-cy)**2),
|
| 292 |
+
)
|
| 293 |
+
for p in geyser_nearby[:geyser_count]:
|
| 294 |
+
resources.append(Resource(resource_type=ResourceType.GEYSER, x=int(round(p[0])), y=int(round(p[1]))))
|
| 295 |
+
return resources
|
| 296 |
|
| 297 |
# Named map zones resolved to (x, y) center coordinates
|
| 298 |
# Zone values depend on player — resolved at engine level using player start positions
|
|
|
|
| 333 |
resources: list[Resource] = Field(default_factory=list)
|
| 334 |
|
| 335 |
@classmethod
|
| 336 |
+
def create_default(cls, resources: list[Resource] | None = None) -> "GameMap":
|
| 337 |
+
"""Create map with given resources, or load from file (legacy flat minerals/geysers), or use hardcoded fallback."""
|
| 338 |
+
if resources is not None:
|
| 339 |
+
return cls(resources=resources)
|
| 340 |
path = _game_positions_path()
|
| 341 |
if path:
|
| 342 |
try:
|
| 343 |
with open(path, encoding="utf-8") as f:
|
| 344 |
data = json.load(f)
|
| 345 |
+
# Legacy format: top-level minerals/geysers
|
| 346 |
+
flat_resources = []
|
| 347 |
for m in data.get("minerals") or []:
|
| 348 |
x, y = int(m.get("x", 0)), int(m.get("y", 0))
|
| 349 |
if 0 <= x < MAP_WIDTH and 0 <= y < MAP_HEIGHT:
|
| 350 |
+
flat_resources.append(Resource(resource_type=ResourceType.MINERAL, x=x, y=y))
|
| 351 |
for g in data.get("geysers") or []:
|
| 352 |
x, y = int(g.get("x", 0)), int(g.get("y", 0))
|
| 353 |
if 0 <= x < MAP_WIDTH and 0 <= y < MAP_HEIGHT:
|
| 354 |
+
flat_resources.append(Resource(resource_type=ResourceType.GEYSER, x=x, y=y))
|
| 355 |
+
if flat_resources:
|
| 356 |
+
return cls(resources=flat_resources)
|
| 357 |
except (OSError, json.JSONDecodeError, KeyError):
|
| 358 |
pass
|
| 359 |
+
# Use nav-based fallback which derives positions from the actual compiled map
|
| 360 |
+
_, r1, _, r2 = _fallback_from_nav()
|
| 361 |
+
return cls(resources=r1 + r2)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 362 |
|
| 363 |
def get_resource(self, resource_id: str) -> Resource | None:
|
| 364 |
return next((r for r in self.resources if r.id == resource_id), None)
|
backend/game/map_compiler.py
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Compile walkable polygons into a single figure: one exterior boundary + holes (internal non-walkable zones).
|
| 3 |
+
Produces compiled_map.json used by pathfinding for walkable test and optional nav points.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
from pathlib import Path
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
def _signed_area(polygon: list[tuple[float, float]]) -> float:
|
| 13 |
+
"""Signed area (positive = CCW)."""
|
| 14 |
+
n = len(polygon)
|
| 15 |
+
if n < 3:
|
| 16 |
+
return 0.0
|
| 17 |
+
area = 0.0
|
| 18 |
+
for i in range(n):
|
| 19 |
+
j = (i + 1) % n
|
| 20 |
+
area += polygon[i][0] * polygon[j][1]
|
| 21 |
+
area -= polygon[j][0] * polygon[i][1]
|
| 22 |
+
return area / 2.0
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def _centroid(polygon: list[tuple[float, float]]) -> tuple[float, float]:
|
| 26 |
+
n = len(polygon)
|
| 27 |
+
if n == 0:
|
| 28 |
+
return (0.0, 0.0)
|
| 29 |
+
cx = sum(p[0] for p in polygon) / n
|
| 30 |
+
cy = sum(p[1] for p in polygon) / n
|
| 31 |
+
return (cx, cy)
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _point_in_polygon(x: float, y: float, polygon: list[tuple[float, float]]) -> bool:
|
| 35 |
+
n = len(polygon)
|
| 36 |
+
inside = False
|
| 37 |
+
j = n - 1
|
| 38 |
+
for i in range(n):
|
| 39 |
+
xi, yi = polygon[i]
|
| 40 |
+
xj, yj = polygon[j]
|
| 41 |
+
if ((yi > y) != (yj > y)) and (x < (xj - xi) * (y - yi) / (yj - yi) + xi):
|
| 42 |
+
inside = not inside
|
| 43 |
+
j = i
|
| 44 |
+
return inside
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def compile_walkable(
|
| 48 |
+
polygons: list[list[tuple[float, float]]],
|
| 49 |
+
) -> tuple[list[tuple[float, float]], list[list[tuple[float, float]]]]:
|
| 50 |
+
"""
|
| 51 |
+
From a list of polygons (each list of (x,y) in 0-100 space),
|
| 52 |
+
return (exterior, holes).
|
| 53 |
+
Exterior = polygon with largest absolute area.
|
| 54 |
+
Holes = polygons whose centroid is inside the exterior.
|
| 55 |
+
"""
|
| 56 |
+
if not polygons:
|
| 57 |
+
return ([], [])
|
| 58 |
+
|
| 59 |
+
# Filter to valid polygons
|
| 60 |
+
valid = [p for p in polygons if len(p) >= 3]
|
| 61 |
+
if not valid:
|
| 62 |
+
return ([], [])
|
| 63 |
+
|
| 64 |
+
# Exterior = largest area
|
| 65 |
+
by_area = [(abs(_signed_area(p)), p) for p in valid]
|
| 66 |
+
by_area.sort(key=lambda x: -x[0])
|
| 67 |
+
exterior = by_area[0][1]
|
| 68 |
+
others = [p for _, p in by_area[1:]]
|
| 69 |
+
|
| 70 |
+
holes: list[list[tuple[float, float]]] = []
|
| 71 |
+
for poly in others:
|
| 72 |
+
cx, cy = _centroid(poly)
|
| 73 |
+
if _point_in_polygon(cx, cy, exterior):
|
| 74 |
+
holes.append(poly)
|
| 75 |
+
|
| 76 |
+
return (exterior, holes)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def build_nav_points(
|
| 80 |
+
polygons: list[list[tuple[float, float]]],
|
| 81 |
+
scale_x: float,
|
| 82 |
+
scale_y: float,
|
| 83 |
+
step: float = 2.0,
|
| 84 |
+
) -> list[list[float]]:
|
| 85 |
+
"""
|
| 86 |
+
Generate navigation points (in game coordinates) for ALL walkable polygons.
|
| 87 |
+
Used for pathfinding graph: units can path between these points.
|
| 88 |
+
"""
|
| 89 |
+
if not polygons:
|
| 90 |
+
return []
|
| 91 |
+
|
| 92 |
+
all_xs = [p[0] for poly in polygons for p in poly]
|
| 93 |
+
all_ys = [p[1] for poly in polygons for p in poly]
|
| 94 |
+
min_x, max_x = min(all_xs), max(all_xs)
|
| 95 |
+
min_y, max_y = min(all_ys), max(all_ys)
|
| 96 |
+
|
| 97 |
+
def is_walkable_100(x: float, y: float) -> bool:
|
| 98 |
+
return any(_point_in_polygon(x, y, poly) for poly in polygons)
|
| 99 |
+
|
| 100 |
+
points: list[list[float]] = []
|
| 101 |
+
x = min_x
|
| 102 |
+
while x <= max_x:
|
| 103 |
+
y = min_y
|
| 104 |
+
while y <= max_y:
|
| 105 |
+
if is_walkable_100(x, y):
|
| 106 |
+
points.append([x * scale_x / 100.0, y * scale_y / 100.0])
|
| 107 |
+
y += step
|
| 108 |
+
x += step
|
| 109 |
+
return points
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
def run_compiler(
|
| 113 |
+
walkable_path: Path,
|
| 114 |
+
output_path: Path,
|
| 115 |
+
map_width: float = 80.0,
|
| 116 |
+
map_height: float = 80.0,
|
| 117 |
+
nav_step: float = 2.0,
|
| 118 |
+
) -> None:
|
| 119 |
+
"""Load walkable.json, compile to exterior + holes, write compiled_map.json."""
|
| 120 |
+
scale_x = map_width / 100.0
|
| 121 |
+
scale_y = map_height / 100.0
|
| 122 |
+
|
| 123 |
+
if not walkable_path.exists():
|
| 124 |
+
raise FileNotFoundError(f"Walkable file not found: {walkable_path}")
|
| 125 |
+
|
| 126 |
+
with open(walkable_path, encoding="utf-8") as f:
|
| 127 |
+
data = json.load(f)
|
| 128 |
+
|
| 129 |
+
raw = data.get("polygons", [])
|
| 130 |
+
if not raw and data.get("polygon"):
|
| 131 |
+
raw = [data["polygon"]]
|
| 132 |
+
|
| 133 |
+
polygons: list[list[tuple[float, float]]] = []
|
| 134 |
+
for poly in raw:
|
| 135 |
+
if len(poly) < 3:
|
| 136 |
+
continue
|
| 137 |
+
polygons.append([(float(p[0]), float(p[1])) for p in poly])
|
| 138 |
+
|
| 139 |
+
exterior, holes = compile_walkable(polygons)
|
| 140 |
+
|
| 141 |
+
nav_points_game = build_nav_points(polygons, map_width, map_height, step=nav_step)
|
| 142 |
+
|
| 143 |
+
out = {
|
| 144 |
+
"exterior": [[round(x, 4), round(y, 4)] for x, y in exterior],
|
| 145 |
+
"holes": [[[round(x, 4), round(y, 4)] for x, y in h] for h in holes],
|
| 146 |
+
"nav_points": [[round(x, 4), round(y, 4)] for x, y in nav_points_game],
|
| 147 |
+
}
|
| 148 |
+
with open(output_path, "w", encoding="utf-8") as f:
|
| 149 |
+
json.dump(out, f, indent=2)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
if __name__ == "__main__":
|
| 153 |
+
static_dir = Path(__file__).resolve().parent.parent / "static"
|
| 154 |
+
run_compiler(
|
| 155 |
+
static_dir / "walkable.json",
|
| 156 |
+
static_dir / "compiled_map.json",
|
| 157 |
+
)
|
backend/game/pathfinding.py
CHANGED
|
@@ -1,28 +1,150 @@
|
|
| 1 |
"""
|
| 2 |
-
Pathfinding constrained to walkable
|
| 3 |
-
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
from __future__ import annotations
|
| 7 |
|
| 8 |
import heapq
|
| 9 |
import json
|
|
|
|
| 10 |
from pathlib import Path
|
| 11 |
from typing import Optional
|
| 12 |
|
| 13 |
from .map import MAP_HEIGHT, MAP_WIDTH
|
| 14 |
|
| 15 |
-
# Walkable data is in 0-100; game is 0-40
|
| 16 |
-
WALKABLE_SCALE = 0.01 # 100 -> 1.0, then we scale by MAP_WIDTH/MAP_HEIGHT
|
| 17 |
-
# So game_x = raw_x * (MAP_WIDTH / 100) = raw_x * 0.4
|
| 18 |
SCALE_X = MAP_WIDTH / 100.0
|
| 19 |
SCALE_Y = MAP_HEIGHT / 100.0
|
| 20 |
|
| 21 |
_GRID_STEP = 0.5
|
| 22 |
_GRID_W = int(MAP_WIDTH / _GRID_STEP) + 1
|
| 23 |
_GRID_H = int(MAP_HEIGHT / _GRID_STEP) + 1
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
_polygons_cache: Optional[list[list[tuple[float, float]]]] = None
|
| 25 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
|
| 27 |
def _load_polygons() -> list[list[tuple[float, float]]]:
|
| 28 |
global _polygons_cache
|
|
@@ -63,8 +185,12 @@ def _point_in_polygon(x: float, y: float, polygon: list[tuple[float, float]]) ->
|
|
| 63 |
return inside
|
| 64 |
|
| 65 |
|
| 66 |
-
def
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
if x < 0 or x > MAP_WIDTH or y < 0 or y > MAP_HEIGHT:
|
| 69 |
return False
|
| 70 |
polygons = _load_polygons()
|
|
@@ -76,6 +202,25 @@ def is_walkable(x: float, y: float) -> bool:
|
|
| 76 |
return False
|
| 77 |
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
def _cell_center(ci: int, cj: int) -> tuple[float, float]:
|
| 80 |
return (ci * _GRID_STEP, cj * _GRID_STEP)
|
| 81 |
|
|
@@ -88,35 +233,126 @@ def _to_cell(x: float, y: float) -> tuple[int, int]:
|
|
| 88 |
return (ci, cj)
|
| 89 |
|
| 90 |
|
| 91 |
-
def _cell_walkable(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
cx, cy = _cell_center(ci, cj)
|
| 93 |
-
return is_walkable(cx, cy)
|
| 94 |
|
| 95 |
|
| 96 |
-
def _neighbors(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
out: list[tuple[int, int, float]] = []
|
| 98 |
for di in (-1, 0, 1):
|
| 99 |
for dj in (-1, 0, 1):
|
| 100 |
if di == 0 and dj == 0:
|
| 101 |
continue
|
| 102 |
ni, nj = ci + di, cj + dj
|
| 103 |
-
if 0 <= ni < _GRID_W and 0 <= nj < _GRID_H and _cell_walkable(ni, nj):
|
| 104 |
cost = 1.414 if di != 0 and dj != 0 else 1.0
|
| 105 |
out.append((ni, nj, cost))
|
| 106 |
return out
|
| 107 |
|
| 108 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 109 |
def find_path(
|
| 110 |
-
sx: float,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 111 |
) -> Optional[list[tuple[float, float]]]:
|
| 112 |
"""
|
| 113 |
A* path from (sx,sy) to (tx,ty) in game coordinates.
|
|
|
|
|
|
|
| 114 |
Returns list of waypoints (including end), or None if no path.
|
| 115 |
-
Returns [] if start/end not walkable or start==end.
|
| 116 |
"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
si, sj = _to_cell(sx, sy)
|
| 118 |
ti, tj = _to_cell(tx, ty)
|
| 119 |
-
if not _cell_walkable(si, sj) or not _cell_walkable(ti, tj):
|
| 120 |
return None
|
| 121 |
if si == ti and sj == tj:
|
| 122 |
return [(tx, ty)]
|
|
@@ -124,59 +360,85 @@ def find_path(
|
|
| 124 |
def heuristic(i: int, j: int) -> float:
|
| 125 |
return ((ti - i) ** 2 + (tj - j) ** 2) ** 0.5
|
| 126 |
|
| 127 |
-
# (f, counter, (ci, cj), path)
|
| 128 |
counter = 0
|
| 129 |
-
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
(heuristic(si, sj), counter, si, sj, [(si, sj)])
|
| 133 |
-
)
|
| 134 |
counter += 1
|
| 135 |
-
closed
|
|
|
|
|
|
|
| 136 |
|
| 137 |
while open_set:
|
| 138 |
-
|
| 139 |
-
if (ci, cj)
|
| 140 |
-
continue
|
| 141 |
-
closed.add((ci, cj))
|
| 142 |
if ci == ti and cj == tj:
|
| 143 |
-
#
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
if waypoints:
|
| 146 |
waypoints[-1] = (tx, ty)
|
| 147 |
else:
|
| 148 |
waypoints = [(tx, ty)]
|
| 149 |
return waypoints
|
| 150 |
-
for ni, nj, cost in _neighbors(ci, cj):
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
| 158 |
-
f = g + heuristic(ni, nj)
|
| 159 |
-
heapq.heappush(open_set, (f, counter, ni, nj, new_path))
|
| 160 |
-
counter += 1
|
| 161 |
return None
|
| 162 |
|
| 163 |
|
| 164 |
-
def
|
| 165 |
-
|
| 166 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 167 |
return (x, y)
|
| 168 |
best = (x, y)
|
| 169 |
best_d = 1e9
|
| 170 |
-
for radius in [0.5, 1.0, 1.5, 2.0]:
|
| 171 |
-
for dx in [-radius, 0, radius]:
|
| 172 |
-
for dy in [-radius, 0, radius]:
|
| 173 |
-
if dx == 0 and dy == 0
|
| 174 |
continue
|
| 175 |
nx = max(0, min(MAP_WIDTH, x + dx))
|
| 176 |
ny = max(0, min(MAP_HEIGHT, y + dy))
|
| 177 |
-
if is_walkable(nx, ny):
|
| 178 |
d = (nx - x) ** 2 + (ny - y) ** 2
|
| 179 |
if d < best_d:
|
| 180 |
best_d = d
|
| 181 |
best = (nx, ny)
|
|
|
|
|
|
|
| 182 |
return best
|
|
|
|
| 1 |
"""
|
| 2 |
+
Pathfinding constrained to walkable area: one exterior polygon + holes (compiled_map.json),
|
| 3 |
+
or fallback to walkable.json polygons. Supports dynamic obstacles (building footprints).
|
| 4 |
+
Coordinates: sources use 0-100; game uses 0-MAP_WIDTH, 0-MAP_HEIGHT.
|
| 5 |
"""
|
| 6 |
|
| 7 |
from __future__ import annotations
|
| 8 |
|
| 9 |
import heapq
|
| 10 |
import json
|
| 11 |
+
import math
|
| 12 |
from pathlib import Path
|
| 13 |
from typing import Optional
|
| 14 |
|
| 15 |
from .map import MAP_HEIGHT, MAP_WIDTH
|
| 16 |
|
|
|
|
|
|
|
|
|
|
| 17 |
SCALE_X = MAP_WIDTH / 100.0
|
| 18 |
SCALE_Y = MAP_HEIGHT / 100.0
|
| 19 |
|
| 20 |
_GRID_STEP = 0.5
|
| 21 |
_GRID_W = int(MAP_WIDTH / _GRID_STEP) + 1
|
| 22 |
_GRID_H = int(MAP_HEIGHT / _GRID_STEP) + 1
|
| 23 |
+
|
| 24 |
+
# Compiled map: (exterior_scaled, holes_scaled) in game coords, or None
|
| 25 |
+
_compiled_cache: Optional[tuple[list[tuple[float, float]], list[list[tuple[float, float]]]]] = None
|
| 26 |
+
# Legacy: list of polygons in game coords (when no compiled map)
|
| 27 |
_polygons_cache: Optional[list[list[tuple[float, float]]]] = None
|
| 28 |
|
| 29 |
+
# Nav graph: (points_list, adjacency_dict). points_list[i] = (x, y) in game coords; adjacency[i] = [(j, cost), ...]
|
| 30 |
+
_nav_graph_cache: Optional[tuple[list[tuple[float, float]], dict[int, list[tuple[int, float]]]]] = None
|
| 31 |
+
# step=2.0 → cardinal dist=2.0, diagonal dist≈2.83; use 2.9 to include diagonals
|
| 32 |
+
_NAV_CONNECT_RADIUS = 2.9
|
| 33 |
+
# Clearance margin around buildings when filtering nav points (should match UNIT_RADIUS in engine)
|
| 34 |
+
_NAV_BUILDING_MARGIN = 0.6
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def _load_compiled() -> Optional[tuple[list[tuple[float, float]], list[list[tuple[float, float]]]]]:
|
| 38 |
+
global _compiled_cache
|
| 39 |
+
if _compiled_cache is not None:
|
| 40 |
+
return _compiled_cache
|
| 41 |
+
path = Path(__file__).resolve().parent.parent / "static" / "compiled_map.json"
|
| 42 |
+
if not path.exists():
|
| 43 |
+
return None
|
| 44 |
+
try:
|
| 45 |
+
with open(path, encoding="utf-8") as f:
|
| 46 |
+
data = json.load(f)
|
| 47 |
+
except (OSError, json.JSONDecodeError):
|
| 48 |
+
return None
|
| 49 |
+
exterior_raw = data.get("exterior", [])
|
| 50 |
+
holes_raw = data.get("holes", [])
|
| 51 |
+
if len(exterior_raw) < 3:
|
| 52 |
+
return None
|
| 53 |
+
exterior = [(float(p[0]) * SCALE_X, float(p[1]) * SCALE_Y) for p in exterior_raw]
|
| 54 |
+
holes = [
|
| 55 |
+
[(float(p[0]) * SCALE_X, float(p[1]) * SCALE_Y) for p in h]
|
| 56 |
+
for h in holes_raw if len(h) >= 3
|
| 57 |
+
]
|
| 58 |
+
_compiled_cache = (exterior, holes)
|
| 59 |
+
return _compiled_cache
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
def _load_nav_graph() -> Optional[tuple[list[tuple[float, float]], dict[int, list[tuple[int, float]]]]]:
|
| 63 |
+
"""Load nav_points from compiled_map.json (game coords) and build adjacency graph. Returns (points, adj) or None."""
|
| 64 |
+
global _nav_graph_cache
|
| 65 |
+
if _nav_graph_cache is not None:
|
| 66 |
+
return _nav_graph_cache
|
| 67 |
+
path = Path(__file__).resolve().parent.parent / "static" / "compiled_map.json"
|
| 68 |
+
if not path.exists():
|
| 69 |
+
return None
|
| 70 |
+
try:
|
| 71 |
+
with open(path, encoding="utf-8") as f:
|
| 72 |
+
data = json.load(f)
|
| 73 |
+
except (OSError, json.JSONDecodeError):
|
| 74 |
+
return None
|
| 75 |
+
raw = data.get("nav_points", [])
|
| 76 |
+
if len(raw) < 2:
|
| 77 |
+
return None
|
| 78 |
+
points: list[tuple[float, float]] = [(float(p[0]), float(p[1])) for p in raw]
|
| 79 |
+
adj: dict[int, list[tuple[int, float]]] = {i: [] for i in range(len(points))}
|
| 80 |
+
for i in range(len(points)):
|
| 81 |
+
xi, yi = points[i]
|
| 82 |
+
for j in range(i + 1, len(points)):
|
| 83 |
+
xj, yj = points[j]
|
| 84 |
+
d = ((xj - xi) ** 2 + (yj - yi) ** 2) ** 0.5
|
| 85 |
+
if d <= _NAV_CONNECT_RADIUS and d > 0:
|
| 86 |
+
adj[i].append((j, d))
|
| 87 |
+
adj[j].append((i, d))
|
| 88 |
+
_nav_graph_cache = (points, adj)
|
| 89 |
+
return _nav_graph_cache
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def _nav_point_blocked(
|
| 93 |
+
x: float,
|
| 94 |
+
y: float,
|
| 95 |
+
blocked_rects: list[tuple[float, float, float, float]],
|
| 96 |
+
) -> bool:
|
| 97 |
+
"""True if nav point (x, y) is inside or too close to any building rect."""
|
| 98 |
+
for rx, ry, w, h in blocked_rects:
|
| 99 |
+
cx = max(rx, min(rx + w, x))
|
| 100 |
+
cy = max(ry, min(ry + h, y))
|
| 101 |
+
if math.hypot(x - cx, y - cy) < _NAV_BUILDING_MARGIN:
|
| 102 |
+
return True
|
| 103 |
+
return False
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
def _nav_valid_set(
|
| 107 |
+
points: list[tuple[float, float]],
|
| 108 |
+
blocked_rects: Optional[list[tuple[float, float, float, float]]],
|
| 109 |
+
) -> Optional[set[int]]:
|
| 110 |
+
"""Return set of nav point indices not blocked by buildings, or None if no filtering needed."""
|
| 111 |
+
if not blocked_rects:
|
| 112 |
+
return None # all valid
|
| 113 |
+
return {
|
| 114 |
+
i for i, (px, py) in enumerate(points)
|
| 115 |
+
if not _nav_point_blocked(px, py, blocked_rects)
|
| 116 |
+
}
|
| 117 |
+
|
| 118 |
+
|
| 119 |
+
def _nearest_nav_index(
|
| 120 |
+
x: float,
|
| 121 |
+
y: float,
|
| 122 |
+
points: list[tuple[float, float]],
|
| 123 |
+
valid: Optional[set[int]] = None,
|
| 124 |
+
) -> int:
|
| 125 |
+
"""Return index of nav point closest to (x, y), restricted to valid set if given."""
|
| 126 |
+
best_i = -1
|
| 127 |
+
best_d = float("inf")
|
| 128 |
+
for i in range(len(points)):
|
| 129 |
+
if valid is not None and i not in valid:
|
| 130 |
+
continue
|
| 131 |
+
px, py = points[i]
|
| 132 |
+
d = (px - x) ** 2 + (py - y) ** 2
|
| 133 |
+
if d < best_d:
|
| 134 |
+
best_d = d
|
| 135 |
+
best_i = i
|
| 136 |
+
if best_i == -1:
|
| 137 |
+
# fallback: ignore valid constraint
|
| 138 |
+
best_i = 0
|
| 139 |
+
best_d = (points[0][0] - x) ** 2 + (points[0][1] - y) ** 2
|
| 140 |
+
for i in range(1, len(points)):
|
| 141 |
+
px, py = points[i]
|
| 142 |
+
d = (px - x) ** 2 + (py - y) ** 2
|
| 143 |
+
if d < best_d:
|
| 144 |
+
best_d = d
|
| 145 |
+
best_i = i
|
| 146 |
+
return best_i
|
| 147 |
+
|
| 148 |
|
| 149 |
def _load_polygons() -> list[list[tuple[float, float]]]:
|
| 150 |
global _polygons_cache
|
|
|
|
| 185 |
return inside
|
| 186 |
|
| 187 |
|
| 188 |
+
def _point_in_rect(x: float, y: float, rx: float, ry: float, w: float, h: float) -> bool:
|
| 189 |
+
return rx <= x < rx + w and ry <= y < ry + h
|
| 190 |
+
|
| 191 |
+
|
| 192 |
+
def _static_walkable(x: float, y: float) -> bool:
|
| 193 |
+
"""True if (x,y) is in any walkable polygon (all polygons from walkable.json)."""
|
| 194 |
if x < 0 or x > MAP_WIDTH or y < 0 or y > MAP_HEIGHT:
|
| 195 |
return False
|
| 196 |
polygons = _load_polygons()
|
|
|
|
| 202 |
return False
|
| 203 |
|
| 204 |
|
| 205 |
+
def is_walkable(
|
| 206 |
+
x: float,
|
| 207 |
+
y: float,
|
| 208 |
+
*,
|
| 209 |
+
blocked_rects: Optional[list[tuple[float, float, float, float]]] = None,
|
| 210 |
+
) -> bool:
|
| 211 |
+
"""
|
| 212 |
+
True if (x, y) is walkable (inside exterior, not in holes, not in any blocked rect).
|
| 213 |
+
blocked_rects: optional list of (x, y, width, height) in game coordinates (e.g. building footprints).
|
| 214 |
+
"""
|
| 215 |
+
if not _static_walkable(x, y):
|
| 216 |
+
return False
|
| 217 |
+
if blocked_rects:
|
| 218 |
+
for rx, ry, w, h in blocked_rects:
|
| 219 |
+
if _point_in_rect(x, y, rx, ry, w, h):
|
| 220 |
+
return False
|
| 221 |
+
return True
|
| 222 |
+
|
| 223 |
+
|
| 224 |
def _cell_center(ci: int, cj: int) -> tuple[float, float]:
|
| 225 |
return (ci * _GRID_STEP, cj * _GRID_STEP)
|
| 226 |
|
|
|
|
| 233 |
return (ci, cj)
|
| 234 |
|
| 235 |
|
| 236 |
+
def _cell_walkable(
|
| 237 |
+
ci: int,
|
| 238 |
+
cj: int,
|
| 239 |
+
blocked_rects: Optional[list[tuple[float, float, float, float]]] = None,
|
| 240 |
+
) -> bool:
|
| 241 |
cx, cy = _cell_center(ci, cj)
|
| 242 |
+
return is_walkable(cx, cy, blocked_rects=blocked_rects)
|
| 243 |
|
| 244 |
|
| 245 |
+
def _neighbors(
|
| 246 |
+
ci: int,
|
| 247 |
+
cj: int,
|
| 248 |
+
blocked_rects: Optional[list[tuple[float, float, float, float]]] = None,
|
| 249 |
+
) -> list[tuple[int, int, float]]:
|
| 250 |
out: list[tuple[int, int, float]] = []
|
| 251 |
for di in (-1, 0, 1):
|
| 252 |
for dj in (-1, 0, 1):
|
| 253 |
if di == 0 and dj == 0:
|
| 254 |
continue
|
| 255 |
ni, nj = ci + di, cj + dj
|
| 256 |
+
if 0 <= ni < _GRID_W and 0 <= nj < _GRID_H and _cell_walkable(ni, nj, blocked_rects):
|
| 257 |
cost = 1.414 if di != 0 and dj != 0 else 1.0
|
| 258 |
out.append((ni, nj, cost))
|
| 259 |
return out
|
| 260 |
|
| 261 |
|
| 262 |
+
def _find_path_navgraph(
|
| 263 |
+
sx: float, sy: float, tx: float, ty: float,
|
| 264 |
+
points: list[tuple[float, float]],
|
| 265 |
+
adj: dict[int, list[tuple[int, float]]],
|
| 266 |
+
blocked_rects: Optional[list[tuple[float, float, float, float]]] = None,
|
| 267 |
+
) -> Optional[list[tuple[float, float]]]:
|
| 268 |
+
"""A* on nav graph, skipping points inside buildings. Returns waypoints or None."""
|
| 269 |
+
if not points or not adj:
|
| 270 |
+
return None
|
| 271 |
+
|
| 272 |
+
valid = _nav_valid_set(points, blocked_rects)
|
| 273 |
+
start_i = _nearest_nav_index(sx, sy, points, valid)
|
| 274 |
+
end_i = _nearest_nav_index(tx, ty, points, valid)
|
| 275 |
+
if start_i == end_i:
|
| 276 |
+
return [(tx, ty)]
|
| 277 |
+
|
| 278 |
+
def heuristic(i: int) -> float:
|
| 279 |
+
px, py = points[i]
|
| 280 |
+
return ((tx - px) ** 2 + (ty - py) ** 2) ** 0.5
|
| 281 |
+
|
| 282 |
+
counter = 0
|
| 283 |
+
best_g: dict[int, float] = {start_i: 0.0}
|
| 284 |
+
came_from: dict[int, Optional[int]] = {start_i: None}
|
| 285 |
+
open_set: list[tuple[float, int, int]] = []
|
| 286 |
+
heapq.heappush(open_set, (heuristic(start_i), counter, start_i))
|
| 287 |
+
counter += 1
|
| 288 |
+
|
| 289 |
+
while open_set:
|
| 290 |
+
f, _, node_i = heapq.heappop(open_set)
|
| 291 |
+
g = best_g.get(node_i, float("inf"))
|
| 292 |
+
if f > g + heuristic(node_i) + 1e-9:
|
| 293 |
+
continue # stale entry
|
| 294 |
+
if node_i == end_i:
|
| 295 |
+
# Reconstruct
|
| 296 |
+
path_indices: list[int] = []
|
| 297 |
+
cur: Optional[int] = node_i
|
| 298 |
+
while cur is not None:
|
| 299 |
+
path_indices.append(cur)
|
| 300 |
+
cur = came_from[cur]
|
| 301 |
+
path_indices.reverse()
|
| 302 |
+
result = [points[i] for i in path_indices]
|
| 303 |
+
if result:
|
| 304 |
+
result[-1] = (tx, ty)
|
| 305 |
+
else:
|
| 306 |
+
result = [(tx, ty)]
|
| 307 |
+
return result
|
| 308 |
+
for j, cost in adj.get(node_i, []):
|
| 309 |
+
if valid is not None and j not in valid:
|
| 310 |
+
continue
|
| 311 |
+
new_g = g + cost
|
| 312 |
+
if new_g < best_g.get(j, float("inf")):
|
| 313 |
+
best_g[j] = new_g
|
| 314 |
+
came_from[j] = node_i
|
| 315 |
+
heapq.heappush(open_set, (new_g + heuristic(j), counter, j))
|
| 316 |
+
counter += 1
|
| 317 |
+
return None
|
| 318 |
+
|
| 319 |
+
|
| 320 |
def find_path(
|
| 321 |
+
sx: float,
|
| 322 |
+
sy: float,
|
| 323 |
+
tx: float,
|
| 324 |
+
ty: float,
|
| 325 |
+
*,
|
| 326 |
+
blocked_rects: Optional[list[tuple[float, float, float, float]]] = None,
|
| 327 |
) -> Optional[list[tuple[float, float]]]:
|
| 328 |
"""
|
| 329 |
A* path from (sx,sy) to (tx,ty) in game coordinates.
|
| 330 |
+
Uses nav-point graph when available and no dynamic obstacles; else grid A*.
|
| 331 |
+
blocked_rects: optional list of (x, y, width, height) to avoid (e.g. buildings).
|
| 332 |
Returns list of waypoints (including end), or None if no path.
|
|
|
|
| 333 |
"""
|
| 334 |
+
nav = _load_nav_graph()
|
| 335 |
+
if nav is not None:
|
| 336 |
+
points, adj = nav
|
| 337 |
+
path = _find_path_navgraph(sx, sy, tx, ty, points, adj, blocked_rects=blocked_rects)
|
| 338 |
+
if path is not None:
|
| 339 |
+
return path
|
| 340 |
+
|
| 341 |
+
return _find_path_grid(sx, sy, tx, ty, blocked_rects=blocked_rects)
|
| 342 |
+
|
| 343 |
+
|
| 344 |
+
def _find_path_grid(
|
| 345 |
+
sx: float,
|
| 346 |
+
sy: float,
|
| 347 |
+
tx: float,
|
| 348 |
+
ty: float,
|
| 349 |
+
*,
|
| 350 |
+
blocked_rects: Optional[list[tuple[float, float, float, float]]] = None,
|
| 351 |
+
) -> Optional[list[tuple[float, float]]]:
|
| 352 |
+
"""Grid A* path (used when nav graph unavailable or blocked_rects present)."""
|
| 353 |
si, sj = _to_cell(sx, sy)
|
| 354 |
ti, tj = _to_cell(tx, ty)
|
| 355 |
+
if not _cell_walkable(si, sj, blocked_rects) or not _cell_walkable(ti, tj, blocked_rects):
|
| 356 |
return None
|
| 357 |
if si == ti and sj == tj:
|
| 358 |
return [(tx, ty)]
|
|
|
|
| 360 |
def heuristic(i: int, j: int) -> float:
|
| 361 |
return ((ti - i) ** 2 + (tj - j) ** 2) ** 0.5
|
| 362 |
|
|
|
|
| 363 |
counter = 0
|
| 364 |
+
# Heap entries: (f, counter, ci, cj, g, parent_cell_or_None)
|
| 365 |
+
open_set: list[tuple[float, int, int, int, float, Optional[tuple[int, int]]]] = []
|
| 366 |
+
heapq.heappush(open_set, (heuristic(si, sj), counter, si, sj, 0.0, None))
|
|
|
|
|
|
|
| 367 |
counter += 1
|
| 368 |
+
# best g seen per cell; also acts as closed set when set to a finite value
|
| 369 |
+
best_g: dict[tuple[int, int], float] = {(si, sj): 0.0}
|
| 370 |
+
came_from: dict[tuple[int, int], Optional[tuple[int, int]]] = {(si, sj): None}
|
| 371 |
|
| 372 |
while open_set:
|
| 373 |
+
f, _, ci, cj, g, _ = heapq.heappop(open_set)
|
| 374 |
+
if g > best_g.get((ci, cj), float("inf")):
|
| 375 |
+
continue # stale entry
|
|
|
|
| 376 |
if ci == ti and cj == tj:
|
| 377 |
+
# Reconstruct path
|
| 378 |
+
cells: list[tuple[int, int]] = []
|
| 379 |
+
cur: Optional[tuple[int, int]] = (ci, cj)
|
| 380 |
+
while cur is not None:
|
| 381 |
+
cells.append(cur)
|
| 382 |
+
cur = came_from[cur]
|
| 383 |
+
cells.reverse()
|
| 384 |
+
waypoints = [_cell_center(i, j) for i, j in cells[1:]]
|
| 385 |
if waypoints:
|
| 386 |
waypoints[-1] = (tx, ty)
|
| 387 |
else:
|
| 388 |
waypoints = [(tx, ty)]
|
| 389 |
return waypoints
|
| 390 |
+
for ni, nj, cost in _neighbors(ci, cj, blocked_rects):
|
| 391 |
+
new_g = g + cost
|
| 392 |
+
if new_g < best_g.get((ni, nj), float("inf")):
|
| 393 |
+
best_g[(ni, nj)] = new_g
|
| 394 |
+
came_from[(ni, nj)] = (ci, cj)
|
| 395 |
+
f_new = new_g + heuristic(ni, nj)
|
| 396 |
+
heapq.heappush(open_set, (f_new, counter, ni, nj, new_g, (ci, cj)))
|
| 397 |
+
counter += 1
|
|
|
|
|
|
|
|
|
|
| 398 |
return None
|
| 399 |
|
| 400 |
|
| 401 |
+
def nearest_walkable_navpoint(
|
| 402 |
+
x: float,
|
| 403 |
+
y: float,
|
| 404 |
+
*,
|
| 405 |
+
blocked_rects: Optional[list[tuple[float, float, float, float]]] = None,
|
| 406 |
+
) -> tuple[float, float]:
|
| 407 |
+
"""Return the nearest nav point that is walkable (not inside any building).
|
| 408 |
+
Falls back to snap_to_walkable if no nav graph available."""
|
| 409 |
+
nav = _load_nav_graph()
|
| 410 |
+
if nav is not None:
|
| 411 |
+
points, _ = nav
|
| 412 |
+
valid = _nav_valid_set(points, blocked_rects)
|
| 413 |
+
best_i = _nearest_nav_index(x, y, points, valid)
|
| 414 |
+
if best_i >= 0:
|
| 415 |
+
return points[best_i]
|
| 416 |
+
return snap_to_walkable(x, y, blocked_rects=blocked_rects)
|
| 417 |
+
|
| 418 |
+
|
| 419 |
+
def snap_to_walkable(
|
| 420 |
+
x: float,
|
| 421 |
+
y: float,
|
| 422 |
+
*,
|
| 423 |
+
blocked_rects: Optional[list[tuple[float, float, float, float]]] = None,
|
| 424 |
+
) -> tuple[float, float]:
|
| 425 |
+
"""Return nearest walkable point to (x,y)."""
|
| 426 |
+
if is_walkable(x, y, blocked_rects=blocked_rects):
|
| 427 |
return (x, y)
|
| 428 |
best = (x, y)
|
| 429 |
best_d = 1e9
|
| 430 |
+
for radius in [0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 5.0]:
|
| 431 |
+
for dx in [-radius, -radius * 0.5, 0, radius * 0.5, radius]:
|
| 432 |
+
for dy in [-radius, -radius * 0.5, 0, radius * 0.5, radius]:
|
| 433 |
+
if dx == 0 and dy == 0:
|
| 434 |
continue
|
| 435 |
nx = max(0, min(MAP_WIDTH, x + dx))
|
| 436 |
ny = max(0, min(MAP_HEIGHT, y + dy))
|
| 437 |
+
if is_walkable(nx, ny, blocked_rects=blocked_rects):
|
| 438 |
d = (nx - x) ** 2 + (ny - y) ** 2
|
| 439 |
if d < best_d:
|
| 440 |
best_d = d
|
| 441 |
best = (nx, ny)
|
| 442 |
+
if best_d < 1e9:
|
| 443 |
+
break
|
| 444 |
return best
|
backend/game/state.py
CHANGED
|
@@ -6,7 +6,8 @@ from typing import Optional
|
|
| 6 |
from pydantic import BaseModel, Field
|
| 7 |
|
| 8 |
from .buildings import Building, BuildingStatus, BuildingType, BUILDING_DEFS
|
| 9 |
-
from .map import GameMap,
|
|
|
|
| 10 |
from .units import Unit, UnitType, UNIT_DEFS, UnitStatus
|
| 11 |
|
| 12 |
|
|
@@ -68,7 +69,7 @@ class PlayerState(BaseModel):
|
|
| 68 |
None,
|
| 69 |
)
|
| 70 |
|
| 71 |
-
def summary(self) -> str:
|
| 72 |
active = [
|
| 73 |
b.building_type.value for b in self.buildings.values()
|
| 74 |
if b.status not in (BuildingStatus.CONSTRUCTING, BuildingStatus.DESTROYED)
|
|
@@ -82,14 +83,21 @@ class PlayerState(BaseModel):
|
|
| 82 |
for u in self.units.values():
|
| 83 |
counts[u.unit_type.value] = counts.get(u.unit_type.value, 0) + 1
|
| 84 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
lines = [
|
| 86 |
-
f"
|
| 87 |
-
f"
|
| 88 |
]
|
| 89 |
if constructing:
|
| 90 |
-
lines.append(f"
|
| 91 |
if counts:
|
| 92 |
-
lines.append(f"
|
| 93 |
return "\n".join(lines)
|
| 94 |
|
| 95 |
|
|
@@ -115,29 +123,38 @@ class GameState(BaseModel):
|
|
| 115 |
p1 = PlayerState(player_id=player1_id, player_name=player1_name)
|
| 116 |
p2 = PlayerState(player_id=player2_id, player_name=player2_name)
|
| 117 |
|
| 118 |
-
start1, start2 =
|
|
|
|
| 119 |
|
| 120 |
-
# Starting Command Centers (already built)
|
| 121 |
-
cc1 = Building.create(BuildingType.COMMAND_CENTER, player1_id,
|
| 122 |
cc1.status = BuildingStatus.ACTIVE
|
| 123 |
cc1.construction_ticks_remaining = 0
|
|
|
|
| 124 |
p1.buildings[cc1.id] = cc1
|
| 125 |
|
| 126 |
-
cc2 = Building.create(BuildingType.COMMAND_CENTER, player2_id,
|
| 127 |
cc2.status = BuildingStatus.ACTIVE
|
| 128 |
cc2.construction_ticks_remaining = 0
|
|
|
|
| 129 |
p2.buildings[cc2.id] = cc2
|
| 130 |
|
| 131 |
-
# 5 starting SCVs per player,
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 136 |
p1.units[scv1.id] = scv1
|
| 137 |
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
|
|
|
|
| 141 |
p2.units[scv2.id] = scv2
|
| 142 |
|
| 143 |
p1.recalculate_supply()
|
|
|
|
| 6 |
from pydantic import BaseModel, Field
|
| 7 |
|
| 8 |
from .buildings import Building, BuildingStatus, BuildingType, BUILDING_DEFS
|
| 9 |
+
from .map import GameMap, get_start_data, get_all_map_resources
|
| 10 |
+
from .pathfinding import snap_to_walkable
|
| 11 |
from .units import Unit, UnitType, UNIT_DEFS, UnitStatus
|
| 12 |
|
| 13 |
|
|
|
|
| 69 |
None,
|
| 70 |
)
|
| 71 |
|
| 72 |
+
def summary(self, lang: str = "fr") -> str:
|
| 73 |
active = [
|
| 74 |
b.building_type.value for b in self.buildings.values()
|
| 75 |
if b.status not in (BuildingStatus.CONSTRUCTING, BuildingStatus.DESTROYED)
|
|
|
|
| 83 |
for u in self.units.values():
|
| 84 |
counts[u.unit_type.value] = counts.get(u.unit_type.value, 0) + 1
|
| 85 |
|
| 86 |
+
if (lang or "fr").lower() == "en":
|
| 87 |
+
minerals_l, gas_l, supply_l = "Minerals", "Gas", "Supply"
|
| 88 |
+
buildings_l, constructing_l, units_l, none_l = "Active buildings", "Under construction", "Units", "none"
|
| 89 |
+
else:
|
| 90 |
+
minerals_l, gas_l, supply_l = "Minéraux", "Gaz", "Supply"
|
| 91 |
+
buildings_l, constructing_l, units_l, none_l = "Bâtiments actifs", "En construction", "Unités", "aucun"
|
| 92 |
+
|
| 93 |
lines = [
|
| 94 |
+
f"{minerals_l}: {self.minerals}, {gas_l}: {self.gas}, {supply_l}: {self.supply_used}/{self.supply_max}",
|
| 95 |
+
f"{buildings_l}: {', '.join(active) or none_l}",
|
| 96 |
]
|
| 97 |
if constructing:
|
| 98 |
+
lines.append(f"{constructing_l}: {', '.join(constructing)}")
|
| 99 |
if counts:
|
| 100 |
+
lines.append(f"{units_l}: {', '.join(f'{v} {k}' for k, v in counts.items())}")
|
| 101 |
return "\n".join(lines)
|
| 102 |
|
| 103 |
|
|
|
|
| 123 |
p1 = PlayerState(player_id=player1_id, player_name=player1_name)
|
| 124 |
p2 = PlayerState(player_id=player2_id, player_name=player2_name)
|
| 125 |
|
| 126 |
+
start1, _, start2, _ = get_start_data()
|
| 127 |
+
state.game_map = GameMap.create_default(resources=get_all_map_resources())
|
| 128 |
|
| 129 |
+
# Starting Command Centers (already built); x,y is center of building footprint
|
| 130 |
+
cc1 = Building.create(BuildingType.COMMAND_CENTER, player1_id, float(start1[0]), float(start1[1]))
|
| 131 |
cc1.status = BuildingStatus.ACTIVE
|
| 132 |
cc1.construction_ticks_remaining = 0
|
| 133 |
+
cc1.hp = float(cc1.max_hp)
|
| 134 |
p1.buildings[cc1.id] = cc1
|
| 135 |
|
| 136 |
+
cc2 = Building.create(BuildingType.COMMAND_CENTER, player2_id, float(start2[0]), float(start2[1]))
|
| 137 |
cc2.status = BuildingStatus.ACTIVE
|
| 138 |
cc2.construction_ticks_remaining = 0
|
| 139 |
+
cc2.hp = float(cc2.max_hp)
|
| 140 |
p2.buildings[cc2.id] = cc2
|
| 141 |
|
| 142 |
+
# 5 starting SCVs per player, spread in x around CC; snap each to nearest walkable point
|
| 143 |
+
_MAP_W = 80.0
|
| 144 |
+
_MAP_H = 80.0
|
| 145 |
+
# Spacing must be > 2*UNIT_RADIUS (1.0) to avoid collision-blocking at spawn
|
| 146 |
+
_scv_offsets = [(-2.4, 3), (-1.2, 3), (0.0, 3), (1.2, 3), (2.4, 3)]
|
| 147 |
+
for i, (ox, oy) in enumerate(_scv_offsets):
|
| 148 |
+
raw1x = max(0.5, min(_MAP_W - 0.5, start1[0] + ox))
|
| 149 |
+
raw1y = max(0.5, min(_MAP_H - 0.5, start1[1] + oy))
|
| 150 |
+
sx1, sy1 = snap_to_walkable(raw1x, raw1y)
|
| 151 |
+
scv1 = Unit.create(UnitType.SCV, player1_id, sx1, sy1)
|
| 152 |
p1.units[scv1.id] = scv1
|
| 153 |
|
| 154 |
+
raw2x = max(0.5, min(_MAP_W - 0.5, start2[0] + ox))
|
| 155 |
+
raw2y = max(0.5, min(_MAP_H - 0.5, start2[1] - oy))
|
| 156 |
+
sx2, sy2 = snap_to_walkable(raw2x, raw2y)
|
| 157 |
+
scv2 = Unit.create(UnitType.SCV, player2_id, sx2, sy2)
|
| 158 |
p2.units[scv2.id] = scv2
|
| 159 |
|
| 160 |
p1.recalculate_supply()
|
backend/game/tech_tree.py
CHANGED
|
@@ -24,7 +24,7 @@ BUILDING_REQUIREMENTS: dict[BuildingType, list[BuildingType]] = {
|
|
| 24 |
BuildingType.COMMAND_CENTER: [],
|
| 25 |
BuildingType.SUPPLY_DEPOT: [],
|
| 26 |
BuildingType.REFINERY: [],
|
| 27 |
-
BuildingType.BARRACKS: [],
|
| 28 |
BuildingType.ENGINEERING_BAY: [BuildingType.BARRACKS],
|
| 29 |
BuildingType.FACTORY: [BuildingType.BARRACKS],
|
| 30 |
BuildingType.ARMORY: [BuildingType.FACTORY],
|
|
|
|
| 24 |
BuildingType.COMMAND_CENTER: [],
|
| 25 |
BuildingType.SUPPLY_DEPOT: [],
|
| 26 |
BuildingType.REFINERY: [],
|
| 27 |
+
BuildingType.BARRACKS: [BuildingType.SUPPLY_DEPOT],
|
| 28 |
BuildingType.ENGINEERING_BAY: [BuildingType.BARRACKS],
|
| 29 |
BuildingType.FACTORY: [BuildingType.BARRACKS],
|
| 30 |
BuildingType.ARMORY: [BuildingType.FACTORY],
|
backend/game/units.py
CHANGED
|
@@ -22,6 +22,7 @@ class UnitStatus(str, Enum):
|
|
| 22 |
ATTACKING = "attacking"
|
| 23 |
MINING_MINERALS = "mining_minerals"
|
| 24 |
MINING_GAS = "mining_gas"
|
|
|
|
| 25 |
BUILDING = "building"
|
| 26 |
HEALING = "healing"
|
| 27 |
SIEGED = "sieged"
|
|
@@ -74,17 +75,15 @@ UNIT_DEFS: dict[UnitType, UnitDef] = {
|
|
| 74 |
supply_cost=2, build_time_ticks=40, attack_cooldown_ticks=4,
|
| 75 |
),
|
| 76 |
UnitType.TANK: UnitDef(
|
| 77 |
-
max_hp=150, armor=1, ground_damage=
|
| 78 |
attack_range=7, move_speed=0.75, mineral_cost=150, gas_cost=100,
|
| 79 |
-
supply_cost=
|
| 80 |
attack_cooldown_ticks=5,
|
| 81 |
-
siege_damage=35, siege_range=12.0, siege_splash_radius=2.0,
|
| 82 |
-
siege_cooldown_ticks=8,
|
| 83 |
),
|
| 84 |
UnitType.WRAITH: UnitDef(
|
| 85 |
max_hp=120, armor=0, ground_damage=8, air_damage=20,
|
| 86 |
attack_range=5, move_speed=2.5, mineral_cost=150, gas_cost=100,
|
| 87 |
-
supply_cost=2, build_time_ticks=60, is_flying=True,
|
| 88 |
attack_cooldown_ticks=4,
|
| 89 |
),
|
| 90 |
}
|
|
@@ -118,6 +117,7 @@ class Unit(BaseModel):
|
|
| 118 |
building_target_id: Optional[str] = None
|
| 119 |
harvest_carry: bool = False # True while carrying resources back to CC
|
| 120 |
harvest_amount: int = 0 # amount being carried (deposited on CC arrival)
|
|
|
|
| 121 |
|
| 122 |
@classmethod
|
| 123 |
def create(cls, unit_type: UnitType, owner: str, x: float, y: float) -> "Unit":
|
|
|
|
| 22 |
ATTACKING = "attacking"
|
| 23 |
MINING_MINERALS = "mining_minerals"
|
| 24 |
MINING_GAS = "mining_gas"
|
| 25 |
+
MOVING_TO_BUILD = "moving_to_build"
|
| 26 |
BUILDING = "building"
|
| 27 |
HEALING = "healing"
|
| 28 |
SIEGED = "sieged"
|
|
|
|
| 75 |
supply_cost=2, build_time_ticks=40, attack_cooldown_ticks=4,
|
| 76 |
),
|
| 77 |
UnitType.TANK: UnitDef(
|
| 78 |
+
max_hp=150, armor=1, ground_damage=35, air_damage=0,
|
| 79 |
attack_range=7, move_speed=0.75, mineral_cost=150, gas_cost=100,
|
| 80 |
+
supply_cost=3, build_time_ticks=50,
|
| 81 |
attack_cooldown_ticks=5,
|
|
|
|
|
|
|
| 82 |
),
|
| 83 |
UnitType.WRAITH: UnitDef(
|
| 84 |
max_hp=120, armor=0, ground_damage=8, air_damage=20,
|
| 85 |
attack_range=5, move_speed=2.5, mineral_cost=150, gas_cost=100,
|
| 86 |
+
supply_cost=2, build_time_ticks=60, is_flying=True,
|
| 87 |
attack_cooldown_ticks=4,
|
| 88 |
),
|
| 89 |
}
|
|
|
|
| 117 |
building_target_id: Optional[str] = None
|
| 118 |
harvest_carry: bool = False # True while carrying resources back to CC
|
| 119 |
harvest_amount: int = 0 # amount being carried (deposited on CC arrival)
|
| 120 |
+
stuck_ticks: int = 0 # consecutive ticks the unit couldn't move
|
| 121 |
|
| 122 |
@classmethod
|
| 123 |
def create(cls, unit_type: UnitType, owner: str, x: float, y: float) -> "Unit":
|
backend/lobby/manager.py
CHANGED
|
@@ -199,6 +199,17 @@ class LobbyManager:
|
|
| 199 |
def get_room(self, room_id: str) -> Optional[Room]:
|
| 200 |
return self._rooms.get(room_id)
|
| 201 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 202 |
def get_room_for_sid(self, sid: str) -> Optional[Room]:
|
| 203 |
return self._get_room(sid)
|
| 204 |
|
|
|
|
| 199 |
def get_room(self, room_id: str) -> Optional[Room]:
|
| 200 |
return self._rooms.get(room_id)
|
| 201 |
|
| 202 |
+
def get_playing_count(self) -> int:
|
| 203 |
+
"""Return number of rooms currently in 'playing' status."""
|
| 204 |
+
return sum(1 for r in self._rooms.values() if r.status == "playing")
|
| 205 |
+
|
| 206 |
+
def get_a_playing_room_id(self) -> Optional[str]:
|
| 207 |
+
"""Return one room_id in 'playing' status for observe, or None."""
|
| 208 |
+
for room_id, room in self._rooms.items():
|
| 209 |
+
if room.status == "playing":
|
| 210 |
+
return room_id
|
| 211 |
+
return None
|
| 212 |
+
|
| 213 |
def get_room_for_sid(self, sid: str) -> Optional[Room]:
|
| 214 |
return self._get_room(sid)
|
| 215 |
|
backend/lobby/safe_name.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Validation du pseudo joueur via Mistral : détection d'insultes / noms inappropriés.
|
| 3 |
+
Si le nom est jugé insultant, il est remplacé par un nom aléatoire inoffensif.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
import json
|
| 9 |
+
import logging
|
| 10 |
+
import random
|
| 11 |
+
|
| 12 |
+
from config import MISTRAL_API_KEY, MISTRAL_CHAT_MODEL
|
| 13 |
+
|
| 14 |
+
log = logging.getLogger(__name__)
|
| 15 |
+
|
| 16 |
+
_FALLBACK_NAMES = [
|
| 17 |
+
"Player",
|
| 18 |
+
"Guest",
|
| 19 |
+
"Strategist",
|
| 20 |
+
"Commander",
|
| 21 |
+
"Pilot",
|
| 22 |
+
"Scout",
|
| 23 |
+
"Rookie",
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
_PROMPT = """Tu dois juger si un pseudo de joueur pour un jeu en ligne est insultant, vulgaire, discriminatoire ou inapproprié pour un public familial.
|
| 27 |
+
|
| 28 |
+
Réponds UNIQUEMENT avec un JSON valide, sans texte avant ni après, avec exactement ces deux champs :
|
| 29 |
+
- "is_insulting": true si le pseudo est inapproprié (insulte, gros mot, contenu choquant), false sinon.
|
| 30 |
+
- "replacement": si is_insulting est true, un pseudo de remplacement court, sympa et inoffensif (ex: "Chevalier", "Pilote"); sinon la chaîne vide "".
|
| 31 |
+
|
| 32 |
+
Pseudo à évaluer : "{name}"
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
|
| 36 |
+
async def sanitize_player_name(name: str) -> str:
|
| 37 |
+
"""
|
| 38 |
+
Vérifie le nom via Mistral. Si insultant, retourne un remplacement ; sinon retourne le nom tel quel.
|
| 39 |
+
En cas d'absence de clé API ou d'erreur, retourne le nom original.
|
| 40 |
+
"""
|
| 41 |
+
name = (name or "").strip() or "Player"
|
| 42 |
+
if not MISTRAL_API_KEY:
|
| 43 |
+
return name
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
from mistralai import Mistral
|
| 47 |
+
|
| 48 |
+
client = Mistral(api_key=MISTRAL_API_KEY)
|
| 49 |
+
response = await client.chat.complete_async(
|
| 50 |
+
model=MISTRAL_CHAT_MODEL,
|
| 51 |
+
messages=[{"role": "user", "content": _PROMPT.format(name=name)}],
|
| 52 |
+
response_format={"type": "json_object"},
|
| 53 |
+
)
|
| 54 |
+
content = response.choices[0].message.content
|
| 55 |
+
if isinstance(content, str):
|
| 56 |
+
raw = content.strip()
|
| 57 |
+
elif content:
|
| 58 |
+
first = content[0]
|
| 59 |
+
raw = (getattr(first, "text", None) or str(first)).strip()
|
| 60 |
+
else:
|
| 61 |
+
raw = ""
|
| 62 |
+
# Extraire un éventuel bloc JSON
|
| 63 |
+
if "```" in raw:
|
| 64 |
+
raw = raw.split("```")[1]
|
| 65 |
+
if raw.startswith("json"):
|
| 66 |
+
raw = raw[4:]
|
| 67 |
+
data = json.loads(raw)
|
| 68 |
+
if data.get("is_insulting") and data.get("replacement"):
|
| 69 |
+
replacement = str(data["replacement"]).strip() or random.choice(_FALLBACK_NAMES)
|
| 70 |
+
log.info("Safe name: %r replaced by %r", name, replacement)
|
| 71 |
+
return replacement[:50]
|
| 72 |
+
return name[:50]
|
| 73 |
+
except Exception as e:
|
| 74 |
+
log.warning("Safe name check failed for %r: %s", name, e)
|
| 75 |
+
return name[:50]
|
backend/main.py
CHANGED
|
@@ -11,6 +11,8 @@ Client → Server
|
|
| 11 |
join_room { room_id, name }
|
| 12 |
quick_match { name }
|
| 13 |
player_ready {}
|
|
|
|
|
|
|
| 14 |
voice_input { audio_b64, mime_type? }
|
| 15 |
text_input { text }
|
| 16 |
disconnect (automatic)
|
|
@@ -25,6 +27,8 @@ Server → Client
|
|
| 25 |
voice_result { transcription, feedback_text, feedback_audio_b64, results }
|
| 26 |
game_over { winner_id, winner_name }
|
| 27 |
error { message }
|
|
|
|
|
|
|
| 28 |
"""
|
| 29 |
|
| 30 |
from __future__ import annotations
|
|
@@ -33,13 +37,14 @@ import asyncio
|
|
| 33 |
import base64
|
| 34 |
import logging
|
| 35 |
import os
|
|
|
|
| 36 |
import subprocess
|
| 37 |
import sys
|
| 38 |
from pathlib import Path
|
| 39 |
-
from typing import Optional
|
| 40 |
|
| 41 |
import socketio
|
| 42 |
-
from fastapi import Body, FastAPI, HTTPException
|
| 43 |
from fastapi.middleware.cors import CORSMiddleware
|
| 44 |
from fastapi.staticfiles import StaticFiles
|
| 45 |
from fastapi.responses import FileResponse
|
|
@@ -48,6 +53,7 @@ from game.bot import BOT_PLAYER_ID, BotPlayer
|
|
| 48 |
from game.engine import GameEngine
|
| 49 |
from game.state import GameState
|
| 50 |
from lobby.manager import LobbyManager
|
|
|
|
| 51 |
from voice import command_parser, stt
|
| 52 |
|
| 53 |
BOT_OFFER_DELAY = 10 # seconds before offering bot opponent
|
|
@@ -55,6 +61,35 @@ BOT_OFFER_DELAY = 10 # seconds before offering bot opponent
|
|
| 55 |
logging.basicConfig(level=logging.INFO)
|
| 56 |
log = logging.getLogger(__name__)
|
| 57 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 58 |
# ---------------------------------------------------------------------------
|
| 59 |
# Socket.IO + FastAPI setup
|
| 60 |
# ---------------------------------------------------------------------------
|
|
@@ -76,6 +111,32 @@ fastapi_app.add_middleware(
|
|
| 76 |
allow_headers=["*"],
|
| 77 |
)
|
| 78 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
# Unit sound effects (generated by scripts/generate_unit_sounds.py)
|
| 80 |
_SOUNDS_DIR = Path(__file__).parent / "static" / "sounds"
|
| 81 |
_UNITS_SOUNDS_DIR = _SOUNDS_DIR / "units"
|
|
@@ -84,23 +145,32 @@ _UNITS_SOUNDS_DIR.mkdir(parents=True, exist_ok=True)
|
|
| 84 |
fastapi_app.mount("/sounds", StaticFiles(directory=str(_SOUNDS_DIR)), name="sounds")
|
| 85 |
|
| 86 |
# Sprites (unit/building icons generated via Mistral image API)
|
| 87 |
-
_SPRITES_DIR = Path(__file__).parent / "static" / "sprites"
|
| 88 |
_SPRITES_DIR.mkdir(parents=True, exist_ok=True)
|
| 89 |
if _SPRITES_DIR.exists():
|
| 90 |
fastapi_app.mount("/sprites", StaticFiles(directory=str(_SPRITES_DIR)), name="sprites")
|
| 91 |
|
| 92 |
-
# Map image, map.json, walkable polygon
|
| 93 |
-
_STATIC_DIR = Path(__file__).parent / "static"
|
| 94 |
if _STATIC_DIR.exists():
|
| 95 |
fastapi_app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
|
| 96 |
|
| 97 |
_MAP_JSON = _STATIC_DIR / "map.json"
|
| 98 |
_WALKABLE_JSON = _STATIC_DIR / "walkable.json"
|
|
|
|
| 99 |
_GAME_POSITIONS_JSON = _STATIC_DIR / "game_positions.json"
|
| 100 |
|
| 101 |
# Game grid size (must match game/map.py)
|
| 102 |
-
_MAP_GRID_W =
|
| 103 |
-
_MAP_GRID_H =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 104 |
|
| 105 |
|
| 106 |
def _load_game_positions() -> dict:
|
|
@@ -238,12 +308,12 @@ def _generate_geysers_around(cx: int, cy: int, count: int = 1, radius: float = 4
|
|
| 238 |
|
| 239 |
@fastapi_app.put("/api/map/positions")
|
| 240 |
async def save_map_positions(body: dict = Body(...)):
|
| 241 |
-
"""Enregistre positions de départ (
|
| 242 |
import json
|
| 243 |
starts = body.get("starting_positions")
|
| 244 |
expansions = body.get("expansion_positions")
|
| 245 |
-
if not isinstance(starts, list) or len(starts)
|
| 246 |
-
raise HTTPException(400, "starting_positions requis (
|
| 247 |
if not isinstance(expansions, list):
|
| 248 |
expansions = []
|
| 249 |
for i, p in enumerate(starts):
|
|
@@ -259,27 +329,37 @@ async def save_map_positions(body: dict = Body(...)):
|
|
| 259 |
if not (0 <= x <= 100 and 0 <= y <= 100):
|
| 260 |
raise HTTPException(400, f"expansion_positions[{i}] hors [0,100]")
|
| 261 |
|
| 262 |
-
# Keep positions in 0-100 for admin display
|
| 263 |
-
starting_positions = [
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
gx, gy =
|
| 270 |
-
|
| 271 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 272 |
|
| 273 |
payload = {
|
| 274 |
"starting_positions": starting_positions,
|
| 275 |
"expansion_positions": expansion_positions,
|
| 276 |
-
"minerals": minerals,
|
| 277 |
-
"geysers": geysers,
|
| 278 |
}
|
| 279 |
_STATIC_DIR.mkdir(parents=True, exist_ok=True)
|
| 280 |
with open(_GAME_POSITIONS_JSON, "w", encoding="utf-8") as f:
|
| 281 |
json.dump(payload, f, indent=2)
|
| 282 |
-
return {"status": "ok", "minerals_count":
|
| 283 |
|
| 284 |
|
| 285 |
@fastapi_app.get("/api/sounds/units")
|
|
@@ -340,7 +420,7 @@ async def generate_sprites():
|
|
| 340 |
|
| 341 |
def run():
|
| 342 |
return subprocess.run(
|
| 343 |
-
[sys.executable, "-m", "scripts.generate_sprites"],
|
| 344 |
cwd=str(Path(__file__).parent),
|
| 345 |
capture_output=True,
|
| 346 |
text=True,
|
|
@@ -398,6 +478,49 @@ async def generate_one_building_sprite(building_id: str):
|
|
| 398 |
return {"status": "ok", "id": building_id}
|
| 399 |
|
| 400 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 401 |
# Serve SvelteKit static build if present (production / HF Spaces)
|
| 402 |
_FRONTEND_BUILD = Path(__file__).parent.parent / "frontend" / "build"
|
| 403 |
if _FRONTEND_BUILD.exists():
|
|
@@ -435,6 +558,28 @@ async def _emit_error(sid: str, message: str) -> None:
|
|
| 435 |
await sio.emit("error", {"message": message}, to=sid)
|
| 436 |
|
| 437 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
async def _start_game(room_id: str) -> None:
|
| 439 |
"""Create game state + engine, emit game_start to human players."""
|
| 440 |
room = lobby.get_room(room_id)
|
|
@@ -540,9 +685,30 @@ async def disconnect(sid: str) -> None:
|
|
| 540 |
# ---------------------------------------------------------------------------
|
| 541 |
|
| 542 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
@sio.event
|
| 544 |
async def create_room(sid: str, data: dict) -> None:
|
| 545 |
name = str(data.get("name", "Player")).strip() or "Player"
|
|
|
|
| 546 |
room = lobby.create_room(sid, name)
|
| 547 |
await sio.enter_room(sid, room.room_id)
|
| 548 |
await sio.emit("room_created", {"room_id": room.room_id, "room": room.to_dict()}, to=sid)
|
|
@@ -556,6 +722,7 @@ async def create_room(sid: str, data: dict) -> None:
|
|
| 556 |
async def join_room(sid: str, data: dict) -> None:
|
| 557 |
room_id = str(data.get("room_id", "")).upper().strip()
|
| 558 |
name = str(data.get("name", "Player")).strip() or "Player"
|
|
|
|
| 559 |
|
| 560 |
room, err = lobby.join_room(sid, room_id, name)
|
| 561 |
if err:
|
|
@@ -576,6 +743,7 @@ async def join_room(sid: str, data: dict) -> None:
|
|
| 576 |
@sio.event
|
| 577 |
async def quick_match(sid: str, data: dict) -> None:
|
| 578 |
name = str(data.get("name", "Player")).strip() or "Player"
|
|
|
|
| 579 |
room, is_new = lobby.quick_match(sid, name)
|
| 580 |
|
| 581 |
if not is_new:
|
|
@@ -706,14 +874,15 @@ async def voice_input(sid: str, data: dict) -> None:
|
|
| 706 |
await sio.emit("voice_result", {
|
| 707 |
"transcription": "",
|
| 708 |
"feedback_text": "Je n'ai rien entendu. Appuie et parle!",
|
| 709 |
-
"
|
| 710 |
"results": [],
|
| 711 |
}, to=sid)
|
| 712 |
return
|
| 713 |
|
| 714 |
# 2. Parse command with Mistral
|
|
|
|
| 715 |
try:
|
| 716 |
-
parsed = await command_parser.parse(transcription, player)
|
| 717 |
except Exception as exc:
|
| 718 |
log.exception("Command parsing failed")
|
| 719 |
await _emit_error(sid, f"Erreur d'interprétation: {exc}")
|
|
@@ -722,21 +891,20 @@ async def voice_input(sid: str, data: dict) -> None:
|
|
| 722 |
# 3. Apply commands to game engine
|
| 723 |
cmd_result = engine.apply_command(sid, parsed)
|
| 724 |
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
feedback_text += "\n" + r.message
|
| 740 |
|
| 741 |
sound_events = []
|
| 742 |
for r in cmd_result.results:
|
|
@@ -744,6 +912,7 @@ async def voice_input(sid: str, data: dict) -> None:
|
|
| 744 |
await sio.emit("voice_result", {
|
| 745 |
"transcription": transcription,
|
| 746 |
"feedback_text": feedback_text,
|
|
|
|
| 747 |
"results": [r.model_dump() for r in cmd_result.results],
|
| 748 |
"sound_events": sound_events,
|
| 749 |
}, to=sid)
|
|
@@ -779,8 +948,9 @@ async def _process_text_command(sid: str, transcription: str) -> None:
|
|
| 779 |
return
|
| 780 |
|
| 781 |
# Parse command with Mistral
|
|
|
|
| 782 |
try:
|
| 783 |
-
parsed = await command_parser.parse(transcription, player)
|
| 784 |
except Exception as exc:
|
| 785 |
log.exception("Command parsing failed")
|
| 786 |
await _emit_error(sid, f"Erreur d'interprétation: {exc}")
|
|
@@ -789,19 +959,20 @@ async def _process_text_command(sid: str, transcription: str) -> None:
|
|
| 789 |
# Apply commands to game engine
|
| 790 |
cmd_result = engine.apply_command(sid, parsed)
|
| 791 |
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
|
|
|
| 805 |
|
| 806 |
sound_events = []
|
| 807 |
for r in cmd_result.results:
|
|
@@ -809,6 +980,7 @@ async def _process_text_command(sid: str, transcription: str) -> None:
|
|
| 809 |
await sio.emit("voice_result", {
|
| 810 |
"transcription": transcription,
|
| 811 |
"feedback_text": feedback_text,
|
|
|
|
| 812 |
"results": [r.model_dump() for r in cmd_result.results],
|
| 813 |
"sound_events": sound_events,
|
| 814 |
}, to=sid)
|
|
|
|
| 11 |
join_room { room_id, name }
|
| 12 |
quick_match { name }
|
| 13 |
player_ready {}
|
| 14 |
+
get_playing_count {} — server responds with playing_count
|
| 15 |
+
observe {} — server responds with observe_room or error
|
| 16 |
voice_input { audio_b64, mime_type? }
|
| 17 |
text_input { text }
|
| 18 |
disconnect (automatic)
|
|
|
|
| 27 |
voice_result { transcription, feedback_text, feedback_audio_b64, results }
|
| 28 |
game_over { winner_id, winner_name }
|
| 29 |
error { message }
|
| 30 |
+
playing_count { count } — in response to get_playing_count
|
| 31 |
+
observe_room { room_id } — in response to observe (spectator)
|
| 32 |
"""
|
| 33 |
|
| 34 |
from __future__ import annotations
|
|
|
|
| 37 |
import base64
|
| 38 |
import logging
|
| 39 |
import os
|
| 40 |
+
import re
|
| 41 |
import subprocess
|
| 42 |
import sys
|
| 43 |
from pathlib import Path
|
| 44 |
+
from typing import Any, Optional
|
| 45 |
|
| 46 |
import socketio
|
| 47 |
+
from fastapi import APIRouter, Body, FastAPI, HTTPException
|
| 48 |
from fastapi.middleware.cors import CORSMiddleware
|
| 49 |
from fastapi.staticfiles import StaticFiles
|
| 50 |
from fastapi.responses import FileResponse
|
|
|
|
| 53 |
from game.engine import GameEngine
|
| 54 |
from game.state import GameState
|
| 55 |
from lobby.manager import LobbyManager
|
| 56 |
+
from lobby.safe_name import sanitize_player_name
|
| 57 |
from voice import command_parser, stt
|
| 58 |
|
| 59 |
BOT_OFFER_DELAY = 10 # seconds before offering bot opponent
|
|
|
|
| 61 |
logging.basicConfig(level=logging.INFO)
|
| 62 |
log = logging.getLogger(__name__)
|
| 63 |
|
| 64 |
+
|
| 65 |
+
def _fill_template(template: str, data: dict[str, Any]) -> str:
|
| 66 |
+
"""Fill placeholders {key} in template with data; missing keys become empty string."""
|
| 67 |
+
placeholders = re.findall(r"\{(\w+)\}", template)
|
| 68 |
+
safe = {k: str(data.get(k, "")) for k in placeholders}
|
| 69 |
+
try:
|
| 70 |
+
return template.format(**safe)
|
| 71 |
+
except KeyError:
|
| 72 |
+
return template
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
def _compute_feedback_level(cmd_result) -> str:
|
| 76 |
+
"""Derive ok/warning/error from command results."""
|
| 77 |
+
if cmd_result.feedback_override:
|
| 78 |
+
return "error"
|
| 79 |
+
results = cmd_result.results
|
| 80 |
+
if not results:
|
| 81 |
+
return "error"
|
| 82 |
+
successes = sum(1 for r in results if r.success)
|
| 83 |
+
if successes == 0:
|
| 84 |
+
return "error"
|
| 85 |
+
if successes < len(results):
|
| 86 |
+
return "warning"
|
| 87 |
+
# All succeeded — check if any result carries an error key
|
| 88 |
+
for r in results:
|
| 89 |
+
if r.data and "error" in r.data:
|
| 90 |
+
return "warning"
|
| 91 |
+
return "ok"
|
| 92 |
+
|
| 93 |
# ---------------------------------------------------------------------------
|
| 94 |
# Socket.IO + FastAPI setup
|
| 95 |
# ---------------------------------------------------------------------------
|
|
|
|
| 111 |
allow_headers=["*"],
|
| 112 |
)
|
| 113 |
|
| 114 |
+
# Paths for static assets (must serve before any catch-all mount)
|
| 115 |
+
_STATIC_DIR = Path(__file__).parent / "static"
|
| 116 |
+
_SPRITES_DIR = Path(__file__).parent / "static" / "sprites"
|
| 117 |
+
|
| 118 |
+
_static_router = APIRouter()
|
| 119 |
+
|
| 120 |
+
@_static_router.get("/static/MAP.png", response_class=FileResponse)
|
| 121 |
+
def _serve_map_png():
|
| 122 |
+
p = _STATIC_DIR / "MAP.png"
|
| 123 |
+
if not p.is_file():
|
| 124 |
+
raise HTTPException(404, "MAP.png not found")
|
| 125 |
+
return FileResponse(p, media_type="image/png")
|
| 126 |
+
|
| 127 |
+
@_static_router.get("/sprites/{kind}/{filename:path}", response_class=FileResponse)
|
| 128 |
+
def _serve_sprite(kind: str, filename: str):
|
| 129 |
+
p = _SPRITES_DIR / kind / filename
|
| 130 |
+
if not p.is_file():
|
| 131 |
+
raise HTTPException(404, "Sprite not found")
|
| 132 |
+
return FileResponse(
|
| 133 |
+
p,
|
| 134 |
+
media_type="image/png",
|
| 135 |
+
headers={"Cache-Control": "no-store"},
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
fastapi_app.include_router(_static_router)
|
| 139 |
+
|
| 140 |
# Unit sound effects (generated by scripts/generate_unit_sounds.py)
|
| 141 |
_SOUNDS_DIR = Path(__file__).parent / "static" / "sounds"
|
| 142 |
_UNITS_SOUNDS_DIR = _SOUNDS_DIR / "units"
|
|
|
|
| 145 |
fastapi_app.mount("/sounds", StaticFiles(directory=str(_SOUNDS_DIR)), name="sounds")
|
| 146 |
|
| 147 |
# Sprites (unit/building icons generated via Mistral image API)
|
|
|
|
| 148 |
_SPRITES_DIR.mkdir(parents=True, exist_ok=True)
|
| 149 |
if _SPRITES_DIR.exists():
|
| 150 |
fastapi_app.mount("/sprites", StaticFiles(directory=str(_SPRITES_DIR)), name="sprites")
|
| 151 |
|
| 152 |
+
# Map image, map.json, walkable polygon (fallback for other static files)
|
|
|
|
| 153 |
if _STATIC_DIR.exists():
|
| 154 |
fastapi_app.mount("/static", StaticFiles(directory=str(_STATIC_DIR)), name="static")
|
| 155 |
|
| 156 |
_MAP_JSON = _STATIC_DIR / "map.json"
|
| 157 |
_WALKABLE_JSON = _STATIC_DIR / "walkable.json"
|
| 158 |
+
_COMPILED_MAP_JSON = _STATIC_DIR / "compiled_map.json"
|
| 159 |
_GAME_POSITIONS_JSON = _STATIC_DIR / "game_positions.json"
|
| 160 |
|
| 161 |
# Game grid size (must match game/map.py)
|
| 162 |
+
_MAP_GRID_W = 80
|
| 163 |
+
_MAP_GRID_H = 80
|
| 164 |
+
|
| 165 |
+
# Compile walkable → exterior + holes once at server startup
|
| 166 |
+
if _WALKABLE_JSON.exists():
|
| 167 |
+
try:
|
| 168 |
+
from game.map import MAP_WIDTH as _MAP_W, MAP_HEIGHT as _MAP_H
|
| 169 |
+
from game.map_compiler import run_compiler
|
| 170 |
+
run_compiler(_WALKABLE_JSON, _COMPILED_MAP_JSON, map_width=_MAP_W, map_height=_MAP_H)
|
| 171 |
+
log.info("Map compiled: %s", _COMPILED_MAP_JSON)
|
| 172 |
+
except Exception as e:
|
| 173 |
+
log.warning("Map compiler skipped: %s", e)
|
| 174 |
|
| 175 |
|
| 176 |
def _load_game_positions() -> dict:
|
|
|
|
| 308 |
|
| 309 |
@fastapi_app.put("/api/map/positions")
|
| 310 |
async def save_map_positions(body: dict = Body(...)):
|
| 311 |
+
"""Enregistre positions de départ (2 ou 3, si 3 alors 2 seront tirées au sort par partie) et expansions."""
|
| 312 |
import json
|
| 313 |
starts = body.get("starting_positions")
|
| 314 |
expansions = body.get("expansion_positions")
|
| 315 |
+
if not isinstance(starts, list) or len(starts) < 2:
|
| 316 |
+
raise HTTPException(400, "starting_positions requis (au moins 2 positions {x, y} en 0-100)")
|
| 317 |
if not isinstance(expansions, list):
|
| 318 |
expansions = []
|
| 319 |
for i, p in enumerate(starts):
|
|
|
|
| 329 |
if not (0 <= x <= 100 and 0 <= y <= 100):
|
| 330 |
raise HTTPException(400, f"expansion_positions[{i}] hors [0,100]")
|
| 331 |
|
| 332 |
+
# Keep positions in 0-100 for admin display; embed minerals/geysers per start so we load only the 2 chosen bases' resources
|
| 333 |
+
starting_positions = []
|
| 334 |
+
total_minerals = 0
|
| 335 |
+
total_geysers = 0
|
| 336 |
+
for p in starts:
|
| 337 |
+
x, y = float(p["x"]), float(p["y"])
|
| 338 |
+
gx, gy = _admin_to_game_x(x), _admin_to_game_y(y)
|
| 339 |
+
minerals = _generate_minerals_around(gx, gy, count=7)
|
| 340 |
+
geysers = _generate_geysers_around(gx, gy, count=1)
|
| 341 |
+
total_minerals += len(minerals)
|
| 342 |
+
total_geysers += len(geysers)
|
| 343 |
+
starting_positions.append({"x": x, "y": y, "minerals": minerals, "geysers": geysers})
|
| 344 |
+
|
| 345 |
+
expansion_positions = []
|
| 346 |
+
for p in expansions:
|
| 347 |
+
x, y = float(p["x"]), float(p["y"])
|
| 348 |
+
gx, gy = _admin_to_game_x(x), _admin_to_game_y(y)
|
| 349 |
+
minerals = _generate_minerals_around(gx, gy, count=7)
|
| 350 |
+
geysers = _generate_geysers_around(gx, gy, count=1)
|
| 351 |
+
total_minerals += len(minerals)
|
| 352 |
+
total_geysers += len(geysers)
|
| 353 |
+
expansion_positions.append({"x": x, "y": y, "minerals": minerals, "geysers": geysers})
|
| 354 |
|
| 355 |
payload = {
|
| 356 |
"starting_positions": starting_positions,
|
| 357 |
"expansion_positions": expansion_positions,
|
|
|
|
|
|
|
| 358 |
}
|
| 359 |
_STATIC_DIR.mkdir(parents=True, exist_ok=True)
|
| 360 |
with open(_GAME_POSITIONS_JSON, "w", encoding="utf-8") as f:
|
| 361 |
json.dump(payload, f, indent=2)
|
| 362 |
+
return {"status": "ok", "minerals_count": total_minerals, "geysers_count": total_geysers}
|
| 363 |
|
| 364 |
|
| 365 |
@fastapi_app.get("/api/sounds/units")
|
|
|
|
| 420 |
|
| 421 |
def run():
|
| 422 |
return subprocess.run(
|
| 423 |
+
[sys.executable, "-m", "scripts.generate_sprites", "--skip-existing"],
|
| 424 |
cwd=str(Path(__file__).parent),
|
| 425 |
capture_output=True,
|
| 426 |
text=True,
|
|
|
|
| 478 |
return {"status": "ok", "id": building_id}
|
| 479 |
|
| 480 |
|
| 481 |
+
@fastapi_app.get("/api/sprites/resources")
|
| 482 |
+
async def list_resource_sprites():
|
| 483 |
+
"""Liste les sprites de ressources présents."""
|
| 484 |
+
return {"sprites": _list_sprites(_SPRITES_DIR / "resources")}
|
| 485 |
+
|
| 486 |
+
|
| 487 |
+
@fastapi_app.post("/api/sprites/generate/resources/{resource_id}")
|
| 488 |
+
async def generate_one_resource_sprite(resource_id: str):
|
| 489 |
+
"""Régénère une seule icône de ressource (mineral, geyser)."""
|
| 490 |
+
resource_id = resource_id.strip().lower()
|
| 491 |
+
valid = ["mineral", "geyser"]
|
| 492 |
+
if resource_id not in valid:
|
| 493 |
+
raise HTTPException(400, f"Resource invalide. Valides: {valid}")
|
| 494 |
+
loop = asyncio.get_event_loop()
|
| 495 |
+
result = await loop.run_in_executor(None, lambda: _run_sprite_script(["--resource", resource_id]))
|
| 496 |
+
if result.returncode != 0:
|
| 497 |
+
raise HTTPException(500, result.stderr or result.stdout or "Échec génération")
|
| 498 |
+
return {"status": "ok", "id": resource_id}
|
| 499 |
+
|
| 500 |
+
|
| 501 |
+
_ICONS_DIR = _SPRITES_DIR / "icons"
|
| 502 |
+
|
| 503 |
+
|
| 504 |
+
@fastapi_app.get("/api/icons")
|
| 505 |
+
async def list_icons():
|
| 506 |
+
"""Liste les icônes UI présentes."""
|
| 507 |
+
return {"icons": _list_sprites(_ICONS_DIR)}
|
| 508 |
+
|
| 509 |
+
|
| 510 |
+
@fastapi_app.post("/api/icons/generate/{icon_id}")
|
| 511 |
+
async def generate_one_icon(icon_id: str):
|
| 512 |
+
"""Génère une icône UI symbolique (mineral, gas, supply)."""
|
| 513 |
+
icon_id = icon_id.strip().lower()
|
| 514 |
+
valid = ["mineral", "gas", "supply"]
|
| 515 |
+
if icon_id not in valid:
|
| 516 |
+
raise HTTPException(400, f"Icône invalide. Valides: {valid}")
|
| 517 |
+
loop = asyncio.get_event_loop()
|
| 518 |
+
result = await loop.run_in_executor(None, lambda: _run_sprite_script(["--icon", icon_id]))
|
| 519 |
+
if result.returncode != 0:
|
| 520 |
+
raise HTTPException(500, result.stderr or result.stdout or "Échec génération")
|
| 521 |
+
return {"status": "ok", "id": icon_id}
|
| 522 |
+
|
| 523 |
+
|
| 524 |
# Serve SvelteKit static build if present (production / HF Spaces)
|
| 525 |
_FRONTEND_BUILD = Path(__file__).parent.parent / "frontend" / "build"
|
| 526 |
if _FRONTEND_BUILD.exists():
|
|
|
|
| 558 |
await sio.emit("error", {"message": message}, to=sid)
|
| 559 |
|
| 560 |
|
| 561 |
+
def _build_resource_zones(engine: Any, player_id: str) -> list[str]:
|
| 562 |
+
"""Return sorted resource zone names (mineral_1…N, geyser_1…M) by proximity to player base."""
|
| 563 |
+
from game.map import ResourceType as _RT
|
| 564 |
+
player = engine.state.players.get(player_id)
|
| 565 |
+
cc = player.command_center() if player else None
|
| 566 |
+
base_x = float(cc.x) + 2 if cc else 40.0
|
| 567 |
+
base_y = float(cc.y) + 2 if cc else 40.0
|
| 568 |
+
resources = engine.state.game_map.resources
|
| 569 |
+
|
| 570 |
+
minerals = sorted(
|
| 571 |
+
[r for r in resources if r.resource_type == _RT.MINERAL and not r.is_depleted],
|
| 572 |
+
key=lambda r: (r.x - base_x) ** 2 + (r.y - base_y) ** 2,
|
| 573 |
+
)
|
| 574 |
+
geysers = sorted(
|
| 575 |
+
[r for r in resources if r.resource_type == _RT.GEYSER],
|
| 576 |
+
key=lambda r: (r.x - base_x) ** 2 + (r.y - base_y) ** 2,
|
| 577 |
+
)
|
| 578 |
+
zones = [f"mineral_{i + 1}" for i in range(len(minerals))]
|
| 579 |
+
zones += [f"geyser_{i + 1}" for i in range(len(geysers))]
|
| 580 |
+
return zones
|
| 581 |
+
|
| 582 |
+
|
| 583 |
async def _start_game(room_id: str) -> None:
|
| 584 |
"""Create game state + engine, emit game_start to human players."""
|
| 585 |
room = lobby.get_room(room_id)
|
|
|
|
| 685 |
# ---------------------------------------------------------------------------
|
| 686 |
|
| 687 |
|
| 688 |
+
@sio.event
|
| 689 |
+
async def get_playing_count(sid: str, data: dict) -> None:
|
| 690 |
+
count = lobby.get_playing_count()
|
| 691 |
+
await sio.emit("playing_count", {"count": count}, to=sid)
|
| 692 |
+
|
| 693 |
+
|
| 694 |
+
@sio.event
|
| 695 |
+
async def observe(sid: str, data: dict) -> None:
|
| 696 |
+
room_id = lobby.get_a_playing_room_id()
|
| 697 |
+
if not room_id:
|
| 698 |
+
await _emit_error(sid, "Aucune partie en cours à observer.")
|
| 699 |
+
return
|
| 700 |
+
await sio.enter_room(sid, room_id)
|
| 701 |
+
engine = engines.get(room_id)
|
| 702 |
+
payload = {"room_id": room_id}
|
| 703 |
+
if engine:
|
| 704 |
+
payload["game_state"] = engine.state.model_dump(mode="json")
|
| 705 |
+
await sio.emit("observe_room", payload, to=sid)
|
| 706 |
+
|
| 707 |
+
|
| 708 |
@sio.event
|
| 709 |
async def create_room(sid: str, data: dict) -> None:
|
| 710 |
name = str(data.get("name", "Player")).strip() or "Player"
|
| 711 |
+
name = await sanitize_player_name(name)
|
| 712 |
room = lobby.create_room(sid, name)
|
| 713 |
await sio.enter_room(sid, room.room_id)
|
| 714 |
await sio.emit("room_created", {"room_id": room.room_id, "room": room.to_dict()}, to=sid)
|
|
|
|
| 722 |
async def join_room(sid: str, data: dict) -> None:
|
| 723 |
room_id = str(data.get("room_id", "")).upper().strip()
|
| 724 |
name = str(data.get("name", "Player")).strip() or "Player"
|
| 725 |
+
name = await sanitize_player_name(name)
|
| 726 |
|
| 727 |
room, err = lobby.join_room(sid, room_id, name)
|
| 728 |
if err:
|
|
|
|
| 743 |
@sio.event
|
| 744 |
async def quick_match(sid: str, data: dict) -> None:
|
| 745 |
name = str(data.get("name", "Player")).strip() or "Player"
|
| 746 |
+
name = await sanitize_player_name(name)
|
| 747 |
room, is_new = lobby.quick_match(sid, name)
|
| 748 |
|
| 749 |
if not is_new:
|
|
|
|
| 874 |
await sio.emit("voice_result", {
|
| 875 |
"transcription": "",
|
| 876 |
"feedback_text": "Je n'ai rien entendu. Appuie et parle!",
|
| 877 |
+
"feedback_level": "warning",
|
| 878 |
"results": [],
|
| 879 |
}, to=sid)
|
| 880 |
return
|
| 881 |
|
| 882 |
# 2. Parse command with Mistral
|
| 883 |
+
resource_zones = _build_resource_zones(engine, sid)
|
| 884 |
try:
|
| 885 |
+
parsed = await command_parser.parse(transcription, player, resource_zones=resource_zones)
|
| 886 |
except Exception as exc:
|
| 887 |
log.exception("Command parsing failed")
|
| 888 |
await _emit_error(sid, f"Erreur d'interprétation: {exc}")
|
|
|
|
| 891 |
# 3. Apply commands to game engine
|
| 892 |
cmd_result = engine.apply_command(sid, parsed)
|
| 893 |
|
| 894 |
+
if cmd_result.feedback_override:
|
| 895 |
+
feedback_text = await command_parser.generate_feedback(
|
| 896 |
+
cmd_result.feedback_override, parsed.language
|
| 897 |
+
)
|
| 898 |
+
else:
|
| 899 |
+
merged: dict[str, Any] = {}
|
| 900 |
+
for r in cmd_result.results:
|
| 901 |
+
merged.update(r.data or {})
|
| 902 |
+
if "error" in merged:
|
| 903 |
+
feedback_text = await command_parser.generate_feedback(
|
| 904 |
+
merged["error"], parsed.language
|
| 905 |
+
)
|
| 906 |
+
else:
|
| 907 |
+
feedback_text = _fill_template(parsed.feedback_template, merged)
|
|
|
|
| 908 |
|
| 909 |
sound_events = []
|
| 910 |
for r in cmd_result.results:
|
|
|
|
| 912 |
await sio.emit("voice_result", {
|
| 913 |
"transcription": transcription,
|
| 914 |
"feedback_text": feedback_text,
|
| 915 |
+
"feedback_level": _compute_feedback_level(cmd_result),
|
| 916 |
"results": [r.model_dump() for r in cmd_result.results],
|
| 917 |
"sound_events": sound_events,
|
| 918 |
}, to=sid)
|
|
|
|
| 948 |
return
|
| 949 |
|
| 950 |
# Parse command with Mistral
|
| 951 |
+
resource_zones = _build_resource_zones(engine, sid)
|
| 952 |
try:
|
| 953 |
+
parsed = await command_parser.parse(transcription, player, resource_zones=resource_zones)
|
| 954 |
except Exception as exc:
|
| 955 |
log.exception("Command parsing failed")
|
| 956 |
await _emit_error(sid, f"Erreur d'interprétation: {exc}")
|
|
|
|
| 959 |
# Apply commands to game engine
|
| 960 |
cmd_result = engine.apply_command(sid, parsed)
|
| 961 |
|
| 962 |
+
if cmd_result.feedback_override:
|
| 963 |
+
feedback_text = await command_parser.generate_feedback(
|
| 964 |
+
cmd_result.feedback_override, parsed.language
|
| 965 |
+
)
|
| 966 |
+
else:
|
| 967 |
+
merged = {}
|
| 968 |
+
for r in cmd_result.results:
|
| 969 |
+
merged.update(r.data or {})
|
| 970 |
+
if "error" in merged:
|
| 971 |
+
feedback_text = await command_parser.generate_feedback(
|
| 972 |
+
merged["error"], parsed.language
|
| 973 |
+
)
|
| 974 |
+
else:
|
| 975 |
+
feedback_text = _fill_template(parsed.feedback_template, merged)
|
| 976 |
|
| 977 |
sound_events = []
|
| 978 |
for r in cmd_result.results:
|
|
|
|
| 980 |
await sio.emit("voice_result", {
|
| 981 |
"transcription": transcription,
|
| 982 |
"feedback_text": feedback_text,
|
| 983 |
+
"feedback_level": _compute_feedback_level(cmd_result),
|
| 984 |
"results": [r.model_dump() for r in cmd_result.results],
|
| 985 |
"sound_events": sound_events,
|
| 986 |
}, to=sid)
|
backend/requirements.txt
CHANGED
|
@@ -5,3 +5,5 @@ python-dotenv>=1.0.0
|
|
| 5 |
pydantic>=2.0.0
|
| 6 |
mistralai>=1.0.0
|
| 7 |
httpx>=0.27.0
|
|
|
|
|
|
|
|
|
| 5 |
pydantic>=2.0.0
|
| 6 |
mistralai>=1.0.0
|
| 7 |
httpx>=0.27.0
|
| 8 |
+
google-genai>=1.0.0
|
| 9 |
+
Pillow>=10.0.0
|
backend/scripts/generate_map_lod.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Génère des versions basse résolution de MAP.png pour le chargement progressif.
|
| 4 |
+
|
| 5 |
+
MAP_quarter.png → 1/4 de la taille originale (chargement rapide ~1-2 Mo)
|
| 6 |
+
MAP_half.png → 1/2 de la taille originale (qualité intermédiaire ~7 Mo)
|
| 7 |
+
|
| 8 |
+
Usage :
|
| 9 |
+
cd backend && python -m scripts.generate_map_lod
|
| 10 |
+
cd backend && python -m scripts.generate_map_lod --input static/MAP.png
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import sys
|
| 16 |
+
from pathlib import Path
|
| 17 |
+
|
| 18 |
+
from PIL import Image
|
| 19 |
+
|
| 20 |
+
_backend = Path(__file__).resolve().parent.parent
|
| 21 |
+
if str(_backend) not in sys.path:
|
| 22 |
+
sys.path.insert(0, str(_backend))
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def generate_lods(input_path: Path) -> None:
|
| 26 |
+
src = Image.open(input_path).convert("RGB")
|
| 27 |
+
w, h = src.size
|
| 28 |
+
size_mb = input_path.stat().st_size / 1024 / 1024
|
| 29 |
+
print(f"Source : {w}×{h} ({size_mb:.1f} Mo) → {input_path}")
|
| 30 |
+
|
| 31 |
+
lods = [
|
| 32 |
+
("MAP_half.png", (w // 2, h // 2)),
|
| 33 |
+
("MAP_quarter.png", (w // 4, h // 4)),
|
| 34 |
+
]
|
| 35 |
+
|
| 36 |
+
for filename, size in lods:
|
| 37 |
+
out_path = input_path.parent / filename
|
| 38 |
+
resized = src.resize(size, Image.LANCZOS)
|
| 39 |
+
resized.save(out_path, format="PNG", optimize=True)
|
| 40 |
+
out_mb = out_path.stat().st_size / 1024 / 1024
|
| 41 |
+
print(f" {filename:25s} {size[0]}×{size[1]} ({out_mb:.1f} Mo) → {out_path}")
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def main() -> None:
|
| 45 |
+
default = _backend / "static" / "MAP.png"
|
| 46 |
+
parser = argparse.ArgumentParser(description="Génère MAP_half.png et MAP_quarter.png")
|
| 47 |
+
parser.add_argument("--input", type=Path, default=default)
|
| 48 |
+
args = parser.parse_args()
|
| 49 |
+
if not args.input.is_file():
|
| 50 |
+
raise SystemExit(f"Fichier introuvable : {args.input}")
|
| 51 |
+
generate_lods(args.input)
|
| 52 |
+
|
| 53 |
+
|
| 54 |
+
if __name__ == "__main__":
|
| 55 |
+
main()
|
backend/scripts/generate_sprites.py
CHANGED
|
@@ -1,14 +1,16 @@
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
-
Génère les sprites PNG (vues de dessus,
|
| 4 |
-
|
| 5 |
-
|
|
|
|
|
|
|
| 6 |
|
| 7 |
Usage : cd backend && python -m scripts.generate_sprites
|
| 8 |
-
Nécessite MISTRAL_API_KEY dans .env
|
| 9 |
"""
|
| 10 |
from __future__ import annotations
|
| 11 |
|
|
|
|
| 12 |
import sys
|
| 13 |
from pathlib import Path
|
| 14 |
|
|
@@ -21,12 +23,10 @@ import random
|
|
| 21 |
import time
|
| 22 |
from typing import Any
|
| 23 |
|
| 24 |
-
from
|
| 25 |
-
from mistralai.models import ToolFileChunk
|
| 26 |
-
|
| 27 |
-
from config import MISTRAL_API_KEY
|
| 28 |
from game.units import UnitType
|
| 29 |
from game.buildings import BuildingType, BUILDING_DEFS
|
|
|
|
| 30 |
|
| 31 |
logging.basicConfig(level=logging.INFO)
|
| 32 |
log = logging.getLogger(__name__)
|
|
@@ -34,6 +34,18 @@ log = logging.getLogger(__name__)
|
|
| 34 |
SPRITES_DIR = _backend / "static" / "sprites"
|
| 35 |
UNITS_DIR = SPRITES_DIR / "units"
|
| 36 |
BUILDINGS_DIR = SPRITES_DIR / "buildings"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
|
| 38 |
# Backoff pour rate limit (429)
|
| 39 |
BACKOFF_BASE_SEC = 30
|
|
@@ -49,25 +61,41 @@ CRITICAL - SMALL SIZE (to avoid rate limits):
|
|
| 49 |
- Generate SMALL, LOW-RESOLUTION images only. For units: exactly 128x128 pixels. For buildings: longest side 128 pixels. Do NOT generate large or high-resolution images (no 512, 1024, etc.). Small output is required.
|
| 50 |
- MANDATORY transparent background: only the subject has visible pixels; everything else fully transparent (PNG with alpha). No ground, no floor, no solid color.
|
| 51 |
|
| 52 |
-
CRITICAL - VIEW ANGLE:
|
| 53 |
-
- Strict top-down view (bird's eye, camera directly above).
|
|
|
|
|
|
|
|
|
|
| 54 |
|
| 55 |
Other: subject fills frame, minimal margins. Dark metallic, blue/gray palette, industrial sci-fi. No text or labels. One image per request, no commentary."""
|
| 56 |
|
| 57 |
# Vue strictement de dessus + fond transparent + remplir le cadre
|
| 58 |
_TOP = "Strict top-down view only (bird's eye, camera directly above, 90°). Only the top surface/footprint visible, no side or perspective. "
|
| 59 |
-
_FILL = "MANDATORY
|
| 60 |
# Petite image demandée à l'API pour éviter le rate limit (pas de post-traitement)
|
| 61 |
_SMALL = "Generate a SMALL low-resolution image only: 128x128 pixels for square sprites, or longest side 128 for rectangles. Tiny size. Do NOT output large or high-resolution (no 512, 1024, etc.). "
|
| 62 |
|
| 63 |
# Prompts par unité : petite 128x128, fond transparent
|
| 64 |
UNIT_PROMPTS: dict[str, str] = {
|
| 65 |
-
UnitType.SCV.value: _TOP + _FILL + _SMALL + "Single SCV worker from above:
|
| 66 |
-
UnitType.MARINE.value: _TOP + _FILL + _SMALL + "Single Terran Marine from above
|
| 67 |
-
UnitType.MEDIC.value: _TOP + _FILL + _SMALL + "Single Medic from above
|
| 68 |
-
UnitType.GOLIATH.value: _TOP + _FILL + _SMALL + "Goliath walker from above: two gun pods,
|
| 69 |
-
UnitType.TANK.value: _TOP + _FILL + _SMALL + "Siege Tank from above: elongated hull, round turret, treads. Military gray and blue.",
|
| 70 |
-
UnitType.WRAITH.value: _TOP + _FILL + _SMALL + "Wraith starfighter from above: delta wing, cockpit. Dark with blue.",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
}
|
| 72 |
|
| 73 |
# Prompts par bâtiment : petite image (longest side 128), fond transparent
|
|
@@ -86,8 +114,110 @@ def _building_prompt(building_type: BuildingType) -> str:
|
|
| 86 |
return base + " Transparent background only. Dark metal and blue."
|
| 87 |
|
| 88 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
def _find_file_id_in_response(response: Any) -> str | None:
|
| 90 |
"""Extrait le file_id du premier ToolFileChunk dans response.outputs."""
|
|
|
|
| 91 |
for entry in getattr(response, "outputs", []) or []:
|
| 92 |
content = getattr(entry, "content", None)
|
| 93 |
if content is None:
|
|
@@ -172,98 +302,186 @@ def _generate_one(client: Mistral, agent_id: str, prompt: str, out_path: Path, p
|
|
| 172 |
if data is None:
|
| 173 |
return False
|
| 174 |
|
| 175 |
-
|
| 176 |
-
out_path.
|
| 177 |
-
|
| 178 |
-
log.info("[%s] OK %s — sauvegardé (%s Ko) → %s", progress or name, name, round(size_kb, 1), out_path)
|
| 179 |
return True
|
| 180 |
|
| 181 |
|
| 182 |
def main() -> None:
|
| 183 |
import argparse
|
| 184 |
-
parser = argparse.ArgumentParser(description="Generate unit/building sprites
|
| 185 |
parser.add_argument("--unit", type=str, metavar="ID", help="Generate only this unit (e.g. marine, scv)")
|
| 186 |
parser.add_argument("--building", type=str, metavar="ID", help="Generate only this building (e.g. barracks)")
|
|
|
|
|
|
|
|
|
|
| 187 |
args = parser.parse_args()
|
| 188 |
|
| 189 |
only_unit = args.unit.strip().lower() if args.unit else None
|
|
|
|
| 190 |
only_building = args.building.strip().lower().replace(" ", "_") if args.building else None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
sys.exit(1)
|
| 195 |
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
agent = client.beta.agents.create(
|
| 201 |
-
model="mistral-medium-2505",
|
| 202 |
-
name="Sprite Generator",
|
| 203 |
-
description="Generates top-down game sprites for units and buildings.",
|
| 204 |
-
instructions=AGENT_INSTRUCTIONS,
|
| 205 |
-
tools=[{"type": "image_generation"}],
|
| 206 |
-
completion_args={"temperature": 0.3, "top_p": 0.95},
|
| 207 |
-
)
|
| 208 |
-
agent_id = agent.id
|
| 209 |
-
log.info("Agent id: %s", agent_id)
|
| 210 |
-
except Exception as e:
|
| 211 |
-
log.exception("Failed to create agent: %s", e)
|
| 212 |
-
sys.exit(1)
|
| 213 |
|
| 214 |
UNITS_DIR.mkdir(parents=True, exist_ok=True)
|
| 215 |
BUILDINGS_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
|
| 216 |
|
| 217 |
-
# Si --unit X : uniquement cette unité (pas de bâtiments). Si --building Y : uniquement ce bâtiment (pas d'unités).
|
| 218 |
units_to_run: list = []
|
| 219 |
-
if not
|
| 220 |
-
units_to_run = [ut for ut in UnitType if not
|
| 221 |
-
if
|
| 222 |
log.error("Unknown unit: %s. Valid: %s", only_unit, [u.value for u in UnitType])
|
| 223 |
sys.exit(1)
|
| 224 |
|
| 225 |
buildings_to_run: list = []
|
| 226 |
-
if not
|
| 227 |
-
buildings_to_run = [bt for bt in BuildingType if not
|
| 228 |
-
if
|
| 229 |
log.error("Unknown building: %s. Valid: %s", only_building, [b.value for b in BuildingType])
|
| 230 |
sys.exit(1)
|
| 231 |
|
| 232 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 233 |
log.info("========== GÉNÉRATION SPRITES ==========")
|
| 234 |
log.info("Unités à générer: %s (%s)", [u.value for u in units_to_run], len(units_to_run))
|
| 235 |
log.info("Bâtiments à générer: %s (%s)", [b.value for b in buildings_to_run], len(buildings_to_run))
|
|
|
|
|
|
|
| 236 |
log.info("Total: %s sprites. Délai entre chaque: %s s.", total, DELAY_BETWEEN_SPRITES_SEC)
|
| 237 |
log.info("=========================================")
|
| 238 |
|
| 239 |
ok, fail = 0, 0
|
| 240 |
t0 = time.monotonic()
|
|
|
|
| 241 |
|
| 242 |
for i, ut in enumerate(units_to_run):
|
| 243 |
-
if i > 0:
|
| 244 |
-
log.info("--- Pause %s s (rate limit) avant prochaine unité ---", DELAY_BETWEEN_SPRITES_SEC)
|
| 245 |
-
time.sleep(DELAY_BETWEEN_SPRITES_SEC)
|
| 246 |
out = UNITS_DIR / f"{ut.value}.png"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 247 |
prompt = UNIT_PROMPTS.get(ut.value, _TOP + _FILL + _SMALL + f"{ut.value} unit from above, sci-fi RTS. Transparent background only.")
|
| 248 |
progress = "unit %s/%s" % (i + 1, len(units_to_run))
|
| 249 |
-
if
|
| 250 |
ok += 1
|
| 251 |
else:
|
| 252 |
fail += 1
|
| 253 |
log.warning("Échec pour unité %s", ut.value)
|
|
|
|
| 254 |
|
| 255 |
for i, bt in enumerate(buildings_to_run):
|
| 256 |
-
if i > 0 or units_to_run:
|
| 257 |
-
log.info("--- Pause %s s (rate limit) avant prochain bâtiment ---", DELAY_BETWEEN_SPRITES_SEC)
|
| 258 |
-
time.sleep(DELAY_BETWEEN_SPRITES_SEC)
|
| 259 |
out = BUILDINGS_DIR / f"{bt.value}.png"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
prompt = _building_prompt(bt)
|
| 261 |
progress = "building %s/%s" % (i + 1, len(buildings_to_run))
|
| 262 |
-
if
|
| 263 |
ok += 1
|
| 264 |
else:
|
| 265 |
fail += 1
|
| 266 |
log.warning("Échec pour bâtiment %s", bt.value)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 267 |
|
| 268 |
elapsed = time.monotonic() - t0
|
| 269 |
log.info("========== FIN ==========")
|
|
|
|
| 1 |
#!/usr/bin/env python3
|
| 2 |
"""
|
| 3 |
+
Génère les sprites PNG (vues de dessus, style SF Starcraft/Warhammer).
|
| 4 |
+
|
| 5 |
+
Backend au choix (priorité GCP si configuré) :
|
| 6 |
+
- GCP Vertex AI Imagen : définir GCP_PROJECT_ID (et optionnellement GCP_LOCATION) dans .env
|
| 7 |
+
- Mistral : définir MISTRAL_API_KEY dans .env
|
| 8 |
|
| 9 |
Usage : cd backend && python -m scripts.generate_sprites
|
|
|
|
| 10 |
"""
|
| 11 |
from __future__ import annotations
|
| 12 |
|
| 13 |
+
import os
|
| 14 |
import sys
|
| 15 |
from pathlib import Path
|
| 16 |
|
|
|
|
| 23 |
import time
|
| 24 |
from typing import Any
|
| 25 |
|
| 26 |
+
from config import GCP_PROJECT_ID, GCP_LOCATION, MISTRAL_API_KEY
|
|
|
|
|
|
|
|
|
|
| 27 |
from game.units import UnitType
|
| 28 |
from game.buildings import BuildingType, BUILDING_DEFS
|
| 29 |
+
from game.map import ResourceType
|
| 30 |
|
| 31 |
logging.basicConfig(level=logging.INFO)
|
| 32 |
log = logging.getLogger(__name__)
|
|
|
|
| 34 |
SPRITES_DIR = _backend / "static" / "sprites"
|
| 35 |
UNITS_DIR = SPRITES_DIR / "units"
|
| 36 |
BUILDINGS_DIR = SPRITES_DIR / "buildings"
|
| 37 |
+
RESOURCES_DIR = SPRITES_DIR / "resources"
|
| 38 |
+
ICONS_DIR = SPRITES_DIR / "icons"
|
| 39 |
+
|
| 40 |
+
# Taille cible et fond transparent obligatoires
|
| 41 |
+
TARGET_SIZE = 128
|
| 42 |
+
|
| 43 |
+
# Chroma vert : fond #00ff00 demandé à l'API, détourage sur cette couleur uniquement
|
| 44 |
+
CHROMA_GREEN_RGB = (0, 255, 0) # #00ff00 pour le prompt
|
| 45 |
+
CHROMA_GREEN_TOLERANCE = 55 # pour vert vif proche de #00ff00
|
| 46 |
+
# Vert "dominant" (G > R et G > B) pour attraper les verts plus foncés renvoyés par Imagen
|
| 47 |
+
CHROMA_GREEN_MIN_G = 60 # G >= ce seuil et G dominant → considéré fond vert
|
| 48 |
+
CHROMA_GREEN_PROMPT = " The background must be solid green #00ff00 only, like a green screen. Subject in gray/blue/metallic, no white or other background."
|
| 49 |
|
| 50 |
# Backoff pour rate limit (429)
|
| 51 |
BACKOFF_BASE_SEC = 30
|
|
|
|
| 61 |
- Generate SMALL, LOW-RESOLUTION images only. For units: exactly 128x128 pixels. For buildings: longest side 128 pixels. Do NOT generate large or high-resolution images (no 512, 1024, etc.). Small output is required.
|
| 62 |
- MANDATORY transparent background: only the subject has visible pixels; everything else fully transparent (PNG with alpha). No ground, no floor, no solid color.
|
| 63 |
|
| 64 |
+
CRITICAL - VIEW ANGLE (NO EXCEPTIONS):
|
| 65 |
+
- Strict top-down view ONLY (bird's eye, camera at exactly 90 degrees directly above the subject). This applies to EVERY sprite without exception, including soldiers, mechs, vehicles, and buildings.
|
| 66 |
+
- You must ONLY see the top surface/footprint of the subject. No side view, no 3/4 view, no isometric, no perspective angle.
|
| 67 |
+
- For units with weapons (marine, goliath, tank, etc.): the weapon barrel must point straight forward, visible from above as a shape pointing toward the top of the image.
|
| 68 |
+
- Reject any tendency to show a front/side/3D perspective — the camera is directly overhead, period.
|
| 69 |
|
| 70 |
Other: subject fills frame, minimal margins. Dark metallic, blue/gray palette, industrial sci-fi. No text or labels. One image per request, no commentary."""
|
| 71 |
|
| 72 |
# Vue strictement de dessus + fond transparent + remplir le cadre
|
| 73 |
_TOP = "Strict top-down view only (bird's eye, camera directly above, 90°). Only the top surface/footprint visible, no side or perspective. "
|
| 74 |
+
_FILL = "MANDATORY green #00ff00 background only (no ground, no floor, no details). Subject must fill the frame tightly, edge to edge. "
|
| 75 |
# Petite image demandée à l'API pour éviter le rate limit (pas de post-traitement)
|
| 76 |
_SMALL = "Generate a SMALL low-resolution image only: 128x128 pixels for square sprites, or longest side 128 for rectangles. Tiny size. Do NOT output large or high-resolution (no 512, 1024, etc.). "
|
| 77 |
|
| 78 |
# Prompts par unité : petite 128x128, fond transparent
|
| 79 |
UNIT_PROMPTS: dict[str, str] = {
|
| 80 |
+
UnitType.SCV.value: _TOP + _FILL + _SMALL + "Single SCV worker robot seen strictly from directly above at 90 degrees, bird's eye view only. Top surface visible: mechanical body footprint, two mechanical arms extended sideways, cockpit hatch on top. Industrial gray and blue. Camera is directly overhead, no side or perspective angle.",
|
| 81 |
+
UnitType.MARINE.value: _TOP + _FILL + _SMALL + "Single Terran Marine soldier seen strictly from directly above at 90 degrees, bird's eye view only. Top of armored helmet visible at center, heavy shoulder pads on both sides, rifle/assault gun barrel pointing straight forward (upward in the image). Dark blue armor. No side view, no isometric, camera directly overhead.",
|
| 82 |
+
UnitType.MEDIC.value: _TOP + _FILL + _SMALL + "Single Terran Medic soldier seen strictly from directly above at 90 degrees, bird's eye view only. Top of helmet visible at center, white/light armor with red cross medical symbol on back, medkit or injector pointing forward. Sci-fi armor. Camera directly overhead, no side or perspective.",
|
| 83 |
+
UnitType.GOLIATH.value: _TOP + _FILL + _SMALL + "Goliath combat walker mech seen strictly from directly above at 90 degrees, bird's eye view only. Top surface: wide armored torso, two autocannon gun pods mounted on shoulders pointing straight forward (upward in image), legs spread below. Dark metal and blue. Camera directly overhead at 90 degrees, no side view.",
|
| 84 |
+
UnitType.TANK.value: _TOP + _FILL + _SMALL + "Siege Tank seen strictly from directly above at 90 degrees, bird's eye view only. Top surface: elongated armored hull, round turret on top with long cannon barrel pointing straight forward (upward in image), tank treads visible on both sides. Military gray and blue. Camera directly overhead.",
|
| 85 |
+
UnitType.WRAITH.value: _TOP + _FILL + _SMALL + "Wraith starfighter aircraft seen strictly from directly above at 90 degrees, bird's eye view only. Top surface: delta wing shape, cockpit bubble at center-front, weapon pods on wing edges pointing forward. Dark metallic with blue highlights. Camera directly overhead.",
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
# Icônes UI : style flat symbolique, fond vert chroma pour détourage
|
| 89 |
+
ICON_PROMPTS: dict[str, str] = {
|
| 90 |
+
"mineral": _FILL + _SMALL + "Flat symbolic icon of a blue mineral crystal, bold angular shape, solid blue-cyan color, thick black outline, 2D game UI icon style. No background, no text, no shading.",
|
| 91 |
+
"gas": _FILL + _SMALL + "Flat symbolic icon of a purple vespene gas canister or flask, bold shape, solid purple-violet color, thick black outline, 2D game UI icon style. No green. No background, no text, no shading.",
|
| 92 |
+
"supply": _FILL + _SMALL + "Flat symbolic icon of a chevron/arrow pointing up or a small house shape, bold, solid yellow-orange color, thick black outline, 2D game UI icon style. No background, no text, no shading.",
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
# Prompts par ressource : icône top-down 64x64
|
| 96 |
+
RESOURCE_PROMPTS: dict[str, str] = {
|
| 97 |
+
ResourceType.MINERAL.value: _TOP + _FILL + _SMALL + "Mineral crystal cluster from above: angular blue-teal crystalline shards, glowing. Sci-fi RTS style. No text.",
|
| 98 |
+
ResourceType.GEYSER.value: _TOP + _FILL + _SMALL + "Vespene gas geyser from above: purple/violet glowing vent/crater with purple gas fumes rising. Purple and magenta colors only, NO green. Sci-fi RTS style. No text.",
|
| 99 |
}
|
| 100 |
|
| 101 |
# Prompts par bâtiment : petite image (longest side 128), fond transparent
|
|
|
|
| 114 |
return base + " Transparent background only. Dark metal and blue."
|
| 115 |
|
| 116 |
|
| 117 |
+
def _is_chroma_green(r: int, g: int, b: int) -> bool:
|
| 118 |
+
"""True si le pixel est fond vert à détourer : proche de #00ff00 ou vert dominant (G > R, G > B)."""
|
| 119 |
+
# Vert vif type #00ff00
|
| 120 |
+
r0, g0, b0 = CHROMA_GREEN_RGB
|
| 121 |
+
if (
|
| 122 |
+
abs(r - r0) <= CHROMA_GREEN_TOLERANCE
|
| 123 |
+
and abs(g - g0) <= CHROMA_GREEN_TOLERANCE
|
| 124 |
+
and abs(b - b0) <= CHROMA_GREEN_TOLERANCE
|
| 125 |
+
):
|
| 126 |
+
return True
|
| 127 |
+
# Vert plus foncé (Imagen renvoie souvent ~50,127,70) : G dominant et G assez élevé
|
| 128 |
+
if g >= CHROMA_GREEN_MIN_G and g >= r and g >= b:
|
| 129 |
+
# exclure les gris (r≈g≈b) et garder un vrai vert (g nettement > r ou b)
|
| 130 |
+
if r > 100 and b > 100 and abs(r - g) < 30 and abs(b - g) < 30:
|
| 131 |
+
return False # gris, pas vert
|
| 132 |
+
return True
|
| 133 |
+
return False
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def _resize_and_make_transparent(data: bytes, out_path: Path, size: int = TARGET_SIZE) -> None:
|
| 137 |
+
"""Redimensionne à size px (côté max). Rend transparent : fond vert chroma ET blanc/clair. Sauvegarde en PNG."""
|
| 138 |
+
from PIL import Image
|
| 139 |
+
import io
|
| 140 |
+
img = Image.open(io.BytesIO(data)).convert("RGBA")
|
| 141 |
+
w, h = img.size
|
| 142 |
+
if w > size or h > size:
|
| 143 |
+
scale = size / max(w, h)
|
| 144 |
+
nw, nh = max(1, int(w * scale)), max(1, int(h * scale))
|
| 145 |
+
img = img.resize((nw, nh), Image.Resampling.LANCZOS)
|
| 146 |
+
pixels = img.load()
|
| 147 |
+
for y in range(img.height):
|
| 148 |
+
for x in range(img.width):
|
| 149 |
+
r, g, b, a = pixels[x, y]
|
| 150 |
+
if _is_chroma_green(r, g, b):
|
| 151 |
+
pixels[x, y] = (r, g, b, 0)
|
| 152 |
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 153 |
+
img.save(out_path, "PNG")
|
| 154 |
+
|
| 155 |
+
|
| 156 |
+
# ---------------------------------------------------------------------------
|
| 157 |
+
# GCP Vertex AI Imagen (optionnel)
|
| 158 |
+
# ---------------------------------------------------------------------------
|
| 159 |
+
|
| 160 |
+
def _generate_one_vertex(prompt: str, out_path: Path, progress: str = "") -> bool:
|
| 161 |
+
"""Génère une image via Vertex AI Imagen et la sauvegarde."""
|
| 162 |
+
from google import genai
|
| 163 |
+
from google.genai.types import GenerateImagesConfig
|
| 164 |
+
|
| 165 |
+
name = out_path.stem
|
| 166 |
+
for attempt in range(BACKOFF_MAX_RETRIES):
|
| 167 |
+
try:
|
| 168 |
+
if attempt == 0:
|
| 169 |
+
log.info("[%s] Appel Vertex Imagen pour %s...", progress or name, name)
|
| 170 |
+
else:
|
| 171 |
+
log.info("[%s] Retry %s/%s — Imagen pour %s...", progress or name, attempt + 1, BACKOFF_MAX_RETRIES, name)
|
| 172 |
+
# GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION, GOOGLE_GENAI_USE_VERTEXAI set in main()
|
| 173 |
+
client = genai.Client(vertexai=True)
|
| 174 |
+
vertex_prompt = prompt + CHROMA_GREEN_PROMPT
|
| 175 |
+
response = client.models.generate_images(
|
| 176 |
+
model="imagen-3.0-fast-generate-001",
|
| 177 |
+
prompt=vertex_prompt,
|
| 178 |
+
config=GenerateImagesConfig(
|
| 179 |
+
numberOfImages=1,
|
| 180 |
+
aspectRatio="1:1",
|
| 181 |
+
),
|
| 182 |
+
)
|
| 183 |
+
if not response.generated_images or not response.generated_images[0].image:
|
| 184 |
+
log.warning("[%s] Réponse Imagen vide pour %s", progress or name, name)
|
| 185 |
+
return False
|
| 186 |
+
img = response.generated_images[0].image
|
| 187 |
+
raw_bytes: bytes
|
| 188 |
+
if hasattr(img, "image_bytes") and img.image_bytes is not None:
|
| 189 |
+
raw_bytes = img.image_bytes
|
| 190 |
+
elif hasattr(img, "save") and callable(img.save):
|
| 191 |
+
import io
|
| 192 |
+
buf = io.BytesIO()
|
| 193 |
+
img.save(buf)
|
| 194 |
+
raw_bytes = buf.getvalue()
|
| 195 |
+
else:
|
| 196 |
+
log.error("[%s] Format image Imagen inattendu pour %s", progress or name, name)
|
| 197 |
+
return False
|
| 198 |
+
_resize_and_make_transparent(raw_bytes, out_path)
|
| 199 |
+
size_kb = out_path.stat().st_size / 1024
|
| 200 |
+
log.info("[%s] OK %s — sauvegardé 128px transparent (%s Ko) → %s", progress or name, name, round(size_kb, 1), out_path)
|
| 201 |
+
return True
|
| 202 |
+
except Exception as e:
|
| 203 |
+
err_msg = str(e)
|
| 204 |
+
if ("429" in err_msg or "rate limit" in err_msg.lower() or "resource exhausted" in err_msg.lower()) and attempt < BACKOFF_MAX_RETRIES - 1:
|
| 205 |
+
delay = min(BACKOFF_BASE_SEC * (2 ** attempt), BACKOFF_MAX_SEC)
|
| 206 |
+
jitter = random.uniform(0, delay * 0.2)
|
| 207 |
+
time.sleep(delay + jitter)
|
| 208 |
+
else:
|
| 209 |
+
log.error("[%s] Échec Imagen pour %s: %s", progress or name, name, err_msg)
|
| 210 |
+
return False
|
| 211 |
+
return False
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
# ---------------------------------------------------------------------------
|
| 215 |
+
# Mistral (fallback si GCP non configuré)
|
| 216 |
+
# ---------------------------------------------------------------------------
|
| 217 |
+
|
| 218 |
def _find_file_id_in_response(response: Any) -> str | None:
|
| 219 |
"""Extrait le file_id du premier ToolFileChunk dans response.outputs."""
|
| 220 |
+
from mistralai.models import ToolFileChunk
|
| 221 |
for entry in getattr(response, "outputs", []) or []:
|
| 222 |
content = getattr(entry, "content", None)
|
| 223 |
if content is None:
|
|
|
|
| 302 |
if data is None:
|
| 303 |
return False
|
| 304 |
|
| 305 |
+
_resize_and_make_transparent(data, out_path)
|
| 306 |
+
size_kb = out_path.stat().st_size / 1024
|
| 307 |
+
log.info("[%s] OK %s — sauvegardé 128px transparent (%s Ko) → %s", progress or name, name, round(size_kb, 1), out_path)
|
|
|
|
| 308 |
return True
|
| 309 |
|
| 310 |
|
| 311 |
def main() -> None:
|
| 312 |
import argparse
|
| 313 |
+
parser = argparse.ArgumentParser(description="Generate unit/building sprites (Vertex Imagen or Mistral)")
|
| 314 |
parser.add_argument("--unit", type=str, metavar="ID", help="Generate only this unit (e.g. marine, scv)")
|
| 315 |
parser.add_argument("--building", type=str, metavar="ID", help="Generate only this building (e.g. barracks)")
|
| 316 |
+
parser.add_argument("--resource", type=str, metavar="ID", help="Generate only this resource (e.g. mineral, geyser)")
|
| 317 |
+
parser.add_argument("--icon", type=str, metavar="ID", help="Generate only this UI icon (e.g. mineral, gas, supply)")
|
| 318 |
+
parser.add_argument("--skip-existing", action="store_true", help="Skip sprites that already exist (do not regenerate)")
|
| 319 |
args = parser.parse_args()
|
| 320 |
|
| 321 |
only_unit = args.unit.strip().lower() if args.unit else None
|
| 322 |
+
skip_existing = args.skip_existing
|
| 323 |
only_building = args.building.strip().lower().replace(" ", "_") if args.building else None
|
| 324 |
+
only_resource = args.resource.strip().lower() if args.resource else None
|
| 325 |
+
only_icon = args.icon.strip().lower() if args.icon else None
|
| 326 |
+
|
| 327 |
+
use_vertex = bool(GCP_PROJECT_ID)
|
| 328 |
+
if use_vertex:
|
| 329 |
+
os.environ["GOOGLE_CLOUD_PROJECT"] = GCP_PROJECT_ID
|
| 330 |
+
os.environ["GOOGLE_CLOUD_LOCATION"] = GCP_LOCATION
|
| 331 |
+
os.environ["GOOGLE_GENAI_USE_VERTEXAI"] = "true"
|
| 332 |
+
log.info("Backend: Vertex AI Imagen (project=%s, location=%s)", GCP_PROJECT_ID, GCP_LOCATION)
|
| 333 |
+
|
| 334 |
+
def do_generate(prompt: str, out: Path, progress: str) -> bool:
|
| 335 |
+
return _generate_one_vertex(prompt, out, progress)
|
| 336 |
+
else:
|
| 337 |
+
if not MISTRAL_API_KEY:
|
| 338 |
+
log.error("Aucun backend image configuré. Définir GCP_PROJECT_ID (ex: mtgbinder) ou MISTRAL_API_KEY dans .env")
|
| 339 |
+
sys.exit(1)
|
| 340 |
+
from mistralai import Mistral
|
| 341 |
+
client = Mistral(api_key=MISTRAL_API_KEY)
|
| 342 |
+
log.info("Creating Mistral image generation agent...")
|
| 343 |
+
try:
|
| 344 |
+
agent = client.beta.agents.create(
|
| 345 |
+
model="mistral-medium-2505",
|
| 346 |
+
name="Sprite Generator",
|
| 347 |
+
description="Generates top-down game sprites for units and buildings.",
|
| 348 |
+
instructions=AGENT_INSTRUCTIONS,
|
| 349 |
+
tools=[{"type": "image_generation"}],
|
| 350 |
+
completion_args={"temperature": 0.3, "top_p": 0.95},
|
| 351 |
+
)
|
| 352 |
+
agent_id = agent.id
|
| 353 |
+
log.info("Agent id: %s", agent_id)
|
| 354 |
+
except Exception as e:
|
| 355 |
+
log.exception("Failed to create agent: %s", e)
|
| 356 |
+
sys.exit(1)
|
| 357 |
|
| 358 |
+
def do_generate(prompt: str, out: Path, progress: str) -> bool:
|
| 359 |
+
return _generate_one(client, agent_id, prompt, out, progress=progress)
|
|
|
|
| 360 |
|
| 361 |
+
only_resource_flag = only_resource is not None
|
| 362 |
+
only_unit_flag = only_unit is not None
|
| 363 |
+
only_building_flag = only_building is not None
|
| 364 |
+
only_icon_flag = only_icon is not None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 365 |
|
| 366 |
UNITS_DIR.mkdir(parents=True, exist_ok=True)
|
| 367 |
BUILDINGS_DIR.mkdir(parents=True, exist_ok=True)
|
| 368 |
+
RESOURCES_DIR.mkdir(parents=True, exist_ok=True)
|
| 369 |
+
ICONS_DIR.mkdir(parents=True, exist_ok=True)
|
| 370 |
|
|
|
|
| 371 |
units_to_run: list = []
|
| 372 |
+
if not only_building_flag and not only_resource_flag and not only_icon_flag:
|
| 373 |
+
units_to_run = [ut for ut in UnitType if not only_unit_flag or ut.value == only_unit]
|
| 374 |
+
if only_unit_flag and not units_to_run:
|
| 375 |
log.error("Unknown unit: %s. Valid: %s", only_unit, [u.value for u in UnitType])
|
| 376 |
sys.exit(1)
|
| 377 |
|
| 378 |
buildings_to_run: list = []
|
| 379 |
+
if not only_unit_flag and not only_resource_flag and not only_icon_flag:
|
| 380 |
+
buildings_to_run = [bt for bt in BuildingType if not only_building_flag or bt.value == only_building]
|
| 381 |
+
if only_building_flag and not buildings_to_run:
|
| 382 |
log.error("Unknown building: %s. Valid: %s", only_building, [b.value for b in BuildingType])
|
| 383 |
sys.exit(1)
|
| 384 |
|
| 385 |
+
resources_to_run: list = []
|
| 386 |
+
if not only_unit_flag and not only_building_flag and not only_icon_flag:
|
| 387 |
+
valid_resources = list(RESOURCE_PROMPTS.keys())
|
| 388 |
+
resources_to_run = [r for r in valid_resources if not only_resource_flag or r == only_resource]
|
| 389 |
+
if only_resource_flag and not resources_to_run:
|
| 390 |
+
log.error("Unknown resource: %s. Valid: %s", only_resource, valid_resources)
|
| 391 |
+
sys.exit(1)
|
| 392 |
+
|
| 393 |
+
icons_to_run: list = []
|
| 394 |
+
if not only_unit_flag and not only_building_flag and not only_resource_flag:
|
| 395 |
+
valid_icons = list(ICON_PROMPTS.keys())
|
| 396 |
+
icons_to_run = [ic for ic in valid_icons if not only_icon_flag or ic == only_icon]
|
| 397 |
+
if only_icon_flag and not icons_to_run:
|
| 398 |
+
log.error("Unknown icon: %s. Valid: %s", only_icon, valid_icons)
|
| 399 |
+
sys.exit(1)
|
| 400 |
+
|
| 401 |
+
total = len(units_to_run) + len(buildings_to_run) + len(resources_to_run) + len(icons_to_run)
|
| 402 |
log.info("========== GÉNÉRATION SPRITES ==========")
|
| 403 |
log.info("Unités à générer: %s (%s)", [u.value for u in units_to_run], len(units_to_run))
|
| 404 |
log.info("Bâtiments à générer: %s (%s)", [b.value for b in buildings_to_run], len(buildings_to_run))
|
| 405 |
+
log.info("Ressources à générer: %s (%s)", resources_to_run, len(resources_to_run))
|
| 406 |
+
log.info("Icônes UI à générer: %s (%s)", icons_to_run, len(icons_to_run))
|
| 407 |
log.info("Total: %s sprites. Délai entre chaque: %s s.", total, DELAY_BETWEEN_SPRITES_SEC)
|
| 408 |
log.info("=========================================")
|
| 409 |
|
| 410 |
ok, fail = 0, 0
|
| 411 |
t0 = time.monotonic()
|
| 412 |
+
generated_count = 0
|
| 413 |
|
| 414 |
for i, ut in enumerate(units_to_run):
|
|
|
|
|
|
|
|
|
|
| 415 |
out = UNITS_DIR / f"{ut.value}.png"
|
| 416 |
+
if skip_existing and out.exists():
|
| 417 |
+
log.info("[unit %s/%s] Déjà présent, ignoré: %s", i + 1, len(units_to_run), out.name)
|
| 418 |
+
ok += 1
|
| 419 |
+
continue
|
| 420 |
+
if generated_count > 0:
|
| 421 |
+
log.info("--- Pause %s s (rate limit) ---", DELAY_BETWEEN_SPRITES_SEC)
|
| 422 |
+
time.sleep(DELAY_BETWEEN_SPRITES_SEC)
|
| 423 |
prompt = UNIT_PROMPTS.get(ut.value, _TOP + _FILL + _SMALL + f"{ut.value} unit from above, sci-fi RTS. Transparent background only.")
|
| 424 |
progress = "unit %s/%s" % (i + 1, len(units_to_run))
|
| 425 |
+
if do_generate(prompt, out, progress):
|
| 426 |
ok += 1
|
| 427 |
else:
|
| 428 |
fail += 1
|
| 429 |
log.warning("Échec pour unité %s", ut.value)
|
| 430 |
+
generated_count += 1
|
| 431 |
|
| 432 |
for i, bt in enumerate(buildings_to_run):
|
|
|
|
|
|
|
|
|
|
| 433 |
out = BUILDINGS_DIR / f"{bt.value}.png"
|
| 434 |
+
if skip_existing and out.exists():
|
| 435 |
+
log.info("[building %s/%s] Déjà présent, ignoré: %s", i + 1, len(buildings_to_run), out.name)
|
| 436 |
+
ok += 1
|
| 437 |
+
continue
|
| 438 |
+
if generated_count > 0:
|
| 439 |
+
log.info("--- Pause %s s (rate limit) ---", DELAY_BETWEEN_SPRITES_SEC)
|
| 440 |
+
time.sleep(DELAY_BETWEEN_SPRITES_SEC)
|
| 441 |
prompt = _building_prompt(bt)
|
| 442 |
progress = "building %s/%s" % (i + 1, len(buildings_to_run))
|
| 443 |
+
if do_generate(prompt, out, progress):
|
| 444 |
ok += 1
|
| 445 |
else:
|
| 446 |
fail += 1
|
| 447 |
log.warning("Échec pour bâtiment %s", bt.value)
|
| 448 |
+
generated_count += 1
|
| 449 |
+
|
| 450 |
+
for i, rid in enumerate(resources_to_run):
|
| 451 |
+
out = RESOURCES_DIR / f"{rid}.png"
|
| 452 |
+
if skip_existing and out.exists():
|
| 453 |
+
log.info("[resource %s/%s] Déjà présent, ignoré: %s", i + 1, len(resources_to_run), out.name)
|
| 454 |
+
ok += 1
|
| 455 |
+
continue
|
| 456 |
+
if generated_count > 0:
|
| 457 |
+
log.info("--- Pause %s s (rate limit) ---", DELAY_BETWEEN_SPRITES_SEC)
|
| 458 |
+
time.sleep(DELAY_BETWEEN_SPRITES_SEC)
|
| 459 |
+
prompt = RESOURCE_PROMPTS[rid]
|
| 460 |
+
progress = "resource %s/%s" % (i + 1, len(resources_to_run))
|
| 461 |
+
if do_generate(prompt, out, progress):
|
| 462 |
+
ok += 1
|
| 463 |
+
else:
|
| 464 |
+
fail += 1
|
| 465 |
+
log.warning("Échec pour ressource %s", rid)
|
| 466 |
+
generated_count += 1
|
| 467 |
+
|
| 468 |
+
for i, ic in enumerate(icons_to_run):
|
| 469 |
+
out = ICONS_DIR / f"{ic}.png"
|
| 470 |
+
if skip_existing and out.exists():
|
| 471 |
+
log.info("[icon %s/%s] Déjà présent, ignoré: %s", i + 1, len(icons_to_run), out.name)
|
| 472 |
+
ok += 1
|
| 473 |
+
continue
|
| 474 |
+
if generated_count > 0:
|
| 475 |
+
log.info("--- Pause %s s (rate limit) ---", DELAY_BETWEEN_SPRITES_SEC)
|
| 476 |
+
time.sleep(DELAY_BETWEEN_SPRITES_SEC)
|
| 477 |
+
prompt = ICON_PROMPTS[ic]
|
| 478 |
+
progress = "icon %s/%s" % (i + 1, len(icons_to_run))
|
| 479 |
+
if do_generate(prompt, out, progress):
|
| 480 |
+
ok += 1
|
| 481 |
+
else:
|
| 482 |
+
fail += 1
|
| 483 |
+
log.warning("Échec pour icône %s", ic)
|
| 484 |
+
generated_count += 1
|
| 485 |
|
| 486 |
elapsed = time.monotonic() - t0
|
| 487 |
log.info("========== FIN ==========")
|
backend/scripts/upscale_cover.py
ADDED
|
@@ -0,0 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Upscale l'image cover (lobby) via Vertex AI Imagen 4.0 upscale.
|
| 4 |
+
|
| 5 |
+
Prérequis : GCP_PROJECT_ID et GCP_LOCATION dans .env, et `gcloud auth application-default login`
|
| 6 |
+
(ou GOOGLE_APPLICATION_CREDENTIALS).
|
| 7 |
+
|
| 8 |
+
Usage :
|
| 9 |
+
cd backend && python -m scripts.upscale_cover
|
| 10 |
+
cd backend && python -m scripts.upscale_cover --factor x2 --input ../frontend/src/cover.jpg --output ../frontend/src/cover.jpg
|
| 11 |
+
"""
|
| 12 |
+
from __future__ import annotations
|
| 13 |
+
|
| 14 |
+
import argparse
|
| 15 |
+
import base64
|
| 16 |
+
import subprocess
|
| 17 |
+
import sys
|
| 18 |
+
from pathlib import Path
|
| 19 |
+
|
| 20 |
+
_backend = Path(__file__).resolve().parent.parent
|
| 21 |
+
if str(_backend) not in sys.path:
|
| 22 |
+
sys.path.insert(0, str(_backend))
|
| 23 |
+
|
| 24 |
+
from config import GCP_PROJECT_ID, GCP_LOCATION
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def get_access_token() -> str:
|
| 28 |
+
"""Token via gcloud (ADC)."""
|
| 29 |
+
out = subprocess.run(
|
| 30 |
+
["gcloud", "auth", "print-access-token"],
|
| 31 |
+
capture_output=True,
|
| 32 |
+
text=True,
|
| 33 |
+
check=False,
|
| 34 |
+
)
|
| 35 |
+
if out.returncode != 0:
|
| 36 |
+
raise RuntimeError(
|
| 37 |
+
"Échec gcloud auth. Lancez: gcloud auth application-default login"
|
| 38 |
+
)
|
| 39 |
+
return out.stdout.strip()
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def upscale_image(
|
| 43 |
+
image_path: Path,
|
| 44 |
+
out_path: Path,
|
| 45 |
+
factor: str = "x4",
|
| 46 |
+
region: str | None = None,
|
| 47 |
+
project_id: str | None = None,
|
| 48 |
+
) -> None:
|
| 49 |
+
region = region or GCP_LOCATION
|
| 50 |
+
project_id = project_id or GCP_PROJECT_ID
|
| 51 |
+
if not project_id:
|
| 52 |
+
raise SystemExit("Définir GCP_PROJECT_ID dans .env")
|
| 53 |
+
|
| 54 |
+
data = image_path.read_bytes()
|
| 55 |
+
if len(data) > 10 * 1024 * 1024:
|
| 56 |
+
raise SystemExit("Image trop lourde (max 10 Mo)")
|
| 57 |
+
|
| 58 |
+
b64 = base64.standard_b64encode(data).decode("ascii")
|
| 59 |
+
token = get_access_token()
|
| 60 |
+
url = (
|
| 61 |
+
f"https://{region}-aiplatform.googleapis.com/v1/projects/{project_id}"
|
| 62 |
+
f"/locations/{region}/publishers/google/models/imagen-4.0-upscale-preview:predict"
|
| 63 |
+
)
|
| 64 |
+
import httpx
|
| 65 |
+
|
| 66 |
+
payload = {
|
| 67 |
+
"instances": [
|
| 68 |
+
{"prompt": "Upscale the image", "image": {"bytesBase64Encoded": b64}}
|
| 69 |
+
],
|
| 70 |
+
"parameters": {
|
| 71 |
+
"mode": "upscale",
|
| 72 |
+
"upscaleConfig": {"upscaleFactor": factor},
|
| 73 |
+
"outputOptions": {"mimeType": "image/png"},
|
| 74 |
+
},
|
| 75 |
+
}
|
| 76 |
+
resp = httpx.post(
|
| 77 |
+
url,
|
| 78 |
+
json=payload,
|
| 79 |
+
headers={
|
| 80 |
+
"Authorization": f"Bearer {token}",
|
| 81 |
+
"Content-Type": "application/json",
|
| 82 |
+
},
|
| 83 |
+
timeout=120.0,
|
| 84 |
+
)
|
| 85 |
+
resp.raise_for_status()
|
| 86 |
+
body = resp.json()
|
| 87 |
+
preds = body.get("predictions") or []
|
| 88 |
+
if not preds or "bytesBase64Encoded" not in preds[0]:
|
| 89 |
+
raise SystemExit("Réponse Vertex sans image: " + str(body)[:500])
|
| 90 |
+
out_b64 = preds[0]["bytesBase64Encoded"]
|
| 91 |
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
| 92 |
+
out_path.write_bytes(base64.standard_b64decode(out_b64))
|
| 93 |
+
print(f"Upscale {factor} OK → {out_path} ({out_path.stat().st_size / 1024:.1f} Ko)")
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def main() -> None:
|
| 97 |
+
parser = argparse.ArgumentParser(description="Upscale cover image via Vertex AI Imagen")
|
| 98 |
+
parser.add_argument(
|
| 99 |
+
"--input",
|
| 100 |
+
type=Path,
|
| 101 |
+
default=_backend.parent / "frontend" / "src" / "cover.jpg",
|
| 102 |
+
help="Image source",
|
| 103 |
+
)
|
| 104 |
+
parser.add_argument(
|
| 105 |
+
"--output",
|
| 106 |
+
type=Path,
|
| 107 |
+
default=None,
|
| 108 |
+
help="Image de sortie (défaut: --input)",
|
| 109 |
+
)
|
| 110 |
+
parser.add_argument(
|
| 111 |
+
"--factor",
|
| 112 |
+
choices=("x2", "x3", "x4"),
|
| 113 |
+
default="x4",
|
| 114 |
+
help="Facteur d'upscale (défaut: x4)",
|
| 115 |
+
)
|
| 116 |
+
args = parser.parse_args()
|
| 117 |
+
out = args.output or args.input
|
| 118 |
+
if not args.input.is_file():
|
| 119 |
+
raise SystemExit(f"Fichier introuvable: {args.input}")
|
| 120 |
+
upscale_image(args.input, out, factor=args.factor)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
if __name__ == "__main__":
|
| 124 |
+
main()
|
backend/scripts/upscale_map.py
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
Upscale MAP.png en 4 passes (4 quadrants) via Vertex AI Imagen upscale.
|
| 4 |
+
|
| 5 |
+
La map étant trop lourde pour l'API en un seul appel (> 10 Mo), on :
|
| 6 |
+
1. Découpe en 4 quadrants de 2048×2048
|
| 7 |
+
2. Upscale chaque quadrant ×2 → 4096×4096 chacun
|
| 8 |
+
3. Recolle en une image finale 8192×8192
|
| 9 |
+
|
| 10 |
+
Usage :
|
| 11 |
+
cd backend && python -m scripts.upscale_map
|
| 12 |
+
cd backend && python -m scripts.upscale_map --factor x2 --input static/MAP.png --output static/MAP.png
|
| 13 |
+
"""
|
| 14 |
+
from __future__ import annotations
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import io
|
| 18 |
+
import sys
|
| 19 |
+
from pathlib import Path
|
| 20 |
+
|
| 21 |
+
from PIL import Image
|
| 22 |
+
|
| 23 |
+
_backend = Path(__file__).resolve().parent.parent
|
| 24 |
+
if str(_backend) not in sys.path:
|
| 25 |
+
sys.path.insert(0, str(_backend))
|
| 26 |
+
|
| 27 |
+
from scripts.upscale_cover import upscale_image
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def upscale_map(
|
| 31 |
+
input_path: Path,
|
| 32 |
+
output_path: Path,
|
| 33 |
+
factor: str = "x2",
|
| 34 |
+
) -> None:
|
| 35 |
+
src = Image.open(input_path).convert("RGB")
|
| 36 |
+
w, h = src.size
|
| 37 |
+
print(f"Image source : {w}×{h} ({input_path.stat().st_size / 1024 / 1024:.1f} Mo)")
|
| 38 |
+
|
| 39 |
+
half_w, half_h = w // 2, h // 2
|
| 40 |
+
quadrants = [
|
| 41 |
+
(0, 0, half_w, half_h), # top-left
|
| 42 |
+
(half_w, 0, w, half_h), # top-right
|
| 43 |
+
(0, half_h, half_w, h ), # bottom-left
|
| 44 |
+
(half_w, half_h, w, h ), # bottom-right
|
| 45 |
+
]
|
| 46 |
+
labels = ["top-left", "top-right", "bottom-left", "bottom-right"]
|
| 47 |
+
|
| 48 |
+
upscaled_quadrants: list[Image.Image] = []
|
| 49 |
+
|
| 50 |
+
for i, (box, label) in enumerate(zip(quadrants, labels)):
|
| 51 |
+
print(f"\n[{i+1}/4] Quadrant {label} {box}")
|
| 52 |
+
|
| 53 |
+
quad_img = src.crop(box)
|
| 54 |
+
tmp_in = Path(f"/tmp/map_quad_{i}.png")
|
| 55 |
+
tmp_out = Path(f"/tmp/map_quad_{i}_up.png")
|
| 56 |
+
|
| 57 |
+
buf = io.BytesIO()
|
| 58 |
+
quad_img.save(buf, format="PNG", optimize=False)
|
| 59 |
+
size_mb = len(buf.getvalue()) / 1024 / 1024
|
| 60 |
+
print(f" Taille quadrant : {size_mb:.1f} Mo", end="")
|
| 61 |
+
if size_mb > 10:
|
| 62 |
+
print(" ⚠ > 10 Mo, passage en JPEG lossy pour respecter la limite API")
|
| 63 |
+
buf = io.BytesIO()
|
| 64 |
+
quad_img.save(buf, format="JPEG", quality=92)
|
| 65 |
+
tmp_in = Path(f"/tmp/map_quad_{i}.jpg")
|
| 66 |
+
else:
|
| 67 |
+
print()
|
| 68 |
+
|
| 69 |
+
tmp_in.write_bytes(buf.getvalue())
|
| 70 |
+
|
| 71 |
+
upscale_image(tmp_in, tmp_out, factor=factor)
|
| 72 |
+
upscaled_quadrants.append(Image.open(tmp_out).convert("RGB"))
|
| 73 |
+
|
| 74 |
+
uw, uh = upscaled_quadrants[0].size
|
| 75 |
+
print(f"\nRecomposition : quadrants upscalés {uw}×{uh} → image finale {uw*2}×{uh*2}")
|
| 76 |
+
final = Image.new("RGB", (uw * 2, uh * 2))
|
| 77 |
+
final.paste(upscaled_quadrants[0], (0, 0))
|
| 78 |
+
final.paste(upscaled_quadrants[1], (uw, 0))
|
| 79 |
+
final.paste(upscaled_quadrants[2], (0, uh))
|
| 80 |
+
final.paste(upscaled_quadrants[3], (uw, uh))
|
| 81 |
+
|
| 82 |
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
| 83 |
+
final.save(output_path, format="PNG", optimize=False)
|
| 84 |
+
print(f"\nSauvegardé : {output_path} ({output_path.stat().st_size / 1024 / 1024:.1f} Mo)")
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def main() -> None:
|
| 88 |
+
default_map = _backend / "static" / "MAP.png"
|
| 89 |
+
parser = argparse.ArgumentParser(description="Upscale MAP.png en 4 quadrants via Vertex AI")
|
| 90 |
+
parser.add_argument("--input", type=Path, default=default_map)
|
| 91 |
+
parser.add_argument("--output", type=Path, default=None)
|
| 92 |
+
parser.add_argument("--factor", choices=("x2", "x4"), default="x2")
|
| 93 |
+
args = parser.parse_args()
|
| 94 |
+
|
| 95 |
+
out = args.output or args.input
|
| 96 |
+
if not args.input.is_file():
|
| 97 |
+
raise SystemExit(f"Fichier introuvable : {args.input}")
|
| 98 |
+
|
| 99 |
+
upscale_map(args.input, out, factor=args.factor)
|
| 100 |
+
|
| 101 |
+
|
| 102 |
+
if __name__ == "__main__":
|
| 103 |
+
main()
|
backend/static/MAP.png
CHANGED
|
Git LFS Details
|
|
Git LFS Details
|
backend/static/MAP_half.png
ADDED
|
Git LFS Details
|
backend/static/MAP_quarter.png
ADDED
|
Git LFS Details
|
backend/static/compiled_map.json
ADDED
|
@@ -0,0 +1,4211 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"exterior": [
|
| 3 |
+
[
|
| 4 |
+
43.7365,
|
| 5 |
+
65.5492
|
| 6 |
+
],
|
| 7 |
+
[
|
| 8 |
+
47.7007,
|
| 9 |
+
67.2865
|
| 10 |
+
],
|
| 11 |
+
[
|
| 12 |
+
41.6913,
|
| 13 |
+
75.7753
|
| 14 |
+
],
|
| 15 |
+
[
|
| 16 |
+
45.6539,
|
| 17 |
+
77.8206
|
| 18 |
+
],
|
| 19 |
+
[
|
| 20 |
+
41.18,
|
| 21 |
+
80.8884
|
| 22 |
+
],
|
| 23 |
+
[
|
| 24 |
+
46.4852,
|
| 25 |
+
85.1941
|
| 26 |
+
],
|
| 27 |
+
[
|
| 28 |
+
41.9342,
|
| 29 |
+
88.3091
|
| 30 |
+
],
|
| 31 |
+
[
|
| 32 |
+
51.1504,
|
| 33 |
+
95.2049
|
| 34 |
+
],
|
| 35 |
+
[
|
| 36 |
+
58.3087,
|
| 37 |
+
94.2606
|
| 38 |
+
],
|
| 39 |
+
[
|
| 40 |
+
62.1435,
|
| 41 |
+
99.1675
|
| 42 |
+
],
|
| 43 |
+
[
|
| 44 |
+
46.8043,
|
| 45 |
+
98.9119
|
| 46 |
+
],
|
| 47 |
+
[
|
| 48 |
+
41.18,
|
| 49 |
+
95.4606
|
| 50 |
+
],
|
| 51 |
+
[
|
| 52 |
+
36.5783,
|
| 53 |
+
98.1449
|
| 54 |
+
],
|
| 55 |
+
[
|
| 56 |
+
29.5478,
|
| 57 |
+
93.2875
|
| 58 |
+
],
|
| 59 |
+
[
|
| 60 |
+
25.0739,
|
| 61 |
+
94.6936
|
| 62 |
+
],
|
| 63 |
+
[
|
| 64 |
+
18.4269,
|
| 65 |
+
93.0319
|
| 66 |
+
],
|
| 67 |
+
[
|
| 68 |
+
14.72,
|
| 69 |
+
90.4753
|
| 70 |
+
],
|
| 71 |
+
[
|
| 72 |
+
11.2687,
|
| 73 |
+
87.9188
|
| 74 |
+
],
|
| 75 |
+
[
|
| 76 |
+
7.6895,
|
| 77 |
+
80.7606
|
| 78 |
+
],
|
| 79 |
+
[
|
| 80 |
+
5.6443,
|
| 81 |
+
77.8206
|
| 82 |
+
],
|
| 83 |
+
[
|
| 84 |
+
10.1182,
|
| 85 |
+
76.5423
|
| 86 |
+
],
|
| 87 |
+
[
|
| 88 |
+
11.1408,
|
| 89 |
+
72.5797
|
| 90 |
+
],
|
| 91 |
+
[
|
| 92 |
+
16.1261,
|
| 93 |
+
72.1962
|
| 94 |
+
],
|
| 95 |
+
[
|
| 96 |
+
16.1261,
|
| 97 |
+
74.6249
|
| 98 |
+
],
|
| 99 |
+
[
|
| 100 |
+
23.2843,
|
| 101 |
+
72.9632
|
| 102 |
+
],
|
| 103 |
+
[
|
| 104 |
+
29.6756,
|
| 105 |
+
69.8953
|
| 106 |
+
],
|
| 107 |
+
[
|
| 108 |
+
33.8939,
|
| 109 |
+
73.091
|
| 110 |
+
],
|
| 111 |
+
[
|
| 112 |
+
37.6009,
|
| 113 |
+
73.9858
|
| 114 |
+
],
|
| 115 |
+
[
|
| 116 |
+
43.0878,
|
| 117 |
+
65.1726
|
| 118 |
+
]
|
| 119 |
+
],
|
| 120 |
+
"holes": [],
|
| 121 |
+
"nav_points": [
|
| 122 |
+
[
|
| 123 |
+
2.7408,
|
| 124 |
+
47.5359
|
| 125 |
+
],
|
| 126 |
+
[
|
| 127 |
+
4.3408,
|
| 128 |
+
45.9359
|
| 129 |
+
],
|
| 130 |
+
[
|
| 131 |
+
4.3408,
|
| 132 |
+
47.5359
|
| 133 |
+
],
|
| 134 |
+
[
|
| 135 |
+
5.9408,
|
| 136 |
+
44.3359
|
| 137 |
+
],
|
| 138 |
+
[
|
| 139 |
+
5.9408,
|
| 140 |
+
45.9359
|
| 141 |
+
],
|
| 142 |
+
[
|
| 143 |
+
5.9408,
|
| 144 |
+
47.5359
|
| 145 |
+
],
|
| 146 |
+
[
|
| 147 |
+
5.9408,
|
| 148 |
+
49.1359
|
| 149 |
+
],
|
| 150 |
+
[
|
| 151 |
+
5.9408,
|
| 152 |
+
61.9359
|
| 153 |
+
],
|
| 154 |
+
[
|
| 155 |
+
5.9408,
|
| 156 |
+
63.5359
|
| 157 |
+
],
|
| 158 |
+
[
|
| 159 |
+
7.5408,
|
| 160 |
+
20.3359
|
| 161 |
+
],
|
| 162 |
+
[
|
| 163 |
+
7.5408,
|
| 164 |
+
44.3359
|
| 165 |
+
],
|
| 166 |
+
[
|
| 167 |
+
7.5408,
|
| 168 |
+
45.9359
|
| 169 |
+
],
|
| 170 |
+
[
|
| 171 |
+
7.5408,
|
| 172 |
+
47.5359
|
| 173 |
+
],
|
| 174 |
+
[
|
| 175 |
+
7.5408,
|
| 176 |
+
61.9359
|
| 177 |
+
],
|
| 178 |
+
[
|
| 179 |
+
7.5408,
|
| 180 |
+
63.5359
|
| 181 |
+
],
|
| 182 |
+
[
|
| 183 |
+
7.5408,
|
| 184 |
+
65.1359
|
| 185 |
+
],
|
| 186 |
+
[
|
| 187 |
+
7.5408,
|
| 188 |
+
66.7359
|
| 189 |
+
],
|
| 190 |
+
[
|
| 191 |
+
9.1408,
|
| 192 |
+
17.1359
|
| 193 |
+
],
|
| 194 |
+
[
|
| 195 |
+
9.1408,
|
| 196 |
+
18.7359
|
| 197 |
+
],
|
| 198 |
+
[
|
| 199 |
+
9.1408,
|
| 200 |
+
20.3359
|
| 201 |
+
],
|
| 202 |
+
[
|
| 203 |
+
9.1408,
|
| 204 |
+
21.9359
|
| 205 |
+
],
|
| 206 |
+
[
|
| 207 |
+
9.1408,
|
| 208 |
+
44.3359
|
| 209 |
+
],
|
| 210 |
+
[
|
| 211 |
+
9.1408,
|
| 212 |
+
45.9359
|
| 213 |
+
],
|
| 214 |
+
[
|
| 215 |
+
9.1408,
|
| 216 |
+
47.5359
|
| 217 |
+
],
|
| 218 |
+
[
|
| 219 |
+
9.1408,
|
| 220 |
+
58.7359
|
| 221 |
+
],
|
| 222 |
+
[
|
| 223 |
+
9.1408,
|
| 224 |
+
60.3359
|
| 225 |
+
],
|
| 226 |
+
[
|
| 227 |
+
9.1408,
|
| 228 |
+
61.9359
|
| 229 |
+
],
|
| 230 |
+
[
|
| 231 |
+
9.1408,
|
| 232 |
+
63.5359
|
| 233 |
+
],
|
| 234 |
+
[
|
| 235 |
+
9.1408,
|
| 236 |
+
65.1359
|
| 237 |
+
],
|
| 238 |
+
[
|
| 239 |
+
9.1408,
|
| 240 |
+
66.7359
|
| 241 |
+
],
|
| 242 |
+
[
|
| 243 |
+
9.1408,
|
| 244 |
+
68.3359
|
| 245 |
+
],
|
| 246 |
+
[
|
| 247 |
+
9.1408,
|
| 248 |
+
69.9359
|
| 249 |
+
],
|
| 250 |
+
[
|
| 251 |
+
10.7408,
|
| 252 |
+
17.1359
|
| 253 |
+
],
|
| 254 |
+
[
|
| 255 |
+
10.7408,
|
| 256 |
+
18.7359
|
| 257 |
+
],
|
| 258 |
+
[
|
| 259 |
+
10.7408,
|
| 260 |
+
20.3359
|
| 261 |
+
],
|
| 262 |
+
[
|
| 263 |
+
10.7408,
|
| 264 |
+
21.9359
|
| 265 |
+
],
|
| 266 |
+
[
|
| 267 |
+
10.7408,
|
| 268 |
+
23.5359
|
| 269 |
+
],
|
| 270 |
+
[
|
| 271 |
+
10.7408,
|
| 272 |
+
28.3359
|
| 273 |
+
],
|
| 274 |
+
[
|
| 275 |
+
10.7408,
|
| 276 |
+
44.3359
|
| 277 |
+
],
|
| 278 |
+
[
|
| 279 |
+
10.7408,
|
| 280 |
+
45.9359
|
| 281 |
+
],
|
| 282 |
+
[
|
| 283 |
+
10.7408,
|
| 284 |
+
47.5359
|
| 285 |
+
],
|
| 286 |
+
[
|
| 287 |
+
10.7408,
|
| 288 |
+
58.7359
|
| 289 |
+
],
|
| 290 |
+
[
|
| 291 |
+
10.7408,
|
| 292 |
+
60.3359
|
| 293 |
+
],
|
| 294 |
+
[
|
| 295 |
+
10.7408,
|
| 296 |
+
61.9359
|
| 297 |
+
],
|
| 298 |
+
[
|
| 299 |
+
10.7408,
|
| 300 |
+
63.5359
|
| 301 |
+
],
|
| 302 |
+
[
|
| 303 |
+
10.7408,
|
| 304 |
+
65.1359
|
| 305 |
+
],
|
| 306 |
+
[
|
| 307 |
+
10.7408,
|
| 308 |
+
66.7359
|
| 309 |
+
],
|
| 310 |
+
[
|
| 311 |
+
10.7408,
|
| 312 |
+
68.3359
|
| 313 |
+
],
|
| 314 |
+
[
|
| 315 |
+
10.7408,
|
| 316 |
+
69.9359
|
| 317 |
+
],
|
| 318 |
+
[
|
| 319 |
+
10.7408,
|
| 320 |
+
71.5359
|
| 321 |
+
],
|
| 322 |
+
[
|
| 323 |
+
12.3408,
|
| 324 |
+
7.5359
|
| 325 |
+
],
|
| 326 |
+
[
|
| 327 |
+
12.3408,
|
| 328 |
+
9.1359
|
| 329 |
+
],
|
| 330 |
+
[
|
| 331 |
+
12.3408,
|
| 332 |
+
15.5359
|
| 333 |
+
],
|
| 334 |
+
[
|
| 335 |
+
12.3408,
|
| 336 |
+
17.1359
|
| 337 |
+
],
|
| 338 |
+
[
|
| 339 |
+
12.3408,
|
| 340 |
+
18.7359
|
| 341 |
+
],
|
| 342 |
+
[
|
| 343 |
+
12.3408,
|
| 344 |
+
20.3359
|
| 345 |
+
],
|
| 346 |
+
[
|
| 347 |
+
12.3408,
|
| 348 |
+
21.9359
|
| 349 |
+
],
|
| 350 |
+
[
|
| 351 |
+
12.3408,
|
| 352 |
+
23.5359
|
| 353 |
+
],
|
| 354 |
+
[
|
| 355 |
+
12.3408,
|
| 356 |
+
26.7359
|
| 357 |
+
],
|
| 358 |
+
[
|
| 359 |
+
12.3408,
|
| 360 |
+
28.3359
|
| 361 |
+
],
|
| 362 |
+
[
|
| 363 |
+
12.3408,
|
| 364 |
+
42.7359
|
| 365 |
+
],
|
| 366 |
+
[
|
| 367 |
+
12.3408,
|
| 368 |
+
44.3359
|
| 369 |
+
],
|
| 370 |
+
[
|
| 371 |
+
12.3408,
|
| 372 |
+
45.9359
|
| 373 |
+
],
|
| 374 |
+
[
|
| 375 |
+
12.3408,
|
| 376 |
+
47.5359
|
| 377 |
+
],
|
| 378 |
+
[
|
| 379 |
+
12.3408,
|
| 380 |
+
49.1359
|
| 381 |
+
],
|
| 382 |
+
[
|
| 383 |
+
12.3408,
|
| 384 |
+
58.7359
|
| 385 |
+
],
|
| 386 |
+
[
|
| 387 |
+
12.3408,
|
| 388 |
+
60.3359
|
| 389 |
+
],
|
| 390 |
+
[
|
| 391 |
+
12.3408,
|
| 392 |
+
61.9359
|
| 393 |
+
],
|
| 394 |
+
[
|
| 395 |
+
12.3408,
|
| 396 |
+
63.5359
|
| 397 |
+
],
|
| 398 |
+
[
|
| 399 |
+
12.3408,
|
| 400 |
+
65.1359
|
| 401 |
+
],
|
| 402 |
+
[
|
| 403 |
+
12.3408,
|
| 404 |
+
66.7359
|
| 405 |
+
],
|
| 406 |
+
[
|
| 407 |
+
12.3408,
|
| 408 |
+
68.3359
|
| 409 |
+
],
|
| 410 |
+
[
|
| 411 |
+
12.3408,
|
| 412 |
+
69.9359
|
| 413 |
+
],
|
| 414 |
+
[
|
| 415 |
+
12.3408,
|
| 416 |
+
71.5359
|
| 417 |
+
],
|
| 418 |
+
[
|
| 419 |
+
13.9408,
|
| 420 |
+
7.5359
|
| 421 |
+
],
|
| 422 |
+
[
|
| 423 |
+
13.9408,
|
| 424 |
+
9.1359
|
| 425 |
+
],
|
| 426 |
+
[
|
| 427 |
+
13.9408,
|
| 428 |
+
10.7359
|
| 429 |
+
],
|
| 430 |
+
[
|
| 431 |
+
13.9408,
|
| 432 |
+
12.3359
|
| 433 |
+
],
|
| 434 |
+
[
|
| 435 |
+
13.9408,
|
| 436 |
+
15.5359
|
| 437 |
+
],
|
| 438 |
+
[
|
| 439 |
+
13.9408,
|
| 440 |
+
17.1359
|
| 441 |
+
],
|
| 442 |
+
[
|
| 443 |
+
13.9408,
|
| 444 |
+
18.7359
|
| 445 |
+
],
|
| 446 |
+
[
|
| 447 |
+
13.9408,
|
| 448 |
+
20.3359
|
| 449 |
+
],
|
| 450 |
+
[
|
| 451 |
+
13.9408,
|
| 452 |
+
21.9359
|
| 453 |
+
],
|
| 454 |
+
[
|
| 455 |
+
13.9408,
|
| 456 |
+
23.5359
|
| 457 |
+
],
|
| 458 |
+
[
|
| 459 |
+
13.9408,
|
| 460 |
+
25.1359
|
| 461 |
+
],
|
| 462 |
+
[
|
| 463 |
+
13.9408,
|
| 464 |
+
26.7359
|
| 465 |
+
],
|
| 466 |
+
[
|
| 467 |
+
13.9408,
|
| 468 |
+
28.3359
|
| 469 |
+
],
|
| 470 |
+
[
|
| 471 |
+
13.9408,
|
| 472 |
+
41.1359
|
| 473 |
+
],
|
| 474 |
+
[
|
| 475 |
+
13.9408,
|
| 476 |
+
42.7359
|
| 477 |
+
],
|
| 478 |
+
[
|
| 479 |
+
13.9408,
|
| 480 |
+
44.3359
|
| 481 |
+
],
|
| 482 |
+
[
|
| 483 |
+
13.9408,
|
| 484 |
+
45.9359
|
| 485 |
+
],
|
| 486 |
+
[
|
| 487 |
+
13.9408,
|
| 488 |
+
47.5359
|
| 489 |
+
],
|
| 490 |
+
[
|
| 491 |
+
13.9408,
|
| 492 |
+
49.1359
|
| 493 |
+
],
|
| 494 |
+
[
|
| 495 |
+
13.9408,
|
| 496 |
+
50.7359
|
| 497 |
+
],
|
| 498 |
+
[
|
| 499 |
+
13.9408,
|
| 500 |
+
60.3359
|
| 501 |
+
],
|
| 502 |
+
[
|
| 503 |
+
13.9408,
|
| 504 |
+
61.9359
|
| 505 |
+
],
|
| 506 |
+
[
|
| 507 |
+
13.9408,
|
| 508 |
+
63.5359
|
| 509 |
+
],
|
| 510 |
+
[
|
| 511 |
+
13.9408,
|
| 512 |
+
65.1359
|
| 513 |
+
],
|
| 514 |
+
[
|
| 515 |
+
13.9408,
|
| 516 |
+
66.7359
|
| 517 |
+
],
|
| 518 |
+
[
|
| 519 |
+
13.9408,
|
| 520 |
+
68.3359
|
| 521 |
+
],
|
| 522 |
+
[
|
| 523 |
+
13.9408,
|
| 524 |
+
69.9359
|
| 525 |
+
],
|
| 526 |
+
[
|
| 527 |
+
13.9408,
|
| 528 |
+
71.5359
|
| 529 |
+
],
|
| 530 |
+
[
|
| 531 |
+
13.9408,
|
| 532 |
+
73.1359
|
| 533 |
+
],
|
| 534 |
+
[
|
| 535 |
+
15.5408,
|
| 536 |
+
5.9359
|
| 537 |
+
],
|
| 538 |
+
[
|
| 539 |
+
15.5408,
|
| 540 |
+
7.5359
|
| 541 |
+
],
|
| 542 |
+
[
|
| 543 |
+
15.5408,
|
| 544 |
+
9.1359
|
| 545 |
+
],
|
| 546 |
+
[
|
| 547 |
+
15.5408,
|
| 548 |
+
10.7359
|
| 549 |
+
],
|
| 550 |
+
[
|
| 551 |
+
15.5408,
|
| 552 |
+
12.3359
|
| 553 |
+
],
|
| 554 |
+
[
|
| 555 |
+
15.5408,
|
| 556 |
+
13.9359
|
| 557 |
+
],
|
| 558 |
+
[
|
| 559 |
+
15.5408,
|
| 560 |
+
15.5359
|
| 561 |
+
],
|
| 562 |
+
[
|
| 563 |
+
15.5408,
|
| 564 |
+
17.1359
|
| 565 |
+
],
|
| 566 |
+
[
|
| 567 |
+
15.5408,
|
| 568 |
+
18.7359
|
| 569 |
+
],
|
| 570 |
+
[
|
| 571 |
+
15.5408,
|
| 572 |
+
20.3359
|
| 573 |
+
],
|
| 574 |
+
[
|
| 575 |
+
15.5408,
|
| 576 |
+
21.9359
|
| 577 |
+
],
|
| 578 |
+
[
|
| 579 |
+
15.5408,
|
| 580 |
+
23.5359
|
| 581 |
+
],
|
| 582 |
+
[
|
| 583 |
+
15.5408,
|
| 584 |
+
25.1359
|
| 585 |
+
],
|
| 586 |
+
[
|
| 587 |
+
15.5408,
|
| 588 |
+
26.7359
|
| 589 |
+
],
|
| 590 |
+
[
|
| 591 |
+
15.5408,
|
| 592 |
+
28.3359
|
| 593 |
+
],
|
| 594 |
+
[
|
| 595 |
+
15.5408,
|
| 596 |
+
29.9359
|
| 597 |
+
],
|
| 598 |
+
[
|
| 599 |
+
15.5408,
|
| 600 |
+
39.5359
|
| 601 |
+
],
|
| 602 |
+
[
|
| 603 |
+
15.5408,
|
| 604 |
+
41.1359
|
| 605 |
+
],
|
| 606 |
+
[
|
| 607 |
+
15.5408,
|
| 608 |
+
42.7359
|
| 609 |
+
],
|
| 610 |
+
[
|
| 611 |
+
15.5408,
|
| 612 |
+
44.3359
|
| 613 |
+
],
|
| 614 |
+
[
|
| 615 |
+
15.5408,
|
| 616 |
+
45.9359
|
| 617 |
+
],
|
| 618 |
+
[
|
| 619 |
+
15.5408,
|
| 620 |
+
47.5359
|
| 621 |
+
],
|
| 622 |
+
[
|
| 623 |
+
15.5408,
|
| 624 |
+
49.1359
|
| 625 |
+
],
|
| 626 |
+
[
|
| 627 |
+
15.5408,
|
| 628 |
+
50.7359
|
| 629 |
+
],
|
| 630 |
+
[
|
| 631 |
+
15.5408,
|
| 632 |
+
60.3359
|
| 633 |
+
],
|
| 634 |
+
[
|
| 635 |
+
15.5408,
|
| 636 |
+
61.9359
|
| 637 |
+
],
|
| 638 |
+
[
|
| 639 |
+
15.5408,
|
| 640 |
+
63.5359
|
| 641 |
+
],
|
| 642 |
+
[
|
| 643 |
+
15.5408,
|
| 644 |
+
65.1359
|
| 645 |
+
],
|
| 646 |
+
[
|
| 647 |
+
15.5408,
|
| 648 |
+
66.7359
|
| 649 |
+
],
|
| 650 |
+
[
|
| 651 |
+
15.5408,
|
| 652 |
+
68.3359
|
| 653 |
+
],
|
| 654 |
+
[
|
| 655 |
+
15.5408,
|
| 656 |
+
69.9359
|
| 657 |
+
],
|
| 658 |
+
[
|
| 659 |
+
15.5408,
|
| 660 |
+
71.5359
|
| 661 |
+
],
|
| 662 |
+
[
|
| 663 |
+
15.5408,
|
| 664 |
+
73.1359
|
| 665 |
+
],
|
| 666 |
+
[
|
| 667 |
+
17.1408,
|
| 668 |
+
5.9359
|
| 669 |
+
],
|
| 670 |
+
[
|
| 671 |
+
17.1408,
|
| 672 |
+
7.5359
|
| 673 |
+
],
|
| 674 |
+
[
|
| 675 |
+
17.1408,
|
| 676 |
+
9.1359
|
| 677 |
+
],
|
| 678 |
+
[
|
| 679 |
+
17.1408,
|
| 680 |
+
10.7359
|
| 681 |
+
],
|
| 682 |
+
[
|
| 683 |
+
17.1408,
|
| 684 |
+
12.3359
|
| 685 |
+
],
|
| 686 |
+
[
|
| 687 |
+
17.1408,
|
| 688 |
+
13.9359
|
| 689 |
+
],
|
| 690 |
+
[
|
| 691 |
+
17.1408,
|
| 692 |
+
15.5359
|
| 693 |
+
],
|
| 694 |
+
[
|
| 695 |
+
17.1408,
|
| 696 |
+
17.1359
|
| 697 |
+
],
|
| 698 |
+
[
|
| 699 |
+
17.1408,
|
| 700 |
+
18.7359
|
| 701 |
+
],
|
| 702 |
+
[
|
| 703 |
+
17.1408,
|
| 704 |
+
21.9359
|
| 705 |
+
],
|
| 706 |
+
[
|
| 707 |
+
17.1408,
|
| 708 |
+
23.5359
|
| 709 |
+
],
|
| 710 |
+
[
|
| 711 |
+
17.1408,
|
| 712 |
+
25.1359
|
| 713 |
+
],
|
| 714 |
+
[
|
| 715 |
+
17.1408,
|
| 716 |
+
26.7359
|
| 717 |
+
],
|
| 718 |
+
[
|
| 719 |
+
17.1408,
|
| 720 |
+
28.3359
|
| 721 |
+
],
|
| 722 |
+
[
|
| 723 |
+
17.1408,
|
| 724 |
+
29.9359
|
| 725 |
+
],
|
| 726 |
+
[
|
| 727 |
+
17.1408,
|
| 728 |
+
39.5359
|
| 729 |
+
],
|
| 730 |
+
[
|
| 731 |
+
17.1408,
|
| 732 |
+
41.1359
|
| 733 |
+
],
|
| 734 |
+
[
|
| 735 |
+
17.1408,
|
| 736 |
+
42.7359
|
| 737 |
+
],
|
| 738 |
+
[
|
| 739 |
+
17.1408,
|
| 740 |
+
47.5359
|
| 741 |
+
],
|
| 742 |
+
[
|
| 743 |
+
17.1408,
|
| 744 |
+
49.1359
|
| 745 |
+
],
|
| 746 |
+
[
|
| 747 |
+
17.1408,
|
| 748 |
+
50.7359
|
| 749 |
+
],
|
| 750 |
+
[
|
| 751 |
+
17.1408,
|
| 752 |
+
52.3359
|
| 753 |
+
],
|
| 754 |
+
[
|
| 755 |
+
17.1408,
|
| 756 |
+
58.7359
|
| 757 |
+
],
|
| 758 |
+
[
|
| 759 |
+
17.1408,
|
| 760 |
+
60.3359
|
| 761 |
+
],
|
| 762 |
+
[
|
| 763 |
+
17.1408,
|
| 764 |
+
61.9359
|
| 765 |
+
],
|
| 766 |
+
[
|
| 767 |
+
17.1408,
|
| 768 |
+
63.5359
|
| 769 |
+
],
|
| 770 |
+
[
|
| 771 |
+
17.1408,
|
| 772 |
+
65.1359
|
| 773 |
+
],
|
| 774 |
+
[
|
| 775 |
+
17.1408,
|
| 776 |
+
66.7359
|
| 777 |
+
],
|
| 778 |
+
[
|
| 779 |
+
17.1408,
|
| 780 |
+
68.3359
|
| 781 |
+
],
|
| 782 |
+
[
|
| 783 |
+
17.1408,
|
| 784 |
+
69.9359
|
| 785 |
+
],
|
| 786 |
+
[
|
| 787 |
+
17.1408,
|
| 788 |
+
71.5359
|
| 789 |
+
],
|
| 790 |
+
[
|
| 791 |
+
17.1408,
|
| 792 |
+
73.1359
|
| 793 |
+
],
|
| 794 |
+
[
|
| 795 |
+
17.1408,
|
| 796 |
+
74.7359
|
| 797 |
+
],
|
| 798 |
+
[
|
| 799 |
+
18.7408,
|
| 800 |
+
5.9359
|
| 801 |
+
],
|
| 802 |
+
[
|
| 803 |
+
18.7408,
|
| 804 |
+
7.5359
|
| 805 |
+
],
|
| 806 |
+
[
|
| 807 |
+
18.7408,
|
| 808 |
+
9.1359
|
| 809 |
+
],
|
| 810 |
+
[
|
| 811 |
+
18.7408,
|
| 812 |
+
10.7359
|
| 813 |
+
],
|
| 814 |
+
[
|
| 815 |
+
18.7408,
|
| 816 |
+
12.3359
|
| 817 |
+
],
|
| 818 |
+
[
|
| 819 |
+
18.7408,
|
| 820 |
+
13.9359
|
| 821 |
+
],
|
| 822 |
+
[
|
| 823 |
+
18.7408,
|
| 824 |
+
15.5359
|
| 825 |
+
],
|
| 826 |
+
[
|
| 827 |
+
18.7408,
|
| 828 |
+
17.1359
|
| 829 |
+
],
|
| 830 |
+
[
|
| 831 |
+
18.7408,
|
| 832 |
+
23.5359
|
| 833 |
+
],
|
| 834 |
+
[
|
| 835 |
+
18.7408,
|
| 836 |
+
25.1359
|
| 837 |
+
],
|
| 838 |
+
[
|
| 839 |
+
18.7408,
|
| 840 |
+
26.7359
|
| 841 |
+
],
|
| 842 |
+
[
|
| 843 |
+
18.7408,
|
| 844 |
+
28.3359
|
| 845 |
+
],
|
| 846 |
+
[
|
| 847 |
+
18.7408,
|
| 848 |
+
29.9359
|
| 849 |
+
],
|
| 850 |
+
[
|
| 851 |
+
18.7408,
|
| 852 |
+
37.9359
|
| 853 |
+
],
|
| 854 |
+
[
|
| 855 |
+
18.7408,
|
| 856 |
+
39.5359
|
| 857 |
+
],
|
| 858 |
+
[
|
| 859 |
+
18.7408,
|
| 860 |
+
41.1359
|
| 861 |
+
],
|
| 862 |
+
[
|
| 863 |
+
18.7408,
|
| 864 |
+
49.1359
|
| 865 |
+
],
|
| 866 |
+
[
|
| 867 |
+
18.7408,
|
| 868 |
+
50.7359
|
| 869 |
+
],
|
| 870 |
+
[
|
| 871 |
+
18.7408,
|
| 872 |
+
52.3359
|
| 873 |
+
],
|
| 874 |
+
[
|
| 875 |
+
18.7408,
|
| 876 |
+
53.9359
|
| 877 |
+
],
|
| 878 |
+
[
|
| 879 |
+
18.7408,
|
| 880 |
+
58.7359
|
| 881 |
+
],
|
| 882 |
+
[
|
| 883 |
+
18.7408,
|
| 884 |
+
60.3359
|
| 885 |
+
],
|
| 886 |
+
[
|
| 887 |
+
18.7408,
|
| 888 |
+
61.9359
|
| 889 |
+
],
|
| 890 |
+
[
|
| 891 |
+
18.7408,
|
| 892 |
+
63.5359
|
| 893 |
+
],
|
| 894 |
+
[
|
| 895 |
+
18.7408,
|
| 896 |
+
65.1359
|
| 897 |
+
],
|
| 898 |
+
[
|
| 899 |
+
18.7408,
|
| 900 |
+
66.7359
|
| 901 |
+
],
|
| 902 |
+
[
|
| 903 |
+
18.7408,
|
| 904 |
+
68.3359
|
| 905 |
+
],
|
| 906 |
+
[
|
| 907 |
+
18.7408,
|
| 908 |
+
69.9359
|
| 909 |
+
],
|
| 910 |
+
[
|
| 911 |
+
18.7408,
|
| 912 |
+
71.5359
|
| 913 |
+
],
|
| 914 |
+
[
|
| 915 |
+
18.7408,
|
| 916 |
+
73.1359
|
| 917 |
+
],
|
| 918 |
+
[
|
| 919 |
+
18.7408,
|
| 920 |
+
74.7359
|
| 921 |
+
],
|
| 922 |
+
[
|
| 923 |
+
20.3408,
|
| 924 |
+
5.9359
|
| 925 |
+
],
|
| 926 |
+
[
|
| 927 |
+
20.3408,
|
| 928 |
+
7.5359
|
| 929 |
+
],
|
| 930 |
+
[
|
| 931 |
+
20.3408,
|
| 932 |
+
9.1359
|
| 933 |
+
],
|
| 934 |
+
[
|
| 935 |
+
20.3408,
|
| 936 |
+
10.7359
|
| 937 |
+
],
|
| 938 |
+
[
|
| 939 |
+
20.3408,
|
| 940 |
+
12.3359
|
| 941 |
+
],
|
| 942 |
+
[
|
| 943 |
+
20.3408,
|
| 944 |
+
13.9359
|
| 945 |
+
],
|
| 946 |
+
[
|
| 947 |
+
20.3408,
|
| 948 |
+
15.5359
|
| 949 |
+
],
|
| 950 |
+
[
|
| 951 |
+
20.3408,
|
| 952 |
+
17.1359
|
| 953 |
+
],
|
| 954 |
+
[
|
| 955 |
+
20.3408,
|
| 956 |
+
23.5359
|
| 957 |
+
],
|
| 958 |
+
[
|
| 959 |
+
20.3408,
|
| 960 |
+
25.1359
|
| 961 |
+
],
|
| 962 |
+
[
|
| 963 |
+
20.3408,
|
| 964 |
+
26.7359
|
| 965 |
+
],
|
| 966 |
+
[
|
| 967 |
+
20.3408,
|
| 968 |
+
29.9359
|
| 969 |
+
],
|
| 970 |
+
[
|
| 971 |
+
20.3408,
|
| 972 |
+
31.5359
|
| 973 |
+
],
|
| 974 |
+
[
|
| 975 |
+
20.3408,
|
| 976 |
+
37.9359
|
| 977 |
+
],
|
| 978 |
+
[
|
| 979 |
+
20.3408,
|
| 980 |
+
39.5359
|
| 981 |
+
],
|
| 982 |
+
[
|
| 983 |
+
20.3408,
|
| 984 |
+
41.1359
|
| 985 |
+
],
|
| 986 |
+
[
|
| 987 |
+
20.3408,
|
| 988 |
+
49.1359
|
| 989 |
+
],
|
| 990 |
+
[
|
| 991 |
+
20.3408,
|
| 992 |
+
52.3359
|
| 993 |
+
],
|
| 994 |
+
[
|
| 995 |
+
20.3408,
|
| 996 |
+
53.9359
|
| 997 |
+
],
|
| 998 |
+
[
|
| 999 |
+
20.3408,
|
| 1000 |
+
55.5359
|
| 1001 |
+
],
|
| 1002 |
+
[
|
| 1003 |
+
20.3408,
|
| 1004 |
+
58.7359
|
| 1005 |
+
],
|
| 1006 |
+
[
|
| 1007 |
+
20.3408,
|
| 1008 |
+
60.3359
|
| 1009 |
+
],
|
| 1010 |
+
[
|
| 1011 |
+
20.3408,
|
| 1012 |
+
61.9359
|
| 1013 |
+
],
|
| 1014 |
+
[
|
| 1015 |
+
20.3408,
|
| 1016 |
+
63.5359
|
| 1017 |
+
],
|
| 1018 |
+
[
|
| 1019 |
+
20.3408,
|
| 1020 |
+
65.1359
|
| 1021 |
+
],
|
| 1022 |
+
[
|
| 1023 |
+
20.3408,
|
| 1024 |
+
66.7359
|
| 1025 |
+
],
|
| 1026 |
+
[
|
| 1027 |
+
20.3408,
|
| 1028 |
+
68.3359
|
| 1029 |
+
],
|
| 1030 |
+
[
|
| 1031 |
+
20.3408,
|
| 1032 |
+
69.9359
|
| 1033 |
+
],
|
| 1034 |
+
[
|
| 1035 |
+
20.3408,
|
| 1036 |
+
71.5359
|
| 1037 |
+
],
|
| 1038 |
+
[
|
| 1039 |
+
20.3408,
|
| 1040 |
+
73.1359
|
| 1041 |
+
],
|
| 1042 |
+
[
|
| 1043 |
+
20.3408,
|
| 1044 |
+
74.7359
|
| 1045 |
+
],
|
| 1046 |
+
[
|
| 1047 |
+
21.9408,
|
| 1048 |
+
5.9359
|
| 1049 |
+
],
|
| 1050 |
+
[
|
| 1051 |
+
21.9408,
|
| 1052 |
+
7.5359
|
| 1053 |
+
],
|
| 1054 |
+
[
|
| 1055 |
+
21.9408,
|
| 1056 |
+
9.1359
|
| 1057 |
+
],
|
| 1058 |
+
[
|
| 1059 |
+
21.9408,
|
| 1060 |
+
10.7359
|
| 1061 |
+
],
|
| 1062 |
+
[
|
| 1063 |
+
21.9408,
|
| 1064 |
+
12.3359
|
| 1065 |
+
],
|
| 1066 |
+
[
|
| 1067 |
+
21.9408,
|
| 1068 |
+
13.9359
|
| 1069 |
+
],
|
| 1070 |
+
[
|
| 1071 |
+
21.9408,
|
| 1072 |
+
15.5359
|
| 1073 |
+
],
|
| 1074 |
+
[
|
| 1075 |
+
21.9408,
|
| 1076 |
+
17.1359
|
| 1077 |
+
],
|
| 1078 |
+
[
|
| 1079 |
+
21.9408,
|
| 1080 |
+
25.1359
|
| 1081 |
+
],
|
| 1082 |
+
[
|
| 1083 |
+
21.9408,
|
| 1084 |
+
29.9359
|
| 1085 |
+
],
|
| 1086 |
+
[
|
| 1087 |
+
21.9408,
|
| 1088 |
+
31.5359
|
| 1089 |
+
],
|
| 1090 |
+
[
|
| 1091 |
+
21.9408,
|
| 1092 |
+
39.5359
|
| 1093 |
+
],
|
| 1094 |
+
[
|
| 1095 |
+
21.9408,
|
| 1096 |
+
47.5359
|
| 1097 |
+
],
|
| 1098 |
+
[
|
| 1099 |
+
21.9408,
|
| 1100 |
+
49.1359
|
| 1101 |
+
],
|
| 1102 |
+
[
|
| 1103 |
+
21.9408,
|
| 1104 |
+
53.9359
|
| 1105 |
+
],
|
| 1106 |
+
[
|
| 1107 |
+
21.9408,
|
| 1108 |
+
55.5359
|
| 1109 |
+
],
|
| 1110 |
+
[
|
| 1111 |
+
21.9408,
|
| 1112 |
+
57.1359
|
| 1113 |
+
],
|
| 1114 |
+
[
|
| 1115 |
+
21.9408,
|
| 1116 |
+
58.7359
|
| 1117 |
+
],
|
| 1118 |
+
[
|
| 1119 |
+
21.9408,
|
| 1120 |
+
60.3359
|
| 1121 |
+
],
|
| 1122 |
+
[
|
| 1123 |
+
21.9408,
|
| 1124 |
+
61.9359
|
| 1125 |
+
],
|
| 1126 |
+
[
|
| 1127 |
+
21.9408,
|
| 1128 |
+
63.5359
|
| 1129 |
+
],
|
| 1130 |
+
[
|
| 1131 |
+
21.9408,
|
| 1132 |
+
65.1359
|
| 1133 |
+
],
|
| 1134 |
+
[
|
| 1135 |
+
21.9408,
|
| 1136 |
+
66.7359
|
| 1137 |
+
],
|
| 1138 |
+
[
|
| 1139 |
+
21.9408,
|
| 1140 |
+
68.3359
|
| 1141 |
+
],
|
| 1142 |
+
[
|
| 1143 |
+
21.9408,
|
| 1144 |
+
69.9359
|
| 1145 |
+
],
|
| 1146 |
+
[
|
| 1147 |
+
21.9408,
|
| 1148 |
+
71.5359
|
| 1149 |
+
],
|
| 1150 |
+
[
|
| 1151 |
+
21.9408,
|
| 1152 |
+
73.1359
|
| 1153 |
+
],
|
| 1154 |
+
[
|
| 1155 |
+
21.9408,
|
| 1156 |
+
74.7359
|
| 1157 |
+
],
|
| 1158 |
+
[
|
| 1159 |
+
23.5408,
|
| 1160 |
+
5.9359
|
| 1161 |
+
],
|
| 1162 |
+
[
|
| 1163 |
+
23.5408,
|
| 1164 |
+
7.5359
|
| 1165 |
+
],
|
| 1166 |
+
[
|
| 1167 |
+
23.5408,
|
| 1168 |
+
9.1359
|
| 1169 |
+
],
|
| 1170 |
+
[
|
| 1171 |
+
23.5408,
|
| 1172 |
+
10.7359
|
| 1173 |
+
],
|
| 1174 |
+
[
|
| 1175 |
+
23.5408,
|
| 1176 |
+
12.3359
|
| 1177 |
+
],
|
| 1178 |
+
[
|
| 1179 |
+
23.5408,
|
| 1180 |
+
13.9359
|
| 1181 |
+
],
|
| 1182 |
+
[
|
| 1183 |
+
23.5408,
|
| 1184 |
+
15.5359
|
| 1185 |
+
],
|
| 1186 |
+
[
|
| 1187 |
+
23.5408,
|
| 1188 |
+
17.1359
|
| 1189 |
+
],
|
| 1190 |
+
[
|
| 1191 |
+
23.5408,
|
| 1192 |
+
31.5359
|
| 1193 |
+
],
|
| 1194 |
+
[
|
| 1195 |
+
23.5408,
|
| 1196 |
+
42.7359
|
| 1197 |
+
],
|
| 1198 |
+
[
|
| 1199 |
+
23.5408,
|
| 1200 |
+
45.9359
|
| 1201 |
+
],
|
| 1202 |
+
[
|
| 1203 |
+
23.5408,
|
| 1204 |
+
47.5359
|
| 1205 |
+
],
|
| 1206 |
+
[
|
| 1207 |
+
23.5408,
|
| 1208 |
+
57.1359
|
| 1209 |
+
],
|
| 1210 |
+
[
|
| 1211 |
+
23.5408,
|
| 1212 |
+
58.7359
|
| 1213 |
+
],
|
| 1214 |
+
[
|
| 1215 |
+
23.5408,
|
| 1216 |
+
60.3359
|
| 1217 |
+
],
|
| 1218 |
+
[
|
| 1219 |
+
23.5408,
|
| 1220 |
+
61.9359
|
| 1221 |
+
],
|
| 1222 |
+
[
|
| 1223 |
+
23.5408,
|
| 1224 |
+
63.5359
|
| 1225 |
+
],
|
| 1226 |
+
[
|
| 1227 |
+
23.5408,
|
| 1228 |
+
65.1359
|
| 1229 |
+
],
|
| 1230 |
+
[
|
| 1231 |
+
23.5408,
|
| 1232 |
+
66.7359
|
| 1233 |
+
],
|
| 1234 |
+
[
|
| 1235 |
+
23.5408,
|
| 1236 |
+
68.3359
|
| 1237 |
+
],
|
| 1238 |
+
[
|
| 1239 |
+
23.5408,
|
| 1240 |
+
69.9359
|
| 1241 |
+
],
|
| 1242 |
+
[
|
| 1243 |
+
23.5408,
|
| 1244 |
+
71.5359
|
| 1245 |
+
],
|
| 1246 |
+
[
|
| 1247 |
+
23.5408,
|
| 1248 |
+
73.1359
|
| 1249 |
+
],
|
| 1250 |
+
[
|
| 1251 |
+
25.1408,
|
| 1252 |
+
7.5359
|
| 1253 |
+
],
|
| 1254 |
+
[
|
| 1255 |
+
25.1408,
|
| 1256 |
+
9.1359
|
| 1257 |
+
],
|
| 1258 |
+
[
|
| 1259 |
+
25.1408,
|
| 1260 |
+
10.7359
|
| 1261 |
+
],
|
| 1262 |
+
[
|
| 1263 |
+
25.1408,
|
| 1264 |
+
12.3359
|
| 1265 |
+
],
|
| 1266 |
+
[
|
| 1267 |
+
25.1408,
|
| 1268 |
+
13.9359
|
| 1269 |
+
],
|
| 1270 |
+
[
|
| 1271 |
+
25.1408,
|
| 1272 |
+
15.5359
|
| 1273 |
+
],
|
| 1274 |
+
[
|
| 1275 |
+
25.1408,
|
| 1276 |
+
17.1359
|
| 1277 |
+
],
|
| 1278 |
+
[
|
| 1279 |
+
25.1408,
|
| 1280 |
+
31.5359
|
| 1281 |
+
],
|
| 1282 |
+
[
|
| 1283 |
+
25.1408,
|
| 1284 |
+
33.1359
|
| 1285 |
+
],
|
| 1286 |
+
[
|
| 1287 |
+
25.1408,
|
| 1288 |
+
42.7359
|
| 1289 |
+
],
|
| 1290 |
+
[
|
| 1291 |
+
25.1408,
|
| 1292 |
+
45.9359
|
| 1293 |
+
],
|
| 1294 |
+
[
|
| 1295 |
+
25.1408,
|
| 1296 |
+
57.1359
|
| 1297 |
+
],
|
| 1298 |
+
[
|
| 1299 |
+
25.1408,
|
| 1300 |
+
58.7359
|
| 1301 |
+
],
|
| 1302 |
+
[
|
| 1303 |
+
25.1408,
|
| 1304 |
+
60.3359
|
| 1305 |
+
],
|
| 1306 |
+
[
|
| 1307 |
+
25.1408,
|
| 1308 |
+
61.9359
|
| 1309 |
+
],
|
| 1310 |
+
[
|
| 1311 |
+
25.1408,
|
| 1312 |
+
63.5359
|
| 1313 |
+
],
|
| 1314 |
+
[
|
| 1315 |
+
25.1408,
|
| 1316 |
+
65.1359
|
| 1317 |
+
],
|
| 1318 |
+
[
|
| 1319 |
+
25.1408,
|
| 1320 |
+
66.7359
|
| 1321 |
+
],
|
| 1322 |
+
[
|
| 1323 |
+
25.1408,
|
| 1324 |
+
68.3359
|
| 1325 |
+
],
|
| 1326 |
+
[
|
| 1327 |
+
25.1408,
|
| 1328 |
+
69.9359
|
| 1329 |
+
],
|
| 1330 |
+
[
|
| 1331 |
+
25.1408,
|
| 1332 |
+
71.5359
|
| 1333 |
+
],
|
| 1334 |
+
[
|
| 1335 |
+
25.1408,
|
| 1336 |
+
73.1359
|
| 1337 |
+
],
|
| 1338 |
+
[
|
| 1339 |
+
25.1408,
|
| 1340 |
+
74.7359
|
| 1341 |
+
],
|
| 1342 |
+
[
|
| 1343 |
+
26.7408,
|
| 1344 |
+
9.1359
|
| 1345 |
+
],
|
| 1346 |
+
[
|
| 1347 |
+
26.7408,
|
| 1348 |
+
10.7359
|
| 1349 |
+
],
|
| 1350 |
+
[
|
| 1351 |
+
26.7408,
|
| 1352 |
+
12.3359
|
| 1353 |
+
],
|
| 1354 |
+
[
|
| 1355 |
+
26.7408,
|
| 1356 |
+
13.9359
|
| 1357 |
+
],
|
| 1358 |
+
[
|
| 1359 |
+
26.7408,
|
| 1360 |
+
15.5359
|
| 1361 |
+
],
|
| 1362 |
+
[
|
| 1363 |
+
26.7408,
|
| 1364 |
+
17.1359
|
| 1365 |
+
],
|
| 1366 |
+
[
|
| 1367 |
+
26.7408,
|
| 1368 |
+
18.7359
|
| 1369 |
+
],
|
| 1370 |
+
[
|
| 1371 |
+
26.7408,
|
| 1372 |
+
31.5359
|
| 1373 |
+
],
|
| 1374 |
+
[
|
| 1375 |
+
26.7408,
|
| 1376 |
+
33.1359
|
| 1377 |
+
],
|
| 1378 |
+
[
|
| 1379 |
+
26.7408,
|
| 1380 |
+
42.7359
|
| 1381 |
+
],
|
| 1382 |
+
[
|
| 1383 |
+
26.7408,
|
| 1384 |
+
44.3359
|
| 1385 |
+
],
|
| 1386 |
+
[
|
| 1387 |
+
26.7408,
|
| 1388 |
+
45.9359
|
| 1389 |
+
],
|
| 1390 |
+
[
|
| 1391 |
+
26.7408,
|
| 1392 |
+
58.7359
|
| 1393 |
+
],
|
| 1394 |
+
[
|
| 1395 |
+
26.7408,
|
| 1396 |
+
60.3359
|
| 1397 |
+
],
|
| 1398 |
+
[
|
| 1399 |
+
26.7408,
|
| 1400 |
+
61.9359
|
| 1401 |
+
],
|
| 1402 |
+
[
|
| 1403 |
+
26.7408,
|
| 1404 |
+
63.5359
|
| 1405 |
+
],
|
| 1406 |
+
[
|
| 1407 |
+
26.7408,
|
| 1408 |
+
65.1359
|
| 1409 |
+
],
|
| 1410 |
+
[
|
| 1411 |
+
26.7408,
|
| 1412 |
+
66.7359
|
| 1413 |
+
],
|
| 1414 |
+
[
|
| 1415 |
+
26.7408,
|
| 1416 |
+
68.3359
|
| 1417 |
+
],
|
| 1418 |
+
[
|
| 1419 |
+
26.7408,
|
| 1420 |
+
69.9359
|
| 1421 |
+
],
|
| 1422 |
+
[
|
| 1423 |
+
26.7408,
|
| 1424 |
+
71.5359
|
| 1425 |
+
],
|
| 1426 |
+
[
|
| 1427 |
+
26.7408,
|
| 1428 |
+
73.1359
|
| 1429 |
+
],
|
| 1430 |
+
[
|
| 1431 |
+
26.7408,
|
| 1432 |
+
74.7359
|
| 1433 |
+
],
|
| 1434 |
+
[
|
| 1435 |
+
26.7408,
|
| 1436 |
+
76.3359
|
| 1437 |
+
],
|
| 1438 |
+
[
|
| 1439 |
+
28.3408,
|
| 1440 |
+
10.7359
|
| 1441 |
+
],
|
| 1442 |
+
[
|
| 1443 |
+
28.3408,
|
| 1444 |
+
12.3359
|
| 1445 |
+
],
|
| 1446 |
+
[
|
| 1447 |
+
28.3408,
|
| 1448 |
+
13.9359
|
| 1449 |
+
],
|
| 1450 |
+
[
|
| 1451 |
+
28.3408,
|
| 1452 |
+
15.5359
|
| 1453 |
+
],
|
| 1454 |
+
[
|
| 1455 |
+
28.3408,
|
| 1456 |
+
17.1359
|
| 1457 |
+
],
|
| 1458 |
+
[
|
| 1459 |
+
28.3408,
|
| 1460 |
+
18.7359
|
| 1461 |
+
],
|
| 1462 |
+
[
|
| 1463 |
+
28.3408,
|
| 1464 |
+
20.3359
|
| 1465 |
+
],
|
| 1466 |
+
[
|
| 1467 |
+
28.3408,
|
| 1468 |
+
31.5359
|
| 1469 |
+
],
|
| 1470 |
+
[
|
| 1471 |
+
28.3408,
|
| 1472 |
+
33.1359
|
| 1473 |
+
],
|
| 1474 |
+
[
|
| 1475 |
+
28.3408,
|
| 1476 |
+
42.7359
|
| 1477 |
+
],
|
| 1478 |
+
[
|
| 1479 |
+
28.3408,
|
| 1480 |
+
44.3359
|
| 1481 |
+
],
|
| 1482 |
+
[
|
| 1483 |
+
28.3408,
|
| 1484 |
+
45.9359
|
| 1485 |
+
],
|
| 1486 |
+
[
|
| 1487 |
+
28.3408,
|
| 1488 |
+
47.5359
|
| 1489 |
+
],
|
| 1490 |
+
[
|
| 1491 |
+
28.3408,
|
| 1492 |
+
60.3359
|
| 1493 |
+
],
|
| 1494 |
+
[
|
| 1495 |
+
28.3408,
|
| 1496 |
+
61.9359
|
| 1497 |
+
],
|
| 1498 |
+
[
|
| 1499 |
+
28.3408,
|
| 1500 |
+
63.5359
|
| 1501 |
+
],
|
| 1502 |
+
[
|
| 1503 |
+
28.3408,
|
| 1504 |
+
65.1359
|
| 1505 |
+
],
|
| 1506 |
+
[
|
| 1507 |
+
28.3408,
|
| 1508 |
+
66.7359
|
| 1509 |
+
],
|
| 1510 |
+
[
|
| 1511 |
+
28.3408,
|
| 1512 |
+
68.3359
|
| 1513 |
+
],
|
| 1514 |
+
[
|
| 1515 |
+
28.3408,
|
| 1516 |
+
69.9359
|
| 1517 |
+
],
|
| 1518 |
+
[
|
| 1519 |
+
28.3408,
|
| 1520 |
+
71.5359
|
| 1521 |
+
],
|
| 1522 |
+
[
|
| 1523 |
+
28.3408,
|
| 1524 |
+
73.1359
|
| 1525 |
+
],
|
| 1526 |
+
[
|
| 1527 |
+
28.3408,
|
| 1528 |
+
74.7359
|
| 1529 |
+
],
|
| 1530 |
+
[
|
| 1531 |
+
28.3408,
|
| 1532 |
+
76.3359
|
| 1533 |
+
],
|
| 1534 |
+
[
|
| 1535 |
+
29.9408,
|
| 1536 |
+
10.7359
|
| 1537 |
+
],
|
| 1538 |
+
[
|
| 1539 |
+
29.9408,
|
| 1540 |
+
12.3359
|
| 1541 |
+
],
|
| 1542 |
+
[
|
| 1543 |
+
29.9408,
|
| 1544 |
+
13.9359
|
| 1545 |
+
],
|
| 1546 |
+
[
|
| 1547 |
+
29.9408,
|
| 1548 |
+
15.5359
|
| 1549 |
+
],
|
| 1550 |
+
[
|
| 1551 |
+
29.9408,
|
| 1552 |
+
18.7359
|
| 1553 |
+
],
|
| 1554 |
+
[
|
| 1555 |
+
29.9408,
|
| 1556 |
+
20.3359
|
| 1557 |
+
],
|
| 1558 |
+
[
|
| 1559 |
+
29.9408,
|
| 1560 |
+
21.9359
|
| 1561 |
+
],
|
| 1562 |
+
[
|
| 1563 |
+
29.9408,
|
| 1564 |
+
23.5359
|
| 1565 |
+
],
|
| 1566 |
+
[
|
| 1567 |
+
29.9408,
|
| 1568 |
+
25.1359
|
| 1569 |
+
],
|
| 1570 |
+
[
|
| 1571 |
+
29.9408,
|
| 1572 |
+
26.7359
|
| 1573 |
+
],
|
| 1574 |
+
[
|
| 1575 |
+
29.9408,
|
| 1576 |
+
33.1359
|
| 1577 |
+
],
|
| 1578 |
+
[
|
| 1579 |
+
29.9408,
|
| 1580 |
+
34.7359
|
| 1581 |
+
],
|
| 1582 |
+
[
|
| 1583 |
+
29.9408,
|
| 1584 |
+
42.7359
|
| 1585 |
+
],
|
| 1586 |
+
[
|
| 1587 |
+
29.9408,
|
| 1588 |
+
44.3359
|
| 1589 |
+
],
|
| 1590 |
+
[
|
| 1591 |
+
29.9408,
|
| 1592 |
+
47.5359
|
| 1593 |
+
],
|
| 1594 |
+
[
|
| 1595 |
+
29.9408,
|
| 1596 |
+
49.1359
|
| 1597 |
+
],
|
| 1598 |
+
[
|
| 1599 |
+
29.9408,
|
| 1600 |
+
60.3359
|
| 1601 |
+
],
|
| 1602 |
+
[
|
| 1603 |
+
29.9408,
|
| 1604 |
+
61.9359
|
| 1605 |
+
],
|
| 1606 |
+
[
|
| 1607 |
+
29.9408,
|
| 1608 |
+
63.5359
|
| 1609 |
+
],
|
| 1610 |
+
[
|
| 1611 |
+
29.9408,
|
| 1612 |
+
65.1359
|
| 1613 |
+
],
|
| 1614 |
+
[
|
| 1615 |
+
29.9408,
|
| 1616 |
+
66.7359
|
| 1617 |
+
],
|
| 1618 |
+
[
|
| 1619 |
+
29.9408,
|
| 1620 |
+
68.3359
|
| 1621 |
+
],
|
| 1622 |
+
[
|
| 1623 |
+
29.9408,
|
| 1624 |
+
69.9359
|
| 1625 |
+
],
|
| 1626 |
+
[
|
| 1627 |
+
29.9408,
|
| 1628 |
+
71.5359
|
| 1629 |
+
],
|
| 1630 |
+
[
|
| 1631 |
+
29.9408,
|
| 1632 |
+
73.1359
|
| 1633 |
+
],
|
| 1634 |
+
[
|
| 1635 |
+
29.9408,
|
| 1636 |
+
74.7359
|
| 1637 |
+
],
|
| 1638 |
+
[
|
| 1639 |
+
29.9408,
|
| 1640 |
+
76.3359
|
| 1641 |
+
],
|
| 1642 |
+
[
|
| 1643 |
+
29.9408,
|
| 1644 |
+
77.9359
|
| 1645 |
+
],
|
| 1646 |
+
[
|
| 1647 |
+
31.5408,
|
| 1648 |
+
10.7359
|
| 1649 |
+
],
|
| 1650 |
+
[
|
| 1651 |
+
31.5408,
|
| 1652 |
+
12.3359
|
| 1653 |
+
],
|
| 1654 |
+
[
|
| 1655 |
+
31.5408,
|
| 1656 |
+
13.9359
|
| 1657 |
+
],
|
| 1658 |
+
[
|
| 1659 |
+
31.5408,
|
| 1660 |
+
21.9359
|
| 1661 |
+
],
|
| 1662 |
+
[
|
| 1663 |
+
31.5408,
|
| 1664 |
+
23.5359
|
| 1665 |
+
],
|
| 1666 |
+
[
|
| 1667 |
+
31.5408,
|
| 1668 |
+
25.1359
|
| 1669 |
+
],
|
| 1670 |
+
[
|
| 1671 |
+
31.5408,
|
| 1672 |
+
26.7359
|
| 1673 |
+
],
|
| 1674 |
+
[
|
| 1675 |
+
31.5408,
|
| 1676 |
+
33.1359
|
| 1677 |
+
],
|
| 1678 |
+
[
|
| 1679 |
+
31.5408,
|
| 1680 |
+
34.7359
|
| 1681 |
+
],
|
| 1682 |
+
[
|
| 1683 |
+
31.5408,
|
| 1684 |
+
42.7359
|
| 1685 |
+
],
|
| 1686 |
+
[
|
| 1687 |
+
31.5408,
|
| 1688 |
+
49.1359
|
| 1689 |
+
],
|
| 1690 |
+
[
|
| 1691 |
+
31.5408,
|
| 1692 |
+
50.7359
|
| 1693 |
+
],
|
| 1694 |
+
[
|
| 1695 |
+
31.5408,
|
| 1696 |
+
57.1359
|
| 1697 |
+
],
|
| 1698 |
+
[
|
| 1699 |
+
31.5408,
|
| 1700 |
+
58.7359
|
| 1701 |
+
],
|
| 1702 |
+
[
|
| 1703 |
+
31.5408,
|
| 1704 |
+
60.3359
|
| 1705 |
+
],
|
| 1706 |
+
[
|
| 1707 |
+
31.5408,
|
| 1708 |
+
61.9359
|
| 1709 |
+
],
|
| 1710 |
+
[
|
| 1711 |
+
31.5408,
|
| 1712 |
+
63.5359
|
| 1713 |
+
],
|
| 1714 |
+
[
|
| 1715 |
+
31.5408,
|
| 1716 |
+
65.1359
|
| 1717 |
+
],
|
| 1718 |
+
[
|
| 1719 |
+
31.5408,
|
| 1720 |
+
66.7359
|
| 1721 |
+
],
|
| 1722 |
+
[
|
| 1723 |
+
31.5408,
|
| 1724 |
+
68.3359
|
| 1725 |
+
],
|
| 1726 |
+
[
|
| 1727 |
+
31.5408,
|
| 1728 |
+
69.9359
|
| 1729 |
+
],
|
| 1730 |
+
[
|
| 1731 |
+
31.5408,
|
| 1732 |
+
71.5359
|
| 1733 |
+
],
|
| 1734 |
+
[
|
| 1735 |
+
31.5408,
|
| 1736 |
+
73.1359
|
| 1737 |
+
],
|
| 1738 |
+
[
|
| 1739 |
+
31.5408,
|
| 1740 |
+
74.7359
|
| 1741 |
+
],
|
| 1742 |
+
[
|
| 1743 |
+
31.5408,
|
| 1744 |
+
76.3359
|
| 1745 |
+
],
|
| 1746 |
+
[
|
| 1747 |
+
33.1408,
|
| 1748 |
+
10.7359
|
| 1749 |
+
],
|
| 1750 |
+
[
|
| 1751 |
+
33.1408,
|
| 1752 |
+
12.3359
|
| 1753 |
+
],
|
| 1754 |
+
[
|
| 1755 |
+
33.1408,
|
| 1756 |
+
23.5359
|
| 1757 |
+
],
|
| 1758 |
+
[
|
| 1759 |
+
33.1408,
|
| 1760 |
+
25.1359
|
| 1761 |
+
],
|
| 1762 |
+
[
|
| 1763 |
+
33.1408,
|
| 1764 |
+
26.7359
|
| 1765 |
+
],
|
| 1766 |
+
[
|
| 1767 |
+
33.1408,
|
| 1768 |
+
28.3359
|
| 1769 |
+
],
|
| 1770 |
+
[
|
| 1771 |
+
33.1408,
|
| 1772 |
+
33.1359
|
| 1773 |
+
],
|
| 1774 |
+
[
|
| 1775 |
+
33.1408,
|
| 1776 |
+
34.7359
|
| 1777 |
+
],
|
| 1778 |
+
[
|
| 1779 |
+
33.1408,
|
| 1780 |
+
42.7359
|
| 1781 |
+
],
|
| 1782 |
+
[
|
| 1783 |
+
33.1408,
|
| 1784 |
+
47.5359
|
| 1785 |
+
],
|
| 1786 |
+
[
|
| 1787 |
+
33.1408,
|
| 1788 |
+
49.1359
|
| 1789 |
+
],
|
| 1790 |
+
[
|
| 1791 |
+
33.1408,
|
| 1792 |
+
50.7359
|
| 1793 |
+
],
|
| 1794 |
+
[
|
| 1795 |
+
33.1408,
|
| 1796 |
+
55.5359
|
| 1797 |
+
],
|
| 1798 |
+
[
|
| 1799 |
+
33.1408,
|
| 1800 |
+
57.1359
|
| 1801 |
+
],
|
| 1802 |
+
[
|
| 1803 |
+
33.1408,
|
| 1804 |
+
58.7359
|
| 1805 |
+
],
|
| 1806 |
+
[
|
| 1807 |
+
33.1408,
|
| 1808 |
+
60.3359
|
| 1809 |
+
],
|
| 1810 |
+
[
|
| 1811 |
+
33.1408,
|
| 1812 |
+
61.9359
|
| 1813 |
+
],
|
| 1814 |
+
[
|
| 1815 |
+
33.1408,
|
| 1816 |
+
63.5359
|
| 1817 |
+
],
|
| 1818 |
+
[
|
| 1819 |
+
33.1408,
|
| 1820 |
+
65.1359
|
| 1821 |
+
],
|
| 1822 |
+
[
|
| 1823 |
+
33.1408,
|
| 1824 |
+
66.7359
|
| 1825 |
+
],
|
| 1826 |
+
[
|
| 1827 |
+
33.1408,
|
| 1828 |
+
68.3359
|
| 1829 |
+
],
|
| 1830 |
+
[
|
| 1831 |
+
33.1408,
|
| 1832 |
+
69.9359
|
| 1833 |
+
],
|
| 1834 |
+
[
|
| 1835 |
+
33.1408,
|
| 1836 |
+
71.5359
|
| 1837 |
+
],
|
| 1838 |
+
[
|
| 1839 |
+
33.1408,
|
| 1840 |
+
73.1359
|
| 1841 |
+
],
|
| 1842 |
+
[
|
| 1843 |
+
33.1408,
|
| 1844 |
+
74.7359
|
| 1845 |
+
],
|
| 1846 |
+
[
|
| 1847 |
+
33.1408,
|
| 1848 |
+
76.3359
|
| 1849 |
+
],
|
| 1850 |
+
[
|
| 1851 |
+
34.7408,
|
| 1852 |
+
10.7359
|
| 1853 |
+
],
|
| 1854 |
+
[
|
| 1855 |
+
34.7408,
|
| 1856 |
+
12.3359
|
| 1857 |
+
],
|
| 1858 |
+
[
|
| 1859 |
+
34.7408,
|
| 1860 |
+
25.1359
|
| 1861 |
+
],
|
| 1862 |
+
[
|
| 1863 |
+
34.7408,
|
| 1864 |
+
26.7359
|
| 1865 |
+
],
|
| 1866 |
+
[
|
| 1867 |
+
34.7408,
|
| 1868 |
+
28.3359
|
| 1869 |
+
],
|
| 1870 |
+
[
|
| 1871 |
+
34.7408,
|
| 1872 |
+
33.1359
|
| 1873 |
+
],
|
| 1874 |
+
[
|
| 1875 |
+
34.7408,
|
| 1876 |
+
34.7359
|
| 1877 |
+
],
|
| 1878 |
+
[
|
| 1879 |
+
34.7408,
|
| 1880 |
+
42.7359
|
| 1881 |
+
],
|
| 1882 |
+
[
|
| 1883 |
+
34.7408,
|
| 1884 |
+
47.5359
|
| 1885 |
+
],
|
| 1886 |
+
[
|
| 1887 |
+
34.7408,
|
| 1888 |
+
49.1359
|
| 1889 |
+
],
|
| 1890 |
+
[
|
| 1891 |
+
34.7408,
|
| 1892 |
+
50.7359
|
| 1893 |
+
],
|
| 1894 |
+
[
|
| 1895 |
+
34.7408,
|
| 1896 |
+
52.3359
|
| 1897 |
+
],
|
| 1898 |
+
[
|
| 1899 |
+
34.7408,
|
| 1900 |
+
53.9359
|
| 1901 |
+
],
|
| 1902 |
+
[
|
| 1903 |
+
34.7408,
|
| 1904 |
+
55.5359
|
| 1905 |
+
],
|
| 1906 |
+
[
|
| 1907 |
+
34.7408,
|
| 1908 |
+
57.1359
|
| 1909 |
+
],
|
| 1910 |
+
[
|
| 1911 |
+
34.7408,
|
| 1912 |
+
61.9359
|
| 1913 |
+
],
|
| 1914 |
+
[
|
| 1915 |
+
34.7408,
|
| 1916 |
+
66.7359
|
| 1917 |
+
],
|
| 1918 |
+
[
|
| 1919 |
+
34.7408,
|
| 1920 |
+
68.3359
|
| 1921 |
+
],
|
| 1922 |
+
[
|
| 1923 |
+
34.7408,
|
| 1924 |
+
73.1359
|
| 1925 |
+
],
|
| 1926 |
+
[
|
| 1927 |
+
34.7408,
|
| 1928 |
+
74.7359
|
| 1929 |
+
],
|
| 1930 |
+
[
|
| 1931 |
+
34.7408,
|
| 1932 |
+
76.3359
|
| 1933 |
+
],
|
| 1934 |
+
[
|
| 1935 |
+
36.3408,
|
| 1936 |
+
10.7359
|
| 1937 |
+
],
|
| 1938 |
+
[
|
| 1939 |
+
36.3408,
|
| 1940 |
+
12.3359
|
| 1941 |
+
],
|
| 1942 |
+
[
|
| 1943 |
+
36.3408,
|
| 1944 |
+
25.1359
|
| 1945 |
+
],
|
| 1946 |
+
[
|
| 1947 |
+
36.3408,
|
| 1948 |
+
26.7359
|
| 1949 |
+
],
|
| 1950 |
+
[
|
| 1951 |
+
36.3408,
|
| 1952 |
+
33.1359
|
| 1953 |
+
],
|
| 1954 |
+
[
|
| 1955 |
+
36.3408,
|
| 1956 |
+
34.7359
|
| 1957 |
+
],
|
| 1958 |
+
[
|
| 1959 |
+
36.3408,
|
| 1960 |
+
36.3359
|
| 1961 |
+
],
|
| 1962 |
+
[
|
| 1963 |
+
36.3408,
|
| 1964 |
+
45.9359
|
| 1965 |
+
],
|
| 1966 |
+
[
|
| 1967 |
+
36.3408,
|
| 1968 |
+
47.5359
|
| 1969 |
+
],
|
| 1970 |
+
[
|
| 1971 |
+
36.3408,
|
| 1972 |
+
49.1359
|
| 1973 |
+
],
|
| 1974 |
+
[
|
| 1975 |
+
36.3408,
|
| 1976 |
+
50.7359
|
| 1977 |
+
],
|
| 1978 |
+
[
|
| 1979 |
+
36.3408,
|
| 1980 |
+
52.3359
|
| 1981 |
+
],
|
| 1982 |
+
[
|
| 1983 |
+
36.3408,
|
| 1984 |
+
53.9359
|
| 1985 |
+
],
|
| 1986 |
+
[
|
| 1987 |
+
36.3408,
|
| 1988 |
+
55.5359
|
| 1989 |
+
],
|
| 1990 |
+
[
|
| 1991 |
+
36.3408,
|
| 1992 |
+
68.3359
|
| 1993 |
+
],
|
| 1994 |
+
[
|
| 1995 |
+
36.3408,
|
| 1996 |
+
73.1359
|
| 1997 |
+
],
|
| 1998 |
+
[
|
| 1999 |
+
36.3408,
|
| 2000 |
+
74.7359
|
| 2001 |
+
],
|
| 2002 |
+
[
|
| 2003 |
+
36.3408,
|
| 2004 |
+
76.3359
|
| 2005 |
+
],
|
| 2006 |
+
[
|
| 2007 |
+
36.3408,
|
| 2008 |
+
77.9359
|
| 2009 |
+
],
|
| 2010 |
+
[
|
| 2011 |
+
37.9408,
|
| 2012 |
+
10.7359
|
| 2013 |
+
],
|
| 2014 |
+
[
|
| 2015 |
+
37.9408,
|
| 2016 |
+
12.3359
|
| 2017 |
+
],
|
| 2018 |
+
[
|
| 2019 |
+
37.9408,
|
| 2020 |
+
25.1359
|
| 2021 |
+
],
|
| 2022 |
+
[
|
| 2023 |
+
37.9408,
|
| 2024 |
+
34.7359
|
| 2025 |
+
],
|
| 2026 |
+
[
|
| 2027 |
+
37.9408,
|
| 2028 |
+
36.3359
|
| 2029 |
+
],
|
| 2030 |
+
[
|
| 2031 |
+
37.9408,
|
| 2032 |
+
45.9359
|
| 2033 |
+
],
|
| 2034 |
+
[
|
| 2035 |
+
37.9408,
|
| 2036 |
+
47.5359
|
| 2037 |
+
],
|
| 2038 |
+
[
|
| 2039 |
+
37.9408,
|
| 2040 |
+
49.1359
|
| 2041 |
+
],
|
| 2042 |
+
[
|
| 2043 |
+
37.9408,
|
| 2044 |
+
50.7359
|
| 2045 |
+
],
|
| 2046 |
+
[
|
| 2047 |
+
37.9408,
|
| 2048 |
+
52.3359
|
| 2049 |
+
],
|
| 2050 |
+
[
|
| 2051 |
+
37.9408,
|
| 2052 |
+
53.9359
|
| 2053 |
+
],
|
| 2054 |
+
[
|
| 2055 |
+
37.9408,
|
| 2056 |
+
74.7359
|
| 2057 |
+
],
|
| 2058 |
+
[
|
| 2059 |
+
37.9408,
|
| 2060 |
+
76.3359
|
| 2061 |
+
],
|
| 2062 |
+
[
|
| 2063 |
+
37.9408,
|
| 2064 |
+
77.9359
|
| 2065 |
+
],
|
| 2066 |
+
[
|
| 2067 |
+
39.5408,
|
| 2068 |
+
10.7359
|
| 2069 |
+
],
|
| 2070 |
+
[
|
| 2071 |
+
39.5408,
|
| 2072 |
+
12.3359
|
| 2073 |
+
],
|
| 2074 |
+
[
|
| 2075 |
+
39.5408,
|
| 2076 |
+
34.7359
|
| 2077 |
+
],
|
| 2078 |
+
[
|
| 2079 |
+
39.5408,
|
| 2080 |
+
36.3359
|
| 2081 |
+
],
|
| 2082 |
+
[
|
| 2083 |
+
39.5408,
|
| 2084 |
+
41.1359
|
| 2085 |
+
],
|
| 2086 |
+
[
|
| 2087 |
+
39.5408,
|
| 2088 |
+
45.9359
|
| 2089 |
+
],
|
| 2090 |
+
[
|
| 2091 |
+
39.5408,
|
| 2092 |
+
47.5359
|
| 2093 |
+
],
|
| 2094 |
+
[
|
| 2095 |
+
39.5408,
|
| 2096 |
+
49.1359
|
| 2097 |
+
],
|
| 2098 |
+
[
|
| 2099 |
+
39.5408,
|
| 2100 |
+
50.7359
|
| 2101 |
+
],
|
| 2102 |
+
[
|
| 2103 |
+
39.5408,
|
| 2104 |
+
52.3359
|
| 2105 |
+
],
|
| 2106 |
+
[
|
| 2107 |
+
39.5408,
|
| 2108 |
+
53.9359
|
| 2109 |
+
],
|
| 2110 |
+
[
|
| 2111 |
+
39.5408,
|
| 2112 |
+
76.3359
|
| 2113 |
+
],
|
| 2114 |
+
[
|
| 2115 |
+
39.5408,
|
| 2116 |
+
77.9359
|
| 2117 |
+
],
|
| 2118 |
+
[
|
| 2119 |
+
41.1408,
|
| 2120 |
+
10.7359
|
| 2121 |
+
],
|
| 2122 |
+
[
|
| 2123 |
+
41.1408,
|
| 2124 |
+
12.3359
|
| 2125 |
+
],
|
| 2126 |
+
[
|
| 2127 |
+
41.1408,
|
| 2128 |
+
34.7359
|
| 2129 |
+
],
|
| 2130 |
+
[
|
| 2131 |
+
41.1408,
|
| 2132 |
+
36.3359
|
| 2133 |
+
],
|
| 2134 |
+
[
|
| 2135 |
+
41.1408,
|
| 2136 |
+
37.9359
|
| 2137 |
+
],
|
| 2138 |
+
[
|
| 2139 |
+
41.1408,
|
| 2140 |
+
39.5359
|
| 2141 |
+
],
|
| 2142 |
+
[
|
| 2143 |
+
41.1408,
|
| 2144 |
+
44.3359
|
| 2145 |
+
],
|
| 2146 |
+
[
|
| 2147 |
+
41.1408,
|
| 2148 |
+
45.9359
|
| 2149 |
+
],
|
| 2150 |
+
[
|
| 2151 |
+
41.1408,
|
| 2152 |
+
47.5359
|
| 2153 |
+
],
|
| 2154 |
+
[
|
| 2155 |
+
41.1408,
|
| 2156 |
+
49.1359
|
| 2157 |
+
],
|
| 2158 |
+
[
|
| 2159 |
+
41.1408,
|
| 2160 |
+
50.7359
|
| 2161 |
+
],
|
| 2162 |
+
[
|
| 2163 |
+
41.1408,
|
| 2164 |
+
52.3359
|
| 2165 |
+
],
|
| 2166 |
+
[
|
| 2167 |
+
41.1408,
|
| 2168 |
+
53.9359
|
| 2169 |
+
],
|
| 2170 |
+
[
|
| 2171 |
+
41.1408,
|
| 2172 |
+
76.3359
|
| 2173 |
+
],
|
| 2174 |
+
[
|
| 2175 |
+
41.1408,
|
| 2176 |
+
77.9359
|
| 2177 |
+
],
|
| 2178 |
+
[
|
| 2179 |
+
42.7408,
|
| 2180 |
+
10.7359
|
| 2181 |
+
],
|
| 2182 |
+
[
|
| 2183 |
+
42.7408,
|
| 2184 |
+
12.3359
|
| 2185 |
+
],
|
| 2186 |
+
[
|
| 2187 |
+
42.7408,
|
| 2188 |
+
34.7359
|
| 2189 |
+
],
|
| 2190 |
+
[
|
| 2191 |
+
42.7408,
|
| 2192 |
+
36.3359
|
| 2193 |
+
],
|
| 2194 |
+
[
|
| 2195 |
+
42.7408,
|
| 2196 |
+
37.9359
|
| 2197 |
+
],
|
| 2198 |
+
[
|
| 2199 |
+
42.7408,
|
| 2200 |
+
39.5359
|
| 2201 |
+
],
|
| 2202 |
+
[
|
| 2203 |
+
42.7408,
|
| 2204 |
+
44.3359
|
| 2205 |
+
],
|
| 2206 |
+
[
|
| 2207 |
+
42.7408,
|
| 2208 |
+
45.9359
|
| 2209 |
+
],
|
| 2210 |
+
[
|
| 2211 |
+
42.7408,
|
| 2212 |
+
47.5359
|
| 2213 |
+
],
|
| 2214 |
+
[
|
| 2215 |
+
42.7408,
|
| 2216 |
+
49.1359
|
| 2217 |
+
],
|
| 2218 |
+
[
|
| 2219 |
+
42.7408,
|
| 2220 |
+
50.7359
|
| 2221 |
+
],
|
| 2222 |
+
[
|
| 2223 |
+
42.7408,
|
| 2224 |
+
52.3359
|
| 2225 |
+
],
|
| 2226 |
+
[
|
| 2227 |
+
42.7408,
|
| 2228 |
+
53.9359
|
| 2229 |
+
],
|
| 2230 |
+
[
|
| 2231 |
+
42.7408,
|
| 2232 |
+
76.3359
|
| 2233 |
+
],
|
| 2234 |
+
[
|
| 2235 |
+
42.7408,
|
| 2236 |
+
77.9359
|
| 2237 |
+
],
|
| 2238 |
+
[
|
| 2239 |
+
44.3408,
|
| 2240 |
+
10.7359
|
| 2241 |
+
],
|
| 2242 |
+
[
|
| 2243 |
+
44.3408,
|
| 2244 |
+
12.3359
|
| 2245 |
+
],
|
| 2246 |
+
[
|
| 2247 |
+
44.3408,
|
| 2248 |
+
34.7359
|
| 2249 |
+
],
|
| 2250 |
+
[
|
| 2251 |
+
44.3408,
|
| 2252 |
+
36.3359
|
| 2253 |
+
],
|
| 2254 |
+
[
|
| 2255 |
+
44.3408,
|
| 2256 |
+
37.9359
|
| 2257 |
+
],
|
| 2258 |
+
[
|
| 2259 |
+
44.3408,
|
| 2260 |
+
39.5359
|
| 2261 |
+
],
|
| 2262 |
+
[
|
| 2263 |
+
44.3408,
|
| 2264 |
+
44.3359
|
| 2265 |
+
],
|
| 2266 |
+
[
|
| 2267 |
+
44.3408,
|
| 2268 |
+
45.9359
|
| 2269 |
+
],
|
| 2270 |
+
[
|
| 2271 |
+
44.3408,
|
| 2272 |
+
47.5359
|
| 2273 |
+
],
|
| 2274 |
+
[
|
| 2275 |
+
44.3408,
|
| 2276 |
+
49.1359
|
| 2277 |
+
],
|
| 2278 |
+
[
|
| 2279 |
+
44.3408,
|
| 2280 |
+
50.7359
|
| 2281 |
+
],
|
| 2282 |
+
[
|
| 2283 |
+
44.3408,
|
| 2284 |
+
52.3359
|
| 2285 |
+
],
|
| 2286 |
+
[
|
| 2287 |
+
44.3408,
|
| 2288 |
+
53.9359
|
| 2289 |
+
],
|
| 2290 |
+
[
|
| 2291 |
+
44.3408,
|
| 2292 |
+
76.3359
|
| 2293 |
+
],
|
| 2294 |
+
[
|
| 2295 |
+
44.3408,
|
| 2296 |
+
77.9359
|
| 2297 |
+
],
|
| 2298 |
+
[
|
| 2299 |
+
45.9408,
|
| 2300 |
+
9.1359
|
| 2301 |
+
],
|
| 2302 |
+
[
|
| 2303 |
+
45.9408,
|
| 2304 |
+
10.7359
|
| 2305 |
+
],
|
| 2306 |
+
[
|
| 2307 |
+
45.9408,
|
| 2308 |
+
12.3359
|
| 2309 |
+
],
|
| 2310 |
+
[
|
| 2311 |
+
45.9408,
|
| 2312 |
+
13.9359
|
| 2313 |
+
],
|
| 2314 |
+
[
|
| 2315 |
+
45.9408,
|
| 2316 |
+
34.7359
|
| 2317 |
+
],
|
| 2318 |
+
[
|
| 2319 |
+
45.9408,
|
| 2320 |
+
36.3359
|
| 2321 |
+
],
|
| 2322 |
+
[
|
| 2323 |
+
45.9408,
|
| 2324 |
+
39.5359
|
| 2325 |
+
],
|
| 2326 |
+
[
|
| 2327 |
+
45.9408,
|
| 2328 |
+
41.1359
|
| 2329 |
+
],
|
| 2330 |
+
[
|
| 2331 |
+
45.9408,
|
| 2332 |
+
45.9359
|
| 2333 |
+
],
|
| 2334 |
+
[
|
| 2335 |
+
45.9408,
|
| 2336 |
+
47.5359
|
| 2337 |
+
],
|
| 2338 |
+
[
|
| 2339 |
+
45.9408,
|
| 2340 |
+
49.1359
|
| 2341 |
+
],
|
| 2342 |
+
[
|
| 2343 |
+
45.9408,
|
| 2344 |
+
50.7359
|
| 2345 |
+
],
|
| 2346 |
+
[
|
| 2347 |
+
45.9408,
|
| 2348 |
+
52.3359
|
| 2349 |
+
],
|
| 2350 |
+
[
|
| 2351 |
+
45.9408,
|
| 2352 |
+
53.9359
|
| 2353 |
+
],
|
| 2354 |
+
[
|
| 2355 |
+
45.9408,
|
| 2356 |
+
76.3359
|
| 2357 |
+
],
|
| 2358 |
+
[
|
| 2359 |
+
45.9408,
|
| 2360 |
+
77.9359
|
| 2361 |
+
],
|
| 2362 |
+
[
|
| 2363 |
+
47.5408,
|
| 2364 |
+
9.1359
|
| 2365 |
+
],
|
| 2366 |
+
[
|
| 2367 |
+
47.5408,
|
| 2368 |
+
10.7359
|
| 2369 |
+
],
|
| 2370 |
+
[
|
| 2371 |
+
47.5408,
|
| 2372 |
+
12.3359
|
| 2373 |
+
],
|
| 2374 |
+
[
|
| 2375 |
+
47.5408,
|
| 2376 |
+
25.1359
|
| 2377 |
+
],
|
| 2378 |
+
[
|
| 2379 |
+
47.5408,
|
| 2380 |
+
26.7359
|
| 2381 |
+
],
|
| 2382 |
+
[
|
| 2383 |
+
47.5408,
|
| 2384 |
+
34.7359
|
| 2385 |
+
],
|
| 2386 |
+
[
|
| 2387 |
+
47.5408,
|
| 2388 |
+
36.3359
|
| 2389 |
+
],
|
| 2390 |
+
[
|
| 2391 |
+
47.5408,
|
| 2392 |
+
41.1359
|
| 2393 |
+
],
|
| 2394 |
+
[
|
| 2395 |
+
47.5408,
|
| 2396 |
+
45.9359
|
| 2397 |
+
],
|
| 2398 |
+
[
|
| 2399 |
+
47.5408,
|
| 2400 |
+
47.5359
|
| 2401 |
+
],
|
| 2402 |
+
[
|
| 2403 |
+
47.5408,
|
| 2404 |
+
49.1359
|
| 2405 |
+
],
|
| 2406 |
+
[
|
| 2407 |
+
47.5408,
|
| 2408 |
+
50.7359
|
| 2409 |
+
],
|
| 2410 |
+
[
|
| 2411 |
+
47.5408,
|
| 2412 |
+
52.3359
|
| 2413 |
+
],
|
| 2414 |
+
[
|
| 2415 |
+
47.5408,
|
| 2416 |
+
53.9359
|
| 2417 |
+
],
|
| 2418 |
+
[
|
| 2419 |
+
47.5408,
|
| 2420 |
+
76.3359
|
| 2421 |
+
],
|
| 2422 |
+
[
|
| 2423 |
+
47.5408,
|
| 2424 |
+
77.9359
|
| 2425 |
+
],
|
| 2426 |
+
[
|
| 2427 |
+
49.1408,
|
| 2428 |
+
9.1359
|
| 2429 |
+
],
|
| 2430 |
+
[
|
| 2431 |
+
49.1408,
|
| 2432 |
+
10.7359
|
| 2433 |
+
],
|
| 2434 |
+
[
|
| 2435 |
+
49.1408,
|
| 2436 |
+
23.5359
|
| 2437 |
+
],
|
| 2438 |
+
[
|
| 2439 |
+
49.1408,
|
| 2440 |
+
25.1359
|
| 2441 |
+
],
|
| 2442 |
+
[
|
| 2443 |
+
49.1408,
|
| 2444 |
+
26.7359
|
| 2445 |
+
],
|
| 2446 |
+
[
|
| 2447 |
+
49.1408,
|
| 2448 |
+
34.7359
|
| 2449 |
+
],
|
| 2450 |
+
[
|
| 2451 |
+
49.1408,
|
| 2452 |
+
36.3359
|
| 2453 |
+
],
|
| 2454 |
+
[
|
| 2455 |
+
49.1408,
|
| 2456 |
+
41.1359
|
| 2457 |
+
],
|
| 2458 |
+
[
|
| 2459 |
+
49.1408,
|
| 2460 |
+
47.5359
|
| 2461 |
+
],
|
| 2462 |
+
[
|
| 2463 |
+
49.1408,
|
| 2464 |
+
49.1359
|
| 2465 |
+
],
|
| 2466 |
+
[
|
| 2467 |
+
49.1408,
|
| 2468 |
+
50.7359
|
| 2469 |
+
],
|
| 2470 |
+
[
|
| 2471 |
+
49.1408,
|
| 2472 |
+
52.3359
|
| 2473 |
+
],
|
| 2474 |
+
[
|
| 2475 |
+
49.1408,
|
| 2476 |
+
53.9359
|
| 2477 |
+
],
|
| 2478 |
+
[
|
| 2479 |
+
49.1408,
|
| 2480 |
+
74.7359
|
| 2481 |
+
],
|
| 2482 |
+
[
|
| 2483 |
+
49.1408,
|
| 2484 |
+
76.3359
|
| 2485 |
+
],
|
| 2486 |
+
[
|
| 2487 |
+
49.1408,
|
| 2488 |
+
77.9359
|
| 2489 |
+
],
|
| 2490 |
+
[
|
| 2491 |
+
50.7408,
|
| 2492 |
+
9.1359
|
| 2493 |
+
],
|
| 2494 |
+
[
|
| 2495 |
+
50.7408,
|
| 2496 |
+
10.7359
|
| 2497 |
+
],
|
| 2498 |
+
[
|
| 2499 |
+
50.7408,
|
| 2500 |
+
23.5359
|
| 2501 |
+
],
|
| 2502 |
+
[
|
| 2503 |
+
50.7408,
|
| 2504 |
+
25.1359
|
| 2505 |
+
],
|
| 2506 |
+
[
|
| 2507 |
+
50.7408,
|
| 2508 |
+
26.7359
|
| 2509 |
+
],
|
| 2510 |
+
[
|
| 2511 |
+
50.7408,
|
| 2512 |
+
28.3359
|
| 2513 |
+
],
|
| 2514 |
+
[
|
| 2515 |
+
50.7408,
|
| 2516 |
+
29.9359
|
| 2517 |
+
],
|
| 2518 |
+
[
|
| 2519 |
+
50.7408,
|
| 2520 |
+
31.5359
|
| 2521 |
+
],
|
| 2522 |
+
[
|
| 2523 |
+
50.7408,
|
| 2524 |
+
34.7359
|
| 2525 |
+
],
|
| 2526 |
+
[
|
| 2527 |
+
50.7408,
|
| 2528 |
+
36.3359
|
| 2529 |
+
],
|
| 2530 |
+
[
|
| 2531 |
+
50.7408,
|
| 2532 |
+
41.1359
|
| 2533 |
+
],
|
| 2534 |
+
[
|
| 2535 |
+
50.7408,
|
| 2536 |
+
42.7359
|
| 2537 |
+
],
|
| 2538 |
+
[
|
| 2539 |
+
50.7408,
|
| 2540 |
+
47.5359
|
| 2541 |
+
],
|
| 2542 |
+
[
|
| 2543 |
+
50.7408,
|
| 2544 |
+
49.1359
|
| 2545 |
+
],
|
| 2546 |
+
[
|
| 2547 |
+
50.7408,
|
| 2548 |
+
50.7359
|
| 2549 |
+
],
|
| 2550 |
+
[
|
| 2551 |
+
50.7408,
|
| 2552 |
+
52.3359
|
| 2553 |
+
],
|
| 2554 |
+
[
|
| 2555 |
+
50.7408,
|
| 2556 |
+
53.9359
|
| 2557 |
+
],
|
| 2558 |
+
[
|
| 2559 |
+
50.7408,
|
| 2560 |
+
55.5359
|
| 2561 |
+
],
|
| 2562 |
+
[
|
| 2563 |
+
50.7408,
|
| 2564 |
+
73.1359
|
| 2565 |
+
],
|
| 2566 |
+
[
|
| 2567 |
+
50.7408,
|
| 2568 |
+
74.7359
|
| 2569 |
+
],
|
| 2570 |
+
[
|
| 2571 |
+
50.7408,
|
| 2572 |
+
76.3359
|
| 2573 |
+
],
|
| 2574 |
+
[
|
| 2575 |
+
52.3408,
|
| 2576 |
+
9.1359
|
| 2577 |
+
],
|
| 2578 |
+
[
|
| 2579 |
+
52.3408,
|
| 2580 |
+
10.7359
|
| 2581 |
+
],
|
| 2582 |
+
[
|
| 2583 |
+
52.3408,
|
| 2584 |
+
23.5359
|
| 2585 |
+
],
|
| 2586 |
+
[
|
| 2587 |
+
52.3408,
|
| 2588 |
+
25.1359
|
| 2589 |
+
],
|
| 2590 |
+
[
|
| 2591 |
+
52.3408,
|
| 2592 |
+
26.7359
|
| 2593 |
+
],
|
| 2594 |
+
[
|
| 2595 |
+
52.3408,
|
| 2596 |
+
28.3359
|
| 2597 |
+
],
|
| 2598 |
+
[
|
| 2599 |
+
52.3408,
|
| 2600 |
+
29.9359
|
| 2601 |
+
],
|
| 2602 |
+
[
|
| 2603 |
+
52.3408,
|
| 2604 |
+
31.5359
|
| 2605 |
+
],
|
| 2606 |
+
[
|
| 2607 |
+
52.3408,
|
| 2608 |
+
33.1359
|
| 2609 |
+
],
|
| 2610 |
+
[
|
| 2611 |
+
52.3408,
|
| 2612 |
+
34.7359
|
| 2613 |
+
],
|
| 2614 |
+
[
|
| 2615 |
+
52.3408,
|
| 2616 |
+
36.3359
|
| 2617 |
+
],
|
| 2618 |
+
[
|
| 2619 |
+
52.3408,
|
| 2620 |
+
39.5359
|
| 2621 |
+
],
|
| 2622 |
+
[
|
| 2623 |
+
52.3408,
|
| 2624 |
+
41.1359
|
| 2625 |
+
],
|
| 2626 |
+
[
|
| 2627 |
+
52.3408,
|
| 2628 |
+
42.7359
|
| 2629 |
+
],
|
| 2630 |
+
[
|
| 2631 |
+
52.3408,
|
| 2632 |
+
47.5359
|
| 2633 |
+
],
|
| 2634 |
+
[
|
| 2635 |
+
52.3408,
|
| 2636 |
+
49.1359
|
| 2637 |
+
],
|
| 2638 |
+
[
|
| 2639 |
+
52.3408,
|
| 2640 |
+
50.7359
|
| 2641 |
+
],
|
| 2642 |
+
[
|
| 2643 |
+
52.3408,
|
| 2644 |
+
52.3359
|
| 2645 |
+
],
|
| 2646 |
+
[
|
| 2647 |
+
52.3408,
|
| 2648 |
+
53.9359
|
| 2649 |
+
],
|
| 2650 |
+
[
|
| 2651 |
+
52.3408,
|
| 2652 |
+
55.5359
|
| 2653 |
+
],
|
| 2654 |
+
[
|
| 2655 |
+
52.3408,
|
| 2656 |
+
57.1359
|
| 2657 |
+
],
|
| 2658 |
+
[
|
| 2659 |
+
52.3408,
|
| 2660 |
+
58.7359
|
| 2661 |
+
],
|
| 2662 |
+
[
|
| 2663 |
+
52.3408,
|
| 2664 |
+
63.5359
|
| 2665 |
+
],
|
| 2666 |
+
[
|
| 2667 |
+
52.3408,
|
| 2668 |
+
65.1359
|
| 2669 |
+
],
|
| 2670 |
+
[
|
| 2671 |
+
52.3408,
|
| 2672 |
+
71.5359
|
| 2673 |
+
],
|
| 2674 |
+
[
|
| 2675 |
+
52.3408,
|
| 2676 |
+
73.1359
|
| 2677 |
+
],
|
| 2678 |
+
[
|
| 2679 |
+
52.3408,
|
| 2680 |
+
74.7359
|
| 2681 |
+
],
|
| 2682 |
+
[
|
| 2683 |
+
52.3408,
|
| 2684 |
+
76.3359
|
| 2685 |
+
],
|
| 2686 |
+
[
|
| 2687 |
+
53.9408,
|
| 2688 |
+
9.1359
|
| 2689 |
+
],
|
| 2690 |
+
[
|
| 2691 |
+
53.9408,
|
| 2692 |
+
15.5359
|
| 2693 |
+
],
|
| 2694 |
+
[
|
| 2695 |
+
53.9408,
|
| 2696 |
+
21.9359
|
| 2697 |
+
],
|
| 2698 |
+
[
|
| 2699 |
+
53.9408,
|
| 2700 |
+
23.5359
|
| 2701 |
+
],
|
| 2702 |
+
[
|
| 2703 |
+
53.9408,
|
| 2704 |
+
25.1359
|
| 2705 |
+
],
|
| 2706 |
+
[
|
| 2707 |
+
53.9408,
|
| 2708 |
+
26.7359
|
| 2709 |
+
],
|
| 2710 |
+
[
|
| 2711 |
+
53.9408,
|
| 2712 |
+
28.3359
|
| 2713 |
+
],
|
| 2714 |
+
[
|
| 2715 |
+
53.9408,
|
| 2716 |
+
29.9359
|
| 2717 |
+
],
|
| 2718 |
+
[
|
| 2719 |
+
53.9408,
|
| 2720 |
+
31.5359
|
| 2721 |
+
],
|
| 2722 |
+
[
|
| 2723 |
+
53.9408,
|
| 2724 |
+
33.1359
|
| 2725 |
+
],
|
| 2726 |
+
[
|
| 2727 |
+
53.9408,
|
| 2728 |
+
34.7359
|
| 2729 |
+
],
|
| 2730 |
+
[
|
| 2731 |
+
53.9408,
|
| 2732 |
+
39.5359
|
| 2733 |
+
],
|
| 2734 |
+
[
|
| 2735 |
+
53.9408,
|
| 2736 |
+
41.1359
|
| 2737 |
+
],
|
| 2738 |
+
[
|
| 2739 |
+
53.9408,
|
| 2740 |
+
42.7359
|
| 2741 |
+
],
|
| 2742 |
+
[
|
| 2743 |
+
53.9408,
|
| 2744 |
+
47.5359
|
| 2745 |
+
],
|
| 2746 |
+
[
|
| 2747 |
+
53.9408,
|
| 2748 |
+
49.1359
|
| 2749 |
+
],
|
| 2750 |
+
[
|
| 2751 |
+
53.9408,
|
| 2752 |
+
50.7359
|
| 2753 |
+
],
|
| 2754 |
+
[
|
| 2755 |
+
53.9408,
|
| 2756 |
+
52.3359
|
| 2757 |
+
],
|
| 2758 |
+
[
|
| 2759 |
+
53.9408,
|
| 2760 |
+
57.1359
|
| 2761 |
+
],
|
| 2762 |
+
[
|
| 2763 |
+
53.9408,
|
| 2764 |
+
58.7359
|
| 2765 |
+
],
|
| 2766 |
+
[
|
| 2767 |
+
53.9408,
|
| 2768 |
+
60.3359
|
| 2769 |
+
],
|
| 2770 |
+
[
|
| 2771 |
+
53.9408,
|
| 2772 |
+
61.9359
|
| 2773 |
+
],
|
| 2774 |
+
[
|
| 2775 |
+
53.9408,
|
| 2776 |
+
63.5359
|
| 2777 |
+
],
|
| 2778 |
+
[
|
| 2779 |
+
53.9408,
|
| 2780 |
+
65.1359
|
| 2781 |
+
],
|
| 2782 |
+
[
|
| 2783 |
+
53.9408,
|
| 2784 |
+
66.7359
|
| 2785 |
+
],
|
| 2786 |
+
[
|
| 2787 |
+
53.9408,
|
| 2788 |
+
71.5359
|
| 2789 |
+
],
|
| 2790 |
+
[
|
| 2791 |
+
53.9408,
|
| 2792 |
+
73.1359
|
| 2793 |
+
],
|
| 2794 |
+
[
|
| 2795 |
+
53.9408,
|
| 2796 |
+
74.7359
|
| 2797 |
+
],
|
| 2798 |
+
[
|
| 2799 |
+
55.5408,
|
| 2800 |
+
9.1359
|
| 2801 |
+
],
|
| 2802 |
+
[
|
| 2803 |
+
55.5408,
|
| 2804 |
+
10.7359
|
| 2805 |
+
],
|
| 2806 |
+
[
|
| 2807 |
+
55.5408,
|
| 2808 |
+
13.9359
|
| 2809 |
+
],
|
| 2810 |
+
[
|
| 2811 |
+
55.5408,
|
| 2812 |
+
15.5359
|
| 2813 |
+
],
|
| 2814 |
+
[
|
| 2815 |
+
55.5408,
|
| 2816 |
+
17.1359
|
| 2817 |
+
],
|
| 2818 |
+
[
|
| 2819 |
+
55.5408,
|
| 2820 |
+
18.7359
|
| 2821 |
+
],
|
| 2822 |
+
[
|
| 2823 |
+
55.5408,
|
| 2824 |
+
20.3359
|
| 2825 |
+
],
|
| 2826 |
+
[
|
| 2827 |
+
55.5408,
|
| 2828 |
+
21.9359
|
| 2829 |
+
],
|
| 2830 |
+
[
|
| 2831 |
+
55.5408,
|
| 2832 |
+
23.5359
|
| 2833 |
+
],
|
| 2834 |
+
[
|
| 2835 |
+
55.5408,
|
| 2836 |
+
25.1359
|
| 2837 |
+
],
|
| 2838 |
+
[
|
| 2839 |
+
55.5408,
|
| 2840 |
+
26.7359
|
| 2841 |
+
],
|
| 2842 |
+
[
|
| 2843 |
+
55.5408,
|
| 2844 |
+
28.3359
|
| 2845 |
+
],
|
| 2846 |
+
[
|
| 2847 |
+
55.5408,
|
| 2848 |
+
29.9359
|
| 2849 |
+
],
|
| 2850 |
+
[
|
| 2851 |
+
55.5408,
|
| 2852 |
+
31.5359
|
| 2853 |
+
],
|
| 2854 |
+
[
|
| 2855 |
+
55.5408,
|
| 2856 |
+
33.1359
|
| 2857 |
+
],
|
| 2858 |
+
[
|
| 2859 |
+
55.5408,
|
| 2860 |
+
34.7359
|
| 2861 |
+
],
|
| 2862 |
+
[
|
| 2863 |
+
55.5408,
|
| 2864 |
+
37.9359
|
| 2865 |
+
],
|
| 2866 |
+
[
|
| 2867 |
+
55.5408,
|
| 2868 |
+
39.5359
|
| 2869 |
+
],
|
| 2870 |
+
[
|
| 2871 |
+
55.5408,
|
| 2872 |
+
41.1359
|
| 2873 |
+
],
|
| 2874 |
+
[
|
| 2875 |
+
55.5408,
|
| 2876 |
+
42.7359
|
| 2877 |
+
],
|
| 2878 |
+
[
|
| 2879 |
+
55.5408,
|
| 2880 |
+
44.3359
|
| 2881 |
+
],
|
| 2882 |
+
[
|
| 2883 |
+
55.5408,
|
| 2884 |
+
47.5359
|
| 2885 |
+
],
|
| 2886 |
+
[
|
| 2887 |
+
55.5408,
|
| 2888 |
+
49.1359
|
| 2889 |
+
],
|
| 2890 |
+
[
|
| 2891 |
+
55.5408,
|
| 2892 |
+
50.7359
|
| 2893 |
+
],
|
| 2894 |
+
[
|
| 2895 |
+
55.5408,
|
| 2896 |
+
52.3359
|
| 2897 |
+
],
|
| 2898 |
+
[
|
| 2899 |
+
55.5408,
|
| 2900 |
+
58.7359
|
| 2901 |
+
],
|
| 2902 |
+
[
|
| 2903 |
+
55.5408,
|
| 2904 |
+
60.3359
|
| 2905 |
+
],
|
| 2906 |
+
[
|
| 2907 |
+
55.5408,
|
| 2908 |
+
61.9359
|
| 2909 |
+
],
|
| 2910 |
+
[
|
| 2911 |
+
55.5408,
|
| 2912 |
+
63.5359
|
| 2913 |
+
],
|
| 2914 |
+
[
|
| 2915 |
+
55.5408,
|
| 2916 |
+
65.1359
|
| 2917 |
+
],
|
| 2918 |
+
[
|
| 2919 |
+
55.5408,
|
| 2920 |
+
66.7359
|
| 2921 |
+
],
|
| 2922 |
+
[
|
| 2923 |
+
55.5408,
|
| 2924 |
+
68.3359
|
| 2925 |
+
],
|
| 2926 |
+
[
|
| 2927 |
+
55.5408,
|
| 2928 |
+
69.9359
|
| 2929 |
+
],
|
| 2930 |
+
[
|
| 2931 |
+
55.5408,
|
| 2932 |
+
71.5359
|
| 2933 |
+
],
|
| 2934 |
+
[
|
| 2935 |
+
55.5408,
|
| 2936 |
+
73.1359
|
| 2937 |
+
],
|
| 2938 |
+
[
|
| 2939 |
+
55.5408,
|
| 2940 |
+
74.7359
|
| 2941 |
+
],
|
| 2942 |
+
[
|
| 2943 |
+
57.1408,
|
| 2944 |
+
9.1359
|
| 2945 |
+
],
|
| 2946 |
+
[
|
| 2947 |
+
57.1408,
|
| 2948 |
+
10.7359
|
| 2949 |
+
],
|
| 2950 |
+
[
|
| 2951 |
+
57.1408,
|
| 2952 |
+
12.3359
|
| 2953 |
+
],
|
| 2954 |
+
[
|
| 2955 |
+
57.1408,
|
| 2956 |
+
13.9359
|
| 2957 |
+
],
|
| 2958 |
+
[
|
| 2959 |
+
57.1408,
|
| 2960 |
+
15.5359
|
| 2961 |
+
],
|
| 2962 |
+
[
|
| 2963 |
+
57.1408,
|
| 2964 |
+
17.1359
|
| 2965 |
+
],
|
| 2966 |
+
[
|
| 2967 |
+
57.1408,
|
| 2968 |
+
18.7359
|
| 2969 |
+
],
|
| 2970 |
+
[
|
| 2971 |
+
57.1408,
|
| 2972 |
+
20.3359
|
| 2973 |
+
],
|
| 2974 |
+
[
|
| 2975 |
+
57.1408,
|
| 2976 |
+
21.9359
|
| 2977 |
+
],
|
| 2978 |
+
[
|
| 2979 |
+
57.1408,
|
| 2980 |
+
26.7359
|
| 2981 |
+
],
|
| 2982 |
+
[
|
| 2983 |
+
57.1408,
|
| 2984 |
+
28.3359
|
| 2985 |
+
],
|
| 2986 |
+
[
|
| 2987 |
+
57.1408,
|
| 2988 |
+
29.9359
|
| 2989 |
+
],
|
| 2990 |
+
[
|
| 2991 |
+
57.1408,
|
| 2992 |
+
31.5359
|
| 2993 |
+
],
|
| 2994 |
+
[
|
| 2995 |
+
57.1408,
|
| 2996 |
+
33.1359
|
| 2997 |
+
],
|
| 2998 |
+
[
|
| 2999 |
+
57.1408,
|
| 3000 |
+
37.9359
|
| 3001 |
+
],
|
| 3002 |
+
[
|
| 3003 |
+
57.1408,
|
| 3004 |
+
39.5359
|
| 3005 |
+
],
|
| 3006 |
+
[
|
| 3007 |
+
57.1408,
|
| 3008 |
+
41.1359
|
| 3009 |
+
],
|
| 3010 |
+
[
|
| 3011 |
+
57.1408,
|
| 3012 |
+
42.7359
|
| 3013 |
+
],
|
| 3014 |
+
[
|
| 3015 |
+
57.1408,
|
| 3016 |
+
44.3359
|
| 3017 |
+
],
|
| 3018 |
+
[
|
| 3019 |
+
57.1408,
|
| 3020 |
+
45.9359
|
| 3021 |
+
],
|
| 3022 |
+
[
|
| 3023 |
+
57.1408,
|
| 3024 |
+
47.5359
|
| 3025 |
+
],
|
| 3026 |
+
[
|
| 3027 |
+
57.1408,
|
| 3028 |
+
60.3359
|
| 3029 |
+
],
|
| 3030 |
+
[
|
| 3031 |
+
57.1408,
|
| 3032 |
+
61.9359
|
| 3033 |
+
],
|
| 3034 |
+
[
|
| 3035 |
+
57.1408,
|
| 3036 |
+
63.5359
|
| 3037 |
+
],
|
| 3038 |
+
[
|
| 3039 |
+
57.1408,
|
| 3040 |
+
65.1359
|
| 3041 |
+
],
|
| 3042 |
+
[
|
| 3043 |
+
57.1408,
|
| 3044 |
+
66.7359
|
| 3045 |
+
],
|
| 3046 |
+
[
|
| 3047 |
+
57.1408,
|
| 3048 |
+
68.3359
|
| 3049 |
+
],
|
| 3050 |
+
[
|
| 3051 |
+
57.1408,
|
| 3052 |
+
69.9359
|
| 3053 |
+
],
|
| 3054 |
+
[
|
| 3055 |
+
57.1408,
|
| 3056 |
+
71.5359
|
| 3057 |
+
],
|
| 3058 |
+
[
|
| 3059 |
+
57.1408,
|
| 3060 |
+
73.1359
|
| 3061 |
+
],
|
| 3062 |
+
[
|
| 3063 |
+
58.7408,
|
| 3064 |
+
7.5359
|
| 3065 |
+
],
|
| 3066 |
+
[
|
| 3067 |
+
58.7408,
|
| 3068 |
+
9.1359
|
| 3069 |
+
],
|
| 3070 |
+
[
|
| 3071 |
+
58.7408,
|
| 3072 |
+
10.7359
|
| 3073 |
+
],
|
| 3074 |
+
[
|
| 3075 |
+
58.7408,
|
| 3076 |
+
12.3359
|
| 3077 |
+
],
|
| 3078 |
+
[
|
| 3079 |
+
58.7408,
|
| 3080 |
+
13.9359
|
| 3081 |
+
],
|
| 3082 |
+
[
|
| 3083 |
+
58.7408,
|
| 3084 |
+
15.5359
|
| 3085 |
+
],
|
| 3086 |
+
[
|
| 3087 |
+
58.7408,
|
| 3088 |
+
17.1359
|
| 3089 |
+
],
|
| 3090 |
+
[
|
| 3091 |
+
58.7408,
|
| 3092 |
+
18.7359
|
| 3093 |
+
],
|
| 3094 |
+
[
|
| 3095 |
+
58.7408,
|
| 3096 |
+
20.3359
|
| 3097 |
+
],
|
| 3098 |
+
[
|
| 3099 |
+
58.7408,
|
| 3100 |
+
21.9359
|
| 3101 |
+
],
|
| 3102 |
+
[
|
| 3103 |
+
58.7408,
|
| 3104 |
+
29.9359
|
| 3105 |
+
],
|
| 3106 |
+
[
|
| 3107 |
+
58.7408,
|
| 3108 |
+
31.5359
|
| 3109 |
+
],
|
| 3110 |
+
[
|
| 3111 |
+
58.7408,
|
| 3112 |
+
37.9359
|
| 3113 |
+
],
|
| 3114 |
+
[
|
| 3115 |
+
58.7408,
|
| 3116 |
+
39.5359
|
| 3117 |
+
],
|
| 3118 |
+
[
|
| 3119 |
+
58.7408,
|
| 3120 |
+
41.1359
|
| 3121 |
+
],
|
| 3122 |
+
[
|
| 3123 |
+
58.7408,
|
| 3124 |
+
42.7359
|
| 3125 |
+
],
|
| 3126 |
+
[
|
| 3127 |
+
58.7408,
|
| 3128 |
+
44.3359
|
| 3129 |
+
],
|
| 3130 |
+
[
|
| 3131 |
+
58.7408,
|
| 3132 |
+
45.9359
|
| 3133 |
+
],
|
| 3134 |
+
[
|
| 3135 |
+
58.7408,
|
| 3136 |
+
58.7359
|
| 3137 |
+
],
|
| 3138 |
+
[
|
| 3139 |
+
58.7408,
|
| 3140 |
+
60.3359
|
| 3141 |
+
],
|
| 3142 |
+
[
|
| 3143 |
+
58.7408,
|
| 3144 |
+
61.9359
|
| 3145 |
+
],
|
| 3146 |
+
[
|
| 3147 |
+
58.7408,
|
| 3148 |
+
63.5359
|
| 3149 |
+
],
|
| 3150 |
+
[
|
| 3151 |
+
58.7408,
|
| 3152 |
+
65.1359
|
| 3153 |
+
],
|
| 3154 |
+
[
|
| 3155 |
+
58.7408,
|
| 3156 |
+
66.7359
|
| 3157 |
+
],
|
| 3158 |
+
[
|
| 3159 |
+
58.7408,
|
| 3160 |
+
68.3359
|
| 3161 |
+
],
|
| 3162 |
+
[
|
| 3163 |
+
58.7408,
|
| 3164 |
+
69.9359
|
| 3165 |
+
],
|
| 3166 |
+
[
|
| 3167 |
+
58.7408,
|
| 3168 |
+
71.5359
|
| 3169 |
+
],
|
| 3170 |
+
[
|
| 3171 |
+
60.3408,
|
| 3172 |
+
7.5359
|
| 3173 |
+
],
|
| 3174 |
+
[
|
| 3175 |
+
60.3408,
|
| 3176 |
+
9.1359
|
| 3177 |
+
],
|
| 3178 |
+
[
|
| 3179 |
+
60.3408,
|
| 3180 |
+
10.7359
|
| 3181 |
+
],
|
| 3182 |
+
[
|
| 3183 |
+
60.3408,
|
| 3184 |
+
12.3359
|
| 3185 |
+
],
|
| 3186 |
+
[
|
| 3187 |
+
60.3408,
|
| 3188 |
+
13.9359
|
| 3189 |
+
],
|
| 3190 |
+
[
|
| 3191 |
+
60.3408,
|
| 3192 |
+
15.5359
|
| 3193 |
+
],
|
| 3194 |
+
[
|
| 3195 |
+
60.3408,
|
| 3196 |
+
17.1359
|
| 3197 |
+
],
|
| 3198 |
+
[
|
| 3199 |
+
60.3408,
|
| 3200 |
+
18.7359
|
| 3201 |
+
],
|
| 3202 |
+
[
|
| 3203 |
+
60.3408,
|
| 3204 |
+
20.3359
|
| 3205 |
+
],
|
| 3206 |
+
[
|
| 3207 |
+
60.3408,
|
| 3208 |
+
21.9359
|
| 3209 |
+
],
|
| 3210 |
+
[
|
| 3211 |
+
60.3408,
|
| 3212 |
+
23.5359
|
| 3213 |
+
],
|
| 3214 |
+
[
|
| 3215 |
+
60.3408,
|
| 3216 |
+
29.9359
|
| 3217 |
+
],
|
| 3218 |
+
[
|
| 3219 |
+
60.3408,
|
| 3220 |
+
31.5359
|
| 3221 |
+
],
|
| 3222 |
+
[
|
| 3223 |
+
60.3408,
|
| 3224 |
+
36.3359
|
| 3225 |
+
],
|
| 3226 |
+
[
|
| 3227 |
+
60.3408,
|
| 3228 |
+
37.9359
|
| 3229 |
+
],
|
| 3230 |
+
[
|
| 3231 |
+
60.3408,
|
| 3232 |
+
39.5359
|
| 3233 |
+
],
|
| 3234 |
+
[
|
| 3235 |
+
60.3408,
|
| 3236 |
+
41.1359
|
| 3237 |
+
],
|
| 3238 |
+
[
|
| 3239 |
+
60.3408,
|
| 3240 |
+
42.7359
|
| 3241 |
+
],
|
| 3242 |
+
[
|
| 3243 |
+
60.3408,
|
| 3244 |
+
44.3359
|
| 3245 |
+
],
|
| 3246 |
+
[
|
| 3247 |
+
60.3408,
|
| 3248 |
+
58.7359
|
| 3249 |
+
],
|
| 3250 |
+
[
|
| 3251 |
+
60.3408,
|
| 3252 |
+
60.3359
|
| 3253 |
+
],
|
| 3254 |
+
[
|
| 3255 |
+
60.3408,
|
| 3256 |
+
63.5359
|
| 3257 |
+
],
|
| 3258 |
+
[
|
| 3259 |
+
60.3408,
|
| 3260 |
+
65.1359
|
| 3261 |
+
],
|
| 3262 |
+
[
|
| 3263 |
+
60.3408,
|
| 3264 |
+
66.7359
|
| 3265 |
+
],
|
| 3266 |
+
[
|
| 3267 |
+
60.3408,
|
| 3268 |
+
68.3359
|
| 3269 |
+
],
|
| 3270 |
+
[
|
| 3271 |
+
60.3408,
|
| 3272 |
+
69.9359
|
| 3273 |
+
],
|
| 3274 |
+
[
|
| 3275 |
+
60.3408,
|
| 3276 |
+
71.5359
|
| 3277 |
+
],
|
| 3278 |
+
[
|
| 3279 |
+
61.9408,
|
| 3280 |
+
10.7359
|
| 3281 |
+
],
|
| 3282 |
+
[
|
| 3283 |
+
61.9408,
|
| 3284 |
+
12.3359
|
| 3285 |
+
],
|
| 3286 |
+
[
|
| 3287 |
+
61.9408,
|
| 3288 |
+
13.9359
|
| 3289 |
+
],
|
| 3290 |
+
[
|
| 3291 |
+
61.9408,
|
| 3292 |
+
15.5359
|
| 3293 |
+
],
|
| 3294 |
+
[
|
| 3295 |
+
61.9408,
|
| 3296 |
+
17.1359
|
| 3297 |
+
],
|
| 3298 |
+
[
|
| 3299 |
+
61.9408,
|
| 3300 |
+
18.7359
|
| 3301 |
+
],
|
| 3302 |
+
[
|
| 3303 |
+
61.9408,
|
| 3304 |
+
20.3359
|
| 3305 |
+
],
|
| 3306 |
+
[
|
| 3307 |
+
61.9408,
|
| 3308 |
+
21.9359
|
| 3309 |
+
],
|
| 3310 |
+
[
|
| 3311 |
+
61.9408,
|
| 3312 |
+
23.5359
|
| 3313 |
+
],
|
| 3314 |
+
[
|
| 3315 |
+
61.9408,
|
| 3316 |
+
25.1359
|
| 3317 |
+
],
|
| 3318 |
+
[
|
| 3319 |
+
61.9408,
|
| 3320 |
+
29.9359
|
| 3321 |
+
],
|
| 3322 |
+
[
|
| 3323 |
+
61.9408,
|
| 3324 |
+
33.1359
|
| 3325 |
+
],
|
| 3326 |
+
[
|
| 3327 |
+
61.9408,
|
| 3328 |
+
34.7359
|
| 3329 |
+
],
|
| 3330 |
+
[
|
| 3331 |
+
61.9408,
|
| 3332 |
+
36.3359
|
| 3333 |
+
],
|
| 3334 |
+
[
|
| 3335 |
+
61.9408,
|
| 3336 |
+
37.9359
|
| 3337 |
+
],
|
| 3338 |
+
[
|
| 3339 |
+
61.9408,
|
| 3340 |
+
39.5359
|
| 3341 |
+
],
|
| 3342 |
+
[
|
| 3343 |
+
61.9408,
|
| 3344 |
+
41.1359
|
| 3345 |
+
],
|
| 3346 |
+
[
|
| 3347 |
+
61.9408,
|
| 3348 |
+
42.7359
|
| 3349 |
+
],
|
| 3350 |
+
[
|
| 3351 |
+
61.9408,
|
| 3352 |
+
57.1359
|
| 3353 |
+
],
|
| 3354 |
+
[
|
| 3355 |
+
61.9408,
|
| 3356 |
+
58.7359
|
| 3357 |
+
],
|
| 3358 |
+
[
|
| 3359 |
+
61.9408,
|
| 3360 |
+
60.3359
|
| 3361 |
+
],
|
| 3362 |
+
[
|
| 3363 |
+
61.9408,
|
| 3364 |
+
63.5359
|
| 3365 |
+
],
|
| 3366 |
+
[
|
| 3367 |
+
61.9408,
|
| 3368 |
+
65.1359
|
| 3369 |
+
],
|
| 3370 |
+
[
|
| 3371 |
+
61.9408,
|
| 3372 |
+
66.7359
|
| 3373 |
+
],
|
| 3374 |
+
[
|
| 3375 |
+
61.9408,
|
| 3376 |
+
68.3359
|
| 3377 |
+
],
|
| 3378 |
+
[
|
| 3379 |
+
61.9408,
|
| 3380 |
+
69.9359
|
| 3381 |
+
],
|
| 3382 |
+
[
|
| 3383 |
+
63.5408,
|
| 3384 |
+
9.1359
|
| 3385 |
+
],
|
| 3386 |
+
[
|
| 3387 |
+
63.5408,
|
| 3388 |
+
10.7359
|
| 3389 |
+
],
|
| 3390 |
+
[
|
| 3391 |
+
63.5408,
|
| 3392 |
+
12.3359
|
| 3393 |
+
],
|
| 3394 |
+
[
|
| 3395 |
+
63.5408,
|
| 3396 |
+
13.9359
|
| 3397 |
+
],
|
| 3398 |
+
[
|
| 3399 |
+
63.5408,
|
| 3400 |
+
15.5359
|
| 3401 |
+
],
|
| 3402 |
+
[
|
| 3403 |
+
63.5408,
|
| 3404 |
+
17.1359
|
| 3405 |
+
],
|
| 3406 |
+
[
|
| 3407 |
+
63.5408,
|
| 3408 |
+
18.7359
|
| 3409 |
+
],
|
| 3410 |
+
[
|
| 3411 |
+
63.5408,
|
| 3412 |
+
20.3359
|
| 3413 |
+
],
|
| 3414 |
+
[
|
| 3415 |
+
63.5408,
|
| 3416 |
+
21.9359
|
| 3417 |
+
],
|
| 3418 |
+
[
|
| 3419 |
+
63.5408,
|
| 3420 |
+
23.5359
|
| 3421 |
+
],
|
| 3422 |
+
[
|
| 3423 |
+
63.5408,
|
| 3424 |
+
25.1359
|
| 3425 |
+
],
|
| 3426 |
+
[
|
| 3427 |
+
63.5408,
|
| 3428 |
+
33.1359
|
| 3429 |
+
],
|
| 3430 |
+
[
|
| 3431 |
+
63.5408,
|
| 3432 |
+
34.7359
|
| 3433 |
+
],
|
| 3434 |
+
[
|
| 3435 |
+
63.5408,
|
| 3436 |
+
36.3359
|
| 3437 |
+
],
|
| 3438 |
+
[
|
| 3439 |
+
63.5408,
|
| 3440 |
+
37.9359
|
| 3441 |
+
],
|
| 3442 |
+
[
|
| 3443 |
+
63.5408,
|
| 3444 |
+
39.5359
|
| 3445 |
+
],
|
| 3446 |
+
[
|
| 3447 |
+
63.5408,
|
| 3448 |
+
41.1359
|
| 3449 |
+
],
|
| 3450 |
+
[
|
| 3451 |
+
63.5408,
|
| 3452 |
+
42.7359
|
| 3453 |
+
],
|
| 3454 |
+
[
|
| 3455 |
+
63.5408,
|
| 3456 |
+
57.1359
|
| 3457 |
+
],
|
| 3458 |
+
[
|
| 3459 |
+
63.5408,
|
| 3460 |
+
58.7359
|
| 3461 |
+
],
|
| 3462 |
+
[
|
| 3463 |
+
63.5408,
|
| 3464 |
+
65.1359
|
| 3465 |
+
],
|
| 3466 |
+
[
|
| 3467 |
+
63.5408,
|
| 3468 |
+
66.7359
|
| 3469 |
+
],
|
| 3470 |
+
[
|
| 3471 |
+
63.5408,
|
| 3472 |
+
68.3359
|
| 3473 |
+
],
|
| 3474 |
+
[
|
| 3475 |
+
63.5408,
|
| 3476 |
+
69.9359
|
| 3477 |
+
],
|
| 3478 |
+
[
|
| 3479 |
+
63.5408,
|
| 3480 |
+
71.5359
|
| 3481 |
+
],
|
| 3482 |
+
[
|
| 3483 |
+
65.1408,
|
| 3484 |
+
7.5359
|
| 3485 |
+
],
|
| 3486 |
+
[
|
| 3487 |
+
65.1408,
|
| 3488 |
+
9.1359
|
| 3489 |
+
],
|
| 3490 |
+
[
|
| 3491 |
+
65.1408,
|
| 3492 |
+
10.7359
|
| 3493 |
+
],
|
| 3494 |
+
[
|
| 3495 |
+
65.1408,
|
| 3496 |
+
12.3359
|
| 3497 |
+
],
|
| 3498 |
+
[
|
| 3499 |
+
65.1408,
|
| 3500 |
+
13.9359
|
| 3501 |
+
],
|
| 3502 |
+
[
|
| 3503 |
+
65.1408,
|
| 3504 |
+
15.5359
|
| 3505 |
+
],
|
| 3506 |
+
[
|
| 3507 |
+
65.1408,
|
| 3508 |
+
17.1359
|
| 3509 |
+
],
|
| 3510 |
+
[
|
| 3511 |
+
65.1408,
|
| 3512 |
+
18.7359
|
| 3513 |
+
],
|
| 3514 |
+
[
|
| 3515 |
+
65.1408,
|
| 3516 |
+
20.3359
|
| 3517 |
+
],
|
| 3518 |
+
[
|
| 3519 |
+
65.1408,
|
| 3520 |
+
21.9359
|
| 3521 |
+
],
|
| 3522 |
+
[
|
| 3523 |
+
65.1408,
|
| 3524 |
+
23.5359
|
| 3525 |
+
],
|
| 3526 |
+
[
|
| 3527 |
+
65.1408,
|
| 3528 |
+
25.1359
|
| 3529 |
+
],
|
| 3530 |
+
[
|
| 3531 |
+
65.1408,
|
| 3532 |
+
31.5359
|
| 3533 |
+
],
|
| 3534 |
+
[
|
| 3535 |
+
65.1408,
|
| 3536 |
+
33.1359
|
| 3537 |
+
],
|
| 3538 |
+
[
|
| 3539 |
+
65.1408,
|
| 3540 |
+
34.7359
|
| 3541 |
+
],
|
| 3542 |
+
[
|
| 3543 |
+
65.1408,
|
| 3544 |
+
36.3359
|
| 3545 |
+
],
|
| 3546 |
+
[
|
| 3547 |
+
65.1408,
|
| 3548 |
+
37.9359
|
| 3549 |
+
],
|
| 3550 |
+
[
|
| 3551 |
+
65.1408,
|
| 3552 |
+
39.5359
|
| 3553 |
+
],
|
| 3554 |
+
[
|
| 3555 |
+
65.1408,
|
| 3556 |
+
41.1359
|
| 3557 |
+
],
|
| 3558 |
+
[
|
| 3559 |
+
65.1408,
|
| 3560 |
+
42.7359
|
| 3561 |
+
],
|
| 3562 |
+
[
|
| 3563 |
+
65.1408,
|
| 3564 |
+
57.1359
|
| 3565 |
+
],
|
| 3566 |
+
[
|
| 3567 |
+
65.1408,
|
| 3568 |
+
58.7359
|
| 3569 |
+
],
|
| 3570 |
+
[
|
| 3571 |
+
65.1408,
|
| 3572 |
+
60.3359
|
| 3573 |
+
],
|
| 3574 |
+
[
|
| 3575 |
+
65.1408,
|
| 3576 |
+
65.1359
|
| 3577 |
+
],
|
| 3578 |
+
[
|
| 3579 |
+
65.1408,
|
| 3580 |
+
66.7359
|
| 3581 |
+
],
|
| 3582 |
+
[
|
| 3583 |
+
65.1408,
|
| 3584 |
+
68.3359
|
| 3585 |
+
],
|
| 3586 |
+
[
|
| 3587 |
+
65.1408,
|
| 3588 |
+
69.9359
|
| 3589 |
+
],
|
| 3590 |
+
[
|
| 3591 |
+
66.7408,
|
| 3592 |
+
7.5359
|
| 3593 |
+
],
|
| 3594 |
+
[
|
| 3595 |
+
66.7408,
|
| 3596 |
+
9.1359
|
| 3597 |
+
],
|
| 3598 |
+
[
|
| 3599 |
+
66.7408,
|
| 3600 |
+
10.7359
|
| 3601 |
+
],
|
| 3602 |
+
[
|
| 3603 |
+
66.7408,
|
| 3604 |
+
12.3359
|
| 3605 |
+
],
|
| 3606 |
+
[
|
| 3607 |
+
66.7408,
|
| 3608 |
+
13.9359
|
| 3609 |
+
],
|
| 3610 |
+
[
|
| 3611 |
+
66.7408,
|
| 3612 |
+
15.5359
|
| 3613 |
+
],
|
| 3614 |
+
[
|
| 3615 |
+
66.7408,
|
| 3616 |
+
17.1359
|
| 3617 |
+
],
|
| 3618 |
+
[
|
| 3619 |
+
66.7408,
|
| 3620 |
+
18.7359
|
| 3621 |
+
],
|
| 3622 |
+
[
|
| 3623 |
+
66.7408,
|
| 3624 |
+
20.3359
|
| 3625 |
+
],
|
| 3626 |
+
[
|
| 3627 |
+
66.7408,
|
| 3628 |
+
21.9359
|
| 3629 |
+
],
|
| 3630 |
+
[
|
| 3631 |
+
66.7408,
|
| 3632 |
+
23.5359
|
| 3633 |
+
],
|
| 3634 |
+
[
|
| 3635 |
+
66.7408,
|
| 3636 |
+
25.1359
|
| 3637 |
+
],
|
| 3638 |
+
[
|
| 3639 |
+
66.7408,
|
| 3640 |
+
31.5359
|
| 3641 |
+
],
|
| 3642 |
+
[
|
| 3643 |
+
66.7408,
|
| 3644 |
+
33.1359
|
| 3645 |
+
],
|
| 3646 |
+
[
|
| 3647 |
+
66.7408,
|
| 3648 |
+
34.7359
|
| 3649 |
+
],
|
| 3650 |
+
[
|
| 3651 |
+
66.7408,
|
| 3652 |
+
36.3359
|
| 3653 |
+
],
|
| 3654 |
+
[
|
| 3655 |
+
66.7408,
|
| 3656 |
+
37.9359
|
| 3657 |
+
],
|
| 3658 |
+
[
|
| 3659 |
+
66.7408,
|
| 3660 |
+
39.5359
|
| 3661 |
+
],
|
| 3662 |
+
[
|
| 3663 |
+
66.7408,
|
| 3664 |
+
41.1359
|
| 3665 |
+
],
|
| 3666 |
+
[
|
| 3667 |
+
66.7408,
|
| 3668 |
+
42.7359
|
| 3669 |
+
],
|
| 3670 |
+
[
|
| 3671 |
+
66.7408,
|
| 3672 |
+
58.7359
|
| 3673 |
+
],
|
| 3674 |
+
[
|
| 3675 |
+
66.7408,
|
| 3676 |
+
60.3359
|
| 3677 |
+
],
|
| 3678 |
+
[
|
| 3679 |
+
66.7408,
|
| 3680 |
+
61.9359
|
| 3681 |
+
],
|
| 3682 |
+
[
|
| 3683 |
+
66.7408,
|
| 3684 |
+
66.7359
|
| 3685 |
+
],
|
| 3686 |
+
[
|
| 3687 |
+
66.7408,
|
| 3688 |
+
68.3359
|
| 3689 |
+
],
|
| 3690 |
+
[
|
| 3691 |
+
68.3408,
|
| 3692 |
+
7.5359
|
| 3693 |
+
],
|
| 3694 |
+
[
|
| 3695 |
+
68.3408,
|
| 3696 |
+
9.1359
|
| 3697 |
+
],
|
| 3698 |
+
[
|
| 3699 |
+
68.3408,
|
| 3700 |
+
10.7359
|
| 3701 |
+
],
|
| 3702 |
+
[
|
| 3703 |
+
68.3408,
|
| 3704 |
+
12.3359
|
| 3705 |
+
],
|
| 3706 |
+
[
|
| 3707 |
+
68.3408,
|
| 3708 |
+
13.9359
|
| 3709 |
+
],
|
| 3710 |
+
[
|
| 3711 |
+
68.3408,
|
| 3712 |
+
15.5359
|
| 3713 |
+
],
|
| 3714 |
+
[
|
| 3715 |
+
68.3408,
|
| 3716 |
+
17.1359
|
| 3717 |
+
],
|
| 3718 |
+
[
|
| 3719 |
+
68.3408,
|
| 3720 |
+
18.7359
|
| 3721 |
+
],
|
| 3722 |
+
[
|
| 3723 |
+
68.3408,
|
| 3724 |
+
20.3359
|
| 3725 |
+
],
|
| 3726 |
+
[
|
| 3727 |
+
68.3408,
|
| 3728 |
+
21.9359
|
| 3729 |
+
],
|
| 3730 |
+
[
|
| 3731 |
+
68.3408,
|
| 3732 |
+
23.5359
|
| 3733 |
+
],
|
| 3734 |
+
[
|
| 3735 |
+
68.3408,
|
| 3736 |
+
25.1359
|
| 3737 |
+
],
|
| 3738 |
+
[
|
| 3739 |
+
68.3408,
|
| 3740 |
+
31.5359
|
| 3741 |
+
],
|
| 3742 |
+
[
|
| 3743 |
+
68.3408,
|
| 3744 |
+
33.1359
|
| 3745 |
+
],
|
| 3746 |
+
[
|
| 3747 |
+
68.3408,
|
| 3748 |
+
34.7359
|
| 3749 |
+
],
|
| 3750 |
+
[
|
| 3751 |
+
68.3408,
|
| 3752 |
+
36.3359
|
| 3753 |
+
],
|
| 3754 |
+
[
|
| 3755 |
+
68.3408,
|
| 3756 |
+
37.9359
|
| 3757 |
+
],
|
| 3758 |
+
[
|
| 3759 |
+
68.3408,
|
| 3760 |
+
39.5359
|
| 3761 |
+
],
|
| 3762 |
+
[
|
| 3763 |
+
68.3408,
|
| 3764 |
+
41.1359
|
| 3765 |
+
],
|
| 3766 |
+
[
|
| 3767 |
+
68.3408,
|
| 3768 |
+
42.7359
|
| 3769 |
+
],
|
| 3770 |
+
[
|
| 3771 |
+
68.3408,
|
| 3772 |
+
44.3359
|
| 3773 |
+
],
|
| 3774 |
+
[
|
| 3775 |
+
68.3408,
|
| 3776 |
+
58.7359
|
| 3777 |
+
],
|
| 3778 |
+
[
|
| 3779 |
+
68.3408,
|
| 3780 |
+
60.3359
|
| 3781 |
+
],
|
| 3782 |
+
[
|
| 3783 |
+
68.3408,
|
| 3784 |
+
61.9359
|
| 3785 |
+
],
|
| 3786 |
+
[
|
| 3787 |
+
68.3408,
|
| 3788 |
+
66.7359
|
| 3789 |
+
],
|
| 3790 |
+
[
|
| 3791 |
+
68.3408,
|
| 3792 |
+
68.3359
|
| 3793 |
+
],
|
| 3794 |
+
[
|
| 3795 |
+
69.9408,
|
| 3796 |
+
9.1359
|
| 3797 |
+
],
|
| 3798 |
+
[
|
| 3799 |
+
69.9408,
|
| 3800 |
+
10.7359
|
| 3801 |
+
],
|
| 3802 |
+
[
|
| 3803 |
+
69.9408,
|
| 3804 |
+
12.3359
|
| 3805 |
+
],
|
| 3806 |
+
[
|
| 3807 |
+
69.9408,
|
| 3808 |
+
13.9359
|
| 3809 |
+
],
|
| 3810 |
+
[
|
| 3811 |
+
69.9408,
|
| 3812 |
+
15.5359
|
| 3813 |
+
],
|
| 3814 |
+
[
|
| 3815 |
+
69.9408,
|
| 3816 |
+
17.1359
|
| 3817 |
+
],
|
| 3818 |
+
[
|
| 3819 |
+
69.9408,
|
| 3820 |
+
18.7359
|
| 3821 |
+
],
|
| 3822 |
+
[
|
| 3823 |
+
69.9408,
|
| 3824 |
+
20.3359
|
| 3825 |
+
],
|
| 3826 |
+
[
|
| 3827 |
+
69.9408,
|
| 3828 |
+
21.9359
|
| 3829 |
+
],
|
| 3830 |
+
[
|
| 3831 |
+
69.9408,
|
| 3832 |
+
23.5359
|
| 3833 |
+
],
|
| 3834 |
+
[
|
| 3835 |
+
69.9408,
|
| 3836 |
+
25.1359
|
| 3837 |
+
],
|
| 3838 |
+
[
|
| 3839 |
+
69.9408,
|
| 3840 |
+
31.5359
|
| 3841 |
+
],
|
| 3842 |
+
[
|
| 3843 |
+
69.9408,
|
| 3844 |
+
33.1359
|
| 3845 |
+
],
|
| 3846 |
+
[
|
| 3847 |
+
69.9408,
|
| 3848 |
+
34.7359
|
| 3849 |
+
],
|
| 3850 |
+
[
|
| 3851 |
+
69.9408,
|
| 3852 |
+
36.3359
|
| 3853 |
+
],
|
| 3854 |
+
[
|
| 3855 |
+
69.9408,
|
| 3856 |
+
37.9359
|
| 3857 |
+
],
|
| 3858 |
+
[
|
| 3859 |
+
69.9408,
|
| 3860 |
+
39.5359
|
| 3861 |
+
],
|
| 3862 |
+
[
|
| 3863 |
+
69.9408,
|
| 3864 |
+
41.1359
|
| 3865 |
+
],
|
| 3866 |
+
[
|
| 3867 |
+
69.9408,
|
| 3868 |
+
42.7359
|
| 3869 |
+
],
|
| 3870 |
+
[
|
| 3871 |
+
69.9408,
|
| 3872 |
+
44.3359
|
| 3873 |
+
],
|
| 3874 |
+
[
|
| 3875 |
+
69.9408,
|
| 3876 |
+
60.3359
|
| 3877 |
+
],
|
| 3878 |
+
[
|
| 3879 |
+
69.9408,
|
| 3880 |
+
61.9359
|
| 3881 |
+
],
|
| 3882 |
+
[
|
| 3883 |
+
69.9408,
|
| 3884 |
+
63.5359
|
| 3885 |
+
],
|
| 3886 |
+
[
|
| 3887 |
+
71.5408,
|
| 3888 |
+
7.5359
|
| 3889 |
+
],
|
| 3890 |
+
[
|
| 3891 |
+
71.5408,
|
| 3892 |
+
9.1359
|
| 3893 |
+
],
|
| 3894 |
+
[
|
| 3895 |
+
71.5408,
|
| 3896 |
+
10.7359
|
| 3897 |
+
],
|
| 3898 |
+
[
|
| 3899 |
+
71.5408,
|
| 3900 |
+
12.3359
|
| 3901 |
+
],
|
| 3902 |
+
[
|
| 3903 |
+
71.5408,
|
| 3904 |
+
13.9359
|
| 3905 |
+
],
|
| 3906 |
+
[
|
| 3907 |
+
71.5408,
|
| 3908 |
+
18.7359
|
| 3909 |
+
],
|
| 3910 |
+
[
|
| 3911 |
+
71.5408,
|
| 3912 |
+
20.3359
|
| 3913 |
+
],
|
| 3914 |
+
[
|
| 3915 |
+
71.5408,
|
| 3916 |
+
21.9359
|
| 3917 |
+
],
|
| 3918 |
+
[
|
| 3919 |
+
71.5408,
|
| 3920 |
+
23.5359
|
| 3921 |
+
],
|
| 3922 |
+
[
|
| 3923 |
+
71.5408,
|
| 3924 |
+
25.1359
|
| 3925 |
+
],
|
| 3926 |
+
[
|
| 3927 |
+
71.5408,
|
| 3928 |
+
29.9359
|
| 3929 |
+
],
|
| 3930 |
+
[
|
| 3931 |
+
71.5408,
|
| 3932 |
+
31.5359
|
| 3933 |
+
],
|
| 3934 |
+
[
|
| 3935 |
+
71.5408,
|
| 3936 |
+
34.7359
|
| 3937 |
+
],
|
| 3938 |
+
[
|
| 3939 |
+
71.5408,
|
| 3940 |
+
36.3359
|
| 3941 |
+
],
|
| 3942 |
+
[
|
| 3943 |
+
71.5408,
|
| 3944 |
+
37.9359
|
| 3945 |
+
],
|
| 3946 |
+
[
|
| 3947 |
+
71.5408,
|
| 3948 |
+
39.5359
|
| 3949 |
+
],
|
| 3950 |
+
[
|
| 3951 |
+
71.5408,
|
| 3952 |
+
41.1359
|
| 3953 |
+
],
|
| 3954 |
+
[
|
| 3955 |
+
71.5408,
|
| 3956 |
+
42.7359
|
| 3957 |
+
],
|
| 3958 |
+
[
|
| 3959 |
+
71.5408,
|
| 3960 |
+
44.3359
|
| 3961 |
+
],
|
| 3962 |
+
[
|
| 3963 |
+
71.5408,
|
| 3964 |
+
58.7359
|
| 3965 |
+
],
|
| 3966 |
+
[
|
| 3967 |
+
71.5408,
|
| 3968 |
+
60.3359
|
| 3969 |
+
],
|
| 3970 |
+
[
|
| 3971 |
+
71.5408,
|
| 3972 |
+
61.9359
|
| 3973 |
+
],
|
| 3974 |
+
[
|
| 3975 |
+
71.5408,
|
| 3976 |
+
63.5359
|
| 3977 |
+
],
|
| 3978 |
+
[
|
| 3979 |
+
73.1408,
|
| 3980 |
+
7.5359
|
| 3981 |
+
],
|
| 3982 |
+
[
|
| 3983 |
+
73.1408,
|
| 3984 |
+
9.1359
|
| 3985 |
+
],
|
| 3986 |
+
[
|
| 3987 |
+
73.1408,
|
| 3988 |
+
10.7359
|
| 3989 |
+
],
|
| 3990 |
+
[
|
| 3991 |
+
73.1408,
|
| 3992 |
+
12.3359
|
| 3993 |
+
],
|
| 3994 |
+
[
|
| 3995 |
+
73.1408,
|
| 3996 |
+
21.9359
|
| 3997 |
+
],
|
| 3998 |
+
[
|
| 3999 |
+
73.1408,
|
| 4000 |
+
23.5359
|
| 4001 |
+
],
|
| 4002 |
+
[
|
| 4003 |
+
73.1408,
|
| 4004 |
+
25.1359
|
| 4005 |
+
],
|
| 4006 |
+
[
|
| 4007 |
+
73.1408,
|
| 4008 |
+
29.9359
|
| 4009 |
+
],
|
| 4010 |
+
[
|
| 4011 |
+
73.1408,
|
| 4012 |
+
34.7359
|
| 4013 |
+
],
|
| 4014 |
+
[
|
| 4015 |
+
73.1408,
|
| 4016 |
+
36.3359
|
| 4017 |
+
],
|
| 4018 |
+
[
|
| 4019 |
+
73.1408,
|
| 4020 |
+
37.9359
|
| 4021 |
+
],
|
| 4022 |
+
[
|
| 4023 |
+
73.1408,
|
| 4024 |
+
39.5359
|
| 4025 |
+
],
|
| 4026 |
+
[
|
| 4027 |
+
73.1408,
|
| 4028 |
+
41.1359
|
| 4029 |
+
],
|
| 4030 |
+
[
|
| 4031 |
+
73.1408,
|
| 4032 |
+
42.7359
|
| 4033 |
+
],
|
| 4034 |
+
[
|
| 4035 |
+
73.1408,
|
| 4036 |
+
44.3359
|
| 4037 |
+
],
|
| 4038 |
+
[
|
| 4039 |
+
73.1408,
|
| 4040 |
+
58.7359
|
| 4041 |
+
],
|
| 4042 |
+
[
|
| 4043 |
+
73.1408,
|
| 4044 |
+
60.3359
|
| 4045 |
+
],
|
| 4046 |
+
[
|
| 4047 |
+
74.7408,
|
| 4048 |
+
7.5359
|
| 4049 |
+
],
|
| 4050 |
+
[
|
| 4051 |
+
74.7408,
|
| 4052 |
+
9.1359
|
| 4053 |
+
],
|
| 4054 |
+
[
|
| 4055 |
+
74.7408,
|
| 4056 |
+
10.7359
|
| 4057 |
+
],
|
| 4058 |
+
[
|
| 4059 |
+
74.7408,
|
| 4060 |
+
12.3359
|
| 4061 |
+
],
|
| 4062 |
+
[
|
| 4063 |
+
74.7408,
|
| 4064 |
+
21.9359
|
| 4065 |
+
],
|
| 4066 |
+
[
|
| 4067 |
+
74.7408,
|
| 4068 |
+
23.5359
|
| 4069 |
+
],
|
| 4070 |
+
[
|
| 4071 |
+
74.7408,
|
| 4072 |
+
25.1359
|
| 4073 |
+
],
|
| 4074 |
+
[
|
| 4075 |
+
74.7408,
|
| 4076 |
+
29.9359
|
| 4077 |
+
],
|
| 4078 |
+
[
|
| 4079 |
+
74.7408,
|
| 4080 |
+
34.7359
|
| 4081 |
+
],
|
| 4082 |
+
[
|
| 4083 |
+
74.7408,
|
| 4084 |
+
36.3359
|
| 4085 |
+
],
|
| 4086 |
+
[
|
| 4087 |
+
74.7408,
|
| 4088 |
+
37.9359
|
| 4089 |
+
],
|
| 4090 |
+
[
|
| 4091 |
+
74.7408,
|
| 4092 |
+
39.5359
|
| 4093 |
+
],
|
| 4094 |
+
[
|
| 4095 |
+
74.7408,
|
| 4096 |
+
41.1359
|
| 4097 |
+
],
|
| 4098 |
+
[
|
| 4099 |
+
74.7408,
|
| 4100 |
+
42.7359
|
| 4101 |
+
],
|
| 4102 |
+
[
|
| 4103 |
+
74.7408,
|
| 4104 |
+
44.3359
|
| 4105 |
+
],
|
| 4106 |
+
[
|
| 4107 |
+
74.7408,
|
| 4108 |
+
58.7359
|
| 4109 |
+
],
|
| 4110 |
+
[
|
| 4111 |
+
74.7408,
|
| 4112 |
+
60.3359
|
| 4113 |
+
],
|
| 4114 |
+
[
|
| 4115 |
+
74.7408,
|
| 4116 |
+
61.9359
|
| 4117 |
+
],
|
| 4118 |
+
[
|
| 4119 |
+
76.3408,
|
| 4120 |
+
9.1359
|
| 4121 |
+
],
|
| 4122 |
+
[
|
| 4123 |
+
76.3408,
|
| 4124 |
+
10.7359
|
| 4125 |
+
],
|
| 4126 |
+
[
|
| 4127 |
+
76.3408,
|
| 4128 |
+
21.9359
|
| 4129 |
+
],
|
| 4130 |
+
[
|
| 4131 |
+
76.3408,
|
| 4132 |
+
23.5359
|
| 4133 |
+
],
|
| 4134 |
+
[
|
| 4135 |
+
76.3408,
|
| 4136 |
+
25.1359
|
| 4137 |
+
],
|
| 4138 |
+
[
|
| 4139 |
+
76.3408,
|
| 4140 |
+
29.9359
|
| 4141 |
+
],
|
| 4142 |
+
[
|
| 4143 |
+
76.3408,
|
| 4144 |
+
34.7359
|
| 4145 |
+
],
|
| 4146 |
+
[
|
| 4147 |
+
76.3408,
|
| 4148 |
+
36.3359
|
| 4149 |
+
],
|
| 4150 |
+
[
|
| 4151 |
+
76.3408,
|
| 4152 |
+
37.9359
|
| 4153 |
+
],
|
| 4154 |
+
[
|
| 4155 |
+
76.3408,
|
| 4156 |
+
39.5359
|
| 4157 |
+
],
|
| 4158 |
+
[
|
| 4159 |
+
76.3408,
|
| 4160 |
+
41.1359
|
| 4161 |
+
],
|
| 4162 |
+
[
|
| 4163 |
+
76.3408,
|
| 4164 |
+
44.3359
|
| 4165 |
+
],
|
| 4166 |
+
[
|
| 4167 |
+
77.9408,
|
| 4168 |
+
20.3359
|
| 4169 |
+
],
|
| 4170 |
+
[
|
| 4171 |
+
77.9408,
|
| 4172 |
+
21.9359
|
| 4173 |
+
],
|
| 4174 |
+
[
|
| 4175 |
+
77.9408,
|
| 4176 |
+
23.5359
|
| 4177 |
+
],
|
| 4178 |
+
[
|
| 4179 |
+
77.9408,
|
| 4180 |
+
25.1359
|
| 4181 |
+
],
|
| 4182 |
+
[
|
| 4183 |
+
77.9408,
|
| 4184 |
+
36.3359
|
| 4185 |
+
],
|
| 4186 |
+
[
|
| 4187 |
+
77.9408,
|
| 4188 |
+
37.9359
|
| 4189 |
+
],
|
| 4190 |
+
[
|
| 4191 |
+
77.9408,
|
| 4192 |
+
39.5359
|
| 4193 |
+
],
|
| 4194 |
+
[
|
| 4195 |
+
77.9408,
|
| 4196 |
+
44.3359
|
| 4197 |
+
],
|
| 4198 |
+
[
|
| 4199 |
+
79.5408,
|
| 4200 |
+
20.3359
|
| 4201 |
+
],
|
| 4202 |
+
[
|
| 4203 |
+
79.5408,
|
| 4204 |
+
21.9359
|
| 4205 |
+
],
|
| 4206 |
+
[
|
| 4207 |
+
79.5408,
|
| 4208 |
+
23.5359
|
| 4209 |
+
]
|
| 4210 |
+
]
|
| 4211 |
+
}
|
backend/static/game_positions.json
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"starting_positions": [
|
| 3 |
+
{
|
| 4 |
+
"x": 25.57603645324707,
|
| 5 |
+
"y": 84.8847885131836,
|
| 6 |
+
"minerals": [
|
| 7 |
+
{
|
| 8 |
+
"x": 19,
|
| 9 |
+
"y": 67
|
| 10 |
+
},
|
| 11 |
+
{
|
| 12 |
+
"x": 21,
|
| 13 |
+
"y": 67
|
| 14 |
+
},
|
| 15 |
+
{
|
| 16 |
+
"x": 21,
|
| 17 |
+
"y": 66
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"x": 21,
|
| 21 |
+
"y": 69
|
| 22 |
+
},
|
| 23 |
+
{
|
| 24 |
+
"x": 19,
|
| 25 |
+
"y": 70
|
| 26 |
+
},
|
| 27 |
+
{
|
| 28 |
+
"x": 21,
|
| 29 |
+
"y": 71
|
| 30 |
+
}
|
| 31 |
+
],
|
| 32 |
+
"geysers": [
|
| 33 |
+
{
|
| 34 |
+
"x": 22,
|
| 35 |
+
"y": 66
|
| 36 |
+
}
|
| 37 |
+
]
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"x": 85.71428680419922,
|
| 41 |
+
"y": 47.096771240234375,
|
| 42 |
+
"minerals": [
|
| 43 |
+
{
|
| 44 |
+
"x": 72,
|
| 45 |
+
"y": 37
|
| 46 |
+
},
|
| 47 |
+
{
|
| 48 |
+
"x": 67,
|
| 49 |
+
"y": 36
|
| 50 |
+
},
|
| 51 |
+
{
|
| 52 |
+
"x": 68,
|
| 53 |
+
"y": 41
|
| 54 |
+
},
|
| 55 |
+
{
|
| 56 |
+
"x": 70,
|
| 57 |
+
"y": 37
|
| 58 |
+
},
|
| 59 |
+
{
|
| 60 |
+
"x": 66,
|
| 61 |
+
"y": 39
|
| 62 |
+
},
|
| 63 |
+
{
|
| 64 |
+
"x": 69,
|
| 65 |
+
"y": 41
|
| 66 |
+
},
|
| 67 |
+
{
|
| 68 |
+
"x": 70,
|
| 69 |
+
"y": 36
|
| 70 |
+
}
|
| 71 |
+
],
|
| 72 |
+
"geysers": [
|
| 73 |
+
{
|
| 74 |
+
"x": 66,
|
| 75 |
+
"y": 37
|
| 76 |
+
}
|
| 77 |
+
]
|
| 78 |
+
},
|
| 79 |
+
{
|
| 80 |
+
"x": 24.654376983642578,
|
| 81 |
+
"y": 13.80184268951416,
|
| 82 |
+
"minerals": [
|
| 83 |
+
{
|
| 84 |
+
"x": 21,
|
| 85 |
+
"y": 8
|
| 86 |
+
},
|
| 87 |
+
{
|
| 88 |
+
"x": 22,
|
| 89 |
+
"y": 9
|
| 90 |
+
},
|
| 91 |
+
{
|
| 92 |
+
"x": 19,
|
| 93 |
+
"y": 10
|
| 94 |
+
},
|
| 95 |
+
{
|
| 96 |
+
"x": 21,
|
| 97 |
+
"y": 10
|
| 98 |
+
},
|
| 99 |
+
{
|
| 100 |
+
"x": 20,
|
| 101 |
+
"y": 13
|
| 102 |
+
},
|
| 103 |
+
{
|
| 104 |
+
"x": 22,
|
| 105 |
+
"y": 11
|
| 106 |
+
},
|
| 107 |
+
{
|
| 108 |
+
"x": 18,
|
| 109 |
+
"y": 12
|
| 110 |
+
}
|
| 111 |
+
],
|
| 112 |
+
"geysers": [
|
| 113 |
+
{
|
| 114 |
+
"x": 22,
|
| 115 |
+
"y": 13
|
| 116 |
+
}
|
| 117 |
+
]
|
| 118 |
+
}
|
| 119 |
+
],
|
| 120 |
+
"expansion_positions": [
|
| 121 |
+
{
|
| 122 |
+
"x": 81.10598754882812,
|
| 123 |
+
"y": 17.373271942138672,
|
| 124 |
+
"minerals": [
|
| 125 |
+
{
|
| 126 |
+
"x": 64,
|
| 127 |
+
"y": 11
|
| 128 |
+
},
|
| 129 |
+
{
|
| 130 |
+
"x": 66,
|
| 131 |
+
"y": 15
|
| 132 |
+
},
|
| 133 |
+
{
|
| 134 |
+
"x": 65,
|
| 135 |
+
"y": 16
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"x": 66,
|
| 139 |
+
"y": 14
|
| 140 |
+
},
|
| 141 |
+
{
|
| 142 |
+
"x": 65,
|
| 143 |
+
"y": 15
|
| 144 |
+
},
|
| 145 |
+
{
|
| 146 |
+
"x": 63,
|
| 147 |
+
"y": 16
|
| 148 |
+
},
|
| 149 |
+
{
|
| 150 |
+
"x": 67,
|
| 151 |
+
"y": 13
|
| 152 |
+
}
|
| 153 |
+
],
|
| 154 |
+
"geysers": [
|
| 155 |
+
{
|
| 156 |
+
"x": 67,
|
| 157 |
+
"y": 10
|
| 158 |
+
}
|
| 159 |
+
]
|
| 160 |
+
},
|
| 161 |
+
{
|
| 162 |
+
"x": 54.838706970214844,
|
| 163 |
+
"y": 61.49769592285156,
|
| 164 |
+
"minerals": [
|
| 165 |
+
{
|
| 166 |
+
"x": 42,
|
| 167 |
+
"y": 49
|
| 168 |
+
},
|
| 169 |
+
{
|
| 170 |
+
"x": 45,
|
| 171 |
+
"y": 52
|
| 172 |
+
},
|
| 173 |
+
{
|
| 174 |
+
"x": 44,
|
| 175 |
+
"y": 48
|
| 176 |
+
},
|
| 177 |
+
{
|
| 178 |
+
"x": 44,
|
| 179 |
+
"y": 50
|
| 180 |
+
},
|
| 181 |
+
{
|
| 182 |
+
"x": 45,
|
| 183 |
+
"y": 48
|
| 184 |
+
},
|
| 185 |
+
{
|
| 186 |
+
"x": 44,
|
| 187 |
+
"y": 46
|
| 188 |
+
},
|
| 189 |
+
{
|
| 190 |
+
"x": 43,
|
| 191 |
+
"y": 47
|
| 192 |
+
}
|
| 193 |
+
],
|
| 194 |
+
"geysers": [
|
| 195 |
+
{
|
| 196 |
+
"x": 44,
|
| 197 |
+
"y": 53
|
| 198 |
+
}
|
| 199 |
+
]
|
| 200 |
+
},
|
| 201 |
+
{
|
| 202 |
+
"x": 76.61289978027344,
|
| 203 |
+
"y": 83.38709259033203,
|
| 204 |
+
"minerals": [
|
| 205 |
+
{
|
| 206 |
+
"x": 61,
|
| 207 |
+
"y": 68
|
| 208 |
+
},
|
| 209 |
+
{
|
| 210 |
+
"x": 61,
|
| 211 |
+
"y": 70
|
| 212 |
+
},
|
| 213 |
+
{
|
| 214 |
+
"x": 61,
|
| 215 |
+
"y": 69
|
| 216 |
+
},
|
| 217 |
+
{
|
| 218 |
+
"x": 59,
|
| 219 |
+
"y": 65
|
| 220 |
+
},
|
| 221 |
+
{
|
| 222 |
+
"x": 62,
|
| 223 |
+
"y": 64
|
| 224 |
+
},
|
| 225 |
+
{
|
| 226 |
+
"x": 60,
|
| 227 |
+
"y": 66
|
| 228 |
+
},
|
| 229 |
+
{
|
| 230 |
+
"x": 60,
|
| 231 |
+
"y": 70
|
| 232 |
+
}
|
| 233 |
+
],
|
| 234 |
+
"geysers": [
|
| 235 |
+
{
|
| 236 |
+
"x": 65,
|
| 237 |
+
"y": 66
|
| 238 |
+
}
|
| 239 |
+
]
|
| 240 |
+
},
|
| 241 |
+
{
|
| 242 |
+
"x": 20.967741012573242,
|
| 243 |
+
"y": 32.926265716552734,
|
| 244 |
+
"minerals": [
|
| 245 |
+
{
|
| 246 |
+
"x": 19,
|
| 247 |
+
"y": 25
|
| 248 |
+
},
|
| 249 |
+
{
|
| 250 |
+
"x": 18,
|
| 251 |
+
"y": 27
|
| 252 |
+
},
|
| 253 |
+
{
|
| 254 |
+
"x": 20,
|
| 255 |
+
"y": 26
|
| 256 |
+
},
|
| 257 |
+
{
|
| 258 |
+
"x": 16,
|
| 259 |
+
"y": 26
|
| 260 |
+
},
|
| 261 |
+
{
|
| 262 |
+
"x": 14,
|
| 263 |
+
"y": 27
|
| 264 |
+
},
|
| 265 |
+
{
|
| 266 |
+
"x": 18,
|
| 267 |
+
"y": 28
|
| 268 |
+
},
|
| 269 |
+
{
|
| 270 |
+
"x": 17,
|
| 271 |
+
"y": 23
|
| 272 |
+
}
|
| 273 |
+
],
|
| 274 |
+
"geysers": [
|
| 275 |
+
{
|
| 276 |
+
"x": 17,
|
| 277 |
+
"y": 23
|
| 278 |
+
}
|
| 279 |
+
]
|
| 280 |
+
},
|
| 281 |
+
{
|
| 282 |
+
"x": 67.97235107421875,
|
| 283 |
+
"y": 36.036865234375,
|
| 284 |
+
"minerals": [
|
| 285 |
+
{
|
| 286 |
+
"x": 56,
|
| 287 |
+
"y": 31
|
| 288 |
+
},
|
| 289 |
+
{
|
| 290 |
+
"x": 57,
|
| 291 |
+
"y": 29
|
| 292 |
+
},
|
| 293 |
+
{
|
| 294 |
+
"x": 54,
|
| 295 |
+
"y": 28
|
| 296 |
+
},
|
| 297 |
+
{
|
| 298 |
+
"x": 52,
|
| 299 |
+
"y": 30
|
| 300 |
+
},
|
| 301 |
+
{
|
| 302 |
+
"x": 54,
|
| 303 |
+
"y": 29
|
| 304 |
+
},
|
| 305 |
+
{
|
| 306 |
+
"x": 52,
|
| 307 |
+
"y": 27
|
| 308 |
+
},
|
| 309 |
+
{
|
| 310 |
+
"x": 53,
|
| 311 |
+
"y": 31
|
| 312 |
+
}
|
| 313 |
+
],
|
| 314 |
+
"geysers": [
|
| 315 |
+
{
|
| 316 |
+
"x": 53,
|
| 317 |
+
"y": 33
|
| 318 |
+
}
|
| 319 |
+
]
|
| 320 |
+
},
|
| 321 |
+
{
|
| 322 |
+
"x": 16.705068588256836,
|
| 323 |
+
"y": 57.926265716552734,
|
| 324 |
+
"minerals": [
|
| 325 |
+
{
|
| 326 |
+
"x": 16,
|
| 327 |
+
"y": 46
|
| 328 |
+
},
|
| 329 |
+
{
|
| 330 |
+
"x": 15,
|
| 331 |
+
"y": 47
|
| 332 |
+
},
|
| 333 |
+
{
|
| 334 |
+
"x": 14,
|
| 335 |
+
"y": 46
|
| 336 |
+
},
|
| 337 |
+
{
|
| 338 |
+
"x": 14,
|
| 339 |
+
"y": 48
|
| 340 |
+
},
|
| 341 |
+
{
|
| 342 |
+
"x": 12,
|
| 343 |
+
"y": 49
|
| 344 |
+
},
|
| 345 |
+
{
|
| 346 |
+
"x": 10,
|
| 347 |
+
"y": 45
|
| 348 |
+
},
|
| 349 |
+
{
|
| 350 |
+
"x": 15,
|
| 351 |
+
"y": 44
|
| 352 |
+
}
|
| 353 |
+
],
|
| 354 |
+
"geysers": [
|
| 355 |
+
{
|
| 356 |
+
"x": 15,
|
| 357 |
+
"y": 43
|
| 358 |
+
}
|
| 359 |
+
]
|
| 360 |
+
}
|
| 361 |
+
]
|
| 362 |
+
}
|
backend/static/sprites/buildings/armory.png
ADDED
|
|
Git LFS Details
|
backend/static/sprites/buildings/barracks.png
ADDED
|
|
Git LFS Details
|
backend/static/sprites/buildings/command_center.png
CHANGED
|
|
Git LFS Details
|
|
|
Git LFS Details
|
backend/static/sprites/buildings/engineering_bay.png
ADDED
|
|
Git LFS Details
|
backend/static/sprites/buildings/factory.png
ADDED
|
|
Git LFS Details
|
backend/static/sprites/buildings/refinery.png
ADDED
|
|
Git LFS Details
|
backend/static/sprites/buildings/starport.png
ADDED
|
|
Git LFS Details
|
backend/static/sprites/buildings/supply_depot.png
ADDED
|
|
Git LFS Details
|
backend/static/sprites/icons/gas.png
ADDED
|
|
Git LFS Details
|
backend/static/sprites/icons/mineral.png
ADDED
|
|
Git LFS Details
|
backend/static/sprites/icons/supply.png
ADDED
|
|
Git LFS Details
|
backend/static/sprites/resources/geyser.png
ADDED
|
|
Git LFS Details
|
backend/static/sprites/resources/mineral.png
ADDED
|
|
Git LFS Details
|
backend/static/sprites/units/goliath.png
ADDED
|
|
Git LFS Details
|
backend/static/sprites/units/marine.png
CHANGED
|
|
Git LFS Details
|
|
|
Git LFS Details
|
backend/static/sprites/units/medic.png
ADDED
|
|
Git LFS Details
|
backend/static/sprites/units/scv.png
CHANGED
|
|
Git LFS Details
|
|
|
Git LFS Details
|
backend/static/sprites/units/tank.png
ADDED
|
|
Git LFS Details
|
backend/static/sprites/units/wraith.png
ADDED
|
|
Git LFS Details
|
backend/static/walkable.json
CHANGED
|
@@ -356,12 +356,12 @@
|
|
| 356 |
12.373571395874023
|
| 357 |
],
|
| 358 |
[
|
| 359 |
-
|
| 360 |
-
|
| 361 |
],
|
| 362 |
[
|
| 363 |
-
|
| 364 |
-
|
| 365 |
],
|
| 366 |
[
|
| 367 |
91.03219604492188,
|
|
@@ -380,8 +380,8 @@
|
|
| 380 |
20.17096710205078
|
| 381 |
],
|
| 382 |
[
|
| 383 |
-
90.
|
| 384 |
-
22.
|
| 385 |
],
|
| 386 |
[
|
| 387 |
88.7313232421875,
|
|
@@ -392,24 +392,24 @@
|
|
| 392 |
26.690099716186523
|
| 393 |
],
|
| 394 |
[
|
| 395 |
-
|
| 396 |
-
|
| 397 |
],
|
| 398 |
[
|
| 399 |
-
|
| 400 |
-
30.
|
| 401 |
],
|
| 402 |
[
|
| 403 |
-
92.
|
| 404 |
-
|
| 405 |
],
|
| 406 |
[
|
| 407 |
-
|
| 408 |
-
31.
|
| 409 |
],
|
| 410 |
[
|
| 411 |
-
77.
|
| 412 |
-
|
| 413 |
],
|
| 414 |
[
|
| 415 |
72.62522888183594,
|
|
@@ -466,12 +466,12 @@
|
|
| 466 |
56.21794128417969
|
| 467 |
],
|
| 468 |
[
|
| 469 |
-
|
| 470 |
-
49.
|
| 471 |
],
|
| 472 |
[
|
| 473 |
-
|
| 474 |
-
|
| 475 |
],
|
| 476 |
[
|
| 477 |
16.765199661254883,
|
|
@@ -532,8 +532,8 @@
|
|
| 532 |
74.11360168457031
|
| 533 |
],
|
| 534 |
[
|
| 535 |
-
|
| 536 |
-
|
| 537 |
],
|
| 538 |
[
|
| 539 |
86.55828094482422,
|
|
@@ -564,8 +564,8 @@
|
|
| 564 |
77.30925750732422
|
| 565 |
],
|
| 566 |
[
|
| 567 |
-
88.
|
| 568 |
-
|
| 569 |
],
|
| 570 |
[
|
| 571 |
79.01653289794922,
|
|
@@ -622,8 +622,8 @@
|
|
| 622 |
65.54924774169922
|
| 623 |
],
|
| 624 |
[
|
| 625 |
-
47.
|
| 626 |
-
67.
|
| 627 |
],
|
| 628 |
[
|
| 629 |
41.69129943847656,
|
|
@@ -638,20 +638,20 @@
|
|
| 638 |
80.88838958740234
|
| 639 |
],
|
| 640 |
[
|
| 641 |
-
|
| 642 |
-
|
| 643 |
],
|
| 644 |
[
|
| 645 |
-
|
| 646 |
-
88.
|
| 647 |
],
|
| 648 |
[
|
| 649 |
51.15043640136719,
|
| 650 |
95.20491790771484
|
| 651 |
],
|
| 652 |
[
|
| 653 |
-
58.
|
| 654 |
-
94.
|
| 655 |
],
|
| 656 |
[
|
| 657 |
62.14348220825195,
|
|
@@ -730,8 +730,8 @@
|
|
| 730 |
73.98577880859375
|
| 731 |
],
|
| 732 |
[
|
| 733 |
-
43.
|
| 734 |
-
65.
|
| 735 |
]
|
| 736 |
],
|
| 737 |
[
|
|
|
|
| 356 |
12.373571395874023
|
| 357 |
],
|
| 358 |
[
|
| 359 |
+
84.77071380615234,
|
| 360 |
+
7.241750717163086
|
| 361 |
],
|
| 362 |
[
|
| 363 |
+
87.53814697265625,
|
| 364 |
+
9.529439926147461
|
| 365 |
],
|
| 366 |
[
|
| 367 |
91.03219604492188,
|
|
|
|
| 380 |
20.17096710205078
|
| 381 |
],
|
| 382 |
[
|
| 383 |
+
90.1014633178711,
|
| 384 |
+
22.626026153564453
|
| 385 |
],
|
| 386 |
[
|
| 387 |
88.7313232421875,
|
|
|
|
| 392 |
26.690099716186523
|
| 393 |
],
|
| 394 |
[
|
| 395 |
+
100,
|
| 396 |
+
24.53423309326172
|
| 397 |
],
|
| 398 |
[
|
| 399 |
+
100,
|
| 400 |
+
30.92753791809082
|
| 401 |
],
|
| 402 |
[
|
| 403 |
+
92.45945739746094,
|
| 404 |
+
32.453460693359375
|
| 405 |
],
|
| 406 |
[
|
| 407 |
+
84.401611328125,
|
| 408 |
+
31.450428009033203
|
| 409 |
],
|
| 410 |
[
|
| 411 |
+
77.79100799560547,
|
| 412 |
+
33.05105209350586
|
| 413 |
],
|
| 414 |
[
|
| 415 |
72.62522888183594,
|
|
|
|
| 466 |
56.21794128417969
|
| 467 |
],
|
| 468 |
[
|
| 469 |
+
29.418794631958008,
|
| 470 |
+
49.158748626708984
|
| 471 |
],
|
| 472 |
[
|
| 473 |
+
24.801074981689453,
|
| 474 |
+
45.830467224121094
|
| 475 |
],
|
| 476 |
[
|
| 477 |
16.765199661254883,
|
|
|
|
| 532 |
74.11360168457031
|
| 533 |
],
|
| 534 |
[
|
| 535 |
+
79.2590103149414,
|
| 536 |
+
69.9880142211914
|
| 537 |
],
|
| 538 |
[
|
| 539 |
86.55828094482422,
|
|
|
|
| 564 |
77.30925750732422
|
| 565 |
],
|
| 566 |
[
|
| 567 |
+
88.57514953613281,
|
| 568 |
+
81.07293701171875
|
| 569 |
],
|
| 570 |
[
|
| 571 |
79.01653289794922,
|
|
|
|
| 622 |
65.54924774169922
|
| 623 |
],
|
| 624 |
[
|
| 625 |
+
47.70072937011719,
|
| 626 |
+
67.28649139404297
|
| 627 |
],
|
| 628 |
[
|
| 629 |
41.69129943847656,
|
|
|
|
| 638 |
80.88838958740234
|
| 639 |
],
|
| 640 |
[
|
| 641 |
+
46.48517608642578,
|
| 642 |
+
85.19413757324219
|
| 643 |
],
|
| 644 |
[
|
| 645 |
+
41.934173583984375,
|
| 646 |
+
88.30909729003906
|
| 647 |
],
|
| 648 |
[
|
| 649 |
51.15043640136719,
|
| 650 |
95.20491790771484
|
| 651 |
],
|
| 652 |
[
|
| 653 |
+
58.30870056152344,
|
| 654 |
+
94.26060485839844
|
| 655 |
],
|
| 656 |
[
|
| 657 |
62.14348220825195,
|
|
|
|
| 730 |
73.98577880859375
|
| 731 |
],
|
| 732 |
[
|
| 733 |
+
43.087799072265625,
|
| 734 |
+
65.17256164550781
|
| 735 |
]
|
| 736 |
],
|
| 737 |
[
|
backend/voice/command_parser.py
CHANGED
|
@@ -50,7 +50,7 @@ supply_depot, barracks, engineering_bay, refinery, factory, armory, starport
|
|
| 50 |
all, all_military, all_marines, all_medics, all_goliaths, all_tanks, all_wraiths, all_scv, idle_scv, most_damaged
|
| 51 |
|
| 52 |
=== ZONES CIBLES ===
|
| 53 |
-
my_base, enemy_base, center, top_left, top_right, bottom_left, bottom_right, front_line
|
| 54 |
|
| 55 |
=== ÉTAT ACTUEL DU JOUEUR ===
|
| 56 |
{player_state}
|
|
@@ -58,9 +58,9 @@ my_base, enemy_base, center, top_left, top_right, bottom_left, bottom_right, fro
|
|
| 58 |
=== CONSIGNES ===
|
| 59 |
- Réponds UNIQUEMENT avec un JSON valide, aucun texte avant ou après.
|
| 60 |
- Une commande peut générer PLUSIEURS actions (ex: "entraîne 4 marines et attaque la base").
|
| 61 |
-
- Le champ "
|
| 62 |
-
-
|
| 63 |
-
- Si
|
| 64 |
|
| 65 |
=== FORMAT DE RÉPONSE ===
|
| 66 |
{
|
|
@@ -72,26 +72,41 @@ my_base, enemy_base, center, top_left, top_right, bottom_left, bottom_right, fro
|
|
| 72 |
"count": <entier, défaut 1>,
|
| 73 |
"unit_selector": "<valeur ou omis>",
|
| 74 |
"target_zone": "<valeur ou omis>",
|
| 75 |
-
"resource_type": "<
|
| 76 |
"group_index": <1, 2 ou 3 pour assign_to_group, omis sinon>,
|
| 77 |
-
"unit_ids": [
|
| 78 |
}
|
| 79 |
],
|
| 80 |
-
"
|
|
|
|
| 81 |
}
|
| 82 |
"""
|
| 83 |
|
| 84 |
|
| 85 |
-
async def parse(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
"""
|
| 87 |
Send transcription + player state to Mistral and return parsed command.
|
| 88 |
Falls back to a query action if parsing fails.
|
|
|
|
|
|
|
| 89 |
"""
|
| 90 |
if not MISTRAL_API_KEY:
|
| 91 |
raise RuntimeError("MISTRAL_API_KEY not set")
|
| 92 |
|
| 93 |
client = Mistral(api_key=MISTRAL_API_KEY)
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
|
| 96 |
response = await client.chat.complete_async(
|
| 97 |
model=MISTRAL_CHAT_MODEL,
|
|
@@ -109,13 +124,64 @@ async def parse(transcription: str, player: PlayerState) -> ParsedCommand:
|
|
| 109 |
try:
|
| 110 |
data = json.loads(raw)
|
| 111 |
actions = [GameAction(**a) for a in data.get("actions", [])]
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
| 113 |
if not actions:
|
| 114 |
raise ValueError("Empty actions list")
|
| 115 |
-
return ParsedCommand(
|
|
|
|
|
|
|
| 116 |
except Exception as exc:
|
| 117 |
log.warning("Failed to parse Mistral response: %s — %s", exc, raw[:200])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 118 |
return ParsedCommand(
|
| 119 |
actions=[GameAction(type=ActionType.QUERY)],
|
| 120 |
-
|
|
|
|
| 121 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
all, all_military, all_marines, all_medics, all_goliaths, all_tanks, all_wraiths, all_scv, idle_scv, most_damaged
|
| 51 |
|
| 52 |
=== ZONES CIBLES ===
|
| 53 |
+
my_base, enemy_base, center, top_left, top_right, bottom_left, bottom_right, front_line{resource_zones}
|
| 54 |
|
| 55 |
=== ÉTAT ACTUEL DU JOUEUR ===
|
| 56 |
{player_state}
|
|
|
|
| 58 |
=== CONSIGNES ===
|
| 59 |
- Réponds UNIQUEMENT avec un JSON valide, aucun texte avant ou après.
|
| 60 |
- Une commande peut générer PLUSIEURS actions (ex: "entraîne 4 marines et attaque la base").
|
| 61 |
+
- Le champ "feedback_template" est une phrase courte DANS LA MÊME LANGUE que la commande, avec des placeholders pour les infos du jeu. Placeholders possibles: {n} (nombre d'unités), {zone}, {building}, {count}, {unit}, {resource}, {summary}, {mode}, {names}, {raw}. Ex: "Déplacement de {n} unités vers {zone}." ou "État: {summary}".
|
| 62 |
+
- Ajoute le champ "language" avec le code ISO de la langue: "fr", "en".
|
| 63 |
+
- Si la commande est incompréhensible, génère une action "query" avec feedback_template explicatif (ex: "Je n'ai pas compris. Voici ton état: {summary}").
|
| 64 |
|
| 65 |
=== FORMAT DE RÉPONSE ===
|
| 66 |
{
|
|
|
|
| 72 |
"count": <entier, défaut 1>,
|
| 73 |
"unit_selector": "<valeur ou omis>",
|
| 74 |
"target_zone": "<valeur ou omis>",
|
| 75 |
+
"resource_type": "<'minerals' ou 'gas', omis sinon>",
|
| 76 |
"group_index": <1, 2 ou 3 pour assign_to_group, omis sinon>,
|
| 77 |
+
"unit_ids": []
|
| 78 |
}
|
| 79 |
],
|
| 80 |
+
"feedback_template": "<phrase avec placeholders {n}, {zone}, etc. dans la langue du joueur>",
|
| 81 |
+
"language": "fr"
|
| 82 |
}
|
| 83 |
"""
|
| 84 |
|
| 85 |
|
| 86 |
+
async def parse(
|
| 87 |
+
transcription: str,
|
| 88 |
+
player: PlayerState,
|
| 89 |
+
resource_zones: list[str] | None = None,
|
| 90 |
+
) -> ParsedCommand:
|
| 91 |
"""
|
| 92 |
Send transcription + player state to Mistral and return parsed command.
|
| 93 |
Falls back to a query action if parsing fails.
|
| 94 |
+
resource_zones: list of zone names like ["mineral_1", ..., "geyser_1", ...]
|
| 95 |
+
sorted by proximity to the player's base.
|
| 96 |
"""
|
| 97 |
if not MISTRAL_API_KEY:
|
| 98 |
raise RuntimeError("MISTRAL_API_KEY not set")
|
| 99 |
|
| 100 |
client = Mistral(api_key=MISTRAL_API_KEY)
|
| 101 |
+
if resource_zones:
|
| 102 |
+
rz_str = ", " + ", ".join(resource_zones)
|
| 103 |
+
else:
|
| 104 |
+
rz_str = ""
|
| 105 |
+
system = (
|
| 106 |
+
_SYSTEM_PROMPT
|
| 107 |
+
.replace("{player_state}", player.summary())
|
| 108 |
+
.replace("{resource_zones}", rz_str)
|
| 109 |
+
)
|
| 110 |
|
| 111 |
response = await client.chat.complete_async(
|
| 112 |
model=MISTRAL_CHAT_MODEL,
|
|
|
|
| 124 |
try:
|
| 125 |
data = json.loads(raw)
|
| 126 |
actions = [GameAction(**a) for a in data.get("actions", [])]
|
| 127 |
+
language = (data.get("language") or "fr").strip().lower() or "fr"
|
| 128 |
+
if language not in ("fr", "en"):
|
| 129 |
+
language = "fr"
|
| 130 |
+
feedback_template = data.get("feedback_template") or data.get("feedback") or "{summary}"
|
| 131 |
if not actions:
|
| 132 |
raise ValueError("Empty actions list")
|
| 133 |
+
return ParsedCommand(
|
| 134 |
+
actions=actions, feedback_template=feedback_template, language=language
|
| 135 |
+
)
|
| 136 |
except Exception as exc:
|
| 137 |
log.warning("Failed to parse Mistral response: %s — %s", exc, raw[:200])
|
| 138 |
+
lang = _detect_language(transcription)
|
| 139 |
+
fallback_template = (
|
| 140 |
+
"I didn't understand. Here is your status: {summary}"
|
| 141 |
+
if lang == "en"
|
| 142 |
+
else "Je n'ai pas compris. Voici ton état: {summary}"
|
| 143 |
+
)
|
| 144 |
return ParsedCommand(
|
| 145 |
actions=[GameAction(type=ActionType.QUERY)],
|
| 146 |
+
feedback_template=fallback_template,
|
| 147 |
+
language=lang,
|
| 148 |
)
|
| 149 |
+
|
| 150 |
+
|
| 151 |
+
def _detect_language(text: str) -> str:
|
| 152 |
+
"""Heuristic: if transcription looks like English, return 'en', else 'fr'."""
|
| 153 |
+
if not text or not text.strip():
|
| 154 |
+
return "fr"
|
| 155 |
+
lower = text.lower().strip()
|
| 156 |
+
en_words = {
|
| 157 |
+
"build", "train", "attack", "move", "send", "create", "gather", "stop",
|
| 158 |
+
"patrol", "go", "select", "unit", "units", "base", "minerals", "gas",
|
| 159 |
+
"barracks", "factory", "scv", "marine", "tank", "all", "my", "the",
|
| 160 |
+
}
|
| 161 |
+
words = set(lower.split())
|
| 162 |
+
if words & en_words:
|
| 163 |
+
return "en"
|
| 164 |
+
return "fr"
|
| 165 |
+
|
| 166 |
+
|
| 167 |
+
async def generate_feedback(error_key: str, language: str) -> str:
|
| 168 |
+
"""
|
| 169 |
+
Call the API to generate a short feedback message for an error, in the given language.
|
| 170 |
+
error_key: e.g. "game_not_in_progress"
|
| 171 |
+
"""
|
| 172 |
+
if not MISTRAL_API_KEY:
|
| 173 |
+
return "Game is not in progress." if language == "en" else "La partie n'est pas en cours."
|
| 174 |
+
|
| 175 |
+
client = Mistral(api_key=MISTRAL_API_KEY)
|
| 176 |
+
prompt = f"""Generate exactly one short sentence for a strategy game feedback.
|
| 177 |
+
Context/error: {error_key}.
|
| 178 |
+
Language: {language}.
|
| 179 |
+
Reply with only that sentence, no quotes, no explanation."""
|
| 180 |
+
response = await client.chat.complete_async(
|
| 181 |
+
model=MISTRAL_CHAT_MODEL,
|
| 182 |
+
messages=[{"role": "user", "content": prompt}],
|
| 183 |
+
temperature=0.2,
|
| 184 |
+
max_tokens=80,
|
| 185 |
+
)
|
| 186 |
+
text = (response.choices[0].message.content or "").strip()
|
| 187 |
+
return text or ("Game is not in progress." if language == "en" else "La partie n'est pas en cours.")
|
frontend/.svelte-kit/non-ambient.d.ts
CHANGED
|
@@ -27,20 +27,21 @@ export {};
|
|
| 27 |
|
| 28 |
declare module "$app/types" {
|
| 29 |
export interface AppTypes {
|
| 30 |
-
RouteId(): "/" | "/admin" | "/admin/map" | "/admin/sounds" | "/admin/sprites" | "/game";
|
| 31 |
RouteParams(): {
|
| 32 |
|
| 33 |
};
|
| 34 |
LayoutParams(): {
|
| 35 |
"/": Record<string, never>;
|
| 36 |
"/admin": Record<string, never>;
|
|
|
|
| 37 |
"/admin/map": Record<string, never>;
|
| 38 |
"/admin/sounds": Record<string, never>;
|
| 39 |
"/admin/sprites": Record<string, never>;
|
| 40 |
"/game": Record<string, never>
|
| 41 |
};
|
| 42 |
-
Pathname(): "/" | "/admin/map" | "/admin/sounds" | "/admin/sprites" | "/game";
|
| 43 |
ResolvedPathname(): `${"" | `/${string}`}${ReturnType<AppTypes['Pathname']>}`;
|
| 44 |
-
Asset(): string & {};
|
| 45 |
}
|
| 46 |
}
|
|
|
|
| 27 |
|
| 28 |
declare module "$app/types" {
|
| 29 |
export interface AppTypes {
|
| 30 |
+
RouteId(): "/" | "/admin" | "/admin/compiled-map" | "/admin/map" | "/admin/sounds" | "/admin/sprites" | "/game";
|
| 31 |
RouteParams(): {
|
| 32 |
|
| 33 |
};
|
| 34 |
LayoutParams(): {
|
| 35 |
"/": Record<string, never>;
|
| 36 |
"/admin": Record<string, never>;
|
| 37 |
+
"/admin/compiled-map": Record<string, never>;
|
| 38 |
"/admin/map": Record<string, never>;
|
| 39 |
"/admin/sounds": Record<string, never>;
|
| 40 |
"/admin/sprites": Record<string, never>;
|
| 41 |
"/game": Record<string, never>
|
| 42 |
};
|
| 43 |
+
Pathname(): "/" | "/admin/compiled-map" | "/admin/map" | "/admin/sounds" | "/admin/sprites" | "/game";
|
| 44 |
ResolvedPathname(): `${"" | `/${string}`}${ReturnType<AppTypes['Pathname']>}`;
|
| 45 |
+
Asset(): "/logos/elevenlabs.svg" | "/logos/huggingface.svg" | "/logos/mistral.svg" | string & {};
|
| 46 |
}
|
| 47 |
}
|