Skip to content

Instantly share code, notes, and snippets.

@corlaez
Created October 5, 2025 15:30
Show Gist options
  • Select an option

  • Save corlaez/3e25d4f31a097e6777805ec3797b2e6e to your computer and use it in GitHub Desktop.

Select an option

Save corlaez/3e25d4f31a097e6777805ec3797b2e6e to your computer and use it in GitHub Desktop.
Juego 1 nivel. Disparos y game over.
#include <iostream>
#include <vector>
#include <string>
#include <map>
#include <cstdlib> // For system() (e.g., cls or clear)
#include <ctime> // Explicitly include for time(0)
#include <chrono> // For timing/sleep (optional)
#include <thread> // For timing/sleep (optional)
#include <algorithm> // For std::find_if, std::min
#include <cmath> // For std::abs
#include <cctype> // For std::tolower
#include <cstdio> // For putchar/printf
#include <queue> // For BFS pathfinding
#include <tuple> // For BFS pathfinding map key/value
#include <set> // For unique destruction in collision system
// Note: conio.h is included conditionally for Windows input functions
#ifdef _WIN32
#include <conio.h>
#define NOMINMAX // Ensure min/max macros don't conflict with std::min/max
#include <windows.h> // For console functions
#endif
// --- ECS CORE STRUCTURE ---
// Component IDs - Simple integer handles for entities
using EntityID = int;
// Global ID counter
EntityID nextEntityID = 0;
// Global component pools (declarations needed before DestroyEntity and Systems)
std::map<EntityID, struct PositionComponent> positions;
std::map<EntityID, struct CharacterTagComponent> characterTags;
std::map<EntityID, struct VelocityComponent> velocities;
std::map<EntityID, struct MoveDelayComponent> moveDelays; // Delay pool
// Global entity ID for the player for easy access in systems
EntityID playerEntityID = -1;
// Function to create a new entity ID
EntityID CreateEntity() {
return nextEntityID++;
}
// Helper function to remove components of an entity (simple cleanup)
void DestroyEntity(EntityID id) {
// Erase components from all pools they might belong to
positions.erase(id);
characterTags.erase(id);
velocities.erase(id);
moveDelays.erase(id);
}
// --- CONSTANTS AND ENUMS ---
// Console Colors using ANSI Escape Codes
#define ANSI_COLOR_RESET "\033[0m"
// Standard 16-color blue, white, red, yellow, cyan
#define ANSI_COLOR_BLUE "\033[34m"
#define ANSI_COLOR_WHITE "\033[37m"
#define ANSI_COLOR_RED "\033[31m"
#define ANSI_COLOR_YELLOW "\033[33m"
#define ANSI_COLOR_CYAN "\033[36m"
// --- Two Tones of Green (using 256-color codes for better differentiation) ---
// ID 40 is a slightly dull, regular green (good for walkable grass/TIERRA)
#define ANSI_COLOR_LIGHT_GREEN "\033[38;5;40m"
// ID 22 is a deep, forest/dark green (good for non-walkable leaves/HOJA_ARBOL)
#define ANSI_COLOR_DARK_GREEN "\033[38;5;22m"
// Map Dimensions (Updated to 80x35)
const int MAP_WIDTH = 80;
const int MAP_HEIGHT = 35;
// Tile Definitions based on user input
const int TIERRA = 3; // Walkable
const int AGUA = 2; // Not-Walkable
const int HOJA_ARBOL = 5; // Not-Walkable
const int TRONCO = 6; // Not-Walkable
// The solid block character for the map background
const char SOLID_BLOCK_SYMBOL = (char)219;
// Character Model (3x3 array)
const std::string CHARACTER_MODEL[3] = {
" O ",
"/|\\",
"/ \\"
};
const int CHARACTER_WIDTH = 3;
const int CHARACTER_HEIGHT = 3;
// Projectile Model (1x1)
const char PROJECTILE_SYMBOL = '*';
const int PROJECTILE_WIDTH = 1;
const int PROJECTILE_HEIGHT = 1;
// Define the game tick rate (33ms per frame = ~30 FPS)
const std::chrono::milliseconds GAME_TICK_RATE = std::chrono::milliseconds(33);
enum class GameState {
PRE_GAME,
RUNNING
};
GameState currentState = GameState::PRE_GAME;
// Game stats
bool isGameOver = false;
int score = 0;
// STATIC MAP DATA (80x35)
int mapa[][MAP_WIDTH] =
{ {3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,3,3,3,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,3,2,3,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,3,3,5,5,5,5,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,3,3,3,6,6,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,3,3,3,3,3},
{3,3,3,3,6,6,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3,3},
{3,3,3,3,6,6,3,3,3,3,3,3,3,3,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3,3,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3,3,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,3,3,3,3,3,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,3,3,3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,3,3,3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,3,3,3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,3,3,3,3,3,6,6,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,5,5,3},
{3,3,3,3,3,3,6,6,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3},
{3,3,3,3,3,3,6,6,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,3,3,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,5,5,3,3,3,3,3,3,3},
{3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,2,2,2,2,3,3,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3,3,3,3,3},
{3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3,3,3,3,3},
{3,3,5,5,5,5,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3,3,3,3,3},
{3,3,3,6,6,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,5,5,5,5,3,3,3,3,3,3},
{3,3,3,6,6,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3,3,3,3,3,3},
{3,3,3,6,6,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,2,2,2,2,2,2,3,3,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3,3,3,3,3,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,2,2,2,3,3,3,3,3,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,6,6,3,3,3,3,3,3,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3},
{3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3}
};
// --- ANSI Escape Code Utilities ---
const std::string CLEAR_SCREEN_HOME = "\033[H"; // Move cursor to home (1,1)
const std::string CLEAR_TO_EOL = "\033[K"; // Clear from cursor to end of line
// Function to move cursor to (row, col). ANSI is 1-based.
std::string moveCursor(int row, int col) {
// We add 1 to convert from 0-based indexing (C++ array) to 1-based indexing (ANSI)
return "\033[" + std::to_string(row + 1) + ";" + std::to_string(col + 1) + "H";
}
// --- COMPONENTS ---
// 1. PositionComponent: Where the entity is located on the map
struct PositionComponent {
int x;
int y;
int prev_x; // Used for potential future optimization/tracking
int prev_y; // Used for potential future optimization/tracking
};
// 2. MapTileComponent: Data specific to map tiles
struct MapTileComponent {
bool isWalkable;
std::string color;
char symbol;
};
// 3. CharacterTagComponent: Identifies the type of character
enum class CharacterTag {
PLAYER,
ALLY,
ENEMY,
PROJECTILE
};
struct CharacterTagComponent {
CharacterTag tag;
std::string color;
};
// 4. VelocityComponent: For movement rate and direction (used by projectiles)
struct VelocityComponent {
int vx; // velocity X
int vy; // velocity Y
};
// 5. MoveDelayComponent: Used to slow down movement rate (for enemies/allies)
struct MoveDelayComponent {
int delayCounter; // Current tick count
int delayMax; // Ticks required before moving again (e.g., 3 means move every 3rd update)
};
// Represents a position on the map
struct Point {
int x, y;
// Required for use in std::map as a key
bool operator<(const Point& other) const {
if (y != other.y) return y < other.y;
return x < other.x;
}
// Required for use in comparison (like checking visited nodes)
bool operator==(const Point& other) const {
return x == other.x && y == other.y;
}
};
// --- SCREEN BUFFER (The Flicker Eliminator) ---
// Stores the rendered content for a single cell, including color codes.
struct ScreenCell {
std::string content = "";
};
class ScreenBuffer {
private:
std::vector<ScreenCell> current_buffer;
std::vector<ScreenCell> previous_buffer;
public:
ScreenBuffer() :
current_buffer(MAP_WIDTH* MAP_HEIGHT),
previous_buffer(MAP_WIDTH* MAP_HEIGHT)
{
// Initial full clear and setup of the terminal (done only once)
std::cout << "\033[2J"; // Clear screen entirely
std::cout << CLEAR_SCREEN_HOME << std::flush;
// Initialize previous buffer with a sentinel value to force initial full map render
force_repaint();
// Initialize previous buffer with spaces to force initial full map render
/* for (auto& cell : previous_buffer) {
cell.content = " ";
}*/
}
// Forces the differential render to draw the entire screen on the next call.
// This is used for console resize/redraw events to fix graphical issues.
void force_repaint() {
for (auto& cell : previous_buffer) {
// Use a unique sentinel value guaranteed not to be present in normal content
cell.content = "FORCE_REDRAW_SENTINEL";
}
}
// Sets the final rendered content for a specific cell (includes color/reset codes)
void set_cell(int x, int y, const std::string& content) {
if (x >= 0 && x < MAP_WIDTH && y >= 0 && y < MAP_HEIGHT) {
current_buffer[y * MAP_WIDTH + x].content = content;
}
}
// The main function that draws ONLY the differences between frames.
void differential_render() {
bool needs_flush = false;
for (int i = 0; i < current_buffer.size(); ++i) {
// Check if the content (including color codes) has changed
if (current_buffer[i].content != previous_buffer[i].content) {
// Calculate 2D coordinates (y is row, x is col)
int y = i / MAP_WIDTH;
int x = i % MAP_WIDTH;
// 1. Move cursor to the exact position of the change.
std::cout << moveCursor(y, x);
// 2. Print ONLY the new colored content.
// We write directly to cout, bypassing the huge intermediate string build
std::cout << current_buffer[i].content;
// 3. Update the previous buffer state
previous_buffer[i].content = current_buffer[i].content;
needs_flush = true;
}
}
// Return cursor to a safe position for the input prompt (MAP_HEIGHT + 1)
// Clear the line just in case the previous line had leftover content from the map
std::cout << moveCursor(MAP_HEIGHT, 0) << CLEAR_TO_EOL;
// Only flush if we actually drew something
if (needs_flush) {
std::cout << std::flush;
}
}
};
// --- GAME DATA STORAGE (The "World") ---
// Map tiles are entities, but we store them in a 2D array for easy lookup
EntityID mapEntities[MAP_HEIGHT][MAP_WIDTH];
std::map<EntityID, MapTileComponent> mapTiles;
// --- UTILITIES ---
/**
* @brief Checks for AABB collision between two rectangular entities.
* @param pos1 Position component of entity 1 (top-left corner).
* @param w1 Width of entity 1.
* @param h1 Height of entity 1.
* @param pos2 Position component of entity 2 (top-left corner).
* @param w2 Width of entity 2.
* @param h2 Height of entity 2.
* @return true if bounding boxes overlap, false otherwise.
*/
bool CheckCollision(const PositionComponent& pos1, int w1, int h1,
const PositionComponent& pos2, int w2, int h2) {
return (pos1.x < pos2.x + w2 &&
pos1.x + w1 > pos2.x &&
pos1.y < pos2.y + h2 &&
pos1.y + h1 > pos2.y);
}
// Utility function to calculate squared distance between two positions
int GetSquaredDistance(const PositionComponent& p1, const PositionComponent& p2) {
int dx = p1.x - p2.x;
int dy = p1.y - p2.y;
return dx * dx + dy * dy;
}
// --- SYSTEMS ---
// System 1: MapInitializationSystem - Loads the static map data
void MapInitializationSystem() {
std::cout << "Initializing " << MAP_WIDTH << "x" << MAP_HEIGHT << " map from static data..." << std::endl;
for (int y = 0; y < MAP_HEIGHT; ++y) {
for (int x = 0; x < MAP_WIDTH; ++x) {
EntityID tileID = CreateEntity();
mapEntities[y][x] = tileID;
MapTileComponent tile;
tile.symbol = SOLID_BLOCK_SYMBOL; // Always the solid block character
// Determine properties based on the integer code in the static map array
switch (mapa[y][x]) {
case TIERRA: // 3 - Walkable Grass/Field
tile.isWalkable = true;
// Using the new LIGHT GREEN for walkable areas
tile.color = ANSI_COLOR_LIGHT_GREEN;
break;
case AGUA: // 2 - Non-Walkable Water
tile.isWalkable = false;
tile.color = ANSI_COLOR_BLUE;
break;
case HOJA_ARBOL: // 5 - Non-Walkable Leaves/Bushes
tile.isWalkable = false;
// Using the new DARK GREEN for non-walkable areas
tile.color = ANSI_COLOR_DARK_GREEN;
break;
case TRONCO: // 6 - Non-Walkable Trunk
tile.isWalkable = false;
// Using white for TRONCO (approximation for gray/brown)
tile.color = ANSI_COLOR_WHITE;
break;
default:
// Should not happen, but default to safe/walkable
tile.isWalkable = true;
tile.color = ANSI_COLOR_LIGHT_GREEN;
break;
}
mapTiles[tileID] = tile;
}
}
}
// Function to create and initialize a projectile entity
void CreateProjectileEntity(EntityID playerID) {
if (positions.find(playerID) == positions.end()) return;
const auto& playerPos = positions[playerID];
// Check if the player is still present
if (playerID >= nextEntityID) return;
EntityID projectileID = CreateEntity();
// Projectile originates on the left side of the player's 3x3 model.
// Player X is top-left corner, so X-1 is one space left.
// Y is center (playerPos.y + 1)
int startX = playerPos.x - 1;
int startY = playerPos.y + 1;
// Basic boundary check for spawning
// Projectile must spawn *on* the map (0 <= x < MAP_WIDTH)
if (startX < 0 || startX >= MAP_WIDTH || startY < 0 || startY >= MAP_HEIGHT) {
// Destroy the entity if it can't spawn in a valid location
DestroyEntity(projectileID);
return;
}
positions[projectileID] = { startX, startY };
// Projectile moves one step left per tick (vx = -1, vy = 0)
velocities[projectileID] = { -1, 0 };
// Use a white color for the projectile
characterTags[projectileID] = { CharacterTag::PROJECTILE, ANSI_COLOR_WHITE };
}
// System 2: CharacterInitializationSystem
void CharacterInitializationSystem() {
std::cout << "Placing characters on the " << MAP_WIDTH << "x" << MAP_HEIGHT << " map..." << std::endl;
// Player Entity (still one player) - Placed near the center/top-left of the main field
EntityID playerID = CreateEntity();
positions[playerID] = { MAP_WIDTH / 3, MAP_HEIGHT / 4 };
characterTags[playerID] = { CharacterTag::PLAYER, ANSI_COLOR_CYAN };
playerEntityID = playerID; // Store player ID globally
// 3 Allies on the right side
for (int i = 0; i < 3; ++i) {
EntityID allyID = CreateEntity();
// Spawn allies in the bottom-right half
int spawnX = MAP_WIDTH * 3 / 4 + (rand() % 5) - (CHARACTER_WIDTH / 2);
int spawnY = MAP_HEIGHT * 3 / 4 + (rand() % 5);
// Ensure spawn is within bounds
spawnX = std::min(spawnX, MAP_WIDTH - CHARACTER_WIDTH);
spawnY = std::min(spawnY, MAP_HEIGHT - CHARACTER_HEIGHT);
positions[allyID] = { spawnX, spawnY };
characterTags[allyID] = { CharacterTag::ALLY, ANSI_COLOR_YELLOW };
// Allies move randomly, so give them a movement delay (e.g., move once every 5 ticks)
moveDelays[allyID] = { 0, 5 };
}
// 3 Enemies on the left side
for (int i = 0; i < 3; ++i) {
EntityID enemyID = CreateEntity();
// Spawn enemies in the top-left quadrant, but away from the player
int spawnX = MAP_WIDTH / 10 + (rand() % 5);
int spawnY = MAP_HEIGHT / 10 + (i * 4) + (rand() % 2);
// Ensure spawn is within bounds
spawnX = std::min(spawnX, MAP_WIDTH - CHARACTER_WIDTH);
spawnY = std::min(spawnY, MAP_HEIGHT - CHARACTER_HEIGHT);
positions[enemyID] = { spawnX, spawnY };
characterTags[enemyID] = { CharacterTag::ENEMY, ANSI_COLOR_RED };
// Enemies move slowly towards the player (e.g., move once every 3 ticks)
moveDelays[enemyID] = { 0, 3 };
}
}
// System 3: InputSystem (Handles player movement and actions)
void MovementSystem(EntityID playerID, char input) {
if (positions.find(playerID) == positions.end()) return;
// Check for non-movement actions first (like shooting)
if (input == ' ') { // Spacebar pressed
CreateProjectileEntity(playerID);
return;
}
int newX = positions[playerID].x;
int newY = positions[playerID].y;
// Calculate new position based on input
switch (input) {
case 'w': newY--; break;
case 's': newY++; break;
case 'a': newX--; break;
case 'd': newX++; break;
default: return; // No movement
}
// Boundary check (PREVENTS CHARACTER FROM VENTURING OFF THE CORNERS)
// The top-left corner (X, Y) must stay within:
// X: [0, MAP_WIDTH - CHARACTER_WIDTH]
// Y: [0, MAP_HEIGHT - CHARACTER_HEIGHT]
if (newX < 0 || newX > MAP_WIDTH - CHARACTER_WIDTH ||
newY < 0 || newY > MAP_HEIGHT - CHARACTER_HEIGHT) {
return;
}
// Walkability check (Checking the 3x3 area for non-walkable tiles)
for (int y_off = 0; y_off < CHARACTER_HEIGHT; ++y_off) {
for (int x_off = 0; x_off < CHARACTER_WIDTH; ++x_off) {
// Get the map tile entity ID at the new position
EntityID tileID = mapEntities[newY + y_off][newX + x_off];
// Check its walkability component
if (mapTiles.find(tileID) != mapTiles.end() && !mapTiles[tileID].isWalkable) {
// If any tile in the 3x3 area is not walkable, block movement
return;
}
}
}
// Apply movement if checks pass
positions[playerID].x = newX;
positions[playerID].y = newY;
}
// System 3.5: ProjectileSystem - Updates position and handles destruction
void ProjectileSystem() {
std::vector<EntityID> entitiesToDestroy;
// Copy IDs to iterate safely while modifying the map
std::vector<EntityID> activeVelocities;
for (const auto& pair : velocities) {
activeVelocities.push_back(pair.first);
}
for (EntityID id : activeVelocities) {
// Ensure the entity still exists and has all required components
if (velocities.count(id) == 0 || positions.count(id) == 0) continue;
VelocityComponent& vel = velocities[id];
PositionComponent& pos = positions[id];
// Only run destruction logic for projectiles (using the tag as a filter)
if (characterTags.count(id) && characterTags[id].tag == CharacterTag::PROJECTILE) {
// Apply velocity
pos.x += vel.vx;
pos.y += vel.vy;
// Check for map boundary destruction (goes off the left side)
if (pos.x < 0) {
entitiesToDestroy.push_back(id);
continue; // Move to the next entity
}
// Check if the projectile is hitting a non-walkable tile (optional: hitting an enemy could go here too)
// Get the map tile entity ID at the new projectile position (1x1)
if (pos.y >= 0 && pos.y < MAP_HEIGHT && pos.x >= 0 && pos.x < MAP_WIDTH) {
EntityID tileID = mapEntities[pos.y][pos.x];
if (mapTiles.count(tileID) && !mapTiles[tileID].isWalkable) {
// Hit an obstacle, destroy projectile
entitiesToDestroy.push_back(id);
continue;
}
}
else {
// Out of bounds (if it didn't hit x < 0 boundary already)
entitiesToDestroy.push_back(id);
continue;
}
}
}
// Clean up destroyed entities
for (EntityID id : entitiesToDestroy) {
DestroyEntity(id);
}
}
/**
* @brief Checks if a 3x3 area starting at (startX, startY) is fully walkable.
* @param startX Top-left X coordinate of the 3x3 area.
* @param startY Top-left Y coordinate of the 3x3 area.
* @return true if the area is within bounds and all 9 tiles are walkable, false otherwise.
*/
bool is_walkable_area(int startX, int startY) {
// Check main boundaries for the 3x3 area
if (startX < 0 || startX > MAP_WIDTH - CHARACTER_WIDTH ||
startY < 0 || startY > MAP_HEIGHT - CHARACTER_HEIGHT) {
return false;
}
// Check individual tile walkability
for (int y_off = 0; y_off < CHARACTER_HEIGHT; ++y_off) {
for (int x_off = 0; x_off < CHARACTER_WIDTH; ++x_off) {
int mapY = startY + y_off;
int mapX = startX + x_off;
EntityID tileID = mapEntities[mapY][mapX];
if (mapTiles.find(tileID) == mapTiles.end() || !mapTiles[tileID].isWalkable) {
return false;
}
}
}
return true;
}
/**
* @brief Finds the next best movement (dx, dy) for a 3x3 entity using BFS.
* @param startPos The entity's current position (top-left X, Y).
* @param targetPos The target entity's position (top-left X, Y).
* @return A Point representing the relative movement (dx, dy). Returns {0, 0} if no path is found.
*/
Point FindNextMoveBFS(const Point& startPos, const Point& targetPos) {
// Using 4 cardinal directions for movement
const Point directions[] = {
{0, -1}, {0, 1}, {-1, 0}, {1, 0} // N, S, W, E
// Optional: include diagonals for 8-way movement:
// {-1, -1}, {1, -1}, {-1, 1}, {1, 1}
};
// --- BFS Setup ---
std::queue<Point> frontier;
// Maps a Point to its immediate parent on the shortest path (for path reconstruction)
std::map<Point, Point> came_from;
frontier.push(startPos);
came_from[startPos] = startPos; // Sentinel value indicating the start node
Point found_target_step = { 0, 0 }; // Stores the step *before* the target
// --- Core BFS Loop ---
while (!frontier.empty()) {
Point current = frontier.front();
frontier.pop();
// Safety break for excessive search (e.g., if target is unreachable)
if (came_from.size() > 1000) break;
// Check 4 cardinal neighbors
for (const auto& dir : directions) {
Point next = { current.x + dir.x, current.y + dir.y };
// 1. Boundary and Walkability Check (3x3 area)
if (!is_walkable_area(next.x, next.y)) {
continue;
}
// 2. Visited Check
if (came_from.count(next)) {
continue;
}
// 3. Goal Check
// A path is found if the next position is the target position.
// Since the target is 3x3, we accept if we reach its top-left corner.
if (next == targetPos) {
// Path found. Reconstruct the first step from the start.
// Trace back from the target (current) until we hit the immediate neighbor of startPos
Point trace_current = current;
Point trace_parent = came_from[trace_current];
// Loop backward until the parent is the start position itself
while (trace_parent.x != startPos.x || trace_parent.y != startPos.y) {
trace_current = trace_parent;
trace_parent = came_from[trace_current];
}
// trace_current is now the first step from startPos
found_target_step = trace_current;
// The movement vector is the difference between the first step and the start
return { found_target_step.x - startPos.x, found_target_step.y - startPos.y };
}
// 4. Queue and Mark Visited
frontier.push(next);
came_from[next] = current;
}
}
// If loop finishes without finding a path
return { 0, 0 };
}
// System 4: AISystem - Handles autonomous movement for enemies and allies
void AISystem() {
// Collect all dynamic entities to iterate safely
std::vector<EntityID> dynamicEntities;
for (const auto& pair : characterTags) {
dynamicEntities.push_back(pair.first);
}
for (EntityID entityID : dynamicEntities) {
// Only process entities that are still alive and have movement logic
if (characterTags.count(entityID) == 0 || positions.count(entityID) == 0 || moveDelays.count(entityID) == 0) {
continue;
}
const auto& tag = characterTags[entityID];
MoveDelayComponent& delay = moveDelays[entityID];
// Apply slowdown logic
delay.delayCounter++;
if (delay.delayCounter < delay.delayMax) continue;
delay.delayCounter = 0; // Reset counter for movement
int moveX = 0;
int moveY = 0;
const auto& currentPos = positions[entityID];
if (tag.tag == CharacterTag::ENEMY) {
// Find closest target (Player or Ally)
EntityID closestTargetID = -1;
int minDistanceSq = 999999;
PositionComponent targetPos = { 0, 0 };
for (const auto& targetPair : characterTags) {
EntityID targetID = targetPair.first;
const auto& targetTag = targetPair.second;
if (positions.count(targetID) == 0) continue;
// Target is Player or Ally
if (targetTag.tag == CharacterTag::PLAYER || targetTag.tag == CharacterTag::ALLY) {
const auto& potentialTargetPos = positions[targetID];
int distSq = GetSquaredDistance(currentPos, potentialTargetPos);
if (distSq < minDistanceSq) {
minDistanceSq = distSq;
closestTargetID = targetID;
targetPos = potentialTargetPos;
}
}
}
// --- NEW PATHFINDING LOGIC FOR ENEMIES ---
if (closestTargetID != -1) {
Point start = { currentPos.x, currentPos.y };
Point target = { targetPos.x, targetPos.y };
// Use BFS to get the next movement step
Point nextMove = FindNextMoveBFS(start, target);
moveX = nextMove.x;
moveY = nextMove.y;
// If BFS returns {0, 0}, it either means we are already next to the target
// or the target is completely unreachable.
// We let moveX/moveY remain 0 and the enemy won't move this turn.
}
}
else if (tag.tag == CharacterTag::ALLY) {
// Random movement: -1, 0, or 1 for X and Y
moveX = (rand() % 3) - 1; // -1, 0, 1
moveY = (rand() % 3) - 1; // -1, 0, 1
}
// --- Apply Movement and Collision Check for 3x3 Characters ---
if (moveX != 0 || moveY != 0) {
int newX = currentPos.x + moveX;
int newY = currentPos.y + moveY;
// AI characters (3x3) use the same full boundary check as the player
bool boundary_ok = (newX >= 0 && newX <= MAP_WIDTH - CHARACTER_WIDTH &&
newY >= 0 && newY <= MAP_HEIGHT - CHARACTER_HEIGHT);
if (tag.tag == CharacterTag::ALLY && newX < MAP_WIDTH / 2) {
// Constraint: Allies must stay on the right side of the map (right of column 40)
boundary_ok = newX >= MAP_WIDTH / 2;
}
if (boundary_ok) {
bool walkable = true;
// Check the 3x3 area for non-walkable tiles
for (int y_off = 0; y_off < CHARACTER_HEIGHT; ++y_off) {
for (int x_off = 0; x_off < CHARACTER_WIDTH; ++x_off) {
EntityID tileID = mapEntities[newY + y_off][newX + x_off];
if (mapTiles.count(tileID) && !mapTiles[tileID].isWalkable) {
walkable = false;
break;
}
}
if (!walkable) break;
}
if (walkable) {
positions[entityID].x = newX;
positions[entityID].y = newY;
}
}
}
}
}
// System 5: CollisionSystem
void CollisionSystem() {
if (isGameOver) return;
std::set<EntityID> uniqueEntitiesToDestroy;
bool playerWasHit = false;
// --- 1. Projectile vs. Enemy ---
for (const auto& projPair : characterTags) {
EntityID projID = projPair.first;
if (projPair.second.tag != CharacterTag::PROJECTILE) continue;
if (positions.count(projID) == 0) continue;
const auto& projPos = positions[projID];
for (const auto& enemyPair : characterTags) {
EntityID enemyID = enemyPair.first;
if (enemyPair.second.tag != CharacterTag::ENEMY) continue;
if (positions.count(enemyID) == 0) continue;
// Skip if enemy is already targeted for destruction by another projectile this tick
if (uniqueEntitiesToDestroy.count(enemyID)) continue;
const auto& enemyPos = positions[enemyID];
if (CheckCollision(projPos, PROJECTILE_WIDTH, PROJECTILE_HEIGHT,
enemyPos, CHARACTER_WIDTH, CHARACTER_HEIGHT)) {
// Projectile hits Enemy: Destroy both
uniqueEntitiesToDestroy.insert(projID);
uniqueEntitiesToDestroy.insert(enemyID);
score += 10;
// Break inner loop since this projectile is destroyed
break;
}
}
}
// --- 2. Enemy vs. Player & 3. Enemy vs. Ally ---
// Separate active enemies from other 3x3 characters (excluding those marked for destruction)
std::vector<EntityID> activeEnemies;
std::vector<EntityID> activeTargets;
for (const auto& pair : characterTags) {
if (uniqueEntitiesToDestroy.count(pair.first)) continue; // Skip already destroyed entities
if (pair.second.tag == CharacterTag::ENEMY) {
activeEnemies.push_back(pair.first);
}
else if (pair.second.tag == CharacterTag::PLAYER || pair.second.tag == CharacterTag::ALLY) {
activeTargets.push_back(pair.first);
}
}
// Check Enemy collisions against targets
for (EntityID enemyID : activeEnemies) {
if (positions.count(enemyID) == 0) continue;
const auto& enemyPos = positions[enemyID];
for (EntityID targetID : activeTargets) {
if (positions.count(targetID) == 0) continue;
if (uniqueEntitiesToDestroy.count(targetID)) continue;
const auto& targetPos = positions[targetID];
const auto& targetTag = characterTags[targetID].tag;
if (CheckCollision(enemyPos, CHARACTER_WIDTH, CHARACTER_HEIGHT,
targetPos, CHARACTER_WIDTH, CHARACTER_HEIGHT)) {
// Enemy vs. Player: Game Over
if (targetTag == CharacterTag::PLAYER) {
playerWasHit = true;
uniqueEntitiesToDestroy.insert(enemyID);
uniqueEntitiesToDestroy.insert(targetID);
break; // Stop processing targets and enemies
}
// Enemy vs. Ally
else if (targetTag == CharacterTag::ALLY) {
uniqueEntitiesToDestroy.insert(enemyID);
uniqueEntitiesToDestroy.insert(targetID);
break; // Move to next enemy
}
}
}
if (playerWasHit) break;
}
// Apply Game Over and destruction
if (playerWasHit) {
isGameOver = true;
}
// Apply destruction
for (EntityID id : uniqueEntitiesToDestroy) {
DestroyEntity(id);
}
}
// System 6: RenderSystem (Now uses Differential Rendering)
void RenderSystem(ScreenBuffer& buffer) {
// We REMOVE the system("cls") / system("clear") calls here.
// 1. Build the current Screen Buffer with MapTiles (Background)
for (int y = 0; y < MAP_HEIGHT; ++y) {
for (int x = 0; x < MAP_WIDTH; ++x) {
EntityID tileID = mapEntities[y][x];
if (mapTiles.find(tileID) != mapTiles.end()) {
const auto& tile = mapTiles[tileID];
// Store the colored symbol string in the buffer
std::string colored_tile = tile.color + tile.symbol + ANSI_COLOR_RESET;
buffer.set_cell(x, y, colored_tile);
}
}
}
// 2. Draw Characters and Projectiles on top of the buffer (Foreground)
// Iterate over entities that have both a PositionComponent and a CharacterTagComponent
for (const auto& pair : positions) {
EntityID entityID = pair.first;
if (characterTags.find(entityID) != characterTags.end()) {
const auto& pos = pair.second;
const auto& tag = characterTags[entityID];
int startX = pos.x;
int startY = pos.y;
// Determine size and model based on tag
const int modelWidth = (tag.tag == CharacterTag::PROJECTILE) ? PROJECTILE_WIDTH : CHARACTER_WIDTH;
const int modelHeight = (tag.tag == CharacterTag::PROJECTILE) ? PROJECTILE_HEIGHT : CHARACTER_HEIGHT;
// Loop through the entity's model area
for (int cy = 0; cy < modelHeight; ++cy) {
for (int cx = 0; cx < modelWidth; ++cx) {
char charToDraw;
if (tag.tag == CharacterTag::PROJECTILE) {
charToDraw = PROJECTILE_SYMBOL;
}
else {
// Standard 3x3 character model
charToDraw = CHARACTER_MODEL[cy][cx];
}
// Check if the character model segment is not a space
if (charToDraw != ' ') {
int mapX = startX + cx;
int mapY = startY + cy;
// Check map boundaries
if (mapX >= 0 && mapX < MAP_WIDTH && mapY >= 0 && mapY < MAP_HEIGHT) {
// OVERWRITE the map tile in the buffer with the entity's colored symbol
std::string colored_char = tag.color + charToDraw + ANSI_COLOR_RESET;
buffer.set_cell(mapX, mapY, colored_char);
}
}
}
}
}
}
// 3. Differential Render: Print ONLY the cells that changed.
buffer.differential_render();
// 3. Differential Render: Print ONLY the cells that changed, and display the UI message.
std::string ui_message = "";
if (currentState == GameState::PRE_GAME) {
ui_message = ANSI_COLOR_YELLOW;
ui_message += "--- PRE-GAME SETUP --- | Press [SPACE] to start the game | Press [R] to repaint the map | Press [Q] to quit";
ui_message += ANSI_COLOR_RESET;
}
else {
ui_message = ANSI_COLOR_CYAN;
ui_message += "Map Size: " + std::to_string(MAP_WIDTH) + "x" + std::to_string(MAP_HEIGHT) + " | Move (W/A/S/D) | Shoot (SPACE) | Quit (Q)";
ui_message += ANSI_COLOR_RESET;
}
// 4. Print UI below the map without scrolling the map (using the cleared line from the buffer)
//std::cout << "--------------------------------------------------------------------------------" << std::endl;
//std::cout << "Map Size: " << MAP_WIDTH << "x" << MAP_HEIGHT << " | Move (W/A/S/D) | Shoot (SPACE) | Quit (Q)" << std::endl;
//std::cout << ">> Waiting for input..." << std::flush;
}
// System 7: RenderHUDSystem - Draws score and messages
void RenderHUDSystem() {
// Row for HUD (below the map)
int hudRow = MAP_HEIGHT + 1;
// Game Over Message
if (isGameOver) {
std::cout << moveCursor(hudRow, 0) << CLEAR_TO_EOL;
std::cout << ANSI_COLOR_RED << "!!! GAME OVER !!! Enemy reached the player. Press 'Q' to restart." << ANSI_COLOR_RESET;
hudRow++;
}
// Score Display
std::string scoreDisplay = std::string(ANSI_COLOR_YELLOW) + "Score: " + std::to_string(score) + std::string(ANSI_COLOR_RESET);
// Output status message to console
std::cout << moveCursor(hudRow, 0) << CLEAR_TO_EOL << scoreDisplay << ANSI_COLOR_RESET;
hudRow++;
// Controls Display
if (!isGameOver) {
std::cout << moveCursor(hudRow, 0) << CLEAR_TO_EOL;
std::cout << ANSI_COLOR_CYAN << "Controls: W/A/S/D to move, SPACE to shoot." << ANSI_COLOR_RESET;
}
hudRow++;
// Final cursor position for input
std::cout << moveCursor(hudRow, 0) << CLEAR_TO_EOL << "> " << std::flush;
}
// Function to get non-blocking input
char GetNonBlockingInput() {
char input = '\0'; // Default to null char (no input)
#ifdef _WIN32
// Check if a key is waiting in the buffer
if (_kbhit()) {
// Read the key without blocking
input = _getch();
// Convert to lowercase for case-insensitive controls
input = static_cast<char>(std::tolower(static_cast<unsigned char>(input)));
}
#endif
// For non-Windows environments, input remains '\0',
// ensuring the game continues to tick.
return input;
}
/**
* @brief Resizes the console window and buffer to fit the map and instructions.
* This is primarily for running on the classic Windows console (cmd.exe)
*/
void resizeConsoleWindow() {
// Target dimensions for map (80x35) + instructions/padding
const int CONSOLE_WIDTH = MAP_WIDTH + 4; // 84
const int CONSOLE_HEIGHT = MAP_HEIGHT + 10; // Enough space for prompt
// Minimum height is necessary to prevent scrolling which would cause flicker
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (hOut == INVALID_HANDLE_VALUE || hOut == NULL) return;
// Get maximum console window dimensions available based on screen size
COORD maxWindow = GetLargestConsoleWindowSize(hOut);
// Ensure the desired size is not larger than the largest possible size
int finalWidth = std::min(CONSOLE_WIDTH, (int)maxWindow.X);
int finalHeight = std::min(CONSOLE_HEIGHT, (int)maxWindow.Y);
// 1. Define the new screen buffer size
COORD newSize = { (short)finalWidth, (short)finalHeight };
// Set the screen buffer size (must be done before setting the window size)
if (!SetConsoleScreenBufferSize(hOut, newSize)) {
return;
}
// 2. Define the new console window rectangle (size is always one unit less than buffer size)
SMALL_RECT rect = { 0, 0, (short)(finalWidth - 1), (short)(finalHeight - 1) };
// Set the console window size
SetConsoleWindowInfo(hOut, TRUE, &rect);
}
// Function to reset the game state
void RestartGame() {
// 1. Reset Global State
isGameOver = false;
score = 0;
playerEntityID = -1; // Reset player ID
// 2. Clear all dynamic component pools
positions.clear();
characterTags.clear();
velocities.clear();
moveDelays.clear();
// 3. Re-initialize systems (Map is first to reset nextEntityID and occupy map IDs)
MapInitializationSystem();
CharacterInitializationSystem();
// Clear the screen and force a full redraw
std::cout << "\033[2J";
}
// --- MAIN GAME LOOP ---
void GameLoop() {
// 1. Setup
MapInitializationSystem();
ScreenBuffer screenBuffer;
CharacterInitializationSystem();
while (currentState == GameState::PRE_GAME) {
char input = GetNonBlockingInput();
if (input == 'q') {
return; // Quit the application
}
else if (input == ' ' || input == 'w' || input == 'a' || input == 's' || input == 'd') {
// Requirement met: The game begins on the first space press.
currentState = GameState::RUNNING;
screenBuffer.force_repaint(); // Force a clean redraw on state change
// The main loop will start immediately, no break needed.
}
else if (input == 'r') {
resizeConsoleWindow();
// Requirement met: Repaint the entire map (simulating resize fix).
screenBuffer.force_repaint();
RenderSystem(screenBuffer);
}
// Wait a short time to prevent maxing out CPU while polling input
std::this_thread::sleep_for(std::chrono::milliseconds(50));
}
auto lastTime = std::chrono::high_resolution_clock::now();
// 2. Main Loop
while (currentState == GameState::RUNNING) {
// A. Get Input (Non-Blocking)
char input = GetNonBlockingInput();
// Check for quit first
if (input == 'q') break;
// B. Game Logic Update (This runs every tick, even if input is '\0')
// Process movement and input actions (Player)
MovementSystem(playerEntityID, input);
// --- 2. Input Handling ---
if (isGameOver) {
if (input == 'q') {
RestartGame();
continue; // Skip game logic until next tick
}
// Game over, only 'q' is processed, render HUD and wait
}
else {
// Process Movement only if game is running
MovementSystem(playerEntityID, input);
}
// Process AI movement (Enemies/Allies)
//AISystem();
// Process projectile movement
//ProjectileSystem();
// C. Draw the current state
//RenderSystem(screenBuffer);
// D. Fixed Time Step Delay (Ensures constant speed/tick rate)
//std::this_thread::sleep_for(GAME_TICK_RATE);
// --- 3. Update (Game Logic) ---
auto currentTime = std::chrono::high_resolution_clock::now();
auto elapsedTime = std::chrono::duration_cast<std::chrono::milliseconds>(currentTime - lastTime);
if (elapsedTime >= GAME_TICK_RATE) {
if (!isGameOver) {
ProjectileSystem(); // Move projectiles, check wall/boundary
AISystem(); // Move enemies/allies (handles map collisions)
CollisionSystem(); // Handles Projectile-Enemy, Enemy-Ally, Enemy-Player collisions
}
// --- 4. Render ---
RenderSystem(screenBuffer);
RenderHUDSystem();
lastTime = currentTime;
}
// Small sleep to prevent CPU hogging if tick rate is high and no input occurs
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
// --- WINDOWS ANSI ENABLE FIX AND MACRO CONFLICT FIX ---
#ifdef _WIN32
// FIX 1: Define NOMINMAX to prevent Windows from defining min/max macros that conflict with C++ standard library functions/syntax.
// NOTE: NOMINMAX is defined globally near the top of the file
void enableVTMode() {
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
if (hOut == INVALID_HANDLE_VALUE) return;
DWORD dwMode = 0;
if (!GetConsoleMode(hOut, &dwMode)) return;
dwMode |= ENABLE_VIRTUAL_TERMINAL_PROCESSING;
SetConsoleMode(hOut, dwMode);
}
#endif // _WIN32
int main() {
// Seed the random number generator once
srand(static_cast<unsigned int>(time(0)));
// --- FIX: Enable Virtual Terminal Processing for ANSI 256 Colors on Windows ---
#ifdef _WIN32
enableVTMode();
resizeConsoleWindow();
// Ensures 'echo off' is run once for cmd.exe environments
// Note: this may be ignored in modern terminals like VS Code Terminal/PowerShell
system("echo off");
#endif
// Hide the cursor during rendering to prevent it from flickering on every cell update
std::cout << "\033[?25l"; // ANSI code to hide cursor
GameLoop();
// Restore the cursor before exiting
std::cout << "\033[?25h"; // ANSI code to show cursor
std::cout << ANSI_COLOR_RESET << "\n\nGame Over. Thanks for playing!" << std::endl;
return 0;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment