Skip to content

Instantly share code, notes, and snippets.

@tirinox
Last active March 26, 2024 11:33
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tirinox/0402862e357dfb7a22370dca6299c6da to your computer and use it in GitHub Desktop.
Save tirinox/0402862e357dfb7a22370dca6299c6da to your computer and use it in GitHub Desktop.
# ЧБ версия для Windows, установи библиотеку:
# pip install windows-curses
import curses
import locale
from math import pi, cos, sin
POS_X, POS_Y, POS_A = 2, 2, 0 # Положение и поворот игрока на карте (начальные)
FOV = pi / 2 # Ширина угла обзор в радинах
RESOLUTION = 0.1 # разрешение шага луча
DEPTH = 16 # Максимальная глубина прорисовки
SPEED = 0.3 # Скорость игрока вперед назад за одно нажатие
ROTATION_SPEED = 0.1 # скорость повотора игрока в радианах
# Наша карта строчкой
MAP = """
################
#..............#
#..............#
#...########...#
#.......#..#...#
#..........#...#
#..............#
#...########...#
#.......#..#...#
#.......####...#
#..............#
#....##..##....#
#...#...#..#...#
#....###...#...#
#..............#
################
"""
def make_map(string_map):
"""
Форматирует карту в одну строку без переносов для лучшего поиска
:param string_map: MAP
:return: форматированная карта, ширина, высота в блоках
"""
rows = string_map.strip().split('\n')
h = len(rows)
w = len(rows[0])
return string_map.replace('\n', ''), w, h
def main_3dwalk(screen):
# для корректного отображение юникода
locale.setlocale(locale.LC_ALL, '')
curses.noecho() # кнопки не печатаются на экране
curses.curs_set(0) # курсор убран
curses.start_color() # цветной режим
curses.use_default_colors() # стандартная палитра
# форматируем карту и получаем ее размеры
level_map, map_width, map_height = make_map(MAP)
def get_block(x, y):
"""
По координатам на карте возращает символ блока
# - это стена, все остальное свободное пространство
:param x: колонка
:param y: строка
:return: сивмвол блока
"""
x, y = int(x), int(y)
if 0 <= x < map_width and 0 <= y < map_height:
return level_map[y * map_width + x]
else:
return '#'
# текущие положение и угол
pos_x, pos_y, pos_a = POS_X, POS_Y, POS_A
exit_flag = False # флаг выхода
while not exit_flag:
# полачем размер экрана в каждом кадре, чтобы не глючить, если юзер изменил размер терминала
screen_height, screen_width = screen.getmaxyx()
# по колонкам "пикселей на экране"
for col in range(screen_width):
# 1. определим направление луча
# угол сканирует от pos_a - FOV / 2 до pos_a + FOV / 2
ray_angle = (pos_a - FOV / 2) + (col / screen_width) * FOV
# вектор, куда смотрит луч на карте
eye_x, eye_y = sin(ray_angle), cos(ray_angle)
# 2. Ищем ближайшую стену и дистанцию до нее
distance = 0.0
# пока не достигли стены и дистанция менее предельной
while distance < DEPTH:
# луч делает шаг вперед
distance += RESOLUTION
# "текущее" положение на луче
test_x = int(pos_x + eye_x * distance)
test_y = int(pos_y + eye_y * distance)
# смотрим карту, есть ли там стена или край
if get_block(test_x, test_y) == '#':
break # стена. расстояние в distance
# 3. Высота пола и потолка
# Чем дальше стена, тем меньше она вертикально занимает на экране, а ниже и вышее ее - потолок и пол
ceiling = int(screen_height / 2 - screen_height / distance) # высота потолка
floor = int(screen_height - ceiling) # высота пола
# рисуем вертикальную линию
for row in range(screen_height):
if row <= ceiling: # Ряд выше или равен границе потолка
shade = '`'
elif floor >= row > ceiling: # Кусок стены
if distance <= DEPTH / 4: # совсем близко
shade = "█"
elif distance <= DEPTH / 3: # ближе
shade = "▓"
elif distance <= DEPTH / 2: # дальше
shade = "▒"
elif distance <= DEPTH: # еще дальше
shade = "░"
else:
shade = " " # совсем далеко
else:
# Оттенок пола, чем ближе к низу экрана, тем гуще заливка
b = 1 - (row - screen_height / 2) / (screen_height / 2)
if b < 0.25:
shade = '#'
elif b < 0.5:
shade = "x"
elif b < 0.75:
shade = "."
else:
shade = ' '
# заменяем символ в row/col на shade с цветом color
screen.insstr(row, col, shade)
# информационная строка положения и поворота
screen.addstr(screen_height - 1, 0, f"x = {pos_x:.2f}, y = {pos_y:.2f}, a = {pos_a:.2f}")
screen.refresh() # отрисуем все
key_code = screen.getch() # ждем клавишу и обрабатываем
key = chr(key_code) if 0 < key_code < 256 else 0
if key in ('w', 's'):
# шаг вперед или назад
dx, dy = sin(pos_a) * SPEED, cos(pos_a) * SPEED
if key == 's': # назад - обратим вектор
dx, dy = -dx, -dy
# сдвинем игрока в направлении
pos_x += dx
pos_y += dy
if get_block(pos_x, pos_y) == '#': # усп, мы в стене
# отменим движение
pos_x -= dx
pos_y -= dy
elif key == 'a': # поворот налево
pos_a -= ROTATION_SPEED
elif key == 'd': # поворот направо
pos_a += ROTATION_SPEED
elif key == chr(27): # esc
break
# завершение работы, восстановление настроек
curses.endwin()
curses.wrapper(main_3dwalk)
import curses
import locale
from math import pi, cos, sin
POS_X, POS_Y, POS_A = 2, 2, 0 # Положение и поворот игрока на карте (начальные)
ROTATION_SPEED = 0.1 # скорость поворота игрока в радианах
SPEED = 0.3 # Скорость игрока вперед назад за одно нажатие
FOV = pi / 2 # Ширина угла обзор в радинах
RESOLUTION = 0.1 # разрешение шага луча
DEPTH = 16 # Максимальная глубина прорисовки
# Наша карта строчкой
MAP = """
################
#..............#
#..............#
#...########...#
#.......#..#...#
#..........#...#
#..............#
#...########...#
#.......#..#...#
#.......####...#
#..............#
#....##..##....#
#...#...#..#...#
#....###...#...#
#..............#
################
"""
def make_map(string_map):
"""
Форматирует карту в одну строку без переносов для лучшего поиска
:param string_map: MAP
:return: форматированная карта, ширина, высота в блоках
"""
rows = string_map.strip().split('\n')
h = len(rows)
w = len(rows[0])
return string_map.replace('\n', ''), w, h
def color_by_distance(d):
"""
Оттенок цвета в зависимости от расстояния в стандартной палитре
:param d: 0.0 -- 1.0
:return:
"""
d = min(1.0, max(0.0, d))
return int(233 + d * (254 - 233))
def main_3dwalk(screen):
# для корректного отображение юникода
locale.setlocale(locale.LC_ALL, '')
curses.noecho() # нажатые клавиши не печатаются на экране
curses.curs_set(0) # курсор убран
curses.start_color() # цветной режим
curses.use_default_colors() # стандартная палитра
# инициализация всех цветов!
for i in range(0, curses.COLORS):
curses.init_pair(i, i, -1)
# форматируем карту и получаем ее размеры
level_map, map_width, map_height = make_map(MAP)
def get_block(x, y):
"""
По координатам на карте возращает символ блока
# - это стена, все остальное свободное пространство
:param x: колонка
:param y: строка
:return: сивмвол блока
"""
x, y = int(x), int(y)
if 0 <= x < map_width and 0 <= y < map_height:
return level_map[y * map_width + x]
else:
return '#'
# текущие положение и угол
pos_x, pos_y, pos_a = POS_X, POS_Y, POS_A
exit_flag = False # флаг выхода
while not exit_flag:
# получаем размер экрана в каждом кадре, чтобы не глючить, если юзер изменил размер терминала
screen_height, screen_width = screen.getmaxyx()
# по колонкам "пикселей на экране"
for col in range(screen_width):
# 1. определим направление луча
# угол сканирует от pos_a - FOV / 2 до pos_a + FOV / 2
ray_angle = (pos_a - FOV / 2) + (col / screen_width) * FOV
# вектор, куда смотрит луч на карте
eye_x, eye_y = sin(ray_angle), cos(ray_angle)
# 2. Ищем ближайшую стену и дистанцию до нее
distance = 0.0
# пока не достигли стены и дистанция менее предельной
while distance < DEPTH:
# луч делает шаг вперед
distance += RESOLUTION
# "текущее" положение на луче
test_x = int(pos_x + eye_x * distance)
test_y = int(pos_y + eye_y * distance)
# смотрим карту, есть ли там стена или край
if get_block(test_x, test_y) == '#':
break # стена. расстояние в distance
# 3. Высота пола и потолка
# Чем дальше стена, тем меньше она вертикально занимает на экране, а ниже и вышее ее - потолок и пол
ceiling = int(screen_height / 2 - screen_height / distance) # высота потолка
floor = int(screen_height - ceiling) # высота пола
# рисуем вертикальную линию
for row in range(screen_height):
if row <= ceiling: # Ряд выше или равен границе потолка
shade = '`'
color = curses.COLOR_RED
elif floor >= row > ceiling: # Кусок стены
if distance <= DEPTH / 4: # совсем близко
shade = "█"
elif distance <= DEPTH / 3: # ближе
shade = "▓"
elif distance <= DEPTH / 2: # дальше
shade = "▒"
elif distance <= DEPTH: # еще дальше
shade = "░"
else:
shade = " " # совсем далеко
# оттенок цвета, нормированный на предельную дистанцию
color = color_by_distance(1 - (distance / DEPTH))
else:
# Оттенок пола, чем ближе к низу экрана, тем гуще заливка
b = 1 - (row - screen_height / 2) / (screen_height / 2)
if b < 0.25:
shade = '#'
elif b < 0.5:
shade = "x"
elif b < 0.75:
shade = "."
else:
shade = ' '
color = curses.COLOR_GREEN
# заменяем символ в row/col на shade с цветом color
screen.insstr(row, col, shade, curses.color_pair(color))
# информационная строка положения и поворота
screen.addstr(screen_height - 1, 0, f"x = {pos_x:.2f}, y = {pos_y:.2f}, a = {pos_a:.2f}")
screen.refresh() # отрисуем все
key_code = screen.getch() # ждем клавишу и обрабатываем
key = chr(key_code) if 0 < key_code < 256 else 0
if key in ('w', 's'):
# шаг вперед или назад
dx, dy = sin(pos_a) * SPEED, cos(pos_a) * SPEED
if key == 's': # назад - обратим вектор
dx, dy = -dx, -dy
# сдвинем игрока в направлении
pos_x += dx
pos_y += dy
if get_block(pos_x, pos_y) == '#': # усп, мы в стене
# отменим движение
pos_x -= dx
pos_y -= dy
elif key == 'a': # поворот налево
pos_a -= ROTATION_SPEED
elif key == 'd': # поворот направо
pos_a += ROTATION_SPEED
elif key_code == 27: # esc
break # выход из игры
# завершение работы, восстановление настроек
curses.endwin()
curses.wrapper(main_3dwalk)
@tirinox
Copy link
Author

tirinox commented Dec 21, 2020

ЧБ версия:
Снимок экрана 2020-12-21 в 14 46 20

Цветная версия:
Снимок экрана 2020-12-21 в 14 46 43

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment