Разработка консольного симулятора баттл-арены на C++ с «умными» ботами

Приветствую, SE7EN! Некоторое время назад мне пришла идея создать собственный симулятор бойцовского клуба. Вместо банальных кулачных боев захотелось добавить глубокие игровые механики. Будучи фанатом фэнтези и научной фантастики (особенно «Ведьмака» и «Властелина колец»), я решил реализовать этот небольшой проект, чтобы закрепить навыки, полученные при создании классических «Змейки» и «Морского боя».

Весь код написан на «чистом» C++ без сторонних зависимостей. Единственное исключение — библиотека <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();
            }
            else if (i == ch2->getPosY() && j == ch2->getPosX())
            {
                cout << ch2->getAppearance();
            }
            else cout << MAP[i][j];
        }
        cout << endl;
    }
}

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

Давайте разберем архитектуру класса Character. Пропуская тривиальные геттеры и сеттеры, сфокусируемся на функционале:

class Character {
protected:
string name;
string feature;
char appearance;
int HP;
int damage;
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&amp; o);
bool inBorders(int d)const;
bool isOccupied(int y, int x, Character&amp; o)const;
void toRun(Character&amp; o, bool isUnderAttack);
void toPursue(Character&amp; o);
bool isAllowedToAttack(Character&amp; o)const;
void showCloseAttack(Character&amp; o)const;
void animateRemoteAttack(Character&amp; o, char symb);
void showRemoteAttack(Character&amp; o, int x, int y, char&amp; symb);

virtual void Attack(Character&amp; obj) = 0;
virtual bool isOnSight(Character&amp; obj) = 0;
virtual void Character_info(Character&amp; obj) = 0;
friend ostream&amp; operator&lt;&lt;(ostream&amp; os, Character&amp; o);

};

Метод isOccupied(...) проверяет, не занята ли клетка другим бойцом, чтобы исключить наложение персонажей друг на друга.

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

Функция move(...) реализует логику перемещения с проверкой на коллизии. Внешние функции move_ch и move_ch_opposite управляют передачей направления.

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 += dx; posY += dy;
}
}

Метод inBorders(...) гарантирует, что персонаж не покинет пределы арены и не пройдет сквозь стены.

bool Character::inBorders(int d) const { 
int newX = posX, 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(...) — если персонаж получает урон от более сильного врага, он пытается разорвать дистанцию.

void Character::toRun(Character& o, bool isUnderAttack)
{
if (!isUnderAttack) return;
int x_enemy = o.getPosX(), y_enemy = o.getPosY();
int dx = (posX < x_enemy) ? -1 : (posX > x_enemy) ? 1 : 0;
int dy = (dx == 0) ? ((posY < y_enemy) ? -1 : (posY > y_enemy) ? 1 : 0) : 0;

auto isWalkable = [](int x, int y) {
    return (x &gt;= 1 &amp;&amp; x &lt; WIDTH - 1 &amp;&amp; y &gt;= 1 &amp;&amp; y &lt; HEIGHT - 1 &amp;&amp; MAP[y][x] != '#');
};

if (isWalkable(posX + dx, posY + dy)) move(dx, dy, o);

}

Метод toPursue(...) заставляет агрессора преследовать жертву.

void Character::toPursue(Character& o)
{
double dist = sqrt(pow(o.getPosX() - posX, 2) + pow(o.getPosY() - posY, 2));
if (dist <= 1) return;
int dx = (posX < o.getPosX()) ? 1 : -1;
int dy = (posY < o.getPosY()) ? 1 : -1;
move(dx, dy, o);
}

Метод isAllowedToAttack проверяет, позволяет ли расстояние нанести удар. Порог 1.5 выбран для корректной работы с диагональными атаками (sqrt(2) ≈ 1.41).

bool Character::isAllowedToAttack(Character& o)const
{
return sqrt(pow(posX - o.getPosX(), 2) + pow(posY - o.getPosY(), 2)) <= 1.5;
}

Остальные функции отрисовки, включая анимацию дальних атак, работают по схожему принципу с обновлением экрана через system("cls"). В реализации animateRemoteAttack(...) есть нюансы при расчете траектории снаряда по диагонали — буду рад конструктивным советам от сообщества по оптимизации алгоритма.

Производные классы

Для создания разнообразия я реализовал классы Warrior, Orc и Magician с уникальными характеристиками и способностями (например, магический урон или бонусы к защите).

class Warrior : virtual public Character { 
string ArmorName;
int remote_damage, defense, SwordSharpness;
public:
Warrior(...) : Character(...), defense(def), SwordSharpness(SS), remote_damage(rd) { HP += defense; }
void Attack(Character& o) override;
// ... прочие методы
};

Полный код проекта с детальными комментариями по каждому классу доступен в репозитории:

GitHub — Console Battle Arena

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

 

Источник

Читайте также