commit 104834099f4f2199f59661287e6be9460e7a976d
parent 6976525c1fdb46b0bc6e3031ae29d759bed3654c
Author: amrfti <andrew@kloet.net>
Date: Sat, 12 Jul 2025 17:33:17 -0400
add config refactor structure
Diffstat:
46 files changed, 1360 insertions(+), 984 deletions(-)
diff --git a/Makefile b/Makefile
@@ -1,27 +1,30 @@
CXX = c++
-CXXFLAGS = -Wall -Wextra -O3
+CXXFLAGS = -Wall -Wextra -O3 -Isrc
LDFLAGS = -static
-
SRC_DIR = src
OBJ_DIR = build
BIN_DIR = bin
-
SRC_EXT = cpp
OBJ_EXT = o
-SOURCES = $(wildcard $(SRC_DIR)/*.$(SRC_EXT))
+# Find all source files recursively in subdirectories
+SOURCES = $(shell find $(SRC_DIR) -name '*.$(SRC_EXT)')
+# Create corresponding object files maintaining directory structure
OBJECTS = $(SOURCES:$(SRC_DIR)/%.$(SRC_EXT)=$(OBJ_DIR)/%.$(OBJ_EXT))
-
EXEC = $(BIN_DIR)/fissh
+# Get all unique subdirectories for build structure
+OBJ_DIRS = $(sort $(dir $(OBJECTS)))
+
all: $(EXEC)
$(EXEC): $(OBJECTS)
- @mkdir -p $(BIN_DIR) # Make sure the bin dir exists
+ @mkdir -p $(BIN_DIR)
$(CXX) $(OBJECTS) -o $(EXEC) $(LDFLAGS)
-$(OBJ_DIR)/%.o: $(SRC_DIR)/%.$(SRC_EXT)
- @mkdir -p $(OBJ_DIR) # Make sure the obj dir exists
+# Create object files, maintaining directory structure
+$(OBJ_DIR)/%.$(OBJ_EXT): $(SRC_DIR)/%.$(SRC_EXT)
+ @mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
@@ -30,4 +33,4 @@ clean:
install: $(EXEC)
install -m 755 $(EXEC) /usr/local/bin/
-.PHONY: all clean run
+.PHONY: all clean install
diff --git a/src/Aquarium.cpp b/src/Aquarium.cpp
@@ -1,340 +0,0 @@
-#include "Aquarium.h"
-#include "Bubble.h"
-#include "Castle.h"
-#include "Fish.h"
-#include "SeaMonster.h"
-#include "Seaweed.h"
-#include "Ship.h"
-#include "Waterline.h"
-#include "Whale.h"
-#include <algorithm>
-#include <cstdio>
-#include <cstring>
-#include <iostream>
-#include <signal.h>
-#include <sys/ioctl.h>
-#include <termios.h>
-#include <unistd.h>
-
-// 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() {
- // 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);
- 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<Cell>(width));
- previousFrame.assign(height, std::vector<Cell>(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 {
- cleanup_terminal(0);
- std::cerr << "Error: Unable to determine terminal size.\n";
- std::exit(1);
- }
-}
-
-void Aquarium::ensureEntitiesSorted() {
- if (entities_need_sorting) {
- std::sort(entities.begin(), entities.end(),
- [](const auto &a, const auto &b) {
- int layerA = a->getPreferredLayer();
- int layerB = b->getPreferredLayer();
- if (layerA != layerB)
- return layerA < layerB;
- return a->getId() < b->getId();
- });
- entities_need_sorting = false;
- }
-}
-
-void Aquarium::redraw() {
- clearCurrentFrame();
- ensureBigEntityExists();
-
- static std::vector<std::unique_ptr<Entity>> newEntities;
- static std::vector<size_t> 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<Fish *>(entity.get())) {
- if (fish->shouldSpawnBubble()) {
- newEntities.emplace_back(
- std::make_unique<Bubble>(fish->getX(), fish->getY()));
- }
- }
-
- if (entity->shouldBeRemoved()) {
- auto replacement = entity->createReplacement();
- if (replacement) {
- entity = std::move(replacement); // Replace in-place
- entities_need_sorting = true;
- } else {
- entitiesToRemove.push_back(i); // Mark for removal
- }
- }
- }
-
- // 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%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::BG_BLACK);
- fflush(stdout);
-
- getTerminalSize();
-
- currentFrame.assign(height, std::vector<Cell>(width));
- previousFrame.assign(height, std::vector<Cell>(width));
-
- entities.clear();
- entities_need_sorting = true;
-
- addWaterline();
- addCastle();
- for (int i = 0; i < width / 15; i++)
- addSeaweed();
- for (int i = 0; i < width * (height - 9) / 350; i++)
- addFish();
-}
-
-void Aquarium::addFish() { addEntityImpl<Fish>(); }
-void Aquarium::addBubble(float x, float y) { addEntityImpl<Bubble>(x, y); }
-void Aquarium::addSeaweed() { addEntityImpl<Seaweed>(); }
-void Aquarium::addWaterline() { addEntityImpl<Waterline>(); }
-void Aquarium::addCastle() { addEntityImpl<Castle>(); }
-void Aquarium::addShip() { addEntityImpl<Ship>(); }
-void Aquarium::addSeaMonster() { addEntityImpl<SeaMonster>(); }
-void Aquarium::addWhale() { addEntityImpl<Whale>(); }
-
-void Aquarium::ensureBigEntityExists() {
- // Check if any big entities exist on screen
- for (const auto &entity : entities) {
- if (dynamic_cast<Ship *>(entity.get()) ||
- dynamic_cast<SeaMonster *>(entity.get()) ||
- dynamic_cast<Whale *>(entity.get())) {
- 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) {
- addEntityImpl<Ship>();
- } else if (entity_type == 1) {
- addEntityImpl<SeaMonster>();
- } else {
- addEntityImpl<Whale>();
- }
- ++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<int>(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<char>(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;
- 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);
-
- int cursor_y = -1, cursor_x = -1;
-
- 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)
- continue;
-
- oldCell = newCell;
-
- // Move cursor only when needed
- if (cursor_y != y || cursor_x != x) {
- output += ANSI::moveTo(y, x);
- cursor_y = y;
- cursor_x = x;
- }
-
- // Apply cell formatting and character
- output += ANSI::RESET_BLACK_BG;
- if (newCell.bold)
- output += ANSI::BOLD;
- output += colorLookup[static_cast<unsigned char>(newCell.colorChar)];
- output += newCell.ch;
-
- ++cursor_x;
- }
- }
-
- if (!output.empty()) {
- std::cout << output << std::flush;
- }
-}
-
-// 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
@@ -1,74 +0,0 @@
-#pragma once
-#include "Entity.h"
-#include <memory>
-#include <vector>
-
-extern int g_maxCells;
-
-class Aquarium {
-private:
- int width;
- int height;
-
- struct Cell {
- char ch = ' ';
- char colorChar = 'k';
- bool bold = false;
- bool operator==(const Cell &other) const {
- return ch == other.ch && colorChar == other.colorChar &&
- bold == other.bold;
- }
- bool operator!=(const Cell &other) const { return !(*this == other); }
- };
-
- std::vector<std::vector<Cell>> currentFrame;
- std::vector<std::vector<Cell>> previousFrame;
- std::vector<std::unique_ptr<Entity>> entities;
- size_t big_entity_index = 0;
- void ensureBigEntityExists();
- bool entities_need_sorting = true;
- static inline const char *colorLookup[256] = {nullptr};
- static inline bool colorLookupInitialized = false;
-
-public:
- Aquarium();
- ~Aquarium();
-
- static Aquarium &getInstance() {
- static Aquarium instance;
- return instance;
- }
-
- [[nodiscard]] int getWidth() const { return width; }
- [[nodiscard]] int getHeight() const { return height; }
-
- void addFish();
- void addBubble(float x, float y);
- void addSeaweed();
- void addWaterline();
- void addCastle();
- 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 <typename T, typename... Args> void addEntityImpl(Args &&...args) {
- entities.emplace_back(std::make_unique<T>(std::forward<Args>(args)...));
- entities_need_sorting = true;
- }
-};
diff --git a/src/Castle.cpp b/src/Castle.cpp
@@ -1,8 +0,0 @@
-#include "Castle.h"
-#include "Aquarium.h"
-#include "assets/CastleAssets.h"
-
-Castle::Castle()
- : Entity(Aquarium::getInstance().getWidth() - 32,
- Aquarium::getInstance().getHeight() - 13),
- image(getCastleAsset().image), mask(getCastleAsset().mask) {}
diff --git a/src/Entity.cpp b/src/Entity.cpp
@@ -1,63 +0,0 @@
-#include "Entity.h"
-#include "Aquarium.h"
-
-void Entity::draw() const {
- auto &aquarium = Aquarium::getInstance();
-
- const auto &image = getImage();
- const auto &mask = getMask();
- const char default_color = getDefaultColor();
-
- const int base_x = static_cast<int>(x);
- const int base_y = static_cast<int>(y);
-
- for (size_t i = 0; i < image.size(); ++i) {
- const std::string &row = image[i];
- const std::string &mask_row = (i < mask.size()) ? mask[i] : "";
-
- // Build complete line at once instead of segments
- std::string line;
- std::string colors;
- line.reserve(row.size());
- colors.reserve(row.size());
-
- int start_x = base_x;
-
- for (size_t j = 0; j < row.size(); ++j) {
- const char ch = row[j];
-
- if (ch == '?') {
- // Flush current line if not empty
- if (!line.empty()) {
- aquarium.drawToFrame(base_y + static_cast<int>(i), start_x, line,
- colors);
- start_x += static_cast<int>(line.size()) + 1; // +1 for the '?' skip
- line.clear();
- colors.clear();
- } else {
- ++start_x; // Just skip the '?' position
- }
- continue;
- }
-
- line.push_back(ch);
-
- char color = default_color;
- if (j < mask_row.size() && mask_row[j] != ' ') {
- color = mask_row[j];
- }
- colors.push_back(color);
- }
-
- // Flush remaining line
- if (!line.empty()) {
- aquarium.drawToFrame(base_y + static_cast<int>(i), start_x, line, colors);
- }
- }
-}
-
-bool Entity::shouldBeRemoved() const noexcept {
- const auto &aquarium = Aquarium::getInstance();
- // unsigned nonsense
- return x < -static_cast<int>(getWidth()) || x > aquarium.getWidth();
-}
diff --git a/src/Fish.cpp b/src/Fish.cpp
@@ -1,63 +0,0 @@
-#include "Fish.h"
-#include "Aquarium.h"
-#include "Random.h"
-#include "assets/FishAssets.h"
-#include "defs.h"
-
-std::unordered_map<char, char> Fish::color_map;
-
-Fish::Fish() : Fish(getRandomAssetIndex()) {}
-
-Fish::Fish(int asset_index)
- : Entity(asset_index % 2 == 0),
- image(getFishAssetPairs()[asset_index].image),
- mask(getFishAssetPairs()[asset_index].mask),
- speed(Random::floatInRange(0.25f, 2.25f)) {
-
- const auto &aquarium = Aquarium::getInstance();
- y = Random::intInRange(static_cast<int>(image.size()) + 6,
- aquarium.getHeight() - static_cast<int>(image.size()));
- x = moving_right ? -this->getWidth()
- : static_cast<float>(aquarium.getWidth());
- randomizeMask();
-}
-
-void Fish::randomizeMask() {
- // Clear and rebuild color map with random colors each time
- color_map.clear();
- color_map['4'] = 'W'; // White is always '4' for eyes
-
- // Assign random colors to digits 1-3, 5-9
- for (char digit = '1'; digit <= '9'; ++digit) {
- if (digit != '4') {
- color_map[digit] = AVAILABLE_COLORS[Random::intInRange(
- 0, static_cast<int>(AVAILABLE_COLORS.size()) - 1)];
- }
- }
-
- // Apply color mapping to mask
- for (auto &line : mask) {
- for (char &ch : line) {
- if (auto it = color_map.find(ch); it != color_map.end()) {
- ch = it->second;
- }
- }
- }
-}
-
-int Fish::getRandomAssetIndex() {
- return Random::intInRange(0,
- static_cast<int>(getFishAssetPairs().size()) - 1);
-}
-
-void Fish::update() noexcept { x += moving_right ? speed : -speed; }
-
-std::unique_ptr<Entity> Fish::createReplacement() const {
- return std::make_unique<Fish>();
-}
-
-int Fish::getPreferredLayer() const noexcept { return 10; }
-
-bool Fish::shouldSpawnBubble() const {
- return Random::floatInRange(0, 1) < BUBBLE_SPAWN_CHANCE;
-}
diff --git a/src/Fish.h b/src/Fish.h
@@ -1,34 +0,0 @@
-#pragma once
-#include "Entity.h"
-#include "assets/FishAssets.h"
-#include <array>
-#include <unordered_map>
-
-class Fish : public Entity {
-private:
- static constexpr std::array<char, 12> AVAILABLE_COLORS = {
- 'c', 'C', 'r', 'R', 'y', 'Y', 'b', 'B', 'g', 'G', 'm', 'M'};
-
- const std::vector<std::string> ℑ
- std::vector<std::string> mask;
- const float speed;
-
- static std::unordered_map<char, char> color_map;
-
- explicit Fish(int asset_index);
- static int getRandomAssetIndex();
- void randomizeMask();
-
-public:
- Fish();
-
- void update() noexcept override;
- const std::vector<std::string> &getImage() const override { return image; }
- const std::vector<std::string> &getMask() const override { return mask; }
- char getDefaultColor() const noexcept override { return 'k'; }
-
- std::unique_ptr<Entity> createReplacement() const override;
- int getPreferredLayer() const noexcept override;
-
- bool shouldSpawnBubble() const;
-};
diff --git a/src/SeaMonster.cpp b/src/SeaMonster.cpp
@@ -1,44 +0,0 @@
-#include "SeaMonster.h"
-#include "Aquarium.h"
-#include "Random.h"
-#include "assets/SeaMonsterAssets.h"
-
-SeaMonster::SeaMonster() : SeaMonster(getRandomDirection()) {}
-
-SeaMonster::SeaMonster(int asset_index)
- : Entity(asset_index == 0),
- frames(getSeaMonsterAssets()[asset_index].frames),
- mask(getSeaMonsterAssets()[asset_index].mask), speed(SEAMONSTER_SPEED) {
-
- const auto &aquarium = Aquarium::getInstance();
- y = WATER_SURFACE_OFFSET;
-
- // Use first frame for positioning calculations
- const auto &first_frame = frames[0];
- if (moving_right) {
- x = -static_cast<float>(first_frame[0].length());
- } else {
- x = static_cast<float>(aquarium.getWidth());
- }
-
- current_image = first_frame;
-}
-
-int SeaMonster::getRandomDirection() { return Random::intInRange(0, 1); }
-
-void SeaMonster::update() noexcept {
- x += moving_right ? speed : -speed;
-
- ++animation_counter;
- if (animation_counter >= ANIMATION_DELAY) {
- current_frame_index = (current_frame_index + 1) % frames.size();
- animation_counter = 0;
- }
-}
-
-const std::vector<std::string> &SeaMonster::getImage() const {
- current_image = frames[current_frame_index];
- return current_image;
-}
-
-int SeaMonster::getPreferredLayer() const noexcept { return 8; }
diff --git a/src/SeaMonster.h b/src/SeaMonster.h
@@ -1,32 +0,0 @@
-#pragma once
-#include "Entity.h"
-#include "assets/SeaMonsterAssets.h"
-
-class SeaMonster : public Entity {
-private:
- static constexpr float SEAMONSTER_SPEED = 0.8f;
- static constexpr int WATER_SURFACE_OFFSET = 2;
-
- const std::vector<std::vector<std::string>> frames;
- const std::vector<std::string> &mask;
- const float speed;
-
- int current_frame_index = 0;
- int animation_counter = 0;
- mutable std::vector<std::string> current_image;
-
- static constexpr int ANIMATION_DELAY = 5;
-
- explicit SeaMonster(int asset_index);
- static int getRandomDirection();
-
-public:
- SeaMonster();
-
- void update() noexcept override;
- const std::vector<std::string> &getImage() const override;
- const std::vector<std::string> &getMask() const override { return mask; }
- char getDefaultColor() const noexcept override { return 'G'; }
-
- int getPreferredLayer() const noexcept override;
-};
diff --git a/src/Seaweed.cpp b/src/Seaweed.cpp
@@ -1,63 +0,0 @@
-#include "Seaweed.h"
-#include "Aquarium.h"
-#include "Random.h"
-#include "defs.h"
-
-Seaweed::Seaweed()
- : Entity(),
- height(Random::intInRange(SEAWEED_MIN_HEIGHT, SEAWEED_MAX_HEIGHT)),
- speed(Random::floatInRange(0.1f, 0.3f)),
- lifetime(Random::intInRange(SEAWEED_MIN_LIFETIME, SEAWEED_MAX_LIFETIME)) {
-
- x = Random::intInRange(0, Aquarium::getInstance().getWidth());
- y = Aquarium::getInstance().getHeight() - height;
-
- current_image.resize(height);
- current_mask.resize(height);
- generateCurrentFrame();
-}
-
-void Seaweed::update() noexcept {
- frame += speed;
- if (frame >= 1.0f) {
- pattern_flipped = !pattern_flipped;
- frame -= 1.0f;
- frame_dirty = true; // mark frame as needing regeneration
- }
- --lifetime;
-}
-
-const std::vector<std::string> &Seaweed::getImage() const {
- if (frame_dirty) {
- generateCurrentFrame();
- frame_dirty = false;
- }
- return current_image;
-}
-
-const std::vector<std::string> &Seaweed::getMask() const {
- if (frame_dirty) {
- generateCurrentFrame();
- frame_dirty = false;
- }
- return current_mask;
-}
-
-void Seaweed::generateCurrentFrame() const {
- for (size_t i = 0; i < height; ++i) {
- const bool use_left = (i % 2 == 0) ^ pattern_flipped;
- const char ch = use_left ? PATTERN_LEFT : PATTERN_RIGHT;
- const int offset = use_left ? 0 : 1;
-
- current_image[i] = std::string(offset, ' ') + ch;
- current_mask[i] = std::string(current_image[i].size(), 'g');
- }
-}
-
-bool Seaweed::shouldBeRemoved() const noexcept { return lifetime == 0; }
-
-std::unique_ptr<Entity> Seaweed::createReplacement() const {
- return std::make_unique<Seaweed>();
-}
-
-int Seaweed::getPreferredLayer() const noexcept { return 1; }
diff --git a/src/Ship.cpp b/src/Ship.cpp
@@ -1,25 +0,0 @@
-#include "Ship.h"
-#include "Aquarium.h"
-#include "Random.h"
-#include "assets/ShipAssets.h"
-
-Ship::Ship() : Ship(getRandomDirection()) {}
-
-Ship::Ship(int asset_index)
- : Entity(asset_index == 0), image(getShipAssets()[asset_index].image),
- mask(getShipAssets()[asset_index].mask), speed(SHIP_SPEED) {
-
- const auto &aquarium = Aquarium::getInstance();
- y = 0;
- if (moving_right) {
- x = -static_cast<float>(image[0].length());
- } else {
- x = static_cast<float>(aquarium.getWidth());
- }
-}
-
-int Ship::getRandomDirection() { return Random::intInRange(0, 1); }
-
-void Ship::update() noexcept { x += moving_right ? speed : -speed; }
-
-int Ship::getPreferredLayer() const noexcept { return 9; }
diff --git a/src/Ship.h b/src/Ship.h
@@ -1,25 +0,0 @@
-#pragma once
-#include "Entity.h"
-#include "assets/ShipAssets.h"
-
-class Ship : public Entity {
-private:
- static constexpr float SHIP_SPEED = 1.0f;
-
- const std::vector<std::string> ℑ
- const std::vector<std::string> &mask;
- const float speed;
-
- explicit Ship(int asset_index);
- static int getRandomDirection();
-
-public:
- Ship();
-
- void update() noexcept override;
- const std::vector<std::string> &getImage() const override { return image; }
- const std::vector<std::string> &getMask() const override { return mask; }
- char getDefaultColor() const noexcept override { return 'W'; }
-
- int getPreferredLayer() const noexcept override;
-};
diff --git a/src/Waterline.cpp b/src/Waterline.cpp
@@ -1,83 +0,0 @@
-#include "Waterline.h"
-#include "Aquarium.h"
-#include "Random.h"
-#include "defs.h"
-
-Waterline::Waterline() : Entity(0, WATERLINE_Y) {
- shape[0] = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~";
- shape[1] = "^^^^ ^^^ ^^^ ^^^ ^^^^ ";
- shape[2] = "^^^^ ^^^^ ^^^ ^^ ";
- shape[3] = "^^ ^^^^ ^^^ ^^^^^^ ";
-
- const size_t width = Aquarium::getInstance().getWidth();
-
- for (size_t i = 0; i < NUM_WAVE_LAYERS; ++i) {
- const std::string &original = shape[i];
- const size_t pattern_len = original.length();
-
- // Calculate how many full patterns + remainder we need
- const size_t full_patterns = width / pattern_len;
- const size_t remainder = width % pattern_len;
-
- shape[i].reserve(width);
- for (size_t p = 0; p < full_patterns; ++p) {
- shape[i] += original;
- }
- if (remainder > 0) {
- shape[i] += original.substr(0, remainder);
- }
-
- // Create color line
- colorLines[i].assign(shape[i].size(), WATERLINE_COLOR);
- }
-
- // Initialize cache vectors
- cached_image.assign(shape.begin(), shape.end());
- cached_mask.assign(colorLines.begin(), colorLines.end());
-}
-
-void Waterline::update() noexcept {
- // Use cached probability calculations
- static constexpr float thresholds[NUM_WAVE_LAYERS] = {
- 0.0f, // Layer 0 never moves
- 0.25f / WAVE_MOVE_CHANCE, 0.5f / WAVE_MOVE_CHANCE,
- 0.75f / WAVE_MOVE_CHANCE};
-
- for (size_t i = 1; i < NUM_WAVE_LAYERS; ++i) {
- if (Random::floatInRange(0.0f, 1.0f) < thresholds[i]) {
- if (Random::intInRange(0, 1) == 0) {
- shiftStringLeft(shape[i]);
- } else {
- shiftStringRight(shape[i]);
- }
- }
- }
-
- // Update cached vectors
- std::copy(shape.begin(), shape.end(), cached_image.begin());
- std::copy(colorLines.begin(), colorLines.end(), cached_mask.begin());
-}
-
-void Waterline::shiftStringLeft(std::string &str) {
- if (!str.empty()) {
- char first = str.front();
- str.erase(0, 1);
- str.push_back(first);
- }
-}
-
-void Waterline::shiftStringRight(std::string &str) {
- if (!str.empty()) {
- char last = str.back();
- str.pop_back();
- str.insert(0, 1, last);
- }
-}
-
-const std::vector<std::string> &Waterline::getImage() const {
- return cached_image;
-}
-
-const std::vector<std::string> &Waterline::getMask() const {
- return cached_mask;
-}
diff --git a/src/Whale.cpp b/src/Whale.cpp
@@ -1,48 +0,0 @@
-#include "Whale.h"
-#include "Aquarium.h"
-#include "Random.h"
-#include "assets/WhaleAssets.h"
-
-Whale::Whale() : Whale(getRandomDirection()) {}
-
-Whale::Whale(int asset_index)
- : Entity((asset_index == 0)), frames(getWhaleAssets()[asset_index].frames),
- mask(getWhaleAssets()[asset_index].mask), speed(WHALE_SPEED) {
-
- const auto &aquarium = Aquarium::getInstance();
- y = 0;
-
- // Use first frame for positioning calculations
- const auto &first_frame = frames[0];
- if (moving_right) {
- x = -static_cast<float>(first_frame[6].length());
- } else {
- x = static_cast<float>(aquarium.getWidth());
- }
-
- current_image = first_frame;
-}
-
-int Whale::getRandomDirection() { return Random::intInRange(0, 1); }
-
-void Whale::update() noexcept {
- x += moving_right ? speed : -speed;
-
- ++animation_counter;
-
- // Use longer delay for first frame (no water spout)
- int current_delay =
- (current_frame_index == 0) ? FIRST_FRAME_PAUSE : ANIMATION_DELAY;
-
- if (animation_counter >= current_delay) {
- current_frame_index = (current_frame_index + 1) % frames.size();
- animation_counter = 0;
- }
-}
-
-const std::vector<std::string> &Whale::getImage() const {
- current_image = frames[current_frame_index];
- return current_image;
-}
-
-int Whale::getPreferredLayer() const noexcept { return 8; }
diff --git a/src/Whale.h b/src/Whale.h
@@ -1,31 +0,0 @@
-#pragma once
-#include "Entity.h"
-#include "assets/WhaleAssets.h"
-
-class Whale : public Entity {
-private:
- static constexpr float WHALE_SPEED = 1.0f;
-
- const std::vector<std::vector<std::string>> frames;
- const std::vector<std::string> &mask;
- const float speed;
-
- int current_frame_index = 0;
- int animation_counter = 0;
- mutable std::vector<std::string> current_image;
-
- static constexpr int ANIMATION_DELAY = 1;
- static constexpr int FIRST_FRAME_PAUSE = 10;
-
- explicit Whale(int asset_index);
- static int getRandomDirection();
-
-public:
- Whale();
-
- void update() noexcept override;
- const std::vector<std::string> &getImage() const override;
- const std::vector<std::string> &getMask() const override { return mask; }
- char getDefaultColor() const noexcept override { return 'B'; }
- int getPreferredLayer() const noexcept override;
-};
diff --git a/src/assets/FishAssets.h b/src/assets/FishAssets.h
@@ -1,7 +1,7 @@
#pragma once
-#include "../Entity.h"
-#include "../SpriteUtils.h"
+#include "../entities/Entity.h"
+#include "../utils/SpriteUtils.h"
#include <vector>
/*
diff --git a/src/assets/SeaMonsterAssets.h b/src/assets/SeaMonsterAssets.h
@@ -1,6 +1,6 @@
#pragma once
-#include "../Entity.h"
-#include "../SpriteUtils.h"
+#include "../entities/Entity.h"
+#include "../utils/SpriteUtils.h"
#include <vector>
struct SeaMonsterAsset {
diff --git a/src/assets/ShipAssets.h b/src/assets/ShipAssets.h
@@ -1,6 +1,6 @@
#pragma once
-#include "../Entity.h"
-#include "../SpriteUtils.h"
+#include "../entities/Entity.h"
+#include "../utils/SpriteUtils.h"
#include <vector>
inline const AssetPair &getShip() {
diff --git a/src/assets/WhaleAssets.h b/src/assets/WhaleAssets.h
@@ -1,6 +1,6 @@
#pragma once
-#include "../Entity.h"
-#include "../SpriteUtils.h"
+#include "../entities/Entity.h"
+#include "../utils/SpriteUtils.h"
#include <vector>
struct WhaleAsset {
diff --git a/src/core/Aquarium.cpp b/src/core/Aquarium.cpp
@@ -0,0 +1,423 @@
+#include "Aquarium.h"
+#include "../core/Config.h"
+#include "../entities/Bubble.h"
+#include "../entities/Castle.h"
+#include "../entities/Fish.h"
+#include "../entities/SeaMonster.h"
+#include "../entities/Seaweed.h"
+#include "../entities/Ship.h"
+#include "../entities/Waterline.h"
+#include "../entities/Whale.h"
+#include <algorithm>
+#include <chrono>
+#include <cstdio>
+#include <cstring>
+#include <iomanip>
+#include <iostream>
+#include <signal.h>
+#include <sstream>
+#include <sys/ioctl.h>
+#include <termios.h>
+#include <unistd.h>
+
+// 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() {
+ // 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);
+ 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<Cell>(width));
+ previousFrame.assign(height, std::vector<Cell>(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 {
+ cleanup_terminal(0);
+ std::cerr << "Error: Unable to determine terminal size.\n";
+ std::exit(1);
+ }
+}
+
+void Aquarium::ensureEntitiesSorted() {
+ if (entities_need_sorting) {
+ std::sort(entities.begin(), entities.end(),
+ [](const auto &a, const auto &b) {
+ int layerA = a->getPreferredLayer();
+ int layerB = b->getPreferredLayer();
+ if (layerA != layerB)
+ return layerA < layerB;
+ return a->getId() < b->getId();
+ });
+ entities_need_sorting = false;
+ }
+}
+
+void Aquarium::redraw() {
+ clearCurrentFrame();
+
+ if (g_config.enable_big_entities) {
+ ensureBigEntityExists();
+ }
+
+ static std::vector<std::unique_ptr<Entity>> newEntities;
+ static std::vector<size_t> entitiesToRemove;
+
+ newEntities.clear();
+ entitiesToRemove.clear();
+
+ // Count current bubbles
+ int bubble_count = 0;
+ for (const auto &entity : entities) {
+ if (dynamic_cast<Bubble *>(entity.get())) {
+ bubble_count++;
+ }
+ }
+
+ // 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 with configuration
+ if (g_config.enable_bubbles && bubble_count < g_config.max_bubbles) {
+ if (auto *fish = dynamic_cast<Fish *>(entity.get())) {
+ if (fish->shouldSpawnBubble()) {
+ // Use configured bubble rate
+ if (rand() % g_config.fish_bubble_rate == 0) {
+ newEntities.emplace_back(
+ std::make_unique<Bubble>(fish->getX(), fish->getY()));
+ bubble_count++;
+ }
+ }
+ }
+ }
+
+ if (entity->shouldBeRemoved()) {
+ auto replacement = entity->createReplacement();
+ if (replacement) {
+ entity = std::move(replacement);
+ entities_need_sorting = true;
+ } else {
+ entitiesToRemove.push_back(i);
+ }
+ }
+ }
+
+ // 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 and haven't exceeded max
+ if (!newEntities.empty() &&
+ entities.size() < static_cast<size_t>(g_config.max_entities)) {
+ 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();
+ }
+
+ // Draw debug information
+ if (g_config.show_fps || g_config.show_entity_count) {
+ drawDebugInfo();
+ }
+
+ renderToScreen();
+}
+
+void Aquarium::drawDebugInfo() {
+ static double last_fps = 0.0;
+ static int frame_counter = 0;
+ static auto last_time = std::chrono::steady_clock::now();
+
+ frame_counter++;
+ auto current_time = std::chrono::steady_clock::now();
+ auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
+ current_time - last_time)
+ .count();
+
+ if (elapsed >= 1000) {
+ last_fps = (frame_counter * 1000.0) / elapsed;
+ frame_counter = 0;
+ last_time = current_time;
+ }
+
+ std::stringstream debug_info;
+ if (g_config.show_fps) {
+ debug_info << "FPS: " << std::fixed << std::setprecision(1) << last_fps;
+ }
+ if (g_config.show_entity_count) {
+ if (g_config.show_fps)
+ debug_info << " | ";
+ debug_info << "Entities: " << entities.size();
+ }
+
+ std::string debug_str = debug_info.str();
+ if (!debug_str.empty()) {
+ std::string color_str(debug_str.length(), 'w'); // White color
+ drawToFrame(0, width - static_cast<int>(debug_str.length()), debug_str,
+ color_str);
+ }
+}
+
+void Aquarium::resize() {
+ printf("%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::BG_BLACK);
+ fflush(stdout);
+
+ getTerminalSize();
+
+ currentFrame.assign(height, std::vector<Cell>(width));
+ previousFrame.assign(height, std::vector<Cell>(width));
+
+ entities.clear();
+ entities_need_sorting = true;
+
+ addWaterline();
+ addCastle();
+
+ // Use configured seaweed count or auto-calculate
+ int seaweed_count = g_config.initial_seaweed_count;
+ if (seaweed_count == -1) {
+ seaweed_count = width / 15;
+ }
+ for (int i = 0; i < seaweed_count; i++) {
+ addSeaweed();
+ }
+
+ // Use configured fish count or auto-calculate
+ int fish_count = g_config.initial_fish_count;
+ if (fish_count == -1) {
+ fish_count = width * (height - 9) / 350;
+ }
+ for (int i = 0; i < fish_count; i++) {
+ addFish();
+ }
+}
+
+void Aquarium::addFish() { addEntityImpl<Fish>(); }
+void Aquarium::addBubble(float x, float y) { addEntityImpl<Bubble>(x, y); }
+void Aquarium::addSeaweed() { addEntityImpl<Seaweed>(); }
+void Aquarium::addWaterline() { addEntityImpl<Waterline>(); }
+void Aquarium::addCastle() { addEntityImpl<Castle>(); }
+void Aquarium::addShip() { addEntityImpl<Ship>(); }
+void Aquarium::addSeaMonster() { addEntityImpl<SeaMonster>(); }
+void Aquarium::addWhale() { addEntityImpl<Whale>(); }
+
+void Aquarium::ensureBigEntityExists() {
+ // Check if any big entities exist on screen
+ for (const auto &entity : entities) {
+ if (dynamic_cast<Ship *>(entity.get()) ||
+ dynamic_cast<SeaMonster *>(entity.get()) ||
+ dynamic_cast<Whale *>(entity.get())) {
+ 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) {
+ addEntityImpl<Ship>();
+ } else if (entity_type == 1) {
+ addEntityImpl<SeaMonster>();
+ } else {
+ addEntityImpl<Whale>();
+ }
+ ++big_entity_index;
+}
+
+void Aquarium::clearCurrentFrame() {
+ static const Cell empty_cell{};
+
+ for (auto &row : currentFrame) {
+ row.assign(width, empty_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<int>(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<char>(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;
+ 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);
+
+ int cursor_y = -1, cursor_x = -1;
+
+ 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)
+ continue;
+
+ oldCell = newCell;
+
+ // Move cursor only when needed
+ if (cursor_y != y || cursor_x != x) {
+ output += ANSI::moveTo(y, x);
+ cursor_y = y;
+ cursor_x = x;
+ }
+
+ // Apply cell formatting and character
+ output += ANSI::RESET_BLACK_BG;
+
+ // Only apply bold if configured
+ if (g_config.use_bold && newCell.bold) {
+ output += ANSI::BOLD;
+ }
+
+ // Only apply colors if configured
+ if (g_config.use_colors) {
+ output += colorLookup[static_cast<unsigned char>(newCell.colorChar)];
+ }
+
+ output += newCell.ch;
+ ++cursor_x;
+ }
+ }
+
+ if (!output.empty()) {
+ std::cout << output << std::flush;
+ }
+}
+
+// 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/core/Aquarium.h b/src/core/Aquarium.h
@@ -0,0 +1,75 @@
+#pragma once
+#include "../entities/Entity.h"
+#include <memory>
+#include <vector>
+
+extern int g_maxCells;
+
+class Aquarium {
+private:
+ int width;
+ int height;
+
+ struct Cell {
+ char ch = ' ';
+ char colorChar = 'k';
+ bool bold = false;
+ bool operator==(const Cell &other) const {
+ return ch == other.ch && colorChar == other.colorChar &&
+ bold == other.bold;
+ }
+ bool operator!=(const Cell &other) const { return !(*this == other); }
+ };
+
+ std::vector<std::vector<Cell>> currentFrame;
+ std::vector<std::vector<Cell>> previousFrame;
+ std::vector<std::unique_ptr<Entity>> entities;
+ size_t big_entity_index = 0;
+ void ensureBigEntityExists();
+ bool entities_need_sorting = true;
+ static inline const char *colorLookup[256] = {nullptr};
+ static inline bool colorLookupInitialized = false;
+ void drawDebugInfo();
+
+public:
+ Aquarium();
+ ~Aquarium();
+
+ static Aquarium &getInstance() {
+ static Aquarium instance;
+ return instance;
+ }
+
+ [[nodiscard]] int getWidth() const { return width; }
+ [[nodiscard]] int getHeight() const { return height; }
+
+ void addFish();
+ void addBubble(float x, float y);
+ void addSeaweed();
+ void addWaterline();
+ void addCastle();
+ 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 <typename T, typename... Args> void addEntityImpl(Args &&...args) {
+ entities.emplace_back(std::make_unique<T>(std::forward<Args>(args)...));
+ entities_need_sorting = true;
+ }
+};
diff --git a/src/core/Config.cpp b/src/core/Config.cpp
@@ -0,0 +1,175 @@
+#include "Config.h"
+#include <cstdlib>
+#include <cstring>
+#include <getopt.h>
+#include <iostream>
+
+AquariumConfig g_config;
+
+void printUsage(const char *program_name) {
+ std::cout << "Usage: " << program_name << " [OPTIONS]\n\n";
+ std::cout << "Aquarium screensaver with customizable settings\n\n";
+ std::cout << "OPTIONS:\n";
+ std::cout << " -h, --help Show this help message\n";
+ std::cout
+ << " -f, --fish COUNT Number of initial fish (default: auto)\n";
+ std::cout << " -s, --seaweed COUNT Number of seaweed plants (default: "
+ "auto)\n";
+ std::cout << " -b, --max-bubbles COUNT Maximum bubbles on screen "
+ "(default: 50)\n";
+ std::cout << " -d, --delay MS Frame delay in milliseconds "
+ "(default: 100)\n";
+ std::cout << " -r, --bubble-rate RATE Fish bubble spawn rate 1/N "
+ "(default: 10)\n";
+ std::cout << " -m, --max-entities COUNT Maximum entities on screen "
+ "(default: 200)\n";
+ std::cout << " --no-big-entities Disable ships, whales, and sea "
+ "monsters\n";
+ std::cout << " --no-bubbles Disable bubble generation\n";
+ std::cout << " --no-colors Disable colors (monochrome mode)\n";
+ std::cout << " --no-bold Disable bold text\n";
+ std::cout << " --show-fps Display FPS counter\n";
+ std::cout << " --show-count Display entity count\n";
+ std::cout << " --debug Enable debug mode\n";
+ std::cout << "\nShort options can be combined: -f20 -s5 -d50\n";
+ std::cout << "Long options accept = syntax: --fish=20 --delay=50\n";
+ std::cout << "\nExamples:\n";
+ std::cout << " " << program_name
+ << " -f20 -s5 -d50 # Fast aquarium with lots of fish\n";
+ std::cout << " " << program_name
+ << " --fish=10 --no-colors # 10 fish, monochrome\n";
+ std::cout << " " << program_name
+ << " --debug --show-fps # Debug mode with FPS\n";
+}
+
+bool parseArguments(int argc, char *argv[]) {
+ // Define long options
+ static const struct option long_options[] = {
+ {"help", no_argument, nullptr, 'h'},
+ {"fish", required_argument, nullptr, 'f'},
+ {"seaweed", required_argument, nullptr, 's'},
+ {"max-bubbles", required_argument, nullptr, 'b'},
+ {"delay", required_argument, nullptr, 'd'},
+ {"bubble-rate", required_argument, nullptr, 'r'},
+ {"max-entities", required_argument, nullptr, 'm'},
+ {"no-big-entities", no_argument, nullptr, 'B'},
+ {"no-bubbles", no_argument, nullptr, 'N'},
+ {"no-colors", no_argument, nullptr, 'C'},
+ {"no-bold", no_argument, nullptr, 'O'},
+ {"show-fps", no_argument, nullptr, 'F'},
+ {"show-count", no_argument, nullptr, 'S'},
+ {"debug", no_argument, nullptr, 'D'},
+ {nullptr, 0, nullptr, 0}};
+
+ // Short options string - : after letter means it takes an argument
+ const char *short_options = "hf:s:b:d:r:m:BNCOFSD";
+
+ int option;
+ int option_index = 0;
+
+ // Reset getopt state (important for testing)
+ optind = 1;
+
+ while ((option = getopt_long(argc, argv, short_options, long_options,
+ &option_index)) != -1) {
+ switch (option) {
+ case 'h':
+ printUsage(argv[0]);
+ return false;
+
+ case 'f':
+ g_config.initial_fish_count = std::atoi(optarg);
+ if (g_config.initial_fish_count < -1) {
+ std::cerr << "Error: Fish count must be >= -1\n";
+ return false;
+ }
+ break;
+
+ case 's':
+ g_config.initial_seaweed_count = std::atoi(optarg);
+ if (g_config.initial_seaweed_count < -1) {
+ std::cerr << "Error: Seaweed count must be >= -1\n";
+ return false;
+ }
+ break;
+
+ case 'b':
+ g_config.max_bubbles = std::atoi(optarg);
+ if (g_config.max_bubbles < 0) {
+ std::cerr << "Error: Max bubbles must be >= 0\n";
+ return false;
+ }
+ break;
+
+ case 'd':
+ g_config.frame_delay_ms = std::atoi(optarg);
+ if (g_config.frame_delay_ms < 1) {
+ std::cerr << "Error: Frame delay must be >= 1ms\n";
+ return false;
+ }
+ break;
+
+ case 'r':
+ g_config.fish_bubble_rate = std::atoi(optarg);
+ if (g_config.fish_bubble_rate < 1) {
+ std::cerr << "Error: Bubble rate must be >= 1\n";
+ return false;
+ }
+ break;
+
+ case 'm':
+ g_config.max_entities = std::atoi(optarg);
+ if (g_config.max_entities < 1) {
+ std::cerr << "Error: Max entities must be >= 1\n";
+ return false;
+ }
+ break;
+
+ case 'B':
+ g_config.enable_big_entities = false;
+ break;
+
+ case 'N':
+ g_config.enable_bubbles = false;
+ break;
+
+ case 'C':
+ g_config.use_colors = false;
+ break;
+
+ case 'O':
+ g_config.use_bold = false;
+ break;
+
+ case 'F':
+ g_config.show_fps = true;
+ break;
+
+ case 'S':
+ g_config.show_entity_count = true;
+ break;
+
+ case 'D':
+ g_config.debug_mode = true;
+ break;
+
+ case '?':
+ // getopt_long already printed an error message
+ std::cerr << "Use --help for usage information\n";
+ return false;
+
+ default:
+ std::cerr << "Error: Unexpected option character\n";
+ return false;
+ }
+ }
+
+ // Check for non-option arguments
+ if (optind < argc) {
+ std::cerr << "Error: Unexpected argument: " << argv[optind] << "\n";
+ std::cerr << "Use --help for usage information\n";
+ return false;
+ }
+
+ return true;
+}
diff --git a/src/core/Config.h b/src/core/Config.h
@@ -0,0 +1,33 @@
+#pragma once
+#include <string>
+
+struct AquariumConfig {
+ // Entity spawn settings
+ int initial_fish_count = -1; // -1 means auto-calculate
+ int initial_seaweed_count = -1; // -1 means auto-calculate
+ int max_bubbles = 50;
+
+ // Animation settings
+ int frame_delay_ms = 100;
+ bool enable_big_entities = true;
+ bool enable_bubbles = true;
+
+ // Visual settings
+ bool use_colors = true;
+ bool use_bold = true;
+
+ // Spawn rates (lower = more frequent)
+ int fish_bubble_rate = 10; // 1 in N chance per frame
+ int big_entity_spawn_delay = 100; // frames between big entities
+
+ // Performance settings
+ bool optimize_rendering = true;
+ int max_entities = 200;
+
+ // Debug settings
+ bool show_fps = false;
+ bool show_entity_count = false;
+ bool debug_mode = false;
+};
+
+extern AquariumConfig g_config;
diff --git a/src/core/main.cpp b/src/core/main.cpp
@@ -0,0 +1,115 @@
+#include "Aquarium.h"
+#include "Config.h"
+#include <chrono>
+#include <iostream>
+#include <thread>
+
+#ifdef __OpenBSD__
+#include <unistd.h>
+#endif
+
+// Forward declaration
+bool parseArguments(int argc, char *argv[]);
+
+int main(int argc, char *argv[]) {
+ // Parse command line arguments
+ if (!parseArguments(argc, argv)) {
+ return 1; // Exit if parsing failed or help was shown
+ }
+
+ // Print configuration if debug mode is enabled
+ if (g_config.debug_mode) {
+ std::cout << "Configuration:\n";
+ std::cout << " Fish count: "
+ << (g_config.initial_fish_count == -1
+ ? "auto"
+ : std::to_string(g_config.initial_fish_count))
+ << "\n";
+ std::cout << " Seaweed count: "
+ << (g_config.initial_seaweed_count == -1
+ ? "auto"
+ : std::to_string(g_config.initial_seaweed_count))
+ << "\n";
+ std::cout << " Max bubbles: " << g_config.max_bubbles << "\n";
+ std::cout << " Frame delay: " << g_config.frame_delay_ms << "ms\n";
+ std::cout << " Big entities: "
+ << (g_config.enable_big_entities ? "enabled" : "disabled")
+ << "\n";
+ std::cout << " Bubbles: "
+ << (g_config.enable_bubbles ? "enabled" : "disabled") << "\n";
+ std::cout << " Colors: " << (g_config.use_colors ? "enabled" : "disabled")
+ << "\n";
+ std::cout << " Bold text: " << (g_config.use_bold ? "enabled" : "disabled")
+ << "\n";
+ std::cout << " Max entities: " << g_config.max_entities << "\n";
+ std::cout << "Press any key to continue...\n";
+ std::cin.get();
+ }
+
+ Aquarium &aquarium = Aquarium::getInstance();
+
+#ifdef __OpenBSD__
+ if (pledge("stdio tty", NULL) == -1) {
+ perror("pledge");
+ return 1;
+ }
+#endif
+
+ // Initialize the aquarium display
+ aquarium.resize(); // Setup initial entities
+
+ // Variables for FPS calculation
+ auto last_time = std::chrono::steady_clock::now();
+ int frame_count = 0;
+ double fps = 0.0;
+
+ // Main game loop
+ while (true) {
+ auto frame_start = std::chrono::steady_clock::now();
+
+ int input = aquarium.checkInput();
+ if (input == 'q' || input == 'Q' || input == 27) { // ESC key
+ break;
+ }
+
+ // Toggle debug info with 'd' key
+ if (input == 'd' || input == 'D') {
+ g_config.show_fps = !g_config.show_fps;
+ g_config.show_entity_count = !g_config.show_entity_count;
+ }
+
+ if (aquarium.checkResize()) {
+ aquarium.resize();
+ }
+
+ aquarium.redraw();
+
+ // Calculate FPS
+ if (g_config.show_fps) {
+ ++frame_count;
+ auto current_time = std::chrono::steady_clock::now();
+ auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
+ current_time - last_time)
+ .count();
+
+ if (elapsed >= 1000) { // Update FPS every second
+ fps = (frame_count * 1000.0) / elapsed;
+ frame_count = 0;
+ last_time = current_time;
+ }
+ }
+
+ // Sleep for configured frame delay
+ auto frame_end = std::chrono::steady_clock::now();
+ auto frame_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
+ frame_end - frame_start);
+ auto sleep_duration =
+ std::chrono::milliseconds(g_config.frame_delay_ms) - frame_duration;
+
+ if (sleep_duration > std::chrono::milliseconds(0)) {
+ std::this_thread::sleep_for(sleep_duration);
+ }
+ }
+
+ return 0;
+}
diff --git a/src/Bubble.cpp b/src/entities/Bubble.cpp
diff --git a/src/Bubble.h b/src/entities/Bubble.h
diff --git a/src/entities/Castle.cpp b/src/entities/Castle.cpp
@@ -0,0 +1,8 @@
+#include "Castle.h"
+#include "../assets/CastleAssets.h"
+#include "../core/Aquarium.h"
+
+Castle::Castle()
+ : Entity(Aquarium::getInstance().getWidth() - 32,
+ Aquarium::getInstance().getHeight() - 13),
+ image(getCastleAsset().image), mask(getCastleAsset().mask) {}
diff --git a/src/Castle.h b/src/entities/Castle.h
diff --git a/src/entities/Entity.cpp b/src/entities/Entity.cpp
@@ -0,0 +1,63 @@
+#include "Entity.h"
+#include "../core/Aquarium.h"
+
+void Entity::draw() const {
+ auto &aquarium = Aquarium::getInstance();
+
+ const auto &image = getImage();
+ const auto &mask = getMask();
+ const char default_color = getDefaultColor();
+
+ const int base_x = static_cast<int>(x);
+ const int base_y = static_cast<int>(y);
+
+ for (size_t i = 0; i < image.size(); ++i) {
+ const std::string &row = image[i];
+ const std::string &mask_row = (i < mask.size()) ? mask[i] : "";
+
+ // Build complete line at once instead of segments
+ std::string line;
+ std::string colors;
+ line.reserve(row.size());
+ colors.reserve(row.size());
+
+ int start_x = base_x;
+
+ for (size_t j = 0; j < row.size(); ++j) {
+ const char ch = row[j];
+
+ if (ch == '?') {
+ // Flush current line if not empty
+ if (!line.empty()) {
+ aquarium.drawToFrame(base_y + static_cast<int>(i), start_x, line,
+ colors);
+ start_x += static_cast<int>(line.size()) + 1; // +1 for the '?' skip
+ line.clear();
+ colors.clear();
+ } else {
+ ++start_x; // Just skip the '?' position
+ }
+ continue;
+ }
+
+ line.push_back(ch);
+
+ char color = default_color;
+ if (j < mask_row.size() && mask_row[j] != ' ') {
+ color = mask_row[j];
+ }
+ colors.push_back(color);
+ }
+
+ // Flush remaining line
+ if (!line.empty()) {
+ aquarium.drawToFrame(base_y + static_cast<int>(i), start_x, line, colors);
+ }
+ }
+}
+
+bool Entity::shouldBeRemoved() const noexcept {
+ const auto &aquarium = Aquarium::getInstance();
+ // unsigned nonsense
+ return x < -static_cast<int>(getWidth()) || x > aquarium.getWidth();
+}
diff --git a/src/Entity.h b/src/entities/Entity.h
diff --git a/src/entities/Fish.cpp b/src/entities/Fish.cpp
@@ -0,0 +1,63 @@
+#include "Fish.h"
+#include "../assets/FishAssets.h"
+#include "../core/Aquarium.h"
+#include "../utils/Random.h"
+#include "../utils/defs.h"
+
+std::unordered_map<char, char> Fish::color_map;
+
+Fish::Fish() : Fish(getRandomAssetIndex()) {}
+
+Fish::Fish(int asset_index)
+ : Entity(asset_index % 2 == 0),
+ image(getFishAssetPairs()[asset_index].image),
+ mask(getFishAssetPairs()[asset_index].mask),
+ speed(Random::floatInRange(0.25f, 2.25f)) {
+
+ const auto &aquarium = Aquarium::getInstance();
+ y = Random::intInRange(static_cast<int>(image.size()) + 6,
+ aquarium.getHeight() - static_cast<int>(image.size()));
+ x = moving_right ? -this->getWidth()
+ : static_cast<float>(aquarium.getWidth());
+ randomizeMask();
+}
+
+void Fish::randomizeMask() {
+ // Clear and rebuild color map with random colors each time
+ color_map.clear();
+ color_map['4'] = 'W'; // White is always '4' for eyes
+
+ // Assign random colors to digits 1-3, 5-9
+ for (char digit = '1'; digit <= '9'; ++digit) {
+ if (digit != '4') {
+ color_map[digit] = AVAILABLE_COLORS[Random::intInRange(
+ 0, static_cast<int>(AVAILABLE_COLORS.size()) - 1)];
+ }
+ }
+
+ // Apply color mapping to mask
+ for (auto &line : mask) {
+ for (char &ch : line) {
+ if (auto it = color_map.find(ch); it != color_map.end()) {
+ ch = it->second;
+ }
+ }
+ }
+}
+
+int Fish::getRandomAssetIndex() {
+ return Random::intInRange(0,
+ static_cast<int>(getFishAssetPairs().size()) - 1);
+}
+
+void Fish::update() noexcept { x += moving_right ? speed : -speed; }
+
+std::unique_ptr<Entity> Fish::createReplacement() const {
+ return std::make_unique<Fish>();
+}
+
+int Fish::getPreferredLayer() const noexcept { return 10; }
+
+bool Fish::shouldSpawnBubble() const {
+ return Random::floatInRange(0, 1) < BUBBLE_SPAWN_CHANCE;
+}
diff --git a/src/entities/Fish.h b/src/entities/Fish.h
@@ -0,0 +1,34 @@
+#pragma once
+#include "../assets/FishAssets.h"
+#include "Entity.h"
+#include <array>
+#include <unordered_map>
+
+class Fish : public Entity {
+private:
+ static constexpr std::array<char, 12> AVAILABLE_COLORS = {
+ 'c', 'C', 'r', 'R', 'y', 'Y', 'b', 'B', 'g', 'G', 'm', 'M'};
+
+ const std::vector<std::string> ℑ
+ std::vector<std::string> mask;
+ const float speed;
+
+ static std::unordered_map<char, char> color_map;
+
+ explicit Fish(int asset_index);
+ static int getRandomAssetIndex();
+ void randomizeMask();
+
+public:
+ Fish();
+
+ void update() noexcept override;
+ const std::vector<std::string> &getImage() const override { return image; }
+ const std::vector<std::string> &getMask() const override { return mask; }
+ char getDefaultColor() const noexcept override { return 'k'; }
+
+ std::unique_ptr<Entity> createReplacement() const override;
+ int getPreferredLayer() const noexcept override;
+
+ bool shouldSpawnBubble() const;
+};
diff --git a/src/entities/SeaMonster.cpp b/src/entities/SeaMonster.cpp
@@ -0,0 +1,44 @@
+#include "SeaMonster.h"
+#include "../assets/SeaMonsterAssets.h"
+#include "../core/Aquarium.h"
+#include "../utils/Random.h"
+
+SeaMonster::SeaMonster() : SeaMonster(getRandomDirection()) {}
+
+SeaMonster::SeaMonster(int asset_index)
+ : Entity(asset_index == 0),
+ frames(getSeaMonsterAssets()[asset_index].frames),
+ mask(getSeaMonsterAssets()[asset_index].mask), speed(SEAMONSTER_SPEED) {
+
+ const auto &aquarium = Aquarium::getInstance();
+ y = WATER_SURFACE_OFFSET;
+
+ // Use first frame for positioning calculations
+ const auto &first_frame = frames[0];
+ if (moving_right) {
+ x = -static_cast<float>(first_frame[0].length());
+ } else {
+ x = static_cast<float>(aquarium.getWidth());
+ }
+
+ current_image = first_frame;
+}
+
+int SeaMonster::getRandomDirection() { return Random::intInRange(0, 1); }
+
+void SeaMonster::update() noexcept {
+ x += moving_right ? speed : -speed;
+
+ ++animation_counter;
+ if (animation_counter >= ANIMATION_DELAY) {
+ current_frame_index = (current_frame_index + 1) % frames.size();
+ animation_counter = 0;
+ }
+}
+
+const std::vector<std::string> &SeaMonster::getImage() const {
+ current_image = frames[current_frame_index];
+ return current_image;
+}
+
+int SeaMonster::getPreferredLayer() const noexcept { return 8; }
diff --git a/src/entities/SeaMonster.h b/src/entities/SeaMonster.h
@@ -0,0 +1,32 @@
+#pragma once
+#include "../assets/SeaMonsterAssets.h"
+#include "Entity.h"
+
+class SeaMonster : public Entity {
+private:
+ static constexpr float SEAMONSTER_SPEED = 0.8f;
+ static constexpr int WATER_SURFACE_OFFSET = 2;
+
+ const std::vector<std::vector<std::string>> frames;
+ const std::vector<std::string> &mask;
+ const float speed;
+
+ int current_frame_index = 0;
+ int animation_counter = 0;
+ mutable std::vector<std::string> current_image;
+
+ static constexpr int ANIMATION_DELAY = 5;
+
+ explicit SeaMonster(int asset_index);
+ static int getRandomDirection();
+
+public:
+ SeaMonster();
+
+ void update() noexcept override;
+ const std::vector<std::string> &getImage() const override;
+ const std::vector<std::string> &getMask() const override { return mask; }
+ char getDefaultColor() const noexcept override { return 'G'; }
+
+ int getPreferredLayer() const noexcept override;
+};
diff --git a/src/entities/Seaweed.cpp b/src/entities/Seaweed.cpp
@@ -0,0 +1,63 @@
+#include "Seaweed.h"
+#include "../core/Aquarium.h"
+#include "../utils/Random.h"
+#include "../utils/defs.h"
+
+Seaweed::Seaweed()
+ : Entity(),
+ height(Random::intInRange(SEAWEED_MIN_HEIGHT, SEAWEED_MAX_HEIGHT)),
+ speed(Random::floatInRange(0.1f, 0.3f)),
+ lifetime(Random::intInRange(SEAWEED_MIN_LIFETIME, SEAWEED_MAX_LIFETIME)) {
+
+ x = Random::intInRange(0, Aquarium::getInstance().getWidth());
+ y = Aquarium::getInstance().getHeight() - height;
+
+ current_image.resize(height);
+ current_mask.resize(height);
+ generateCurrentFrame();
+}
+
+void Seaweed::update() noexcept {
+ frame += speed;
+ if (frame >= 1.0f) {
+ pattern_flipped = !pattern_flipped;
+ frame -= 1.0f;
+ frame_dirty = true; // mark frame as needing regeneration
+ }
+ --lifetime;
+}
+
+const std::vector<std::string> &Seaweed::getImage() const {
+ if (frame_dirty) {
+ generateCurrentFrame();
+ frame_dirty = false;
+ }
+ return current_image;
+}
+
+const std::vector<std::string> &Seaweed::getMask() const {
+ if (frame_dirty) {
+ generateCurrentFrame();
+ frame_dirty = false;
+ }
+ return current_mask;
+}
+
+void Seaweed::generateCurrentFrame() const {
+ for (size_t i = 0; i < height; ++i) {
+ const bool use_left = (i % 2 == 0) ^ pattern_flipped;
+ const char ch = use_left ? PATTERN_LEFT : PATTERN_RIGHT;
+ const int offset = use_left ? 0 : 1;
+
+ current_image[i] = std::string(offset, ' ') + ch;
+ current_mask[i] = std::string(current_image[i].size(), 'g');
+ }
+}
+
+bool Seaweed::shouldBeRemoved() const noexcept { return lifetime == 0; }
+
+std::unique_ptr<Entity> Seaweed::createReplacement() const {
+ return std::make_unique<Seaweed>();
+}
+
+int Seaweed::getPreferredLayer() const noexcept { return 1; }
diff --git a/src/Seaweed.h b/src/entities/Seaweed.h
diff --git a/src/entities/Ship.cpp b/src/entities/Ship.cpp
@@ -0,0 +1,25 @@
+#include "Ship.h"
+#include "../assets/ShipAssets.h"
+#include "../core/Aquarium.h"
+#include "../utils/Random.h"
+
+Ship::Ship() : Ship(getRandomDirection()) {}
+
+Ship::Ship(int asset_index)
+ : Entity(asset_index == 0), image(getShipAssets()[asset_index].image),
+ mask(getShipAssets()[asset_index].mask), speed(SHIP_SPEED) {
+
+ const auto &aquarium = Aquarium::getInstance();
+ y = 0;
+ if (moving_right) {
+ x = -static_cast<float>(image[0].length());
+ } else {
+ x = static_cast<float>(aquarium.getWidth());
+ }
+}
+
+int Ship::getRandomDirection() { return Random::intInRange(0, 1); }
+
+void Ship::update() noexcept { x += moving_right ? speed : -speed; }
+
+int Ship::getPreferredLayer() const noexcept { return 9; }
diff --git a/src/entities/Ship.h b/src/entities/Ship.h
@@ -0,0 +1,25 @@
+#pragma once
+#include "../assets/ShipAssets.h"
+#include "Entity.h"
+
+class Ship : public Entity {
+private:
+ static constexpr float SHIP_SPEED = 1.0f;
+
+ const std::vector<std::string> ℑ
+ const std::vector<std::string> &mask;
+ const float speed;
+
+ explicit Ship(int asset_index);
+ static int getRandomDirection();
+
+public:
+ Ship();
+
+ void update() noexcept override;
+ const std::vector<std::string> &getImage() const override { return image; }
+ const std::vector<std::string> &getMask() const override { return mask; }
+ char getDefaultColor() const noexcept override { return 'W'; }
+
+ int getPreferredLayer() const noexcept override;
+};
diff --git a/src/entities/Waterline.cpp b/src/entities/Waterline.cpp
@@ -0,0 +1,83 @@
+#include "Waterline.h"
+#include "../core/Aquarium.h"
+#include "../utils/Random.h"
+#include "../utils/defs.h"
+
+Waterline::Waterline() : Entity(0, WATERLINE_Y) {
+ shape[0] = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~";
+ shape[1] = "^^^^ ^^^ ^^^ ^^^ ^^^^ ";
+ shape[2] = "^^^^ ^^^^ ^^^ ^^ ";
+ shape[3] = "^^ ^^^^ ^^^ ^^^^^^ ";
+
+ const size_t width = Aquarium::getInstance().getWidth();
+
+ for (size_t i = 0; i < NUM_WAVE_LAYERS; ++i) {
+ const std::string &original = shape[i];
+ const size_t pattern_len = original.length();
+
+ // Calculate how many full patterns + remainder we need
+ const size_t full_patterns = width / pattern_len;
+ const size_t remainder = width % pattern_len;
+
+ shape[i].reserve(width);
+ for (size_t p = 0; p < full_patterns; ++p) {
+ shape[i] += original;
+ }
+ if (remainder > 0) {
+ shape[i] += original.substr(0, remainder);
+ }
+
+ // Create color line
+ colorLines[i].assign(shape[i].size(), WATERLINE_COLOR);
+ }
+
+ // Initialize cache vectors
+ cached_image.assign(shape.begin(), shape.end());
+ cached_mask.assign(colorLines.begin(), colorLines.end());
+}
+
+void Waterline::update() noexcept {
+ // Use cached probability calculations
+ static constexpr float thresholds[NUM_WAVE_LAYERS] = {
+ 0.0f, // Layer 0 never moves
+ 0.25f / WAVE_MOVE_CHANCE, 0.5f / WAVE_MOVE_CHANCE,
+ 0.75f / WAVE_MOVE_CHANCE};
+
+ for (size_t i = 1; i < NUM_WAVE_LAYERS; ++i) {
+ if (Random::floatInRange(0.0f, 1.0f) < thresholds[i]) {
+ if (Random::intInRange(0, 1) == 0) {
+ shiftStringLeft(shape[i]);
+ } else {
+ shiftStringRight(shape[i]);
+ }
+ }
+ }
+
+ // Update cached vectors
+ std::copy(shape.begin(), shape.end(), cached_image.begin());
+ std::copy(colorLines.begin(), colorLines.end(), cached_mask.begin());
+}
+
+void Waterline::shiftStringLeft(std::string &str) {
+ if (!str.empty()) {
+ char first = str.front();
+ str.erase(0, 1);
+ str.push_back(first);
+ }
+}
+
+void Waterline::shiftStringRight(std::string &str) {
+ if (!str.empty()) {
+ char last = str.back();
+ str.pop_back();
+ str.insert(0, 1, last);
+ }
+}
+
+const std::vector<std::string> &Waterline::getImage() const {
+ return cached_image;
+}
+
+const std::vector<std::string> &Waterline::getMask() const {
+ return cached_mask;
+}
diff --git a/src/Waterline.h b/src/entities/Waterline.h
diff --git a/src/entities/Whale.cpp b/src/entities/Whale.cpp
@@ -0,0 +1,48 @@
+#include "Whale.h"
+#include "../assets/WhaleAssets.h"
+#include "../core/Aquarium.h"
+#include "../utils/Random.h"
+
+Whale::Whale() : Whale(getRandomDirection()) {}
+
+Whale::Whale(int asset_index)
+ : Entity((asset_index == 0)), frames(getWhaleAssets()[asset_index].frames),
+ mask(getWhaleAssets()[asset_index].mask), speed(WHALE_SPEED) {
+
+ const auto &aquarium = Aquarium::getInstance();
+ y = 0;
+
+ // Use first frame for positioning calculations
+ const auto &first_frame = frames[0];
+ if (moving_right) {
+ x = -static_cast<float>(first_frame[6].length());
+ } else {
+ x = static_cast<float>(aquarium.getWidth());
+ }
+
+ current_image = first_frame;
+}
+
+int Whale::getRandomDirection() { return Random::intInRange(0, 1); }
+
+void Whale::update() noexcept {
+ x += moving_right ? speed : -speed;
+
+ ++animation_counter;
+
+ // Use longer delay for first frame (no water spout)
+ int current_delay =
+ (current_frame_index == 0) ? FIRST_FRAME_PAUSE : ANIMATION_DELAY;
+
+ if (animation_counter >= current_delay) {
+ current_frame_index = (current_frame_index + 1) % frames.size();
+ animation_counter = 0;
+ }
+}
+
+const std::vector<std::string> &Whale::getImage() const {
+ current_image = frames[current_frame_index];
+ return current_image;
+}
+
+int Whale::getPreferredLayer() const noexcept { return 8; }
diff --git a/src/entities/Whale.h b/src/entities/Whale.h
@@ -0,0 +1,31 @@
+#pragma once
+#include "../assets/WhaleAssets.h"
+#include "Entity.h"
+
+class Whale : public Entity {
+private:
+ static constexpr float WHALE_SPEED = 1.0f;
+
+ const std::vector<std::vector<std::string>> frames;
+ const std::vector<std::string> &mask;
+ const float speed;
+
+ int current_frame_index = 0;
+ int animation_counter = 0;
+ mutable std::vector<std::string> current_image;
+
+ static constexpr int ANIMATION_DELAY = 1;
+ static constexpr int FIRST_FRAME_PAUSE = 10;
+
+ explicit Whale(int asset_index);
+ static int getRandomDirection();
+
+public:
+ Whale();
+
+ void update() noexcept override;
+ const std::vector<std::string> &getImage() const override;
+ const std::vector<std::string> &getMask() const override { return mask; }
+ char getDefaultColor() const noexcept override { return 'B'; }
+ int getPreferredLayer() const noexcept override;
+};
diff --git a/src/main.cpp b/src/main.cpp
@@ -1,34 +0,0 @@
-#include "Aquarium.h"
-#ifdef __OpenBSD__
-#include <unistd.h>
-#endif
-
-int main() {
- Aquarium &aquarium = Aquarium::getInstance();
-
-#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) {
- int input = aquarium.checkInput();
- if (input == 'q' || input == 'Q' || input == 27) { // ESC key
- break;
- }
-
- if (aquarium.checkResize()) {
- aquarium.resize();
- }
-
- aquarium.redraw();
- }
-
- return 0;
-}
diff --git a/src/Random.h b/src/utils/Random.h
diff --git a/src/SpriteUtils.h b/src/utils/SpriteUtils.h
diff --git a/src/defs.h b/src/utils/defs.h