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