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.