├── tetris_game.png ├── README.md ├── com └── zetcode │ ├── Tetris.java │ ├── Shape.java │ └── Board.java └── LICENSE /tetris_game.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/janbodnar/Java-Tetris-Game/HEAD/tetris_game.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java-Tetris-Game 2 | Java Tetris game clone source code. Uses Java 12. 3 | https://zetcode.com/javagames/tetris/ 4 | 5 | ![Tetris game screenshot](tetris_game.png) 6 | 7 | -------------------------------------------------------------------------------- /com/zetcode/Tetris.java: -------------------------------------------------------------------------------- 1 | package com.zetcode; 2 | 3 | import java.awt.BorderLayout; 4 | import java.awt.EventQueue; 5 | import javax.swing.JFrame; 6 | import javax.swing.JLabel; 7 | 8 | /* 9 | Java Tetris game clone 10 | 11 | Author: Jan Bodnar 12 | Website: https://zetcode.com 13 | */ 14 | public class Tetris extends JFrame { 15 | 16 | private JLabel statusbar; 17 | 18 | public Tetris() { 19 | 20 | initUI(); 21 | } 22 | 23 | private void initUI() { 24 | 25 | statusbar = new JLabel(" 0"); 26 | add(statusbar, BorderLayout.SOUTH); 27 | 28 | var board = new Board(this); 29 | add(board); 30 | board.start(); 31 | 32 | setTitle("Tetris"); 33 | setSize(200, 400); 34 | setDefaultCloseOperation(EXIT_ON_CLOSE); 35 | setLocationRelativeTo(null); 36 | } 37 | 38 | JLabel getStatusBar() { 39 | 40 | return statusbar; 41 | } 42 | 43 | public static void main(String[] args) { 44 | 45 | EventQueue.invokeLater(() -> { 46 | 47 | var game = new Tetris(); 48 | game.setVisible(true); 49 | }); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 2-Clause License 2 | 3 | Copyright (c) 2022, Jan Bodnar 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 17 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 18 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /com/zetcode/Shape.java: -------------------------------------------------------------------------------- 1 | package com.zetcode; 2 | 3 | import java.util.Random; 4 | 5 | public class Shape { 6 | 7 | protected enum Tetrominoe { NoShape, ZShape, SShape, LineShape, 8 | TShape, SquareShape, LShape, MirroredLShape } 9 | 10 | private Tetrominoe pieceShape; 11 | private int coords[][]; 12 | private int[][][] coordsTable; 13 | 14 | 15 | public Shape() { 16 | 17 | initShape(); 18 | } 19 | 20 | private void initShape() { 21 | 22 | coords = new int[4][2]; 23 | 24 | coordsTable = new int[][][] { 25 | { { 0, 0 }, { 0, 0 }, { 0, 0 }, { 0, 0 } }, 26 | { { 0, -1 }, { 0, 0 }, { -1, 0 }, { -1, 1 } }, 27 | { { 0, -1 }, { 0, 0 }, { 1, 0 }, { 1, 1 } }, 28 | { { 0, -1 }, { 0, 0 }, { 0, 1 }, { 0, 2 } }, 29 | { { -1, 0 }, { 0, 0 }, { 1, 0 }, { 0, 1 } }, 30 | { { 0, 0 }, { 1, 0 }, { 0, 1 }, { 1, 1 } }, 31 | { { -1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } }, 32 | { { 1, -1 }, { 0, -1 }, { 0, 0 }, { 0, 1 } } 33 | }; 34 | 35 | setShape(Tetrominoe.NoShape); 36 | } 37 | 38 | protected void setShape(Tetrominoe shape) { 39 | 40 | for (int i = 0; i < 4 ; i++) { 41 | 42 | for (int j = 0; j < 2; ++j) { 43 | 44 | coords[i][j] = coordsTable[shape.ordinal()][i][j]; 45 | } 46 | } 47 | 48 | pieceShape = shape; 49 | } 50 | 51 | private void setX(int index, int x) { coords[index][0] = x; } 52 | private void setY(int index, int y) { coords[index][1] = y; } 53 | public int x(int index) { return coords[index][0]; } 54 | public int y(int index) { return coords[index][1]; } 55 | public Tetrominoe getShape() { return pieceShape; } 56 | 57 | public void setRandomShape() { 58 | 59 | var r = new Random(); 60 | int x = Math.abs(r.nextInt()) % 7 + 1; 61 | 62 | Tetrominoe[] values = Tetrominoe.values(); 63 | setShape(values[x]); 64 | } 65 | 66 | public int minX() { 67 | 68 | int m = coords[0][0]; 69 | 70 | for (int i=0; i < 4; i++) { 71 | 72 | m = Math.min(m, coords[i][0]); 73 | } 74 | 75 | return m; 76 | } 77 | 78 | 79 | public int minY() { 80 | 81 | int m = coords[0][1]; 82 | 83 | for (int i=0; i < 4; i++) { 84 | 85 | m = Math.min(m, coords[i][1]); 86 | } 87 | 88 | return m; 89 | } 90 | 91 | public Shape rotateLeft() { 92 | 93 | if (pieceShape == Tetrominoe.SquareShape) { 94 | 95 | return this; 96 | } 97 | 98 | var result = new Shape(); 99 | result.pieceShape = pieceShape; 100 | 101 | for (int i = 0; i < 4; ++i) { 102 | 103 | result.setX(i, y(i)); 104 | result.setY(i, -x(i)); 105 | } 106 | 107 | return result; 108 | } 109 | 110 | public Shape rotateRight() { 111 | 112 | if (pieceShape == Tetrominoe.SquareShape) { 113 | 114 | return this; 115 | } 116 | 117 | var result = new Shape(); 118 | result.pieceShape = pieceShape; 119 | 120 | for (int i = 0; i < 4; ++i) { 121 | 122 | result.setX(i, -y(i)); 123 | result.setY(i, x(i)); 124 | } 125 | 126 | return result; 127 | } 128 | } 129 | 130 | -------------------------------------------------------------------------------- /com/zetcode/Board.java: -------------------------------------------------------------------------------- 1 | package com.zetcode; 2 | 3 | import com.zetcode.Shape.Tetrominoe; 4 | 5 | import javax.swing.JLabel; 6 | import javax.swing.JPanel; 7 | import javax.swing.Timer; 8 | import java.awt.Color; 9 | import java.awt.Graphics; 10 | import java.awt.event.ActionEvent; 11 | import java.awt.event.ActionListener; 12 | import java.awt.event.KeyAdapter; 13 | import java.awt.event.KeyEvent; 14 | 15 | public class Board extends JPanel { 16 | 17 | private final int BOARD_WIDTH = 10; 18 | private final int BOARD_HEIGHT = 22; 19 | private final int PERIOD_INTERVAL = 300; 20 | 21 | private Timer timer; 22 | private boolean isFallingFinished = false; 23 | private boolean isPaused = false; 24 | private int numLinesRemoved = 0; 25 | private int curX = 0; 26 | private int curY = 0; 27 | private JLabel statusbar; 28 | private Shape curPiece; 29 | private Tetrominoe[] board; 30 | 31 | public Board(Tetris parent) { 32 | 33 | initBoard(parent); 34 | } 35 | 36 | private void initBoard(Tetris parent) { 37 | 38 | setFocusable(true); 39 | statusbar = parent.getStatusBar(); 40 | addKeyListener(new TAdapter()); 41 | } 42 | 43 | private int squareWidth() { 44 | 45 | return (int) getSize().getWidth() / BOARD_WIDTH; 46 | } 47 | 48 | private int squareHeight() { 49 | 50 | return (int) getSize().getHeight() / BOARD_HEIGHT; 51 | } 52 | 53 | private Tetrominoe shapeAt(int x, int y) { 54 | 55 | return board[(y * BOARD_WIDTH) + x]; 56 | } 57 | 58 | void start() { 59 | 60 | curPiece = new Shape(); 61 | board = new Tetrominoe[BOARD_WIDTH * BOARD_HEIGHT]; 62 | 63 | clearBoard(); 64 | newPiece(); 65 | 66 | timer = new Timer(PERIOD_INTERVAL, new GameCycle()); 67 | timer.start(); 68 | } 69 | 70 | private void pause() { 71 | 72 | isPaused = !isPaused; 73 | 74 | if (isPaused) { 75 | 76 | statusbar.setText("paused"); 77 | } else { 78 | 79 | statusbar.setText(String.valueOf(numLinesRemoved)); 80 | } 81 | 82 | repaint(); 83 | } 84 | 85 | @Override 86 | public void paintComponent(Graphics g) { 87 | 88 | super.paintComponent(g); 89 | doDrawing(g); 90 | } 91 | 92 | private void doDrawing(Graphics g) { 93 | 94 | var size = getSize(); 95 | int boardTop = (int) size.getHeight() - BOARD_HEIGHT * squareHeight(); 96 | 97 | for (int i = 0; i < BOARD_HEIGHT; i++) { 98 | 99 | for (int j = 0; j < BOARD_WIDTH; j++) { 100 | 101 | Tetrominoe shape = shapeAt(j, BOARD_HEIGHT - i - 1); 102 | 103 | if (shape != Tetrominoe.NoShape) { 104 | 105 | drawSquare(g, j * squareWidth(), 106 | boardTop + i * squareHeight(), shape); 107 | } 108 | } 109 | } 110 | 111 | if (curPiece.getShape() != Tetrominoe.NoShape) { 112 | 113 | for (int i = 0; i < 4; i++) { 114 | 115 | int x = curX + curPiece.x(i); 116 | int y = curY - curPiece.y(i); 117 | 118 | drawSquare(g, x * squareWidth(), 119 | boardTop + (BOARD_HEIGHT - y - 1) * squareHeight(), 120 | curPiece.getShape()); 121 | } 122 | } 123 | } 124 | 125 | private void dropDown() { 126 | 127 | int newY = curY; 128 | 129 | while (newY > 0) { 130 | 131 | if (!tryMove(curPiece, curX, newY - 1)) { 132 | 133 | break; 134 | } 135 | 136 | newY--; 137 | } 138 | 139 | pieceDropped(); 140 | } 141 | 142 | private void oneLineDown() { 143 | 144 | if (!tryMove(curPiece, curX, curY - 1)) { 145 | 146 | pieceDropped(); 147 | } 148 | } 149 | 150 | private void clearBoard() { 151 | 152 | for (int i = 0; i < BOARD_HEIGHT * BOARD_WIDTH; i++) { 153 | 154 | board[i] = Tetrominoe.NoShape; 155 | } 156 | } 157 | 158 | private void pieceDropped() { 159 | 160 | for (int i = 0; i < 4; i++) { 161 | 162 | int x = curX + curPiece.x(i); 163 | int y = curY - curPiece.y(i); 164 | board[(y * BOARD_WIDTH) + x] = curPiece.getShape(); 165 | } 166 | 167 | removeFullLines(); 168 | 169 | if (!isFallingFinished) { 170 | 171 | newPiece(); 172 | } 173 | } 174 | 175 | private void newPiece() { 176 | 177 | curPiece.setRandomShape(); 178 | curX = BOARD_WIDTH / 2 + 1; 179 | curY = BOARD_HEIGHT - 1 + curPiece.minY(); 180 | 181 | if (!tryMove(curPiece, curX, curY)) { 182 | 183 | curPiece.setShape(Tetrominoe.NoShape); 184 | timer.stop(); 185 | 186 | var msg = String.format("Game over. Score: %d", numLinesRemoved); 187 | statusbar.setText(msg); 188 | } 189 | } 190 | 191 | private boolean tryMove(Shape newPiece, int newX, int newY) { 192 | 193 | for (int i = 0; i < 4; i++) { 194 | 195 | int x = newX + newPiece.x(i); 196 | int y = newY - newPiece.y(i); 197 | 198 | if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT) { 199 | 200 | return false; 201 | } 202 | 203 | if (shapeAt(x, y) != Tetrominoe.NoShape) { 204 | 205 | return false; 206 | } 207 | } 208 | 209 | curPiece = newPiece; 210 | curX = newX; 211 | curY = newY; 212 | 213 | repaint(); 214 | 215 | return true; 216 | } 217 | 218 | private void removeFullLines() { 219 | 220 | int numFullLines = 0; 221 | 222 | for (int i = BOARD_HEIGHT - 1; i >= 0; i--) { 223 | 224 | boolean lineIsFull = true; 225 | 226 | for (int j = 0; j < BOARD_WIDTH; j++) { 227 | 228 | if (shapeAt(j, i) == Tetrominoe.NoShape) { 229 | 230 | lineIsFull = false; 231 | break; 232 | } 233 | } 234 | 235 | if (lineIsFull) { 236 | 237 | numFullLines++; 238 | 239 | for (int k = i; k < BOARD_HEIGHT - 1; k++) { 240 | for (int j = 0; j < BOARD_WIDTH; j++) { 241 | board[(k * BOARD_WIDTH) + j] = shapeAt(j, k + 1); 242 | } 243 | } 244 | } 245 | } 246 | 247 | if (numFullLines > 0) { 248 | 249 | numLinesRemoved += numFullLines; 250 | 251 | statusbar.setText(String.valueOf(numLinesRemoved)); 252 | isFallingFinished = true; 253 | curPiece.setShape(Tetrominoe.NoShape); 254 | } 255 | } 256 | 257 | private void drawSquare(Graphics g, int x, int y, Tetrominoe shape) { 258 | 259 | Color colors[] = {new Color(0, 0, 0), new Color(204, 102, 102), 260 | new Color(102, 204, 102), new Color(102, 102, 204), 261 | new Color(204, 204, 102), new Color(204, 102, 204), 262 | new Color(102, 204, 204), new Color(218, 170, 0) 263 | }; 264 | 265 | var color = colors[shape.ordinal()]; 266 | 267 | g.setColor(color); 268 | g.fillRect(x + 1, y + 1, squareWidth() - 2, squareHeight() - 2); 269 | 270 | g.setColor(color.brighter()); 271 | g.drawLine(x, y + squareHeight() - 1, x, y); 272 | g.drawLine(x, y, x + squareWidth() - 1, y); 273 | 274 | g.setColor(color.darker()); 275 | g.drawLine(x + 1, y + squareHeight() - 1, 276 | x + squareWidth() - 1, y + squareHeight() - 1); 277 | g.drawLine(x + squareWidth() - 1, y + squareHeight() - 1, 278 | x + squareWidth() - 1, y + 1); 279 | } 280 | 281 | private class GameCycle implements ActionListener { 282 | 283 | @Override 284 | public void actionPerformed(ActionEvent e) { 285 | 286 | doGameCycle(); 287 | } 288 | } 289 | 290 | private void doGameCycle() { 291 | 292 | update(); 293 | repaint(); 294 | } 295 | 296 | private void update() { 297 | 298 | if (isPaused) { 299 | 300 | return; 301 | } 302 | 303 | if (isFallingFinished) { 304 | 305 | isFallingFinished = false; 306 | newPiece(); 307 | } else { 308 | 309 | oneLineDown(); 310 | } 311 | } 312 | 313 | class TAdapter extends KeyAdapter { 314 | 315 | @Override 316 | public void keyPressed(KeyEvent e) { 317 | 318 | if (curPiece.getShape() == Tetrominoe.NoShape) { 319 | 320 | return; 321 | } 322 | 323 | int keycode = e.getKeyCode(); 324 | 325 | // Java 12 switch expressions 326 | switch (keycode) { 327 | 328 | case KeyEvent.VK_P -> pause(); 329 | case KeyEvent.VK_LEFT -> tryMove(curPiece, curX - 1, curY); 330 | case KeyEvent.VK_RIGHT -> tryMove(curPiece, curX + 1, curY); 331 | case KeyEvent.VK_DOWN -> tryMove(curPiece.rotateRight(), curX, curY); 332 | case KeyEvent.VK_UP -> tryMove(curPiece.rotateLeft(), curX, curY); 333 | case KeyEvent.VK_SPACE -> dropDown(); 334 | case KeyEvent.VK_D -> oneLineDown(); 335 | } 336 | } 337 | } 338 | } 339 | --------------------------------------------------------------------------------