Головоломки тетрис

В предыдущих статьях этой серии мы уже успели написать сапёра, змейку и десктопный клон игры 2048. Попробуем теперь написать свой Тетрис.

Содержание

Нам, как обычно, понадобятся:

  • 30 минут свободного времени;
  • Настроенная рабочая среда, т.е. JDK и IDE (например, Eclipse);
  • Библиотека LWJGL (версии 2.x.x) для работы с графикой (опционально). Обратите внимание, что для LWJGL версий выше 3 потребуется написать код, отличающийся от того, что приведён в статье;
  • Спрайты, т.е. картинки плиток всех возможных состояний (пустая, и со степенями двойки до 2048). Можно нарисовать самому, или скачать использовавшиеся при написании статьи.

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

С чего начать?

Начать стоит с главного управляющего класса, который в нашем проекте находится выше остальных по уровню абстракции. Вообще отличный совет – в начале работы всегда пишите код вида if (getKeyPressed()) doSomething(), так вы быстро определите фронт работ.

public static void main(String args) { initFields(); while(!endOfGame){ input(); logic(); graphicsModule.draw(gameField); graphicsModule.sync(FPS); } graphicsModule.destroy(); }

Это наш main(). Он ничем принципиально не отличается от тех, что мы писали в предыдущих статьях – мы всё так же инициализируем поля и, пока игра не закончится, осуществляем по очереди: ввод пользовательских данных (input()), основные игровые действия (logic()) и вызов метода отрисовки у графического модуля (graphicsModule.draw()), в который передаём текущее игровое поле (gameField). Из нового разве что метод sync – метод, который должен будет гарантировать нам определённую частоту выполнения итераций. С его помощью мы сможем задать скорость падения фигуры в клетках-в-секунду.

Вы могли заметить, что в коде использована константа FPS. Все константы удобно определять в классе с public static final полями. Полный список констант, который нам потребуется в ходе разработки, можно посмотреть в классе Constants на GitHub.

Оставим пока инициализацию полей на потом (мы же ещё не знаем, какие нам вообще понадобятся поля). Разберёмся сначала с input() и logic().

Получение данных от пользователя

Код, честно говоря, достаточно капитанский:

private static void input(){ /// Обновляем данные модуля ввода keyboardModule.update(); /// Считываем из модуля ввода направление для сдвига падающей фигурки shiftDirection = keyboardModule.getShiftDirection(); /// Считываем из модуля ввода, хочет ли пользователь повернуть фигурку isRotateRequested = keyboardModule.wasRotateRequested(); /// Считываем из модуля ввода, хочет ли пользователь «уронить» фигурку вниз isBoostRequested = keyboardModule.wasBoostRequested(); /// Если был нажат ESC или «крестик» окна, завершаем игру endOfGame = endOfGame || keyboardModule.wasEscPressed() || graphicsModule.isCloseRequested(); }

Все данные от ввода мы просто сохраняем в соответствующие поля, действия на основе них будет выполнять метод logic().

Теперь уже потихоньку становится понятно, что нам необходимо. Во-первых, нам нужны клавиатурный и графический модули. Во-вторых, нужно как-то хранить направление, которое игрок выбрал для сдвига. Вторая задача решается просто – создадим enum с тремя состояниями: AWAITING, LEFT, RIGHT. Зачем нужен AWAITING? Чтобы хранить информацию о том, что сдвиг не требуется (использования в программе null следует всеми силами избегать). Перейдём к интерфейсам.

Интерфейсы для клавиатурного и графического модулей

Так как многим не нравится, что я пишу эти модули на LWJGL, я решил в статье уделить время только интерфейсам этих классов. Каждый может написать их с помощью той GUI-библиотеки, которая ему нравится (или вообще сделать консольный вариант). Я же по старинке реализовал их на LWJGL, код можно посмотреть в папках graphics/lwjglmodule и keyboard/lwjglmodule.

Интерфейсы же, после добавления в них всех упомянутых выше методов, будут выглядеть следующим образом:

public interface GraphicsModule { /** * Отрисовывает переданное игровое поле * * @param field Игровое поле, которое необходимо отрисовать */ void draw(GameField field); /** * @return Возвращает true, если в окне нажат «крестик» */ boolean isCloseRequested(); /** * Заключительные действия, на случай, если модулю нужно подчистить за собой. */ void destroy(); /** * Заставляет программу немного поспать, если последний раз метод вызывался * менее чем 1/fps секунд назад */ void sync(int fps); } public interface KeyboardHandleModule { /** * Считывание последних данных из стека событий, если модулю это необходимо */ void update(); /** * @return Возвращает информацию о том, был ли нажат ESCAPE за последнюю итерацию */ boolean wasEscPressed(); /** * @return Возвращает направление, в котором пользователь хочет сдвинуть фигуру. * Если пользователь не пытался сдвинуть фигуру, возвращает ShiftDirection.AWAITING. */ ShiftDirection getShiftDirection(); /** * @return Возвращает true, если пользователь хочет повернуть фигуру. */ boolean wasRotateRequested(); /** * @return Возвращает true, если пользователь хочет ускорить падение фигуры. */ boolean wasBoostRequested(); }

Отлично, мы получили от пользователя данные. Что дальше?

А дальше мы должны эти данные обработать и что-то сделать с игровым полем. Если пользователь сказал сдвинуть фигуру куда-то, то передаём полю, что нужно сдвинуть фигуру в таком-то направлении. Если пользователь сказал, что нужно фигуру повернуть, поворачиваем, и так далее. Кроме этого нельзя забывать, что 1 раз в FRAMES_PER_MOVE (вы же открывали файл с константами?) итераций нам необходимо сдвигать падающую фигурку вниз.

private static void logic(){ if(shiftDirection != ShiftDirection.AWAITING){ // Если есть запрос на сдвиг фигуры /* Пробуем сдвинуть */ gameField.tryShiftFigure(shiftDirection); /* Ожидаем нового запроса */ shiftDirection = ShiftDirection.AWAITING; } if(isRotateRequested){ // Если есть запрос на поворот фигуры /* Пробуем повернуть */ gameField.tryRotateFigure(); /* Ожидаем нового запроса */ isRotateRequested = false; } /* Падение фигуры вниз происходит если loopNumber % FRAMES_PER_MOVE == 0 * Т.е. 1 раз за FRAMES_PER_MOVE итераций. */ if( (loopNumber % (FRAMES_PER_MOVE / (isBoostRequested ? BOOST_MULTIPLIER : 1)) ) == 0) gameField.letFallDown(); /* Увеличение номера итерации (по модулю FPM)*/ loopNumber = (loopNumber+1)% (FRAMES_PER_MOVE);

Сюда же добавим проверку на переполнение поля (в Тетрисе игра завершается, когда фигурам некуда падать):

/* Если поле переполнено, игра закончена */ endOfGame = endOfGame || gameField.isOverfilled(); }

Так, а теперь мы напишем класс для того магического gameField, в который мы всё это передаём, да?

Не совсем. Сначала мы пропишем поля класса Main и метод initFields(), чтобы совсем с ним закончить. Вот все поля, которые мы использовали:

/** Флаг для завершения основного цикла программы */ private static boolean endOfGame; /** Графический модуль игры*/ private static GraphicsModule graphicsModule; /** «Клавиатурный» модуль игры, т.е. модуль для чтения запросов с клавиатуры*/ private static KeyboardHandleModule keyboardModule; /** Игровое поле. См. документацию GameField */ private static GameField gameField; /** Направление для сдвига, полученное за последнюю итерацию */ private static ShiftDirection shiftDirection; /** Был ли за последнюю итерацию запрошен поворот фигуры */ private static boolean isRotateRequested; /** Было ли за последнюю итерацию запрошено ускорение падения*/ private static boolean isBoostRequested; /** Номер игровой итерации по модулю FRAMES_PER_MOVE. * Падение фигуры вниз происходит если loopNumber % FRAMES_PER_MOVE == 0 * Т.е. 1 раз за FRAMES_PER_MOVE итераций. */ private static int loopNumber;

А инициализировать мы их будем так:

private static void initFields() { loopNumber = 0; endOfGame = false; shiftDirection = ShiftDirection.AWAITING; isRotateRequested = false; graphicsModule = new LwjglGraphicsModule(); keyboardModule = new LwjglKeyboardHandleModule(); gameField = new GameField(); }

Если вы решили не использовать LWJGL и написали свои классы, реализующие GraphicsModule и KeyboardHandleModule, то здесь нужно указать их конструкторы вместо, соответственно new LwjglGraphicsModule() и new LwjglKeyboardHandleModule().

А вот теперь мы переходим к классу, который отвечает за хранение информации об игровом поле и её обновление.

Класс GameField

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

Начнём по порядку.

Хранить информацию о поле…

/** Цвета ячеек поля. Для пустых ячеек используется константа EMPTINESS_COLOR */ private TpReadableColor theField; /** Количество непустых ячеек строки. * Можно было бы получать динамически из theField, но это дольше. */ private int countFilledCellsInLine;

…и о падающей фигуре

/** Информация о падающей в данный момент фигуре */ private Figure figure;

TpReadableColor — простой enum, содержащий элементы с говорящими названиями (RED, ORANGE и т.п.) и метод, позволяющий получить случайным образом один из этих элементов. Ничего особенного в нём нет, код можно посмотреть .

Это все поля, которые нам понадобятся. Как известно, поля любят быть инициализированными.
Сделать это следует в конструкторе.

Конструктор и инициализация полей

public GameField(){ spawnNewFigure(); theField = new TpReadableColor; countFilledCellsInLine = new int;

«Что это за OFFSET_TOP?» – спросите вы. OFFSET_TOP это количество неотображаемых ячеек сверху, в которых создаются падающие фигуры. Если фигуре не сможет «выпасть» из этого пространства, и хоть одна ячеек theField выше уровня COUNT_CELLS_Y будет заполнена, это будет обозначать, что поле переполнено и пользователь проиграл, поэтому OFFSET_TOP должен быть строго больше нуля.

Далее в конструкторе стоит заполнить массив theField значениями константы EMPTINESS_COLOR , а countFilledCellsInLine – нулями (второе в Java не требуется, при инициализации массива все int‘ы равны 0). Или можно создать несколько слоёв уже заполненных ячейкам — на GitHub вы можете увидеть реализацию именно второго варианта.

А что это там за spawnNewFigure()? Почему инициализация фигуры вынесена в отдельный метод?

Вы правильно догадались, spawnNewFigure() действительно инициализирует поле figure. А в отдельный метод это вынесено, потому что нам придётся делать инициализацию каждый раз, когда будет создаваться новая фигура.

/** * Создаёт новую фигуру в невидимой зоне * X-координата для генерации не должна быть ближе к правому краю, * чем максимальная ширина фигуры (MAX_FIGURE_WIDTH), чтобы влезть в экран */ private void spawnNewFigure(){ int randomX = new Random().nextInt(COUNT_CELLS_X — MAX_FIGURE_WIDTH); this.figure = new Figure(new Coord(randomX, COUNT_CELLS_Y + OFFSET_TOP — 1)); }

На этом с хранением данных мы закончили. Переходим к методам, которые отдают информацию о поле другим классам.

Методы, передающие информацию об игровом поле

Таких метода всего два. Первый возвращает цвет ячейки (для графического модуля):

public TpReadableColor getColor(int x, int y) { return theField; }

А второй сообщает, переполнено ли поле (как это происходит, мы разобрали выше):

public boolean isOverfilled(){ for(int i = 0; i < OFFSET_TOP; i++){ if(countFilledCellsInLine != 0) return true; } return false; }

Методы, обновляющие фигуру и игровое поле

Начнём реализовывать методы, которые мы вызывали из Main.logic().

Сдвиг фигуры

За это отвечает метод tryShiftFigure(). В комментариях к его вызову из Main было сказано, что он «пробует сдвинуть фигуру». Почему пробует? Потому что если фигура находится вплотную к стене, а пользователь пытается её сдвинуть в направлении этой стены, никакого сдвига в реальности происходить не должно. Так же нельзя сдвинуть фигуру в статические ячейки на поле.

public void tryShiftFigure(ShiftDirection shiftDirection) { Coord shiftedCoords = figure.getShiftedCoords(shiftDirection); boolean canShift = true; for(Coord coord: shiftedCoords) { if((coord.y<0 || coord.y>=COUNT_CELLS_Y+OFFSET_TOP) ||(coord.x<0 || coord.x>=COUNT_CELLS_X) || ! isEmpty(coord.x, coord.y)){ canShift = false; } } if(canShift){ figure.shift(shiftDirection); } }

Что мы сделали в этом методе? Мы запросили у фигуры ячейки, которые бы она заняла в случае сдвига. А затем для каждой из этих ячеек мы проверяем, не выходит ли она за пределы поля, и не находится ли по её координатам в сетке статичный блок. Если хоть одна ячейка фигуры выходит за пределы или пытается встать на место блока – сдвига не происходит. Coord здесь – класс-оболочка с двумя публичными числовыми полями (x и y координаты).

Поворот фигуры

Логика аналогична сдвигу:

Coord rotatedCoords = figure.getRotatedCoords(); boolean canRotate = true; for(Coord coord: rotatedCoords) { if((coord.y<0 || coord.y>=COUNT_CELLS_Y+OFFSET_TOP) ||(coord.x<0 || coord.x>=COUNT_CELLS_X) ||! isEmpty(coord.x, coord.y)){ canRotate = false; } } if(canRotate){ figure.rotate(); }

Падение фигуры

Сначала код в точности повторяет предыдущие два метода:

public void letFallDown() { Coord fallenCoords = figure.getFallenCoords(); boolean canFall = true; for(Coord coord: fallenCoords) { if((coord.y<0 || coord.y>=COUNT_CELLS_Y+OFFSET_TOP) ||(coord.x<0 || coord.x>=COUNT_CELLS_X) ||! isEmpty(coord.x, coord.y)){ canFall = false; } } if(canFall) { figure.fall();

Однако теперь, в случае, если фигура дальше падать не может, нам необходимо перенести её ячейки («кубики») в theField, т.е. в разряд статичных блоков, после чего создать новую фигуру:

} else { Coord figureCoords = figure.getCoords(); /* Флаг, говорящий о том, что после будет необходимо сместить линии вниз * (т.е. какая-то линия была уничтожена) */ boolean haveToShiftLinesDown = false; for(Coord coord: figureCoords) { theField = figure.getColor(); /* Увеличиваем информацию о количестве статичных блоков в линии*/ countFilledCellsInLine++; /* Проверяем, полностью ли заполнена строка Y * Если заполнена полностью, устанавливаем haveToShiftLinesDown в true */ haveToShiftLinesDown = tryDestroyLine(coord.y) || haveToShiftLinesDown; } /* Если это необходимо, смещаем линии на образовавшееся пустое место */ if(haveToShiftLinesDown) shiftLinesDown(); /* Создаём новую фигуру взамен той, которую мы перенесли*/ spawnNewFigure(); }

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

private boolean tryDestroyLine(int y) { if(countFilledCellsInLine < COUNT_CELLS_X){ return false; } for(int x = 0; x < COUNT_CELLS_X; x++){ theField = EMPTINESS_COLOR; } /* Не забываем обновить мета-информацию! */ countFilledCellsInLine = 0; return true; }

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

private void shiftLinesDown() { /* Номер обнаруженной пустой линии (-1, если не обнаружена) */ int fallTo = -1; /* Проверяем линии снизу вверх*/ for(int y = 0; y < COUNT_CELLS_Y; y++){ if(fallTo == -1){ //Если пустот ещё не обнаружено if(countFilledCellsInLine == 0) fallTo = y; //…пытаемся обнаружить (._.) } else { //А если обнаружено if(countFilledCellsInLine != 0){ // И текущую линию есть смысл сдвигать… /* Сдвигаем… */ for(int x = 0; x < COUNT_CELLS_X; x++){ theField = theField; theField = EMPTINESS_COLOR; } /* Не забываем обновить мета-информацию*/ countFilledCellsInLine = countFilledCellsInLine; countFilledCellsInLine = 0; /* * В любом случае линия сверху от предыдущей пустоты пустая. * Если раньше она не была пустой, то сейчас мы её сместили вниз. * Если раньше она была пустой, то и сейчас пустая — мы её ещё не заполняли. */ fallTo++; } } } }

Теперь GameField реализован почти полностью — за исключением геттера для фигуры. Её ведь графическому модулю тоже придётся отрисовывать:

public Figure getFigure() { return figure; }

Теперь нам нужно написать алгоритмы, по которым фигура определяет свои координаты в разных состояниях. Да и вообще весь класс фигуры.

Класс фигуры

Реализовать это всё я предлагаю следующим образом – хранить для фигуры (1) «мнимую» координату, такую, что все реальные блоки находятся ниже и правее неё, (2) состояние поворота (их всего 4, после 4-х поворотов фигура всегда возвращается в начальное положение) и (3) маску, которая по первым двум параметрам будет определять положение реальных блоков:

/** * Мнимая координата фигуры. По этой координате * через маску генерируются координаты реальных * блоков фигуры. */ private Coord metaPointCoords; /** * Текущее состояние поворота фигуры. */ private RotationMode currentRotation; /** * Форма фигуры. */ private FigureForm form;

Rotation мод здесь будет выглядеть таким образом:

public enum RotationMode { /** Начальное положение */ NORMAL(0), /** Положение, соответствующее повороту против часовой стрелки*/ FLIP_CCW(1), /** Положение, соответствующее зеркальному отражению*/ INVERT(2), /** Положение, соответствующее повороту по часовой стрелке (или трём поворотам против)*/ FLIP_CW(3); /** Количество поворотов против часовой стрелки, необходимое для принятия положения*/ private int number; /** * Конструктор. * * @param number Количество поворотов против часовой стрелки, необходимое для принятия положения */ RotationMode(int number){ this.number = number; } /** Хранит объекты enum’а. Индекс в массиве соответствует полю number. * Для более удобной работы getNextRotationForm(). */ private static RotationMode rotationByNumber = {NORMAL, FLIP_CCW, INVERT, FLIP_CW}; /** * Возвращает положение, образованое в результате поворота по часовой стрелке * из положения perviousRotation * * @param perviousRotation Положение из которого был совершён поворот * @return Положение, образованное в результате поворота */ public static RotationMode getNextRotationFrom(RotationMode perviousRotation) { int newRotationIndex = (perviousRotation.number + 1) % rotationByNumber.length; return rotationByNumber; } }

Соответственно, от самого класса Figure нам нужен только конструктор, инициализирующий поля:

/** * Конструктор. * Состояние поворота по умолчанию: RotationMode.NORMAL * Форма задаётся случайная. * * @param metaPointCoords Мнимая координата фигуры. См. документацию одноимённого поля */ public Figure(Coord metaPointCoords){ this(metaPointCoords, RotationMode.NORMAL, FigureForm.getRandomForm()); } public Figure(Coord metaPointCoords, RotationMode rotation, FigureForm form){ this.metaPointCoords = metaPointCoords; this.currentRotation = rotation; this.form = form; } }

И методы, которыми мы пользовались в GameField следующего вида:

/** * @return Координаты реальных ячеек фигуры в текущем состоянии */ public Coord getCoords(){ return form.getMask().generateFigure(metaPointCoords, currentRotation); } /** * @return Координаты ячеек фигуры, как если бы * она была повёрнута проти часовой стрелки от текущего положения */ public Coord getRotatedCoords(){ return form.getMask().generateFigure(metaPointCoords, RotationMode.getNextRotationFrom(currentRotation)); } /** * Поворачивает фигуру против часовой стрелки */ public void rotate(){ this.currentRotation = RotationMode.getNextRotationFrom(currentRotation); } /** * @param direction Направление сдвига * @return Координаты ячеек фигуры, как если бы * она была сдвинута в указано направлении */ public Coord getShiftedCoords(ShiftDirection direction){ Coord newFirstCell = null; switch (direction){ case LEFT: newFirstCell = new Coord(metaPointCoords.x — 1, metaPointCoords.y); break; case RIGHT: newFirstCell = new Coord(metaPointCoords.x + 1, metaPointCoords.y); break; default: ErrorCatcher.wrongParameter(«direction (for getShiftedCoords)», «Figure»); } return form.getMask().generateFigure(newFirstCell, currentRotation); } /** * Меняет мнимую X-координату фигуры * для сдвига в указаном направлении * * @param direction Направление сдвига */ public void shift(ShiftDirection direction){ switch (direction){ case LEFT: metaPointCoords.x—; break; case RIGHT: metaPointCoords.x++; break; default: ErrorCatcher.wrongParameter(«direction (for shift)», «Figure»); } } /** * @return Координаты ячеек фигуры, как если бы * она была сдвинута вниз на одну ячейку */ public Coord getFallenCoords(){ Coord newFirstCell = new Coord(metaPointCoords.x, metaPointCoords.y — 1); return form.getMask().generateFigure(newFirstCell, currentRotation); } /** * Меняет мнимую Y-координаты фигуры * для сдвига на одну ячейку вниз */ public void fall(){ metaPointCoords.y—; }

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

public TpReadableColor getColor() { return form.getColor(); }

Форма фигуры и маски координат

Чтобы не занимать лишнее место, здесь я приведу реализацию только для двух форм: I-образной и J-образной. Код для остальных фигур принципиально не отличается и выложен на GitHub.

Храним для каждой фигуры маску координат (которая определяет, насколько каждый реальный блок должен отстоять от «мнимой» координаты фигуры) и цвет:

public enum FigureForm { I_FORM (CoordMask.I_FORM, TpReadableColor.BLUE), J_FORM (CoordMask.J_FORM, TpReadableColor.ORANGE); /** Маска координат (задаёт геометрическую форму) */ private CoordMask mask; /** Цвет, характерный для этой формы */ private TpReadableColor color; FigureForm(CoordMask mask, TpReadableColor color){ this.mask = mask; this.color = color; }

Реализуем методы, которые использовали выше:

/** * Массив со всеми объектами этого enum’а (для удобной реализации getRandomForm() ) */ private static final FigureForm formByNumber = {I_FORM, J_FORM, L_FORM, O_FORM, S_FORM, Z_FORM, T_FORM,}; /** * @return Маска координат данной формы */ public CoordMask getMask(){ return this.mask; } /** * @return Цвет, специфичный для этой формы */ public TpReadableColor getColor(){ return this.color; } /** * @return Случайный объект этого enum’а, т.е. случайная форма */ public static FigureForm getRandomForm() { int formNumber = new Random().nextInt(formByNumber.length); return formByNumber; }

Ну а сами маски координат я предлагаю просто захардкодить следующим образом:

/** * Каждая маска — шаблон, который по мнимой координате фигуры и * состоянию её поворота возвращает 4 координаты реальных блоков * фигуры, которые должны отображаться. * Т.е. маска задаёт геометрическую форму фигуры. * * @author DoKel * @version 1.0 */ public enum CoordMask { I_FORM( new GenerationDelegate() { @Override public Coord generateFigure(Coord initialCoord, RotationMode rotation) { Coord ret = new Coord; switch (rotation){ case NORMAL: case INVERT: ret = initialCoord; ret = new Coord(initialCoord.x , initialCoord.y — 1); ret = new Coord(initialCoord.x, initialCoord.y — 2); ret = new Coord(initialCoord.x, initialCoord.y — 3); break; case FLIP_CCW: case FLIP_CW: ret = initialCoord; ret = new Coord(initialCoord.x + 1, initialCoord.y); ret = new Coord(initialCoord.x + 2, initialCoord.y); ret = new Coord(initialCoord.x + 3, initialCoord.y); break; } return ret; } } ), J_FORM( new GenerationDelegate() { @Override public Coord generateFigure(Coord initialCoord, RotationMode rotation) { Coord ret = new Coord; switch (rotation){ case NORMAL: ret = new Coord(initialCoord.x + 1 , initialCoord.y); ret = new Coord(initialCoord.x + 1, initialCoord.y — 1); ret = new Coord(initialCoord.x + 1, initialCoord.y — 2); ret = new Coord(initialCoord.x, initialCoord.y — 2); break; case INVERT: ret = new Coord(initialCoord.x + 1 , initialCoord.y); ret = initialCoord; ret = new Coord(initialCoord.x, initialCoord.y — 1); ret = new Coord(initialCoord.x, initialCoord.y — 2); break; case FLIP_CCW: ret = initialCoord; ret = new Coord(initialCoord.x + 1, initialCoord.y); ret = new Coord(initialCoord.x + 2, initialCoord.y); ret = new Coord(initialCoord.x + 2, initialCoord.y — 1); break; case FLIP_CW: ret = initialCoord; ret = new Coord(initialCoord.x, initialCoord.y — 1); ret = new Coord(initialCoord.x + 1, initialCoord.y — 1); ret = new Coord(initialCoord.x + 2, initialCoord.y — 1); break; } return ret; } } ); /** * Делегат, содержащий метод, * который должен определять алгоритм для generateFigure() */ private interface GenerationDelegate{ /** * По мнимой координате фигуры и состоянию её поворота * возвращает 4 координаты реальных блоков фигуры, которые должны отображаться * * @param initialCoord Мнимая координата * @param rotation Состояние поворота * @return 4 реальные координаты */ Coord generateFigure(Coord initialCoord, RotationMode rotation); } private GenerationDelegate forms; CoordMask(GenerationDelegate forms){ this.forms = forms; } /** * По мнимой координате фигуры и состоянию её поворота * возвращает 4 координаты реальных блоков фигуры, которые должны отображаться. * * Запрос передаётся делегату, спецефичному для каждого объекта enum’а. * * @param initialCoord Мнимая координата * @param rotation Состояние поворота * @return 4 реальные координаты */ public Coord generateFigure(Coord initialCoord, RotationMode rotation){ return this.forms.generateFigure(initialCoord, rotation); } }

Т.е. для каждого объекта enum‘а мы передаём с помощью импровизированных (других в Java нет) делегатов метод, в котором в зависимости от переданного состояния поворота возвращаем разные реальные координаты блоков. В общем-то, можно обойтись и без делегатов, если хранить в каждом элементе отсупы для каждого из режимов поворота.

Наслаждаемся результатом

Работающая программа

P.S. Ещё раз напомню, что исходники готового проекта доступны на GitHub.

Хинт для программистов: если зарегистрируетесь на соревнования Huawei Honor Cup, бесплатно получите доступ к онлайн-школе для участников. Можно прокачаться по разным навыкам и выиграть призы в самом соревновании.

Brick puzzle: Fill tetris — перемещайте из стороны в сторону падающие сверху блоки, имеющие разную форму. Выстраивайте непрерывные ряды, чтобы уничтожить их.

Особенности:

  • Графика в стиле ретро
  • Удобное управление
  • Таблица рекордов
  • Множество увлекательных уровней

Brick puzzle: Fill tetris — перемещайте из стороны в сторону падающие сверху блоки, имеющие разную форму. Выстраивайте непрерывные ряды, чтобы уничтожить их.

Особенности:

  • Графика в стиле ретро
  • Удобное управление
  • Таблица рекордов
  • Множество увлекательных уровней

Polyblocks: Falling blocks game — перемещайте по экрану падающие блоки, которые имеют разную форму. Составляйте из стеклянных блоков непрерывные ряды, чтобы убрать их с поля.

Особенности:

  • Оригинальная графика
  • Удобное управление
  • Таблица рекордов
  • Качественный звук

Polyblocks: Falling blocks game — перемещайте по экрану падающие блоки, которые имеют разную форму. Составляйте из стеклянных блоков непрерывные ряды, чтобы убрать их с поля.

Особенности:

  • Оригинальная графика
  • Удобное управление
  • Таблица рекордов
  • Качественный звук

Block crush pop — перемещайте разноцветные блоки, имеющие различные геометрические формы, на игровое поле. Составляйте непрерывные ряды из блоков, чтобы они исчезли.

Особенности:

  • Красочная графика
  • Качественная музыка и звук
  • Простое управление
  • Таблица рекордов

Block crush pop — перемещайте разноцветные блоки, имеющие различные геометрические формы, на игровое поле. Составляйте непрерывные ряды из блоков, чтобы они исчезли.

Особенности:

  • Красочная графика
  • Качественная музыка и звук
  • Простое управление
  • Таблица рекордов

Mino battle — составляйте горизонтальные ряды из блоков, которые падают сверху. Непрерывные ряды исчезнут с игрового поля, а Вы получите игровые очки. Особенности:

  • Красочная графика
  • Простое управление
  • Таблица рекордов
  • Играйте с друзьями
  • Интересный игровой процесс

Mino battle — составляйте горизонтальные ряды из блоков, которые падают сверху. Непрерывные ряды исчезнут с игрового поля, а Вы получите игровые очки.

Особенности:

  • Красочная графика
  • Простое управление
  • Таблица рекордов
  • Играйте с друзьями
  • Интересный игровой процесс

Shining cubes — очистите игровое поле от разноцветных кубиков, падающих сверху. Формируйте ряды, состоящие 3 и более одинаковых кубиков, чтобы взорвать их.

Особенности:

  • Красочная графика
  • Хорошая музыка и звук
  • Таблица рекордов
  • Захватывающий игровой процесс

Shining cubes — очистите игровое поле от разноцветных кубиков, падающих сверху. Формируйте ряды, состоящие 3 и более одинаковых кубиков, чтобы взорвать их.

Особенности:

  • Красочная графика
  • Хорошая музыка и звук
  • Таблица рекордов
  • Захватывающий игровой процесс

Тетрис — сохраняющая мегапопулярность уже несколько десятилетий логическая игра. Изначально работала еще на не графических дисплеях компьютеров 80-х годов. Новые реализации игры гораздо красочней и разнообразней. Но все же это по сути та же игра, что и была изначально.

В интеренте можно найти десятки самых разнообразных вариантов програм для игры в Тетрис. Предлагаемая Вашему вниманию игра на наш взгляд одна из лучших. Есть выбор количества фигур (9 — как в классическом варианте, 11 — смесь фигур из трех и четырех квадратов, пентикс, 17 — все фигуры). Возможна настройка типа движения фигур (плавное или прерывистое), начальной скорости, цвета фигур (например, вариант похожий на Tetcolor). При всем этом нет «наворотов» и игра должна понравится любителям классического Тетрис.

Как и во многих других играх интерсно знать насколько твой результат сравним с тем, что показывают другие игроки. В данной игре можно как играть анономно, так и зарегистрироваться и получить уникальный ник. После этого результаты попадают в глобальную таблицу результатов. Десятка лучших игроков видна всем «тетрисоманам» 🙂

Как и все остальные игра на нашем сайте, Вы можете скачать игру Tetris абсолютно бесплатно, без регистрации и по прямой ссылке.

Фигуры выбранного типа в случайной последовательности перемещаются от вернего края стакана шириной 10 и высотой 20 клеток вниз (падают на дно стакана). В полёте Вы можете вращать фигуру по часовой стрелке, перемещать ее по горизонтали вправо и влево, а так же ускорять ее падение вниз. Фигура падает пока не наткнется на дно стакана или на упавшую ранее фигурку. Если после остановки фигуры оказываются полностью заполненными горизонтальные строки, то они сгорают, смещая не пустые строки выше вниз. Таким образом складывая падающие фигуры так, чтобы не образовалось пустых клеток можно играть продолжительное время. Темп игры постепенно увеличивается и все сложнее становиться укладывать фигуры компактно. Когда нет местя для появления следующей фигуры игра заканчивается.

Очки начисляются за ускоренный сброс фигуры и сгорающие (заполненные полностью) строки. При этом учитывается текущая скорость перемещения фигур (уровень). Дополнительный бонус засчитывается если после завершения движения фигуры сгорает более одной строки. очков.

Другие интересные игры

  • Тетрис онлайн – одна из первых игр, появившихся на заре развития компьютеров и ранее не имевшая аналогов в…

  • Наверное, многие геймеры знают, что легенды никогда не умирают, а, наоборот – пытаются захватить максимальное…

  • Как играть: Перед вами увлекательная игрушка, напоминающая классический Тетрис. Управление здесь максимально…

  • Популярная компьютерная игра, вышедшая в далёком 1984 году. Спустя десятки лет популярность игры остаётся на…

  • 1010 ДЕЛЮКС Оттенки тетриса присутствуют в этой игре в качестве аналогии фигур, но в отличии от классики, здесь…

Игра на логику и сообразительность для команд

Какая команда самая смекалистая?

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

Чья команда будет первой — та и победила!

Для этого вам понадобится всего одна такая командная головоломка. Но если вы хотите зарядить позитивом всю группу на тренинге, то приобретите несколько Тетрисов кубика рубика, и ваша разминка превратиться в настоящее поле для соревнований.

Сам командный инструмент представляет из себя разные детали скомпонованные по принципу фигур из тетриса. Они сделаны из дерева и покрыты краской. 7 деталей игры имеют разные цвета.

На что направлена игра Тетрис кубика рубика?

  • Логика
  • Командное взаимодействие
  • Обратная связь
  • Лидерство
  • Сообразительность
  • Коммуникации
  • Соревновательность

Цель игры: Собрать из деталей тетриса Кубик рубика

Формат игры: Групповой

Ход игры: Участники получают детали тетриса и на время собирают из них Кубик рубика.

В зависимости от разных упражнений выбранных ведущим для игры она может быть вариативной.
В инструкции к игре приводится несколько вариантов упражнений.

Количество участников: 1-7 человек

Пространство: Аудитория 1х1м

Время игры: 5-15 мин + обсуждение

Что входит в логическую командную игру Тетрис кубика рубика?

  • Деревянные детали тетриса
  • Инструкция по игре
  • Сумка-мешок для хранения игрового инструмента

Сделайте вызов своим командам логической игрой
и купите Тетрис кубика рубика!

Добро пожаловать в Block Puzzle Jewel , простую, но увлекательную головоломку с драгоценными камнями! 💎
Strong Block Puzzle Jewel 2019 — бесплатная головоломка. Вы можете наслаждаться этим везде, каждый раз без необходимости онлайн. Этот оффлайн имеет очень простой игровой процесс: просто перетащите, бросьте и заполните всю сетку. Но однажды сыграв, вас не остановит эта классическая кирпичная игра «.
NeОдной из самых ярких черт этого онлайн-блока является дизайн и цвет. Он имеет много красивых тем на ваш выбор. Кроме того, он имеет различные уровни сложности для создателя головоломки: простой, сложный и опытный. В игре есть Рейтинговая доска. Вы можете поделиться своим достижением с другими. Вы также делитесь им с друзьями через социальные сети.
asyЛегко играть.
— Очистите все блоки, чтобы сделать взрыв драгоценного камня!
— Блоки драгоценностей не могут вращаться.
— Перетащите блоки в любое место в сетке.
— Заполните линию и очистите блок драгоценного камня и сделайте взрыв драгоценного камня.
— Игра будет закончена, когда нет места для камней.
PuzzleБлок-головоломка лучше:
— Классическая игра-головоломка с драгоценными камнями.
— Новые игровые режимы
— Абсолютно бесплатная игра-головоломка с драгоценными камнями и без взрывов в приложении.
— Веселая игра-головоломка с драгоценными камнями.
— Нет ограничения по времени. Играйте в веселые пазлы с драгоценными камнями и играми-головоломками в любом месте и в любое время!
— Новые классические пазлы с драгоценными камнями и играми! Посмотрите, как взрыв драгоценного камня!
— Красивые блоки драгоценностей и фантастический взрыв драгоценного камня!
— Таинственный рисунок в джунглях. Посмотрите на блоки и взрыв драгоценного камня!
— Легко играть, и головоломка с драгоценными камнями и играми-головоломками для всех возрастов
📝 ПРИМЕЧАНИЯ
• Block Puzzle Jewel поддерживает мобильные телефоны.
• Block Puzzle Jewel содержит различные объявления.
• Block Puzzle Jewel — это бесплатная игра-головоломка с драгоценными камнями.
Готовы ли вы стать лучшим создателем Block Puzzle? Получи это сейчас!