Задание 1 Stanford CS 193P Fall 2017. Игра Концентрация. Решение.

Содержание

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

Текст Домашнего задания на английском языке доступен на  iTunes в пункте “Programming: Project 1: Concentration″. На русском языке вы можете прочитать текст Задания 1 здесь:, а скачать здесь:

Задание 1 Игра Концентрация iOS 11.pdf

Начинаем выполнять Задание 1 с кода, полученного в конце Лекции 2. Профессор настоятельно рекомендует не копировать код первых 2-х Лекций, а непосредственно печатать его в Xcode, так как это даст хороший опыт освоения среды разработки Xcode 9.

Я все-таки привела на Github коды демонстрационного примера, соответствующие окончанию Лекций 1 и 2. Это позволит совсем начинающим не «застрять» на самом первом этапе.

Решение данного Задания 1 находится на Github :

  • со структурой struct в папке Concentration I struct.
  • с кортежем (tuple)  в папке Concentration I tuple.

ОБСУЖДЕНИЕ МАТЕРИАЛОВ курса «Разработка iOS приложений с Swift» проводится на private новом форуме на Piazza. Делиться своими решениями и задавать вопросы можно там.
Для регистрации вам необходимо пройти по ссылке:
http://piazza.com/moscow_physical_engineering_institute_bestkora.com/spring2017/mf141
и набрать private  код mf141.

Пункт 1 обязательный

Заставьте работать игру Концентрация, которая демонстрировалась на Лекциях 1 и 2. Печатайте весь код, не пользуйтесь копированием и вставкой кода откуда-то.

Замечание. Хотя предлагается начать с того, что набирать код вплоть до конца Лекции 2, я продлила набор кода до окончания Лекции 3, ибо на Лекции 3 профессор, используя игру Концентрация как «полигон» для показа возможностей языка Swift, сделал ряд небольших, но значительных усовершенствований игры Концентрация. Эти усовершенствования, во-первых, позволили получить полноценное приложение, работающее на различных iOS устройствах, а также в портретном и ландшафтном режимах, а во-вторых, значительно упростить код для логики игры Концентрация, который в этом Задании нам придется наращивать. В принципе, вы можете пойти и дальше, до Лекции 4, в которой профессор продолжает улучшать код игры Концентрация, но я решила остановиться на Лекции 3.

Код, который соответствует окончанию Лекции 3 находится на Github в папке Concentration L3.

Пункт 2 обязательный

Добавьте больше карт в вашу игру.

Выделяем первую строку в «сетке», составленной из карт, и дублируем эту строку, используя меню Edit -> Duplicate:

Появляется дубликат над нашим экраном фрагментом, который мы перетягиваем в стек Stack View, соответствующий общей сетке карт :

новая строка становится 4-ой строкой в нашей «сетке»:

Точно также добавим 5-ую строку карт в нашу «сетку»:

Проверим, подсоединены ли вновь появившиеся кнопки к методу @IBAction touchCard и нашей «сетке» карт @IBOutlet cardButtons: 

c методом @IBAction touchCard все в порядке, а вот с «сеткой» карт @IBOutlet cardButtons есть проблемы:

Новые кнопки оказались не подсоединенными к сетке карт cardButtons, и, как показано в начале Лекции 3,  мы должны подсоединить их одну за другой с помощью CTRL-перетягивания:

В результате вся «сетка» карт окажется полностью подсоединенной к  @IBOutlet cardButtons:

Но добавление новых карт приводит к тому, что в ландшафтном режиме метка «Flips: 0» вытесняется за пределы экрана:

И НЕ помогает НИЧЕГО: ни изменение размера шрифта, ни задание режима  Autoshrink: («автоматического сжатия»):

Единственное, что помогает вернуть метку «Flips: 0» на экран, — это увеличение приоритета «сопротивления сжатию» (Content Compression Resistance Priority)  по вертикали даже на 1 (вместо устанавливаемого по умолчанию 750 — > 751):

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

Если мы посмотрим на аналогичную характеристику кнопок, соответствующих «сетке» карт, то увидим, что ее вертикальное значение равно 750, что меньше 751, и если обстоятельства на экране сложатся так, что нужно «что-то» сжать, то это будет «сетка» карт :

В результате метка «Flips: 0» вернется на экран, а «сетка» карт сожмется:

Это то, что нам нужно.

Пункт 3 обязательный

Добавьте на ваш UI кнопку “New Game”, которая заканчивает текущую игру и начинает новую.

Вытягиваем кнопку из Палитры объектов, размещаем ее пока рядом с меткой «Flips: 0», меняем ее атрибуты:

Убираем все ограничения, которые мы установили метке «Flips: 0» и размещаем ее в один горизонтальный Stack View вместе с кнопкой «New Game» :

Устанавливаем ограничения на Stack View  :

  • «лидирующий» и «хвостовой» края выравниваем по краям «сетки»
  • «нижний» край приклеиваем к нижнему краю «безопасной области»
  • расстояние по вертикали между Stack View  и «сетке»  >=  Standard Value:

Никаких дополнительных характеристик для кнопки «New Game» типа приоритета «сопротивления сжатию» (Content Compression Resistance Priority)  по вертикали не нужно из-за того, что она находится в одном Stack View с меткой «Flips: 0». Этот  Stack View имеет следующие настройки:

Для кнопки «New Game» напишем в классе Concentration public метод resetGame ():

Этот метод переворачивает все карты «лицом» вниз и убирает все совпадения. Кнопку «New Game» связываем в ViewController c методом  @IBAction newGame():

В этом методе мы устанавливаем карты в игре в исходное состояние с помощью метода game.resetGame (), обновляем UI и устанавливаем число переворотов flipCount в 0.

Пункт 4 обязательный

В данный момент карты в Модели не рандомизированы ( именно поэтому в вашем UI парные карты всегда лежат на тех же самых местах). Перетасуйте карты в методе init() класса Concentration.

Для реорганизации элементов массива Array есть множество алгоритмов «перемешивания» (Shuffle) его элементов. Очень хорошо преимущества некоторых алгоритмов «перемешивания» (Shuffle) описаны в статье . Наиболее известным является алгоритм «тасования Фишера- Йенса». Реализацию алгоритмов «тасования» в Swift можно посмотреть здесь. Я выбрала один из наиболее эффективных алгоритмов тасования «по месту» и поместила его в расширение extension Array:

Получение случайного целого числа выполняется с помощью свойства arc4random, которое присутствует в расширении extension Int, созданным профессором на Лекции 2:

Добавляем «тасование» карт в инициализатор init и метод переустановки игры resetGame() в классе Concentration:

Пункт 5 обязательный

Введите в игру концепцию “Тема” (“theme”). Тема theme определяет множество эмоджи, из которого выбираются эмоджи для карт. Все эмоджи в определенной теме theme должны иметь отношение к этой теме. Смотри Подсказки (Hints), в которых есть примеры таких тем. Ваша игра должна, по крайней мере, иметь 6 различных тем, и темы должны выбираться случайно каждый раз при старте новой игры

 

Темы запоминаются в словаре emojiThemes с ключом в виде названия темы и значением в виде массива доступных для данной темы эмоджи:

Управление темами осуществляется с помощью индекса indexTheme в вычисляемом массиве ключей keys словаря emojiThemes :

Если кто-то изменяет индекса indexTheme, то мы заполняем «расходный материал» — массив emojiChoices, и обнуляем словарь эмоджи для карт emoji, формируемый «на лету».

Индекс indexTheme, мы будем получать случайным образом в двух места: в методе «жизненного цикла» viewDidload при старте приложения и при начале новой игры:

В результате, кликая на кнопке «New Game«, мы будем иметь различный набор эмоджи:

Пункт 6 обязательный

Ваша архитектура должна давать возможность добавлять новую тему одной строкой кода.

Это так и есть. Мы можем с помощью одной строки кода добавить, например, тему «Transport«:

И мы можем поиграть с новой темой :

Пункт 7 обязательный

Добавьте на ваш UI метку для счета в игре (score label). Счет в игре формируется добавлением 2-х очков за каждое совпадение и штрафом в 1 очко за каждое несовпадение ранее увиденной карты.

Скопируем метку «Flips: 0» и дадим новой метке заголовок «Score: 0». Поместим новую метку в тот же самый стек Stack View и изменим всем элементам в этом стеке размер шрифта с 40 points на 25 points. И все:

Получим в ViewController с помощью CTRL-перетягивания в код Outlet scoreLabel на эту метку:

Все, наш UI готов для вычисления счета в игре Концентрация.

Идем в Модель и в классе Concentration вводим переменную score и множество уже ранее увиденных и не совпавших карт seenCards :

Еще у нас есть структура Points, задающая бонусы и штрафы:

Начисление бонусных очков и штрафов ведется в методе chooseCard, если у вас открыты 2 карты: карты совпали — бонус, карты не совпали — штраф за каждую ранее увиденную и не совпавшую карту:

Затем в ViewController синхронизируем наш UI со значение счета score в игре :

Пункт 8 обязательный

Отслеживание числа переворотов карт flipСount определенно НЕ принадлежит вашему Controller в правильной MVC архитектуре. Исправьте это.

Точно также, как со счетом игры score, вводим переменную flipCount в классе Concentration:

Мы считаем число переворотов карт flipCount в методе chooseCard:

.  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .

и обнуляем при установке новой игры:

На этом мы заканчиваем внесение изменений в Модель в классе Concentration и переходим в ViewController. В ViewController убираем переменную flipCount и оставляем только Outlet для метки flipCount. Синхронизация этой метки с Моделью потребует всего одной строки при обновлении UI в методе updateViewFromModel ():

Это существенно упростило код ViewController.

Пункт 9 обязательный

Весь новый добавленный UI должен правильно располагаться и выглядеть хорошо в портретном режиме на iPhone X.

Мы уже позаботились об этом, и наш UI выглядит прекрасно. Но хотелось бы добавить заголовок с названием темы. Для этого добавим метку UILabel в тот же самый Stack View, в котором находится «сетка» карт и немного увеличим размер шрифта для нее:

Создадим Outlet titleLabel и будем устанавливать ее содержимое при каждом изменении темы:

В результате у нас появился заголовок с названием темы:

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

Пункт 1 дополнительный

Измените цвет фона (background) и цвет “обратной” стороны карты (“рубашки”) так, чтобы они соответствовали теме theme. Например, у нашей темы Хэллоуин черный цвет фона и оранжевый цвет “рубашки” карт. Возможно, у темы Зима будет голубой фон и белая “рубашка” карт. Тема Строительство будет черной и желтой. У UIViewController есть свойство с именем view, которое подсоединено к топовому view в экранном фрагменте (scene) (то есть view, у которого цвет фона был черным в Лекции).

Мы расширим нашу тему theme, и включим в нее не только набор эмоджи emojiChoices, но цвет для фона backgroundColor и цвет для обратной стороны карт cardBackColor. В этом случае можно использовать различные структуры данных для моделирования темы — кортежи, структуры и т.д.

Я предлагаю два решения: одно — с применением кортежа (tuples), а другое — с применение структуры struct.

Решение с применением кортежа (tuples).

Смоделируем тему с помощью кортежа Theme:

и будем использовать переменные backgroundColor и cardBackColor внутри ViewController для изменения цвета фона и цвета «обратной» стороны карты, которые обновляются при смены индекса темы IndexTheme:

Изменение цветов выполняется в специальном методе updateAppearance():

Метод updateAppearance() работает только при смене темы, в то время как метод updateViewFromModel() работает при каждом клике на карте, и в нем необходимо произвести минимальные изменения :

В результате получаем:

Решение с кортежем (tuple) находится на Github в папке «Concentration I tuple».

Решение с применением структуры struct.

Для моделирования темы используем вложенную структуру Theme,

и все темы пакуем в массив emojiThemes:

С массивом emojiThemes проще работать, чем со словарем emojiThemes, так как нам не нужен массив ключей keys. Получаем случайный индекс темы indexTheme напрямую из массива emojiThemes:

При смены индекса темы IndexTheme вытаскиваем «расходный материал», массив  эмоджи emojiChoices, цвет фона backgroundColor и цвет «обратной» стороны карты cardBackColor и обновляем «внешний вид» нашего UI:

Результат получаем точно такой же, как и в предыдущем разделе:

Решение со структурой struct находится на Github в папке «Concentration I struct«

Пункт 2 дополнительный

Вы можете определять время с помощью структуры Date. Почитайте документацию о том, как она работает и используйте ее при вычислении счета в игре (score) таким образом, что чем быстрее мы выбираем карты, тем лучше счет пользователя. Вы можете модифицировать Обязательное Задание 3, связанное с вычислением счета в игре, но счет все равно должен отражать вознаграждение от совпадений и штраф от несовпадений ранее виденных карт (а дополнительно основываться на времени). Совершенно нормально, если “хороший счет” будет меньшим числом, а “плохой счет” — большим числом.

Этот дополнительный пункт я выполню только для версии  приложения «Concentration I tuple«. В Модели в классе Concentration введем переменную dateClick, которая соответствует времени клика на карте и вычисляемую Int переменную timePenalty, которая показывает сколько секунд прошло с момента последнего клика:

При вычислении timePenalty мы используем расширение extension для Date,  в котором определена Int переменная sinceNow:

и ограничение на максимальный временной штраф Points.maxTimePenalty, так как не хотим наращивать временной штраф бесконечно:

При вычислении очков score учитывается временной штраф timePenalty:

Решение с кортежем (tuple) находится на Github в папке «Concentration I tuple«.

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *