Compare commits

...

24 Commits

Author SHA1 Message Date
user
e15b5db7ff add config refactor structure 2025-07-12 17:33:17 -04:00
user
78d6209818 only move cursor when needed 2025-07-12 09:12:08 -04:00
75e147f511 add fish 2025-07-10 18:43:38 +03:00
user
2fe0538857 update makefile 2025-07-08 23:16:51 -04:00
user
f236af2709 change fallback behaviour 2025-07-08 20:35:55 -04:00
user
16d8858841 openbsd fix 2025-07-08 20:16:20 -04:00
user
8342e60547 cleanup 2025-07-08 19:59:31 -04:00
user
b36242c78d enable special key signal generation 2025-07-08 19:57:01 -04:00
4f44c2fc34 Merge pull request 'termios' (#4) from termios into master
Reviewed-on: #4
2025-07-09 02:50:41 +03:00
user
36e6c3786f cleanup 2025-07-08 19:47:02 -04:00
user
2f44398664 cleanup 2025-07-08 17:30:28 -04:00
user
d93d351d38 black background 2025-07-08 17:25:27 -04:00
b4f1826464 replace ncurses with termios 2025-07-08 15:36:21 -04:00
8302503333 cleanup 2025-07-08 13:25:56 -04:00
5bae9a07af add input delay 2025-07-08 13:18:16 -04:00
348b687bd9 fix whale sprite2 2025-07-08 11:28:08 -04:00
1670bccc4d fix whale sprite 2025-07-08 11:24:16 -04:00
cda06a5169 fix segfault for real 2025-07-08 11:08:38 -04:00
9c631d3d9a undo 2025-07-08 10:40:56 -04:00
2eead14b8e segfault test fix 2025-07-08 10:40:26 -04:00
d350044cb2 fix segfault 2025-07-08 09:30:11 -04:00
user
c54089ae19 finish mirroring. abstract entity removal 2025-07-07 17:25:29 -04:00
5706c90f87 store only one direction of sprites 2025-07-07 14:57:52 -04:00
f4ec53cbbe fix whale delete 2025-07-07 14:18:30 -04:00
36 changed files with 1289 additions and 816 deletions

View File

@@ -1,44 +1,36 @@
# Compiler and flags
CXX = g++
CXXFLAGS = -std=c++17 -Wall -Wextra -O3 -fno-exceptions -fno-asynchronous-unwind-tables -fno-unwind-tables
LDFLAGS = -lncurses -ltinfo -Wl,--gc-sections -s
# Directories
CXX = c++
CXXFLAGS = -Wall -Wextra -O3 -Isrc
LDFLAGS = -static
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))
# Find all source files recursively in subdirectories
SOURCES = $(shell find $(SRC_DIR) -name '*.$(SRC_EXT)')
# Create corresponding object files maintaining directory structure
OBJECTS = $(SOURCES:$(SRC_DIR)/%.$(SRC_EXT)=$(OBJ_DIR)/%.$(OBJ_EXT))
# Output executable
EXEC = $(BIN_DIR)/fissh
# Default target: build everything
# Get all unique subdirectories for build structure
OBJ_DIRS = $(sort $(dir $(OBJECTS)))
all: $(EXEC)
# Rule to link the object files into the final executable
$(EXEC): $(OBJECTS)
@mkdir -p $(BIN_DIR) # Make sure the bin dir exists
@mkdir -p $(BIN_DIR)
$(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
# Create object files, maintaining directory structure
$(OBJ_DIR)/%.$(OBJ_EXT): $(SRC_DIR)/%.$(SRC_EXT)
@mkdir -p $(dir $@)
$(CXX) $(CXXFLAGS) -c $< -o $@
# Clean up the build directory
clean:
rm -rf $(OBJ_DIR) $(BIN_DIR)
# Run the program
run: $(EXEC)
./$(EXEC)
install: $(EXEC)
install -m 755 $(EXEC) /usr/local/bin/
# Phony targets
.PHONY: all clean run
.PHONY: all clean install

View File

@@ -1,237 +0,0 @@
#include "Aquarium.h"
#include "Bubble.h"
#include "Castle.h"
#include "Fish.h"
#include "SeaMonster.h"
#include "Seaweed.h"
#include "Ship.h"
#include "Waterline.h"
#include "Whale.h"
#include <algorithm>
#include <iostream>
int g_maxCells = 0;
Aquarium::Aquarium() {
initscr();
noecho();
cbreak();
nodelay(stdscr, TRUE);
curs_set(0);
initColors();
timeout(100);
getmaxyx(stdscr, height, width);
currentFrame.assign(height, std::vector<Cell>(width));
previousFrame.assign(height, std::vector<Cell>(width));
if (!colorLookupInitialized) {
initColorLookup();
colorLookupInitialized = true;
}
}
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;
}
}
void Aquarium::redraw() {
clearCurrentFrame();
ensureBigEntityExists();
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();
}
renderToScreen();
}
void Aquarium::resize() {
clear();
getmaxyx(stdscr, height, width);
if (g_maxCells && height * width > g_maxCells) {
endwin();
std::cerr << "Error: Terminal too large. Maximum allowed area is "
<< g_maxCells << " cells, but current size is "
<< (height * width) << ".\n";
std::exit(1);
}
currentFrame.assign(height, std::vector<Cell>(width));
previousFrame.assign(height, std::vector<Cell>(width));
entities.clear();
entities_need_sorting = true;
addWaterline();
addCastle();
for (int i = 0; i < width / 15; i++)
addSeaweed();
for (int i = 0; i < width * (height - 9) / 350; i++)
addFish();
}
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::addSeaMonster() { addEntityImpl<SeaMonster>(); }
void Aquarium::addWhale() { addEntityImpl<Whale>(); }
void Aquarium::ensureBigEntityExists() {
// Check if any big entities exist on screen
for (const auto &entity : entities) {
if (dynamic_cast<Ship *>(entity.get()) ||
dynamic_cast<SeaMonster *>(entity.get()) ||
dynamic_cast<Whale *>(entity.get())) {
return; // Big entity found, do nothing
}
}
// No big entity found, spawn next in cycle (Ship, SeaMonster, Whale)
int entity_type = big_entity_index % 3;
if (entity_type == 0) {
addEntityImpl<Ship>();
} else if (entity_type == 1) {
addEntityImpl<SeaMonster>();
} else {
addEntityImpl<Whale>();
}
++big_entity_index;
}
void Aquarium::clearCurrentFrame() {
for (auto &row : currentFrame) {
std::fill(row.begin(), row.end(), Cell());
}
}
void Aquarium::drawToFrame(int y, int x, 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 = (colorChar >= 'A' && colorChar <= 'Z');
currentFrame[y][cx] = {
ch, static_cast<char>(isBold ? colorChar + 32 : colorChar), isBold};
}
}
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::renderToScreen() {
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
const Cell &newCell = currentFrame[y][x];
Cell &oldCell = previousFrame[y][x];
if (newCell != oldCell) {
oldCell = newCell;
move(y, x);
int colorPair =
colorLookup[static_cast<unsigned char>(newCell.colorChar)];
attrset(COLOR_PAIR(colorPair) | (newCell.bold ? A_BOLD : A_NORMAL));
addch(newCell.ch);
}
}
}
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(); }

View File

@@ -1,56 +0,0 @@
#include "SeaMonster.h"
#include "Aquarium.h"
#include "Random.h"
#include "assets/SeaMonsterAssets.h"
SeaMonster::SeaMonster() : SeaMonster(getRandomDirection()) {}
SeaMonster::SeaMonster(int asset_index)
: Entity(), frame1(seaMonsterAssets[asset_index].frame1),
frame2(seaMonsterAssets[asset_index].frame2),
mask(seaMonsterAssets[asset_index].mask), speed(SEAMONSTER_SPEED),
moving_right(asset_index == 0) {
const auto &aquarium = Aquarium::getInstance();
y = WATER_SURFACE_OFFSET;
if (moving_right) {
x = -static_cast<float>(frame1[0].length());
} else {
x = static_cast<float>(aquarium.getWidth());
}
current_image = frame1;
}
int SeaMonster::getRandomDirection() { return Random::intInRange(0, 1); }
void SeaMonster::update() noexcept {
x += moving_right ? speed : -speed;
++animation_counter;
if (animation_counter >= ANIMATION_DELAY) {
current_frame = !current_frame;
animation_counter = 0;
}
}
const std::vector<std::string> &SeaMonster::getImage() const {
if (current_frame) {
current_image = frame2;
} else {
current_image = frame1;
}
return current_image;
}
bool SeaMonster::shouldBeRemoved() const noexcept {
const auto &aquarium = Aquarium::getInstance();
if (moving_right) {
return x > static_cast<float>(aquarium.getWidth());
} else {
return (x + static_cast<float>(frame1[0].length())) < 0;
}
}
int SeaMonster::getPreferredLayer() const noexcept { return 8; }

View File

@@ -1,35 +0,0 @@
#include "Ship.h"
#include "Aquarium.h"
#include "Random.h"
#include "assets/ShipAssets.h"
Ship::Ship() : Ship(getRandomDirection()) {}
Ship::Ship(int asset_index)
: Entity(), image(shipAssets[asset_index].image),
mask(shipAssets[asset_index].mask), speed(SHIP_SPEED),
moving_right(asset_index == 0) {
const auto &aquarium = Aquarium::getInstance();
y = WATER_SURFACE_OFFSET;
if (moving_right) {
x = -static_cast<float>(image[0].length());
} else {
x = static_cast<float>(aquarium.getWidth());
}
}
int Ship::getRandomDirection() { return Random::intInRange(0, 1); }
void Ship::update() noexcept { x += moving_right ? speed : -speed; }
bool Ship::shouldBeRemoved() const noexcept {
const auto &aquarium = Aquarium::getInstance();
if (moving_right) {
return x > static_cast<float>(aquarium.getWidth());
} else {
return (x + static_cast<float>(image[0].length())) < 0;
}
}
int Ship::getPreferredLayer() const noexcept { return 9; }

View File

@@ -8,15 +8,36 @@ 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 const CastleAsset& getCastleAsset() {
static const 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)"}
};
return castleAsset;
}

View File

@@ -1,51 +1,44 @@
#pragma once
#include "../Entity.h"
#include "../entities/Entity.h"
#include "../utils/SpriteUtils.h"
#include <vector>
inline std::vector<AssetPair> fishAssetPairs = {
/*
Mask Definitions:
1: body
2: dorsal fin
3: flippers
4: eye
5: mouth
6: tailfin
7: gills
*/
inline const std::vector<AssetPair>& getFishAssets() {
static const std::vector<AssetPair> fishAssets = {
{
{
R"(???\)",
R"(??/ \)",
R"(???\??)",
R"(??/ \?)",
R"(>=_('>)",
R"(??\_/)",
R"(???/)"
},
R"(??\_/?)",
R"(???/??)"},
{
R"( 1 )",
R"( 1 1 )",
R"(663745)",
R"( 111 )",
R"( 3)"
}
},
R"( 3 )"}},
{
{
R"(??/)",
R"(?/ \)",
R"(<')_=<)",
R"(?\_/)",
R"(??\)"
},
{
R"( 2)",
R"( 111)",
R"(547366)",
R"( 111)",
R"( 3)"
}
},
{
{
R"(?????,)",
R"(?????}\)",
R"(\??.' `\)",
R"(?????,????)",
R"(?????}\???)",
R"(\??.' `\?)",
R"(}}< ( 6>)",
R"(/??`, .')",
R"(?????}/)",
R"(?????')"
},
R"(/??`, .'?)",
R"(?????}/???)",
R"(?????'????)"},
{
R"( 2 )",
R"( 22 )",
@@ -53,129 +46,52 @@ inline std::vector<AssetPair> fishAssetPairs = {
R"(661 7 45)",
R"(6 11 11 )",
R"( 33 )",
R"( 3)"
}
},
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"(???????,--,_????)",
R"(__????_\.---'-.?)",
R"(\ '.-" // o\)",
R"(/_.'-._ \\ /)",
R"(???????`"--(/"`)"
},
R"(???????`"--(/"`?)"},
{
R"( 22222 )",
R"(66 121111211 )",
R"(6 6111 77 41)",
R"(6661111 77 1)",
R"( 11113311)"
}
},
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"(???')"
},
R"(???'?)"},
{
R"( 11 )",
R"(61145)",
R"( 3)"
}
},
R"( 3 )"}},
{
{
R"(?__)",
R"(<'_><)",
R"(?`)"
},
{
R"( 11)",
R"(54116)",
R"( 3)"
}
},
{
{
R"(????????_.-`\)",
R"(?????-:`_..,_\)",
R"(('-..:-` , '-.,)",
R"(????????_.-`\??????)",
R"(?????-:`_..,_\?????)",
R"(('-..:-` , '-.,?)",
R"(?} _ ;':( o :)",
R"((.-`/'-.,__'` _.-`)",
R"(???`'-.,/??//`)"
},
R"((.-`/'-.,__'` _.-`?)",
R"(???`'-.,/??//`?????)"},
{
R"( 22222 )",
R"( 222111112 )",
R"(66661111 7 1111 )",
R"( 6 1 7777 4 1)",
R"(6666211111177 1111 )",
R"( 222222 333)"
}
},
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"(????????/\??????)",
R"(????????\.\_????)",
R"(\'-,.:-` '-,?)",
R"( ) _ (>( o <)",
R"(/.-`?':._ _.-`)",
R"(??????;/?``)",
R"(/.-`?':._ _.-`?)",
R"(??????;/?``?????)",
},
{
R"( 22 )",
@@ -183,53 +99,38 @@ inline std::vector<AssetPair> fishAssetPairs = {
R"(66661111 111 )",
R"( 6 1 777 4 1)",
R"(6666 1111 1111 )",
R"( 22 33)",
}
},
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"(_?????????_.*"\??????)",
R"(\'-._..-*` `'*-.??)",
R"(?) , (( o >)",
R"(/.`"*--.__)_.`_.-*`)"
},
R"(/.`"*--.__)_.`_.-*`??)"},
{
R"(6 11222 )",
R"(6661111111 11111 )",
R"( 6 3 77 4 1)",
R"(6661111111311311111)",
}
},
R"(6661111111311311111 )",}},
{
{
R"(??????/"*._?????????_)",
R"(??.-*'` `*-.._.-'/)",
R"(< o )) , ()",
R"(??`*-._`._(__.--*"`.\)",
R"(???????,-.?????)",
R"(__ ????) \.????)",
R"(\ \_.!` '-.?)",
R"(?> _ |<( O <)",
R"(/_/?';,,....-`?)",
},
{
R"( 22211 6)",
R"( 11111 1111111666)",
R"(1 4 77 3 6)",
R"( 1111131131111111666)",
},
}
R"( 222 )",
R"(66 2 22 )",
R"(6 61111 111 )",
R"( 6 1 777 4 5)",
R"(666 '333111111 )",}}
};
return fishAssets;
}
inline const std::vector<AssetPair>& getFishAssetPairs() {
static const std::vector<AssetPair> fishAssetPairs =
createBidirectionalAssets(getFishAssets());
return fishAssetPairs;
}

View File

@@ -1,38 +1,38 @@
#pragma once
#include "../Entity.h"
#include "../entities/Entity.h"
#include "../utils/SpriteUtils.h"
#include <vector>
struct SeaMonsterAsset {
std::vector<std::string> frame1;
std::vector<std::string> frame2;
std::vector<std::vector<std::string>> frames;
std::vector<std::string> mask;
};
inline const std::vector<SeaMonsterAsset> seaMonsterAssets = {
{{
R"( _???_?????????????????????_???_???????_a_a)",
R"( _{.`=`.}_??????_???_??????_{.`=`.}_????{/ ''\_)",
R"(?_????{.' _ '.}????{.`'`.}????{.' _ '.}??{| ._oo))",
R"({ \??{/ .'?'. \}??{/ .-. \}??{/ .'?'. \}?{/ |)",
},
inline const SeaMonsterAsset& getSeaMonster() {
static const SeaMonsterAsset seaMonster = {
{
R"( _???_????????????????????_a_a)",
R"(??_??????_???_??????_{.`=`.}_??????_???_??????{/ ''\_)",
{
R"(?????????_???_?????????????????????_???_???????_a_a???)",
R"(???????_{.`=`.}_??????_???_??????_{.`=`.}_????{/ ''\_?)",
R"(?_????{.' _ '.}????{.`'`.}????{.' _ '.}??{| ._oo))",
R"({ \??{/ .'?'. \}??{/ .-. \}??{/ .'?'. \}?{/ |????)"},
{
R"(??????????????????????_???_????????????????????_a_a???)",
R"(??_??????_???_??????_{.`=`.}_??????_???_??????{/ ''\_?)",
R"(?{ \????{.`'`.}????{.' _ '.}????{.`'`.}????{| ._oo))",
R"(??\ \??{/ .-. \}??{/ .'?'. \}??{/ .-. \}???{/ |)",
},
R"(??\ \??{/ .-. \}??{/ .'?'. \}??{/ .-. \}???{/ |????)"}},
{
R"( W W )",
R"()",
R"()",
R"()",
}},
{{R"( a_a_???????_???_?????????????????????_???_)",
R"( _/'' \}????_{.`=`.}_??????_???_??????_{.`=`.}_)",
R"((oo_. |}??{.' _ '.}????{.`'`.}????{.' _ '.}????_)",
R"(????| \}?{/ .'?'. \}??{/ .-. \}??{/ .'?'. \}??/ })"},
{R"( a_a_????????????????????_ _)",
R"( _/'' \}??????_???_??????_{.`=`.}_??????_???_??????_)",
R"((oo_. |}????{.`'`.}????{.' _ '.}????{.`'`.}????/ })",
R"(????| \}???{/ .-. \}??{/ .'?'. \}??{/ .-. \}??/ /)"},
{R"( W W)", R"()", R"()", R"()"}}};
R"()"
}
};
return seaMonster;
}
inline const std::vector<SeaMonsterAsset>& getSeaMonsterAssets() {
static const std::vector<SeaMonsterAsset> seaMonsterAssets =
createBidirectionalFramedAssets(getSeaMonster());
return seaMonsterAssets;
}

View File

@@ -1,37 +1,29 @@
#pragma once
#include "../Entity.h"
#include "../entities/Entity.h"
#include "../utils/SpriteUtils.h"
#include <vector>
inline std::vector<AssetPair> shipAssets = {
{{
inline const AssetPair &getShip() {
static const AssetPair ship = {
{
R"( | | | )",
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)",
}}};
R"(y y )"}};
return ship;
}
inline const std::vector<AssetPair> &getShipAssets() {
static const std::vector<AssetPair> shipAssets =
createBidirectionalAssets({getShip()});
return shipAssets;
}

View File

@@ -1,5 +1,6 @@
#pragma once
#include "../Entity.h"
#include "../entities/Entity.h"
#include "../utils/SpriteUtils.h"
#include <vector>
struct WhaleAsset {
@@ -7,47 +8,97 @@ struct WhaleAsset {
std::vector<std::string> mask;
};
inline const std::vector<WhaleAsset> whaleAssets = {
{{{{R"()", R"()", R"()", R"( .-----.)", R"( .' `.)",
R"(,????/ (o) \)", R"(\`._/ ,__)"},
{R"()", R"()", R"( :)", R"( .-----.)",
R"( .' `.)", R"(,????/ (o) \)",
R"(\`._/ ,__)"},
{R"()", R"( :)", R"( :)", R"( .-----.)",
R"( .' `.)", R"(,????/ (o) \)",
R"(\`._/ ,__)"},
{R"( . .)", R"( -:-)", R"( :)",
R"( .-----.)", R"( .' `.)", R"(,????/ (o) \)",
R"(\`._/ ,__)"},
{R"( . .)", R"( .-.-.)", R"( :)",
R"( .-----.)", R"( .' `.)", R"(,????/ (o) \)",
R"(\`._/ ,__)"},
{R"( . .)", R"( '.-:-.')", R"( ' : ')",
R"( .-----.)", R"( .' `.)", R"(,????/ (o) \)",
R"(\`._/ ,__)"},
{R"()", R"( .- -.)", R"( ; : ;)", R"( .-----.)",
R"( .' `.)", R"(,????/ (o) \)",
R"(\`._/ ,__)"},
{R"()", R"()", R"( ; ;)", R"( .-----.)",
R"( .' `.)", R"(,????/ (o) \)",
R"(\`._/ ,__)"}},
{R"( C C)", R"( CCCCCCC)", R"( C C C)", R"()",
R"()", R"( W)", R"()"}},
{{{R"()", R"()", R"()", R"( .-----.)", R"( .' `.)",
R"( / (o) \????)", R"((__, \_.'/)"},
{R"()", R"()", R"( :)", R"( .-----.)", R"( .' `.)",
R"( / (o) \????)", R"((__, \_.'/)"},
{R"()", R"( :)", R"( :)", R"( .-----.)",
R"( .' `.)", R"( / (o) \????)", R"((__, \_.'/)"},
{R"( . .)", R"( -:-)", R"( :)", R"( .-----.)",
R"( .' `.)", R"( / (o) \????)", R"((__, \_.'/)"},
{R"( . .)", R"( .-.-.)", R"( :)", R"( .-----.)",
R"( .' `.)", R"( / (o) \????)", R"((__, \_.'/)"},
{R"( . .)", R"( '.-:-.')", R"( ' : ')", R"( .-----.)",
R"( .' `.)", R"( / (o) \????)", R"((__, \_.'/)"},
{R"()", R"( .- -.)", R"( ; : ;)", R"( .-----.)",
R"( .' `.)", R"( / (o) \????)", R"((__, \_.'/)"},
{R"()", R"()", R"( ; ;)", R"( .-----.)", R"( .' `.)",
R"( / (o) \????)", R"((__, \_.'/)"}},
{R"( C C)", R"( CCCCCCC)", R"( C C C)", R"()", R"()",
R"( W)", R"()"}}}};
inline const WhaleAsset& getWhale() {
static const WhaleAsset whale = {
{
{
R"( )",
R"()",
R"()",
R"( .-----. )",
R"( .' `. )",
R"(,????/ (o) \?)",
R"(\`._/ ,__))"
},
{
R"( )",
R"()",
R"( : )",
R"( .-----. )",
R"( .' `. )",
R"(,????/ (o) \?)",
R"(\`._/ ,__))"
},
{
R"( )",
R"( : )",
R"( : )",
R"( .-----. )",
R"( .' `. )",
R"(,????/ (o) \?)",
R"(\`._/ ,__))"
},
{
R"( . . )",
R"( -:- )",
R"( : )",
R"( .-----. )",
R"( .' `. )",
R"(,????/ (o) \?)",
R"(\`._/ ,__))"
},
{
R"( . . )",
R"( .-.-. )",
R"( : )",
R"( .-----. )",
R"( .' `. )",
R"(,????/ (o) \?)",
R"(\`._/ ,__))"
},
{
R"( . . )",
R"( '.-:-.' )",
R"( ' : ' )",
R"( .-----. )",
R"( .' `. )",
R"(,????/ (o) \?)",
R"(\`._/ ,__))"
},
{
R"( )",
R"( .- -. )",
R"( ; : ; )",
R"( .-----. )",
R"( .' `. )",
R"(,????/ (o) \?)",
R"(\`._/ ,__))"
},
{
R"( )",
R"( )",
R"( ; ; )",
R"( .-----. )",
R"( .' `. )",
R"(,????/ (o) \?)",
R"(\`._/ ,__))"
}
},
{
R"( C C )",
R"( CCCCCCC )",
R"( C C C )",
R"()",
R"()",
R"( W )",
R"()"
}
};
return whale;
}
inline const std::vector<WhaleAsset>& getWhaleAssets() {
static const std::vector<WhaleAsset> whaleAssets =
createBidirectionalFramedAssets(getWhale());
return whaleAssets;
}

423
src/core/Aquarium.cpp Normal file
View File

@@ -0,0 +1,423 @@
#include "Aquarium.h"
#include "../core/Config.h"
#include "../entities/Bubble.h"
#include "../entities/Castle.h"
#include "../entities/Fish.h"
#include "../entities/SeaMonster.h"
#include "../entities/Seaweed.h"
#include "../entities/Ship.h"
#include "../entities/Waterline.h"
#include "../entities/Whale.h"
#include <algorithm>
#include <chrono>
#include <cstdio>
#include <cstring>
#include <iomanip>
#include <iostream>
#include <signal.h>
#include <sstream>
#include <sys/ioctl.h>
#include <termios.h>
#include <unistd.h>
// ANSI color codes
namespace ANSI {
const char *RESET = "\033[0m";
const char *BOLD = "\033[1m";
const char *CLEAR_SCREEN = "\033[2J";
const char *CURSOR_HOME = "\033[H";
const char *HIDE_CURSOR = "\033[?25l";
const char *SHOW_CURSOR = "\033[?25h";
// Colors (foreground)
const char *BLACK = "\033[30m";
const char *RED = "\033[31m";
const char *GREEN = "\033[32m";
const char *YELLOW = "\033[33m";
const char *BLUE = "\033[34m";
const char *MAGENTA = "\033[35m";
const char *CYAN = "\033[36m";
const char *WHITE = "\033[37m";
// Colors (background)
const char *BG_BLACK = "\033[40m";
const char *RESET_BLACK_BG = "\033[0;40m";
// Move cursor to position
std::string moveTo(int row, int col) {
char buffer[32];
snprintf(buffer, sizeof(buffer), "\033[%d;%dH", row + 1, col + 1);
return std::string(buffer);
}
} // namespace ANSI
// Global terminal state
static struct termios original_termios;
static bool termios_saved = false;
// Signal handler for cleanup
void cleanup_terminal(int sig) {
if (termios_saved) {
tcsetattr(STDIN_FILENO, TCSANOW, &original_termios);
}
printf("%s%s", ANSI::SHOW_CURSOR, ANSI::RESET);
fflush(stdout);
if (sig != 0) {
exit(sig);
}
}
Aquarium::Aquarium() {
// Save original terminal settings
if (tcgetattr(STDIN_FILENO, &original_termios) == 0) {
termios_saved = true;
}
// Set up signal handlers for cleanup
signal(SIGINT, cleanup_terminal);
signal(SIGTERM, cleanup_terminal);
signal(SIGQUIT, cleanup_terminal);
// Set terminal to raw mode
struct termios raw = original_termios;
raw.c_lflag &= ~(ECHO | ICANON);
raw.c_iflag &= ~(IXON | ICRNL);
raw.c_oflag &= ~(OPOST);
raw.c_cc[VMIN] = 0; // Non-blocking read
raw.c_cc[VTIME] = 1; // 100ms timeout
tcsetattr(STDIN_FILENO, TCSAFLUSH, &raw);
// Initialize display
printf("%s%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::HIDE_CURSOR,
ANSI::BG_BLACK);
fflush(stdout);
// Get terminal size
getTerminalSize();
currentFrame.assign(height, std::vector<Cell>(width));
previousFrame.assign(height, std::vector<Cell>(width));
if (!colorLookupInitialized) {
initColorLookup();
colorLookupInitialized = true;
}
}
void Aquarium::getTerminalSize() {
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) {
height = ws.ws_row;
width = ws.ws_col;
} else {
cleanup_terminal(0);
std::cerr << "Error: Unable to determine terminal size.\n";
std::exit(1);
}
}
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;
}
}
void Aquarium::redraw() {
clearCurrentFrame();
if (g_config.enable_big_entities) {
ensureBigEntityExists();
}
static std::vector<std::unique_ptr<Entity>> newEntities;
static std::vector<size_t> entitiesToRemove;
newEntities.clear();
entitiesToRemove.clear();
// Count current bubbles
int bubble_count = 0;
for (const auto &entity : entities) {
if (dynamic_cast<Bubble *>(entity.get())) {
bubble_count++;
}
}
// Update all entities and collect changes
for (size_t i = 0; i < entities.size(); ++i) {
auto &entity = entities[i];
entity->update();
// Handle fish bubble spawning with configuration
if (g_config.enable_bubbles && bubble_count < g_config.max_bubbles) {
if (auto *fish = dynamic_cast<Fish *>(entity.get())) {
if (fish->shouldSpawnBubble()) {
// Use configured bubble rate
if (rand() % g_config.fish_bubble_rate == 0) {
newEntities.emplace_back(
std::make_unique<Bubble>(fish->getX(), fish->getY()));
bubble_count++;
}
}
}
}
if (entity->shouldBeRemoved()) {
auto replacement = entity->createReplacement();
if (replacement) {
entity = std::move(replacement);
entities_need_sorting = true;
} else {
entitiesToRemove.push_back(i);
}
}
}
// Remove entities in reverse order to maintain indices
for (auto it = entitiesToRemove.rbegin(); it != entitiesToRemove.rend();
++it) {
entities.erase(entities.begin() + *it);
entities_need_sorting = true;
}
// Add new entities if we have them and haven't exceeded max
if (!newEntities.empty() &&
entities.size() < static_cast<size_t>(g_config.max_entities)) {
entities.reserve(entities.size() + newEntities.size());
for (auto &newEntity : newEntities) {
entities.emplace_back(std::move(newEntity));
}
entities_need_sorting = true;
}
ensureEntitiesSorted();
// Draw all entities
for (const auto &entity : entities) {
entity->draw();
}
// Draw debug information
if (g_config.show_fps || g_config.show_entity_count) {
drawDebugInfo();
}
renderToScreen();
}
void Aquarium::drawDebugInfo() {
static double last_fps = 0.0;
static int frame_counter = 0;
static auto last_time = std::chrono::steady_clock::now();
frame_counter++;
auto current_time = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
current_time - last_time)
.count();
if (elapsed >= 1000) {
last_fps = (frame_counter * 1000.0) / elapsed;
frame_counter = 0;
last_time = current_time;
}
std::stringstream debug_info;
if (g_config.show_fps) {
debug_info << "FPS: " << std::fixed << std::setprecision(1) << last_fps;
}
if (g_config.show_entity_count) {
if (g_config.show_fps)
debug_info << " | ";
debug_info << "Entities: " << entities.size();
}
std::string debug_str = debug_info.str();
if (!debug_str.empty()) {
std::string color_str(debug_str.length(), 'w'); // White color
drawToFrame(0, width - static_cast<int>(debug_str.length()), debug_str,
color_str);
}
}
void Aquarium::resize() {
printf("%s%s%s", ANSI::CLEAR_SCREEN, ANSI::CURSOR_HOME, ANSI::BG_BLACK);
fflush(stdout);
getTerminalSize();
currentFrame.assign(height, std::vector<Cell>(width));
previousFrame.assign(height, std::vector<Cell>(width));
entities.clear();
entities_need_sorting = true;
addWaterline();
addCastle();
// Use configured seaweed count or auto-calculate
int seaweed_count = g_config.initial_seaweed_count;
if (seaweed_count == -1) {
seaweed_count = width / 15;
}
for (int i = 0; i < seaweed_count; i++) {
addSeaweed();
}
// Use configured fish count or auto-calculate
int fish_count = g_config.initial_fish_count;
if (fish_count == -1) {
fish_count = width * (height - 9) / 350;
}
for (int i = 0; i < fish_count; i++) {
addFish();
}
}
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::addSeaMonster() { addEntityImpl<SeaMonster>(); }
void Aquarium::addWhale() { addEntityImpl<Whale>(); }
void Aquarium::ensureBigEntityExists() {
// Check if any big entities exist on screen
for (const auto &entity : entities) {
if (dynamic_cast<Ship *>(entity.get()) ||
dynamic_cast<SeaMonster *>(entity.get()) ||
dynamic_cast<Whale *>(entity.get())) {
return; // Big entity found, do nothing
}
}
// No big entity found, spawn next in cycle
int entity_type = big_entity_index % 3;
if (entity_type == 0) {
addEntityImpl<Ship>();
} else if (entity_type == 1) {
addEntityImpl<SeaMonster>();
} else {
addEntityImpl<Whale>();
}
++big_entity_index;
}
void Aquarium::clearCurrentFrame() {
static const Cell empty_cell{};
for (auto &row : currentFrame) {
row.assign(width, empty_cell);
}
}
void Aquarium::drawToFrame(int y, int x, const std::string &line,
const std::string &colorLine) {
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 = (colorChar >= 'A' && colorChar <= 'Z');
currentFrame[y][cx] = {
ch, static_cast<char>(isBold ? colorChar + 32 : colorChar), isBold};
}
}
void Aquarium::initColorLookup() {
for (int i = 0; i < 256; ++i)
colorLookup[i] = ANSI::BLACK; // Default black
colorLookup['r'] = ANSI::RED;
colorLookup['g'] = ANSI::GREEN;
colorLookup['y'] = ANSI::YELLOW;
colorLookup['b'] = ANSI::BLUE;
colorLookup['m'] = ANSI::MAGENTA;
colorLookup['c'] = ANSI::CYAN;
colorLookup['w'] = ANSI::WHITE;
colorLookup['k'] = ANSI::BLACK;
}
void Aquarium::renderToScreen() {
static std::string output;
output.clear();
output.reserve(height * width * 20);
int cursor_y = -1, cursor_x = -1;
for (int y = 0; y < height; ++y) {
for (int x = 0; x < width; ++x) {
const Cell &newCell = currentFrame[y][x];
Cell &oldCell = previousFrame[y][x];
if (newCell == oldCell)
continue;
oldCell = newCell;
// Move cursor only when needed
if (cursor_y != y || cursor_x != x) {
output += ANSI::moveTo(y, x);
cursor_y = y;
cursor_x = x;
}
// Apply cell formatting and character
output += ANSI::RESET_BLACK_BG;
// Only apply bold if configured
if (g_config.use_bold && newCell.bold) {
output += ANSI::BOLD;
}
// Only apply colors if configured
if (g_config.use_colors) {
output += colorLookup[static_cast<unsigned char>(newCell.colorChar)];
}
output += newCell.ch;
++cursor_x;
}
}
if (!output.empty()) {
std::cout << output << std::flush;
}
}
// Check for input (non-blocking)
int Aquarium::checkInput() {
char c;
if (read(STDIN_FILENO, &c, 1) == 1) {
return c;
}
return -1; // No input available
}
// Check if terminal was resized
bool Aquarium::checkResize() {
struct winsize ws;
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0) {
if (ws.ws_row != height || ws.ws_col != width) {
return true;
}
}
return false;
}
Aquarium::~Aquarium() { cleanup_terminal(0); }

View File

@@ -1,7 +1,6 @@
#pragma once
#include "Entity.h"
#include "../entities/Entity.h"
#include <memory>
#include <ncurses.h>
#include <vector>
extern int g_maxCells;
@@ -15,7 +14,6 @@ private:
char ch = ' ';
char colorChar = 'k';
bool bold = false;
bool operator==(const Cell &other) const {
return ch == other.ch && colorChar == other.colorChar &&
bold == other.bold;
@@ -28,10 +26,10 @@ private:
std::vector<std::unique_ptr<Entity>> entities;
size_t big_entity_index = 0;
void ensureBigEntityExists();
bool entities_need_sorting = true;
static inline short colorLookup[256] = {0};
static inline const char *colorLookup[256] = {nullptr};
static inline bool colorLookupInitialized = false;
void drawDebugInfo();
public:
Aquarium();
@@ -53,17 +51,21 @@ public:
void addShip();
void addSeaMonster();
void addWhale();
void redraw();
void initColors();
void resize();
void drawToFrame(int y, int x, const std::string &line,
const std::string &colorLine);
// New termios-specific methods
int checkInput(); // Returns character code or -1 if no input
bool checkResize(); // Returns true if terminal was resized
private:
void clearCurrentFrame();
void renderToScreen();
void ensureEntitiesSorted();
void getTerminalSize();
static void initColorLookup();
template <typename T, typename... Args> void addEntityImpl(Args &&...args) {

175
src/core/Config.cpp Normal file
View File

@@ -0,0 +1,175 @@
#include "Config.h"
#include <cstdlib>
#include <cstring>
#include <getopt.h>
#include <iostream>
AquariumConfig g_config;
void printUsage(const char *program_name) {
std::cout << "Usage: " << program_name << " [OPTIONS]\n\n";
std::cout << "Aquarium screensaver with customizable settings\n\n";
std::cout << "OPTIONS:\n";
std::cout << " -h, --help Show this help message\n";
std::cout
<< " -f, --fish COUNT Number of initial fish (default: auto)\n";
std::cout << " -s, --seaweed COUNT Number of seaweed plants (default: "
"auto)\n";
std::cout << " -b, --max-bubbles COUNT Maximum bubbles on screen "
"(default: 50)\n";
std::cout << " -d, --delay MS Frame delay in milliseconds "
"(default: 100)\n";
std::cout << " -r, --bubble-rate RATE Fish bubble spawn rate 1/N "
"(default: 10)\n";
std::cout << " -m, --max-entities COUNT Maximum entities on screen "
"(default: 200)\n";
std::cout << " --no-big-entities Disable ships, whales, and sea "
"monsters\n";
std::cout << " --no-bubbles Disable bubble generation\n";
std::cout << " --no-colors Disable colors (monochrome mode)\n";
std::cout << " --no-bold Disable bold text\n";
std::cout << " --show-fps Display FPS counter\n";
std::cout << " --show-count Display entity count\n";
std::cout << " --debug Enable debug mode\n";
std::cout << "\nShort options can be combined: -f20 -s5 -d50\n";
std::cout << "Long options accept = syntax: --fish=20 --delay=50\n";
std::cout << "\nExamples:\n";
std::cout << " " << program_name
<< " -f20 -s5 -d50 # Fast aquarium with lots of fish\n";
std::cout << " " << program_name
<< " --fish=10 --no-colors # 10 fish, monochrome\n";
std::cout << " " << program_name
<< " --debug --show-fps # Debug mode with FPS\n";
}
bool parseArguments(int argc, char *argv[]) {
// Define long options
static const struct option long_options[] = {
{"help", no_argument, nullptr, 'h'},
{"fish", required_argument, nullptr, 'f'},
{"seaweed", required_argument, nullptr, 's'},
{"max-bubbles", required_argument, nullptr, 'b'},
{"delay", required_argument, nullptr, 'd'},
{"bubble-rate", required_argument, nullptr, 'r'},
{"max-entities", required_argument, nullptr, 'm'},
{"no-big-entities", no_argument, nullptr, 'B'},
{"no-bubbles", no_argument, nullptr, 'N'},
{"no-colors", no_argument, nullptr, 'C'},
{"no-bold", no_argument, nullptr, 'O'},
{"show-fps", no_argument, nullptr, 'F'},
{"show-count", no_argument, nullptr, 'S'},
{"debug", no_argument, nullptr, 'D'},
{nullptr, 0, nullptr, 0}};
// Short options string - : after letter means it takes an argument
const char *short_options = "hf:s:b:d:r:m:BNCOFSD";
int option;
int option_index = 0;
// Reset getopt state (important for testing)
optind = 1;
while ((option = getopt_long(argc, argv, short_options, long_options,
&option_index)) != -1) {
switch (option) {
case 'h':
printUsage(argv[0]);
return false;
case 'f':
g_config.initial_fish_count = std::atoi(optarg);
if (g_config.initial_fish_count < -1) {
std::cerr << "Error: Fish count must be >= -1\n";
return false;
}
break;
case 's':
g_config.initial_seaweed_count = std::atoi(optarg);
if (g_config.initial_seaweed_count < -1) {
std::cerr << "Error: Seaweed count must be >= -1\n";
return false;
}
break;
case 'b':
g_config.max_bubbles = std::atoi(optarg);
if (g_config.max_bubbles < 0) {
std::cerr << "Error: Max bubbles must be >= 0\n";
return false;
}
break;
case 'd':
g_config.frame_delay_ms = std::atoi(optarg);
if (g_config.frame_delay_ms < 1) {
std::cerr << "Error: Frame delay must be >= 1ms\n";
return false;
}
break;
case 'r':
g_config.fish_bubble_rate = std::atoi(optarg);
if (g_config.fish_bubble_rate < 1) {
std::cerr << "Error: Bubble rate must be >= 1\n";
return false;
}
break;
case 'm':
g_config.max_entities = std::atoi(optarg);
if (g_config.max_entities < 1) {
std::cerr << "Error: Max entities must be >= 1\n";
return false;
}
break;
case 'B':
g_config.enable_big_entities = false;
break;
case 'N':
g_config.enable_bubbles = false;
break;
case 'C':
g_config.use_colors = false;
break;
case 'O':
g_config.use_bold = false;
break;
case 'F':
g_config.show_fps = true;
break;
case 'S':
g_config.show_entity_count = true;
break;
case 'D':
g_config.debug_mode = true;
break;
case '?':
// getopt_long already printed an error message
std::cerr << "Use --help for usage information\n";
return false;
default:
std::cerr << "Error: Unexpected option character\n";
return false;
}
}
// Check for non-option arguments
if (optind < argc) {
std::cerr << "Error: Unexpected argument: " << argv[optind] << "\n";
std::cerr << "Use --help for usage information\n";
return false;
}
return true;
}

33
src/core/Config.h Normal file
View File

@@ -0,0 +1,33 @@
#pragma once
#include <string>
struct AquariumConfig {
// Entity spawn settings
int initial_fish_count = -1; // -1 means auto-calculate
int initial_seaweed_count = -1; // -1 means auto-calculate
int max_bubbles = 50;
// Animation settings
int frame_delay_ms = 100;
bool enable_big_entities = true;
bool enable_bubbles = true;
// Visual settings
bool use_colors = true;
bool use_bold = true;
// Spawn rates (lower = more frequent)
int fish_bubble_rate = 10; // 1 in N chance per frame
int big_entity_spawn_delay = 100; // frames between big entities
// Performance settings
bool optimize_rendering = true;
int max_entities = 200;
// Debug settings
bool show_fps = false;
bool show_entity_count = false;
bool debug_mode = false;
};
extern AquariumConfig g_config;

115
src/core/main.cpp Normal file
View File

@@ -0,0 +1,115 @@
#include "Aquarium.h"
#include "Config.h"
#include <chrono>
#include <iostream>
#include <thread>
#ifdef __OpenBSD__
#include <unistd.h>
#endif
// Forward declaration
bool parseArguments(int argc, char *argv[]);
int main(int argc, char *argv[]) {
// Parse command line arguments
if (!parseArguments(argc, argv)) {
return 1; // Exit if parsing failed or help was shown
}
// Print configuration if debug mode is enabled
if (g_config.debug_mode) {
std::cout << "Configuration:\n";
std::cout << " Fish count: "
<< (g_config.initial_fish_count == -1
? "auto"
: std::to_string(g_config.initial_fish_count))
<< "\n";
std::cout << " Seaweed count: "
<< (g_config.initial_seaweed_count == -1
? "auto"
: std::to_string(g_config.initial_seaweed_count))
<< "\n";
std::cout << " Max bubbles: " << g_config.max_bubbles << "\n";
std::cout << " Frame delay: " << g_config.frame_delay_ms << "ms\n";
std::cout << " Big entities: "
<< (g_config.enable_big_entities ? "enabled" : "disabled")
<< "\n";
std::cout << " Bubbles: "
<< (g_config.enable_bubbles ? "enabled" : "disabled") << "\n";
std::cout << " Colors: " << (g_config.use_colors ? "enabled" : "disabled")
<< "\n";
std::cout << " Bold text: " << (g_config.use_bold ? "enabled" : "disabled")
<< "\n";
std::cout << " Max entities: " << g_config.max_entities << "\n";
std::cout << "Press any key to continue...\n";
std::cin.get();
}
Aquarium &aquarium = Aquarium::getInstance();
#ifdef __OpenBSD__
if (pledge("stdio tty", NULL) == -1) {
perror("pledge");
return 1;
}
#endif
// Initialize the aquarium display
aquarium.resize(); // Setup initial entities
// Variables for FPS calculation
auto last_time = std::chrono::steady_clock::now();
int frame_count = 0;
double fps = 0.0;
// Main game loop
while (true) {
auto frame_start = std::chrono::steady_clock::now();
int input = aquarium.checkInput();
if (input == 'q' || input == 'Q' || input == 27) { // ESC key
break;
}
// Toggle debug info with 'd' key
if (input == 'd' || input == 'D') {
g_config.show_fps = !g_config.show_fps;
g_config.show_entity_count = !g_config.show_entity_count;
}
if (aquarium.checkResize()) {
aquarium.resize();
}
aquarium.redraw();
// Calculate FPS
if (g_config.show_fps) {
++frame_count;
auto current_time = std::chrono::steady_clock::now();
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
current_time - last_time)
.count();
if (elapsed >= 1000) { // Update FPS every second
fps = (frame_count * 1000.0) / elapsed;
frame_count = 0;
last_time = current_time;
}
}
// Sleep for configured frame delay
auto frame_end = std::chrono::steady_clock::now();
auto frame_duration = std::chrono::duration_cast<std::chrono::milliseconds>(
frame_end - frame_start);
auto sleep_duration =
std::chrono::milliseconds(g_config.frame_delay_ms) - frame_duration;
if (sleep_duration > std::chrono::milliseconds(0)) {
std::this_thread::sleep_for(sleep_duration);
}
}
return 0;
}

View File

@@ -18,6 +18,7 @@ void Bubble::updateFrame() {
current_mask[0] = BUBBLE_COLOR;
}
// Waterline collision
bool Bubble::shouldBeRemoved() const noexcept { return y < 10; }
// Bubbles don't create replacements

View File

@@ -1,8 +1,8 @@
#include "Castle.h"
#include "Aquarium.h"
#include "assets/CastleAssets.h"
#include "../assets/CastleAssets.h"
#include "../core/Aquarium.h"
Castle::Castle()
: Entity(Aquarium::getInstance().getWidth() - 32,
Aquarium::getInstance().getHeight() - 13),
image(castleAsset.image), mask(castleAsset.mask) {}
image(getCastleAsset().image), mask(getCastleAsset().mask) {}

View File

@@ -14,7 +14,6 @@ public:
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; }
};

View File

@@ -1,5 +1,5 @@
#include "Entity.h"
#include "Aquarium.h"
#include "../core/Aquarium.h"
void Entity::draw() const {
auto &aquarium = Aquarium::getInstance();
@@ -55,3 +55,9 @@ void Entity::draw() const {
}
}
}
bool Entity::shouldBeRemoved() const noexcept {
const auto &aquarium = Aquarium::getInstance();
// unsigned nonsense
return x < -static_cast<int>(getWidth()) || x > aquarium.getWidth();
}

View File

@@ -10,27 +10,31 @@ struct AssetPair {
class Entity {
protected:
const bool moving_right;
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_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 const std::vector<std::string> &getImage() const = 0;
virtual const std::vector<std::string> &getMask() const = 0;
virtual char getDefaultColor() const noexcept = 0;
virtual bool shouldBeRemoved() const noexcept = 0;
public:
Entity() : moving_right(false), x(0.0f), y(0.0f), entity_id(++next_id) {}
Entity(bool moving_right)
: moving_right(moving_right), x(0.0f), y(0.0f), entity_id(++next_id) {}
Entity(float init_x, float init_y)
: moving_right(false), 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; }
virtual int getWidth() const noexcept { return getImage()[0].length(); }
size_t getId() const noexcept { return entity_id; }
virtual void update() noexcept = 0;
virtual bool shouldBeRemoved() const noexcept;
virtual std::unique_ptr<Entity> createReplacement() const { return nullptr; }
virtual int getPreferredLayer() const noexcept = 0;

View File

@@ -1,30 +1,31 @@
#include "Fish.h"
#include "Aquarium.h"
#include "Random.h"
#include "assets/FishAssets.h"
#include "defs.h"
#include "../assets/FishAssets.h"
#include "../core/Aquarium.h"
#include "../utils/Random.h"
#include "../utils/defs.h"
std::unordered_map<char, char> Fish::color_map;
Fish::Fish() : Fish(getRandomAssetIndex()) {}
Fish::Fish(int asset_index)
: Entity(), image(fishAssetPairs[asset_index].image),
mask(fishAssetPairs[asset_index].mask),
speed(Random::floatInRange(0.25f, 2.25f)),
moving_right(asset_index % 2 == 0) {
: Entity(asset_index % 2 == 0),
image(getFishAssetPairs()[asset_index].image),
mask(getFishAssetPairs()[asset_index].mask),
speed(Random::floatInRange(0.25f, 2.25f)) {
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());
x = moving_right ? -this->getWidth()
: static_cast<float>(aquarium.getWidth());
randomizeMask();
}
void Fish::randomizeMask() {
// Clear and rebuild color map with fresh random colors each time
// Clear and rebuild color map with random colors each time
color_map.clear();
color_map['4'] = 'W'; // White is always '4'
color_map['4'] = 'W'; // White is always '4' for eyes
// Assign random colors to digits 1-3, 5-9
for (char digit = '1'; digit <= '9'; ++digit) {
@@ -45,20 +46,12 @@ void Fish::randomizeMask() {
}
int Fish::getRandomAssetIndex() {
return Random::intInRange(0, static_cast<int>(fishAssetPairs.size()) - 1);
return Random::intInRange(0,
static_cast<int>(getFishAssetPairs().size()) - 1);
}
void Fish::update() noexcept { x += moving_right ? speed : -speed; }
bool Fish::shouldBeRemoved() const noexcept {
const auto &aquarium = Aquarium::getInstance();
if (moving_right) {
return x > static_cast<float>(aquarium.getWidth());
} else {
return (x + static_cast<float>(image[0].length())) < 0;
}
}
std::unique_ptr<Entity> Fish::createReplacement() const {
return std::make_unique<Fish>();
}

View File

@@ -1,6 +1,6 @@
#pragma once
#include "../assets/FishAssets.h"
#include "Entity.h"
#include "assets/FishAssets.h"
#include <array>
#include <unordered_map>
@@ -12,7 +12,6 @@ private:
const std::vector<std::string> &image;
std::vector<std::string> mask;
const float speed;
const bool moving_right;
static std::unordered_map<char, char> color_map;
@@ -28,7 +27,6 @@ public:
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;

View File

@@ -0,0 +1,44 @@
#include "SeaMonster.h"
#include "../assets/SeaMonsterAssets.h"
#include "../core/Aquarium.h"
#include "../utils/Random.h"
SeaMonster::SeaMonster() : SeaMonster(getRandomDirection()) {}
SeaMonster::SeaMonster(int asset_index)
: Entity(asset_index == 0),
frames(getSeaMonsterAssets()[asset_index].frames),
mask(getSeaMonsterAssets()[asset_index].mask), speed(SEAMONSTER_SPEED) {
const auto &aquarium = Aquarium::getInstance();
y = WATER_SURFACE_OFFSET;
// Use first frame for positioning calculations
const auto &first_frame = frames[0];
if (moving_right) {
x = -static_cast<float>(first_frame[0].length());
} else {
x = static_cast<float>(aquarium.getWidth());
}
current_image = first_frame;
}
int SeaMonster::getRandomDirection() { return Random::intInRange(0, 1); }
void SeaMonster::update() noexcept {
x += moving_right ? speed : -speed;
++animation_counter;
if (animation_counter >= ANIMATION_DELAY) {
current_frame_index = (current_frame_index + 1) % frames.size();
animation_counter = 0;
}
}
const std::vector<std::string> &SeaMonster::getImage() const {
current_image = frames[current_frame_index];
return current_image;
}
int SeaMonster::getPreferredLayer() const noexcept { return 8; }

View File

@@ -1,19 +1,17 @@
#pragma once
#include "../assets/SeaMonsterAssets.h"
#include "Entity.h"
#include "assets/SeaMonsterAssets.h"
class SeaMonster : public Entity {
private:
static constexpr float SEAMONSTER_SPEED = 0.8f;
static constexpr int WATER_SURFACE_OFFSET = 2;
const std::vector<std::string> &frame1;
const std::vector<std::string> &frame2;
const std::vector<std::vector<std::string>> frames;
const std::vector<std::string> &mask;
const float speed;
const bool moving_right;
bool current_frame = false;
int current_frame_index = 0;
int animation_counter = 0;
mutable std::vector<std::string> current_image;
@@ -30,6 +28,5 @@ public:
const std::vector<std::string> &getMask() const override { return mask; }
char getDefaultColor() const noexcept override { return 'G'; }
bool shouldBeRemoved() const noexcept override;
int getPreferredLayer() const noexcept override;
};

View File

@@ -1,7 +1,7 @@
#include "Seaweed.h"
#include "Aquarium.h"
#include "Random.h"
#include "defs.h"
#include "../core/Aquarium.h"
#include "../utils/Random.h"
#include "../utils/defs.h"
Seaweed::Seaweed()
: Entity(),

25
src/entities/Ship.cpp Normal file
View File

@@ -0,0 +1,25 @@
#include "Ship.h"
#include "../assets/ShipAssets.h"
#include "../core/Aquarium.h"
#include "../utils/Random.h"
Ship::Ship() : Ship(getRandomDirection()) {}
Ship::Ship(int asset_index)
: Entity(asset_index == 0), image(getShipAssets()[asset_index].image),
mask(getShipAssets()[asset_index].mask), speed(SHIP_SPEED) {
const auto &aquarium = Aquarium::getInstance();
y = 0;
if (moving_right) {
x = -static_cast<float>(image[0].length());
} else {
x = static_cast<float>(aquarium.getWidth());
}
}
int Ship::getRandomDirection() { return Random::intInRange(0, 1); }
void Ship::update() noexcept { x += moving_right ? speed : -speed; }
int Ship::getPreferredLayer() const noexcept { return 9; }

View File

@@ -1,16 +1,14 @@
#pragma once
#include "../assets/ShipAssets.h"
#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();
@@ -23,6 +21,5 @@ public:
const std::vector<std::string> &getMask() const override { return mask; }
char getDefaultColor() const noexcept override { return 'W'; }
bool shouldBeRemoved() const noexcept override;
int getPreferredLayer() const noexcept override;
};

View File

@@ -1,7 +1,7 @@
#include "Waterline.h"
#include "Aquarium.h"
#include "Random.h"
#include "defs.h"
#include "../core/Aquarium.h"
#include "../utils/Random.h"
#include "../utils/defs.h"
Waterline::Waterline() : Entity(0, WATERLINE_Y) {
shape[0] = "~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~";

View File

@@ -24,7 +24,6 @@ public:
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; }

View File

@@ -1,14 +1,13 @@
#include "Whale.h"
#include "Aquarium.h"
#include "Random.h"
#include "assets/WhaleAssets.h"
#include "../assets/WhaleAssets.h"
#include "../core/Aquarium.h"
#include "../utils/Random.h"
Whale::Whale() : Whale(getRandomDirection()) {}
Whale::Whale(int asset_index)
: Entity(), frames(whaleAssets[asset_index].frames),
mask(whaleAssets[asset_index].mask), speed(WHALE_SPEED),
moving_right(asset_index == 0) {
: Entity((asset_index == 0)), frames(getWhaleAssets()[asset_index].frames),
mask(getWhaleAssets()[asset_index].mask), speed(WHALE_SPEED) {
const auto &aquarium = Aquarium::getInstance();
y = 0;
@@ -46,15 +45,4 @@ const std::vector<std::string> &Whale::getImage() const {
return current_image;
}
bool Whale::shouldBeRemoved() const noexcept {
const auto &aquarium = Aquarium::getInstance();
const auto &first_frame = frames[0];
if (moving_right) {
return x > static_cast<float>(aquarium.getWidth());
} else {
return (x + static_cast<float>(first_frame[0].length())) < 0;
}
}
int Whale::getPreferredLayer() const noexcept { return 8; }

View File

@@ -1,6 +1,6 @@
#pragma once
#include "../assets/WhaleAssets.h"
#include "Entity.h"
#include "assets/WhaleAssets.h"
class Whale : public Entity {
private:
@@ -9,7 +9,6 @@ private:
const std::vector<std::vector<std::string>> frames;
const std::vector<std::string> &mask;
const float speed;
const bool moving_right;
int current_frame_index = 0;
int animation_counter = 0;
@@ -28,7 +27,5 @@ public:
const std::vector<std::string> &getImage() const override;
const std::vector<std::string> &getMask() const override { return mask; }
char getDefaultColor() const noexcept override { return 'B'; }
bool shouldBeRemoved() const noexcept override;
int getPreferredLayer() const noexcept override;
};

View File

@@ -1,32 +0,0 @@
#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;
}

80
src/utils/SpriteUtils.h Normal file
View File

@@ -0,0 +1,80 @@
#pragma once
#include <algorithm>
#include <string>
#include <unordered_map>
#include <vector>
struct AssetPair;
// Character mapping for directional elements
const std::unordered_map<char, char> charFlipMap = {
{'(', ')'}, {')', '('}, {'[', ']'}, {']', '['}, {'{', '}'},
{'}', '{'}, {'<', '>'}, {'>', '<'}, {'/', '\\'}, {'\\', '/'}};
inline char mirrorChar(char c) {
auto it = charFlipMap.find(c);
return (it != charFlipMap.end()) ? it->second : c;
}
inline std::string mirrorRow(std::string row) {
std::reverse(row.begin(), row.end());
for (char &c : row) {
c = mirrorChar(c);
}
return row;
}
// Mirror an entire sprite (vector of strings)
inline std::vector<std::string>
mirrorSprite(const std::vector<std::string> &sprite) {
std::vector<std::string> mirrored;
mirrored.reserve(sprite.size());
for (const auto &row : sprite) {
mirrored.push_back(mirrorRow(row));
}
return mirrored;
}
inline AssetPair mirrorAssetPair(const AssetPair &asset) {
return {mirrorSprite(asset.image), mirrorSprite(asset.mask)};
}
// Create bidirectional assets from simple AssetPair vector
inline std::vector<AssetPair>
createBidirectionalAssets(const std::vector<AssetPair> &rightFacingAssets) {
std::vector<AssetPair> result;
result.reserve(rightFacingAssets.size() * 2);
for (const auto &asset : rightFacingAssets) {
result.push_back(asset); // Right-facing
result.push_back(mirrorAssetPair(asset)); // Left-facing (mirrored)
}
return result;
}
// Generic template for any asset type with frames and mask
template <typename AssetType>
AssetType mirrorFramedAsset(const AssetType &asset) {
AssetType mirrored;
// Mirror all frames
for (const auto &frame : asset.frames) {
mirrored.frames.push_back(mirrorSprite(frame));
}
// Mirror mask
mirrored.mask = mirrorSprite(asset.mask);
return mirrored;
}
// Create bidirectional assets from single framed asset
template <typename AssetType>
std::vector<AssetType>
createBidirectionalFramedAssets(const AssetType &rightFacingAsset) {
return {
rightFacingAsset, // [0] = Right-facing
mirrorFramedAsset(rightFacingAsset) // [1] = Left-facing (mirrored)
};
}