Задание 2 Stanford CS 193P Fall 2017. Игра Set. Решение обязательных пунктов.

Содержание

Цель задания состоит в том, чтобы дать вам возможность создать свое первое приложение полностью с “нуля” и самостоятельно. Оно достаточно похоже на Задание 1, которое помогло обрести вам уверенность, но достаточно отличающееся, чтобы дать вам полную свободу для накопления опыта!

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

Задание 2 Игра Set iOS 11.pdf

Начинаем выполнять Задание 2 c NewProject в Xcode.

Правила игры Set:

SET INSTRUCTIONS - RUSSIAN.pdf

Для решения Задания 2 необходимо ознакомиться с Лекциями 1 — 6.

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

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

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

1. Реализуйте игру Set в версии соло (для одного игрока).

2. Разместите на экране по крайней мере 24 карты игры Set. В Set все карты всегда лежат “лицом” вверх.

 

Начинаем новый проект File-> New -> Project и выбираем шаблон Single View App. Мы не будем использовать файлы LaunchScreen.storyboard и AppDelegate.swift. И я опять размещу их в папке Supporting Files. Я сделала это исключительно из тех соображений, чтобы они не привлекали мое внимание и в результате получаем следующие два файлы — ViewController.swift и Main.storyboard:

Далее следуем подсказке № 1:

Подсказка 1

Вы можете использовать тот же самый механизм расположения UI, который мы использовали в Concentration (то есть Stack Views). В начальной стадии игры некоторые кнопки начинают с того, что не показывают карту (так как в начале у нас только 12 карт, но вы должны иметь достаточно места для размещения  24 карт) , а в более поздней стадии игры будут кнопки, представляющие совпавшие карты, которые не могут быть заменены. Мы будем обращаться с этим также, как обращались в Concentration (совпавшей и удаленной) картой (то есть кнопка существует, но она не видна пользователю).

Размещаем на storyboard одну карту (кнопку UIButton) будущей игры Set «лицевой» стороной вверх, сделав ее фон белым и поместив вертикально символы, которые станут потом в нашей игре основными. Для того, чтобы разместить на storyboard вертикальные символы, устанавливаем свойство Line Break кнопки UIButton в Character Wrap, а переносим на другую строку с помощью Alt/Option + Enter:

Мы хотим сделать нашу карту похожей на настоящую карту с закругленными краями, кроме то, нам придется управлять показом этой карты, если пользователь выбрал ее (selected), если она входит в число 3-х совпавших (matched)  или не совпавших (dismatched) карт. Для этого мы воспользуемся подсказкой № 6:

Подсказка 6

Вы можете показать выбор (selection), используя цвет фона backgroundColor кнопки UIButton, если хотите, но UIKit также знает, как разместить границу вокруг любого UIView (включая UIButton) с помощью следующего кода (который будет рисовать границу шириной 3 points голубым цветом, например):

button.layer.borderWidth = 3.0

button.layer.borderColor = UIColor.blue.cgColor

Мы будем подсвечивать границы кнопки разными цветами в зависимости от того, является ли она выбранной (selected), одной из 3-х совпавших (matched) или не совпавших (dismatched). Для этого мы создадим пользовательский класс BorderButton: с тремя основными свойствами : цветом границы borderColor, шириной границы borderWidth, радиус закругленных углов cornerRadius:

В Инспекторе Идентичности мы установим нашей кнопке класс BorderButton:

и так как свойства кнопки borderColor, borderWidth, cornerRadius являются @IBInspectable,то мы их видим в Инспекторе Атрибутов — там показаны значения, выставленные в классе BorderButton по умолчанию и именно поэтому у нас граница кнопки имеет ярко салатовый цвет. Мы можем регулировать эти параметры и я сделаю границу кнопки прозрачной:

Разместим 24 кнопки-карты простым копированием и подсоединим к нашей «сетке» карт @IBOutlet cardButtons:

Мы добавили также кнопки управления и метки, которые нам понадобятся в будущем. Заметим, что кнопки также являются пользовательскими кнопками, которые обслужите класс BorderButton:

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

При старте сдайте только 12 карт. Они могут появиться где угодно на экране (то есть необязательно их выравнивать по верху или по низу экрана или как-то еще; при старте они могут быть рассеяны, если хотите), но они не должны перекрываться.

Давайте распечатаем на кнопках-картах их индексы  и подсветим первые 12 карт оранжевым цветом, то есть карт с индексами в диапазоне 0.. <12:

Мы видим, что индексы карт не обязательно следуют в порядке слева направо сверху вниз. То есть обязательный пункт 3 предлагает не заботится о том, что первые 12 карт могут располагаться в произвольном месте, лишь бы они не перекрывали друг друга. Об этом же говорит подсказка № 3:

Подсказка 3

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

И тем не менее отладку Задания 2 легче вести, если кнопки-карты располагаются в правильном порядке  — слева направо сверху вниз :

Этого можно довольно просто добиться, если «подвязывать» кнопки одну за другой с помощью CTRL-перетягивания к @IBOutlet cardButtons в правильном порядке.

Теперь займемся Моделью игры Set.

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

5. Разрешите пользователю выбирать карты касанием для того, чтобы попытаться составить Set. На ваше усмотрение, как показывать “выбор”  в вашем UI. Некоторые идеи того, как это можно сделать представлены ниже в подсказках. Также обеспечьте возможность переход из состояние “выбрано” (selected) в состояние “не выбрано”(deselected) (но только когда 1 или 2 (не 3) карты выбраны в данный момент).

6. После того, как выбраны 3 карты, вы должны дать пользователю индикацию, совпали ли эти 3 карты или нет (согласно правилам игры Set). Вы можете сделать это с помощью цвета или как хотите, но пользователю должно быть понятно, совпали эти 3 карты или нет…

При построении Модели мы будем принимать во внимание следующие подсказки:

Подсказка 16 и 17

16. Было бы хорошо иметь MVC дизайн, который бы не привязывал жестко специальные имена цветов и фигур (типа diamond (ромб) или oval (овал) или green (зеленый) или striped (штрихованный)) к именам свойств в коде нашей Model. Как видите, в этом домашнем Задании (где мы используем ▲●■ вместо стандартных фигур и “затенение” (shading) вместо  “штриховки” (striping) и т.д. ) цвета, фигуры и т.д. действительно являются UI концепцией и не должны иметь ничего общего с Model.

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

Карта в игре Set имеет 4 характеристики: количество символом number, цвет символов color, тип символа (или фигура) shape и наполнение fill. У каждой из этих характеристик есть 3 варианта значений v1, v2, v3, v4 . Карта Set в нашем приложении моделируется структурой struct SetCard :

Как видите, у нас получилась карта  Set полностью абстрактной, мы не привязаны ни к каким характеристикам ее визуализации. Кроме того структура SetCard является  полностью неизменяемой (immutable) (так как в ней одни только константы lets и вычисляемые переменные vars типа read- only).

В структуре  struct SetCard есть static var isSet, которая отвечает на вопрос о том, составляет ли набор из 3-х карт Set или нет. Нашу структура SetCard реализует протокол Equatable, это даст нам возможность в дальнейшем воспользоваться при реализации игры Set такими удобными методами массива Array как index(of:) и contains(). Но они работают только с теми массивами Arrays, элементы которых реализуют протокол Equatable (как это делают, например, Int и String).

Колода Set карт моделируется с помощью структуры struct SetCardDeck и полностью повторяет логику построения колоды игральных карт, которую профессор демонстрировал на Лекции 5:

С помощью изменяемой функции draw() мы можем вытянуть случайную карту из колоды Set карт. Расширение extension Int взято из игры Concentration, которая обсуждалась на Лекции 1 и Лекции 2.

Логика игры Set в нашем приложении моделируется структурой struct SetGame. Единственная действительная функциональность нашей Model состоит в выборе (selecting) карт с тем, чтобы попытаться обеспечить их совпадение, и сдаче 3-х новых карт по требованию (потому что это фундаментальная концепция игры Set).

Поэтому основным методом в ней, также как и в приложении Concentration в struct Concentration, является изменяемый метод:


 mutating func chooseCard(at index: Int)


Но в отличие от игры  Concentration, в которой мы отслеживали состояния “лицом вверх” (faceUp) и “совпадение” (isMatched), легче отслеживать список всех выбранных (selected) карт или всех уже совпавших карт в структуре struct SetGame, чем иметь Bool переменную var  в нашей структуре данных для SetCard.

У игры Set есть список карт, которые находятся в игре, у нее есть несколько выбранных (selected) карт, она знает, совпадают (match) или нет выбранные (selected) в настоящий момент карты, у нее есть колода карт, из которой карты сдаются (deal), и, возможно, она хочет отслеживать, какие карты уже совпали (matched). В ней действительно очень много чего. API вашей Model должно представлять все эти концепции очень ясно. В нашей структуре SetGame для этой цели созданы массивы:

Их названия говорят сами за себя:

  • cardsOnTable — карты, находящиеся в данный момент на игровом столе
  • cardsSelected  — выбранные карты, их не может быть больше 3-х
  •  cardsTryMatched  — 3 карты для испытания на Set
  •  cardsRemoved — карты, составившие Set, и удаленные из игры

Кроме того, у нас есть колода deck, состоящая из 81 Set карты :

и метод получения 3-х случайных карт [SetCard]? из колоды take3FromDeck, если они так еще остались:

Есть возможность сдачи 3-х случайных карт из колоды, то есть размещения их на игровом столе, с помощью метода deal3( ):

Есть метод replaceOrRemove3Cards, с помощью которого можно заменить 3 карты cardsTryMatched на игровом столе, которые предназначены для испытания на Set, на любые 3 случайные карты из колоды. Если в колоде не остается карт для замены, то эти 3 карты удаляются с игрового стола :

Есть очень интересная вычисляемая переменная isSet — это Optional<Bool>,  она работает (то есть возвращает true или false) только тогда, когда в массиве cardsTryMatched находятся 3 карты для испытания на Set. Если вы устанавливаете переменную isSet в true или false, то три выбранные карты «перемещаются» из массива выбранных карт cardsSelected в массив cardsTryMatched испытания на Set, а если присваиваете значение nil, то массив cardsTryMatched очищается.

Но вся логика игры Set находится в методе chooseCard(at index: Int), который следует нескольким обязательным пунктам Задания 2.

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

7. Когда выбрана новая карта и есть уже 3 выбранных (selected) и не совпавших Set карты, сделайте эти 3 не совпавших карты не выбранными (deselected), а новую карту выбранной (selected).

8. Согласно правилам игры Set, когда выбрана новая карта и есть уже 3 совпавших (matching) и выбранных (selected) Set карты, замените эти 3 совпавших (matching) Set карты новыми из колоды в 81 Set >карту (опять, смотрите правила игры Set и что собой представляет колода Set карт). Если колода пуста, то совпавшие (matching) Set карты не могут быть заменены, но они могут быть скрыты (hidden) в вашем UI. Если вновь выбранная карта является одной из 3-х совпавших (matching) Set карт, то никакие карты не должны быть выбранными (selected) (так как вновь выбранная карта либо будет заменена, либо будет больше невидима на UI).

Давайте посмотрим, как выглядит метод chooseCard(at index: Int):

Центральная и главная часть метода — выбор 3-х карт, которые мы должны проверить на Set с помощью static метода isSet структуры SetCard:

if cardsSelected.count == 2, !cardsSelected.contains(cardChoosen){
                cardsSelected += [cardChoosen]
                isSet = SetCard.isSet(cards: cardsSelected)
} else {
               cardsSelected.inOut(element: cardChoosen)
}

В любом случае переменная isSet устанавливается в false или в true, а в этом случае все 3 выбранные карты удаляются из массива cardsSelected выбранных карт и перемещаются в массив карт cardsTryMatched, которые мы должны испытывать на Set, и мы сразу же сможем в дальнейшем отобразить их на UI соответствующим образом:

var isSet: Bool? {
     get {
             guard cardsTryMatched.count == 3 else {return nil}
            return SetCard.isSet(cards: cardsTryMatched)
     }
     set {
            if newValue != nil {
                   cardsTryMatched = cardsSelected
                   cardsSelected.removeAll()
            } else {
                   cardsTryMatched.removeAll()
            }
     }
}

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

Вместо рисования Set карт в классической форме (мы будем это делать на следующей неделе), мы будем использовать эти 3 символ ▲ ● ■ и использовать атрибуты в NSAttributedString для соответствующего их рисования (то есть цвета и затенение (shading)). И таким образом, ваши карты могут быть просто кнопками UIButtons. Смотри подсказки с предложениями о том, как показывать различные варианты Set карт.

Посмотрим, какие нам предлагают подсказки.

Подсказка 8

Если вы хотите закрасить (fill) символ в NSAttributedString, то используйте NSAttributedStringKey.strokeWidth с отрицательным числом.

Подсказка 9

Для того, чтобы Set карта выглядела “заштрихованной” (“striped”), просто используйте NSAttributedStringKey.foregroundColor с 15% alpha (создается с помощью UIColor метода withAlphaComponent>). Цвет foregroundColor со 100% alpha может быть использован для “закрашенной“ (“filled”) карты и позитивное значение strokeWidth для “пустой” (“outline”) карты.

Подсказка 10

Помимо указанных выше двух атрибутов NSAttributedStringKeys, вам еще может понадобиться только NSAttributedStringKey.strokeColor>.

Подсказка 11

Вы можете использовать какие хотите цвета для вашего UI (то есть вы не обязаны использовать “стандартные” цвета игры Set).

Подсказка 12

кнопок с вашими Set картами. У некоторых шрифтов эти три фигуры (▲●■) могут иметь различный размер. Похоже, что у systemFont размеры всех фигур одинаковые.

Мы создадим класс SetCardButton, для кнопки, представляющей Set карту на нашем UI, он наследует от класса BorderButton, так как нам придется использовать границу для выделения Set карты в различных обстоятельствах, и у него есть свойство setCard, которое является Optional SetCard? :

Установив это свойство извне, мы можем настроить внешний вид кнопки с помощью метода updateButton(), сделав заголовок кнопки строкой с атрибутами attributedString, установив цвет фона backgroundColor, цвет рамки  borderColor, возможность взаимодействия с кнопкой isEnable:

Если свойство setCard не равно nil, то есть действительно установлена какая-то Set карта, то мы определяем строку с атрибутами attributedString как NSAttributedString с помощью private вспомогательного метода 

setAttributedString (card: SetCard) -> NSAttributedString

а затем устанавливаем ее в качестве заголовок кнопки помощью метода:

 setAttributedTitle (attributedString, for: .normal)

Попутно устанавливаем цвет фона Set карты в белый и делаем доступной кнопку для пользователя.

Если свойство setCard не равно nil, то делаем кнопку прозрачной, все заголовки ее устанавливаем в nil и делаем недоступной пользователю:

Значение nil для свойства setCard очень удобно, если карта должна оставаться на UI, но должна быть невидима и недоступна для пользователя. Именно такая ситуация складывается в конце игры Set, когда карт в колоде не остается, замены совпавших карт не происходит и они должны быть убраны с игрового стола.

Ключевым методом, формирующим UI нашей Set карты, является метод setAttributedString (card: ), который в качестве аргумента берет нашу Set карту. Set карта в нашем приложении моделируется структурой struct SetCard со следующими свойствами:

Все свойства имеют ТИП Variant, который является перечислением enum:

Для отображения этих характеристик Set карты на UI у нас будут 4 массива:

Массив colors для 3-х значений свойства color карты SetCard, массивы alphas и strokeWidths — для 3-х значений свойства fill карты SetCard, массив symbols — для 3-х значений свойства shape, 3 значения свойства number отображаются просто количеством символов (1, 2 или 3) в строке с атрибутами.

Наш вспомогательная функция 

setAttributedString(card: SetCard) -> NSAttributedString

которая берет в качестве аргумента Set карту card ТИПА SetCard и, используя массивы symbolscolors, alphas и strokeWidths, возвращает строку с атрибутами NSAttributedString, соответствующую этой карте. Давайте посмотрим, как эта функция работает :

Имея в распоряжении Set карту card и ее свойство card.shape, мы определяем, какой символ symbol будет отображаться на нашей карте, то есть какой элемент массива

symbols = [«●», «▲», «■»]

нам следует взять, исходя из значения свойства card.shape. Свойство card.shape имеет ТИП перечисления enum Variant с rawValues 1,2,3, и мы могли бы использовать его rawValue для доступа к элементу массива symbols, но чтобы каждый раз не писать card.shape.rawValue — 1, мы наделили наше перечисление Variant вычисляемой переменной idx :

Пользуясь свойством idx нашего перечисления card.shape мы можем очень легко определить требуемый символ symbol :

let symbol = symbols [card.shape.idx]

Нам нужно сформировать строку, отображаемую на нашей Set карте, состоящую из card.number символов, разделенных некоторым разделителем separator, который будет зависеть от того, в каком режиме мы находимся —  в портретном или ландшафтном. В портретном режиме наши символы будут располагаться вертикально и, следовательно, разделителем separator будет символ «\n» перехода на другую строку, а в ландшафтном — горизонтально и, следовательно, разделителем separator будет просто пробел » « :

Для удобства формирования строки String из произвольного числа заданных символов с разделителем separator мы разместим в расширении extension строки String функцию: 

func join(n: Int, with separator:String )-> String

у которой два аргумента: n — число повторений исходной строки и separator — разделитель, на выходе этой функции — строка String.

Вот реализация функции join (n: , with separator:) :

Она нам поможет сформировать строку symbolsString для нашей Set карты:

Далее нашу Set карту нужно наделить атрибутами: шириной обводки символов  strokeWidth, прозрачностью alpha и цветом color, которые определяются свойствами color (цвет) и fill (заполнение) нашей Set карты card и также являются также перечислениями enum Variant с rawValues 1,2,3, и мы опять будем использовать их вычисляемое свойство idx для доступа к массиву прозрачностей alphas, к массиву различных вариантов ширины линии обводки strokeWidths и массиву цветов colors:

Имея строку symbolsString и атрибуты attributes, нам очень просто сформировать и вернуть строку с атрибутами:

Портретный или ландшафтный режим мы определяем с помощью вычисляемой переменной var c именем verticalSizeClass:

именно она участвует в определении разделителя separator :

Она должна изменяться при изменении границ bounds нашей копки SetCardButton, например, при повороте нашего устройства. Следовательно, мы должны обновить кнопку в методе layoutSubviews() :

Это даст возможность иметь различное адаптивное расположение символов на кнопке-карте в портретном и ландшафтном режимах:

У класса SetCardButton есть также метод setBorderColor установки цвета границы кнопки, который, конечно, нам необходим при индикации выбранных (selected) Set карт; карт, представляющих собой Set, и карт, которые мы испытывали на Set и которые ему не удовлетворяют:

Теперь обратимся к ViewController. Он будет выглядеть очень похоже на тот, что был в игре Concentration:

Также есть переменная var game,  которая представляет собой Model игры Set, есть Action touchCard (_ sender: SetCardButton) который срабатывает при клике на кнопке-карте и вызывает метод game.chooseCard (at:), принадлежащий Model, есть также метод  updateViewFromModel(), который в нашем случае представляет собой обновление по данным Model не только кнопок-карт, но и заголовков кнопок и меток, но все же его главной частью является метод  updateButtonsFromModel(), который обновляет кнопки-карты и который в нашем случае намного интереснее аналогичного метода в игре Concentration:

Мы устанавливаем цвета для индикации карт с помощью структуры struct Colors:

Запускаем приложение, и смотрим, что будет происходить, если мы кликаем сначала на одну карту, затем еще на одну и, наконец, на третью карту:

Мы получили Set — об этом говорит подсветка границ совпавших карт бирюзовым цветом, изменившийся счет Score и текст «СОВПАДЕНИЕ» в информационной метке внизу. Кроме того, пользователь видит все 3 карты, принадлежащие Set. На следующем шаге совпавшие карты будут заменены на новые карты из колоды и удалены с игрового стола, если в колоде карт не окажется, то замены карт не произойдет, а соответствующие совпавшим картам кнопки будут невидимы и недоступны для пользователя.  Игра Set продолжится в новой конфигурации карт :

Если мы не получили Set, то карты НЕ удаляются и НЕ заменяются на новые на следующем шаге и игра начинается сначала при той же конфигурации.

Еще раз вернемся к нашему пользовательскому классу  SetCardButton, представляющему Set карту в виде кнопки на нашем UI. У него 5 public переменных, которые мы можем устанавливать извне:

Массивы symbolscolors , alphas и strokeWidths задают UI Set кнопки по умолчанию, все эти массивы могут быть установлены извне, например, из ViewController, что полностью поменяет UI ваших Set кнопок:

Ваш UI изменится:

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

Вам необходимо также иметь кнопку “Deal 3 More Cards” (Сдай еще 3 карты) (согласно правилам игры Set).

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

Когда кнопка “Deal 3 More Cards” (Сдай еще 3 карты) нажата, то  либо a) происходит замена выбранных карт, если они совпали, либо b) добавляются 3 карты в игру.

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

Кнопка “Deal 3 More Cards” (Сдай еще 3 карты) должна быть недоступна, если a) больше нет карт в Set колоде или  b) больше нет мести на UI, чтобы принять еще 3 карты (заметьте, что всегда есть место для размещения еще 3-х карт, если выбранные в данный момент карты совпали (match), так как они заменяются).

Кнопка “Deal 3+» » сдает » 3 карты из колоды, если есть место на UI для их добавления:

Кнопка “Deal 3+» становится недоступной, если в колоде нет больше карт, или если отсутствует место на UI:

Поэтому если колода пуста, то кнопка  “Deal 3+» будет выглядеть так:

Подсказка 20

Внимательно проверяй “конец игры.” Когда колода Set карт исчерпалась, успешно совпавшие карты не подлежат замене новыми картами. Эти НЕ-заменяемые совпавшие карты не могут появляться на UI (иначе пользователи могут попытаться найти совпадение их с другими картами!). По этой причине API вашей Model должен выявлять, какие карты уже успешно совпали.

В нашей Модели есть массив cardsRemoved уже совпавших и удаленных с игрового стола карт:

Мы обнаруживаем ( или не обнаруживаем) Set в Модели сразу же, как только выбрана 3-ая карта в добавок к остальным 2-м. Мы списываем массив выбранных 3х карт cardsSelected  в массив карт cardsTryMatched для тестирования на Set :

и можем показывать их на UI с помощью рамки определенного цвета:

Мы получим экран одного из 2-х видов: бирюзовый цвет рамки — Set, черный цвет рамки — НЕ Set:

Продолжить игру мы сможем только, если выберем новую карту, не принадлежащую ни уже совпавшим и убранным с игорного стола картам cardsRemoved, ни картам cardsTryMatched попавшим на испытание на Set :

Далее анализируя Optional переменную isSet, мы смотрим, принималось ли решение о Set на предыдущем шаге. Если да, то в случае Set мы либо заменяем совпавшие карты cardsTryMatched на новые карты из колоды, либо нет, но в любом случае переводим совпавшие карты в карты cardsRemoved, удаленные с игорного стола. Если Set не обнаружен, то с картами ничего, они остаются в игре. В обоих этих случаях  мы устанавливаем переменную isSet в nil, тем самым освобождая массив карт  cardsTryMatched, собранных для испытания на Set:

Таким образом, Set карта проходит следующий путь: из колоды deck попадает на игорный стол  cardsOnTable, затем в выбранные карты selectedCards, затем в массив карт cardsTryMatched, собранных для испытания на Set, и в случае успешных испытаний в совпавшие и удаленные с игорного стола карты cardsRemoved:

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

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

Если вы напишете алгоритм определения Sets, то сможете добавить “мошенническую” кнопку для того, чтобы испытывающий трудности пользователь смог использовать ее для нахождения Set!

В Модели в структуре SetGame пишем алгоритм нахождения всех Sets для карт, лежащих на игровом столе — это будет переменная var hints, которая содержит массив Sets, то есть массивы индексов карт, составляющих Set:

И на нашем UI размещаем  “мошенническую” кнопку, на которой показывается количество Sets для карт, лежащих на игровом столе:

Но это не все, при нажатии на кнопку «Hint» у вас последовательно будут подсвечиваться на очень короткое время кнопки, составляющие Set, а на информационной метке появится сообщение о номере подсвечиваемого Set:

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

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

При определении Set в структуре SetCard мы использовали метод reduce, требующий замыканий:

Для индикации карт, составляющих Set, мы использовали в ViewController метод forEach и метод Timer.scheduledTimer(withTimeInterval: Constants.flashTime, repeats: false), требующие замыканий в качестве аргументов :

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

Используйте перечисление enum как значимую часть вашего решения.

В Модели Set карты в структуре SetCard мы использовали enum для представления 4-х характеристик:

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

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

В Модели игры SetGame мы использовали расширения массива Array c Equatable элементами:

На Swift форуме есть интересная статья «Conditional Conformance in the Standard Library» на этот счет, но она относится к Swift 4.1, которого пока нет.

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

Ваш UI должен иметь прекрасно расположенные UI элементы и хорошо выглядеть ( по крайней мере в портретном режиме, желательно также и в ландшафтном режиме, хотя это не обязательно) на любом iPhone 7 или старше. Это означает, что вам следует использовать несколько простых приемов работы с Autolayout, включая Stack Views.

Используем приемов работы с Autolayout, включая  Stack Views, точно такие же как и для Задания 1 и так, как показывал профессор на Лекции 2. Но у нас есть возможность улучшить вид нашей игры Set в ландшафтном режиме за счет расположения символов игры Set не по вертикали, а по горизонтали:

Этого мы добились использованием в класс SetCardButton переменной var verticalSizeClass, которая меняется, например, при повороте нашего устройства. Для этого мы обновляем нашу кнопку SetCardButton в методе layoutSubviews (), о котором профессор рассказывает на Лекции 6.

Замечания.

Замечание 1. Когда мы писали метод replaceOrRemove3Cards, в нашей игре SetGame,   мы предполагали, что совпавшие и образующие Set карты убираются с игорного стола с только в том случае, если колода карт пустая:

Таким образом, если мы добавили 3 карты к 12 картам, то мы до конца игры продолжаем играть с 15 картами или больше, если была необходимость еще добавлять по 3 карты. В то же время классическая игра Set предполагает, что как только Set обнаружен, мы опять возвращаемся к игре с 12 картами, то есть удаление карт с игорного стола происходит еще и тогда, когда на игорном столе карт больше, чем первоначальное количество Constants.startNumberCards:

Это более «жесткая игра», так как мы опять возвращаемся к ситуации 12 карт, когда может быть от 0 до 3-4 Sets и, вполне возможно, что нам снова придется добавлять 3 карты.  В случае, если у нас остается 15 карт, число возможных Sets существенно возрастает от 0 до 5-9 Sets.

Замечание 2. Когда мы писали в структуре SetGame алгоритм нахождения всех Sets для карт, лежащих на игровом столе cardsOnTable, мы не принимали во внимание, что мы можем находится в состоянии, когда Set только что определен, и все карты, принадлежащие ему, пока находятся на игорном столе:

Таково требование Задания № 2, чтобы пользователь смог видеть, какие карты вошли в Set. На следующем шаге, когда будет выбрана следующая карта и начнется следующий цикл определения Set, эти совпавшие карты будут убраны с игорного стола и уже не будут участвовать в дальнейшей игре. Но даже, если мы еще не сделали этот следующий шаг, то есть мы находимся в состоянии, когда Set только что определен, мы уже знаем, что эти карты не стоит учитывать при формировании массива подсказок hints,  поэтому мы должны исключить все Sets, вошедшие в подсказку hints, которые содержат совпавшие карты cardsTryMatched:

Мы определяем индексы matchIndices совпавших и образующих Set карт и исключаем из плсказок hints те Sets, которые имеют в своем составе индексы совпавших карт с помощью функции filter, принимающей замыкание в качестве аргумента. Это замыкание определяет пересечение множества индексов Sets из подсказок hints и множества matchIndices совпавших карт.

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

 

Вы видите, что в подсказках указываются 2 Sets, которые совсем не пересекаются с выбранным Set, и вы можете воспользоваться любым из них, потому что на следующем шаге карты из выбранного Set уйдут с игрового стола, а карты в подсказках — нет.

Бывает ситуация, когда на игорном столе при совпадении карт и получении Set может не остаться других Sets, составленных из карт, не являющихся совпавшими. Тогда мы увидим в подсказках 2 Sets, но это не означает, что мы должны добавлять из колоды еще 3 карты, это лишь означает, что на следующем шаге нам придется выбрать карту наугад, тогда совпавшие карты будут заменены новые и у нас появится маневр для дальнейших действий:

 


После того, как мы кликнули на случайной карте, совпавшие карты заменились на новые и у нас теперь 3 Sets. Естественно, в каждой подсказке из 3-х Sets содержится хотя бы одна новая карта, потому что в старых картах больше не было Sets, кроме уже выбранного iPhone.

Код находится на Github для iOS 11 и на Github для iOS 12.

Продолжение следует…

Задание 2 Stanford CS 193P Fall 2017. Игра Set. Решение обязательных пунктов.: 11 комментариев

  1. В методе takeThreeFromDeck, если в колоде осталось две карты, то метод берет эти две карты, пытается взять третью, не может, затем возвращает nil. И две карты бесследно пропадают, потому что уже были взяты, как я понял?

    • В игре Set НИКОГДА в колоде не остается НИ 2 карты, НИ 1 карта: всего в колоде 81 карта, мы начинаем с 12 карт и добавляем по 3 карты каждый раз. Так что нет смысла городить дополнительные проверки.

  2. 1. В методе replaceOrRemove3Cards всегда происходит замена 3х карт, если карты еще остались в колоде. Если понажимать несколько раз «Deal 3+» и например довести общее кол-во карт до 24, то последующее определение сетов не будет уменьшать количество карт на столе, оно так и будет оставать 24, пока не иссякнут карты в колоде, в версиях игры, в которую играл я, карты заменяются только, если их 12, если их больше, то угаданные карты просто удаляются, пока количество опять не дойдет до минимального — 12. Может стоит написать услвоие так: if cardsOnTable.count == Constants.startNumberCards, let take3Cards = take3FromDeck()
    2) Почему при использовании подсказки — Поиск Сета, выделенные ранее пользователем карты не очищаются? Получается сначала нужно снять выделение с уже выделенных карт, а уже потом поспользоваться подсказкой и выделить нужные. Может быть этого нет в условиях задачи, но работать с таким интерфейсом, как минимум неудобно.

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

  3. В replaceOrRemove3Cards в вызове методов cardsOnTable.replace и cardsOnTable.remove получаю сообщения XCode «Value of type ‘[SetCard]’ has no member ‘replace'» и «Incorrect argument label in call (have ‘elements:’, expected ‘at:’)» соответственно. В документации Swift по массивам не нашел таких методов. Почему так? (XCode 9.2).

    • Потому что эти методы находятся в расширении extension массива Array:

      • Так и знал, что решение где-то рядом! Спасибо большое.

  4. Добрый день!
    Что-то я запутался, разве игру нужно было написать не после 4-й лекции, т.е. не пользуясь материалами следующих лекций? А то много чего позаимствовано из игры демонстрационного примера 5-6 лекции?
    Если кому интересно нашел решение на основе лекций 1-4: https://medium.com/@AlanChenYY/cs193p-fall-2017-assignment-2-c1f82701e4a6
    я, если честно, вообще не знал, как к этой задаче подступиться, хотя с первым заданием разобрался вроде полностью, да и по лекциям вопросов нет, все понятно и доступно 🙁

    • Очень плохо. Вы действительно запутались.
      Ваша выполнение Задания 2 пример того, как не нужно программировать на Swift.
      Непременным условием прохождения этого стэнфордского курса является знание Объектно-Ориентированного Программирования (ООП), вам по-видимому этот раздел программирования незнаком.
      Там столько «ляпов», что уму непостижимо, как может совершить их человек, хоть сколько знакомый с программированием, но приведу только два из них:


      Вам нужно учиться и смотреть, как делают другие и почему.
      Я готова обсуждать с вами ваше решение на форуме Piazza. Вопросы можно задавать там.
      Для регистрации вам необходимо пройти по ссылке:
      http://piazza.com/moscow_physical_engineering_institute_bestkora.com/spring2017/mf141
      и набрать private  код mf141.

  5. Добрый день!
    Подскажите почему в этом методе не вернуть просто return true?
    /Users/engwar/Desktop/Screen Shot 2019-04-25 at 14.33.47.png

    • К сожалению. я не вижу вашей картинки. Она осталась локально на вашем компьютере.
      Но речь, по-видимому, идет о методе isSet.
      Вы не правы.
      Метод isSet проверяет, составляют ли любые три карты Set.
      Каждая карта имеет 4 признака, перечисленных ниже:

      Количество: на каждой карте есть один, два или три символа.
      Тип символов: овалы, ромбы или волны.
      Цвет: символы могут быть красными, зелеными или фиолетовыми.
      Заполнение: символы могут быть пустыми, зашрихованными или закрашенными.

      Цель игры SET: Среди 12 карт, разложенных на столе, нужно найти SET (набор), состоящий из 3-х карт, у которых каждый из признаков либо полностью совпадает, либо полностью различается на всех 3-х картах. Все признаки должны полностью подчиняться этому правилу.

      Например, количество символов на всех 3-х картах должно быть или одинаковым, или различным, цвет на всех 3-х картах должно быть или одинаковым, или различным, и так далее…
      В Модели каждый признак — Количество number, Тип символа shape, Цвет color и Заполнение fill— представлены перечислением Variant, имеющим 3 возможных значения: var1, var2 и var3, что соответствует 3-м целым числам rawValue — 1,2,3. В таком виде с rawValue легко оперировать. Если мы возьмем какой-нибудь признак, например, color, то сложив все rawValue для colors 3-х карт, мы обнаружим, что если colors для всех 3-х карт равны, то сумма будет равняться 3, 6 или 9, а если они все будут разные, то сумма будет равняться 6. В любом из этих случаев у нас имеет место кратность 3-м суммы rawValue для colors всех 3-х карт. Мы знаем, что это и является необходимым условием того, что 3 карты составляют SET. Для того, чтобы 3 карты действительно стали SET необходимо, чтобы для всех признаков SetCard — Количество number, Тип символа shape, Цвет color и Заполнение fill — сумма их rawValue была кратна 3-м.

      Поэтому в static методе isSet( cards:[SetCard]) мы сначала вычисляем массив sumsсумм rawValue для всех 3-х карт и для всех 4-х характеристик карты с помощью функций высшего порядка reduce с начальным значением, равным 0, и аккумулирующими функциями {$0 + $1.number.rawValue}, {$0 + $1.color.rawValue}, {$0 + $1.shape.rawValue}, { {$0 + $1.fill.rawValue}. Каждый элемент массива sums должен быть кратен 3-м, и мы опять используем функцию reduce, но на этот раз с начальным значением, равным true и аккумулирующей логической функцией «AND» {$0 && ($1 % 3) == 0}.
      Метод isSet( cards:[SetCard]) возвращает true только для 3-х карт, которые составляют Set.
      Если карты, например такие:
      SetCard( number:.v1, color: .v2, shape: .v3, fill: .v2)
      SetCard( number:.v2, color: .v3, shape: .v3, fill: .v1)
      SetCard( number:.v1, color: .v2, shape: .v3, fill: v.v2)

      то этот метод вернет false, что нам и нужно.
      Этот фантастически короткий код для выяснения того, являются ли 3 SetCard карты SET-ом, получен благодаря «функциональному» подходу.
      А вы как проверяете карты на то, что лни соствояют SET.

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