I went on a bit of a terminal ANSI color spree.
Note: I will be using Unreal’s conventions when I am actually in Unreal
#include <iostream>
#include <string>
#include <array>
#include <chrono>
#include <random>
#if WIN32
#include <windows.h>
bool EnableAnsiEscapes() {
HANDLE hConsole = GetStdHandle(STD_OUTPUT_HANDLE);
if(hConsole != INVALID_HANDLE_VALUE) {
DWORD currentMode = 0;
if(GetConsoleMode(hConsole, ¤tMode)) {
SetConsoleMode(hConsole, currentMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
}
}
// Used to return false on error, but that breaks support for terminals like MINGW, so if the Windows version
// does not support ansi escape codes, ANSI support will not be disabled, so ANSI escapes will still be printed.
return true;
}
#else
bool EnableAnsiEscapes() { return true; }
#endif //WIN32
bool ansiSupported = true;
std::mt19937 random;
namespace Ansi {
inline std::string Reset() {
if (!ansiSupported)
return "";
return "\033[0m";
}
inline std::string Escape(const std::string& flag, const std::string& val) {
if (!ansiSupported)
return "";
return "\x1B[" + flag + val + "m";
}
std::string Clear() {
if (!ansiSupported)
return "\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n";
// Clear whole screen and reset cursor to 0, 0
return Reset() + "\x1B[2J\x1B[0;0H";
}
std::string Bg(int val) {
return Escape("48;5;", std::to_string(val));
}
std::string Fg(int val) {
return Escape("38;5;", std::to_string(val));
}
}
long GetTime() {
auto now = std::chrono::system_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(now.time_since_epoch()).count();
}
void DrawCorrect() {
auto start = GetTime();
bool redraw = true;
bool invert = false;
auto last = GetTime();
while (GetTime() - start < 1500) {
if (GetTime() - last > 500) {
invert = !invert;
redraw = true;
}
if (redraw) {
std::cout << Ansi::Clear();
if (!invert)
std::cout << Ansi::Fg(2);
else
std::cout << Ansi::Fg(0) << Ansi::Bg(2);
std::cout << R"(/=======================================\)" << "\n"
<< R"(| _____ _ |)" << "\n"
<< R"(| / ____| | | |)" << "\n"
<< R"(| | | ___ _ __ _ __ ___ ___| |_ |)" << "\n"
<< R"(| | | / _ \| '__| '__/ _ \/ __| __| |)" << "\n"
<< R"(| | |___| (_) | | | | | __/ (__| |_ |)" << "\n"
<< R"(| \_____\___/|_| |_| \___|\___|\__| |)" << "\n"
<< R"(| |)" << "\n"
<< R"(\=======================================/)" << Ansi::Reset() << std::endl;
last = GetTime();
redraw = false;
}
}
}
void DrawWrong(std::array<int, 3> numbers) {
std::string correctNumbers;
for(int num : numbers) {
if(!correctNumbers.empty())
correctNumbers += ", ";
correctNumbers += std::to_string(num);
}
auto start = GetTime();
bool redraw = true;
bool invert = false;
auto last = GetTime();
while (GetTime() - start < 2500) {
if (GetTime() - last > 500) {
invert = !invert;
redraw = true;
}
if (redraw) {
std::cout << Ansi::Clear();
std::cout << "The correct numbers were: " << correctNumbers << "\n\n";
if (!invert)
std::cout << Ansi::Fg(1);
else
std::cout << Ansi::Fg(0) << Ansi::Bg(1);
std::cout << R"(/======================================\)" << "\n"
<< R"(| __ __ |)" << "\n"
<< R"(| \ \ / / |)" << "\n"
<< R"(| \ \ /\ / / __ ___ _ __ __ _ |)" << "\n"
<< R"(| \ \/ \/ / '__/ _ \| '_ \ / _` | |)" << "\n"
<< R"(| \ /\ /| | | (_) | | | | (_| | |)" << "\n"
<< R"(| \/ \/ |_| \___/|_| |_|\__, | |)" << "\n"
<< R"(| __/ | |)" << "\n"
<< R"(\======================================/)" << Ansi::Reset() << std::endl;
last = GetTime();
redraw = false;
}
}
}
bool SimulateGame(int level, int lives) {
std::cout << Ansi::Fg(3) << "You are in a bank trying to break into a " << Ansi::Fg(5) << "level " << level
<< Ansi::Fg(3) << " vault... \n"
<< Ansi::Fg(3) << "You have " << Ansi::Fg(5) << lives << Ansi::Fg(3) << " lives remaining.\n"
<< "Enter the correct code to continue..." << Ansi::Reset() << "\n\n";
// Generates a random distribution based on level and how many lives the player lost
std::uniform_int_distribution<int> distrib(level + (level - 1), level * 2 - ((3 - lives) / 3));
const std::array<int, 3> numbers{distrib(random), distrib(random), distrib(random)};
const int sum = numbers[0] + numbers[1] + numbers[2];
const int product = numbers[0] * numbers[1] * numbers[2];
std::cout << Ansi::Fg(3) << "* There are three numbers in the code\n"
<< "* The codes add-up to: " << Ansi::Fg(5) << sum << Ansi::Fg(3) << "\n"
<< "* The codes multiply to give: " << Ansi::Fg(5) << product << Ansi::Fg(3) << "\n"
<< "\nPlease enter your guess: " << Ansi::Fg(5);
std::flush(std::cout);
std::array<int, 3> guesses{};
for (int& guess : guesses) {
std::cin >> guess;
std::cin.clear(); // Clear any potential errors
}
const int guessSum = guesses[0] + guesses[1] + guesses[2];
const int guessProduct = guesses[0] * guesses[1] * guesses[2];
bool wasCorrect = false;
std::cout << "\n";
if (sum == guessSum && product == guessProduct) {
DrawCorrect();
wasCorrect = true;
} else {
DrawWrong(numbers);
}
std::cout << Ansi::Reset() << std::endl;
return wasCorrect;
}
int main() {
#if WIN32
ansiSupported = EnableAnsiEscapes();
#endif //WIN32
// Initialize randomizer
random = std::mt19937(GetTime());
constexpr int maxLevel = 10;
int level = 1;
int lives = 3;
while (level <= maxLevel) {
std::cout << Ansi::Clear();
bool correct = SimulateGame(level, lives);
std::cin.ignore(std::cin.rdbuf()->in_avail());
std::cout << Ansi::Clear();
if (correct)
level++;
else {
lives--;
}
if (lives <= 0) {
std::cout << Ansi::Fg(1) << "The police have found you, game over!";
break;
}
}
if (level > maxLevel)
std::cout << Ansi::Fg(2) << "You have successfully opened the last vault, the riches are yours!";
// Always reset the console formatting at the end, some consoles don't do it for us
// looking at you powershell
std::cout << Ansi::Reset() << std::endl;
std::cin.clear();
std::cin.ignore(std::cin.rdbuf()->in_avail());
std::cout << "\nPress enter to exit...";
std::cin.get(); // Prevent terminal from closing until player presses enter
return 0;
}
This should work correctly on all platforms that support ANSI color codes (including Windows 10), unless I missed something obvious.