Это четвертая часть My Ruby Chess Journey, серии статей о написании шахматной игры из командной строки на чистом Ruby.
«Огнеокая дева дымной войны/ Всех горячих и кровоточащих поднесем им…»
…очень скоро, но не сейчас. Сначала нам нужна армия! Этим мы сегодня и займемся: сейчас строим тела для пешек, коня, ладей, слонов, ферзей и королей. Завтра мы дадим им души.
Нашей следующей задачей будет написать соответствующие классы для каждого типа фигур, дать им красивое графическое представление на доске и научить доску расставлять фигуры до того, как мы начнем игру. Но сначала нам нужно закончить работу, которую мы начали на днях: у нашей платы есть некоторые проблемы, которые нам нужно исправить в первую очередь. Давайте начнем добавлять подсказку для алгебраической записи.
Итак, в основном, для букв (столбцов в нашей матрице) нам нужно напечатать строку букв, прежде чем мы напечатаем саму доску, вот так:
a b c d e f g h
Давайте добавим константу в наш класс BoardRenderer
:
COLUMN_LETTERS = [*('a'..'h')]
Нам не нужно писать каждую букву благодаря основному классу Range и нашему маленькому другу, оператору знака. Это построит для нас этот массив:
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
Единственное, что нам сейчас нужно, это создать метод, который будет выполнять эту работу, и вызвать его… догадаетесь, где? Вот как я это сделал:
def render print_column_letters # At the top... print_floor SQUARE_ORDER.times do |row_number| SQUARE_HEIGHT.times { print_row(row_number) } print_floor end new_line print_column_letters # ...and the bottom. end private def print_column_letters COLUMN_LETTERS.each { |letter| print " #{letter}" } new_line(2) end # Here is a possible definiton for the new_line helper (very handy!): def new_line(lines=1) lines.times { puts '' } end
И это результат:
Вы видите, что буквы не совпадают с квадратами? Мы можем либо переместить буквы влево (удалив некоторые пробелы в строке, напечатанной каждой итерацией print_column_letters
), либо переместить доску вправо, добавив небольшое поле. Поскольку нам все еще нужно добавить номера строк, это будет наш лучший вариант.
Как мы можем «переместить» доску? В этом случае под перемещением мы подразумеваем добавление дополнительных пробелов в левой части доски; а добавление дополнительного пробела в левую часть доски здесь означает добавление дополнительного пробела слева от каждой строки, составляющей доску. Нам придется немного изменить наши методы row
и print_floor
, чтобы добавить это поле. Начнем с добавления константы LEFT_MARGIN
со значением… ' ' * 3
? Мы всегда можем изменить это значение, если нас не устраивает результат. Давайте посмотрим:
class BoardRenderer # ... LEFT_MARGIN = ' ' * 3 # ... def print_floor puts LEFT_MARGIN + FLOOR_0 + FLOOR * (SQUARE_ORDER - 1) end # ... def row LEFT_MARGIN + # We've added this extra line. EMPTY_ROW_0_WHITE + (EMPTY_ROW + EMPTY_ROW_WHITE) * 3 + EMPTY_ROW end end
Но…
Можете ли вы догадаться, почему это происходит? Да, именно… это работает для всех строк… кроме тех, которые мы перевернули! Левые поля перевернутых рядов (или черных начальных рядов) находятся не на левой, а на правой стороне доски! Это все усложняет, потому что теперь нам каким-то образом нужно различать два типа строк: те, которые начинаются с белого квадрата, и те, которые заканчиваются черным квадратом. Если хотите, найдите несколько минут, чтобы подумать, как мы можем это сделать, прежде чем прокрутить вниз, чтобы увидеть, что я сделал. (Подсказка: подумайте о номерах строк).
Я добавил два разных метода для печати двух типов строк, прежде чем немного изменить метод print_row
(помните, что параметр number
назначается каждому целому числу от 0 до 7, переданному из метода times
, вызываемого для SQUARE_ORDER
внутри render
)
def print_row(number) puts number.even? ? white_starting_row : black_starting_row end def white_starting_row LEFT_MARGIN + EMPTY_ROW_0_WHITE + (EMPTY_ROW + EMPTY_ROW_WHITE) * 3 + EMPTY_ROW end def black_starting_row LEFT_MARGIN + EMPTY_ROW_0 + (EMPTY_ROW_WHITE + EMPTY_ROW) * 3 + EMPTY_ROW_WHITE end
Это то, что мы получаем с множителем 3 в LEFT_MARGIN
constant. Что, если мы поменяем его на 4?
Теперь наша следующая задача — добавить номера рядов в левую и правую части шахматной доски. Мы хотим, чтобы числа были отцентрованы, четко выровнены с соответствующим квадратом. Как мы собираемся это сделать? Вы видите, что каждая квадратная строка состоит из трех подстрок, верно? По одному сверху, посередине и снизу. Это по-прежнему важно по разным причинам, но сейчас это важно, потому что мы собираемся использовать среднюю строку для отображения номеров строк, прежде чем напечатать саму шахматную доску.
Но поскольку мы сейчас различаем три типа строк, нам нужно немного изменить метод render
, так как теперь мы не сможем просто times
на SQUARE_HEIGHT
печатать три строки:
def render print_column_letters print_floor SQUARE_ORDER.times do |row_number| print_row(row_number) # Top print_row(row_number) # Middle print_row(row_number) # Bottom print_floor end new_line print_column_letters end
Как и в случае с буквами столбца, мы должны добавить константу ROW_NUMBERS
:
ROW_NUMBERS = [*('1'..'8')].reverse
Помните, что номера строк отображаются от 8 до 1, а доска отображается сверху вниз, поэтому нам нужно перевернуть массив (диапазоны Ruby не допускают обратного ранжирования само по себе) . Как только мы это сделали, остальное должно быть легко. И нам не терпится увидеть кусочки! Как и когда мы можем печатать номера строк? Как всегда, вы можете попробовать реализовать то, что считаете лучшим, именно так я сначала и поступил. Я начал добавлять эту строку в метод render
:
def render print_column_letters print_floor SQUARE_ORDER.times do |row_number| print_row(row_number) print ROW_NUMBERS[row_number] # The row_number parameter works as index print_row(row_number) print_row(row_number) print_floor end new_line print_column_letters end
Но, опять же, это не приносит нам результатов, на которые мы надеялись:
Лучше привыкнуть: в программировании одно исправление тут — там головная боль. Часто бывает так, что когда мы модифицируем или добавляем какую-то реализацию, хотя это и мало, это вызовет волновой эффект в нашей программе, который заставит нас изменить многие другие части нашего кода, которые, как мы даже не могли подумать, будут затронуты. . Это нормально и ожидаемо, но до определенного момента это можно смягчить, и есть несколько способов лучше сохранять спокойствие, когда это происходит. Это очень важно, поверьте. Я выучил это трудным путем:
Не начинайте программировать сразу, как только у вас появится идея. Сначала напишите идею, если можете, от руки, поскольку мозг обрабатывает рукописный ввод лучше, чем печатание. Всегда контролируйте код. Это означает, что не должно быть никаких областей (методов, классов…) вне нашего полного понимания, что на практике означает способность читать любую строку нашего кода и, по сути, быть в состоянии ответить на четыре основных вопроса о КАЖДОМ методе: Что такое вход? Что такое выход? , Есть ли какие-либо побочные эффекты?, Является ли какой-либо объект постоянной мутацией? Чем лучше мы назовем артефакты в нашей программе и запомним карту, тем лучше и быстрее мы сможем ответить на эти вопросы. Чем быстрее мы сможем ответить на эти вопросы, тем лучше мы станем отлаживать. Также важно знать, как стратегически разместить наши binding.irb
или binding.pry
точек. Никогда не прекращайте экспериментировать и исследовать, что является привязкой в любой точке программы, вы будете удивлены, узнав, как быстро вы улучшите свою способность читать код, если сделаете это правильно. Однако гораздо важнее научиться правильно называть вещи.
Иногда, обычно когда мы программируем какое-то время, возникает ощущение, что программа становится похожей на огромного Бегемота, нечеткую пропасть, невыносимую рутину и т. д. Наша программа работает, и мы успешно добавляем функциональность, но мы есть какое-то ощущение, что код выливается из рук, что мы теряем хватку и, чем больше кодим, тем меньше понимаем. Это усталость от кодирования, и это также нормально и ожидаемо. Но в тот момент, когда вы это почувствуете (а вы поймете, когда почувствуете), остановитесь. Разомните ноги, прогуляйтесь, поговорите с кем-нибудь, приготовьте кофе, что угодно. Хотя бы на 5-10 минут. Затем, когда вы вернетесь к своему компьютеру, приготовьте лист бумаги вместо того, чтобы сразу начинать программировать, и начните рисовать схему программы. Если у вас есть сомнения, пока не кодируйте. Вместо этого найдите время, чтобы прочитать код, установить некоторые привязки, если хотите, и попытаться снова взять ситуацию под контроль. Когда вы сможете нарисовать небольшую диаграмму (это может быть несколько рисунков, работает все, что угодно, если вы знаете, что делаете), вы почувствуете новую мотивацию, и программа покажется вам менее сложной. Когда вы снова почувствуете мотивацию, тогда вам нужно снова начать программировать. Если нет, промойте и повторяйте, пока не сделаете. Другого пути нет. Если вы заставите себя выполнять, не полностью понимая код, вы не только напишете плохой код, но и обожжетесь. И довольно быстро. Будьте немного дзен здесь: не кодирование важнее, чем кодирование. Иногда время, проведенное без программирования, более ценно, чем время, потраченное на кодирование.
Средний ряд: вам суждено стать великим.
Итак, теперь идея состоит в том, чтобы каким-то образом создать специальный метод для специальной строки, средней строки, которая может иметь меньшее левое поле, чем другие строки; таким образом, его можно распечатать совместив с другими, а не с этим уродливым смещением от последнего изображения.
Давайте начнем писать возможный метод print_middle_row
…
def print_middle_row(row) print number.even? ? white_starting_row[1..-1] : black_starting_row[1..-1] # It has to be print and not puts, because we still don't want a newline # character at the end of the row, but the row number we will print later. end
…и заменив старое print_row
на render
:
def render print_column_letters print_floor SQUARE_ORDER.times do |row_number| print_row(row_number) print ROW_NUMBERS[row_number] print_middle_row(row_number) print_row(row_number) print_floor end new_line print_column_letters end
И…
…оно работает! Нам просто нужно добавить числа в правую сторону доски! Это очень просто: будет достаточно простого вызова puts. Не забывайте о RIGHT_MARGIN = ' ' * 3
constant. Кстати, я тестировал это с 3 и 4 пробелами, и я думаю, что 3 выглядит лучше. Вы можете проверить свои собственные идеи и цифры.
def render new_line # I think an extra line first looks good. print_column_letters print_floor SQUARE_ORDER.times do |row_number| print_row(row_number) print ROW_NUMBERS[row_number] print_middle_row(row_number) puts RIGHT_MARGIN + ROW_NUMBERS[row_number] # The last touch! print_row(row_number) print_floor end new_line print_column_letters end
Наш первый взгляд на Писленд
Я понимаю. У нас уже было слишком много доски, и вам не терпится увидеть фигуры, верно? Верно. Итак, прежде чем мы научим рендерер платы считывать данные из матрицы и отображать фигуры, давайте напишем базовую схему для классов фигур.
Мы можем начать с того, что, как и с доской, создадим для них отдельную папку. Помните, что (почти) всегда лучше иметь один файл для каждого класса или модуля: если класс/два очень малы или у нас есть небольшие модули, которые работают вместе для получения одной функции или функциональности, в качестве исключения мы можем группировать по два-три класса в файл, но не более. Однако я не рекомендую этого, и я всегда стремлюсь иметь один файл для каждого класса/модуля.
Первый шаг — создать папку pieces
внутри lib
. Затем внутри папки pieces
мы создадим семь .rb
файлов, по одному для каждого типа шахматной фигуры, плюс один дополнительный pieces.rb
для родительского класса _29, содержащий общие поведения, которые каждый тип фигуры унаследует (или перезапишет). Это должна быть результирующая файловая структура в нашей основной папке:
И затем, так же, как мы сделали с ./lib/board.rb
, мы добавим все необходимые относительные файлы в ./lib/pieces.rb
:
require_relative 'pieces/piece.rb' require_relative 'pieces/pawn.rb' require_relative 'pieces/knight.rb' require_relative 'pieces/bishop.rb' require_relative 'pieces/rook.rb' require_relative 'pieces/queen.rb' require_relative 'pieces/king.rb'
Таким образом, вместо того, чтобы требовать все эти файлы в других файлах, нам просто нужно будет потребовать ./lib/pieces.rb
, чтобы добавить функциональность всех классов частей в другие классы или модули.
На данный момент мы реализуем основы в каждом классе частей, чтобы он мог взаимодействовать с классом Board
, и из этого мы будем добавлять больше функций и поведений, таким образом увеличивая сложность. Давайте начнем писать базовый родительский класс для всех частей, Piece
, в ./lib/pieces/piece.rb
.
class Piece attr_reader :color attr_accessor :location def initialize(location, color) @location = location @color = color end end
И это довольно много! Мы определяем два наиболее важных атрибута для части с соответствующими геттерами и сеттерами:
- a
location
, содержащее текущее положение фигуры на доске в формате[row, column]
, определенном нами ранее. Это способ реализовать внутреннее сознание фигур о том, где они находятся на доске в конкретный момент игры: Я здесь. Это важно, потому что мы будем использовать эти данные, чтобы узнать, на какие клетки можно будет двигаться за ход. Внутренние данные этой фигуры должны совпадать с квадратом на доске, где находится фигура в любое время: если есть несоответствие, это ошибка, и мы можем использовать это как индикатор ошибок, например, при написании собственных тестов. . - a
color
: самый основной атрибут фигуры. Он будет содержать один из двух символов::white
или:black.
.
Что теперь? Прежде чем мы начнем писать классы, я должен показать вам кое-что, что, возможно, вы хотели спросить. Да, для шахматных фигур существуют настоящие символы Юникода! И для обоих цветов тоже! Они не супер-причудливые или что-то в этом роде, и вы едва можете различить черные и белые части, но они сделают свою работу.
╔═══════════╦═══════════════════╗ ║ Symbol ║ Name ║ ╚═══════════╩═══════════════════╝ ♔ White King ♕ White Queen ♗ White Bishop ♖ White Rook ♘ White Knight ♙ White Pawn ♚ Black King ♛ Black Queen ♝ Black Bishop ♖ Black Rook ♞ Black Knight ♟︎ Black Pawn
Наша следующая задача — добавить к каждому классу соответствующий символ в две константы, одну WHITE
и одну BLACK
. Вы можете сделать это с помощью одной константы SYMBOL
, назначенной, например, хэшу, используя цвета в качестве ключей и символы в качестве значений, но меня это не беспокоит, потому что нам все равно придется использовать условное выражение, как вы увидите. очень скоро, в очень важном методе.
Я приведу вам пару примеров, а потом вы сделаете все остальное, хорошо?
Во-первых, пешка (пока даже не думайте о продвинутых пешках):
class Pawn < Piece # Don't forget to make every piece type a child of Piece! WHITE = ♙ BLACK = ♟︎ end
А рыцарь…
class Knight < Piece WHITE = ♘ BLACK = ♞ end
И так далее, и так далее.
Свет в конце туннеля
Вы помните, что у нас все еще есть двумерный массив, который мы назвали matrix
, в духовке, заполненный уродливыми значениями nil
, не так ли? Если бы, например, мы хотели узнать, что находится на квадрате [0, 0]
(a8 в алгебраической записи), мы могли бы сказать… matrix[0, 0].class
и увидеть на экране Rook
. Но… как представить, что на этом поле стоит ладья? Мы сделали это с первой буквой класса, но мы просто включили каждый символ типа фигуры в каждый класс. Как сделать так, чтобы символ, простой символ UNICODE, отображался на доске? Следите за мной в этом, потому что, когда я наконец узнал, как это сделать, я подумал, что это действительно захватывающе.
В Ruby есть нечто, называемое «интерполяцией строк». Маленькая приятная синтаксическая функция, которая позволяет печатать любое значение переменной где-нибудь в строке. Например,
a_variable = 2022 puts "Hi, I am a variable and my value is #{a_variable}" # => "Hi, I am a variable and my value is 2022"
Мы даже можем включить такое выражение, как 1 + 1
, или даже более сложные, и мы все равно увидим возвращаемое значение выражения в том месте, где мы добавили синтаксис #{}
в любом месте строки. Что происходит под капотом, так это то, что Ruby сначала оценивает выражение, а затем вызывает метод #to_s
для результирующего объекта этой оценки. И вы помните, что, по сути, наш BoardRenderer
печатает строки… так что, если я скажу вам, что мы можем написать метод экземпляра Piece#to_s
, который будет отображать символ каждой части, и что мы добавили специальную строку с визуализатором со всеми квадраты внутри интерполяции строк? Бинго! Руби вызывал метод Piece#to_s
для каждого из них, что приводило к печати символов на доске! Сейчас это может показаться немного запутанным, но вам понравится, насколько это просто и эффективно.
Давайте продолжим! Во-первых, конечно, мы должны определить метод экземпляра to_s
в классе Piece
, от которого будут наследоваться все классы типов:
def to_s case color when :white then self.class::WHITE else self.class::BLACK end end
Мы применяем это на практике с помощью некоторых экспериментов. Создайте файл main.rb
file в корневой папке проекта (не lib
) с этим сверху:
require_relative 'lib/board.rb' require_relative 'lib/pieces.rb'
Если мы попытаемся напечатать любое содержимое в точном квадрате, скажем, [0, 1]
(b8), мы ничего не увидим, потому что nil
будет напечатано как пустое место:
board = Board.new puts "This is the content of the [0, 1] square (b8):" puts "#{board[[0, 1]]}"
Так мы раньше учили доску говорить: «В этой клетке нет фигуры». На данный момент у нас есть просто пустая доска без каких-либо фигур, представленная двумерным массивом, полным nil
значений. А вдруг…
board = Board.new board[[0, 1]] = Knight.new([0, 1], :black) puts "This is the content of the [0, 1] square (b8):" puts "#{board[[0, 1]]}"
Вы начинаете видеть свет здесь? Если да, то это здорово, потому что в тот момент, когда я понял, что можно сделать с помощью этой техники, это одно из моих любимых воспоминаний о разработке этой игры.
И это все на сегодня! Надеюсь, вам понравилась эта статья. Но следующий, обещаю, будет еще интереснее.