## **1. Определение Целей и Задач**

### **Цели:**
- Разработка мультиагентной системы, способной к эффективной коммуникации и сотрудничеству.
- Исследование эмерджентных форм коммуникации (нейроязыка) между агентами.
- Анализ влияния структуры среды на развитие взаимодействий между агентами.

### **Задачи:**
- Создание процедурной генерации лабиринта.
- Разработка архитектуры агентов с возможностью коммуникации.
- Определение и настройка системы вознаграждений и штрафов.
- Реализация алгоритмов обучения с подкреплением для агентов.
- Анализ и визуализация поведения агентов и сформированного нейроязыка.

---

## **2. Проектирование Среды (Лабиринта)**

### **2.1. Генерация Лабиринта:**
- **Процедурная генерация:** Разработать алгоритм генерации лабиринта с параметрами, контролирующими сложность и размеры.
- **Точки встречи:** Внедрить промежуточные точки встречи для агентов, а не только в конце лабиринта, чтобы стимулировать частые взаимодействия.
- **Изменяемость окружения:** Возможность динамического изменения лабиринта во время обучения для увеличения разнообразия ситуаций.

### **2.2. Видимость и Сенсоры:**
- **Ограниченная видимость:** Определить радиус видимости или ограниченное поле зрения для агентов.
- **Сенсорные данные:** Предоставить информацию о ближайших препятствиях, дверях, партнерах и выходе.

---

## **3. Разработка Архитектуры Агентов**

### **3.1. Структура Агентов:**
- **Латентное представление:** Включить скрытые слои для хранения внутреннего состояния и размышлений агента.
- **Модули наблюдения:** Обработка входных сенсорных данных (топология лабиринта, позиция партнера и т.д.).
- **Модули действия:** Определение движений и коммуникационных сигналов.

### **3.2. Механизм Коммуникации:**
- **Аудиосигналы:** Реализовать передачу и прием аудиосигналов между агентами.
- **Кодирование/декодирование:** Разработать методику кодирования сообщений в аудиоформате и их последующей интерпретации.
- **Протокол общения:** Определить, как агенты инициируют, поддерживают и завершают обмен сообщениями.

---

## **4. Разработка Коммуникационного Протокола**

### **4.1. Формат Аудиосигналов:**
- **Фиксированные параметры:** Например, частота, длительность, амплитуда.
- **Множественные сигналы:** Возможность отправки последовательностей сигналов для передачи более сложной информации.

### **4.2. Обработка Сообщений:**
- **Декодирование:** Обучение агентов распознаванию и интерпретации полученных сигналов.
- **Ответные действия:** Реакция агентов на полученные сообщения (например, открытие дверей).

### **4.3. Эволюция Языка:**
- **Обучаемость:** Позволить системе самостоятельно развивать эффективные способы коммуникации через взаимодействие и обратную связь.
- **Адаптивность:** Обновление протокола сообщений по мере обучения агентов.

---

## **5. Определение Системы Наград и Штрафов**

### **5.1. Награды:**
- **Успешное взаимодействие:** Открытие двери по инструкции партнера.
- **Прогресс к выходу:** Награды увеличиваются по мере приближения к выходу лабиринта.

### **5.2. Штрафы:**
- **Неэффективная коммуникация:** Отправка сообщений, когда партнер находится слишком далеко.
- **Неполадки во взаимодействии:** Негативные последствия за неправильные или вредные команды.

### **5.3. Балансировка:**
- **Параметры наград и штрафов:** Экспериментальное определение оптимальных значений для стимуляции желаемого поведения.
- **Динамическая настройка:** Возможность изменения наград в зависимости от стадии обучения.

---

## **6. Реализация Алгоритмов Обучения с Подкреплением**

### **6.1. Выбор Алгоритмов:**
- **Deep Q-Networks (DQN):** Для обработки дискретных действий.
- **Proximal Policy Optimization (PPO):** Для более стабильного обучения.
- **Multi-Agent Reinforcement Learning (MARL):** Специализированные подходы для мультиагентных систем.

### **6.2. Обучающий Процесс:**
- **Эпохи и итерации:** Определение структуры обучающих эпох и шагов.
- **Совместное обучение:** Использование общего или индивидуальных наград для агентов.
- **Инициализация и разнообразие:** Начальные условия и вариативность лабиринтов для предотвращения переобучения.

### **6.3. Инфраструктура:**
- **Использование существующих фреймворков:** Например, TensorFlow, PyTorch, OpenAI Gym для облегчения разработки.
- **Параллельное обучение:** Ускорение процесса обучения путем параллельного запуска симуляций.

---

## **7. Тестирование и Отладка**

### **7.1. Валидация Коммуникации:**
- **Тестовые сценарии:** Создание разнообразных ситуаций для проверки эффективности коммуникации.
- **Мониторинг сообщений:** Анализ отправляемых и получаемых сигналов.

### **7.2. Отладка Среды:**
- **Проверка генерации лабиринтов:** Убедиться в корректности и разнообразии генерируемых лабиринтов.
- **Тестирование сенсоров:** Проверка точности и релевантности передаваемых данных агентам.

### **7.3. Анализ Производительности:**
- **Скорость обучения:** Оценка временных затрат на обучение агентов.
- **Стабильность:** Проверка на переобучение и устойчивость к изменениям среды.

---

## **8. Анализ и Визуализация Результатов**

### **8.1. Визуализация Поведения Агентов:**
- **Реалтайм визуализация:** Отображение движения агентов и их взаимодействий в лабиринте.
- **Анализ траекторий:** Изучение путей, которые проходят агенты, и места их взаимодействия.

### **8.2. Анализ Коммуникации:**
- **Визуализация нейроязыка:** Представление развивающихся коммуникационных сигналов.
- **Классификация сигналов:** Определение паттернов и их соответствие успешным взаимодействиям.

### **8.3. Метрики Успеха:**
- **Количество успешных взаимодействий.**
- **Эффективность коммуникации (скорость и точность передачи информации).**
- **Прогресс агентов к выходу из лабиринта.**

---

## **9. Постоянное Улучшение и Настройка Параметров**

### **9.1. Итеративное Обучение:**
- **Регулярное обновление модели:** Внедрение новых данных и корректировка моделей на основе результатов.
- **Эксперименты с архитектурами:** Тестирование различных архитектур агентов и коммуникационных протоколов.

### **9.2. Испытания с Разными Настройками:**
- **Разнообразие лабиринтов:** Увеличение сложности и размеров лабиринтов для улучшения общих навыков агентов.
- **Изменение системы наград:** Тестирование различных схем наград и штрафов для оптимизации поведения.

### **9.3. Внедрение Обратной Связи:**
- **Анализ неудач:** Изучение случаев неэффективной коммуникации для выявления причин и исправления.
- **Обратная связь от агентов:** Возможность предоставлять дополнительные данные или сигналы для улучшения взаимодействий.

---

## **10. Исследование и Сравнение с Существующими Подходами**

### **10.1. Литературный Обзор:**
- Изучение существующих исследований в области эмерджентной коммуникации в мультиагентных системах.
- Анализ успешных и неуспешных подходов для извлечения ценных уроков.

### **10.2. Сравнительный Анализ:**
- Сравнение вашей системы с аналогичными системами по метрикам эффективности, сложности коммуникации и скорости обучения.
- Определение уникальных преимуществ и областей для улучшения.

---

## **11. Документация и Отчетность**

### **11.1. Техническая Документация:**
- Подробное описание архитектуры системы, алгоритмов и методов коммуникации.
- Руководство по воспроизведению экспериментов и настройки среды.

### **11.2. Научные Публикации:**
- Подготовка статей для публикации результатов исследования в научных журналах или на конференциях.
- Представление выводов и обсуждение значимости результатов.

### **11.3. Визуальные Отчеты:**
- Создание графиков, диаграмм и других визуальных средств для иллюстрации поведения агентов и эффективности системы.
- Представление примеров коммуникаций и их влияния на успех взаимодействий.

---

## **12. Внедрение Дополнительных Идей**

### **12.1. Разнообразие Агентов:**
- **Разные типы агентов:** Введение агентов с различными характеристиками (например, скорость, дальность видимости) для стимулирования разнообразных взаимодействий.
- **Специализация:** Разработка агентов, специализирующихся на определенных задачах (например, навигация, коммуникация).

### **12.2. Дополнительные Каналы Коммуникации:**
- **Визуальные сигналы:** Внедрение визуального общения (например, мигание, цвета).
- **Состояния агентов:** Использование изменений состояния агента (например, положение, ориентация) для передачи информации.

### **12.3. Обучение с Частичным Наблюдением:**
- **Ограниченная информация:** Агенты имеют доступ только к части информации о лабиринте и позиции других агентов, стимулируя необходимость коммуникации для полного восприятия среды.
- **Случайные события:** Введение случайных событий в лабиринт для увеличения сложности и необходимости адаптации.

---

Почини код генерации лабиринта:
```py
import os
import random
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
import torch.nn.functional as F
import soundfile as sf
from semanticodec import SemantiCodec
from collections import deque
import matplotlib.pyplot as plt
import pygame

# Set random seeds for reproducibility
torch.manual_seed(42)
np.random.seed(42)
random.seed(42)

# Device configuration
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Ensure the messages directory exists
os.makedirs("messages", exist_ok=True)

# Initialize SemantiCodec
semanticodec = SemantiCodec(token_rate=100, semantic_vocab_size=16384)  # You can choose appropriate settings

class MazeEnvironment:
    CELL_SIZE = 40  # pixels

    WALL_COLOR = (0, 0, 0)           # Black
    FREE_COLOR = (255, 255, 255)     # White
    GOAL_COLOR = (0, 255, 0)         # Green
    AGENT_COLORS = [(0, 0, 255), (255, 0, 0)]  # Blue, Red

    def __init__(self, maze_size=(21, 21), num_agents=2, visibility_radius=3, render=False):
        self.maze_size = maze_size
        self.num_agents = num_agents
        self.visibility_radius = visibility_radius
        self.rendering = render
        if self.rendering:
            pygame.init()
            self.screen_width = self.maze_size[1] * self.CELL_SIZE
            self.screen_height = self.maze_size[0] * self.CELL_SIZE
            self.screen = pygame.display.set_mode((self.screen_width, self.screen_height))
            pygame.display.set_caption("Maze Environment")
            self.clock = pygame.time.Clock()
        self.reset()

    def reset(self):
        # Generate a random maze using recursive backtracking
        self.maze = self.generate_maze(self.maze_size[0], self.maze_size[1])
        print("Generated Maze:")
        print(self.maze)  # Debugging: Print the maze array

        # Count free cells
        free_cells = np.argwhere(self.maze == 0)
        num_free_cells = free_cells.shape[0]
        print(f"Number of free cells: {num_free_cells}")  # Debugging

        if num_free_cells < self.num_agents:
            raise Exception(f"Not enough free cells to place all agents. Free cells: {num_free_cells}, Agents: {self.num_agents}")

        # Initialize agent positions
        self.agent_positions = []
        max_attempts = 1000  # Maximum attempts to place an agent
        for agent_idx in range(self.num_agents):
            attempts = 0
            while attempts < max_attempts:
                pos = (random.randint(0, self.maze_size[0]-1), random.randint(0, self.maze_size[1]-1))
                if self.maze[pos[0]][pos[1]] == 0 and pos not in self.agent_positions:
                    self.agent_positions.append(pos)
                    print(f"Agent {agent_idx} placed at: {pos}")  # Debugging: Print agent positions
                    break
                attempts += 1
            else:
                raise Exception(f"Could not place agent {agent_idx} after {max_attempts} attempts. Not enough free cells.")
        
        # Define goal position
        self.goal_position = (self.maze_size[0]-1, self.maze_size[1]-1)
        if self.maze[self.goal_position[0]][self.goal_position[1]] == 1:
            self.maze[self.goal_position[0]][self.goal_position[1]] = 0
        print(f"Goal position set at: {self.goal_position}")  # Debugging: Print goal position

        # Verify agent positions
        for idx, pos in enumerate(self.agent_positions):
            assert self.maze[pos[0]][pos[1]] == 0, f"Agent {idx} placed on a wall at {pos}"
        
        assert self.maze[self.goal_position[0]][self.goal_position[1]] == 0, "Goal placed on a wall"

        # Initialize done flags
        self.done = False

        # Initialize communication buffers
        self.communication_buffers = [deque(maxlen=5) for _ in range(self.num_agents)]

        # Initialize rewards
        self.rewards = [0 for _ in range(self.num_agents)]

        if self.rendering:
            self.render()

        return self.get_observations()

    def generate_maze(self, width, height):
        # Recursive Backtracker Maze Generator
        maze = np.ones((width, height), dtype=int)
        stack = []

        # Starting point (ensure it's odd to guarantee carvability)
        start_x, start_y = 1, 1 
        maze[start_x][start_y] = 0
        stack.append((start_x, start_y))

        while stack:
            current = stack[-1]
            x, y = current
            directions = [(2, 0), (-2, 0), (0, 2), (0, -2)]
            random.shuffle(directions)
            carved = False
            for dx, dy in directions:
                nx, ny = x + dx, y + dy
                if 0 < nx < width - 1 and 0 < ny < height - 1 and maze[nx][ny] == 1:  # Check boundaries
                    maze[nx][ny] = 0
                    maze[x + dx // 2][y + dy // 2] = 0
                    stack.append((nx, ny))
                    carved = True
                    break
            if not carved:
                stack.pop()
        return maze

    def get_observations(self):
        observations = []
        for pos in self.agent_positions:
            obs = self.get_observation(pos)
            observations.append(obs)
        return observations

    def get_observation(self, position):
        # Create a grid centered at the agent's position with size visibility_radius*2+1
        x, y = position
        vr = self.visibility_radius
        obs = np.ones((2*vr+1, 2*vr+1), dtype=int)  # 0 - free, 1 - wall, 2 - agent, 3 - goal

        for i in range(-vr, vr+1):
            for j in range(-vr, vr+1):
                nx, ny = x + i, y + j
                if 0 <= nx < self.maze_size[0] and 0 <= ny < self.maze_size[1]:
                    obs[i+vr][j+vr] = self.maze[nx][ny]
                    # Check for agents
                    for idx, agent_pos in enumerate(self.agent_positions):
                        if agent_pos == (nx, ny):
                            obs[i+vr][j+vr] = 2
                    # Check for goal
                    if (nx, ny) == self.goal_position:
                        obs[i+vr][j+vr] = 3
        return obs.flatten()  # Flatten the observation for the neural network

    def step(self, actions, messages):
        '''
        :param actions: List of actions for each agent
                        0 - Up, 1 - Down, 2 - Left, 3 - Right, 4 - Stay, 5 - Send Message
        :param messages: List of messages (filepaths) sent by each agent
        :return: observations, rewards, dones, info
        '''
        rewards = [0 for _ in range(self.num_agents)]
        infos = {}

        # Handle messages
        for idx, message in enumerate(messages):
            if message:
                # Encode and save the message
                tokens = semanticodec.encode(message)
                tokens = torch.tensor(tokens, dtype=torch.long).to(device)
                self.communication_buffers[idx].append(tokens)
                # Implement reward/penalty for sending messages based on distance
                # For simplicity, let's assume that sending a message when another agent is within a certain range is rewarded
                sender_pos = self.agent_positions[idx]
                message_sent = False
                for other_idx, other_pos in enumerate(self.agent_positions):
                    if other_idx != idx:
                        distance = abs(sender_pos[0] - other_pos[0]) + abs(sender_pos[1] - other_pos[1])
                        if distance <= self.visibility_radius:
                            rewards[idx] += 1  # Reward for sending a message within range
                            message_sent = True
                        else:
                            rewards[idx] -= 1  # Penalty for sending a message out of range
                if not message_sent:
                    rewards[idx] -= 1  # Extra penalty if no other agents are nearby
                print(f"Agent {idx} sent a message. Rewards updated: {rewards[idx]}")  # Debugging

        # Move agents
        for idx, action in enumerate(actions):
            if action < 0 or action > 5:
                continue  # Invalid action
            current_pos = self.agent_positions[idx]
            new_pos = list(current_pos)
            if action == 0:  # Up
                new_pos[0] -= 1
            elif action == 1:  # Down
                new_pos[0] += 1
            elif action == 2:  # Left
                new_pos[1] -= 1
            elif action == 3:  # Right
                new_pos[1] += 1
            elif action == 4:  # Stay
                pass
            elif action == 5:  # Send Message
                pass  # Message already handled

            # Boundary and wall checks
            if 0 <= new_pos[0] < self.maze_size[0] and 0 <= new_pos[1] < self.maze_size[1]:
                if self.maze[new_pos[0]][new_pos[1]] == 0 and tuple(new_pos) not in self.agent_positions:
                    self.agent_positions[idx] = tuple(new_pos)
                    rewards[idx] += 0.1  # Small reward for moving
                    print(f"Agent {idx} moved to {self.agent_positions[idx]}")  # Debugging: Print movement
                else:
                    print(f"Agent {idx} hit a wall or another agent at {tuple(new_pos)}")  # Debugging
            else:
                print(f"Agent {idx} tried to move out of bounds to {tuple(new_pos)}")  # Debugging

            # Check if reached the goal
            if self.agent_positions[idx] == self.goal_position:
                rewards[idx] += 10  # Reward for reaching the goal
                self.done = True
                print(f"Agent {idx} reached the goal!")  # Debugging

        # Additional reward for agents moving closer to each other
        if self.num_agents > 1:
            distance = abs(self.agent_positions[0][0] - self.agent_positions[1][0]) + \
                       abs(self.agent_positions[0][1] - self.agent_positions[1][1])
            rewards[0] += (1 / (distance + 1))  # Closer proximity yields higher reward
            rewards[1] += (1 / (distance + 1))
            print(f"Agents' distance: {distance}, Rewards updated: {rewards}")  # Debugging

        observations = self.get_observations()
        dones = [self.done for _ in range(self.num_agents)]
        info = {"distance": distance if self.num_agents > 1 else None}

        if self.rendering:
            self.render()

        return observations, rewards, dones, info

    def render(self):
        # Handle pygame events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                pygame.quit()
                exit()

        # Fill background
        self.screen.fill(self.FREE_COLOR)

        # Draw maze
        for i in range(self.maze_size[0]):
            for j in range(self.maze_size[1]):
                rect = pygame.Rect(j * self.CELL_SIZE, i * self.CELL_SIZE, self.CELL_SIZE, self.CELL_SIZE)
                if self.maze[i][j] == 1:
                    pygame.draw.rect(self.screen, self.WALL_COLOR, rect)
                else:
                    pygame.draw.rect(self.screen, self.FREE_COLOR, rect)
                pygame.draw.rect(self.screen, (200,200,200), rect, 1)  # Cell border

        # Draw goal
        goal_x, goal_y = self.goal_position
        goal_rect = pygame.Rect(goal_y * self.CELL_SIZE, goal_x * self.CELL_SIZE, self.CELL_SIZE, self.CELL_SIZE)
        pygame.draw.rect(self.screen, self.GOAL_COLOR, goal_rect)

        # Draw agents
        for idx, pos in enumerate(self.agent_positions):
            agent_color = self.AGENT_COLORS[idx % len(self.AGENT_COLORS)]
            agent_x, agent_y = pos
            center = (agent_y * self.CELL_SIZE + self.CELL_SIZE // 2, agent_x * self.CELL_SIZE + self.CELL_SIZE // 2)
            radius = self.CELL_SIZE // 3
            pygame.draw.circle(self.screen, agent_color, center, radius)

            # Optional: Draw communication lines
            for token in self.communication_buffers[idx]:
                # Example: Draw line to goal (customize as needed)
                pygame.draw.line(self.screen, (255, 165, 0), center,
                                 (goal_y * self.CELL_SIZE + self.CELL_SIZE // 2, goal_x * self.CELL_SIZE + self.CELL_SIZE // 2), 2)

        # Draw Debug Text (Optional)
        font = pygame.font.SysFont(None, 24)
        debug_text = f"Agents: {self.agent_positions}, Goal: {self.goal_position}"
        text_surface = font.render(debug_text, True, (255, 0, 0))
        self.screen.blit(text_surface, (10, 10))

        pygame.display.flip()
        self.clock.tick(10)  # Limit to 10 FPS to reduce CPU usage

# ----------------------------
# 2. Communication Network
# ----------------------------

class CommunicationNetwork(nn.Module):
    def __init__(self, input_size, hidden_size, output_size, codec):
        super(CommunicationNetwork, self).__init__()
        self.codec = codec
        self.encoder = nn.Sequential(
            nn.Linear(input_size, hidden_size),
            nn.ReLU(),
            nn.Linear(hidden_size, hidden_size),
            nn.ReLU()
        )
        self.decoder = nn.Sequential(
            nn.Linear(hidden_size, hidden_size),
            nn.ReLU(),
            nn.Linear(hidden_size, output_size),
            nn.Sigmoid()  # Assuming output is normalized
        )
    
    def encode_message(self, message_filepath):
        tokens = self.codec.encode(message_filepath)
        token_tensor = torch.tensor(tokens, dtype=torch.long).to(device)
        return token_tensor
    
    def decode_message(self, tokens, output_filepath):
        tokens = tokens.cpu().numpy().tolist()
        waveform = self.codec.decode(tokens)
        sf.write(output_filepath, waveform[0,0], 16000)
        return waveform

    def forward(self, x):
        encoded = self.encoder(x)
        decoded = self.decoder(encoded)
        return decoded
    
# ----------------------------
# 3. Agent Definition
# ----------------------------

class Agent(nn.Module):
    def __init__(self, observation_space, action_space, communication_network):
        super(Agent, self).__init__()
        self.observation_space = observation_space
        self.action_space = action_space
        self.comm_net = communication_network
        
        # Define the observation processing network
        self.obs_net = nn.Sequential(
            nn.Linear(observation_space, 128),
            nn.ReLU(),
            nn.Linear(128, 128),
            nn.ReLU()
        )
        
        # Define the policy network
        self.policy_net = nn.Sequential(
            nn.Linear(128, 128),
            nn.ReLU(),
            nn.Linear(128, action_space)
        )
        
        # Define the value network
        self.value_net = nn.Sequential(
            nn.Linear(128, 128),
            nn.ReLU(),
            nn.Linear(128, 1)
        )
    
    def forward(self, obs):
        obs_encoded = self.obs_net(obs)
        policy = self.policy_net(obs_encoded)
        value = self.value_net(obs_encoded)
        return policy, value
    
    def select_action(self, obs, epsilon=0.1):
        policy, value = self.forward(obs)
        if random.random() < epsilon:
            action = random.randint(0, self.action_space -1)
        else:
            action = torch.argmax(policy).item()
        return action
    
    def send_message(self, message_filepath):
        tokens = self.comm_net.encode_message(message_filepath)
        return tokens
    
    def receive_message(self, tokens, output_filepath):
        waveform = self.comm_net.decode_message(tokens, output_filepath)
        return waveform
    
# ----------------------------
# 4. Replay Buffer
# ----------------------------

class ReplayBuffer:
    def __init__(self, capacity=10000):
        self.buffer = deque(maxlen=capacity)
    
    def push(self, state, action, reward, next_state, done, message):
        self.buffer.append((state, action, reward, next_state, done, message))
    
    def sample(self, batch_size):
        batch = random.sample(self.buffer, batch_size)
        state, action, reward, next_state, done, message = map(np.array, zip(*batch))
        return state, action, reward, next_state, done, message
    
    def __len__(self):
        return len(self.buffer)
    
# ----------------------------
# 5. Agent Trainers
# ----------------------------

class AgentTrainer:
    def __init__(self, agents, optimizers, replay_buffers, gamma=0.99, batch_size=64):
        self.agents = agents
        self.optimizers = optimizers
        self.replay_buffers = replay_buffers
        self.gamma = gamma
        self.batch_size = batch_size
    
    def compute_loss(self, agent_idx):
        agent = self.agents[agent_idx]
        optimizer = self.optimizers[agent_idx]
        replay_buffer = self.replay_buffers[agent_idx]

        if len(replay_buffer) < self.batch_size:
            return

        state, action, reward, next_state, done, message = replay_buffer.sample(self.batch_size)

        state = torch.FloatTensor(state).to(device)
        next_state = torch.FloatTensor(next_state).to(device)
        action = torch.LongTensor(action).to(device)
        reward = torch.FloatTensor(reward).to(device)
        done = torch.FloatTensor(done).to(device)

        # Current policy and value
        policy, value = agent(state)
        action_log_probs = F.softmax(policy, dim=-1)
        selected_actions = action.unsqueeze(1)
        log_probs = torch.log(torch.gather(action_log_probs, 1, selected_actions).squeeze())

        # Compute target values
        with torch.no_grad():
            next_policy, next_value = agent(next_state)
            target_value = reward + self.gamma * next_value.squeeze() * (1 - done)

        # Compute advantage
        advantage = target_value - value.squeeze()

        # Policy loss
        policy_loss = -torch.mean(log_probs * advantage)

        # Value loss
        value_loss = F.mse_loss(value.squeeze(), target_value)

        # Total loss
        loss = policy_loss + value_loss

        # Backpropagation
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()

        # Print loss values
        print(f"Agent {agent_idx} - Policy Loss: {policy_loss.item():.4f}, Value Loss: {value_loss.item():.4f}")

    def train(self):
        for idx in range(len(self.agents)):
            self.compute_loss(idx)

# ----------------------------
# 6. Training Loop
# ----------------------------

MAZE_SIZE = (21, 21)
NUM_AGENTS = 2
VISIBILITY_RADIUS = 3
NUM_EPISODES = 1000
MAX_STEPS = 100
RENDER = True

# Initialize environment
env = MazeEnvironment(maze_size=MAZE_SIZE, num_agents=NUM_AGENTS, visibility_radius=VISIBILITY_RADIUS, render=RENDER)

# Initialize agents
agents = []
optimizers = []
replay_buffers = []
for _ in range(env.num_agents):
    comm_net = CommunicationNetwork(input_size=256, hidden_size=128, output_size=256, codec=semanticodec).to(device)
    agent = Agent(observation_space=256, action_space=6, communication_network=comm_net).to(device)
    agents.append(agent)
    optimizer = optim.Adam(agent.parameters(), lr=1e-3)
    optimizers.append(optimizer)
    replay_buffers.append(ReplayBuffer(capacity=10000))

# Initialize trainer
trainer = AgentTrainer(agents, optimizers, replay_buffers, gamma=0.99, batch_size=64)

# Training statistics
success_count = 0
reward_history = []

for episode in range(1, NUM_EPISODES+1):
    try:
        observations = env.reset()
    except Exception as e:
        print(f"Error during environment reset: {e}")
        exit(1)
    total_rewards = [0 for _ in range(env.num_agents)]
    for step in range(MAX_STEPS):
        actions = []
        messages = []

        for idx, agent in enumerate(agents):
            obs = torch.FloatTensor(observations[idx]).to(device)
            action = agent.select_action(obs, epsilon=0.1)
            actions.append(action)

            if action == 5:  # Send Message
                # For simplicity, send a predefined message
                message_text = f"Agent {idx} message at step {step}"
                message_filepath = f"messages/agent_{idx}_ep{episode}_step{step}.wav"
                # Generate a simple waveform for the message
                duration = 1.0  # seconds
                samplerate = 16000
                t = np.linspace(0, duration, int(samplerate * duration), False)
                waveform = 0.5 * np.sin(2 * np.pi * 440 * t)  # A simple sine wave tone
                sf.write(message_filepath, waveform, samplerate)
                messages.append(message_filepath)
            else:
                messages.append(None)

        # Step the environment
        next_observations, rewards, dones, info = env.step(actions, messages)

        # Store transitions in replay buffer
        for idx in range(env.num_agents):
            replay_buffers[idx].push(
                state=observations[idx],
                action=actions[idx],
                reward=rewards[idx],
                next_state=next_observations[idx],
                done=dones[idx],
                message=messages[idx]
            )
            total_rewards[idx] += rewards[idx]

        observations = next_observations

        if any(dones):
            break

        # If rendering, delay to make visualization visible
        if RENDER:
            env.clock.tick(10)  # Keep the rendering at 10 FPS

    # Training step
    trainer.train()

    # Statistics
    episode_reward = sum(total_rewards)
    reward_history.append(episode_reward)
    if sum([1 for r in total_rewards if r >=10 ]) >0:
        success_count +=1

    if episode % 100 == 0:
        avg_reward = np.mean(reward_history[-100:])
        success_rate = success_count / 100
        print(f"Episode {episode}, Average Reward: {avg_reward:.2f}, Success Rate: {success_rate*100:.2f}%")
        success_count = 0

# Plot reward history
plt.plot(reward_history)
plt.xlabel('Episode')
plt.ylabel('Total Reward')
plt.title('Training Reward History')
plt.show()

if RENDER:
    pygame.quit()
```