Недавно я наткнулся на статью Сандры Исраэль о том, как она создала игру сопоставления памяти для своего проекта FEND (найдена здесь), и мне показалось, что это было весело, поэтому я решил продолжить и повторить кое-что. нравится это. I Однако у меня был поворот. Я хотел, чтобы приложение было как можно более легким - без адаптивных фреймворков, без библиотек JavaScript, без специальных библиотек шрифтов значков (я люблю ваш шрифт потрясающий, но…) - только HTML, CSS и ванильный JavaScript, а также поработаю над своими навыками манипулирования DOM.

Так как я это сделал? Что я сделал иначе? Давайте углубимся.

Создание структуры игрового поля.

Как указала Сандра в своей статье, для начала у нее была папка с начальными файлами. У меня этого не было, поэтому пришлось сделать свою. Я начал с создания игрового поля с сеткой 4 x 4 для хранения 16 карт, используя элемент html <table>. Я создал таблицу с 4 строками и 4 столбцами. Затем я стилизовал игровое поле так, чтобы каждая ячейка имела черный фон. Я не хотел использовать значки для моей игры, как в ее учебнике, поэтому я использовал элемент изображения с 8 изображениями (каждое повторяется один раз, чтобы сделать 16 изображений) в каждой ячейке следующим образом: <td class='game-card><img src=”img/1.jpg” class='game-card-img' alt="cake1"></td>и поместите изображения в ячейку с помощью атрибута CSS object-fit:cover . Итак, после стилизации моя доска выглядела примерно так:

Затем я установил видимость изображений на скрытую, что превратило игровое поле в сетку с 16 черными ячейками.

Отображение изображений карточек при нажатии каждой карточки

Итак, сейчас похоже, что изображения находятся за карточкой и будут переворачиваться, чтобы отобразить изображения при нажатии. Как я реализовал это событие щелчка? Сначала я собрал все карты, то есть все <td class=”game-card”>, и сохранил их в переменной таким образом: let cardElements = document.getElementByClassName(‘game-card’). Затем я использовал цикл for, чтобы перебрать их и добавить прослушиватель событий щелчка.

for(let i = 0; i < cardElementsArray.length; i++) {
cardElementsArray[i].addEventListener("click", displayCard)
}

displayCard - это функция, которая будет вызываться при щелчке по карте. Он почти такой же, как у нее, но с небольшой разницей. Поскольку я использовал изображения вместо значков, мне пришлось настроить таргетинг и на эти изображения. Итак, я добавил эту строку: this.children[0].classList.toggle(‘show-img’). show-img устанавливает видимость изображения как видимое.

Перемешивание карт

Игра требует, чтобы карты перетасовывались при загрузке страницы (при запуске игры) или при перезапуске. Поскольку я использовал изображения вместо значков, то, как я это получил, немного отличался от примера. Во-первых, я создал массив с именем cardElementsArray со всеми картами, которые я собрал, поскольку cardElements был nodeList, не обязательно массивом. Я воспользовался оператором распространения [...]. Затем я собрал все элементы изображения, как раньше: let imgElements = document.getElementsByClassName('game-card-img'), а также сделал массив let imgElementsArray = [...imgElements].

Функция перемешивания под названием Fisher-Yates (aka Knuth) Shuffle уже была предоставлена. Затем я создал функцию startGame() следующим образом:

function startGame() {
//shuffle cards using the Fisher-Yates (aka Knuth) Shuffle function
let shuffledImages = shuffle(imgElementsArray);
for(i=0; i<shuffledImages.length; i++) {
//add the shuffled images to each card
cardElements[i].appendChild(shuffledImages[i]);
}
}

В основном это делается: создается массив shuffledImages, затем проходит цикл по массиву и добавляется каждое изображение в карточку. Затем я вызываю функцию startGame() при загрузке страницы, то есть window.onload = startGame().

Соответствующие карты

На этом этапе каждая карта должна быть уникальной. Поскольку у каждой карты были разные изображения, я дал каждому объекту карты свойство type, которое соответствует тексту alt каждого изображения, чтобы отличать карту от других. Итак, в моей функции startGame () после строки, которая добавляет каждое изображение к каждой карточке, я добавил эту строку: cardElements[i].type = `${shuffledImages[i].alt}`; Затем я создал свои функции cardOpen(), matched(), unmatched(), disable() и enable(), как она.

Функция cardOpen() должна запускаться при каждом щелчке по карте точно так же, как функция displayCard(). Однако, поскольку я добавлял изображения к каждой карточке, то есть элемент <td>, вместо того, чтобы просто отображать значки и увеличивать их, как в учебнике, я не мог создать прослушиватели событий щелчка и запускать обе функции в этом событии. Примерно так: cardElement[i].addEventlistener('click', function(e){ displayCard(); cardOpen(); }). Я также не мог создать два отдельных прослушивателя событий щелчка для запуска каждой функции.

Почему? Событие щелчка нацелено на карточку, и когда эта карточка нацелена, элемент изображения становится видимым. Повторный щелчок по этой карточке больше не нацелен на элемент card <td>, а на элемент <img>, поэтому переключение классов в функции displayCard() возвращает ошибку.

Как я решил эту проблему? Я решил вызвать функцию cardOpen() в последней строке функции displayCard(). Функция cardOpen() работает следующим образом, как она объяснила:

  1. Добавьте открытые карты в массив под названием openedCards
  2. Если в массиве две карты, то есть openCards.length == 2, он проверяет, совпадают ли они, сравнивая свойство type двух карт с помощью оператора if-else.
  3. Если type properties совпадают или равны, т.е. openedCards[0].type === openedCards[1].type, вызывается функция matched(). Эта функция добавляет класс match в список классов каждой карты, удаляет классы show и open, помещает две карты в массив с именем matchedCards и очищает openedCards.
  4. Если свойства type не совпадают, вызывается функция unmatched(). Функция добавляет класс unmatched к каждой карте, временно отключает нажатие на карту с помощью функции disable(), устанавливая время выхода 1100 мс, по истечении которого классы show, open и unmatched удаляются, изображения становятся невидимыми, затем карты становятся включен с помощью функции enable(). Наконец, массив openedCards очищается.

Подсчет и отображение количества ходов игрока

Я сделал то же самое, что и она в учебнике: вызвал функцию moveCounter(), которая увеличивает количество ходов, сделанных игроком, когда были выбраны две карты, а затем устанавливает innerHTML моего элемента счетчика на это значение.

Отображение звездного рейтинга, отражающего результативность игрока в зависимости от количества сделанных ходов.

Для своих значков звездочки я использовал родительский элемент div и дал ему имя класса rating, затем я создал 5 элементов span с текстом значка звездочка 🌟 с alt-codes.net (помните, я сказал, что не хочу использовать какой-либо шрифт библиотеки) и присвоил каждой из них класс star. Так что моя рейтинговая система должна была сильно отличаться от ее. Я собрал все .star элемента в массив под названием starElementsArray, как и для изображений и карточек. Затем я изменил свою moveCounter() функцию; уменьшение непрозрачности вместо удаления видимости, как в учебнике:

function moveCounter() {
moves++;
counter.innerHTML = `${moves} move(s)`;
//setting rating based on moves
if(moves > 8 && moves <= 12) {
for(let i=0; i<5; i++) {
starElementsArray[i].opacity = 1;
}
} else if(moves > 12 && moves <= 16) {
for(let i=0; i<5; i++) {
if(i > 3) {
starElementsArray[i].style.opacity = 0.1;
}
}
} else if(moves > 16 && moves <= 20) {
for(let i=0; i<5; i++) {
if(i > 2) {
starElementsArray[i].style.opacity = 0.1;
}
}
} else if(moves > 20 && moves <= 24) {
for(let i=0; i<5; i++) {
if(i > 1) {
starElementsArray[i].style.opacity = 0.1;
}
}
} else if(moves > 24){
for(let i=0; i<5; i++) {
if(i > 0) {
starElementsArray[i].style.opacity = 0.1;
}
}
}
}

Установка и запуск таймера

Здесь особо не на что смотреть. То же, что и в учебнике.

Перезапуск игры

Я предоставил две кнопки перезапуска для обновления или перезапуска игры; один в строке состояния игры - рядом со звездным рейтингом, счетчиком ходов и таймером - и один под игровым полем. При нажатии любой из этих кнопок вызывается измененная функция startGame(). Он удаляет все дополнительные классы, удаляет изображения, сбрасывает ходы на 0, сбрасывает таймер и восстанавливает рейтинг до 5 звезд с непрозрачностью 1. Слушатель событий для каждого щелчка карты теперь также находится в функции startGame().

function startGame() {
//shuffle cards
let shuffledImages = shuffle(imgElementsArray);
for(i=0; i<shuffledImages.length; i++) {
//remove all images from previous games from each card (if any)
cardElements[i].innerHTML = "";
//add the shuffled images to each card
cardElements[i].appendChild(shuffledImages[i]);
cardElements[i].type = `${shuffledImages[i].alt}`;
//remove all extra classes for new game play
cardElements[i].classList.remove("show", "open", "match", "disabled");
cardElements[i].children[0].classList.remove("show-img");
}
//listen for events on the cards
for(let i = 0; i < cardElementsArray.length; i++) {
cardElementsArray[i].addEventListener("click", displayCard)
}
//reset moves
moves = 0;
counter.innerText = `${moves} move(s)`;
//reset star rating
for(let i=0; i<starElementsArray.length; i++) {
starElementsArray[i].style.opacity = 1;
}
//Reset Timer on game reset
timer.innerHTML = '0 mins 0 secs';
clearInterval(interval);
}

Отображение модального окна по окончании игры

Когда все карты были правильно сопоставлены; игра заканчивается, и должно появиться модальное окно, предупреждающее пользователя. В моем собственном случае; в функции matched(), когда matchedCards.length == 16 я вызываю функцию endGame(). Это означает, что он очищает интервал, использованный для создания таймера, извлекает значение счетчика ходов, таймера и рейтинга, а затем показывает модальное окно с этими деталями. Он показывает кнопку playAgain, которая вызывает playAgain()функцию, закрывающую модальное окно и обновляющую игру.

Он также очищает массив matchedCards, чтобы при перезапуске / обновлении игры без перезагрузки страницы совпавшие карты из предыдущих игр не оставались там. Это предотвращает отключение в игре всех ячеек на доске при нажатии двух карт. Также есть функция closeModal(), которая вызывается при нажатии кнопки закрытия X, чтобы закрыть модальное окно.

function endGame() {
clearInterval(interval);
totalGameTime = timer.innerHTML;
starRating = document.querySelector('.rating').innerHTML;
//show modal on game end
modalElement.classList.add("show-modal");
//show totalGameTime, moves and finalStarRating in Modal
totalGameTimeElement.innerHTML = totalGameTime;
totalGameMovesElement.innerHTML = moves;
finalStarRatingElement.innerHTML = starRating;
matchedCards = [];
closeModal();
}

Дополнительные вещи, которые я добавил ради забавы

  1. Я также пытался поработать над своими навыками CSS-анимации, поэтому я добавил CSS-анимацию, чтобы при отображении изображений было что-то вроде эффекта переворота.
.show-img {
visibility: visible;
animation: animateShowImage 0.4s linear alternate;
}
@keyframes animateShowImage {
0% { transform: rotateY(90deg); opacity: 0;}
100%{ transform: rotateY(0); opacity: 1; }
}

2. Я оживил свое сообщение в модальном окне. 😃 Поздравляю, смайлики 🎊 и 🎉, а также модальный значок закрытия были переведены в отскок. Вот несколько ресурсов, из которых вы можете получить эти значки без специальной библиотеки значков: altcodeunicode, alt-коды и emojipedia. Просто скопируйте значки с сайта как есть и вставьте их в свой редактор.

Однако одно предостережение: перед использованием убедитесь, что они видны поперек и не показывают что-то подобное 🥰 в веб-браузере.

3. Я хотел, чтобы все карты мигали сразу после загрузки страницы, поэтому я создал функцию с именем flashCards() и сразу после прослушивателя событий щелчка для каждой карты в startGame(). Затем вместо вызова startGame() немедленной загрузки страницы я задержал ее на 1200 мс, используя setTimeOut(), чтобы пользователь мог видеть, когда карты мигают в начале игры.

function flashCards() {
for(i=0; i<cardElements.length; i++) {
cardElements[i].children[0].classList.add("show-img")
}
setTimeout(function(){
for(i=0; i<cardElements.length; i++) {
cardElements[i].children[0].classList.remove("show-img")
}
}, 1000)
}

// wait for some milliseconds before game starts
window.onload = function () {
setTimeout(function() {
startGame()
}, 1200);
}

4. Я добавил модальное окно справки, содержащее инструкции о том, как играть в игру. Доступ к нему можно получить, щелкнув значок? в правом верхнем углу страницы. Я искал значок с вопросительным знаком в виде круга для этой кнопки, но я не нашел его, поэтому сделал свой, используя svg. Пример можно найти здесь.

5. Я добавил медиа-запросы для веб-отзывчивости страницы.

Заключение.

Мне очень понравилось работать над этим в выходные, несмотря на то, что я был немного не в себе. Я узнал довольно много нового, что могу применить к другим проектам, и я смог вспомнить некоторые другие вещи, которые я узнал из предыдущих курсов, которые я прошел. Если вы хотите увидеть, как выглядит демоверсия моей игры, вы можете нажать здесь. Файлы игры хранятся на моем гитхабе.

Могу ли я добавить какие-нибудь дополнения или функции? Есть ли способ улучшить свой код? Пожалуйста, дайте мне знать в комментариях.

P.S Это моя первая техническая статья о Medium (надеюсь, у меня все получилось неплохо 😊).