```python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Hammer Editor Source Map GENERATOR 2D (Enhanced)
Generates a map with rooms and corridors using BSP, visualizes and exports it.
"""

import argparse
import json
import logging
import math
import os
import random
import sys
from dataclasses import dataclass, asdict
from typing import List, Tuple, Optional

import matplotlib.pyplot as plt
import matplotlib.patches as patches
import networkx as nx

# Configure logging
logger = logging.getLogger('MapGenerator')
logger.setLevel(logging.INFO)
handler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter('[%(levelname)s] %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)


@dataclass
class Config:
    num_rooms: int = 100
    map_width: int = 250
    map_height: int = 250
    room_min_size: int = 6
    room_max_size: int = 20
    passageway_width: int = 3
    extra_connections_factor: float = 0.6
    max_depth: int = 25
    split_ratio: float = 0.55
    random_seed: Optional[int] = None

    @staticmethod
    def load_from_file(path: str) -> 'Config':
        """Load configuration from a JSON file."""
        try:
            with open(path, 'r') as file:
                data = json.load(file)
                config = Config(**data)
                logger.info(f"Configuration loaded from {path}.")
                return config
        except Exception as e:
            logger.error(f"Failed to load configuration from {path}: {e}")
            sys.exit(1)


@dataclass
class Room:
    x: int
    y: int
    width: int
    height: int

    def center(self) -> Tuple[float, float]:
        """Calculate the center coordinates of the room."""
        center_x = self.x + self.width / 2
        center_y = self.y + self.height / 2
        return center_x, center_y


@dataclass
class Leaf:
    x: int
    y: int
    width: int
    height: int
    left_child: Optional['Leaf'] = None
    right_child: Optional['Leaf'] = None
    room: Optional[Room] = None

    def split(self, config: Config) -> bool:
        """Attempt to split the leaf into two children."""
        if self.left_child or self.right_child:
            return False  # Already split

        split_horizontally = random.choice([True, False])
        max_split = self.height if split_horizontally else self.width

        if max_split < config.room_min_size * 2:
            return False  # Too small to split

        split_ratio = config.split_ratio + random.uniform(-0.05, 0.05)
        split = int(split_ratio * max_split)

        if split < config.room_min_size or (max_split - split) < config.room_min_size:
            return False

        if split_horizontally:
            self.left_child = Leaf(self.x, self.y, self.width, split)
            self.right_child = Leaf(self.x, self.y + split, self.width, self.height - split)
        else:
            self.left_child = Leaf(self.x, self.y, split, self.height)
            self.right_child = Leaf(self.x + split, self.y, self.width - split, self.height)

        logger.debug(f"Split leaf at ({self.x}, {self.y}, {self.width}x{self.height}) into "
                     f"left_child({self.left_child.x}, {self.left_child.y}, {self.left_child.width}x{self.left_child.height}) "
                     f"and right_child({self.right_child.x}, {self.right_child.y}, {self.right_child.width}x{self.right_child.height})")
        return True

    def create_rooms(self, config: Config, rooms: List[Room]) -> None:
        """Recursively create rooms within the leaf."""
        if self.left_child or self.right_child:
            if self.left_child:
                self.left_child.create_rooms(config, rooms)
            if self.right_child:
                self.right_child.create_rooms(config, rooms)
        else:
            # Ensure leaf is big enough to hold a room with margins
            margin = 1  # Minimum distance from leaf edges
            available_width = self.width - 2 * margin
            available_height = self.height - 2 * margin

            if available_width < config.room_min_size or available_height < config.room_min_size:
                logger.debug(f"Leaf too small for a room: width={self.width}, height={self.height}")
                return

            room_width = random.randint(config.room_min_size, min(config.room_max_size, available_width))
            room_height = random.randint(config.room_min_size, min(config.room_max_size, available_height))
            room_x = self.x + margin + random.randint(0, available_width - room_width)
            room_y = self.y + margin + random.randint(0, available_height - room_height)
            self.room = Room(room_x, room_y, room_width, room_height)
            rooms.append(self.room)
            logger.debug(f"Created room at ({room_x}, {room_y}) size ({room_width}x{room_height})")


class BSPMapGenerator:
    def __init__(self, config: Config):
        self.config = config
        if config.random_seed is not None:
            random.seed(config.random_seed)
            logger.info(f"Random seed set to {config.random_seed}")
        self.rooms: List[Room] = []

    def generate_rooms(self) -> List[Room]:
        """Generate rooms using Binary Space Partitioning."""
        root_leaf = Leaf(0, 0, self.config.map_width, self.config.map_height)
        leaves = [root_leaf]

        # Split the leaves
        split_successful = True
        depth = 0
        while split_successful and depth < self.config.max_depth:
            split_successful = False
            new_leaves = []
            for leaf in leaves:
                if not leaf.left_child and not leaf.right_child:
                    if leaf.split(self.config):
                        new_leaves.extend([leaf.left_child, leaf.right_child])
                        split_successful = True
            if split_successful:
                leaves = new_leaves
                depth += 1
                logger.debug(f"Partitioning depth {depth}, total leaves: {len(leaves)}")

        logger.info(f"Total partitioning depth: {depth}")

        # Create rooms
        root_leaf.create_rooms(self.config, self.rooms)
        logger.info(f"Total rooms created: {len(self.rooms)}")

        # Trim the number of rooms if necessary
        if len(self.rooms) > self.config.num_rooms:
            self.rooms = self.rooms[:self.config.num_rooms]
            logger.info(f"Trimmed rooms to {len(self.rooms)} to match num_rooms")

        return self.rooms


class CorridorGenerator:
    def __init__(self, rooms: List[Room], config: Config):
        self.rooms = rooms
        self.config = config

    def connect_rooms(self) -> nx.Graph:
        """Connect rooms using Minimum Spanning Tree and add extra connections."""
        G = nx.Graph()

        # Add nodes with positions
        for idx, room in enumerate(self.rooms):
            G.add_node(idx, pos=room.center())

        # Add all possible edges with Euclidean distance as weight
        for i in range(len(self.rooms)):
            for j in range(i + 1, len(self.rooms)):
                pos_i = self.rooms[i].center()
                pos_j = self.rooms[j].center()
                distance = math.hypot(pos_i[0] - pos_j[0], pos_i[1] - pos_j[1])
                G.add_edge(i, j, weight=distance)

        # Compute Minimum Spanning Tree (MST)
        mst = nx.minimum_spanning_tree(G)
        logger.info("Minimum Spanning Tree (MST) computed.")

        # Determine number of additional connections
        extra_connections = int(self.config.extra_connections_factor * len(self.rooms))
        potential_extra = set(G.edges()) - set(mst.edges())

        if potential_extra:
            # Sort potential extras by weight (ascending for shorter corridors first)
            sorted_extra = sorted(
                potential_extra,
                key=lambda edge: G.edges[edge]['weight']
            )
            num_extra = min(extra_connections, len(sorted_extra))
            extra_edges = sorted_extra[:num_extra]
            mst.add_edges_from(extra_edges)
            logger.info(f"Added {len(extra_edges)} extra connections.")
        else:
            logger.info("No extra connections available.")

        return mst


class MapVisualizer:
    def __init__(self, config: Config, output_image: Optional[str] = None):
        self.config = config
        self.output_image = output_image

    def draw_map_2d(self, rooms: List[Room], connections: nx.Graph) -> None:
        """Render the map in 2D using matplotlib."""
        fig, ax = plt.subplots(figsize=(15, 15))

        # Draw rooms
        for idx, room in enumerate(rooms):
            rect = patches.Rectangle(
                (room.x, room.y),
                room.width,
                room.height,
                linewidth=1,
                edgecolor='black',
                facecolor='cyan',
                alpha=0.8
            )
            ax.add_patch(rect)

            cx, cy = room.center()
            ax.plot(cx, cy, 'ro')  # Red dot at center
            ax.text(
                cx, cy, str(idx + 1),
                color='white', ha='center', va='center',
                fontsize=8, fontweight='bold'
            )

        # Draw corridors as orthogonal corridors
        for edge in connections.edges():
            self._draw_corridor_2d(ax, rooms[edge[0]], rooms[edge[1]])

        # Configure plot
        ax.set_xlim(-10, self.config.map_width + 10)
        ax.set_ylim(-10, self.config.map_height + 10)
        ax.set_aspect('equal')
        ax.set_title(
            'Hammer Editor - Randomly Generated Rooms (BSP)',
            fontsize=16
        )
        ax.set_xlabel('X', fontsize=12)
        ax.set_ylabel('Y', fontsize=12)
        ax.grid(True, which='both', linestyle='--', linewidth=0.5, alpha=0.7)

        # Save or show the plot
        if self.output_image:
            try:
                plt.savefig(self.output_image, bbox_inches='tight', dpi=300)
                logger.info(f"Map visualization saved to {self.output_image}.")
            except Exception as e:
                logger.error(f"Failed to save map visualization: {e}")
        else:
            plt.show()

    def _draw_corridor_2d(self, ax, room_a: Room, room_b: Room) -> None:
        """Draw an orthogonal corridor between two rooms in 2D."""
        ax_center = room_a.center()
        bx_center = room_b.center()

        # Randomly decide whether to go horizontal first or vertical
        horizontal_first = random.choice([True, False])

        if horizontal_first:
            intermediate_point = (bx_center[0], ax_center[1])
            self._draw_rectangle_corridor(
                ax,
                start=ax_center,
                end=intermediate_point,
                orientation='horizontal'
            )
            self._draw_rectangle_corridor(
                ax,
                start=intermediate_point,
                end=bx_center,
                orientation='vertical'
            )
        else:
            intermediate_point = (ax_center[0], bx_center[1])
            self._draw_rectangle_corridor(
                ax,
                start=ax_center,
                end=intermediate_point,
                orientation='vertical'
            )
            self._draw_rectangle_corridor(
                ax,
                start=intermediate_point,
                end=bx_center,
                orientation='horizontal'
            )

    def _draw_rectangle_corridor(self, ax, start: Tuple[float, float], end: Tuple[float, float], orientation: str) -> None:
        """Helper method to draw a rectangular corridor."""
        if orientation == 'horizontal':
            corridor_length = abs(end[0] - start[0])
            corridor_x = min(start[0], end[0])
            corridor_y = start[1] - self.config.passageway_width / 2
            rectangle = patches.Rectangle(
                (corridor_x, corridor_y),
                corridor_length if corridor_length > 0 else 1,
                self.config.passageway_width,
                linewidth=0,
                edgecolor=None,
                facecolor='blue',
                alpha=0.6
            )
        else:
            corridor_length = abs(end[1] - start[1])
            corridor_x = start[0] - self.config.passageway_width / 2
            corridor_y = min(start[1], end[1])
            rectangle = patches.Rectangle(
                (corridor_x, corridor_y),
                self.config.passageway_width,
                corridor_length if corridor_length > 0 else 1,
                linewidth=0,
                edgecolor=None,
                facecolor='blue',
                alpha=0.6
            )
        ax.add_patch(rectangle)


def parse_arguments(default_config: Config) -> Tuple[argparse.Namespace, Config]:
    """Parse command-line arguments and update the config accordingly."""
    parser = argparse.ArgumentParser(
        description="Hammer Editor Source Map GENERATOR 2D - Generate and visualize maps with rooms and corridors."
    )

    # Visualization options
    parser.add_argument(
        '--view', type=str, default='2d',
        choices=['2d'],
        help="Type of visualization: '2d'. Default is '2d'."
    )

    # Random seed for reproducibility
    parser.add_argument(
        '--seed', type=int,
        help="Random seed for reproducibility."
    )

    # Configuration file
    parser.add_argument(
        '--config', type=str, default=None,
        help="Path to a JSON configuration file (optional)."
    )

    # Output options
    parser.add_argument(
        '--output_image', type=str, default=None,
        help="Path to save the output image (e.g., map.png). If not provided, the image will be displayed."
    )
    parser.add_argument(
        '--output_json', type=str, default=None,
        help="Path to save the map data as JSON (e.g., map.json)."
    )

    # Configuration parameters
    parser.add_argument(
        '--num_rooms', type=int, default=default_config.num_rooms,
        help=f"Number of rooms to generate. Default is {default_config.num_rooms}."
    )
    parser.add_argument(
        '--map_width', type=int, default=default_config.map_width,
        help=f"Width of the map. Default is {default_config.map_width}."
    )
    parser.add_argument(
        '--map_height', type=int, default=default_config.map_height,
        help=f"Height of the map. Default is {default_config.map_height}."
    )
    parser.add_argument(
        '--room_min_size', type=int, default=default_config.room_min_size,
        help=f"Minimum size of a room. Default is {default_config.room_min_size}."
    )
    parser.add_argument(
        '--room_max_size', type=int, default=default_config.room_max_size,
        help=f"Maximum size of a room. Default is {default_config.room_max_size}."
    )
    parser.add_argument(
        '--passageway_width', type=int, default=default_config.passageway_width,
        help=f"Width of the passageways. Default is {default_config.passageway_width}."
    )
    parser.add_argument(
        '--extra_connections_factor', type=float, default=default_config.extra_connections_factor,
        help=f"Factor to determine extra connections. Default is {default_config.extra_connections_factor}."
    )
    parser.add_argument(
        '--max_depth', type=int, default=default_config.max_depth,
        help=f"Maximum BSP tree depth. Default is {default_config.max_depth}."
    )
    parser.add_argument(
        '--split_ratio', type=float, default=default_config.split_ratio,
        help=f"Split ratio for BSP. Default is {default_config.split_ratio}."
    )

    # Logging verbosity
    parser.add_argument(
        '--verbose', action='store_true',
        help="Enable verbose logging for debugging."
    )

    args = parser.parse_args()

    # Load configuration from file if provided
    config = default_config
    if args.config:
        if os.path.isfile(args.config):
            config = Config.load_from_file(args.config)
        else:
            logger.error(f"Configuration file {args.config} does not exist.")
            sys.exit(1)

    # Override config with command-line arguments
    config.num_rooms = args.num_rooms
    config.map_width = args.map_width
    config.map_height = args.map_height
    config.room_min_size = args.room_min_size
    config.room_max_size = args.room_max_size
    config.passageway_width = args.passageway_width
    config.extra_connections_factor = args.extra_connections_factor
    config.max_depth = args.max_depth
    config.split_ratio = args.split_ratio

    if args.seed is not None:
        config.random_seed = args.seed

    # Adjust logging level based on verbosity
    if args.verbose:
        logger.setLevel(logging.DEBUG)
        logger.debug("Verbose logging enabled.")

    return args, config


def export_map_data(rooms: List[Room], connections: nx.Graph, path: str) -> None:
    """Export room and corridor data to a JSON file."""
    map_data = {
        "rooms": [asdict(room) for room in rooms],
        "corridors": [{"from": edge[0], "to": edge[1]} for edge in connections.edges()]
    }
    try:
        with open(path, 'w') as file:
            json.dump(map_data, file, indent=4)
        logger.info(f"Map data exported to {path}.")
    except Exception as e:
        logger.error(f"Failed to export map data to {path}: {e}")


def main():
    # Initialize default configuration
    default_config = Config()

    # Parse command-line arguments and update the configuration
    args, config = parse_arguments(default_config)

    # Generate map using BSP
    bsp_generator = BSPMapGenerator(config)
    rooms = bsp_generator.generate_rooms()

    if not rooms:
        logger.error("No rooms were generated. Please check configuration parameters.")
        sys.exit(1)

    # Generate corridors
    corridor_generator = CorridorGenerator(rooms, config)
    connections = corridor_generator.connect_rooms()

    # Visualize map
    visualizer = MapVisualizer(config, output_image=args.output_image)
    if args.view == '2d':
        visualizer.draw_map_2d(rooms, connections)

    # Export map data if requested
    if args.output_json:
        export_map_data(rooms, connections, args.output_json)


if __name__ == "__main__":
    main()
```
Сделай возможность добавить в генерацию обязательную комнату. Например в случайной генерации обязательно должна быть одна комната с размером 50