До настоящего момента все внимание мы уделяли серьезным областям применения сценариев, чтобы улучшить взаимодействие с системой и сделать систему более гибкой и мощной. Но существует еще одна область, которую стоит рассмотреть: игры.
Не волнуйтесь — мы не предлагаем вам написать Fallout 4. Просто так получилось, что некоторые простые игры легко можно создать в виде сценариев командной оболочки. И разве не лучше учиться отладке сценариев на примере чего-то более забавного, чем утилита для приостановки действия учетной записи или анализа журнала ошибок Apache?
Для некоторых сценариев вам потребуются файлы, размещенные по адресу: http://www.nostarch.com/wcss2/. Загрузите этот архив прямо сейчас, если вы его еще не скачали.
ДВА КОРОТКИХ ТРЮКА
Здесь мы покажем два коротких примера, чтобы пояснить, что мы имеем в виду. Во-первых, пользователи, заставшие Usenet, хорошо знают алгоритм rot13, помогающий сделать непристойные шуточки и оскорбления менее читаемыми. Это алгоритм подстановочного шифрования, поразительно легко реализуемый в Unix.
Чтобы что-то зашифровать по алгоритму rot13, это «что-то» нужно передать команде tr:
tr '[a-zA-Z]' '[n-za-mN-ZA-M]'
Например:
$ echo "So two people walk into a bar..." | tr '[a-zA-Z]' '[n-za-mN-ZA-M]'
Fb gjb crbcyr jnyx vagb n one...
Чтобы вернуть строке читаемый вид, достаточно применить то же преобразование:
$ echo 'Fb gjb crbcyr jnyx vagb n one...' | tr '[a-zA-Z]' '[n-za-mN-ZA-M]'
So two people walk into a bar...
Известно, что этот подстановочный шифр использовался в фильме «2001: A Space Odyssey» . Помните, как там назывался компьютер? Проверим его:
$ echo HAL | tr '[a-zA-Z]' '[b-zaB-ZA]'
IBM
Другой короткий пример — проверка палиндромов. Введите что-нибудь, что на ваш взгляд является палиндромом, и этот код проверит его:
testit="$(echo $@ | sed 's/[^[:alpha:]]//g' | tr '[:upper:]' '[:lower:]')"
backward="$(echo $testit | rev)"
if [ "$testit" = "$backward" ] ; then
echo "$@ is a palindrome"
else
echo "$@ is not a palindrome"
fi
Палиндромом называется слово, которое одинаково читается слева направо и справа налево, поэтому первым делом код удаляет все неалфавитные символы и преобразует все буквы в нижний регистр. Затем Unix-утилита rev переворачивает задом наперед строку, полученную ею из стандартного ввода. Если прямая и перевернутая версии совпадают, значит, проверяемая строка является палиндромом; если они различаются, это не палиндром.
Показанные далее игры немногим сложнее, но все они достаточно забавны, чтобы вы добавили их в свою систему.
Это простейшая игра в анаграммы. Если вы когда-нибудь сталкивались с подобными играми, то легко поймете суть: выбирается случайное слово, и буквы в нем переставляются случайным образом. Задача игрока — угадать исходное слово за как можно меньшее число попыток. Полный сценарий, реализующий эту игру, приводится в листинге 12.1, но, чтобы получить список слов, вам также нужно загрузить файл long-words.txt из ресурсов книги http://www.nostarch.com/wcss2/ и сохранить его в каталоге /usr/lib/games.
Листинг 12.1. Игровой сценарий unscramble
#!/bin/bash
# unscramble -- выбирает слово, кодирует его, переставляя буквы,
# и предлагает пользователю угадать исходное слово (или фразу).
wordlib="/usr/lib/games/long-words.txt"
scrambleword()
{
# Выбирает случайное слово из wordlib и кодирует его.
# Исходное слово сохраняется в $match, закодированное -- в $scrambled.
match="$(randomquote $wordlib)"
echo "Picked out a word!"
len=${#match}
scrambled=""; lastval=1
for (( val=1; $val < $len ; ))
do
if [ $(($RANDOM % 2)) -eq 1 ] ; then
scrambled=$scrambled$(echo $match | cut -c$val)
else
scrambled=$(echo $match | cut -c$val)$scrambled
fi
val=$(( $val + 1 ))
done
}
if [ ! -r $wordlib ] ; then
echo "$0: Missing word library $wordlib" >&2
echo "(online: http://www.intuitive.com/wicked/examples/long-words.txt" >&2
echo "save the file as $wordlib and you're ready to play!)" >&2
exit 1
fi
newgame=""; guesses=0; correct=0; total=0
until [ "$guess" = "quit" ] ; do
scrambleword
echo ""
echo "You need to unscramble: $scrambled"
guess="??" ; guesses=0
total=$(( $total + 1 ))
while [ "$guess" != "$match" -a "$guess" != "quit" -a "$guess" != "next" ]
do
echo ""
/bin/echo -n "Your guess (quit|next) : "
read guess
if [ "$guess" = "$match" ] ; then
guesses=$(( $guesses + 1 ))
echo ""
echo "*** You got it with tries = ${guesses}! Well done!! ***"
echo ""
correct=$(( $correct + 1 ))
elif [ "$guess" = "next" -o "$guess" = "quit" ] ; then
echo "The unscrambled word was \"$match\". Your tries: $guesses"
else
echo "Nope. That's not the unscrambled word. Try again."
guesses=$(( $guesses + 1 ))
fi
done
done
echo "Done. You correctly figured out $correct out of $total scrambled words."
exit 0
Для выбора случайной строки из файла используется randomquote (сценарий № 68 из главы 8) , несмотря на то что первоначально этот сценарий был написан для работы с веб-страницами (многие хорошие утилиты Unix оказываются полезными в контекстах, отличных от тех, для которых они создавались).
Сложнее всего было реализовать кодирование слова. В Unix нет удобных утилит для этого, но, оказывается, слово можно закодировать непредсказуемым способом, если выполнить последовательный перебор букв в нем и случайно добавлять каждую следующую букву в начало или в конец закодированной последовательности .
Обратите внимание на местоположение $scrambled в двух строках: в первой следующая буква добавляется в конец $scrambled, а во второй — в начало.
В остальном логика игры должна быть понятна: внешний цикл until выполняется, пока пользователь не введет слово quit в качестве ответа, а внутренний цикл while выполняется, пока пользователь не угадает слово или не введет next, чтобы перейти к следующему слову.
Этот сценарий не имеет аргументов, так что просто введите его имя в командной строке и начните игру!
После запуска сценарий выводит закодированные слова разной длины и подсчитывает количество слов, угаданных пользователем, как показано в листинге 12.2.
Листинг 12.2. Запуск игрового сценария unscramble
$ unscramble
Picked out a word!
You need to unscramble: ninrenoccg
Your guess (quit|next) : concerning
*** You got it with tries = 1! Well done!! ***
Picked out a word!
You need to unscramble: esivrmipod
Your guess (quit|next) : quit
The unscrambled word was "improvised". Your tries: 0
Done. You correctly figured out 1 out of 2 scrambled words.
Первая же попытка оказалась успешной, и это вдохновляет!
Вывод подсказки, хоть в каком-нибудь виде, сделал бы игру более интересной, также пригодился бы флаг, позволяющий ограничивать минимальную длину слова. В первом случае можно было бы за определенные штрафные очки показывать первые n букв из исходного слова и в ответ на каждый запрос подсказки добавлять еще одну букву. Для второго случая необходимо расширить словарь, так как предлагаемый вместе со сценарием содержит слова не короче 10 букв — хитро!
Несмотря на жутковатую идею, игра в «виселицу» давно стала классикой. Вы пытаетесь угадать, какие буквы есть в задуманном слове, и каждый раз, когда вы ошибаетесь, у человечка на виселице дорисовывается очередная часть тела. Когда ошибок оказывается слишком много, появляется полное изображение, что означает проигрыш и... как вы понимаете, смерть человечка. Довольно безжалостная игра!
Однако сама по себе игра довольно забавная, а ее реализация в виде сценария командной оболочки оказывается на удивление простой, как демонстрирует листинг 12.3. Вам также потребуется список слов, использовавшийся в сценарии № 83: сохраните файл long-words.txt из ресурсов книги в каталоге /usr/lib/games.
Листинг 12.3. Игровой сценарий hangman
#!/bin/bash
# hangman -- простая версия игры "виселица". Вместо постепенного рисования
# человечка он просто ведет обратный отсчет ошибочных попыток.
# В единственном необязательном аргументе сценарий принимает начальное
# расстояние до эшафота.
wordlib="/usr/lib/games/long-words.txt"
empty="\." # Нам нужно что-то для sed [set], когда $guessed="".
games=0
# Сначала проверить наличие библиотеки слов -- файла wordlib.
if [ ! -r "$wordlib" ] ; then
echo "$0: Missing word library $wordlib" >&2
echo "(online: http://www.intuitive.com/wicked/examples/long-words.txt" >&2
echo "save the file as $wordlib and you're ready to play!)" >&2
exit 1
fi
# Большой цикл while. Здесь все и происходит.
while [ "$guess" != "quit" ] ; do
match="$(randomquote $wordlib)" # Выбрать новое слово из библиотеки.
if [ $games -gt 0 ] ; then
echo ""
echo "*** New Game! ***"
fi
games="$(( $games + 1 ))"
guessed="" ; guess="" ; bad=${1:-6}
partial="$(echo $match | sed "s/[^$empty${guessed}]/-/g")"
# В этом блоке производится:
# ввод буквы > анализ > вывод результатов > переход к началу.
while [ "$guess" != "$match" -a "$guess" != "quit" ] ; do
echo ""
if [ ! -z "$guessed" ] ; then # ! –z означает "непустое значение".
/bin/echo -n "guessed: $guessed, "
fi
echo "steps from gallows: $bad, word so far: $partial"
/bin/echo -n "Guess a letter: "
read guess
echo ""
if [ "$guess" = "$match" ] ; then # Угадано!
echo "You got it!"
elif [ "$guess" = "quit" ] ; then # Запрошен выход? Хорошо.
exit 0
# Теперь нужно проверить присутствие введенной буквы в слове.
elif [ $(echo $guess | wc -c | sed 's/[^[:digit:]]//g') -ne 2 ] ; then
echo "Uh oh: You can only guess a single letter at a time"
elif [ ! -z "$(echo $guess | sed 's/[[:lower:]]//g')" ] ; then
echo "Uh oh: Please only use lowercase letters for your guesses"
elif [ -z "$(echo $guess | sed "s/[$empty$guessed]//g")" ] ; then
echo "Uh oh: You have already tried $guess"
# Теперь можно проверить присутствие буквы в слове.
elif [ "$(echo $match | sed "s/$guess/-/g")" != "$match" ] ; then
guessed="$guessed$guess"
partial="$(echo $match | sed "s/[^$empty${guessed}]/-/g")"
if [ "$partial" = "$match" ] ; then
echo "** You've been pardoned!! Well done! The word was \"$match\"."
guess="$match"
else
echo "* Great! The letter \"$guess\" appears in the word!"
fi
elif [ $bad -eq 1 ] ; then
echo "** Uh oh: you've run out of steps. You're on the platform..."
echo "** The word you were trying to guess was \"$match\""
guess="$match"
else
echo "* Nope, \"$guess\" does not appear in the word."
guessed="$guessed$guess"
bad=$(( $bad - 1 ))
fi
done
done
exit 0
Все проверки в этом сценарии достаточно интересны, чтобы исследовать их подробнее. Взгляните на строку , которая проверяет, не ввел ли пользователь больше одного символа.
Почему в сравнении используется число 2, а не 1? Потому что к предложенной пользователем букве в результате нажатия клавиши Enter добавляется символ перевода строки (\n). То есть правильно, когда строка содержит два символа, а не один. Команда sed в этой инструкции отбрасывает все нецифровые символы, чтобы исключить любые проблемы с начальными символами табуляции, которые так любит добавлять команда wc.
Проверка принадлежности к нижнему регистру в строке проста и понятна. Здесь удаляются все буквы нижнего регистра, и результат проверяется на равенство пустой строке.
Наконец, чтобы увидеть, предлагал ли уже пользователь эту букву: содержимое guess преобразуется так, что из него удаляются все буквы, присутствующие в переменной guessed. Затем проверяется, получилась ли в результате пустая строка .
Кроме всех этих проверок в сценарии hangman используется еще одна хитрость: в исходном слове все буквы, совпадающие с введенной, заменяются дефисами. Затем результат сравнивается с исходным словом . Если они различаются (то есть если одна или несколько букв заменены дефисами), значит, предложенная пользователем буква есть в слове. Например, если для загаданного слова cat пользователь ввел букву a, она будет записана в переменную guessed, которая получит значение '-a-'.
Одна из ключевых идей, лежащих в основе сценария hangman, — вывод частично угаданного слова из переменной partial, значение которой повторно собирается с каждой угаданной буквой. Поскольку в переменной guessed накапливаются буквы, угаданные игроком, команда sed преобразует в дефисы все буквы в исходном слове, которые отсутствуют в строке guessed .
Игра «виселица» принимает один необязательный аргумент: переданное ей число используется как максимально допустимое число ошибочных попыток, в противном случае их будет шесть (значение по умолчанию). В листинге 12.4 демонстрируется пример запуска сценария hangman без аргументов.
Листинг 12.4. Сеанс игры «виселица»
$ hangman
steps from gallows: 6, word so far: -------------
Guess a letter: e
* Great! The letter "e" appears in the word!
guessed: e, steps from gallows: 6, word so far: -e--e--------
Guess a letter: i
* Great! The letter "i" appears in the word!
guessed: ei, steps from gallows: 6, word so far: -e--e--i-----
Guess a letter: o
* Great! The letter "o" appears in the word!
guessed: eio, steps from gallows: 6, word so far: -e--e--io----
Guess a letter: u
* Great! The letter "u" appears in the word!
guessed: eiou, steps from gallows: 6, word so far: -e--e--iou---
Guess a letter: m
* Nope, "m" does not appear in the word.
guessed: eioum, steps from gallows: 5, word so far: -e--e--iou---
Guess a letter: n
* Great! The letter "n" appears in the word!
guessed: eioumn, steps from gallows: 5, word so far: -en-en-iou---
Guess a letter: r
* Nope, "r" does not appear in the word.
guessed: eioumnr, steps from gallows: 4, word so far: -en-en-iou---
Guess a letter: s
* Great! The letter "s" appears in the word!
guessed: eioumnrs, steps from gallows: 4, word so far: sen-en-ious--
Guess a letter: t
* Great! The letter "t" appears in the word!
guessed: eioumnrst, steps from gallows: 4, word so far: sententious--
Guess a letter: l
* Great! The letter "l" appears in the word!
guessed: eioumnrstl, steps from gallows: 4, word so far: sententiousl-
Guess a letter: y
** You’ve been pardoned!! Well done! The word was "sententiously".
*** New Game! ***
steps from gallows: 6, word so far: ----------
Guess a letter: quit
В сценарии командной оболочки трудно реализовать вывод графического изображения человечка на виселице, поэтому мы использовали альтернативу: подсчет «шагов до эшафота». Однако вы настроены серьезно, попробуйте использовать серии предопределенных фрагментов текстовой псевдографики, по одному для каждого шага, и выводить их по мере игры. Или поищите какую-нибудь другую метафору, не связанную с насилием!
Обратите внимание, что одно и то же слово может быть выбрано несколько раз в одном сеансе игры, хотя со списком по умолчанию, насчитывающим 2882 варианта, это маловероятно. Однако если вы все же беспокоитесь, можно добавить переменную для хранения всех использованных слов и при выборе нового сверяться с ней.
Наконец, попробуйте сортировать угаданные буквы в переменной guessed в алфавитном порядке. Это можно сделать несколькими способами, но мы советуем попробовать связку sed|sort.
Теперь, когда у нас есть инструмент выбора случайной строки из файла, перед нами открываются широкие перспективы в создании самых разных игр-викторин. Мы поместили в список столицы всех 50 штатов США, доступный для загрузки по адресу: http://www.nostarch.com/wcss2/. Сохраните файл state.capitals.txt в своем каталоге /usr/lib/games. Сценарий в листинге 12.5 выбирает случайную строку из файла, выводит название штата и предлагает пользователю ввести название его столицы.
Листинг 12.5. Сценарий states? реализующий игру «Угадай столицу»
#!/bin/bash
# states -- игра "Угадай столицу". Требует наличия файла со списком
# штатов и их столиц state.capitals.txt.
db="/usr/lib/games/state.capitals.txt" # Формат: Штат[табуляция]Город.
if [ ! -r "$db" ] ; then
echo "$0: Can't open $db for reading." >&2
echo "(get state.capitals.txt" >&2
echo "save the file as $db and you're ready to play!)" >&2
exit 1
fi
guesses=0; correct=0; total=0
while [ "$guess" != "quit" ] ; do
thiskey="$(randomquote $db)"
# $thiskey -- выбранная строка. Теперь нужно извлечь название штата
# и города, и затем создать версию названия города со всеми буквами
# в нижнем регистре для сопоставления.
state="$(echo $thiskey | cut -d\ -f1 | sed 's/-/ /g')"
city="$(echo $thiskey | cut -d\ -f2 | sed 's/-/ /g')"
match="$(echo $city | tr '[:upper:]' '[:lower:]')"
guess="??" ; total=$(( $total + 1 )) ;
echo ""
echo "What city is the capital of $state?"
# Главный цикл, где все и происходит. Сценарий выполняет его, пока
# город не будет правильно угадан, или пока пользователь не введет
# "next", чтобы пропустить штат, или "quit", чтобы завершить игру.
while [ "$guess" != "$match" -a "$guess" != "next" -a "$guess" != "quit" ]
do
/bin/echo -n "Answer: "
read guess
if [ "$guess" = "$match" -o "$guess" = "$city" ] ; then
echo ""
echo "*** Absolutely correct! Well done! ***"
correct=$(( $correct + 1 ))
guess=$match
elif [ "$guess" = "next" -o "$guess" = "quit" ] ; then
echo ""
echo "$city is the capital of $state." # Вы ДОЛЖНЫ это знать :)
else
echo "I'm afraid that's not correct."
fi
done
done
echo "You got $correct out of $total presented."
exit 0
Для такой увлекательной игры сценарий states имеет очень простую реализацию. Файл с данными хранит пары штат/столица, все пробелы в названиях заменены дефисами, и два поля разделены единственным пробелом. Благодаря этому извлечение названий городов и штатов осуществляется очень просто .
Каждая попытка сравнивается с версией названия города, состоящей только из букв нижнего регистра (match), и с версией, где все буквы стоят в правильном регистре. Если ни в одном случае нет совпадения, введенное слово сравнивается с двумя командами: next и quit. Если обнаружится совпадение с любой из них, сценарий выводит правильный ответ и либо предлагает ввести столицу следующего штата, либо завершается. Если не найдено ни одного совпадения, попытка считается неудачной.
Этот сценарий не имеет аргументов, флагов или команд. Просто запустите его и играйте!
Готовы проверить свое знание столиц штатов? Листинг 12.6 демонстрирует, насколько хорошо мы знаем столицы!
Листинг 12.6. Запуск игрового сценария states
$ states
What city is the capital of Indiana?
Answer: Bloomington
I’m afraid that’s not correct.
Answer: Indianapolis
*** Absolutely correct! Well done! ***
What city is the capital of Massachusetts?
Answer: Boston
*** Absolutely correct! Well done! ***
What city is the capital of West Virginia?
Answer: Charleston
*** Absolutely correct! Well done! ***
What city is the capital of Alaska?
Answer: Fairbanks
I’m afraid that’s not correct.
Answer: Anchorage
I’m afraid that’s not correct.
Answer: Nome
I’m afraid that’s not correct.
Answer: Juneau
*** Absolutely correct! Well done! ***
What city is the capital of Oregon?
Answer: quit
Salem is the capital of Oregon.
You got 4 out of 5 presented.
К счастью, игра запоминает количество только успешных попыток, а также не фиксирует, сколько раз вы обращались к Google, чтобы узнать ответ!
Самый большой недостаток этой игры, пожалуй, ее придирчивость к правописанию. Неплохим усовершенствованием стал бы код, реализующий нечеткое сопоставление: чтобы, например, ответ пользователя Juneu вместо Juneau расценивался как правильный. Для этого можно было бы использовать модифицированный алгоритм Soundex (созвучия), который удаляет все гласные и из удвоенных согласных оставляет только одну (например, название Annapolis было бы преобразовано в npls). Это может оказаться слишком вольной трактовкой, но сама идея стоит того, чтобы ее рассмотреть.
Так же, как в других играх, в этой пригодились бы подсказки. Например, можно по запросу вывести первую букву правильного ответа и запоминать, как много подсказок запросил пользователь в сеансе игры.
Несмотря на то что эта игра написана для столиц штатов, ее легко переделать для работы с любыми парами данных. Например, создать отдельный файл для Италии, с соответствиями страна/валюта или политик/партия. Как мы не раз видели в Unix, достаточно универсальные сценарии порой можно использовать совершенно неожиданными и непредусмотренными ранее способами.
Простыми называют числа, которые делятся без остатка только на самих себя, например 7. С другой стороны, 6 и 8 не являются простыми числами. Простые однозначные числа распознаются легко, но с большими приходится попотеть.
В математике имеется несколько приемов определения простых чисел, но мы воспользуемся методом простого перебора и будем пытаться применить все возможные делители, сравнивая остаток от деления с нулем, как показано в листинге 12.7.
Листинг 12.7. Сценарий isprime
#!/bin/bash
# isprime -- получает число и проверяет, является ли оно простым.
# Использует прием, известный как пробное деление: просто перебирает
# числа от 2 до (n/2) и пытается использовать их в качестве делителя,
# проверяя остаток от деления.
counter=2
remainder=1
if [ $# -eq 0 ] ; then
echo "Usage: isprime NUMBER" >&2
exit 1
fi
number=$1
# 3 и 2 -- простые числа, 1 -- нет.
if [ $number -lt 2 ] ; then
echo "No, $number is not a prime"
exit 0
fi
# Теперь выполним некоторые вычисления.
while [ $counter -le $(expr $number / 2) -a $remainder -ne 0 ]
do
remainder=$(expr $number % $counter) # '/’ деление, '%' остаток
# echo " for counter $counter, remainder = $remainder"
counter=$(expr $counter + 1)
done
if [ $remainder -eq 0 ] ; then
echo "No, $number is not a prime"
else
echo "Yes, $number is a prime"
fi
exit 0
Основу сценария составляет цикл while , поэтому уделим ему основное внимание. Для числа 77 условное выражение в заголовке цикла приняло бы вид:
while [ 2 -le 38 -a 1 -ne 0 ]
Очевидно, что оно не выполняется: 77 не делится нацело на 2. Каждый раз, когда код проверяет потенциальный делитель ($counter) и обнаруживает, что исходное число не делится на него нацело, он вычисляет остаток ($number % $counter), увеличивает $counter на 1 и повторяет вычисления.
Давайте выберем несколько чисел, которые на первый взгляд кажутся простыми, и проверим их, как показано в листинге 12.8.
Листинг 12.8. Опробование сценария isprime с несколькими числами
$ isprime 77
No, 77 is not a prime
$ isprime 771
No, 771 is not a prime
$ isprime 701
Yes, 701 is a prime
Если вам любопытно, раскомментируйте команду echo в цикле while, чтобы увидеть, как выполняются вычисления, и получить представление, насколько быстро или медленно сценарий находит делитель, делящий число нацело, без остатка. В листинге 12.9 показано, что в этом случае получается при попытке проверить число 77.
Листинг 12.9. Запуск сценария с раскомментированной отладочной строкой
$ isprime 77
for counter 2, remainder = 1
for counter 3, remainder = 2
for counter 4, remainder = 1
for counter 5, remainder = 2
for counter 6, remainder = 5
for counter 7, remainder = 0
No, 77 is not a prime
Сценарий реализует не самый эффективный алгоритм проверки числа, который работает тем медленнее, чем больше число. Например, взгляните на условие в цикле while. Сценарий раз за разом продолжает вычислять выражение $(expr $number / 2), тогда как его можно было вычислить только один раз и использовать вычисленное значение во всех последующих итерациях, сэкономив время на запуске подоболочки и вызове команды expr для вычисления значения, которое не изменяется ни на йоту до самой последней итерации.
Существуют также другие, не такие примитивные способы проверки простых чисел, включая замечательный алгоритм с интересным названием «Решето Эратосфена», более современный алгоритм «Решето Сундарама» или более сложный — «Решето Аткина». Поищите их описания в Интернете и проверьте с их помощью номер своего телефона (без дефисов!).
Это удобный сценарий для всех, кто любит играть в настольные игры, особенно ролевые, такие как Dungeons & Dragons.
В таких играх обычно бросаются кости, это может быть и один двадцатигранник, и шесть шестигранников, но в любом случае процесс основан на вероятностях. Игральные кости — по сути, простейший генератор случайных чисел, независимо от того, сколько таких генераторов задействовано в игре: один, два (Monopoly или Trouble) или больше.
Все эти случаи, как оказывается, легко смоделировать, что и делает сценарий в листинге 12.10, позволяющий указать количество и тип костей и затем «бросающий» их и возвращающий сумму.
Листинг 12.10. Сценарий rolldice
#!/bin/bash
# rolldice -- анализирует количество и тип костей и имитирует их броски.
# Примеры: d6 = один шестигранник
# 2d12 = два двенадцатигранника у каждого
# d4 3d8 2d20 = один четырехгранник, три восьмигранника
# и два двадцатигранника.
rolldie()
{
dice=$1
dicecount=1
sum=0
# Первый шаг: представить аргумент в формате MdN.
if [ -z "$(echo $dice | grep 'd')" ] ; then
quantity=1
sides=$dice
else
quantity=$(echo $dice | cut -dd -f1)
if [ -z "$quantity" ] ; then # Пользователь указал dN, а не просто N.
quantity=1
fi
sides=$(echo $dice | cut -dd -f2)
fi
echo "" ; echo "rolling $quantity $sides-sided die"
# Бросить кубик...
while [ $dicecount -le $quantity ] ; do
roll=$(( ( $RANDOM % $sides ) + 1 ))
sum=$(( $sum + $roll ))
echo " roll #$dicecount = $roll"
dicecount=$(( $dicecount + 1 ))
done
echo "I rolled $dice and it added up to $sum"
}
while [ $# -gt 0 ] ; do
rolldie $1
sumtotal=$(( $sumtotal + $sum ))
shift
done
echo ""
echo "In total, all of those dice add up to $sumtotal"
echo ""
exit 0
Сценарий вращается вокруг простой строки кода, которая вызывает генератор случайных чисел оболочки bash, используя говорящую ссылку $RANDOM . Это ключевая строка в сценарии, всё остальное лишь внешнее оформление.
Другой интересный фрагмент — где выполняется анализ описания костей , потому что сценарий поддерживает все три формы: 3d8, d6 и 20. Это стандартная игровая нотация: количество костей + d + количество граней. Например, 2d6 означает: две кости с шестью гранями у каждой. Проверьте себя — сумеете ли вы сами разобраться, как обрабатываются все формы.
Для такого простого кода сценарий выводит довольно много информации. Возможно, у вас появится желание скорректировать вывод под свои предпочтения, но здесь, как видите, он служит в основном, чтобы показать, что количество и форма костей были правильно опознаны.
Споткнулись о команду cut ? Напомним, что флаг -d определяет разделитель полей, то есть -dd просто сообщает, что роль разделителя играет буква d, которая должна присутствовать в данной конкретной нотации.
Начнем с простого: в листинге 12.11 мы используем два шестигранных кубика, как в игре Monopoly.
Листинг 12.11. Проверка сценария на примере пары шестигранных кубиков
$ rolldice 2d6
rolling 2 6-sided die
roll #1 = 6
roll #2 = 2
I rolled 2d6 and it added up to 8
In total, all of those dice add up to 8
$ rolldice 2d6
rolling 2 6-sided die
roll #1 = 4
roll #2 = 2
I rolled 2d6 and it added up to 6
In total, all of those dice add up to 6
Обратите внимание, что при первом «броске» двух кубиков на них выпали числа 6 и 2, а во второй раз — 4 и 2.
А можно сыграть в Yahtzee (в кости)? Легко. Бросим пять шестигранных кубиков, как показано в листинге 12.12.
Листинг 12.12. Проверка сценария на примере пяти шестигранных кубиков
$ rolldice 5d6
rolling 5 6-sided die
roll #1 = 2
roll #2 = 1
roll #3 = 3
roll #4 = 5
roll #5 = 2
I rolled 5d6 and it added up to 13
In total, all of those dice add up to 13
Не самый удачный бросок: 1, 2, 2, 3, 5. Если бы мы играли в кости, мы оставили бы пару двоек и бросили бы остальные кубики еще раз.
Но самый интересный момент наступает, когда в игре используется несколько разных кубиков. В листинге 12.13 мы попытались бросить два кубика с 18 гранями, один с 37 гранями и один с 3 гранями (нас не волнуют ограничения трехмерных геометрических фигур).
Листинг 12.13. Запуск сценария с комплектом разнородных кубиков
$ rolldice 2d18 1d37 1d3
rolling 2 18-sided die
roll #1 = 16
roll #2 = 14
I rolled 2d18 and it added up to 30
rolling 1 37-sided die
roll #1 = 29
I rolled 1d37 and it added up to 29
rolling 1 3-sided die
roll #1 = 2
I rolled 1d3 and it added up to 2
In total, all of those dice add up to 61
Круто, правда? Несколько дополнительных бросков этого набора кубиков дали нам 22, 49 и 47. Теперь вы, игроки, можете это!
Трудно придумать какие-нибудь усовершенствования к этому сценарию, потому что он решает очень простую задачу. Единственное, что можно порекомендовать, — скорректировать вывод. Например, вполне достаточно было бы результата: 5d6: 2 3 1 3 7 = 16.
Завершим главу сценарием, в котором реализуется карточная игра «раз-два» (Acey Deucey). А значит, нам потребуется создать и «перетасовать» колоду игральных карт, чтобы получить случайный результат. Это довольно сложно, зато функции, которые мы напишем, дадут вам общее решение, которое вы сможете использовать для создания более сложных игр, таких как «очко» (blackjack) и даже «пьяница» (rummy) или «рыбалка» (Go Fish).
Суть игры проста. Раздаются две карты, и игрок пытается угадать: окажется ли числовое значение третьей в промежутке значений этих двух. Затем вскрывается третья карта. Масть не важна; важны достоинства карт и их комбинации. То есть если выпали шестерка червей и девятка треф, а третья выпавшая карта — шестерка бубен, комбинация проигрышная. Четверка пик тоже даст проигрышную комбинацию, а семерка треф — выигрышную.
Итак, перед нами стоит две задачи: сымитировать полную колоду карт и логику самой игры, включая предложение пользователю ввести свою догадку. Ах да, еще одно правило: если первые две карты имеют одинаковое достоинство, нет никакого смысла угадывать, потому что в этом случае нельзя победить.
Сценарий обещает быть интересным. Готовы? Тогда переходите к листингу 12.14.
Листинг 12.14. Игровой сценарий aceydeucey
#!/bin/bash
# aceyduecey: Дилер выкладывает две карты, и вы должны угадать, попадает ли
# числовое значение следующей карты в колоде между значениями этих двух
# карт. Например, выпали 6 и 8, если следующая карта окажется 7, вы выиграли
# если 9 -- проиграли.
function initializeDeck
{
# Создать колоду карт.
card=1
while [ $card –le 52 ] # 52 карты в колоде. Вы это знаете, правда?
do
deck[$card]=$card
card=$(( $card + 1 ))
done
}
function shuffleDeck
{
# Это не настоящее перемешивание. Это случайное извлечение значений карт
# из массива 'deck' и создание массива newdeck[], представляющего
# "перемешанную" колоду.
count=1
while [ $count != 53 ]
do
pickCard
newdeck[$count]=$picked
count=$(( $count + 1 ))
done
}
function pickCard
{
# Это самая интересная функция: выбор случайной карты из колоды.
# Поиск карты осуществляется в массиве deck[].
local errcount randomcard
threshold=10 # Максимальное число попыток, прежде чем признать неудачу
errcount=0
# Выбирает из колоды случайную карту, которая еще не была выбрана,
# выполняя не более $threshold попыток. В случае неудачи (чтобы
# избежать зацикливания, когда раз за разом выполняется попытка
# извлечь уже извлеченную карту) выполняется переход к запасному
# варианту.
while [ $errcount -lt $threshold ]
do
randomcard=$(( ( $RANDOM % 52 ) + 1 ))
errcount=$(( $errcount + 1 ))
if [ ${deck[$randomcard]} -ne 0 ] ; then
picked=${deck[$randomcard]}
deck[$picked]=0 # Выбрали -- удалить ее.
return $picked
fi
done
# Если сценарий оказался здесь, значит, он не смог выбрать случайную карту,
# поэтому дальше просто продолжается последовательный обход массива
# до обнаружения первой доступной карты.
randomcard=1
while [ ${newdeck[$randomcard]} -eq 0 ]
do
randomcard=$(( $randomcard + 1 ))
done
picked=$randomcard
deck[$picked]=0 # Выбрали -- удалить ее.
return $picked
}
function showCard
{
# Функция использует операции деления и взятия остатка для определения
# масти и достоинства, хотя в этой игре значение имеет только достоинство.
# Однако для представления важно иметь полную информацию.
card=$1
if [ $card -lt 1 -o $card -gt 52 ] ; then
echo "Bad card value: $card"
exit 1
fi
# деление и взятие остатка -- школьные годы не были потрачены впустую!
suit="$(( ( ( $card - 1) / 13 ) + 1))"
rank="$(( $card % 13))"
case $suit in
1 ) suit="Hearts" ;;
2 ) suit="Clubs" ;;
3 ) suit="Spades" ;;
4 ) suit="Diamonds" ;;
* ) echo "Bad suit value: $suit"
exit 1
esac
case $rank in
0 ) rank="King" ;;
1 ) rank="Ace" ;;
11) rank="Jack" ;;
12) rank="Queen" ;;
esac
cardname="$rank of $suit"
}
function dealCards
{
# В игре Раз-два раздаются две карты...
card1=${newdeck[1]} # Колода перетасована, поэтому
card2=${newdeck[2]} # выдаем две верхних карты
card3=${newdeck[3]} # и снимаем третью, но пока не показываем.
rank1=$(( ${newdeck[1]} % 13 )) # И сразу определяем достоинства, чтобы
rank2=$(( ${newdeck[2]} % 13 )) # упростить последующие вычисления.
rank3=$(( ${newdeck[3]} % 13 ))
# Исправить значение для короля: по умолчанию оно равно 0,
# сделать равным 13.
if [ $rank1 -eq 0 ] ; then
rank1=13;
fi
if [ $rank2 -eq 0 ] ; then
rank2=13;
fi
if [ $rank3 -eq 0 ] ; then
rank3=13;
fi
# Теперь поменяем сданные карты местами так, чтобы card1 всегда
# была меньше card2.
if [ $rank1 -gt $rank2 ] ; then
temp=$card1; card1=$card2; card2=$temp
temp=$rank1; rank1=$rank2; rank2=$temp
fi
showCard $card1 ; cardname1=$cardname
showCard $card2 ; cardname2=$cardname
showCard $card3 ; cardname3=$cardname # Shhh, it's a secret for now.
echo "I've dealt:" ; echo " $cardname1" ; echo " $cardname2"
}
function introblurb
{
cat << EOF
Welcome to Acey Deucey. The goal of this game is for you to correctly guess
whether the third card is going to be between the two cards I'll pull from
the deck. For example, if I flip up a 5 of hearts and a jack of diamonds,
you'd bet on whether the next card will have a higher rank than a 5 AND a
lower rank than a jack (that is, a 6, 7, 8, 9, or 10 of any suit).
Ready? Let's go!
EOF
}
games=0
won=0
if [ $# -gt 0 ] ; then # Полезная информация, если параметр определен
introblurb
fi
while [ /bin/true ] ; do
initializeDeck
shuffleDeck
dealCards
splitValue=$(( $rank2 - $rank1 ))
if [ $splitValue -eq 0 ] ; then
echo "No point in betting when they're the same rank!"
continue
fi
/bin/echo -n "The spread is $splitValue. Do you think the next card will "
/bin/echo -n "be between them? (y/n/q) "
read answer
if [ "$answer" = "q" ] ; then
echo ""
echo "You played $games games and won $won times."
exit 0
fi
echo "I picked: $cardname3"
# Третья карта попадает между первыми двумя? Проверим.
# Помните, равные значения = проигрыш.
if [ $rank3 -gt $rank1 -a $rank3 -lt $rank2 ] ; then # Выигрыш!
winner=1
else
winner=0
fi
if [ $winner -eq 1 -a "$answer" = "y" ] ; then
echo "You bet that it would be between the two, and it is. WIN!"
won=$(( $won + 1 ))
elif [ $winner -eq 0 -a "$answer" = "n" ] ; then
echo "You bet that it would not be between the two, and it isn't. WIN!"
won=$(( $won + 1 ))
else
echo "Bad betting strategy. You lose."
fi
games=$(( $games + 1 )) # How many times do you play?
done
exit 0
Моделирование колоды карт — непростая задача. Важно решить, как представлять сами карты и как «тасовать» или случайным образом организовывать колоду.
Чтобы решить эту проблему, в сценарии создается два массива по 52 элемента: deck[] и newdeck[] . Первый массив представляет упорядоченную колоду карт, в котором достоинство каждой карты замещается значением -1 при ее «извлечении» из колоды, а второй, newdeck[], представляет колоду, куда в случайное место помещается извлеченная карта.
То есть массив newdeck[] представляет «перетасованную» колоду. Даже при том, что в этой игре используются только первые три карты, для нас гораздо интереснее рассмотреть универсальное решение, чем специализированное.
Это означает, что сценарий в чем-то избыточен. Но ведь правда же интересно!
Давайте пройдемся по функциям и посмотрим, как все устроено. Во-первых, инициализация колоды реализуется действительно просто, в чем нетрудно убедиться, вернувшись назад и взглянув на функцию initializeDeck.
Аналогично функция shuffleDeck выглядит на удивление прямолинейной, потому что всю основную работу в действительности выполняет функция pickCard. Функция shuffleDeck просто обходит 52 элемента в массиве deck[], случайно выбирая элемент, который прежде не был выбран, и сохраняет его в очередном n-м элементе массива newdeck[].
Давайте внимательно исследуем функцию pickCard , потому что именно она выполняет основную работу, связанную с тасованием колоды. Функция разбита на два блока: первый пытается выбрать случайную доступную карту, ограничивая число попыток значением $threshold. Так как функция вызывается снова и снова, первые вызовы всегда будут успешно обрабатываться этим блоком, но потом, когда 50 карт окажутся перемещены в newdeck[], высока вероятность того, что 10 попыток случайно попасть в элементы с оставшимися картами не увенчаются успехом. Это блок цикла while .
После того как значение $errcount увеличится до значения $threshold, стратегия случайного выбора отбрасывается ради сохранения высокой производительности и сценарий переходит ко второму блоку: он выполняет обход массива, пока не найдет первую доступную карту. Это блок .
Задумавшись над последствиями такой стратегии, вы поймете, что чем ниже порог, тем выше вероятность появления в newdeck упорядоченных последовательностей карт, особенно в конце. В экстремальном случае threshold = 1 получится упорядоченная колода, где newdeck[] = deck[]. Является ли число 10 достаточно большим значением? Хотя исследование этого вопроса далеко выходит за рамки нашей книги, мы будем рады получить письмо от любого, кто проведет эксперименты и найдет лучший баланс между качеством тасования и производительностью!
Функция showCard не короче других, но большая ее часть связана лишь с форматированием результата. Основа моделирования всей колоды сосредоточена в двух строках .
В этой игре масть не играет никакой роли, но, как можно заметить, достоинство карты определяется числом от 0 до 12, а масть — числом от 0 до 3. Достоинства карт нужно преобразовать в значения, понятные пользователю. Чтобы упростить отладку, шестерке треф мы присвоили значение 6, а тузу — значение 1. Король по умолчанию имеет значение 0, но в сценарии оно корректируется до 13, чтобы упростить математические вычисления.
Функция dealCards реализует логику игры «Раз-два»: все предыдущие реализуют операции, которые могут пригодиться в любой карточной игре. Функция dealCards извлекает из колоды три карты, но третья карта не вскрывается и остается в тайне, пока игрок не выдвинет свое предположение. Это делается, чтобы упростить вычисления, а вовсе не для того, чтобы обмануть игрока! Здесь вы можете также видеть, что отдельно сохраняемые значения карт ($rank1, $rank2 и $rank3) исправляются для случая king = 13. Кроме того, для упрощения вычислений первые две сданные карты сортируются, чтобы первой следовала карта с меньшим достоинством. Это выполняет инструкция if в .
В строке сценарий выводит сданные карты. Последний шаг — их представление, проверка на совпадение значений (если они совпадают, сценарий не предлагает пользователю угадать) и затем проверка третьей карты — попадает ли ее значение между значениями первых двух. Эта проверка выполняется в блоке .
Наконец, проверка догадки. Если вы предположили, что значение третьей карты попадет между первыми двумя, и оно попало между ними, вы выиграли. Если вы предположили, что значение третьей карты не попадет между первыми двумя, и оно не попало между ними, вы выиграли. Иначе вы проиграли. Этот результат определяется в последнем блоке.
Укажите начальный параметр, и сценарий выведет краткие правила игры. При запуске без параметра сценарий сразу начнет игру. Давайте посмотрим, как выглядит введение (листинг 12.15).
Листинг 12.15. Сеанс игры со сценарием aceydeucey
$ aceydeucey intro
Welcome to Acey Deucey. The goal of this game is for you to correctly guess
whether the third card is going to be between the two cards I’ll pull from
the deck. For example, if I flip up a 5 of hearts and a jack of diamonds,
you’d bet on whether the next card will have a higher rank than a 5 AND a
lower rank than a jack (that is, a 6, 7, 8, 9, or 10 of any suit).
Ready? Let’s go!
I’ve dealt:
3 of Hearts
King of Diamonds
The spread is 10. Do you think the next card will be between them? (y/n/q) y
I picked: 4 of Hearts
You bet that it would be between the two, and it is. WIN!
I've dealt:
8 of Clubs
10 of Hearts
The spread is 2. Do you think the next card will be between them? (y/n/q) n
I picked: 6 of Diamonds
You bet that it would not be between the two, and it isn't. WIN!
I've dealt:
3 of Clubs
10 of Spades
The spread is 7. Do you think the next card will be between them? (y/n/q) y
I picked: 5 of Clubs
You bet that it would be between the two, and it is. WIN!
I've dealt:
5 of Diamonds
Queen of Spades
The spread is 7. Do you think the next card will be between them? (y/n/q) q
You played 3 games and won 3 times.
Снова упомянем вопрос о качестве тасования колоды с пороговым значением 10; это один из аспектов, которые определенно можно было бы улучшить. Также не совсем ясно, имеет ли смысл показывать разность достоинств двух первых карт. В настоящей игре мы бы точно не стали этого делать; игрок должен определить ее сам.
С другой стороны, можно пойти в противоположном направлении и вычислять шансы попадания третьей карты между двумя произвольными. Давайте подумаем об этом: шанс извлечения любой карты равен 1 из 52. Если в колоде осталось 50 карт, потому что две уже были извлечены, шанс взять любую другую карту составляет 1 из 50. Так как масть не имеет значения, у нас есть 4 шанса из 50 извлечь карту каждого другого достоинства. То есть шанс, что достоинство выпавшей карты окажется в нужном промежутке, составляет (количество карт с промежуточными достоинствами × 4) из 50. Если первыми выпали пятерка и десятка, разность составляет 4, поскольку выигравшими будут считаться шестерка, семерка, восьмерка или девятка. То есть шанс победить составляет 4 × 4 из 50. Поняли, что мы имеем в виду?
Наконец, как в любой другой игре командной строки, интерфейс можно было бы доработать. Мы оставим это на ваше усмотрение. Мы также предлагаем вам самим подумать о том, какие другие карточные игры можно было бы реализовать со столь удобной библиотекой функций.
В отечественном кинопрокате вышел под названием «Космическая одиссея 2001 года». — Примеч. пер.