from abc import ABC, abstractmethod import uuid from enum import Enum, auto class UnitType(Enum): RAT_MALE = auto() RAT_FEMALE = auto() BOMB_TIMER = auto() BOMB_NUCLEAR = auto() GAS = auto() MINE = auto() POINT = auto() EXPLOSION = auto() class Unit(ABC): """ Abstract base class for all game units. Attributes ---------- id : UUID Unique identifier for the unit. game : Game Reference to the main game object. position : tuple The current position of the unit (x, y). position_before : tuple The position of the unit before the last update. age : int The age of the unit in game ticks. speed : float Movement speed of the unit. partial_move : float Partial movement progress for smooth animation. bbox : tuple Bounding box for collision detection (x1, y1, x2, y2). stop : int Number of ticks to remain stationary. collision_layer : int Collision layer for the optimized collision system. type : UnitType The specific type of the unit. draw_last : bool Render order hint: when True, the unit is drawn after all other top-layer units so it always appears in the foreground (e.g. points). Methods ------- move() Update unit position and state (must be implemented by subclasses). draw() Render the unit on screen (must be implemented by subclasses). collisions() Handle collisions with other units (optional override). die() Remove unit from game and handle cleanup. """ # Render order hint: units with draw_on_top=True are drawn after mobile # units so they appear layered on top (e.g. gas, mines, explosions). draw_on_top = False # Render order hint: units with draw_last=True are drawn after all other # top-layer units so they always appear in the foreground. draw_last = False def __init__(self, game, position=(0, 0), id=None, collision_layer=0, unit_type=None): """Initialize a unit with game reference and position.""" self.id = id if id else uuid.uuid4() self.game = game self.position = position self.position_before = position self.age = 0 self.speed = 1.0 self.partial_move = 0 self.bbox = (0.0, 0.0, 0.0, 0.0) # Ensure it's a tuple of floats self.stop = 0 self.collision_layer = collision_layer self.type = unit_type if unit_type else UnitType.POINT # Default type @abstractmethod def move(self): """Update unit position and state. Must be implemented by subclasses.""" pass @abstractmethod def draw(self): """Render the unit on screen. Must be implemented by subclasses.""" pass def collisions(self): """Handle collisions with other units. Default implementation does nothing.""" pass def is_hidden_in_tunnel(self, image_size, position=None): """Return True when the sprite center falls inside a tunnel cell.""" if position is None: position = self.position_before x_pos = position[0] * self.game.cell_size + (self.game.cell_size - image_size[0]) // 2 y_pos = position[1] * self.game.cell_size + (self.game.cell_size - image_size[1]) // 2 center_x = int(x_pos + image_size[0] / 2) center_y = int(y_pos + image_size[1] / 2) cell_x = center_x // self.game.cell_size cell_y = center_y // self.game.cell_size if not self.game.map.in_bounds(cell_x, cell_y): return False return self.game.map.is_tunnel(cell_x, cell_y) def die(self, score=None): """Remove unit from game and handle basic cleanup.""" if self.id in self.game.units: self.game.units.pop(self.id)