import pygame import math import random import os WIDTH, HEIGHT = 800, 600 HALF_HEIGHT = HEIGHT // 2 FPS = 60 pygame.init() screen = pygame.display.set_mode((WIDTH, HEIGHT)) pygame.display.set_caption("3DMAZE") clock = pygame.time.Clock() TILE_SIZE = 64 fov = math.pi / 3 NUM_RAYS = 160 SCALE = WIDTH // NUM_RAYS # --- НАСТРОЙКА РАЗМЕРА ЛАБИРИНТА --- MAP_SIZE = 32 # Задайте любой размер (например, 32, 64, 128) world_map = {} def generate_maze(size): """Генерирует карту заданного размера с границами и случайными стенами""" grid = {} for row in range(size): for col in range(size): # Внешние границы лабиринта — всегда стены if col == 0 or col == size - 1 or row == 0 or row == size - 1: grid[(col, row)] = 1 # Стартовая зона 3х3 в левом верхнем углу — всегда пустая elif 1 <= col <= 3 and 1 <= row <= 3: grid[(col, row)] = 0 else: # Внутри — случайные стены (плотность 30%) grid[(col, row)] = 1 if random.random() < 0.30 else 0 return grid world_map = generate_maze(MAP_SIZE) # Игрок начинает в пустой зоне (клетка 2, 2) cell_x = 2 cell_y = 2 player_x = cell_x * TILE_SIZE + 32 player_y = cell_y * TILE_SIZE + 32 player_angle = 0.0 player_speed = 100 rotation_speed = 2.0 demo_mode = True day_mode = False current_dir_index = 0 DIRECTIONS = [0.0, math.pi / 2, math.pi, -math.pi / 2] ai_state = "MOVE" target_angle = 0.0 def get_wall_type(col, row): # Если вышли за пределы сгенерированной карты, возвращаем стену if (col, row) in world_map: return world_map[(col, row)] return 1 def load_or_generate_textures(): if os.path.exists("wall.png"): try: wall_tex = pygame.image.load("wall.png").convert() wall_tex = pygame.transform.scale(wall_tex, (TILE_SIZE, TILE_SIZE)) except Exception: wall_tex = None else: wall_tex = None if wall_tex is None: wall_tex = pygame.Surface((TILE_SIZE, TILE_SIZE)) wall_tex.fill((125, 45, 30)) for y in [0, 16, 32, 48, 63]: pygame.draw.line(wall_tex, (110, 105, 100), (0, y), (64, y), 2) for x in [0, 32, 64]: pygame.draw.line(wall_tex, (110, 105, 100), (x, 0), (x, 16), 2) pygame.draw.line(wall_tex, (110, 105, 100), (x, 32), (x, 48), 2) for x in [16, 48]: pygame.draw.line(wall_tex, (110, 105, 100), (x, 16), (x, 32), 2) pygame.draw.line(wall_tex, (110, 105, 100), (x, 48), (x, 64), 2) if os.path.exists("top.png"): try: ceil_tex = pygame.image.load("top.png").convert() ceil_tex = pygame.transform.scale(ceil_tex, (TILE_SIZE, TILE_SIZE)) except Exception: ceil_tex = None else: ceil_tex = None if ceil_tex is None: ceil_tex = pygame.Surface((TILE_SIZE, TILE_SIZE)) ceil_tex.fill((75, 75, 75)) if os.path.exists("floor.png"): try: floor_tex = pygame.image.load("floor.png").convert() floor_tex = pygame.transform.scale(floor_tex, (TILE_SIZE, TILE_SIZE)) except Exception: floor_tex = None else: floor_tex = None if floor_tex is None: floor_tex = pygame.Surface((TILE_SIZE, TILE_SIZE)) floor_tex.fill((45, 45, 50)) for i in range(0, TILE_SIZE, 16): pygame.draw.line(floor_tex, (25, 25, 28), (i, 0), (i, TILE_SIZE), 1) pygame.draw.line(floor_tex, (25, 25, 28), (0, i), (TILE_SIZE, i), 1) return wall_tex, ceil_tex, floor_tex tex_wall, tex_ceil, tex_floor = load_or_generate_textures() running = True while running: dt = clock.tick(FPS) / 1000.0 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_y: demo_mode = not demo_mode ai_state = "MOVE" elif event.key == pygame.K_l: day_mode = not day_mode if demo_mode: if ai_state == "MOVE": target_x = cell_x * TILE_SIZE + 32 target_y = cell_y * TILE_SIZE + 32 move_cos = round(math.cos(DIRECTIONS[current_dir_index])) move_sin = round(math.sin(DIRECTIONS[current_dir_index])) dist = math.hypot(target_x - player_x, target_y - player_y) if dist > 1.0: step = player_speed * dt if step > dist: step = dist player_x += move_cos * step player_y += move_sin * step else: # Мы дошли до центра клетки. Определяем, куда можно идти дальше. player_x, player_y = target_x, target_y # Проверяем все 4 направления: какое из них свободно? available_dirs = [] for idx, angle in enumerate(DIRECTIONS): t_cos = round(math.cos(angle)) t_sin = round(math.sin(angle)) if get_wall_type(cell_x + t_cos, cell_y + t_sin) == 0: available_dirs.append(idx) # Направление, противоположное текущему (откуда пришли) backward_dir_index = (current_dir_index + 2) % 4 # Фильтруем варианты, убирая путь назад (чтобы не топтаться на месте) forward_and_sides = [d for d in available_dirs if d != backward_dir_index] if forward_and_sides: # Если можно идти вперед, даем этому приоритет (например, 75%) if current_dir_index in forward_and_sides and random.random() < 0.75: next_dir = current_dir_index else: # В остальных случаях случайно выбираем поворот (налево/направо/вперед) next_dir = random.choice(forward_and_sides) else: # Если идти вообще некуда (тупик) — только тогда разворачиваемся назад next_dir = backward_dir_index if backward_dir_index in available_dirs else current_dir_index # Если направление изменилось, запускаем режим поворота камеры if next_dir != current_dir_index: current_dir_index = next_dir target_angle = DIRECTIONS[current_dir_index] ai_state = "TURN" else: # Если идем прямо, просто обновляем целевую клетку cell_x += move_cos cell_y += move_sin elif ai_state == "TURN": angle_diff = (target_angle - player_angle + math.pi) % (2 * math.pi) - math.pi if abs(angle_diff) > 0.01: player_angle += math.copysign(min(rotation_speed * dt, abs(angle_diff)), angle_diff) else: player_angle = target_angle move_cos = round(math.cos(DIRECTIONS[current_dir_index])) move_sin = round(math.sin(DIRECTIONS[current_dir_index])) cell_x += move_cos cell_y += move_sin ai_state = "MOVE" else: keys = pygame.key.get_pressed() if keys[pygame.K_LEFT] or keys[pygame.K_a]: player_angle -= 3.0 * dt if keys[pygame.K_RIGHT] or keys[pygame.K_d]: player_angle += 3.0 * dt cos_a, sin_a = math.cos(player_angle), math.sin(player_angle) mx = player_speed * cos_a * dt if (keys[pygame.K_w] or keys[pygame.K_UP]) else (-player_speed * cos_a * dt if (keys[pygame.K_s] or keys[pygame.K_DOWN]) else 0) my = player_speed * sin_a * dt if (keys[pygame.K_w] or keys[pygame.K_UP]) else (-player_speed * sin_a * dt if (keys[pygame.K_s] or keys[pygame.K_DOWN]) else 0) r = 12 if get_wall_type(int((player_x + mx + math.copysign(r, mx)) // TILE_SIZE), int(player_y // TILE_SIZE)) == 0: player_x += mx if get_wall_type(int(player_x // TILE_SIZE), int((player_y + my + math.copysign(r, my)) // TILE_SIZE)) == 0: player_y += my cell_x = int(player_x // TILE_SIZE) cell_y = int(player_y // TILE_SIZE) screen.fill((140, 140, 145) if day_mode else (15, 15, 15)) # Рэйкастинг start_angle = player_angle - fov / 2 for ray in range(NUM_RAYS): angle = start_angle + ray * (fov / NUM_RAYS) r_cos = math.cos(angle) if math.cos(angle) != 0 else 0.00001 r_sin = math.sin(angle) if math.sin(angle) != 0 else 0.00001 # Вертикали x_depth, x_texture = 9999, 0 x_step = TILE_SIZE / r_cos first_x = (int(player_x // TILE_SIZE) + 1) * TILE_SIZE if r_cos > 0 else int(player_x // TILE_SIZE) * TILE_SIZE current_depth_x = (first_x - player_x) / r_cos for _ in range(30): tx = player_x + current_depth_x * r_cos ty = player_y + current_depth_x * r_sin col = int((tx + (1 if r_cos > 0 else -1)) // TILE_SIZE) row = int(ty // TILE_SIZE) if get_wall_type(col, row) > 0: x_depth, x_texture = current_depth_x, int(ty % TILE_SIZE) break current_depth_x += abs(x_step) # Горизонтали y_depth, y_texture = 9999, 0 y_step = TILE_SIZE / r_sin first_y = (int(player_y // TILE_SIZE) + 1) * TILE_SIZE if r_sin > 0 else int(player_y // TILE_SIZE) * TILE_SIZE current_depth_y = (first_y - player_y) / r_sin for _ in range(30): tx = player_x + current_depth_y * r_cos ty = player_y + current_depth_y * r_sin col = int(tx // TILE_SIZE) row = int((ty + (1 if r_sin > 0 else -1)) // TILE_SIZE) if get_wall_type(col, row) > 0: y_depth, y_texture = current_depth_y, int(tx % TILE_SIZE) break current_depth_y += abs(y_step) if x_depth < y_depth: depth, x_tex_coord, is_vertical_wall = x_depth, x_texture, True else: depth, x_tex_coord, is_vertical_wall = y_depth, y_texture, False if depth < 9999: fixed_depth = depth * math.cos(player_angle - angle) fixed_depth = max(fixed_depth, 1) proj_height = min(int((TILE_SIZE / fixed_depth) * 600), HEIGHT * 2) wall_column = tex_wall.subsurface(x_tex_coord, 0, 1, TILE_SIZE) wall_column = pygame.transform.scale(wall_column, (SCALE, proj_height)) if day_mode: shadow = 15 if is_vertical_wall else 0 else: base_shadow = int(fixed_depth * 0.75) if is_vertical_wall: base_shadow += 20 shadow = max(0, min(255, base_shadow)) if shadow > 0: shadow_surf = pygame.Surface((SCALE, proj_height)) shadow_surf.fill((shadow, shadow, shadow)) wall_column.blit(shadow_surf, (0, 0), special_flags=pygame.BLEND_SUB) wall_y_pos = HALF_HEIGHT - proj_height // 2 screen.blit(wall_column, (ray * SCALE, wall_y_pos)) # Пол / Потолок cos_tilt = math.cos(player_angle - angle) for screen_y in range(0, wall_y_pos, 4): if screen_y >= HALF_HEIGHT: break current_dist = (TILE_SIZE * HEIGHT) / (2.0 * (HALF_HEIGHT - screen_y) * cos_tilt) tex_x = int(player_x + r_cos * current_dist) % TILE_SIZE tex_y = int(player_y + r_sin * current_dist) % TILE_SIZE f_shadow = 0 if day_mode else max(0, min(255, int(current_dist * 0.75))) c_color = tex_ceil.get_at((tex_x, tex_y)) if f_shadow > 0: c_color = (max(0, c_color[0] - f_shadow), max(0, c_color[1] - f_shadow), max(0, c_color[2] - f_shadow)) pygame.draw.rect(screen, c_color, (ray * SCALE, screen_y, SCALE, 4)) floor_y = HEIGHT - screen_y - 4 fl_color = tex_floor.get_at((tex_x, tex_y)) if f_shadow > 0: fl_color = (max(0, fl_color[0] - f_shadow), max(0, fl_color[1] - f_shadow), max(0, fl_color[2] - f_shadow)) pygame.draw.rect(screen, fl_color, (ray * SCALE, floor_y, SCALE, 4)) pygame.display.flip() pygame.quit()