Hlavní navigace

Hra Tetris naprogramovaná v Jave a Swingu

Ján Bodnár

V tomto článku si ukážeme, ako naprogramovať klon hry Tetris v Jave a toolkitu Swing. Tetris je jednou z najpopulárnejších hier. Originál hry bol vytvorený ruským programátorom Alexejom Pažitnovom v roku 1985.

Doba čtení: 15 minut

Sdílet

Swing

Swing je knižnica na tvorbu grafického rozhrania v jazyku Java. Jedná sa o mimoriadne prepacovaný nástroj, s ktorým dokážeme vytvárať profesionálne multiplatformové aplikácie. Swing obsahuje aj grafické API, s ktorými môžeme vytvárať jednoduché 2D hry.

Tetris

Tetris je puzzle hra padajúcich blokov. V hre máme sedem rôznych tvarov. Každý z týchto tvarov je tvorený štyroma štvorcami. Tieto tvary padajú dolu po hracej ploche. Našou úlohou je formovať tvary takým spôsobom, aby do seba zapadali. Ak sa nám podarí vytvoriť neporušenú líniu, línia sa zruší a pribudne nám skóre. Hra pokračuje dovtedy, pokým sa tvary nedostanú na vrch hracej plochy.

Vývoj hry Tetris

V hre nepoužívame obrázky; vlastné tvary si nakreslíme pomocou Swing API. Pre vytvorenie herného cyklu využívame Timer.


Tvary Tetrominoe

Hra je pre ľahšie pochopenie zjednodušená. Tetris štartuje okamžite, keď sa spustí aplikácia. Hru môžeme prerušiť pomocou klávesy P. Medzerník spustí tvar okamžite na spodok hracej plochy. Klávesa D spustí tvar o jeden štvorec. (Týmto spôsobom môžeme zrýchliť hru.) Hra prebieha konštantnou rýchlosťou, nie je implementované zrýchlenie. Skóre je číslo zmazaných línií; zobrazuje sa naspodku okna na tzv. statusbare.

Zdrojáky sú k dispozícii v Java-Tetris-Game repozitáry. V kóde sme použili Java switch expressions, ktoré priniesla Java 12.

Kód hry tetris je rozdelený do troch súborov: Tetris.java, Shape.java a Board.java.

// com/zetcode/Tetris.java

package com.zetcode;

import java.awt.BorderLayout;
import java.awt.EventQueue;
import javax.swing.JFrame;
import javax.swing.JLabel;

/*
Java Tetris game clone

Author: Jan Bodnar
Website: http://zetcode.com
 */
public class Tetris extends JFrame {

    private JLabel statusbar;

    public Tetris() {

        initUI();
    }

    private void initUI() {

        statusbar = new JLabel(" 0");
        add(statusbar, BorderLayout.SOUTH);

        var board = new Board(this);
        add(board);
        board.start();

        setTitle("Tetris");
        setSize(200, 400);
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLocationRelativeTo(null);
    }

    JLabel getStatusBar() {

        return statusbar;
    }

    public static void main(String[] args) {

        EventQueue.invokeLater(() -> {

            var game = new Tetris();
            game.setVisible(true);
        });
    }
}

V súbore Tetris.java vytvoríme Java aplikáciu hry Tetris. Vytvoríme board, na ktorom sa odohráva hra. Na spodok okna aplikácie pridáme statusbar, ktorý nám bude zobrazovať skóre hry.

// com/zetcode/Shape.java

package com.zetcode;

import java.util.Random;

public class Shape {

    protected enum Tetrominoe {
        NoShape, ZShape, SShape, LineShape,
        TShape, SquareShape, LShape, MirroredLShape
    }

    private Tetrominoe pieceShape;
    private int[][] coords;

    public Shape() {

        coords = new int[4][2];
        setShape(Tetrominoe.NoShape);
    }

    void setShape(Tetrominoe shape) {

        int[][][] coordsTable = new int[][][]{
                {{0, 0}, {0, 0}, {0, 0}, {0, 0}},
                {{0, -1}, {0, 0}, {-1, 0}, {-1, 1}},
                {{0, -1}, {0, 0}, {1, 0}, {1, 1}},
                {{0, -1}, {0, 0}, {0, 1}, {0, 2}},
                {{-1, 0}, {0, 0}, {1, 0}, {0, 1}},
                {{0, 0}, {1, 0}, {0, 1}, {1, 1}},
                {{-1, -1}, {0, -1}, {0, 0}, {0, 1}},
                {{1, -1}, {0, -1}, {0, 0}, {0, 1}}
        };

        for (int i = 0; i < 4; i++) {

            System.arraycopy(coordsTable[shape.ordinal()], 0, coords, 0, 4);
        }

        pieceShape = shape;
    }

    private void setX(int index, int x) {

        coords[index][0] = x;
    }

    private void setY(int index, int y) {

        coords[index][1] = y;
    }

    int x(int index) {

        return coords[index][0];
    }

    int y(int index) {

        return coords[index][1];
    }

    Tetrominoe getShape() {

        return pieceShape;
    }

    void setRandomShape() {

        var r = new Random();
        int x = Math.abs(r.nextInt()) % 7 + 1;

        Tetrominoe[] values = Tetrominoe.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;
    }


    int minY() {

        int m = coords[0][1];

        for (int i = 0; i < 4; i++) {

            m = Math.min(m, coords[i][1]);
        }

        return m;
    }

    Shape rotateLeft() {

        if (pieceShape == Tetrominoe.SquareShape) {

            return this;
        }

        var 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;
    }

    Shape rotateRight() {

        if (pieceShape == Tetrominoe.SquareShape) {

            return this;
        }

        var 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;
    }
}

V triede Shape uchovávame informácie o Tetris prvku.

protected enum Tetrominoe {
    NoShape, ZShape, SShape, LineShape,
    TShape, SquareShape, LShape, MirroredLShape
}

Enumerácia Tetrominoe obsahuje názvy siedmich padajúcich prvkov a jedného prázdneho prvku nazvaného NoShape.

public Shape() {

    coords = new int[4][2];
    setShape(Tetrominoe.NoShape);
}

V konštruktore triedy Shape sa nám vytvorí pole coords, ktoré obsahuje súradnice padajúceho prvku.

int[][][] coordsTable = new int[][][]{
        {{0, 0}, {0, 0}, {0, 0}, {0, 0}},
        {{0, -1}, {0, 0}, {-1, 0}, {-1, 1}},
        {{0, -1}, {0, 0}, {1, 0}, {1, 1}},
        {{0, -1}, {0, 0}, {0, 1}, {0, 2}},
        {{-1, 0}, {0, 0}, {1, 0}, {0, 1}},
        {{0, 0}, {1, 0}, {0, 1}, {1, 1}},
        {{-1, -1}, {0, -1}, {0, 0}, {0, 1}},
        {{1, -1}, {0, -1}, {0, 0}, {0, 1}}
};

Pole coordsTable obsahuje možné súradnice padajúcich prvkov.

for (int i = 0; i < 4; i++) {

    System.arraycopy(coordsTable[shape.ordinal()], 0, coords, 0, 4);
}

Podľa zvoleného tvaru sa skopírujú súradnice z tabuľky do poľa súradníc daného padajúceho prvku.


Súradnice rotovaného S tvaru

Pre lepšie pochopenie čo vlastne sú tie súradnice sme si vytvorili náčrt rotovaného S tvaru. Ten sa skladá z nasledovných súradníc: (-1, 1) , (-1, 0) ,(0, 0) (0, -1) .

Shape rotateLeft() {

    if (pieceShape == Tetrominoe.SquareShape) {

        return this;
    }

    var 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;
}

Metóda rotateLeft() rotuje padajúci tvar doľava. (Štvorec nemusíme rotovať.) Úpravou x-vých a y-vých súradníc sa nám tvar zrotuje doľava.

// com/zetcode/Board.java

package com.zetcode;

import com.zetcode.Shape.Tetrominoe;

import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.Timer;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;

public class Board extends JPanel {

    private final int BOARD_WIDTH = 10;
    private final int BOARD_HEIGHT = 22;
    private final int PERIOD_INTERVAL = 300;

    private Timer timer;
    private boolean isFallingFinished = false;
    private boolean isPaused = false;
    private int numLinesRemoved = 0;
    private int curX = 0;
    private int curY = 0;
    private JLabel statusbar;
    private Shape curPiece;
    private Tetrominoe[] board;

    public Board(Tetris parent) {

        initBoard(parent);
    }

    private void initBoard(Tetris parent) {


        setFocusable(true);
        statusbar = parent.getStatusBar();
        addKeyListener(new TAdapter());
    }

    private int squareWidth() {

        return (int) getSize().getWidth() / BOARD_WIDTH;
    }

    private int squareHeight() {

        return (int) getSize().getHeight() / BOARD_HEIGHT;
    }

    private Tetrominoe shapeAt(int x, int y) {

        return board[(y * BOARD_WIDTH) + x];
    }

    void start() {

        isFallingFinished = false;
        numLinesRemoved = 0;

        curPiece = new Shape();
        board = new Tetrominoe[BOARD_WIDTH * BOARD_HEIGHT];

        clearBoard();
        newPiece();

        timer = new Timer(PERIOD_INTERVAL, new GameCycle());
        timer.start();
    }

    private void pause() {

        isPaused = !isPaused;

        if (isPaused) {

            statusbar.setText("paused");
        } else {

            statusbar.setText(String.valueOf(numLinesRemoved));
        }

        repaint();
    }

    @Override
    public void paintComponent(Graphics g) {

        super.paintComponent(g);
        doDrawing(g);
    }

    private void doDrawing(Graphics g) {

        var 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++) {

                Tetrominoe shape = shapeAt(j, BOARD_HEIGHT - i - 1);

                if (shape != Tetrominoe.NoShape) {

                    drawSquare(g, j * squareWidth(),
                            boardTop + i * squareHeight(), shape);
                }
            }
        }

        if (curPiece.getShape() != Tetrominoe.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());
            }
        }
    }

    private void dropDown() {

        int newY = curY;

        while (newY > 0) {

            if (!tryMove(curPiece, curX, newY - 1)) {

                break;
            }

            newY--;
        }

        pieceDropped();
    }

    private void oneLineDown() {

        if (!tryMove(curPiece, curX, curY - 1)) {

            pieceDropped();
        }
    }

    private void clearBoard() {

        for (int i = 0; i < BOARD_HEIGHT * BOARD_WIDTH; i++) {

            board[i] = Tetrominoe.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();
        }
    }

    private void newPiece() {

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

        if (!tryMove(curPiece, curX, curY)) {

            curPiece.setShape(Tetrominoe.NoShape);
            timer.stop();

            var msg = String.format("Game over. Score: %d", numLinesRemoved);
            statusbar.setText(msg);
        }
    }

    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) != Tetrominoe.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) == Tetrominoe.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(Tetrominoe.NoShape);
        }
    }

    private void drawSquare(Graphics g, int x, int y, Tetrominoe shape) {

        Color colors[] = {new Color(0, 0, 0), new Color(204, 102, 102),
                new Color(102, 204, 102), new Color(102, 102, 204),
                new Color(204, 204, 102), new Color(204, 102, 204),
                new Color(102, 204, 204), new Color(218, 170, 0)
        };

        var color = colors[shape.ordinal()];

        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);
    }

    private class GameCycle implements ActionListener {

        @Override
        public void actionPerformed(ActionEvent e) {

            doGameCycle();
        }
    }

    private void doGameCycle() {

        update();
        repaint();
    }

    private void update() {

        if (isPaused) {

            return;
        }

        if (isFallingFinished) {

            isFallingFinished = false;
            newPiece();
        } else {

            oneLineDown();
        }
    }

    class TAdapter extends KeyAdapter {

        @Override
        public void keyPressed(KeyEvent e) {

            if (curPiece.getShape() == Tetrominoe.NoShape) {

                return;
            }

            int keycode = e.getKeyCode();

            // Java 12 switch expressions
            switch (keycode) {

                case KeyEvent.VK_P -> pause();
                case KeyEvent.VK_LEFT -> tryMove(curPiece, curX - 1, curY);
                case KeyEvent.VK_RIGHT -> tryMove(curPiece, curX + 1, curY);
                case KeyEvent.VK_DOWN -> tryMove(curPiece.rotateRight(), curX, curY);
                case KeyEvent.VK_UP -> tryMove(curPiece.rotateLeft(), curX, curY);
                case KeyEvent.VK_SPACE -> dropDown();
                case KeyEvent.VK_D -> oneLineDown();
            }
        }
    }
}

Hra sa odohráva v triede Board.

private final int BOARD_WIDTH = 10;
private final int BOARD_HEIGHT = 22;
private final int PERIOD_INTERVAL = 300;

Konštanty BOARD_WIDTH a BOARD_HEIGHT určujú veľkosť boardu. Veľkosť je stanovená v štvorčekoch, z ktorých sa tvary skladajú. Každý z tvarov je tvorený štyroma takýmito štvorcami. Konštanta PERIOD_INTERVAL definuje rýchlosť hry.

...
private boolean isFallingFinished = false;
private boolean isPaused = false;
private int numLinesRemoved = 0;
private int curX = 0;
private int curY = 0;
...

Tu inicializujeme dôležité premenné. Premenná isFallingFinished určuje, či daný Tetris prvok ukončil padanie a tak potrebujeme vytvoriť nový prvok. Premenná isPaused definuje, či je hra prerušená. Premenná numLinesRemoved definuje počet riadkov, ktoré sa nám dosiaľ podarilo vymazať. Nakoniec premenné curX a curY určujú súradnice padajúceho prvku. (Jeho horného ľavého rohu.)

private Tetrominoe[] board;

Pole Tetrominoe objektov uchováva všetky dopadnuté tvary v našej hre. Padajúci prvok je uložený vo zvláštnom objekte. Pri svojom dopade sa prvok potom pridá do poľa.

private void initBoard(Tetris parent) {

    setFocusable(true);
    statusbar = parent.getStatusBar();
    addKeyListener(new TAdapter());
}

Ak chceme prijímať vstup z klávesnice, musíme zavolať metódu setFocusable(). Z predka získame handle na statusbar a pridáme poslucháča klávesových udalostí.

private int squareWidth() {

    return (int) getSize().getWidth() / BOARD_WIDTH;
}

private int squareHeight() {

    return (int) getSize().getHeight() / BOARD_HEIGHT;
}

Keďže veľkosť okna hry môžeme zmeniť, tieto dve metódy slúžia na výpočet veľkosti štvorca, z ktorého sa Tetrominoe tvary skladajú.

private Tetrominoe shapeAt(int x, int y) {

    return board[(y * BOARD_WIDTH) + x];
}

Metóda shapteAt() nám zisťuje Tetrominoe tvar na konkrétnych x a y súradniciach. Používame ju napríklad pri mazaní spadnutých tvarov alebo pri vykresľovaní tvarov.

void start() {
...

Metóda start() spúšťa hru.

curPiece = new Shape();

Vytvoríme nový objekt Shape. Neskôr mu v metóde newPiece() priradíme kontrétny tvar. Tento objekt bude reprezentovať práve padajúci Tetrominoe tvar.

board = new Tetrominoe[BOARD_WIDTH * BOARD_HEIGHT];

Inicializujeme pole Tetrominoe objektov. Celková veľkosť poľa je daná v malých štvorčekoch, z ktorých sa tvary skladajú.

timer = new Timer(PERIOD_INTERVAL, new GameCycle());
timer.start();

Vytvoríme časovač, ktorý nám vytvorí herný cyklus. V skutočnosti na stanovenie rýchlosti hry potrebujeme zložitejší postup; kvôli zjednodušeniu však použijeme dopredu stanovenú konštantu. V prípade potreby si ju jednoducho upravíme na inú hodnotu. (Na základe komentárov sme zmenili časovač na javax.swing.Timer.)

private void pause() {

    isPaused = !isPaused;

    if (isPaused) {

        statusbar.setText("paused");
    } else {

        statusbar.setText(String.valueOf(numLinesRemoved));
    }

    repaint();
}

Metóda pause() nastaví premennú isPaused a vypíše buď hlášku paused alebo skóre hry. Ak je premenná isPaused nastavená na true, hra sa prestane aktualizovať.

@Override
public void paintComponent(Graphics g) {

    super.paintComponent(g);
    doDrawing(g);
}

V Java Swing knižnici vykresľujeme na komponentu pomocou paintComponent() metódy. Konkrétne kresliace rutiny delegujeme do metódy doDrawing().

private void doDrawing(Graphics g) {
...

Metóda doDrawing() zabezpečuje vykreslenie objektov na obrazovku. Vykreslenie sa deje v dvoch krokoch.

for (int i = 0; i < BOARD_HEIGHT; i++) {

    for (int j = 0; j < BOARD_WIDTH; j++) {

        Tetrominoe shape = shapeAt(j, BOARD_HEIGHT - i - 1);

        if (shape != Tetrominoe.NoShape) {

            drawSquare(g, j * squareWidth(),
                    boardTop + i * squareHeight(), shape);
        }
    }
}

V prvom kroku vykreslíme všetky dopadnuté prvky a ich zvyšky. Všetky prvky sú uložené v poli board. Jednotlivé časti prvkov zistíme pomocou metódy shapeAt().

if (curPiece.getShape() != Tetrominoe.NoShape) {

    for (int i = 0; i < 4; i++) {

        int x = curX + curPiece.x(i);
        int y = curY - curPiece.y(i);
        drawSquare(g, 0 + x * squareWidth(),
                   boardTop + (BOARD_HEIGHT - y - 1) * squareHeight(),
                   curPiece.getShape());
    }
}

V druhom kroku sa vykreslí aktuálne padajúci prvok. Detaily pre vykreslenie získame z objektu curPiece.

private void dropDown() {

    int newY = curY;

    while (newY > 0) {

        if (!tryMove(curPiece, curX, newY - 1)) {

            break;
        }

        newY--;
    }

    pieceDropped();
}

Kláveskou Space spustíme padajúci prvok okamžite na spodok hracej plochy. Deje sa to v metóde dropDown(). Postupujeme tak, že v cykle while spúšťame prvok o jeden riadok dolu, pokým nenarazíme na dno alebo vrchnú časť dosiaľ spadnutých prvkov. Keď padajúci prvok ukončí svoj pád, zavolá sa metóda pieceDropped().

private void oneLineDown() {

    if (!tryMove(curPiece, curX, curY - 1)) {

        pieceDropped();
    }
}

V metóde oneLineDown() sa pokúsime posunúť padajúci prvok o jeden riadok nadol. Ak už nie je kam padať, voláme metódu pieceDropped().

private void clearBoard() {

    for (int i = 0; i < BOARD_HEIGHT * BOARD_WIDTH; i++) {

        board[i] = Tetrominoe.NoShape;
    }
}

Metódou clearBoard() vymažeme hraciu plochu. Všetkým prvkom poľa board priradíme hodnotu Tetrominoe.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();
    }
}

Keď sa ukončil pád prvku, tak sa súradnice častí prvku uložia do poľa board. Pomocou metódy removeFullLines() vymažeme plné čiary, ak sa na boarde nachádzajú. Nakoniec zavoláme metódu newPiece(), ktorá nám vytvorí nový padajúci prvok.

private void newPiece() {

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

    if (!tryMove(curPiece, curX, curY)) {

        curPiece.setShape(Tetrominoe.NoShape);
        timer.cancel();

        var msg = String.format("Game over. Score: %d", numLinesRemoved);
        statusbar.setText(msg);
    }
}

Metóda newPiece() vytvorí náhodným spôsobom nový prvok. Vypočítajú sa súradnice ľavého horného bodu nového prvku. Potom sa pokúsime posunúť prvok smerom nadol. Ak sa nám to nepodarí, hra sa ukončí. Zrušíme timer a vypíšeme ukončujúcu hlášku na statusbar.

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) != Tetrominoe.NoShape) {

            return false;
        }
    }

    curPiece = newPiece;
    curX = newX;
    curY = newY;

    repaint();

    return true;
}

Metóda tryMove() sa pokúsi zmeniť polohu tvaru. Polohu tvaru meníme pomocou kurzorových kláves. Tvary môžeme posunúť alebo rotovať. Poloha sa nezmení, ak dosiahneme okraje plochy alebo iného spadnutého prvku.

private void removeFullLines() {
...

V metóde removeFullLines() vymažeme plné čiary, ktoré boli vytvorené padajúcimi prvkami. (V jednom okamihu sa môže vytvoriť viacero takýchto čiar.)

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) == Tetrominoe.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);
            }
        }
    }
}

Najprv zisťujeme, či sa na ploche nenachádzajú plné čiary spadnutých prvkov. Ak sa nachádzajú, vymažeme ich. Riadok sa vymaže tak, že všetky prvky nad plnou čiarov sa posunú o riadok nadol. V hre sa používa tzv. naive gravity, keď prvky môžu visieť nad prázdnymi štvorcami.

if (numFullLines > 0) {

    numLinesRemoved += numFullLines;

    statusbar.setText(String.valueOf(numLinesRemoved));
    isFallingFinished = true;
    curPiece.setShape(Tetrominoe.NoShape);
}

V prípade, že sme vymazali nejaké riadky, zvýšime hodnotu celkového počtu vymazaných riadkov. To je naše skóre. Nastavíme premennú isFallingFinished na true a vynulujeme objekt padajúceho tvaru. V update()  metóde sa kontroluje hodnota isFallingFinished a v prípade dopadu sa vytvorí nový tvar.

private void drawSquare(Graphics g, int x, int y, Tetrominoe shape)  {

    Color colors[] = { new Color(0, 0, 0), new Color(204, 102, 102),
            new Color(102, 204, 102), new Color(102, 102, 204),
            new Color(204, 204, 102), new Color(204, 102, 204),
            new Color(102, 204, 204), new Color(218, 170, 0)
    };

    var color = colors[shape.ordinal()];

    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);
}

Tetrominoe tvary sa skladajú zo štyroch štvorcov. Každý z týchto štvorcov sa vykreslí pomocou drawSquare() metódy. Jednotlivé tvary majú rôznu farbu. Ľavé a horné hrany štvorcov sú vykreslené v jasnejších farbách, dolné a pravé v tmavších. Týmto sa simulujú 3D hrany.

private class GameCycle implements ActionListener {

    @Override
    public void actionPerformed(ActionEvent e) {

        doGameCycle();
    }
}

Časovač periodicky volá actionPerformed() metódu v ktorej voláme doGameCycle().

private void doGameCycle() {

    update();
    repaint();
}

Hra sa skladá z herných cyklov. V každom cykle aktualizujeme vývoj hry a necháme prekresliť board.

private void update() {

    if (isPaused) {

        return;
    }

    if (isFallingFinished) {

        isFallingFinished = false;
        newPiece();
    } else {

        oneLineDown();
    }
}

V update()  metóde vykonáme jeden krok hry. Ten sa nevykoná v prípade, že je hra prerušená. Inak sa buď vytvorí nový prvok, alebo sa padajúci prvok posunie o riadok nadol.

class TAdapter extends KeyAdapter {

    @Override
    public void keyPressed(KeyEvent e) {
...

Hra sa ovláda kurzorovými šípkami. Pomocou KeyAdapter objektu sledujeme klávesové udalosti. Prepísaním metódy keyPressed() reagujeme na udalosti stlačenia klávesy.

int keycode = e.getKeyCode();

Kód stlačenej klávesy získame z event objektu pomocou metódy getKeyCode().

case KeyEvent.VK_P -> pause();

Hra sa preruší stlačením klávesy P.

case KeyEvent.VK_LEFT -> tryMove(curPiece, curX - 1, curY);

Ak stlačíme ľavý kurzor, pokúsime sa posunúť padajúci tvar smerom doľava.

case KeyEvent.VK_DOWN ->  tryMove(curPiece.rotateRight(), curX, curY);

Ak stlačíme dolný kurzor, pokúsime sa rotovať padajúci tvar smerom doprava.


Hra Tetris