fissh

termios terminal aquarium. demo at ssh://fish@kloet.net
Download | Log | Files | Refs

commit 633070b960cc8776c7b7521b4709fa0ae1d70103
parent b0ea0d087b1218f73451036ddc1fa5177420f0c3
Author: amrfti <andrew@kloet.net>
Date:   Fri, 23 May 2025 11:48:30 -0400

add ship, improve inheritance

Diffstat:
Msrc/Aquarium.cpp | 22+++++++++++++++++-----
Msrc/Aquarium.h | 4++++
Asrc/Entity.cpp | 59+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/Entity.h | 11++++++++---
Msrc/Fish.cpp | 58++++++++--------------------------------------------------
Msrc/Fish.h | 12++++++++----
Msrc/Seaweed.h | 3+--
Asrc/Ship.cpp | 39+++++++++++++++++++++++++++++++++++++++
Asrc/Ship.h | 26++++++++++++++++++++++++++
Asrc/assets/ShipAssets.h | 37+++++++++++++++++++++++++++++++++++++
10 files changed, 207 insertions(+), 64 deletions(-)

diff --git a/src/Aquarium.cpp b/src/Aquarium.cpp @@ -3,6 +3,7 @@ #include "defs.h" #include <algorithm> #include <iostream> +#include <memory> #include <ncurses.h> int g_maxCells = 0; @@ -72,6 +73,7 @@ void Aquarium::resize() { addWaterline(); addCastle(); + addShip(); for (int i = 0; i < width / 15; i++) addSeaweed(); for (int i = 0; i < width * (height - 9) / 350; i++) @@ -112,10 +114,13 @@ void Aquarium::redraw() { } int baseFishLayer = 10; - for (auto it = fishes.begin(); it != fishes.end();) { // use an iterator + for (auto it = fishes.begin(); it != fishes.end();) { auto &fish = *it; - fish->draw(baseFishLayer + + + static_cast<const Entity *>(fish.get()) + ->draw(baseFishLayer + static_cast<int>(std::distance(fishes.begin(), it))); + fish->update(); float fx = fish->getX(); @@ -123,17 +128,22 @@ void Aquarium::redraw() { addBubble(fx, fish->getY()); } - if (fx > width || fx < -30) { - it = fishes.erase(it); // erase and update iterator + if (fish->isOffScreen()) { + it = fishes.erase(it); addFish(); } else { - ++it; // only increment if not erasing + ++it; } } waterline->draw(); waterline->update(); + static_cast<const Entity *>(ship.get())->draw(9); + if (ship->isOffScreen()) + addShip(); + ship->update(); + applyBackBuffer(); } @@ -151,6 +161,8 @@ 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::clearBackBuffer() { for (auto &row : backBuffer) std::fill(row.begin(), row.end(), Cell()); diff --git a/src/Aquarium.h b/src/Aquarium.h @@ -3,8 +3,10 @@ #include "Castle.h" #include "Fish.h" #include "Seaweed.h" +#include "Ship.h" #include "Waterline.h" #include <memory> +#include <ncurses.h> #include <vector> extern int g_maxCells; @@ -67,6 +69,7 @@ private: 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; @@ -84,6 +87,7 @@ public: void addSeaweed(); void addWaterline(); void addCastle(); + void addShip(); void redraw(); void initColors(); void initColorLookup(); diff --git a/src/Entity.cpp b/src/Entity.cpp @@ -0,0 +1,59 @@ +#include "Entity.h" +#include "Aquarium.h" + +void Entity::draw(int layer) const { + auto &aquarium = Aquarium::getInstance(); + + const auto &image = getImage(); + 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); + + for (size_t i = 0; i < image.size(); ++i) { + const std::string &row = image[i]; + const std::string &mask_row = (i < mask.size()) ? mask[i] : ""; + + int cursor_x = base_x; + current_segment.clear(); + current_colors.clear(); + + 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(); + } + ++cursor_x; // Skip transparent character + continue; + } + + current_segment.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]; + } + current_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); + } + } +} diff --git a/src/Entity.h b/src/Entity.h @@ -1,8 +1,5 @@ #pragma once -#include <cctype> -#include <ncurses.h> #include <string> -#include <unordered_map> #include <vector> struct AssetPair { @@ -21,4 +18,12 @@ public: float getX() const noexcept { return x; } float getY() const noexcept { return y; } + + 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; }; diff --git a/src/Fish.cpp b/src/Fish.cpp @@ -14,12 +14,9 @@ Fish::Fish(int asset_index) speed(Random::floatInRange(0.25f, 2.25f)), moving_right(asset_index % 2 == 0) { 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(); } @@ -27,7 +24,6 @@ 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') { @@ -35,7 +31,6 @@ void Fish::randomizeMask() { 0, static_cast<int>(AVAILABLE_COLORS.size()) - 1)]; } } - // Apply color mapping to mask for (auto &line : mask) { for (char &ch : line) { @@ -52,50 +47,13 @@ int Fish::getRandomAssetIndex() { void Fish::update() noexcept { x += moving_right ? speed : -speed; } -void Fish::draw(int layer) const { - auto &aquarium = Aquarium::getInstance(); - - // Pre-allocate strings to avoid repeated allocations - std::string current_segment; - std::string current_colors; - current_segment.reserve(32); // Reserve reasonable capacity - current_colors.reserve(32); - - const int base_x = static_cast<int>(x); - const int base_y = static_cast<int>(y); - - for (size_t i = 0; i < image.size(); ++i) { - const std::string &row = image[i]; - const std::string &mask_row = (i < mask.size()) ? mask[i] : ""; - - int cursor_x = base_x; - current_segment.clear(); - current_colors.clear(); - - 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(); - } - ++cursor_x; // Skip transparent character - continue; - } - - current_segment.push_back(ch); - current_colors.push_back((j < mask_row.size()) ? mask_row[j] : 'k'); - } - - // Flush remaining segment - if (!current_segment.empty()) { - aquarium.drawToBackBuffer(base_y + static_cast<int>(i), cursor_x, layer, - current_segment, current_colors); - } +bool Fish::isOffScreen() 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; } } diff --git a/src/Fish.h b/src/Fish.h @@ -2,6 +2,7 @@ #include "Entity.h" #include "assets/FishAssets.h" #include <array> +#include <unordered_map> class Fish : public Entity { private: @@ -9,11 +10,10 @@ private: 'c', 'C', 'r', 'R', 'y', 'Y', 'b', 'B', 'g', 'G', 'm', 'M'}; const std::vector<std::string> &image; - std::vector<std::string> mask; // Copy needed for color randomization + std::vector<std::string> mask; const float speed; const bool moving_right; - // Static color map to avoid recreation static std::unordered_map<char, char> color_map; explicit Fish(int asset_index); @@ -22,6 +22,10 @@ private: public: Fish(); - void update() noexcept; - void draw(int layer) const; + + 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'; } }; diff --git a/src/Seaweed.h b/src/Seaweed.h @@ -3,11 +3,10 @@ class Seaweed { private: - const int y; + const size_t x, y; static constexpr char PATTERN_LEFT = '('; static constexpr char PATTERN_RIGHT = ')'; - const size_t x; const size_t height; const float speed; diff --git a/src/Ship.cpp b/src/Ship.cpp @@ -0,0 +1,39 @@ +#include "Ship.h" +#include "Aquarium.h" +#include "Random.h" +#include "assets/ShipAssets.h" + +Ship::Ship() : Ship(getRandomDirection()) {} + +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 + } else { + x = static_cast<float>(aquarium.getWidth()); // Start off right side + } +} + +int Ship::getRandomDirection() { + return Random::intInRange(0, 1); // 0 = right, 1 = left +} + +void Ship::update() noexcept { x += moving_right ? speed : -speed; } + +bool Ship::isOffScreen() 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; + } +} diff --git a/src/Ship.h b/src/Ship.h @@ -0,0 +1,26 @@ +#pragma once +#include "Entity.h" +#include "assets/ShipAssets.h" + +class Ship : public Entity { +private: + static constexpr float SHIP_SPEED = 1.0f; + static constexpr int WATER_SURFACE_OFFSET = 0; + + const std::vector<std::string> &image; + const std::vector<std::string> &mask; + const float speed; + const bool moving_right; + + explicit Ship(int asset_index); + static int getRandomDirection(); + +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'; } +}; diff --git a/src/assets/ShipAssets.h b/src/assets/ShipAssets.h @@ -0,0 +1,37 @@ +#pragma once +#include "../Entity.h" +#include <vector> + +inline std::vector<AssetPair> pirateShipAssets = { + {{ + R"( | | |)", + R"( )_) )_) )_))", + R"( )___))___))___)\)", + R"( )____)____)_____)\\)", + R"(_____|____|____|____\\\__)", + R"(\ /)", + }, + { + R"( y y y)", + R"()", + R"( w)", + R"( ww)", + R"(yyyyyyyyyyyyyyyyyyyywwwyy)", + R"(y y)", + }}, + {{ + R"( | | |)", + R"( (_( (_( (_()", + R"( /(___((___((___()", + R"( //(_____(____(____()", + R"(__///____|____|____|_____)", + R"( \ /)", + }, + { + R"( y y y)", + R"()", + R"( w)", + R"( ww)", + R"(yywwwyyyyyyyyyyyyyyyyyyyy)", + R"( y y)", + }}};