|  | Home · Examples

# Tetrix Example

Code:

- Tetrix.java

The Tetrix example is a Qt version of the classic Tetrix game.
The object of the game is to stack pieces dropped from the top of the playing area so that they fill entire rows at the bottom of the playing area.

When a row is filled, all the blocks on that row are removed, the player earns a number of points, and the pieces above are moved down to occupy that row. If more than one row is filled, the blocks on each row are removed, and the player earns extra points.

The Left cursor key moves the current piece one space to the left, the Right cursor key moves it one space to the right, the Up cursor key rotates the piece counter-clockwise by 90 degrees, and the Down cursor key rotates the piece clockwise by 90 degrees.

To avoid waiting for a piece to fall to the bottom of the board, press D to immediately move the piece down by one row, or press the Space key to drop it as close to the bottom of the board as possible.

This example shows how a simple game can be created using only two classes:

- The `Tetrix` class is used to display the player's score, number of lives, and information about the next piece to appear.
- The `TetrixBoard` class contains the game logic, handles keyboard input, and displays the pieces on the playing area. It also shows the player's score, number of lives, and information about the next piece to appear.
- The `TetrixPiece` class contains information about each piece.

In this approach, the `TetrixBoard` class is the most complex class, since it handles the game logic and rendering. One benefit of this is that the `Tetrix` and `TetrixPiece` classes are very simple and contain only a minimum of code.

## Tetrix Class Implementation

The `Tetrix` class is used to display the game information and contains the playing area:

    class TetrixBoard extends QFrame
    {

We use private member variables (shown later) for the board, various display widgets, and buttons to allow the user to start a new game, pause the current game, and quit.

Although the window inherits QWidget, the constructor does not provide an argument to allow a parent widget to be specified. This is because the window will always be used as a top-level widget.

        public Tetrix() {
            this(null);
        }

        public Tetrix(QWidget parent)
        {
            super(parent);

            board = new TetrixBoard(null);

We begin by constructing a `TetrixBoard` instance for the playing area and a label that shows the next piece to be dropped into the playing area; the label is initially empty.

Three QLCDNumber objects are used to display the score, number of lives, and lines removed. These initially show default values, and will be filled in when a game begins:

            scoreLcd = new QLCDNumber(5);
            scoreLcd.setSegmentStyle(QLCDNumber.SegmentStyle.Filled);

Three buttons with shortcuts are constructed so that the user can start a new game, pause the current game, and quit the application:

            startButton = new QPushButton("&Start");
            startButton.setFocusPolicy(Qt.FocusPolicy.NoFocus);
            quitButton = new QPushButton("&Quit");
            quitButton.setFocusPolicy(Qt.FocusPolicy.NoFocus);
            pauseButton = new QPushButton("&Pause");

            pauseButton.setFocusPolicy(Qt.FocusPolicy.NoFocus);

These buttons are configured so that they never receive the keyboard focus; we want the keyboard focus to remain with the `TetrixBoard` instance so that it receives all the keyboard events. Nonetheless, the buttons will still respond to Alt key shortcuts.

We connect clicked() signals from the Start and Pause buttons to the board, and from the Quit button to the application's quit() slot.

            startButton.clicked.connect(board, "start()");

            quitButton.clicked.connect(this, "close()");
            pauseButton.clicked.connect(board, "pause()");
            board.scoreChanged.connect(scoreLcd, "display(int)");
            board.levelChanged.connect(levelLcd, "display(int)");
            board.linesRemovedChanged.connect(linesLcd, "display(int)");

Signals from the board are also connected to the LCD widgets for the purpose of updating the score, number of lives, and lines removed from the playing area.

We place the label, LCD widgets, and the board into a QGridLayout along with some labels that we create with the `createLabel()` convenience method:

            layout = new QGridLayout();
            layout.addWidget(createLabel("NEXT"), 0, 0);
            layout.addWidget(nextPieceLabel, 1, 0);
            layout.addWidget(createLabel("LEVEL"), 2, 0);
            layout.addWidget(levelLcd, 3, 0);
            layout.addWidget(startButton, 4, 0);
            layout.addWidget(board, 0, 1, 6, 1);
            layout.addWidget(createLabel("SCORE"), 0, 2);
            layout.addWidget(scoreLcd, 1, 2);
            layout.addWidget(createLabel("LINES REMOVED"), 2, 2);
            layout.addWidget(linesLcd, 3, 2);
            layout.addWidget(quitButton, 4, 2);
            layout.addWidget(pauseButton, 5, 2);

            setLayout(layout);

            setWindowTitle("Tetrix");
            setWindowIcon(new QIcon("classpath:com/trolltech/images/qt-logo.png"));
            resize(550, 370);
        }

Finally, we set the grid layout on the widget, give the window a title, and resize it to an appropriate size.

The `createLabel()` convenience method simply creates a new label on the heap, gives it an appropriate alignment, and returns it to the caller:

        private QLabel createLabel(String text)
        {
            QLabel lbl = new QLabel(text);
            lbl.setAlignment(new Qt.Alignment(Qt.AlignmentFlag.AlignBottom));
            return lbl;
        }

Since each label will be used in the widget's layout, it will become a child of the `TetrixWindow` widget and, as a result, it will be deleted when the window is deleted.

We initialize the private variables for the board and each of the display widgets:

        private TetrixBoard board = null;
        private QPushButton startButton = null;
        private QPushButton quitButton = null;
        private QPushButton pauseButton = null;
        private QLabel nextPieceLabel = null;
        private QLCDNumber scoreLcd = null;
        private QLCDNumber levelLcd = null;
        private QLCDNumber linesLcd = null;
        private QGridLayout layout = null;

Finally, the `main()` method looks like this:

        public static void main(String args[])
        {
            QApplication.initialize(args);

            Tetrix window = new Tetrix();
            window.show();

            QApplication.exec();
        }

    }

## TetrixPiece Class Definition

The `TetrixPiece` class holds information about a piece in the game's playing area, including its shape, position, and the range of positions it can occupy on the board:

    class TetrixPiece
    {
        static final int coordsTable[][][] =
        { { { 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 } } };

Since there are only a few different shapes of pieces, we define a look-up table of pieces to associate each shape with an array of block positions.

        private TetrixBoard.TetrixShape pieceShape;
        private int coords[][] = new int[4][2];

Each shape contains four blocks, and these are defined by the `coords` private member variable. Additionally, each piece has a high-level description that is stored internally in the `pieceShape` variable.

The constructor simply ensures that each piece is initially created with no shape:

        public TetrixPiece()
        {
            setShape(TetrixBoard.TetrixShape.NoShape);
        }

We also provide a copy constructor:

        public TetrixPiece(TetrixPiece copy)
        {
            pieceShape = copy.shape();
            for (int i=0; i<4; ++i) {
                setX(i, copy.x(i));
                setY(i, copy.y(i));
            }

        }

The `setRandomShape()` method is used to select a random shape for a piece:

        public void setRandomShape()
        {
            Random rand = new Random();
            int shapeint = rand.nextInt(7) + 1;

            TetrixBoard.TetrixShape shape = TetrixBoard.TetrixShape.NoShape;
            switch (shapeint) {
            case 1: shape = TetrixBoard.TetrixShape.ZShape; break ;
            case 2: shape = TetrixBoard.TetrixShape.SShape; break ;
            case 3: shape = TetrixBoard.TetrixShape.LineShape; break ;
            case 4: shape = TetrixBoard.TetrixShape.TShape; break ;
            case 5: shape = TetrixBoard.TetrixShape.SquareShape; break ;
            case 6: shape = TetrixBoard.TetrixShape.LShape; break ;
            case 7: shape = TetrixBoard.TetrixShape.MirroredLShape; break ;
            }

            setShape(shape);

        }

For convenience, it simply chooses a random shape from the `TetrixShape` enum and calls the `setShape()` method to perform the task of positioning the blocks.

The `shape()` method simply returns the contents of the `pieceShape` variable:

        public TetrixBoard.TetrixShape shape()
        {
            return pieceShape;
        }

The `setShape()` method uses a look-up table of pieces to associated each shape with an array of block positions:

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

            pieceShape = shape;

        }

These positions are read from the table into the piece's own array of positions, and the piece's internal shape information is updated to use the new shape.

The `minX()` and `maxX()` methods return the minimum and maximum horizontal coordinates occupied by the blocks that make up the piece:

        public int minX()
        {
            int min = coords[0][0];
            for (int i=1; i<4; ++i)
                min = min < coords[i][0] ? min : coords[i][0];
            return min;
        }

        public int maxX()

        {
            int max = coords[0][0];
            for (int i=1; i<4; ++i)
                max = max > coords[i][0] ? max : coords[i][0];
            return max;
        }

Similarly, the `minY()` and `maxY()` methods return the minimum and maximum vertical coordinates occupied by the blocks:

        public int minY()
        {
            int min = coords[0][1];
            for (int i=1; i<4; ++i)
                min = min < coords[i][1] ? min : coords[i][1];
            return min;
        }

        public int maxY()

        {
            int max = coords[0][1];
            for (int i=1; i<4; ++i)
                max = max > coords[i][1] ? max : coords[i][1];
            return max;
        }

The `rotatedLeft()` method returns a new piece with the same shape as an existing piece, but rotated counter-clockwise by 90 degrees:

        public TetrixPiece rotatedLeft()
        {
            if (pieceShape == TetrixBoard.TetrixShape.SquareShape)
                return this;

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

            return result;

        }

Similarly, the `rotatedRight()` method returns a new piece with the same shape as an existing piece, but rotated clockwise by 90 degrees:

        public TetrixPiece rotatedRight()
        {
            if (pieceShape == TetrixBoard.TetrixShape.SquareShape)
                return this;

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

            return result;

        }

These two methods enable each piece to create rotated copies of itself.

The `x()` and `y()` methods return the x and y-coordinates of any given block in the shape:

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

        public int y(int index)

        {
            return coords[index][1];
        }

The positions returned by these methods are defined on a grid that extends horizontally and vertically with coordinates from -2 to 2. Although the predefined coordinates for each piece only vary horizontally from -1 to 1 and vertically from -1 to 2, each piece can be rotated by 90, 180, and 270 degrees.

We use `setX()` and `setY()` to set new coordinates for the blocks in the shape:

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

        private void setY(int index, int y)

        {
            coords[index][1] = y;
        }

    }

## TetrixBoard Class Definition

The `TetrixBoard` class inherits from QFrame and contains the game logic and display features:

    class TetrixBoard extends QFrame
    {

        static final int redTable[] = new int[8];
        static final int greenTable[] = new int[8];
        static final int blueTable[] = new int[8];
        static {
            redTable[0] = 0x00;
            redTable[1] = 0xCC;
            redTable[2] = 0x66;
            redTable[3] = 0x66;
            redTable[4] = 0xCC;
            redTable[5] = 0xCC;
            redTable[6] = 0x66;
            redTable[7] = 0xDA;

            greenTable[0] = 0x00;
            greenTable[1] = 0x66;
            greenTable[2] = 0xCC;
            greenTable[3] = 0x66;
            greenTable[4] = 0xCC;
            greenTable[5] = 0x66;
            greenTable[6] = 0xCC;
            greenTable[7] = 0xAA;

            blueTable[0] = 0x00;
            blueTable[1] = 0x66;
            blueTable[2] = 0x66;
            blueTable[3] = 0xCC;
            blueTable[4] = 0x66;
            blueTable[5] = 0xCC;
            blueTable[6] = 0xCC;
            blueTable[7] = 0x00;

        };

        enum TetrixShape { NoShape, ZShape, SShape, LineShape, TShape, SquareShape,
                       LShape, MirroredLShape }
        private static final int BoardWidth = 10;
        private static final int BoardHeight = 22;

The colors used to display each type of shape are defined in separate tables for red, green, and blue components. We also define a set of shapes that will be used for pieces in the game, and set fixed dimensions for the playing area.

        private boolean isStarted = false;
        private boolean isPaused = false;
        private boolean isWaitingAfterLine = false;
        private int numLinesRemoved = 0;
        private int numPiecesDropped = 0;
        private int score = 0;
        private int level = 0;
        private int curX = 0;
        private int curY = 0;
        private QLabel nextPieceLabel = null;
        private QBasicTimer timer = new QBasicTimer();

We use a QBasicTimer to control the rate at which pieces fall toward the bottom of the playing area. This allows us to provide an implementation of timerEvent() that we can use to update the widget.

        private TetrixPiece curPiece = new TetrixPiece();
        private TetrixPiece nextPiece = new TetrixPiece();
        private TetrixShape board[] = new TetrixShape[BoardWidth * BoardHeight];

The board is composed of a fixed-size array whose elements correspond to spaces for individual blocks. Each element in the array contains a `TetrixShape` value corresponding to the type of shape that occupies that element.

Each shape on the board will occupy four elements in the array, and these will all contain the enum value that corresponds to the type of the shape.

        public Signal1<Integer> scoreChanged = new Signal1<Integer>();
        public Signal1<Integer> levelChanged = new Signal1<Integer>();
        public Signal1<Integer> linesRemovedChanged = new Signal1<Integer>();

Three signals are used to communicate changes to the player's information to the `Tetrix` instance.

In the constructor, we customize the frame style of the widget, ensure that keyboard input will be received by the widget by using Qt::StrongFocus for the focus policy, and initialize the game state:

        public TetrixBoard(QWidget parent)
        {
            super(parent);

            setFrameStyle(QFrame.Shape.Panel.value() | QFrame.Shadow.Sunken.value());
            setFocusPolicy(Qt.FocusPolicy.StrongFocus);
            clearBoard();

            nextPiece.setRandomShape();
        }

The first (next) piece is also set up with a random shape.

The `setNextPieceLabel()` method is used to pass in an externally-constructed label to the board, so that it can be shown alongside the playing area:

        public void setNextPieceLabel(QLabel label)
        {
            nextPieceLabel = label;
        }

We provide a reasonable size hint and minimum size hint for the board, based on the size of the space for each block in the playing area:

        public QSize sizeHint()
        {
            return new QSize(BoardWidth * 15 + frameWidth() * 2, BoardHeight * 15 + frameWidth() * 2);
        }

        @Override
        public QSize minimumSizeHint()

        {
            return new QSize(BoardWidth * 5 + frameWidth() * 2, BoardHeight * 5 + frameWidth() * 2);
        }

By using a minimum size hint, we indicate to the layout in the parent widget that the board should not shrink below a minimum size.

A new game is started when the `start()` slot is called. This resets the game's state, the player's score and level, and the contents of the board:

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

            isStarted = true;
            isWaitingAfterLine = false;
            numLinesRemoved = 0;
            numPiecesDropped = 0;
            score = 0;
            level = 1;
            clearBoard();

            linesRemovedChanged(numLinesRemoved);
            scoreChanged(score);
            levelChanged(level);

            newPiece();

            timer.start(timeoutTime(), this);
        }

We also emit signals to inform other components of these changes before creating a new piece that is ready to be dropped into the playing area. We start the timer that determines how often the piece drops down one row on the board.

The `pause()` slot is used to temporarily stop the current game by stopping the internal timer:

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

            isPaused = !isPaused;
            if (isPaused) {
                timer.stop();
            } else {
                timer.start(timeoutTime(), this);
            }

            update();

        }

We perform checks to ensure that the game can only be paused if it is already running and not already paused.

The `paintEvent()` method is straightforward to implement. We begin by calling the base class's implementation of paintEvent() before constructing a QPainter for use on the board:

        protected void paintEvent(QPaintEvent e)
        {
            super.paintEvent(e);

            QPainter painter = new QPainter();
            painter.begin(this);
            QRect rect = contentsRect();

Since the board is a subclass of QFrame, we obtain a QRect that covers the area inside the frame decoration before drawing our own content.

If the game is paused, we want to hide the existing state of the board and show some text. We achieve this by painting text onto the widget and returning early from the method. The rest of the painting is performed after this point.

The position of the top of the board is found by subtracting the total height of each space on the board from the bottom of the frame's internal rectangle. For each space on the board that is occupied by a piece, we call the `drawSquare()` method to draw a block at that position.

            int boardTop = rect.bottom() - BoardHeight * squareHeight();

            for (int i=0; i<BoardHeight; ++i) {
                for (int j=0; j<BoardWidth; ++j) {
                    TetrixShape shape = shapeAt(j, BoardHeight - i - 1);
                    if (shape != TetrixShape.NoShape) {
                        drawSquare(painter, rect.left() + j * squareWidth(),
                            boardTop + i * squareHeight(), shape);
                    }

                }

Spaces that are not occupied by blocks are left blank.

Unlike the existing pieces on the board, the current piece is drawn block-by-block at its current position:

            if (curPiece.shape() != TetrixShape.NoShape) {
                for (int i=0; i<4; ++i) {
                    int x = curX + curPiece.x(i);
                    int y = curY - curPiece.y(i);

                    drawSquare(painter, rect.left() + x * squareWidth(),
                        boardTop + (BoardHeight - y - 1) * squareHeight(),
                        curPiece.shape());
                }

            }

            painter.end();
        }

The `keyPressEvent()` handler is called whenever the player presses a key while the `TetrixBoard` widget has the keyboard focus.

        protected void keyPressEvent(QKeyEvent event)
        {
            if (!isStarted || isPaused || curPiece.shape() == TetrixShape.NoShape) {
                super.keyPressEvent(event);
                return ;
            }

If there is no current game, the game is running but paused, or if there is no current shape to control, we simply pass on the event to the base class.

We check whether the event is about any of the keys that the player uses to control the current piece and, if so, we call the relevant method to handle the input:

            if (event.key() == Qt.Key.Key_Left.value())
                tryMove(curPiece, curX - 1, curY);
            else if (event.key() == Qt.Key.Key_Right.value())
                tryMove(curPiece, curX + 1, curY);
            else if (event.key() == Qt.Key.Key_Down.value())
                tryMove(curPiece.rotatedRight(), curX, curY);
            else if (event.key() == Qt.Key.Key_Up.value())
                tryMove(curPiece.rotatedLeft(), curX, curY);
            else if (event.key() == Qt.Key.Key_Space.value())
                dropDown();
            else if (event.key() == Qt.Key.Key_D.value())
                oneLineDown();
            else
                super.keyPressEvent(event);
        }

In the case where the player presses a key that we are not interested in, we again pass on the event to the base class's implementation of keyPressEvent().

The `timerEvent()` handler is called every time the class's QBasicTimer instance times out. We need to check that the event we receive corresponds to our timer. If it does, we can update the board:

        protected void timerEvent(QTimerEvent event)
        {
            if (event.timerId() == timer.timerId()) {
                if (isWaitingAfterLine) {
                    isWaitingAfterLine = false;
                    newPiece();
                    timer.start(timeoutTime(), this);
                } else {
                    oneLineDown();
                }
            } else {
                super.timerEvent(event);

            }

        }

If a row (or line) has just been filled, we create a new piece and reset the timer; otherwise we move the current piece down by one row. We let the base class handle other timer events that we receive.

The `clearBoard()` method simply fills the board with the `TetrixShape::NoShape` value:

        void clearBoard()
        {
            for (int i=0; i<BoardHeight * BoardWidth; ++i)
                board[i] = TetrixShape.NoShape;
        }

The `dropDown()` method moves the current piece down as far as possible on the board, either until it is touching the bottom of the playing area or it is stacked on top of another piece:

        void dropDown()
        {
            int dropHeight = 0;
            int newY = curY;
            while (newY > 0) {
                if (!tryMove(curPiece, curX, newY - 1))
                    break ;
                --newY;
                ++dropHeight;
            }
            pieceDropped(dropHeight);

        }

The number of rows the piece has dropped is recorded and passed to the `pieceDropped()` method so that the player's score can be updated.

The `oneLineDown()` method is used to move the current piece down by one row (line), either when the user presses the D key or when the piece is scheduled to move:

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

If the piece cannot drop down by one line, we call the `pieceDropped()` method with zero as the argument to indicate that it cannot fall any further, and that the player should receive no extra points for the fall.

The `pieceDropped()` method itself is responsible for awarding points to the player for positioning the current piece, checking for full rows on the board and, if no lines have been removed, creating a new piece to replace the current one:

        void pieceDropped(int dropHeight)
        {
            for (int i=0; i<4; ++i) {
                int x = curX + curPiece.x(i);
                int y = curY - curPiece.y(i);
                setShapeAt(x, y, curPiece.shape());
            }

            ++numPiecesDropped;
            if (numPiecesDropped % 25 == 0) {
                ++level;
                timer.start(timeoutTime(), this);
                levelChanged(level);
            }

            score += dropHeight + 7;
            scoreChanged(score);

            removeFullLines();

            if (!isWaitingAfterLine)
                newPiece();

        }

We call `removeFullLines()` each time a piece has been dropped. This scans the board from bottom to top, looking for blank spaces on each row.

        void removeFullLines()
        {
            int numFullLines = 0;

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

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

                if (lineIsFull) {

                    ++numFullLines;
                    for (int k=i; k<BoardHeight - 1; ++k) {
                        for (int j=0; j<BoardWidth; ++j)
                            setShapeAt(j, k, shapeAt(j, k + 1));
                    }

                    for (int j=0; j<BoardWidth; ++j)
                        setShapeAt(j, BoardHeight - 1, TetrixShape.NoShape);
                }

            }

If a row contains no blank spaces, the rows above it are copied down by one row to compress the stack of pieces, the top row on the board is cleared, and the number of full lines found is incremented.

            if (numFullLines > 0) {
                numLinesRemoved += numFullLines;
                score += 10 * numFullLines;
                linesRemovedChanged(numLinesRemoved);
                scoreChanged(score);

                timer.start(500, this);
                isWaitingAfterLine = true;
                curPiece.setShape(TetrixShape.NoShape);
                update();
            }

        }

If some lines have been removed, the player's score and the total number of lines removed are updated. The `linesRemoved()` and `scoreChanged()` signals are emitted to send these new values to other widgets in the window.

Additionally, we set the timer to elapse after half a second, set the `isWaitingAfterLine` flag to indicate that lines have been removed, unset the piece's shape to ensure that it is not drawn, and update the widget. The next time that the `timerEvent()` handler is called, a new piece will be created and the game will continue.

The `newPiece()` method places the next available piece at the top of the board, and creates a new piece with a random shape:

        void newPiece()
        {
            curPiece = new TetrixPiece(nextPiece);

            nextPiece.setRandomShape();
            showNextPiece();
            curX = BoardWidth / 2 + 1;
            curY = BoardHeight - 1 + curPiece.minY();

            if (!tryMove(curPiece, curX, curY)) {
                curPiece.setShape(TetrixShape.NoShape);
                timer.stop();
                isStarted = false;
            }

        }

We place a new piece in the middle of the board at the top. The game is over if the piece can't move, so we unset its shape to prevent it from being drawn, stop the timer, and unset the `isStarted` flag.

The `showNextPiece()` method updates the label that shows the next piece to be dropped:

        void showNextPiece()
        {
            if (nextPieceLabel == null)
                return ;

            int dx = nextPiece.maxX() - nextPiece.minX() + 1;
            int dy = nextPiece.maxY() - nextPiece.minY() + 1;

            QPixmap pixmap = new QPixmap(dx * squareWidth(), dy * squareHeight());
            QPainter painter = new QPainter();
            painter.begin(pixmap);
            painter.fillRect(pixmap.rect(), nextPieceLabel.palette().window());

            for (int i=0; i<4; ++i) {
                int x = nextPiece.x(i) - nextPiece.minX();
                int y = nextPiece.y(i) - nextPiece.minY();
                drawSquare(painter, x * squareWidth(), y * squareHeight(), nextPiece.shape());
            }
            painter.end();

            nextPieceLabel.setPixmap(pixmap);

        }

We draw the piece's component blocks onto a pixmap that is then set on the label.

The `tryMove()` method is used to determine whether a piece can be positioned at the specified coordinates:

        boolean tryMove(TetrixPiece 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 >= BoardWidth || y < 0 || y >= BoardHeight)
                    return false;
                if (shapeAt(x, y) != TetrixShape.NoShape)
                    return false;
            }

We examine the spaces on the board that the piece needs to occupy and, if they are already occupied by other pieces, we return `false` to indicate that the move has failed.

            curPiece = new TetrixPiece(newPiece);
            curX = newX;
            curY = newY;
            update();
            return true;
        }

If the piece could be placed on the board at the desired location, we update the current piece and its position, update the widget, and return `true` to indicate success.

The `drawSquare()` method draws the blocks (normally squares) that make up each piece using different colors for pieces with different shapes:

        void drawSquare(QPainter painter, int x, int y, TetrixShape shape)
        {
            QColor color = new QColor(redTable[shape.ordinal()], greenTable[shape.ordinal()], blueTable[shape.ordinal()]);
            painter.fillRect(x + 1, y + 1, squareWidth() - 2, squareHeight() - 2,
                new QBrush(color));

            painter.setPen(color.lighter());
            painter.drawLine(x, y + squareHeight() - 1, x, y);
            painter.drawLine(x, y, x + squareWidth() - 1, y);

            painter.setPen(color.darker());
            painter.drawLine(x + 1, y + squareHeight() - 1, x + squareWidth() - 1, y + squareHeight() - 1);
            painter.drawLine(x + squareWidth() - 1, y + squareHeight() - 1, x + squareWidth() - 1, y + 1);
        }

    }

We obtain the color to use from a look-up table that relates each shape to an RGB value, and use the painter provided to draw the block at the specified coordinates.