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

Содержание

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

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

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

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

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

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

  • со структурой 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  для iOS 11  и на Github для iOS 12 в папке 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  для iOS 11  и на Github для iOS 12 в папке «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  для iOS 11  и на Github для iOS 12 в папке «Concentration I tuple«.

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

  1. Мне кажется в 3 пункте нужно массив и словарь с emoji привести в исходное состояние. Или я что-то путаю?

  2. Нашел (в инете) более красивое решение для shuffle. Однако, я абсолютно не понимаю, как оно работает 🙂

    extension Array
    {
    mutating func shuffle()
    {
    for _ in 0..<10
    {
    sort { (_,_) in arc4random() < arc4random() }
    }
    }
    }

    cards.shuffle()

    • Работает оно согласно синтаксису.
      Это расширение extension для массива Array.
      В этом расширении вы определяете изменяемую (mutating) функцию shuffle(), которая «по месту» (из-за mutating) будет перетасовывать массив.
      Данный алгоритм тасования сводится к тому, что вы будете 10 раз (почему 10?) пересортировывать массив «по месту», так как у Array есть функция сортировки «по месту»:
      mutating func sort(by areInIncreasingOrder: (Element, Element) throws -> Bool) rethrows
      Единственным аргументом этой функции является замыкание (Element, Element) throws -> Bool, поэтому мы можем применить «хвостовой» синтаксис для функции sort, опустив название аргумента и круглые скобки, и просто записать:
      sort { (Element1, Element2) in Element1 < Element2 }
      В этом случае мы могли бы получить возрастующую сортировку, если бы элементы массива Element могли бы сравниваться.
      Но нам это не надо.
      Когда доходит дело до сравнения элементов Element1, Element2) и решения о том, нужно ли их менять местами, мы будем выбрасывать два случайных числа arc4random() и arc4random() и сравнивать их, то есть на основе их случайных значений мы будем принимать решение о том, нужно ли менять местами два элемента массива.
      Таким образом, сами элементы Element1, Element2 нам не нужны, поэтому на их месте мы ставим знак "подчеркивания"
      (_,_). Далее синтаксис самого замыкания (_,_) in означает аргументы в теле замыкания.
      Далее само тело замыкания. Мы должны вернуть Bool,но в замыкании можно опустить ключевое слово return, и получается вот такое вот замыкание:
      {(_,_) in arc4random() < arc4random() }
      Самое загадочное в этом алгоритме: почему нужно повторять сортировку 10 раз. Я могу и 5 раз указать, верно?

      Вообще о синтаксисе замыканий подробно рассказывается на Лекциях 3 и 4.

  3. Пытаюсь перетащить дублированный Stack View в уже существующий, как описано и нарисовано в решении 1-го пункта. Но он почему то не хочет там оставаться. И вообще Xcode не выдает ни единого признака, что таким образом можно скомбинировать 2 Stack Vew. У вас в XCode 10 работает такая возможность?

    • Все работает в Xcode 10. Как это Xcode не выдает ни единого признака, что таким образом можно скомбинировать 2 Stack Vew?
      Вы должны работать с Document Outline ( Схема UI), который находится слева от storyboard, и там можно перетаскивать любые объекты.
      Во-первых, перед дублированием убедитесь, что вы выделили нужный Stack Vew:

      Во-вторых, после дублирования ваш дубль разместился сверху:

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

      И тогда ваш Stack View встанет в один ряд с остальными, а не заберется внутрь другого стека :

      Если не получится, пришлите ссылку на ваш Github и я посмотрю, куда пропал ваш Stack View.

      • Вот ссылка на GitHub https://github.com/matt-spb/Concentration
        Надеюсь, все правильно залил.
        Первый вопрос про Stack View, что он не хочет оставаться на том месте, куда я его перетаскиваю.

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

        • Начнем со второго вопроса.
          Самый внешний Stack View должен иметь свойство Distribution равным Fill Equally, а НЕ просто Fill, как у Вас:

          Свойство Distribution равное Fill Equally означает, что внутри Stack View размеры компонентов подгоняются так, чтобы они были равны между собой (в нашем случае по вертикали. так как у нас вертикальный стэк) несмотря на их первоначальные размеры, а просто Fill означает, что компоненты входят в Stack View со своими размерами и их сохраняют несмотря ни на что. Мы не хотим Fill, так как нам нужно, чтобы размер сетки кнопок АВТОМАТИЧЕСКИ уменьшался или увеличивался.

          Если вы хотите, чтобы метка «Flips:0» «улетала», вы должны снизить ее приоритет «сопротивляемости к сжатию» (Content Compression Resistance Priority):

          После того как вы исправите «сопротивляемости к сжатию» (Content Compression Resistance Priority) метка «Flips:0» с 751 на 750, перейдите на любое другое поле, чтобы произошло запоминание, и метка «улетит»:

          Первый вопрос. Перетащите мышкой дубль Stack View снизу наверх, прямо вслед за внешним Stack View, и он встанет на место:

          Почему вы не хотите «перетаскивать»?
          Перетащите, залейте в Github, сообщите мне и я посмотрю, что у вас получилось.

          • Благодарю вас. Теперь кнопки подрезаются. А стэк так и не остается при перетаскиваении, возвращается на свое место. У вас перетаскивается? Может еще какая то галочка отвечает за возможность добавления новых элементов в стэк?

          • Я использую ваш код в Github и все перетаскивается, иногда не в то место, если неточно выставишь указатель, но ВСЕГДА куда-то перетаскивается.
            То, что вы описываете просто не может быть.
            Можете разместить в ответе screenshot того, куда вы перетаскиваете?
            Ну, в конце концов можно использовать Copy — Paste.

          • Я тоже сделала подобное видео https://drive.google.com/file/d/1LMBp7IiLT9cnPgfCijCyopNTqenQuegK/view?usp=sharing, но у меня оно выглядит немного по-другому. Синий фон, с помощью которого выделяется Stack View, у меня при перемещении исчезает и дальше я вижу только прямую, которая направляет расположение перемещаемого Stack View. У вас же Синий фон, с помощью которого выделяется Stack View, вообще не шелохнется и нет направляющей линии при перемещении.
            Какая у Вас версия Xcode 10 и Mac OS?
            У меня версия Xcode 10 Version 10.1 (10B61).
            На Mac стоит Mac OS Mojave 10.14.2.
            Может стоит перегрузить Xcode 10?

          • Матвей, то поведение, которое вы показали на видео, наблюдается, если нажата клавиша command. Вы должны выполнять действия с мышкой, никаких клавиш не нажимая дополнительно.

  4. Подскажите а разве кортеж(tuple) не создается просто:
    let Theme = (emojiChoices: [String], backgroundColor: UIColor, cardBackColor: UIColor)
    Как я понял typealias это создание типа: например мы можем переименовать тип Int в Chislo
    typealias Chislo = Int
    var num: Chislo = 0
    просто не сразу понял почему мы для создания тюпла использовали typealias

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

Обсуждение закрыто.