diff --git a/Makefile b/Makefile index 2d4ffdf..1577f75 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Compiler and flags CXX = g++ -CXXFLAGS = -std=c++17 -Wall -Wextra -O3 -LDFLAGS = -lncurses -ltinfo +CXXFLAGS = -Wall -Wextra -O3 +LDFLAGS = -static # Directories SRC_DIR = src diff --git a/src/Aquarium.cpp b/src/Aquarium.cpp index 65010e9..9f8cbd5 100644 --- a/src/Aquarium.cpp +++ b/src/Aquarium.cpp @@ -8,19 +8,88 @@ #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"; +// 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() { - 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%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)); @@ -31,6 +100,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 + height = 24; + width = 80; + } +} + void Aquarium::ensureEntitiesSorted() { if (entities_need_sorting) { std::sort(entities.begin(), entities.end(), @@ -49,7 +130,6 @@ void Aquarium::redraw() { clearCurrentFrame(); ensureBigEntityExists(); - // Use static vectors to avoid per-frame allocations static std::vector> newEntities; static std::vector entitiesToRemove; @@ -109,16 +189,10 @@ void Aquarium::redraw() { } void Aquarium::resize() { - clear(); - getmaxyx(stdscr, height, width); + printf("%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::BG_BLACK); + fflush(stdout); - if (g_maxCells && height * width > g_maxCells) { - endwin(); - std::cerr << "Error: Terminal too large. Maximum allowed area is " - << g_maxCells << " cells, but current size is " - << (height * width) << ".\n"; - std::exit(1); - } + getTerminalSize(); currentFrame.assign(height, std::vector(width)); previousFrame.assign(height, std::vector(width)); @@ -153,7 +227,7 @@ void Aquarium::ensureBigEntityExists() { } } - // 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(); @@ -191,18 +265,23 @@ 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::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 +289,49 @@ 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); + + // 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; } } } - 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 + if (!output.empty()) { + std::cout << output << std::flush; } } -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 +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..aadd1f1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,35 +1,32 @@ +// main.cpp #include "Aquarium.h" -#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__ + 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); + int input = aquarium.checkInput(); + if (input == 'q' || input == 'Q' || input == 27) { // ESC key + break; } + + if (aquarium.checkResize()) { + aquarium.resize(); + } + + aquarium.redraw(); } return 0;