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

推荐订阅源

Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
博客园 - 【当耐特】
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Recent Announcements
Recent Announcements
D
Docker
GbyAI
GbyAI
酷 壳 – CoolShell
酷 壳 – CoolShell
WordPress大学
WordPress大学
The Cloudflare Blog
雷峰网
雷峰网
A
About on SuperTechFans
小众软件
小众软件
博客园 - Franky
博客园 - 聂微东
F
Full Disclosure
大猫的无限游戏
大猫的无限游戏
C
Check Point Blog
MongoDB | Blog
MongoDB | Blog
G
Google Developers Blog
Microsoft Azure Blog
Microsoft Azure Blog
U
Unit 42
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
V
V2EX
Engineering at Meta
Engineering at Meta
宝玉的分享
宝玉的分享
aimingoo的专栏
aimingoo的专栏
量子位
P
Proofpoint News Feed
Hugging Face - Blog
Hugging Face - Blog
博客园_首页
罗磊的独立博客
Martin Fowler
Martin Fowler
D
DataBreaches.Net
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Secure Thoughts
Project Zero
Project Zero
L
LangChain Blog
阮一峰的网络日志
阮一峰的网络日志
C
Cybersecurity and Infrastructure Security Agency CISA
T
Tailwind CSS Blog
S
Schneier on Security
Blog — PlanetScale
Blog — PlanetScale
The Hacker News
The Hacker News
Spread Privacy
Spread Privacy
Security Latest
Security Latest
NISL@THU
NISL@THU
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
C
CXSECURITY Database RSS Feed - CXSecurity.com
J
Java Code Geeks

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

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Как уместить DOOM в QR-код
Cloud4Y (Clo · 2026-05-26 · via Все публикации подряд на Хабре

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

Охват и читатели4.3K

Перевод

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 КБ помещается не только игра, но и вредоносная нагрузка или ссылка.