diff --git a/Makefile b/Makefile index 75fdb17..78e859c 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ # Compiler and flags CXX = g++ -CXXFLAGS = -std=c++17 -Wall -Wextra -O3 --function-sections -fdata-sections -flto -march=native -ffast-math -LDFLAGS = -lncurses -ltinfo -Wl,--gc-sections -flto -Wl,-O2 +CXXFLAGS = -std=c++17 -Wall -Wextra -O3 +LDFLAGS = -lncurses -ltinfo # Directories SRC_DIR = src diff --git a/src/Aquarium.cpp b/src/Aquarium.cpp index 87010c1..c5fd080 100644 --- a/src/Aquarium.cpp +++ b/src/Aquarium.cpp @@ -1,12 +1,15 @@ #include "Aquarium.h" -#include "Random.h" -#include "defs.h" +#include "Bubble.h" +#include "Castle.h" +#include "Fish.h" +#include "Seaweed.h" +#include "Ship.h" +#include "Waterline.h" #include #include -#include -#include int g_maxCells = 0; + Aquarium::Aquarium() { initscr(); noecho(); @@ -14,41 +17,86 @@ Aquarium::Aquarium() { nodelay(stdscr, TRUE); curs_set(0); initColors(); - initColorLookup(); timeout(100); getmaxyx(stdscr, height, width); - frontBuffer.assign(height, std::vector(width)); - backBuffer.assign(height, std::vector(width)); - layeredMap.assign(height, std::vector(width)); -} -void Aquarium::initColors() { - if (has_colors()) { - start_color(); - init_pair(1, COLOR_RED, COLOR_BLACK); // 'r' - init_pair(2, COLOR_GREEN, COLOR_BLACK); // 'g' - init_pair(3, COLOR_YELLOW, COLOR_BLACK); // 'y' - init_pair(4, COLOR_BLUE, COLOR_BLACK); // 'b' - init_pair(5, COLOR_MAGENTA, COLOR_BLACK); // 'm' - init_pair(6, COLOR_CYAN, COLOR_BLACK); // 'c' - init_pair(7, COLOR_WHITE, COLOR_BLACK); // 'w' - init_pair(8, COLOR_BLACK, COLOR_BLACK); // 'k' + currentFrame.assign(height, std::vector(width)); + previousFrame.assign(height, std::vector(width)); + + if (!colorLookupInitialized) { + initColorLookup(); + colorLookupInitialized = true; } } -short colorLookup[256]; -void Aquarium::initColorLookup() { - for (int i = 0; i < 256; ++i) - colorLookup[i] = 8; // default to black +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; + } +} - colorLookup['r'] = 1; - colorLookup['g'] = 2; - colorLookup['y'] = 3; - colorLookup['b'] = 4; - colorLookup['m'] = 5; - colorLookup['c'] = 6; - colorLookup['w'] = 7; - colorLookup['k'] = 8; +void Aquarium::redraw() { + clearCurrentFrame(); + + std::vector> newEntities; + bool entities_modified = false; + + // Update and check for removal/replacement + for (auto it = entities.begin(); it != entities.end();) { + auto &entity = *it; + entity->update(); + + // Handle fish bubble spawning + if (auto *fish = dynamic_cast(entity.get())) { + if (fish->shouldSpawnBubble()) { + newEntities.emplace_back( + std::make_unique(fish->getX(), fish->getY())); + } + } + + if (entity->shouldBeRemoved()) { + auto replacement = entity->createReplacement(); + if (replacement) { + *it = std::move(replacement); + entities_modified = true; + ++it; + } else { + it = entities.erase(it); + entities_modified = true; + } + } else { + ++it; + } + } + + // Add new entities if any + if (!newEntities.empty()) { + for (auto &newEntity : newEntities) { + entities.emplace_back(std::move(newEntity)); + } + entities_modified = true; + } + + // Only sort if entities were modified + if (entities_modified) { + entities_need_sorting = true; + } + ensureEntitiesSorted(); + + // Draw all entities + for (const auto &entity : entities) { + entity->draw(0); + } + + renderToScreen(); } void Aquarium::resize() { @@ -56,20 +104,18 @@ void Aquarium::resize() { getmaxyx(stdscr, height, width); if (g_maxCells && height * width > g_maxCells) { - endwin(); // Cleanly shut down ncurses + endwin(); std::cerr << "Error: Terminal too large. Maximum allowed area is " << g_maxCells << " cells, but current size is " << (height * width) << ".\n"; std::exit(1); } - frontBuffer.assign(height, std::vector(width)); - backBuffer.assign(height, std::vector(width)); - layeredMap.assign(height, std::vector(width)); + currentFrame.assign(height, std::vector(width)); + previousFrame.assign(height, std::vector(width)); - fishes.clear(); - bubbles.clear(); - seaweeds.clear(); + entities.clear(); + entities_need_sorting = true; addWaterline(); addCastle(); @@ -80,101 +126,21 @@ void Aquarium::resize() { addFish(); } -Aquarium::~Aquarium() { endwin(); } +void Aquarium::addFish() { addEntityImpl(); } +void Aquarium::addBubble(float x, float y) { addEntityImpl(x, y); } +void Aquarium::addSeaweed() { addEntityImpl(); } +void Aquarium::addWaterline() { addEntityImpl(); } +void Aquarium::addCastle() { addEntityImpl(); } +void Aquarium::addShip() { addEntityImpl(); } -void Aquarium::redraw() { - clearBackBuffer(); - - for (auto &row : layeredMap) - for (auto &cell : row) - cell.layers.clear(); - - for (auto it = bubbles.begin(); it != bubbles.end();) { - auto &bubble = *it; - bubble->draw(); - bubble->update(); - if (bubble->isOutOfWater()) - it = bubbles.erase(it); - else - ++it; - } - - castle->draw(); - - for (auto it = seaweeds.begin(); it != seaweeds.end();) { - auto &seaweed = *it; - seaweed->draw(); - seaweed->update(); - if (seaweed->getLifetime() < 0) { - it = seaweeds.erase(it); - addSeaweed(); - } else { - ++it; - } - } - - int baseFishLayer = 10; - for (auto it = fishes.begin(); it != fishes.end();) { - auto &fish = *it; - - static_cast(fish.get()) - ->draw(baseFishLayer + - static_cast(std::distance(fishes.begin(), it))); - - fish->update(); - - float fx = fish->getX(); - if (Random::floatInRange(0, 1) < BUBBLE_SPAWN_CHANCE) { - addBubble(fx, fish->getY()); - } - - if (fish->isOffScreen()) { - it = fishes.erase(it); - addFish(); - } else { - ++it; - } - } - - waterline->draw(); - waterline->update(); - - static_cast(ship.get())->draw(9); - if (ship->isOffScreen()) - addShip(); - ship->update(); - - applyBackBuffer(); -} - -void Aquarium::addBubble(size_t x, size_t y) { - bubbles.emplace_back(std::make_unique(x, y)); -} - -void Aquarium::addWaterline() { waterline = std::make_unique(); }; - -void Aquarium::addSeaweed() { - seaweeds.emplace_back(std::make_unique()); -} - -void Aquarium::addCastle() { castle = std::make_unique(); } - -void Aquarium::addFish() { fishes.emplace_back(std::make_unique()); } - -void Aquarium::addShip() { ship = std::make_unique(); } - -void Aquarium::clearBackBuffer() { - for (auto &row : backBuffer) +void Aquarium::clearCurrentFrame() { + for (auto &row : currentFrame) { std::fill(row.begin(), row.end(), Cell()); + } } -inline char fastToLower(char c) { return (c >= 'A' && c <= 'Z') ? c + 32 : c; } - -inline bool fastIsUpper(char c) { return (c >= 'A' && c <= 'Z'); } - -void Aquarium::drawToBackBuffer(int y, int x, int layer, - const std::string &line, - const std::string &colorLine) { +void Aquarium::drawToFrame(int y, int x, const std::string &line, + const std::string &colorLine) { if (y < 0 || y >= height) return; @@ -187,89 +153,58 @@ void Aquarium::drawToBackBuffer(int y, int x, int layer, const char ch = line[j]; const char colorChar = colorLine[j]; - const bool isBold = fastIsUpper(static_cast(colorChar)); + const bool isBold = (colorChar >= 'A' && colorChar <= 'Z'); - Cell cell{ - ch, - static_cast(fastToLower(static_cast(colorChar))), - isBold}; - - auto &cellLayers = layeredMap[y][cx].layers; - - bool replaced = false; - for (auto &p : cellLayers) { - if (p.first == layer) { - p.second = cell; - replaced = true; - break; - } - } - - if (!replaced) { - cellLayers.emplace_back(layer, cell); - } - - // Set back buffer to topmost layer (assume highest layer is last) - if (!cellLayers.empty()) { - backBuffer[y][cx] = cellLayers.back().second; - } + currentFrame[y][cx] = { + ch, static_cast(isBold ? colorChar + 32 : colorChar), isBold}; } } -void Aquarium::removeFromBackBuffer(int y, int x, int layer, - const std::string &line) { - for (size_t j = 0; j < line.size(); ++j) { - int cx = x + static_cast(j); - if (y < 0 || y >= height || cx < 0 || cx >= width) - continue; - - auto &cellLayers = layeredMap[y][cx].layers; - - auto it = std::find_if(cellLayers.begin(), cellLayers.end(), - [layer](const auto &p) { return p.first == layer; }); - - if (it != cellLayers.end()) - cellLayers.erase(it); - - if (!cellLayers.empty()) - backBuffer[y][cx] = cellLayers.back().second; - else - backBuffer[y][cx] = Cell(); // Clear - } +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; } -void Aquarium::applyBackBuffer() { +void Aquarium::renderToScreen() { for (int y = 0; y < height; ++y) { - std::string rowStr; - int lastPair = -1; - bool lastBold = false; - - move(y, 0); - for (int x = 0; x < width; ++x) { - const Cell &newCell = backBuffer[y][x]; - Cell &oldCell = frontBuffer[y][x]; + const Cell &newCell = currentFrame[y][x]; + Cell &oldCell = previousFrame[y][x]; if (newCell != oldCell) { oldCell = newCell; + move(y, x); - int pairId = colorLookup[static_cast(newCell.colorChar)]; - bool bold = newCell.bold; - - // Change attr if needed - if (pairId != lastPair || bold != lastBold) { - attrset(COLOR_PAIR(pairId) | (bold ? A_BOLD : A_NORMAL)); - lastPair = pairId; - lastBold = bold; - } - + int colorPair = + colorLookup[static_cast(newCell.colorChar)]; + attrset(COLOR_PAIR(colorPair) | (newCell.bold ? A_BOLD : A_NORMAL)); addch(newCell.ch); - } else { - // Still move the cursor to stay aligned - move(y, x + 1); } } } - 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); + } +} + +Aquarium::~Aquarium() { endwin(); } diff --git a/src/Aquarium.h b/src/Aquarium.h index 414e564..033f510 100644 --- a/src/Aquarium.h +++ b/src/Aquarium.h @@ -1,15 +1,11 @@ #pragma once -#include "Bubble.h" -#include "Castle.h" -#include "Fish.h" -#include "Seaweed.h" -#include "Ship.h" -#include "Waterline.h" +#include "Entity.h" #include #include #include extern int g_maxCells; + class Aquarium { private: int width; @@ -17,84 +13,57 @@ private: struct Cell { char ch = ' '; - char colorChar = 'k'; // Default to black + 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); } }; - struct LayeredCell { - std::vector> layers; // Sorted by layer (ascending) - }; + std::vector> currentFrame; + std::vector> previousFrame; + std::vector> entities; - std::vector> frontBuffer; - std::vector> backBuffer; - std::vector> layeredMap; - - inline static const std::unordered_map colorCharToPair = { - {'r', 1}, // Red - {'g', 2}, // Green - {'y', 3}, // Yellow - {'b', 4}, // Blue - {'m', 5}, // Magenta - {'c', 6}, // Cyan - {'w', 7}, // White - {'k', 8} // Black - }; - - inline void applyColorAttr(char colorChar, bool enable) const { - bool bold = std::isupper(colorChar); - char lowerChar = std::tolower(static_cast(colorChar)); - - auto it = colorCharToPair.find(lowerChar); - if (it != colorCharToPair.end()) { - int colorPairId = it->second; - if (enable) { - attron(COLOR_PAIR(colorPairId)); - if (bold) - attron(A_BOLD); - } else { - attroff(COLOR_PAIR(colorPairId)); - if (bold) - attroff(A_BOLD); - } - } - } - - std::vector> fishes; - std::vector> bubbles; - std::vector> seaweeds; - std::unique_ptr ship; - std::unique_ptr waterline; - std::unique_ptr castle; + bool entities_need_sorting = true; + static inline short colorLookup[256] = {0}; + 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(size_t x, size_t y); + void addBubble(float x, float y); void addSeaweed(); void addWaterline(); void addCastle(); void addShip(); + void redraw(); void initColors(); - void initColorLookup(); void resize(); - void clearBackBuffer(); - void drawToBackBuffer(int y, int x, int layer, const std::string &line, - const std::string &colorLine); - void removeFromBackBuffer(int y, int x, int layer, const std::string &line); - void applyBackBuffer(); + void drawToFrame(int y, int x, const std::string &line, + const std::string &colorLine); + +private: + void clearCurrentFrame(); + void renderToScreen(); + void ensureEntitiesSorted(); + static void initColorLookup(); + + template void addEntityImpl(Args &&...args) { + entities.emplace_back(std::make_unique(std::forward(args)...)); + entities_need_sorting = true; + } }; diff --git a/src/Bubble.cpp b/src/Bubble.cpp index 01f98a4..3940a32 100644 --- a/src/Bubble.cpp +++ b/src/Bubble.cpp @@ -1,18 +1,26 @@ #include "Bubble.h" -#include "Aquarium.h" -#include -Bubble::Bubble(size_t x, size_t y) : x(x), y(y) {} +Bubble::Bubble(float x, float y) : Entity(x, y) { + current_image.resize(1); + current_mask.resize(1); + updateFrame(); +} -void Bubble::update() { +void Bubble::update() noexcept { --y; ++lifetime; + updateFrame(); } -void Bubble::draw() const { - static const std::string colorString(1, BUBBLE_COLOR); - // Clamp frame index +void Bubble::updateFrame() { int frameIndex = std::min(lifetime / FRAMES_PER_ANIMATION, MAX_FRAME_INDEX); - Aquarium::getInstance().drawToBackBuffer(y, x, 0, BUBBLE_FRAMES[frameIndex], - colorString); + current_image[0] = BUBBLE_FRAMES[frameIndex]; + current_mask[0] = BUBBLE_COLOR; } + +bool Bubble::shouldBeRemoved() const noexcept { return y < 10; } + +// Bubbles don't create replacements +std::unique_ptr Bubble::createReplacement() const { return nullptr; } + +int Bubble::getPreferredLayer() const noexcept { return 5; } diff --git a/src/Bubble.h b/src/Bubble.h index 761db1b..d5fbdb0 100644 --- a/src/Bubble.h +++ b/src/Bubble.h @@ -1,21 +1,33 @@ #pragma once -#include +#include "Entity.h" #include -class Bubble { +class Bubble : public Entity { private: static constexpr const char *BUBBLE_FRAMES[3] = {".", "o", "O"}; static constexpr int FRAMES_PER_ANIMATION = 9; static constexpr int MAX_FRAME_INDEX = 2; static constexpr char BUBBLE_COLOR = 'c'; - size_t x, y; + int lifetime = 0; + std::vector current_image; + std::vector current_mask; + + void updateFrame(); public: - Bubble(size_t x, size_t y); + Bubble(float x, float y); - bool isOutOfWater() const { return y < 5; } + void update() noexcept override; + const std::vector &getImage() const override { + return current_image; + } + const std::vector &getMask() const override { + return current_mask; + } + char getDefaultColor() const noexcept override { return BUBBLE_COLOR; } - void update(); - void draw() const; + bool shouldBeRemoved() const noexcept override; + std::unique_ptr createReplacement() const override; + int getPreferredLayer() const noexcept override; }; diff --git a/src/Castle.cpp b/src/Castle.cpp index dc92ff6..7be3320 100644 --- a/src/Castle.cpp +++ b/src/Castle.cpp @@ -3,32 +3,6 @@ #include "assets/CastleAssets.h" Castle::Castle() - : x(Aquarium::getInstance().getWidth() - 32), - y(Aquarium::getInstance().getHeight() - 13), image(castleAsset.image), - mask(castleAsset.mask) {} - -void Castle::draw() const { - auto &aquarium = Aquarium::getInstance(); - - for (size_t i = 0; i < image.size(); ++i) { - std::string currentLine; - std::string colorLine; - currentLine.reserve(image[i].size()); - colorLine.reserve(image[i].size()); - - // Iterate over characters in the current line - for (size_t j = 0; j < image[i].size(); ++j) { - char ch = image[i][j]; - - char colorChar = 'K'; // default to black - if (i < mask.size() && j < mask[i].size() && mask[i][j] != ' ') - colorChar = mask[i][j]; - - // Store the character and color - currentLine += ch; - colorLine += colorChar; - } - - aquarium.drawToBackBuffer(y + i, x, 0, currentLine, colorLine); - } -} + : Entity(Aquarium::getInstance().getWidth() - 32, + Aquarium::getInstance().getHeight() - 13), + image(castleAsset.image), mask(castleAsset.mask) {} diff --git a/src/Castle.h b/src/Castle.h index 762c5b3..6186ead 100644 --- a/src/Castle.h +++ b/src/Castle.h @@ -1,14 +1,20 @@ #pragma once -#include -#include +#include "Entity.h" -class Castle { +class Castle : public Entity { private: - const size_t x, y; const std::vector image; const std::vector mask; public: Castle(); - void draw() const; + + void update() noexcept override {} // Castle doesn't move + const std::vector &getImage() const override { return image; } + const std::vector &getMask() const override { return mask; } + char getDefaultColor() const noexcept override { return 'K'; } + + bool shouldBeRemoved() const noexcept override { return false; } + std::unique_ptr createReplacement() const override { return nullptr; } + int getPreferredLayer() const noexcept override { return 0; } }; diff --git a/src/Entity.cpp b/src/Entity.cpp index d7fcb10..3d35815 100644 --- a/src/Entity.cpp +++ b/src/Entity.cpp @@ -8,11 +8,6 @@ void Entity::draw(int layer) const { const auto &mask = getMask(); const char default_color = getDefaultColor(); - std::string current_segment; - std::string current_colors; - current_segment.reserve(32); - current_colors.reserve(32); - const int base_x = static_cast(x); const int base_y = static_cast(y); @@ -20,40 +15,43 @@ void Entity::draw(int layer) const { const std::string &row = image[i]; const std::string &mask_row = (i < mask.size()) ? mask[i] : ""; - int cursor_x = base_x; - current_segment.clear(); - current_colors.clear(); + // 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 segment if not empty - if (!current_segment.empty()) { - aquarium.drawToBackBuffer(base_y + static_cast(i), cursor_x, - layer, current_segment, current_colors); - cursor_x += static_cast(current_segment.size()); - current_segment.clear(); - current_colors.clear(); + // Flush current line if not empty + if (!line.empty()) { + aquarium.drawToFrame(base_y + static_cast(i), start_x, line, + colors); + start_x += static_cast(line.size()) + 1; // +1 for the '?' skip + line.clear(); + colors.clear(); + } else { + ++start_x; // Just skip the '?' position } - ++cursor_x; // Skip transparent character continue; } - current_segment.push_back(ch); + line.push_back(ch); - // Use mask color if available, otherwise use default color for spaces char color = default_color; - if (j < mask_row.size()) { - color = (mask_row[j] == ' ') ? default_color : mask_row[j]; + if (j < mask_row.size() && mask_row[j] != ' ') { + color = mask_row[j]; } - current_colors.push_back(color); + colors.push_back(color); } - // Flush remaining segment - if (!current_segment.empty()) { - aquarium.drawToBackBuffer(base_y + static_cast(i), cursor_x, layer, - current_segment, current_colors); + // Flush remaining line + if (!line.empty()) { + aquarium.drawToFrame(base_y + static_cast(i), start_x, line, colors); } } } diff --git a/src/Entity.h b/src/Entity.h index e47ef95..110b927 100644 --- a/src/Entity.h +++ b/src/Entity.h @@ -1,4 +1,5 @@ #pragma once +#include #include #include @@ -11,19 +12,27 @@ class Entity { protected: float x; float y; + static inline size_t next_id = 0; + const size_t entity_id; public: - Entity() : x(0.0f), y(0.0f) {} + Entity() : x(0.0f), y(0.0f), entity_id(++next_id) {} + Entity(float init_x, float init_y) + : x(init_x), y(init_y), entity_id(++next_id) {} virtual ~Entity() = default; float getX() const noexcept { return x; } float getY() const noexcept { return y; } + size_t getId() const noexcept { return entity_id; } virtual void update() noexcept = 0; - virtual bool isOffScreen() const noexcept = 0; virtual const std::vector &getImage() const = 0; virtual const std::vector &getMask() const = 0; virtual char getDefaultColor() const noexcept = 0; - void draw(int layer) const; + virtual bool shouldBeRemoved() const noexcept = 0; + virtual std::unique_ptr createReplacement() const = 0; + virtual int getPreferredLayer() const noexcept = 0; + + void draw(int) const; }; diff --git a/src/Fish.cpp b/src/Fish.cpp index a52d1f8..b85cce3 100644 --- a/src/Fish.cpp +++ b/src/Fish.cpp @@ -2,9 +2,26 @@ #include "Aquarium.h" #include "Random.h" #include "assets/FishAssets.h" -#include +#include "defs.h" -std::unordered_map Fish::color_map; +std::array, 20> Fish::color_map_cache; +bool Fish::color_maps_initialized = false; + +// Pre-generate 20 different color mappings at startup +void Fish::initializeColorMaps() { + for (auto &color_map : color_map_cache) { + color_map.clear(); + color_map['4'] = 'W'; // White is always '4' + + for (char digit = '1'; digit <= '9'; ++digit) { + if (digit != '4') { + color_map[digit] = AVAILABLE_COLORS[Random::intInRange( + 0, static_cast(AVAILABLE_COLORS.size()) - 1)]; + } + } + } + color_maps_initialized = true; +} Fish::Fish() : Fish(getRandomAssetIndex()) {} @@ -13,28 +30,25 @@ Fish::Fish(int asset_index) mask(fishAssetPairs[asset_index].mask), speed(Random::floatInRange(0.25f, 2.25f)), moving_right(asset_index % 2 == 0) { + + if (!color_maps_initialized) { + initializeColorMaps(); + } + const auto &aquarium = Aquarium::getInstance(); y = Random::intInRange(static_cast(image.size()) + 6, aquarium.getHeight() - static_cast(image.size())); x = moving_right ? -20.0f : static_cast(aquarium.getWidth()); - randomizeMask(); + applyRandomColorMapping(); } -void Fish::randomizeMask() { - // Clear and rebuild color map - color_map.clear(); - color_map['4'] = 'W'; // White is always '4' - // 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(AVAILABLE_COLORS.size()) - 1)]; - } - } - // Apply color mapping to mask +// Pick one of the pre-generated color mappings +void Fish::applyRandomColorMapping() { + const auto &selected_map = color_map_cache[Random::intInRange(0, 9)]; + for (auto &line : mask) { for (char &ch : line) { - if (auto it = color_map.find(ch); it != color_map.end()) { + if (auto it = selected_map.find(ch); it != selected_map.end()) { ch = it->second; } } @@ -47,13 +61,21 @@ int Fish::getRandomAssetIndex() { void Fish::update() noexcept { x += moving_right ? speed : -speed; } -bool Fish::isOffScreen() const noexcept { +bool Fish::shouldBeRemoved() const noexcept { const auto &aquarium = Aquarium::getInstance(); if (moving_right) { - // Fish is off screen when its left edge is past the right border return x > static_cast(aquarium.getWidth()); } else { - // Fish is off screen when its right edge is past the left border return (x + static_cast(image[0].length())) < 0; } } + +std::unique_ptr Fish::createReplacement() const { + return std::make_unique(); +} + +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 index 9f25ba1..e377f26 100644 --- a/src/Fish.h +++ b/src/Fish.h @@ -14,18 +14,25 @@ private: const float speed; const bool moving_right; - static std::unordered_map color_map; + static std::array, 20> color_map_cache; + static bool color_maps_initialized; + static void initializeColorMaps(); explicit Fish(int asset_index); static int getRandomAssetIndex(); - void randomizeMask(); + void applyRandomColorMapping(); public: Fish(); void update() noexcept override; - bool isOffScreen() const noexcept override; const std::vector &getImage() const override { return image; } const std::vector &getMask() const override { return mask; } char getDefaultColor() const noexcept override { return 'k'; } + + bool shouldBeRemoved() const noexcept override; + std::unique_ptr createReplacement() const override; + int getPreferredLayer() const noexcept override; + + bool shouldSpawnBubble() const; }; diff --git a/src/Seaweed.cpp b/src/Seaweed.cpp index f20a82a..0d1087f 100644 --- a/src/Seaweed.cpp +++ b/src/Seaweed.cpp @@ -2,14 +2,19 @@ #include "Aquarium.h" #include "Random.h" #include "defs.h" -#include Seaweed::Seaweed() - : x(Random::intInRange(0, Aquarium::getInstance().getWidth())), - y(Aquarium::getInstance().getHeight()), + : 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 { @@ -17,24 +22,42 @@ void Seaweed::update() noexcept { if (frame >= 1.0f) { pattern_flipped = !pattern_flipped; frame -= 1.0f; + frame_dirty = true; // mark frame as needing regeneration } --lifetime; } -void Seaweed::draw() const { - auto &aquarium = Aquarium::getInstance(); +const std::vector &Seaweed::getImage() const { + if (frame_dirty) { + generateCurrentFrame(); + frame_dirty = false; + } + return current_image; +} - std::string line(1, '\0'); - std::string colorLine(1, 'g'); +const std::vector &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) { - // Determine character and position for this segment const bool use_left = (i % 2 == 0) ^ pattern_flipped; const char ch = use_left ? PATTERN_LEFT : PATTERN_RIGHT; - const int drawX = static_cast(x) + (use_left ? 0 : 1); - const int drawY = y - static_cast(i); + const int offset = use_left ? 0 : 1; - line[0] = ch; - aquarium.drawToBackBuffer(drawY, drawX, 0, line, colorLine); + 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 Seaweed::createReplacement() const { + return std::make_unique(); +} + +int Seaweed::getPreferredLayer() const noexcept { return 1; } diff --git a/src/Seaweed.h b/src/Seaweed.h index 600d8e4..da9fd88 100644 --- a/src/Seaweed.h +++ b/src/Seaweed.h @@ -1,9 +1,8 @@ #pragma once -#include +#include "Entity.h" -class Seaweed { +class Seaweed : public Entity { private: - const size_t x, y; static constexpr char PATTERN_LEFT = '('; static constexpr char PATTERN_RIGHT = ')'; @@ -13,11 +12,22 @@ private: float frame = 0.0f; size_t lifetime; bool pattern_flipped = false; + mutable bool frame_dirty = true; + + mutable std::vector current_image; + mutable std::vector current_mask; + + void generateCurrentFrame() const; public: Seaweed(); - size_t getLifetime() const noexcept { return lifetime; } - void update() noexcept; - void draw() const; + void update() noexcept override; + const std::vector &getImage() const override; + const std::vector &getMask() const override; + char getDefaultColor() const noexcept override { return 'g'; } + + bool shouldBeRemoved() const noexcept override; + std::unique_ptr createReplacement() const override; + int getPreferredLayer() const noexcept override; }; diff --git a/src/Ship.cpp b/src/Ship.cpp index 82d7b75..50dae74 100644 --- a/src/Ship.cpp +++ b/src/Ship.cpp @@ -9,31 +9,33 @@ Ship::Ship(int asset_index) : Entity(), image(pirateShipAssets[asset_index].image), mask(pirateShipAssets[asset_index].mask), speed(SHIP_SPEED), moving_right(asset_index == 0) { + const auto &aquarium = Aquarium::getInstance(); - // Ships move along the water surface (top of aquarium) y = WATER_SURFACE_OFFSET; - // Start off-screen on appropriate side if (moving_right) { - x = -static_cast( - image[0].length()); // Start completely off left side + x = -static_cast(image[0].length()); } else { - x = static_cast(aquarium.getWidth()); // Start off right side + x = static_cast(aquarium.getWidth()); } } -int Ship::getRandomDirection() { - return Random::intInRange(0, 1); // 0 = right, 1 = left -} +int Ship::getRandomDirection() { return Random::intInRange(0, 1); } void Ship::update() noexcept { x += moving_right ? speed : -speed; } -bool Ship::isOffScreen() const noexcept { +bool Ship::shouldBeRemoved() const noexcept { const auto &aquarium = Aquarium::getInstance(); if (moving_right) { - // Ship is off screen when its left edge is past the right border return x > static_cast(aquarium.getWidth()); } else { - // Ship is off screen when its right edge is past the left border return (x + static_cast(image[0].length())) < 0; } } + +std::unique_ptr Ship::createReplacement() const { + return std::make_unique(); +} + +int Ship::getPreferredLayer() const noexcept { + return 9; // Ships on layer 9 +} diff --git a/src/Ship.h b/src/Ship.h index d719e16..7f0efe4 100644 --- a/src/Ship.h +++ b/src/Ship.h @@ -19,8 +19,11 @@ public: Ship(); void update() noexcept override; - bool isOffScreen() const noexcept override; const std::vector &getImage() const override { return image; } const std::vector &getMask() const override { return mask; } char getDefaultColor() const noexcept override { return 'W'; } + + bool shouldBeRemoved() const noexcept override; + std::unique_ptr createReplacement() const override; + int getPreferredLayer() const noexcept override; }; diff --git a/src/Waterline.cpp b/src/Waterline.cpp index 0fae247..03522ba 100644 --- a/src/Waterline.cpp +++ b/src/Waterline.cpp @@ -2,49 +2,82 @@ #include "Aquarium.h" #include "Random.h" #include "defs.h" -#include -Waterline::Waterline() : x(0), y(WATERLINE_Y) { - shape = { - "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", "^^^^ ^^^ ^^^ ^^^ ^^^^ ", - "^^^^ ^^^^ ^^^ ^^ ", "^^ ^^^^ ^^^ ^^^^^^ "}; +Waterline::Waterline() : Entity(0, WATERLINE_Y) { + shape[0] = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~"; + shape[1] = "^^^^ ^^^ ^^^ ^^^ ^^^^ "; + shape[2] = "^^^^ ^^^^ ^^^ ^^ "; + shape[3] = "^^ ^^^^ ^^^ ^^^^^^ "; const size_t width = Aquarium::getInstance().getWidth(); - for (auto &line : shape) { - const std::string original = line; - while (line.size() < width) { - line += original; + + 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; } - colorLines.emplace_back(line.size(), WATERLINE_COLOR); + 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::draw() const { - for (size_t i = 0; i < shape.size(); ++i) { - Aquarium::getInstance().drawToBackBuffer(y + static_cast(i), x, 0, - shape[i], colorLines[i]); - } -} +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}; -void Waterline::update() { - // Skip the first line (index 0) as it's static - for (size_t i = 1; i < shape.size(); ++i) { - // Probability increases with depth (higher index = more movement) - float movementChance = - static_cast(i) / static_cast(shape.size()); - float threshold = movementChance / WAVE_MOVE_CHANCE; - - if (Random::floatInRange(0.0f, 1.0f) < threshold) { - int direction = Random::intInRange(0, 1) == 0 ? -1 : 1; - shiftString(shape[i], direction); + 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::shiftString(std::string &str, int direction) { - if (direction > 0) { - std::rotate(str.rbegin(), str.rbegin() + 1, str.rend()); - } else { - std::rotate(str.begin(), str.begin() + 1, str.end()); +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 &Waterline::getImage() const { + return cached_image; +} + +const std::vector &Waterline::getMask() const { + return cached_mask; +} diff --git a/src/Waterline.h b/src/Waterline.h index fff1493..d0845c3 100644 --- a/src/Waterline.h +++ b/src/Waterline.h @@ -1,21 +1,35 @@ #pragma once -#include -#include +#include "Entity.h" +#include -class Waterline { +class Waterline : public Entity { private: static constexpr int WATERLINE_Y = 5; static constexpr char WATERLINE_COLOR = 'c'; + static constexpr size_t NUM_WAVE_LAYERS = 4; - size_t x, y; - std::vector shape; - std::vector colorLines; + // Use arrays instead of vectors for fixed-size data + std::array shape; + std::array colorLines; - void shiftString(std::string &str, int direction); - void initializeShape(); + // Pre-compute shift operations + void shiftStringLeft(std::string &str); + void shiftStringRight(std::string &str); public: Waterline(); - void draw() const; - void update(); + + void update() noexcept override; + const std::vector &getImage() const override; + const std::vector &getMask() const override; + char getDefaultColor() const noexcept override { return WATERLINE_COLOR; } + + bool shouldBeRemoved() const noexcept override { return false; } + std::unique_ptr createReplacement() const override { return nullptr; } + int getPreferredLayer() const noexcept override { return 0; } + +private: + // Cache vectors to avoid allocation each frame + mutable std::vector cached_image; + mutable std::vector cached_mask; }; diff --git a/src/assets/CastleAssets.h b/src/assets/CastleAssets.h index 494038a..581a977 100644 --- a/src/assets/CastleAssets.h +++ b/src/assets/CastleAssets.h @@ -8,31 +8,15 @@ struct CastleAsset { std::vector mask; }; -inline CastleAsset castleAsset = {{ - R"( T~~)", - R"( |)", - R"( /^\)", - R"( / \)", - R"( _ _ _ / \ _ _ _)", - R"([ ]_[ ]_[ ]/ _ _ \[ ]_[ ]_[ ])", - R"(|_=__-_ =_|_[ ]_[ ]_|_=-___-__|)", - R"( | _- = | =_ = _ |= _= |)", - R"( |= -[] |- = _ = |_-=_[] |)", - R"( | =_ |= - ___ | =_ = |)", - R"( |= []- |- /| |\ |=_ =[] |)", - R"( |- =_ | =| | | | |- = - |)", - R"( |_______|__|_|_|_|__|_______|)"}, - { - R"( RR)", - R"()", - R"( yyy)", - R"( y y)", - R"( y y)", - R"( y y)", - R"()", - R"()", - R"()", - R"( yyy)", - R"( yy yy)", - R"( y y y y)", - R"( yyyyyyy)"}}; +inline CastleAsset castleAsset = { + {R"( T~~)", R"( |)", R"( /^\)", + R"( / \)", R"( _ _ _ / \ _ _ _)", + R"([ ]_[ ]_[ ]/ _ _ \[ ]_[ ]_[ ])", R"(|_=__-_ =_|_[ ]_[ ]_|_=-___-__|)", + R"( | _- = | =_ = _ |= _= |)", R"( |= -[] |- = _ = |_-=_[] |)", + R"( | =_ |= - ___ | =_ = |)", R"( |= []- |- /| |\ |=_ =[] |)", + R"( |- =_ | =| | | | |- = - |)", R"( |_______|__|_|_|_|__|_______|)"}, + {R"( RR)", R"()", R"( yyy)", + R"( y y)", R"( y y)", + R"( y y)", R"()", R"()", R"()", R"( yyy)", + R"( yy yy)", R"( y y y y)", + R"( yyyyyyy)"}}; diff --git a/src/assets/ShipAssets.h b/src/assets/ShipAssets.h index 4d44d9a..8f17d05 100644 --- a/src/assets/ShipAssets.h +++ b/src/assets/ShipAssets.h @@ -25,7 +25,7 @@ inline std::vector pirateShipAssets = { R"( /(___((___((___()", R"( //(_____(____(____()", R"(__///____|____|____|_____)", - R"( \ /)", + R"(????\ /)", }, { R"( y y y)",