#include "Aquarium.h" #include "../core/Config.h" #include "../entities/Bubble.h" #include "../entities/Castle.h" #include "../entities/Fish.h" #include "../entities/SeaMonster.h" #include "../entities/Seaweed.h" #include "../entities/Ship.h" #include "../entities/Waterline.h" #include "../entities/Whale.h" #include #include #include #include #include #include #include #include #include #include #include // ANSI color codes namespace ANSI { const char *RESET = "\033[0m"; const char *BOLD = "\033[1m"; const char *CLEAR_SCREEN = "\033[2J"; const char *CURSOR_HOME = "\033[H"; const char *HIDE_CURSOR = "\033[?25l"; const char *SHOW_CURSOR = "\033[?25h"; // Colors (foreground) const char *BLACK = "\033[30m"; const char *RED = "\033[31m"; const char *GREEN = "\033[32m"; const char *YELLOW = "\033[33m"; const char *BLUE = "\033[34m"; const char *MAGENTA = "\033[35m"; const char *CYAN = "\033[36m"; const char *WHITE = "\033[37m"; // Colors (background) const char *BG_BLACK = "\033[40m"; const char *RESET_BLACK_BG = "\033[0;40m"; // Move cursor to position std::string moveTo(int row, int col) { char buffer[32]; snprintf(buffer, sizeof(buffer), "\033[%d;%dH", row + 1, col + 1); return std::string(buffer); } } // namespace ANSI // Global terminal state static struct termios original_termios; static bool termios_saved = false; // Signal handler for cleanup void cleanup_terminal(int sig) { if (termios_saved) { tcsetattr(STDIN_FILENO, TCSANOW, &original_termios); } printf("%s%s", ANSI::SHOW_CURSOR, ANSI::RESET); fflush(stdout); if (sig != 0) { exit(sig); } } Aquarium::Aquarium() { // Save original terminal settings if (tcgetattr(STDIN_FILENO, &original_termios) == 0) { termios_saved = true; } // Set up signal handlers for cleanup signal(SIGINT, cleanup_terminal); signal(SIGTERM, cleanup_terminal); signal(SIGQUIT, cleanup_terminal); // Set terminal to raw mode struct termios raw = original_termios; raw.c_lflag &= ~(ECHO | ICANON); raw.c_iflag &= ~(IXON | ICRNL); raw.c_oflag &= ~(OPOST); raw.c_cc[VMIN] = 0; // Non-blocking read raw.c_cc[VTIME] = 1; // 100ms timeout tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); // Initialize display printf("%s%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::HIDE_CURSOR, ANSI::BG_BLACK); fflush(stdout); // Get terminal size getTerminalSize(); currentFrame.assign(height, std::vector(width)); previousFrame.assign(height, std::vector(width)); if (!colorLookupInitialized) { initColorLookup(); colorLookupInitialized = true; } } void Aquarium::getTerminalSize() { struct winsize ws; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { height = ws.ws_row; width = ws.ws_col; } else { cleanup_terminal(0); std::cerr << "Error: Unable to determine terminal size.\n"; std::exit(1); } } void Aquarium::ensureEntitiesSorted() { if (entities_need_sorting) { std::sort(entities.begin(), entities.end(), [](const auto &a, const auto &b) { int layerA = a->getPreferredLayer(); int layerB = b->getPreferredLayer(); if (layerA != layerB) return layerA < layerB; return a->getId() < b->getId(); }); entities_need_sorting = false; } } void Aquarium::redraw() { clearCurrentFrame(); if (g_config.enable_big_entities) { ensureBigEntityExists(); } static std::vector> newEntities; static std::vector entitiesToRemove; newEntities.clear(); entitiesToRemove.clear(); // Count current bubbles int bubble_count = 0; for (const auto &entity : entities) { if (dynamic_cast(entity.get())) { bubble_count++; } } // Update all entities and collect changes for (size_t i = 0; i < entities.size(); ++i) { auto &entity = entities[i]; entity->update(); // Handle fish bubble spawning with configuration if (g_config.enable_bubbles && bubble_count < g_config.max_bubbles) { if (auto *fish = dynamic_cast(entity.get())) { if (fish->shouldSpawnBubble()) { // Use configured bubble rate if (rand() % g_config.fish_bubble_rate == 0) { newEntities.emplace_back( std::make_unique(fish->getX(), fish->getY())); bubble_count++; } } } } if (entity->shouldBeRemoved()) { auto replacement = entity->createReplacement(); if (replacement) { entity = std::move(replacement); entities_need_sorting = true; } else { entitiesToRemove.push_back(i); } } } // Remove entities in reverse order to maintain indices for (auto it = entitiesToRemove.rbegin(); it != entitiesToRemove.rend(); ++it) { entities.erase(entities.begin() + *it); entities_need_sorting = true; } // Add new entities if we have them and haven't exceeded max if (!newEntities.empty() && entities.size() < static_cast(g_config.max_entities)) { entities.reserve(entities.size() + newEntities.size()); for (auto &newEntity : newEntities) { entities.emplace_back(std::move(newEntity)); } entities_need_sorting = true; } ensureEntitiesSorted(); // Draw all entities for (const auto &entity : entities) { entity->draw(); } // Draw debug information if (g_config.show_fps || g_config.show_entity_count) { drawDebugInfo(); } renderToScreen(); } void Aquarium::drawDebugInfo() { static double last_fps = 0.0; static int frame_counter = 0; static auto last_time = std::chrono::steady_clock::now(); frame_counter++; auto current_time = std::chrono::steady_clock::now(); auto elapsed = std::chrono::duration_cast( current_time - last_time) .count(); if (elapsed >= 1000) { last_fps = (frame_counter * 1000.0) / elapsed; frame_counter = 0; last_time = current_time; } std::stringstream debug_info; if (g_config.show_fps) { debug_info << "FPS: " << std::fixed << std::setprecision(1) << last_fps; } if (g_config.show_entity_count) { if (g_config.show_fps) debug_info << " | "; debug_info << "Entities: " << entities.size(); } std::string debug_str = debug_info.str(); if (!debug_str.empty()) { std::string color_str(debug_str.length(), 'w'); // White color drawToFrame(0, width - static_cast(debug_str.length()), debug_str, color_str); } } void Aquarium::resize() { printf("%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::BG_BLACK); fflush(stdout); getTerminalSize(); currentFrame.assign(height, std::vector(width)); previousFrame.assign(height, std::vector(width)); entities.clear(); entities_need_sorting = true; addWaterline(); addCastle(); // Use configured seaweed count or auto-calculate int seaweed_count = g_config.initial_seaweed_count; if (seaweed_count == -1) { seaweed_count = width / 15; } for (int i = 0; i < seaweed_count; i++) { addSeaweed(); } // Use configured fish count or auto-calculate int fish_count = g_config.initial_fish_count; if (fish_count == -1) { fish_count = width * (height - 9) / 350; } for (int i = 0; i < fish_count; i++) { addFish(); } } void Aquarium::addFish() { addEntityImpl(); } void Aquarium::addBubble(float x, float y) { addEntityImpl(x, y); } void Aquarium::addSeaweed() { addEntityImpl(); } void Aquarium::addWaterline() { addEntityImpl(); } void Aquarium::addCastle() { addEntityImpl(); } void Aquarium::addShip() { addEntityImpl(); } void Aquarium::addSeaMonster() { addEntityImpl(); } void Aquarium::addWhale() { addEntityImpl(); } void Aquarium::ensureBigEntityExists() { // Check if any big entities exist on screen for (const auto &entity : entities) { if (dynamic_cast(entity.get()) || dynamic_cast(entity.get()) || dynamic_cast(entity.get())) { return; // Big entity found, do nothing } } // No big entity found, spawn next in cycle int entity_type = big_entity_index % 3; if (entity_type == 0) { addEntityImpl(); } else if (entity_type == 1) { addEntityImpl(); } else { addEntityImpl(); } ++big_entity_index; } void Aquarium::clearCurrentFrame() { static const Cell empty_cell{}; for (auto &row : currentFrame) { row.assign(width, empty_cell); } } void Aquarium::drawToFrame(int y, int x, const std::string &line, const std::string &colorLine) { const size_t len = std::min(line.size(), colorLine.size()); for (size_t j = 0; j < len; ++j) { int cx = x + static_cast(j); if (cx < 0 || cx >= width) continue; const char ch = line[j]; const char colorChar = colorLine[j]; const bool isBold = (colorChar >= 'A' && colorChar <= 'Z'); currentFrame[y][cx] = { ch, static_cast(isBold ? colorChar + 32 : colorChar), isBold}; } } void Aquarium::initColorLookup() { for (int i = 0; i < 256; ++i) colorLookup[i] = ANSI::BLACK; // Default black colorLookup['r'] = ANSI::RED; colorLookup['g'] = ANSI::GREEN; colorLookup['y'] = ANSI::YELLOW; colorLookup['b'] = ANSI::BLUE; colorLookup['m'] = ANSI::MAGENTA; colorLookup['c'] = ANSI::CYAN; colorLookup['w'] = ANSI::WHITE; colorLookup['k'] = ANSI::BLACK; } void Aquarium::renderToScreen() { static std::string output; output.clear(); output.reserve(height * width * 20); int cursor_y = -1, cursor_x = -1; for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { const Cell &newCell = currentFrame[y][x]; Cell &oldCell = previousFrame[y][x]; if (newCell == oldCell) continue; oldCell = newCell; // Move cursor only when needed if (cursor_y != y || cursor_x != x) { output += ANSI::moveTo(y, x); cursor_y = y; cursor_x = x; } // Apply cell formatting and character output += ANSI::RESET_BLACK_BG; // Only apply bold if configured if (g_config.use_bold && newCell.bold) { output += ANSI::BOLD; } // Only apply colors if configured if (g_config.use_colors) { output += colorLookup[static_cast(newCell.colorChar)]; } output += newCell.ch; ++cursor_x; } } if (!output.empty()) { std::cout << output << std::flush; } } // Check for input (non-blocking) int Aquarium::checkInput() { char c; if (read(STDIN_FILENO, &c, 1) == 1) { return c; } return -1; // No input available } // Check if terminal was resized bool Aquarium::checkResize() { struct winsize ws; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { if (ws.ws_row != height || ws.ws_col != width) { return true; } } return false; } Aquarium::~Aquarium() { cleanup_terminal(0); }