using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

namespace Tumble
{
    public static class Backrooms3D
    {
        // Screen dimensions
        static int screenWidth;
        static int screenHeight;

        // Map storage using a dictionary to support an infinite map
        static Dictionary<(int, int), char> map = new Dictionary<(int, int), char>();

        // Player variables
        static double posX = 0.0, posY = 0.0; // Player position starts at (0,0)
        static double dirX = 1.0, dirY = 0.0;    // Player direction (facing right initially)
        static double planeX = 0, planeY = 0.66; // Camera plane

        // **Added Pitch Variables**
        static double pitch = 0.0; // Pitch angle in radians (-π/4 to +π/4)
        const double maxPitch = Math.PI; // 180 degrees
        const double minPitch = -Math.PI; // -180 degrees
        const double pitchSpeed = 2; // Radians per second

        static double yaw = 0.0;   // Yaw is equivalent to dirX and dirY rotation

        // Timing
        static double time = 0; // Time of current frame
        static double oldTime = 0; // Time of previous frame

        // ANSI Escape Codes
        const string resetColor = "\u001b[0m";

        // Minimap dimensions
        const int minimapSize = 20;

        // Debug information toggle
        static bool showUI = false;
        static bool wallTexture = false;
        static bool endGame = false;

        // Autoplay variables
        static bool isAutoplay = false;
        static readonly Random autoplayRandom = new Random();
        static string currentAutoplayAction = "idle";
        const double wallThreshold = 1.9; // Distance threshold to start avoiding walls
        const int wallRotations = 3; // Number of rotations when facing a wall
        const int noWallSwitchThreshold = 15; // Number of frames not facing a wall to switch primary turn direction
        static bool fullscreenMinimap = false;
        static int autoplaySpeed = 1;
        public static void Start()
        {
            var tempCursor = Console.CursorVisible;
            Console.CursorVisible = false;
            UI.Wipe();
            UI.WriteCenterCenter("Loading...");
            screenWidth = Console.WindowWidth;
            screenHeight = Console.WindowHeight;

            // Initialize player's starting position to ensure adequate open space
            posX = 0.0;
            posY = 0.0;
            InitializePlayerStartingArea();

            Stopwatch stopwatch = new Stopwatch();
            stopwatch.Start();

            while (true)
            {
                // Handle window resizing
                if (screenWidth != Console.WindowWidth || screenHeight != Console.WindowHeight)
                {
                    screenWidth = Console.WindowWidth;
                    screenHeight = Console.WindowHeight;
                    try
                    {
                        Console.SetBufferSize(screenWidth, screenHeight);
                        UI.Wipe();
                    }
                    catch
                    {
                        // Ignore if setting buffer size fails
                    }
                }

                // Timing
                oldTime = time;
                time = stopwatch.Elapsed.TotalSeconds;
                double frameTime = time - oldTime;

                // Input
                ProcessInput(frameTime);

                // Autoplay
                if (isAutoplay)
                {
                    for (int i = 0; i < autoplaySpeed; i++)
                    {
                        AutoplayMaze(frameTime);
                    }
                }

                // Render frame
                RenderFrame();

                // Sleep to reduce CPU usage
                Thread.Sleep(1);
                if (endGame)
                {
                    endGame = false;
                    UI.Wipe();
                    Console.CursorVisible = tempCursor;
                    break;
                }
            }
        }

        /// <summary>
        /// Initializes a larger open area around the player's starting position
        /// with a structured grid pattern and randomized walls to prevent immediate trapping.
        /// </summary>
        static void InitializePlayerStartingArea()
        {
            int baseX = (int)Math.Floor(posX);
            int baseY = (int)Math.Floor(posY);
            Random random = new Random(1);

            // Define the radius of the open area
            int openRadius = 1;

            // Clear the base area
            for (int x = baseX - openRadius; x <= baseX + openRadius; x++)
            {
                for (int y = baseY - openRadius; y <= baseY + openRadius; y++)
                {
                    map[(x, y)] = ' ';
                }
            }

            // Ensure the player's starting position is clear
            map[(baseX, baseY)] = ' ';
        }

        static void ProcessInput(double frameTime)
        {
            double moveSpeed = frameTime * 3.75; // squares per second
            double rotSpeed = frameTime * 1.5;  // radians per second
            double pitchAdjust = frameTime * pitchSpeed; // radians per second

            while (Console.KeyAvailable)
            {
                ConsoleKeyInfo ki = Console.ReadKey(true);
                var key = ki.Key;
                bool shift = ki.Modifiers == ConsoleModifiers.Shift;
                var mult = shift ? 0.2 : 1;
                double adjustedMoveSpeed = moveSpeed * (shift ? 0.2 : 1);
                double adjustedRotSpeed = rotSpeed * (shift ? 0.2 : 1);
                double adjustedPitchAdjust = pitchAdjust * (shift ? 0.5 : 1); // Slower pitch adjustment when holding shift

                if (key == ConsoleKey.W)
                {
                    if (IsPositionEmpty(posX + dirX * adjustedMoveSpeed, posY))
                        posX += dirX * adjustedMoveSpeed;
                    if (IsPositionEmpty(posX, posY + dirY * adjustedMoveSpeed))
                        posY += dirY * adjustedMoveSpeed;

                    EnsureMapAroundPlayer();
                }
                if (key == ConsoleKey.S)
                {
                    if (IsPositionEmpty(posX - dirX * adjustedMoveSpeed, posY))
                        posX -= dirX * adjustedMoveSpeed;
                    if (IsPositionEmpty(posX, posY - dirY * adjustedMoveSpeed))
                        posY -= dirY * adjustedMoveSpeed;

                    EnsureMapAroundPlayer();
                }
                if (key == ConsoleKey.A)
                {
                    // Move left (strafe) relative to yaw
                    double perpDirX = Math.Cos(yaw - Math.PI / 2);
                    double perpDirY = Math.Sin(yaw - Math.PI / 2);
                    if (IsPositionEmpty(posX + perpDirX * adjustedMoveSpeed, posY))
                        posX += perpDirX * adjustedMoveSpeed;
                    if (IsPositionEmpty(posX, posY + perpDirY * adjustedMoveSpeed))
                        posY += perpDirY * adjustedMoveSpeed;

                    EnsureMapAroundPlayer();
                }
                if (key == ConsoleKey.D)
                {
                    // Move right (strafe) relative to yaw
                    double perpDirX = Math.Cos(yaw + Math.PI / 2);
                    double perpDirY = Math.Sin(yaw + Math.PI / 2);
                    if (IsPositionEmpty(posX + perpDirX * adjustedMoveSpeed, posY))
                        posX += perpDirX * adjustedMoveSpeed;
                    if (IsPositionEmpty(posX, posY + perpDirY * adjustedMoveSpeed))
                        posY += perpDirY * adjustedMoveSpeed;

                    EnsureMapAroundPlayer();
                }
                if (key == ConsoleKey.LeftArrow)
                {
                    // Rotate left (Yaw)
                    Rotate(-adjustedRotSpeed);
                }
                if (key == ConsoleKey.RightArrow)
                {
                    // Rotate right (Yaw)
                    Rotate(adjustedRotSpeed);
                }
                if (key == ConsoleKey.UpArrow)
                {
                    // Look up (increase pitch)
                    pitch += adjustedPitchAdjust;
                    if (pitch > maxPitch) pitch = maxPitch;
                }
                if (key == ConsoleKey.DownArrow)
                {
                    // Look down (decrease pitch)
                    pitch -= adjustedPitchAdjust;
                    if (pitch < minPitch) pitch = minPitch;
                }
                if (key == ConsoleKey.F1)
                {
                    // Toggle debug information
                    showUI = !showUI;
                }
                if (key == ConsoleKey.Escape)
                {
                    endGame = true;
                }
                if (key == ConsoleKey.G)
                {
                    wallTexture = !wallTexture;
                }
                if (key == ConsoleKey.Spacebar)
                {
                    // Toggle autoplay
                    isAutoplay = !isAutoplay;
                    currentAutoplayAction = "idle";
                }
                if (key == ConsoleKey.M)
                {
                    fullscreenMinimap = !fullscreenMinimap;
                }
                if (key == ConsoleKey.Add)
                {
                    autoplaySpeed *= 2;
                }
                if (key == ConsoleKey.Subtract)
                {
                    autoplaySpeed /= 2;
                    if (autoplaySpeed < 1)
                    {
                        autoplaySpeed = 1;
                    }
                }
            }
        }

        static bool IsPositionEmpty(double x, double y)
        {
            int mapX = (int)Math.Floor(x);
            int mapY = (int)Math.Floor(y);
            if (map.ContainsKey((mapX, mapY)))
                return map[(mapX, mapY)] == ' ';
            else
                return false; // Treat unknown areas as walls
        }

        static void Rotate(double angle)
        {
            double oldDirX = dirX;
            dirX = dirX * Math.Cos(angle) - dirY * Math.Sin(angle);
            dirY = oldDirX * Math.Sin(angle) + dirY * Math.Cos(angle);

            // Update camera plane based on new direction
            double oldPlaneX = planeX;
            planeX = planeX * Math.Cos(angle) - planeY * Math.Sin(angle);
            planeY = oldPlaneX * Math.Sin(angle) + planeY * Math.Cos(angle);

            yaw += angle;
            // Keep yaw within 0 to 2*PI
            yaw %= 2 * Math.PI;
            if (yaw < 0) yaw += 2 * Math.PI;
        }

        static void RenderFrame()
        {
            StringBuilder sb = new StringBuilder();

            int ceilingColor = 0x262515;
            int floorColor = 0x6c6939;
            string ceilingColorCode = UI.GetColorCode(ceilingColor);
            string floorColorCode = UI.GetColorCode(floorColor);

            // Initialize a 2D character array for the screen
            char[,] screenBuffer = new char[screenHeight, screenWidth];
            string[,] colorBuffer = new string[screenHeight, screenWidth];

            // Fill screen buffer with default characters and colors
            for (int y = 0; y < screenHeight; y++)
                for (int x = 0; x < screenWidth; x++)
                {
                    screenBuffer[y, x] = ' ';
                    colorBuffer[y, x] = "\u001b[0m"; // Reset color
                }

            // Calculate the vertical center shift based on pitch
            // Positive pitch means looking up, so shift center down
            // Negative pitch means looking down, so shift center up
            // Define a maximum shift based on the pitch range
            double pitchFactor = screenHeight; // Adjust as needed
            int pitchShift = (int)(pitch / maxPitch * pitchFactor);

            // Raycasting
            for (int x = 0; x < screenWidth; x++)
            {
                // Calculate ray position and direction
                double cameraX = 2 * x / (double)screenWidth - 1; // x-coordinate in camera space
                double rayDirX = dirX + planeX * cameraX;
                double rayDirY = dirY + planeY * cameraX;

                // Which box of the map we're in
                int mapX = (int)Math.Floor(posX);
                int mapY = (int)Math.Floor(posY);

                // Length of ray from current position to next x or y-side
                double sideDistX;
                double sideDistY;

                // Length of ray from one x or y-side to next x or y-side
                double deltaDistX = (rayDirX == 0) ? 1e30 : Math.Abs(1 / rayDirX);
                double deltaDistY = (rayDirY == 0) ? 1e30 : Math.Abs(1 / rayDirY);
                double perpWallDist;

                // What direction to step in x or y-direction (either +1 or -1)
                int stepX, stepY;

                bool hit = false; // Was a wall hit?
                int side = 0; // Was the wall vertical or horizontal?

                // Calculate step and initial sideDist
                if (rayDirX < 0)
                {
                    stepX = -1;
                    sideDistX = (posX - mapX) * deltaDistX;
                }
                else
                {
                    stepX = 1;
                    sideDistX = (mapX + 1.0 - posX) * deltaDistX;
                }
                if (rayDirY < 0)
                {
                    stepY = -1;
                    sideDistY = (posY - mapY) * deltaDistY;
                }
                else
                {
                    stepY = 1;
                    sideDistY = (mapY + 1.0 - posY) * deltaDistY;
                }

                // Perform DDA
                while (!hit)
                {
                    // Jump to next map square, either in x-direction or y-direction
                    if (sideDistX < sideDistY)
                    {
                        sideDistX += deltaDistX;
                        mapX += stepX;
                        side = 0;
                    }
                    else
                    {
                        sideDistY += deltaDistY;
                        mapY += stepY;
                        side = 1;
                    }

                    // Check if ray has hit a wall
                    if (map.ContainsKey((mapX, mapY)))
                    {
                        if (map[(mapX, mapY)] == '#') hit = true;
                    }
                    else
                    {
                        // Treat unknown areas as walls
                        hit = true;
                    }
                }

                // Calculate distance to the point of impact
                if (side == 0)
                    perpWallDist = (sideDistX - deltaDistX);
                else
                    perpWallDist = (sideDistY - deltaDistY);

                // Calculate height of line to draw on screen
                int lineHeight = (int)(screenHeight / perpWallDist);

                // Calculate lowest and highest pixel to fill in current stripe
                // **Modified to include pitchShift**
                int drawStart = Math.Max(-lineHeight / 2 + screenHeight / 2 + pitchShift, 0);
                int drawEnd = Math.Min(lineHeight / 2 + screenHeight / 2 + pitchShift, screenHeight - 1);

                if (wallTexture)
                {
                    // Base color components for #9e9a56
                    int baseR = 158; // 0x9e
                    int baseG = 154; // 0x9a
                    int baseB = 86;  // 0x56

                    // Adjust color intensity based on distance
                    float distanceFactor = (float)Math.Clamp(1.0 / (perpWallDist * 0.15), 0.0, 1.0);
                    int colorR = (int)(baseR * distanceFactor);
                    int colorG = (int)(baseG * distanceFactor);
                    int colorB = (int)(baseB * distanceFactor);

                    string baseWallColor = $"\u001b[38;2;{colorR};{colorG};{colorB}m";
                    string darkWallColor = $"\u001b[38;2;{Math.Max(colorR - 30, 0)};{Math.Max(colorG - 30, 0)};{Math.Max(colorB - 30, 0)}m";

                    // Darken colors for y-sides to add depth
                    if (side == 1)
                    {
                        colorR = Math.Max(colorR - 30, 0);
                        colorG = Math.Max(colorG - 30, 0);
                        colorB = Math.Max(colorB - 30, 0);
                        baseWallColor = $"\u001b[38;2;{colorR};{colorG};{colorB}m";
                        darkWallColor = $"\u001b[38;2;{Math.Max(colorR - 20, 0)};{Math.Max(colorG - 20, 0)};{Math.Max(colorB - 20, 0)}m";
                    }

                    for (int y = drawStart; y <= drawEnd; y++)
                    {
                        if (IsInScreenBounds(x, y))
                        {
                            // Create a simple pattern
                            bool isAlternateColor = ((y + x) % 2 == 0);

                            // Set wall color and pattern
                            colorBuffer[y, x] = isAlternateColor ? darkWallColor : baseWallColor;
                            screenBuffer[y, x] = '█'; // Full block for texture
                        }
                    }
                }
                else
                {
                    // Base color components for #9e9a56
                    int baseR = 158; // 0x9e
                    int baseG = 154; // 0x9a
                    int baseB = 86;  // 0x56

                    // Adjust color intensity based on distance
                    float distanceFactor = (float)Math.Clamp(1.0 / (perpWallDist * 1.5), 0.0, 1.0);
                    int colorR = (int)(baseR * distanceFactor);
                    int colorG = (int)(baseG * distanceFactor);
                    int colorB = (int)(baseB * distanceFactor);

                    string wallColor = $"\u001b[38;2;{colorR};{colorG};{colorB}m";

                    // Darken colors for y-sides to add depth
                    if (side == 1)
                    {
                        colorR = Math.Max(colorR - 30, 0);
                        colorG = Math.Max(colorG - 30, 0);
                        colorB = Math.Max(colorB - 30, 0);
                        wallColor = $"\u001b[38;2;{colorR};{colorG};{colorB}m";
                    }

                    for (int y = drawStart; y <= drawEnd; y++)
                    {
                        if (IsInScreenBounds(x, y))
                        {
                            // Set wall color
                            colorBuffer[y, x] = wallColor;
                            screenBuffer[y, x] = '\u2588'; // Full block character
                        }
                    }
                }

                // **Modified Ceiling Rendering Based on Pitch**
                for (int y = 0; y < drawStart; y++)
                {
                    if (IsInScreenBounds(x, y))
                    {
                        colorBuffer[y, x] = ceilingColorCode; // Bright yellow
                        screenBuffer[y, x] = '\u2588'; // Full block character
                    }
                }

                // **Modified Floor Rendering Based on Pitch**
                for (int y = drawEnd + 1; y < screenHeight; y++)
                {
                    if (IsInScreenBounds(x, y))
                    {
                        //65541e
                        colorBuffer[y, x] = floorColorCode; // Dark yellow
                        screenBuffer[y, x] = '\u2588'; // Full block character
                    }
                }
            }

            if (showUI)
            {
                // Overlay Minimap at bottom-right corner
                RenderMinimap(screenBuffer, colorBuffer);
                // Overlay Debug Information at bottom-left corner
                RenderDebugInfo(screenBuffer, colorBuffer);
            }

            // Build the final string with colors
            for (int y = 0; y < screenHeight; y++)
            {
                for (int x = 0; x < screenWidth; x++)
                {
                    sb.Append($"{colorBuffer[y, x]}{screenBuffer[y, x]}");
                }
                sb.Append(resetColor); // Reset color at end of line
                if (y != screenHeight - 1)
                {
                    sb.Append('\n');
                }
            }

            // Output to console
            try
            {
                Console.SetCursorPosition(0, 0);
                Console.Write(sb.ToString());
            }
            catch (ArgumentOutOfRangeException)
            {
                // Handle potential exceptions when writing outside the buffer
            }
        }

        private static bool IsInScreenBounds(int x, int y)
        {
            return y >= 0 && y < screenHeight && x >= 0 && x < screenWidth;
        }

        const int minimapEdgeAmount = 5;

        /// <summary>
        /// Restores the minimap to its original aspect ratio without horizontal stretching.
        /// </summary>
        static void RenderMinimap(char[,] screenBuffer, string[,] colorBuffer)
        {
            int minimapWidth, minimapHeight, startX, startY;

            if (fullscreenMinimap)
            {
                minimapWidth = screenWidth - 2 * minimapEdgeAmount;
                minimapHeight = screenHeight - 2 * minimapEdgeAmount;
                startX = minimapEdgeAmount;
                startY = minimapEdgeAmount;
            }
            else
            {
                minimapWidth = minimapSize;
                minimapHeight = minimapSize;
                startX = screenWidth - minimapWidth - 2;
                startY = screenHeight - minimapHeight - 1;
            }

            bool playerAdded = false;

            for (int y = 0; y < minimapHeight; y++)
            {
                for (int x = 0; x < minimapWidth; x++)
                {
                    int mapX = (int)(posX - minimapWidth / 2 + x);
                    int mapY = (int)(posY - minimapHeight / 2 + y);

                    if (mapX == (int)posX && mapY == (int)posY && !playerAdded)
                    {
                        // Player position on minimap: Bright Green '█'
                        if (IsWithinScreen(startX + x, startY + y))
                        {
                            colorBuffer[startY + y, startX + x] = "\u001b[38;2;0;255;0m"; // Bright Green
                            screenBuffer[startY + y, startX + x] = '█';
                            playerAdded = true;
                        }
                    }
                    else if (map.ContainsKey((mapX, mapY)))
                    {
                        if (map[(mapX, mapY)] == '#')
                        {
                            // Wall on minimap: White '#'
                            if (IsWithinScreen(startX + x, startY + y))
                            {
                                colorBuffer[startY + y, startX + x] = "\u001b[38;2;164;156;81m"; // White
                                screenBuffer[startY + y, startX + x] = '█';
                            }
                        }
                        else
                        {
                            // Empty space on minimap: Light Gray '.'
                            if (IsWithinScreen(startX + x, startY + y))
                            {
                                colorBuffer[startY + y, startX + x] = "\u001b[38;2;194;169;116m"; // Light Gray
                                screenBuffer[startY + y, startX + x] = '█';
                            }
                        }
                    }
                    else
                    {
                        // Outside map bounds: Render space
                        if (IsWithinScreen(startX + x, startY + y))
                        {
                            colorBuffer[startY + y, startX + x] = "\u001b[38;2;0;0;0m"; // Reset color
                            screenBuffer[startY + y, startX + x] = '█';
                        }
                    }
                }
            }
        }
        static void RenderDebugInfo(char[,] screenBuffer, string[,] colorBuffer)
        {
            string posInfo = $"Pos: X={posX:F2}, Y={posY:F2}";
            string rotInfo = $"Yaw: {yaw * (180 / Math.PI):F2}° Pitch: {pitch * (180 / Math.PI):F2}°"; // Updated to show pitch
            string autoplayStatus = $"Autoplay: {(isAutoplay ? "ON" : "OFF")} ({autoplaySpeed}x)";

            for (int i = 0; i < posInfo.Length && i < screenWidth; i++)
            {
                if (IsWithinScreen(i, screenHeight - 3))
                {
                    colorBuffer[screenHeight - 3, i] = "\u001b[38;2;255;255;0m"; // Yellow
                    screenBuffer[screenHeight - 3, i] = posInfo[i];
                }
            }

            for (int i = 0; i < rotInfo.Length && i < screenWidth; i++)
            {
                if (IsWithinScreen(i, screenHeight - 2))
                {
                    colorBuffer[screenHeight - 2, i] = "\u001b[38;2;255;255;0m"; // Yellow
                    screenBuffer[screenHeight - 2, i] = rotInfo[i];
                }
            }

            for (int i = 0; i < autoplayStatus.Length && i < screenWidth; i++)
            {
                if (IsWithinScreen(i, screenHeight - 1))
                {
                    colorBuffer[screenHeight - 1, i] = isAutoplay ? "\u001b[38;2;0;255;0m" : "\u001b[38;2;255;0;0m"; // Green if on, Red if off
                    screenBuffer[screenHeight - 1, i] = autoplayStatus[i];
                }
            }
        }

        static bool IsWithinScreen(int x, int y)
        {
            return x >= 0 && x < screenWidth && y >= 0 && y < screenHeight;
        }

        /// <summary>
        /// Ensures that the map is generated around the player's current position.
        /// Expands the map dynamically as the player moves.
        /// </summary>
        static void EnsureMapAroundPlayer()
        {
            int baseX = (int)Math.Floor(posX);
            int baseY = (int)Math.Floor(posY);

            // Define the radius around the player to generate the map
            int generationRadius = 10;

            for (int x = baseX - generationRadius; x <= baseX + generationRadius; x++)
            {
                for (int y = baseY - generationRadius; y <= baseY + generationRadius; y++)
                {
                    if (!map.ContainsKey((x, y)))
                    {
                        // Simple procedural generation: walls are less likely to be placed near the center
                        double distance = Math.Sqrt(Math.Pow(x - baseX, 2) + Math.Pow(y - baseY, 2));
                        double wallProbability = Math.Clamp(0.3 - (distance / generationRadius) * 0.2, 0.1, 0.3);
                        map[(x, y)] = (autoplayRandom.NextDouble() < wallProbability) ? '#' : ' ';
                    }
                }
            }
        }

        static bool turningRight = true;
        static int noWallCounter = 0;
        static List<(int, int)> latestPositions = new List<(int, int)>();
        /// <summary>
        /// AutoplayMaze method that automatically explores the maze with intelligent wall avoidance.
        /// </summary>
        static void AutoplayMaze(double frameTime)
        {
            if (latestPositions.Count > 250)
            {
                latestPositions.RemoveAt(0);
                // Check if all positions are within a 7x7 radius
                bool allWithinRadius = CheckPositionsWithinRadius(latestPositions, 4);

                if (allWithinRadius)
                {
                    RemoveCharactersInRadius((int)posX, (int)posY, 3);
                }
            }
            latestPositions.Add(((int)posX, (int)posY));

            // Execute the chosen action
            double moveSpeed = frameTime * 3.75; // Consistent with manual movement
            double rotSpeed = frameTime * 1.5;   // Radians per frame
            double distanceAhead = GetDistanceToWall(dirX, dirY);
            if (distanceAhead < wallThreshold)
            {
                if (noWallCounter > noWallSwitchThreshold)
                {
                    turningRight = !turningRight;
                }
                noWallCounter = 0;
                currentAutoplayAction = "move_forward_small";
                for (int i = 0; i < wallRotations; i++)
                {
                    DoAction(turningRight ? "rotate_right" : "rotate_left", moveSpeed, rotSpeed);
                }
            }
            else
            {
                noWallCounter++;
                var choice = autoplayRandom.Next(0, 100);
                if (choice < 70)
                {
                    currentAutoplayAction = "move_forward";
                }
                else if (choice < 95)
                {
                    currentAutoplayAction = turningRight ? "rotate_left" : "rotate_right";
                }
                else
                {
                    currentAutoplayAction = turningRight ? "rotate_right" : "rotate_left";
                }
            }

            DoAction(currentAutoplayAction, moveSpeed, rotSpeed);
        }
        static bool CheckPositionsWithinRadius(List<(int, int)> positions, int radius)
        {
            if (positions.Count == 0) return false;

            int minX = positions.Min(p => p.Item1);
            int maxX = positions.Max(p => p.Item1);
            int minY = positions.Min(p => p.Item2);
            int maxY = positions.Max(p => p.Item2);

            return (maxX - minX <= radius) && (maxY - minY <= radius);
        }
        static void RemoveCharactersInRadius(int centerX, int centerY, int radius)
        {
            for (int x = centerX - radius; x <= centerX + radius; x++)
            {
                for (int y = centerY - radius; y <= centerY + radius; y++)
                {
                    if (map.TryGetValue((x, y), out char value) && value == '#')
                    {
                        map[(x, y)] = ' ';
                    }
                }
            }
        }

        private static void DoAction(string action, double moveSpeed, double rotSpeed)
        {
            var smallStepFactor = 3;
            switch (action)
            {
                case "move_forward_small":
                    if (IsPositionEmpty(posX + dirX * moveSpeed / smallStepFactor, posY))
                        posX += dirX * moveSpeed / smallStepFactor;
                    if (IsPositionEmpty(posX, posY + dirY * moveSpeed / smallStepFactor))
                        posY += dirY * moveSpeed / smallStepFactor;

                    EnsureMapAroundPlayer();
                    break;
                case "move_forward":
                    if (IsPositionEmpty(posX + dirX * moveSpeed, posY))
                        posX += dirX * moveSpeed;
                    if (IsPositionEmpty(posX, posY + dirY * moveSpeed))
                        posY += dirY * moveSpeed;

                    EnsureMapAroundPlayer();
                    break;
                case "move_back":
                    if (IsPositionEmpty(posX - dirX * moveSpeed / 3, posY))
                        posX -= dirX * moveSpeed / 3;
                    if (IsPositionEmpty(posX, posY - dirY * moveSpeed / 3))
                        posY -= dirY * moveSpeed / 3;

                    EnsureMapAroundPlayer();
                    break;

                case "rotate_left":
                    Rotate(-rotSpeed);
                    break;

                case "rotate_right":
                    Rotate(rotSpeed);
                    break;

                default:
                    // Idle or unknown action
                    break;
            }
        }

        /// <summary>
        /// Calculates the distance from the player's current position in the facing direction to the nearest wall.
        /// </summary>
        /// <param name="dirX">The X component of the player's direction vector.</param>
        /// <param name="dirY">The Y component of the player's direction vector.</param>
        /// <returns>The perpendicular distance to the nearest wall.</returns>
        static double GetDistanceToWall(double dirX, double dirY)
        {
            // Initialize ray position and direction
            double rayPosX = posX;
            double rayPosY = posY;
            double rayDirX = dirX;
            double rayDirY = dirY;

            // Map position
            int mapX = (int)Math.Floor(rayPosX);
            int mapY = (int)Math.Floor(rayPosY);

            // Length of ray from one x or y-side to next x or y-side
            double deltaDistX = (rayDirX == 0) ? 1e30 : Math.Abs(1 / rayDirX);
            double deltaDistY = (rayDirY == 0) ? 1e30 : Math.Abs(1 / rayDirY);

            double sideDistX;
            double sideDistY;

            int stepX, stepY;

            bool hit = false; // Was a wall hit?
            int side = 0; // Was the wall hit a vertical side?

            // Calculate step and initial sideDist
            if (rayDirX < 0)
            {
                stepX = -1;
                sideDistX = (rayPosX - mapX) * deltaDistX;
            }
            else
            {
                stepX = 1;
                sideDistX = (mapX + 1.0 - rayPosX) * deltaDistX;
            }
            if (rayDirY < 0)
            {
                stepY = -1;
                sideDistY = (rayPosY - mapY) * deltaDistY;
            }
            else
            {
                stepY = 1;
                sideDistY = (mapY + 1.0 - rayPosY) * deltaDistY;
            }

            // Perform DDA
            while (!hit)
            {
                // Jump to next map square, either in x-direction or y-direction
                if (sideDistX < sideDistY)
                {
                    sideDistX += deltaDistX;
                    mapX += stepX;
                    side = 0;
                }
                else
                {
                    sideDistY += deltaDistY;
                    mapY += stepY;
                    side = 1;
                }

                // Check if ray has hit a wall
                if (map.ContainsKey((mapX, mapY)))
                {
                    if (map[(mapX, mapY)] == '#') hit = true;
                }
                else
                {
                    // Treat unknown areas as walls
                    hit = true;
                }
            }

            // Calculate distance to the point of impact
            double perpWallDist;
            if (side == 0)
                perpWallDist = (sideDistX - deltaDistX);
            else
                perpWallDist = (sideDistY - deltaDistY);

            return perpWallDist;
        }
    }
}

add a zoom in/out in the fullscreen minimap, with loss of detail (player always appears in the center, but the surrounding space and obstacles are reduced (or increased) quality and zoomed in or out based on their position and projected over multiple or less full block characters.) zooming is done with Multiply and Divide keys.