import random import uuid from units import gas, rat, bomb, mine class UnitManager: def _spawnable_rat_positions(self): positions = [] for y in range(1, self.map.height - 1): for x in range(1, self.map.width - 1): if not self.map.is_empty(x, y): continue for dx, dy in [(-1, 0), (1, 0), (0, -1), (0, 1)]: nx = x + dx ny = y + dy if self.map.in_bounds(nx, ny) and self.map.is_empty(nx, ny): positions.append((x, y)) break return positions def has_weapon_at(self, position): """Check if there's a weapon (bomb, gas, mine) at the given position""" for unit in self.units.values(): if unit.position == position: # Check if it's a weapon type (not a rat or points) if isinstance(unit, (bomb.Timer, bomb.NuclearBomb, gas.Gas, mine.Mine)): return True return False def can_place_weapon_at(self, position): x, y = position if not self.map.in_bounds(x, y): return False if not self.map.is_empty(x, y): return False if self.has_weapon_at(position): return False return True def count_rats(self): count = 0 for unit in self.units.values(): if isinstance(unit, rat.Rat): count += 1 return count def spawn_gas(self, parent_id=None): if not self.can_place_weapon_at(self.pointer): return if self.ammo["gas"]["count"] <= 0: return self.ammo["gas"]["count"] -= 1 self.render_engine.play_sound("GAS.WAV") self.spawn_unit(gas.Gas, self.pointer, parent_id=parent_id) def spawn_rat(self, position=None): if position is None: position = self.choose_start() if position is None: print("[flow] spawn_rat aborted: no valid spawn position", flush=True) return print(f"[flow] spawn_rat using position={position}", flush=True) # Don't spawn rats on top of weapons if self.has_weapon_at(position): # Try nearby positions for dx, dy in [(0,1), (1,0), (0,-1), (-1,0), (1,1), (-1,-1), (1,-1), (-1,1)]: alt_pos = (position[0] + dx, position[1] + dy) if not self.map.in_bounds(alt_pos[0], alt_pos[1]): continue if self.map.is_empty(alt_pos[0], alt_pos[1]) and not self.has_weapon_at(alt_pos): position = alt_pos break else: # All nearby positions blocked, abort spawn print(f"[flow] spawn_rat aborted: weapon blocks spawn near {position}", flush=True) return rat_class = rat.Male if random.random() < 0.5 else rat.Female self.spawn_unit(rat_class, position) def spawn_bomb(self, position): if not self.can_place_weapon_at(position): return if self.ammo["bomb"]["count"] <= 0: return self.render_engine.play_sound("PUTDOWN.WAV") self.spawn_unit(bomb.Timer, position) self.ammo["bomb"]["count"] -= 1 def spawn_nuclear_bomb(self, position): """Spawn a nuclear bomb at the specified position""" if self.ammo["nuclear"]["count"] <= 0: return if not self.can_place_weapon_at(position): return self.render_engine.play_sound("NUCLEAR.WAV") self.ammo["nuclear"]["count"] -= 1 self.spawn_unit(bomb.NuclearBomb, position) def spawn_mine(self, position): if self.ammo["mine"]["count"] <= 0: return if not self.can_place_weapon_at(position): return self.render_engine.play_sound("PUTDOWN.WAV") self.ammo["mine"]["count"] -= 1 self.spawn_unit(mine.Mine, position, on_bottom=True) def spawn_unit(self, unit, position, on_bottom=False, **kwargs): id = uuid.uuid4() if on_bottom: self.units = {id: unit(self, position, id, **kwargs), **self.units} else: self.units[id] = unit(self, position, id, **kwargs) def choose_start(self): if not hasattr(self, '_valid_positions') or self._valid_positions is None: self._valid_positions = self._spawnable_rat_positions() print(f"[flow] choose_start computed {len(self._valid_positions)} spawnable cells", flush=True) if not self._valid_positions: return None return random.choice(self._valid_positions) def get_unit_by_id(self, id): return self.units.get(id) or None