
Недавно в App Store состоялся релиз моей игры Lumio — это мобильная головоломка про логические схемы, которую я делаю последние полгода. Поначалу игрок расставляет вентили на уже разведённой схеме, потом собирает триггеры, счётчики, а к финалу — маленький процессор. Всё это с постепенным изучением схемотехники.
Идея из грузинского кафе
Наверное, почти у всех читателей Хабра в университете была дискретная математика. У меня ещё были «Функциональные основы электротехники»: по формулам из дискретки мы собирали схемы на макетках. Предмет был интересный — по идее «из этих кубиков можно собрать что угодно», но... сами представляете состояние советских макеток в региональном вузе в начале десятых годов. А по вечерам с одногруппниками играли в Minecraft и собирали схемы из редстоуна.
Много лет спустя на обеде с коллегами в грузинском кафе, пока ждали заказ, обсуждали разные мобильные игровые механики и то, как вроде бы простые игры типа «три в ряд» заставляют игрока подумать. После обеда захотелось сделать что‑то похожее, но своё — не ещё одну казуалку, а что‑то «умственное». Из чертогов разума достался этот сюжет: головоломка про логические схемы.
Вообще я занимаюсь фронтендом, но эта игра сразу планировалась мобильной. Во‑первых, это дало бесконечные плейтесты на следующих обедах (коллеги, наверное, меня прокляли — им некуда было бежать). Во‑вторых, выбор платформы определил целую кучу механик.

Игры про логические схемы существуют — и они сложные. Обычно (например, в nandgame или Turing Complete) игрок должен и вентили расставлять, и провода тянуть, а на телефоне, где до приложений‑убийц времени пара жестов, в такое играть никто не будет. Ещё до первой строчки кода друзья получили от меня задачку: нарисовать полусумматор. Кто‑то рисовал в четыре элемента, кто‑то в шесть — так появилось ключевое решение: схема на уровне уже разведена, пустые слоты стоят на местах, игрок только выбирает, какой вентиль куда поставить.
По факту полусумматор и определил игру — этот уровень до сих пор мой любимый
Главное допущение — наличие сигнала показывается лампочкой: горит — 1, не горит — 0. С одной стороны, игроку надо просто «зажечь» лампы; с другой — понять правила, по которым элементы влияют на сигналы, а это уже сложнее, чем выставить три камушка в ряд. Первые уровни знакомят с вентилями и их правилами, с механикой игры и — незаметно — с основами дискретной математики. Провода игрок начинает тянуть позже, когда уже понимает, зачем они.
Типичный пет‑проект
Прототип был написан буквально за три дня, без всяких нейронок: поле с клетками, зум прямо под пальцами, сетка, элементы, рендер проводов, панель с информацией о чертеже. Ещё через месяц работы по вечерам был первый расчёт схемы, подсветка проводов по сигналу, чуть больше десяти уровней. Логика сразу отделилась от данных — уровни описаны декларативно в JSON.
Друзьям понравилось — но они всё равно «сыпались», когда в палитре элементов появлялся «XOR». В итоге перед знакомством с новым элементом появились «уроки», а вместе с ними и первая проблема: рендеринг схемы‑то можно переиспользовать, а вот писать тексты…

Тогда же появилась механика «паузы» — игрок может выставить любое положение входов и проверить конкретную строку таблицы истинности.
Вторая проблема — алгоритм расчёта схемы: узлы в уровне были расставлены по слоям и не поддерживали петли — на триггерах обход никогда не заканчивался. Новый алгоритм придумался быстро (по факту была двоичная логика сигналов, а стала троичная), а вот реализацию — как это часто бывает с пет‑проектами — остановила моя лень.
Не просто так же я подписку оплачиваю!
Всё изменил Claude. Лень ему незнакома. Хуже того — подписка стоит денег, а значит, есть мотивация продолжать работу над проектом.
Новая модель вычислителя — событийная симуляция с дискретными тиками. Схема делится на статику и динамику. Нетлист — это описание узлов и проводов: у провода есть «источник» и «сток», номер пина и задержка. Симулятор хранит выходы узлов, значения на входах и сигналы в пути (это называется фронт).
// Тик (сокращённо)
// Доставить всё, что прибыло в этот тик, во входы узлов
for arrival in queue[tick % queue.count] {
let wire = net.wires[arrival.wire]
delivered[arrival.wire] = arrival.value
pins[wire.to][wire.pin] = arrival.value
pending.insert(wire.to)
}
// Пересчитать только тронутые узлы. Порядок не важен
pending.drain(into: &touched)
for k in touched where !net.types[k].isSource {
if recompute(k) { fired.append(k) } // выход сменился → фронты по проводам
}Узел не пересчитывается, пока до него ничего не дошло. Память схемы — это значения на проводах, поэтому петли, то есть триггеры, работают сами, без особых случаев.
Вторая половина решения — трёхзначная логика. Значение сигнала — Bool?, и nil означает «сигнал ещё не дошёл». «И» и «ИЛИ» считаются по Клини: выход известен, если он одинаков при любых значениях неизвестных входов.
// nil — «сигнал ещё не дошёл / не определён»
public typealias Signal = Bool?
extension Optional where Wrapped == Bool {
/// «И» по Клини: хоть один 0 — 0; хоть одно неизвестное — неизвестно, иначе 1
public static func all(_ parts: [Signal]) -> Signal {
parts.contains(false) ? false : parts.contains(nil) ? nil : true
}
/// «ИЛИ» по Клини: хоть одна 1 — 1; хоть одно неизвестное — неизвестно, иначе 0
public static func any(_ parts: [Signal]) -> Signal {
parts.contains(true) ? true : parts.contains(nil) ? nil : false
}
}Зачем это нужно, видно на RS‑триггере. Если стартовать из «все нули», два перекрёстных ИЛИ‑НЕ начинают переключаться по очереди — схема уходит в генерацию ещё до того, как игрок к ней прикоснулся. С неизвестным на старте выходы остаются неизвестными, пока игрок не нажмёт S или R, — и генерации нет
// Шагать до затишья. Возвращает число тиков;
// `nil` — схема не успокоилась (генерация или гонка)
public mutating func settle(limit: Int? = nil) -> Int? {
let limit = limit ?? settleLimit
let start = tick
while !isQuiet {
advance()
if tick - start > limit { return nil }
}
return tick - start
}Вердикт «схема не успокоилась» оказался не багом, а механикой: позже появились уровни «найди поломку» и «убери гонку», где нужно понять, почему триггер генерирует, и починить его. Проверка решения — тот же симулятор без анимации: для комбинационных уровней шаги генерируются из таблицы истинности, для триггеров уровень хранит последовательность шагов.

Логика тоже отделена от интерфейса: движок — это отдельный Swift‑пакет (там всё, что считается целыми тиками), а приложение — чертёж, экран, меню, доля тика, геометрия и касания.
Уровни как данные
Обычно уровни создавались и редактировались фактически на лету — прямо по живому фидбеку от друзей. И сразу лежали отдельно от кода. Каждый уровень — это JSON‑файл, где провода записаны строками «откуда куда», решение — какие вентили стоят в слотах, нетлист и формула; тексты задания и подсказок — ключи локализации.
"wires": [
"A X1", "A X2",
"B X1", "B X2",
"X1 X3", "X3 X4", "X2 X4",
"X4 D", "X1 C"
],
"solution": {
"gates": { "X1": "and", "X2": "or", "X3": "not", "X4": "and" },
"netlist": [
"A input", "B input",
"X1 and A B", "X2 or A B", "X3 not X1", "X4 and X2 X3",
"C output X1", "D output X4"
],
"formula": ["C = A & B", "D = !(A & B) & (A + B)"]
}Это полусумматор. Из такого описания движок сам строит нетлист, генерирует тесты по таблице истинности и проверяет решение игрока.
Геометрия лежит в том же файле: координаты узлов в клетках сетки и списки точек для прокладки проводов. Расставляются они руками, в основном копипастой с других уровней. При рисовании углы скругляются дугой; там, где пересекаются провода от разных выходов, рисуется мостик; у шин — жилки по разрядам, старший слева по ходу сигнала, как в записи числа.
Решение не всегда единственное. Есть уровни, где нужно найти любое работающее, есть ловушки, уровни на оптимизацию, где звёзды дают за экономию элементов. Всего их набралось больше трёхсот. Появляется новый элемент или прибор — я придумываю несколько уровней, а дальше по фидбеку: если между уровнями слишком резкий скачок сложности, придумываются промежуточные варианты. Мобильная игра накладывает свои ограничения: нельзя усложнять уровни, просто увеличивая размеры схемы, — они не влезут в экран. Так появились механики из реального мира: роботы, которыми управляет схема, датчики умного дома, герконы и радио, а цветные лампы — это светофор.

Отрисовка и «борьба» с нагревом
Главный визуальный приём — сигнал виден в пути. Провод рисуется один раз в клетках сетки и ложится на экран аффинным преобразованием; пройденная часть рисуется цветом нового значения в фазе тика от 0 до 1. Базовый тик — 0,4 секунды, есть ускорение ×4 и ручной режим — активная пауза, в которой игрок переключает входы самостоятельно, а схема после каждого щелчка дорабатывает до покоя. Они появились не как фичи, а как отладка: чтобы разбираться с алгоритмом, нужно было и прокрутить схему быстро, и остановить и пощёлкать входами руками.
Первая версия этой анимации грела мой iPhone 13 mini так сильно, что телефон нельзя было держать в руках — по неопытности в разработке игр под iOS: куча лишних вызовов отрисовки, когда рисовать нечего (даже на паузе и в свёрнутом приложении), просто плохой код, и на нагрев устройства приложение никак не реагировало. Исправлять это непотребство помогал, конечно, ИИ.
Про ИИ: это отличный инструмент, если пользоваться им как инструментом. На нём — оптимизация, энергопотребление, адаптация под все устройства — не только iPhone, но и iPad, Mac и iPhone Duo со сгибом, а также тесты, переводы, вспомогательные инструменты, ведение бэклога. И рутина. Иногда даёшь задание и уходишь на ночь, иногда надо сидеть рядом — умный чатик помогает найти ошибки, проверить (улучшить) решение. Разработка под iOS — отдельный прикол: параллельные сборки регулярно кладут Mac на лопатки — xcodebuild не умеет собирать и гонять тесты параллельно из нескольких worktree, а симуляторы начинают мешать друг другу. В итоге на проект пул из двух симуляторов, и все рабочие деревья встают к ним в очередь.
Где ИИ не помогает. В SwiftUI — что где расположить — решаю я, причём из‑за необходимости сделать интерфейс под кучу устройств ИИ сделал инструмент, где можно расположить блоки интерфейса на разных экранах.

Огромное количество побочных задач — это то, чего ты никак не предполагаешь при разработке игры. Из основного: помимо редактора раскладок, были сделаны также редактор порядка уровней (акты и группы), редактор «уроков», редактор прологов (это описание анимаций между группами уровней), а также дашборд статистики с TestFlight. И это не считая кучи скриптов и скиллов на рутину.
Что ещё внутри
Разумеется, симулятор — не единственный алгоритм в игре. И тем более не самый сложный.
В некоторых актах игрок тянет провода сам, и многие варианты заранее записаны в уровне, а вот для остального используется A*
Карта Карно в игре настоящая и используется для валидации решения
Формулы в уроках — «¬(A ∧ B)», «(A → ¬B) → 0» — не картинки, а строки на своём маленьком языке с приоритетами операций. Это нужно в «демо‑зонах»
Манки‑тестер — бот, который «рандомит» решения уровней, нужен, чтобы решить, какие элементы в палитре убрать или добавить, а также построить предположения о кривой сложности игры
Побочный результат лично для меня: за эти месяцы дискретка неплохо так вспомнилась — уровни про законы логики и карты Карно не придумаешь и не проверишь, не вспомнив её как следует.
Релиз и что дальше
Тестеров было 4–7 человек — друзья; приложение поставили не все, но фидбека было много. Плейтесты проходили с шашлыками и кальяном: так удобно обсуждать фичи. Мнения расходились радикально: кому‑то много текста, кому‑то слишком легко, кто‑то хотел выбирать элементы из палитры, кто‑то — тянуть провода. Рекорд — 88 пройденных уровней за одну сессию, с заметками обо всём, что не так. Из плейтестов, кстати, выросли прологи — короткие сценки между группами уровней.
Релиз 1.0 для iOS и macOS уже в App Store — проверка у Apple заняла почти две недели. И, конечно, у меня есть несколько нереализованных идей для обновлений. Друзья с андроидами тоже ждут свою версию. Никакой рекламы и микротранзакций. Уровни открываются за набранные звёзды, и есть «Полная игра» — одна встроенная покупка, которая открывает последние акты, — по сути, это благодарность мне как разработчику на чашку кофе.
Что будет успехом? Как ни странно, не цифра продаж. Если люди хоть чуть‑чуть отвлекутся от «три в ряд» и бросков птиц в поросят и вспомнят времена, когда в играх нужно было думать, — уже хорошо. Целевая аудитория у меня явно более айтишная; собственно, это вы, так что приглашаю вас обсудить игру в комментариях!
Спасибо что дочитали до конца!

Комментарии 0