fissh

termios terminal aquarium. demo at ssh://fish@kloet.net
Download | Log | Files | Refs

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); }