Книга: Сценарии командной оболочки. Linux, OS X и Unix. 2-е издание
Назад: Глава 11. Сценарии для OS X
Дальше: Глава 13. Работа в облаке

Глава 12. Сценарии для игр и развлечений

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

Не волнуйтесь — мы не предлагаем вам написать 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 переворачивает задом наперед строку, полученную ею из стандартного ввода. Если прямая и перевернутая версии совпадают, значит, проверяемая строка является палиндромом; если они различаются, это не палиндром.

Показанные далее игры немногим сложнее, но все они достаточно забавны, чтобы вы добавили их в свою систему.

№ 83. Декодирование: игра в слова

Это простейшая игра в анаграммы. Если вы когда-нибудь сталкивались с подобными играми, то легко поймете суть: выбирается случайное слово, и буквы в нем переставляются случайным образом. Задача игрока — угадать исходное слово за как можно меньшее число попыток. Полный сценарий, реализующий эту игру, приводится в листинге 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 букв — хитро!

№ 84. Виселица: угадай слово, пока не поздно

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

Однако сама по себе игра довольно забавная, а ее реализация в виде сценария командной оболочки оказывается на удивление простой, как демонстрирует лис­тинг 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.

№ 85. Угадай столицу

Теперь, когда у нас есть инструмент выбора случайной строки из файла, перед нами открываются широкие перспективы в создании самых разных игр-викторин. Мы поместили в список столицы всех 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, достаточно универсальные сценарии порой можно использовать совершенно неожиданными и непредусмотренными ранее способами.

№ 86. Является ли число простым?

Простыми называют числа, которые делятся без остатка только на самих себя, например 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 для вычисления значения, которое не изменяется ни на йоту до самой последней итерации.

Существуют также другие, не такие примитивные способы проверки простых чисел, включая замечательный алгоритм с интересным названием «Решето Эратосфена», более современный алгоритм «Решето Сундарама» или более сложный — «Решето Аткина». Поищите их описания в Интернете и проверьте с их помощью номер своего телефона (без дефисов!).

№ 87. Игральные кости

Это удобный сценарий для всех, кто любит играть в настольные игры, особенно ролевые, такие как 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.

№ 88. «Раз-два»

Завершим главу сценарием, в котором реализуется карточная игра «раз-два» (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 года». — Примеч. пер.

Назад: Глава 11. Сценарии для OS X
Дальше: Глава 13. Работа в облаке