Aquarium.cpp (11256B)
1 #include "Aquarium.h" 2 #include "../core/Config.h" 3 #include "../entities/Bubble.h" 4 #include "../entities/Castle.h" 5 #include "../entities/Fish.h" 6 #include "../entities/SeaMonster.h" 7 #include "../entities/Seaweed.h" 8 #include "../entities/Ship.h" 9 #include "../entities/Waterline.h" 10 #include "../entities/Whale.h" 11 #include <algorithm> 12 #include <chrono> 13 #include <cstdio> 14 #include <cstring> 15 #include <iomanip> 16 #include <iostream> 17 #include <signal.h> 18 #include <sstream> 19 #include <sys/ioctl.h> 20 #include <termios.h> 21 #include <unistd.h> 22 23 // ANSI color codes 24 namespace ANSI { 25 const char *RESET = "\033[0m"; 26 const char *BOLD = "\033[1m"; 27 const char *CLEAR_SCREEN = "\033[2J"; 28 const char *CURSOR_HOME = "\033[H"; 29 const char *HIDE_CURSOR = "\033[?25l"; 30 const char *SHOW_CURSOR = "\033[?25h"; 31 32 // Colors (foreground) 33 const char *BLACK = "\033[30m"; 34 const char *RED = "\033[31m"; 35 const char *GREEN = "\033[32m"; 36 const char *YELLOW = "\033[33m"; 37 const char *BLUE = "\033[34m"; 38 const char *MAGENTA = "\033[35m"; 39 const char *CYAN = "\033[36m"; 40 const char *WHITE = "\033[37m"; 41 // Colors (background) 42 const char *BG_BLACK = "\033[40m"; 43 const char *RESET_BLACK_BG = "\033[0;40m"; 44 45 // Move cursor to position 46 std::string moveTo(int row, int col) { 47 char buffer[32]; 48 snprintf(buffer, sizeof(buffer), "\033[%d;%dH", row + 1, col + 1); 49 return std::string(buffer); 50 } 51 } // namespace ANSI 52 53 // Global terminal state 54 static struct termios original_termios; 55 static bool termios_saved = false; 56 57 // Signal handler for cleanup 58 void cleanup_terminal(int sig) { 59 if (termios_saved) { 60 tcsetattr(STDIN_FILENO, TCSANOW, &original_termios); 61 } 62 printf("%s%s", ANSI::SHOW_CURSOR, ANSI::RESET); 63 fflush(stdout); 64 if (sig != 0) { 65 exit(sig); 66 } 67 } 68 69 Aquarium::Aquarium() { 70 // Save original terminal settings 71 if (tcgetattr(STDIN_FILENO, &original_termios) == 0) { 72 termios_saved = true; 73 } 74 75 // Set up signal handlers for cleanup 76 signal(SIGINT, cleanup_terminal); 77 signal(SIGTERM, cleanup_terminal); 78 signal(SIGQUIT, cleanup_terminal); 79 80 // Set terminal to raw mode 81 struct termios raw = original_termios; 82 raw.c_lflag &= ~(ECHO | ICANON); 83 raw.c_iflag &= ~(IXON | ICRNL); 84 raw.c_oflag &= ~(OPOST); 85 raw.c_cc[VMIN] = 0; // Non-blocking read 86 raw.c_cc[VTIME] = 1; // 100ms timeout 87 88 tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); 89 90 // Initialize display 91 printf("%s%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::HIDE_CURSOR, 92 ANSI::BG_BLACK); 93 fflush(stdout); 94 95 // Get terminal size 96 getTerminalSize(); 97 98 currentFrame.assign(height, std::vector<Cell>(width)); 99 previousFrame.assign(height, std::vector<Cell>(width)); 100 101 if (!colorLookupInitialized) { 102 initColorLookup(); 103 colorLookupInitialized = true; 104 } 105 } 106 107 void Aquarium::getTerminalSize() { 108 struct winsize ws; 109 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { 110 height = ws.ws_row; 111 width = ws.ws_col; 112 } else { 113 cleanup_terminal(0); 114 std::cerr << "Error: Unable to determine terminal size.\n"; 115 std::exit(1); 116 } 117 } 118 119 void Aquarium::ensureEntitiesSorted() { 120 if (entities_need_sorting) { 121 std::sort(entities.begin(), entities.end(), 122 [](const auto &a, const auto &b) { 123 int layerA = a->getPreferredLayer(); 124 int layerB = b->getPreferredLayer(); 125 if (layerA != layerB) 126 return layerA < layerB; 127 return a->getId() < b->getId(); 128 }); 129 entities_need_sorting = false; 130 } 131 } 132 133 void Aquarium::redraw() { 134 clearCurrentFrame(); 135 136 if (g_config.enable_big_entities) { 137 ensureBigEntityExists(); 138 } 139 140 static std::vector<std::unique_ptr<Entity>> newEntities; 141 static std::vector<size_t> entitiesToRemove; 142 143 newEntities.clear(); 144 entitiesToRemove.clear(); 145 146 // Count current bubbles 147 int bubble_count = 0; 148 for (const auto &entity : entities) { 149 if (dynamic_cast<Bubble *>(entity.get())) { 150 bubble_count++; 151 } 152 } 153 154 // Update all entities and collect changes 155 for (size_t i = 0; i < entities.size(); ++i) { 156 auto &entity = entities[i]; 157 entity->update(); 158 159 // Handle fish bubble spawning with configuration 160 if (g_config.enable_bubbles && bubble_count < g_config.max_bubbles) { 161 if (auto *fish = dynamic_cast<Fish *>(entity.get())) { 162 if (fish->shouldSpawnBubble()) { 163 // Use configured bubble rate 164 if (rand() % g_config.fish_bubble_rate == 0) { 165 newEntities.emplace_back( 166 std::make_unique<Bubble>(fish->getX(), fish->getY())); 167 bubble_count++; 168 } 169 } 170 } 171 } 172 173 if (entity->shouldBeRemoved()) { 174 auto replacement = entity->createReplacement(); 175 if (replacement) { 176 entity = std::move(replacement); 177 entities_need_sorting = true; 178 } else { 179 entitiesToRemove.push_back(i); 180 } 181 } 182 } 183 184 // Remove entities in reverse order to maintain indices 185 for (auto it = entitiesToRemove.rbegin(); it != entitiesToRemove.rend(); 186 ++it) { 187 entities.erase(entities.begin() + *it); 188 entities_need_sorting = true; 189 } 190 191 // Add new entities if we have them and haven't exceeded max 192 if (!newEntities.empty() && 193 entities.size() < static_cast<size_t>(g_config.max_entities)) { 194 entities.reserve(entities.size() + newEntities.size()); 195 for (auto &newEntity : newEntities) { 196 entities.emplace_back(std::move(newEntity)); 197 } 198 entities_need_sorting = true; 199 } 200 201 ensureEntitiesSorted(); 202 203 // Draw all entities 204 for (const auto &entity : entities) { 205 entity->draw(); 206 } 207 208 // Draw debug information 209 if (g_config.show_fps || g_config.show_entity_count) { 210 drawDebugInfo(); 211 } 212 213 renderToScreen(); 214 } 215 216 void Aquarium::drawDebugInfo() { 217 static double last_fps = 0.0; 218 static int frame_counter = 0; 219 static auto last_time = std::chrono::steady_clock::now(); 220 221 frame_counter++; 222 auto current_time = std::chrono::steady_clock::now(); 223 auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>( 224 current_time - last_time) 225 .count(); 226 227 if (elapsed >= 1000) { 228 last_fps = (frame_counter * 1000.0) / elapsed; 229 frame_counter = 0; 230 last_time = current_time; 231 } 232 233 std::stringstream debug_info; 234 if (g_config.show_fps) { 235 debug_info << "FPS: " << std::fixed << std::setprecision(1) << last_fps; 236 } 237 if (g_config.show_entity_count) { 238 if (g_config.show_fps) 239 debug_info << " | "; 240 debug_info << "Entities: " << entities.size(); 241 } 242 243 std::string debug_str = debug_info.str(); 244 if (!debug_str.empty()) { 245 std::string color_str(debug_str.length(), 'w'); // White color 246 drawToFrame(0, width - static_cast<int>(debug_str.length()), debug_str, 247 color_str); 248 } 249 } 250 251 void Aquarium::resize() { 252 printf("%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::BG_BLACK); 253 fflush(stdout); 254 255 getTerminalSize(); 256 257 currentFrame.assign(height, std::vector<Cell>(width)); 258 previousFrame.assign(height, std::vector<Cell>(width)); 259 260 entities.clear(); 261 entities_need_sorting = true; 262 263 addWaterline(); 264 addCastle(); 265 266 // Use configured seaweed count or auto-calculate 267 int seaweed_count = g_config.initial_seaweed_count; 268 if (seaweed_count == -1) { 269 seaweed_count = width / 15; 270 } 271 for (int i = 0; i < seaweed_count; i++) { 272 addSeaweed(); 273 } 274 275 // Use configured fish count or auto-calculate 276 int fish_count = g_config.initial_fish_count; 277 if (fish_count == -1) { 278 fish_count = width * (height - 9) / 350; 279 } 280 for (int i = 0; i < fish_count; i++) { 281 addFish(); 282 } 283 } 284 285 void Aquarium::addFish() { addEntityImpl<Fish>(); } 286 void Aquarium::addBubble(float x, float y) { addEntityImpl<Bubble>(x, y); } 287 void Aquarium::addSeaweed() { addEntityImpl<Seaweed>(); } 288 void Aquarium::addWaterline() { addEntityImpl<Waterline>(); } 289 void Aquarium::addCastle() { addEntityImpl<Castle>(); } 290 void Aquarium::addShip() { addEntityImpl<Ship>(); } 291 void Aquarium::addSeaMonster() { addEntityImpl<SeaMonster>(); } 292 void Aquarium::addWhale() { addEntityImpl<Whale>(); } 293 294 void Aquarium::ensureBigEntityExists() { 295 // Check if any big entities exist on screen 296 for (const auto &entity : entities) { 297 if (dynamic_cast<Ship *>(entity.get()) || 298 dynamic_cast<SeaMonster *>(entity.get()) || 299 dynamic_cast<Whale *>(entity.get())) { 300 return; // Big entity found, do nothing 301 } 302 } 303 304 // No big entity found, spawn next in cycle 305 int entity_type = big_entity_index % 3; 306 if (entity_type == 0) { 307 addEntityImpl<Ship>(); 308 } else if (entity_type == 1) { 309 addEntityImpl<SeaMonster>(); 310 } else { 311 addEntityImpl<Whale>(); 312 } 313 ++big_entity_index; 314 } 315 316 void Aquarium::clearCurrentFrame() { 317 static const Cell empty_cell{}; 318 319 for (auto &row : currentFrame) { 320 row.assign(width, empty_cell); 321 } 322 } 323 324 void Aquarium::drawToFrame(int y, int x, const std::string &line, 325 const std::string &colorLine) { 326 const size_t len = std::min(line.size(), colorLine.size()); 327 328 for (size_t j = 0; j < len; ++j) { 329 int cx = x + static_cast<int>(j); 330 if (cx < 0 || cx >= width) 331 continue; 332 333 const char ch = line[j]; 334 const char colorChar = colorLine[j]; 335 const bool isBold = (colorChar >= 'A' && colorChar <= 'Z'); 336 337 currentFrame[y][cx] = { 338 ch, static_cast<char>(isBold ? colorChar + 32 : colorChar), isBold}; 339 } 340 } 341 342 void Aquarium::initColorLookup() { 343 for (int i = 0; i < 256; ++i) 344 colorLookup[i] = ANSI::BLACK; // Default black 345 346 colorLookup['r'] = ANSI::RED; 347 colorLookup['g'] = ANSI::GREEN; 348 colorLookup['y'] = ANSI::YELLOW; 349 colorLookup['b'] = ANSI::BLUE; 350 colorLookup['m'] = ANSI::MAGENTA; 351 colorLookup['c'] = ANSI::CYAN; 352 colorLookup['w'] = ANSI::WHITE; 353 colorLookup['k'] = ANSI::BLACK; 354 } 355 356 void Aquarium::renderToScreen() { 357 static std::string output; 358 output.clear(); 359 output.reserve(height * width * 20); 360 361 int cursor_y = -1, cursor_x = -1; 362 363 for (int y = 0; y < height; ++y) { 364 for (int x = 0; x < width; ++x) { 365 const Cell &newCell = currentFrame[y][x]; 366 Cell &oldCell = previousFrame[y][x]; 367 368 if (newCell == oldCell) 369 continue; 370 371 oldCell = newCell; 372 373 // Move cursor only when needed 374 if (cursor_y != y || cursor_x != x) { 375 output += ANSI::moveTo(y, x); 376 cursor_y = y; 377 cursor_x = x; 378 } 379 380 // Apply cell formatting and character 381 output += ANSI::RESET_BLACK_BG; 382 383 // Only apply bold if configured 384 if (g_config.use_bold && newCell.bold) { 385 output += ANSI::BOLD; 386 } 387 388 // Only apply colors if configured 389 if (g_config.use_colors) { 390 output += colorLookup[static_cast<unsigned char>(newCell.colorChar)]; 391 } 392 393 output += newCell.ch; 394 ++cursor_x; 395 } 396 } 397 398 if (!output.empty()) { 399 std::cout << output << std::flush; 400 } 401 } 402 403 // Check for input (non-blocking) 404 int Aquarium::checkInput() { 405 char c; 406 if (read(STDIN_FILENO, &c, 1) == 1) { 407 return c; 408 } 409 return -1; // No input available 410 } 411 412 // Check if terminal was resized 413 bool Aquarium::checkResize() { 414 struct winsize ws; 415 if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { 416 if (ws.ws_row != height || ws.ws_col != width) { 417 return true; 418 } 419 } 420 return false; 421 } 422 423 Aquarium::~Aquarium() { cleanup_terminal(0); }