Как написать тетрис на java

В предыдущих статьях этой серии мы уже успели написать сапёра, змейку и десктопный клон игры 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[COUNT_CELLS_X][COUNT_CELLS_Y+OFFSET_TOP];
    countFilledCellsInLine = new int[COUNT_CELLS_Y+OFFSET_TOP];

«Что это за 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[x][y];
}

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

public boolean isOverfilled(){
    for(int i = 0; i < OFFSET_TOP; i++){
        if(countFilledCellsInLine[COUNT_CELLS_Y+i] != 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[coord.x][coord.y] = figure.getColor();

            /* Увеличиваем информацию о количестве статичных блоков в линии*/
            countFilledCellsInLine[coord.y]++;

            /* Проверяем, полностью ли заполнена строка Y
             * Если заполнена полностью, устанавливаем  haveToShiftLinesDown в true
             */
            haveToShiftLinesDown = tryDestroyLine(coord.y) || haveToShiftLinesDown;
        }

        /* Если это необходимо, смещаем линии на образовавшееся пустое место */
        if(haveToShiftLinesDown) shiftLinesDown();

        /* Создаём новую фигуру взамен той, которую мы перенесли*/
        spawnNewFigure();
    }

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

private boolean tryDestroyLine(int y) {
    if(countFilledCellsInLine[y] < COUNT_CELLS_X){
        return false;
    }

    for(int x = 0; x < COUNT_CELLS_X; x++){
        theField[x][y] = EMPTINESS_COLOR;
    }

    /* Не забываем обновить мета-информацию! */
    countFilledCellsInLine[y] = 0;

    return true;
}

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

private void shiftLinesDown() {

    /* Номер обнаруженной пустой линии (-1, если не обнаружена) */
    int fallTo = -1;

    /* Проверяем линии снизу вверх*/
    for(int y = 0; y < COUNT_CELLS_Y; y++){
        if(fallTo == -1){ //Если пустот ещё не обнаружено
            if(countFilledCellsInLine[y] == 0) fallTo = y; //...пытаемся обнаружить (._.)
        } else { //А если обнаружено
            if(countFilledCellsInLine[y] != 0){ // И текущую линию есть смысл сдвигать...

                /* Сдвигаем... */
                for(int x = 0; x < COUNT_CELLS_X; x++){
                    theField[x][fallTo] = theField[x][y];
                    theField[x][y] = EMPTINESS_COLOR;
                }

                /* Не забываем обновить мета-информацию*/
                countFilledCellsInLine[fallTo] = countFilledCellsInLine[y];
                countFilledCellsInLine[y] = 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[newRotationIndex];
    }
}

Соответственно, от самого класса 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[formNumber];
    }

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

/**
 * Каждая маска -- шаблон, который по мнимой координате фигуры и
 * состоянию её поворота возвращает 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[4];

                    switch (rotation){
                        case NORMAL:
                        case INVERT:
                            ret[0] = initialCoord;
                            ret[1] = new Coord(initialCoord.x , initialCoord.y - 1);
                            ret[2] = new Coord(initialCoord.x, initialCoord.y - 2);
                            ret[3] = new Coord(initialCoord.x, initialCoord.y - 3);
                            break;
                        case FLIP_CCW:
                        case FLIP_CW:
                            ret[0] = initialCoord;
                            ret[1] = new Coord(initialCoord.x + 1, initialCoord.y);
                            ret[2] = new Coord(initialCoord.x + 2, initialCoord.y);
                            ret[3] = 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[4];

                    switch (rotation){
                        case NORMAL:
                            ret[0] = new Coord(initialCoord.x + 1 , initialCoord.y);
                            ret[1] = new Coord(initialCoord.x + 1, initialCoord.y - 1);
                            ret[2] = new Coord(initialCoord.x + 1, initialCoord.y - 2);
                            ret[3] = new Coord(initialCoord.x, initialCoord.y - 2);
                            break;
                        case INVERT:
                            ret[0] = new Coord(initialCoord.x + 1 , initialCoord.y);
                            ret[1] = initialCoord;
                            ret[2] = new Coord(initialCoord.x, initialCoord.y - 1);
                            ret[3] = new Coord(initialCoord.x, initialCoord.y - 2);
                            break;
                        case FLIP_CCW:
                            ret[0] = initialCoord;
                            ret[1] = new Coord(initialCoord.x + 1, initialCoord.y);
                            ret[2] = new Coord(initialCoord.x + 2, initialCoord.y);
                            ret[3] = new Coord(initialCoord.x + 2, initialCoord.y - 1);
                            break;
                        case FLIP_CW:
                            ret[0] = initialCoord;
                            ret[1] = new Coord(initialCoord.x, initialCoord.y - 1);
                            ret[2] = new Coord(initialCoord.x + 1, initialCoord.y - 1);
                            ret[3] = 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.

Задание 17 Я тут немножечко подправил код. Но в целом — отлично. Запускаем и наслаждаемся тетрисом. P.S. Не забудь отрегулировать высоту консоли Задание 16 Напиши свою реализацию методов left(), right(), up(), down() в классе Figure. Подумай, что должны делать эти методы? Задание 15 Напиши реализацию метода step в классе Tetris. В методе надо переместить фигурку вниз на один шаг. Если после перемещения положить фигурку на текущее место невозможно, то: а) поднять ее обратно (up) б) «приземлить» ее (landed) в) удалить все «полные линии» в объекте field г) создать новую фигурку взамен старой. Задание 14 Напиши реализацию метода removeFullLines в классе Field Надо а) удалить все строки из матрицы, которые полностью заполнены (состоят из одних единиц) б) сместить оставшиеся строки вниз в) создать новые строки взамен отсутствующих. ВАЖНО! matrix[y][x] содержит элемент с координатами (x,y) matrix[i] содержит i-ю строку а) Мы можем удалить стоку: matrix[i] = null б)Скопировать [ссылку на] строку: matrix[i+1] = matrix[i]; в)Создать новую строку: matrix[i] = new int[width]; Задание 13 Теперь приступим к реализации созданных методов. Напиши реализацию метода print в классе Field а) Метода должен выводить на экран прямоугольник. б) Высота прямоугольника равна height, ширина — width в) Если данная клетка пустая — вывести точку, если не пустая — английский X Подсказка: if (matrix[y][x]==0) … Задание 12 В тетрисе мы управляем движением фигурки с помощью клавиатуры. Тут есть 4 действия: движение влево (кнопка влево) движение вправо (кнопка влево) поворот фигурки (цифра 5 на доп.клавиатуре справа) падение вниз (пробел) Мы будем обрабатывать ввод с клавиатуры в методе run() класса Tetris. И тут у меня для тебя две новости: хорошая и плохая Плохая новость состоит в том, что java не позволяет считать нажатые символы с клавиатуры, пока пользователь не нажмет enter. Не очень удобно, правда? Хорошая новость состоит в том, я написал специальный класс (KeyboardObserver), который позволяет обойти это ограничение. Так что ты можешь воспользоваться им. Есть еще и отличная новость. Ты до сих пор отлично справлялся, поэтому я помогу тебе немного. Я написал за тебя методы: а) createRandomFigure в FigureFactory б) run в Tetris Изучи их внимательно и переходи дальше. Задание 11 Теперь создай класс FigureFactory. С его помощью мы будем создавать фигуры различных форм. Пока он будет содержать только один статический метод createRandomFigure: public static Figure createRandomFigure(int x,int y) Задание 10 Так же нам понадобятся методы для управления фигуркой. Добавь в класс Figure методы: left() — для движения фигурки влево. right() — для движения фигурки вправо. down() — для движения фигурки вниз. up() — для движения фигурки вверх. downMaximum() — падение фигурки в низ до дна. boolean isCurrentPositionAvailable() — проверка — может ли фигурка быть помещена в текущую позицию. landed() — вызывается, когда фигурка достигла дна или уперлась в другую фигурку Все ее занятые клетки теперь должны добавиться в Field. Задание 9 Если ты обратил внимание, мы пишем программу «сверху вниз». Сначала решили, какие классы нам нужны. Затем — какие методы. А потом уже начнем писать код этих методов. Таким образом мы разбиваем большую задачу на множество маленьких. Когда код всех методов будет готов, останется только проверить — так ли все работает, как должно быть. И если надо — внести некоторые изменения. Задание 8 Теперь создадим костяк класса Figure. Этот класс будет описывают падающую фигурку. Нам понадобятся ее координаты и форма. За координаты будут отвечать две переменные x и y. За форму — матрица. Двумерный массив 3×3, состоящий из единиц и нулей. Единицей мы обозначаем что клетка есть, нулем — что она пустая. Добавь в класс Figure два поля поля: x типа int, y типа int. Еще добавь двумерный массив: matrix(матрица) типа int[][]. Там же добавь getter’ы для созданных переменных. Задание 7 Нам понадобится еще 4 метда в классе Field: а) print() — объект будет отрисовывать на экран свое текущее состояние; б) removeFullLines() — будет удалять из матрицы полностью заполненные строки и сдвигать вышележащие строки вниз; в) Integer getValue(int x, int y) — возвращает значение которое находится в матрице с координатами x и y; г) void setValue(int x, int y, int value) — устанавливает переданное значение в ячейку массива (матрицы) с координатами x, y. Задание 6 Теперь перейдем к классу Field. Он будет отвечать за хранение данных о текущих занятых и свободных клетках на поле игры. Добавь в класс Field два поля поля: width (ширина) типа int, height(высота) типа int. Так же нам понадобится матрица — двумерный массив: matrix(матрица) типа int[][]; Там же добавь getter’ы для созданных переменных. ВАЖНО! Двумерный массив можно представить как массив массивов или как прямоугольную матрицу. При этом первой координатой в массиве у нас будет номер строки, а второй — столбца. Другими словами ячейка с координатами x, y — это matrix[y][x]. Задание 5 Теперь нужно создать объект самого Тетриса. Добавь в класс Tetris статическую переменную game. (тип — Tetris, видимость — public) Затем в методе main создай объект типа Тетрис и сохрани его в эту переменную. Затем добавь вызов метода run. Должно получиться что-то типа такого: game = new Tetris(); game.run(); Задание 4 Так же еще нам понадобится пара методов. Добавь в класс Tetris методы run() и step(): run() будет отвечать за всю игру. А step() — за один шаг в игре. Задание 3 Отлично! Теперь добавь в Tetris две переменные: field типа Field и figure типа Figure. С их помощью мы будем хранить информацию о текущей фигурке и о состоянии дел на «поле клеток». Так же добавь getter’ы для созданных переменных. Задание 2 А еще нам понадобится метод main. Как же без него. Добавь метод main в класс Tetris Задание 1 Давай напишем игру Тетрис. Наш Тетрис будет состоять из двух вещей: поля из клеток и фигурки, которая падает. Поэтому для начала создай три класса: Field(поле с клетками), Figure(фигура) и сам Tetris.

Released in 1984, Tetris is a Russian tile-matching puzzle video game originally designed and programmed by Alexey Pajitnov. Tetris is based on usage of Tetrominoes. Where as we are in 2016, Tetris stays the most known game of history. In the following article, we’re going to create a Tetris game in Java with Swing and Java 2D API.

Programming Tip : Level up your programming skills by migrating your software development/testing tools online into the cloud with hosted virtual desktop from CloudDesktopOnline and remote accessibility from anywhere on any device(PC/Mac/android/iOS). Looking for a dedicated server? Try out dedicated gpu server hosting from Apps4Rent.com with unbelievable plans & pricing.

1. Create the Tetrominoes

To design Tetrominoes, we’re going to use a Java enum with 2 fields : one for coords and one for color of the Tetrominoe.

enum Tetrominoes {
  NoShape(new int[][] { { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } }, new Color(0, 0, 0)),
  ZShape(new int[][] { { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, 1 } }, new Color(204, 102, 102)),
  SShape(new int[][] { { 0, -1 }, { 0, 0 }, { 1, 0 }, { 1, 1 } }, new Color(102, 204, 102)),
  LineShape(new int[][] { { 0, -1 }, { 0, 0 }, { 0, 1 }, { 0, 2 } }, new Color(102, 102, 204)),
  TShape(new int[][] { { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, 1 } }, new Color(204, 204, 102)),
  SquareShape(new int[][] { { 0, 0 }, { 1, 0 }, { 0, 1 }, { 1, 1 } }, new Color(204, 102, 204)),
  LShape(new int[][] { { -1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } }, new Color(102, 204, 204)),
  MirroredLShape(new int[][] { { 1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } }, new Color(218, 170, 0));

  public int[][] coords;
  public Color color;

  private Tetrominoes(int[][] coords, Color c) {
    this.coords = coords;
    color = c;
  }
}

2. Create the Shape

Basically, we’re going to create a Shape object having a Tetrominoe as a field and coords to define the position on the screen of a current shape. We must also define methods to rotate a shape on the left and on the right in the space. Then, methods to change coords when a shape scroll down.

public class Shape {
  private Tetrominoes pieceShape;
  private int[][] coords;

  public Shape() {
    coords = new int[4][2];
    setShape(Tetrominoes.NoShape);
  }

  public void setShape(Tetrominoes shape) {
    for (int i = 0; i < 4; i++) {
      for (int j = 0; j < 2; ++j) {
        coords[i][j] = shape.coords[i][j];
      }
    }

    pieceShape = shape;
  }

  private void setX(int index, int x) {
    coords[index][0] = x;
  }

  private void setY(int index, int y) {
    coords[index][1] = y;
  }

  public int x(int index) {
    return coords[index][0];
  }

  public int y(int index) {
    return coords[index][1];
  }

  public Tetrominoes getShape() {
    return pieceShape;
  }

  public void setRandomShape() {
    Random r = new Random();
    int x = Math.abs(r.nextInt()) % 7 + 1;
    Tetrominoes[] values = Tetrominoes.values();
    setShape(values[x]);
  }

  public int minX() {
    int m = coords[0][0];

    for (int i = 0; i < 4; i++) {
      m = Math.min(m, coords[i][0]);
    }

    return m;
  }

  public int minY() {
    int m = coords[0][1];

    for (int i = 0; i < 4; i++) {
      m = Math.min(m, coords[i][1]);
    }

    return m;
  }

  public Shape rotateLeft() {
    if (pieceShape == Tetrominoes.SquareShape)
      return this;

    Shape result = new Shape();
    result.pieceShape = pieceShape;

    for (int i = 0; i < 4; i++) {
      result.setX(i, y(i));
      result.setY(i, -x(i));
    }

    return result;
  }

  public Shape rotateRight() {
    if (pieceShape == Tetrominoes.SquareShape)
      return this;

    Shape result = new Shape();
    result.pieceShape = pieceShape;

    for (int i = 0; i < 4; i++) {
      result.setX(i, -y(i));
      result.setY(i, x(i));
    }

    return result;
  }

}

3. Create the Board

To display our Tetrominoes, we need to create a board that is a Swing JPanel. We define a width and a height. Then, to animate the game we use a javax.swing.Timer which is in charge to call at a define frequency the method actionPerformed of an implementation of interface ActionListener. So, our Board class must implement ActionListener interface from Swing API. Rendering of our Tetris game is made in the paint method. To manage interactions with players, we need to add our own key listener that must extend KeyAdapter class.

public class Board extends JPanel implements ActionListener {

  private static final int BOARD_WIDTH = 10;
  private static final int BOARD_HEIGHT = 22;
  private Timer timer;
  private boolean isFallingFinished = false;
  private boolean isStarted = false;
  private boolean isPaused = false;
  private int numLinesRemoved = 0;
  private int curX = 0;
  private int curY = 0;
  private JLabel statusBar;
  private Shape curPiece;
  private Tetrominoes[] board;

  public Board(Tetris parent) {
    setFocusable(true);
    curPiece = new Shape();
    timer = new Timer(400, this); // timer for lines down
    statusBar = parent.getStatusBar();
    board = new Tetrominoes[BOARD_WIDTH * BOARD_HEIGHT];
    clearBoard();
    addKeyListener(new MyTetrisAdapter());
  }

  public int squareWidth() {
    return (int) getSize().getWidth() / BOARD_WIDTH;
  }

  public int squareHeight() {
    return (int) getSize().getHeight() / BOARD_HEIGHT;
  }

  public Tetrominoes shapeAt(int x, int y) {
    return board[y * BOARD_WIDTH + x];
  }

  private void clearBoard() {
    for (int i = 0; i < BOARD_HEIGHT * BOARD_WIDTH; i++) {
      board[i] = Tetrominoes.NoShape;
    }
  }

  private void pieceDropped() {
    for (int i = 0; i < 4; i++) {
      int x = curX + curPiece.x(i);
      int y = curY - curPiece.y(i);
      board[y * BOARD_WIDTH + x] = curPiece.getShape();
    }

    removeFullLines();

    if (!isFallingFinished) {
      newPiece();
    }
  }

  public void newPiece() {
    curPiece.setRandomShape();
    curX = BOARD_WIDTH / 2 + 1;
    curY = BOARD_HEIGHT - 1 + curPiece.minY();

    if (!tryMove(curPiece, curX, curY - 1)) {
      curPiece.setShape(Tetrominoes.NoShape);
      timer.stop();
      isStarted = false;
      statusBar.setText("Game Over");
    }
  }

  private void oneLineDown() {
    if (!tryMove(curPiece, curX, curY - 1))
      pieceDropped();
  }

  @Override
  public void actionPerformed(ActionEvent ae) {
    if (isFallingFinished) {
      isFallingFinished = false;
      newPiece();
    } else {
      oneLineDown();
    }
  } 

  private void drawSquare(Graphics g, int x, int y, Tetrominoes shape) {
    Color color = shape.color;
    g.setColor(color);
    g.fillRect(x + 1, y + 1, squareWidth() - 2, squareHeight() - 2);
    g.setColor(color.brighter());
    g.drawLine(x, y + squareHeight() - 1, x, y);
    g.drawLine(x, y, x + squareWidth() - 1, y);
    g.setColor(color.darker());
    g.drawLine(x + 1, y + squareHeight() - 1, x + squareWidth() - 1, y + squareHeight() - 1);
    g.drawLine(x + squareWidth() - 1, y + squareHeight() - 1, x + squareWidth() - 1, y + 1);
  }

  @Override
  public void paint(Graphics g) {
    super.paint(g);
    Dimension size = getSize();
    int boardTop = (int) size.getHeight() - BOARD_HEIGHT * squareHeight();

    for (int i = 0; i < BOARD_HEIGHT; i++) {
      for (int j = 0; j < BOARD_WIDTH; ++j) {
        Tetrominoes shape = shapeAt(j, BOARD_HEIGHT - i - 1);

        if (shape != Tetrominoes.NoShape) {
          drawSquare(g, j * squareWidth(), boardTop + i * squareHeight(), shape);
        }
      }
    }

    if (curPiece.getShape() != Tetrominoes.NoShape) {
      for (int i = 0; i < 4; ++i) {
        int x = curX + curPiece.x(i);
        int y = curY - curPiece.y(i);
        drawSquare(g, x * squareWidth(), boardTop + (BOARD_HEIGHT - y - 1) * squareHeight(), curPiece.getShape());
      }
    }
  }

  public void start() {
    if (isPaused)
      return;

    isStarted = true;
    isFallingFinished = false;
    numLinesRemoved = 0;
    clearBoard();
    newPiece();
    timer.start();
  }

  public void pause() {
    if (!isStarted)
      return;

    isPaused = !isPaused;

    if (isPaused) {
      timer.stop();
      statusBar.setText("Paused");
    } else {
      timer.start();
      statusBar.setText(String.valueOf(numLinesRemoved));
    }

    repaint();
  }

  private boolean tryMove(Shape newPiece, int newX, int newY) {
    for (int i = 0; i < 4; ++i) {
      int x = newX + newPiece.x(i);
      int y = newY - newPiece.y(i);

      if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT)
        return false;

      if (shapeAt(x, y) != Tetrominoes.NoShape)
        return false;
    }

    curPiece = newPiece;
    curX = newX;
    curY = newY;
    repaint();

    return true;
  }

  private void removeFullLines() {
    int numFullLines = 0;

    for (int i = BOARD_HEIGHT - 1; i >= 0; --i) {
      boolean lineIsFull = true;

      for (int j = 0; j < BOARD_WIDTH; ++j) {
        if (shapeAt(j, i) == Tetrominoes.NoShape) {
          lineIsFull = false;
          break;
        }
      }

      if (lineIsFull) {
        ++numFullLines;

        for (int k = i; k < BOARD_HEIGHT - 1; ++k) {
          for (int j = 0; j < BOARD_WIDTH; ++j) {
            board[k * BOARD_WIDTH + j] = shapeAt(j, k + 1);
          }
        }
      }

      if (numFullLines > 0) {
        numLinesRemoved += numFullLines;
        statusBar.setText(String.valueOf(numLinesRemoved));
        isFallingFinished = true;
        curPiece.setShape(Tetrominoes.NoShape);
        repaint();
      }
    }
  }

  private void dropDown() {
    int newY = curY;

    while (newY > 0) {
      if (!tryMove(curPiece, curX, newY - 1))
        break;

      --newY;
    }

    pieceDropped();
  }

  class MyTetrisAdapter extends KeyAdapter {
    @Override
    public void keyPressed(KeyEvent ke) {
      if (!isStarted || curPiece.getShape() == Tetrominoes.NoShape)
        return;

      int keyCode = ke.getKeyCode();

      if (keyCode == 'p' || keyCode == 'P')
        pause();

      if (isPaused)
        return;

      switch (keyCode) {
        case KeyEvent.VK_LEFT:
          tryMove(curPiece, curX - 1, curY);
          break;
        case KeyEvent.VK_RIGHT:
          tryMove(curPiece, curX + 1, curY);
          break;
        case KeyEvent.VK_DOWN:
          tryMove(curPiece.rotateRight(), curX, curY);
          break;
        case KeyEvent.VK_UP:
          tryMove(curPiece.rotateLeft(), curX, curY);
          break;
        case KeyEvent.VK_SPACE:
          dropDown();
          break;
        case 'd':
        case 'D':
          oneLineDown();
          break;
      }

    }
  }

}

4. Final Touch

Now, we just need to assemble all the elements in a Tetris class with a main entry point.

public class Tetris extends JFrame {

  private JLabel statusBar;

  public Tetris() {
    statusBar = new JLabel("0");
    add(statusBar, BorderLayout.SOUTH);
    Board board = new Board(this);
    add(board);
    board.start();
    setSize(200, 400);
    setTitle("My Tetris");
    setDefaultCloseOperation(EXIT_ON_CLOSE);
  }

  public JLabel getStatusBar() {
    return statusBar;
  }

  public static void main(String[] args) {
    Tetris myTetris = new Tetris();
    myTetris.setLocationRelativeTo(null);
    myTetris.setVisible(true);
  }

}

5. Demo

Now, we can run our Tetris made in Java with Swing and Java 2D API.

tetris

6. Extra

In extra, you can see tutorial to create this Tetris game in live on Youtube. Tutorial is in 3 parts.

Here in this post, i will share a Java program to make a Tetris game. I have already wrote a post on how to make a tetris game in C language. I have discussed the data structures used in the program in that post. The Java program is almost similar to it. The description is available in this post: Making of Tetris Game in C.


The following video shows the java code in action:

The tetris game Java source code is as follows:

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.util.Random;
import javax.swing.JFrame;
import javax.swing.JOptionPane;



public class Tetris extends JFrame{

boolean stopthread=false;
int SIZE_HORIZ=13;
int SIZE_VERTI=35;
Graphics g;
boolean waitnow=false;
int[][] board=new int[SIZE_VERTI][SIZE_HORIZ];
int t_[]={1,0,1,1,1,2,2,1};
int t_90[]={0,1,1,0,1,1,2,1};
int t_180[]={0,1,1,0,1,1,1,2};
int t_270[]={0,1,1,1,1,2,2,1};
int l_[]={0,2,1,0,1,1,1,2};
int l_90[]={0,1,1,1,2,1,2,2};
int l_180[]={1,0,1,1,1,2,2,0};
int l_270[]={0,0,0,1,1,1,2,1};
int s_[]={0,0,0,1,1,0,1,1};
int z_[]={1,1,1,2,2,0,2,1};
int z_90[]={0,1,1,1,1,2,2,2};
int i_ver[]={0,1,1,1,2,1,3,1};
int i_hor[]={1,0,1,1,1,2,1,3};
String scorestr;
Random rand=new Random(System.currentTimeMillis());
Color colors[]={Color.BLUE,Color.GREEN,Color.MAGENTA,Color.RED,Color.YELLOW};
File HighScore;
Color bgcolor=Color.WHITE;
/*
Numbering for blocks:
(values of fallingBlockNumber)
0=T
1=L
2=S
3=Z
4=I
*/
int [] blockarray;
int fallingblockNum;
int fallingBlockVersion=0;
int fallingBlockRow=0;
int fallingBlockCol=0;
int startdelay=200;
int motiondelay;
int scoreInc=5;
int myscore=0;
int tversion;
boolean spawn=true;
int scorespeedctrl=0;
int timehalving=0;
String highscoreholder;
int highscore;
KeyListener kl;

public Tetris()
{
int i,j;
startdelay=250;
scoreInc=5;
myscore=0;
motiondelay=startdelay;
NextBlock();
for(i=0;i<35;i++)
    for(j=0;j<13;j++)
 board[i][j]=0;
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(480,500);
setResizable(false);
setLocationRelativeTo(null);
HighScore=new File(System.getProperty("java.io.tmpdir")+File.separator+"TetrisHighScore");
try{
if(HighScore.exists())
    {
    BufferedReader br=new BufferedReader(new FileReader(HighScore));
    highscoreholder=br.readLine();
    highscore=Integer.parseInt(br.readLine());
    br.close();
    }
else
    {
    highscoreholder=null;
    highscore=0;
    }
}catch(Exception ex){}
kl=new KeyListener() {
    @Override
    public void keyTyped(KeyEvent e) {}

    @Override
    public void keyPressed(KeyEvent e) {
    
        if(!spawn&&!waitnow)
 {
            waitnow=true;
            int key=e.getKeyCode();
     if(key==KeyEvent.VK_UP)//up
  {
  if(fallingblockNum==0||fallingblockNum==1)
      tversion=(fallingBlockVersion+1)%4;
  else if(fallingblockNum==4||fallingblockNum==3)
      tversion=(fallingBlockVersion+1)%2;

  if(fallingblockNum!=2&&isDrawable(fallingBlockRow,fallingBlockCol,tversion))
      {
      clearOldBlockVersion(g);
      fallingBlockVersion=tversion;
      blockarray=getFallingBlockArray();
      drawNewBlockVersion(g);
      }
  }
     else if(key==KeyEvent.VK_LEFT)//left
  {
  if(isDrawable(fallingBlockRow,fallingBlockCol-1,fallingBlockVersion))
      {
      clearOldBlockVersion(g);
      fallingBlockCol--;
      drawNewBlockVersion(g);
      }
  }
     else if(key==KeyEvent.VK_RIGHT)//right
  {
  if(isDrawable(fallingBlockRow,fallingBlockCol+1,fallingBlockVersion))
      {
      clearOldBlockVersion(g);
      fallingBlockCol++;
      drawNewBlockVersion(g);
      }
  }
     else if(key==KeyEvent.VK_DOWN)//down
  {
  if(isDrawable(fallingBlockRow+1,fallingBlockCol,fallingBlockVersion))
      {
      clearOldBlockVersion(g);
      fallingBlockRow++;
      drawNewBlockVersion(g);
      }
  }
            waitnow=false;
 }
    }
    @Override
    public void keyReleased(KeyEvent e) {}
};
th.start();
addKeyListener(kl);
}

void NextBlock()
{
fallingblockNum=rand.nextInt(5);
if(fallingblockNum==0||fallingblockNum==1)
 fallingBlockVersion=rand.nextInt(4);
else if(fallingblockNum==4||fallingblockNum==3)
 fallingBlockVersion=rand.nextInt(2);
else
 fallingBlockVersion=0;
fallingBlockRow=0;
fallingBlockCol=5;
blockarray=getFallingBlockArray();
}

int[] getFallingBlockArray()
{
int a=fallingblockNum*10+fallingBlockVersion;
switch(a)
    {
    case 0:return (t_);
    case 1:return (t_90);
    case 2:return (t_180);
    case 3:return (t_270);
    case 10:return (l_);
    case 11:return (l_90);
    case 12:return (l_180);
    case 13:return (l_270);
    case 20:return (s_);
    case 30:return (z_);
    case 31:return (z_90);
    case 40:return (i_hor);
    case 41:return (i_ver);
    }
return (i_ver);
}

boolean isDrawable(int newrow,int newcol,int blockversion)
{
int i,tempversion;
boolean flag=true;
tempversion=fallingBlockVersion;
fallingBlockVersion=blockversion;
blockarray=getFallingBlockArray();
for(i=0;i<8;i+=2)
    {
    if(newrow+blockarray[i]>34||newrow+blockarray[i]<0)
 {
 flag=false;
 break;
 }
    if(newcol+blockarray[i+1]>12||newcol+blockarray[i+1]<0)
 {
 flag=false;
 break;
 }
    if(board[(newrow+blockarray[i])][(newcol+blockarray[i+1])]==2)
 {
 flag=false;
 break;
 }
    }
fallingBlockVersion=tempversion;
blockarray=getFallingBlockArray();
return flag;
}

void clearOldBlockVersion(Graphics g)
{
int i,r,c;
for(i=0;i<8;i+=2)
 {
 r=fallingBlockRow+blockarray[i];
 c=fallingBlockCol+blockarray[i+1];
 board[r][c]=0;
 g.setColor(bgcolor);
        g.fillRect(8+c*13,32+r*13,14,14);
 }
}

void drawNewBlockVersion(Graphics g)
{
int i,r,c;
for(i=0;i<8;i+=2)
 {
 r=fallingBlockRow+blockarray[i];
 c=fallingBlockCol+blockarray[i+1];
 board[r][c]=1;
        g.setColor(colors[fallingblockNum]);
 g.fillRect(8+c*13,32+r*13,13,13);
        g.setColor(Color.BLACK);//cyan,orange
 g.drawRect(8+c*13,32+r*13,13,13);
 }
}
boolean isGameOver(Graphics g)
{
if(isDrawable(0,5,fallingBlockVersion)==false)
    return true;
drawNewBlockVersion(g);
if(isAtBottom())
    return true;
return false;
}

boolean isAtBottom()
{
int i,max=0,ti,tj;
for(i=0;i<8;i+=2)
    if(blockarray[i]>max)
 max=blockarray[i];
if(fallingBlockRow+max>=34)
 return true;
for(i=0;i<8;i+=2)
    {
    ti=blockarray[i]+fallingBlockRow;
    tj=blockarray[i+1]+fallingBlockCol;
    if(board[ti+1][tj]==2)
       return true;
    }
return false;
}

void showScore(Graphics g)
{
int left,top;
left=getWidth()-100;
top=getHeight()/2;
g.setColor(bgcolor);
g.fillRect(left,top,80,70);
g.setColor(Color.RED);
g.setFont(new Font("Arial",Font.BOLD,14));
g.drawString("Score: "+Integer.toString(myscore),left,top+20);
}

void CollapseFilledRow(Graphics g)
{
int i,j,k,sum,copyskipover=0,r;
for(i=34;i>=0;)
    {
    sum=0;//full flag
    for(j=0;j<13;j++)
 sum+=board[i][j];
    if(sum==2*13)//row full
 {
 myscore+=scoreInc;
 copyskipover++;
 }
    if(sum==0)
 break;
    i--;
    if(copyskipover>0)
 {
 for(j=0;j<13;j++)
     {
     r=i+copyskipover;
     board[r][j]=board[i][j];
     if(board[i][j]==0)
  {
                g.setColor(bgcolor);
                g.fillRect(8+j*13,32+r*13,14,14);
  }
     else
  {
                g.setColor(Color.GREEN);
                g.fillRect(8+j*13,32+r*13,13,13);
                g.setColor(Color.BLACK);
                g.drawRect(8+j*13,32+r*13,13,13);
  }
     }
 }
    }
for(k=0;k<copyskipover;k++)
    {
    r=i+k;
    for(j=0;j<13;j++)
 {
 board[r][j]=0;
        g.setColor(bgcolor);
 g.fillRect(8+j*13,32+r*13,14,14);
 }
    }
showScore(g);
}


public static void main(String[] args)
{
Tetris tt=new Tetris();
tt.setVisible(true);
}

void GameOver(Graphics g)
{
stopthread=true;
g.setColor(Color.RED);
g.setFont(new Font("Arial",Font.BOLD,28));
String str="Game Over.";
g.drawString(str,getWidth()/2-10, getHeight()/2);
if(highscore>0)
    str="Highscore : "+highscoreholder+" - "+Integer.toString(highscore);
g.setFont(new Font("Arial",Font.BOLD,16));
g.drawString(str,getWidth()/2-30, getHeight()/2+80);
if(myscore>highscore)
    {
        highscoreholder=JOptionPane.showInputDialog("New high score. Enter your name:");
        highscore=myscore;
        try{
        if(!HighScore.exists())
            HighScore.createNewFile();
        BufferedWriter bw=new BufferedWriter(new FileWriter(HighScore));
        bw.write(highscoreholder);
        bw.newLine();
        bw.write(Integer.toString(highscore));
        bw.close();
        }catch(Exception ee){}
    }
}


Thread th=new Thread()
{
int i;
public void run()

{
try {
    while(!isShowing())
        Thread.sleep(1000);
    } catch (InterruptedException ex) {}
setOpacity(1f);
setBackground(bgcolor);
getContentPane().setBackground(bgcolor);
g=getGraphics();
g.setColor(Color.RED);
g.drawRect(6,30,13*13+6,35*13+6);
showScore(g);
while(!stopthread)
{
g.setColor(Color.RED);
g.drawRect(6,30,13*13+6,35*13+6);
while (waitnow)
    {
    try {
        Thread.sleep(20);
    } catch (InterruptedException ex) {}
    }
waitnow=true;
if(isAtBottom()&&!spawn)
 {
 for(i=0;i<8;i+=2)
     {
     board[fallingBlockRow+blockarray[i]][fallingBlockCol+blockarray[i+1]]=2;
     }
 spawn=true;
 CollapseFilledRow(g);
 }
    if(spawn)
 {
 NextBlock();
 blockarray=getFallingBlockArray();
 spawn=false;
 if(isGameOver(g))
     {
     GameOver(g);
     return;
     }
 }
    else
 {
 timehalving=(timehalving+1)%3;
 if(timehalving==2)
  {
  clearOldBlockVersion(g);
  fallingBlockRow++;
  drawNewBlockVersion(g);
  }
 }
    scorespeedctrl=(scorespeedctrl+1)%140;
    if(scorespeedctrl==0&&motiondelay>0)
 {
 motiondelay-=12;
 scoreInc+=2;
        if(motiondelay<0)
            motiondelay=0;
 }
    waitnow=false;
    try {
        Thread.sleep(motiondelay);
            } catch (InterruptedException ex) {}
}
}
};
}

Tetris/Java is part of Tetris. You may find other members of Tetris at Category:Tetris.

Code[edit]

Works with: java version 8

package tetris;

import java.awt.*;
import java.awt.event.*;
import static java.lang.Math.*;
import static java.lang.String.format;
import java.util.*;
import javax.swing.*;
import static tetris.Config.*;

public class Tetris extends JPanel implements Runnable {
    enum Dir {
        right(1, 0), down(0, 1), left(-1, 0);

        Dir(int x, int y) {
            this.x = x;
            this.y = y;
        }
        final int x, y;
    };

    public static final int EMPTY = -1;
    public static final int BORDER = -2;

    Shape fallingShape;
    Shape nextShape;

    // position of falling shape
    int fallingShapeRow;
    int fallingShapeCol;

    final int[][] grid = new int[nRows][nCols];

    Thread fallingThread;
    final Scoreboard scoreboard = new Scoreboard();
    static final Random rand = new Random();

    public Tetris() {
        setPreferredSize(dim);
        setBackground(bgColor);
        setFocusable(true);

        initGrid();
        selectShape();

        addMouseListener(new MouseAdapter() {
            @Override
            public void mousePressed(MouseEvent e) {
                if (scoreboard.isGameOver()) {
                    startNewGame();
                    repaint();
                }
            }
        });

        addKeyListener(new KeyAdapter() {
            boolean fastDown;

            @Override
            public void keyPressed(KeyEvent e) {

                if (scoreboard.isGameOver())
                    return;

                switch (e.getKeyCode()) {

                    case KeyEvent.VK_UP:
                        if (canRotate(fallingShape))
                            rotate(fallingShape);
                        break;

                    case KeyEvent.VK_LEFT:
                        if (canMove(fallingShape, Dir.left))
                            move(Dir.left);
                        break;

                    case KeyEvent.VK_RIGHT:
                        if (canMove(fallingShape, Dir.right))
                            move(Dir.right);
                        break;

                    case KeyEvent.VK_DOWN:
                        if (!fastDown) {
                            fastDown = true;
                            while (canMove(fallingShape, Dir.down)) {
                                move(Dir.down);
                                repaint();
                            }
                            shapeHasLanded();
                        }
                }
                repaint();
            }

            @Override
            public void keyReleased(KeyEvent e) {
                fastDown = false;
            }
        });
    }

    void selectShape() {
        fallingShapeRow = 1;
        fallingShapeCol = 5;
        fallingShape = nextShape;
        Shape[] shapes = Shape.values();
        nextShape = shapes[rand.nextInt(shapes.length)];
        if (fallingShape != null)
            fallingShape.reset();
    }

    void startNewGame() {
        stop();
        initGrid();
        selectShape();
        scoreboard.reset();
        (fallingThread = new Thread(this)).start();
    }

    void stop() {
        if (fallingThread != null) {
            Thread tmp = fallingThread;
            fallingThread = null;
            tmp.interrupt();
        }
    }

    void initGrid() {
        for (int r = 0; r < nRows; r++) {
            Arrays.fill(grid[r], EMPTY);
            for (int c = 0; c < nCols; c++) {
                if (c == 0 || c == nCols - 1 || r == nRows - 1)
                    grid[r][c] = BORDER;
            }
        }
    }

    @Override
    public void run() {

        while (Thread.currentThread() == fallingThread) {

            try {
                Thread.sleep(scoreboard.getSpeed());
            } catch (InterruptedException e) {
                return;
            }

            if (!scoreboard.isGameOver()) {
                if (canMove(fallingShape, Dir.down)) {
                    move(Dir.down);
                } else {
                    shapeHasLanded();
                }
                repaint();
            }
        }
    }

    void drawStartScreen(Graphics2D g) {
        g.setFont(mainFont);

        g.setColor(titlebgColor);
        g.fill(titleRect);
        g.fill(clickRect);

        g.setColor(textColor);
        g.drawString("Tetris", titleX, titleY);

        g.setFont(smallFont);
        g.drawString("click to start", clickX, clickY);
    }

    void drawSquare(Graphics2D g, int colorIndex, int r, int c) {
        g.setColor(colors[colorIndex]);
        g.fillRect(leftMargin + c * blockSize, topMargin + r * blockSize,
                blockSize, blockSize);

        g.setStroke(smallStroke);
        g.setColor(squareBorder);
        g.drawRect(leftMargin + c * blockSize, topMargin + r * blockSize,
                blockSize, blockSize);
    }

    void drawUI(Graphics2D g) {
        // grid background
        g.setColor(gridColor);
        g.fill(gridRect);

        // the blocks dropped in the grid
        for (int r = 0; r < nRows; r++) {
            for (int c = 0; c < nCols; c++) {
                int idx = grid[r][c];
                if (idx > EMPTY)
                    drawSquare(g, idx, r, c);
            }
        }

        // the borders of grid and preview panel
        g.setStroke(largeStroke);
        g.setColor(gridBorderColor);
        g.draw(gridRect);
        g.draw(previewRect);

        // scoreboard
        int x = scoreX;
        int y = scoreY;
        g.setColor(textColor);
        g.setFont(smallFont);
        g.drawString(format("hiscore  %6d", scoreboard.getTopscore()), x, y);
        g.drawString(format("level    %6d", scoreboard.getLevel()), x, y + 30);
        g.drawString(format("lines    %6d", scoreboard.getLines()), x, y + 60);
        g.drawString(format("score    %6d", scoreboard.getScore()), x, y + 90);

        // preview
        int minX = 5, minY = 5, maxX = 0, maxY = 0;
        for (int[] p : nextShape.pos) {
            minX = min(minX, p[0]);
            minY = min(minY, p[1]);
            maxX = max(maxX, p[0]);
            maxY = max(maxY, p[1]);
        }
        double cx = previewCenterX - ((minX + maxX + 1) / 2.0 * blockSize);
        double cy = previewCenterY - ((minY + maxY + 1) / 2.0 * blockSize);

        g.translate(cx, cy);
        for (int[] p : nextShape.shape)
            drawSquare(g, nextShape.ordinal(), p[1], p[0]);
        g.translate(-cx, -cy);
    }

    void drawFallingShape(Graphics2D g) {
        int idx = fallingShape.ordinal();
        for (int[] p : fallingShape.pos)
            drawSquare(g, idx, fallingShapeRow + p[1], fallingShapeCol + p[0]);
    }

    @Override
    public void paintComponent(Graphics gg) {
        super.paintComponent(gg);
        Graphics2D g = (Graphics2D) gg;
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                RenderingHints.VALUE_ANTIALIAS_ON);

        drawUI(g);

        if (scoreboard.isGameOver()) {
            drawStartScreen(g);
        } else {
            drawFallingShape(g);
        }
    }

    boolean canRotate(Shape s) {
        if (s == Shape.Square)
            return false;

        int[][] pos = new int[4][2];
        for (int i = 0; i < pos.length; i++) {
            pos[i] = s.pos[i].clone();
        }

        for (int[] row : pos) {
            int tmp = row[0];
            row[0] = row[1];
            row[1] = -tmp;
        }

        for (int[] p : pos) {
            int newCol = fallingShapeCol + p[0];
            int newRow = fallingShapeRow + p[1];
            if (grid[newRow][newCol] != EMPTY) {
                return false;
            }
        }
        return true;
    }

    void rotate(Shape s) {
        if (s == Shape.Square)
            return;

        for (int[] row : s.pos) {
            int tmp = row[0];
            row[0] = row[1];
            row[1] = -tmp;
        }
    }

    void move(Dir dir) {
        fallingShapeRow += dir.y;
        fallingShapeCol += dir.x;
    }

    boolean canMove(Shape s, Dir dir) {
        for (int[] p : s.pos) {
            int newCol = fallingShapeCol + dir.x + p[0];
            int newRow = fallingShapeRow + dir.y + p[1];
            if (grid[newRow][newCol] != EMPTY)
                return false;
        }
        return true;
    }

    void shapeHasLanded() {
        addShape(fallingShape);
        if (fallingShapeRow < 2) {
            scoreboard.setGameOver();
            scoreboard.setTopscore();
            stop();
        } else {
            scoreboard.addLines(removeLines());
        }
        selectShape();
    }

    int removeLines() {
        int count = 0;
        for (int r = 0; r < nRows - 1; r++) {
            for (int c = 1; c < nCols - 1; c++) {
                if (grid[r][c] == EMPTY)
                    break;
                if (c == nCols - 2) {
                    count++;
                    removeLine(r);
                }
            }
        }
        return count;
    }

    void removeLine(int line) {
        for (int c = 0; c < nCols; c++)
            grid[line][c] = EMPTY;

        for (int c = 0; c < nCols; c++) {
            for (int r = line; r > 0; r--)
                grid[r][c] = grid[r - 1][c];
        }
    }

    void addShape(Shape s) {
        for (int[] p : s.pos)
            grid[fallingShapeRow + p[1]][fallingShapeCol + p[0]] = s.ordinal();
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {
            JFrame f = new JFrame();
            f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            f.setTitle("Tetris");
            f.setResizable(false);
            f.add(new Tetris(), BorderLayout.CENTER);
            f.pack();
            f.setLocationRelativeTo(null);
            f.setVisible(true);
        });
    }
}
package tetris;

class Scoreboard {
    static final int MAXLEVEL = 9;

    private int level;
    private int lines;
    private int score;
    private int topscore;
    private boolean gameOver = true;

    void reset() {
        setTopscore();
        level = lines = score = 0;
        gameOver = false;
    }

    void setGameOver() {
        gameOver = true;
    }

    boolean isGameOver() {
        return gameOver;
    }

    void setTopscore() {
        if (score > topscore)
            topscore = score;
    }

    int getTopscore() {
        return topscore;
    }

    int getSpeed() {

        switch (level) {
            case 0:
                return 700;
            case 1:
                return 600;
            case 2:
                return 500;
            case 3:
                return 400;
            case 4:
                return 350;
            case 5:
                return 300;
            case 6:
                return 250;
            case 7:
                return 200;
            case 8:
                return 150;
            case 9:
                return 100;
            default:
                return 100;
        }
    }

    void addScore(int sc) {
        score += sc;
    }

    void addLines(int line) {

        switch (line) {
            case 1:
                addScore(10);
                break;
            case 2:
                addScore(20);
                break;
            case 3:
                addScore(30);
                break;
            case 4:
                addScore(40);
                break;
            default:
                return;
        }

        lines += line;
        if (lines > 10)
            addLevel();
    }

    void addLevel() {
        lines %= 10;
        if (level < MAXLEVEL)
            level++;
    }

    int getLevel() {
        return level;
    }

    int getLines() {
        return lines;
    }

    int getScore() {
        return score;
    }
}
package tetris;

enum Shape {
    ZShape(new int[][]{{0, -1}, {0, 0}, {-1, 0}, {-1, 1}}),
    SShape(new int[][]{{0, -1}, {0, 0}, {1, 0}, {1, 1}}),
    IShape(new int[][]{{0, -1}, {0, 0}, {0, 1}, {0, 2}}),
    TShape(new int[][]{{-1, 0}, {0, 0}, {1, 0}, {0, 1}}),
    Square(new int[][]{{0, 0}, {1, 0}, {0, 1}, {1, 1}}),
    LShape(new int[][]{{-1, -1}, {0, -1}, {0, 0}, {0, 1}}),
    JShape(new int[][]{{1, -1}, {0, -1}, {0, 0}, {0, 1}});

    private Shape(int[][] shape) {
        this.shape = shape;
        pos = new int[4][2];
        reset();
    }

    void reset() {
        for (int i = 0; i < pos.length; i++) {
            pos[i] = shape[i].clone();
        }
    }

    final int[][] pos, shape;
}
package tetris;

import java.awt.*;

final class Config {
    final static Color[] colors = {Color.green, Color.red, Color.blue,
        Color.pink, Color.orange, Color.cyan, Color.magenta};

    final static Font mainFont = new Font("Monospaced", Font.BOLD, 48);
    final static Font smallFont = mainFont.deriveFont(Font.BOLD, 18);

    final static Dimension dim = new Dimension(640, 640);

    final static Rectangle gridRect = new Rectangle(46, 47, 308, 517);
    final static Rectangle previewRect = new Rectangle(387, 47, 200, 200);
    final static Rectangle titleRect = new Rectangle(100, 85, 252, 100);
    final static Rectangle clickRect = new Rectangle(50, 375, 252, 40);

    final static int blockSize = 30;
    final static int nRows = 18;
    final static int nCols = 12;
    final static int topMargin = 50;
    final static int leftMargin = 20;
    final static int scoreX = 400;
    final static int scoreY = 330;
    final static int titleX = 130;
    final static int titleY = 150;
    final static int clickX = 120;
    final static int clickY = 400;
    final static int previewCenterX = 467;
    final static int previewCenterY = 97;

    final static Stroke largeStroke = new BasicStroke(5);
    final static Stroke smallStroke = new BasicStroke(2);

    final static Color squareBorder = Color.white;
    final static Color titlebgColor = Color.white;
    final static Color textColor = Color.black;
    final static Color bgColor = new Color(0xDDEEFF);
    final static Color gridColor = new Color(0xBECFEA);
    final static Color gridBorderColor = new Color(0x7788AA);
}

Эффекты страницы игры следующие:

Логика самой игры Тетрис:

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

Давайте сначала рассмотрим вопрос о данных. Для интерфейса необходим двумерный массив типа int, который содержит те места, которые должны быть раскрашены, а какие нет; тогда сами квадраты, хотя их формы неоднородны, они могут быть окружены квадратом масштаба 4X4. Следовательно, 16 байтов могут сохранить информацию о блоке,

Примечание. Фактически, данные квадрата также могут быть представлены массивом int, но когда дело доходит до эффективности, побитовые операции выполняются быстрее, чем обычные арифметические операции.

Затем подумайте о следующих конкретных действиях:

(1) Рождение куба. Его рождение требует принципа случайности.Кроме того, как он инициализируется для размещения в верхней части игрового интерфейса?

(2) Квадрат должен падать автоматически.В процессе падения он должен оценить, конфликтует ли он с окружающей средой и может ли он продолжать падать.

(3) Сам блок также может быть деформирован, и деформированный блок имеет другие данные, и метод оценки будет другим. (4) Когда пользователь продолжает нажимать клавишу s, квадрат должен продолжать падать.

Затем идет процесс. Игроки в основном действуют в следующих аспектах:

(1) Работа влево и вправо. Вам нужно прослушать KeyEvent и позволить квадрату перемещаться влево и вправо, пока не достигнет границы.

(2) Деформационная операция. Также послушайте KeyEvent, чтобы блок автоматически деформировался.

(3) Операция опускания. Также послушайте KeyEvent, чтобы ящик упал быстро.

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

Исходный код выглядит следующим образом:Детали аннотации

package tetris;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.GridLayout;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.JTextField;

public class Main extends JFrame implements KeyListener {
	private JTextArea [] [] grids; // Превращаем весь интерфейс в текстовую область, и вся игра воспроизводится внутри
	 private int data [] []; // Для данных каждой сетки 1 означает квадрат, 0 означает пустую область
	 private int [] allRect; // Все типы блоков хранятся в 16 байтах, а графика Тетриса - в сетке 4 * 4
	 private int rect; // Тип блока, на который падает текущая игра;
	 private int x, y; // Координатная позиция текущего квадрата, x представляет строку, y представляет столбец
	 private int score = 0; // Записываем текущий счет игры, по 10 очков для каждого уровня
	 private JLabel label; // Отобразить метку оценки
	 private JLabel label1; // Показываем, окончена ли игра
	 private boolean running; // Используется, чтобы определить, окончена ли игра
	 / * Конструктор без параметров * /
	public Main() {
		 grids = new JTextArea [26] [12]; // Устанавливаем строку и столбец игровой области
		 data = new int [26] [12]; // Открытие пространства массива данных согласуется со строками и столбцами игровой области
		allRect = new int[] { 0x00cc, 0x8888, 0x000f, 0x0c44, 0x002e, 0x088c, 0x00e8, 0x0c88, 0x00e2, 0x044c, 0x008e,
				 0x08c4, 0x006c, 0x04c8, 0x00c6, 0x08c8, 0x004e, 0x04c4, 0x00e4}; // 19 видов квадратных форм, например, 0x00cc равно 0000, что означает квадрат 2 * 2
																										//0000 
																										//1100 
																										//1100
		 label = new JLabel ("score: 0"); // Эта метка хранит счет, инициализированный 0 баллами
		 label1 = new JLabel ("Начать игру"); // Этот ярлык напоминает о состоянии игры: начало или конец
		 running = false; // это флаговая переменная, false означает, что игра окончена, true означает, что игра продолжается
		 init (); // Инициализируем игровой интерфейс
	}
	 / * Функция инициализации игрового интерфейса * /
	public void init() {
		 JPanel center = new JPanel (); // Эта панель является основной областью игры
		 JPanel right = new JPanel (); // Эта панель является областью описания игры
		 center.setLayout (new GridLayout (26, 12, 1, 1)); // делим строки и столбцы на основную область игры, всего 26 строк и 12 столбцов
		 for (int i = 0; i <grids.length; i ++) {// инициализируем панель
			for (int j = 0; j < grids[i].length; j++) {
				grids[i][j] = new JTextArea(20, 20);
				grids[i][j].setBackground(Color.WHITE);
				 grids [i] [j] .addKeyListener (this); // Добавляем событие прослушивания клавиатуры
				 // Инициализируем границу игры
				if (j == 0 || j == grids[i].length - 1 || i == grids.length - 1) {
					grids[i][j].setBackground(Color.PINK);
					data[i][j] = 1;
				}
				 grids [i] [j] .setEditable (false); // Текстовая область не редактируется
				 center.add (grids [i] [j]); // Добавляем текстовую область на главную панель
			}
		}
		 // Инициализируем панель описания игры
		right.setLayout(new GridLayout(4, 1));
		right.add(new JLabel(" a : left        d : right"));
		right.add(new JLabel(" s : down   w : change"));
		right.add(label);
		 label1.setForeground (Color.RED); // Установите для содержимого метки красный шрифт
		right.add(label1);
		 // Добавляем в форму основную панель и панель описания
		this.setLayout(new BorderLayout());
		this.add(center, BorderLayout.CENTER);
		this.add(right, BorderLayout.EAST);
		 running = true; // Инициализируем текущее состояние на true, что означает, что программа запущена и игра запускается
		 this.setSize (600, 850); // Устанавливаем размер окна
		 this.setVisible (true); // Форма видна
		 this.setLocationRelativeTo (null); // Устанавливаем центр формы
		 this.setResizable (false); // Размер формы изменить нельзя
		 this.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE); // Освободить форму
	}
	 /*Основная функция*/
	public static void main(String[] args) {
		 Main m = new Main (); // Создаем объект Main, в основном используемый для инициализации данных
		 m.go (); // Запускаем игру
	}
	 /*Начать игру*/
	 public void go () {// запускаем игру
		 while (true) {// Игра запускается до тех пор, пока игра не завершится ошибкой и не закончится, иначе она была выполнена
			 if (running == false) {// Если игра не удалась
				break;
			}
			 ranRect (); // Рисуем падающую форму сетки
			 start (); // запускаем игру
		}
		 label1.setText ("Игра окончена!"); // Игра окончена
	}
	 / * Рисуем падающую сетку * /
	public void ranRect() {
		 rect = allRect [(int) (Math.random () * 19)]; // Произвольно генерируем типы блоков (всего 7 типов, 19 форм)
	}
	 / * Функция запуска игры * /
	public void start() {
		x = 0;
		 y = 5; // Инициализируем положение падающего квадрата
		for (int i = 0; i <26; i ++) {// Всего 26 слоев, падающих один за другим
			try {
				 Thread.sleep (1000); // Задержка на 1 секунду на слой
				 if (canFall (x, y) == false) {// Если его нельзя отбросить
					 saveData (x, y); // Помечаем эту квадратную область data [] [] как 1, что указывает на наличие данных
					 for (int k = x; k <x + 4; k ++) {// Пройдите по 4 слоям, чтобы увидеть, есть ли квадраты в каждом слое, чтобы удалить этот ряд квадратов и подсчитать результат
						int sum = 0;
						for (int j = 1; j <= 10; j++) {
							if (data[k][j] == 1) {
								sum++;
							}
						}
						 if (sum == 10) {// Если в слое k есть блоки, удалите блоки в слое k
							removeRow(k);
						}
					}
					 for (int j = 1; j <= 10; j ++) {// 4 верхних слоя игры не могут иметь квадратов, иначе игра завершится ошибкой
						if (data[3][j] == 1) {
							running = false;
							break;
						}
					}
					break;
				}
				 // если его можно отбросить
				 x ++; // слой плюс один
				 fall (x, y); // Падаем на один слой
			} catch (InterruptedException e) {
				e.printStackTrace();
			}

		}
	}
	 / * Определяем, может ли падающий блок упасть * /
	public boolean canFall(int m, int n) {
		 int temp = 0x8000; // означает 1000 0000 0000 0000
		 for (int i = 0; i <4; i ++) {// Проходим через 16 квадратов (4 * 4)
			for (int j = 0; j < 4; j++) {
				 if ((temp & rect)! = 0) {// Когда здесь квадрат
					 if (data [m + 1] [n] == 1) // Если на следующем месте стоит квадрат, сразу вернуть false
						return false;
				}
				 n ++; // Столбец плюс один
				temp >>= 1;
			}
			 m ++; // Следующая строка
			 n = n-4; // Вернуться к первому столбцу
		}
		 return true; // можно отбросить, чтобы вернуть true
	}
	 / * Сохраняем соответствующие данные неубывающего блока как 1, указывая, что в этой координате есть блок * /
	public void saveData(int m, int n) {
		 int temp = 0x8000; // означает 1000 0000 0000 0000
		 for (int i = 0; i <4; i ++) {// Проходим через 16 квадратов (4 * 4)
			for (int j = 0; j < 4; j++) {
				 if ((temp & rect)! = 0) {// Когда здесь квадрат
					 data [m] [n] = 1; // массив данных хранится как 1
				}
				 n ++; // Следующий столбец
				temp >>= 1;
			}
			 m ++; // Следующая строка
			 n = n-4; // Вернуться к первому столбцу
		}
	}
	 / * Удаляем все квадраты в ряду рядов, и вышеперечисленные будут спускаться по очереди * /
	public void removeRow(int row) {
		for (int i = row; i >= 1; i--) {
			for (int j = 1; j <= 10; j++) {
				data[i][j] = data[i - 1][j];//
			}
		}
		 reflesh (); // Обновляем область главной панели игры после удаления блока строки
		 score + = 10; // Оценка плюс 10;
		 label.setText ("score:" + score); // Показать счет
	}
	 / * Обновляем область главной панели игры после удаления блока строки * /
	public void reflesh() {
		for (int i = 1; i < 25; i++) {
			for (int j = 1; j < 11; j++) {
				 if (data [i] [j] == 1) {// Сделать квадрат зеленым там, где есть квадрат
					grids[i][j].setBackground(Color.GREEN);
				 } else {// Устанавливаем белый квадрат там, где нет квадрата
					grids[i][j].setBackground(Color.WHITE);
				}
			}
		}
	}
	 / * Блок отбрасывает слой * /
	public void fall(int m, int n) {
		 if (m> 0) // когда блок падает на один уровень
			 clear (m-1, n); // Очищаем цветные квадраты на предыдущем слое
		 draw (m, n); // перерисовываем квадратное изображение
	}
	 / * Очищаем цветные области до падения блока * /
	public void clear(int m, int n) {
		 int temp = 0x8000; // означает 1000 0000 0000 0000
		 for (int i = 0; i <4; i ++) {// Проходим через 16 квадратов (4 * 4)
			for (int j = 0; j < 4; j++) {
				 if ((temp & rect)! = 0) {// Когда здесь квадрат
					 grids [m] [n] .setBackground (Color.WHITE); // Очищаем цвет и превращаем его в белый
				}
				 n ++; // Следующий столбец
				temp >>= 1;
			}
			 m ++; // Следующая строка
			 n = n-4; // Вернуться к первому столбцу
		}
	}
	 / * Рисуем изображение обратного блока * /
	public void draw(int m, int n) {
		 int temp = 0x8000; // означает 1000 0000 0000 0000
		 for (int i = 0; i <4; i ++) {// Проходим через 16 квадратов (4 * 4)
			for (int j = 0; j < 4; j++) {
				 if ((temp & rect)! = 0) {// Когда здесь квадрат
					 grids [m] [n] .setBackground (Color.GREEN); // Место с квадратами становится зеленым
				}
				 n ++; // Следующий столбец
				temp >>= 1;
			}
			 m ++; // Следующая строка
			 n = n-4; // Вернуться к первому столбцу
		}
	}

	@Override
	public void keyPressed(KeyEvent e) {
	}

	@Override
	public void keyReleased(KeyEvent e) {
	}

	@Override
	public void keyTyped(KeyEvent e) {
		 if (e.getKeyChar () == 'a') {// Перемещаем квадрат влево
			if (running == false) {
				return;
			}
			 if (y <= 1) // Когда попадает в левую стену
				return;
			 int temp = 0x8000; // означает 1000 0000 0000 0000
			for (int i = x; i <x + 4; i ++) {// Проходим через 16 квадратов (4 * 4)
				for (int j = y; j < y + 4; j++) {
					 if ((rect & temp)! = 0) {// Когда здесь квадрат
						 if (data [i] [j-1] == 1) {// Если в левом блоке
							return;
						}
					}
					temp >>= 1;
				}
			}
			 clear (x, y); // Когда вы можете двигаться влево, очистите цвет квадрата перед перемещением влево
			y--;
			 draw (x, y); // Затем перерисовываем изображение квадрата после сдвига влево
		}
		 if (e.getKeyChar () == 'd') {// Квадрат перемещается вправо
			if (running == false) {
				return;
			}
			int temp = 0x8000;
			int m = x, n = y;
			int num = 7;
			for (int i = 0; i < 4; i++) {
				for (int j = 0; j < 4; j++) {
					if ((temp & rect) != 0) {
						if (n > num) {
							num = n;
						}
					}
					temp >>= 1;
					n++;
				}
				m++;
				n = n - 4;
			}
			if (num >= 10) {
				return;
			}
			temp = 0x8000;
			for (int i = x; i < x + 4; i++) {
				for (int j = y; j < y + 4; j++) {
					if ((rect & temp) != 0) {
						if (data[i][j + 1] == 1) {
							return;
						}
					}
					temp >>= 1;
				}
			}
			 clear (x, y); // Когда вы можете двигаться вправо, очистите цвет квадрата перед перемещением вправо
			y++;
			 draw (x, y); // Затем перерисовываем изображение квадрата после перемещения вправо
		}
		 if (e.getKeyChar () == 's') {// Блок перемещается вниз
			if (running == false) {
				return;
			}
			if (canFall(x, y) == false) {
				saveData(x, y);
				return;
			}
			 clear (x, y); // Когда вы можете двигаться вниз, очистите цвет квадрата перед перемещением вниз
			x++;
			 draw (x, y); // Затем перерисовываем изображение квадрата после движения вниз
		}
		 if (e.getKeyChar () == 'w') {// Изменение формы поля
			if (running == false) {
				return;
			}
			int i = 0;
			 for (i = 0; i <allRect.length; i ++) {// Перебираем 19 квадратных фигур
				 if (allRect [i] == rect) // Находим форму, соответствующую падающему квадрату, а затем меняем форму
					break;
			}
			 if (i == 0) // квадратный блок без изменения формы, это блок типа 1
				return;
			clear(x, y);
			 if (i == 1 || i == 2) {// тип блочной графики 2
				rect = allRect[i == 1 ? 2 : 1];
				if (y > 7)
					y = 7;
			}
			 if (i> = 3 && i <= 6) {// тип графического блока 3
				rect = allRect[i + 1 > 6 ? 3 : i + 1];
			}
			 if (i> = 7 && i <= 10) {// тип графики блока 4
				rect = allRect[i + 1 > 10 ? 7 : i + 1];
			}
			 if (i == 11 || i == 12) {// тип блочной графики 5
				rect = allRect[i == 11 ? 12 : 11];
			}
			 if (i == 13 || i == 14) {// тип блочной графики 6
				rect = allRect[i == 13 ? 14 : 13];
			}
			 if (i> = 15 && i <= 18) {// тип графического блока 7
				rect = allRect[i + 1 > 18 ? 15 : i + 1];
			}
			draw(x, y);
		}
	}
}
Задание 17
Я тут немножечко подправил код.
Но в целом - отлично.
Запускаем и наслаждаемся тетрисом.
P.S.
Не забудь отрегулировать высоту консоли



Задание 16
Напиши свою реализацию методов left(), right(), up(), down() в классе Figure.
Подумай, что должны делать эти методы?



Задание 15
Напиши реализацию метода step в классе Tetris.
В методе надо переместить фигурку вниз на один шаг.
Если после перемещения положить фигурку на текущее место невозможно, то:
а) поднять ее обратно (up)
б) "приземлить" ее (landed)
в) удалить все "полные линии" в объекте field
г) создать новую фигурку взамен старой.



Задание 14
Напиши реализацию метода removeFullLines в классе Field
Надо
а) удалить все строки из матрицы, которые полностью заполнены (состоят из одних единиц)
б) сместить оставшиеся строки вниз
в) создать новые строки взамен отсутствующих.

ВАЖНО!
matrix[y][x] содержит элемент с координатами (x,y)
matrix[i] содержит i-ю строку
а) Мы можем удалить стоку:
matrix[i] = null

б)Скопировать [ссылку на] строку:
matrix[i+1] = matrix[i];

в)Создать новую строку:
matrix[i] = new int[width];



Задание 13
Теперь приступим к реализации созданных методов.
Напиши реализацию метода print в классе Field
а) Метод должен выводить на экран прямоугольник.
б) Высота прямоугольника равна height, ширина - width
в) Если данная клетка пустая - вывести точку, если не пустая - английский X

Подсказка:
if (matrix[y][x]==0) ...



Задание 12
В тетрисе мы управляем движением фигурки с помощью клавиатуры.
Тут есть 4 действия:
движение влево (кнопка влево)
движение вправо (кнопка влево)
поворот фигурки (цифра 5 на доп.клавиатуре справа)
падение вниз (пробел)

Мы будем обрабатывать ввод с клавиатуры в методе run() класса Tetris.

И тут у меня для тебя две новости: хорошая и плохая
Плохая новость состоит в том, что java не позволяет считать нажатые символы с клавиатуры,
пока пользователь не нажмет enter.
Не очень удобно, правда?

Хорошая новость состоит в том, я написал специальный класс (KeyboardObserver), который позволяет обойти это ограничение.
Так что ты можешь воспользоваться им.

Есть еще и отличная новость.
Ты до сих пор отлично справлялся, поэтому я помогу тебе немного.
Я написал за тебя методы:
а) createRandomFigure в FigureFactory
б) run в Tetris

Изучи их внимательно и переходи дальше.



Задание 11
Теперь создай класс FigureFactory.
С его помощью мы будем создавать фигуры различных форм.
Пока он будет содержать только один статический метод createRandomFigure:
public static Figure createRandomFigure(int x,int y)



Задание 10
Так же нам понадобятся методы для управления фигуркой.
Добавь в класс Figure методы:
left() - для движения фигурки влево.
right() - для движения фигурки вправо.
down() - для движения фигурки вниз.
up() - для движения фигурки вверх.
downMaximum() - падение фигурки в низ до дна.
rotate() - для поворота фигурки вокруг главной диагонали.
boolean isCurrentPositionAvailable() - проверка - может ли фигурка быть помещена в текущую позицию. Для теста захардкодь результат в true.
landed() - вызывается, когда фигурка достигла дна или уперлась в другую фигурку
Все ее занятые клетки теперь должны добавиться в Field.



Задание 9
Если ты обратил внимание, мы пишем программу "сверху вниз".
Сначала решили, какие классы нам нужны. Затем - какие методы.
А потом уже начнем писать код этих методов.
Таким образом мы разбиваем большую задачу на множество маленьких.
Когда код всех методов будет готов, останется только проверить - так ли все работает, как должно быть.
И если надо - внести некоторые изменения.



Задание 8
Теперь создадим костяк класса Figure.
Этот класс будет описывать падающую фигурку.

Нам понадобятся ее координаты и форма.
За координаты будут отвечать две переменные x и y.
За форму - матрица. Двумерный массив 3x3, состоящий из единиц и нулей.
Единицей мы обозначаем что клетка есть, нулем - что она пустая.

Добавь в класс Figure два поля поля: x типа int, y типа int.
Еще добавь двумерный массив: matrix(матрица) типа int[][].
Там же добавь getter'ы для созданных переменных.
Добавь конструктор с тремя параметрами x, y, matrix.



Задание 7
Нам понадобится еще 4 метода в классе Field:
а) print() - объект будет отрисовывать на экран свое текущее состояние;
б) removeFullLines() - будет удалять из матрицы полностью заполненные строки и сдвигать вышележащие строки вниз;
в) Integer getValue(int x, int y) - возвращает значение которое находится в матрице с координатами x и y;
г) void setValue(int x, int y, int value) - устанавливает переданное значение в ячейку массива (матрицы) с координатами x, y.



Задание 6
Теперь перейдем к классу Field.
Он будет отвечать за хранение данных о текущих занятых и свободных клетках на поле игры.
Добавь в класс Field два поля поля: width (ширина) типа int, height(высота) типа int.
Так же нам понадобится матрица - двумерный массив: matrix(матрица) типа int[][];
Там же добавь getter'ы для созданных переменных.
Добавь конструктор с двумя параметрами width и height. И не забудь про матрицу.

ВАЖНО!
Двумерный массив можно представить как массив массивов или как прямоугольную матрицу.
При этом первой координатой в массиве у нас будет номер строки, а второй - столбца.
Другими словами ячейка с координатами x, y - это matrix[y][x].



Задание 5
Теперь нужно создать объект самого Тетриса.
Добавь в класс Tetris статическую переменную game. (тип - Tetris, видимость - public)

Затем в методе main создай объект типа Тетрис и сохрани его в эту переменную.
Затем добавь вызов метода run.

Должно получиться что-то типа такого:
game = new Tetris();
game.run();



Задание 4
Так же еще нам понадобится пара методов.
Добавь в класс Tetris методы run() и step():

run() будет отвечать за всю игру.
А step() - за один шаг в игре.



Задание 3
Отлично! Теперь добавь в Tetris две переменные: field типа Field  и figure типа Figure.
С их помощью мы будем хранить информацию о текущей фигурке и о состоянии дел на "поле клеток".
Так же добавь getter'ы для созданных переменных.



Задание 2
А еще нам понадобится метод main. Как же без него.
Добавь метод main в класс Tetris



Задание 1
Давай напишем игру Тетрис.
Наш Тетрис будет состоять из двух вещей: поля из клеток и фигурки, которая падает.
Поэтому для начала создай три класса: Field(поле с клетками), Figure(фигура) и сам Tetris.

package com.javarush.test.level22.lesson18.big01;

import java.awt.event.KeyEvent;

/**
 * Класс Tetris - содержит основной функционал игры.
 */
public class Tetris {

    public static Tetris game;
    private Field field;                //Поле с клетками
    private Figure figure;              //Фигурка
    private boolean isGameOver;         //Игра Окончена?

    public Tetris(int width, int height) {
        field = new Field(width, height);
        figure = null;
    }

    public static void main(String[] args) throws Exception {
        game = new Tetris(10, 20);
        game.run();
    }

    /**
     * Геттер переменной field.
     */
    public Field getField() {
        return field;
    }

    /**
     * Сеттер для field
     */
    public void setField(Field field) {
        this.field = field;
    }

    /**
     * Геттер переменной figure.
     */
    public Figure getFigure() {
        return figure;
    }

    /**
     * Сеттер для figure
     */
    public void setFigure(Figure figure) {
        this.figure = figure;
    }

    /**
     * Основной цикл программы.
     * Тут происходят все важные действия
     */
    public void run() throws Exception {
        //Создаем объект "наблюдатель за клавиатурой" и стартуем его.
        KeyboardObserver keyboardObserver = new KeyboardObserver();
        keyboardObserver.start();

        //выставляем начальное значение переменной "игра окончена" в ЛОЖЬ
        isGameOver = false;
        //создаем первую фигурку посередине сверху: x - половина ширины, y - 0.
        figure = FigureFactory.createRandomFigure(field.getWidth() / 2, 0);

        //пока игра не окончена
        while (!isGameOver) {
            //"наблюдатель" содержит события о нажатии клавиш?
            if (keyboardObserver.hasKeyEvents()) {
                //получить самое первое событие из очереди
                KeyEvent event = keyboardObserver.getEventFromTop();
                //Если равно символу 'q' - выйти из игры.
                if (event.getKeyChar() == 'q') return;
                //Если "стрелка влево" - сдвинуть фигурку влево
                if (event.getKeyCode() == KeyEvent.VK_LEFT)
                    figure.left();
                    //Если "стрелка вправо" - сдвинуть фигурку вправо
                else if (event.getKeyCode() == KeyEvent.VK_RIGHT)
                    figure.right();
                    //Если  код клавиши равен 12 ("цифра 5 на доп. клавиатуре") - повернуть фигурку
                else if (event.getKeyCode() == 12)
                    figure.rotate();
                    //Если "пробел" - фигурка падает вниз на максимум
                else if (event.getKeyCode() == KeyEvent.VK_SPACE)
                    figure.downMaximum();
            }

            step();             //делаем очередной шаг
            field.print();      //печатаем состояние "поля"
            Thread.sleep(300);  //пауза 300 миллисекунд - 1/3 секунды
        }

        //Выводим сообщение "Game Over"
        System.out.println("Game Over");
    }

    public void step() {
        //опускам фигурку вниз
        figure.down();

        //если разместить фигурку на текущем месте невозможно
        if (!figure.isCurrentPositionAvailable()) {
            figure.up();                    //поднимаем обратно
            figure.landed();                //приземляем

            isGameOver = figure.getY() <= 1;//если фигурка приземлилась на самом верху - игра окончена

            field.removeFullLines();        //удаляем заполненные линии

            figure = FigureFactory.createRandomFigure(field.getWidth() / 2, 0); //создаем новую фигурку
        }
    }
}
package com.javarush.test.level22.lesson18.big01;

import javax.swing.*;
import java.awt.*;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.util.Queue;
import java.util.concurrent.ArrayBlockingQueue;

public class KeyboardObserver extends Thread {
    private Queue<KeyEvent> keyEvents = new ArrayBlockingQueue<KeyEvent>(100);

    private JFrame frame;

    @Override
    public void run() {
        frame = new JFrame("KeyPress Tester");
        frame.setTitle("Transparent JFrame Demo");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        frame.setUndecorated(true);
        frame.setSize(400, 400);
        frame.setExtendedState(JFrame.MAXIMIZED_BOTH);
        frame.setLayout(new GridBagLayout());

        frame.setOpacity(0.0f);
        frame.setVisible(true);

        frame.addFocusListener(new FocusListener() {
            @Override
            public void focusGained(FocusEvent e) {
                //do nothing
            }

            @Override
            public void focusLost(FocusEvent e) {
                System.exit(0);
            }
        });


        frame.addKeyListener(new KeyListener() {

            public void keyTyped(KeyEvent e) {
                //do nothing
            }

            public void keyReleased(KeyEvent e) {
                //do nothing
            }

            public void keyPressed(KeyEvent e) {
                keyEvents.add(e);
            }
        });
    }


    public boolean hasKeyEvents() {
        return !keyEvents.isEmpty();
    }

    public KeyEvent getEventFromTop() {
        return keyEvents.poll();
    }
}
package com.javarush.test.level22.lesson18.big01;


/**
 * Класс Figure описывает фигурку тетриса
 */
public class Figure {
    //метрица которая определяет форму фигурки: 1 - клетка не пустая, 0 - пустая
    private int[][] matrix;
    //координаты
    private int x;
    private int y;

    public Figure(int x, int y, int[][] matrix) {
        this.x = x;
        this.y = y;
        this.matrix = matrix;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int[][] getMatrix() {
        return matrix;
    }

    /**
     * Поворачаиваем фигурку.
     * Для простоты - просто вокруг главной диагонали.
     */
    public void rotate() {
        int[][] matrix2 = new int[3][3];

        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                matrix2[i][j] = matrix[j][i];
            }
        }

        matrix = matrix2;
    }

    /**
     * Двигаем фигурку влево.
     * Проверяем не вылезла ли она за границу поля и/или не залезла ли на занятые клетки.
     */
    public void left() {
        x--;
        if (!isCurrentPositionAvailable())
            x++;
    }

    /**
     * Двигаем фигурку вправо.
     * Проверяем не вылезла ли она за границу поля и/или не залезла ли на занятые клетки.
     */
    public void right() {
        x++;
        if (!isCurrentPositionAvailable())
            x--;
    }

    /**
     * Двигаем фигурку вверх.
     * Используется, если фигурка залезла на занятые клетки.
     */
    public void up() {
        y--;
    }

    /**
     * Двигаем фигурку вниз.
     */
    public void down() {
        y++;
    }

    /**
     * Двигаем фигурку вниз до тех пор, пока не залезем на кого-нибудь.
     */
    public void downMaximum() {
        while (isCurrentPositionAvailable()) {
            y++;
        }

        y--;
    }

    /**
     * Проверяем - может ли фигурка находится на текущей позици:
     * а) не вылазиет ли она за границы поля
     * б) не залазиет ли она на занятые клетки
     */
    public boolean isCurrentPositionAvailable() {
        Field field = Tetris.game.getField();

        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (matrix[i][j] == 1) {
                    if (y + i >= field.getHeight())
                        return false;

                    Integer value = field.getValue(x + j, y + i);
                    if (value == null || value == 1)
                        return false;
                }
            }
        }

        return true;
    }

    /**
     * Приземляем фигурку - добавляем все ее непустые клетки к клеткам поля.
     */
    public void landed() {
        Field field = Tetris.game.getField();

        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (matrix[i][j] == 1)
                    field.setValue(x + j, y + i, 1);
            }
        }
    }
}
package com.javarush.test.level22.lesson18.big01;

/**
 * Клсс FigureFactory отвечает за создание объектов-фигурок.
 */
public class FigureFactory {
    /**
     * Набор из шести шаблонов для фигурок
     */
    public static final int[][][] BRICKS = {{
            {1, 1, 0},                          //   X X
            {0, 1, 1},                          //     X X
            {0, 0, 0}}, {                       //

            {1, 0, 0},                          //   X
            {1, 1, 0},                          //   X X
            {0, 1, 0}}, {                       //     X

            {0, 1, 0},                          //   X
            {0, 1, 0},                          //   X
            {0, 0, 0}}, {                       //   X

            {1, 1, 0},                          //   X X
            {1, 1, 0},                          //   X X
            {0, 0, 0}}, {                       //

            {1, 1, 1},                          //   X X X
            {0, 1, 0},                          //     X
            {0, 0, 0}}, {                       //

            {1, 1, 1},                          //   X X X
            {1, 1, 1},                          //   X X X
            {0, 0, 0}}                          //
    };

    /**
     * Метод выбирает случайный шаблон и создает с ним новую фигурку.
     */
    public static Figure createRandomFigure(int x, int y) {
        int index = (int) (Math.random() * 6);
        return new Figure(x, y, BRICKS[index]);
    }
}
package com.javarush.test.level22.lesson18.big01;

import java.util.ArrayList;

/**
 * Класс Field описывает "поле клеток" игры Тетрис
 */
public class Field {
    //ширина и высота
    private int width;
    private int height;

    //матрица поля: 1 - клетка занята, 0 - свободна
    private int[][] matrix;

    public Field(int width, int height) {
        this.width = width;
        this.height = height;
        matrix = new int[height][width];
    }

    public int getWidth() {
        return width;
    }

    public int getHeight() {
        return height;
    }

    public int[][] getMatrix() {
        return matrix;
    }

    /**
     * Метод возвращает значение, которое содержится в матрице с координатами (x,y)
     * Если координаты за пределами матрицы, метод возвращает null.
     */
    public Integer getValue(int x, int y) {
        if (x >= 0 && x < width && y >= 0 && y < height)
            return matrix[y][x];

        return null;
    }

    /**
     * Метод устанавливает переданное значение(value) в ячейку матрицы с координатами (x,y)
     */
    public void setValue(int x, int y, int value) {
        if (x >= 0 && x < width && y >= 0 && y < height)
            matrix[y][x] = value;
    }

    /**
     * Метод печатает на экран текущее содержание матрицы
     */
    public void print() {
        //Создаем массив, куда будем "рисовать" текущее состояние игры
        int[][] canvas = new int[height][width];

        //Копируем "матрицу поля" в массив
        for (int i = 0; i < height; i++) {
            for (int j = 0; j < width; j++) {
                canvas[i][j] = matrix[i][j];
            }
        }

        //Копируем фигурку в массив, только непустые клетки
        int left = Tetris.game.getFigure().getX();
        int top = Tetris.game.getFigure().getY();
        int[][] brickMatrix = Tetris.game.getFigure().getMatrix();

        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                if (top + i >= height || left + j >= width) continue;
                if (brickMatrix[i][j] == 1)
                    canvas[top + i][left + j] = 2;
            }
        }


        //Выводим "нарисованное" на экран, но начинаем с "границы кадра".
        System.out.println("---------------------------------------------------------------------------n");

        for (int i = 0; i < height; i++) {
            for (int j = 0; j < width; j++) {
                int index = canvas[i][j];
                if (index == 0)
                    System.out.print(" . ");
                else if (index == 1)
                    System.out.print(" X ");
                else if (index == 2)
                    System.out.print(" X ");
                else
                    System.out.print("???");
            }
            System.out.println();
        }


        System.out.println();
        System.out.println();
    }

    /**
     * Удаляем заполненные линии
     */
    public void removeFullLines() {
        //Создаем список для хранения линий
        ArrayList<int[]> lines = new ArrayList<int[]>();

        //Копируем все непустые линии в список.
        for (int i = 0; i < height; i++) {
            //подсчитываем количество единиц в строке - просто суммируем все ее значения
            int count = 0;
            for (int j = 0; j < width; j++) {
                count += matrix[i][j];
            }

            //Если сумма строки не равно ее ширине - добавляем в список
            if (count != width)
                lines.add(matrix[i]);
        }

        //Добавляем недостающие строки в начало списка.
        while (lines.size() < height) {
            lines.add(0, new int[width]);
        }

        //Преобразуем список обратно в матрицу
        matrix = lines.toArray(new int[height][width]);
    }
}

Понравилась статья? Поделить с друзьями:
  • Как написать тесты на java
  • Как написать тестовую стратегию
  • Как написать тестовую документацию
  • Как написать тесто слоеное
  • Как написать тестбенч на верилог