Игра «Угадай число»

Вот так в результате будет выглядеть игра.
Вот так в результате будет выглядеть игра.
Вот так в результате будет выглядеть игра.
Привет! В этой статье мы создадим логическую игру «Угадай число». Статья может быть полезна начинающим программистам. Игра будет реализована на языке программирования C++ в соответствии со стандартом C++11 с применением интегрированной среды разработки (IDE) Microsoft Visual Studio 2019 Community Edition для операционной системы из семейства Windows. Данная редакция IDE бесплатна, и Вы можете скачать её с официального сайта Microsoft. Предполагается, что у читателя есть базовые знания языка C++ и он знаком с основами работы в IDE Visual Studio. Также Вы можете использовать любые другие языки программирования и интегрированные среды разработки, так как в статье будет рассмотрен алгоритм игры, который позволяет абстрагироваться от инструментов.

Правила игры

Человек (он же далее по тексту — пользователь и игрок) играет с компьютером. Компьютер загадывает натуральное число из интервала от 1 до 100 включительно, которое игроку нужно отгадать. Если игрок называет неправильное число, то компьютер дает игроку подсказку: загаданное число больше или меньше числа игрока. Очередная партия продолжается до тех пор, пока игрок не угадает число, либо ему не надоест играть в эту захватывающую игру.

Алгоритм программы

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

У игры «Угадай число» очень простой алгоритм, который представлен в виде блок-схемы на рисунке 1.

Рисунок 1 – Представление алгоритма игры в виде блок-схемы.
Рисунок 1 – Представление алгоритма игры в виде блок-схемы.
Рисунок 1 – Представление алгоритма игры в виде блок-схемы.

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

Шаг 2. Компьютер запускает генератор случайных чисел (ГСЧ)…;

Шаг 3. … и загадывает случайное натуральное число от 1 до 100 включительно;

Шаг 4. Игрок вводит предполагаемое число (ответ) с клавиатуры;

Шаг 5. Компьютер сравнивает ответ игрока с ранее загаданным им числом: если ответ неверный, то необходимо дать игроку подсказку и вернуться на Шаг 4; если игрок угадал число, то поздравить игрока с победой и перейти на Шаг 6;

Шаг 6. Спросить у игрока, хочет ли он чтобы компьютер загадал новое число и тем самым продолжить игру. При положительном ответе вернуться на Шаг 3, в случае отрицательного — перейти на Шаг 7;

Шаг 7. Вежливый компьютер прощается с пользователем и программа завершает работу.

Реализация на языке C++

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

Исходный код игры и проект для Microsoft Visual Studio 2019 Community Edition можно скачать из репозитория автора на Bitbucket или с Google Диска. Автор советует просматривать код из файла guessthenumber.cpp параллельно с прочтением статьи.

Ввиду того, что программа довольно простая, будет предполагаться, что весь код, приведенный далее, будет написан в функции main, которая определяет точку входа в программу.

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

setlocale(LC_ALL, "Russian");

Далее напишем приветственное сообщение пользователю с помощью объекта потока вывода std::cout, оператора вывода в поток << и модификатора потока std::endl, который необходим для переноса текста на следующую строку. Не забудьте подключить стандартную библиотеку ввода-вывода <iostream>!

std::cout << "Привет! Это игра 'Угадай число' и Вы будете играть с компьютером." << std::endl;

Затем запускаем генератор случайных чисел используя функции std::srand и std::time.

std::srand(std::time(nullptr));

Если этого не сделать, то при каждом последующем запуске игры компьютер будет загадывать одну и ту же последовательность чисел. Например, игрок запустил игру и компьютер загадал число 54. Игрок его отгадал, тогда игра загадала число 87. Игрок снова отгадал загаданное число, после чего компьютер загадал число 33, но игроку надоело играть и он закрывает программу. Спустя какое-то время пользователь снова запускает игру, а компьютер опять загадывает числа 54, 87, 33! То есть, загадываемые числа перестают иметь случайный характер и игра теряет свой смысл.

Функция std::srand принимает единственный параметр целочисленного типа seed, относительно которого генерируется очередная последовательность случайных чисел. Значит, каждый раз при запуске генератора случайных чисел нам нужно передавать в качестве seed новое значение. Для этого отлично подойдет текущее время, которое мы можем получить в виде числа секунд, прошедших с 1 января 1970 года, применив функцию std::time из стандартной библиотеки <ctime>. На момент написания статьи спустя 01.01.1970 прошло 1578060105 секунд и каждую секунду это значение увеличивалось на 1 и продолжает увеличиваться, что вполне логично.

Следует отметить, что числа, которые мы будем получать в ходе игры, не случайны, а псевдослучайны. Хотя они и выглядят случайными, на самом деле они вычисляются по вполне конкретной формуле. Генератор случайных чисел (ГСЧ) также правильнее называть генератором псевдослучайных чисел (ГПСЧ), так как мы используем только один источник энтропии — предсказуемое системное время. Но для просты мы опустим данную особенность и продолжим называть вновь получаемые числа случайными.

Поскольку наш алгоритм предусматривает продолжение игры (Шаг 6 → Шаг 3), то определим переменную playing булева (логического) типа и запустим основной цикл программы с помощью оператора do … while, который будет продолжать свою работу пока переменная playing имеет значение true. Вспомним, что особенностью цикла do … while является то, что он точно выполнится хотя бы один раз, в отличие от простого цикла while.

bool playing = true;
do
{
// 1. Создать счетчик ходов
// 2. Загадать очередное число
// 3. Сообщить игроку что очередное число было загадано
// 4. Получить от игрока ответ и проанализировать его
// 5. Спросить у игрока хочет ли он продолжить игру
}
while (playing == true);

Далее будем работать внутри этого цикла.

Добавим в нашу игру элемент статистики — счетчик ходов. Для этого определим целочисленную переменную moves.

int moves = 0;

Пришло время загадать для игрока случайное число! В языке C++ для этого существует функция std::rand.

int number = std::rand() % 100 + 1;

Функция std::rand возвращает случайное целое число в интервале от 0 до 32767 включительно, но в правилах нашей игры мы определили, что компьютер должен загадывать числа от 1 до 100 включительно. Необходимо преобразовать интервал [0; 32767] в интервал [1; 100]. Для этого мы от результата работы функции std::rand с помощью оператора % берем остаток от деления на 100 и прибавляем к нему 1.

Например, функция std::rand вернула число 644. Остаток от деления 644 на 100 будет равен 44. Прибавляем к остатку 1 и получаем загаданное число 45 для игрока.

644 % 100 = 44
44 + 1 = 45

Загаданное компьютером число сохраняем в переменную number целочисленного типа.

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

std::cout << std::endl << "Я загадал число от 1 до 100. Отгадайте его!" << std::endl;

Теперь ход за игроком, который должен ввести предполагаемое число с клавиатуры. Для этого запустим еще один бесконечный цикл while в котором будем считать ходы, опрашивать игрока, сравнивать число игрока с загаданным компьютером и давать игроку подсказки, если он не угадал. Назовем этот цикл циклом ввода игрока. Цикл прерывается если игрок угадал число (Шаг 5 → Шаг 6), в противном случае компьютер дает подсказку и происходит следующая итерация (Шаг 5 → Шаг 4).

while (true)
{
// 1. Увеличить счетчик ходов
// 2. Получить ответ от игрока
// 3. Проверить что игрок ввел корректное число от 1 до 100
// 4. Проверить что игрок угадал число
// 5. Поздравить игрока с победой и выйти из цикла, иначе дать подсказку и перейти на следующую итерацию
}

Начнем программирование цикла ввода игрока с увеличения счетчика ходов moves, который мы создали перед тем, как загадать случайное число.

moves++;

Спросим у игрока ответ. Для этого создадим переменную guess целочисленного типа, в которую извлечем ответ игрока из потока ввода std::cin с помощью оператора извлечения из потока >>. Перед выполнением операции извлечения числа из потока ввода напечатаем курсор «>> », который будет сообщать пользователю что от него ожидается ввод числа с клавиатуры.

int guess = 0;
std::cout << ">> ";
std::cin >> guess;

Пользователь случайно или специально может ввести в программу некорректные данные вместо числа: символ или строку. Также он может ввести число, которое выходит за пределы интервала [1; 100]. Чтобы избежать ошибок и не сломать игру, с помощью условного оператора if проверим данные, которые игрок ввел с клавиатуры.

if (std::cin.fail() == true || (guess <= 0 || guess > 100))
{
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(),'\n');
std::cout << "Введите натуральное число от 1 до 100!" << std::endl;
continue;
}

Метод std::cin.fail возвращает true, если при работе с потоком ввода std::cin произошла ошибка, например, мы попытались извлечь из потока строку, которую ввел игрок вместо числа, в целочисленную переменную. Далее, если игрок ввел число, мы проверяем, что оно входит в интервал значений от 1 до 100 включительно.

Если наша проверка показала, что игрок ввел некорректное число, то мы сбрасываем состояние потока ввода std::cin методом std::cin.clear, очищаем его буфер методом std::cin.ignore, пишем игроку сообщение об ошибке и завершаем текущую итерацию цикла ввода игрока оператором continue.

Игрок ввел ответ, который мы проверили и точно знаем что он корректен. Напишем самую главную проверку в нашей игре — условие победы, заключающееся в совпадении загаданного компьютером числа number с числом guess, которое ввел игрок. В этом нам снова поможет условный оператор if в связке с оператором ветвления else.

if (number == guess)
{
std::cout << "Поздравляю, Вы угадали число за " << moves << " ходов! Я загадывал " << number << "." << std::endl;
break;
}
else
{
// Игрок не угадал число!
// Дать ему подсказку…
}

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

Иначе, если игрок оказался не очень удачливым, то компьютеру ничего не остается как поддержать его подсказкой, код которой необходимо записать в блоке({ … }) оператора else условия победы.

if (number > guess)
{
std::cout << "Не угадали! Моё число больше " << guess << "." << std::endl;
}
else
{
std::cout << "Не угадали! Моё число меньше " << guess << "." << std::endl;
}

Исходя из правил игры, подсказка заключается в том, что компьютер с помощью связки операторов if-else сравнивает на больше-меньше своё загаданное число number с числом игрока guess и выдает игроку соответствующее сообщение.

После подсказки цикл ввода игрока повторяется и игрок вводит новое число. На этом программирование данного цикла завершается и мы возвращаемся в основной цикл программы.

Очередная партия игры завершена, поэтому необходимо спросить у пользователя, хочет ли он продолжить игру чтобы компьютер загадал для него новое число. Для этого мы в начале программы объявили логическую переменную playing, которая управляет основным циклом программы. Чтобы продолжить или завершить игру, нужно повлиять на эту переменную, например, на основании ответа пользователя на вопрос «Загадать еще число? (Y/N)». Если игрок вводит с клавиатуры букву Y, то игра продолжается, иначе — завершается. Для этого создадим переменную c символьного типа, в которую будем записывать ответ пользователя. С помощью объекта std::cout выводим пользователю наш вопрос, а с помощью объекта std::cin получаем ответ и записываем его в переменную c. Не забываем про курсор «>> ».

char c = '\0';
std::cout << "Загадать еще число? (Y/N)" << std::endl << ">> ";
std::cin >> c;

С помощью функции std::strchr проверим, что ответ пользователя входит в строку «Yy» . Это позволит нам не задумываться о регистре буквы, введенной пользователем, так как Y и y — это разные символы с кодами ASCII 89 и 79 соответственно в десятичной системе счисления.

playing = std::strchr("Yy", c) != nullptr;

Функция std::strchr возвращает указатель на первый символ, найденный в строке «Yy» или нулевой указатель (nullptr), если искомый символ не найден. Мы сравниваем указатель, который возвращает функция std::strchr, c нулевым и записываем результат сравнения в переменную playing. Если пользователь вводит символ Y или y, то переменная playing принимает значение true и игра продолжается. Если пользователь вводит любой другой символ, то переменная playing принимает значение false и основной цикл программы завершается — происходит переход на Шаг 7.

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

std::cout << "Всего хорошего! Приходите еще!" << std::endl;
return 0;

Реализация игры завершена! Теперь можно запустить игру и немного поиграть…

Запуск игры

Откройте командную строку (cmd.exe) и введите следующие команды:

cd /d <Путь до директории с исполняемым файлом игры>
<Название исполняемого файла игры>.exe

Например:

cd /d "D:\Projects\Visual Studio\games.guessthenumber\guessthenumber\Release"
guessthenumber.exe

Оптимальная стратегия игры

Давайте подумаем как угадать загаданное компьютером число за наименьшее количество ходов.

В игру можно играть тремя способами:

Способ 1. Самый простой — перебирать все числа от 1 до 100 до тех пор, пока не будет найдено загаданное число: 1, 2, 3, … 100.

Способ 2. Вводить числа от 1 до 100 в случайном порядке: 5, 88, 33, … .

Способ 3. В нашей реализации игры компьютер дает подсказку после каждого неудачного хода. Можно ей воспользоваться и сузить диапазон поиска загаданного числа в два раза. Например, на первом ходе мы вводим число 50. Компьютер отвечает, что его число меньше. Тогда мы вводим число 25. Загаданное число по-прежнему меньше. Пробуем число 15. Оно уже меньше загаданного, следовательно, нам нужно продолжать искать число в интервале от 16 до 24. Вводим число 20, но оно оказывается меньше искомого. Отвечаем 24. Число компьютера меньше. Остались числа 21, 22 и 23. Выбираем 22 и оно оказывается верным!

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

Рисунок 2 — Среднее число ходов, которое необходимо совершить, чтобы угадать число тем или иным способом игры.
Рисунок 2 — Среднее число ходов, которое необходимо совершить, чтобы угадать число тем или иным способом игры.
Рисунок 2 — Среднее число ходов, которое необходимо совершить, чтобы угадать число тем или иным способом игры.

Как видно из гистограммы, среднее число ходов при Способе 1 будет стремится к 100 — это самая неоптимальная стратегия игры. Способ 2, ввод случайных чисел из ряда 1…100, потребует около 50 ходов в среднем. Самый оптимальный вариант игры — Способ 3, который потребует для победы всего лишь порядка 7 ходов.

Что можно улучшить?

При желании Вы можете самостоятельно доработать игру добавив в нее, например, уровни сложности:

  1. Легкий уровень — загадывается число в интервале от 1 до 100, неограниченное число попыток;
  2. Средний уровень — загадывается число в интервале от 1 до 1000, число попыток 25;
  3. Сложный уровень — загадывается число в интервале от 1 до 10000, число попыток 50.

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

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

<Заработанные очки> = max(0, 100 - <Число ходов>)

Заработанные очки можно сохранять в файл. Для этого Вам понадобится класс std::ofstream из стандартной библиотеки файлового ввода-вывода <fstream> и его методы open, write, close, а также оператор вывода в поток <<.

Вот и все…

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