├── .clang-format ├── .gitignore ├── CMakeLists.txt ├── LICENSE.txt ├── README.md ├── resources ├── Keyboard_White_Arrow_Down.png ├── Keyboard_White_Arrow_Left.png ├── Keyboard_White_Arrow_Right.png ├── Keyboard_White_Arrow_Up.png ├── Keyboard_White_C.png ├── Keyboard_White_Enter.png ├── Keyboard_White_Esc.png ├── Keyboard_White_Space.png ├── Keyboard_White_X.png ├── Keyboard_White_Z.png ├── contour_blue.png ├── contour_cyan.png ├── contour_green.png ├── contour_orange.png ├── contour_purple.png ├── contour_red.png ├── contour_yellow.png ├── kenvector_future.ttf ├── tile_blue.png ├── tile_cyan.png ├── tile_green.png ├── tile_orange.png ├── tile_purple.png ├── tile_red.png └── tile_yellow.png └── src ├── game.cpp ├── render.cpp ├── render.h ├── stb_image.h ├── tetris.cpp ├── tetris.h ├── util.cpp └── util.h /.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: Google 2 | IndentWidth: 4 3 | ColumnLimit: 120 4 | IndentCaseLabels: false 5 | AllowShortCaseLabelsOnASingleLine: true 6 | SpaceAfterCStyleCast: true 7 | SortIncludes: Never 8 | AccessModifierOffset: -4 9 | BreakConstructorInitializers: BeforeComma 10 | SpaceBeforeCpp11BracedList: true 11 | DerivePointerAlignment: false 12 | PointerAlignment: Left 13 | InsertBraces: true 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | cmake-build-* 2 | .DS_store 3 | .idea 4 | vs2017_project/Debug 5 | vs2017_project/Release 6 | vs2017_project/.vs 7 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.6) 2 | project(tetris) 3 | 4 | set(CMAKE_CXX_STANDARD 11) 5 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 6 | add_compile_options(-Wall -Wextra) 7 | 8 | find_package(OpenGL REQUIRED) 9 | find_package(Freetype REQUIRED) 10 | find_package(glfw3 REQUIRED) 11 | find_package(glm REQUIRED) 12 | find_package(GLEW REQUIRED) 13 | 14 | set(SOURCE_FILES 15 | src/game.cpp 16 | src/tetris.h src/tetris.cpp 17 | src/render.h src/render.cpp 18 | src/util.h src/util.cpp 19 | src/stb_image.h) 20 | 21 | set(OpenGL_GL_PREFERENCE GLVND) 22 | add_executable(tetris ${SOURCE_FILES}) 23 | target_link_libraries(tetris glfw glm::glm Freetype::Freetype OpenGL::GL GLEW::glew) 24 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Nikolay Mayorov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A simple tetris game implemented for self-education purposes. Here is a [link](https://youtu.be/-8teaOoZdxA) to a video with gameplay for illustration. 2 | 3 | Code organization 4 | ----------------- 5 | 6 | The game logic is implemented in `tetris.cpp`. The exact game rules and mechanics are figured from several wikis about Tetris. 7 | 8 | Class `Piece` represents a game piece ("tetrimino"), it defines how a piece rotates and kicks off obstacles. 9 | 10 | Class `Board` represents the geometric state of the board. It stores which tiles are occupied, the position of the current piece and processes required motions obeying geometric constraints. Class `Tetris` operates on `Board` and defines game timings, user input processing and scoring. 11 | 12 | The drawing functions are implemented in `render.cpp`. It defines several convenience classes to render board, pieces and text using simple OpenGL shaders. 13 | 14 | File `utility.cpp` contains classes representing a shader, a texture and a font glyph. As well as functions to load a texture and a font from a file. 15 | 16 | The game initialization and main loop is implemented in `game.cpp` in the most straightforward manner without farther abstractions. 17 | 18 | Building 19 | -------- 20 | Building was reworked and tested for Ubuntu 20.04. 21 | It's too much hassle to properly support all platforms because of peculiar dependency libraries. 22 | 23 | Install required packages: 24 | ```shell 25 | sudo apt-get install libglew-dev libfreetype-dev libglfw3-dev libglm-dev libopengl-dev 26 | ``` 27 | Then use CMake to generate and execute build. 28 | 29 | Make sure that `resources` folder is near the executable before running. 30 | 31 | Credits 32 | ------- 33 | 34 | http://learnopengl.com was amazingly helpful to learn basics of OpenGL. Some utility code was based on samples from there. 35 | 36 | Tile sprites and font by kenney.nl. 37 | 38 | Keyboard key sprites by Xelu. 39 | -------------------------------------------------------------------------------- /resources/Keyboard_White_Arrow_Down.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/Keyboard_White_Arrow_Down.png -------------------------------------------------------------------------------- /resources/Keyboard_White_Arrow_Left.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/Keyboard_White_Arrow_Left.png -------------------------------------------------------------------------------- /resources/Keyboard_White_Arrow_Right.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/Keyboard_White_Arrow_Right.png -------------------------------------------------------------------------------- /resources/Keyboard_White_Arrow_Up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/Keyboard_White_Arrow_Up.png -------------------------------------------------------------------------------- /resources/Keyboard_White_C.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/Keyboard_White_C.png -------------------------------------------------------------------------------- /resources/Keyboard_White_Enter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/Keyboard_White_Enter.png -------------------------------------------------------------------------------- /resources/Keyboard_White_Esc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/Keyboard_White_Esc.png -------------------------------------------------------------------------------- /resources/Keyboard_White_Space.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/Keyboard_White_Space.png -------------------------------------------------------------------------------- /resources/Keyboard_White_X.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/Keyboard_White_X.png -------------------------------------------------------------------------------- /resources/Keyboard_White_Z.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/Keyboard_White_Z.png -------------------------------------------------------------------------------- /resources/contour_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/contour_blue.png -------------------------------------------------------------------------------- /resources/contour_cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/contour_cyan.png -------------------------------------------------------------------------------- /resources/contour_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/contour_green.png -------------------------------------------------------------------------------- /resources/contour_orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/contour_orange.png -------------------------------------------------------------------------------- /resources/contour_purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/contour_purple.png -------------------------------------------------------------------------------- /resources/contour_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/contour_red.png -------------------------------------------------------------------------------- /resources/contour_yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/contour_yellow.png -------------------------------------------------------------------------------- /resources/kenvector_future.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/kenvector_future.ttf -------------------------------------------------------------------------------- /resources/tile_blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/tile_blue.png -------------------------------------------------------------------------------- /resources/tile_cyan.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/tile_cyan.png -------------------------------------------------------------------------------- /resources/tile_green.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/tile_green.png -------------------------------------------------------------------------------- /resources/tile_orange.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/tile_orange.png -------------------------------------------------------------------------------- /resources/tile_purple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/tile_purple.png -------------------------------------------------------------------------------- /resources/tile_red.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/tile_red.png -------------------------------------------------------------------------------- /resources/tile_yellow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nmayorov/tetris-game/15561a0cdfd7f3ad95641ec7b5e8e499d28f83ab/resources/tile_yellow.png -------------------------------------------------------------------------------- /src/game.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include "render.h" 6 | 7 | const GLfloat kTileSize = 32; 8 | const GLint kBoardNumRows = 20; 9 | const GLint kBoardNumCols = 10; 10 | const GLfloat kBoardWidth = kBoardNumCols * kTileSize; 11 | const GLfloat kBoardHeight = kBoardNumRows * kTileSize; 12 | const GLfloat kMargin = 10; 13 | const GLfloat kHudWidth = 160; 14 | const GLfloat kWidth = 3 * kMargin + kBoardWidth + kHudWidth; 15 | const GLfloat kHeight = 2 * kMargin + kBoardHeight; 16 | const GLfloat kHudX = kMargin; 17 | const GLfloat kHudY = kMargin; 18 | const GLfloat kBoardX = 2 * kMargin + kHudWidth; 19 | const GLfloat kBoardY = kMargin; 20 | const GLfloat kHudPieceBoxHeight = 2.5f * kTileSize; 21 | const GLuint kFontSize = 18; 22 | 23 | const double kGameTimeStep = 0.005; 24 | const double kFps = 30; 25 | const double kSecondsPerFrame = 1.0 / kFps; 26 | 27 | Board board(kBoardNumRows, kBoardNumCols); 28 | Tetris* tetris; 29 | 30 | enum GameState { kGameStart, kGameRun, kGamePaused, kGameOver }; 31 | GameState gameState = kGameStart; 32 | 33 | bool softDrop = false; 34 | bool moveRight = false; 35 | bool moveLeft = false; 36 | int startLevel = 1; 37 | 38 | GLFWwindow* setupGlContext() { 39 | if (!glfwInit()) { 40 | return nullptr; 41 | } 42 | 43 | glfwWindowHint(GLFW_RESIZABLE, GL_FALSE); 44 | glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 3); 45 | glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 2); 46 | glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE); 47 | glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE); 48 | GLFWwindow* window = glfwCreateWindow(kWidth, kHeight, "TETRIS", nullptr, nullptr); 49 | 50 | if (window == nullptr) { 51 | glfwTerminate(); 52 | } 53 | 54 | glfwMakeContextCurrent(window); 55 | glewInit(); 56 | 57 | return window; 58 | } 59 | 60 | void keyCallback(GLFWwindow* /*window*/, int key, int /*scancode*/, int action, int /*mods*/) { 61 | switch (gameState) { 62 | case kGameRun: 63 | if (action == GLFW_PRESS) { 64 | switch (key) { 65 | case GLFW_KEY_Z: tetris->rotate(Rotation::kLeft); break; 66 | case GLFW_KEY_X: tetris->rotate(Rotation::kRight); break; 67 | case GLFW_KEY_SPACE: tetris->hardDrop(); break; 68 | case GLFW_KEY_C: tetris->hold(); break; 69 | case GLFW_KEY_LEFT: moveLeft = true; break; 70 | case GLFW_KEY_RIGHT: moveRight = true; break; 71 | case GLFW_KEY_DOWN: softDrop = true; break; 72 | case GLFW_KEY_ESCAPE: gameState = kGamePaused; 73 | } 74 | } else if (action == GLFW_RELEASE) { 75 | switch (key) { 76 | case GLFW_KEY_LEFT: moveLeft = false; break; 77 | case GLFW_KEY_RIGHT: moveRight = false; break; 78 | case GLFW_KEY_DOWN: softDrop = false; 79 | } 80 | } 81 | break; 82 | case kGamePaused: 83 | if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) { 84 | gameState = kGameRun; 85 | } else if (key == GLFW_KEY_ENTER && action == GLFW_PRESS) { 86 | gameState = kGameStart; 87 | } 88 | break; 89 | case kGameOver: 90 | if (key == GLFW_KEY_ENTER && action == GLFW_PRESS) { 91 | gameState = kGameStart; 92 | } 93 | break; 94 | case kGameStart: 95 | if (key == GLFW_KEY_ENTER && action == GLFW_PRESS) { 96 | moveRight = false; 97 | moveLeft = false; 98 | softDrop = false; 99 | tetris->restart(startLevel); 100 | gameState = kGameRun; 101 | } else if (key == GLFW_KEY_UP && action == GLFW_PRESS) { 102 | startLevel = std::min(15, startLevel + 1); 103 | } else if (key == GLFW_KEY_DOWN && action == GLFW_PRESS) { 104 | startLevel = std::max(1, startLevel - 1); 105 | } 106 | } 107 | } 108 | 109 | void windowFocusCallback(GLFWwindow* /*window*/, int focused) { 110 | if (!focused && gameState == kGameRun) { 111 | gameState = kGamePaused; 112 | } 113 | } 114 | 115 | int main() { 116 | GLFWwindow* window = setupGlContext(); 117 | 118 | if (window == nullptr) { 119 | return EXIT_FAILURE; 120 | } 121 | 122 | auto font = loadFont("resources/kenvector_future.ttf", kFontSize); 123 | 124 | std::vector tileTextures, ghostTextures; 125 | std::vector colors = {"cyan", "blue", "orange", "yellow", "green", "purple", "red"}; 126 | for (int color = kCyan; color <= kRed; ++color) { 127 | std::string path = "resources/tile_" + colors[color] + ".png"; 128 | tileTextures.push_back(loadRgbaTexture(path)); 129 | path = "resources/contour_" + colors[color] + ".png"; 130 | ghostTextures.push_back(loadRgbaTexture(path)); 131 | } 132 | 133 | Texture keyArrowLeft = loadRgbaTexture("resources/Keyboard_White_Arrow_Left.png"); 134 | Texture keyArrowRight = loadRgbaTexture("resources/Keyboard_White_Arrow_Right.png"); 135 | Texture keyArrowDown = loadRgbaTexture("resources/Keyboard_White_Arrow_Down.png"); 136 | Texture keyArrowUp = loadRgbaTexture("resources/Keyboard_White_Arrow_Up.png"); 137 | Texture keyZ = loadRgbaTexture("resources/Keyboard_White_Z.png"); 138 | Texture keyX = loadRgbaTexture("resources/Keyboard_White_X.png"); 139 | Texture keyC = loadRgbaTexture("resources/Keyboard_White_C.png"); 140 | Texture keySpace = loadRgbaTexture("resources/Keyboard_White_Space.png"); 141 | Texture keyEsc = loadRgbaTexture("resources/Keyboard_White_Esc.png"); 142 | Texture keyEnter = loadRgbaTexture("resources/Keyboard_White_Enter.png"); 143 | 144 | glEnable(GL_BLEND); 145 | glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); 146 | 147 | glm::mat4 projection = glm::ortho(0.0f, kWidth, kHeight, 0.0f, -1.0f, 1.0f); 148 | 149 | tetris = new Tetris(board, kGameTimeStep, static_cast(glfwGetTime() * 1e4)); 150 | 151 | glfwSetKeyCallback(window, keyCallback); 152 | glfwSetWindowFocusCallback(window, windowFocusCallback); 153 | 154 | TextRenderer textRenderer(projection, font); 155 | 156 | GLfloat letterHeight = textRenderer.computeHeight("A"); 157 | GLfloat letterWidth = textRenderer.computeWidth("A"); 158 | 159 | SpriteRenderer spriteRenderer(projection); 160 | PieceRenderer pieceRenderer(kTileSize, tileTextures, spriteRenderer); 161 | PieceRenderer ghostRenderer(kTileSize, ghostTextures, spriteRenderer); 162 | BoardRenderer boardRenderer(projection, kTileSize, kBoardX, kBoardY, kBoardNumRows, kBoardNumCols, tileTextures, 163 | spriteRenderer, pieceRenderer, ghostRenderer); 164 | 165 | double timeLastGameUpdate = 0; 166 | double timeLastRender = 0; 167 | 168 | while (!glfwWindowShouldClose(window)) { 169 | glfwPollEvents(); 170 | 171 | std::this_thread::sleep_for(std::chrono::duration(timeLastGameUpdate + kGameTimeStep - glfwGetTime())); 172 | if (gameState == kGameRun) { 173 | tetris->update(softDrop, moveRight, moveLeft); 174 | if (tetris->isGameOver()) { 175 | gameState = kGameOver; 176 | } 177 | } 178 | timeLastGameUpdate = glfwGetTime(); 179 | 180 | double time = glfwGetTime(); 181 | if (time - timeLastRender >= kSecondsPerFrame) { 182 | timeLastRender = time; 183 | 184 | glClearColor(1, 1, 1, 1); 185 | glClear(GL_COLOR_BUFFER_BIT); 186 | 187 | textRenderer.renderCentered("NEXT", kHudX, kHudY, kHudWidth, kColorBlack); 188 | textRenderer.renderCentered("HOLD", kHudX, kHudY + 2 * kHudPieceBoxHeight, kHudWidth, kColorBlack); 189 | 190 | if (gameState != kGameStart) { 191 | pieceRenderer.renderInitialShapeCentered( 192 | tetris->nextPiece(), kHudX, std::round(kHudY + 1.5f * letterHeight), kHudWidth, kHudPieceBoxHeight); 193 | 194 | pieceRenderer.renderInitialShapeCentered( 195 | tetris->heldPiece(), kHudX, std::round(kHudY + 2 * kHudPieceBoxHeight + 1.5f * letterHeight), 196 | kHudWidth, kHudPieceBoxHeight); 197 | } 198 | 199 | int level, linesCleared, score; 200 | if (gameState == kGameStart) { 201 | level = startLevel; 202 | linesCleared = 0; 203 | score = 0; 204 | } else { 205 | level = tetris->level(); 206 | linesCleared = tetris->linesCleared(); 207 | score = tetris->score(); 208 | } 209 | 210 | GLfloat y = 0.6f * kHeight; 211 | textRenderer.renderCentered("LEVEL", kHudX, y, kHudWidth, kColorBlack); 212 | y += 1.4f * letterHeight; 213 | textRenderer.renderCentered(std::to_string(level), kHudX, y, kHudWidth, kColorBlack); 214 | 215 | y += 2.5f * letterHeight; 216 | textRenderer.renderCentered("LINES", kHudX, y, kHudWidth, kColorBlack); 217 | y += 1.4f * letterHeight; 218 | textRenderer.renderCentered(std::to_string(linesCleared), kHudX, y, kHudWidth, kColorBlack); 219 | 220 | y += 2.5f * letterHeight; 221 | textRenderer.renderCentered("SCORE", kHudX, y, kHudWidth, kColorBlack); 222 | y += 1.4f * letterHeight; 223 | textRenderer.renderCentered(std::to_string(score), kHudX, y, kHudWidth, kColorBlack); 224 | 225 | boardRenderer.renderBackground(); 226 | 227 | switch (gameState) { 228 | case kGameRun: 229 | boardRenderer.renderTiles(board); 230 | if (tetris->isPausedForLinesClear()) { 231 | boardRenderer.playLinesClearAnimation(board, tetris->linesClearPausePercent()); 232 | } else { 233 | boardRenderer.renderGhost(board.piece(), board.ghostRow(), board.pieceCol()); 234 | boardRenderer.renderPiece(board.piece(), board.pieceRow(), board.pieceCol(), tetris->lockPercent()); 235 | } 236 | break; 237 | case kGamePaused: { 238 | boardRenderer.renderTiles(board, 0.4); 239 | boardRenderer.renderPiece(board.piece(), board.pieceRow(), board.pieceCol(), 0, 0.4); 240 | 241 | GLfloat y = kBoardY + 0.38f * kBoardHeight; 242 | 243 | textRenderer.renderCentered("PAUSED", kBoardX, y, kBoardWidth, kColorWhite); 244 | 245 | y = kBoardY + 0.5f * kBoardHeight; 246 | GLfloat xName = kBoardX + 0.1f * kBoardWidth; 247 | GLfloat xIcon = kBoardX + 0.9f * kBoardWidth; 248 | 249 | GLfloat dyAlignment = 0.5f * (keyArrowLeft.height - letterHeight); 250 | textRenderer.render("CONTINUE", xName, y, kColorWhite); 251 | spriteRenderer.render(keyEsc, xIcon - keyEsc.width, y - dyAlignment, keyEsc.width, keyEsc.height); 252 | 253 | y += 5.5f * letterHeight; 254 | textRenderer.render("START SCREEN", xName, y, kColorWhite); 255 | dyAlignment = 0.75f * (keyEnter.height - letterHeight); 256 | spriteRenderer.render(keyEnter, xIcon - keyEnter.width, y - dyAlignment, keyEnter.width, 257 | keyEnter.height); 258 | 259 | break; 260 | } 261 | case kGameStart: { 262 | GLfloat y = kBoardY + 0.05f * kBoardHeight; 263 | 264 | textRenderer.renderCentered("CONTROLS", kBoardX, y, kBoardWidth, kColorWhite); 265 | 266 | y += 4 * letterHeight; 267 | 268 | GLfloat xName = kBoardX + 0.1f * kBoardWidth; 269 | GLfloat xIcon = kBoardX + 0.9f * kBoardWidth; 270 | GLfloat dyAlignment = 0.5f * (keyArrowLeft.height - letterHeight); 271 | GLfloat dyBetweenRows = 3.8f * letterHeight; 272 | 273 | textRenderer.render("MOVE", xName, y, kColorWhite); 274 | 275 | GLfloat iconsWidth = keyArrowLeft.width + keyArrowRight.width; 276 | spriteRenderer.render(keyArrowLeft, xIcon - iconsWidth, y - dyAlignment, keyArrowLeft.width, 277 | keyArrowLeft.height); 278 | spriteRenderer.render(keyArrowRight, xIcon - iconsWidth + keyArrowLeft.width, y - dyAlignment, 279 | keyArrowLeft.width, keyArrowLeft.height); 280 | 281 | y += dyBetweenRows; 282 | textRenderer.render("ROTATE", xName, y, kColorWhite); 283 | iconsWidth = keyZ.width + keyX.width; 284 | spriteRenderer.render(keyZ, xIcon - iconsWidth, y - dyAlignment, keyZ.width, keyZ.height); 285 | spriteRenderer.render(keyX, xIcon - iconsWidth + keyZ.width, y - dyAlignment, keyX.width, keyZ.height); 286 | 287 | y += dyBetweenRows; 288 | textRenderer.render("SOFT DROP", xName, y, kColorWhite); 289 | spriteRenderer.render(keyArrowDown, xIcon - keyArrowDown.width, y - dyAlignment, keyArrowDown.width, 290 | keyArrowDown.height); 291 | 292 | y += dyBetweenRows; 293 | textRenderer.render("HARD DROP", xName, y, kColorWhite); 294 | spriteRenderer.render(keySpace, xIcon - keySpace.width, y - dyAlignment, keySpace.width, 295 | keySpace.height); 296 | 297 | y += dyBetweenRows; 298 | textRenderer.render("HOLD", xName, y, kColorWhite); 299 | spriteRenderer.render(keyC, xIcon - keyC.width, y - dyAlignment, keyC.width, keyC.height); 300 | 301 | y += dyBetweenRows; 302 | textRenderer.render("PAUSE", xName, y, kColorWhite); 303 | spriteRenderer.render(keyEsc, xIcon - keyEsc.width, y - dyAlignment, keyEsc.width, keyEsc.height); 304 | 305 | y = kBoardY + 0.585f * kBoardHeight; 306 | GLfloat lineWidth = textRenderer.computeWidth("USE") + keyArrowDown.width + keyArrowUp.width + 307 | 2 * letterWidth + textRenderer.computeWidth("TO SELECT"); 308 | GLfloat x = kBoardX + 0.5f * (kBoardWidth - lineWidth); 309 | 310 | textRenderer.render("USE", x, y, kColorWhite); 311 | x += textRenderer.computeWidth("USE") + letterWidth; 312 | spriteRenderer.render(keyArrowDown, x, y - dyAlignment, keyArrowDown.width, keyArrowDown.height); 313 | x += keyArrowDown.width; 314 | spriteRenderer.render(keyArrowUp, x, y - dyAlignment, keyArrowUp.width, keyArrowUp.height); 315 | x += keyArrowUp.width + letterWidth; 316 | textRenderer.render("TO SELECT", x, y, kColorWhite); 317 | y += dyBetweenRows; 318 | textRenderer.renderCentered("THE LEVEL", kBoardX, y, kBoardWidth, kColorWhite); 319 | 320 | y = kBoardY + 0.8f * kBoardHeight; 321 | lineWidth = textRenderer.computeWidth("PRESS") + keyEnter.width + 2 * letterWidth + 322 | textRenderer.computeWidth("TO START"); 323 | x = kBoardX + 0.5f * (kBoardWidth - lineWidth); 324 | dyAlignment = 0.7f * (keyEnter.height - letterHeight); 325 | 326 | textRenderer.render("PRESS", x, y, kColorWhite); 327 | x += textRenderer.computeWidth("PRESS") + letterWidth; 328 | spriteRenderer.render(keyEnter, x, y - dyAlignment, keyEnter.width, keyEnter.height); 329 | x += keyEnter.width + letterWidth; 330 | textRenderer.render("TO START", x, y, kColorWhite); 331 | break; 332 | } 333 | case kGameOver: { 334 | boardRenderer.renderTiles(board, 0.4); 335 | 336 | GLfloat y = kBoardY + 0.4f * kBoardHeight; 337 | textRenderer.renderCentered("GAME OVER", 2 * kMargin + kHudWidth, y, kBoardWidth, kColorWhite); 338 | 339 | y = kBoardY + 0.53f * kBoardHeight; 340 | GLfloat lineWidth = textRenderer.computeWidth("PRESS") + keyEnter.width + 2 * letterWidth + 341 | textRenderer.computeWidth("TO"); 342 | GLfloat x = kBoardX + 0.5f * (kBoardWidth - lineWidth); 343 | GLfloat dyAlignment = 0.7f * (keyEnter.height - letterHeight); 344 | 345 | textRenderer.render("PRESS", x, y, kColorWhite); 346 | x += textRenderer.computeWidth("PRESS") + letterWidth; 347 | spriteRenderer.render(keyEnter, x, y - dyAlignment, keyEnter.width, keyEnter.height); 348 | x += keyEnter.width + letterWidth; 349 | textRenderer.render("TO", x, y, kColorWhite); 350 | y += 3.5 * letterHeight; 351 | textRenderer.renderCentered("CONTINUE", kBoardX, y, kBoardWidth, kColorWhite); 352 | } 353 | } 354 | glfwSwapBuffers(window); 355 | } 356 | } 357 | 358 | return EXIT_SUCCESS; 359 | } 360 | -------------------------------------------------------------------------------- /src/render.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include "render.h" 3 | 4 | const char* kColoredPrimitiveVertexShader = R"glsl( 5 | # version 330 core 6 | 7 | layout (location = 0) in vec2 position; 8 | uniform mat4 projection; 9 | 10 | void main() { 11 | gl_Position = projection * vec4(position, 0, 1); 12 | } 13 | 14 | )glsl"; 15 | 16 | const char* kColoredPrimitiveFragmentShader = R"glsl( 17 | # version 330 core 18 | 19 | uniform vec3 inColor; 20 | out vec4 color; 21 | 22 | void main() { 23 | color = vec4(inColor, 1); 24 | } 25 | 26 | )glsl"; 27 | 28 | const char* kTileVertexShader = R"glsl( 29 | # version 330 core 30 | 31 | layout (location = 0) in vec2 position; 32 | layout (location = 1) in vec2 texCoord; 33 | 34 | out vec2 texCoordFragment; 35 | 36 | uniform vec2 shift; 37 | uniform vec2 scale = vec2(1, 1); 38 | uniform mat4 projection; 39 | 40 | void main() { 41 | gl_Position = projection * vec4(scale * position + shift, 0, 1); 42 | texCoordFragment = texCoord; 43 | } 44 | )glsl"; 45 | 46 | const char* kTileFragmentShader = R"glsl( 47 | 48 | # version 330 core 49 | 50 | in vec2 texCoordFragment; 51 | out vec4 color; 52 | 53 | uniform sampler2D sampler; 54 | uniform vec3 mixColor; 55 | uniform float mixCoeff = 0; 56 | uniform float alphaMultiplier = 1; 57 | 58 | void main() { 59 | color = mix(texture(sampler, texCoordFragment), vec4(mixColor, 1), mixCoeff); 60 | color.a *= alphaMultiplier; 61 | } 62 | 63 | )glsl"; 64 | 65 | const char* kGlyphVertexShader = R"glsl( 66 | 67 | #version 330 core 68 | 69 | layout (location = 0) in vec2 position; 70 | layout (location = 1) in vec2 texCoord; 71 | 72 | out vec2 texCoordFragment; 73 | 74 | uniform mat4 projection; 75 | 76 | void main() { 77 | gl_Position = projection * vec4(position, 0, 1); 78 | texCoordFragment = texCoord; 79 | } 80 | 81 | )glsl"; 82 | 83 | const char* kGlyphFragmentShader = R"glsl( 84 | 85 | #version 330 core 86 | 87 | in vec2 texCoordFragment; 88 | out vec4 color; 89 | 90 | uniform vec3 textColor; 91 | uniform sampler2D glyph; 92 | 93 | void main() { 94 | float alpha = texture(glyph, texCoordFragment).r; 95 | color = vec4(textColor, alpha); 96 | } 97 | 98 | )glsl"; 99 | 100 | const glm::vec3 kColorBlack(0, 0, 0); 101 | const glm::vec3 kColorWhite(1, 1, 1); 102 | 103 | SpriteRenderer::SpriteRenderer(const glm::mat4& projection) : shader_(kTileVertexShader, kTileFragmentShader) { 104 | GLfloat vertices[] = {0, 0, 0, 1, 0, 1, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0}; 105 | 106 | GLuint vbo; 107 | glGenBuffers(1, &vbo); 108 | 109 | glGenVertexArrays(1, &vao_); 110 | glBindVertexArray(vao_); 111 | glBindBuffer(GL_ARRAY_BUFFER, vbo); 112 | glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW); 113 | glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*) 0); 114 | glEnableVertexAttribArray(0); 115 | glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*) (2 * sizeof(GLfloat))); 116 | glEnableVertexAttribArray(1); 117 | glBindVertexArray(0); 118 | 119 | shader_.use(); 120 | shader_.setMat4("projection", projection); 121 | } 122 | 123 | void SpriteRenderer::render(const Texture& texture, GLfloat x, GLfloat y, GLfloat width, GLfloat height, 124 | GLfloat mixCoeff, const glm::vec3& mixColor, GLfloat alphaMultiplier) { 125 | texture.bind(); 126 | shader_.use(); 127 | shader_.setVec2("shift", glm::vec2(x, y)); 128 | shader_.setVec2("scale", glm::vec2(width, height)); 129 | shader_.setFloat("mixCoeff", mixCoeff); 130 | shader_.setVec3("mixColor", mixColor); 131 | shader_.setFloat("alphaMultiplier", alphaMultiplier); 132 | glBindVertexArray(vao_); 133 | glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); 134 | } 135 | 136 | void PieceRenderer::renderShape(const Piece& piece, GLfloat x, GLfloat y, GLfloat mixCoeff, const glm::vec3& mixColor, 137 | GLfloat alphaMultiplier, int startRow) const { 138 | if (piece.kind() == kNone) { 139 | return; 140 | } 141 | 142 | Texture texture = textures_.at(piece.color()); 143 | 144 | int index = startRow * piece.bBoxSide(); 145 | auto shape = piece.shape(); 146 | for (int row = startRow; row < piece.bBoxSide(); ++row) { 147 | for (int col = 0; col < piece.bBoxSide(); ++col) { 148 | if (shape[index] != kEmpty) { 149 | spriteRenderer_.render(texture, x + col * tileSize_, y + row * tileSize_, tileSize_, tileSize_, 150 | mixCoeff, mixColor, alphaMultiplier); 151 | } 152 | 153 | ++index; 154 | } 155 | } 156 | } 157 | 158 | void PieceRenderer::renderInitialShape(const Piece& piece, GLfloat x, GLfloat y) const { 159 | if (piece.kind() == kNone) { 160 | return; 161 | } 162 | 163 | Texture texture = textures_.at(piece.color()); 164 | 165 | int index = 0; 166 | auto shape = piece.initialShape(); 167 | for (int row = 0; row < piece.nRows(); ++row) { 168 | for (int col = 0; col < piece.nCols(); ++col) { 169 | if (shape[index] != kEmpty) { 170 | spriteRenderer_.render(texture, x + col * tileSize_, y + row * tileSize_, tileSize_, tileSize_); 171 | } 172 | 173 | ++index; 174 | } 175 | } 176 | } 177 | 178 | void PieceRenderer::renderInitialShapeCentered(const Piece& piece, GLfloat x, GLfloat y, GLfloat width, 179 | GLfloat height) const { 180 | GLfloat pieceWidth = tileSize_ * piece.nCols(); 181 | GLfloat pieceHeight = tileSize_ * piece.nRows(); 182 | GLfloat xShift = 0.5f * (width - pieceWidth); 183 | GLfloat yShift = 0.5f * (height - pieceHeight); 184 | 185 | renderInitialShape(piece, x + xShift, y + yShift); 186 | } 187 | 188 | const glm::vec3 kBackgroundColor(0.05, 0.05, 0.05); 189 | const glm::vec3 kGridColor(0.2, 0.2, 0.2); 190 | 191 | BoardRenderer::BoardRenderer(const glm::mat4& projection, GLfloat tileSize, GLfloat x, GLfloat y, int nRows, int nCols, 192 | const std::vector& tileTextures, SpriteRenderer& spriteRenderer, 193 | PieceRenderer& pieceRenderer, PieceRenderer& ghostRenderer) 194 | : tileSize_(tileSize) 195 | , x_(x) 196 | , y_(y) 197 | , nRows_(nRows) 198 | , nCols_(nCols) 199 | , tileTextures_(tileTextures) 200 | , pieceRenderer_(pieceRenderer) 201 | , ghostRenderer_(ghostRenderer) 202 | , spriteRenderer_(spriteRenderer) 203 | , backgroundShader_(kColoredPrimitiveVertexShader, kColoredPrimitiveFragmentShader) { 204 | backgroundShader_.use(); 205 | backgroundShader_.setMat4("projection", projection); 206 | 207 | GLfloat width = nCols_ * tileSize_; 208 | GLfloat height = nRows_ * tileSize_; 209 | 210 | verticesBackground_ = {x_, y_, x_, y_ + height, x_ + width, y_, x_ + width, y_ + height}; 211 | 212 | GLfloat yGrid = y_; 213 | for (int row = 0; row < nRows_ + 1; ++row) { 214 | verticesBackground_.push_back(x_); 215 | verticesBackground_.push_back(yGrid); 216 | verticesBackground_.push_back(x_ + width); 217 | verticesBackground_.push_back(yGrid); 218 | yGrid += tileSize_; 219 | } 220 | 221 | GLfloat xGrid = x_; 222 | for (int col = 0; col < nCols_ + 1; ++col) { 223 | verticesBackground_.push_back(xGrid); 224 | verticesBackground_.push_back(y_); 225 | verticesBackground_.push_back(xGrid); 226 | verticesBackground_.push_back(y_ + height); 227 | xGrid += tileSize_; 228 | } 229 | 230 | GLuint vbo; 231 | glGenBuffers(1, &vbo); 232 | glGenVertexArrays(1, &vaoBackground_); 233 | 234 | glBindVertexArray(vaoBackground_); 235 | 236 | glBindBuffer(GL_ARRAY_BUFFER, vbo); 237 | glBufferData(GL_ARRAY_BUFFER, verticesBackground_.size() * sizeof(GLfloat), verticesBackground_.data(), 238 | GL_STATIC_DRAW); 239 | glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 2 * sizeof(GLfloat), (GLvoid*) 0); 240 | glEnableVertexAttribArray(0); 241 | glBindVertexArray(0); 242 | } 243 | 244 | void BoardRenderer::renderBackground() const { 245 | backgroundShader_.use(); 246 | glBindVertexArray(vaoBackground_); 247 | 248 | backgroundShader_.setVec3("inColor", kBackgroundColor); 249 | glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); 250 | 251 | backgroundShader_.setVec3("inColor", kGridColor); 252 | glDrawArrays(GL_LINES, 4, 2 * (nRows_ + nCols_ + 2)); 253 | } 254 | 255 | void BoardRenderer::renderTiles(const Board& board, GLfloat alphaMultiplier) const { 256 | int row, col; 257 | GLfloat y, x; 258 | GLfloat y0 = y_; 259 | GLfloat x0 = x_; 260 | 261 | for (row = 0, y = y0; row < board.nRows; ++row, y += tileSize_) { 262 | for (col = 0, x = x0; col < board.nCols; ++col, x += tileSize_) { 263 | TileColor tile = board.tileAt(row, col); 264 | if (tile == kEmpty) { 265 | continue; 266 | } 267 | 268 | spriteRenderer_.render(tileTextures_.at(tile), x, y, tileSize_, tileSize_, 0, glm::vec3(), alphaMultiplier); 269 | } 270 | } 271 | } 272 | 273 | void BoardRenderer::renderPiece(const Piece& piece, int row, int col, double lockPercent, 274 | double alphaMultiplier) const { 275 | int startRow = std::max(0, -row); 276 | GLfloat mixCoeff = 0.5f * sin(M_PI_2 * lockPercent); 277 | pieceRenderer_.renderShape(piece, x_ + col * tileSize_, y_ + row * tileSize_, mixCoeff, kColorBlack, 278 | alphaMultiplier, startRow); 279 | } 280 | 281 | void BoardRenderer::renderGhost(const Piece& piece, int ghostRow, int col) const { 282 | int startRow = std::max(0, -ghostRow); 283 | ghostRenderer_.renderShape(piece, x_ + col * tileSize_, y_ + ghostRow * tileSize_, 0, kColorBlack, 0.7, startRow); 284 | } 285 | 286 | void BoardRenderer::playLinesClearAnimation(const Board& board, double percentFinished) const { 287 | double t = 0.3; 288 | 289 | glm::vec3 mixColor; 290 | GLfloat mixCoeff; 291 | 292 | if (percentFinished < t) { 293 | double s = sin(M_PI * percentFinished / t); 294 | mixColor = kColorWhite; 295 | mixCoeff = 0.8f * s; 296 | } else { 297 | mixColor = kBackgroundColor; 298 | mixCoeff = (percentFinished - t) / (1 - t); 299 | } 300 | 301 | for (int row : board.linesToClear()) { 302 | for (int col = 0; col < nCols_; ++col) { 303 | GLfloat x = x_ + col * tileSize_; 304 | GLfloat y = y_ + row * tileSize_; 305 | spriteRenderer_.render(tileTextures_.at(board.tileAt(row, col)), x, y, tileSize_, tileSize_, mixCoeff, 306 | mixColor, 1); 307 | } 308 | } 309 | } 310 | 311 | TextRenderer::TextRenderer(const glm::mat4& projection, const std::vector& font) 312 | : font_(font), shader_(kGlyphVertexShader, kGlyphFragmentShader) { 313 | shader_.use(); 314 | shader_.setMat4("projection", projection); 315 | 316 | glGenVertexArrays(1, &vao_); 317 | glGenBuffers(1, &vbo_); 318 | glBindVertexArray(vao_); 319 | glBindBuffer(GL_ARRAY_BUFFER, vbo_); 320 | glBufferData(GL_ARRAY_BUFFER, 4 * 4 * sizeof(GLfloat), NULL, GL_DYNAMIC_DRAW); 321 | glVertexAttribPointer(0, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*) 0); 322 | glEnableVertexAttribArray(0); 323 | glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 4 * sizeof(GLfloat), (GLvoid*) (2 * sizeof(GLfloat))); 324 | glEnableVertexAttribArray(1); 325 | glBindBuffer(GL_ARRAY_BUFFER, 0); 326 | glBindVertexArray(0); 327 | } 328 | 329 | void TextRenderer::render(const std::string& text, GLfloat x, GLfloat y, glm::vec3 color) const { 330 | shader_.use(); 331 | shader_.setVec3("textColor", color); 332 | glBindVertexArray(vao_); 333 | 334 | x = std::round(x); 335 | y = std::round(y); 336 | 337 | for (char c : text) { 338 | Glyph glyph = font_.at(c); 339 | 340 | GLfloat xBbox = x + glyph.bearing.x; 341 | GLfloat yBbox = y + (font_.at('A').bearing.y - glyph.bearing.y); 342 | 343 | GLfloat width = glyph.texture.width; 344 | GLfloat height = glyph.texture.height; 345 | 346 | GLfloat vertices[] = {xBbox, yBbox, 0, 0, xBbox, yBbox + height, 0, 1, 347 | xBbox + width, yBbox, 1, 0, xBbox + width, yBbox + height, 1, 1}; 348 | 349 | glyph.texture.bind(); 350 | glBindBuffer(GL_ARRAY_BUFFER, vbo_); 351 | glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertices), vertices); 352 | glBindBuffer(GL_ARRAY_BUFFER, 0); 353 | glDrawArrays(GL_TRIANGLE_STRIP, 0, 4); 354 | 355 | x += glyph.advance; 356 | } 357 | } 358 | 359 | void TextRenderer::renderCentered(const std::string& text, GLfloat x, GLfloat y, GLfloat width, 360 | const glm::vec3& color) const { 361 | GLfloat textWidth = computeWidth(text); 362 | GLfloat shift = 0.5f * (width - textWidth); 363 | render(text, std::round(x + shift), std::round(y), color); 364 | } 365 | 366 | GLint TextRenderer::computeWidth(const std::string& text) const { 367 | GLint width = 0; 368 | for (auto c = text.begin(); c != text.end() - 1; ++c) { 369 | width += font_.at(*c).advance; 370 | } 371 | width += font_.at(text.back()).texture.width; 372 | return width; 373 | } 374 | 375 | GLint TextRenderer::computeHeight(const std::string& text) const { 376 | GLint height = 0; 377 | for (char c : text) { 378 | Glyph glyph = font_.at(c); 379 | auto textureHeight = static_cast(glyph.texture.height); 380 | height = std::max(height, font_.at('H').bearing.y - glyph.bearing.y + textureHeight); 381 | } 382 | return height; 383 | } 384 | -------------------------------------------------------------------------------- /src/render.h: -------------------------------------------------------------------------------- 1 | #ifndef TETRIS_RENDER_H 2 | #define TETRIS_RENDER_H 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | #include "tetris.h" 13 | #include "util.h" 14 | 15 | extern const glm::vec3 kColorBlack; 16 | extern const glm::vec3 kColorWhite; 17 | 18 | class SpriteRenderer { 19 | public: 20 | explicit SpriteRenderer(const glm::mat4& projection); 21 | 22 | void render(const Texture& texture, GLfloat x, GLfloat y, GLfloat width, GLfloat height, GLfloat mixCoeff = 0, 23 | const glm::vec3& mixColor = kColorBlack, GLfloat alphaMultiplier = 1); 24 | 25 | private: 26 | Shader shader_; 27 | GLuint vao_ = 0; 28 | }; 29 | 30 | class PieceRenderer { 31 | public: 32 | PieceRenderer(GLfloat tileSize, const std::vector& textures, SpriteRenderer& spriteRenderer) 33 | : tileSize_(tileSize), textures_(textures), spriteRenderer_(spriteRenderer) {} 34 | 35 | void renderShape(const Piece& piece, GLfloat x, GLfloat y, GLfloat mixCoeff = 0, 36 | const glm::vec3& mixColor = kColorBlack, GLfloat alphaMultiplier = 1, int startRow = 0) const; 37 | void renderInitialShape(const Piece& piece, GLfloat x, GLfloat y) const; 38 | void renderInitialShapeCentered(const Piece& piece, GLfloat x, GLfloat y, GLfloat width, GLfloat height) const; 39 | 40 | private: 41 | GLfloat tileSize_; 42 | std::vector textures_; 43 | SpriteRenderer& spriteRenderer_; 44 | }; 45 | 46 | class BoardRenderer { 47 | public: 48 | BoardRenderer(const glm::mat4& projection, GLfloat tileSize, GLfloat x, GLfloat y, int nRows, int nCols, 49 | const std::vector& tileTextures, SpriteRenderer& spriteRenderer, 50 | PieceRenderer& pieceRenderer, PieceRenderer& ghostRenderer); 51 | 52 | void renderBackground() const; 53 | void renderTiles(const Board& board, GLfloat alphaMultiplier = 1) const; 54 | void renderPiece(const Piece& piece, int row, int col, double lockPercent, double alphaMultiplier = 1) const; 55 | void renderGhost(const Piece& piece, int ghostRow, int col) const; 56 | void playLinesClearAnimation(const Board& board, double percentFinished) const; 57 | 58 | private: 59 | GLfloat tileSize_; 60 | GLfloat x_, y_; 61 | int nRows_, nCols_; 62 | 63 | const std::vector tileTextures_; 64 | 65 | PieceRenderer &pieceRenderer_, ghostRenderer_; 66 | SpriteRenderer& spriteRenderer_; 67 | 68 | Shader backgroundShader_; 69 | std::vector verticesBackground_; 70 | GLuint vaoBackground_ = 0; 71 | }; 72 | 73 | class TextRenderer { 74 | public: 75 | TextRenderer(const glm::mat4& projection, const std::vector& font); 76 | 77 | void render(const std::string& text, GLfloat x, GLfloat y, glm::vec3 color) const; 78 | void renderCentered(const std::string& text, GLfloat x, GLfloat y, GLfloat width, const glm::vec3& color) const; 79 | 80 | GLint computeWidth(const std::string& text) const; 81 | GLint computeHeight(const std::string& text) const; 82 | 83 | private: 84 | std::vector font_; 85 | Shader shader_; 86 | GLuint vao_ = 0; 87 | GLuint vbo_ = 0; 88 | }; 89 | 90 | #endif // TETRIS_RENDER_H 91 | -------------------------------------------------------------------------------- /src/tetris.cpp: -------------------------------------------------------------------------------- 1 | #include "tetris.h" 2 | 3 | const int Piece::kNumStates_ = 4; 4 | 5 | const std::vector>> Piece::kKicksIRight_ = { 6 | {{0, 0}, {0, -2}, {0, 1}, {1, -2}, {-2, 1}}, 7 | {{0, 0}, {0, -1}, {0, 2}, {-2, -1}, {1, 2}}, 8 | {{0, 0}, {0, 2}, {0, -1}, {-1, 2}, {2, -1}}, 9 | {{0, 0}, {0, 1}, {0, -2}, {2, 1}, {-1, -2}}}; 10 | 11 | const std::vector>> Piece::kKicksILeft_ = {{{0, 0}, {0, -1}, {0, 2}, {-2, -1}, {1, 2}}, 12 | {{0, 0}, {0, 2}, {0, -1}, {-1, 2}, {2, -1}}, 13 | {{0, 0}, {0, 1}, {0, -2}, {2, 1}, {-1, -2}}, 14 | {{0, 0}, {0, -2}, {0, 1}, {1, -2}, {-2, 1}}}; 15 | 16 | const std::vector>> Piece::kKicksOtherRight_ = { 17 | {{0, 0}, {0, 1}, {-1, -1}, {2, 0}, {2, -1}}, 18 | {{0, 0}, {0, 1}, {1, 1}, {-2, 0}, {-2, 1}}, 19 | {{0, 0}, {0, 1}, {-1, 1}, {2, 0}, {2, 1}}, 20 | {{0, 0}, {0, -1}, {1, -1}, {-2, 0}, {-2, -1}}}; 21 | 22 | const std::vector>> Piece::kicksOtherLeft_ = { 23 | {{0, 0}, {0, 1}, {-1, 1}, {2, 0}, {2, 1}}, 24 | {{0, 0}, {0, -1}, {1, 1}, {-2, 0}, {-2, 1}}, 25 | {{0, 0}, {0, -1}, {-1, -1}, {2, 0}, {2, -1}}, 26 | {{0, 0}, {0, -1}, {1, -1}, {-2, 0}, {-2, -1}}}; 27 | 28 | Piece::Piece(PieceKind kind) : kind_(kind), color_(static_cast(kind)), state_(0) { 29 | TileColor e = kEmpty; 30 | TileColor c = color_; 31 | switch (kind) { 32 | case kNone: 33 | nRows_ = 0; 34 | nCols_ = 0; 35 | bBoxSide_ = 0; 36 | break; 37 | case kPieceI: 38 | nRows_ = 1; 39 | nCols_ = 4; 40 | bBoxSide_ = 4; 41 | initialShape_ = {c, c, c, c}; 42 | shape_ = {e, e, e, e, c, c, c, c, e, e, e, e, e, e, e, e}; 43 | break; 44 | case kPieceJ: 45 | nRows_ = 2; 46 | nCols_ = 3; 47 | bBoxSide_ = 3; 48 | initialShape_ = {c, e, e, c, c, c}; 49 | shape_ = {c, e, e, c, c, c, e, e, e}; 50 | break; 51 | case kPieceL: 52 | nRows_ = 2; 53 | nCols_ = 3; 54 | bBoxSide_ = 3; 55 | initialShape_ = {e, e, c, c, c, c}; 56 | shape_ = {e, e, c, c, c, c, e, e, e}; 57 | break; 58 | case kPieceO: 59 | nRows_ = 2; 60 | nCols_ = 2; 61 | bBoxSide_ = 2; 62 | initialShape_ = {c, c, c, c}; 63 | shape_ = initialShape_; 64 | break; 65 | case kPieceS: 66 | nRows_ = 2; 67 | nCols_ = 3; 68 | bBoxSide_ = 3; 69 | initialShape_ = {e, c, c, c, c, e}; 70 | shape_ = {e, c, c, c, c, e, e, e, e}; 71 | break; 72 | case kPieceT: 73 | nRows_ = 2; 74 | nCols_ = 3; 75 | bBoxSide_ = 3; 76 | initialShape_ = {e, c, e, c, c, c}; 77 | shape_ = {e, c, e, c, c, c, e, e, e}; 78 | break; 79 | case kPieceZ: 80 | nRows_ = 2; 81 | nCols_ = 3; 82 | bBoxSide_ = 3; 83 | initialShape_ = {c, c, e, e, c, c}; 84 | shape_ = {c, c, e, e, c, c, e, e, e}; 85 | break; 86 | } 87 | 88 | if (kind == kPieceO || kind == kNone) { 89 | return; 90 | } 91 | 92 | if (kind == kPieceI) { 93 | kicksRight_ = kKicksIRight_; 94 | kicksLeft_ = kKicksILeft_; 95 | } else { 96 | kicksRight_ = kKicksOtherRight_; 97 | kicksLeft_ = kicksOtherLeft_; 98 | } 99 | } 100 | 101 | void Piece::rotate(Rotation rotation) { 102 | if (kind_ == kPieceO) { 103 | return; 104 | } 105 | 106 | std::vector newShape(shape_.size()); 107 | int index = 0; 108 | switch (rotation) { 109 | case Rotation::kRight: 110 | state_ += 1; 111 | for (int col = bBoxSide_ - 1; col >= 0; --col) { 112 | for (int row = 0; row < bBoxSide_; ++row) { 113 | newShape[row * bBoxSide_ + col] = shape_[index]; 114 | ++index; 115 | } 116 | } 117 | break; 118 | case Rotation::kLeft: 119 | state_ -= 1; 120 | for (int col = 0; col < bBoxSide_; ++col) { 121 | for (int i = bBoxSide_ - 1; i >= 0; --i) { 122 | newShape[i * bBoxSide_ + col] = shape_[index]; 123 | ++index; 124 | } 125 | } 126 | } 127 | shape_ = newShape; 128 | 129 | if (state_ == -1) { 130 | state_ = kNumStates_ - 1; 131 | } else if (state_ == kNumStates_) { 132 | state_ = 0; 133 | } 134 | } 135 | 136 | const std::vector>& Piece::kicks(Rotation rotation) const { 137 | switch (rotation) { 138 | case Rotation::kRight: return kicksRight_[state_]; 139 | case Rotation::kLeft: return kicksLeft_[state_]; 140 | } 141 | throw std::runtime_error("This line is unreachable!"); 142 | } 143 | 144 | const int Board::kRowsAbove_ = 2; 145 | 146 | Board::Board(int nRows, int nCols) 147 | : nRows(nRows), nCols(nCols), tiles_((nRows + kRowsAbove_) * nCols, kEmpty), piece_(kNone) {} 148 | 149 | void Board::clear() { std::fill(tiles_.begin(), tiles_.end(), kEmpty); } 150 | 151 | bool Board::frozePiece() { 152 | auto shape = piece_.shape(); 153 | bool belowSkyline = false; 154 | int index = 0; 155 | for (int row = row_; row < row_ + piece_.bBoxSide(); ++row) { 156 | for (int col = col_; col < col_ + piece_.bBoxSide(); ++col) { 157 | if (shape[index] != kEmpty) { 158 | if (row >= 0) { 159 | belowSkyline = true; 160 | } 161 | 162 | setTile(row, col, shape[index]); 163 | } 164 | ++index; 165 | } 166 | } 167 | findLinesToClear(); 168 | piece_ = Piece(kNone); 169 | return belowSkyline; 170 | } 171 | 172 | bool Board::spawnPiece(PieceKind kind) { 173 | piece_ = Piece(kind); 174 | row_ = -2; 175 | col_ = (nCols - piece_.bBoxSide()) / 2; 176 | 177 | if (!isPositionPossible(row_, col_, piece_)) { 178 | return false; 179 | } 180 | 181 | int maxMoveDown = kind == kPieceI ? 1 : 2; 182 | for (int moveDown = 0; moveDown < maxMoveDown; ++moveDown) { 183 | if (!isPositionPossible(row_ + 1, col_, piece_)) { 184 | break; 185 | } 186 | ++row_; 187 | } 188 | updateGhostRow(); 189 | return true; 190 | } 191 | 192 | bool Board::moveHorizontal(int dCol) { 193 | if (isPositionPossible(row_, col_ + dCol, piece_)) { 194 | col_ += dCol; 195 | updateGhostRow(); 196 | return true; 197 | } 198 | 199 | return false; 200 | } 201 | 202 | bool Board::moveVertical(int dRow) { 203 | if (isPositionPossible(row_ + dRow, col_, piece_)) { 204 | row_ += dRow; 205 | return true; 206 | } 207 | 208 | return false; 209 | } 210 | 211 | bool Board::rotate(Rotation rotation) { 212 | if (piece_.kind() == kPieceO || piece_.kind() == kNone) { 213 | return false; 214 | } 215 | 216 | Piece testPiece(piece_); 217 | testPiece.rotate(rotation); 218 | 219 | for (const auto kick : piece_.kicks(rotation)) { 220 | int dRow = kick.first; 221 | int dCol = kick.second; 222 | if (isPositionPossible(row_ + dRow, col_ + dCol, testPiece)) { 223 | piece_ = testPiece; 224 | row_ += dRow; 225 | col_ += dCol; 226 | updateGhostRow(); 227 | return true; 228 | } 229 | } 230 | 231 | return false; 232 | } 233 | 234 | int Board::hardDrop() { 235 | int rowsPassed = ghostRow_ - row_; 236 | row_ = ghostRow_; 237 | return rowsPassed; 238 | } 239 | 240 | bool Board::isOnGround() const { return !isPositionPossible(row_ + 1, col_, piece_); } 241 | 242 | void Board::clearLines() { 243 | if (linesToClear_.empty()) { 244 | return; 245 | } 246 | 247 | linesToClear_.clear(); 248 | tiles_ = tilesAfterClear_; 249 | } 250 | 251 | void Board::setTile(int row, int col, TileColor color) { tiles_[(row + kRowsAbove_) * nCols + col] = color; } 252 | 253 | bool Board::isTileFilled(int row, int col) const { 254 | if (col < 0 || col >= nCols || row < -kRowsAbove_ || row >= nRows) { 255 | return true; 256 | } 257 | 258 | return tileAt(row, col) != kEmpty; 259 | } 260 | 261 | bool Board::isPositionPossible(int row, int col, const Piece& piece) const { 262 | if (piece.kind() == kNone) { 263 | return false; 264 | } 265 | 266 | auto shape = piece.shape(); 267 | int index = 0; 268 | for (int pieceRow = 0; pieceRow < piece.bBoxSide(); ++pieceRow) { 269 | for (int pieceCol = 0; pieceCol < piece.bBoxSide(); ++pieceCol) { 270 | if (shape[index] != kEmpty && isTileFilled(row + pieceRow, col + pieceCol)) { 271 | return false; 272 | } 273 | 274 | ++index; 275 | } 276 | } 277 | 278 | return true; 279 | } 280 | 281 | void Board::updateGhostRow() { 282 | ghostRow_ = row_; 283 | while (isPositionPossible(ghostRow_ + 1, col_, piece_)) { 284 | ++ghostRow_; 285 | } 286 | } 287 | 288 | void Board::findLinesToClear() { 289 | linesToClear_.clear(); 290 | tilesAfterClear_ = tiles_; 291 | 292 | int linesCleared = 0; 293 | int index = tiles_.size() - 1; 294 | for (int row = nRows - 1; row >= -kRowsAbove_; --row) { 295 | bool fullRow = true; 296 | for (int col = 0; col < nCols; ++col) { 297 | if (!isTileFilled(row, col)) { 298 | fullRow = false; 299 | break; 300 | } 301 | } 302 | 303 | if (fullRow) { 304 | linesToClear_.push_back(row); 305 | linesCleared++; 306 | index -= nCols; 307 | } else if (linesCleared > 0) { 308 | int indexShift = linesCleared * nCols; 309 | for (int col = 0; col < nCols; ++col) { 310 | tilesAfterClear_[index + indexShift] = tiles_[index]; 311 | --index; 312 | } 313 | } else { 314 | index -= nCols; 315 | } 316 | } 317 | 318 | std::fill(tilesAfterClear_.begin(), tilesAfterClear_.begin() + linesCleared * nCols, kEmpty); 319 | } 320 | 321 | const int Tetris::kLinesToClearPerLevel_ = 10; 322 | const int Tetris::kMaxLevel_ = 15; 323 | const double Tetris::kMoveDelay_ = 0.05; 324 | const double Tetris::kMoveRepeatDelay_ = 0.15; 325 | const double Tetris::kSoftDropSpeedFactor_ = 20; 326 | const double Tetris::kLockDownTimeLimit_ = 0.4; 327 | const int Tetris::kLockDownMovesLimit_ = 15; 328 | const double Tetris::kPauseAfterLineClear_ = 0.3; 329 | 330 | Tetris::Tetris(Board& board, double timeStep, unsigned int randomSeed) 331 | : board_(board), timeStep_(timeStep), rng_(randomSeed), bag_(2 * kNumPieces), nextPiece_(0), heldPiece_(kNone) { 332 | bag_[0] = kPieceI; 333 | bag_[1] = kPieceJ; 334 | bag_[2] = kPieceL; 335 | bag_[3] = kPieceO; 336 | bag_[4] = kPieceS; 337 | bag_[5] = kPieceT; 338 | bag_[6] = kPieceZ; 339 | std::copy(bag_.begin(), bag_.begin() + kNumPieces, bag_.begin() + kNumPieces); 340 | restart(1); 341 | } 342 | 343 | 344 | static double secondsPerLineForLevel(int level) { 345 | return std::pow(0.8 - (level - 1) * 0.007, level - 1); 346 | } 347 | 348 | 349 | void Tetris::restart(int level) { 350 | board_.clear(); 351 | gameOver_ = false; 352 | level_ = level; 353 | secondsPerLine_ = secondsPerLineForLevel(level); 354 | linesCleared_ = 0; 355 | score_ = 0; 356 | canHold_ = true; 357 | motion_ = Motion::kNone; 358 | moveLeftPrev_ = false; 359 | moveRightPrev_ = false; 360 | moveDownTimer_ = 0; 361 | moveRepeatTimer_ = 0; 362 | moveRepeatDelayTimer_ = 0; 363 | 364 | isOnGround_ = false; 365 | lockingTimer_ = 0; 366 | pausedForLinesClear_ = false; 367 | linesClearTimer_ = 0; 368 | 369 | std::shuffle(bag_.begin(), bag_.begin() + kNumPieces, rng_); 370 | std::shuffle(bag_.begin() + kNumPieces, bag_.end(), rng_); 371 | 372 | std::uniform_int_distribution holdPieceSelector(0, kNumPieces - 1); 373 | heldPiece_ = bag_[holdPieceSelector(rng_)]; 374 | 375 | spawnPiece(); 376 | } 377 | 378 | void Tetris::update(bool softDrop, bool moveRight, bool moveLeft) { 379 | if (pausedForLinesClear_) { 380 | linesClearTimer_ += timeStep_; 381 | 382 | if (linesClearTimer_ < kPauseAfterLineClear_) { 383 | return; 384 | } 385 | 386 | updateScore(board_.numLinesToClear()); 387 | board_.clearLines(); 388 | spawnPiece(); 389 | pausedForLinesClear_ = false; 390 | } 391 | 392 | moveDownTimer_ += timeStep_; 393 | moveRepeatTimer_ += timeStep_; 394 | moveRepeatDelayTimer_ += timeStep_; 395 | 396 | if (isOnGround_) { 397 | lockingTimer_ += timeStep_; 398 | } else { 399 | lockingTimer_ = 0; 400 | } 401 | 402 | bool moveLeftInput = moveLeft; 403 | bool moveRightInput = moveRight; 404 | 405 | if (moveLeft && moveRight) { 406 | if (!moveRightPrev_) { 407 | moveLeft = false; 408 | } else if (!moveLeftPrev_) { 409 | moveRight = false; 410 | } else if (motion_ == Motion::kLeft) { 411 | moveRight = false; 412 | } else { 413 | moveLeft = false; 414 | } 415 | } 416 | 417 | if (moveRight) { 418 | if (motion_ != Motion::kRight) { 419 | moveRepeatDelayTimer_ = 0; 420 | moveRepeatTimer_ = 0; 421 | moveHorizontal(1); 422 | } else if (moveRepeatDelayTimer_ >= kMoveRepeatDelay_ && moveRepeatTimer_ >= kMoveDelay_) { 423 | moveRepeatTimer_ = 0; 424 | moveHorizontal(1); 425 | } 426 | motion_ = Motion::kRight; 427 | } else if (moveLeft) { 428 | if (motion_ != Motion::kLeft) { 429 | moveRepeatDelayTimer_ = 0; 430 | moveRepeatTimer_ = 0; 431 | moveHorizontal(-1); 432 | } else if (moveRepeatDelayTimer_ >= kMoveRepeatDelay_ && moveRepeatTimer_ >= kMoveDelay_) { 433 | moveRepeatTimer_ = 0; 434 | moveHorizontal(-1); 435 | } 436 | motion_ = Motion::kLeft; 437 | } else { 438 | motion_ = Motion::kNone; 439 | } 440 | 441 | moveLeftPrev_ = moveLeftInput; 442 | moveRightPrev_ = moveRightInput; 443 | 444 | double speedFactor_ = softDrop ? kSoftDropSpeedFactor_ : 1; 445 | if (moveDownTimer_ >= secondsPerLine_ / speedFactor_) { 446 | if (board_.moveVertical(1) && softDrop) { 447 | score_ += level_; 448 | } 449 | moveDownTimer_ = 0; 450 | } 451 | 452 | checkLock(); 453 | } 454 | 455 | void Tetris::moveHorizontal(int dCol) { 456 | if (board_.moveHorizontal(dCol) && isOnGround_) { 457 | lockingTimer_ = 0; 458 | nMovesWhileLocking_ += 1; 459 | } 460 | } 461 | 462 | void Tetris::rotate(Rotation rotation) { 463 | if (board_.rotate(rotation) && isOnGround_) { 464 | lockingTimer_ = 0; 465 | nMovesWhileLocking_ += 1; 466 | } 467 | 468 | checkLock(); 469 | } 470 | 471 | void Tetris::hardDrop() { 472 | if (board_.piece().kind() == kNone) { 473 | return; 474 | } 475 | score_ += 2 * level_ * board_.hardDrop(); 476 | lock(); 477 | } 478 | 479 | void Tetris::hold() { 480 | if (!canHold_ || pausedForLinesClear_) { 481 | return; 482 | } 483 | 484 | PieceKind currentPiece = board_.piece().kind(); 485 | board_.spawnPiece(heldPiece_); 486 | heldPiece_ = currentPiece; 487 | 488 | canHold_ = false; 489 | } 490 | 491 | void Tetris::checkLock() { 492 | if (!board_.isOnGround()) { 493 | isOnGround_ = false; 494 | return; 495 | } 496 | 497 | isOnGround_ = true; 498 | 499 | if (lockingTimer_ >= kLockDownTimeLimit_ || nMovesWhileLocking_ >= kLockDownMovesLimit_) { 500 | lock(); 501 | } 502 | } 503 | 504 | void Tetris::lock() { 505 | lockingTimer_ = 0; 506 | isOnGround_ = false; 507 | canHold_ = true; 508 | 509 | if (!board_.frozePiece()) { 510 | gameOver_ = true; 511 | return; 512 | } 513 | 514 | if (board_.numLinesToClear() == 0) { 515 | spawnPiece(); 516 | return; 517 | } 518 | 519 | pausedForLinesClear_ = true; 520 | linesClearTimer_ = 0; 521 | } 522 | 523 | void Tetris::spawnPiece() { 524 | gameOver_ = !board_.spawnPiece(bag_[nextPiece_]); 525 | ++nextPiece_; 526 | if (nextPiece_ == kNumPieces) { 527 | std::copy(bag_.begin() + kNumPieces, bag_.end(), bag_.begin()); 528 | std::shuffle(bag_.begin() + kNumPieces, bag_.end(), rng_); 529 | nextPiece_ = 0; 530 | } 531 | nMovesWhileLocking_ = 0; 532 | } 533 | 534 | void Tetris::updateScore(int linesCleared) { 535 | int deltaScore = 0; 536 | switch (linesCleared) { 537 | case 1: deltaScore = 100; break; 538 | case 2: deltaScore = 300; break; 539 | case 3: deltaScore = 400; break; 540 | case 4: deltaScore = 800; break; 541 | default: assert(false); 542 | } 543 | linesCleared_ += linesCleared; 544 | score_ += deltaScore * level_; 545 | if (level_ < kMaxLevel_ && linesCleared_ >= kLinesToClearPerLevel_ * level_) { 546 | ++level_; 547 | secondsPerLine_ = secondsPerLineForLevel(level_); 548 | } 549 | } 550 | -------------------------------------------------------------------------------- /src/tetris.h: -------------------------------------------------------------------------------- 1 | #ifndef TETRIS_TETRIS_H 2 | #define TETRIS_TETRIS_H 3 | 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | const int kNumPieces = 7; 11 | 12 | enum TileColor { kEmpty = -1, kCyan, kBlue, kOrange, kYellow, kGreen, kPurple, kRed }; 13 | enum PieceKind { kNone = -1, kPieceI, kPieceJ, kPieceL, kPieceO, kPieceS, kPieceT, kPieceZ }; 14 | enum class Rotation { kRight, kLeft }; 15 | enum class Motion { kNone, kRight, kLeft }; 16 | 17 | class Piece { 18 | public: 19 | explicit Piece(PieceKind kind); 20 | 21 | PieceKind kind() const { return kind_; } 22 | TileColor color() const { return color_; } 23 | int bBoxSide() const { return bBoxSide_; } 24 | int nRows() const { return nRows_; } 25 | int nCols() const { return nCols_; } 26 | 27 | const std::vector& shape() const { return shape_; } 28 | const std::vector& initialShape() const { return initialShape_; } 29 | 30 | void rotate(Rotation rotation); 31 | const std::vector>& kicks(Rotation rotation) const; 32 | 33 | private: 34 | static const int kNumStates_; 35 | static const std::vector>> kKicksIRight_, kKicksILeft_; 36 | static const std::vector>> kKicksOtherRight_, kicksOtherLeft_; 37 | 38 | PieceKind kind_; 39 | TileColor color_; 40 | 41 | int nRows_, nCols_; 42 | std::vector initialShape_; 43 | 44 | int bBoxSide_; 45 | std::vector shape_; 46 | 47 | int state_; 48 | std::vector>> kicksRight_; 49 | std::vector>> kicksLeft_; 50 | }; 51 | 52 | class Board { 53 | public: 54 | const int nRows, nCols; 55 | 56 | Board(int nRows, int nCols); 57 | 58 | void clear(); 59 | 60 | TileColor tileAt(int row, int col) const { return tiles_[(row + kRowsAbove_) * nCols + col]; }; 61 | 62 | bool frozePiece(); 63 | bool spawnPiece(PieceKind kind); 64 | 65 | bool moveHorizontal(int dCol); 66 | bool moveVertical(int dRow); 67 | bool rotate(Rotation rotation); 68 | int hardDrop(); 69 | 70 | bool isOnGround() const; 71 | 72 | int numLinesToClear() const { return linesToClear_.size(); }; 73 | void clearLines(); 74 | 75 | const std::vector& linesToClear() const { return linesToClear_; } 76 | Piece piece() const { return piece_; } 77 | int pieceRow() const { return row_; } 78 | int pieceCol() const { return col_; } 79 | int ghostRow() const { return ghostRow_; } 80 | 81 | private: 82 | static const int kRowsAbove_; 83 | 84 | std::vector tiles_; 85 | 86 | Piece piece_; 87 | int row_ = 0; 88 | int col_ = 0; 89 | int ghostRow_ = 0; 90 | 91 | std::vector tilesAfterClear_; 92 | std::vector linesToClear_; 93 | 94 | void setTile(int row, int col, TileColor color); 95 | bool isTileFilled(int row, int col) const; 96 | bool isPositionPossible(int row, int col, const Piece& piece) const; 97 | void updateGhostRow(); 98 | void findLinesToClear(); 99 | }; 100 | 101 | class Tetris { 102 | public: 103 | Tetris(Board& board, double timeStep, unsigned int randomSeed); 104 | 105 | void restart(int level); 106 | bool isGameOver() const { return gameOver_; } 107 | 108 | void update(bool softDrop, bool moveRight, bool moveLeft); 109 | void rotate(Rotation rotation); 110 | void hardDrop(); 111 | void hold(); 112 | 113 | double lockPercent() const { return lockingTimer_ / kLockDownTimeLimit_; } 114 | bool isPausedForLinesClear() const { return pausedForLinesClear_; } 115 | double linesClearPausePercent() const { return linesClearTimer_ / kPauseAfterLineClear_; } 116 | 117 | int level() const { return level_; } 118 | int linesCleared() const { return linesCleared_; } 119 | int score() const { return score_; } 120 | Piece nextPiece() const { return Piece(bag_[nextPiece_]); } 121 | Piece heldPiece() const { return Piece(heldPiece_); } 122 | 123 | private: 124 | static const int kLinesToClearPerLevel_; 125 | static const int kMaxLevel_; 126 | static const double kMoveDelay_; 127 | static const double kMoveRepeatDelay_; 128 | static const double kSoftDropSpeedFactor_; 129 | static const double kLockDownTimeLimit_; 130 | static const int kLockDownMovesLimit_; 131 | static const double kPauseAfterLineClear_; 132 | 133 | Board& board_; 134 | 135 | bool gameOver_ = false; 136 | 137 | double timeStep_; 138 | 139 | std::default_random_engine rng_; 140 | std::vector bag_; 141 | int nextPiece_; 142 | 143 | PieceKind heldPiece_; 144 | bool canHold_; 145 | 146 | int level_; 147 | int linesCleared_; 148 | int score_; 149 | 150 | double secondsPerLine_; 151 | double moveDownTimer_; 152 | 153 | Motion motion_; 154 | bool moveLeftPrev_, moveRightPrev_; 155 | double moveRepeatDelayTimer_; 156 | double moveRepeatTimer_; 157 | 158 | bool isOnGround_; 159 | double lockingTimer_; 160 | int nMovesWhileLocking_; 161 | 162 | bool pausedForLinesClear_; 163 | double linesClearTimer_; 164 | 165 | void moveHorizontal(int dCol); 166 | void checkLock(); 167 | void lock(); 168 | void spawnPiece(); 169 | void updateScore(int linesCleared); 170 | }; 171 | 172 | #endif // TETRIS_TETRIS_H 173 | -------------------------------------------------------------------------------- /src/util.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "util.h" 6 | 7 | #include 8 | #include FT_FREETYPE_H 9 | 10 | #define STB_IMAGE_IMPLEMENTATION 11 | #define STBI_ONLY_PNG 12 | #include "stb_image.h" 13 | 14 | Shader::Shader(const GLchar* sourceVertex, const GLchar* sourceFragment) { 15 | GLuint vertexShader = glCreateShader(GL_VERTEX_SHADER); 16 | glShaderSource(vertexShader, 1, &sourceVertex, NULL); 17 | glCompileShader(vertexShader); 18 | 19 | GLchar infoLog[512]; 20 | GLint success; 21 | 22 | glGetShaderiv(vertexShader, GL_COMPILE_STATUS, &success); 23 | if (success == 0) { 24 | glGetShaderInfoLog(vertexShader, 512, NULL, infoLog); 25 | std::cerr << "Error compiling vector shader: " << infoLog << std::endl; 26 | } 27 | 28 | GLuint fragmentShader = glCreateShader(GL_FRAGMENT_SHADER); 29 | glShaderSource(fragmentShader, 1, &sourceFragment, NULL); 30 | glCompileShader(fragmentShader); 31 | 32 | glGetShaderiv(fragmentShader, GL_COMPILE_STATUS, &success); 33 | if (success == 0) { 34 | glGetShaderInfoLog(fragmentShader, 512, NULL, infoLog); 35 | std::cerr << "Error compiling fragment shader: " << infoLog << std::endl; 36 | } 37 | 38 | GLuint program = glCreateProgram(); 39 | glAttachShader(program, vertexShader); 40 | glAttachShader(program, fragmentShader); 41 | glLinkProgram(program); 42 | 43 | glGetProgramiv(program, GL_LINK_STATUS, &success); 44 | if (success == 0) { 45 | glGetProgramInfoLog(program, 512, NULL, infoLog); 46 | std::cerr << "Error linking shader program: " << infoLog << std::endl; 47 | } 48 | 49 | glDeleteShader(vertexShader); 50 | glDeleteShader(fragmentShader); 51 | 52 | id_ = program; 53 | } 54 | 55 | Texture loadRgbaTexture(const std::string& path) { 56 | stbi_set_flip_vertically_on_load(1); 57 | int width, height, numChannels; 58 | GLubyte* image = stbi_load(path.c_str(), &width, &height, &numChannels, 4); 59 | Texture texture(GL_RGBA, width, height, image); 60 | stbi_image_free(image); 61 | return texture; 62 | } 63 | 64 | Texture::Texture(GLenum format, GLuint width, GLuint height, GLubyte* image) : width(width), height(height) { 65 | glGenTextures(1, &id_); 66 | glBindTexture(GL_TEXTURE_2D, id_); 67 | glTexImage2D(GL_TEXTURE_2D, 0, format, width, height, 0, format, GL_UNSIGNED_BYTE, image); 68 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); 69 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); 70 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); 71 | glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); 72 | } 73 | 74 | std::vector loadFont(const std::string& path, unsigned int glyphHeight) { 75 | FT_Library ft; 76 | FT_Init_FreeType(&ft); 77 | 78 | FT_Face face; 79 | FT_New_Face(ft, path.c_str(), 0, &face); 80 | 81 | FT_Set_Pixel_Sizes(face, 0, glyphHeight); 82 | 83 | glPixelStorei(GL_UNPACK_ALIGNMENT, 1); 84 | 85 | std::vector glyphs; 86 | for (GLubyte c = 0; c < 128; c++) { 87 | if (FT_Load_Char(face, c, FT_LOAD_RENDER)) { 88 | std::cerr << "Failed to load glyph " << c << "." << std::endl; 89 | continue; 90 | } 91 | 92 | Texture texture(GL_RED, face->glyph->bitmap.width, face->glyph->bitmap.rows, face->glyph->bitmap.buffer); 93 | Glyph glyph = {texture, glm::ivec2(face->glyph->bitmap_left, face->glyph->bitmap_top), 94 | face->glyph->advance.x >> 6}; 95 | 96 | glyphs.push_back(glyph); 97 | } 98 | 99 | // Should handle face as well. 100 | FT_Done_FreeType(ft); 101 | 102 | return glyphs; 103 | } 104 | -------------------------------------------------------------------------------- /src/util.h: -------------------------------------------------------------------------------- 1 | #ifndef TETRIS_UTIL_H 2 | #define TETRIS_UTIL_H 3 | 4 | #include 5 | #include "glm/glm.hpp" 6 | #include 7 | 8 | class Texture { 9 | public: 10 | const GLuint width, height; 11 | Texture() : width(0), height(0) {}; 12 | Texture(GLenum format, GLuint width, GLuint height, GLubyte* image); 13 | 14 | void bind() const { glBindTexture(GL_TEXTURE_2D, id_); } 15 | 16 | private: 17 | GLuint id_ = 0; 18 | }; 19 | 20 | class Shader { 21 | public: 22 | Shader(const GLchar* sourceVertex, const GLchar* sourceFragment); 23 | 24 | void setFloat(const GLchar* name, GLfloat value) const { glUniform1f(glGetUniformLocation(id_, name), value); } 25 | 26 | void setMat4(const GLchar* name, const glm::mat4& matrix) const { 27 | glUniformMatrix4fv(glGetUniformLocation(id_, name), 1, GL_FALSE, glm::value_ptr(matrix)); 28 | } 29 | 30 | void setVec3(const GLchar* name, glm::vec3 vec) const { 31 | glUniform3f(glGetUniformLocation(id_, name), vec.x, vec.y, vec.z); 32 | } 33 | 34 | void setVec2(const GLchar* name, glm::vec2 vec) const { 35 | glUniform2f(glGetUniformLocation(id_, name), vec.x, vec.y); 36 | } 37 | 38 | void use() const { glUseProgram(id_); } 39 | 40 | private: 41 | GLuint id_; 42 | }; 43 | 44 | struct Glyph { 45 | Texture texture; 46 | glm::ivec2 bearing; 47 | GLint64 advance; 48 | }; 49 | 50 | std::vector loadFont(const std::string& path, unsigned int glyphHeight); 51 | Texture loadRgbaTexture(const std::string& path); 52 | 53 | #endif // TETRIS_UTIL_H 54 | --------------------------------------------------------------------------------