commit 04133e71173affddbf4b53562da9795d77c9dcef
parent 633070b960cc8776c7b7521b4709fa0ae1d70103
Author: amrfti <andrew@kloet.net>
Date: Fri, 23 May 2025 14:59:24 -0400
REBASE AWESOME
Diffstat:
19 files changed, 466 insertions(+), 457 deletions(-)
diff --git 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
@@ -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 <algorithm>
#include <iostream>
-#include <memory>
-#include <ncurses.h>
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<Cell>(width));
- backBuffer.assign(height, std::vector<Cell>(width));
- layeredMap.assign(height, std::vector<LayeredCell>(width));
+
+ currentFrame.assign(height, std::vector<Cell>(width));
+ previousFrame.assign(height, std::vector<Cell>(width));
+
+ if (!colorLookupInitialized) {
+ initColorLookup();
+ colorLookupInitialized = true;
+ }
}
-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'
+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;
}
}
-short colorLookup[256];
-void Aquarium::initColorLookup() {
- for (int i = 0; i < 256; ++i)
- colorLookup[i] = 8; // default to black
+void Aquarium::redraw() {
+ clearCurrentFrame();
- colorLookup['r'] = 1;
- colorLookup['g'] = 2;
- colorLookup['y'] = 3;
- colorLookup['b'] = 4;
- colorLookup['m'] = 5;
- colorLookup['c'] = 6;
- colorLookup['w'] = 7;
- colorLookup['k'] = 8;
+ std::vector<std::unique_ptr<Entity>> 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<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) {
+ *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<Cell>(width));
- backBuffer.assign(height, std::vector<Cell>(width));
- layeredMap.assign(height, std::vector<LayeredCell>(width));
+ currentFrame.assign(height, std::vector<Cell>(width));
+ previousFrame.assign(height, std::vector<Cell>(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::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<const Entity *>(fish.get())
- ->draw(baseFishLayer +
- static_cast<int>(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<const Entity *>(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<Bubble>(x, y));
-}
-
-void Aquarium::addWaterline() { waterline = std::make_unique<Waterline>(); };
-
-void Aquarium::addSeaweed() {
- seaweeds.emplace_back(std::make_unique<Seaweed>());
-}
-
-void Aquarium::addCastle() { castle = std::make_unique<Castle>(); }
-
-void Aquarium::addFish() { fishes.emplace_back(std::make_unique<Fish>()); }
-
-void Aquarium::addShip() { ship = std::make_unique<Ship>(); }
+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::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<unsigned char>(colorChar));
-
- Cell cell{
- ch,
- static_cast<char>(fastToLower(static_cast<unsigned char>(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);
- }
+ const bool isBold = (colorChar >= 'A' && colorChar <= 'Z');
- // 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<char>(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<int>(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<unsigned char>(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<unsigned char>(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
@@ -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 <memory>
#include <ncurses.h>
#include <vector>
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<std::pair<int, Cell>> layers; // Sorted by layer (ascending)
- };
-
- std::vector<std::vector<Cell>> frontBuffer;
- std::vector<std::vector<Cell>> backBuffer;
- std::vector<std::vector<LayeredCell>> layeredMap;
-
- inline static const std::unordered_map<char, int> 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<unsigned char>(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<std::vector<Cell>> currentFrame;
+ std::vector<std::vector<Cell>> previousFrame;
+ std::vector<std::unique_ptr<Entity>> entities;
- std::vector<std::unique_ptr<Fish>> fishes;
- std::vector<std::unique_ptr<Bubble>> bubbles;
- std::vector<std::unique_ptr<Seaweed>> seaweeds;
- std::unique_ptr<Ship> ship;
- std::unique_ptr<Waterline> waterline;
- std::unique_ptr<Castle> 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 <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/Bubble.cpp b/src/Bubble.cpp
@@ -1,18 +1,26 @@
#include "Bubble.h"
-#include "Aquarium.h"
-#include <ncurses.h>
-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<Entity> Bubble::createReplacement() const { return nullptr; }
+
+int Bubble::getPreferredLayer() const noexcept { return 5; }
diff --git a/src/Bubble.h b/src/Bubble.h
@@ -1,21 +1,33 @@
#pragma once
-#include <cstddef>
+#include "Entity.h"
#include <string>
-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<std::string> current_image;
+ std::vector<std::string> 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<std::string> &getImage() const override {
+ return current_image;
+ }
+ const std::vector<std::string> &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<Entity> createReplacement() const override;
+ int getPreferredLayer() const noexcept override;
};
diff --git 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
@@ -1,14 +1,20 @@
#pragma once
-#include <string>
-#include <vector>
+#include "Entity.h"
-class Castle {
+class Castle : public Entity {
private:
- const size_t x, y;
const std::vector<std::string> image;
const std::vector<std::string> mask;
public:
Castle();
- void draw() const;
+
+ void update() noexcept override {} // Castle doesn't move
+ 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'; }
+
+ bool shouldBeRemoved() const noexcept override { return false; }
+ std::unique_ptr<Entity> createReplacement() const override { return nullptr; }
+ int getPreferredLayer() const noexcept override { return 0; }
};
diff --git 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<int>(x);
const int base_y = static_cast<int>(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<int>(i), cursor_x,
- layer, current_segment, current_colors);
- cursor_x += static_cast<int>(current_segment.size());
- current_segment.clear();
- current_colors.clear();
+ // 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
}
- ++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<int>(i), cursor_x, layer,
- current_segment, current_colors);
+ // Flush remaining line
+ if (!line.empty()) {
+ aquarium.drawToFrame(base_y + static_cast<int>(i), start_x, line, colors);
}
}
}
diff --git a/src/Entity.h b/src/Entity.h
@@ -1,4 +1,5 @@
#pragma once
+#include <memory>
#include <string>
#include <vector>
@@ -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<std::string> &getImage() const = 0;
virtual const std::vector<std::string> &getMask() const = 0;
virtual char getDefaultColor() const noexcept = 0;
- void draw(int layer) const;
+ virtual bool shouldBeRemoved() const noexcept = 0;
+ virtual std::unique_ptr<Entity> createReplacement() const = 0;
+ virtual int getPreferredLayer() const noexcept = 0;
+
+ void draw(int) const;
};
diff --git a/src/Fish.cpp b/src/Fish.cpp
@@ -2,9 +2,26 @@
#include "Aquarium.h"
#include "Random.h"
#include "assets/FishAssets.h"
-#include <ncurses.h>
+#include "defs.h"
-std::unordered_map<char, char> Fish::color_map;
+std::array<std::unordered_map<char, char>, 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<int>(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<int>(image.size()) + 6,
aquarium.getHeight() - static_cast<int>(image.size()));
x = moving_right ? -20.0f : static_cast<float>(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<int>(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<float>(aquarium.getWidth());
} else {
- // Fish is off screen when its right edge is past the left border
return (x + static_cast<float>(image[0].length())) < 0;
}
}
+
+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
@@ -14,18 +14,25 @@ private:
const float speed;
const bool moving_right;
- static std::unordered_map<char, char> color_map;
+ static std::array<std::unordered_map<char, char>, 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<std::string> &getImage() const override { return image; }
const std::vector<std::string> &getMask() const override { return mask; }
char getDefaultColor() const noexcept override { return 'k'; }
+
+ bool shouldBeRemoved() const noexcept override;
+ std::unique_ptr<Entity> createReplacement() const override;
+ int getPreferredLayer() const noexcept override;
+
+ bool shouldSpawnBubble() const;
};
diff --git a/src/Seaweed.cpp b/src/Seaweed.cpp
@@ -2,14 +2,19 @@
#include "Aquarium.h"
#include "Random.h"
#include "defs.h"
-#include <ncurses.h>
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<std::string> &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<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) {
- // 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<int>(x) + (use_left ? 0 : 1);
- const int drawY = y - static_cast<int>(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<Entity> Seaweed::createReplacement() const {
+ return std::make_unique<Seaweed>();
+}
+
+int Seaweed::getPreferredLayer() const noexcept { return 1; }
diff --git a/src/Seaweed.h b/src/Seaweed.h
@@ -1,9 +1,8 @@
#pragma once
-#include <cstddef>
+#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<std::string> current_image;
+ mutable std::vector<std::string> 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<std::string> &getImage() const override;
+ const std::vector<std::string> &getMask() const override;
+ char getDefaultColor() const noexcept override { return 'g'; }
+
+ bool shouldBeRemoved() const noexcept override;
+ std::unique_ptr<Entity> createReplacement() const override;
+ int getPreferredLayer() const noexcept override;
};
diff --git 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<float>(
- image[0].length()); // Start completely off left side
+ x = -static_cast<float>(image[0].length());
} else {
- x = static_cast<float>(aquarium.getWidth()); // Start off right side
+ x = static_cast<float>(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<float>(aquarium.getWidth());
} else {
- // Ship is off screen when its right edge is past the left border
return (x + static_cast<float>(image[0].length())) < 0;
}
}
+
+std::unique_ptr<Entity> Ship::createReplacement() const {
+ return std::make_unique<Ship>();
+}
+
+int Ship::getPreferredLayer() const noexcept {
+ return 9; // Ships on layer 9
+}
diff --git 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<std::string> &getImage() const override { return image; }
const std::vector<std::string> &getMask() const override { return mask; }
char getDefaultColor() const noexcept override { return 'W'; }
+
+ bool shouldBeRemoved() const noexcept override;
+ std::unique_ptr<Entity> createReplacement() const override;
+ int getPreferredLayer() const noexcept override;
};
diff --git a/src/Waterline.cpp b/src/Waterline.cpp
@@ -2,49 +2,82 @@
#include "Aquarium.h"
#include "Random.h"
#include "defs.h"
-#include <algorithm>
-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;
+ }
+ if (remainder > 0) {
+ shape[i] += original.substr(0, remainder);
}
- colorLines.emplace_back(line.size(), WATERLINE_COLOR);
+
+ // 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<int>(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};
+
+ 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::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<float>(i) / static_cast<float>(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);
- }
+void Waterline::shiftStringLeft(std::string &str) {
+ if (!str.empty()) {
+ char first = str.front();
+ str.erase(0, 1);
+ str.push_back(first);
}
}
-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::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/Waterline.h
@@ -1,21 +1,35 @@
#pragma once
-#include <string>
-#include <vector>
+#include "Entity.h"
+#include <array>
-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<std::string> shape;
- std::vector<std::string> colorLines;
+ // Use arrays instead of vectors for fixed-size data
+ std::array<std::string, NUM_WAVE_LAYERS> shape;
+ std::array<std::string, NUM_WAVE_LAYERS> 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<std::string> &getImage() const override;
+ const std::vector<std::string> &getMask() const override;
+ char getDefaultColor() const noexcept override { return WATERLINE_COLOR; }
+
+ bool shouldBeRemoved() const noexcept override { return false; }
+ std::unique_ptr<Entity> createReplacement() const override { return nullptr; }
+ int getPreferredLayer() const noexcept override { return 0; }
+
+private:
+ // Cache vectors to avoid allocation each frame
+ mutable std::vector<std::string> cached_image;
+ mutable std::vector<std::string> cached_mask;
};
diff --git a/src/assets/CastleAssets.h b/src/assets/CastleAssets.h
@@ -8,31 +8,15 @@ struct CastleAsset {
std::vector<std::string> 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
@@ -25,7 +25,7 @@ inline std::vector<AssetPair> pirateShipAssets = {
R"( /(___((___((___()",
R"( //(_____(____(____()",
R"(__///____|____|____|_____)",
- R"( \ /)",
+ R"(????\ /)",
},
{
R"( y y y)",