«Sandtrix»: как фильм вдохновил на переосмысление легенды
- суббота, 21 февраля 2026 г. в 00:00:10

Недавно я решил посмотреть фильм, о котором много говорили при выходе, и, как можно понять из названия статьи, речь идёт о картине «Тетрис» (2023) — работе режиссёра Джона С. Бейрда и сценариста Ноя Пинка. Картина мгновенно зацепила духом ретроигровой индустрии, напряжёнными переговорами и хитросплетениями вокруг игры. Первые полчаса мне очень понравились: здесь и динамичные сцены с Хэнком Роджерсом, и начало появления игровых технологий. Поэтому могу заверить, что первые минуты смотрятся на одном дыхании.
Но, как это часто бывает, ближе к середине создатели решили добавить акцент на драму и заодно немного «клюквы». Москва в фильме превращается в какую-то серую помойку с погонями и слежками — зачем это в картине про создание игры? Думаю, тот, кто смотрел, поймёт: создаётся ощущение, что сценаристы решили — раз фильм про СССР, то должны быть все стереотипы, о которых мы «обязаны» знать, а саму игру отодвинули на второй план.
Мой совет: если решите смотреть, первые 30 минут — это чистое золото, а затем можно смело перематывать к финалу, чтобы узнать, чем всё кончилось. Фильм стоит просмотра хотя бы ради понимания того, как одна маленькая игра свела с ума весь мир.
А теперь немного про проект, и почему именно «Песочный тетрис»? Здесь нужно вспомнить историю создания оригинальной игры, ведь не зря же я смотрел фильм (шутка).
Всё началось 6 июня 1984 года. Алексей Пажитнов в Вычислительном центре Академии наук СССР создал игру для себя, вдохновившись головоломкой «Пентамино». На «Электронике-60» появились первые пиксельные тетрамино — фигуры из пяти клеток. Чтобы не перегружать систему, он сократил число квадратов в фигурах до четырёх.

Интересный факт: Название «Тетрис» произошло от сочетания древнегреческого «тетра» (четыре — количество клеток в каждой фигуре) и любимого спорта Пажитнова — тенниса. И в фильме есть на это отсылка.
Особенно известной стала версия для карманной приставки Game Boy. Именно с ней игра продалась тиражом более 200 миллионов копий. Если кратко по истории, то вот что нужно знать обычному человеку. Но если вам интересна полная история, то можно взять из Википедии, статьи Mail, хорошем видео во «ВКонтакте» или в фильме (но там есть неточность). Это не реклама, а просто то, что нашёл после просмотра фильма.
«В каждой игре есть соревновательная составляющая, какой бы она ни была. Каждый раз, когда вы сталкиваетесь с проблемой, это создаёт конкуренцию: против вас самих, против разработчика или против других людей. Я сразу осознал очевидный потенциал моей игры, потому что, помимо всех её специфических особенностей, это просто обычная игра, и люди играют, чтобы соревноваться», — поделился Алексей Пажитнов.
Эти слова показывают, что игра соревновательная, поэтому люди до сих пор задаются вопросом: «О чём на самом деле эта игра?» Также, чтобы прочувствовать дух этих слов и взглянуть на игру с другой стороны, вот видео с реальных соревнований по классическому «Тетрису» — не 2025 года, конечно, но главное здесь — увидеть, как это выглядит.
И чтобы окончательно убедиться, что гении живут среди нас: на начало 2026 года королём классического «Тетриса» (NES) остаётся 16-летний американец Майкл Артиага, известный как dogplayingtetris. В октябре 2024 года он сделал то, что казалось невозможным — первым в истории достиг «перерождения» (255-й уровень), набрав больше 29 миллионов очков и сбросив 4216 линий. Просто вдумайтесь: парень, который застал только современные версии игры, взял и переписал историю сорокалетней легенды.
Вдохновившись этой историей, я решил создать свою версию — эксперимент с современными технологиями. Подумал, что просто «Тетрис» — это будет немного скучно: там нужно либо пытаться уместить всё в минимальное количество строк, либо придумывать что-то ещё. Поэтому я решил повторить игру, которую давно видел на просторах интернета — «Песочный тетрис».
Главная задача — не создать суперчистый код, не показать, какой я крутой, и не утверждать, что это нечто революционное. Всё гораздо проще: вспомнить старые времена, написать код для души и поделиться результатом.
Для этого нужно было отработать физику падения и эффекты песка. Я стремился добиться плавности: вращение фигур, сброс и заполнение линий должны были работать без лагов — вроде получилось. Также добавил пару новых вещей в CSS-файл.
Сразу скажу: весь код разбирать не буду — остановлюсь на главном и паре интересных моментов. Если кому-то интересно, код открыт и находится на GitHub: играйте, копируйте или форкайте. Но хотя бы упоминайте источник — мне будет приятно увидеть ваши версии.
Как и в прошлых моих статьях, здесь ничего сверхсложного нет. Всё делается на том, что уже есть в любом браузере.
Что использовал:
HTML — чтобы разложить поле для игры, кнопки, счёт.
CSS — градиенты для объёмных блоков, тени, чтобы фигурки блестели.
JavaScript — ну куда без него, надо же фигурками управлять.
C: ├── index.html # Сама страница с игрой ├── style.css # Дизайн ├── game.js # Как фигурки падают и крутятся ├── blocks.js # Сами фигурки (7 штук, как в классике + 12 для доп режима) ├── controls.js # Управление или по простому, чтобы стрелочки работали └── config.js # Настройки: скорость и размеры
Вся игра построена на двумерном массиве. Представьте себе таблицу с 20 строками и 10 колонками. Каждая ячейка может быть либо пустой (null), либо заполненной каким-то цветом.
clearGrid() { this.grid = []; for (let i = 0; i < this.height; i++) { this.grid[i] = new Array(this.width).fill(null); } }
Когда фигура падает и останавливается, мы просто записываем её цвет в нужные ячейки:
addBlock(block) { for (let row = 0; row < block.size; row++) { for (let col = 0; col < block.size; col++) { if (block.cells[row]?.[col]) { let gridRow = block.y + row; let gridCol = block.x + col; if (gridRow < this.height && gridCol < this.width) { this.grid[gridRow][gridCol] = block.color; game.lastColor = block.color; } } } } this.updateLastColor(); }
Прежде чем подвинуть фигуру, нужно проверить — а можно ли? Не вылезем ли за границы? Не наедем ли на другие блоки?
Для этого есть метод isValid(). Он проходит по всем клеткам фигуры и смотрит: либо мы упираемся в границу поля, либо в уже занятую клетку. Если хоть одна проблема — движение запрещено.
isValid(block) { for (let row = 0; row < block.size; row++) { for (let col = 0; col < block.size; col++) { if (block.cells[row]?.[col]) { let gridRow = block.y + row; let gridCol = block.x + col; if (gridRow < 0 || gridRow >= this.height || gridCol < 0 || gridCol >= this.width || this.grid[gridRow][gridCol] !== null) { return false; } } } } return true; }
На этой простой проверке держится вся физика движения. Фигура идёт вниз, проверяем — можно? Если да, идём дальше. Нельзя? Значит, пора останавливаться.
Когда игрок нажимает кнопку поворота, мы не колдуем с графикой, а просто переставляем значения в матрице фигуры. Первый столбец становится первой строкой, но в обратном порядке:
rotateClockwise() { const newCells = []; for (let col = 0; col < this.size; col++) { const newRow = []; for (let row = this.size - 1; row >= 0; row--) { newRow.push(this.cells[row][col]); } newCells.push(newRow); } this.cells = newCells; }
А если нужно повернуть в другую сторону — делаем то же самое, но в обратном направлении:
rotateCounterClockwise() { const newCells = []; for (let col = this.size - 1; col >= 0; col--) { const newRow = []; for (let row = 0; row < this.size; row++) { newRow.push(this.cells[row][col]); } newCells.push(newRow); } this.cells = newCells; }
Звучит сложно, но на деле — просто перекладывание значений из одной коробки в другую.
А вот здесь самое интересное. В классическом Тетрисе линии просто исчезают, и всё. В моей версии блоки должны вести себя как песчинки, если снизу пусто — падать, если есть наклон — скатываться по диагонали.
Работает это так: после каждого хода мы проходим по всему полю снизу вверх (чтобы не было эффекта «летающих» блоков) и для каждой клетки проверяем:
applySandPhysics() { for (let row = this.height - 2; row >= 0; row--) { for (let col = 0; col < this.width; col++) { if (!this.grid[row][col]) continue; const currentColor = this.grid[row][col]; if (row < this.height - 1 && !this.grid[row + 1][col]) { this.grid[row + 1][col] = currentColor; this.grid[row][col] = null; } else if (row < this.height - 1 && col > 0 && !this.grid[row + 1][col - 1] && !this.grid[row][col - 1]) { this.grid[row + 1][col - 1] = currentColor; this.grid[row][col] = null; } else if (row < this.height - 1 && col < this.width - 1 && !this.grid[row + 1][col + 1] && !this.grid[row][col + 1]) { this.grid[row + 1][col + 1] = currentColor; this.grid[row][col] = null; } } } }
Сначала проверяем — пусто ли прямо внизу? Если да — падаем туда. Если нет — смотрим налево вниз, потом направо вниз. Но с условием, что рядом тоже должно быть пусто, иначе блок просто зависнет в воздухе.
Вместо того чтобы просто стирать заполненные строки, я сделал систему «соединённых клеток». Идея в том, что линия считается заполненной, если цепочка клеток одного цвета дотянулась от левого края до правого.
checkLines() { let leftCells = []; for (let row = 0; row < this.height; row++) { const cellValue = this.grid[row][0]; if (cellValue !== null) { const prevCell = leftCells[leftCells.length - 1]; if (!prevCell || prevCell.color !== cellValue) { leftCells.push({ row, col: 0, color: cellValue }); } } } let totalCleared = 0; for (let idx = 0; idx < leftCells.length; idx++) { let connected = this.findConnected(leftCells[idx].row, leftCells[idx].col); const reachesRight = connected.some(cell => cell.col === this.width - 1); if (reachesRight) { for (let j = 0; j < connected.length; j++) { this.grid[connected[j].row][connected[j].col] = null; } totalCleared += connected.length; } } if (totalCleared > 0) { game.score += totalCleared; game.elements.score.textContent = game.score; } return totalCleared; }
А метод findConnected — это классический поиск в глубину. Он ходит по соседям (даже по диагонали) и собирает все клетки того же цвета, до которых может добраться:
findConnected(startRow, startCol) { const targetColor = this.grid[startRow][startCol]; const visited = Array(this.height).fill().map(() => Array(this.width).fill(false)); const connected = []; const search = (row, col) => { if (row < 0 || row >= this.height || col < 0 || col >= this.width || visited[row][col]) { return; } visited[row][col] = true; if (this.grid[row][col] === targetColor) { connected.push({ row, col }); for (let dr = -1; dr <= 1; dr++) { for (let dc = -1; dc <= 1; dc++) { if (dr !== 0 || dc !== 0) { search(row + dr, col + dc); } } } } }; search(startRow, startCol); return connected; }
Чтобы игра не надоедала, скорость постепенно увеличивается. Всё просто: засекаем время с начала игры и каждые N секунд поднимаем уровень скорости на единицу:
if (game.currentTime <= CONFIG.SPEED_INCREASE_INTERVAL * CONFIG.MAX_SPEED_LEVEL) { const newLevel = Math.min(CONFIG.MAX_SPEED_LEVEL, Math.floor(game.currentTime / CONFIG.SPEED_INCREASE_INTERVAL) + 1); if (newLevel !== game.speedLevel) { game.speedLevel = newLevel; game.elements.speed.textContent = game.speedLevel; if (game.currentBlock) game.currentBlock.updateSpeed(); } }
А в самом блоке скорость считается так: чем выше уровень, тем меньше интервал между шагами падения:
updateSpeed() { this.fallSpeed = Math.max(CONFIG.MIN_FALL_SPEED, CONFIG.BASE_FALL_SPEED - (game.speedLevel - 1) * 60); } if (game.speedLevel === CONFIG.MAX_SPEED_LEVEL) { game.elements.status.textContent = "Макс. скорость"; game.elements.status.className = 'gameStatus gameWon'; }
Когда техническая часть готова, хочется добавить проекту характера — или, проще говоря, поработать над внешним видом. Здесь я разошёлся, так как мне недавно попались короткие видео с новыми фишками CSS, и я решил их добавить: например, подсветку заголовка, кольцо последнего цвета и градиентные рамки.
Заголовок «Песочный тетрис» с анимациейitleGlow перебирает цвета радуги, создавая эффект пульсирующего неона:
@keyframes titleGlow { 0% { text-shadow: 0 0 10px #ff0000, 0 0 20px #ff7f00, 0 0 30px #ffff00; } 33% { text-shadow: 0 0 10px #00ff00, 0 0 20px #0000ff, 0 0 30px #4b0082; } 66% { text-shadow: 0 0 10px #8f00ff, 0 0 20px #ff1493, 0 0 30px #ff0000; } 100% { text-shadow: 0 0 10px #ff0000, 0 0 20px #ff7f00, 0 0 30px #ffff00; } }

Самая интересная деталь интерфейса — индикатор последнего использованного цвета. Когда игрок ставит блок, в правой панели загорается кружок, окружённый вращающимся кольцом.
.hoja:after, .hoja:before { content: ""; border-radius: 100%; position: absolute; top: 0; left: 0; width: 100%; height: 100%; transform-origin: center center; } .hoja:after { animation: rotar 2s -0.5s linear infinite; } .hoja:before { animation: rotarIz 2s -0.5s linear infinite; }
Два псевдоэлемента крутятся в разные стороны с лёгкой деформацией по осям — создаётся ощущение, что кольцо переливается. Анимации rotar и rotarIz работают в противофазе, добавляя глубины.
В JavaScript, когда фигура падает и оставляет цвет, мы не просто запоминаем его — мы создаём CSS-стиль на лету:
const r = parseInt(game.lastColor.slice(1, 3), 16); const g = parseInt(game.lastColor.slice(3, 5), 16); const b = parseInt(game.lastColor.slice(5, 7), 16); const boxShadow = ` inset 0 5px 0 rgba(${r}, ${g}, ${b}, 0.8), inset 5px 0 0 rgba(${r}, ${g}, ${b}, 0.8), inset 0 -5px 0 rgba(${r}, ${g}, ${b}, 0.8), inset -5px 0 0 rgba(${r}, ${g}, ${b}, 0.8) `; const style = document.createElement('style'); style.id = 'ring-color-style'; style.textContent = ` .hoja:after { box-shadow: ${boxShadow} !important; } .hoja:before { box-shadow: ${boxShadow} !important; } `;
Каждый новый блок перекрашивает кольцо в свой цвет, добавляя внутреннюю рамку соответствующего оттенка. Если блоков ещё не было, вместо цветного кружка честно висит вопросительный знак.

Вся физика держится на трёх китах:
Матрица состояний — мы точно знаем, где какой цвет лежит — всё хранится в двумерном массиве grid.
Проверка коллизий — метод isValid() не даёт блокам проходить сквозь друг друга или выходить за границы.
Обработка сверху вниз — когда сыплем песок, идём снизу, чтобы блоки не зависали в воздухе.
И никаких сложных физических движков. Только массивы, циклы и немного математики, именно за это я и люблю такие проекты.
В результате разработки я сделал версию легендарного «Тетриса» — проект, в который можно играть прямо в браузере, но при этом его код остаётся открытым для улучшений и доработок. Это не просто клон классической игры, а небольшая дань уважения гениальной головоломке, покорившей весь мир.


Этот проект — напоминание о том, что «Тетрис» не требовал мощных процессоров, сложной графики или многомиллионного бюджета. Всё, что нужно, — несколько гениальных людей и пара падающих блоков, после чего появляется желание сложить из них гармонию.
Старые добрые «кирпичики» продолжают собирать миллионы людей по всему миру, потому что они про другое, они про состояние души, про мгновенные решения, про тот самый момент жизни, когда последний кусочек встаёт на место и всё выглядит гармонично, освобождая место для новых возможностей.
Если говорить про проект, а не про игру, то эксперимент можно считать удачным, но в процессе я заметил одну важную вещь. «Песочный тетрис», хоть и использует ту же логику, что и классический, не обладает той самой магией, которая заставляет возвращаться к оригиналу снова и снова. Почему? Думаю, дело в предсказуемости: получается, что оригинальный «Тетрис» — это дзен, а «Песочный тетрис» — скорее головоломка с элементом случайности. В этом и заключается главное отличие этой версии от оригинала.
Поиграть в мою версию «Тетриса» можно здесь.
Исходный код открыт на GitHub — используйте его как основу для своих творческих экспериментов.
Если у вас есть идеи по улучшению проекта, вы нашли баг или просто хотите вспомнить, как впервые увидели «Тетрис» (на «Электронике», Dendy, в мобильнике или в тетрадке друга), — пишите в комментариях.