Spaces:
Runtime error
Runtime error
| from __future__ import annotations | |
| from enum import Enum | |
| from typing import Optional | |
| from pydantic import BaseModel, Field | |
| from .buildings import Building, BuildingStatus, BuildingType, BUILDING_DEFS | |
| from .map import GameMap, get_start_data, get_all_map_resources, get_tutorial_start_data | |
| from .pathfinding import snap_to_walkable | |
| from .units import Unit, UnitType, UNIT_DEFS, UnitStatus | |
| class GamePhase(str, Enum): | |
| LOBBY = "lobby" | |
| PLAYING = "playing" | |
| GAME_OVER = "game_over" | |
| # Control groups: 1, 2, 3 → list of unit IDs (and optionally building IDs) | |
| ControlGroups = dict[int, list[str]] | |
| class PlayerState(BaseModel): | |
| player_id: str | |
| player_name: str | |
| minerals: int = 50 | |
| gas: int = 0 | |
| supply_used: int = 0 | |
| supply_max: int = 10 | |
| units: dict[str, Unit] = Field(default_factory=dict) | |
| buildings: dict[str, Building] = Field(default_factory=dict) | |
| control_groups: ControlGroups = Field(default_factory=lambda: {1: [], 2: [], 3: []}) | |
| is_defeated: bool = False | |
| def recalculate_supply(self) -> None: | |
| self.supply_max = sum( | |
| BUILDING_DEFS[b.building_type].supply_provided | |
| for b in self.buildings.values() | |
| if b.status not in (BuildingStatus.CONSTRUCTING, BuildingStatus.DESTROYED) | |
| ) | |
| self.supply_used = sum( | |
| UNIT_DEFS[u.unit_type].supply_cost | |
| for u in self.units.values() | |
| ) | |
| def has_active(self, bt: BuildingType) -> bool: | |
| return any( | |
| b.building_type == bt | |
| and b.status not in (BuildingStatus.CONSTRUCTING, BuildingStatus.DESTROYED) | |
| for b in self.buildings.values() | |
| ) | |
| def active_buildings_of(self, bt: BuildingType) -> list[Building]: | |
| return [ | |
| b for b in self.buildings.values() | |
| if b.building_type == bt | |
| and b.status not in (BuildingStatus.CONSTRUCTING, BuildingStatus.DESTROYED) | |
| ] | |
| def units_of(self, ut: UnitType) -> list[Unit]: | |
| return [u for u in self.units.values() if u.unit_type == ut] | |
| def command_center(self) -> Optional[Building]: | |
| return next( | |
| (b for b in self.buildings.values() | |
| if b.building_type == BuildingType.COMMAND_CENTER | |
| and b.status != BuildingStatus.DESTROYED), | |
| None, | |
| ) | |
| def command_centers(self) -> list[Building]: | |
| """Return all non-destroyed command centers.""" | |
| return [ | |
| b for b in self.buildings.values() | |
| if b.building_type == BuildingType.COMMAND_CENTER | |
| and b.status != BuildingStatus.DESTROYED | |
| ] | |
| def nearest_command_center(self, x: float, y: float) -> Optional[Building]: | |
| """Return the non-destroyed command center closest to (x, y).""" | |
| ccs = self.command_centers() | |
| if not ccs: | |
| return None | |
| return min(ccs, key=lambda b: (b.x - x) ** 2 + (b.y - y) ** 2) | |
| def summary(self, lang: str = "fr") -> str: | |
| active = [ | |
| b.building_type.value for b in self.buildings.values() | |
| if b.status not in (BuildingStatus.CONSTRUCTING, BuildingStatus.DESTROYED) | |
| ] | |
| constructing = [ | |
| f"{b.building_type.value}({b.construction_ticks_remaining}t)" | |
| for b in self.buildings.values() | |
| if b.status == BuildingStatus.CONSTRUCTING | |
| ] | |
| counts: dict[str, int] = {} | |
| for u in self.units.values(): | |
| counts[u.unit_type.value] = counts.get(u.unit_type.value, 0) + 1 | |
| if (lang or "fr").lower() == "en": | |
| minerals_l, gas_l, supply_l = "Minerals", "Gas", "Supply" | |
| buildings_l, constructing_l, units_l, none_l = "Active buildings", "Under construction", "Units", "none" | |
| else: | |
| minerals_l, gas_l, supply_l = "Minéraux", "Gaz", "Supply" | |
| buildings_l, constructing_l, units_l, none_l = "Bâtiments actifs", "En construction", "Unités", "aucun" | |
| lines = [ | |
| f"{minerals_l}: {self.minerals}, {gas_l}: {self.gas}, {supply_l}: {self.supply_used}/{self.supply_max}", | |
| f"{buildings_l}: {', '.join(active) or none_l}", | |
| ] | |
| if constructing: | |
| lines.append(f"{constructing_l}: {', '.join(constructing)}") | |
| if counts: | |
| lines.append(f"{units_l}: {', '.join(f'{v} {k}' for k, v in counts.items())}") | |
| return "\n".join(lines) | |
| TUTORIAL_DUMMY_ID = "tutorial_dummy" | |
| class GameState(BaseModel): | |
| room_id: str | |
| tick: int = 0 | |
| phase: GamePhase = GamePhase.LOBBY | |
| players: dict[str, PlayerState] = Field(default_factory=dict) | |
| game_map: GameMap = Field(default_factory=GameMap.create_default) | |
| winner: Optional[str] = None | |
| is_tutorial: bool = False | |
| tutorial_target_x: Optional[float] = None | |
| tutorial_target_y: Optional[float] = None | |
| def create_tutorial( | |
| cls, | |
| room_id: str, | |
| player_id: str, | |
| player_name: str, | |
| ) -> "GameState": | |
| state = cls(room_id=room_id, phase=GamePhase.PLAYING, is_tutorial=True) | |
| p1 = PlayerState(player_id=player_id, player_name=player_name) | |
| start1, _, target = get_tutorial_start_data() | |
| state.game_map = GameMap.create_default(resources=get_all_map_resources()) | |
| state.tutorial_target_x = float(target[0]) | |
| state.tutorial_target_y = float(target[1]) | |
| cc1 = Building.create(BuildingType.COMMAND_CENTER, player_id, float(start1[0]), float(start1[1])) | |
| cc1.status = BuildingStatus.ACTIVE | |
| cc1.construction_ticks_remaining = 0 | |
| cc1.hp = float(cc1.max_hp) | |
| p1.buildings[cc1.id] = cc1 | |
| _MAP_W = 80.0 | |
| _MAP_H = 80.0 | |
| _scv_offsets = [(-2.4, 3), (-1.2, 3), (0.0, 3), (1.2, 3), (2.4, 3)] | |
| for ox, oy in _scv_offsets: | |
| raw_x = max(0.5, min(_MAP_W - 0.5, start1[0] + ox)) | |
| raw_y = max(0.5, min(_MAP_H - 0.5, start1[1] + oy)) | |
| sx, sy = snap_to_walkable(raw_x, raw_y) | |
| scv = Unit.create(UnitType.SCV, player_id, sx, sy) | |
| p1.units[scv.id] = scv | |
| p1.recalculate_supply() | |
| state.players[player_id] = p1 | |
| # Dummy opponent with two marines blocking the path to the objective | |
| dummy = PlayerState(player_id=TUTORIAL_DUMMY_ID, player_name="Training Ground") | |
| for ex, ey in [(40.0, 50.0), (43.0, 54.0)]: | |
| raw_x = max(0.5, min(_MAP_W - 0.5, ex)) | |
| raw_y = max(0.5, min(_MAP_H - 0.5, ey)) | |
| mx, my = snap_to_walkable(raw_x, raw_y) | |
| enemy_marine = Unit.create(UnitType.MARINE, TUTORIAL_DUMMY_ID, mx, my) | |
| dummy.units[enemy_marine.id] = enemy_marine | |
| state.players[TUTORIAL_DUMMY_ID] = dummy | |
| return state | |
| def create_new( | |
| cls, | |
| room_id: str, | |
| player1_id: str, | |
| player1_name: str, | |
| player2_id: str, | |
| player2_name: str, | |
| ) -> "GameState": | |
| state = cls(room_id=room_id, phase=GamePhase.PLAYING) | |
| p1 = PlayerState(player_id=player1_id, player_name=player1_name) | |
| p2 = PlayerState(player_id=player2_id, player_name=player2_name) | |
| start1, _, start2, _ = get_start_data() | |
| state.game_map = GameMap.create_default(resources=get_all_map_resources()) | |
| # Starting Command Centers (already built); x,y is center of building footprint | |
| cc1 = Building.create(BuildingType.COMMAND_CENTER, player1_id, float(start1[0]), float(start1[1])) | |
| cc1.status = BuildingStatus.ACTIVE | |
| cc1.construction_ticks_remaining = 0 | |
| cc1.hp = float(cc1.max_hp) | |
| p1.buildings[cc1.id] = cc1 | |
| cc2 = Building.create(BuildingType.COMMAND_CENTER, player2_id, float(start2[0]), float(start2[1])) | |
| cc2.status = BuildingStatus.ACTIVE | |
| cc2.construction_ticks_remaining = 0 | |
| cc2.hp = float(cc2.max_hp) | |
| p2.buildings[cc2.id] = cc2 | |
| # 5 starting SCVs per player, spread in x around CC; snap each to nearest walkable point | |
| _MAP_W = 80.0 | |
| _MAP_H = 80.0 | |
| # Spacing must be > 2*UNIT_RADIUS (1.0) to avoid collision-blocking at spawn | |
| _scv_offsets = [(-2.4, 3), (-1.2, 3), (0.0, 3), (1.2, 3), (2.4, 3)] | |
| for i, (ox, oy) in enumerate(_scv_offsets): | |
| raw1x = max(0.5, min(_MAP_W - 0.5, start1[0] + ox)) | |
| raw1y = max(0.5, min(_MAP_H - 0.5, start1[1] + oy)) | |
| sx1, sy1 = snap_to_walkable(raw1x, raw1y) | |
| scv1 = Unit.create(UnitType.SCV, player1_id, sx1, sy1) | |
| p1.units[scv1.id] = scv1 | |
| raw2x = max(0.5, min(_MAP_W - 0.5, start2[0] + ox)) | |
| raw2y = max(0.5, min(_MAP_H - 0.5, start2[1] - oy)) | |
| sx2, sy2 = snap_to_walkable(raw2x, raw2y) | |
| scv2 = Unit.create(UnitType.SCV, player2_id, sx2, sy2) | |
| p2.units[scv2.id] = scv2 | |
| p1.recalculate_supply() | |
| p2.recalculate_supply() | |
| state.players[player1_id] = p1 | |
| state.players[player2_id] = p2 | |
| return state | |
| def enemy_of(self, player_id: str) -> Optional[PlayerState]: | |
| return next((p for pid, p in self.players.items() if pid != player_id), None) | |