├── README.md ├── .gitignore └── src ├── colors.h ├── tetris.cpp └── tetris.c /README.md: -------------------------------------------------------------------------------- 1 | # Tetris 2 | 3 | Source code for "How to make a full Tetris game in C/C++": 4 | 5 | https://www.youtube.com/watch?v=kh3rkt6nZ2c 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | -------------------------------------------------------------------------------- /src/colors.h: -------------------------------------------------------------------------------- 1 | typedef struct Color 2 | { 3 | u8 r; 4 | u8 g; 5 | u8 b; 6 | u8 a; 7 | } Color; 8 | 9 | inline Color 10 | color(u8 r, u8 g, u8 b, u8 a) 11 | { 12 | Color result; 13 | result.r = r; 14 | result.g = g; 15 | result.b = b; 16 | result.a = a; 17 | return result; 18 | } 19 | 20 | const Color BASE_COLORS[] = { 21 | color(0x28, 0x28, 0x28, 0xFF), 22 | color(0x2D, 0x99, 0x99, 0xFF), 23 | color(0x99, 0x99, 0x2D, 0xFF), 24 | color(0x99, 0x2D, 0x99, 0xFF), 25 | color(0x2D, 0x99, 0x51, 0xFF), 26 | color(0x99, 0x2D, 0x2D, 0xFF), 27 | color(0x2D, 0x63, 0x99, 0xFF), 28 | color(0x99, 0x63, 0x2D, 0xFF) 29 | }; 30 | 31 | const Color LIGHT_COLORS[] = { 32 | color(0x28, 0x28, 0x28, 0xFF), 33 | color(0x44, 0xE5, 0xE5, 0xFF), 34 | color(0xE5, 0xE5, 0x44, 0xFF), 35 | color(0xE5, 0x44, 0xE5, 0xFF), 36 | color(0x44, 0xE5, 0x7A, 0xFF), 37 | color(0xE5, 0x44, 0x44, 0xFF), 38 | color(0x44, 0x95, 0xE5, 0xFF), 39 | color(0xE5, 0x95, 0x44, 0xFF) 40 | }; 41 | 42 | const Color DARK_COLORS[] = { 43 | color(0x28, 0x28, 0x28, 0xFF), 44 | color(0x1E, 0x66, 0x66, 0xFF), 45 | color(0x66, 0x66, 0x1E, 0xFF), 46 | color(0x66, 0x1E, 0x66, 0xFF), 47 | color(0x1E, 0x66, 0x36, 0xFF), 48 | color(0x66, 0x1E, 0x1E, 0xFF), 49 | color(0x1E, 0x42, 0x66, 0xFF), 50 | color(0x66, 0x42, 0x1E, 0xFF) 51 | }; 52 | -------------------------------------------------------------------------------- /src/tetris.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | #include 8 | #include 9 | 10 | 11 | typedef uint8_t u8; 12 | typedef uint16_t u16; 13 | typedef uint32_t u32; 14 | typedef int8_t s8; 15 | typedef int16_t s16; 16 | typedef int32_t s32; 17 | typedef float f32; 18 | typedef double f64; 19 | 20 | #include "colors.h" 21 | 22 | #define WIDTH 10 23 | #define HEIGHT 22 24 | #define VISIBLE_HEIGHT 20 25 | #define GRID_SIZE 30 26 | 27 | #define ARRAY_COUNT(x) (sizeof(x) / sizeof((x)[0])) 28 | 29 | const u8 FRAMES_PER_DROP[] = { 30 | 48, 31 | 43, 32 | 38, 33 | 33, 34 | 28, 35 | 23, 36 | 18, 37 | 13, 38 | 8, 39 | 6, 40 | 5, 41 | 5, 42 | 5, 43 | 4, 44 | 4, 45 | 4, 46 | 3, 47 | 3, 48 | 3, 49 | 2, 50 | 2, 51 | 2, 52 | 2, 53 | 2, 54 | 2, 55 | 2, 56 | 2, 57 | 2, 58 | 2, 59 | 1 60 | }; 61 | 62 | const f32 TARGET_SECONDS_PER_FRAME = 1.f / 60.f; 63 | 64 | struct Tetrino 65 | { 66 | const u8 *data; 67 | const s32 side; 68 | }; 69 | 70 | inline Tetrino 71 | tetrino(const u8 *data, s32 side) 72 | { 73 | return { data, side }; 74 | } 75 | 76 | const u8 TETRINO_1[] = { 77 | 0, 0, 0, 0, 78 | 1, 1, 1, 1, 79 | 0, 0, 0, 0, 80 | 0, 0, 0, 0 81 | }; 82 | 83 | const u8 TETRINO_2[] = { 84 | 2, 2, 85 | 2, 2 86 | }; 87 | 88 | const u8 TETRINO_3[] = { 89 | 0, 0, 0, 90 | 3, 3, 3, 91 | 0, 3, 0 92 | }; 93 | 94 | const u8 TETRINO_4[] = { 95 | 0, 4, 4, 96 | 4, 4, 0, 97 | 0, 0, 0 98 | }; 99 | 100 | const u8 TETRINO_5[] = { 101 | 5, 5, 0, 102 | 0, 5, 5, 103 | 0, 0, 0 104 | }; 105 | 106 | const u8 TETRINO_6[] = { 107 | 6, 0, 0, 108 | 6, 6, 6, 109 | 0, 0, 0 110 | }; 111 | 112 | const u8 TETRINO_7[] = { 113 | 0, 0, 7, 114 | 7, 7, 7, 115 | 0, 0, 0 116 | }; 117 | 118 | 119 | const Tetrino TETRINOS[] = { 120 | tetrino(TETRINO_1, 4), 121 | tetrino(TETRINO_2, 2), 122 | tetrino(TETRINO_3, 3), 123 | tetrino(TETRINO_4, 3), 124 | tetrino(TETRINO_5, 3), 125 | tetrino(TETRINO_6, 3), 126 | tetrino(TETRINO_7, 3), 127 | }; 128 | 129 | enum Game_Phase 130 | { 131 | GAME_PHASE_START, 132 | GAME_PHASE_PLAY, 133 | GAME_PHASE_LINE, 134 | GAME_PHASE_GAMEOVER 135 | }; 136 | 137 | struct Piece_State 138 | { 139 | u8 tetrino_index; 140 | s32 offset_row; 141 | s32 offset_col; 142 | s32 rotation; 143 | }; 144 | 145 | struct Game_State 146 | { 147 | u8 board[WIDTH * HEIGHT]; 148 | u8 lines[HEIGHT]; 149 | s32 pending_line_count; 150 | 151 | Piece_State piece; 152 | 153 | Game_Phase phase; 154 | 155 | s32 start_level; 156 | s32 level; 157 | s32 line_count; 158 | s32 points; 159 | 160 | f32 next_drop_time; 161 | f32 highlight_end_time; 162 | f32 time; 163 | }; 164 | 165 | struct Input_State 166 | { 167 | u8 left; 168 | u8 right; 169 | u8 up; 170 | u8 down; 171 | 172 | u8 a; 173 | 174 | s8 dleft; 175 | s8 dright; 176 | s8 dup; 177 | s8 ddown; 178 | s8 da; 179 | }; 180 | 181 | enum Text_Align 182 | { 183 | TEXT_ALIGN_LEFT, 184 | TEXT_ALIGN_CENTER, 185 | TEXT_ALIGN_RIGHT 186 | }; 187 | 188 | inline u8 189 | matrix_get(const u8 *values, s32 width, s32 row, s32 col) 190 | { 191 | s32 index = row * width + col; 192 | return values[index]; 193 | } 194 | 195 | inline void 196 | matrix_set(u8 *values, s32 width, s32 row, s32 col, u8 value) 197 | { 198 | s32 index = row * width + col; 199 | values[index] = value; 200 | } 201 | 202 | inline u8 203 | tetrino_get(const Tetrino *tetrino, s32 row, s32 col, s32 rotation) 204 | { 205 | s32 side = tetrino->side; 206 | switch (rotation) 207 | { 208 | case 0: 209 | return tetrino->data[row * side + col]; 210 | case 1: 211 | return tetrino->data[(side - col - 1) * side + row]; 212 | case 2: 213 | return tetrino->data[(side - row - 1) * side + (side - col - 1)]; 214 | case 3: 215 | return tetrino->data[col * side + (side - row - 1)]; 216 | } 217 | return 0; 218 | } 219 | 220 | inline u8 221 | check_row_filled(const u8 *values, s32 width, s32 row) 222 | { 223 | for (s32 col = 0; 224 | col < width; 225 | ++col) 226 | { 227 | if (!matrix_get(values, width, row, col)) 228 | { 229 | return 0; 230 | } 231 | } 232 | return 1; 233 | } 234 | 235 | inline u8 236 | check_row_empty(const u8 *values, s32 width, s32 row) 237 | { 238 | for (s32 col = 0; 239 | col < width; 240 | ++col) 241 | { 242 | if (matrix_get(values, width, row, col)) 243 | { 244 | return 0; 245 | } 246 | } 247 | return 1; 248 | } 249 | 250 | s32 251 | find_lines(const u8 *values, s32 width, s32 height, u8 *lines_out) 252 | { 253 | s32 count = 0; 254 | for (s32 row = 0; 255 | row < height; 256 | ++row) 257 | { 258 | u8 filled = check_row_filled(values, width, row); 259 | lines_out[row] = filled; 260 | count += filled; 261 | } 262 | return count; 263 | } 264 | 265 | void 266 | clear_lines(u8 *values, s32 width, s32 height, const u8 *lines) 267 | { 268 | s32 src_row = height - 1; 269 | for (s32 dst_row = height - 1; 270 | dst_row >= 0; 271 | --dst_row) 272 | { 273 | while (src_row >= 0 && lines[src_row]) 274 | { 275 | --src_row; 276 | } 277 | 278 | if (src_row < 0) 279 | { 280 | memset(values + dst_row * width, 0, width); 281 | } 282 | else 283 | { 284 | if (src_row != dst_row) 285 | { 286 | memcpy(values + dst_row * width, 287 | values + src_row * width, 288 | width); 289 | } 290 | --src_row; 291 | } 292 | } 293 | } 294 | 295 | 296 | bool 297 | check_piece_valid(const Piece_State *piece, 298 | const u8 *board, s32 width, s32 height) 299 | { 300 | const Tetrino *tetrino = TETRINOS + piece->tetrino_index; 301 | assert(tetrino); 302 | 303 | for (s32 row = 0; 304 | row < tetrino->side; 305 | ++row) 306 | { 307 | for (s32 col = 0; 308 | col < tetrino->side; 309 | ++col) 310 | { 311 | u8 value = tetrino_get(tetrino, row, col, piece->rotation); 312 | if (value > 0) 313 | { 314 | s32 board_row = piece->offset_row + row; 315 | s32 board_col = piece->offset_col + col; 316 | if (board_row < 0) 317 | { 318 | return false; 319 | } 320 | if (board_row >= height) 321 | { 322 | return false; 323 | } 324 | if (board_col < 0) 325 | { 326 | return false; 327 | } 328 | if (board_col >= width) 329 | { 330 | return false; 331 | } 332 | if (matrix_get(board, width, board_row, board_col)) 333 | { 334 | return false; 335 | } 336 | } 337 | } 338 | } 339 | return true; 340 | } 341 | 342 | void 343 | merge_piece(Game_State *game) 344 | { 345 | const Tetrino *tetrino = TETRINOS + game->piece.tetrino_index; 346 | for (s32 row = 0; 347 | row < tetrino->side; 348 | ++row) 349 | { 350 | for (s32 col = 0; 351 | col < tetrino->side; 352 | ++col) 353 | { 354 | u8 value = tetrino_get(tetrino, row, col, game->piece.rotation); 355 | if (value) 356 | { 357 | s32 board_row = game->piece.offset_row + row; 358 | s32 board_col = game->piece.offset_col + col; 359 | matrix_set(game->board, WIDTH, board_row, board_col, value); 360 | } 361 | } 362 | } 363 | } 364 | 365 | inline s32 366 | random_int(s32 min, s32 max) 367 | { 368 | s32 range = max - min; 369 | return min + rand() % range; 370 | } 371 | 372 | inline f32 373 | get_time_to_next_drop(s32 level) 374 | { 375 | if (level > 29) 376 | { 377 | level = 29; 378 | } 379 | return FRAMES_PER_DROP[level] * TARGET_SECONDS_PER_FRAME; 380 | } 381 | 382 | 383 | void 384 | spawn_piece(Game_State *game) 385 | { 386 | game->piece = {}; 387 | game->piece.tetrino_index = (u8)random_int(0, ARRAY_COUNT(TETRINOS)); 388 | game->piece.offset_col = WIDTH / 2; 389 | game->next_drop_time = game->time + get_time_to_next_drop(game->level); 390 | } 391 | 392 | 393 | inline bool 394 | soft_drop(Game_State *game) 395 | { 396 | ++game->piece.offset_row; 397 | if (!check_piece_valid(&game->piece, game->board, WIDTH, HEIGHT)) 398 | { 399 | --game->piece.offset_row; 400 | merge_piece(game); 401 | spawn_piece(game); 402 | return false; 403 | } 404 | 405 | game->next_drop_time = game->time + get_time_to_next_drop(game->level); 406 | return true; 407 | } 408 | 409 | inline s32 410 | compute_points(s32 level, s32 line_count) 411 | { 412 | switch (line_count) 413 | { 414 | case 1: 415 | return 40 * (level + 1); 416 | case 2: 417 | return 100 * (level + 1); 418 | case 3: 419 | return 300 * (level + 1); 420 | case 4: 421 | return 1200 * (level + 1); 422 | } 423 | return 0; 424 | } 425 | 426 | inline s32 427 | min(s32 x, s32 y) 428 | { 429 | return x < y ? x : y; 430 | } 431 | inline s32 432 | max(s32 x, s32 y) 433 | { 434 | return x > y ? x : y; 435 | } 436 | 437 | inline s32 438 | get_lines_for_next_level(s32 start_level, s32 level) 439 | { 440 | s32 first_level_up_limit = min( 441 | (start_level * 10 + 10), 442 | max(100, (start_level * 10 - 50))); 443 | if (level == start_level) 444 | { 445 | return first_level_up_limit; 446 | } 447 | s32 diff = level - start_level; 448 | return first_level_up_limit + diff * 10; 449 | } 450 | 451 | void 452 | update_game_start(Game_State *game, const Input_State *input) 453 | { 454 | if (input->dup > 0) 455 | { 456 | ++game->start_level; 457 | } 458 | 459 | if (input->ddown > 0 && game->start_level > 0) 460 | { 461 | --game->start_level; 462 | } 463 | 464 | if (input->da > 0) 465 | { 466 | memset(game->board, 0, WIDTH * HEIGHT); 467 | game->level = game->start_level; 468 | game->line_count = 0; 469 | game->points = 0; 470 | spawn_piece(game); 471 | game->phase = GAME_PHASE_PLAY; 472 | } 473 | } 474 | 475 | void 476 | update_game_gameover(Game_State *game, const Input_State *input) 477 | { 478 | if (input->da > 0) 479 | { 480 | game->phase = GAME_PHASE_START; 481 | } 482 | } 483 | 484 | void 485 | update_game_line(Game_State *game) 486 | { 487 | if (game->time >= game->highlight_end_time) 488 | { 489 | clear_lines(game->board, WIDTH, HEIGHT, game->lines); 490 | game->line_count += game->pending_line_count; 491 | game->points += compute_points(game->level, game->pending_line_count); 492 | 493 | s32 lines_for_next_level = get_lines_for_next_level(game->start_level, 494 | game->level); 495 | if (game->line_count >= lines_for_next_level) 496 | { 497 | ++game->level; 498 | } 499 | 500 | game->phase = GAME_PHASE_PLAY; 501 | } 502 | } 503 | 504 | void 505 | update_game_play(Game_State *game, 506 | const Input_State *input) 507 | { 508 | Piece_State piece = game->piece; 509 | if (input->dleft > 0) 510 | { 511 | --piece.offset_col; 512 | } 513 | if (input->dright> 0) 514 | { 515 | ++piece.offset_col; 516 | } 517 | if (input->dup > 0) 518 | { 519 | piece.rotation = (piece.rotation + 1) % 4; 520 | } 521 | 522 | if (check_piece_valid(&piece, game->board, WIDTH, HEIGHT)) 523 | { 524 | game->piece = piece; 525 | } 526 | 527 | if (input->ddown > 0) 528 | { 529 | soft_drop(game); 530 | } 531 | 532 | if (input->da > 0) 533 | { 534 | while(soft_drop(game)); 535 | } 536 | 537 | while (game->time >= game->next_drop_time) 538 | { 539 | soft_drop(game); 540 | } 541 | 542 | game->pending_line_count = find_lines(game->board, WIDTH, HEIGHT, game->lines); 543 | if (game->pending_line_count > 0) 544 | { 545 | game->phase = GAME_PHASE_LINE; 546 | game->highlight_end_time = game->time + 0.5f; 547 | } 548 | 549 | s32 game_over_row = 0; 550 | if (!check_row_empty(game->board, WIDTH, game_over_row)) 551 | { 552 | game->phase = GAME_PHASE_GAMEOVER; 553 | } 554 | } 555 | 556 | void 557 | update_game(Game_State *game, 558 | const Input_State *input) 559 | { 560 | switch(game->phase) 561 | { 562 | case GAME_PHASE_START: 563 | update_game_start(game, input); 564 | break; 565 | case GAME_PHASE_PLAY: 566 | update_game_play(game, input); 567 | break; 568 | case GAME_PHASE_LINE: 569 | update_game_line(game); 570 | break; 571 | case GAME_PHASE_GAMEOVER: 572 | update_game_gameover(game, input); 573 | break; 574 | } 575 | } 576 | 577 | void 578 | fill_rect(SDL_Renderer *renderer, 579 | s32 x, s32 y, s32 width, s32 height, Color color) 580 | { 581 | SDL_Rect rect = {}; 582 | rect.x = x; 583 | rect.y = y; 584 | rect.w = width; 585 | rect.h = height; 586 | SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); 587 | SDL_RenderFillRect(renderer, &rect); 588 | } 589 | 590 | 591 | void 592 | draw_rect(SDL_Renderer *renderer, 593 | s32 x, s32 y, s32 width, s32 height, Color color) 594 | { 595 | SDL_Rect rect = {}; 596 | rect.x = x; 597 | rect.y = y; 598 | rect.w = width; 599 | rect.h = height; 600 | SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); 601 | SDL_RenderDrawRect(renderer, &rect); 602 | } 603 | 604 | void 605 | draw_string(SDL_Renderer *renderer, 606 | TTF_Font *font, 607 | const char *text, 608 | s32 x, s32 y, 609 | Text_Align alignment, 610 | Color color) 611 | { 612 | SDL_Color sdl_color = SDL_Color { color.r, color.g, color.b, color.a }; 613 | SDL_Surface *surface = TTF_RenderText_Solid(font, text, sdl_color); 614 | SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface); 615 | 616 | SDL_Rect rect; 617 | rect.y = y; 618 | rect.w = surface->w; 619 | rect.h = surface->h; 620 | switch (alignment) 621 | { 622 | case TEXT_ALIGN_LEFT: 623 | rect.x = x; 624 | break; 625 | case TEXT_ALIGN_CENTER: 626 | rect.x = x - surface->w / 2; 627 | break; 628 | case TEXT_ALIGN_RIGHT: 629 | rect.x = x - surface->w; 630 | break; 631 | } 632 | 633 | SDL_RenderCopy(renderer, texture, 0, &rect); 634 | SDL_FreeSurface(surface); 635 | SDL_DestroyTexture(texture); 636 | } 637 | 638 | void 639 | draw_cell(SDL_Renderer *renderer, 640 | s32 row, s32 col, u8 value, 641 | s32 offset_x, s32 offset_y, 642 | bool outline = false) 643 | { 644 | Color base_color = BASE_COLORS[value]; 645 | Color light_color = LIGHT_COLORS[value]; 646 | Color dark_color = DARK_COLORS[value]; 647 | 648 | 649 | s32 edge = GRID_SIZE / 8; 650 | 651 | s32 x = col * GRID_SIZE + offset_x; 652 | s32 y = row * GRID_SIZE + offset_y; 653 | 654 | if (outline) 655 | { 656 | draw_rect(renderer, x, y, GRID_SIZE, GRID_SIZE, base_color); 657 | return; 658 | } 659 | 660 | fill_rect(renderer, x, y, GRID_SIZE, GRID_SIZE, dark_color); 661 | fill_rect(renderer, x + edge, y, 662 | GRID_SIZE - edge, GRID_SIZE - edge, light_color); 663 | fill_rect(renderer, x + edge, y + edge, 664 | GRID_SIZE - edge * 2, GRID_SIZE - edge * 2, base_color); 665 | } 666 | 667 | void 668 | draw_piece(SDL_Renderer *renderer, 669 | const Piece_State *piece, 670 | s32 offset_x, s32 offset_y, 671 | bool outline = false) 672 | { 673 | const Tetrino *tetrino = TETRINOS + piece->tetrino_index; 674 | for (s32 row = 0; 675 | row < tetrino->side; 676 | ++row) 677 | { 678 | for (s32 col = 0; 679 | col < tetrino->side; 680 | ++col) 681 | { 682 | u8 value = tetrino_get(tetrino, row, col, piece->rotation); 683 | if (value) 684 | { 685 | draw_cell(renderer, 686 | row + piece->offset_row, 687 | col + piece->offset_col, 688 | value, 689 | offset_x, offset_y, 690 | outline); 691 | } 692 | } 693 | } 694 | } 695 | 696 | void 697 | draw_board(SDL_Renderer *renderer, 698 | const u8 *board, s32 width, s32 height, 699 | s32 offset_x, s32 offset_y) 700 | { 701 | fill_rect(renderer, offset_x, offset_y, 702 | width * GRID_SIZE, height * GRID_SIZE, 703 | BASE_COLORS[0]); 704 | for (s32 row = 0; 705 | row < height; 706 | ++row) 707 | { 708 | for (s32 col = 0; 709 | col < width; 710 | ++col) 711 | { 712 | u8 value = matrix_get(board, width, row, col); 713 | if (value) 714 | { 715 | draw_cell(renderer, row, col, value, offset_x, offset_y); 716 | } 717 | } 718 | } 719 | } 720 | 721 | void 722 | render_game(const Game_State *game, 723 | SDL_Renderer *renderer, 724 | TTF_Font *font) 725 | { 726 | 727 | char buffer[4096]; 728 | 729 | Color highlight_color = color(0xFF, 0xFF, 0xFF, 0xFF); 730 | 731 | s32 margin_y = 60; 732 | 733 | draw_board(renderer, game->board, WIDTH, HEIGHT, 0, margin_y); 734 | 735 | if (game->phase == GAME_PHASE_PLAY) 736 | { 737 | draw_piece(renderer, &game->piece, 0, margin_y); 738 | 739 | Piece_State piece = game->piece; 740 | while (check_piece_valid(&piece, game->board, WIDTH, HEIGHT)) 741 | { 742 | piece.offset_row++; 743 | } 744 | --piece.offset_row; 745 | 746 | draw_piece(renderer, &piece, 0, margin_y, true); 747 | 748 | } 749 | 750 | if (game->phase == GAME_PHASE_LINE) 751 | { 752 | for (s32 row = 0; 753 | row < HEIGHT; 754 | ++row) 755 | { 756 | if (game->lines[row]) 757 | { 758 | s32 x = 0; 759 | s32 y = row * GRID_SIZE + margin_y; 760 | 761 | fill_rect(renderer, x, y, 762 | WIDTH * GRID_SIZE, GRID_SIZE, highlight_color); 763 | } 764 | } 765 | } 766 | else if (game->phase == GAME_PHASE_GAMEOVER) 767 | { 768 | s32 x = WIDTH * GRID_SIZE / 2; 769 | s32 y = (HEIGHT * GRID_SIZE + margin_y) / 2; 770 | draw_string(renderer, font, "GAME OVER", 771 | x, y, TEXT_ALIGN_CENTER, highlight_color); 772 | } 773 | else if (game->phase == GAME_PHASE_START) 774 | { 775 | s32 x = WIDTH * GRID_SIZE / 2; 776 | s32 y = (HEIGHT * GRID_SIZE + margin_y) / 2; 777 | draw_string(renderer, font, "PRESS START", 778 | x, y, TEXT_ALIGN_CENTER, highlight_color); 779 | 780 | snprintf(buffer, sizeof(buffer), "STARTING LEVEL: %d", game->start_level); 781 | draw_string(renderer, font, buffer, 782 | x, y + 30, TEXT_ALIGN_CENTER, highlight_color); 783 | } 784 | 785 | fill_rect(renderer, 786 | 0, margin_y, 787 | WIDTH * GRID_SIZE, (HEIGHT - VISIBLE_HEIGHT) * GRID_SIZE, 788 | color(0x00, 0x00, 0x00, 0x00)); 789 | 790 | 791 | snprintf(buffer, sizeof(buffer), "LEVEL: %d", game->level); 792 | draw_string(renderer, font, buffer, 5, 5, TEXT_ALIGN_LEFT, highlight_color); 793 | 794 | snprintf(buffer, sizeof(buffer), "LINES: %d", game->line_count); 795 | draw_string(renderer, font, buffer, 5, 35, TEXT_ALIGN_LEFT, highlight_color); 796 | 797 | snprintf(buffer, sizeof(buffer), "POINTS: %d", game->points); 798 | draw_string(renderer, font, buffer, 5, 65, TEXT_ALIGN_LEFT, highlight_color); 799 | } 800 | 801 | int 802 | main() 803 | { 804 | if (SDL_Init(SDL_INIT_VIDEO) < 0) 805 | { 806 | return 1; 807 | } 808 | 809 | if (TTF_Init() < 0) 810 | { 811 | return 2; 812 | } 813 | 814 | SDL_Window *window = SDL_CreateWindow( 815 | "Tetris", 816 | SDL_WINDOWPOS_UNDEFINED, 817 | SDL_WINDOWPOS_UNDEFINED, 818 | 300, 819 | 720, 820 | SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN); 821 | SDL_Renderer *renderer = SDL_CreateRenderer( 822 | window, 823 | -1, 824 | SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); 825 | 826 | const char *font_name = "novem___.ttf"; 827 | TTF_Font *font = TTF_OpenFont(font_name, 24); 828 | 829 | Game_State game = {}; 830 | Input_State input = {}; 831 | 832 | spawn_piece(&game); 833 | 834 | game.piece.tetrino_index = 2; 835 | 836 | bool quit = false; 837 | while (!quit) 838 | { 839 | game.time = SDL_GetTicks() / 1000.0f; 840 | 841 | SDL_Event e; 842 | while (SDL_PollEvent(&e) != 0) 843 | { 844 | if (e.type == SDL_QUIT) 845 | { 846 | quit = true; 847 | } 848 | } 849 | 850 | s32 key_count; 851 | const u8 *key_states = SDL_GetKeyboardState(&key_count); 852 | 853 | if (key_states[SDL_SCANCODE_ESCAPE]) 854 | { 855 | quit = true; 856 | } 857 | 858 | Input_State prev_input = input; 859 | 860 | input.left = key_states[SDL_SCANCODE_LEFT]; 861 | input.right = key_states[SDL_SCANCODE_RIGHT]; 862 | input.up = key_states[SDL_SCANCODE_UP]; 863 | input.down = key_states[SDL_SCANCODE_DOWN]; 864 | input.a = key_states[SDL_SCANCODE_SPACE]; 865 | 866 | input.dleft = input.left - prev_input.left; 867 | input.dright = input.right - prev_input.right; 868 | input.dup = input.up - prev_input.up; 869 | input.ddown = input.down - prev_input.down; 870 | input.da = input.a - prev_input.a; 871 | 872 | SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); 873 | SDL_RenderClear(renderer); 874 | 875 | update_game(&game, &input); 876 | render_game(&game, renderer, font); 877 | 878 | SDL_RenderPresent(renderer); 879 | } 880 | 881 | TTF_CloseFont(font); 882 | SDL_DestroyRenderer(renderer); 883 | SDL_Quit(); 884 | 885 | return 0; 886 | } 887 | -------------------------------------------------------------------------------- /src/tetris.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | #include 10 | 11 | 12 | typedef uint8_t u8; 13 | typedef uint16_t u16; 14 | typedef uint32_t u32; 15 | typedef int8_t s8; 16 | typedef int16_t s16; 17 | typedef int32_t s32; 18 | typedef float f32; 19 | typedef double f64; 20 | 21 | #define WIDTH 10 22 | #define HEIGHT 22 23 | #define VISIBLE_HEIGHT 20 24 | #define GRID_SIZE 30 25 | 26 | #define ARRAY_COUNT(x) (sizeof(x) / sizeof((x)[0])) 27 | #define ZERO_STRUCT(obj) memset(&(obj), 0, sizeof(obj)) 28 | 29 | static const u8 FRAMES_PER_DROP[] = { 30 | 48, 31 | 43, 32 | 38, 33 | 33, 34 | 28, 35 | 23, 36 | 18, 37 | 13, 38 | 8, 39 | 6, 40 | 5, 41 | 5, 42 | 5, 43 | 4, 44 | 4, 45 | 4, 46 | 3, 47 | 3, 48 | 3, 49 | 2, 50 | 2, 51 | 2, 52 | 2, 53 | 2, 54 | 2, 55 | 2, 56 | 2, 57 | 2, 58 | 2, 59 | 1 60 | }; 61 | 62 | static const f32 TARGET_SECONDS_PER_FRAME = 1.f / 60.f; 63 | 64 | 65 | struct Color 66 | { 67 | u8 r; 68 | u8 g; 69 | u8 b; 70 | u8 a; 71 | }; 72 | 73 | static const struct Color BASE_COLORS[] = { 74 | { 0x28, 0x28, 0x28, 0xFF }, 75 | { 0x2D, 0x99, 0x99, 0xFF }, 76 | { 0x99, 0x99, 0x2D, 0xFF }, 77 | { 0x99, 0x2D, 0x99, 0xFF }, 78 | { 0x2D, 0x99, 0x51, 0xFF }, 79 | { 0x99, 0x2D, 0x2D, 0xFF }, 80 | { 0x2D, 0x63, 0x99, 0xFF }, 81 | { 0x99, 0x63, 0x2D, 0xFF } 82 | }; 83 | 84 | static const struct Color LIGHT_COLORS[] = { 85 | { 0x28, 0x28, 0x28, 0xFF }, 86 | { 0x44, 0xE5, 0xE5, 0xFF }, 87 | { 0xE5, 0xE5, 0x44, 0xFF }, 88 | { 0xE5, 0x44, 0xE5, 0xFF }, 89 | { 0x44, 0xE5, 0x7A, 0xFF }, 90 | { 0xE5, 0x44, 0x44, 0xFF }, 91 | { 0x44, 0x95, 0xE5, 0xFF }, 92 | { 0xE5, 0x95, 0x44, 0xFF } 93 | }; 94 | 95 | static const struct Color DARK_COLORS[] = { 96 | { 0x28, 0x28, 0x28, 0xFF }, 97 | { 0x1E, 0x66, 0x66, 0xFF }, 98 | { 0x66, 0x66, 0x1E, 0xFF }, 99 | { 0x66, 0x1E, 0x66, 0xFF }, 100 | { 0x1E, 0x66, 0x36, 0xFF }, 101 | { 0x66, 0x1E, 0x1E, 0xFF }, 102 | { 0x1E, 0x42, 0x66, 0xFF }, 103 | { 0x66, 0x42, 0x1E, 0xFF } 104 | }; 105 | 106 | 107 | struct Tetrino 108 | { 109 | const u8 *data; 110 | const s32 side; 111 | }; 112 | 113 | static const u8 TETRINO_1[] = { 114 | 0, 0, 0, 0, 115 | 1, 1, 1, 1, 116 | 0, 0, 0, 0, 117 | 0, 0, 0, 0 118 | }; 119 | 120 | static const u8 TETRINO_2[] = { 121 | 2, 2, 122 | 2, 2 123 | }; 124 | 125 | static const u8 TETRINO_3[] = { 126 | 0, 0, 0, 127 | 3, 3, 3, 128 | 0, 3, 0 129 | }; 130 | 131 | static const u8 TETRINO_4[] = { 132 | 0, 4, 4, 133 | 4, 4, 0, 134 | 0, 0, 0 135 | }; 136 | 137 | static const u8 TETRINO_5[] = { 138 | 5, 5, 0, 139 | 0, 5, 5, 140 | 0, 0, 0 141 | }; 142 | 143 | static const u8 TETRINO_6[] = { 144 | 6, 0, 0, 145 | 6, 6, 6, 146 | 0, 0, 0 147 | }; 148 | 149 | static const u8 TETRINO_7[] = { 150 | 0, 0, 7, 151 | 7, 7, 7, 152 | 0, 0, 0 153 | }; 154 | 155 | 156 | static const struct Tetrino TETRINOS[] = { 157 | { TETRINO_1, 4 }, 158 | { TETRINO_2, 2 }, 159 | { TETRINO_3, 3 }, 160 | { TETRINO_4, 3 }, 161 | { TETRINO_5, 3 }, 162 | { TETRINO_6, 3 }, 163 | { TETRINO_7, 3 } 164 | }; 165 | 166 | enum Game_Phase 167 | { 168 | GAME_PHASE_START, 169 | GAME_PHASE_PLAY, 170 | GAME_PHASE_LINE, 171 | GAME_PHASE_GAMEOVER 172 | }; 173 | 174 | struct Piece_State 175 | { 176 | u8 tetrino_index; 177 | s32 offset_row; 178 | s32 offset_col; 179 | s32 rotation; 180 | }; 181 | 182 | struct Game_State 183 | { 184 | u8 board[WIDTH * HEIGHT]; 185 | u8 lines[HEIGHT]; 186 | s32 pending_line_count; 187 | 188 | struct Piece_State piece; 189 | 190 | enum Game_Phase phase; 191 | 192 | s32 start_level; 193 | s32 level; 194 | s32 line_count; 195 | s32 points; 196 | 197 | f32 next_drop_time; 198 | f32 highlight_end_time; 199 | f32 time; 200 | }; 201 | 202 | struct Input_State 203 | { 204 | u8 left; 205 | u8 right; 206 | u8 up; 207 | u8 down; 208 | 209 | u8 a; 210 | 211 | s8 dleft; 212 | s8 dright; 213 | s8 dup; 214 | s8 ddown; 215 | s8 da; 216 | }; 217 | 218 | enum Text_Align 219 | { 220 | TEXT_ALIGN_LEFT, 221 | TEXT_ALIGN_CENTER, 222 | TEXT_ALIGN_RIGHT 223 | }; 224 | 225 | static u8 226 | matrix_get(const u8 *values, s32 width, s32 row, s32 col) 227 | { 228 | s32 index = row * width + col; 229 | return values[index]; 230 | } 231 | 232 | static void 233 | matrix_set(u8 *values, s32 width, s32 row, s32 col, u8 value) 234 | { 235 | s32 index = row * width + col; 236 | values[index] = value; 237 | } 238 | 239 | static u8 240 | tetrino_get(const struct Tetrino *tetrino, s32 row, s32 col, s32 rotation) 241 | { 242 | s32 side = tetrino->side; 243 | switch (rotation) 244 | { 245 | case 0: 246 | return tetrino->data[row * side + col]; 247 | case 1: 248 | return tetrino->data[(side - col - 1) * side + row]; 249 | case 2: 250 | return tetrino->data[(side - row - 1) * side + (side - col - 1)]; 251 | case 3: 252 | return tetrino->data[col * side + (side - row - 1)]; 253 | } 254 | return 0; 255 | } 256 | 257 | static u8 258 | check_row_filled(const u8 *values, s32 width, s32 row) 259 | { 260 | for (s32 col = 0; 261 | col < width; 262 | ++col) 263 | { 264 | if (!matrix_get(values, width, row, col)) 265 | { 266 | return 0; 267 | } 268 | } 269 | return 1; 270 | } 271 | 272 | static u8 273 | check_row_empty(const u8 *values, s32 width, s32 row) 274 | { 275 | for (s32 col = 0; 276 | col < width; 277 | ++col) 278 | { 279 | if (matrix_get(values, width, row, col)) 280 | { 281 | return 0; 282 | } 283 | } 284 | return 1; 285 | } 286 | 287 | static s32 288 | find_lines(const u8 *values, s32 width, s32 height, u8 *lines_out) 289 | { 290 | s32 count = 0; 291 | for (s32 row = 0; 292 | row < height; 293 | ++row) 294 | { 295 | u8 filled = check_row_filled(values, width, row); 296 | lines_out[row] = filled; 297 | count += filled; 298 | } 299 | return count; 300 | } 301 | 302 | static void 303 | clear_lines(u8 *values, s32 width, s32 height, const u8 *lines) 304 | { 305 | s32 src_row = height - 1; 306 | for (s32 dst_row = height - 1; 307 | dst_row >= 0; 308 | --dst_row) 309 | { 310 | while (src_row >= 0 && lines[src_row]) 311 | { 312 | --src_row; 313 | } 314 | 315 | if (src_row < 0) 316 | { 317 | memset(values + dst_row * width, 0, width); 318 | } 319 | else 320 | { 321 | if (src_row != dst_row) 322 | { 323 | memcpy(values + dst_row * width, 324 | values + src_row * width, 325 | width); 326 | } 327 | --src_row; 328 | } 329 | } 330 | } 331 | 332 | 333 | static bool 334 | check_piece_valid(const struct Piece_State *piece, 335 | const u8 *board, s32 width, s32 height) 336 | { 337 | const struct Tetrino *tetrino = TETRINOS + piece->tetrino_index; 338 | assert(tetrino); 339 | 340 | for (s32 row = 0; 341 | row < tetrino->side; 342 | ++row) 343 | { 344 | for (s32 col = 0; 345 | col < tetrino->side; 346 | ++col) 347 | { 348 | u8 value = tetrino_get(tetrino, row, col, piece->rotation); 349 | if (value > 0) 350 | { 351 | s32 board_row = piece->offset_row + row; 352 | s32 board_col = piece->offset_col + col; 353 | if (board_row < 0) 354 | { 355 | return false; 356 | } 357 | if (board_row >= height) 358 | { 359 | return false; 360 | } 361 | if (board_col < 0) 362 | { 363 | return false; 364 | } 365 | if (board_col >= width) 366 | { 367 | return false; 368 | } 369 | if (matrix_get(board, width, board_row, board_col)) 370 | { 371 | return false; 372 | } 373 | } 374 | } 375 | } 376 | return true; 377 | } 378 | 379 | static void 380 | merge_piece(struct Game_State *game) 381 | { 382 | const struct Tetrino *tetrino = TETRINOS + game->piece.tetrino_index; 383 | for (s32 row = 0; 384 | row < tetrino->side; 385 | ++row) 386 | { 387 | for (s32 col = 0; 388 | col < tetrino->side; 389 | ++col) 390 | { 391 | u8 value = tetrino_get(tetrino, row, col, game->piece.rotation); 392 | if (value) 393 | { 394 | s32 board_row = game->piece.offset_row + row; 395 | s32 board_col = game->piece.offset_col + col; 396 | matrix_set(game->board, WIDTH, board_row, board_col, value); 397 | } 398 | } 399 | } 400 | } 401 | 402 | static s32 403 | random_int(s32 min, s32 max) 404 | { 405 | s32 range = max - min; 406 | return min + rand() % range; 407 | } 408 | 409 | static f32 410 | get_time_to_next_drop(s32 level) 411 | { 412 | if (level > 29) 413 | { 414 | level = 29; 415 | } 416 | return FRAMES_PER_DROP[level] * TARGET_SECONDS_PER_FRAME; 417 | } 418 | 419 | 420 | static void 421 | spawn_piece(struct Game_State *game) 422 | { 423 | ZERO_STRUCT(game->piece); 424 | game->piece.tetrino_index = (u8)random_int(0, ARRAY_COUNT(TETRINOS)); 425 | game->piece.offset_col = WIDTH / 2; 426 | game->next_drop_time = game->time + get_time_to_next_drop(game->level); 427 | } 428 | 429 | 430 | static bool 431 | soft_drop(struct Game_State *game) 432 | { 433 | ++game->piece.offset_row; 434 | if (!check_piece_valid(&game->piece, game->board, WIDTH, HEIGHT)) 435 | { 436 | --game->piece.offset_row; 437 | merge_piece(game); 438 | spawn_piece(game); 439 | return false; 440 | } 441 | 442 | game->next_drop_time = game->time + get_time_to_next_drop(game->level); 443 | return true; 444 | } 445 | 446 | static s32 447 | compute_points(s32 level, s32 line_count) 448 | { 449 | switch (line_count) 450 | { 451 | case 1: 452 | return 40 * (level + 1); 453 | case 2: 454 | return 100 * (level + 1); 455 | case 3: 456 | return 300 * (level + 1); 457 | case 4: 458 | return 1200 * (level + 1); 459 | } 460 | return 0; 461 | } 462 | 463 | static s32 464 | min(s32 x, s32 y) 465 | { 466 | return x < y ? x : y; 467 | } 468 | static s32 469 | max(s32 x, s32 y) 470 | { 471 | return x > y ? x : y; 472 | } 473 | 474 | static s32 475 | get_lines_for_next_level(s32 start_level, s32 level) 476 | { 477 | s32 first_level_up_limit = min( 478 | (start_level * 10 + 10), 479 | max(100, (start_level * 10 - 50))); 480 | if (level == start_level) 481 | { 482 | return first_level_up_limit; 483 | } 484 | s32 diff = level - start_level; 485 | return first_level_up_limit + diff * 10; 486 | } 487 | 488 | static void 489 | update_game_start(struct Game_State *game, const struct Input_State *input) 490 | { 491 | if (input->dup > 0) 492 | { 493 | ++game->start_level; 494 | } 495 | 496 | if (input->ddown > 0 && game->start_level > 0) 497 | { 498 | --game->start_level; 499 | } 500 | 501 | if (input->da > 0) 502 | { 503 | memset(game->board, 0, WIDTH * HEIGHT); 504 | game->level = game->start_level; 505 | game->line_count = 0; 506 | game->points = 0; 507 | spawn_piece(game); 508 | game->phase = GAME_PHASE_PLAY; 509 | } 510 | } 511 | 512 | static void 513 | update_game_gameover(struct Game_State *game, const struct Input_State *input) 514 | { 515 | if (input->da > 0) 516 | { 517 | game->phase = GAME_PHASE_START; 518 | } 519 | } 520 | 521 | static void 522 | update_game_line(struct Game_State *game) 523 | { 524 | if (game->time >= game->highlight_end_time) 525 | { 526 | clear_lines(game->board, WIDTH, HEIGHT, game->lines); 527 | game->line_count += game->pending_line_count; 528 | game->points += compute_points(game->level, game->pending_line_count); 529 | 530 | s32 lines_for_next_level = get_lines_for_next_level(game->start_level, 531 | game->level); 532 | if (game->line_count >= lines_for_next_level) 533 | { 534 | ++game->level; 535 | } 536 | 537 | game->phase = GAME_PHASE_PLAY; 538 | } 539 | } 540 | 541 | static void 542 | update_game_play(struct Game_State *game, 543 | const struct Input_State *input) 544 | { 545 | struct Piece_State piece = game->piece; 546 | if (input->dleft > 0) 547 | { 548 | --piece.offset_col; 549 | } 550 | if (input->dright> 0) 551 | { 552 | ++piece.offset_col; 553 | } 554 | if (input->dup > 0) 555 | { 556 | piece.rotation = (piece.rotation + 1) % 4; 557 | } 558 | 559 | if (check_piece_valid(&piece, game->board, WIDTH, HEIGHT)) 560 | { 561 | game->piece = piece; 562 | } 563 | 564 | if (input->ddown > 0) 565 | { 566 | soft_drop(game); 567 | } 568 | 569 | if (input->da > 0) 570 | { 571 | while(soft_drop(game)); 572 | } 573 | 574 | while (game->time >= game->next_drop_time) 575 | { 576 | soft_drop(game); 577 | } 578 | 579 | game->pending_line_count = find_lines(game->board, WIDTH, HEIGHT, game->lines); 580 | if (game->pending_line_count > 0) 581 | { 582 | game->phase = GAME_PHASE_LINE; 583 | game->highlight_end_time = game->time + 0.5f; 584 | } 585 | 586 | s32 game_over_row = 0; 587 | if (!check_row_empty(game->board, WIDTH, game_over_row)) 588 | { 589 | game->phase = GAME_PHASE_GAMEOVER; 590 | } 591 | } 592 | 593 | static void 594 | update_game(struct Game_State *game, 595 | const struct Input_State *input) 596 | { 597 | switch(game->phase) 598 | { 599 | case GAME_PHASE_START: 600 | update_game_start(game, input); 601 | break; 602 | case GAME_PHASE_PLAY: 603 | update_game_play(game, input); 604 | break; 605 | case GAME_PHASE_LINE: 606 | update_game_line(game); 607 | break; 608 | case GAME_PHASE_GAMEOVER: 609 | update_game_gameover(game, input); 610 | break; 611 | } 612 | } 613 | 614 | static void 615 | fill_rect(SDL_Renderer *renderer, 616 | s32 x, s32 y, s32 width, s32 height, struct Color color) 617 | { 618 | SDL_Rect rect = {0}; 619 | rect.x = x; 620 | rect.y = y; 621 | rect.w = width; 622 | rect.h = height; 623 | SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); 624 | SDL_RenderFillRect(renderer, &rect); 625 | } 626 | 627 | 628 | static void 629 | draw_rect(SDL_Renderer *renderer, 630 | s32 x, s32 y, s32 width, s32 height, struct Color color) 631 | { 632 | SDL_Rect rect = {0}; 633 | rect.x = x; 634 | rect.y = y; 635 | rect.w = width; 636 | rect.h = height; 637 | SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a); 638 | SDL_RenderDrawRect(renderer, &rect); 639 | } 640 | 641 | static void 642 | draw_string(SDL_Renderer *renderer, 643 | TTF_Font *font, 644 | const char *text, 645 | s32 x, s32 y, 646 | enum Text_Align alignment, 647 | struct Color color) 648 | { 649 | SDL_Color sdl_color = { color.r, color.g, color.b, color.a }; 650 | SDL_Surface *surface = TTF_RenderText_Solid(font, text, sdl_color); 651 | SDL_Texture *texture = SDL_CreateTextureFromSurface(renderer, surface); 652 | 653 | SDL_Rect rect; 654 | rect.y = y; 655 | rect.w = surface->w; 656 | rect.h = surface->h; 657 | switch (alignment) 658 | { 659 | case TEXT_ALIGN_LEFT: 660 | rect.x = x; 661 | break; 662 | case TEXT_ALIGN_CENTER: 663 | rect.x = x - surface->w / 2; 664 | break; 665 | case TEXT_ALIGN_RIGHT: 666 | rect.x = x - surface->w; 667 | break; 668 | } 669 | 670 | SDL_RenderCopy(renderer, texture, 0, &rect); 671 | SDL_FreeSurface(surface); 672 | SDL_DestroyTexture(texture); 673 | } 674 | 675 | static void 676 | draw_cell(SDL_Renderer *renderer, 677 | s32 row, s32 col, u8 value, 678 | s32 offset_x, s32 offset_y, 679 | bool outline) 680 | { 681 | struct Color base_color = BASE_COLORS[value]; 682 | struct Color light_color = LIGHT_COLORS[value]; 683 | struct Color dark_color = DARK_COLORS[value]; 684 | 685 | 686 | s32 edge = GRID_SIZE / 8; 687 | 688 | s32 x = col * GRID_SIZE + offset_x; 689 | s32 y = row * GRID_SIZE + offset_y; 690 | 691 | if (outline) 692 | { 693 | draw_rect(renderer, x, y, GRID_SIZE, GRID_SIZE, base_color); 694 | return; 695 | } 696 | 697 | fill_rect(renderer, x, y, GRID_SIZE, GRID_SIZE, dark_color); 698 | fill_rect(renderer, x + edge, y, 699 | GRID_SIZE - edge, GRID_SIZE - edge, light_color); 700 | fill_rect(renderer, x + edge, y + edge, 701 | GRID_SIZE - edge * 2, GRID_SIZE - edge * 2, base_color); 702 | } 703 | 704 | static void 705 | draw_piece(SDL_Renderer *renderer, 706 | const struct Piece_State *piece, 707 | s32 offset_x, s32 offset_y, 708 | bool outline) 709 | { 710 | const struct Tetrino *tetrino = TETRINOS + piece->tetrino_index; 711 | for (s32 row = 0; 712 | row < tetrino->side; 713 | ++row) 714 | { 715 | for (s32 col = 0; 716 | col < tetrino->side; 717 | ++col) 718 | { 719 | u8 value = tetrino_get(tetrino, row, col, piece->rotation); 720 | if (value) 721 | { 722 | draw_cell(renderer, 723 | row + piece->offset_row, 724 | col + piece->offset_col, 725 | value, 726 | offset_x, offset_y, 727 | outline); 728 | } 729 | } 730 | } 731 | } 732 | 733 | static void 734 | draw_board(SDL_Renderer *renderer, 735 | const u8 *board, s32 width, s32 height, 736 | s32 offset_x, s32 offset_y) 737 | { 738 | fill_rect(renderer, offset_x, offset_y, 739 | width * GRID_SIZE, height * GRID_SIZE, 740 | BASE_COLORS[0]); 741 | for (s32 row = 0; 742 | row < height; 743 | ++row) 744 | { 745 | for (s32 col = 0; 746 | col < width; 747 | ++col) 748 | { 749 | u8 value = matrix_get(board, width, row, col); 750 | if (value) 751 | { 752 | draw_cell(renderer, row, col, value, offset_x, offset_y, false); 753 | } 754 | } 755 | } 756 | } 757 | 758 | static void 759 | render_game(const struct Game_State *game, 760 | SDL_Renderer *renderer, 761 | TTF_Font *font) 762 | { 763 | 764 | char buffer[4096]; 765 | 766 | struct Color highlight_color = { 0xFF, 0xFF, 0xFF, 0xFF }; 767 | 768 | s32 margin_y = 60; 769 | 770 | draw_board(renderer, game->board, WIDTH, HEIGHT, 0, margin_y); 771 | 772 | if (game->phase == GAME_PHASE_PLAY) 773 | { 774 | draw_piece(renderer, &game->piece, 0, margin_y, false); 775 | 776 | struct Piece_State piece = game->piece; 777 | while (check_piece_valid(&piece, game->board, WIDTH, HEIGHT)) 778 | { 779 | piece.offset_row++; 780 | } 781 | --piece.offset_row; 782 | 783 | draw_piece(renderer, &piece, 0, margin_y, true); 784 | 785 | } 786 | 787 | if (game->phase == GAME_PHASE_LINE) 788 | { 789 | for (s32 row = 0; 790 | row < HEIGHT; 791 | ++row) 792 | { 793 | if (game->lines[row]) 794 | { 795 | s32 x = 0; 796 | s32 y = row * GRID_SIZE + margin_y; 797 | 798 | fill_rect(renderer, x, y, 799 | WIDTH * GRID_SIZE, GRID_SIZE, highlight_color); 800 | } 801 | } 802 | } 803 | else if (game->phase == GAME_PHASE_GAMEOVER) 804 | { 805 | s32 x = WIDTH * GRID_SIZE / 2; 806 | s32 y = (HEIGHT * GRID_SIZE + margin_y) / 2; 807 | draw_string(renderer, font, "GAME OVER", 808 | x, y, TEXT_ALIGN_CENTER, highlight_color); 809 | } 810 | else if (game->phase == GAME_PHASE_START) 811 | { 812 | s32 x = WIDTH * GRID_SIZE / 2; 813 | s32 y = (HEIGHT * GRID_SIZE + margin_y) / 2; 814 | draw_string(renderer, font, "PRESS START", 815 | x, y, TEXT_ALIGN_CENTER, highlight_color); 816 | 817 | snprintf(buffer, sizeof(buffer), "STARTING LEVEL: %d", game->start_level); 818 | draw_string(renderer, font, buffer, 819 | x, y + 30, TEXT_ALIGN_CENTER, highlight_color); 820 | } 821 | 822 | struct Color black_color = { 0x00, 0x00, 0x00, 0x00 }; 823 | 824 | fill_rect(renderer, 825 | 0, margin_y, 826 | WIDTH * GRID_SIZE, (HEIGHT - VISIBLE_HEIGHT) * GRID_SIZE, 827 | black_color); 828 | 829 | 830 | snprintf(buffer, sizeof(buffer), "LEVEL: %d", game->level); 831 | draw_string(renderer, font, buffer, 5, 5, TEXT_ALIGN_LEFT, highlight_color); 832 | 833 | snprintf(buffer, sizeof(buffer), "LINES: %d", game->line_count); 834 | draw_string(renderer, font, buffer, 5, 35, TEXT_ALIGN_LEFT, highlight_color); 835 | 836 | snprintf(buffer, sizeof(buffer), "POINTS: %d", game->points); 837 | draw_string(renderer, font, buffer, 5, 65, TEXT_ALIGN_LEFT, highlight_color); 838 | } 839 | 840 | int 841 | main() 842 | { 843 | if (SDL_Init(SDL_INIT_VIDEO) < 0) 844 | { 845 | return 1; 846 | } 847 | 848 | if (TTF_Init() < 0) 849 | { 850 | return 2; 851 | } 852 | 853 | SDL_Window *window = SDL_CreateWindow( 854 | "Tetris", 855 | SDL_WINDOWPOS_UNDEFINED, 856 | SDL_WINDOWPOS_UNDEFINED, 857 | 300, 858 | 720, 859 | SDL_WINDOW_OPENGL | SDL_WINDOW_SHOWN); 860 | SDL_Renderer *renderer = SDL_CreateRenderer( 861 | window, 862 | -1, 863 | SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC); 864 | 865 | const char *font_name = "novem___.ttf"; 866 | TTF_Font *font = TTF_OpenFont(font_name, 24); 867 | 868 | struct Game_State game; 869 | struct Input_State input; 870 | 871 | ZERO_STRUCT(game); 872 | ZERO_STRUCT(input); 873 | 874 | spawn_piece(&game); 875 | 876 | game.piece.tetrino_index = 2; 877 | 878 | bool quit = false; 879 | while (!quit) 880 | { 881 | game.time = SDL_GetTicks() / 1000.0f; 882 | 883 | SDL_Event e; 884 | while (SDL_PollEvent(&e) != 0) 885 | { 886 | if (e.type == SDL_QUIT) 887 | { 888 | quit = true; 889 | } 890 | } 891 | 892 | s32 key_count; 893 | const u8 *key_states = SDL_GetKeyboardState(&key_count); 894 | 895 | if (key_states[SDL_SCANCODE_ESCAPE]) 896 | { 897 | quit = true; 898 | } 899 | 900 | struct Input_State prev_input = input; 901 | 902 | input.left = key_states[SDL_SCANCODE_LEFT]; 903 | input.right = key_states[SDL_SCANCODE_RIGHT]; 904 | input.up = key_states[SDL_SCANCODE_UP]; 905 | input.down = key_states[SDL_SCANCODE_DOWN]; 906 | input.a = key_states[SDL_SCANCODE_SPACE]; 907 | 908 | input.dleft = (s8)input.left - (s8)prev_input.left; 909 | input.dright = (s8)input.right - (s8)prev_input.right; 910 | input.dup = (s8)input.up - (s8)prev_input.up; 911 | input.ddown = (s8)input.down - (s8)prev_input.down; 912 | input.da = (s8)input.a - (s8)prev_input.a; 913 | 914 | SDL_SetRenderDrawColor(renderer, 0, 0, 0, 0); 915 | SDL_RenderClear(renderer); 916 | 917 | update_game(&game, &input); 918 | render_game(&game, renderer, font); 919 | 920 | SDL_RenderPresent(renderer); 921 | } 922 | 923 | TTF_CloseFont(font); 924 | SDL_DestroyRenderer(renderer); 925 | SDL_Quit(); 926 | 927 | return 0; 928 | } 929 | --------------------------------------------------------------------------------