Наш чатик

Телеграм чат начинающих программистов. Общаемся и помогаем друг другу

Если ссылка не открывается, можно найти нас в поиске по чатам @rubyrush или пойти другими путем

Объектная «Виселица» v.2

В этом уроке мы будем использовать наши знания о классах в деле и перепишем с их помощью игру «Виселица».

Мы напишем вторую версию игры, разбив программу на два класса Game и ResultPrinter. Узнаем как работает оператор case в Ruby, как создавать поля класса (переменные экземпляра) и немного о спецсимволах и псевдографике.

План урока

  1. Переделываем структуру программы «Виселица»
  2. Добавляем к выводу результата изображение виселиц

Новая виселица!

Деление программы на классы

Удобство деления программы на классы легко продемонстрировать на таком примере: у топора есть древко, а есть металлическое лезвие. Если лезвие топора снять и закрепить на древке ударную часть молотка, получится молоток (замечание для плотников, которые будут читать этот текст: конечно, получится лишь жалкое подобие молотка, это просто для примера).

Молотки, топоры

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

Вы, наверное, неоднократно видели отвёртки с набором насадок для различных шурупов и винтов.

Универсальная отвёртка

В такой ситуации можно сказать, что держатель отвёртки является экземпляром класса «основание», а каждая насадка — экземпляром класса «насадка». Удобство в том, что класс «основание» ничего не знает о том, какие болты будут с его помощью закручивать, зато он может уметь, например, фиксировать вращение только в одну сторону, для удобства работы.

Если вдруг изобретут насадку в форме звёздочки, вам не придётся выкидывать «основание» такой отвёртки. Вам просто надо будет найти соответствующую насадку.

В то время класс «насадка» совершенно ничего не знает про то, с каким основанием он будет работать: оно может быть длинным, коротким, угловым, это вообще может быть дрель-шуруповёрт.

Задумайтесь. Каждая деталь фактически становится самостоятельным инструментом, который хоть и бесполезен без своей «второй половинки», но является универсальным для всех таких половинок. Это удобно.

Делим «Виселицу» на классы

Грубо говоря, все методы нашей виселицы v.1 по факту занимаются двумя вещами:

Меняют состояние игры

  • get_letters
  • get_user_input
  • check_input

Выводят что-то на экран (или чистят его)

  • get_word_for_print
  • print_status
  • cls

Это-то наталкивает нас на мысль, что игра по факту состоит из двух глобальных частей: «внутренность игры» и «интерфейс вывода». Отделим вывод информации для игрока от внутренней игровой логики. В действительности, методов окажется немного больше, но не пугайтесь, мы всё объясним.

Класс Game

В первой части (игра) у нас будет всё, что связано с состоянием игры: загаданное слово, отгаданные буквы, буквы, которых в слове не оказалось и количество ошибок будут полями (переменными экземпляров) класса Game.

Для этого класса создадим отдельный файл: game.rb (как обычно, в новой папке урока c:\rubytut\lesson12).

А методы get_user_input и check_input мы соберём в один метод ask_next_letter.

Итак, поехали, повторяем логику программы.

class Game
  # тут будет описание класса Game
end

Метод initialize

Конструктор класса Game у нас будет вызывать метод get_letters (так как игра начинается с загадывания слова и без этого слова мы не можем продолжать игру). Так как метод get_letters принадлежит тому же классу Game, в конструкторе нет необходимости писать Game.get_letters, достаточно написать просто get_letters. Это верно для всех методов класса Game, вызываемых из других методов класса Game.

def initialize(slovo)
  @letters = get_letters(slovo)
  @errors = 0
  @good_letters = []
  @bad_letters = []
  @status = 0
end

Заметьте, мы ждём в конструкторе параметр slovo (да, конструктор - это обычный метод, и в него тоже можно передать параметр).

При вызове

Game.new("жираф")

нам нужно будет в скобках указать загаданное слово, чтобы игра началась, а слово сохранилось в переменной letters нового экземпляра класса Game. Обратите также внимание на новое поле @status, оно нам понадобится в дальнейшем.

Метод get_letters

def get_letters(slovo)
  if (slovo == nil || slovo == "")
    abort "Для игры введите загаданное слово в качестве аргумента при запуске программы"
  end

  return slovo.split("")
end

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

Если же там что-то есть, он разбивает слово на буквы уже знакомым нам способом и возвращает получившийся массив конструктору, чтобы тот мог его записать в поле класса letters.

Метод ask_next_letter

def ask_next_letter
  puts "Введите следующую букву"
  letter = ""
  while letter == "" do
    letter = STDIN.gets.encode("UTF-8").chomp
  end

  next_step(letter)
end

Мы немного переименовали метод get_user_input, чтобы было понятнее, что именно он делает. Он не просто берёт то, что ввёл пользователь, а именно спрашивает следующую букву. Всегда старайтесь называть ваши классы и методы максимально точно.

В нём мы спрашиваем у пользователя букву и добиваемся, чтобы он её-таки ввёл (в цикле проверяя, не ввёл ли он пустоту), а потом вызываем метод next_step, который эту букву обработает, как надо. Обратите внимание, опять внутренний метод класса мы вызываем без упоминания самого класса (не пишем Game. перед названием метода).

Метод next_step

Метод next_step по сути, самый важный, он передвигает состояние игры на следующий шаг, проверяя букву в слове.

def next_step(bukva)
  if @status == -1 || @status == 1
    return
  end

  if @good_letters.include?(bukva) || @bad_letters.include?(bukva)
    return
  end

  if @letters.include? bukva
    @good_letters << bukva

    if @good_letters.uniq.sort == @letters.uniq.sort
      @status = 1
    end
  else
    @bad_letters << bukva
    @errors += 1

    if @errors >= 7
      @status = -1
    end
  end
end

Этот метод очень похож на наш старый метод check_input с той лишь разницей, что все данные он берёт не из параметров, а из полей класса.

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

Обратите внимание, также, что этот метод ничего не возвращает, он просто меняет состояние поля @status (вот оно нам и пригодилось), это ещё одно удобство: методам класса Game не надо ничего возвращать, они просто меняют поля класса Game.

Методы-геттеры

Ещё одна особенность классов заключается в том, что они «прячут» переменные своих экземпляров от чужих глаз. Если у нас в программе будет экземпляр класса Game

game = Game.new('слово')

То мы не сможем достать переменные экземпляра game (@status, например) просто так (нельзя просто взять и написать game.@status — будет ошибка). Зачем так придумали мы сейчас рассказывать не будем, важно лишь то, что нам нужно научиться доставать значения переменных для экземпляров класса Game.

Для этого нам надо также написать 5 так называемых геттеров (не путать с гетрами) — методов класса Game, которые просто возвращают значения соответствующих полей класса, чтобы ими можно было пользоваться из основной программы:

def status
  return @status
end

def errors
  return @errors
end

def letters
  return @letters
end

def good_letters
  return @good_letters
end

def bad_letters
  return @bad_letters
end

Для каждого поля класса мы написали метод, который, будучи вызванным у экземпляра этого класса, возвращает значение одноимённой переменной класса.

Класс ResultPrinter

Теперь переходим ко второй части нашей программы: выводу информации на экран. Этим будет заниматься класс ResultPrinter, который как и полагается новому классу, мы будем описывать в отдельном файле result_printer.rb.

Обратите внимание, что для названий классов мы используем CamelCase — написание словосочетаний без пробелов с увеличенной большой буквой каждого слова, а названия файлов этих классов мы пишем маленькими буквами, заменяя пробелы нижним подчёркиванием — это соглашение, принятое в сообществе Ruby, настоятельно советуем вам поступать также.

class ResultPrinter
end

Метод print_status

Первый и самый важный метод класса ResultPrinter будет заниматься выводом состояния игры на экран.

def print_status(game)
  cls
  puts "Слово: #{get_word_for_print(game.letters, game.good_letters)}"
  puts "Ошибки: #{game.bad_letters.join(", ").to_s}"

  if game.status == -1
    puts "Вы проиграли :("
    puts "Загаданное слово было: " + game.letters.join("")
  elsif game.status == 1
    puts "Поздравляем, вы выиграли!"
  else
    puts "У вас осталось ошибок: " + (7 - game.errors).to_s
  end
end

Этот метод вызывается каждый раз, когда нам нужно обновить картинку для игрока — показать ему новую виселицу.

Во-первых, этот метод чистит экран с помощью метода cls, который тоже, конечно же, логично сделать частью класса ResultPrinter (если вы забыли, как он работает — вспомните).

def cls
  system "clear" || system "cls"
end

Во-вторых, он не может просто взять данные из полей объекта класса Game. Ему нужно передать этот объект, а уже состояние игры наш ResultPrinter сам возьмет из объекта Game с помощью геттеров.

Ну и в-третьих, здесь описан вспомогательный метод get_word_for_print, чтобы напечатать слово с закрытыми неразгаданными буквами (как в «Поле чудес»):

def get_word_for_print(letters, good_letters)
  result = ""

  for item in letters do
    if good_letters.include?(item)
      result += item + " "
    else
      result += "__ "
    end
  end

  return result
end

Основная программа viselitsa.rb

Самое время написать нашу программу с использованием новых классов Game и ResultPrinter: пора взять основание отвёртки, насадить на него наконечник и закрутить парочку шурупов, Дамы и Господа!

Сперва нам надо наши классы подключить:

require_relative "game"
require_relative "result_printer"

Теперь создадим по экземпляру каждого класса. ResultPrinter создаём просто вызвав у него new (у него даже конструктора нет, ничего страшного, так можно), а вот для игры нам нужно получить слово.

Обратите внимание, классу game абсолютно плевать, откуда мы возьмём это слово, главное чтобы мы передали его конструктору. А берём мы слово как обычно из строки запуска и передаём его в конструктор класса Game.

slovo = ARGV[0]
  if (Gem.win_platform? && ARGV[0])
    slovo = slovo.dup
      .force_encoding("IBM866")
      .encode("IBM866", "cp1251")
      .encode("UTF-8")
  end

game = Game.new(slovo)
printer = ResultPrinter.new

Время запустить основной игровой цикл. Условием выхода из цикла будет смена статуса игры (game.status, не забываем, что у нас есть такой метод-геттер), который изначально равен 0 (прописали в конструкторе).

Теперь мы знаем, что как только @status в нашем объекте game (не путать с классом Game) станет отличным от 0, мы выйдем из цикла и закончим работу программы. В цикле мы выводим текущее состояние игры на экран и спрашиваем новую букву у игрока:

while game.status == 0 do
  printer.print_status(game)
  game.ask_next_letter
end

Обратите внимание, насколько ёмким теперь выглядит тело цикла. Вся сложность ушла туда, где ей и суждено быть — во внутреннюю логику методов классов Game и ResultPrinter.

После цикла нам только ещё раз нужно написать результат и вуаля! Программа готова! Дальше всё произойдёт само, программа стала простой и ясной. Не бойтесь ошибок и опечаток, как с ними бороться мы рассказывали в 3-м уроке. Если что-то не получается, скачайте наши исходники этих файлов и внимательно посмотрите их.

Теперь программу можно запустить:

cd c:\rubytut\lesson12
ruby viselitsa "космонавт"

Визуализация результата

Теперь продемонстрируем еще одну крутую вещь, которую нам помогут сделать классы.

Мы поменяем метод print_status класса ResultPrinter, добавив туда псевдографику - картинку, составленную из текстовых символов, тире, подчёркиваний, скобочек и прочего.

Все остальные части нашей программы, а именно, код основной её части viselitsa.rb и код класса Game останется прежним.

Метод print_viselitsa

А в класса ResultPrinter добавим новый достаточно громоздкий метод для отрисовки картинки виселицы в зависимости от количества ошибок (вы не пугайтесь, он только выглядит страшно):

def print_viselitsa(errors)
  case errors
  when 0
    puts "
          _______
          |/
          |
          |
          |
          |
          |
          |
          |
        __|________
        |         |
        "
    when 1
      puts "
          _______
          |/
          |     ( )
          |
          |
          |
          |
          |
          |
        __|________
        |         |
        "
    when 2
      puts "
          _______
          |/
          |     ( )
          |      |
          |
          |
          |
          |
          |
        __|________
        |         |
        "
    when 3
      puts "
          _______
          |/
          |     ( )
          |      |_
          |        \\
          |
          |
          |
          |
        __|________
        |         |
        "
    when 4
      puts "
          _______
          |/
          |     ( )
          |     _|_
          |    /   \\
          |
          |
          |
          |
        __|________
        |         |
        "
    when 5
      puts "
          _______
          |/
          |     ( )
          |     _|_
          |    / | \\
          |      |
          |
          |
          |
        __|________
        |         |
        "

    when 6
      puts "
          _______
          |/
          |     ( )
          |     _|_
          |    / | \\
          |      |
          |     / \\
          |    /   \\
          |
        __|________
        |         |
        "
    when 7
      puts "
          _______
          |/     |
          |     (_)
          |     _|_
          |    / | \\
          |      |
          |     / \\
          |    /   \\
          |
        __|________
        |         |
        "

  end
end

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

Обратите внимание, что вместо одного обратного слеша \ мы пишем два \\, это так называемые спец-символы. Например, символ переноса строки тоже начинается со слеша: \n, так Ruby (и не только Ruby, это во многих языках верно) понимает, что эта n (что после слеша) - не просто буква n, а именно перенос строки.

Если же мы хотим напечатать просто обратный слеш \, как он есть, то Ruby может подумать, что мы хотим начать таким образом какой-то спец-символ, поэтому умные программисты добавили спец. символ, который просто выводит обратный слеш: \\. Просто запомните это. Пригодится.

Оператор case

В нашем методе print_viselitsa мы использовали конструкцию case-when, которая очень удобна, когда у нас есть переменная и много (больше 2-х) вариантов развития событий в зависимости от того, что в этой переменной находится. Если if-else это развилка, где всего два пути, то case — развилка где дорог может быть сколько угодно:

case fruit
when 'banana'
  puts "Это банан"
when 'apple'
  puts "Это яблоко"
when 'orange'
  puts "Это апельсин"
else
  puts "Это какой-то непонятный фрукт"
end

Оператор case сравнивает значение выражения (в данном случае fruit) со всеми вариантами. Если в переменной fruit записана строка "banana", то на экран выведется строчка

"Это банан"

если в переменной fruit записана строка "apple", то на экране окажется

"Это яблоко"

если же в переменной fruit"вишня" или вообще цифра 5, то программа не выберет ни один из вариантов и пойдёт по варианту, который начинается со спец-слова default (по умолчанию, как аналог else):

"Это какой-то непонятный фрукт"

Запустите вашу новую виселицу и поиграйтесь с ней, глядя на новую псевдографику.

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

И помните, что с первого раза (и даже с десятого) может не получиться даже у гениальных программистов. Ваша настойчивость в борьбе с трудностями и ошибками — это важнейшая часть вашего обучения. Чем труднее сейчас, тем проще будет потом. Поверьте, мы это проходили.

А в этом уроке мы попробовали классы в деле, разбив нашу программу Виселица на два класса Game и ResultPrinter, узнали, как работает оператор case, как пользоваться полями класса и немного узнали о спецсимволах и псевдографике.

В следующем уроке мы научимся открывать файлы в Ruby, читать их и выводить их содержимое на экран в различных формах.