From 46b75d19e4f7dacaccc46a73bbd561d67c7b20c7 Mon Sep 17 00:00:00 2001 From: GLaDOS Date: Tue, 4 Nov 2025 06:15:13 +0300 Subject: [PATCH] =?UTF-8?q?=D0=97=D0=B0=D0=B3=D1=80=D1=83=D0=B7=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D0=B0=D0=B9=D0=BB=D1=8B=20=D0=B2=20=C2=AB?= =?UTF-8?q?/=C2=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ai_decision.py | 202 ++++++++++++++++++++++++++++++++++++ entity_behavior.py | 250 +++++++++++++++++++++++++++++++++++++++++++++ entity_creation.py | 140 +++++++++++++++++++++++++ input_commands.py | 92 +++++++++++++++++ input_events.py | 56 ++++++++++ 5 files changed, 740 insertions(+) create mode 100644 ai_decision.py create mode 100644 entity_behavior.py create mode 100644 entity_creation.py create mode 100644 input_commands.py create mode 100644 input_events.py diff --git a/ai_decision.py b/ai_decision.py new file mode 100644 index 0000000..38f26d1 --- /dev/null +++ b/ai_decision.py @@ -0,0 +1,202 @@ + + +import random + +from typing import List, Dict + + + +class AIDecision: + + def __init__(self, difficulty: str = "medium"): + + self.difficulty = difficulty + + self.last_decision_time = 0 + + self.decision_cooldown = self._get_decision_cooldown() + + + + def decide(self, snapshot: Dict, dt: float) -> List[Dict]: + + self.last_decision_time += dt + + if self.last_decision_time < self.decision_cooldown: + + return [] + + + + commands = [] + + commands.extend(self.plan_expand(snapshot)) + + commands.extend(self.plan_attack(snapshot)) + + commands.extend(self.plan_build(snapshot)) + + + + self.last_decision_time = 0 + + return commands + + + + def plan_expand(self, snapshot: Dict) -> List[Dict]: + + commands = [] + + resources = snapshot.get('resources', {}) + + minerals = resources.get('minerals', 0) + + + + if minerals >= 400 and random.random() < 0.3: + + commands.append({ + + 'type': 'create_entity', + + 'entity_type': 'supply_depot', + + 'position': self._find_expansion_position(snapshot) + + }) + + + + return commands + + + + def plan_attack(self, snapshot: Dict) -> List[Dict]: + + commands = [] + + entities = snapshot.get('entities', []) + + ai_units = [e for e in entities if e.get('owner') == 'ai'] + + player_units = [e for e in entities if e.get('owner') == 'player'] + + + + if len(ai_units) >= 5 and player_units and random.random() < 0.4: + + target_unit = random.choice(player_units) + + for unit in ai_units: + + if unit.get('type') == 'unit' and unit.get('state') == 'idle': + + commands.append({ + + 'type': 'attack', + + 'entity_id': unit['id'], + + 'target_id': target_unit['id'] + + }) + + + + return commands + + + + def plan_build(self, snapshot: Dict) -> List[Dict]: + + commands = [] + + resources = snapshot.get('resources', {}) + + minerals = resources.get('minerals', 0) + + entities = snapshot.get('entities', []) + + ai_units = [e for e in entities if e.get('owner') == 'ai'] + + + + if minerals >= 150 and len(ai_units) < 8 and random.random() < 0.5: + + commands.append({ + + 'type': 'create_entity', + + 'entity_type': 'worker', + + 'position': self._find_build_position(snapshot) + + }) + + + + if minerals >= 300 and random.random() < 0.2: + + commands.append({ + + 'type': 'create_entity', + + 'entity_type': 'barracks', + + 'position': self._find_build_position(snapshot) + + }) + + + + return commands + + + + def _find_expansion_position(self, snapshot: Dict) -> tuple: + + base_x = snapshot.get('ai_base_x', 100) + + base_y = snapshot.get('ai_base_y', 100) + + offset_x = random.randint(-200, 200) + + offset_y = random.randint(-200, 200) + + return (base_x + offset_x, base_y + offset_y) + + + + def _find_build_position(self, snapshot: Dict) -> tuple: + + base_x = snapshot.get('ai_base_x', 100) + + base_y = snapshot.get('ai_base_y', 100) + + offset_x = random.randint(-50, 50) + + offset_y = random.randint(-50, 50) + + return (base_x + offset_x, base_y + offset_y) + + + + def _get_decision_cooldown(self) -> float: + + if self.difficulty == "easy": + + return 3.0 + + elif self.difficulty == "medium": + + return 2.0 + + elif self.difficulty == "hard": + + return 1.0 + + else: + + return 2.0 + diff --git a/entity_behavior.py b/entity_behavior.py new file mode 100644 index 0000000..01cd885 --- /dev/null +++ b/entity_behavior.py @@ -0,0 +1,250 @@ + + +from typing import List, Dict, Tuple + +import math + + + +class EntityBehavior: + + def __init__(self): + + pass + + + + def process_behaviors(self, state, dt: float) -> List[Dict]: + + actions = [] + + if not hasattr(state, 'entities'): + + return actions + + + + for entity in state.entities: + + if entity.get('type') == 'unit': + + entity_actions = self._process_unit_behavior(state, entity, dt) + + actions.extend(entity_actions) + + elif entity.get('type') == 'building': + + entity_actions = self._process_building_behavior(state, entity, dt) + + actions.extend(entity_actions) + + + + return actions + + + + def _process_unit_behavior(self, state, entity: Dict, dt: float) -> List[Dict]: + + actions = [] + + current_state = entity.get('state', 'idle') + + + + if current_state == 'moving': + + if self._has_reached_target(entity): + + actions.append({'type': 'change_state', 'entity_id': entity['id'], 'new_state': 'idle'}) + + else: + + self._continue_movement(entity, dt) + + + + elif current_state == 'attacking': + + target_id = entity.get('target_id') + + target = self._find_entity_by_id(state, target_id) + + if not target or target.get('health', 0) <= 0: + + actions.append({'type': 'change_state', 'entity_id': entity['id'], 'new_state': 'idle'}) + + else: + + if self._is_in_attack_range(entity, target): + + actions.append({'type': 'deal_damage', 'attacker_id': entity['id'], 'target_id': target_id, 'damage': entity.get('attack_power', 10)}) + + else: + + entity['target_x'] = target['x'] + + entity['target_y'] = target['y'] + + actions.append({'type': 'change_state', 'entity_id': entity['id'], 'new_state': 'moving'}) + + + + elif current_state == 'gathering': + + target_id = entity.get('target_id') + + target = self._find_entity_by_id(state, target_id) + + if not target or target.get('type') != 'resource': + + actions.append({'type': 'change_state', 'entity_id': entity['id'], 'new_state': 'idle'}) + + else: + + if self._is_in_gather_range(entity, target): + + gather_rate = entity.get('gather_rate', 5) + + actions.append({'type': 'gather_resources', 'gatherer_id': entity['id'], 'resource_id': target_id, 'amount': gather_rate}) + + else: + + entity['target_x'] = target['x'] + + entity['target_y'] = target['y'] + + actions.append({'type': 'change_state', 'entity_id': entity['id'], 'new_state': 'moving'}) + + + + return actions + + + + def _process_building_behavior(self, state, entity: Dict, dt: float) -> List[Dict]: + + actions = [] + + if not entity.get('completed', False): + + build_progress = entity.get('build_progress', 0) + dt + + build_time = entity.get('build_time', 0) + + if build_progress >= build_time: + + actions.append({'type': 'building_completed', 'entity_id': entity['id']}) + + else: + + actions.append({'type': 'building_progress', 'entity_id': entity['id'], 'progress': build_progress}) + + + + return actions + + + + def pathfind(self, entity: Dict, target_pos: Tuple[float, float], map_tiles: List[List[Dict]]) -> List[Tuple[float, float]]: + + start_pos = (entity['x'], entity['y']) + + path = [start_pos, target_pos] + + return path + + + + def _has_reached_target(self, entity: Dict) -> bool: + + if 'target_x' not in entity or 'target_y' not in entity: + + return True + + + + dx = entity['target_x'] - entity['x'] + + dy = entity['target_y'] - entity['y'] + + distance = math.sqrt(dx*dx + dy*dy) + + + + return distance < 5.0 + + + + def _continue_movement(self, entity: Dict, dt: float) -> None: + + if 'target_x' not in entity or 'target_y' not in entity: + + return + + + + dx = entity['target_x'] - entity['x'] + + dy = entity['target_y'] - entity['y'] + + distance = math.sqrt(dx*dx + dy*dy) + + + + if distance > 0: + + speed = entity.get('speed', 50.0) + + move_dist = speed * dt + + entity['x'] += (dx / distance) * move_dist + + entity['y'] += (dy / distance) * move_dist + + + + def _is_in_attack_range(self, entity: Dict, target: Dict) -> bool: + + dx = target['x'] - entity['x'] + + dy = target['y'] - entity['y'] + + distance = math.sqrt(dx*dx + dy*dy) + + attack_range = entity.get('attack_range', 50.0) + + + + return distance <= attack_range + + + + def _is_in_gather_range(self, entity: Dict, target: Dict) -> bool: + + dx = target['x'] - entity['x'] + + dy = target['y'] - entity['y'] + + distance = math.sqrt(dx*dx + dy*dy) + + gather_range = entity.get('gather_range', 20.0) + + + + return distance <= gather_range + + + + def _find_entity_by_id(self, state, entity_id: int) -> Dict: + + if hasattr(state, 'entities'): + + for entity in state.entities: + + if entity.get('id') == entity_id: + + return entity + + return None + diff --git a/entity_creation.py b/entity_creation.py new file mode 100644 index 0000000..e5e348b --- /dev/null +++ b/entity_creation.py @@ -0,0 +1,140 @@ + + +class EntityCreation: + + def __init__(self): + + self.entity_templates = { + + 'worker': { + + 'type': 'unit', + + 'name': 'Worker', + + 'health': 100, + + 'movable': True, + + 'speed': 40.0, + + 'gather_rate': 10, + + 'build_power': 5 + + }, + + 'barracks': { + + 'type': 'building', + + 'name': 'Barracks', + + 'health': 500, + + 'movable': False, + + 'build_time': 30.0 + + }, + + 'supply_depot': { + + 'type': 'building', + + 'name': 'Supply Depot', + + 'health': 400, + + 'movable': False, + + 'build_time': 20.0 + + }, + + 'mineral_field': { + + 'type': 'resource', + + 'name': 'Mineral Field', + + 'resources': {'minerals': 1500} + + } + + } + + + + def create_entity(self, state, create_cmd: dict) -> dict: + + entity_type = create_cmd.get('entity_type') + + position = create_cmd.get('position', (0, 0)) + + + + if entity_type not in self.entity_templates: + + return None + + + + entity_def = self.entity_templates[entity_type] + + entity = self.init_entity(entity_def, position) + + + + return { + + 'type': 'add_entity', + + 'entity': entity + + } + + + + def init_entity(self, entity_def: dict, position: tuple) -> dict: + + entity = entity_def.copy() + + entity['x'] = position[0] + + entity['y'] = position[1] + + + + if entity['type'] == 'unit': + + entity['id'] = self._generate_id() + + entity['state'] = 'idle' + + + + elif entity['type'] == 'building': + + entity['id'] = self._generate_id() + + entity['build_progress'] = 0.0 + + entity['completed'] = False + + + + elif entity['type'] == 'resource': + + entity['id'] = self._generate_id() + + + + return entity + + + + def _generate_id(self) -> int: + + return id(self) + diff --git a/input_commands.py b/input_commands.py new file mode 100644 index 0000000..43aea9e --- /dev/null +++ b/input_commands.py @@ -0,0 +1,92 @@ + + +from typing import List, Dict, Tuple + + + +class InputCommands: + + def __init__(self): + + pass + + + + def generate_commands(self, events: List, snapshot: Dict) -> List[Dict]: + + commands = [] + + camera = snapshot.get('camera', {}) + + + + for event in events: + + command = self._process_event(event, camera) + + if command: + + commands.append(command) + + + + return commands + + + + def _process_event(self, event, camera: Dict) -> Dict: + + if event.type == event.MOUSEBUTTONDOWN: + + if event.button == 1: # Left click + + world_x, world_y = self.screen_to_world(event.pos[0], event.pos[1], camera) + + return {'type': 'select', 'world_pos': (world_x, world_y)} + + elif event.button == 3: # Right click + + world_x, world_y = self.screen_to_world(event.pos[0], event.pos[1], camera) + + return {'type': 'move', 'world_pos': (world_x, world_y)} + + + + elif event.type == event.KEYDOWN: + + if event.key == event.K_b: + + return {'type': 'build', 'building_type': 'barracks'} + + elif event.key == event.K_s: + + return {'type': 'build', 'building_type': 'supply_depot'} + + elif event.key == event.K_g: + + return {'type': 'gather'} + + + + return None + + + + def screen_to_world(self, x: int, y: int, camera: Dict) -> Tuple[float, float]: + + camera_x = camera.get('x', 0) + + camera_y = camera.get('y', 0) + + zoom = camera.get('zoom', 1.0) + + + + world_x = (x / zoom) + camera_x + + world_y = (y / zoom) + camera_y + + + + return world_x, world_y + diff --git a/input_events.py b/input_events.py new file mode 100644 index 0000000..a2e8369 --- /dev/null +++ b/input_events.py @@ -0,0 +1,56 @@ + + +import pygame + +from typing import List + + + +class InputEvents: + + def __init__(self): + + self.quit_requested = False + + + + def poll_events(self) -> List[pygame.event.Event]: + + raw_events = pygame.event.get() + + filtered_events = [] + + + + for event in raw_events: + + if self._handle_quit(event): + + continue + + filtered_events.append(event) + + + + return filtered_events + + + + def _handle_quit(self, event: pygame.event.Event) -> bool: + + if event.type == pygame.QUIT: + + self.quit_requested = True + + return True + + elif event.type == pygame.KEYDOWN: + + if event.key == pygame.K_ESCAPE: + + self.quit_requested = True + + return True + + return False +