惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

H
Help Net Security
T
ThreatConnect
SecWiki News
SecWiki News
F
Future of Privacy Forum
AWS News Blog
AWS News Blog
C
Cisco Blogs
A
Arctic Wolf
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
Scott Helme
Scott Helme
V
V2EX
博客园 - 叶小钗
阮一峰的网络日志
阮一峰的网络日志
K
Kaspersky official blog
G
Google Developers Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
P
Privacy International News Feed
C
Cyber Attacks, Cyber Crime and Cyber Security
N
News | PayPal Newsroom
Schneier on Security
Schneier on Security
NISL@THU
NISL@THU
Microsoft Azure Blog
Microsoft Azure Blog
量子位
The Hacker News
The Hacker News
Stack Overflow Blog
Stack Overflow Blog
Security Latest
Security Latest
M
Microsoft Research Blog - Microsoft Research
Google Online Security Blog
Google Online Security Blog
博客园_首页
C
CXSECURITY Database RSS Feed - CXSecurity.com
I
InfoQ
Google DeepMind News
Google DeepMind News
Y
Y Combinator Blog
The Cloudflare Blog
Microsoft Security Blog
Microsoft Security Blog
Martin Fowler
Martin Fowler
Cisco Talos Blog
Cisco Talos Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Troy Hunt's Blog
F
Fox-IT International blog
S
Security @ Cisco Blogs
博客园 - 司徒正美
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
C
Comments on: Blog
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
L
LINUX DO - 最新话题
GbyAI
GbyAI
Project Zero
Project Zero
腾讯CDC
T
Tailwind CSS Blog

Все публикации подряд на Хабре

Почему российские компании остаются на серой Jira [Перевод] Тонус в реактивных системах Факап инженера АСУ ТП, как мы перепутали физические COM-порты на подстанции Cache is hard — почему инвалидация кэша — это проблема согласованности, а не производительности Щелевая коррозия: порча нержавейки и «ржавые» имплантаты — почему это происходит? Строим первую линию техподдержки на n8n за 250$ в месяц. Часть 2 Покопались в .cursorrules на GitHub и нашли там волка-фурри, Star Trek и 28.7% копипасты Не наступайте на наши грабли, если собираетесь использовать Temporal Как создать дебат-клуб в компании: пошаговое руководство от бизнес-тренера Как экономят на метановых автозаправках Самодельный elgato-like макропад. Часть 1, железная Всё есть код, или зачем внедрять GitOps в разработку Как получить root на Urovo DT40 Pro (CT48): Android 12 (Проверено на практике) C# мне нравится больше Java. Но в банковском enterprise мне всё равно понадобилась Java Биткоин на Московской бирже — что это? Как мы переводим миллионы iOS-пользователей на новое приложение каждые несколько месяцев Кейс. Zero Bug Policy: как мы снизили бэклог багов в 4 раза за месяц Shadow AI: 80% сотрудников уже пишут в ChatGPT. Почему мы делим задачи на красные, зелёные и серые Попытка пересмотреть ограничения рынка тяжелых БАС: нужен ли вообще кому-то легкий и дешевый электромотор Менеджер, который хакнул систему. И что AI на самом деле умножает Spec-driven development в микросервисах, часть 2: как archspec делает контекст сервисов явным Запись в Kubernetes: как контроллеры учились не перезаписывать друг друга Игровой движок 2.5D, короткие тренировки для ПК-пользователей –и еще 8 российских стартапов MCP в системе управления проектами: как поручить ИИ работу с корпоративными данными Бэклог болей: как hh работает с тем, что не нравится пользователям brec: контролируемая обратная совместимость протокола AI обнулил benchmark и пытался шантажировать инженера. И почему это решаемо Почему пластиковый корпус оказался в 3 раза дороже металлического Как спроектировать API, которое не придется переписывать через полгода Трекинг посетителей на fisheye-камерах: задача “со звездочкой” Красивый скриншот вашего кода. Большое обновление Я создаю проекты без единого созвона с командой Content Pipeline в MonoGame: почему я его не использую Гемблинг партнерки: Как выбрать, ТОП 5 в 2026 За пределами LLM, часть 2: якорная таблица Кэли, которая не является ни полем, ни моноидом Pixverse купить подписку: для чего нужна Пиксверс подписка, как выбрать тариф и оплатить в рублях Meshy AI нейросеть: как создавать 3D-модели из текста и изображений в Меши АИ на русском бесплатно Skywork AI: как использовать Скайворк АИ нейросеть на русском бесплатно, работать с промтами и создавать видео Технотекст 8: победа естественного интеллекта Capacitor: от веба к мобильным приложениям. Часть 4. Интегрируем локальный LLM в проект 20 лет видеокарт в цифрах: как росли FLOPS и TDP и кто вёл в дуэли NVIDIA vs AMD (+ открытый датасет на 13 500 GPU) Архитектура крипто-сканера для биржи: Open Interest, Funding Rate, EMA и MACD в реальном времени @tanstack/vue-table: почему я почти отказался от этого… WHERE превращает ваш LEFT JOIN в INNER JOIN. И никто вам об этом не скажет Гравитация не существует. Вы задали 454 вопроса о времени. Вот ответы с уравнениями Эйнштейна Конец бесплатного кремния: как Google AI Studio превратилась из рая для инженеров в симулятор смены аккаунтов Свой AI-агент из почты, systemd и LLM MemForge2: загрузочная флешка, которая за минуту говорит — какую планку памяти менять Лицензии важны. Разбор ошибок авторов и пользователей программ От RAG-прототипа к агенту в продакшн: путь по метрикам, а не по моде Serial Terminal: кастомный веб-терминал для последовательного порта на Web Serial API Китайский стартап GigaAI обещает робота-домработника за 1 млн рублей уже в 2027 году — правда или PR? Open-source VPN клиент Tunguska Роман за 6 недель без идеи на старте: миф или реальность? ИИ построит ваш план действий за 10 секунд Security Week 2622: эффективность Claude Mythos по версии Cloudflare Reactive Forms vs Signal Forms: Эволюция сложных форм в Angular TorFlash — приложение для Linux: поиск торрентов, скачивание и копирование на флешку в одно нажатие Как я решил проблему русской диктовки для ИИ Оверинжиниринг, потопивший немецкую подлодку или некоторые «баги» не чинятся десятилетиями Как ставить цели и не забывать о них: пошаговая система с примерами в таск-менеджере Как настроить observability в Spring Boot 3 HackTheBox. Прохождение Mini Pro Lab Puppet Обзор серверного ускорителя NVIDIA Tesla V100 16 Gb в корпусе от RTX 4090: Часть 3 — Запуск локальных моделей ИИ Редактирование текста нейросетью: как сделать диплом и курсовую более человечными Самодельный ARM ноутбук, реально ли? Как 100+ авторов пишут 100+ процессов в 3 версиях и не путаются. Или как мы переехали с Wiki на Git Прошла AnalystDays – хорошие выступления и нетворкинг VSCode как IDE для embedded разработки Моделирование широкополосной антенны с двойной круговой поляризацией и высокой изоляцией Ваше прошлое физически существует прямо сейчас. И вы заморожены там навсегда От списка инструментов к technical output: как security engineer’у описывать hands-on опыт в CV и на интервью I just want an agent. Часть 1. Как я научил ИИ собирать ИИ-агентов за пользователей и выиграл конкурс I just want an agent. Часть 1. Как я научил ИИ собирать ИИ-агентов за пользователей и выиграл конкурс Вайбкодинг спас меня от подрядчиков. А потом я поняла, что сама стала подрядчиком для своих агентов Святой Августин и GAN: почему борьба добра и зла — это генеративная состязательная сеть В каждом QR-коде зашита половина лишней информации. Намеренно Я открываю автомат ключом, меняю рулон бумаги и зарабатываю 180 тысяч в месяц с точки Мастер восстановления. Культура достиженства и выгорание Недельный геймдев: #279 — 24 мая, 2026 Защита от дублирования кода агентами: семантические концепции Frontend Status: свежий дайджест фронтенда и AI — 25.05.2026 Где искать IT-работу кроме HH: подборка платформ 2026 Почему простые числа собираются в спирали? OCR для Data Lakehouse: от Apache Tika к собственному решению на базе Docling Jira — Тьюринг-полная Kubernetes-аудит после Wiz и Prisma: как живут без CNAPP в 2026 «Тестируем MVP в 4 раза быстрее»: как нейросети изменили жизнь предпринимателей На каком стеке и железе работает умное наблюдение в вашем городе: обзор технологий от разработчиков видеоаналитики Как мы ускорили согласования на двух заводах в 24 раза Heartbeat-мониторинг cron-job'ов: dead-man-switch на FastAPI [Перевод] Сегодня нет джуниоров, а в 2031 году не станет и синьоров Профайлер для PostgreSQL: от идеи до работающего MVP за сутки [Перевод] Ограничения размера cookie в ASP.NET Core в продакшене: причины и способы решения Проблема «божественного» Obsidian: почему я отказался от централизованного подхода в работе Лицензии GNU GPL: как пройти проверку Минцифры и заказчика для госзакупок и КИИ Хакатон Samsung IT Academy Hack 2026: как студенты оптимизировали поиск в корпоративном мессенджере Хакатон Samsung IT Academy Hack 2026: как студенты оптимизировали поиск в корпоративном мессенджере MTProxy jumper — делаем автоматическое переключение прокси-серверов Telegram Ты уже используешь агента. Просто не заметил
Как уместить DOOM в QR-код
Cloud4Y (Clo · 2026-05-26 · via Все публикации подряд на Хабре

Время на прочтение6 мин

Охват и читатели7

Перевод

DOOM известен тем, что запускается где угодно – различные порты игры появляются с 1993 года. Мем «It Runs Doom» живёт в интернете уже больше десяти лет. Люди запускали DOOM на тостерах, на тачбарах макбуков, на умных холодильниках.

И, кажется, я – первый человек, который засунул DOOM в QR-код.

Абсурдная постановка задачи

QR-код может хранить до ~3 КБ текстовых и бинарных данных. Для сравнения:

  • Этот абзац до текущего момента весит около 0,4 КБ.

  • Спрайт «chaingun» из оригинального DOOM – 1,2 КБ.

Моя цель: сделать работоспособную DOOM-подобную игру, которая будет весить меньше трёх абзацев обычного текста.

С чего всё началось

Всё началось с того, что пару лет назад я посмотрел видео автора matttkc и был им крайне заинтригован. Главный вопрос, который там звучал: можно ли уместить целую игру в QR-код?

Похоже, идея такого проекта всё это время сидела у меня в подсознании, но я за неё не брался – просто потому, что считал, что мне это не по зубам. Когда на прошлой неделе идея всплыла снова, я понял, что хочу сделать именно DOOM. Но найти реализации или HTML-ремейки DOOM оказалось практически невозможно, так что я пошёл по следующему очевидному пути – сделать DOOM-подобную игру с нуля.

Это оказалось крайне сложно, потому что в моём распоряжении не было:

  • игрового движка – только чистый HTML/JavaScript;

  • ассетов – вся графика генерируется кодом;

  • библиотек с кодом – на счету каждый байт.

По ходу разработки я остановился на концепции backdooms – из-за созвучия и схожести идей бэкрумов (the Backrooms) и DOOM.

В отличие от автора видео, свою игру для QR-кода я делал на HTML. Глупо ли это было? Возможно. Оказалось ли это в итоге очень удачным решением? Безусловно.

Минификация

Чтобы уместить игру в такой абсурдно малый размер файла, нужна минификация – а в данном случае крайне агрессивная минификация.

Вот как выглядит часть кода игры:

<!DOCTYPE html><html><head><meta charset="utf-8"><style>body{margin:0;overflow:hidden;background:#000;cursor:crosshair}canvas{width:100vw;height:100vh}</style></head><body><canvas id=c></canvas><script>
M=Math,c=document.getElementById("c"),c.width=320,c.height=240,h=c.getContext("2d"),x=4,y=4,a=0,H=100,am=25,rc=0,fl=0;
f=(i,j)=>(Math.abs(i-4)<4&&Math.abs(j-4)<4)?"0":((((i+1000)%7)==3||((j+1000)%7)==3)?"0":(Math.random()<.05?"1":"0"));
e=[{x:5,y:4,h:100},{x:4,y:5,h:100}],k={};onkeydown=e=>k[e.key]=1;onkeyup=e=>k[e.key]=0;
onclick=_=>{if(am){am--;fl=2;rc=.2;e.forEach(o=>{d=M.hypot(o.x-x,o.y-y),r=M.atan2(o.y-y,o.x-x);if(d<5&&Math.abs(r-a)<.3)o.h-=50})}};
R=_=>{
rc=Math.max(0,rc-.02);fl=Math.max(0,fl-1);e=e.filter(o=>o.h>0);
h.fillStyle="#000";h.fillRect(0,0,320,240);
k.ArrowLeft&&(a-=.1);k.ArrowRight&&(a+=.1);m=.1;

Если бы не <!DOCTYPE> в начале, то вы вполне могли бы решить, что это вообще не HTML.

Приведу пример попроще. Было:

function drawWall(distance) {  
  const height = 240 / distance;  
  context.fillRect(x, 120 - height/2, 1, height);  
}

После минификации:

h.fillRect(i,120-240/d/2,1,240/d)  

Переменные превращаются в одиночные буквы, комментарии испаряются, и код начинает напоминать зашифрованную записку с требованием выкупа.

Генерация карты

На ранних этапах разработки карта была совсем маленькой – 16×16 и 8×8. Для такой крошечной игры это в принципе приемлемо, но мне хотелось чего-то более играбельного, поэтому я реализовал бесконечную генерацию карт – да ещё и с генерацией по сидам.

Если вы играли в Minecraft, вы знаете, что такое сиды: предельно случайные значения из набора символов, которые служат основой для генерации игровых миров.

Псевдо-3D на основе техник оригинального DOOM

Теоретически, если вам понравилась какая-то конкретная генерация и вы знаете её сид, вы можете захардкодить его в код, чтобы каждый раз получать одну и ту же карту:

SEED = Math.random() * 100;

Моя версия симуляции 3D-эффекта использует raycasting (бросание лучей) – приём рендеринга родом из 1992 года. Для каждого вертикального столбца экрана (из 320):

  1. Бросаем луч под чуть изменённым углом.

  2. Измеряем расстояние до ближайшей стены.

  3. Рисуем прямоугольник тем выше, чем ближе стена.

for (let i = 0; i < 320; i++) {  
  const rayAngle = playerAngle + (i - 160) / 500;  
  let distance = 0;  
  while (!isWall(x + distance * cos(rayAngle), y + distance * sin(rayAngle))) {  
    distance += 0.1; // March forward  
  }  
  drawColumn(i, distance);  
}

Хотя это базовая тригонометрия, на неё приходится значительная часть всей игры. Честно говоря, если бы не бесконечная генерация карты, я бы просто закодировал URL в Base64 – и его одного хватило бы, чтобы запускаться напрямую. Но оно того стоило.

Примечание переводчика. В оригинале raycasting назван техникой DOOM. Строго говоря, это не так: raycasting прославил Wolfenstein 3D (1992), а сам DOOM (1993) использовал другой подход – рендеринг на основе BSP-деревьев. Игра автора – DOOM-подобная, однако под капотом у неё именно raycasting.

Механика противников

Это была ещё одна большая головная боль. В ранних версиях враги были только в начале, а стоило отойти подальше – их не оставалось вовсе. Для маленькой карты это ещё работало, но для бесконечной генерации – нет.

Противники дались мне тяжело. Во-первых, очень сложно сделать сколько-нибудь реалистичные эффекты стрельбы или хоть сколько-то реалистичных врагов, когда ты так зажат размером файла. Во-вторых, гейм-дев – не самая моя сильная сторона.

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

if ((k.ArrowUp || k.w || k.ArrowDown || k.s || k.ArrowLeft || k.ArrowRight) && e.length < 10 && Math.random() < .01) {
  t = Math.random() * 6.283;
  Rdist = 1 + Math.random();
  X = x + M(t) * Rdist;
  Y = y + N(t) * Rdist;
  f(~~X, ~~Y) == "0" && e.push({ x: X, y: Y, h: 100 });
}

Сделать игру – это была лишь половина задачи. Настоящий вызов был в том, чтобы засунуть её в QR-код.

Концепция и реализуемость

Самый большой стандартный QR-код (Version 40) вмещает 2 953 байта (~2,9 КБ). Это очень мало. Например:

  • Звуковой файл Windows длиной 1/15 секунды весит 11 КБ.

  • На дискету (1,44 МБ) поместилось бы почти 500 QR-кодов данных.

Начальный размер моей игры составил 3,4 КБ. После изнурительного четырёхдневного процесса оптимизации я успешно уменьшил размер файла до 2,4 КБ – пусть и ценой нескольких тщательно взвешенных компромиссов.

Помните, я говорил, что QR-код хранит текстовые и бинарные данные? Так вот, HTML – это ни бинарные данные, ни обычный текст, так что попытка напрямую скормить HTML генератору QR-кодов провалилась.

Обычно в таких случаях советуют конвертацию в Base64 – но у этого подхода огромный оверхед в 33%! Оставалось меньше 1,9 КБ под саму игру. Теперь мне стало понятно, почему matttkc выбрал змейку.

Признаюсь, на этом этапе я подумывал сдаться. Я обсуждал эту проблему с тремя разными ИИ-чат-ботами – ChatGPT, DeepSeek и Claude – пытаясь хоть что-то с этим сделать (и каждый раз слышал, что разместить игру на сайте было бы проще). А потом ChatGPT между делом подкинул DecompressionStream.

DecompressionStream

DecompressionStream – малоизвестный компонент Web API, который встроен буквально в каждый современный браузер. Считайте его чем-то вроде WinRAR для браузера, только работает он с потоками данных, а не с zip-файлами.

Единственный способ добиться нужного результата (а я в этом практически уверен – после всех поисков у меня диплом по микро-играм) был таким:

  1. Читаем исходный HTML.

  2. Сжимаем его gzip с максимальным уровнем сжатия (9).

  3. Кодируем результат в Base64.

  4. Встраиваем в самораспаковывающуюся HTML-обёртку.

  5. Преобразуем в data URI.

  6. Проверяем: влезают ли данные в QR-код?

    • Да → генерируем QR-код.

    • Нет (превышен лимит QR Version 40) → берём максимально допустимую версию QR и проверяем, влезет ли при низкой избыточности (low redundancy). Если да – генерируем QR-код. Если нет – данные слишком большие, возвращаемся назад и уменьшаем HTML.

  7. Показываем готовый QR-код для сканирования.

Я быстро набросал на Python инструмент, автоматизирующий весь этот процесс, и оно сработало. Доведение скрипта до ума заняло больше 34 итераций, а также крови, пота, слёз и процессорного времени.

Да, это буквально вся игра целиком. Отсканируйте и поиграйте, если хотите.

Да, это буквально вся игра целиком. Отсканируйте и поиграйте, если хотите.

Отсканируйте QR-код, скопируйте полученную строку data:text/html;base64,… и вставьте её в адресную строку десктопного браузера (Chrome или Firefox) – игра запустится прямо оттуда.

Наследие и доступность

Проект (репозиторий на GitHub) показывает, что с помощью сжатия даже такие ограниченные носители, как QR-коды, могут вмещать интерактивные игры. Это непрактично для сложных игр, но открывает возможности для:

  • офлайн-распространения игр через QR-постеры;

  • ретро-творчества в духе демосцены;

  • образовательных инструментов для сред с низкой пропускной способностью.

Что насчёт будущих обновлений? Изначально я пошутил, что выпуск «версии 1.1» потребовал бы убрать букву «e» из всей кодовой базы – некоторые ограничения лучше не трогать. Но я сглазил: всего день спустя я обновил проект, добившись лучшего сжатия на целых 15%. Об этом – в отдельной статье: «How I Managed to Make HTML Game Compression so Much Better».


Примечание переводчика. Пара слов о том, почему в QR-код вообще влезает так мало. Ёмкость QR-кода определяется его версией (от 1 до 40) и уровнем коррекции ошибок. Version 40 в бинарном режиме при минимальном уровне коррекции (L) вмещает те самые 2 953 байта – и чем выше уровень коррекции (L → M → Q → H), тем больше места уходит на избыточность, позволяющую считать повреждённый или частично закрытый код, и тем меньше остаётся под полезные данные.

Именно поэтому автор выбирал «низкую избыточность»: он разменивал устойчивость кода к повреждениям на дополнительные байты для игры. Здесь же кроется и причина, по которой стоит с осторожностью сканировать произвольные QR-коды: в те же 2,9 КБ помещается не только игра, но и вредоносная нагрузка или ссылка.