black background

This commit is contained in:
user
2025-07-08 17:25:27 -04:00
parent b4f1826464
commit d93d351d38
3 changed files with 24 additions and 84 deletions

View File

@@ -1,7 +1,7 @@
# Compiler and flags # Compiler and flags
CXX = g++ CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -O3 CXXFLAGS = -std=c++17 -Wall -Wextra -O3
LDFLAGS = -static -lncurses -ltinfo LDFLAGS = -static
# Directories # Directories
SRC_DIR = src SRC_DIR = src

View File

@@ -15,9 +15,7 @@
#include <sys/ioctl.h> #include <sys/ioctl.h>
#include <termios.h> #include <termios.h>
#include <unistd.h> #include <unistd.h>
int g_maxCells = 0; int g_maxCells = 0;
// ANSI color codes // ANSI color codes
namespace ANSI { namespace ANSI {
const char *RESET = "\033[0m"; const char *RESET = "\033[0m";
@@ -26,7 +24,6 @@ const char *CLEAR_SCREEN = "\033[2J";
const char *CURSOR_HOME = "\033[H"; const char *CURSOR_HOME = "\033[H";
const char *HIDE_CURSOR = "\033[?25l"; const char *HIDE_CURSOR = "\033[?25l";
const char *SHOW_CURSOR = "\033[?25h"; const char *SHOW_CURSOR = "\033[?25h";
// Colors (foreground) // Colors (foreground)
const char *BLACK = "\033[30m"; const char *BLACK = "\033[30m";
const char *RED = "\033[31m"; const char *RED = "\033[31m";
@@ -36,7 +33,17 @@ const char *BLUE = "\033[34m";
const char *MAGENTA = "\033[35m"; const char *MAGENTA = "\033[35m";
const char *CYAN = "\033[36m"; const char *CYAN = "\033[36m";
const char *WHITE = "\033[37m"; 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 // Move cursor to position
std::string moveTo(int row, int col) { std::string moveTo(int row, int col) {
char buffer[32]; char buffer[32];
@@ -44,11 +51,9 @@ std::string moveTo(int row, int col) {
return std::string(buffer); return std::string(buffer);
} }
} // namespace ANSI } // namespace ANSI
// Global terminal state // Global terminal state
static struct termios original_termios; static struct termios original_termios;
static bool termios_saved = false; static bool termios_saved = false;
// Signal handler for cleanup // Signal handler for cleanup
void cleanup_terminal(int sig) { void cleanup_terminal(int sig) {
if (termios_saved) { if (termios_saved) {
@@ -60,18 +65,15 @@ void cleanup_terminal(int sig) {
exit(sig); exit(sig);
} }
} }
Aquarium::Aquarium() { Aquarium::Aquarium() {
// Save original terminal settings // Save original terminal settings
if (tcgetattr(STDIN_FILENO, &original_termios) == 0) { if (tcgetattr(STDIN_FILENO, &original_termios) == 0) {
termios_saved = true; termios_saved = true;
} }
// Set up signal handlers for cleanup // Set up signal handlers for cleanup
signal(SIGINT, cleanup_terminal); signal(SIGINT, cleanup_terminal);
signal(SIGTERM, cleanup_terminal); signal(SIGTERM, cleanup_terminal);
signal(SIGQUIT, cleanup_terminal); signal(SIGQUIT, cleanup_terminal);
// Set terminal to raw mode // Set terminal to raw mode
struct termios raw = original_termios; struct termios raw = original_termios;
raw.c_lflag &= ~(ECHO | ICANON | ISIG); raw.c_lflag &= ~(ECHO | ICANON | ISIG);
@@ -79,37 +81,31 @@ Aquarium::Aquarium() {
raw.c_oflag &= ~(OPOST); raw.c_oflag &= ~(OPOST);
raw.c_cc[VMIN] = 0; // Non-blocking read raw.c_cc[VMIN] = 0; // Non-blocking read
raw.c_cc[VTIME] = 1; // 100ms timeout raw.c_cc[VTIME] = 1; // 100ms timeout
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw); 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,
printf("%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::HIDE_CURSOR); ANSI::BG_BLACK);
fflush(stdout); fflush(stdout);
// Get terminal size // Get terminal size
getTerminalSize(); getTerminalSize();
currentFrame.assign(height, std::vector<Cell>(width)); currentFrame.assign(height, std::vector<Cell>(width));
previousFrame.assign(height, std::vector<Cell>(width)); previousFrame.assign(height, std::vector<Cell>(width));
if (!colorLookupInitialized) { if (!colorLookupInitialized) {
initColorLookup(); initColorLookup();
colorLookupInitialized = true; colorLookupInitialized = true;
} }
} }
void Aquarium::getTerminalSize() { void Aquarium::getTerminalSize() {
struct winsize ws; struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) {
height = ws.ws_row; height = ws.ws_row;
width = ws.ws_col; width = ws.ws_col;
} else { } else {
// Fallback to reasonable defaults // Fallback
height = 24; height = 24;
width = 80; width = 80;
} }
} }
void Aquarium::ensureEntitiesSorted() { void Aquarium::ensureEntitiesSorted() {
if (entities_need_sorting) { if (entities_need_sorting) {
std::sort(entities.begin(), entities.end(), std::sort(entities.begin(), entities.end(),
@@ -123,23 +119,18 @@ void Aquarium::ensureEntitiesSorted() {
entities_need_sorting = false; entities_need_sorting = false;
} }
} }
void Aquarium::redraw() { void Aquarium::redraw() {
clearCurrentFrame(); clearCurrentFrame();
ensureBigEntityExists(); ensureBigEntityExists();
// Use static vectors to avoid per-frame allocations // Use static vectors to avoid per-frame allocations
static std::vector<std::unique_ptr<Entity>> newEntities; static std::vector<std::unique_ptr<Entity>> newEntities;
static std::vector<size_t> entitiesToRemove; static std::vector<size_t> entitiesToRemove;
newEntities.clear(); newEntities.clear();
entitiesToRemove.clear(); entitiesToRemove.clear();
// Update all entities and collect changes // Update all entities and collect changes
for (size_t i = 0; i < entities.size(); ++i) { for (size_t i = 0; i < entities.size(); ++i) {
auto &entity = entities[i]; auto &entity = entities[i];
entity->update(); entity->update();
// Handle fish bubble spawning // Handle fish bubble spawning
if (auto *fish = dynamic_cast<Fish *>(entity.get())) { if (auto *fish = dynamic_cast<Fish *>(entity.get())) {
if (fish->shouldSpawnBubble()) { if (fish->shouldSpawnBubble()) {
@@ -147,7 +138,6 @@ void Aquarium::redraw() {
std::make_unique<Bubble>(fish->getX(), fish->getY())); std::make_unique<Bubble>(fish->getX(), fish->getY()));
} }
} }
if (entity->shouldBeRemoved()) { if (entity->shouldBeRemoved()) {
auto replacement = entity->createReplacement(); auto replacement = entity->createReplacement();
if (replacement) { if (replacement) {
@@ -158,41 +148,33 @@ void Aquarium::redraw() {
} }
} }
} }
// Remove entities in reverse order to maintain indices // Remove entities in reverse order to maintain indices
for (auto it = entitiesToRemove.rbegin(); it != entitiesToRemove.rend(); for (auto it = entitiesToRemove.rbegin(); it != entitiesToRemove.rend();
++it) { ++it) {
entities.erase(entities.begin() + *it); entities.erase(entities.begin() + *it);
entities_need_sorting = true; entities_need_sorting = true;
} }
// Add new entities if we have them // Add new entities if we have them
if (!newEntities.empty()) { if (!newEntities.empty()) {
// Reserve space to minimize reallocations // Reserve space to minimize reallocations
entities.reserve(entities.size() + newEntities.size()); entities.reserve(entities.size() + newEntities.size());
for (auto &newEntity : newEntities) { for (auto &newEntity : newEntities) {
entities.emplace_back(std::move(newEntity)); entities.emplace_back(std::move(newEntity));
} }
entities_need_sorting = true; entities_need_sorting = true;
} }
ensureEntitiesSorted(); ensureEntitiesSorted();
// Draw all entities // Draw all entities
for (const auto &entity : entities) { for (const auto &entity : entities) {
entity->draw(); entity->draw();
} }
renderToScreen(); renderToScreen();
} }
void Aquarium::resize() { 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); fflush(stdout);
getTerminalSize(); getTerminalSize();
if (g_maxCells && height * width > g_maxCells) { if (g_maxCells && height * width > g_maxCells) {
cleanup_terminal(0); cleanup_terminal(0);
std::cerr << "Error: Terminal too large. Maximum allowed area is " std::cerr << "Error: Terminal too large. Maximum allowed area is "
@@ -200,13 +182,10 @@ void Aquarium::resize() {
<< (height * width) << ".\n"; << (height * width) << ".\n";
std::exit(1); std::exit(1);
} }
currentFrame.assign(height, std::vector<Cell>(width)); currentFrame.assign(height, std::vector<Cell>(width));
previousFrame.assign(height, std::vector<Cell>(width)); previousFrame.assign(height, std::vector<Cell>(width));
entities.clear(); entities.clear();
entities_need_sorting = true; entities_need_sorting = true;
addWaterline(); addWaterline();
addCastle(); addCastle();
for (int i = 0; i < width / 15; i++) 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++) for (int i = 0; i < width * (height - 9) / 350; i++)
addFish(); addFish();
} }
void Aquarium::addFish() { addEntityImpl<Fish>(); } void Aquarium::addFish() { addEntityImpl<Fish>(); }
void Aquarium::addBubble(float x, float y) { addEntityImpl<Bubble>(x, y); } void Aquarium::addBubble(float x, float y) { addEntityImpl<Bubble>(x, y); }
void Aquarium::addSeaweed() { addEntityImpl<Seaweed>(); } void Aquarium::addSeaweed() { addEntityImpl<Seaweed>(); }
@@ -223,7 +201,6 @@ void Aquarium::addCastle() { addEntityImpl<Castle>(); }
void Aquarium::addShip() { addEntityImpl<Ship>(); } void Aquarium::addShip() { addEntityImpl<Ship>(); }
void Aquarium::addSeaMonster() { addEntityImpl<SeaMonster>(); } void Aquarium::addSeaMonster() { addEntityImpl<SeaMonster>(); }
void Aquarium::addWhale() { addEntityImpl<Whale>(); } void Aquarium::addWhale() { addEntityImpl<Whale>(); }
void Aquarium::ensureBigEntityExists() { void Aquarium::ensureBigEntityExists() {
// Check if any big entities exist on screen // Check if any big entities exist on screen
for (const auto &entity : entities) { for (const auto &entity : entities) {
@@ -233,8 +210,7 @@ void Aquarium::ensureBigEntityExists() {
return; // Big entity found, do nothing return; // Big entity found, do nothing
} }
} }
// No big entity found, spawn next in cycle
// No big entity found, spawn next in cycle (Ship, SeaMonster, Whale)
int entity_type = big_entity_index % 3; int entity_type = big_entity_index % 3;
if (entity_type == 0) { if (entity_type == 0) {
addEntityImpl<Ship>(); addEntityImpl<Ship>();
@@ -245,35 +221,28 @@ void Aquarium::ensureBigEntityExists() {
} }
++big_entity_index; ++big_entity_index;
} }
void Aquarium::clearCurrentFrame() { void Aquarium::clearCurrentFrame() {
for (auto &row : currentFrame) { for (auto &row : currentFrame) {
std::fill(row.begin(), row.end(), Cell()); std::fill(row.begin(), row.end(), Cell());
} }
} }
void Aquarium::drawToFrame(int y, int x, const std::string &line, void Aquarium::drawToFrame(int y, int x, const std::string &line,
const std::string &colorLine) { const std::string &colorLine) {
const size_t len = std::min(line.size(), colorLine.size()); const size_t len = std::min(line.size(), colorLine.size());
for (size_t j = 0; j < len; ++j) { for (size_t j = 0; j < len; ++j) {
int cx = x + static_cast<int>(j); int cx = x + static_cast<int>(j);
if (cx < 0 || cx >= width) if (cx < 0 || cx >= width)
continue; continue;
const char ch = line[j]; const char ch = line[j];
const char colorChar = colorLine[j]; const char colorChar = colorLine[j];
const bool isBold = (colorChar >= 'A' && colorChar <= 'Z'); const bool isBold = (colorChar >= 'A' && colorChar <= 'Z');
currentFrame[y][cx] = { currentFrame[y][cx] = {
ch, static_cast<char>(isBold ? colorChar + 32 : colorChar), isBold}; ch, static_cast<char>(isBold ? colorChar + 32 : colorChar), isBold};
} }
} }
void Aquarium::initColorLookup() { void Aquarium::initColorLookup() {
for (int i = 0; i < 256; ++i) for (int i = 0; i < 256; ++i)
colorLookup[i] = ANSI::BLACK; // Default black colorLookup[i] = ANSI::BLACK; // Default black
colorLookup['r'] = ANSI::RED; colorLookup['r'] = ANSI::RED;
colorLookup['g'] = ANSI::GREEN; colorLookup['g'] = ANSI::GREEN;
colorLookup['y'] = ANSI::YELLOW; colorLookup['y'] = ANSI::YELLOW;
@@ -283,48 +252,34 @@ void Aquarium::initColorLookup() {
colorLookup['w'] = ANSI::WHITE; colorLookup['w'] = ANSI::WHITE;
colorLookup['k'] = ANSI::BLACK; 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() { void Aquarium::renderToScreen() {
static std::string output; static std::string output;
output.clear(); output.clear();
output.reserve(height * width * 20); // Reserve space for efficiency output.reserve(height * width * 20); // Reserve space for efficiency
for (int y = 0; y < height; ++y) { for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) { for (int x = 0; x < width; ++x) {
const Cell &newCell = currentFrame[y][x]; const Cell &newCell = currentFrame[y][x];
Cell &oldCell = previousFrame[y][x]; Cell &oldCell = previousFrame[y][x];
if (newCell != oldCell) { if (newCell != oldCell) {
oldCell = newCell; oldCell = newCell;
// Move cursor to position // Move cursor to position
output += ANSI::moveTo(y, x); output += ANSI::moveTo(y, x);
// Set color and attributes with black background
// Set color and attributes output += ANSI::RESET_BLACK_BG; // Reset with black background
output += ANSI::RESET; // Reset first
if (newCell.bold) { if (newCell.bold) {
output += ANSI::BOLD; output += ANSI::BOLD;
} }
output += colorLookup[static_cast<unsigned char>(newCell.colorChar)]; output += colorLookup[static_cast<unsigned char>(newCell.colorChar)];
// Add the character // Add the character
output += newCell.ch; output += newCell.ch;
} }
} }
} }
// Output everything at once
// Output everything at once for better performance
if (!output.empty()) { if (!output.empty()) {
printf("%s", output.c_str()); std::cout << output << std::flush;
fflush(stdout);
} }
} }
// Check for input (non-blocking) // Check for input (non-blocking)
int Aquarium::checkInput() { int Aquarium::checkInput() {
char c; char c;
@@ -333,8 +288,7 @@ int Aquarium::checkInput() {
} }
return -1; // No input available return -1; // No input available
} }
// Check if terminal was resized
// Check if terminal was resized (you'll need to call this periodically)
bool Aquarium::checkResize() { bool Aquarium::checkResize() {
struct winsize ws; struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) { if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) {
@@ -344,5 +298,4 @@ bool Aquarium::checkResize() {
} }
return false; return false;
} }
Aquarium::~Aquarium() { cleanup_terminal(0); } Aquarium::~Aquarium() { cleanup_terminal(0); }

View File

@@ -1,18 +1,11 @@
// main.cpp // main.cpp
#include "Aquarium.h" #include "Aquarium.h"
#ifdef __OpenBSD__
#include <unistd.h>
#endif
#include <chrono>
#include <iostream>
#include <thread>
int main() { int main() {
// Get the singleton instance // Get the singleton instance
Aquarium &aquarium = Aquarium::getInstance(); Aquarium &aquarium = Aquarium::getInstance();
#ifdef __OpenBSD__ #ifdef __OpenBSD__
// Most restrictive pledge - no file access needed!
if (pledge("stdio tty", NULL) == -1) { if (pledge("stdio tty", NULL) == -1) {
perror("pledge"); perror("pledge");
return 1; return 1;
@@ -24,22 +17,16 @@ int main() {
// Main game loop // Main game loop
while (true) { while (true) {
// Check for user input
int input = aquarium.checkInput(); int input = aquarium.checkInput();
if (input == 'q' || input == 'Q' || input == 27) { // ESC key if (input == 'q' || input == 'Q' || input == 27) { // ESC key
break; break;
} }
// Check if terminal was resized
if (aquarium.checkResize()) { if (aquarium.checkResize()) {
aquarium.resize(); aquarium.resize();
} }
// Redraw the aquarium
aquarium.redraw(); aquarium.redraw();
// Control frame rate (~20 FPS)
// std::this_thread::sleep_for(std::chrono::milliseconds(1));
} }
return 0; return 0;