Last active
February 8, 2024 09:29
-
-
Save b3x206/bd64ae205e38bbea2298b915451e0f59 to your computer and use it in GitHub Desktop.
a bad snake for console
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is snake for windows console that can be compiled using mingw | |
// Compile using => $ g++ ./terrible_snake.cpp -o snake.exe <optional flags> | |
// (msvc will probably work, didn't test though) | |
// Features : | |
// * Has color | |
// * Is playable (wow!!1!) | |
// * Has argument options | |
// * Runs fine (unless you make the console larger because buffering is not done by the winapi but by std::cout which sucks, but the winapi requires buffer sizing-juggling so idk) | |
// * Single-file (but that is indeed a stretch, AppArgument is a .h + .cpp file because the linker says no [symbol AAÆ@@@@=DFAghsdao234<<<__<<< on terrible_snake.obj not found or some thing idk]) | |
// * Github gist editor also has a new feature called "mix tabs with spaces™", thanks. | |
#include <iostream> // I/O console | |
#include <thread> // Threading | |
#include <chrono> // Time (threading) | |
#include <vector> // Arrays (why call it vector) | |
#include <cmath> // Math (unused except for debug) | |
// #include <fstream> // Serialization (for high score, this is still TODO) | |
#include <functional> // Argument delegates | |
#include <string> | |
#include <sstream> | |
#include <windows.h> // Windows API (could use for console buffer, used for console cursor instead lol) | |
#include <conio.h> // Input side of I/O console | |
// Because winapi is very good and defines these as macros | |
#undef min | |
#undef max | |
// Winapi mostly uses 'WORD' for colors, not for pointers (which the stdint.h numbers are incompatible with) | |
typedef uint16_t WORD; | |
// TODO : Lower code re-use (by doing methods/prev-next methods and/or having a snake::update() method) | |
// Settings | |
int ms_delay_draw = 80; // Delay (in milliseconds) | |
int64_t area_sz_x = 20, area_sz_y = 10; // 20 x 10 area | |
std::string exit_message = "You lost."; // Message printing for exit. (game over message by default) | |
// TODO 2 : stage implement | |
// Note : The 'V0G0N' escape sequence is for allowing () characters in the string | |
// const char* stage = R"V0G0N( | |
// | |
// )V0G0N"; | |
// Windows stuff | |
// (can be ported to linux / terminals using ansi escape chars, i compile this using gcc anyways) | |
// (look who forgot about conio lol) | |
namespace console | |
{ | |
// @brief The StdOut handle used for the console, for the winapi methods. | |
// This can be gathered in functions (as it's a light api call) as well but it is ok as is.a | |
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); | |
// @brief Color defined for the windows consoles. Unlike ansi escapes, these are more limited. | |
// (but ansi terminal escapes are hell to implement so have to live with this subset..) | |
enum class Color : WORD | |
{ | |
BLACK = 0, | |
DARKBLUE = FOREGROUND_BLUE, | |
DARKGREEN = FOREGROUND_GREEN, | |
DARKCYAN = FOREGROUND_GREEN | FOREGROUND_BLUE, | |
DARKRED = FOREGROUND_RED, | |
DARKMAGENTA = FOREGROUND_RED | FOREGROUND_BLUE, | |
DARKYELLOW = FOREGROUND_RED | FOREGROUND_GREEN, | |
DARKGRAY = FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, | |
GRAY = FOREGROUND_INTENSITY, | |
BLUE = FOREGROUND_INTENSITY | FOREGROUND_BLUE, | |
GREEN = FOREGROUND_INTENSITY | FOREGROUND_GREEN, | |
CYAN = FOREGROUND_INTENSITY | FOREGROUND_GREEN | FOREGROUND_BLUE, | |
RED = FOREGROUND_INTENSITY | FOREGROUND_RED, | |
MAGENTA = FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_BLUE, | |
YELLOW = FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_GREEN, | |
WHITE = FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE, | |
}; | |
// Note : Smaller data structures are fine to pass-by-copy, in fact it is faster to pass by copy for smaller datas. | |
// @brief Clears the console screen with the given 'fill' parameter. | |
// @param fill : The character to fill the screen with for clearing. This is an ascii character. | |
void clear(const char fill = ' ') | |
{ | |
CONSOLE_SCREEN_BUFFER_INFO screen; | |
DWORD written; | |
// Should use an actual buffer like this instead of std cout | |
GetConsoleScreenBufferInfo(handle, &screen); | |
FillConsoleOutputCharacterA(handle, fill, screen.dwSize.X * screen.dwSize.Y, { 0, 0 }, &written); | |
FillConsoleOutputAttribute(handle, (WORD)Color::WHITE, screen.dwSize.X * screen.dwSize.Y, { 0, 0 }, &written); | |
SetConsoleCursorPosition(handle, { 0, 0 }); | |
} | |
// @brief Sets the console cursor/caret visibility | |
// @param visible : Whether to hide/show the cursor. | |
void visibility_cursor(const bool visible) | |
{ | |
CONSOLE_CURSOR_INFO info; | |
GetConsoleCursorInfo(handle, &info); | |
info.bVisible = visible; | |
info.dwSize = 100; // dunno what this is | |
SetConsoleCursorInfo(handle, &info); | |
} | |
// @brief Sets the console cursor position. | |
void set_cursor(const short int x, const short int y) | |
{ | |
SetConsoleCursorPosition(handle, { x, y }); | |
} | |
// @brief Sets the console colors, will affect characters printed after this thing's call. | |
void set_color(const Color c) | |
{ | |
SetConsoleTextAttribute(handle, (WORD)c); | |
} | |
// @brief Waits for an enter input with 10ms delays. Uses <conio.h>'s methods. | |
bool wait_enter(void) | |
{ | |
if (_kbhit()) | |
{ | |
switch (_getch()) | |
{ | |
case 'Q': | |
case 'q': | |
return true; | |
// Enter is apparently '\r' | |
case '\r': | |
return true; | |
} | |
} | |
std::this_thread::sleep_for(std::chrono::milliseconds(10)); | |
return false; | |
} | |
} | |
// Math / string utils | |
namespace util | |
{ | |
template<typename TInt = int64_t> | |
TInt clamp(const TInt value, const TInt min, const TInt max) | |
{ | |
static_assert(std::is_integral<TInt>::value || std::is_floating_point<TInt>::value, "[util::clamp] Type isn't an integer / float."); | |
// Fix common overflow bugs (with unsigned, signed should be more careful) | |
// This can be an 'if constexpr' but c++ features, who knows who supports what so we are stuck with this :( | |
if (std::is_unsigned<TInt>::value) | |
{ | |
const TInt& max_int = std::numeric_limits<TInt>::max(); | |
// Overflow to std::numeric_limits<TInt>::max; | |
if (min == 0U) | |
{ | |
// Check if value is bigger than signed int range | |
// Signed range : max_int / 2 | |
if (value > max_int / 2) | |
return min; | |
} | |
// Overflow to 0 | |
if (max == max_int) | |
{ | |
// Check if value is lower than max | |
// This check is pointless but it will do | |
if (value < max_int / 2) | |
return max; | |
} | |
} | |
if (value > max) | |
return max; | |
if (value < min) | |
return min; | |
return value; | |
} | |
// @brief Mathematically wraps the number. Does not work great with unsigned integers (may cause overflows). | |
template<typename TInt = int64_t> | |
TInt wrapi(TInt value, const TInt min, const TInt max) | |
{ | |
static_assert(!std::is_floating_point<TInt>::value, "[util::wrapi] Type has to be an integer."); | |
static_assert(std::is_integral<TInt>::value, "[util::wrapi] Type isn't an integer."); | |
TInt minMaxRange = max - min; | |
if (minMaxRange == 0) | |
{ | |
return min; | |
} | |
return min + ((((value - min) % minMaxRange) + minMaxRange) % minMaxRange); | |
} | |
constexpr const char* whitespace = " \t\n\v\f\r"; | |
// @brief Trims 'util::whitespace' from the left of the string. | |
template<typename TChar = char> | |
std::basic_string<TChar> ltrim(const std::basic_string<TChar>& str, const TChar* trim_chars = whitespace) | |
{ | |
size_t start = str.find_first_not_of(trim_chars); | |
return start == std::string::npos ? "" : str.substr(start); | |
} | |
// @brief Trims 'util::whitespace' from the right of the string. | |
template<typename TChar = char> | |
std::basic_string<TChar> rtrim(const std::basic_string<TChar>& str, const TChar* trim_chars = whitespace) | |
{ | |
size_t end = str.find_last_not_of(trim_chars); | |
return end == std::string::npos ? "" : str.substr(0, end + 1); | |
} | |
// @brief Trims 'util::whitespace' from both the left and the right of the string. | |
template<typename TChar = char> | |
std::basic_string<TChar> trim(const std::basic_string<TChar>& str, const TChar* trim_chars = whitespace) | |
{ | |
return rtrim(ltrim(str, trim_chars), trim_chars); | |
} | |
} | |
// @brief Out of bounds kill snake (loops around otherwise) | |
bool bounds_kill = false; | |
// @brief Color used for the normal bounds that is always shown. | |
const console::Color bounds_normal_color = console::Color::GRAY; | |
// @brief Color used for the killing bounds. | |
const console::Color bounds_killer_color = console::Color::WHITE; | |
// @brief Snake no longer has a tail (if true, used for debug) | |
bool no_tail = false; | |
// @brief Manages argv + argc | |
// Doesn't handle special cases like piping the stdout of your app into another app or stuff like that | |
// Probably the most overengineered class in this whole mess. | |
class AppArgument | |
{ | |
protected: | |
std::string arg_name; | |
std::string arg_desc; | |
const int64_t arg_capture_options_count = 0; | |
const bool arg_help_printable = true; | |
std::function<bool(const std::vector<std::string>&)> arg_delegate; | |
public: | |
// Standard setting (capture_options = false, help_printable = true) | |
AppArgument(const std::string& name, const std::string& desc, const std::function<bool(const std::vector<std::string>&)> delegate) | |
: arg_name(name), arg_desc(desc), arg_delegate(delegate) {} | |
// Capture options setting | |
AppArgument(const std::string& name, const std::string& desc, const int64_t& capture_options_count, const std::function<bool(const std::vector<std::string>&)> delegate) | |
: arg_name(name), arg_desc(desc), arg_delegate(delegate), arg_capture_options_count(capture_options_count) {} | |
// Help-printable setting | |
AppArgument(const std::string& name, const std::string& desc, const int64_t& capture_options_count, const bool& help_printable, const std::function<bool(const std::vector<std::string>&)> delegate) | |
: arg_name(name), arg_desc(desc), arg_delegate(delegate), arg_capture_options_count(capture_options_count), arg_help_printable(help_printable) {} | |
protected: | |
// Anything except arg_list can be defined inline. | |
static std::vector<AppArgument> arg_list; | |
public: | |
static bool is_error; | |
static void print_help(const std::string& info, std::ostream& os) | |
{ | |
if (!info.empty()) | |
os << "Info : " << info << '\n'; | |
os << "Arguments help : \n"; | |
for (AppArgument& elem : arg_list) | |
{ | |
// Indent the argument start once | |
// Indent the name and description twice | |
if (elem.arg_help_printable) | |
os << "\tArgument =>\n\t\tName: " << elem.arg_name << "\n\t\tDescription: " << elem.arg_desc << '\n'; | |
if (elem.arg_capture_options_count > 0) | |
os << "\t\tThis argument captures '" << elem.arg_capture_options_count << "' options, the following passed next argument(s) is the option.\n"; | |
} | |
} | |
// Registers a 'AppArgument' | |
// uses an internal array. | |
static void add(const AppArgument& arg) | |
{ | |
arg_list.push_back(arg); | |
} | |
// clears the arguments | |
static void clear() | |
{ | |
arg_list.clear(); | |
} | |
// parses & runs arguments | |
static void parse(const int& argc, const char* argv[]) | |
{ | |
// No arguments? (except for the executable name) | |
if (argc <= 1) | |
return; | |
is_error = false; | |
std::stringstream ss; | |
// Ignore first argument (even though it could be empty / non-existent i don't care) | |
for (int i = 1; i < argc; i++) | |
{ | |
// Get trimmed argument | |
bool found_arg = false; | |
std::string current_arg = util::trim(std::string(argv[i])); | |
for (AppArgument& elem : arg_list) | |
{ | |
// Find the position of the argument name in the passed argument name | |
size_t pos = current_arg.find(elem.arg_name); | |
// Found a matching argument for current argument. (pos isn't npos) | |
if (pos != std::string::npos) | |
{ | |
found_arg = true; | |
// Capture next argument as option | |
std::vector<std::string> arg_option; | |
arg_option.reserve(elem.arg_capture_options_count); | |
for (int64_t j = 0; j < elem.arg_capture_options_count; j++) | |
{ | |
i++; // add 1 to i here because doing it inline in the if statement depends on the compiler | |
if (i < argc) | |
{ | |
// Get next argument | |
arg_option.emplace_back(util::trim(std::string(argv[i]))); | |
} | |
else // not enough arguments | |
{ | |
is_error = true; | |
// can pass 'j' as got parameter because it's 1 less than expected | |
ss << "Argument '" << current_arg << "' couldn't capture options. (Not enough options, expected " << | |
elem.arg_capture_options_count << ", got " << j << ")\n"; | |
break; | |
} | |
} | |
// Invoke using the passed options | |
// If the delegate returns false, this means the invocation failed | |
// There will be no exception handling in here, do it in the delegate | |
if (!elem.arg_delegate(arg_option)) | |
{ | |
is_error = true; | |
ss << "Error occured while executing argument : " << current_arg << '\n'; | |
} | |
break; | |
} | |
} | |
// Unknown argument | |
if (!found_arg) | |
{ | |
is_error = true; | |
ss << "Unknown argument : " << current_arg << '\n'; | |
} | |
} | |
if (is_error) | |
{ | |
// Display help text | |
print_help(ss.str(), std::cout); | |
} | |
} | |
}; | |
// Define the 'std::vector' here, for some reason methods can be inline though | |
// Probably the 'extern/linking' stuff, the c++ linker scares me anyways. | |
// tl;dr : put AppArgument to a seperate .hpp + .cpp file + name the class better | |
bool AppArgument::is_error = false; | |
std::vector<AppArgument> AppArgument::arg_list; | |
// -- Snake | |
// `enum class` is strong typing for the enum type | |
// meaning that this enum type can be only accessed by doing Direction:: and can be only stored in Direction. | |
// Usual enums act like 'C' enums and can have their constants referred without the 'Direction::' type name. | |
// @brief Defines a direction for the snake. | |
enum class Direction | |
{ | |
Up, Down, Left, Right | |
}; | |
// @brief Contains the stats about the current snake. | |
namespace stats | |
{ | |
bool is_paused = false; | |
bool is_game_over = false; | |
int64_t score = 0; | |
} | |
// @brief Contains the globals for the snake. | |
namespace snake | |
{ | |
// snake player's character | |
constexpr char pl_char = 'o'; | |
constexpr char pl_tail_char = 'O'; | |
constexpr console::Color pl_color = console::Color::GREEN; // Primary color | |
constexpr console::Color pl_tail_dark_color = console::Color::DARKGREEN; // Set color every tail index divisible by 2 | |
// snake direction | |
Direction dir = Direction::Right; | |
// snake position | |
int64_t x, y; | |
// tail positions | |
std::vector<std::pair<int64_t, int64_t>> tail; | |
// loop counts to wait until the frames end | |
// Basically doesn't instantly kill the player, unless the ::snake::tick_grace ends. | |
int64_t grace_frames = 2; // grace frames to wait | |
int64_t waited_grace_frames = 0; // tried to make this private (using class abuse) and it didn't work because linker | |
// Player is in danger, tick the grace frames. | |
void tick_grace(const char* msg_on_dead = nullptr) | |
{ | |
waited_grace_frames++; | |
if (waited_grace_frames > grace_frames) | |
{ | |
stats::is_game_over = true; | |
if (msg_on_dead != nullptr) | |
{ | |
exit_message = std::string(msg_on_dead); | |
} | |
} | |
} | |
// @brief Used when the Player is no longer in danger, this resets the grace frames. | |
void reset_grace() | |
{ | |
waited_grace_frames = 0; | |
} | |
// Note : This method will kill the snake | |
// Check only once before you move. | |
bool is_next_tile_safe(const bool& allow_tick_grace = true) | |
{ | |
bool is_safe = true; | |
// Check blocks | |
// TODO : Map feature impl, will not do it because | |
// 1. I don't care | |
// 2. I am lazy and this code is not very flexible. Ignore the 'flexibility' part of it because it is actually concise but i am more lazy. | |
// Check snake itself | |
int64_t sn_next_x = x, sn_next_y = y; | |
switch (dir) | |
{ | |
case Direction::Up: | |
sn_next_y = util::wrapi<int64_t>(sn_next_y - 1, 1, area_sz_y - 1); | |
break; | |
case Direction::Down: | |
sn_next_y = util::wrapi<int64_t>(sn_next_y + 1, 1, area_sz_y - 1); | |
break; | |
case Direction::Left: | |
sn_next_x = util::wrapi<int64_t>(sn_next_x - 1, 1, area_sz_x - 1); | |
break; | |
case Direction::Right: | |
sn_next_x = util::wrapi<int64_t>(sn_next_x + 1, 1, area_sz_x - 1); | |
break; | |
} | |
// bounds | |
if (bounds_kill) | |
{ | |
// snake moved more than expected | |
// snake can only move 1 tile per frame | |
is_safe = std::abs(snake::x - sn_next_x) <= 1 && std::abs(snake::y - sn_next_y) <= 1; | |
} | |
for (const std::pair<int64_t, int64_t>& tail_pos : tail) | |
{ | |
if (sn_next_x == tail_pos.first && sn_next_y == tail_pos.second) | |
{ | |
is_safe = false; | |
break; | |
} | |
} | |
if (allow_tick_grace) | |
{ | |
if (!is_safe) | |
snake::tick_grace(); | |
else | |
snake::reset_grace(); | |
} | |
return is_safe; | |
} | |
} | |
namespace fruit | |
{ | |
// fruit character | |
constexpr char f_char = (unsigned char)30; | |
constexpr console::Color f_color = console::Color::YELLOW; | |
// fruit position | |
int64_t x, y; | |
void spawn() | |
{ | |
// Do this as the '0' and the last 'area_sz_x' is reserved for the characters of the border | |
// Play area is essentially smaller than promised. (but not important for crappy console snake.) | |
x = util::wrapi<int64_t>(rand() % area_sz_x, 1, area_sz_x - 1); | |
y = util::wrapi<int64_t>(rand() % area_sz_y, 1, area_sz_y - 1); | |
} | |
void collect() | |
{ | |
// Increment score and spawn | |
stats::score++; | |
spawn(); | |
} | |
} | |
// initilaze the game | |
void init() | |
{ | |
// Runtime environment | |
console::clear(); | |
console::visibility_cursor(false); | |
srand(time(nullptr)); | |
// Snake | |
snake::x = area_sz_x / 2; | |
snake::y = area_sz_y / 2; | |
// Fruit | |
fruit::spawn(); | |
} | |
// snake / fruit logic | |
void logic() | |
{ | |
// Snake position before changing | |
int64_t sn_prev_x = snake::x, sn_prev_y = snake::y; | |
if (!snake::is_next_tile_safe()) | |
return; | |
switch (snake::dir) | |
{ | |
// Up and down is reverted | |
// Roll clamp sets the value to min/max, if the values set are bigger than max/smaller than min | |
case Direction::Up: | |
snake::y = util::wrapi<int64_t>(snake::y - 1, 1, area_sz_y - 1); | |
break; | |
case Direction::Down: | |
snake::y = util::wrapi<int64_t>(snake::y + 1, 1, area_sz_y - 1); | |
break; | |
case Direction::Left: | |
snake::x = util::wrapi<int64_t>(snake::x - 1, 1, area_sz_x - 1); | |
break; | |
case Direction::Right: | |
snake::x = util::wrapi<int64_t>(snake::x + 1, 1, area_sz_x - 1); | |
break; | |
} | |
// Eat froot if the snek collects it | |
if (snake::x == fruit::x && snake::y == fruit::y) | |
{ | |
fruit::collect(); | |
// Tail has space reserved. | |
if (!no_tail) | |
{ | |
// Maybe the issue is in the placement of the tail | |
// The 0'th index tail is in this position | |
// TODO : Try placing tails correctly. | |
auto snake_tail_pos = std::pair<int64_t, int64_t>(sn_prev_x, sn_prev_y); | |
if (snake::tail.size() > 1) | |
{ | |
snake_tail_pos.first = snake::tail.at(snake::tail.size() - 1).first; | |
snake_tail_pos.second = snake::tail.at(snake::tail.size() - 1).second; | |
switch (snake::dir) | |
{ | |
case Direction::Up: | |
snake_tail_pos.second -= 1; | |
break; | |
case Direction::Down: | |
snake_tail_pos.second += 1; | |
break; | |
case Direction::Left: | |
snake_tail_pos.first += 1; | |
break; | |
case Direction::Right: | |
snake_tail_pos.first -= 1; | |
break; | |
} | |
} | |
snake::tail.push_back(snake_tail_pos); | |
} | |
} | |
// Finish game if the player was too good | |
// Area size has 2 border, so the size is actually smaller than promised | |
// add 1 as the player head doesn't count as score. | |
if (stats::score + 1 >= (area_sz_x - 2) * (area_sz_y - 2)) | |
{ | |
exit_message = "You won!\nTo try bigger area challenge, use the --area-size with (size_x, size_y) parameters as program argument."; | |
stats::is_game_over = true; | |
} | |
// Update tail | |
if (!no_tail) | |
{ | |
size_t tail_size = snake::tail.size(); | |
if (tail_size == 1) | |
{ | |
// Since the for loop won't execute | |
snake::tail[0].first = sn_prev_x; | |
snake::tail[0].second = sn_prev_y; | |
} | |
// Position tail (if more than 1) | |
for (size_t i = 1; i < tail_size; i++) | |
{ | |
// this is what happens when you are lazy and not write a vector2I struct | |
static std::pair<int64_t, int64_t> pair_dir_reference; // Copy of Previous (for ref purposes) | |
std::pair<int64_t, int64_t>& pair_prev = snake::tail[i - 1]; // Previous | |
std::pair<int64_t, int64_t>& pair_current = snake::tail[i]; // Current | |
// Follow previous one | |
// Tail should follow the player | |
// The first pair_prev[i == 1 - 1] tail should be the previous position of the player | |
// Make the current pair follow the previous tail | |
// The tail can be at 4 different positions | |
// O // Up if X is same but Y is 1 less | |
// O C O // Left Or Right if Y is same, Left => X is 1 less, Right => X is 1 more | |
// O // Down if X same but Y is 1 more | |
// Using the direction, manipulate the current tail accordingly | |
if (i == 1) | |
{ | |
// Keep direction reference | |
pair_dir_reference = pair_current; | |
// For the first 2 tails, we do have directions | |
// The other ones are unknown. | |
pair_current = pair_prev; | |
pair_prev.first = sn_prev_x; | |
pair_prev.second = sn_prev_y; | |
} | |
else | |
{ | |
// Other tails (except for the current) is set correctly, but 1 behind. | |
auto pair_prev_current = pair_current; // Keep previous position. | |
pair_current = pair_dir_reference; // Set position | |
pair_dir_reference = pair_prev_current; // Set direction reference to current. | |
// Perhaps we could use pair_prev but it doesn't work, and this works fine so keep | |
// Oh well. | |
} | |
// Check self collision with the both tails | |
// If that's the case, oh well. | |
// Collision check is done in 'snake::is_next_tile_safe()' | |
// if (snake::x == pair_current.first && snake::y == pair_current.second) | |
// { | |
// snake::x = sn_prev_x; | |
// snake::y = sn_prev_y; | |
// // stats::is_game_over = true; | |
// } | |
} | |
} | |
} | |
// conio input | |
void input() | |
{ | |
// return if no kbhit | |
if (!_kbhit()) | |
return; | |
bool has_tail = !no_tail && snake::tail.size() > 0; | |
// Apparently mingw _getch is non-standard | |
// so the special chars are | |
// Get current ascii character | |
int input_ch = _getch(); | |
if (input_ch != 0 && input_ch != 224) | |
{ | |
// Keyboard non-special chars | |
switch (std::tolower(input_ch)) | |
{ | |
// Change direction | |
case 'w': | |
if (has_tail && snake::dir == Direction::Down) | |
break; | |
snake::dir = Direction::Up; | |
break; | |
case 'a': | |
if (has_tail && snake::dir == Direction::Right) | |
break; | |
snake::dir = Direction::Left; | |
break; | |
case 's': | |
if (has_tail && snake::dir == Direction::Up) | |
break; | |
snake::dir = Direction::Down; | |
break; | |
case 'd': | |
if (has_tail && snake::dir == Direction::Left) | |
break; | |
snake::dir = Direction::Right; | |
break; | |
case 'p': | |
stats::is_paused = !stats::is_paused; | |
break; | |
// Exit if q is pressed | |
case 'q': | |
stats::is_game_over = true; | |
exit_message = "You quit the game."; | |
break; | |
default: | |
break; | |
} | |
} | |
else // Arrow keys (or any special key in this matter) are outputted as special characters | |
{ | |
// For unix : Arrow keys == \033 + [ + key code (A, B, C or D) | |
// For windows (mingw + msvc) : Arrow keys == 0 or 224 + key code (72, 75, 77 or 80) | |
// Special chars | |
input_ch = _getch(); | |
switch (input_ch) | |
{ | |
case 72: // up | |
if (has_tail && snake::dir == Direction::Down) | |
break; | |
snake::dir = Direction::Up; | |
break; | |
case 75: // left | |
if (has_tail && snake::dir == Direction::Right) | |
break; | |
snake::dir = Direction::Left; | |
break; | |
case 80: // down | |
if (has_tail && snake::dir == Direction::Up) | |
break; | |
snake::dir = Direction::Down; | |
break; | |
case 77: // right | |
if (has_tail && snake::dir == Direction::Left) | |
break; | |
snake::dir = Direction::Right; | |
break; | |
} | |
} | |
} | |
// draw() method debugs | |
#define DRAW_DEBUG_STATS 0 | |
// draw into console | |
void draw() | |
{ | |
// Draw play area & players | |
if (!stats::is_paused) // Only draw stat / help text if paused. | |
{ | |
int64_t tail_current_index = 0; // Drawn Tail count (for different tail colors) | |
for (int64_t y = 0; y < area_sz_y; y++) | |
{ | |
for (int64_t x = 0; x < area_sz_x; x++) | |
{ | |
// Print border if in edges | |
bool is_newline = x == area_sz_x - 1; | |
if (y == 0 || y == area_sz_y - 1 || | |
x == 0 || is_newline) | |
{ | |
console::set_color(bounds_kill ? bounds_killer_color : bounds_normal_color); | |
std::cout << '#'; | |
if (is_newline) | |
std::cout << '\n'; | |
continue; | |
} | |
// Print player | |
console::set_color(snake::pl_color); | |
if (snake::x == x && snake::y == y) | |
{ | |
std::cout << snake::pl_char; | |
continue; | |
} | |
// Tail (is slow) | |
// But i really don't care | |
if (!no_tail) | |
{ | |
bool printed_tail = false; | |
for (const std::pair<int64_t, int64_t>& tail : snake::tail) | |
{ | |
if (tail.first == x && tail.second == y) | |
{ | |
if (tail_current_index % 2 == 0) | |
console::set_color(snake::pl_tail_dark_color); | |
std::cout << snake::pl_tail_char; | |
tail_current_index++; | |
printed_tail = true; | |
break; | |
} | |
} | |
if (printed_tail) | |
continue; | |
} | |
console::set_color(fruit::f_color); | |
// Print fruit | |
if (fruit::x == x && fruit::y == y) | |
{ | |
std::cout << fruit::f_char; | |
continue; | |
} | |
// Print space normally | |
console::set_color(console::Color::WHITE); | |
std::cout << ' '; | |
} | |
} | |
} | |
// Draw stats | |
console::set_color(console::Color::WHITE); | |
console::set_cursor(area_sz_x + 1, 0); | |
std::cout << "Score : " << stats::score << '\n'; | |
console::set_cursor(area_sz_x + 1, 1); | |
std::cout << "Press Q to quit." << '\n'; | |
console::set_cursor(area_sz_x + 1, 2); | |
std::cout << (!stats::is_paused ? "Press P to pause." : "[PAUSED !!] Press P to unpause.") << '\n'; | |
console::set_cursor(area_sz_x + 1, 3); | |
// Debug | |
#if DRAW_DEBUG_STATS | |
std::cout << "Snake X " << snake::x << ", Snake Y " << snake::y << '\n'; | |
console::set_cursor(area_sz_x + 1, 4); | |
std::cout << "Fruit X " << fruit::x << ", Fruit Y " << fruit::y << '\n'; | |
console::set_cursor(area_sz_x + 1, 5); | |
std::cout << "Tail Length : " << snake::tail.size() << '\n'; | |
console::set_cursor(area_sz_x + 1, 6); | |
size_t current_cursor = 7; | |
for (const auto& tailPos : snake::tail) // the following terrible code is for debug purposes | |
{ | |
std::cout << "Tail[" << current_cursor - 4 << "] = X:" << tailPos.first << " Y:" << tailPos.second << '\n'; | |
current_cursor++; | |
console::set_cursor(area_sz_x + 1, current_cursor); | |
} | |
#endif | |
// Delay drawing | |
console::set_cursor(0, 0); // Reset cursor | |
std::this_thread::sleep_for(std::chrono::milliseconds(ms_delay_draw)); | |
} | |
// this probably is a world record of most lines of c++ code to create snake in the console | |
int main(int argc, char* argv[]) | |
{ | |
// -- AppArgument | |
AppArgument::add(AppArgument("--snake-no-tail", "Removes the snake's tail.", | |
[] (const std::vector<std::string>& _) | |
{ | |
no_tail = true; | |
// Return true if the setting succeeded | |
return true; | |
})); | |
AppArgument::add(AppArgument("--borders-kill", "Borders kill the snake.", | |
[] (const std::vector<std::string>& _) | |
{ | |
bounds_kill = true; | |
// Return true if the setting succeeded | |
return true; | |
})); | |
AppArgument::add(AppArgument("--snake-speed", "Snake's speed multiplier. Pass as single double (can be number with decimal) value without any special things.", 1, | |
[] (const std::vector<std::string>& param) | |
{ | |
const std::string& text = param.at(0); | |
double speed_mul = 1.0; | |
try | |
{ | |
speed_mul = util::clamp<double>(std::stod(text), 0.0, std::numeric_limits<double>::max()); | |
} | |
catch (const std::exception& e) | |
{ | |
std::cerr << "\nError: --snake-speed: '" << e.what() << "' failed. Passed text was '" << text << "'.\n"; | |
return false; | |
} | |
ms_delay_draw /= speed_mul; | |
// Return true if the setting succeeded | |
return true; | |
})); | |
AppArgument::add(AppArgument("--area-size", "Size of the area. Pass as integers formatted like (x size, y size).", 2, | |
[] (const std::vector<std::string>& param) | |
{ | |
// parse param (with integers) | |
// apparently windows parses parameters before giving it in as an argument | |
// why?? | |
for (int64_t i = 0; i < param.size(); i++) | |
{ | |
const std::string& fmt_param = util::trim(param[i], "()"); | |
int64_t size_param = 0; | |
try | |
{ | |
size_param = std::stoll(fmt_param); | |
} | |
catch(const std::exception& e) | |
{ | |
std::cerr << "\nError: --area-size: '" << e.what() << "' failed. Passed text was '" << fmt_param << "'.\n"; | |
return false; | |
} | |
switch (i) | |
{ | |
case 0: | |
area_sz_x = util::clamp<int64_t>(size_param, 4, std::numeric_limits<int64_t>::max()); | |
break; | |
case 1: | |
area_sz_y = util::clamp<int64_t>(size_param, 4, std::numeric_limits<int64_t>::max()); | |
break; | |
default: | |
std::cerr << "[--area-size] No such size case as " << i << '\n'; | |
return false; | |
} | |
} | |
// Return true if the setting succeeded | |
return true; | |
})); | |
AppArgument::parse(argc, (const char**)argv); | |
if (AppArgument::is_error) | |
{ | |
std::cout << "Press enter to continue.\n"; | |
while (!console::wait_enter()); | |
} | |
init(); | |
while (!stats::is_game_over) | |
{ | |
// Only pause logic | |
if (!stats::is_paused) | |
logic(); | |
input(); | |
draw(); | |
} | |
console::clear(); | |
std::cout << exit_message << '\n' << | |
"Score was : " << stats::score << '\n' << | |
"Press enter to exit.\n"; | |
while (!console::wait_enter()); | |
return 0; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment