#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #define TILE_SIZE 20 #define GRID_WIDTH 14 #define GRID_HEIGHT 20 #define WINDOW_WIDTH 500 #define WINDOW_HEIGHT 440 #define PLAYFIELD_X 20 #define PLAYFIELD_Y 20 #define LINES_PER_LEVEL 5 #define POINTS_1_LINE 40 #define POINTS_2_LINES 100 #define POINTS_3_LINES 300 #define POINTS_4_LINES 1200 class NumberRenderer { public: sf::Texture texture; sf::IntRect comma_rect; sf::IntRect numeral_rects[10]; NumberRenderer( sf::Texture _texture, sf::IntRect _comma_rect, std::initializer_list _numeral_rects ) { texture = _texture; comma_rect = _comma_rect; sprite = sf::Sprite(texture); int i = 0; for (auto numeral_rect = _numeral_rects.begin(); numeral_rect != _numeral_rects.end(); ++numeral_rect) { numeral_rects[i] = *numeral_rect; i++; } } void render(sf::RenderWindow* window, uint number, int x, int y) { auto number_string = std::to_string(number); std::string numeral_string; numeral_string.push_back(number_string.back()); auto numeral_rect = numeral_rects[std::stoi(numeral_string)]; int x_offset = -numeral_rect.width; uint digits = number_string.length(); for (int i = digits - 1; i >= 0; i--) { char numeral_string[] = {number_string[i]}; auto numeral_rect = numeral_rects[std::stoi(numeral_string)]; if ((digits - i) % 3 == 1 && i != digits - 1) { sprite.setTextureRect(comma_rect); sprite.setPosition(x + x_offset, y); window->draw(sprite); x_offset -= numeral_rect.width; } sprite.setTextureRect(numeral_rect); sprite.setPosition(x + x_offset, y); window->draw(sprite); if (i == 0) { break; } if ((digits - i) % 3 == 0) { x_offset -= comma_rect.width; continue; } numeral_string[0] = number_string[i - 1]; numeral_rect = numeral_rects[std::stoi(numeral_string)]; x_offset -= numeral_rect.width; } } private: sf::Sprite sprite; }; class TileType { public: sf::IntRect texture_rect; sf::IntRect ghost_texture_rect; TileType(sf::IntRect _texture_rect, sf::IntRect _ghost_texture_rect) { texture_rect = _texture_rect; ghost_texture_rect = _ghost_texture_rect; } }; TileType tile_type_0( sf::IntRect(0, 0, TILE_SIZE, TILE_SIZE), sf::IntRect(0, TILE_SIZE, TILE_SIZE, TILE_SIZE) ); TileType tile_type_1( sf::IntRect(TILE_SIZE, 0, TILE_SIZE, TILE_SIZE), sf::IntRect(TILE_SIZE, TILE_SIZE, TILE_SIZE, TILE_SIZE) ); TileType tile_type_2( sf::IntRect(TILE_SIZE * 2, 0, TILE_SIZE, TILE_SIZE), sf::IntRect(TILE_SIZE * 2, TILE_SIZE, TILE_SIZE, TILE_SIZE) ); TileType tile_type_3( sf::IntRect(TILE_SIZE * 3, 0, TILE_SIZE, TILE_SIZE), sf::IntRect(TILE_SIZE * 3, TILE_SIZE, TILE_SIZE, TILE_SIZE) ); TileType tile_type_4( sf::IntRect(TILE_SIZE * 4, 0, TILE_SIZE, TILE_SIZE), sf::IntRect(TILE_SIZE * 4, TILE_SIZE, TILE_SIZE, TILE_SIZE) ); TileType tile_type_5( sf::IntRect(TILE_SIZE * 5, 0, TILE_SIZE, TILE_SIZE), sf::IntRect(TILE_SIZE * 5, TILE_SIZE, TILE_SIZE, TILE_SIZE) ); TileType tile_type_6( sf::IntRect(TILE_SIZE * 6, 0, TILE_SIZE, TILE_SIZE), sf::IntRect(TILE_SIZE * 6, TILE_SIZE, TILE_SIZE, TILE_SIZE) ); class BlockType { public: static BlockType i, j, l, o, s, t, z; static BlockType* list[]; static BlockType* random() { return list[rand() % 7]; } TileType* tile_type; std::vector> grid; uint width, height, starting_line; bool rotate; BlockType(TileType* _tile_type, const std::vector> _grid, bool _rotate = true) { tile_type = _tile_type; grid = _grid; rotate = _rotate; // Used for alignment in "next block" area width = 0; starting_line = 0; for (uint y = 0; y < grid.size(); y++) { bool has_content = false; for (uint x = 0; x < grid[y].size(); x++) { if (grid[y][x]) { width = std::max({width, x + 1}); has_content = true; } } if (has_content) { if (height == 0) { starting_line = y; } height = y + 1 - starting_line; } } } }; // https://gamedev.stackexchange.com/a/17978 BlockType BlockType::i(&tile_type_0, { {0, 0, 0, 0}, {1, 1, 1, 1}, {0, 0, 0, 0}, {0, 0, 0, 0} }); BlockType BlockType::j(&tile_type_1, { {1, 0, 0}, {1, 1, 1}, {0, 0, 0} }); BlockType BlockType::l(&tile_type_2, { {0, 0, 1}, {1, 1, 1}, {0, 0, 0} }); BlockType BlockType::o(&tile_type_3, { {1, 1}, {1, 1} }, false); BlockType BlockType::s(&tile_type_4, { {0, 1, 1}, {1, 1, 0}, {0, 0, 0} }); BlockType BlockType::t(&tile_type_5, { {0, 1, 0}, {1, 1, 1}, {0, 0, 0} }); BlockType BlockType::z(&tile_type_6, { {1, 1, 0}, {0, 1, 1}, {0, 0, 0} }); BlockType* BlockType::list[] = {&i, &j, &l, &o, &s, &t, &z}; class Block { public: BlockType* type; sf::Vector2i position; int rotation_state; Block() { type = BlockType::random(); position = sf::Vector2i(GRID_WIDTH / 2 - type->grid[0].size() / 2, 0); rotation_state = 0; } std::vector get_tiles() { std::vectortiles = {}; for (int y = 0; y < type->grid.size(); y++) { for (int x = 0; x < type->grid[y].size(); x++) { if (!type->grid[y][x]) { continue; } int rotated_x = x; int rotated_y = y; if (type->rotate) { int center_x = type->grid[0].size() / 2; int center_y = type->grid.size() / 2; int offset_x = x - center_x; int offset_y = y - center_y; switch (rotation_state) { case 0: rotated_x = x; rotated_y = y; break; case 1: rotated_x = center_x + offset_y; rotated_y = center_y - offset_x; break; case 2: rotated_x = center_x - offset_x; rotated_y = center_y - offset_y; break; case 3: rotated_x = center_x - offset_y; rotated_y = center_y + offset_x; break; default: rotation_state %= 4; } } int global_x = rotated_x + position.x; int global_y = rotated_y + position.y; tiles.push_back(sf::Vector2i(global_x, global_y)); } } return tiles; } }; uint get_level(int lines) { return std::min(lines / LINES_PER_LEVEL, 15); } uint get_update_interval(int level) { // From Tetris Worlds, see https://harddrop.com/wiki/Tetris_Worlds#Gravity return pow(0.8 - (level - 1) * 0.007, level - 1) * 1000; } int main() { srand(time(NULL)); sf::RenderWindow window(sf::VideoMode(WINDOW_WIDTH, WINDOW_HEIGHT), "elnutris", sf::Style::Close); window.setFramerateLimit(60); window.setKeyRepeatEnabled(false); // prevent keys from retriggering when held Block block; Block next_block; TileType* grid[GRID_HEIGHT][GRID_WIDTH] = { nullptr }; sf::Texture blocks_texture; blocks_texture.loadFromMemory(&BLOCKS_TEXTURE_DATA, sizeof(BLOCKS_TEXTURE_DATA)); sf::Sprite sprite; sprite.setTexture(blocks_texture); sf::Texture background_texture; background_texture.loadFromMemory(&BACKGROUND_TEXTURE_DATA, sizeof(BACKGROUND_TEXTURE_DATA)); sf::Sprite background; background.setTexture(background_texture); sf::Texture numeral_texture; numeral_texture.loadFromMemory(&NUMERALS_TEXTURE_DATA, sizeof(NUMERALS_TEXTURE_DATA)); NumberRenderer number_renderer(numeral_texture, sf::IntRect(134, 0, 10, 16), { sf::IntRect(0, 0, 14, 16), sf::IntRect(14, 0, 8, 16), sf::IntRect(22, 0, 14, 16), sf::IntRect(36, 0, 14, 16), sf::IntRect(50, 0, 14, 16), sf::IntRect(64, 0, 14, 16), sf::IntRect(78, 0, 14, 16), sf::IntRect(92, 0, 14, 16), sf::IntRect(106, 0, 14, 16), sf::IntRect(120, 0, 14, 16) }); bool rotate = false; bool move_left = false; bool move_right = false; bool move_left_immediate = false; bool move_right_immediate = false; bool snap = false; sf::Clock update_clock; sf::Clock move_clock; // https://stackoverflow.com/a/478088 const char *homedir; if ((homedir = getenv("HOME")) == NULL) { homedir = getpwuid(getuid())->pw_dir; } std::string highscore_file_path = homedir; highscore_file_path += "/.elnutris"; if (!std::filesystem::exists(highscore_file_path)) { std::ofstream highscore_file(highscore_file_path); highscore_file << "0"; highscore_file.close(); } std::fstream highscore_file(highscore_file_path); std::string highscore_string; highscore_file >> highscore_string; uint highscore = std::stoi(highscore_string); uint score = 0; uint lines = 0; uint blocks = 0; uint tiles = 0; uint update_interval = get_update_interval(0); // https://sfxr.me/#57uBnWWZeyDTsBRrJsAp2Vwd76cMVrdeRQ7DirNQW5XekKxcrCUNx47Zggh7Uqw4R5FdeUpyk362uhjWmpNHmqxE7JBp3EkxDxfJ1VjzMRpuSHieW6B5iyVFM sf::SoundBuffer rotate_buffer; rotate_buffer.loadFromMemory(&ROTATE_AUDIO_DATA, sizeof(ROTATE_AUDIO_DATA)); sf::Sound rotate_sound; rotate_sound.setBuffer(rotate_buffer); // https://sfxr.me/#57uBnWTMa2LUtaPa3P8xWZekiRxNwCPFWpRoPDVXDJM9KHkiGJcs6J62FRcjMY5oVNdT73MtmUf5rXCPvSZWL7AZuTRWWjKbPKTpZjT85AcZ6htUqTswkjksZ sf::SoundBuffer snap_buffer; snap_buffer.loadFromMemory(&SNAP_AUDIO_DATA, sizeof(SNAP_AUDIO_DATA)); sf::Sound snap_sound; snap_sound.setBuffer(snap_buffer); // https://sfxr.me/#57uBnWbareN7MJJsWGD8eFCrqjikS9f8JXg8jvmKzMdVtqmRsb81eToSUpnkqgFhvxD2QoAjpw4SmGZHZjbhEiPQKetRSHCHXYFZzD7Q6RVVS9CRSeRAb6bZp sf::SoundBuffer game_over_buffer; game_over_buffer.loadFromMemory(&GAME_OVER_AUDIO_DATA, sizeof(GAME_OVER_AUDIO_DATA)); sf::Sound game_over_sound; game_over_sound.setBuffer(game_over_buffer); // https://sfxr.me/#7BMHBGMfGk8EHV8czJkUucUm8EMAnMNxiqYyTfKkMpHFJu44GEdD7xP6E8NM3K7RKRExTpagPBAiWf7BLtC52CEWJVGHh8hwDLygoEG86tcPth2UtmfdrXLoh sf::SoundBuffer row_clear_buffer; row_clear_buffer.loadFromMemory(&ROW_CLEAR_AUDIO_DATA, sizeof(ROW_CLEAR_AUDIO_DATA)); sf::Sound row_clear_sound; row_clear_sound.setBuffer(row_clear_buffer); // https://sfxr.me/#57uBnWg8448kTPqWAxeDvZ5CP5JWbrfJGWuRcTjva5uX3vvBnEAZ6SfiH9oLKMXgsusuJwGWx6KPfvLfHtqnhLxr476ptGv4jPbfNhQaFMYeMHFdHk9SotQ4X sf::SoundBuffer level_up_buffer; level_up_buffer.loadFromMemory(&LEVEL_UP_AUDIO_DATA, sizeof(LEVEL_UP_AUDIO_DATA)); sf::Sound level_up_sound; level_up_sound.setBuffer(level_up_buffer); // https://sfxr.me/#34T6PkzvrkfdahGDBAh1uYGXTwZ8rG54kxfHpgdVCPxqG7yyK5UuqgiK9Z8Q5177itxbkSNfLSHm4zTkemT4iyxJpW89VJx82feaq8qxZeA5AJR2nWZZR59hq sf::SoundBuffer new_highscore_buffer; new_highscore_buffer.loadFromMemory(&NEW_HIGHSCORE_AUDIO_DATA, sizeof(NEW_HIGHSCORE_AUDIO_DATA)); sf::Sound new_highscore_sound; new_highscore_sound.setBuffer(new_highscore_buffer); while (window.isOpen()) { sf::Event event; while (window.pollEvent(event)) { switch (event.type) { case sf::Event::Closed: window.close(); break; case sf::Event::KeyPressed: switch (event.key.code) { case sf::Keyboard::Space: snap = true; break; case sf::Keyboard::Up: rotate = true; break; case sf::Keyboard::Left: move_left = true; move_left_immediate = true; move_clock.restart(); break; case sf::Keyboard::Right: move_right = true; move_right_immediate = true; move_clock.restart(); break; default: break; } break; case sf::Event::KeyReleased: switch (event.key.code) { case sf::Keyboard::Left: move_left = false; break; case sf::Keyboard::Right: move_right = false; break; default: break; } default: break; } } bool is_update_frame = update_clock.getElapsedTime().asMilliseconds() > (sf::Keyboard::isKeyPressed(sf::Keyboard::Down) ? std::min({update_interval, 125u}) : update_interval); if (is_update_frame) { update_clock.restart(); } bool is_move_frame = move_clock.getElapsedTime().asMilliseconds() > 125; if (is_move_frame) { move_clock.restart(); } // Rotation if (rotate) { block.rotation_state++; // Check to see if new rotation state is overlapping any tiles for (auto tile : block.get_tiles()) { if (tile.x <= 0 || tile.x >= GRID_WIDTH || grid[tile.y][tile.x]) { block.rotation_state--; break; } } rotate = false; rotate_sound.play(); } // Horizontal movement int movement = 0; if (move_left_immediate || is_move_frame && move_left) { movement--; move_left_immediate = false; } if (move_right_immediate || is_move_frame && move_right) { movement++; move_right_immediate = false; } if (movement != 0) { for (auto tile : block.get_tiles()) { if (tile.x + movement < 0 || tile.x + movement >= GRID_WIDTH || grid[tile.y][tile.x + movement]) { goto after_movement_loop; } } block.position.x += movement; } after_movement_loop: // Snapping int snap_offset = 0; while (true) { for (auto tile : block.get_tiles()) { int y = tile.y + snap_offset; if (y == GRID_HEIGHT - 1 || grid[y + 1][tile.x] != nullptr) { goto after_snap_loop; } } snap_offset++; } after_snap_loop: bool landed = snap; if (snap) { block.position.y += snap_offset; snap = false; snap_sound.play(); } // Land checking if (!snap && is_update_frame) { for (auto tile : block.get_tiles()) { if (tile.y == GRID_HEIGHT - 1 || grid[tile.y + 1][tile.x] != nullptr) { landed = true; break; } } } // Clear window // Normally, one would run window.clear(), // but the background image covers the entire window. window.draw(background); // Draw block if (!landed) { for (auto tile : block.get_tiles()) { int snap_y = tile.y + snap_offset; sprite.setTextureRect(block.type->tile_type->texture_rect); sprite.setPosition(PLAYFIELD_X + tile.x * TILE_SIZE, PLAYFIELD_Y + tile.y * TILE_SIZE); window.draw(sprite); sprite.setTextureRect(block.type->tile_type->ghost_texture_rect); sprite.setPosition(PLAYFIELD_X + tile.x * TILE_SIZE, PLAYFIELD_Y + snap_y * TILE_SIZE); window.draw(sprite); } } // Draw next block auto next_block_tiles = next_block.get_tiles(); // This is assuming the next block spawns unrotated. // Refactoring is needed if random rotations are added uint x_offset = next_block.type->width * TILE_SIZE / 2; uint y_offset = (next_block.type->height + next_block.type->starting_line * 2) * TILE_SIZE / 2; for (auto tile : next_block_tiles) { sprite.setTextureRect(next_block.type->tile_type->texture_rect); sprite.setPosition( 370 + (tile.x - next_block.position.x) * TILE_SIZE - x_offset, 70 + (tile.y - next_block.position.y) * TILE_SIZE - y_offset ); window.draw(sprite); } // Landing (transfering block to grid and reinitializing) if (landed) { if (block.position.y == 0) { score = 0; lines = 0; blocks = 0; tiles = 0; for (int y = 0; y < GRID_HEIGHT; y++) { for (int x = 0; x < GRID_WIDTH; x++) { grid[y][x] = nullptr; } } game_over_sound.play(); } else { tiles += block.get_tiles().size(); blocks++; for (auto tile : block.get_tiles()) { grid[tile.y][tile.x] = block.type->tile_type; } uint cleared_lines = 0; // Check for completed rows for (int y = 0; y < GRID_HEIGHT; y++) { bool completed = true; for (int x = 0; x < GRID_WIDTH; x++) { if (!grid[y][x]) { completed = false; break; } } if (!completed) { continue; } for (int z = y - 1; z >= 0; z--) { for (int x = 0; x < GRID_WIDTH; x++) { grid[z + 1][x] = grid[z][x]; } } cleared_lines++; } uint scored; switch (cleared_lines) { case 0: scored = 0; break; case 1: scored = POINTS_1_LINE; break; case 2: scored = POINTS_2_LINES; break; case 3: scored = POINTS_3_LINES; break; default: scored = POINTS_4_LINES; break; } if (scored > 0) { int level = get_level(lines); scored *= level + 1; if (score + scored > highscore && score < highscore) { new_highscore_sound.play(); } score += scored; lines += cleared_lines; if (level != get_level(lines)) { level_up_sound.play(); } if (score > highscore) { highscore = score; std::ofstream highscore_file(highscore_file_path); highscore_file << highscore; highscore_file.close(); } update_interval = get_update_interval(level); row_clear_sound.play(); } } block = next_block; next_block = Block(); } else if(is_update_frame) { block.position.y++; } // Drawing grid for (int y = 0; y < GRID_HEIGHT; y++) { for (int x = 0; x < GRID_WIDTH; x++) { auto tile_type = grid[y][x]; if (tile_type == nullptr) { // If tile_type is a nullptr (no block), continue continue; } sprite.setTextureRect(tile_type->texture_rect); sprite.setPosition(PLAYFIELD_X + x * TILE_SIZE, PLAYFIELD_Y + y * TILE_SIZE); window.draw(sprite); } } number_renderer.render(&window, score, 477, 162); number_renderer.render(&window, highscore, 477, 202); number_renderer.render(&window, lines, 477, 242); number_renderer.render(&window, get_level(lines), 477, 282); number_renderer.render(&window, blocks, 477, 322); number_renderer.render(&window, tiles, 477, 362); window.display(); } return 0; }