Как эта простая бумажная игра заставила меня почти отказаться от упражнения

На уроке объектно-ориентированного программирования из школы Launch, курс 120, вы снова напишете игру в крестики-нолики, но теперь уже в объектно-ориентированном подходе. Должен сказать, для меня это была поездка.

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

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

Вы можете проверить мой код здесь, прочитать его и поиграть в игру, если хотите.

В игре используются 4 пользовательских класса и 2 подкласса. Диаграмма Classes of TTTGame показывает структуру классов в игре.

Мне пришлось принять несколько важных решений, на основе которых я создал структуры. Начнем с класса Board.

Обычно доска для обычной игры 3x3 имеет фиксированные строки и столбцы, но в разделе дополнительных услуг вы можете создать доску разных размеров. Эта игра способна отображать любой размер, который вы можете себе представить, например 4x9, 2x254, 53x87 и т. Д. Поскольку игра отображается в Терминале, мне пришлось ограничить размеры, в которых столбцы и строки совпадают, и только до 9 по 9 сетке.

Самая большая проблема, с которой я столкнулся, была с сеточной структурой данных Совета. Я думал так. Реальная доска на бумаге не заботится о том, где размещены квадраты, в какой форме, размере или как они помечены, поэтому изначально на плате почти не было переменных и методов экземпляра, а структура данных сетки была массивом. Я хотел, чтобы вся логика для принятия решений находилась в классе Square и классе Player. Таким образом, любой игрок может взглянуть на Квадраты, увидеть положение Квадрата и сделать что угодно на его основе. Также каждый Квадрат должен знать, какие соседние Квадраты находятся вокруг него, чтобы он мог реагировать и давать вам обратную связь, что есть возможность выиграть или необходимость заблокировать соседний Квадрат.

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

Затем я решил, что сетка должна знать, где находится квадрат. Я создал структуру сетки, которая представляет собой хэш, где ключ - это позиция Square, а значение - это сам Square. Для сетки 3 на 3 это выглядело так:

{
  [0, 0]: #<Square:0x007fbe0f98e5c0>
  [1, 0]: #<Square:0x007fbe0f98d738>
  [2, 0]: #<Square:0x007fbe0f98cf40>
  [0, 1]: #<Square:0x007fbe0f98cbd0>
  [1, 1]: #<Square:0x007fbe0f98c950>
  [2, 1]: #<Square:0x007fbe0f98c4f0>
  [0, 2]: #<Square:0x007fbe0f98c3b0>
  [1, 2]: #<Square:0x007fbe0f98c360>
  [2, 2]: #<Square:0x007fbe0f98c310>
}

Теперь я был намного более свободен, так как я только просил сетку у Правления и работал до конца.

Мне удалось создать метод в классе TTTGame, который может отображать сетку любого размера, и я мог свободно изменять форму сетки, и он мог применяться для любого размера. Итак, моя сетка может выглядеть так:

/   \ /   \ /   \
|   | |   | |   |
\   / \   / \   /
  -  +  -  +  -
/   \ /   \ /   \
|   | |   | |   |
\   / \   / \   /
  -  +  -  +  -
/   \ /   \ /   \
|   | |   | |   |
\   / \   / \   /

Это ужасно, кто захочет на этом сыграть? Но если есть кто, то могли.

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

Если бы у меня была другая структура данных, я бы наверняка мог спросить доску, есть ли доступный квадрат, и определить полную доску, но мне было бы трудно оценить победителя. Я мог бы попросить каждого игрока проверить, выиграли ли они, но мне нужно будет воссоздать структуру доски. Или была вероятность, что каждый игрок запомнит свои ходы и отмеченные им квадраты, и они будут знать, что они выиграли. Точно так же, как в реальном мире, где бумажная сетка не говорит вам: «Эй, ты, ты выиграл!». Это было моим первоначальным намерением, но код выглядел очень плохо, так как мне нужно было отслеживать две разные структуры данных, содержащие одни и те же объекты. И самые большие проблемы начались, когда я ввел сетку разных размеров.

В игре 3 на 3 вы точно знаете, какие ходы выиграют, поэтому вы можете сказать, что если в этих комбинациях квадратов есть один и тот же маркер, то есть победитель. Когда у вас есть доска 9 на 9 и вы говорите, что любые 3 последующих квадрата в любом направлении могут выиграть, вы получите 224 возможных комбинации в любой строке, столбце или диагонали. Было бы слишком сложно написать все вручную, воображая, что есть бесконечные размеры досок и последующих квадратов, которые могут выиграть.

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

Здесь я очень много думал, как мне это сделать. Выбрать строки и столбцы было несложно. Массив позиций моей структуры сетки[0, 0] сообщал мне координаты x и y, поэтому я мог легко выбрать все строки и столбцы. Но как выбрать все диагонали? Представим себе:

В этом примере у меня есть сетка 5 на 5, и мне нужно 3 следующих квадрата. Итак, у меня есть 10 диагоналей, чтобы проверить, содержит ли какая-либо последующие квадраты.

Я математически думал, как получить из сетки нужные квадраты. А потом я понял, что работаю с Ruby, поэтому, вероятно, есть способ определения диагонали с помощью Ruby. Но не только главная диагональ от [0, 0] до [4, 4], но и диагонали, которые могут содержать возможные квадраты следствия, итак с [2, 0] до [4, 2].

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

И я мог бы сделать то же самое, чтобы сместить строки влево и взять столбцы с разного диагонального направления. Но я уже знал, как получить столбцы при смещении строк вправо. И оказывается, что когда вы берете столбцы и помещаете их на место строк и перевертываете их, вы получаете своего рода зеркальную структуру в диагональном смысле. Затем вы можете использовать те же методы для смещения строк вправо и получить правильные столбцы, представляющие диагонали справа налево.

Итак, когда у меня были все строки, столбцы и диагонали, я мог разделить их, чтобы получить все подпоследовательности Squares. Таким образом, ряд из 5 квадратов даст мне три возможных последующих квадрата: (от 1 до 3), (от 2 до 4) и (от 3 до 5). Затем у меня были все возможные комбинации 3 подсетей Squares в любом направлении.

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

Также я мог бы использовать эту структуру для определения победителя, выбрав массив с квадратами одного и того же маркера из всех возможных подпоследовательностей. Но я этого не сделал, так как придумал другой алгоритм для оценки победителя и оставил его в коде. Если бы я избавился от этого, код был бы короче примерно на 100 строк.

Эту игру было действительно сложно пройти. По-прежнему есть проблемы, например, компьютер не всегда показывает наилучшую возможную площадь. Может быть, презентация могла бы быть лучше. Когда вы играете на доске большего размера, чем 3 на 3, я показываю вспомогательные числа, чтобы вы могли легко выбрать правильный квадрат, но сложнее увидеть все возможные ходы, которые вы можете сделать как игрок.

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

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

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