Написание консольного симулятора баттл-арены на языке С++ с реализацией «умных» ботов

от автора

Привет, Хабр! Как‑то появилась у меня идея сделать свой симулятор бойцовского клуба, но чтобы бой был не кулачный, а с элементами интересных механик, так как я люблю фэнтези и фантастику и моими любимыми сагами являются:«Ведьмак» и «Властелин колец»(да Азог из другой книги, но это ведь одна вселенная), то я решил написать этот небольшой проектик для усвоения теории, полученной при создании таких мейнстримных консольных игр как змейка и морской бой.

Проект написан полностью на чистом С++ без применения специфических библиотек, единственная «экзотика» которая может встретиться это #include <windows.h>, но применение этой библиотеки обосновывается необходимостью в создании задержки для того, чтобы человек смог воспринять происходящее на экране(можно использовать другой способ, как вам угодно).


Инициализация карты и её отрисовка

Карта по канону создается с помощью двумерного массива char.

const int HEIGHT = 14;const int WIDTH = 14;char MAP[HEIGHT][WIDTH] ={    '#','#','#','#','#','#','#','#','#','#','#','#','#','#',    '#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',    '#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',    '#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',    '#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',    '#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',    '#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',    '#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',    '#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',    '#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',    '#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',    '#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',    '#',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ',' ','#',    '#','#','#','#','#','#','#','#','#','#','#','#','#','#'};

Отрисовкой карты занимается функция showMap(…), объявленная в файле main.cpp. Функция принимает два указателя на базовый класс Character, о котором мы поговорим чуть ниже.

void showMap(Character* ch1, Character* ch2){    for (int i = 0; i < HEIGHT; i++)    {        for (int j = 0; j < WIDTH; j++)        {            if (i == ch1->getPosY() && j == ch1->getPosX())            {                cout << ch1->getAppearance();//вывести отображение бойца 1 на экран            }            else if (i == ch2->getPosY() && j == ch2->getPosX())            {                cout << ch2->getAppearance();//вывести отображение бойца 2 на экран            }            else cout << MAP[i][j];//вывести границы арены и незанятые клетки на экран        }        cout << endl;    }}

Базовый виртуальный класс

Пробежимся по основному функционалу класса, не принимая во внимание геттеры и сеттеры, основная задача которых заложена в их названиях get-выдать, set-установить.

class Character {protected:    string name;//имя персонажа    string feature;//его особенность(преимущество)    char appearance;//оторажение на экране    int HP;//уровень здоровья    int damage;//сколько HP снимает ближняя атака    string weapon;//название оружия    int posX, posY;//позиция на аренеpublic:    Character(string C = "Unknown", int h = 100, int d = 10, int x = 5, int y = 5, string f = "close combat", char ch = 'W',string w="sword");    virtual ~Character() {}    //геттеры     string getName() const { return name; }    int getHP() const { return HP; }    int getDamage() const { return damage; }    char getAppearance() const { return appearance; }    string getFeature() const { return feature; }    string getWeapon()const { return weapon; }    int getPosX()const { return posX; };    int getPosY()const { return posY; };    //сеттеры    int setHP(int hp=0) { return HP=hp; };    //функционал    void move(int dx, int dy, Character& o);//передвижение по арене    bool inBorders(int d)const;//проверка на достижение границы    bool isOccupied(int y, int x, Character& o)const;//проверка на занятость клетки на арене,чтобы избежать столкновение двух бойцов    void toRun(Character& o, bool isUnderAttack);//отбежать, если враг имеет преимущество в ближнем бою    void toPursue(Character& o);//преследовать врага, если он слаб в ближнем бою    bool isAllowedToAttack(Character& o)const;//проверка достаточно ли близко подошел для ближней атаки    void showCloseAttack(Character& o)const;//отобразить ближнюю атаку на экране    void animateRemoteAttack(Character& o, char symb);//отобразить атаку на расстоянии    void showRemoteAttack(Character& o, int x, int y, char& symb);//отобразить позицию стрелы(пламенного шара) на данный момент    virtual void Attack(Character& obj) = 0;    virtual bool isOnSight(Character& obj) = 0;    virtual void Character_info(Character& obj) = 0;    friend ostream& operator<<(ostream& os, Character& o);//вывести основную информацию о персонаже};

Функция isOccupied(…) проверяет не занята ли клетка, куда хочет шагнуть боец 1 бойцом 2.

bool Character::isOccupied(int y, int x,Character& o) const{    return o.getPosY() == y&&o.getPosX()==x;}

Функция move(…) позволяет перемещать бойца по полю, проверяя каждую клетку на занятость другим бойцом. Данная функция используется в функциях, объявленных в файле main.cpp: void move_ch(int& d, Character& o, Character& check) и void move_ch_opposite(int& d, Character& o, Character& check) в которых и реализована логика выбора направления и передачи координат.

void Character::move(int dx, int dy,Character& o)  {    if (isOccupied(posY + dy, posX + dx, o))    {        int ddx = -dx;        int ddy = -dy;        int newX = posX + ddx;        int newY = posY + ddy;        if (MAP[newY][newX] != '#')        {            posX = newX; posY = newY;        }        else        {            posX += 0;            posY += 0;        }    }    else    {        posX += dx; posY += dy;    }}

Функция inBorders(…) проверяет не вышел ли боец за границы арены. Если вышел функция возвращает false, иначе true.

bool Character::inBorders(int d) const {     int newX = posX;    int newY = posY;    switch (d) {    case UP:    newY--; break;    case DOWN:  newY++; break;    case LEFT:  newX--; break;    case RIGHT: newX++; break;    default: return false;    }    // Проверка выхода за границы массива    if (newX < 1 || newX >= WIDTH-1 || newY < 1 || newY >= HEIGHT-1)        return false;    // Проверка стены     if (MAP[newY][newX] == '#') return false;    return true;}

Функция toRun(…) позволяет добавить логику побега от более сильного физически бойца. Если боец 1 сильнее бойца 2 в ближнем бою и боец 1 наносит урон бойцу 2, то боец 2 старается отдалиться от противника.

void Character::toRun(Character& o, bool isUnderAttack){    if (!isUnderAttack) return;          // убегаем только после получения удара    int x_enemy = o.getPosX();    int y_enemy = o.getPosY();    int x_me = posX;    int y_me = posY;    // Направление ОТ врага    int dx = 0, dy = 0;    if (x_me < x_enemy) dx = -1;         // враг справа - бежим влево    else if (x_me > x_enemy) dx = 1;     // враг слева – бежим вправо    if (dx == 0)    {        if (y_me < y_enemy) dy = -1;         // враг снизу – бежим вверх        else if (y_me > y_enemy) dy = 1;     // враг сверху – бежим вниз    }    int newX = x_me + dx;    int newY = y_me + dy;    // Функция проверки проходимости (стены + границы)    auto isWalkable = [](int x, int y) -> bool {        return (x >= 1 && x < WIDTH - 1 && y >= 1 && y < HEIGHT - 1 && MAP[y][x] != '#');        };    if (isWalkable(newX, newY)) {        move(dx, dy, o);        return;    }}

Функция toPursue(…) противоположна по логике функции toRun(…): более сильный боец преследует более слабого пока не сблизится с ним.

void Character::toPursue(Character& o){    int x_enemy = o.getPosX();    int y_enemy = o.getPosY();    int x_me = posX;    int y_me = posY;    int dist = sqrt(pow(x_enemy - x_me, 2) + pow(y_enemy - y_me, 2));    if (dist <= 1) return;              // уже рядом – атакуем в основном цикле    // Направление к врагу    int dx = 0, dy = 0;    if (x_me < x_enemy) dx = 1;    else if (x_me > x_enemy) dx = -1;    if (dx == 0)    {        if (y_me < y_enemy) dy = 1;        else if (y_me > y_enemy) dy = -1;    }    int newX = x_me + dx;    int newY = y_me + dy;    // Проверка на стены    bool walkable = (newX >= 1 && newX < WIDTH - 1 && newY >= 1 && newY < HEIGHT - 1 && MAP[newY][newX] != '#');    // Запрещаем занимать клетку врага    if (walkable && (newX != x_enemy || newY != y_enemy))        move(dx, dy, o);}

Функция isAllowedToAttack(…) позволяет проверить достаточно ли близко подошел боец 1 для того чтобы атаковать бойца 2 в ближнем бою. Число 1.5 выбрано по причине того что sqrt(2) это приблизительно 1,41 , соответственно бить по диагонали можно.

bool Character::isAllowedToAttack(Character& o)const{    int x_enemy = o.getPosX();    int y_enemy = o.getPosY();    int x_me = posX;    int y_me = posY;    double distance = sqrt(pow(x_me - x_enemy, 2) + pow(y_me - y_enemy, 2));    return distance <= 1.5;}

Функция showCloseAttack(…) обновляет карту и отображает бойца по которому наносится урон в виде ‘X’.

void Character::showCloseAttack(Character& o) const{    system("cls");    for (int i = 0; i < HEIGHT; i++)    {        for (int j = 0; j < WIDTH; j++)        {            if (i == posY && j == posX)            {                cout << getAppearance();            }            else if (i == o.getPosY() && j == o.getPosX())            {                cout << 'X';            }            else cout << MAP[i][j];        }        cout << endl;    }}

Функция showRemoteAttack(…) отображает позицию снаряда в данном кадре. Сама анимация полета снаряда происходит в функции animateRemoteAttack(…).

void Character::showRemoteAttack(Character& o, int x, int y, char& symb){    for (int i = 0; i < HEIGHT; i++)    {        for (int j = 0; j < WIDTH; j++)        {            if (i == posY && j == posX)            {                cout << getAppearance();            }            else if (i == o.getPosY() && j == o.getPosX())            {                cout << o.getAppearance();            }            else if (i == y && j == x)cout << symb;            else cout << MAP[i][j];        }        cout << endl;    }}

Функция animateRemoteAttack(…), как было сказано выше реализует отрисовку полета снаряда. При расчете количества шагов до цели я просто вычислил длину вектора и данный способ оказался неверным, поскольку по диагонали не всегда получается целое число и при такой отрисовке снаряд отображается некорректно для выстрела по диагонали. Я надеюсь более опытные программисты подправят данную функцию и скажут о решении, которое они нашли в комментариях.

void Character::animateRemoteAttack(Character& o, char symb){    system("cls");    int x_target = o.getPosX();    int y_target = o.getPosY();    int dx = (x_target > posX) ? 1 : (x_target < posX) ? -1 : 0;    int dy = (y_target > posY) ? 1 : (y_target < posY) ? -1 : 0;    double realDistance = sqrt(dx * dx + dy * dy);    int steps = int(realDistance);    for (int i = 0; i <= steps; i++)    {        int cx = posX + dx * i, cy = posY + dy * i;        showRemoteAttack(o, cx, cy, symb);        Sleep(100);        system("cls");    }    showCloseAttack(o);    Sleep(100);}

Остальные функции являются чисто виртуальными и реализуются в каждом производном классе в зависимости от параметров, переданных ему.

Класс Warrior

Я не буду подробно расписывать каждую функцию для дальнейших классов, просто приведу объявление для каждого класса.

class Warrior : virtual public Character {     string ArmorName;//название брони    int remote_damage;//урон от дальней атаки    int defense;//добавляет хп в зависимости от брони    int SwordSharpness;//добавляет урон в зависимости от остроты клинкаpublic:    Warrior(string cl, int hp, int dam, int x, int y, string f, char a,string w,        string A = "Wolf school armor", int def = 25, int SS = 10, int rd = 10)        : Character(cl, hp, dam, x, y, f, a,w), ArmorName(A), defense(def), SwordSharpness(SS),remote_damage(rd) {        HP += defense;    }    virtual ~Warrior() {}    //геттеры    string getArmorName()const { return ArmorName; }    int getRemote_Damage()const { return remote_damage; }    int get_defense()const { return defense; }    int getSwordSharpness()const { return SwordSharpness; }    //функционал    void Attack(Character& o)override;//виртуальная функция атаки(здесь происходит логика отнимания HP при ударе),т.к. добавляется доп.урон в зависимости от остроты клинка    void Remote_Attack(Character& o);//атака на расстоянии с помощью арбалета    bool isAllowedRemoteAttack(Character& o);//проверка достаточно ли расстояния для того чтобы атаковать из арбалета, если слишком близко или далеко - не атаковать    bool isOnSight(Character& obj)override;//вирутальная функция позволяющая обнаружить врага в поле зрения    void Character_info(Character& obj)override;//виртуальная функция, выводит информацию об атаке:кто кого атаковал,тип атаки(вблизи), сколько HP снесла атака и сколько HP осталось у противника    void Character_info_remote(Character& obj);//вывести информацию об атаке:кто кого атаковал,тип атаки(на расстоянии), сколько HP снесла атака и сколько HP осталось у противника    friend ostream& operator<<(ostream& os, const Warrior& o); //вывести информацию о параметрах класса};

Класс Orc

class Orc :virtual public Character{private:string race;//раса оркаint buff;//доп.урон в зависимости от расыpublic:Orc(string cl, int hp, int dam, int x, int y, string f, char a,string w,string r="Grey orc", int b=20) :Character(cl, hp, dam, x, y, f, a,w), race(r), buff(b) { }virtual ~Orc() {}void Attack(Character& o)override;//виртуальная функция атаки(здесь происходит логика отнимания HP при ударе),т.к. добавляется доп.урон в зависимости от расы орка(физ.сила)bool isOnSight(Character& o)override;//вирутальная функция позволяющая обнаружить врага в поле зрения(каждый класс имеет свою дальность)void Character_info(Character& obj)override;//виртуальная функция, выводит информацию об атаке:кто кого атаковал,тип атаки(вблизи), сколько HP снесла атака и сколько HP осталось у противникаfriend ostream& operator<<(ostream& os, const Orc& o);//вывести информацию о параметрах класса};

Класс Magician

class Magician :public Character{private:string power;int power_damage;public:Magician(string cl, int hp, int dam, int x, int y, string f, char a,string w,string p = "Fire", int pd = 25) :Character(cl, hp, dam, x, y, f, a,w),power(p),power_damage(pd){}virtual ~Magician() {}void Attack(Character& o)override;//ближняя атака стальным шестомvoid Magic_Attack(Character& o);//атака магией на расстоянииbool isAllowedMagicAttack(Character& o);//проверка достаточно ли расстояния для того чтобы атаковать магией, если слишком близко или далеко - не атаковатьbool isOnSight(Character& obj)override;//видно ли врага, если расстояние до врага меньше или равно 15.0, то true, иначе falsevoid Character_info(Character& obj)override;//вывести информацию об атаке:кто кого атаковал,тип атаки(вблизи), сколько HP снесла атака и сколько HP осталось у противникаvoid Character_info_remote(Character& obj)const;//вывести информацию об атаке:кто кого атаковал,тип атаки(на расстоянии), сколько HP снесла атака и сколько HP осталось у противникаfriend ostream& operator<<(ostream& os, Magician& o);//вывести информацию о параметрах класса};

Кто хочет более подробно разобрать код данного проекта, вот ссылка на данный проект, в описании рассказано более подробно о логике работы каждого класса:

GitHub

Надеюсь вам было интересно читать данную статью и у вас появились идеи как улучшить данный проект или создать свой! Делитесь своими идеями в комментариях, пожалуйста, будет интересно почитать как можно улучшить код, поскольку я не силен в алгоритмах, да и в программировании я ещё зелёный.

ссылка на оригинал статьи https://habr.com/ru/articles/1038376/