Игра ZX80 на C ++

Чтобы помочь в изучении C ++, я повторно реализовал игры из старых книг ZX80 на C ++. Вот описание одного, над которым я сейчас работаю:

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

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

Вероятно, он чрезмерно спроектирован для того, что есть, но я попытался использовать его в качестве упражнения в таких вещах, как:

  • Перегрузка оператора
  • Использование системы типов для проверки ввода данных пользователем (см. Guess класс.
  • Я не включил сюда модульные тесты, но код был написан так, чтобы можно было протестировать основную логику.

game.h

#ifndef GAME_H
#define GAME_H

#include <string>

enum class GuessResponse { TooLow, TooHigh, TakeOff, GameOver };

std::ostream& operator<<(std::ostream& os, const GuessResponse response);

class CountDown {
public:
    CountDown(int start);
    void decrement();
    bool is_finished() const;

private:
    int value_;
};

class Guess final {
public:
    Guess() {};
    Guess(int value);
    void setValue(int value);
    friend bool operator>(const Guess& guess, const int other);
    friend bool operator<(const Guess& guess, const int other);
    friend bool operator>=(const Guess& guess, const int other);
    friend bool operator<=(const Guess& guess, const int other);
    friend bool operator==(const Guess& guess, const int other);
    friend bool operator!=(const Guess& guess, const int other);
private:
    int value_{1};
};

std::istream& operator>>(std::istream& is, Guess& guess);

class SpaceTakeoffGame {
public:
    SpaceTakeoffGame(int gravity, int weight);
    GuessResponse make_guess(const Guess& guess);
    bool over() const;

private:
    const int gravity_;
    const int weight_;
    const int force_;
    CountDown tries_remaining_{10};
    bool over_{false};

};

#endif

game.cpp

#include "game.h"
#include <string>
#include <stdexcept>
#include <ios>
#include <istream>

SpaceTakeoffGame::SpaceTakeoffGame(const int gravity, const int weight)
    : gravity_{ gravity },
      weight_{ weight },
      force_{ weight * gravity } {}

GuessResponse SpaceTakeoffGame::make_guess(const Guess& guess) {
    if (tries_remaining_.is_finished()) {
        over_ = true;
        return GuessResponse::GameOver;
    }
    tries_remaining_.decrement();
    if (guess > force_) {
        return GuessResponse::TooHigh;
    }
    if (guess < force_) {
        return GuessResponse::TooLow;
    }
    over_ = true;
    return GuessResponse::TakeOff;
}

bool SpaceTakeoffGame::over() const {
    return over_;
}

CountDown::CountDown(const int start): value_{ start } {}

void CountDown::decrement() {
    if (value_ > 0) {
        value_ -= 1;
    }
}

bool CountDown::is_finished() const {
    return value_ == 0;
}

std::ostream& operator<<(std::ostream& os, const GuessResponse response) {
    switch(response) {
        case GuessResponse::TooHigh: {
            os << std::string{"TOO HIGH, TRY AGAIN"};
            break;
        }
        case GuessResponse::TooLow: {
            os << std::string{"TOO LOW, TRY AGAIN"};
            break;
        }
        case GuessResponse::TakeOff: {
            os << std::string{"GOOD TAKE OFF"};
            break;
        }
        default: {
            os << std::string{"YOU FAILED - THE ALIENS GOT YOU"};
            break;
        }
    }
    return os;
}

Guess::Guess(int value) {
    setValue(value);
}

void Guess::setValue(int value) {
    if (value < 1) throw std::invalid_argument{ "Guess must be a positive integer" };
    value_ = value;
}

std::istream& operator>>(std::istream& is, Guess& guess) {
    int n;
    is >> n;
    if (is.fail()) {
        return is;
    }
    try {
        guess.setValue(n);
    } catch (std::invalid_argument&) {
        is.setstate(std::ios::failbit);
    }
    return is;
}

bool operator>(const Guess& guess, const int other) {
    return guess.value_ > other;
}

bool operator<(const Guess& guess, const int other) {
    return guess.value_ < other;
}

bool operator>=(const Guess& guess, const int other) {
    return !(guess < other);
}

bool operator<=(const Guess& guess, const int other) {
    return !(guess > other);
}

bool operator==(const Guess& guess, const int other) {
    return !(guess < other || guess > other);
}

bool operator!=(const Guess& guess, const int other) {
    return !(guess == other);
}

main.cpp

#include <iostream>
#include <random>
#include <limits>
#include <ios>
#include "game.h"

int main()
{
    std::random_device rd;
    std::mt19937_64 eng(rd());

    std::uniform_int_distribution<int> gravity_distr{1, 20};
    std::uniform_int_distribution<int> weight_distr{1, 40};

    const auto gravity =  gravity_distr(eng);
    const auto weight =  weight_distr(eng);

    SpaceTakeoffGame game(gravity, weight);

    std::cout << "STARSHIP TAKE-OFFn"
              << "GRAVITY=" << gravity << "n"
              << "TYPE IN FORCE" << std::endl;
    do {
        Guess guess;
        std::cin >> guess;
        if (std::cin.fail()) {
            std::cin.clear();
            std::cin.ignore(std::numeric_limits<std::streamsize>::max(),'n');
            std::cout << "INVALID GUESS, TRY AGAIN" << std::endl;
            continue;
        }
        std::cout << game.make_guess(guess) << std::endl;
    } while (!game.over());
    return 0;
}
```

2 ответа
2

Определите простые функции-члены в файлах заголовков

Если вы определяете функции-члены внутри определения класса, они могут быть встроены компилятором. Для простых функций, например CountDown::is_finished(), это сгенерирует гораздо более эффективный код даже на Z80.

Ненужные броски в std::string

Есть несколько ненужных приведений к std::string в вашем коде, например:

os << std::string{"TOO HIGH, TRY AGAIN"};

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

os << "TOO HIGH, TRY AGAIN";

Выбор генератора случайных чисел

Пока std::mt19937_64 — очень хороший генератор случайных чисел, он имеет очень большое внутреннее состояние в 19937 бит (отсюда и его название), что составляет 2493 байта. Это огромное количество места, которое можно тратить на 8-битную машину. Он также использует 64-битную целочисленную арифметику, что менее желательно на 8-битной машине. С другой стороны, это один из самых быстрых алгоритмы генератора случайных чисел имеется в стандартной библиотеке.

Если пространство ограничено, то подумайте об использовании более простого ГСЧ, в частности, для линейных конгруэнтных систем требуется всего несколько байтов. Для такой игры вам не нужен высококачественный ГСЧ.

Избегайте использования исключений для обработки ошибок ввода

Исключения связаны с определенными затратами, даже если вы ничего не вызываете. throw. Компилятор должен сгенерировать требуемый код или данные, чтобы иметь возможность обрабатывать раскручивание стека. На обычном ПК я обычно считаю эту цену приемлемой, но на машине Z80 эта цена будет относительно высокой. Исключения также следует использовать только в очень исключительных ситуациях, с которыми код не может справиться, но в этом случае довольно тривиально обрабатывать недопустимые входные данные без необходимости throw. Есть несколько способов справиться с этим, я бы просто удалил setValue(), и сделать operator>> а friend функция, например:

class Guess {
public:
    ...
    friend std::istream& operator>>(std::istream& is, Guess& guess);
    ...
};

std::istream& operator>>(std::istream& is, Guess& guess) {
    int n;

    if ((is >> n) && n >= 1) {
        guess.value_ = n;
    } else {
        is.setstate(std::ios::failbit);
    }

    return is;
}            

  • Хороший улов на генераторе случайных чисел. Я полностью упустил это из виду.

    — Эдвард

Вот некоторые вещи, которые могут помочь вам улучшить ваш код.

Убедитесь, что у вас есть все необходимое #includes

Код в game.h относится к std::ostream а также std::istream но не #include <iostream> где они определены. Также внимательно рассмотрите, какие #includes являются частью интерфейса (и принадлежат .h файл) и которые являются частью реализации. В <string> заголовок нужен только в game.cpp а не в game.h.

Не все должно быть классом

Изучение объектно-ориентированного программирования полезно, но также стоит отметить, что иногда class не обязательно лучший подход. В Guess class представляет собой довольно сложную оболочку вокруг простого int. Думаю, я бы просто использовал int. То же самое и с CountDown.

Не сохраняйте значения, которые вам не нужны

В SpaceTakeoffGame конструктор принимает gravity а также weight как аргументы и вычисляет force_ От этого. Во-первых, с точки зрения физики это должно быть mass скорее, чем weight. Во-вторых, ни gravity ни weight когда-либо снова используются, поэтому нет особой необходимости их сохранять.

Не используйте исключения для проверки ввода данных пользователем

Исключение должно быть исключительным. То, что пользователь вводит отрицательное число, не является чем-то особенным; Я бы посоветовал просто проверить значение без использования исключений.

Не создавайте объекты без нужды

Код в настоящее время содержит эту строку:

os << std::string{"TOO HIGH, TRY AGAIN"};

Это заставляет компилятор создать строку, передать ее потоку и затем уничтожить строку. Лучше было бы просто написать это:

os << "TOO HIGH, TRY AGAIN";

Избегайте ненужных сложностей

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

#include <iostream>
#include <limits>
#include <random>

void play(int force) {
    bool takeoff{false};
    for (unsigned tries{10}; !takeoff && tries; --tries) {
        int guess{0};
        while (guess < 1) {
            std::cin >> guess;
            if (guess < 1) {
                if (std::cin.fail()) {
                    std::cin.clear();
                    std::cin.ignore(std::numeric_limits<std::streamsize>::max(),'n');
                }
                std::cout << "INVALID GUESS, TRY AGAIN" << std::endl;
            }
        }
        if (guess == force) {
            takeoff = true;
        } else if (guess < force && tries > 1) {
            std::cout << "TOO LOW, TRY AGAINn";
        } else if (guess > force && tries > 1) {
            std::cout << "TOO HIGH, TRY AGAINn";
        }
    } 
    std::cout << (takeoff ? "GOOD TAKE OFFn" :
        "YOU FAILED - THE ALIENS GOT YOUn");
}

int main() {
    std::random_device rd;
    std::mt19937 eng(rd());

    std::uniform_int_distribution<int> gravity_distr{1, 20};
    std::uniform_int_distribution<int> mass_distr{1, 40};

    const auto gravity{gravity_distr(eng)};
    const auto force{gravity * mass_distr(eng)};

    std::cout << "STARSHIP TAKE-OFFn"
              "GRAVITY=" << gravity 
              << "nTYPE IN FORCEn";
    play(force);
}

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

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