ОСНОВНЫЕ Компьютерные игры: Битва на C ++

Как новичок в C ++, я практикую свои навыки C ++, реализуя игры в ОСНОВНЫЕ компьютерные игры а вот моя версия на C ++ для Боевой.

Предложения, которые я хочу

Любые предложения приветствуются!

Текущий код проекта с моего рабочего места, полный стилей C в C ++ (что не очень хорошо), я хочу, чтобы мой код был в стиле C ++ и больше напоминал C ++ 11 или современный стиль C ++. И я был программистом на Python, я до сих пор плохо разбираюсь в таких вещах в C ++, как static, pointer и так далее.

О моем коде

В моей реализации Battle я жестко кодирую часть игровой карты, я не очень хорошо понимаю этот XD. Так что сначала пропустим, это моя TODO

void InitMap()
{
    // TODO: random map algorithm
    m_gameMap = {
        {0,0,0,2,2,6},
        {0,4,4,4,6,0},
        {5,0,0,6,0,0},
        {5,0,6,0,0,3},
        {5,1,0,0,0,3},
        {5,0,1,0,0,3},
    };
}

Игра проста, пользователь вводит координату удара, а затем сообщает пользователю результат.

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

if (iAttack > 0) {
    std::cout << "A Direction Hit On Ship Number " << iAttack << std::endl;
    if (!game.IsShipStillSurvive(iAttack)) {
        std::cout << "And you sunk it. Hurrah for the good guys." << std::endl;
        std::cout << "So far the bad gut have lost" << std::endl;
        game.ShipLost(iDestroyerLost, iCruisersLost, iAircraftLost);
        std::cout << iDestroyerLost << " Destroyer(s), " << iCruisersLost;
        std::cout << " Cruiser(s), And " << iAircraftLost << " Aircraft Carrier(s)." << std::endl;
    }
} else {
    std::cout << "Splash!" << std::endl;
}

Код

#include <iostream>
#include <vector>
#include <map>
#include <string>
#include <sstream>


typedef  enum {
    SHIP_TYPE_DESTROYER,
    SHIP_TYPE_CRUISERS,
    SHIP_TYPE_AIRCRAFT,
} ShipType;

typedef unsigned int ShipNumber ;

struct Coordinate {
    unsigned int x;
    unsigned int y;
};

class Game
{
public:
    Game()
    {
        m_shipLiveInfo.clear();
        for (auto iter = m_shipNumberInfo.begin(); iter != m_shipNumberInfo.end(); ++iter) {
            unsigned int iLive = 0;
            auto fleetIter = m_shipFleetInfo.find(iter->second);
            if (fleetIter != m_shipFleetInfo.end()) {
                iLive = fleetIter->second;
            }
            m_shipLiveInfo.insert(std::pair<ShipNumber, unsigned int>(iter->first, iLive));
        }
        InitMap();
    }
    virtual ~Game() {}
    void Attack(const Coordinate& coordinate, ShipNumber& iAttackShip)
    {
        iAttackShip = 0;
        if (coordinate.x <= m_iMapWidth && coordinate.y <= m_iMapHeight && coordinate.x > 0 && coordinate.y > 0) {
            iAttackShip = m_gameMap[coordinate.x - 1][coordinate.y - 1];
            std::cout << "Attack " << iAttackShip << std::endl;
            m_gameMap[coordinate.x - 1][coordinate.y - 1] = 0;
            auto iter = m_shipLiveInfo.find(iAttackShip);
            if (iter != m_shipLiveInfo.end()) {
                --(iter->second);   
            }
        }
    }

    bool IsShipStillSurvive(ShipNumber& iShip)
    {
        std::map<ShipNumber, unsigned int>::iterator iter = m_shipLiveInfo.find(iShip);
        if (iter == m_shipLiveInfo.end()) {
            return true;
        }
        return (iter->second > 0);
    }

    bool IsGameOver()
    {
        for (auto iter = m_shipLiveInfo.begin(); iter != m_shipLiveInfo.end(); ++iter) {
            if (iter->second > 0) {
                return false;
            }
        }
        return true;
    }
    

    void ShipLost(unsigned int& iDestroyer, unsigned int& iCruisers, unsigned int& iAircraft)
    {
        for (auto iter = m_shipLiveInfo.begin(); iter != m_shipLiveInfo.end(); ++iter) {
            auto shipIter = m_shipNumberInfo.find(iter->first);
            if (shipIter == m_shipNumberInfo.end()) {
                continue;
            }
            ShipType shipType = shipIter->second;
            if (iter->second == 0) {
                switch (shipType) {
                    case SHIP_TYPE_DESTROYER:
                        ++iDestroyer;
                        break;
                    case SHIP_TYPE_CRUISERS:
                        ++iCruisers;
                        break;
                    case SHIP_TYPE_AIRCRAFT:
                        ++iAircraft;
                        break;
                }
            }
        }
    }


    static const unsigned int m_iMapWidth = 6;
    static const unsigned int m_iMapHeight = 6;

private:
    
    std::vector<std::vector<ShipNumber>> m_gameMap;
    std::map<ShipNumber, unsigned int> m_shipLiveInfo;
    static const std::map<ShipType, unsigned int> m_shipFleetInfo;
    static const std::map<unsigned int, ShipType> m_shipNumberInfo;

    void InitMap()
    {
        // TODO: random map algorithm
        m_gameMap = {
            {0,0,0,2,2,6},
            {0,4,4,4,6,0},
            {5,0,0,6,0,0},
            {5,0,6,0,0,3},
            {5,1,0,0,0,3},
            {5,0,1,0,0,3},
        };
    }

};

const std::map<ShipNumber, ShipType> Game::m_shipNumberInfo = {
    {1, SHIP_TYPE_DESTROYER},
    {2, SHIP_TYPE_DESTROYER},
    {3, SHIP_TYPE_CRUISERS},
    {4, SHIP_TYPE_CRUISERS},
    {5, SHIP_TYPE_AIRCRAFT},
    {6, SHIP_TYPE_AIRCRAFT},
};

const std::map<ShipType, unsigned int> Game::m_shipFleetInfo = {
    {SHIP_TYPE_DESTROYER, 2},
    {SHIP_TYPE_CRUISERS, 3},
    {SHIP_TYPE_AIRCRAFT, 4}
};

int main(int argc, char** argv)
{
    Game game = Game();
    Coordinate cor;
    std::string strPosition;
    std::vector<unsigned int> positionNums;
    ShipNumber iAttack;
    bool isFirstStart = true;
    unsigned int iDestroyerLost = 0;
    unsigned int iCruisersLost = 0;
    unsigned int iAircraftLost = 0;
    while (!game.IsGameOver()) {
        iDestroyerLost = 0;
        iCruisersLost = 0;
        iAircraftLost = 0;
        
        if (isFirstStart) {
            std::cout << "Start Game" << std::endl;
            isFirstStart = false;
        } else {
            std::cout << "Try Again" << std::endl;
        }

        positionNums.clear();
        std::cin >> strPosition;
        std::stringstream ss(strPosition);
        for (unsigned int i; ss>>i;) {
            positionNums.push_back(i);
            if (ss.peek() == ',') {
                ss.ignore();
            }
        }
        if (positionNums.size() < 2) {
            std::cout << "InValid Input" << std::endl;
            continue;
        }
        cor.x = positionNums[0];
        cor.y = positionNums[1];

        if (cor.x > Game::m_iMapWidth || cor.x == 0 || cor.y > Game::m_iMapHeight || cor.y == 0) {
            std::cout << "InValid Input" << std::endl;
            continue;
        }

        game.Attack(cor, iAttack);
        if (iAttack > 0) {
            std::cout << "A Direction Hit On Ship Number " << iAttack << std::endl;
            if (!game.IsShipStillSurvive(iAttack)) {
                std::cout << "And you sunk it. Hurrah for the good guys." << std::endl;
                std::cout << "So far the bad gut have lost" << std::endl;
                game.ShipLost(iDestroyerLost, iCruisersLost, iAircraftLost);
                std::cout << iDestroyerLost << " Destroyer(s), " << iCruisersLost;
                std::cout << " Cruiser(s), And " << iAircraftLost << " Aircraft Carrier(s)." << std::endl;
            }
        } else {
            std::cout << "Splash!" << std::endl;
        }
    }
    
    return 0;
}

2 ответа
2

Код неплохой, но я вижу некоторые вещи, которые могут помочь вам его улучшить.

Проверить написание

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

Пропустить автоконструируемые функции

В настоящий момент в коде есть такая строка:

virtual ~Game() {}

Разумно сделать деструктор виртуальным, если вы ожидаете, что он будет производным от него, но здесь это не так, поэтому я бы рекомендовал просто опустить его и позволить компилятору сгенерировать деструктор по умолчанию.

Использовать const где это возможно

Во многих случаях, например, IsGameOver(), код не должен и не должен изменять базовый Game объект, и поэтому он должен быть объявлен const:

bool IsGameOver() const;

Предпочитать std::array к std::vector если размеры фиксированы во время компиляции

Поскольку мы знаем размеры во время компиляции, везде, где вы использовали std::vector, Я бы предложил использовать std::array вместо. Для этого кода это, вероятно, не будет иметь большого значения, но полезно выработать привычку использовать наиболее экономичный тип данных, который является практичным.

Не использовать std::endl если тебе это действительно не нужно

Разница между std::endl и 'n' в том, что 'n' просто испускает символ новой строки, а std::endl фактически сбрасывает поток. Это может занять много времени в программе с большим количеством операций ввода-вывода и на самом деле редко требуется. Лучше всего Только использовать std::endl когда у вас есть веская причина для очистки потока, а это не очень часто требуется для простых программ, таких как эта. Избегайте привычки использовать std::endl когда 'n' будет делать, принесет дивиденды в будущем, поскольку вы будете писать более сложные программы с большим количеством операций ввода-вывода и в которых необходимо максимизировать производительность.

Переосмыслите использование объектов

В Game класс — хорошая идея, но информация о кораблях разбросана по разным местам. Например, m_shipNumberInfo имеет карту номера корабля к типу корабля, а затем m_shipFleetInfo имеет карту типа корабля к длине корабля. Оба ссылаются на enum ShipType. Для этого я бы предложил вместо этого использовать объекты. Начать можно с этого:

class Ship {
public:
    enum ShipType { DESTROYER=2, CRUISER=3, AIRCRAFT=4 };
    Ship(ShipType t) : myType{t}, remaining{t} {}
    unsigned hit() { return remaining ? --remaining : 0; }
    // return true if ship is still alive
    bool operator()() const { return remaining; }
    // data members
    ShipType myType;
    unsigned remaining;
};

Теперь вся информация о корабле собрана в одном месте. Затем мы можем сделать этот класс private в пределах Game и отслеживайте их с помощью инициализации по умолчанию, например:

std::array<Ship, 6> ships = { 
    Ship::ShipType::DESTROYER, 
    Ship::ShipType::DESTROYER, 
    Ship::ShipType::CRUISER, 
    Ship::ShipType::CRUISER, 
    Ship::ShipType::CARRIER, 
    Ship::ShipType::CARRIER,
};

Теперь многое стало намного проще. Например:

bool IsShipStillSurvive(ShipNumber& iShip) const
{
    return ships.at(iShip - 1)();
}

bool IsGameOver() const
{
    return std::none_of(ships.begin(), ships.end(), [](const Ship& s){return s();});
}

Подумайте о пользователе

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

Рассмотрим дополнительные объекты

В добавок к Ship упомянутый выше объект, также было бы неплохо отделить Board объект из Game и использовать Модель-Представление-Контейнер подход. Так же Coordinate может быть публичным объектом внутри Game а ввод и проверка могут быть функцией-членом этого класса, а не в main.

Используйте возможности C ++ 20

Одна удобная функция C ++ 20 — возможность возвращать std::tuple. Итак, вместо этого:

game.ShipLost(iDestroyerLost, iCruisersLost, iAircraftLost);

Вместо этого мы можем написать это:

auto [iDestroyerLost, iCruisersLost, iAircraftLost] = game.ShipLost();

Соответствующую функцию можно упростить, используя std::count_if:

std::tuple<unsigned, unsigned, unsigned > ShipLost()
{
    unsigned iDestroyer = std::count_if(ships.begin(), ships.end(), [](const Ship& s){ 
        return s.myType == Ship::ShipType::DESTROYER && !s(); });
    unsigned iCruisers = std::count_if(ships.begin(), ships.end(), [](const Ship& s){ 
        return s.myType == Ship::ShipType::CRUISER && !s(); });
    unsigned iAircraft = std::count_if(ships.begin(), ships.end(), [](const Ship& s){ 
        return s.myType == Ship::ShipType::CARRIER && !s(); });
    return std::make_tuple(iDestroyer, iCruisers, iAircraft);
}

    Во-первых, ваш «TODO» для заполнения карты хорош. Это пример нисходящей декомпозиции, и неплохо было бы заблокировать ваш код и начать с заглушки.


    typedef  enum {
        SHIP_TYPE_DESTROYER,
        SHIP_TYPE_CRUISERS,
        SHIP_TYPE_AIRCRAFT,
    } ShipType;
    
    typedef unsigned int ShipNumber ;
    

    typedef enum? Это не C. Write enum ShipType { ⋯ }; или даже enum class в зависимости от ваших потребностей. Вам не нужно typedef что такое «теги» в C для получения имен типов; они уже являются именами типов в C ++.

    Хорошо давать вашим типам символические имена, даже если они не являются «сильными типами», но не используйте typedef ключевое слово больше, вообще. Напишите это как:

    using ShipNumber = unsigned int;
    

    или еще лучше, используйте <stdint> такие как uint32_t.


    Зачем ты делаешь свой деструктор virtual? У вас есть нет (другие) виртуальные функции в классе, и вы никоим образом не используете полиморфизм. Вы используете сам класс, а не производные классы.


    Game game = Game();
    

    Только:

    Game game;
    

    это то, что вам нужно. Ты понимаешь это Game создается в стеке, а не в куче? Интересно, думаете ли вы, что это похоже на Java, потому что другие переменные не инициализируются таким образом.


    Используйте целочисленные типы со знаком, а не unsigned, если вы не выполняете манипуляции с битами, полагайтесь на переполнение, или очень нужно еще один бит диапазона.


    Coordinate cor;
    std::string strPosition;
    std::vector<unsigned int> positionNums;
    

    Они определены как часть группы в начале main. Но вам не нужно, чтобы они сохраняли свое значение при итерациях цикла; на самом деле, вы звоните positionNums.clear() в верхней части петли. Объявить переменные там, где они вам нужны в первую очередь.


    Абстрагируйте ввод / взаимодействие пользователей

    Вместо смешивания cin >> вещи в огромном цикле, который делает все,
    ⭐ писать функции, чтобы делать одна вещь как единый уровень абстракции дальше вниз.
    Ваш игровой цикл должен выглядеть как набросок, с именованными функциями, выражающими шаги высокого уровня, а не кусками кода. делает эти шаги.

    Что касается ввода, в частности, используя cin>> неприятен тем, что противоречит лучшим методам определения переменных там, где они вам нужны, их инициализации и, надеюсь, их создания const. А затем вам нужно проанализировать введенные данные и исправить ошибки на всех более «глубоких» уровнях детализации, чем вам нужно. Основной цикл должен указывать просто:

    const Coordinate cor = get_play();
    

    Весь этот кусок кода:

            positionNums.clear();
            std::cin >> strPosition;
            std::stringstream ss(strPosition);
            for (unsigned int i; ss>>i;) {
                positionNums.push_back(i);
                if (ss.peek() == ',') {
                    ss.ignore();
                }
            }
            if (positionNums.size() < 2) {
                std::cout << "InValid Input" << std::endl;
                continue;
            }
            cor.x = positionNums[0];
            cor.y = positionNums[1];
    

    это деталь, которая не принадлежит здесь в основном цикле.

    В любом случае полезно абстрагироваться от ввода / взаимодействия как отдельного предмета. В порте BASIC вы можете использовать ввод терминала, как здесь, но вы можете позже заменить его каким-либо графическим интерфейсом или элементами управления игрой, просто изменив реализацию этих четко определенных функций и вообще не влияя на основную игровую логику. .


    void Attack(const Coordinate& coordinate, ShipNumber& iAttackShip)
    

    Я предполагаю, что второй должен быть «выходным» параметром, и эта функция предоставляет эту информацию вызывающему.

    Строго избегайте «выходных» параметров.
    Возврат вещей с использованием значений, возвращаемых функцией.

    if (coordinate.x <= m_iMapWidth && coordinate.y <= m_iMapHeight && coordinate.x > 0 && coordinate.y > 0) {
        // the entire meat of the function
        }
    

    Напишите «в левое поле». Не открывайте все более и более глубокие уровни вложенности. У вас есть предварительное условие. Поскольку это глобальное ограничение (вся карта), оно должно быть уже проверено как часть функции ввода, и эта функция может предполагать, что ей передано допустимое значение coordinate.

    Но, чтобы проиллюстрировать, предварительные условия должны возвращать или бросать в качестве прелюдии к реальному телу функции. Вместо того, чтобы открывать еще один уровень вложенности:

    if (!mapbounds.contains(coordinate)) return 0;
    

    И обратите внимание, что проверка того, находится ли точка внутри прямоугольника, — это обычная операция многократного использования. Вы делаете его более сложным и менее «высокоуровневым», сохраняя размер карты в двух отдельных переменных, а не в другой паре координат.


    [coordinate.x - 1][coordinate.y - 1]
    

    Вы постоянно корректируете индексы при доступе к своим игровым данным. Используйте значения внутренних координат, которые соответствовать представительство правления. То есть с нуля. Регулировка должна выполняться как часть пользовательский ввод / вывод. Фактически, на реальной доске линкора вы используете буквы для одной из осей, так что на самом деле это будет A через K или какой бы большой ни была доска. Таким образом, отображение пользовательского ввода-вывода является более общим, чем просто вычитание 1; это может быть совсем другое представление. Одна ось может вычитать 1 из введенного числа, другая отображает букву на число. Точно так же отображение координат — это проблема ввода-вывода пользователя, и функции вывода должны принимать то же самое. coordinate это используется структурой данных.

      Добавить комментарий

      Ваш адрес email не будет опубликован. Обязательные поля помечены *