Last active
March 15, 2025 12:34
-
-
Save su8/bd20e3fcbe4c72978a3f6ca0130944a4 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
/* | |
Copyright 03/13/2025 https://github.com/su8/0verknigh7 | |
This program is free software; you can redistribute it and/or modify | |
it under the terms of the GNU General Public License as published by | |
the Free Software Foundation; either version 2 of the License, or | |
(at your option) any later version. | |
This program is distributed in the hope that it will be useful, | |
but WITHOUT ANY WARRANTY; without even the implied warranty of | |
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
GNU General Public License for more details. | |
You should have received a copy of the GNU General Public License | |
along with this program; if not, write to the Free Software | |
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, | |
MA 02110-1301, USA. | |
*/ | |
#include <iostream> | |
#include <cstdlib> | |
#include <random> | |
#include <algorithm> | |
#include <array> | |
#include <vector> | |
#include <string> | |
#include <utility> | |
#include <ctime> | |
#ifdef _WIN32 | |
#include <conio.h> | |
#include <windows.h> | |
#endif /* _WIN32 */ | |
struct ivec2 // our basic 2d point plus a few operations | |
{ | |
int x = 0; | |
int y = 0; | |
ivec2 operator*(int s) const { return { x * s,y * s }; } | |
ivec2 operator+(ivec2 q) const { return { x + q.x,y + q.y }; } | |
ivec2 operator%(ivec2 q) const { return { x % q.x,y % q.y }; } | |
bool operator ==(ivec2 q) const {return x==q.x && y==q.y;} | |
}; | |
enum {FLOOR =0, EXIT, TREASURE, ORB, ENEMY, HERO}; // cell flags, in display order | |
constexpr std::array<ivec2, 4> nbo{ivec2{1,0},{0,1},{-1,0},{0,-1}}; // direction offsets | |
constexpr char symbols[] = ".>$YD@"; // symbols for the bit defines | |
constexpr ivec2 dims{ 33,21 }; // map dimensions | |
/// Game state | |
int keys[6]; | |
std::array<uint8_t, dims.x* dims.y> map; | |
int treasure = 0; // collected treasure | |
int level = 0; | |
int steps = 0; | |
int dynamite = 0; | |
ivec2 hero; | |
std::vector<std::pair<ivec2,int>> enemy_position_and_last_direction; | |
// function prototypes | |
int read_key(void); | |
void bit_set(uint8_t& bits, int pos); | |
void bit_clear(uint8_t& bits, int pos); | |
bool bit_test(uint8_t& bits, int pos); | |
bool oob(ivec2 p); | |
bool oobb(ivec2 p); | |
uint8_t &mapelem(ivec2 p); | |
void setup_keys(void); | |
void create_iteration(ivec2 p); | |
void place_feature(int feature_bit, int num); | |
void clearscreen(unsigned short int clearOnWin); | |
void display(void); | |
void create_level(void); | |
void startgame(void); | |
void use_dynamite(void); | |
void move(int feature_bit, ivec2& from, ivec2 to); | |
void win(void); | |
void welcome(void); | |
#ifdef _WIN32 | |
int read_key(void) | |
{ | |
int c = _getch(); | |
if (c == 0 || c == 224) // in some cases on windows(?), pressing arrow keys returns two of these values, where the first is 0 or 224. | |
c = _getch() + (c<<8); | |
return c; | |
} | |
#else | |
#include <termios.h> | |
#include <unistd.h> | |
int read_key(void) | |
{ | |
char buf = 0; | |
struct termios old = {0}; | |
fflush(stdout); | |
if(tcgetattr(0, &old) < 0) | |
perror("tcsetattr()"); | |
old.c_lflag &= (~ICANON) & (~ECHO); // local modes = Non Canonical mode and Disable echo | |
old.c_cc[VMIN] = 1; // control chars (MIN value) = 1 | |
old.c_cc[VTIME] = 0; // control chars (TIME value) = 0 (No time) | |
if(tcsetattr(0, TCSANOW, &old) < 0) | |
perror("tcsetattr ICANON"); | |
if(read(0, &buf, 1) < 0) | |
perror("read()"); | |
old.c_lflag |= ICANON | ECHO; // local modes = Canonical mode and Enable echo | |
if(tcsetattr(0, TCSADRAIN, &old) < 0) | |
perror("tcsetattr ~ICANON"); | |
return buf; | |
} | |
#endif /* _WIN32 */ | |
void bit_set(uint8_t& bits, int pos) { bits |= 1 << pos; } | |
void bit_clear(uint8_t& bits, int pos) { bits &= ~(1UL << pos); } | |
bool bit_test(uint8_t& bits, int pos) { return bits & (1 << pos); } | |
// out of bounds | |
bool oob(ivec2 p) { return p.x < 0 || p.y < 0 || p.x >= dims.x || p.y >= dims.y; } | |
// out of bounds or on border | |
bool oobb(ivec2 p) { return p.x <= 0 || p.y <= 0 || p.x >= (dims.x-1) || p.y >= (dims.y-1); } | |
// get the map element at position | |
uint8_t& mapelem(ivec2 p) { return map[p.x + p.y * dims.x]; } | |
void setup_keys(void) | |
{ | |
int i = 0; | |
std::cout<<"Press key for "; | |
for (auto text : { "RIGHT",", UP",", LEFT",", DOWN",", DYNAMITE", " and RESTART" }) | |
{ | |
std::cout<<text; | |
keys[i++] = read_key(); | |
} | |
} | |
// a step of the algorithm, given an active, carved cell | |
void create_iteration(ivec2 p) | |
{ | |
std::random_device rd; | |
std::mt19937 g(rd()); | |
// for each of the 4 directions, in random order | |
std::array<int, 4> dir4_index{ 0,1,2,3 }; | |
std::shuffle(dir4_index.begin(), dir4_index.end(), g); | |
for (auto i : dir4_index) | |
{ | |
auto nb = p + nbo[i] * 2; // get the nb coord | |
if (oobb(nb) || mapelem(nb) != 0) // if oob or already visited, skip | |
continue; | |
mapelem(nb) = 1; // otherwise, mark it as visited (carve) | |
mapelem(p + nbo[i]) = 1; // ... and also carve the intermediate tile, to make a path there | |
create_iteration(nb); // Now repeat the process from this current tile | |
} | |
} | |
void place_feature(int feature_bit, int num) | |
{ | |
int floors_traversed = 0; | |
int place_at_multiples_of = 50 + (std::rand()%100); | |
while(num > 0) | |
for(auto& m : map) | |
if(m == 1 && (++floors_traversed % place_at_multiples_of) == 0) | |
{ | |
bit_set( m, feature_bit); | |
--num; | |
} | |
} | |
void clearscreen(unsigned short int clearOnWin) { | |
#ifdef _WIN32 | |
if (clearOnWin == 1U) std::cout << "\033[2J\033[0;0H"; | |
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE); | |
COORD Position; | |
Position.X = 0; | |
Position.Y = 0; | |
SetConsoleCursorPosition(hOut, Position); | |
#else | |
static_cast<void>(clearOnWin); | |
std::cout << "\033[2J\033[0;0H"; | |
#endif /*_WIN32 */ | |
} | |
void display(void) | |
{ | |
static const std::vector<std::pair<char, std::string>> reqColour = { {'@', "\033[1;32m@\033[0;0m"}, {'D', "\033[1;31mD\033[0;0m"}, {'$', "\033[1;33m$\033[0;0m"}, {'>', "\033[1;34m>\033[0;0m"}, {'Y', "\033[1;35mY\033[0;0m"} }; | |
clearscreen(0U); | |
for (int y = dims.y - 1; y >= 0; --y) | |
{ | |
for (int x = 0; x < dims.x; ++x) | |
{ | |
auto m = map[x+y*dims.x]; | |
char c = '#'; | |
for (int j = 0; j <= HERO; ++j) | |
if (m & (1 << j)) | |
c = symbols[j]; | |
unsigned short int foundReqChar = 0U; | |
for (const auto &[key, val] : reqColour) { | |
if (key == c) { | |
std::cout << val; | |
foundReqChar = 1U; | |
break; | |
} | |
} | |
if (foundReqChar == 0U) | |
std::cout << c; | |
} | |
if (y == (dims.y - 1)) std::cout << " Level: " << level; | |
else if (y == (dims.y - 2)) std::cout << " Treasure: " << treasure; | |
else if (y == (dims.y - 3)) std::cout << " Dynamite: " << dynamite; | |
else if (y == (dims.y - 4)) std::cout << " Steps: " << steps; | |
std::cout << '\n'; | |
} | |
} | |
void create_level(void) | |
{ | |
++level; | |
map.fill(0); // clear the map | |
ivec2 p = { (std::rand() % (dims.x-1)) | 1, (std::rand() % (dims.y-1)) | 1 }; // pick a point (odd coords) and carve it | |
mapelem(p) = 1; | |
create_iteration(p); // run the algorithm | |
bit_set( mapelem(p) , HERO); | |
place_feature(TREASURE, 10); | |
place_feature(ENEMY, 4+level); | |
place_feature(EXIT, 1); | |
int orbchance = 10*(level-15); | |
if( (std::rand()%100) < orbchance) | |
place_feature(ORB, 1); | |
enemy_position_and_last_direction.clear(); | |
hero = p; | |
for (p.y = 0; p.y < dims.y; ++p.y) | |
for (p.x = 0; p.x < dims.x; ++p.x) | |
if (bit_test(mapelem(p), ENEMY)) | |
enemy_position_and_last_direction.emplace_back(p, -1 /*invalid direction*/); | |
// remove some walls | |
int to_remove = 20; | |
while(to_remove > 0) | |
{ | |
auto& m = mapelem(ivec2{std::rand(),std::rand()}%dims); | |
if (m == 0) | |
{ | |
m = 1; | |
--to_remove; | |
} | |
} | |
} | |
void startgame(void) | |
{ | |
#ifdef _WIN32 | |
std::cout << "\033[2J\033[0;0H"; | |
#endif /* _WIN32 */ | |
treasure = 0; | |
level = 0; | |
steps = 0; | |
dynamite = 3; | |
create_level(); | |
} | |
// dynamite clears neighbour walls and enemies | |
void use_dynamite(void) | |
{ | |
--dynamite; | |
auto& vec = enemy_position_and_last_direction; | |
for(int i=0;i<4;++i) | |
{ | |
auto p = (hero + nbo[i] + dims)%dims; | |
auto& m = mapelem(p); | |
if(bit_test(m, ENEMY)) | |
vec.erase(std::remove_if(vec.begin(), vec.end(), [p](auto x){return x.first == p;}), vec.end()); | |
bit_clear(m, ENEMY); | |
bit_set(m, FLOOR); | |
} | |
} | |
void win(void) // ansi shadow font from TAAG | |
{ | |
clearscreen(1U); | |
std::cout<<"\n\n"<< R"( | |
██╗ ██╗ ██████╗ ██╗ ██╗ ██╗ ██╗ ██████╗ ███╗ ██╗ | |
╚██╗ ██╔╝██╔═══██╗██║ ██║ ██║ ██║██╔═══██╗████╗ ██║ | |
╚████╔╝ ██║ ██║██║ ██║ ██║ █╗ ██║██║ ██║██╔██╗ ██║ | |
╚██╔╝ ██║ ██║██║ ██║ ██║███╗██║██║ ██║██║╚██╗██║ | |
██║ ╚██████╔╝╚██████╔╝ ╚███╔███╔╝╚██████╔╝██║ ╚████║ | |
╚═╝ ╚═════╝ ╚═════╝ ╚══╝╚══╝ ╚═════╝ ╚═╝ ╚═══╝ | |
)"; | |
std::cout << "\n... as the mighty orb is yours to wield!\n\n"; | |
constexpr char tabs[] = "\t\t\t\t\t"; | |
std::cout << tabs<<"Treasure : "<<treasure<<'\n'; | |
std::cout << tabs<<"Steps : "<<steps<<"\n\n"; | |
std::cout << tabs<<"Dynamite : "<<dynamite<<"\n\n"; | |
std::cout << tabs<<"Total score : "<< int((treasure + 10*dynamite)*1000/(float)(steps+1))<<'\n'; | |
getchar(); | |
startgame(); | |
} | |
void welcome(void) // ansi shadow font from TAAG | |
{ | |
std::cout << "\n\n"<<R"( | |
██████╗ ██╗ ██╗███████╗███████╗████████╗ ███████╗ ██████╗ ██████╗ | |
██╔═══██╗██║ ██║██╔════╝██╔════╝╚══██╔══╝ ██╔════╝██╔═══██╗██╔══██╗ | |
██║ ██║██║ ██║█████╗ ███████╗ ██║ █████╗ ██║ ██║██████╔╝ | |
██║▄▄ ██║██║ ██║██╔══╝ ╚════██║ ██║ ██╔══╝ ██║ ██║██╔══██╗ | |
╚██████╔╝╚██████╔╝███████╗███████║ ██║ ██║ ╚██████╔╝██║ ██║ | |
╚══▀▀═╝ ╚═════╝ ╚══════╝╚══════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ | |
████████╗██╗ ██╗███████╗ ██████╗ ██████╗ ██████╗ | |
╚══██╔══╝██║ ██║██╔════╝ ██╔═══██╗██╔══██╗██╔══██╗ | |
██║ ███████║█████╗ ██║ ██║██████╔╝██████╔╝ | |
██║ ██╔══██║██╔══╝ ██║ ██║██╔══██╗██╔══██╗ | |
██║ ██║ ██║███████╗ ╚██████╔╝██║ ██║██████╔╝ | |
╚═╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝╚═════╝ | |
)" <<"\n\n\n\n\t\t\tWelcome brave adventurer, and may fate shine upon you!\n\n"; | |
} | |
void move(int feature_bit, ivec2& from, ivec2 to) | |
{ | |
bit_clear( mapelem(from), feature_bit); // clear | |
bit_set( mapelem(to), feature_bit); // set | |
from = to; | |
auto v = mapelem(to); | |
if (bit_test(v, HERO) && bit_test(v, ENEMY)) | |
startgame(); | |
else if (bit_test(v, HERO) && bit_test(v, TREASURE)) | |
{ | |
++treasure; | |
bit_clear(mapelem(to), TREASURE); | |
} | |
else if (bit_test(v, HERO) && bit_test(v, ORB)) | |
win(); | |
else if (bit_test(v, HERO) && bit_test(v, EXIT)) | |
create_level(); | |
} | |
int main(void) { | |
#ifdef _WIN32 | |
SetConsoleTitle("0verknigh7"); | |
#endif /*_WIN32 */ | |
welcome(); // show welcome screen | |
setup_keys(); // setup movement, dynamite and restart game keys | |
startgame(); | |
while (true) // main loop | |
{ | |
display(); | |
int c = read_key(); | |
if (c == keys[5]) // restart key pressed? | |
startgame(); | |
if(c == keys[4] && dynamite > 0) // dynamite key pressed? | |
use_dynamite(); | |
else // might be a movement key then | |
{ | |
ivec2 dir; // defaults to 0 | |
for(int i=0;i<4;++i) // set direction if correct key pressed | |
if (c == keys[i]) | |
dir = nbo[i]; | |
auto q = (hero + dir + dims)%dims; // make new point | |
if (mapelem(q) > 0) // if it's not a wall | |
{ | |
move(HERO, hero, q); | |
++steps; | |
} | |
} | |
// Play all enemies | |
for (auto& e : enemy_position_and_last_direction) | |
{ | |
int i0 = std::rand() % 4; // pick random start dir | |
bool moved = false; | |
for (int i = 0; i<4;++i) // try all dirs, starting at the random one | |
{ | |
int iDirOpposite = (i0 + i+2) % 4; | |
if (iDirOpposite == e.second) // don't go opposite of last direction | |
continue; | |
int iDir = (i0 + i) % 4; | |
auto p = (e.first + nbo[iDir] + dims)%dims; // wrap around map | |
if (bit_test(mapelem(p), FLOOR)) // if movable target pos, go there | |
{ | |
if (!bit_test(mapelem(p), ENEMY)) { // don't dissapear ENEMY when colliding 2 or more enemies | |
move(ENEMY, e.first, p); | |
e.second = iDir; | |
moved = true; | |
break; | |
} | |
} | |
} | |
if (!moved) // failed to move? Can surely go opposite of last position | |
{ | |
int iDirOpposite = (e.second + 2) % 4; | |
move(ENEMY, e.first, e.first + nbo[iDirOpposite]); | |
e.second = iDirOpposite; | |
} | |
} | |
} | |
return EXIT_SUCCESS; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment