From 614ada45fd4b01a4d63bfe621fd137af7184b1ba Mon Sep 17 00:00:00 2001 From: Andrew Kloet Date: Wed, 21 May 2025 11:29:48 -0400 Subject: [PATCH] initial commit --- .gitignore | 2 + Makefile | 43 ++++++++ src/Aquarium.cpp | 263 ++++++++++++++++++++++++++++++++++++++++++++++ src/Aquarium.h | 96 +++++++++++++++++ src/Bubble.cpp | 26 +++++ src/Bubble.h | 14 +++ src/Castle.cpp | 52 +++++++++ src/Castle.h | 14 +++ src/Entity.h | 19 ++++ src/Fish.cpp | 105 ++++++++++++++++++ src/Fish.h | 37 +++++++ src/FishAssets.h | 238 +++++++++++++++++++++++++++++++++++++++++ src/Random.h | 19 ++++ src/Seaweed.cpp | 41 ++++++++ src/Seaweed.h | 18 ++++ src/Waterline.cpp | 53 ++++++++++ src/Waterline.h | 13 +++ src/defs.h | 8 ++ src/main.cpp | 32 ++++++ 19 files changed, 1093 insertions(+) create mode 100644 .gitignore create mode 100644 Makefile create mode 100644 src/Aquarium.cpp create mode 100644 src/Aquarium.h create mode 100644 src/Bubble.cpp create mode 100644 src/Bubble.h create mode 100644 src/Castle.cpp create mode 100644 src/Castle.h create mode 100644 src/Entity.h create mode 100644 src/Fish.cpp create mode 100644 src/Fish.h create mode 100644 src/FishAssets.h create mode 100644 src/Random.h create mode 100644 src/Seaweed.cpp create mode 100644 src/Seaweed.h create mode 100644 src/Waterline.cpp create mode 100644 src/Waterline.h create mode 100644 src/defs.h create mode 100644 src/main.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9eca9b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +build +bin diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..dd10784 --- /dev/null +++ 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 new file mode 100644 index 0000000..1437efa --- /dev/null +++ b/src/Aquarium.cpp @@ -0,0 +1,263 @@ +#include "Aquarium.h" +#include "Random.h" +#include "defs.h" +#include +#include +#include + +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(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' + } +} + +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(width)); + backBuffer.assign(height, std::vector(width)); + layeredMap.assign(height, std::vector(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(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(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::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(j); + if (cx < 0 || cx >= width) + continue; + + const char ch = line[j]; + const char colorChar = colorLine[j]; + const bool isBold = fastIsUpper(static_cast(colorChar)); + + 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; + } + } +} + +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::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(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 new file mode 100644 index 0000000..72b6a43 --- /dev/null +++ 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 +#include + +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> layers; // Sorted by layer (ascending) + }; + + 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 waterline; + std::unique_ptr 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 new file mode 100644 index 0000000..3a6ca49 --- /dev/null +++ b/src/Bubble.cpp @@ -0,0 +1,26 @@ +#include "Bubble.h" +#include "Aquarium.h" +#include + +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 new file mode 100644 index 0000000..a9bfd59 --- /dev/null +++ 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 new file mode 100644 index 0000000..e442fe8 --- /dev/null +++ b/src/Castle.cpp @@ -0,0 +1,52 @@ +#include "Castle.h" +#include "Aquarium.h" + +const std::vector Castle::image = { + R"( T~~ )", R"( | )", + R"( /^\ )", R"( / \ )", + R"( _ _ _ / \ _ _ _ )", R"([ ]_[ ]_[ ]/ _ _ \[ ]_[ ]_[ ])", + R"(|_=__-_ =_|_[ ]_[ ]_|_=-___-__|)", R"( | _- = | =_ = _ |= _= | )", + R"( |= -[] |- = _ = |_-=_[] | )", R"( | =_ |= - ___ | =_ = | )", + R"( |= []- |- /| |\ |=_ =[] | )", R"( |- =_ | =| | | | |- = - | )", + R"( |_______|__|_|_|_|__|_______| )"}; + +const std::vector 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 new file mode 100644 index 0000000..bafe70a --- /dev/null +++ b/src/Castle.h @@ -0,0 +1,14 @@ +#pragma once +#include "Entity.h" + +class Castle : public Entity { +private: + static const std::vector image; + static const std::vector mask; + +public: + Castle(); + + void draw(); + void update(); +}; diff --git a/src/Entity.h b/src/Entity.h new file mode 100644 index 0000000..3e7ffd6 --- /dev/null +++ b/src/Entity.h @@ -0,0 +1,19 @@ +#pragma once +#include +#include +#include +#include +#include + +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 new file mode 100644 index 0000000..d6cbfb9 --- /dev/null +++ b/src/Fish.cpp @@ -0,0 +1,105 @@ +#include "Fish.h" +#include "Aquarium.h" +#include "FishAssets.h" +#include "Random.h" +#include + +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::vector>> + 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 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(i); + int cursorX = static_cast(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 new file mode 100644 index 0000000..95c626f --- /dev/null +++ b/src/Fish.h @@ -0,0 +1,37 @@ +#pragma once +#include "Entity.h" +#include + +class Fish : public Entity { +private: + struct FishAssetPair { + int index; + const std::vector *image; + const std::vector *mask; + }; + + Fish(const FishAssetPair &pair); + + static std::vector< + std::pair, std::vector>> + fishPairs; + static bool initialized; + + const std::vector ℑ + const std::vector &refMask; + std::vector mask; + + static constexpr std::array 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 new file mode 100644 index 0000000..f694b7f --- /dev/null +++ b/src/FishAssets.h @@ -0,0 +1,238 @@ +#pragma once + +#include +#include + +using FishAssetPairRaw = + std::pair, std::vector>; + +inline std::vector 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 new file mode 100644 index 0000000..c4e69c5 --- /dev/null +++ b/src/Random.h @@ -0,0 +1,19 @@ +#pragma once +#include +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 dist(min, max); + return dist(engine()); + } + + static float floatInRange(float min, float max) { + std::uniform_real_distribution dist(min, max); + return dist(engine()); + } +}; diff --git a/src/Seaweed.cpp b/src/Seaweed.cpp new file mode 100644 index 0000000..5a71115 --- /dev/null +++ b/src/Seaweed.cpp @@ -0,0 +1,41 @@ +#include "Seaweed.h" +#include "Aquarium.h" +#include "Random.h" +#include "defs.h" +#include + +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 new file mode 100644 index 0000000..5f62e00 --- /dev/null +++ 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 new file mode 100644 index 0000000..1dfa44a --- /dev/null +++ b/src/Waterline.cpp @@ -0,0 +1,53 @@ +#include "Waterline.h" +#include "Aquarium.h" +#include "Random.h" +#include "defs.h" +#include +#include + +Waterline::Waterline() { + std::vector 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(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 new file mode 100644 index 0000000..997651f --- /dev/null +++ b/src/Waterline.h @@ -0,0 +1,13 @@ +#pragma once +#include "Entity.h" + +class Waterline : public Entity { +private: + std::vector shape; + void shiftString(std::string &, int direction); + +public: + Waterline(); + void draw(); + void update(); +}; diff --git a/src/defs.h b/src/defs.h new file mode 100644 index 0000000..4a2f1bf --- /dev/null +++ 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 new file mode 100644 index 0000000..8e524d8 --- /dev/null +++ b/src/main.cpp @@ -0,0 +1,32 @@ +#include "Aquarium.h" +#include +#include +#include + +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; +}