code:
body {
	width: 600px;
	font-family: Sans Serif;
	background-color: black;
	color: gray;
	padding-bottom: 400px;
}
footnote {
	display: none;
	position: relative;
	font-size: .7em;
	vertical-align: top;
	margin: 0px 2px;
	color: #bada55;
	border-bottom: 1px solid;
	cursor: pointer;
}

footnote > div {
	position: absolute;
	left: 10px;
	top: -5px;
	padding: 12px;
	font-size: 1.1em;
	color: black;
	border: 1px dashed #bada55;
	background-color: gray;
	text-decoration: none;
	width: 230px;
	z-index: 200;
}

li {
	padding-bottom: 12px;
}

hr {
	border-color: #bada55;
	width: 30%;
	height: 1px;
	margin-top: 35px;
	margin-bottom: 35px;
}

/* tables */

table {
	border-collapse: collapse;
}

th {
	color: #222222;
	background-color: #7a9a15;
}

td, th {
	border: 1px solid #7a9a15;
	padding-top: 5px;
	padding-bottom: 5px;
	padding-left: 10px;
	padding-right: 5px;
}

td > a {
	color: #bada55;
}
td > a:visited {
	color: #bada55;
}

table.striped3 tbody tr:nth-child(3n) {
	background-color: #333333;
}
table.striped3 tbody tr:nth-child(12n) td {
	border-bottom-width: 4px !important;
}


code:
	<meta charset="utf8">

	<link rel="stylesheet" type="text/css" href="http://cdnjs.cloudflare.com/ajax/libs/metrics-graphics/2.4.0/metricsgraphics.min.css" />
	<link rel="stylesheet" type="text/css" href="./bada55.css" />

license: mit


code:
# grnMatrix

Программа представляет собой визуальную анимацию, имитирующую эффект "цифрового дождя" из фильма "Матрица".

repo_name: Grannik/grnMatrix

license: unknown


code:
#!/usr/bin/env python3
# TERMINAL_MODE
import curses
import random
import time
import math

# Переменные для настройки
SYMBOL_SET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*+-=[]{}|;:,.<>?/~│_─▔╴╵╶╷╌╎⌜⌝⌞⌟┌┐┘└╭╮╯╰┴┬⊺├┤┼⌈⌉⌊⌋"
HEAD_COLOR = "white"   # Цвет головы
TAIL_GRADIENT_COLORS = ["cyan", "magenta", "magenta"]
# Список цветов для градиента хвоста: "green" — зелёный, "red" — красный, "blue" — синий, "yellow" — жёлтый, "white" — белый, "cyan" — голубой, "magenta" — пурпурный
SYMBOL_FALL_SPEED = 0.125  # Базовая скорость падения символов (строки за кадр)
FADE_SPEED = 0.125  # Скорость затухания символов
FRAME_RATE = 20  # Частота обновления экрана (кадров в секунду)
TAIL_LENGTH_MIN = 5   # Минимальная длина хвоста колонки
TAIL_LENGTH_MAX = 34  # Максимальная длина хвоста колонки
FADE_STEPS = 256  # Максимальное количество шагов затухания символов
BOTTOM_FADE_BOOST = 1.0  # Коэффициент ускорения затухания внизу экрана
CHAR_CHANGE_RATE = 0.05  # Вероятность смены символа в хвосте
COLUMN_DENSITY = 0.6  # Доля ширины экрана, заполненная колонками
SYMBOL_BRIGHTNESS = 0.5  # Яркость символов (влияет на A_BOLD, A_NORMAL, A_DIM)
TAIL_FADE_CURVE = 0.7  # Кривая затухания хвоста
RANDOM_RESET_CHANCE = 0.002  # Вероятность случайного сброса колонки
PAUSE_DURATION = 0.0  # Время паузы колонок после сброса
BACKGROUND_CHAR = ' '  # Символ фона
SYMBOL_DIVERSITY = 1.0  # Доля символов из SYMBOL_SET
MOVEMENT_VARIATION = 0.05  # Диапазон отклонения скорости
PULSE_RATE = 0.3  # Частота пульсации яркости
SCREEN_FILL_RATE = 0.0  # Доля экрана, заполненная при старте
PROGRESS_FILLED_CHAR = '│'  # Символ для головы колонок
TEXT_PULSE_AMPLITUDE = 0.0  # Амплитуда пульсации яркости
EMOTION_INTENSITY = 0.6  # Интенсивность анимации

CHARS = list(SYMBOL_SET)[:int(len(SYMBOL_SET) * SYMBOL_DIVERSITY)]

class MatrixColumn:
    def __init__(self, max_y):
        self.max_y = max_y
        self.pos = random.randint(0, max_y - 1)
        self.speed = SYMBOL_FALL_SPEED + random.uniform(-MOVEMENT_VARIATION, MOVEMENT_VARIATION)
        self.tail_length = random.randint(TAIL_LENGTH_MIN, TAIL_LENGTH_MAX)
        self.pos_float = float(self.pos)
        self.tail_chars = [random.choice(CHARS) for _ in range(self.tail_length)]
        self.paused = PAUSE_DURATION > 0
        self.pause_time = time.time() if self.paused else 0

    def update(self, x, buffer, dirty_positions, color_pairs):
        if self.paused:
            if time.time() - self.pause_time >= PAUSE_DURATION:
                self.paused = False
            return

        self.pos_float += self.speed * EMOTION_INTENSITY
        new_pos = int(self.pos_float)

        if new_pos > self.max_y + self.tail_length or random.random() < RANDOM_RESET_CHANCE:
            for offset in range(self.tail_length):
                y = int(self.pos - offset)
                if 0 <= y < self.max_y and buffer[y][x][0] != BACKGROUND_CHAR:
                    buffer[y][x] = (BACKGROUND_CHAR, curses.color_pair(0), 0.0)
                    dirty_positions.add((y, x))
            self.pos = 0
            self.pos_float = 0.0
            self.tail_length = random.randint(TAIL_LENGTH_MIN, TAIL_LENGTH_MAX)
            self.tail_chars = [random.choice(CHARS) for _ in range(self.tail_length)]
            self.paused = PAUSE_DURATION > 0
            self.pause_time = time.time() if self.paused else 0
        else:
            old_tail_y = int(self.pos - self.tail_length)
            new_tail_y = new_pos - self.tail_length
            if 0 <= old_tail_y < self.max_y and old_tail_y < new_tail_y and buffer[old_tail_y][x][0] != BACKGROUND_CHAR:
                buffer[old_tail_y][x] = (BACKGROUND_CHAR, curses.color_pair(0), 0.0)
                dirty_positions.add((old_tail_y, x))

        self.pos = new_pos
        for i in range(self.tail_length):
            if random.random() < CHAR_CHANGE_RATE:
                self.tail_chars[i] = random.choice(CHARS)

        for offset in range(self.tail_length):
            y = self.pos - offset
            if 0 <= y < self.max_y:
                char = self.tail_chars[offset] if offset > 0 else PROGRESS_FILLED_CHAR
                fade_level = FADE_STEPS * (1 - (offset / self.tail_length) ** TAIL_FADE_CURVE)
                # Градиент цвета для хвоста
                if offset == 0:
                    attr = curses.A_BOLD | curses.color_pair(1)  # Голова
                else:
                    gradient_step = min(offset / (self.tail_length - 1), 1.0) if self.tail_length > 1 else 0
                    color_index = int(gradient_step * (len(color_pairs) - 2)) + 2  # Пропускаем пару 0 и 1
                    attr = curses.A_NORMAL | curses.color_pair(color_index)
                    if SYMBOL_BRIGHTNESS < 0.5 and offset > self.tail_length // 2:
                        attr = curses.A_DIM | curses.color_pair(color_index)
                if buffer[y][x] != (char, attr, fade_level):
                    buffer[y][x] = (char, attr, fade_level)
                    dirty_positions.add((y, x))

def main(stdscr):
    curses.curs_set(0)
    stdscr.timeout(0)

    # Инициализация цветов
    curses.start_color()
    curses.use_default_colors()
    color_map = {
        "green": curses.COLOR_GREEN, "red": curses.COLOR_RED, "blue": curses.COLOR_BLUE,
        "yellow": curses.COLOR_YELLOW, "white": curses.COLOR_WHITE, "cyan": curses.COLOR_CYAN,
        "magenta": curses.COLOR_MAGENTA
    }
    head_color = color_map.get(HEAD_COLOR.lower(), curses.COLOR_WHITE)
    curses.init_pair(1, head_color, -1)  # Цвет головы
    # Инициализация градиента цветов для хвоста
    tail_colors = [color_map.get(c.lower(), curses.COLOR_YELLOW) for c in TAIL_GRADIENT_COLORS]
    color_pairs = [1]  # Начальная пара для головы
    for i, color in enumerate(tail_colors, start=2):
        curses.init_pair(i, color, -1)
        color_pairs.append(i)
    stdscr.bkgd(BACKGROUND_CHAR, curses.color_pair(0))

    max_y, max_x = stdscr.getmaxyx()
    num_columns = int(max_x * COLUMN_DENSITY)
    columns = [MatrixColumn(max_y) for _ in range(num_columns)]
    buffer = [[(BACKGROUND_CHAR, curses.color_pair(0), 0.0) for _ in range(max_x)] for _ in range(max_y)]
    dirty_positions = set()

    if SCREEN_FILL_RATE > 0:
        for y in range(max_y):
            for x in range(max_x):
                if random.random() < SCREEN_FILL_RATE:
                    stdscr.addch(y, x, random.choice(CHARS), curses.A_DIM | curses.color_pair(2))
        stdscr.refresh()
        time.sleep(0.01)

    pulse_time = time.time()
    while True:
        current_max_y, current_max_x = stdscr.getmaxyx()
        if current_max_y != max_y or current_max_x != max_x:
            max_y, max_x = current_max_y, current_max_x
            num_columns = int(max_x * COLUMN_DENSITY)
            columns = [MatrixColumn(max_y) for _ in range(num_columns)]
            buffer = [[(BACKGROUND_CHAR, curses.color_pair(0), 0.0) for _ in range(max_x)] for _ in range(max_y)]
            stdscr.clear()
            dirty_positions.clear()

        pulse_factor = 1.0
        if PULSE_RATE > 0:
            pulse_factor = 1.0 + TEXT_PULSE_AMPLITUDE * math.sin(2 * math.pi * PULSE_RATE * (time.time() - pulse_time))

        for y in range(max_y):
            for x in range(max_x):
                char, attr, fade_level = buffer[y][x]
                if fade_level > 0:
                    fade_factor = FADE_SPEED * (1 + BOTTOM_FADE_BOOST * (y / max_y)) * pulse_factor
                    new_fade_level = max(fade_level - fade_factor * EMOTION_INTENSITY, 0)
                    if new_fade_level != fade_level:
                        if new_fade_level == 0:
                            buffer[y][x] = (BACKGROUND_CHAR, curses.color_pair(0), 0.0)
                        else:
                            buffer[y][x] = (char, attr, new_fade_level)
                        dirty_positions.add((y, x))

        for i, col in enumerate(columns):
            col.max_y = max_y
            x = i * (max_x - 1) // (num_columns - 1) if num_columns > 1 else 0
            col.update(x, buffer, dirty_positions, color_pairs)

        for y, x in dirty_positions:
            char, attr, fade_level = buffer[y][x]
            try:
                if fade_level > FADE_STEPS * 0.8:
                    stdscr.addch(y, x, char, curses.A_BOLD | attr)
                elif fade_level > FADE_STEPS * 0.6:
                    stdscr.addch(y, x, char, curses.A_NORMAL | attr)
                else:
                    stdscr.addch(y, x, char, curses.A_DIM | attr)
            except curses.error:
                pass
        dirty_positions.clear()

        stdscr.refresh()
        key = stdscr.getch()
        if key in (ord('q'), ord('Q')):
            break

        time.sleep(1 / FRAME_RATE)

if __name__ == "__main__":
    curses.wrapper(main)


code:
 Приветствую рузья! Представляю вам свой (совместно с ИИ) питонический труд.
 *** grnMatrix ***
 И так что было до сих пор:
 - cmatrix     Programming language: C           Github: https://github.com/abishekvashok/cmatrix
 - unimatrix   Programming language: Python      Github: https://github.com/will8211/unimatrix
 - greenrain   Programming language: C           Github: https://github.com/aguegu/greenrain
 - tmatrix     Programming language: C++         Github: https://github.com/M4444/TMatrix
 - bash_matrix Programming language: Bash        Github: Примеры: https://github.com/rdok/matrix-script, https://github.com/jcgonzalez25/MatrixRainBash
                                     JavaScript: Нет единого официального репозитория. Варианты можно глягуть у меня на сайте: https://grannik.neocities.org/
 Теперь сравнение:
 Отметить можно только две программы это
 cmatrix   - Наиболее знаменитый вариант. Классика жанра для терминала. А так же
 unimatrix - Наиболее хорошо написанная и исполняемая программа.

 Так что же меня сподвигло меня самому (совместно с ИИ) создать подобную программу, хотя есть аж целых две программы действительно заслуживающих внимания?
 Оратимся с вопросом к мнению ИИ и почитаем его мнение?
 Проанализируй мой скрипт. Сравни его с cmatrix unimatrix greenrain tmatrix bash_matrix. Кратко назови те положительные черты, ради которых он был написан.
 Его ответ.
 Положительные черты твоего скрипта
 Твой скрипт явно создавался с целью улучшить визуальный и функциональный опыт "цифрового дождя". Вот ключевые преимущества, ради которых он был написан:
 + Гибкость настройки: Огромное количество параметров (скорость, длина хвоста, затухание, плотность колонок, пульсация) позволяет пользователю адаптировать эффект под
 свои предпочтения.
 + Визуальная сложность: Градиент цветов хвоста, кривая затухания и пульсация яркости создают более богатый и современный вид по сравнению с классическими аналогами.
 + Разнообразие символов: Поддержка широкого набора символов (включая Unicode) делает эффект более разнообразным и уникальным.
 + Динамичность: Случайный сброс колонок, изменение символов в хвосте и адаптация к размеру окна добавляют живости и интерактивности.
 + Эмоциональная выразительность: Параметр EMOTION_INTENSITY и пульсация подчеркивают стремление к созданию "живого" и атмосферного эффекта.
 Итог
 Скрипт выделяется среди аналогов благодаря детальной настройке и визуальной изысканности. Он ближе к unimatrix по духу (Python, гибкость),
 но идет дальше за счет уникальных эффектов вроде градиента и пульсации. По сравнению с cmatrix и tmatrix, он менее оптимизирован для скорости,
 но компенсирует это богатством возможностей. Это отличный выбор для тех, кто хочет не просто "Матрицу", а персонализированный и выразительный терминал-арт.

 Да. Всё верно. Все цели достигнуты. Могу только от себя добавить. Создатели cmatrix unimatrix умнички и молодцы! Действительно постарались.
 * Дополню только, что к примеру мне не понятно зачем аргументы программе или конфигурационный файл, когда это иоткрытый исходный код.
 Я специально вынес все переменные в начало скрипта.
 * И как самая вкусная еда - это еда выращенная на своём огороде и приготовленная в собственной кухне,
 так и самый лучший скрипт - это скрипт написанный тобой и под тебя.
 * У меня повышенные эстетические требования ко всем этим скриптам. Требования, которые не выдерживает ни одна программа.
 Я понимаю, что это моя проблема. Ну так вот я и решил эту проблему.
 * Ещё одна причина это то, что данная программа - это часть ЕДИНОГО комплекса моего личного софта, над которым я работаю не первый год,
 а коиплекс - это целый живой организм в моём компьютере.

 Подробное техническое описание программы grnMatrix.

 * Назначение программы
 Программа представляет собой визуальную анимацию, имитирующую эффект "цифрового дождя" из фильма "Матрица". Символы падают сверху вниз по колонкам в терминале,
 создавая динамичный и атмосферный эффект. Реализация использует библиотеку curses для управления терминалом.
 * Основные характеристики
 - **Язык программирования**: Python 3
 - **Библиотека**: `curses` (управление терминалом)
 - **Режим работы**: Терминальный интерфейс
 - **Особенности**:
 - Настраиваемые символы, цвета и градиенты.
 - Плавное падение символов с затуханием хвоста.
 - Адаптация к изменению размера окна терминала.
 - Поддержка пульсации яркости и случайных сбросов колонок.

 * Структура программы
 1. **Глобальные настройки**:
   - Переменные для конфигурации анимации (например, `SYMBOL_SET`, `HEAD_COLOR`, `TAIL_GRADIENT_COLORS`, `SYMBOL_FALL_SPEED` и др.).
   - Определяют поведение и внешний вид эффекта.

 2. **Класс `MatrixColumn`**:
   - Отвечает за управление одной колонкой "дождя".
   - Хранит позицию, скорость, длину хвоста и символы.

 3. **Функция `main`**:
   - Основной цикл программы.
   - Инициализирует `curses`, управляет отрисовкой и обновлением экрана.

 4. **Точка входа**:
   - Использует `curses.wrapper(main)` для корректной инициализации и завершения работы с терминалом.

 ### Основные компоненты
#### Настройка и кастомизация
Пользователь может изменять поведение и внешний вид анимации "Matrix Rain" через глобальные переменные, определенные в начале программы. Ниже приведен полный список параметров с объяснением их назначения и инструкциями по настройке:

- `[1] **`SYMBOL_SET`**`:
  - **Описание**: Строка, содержащая набор символов, используемых в анимации (цифры, буквы, специальные знаки, Unicode-символы).
  - **Значение по умолчанию**: `"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!@#$%^&*+-=[]{}|;:,.<>?/~│_─▔╴╵╶╷╌╎⌜⌝⌞⌟┌┐┘└╭╮╯╰┴┬⊺├┤┼⌈⌉⌊⌋"`
  - **Как настроить**: Измените строку, добавляя или удаляя символы. Например, для использования только цифр: `SYMBOL_SET = "0123456789"`.

- `[2] **`HEAD_COLOR`**`:
  - **Описание**: Цвет символа в голове каждой колонки.
  - **Значение по умолчанию**: `"white"`
  - **Как настроить**: Укажите один из поддерживаемых цветов: `"green"`, `"red"`, `"blue"`, `"yellow"`, `"white"`, `"cyan"`, `"magenta"`. Например, для красного: `HEAD_COLOR = "red"`.

- `[3] **`TAIL_GRADIENT_COLORS`**`:
  - **Описание**: Список цветов, формирующих градиент хвоста колонки.
  - **Значение по умолчанию**: `["cyan", "magenta", "magenta"]`
  - **Как настроить**: Измените список, указав от 1 до нескольких цветов из списка: `"green"`, `"red"`, `"blue"`, `"yellow"`, `"white"`, `"cyan"`, `"magenta"`. Например, для зеленого градиента: `TAIL_GRADIENT_COLORS = ["green", "yellow"]`.

- `[4] **`SYMBOL_FALL_SPEED`**`:
  - **Описание**: Базовая скорость падения символов (в строках за кадр).
  - **Значение по умолчанию**: `0.125`
  - **Как настроить**: Увеличьте для ускорения (например, `0.25`) или уменьшите для замедления (например, `0.05`).

- `[5] **`FADE_SPEED`**`:
  - **Описание**: Скорость затухания символов в хвосте (чем выше, тем быстрее исчезают).
  - **Значение по умолчанию**: `0.125`
  - **Как настроить**: Увеличьте (например, `0.25`) для быстрого затухания или уменьшите (например, `0.05`) для более длинного шлейфа.

- `[6] **`FRAME_RATE`**`:
  - **Описание**: Частота обновления экрана (кадров в секунду).
  - **Значение по умолчанию**: `20`
  - **Как настроить**: Увеличьте (например, `30`) для более плавной анимации или уменьшите (например, `10`) для экономии ресурсов.

- `[7] **`TAIL_LENGTH_MIN`**`:
  - **Описание**: Минимальная длина хвоста колонок (в строках).
  - **Значение по умолчанию**: `5`
  - **Как настроить**: Установите меньшее значение, например, `TAIL_LENGTH_MIN = 3`, для более коротких хвостов.

- `[8] **`TAIL_LENGTH_MAX`**`:
  - **Описание**: Максимальная длина хвоста колонок (в строках).
  - **Значение по умолчанию**: `34`
  - **Как настроить**: Установите большее или меньшее значение, например, `TAIL_LENGTH_MAX = 10`, для контроля максимальной длины хвостов.

- `[9] **`FADE_STEPS`**`:
  - **Описание**: Максимальное количество шагов затухания символов (влияет на плавность перехода).
  - **Значение по умолчанию**: `256`
  - **Как настроить**: Уменьшите (например, `128`) для более резкого затухания или увеличьте (например, `512`) для большей плавности.

- `[10] **`BOTTOM_FADE_BOOST`**`:
  - **Описание**: Коэффициент ускорения затухания в нижней части экрана.
  - **Значение по умолчанию**: `1.0`
  - **Как настроить**: Увеличьте (например, `2.0`) для быстрого исчезновения внизу или уменьшите (например, `0.5`) для равномерного затухания.

- `[11] **`CHAR_CHANGE_RATE`**`:
  - **Описание**: Вероятность смены символа в хвосте на каждом кадре.
  - **Значение по умолчанию**: `0.05`
  - **Как настроить**: Увеличьте (например, `0.1`) для частой смены символов или уменьшите (например, `0.01`) для большей стабильности.

- `[12] **`COLUMN_DENSITY`**`:
  - **Описание**: Доля ширины экрана, заполненная колонками (от 0 до 1).
  - **Значение по умолчанию**: `0.6`
  - **Как настроить**: Увеличьте (например, `0.9`) для плотного заполнения или уменьшите (например, `0.3`) для редких колонок.

- `[13] **`SYMBOL_BRIGHTNESS`**`:
  - **Описание**: Уровень яркости символов (влияет на использование `A_BOLD`, `A_NORMAL`, `A_DIM`).
  - **Значение по умолчанию**: `0.5`
  - **Как настроить**: Увеличьте (например, `0.75`) для более ярких символов или уменьшите (например, `0.25`) для тусклых.

- `[14] **`TAIL_FADE_CURVE`**`:
  - **Описание**: Кривая затухания хвоста (влияет на распределение яркости).
  - **Значение по умолчанию**: `0.7`
  - **Как настроить**: Увеличьте (например, `1.0`) для линейного затухания или уменьшите (например, `0.3`) для резкого перехода.

- `[15] **`RANDOM_RESET_CHANCE`**`:
  - **Описание**: Вероятность случайного сброса колонки на каждом кадре.
  - **Значение по умолчанию**: `0.002`
  - **Как настроить**: Увеличьте (например, `0.01`) для частых сбросов или уменьшите (например, `0.0005`) для редких.

- `[16] **`PAUSE_DURATION`**`:
  - **Описание**: Время паузы (в секундах) после сброса колонки.
  - **Значение по умолчанию**: `0.0`
  - **Как настроить**: Установите, например, `1.0` для паузы в 1 секунду.

- `[17] **`BACKGROUND_CHAR`**`:
  - **Описание**: Символ фона экрана.
  - **Значение по умолчанию**: `' '`
  - **Как настроить**: Замените, например, на `'.'` для точечного фона.

- `[18] **`SYMBOL_DIVERSITY`**`:
  - **Описание**: Доля символов из `SYMBOL_SET`, используемых в анимации (от 0 до 1).
  - **Значение по умолчанию**: `1.0`
  - **Как настроить**: Уменьшите (например, `0.5`) для использования половины символов.

- `[19] **`MOVEMENT_VARIATION`**`:
  - **Описание**: Диапазон случайного отклонения скорости для каждой колонки.
  - **Значение по умолчанию**: `0.05`
  - **Как настроить**: Увеличьте (например, `0.1`) для большего разнообразия скоростей.

- `[20] **`PULSE_RATE`**`:
  - **Описание**: Частота пульсации яркости (в циклах в секунду).
  - **Значение по умолчанию**: `0.3`
  - **Как настроить**: Увеличьте (например, `0.5`) для быстрой пульсации или установите `0.0` для отключения.

- `[21] **`SCREEN_FILL_RATE`**`:
  - **Описание**: Доля экрана, заполненная случайными символами при запуске (от 0 до 1).
  - **Значение по умолчанию**: `0.0`
  - **Как настроить**: Установите, например, `0.2` для заполнения 20% экрана.

- `[22] **`PROGRESS_FILLED_CHAR`**`:
  - **Описание**: Символ, используемый для головы колонок.
  - **Значение по умолчанию**: `'│'`
  - **Как настроить**: Замените, например, на `'█'` для другого вида головы.

- `[23] **`TEXT_PULSE_AMPLITUDE`**`:
  - **Описание**: Амплитуда пульсации яркости (влияет на интенсивность эффекта).
  - **Значение по умолчанию**: `0.0`
  - **Как настроить**: Увеличьте (например, `0.5`) для заметной пульсации.

- `[24] **`EMOTION_INTENSITY`**`:
  - **Описание**: Общий множитель интенсивности анимации (влияет на скорость и затухание).
  - **Значение по умолчанию**: `0.6`
  - **Как настроить**: Увеличьте (например, `1.0`) для более активной анимации или уменьшите (например, `0.3`) для спокойной.

 Для изменения любого параметра достаточно открыть файл программы в текстовом редакторе и отредактировать значение соответствующей переменной в секции настроек
 перед запуском.
 2. Класс `MatrixColumn`
 - **Конструктор `__init__`**:
  - Принимает максимальную высоту экрана (`max_y`).
  - Инициализирует позицию (`pos`), скорость (`speed`), длину хвоста (`tail_length`) и символы хвоста (`tail_chars`).
  - Поддерживает паузу после сброса (`paused`).

 - **Метод `update`**:
  - Обновляет позицию колонки с учетом скорости и интенсивности (`EMOTION_INTENSITY`).
  - При достижении низа экрана или случайном сбросе (`RANDOM_RESET_CHANCE`) перезапускает колонку.
  - Обновляет символы хвоста с вероятностью `CHAR_CHANGE_RATE`.
  - Применяет градиент цвета и яркость к символам.

 3. Функция `main`
 - **Инициализация**:
  - Отключает курсор (`curses.curs_set(0)`).
  - Настраивает цвета через `curses.init_pair` (голова и градиент хвоста).
  - Создает буфер экрана и список колонок (`columns`).

 - **Основной цикл**:
  - Адаптируется к изменению размера окна терминала.
  - Применяет пульсацию яркости (если `PULSE_RATE > 0`).
  - Обновляет состояние колонок и буфер экрана.
  - Отрисовывает только измененные позиции (`dirty_positions`) для оптимизации.
  - Управляет частотой кадров через `time.sleep(1 / FRAME_RATE)`.

 - **Выход**:
  - Завершает работу при нажатии клавиш 'q' или 'Q'.

 Алгоритм работы
 1. Инициализация терминала и цветов.
 2. Создание колонок с случайными параметрами.
 3. В цикле:
   - Обновление позиций и символов колонок.
   - Применение затухания к хвостам.
   - Отрисовка измененных позиций на экране.
   - Проверка ввода пользователя.
 4. Завершение при выходе.

 Ограничения
 - Работает только в терминале с поддержкой `curses`.
 - Производительность зависит от размера окна и частоты кадров.

 Пример запуска: ./grnMatrix.py
 Завершает работу при нажатии клавиш 'q' или 'Q'.

 asciinema: https://asciinema.org/a/709941
 codeberg: https://codeberg.org/Grannik/grnMatrix
 github: https://github.com/Grannik/grnMatrix
 gitlab: https://gitlab.com/grannik/grnmatrix
 sourceforge: https://sourceforge.net/projects/grnmatrix/


code:
 Nuitka для компиляции grnMatrix.py на Linux. Инструкцию строго по одной строке, чтобы всё было максимально понятно.
 Предполагаю, что у тебя уже установлен Python (версия 3.x) и ты работаешь в терминале.

 Инструкция по компиляции grnMatrix.py с помощью Nuitka на Linux
 Открой терминал. Убедись, что у тебя установлен Python: python3 --version
 Установи компилятор GCC (нужен для Nuitka): sudo apt update
 Установи GCC, если его нет: sudo apt install gcc
 Проверь, что GCC установлен: gcc --version
 Установи менеджер пакетов pip, если его нет: sudo apt install python3-pip
 Проверь версию pip: pip3 --version
 Установи Nuitka через pip: pip3 install nuitka
 Проверь, что Nuitka установлена: nuitka --version
 Перейди в директорию, где лежит твой файл grnMatrix.py (замени /path/to/your/folder на свой путь): cd /path/to/your/folder
 Убедись, что файл grnMatrix.py там: ls grnMatrix.py
 Установи зависимости, если их ещё нет (в твоем случае нужна только curses, которая обычно встроена в Python на Linux):
 pip3 install -r requirements.txt
 (Если у тебя нет requirements.txt, пропусти этот шаг, так как curses стандартная библиотека.)
 Установи patchelf, который требуется для --standalone на Linux: sudo apt install patchelf
 Проверь, что patchelf установлен: patchelf --version
 Запусти компиляцию с Nuitka, создавая автономный исполняемый файл: nuitka --standalone --onefile grnMatrix.py
 Дождись завершения компиляции (это может занять 1-2 минуты, в терминале будут сообщения о процессе).
 Проверь, что файл появился (он будет называться grnMatrix): ls grnMatrix
 Дай файлу права на исполнение: chmod +x grnMatrix
 Запусти скомпилированный файл: ./grnMatrix
 Наблюдай анимацию "Matrix Rain" в терминале!
 Чтобы выйти из программы, нажми q или Q (как указано в коде).
 Ты уже можешь запустить grnMatrix.bin:
 Убедись, что у файла есть права на выполнение: chmod +x grnMatrix.bin
 Запусти: ./grnMatrix.bin
 Если всё работает, а место на диске хочется сэкономить, удали временные папки:
 rm -rf grnMatrix.build grnMatrix.dist grnMatrix.onefile-build
 Теперь у тебя останется только grnMatrix.bin и исходный grnMatrix.py.
 Примечания:
 Флаг --standalone включает все зависимости, а --onefile создает один файл вместо папки.
 Размер файла может быть около 5-15 МБ, что нормально для Nuitka.
 Убедись, что у тебя достаточно места на диске и прав для записи в текущей папке.
 Размер файла: du -h grnMatrix.py 12K
 Размер файла: du -h grnMatrix.bin 5.3M
 Разница в размерах между grnMatrix.py (12K) и grnMatrix.bin (5.3M) объясняется тем, как работает Nuitka и что именно включено в скомпилированный файл.
 Вот почему так происходит:
 grnMatrix.py (12K) Это просто исходный текстовый файл с твоим кодом на Python. Он содержит только сам скрипт — несколько строк логики для анимации "Matrix Rain"
 с использованием библиотеки curses. Его маленький размер — это просто текст без зависимостей или интерпретатора.
 grnMatrix.bin (5.3M) Это полноценный автономный исполняемый файл, созданный Nuitka с опциями --standalone и --onefile. Он включает:
 Скомпилированный код: Твой Python-код переведён в C и скомпилирован в машинный код.
 Интерпретатор Python: Nuitka встраивает части Python (например, стандартную библиотеку), чтобы программа могла работать без установленного Python на компьютере.
 Зависимости: Даже если ты явно используешь только curses (встроенная библиотека), Nuitka включает дополнительные модули Python и системные библиотеки,
 которые могут понадобиться.
 Сжатие: Хотя Nuitka сжимает данные (в твоём случае с 16.8M до 5.3M, как было в логах), итоговый размер всё равно больше исходного кода из-за всего встроенного.
 Почему такая разница?
 Исходный файл — это "рецепт", который требует Python и библиотек на твоей системе для работы.
 Скомпилированный файл — это "готовое блюдо", которое работает само по себе, но для этого в него "упаковали кухню" (интерпретатор и зависимости).
 Это нормально? Да, размер 5-15 МБ для автономного файла от Nuitka — типичное явление, особенно с --standalone.
 Если бы ты убрал эту опцию, размер был бы меньше, но тогда программа зависела бы от установленного Python и библиотек на машине.

 grnMatrixPy>time python3 grnMatrix.py
 real 0m1.773s
 user 0m0.465s
 sys  0m0.050s

 grnMatrixPy>time ./grnMatrix.bin
 real 0m2.430s
 user 0m0.827s
 sys  0m0.143s

 Краткое объяснение
 real: Общее время работы.        У grnMatrix.py — 1.773 с, у grnMatrix.bin — 2.430 с. Python быстрее.
 user: Время CPU на код.          У .py          — 0.465 с, у .bin          — 0.827 с. Python меньше нагружает CPU.
 sys:  Время на системные вызовы. У .py          — 0.050 с, у .bin          — 0.143 с. Python меньше использует системные ресурсы.
 Краткий вывод
 grnMatrix.py (с Python) работает быстрее и потребляет меньше ресурсов, чем grnMatrix.bin.
 Скорее всего, из-за простоты скрипта компиляция в бинарник добавляет накладные расходы, которые не окупаются.
 Зачем создали Nuitka и бинарники?
 Автономность: Бинарник работает без Python и зависимостей на любой машине. Не надо ставить интерпретатор или библиотеки — это удобно для распространения софта.
 Защита кода: Исходник .py легко открыть и изменить, а бинарник сложнее разобрать. Это важно для коммерческих проектов.
 Потенциальная скорость: Для сложных программ с вычислениями (не анимаций) скомпилированный код может быть быстрее интерпретируемого Python.
 Nuitka убирает накладные расходы интерпретатора.
 Упаковка: Один файл проще раздавать, чем кучу .py с зависимостями.
 В данном случае бинарник — это как грузовик для перевозки пёрышка: можно, но велосипед (Python) справляется лучше.
 Если цель — просто запустить анимацию, .py хватает за глаза.


code:
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3

[[package]]
name = "adler32"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234"

[[package]]
name = "ahash"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e"

[[package]]
name = "audir-sles"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea47348666a8edb7ad80cbee3940eb2bccf70df0e6ce09009abe1a836cb779f5"

[[package]]
name = "audrey"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58b92a84e89497e3cd25d3672cd5d1c288abaac02c18ff21283f17d118b889b8"
dependencies = [
 "dasp_frame",
 "dasp_sample",
 "hound",
 "lewton",
]

[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"

[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"

[[package]]
name = "bumpalo"
version = "3.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899"

[[package]]
name = "bytemuck"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdead85bdec19c194affaeeb670c0e41fe23de31459efd1c174d049269cf02cc"

[[package]]
name = "byteorder"
version = "1.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"

[[package]]
name = "cc"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"

[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"

[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"

[[package]]
name = "color_quant"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"

[[package]]
name = "crc32fast"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [
 "cfg-if 1.0.0",
]

[[package]]
name = "dasp_frame"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a3937f5fe2135702897535c8d4a5553f8b116f76c1529088797f2eee7c5cd6"
dependencies = [
 "dasp_sample",
]

[[package]]
name = "dasp_sample"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c87e182de0887fd5361989c677c4e8f5000cd9491d6d563161a8f3a5519fc7f"

[[package]]
name = "deflate"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73770f8e1fe7d64df17ca66ad28994a0a623ea497fa69486e14984e715c5d174"
dependencies = [
 "adler32",
 "byteorder",
]

[[package]]
name = "fen2"
version = "0.1.0"
dependencies = [
 "hlua",
 "lazy_static",
 "macroquad",
 "notify",
]

[[package]]
name = "filetime"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c"
dependencies = [
 "cfg-if 1.0.0",
 "libc",
 "redox_syscall",
 "winapi 0.3.9",
]

[[package]]
name = "fontdue"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75712fff1702bac51b7eaa5a5ca9f9853b8055ef5906088a32f4fe196595a1d"
dependencies = [
 "hashbrown",
 "ttf-parser",
]

[[package]]
name = "fsevent"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
dependencies = [
 "bitflags",
 "fsevent-sys",
]

[[package]]
name = "fsevent-sys"
version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0"
dependencies = [
 "libc",
]

[[package]]
name = "fuchsia-zircon"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
dependencies = [
 "bitflags",
 "fuchsia-zircon-sys",
]

[[package]]
name = "fuchsia-zircon-sys"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"

[[package]]
name = "glam"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "333928d5eb103c5d4050533cec0384302db6be8ef7d3cebd30ec6a35350353da"

[[package]]
name = "hashbrown"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
dependencies = [
 "ahash",
]

[[package]]
name = "hlua"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "943be6896e8b77886a45a0fbb823efa6550a17cf01a8745a8a24435c117144ea"
dependencies = [
 "libc",
 "lua52-sys",
]

[[package]]
name = "hound"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a164bb2ceaeff4f42542bdb847c41517c78a60f5649671b2a07312b6e117549"

[[package]]
name = "image"
version = "0.23.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24ffcb7e7244a9bf19d35bf2883b9c080c4ced3c07a9895572178cdb8f13f6a1"
dependencies = [
 "bytemuck",
 "byteorder",
 "color_quant",
 "num-iter",
 "num-rational",
 "num-traits",
 "png",
]

[[package]]
name = "inotify"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f"
dependencies = [
 "bitflags",
 "inotify-sys",
 "libc",
]

[[package]]
name = "inotify-sys"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
dependencies = [
 "libc",
]

[[package]]
name = "iovec"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
dependencies = [
 "libc",
]

[[package]]
name = "kernel32-sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
dependencies = [
 "winapi 0.2.8",
 "winapi-build",
]

[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"

[[package]]
name = "lazycell"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"

[[package]]
name = "lewton"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d542c1a317036c45c2aa1cf10cc9d403ca91eb2d333ef1a4917e5cb10628bd0"
dependencies = [
 "byteorder",
 "ogg",
 "smallvec",
]

[[package]]
name = "libc"
version = "0.2.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"

[[package]]
name = "log"
version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
 "cfg-if 1.0.0",
]

[[package]]
name = "lua52-sys"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d451db153c94e455dc817d388f9674f6232425c28db3509e90251c55b8df2f94"
dependencies = [
 "cc",
 "libc",
 "pkg-config",
]

[[package]]
name = "macroquad"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b723ade71357d07177c769af9ae42beff73c854e827ee5a4ec765dd2beb95e1"
dependencies = [
 "bumpalo",
 "fontdue",
 "glam",
 "image",
 "macroquad_macro",
 "miniquad",
 "quad-rand",
 "quad-snd",
]

[[package]]
name = "macroquad_macro"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5cecfede1e530599c8686f7f2d609489101d3d63741a6dc423afc997ce3fcc8"

[[package]]
name = "maybe-uninit"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00"

[[package]]
name = "miniquad"
version = "0.3.0-alpha.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1462459fb1813decc7115ab4aed8f41183612557b08bf604bfb46b05286d703"
dependencies = [
 "sapp-android",
 "sapp-darwin",
 "sapp-dummy",
 "sapp-ios",
 "sapp-linux",
 "sapp-wasm",
 "sapp-windows",
]

[[package]]
name = "miniz_oxide"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "791daaae1ed6889560f8c4359194f56648355540573244a5448a83ba1ecc7435"
dependencies = [
 "adler32",
]

[[package]]
name = "mio"
version = "0.6.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
dependencies = [
 "cfg-if 0.1.10",
 "fuchsia-zircon",
 "fuchsia-zircon-sys",
 "iovec",
 "kernel32-sys",
 "libc",
 "log",
 "miow",
 "net2",
 "slab",
 "winapi 0.2.8",
]

[[package]]
name = "mio-extras"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
dependencies = [
 "lazycell",
 "log",
 "mio",
 "slab",
]

[[package]]
name = "miow"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
dependencies = [
 "kernel32-sys",
 "net2",
 "winapi 0.2.8",
 "ws2_32-sys",
]

[[package]]
name = "ndk-sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1bcdd74c20ad5d95aacd60ef9ba40fdf77f767051040541df557b7a9b2a2121"

[[package]]
name = "net2"
version = "0.2.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
dependencies = [
 "cfg-if 0.1.10",
 "libc",
 "winapi 0.3.9",
]

[[package]]
name = "notify"
version = "4.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae03c8c853dba7bfd23e571ff0cff7bc9dceb40a4cd684cd1681824183f45257"
dependencies = [
 "bitflags",
 "filetime",
 "fsevent",
 "fsevent-sys",
 "inotify",
 "libc",
 "mio",
 "mio-extras",
 "walkdir",
 "winapi 0.3.9",
]

[[package]]
name = "num-integer"
version = "0.1.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
dependencies = [
 "autocfg",
 "num-traits",
]

[[package]]
name = "num-iter"
version = "0.1.43"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252"
dependencies = [
 "autocfg",
 "num-integer",
 "num-traits",
]

[[package]]
name = "num-rational"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "12ac428b1cb17fce6f731001d307d351ec70a6d202fc2e60f7d4c5e42d8f4f07"
dependencies = [
 "autocfg",
 "num-integer",
 "num-traits",
]

[[package]]
name = "num-traits"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd"
dependencies = [
 "autocfg",
]

[[package]]
name = "ogg"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13e571c3517af9e1729d4c63571a27edd660ade0667973bfc74a67c660c2b651"
dependencies = [
 "byteorder",
]

[[package]]
name = "pkg-config"
version = "0.3.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae"

[[package]]
name = "png"
version = "0.16.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3c3287920cb847dee3de33d301c463fba14dda99db24214ddf93f83d3021f4c6"
dependencies = [
 "bitflags",
 "crc32fast",
 "deflate",
 "miniz_oxide",
]

[[package]]
name = "quad-alsa-sys"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c66c2f04a6946293477973d85adc251d502da51c57b08cd9c997f0cfd8dcd4b5"
dependencies = [
 "libc",
]

[[package]]
name = "quad-rand"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "658fa1faf7a4cc5f057c9ee5ef560f717ad9d8dc66d975267f709624d6e1ab88"

[[package]]
name = "quad-snd"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86e0b4259cfd6a317a46df7b7cb4c09a08ba150642e6f6fb7df5a6b3450a0a29"
dependencies = [
 "audir-sles",
 "audrey",
 "libc",
 "quad-alsa-sys",
 "winapi 0.3.9",
]

[[package]]
name = "redox_syscall"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
dependencies = [
 "bitflags",
]

[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
 "winapi-util",
]

[[package]]
name = "sapp-android"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c0d0e6f562c01c533f693ac9c045d69cbeab24d2e16caaaa0e67d06ae6e0940"
dependencies = [
 "libc",
 "ndk-sys",
]

[[package]]
name = "sapp-darwin"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1282369f486bc334efa2d76fa4d54c20b37664d83da14fc60afbe941814e374"
dependencies = [
 "cc",
]

[[package]]
name = "sapp-dummy"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66f1ad26a5b6c682b9ca27c66db9aa91002b8d98a82ac7101ded57285215a478"
dependencies = [
 "libc",
]

[[package]]
name = "sapp-ios"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "081e6e5261c9ac2e938979b6a854a53b439f065fc3c897205ce7e69d3028b4a9"
dependencies = [
 "cc",
]

[[package]]
name = "sapp-linux"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc71510a7781d8f31d58046a9117682c8e8546ad43f52c0e31275db214495c3d"
dependencies = [
 "libc",
]

[[package]]
name = "sapp-wasm"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00e859e8645a3bcb85aecd40bab883438e4105f21b21bccbeac2348760f508bb"

[[package]]
name = "sapp-windows"
version = "0.2.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "088f921b591fdcabf5f2dc17237be5203b6a1a6988f252d36e153f1442a8138e"
dependencies = [
 "winapi 0.3.9",
]

[[package]]
name = "slab"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"

[[package]]
name = "smallvec"
version = "0.6.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0"
dependencies = [
 "maybe-uninit",
]

[[package]]
name = "ttf-parser"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ae2f58a822f08abdaf668897e96a5656fe72f5a9ce66422423e8849384872e6"

[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
 "same-file",
 "winapi 0.3.9",
 "winapi-util",
]

[[package]]
name = "winapi"
version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"

[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
 "winapi-i686-pc-windows-gnu",
 "winapi-x86_64-pc-windows-gnu",
]

[[package]]
name = "winapi-build"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"

[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"

[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
 "winapi 0.3.9",
]

[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

[[package]]
name = "ws2_32-sys"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
dependencies = [
 "winapi 0.2.8",
 "winapi-build",
]


code:
[package]
name = "fen2"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
# miniquad = "0.3.2"
lazy_static = "1.4.0"
macroquad = "0.3.16"
hlua = "0.3"
notify = "4.0.17"


code:
## TODO

### basics

- [ ] lua bindings
  - [x] print to console (this was just lua 'print')
  - [ ] draw pixel
  - [ ] draw primitives
  - [x] draw font
    - [ ] newlines
  - [ ] clear screen
  - [x] blit texture

### systems

- [ ] 'application' struct
  - [x] inject bindings
  - [x] load fennel and loaders
  - [ ] folder 'app' structure
    - [ ] run lua from 'app' cwd
    - [ ] `app.fnl`, can require other files normally
    - [ ] watch files, live reload (https://docs.rs/notify/latest/notify/)
  - [ ] error handling (at least just print the error and recover)

## OS
- [ ] 'app' processes, focused app
- [ ] `input` and `update` routing
- [ ] "operating system" is just a fennel app that can manage window draw_targets, focus

- [ ] img format
- [ ] img section blitting
- [ ] pattern fill
- [ ] post processing that enforces 2 bit color

- [ ] fen2 user storage
  - [ ] windows, linux, localStorage
- [ ] sandbox filesystem access

# Log

## 5-19-2022

Figured out https://docs.rs/notify/5.0.0-pre.15/notify/ has a `try_recv` to not block, seems to be reliable (i.e. not missing any events). Next I want to match on `NoticeWrite` event and reload the specific lua file in question.

Reading https://technomancy.us/189 to figure out how to reload fennel files, it mentions `package.path` which might help set each app's root path.

Believe i have a reload fn (had to slightly modify the emacs quoted one from that link) but I'm having trouble understanding the lua and fennel module system, mainly i just want to call a fn i defined in the previous require

## 5-20-2022

The `reload` works so next I want to move it into some sort of utility module. After that I'd like to have file changes reload the specific module that file created (I'm guessing there might be some metadata in the package table).  I imagine it will be tricky testing paths against each other.

I have a `system.fnl` module for general fennel code.  Wanted to move the test drawing snippet into `app.fnl` but realized I would need to pump an `update`, tried setting that up by calling `app.update` if it exists but after reload `app` is nil.. oh duh reload probably needs to return something.

This is working great, some TODO for tomorrow:

-[x] delta in update
-[ ] clear screen
-[ ] input polling

## 5-21-2022

Good morning, I am reading about https://docs.rs/notify/latest/notify/enum.DebouncedEvent.html, think `NoticeWrite` really indicates some writing events are starting so maybe i should use `Write` with like a 100ms debounce window. Actually now that I play with it the double reload when i first start changing a file is not from two notify events, must be some fennel thing with `reload`?

"clear screen" brings up a consideration, am i going to be drawing to one texture or providing bindings to create textures that get drawn in the shader.

Tinkering with macroquad's text rendering, I want pixel perfect bitmap fonts, it uses truetype.  There's no exposed filtering options for the text renderer, but it looks like if I guess the right font size with a pixel ttf font i can get no dithering (using size 16 with http://www.pentacom.jp/pentacom/bitfontmaker2/gallery/?id=381 for example). I can just hard code the font sizes and force the user to use integer text locations.

I still need to figure out how to make the fonts global, the ttf loader is async which i can't put in lazy static i guess.

Ended up using a `Mutex<HashMap<String, Font>>` which like.. doesn't feel great but it's working.

## 5-22-2022

Good morning, I would like to sort out sprite rendering today.  Had some issues loading textures because it was async and lua bindings are sync, ended up with a mutex hashset of image paths to load that gets drained in the main loop. (goofy but it's working).

Next i probably need to look into
- [ ] image alpha
- [ ] clipping (maybe render textures? all I really need is squares)

Closing thoughts for the night, `(print (fennel.view package.loaded))` shows all loaded code keyed by the module path, apparently they can have `/` or `.` separators

```
   :files.testapp.util {:frog #<function: 000001A42BF9DDE0>}
   :files/testapp/app {:update #<function: 000001A42C080940>}
   :files/testapp/util {:frog #<function: 000001A42C1CA680>}
```

I'll just need to identify which of these path fragment keys match the file path that changed and `reload` all matching keys

## 5-23-2022

Let's try setting the working directory for the lua processes and resolving the specific file that changed.

changing the working directory to the "app" but first problem is my fennel and system files are elsewhere, worried that lua needs to be spun up with the right cwd for it's searchers. Guess i could load those two files from rust/hlua..

OK thinking my easiest route is to add BASE_PATH to package.path, then i should be able to load `fennel.lua` and `system.fnl` with no fuss. ... This worked (had to add it to fennel.path as well) My app is running with it's own root path, and seems like `load_img` and `require` is also working from this new root.

Small fix giving the test app an absolute root path, eventually these will come from scanning the user file system.

Oh a caveat i was in the app cwd when the first `_handle_unloaded_textures` was called, so image loading worked but the keys were app local.  I probably want to either fully qualify the image paths or have a texture store for each app.

## 5-24-2022

Yeah lets have a per app texture store, then the memory should be freed as well when an app closes. ... ..... ... OK that didn't work out. I tried changing the lua bindings to closures so they could call app methods for the texture stuff (because app now had the texture store) but to Rust that means moving app into the closure.

Apparently that global Mutex wasn't the worst pattern for this.  I can probably do something like:
* give each app a uuid
* have a hashmap of hashmaps for each app's uuid

That'll let me avoid collisions and clear texture memory when I destruct the apps.

## 5-25-2022

Fennel game jam started today, going to try and do something platformer-like while working on this this week. Maybe good goals for tonight will be mouse input and cursors.

mouse input done, there's no built in cursor stuff. `show_mouse` is very hit or miss, at least as a runtime thing you toggle.

I got the filewatcher reload to target the exact file, which is great! should be able to reload images in the future as well when they change.

## 5-26-2022

Going to do jam stuff tonight: working on a sprite sheet and a map editor.


code:
(var globals (require "globals"))
(var v (require "v"))
(var ui (require "ui"))
(var util (require "util"))
(var grid (require "grid"))
(var view (require "view"))
(var editor (require "editor"))
(var entity (require "entity"))
(var bucket (require "deps/bucket"))

(set _G.prn (fn prn [x] (print (fennel.view x))))
(var prn _G.prn)

;TODO
; [x] hurt collisions
; [x] hurt animation
; [x] jump pickups
; [x] star pickups
; [x] dead state
; [x] in game notifications (drop down message rect)
; [x] win condition (just a notification)
; [x] content
; [x] simple title screen
; [ ] fix multiple jump used bug

; [x] sounds
; [ ] fx entities

(var state _G.state)

(set _G.time 0)

(var tile 0)

(load_img "title.png")
(load_img "world_sprites.png")
(load_img "background_sprites.png")
(load_img "entities.png")

(load_sound "audio/blip.wav")
(load_sound "audio/buzz.wav")
(load_sound "audio/death1.wav")
(load_sound "audio/death2.wav")
(load_sound "audio/death3.wav")
(load_sound "audio/hurt1.wav")
(load_sound "audio/jump.wav")
(load_sound "audio/jump2.wav")
(load_sound "audio/jump3.wav")
(load_sound "audio/pickup1.wav")
(load_sound "audio/pickup2.wav")
(load_sound "audio/pickup3.wav")
(load_sound "audio/restart.wav")

(var window (view.new 170 20 450 440))

(var player (entity.new :player))
(set window.camera player.pos)
(set window.camera-target player.pos)
(var guys [])

(fn new-game []
  (set player (entity.new :player))
  (set guys (util.map (fn [e] (let [e (util.copy e)] (set e.initial-pos e.pos) e)) state.entities))
  (set _G.guys guys)
  (table.insert guys player)
  (set _G.view_bucket {:size 600 :prop "pos"})
  (set _G.collision_bucket {:size 16 :prop "pos"})
  (entity.stores guys)

  (set _G.max_jumps 0)
  (set _G.jumps 0)
  (set _G.max_health 1)
  (set _G.health 1)

  (set _G.stars 0)
  (set _G.max_stars (# (util.filter (fn [e] (= e.type :star)) guys))))

(fn restart []
  (play_sound "audio/restart.wav" false 0.3)
  ;TODO should swap player in guys for fresh table?
  (set _G.notifications [])
  (bucket.bdel _G.view_bucket player)
  (bucket.bdel _G.collision_bucket player)
  (set player (entity.new :player))
  (entity.stores [player])
  (set _G.guys (util.filter (fn [e] (not (= e.type :player))) guys))
  (table.insert _G.guys player)
  (each [i guy (ipairs _G.guys)]
    (set guy.pos guy.initial-pos))

  (set _G.jumps _G.max_jumps)
  (set _G.health _G.max_health))

(new-game)

;(set state.entities (util.map (fn [e] (set e.initial-pos nil)) state.entities))
;(prn state.entities)

(fn open-editor []
  (set _G.mouse_bad 20)
  (set _G.editor_window.camera (util.copy player.pos))
  (set _G.editor_window.camera-target (util.copy player.pos))
  (set _G.mode :editor))

(fn update [dt]
  (set _G.time (+ _G.time dt))
  (set _G.dt dt)
  (if (key_pressed "escape") (set _G.mode :menu))
  (if (key_pressed "e") (open-editor))
  (when (key_pressed "r") (set _G.mode :game) (restart))
  (when (key_pressed "key0") (set _G.mode :game) (new-game))
  (when (= _G.mode :editor)
    (editor.update dt))
  (when (= _G.mode :game)
    (set _G.window window)
    (view.draw window state)
    (let [viewable (bucket.bget _G.view_bucket window.camera)]
      (entity.controls [player])
      (entity.collisions [player])
      (entity.sprites viewable)
      (entity.gravities viewable)
      (entity.velocities viewable)
      (entity.physics viewable)
      (entity.ais viewable))
    (view.draw-notifications window)
    (view.mask-view window state)

    (draw_text "JUMPMINSTER" 170 8 true)

    (if player.touching-floor
      (draw_text "FLOOR" 170 470 true))
    (if player.touching-wall
      (draw_text "WALL" 210 470 true))

    (draw_text "HEARTS" 20 27 true)
    (ui.icon-bar _G.health _G.max_health 20 37 11 (v.v2 0 3))

    (draw_text "STARS " 20 80 true)
    (ui.icon-bar _G.stars _G.max_stars 20 90 11 (v.v2 0 4))

    (draw_text "JUMPS"  20 140 true)
    (ui.icon-bar _G.jumps _G.max_jumps 20 150 11 (v.v2 0 5))

    (draw_text "SPACE TO JUMP"  20 300 true)
    (draw_text "ARROWS TO MOVE"  20 320 true)
    (draw_text "R TO RESET"  20 340 true)
    (draw_text "ESC TO MENU"  20 360 true)

    (draw_text "FIND JUMP BAGS"  20 420 true)
    (draw_text "FIND HEARTS"  20 440 true)
    (draw_text "FIND THE STARS"  20 460 true)

    ; camera should stay within X distance of player
    (let [cam-dist (v.dist window.camera-target player.pos)]
      (if (> cam-dist 50)
        (let [to-player (v.vmul (v.vnorm (v.vsub player.pos window.camera-target)) (- cam-dist 50))]
          (set window.camera-target (v.vadd window.camera-target to-player)))))
    (set window.camera (util.vlerp window.camera window.camera-target 0.1)))

  (when (= _G.mode :menu)
    (draw_rect 0 0 640 480 true)
    (draw_text "selfsame.itch.io presents"  258 50 false)
    (draw_img "title.png" 100 80)
    (ui.button 220 280 200 30 "enter game" (fn [] (set _G.mode :game)))
    (ui.button 220 320 200 30 "new game" (fn [] (new-game) (set _G.mode :game)))
    (ui.button 220 360 200 30 "map editor" open-editor))
  (draw_text (.. "FPS: " (math.floor (/ 1 dt))) 580 8 true))

{:update update}


code:
-- binser.lua

--[[
Copyright (c) 2016-2019 Calvin Rose

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
]]

local assert = assert
local error = error
local select = select
local pairs = pairs
local getmetatable = getmetatable
local setmetatable = setmetatable
local type = type
local loadstring = loadstring or load
local concat = table.concat
local char = string.char
local byte = string.byte
local format = string.format
local sub = string.sub
local dump = string.dump
local floor = math.floor
local frexp = math.frexp
local unpack = unpack or table.unpack

-- Lua 5.3 frexp polyfill
-- From https://github.com/excessive/cpml/blob/master/modules/utils.lua
if not frexp then
    local log, abs, floor = math.log, math.abs, math.floor
    local log2 = log(2)
    frexp = function(x)
        if x == 0 then return 0, 0 end
        local e = floor(log(abs(x)) / log2 + 1)
        return x / 2 ^ e, e
    end
end

local function pack(...)
    return {...}, select("#", ...)
end

local function not_array_index(x, len)
    return type(x) ~= "number" or x < 1 or x > len or x ~= floor(x)
end

local function type_check(x, tp, name)
    assert(type(x) == tp,
        format("Expected parameter %q to be of type %q.", name, tp))
end

local bigIntSupport = false
local isInteger
if math.type then -- Detect Lua 5.3
    local mtype = math.type
    bigIntSupport = loadstring[[
    local char = string.char
    return function(n)
        local nn = n < 0 and -(n + 1) or n
        local b1 = nn // 0x100000000000000
        local b2 = nn // 0x1000000000000 % 0x100
        local b3 = nn // 0x10000000000 % 0x100
        local b4 = nn // 0x100000000 % 0x100
        local b5 = nn // 0x1000000 % 0x100
        local b6 = nn // 0x10000 % 0x100
        local b7 = nn // 0x100 % 0x100
        local b8 = nn % 0x100
        if n < 0 then
            b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
            b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
        end
        return char(212, b1, b2, b3, b4, b5, b6, b7, b8)
    end]]()
    isInteger = function(x)
        return mtype(x) == 'integer'
    end
else
    isInteger = function(x)
        return floor(x) == x
    end
end

-- Copyright (C) 2012-2015 Francois Perrad.
-- number serialization code modified from https://github.com/fperrad/lua-MessagePack
-- Encode a number as a big-endian ieee-754 double, big-endian signed 64 bit integer, or a small integer
local function number_to_str(n)
    if isInteger(n) then -- int
        if n <= 100 and n >= -27 then -- 1 byte, 7 bits of data
            return char(n + 27)
        elseif n <= 8191 and n >= -8192 then -- 2 bytes, 14 bits of data
            n = n + 8192
            return char(128 + (floor(n / 0x100) % 0x100), n % 0x100)
        elseif bigIntSupport then
            return bigIntSupport(n)
        end
    end
    local sign = 0
    if n < 0.0 then
        sign = 0x80
        n = -n
    end
    local m, e = frexp(n) -- mantissa, exponent
    if m ~= m then
        return char(203, 0xFF, 0xF8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
    elseif m == 1/0 then
        if sign == 0 then
            return char(203, 0x7F, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
        else
            return char(203, 0xFF, 0xF0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00)
        end
    end
    e = e + 0x3FE
    if e < 1 then -- denormalized numbers
        m = m * 2 ^ (52 + e)
        e = 0
    else
        m = (m * 2 - 1) * 2 ^ 52
    end
    return char(203,
                sign + floor(e / 0x10),
                (e % 0x10) * 0x10 + floor(m / 0x1000000000000),
                floor(m / 0x10000000000) % 0x100,
                floor(m / 0x100000000) % 0x100,
                floor(m / 0x1000000) % 0x100,
                floor(m / 0x10000) % 0x100,
                floor(m / 0x100) % 0x100,
                m % 0x100)
end

-- Copyright (C) 2012-2015 Francois Perrad.
-- number deserialization code also modified from https://github.com/fperrad/lua-MessagePack
local function number_from_str(str, index)
    local b = byte(str, index)
    if not b then error("Expected more bytes of input.") end
    if b < 128 then
        return b - 27, index + 1
    elseif b < 192 then
        local b2 = byte(str, index + 1)
        if not b2 then error("Expected more bytes of input.") end
        return b2 + 0x100 * (b - 128) - 8192, index + 2
    end
    local b1, b2, b3, b4, b5, b6, b7, b8 = byte(str, index + 1, index + 8)
    if (not b1) or (not b2) or (not b3) or (not b4) or
        (not b5) or (not b6) or (not b7) or (not b8) then
        error("Expected more bytes of input.")
    end
    if b == 212 then
        local flip = b1 >= 128
        if flip then -- negative
            b1, b2, b3, b4 = 0xFF - b1, 0xFF - b2, 0xFF - b3, 0xFF - b4
            b5, b6, b7, b8 = 0xFF - b5, 0xFF - b6, 0xFF - b7, 0xFF - b8
        end
        local n = ((((((b1 * 0x100 + b2) * 0x100 + b3) * 0x100 + b4) *
            0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
        if flip then
            return (-n) - 1, index + 9
        else
            return n, index + 9
        end
    end
    if b ~= 203 then
        error("Expected number")
    end
    local sign = b1 > 0x7F and -1 or 1
    local e = (b1 % 0x80) * 0x10 + floor(b2 / 0x10)
    local m = ((((((b2 % 0x10) * 0x100 + b3) * 0x100 + b4) * 0x100 + b5) * 0x100 + b6) * 0x100 + b7) * 0x100 + b8
    local n
    if e == 0 then
        if m == 0 then
            n = sign * 0.0
        else
            n = sign * (m / 2 ^ 52) * 2 ^ -1022
        end
    elseif e == 0x7FF then
        if m == 0 then
            n = sign * (1/0)
        else
            n = 0.0/0.0
        end
    else
        n = sign * (1.0 + m / 2 ^ 52) * 2 ^ (e - 0x3FF)
    end
    return n, index + 9
end

local function newbinser()

    -- unique table key for getting next value
    local NEXT = {}
    local CTORSTACK = {}

    -- NIL = 202
    -- FLOAT = 203
    -- TRUE = 204
    -- FALSE = 205
    -- STRING = 206
    -- TABLE = 207
    -- REFERENCE = 208
    -- CONSTRUCTOR = 209
    -- FUNCTION = 210
    -- RESOURCE = 211
    -- INT64 = 212
    -- TABLE WITH META = 213

    local mts = {}
    local ids = {}
    local serializers = {}
    local deserializers = {}
    local resources = {}
    local resources_by_name = {}
    local types = {}

    types["nil"] = function(x, visited, accum)
        accum[#accum + 1] = "\202"
    end

    function types.number(x, visited, accum)
        accum[#accum + 1] = number_to_str(x)
    end

    function types.boolean(x, visited, accum)
        accum[#accum + 1] = x and "\204" or "\205"
    end

    function types.string(x, visited, accum)
        local alen = #accum
        if visited[x] then
            accum[alen + 1] = "\208"
            accum[alen + 2] = number_to_str(visited[x])
        else
            visited[x] = visited[NEXT]
            visited[NEXT] =  visited[NEXT] + 1
            accum[alen + 1] = "\206"
            accum[alen + 2] = number_to_str(#x)
            accum[alen + 3] = x
        end
    end

    local function check_custom_type(x, visited, accum)
        local res = resources[x]
        if res then
            accum[#accum + 1] = "\211"
            types[type(res)](res, visited, accum)
            return true
        end
        local mt = getmetatable(x)
        local id = mt and ids[mt]
        if id then
            local constructing = visited[CTORSTACK]
            if constructing[x] then
                error("Infinite loop in constructor.")
            end
            constructing[x] = true
            accum[#accum + 1] = "\209"
            types[type(id)](id, visited, accum)
            local args, len = pack(serializers[id](x))
            accum[#accum + 1] = number_to_str(len)
            for i = 1, len do
                local arg = args[i]
                types[type(arg)](arg, visited, accum)
            end
            visited[x] = visited[NEXT]
            visited[NEXT] = visited[NEXT] + 1
            -- We finished constructing
            constructing[x] = nil
            return true
        end
    end

    function types.userdata(x, visited, accum)
        if visited[x] then
            accum[#accum + 1] = "\208"
            accum[#accum + 1] = number_to_str(visited[x])
        else
            if check_custom_type(x, visited, accum) then return end
            error("Cannot serialize this userdata.")
        end
    end

    function types.table(x, visited, accum)
        if visited[x] then
            accum[#accum + 1] = "\208"
            accum[#accum + 1] = number_to_str(visited[x])
        else
            if check_custom_type(x, visited, accum) then return end
            visited[x] = visited[NEXT]
            visited[NEXT] =  visited[NEXT] + 1
            local xlen = #x
            local mt = getmetatable(x)
            if mt then
                accum[#accum + 1] = "\213"
                types.table(mt, visited, accum)
            else
                accum[#accum + 1] = "\207"
            end
            accum[#accum + 1] = number_to_str(xlen)
            for i = 1, xlen do
                local v = x[i]
                types[type(v)](v, visited, accum)
            end
            local key_count = 0
            for k in pairs(x) do
                if not_array_index(k, xlen) then
                    key_count = key_count + 1
                end
            end
            accum[#accum + 1] = number_to_str(key_count)
            for k, v in pairs(x) do
                if not_array_index(k, xlen) then
                    types[type(k)](k, visited, accum)
                    types[type(v)](v, visited, accum)
                end
            end
        end
    end

    types["function"] = function(x, visited, accum)
        if visited[x] then
            accum[#accum + 1] = "\208"
            accum[#accum + 1] = number_to_str(visited[x])
        else
            if check_custom_type(x, visited, accum) then return end
            visited[x] = visited[NEXT]
            visited[NEXT] =  visited[NEXT] + 1
            local str = dump(x)
            accum[#accum + 1] = "\210"
            accum[#accum + 1] = number_to_str(#str)
            accum[#accum + 1] = str
        end
    end

    types.cdata = function(x, visited, accum)
        if visited[x] then
            accum[#accum + 1] = "\208"
            accum[#accum + 1] = number_to_str(visited[x])
        else
            if check_custom_type(x, visited, #accum) then return end
            error("Cannot serialize this cdata.")
        end
    end

    types.thread = function() error("Cannot serialize threads.") end

    local function deserialize_value(str, index, visited)
        local t = byte(str, index)
        if not t then return nil, index end
        if t < 128 then
            return t - 27, index + 1
        elseif t < 192 then
            local b2 = byte(str, index + 1)
            if not b2 then error("Expected more bytes of input.") end
            return b2 + 0x100 * (t - 128) - 8192, index + 2
        elseif t == 202 then
            return nil, index + 1
        elseif t == 203 or t == 212 then
            return number_from_str(str, index)
        elseif t == 204 then
            return true, index + 1
        elseif t == 205 then
            return false, index + 1
        elseif t == 206 then
            local length, dataindex = number_from_str(str, index + 1)
            local nextindex = dataindex + length
            if not (length >= 0) then error("Bad string length") end
            if #str < nextindex - 1 then error("Expected more bytes of string") end
            local substr = sub(str, dataindex, nextindex - 1)
            visited[#visited + 1] = substr
            return substr, nextindex
        elseif t == 207 or t == 213 then
            local mt, count, nextindex
            local ret = {}
            visited[#visited + 1] = ret
            nextindex = index + 1
            if t == 213 then
                mt, nextindex = deserialize_value(str, nextindex, visited)
                if type(mt) ~= "table" then error("Expected table metatable") end
            end
            count, nextindex = number_from_str(str, nextindex)
            for i = 1, count do
                local oldindex = nextindex
                ret[i], nextindex = deserialize_value(str, nextindex, visited)
                if nextindex == oldindex then error("Expected more bytes of input.") end
            end
            count, nextindex = number_from_str(str, nextindex)
            for i = 1, count do
                local k, v
                local oldindex = nextindex
                k, nextindex = deserialize_value(str, nextindex, visited)
                if nextindex == oldindex then error("Expected more bytes of input.") end
                oldindex = nextindex
                v, nextindex = deserialize_value(str, nextindex, visited)
                if nextindex == oldindex then error("Expected more bytes of input.") end
                if k == nil then error("Can't have nil table keys") end
                ret[k] = v
            end
            if mt then setmetatable(ret, mt) end
            return ret, nextindex
        elseif t == 208 then
            local ref, nextindex = number_from_str(str, index + 1)
            return visited[ref], nextindex
        elseif t == 209 then
            local count
            local name, nextindex = deserialize_value(str, index + 1, visited)
            count, nextindex = number_from_str(str, nextindex)
            local args = {}
            for i = 1, count do
                local oldindex = nextindex
                args[i], nextindex = deserialize_value(str, nextindex, visited)
                if nextindex == oldindex then error("Expected more bytes of input.") end
            end
            if not name or not deserializers[name] then
                error(("Cannot deserialize class '%s'"):format(tostring(name)))
            end
            local ret = deserializers[name](unpack(args))
            visited[#visited + 1] = ret
            return ret, nextindex
        elseif t == 210 then
            local length, dataindex = number_from_str(str, index + 1)
            local nextindex = dataindex + length
            if not (length >= 0) then error("Bad string length") end
            if #str < nextindex - 1 then error("Expected more bytes of string") end
            local ret = loadstring(sub(str, dataindex, nextindex - 1))
            visited[#visited + 1] = ret
            return ret, nextindex
        elseif t == 211 then
            local resname, nextindex = deserialize_value(str, index + 1, visited)
            if resname == nil then error("Got nil resource name") end
            local res = resources_by_name[resname]
            if res == nil then
                error(("No resources found for name '%s'"):format(tostring(resname)))
            end
            return res, nextindex
        else
            error("Could not deserialize type byte " .. t .. ".")
        end
    end

    local function serialize(...)
        local visited = {[NEXT] = 1, [CTORSTACK] = {}}
        local accum = {}
        for i = 1, select("#", ...) do
            local x = select(i, ...)
            types[type(x)](x, visited, accum)
        end
        return concat(accum)
    end

    local function make_file_writer(file)
        return setmetatable({}, {
            __newindex = function(_, _, v)
                file:write(v)
            end
        })
    end

    local function serialize_to_file(path, mode, ...)
        local file, err = io.open(path, mode)
        assert(file, err)
        local visited = {[NEXT] = 1, [CTORSTACK] = {}}
        local accum = make_file_writer(file)
        for i = 1, select("#", ...) do
            local x = select(i, ...)
            types[type(x)](x, visited, accum)
        end
        -- flush the writer
        file:flush()
        file:close()
    end

    local function writeFile(path, ...)
        return serialize_to_file(path, "wb", ...)
    end

    local function appendFile(path, ...)
        return serialize_to_file(path, "ab", ...)
    end

    local function deserialize(str, index)
        assert(type(str) == "string", "Expected string to deserialize.")
        local vals = {}
        index = index or 1
        local visited = {}
        local len = 0
        local val
        while true do
            local nextindex
            val, nextindex = deserialize_value(str, index, visited)
            if nextindex > index then
                len = len + 1
                vals[len] = val
                index = nextindex
            else
                break
            end
        end
        return vals, len
    end

    local function deserializeN(str, n, index)
        assert(type(str) == "string", "Expected string to deserialize.")
        n = n or 1
        assert(type(n) == "number", "Expected a number for parameter n.")
        assert(n > 0 and floor(n) == n, "N must be a poitive integer.")
        local vals = {}
        index = index or 1
        local visited = {}
        local len = 0
        local val
        while len < n do
            local nextindex
            val, nextindex = deserialize_value(str, index, visited)
            if nextindex > index then
                len = len + 1
                vals[len] = val
                index = nextindex
            else
                break
            end
        end
        vals[len + 1] = index
        return unpack(vals, 1, n + 1)
    end

    local function readFile(path)
        local file, err = io.open(path, "rb")
        assert(file, err)
        local str = file:read("*all")
        file:close()
        return deserialize(str)
    end

    -- Resources

    local function registerResource(resource, name)
        type_check(name, "string", "name")
        assert(not resources[resource],
            "Resource already registered.")
        assert(not resources_by_name[name],
            format("Resource %q already exists.", name))
        resources_by_name[name] = resource
        resources[resource] = name
        return resource
    end

    local function unregisterResource(name)
        type_check(name, "string", "name")
        assert(resources_by_name[name], format("Resource %q does not exist.", name))
        local resource = resources_by_name[name]
        resources_by_name[name] = nil
        resources[resource] = nil
        return resource
    end

    -- Templating

    local function normalize_template(template)
        local ret = {}
        for i = 1, #template do
            ret[i] = template[i]
        end
        local non_array_part = {}
        -- The non-array part of the template (nested templates) have to be deterministic, so they are sorted.
        -- This means that inherently non deterministicly sortable keys (tables, functions) should NOT be used
        -- in templates. Looking for way around this.
        for k in pairs(template) do
            if not_array_index(k, #template) then
                non_array_part[#non_array_part + 1] = k
            end
        end
        table.sort(non_array_part)
        for i = 1, #non_array_part do
            local name = non_array_part[i]
            ret[#ret + 1] = {name, normalize_template(template[name])}
        end
        return ret
    end

    local function templatepart_serialize(part, argaccum, x, len)
        local extras = {}
        local extracount = 0
        for k, v in pairs(x) do
            extras[k] = v
            extracount = extracount + 1
        end
        for i = 1, #part do
            local name
            if type(part[i]) == "table" then
                name = part[i][1]
                len = templatepart_serialize(part[i][2], argaccum, x[name], len)
            else
                name = part[i]
                len = len + 1
                argaccum[len] = x[part[i]]
            end
            if extras[name] ~= nil then
                extracount = extracount - 1
                extras[name] = nil
            end
        end
        if extracount > 0 then
            argaccum[len + 1] = extras
        else
            argaccum[len + 1] = nil
        end
        return len + 1
    end

    local function templatepart_deserialize(ret, part, values, vindex)
        for i = 1, #part do
            local name = part[i]
            if type(name) == "table" then
                local newret = {}
                ret[name[1]] = newret
                vindex = templatepart_deserialize(newret, name[2], values, vindex)
            else
                ret[name] = values[vindex]
                vindex = vindex + 1
            end
        end
        local extras = values[vindex]
        if extras then
            for k, v in pairs(extras) do
                ret[k] = v
            end
        end
        return vindex + 1
    end

    local function template_serializer_and_deserializer(metatable, template)
        return function(x)
            local argaccum = {}
            local len = templatepart_serialize(template, argaccum, x, 0)
            return unpack(argaccum, 1, len)
        end, function(...)
            local ret = {}
            local args = {...}
            templatepart_deserialize(ret, template, args, 1)
            return setmetatable(ret, metatable)
        end
    end

    -- Used to serialize classes withh custom serializers and deserializers.
    -- If no _serialize or _deserialize (or no _template) value is found in the
    -- metatable, then the metatable is registered as a resources.
    local function register(metatable, name, serialize, deserialize)
        if type(metatable) == "table" then
            name = name or metatable.name
            serialize = serialize or metatable._serialize
            deserialize = deserialize or metatable._deserialize
            if (not serialize) or (not deserialize) then
                if metatable._template then
                    -- Register as template
                    local t = normalize_template(metatable._template)
                    serialize, deserialize = template_serializer_and_deserializer(metatable, t)
                else
                    -- Register the metatable as a resource. This is semantically
                    -- similar and more flexible (handles cycles).
                    registerResource(metatable, name)
                    return
                end
            end
        elseif type(metatable) == "string" then
            name = name or metatable
        end
        type_check(name, "string", "name")
        type_check(serialize, "function", "serialize")
        type_check(deserialize, "function", "deserialize")
        assert((not ids[metatable]) and (not resources[metatable]),
            "Metatable already registered.")
        assert((not mts[name]) and (not resources_by_name[name]),
            ("Name %q already registered."):format(name))
        mts[name] = metatable
        ids[metatable] = name
        serializers[name] = serialize
        deserializers[name] = deserialize
        return metatable
    end

    local function unregister(item)
        local name, metatable
        if type(item) == "string" then -- assume name
            name, metatable = item, mts[item]
        else -- assume metatable
            name, metatable = ids[item], item
        end
        type_check(name, "string", "name")
        mts[name] = nil
        if (metatable) then
            resources[metatable] = nil
            ids[metatable] = nil
        end
        serializers[name] = nil
        deserializers[name] = nil
        resources_by_name[name] = nil;
        return metatable
    end

    local function registerClass(class, name)
        name = name or class.name
        if class.__instanceDict then -- middleclass
            register(class.__instanceDict, name)
        else -- assume 30log or similar library
            register(class, name)
        end
        return class
    end

    return {
        VERSION = "0.0-8",
        -- aliases
        s = serialize,
        d = deserialize,
        dn = deserializeN,
        r = readFile,
        w = writeFile,
        a = appendFile,

        serialize = serialize,
        deserialize = deserialize,
        deserializeN = deserializeN,
        readFile = readFile,
        writeFile = writeFile,
        appendFile = appendFile,
        register = register,
        unregister = unregister,
        registerResource = registerResource,
        unregisterResource = unregisterResource,
        registerClass = registerClass,

        newbinser = newbinser
    }
end

return newbinser()


code:
_9 = {}

function del(col, o)
  local found = false
  for i,v in ipairs(col) do
    if v == o and found == false then
      table.remove(col, i)
      found = true
    end
  end
end

function point(x,y) return {x=x,y=y} end
for x=-1,1 do for y=-1,1 do table.insert(_9,point(x,y)) end end
function p_str(p) return p.x..","..p.y end
function coords(p, s) return point(math.floor(p.x*(1/s)),math.floor(p.y*(1/s))) end

function str_p(s)
  for i=1,#s do if sub(s,i,i) == "," then
    return point(sub(s, 1, i-1)+0, sub(s, i+1, #s)+0)
end end end

function badd(k,e,_b)
  _b[k] = _b[k] or {}
  table.insert(_b[k], e)
end

function bdel(_b, e)
  local k = e["_k".._b.size]
  if k then
    local b = _b[k]
    if b then
      del(b,e)
      if (#b == 0) then _b[k]=nil end
    end
  end
end

function bstore(_b,e)
  local p = p_str(coords(e[_b.prop],_b.size))
  local k = e["_k".._b.size]
  if k then
    if (k ~= p) then
      local b = _b[k]
      if b then
        del(b,e)
        if (#b == 0) then _b[k]=nil end
      end
      badd(p,e,_b)
    end
  else badd(p,e,_b) end
  e["_k".._b.size] = p
end

function bget(_b, p)
  local p = coords(p, _b.size)
  local _ = {}
  for i, o in ipairs(_9) do
    local found = _b[p_str(point(p.x+o.x,p.y+o.y))]
    if found then for i, e in ipairs(found) do table.insert(_,e) end end
  end
  return _
end

return {bstore=bstore, bget=bget, bdel=bdel}

-- usage

-- create a data 'store' {size=30,prop="pos"}
-- store.prop should match your entities' position property name,
-- which should be a 'point' value like {x=0,y=0}
-- store.size should be tuned to the max neighbor distance you'll be finding

-- periodically call bstore(store, entity) to update their bucket membership

-- bget(store, point) returns stored entities from a 3x3 square of buckets around
-- the given point, filter these by a distance function if you need more precision

-- bdel(store, entity) to remove an entity

-- remember you can maintain multiple stores based on the needs of your game!


code:
(var globals (require "globals"))
(var v (require "v"))
(var ui (require "ui"))
(var util (require "util"))
(var grid (require "grid"))
(var view (require "view"))
(var entity (require "entity"))

(fn prn [x] (print (fennel.view x)))

(var state _G.state)

(var tile 0)

(var layer :world)

(var current-entity :jump-bag)

(var window (view.new 10 20 380 448))
(set _G.editor_window window)

(set window.camera-target (v.v2 250 200))

(fn save-state []
  (util.write-bin "state.data" state))

(fn handle-draw []
  (let [target (if (= layer :world) state.world
                   (= layer :background) state.background)
        (mx my) (mouse_pos)
        tloc (view.screen->tile window (v.v2 mx my))
        sloc (view.tile->screen window tloc)]
    (when (and (v.v_in_rect (v.v2 mx my) window.tl window.br)
               (grid.in-bounds target tloc))
      (draw_text (fennel.view tloc) 10 15 true)
      (draw_rect_lines (- sloc.x 2) (- sloc.y 2) 20 20 2 false)
      (when (and (mouse_down 1) (< _G.mouse_bad 0))
        (grid.gset target tloc tile))
      (when (mouse_down 0)
        (grid.gset target tloc -1)))))

(fn entity-placer []
  (let [(mx my) (mouse_pos)
        tloc (view.screen->tile window (v.v2 mx my))
        sloc (view.tile->screen window tloc)]
    (when (and (v.v_in_rect (v.v2 mx my) window.tl window.br)
               (grid.in-bounds state.world tloc))
      (draw_text (fennel.view tloc) 10 15 true)
      (draw_rect_lines (- sloc.x 2) (- sloc.y 2) 20 20 2 false)
      (when (mouse_pressed 1)
        (let [guy (entity.new current-entity)]
          (set guy.tile-pos tloc)
          (set guy.pos (grid.t->p tloc))
          (table.insert state.entities guy)))
      (when (mouse_pressed 0)
        (set state.entities
          (util.filter
            (fn [e] (not (v.v2= e.tile-pos tloc)))
            state.entities))  ))))

(fn draw-tiles []
  (let [(mx my) (mouse_pos)]
    (for [x 0 7]
      (for [y 0 7]
        (let [idx (+ x (* y 8))
              sv (v.vmul (v.v2 x y) 16) ;sprite coords
              p (v.vadd (v.vmul (v.v2 x y) 22) (v.v2 420 20))
              outline (fn [] (draw_rect_lines (- p.x 2) (- p.y 2) 22 22 1 true))]
          (draw_rect p.x p.y 18 18 true)
          (draw_sprite (if (= layer :world) "world_sprites.png"
                           (= layer :background) "background_sprites.png")
            (+ p.x 1) (+ p.y 1) sv.x sv.y 16 16)

          (when (= idx tile)
            (outline))

          (when (v.v_in_rect (v.v2 mx my) p (v.vadd p (v.v2 16 16)))
            (outline)
            (when (mouse_pressed 1) (set tile idx)
              (prn tile))))))))

(fn entity-chooser []
  (let [(mx my) (mouse_pos)]
    (var i 0)
    (each [k e  (pairs entity.types)]
      (ui.checkbox 420 (+ 30 (* i 22)) k (= current-entity k)
        (fn [] (set current-entity k)))
      (set i (+ i 1)))))

;run this when the entity composition changes
(fn update-saved-entities []
  (set state.entities
    (util.map
      (fn [e]
        (let [n (entity.new e.type)]
          (set n.pos e.pos)
          (set n.tile-pos e.tile-pos) n))
      state.entities)))

(update-saved-entities)

(fn update [dt]
  (set _G.window window)
  (set _G.mouse_bad (- _G.mouse_bad 1))
  (let [pan (v.v2 (if (key_down "left") -1  (key_down "right") 1 0)
                  (if (key_down "up") -1  (key_down "down") 1 0))
        pan (v.vmul pan 10 dt)]
    (set window.camera-target (v.vadd window.camera-target pan)))

  (set window.camera (util.vlerp window.camera window.camera-target 0.1))

  (view.draw window state)
  (entity.sprites state.entities)
  (view.mask-view window)

  (if (= layer :entities)
    (do
      (entity-chooser)
      (entity-placer))
    (do
      (draw-tiles)
      (handle-draw)))

  (draw_text (.. "editing " layer " layer") 454 204 true)
  (ui.button 420 210 40 16 "world" (fn [] (set layer :world)))
  (ui.button 462 210 74 16 "background " (fn [] (set layer :background)))
  (ui.button 538 210 54 16 "  entities" (fn [] (set layer :entities)))
  (ui.button 420 240 172 30 "save map" (fn [] (save-state)))
  (ui.button 420 280 172 30 "back to menu" (fn [] (set _G.mode :menu) )))

{:update update}