Hello! Can you improve it and fix bugs?
import pygame
import math
import numpy as np


pygame.init()


SCREEN_WIDTH = 800
SCREEN_HEIGHT = 600
FOV = math.pi / 3  
HALF_FOV = FOV / 2
NUM_RAYS = 200  
MAX_DEPTH = 800
CELL_SIZE = 64
PLAYER_SIZE = 10
FLOOR_COLOR = (30, 30, 30)
CEILING_COLOR = (50, 50, 80)
TEXTURE_SIZE = 64


WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
GRAY = (128, 128, 128)


WALL_TEXTURES = {
    1: np.array([[(int((i^j)%2)*255) for i in range(TEXTURE_SIZE)] for j in range(TEXTURE_SIZE)], dtype=np.uint8),
    2: np.array([[(int((i*j)%32)*8) for i in range(TEXTURE_SIZE)] for j in range(TEXTURE_SIZE)], dtype=np.uint8),
    3: np.array([[int(abs(128-i)*2) for i in range(TEXTURE_SIZE)] for j in range(TEXTURE_SIZE)], dtype=np.uint8)
}


MAP = [
    [1,1,1,2,2,1,1,1],
    [1,0,0,0,0,0,0,1],
    [1,0,3,0,2,0,0,2],
    [2,0,0,0,0,3,0,1],
    [2,0,2,0,0,0,0,1],
    [1,0,3,0,2,0,0,2],
    [1,0,0,0,0,0,0,1],
    [1,1,2,1,1,2,1,1]
]


screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
pygame.display.set_caption('2.5D Raycaster')


player_x = CELL_SIZE * 1.5
player_y = CELL_SIZE * 1.5
player_angle = 0
player_speed = 3
rotation_speed = 0.03

class TextureCache:
    def __init__(self):
        self.wall_textures = {}
        self.scaled_textures = {}
        self._generate_textures()
    
    def _generate_textures(self):
        # Pre-render base textures
        for wall_type, pattern in WALL_TEXTURES.items():
            surface = pygame.Surface((TEXTURE_SIZE, TEXTURE_SIZE))
            for y in range(TEXTURE_SIZE):
                for x in range(TEXTURE_SIZE):
                    # Ensure color value is an integer in range 0-255
                    shade = int(min(255, max(0, pattern[y][x])))
                    surface.set_at((x, y), (shade, shade, shade))
            self.wall_textures[wall_type] = surface.convert()  # Convert for faster blitting
    
    def get_scaled_texture(self, wall_type, height):
        key = (wall_type, int(height))  # Convert height to int for proper caching
        if key not in self.scaled_textures:
            orig = self.wall_textures[wall_type]
            if height < 1:
                height = 1
            scaled = pygame.transform.scale(orig, (int(SCREEN_WIDTH / NUM_RAYS) + 1, int(height)))
            self.scaled_textures[key] = scaled.convert()  # Convert for faster blitting
        return self.scaled_textures[key]

texture_cache = TextureCache()

def cast_ray(angle):
    
    ray_x = math.cos(angle)
    ray_y = math.sin(angle)
    
    
    cell_x = int(player_x / CELL_SIZE)
    cell_y = int(player_y / CELL_SIZE)
    
    ray_length = 0
    distance = 0
    
    while distance < MAX_DEPTH:
        distance += 1
        ray_end_x = player_x + ray_x * distance
        ray_end_y = player_y + ray_y * distance
        
        
        test_cell_x = int(ray_end_x / CELL_SIZE)
        test_cell_y = int(ray_end_y / CELL_SIZE)
        
        
        if MAP[test_cell_y][test_cell_x] > 0:
            
            if abs(ray_end_x % CELL_SIZE) < 1:
                texture_pos = ray_end_y % CELL_SIZE / CELL_SIZE
            else:
                texture_pos = ray_end_x % CELL_SIZE / CELL_SIZE
            return distance, angle, MAP[test_cell_y][test_cell_x], texture_pos
            
    return distance, angle, 0, 0

def draw_scene():
    screen.fill(CEILING_COLOR, (0, 0, SCREEN_WIDTH, SCREEN_HEIGHT // 2))
    screen.fill(FLOOR_COLOR, (0, SCREEN_HEIGHT // 2, SCREEN_WIDTH, SCREEN_HEIGHT // 2))
    
    for i in range(NUM_RAYS):
        ray_angle = (player_angle - HALF_FOV) + (i / NUM_RAYS) * FOV
        distance, angle, wall_type, texture_pos = cast_ray(ray_angle)
        
        if wall_type > 0:
            # Fix fisheye effect
            distance = distance * math.cos(angle - player_angle)
            
            # Calculate wall height
            wall_height = (CELL_SIZE * SCREEN_HEIGHT) / (distance + 0.0001)
            
            # Calculate wall position
            wall_top = (SCREEN_HEIGHT - wall_height) / 2
            
            # Get cached texture
            texture = texture_cache.get_scaled_texture(wall_type, wall_height)
            
            # Apply fog effect to texture
            fog_factor = max(0, min(1, (MAX_DEPTH - distance) / MAX_DEPTH))
            if fog_factor < 1:
                texture = texture.copy()
                dark = pygame.Surface(texture.get_size()).convert_alpha()
                dark.fill((0, 0, 0, int(255 * (1 - fog_factor))))
                texture.blit(dark, (0, 0))
            
            # Draw wall slice
            x = i * (SCREEN_WIDTH / NUM_RAYS)
            screen.blit(texture, (x, wall_top))

running = True
clock = pygame.time.Clock()
fps_update_timer = 0
fps_update_interval = 0.5  

while running:
    
    dt = clock.tick(144) / 1000.0  
    
    
    fps_update_timer += dt
    if fps_update_timer >= fps_update_interval:
        fps = clock.get_fps()
        pygame.display.set_caption(f'2.5D Raycaster - FPS: {int(fps)}')
        fps_update_timer = 0

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
    
    
    keys = pygame.key.get_pressed()
    current_speed = player_speed * dt * 60  
    current_rotation = rotation_speed * dt * 60
    
    if keys[pygame.K_LEFT]:
        player_angle -= current_rotation
    if keys[pygame.K_RIGHT]:
        player_angle += current_rotation
    if keys[pygame.K_UP]:
        next_x = player_x + math.cos(player_angle) * current_speed
        next_y = player_y + math.sin(player_angle) * current_speed
        if MAP[int(next_y/CELL_SIZE)][int(next_x/CELL_SIZE)] == 0:
            player_x = next_x
            player_y = next_y
    if keys[pygame.K_DOWN]:
        next_x = player_x - math.cos(player_angle) * current_speed
        next_y = player_y - math.sin(player_angle) * current_speed
        if MAP[int(next_y/CELL_SIZE)][int(next_x/CELL_SIZE)] == 0:
            player_x = next_x
            player_y = next_y
    
    
    draw_scene()
    pygame.display.flip()

pygame.quit()