Обложка: Это просто: пишем свою «Матрицу» на Python

Это просто: пишем свою «Матрицу» на Python

Изучать новое можно по-разному. Одним нравится сначала штудировать теорию, а потом применять её на практике. Другие предпочитают учиться исключительно на примерах, найденных на любимом Stackoverflow. А кто-то вообще загружает навыки управления вертолётом и технику боевых единоборств по выделенному каналу прямо в мозг.

В любом случае без практических упражнений не обойтись. После ускоренной загрузки знаний Нео всё равно должен пройти спарринг с Морфеусом, чтобы научиться применять терабайты загруженных навыков на практике. Только вот упражнения бывают разные. Одно дело — лихо взлететь под потолок и проломить балки восточного спортзала, и совсем другое — методично час за часом шлифовать своё мастерство.

Изображение: Wikimedia Commons

Определяем цели

Учебники программирования разнообразием примеров обычно не блещут. Встречаются, конечно, исключения, но в большинстве пособий упражнения похожи друг на друга и не особо интересны: создайте очередную адресную книгу, нарисуйте круг черепашкой, разработайте сайт магазина по продаже какой-нибудь «нужной» рекламной ерунды. Куда уж тут до достоверной имитации матрицы. Хотя…

Почему бы не взять всё это в свои руки и не начать придумывать упражнения самостоятельно? Да хоть бы и написать свою маленькую личную «Матрицу». Конечно, не ту, в которой будут небоскрёбы, стильные телефоны той эпохи и вездесущие непобедимые агенты Смиты. Для этого нам нужно ещё пару-тройку месяцев поучиться. Но любому начинающему программисту под силу написать модель культовой заставки со стекающими по экрану зелёными струйками цифр. Этим и займёмся.

Запрограммировать такую заставку можно практически на любом языке. Попробуем это сделать на «великом и могучем» Python.

Пишем техническое задание

Для начала определимся, что же мы хотим в итоге получить. Напишем сами себе небольшое техническое задание — всегда полезно. Подумайте про «Матрицу», и память сама подскажет образ — тёмное консольное окно, в котором будут стекать струйки зелёных цифр. Чтобы было интереснее, пусть они двигаются с разной скоростью. У каждой струйки должны быть начало — яркий зелёный ноль — и конец. Кстати, пусть скорости движения начала и конца струйки тоже будут разными и определяются случайным образом.

Звучит не так и сложно. Теперь дело за малым — всего лишь написать код. Приступим.

Готовим инструменты

Как полагается, подключим все необходимые модули. Сначала стандартные:

import sys
import random
import time

Для работы с консолью Windows нам понадобятся модули bext и colorama. Как их установить с помощью pip, вы уже, наверняка, знаете. Нужно их подключить. Но сделаем это по всем правилам — с проверкой:

try:
    import bext, colorama
except ImportError:
    print ('Для запуска программы нужны модули bext и colorama.')
    sys.exit ()

Подготовим к работе консоль:

bext.title ('Matrix') # Меняем заголовок консольного окна
bext.clear () # Очищаем консольное окно
bext.hide () # Скрываем курсор в консольном окне
width, height = bext.size () # Получаем размер консольного окна
width -= 1
height -= 1

Теперь осталось только завести константы с цветами для модуля colorama. Нам нужен зелёный и тёмно-зелёный:

lgreen = colorama.Fore.LIGHTGREEN_EX
green = colorama.Fore.GREEN

Windows — штука хитрая и противоречивая. Базовый цвет green в консоли — это тёмно-зелёный.

Изобретаем антиматерию

Теперь подумаем над самым главным вопросом: как же нам запрограммировать стекающие капли «Матрицы»? Каждая капля — это объект. Все капли разные, но ведут себя одинаково. Поэтому нам надо создать класс, в котором мы опишем основные действия с каплей и все её атрибуты.

Чтобы капли стекали с разной скоростью, зададим каждой из них случайный тайм-аут задержки — в шагах. Какие-то капли будут перемещаться на каждом шаге обновления нашей картинки, какие-то реже.

Как же быть с верхним концом струйки? Он должен «высыхать», причём со своей скоростью. Не будем придумывать ничего сложного. Пусть верхний конец тоже будет каплей, но чёрного цвета. Такая «капля» при перемещении будет не добавлять цифры, а наоборот стирать их. Получается прямо какая-то антикапля, антиматерия — красиво и стильно.

Поскольку капля и антикапля ведут себя одинаково, запрограммируем их в одном классе.

Создаём капли

Итак, все действия с каплями и антикаплями мы будем выполнять в методах класса. Назовём его Drop и напишем метод создания объекта класса:

def __init__ (self):
    self.x = random.randint (0, width) # Начальное положение по горизонтали
    self.y = -1 # Начальное положение по вертикали - за верхней границей экрана
    self.drop_type = random.randint (0, 1) # Тип: антикапля или капля
    self.timeout = random.randint (0, 3) # Задержка до следующего перемещения
    self.wait_count = random.randint (0, 3) # Счетчик паузы

С атрибутами x и y всё понятно. Второй равен -1, чтобы капля раньше времени не показывалась на экране. Атрибуты timeout и wait_count нужны для обеспечения разной скорости падения капель. Первый задаёт постоянную скорость стекания, второй — итерационный счётчик.

Перемещаем капли

Теперь напишем метод перемещения капли с учётом её скорости.

def move (self):
    if drop.wait_count < drop.timeout: # Пока рано перемещать
        drop.wait_count += 1 # Увеличиваем счётчик паузы
        return False
    else: # Уже можно перемещать
        drop.wait_count = 0 # Сбрасываем счётчик паузы
        drop.y += 1 # Перемещаем каплю или антикаплю на шаг вниз
        return True

Метод возвращает логическое значение — факт перемещения капли. Или антикапли — всё равно.

Рисуем струйку

С перемещением разобрались. Настало время рисовать.

def draw (self):
    if self.drop_type == 1:
        symbol = str (random.randint (1, 9))
        con_print (self.x, self.y, green, symbol)
        self.zero_draw () # Рисуем яркий ноль
    else:
        con_print (self.x, self.y, green, ' ')

Здесь мы вызываем два новых ещё не написанных метода: con_print и zero_draw. Первый будет выводить символ нужного цвета в указанное место консольного окна. Второй будет рисовать дополнительный яркий ноль в начале струйки.

Вот, собственно, и второй метод:

def zero_draw (self):
    if (self.y < height):
    con_print (self.x, self.y+1, lgreen, '0')

Организуем перерождение капель

Мы уже поняли, что у нас получается не простая «Матрица», а философская. Посему, когда капли и антикапли будут достигать нижнего края экрана, заставим их переродиться. Они получат новую жизнь у верхней границы. Это будет стоить нам всего двух строк:

def renew (self):
    self.__init__ ()

Теперь у нас есть всё, что нужно. Класс Drop готов.

Выводим текст в консоль

Вырвемся за границы класса Drop и напишем две функции для вывода текста в консольное окно.

Если мы попытаемся напечатать что-то в нижнем правом углу консольного окна, то снизу автоматически добавится ещё одна строка. Ничего не поделаешь: в этом месте в нашей идеальной «Матрице» будет «битый пиксель»:

def is_rb_corner (x, y):
    if x==width and y==height:
        return True
    else:
        return False

Теперь всё готово к тому, чтобы напечатать запрошенный символ в нужном месте.

def con_print (x, y, color, symbol):
    if not is_rb_corner (x, y):
        bext.goto (x, y)
        sys.stdout.write (color)
        print (symbol, end='')

Собираем всё воедино

Все компоненты нашей будущей «Матрицы» готовы. Теперь осталось всё собрать вместе и запустить.

Создадим массив капель и антикапель (какого типа родится объект — дело случая).

drops = []
for i in range (1, width*2//3):
    drop = Drop ()
    drops.append (drop)

И, наконец, самый главный цикл:

while True:
    for drop in drops:
        if drop.move (): # Проверяем перемещение элемента
            drop.draw () # Отображаем элемент
            if drop.y >= height: # Достигли дна
                drop.renew () # Обновляем элемент
    key = bext.getKey (blocking = False) # Проверяем, нажата ли клавиша
    if key == 'esc': # Если нажата ESC, то выходим из программы
        bext.clear ()
        sys.exit ()
    time.sleep (0.02) # Задержка

Вся программа целиком

import sys
import random
import time

# Подключаем дополнительные модули
try:
    import bext, colorama
except ImportError:
    print ('Для запуска программы нужны модули bext и colorama.')
    sys.exit ()

class Drop:
    def __init__ (self):
        self.x = random.randint (0, width) # Начальное положение по горизонтали
        self.y = -1 # Начальное положение по вертикали - за верхней границей экрана
        self.drop_type = random.randint (0, 1) # Тип: антикапля или капля
        self.timeout = random.randint (0, 3) # Задержка до следующего перемещения
        self.wait_count = random.randint (0, 3) # Счетчик паузы
    def renew (self):
        self.__init__ ()
    def move (self):
        if drop.wait_count < drop.timeout: # Пока рано перемещать
            drop.wait_count += 1 # Увеличиваем счётчик паузы
            return False
        else: # Уже можно перемещать
            drop.wait_count = 0 # Сбрасываем счётчик паузы
            drop.y += 1 # Перемещаем каплю или антикаплю на шаг вниз
            return True
    def draw (self):
        if self.drop_type == 1:
            symbol = str (random.randint (1, 9))
            con_print (self.x, self.y, green, symbol)
            self.zero_draw () # Рисуем яркий ноль
        else:
            con_print (self.x, self.y, green, ' ')
    def zero_draw (self):
        if (self.y < height):
            con_print (self.x, self.y+1, lgreen, '0')

def is_rb_corner (x, y):
    if x==width and y==height:
        return True
    else:
        return False

def con_print (x, y, color, symbol):
    if not is_rb_corner (x, y):
        bext.goto (x, y)
        sys.stdout.write (color)
        print (symbol, end='')

bext.title ('Matrix') # Меняем заголовок консольного окна
bext.clear () # Очищаем консольное окно
bext.hide () # Скрываем курсор в консольном окне
width, height = bext.size () # Получаем размер консольного окна
width -= 1
height -= 1

green = colorama.Fore.GREEN
lgreen = colorama.Fore.LIGHTGREEN_EX

# Создаём массив капель и антикапель
drops = []
for i in range (1, width*2//3):
    drop = Drop ()
    drops.append (drop)

while True:
    for drop in drops:
        if drop.move (): # Проверяем перемещение элемента
            drop.draw () # Отображаем элемент
            if drop.y >= height: # Достигли дна
                drop.renew () # Обновляем элемент
    key = bext.getKey (blocking = False) # Проверяем, нажата ли клавиша
    if key == 'esc': # Если нажата ESC, то выходим из программы
        bext.clear ()
        sys.exit ()
    time.sleep (0.02) # Задержка

Предлагаю вам поэкспериментировать с этой программой. Попробуйте добавить к «Матрице» свои дополнительные необычные эффекты. Интересно, что у вас получится.