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

        // Camera rotation angles
        static double pitch = 0.0; // Not used in rendering but tracked for potential future use
        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 (restored to original aspect ratio)
        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 = 4; // 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;
        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)
                {
                    //PreventAutoplayLoop();
                    AutoplayMaze(frameTime);
                }

                // Render frame
                RenderFrame();

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

        private static void PreventAutoplayLoop()
        {
            // check if player was in the same 3x3 area for the last 150 frames
            // if yes, remove a radius 3 area of obstacles around the player
            int maxFramesInArea = 100;
            int removeRadius = 3;
            lastPositions.Add(((int)posX, (int)posY));
            // complete here
            if (lastPositions.Count >= maxFramesInArea)
            {
                var uniquePositions = lastPositions.Distinct().ToList();
                if (uniquePositions.Count <= 9) // Player has been in a 3x3 area for the last 150 frames
                {
                    int centerX = (int)posX;
                    int centerY = (int)posY;

                    for (int y = centerY - removeRadius; y <= centerY + removeRadius; y++)
                    {
                        for (int x = centerX - removeRadius; x <= centerX + removeRadius; x++)
                        {
                            try
                            {
                                map[(x, y)] = ' '; // Remove obstacles in a radius 3 area
                            }
                            catch (Exception)
                            {

                                throw;
                            }

                        }
                    }
                    lastPositions.Clear(); // Reset the position history
                }
            }
        }

        static List<(int, int)> lastPositions = [];

        /// <summary>
        /// Initializes a larger open area around the player's starting position
        /// 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 = 500;

            // 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)] = ' ';
                }
            }

            // Add random walls
            for (int x = baseX - openRadius + 1; x < baseX + openRadius; x++)
            {
                for (int y = baseY - openRadius + 1; y < baseY + openRadius; y++)
                {
                    if (random.Next(100) < 30) // 30% chance of placing a wall
                    {
                        map[(x, y)] = '#';
                    }
                }
            }

            // Add random 3x3 rooms
            int numRooms = random.Next(3, 6); // 3 to 5 rooms
            for (int i = 0; i < numRooms; i++)
            {
                int roomX = random.Next(baseX - openRadius + 2, baseX + openRadius - 2);
                int roomY = random.Next(baseY - openRadius + 2, baseY + openRadius - 2);

                for (int x = roomX - 1; x <= roomX + 1; x++)
                {
                    for (int y = roomY - 1; y <= roomY + 1; y++)
                    {
                        map[(x, y)] = ' '; // Clear the room area
                    }
                }
            }

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

            // Add walls around the perimeter
            for (int x = baseX - openRadius - 1; x <= baseX + openRadius + 1; x++)
            {
                for (int y = baseY - openRadius - 1; y <= baseY + openRadius + 1; y++)
                {
                    if (Math.Abs(x - baseX) == openRadius + 1 || Math.Abs(y - baseY) == openRadius + 1)
                    {
                        map[(x, y)] = '#';
                    }
                }
            }
        }

        static void ProcessInput(double frameTime)
        {
            double moveSpeed = frameTime * 3.75; // squares per second
            double rotSpeed = frameTime * 1.5;  // 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);

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

                    EnsureMapAroundPlayer();
                }
                if (key == ConsoleKey.S || key == ConsoleKey.DownArrow)
                {
                    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.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;
                }
            }
        }

        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();

            // 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
                }

            // 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
                int drawStart = Math.Max(-lineHeight / 2 + screenHeight / 2, 0);
                int drawEnd = Math.Min(lineHeight / 2 + screenHeight / 2, 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 * 1.5), 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 (y >= 0 && y < screenHeight && x >= 0 && x < screenWidth)
                        {
                            // 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 (y >= 0 && y < screenHeight && x >= 0 && x < screenWidth)
                        {
                            // Set wall color
                            colorBuffer[y, x] = wallColor;
                            screenBuffer[y, x] = '\u2588'; // Full block character
                        }
                    }
                }

                // Ceiling: Bright yellow
                for (int y = 0; y < drawStart; y++)
                {
                    if (y >= 0 && y < screenHeight && x >= 0 && x < screenWidth)
                    {
                        colorBuffer[y, x] = "\u001b[38;2;111;99;39m";
                        screenBuffer[y, x] = '\u2588'; // Full block character
                    }
                }

                // Floor: Dark yellow
                for (int y = drawEnd + 1; y < screenHeight; y++)
                {
                    if (y >= 0 && y < screenHeight && x >= 0 && x < screenWidth)
                    {
                        //65541e
                        colorBuffer[y, x] = "\u001b[38;2;101;84;30m";
                        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
            }
        }
        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}°";
            string autoplayStatus = $"Autoplay: {(isAutoplay ? "ON" : "OFF")}";

            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;
        /// <summary>
        /// AutoplayMaze method that automatically explores the maze with intelligent wall avoidance.
        /// </summary>
        static void AutoplayMaze(double frameTime)
        {
            // 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);
        }

        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 white square lights on the ceiling randomly, projected in the 3D view and visible on the map.