Перетасовуємо колоду карт з Vue.js

32 хв. читання

Передмова

У статті створимо UI, що дозволяє перетасовувати карти у випадковому порядку.

Повна версія застосунку:

Контекст

Оскільки стаття розрахована на розробників, які не знайомі з Vue, ми детально оглянемо такі пункти:

Підготовка застосунку

Відправним пунктом нашого застосунку будуть два файли — index.html та styles.css.

index.html

index.html представлятиме розмітку основної сторінки нашого застосунку і починатиметься так:

<html>

<head>
  <title>Vue Transitions - Shuffle a Deck of Cards</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>

  <link rel='stylesheet' href='https://web.archive.org/web/20230325135840/https://cdnjs.cloudflare.com/ajax/libs/bulma/0.6.2/css/bulma.min.css'>
  <link rel='stylesheet' href='https://web.archive.org/web/20230325135840/https://use.fontawesome.com/releases/v5.0.6/css/all.css'>
  <link rel="stylesheet" href="styles.css">
</head>

<body>
  <div id="app">
    <h1 class="title">
      <img class="vue-logo" src="https://vuejs.org/images/logo.png" />
      Card Shuffling
    </h1>
    <div class="deck">
      <div class="card">
        <span class="card__suit card__suit--top">♣</span>
        <span class="card__number">A</span>
        <span class="card__suit card__suit--bottom">♣</span>
      </div>
    </div>
  </div>

  <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</body>

</html>

У тегу <head> бачимо три посилання: Bulma застосовується у ролі CSS-фреймворку нашого застосунку, Font Awesome, що потрібний для іконок, та файл стилів styles.css, розміщений у корені проєкту.

Уся розмітка застосунку розташовується всередині елемента <div id="app"></div>, що у тегу <body>. У кореневому елементі id="app" на цей момент існує два дочірні елементи, які мають початкову розмітку: <h1 class="title"></h1> та <div class="deck"></div>.

У кінці тега <body> створено єдиний елемент <script>, який підвантажує Vue з Content Delivery Network (CDN).

Використання CDN для завантаження залежностей Vue — один із найпростіших та найшвидших способів використати його у застосунку.

styles.css

Файл styles.css вміщує увесь користувацький CSS, який є необхідним для застосунку. Коли ми створюємо розмітку, просто оголошуємо кожен елемент з відповідними атрибутами класу. Так ми зосереджуємося на самому Vue.

На цьому етапі наш застосунок складається лише із заголовка та одного статичного елемента карти.

HTML-секція нашого Codepen представляє користувацький інтерфейс застосунку (наприклад, елемент <div id="app"></div>) оскільки всі залежності (Bulma/Vue/JS та ін.) вводяться ззовні через редактор Codepen.

Створюємо елементи карти

Спершу нам необхідно представити список елементів для усієї колоди карт. Кожен такий елемент буде показуватись у простий спосіб, з невеликою кількістю розмітки:

<div class="card">
  <span class="card__suit card__suit--top">♣</span>
  <span class="card__number">A</span>
  <span class="card__suit card__suit--bottom">♣</span>
</div>

Враховуючи вже встановлені стилі, наведена розмітка буде мати приблизно такий вигляд:

the card

Стандартна колода карт містить чотири масті (♣, ♦, ♥, ♠), кожна з них складається з тринадцяти різних рангів: туз, числа від одного до десяти, валет, дама і король. Тобто 52 унікальні карти в колоді.

Такий стандартний тип колоди найбільш поширений і відомий як «Стандартна французька колода». Існують також інші варіації з різними розмірами колоди та унікальними картами (наприклад, з Джокером).

Для початкового стану ми розміщуємо карти в колоді, розмежовуючи між чотирма мастями. Кожна секція масті буде показувати карти в порядку зростання (наприклад, від туза до короля).

Оголошуємо статично 52 елементи різних карт у файлі index.html. Однак такий спосіб призведе до великої кількості повторюваного коду, який буже важко підтримувати, тому будемо намагатись показати колоду карт динамічно.

Екземпляр Vue

Насамперед створимо основу нашого застосунку — екземпляр Vue. Він приймає параметри об'єкта, які вміщують деталі екземпляру: шаблон, дані, методи тощо.

У новому файлі main.js ми створимо екземпляр Vue кореневого рівня та оголосимо DOM-елемент з id="app", на якому базуватиметься наш застосунок.

new Vue({
  el: '#app',
});

Для динамічного рендеру неперетасованої колоди карт у нашому застосунку ми визначимо деякі дані в екземплярі. Проініціалізуємо три різні властивості — rank, suits та cards. Властивості rank та suits призначені для показування усіх можливих рангів та мастей, які містяться в колоді. Властивість cards ініціалізується порожнім масивом та буде заповнюватись динамічно при запуску застосунку.

Код буде таким:

new Vue({
  el: '#app',
  data: {
    ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
    suits: ['♥','♦','♠','♣'],
    cards: [],
  },
});

Нам необхідно, щоб елементами масиву cards була повна колода карт у неперемішаному стані. Створимо метод під назвою displayInitialDeck(), що використає з цією метою масиви ranks та suits. Метод буде всередині властивості methods екземпляру.

new Vue({
  el: '#app',
  data: {
    ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
    suits: ['♥','♦','♠','♣'],
    cards: [],
  },
  methods: {
    displayInitialDeck() {},
  }
});

Щоб заповнити масив cards, необхідно оголосити елемент карти для кожного рангу в кожній масті. Усі представлені елементи будуть об'єктами, що складатимуться з властивостей id, rank та suit. Ці властивості будуть динамічно прив'язані до шаблону, який ми побачимо незабаром.

У методі displayInitialDeck():

  • По-перше, оголошуємо змінну id, що застосовуватиметься для кожного об'єкту card, який ми створимо. Проініціалізуємо змінну id значенням 1.
  • Встановимо значення cards як пустий масив, щоб переконатися, що ми починаємо з чистого аркуша. (Зауважте: це знадобиться нам пізніше, коли викличемо метод displayInitialDeck() після створення колоди).
  • Далі створюємо вкладений цикл for, щоб пробігтися кожним елементом у ranks для кожного елемента з suits. На кожній ітерації будемо створювати об'єкт карти та встановлювати необхідні властивості.
  • Усередині вкладеного циклу додамо оброблений об'єкт картки до cards та збільшимо на 1 властивість id.

У результаті метод displayInitialDeck() буде таким:

new Vue({
  el: '#app',
  data: {
    ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
    suits: ['♥','♦','♠','♣'],
    cards: [],
  },  
  methods: {
    displayInitialDeck() {
      let id = 1;
      this.cards = [];

      for( let s = 0; s < this.suits.length; s++ ) {
        for( let r = 0; r < this.ranks.length; r++ ) {
          let card = {
            id: id,
            rank: this.ranks[r],
            suit: this.suits[s]
          }
          this.cards.push(card);
          id++;
        }
      }
    },
  },
});

Наш метод не повинен повертати this.cards, оскільки змінні реактивні автоматично (коли this.cards змінюється — зображення перезавантажується).

Однак displayInitialDeck() необхідно викликати на самому початку, коли наш застосунок створюється. Тож викличемо метод у нескінченному циклі екземпляру [created(), який є методом, що запускається, коли компонент або екземпляр створюється у перший раз.

new Vue({
  el: '#app',
  data: {
    ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
    suits: ['♥','♦','♠','♣'],
    cards: [],
  },
  created() {
    this.displayInitialDeck();
  },  
  methods: {
    displayInitialDeck() {
      // ...
    },
  },
});

Тепер, коли наш застосунок створено, властивість cards буде заповнено 52 картами у впорядкованому форматі. Отримуємо HTML-шаблон, який може показати ці карти. Оскільки ми прагнемо показати список елементів, що базується на основі джерела даних, використаємо директиву v-for з Vue native.

Візуалізація списку карт

У файлі index.html оголошуємо директиву v-for у розмітці, пов'язаній зі створенням елемента карти (наприклад, елемент <div class="card"></div>).

Директива v-for вимагає синтаксис item в масиві items. Оголосимо оператор з аліасом card як item, який циклічно обробляється. Для кожного елемента card ми пов'язуємо значення рангу та масті card до елемента. Тобто наш index.html доповнюється так:

<html>

<head>
  // ...
</head>

<body>
  <div id="app">
    // ...
    <div class="deck">
      <div v-for="card in cards" :key="card.id"
         class="card"
           :class="{ 'black': card.suit === '♠' || card.suit ===  '♣',
             'red': card.suit === '♥' || card.suit ===  '♦' }">
        <span class="card__suit card__suit--top">{{ card.suit }}</span>
        <span class="card__number">{{ card.rank }} </span>
        <span class="card__suit card__suit--bottom">{{ card.suit }}</span>
      </div>
    </div>
  </div>
  
  <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
  <script src="./main.js"></script>
</body>

</html>
Тепер ми посилаємось на нещодавно створений файл main.js у тегу <script>.

Треба звернути увагу на такі пункти:

  • Ми використовуємо синтаксис Mustache, щоб прив'язати значення card.suit та card.rank до шаблону.
  • Щоб визначити унікальність кожного зображуваного елементу карти, прив'язуємо атрибут key до значення card.id для кожного елемента карти. Оскільки використовуються динамічні значення, застосовується скорочений синтаксис директиви v-bind для зв'язування нашого key з card.id.
  • Ми також застосовуємо умовну прив'язку класу до елемента card за допомогою директиви v-bind. Наш умовний клас стверджує, що ми додамо клас .black до елемента зі значенням card.suit ♠ або ♣ Якщо ж card.suit має значення ♦ або ♥, додаємо клас .red.

Хоча умовна прив'язка класу, яку ми встановили вище, працює добре, ми можемо зробити код трохи читабельнішим. Замість того щоб логіка класу card була вказана в шаблоні самостійно, ми можемо ввести властивість data, яка допоможе нам. Представляємо об'єкт suitColor у полі data нашого екземпляру:

new Vue({
  el: '#app',
  data: {
    ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
    suits: ['♥','♦','♠','♣'],
    cards: [],
    suitColor: {
      '♠': 'black',
      '♣': 'black',
      '♦': 'red',
      '♥': 'red',
    },
  },
  created() {
    this.displayInitialDeck();
  },  
  methods: {
    displayInitialDeck() {
      // ...
    },
  },
});

У показаному списку елементів тепер можемо визначити умовну прив'язку класу виразом :class="suitColor[card.suit]":

<html>

<head>
  // ...
</head>

<body>
  <div id="app">
    // ...
    <div class="deck">
      <div v-for="card in cards" :key="card.id"
         class="card"
           :class="suitColor[card.suit]">
        <span class="card__suit card__suit--top">{{ card.suit }}</span>
        <span class="card__number">{{ card.rank }} </span>
        <span class="card__suit card__suit--bottom">{{ card.suit }}</span>
      </div>
    </div>
  </div>
  
  // ...
</body>

</html>

Тепер, запустивши наш застосунок, побачимо усю колоду у початковому стані.

non-shuffle

Застосунок на цьому етапі буде таким:

Тасування Фішера-Єтса

shuffleDeck( )

Наші карти візуалізуються як елементи масиву cards. Правильне перемішування спричиняє зміну елементів у масиві у випадковому порядку. Перестановка елементів у масиві — це просто, однак перестановка у випадковому порядку вимагає деяких зусиль.

Хоча існує багато способів перетасовування, не всі вони є ефективними, якщо необхідна випадковість. Надійним та стандартним методом є алгоритм Фішера-Єтса (також відомий як алгоритм Кнута).

Для ліпшого ознайомлення з тасуванням Фішера-Єтса, перегляньте цю статтю.

Ми не будемо вдаватися у подробиці, а лише оглянемо як організувати тасування Фішера-Єтса. По-перше, створимо метод, що буде відповідати за це і матиме назву shuffleDeck():

new Vue({
  el: '#app',
  data: {
    ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
    suits: ['♥','♦','♠','♣'],
    cards: [],
    suitColor: {
      '♠': 'black',
      '♣': 'black',
      '♦': 'red',
      '♥': 'red',
    },
  },
  created() {
    this.displayInitialDeck();
  },  
  methods: {
    displayInitialDeck() {
      // ...
    },
    shuffleDeck() {},
  },
});

Будемо описувати метод shuffleDeck() крок за кроком. Алгоритм Фішера-Єтса полягає у циклічному переборі та перетасуванні кожного елемента у масиві даних. Для початку створимо цикл for, що пробіжиться кожним елементом масиву cards. Проініціалізуємо лічильник циклу значенням this.cards.length — 1, а далі на кожному кроці будемо виконувати його декремент, доки не буде досягнуто 0:

shuffleDeck() {
  for(let i = this.cards.length - 1; i > 0; i--) {
	
  }
}

Спочатку у перетасуванні буде залучено випадкові елементи. Цього можна досягнути, генеруючи випадкове число між 0 та довжиною ітерованого масиву за допомогою Math.floor(Math.random() * i). Ми отримаємо випадковий індекс у діапазоні між 0 та довжиною масиву, що залишилася для тасування. Присвоїмо це випадкове число змінній randomIndex:

shuffleDeck() {
  for(let i = this.cards.length - 1; i > 0; i--) {
    let randomIndex = Math.floor(Math.random() * i);
  }
}

Далі звертаємось до елемента в масиві, який обробляємо у циклі (тобто поточний елемент cards), та обміняємо значення цього елемента зі значенням елемента, отриманим за індексом randomIndex. Щоб реалізувати такий обмін:

  • Присвоюємо змінній temp значення поточного елемента cards.
  • Присвоюємо елементу масиву з поточним індексом значення елемента з randomIndex.
  • Присвоюємо елементу з randomIndex значення змінної temp, яка насправді містить значення елемента масиву з поточним індексом.

Спробуємо реалізувати викладене у коді:

shuffleDeck() {
  for(let i = this.cards.length - 1; i > 0; i--) {
    let randomIndex = Math.floor(Math.random() * i);
    
    let temp = this.cards[i];
    this.cards[i] = this.cards[randomIndex];
    this.cards[randomIndex] = temp;
  }
}

Коли справа доходить до оновлення користувацького інтерфейсу з впровадженими змінами, такий спосіб не спрацює. Причиною є те, що Vue не в змозі підхопити зміни, які ми зробили, коли прямо встановлювали значення елементу масиву або змінювали його довжину. Аби мати можливість прямо оновлювати значення елементу даних масиву, нам необхідно застосувати метод Vue.set().

Vue.set() приймає три аргументи:

  1. масив, що оновлюється;
  2. значення індексу елементу, що оновлюватиметься;
  3. нове значення.

Наприклад, можна замінити перше значення в масиві suits на 🦆 ось так:

Vue.set(this.suits, 0, 🦆);

У нашому методі shuffleDeck() ми використаємо Vue.set() два рази для того, щоб здійснити обмін необхідних елементів. Доповнений метод shuffleDeck() буде таким:

new Vue({
  el: '#app',
  data: {
    ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
    suits: ['♥','♦','♠','♣'],
    cards: [],
    suitColor: {
      '♠': 'black',
      '♣': 'black',
      '♦': 'red',
      '♥': 'red',
    },
  },
  created() {
    this.displayInitialDeck();
  },  
  methods: {
    displayInitialDeck() {
      // ...
    },
    shuffleDeck() {        
      for(let i = this.cards.length - 1; i > 0; i--) {
        let randomIndex = Math.floor(Math.random() * i);
        
        let temp = this.cards[i];
        Vue.set(this.cards, i, this.cards[randomIndex]);
        Vue.set(this.cards, randomIndex, temp);
      }
    },
  },
* });

Перший метод Vue.set() змінює значення елемента cards поточного індексу на значення елемента з randomIndex. Наступний Vue.set() змінює значення елемента з індексом randomIndex на значення змінної temp.

На цьому все! Тепер можемо створити заклик до дії, що запустить метод shuffleDeck().

Однією з важливих нововведень у Vue 3.0 буде можливість прямого оновлення значень елементів масиву без використання Vue.set().
Метод shuffle з бібліотеки Lodash реалізовує версію алгоритму тасування Фішера-Єтса.

Кнопка тасування

Створимо кнопку для виклику тасування безпосередньо над колодою карт. У нашій HTML-розмітці, ми розташуємо цей елемент у тегу <div class="main-buttons"></div>:

<html>

<head>
  // ...
</head>

<body>
  <div id="app">
    <h1 class="title">
      // ...
    </h1>
    <div class="main-buttons">
      <button @click="shuffleDeck" class="button is-primary">
        Shuffle <i class="fas fa-random"></i>
      </button>
    </div>
    <div class="deck">
      // ...
    </div>
  </div>

  // ...
</body>

</html>

Оголосимо слухача подій на цю кнопку за допомогою @click="shuffleDeck". Тобто встановлюється обробник події натискання, що викликає метод shuffleDeck(). Натиснувши на кнопку Shuffle, запускаємо застосунок.

@clickскорочена версія директиви v-on:click.

shuffle

Чудово! З кожним кліком можна помітити, як карти з колоди перемішуються. Хоч карти змінюють порядок, перезавантаження під час кожного тасування відбувається миттєво. Щоб спостерігати за процесом зміни місць, можна застосувати переходи Vue.

Застосунок на цьому етапі:

Переходи

transition-group

Vue пропонує нам декілька різних способів для введення переходів, як от: переходи єдиного вузла, переходи декількох вузлів, де кожної миті показується лише один, та переходи списку елементів. Наша колода карт використовує директиву v-for для візуалізації списку елементів, тому ми будемо застосовувати List Move Transition, щоб досягти анімації при зміні позиції кожної карти.

Для досягнення цього ми замінимо елемент <div class="decks"></div> елементом transition-group, що виступає у якості обгортки для списку v-for (тобто колоди карт). Оголосивши transition-group, ми зв'язуємо назву переходу shuffleSpeed (тобто :name="shuffleSpeed") та структуру, яку група переходу повинна показати як елемент div (тобто tag="div"):

<html>

<head>
  // ...
</head>

<body>
  <div id="app">
    // ...
    <transition-group :name="shuffleSpeed" tag="div" class="deck">
      <div v-for="card in cards" :key="card.id"
         class="card"
           :class="suitColor[card.suit]">
        <span class="card__suit card__suit--top">{{ card.suit }}</span>
        <span class="card__number">{{ card.rank }} </span>
        <span class="card__suit card__suit--bottom">{{ card.suit }}</span>
      </div>
    </transition-group>
  </div>
  
  // ...
</body>

</html>

У нашому екземплярі Vue ми оголосили властивість shuffleSpeed та встановили за значення рядок shuffleMedium. Так ми можемо контролювати швидкість переходів, що побачимо незабаром.

new Vue({
  el: '#app',
  data: {
    ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
    suits: ['♥','♦','♠','♣'],
    cards: [],
    suitColor: {
      '♠': 'black',
      '♣': 'black',
      '♦': 'red',
      '♥': 'red',
    },
    shuffleSpeed: 'shuffleMedium',
  },
  created() {
    this.displayInitialDeck();
  },  
  methods: {
    displayInitialDeck() {
      // ...
    },
    shuffleDeck() {
      // ...
    },
  },
});

Коли застосунок завантажується, transition-group, що огортає колоду карт, матиме атрибут зі значенням shuffleMedium. На основі назви переходу Vue автоматично розпізнає, чи були задані певні CSS-переходи/анімації. Оскільки ми прагнемо викликати ефект переходу при переміщенні елементів списку, Vue буде шукати зазначений CSS-перехід на рядку shuffleMedium-move (де shuffleMedium — ім'я нашої групи переходу).

У файлі styles.css нашого застосунку, зараз вже існують три класи: shuffleSlow-move, shuffleMedium-move та shuffleFast-move, кожен з яких відповідальний у впровадженні CSS-переходу. Усі класи мають однаковий тип, але різну тривалість переходу.

// Переходи
.shuffleSlow-move {
  transition: transform 2s;
}
.shuffleMedium-move {
  transition: transform 1s;
}
.shuffleFast-move {
  transition: transform 0.5s;

З уже доданими CSS-переходами наші карти тепер будуть виконувати переміщення, коли запущено тасування.

transition

Коли ми натискаємо кнопку Shuffle, вірогідність отримання певного порядку карт унікальна, адже число можливих перестановок становить 52 факторіал, неймовірно велике число. У відео Vsauce усе пояснюється докладніше.

Тепер нам залишається дозволити користувачеві вказувати, з якою швидкістю виконувати тасування.

Швидкість перестановок

Елемент transition-group спочатку отримує значення для shuffleSpeed (ініціалізовано як shuffleMedium). Аби користувач зміг змінювати швидкість переходу, ми, по суті, повинні дозволити зміну значення shuffleSpeed.

По-перше, створимо три кнопки з назвами Slow, Medium, та Fast. Розташуємо їх всередині елементу <div class="speed-buttons"></div>, що одразу над кнопкою Shuffle.

<html>

<head>
  // ...
</head>

<body>
  <div id="app">
    <h1 class="title">
      // ...
    </h1>
    <div class="speed-buttons">
      <button class="button is-small"
         :class="{ 'is-light': shuffleSpeed != 'shuffleSlow' }"
         @click="shuffleSpeed = 'shuffleSlow'">
        Slow
      </button>
      <button class="button is-small"
         :class="{ 'is-light': shuffleSpeed != 'shuffleMedium' }"
         @click="shuffleSpeed = 'shuffleMedium'">
        Medium
      </button>
      <button class="button is-small"
         :class="{ 'is-light': shuffleSpeed != 'shuffleFast' }"
         @click="shuffleSpeed = 'shuffleFast'">
        Fast
      </button>
    </div>
    <div class="main-buttons">
      // ...
    </div>
    <div class="deck">
      // ...
    </div>
  </div>

  // ...
</body>

</html>

У кожній з представлених кнопок ми визначили умовну прив'язку класу для того, щоб умовно додати клас .is-light, зважаючи на значення shuffleSpeed. .is-light є класом Bulma, що додає сірий фон до кнопки. Ми просто встановили наші умови, аби вказати, що клас .is-light треба додати до неактивних кнопок.

is-light

Кожен елемент кнопки також має слухача подій, який встановлює значення shuffleSpeed коректно (наприклад, натиснувши Slow, ви встановлюєте shuffleSpeed як shuffleSlow). З різними підготовленими CSS-переходами ми маємо змогу контролювати швидкість тасування:

speed

Оскільки усі елементи кнопок дуже подібні, ми можемо зменшити повторення у нашій HTML-розмітці. Для цього створимо масив shuffleTypes в екземплярі Vue, який містить усі можливі типи тасування:

new Vue({
  el: '#app',
  data: {
    ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
    suits: ['♥','♦','♠','♣'],
    cards: [],
    suitColor: {
      '♠': 'black',
      '♣': 'black',
      '♦': 'red',
      '♥': 'red',
    },
    shuffleSpeed: 'shuffleMedium',
    shuffleTypes: ['Slow', 'Medium', 'Fast'],
  },
  created() {
    this.displayInitialDeck();
  },  
  methods: {
    displayInitialDeck() {
      // ...
    },
    shuffleDeck() {
      // ...
    },
  },
});

У нашому HTML ми можемо використовувати директиву v-for для перебору кожного типу тасування та прив'язки відповідної інформації:

<html>

<head>
  // ...
</head>

<body>
  <div id="app">
    <h1 class="title">
      // ...
    </h1>
    <div class="speed-buttons">
      <button v-for="type in shuffleTypes"
         class="button is-small"
           :class="{ 'is-light': shuffleSpeed != `shuffle${type}` }"
              @click="shuffleSpeed = `shuffle${type}`">
        {{ type }}
      </button>
    </div>
    <div class="main-buttons">
      // ...
    </div>
    <div class="deck">
      // ...
    </div>
  </div>

  // ...
</body>

</html>

Так наша розмітка стає чистішою при збереженні усієї інформації.

Ми майже завершили застосунок! Наостанок додамо кнопку Reset, аби користувач мав змогу скидати перетасовану колоду до первинного стану, та лічильник для підрахунку кількості здійснених тасувань.

Reset & Лічильник

Кнопка Reset

Щоб мати можливість повернути колоду карт до первинного стану, реалізуємо кнопку Reset поруч з кнопкою Shuffle.

<html>

<head>
  // ...
</head>

<body>
  <div id="app">
    <h1 class="title">
      // ...
    </h1>
    <div class="main-buttons">
      <button v-if="isDeckShuffled" @click="displayInitialDeck" class="button is-primary is-outlined">
        Reset <i class="fas fa-undo"></i>
      </button>
      <button @click="shuffleDeck" class="button is-primary">
        Shuffle <i class="fas fa-random"></i>
      </button>
    </div>
    <div class="deck">
      // ...
    </div>
  </div>

  // ...
</body>

</html>

У нещодавно доданій кнопці Reset ми оголосили v-if="isDeckShuffled" для елемента. v-if є директивою, яка умовно показує елемент на основі істинності зазначеного виразу. Так ми стверджуємо, що кнопка Reset повинна показуватись, коли властивість isDeckShuffled правдива. Ми використовуватимемо цю властивість лише для того, щоб кнопка Reset з'являлася лише тоді, коли колоду було перетасовано (тобто коли колода не в початковому стані).

Оголосимо властивість isDeckShuffled для нашого екземпляра та проініціалізуємо її значенням false. У кінці методу shuffleDeck(), значення isDeckShuffled зміниться на true, щоб позначити, що наша колода була перетасована. У методі displayInitialDeck() ми скинемо властивість isDeckShuffled до значення false, оскільки в цьому стані колода більше не перемішується.

new Vue({
  el: '#app',
  data: {
    ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
    suits: ['♥','♦','♠','♣'],
    cards: [],
    suitColor: {
      '♠': 'black',
      '♣': 'black',
      '♦': 'red',
      '♥': 'red',
    },
    shuffleSpeed: 'shuffleMedium',
    shuffleTypes: ['Slow', 'Medium', 'Fast'],
    isDeckShuffled: false,
  },
  created() {
    this.displayInitialDeck();
  },  
  methods: {
    displayInitialDeck() {
      let id = 1;
      this.cards = [];

      for( let s = 0; s < this.suits.length; s++ ) {
        for( let r = 0; r < this.ranks.length; r++ ) {
          let card = {
            id: id,
            rank: this.ranks[r],
            suit: this.suits[s]
          }
          this.cards.push(card);
          id++;
        }
      }

      this.isDeckShuffled = false;
    },
    shuffleDeck() {        
      for(let i = this.cards.length - 1; i > 0; i--) {
        let randomIndex = Math.floor(Math.random() * i);
        
        let temp = this.cards[i];
        Vue.set(this.cards, i, this.cards[randomIndex]);
        Vue.set(this.cards, randomIndex, temp);
      }

      this.isDeckShuffled = true;
    }
  },
});

Тож тепер маємо змогу скинути перетасовану колоду до первинного стану. Нам залишилось додати лічильник для підрахунку здійснених тасувань.

Послідовні тасування

Введемо властивість shuffleCount, відповідальну за відстеження кількості послідовних тасувань. Проініціалізуємо її значенням 0. Коли тасування проводиться (тобто викликається shuffleDeck()), ми інкрементуємо shuffleCount. Коли ж колода повертається у первинний стан (викликається displayInitialDeck()), повертаємо значення shuffleCount до 0.

Тепер наш екземпляр Vue буде таким:

new Vue({
  el: '#app',
  data: {
    ranks: ['A', '2', '3', '4', '5', '6', '7', '8', '9', '10', 'J', 'Q', 'K'],
    suits: ['♥','♦','♠','♣'],
    cards: [],
    suitColor: {
      '♠': 'black',
      '♣': 'black',
      '♦': 'red',
      '♥': 'red',
    },
    shuffleSpeed: 'shuffleMedium',
    shuffleTypes: ['Slow', 'Medium', 'Fast'],
    isDeckShuffled: false,
    shuffleCount: 0,
  },
  created() {
    this.displayInitialDeck();
  },  
  methods: {
    displayInitialDeck() {
      let id = 1;
      this.cards = [];

      for( let s = 0; s < this.suits.length; s++ ) {
        for( let r = 0; r < this.ranks.length; r++ ) {
          let card = {
            id: id,
            rank: this.ranks[r],
            suit: this.suits[s]
          }
          this.cards.push(card);
          id++;
        }
      }

      this.isDeckShuffled = false;
      this.shuffleCount = 0;
    },
    shuffleDeck() {        
      for(let i = this.cards.length - 1; i > 0; i--) {
        let randomIndex = Math.floor(Math.random() * i);
        
        let temp = this.cards[i];
        Vue.set(this.cards, i, this.cards[randomIndex]);
        Vue.set(this.cards, randomIndex, temp);
      }

      this.isDeckShuffled = true;
      this.shuffleCount = this.shuffleCount + 1;
    }
  },
});

Додамо елемент <div class="count-section"></div> у початок нашої розмітки. Цей елемент буде розташований у верхньому правому кутку інтерфейсу та буде зв'язаний зі значенням shuffleCount, коли ми вкажемо # of Shuffles: {{ shuffleCount }}.

Наш користувацький інтерфейс (елемент <div id="app"></div>) у повному обсязі:

<html>

<head>
  // ...
</head>

<body>
  <div id="app">
    <div class="count-section">
      # of Shuffles: {{ shuffleCount }}
    </div>
    <h1 class="title">
      <img class="vue-logo" src="https://vuejs.org/images/logo.png" />
      Card Shuffling
    </h1>
    <div class="speed-buttons">
      <button v-for="type in shuffleTypes"
        class="button is-small"
          :class="{ 'is-light': shuffleSpeed != `shuffle${type}` }"
           @click="shuffleSpeed = `shuffle${type}`">
        {{ type }}
      </button>
    </div>
    <div class="main-buttons">
      <button v-if="isDeckShuffled" @click="displayInitialDeck" class="button is-primary is-outlined">
        Reset <i class="fas fa-undo"></i>
      </button>
      <button @click="shuffleDeck" class="button is-primary">
        Shuffle <i class="fas fa-random"></i>
      </button>
    </div>
    <transition-group :name="shuffleSpeed" tag="div" class="deck">
      <div v-for="card in cards" :key="card.id"
         class="card"
           :class="suitColor[card.suit]">
        <span class="card__suit card__suit--top">{{ card.suit }}</span>
        <span class="card__number">{{ card.rank }} </span>
        <span class="card__suit card__suit--bottom">{{ card.suit }}</span>
      </div>
    </transition-group>
  </div>
  
  // ...
</body>
  
</html>

Ми завершили застосунок! З останніми змінами ми можемо відстежувати кількість послідовних тасувань та скидати перетасовану колоду до початкового стану.

finish

Завершений застосунок:

Висновок

Не звертаючись до деталей реалізації стилів, ми розглянули створення користувацького інтерфейсу колоди карт та використали алгоритм Фішера-Єтса.

Повний застосунок, разом з незавершеними версіями, які ми створювали протягом статті, можна знайти у GitHub-репозиторії awesome-fullstack-tutorials.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація