From b4f1826464a3faabd053eed8fba9f9cc7f90f087 Mon Sep 17 00:00:00 2001 From: Andrew Kloet Date: Tue, 8 Jul 2025 15:36:21 -0400 Subject: [PATCH 1/4] replace ncurses with termios --- Makefile | 2 +- src/Aquarium.cpp | 187 +++++++++++++++++++++++++++++++++++++---------- src/Aquarium.h | 11 +-- src/main.cpp | 60 ++++++++------- 4 files changed, 190 insertions(+), 70 deletions(-) diff --git a/Makefile b/Makefile index 2d4ffdf..28bc548 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Compiler and flags CXX = g++ CXXFLAGS = -std=c++17 -Wall -Wextra -O3 -LDFLAGS = -lncurses -ltinfo +LDFLAGS = -static -lncurses -ltinfo # Directories SRC_DIR = src diff --git a/src/Aquarium.cpp b/src/Aquarium.cpp index 65010e9..ea67c77 100644 --- a/src/Aquarium.cpp +++ b/src/Aquarium.cpp @@ -8,19 +8,86 @@ #include "Waterline.h" #include "Whale.h" #include +#include +#include #include +#include +#include +#include +#include int g_maxCells = 0; +// 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"; + +// 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() { - initscr(); - noecho(); - cbreak(); - nodelay(stdscr, TRUE); - curs_set(0); - initColors(); - timeout(100); - getmaxyx(stdscr, height, width); + // 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 | ISIG); + 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", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::HIDE_CURSOR); + fflush(stdout); + + // Get terminal size + getTerminalSize(); currentFrame.assign(height, std::vector(width)); previousFrame.assign(height, std::vector(width)); @@ -31,6 +98,18 @@ Aquarium::Aquarium() { } } +void Aquarium::getTerminalSize() { + struct winsize ws; + if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { + height = ws.ws_row; + width = ws.ws_col; + } else { + // Fallback to reasonable defaults + height = 24; + width = 80; + } +} + void Aquarium::ensureEntitiesSorted() { if (entities_need_sorting) { std::sort(entities.begin(), entities.end(), @@ -109,11 +188,13 @@ void Aquarium::redraw() { } void Aquarium::resize() { - clear(); - getmaxyx(stdscr, height, width); + printf("%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME); + fflush(stdout); + + getTerminalSize(); if (g_maxCells && height * width > g_maxCells) { - endwin(); + cleanup_terminal(0); std::cerr << "Error: Terminal too large. Maximum allowed area is " << g_maxCells << " cells, but current size is " << (height * width) << ".\n"; @@ -191,18 +272,28 @@ void Aquarium::drawToFrame(int y, int x, const std::string &line, void Aquarium::initColorLookup() { for (int i = 0; i < 256; ++i) - colorLookup[i] = 8; // Default black - colorLookup['r'] = 1; - colorLookup['g'] = 2; - colorLookup['y'] = 3; - colorLookup['b'] = 4; - colorLookup['m'] = 5; - colorLookup['c'] = 6; - colorLookup['w'] = 7; - colorLookup['k'] = 8; + 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::initColors() { + // This function is kept for compatibility but does nothing + // since we're using ANSI colors directly } void Aquarium::renderToScreen() { + static std::string output; + output.clear(); + output.reserve(height * width * 20); // Reserve space for efficiency + for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { const Cell &newCell = currentFrame[y][x]; @@ -210,30 +301,48 @@ void Aquarium::renderToScreen() { if (newCell != oldCell) { oldCell = newCell; - move(y, x); - int colorPair = - colorLookup[static_cast(newCell.colorChar)]; - attrset(COLOR_PAIR(colorPair) | (newCell.bold ? A_BOLD : A_NORMAL)); - addch(newCell.ch); + // Move cursor to position + output += ANSI::moveTo(y, x); + + // Set color and attributes + output += ANSI::RESET; // Reset first + if (newCell.bold) { + output += ANSI::BOLD; + } + output += colorLookup[static_cast(newCell.colorChar)]; + + // Add the character + output += newCell.ch; } } } - refresh(); -} -void Aquarium::initColors() { - if (has_colors()) { - start_color(); - init_pair(1, COLOR_RED, COLOR_BLACK); - init_pair(2, COLOR_GREEN, COLOR_BLACK); - init_pair(3, COLOR_YELLOW, COLOR_BLACK); - init_pair(4, COLOR_BLUE, COLOR_BLACK); - init_pair(5, COLOR_MAGENTA, COLOR_BLACK); - init_pair(6, COLOR_CYAN, COLOR_BLACK); - init_pair(7, COLOR_WHITE, COLOR_BLACK); - init_pair(8, COLOR_BLACK, COLOR_BLACK); + // Output everything at once for better performance + if (!output.empty()) { + printf("%s", output.c_str()); + fflush(stdout); } } -Aquarium::~Aquarium() { endwin(); } +// 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 (you'll need to call this periodically) +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); } diff --git a/src/Aquarium.h b/src/Aquarium.h index 80e00a8..5464b87 100644 --- a/src/Aquarium.h +++ b/src/Aquarium.h @@ -1,7 +1,6 @@ #pragma once #include "Entity.h" #include -#include #include extern int g_maxCells; @@ -15,7 +14,6 @@ private: char ch = ' '; char colorChar = 'k'; bool bold = false; - bool operator==(const Cell &other) const { return ch == other.ch && colorChar == other.colorChar && bold == other.bold; @@ -28,9 +26,8 @@ private: std::vector> entities; size_t big_entity_index = 0; void ensureBigEntityExists(); - bool entities_need_sorting = true; - static inline short colorLookup[256] = {0}; + static inline const char *colorLookup[256] = {nullptr}; static inline bool colorLookupInitialized = false; public: @@ -53,17 +50,21 @@ public: void addShip(); void addSeaMonster(); void addWhale(); - void redraw(); void initColors(); void resize(); void drawToFrame(int y, int x, const std::string &line, const std::string &colorLine); + // New termios-specific methods + int checkInput(); // Returns character code or -1 if no input + bool checkResize(); // Returns true if terminal was resized + private: void clearCurrentFrame(); void renderToScreen(); void ensureEntitiesSorted(); + void getTerminalSize(); static void initColorLookup(); template void addEntityImpl(Args &&...args) { diff --git a/src/main.cpp b/src/main.cpp index 6a3a919..b749ffe 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,35 +1,45 @@ +// main.cpp #include "Aquarium.h" -#include -#include +#ifdef __OpenBSD__ #include +#endif +#include +#include +#include -int main(int argc, char *argv[]) { - int opt; - while ((opt = getopt(argc, argv, "r:")) != -1) { - switch (opt) { - case 'r': - g_maxCells = std::atoi(optarg); - break; - default: - fprintf(stderr, "Usage: %s [-r max_cells]\n", argv[0]); - return 1; - } - } - +int main() { + // Get the singleton instance Aquarium &aquarium = Aquarium::getInstance(); - aquarium.resize(); +#ifdef __OpenBSD__ + // Most restrictive pledge - no file access needed! + if (pledge("stdio tty", NULL) == -1) { + perror("pledge"); + return 1; + } +#endif + + // Initialize the aquarium display + aquarium.resize(); // Setup initial entities + + // Main game loop while (true) { - aquarium.redraw(); - int ch = getch(); - if (ch != ERR) { - if (ch == 'q') - break; - if (ch == 'r') - aquarium.resize(); - flushinp(); - usleep(100000); + // Check for user input + int input = aquarium.checkInput(); + if (input == 'q' || input == 'Q' || input == 27) { // ESC key + break; } + + // Check if terminal was resized + if (aquarium.checkResize()) { + aquarium.resize(); + } + + // Redraw the aquarium + aquarium.redraw(); + + // Control frame rate (~20 FPS) + // std::this_thread::sleep_for(std::chrono::milliseconds(1)); } return 0; -- 2.51.0 From d93d351d38794218bf16d7fb8a72582ac9c45ae0 Mon Sep 17 00:00:00 2001 From: user Date: Tue, 8 Jul 2025 17:25:27 -0400 Subject: [PATCH 2/4] black background --- Makefile | 2 +- src/Aquarium.cpp | 93 ++++++++++++------------------------------------ src/main.cpp | 13 ------- 3 files changed, 24 insertions(+), 84 deletions(-) diff --git a/Makefile b/Makefile index 28bc548..29eac28 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Compiler and flags CXX = g++ CXXFLAGS = -std=c++17 -Wall -Wextra -O3 -LDFLAGS = -static -lncurses -ltinfo +LDFLAGS = -static # Directories SRC_DIR = src diff --git a/src/Aquarium.cpp b/src/Aquarium.cpp index ea67c77..209f2e7 100644 --- a/src/Aquarium.cpp +++ b/src/Aquarium.cpp @@ -15,9 +15,7 @@ #include #include #include - int g_maxCells = 0; - // ANSI color codes namespace ANSI { const char *RESET = "\033[0m"; @@ -26,7 +24,6 @@ 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"; @@ -36,7 +33,17 @@ const char *BLUE = "\033[34m"; const char *MAGENTA = "\033[35m"; const char *CYAN = "\033[36m"; const char *WHITE = "\033[37m"; - +// Background colors +const char *BG_BLACK = "\033[40m"; +const char *BG_RED = "\033[41m"; +const char *BG_GREEN = "\033[42m"; +const char *BG_YELLOW = "\033[43m"; +const char *BG_BLUE = "\033[44m"; +const char *BG_MAGENTA = "\033[45m"; +const char *BG_CYAN = "\033[46m"; +const char *BG_WHITE = "\033[47m"; +// Combined reset with black background +const char *RESET_BLACK_BG = "\033[0;40m"; // Move cursor to position std::string moveTo(int row, int col) { char buffer[32]; @@ -44,11 +51,9 @@ std::string moveTo(int row, int col) { 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) { @@ -60,18 +65,15 @@ void cleanup_terminal(int sig) { 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 | ISIG); @@ -79,37 +81,31 @@ Aquarium::Aquarium() { 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", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::HIDE_CURSOR); + // Initialize display with black background + 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 { - // Fallback to reasonable defaults + // Fallback height = 24; width = 80; } } - void Aquarium::ensureEntitiesSorted() { if (entities_need_sorting) { std::sort(entities.begin(), entities.end(), @@ -123,23 +119,18 @@ void Aquarium::ensureEntitiesSorted() { entities_need_sorting = false; } } - void Aquarium::redraw() { clearCurrentFrame(); ensureBigEntityExists(); - // Use static vectors to avoid per-frame allocations static std::vector> newEntities; static std::vector entitiesToRemove; - newEntities.clear(); entitiesToRemove.clear(); - // 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 if (auto *fish = dynamic_cast(entity.get())) { if (fish->shouldSpawnBubble()) { @@ -147,7 +138,6 @@ void Aquarium::redraw() { std::make_unique(fish->getX(), fish->getY())); } } - if (entity->shouldBeRemoved()) { auto replacement = entity->createReplacement(); if (replacement) { @@ -158,41 +148,33 @@ void Aquarium::redraw() { } } } - // 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 if (!newEntities.empty()) { // Reserve space to minimize reallocations 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(); } - renderToScreen(); } - void Aquarium::resize() { - printf("%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME); + // Clear screen and set black background + printf("%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::BG_BLACK); fflush(stdout); - getTerminalSize(); - if (g_maxCells && height * width > g_maxCells) { cleanup_terminal(0); std::cerr << "Error: Terminal too large. Maximum allowed area is " @@ -200,13 +182,10 @@ void Aquarium::resize() { << (height * width) << ".\n"; std::exit(1); } - currentFrame.assign(height, std::vector(width)); previousFrame.assign(height, std::vector(width)); - entities.clear(); entities_need_sorting = true; - addWaterline(); addCastle(); for (int i = 0; i < width / 15; i++) @@ -214,7 +193,6 @@ void Aquarium::resize() { for (int i = 0; i < width * (height - 9) / 350; i++) addFish(); } - void Aquarium::addFish() { addEntityImpl(); } void Aquarium::addBubble(float x, float y) { addEntityImpl(x, y); } void Aquarium::addSeaweed() { addEntityImpl(); } @@ -223,7 +201,6 @@ 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) { @@ -233,8 +210,7 @@ void Aquarium::ensureBigEntityExists() { return; // Big entity found, do nothing } } - - // No big entity found, spawn next in cycle (Ship, SeaMonster, Whale) + // No big entity found, spawn next in cycle int entity_type = big_entity_index % 3; if (entity_type == 0) { addEntityImpl(); @@ -245,35 +221,28 @@ void Aquarium::ensureBigEntityExists() { } ++big_entity_index; } - void Aquarium::clearCurrentFrame() { for (auto &row : currentFrame) { std::fill(row.begin(), row.end(), 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; @@ -283,48 +252,34 @@ void Aquarium::initColorLookup() { colorLookup['w'] = ANSI::WHITE; colorLookup['k'] = ANSI::BLACK; } - -void Aquarium::initColors() { - // This function is kept for compatibility but does nothing - // since we're using ANSI colors directly -} - void Aquarium::renderToScreen() { static std::string output; output.clear(); output.reserve(height * width * 20); // Reserve space for efficiency - 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) { oldCell = newCell; - // Move cursor to position output += ANSI::moveTo(y, x); - - // Set color and attributes - output += ANSI::RESET; // Reset first + // Set color and attributes with black background + output += ANSI::RESET_BLACK_BG; // Reset with black background if (newCell.bold) { output += ANSI::BOLD; } output += colorLookup[static_cast(newCell.colorChar)]; - // Add the character output += newCell.ch; } } } - - // Output everything at once for better performance + // Output everything at once if (!output.empty()) { - printf("%s", output.c_str()); - fflush(stdout); + std::cout << output << std::flush; } } - // Check for input (non-blocking) int Aquarium::checkInput() { char c; @@ -333,8 +288,7 @@ int Aquarium::checkInput() { } return -1; // No input available } - -// Check if terminal was resized (you'll need to call this periodically) +// Check if terminal was resized bool Aquarium::checkResize() { struct winsize ws; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { @@ -344,5 +298,4 @@ bool Aquarium::checkResize() { } return false; } - Aquarium::~Aquarium() { cleanup_terminal(0); } diff --git a/src/main.cpp b/src/main.cpp index b749ffe..aadd1f1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,18 +1,11 @@ // main.cpp #include "Aquarium.h" -#ifdef __OpenBSD__ -#include -#endif -#include -#include -#include int main() { // Get the singleton instance Aquarium &aquarium = Aquarium::getInstance(); #ifdef __OpenBSD__ - // Most restrictive pledge - no file access needed! if (pledge("stdio tty", NULL) == -1) { perror("pledge"); return 1; @@ -24,22 +17,16 @@ int main() { // Main game loop while (true) { - // Check for user input int input = aquarium.checkInput(); if (input == 'q' || input == 'Q' || input == 27) { // ESC key break; } - // Check if terminal was resized if (aquarium.checkResize()) { aquarium.resize(); } - // Redraw the aquarium aquarium.redraw(); - - // Control frame rate (~20 FPS) - // std::this_thread::sleep_for(std::chrono::milliseconds(1)); } return 0; -- 2.51.0 From 2f44398664a41d1d0ee52b9eec267a60bdb9bfa1 Mon Sep 17 00:00:00 2001 From: user Date: Tue, 8 Jul 2025 17:30:28 -0400 Subject: [PATCH 3/4] cleanup --- src/Aquarium.cpp | 73 +++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/src/Aquarium.cpp b/src/Aquarium.cpp index 209f2e7..b459523 100644 --- a/src/Aquarium.cpp +++ b/src/Aquarium.cpp @@ -15,7 +15,9 @@ #include #include #include + int g_maxCells = 0; + // ANSI color codes namespace ANSI { const char *RESET = "\033[0m"; @@ -24,6 +26,7 @@ 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"; @@ -33,17 +36,10 @@ const char *BLUE = "\033[34m"; const char *MAGENTA = "\033[35m"; const char *CYAN = "\033[36m"; const char *WHITE = "\033[37m"; -// Background colors +// Colors (background) const char *BG_BLACK = "\033[40m"; -const char *BG_RED = "\033[41m"; -const char *BG_GREEN = "\033[42m"; -const char *BG_YELLOW = "\033[43m"; -const char *BG_BLUE = "\033[44m"; -const char *BG_MAGENTA = "\033[45m"; -const char *BG_CYAN = "\033[46m"; -const char *BG_WHITE = "\033[47m"; -// Combined reset with black background const char *RESET_BLACK_BG = "\033[0;40m"; + // Move cursor to position std::string moveTo(int row, int col) { char buffer[32]; @@ -51,9 +47,11 @@ std::string moveTo(int row, int col) { 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) { @@ -65,15 +63,18 @@ void cleanup_terminal(int sig) { 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 | ISIG); @@ -81,20 +82,26 @@ Aquarium::Aquarium() { 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 with black background + + // 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) { @@ -106,6 +113,7 @@ void Aquarium::getTerminalSize() { width = 80; } } + void Aquarium::ensureEntitiesSorted() { if (entities_need_sorting) { std::sort(entities.begin(), entities.end(), @@ -119,18 +127,23 @@ void Aquarium::ensureEntitiesSorted() { entities_need_sorting = false; } } + void Aquarium::redraw() { clearCurrentFrame(); ensureBigEntityExists(); + // Use static vectors to avoid per-frame allocations static std::vector> newEntities; static std::vector entitiesToRemove; + newEntities.clear(); entitiesToRemove.clear(); + // 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 if (auto *fish = dynamic_cast(entity.get())) { if (fish->shouldSpawnBubble()) { @@ -138,6 +151,7 @@ void Aquarium::redraw() { std::make_unique(fish->getX(), fish->getY())); } } + if (entity->shouldBeRemoved()) { auto replacement = entity->createReplacement(); if (replacement) { @@ -148,33 +162,41 @@ void Aquarium::redraw() { } } } + // 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 if (!newEntities.empty()) { // Reserve space to minimize reallocations 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(); } + renderToScreen(); } + void Aquarium::resize() { - // Clear screen and set black background printf("%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::BG_BLACK); fflush(stdout); + getTerminalSize(); + if (g_maxCells && height * width > g_maxCells) { cleanup_terminal(0); std::cerr << "Error: Terminal too large. Maximum allowed area is " @@ -182,10 +204,13 @@ void Aquarium::resize() { << (height * width) << ".\n"; std::exit(1); } + currentFrame.assign(height, std::vector(width)); previousFrame.assign(height, std::vector(width)); + entities.clear(); entities_need_sorting = true; + addWaterline(); addCastle(); for (int i = 0; i < width / 15; i++) @@ -193,6 +218,7 @@ void Aquarium::resize() { for (int i = 0; i < width * (height - 9) / 350; i++) addFish(); } + void Aquarium::addFish() { addEntityImpl(); } void Aquarium::addBubble(float x, float y) { addEntityImpl(x, y); } void Aquarium::addSeaweed() { addEntityImpl(); } @@ -201,6 +227,7 @@ 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) { @@ -210,6 +237,7 @@ void Aquarium::ensureBigEntityExists() { 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) { @@ -221,28 +249,35 @@ void Aquarium::ensureBigEntityExists() { } ++big_entity_index; } + void Aquarium::clearCurrentFrame() { for (auto &row : currentFrame) { std::fill(row.begin(), row.end(), 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; @@ -252,34 +287,44 @@ void Aquarium::initColorLookup() { colorLookup['w'] = ANSI::WHITE; colorLookup['k'] = ANSI::BLACK; } + void Aquarium::renderToScreen() { static std::string output; output.clear(); output.reserve(height * width * 20); // Reserve space for efficiency + 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) { oldCell = newCell; + // Move cursor to position output += ANSI::moveTo(y, x); - // Set color and attributes with black background - output += ANSI::RESET_BLACK_BG; // Reset with black background + + // Reset cell + output += ANSI::RESET_BLACK_BG; + + // Set color and attributes if (newCell.bold) { output += ANSI::BOLD; } output += colorLookup[static_cast(newCell.colorChar)]; + // Add the character output += newCell.ch; } } } + // Output everything at once if (!output.empty()) { std::cout << output << std::flush; } } + // Check for input (non-blocking) int Aquarium::checkInput() { char c; @@ -288,6 +333,7 @@ int Aquarium::checkInput() { } return -1; // No input available } + // Check if terminal was resized bool Aquarium::checkResize() { struct winsize ws; @@ -298,4 +344,5 @@ bool Aquarium::checkResize() { } return false; } + Aquarium::~Aquarium() { cleanup_terminal(0); } -- 2.51.0 From 36e6c3786f136ff2ac435003e9cdfaf8acb9386a Mon Sep 17 00:00:00 2001 From: user Date: Tue, 8 Jul 2025 19:47:02 -0400 Subject: [PATCH 4/4] cleanup --- Makefile | 2 +- src/Aquarium.cpp | 11 ----------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 29eac28..1577f75 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ # Compiler and flags CXX = g++ -CXXFLAGS = -std=c++17 -Wall -Wextra -O3 +CXXFLAGS = -Wall -Wextra -O3 LDFLAGS = -static # Directories diff --git a/src/Aquarium.cpp b/src/Aquarium.cpp index b459523..9f8cbd5 100644 --- a/src/Aquarium.cpp +++ b/src/Aquarium.cpp @@ -16,8 +16,6 @@ #include #include -int g_maxCells = 0; - // ANSI color codes namespace ANSI { const char *RESET = "\033[0m"; @@ -132,7 +130,6 @@ void Aquarium::redraw() { clearCurrentFrame(); ensureBigEntityExists(); - // Use static vectors to avoid per-frame allocations static std::vector> newEntities; static std::vector entitiesToRemove; @@ -197,14 +194,6 @@ void Aquarium::resize() { getTerminalSize(); - if (g_maxCells && height * width > g_maxCells) { - cleanup_terminal(0); - std::cerr << "Error: Terminal too large. Maximum allowed area is " - << g_maxCells << " cells, but current size is " - << (height * width) << ".\n"; - std::exit(1); - } - currentFrame.assign(height, std::vector(width)); previousFrame.assign(height, std::vector(width)); -- 2.51.0