Игра, която ме вдъхнови да я превърна в алгоритъм.

SET е игра на карти, която предизвиква играчите не само да разпознават модели, но и да забелязват анти-модели. Играта започва с 12 карти с лицето нагоре на масата и всички играчи напрегнато се взират в дизайните на всяка карта, за да образуват комплект. Комплектът представлява три карти, които просто са свързани помежду си с техните детайли. Има четири атрибута за всеки дизайн и те имат по три възможности:

Има два основни начина за създаване на набор: намиране на модел (когато даден атрибут е един и същ за трите карти) или намиране на обикновено по-трудно забележимия анти-модел (когато атрибут е различен за трите карти). За да поясним, наборът може да има произволна комбинация от атрибути, всички съвпадащи или всички различни, така че детайлите трябва да са последователни за този конкретен атрибут. Ето няколко примера:

След като бях запознат с SET този уикенд, открих, че си мисля „Мога да напиша алгоритъм за тази игра“. (Странична бележка: като човек, който наскоро се е прехвърлил в технологиите, да имаш този вид мисъл беше като обратното на синдрома на самозванеца и се чувства страхотно). Преди да напиша какъвто и да е код, помислих каква информация ще предоставя и какво ще направи алгоритъмът ми с тази информация. Първоначалната ми мисъл беше да напиша функция, на която са дадени 2 параметъра: 1 масив, който съдържа 12-те карти, които представляват тези, поставени на масата, и друг масив, който ще има всички възможни комбинации от набори. Този първи подход беше вдъхновен от старо предизвикателство, което изпълних, когато за първи път се учех да програмирам. Предизвикателството беше да се изгради програма, която, когато й бъде дадена дъска с тик-так, да върне печелившия играч (X или O) или да начертае.

Въпреки че това е напълно добър подход, разбрах, че има много стъпки за постигане на тази крайна цел. Затова реших да разделя тази задача на по-малки части. Първата ми цел: да създам алгоритъм, който, когато му бъде даден масив, представляващ три карти, ще върне вярно или невярно по отношение на това дали тези три карти ще направят валиден набор или не.

Сега съм готов да се потопя в моя текстов редактор, но не толкова бързо. Дори тази опростена цел трябва да бъде разделена на по-малки стъпки. Така че го разделих на още по-малки задачи и се съсредоточих върху алгоритъм за всеки атрибут. Например, за да проверите дали цветовият атрибут на всяка карта е еднакъв или различен:

function colorCheck(c1, c2, c3) {
 if (c1 === c2 && c1 === c3) {
  return true;
 } else if (c1 !== c2 && c1 !== c3) {
  if (c2 !== c3) {
   return true;
  } else {
   return false;
  }
 } else {
 return false;
 }
}

След като създадох подобна функция за всяка проверка на атрибут, създадох няколко прости теста за всеки вариант:

// all different
console.log('color check: should return true => ', colorCheck('purple', 'green', 'orange'))
// all same
console.log('color check: should return true => ', colorCheck('purple', 'purple', 'purple'))
// 2 same, 1 different
console.log('color check: should return false => ', colorCheck('purple', 'green', 'purple'))
console.log('color check: should return false => ', colorCheck('purple', 'purple', 'green'))
console.log('color check: should return false => ', colorCheck('orange', 'green', 'green'))

След като тестовете преминат, всички части се нанизват заедно, за да се създаде тази чудовищна функция:

// given 3 cards from the game SET:
// return true if those 3 would make a valid set
// return false if they would not
function threeIsSet(cards) {
 let color1 = cards[0].color
 let color2 = cards[1].color
 let color3 = cards[2].color
 let number1 = cards[0].number
 let number2 = cards[1].number
 let number3 = cards[2].number
 let pattern1 = cards[0].pattern
 let pattern2 = cards[1].pattern
 let pattern3 = cards[2].pattern
 let shape1 = cards[0].shape
 let shape2 = cards[1].shape
 let shape3 = cards[2].shape
 let colorSet = colorCheck(color1, color2, color3);
 let numberSet = numberCheck(number1, number2, number3);
 let patternSet = patternCheck(pattern1, pattern2, pattern3);
 let shapeSet = shapeCheck(shape1, shape2, shape3);
 let isSet;
if (colorSet && numberSet) {
 if (patternSet && shapeSet) {
  isSet = true;
 } else {
  isSet = false;
 }
 } else {
  isSet = false;
}
return isSet;
}

За да тествам функцията threeIsSet, създадох 2 обекта. Единият обект представлява три карти, които биха съставили валиден набор, а другият обект представлява три карти, които не биха съставили валиден набор:

//hard-coded set of 3 that would return true:
let threeCardsTrue = [
 {
  color: 'green',
  number: 1,
  pattern: 'solid',
  shape: 'pill'
 },
 {
  color: 'green',
  number: 1,
  pattern: 'empty',
  shape: 'diamond'
 },
 {
  color: 'green',
  number: 1,
  pattern: 'striped',
  shape: 'curve'
 }
]
//hard-coded set of 3 that would return false:
let threeCardsFalse = [
 {
  color: 'orange',
  number: 1,
  pattern: 'empty',
  shape: 'diamond'
 },
 {
  color: 'orange',
  number: 2,
  pattern: 'empty',
  shape: 'diamond'
 },
 {
  color: 'purple',
  number: 3,
  pattern: 'empty',
  shape: 'diamond'
 }
]

Последната стъпка (засега) е да тествате тази функция с тези 2 обекта:

threeIsSet(threeCardsTrue))
// => true ✅

threeIsSet(threeCardsFalse)
// => false✅

🎉 🎊 🥳 Хуза! Алгоритъм, който ще потвърди или отхвърли дали три карти биха направили валиден комплект в играта на карти SET.

Ако сте запознати с термина DRY (не се повтаряйте), вероятно вече сте разбрали, че първата ми итерация на този алгоритъм не е много DRY. Тук има много място за оптимизация, преди да преминете към по-сложни проблеми. По-конкретно, местата, където повтарям код в моята програма, могат да бъдат пренаписани, за да станат динамични. (Например: мога да създам една функция за проверка, която може да се използва повторно за всеки атрибут, тъй като логиката за всеки от тях е абсолютно същата). Може би следващият ми блог ще бъде за това как подобрих тази функция... 😅

Ако искате да си поиграете с този код сами 👩‍💻, вижте го в REPL или Github