#include "game.hpp" #include "engine/data/bounds.hpp" #include "engine/escape.hpp" #include "engine/keycodes.hpp" #include "errors/RLE_reader.hpp" #include "errors/io.hpp" #include "util/algorithm.hpp" #include "util/fs.hpp" #include "util/string.hpp" #include #include #include #include #include #include #include #include Game::Game( IStatusLineFactory statusline_factory, std::shared_ptr scene, std::shared_ptr cursor_controller, std::shared_ptr generation_tracker, std::shared_ptr status_manager, std::shared_ptr user_input_observer, std::shared_ptr cell_helper, std::shared_ptr rle_reader) noexcept : _statusline_factory(std::move(statusline_factory)), _scene(std::move(scene)), _cursor_controller(std::move(cursor_controller)), _generation_tracker(std::move(generation_tracker)), _status_manager(std::move(status_manager)), _user_input_observer(std::move(user_input_observer)), _cell_helper(std::move(cell_helper)), _rle_reader(std::move(rle_reader)), _current_mode(Mode::NORMAL), _normal_mode_cursor_style(CursorStyle::SteadyBlock), _command_mode_cursor_style(CursorStyle::SteadyBar), _minimum_cursor_pos_y(0) { } void Game::on_start() { const auto scene_size = _scene->size(); std::shared_ptr statusline = _statusline_factory(Bounds({.width = scene_size.get_width(), .height = 1})); const auto statusline_section_a_length = 15; const auto statusline_section_b_length = 15; const auto statusline_section_c_length = 15; const auto statusline_section_d_length = 20; const auto statusline_section_e_length = 25; const auto statusline_section_f_length = 30; const auto statusline_section_g_length = 20; const auto statusline_section_h_length = 3; const auto statusline_section_length_total = statusline_section_a_length + statusline_section_b_length + statusline_section_c_length + statusline_section_d_length + statusline_section_e_length + statusline_section_f_length + statusline_section_g_length + statusline_section_h_length; if (statusline_section_length_total > scene_size.get_width()) { throw std::runtime_error( "To small window terminal size. The statusline doesn't fit"); } statusline->set_section_length(StatusLineSection::A, statusline_section_a_length); statusline->set_section_length(StatusLineSection::B, statusline_section_b_length); statusline->set_section_length(StatusLineSection::C, statusline_section_c_length); statusline->set_section_length(StatusLineSection::D, statusline_section_d_length); statusline->set_section_length(StatusLineSection::E, statusline_section_e_length); statusline->set_section_length(StatusLineSection::F, statusline_section_f_length); statusline->set_section_length(StatusLineSection::G, statusline_section_g_length); statusline->set_section_length(StatusLineSection::H, statusline_section_h_length); statusline->set_section_style( StatusLineSection::A, {.bg_color = static_cast(fmt::color::green), .bold = true, .padding_left = 1U, .padding_right = 1U}); _scene->register_component(statusline, Vector2({.x = 0, .y = 1})); _status_manager->bind(statusline); _minimum_cursor_pos_y = 2; _status_manager->set_section_title(StatusLineSection::A, ""); _status_manager->set_section_title(StatusLineSection::B, "X: "); _status_manager->set_section_title(StatusLineSection::C, "Y: "); _status_manager->set_section_title(StatusLineSection::D, "Paused: "); _status_manager->set_section_title(StatusLineSection::E, "Generation: "); _status_manager->set_section_title(StatusLineSection::F, "Generation speed: "); _status_manager->set_section_title(StatusLineSection::G, "Window size: "); const auto center_position = Vector2( {.x = static_cast(scene_size.get_width()) / 2, .y = static_cast(scene_size.get_height()) / 2}); _cursor_controller->move_to(center_position); _status_manager->set_section_body(StatusLineSection::A, "NORMAL"); _status_manager->set_section_body( StatusLineSection::B, fmt::format("{}", center_position.get_x())); _status_manager->set_section_body( StatusLineSection::C, fmt::format("{}", center_position.get_y())); _status_manager->set_section_body( StatusLineSection::D, _generation_tracker->get_is_paused() ? "yes" : "no"); _status_manager->set_section_body(StatusLineSection::E, "0"); _status_manager->set_section_body(StatusLineSection::F, "0"); _status_manager->set_section_body( StatusLineSection::G, fmt::format("{}x{}", scene_size.get_width(), scene_size.get_height())); _cursor_controller->set_cursor_style(_normal_mode_cursor_style); _commands["open"] = CommandInfo( {.option_cnt = 1U, .function = [this](CommandInfo::Options options) { const auto rle_file_path = expand_path_home(std::filesystem::path(options[0])); std::unique_ptr> rle_matrix; try { rle_matrix = _rle_reader->read_RLE_file(rle_file_path); } catch (const InvalidRLEFileError &error) { _show_command_error(fmt::format("Error: {}", error.what())); return; } catch (const IOError &error) { _show_command_error(fmt::format("Error: {}", error.what())); return; } if (rle_matrix == nullptr) { _show_command_error(fmt::format( "A unknown error occurred while reading RLE file with path '{}'", rle_file_path.string())); return; } _return_to_normal_mode(); const auto previous_pos = _cursor_controller->where(); auto scene_matrix = _scene->get_matrix(); for (auto row : *rle_matrix) { for (auto &col : row) { const auto col_pos = _cursor_controller->where(); std::cout.put(col); _cursor_controller->move_to(col_pos); scene_matrix->set(col_pos, col); if (!container_has(_living_cell_positions, col_pos)) { _living_cell_positions.push_back(col_pos); } _cursor_controller->move(Vector2::right(), 1U); } fmt::print("\n"); const auto current_pos = _cursor_controller->where(); _cursor_controller->move_to( Vector2({.x = previous_pos.get_x(), .y = current_pos.get_y() - 1})); } std::cout.flush(); _cursor_controller->move_to(previous_pos); }}); } void Game::on_update() noexcept { if (_current_mode == Mode::COMMAND) { _on_command_mode_update(); return; } _on_normal_mode_update(); } void Game::on_exit() const noexcept { for (auto row : *_scene->get_matrix()) { for (auto &col : row) { fmt::print("{}", col); } fmt::print("\n"); } std::cout.flush(); } void Game::_on_normal_mode_update() noexcept { const auto pressed_key = _user_input_observer->get_currently_pressed_key(); auto cursor_has_moved = false; auto is_generation_stepping = false; switch (pressed_key) { case 'h': if (_move_cursor(Vector2::left())) { cursor_has_moved = true; } break; case 'j': if (_move_cursor(Vector2::down())) { cursor_has_moved = true; } break; case 'k': if (_move_cursor(Vector2::up())) { cursor_has_moved = true; } break; case 'l': if (_move_cursor(Vector2::right())) { cursor_has_moved = true; } break; case 'q': std::exit(EXIT_SUCCESS); case ':': _last_pos_before_command_mode = _cursor_controller->where(); _cursor_controller->move_to(Vector2({.x = 0, .y = 0})); _erase_entire_line(); std::cout << ":"; std::cout.flush(); _cursor_controller->update_position( _cursor_controller->where() + Vector2::right()); _cursor_controller->set_cursor_style(_command_mode_cursor_style); _command_mode_input = ""; _current_mode = Mode::COMMAND; _status_manager->set_section_body(StatusLineSection::A, "COMMAND"); return; case 'i': { const auto position = _cursor_controller->where(); const auto matrix = _scene->get_matrix(); if (matrix->get(position) == 'x') { break; } _set_space(matrix, position, 'x'); _living_cell_positions.push_back(position); break; } case 'x': { const auto position = _cursor_controller->where(); const auto matrix = _scene->get_matrix(); if (matrix->get(position) == ' ') { break; } _set_space(matrix, position, ' '); _living_cell_positions.remove(position); break; } case 'p': { auto onoff = !_generation_tracker->get_is_paused(); _generation_tracker->set_is_paused(onoff); _status_manager->set_section_body(StatusLineSection::D, onoff ? "yes" : "no"); break; } case 's': is_generation_stepping = true; break; case '+': if (_generations_per_second < MAXIMUM_GENERATIONS_PER_SECOND) { _generations_per_second++; } break; case '-': if (_generations_per_second > 1) { _generations_per_second--; } break; default: break; } if (cursor_has_moved) { const auto current_pos = _cursor_controller->where(); _status_manager->set_section_body( StatusLineSection::B, fmt::format("{}", current_pos.get_x())); _status_manager->set_section_body( StatusLineSection::C, fmt::format("{}", current_pos.get_y())); } _status_manager->set_section_body( StatusLineSection::F, fmt::format("{}/s", _generations_per_second)); const auto time_now = std::chrono::system_clock::now(); if (_generation_tracker->get_is_paused() && !is_generation_stepping) { return; } const auto time_since_last_gen = std::chrono::duration_cast( time_now - _last_gen_update_time); if (time_since_last_gen.count() <= MILLIS_IN_SECOND / _generations_per_second) { return; } const auto new_current_gen = _generation_tracker->get_current_generation() + 1U; _generation_tracker->set_current_generation(new_current_gen); _status_manager->set_section_body( StatusLineSection::E, fmt::format("{}", new_current_gen)); _last_gen_update_time = time_now; auto matrix = _scene->get_matrix(); const auto dying_cell_positions = container_filter( _living_cell_positions, [this](const Vector2 &cell_pos) { return _cell_helper->is_cell_dying(cell_pos); }); auto birth_cell_positions = _cell_helper->get_birth_cell_positions(_living_cell_positions); for (const auto &dying_cell_pos : dying_cell_positions) { _set_space(matrix, dying_cell_pos, ' '); const auto cell_found = container_find(_living_cell_positions, dying_cell_pos); if (cell_found != _living_cell_positions.end()) { _living_cell_positions.erase(cell_found); } } for (const auto &birth_cell_pos : birth_cell_positions) { if (birth_cell_pos.get_y() >= _minimum_cursor_pos_y) { _set_space(matrix, birth_cell_pos, 'x'); _living_cell_positions.push_back(birth_cell_pos); } } } void Game::_on_command_mode_update() noexcept { const auto pressed_key = _user_input_observer->get_currently_pressed_key(); switch (pressed_key) { case keycodes::ESCAPE: _erase_entire_line(); _return_to_normal_mode(); break; case keycodes::ENTER: if (!_command_mode_input.empty()) { _run_command(_command_mode_input); } if (_current_mode != Mode::NORMAL) { _return_to_normal_mode(); } break; case keycodes::BACKSPACE: { const auto position = _cursor_controller->where(); const auto input_pos_x = static_cast(position.get_x()) - 1U; if (input_pos_x == 0U) { break; } _command_mode_input.erase(input_pos_x - 1U, 1U); _cursor_controller->move(Vector2::left(), 1U); std::cout.put(' '); std::cout.flush(); _cursor_controller->update_position( _cursor_controller->where() + Vector2::right()); _cursor_controller->move(Vector2::left(), 1U); _erase_line_from_cursor(); std::cout << _command_mode_input.substr(input_pos_x - 1U); std::cout.flush(); _cursor_controller->move_to(position); _cursor_controller->move(Vector2::left(), 1U); break; } case keycodes::LEFT_ARROW: if (_cursor_controller->where().get_x() >= 2) { _cursor_controller->move(Vector2::left(), 1U); } break; case keycodes::RIGHT_ARROW: if (static_cast(_cursor_controller->where().get_x()) - 1U < _command_mode_input.size()) { _cursor_controller->move(Vector2::right(), 1U); } break; case keycodes::UP_ARROW: case keycodes::DOWN_ARROW: case 0: break; default: const auto position = _cursor_controller->where(); const auto input_pos_x = static_cast(position.get_x()) - 1U; if (input_pos_x < _command_mode_input.length()) { _command_mode_input.insert(input_pos_x, 1, pressed_key); _erase_line_from_cursor(); std::cout << pressed_key << _command_mode_input.substr(input_pos_x + 1U); std::cout.flush(); _cursor_controller->move_to(position + Vector2::right()); break; } _command_mode_input += pressed_key; std::cout << pressed_key; std::cout.flush(); _cursor_controller->update_position( _cursor_controller->where() + Vector2::right()); break; } } void Game::_return_to_normal_mode() noexcept { _cursor_controller->move_to(_last_pos_before_command_mode.value_or( Vector2({.x = CURSOR_FALLBACK_POS_X, .y = CURSOR_FALLBACK_POS_Y}))); _cursor_controller->set_cursor_style(_normal_mode_cursor_style); _current_mode = Mode::NORMAL; _status_manager->set_section_body(StatusLineSection::A, "NORMAL"); } void Game::_run_command(const std::string &command) noexcept { const auto split_command = split_string>(command, ' '); if (split_command.size() == 1) { if (!_commands.contains(command)) { _show_command_error(fmt::format("Error: Not a command: {}", command)); return; } if (_commands.at(command).option_cnt != 0U) { _show_command_error( fmt::format("Error: Insufficient arguments for command: {}", command)); return; } } const auto &command_name = split_command[0]; if (!_commands.contains(command_name)) { _show_command_error(fmt::format("Error: Not a command: {}", command)); return; } const auto &command_info = _commands.at(command_name); const auto args = decltype(split_command)(split_command.begin() + 1, split_command.end()); command_info.function(args); } void Game::_show_command_error(const std::string_view &error_message) noexcept { _erase_entire_line(); const auto position = _cursor_controller->where(); _cursor_controller->move_to(Vector2({.x = 0, .y = position.get_y()})); fmt::print(fmt::fg(fmt::color::red), "{}", error_message); } auto Game::_move_cursor(const Vector2 &direction) noexcept -> bool { const auto current_position = _cursor_controller->where(); const auto dest_position = current_position + direction; const auto scene_size = _scene->size(); if (scene_size.validate_coords(dest_position) != CoordsValidation::VALID) { return false; } if (dest_position.get_y() < _minimum_cursor_pos_y) { return false; } _cursor_controller->move_to(dest_position); return true; } void Game::_set_space( const std::shared_ptr> &matrix, const Vector2 &position, char character) noexcept { const auto prev_position = _cursor_controller->where(); _cursor_controller->move_to(position); std::cout.put(character); std::cout.flush(); matrix->set(position, character); _cursor_controller->move_to(prev_position); } void Game::_erase_entire_line() noexcept { fmt::print(ERASE_ENTIRE_LINE, fmt::arg("esc", ESC)); std::cout.flush(); } void Game::_erase_line_from_cursor() noexcept { fmt::print(ERASE_LINE_FROM_CURSOR, fmt::arg("esc", ESC)); std::cout.flush(); }