commit b751d07d40e06857f8c2932fbf61546ca76b26a2
Author: amrfti <andrew@kloet.net>
Date: Wed, 21 May 2025 11:29:48 -0400
initial commit
Diffstat:
19 files changed, 1093 insertions(+), 0 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -0,0 +1,2 @@
+build
+bin
diff --git a/Makefile b/Makefile
@@ -0,0 +1,43 @@
+# Compiler and flags
+CXX = g++
+CXXFLAGS = -std=c++17 -Wall -Wextra -O3
+LDFLAGS = -lncurses -ltinfo
+
+# Directories
+SRC_DIR = src
+OBJ_DIR = build
+BIN_DIR = bin
+
+# File extensions
+SRC_EXT = cpp
+OBJ_EXT = o
+
+# Find all source files and generate object files
+SOURCES = $(wildcard $(SRC_DIR)/*.$(SRC_EXT))
+OBJECTS = $(SOURCES:$(SRC_DIR)/%.$(SRC_EXT)=$(OBJ_DIR)/%.$(OBJ_EXT))
+
+# Output executable
+EXEC = $(BIN_DIR)/fissh
+
+# Default target: build everything
+all: $(EXEC)
+
+# Rule to link the object files into the final executable
+$(EXEC): $(OBJECTS)
+ $(CXX) $(OBJECTS) -o $(EXEC) $(LDFLAGS)
+
+# Rule to compile .cpp files into .o files
+$(OBJ_DIR)/%.o: $(SRC_DIR)/%.$(SRC_EXT)
+ @mkdir -p $(OBJ_DIR) # Make sure the obj dir exists
+ $(CXX) $(CXXFLAGS) -c $< -o $@
+
+# Clean up the build directory
+clean:
+ rm -rf $(OBJ_DIR) $(BIN_DIR)
+
+# Run the program
+run: $(EXEC)
+ ./$(EXEC)
+
+# Phony targets
+.PHONY: all clean run
diff --git a/src/Aquarium.cpp b/src/Aquarium.cpp
@@ -0,0 +1,263 @@
+#include "Aquarium.h"
+#include "Random.h"
+#include "defs.h"
+#include <algorithm>
+#include <iostream>
+#include <ncurses.h>
+
+int g_maxCells = 0;
+Aquarium::Aquarium() {
+ initscr();
+ noecho();
+ cbreak();
+ 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));
+}
+
+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'
+ }
+}
+
+short colorLookup[256];
+void Aquarium::initColorLookup() {
+ for (int i = 0; i < 256; ++i)
+ colorLookup[i] = 8; // default to 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::resize() {
+ clear();
+ getmaxyx(stdscr, height, width);
+
+ if (g_maxCells && height * width > g_maxCells) {
+ endwin(); // Cleanly shut down ncurses
+ 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));
+
+ fishes.clear();
+ bubbles.clear();
+ seaweeds.clear();
+
+ addWaterline();
+ addCastle();
+ for (int i = 0; i < width / 15; i++)
+ addSeaweed();
+ for (int i = 0; i < width * (height - 9) / 350; i++)
+ 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->getY() < 9)
+ 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();) { // use an iterator
+ auto &fish = *it;
+ fish->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 (fx > width || fx < -30) {
+ it = fishes.erase(it); // erase and update iterator
+ addFish();
+ } else {
+ ++it; // only increment if not erasing
+ }
+ }
+
+ waterline->draw();
+ waterline->update();
+
+ applyBackBuffer();
+}
+
+void Aquarium::addBubble(float x, float 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::clearBackBuffer() {
+ for (auto &row : backBuffer)
+ 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) {
+ if (y < 0 || y >= height)
+ return;
+
+ 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 = 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);
+ }
+
+ // Set back buffer to topmost layer (assume highest layer is last)
+ if (!cellLayers.empty()) {
+ backBuffer[y][cx] = cellLayers.back().second;
+ }
+ }
+}
+
+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::applyBackBuffer() {
+ 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];
+
+ if (newCell != oldCell) {
+ oldCell = newCell;
+
+ 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;
+ }
+
+ addch(newCell.ch);
+ } else {
+ // Still move the cursor to stay aligned
+ move(y, x + 1);
+ }
+ }
+ }
+
+ refresh();
+}
diff --git a/src/Aquarium.h b/src/Aquarium.h
@@ -0,0 +1,96 @@
+#pragma once
+#include "Bubble.h"
+#include "Castle.h"
+#include "Fish.h"
+#include "Seaweed.h"
+#include "Waterline.h"
+#include <memory>
+#include <vector>
+
+extern int g_maxCells;
+class Aquarium {
+private:
+ int width;
+ int height;
+
+ struct Cell {
+ char ch = ' ';
+ char colorChar = 'k'; // Default to black
+ 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::unique_ptr<Fish>> fishes;
+ std::vector<std::unique_ptr<Bubble>> bubbles;
+ std::vector<std::unique_ptr<Seaweed>> seaweeds;
+ std::unique_ptr<Waterline> waterline;
+ std::unique_ptr<Castle> castle;
+
+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 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();
+};
diff --git a/src/Bubble.cpp b/src/Bubble.cpp
@@ -0,0 +1,26 @@
+#include "Bubble.h"
+#include "Aquarium.h"
+#include <ncurses.h>
+
+Bubble::Bubble(float x, float y) : Entity() {
+ this->x = x;
+ this->y = y;
+}
+
+void Bubble::update() { y -= 1; }
+
+void Bubble::draw() {
+ lifetime++;
+
+ // Determine the frame based on lifetime
+ int frameNumber = lifetime / 9;
+ if (frameNumber > 2)
+ frameNumber = 2;
+
+ char frame = bubbleChars[frameNumber];
+
+ std::string line(1, frame);
+ std::string colorLine(1, 'c');
+
+ Aquarium::getInstance().drawToBackBuffer(y, x, 0, line, colorLine);
+}
diff --git a/src/Bubble.h b/src/Bubble.h
@@ -0,0 +1,14 @@
+#pragma once
+#include "Entity.h"
+
+class Bubble : public Entity {
+private:
+ static constexpr char bubbleChars[3] = {'.', 'o', 'O'};
+ int lifetime = 0;
+
+public:
+ Bubble(float x, float y);
+
+ void update();
+ void draw();
+};
diff --git a/src/Castle.cpp b/src/Castle.cpp
@@ -0,0 +1,52 @@
+#include "Castle.h"
+#include "Aquarium.h"
+
+const std::vector<std::string> Castle::image = {
+ R"( T~~ )", R"( | )",
+ R"( /^\ )", R"( / \ )",
+ R"( _ _ _ / \ _ _ _ )", R"([ ]_[ ]_[ ]/ _ _ \[ ]_[ ]_[ ])",
+ R"(|_=__-_ =_|_[ ]_[ ]_|_=-___-__|)", R"( | _- = | =_ = _ |= _= | )",
+ R"( |= -[] |- = _ = |_-=_[] | )", R"( | =_ |= - ___ | =_ = | )",
+ R"( |= []- |- /| |\ |=_ =[] | )", R"( |- =_ | =| | | | |- = - | )",
+ R"( |_______|__|_|_|_|__|_______| )"};
+
+const std::vector<std::string> Castle::mask = {
+ 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 )"};
+
+Castle::Castle() : Entity() {
+ x = Aquarium::getInstance().getWidth() - 32;
+ y = Aquarium::getInstance().getHeight() - 13;
+}
+
+void Castle::draw() {
+ for (size_t i = 0; i < image.size(); ++i) {
+ std::string currentLine;
+ std::string colorLine;
+
+ // Iterate over characters in the current line
+ for (size_t j = 0; j < image[i].size(); ++j) {
+ char ch = image[i][j];
+ if (ch == '?')
+ continue;
+
+ 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::getInstance().drawToBackBuffer(y + i, x, 0, currentLine,
+ colorLine);
+ }
+}
+
+void Castle::update() { return; }
diff --git a/src/Castle.h b/src/Castle.h
@@ -0,0 +1,14 @@
+#pragma once
+#include "Entity.h"
+
+class Castle : public Entity {
+private:
+ static const std::vector<std::string> image;
+ static const std::vector<std::string> mask;
+
+public:
+ Castle();
+
+ void draw();
+ void update();
+};
diff --git a/src/Entity.h b/src/Entity.h
@@ -0,0 +1,19 @@
+#pragma once
+#include <cctype>
+#include <ncurses.h>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+class Entity {
+protected:
+ float x;
+ float y;
+
+public:
+ Entity() : x(0), y(0) {}
+ virtual ~Entity() {}
+
+ inline float getX() const { return x; }
+ inline float getY() const { return y; }
+};
diff --git a/src/Fish.cpp b/src/Fish.cpp
@@ -0,0 +1,105 @@
+#include "Fish.h"
+#include "Aquarium.h"
+#include "FishAssets.h"
+#include "Random.h"
+#include <ncurses.h>
+
+Fish::Fish() : Fish(getRandomFishPair()) {}
+
+Fish::Fish(const FishAssetPair &pair)
+ : Entity(),
+ speed((pair.index % 2 == 0) ? Random::floatInRange(0.25, 2.25)
+ : -Random::floatInRange(0.25, 2.25)),
+ image(*pair.image), refMask(*pair.mask) {
+ y = Random::intInRange(image.size() + 6,
+ Aquarium::getInstance().getHeight() - image.size());
+
+ x = (speed < 0) ? Aquarium::getInstance().getWidth() : -20;
+
+ randomizeMask();
+}
+
+std::vector<std::pair<std::vector<std::string>, std::vector<std::string>>>
+ Fish::fishPairs;
+
+bool Fish::initialized = false;
+
+void Fish::initializeFishAssets() {
+ if (initialized)
+ return;
+ fishPairs = fishAssetPairs;
+ initialized = true;
+}
+
+void Fish::randomizeMask() {
+ // Create a mapping of digit to color
+ std::unordered_map<char, char> colorMap;
+ mask = refMask;
+
+ // For each digit 1-9, assign a random color
+ for (char digit = '1'; digit <= '9'; ++digit) {
+ colorMap[digit] =
+ availableColors[Random::intInRange(0, availableColors.size() - 1)];
+ }
+
+ // Special case: '4' always maps to 'W'
+ colorMap['4'] = 'W';
+
+ // Apply the color mapping to each character in the mask
+ for (auto &line : mask) {
+ for (auto &ch : line) {
+ if (ch >= '1' && ch <= '9') {
+ ch = colorMap[ch];
+ }
+ }
+ }
+}
+
+Fish::FishAssetPair Fish::getRandomFishPair() {
+ if (!initialized)
+ initializeFishAssets();
+
+ int index = Random::intInRange(0, fishPairs.size() - 1);
+
+ return FishAssetPair{index, &fishPairs[index].first,
+ &fishPairs[index].second};
+}
+
+void Fish::update() { x += speed; }
+
+void Fish::draw(int layer) {
+ Aquarium &aq = Aquarium::getInstance();
+
+ for (size_t i = 0; i < image.size(); ++i) {
+ const std::string &row = image[i];
+ const std::string &maskRow = (i < mask.size()) ? mask[i] : "";
+
+ int baseY = y + static_cast<int>(i);
+ int cursorX = static_cast<int>(x);
+ std::string currentSegment;
+ std::string currentColors;
+
+ for (size_t j = 0; j < row.size(); ++j) {
+ char ch = row[j];
+
+ if (ch == '?') {
+ if (!currentSegment.empty()) {
+ aq.drawToBackBuffer(baseY, cursorX, layer, currentSegment,
+ currentColors);
+ cursorX += currentSegment.size();
+ currentSegment.clear();
+ currentColors.clear();
+ }
+ cursorX += 1;
+ continue;
+ }
+
+ currentSegment.push_back(ch);
+ currentColors.push_back((j < maskRow.size()) ? maskRow[j] : 'k');
+ }
+
+ if (!currentSegment.empty()) {
+ aq.drawToBackBuffer(baseY, cursorX, layer, currentSegment, currentColors);
+ }
+ }
+}
diff --git a/src/Fish.h b/src/Fish.h
@@ -0,0 +1,37 @@
+#pragma once
+#include "Entity.h"
+#include <array>
+
+class Fish : public Entity {
+private:
+ struct FishAssetPair {
+ int index;
+ const std::vector<std::string> *image;
+ const std::vector<std::string> *mask;
+ };
+
+ Fish(const FishAssetPair &pair);
+
+ static std::vector<
+ std::pair<std::vector<std::string>, std::vector<std::string>>>
+ fishPairs;
+ static bool initialized;
+
+ const std::vector<std::string> ℑ
+ const std::vector<std::string> &refMask;
+ std::vector<std::string> mask;
+
+ static constexpr std::array<char, 12> availableColors = {
+ 'c', 'C', 'r', 'R', 'y', 'Y', 'b', 'B', 'g', 'G', 'm', 'M'};
+
+ const float speed;
+ static FishAssetPair getRandomFishPair();
+
+ static void initializeFishAssets();
+ void randomizeMask();
+
+public:
+ Fish();
+ void update();
+ void draw(int layer);
+};
diff --git a/src/FishAssets.h b/src/FishAssets.h
@@ -0,0 +1,238 @@
+#pragma once
+
+#include <string>
+#include <vector>
+
+using FishAssetPairRaw =
+ std::pair<std::vector<std::string>, std::vector<std::string>>;
+
+inline std::vector<FishAssetPairRaw> fishAssetPairs = {
+ {
+ {
+ R"(???\)",
+ R"(??/ \)",
+ R"(>=_('>)",
+ R"(??\_/)",
+ R"(???/)"
+ },
+ {
+ R"( 1)",
+ R"( 1 1)",
+ R"(663745)",
+ R"( 111)",
+ R"( 3)"
+ }
+ },
+ {
+ {
+ R"(??/)",
+ R"(?/ \)",
+ R"(<')_=<)",
+ R"(?\_/)",
+ R"(??\)"
+ },
+ {
+ R"( 2)",
+ R"( 111)",
+ R"(547366)",
+ R"( 111)",
+ R"( 3)"
+ }
+ },
+ {
+ {
+ R"(?????,)",
+ R"(?????}\)",
+ R"(\??.' `\)",
+ R"(}}< ( 6>)",
+ R"(/??`, .')",
+ R"(?????}/)",
+ R"(?????')"
+ },
+ {
+ R"( 2)",
+ R"( 22)",
+ R"(6 11 11)",
+ R"(661 7 45)",
+ R"(6 11 11)",
+ R"( 33)",
+ R"( 3)"
+ }
+ },
+ {
+ {
+ R"(????,)",
+ R"(???/{)",
+ R"(?/' `. /)",
+ R"(<6 ) >{{)",
+ R"(?`. ,' \)",
+ R"(???\{)",
+ R"(????`)"
+ },
+ {
+ R"( 2)",
+ R"( 22)",
+ R"( 11 11 6)",
+ R"(54 7 166)",
+ R"( 11 11 6)",
+ R"( 33)",
+ R"( 3)"
+ }
+ },
+ {
+ {
+ R"(???????,--,_???)",
+ R"(__????_\.---'-.)",
+ R"(\ '.-" // o\)",
+ R"(/_.'-._ \\ /)",
+ R"(???????`"--(/"`)"
+ },
+ {
+ R"( 22222)",
+ R"(66 121111211)",
+ R"(6 6111 77 41)",
+ R"(6661111 77 1)",
+ R"( 11113311)"
+ }
+ },
+ {
+ {
+ R"(????_,--,???????)",
+ R"(?.-'---./_????__)",
+ R"(/o \\ "-.' /)",
+ R"(\ // _.-'._\)",
+ R"(?`"\)--"`???????)"
+ },
+ {
+ R"( 22222 )",
+ R"( 112111121 66)",
+ R"(14 77 1116 6)",
+ R"(1 77 1111666)",
+ R"( 11331111 )"
+ }
+ },
+ {
+ {
+ R"(??__)",
+ R"(><_'>)",
+ R"(???')"
+ },
+ {
+ R"( 11)",
+ R"(61145)",
+ R"( 3)"
+ }
+ },
+ {
+ {
+ R"(?__)",
+ R"(<'_><)",
+ R"(?`)"
+ },
+ {
+ R"( 11)",
+ R"(54116)",
+ R"( 3)"
+ }
+ },
+ {
+ {
+ R"(????????_.-`\??????)",
+ R"(?????-:`_..,_\?????)",
+ R"(('-..:-` , '-.,?)",
+ R"(?} _ ;':( o :)",
+ R"((.-`/'-.,__'` _.-`?)",
+ R"(???`'-.,/??//`?????)"
+ },
+ {
+ R"( 22222 )",
+ R"( 222111112 )",
+ R"(66661111 7 1111 )",
+ R"( 6 1 7777 4 1)",
+ R"(6666211111177 1111 )",
+ R"( 222222 333 )"
+ }
+ },
+ {
+ {
+ R"(??????/`-._????????)",
+ R"(?????/_,.._`:-?????)",
+ R"(?,.-' , `-:..-'))",
+ R"(: o ):'; _ {?)",
+ R"(?`-._ `'__,.-'\`-.))",
+ R"(?????`\\??\,.-'`???)"
+ },
+ {
+ R"( 22222 )",
+ R"( 211111222 )",
+ R"( 1111 7 11116666)",
+ R"(1 4 7777 1 6 )",
+ R"( 1111 7711111126666)",
+ R"( 333 222222 )"
+ }
+ },
+ {
+ {
+ R"(????????/\??????)",
+ R"(????????\.\_????)",
+ R"(\'-,.:-` '-,?)",
+ R"( ) _ (>( o <)",
+ R"(/.-`?':._ _.-`?)",
+ R"(??????;/?``?????)",
+ },
+ {
+ R"( 22 )",
+ R"( 2121 )",
+ R"(66661111 111 )",
+ R"( 6 1 777 4 1)",
+ R"(6666 1111 1111 )",
+ R"( 22 33 )",
+ }
+ },
+ {
+ {
+ R"(??????/\????????)",
+ R"(????_/./????????)",
+ R"(?,-' `-:.,-'/)",
+ R"(> o )<) _ ( )",
+ R"(?`-._ _.:'?`-.\)",
+ R"(?????``?\;??????)",
+ },
+ {
+ R"( 22 )",
+ R"( 1212 )",
+ R"( 111 11116666)",
+ R"(1 4 777 1 6 )",
+ R"( 1111 1111 6666)",
+ R"( 33 22 )",
+ }
+ },
+ {
+ {
+ R"(_?????????_.*"\??????)",
+ R"(\'-._..-*` `'*-.??)",
+ R"(?) , (( o >)",
+ R"(/.`"*--.__)_.`_.-*`??)"
+ },
+ {
+ R"(6 11222 )",
+ R"(6661111111 11111 )",
+ R"( 6 3 77 4 1)",
+ R"(6661111111311311111 )",
+ }
+ },
+ {
+ {
+ R"(??????/"*._?????????_)",
+ R"(??.-*'` `*-.._.-'/)",
+ R"(< o )) , ( )",
+ R"(??`*-._`._(__.--*"`.\)",
+ },
+ {
+ R"( 22211 6)",
+ R"( 11111 1111111666)",
+ R"(1 4 77 3 6 )",
+ R"( 1111131131111111666)",
+ },
+ }
+};
diff --git a/src/Random.h b/src/Random.h
@@ -0,0 +1,19 @@
+#pragma once
+#include <random>
+class Random {
+public:
+ static std::mt19937 &engine() {
+ static std::mt19937 gen(std::random_device{}());
+ return gen;
+ }
+
+ static int intInRange(int min, int max) {
+ std::uniform_int_distribution<int> dist(min, max);
+ return dist(engine());
+ }
+
+ static float floatInRange(float min, float max) {
+ std::uniform_real_distribution<float> dist(min, max);
+ return dist(engine());
+ }
+};
diff --git a/src/Seaweed.cpp b/src/Seaweed.cpp
@@ -0,0 +1,41 @@
+#include "Seaweed.h"
+#include "Aquarium.h"
+#include "Random.h"
+#include "defs.h"
+#include <ncurses.h>
+
+Seaweed::Seaweed() : Entity() {
+ speed = Random::floatInRange(0.1f, 0.3f);
+ height = Random::intInRange(SEAWEED_MIN_HEIGHT, SEAWEED_MAX_HEIGHT);
+ x = Random::intInRange(0, Aquarium::getInstance().getWidth());
+ y = Aquarium::getInstance().getHeight() - 1;
+ lifetime = Random::intInRange(SEAWEED_MIN_LIFETIME, SEAWEED_MAX_LIFETIME);
+}
+
+void Seaweed::update() {
+ frame += speed;
+ if (frame >= 1.0f) {
+ std::swap(pattern[0], pattern[1]);
+ frame -= 1.0f;
+ }
+ --lifetime;
+}
+
+void Seaweed::draw() {
+ std::string line;
+ std::string colorLine;
+
+ for (int i = 0; i < height; ++i) {
+ line.clear();
+ char ch = (pattern[i % 2] == '(') ? '(' : ')';
+
+ // Adjust x and y based on the pattern
+ int drawX = (ch == '(') ? x : x + 1;
+ int drawY = y - i;
+
+ line.push_back(ch);
+ colorLine.push_back('g');
+
+ Aquarium::getInstance().drawToBackBuffer(drawY, drawX, 0, line, colorLine);
+ }
+}
diff --git a/src/Seaweed.h b/src/Seaweed.h
@@ -0,0 +1,18 @@
+#pragma once
+#include "Entity.h"
+
+class Seaweed : public Entity {
+private:
+ char pattern[2] = {'(', ')'};
+
+ float speed, frame = 0;
+ int lifetime;
+ int height;
+
+public:
+ Seaweed();
+
+ int getLifetime() { return lifetime; };
+ void update();
+ void draw();
+};
diff --git a/src/Waterline.cpp b/src/Waterline.cpp
@@ -0,0 +1,53 @@
+#include "Waterline.h"
+#include "Aquarium.h"
+#include "Random.h"
+#include "defs.h"
+#include <algorithm>
+#include <ncurses.h>
+
+Waterline::Waterline() {
+ std::vector<std::string> baseShape = {
+ "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~", "^^^^ ^^^ ^^^ ^^^ ^^^^ ",
+ "^^^^ ^^^^ ^^^ ^^ ", "^^ ^^^^ ^^^ ^^^^^^ "};
+
+ int seg_len = baseShape[0].size();
+ int repeat = Aquarium::getInstance().getWidth() / seg_len + 1;
+
+ for (auto &line : baseShape) {
+ std::string original = line;
+ while (line.size() < Aquarium::getInstance().getWidth()) {
+ line += original;
+ }
+ }
+ shape = std::move(baseShape);
+ y = 5;
+}
+
+void Waterline::draw() {
+ for (size_t i = 0; i < shape.size(); ++i) {
+ Aquarium::getInstance().drawToBackBuffer(i + y, x, 0, shape[i],
+ std::string(shape[i].size(), 'c'));
+ }
+}
+
+void Waterline::update() {
+ for (size_t i = 1; i < shape.size(); ++i) {
+ // Probability increases with line index (later lines = higher chance)
+ float chance = static_cast<float>(i) / shape.size();
+ if (Random::floatInRange(0.0f, 1.0f) < chance * (1.0f / WAVE_MOVE_CHANCE)) {
+ int direction = Random::intInRange(0, 1) == 0 ? -1 : 1;
+ shiftString(shape[i], direction);
+ }
+ }
+}
+
+void Waterline::shiftString(std::string &str, int direction) {
+ if (str.empty() || (direction != 1 && direction != -1))
+ return;
+
+ if (direction == 1) {
+ std::rotate(str.rbegin(), str.rbegin() + 1, str.rend());
+ } else {
+ std::rotate(str.begin(), str.begin() + 1, str.end());
+ }
+}
diff --git a/src/Waterline.h b/src/Waterline.h
@@ -0,0 +1,13 @@
+#pragma once
+#include "Entity.h"
+
+class Waterline : public Entity {
+private:
+ std::vector<std::string> shape;
+ void shiftString(std::string &, int direction);
+
+public:
+ Waterline();
+ void draw();
+ void update();
+};
diff --git a/src/defs.h b/src/defs.h
@@ -0,0 +1,8 @@
+#pragma once
+
+#define BUBBLE_SPAWN_CHANCE 0.02f
+#define SEAWEED_MIN_HEIGHT 3
+#define SEAWEED_MAX_HEIGHT 7
+#define SEAWEED_MIN_LIFETIME 5000
+#define SEAWEED_MAX_LIFETIME 15000
+#define WAVE_MOVE_CHANCE 15
diff --git a/src/main.cpp b/src/main.cpp
@@ -0,0 +1,32 @@
+#include "Aquarium.h"
+#include <cstdio>
+#include <cstdlib>
+#include <unistd.h>
+
+int main(int argc, char *argv[]) {
+ int opt;
+ while ((opt = getopt(argc, argv, "r:")) != -1) {
+ switch (opt) {
+ case 'r':
+ g_maxCells = std::atoi(optarg);
+ break;
+ default:
+ fprintf(stderr, "Usage: %s [-r max_cells]\n", argv[0]);
+ return 1;
+ }
+ }
+
+ Aquarium &aquarium = Aquarium::getInstance();
+ aquarium.resize();
+
+ while (true) {
+ aquarium.redraw();
+ int ch = getch();
+ if (ch == 'q')
+ break;
+ if (ch == 'r')
+ aquarium.resize();
+ }
+
+ return 0;
+}