Программирование игр. Урок 3. Программируем нашу первую игру
Программирование игр. Урок 3. Программируем нашу первую игру
Ну что ребята, начнём программировать по настоящему?
Для первой программы нам хватит лишь браузера на компьютере. Желательно Google Chrome, пожалуй он наиболее удобен для разработки и программирования. Но и другие браузеры подойдут. Я же буду приводить все примеры на основе именно Google Chrome. Ну и в качестве бонуса расскажу как можно написать нашу программу на телефоне. Я это уже сделал, это возможно.
Простую программу на JavaScript можно написать в браузере, используя только средства отладки и разработки. Чтобы до них добраться, нужно нажать клавишу F12 на клавиатуре или кликнуть правой кнопкой мыши внутри страницы, и выбрать "инспектировать элемент".
Далее нам понадобиться вкладка ресурсы (я работаю в операционной системе Linux, и у меня весь софт на английском языке).
Тут нам нужна вкладка снипетов, это маленькие кусочки кода, которые можно выполнять прямо на текущей странице.
Создайте новый снипет, и дайте ему название cows_bulls.js. Привыкайте все называть на английском языке, это негласное правило в программировании. Я в дальнейшем буду использовать русский язык в программе только в комментариях (хотя и их лучше писать на английском, вдруг ваш код будет читать иностранец, я встречал программы, где комментарии, которые могли бы помочь понять код, были написаны на китайском) и в строках, выводимых для пользователя.
Вот в этом маленьком снипете мы и будем писать нашу игру. Я всегда предлагаю писать любую программу с описания алгоритма. Алгоритм для игры мы написали на прошлом уроке, давайте перенесём его в сокращенном варианте в виде комментариев в наш снипет:
// Создаём переменную для числа компьютера
// и сохраняем в нее случайное четырехзначное число.
// Generate random 4 digits number.
// Спрашиваем у пользователя его вариант ответа.
// Ask user his first guess.
// Вызываем функцию разбиение чисел на отдельные цифры.
// Call devide function for user and pc numbers.
// Вызываем функцию подсчёта быков и коров.
// Call calc numbers of cows and bulls function.
// Проверяем равно ли количество быков четырём.
// Если да, поздравляем пользователя с победой.
// If bulls = 4, congrats user.
// Если нет, показываем пользователю количество быков и коров,
// запускаем цикл в котором будем спрашивать пользователя
// очередной вариант ответа и повторять вычисления.
// Show user information about amount of cows and bulls,
// run cycle with asking user next try,
// and do the calculations again.
Начиная с комментариев мы как бы определяем общую структуру программы, дальше нам остаётся только написать код.
// Создаём переменную для числа компьютера
// и сохраняем в нее случайное четырехзначное число.
// Generate random 4 digits number.
var pcNumber = Math.floor(Math.random() * 9000) + 1000;
Для создания переменной в JavaScript нужно её объявить с помощью ключевого слова var
(от английского variable, что означает переменная) , дать ей название (в нашем случае она называется pcNumber
и присвоить ей некоторое значение (этот шаг необязателен, но в нашем случае мы же хотим чтобы в ней хранилось число, задуманное компьютером).
Давайте теперь разберём что происходит в этой строчке кода:
Math.floor(Math.random() * 9000) + 1000;
Здесь мы используем две функции из библиотеки Math
- это стандартная библиотека, которая идёт вместе с языком JavaScript и представляет нам много полезных функций.
Мы используем две функции:
Math.random()
- функция, которая возвращает случайное число от 0 до 1, например 0.1638.Math.floor()
- функция, которая принимает два аргумента и округляет первый в меньшую сторону с точностью, указанной во втором аргументе. Пример:Math.floor(12.7473, 2)
вернёт нам число 12.74. Если второй аргумент не передавать, то функция будет округлять число до целых.
Нам нужно четырехзначное число, это значит что нас интересуют числа от 1000 до 9999, а функция Math.random()
возвращает нам числа от 0 до 1, поэтому мы должны сместить этот диапазон. Для этого мы умножаем случайное число на 9000, в итоге мы получим число от 0 до 8999, но нам нужно, чтобы наше число начиналось с 1000 и было не больше 9999, поэтому к этому числу мы дополнительно прибавляет 1000.
А после этого мы округляем получившееся число до целых используя функцию Math.floor()
.
Давайте посмотрим что у нас получилось. Есть множество разных способов вывести информацию на экран: показать всплывающее окно, вывести в один из элементов на странице, вывести как техническую информацию в консоль. Мы воспользуемся последним способом, и для этого напишем следующую команду:
console.log('компьютер загадал: ' pcNumber);
Если вы запустите свой снипет на выполнение, то в консоле средств отладка (если у вас она не отображается, нажмите клавишу "esc" в левом углу клавиатуры) должны увидеть сгенерированное число
Следующий кусочек кода:
// Спрашиваем у пользователя его вариант ответа.
// Ask user his first guess.
var userNumber = prompt('Ваша первая попытка:');
Здесь мы объявляем ещё одну переменную
userNumber
, и вызываем функцию prompt()
, которая вызывает диалоговое окно для пользователя с текстом и полем ввода.
Если пользователь введёт текст и нажмет "Ok", в нашу переменную будет записано значение из поля, заполненного пользователем. Если пользователь нажимает "Cancel", то ввод будет отменен, и в нашу переменную будет записано значение null
. Null
- специальное значение, которое используется в JavaScript для переменных у которых нужно убрать значение, это не ноль, а именно обозначение что переменная не имеет значения. Также в JavaScript переменная, которой не задали значение изначально будет иметь значение undefined
- это тоже специальный тип значения, который показывает, что переменная ещё не инициализированна. Мы используем значение null
в дальнейшем чтобы выйти из программы, когда пользователю надоест угадывать и он захочет остановить игру.
Текст можно писать любой, это на ваше усмотрение.
Давайте посмотрим что возвращает данная строка, для этого добавим ещё один вывод в консоль:
console.log('То, что ввёл пользователь: ', userNumber);
Итак, у нас число, которое загадал компьютер и число, которое дал пользователь, как вариант ответа. Для поиска коров и быков, нам нужно эти числа разбить на отдельные цифры:
// Вызываем функцию разбиение чисел на отдельные цифры.
// Call devide function for user and pc numbers.
var devidedNumbers = devide(pcNumber, userNumber);
Тут мы объявили новую переменную devidedNumbers
(не бойтесь больших названий переменных, главное чтобы было понятно, что в ней хранится) и присвоили ей результат вызова функции, в которую мы в качестве аргументов передали число компьютера pcNumber
и число пользователя userNumber
. Если мы сейчас запустим программу, то увидим ошибку в консоли
Это потому, что мы вызываем функцию, которую ещё не написали. Давайте её напишем. Последовательность действий будет такая же, как для всей программы, сначала комментарии, потом код:
// Функция разделения чисел на отдельные цифры.
// Function for deviding numbers to separate digits.
// Принимает два числа и возвращает объект с результатом.
// Get two numbers as arguments and return object with result.
// Объявим два пустых массива для хранения цифр каждого числа.
// Define two empty arrays, for storing digits of every number.
// Вычислил и сохраним каждый разряд в элемент массива.
// Calculate and save every digit to array.
// Вернём результат в виде объекта.
// Return result as object.
Ну вот, комментарии написаны, добавим к ним кода. Тут мы будем использовать новый тип данных, который мы ещё не разбирали - "Объект". Я обязательно оставлю ссылку, чтобы вы могли прочитать про него подробно, пока лишь нам хватит того, что объект это удобная форма хранения данных. Он похож на массив, только его элементы имеют текстовые индексы в отличии от массива, который использует числовые индексы. Вот пример объекта, чтобы вам было проще понять суть:
var person = { 'name' : 'Ivan', 'age' : 27 }
Все просто, не так ли?
Вот полный код функции:
// Функция разделения чисел на отдельные цифры.
// Function for deviding numbers to separate digits.
// Принимает два числа и возвращает объект с результатом.
// Get two numbers as arguments and return object with result.
function devide(numberPc, numberUser) {
// Объявим два пустых массива для хранения цифр каждого числа.
// Define two empty arrays, for storing digits of every number.
var numberPcArray = [];
var numberUserArray = [];
// Вычислил и сохраним каждый разряд в элемент массива.
// Calculate and save every digit to array.
numberPcArray[0] = Math.floor(numberPc/1000);
numberPc = numberPc % 1000;
numberUserArray[0] = Math.floor(numberUser/1000);
numberUser = numberUser % 1000;
numberPcArray[1] = Math.floor(numberPc/100);
numberPc = numberPc % 100;
numberUserArray[1] = Math.floor(numberUser/100);
numberUser = numberUser % 100;
numberPcArray[2] = Math.floor(numberPc/10);
numberPcArray[3] = numberPc % 10;
numberUserArray[2] = Math.floor(numberUser/10);
numberUserArray[3] = numberUser % 10;
// Вернём результат в виде объекта.
// Return result as object.
return {
pcNumber: numberPcArray,
userNumber: numberUserArray
};
}
Давайте разберём по порядоку что у нас получилось.
Сначала мы объявляем функцию:
function devide(numberPc, numberUser) {
Ключевое слово function
указывает на то, что мы хотим создать функцию, потом указывается её имя devide
, по которому мы сможем её вызвать и в круглых скобках указываются имена аргументов, которые она может принять. После объявления идёт тело функции, заключенное в фигурные скобки.
Внутри тела функции мы можем использовать аргументы функции, создавать новые переменные, выполнять различные вычисления. В общем функция это небольшой самостоятельный кусочек программы, который мы можем вызвать когда нам удобно. Также функции позволяют следовать принципу DRY в программировании.
DRY - don't repeat yourself. Не повторяй свой код.
Т.е. вместо того чтобы несколько раз писать одинаковый код в разных местах программы, мы можем написать функцию и вызвать её одной строчкой.
Что же происходит внутри функции? А там все достаточно просто, эту часть мы рассмотрели в предыдущем уроке, давайте вспомним.
// Объявим два пустых массива для хранения цифр каждого числа.
// Define two empty arrays, for storing digits of every number.
var numberPcArray = [];
var numberUserArray = [];
Мы объявили два пустых массива, куда будем сохранять отдельные цифры разрядов наших чисел.
// Вычислил и сохраним каждый разряд в элемент массива.
// Calculate and save every digit to array.
numberPcArray[0] = Math.floor(numberPc/1000);
numberPc = numberPc % 1000;
numberUserArray[0] = Math.floor(numberUser/1000);
numberUser = numberUser % 1000;
numberPcArray[1] = Math.floor(numberPc/100);
numberPc = numberPc % 100;
numberUserArray[1] = Math.floor(numberUser/100);
numberUser = numberUser % 100;
numberPcArray[2] = Math.floor(numberPc/10);
numberPcArray[3] = numberPc % 10;
numberUserArray[2] = Math.floor(numberUser/10);
numberUserArray[3] = numberUser % 10;
Тут мы делаем разделение чисел компьютера и пользователя на части и сохраняем в отдельные элементы массива. Кто не помнит как это делается, прошу перечитать второй урок.
И остаётся вернуть результаты наших расчётов. Так как функция может возвращать только одно значение, то два наших массива надо собрать в виде одной переменной, вот тут нам на помощь и приходят объекты. Конечно, мы могли бы создать ещё один массив и положить два наших массива в этот третий как отдельные элементы. Тогда у нас получился бы двумерный массив. Его хорошо использовать для хранения таблиц, и мы в дальнейшем будем с ним работать, когда будем писать игру "Memorize". А сейчас формат объекта, который имеет именованные ключи, удобнее.
// Вернём результат в виде объекта.
// Return result as object.
return {
pcNumber: numberPcArray,
userNumber: numberUserArray
};
Мы возвращаем анонимный объект. Это значит что мы не положили наш объект ни в какую переменную, поэтому у него нет имени. В JavaScript понятие анонимный (ная) используется очень часто. Это позволяет писать меньше кода и сократить количество выполняемых операций.
Итак - наша функция возвращает нам объект с двумя ключами pcNumber
и userNumber
, в каждом ключе сохранен соответствующий массив.
Теперь при вызове нашего кода:
var devidedNumbers = devide(pcNumber, userNumber);
Ошибка больше проявляться не будет, и мы можем вывести в консоль результат выполнения нашей функции:
console.log(devidedNumbers);
Мы увидим наш объект, который содержит два массива:
Числа мы разделили, теперь нам нужно посчитать сколько "быков" и "коров" угадал пользователь. Мы не будем повторять прошлую ошибку, и сразу начнём с написания функции:
// Функция подсчёта быков и коров, принимает два массива разбитых чисел, возвращает объект с двумя ключами.
// Function for calculating numbers of cows and bulls. Gets two arrays of devided numbers and returns object.
function checkCB(pcNumber, userNumber) {
var cows = 0;
var bulls = 0;
for (var i = 0; i < 4; i++) {
for(var j = 0; j < 4; j++) {
if (i === j) {
// Calculate bulls.
if (numbers.pcNumber[i] == numbers.userNumber[j]) {
bulls++;
}
}
else {
if (numbers.pcNumber[i] == numbers.userNumber[j]) {
cows ++;
}
}
}
}
return {
cows: cows,
bulls: bulls
}
}
Давайте разбираться что тут происходит.
Функция принимает два массива с цифрами pcNumber
и userNumber
, эти массивы мы получили на предыдущем шаге, только они у нас лежат в объекте, когда мы будем вызывать эту функцию для расчёта, вы увидите как легко оперировать объектами.
Далее, как в большинстве нашего кода мы определяем две переменных cows
и bulls
и присваиваем им значение ноль - это переменные счётчики, часто им дают названия типа cowsCounter
, чтобы сразу было видно, что это счётчик. Но у нас программа маленькая и эти переменные внутри маленькой функции, поэтому мы оставим короткие названия.
После подготовки мы начинаем первый цикл:
for (var i = 0; i < 4; i++) {
Тут for
это ключевое слово в языке JavaScript для простых циклов, которые должны выполнить действия определённое количество раз. В круглых скобках задаются параметры цикла в следующем порядке:
- Сначала объявляется переменная - счётчик цикла, и ей задается начальное значение.
var i = 0;
- Потом задаётся условие, пока оно истинно, цикл будет выполняться.
i < 4;
В нашем случае цикл будет выполняться, пока счётчик не дойдёт до четырёх, и тогда остановится.
- И последним параметром задаётся, как будет изменяться наша переменная - счётчик, после каждой итерации цикла.
i++
Эта странная запись называется инкремент. А означает она простое увеличение нашей переменой на 1. Эта запись равносильна записи:
i = i + 1
Только значительно короче.
Сразу внутри первого цикла мы запускаем точно такой же второй (вложенный) цикл, и у него своя переменная - счётчик j
.
Первый цикл перебирает цифры первого числа, а второй - второго соответственно.
if (i === j) {
Внутри циклов мы сначала проверяем равны ли переменные-счётчики, и если это так, значит мы будем сравнивать цифры, стоящие на одинаковых позициях, поэтому это "Быки" .
// Calculate bulls.
if (pcNumber[i] == userNumber[j]) {
bulls++;
}
Тут мы сравниваем две цифры: одну из числа компьютера, вторую из числа пользователя. Мы передаём в функцию наши массивы с цифрами, и может получать каждый элемент по индексу, что мы и делаем pcNumber[i]
. Если цифры равны, значит мы нашли "быка", и увеличиваем переменную-счётчик на один, используя уже знакомый вам инкремент: bulls++;
В противном случае (если переменные-счетчики не равны) мы проверяем равенство и увеличиваем количество "коров" на единицу.
else {
if (pcNumber[i] == userNumber[j]) {
cows ++;
}
}
Все тоже самое что и с "быками" только увеличиваем счётчик "коров".
Когда внутренний цикл отработает свое, внешнему цикл начнёт вторую итерации, а внутренний пойдёт все четыре итерации ещё раз, и так пока циклы не пойдут по всем цифрами в наших числах.
Когда циклы свою работу выполнят, мы получим две переменных с количеством "быков" и "коров". Вернём их как объект с двумя ключами:
return {
cows: cows,
bulls: bulls
}
Ну вот, наша функция для подсчёта готова, можно её вызвать, и посмотреть что получается:
var cb = checkCB(numbers.pcNumber, numbers.userNumber);
console.log(cb);
В итоге в консоле мы должны увидеть наш объект:
Наша программа почти готова, все что нам осталось - это проверить результаты и либо поздравить пользователя с победой или спросить у него следующий вариант.
Проверить очень просто, надо просто посмотреть равно ли количество "быков" четырём.
if (cb.bulls == 4) {
console.log('Вы угадали число!');
}
Если быков меньше четырёх, алгоритм будет выполняться дальше и мы можем написать цикл, который будет спрашивать пользователя о следующей попытке, пока тот не нажимает кнопку "Cancel":
// Цикл, пока пользователь не угадает число или отменить ввод.
while (cb.bulls < 4 && userNumber != null) {
userNumber = prompt('Try again: ');
numbers = devide(pcNumber, userNumber);
cb = checkCB(numbers.pcNumber, numbers.userNumber);
}
console.log('Спасибо за игру!');
Тут мы используем другой тип цикла, он называется цикл "пока" (while) - это цикл, который будет выполняться пока истинно условие. Такой цикл удобно использовать, когда мы не знаем точно количество итераций, например как в нашем случае, мы же не знаем сколько пользователю понадобится попыток или когда ему надоест. Также подобный цикл часто используется для работы с файловой системой, когда мы перебираем файлы в какой то папке или считаем файл построчно.
В нашем случае мы задали двойное условие:
- Количество угаданных быков меньше четырёх
cb.bulls < 4
- И пользователь ввёл очередной вариант
userNumber != null
Пока эти условия выполняются, будет выполняться и тело цикла.
Между двумя условиями стоит &&
это логическое "и", оно означает что все условие будет считаться истинным, только если оба условия (слова и справа от "и") будут истины. Я дам вам ссылку, чтобы прочитать больше о логических операциях.
Ну а внутри вызываются все теже функции
prompt
, devide
и checkCB
, которые и составляют основу нашей программы.
В вот и наша первая игра. Уберите из неё технический вывод числа, которое загадал компьютер и попробуйте отгадать.
Полный код программы можете посмотреть тут:
https://yadi.sk/d/ZFl1f9B93Tb9sx
В нашей реализации есть одна ошибка, которая влиять на подсчёт "коров" и "быков", а именно то, что компьютер должен загадать число в котором все цифры разные, пока это не так. В следующем занятии мы это оставим, сделаем оптимизацию нашего кода, создаём его в систему контроля версий и перейдём к использованию редактора.
Если у вас возникли вопросы или что - то не получилось, пишите комментарии, будем разбираться вместе. Может есть предложения, нашли какие - то неточности, хотите предложить написать какую-то свою игру, тоже пишите, я всегда готов идти навстречу и помогать!
Ссылки для более глубоко изучения:
- Средства отладки браузера Chrome Мы поработаем плотнее со средствами отладки в следующих уроках.
- Работа с числами в JavaScript и математические операции.
- Взаимодействие с пользователем: alert, prompt, confirm
- Объекты как ассоциативные массивы - это только маленький кусочек информации об объектах в JavaScript, с использованием объектов в объектно-ориентированном программировании мы поговорим с вами позже, когда будем программировать игру "Memories".
- DRY принцип. - описан несколько сложно. Вот статья попроще, которая также описывает ещё два очень важных подхода в программировании Три ключевых принципа ПО, которые вы должны понимать.
- Циклы в JavaScript - while и for.
- Основные операторы в JavaScript.
- Логические операторы.
Ссылки на предыдущие уроки:
- Программирование. Как работает компьютер. Что такое алгоритм.
- Программирование игр. Урок 2. Основные структуры в алгоритме.
Теги которые я буду использовать и по которым можно будет найти мои статьи:
программированиеигр,
javascript,
phaserjs.
P. S. Данный пост также подготовлен с помощью редактора Markor на телефоне, но в этот раз мне был необходим компьютер для создания скриншотов, а так же сервис telegra.ph для загрузки картинок. Следующий пост о системе контроля версий, редакторах и улучшении нашей игры будет готовиться по большей части на компьютере.
P. P. S. Картинки быка и коровы от Виктории мы не использовали в этом уроке, я оставил их на следующий урок, мы создадим интерфейс для нашей игры и там их применим.
P. P. P. S. Как написать такую игру на телефоне? Я использовал программу JS Run для Android (JS Run).