тетрис на java код
🎮Тетрис на JavaScript: разбираем возможности языка через геймификацию
Тетрис на JavaScript + изучение современных возможностей языка
Лучший способ узнавать новое и закреплять полученные знания – практика. Лучшая практика в программировании – создание игр. Лучшая игра в мире – Тетрис. Сегодня мы будем узнавать новое в процессе написания тетриса на javaScript.
В конце руководства у нас будет полностью функционирующая игра с уровнями сложности и системой очков. По ходу дела разберемся с важными игровыми концепциями, вроде графики и игрового цикла, а также научимся определять коллизии блоков и изучим возможности современного JavaScript (ES6):
Весь код проекта вы можете найти в github-репозитории.
Тетрис
Эта всемирно известная игра появилась в далеком 1984 году. Придумал ее русский программист Алексей Пажитнов. Правила очень просты и известны каждому. Сверху вниз падают фигурки разной формы, которые можно вращать и перемещать. Игрок должен складывать их внизу игрового поля. Если получается заполнить целый ряд, он пропадает. Игра заканчивается, когда башня из фигурок достигает верха игрового поля.
Тетрис – великолепный выбор для первого знакомства с гейм-разработкой. Он достаточно прост для программирования, но в то же время содержит все принципиальные игровые элементы. К тому же в нем максимально простая графика.
Структура проекта
Для удобства разобьем весь проект на отдельные файлы:
Сразу же подключим все нужное в index.html:
Создание каркаса
Для отрисовки графики будем использовать холст – элемент HTML5 canvas. Добавим его в html-файл с инфраструктурой будущей игры:
Теперь в главном скрипте проекта main.js нужно найти элемент холста и получить контекст 2D для рисования:
Здесь мы используем установленные ранее константные значения.
Метод scale используется, чтобы избежать постоянного умножения всех значений на BLOCK_SIZE и упростить код.
Оформление
Для оформления такой ретро-игры идеально подходит пиксельный стиль, поэтому мы будем использовать шрифт Press Start 2P. Подключите его в секции head :
Теперь добавим основные стили в style.css :
Для разметки используются системы CSS Grid и Flexbox.
Вот, что у нас получилось:
Пустое игровое поле
Игровое поле
Поле состоит из клеточек, у которых есть два состояния: занята и свободна. Можно было бы просто представить клетку булевым значением, но мы собираемся раскрасить каждую фигурку в свой цвет. Лучше использовать числа: пустая клетка – 0, а занятая – от 1 до 7, в зависимости от цвета.
Само поле будет представлено в виде двумерного массива (матрицы). Каждый ряд – массив клеток, а массив рядов – это, собственно, поле.
Для создания пустой матрицы поля и заполнения ее нулями используются методы массивов: Array.from() и Array.fill().
Теперь создадим экземпляр класса Board в основном файле игры.
Функция play будет вызвана при нажатии на кнопку Play. Она очистит игровое поле с помощью метода reset :
Для наглядного представления матрицы удобно использовать метод console.table:
Тетрамино
Каждая фигурка в тетрисе состоит из четырех блоков и называется тетрамино. Всего комбинаций семь – дадим каждой из них имя (I, J, L, O, S, T, Z) и свой цвет:
Для удобства вращения каждое тетрамино будет представлено в виде квадратной матрицы 3х3. Например, J-тетрамино выглядит так:
Для представления I-тетрамино потребуется матрица 4×4.
Заведем отдельный класс Piece для фигурок, чтобы отслеживать их положение на доске, а также хранить цвет и форму. Чтобы фигурки могли отрисовывать себя на поле, нужно передать им контекст рисования:
Итак, нарисуем первое тетрамино на поле:
Активная фигурка сохраняется в свойстве board.piece для удобного доступа.
Первое тетрамино на поле
Управление с клавиатуры
Передвигать фигурки по полю (влево, вправо и вниз) можно с помощью клавиш-стрелок.
Перечисления
Вычисляемые имена свойств
Теперь нужно сопоставить коды клавиш и действия, которые следует выполнить при их нажатии.
ES6 позволяет добавлять в объекты свойства с вычисляемыми именами. Другими словами, в имени свойства можно использовать переменные и даже выражения.
Для установки такого свойства нужны квадратные скобки:
Для перемещения тетрамино мы будем стирать старое отображение и копировать его в новых координатах. Чтобы получить эти новые координаты, сначала скопируем текущие, а затем изменим нужную ( x или y ) на единицу.
Так как координаты являются примитивными значениями, мы можем использовать spread-оператор, чтобы перенести их в новый объект. В ES6 существует еще один механизм копирования: Object.assign().
В объекте moves теперь хранятся функции вычисления новых координат для каждой клавиши. Получить их можно так:
Очень важно, что при этом не меняются текущие координаты самого тетрамино, так как нажатие клавиши не всегда будет приводить к реальному изменению положения.
Теперь добавим обработчик для события keydown:
Метод board.valid() будет реализован в следующем разделе. Его задача – определять допустимость новых координат на игровом поле.
board.js Управление с клавиатуры
Обнаружение столкновений
Если бы фигурки тетриса могли проходить сквозь друг друга, а также сквозь пол и стены игрового поля, игра не имела бы смысла. Важно проверить возможные столкновения элементов перед изменением их положения.
Возможные столкновения одного тетрамино:
Фигурки можно будет вращать, поэтому при вращении тоже нужно учитывать возможные столкновения.
Мы уже умеем вычислять новую позицию фигурки на поле при нажатии клавиш-стрелок. Теперь нужно добавить проверку на ее допустимость. Для этого мы должны проверить все клетки, которые будет занимать тетрамино в новом положении.
Для такой проверки удобно использовать метод массива every(). Для каждой клетки в матрице тетрамино нужно определить абсолютные координаты на игровом поле, а затем проверить, свободно ли это место и не выходит ли оно за границы поля.
Пустые клетки матрицы тетрамино при этом не учитываются.
Если проверка прошла удачно, передвигаем фигурку в новое место.
Обнаружение столкновений
Теперь мы можем добавить возможность ускоренного падения (hard drop) фигурок при нажатии на пробел. Тетрамино при этом будет падать пока не столкнется с чем-нибудь.
Вращение
Фигурки можно вращать относительно их «центра масс»:
Вращение тетрамино относительно центра
Чтобы реализовать такую возможность, нам понадобятся базовые знания линейной алгебры. Мы должны транспонировать матрицу, а затем умножить ее на матрицу преобразования, которая изменит порядок столбцов.
Вращение тетрамино в двумерном пространстве
На JavaScript это выглядит так:
Эту функцию можно использовать для вращения фигурок, но перед началом манипуляций с матрицей, ее нужно скопировать, чтобы не допускать мутаций. Вместо spread-оператора, который работает лишь на один уровень в глубину, мы используем трюк с сериализацией – превратим матрицу в JSON-строку, а затем распарсим ее.
Теперь при нажатии на клавишу Вверх, активная фигурка будет вращаться:
constants.js main.js Вращение фигурки при нажатии на клавишу Вверх
Случайный выбор фигурок
Чтобы каждый раз появлялись разные фигурки, придется реализовать рандомизацию, следуя стандарту SRS (Super Rotation System).
Добавим цвета и формы фигурок в файл constants.js:
Теперь нужно случайным образом выбрать порядковый номер тетрамино:
На этом этапе мы можем выбирать тип фигурки случайным образом при создании.
Добавим в класс Piece метод spawn :
piece.js
Игровой цикл
Почти во всех играх есть одна главная функция, которая постоянно делает что-то, даже если игрок пассивен – это игровой цикл. Нам он тоже понадобится, чтобы фигурки постоянно генерировались и падали сверху вниз на игровом поле.
RequestAnimationFrame
Для совершения циклических действий удобно использовать метод requestAnimationFrame. Он сообщает браузеру о том, что нужно сделать, а браузер выполняет это во время следующей перерисовки экрана.
Таймер
Нам также потребуется таймер, чтобы в каждом фрейме анимации «ронять» активное тетрамино вниз. Возьмем готовый пример с MDN и немного модифицируем его.
Для начала создадим объект для хранения нужной информации:
В цикле мы будем обновлять это состояние и отрисовывать текущее отображение:
main.js board.js
Заморозка состояния
При достижении активной фигуркой низа игрового поля, ее нужно «заморозить» в текущем положении и создать новое активное тетрамино.
Теперь при достижении фигуркой низа поля, мы увидим в консоли, что матрица самого поля изменилась:
Добавим метод для отрисовки целого поля (с уже «замороженными» тетрамино):
Обратите внимание, что теперь объекту игрового поля тоже нужен контекст рисования, не забудьте передать его:
main.js Отрисовка уже размещенных тетрамино
Очистка линий
Главная задача игры – собирать из блоков целые ряды, которые должны пропадать с поля, освобождая место для новых фигурок.
Добавим в класс Board метод для проверки, не собрана ли целая линия, которую можно удалить, и удаления всех таких линий:
Его нужно вызывать каждый раз после «заморозки» активного тетрамино при достижении низа игрового поля:
board.js Удаление собранных рядов
Система баллов
Чтобы сделать игру еще интереснее, нужно добавить баллы за сбор целых рядов.
Чем больше рядов собрано за один цикл, тем больше будет начислено очков.
При каждом изменении счета нужно обновлять данные на экране. Для этого мы обратимся к возможностям метапрограммирования в JavaScript – Proxy.
Прокси позволяет отслеживать обращение к свойствам объекта, например, для их чтения (get) или обновления (set) и реализовывать собственную логику:
Добавим логику начисления очков в обработчик события keydown :
и в метод очистки собранных рядов:
board.js
Уровни
Чем лучше вы играете в тетрис, тем быстрее должны падать фигурки, чтобы вам не стало скучно. Придется добавить уровни сложности в нашу игру, постепенно увеличивая частоту фреймов игрового цикла.
Напишем отдельную функцию resetGame, в которую поместим всю логику для начала новой игры:
Теперь нужно немного обновить логику начисления очков за собранные линии. С каждым уровнем очков должно быть больше.
При сборке каждых десяти рядов, уровень будет повышаться, а скорость – увеличиваться.
Завершение игры
Игра завершается, когда пирамида фигурок достигает самого верха игрового поля.
main.js Сообщение об окончании игры
Следующая фигура
Для удобства игрока мы можем добавить подсказку – какая фигурка будет следующей. Для этого используем еще один холст меньшего размера:
Получим его контекст для рисования и установим размеры:
Осталось внести изменения в метод board.drop :
Теперь игрок знает, какое тетрамино будет следующим, и может выстраивать стратегию игры.
Подсказка о следующей фигурке
Tetris на javascript (в 30+ строк)
1. Получаем фигурки
Все фигурки хранятся в переменной fs=«1111:01|01|01|01*011|110:010|011|001*. » в виде строки. Чтобы получить массив фигур — делаем split(‘*’), далее в каждой фигуре есть от 1-го (для «палки») до 4-х (для L, Г и «пирамидки») состояния (чтобы их можно было переворачивать) — они разделены «:» — соответственно чтобы получить одно состояние — split(‘:’). Допустим получили «пирамидку» — «010|111», здесь делаем split(‘|’) — и получаем уже конечный двумерный массив для одного состояния одной фигуры. «0»- пустое пространство (не нужно отрисовывать), «1» — нужно рисовать.
2. Всё перемещение фигур делается двумя функциями — «стереть фигуру» и «попытаться построить».
При любых перемещениях вправо-влево или вниз, или даже переворот фигуры — сначала стираем текущую отображаемую фигуру, потом пытаемся построить фигуру на новом месте — если при постройке какой-то из квадратиков вылазит за край «стакана», или попадает на место, где уже есть заполненный квадратик от предыдущих фигур — стираем «активную фигуру», и стоим ее на предыдущем месте.
3. Перемещения делаются с клавиатуры. По таймеру просто вызывается функция, которая обрабатывает нажатия с клавиатуры с кодом кнопки ВНИЗ. При каждом вызове таймаута — время до вызова уменьшается.
4. Если не удалось переместить фигуру вниз — значит она уперлась в предыдущие фигуры или дно. В таком случае проверяем нет ли заполненных строк, и рисуем новую фигуру вверху стакана. Если вверху стакана нарисовать фигуру не удалось — игра закончена!
Свой тетрис на JavaScript: прокачиваем проект
Доработки, чтобы получилась настоящая игра.
Как-то раз мы писали собственный тетрис на JavaScript. Мы закончили на том, что у нас есть простое игровое поле, фигуры и базовая логика игры. Ещё игра умеет останавливаться, когда для фигур больше нет места. Но этого недостаточно, чтобы считаться полноценной игрой.
Чтобы это исправить, вот что мы добавим сегодня в игру:
Всё это несложно, но требует внимания. Чтобы доработать игру, будем использовать код из первой части проекта:
Отображаем текущий уровень и набранные очки
Если мы сделаем вывод информации об уровнях и статистике внутри игрового стакана, это будет неудобно: текст будет наползать на фигуры и мешать игре. Значит, нам нужно добавить отдельный блок, где мы будем писать все игровые данные. Сделаем его сразу после объявления поля для игры и даём блоку имя score:
Так как у нас в глобальных стилях прописана рамка для холста, а для отображения статистики рамка не нужна, поставим ей нулевую толщину.
Заметьте: теперь у нас на странице есть два объекта типа canvas — то есть два холста. В одном рисуется игра, в другом выводятся очки. Теперь мы сможем обращаться к этим холстам отдельно и рисовать в каждом то, что нам нужно, независимо друг от друга.
Вот как получить доступ к новому холсту:
// получаем доступ к холсту с игровой статистикой
const canvasScore = document.getElementById(‘score’);
const contextScore = canvasScore.getContext(‘2d’);
Добавим в скрипт новые переменные — они нам сразу пригодятся на следующем этапе:
Теперь выведем на экран всю игровую статистику. Для этого перед главным циклом игры сделаем новую функцию showScore():
Цифры вроде 15, 20, 50, 160 — это координаты по вертикали и горизонтали, где должны находиться наши текстовые блоки. Без этих координат они все налезут друг на друга.
Последнее, что нам осталось сделать — вызвать эту функцию внутри основной функции loop() командой showScore().
У нас вывелась статистика, но программа пока никак её не считает. Исправим это.
Спрашиваем имя игрока
Чтобы знать, кому принадлежит рекорд игры, будем спрашивать имя при каждом запуске игры:
name = prompt(«Ваше имя», «»);
Этот код мы напишем сразу после объявления всех переменных, чтобы он выполнился на старте один раз. Сменить имя в процессе игры будет нельзя, но это и не нужно.
Считаем очки
Считать будем так: за каждую собранную линию начисляем 10 очков, по количеству кубиков в ней. В оригинальном тетрисе считается немного иначе, но для простоты сделаем так.
Для подсчёта очков находим в функции placeTetromino() проверку, заполнен ли ряд целиком, и сразу первым действием в этой проверке пишем:
Заметьте: нам не нужно городить новую сложную функцию по определению заполняемости строк — мы ее уже написали, когда программировали основную логику игры. Нужно просто дополнить уже существующую рабочую функцию одним новым действием — начислением очков. Если бы мы хотели добавить звуков или спецэффектов, мы бы точно так же добавили в эту функцию новые команды.
Теперь добавим проверку рекорда — побили мы его уже или нет. Для этого будем постоянно сравнивать текущие очки с рекордом, и если текущие больше — установим новый рекорд и запишем имя победителя:
Запоминаем рекорды
Сейчас у игры есть существенный недостаток — если открыть страницу заново в том же браузере, она не вспомнит имя чемпиона. Когда мы делали свой туду-лист на JavaScript, то использовали для этого локальное хранилище браузера — localstorage. При новом открытии страницы можно будет взять данные о рекорде оттуда.
Возьмём из той статьи наш код для локального хранилища и поправим его под нынешнюю задачу — хранить имя игрока и его рекорд. Поставим этот код сразу после того, как спросим имя игрока на старте:
Но в хранилище ничего не появится автоматически — мы сами должны положить туда значение рекорда и имя чемпиона. Сделаем это в том же разделе, где мы начисляем очки за линии. Поправим немного, чтобы код выглядел так:
Теперь при каждом обновлении рекорда в хранилище сразу будет записываться актуальная информация. Даже если мы обновим страницу, записи останутся:
Добавляем сложность и уровни
Чтобы играть было интереснее, сделаем так: с каждым новым уровнем фигуры будут падать всё быстрее. Для этого находим в коде строчку, которая отвечает за скорость движения фигур вниз и правим её следующим образом:
// фигура сдвигается вниз каждые 36 кадров минус значение текущего уровня. Чем больше уровень, тем быстрее падает.
Логика тут такая: чем выше уровень, тем меньше интервал обновления между сдвигами, что даёт нам ощущение повышенной скорости. Хотя на самом деле фигуры не падают быстрее, просто они меньше тупят.
Например, на первом уровне фигура сдвигается на одну клетку вниз каждые 35 кадров, а на 10 уровне — каждые 25 кадров, на треть быстрее.
Сами уровни будем считать там же, где и очки. Чтобы перейти на новый уровень, нужно набрать 100 очков. Зная это, легко получить значение уровня — достаточно разделить нацело количество очков на 100 и прибавить 1. Такой подход и даст нам нужный уровень.
Запишем это там же, где и идёт подсчёт очков:
level = Math.floor(score/100) + 1;
Теперь всё в порядке. Можете скопировать код себе или поиграть на странице проекта.
Tetris
last modified July 7, 2020
In this chapter, we will create a Tetris game clone in Java Swing.
Tetris
The Tetris game is one of the most popular computer games ever created. The original game was designed and programmed by a Russian programmer Alexey Pajitnov in 1985. Since then, Tetris is available on almost every computer platform in lots of variations.
Tetris is called a falling block puzzle game. In this game, we have seven different shapes called tetrominoes: S-shape, Z-shape, T-shape, L-shape, Line-shape, MirroredL-shape, and a Square-shape. Each of these shapes is formed with four squares. The shapes are falling down the board. The object of the Tetris game is to move and rotate the shapes so that they fit as much as possible. If we manage to form a row, the row is destroyed and we score. We play the Tetris game until we top out.
Figure: Tetrominoes
The development
We do not have images for our Tetris game, we draw the tetrominoes using Swing drawing API. Behind every computer game, there is a mathematical model. So it is in Tetris.
Some ideas behind the game.
The game is simplified so that it is easier to understand. The game starts immediately after it is launched. We can pause the game by pressing the p key. The space key drops the Tetris piece to the bottom. The d key drops the piece one line down. (It can be used to speed up the falling a bit.) The game goes at constant speed, no acceleration is implemented. The score is the number of lines that we have removed.
The start() method starts the Tetris game.
Shape provides information about a Tetris piece.
This is the constructor of the Shape class. The coords array holds the actual coordinates of a Tetris piece.
The coordsTable array holds all possible coordinate values of our Tetris pieces. This is a template from which all pieces take their coordinate values.
Here we copy one row of the coordinate values from the coordsTable to a coords array of a Tetris piece. Note the use of the ordinal() method. In C++, an enum type is esencially an integer. Unlike in C++, Java enums are full classes and the ordinal() method returns the current position of the enum type in the enum object.
Figure: Coordinates
The BOARD_WIDTH and BOARD_HEIGHT constants define the size of the board.
We initialize some important variables. The isFallingFinished variable determines, if the Tetris shape has finished falling and we then need to create a new shape. The numLinesRemoved counts the number of lines, we have removed so far. The curX and curY variables determine the actual position of the falling Tetris shape.
We must explicitly call the setFocusable() method. From now, the board has the keyboard input.
The DELAY constant defines the speed of the game.
The Timer object fires one or more action events after a specified delay. In our case, the timer calls the actionPerformed() method each DELAY ms.
The actionPerformed() method checks if the falling has finished. If so, a new piece is created. If not, the falling Tetris piece goes one line down.
Inside the doDrawing() method, we draw all objects on the board. The painting has two steps.
In the first step we paint all the shapes or remains of the shapes that have been dropped to the bottom of the board. All the squares are remembered in the board array. We access it using the shapeAt() method.
In the second step, we paint the actual falling piece.
If we press the Space key, the piece is dropped to the bottom. We simply try to drop the piece one line down until it reaches the bottom or the top of another fallen Tetris piece. When the Tetris piece finishes falling, the pieceDropped() is called.
The pieceDropped() method puts the falling piece into the board array. Once again, the board holds all the squares of the pieces and remains of the pieces that has finished falling. When the piece has finished falling, it is time to check, if we can remove some lines off the board. This is the job of the removeFullLines() method. Then we try to create a new piece.
The newPiece() method creates a new Tetris piece. The piece gets a new random shape. Then we compute the initial curX and curY values. If we cannot move to the initial positions, the game is over. We top out. The timer is stopped. We put game over string on the statusbar.
The tryMove() method tries to move the Tetris piece. The method returns false if it has reached the board boundaries or it is adjacent to the already fallen Tetris pieces.
Every Tetris piece has four squares. Each of the squares is drawn with the drawSquare() method. Tetris pieces have different colours.
The left and top sides of a square are drawn with a brighter color. Similarly, the bottom and right sides are drawn with darker colours. This is to simulate a 3D edge.
If we press the left arrow key, we try to move the falling piece one square to the left.
Figure: Tetris