Недавно я наткнулся на статью Сандры Исраэль о том, как она создала игру сопоставления памяти для своего проекта 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()
работает следующим образом, как она объяснила:
- Добавьте открытые карты в массив под названием
openedCards
- Если в массиве две карты, то есть
openCards.length == 2
, он проверяет, совпадают ли они, сравнивая свойствоtype
двух карт с помощью оператораif-else
. - Если
type
properties совпадают или равны, т.е.openedCards[0].type === openedCards[1].type
, вызывается функцияmatched()
. Эта функция добавляет классmatch
в список классов каждой карты, удаляет классыshow
иopen
, помещает две карты в массив с именемmatchedCards
и очищаетopenedCards.
- Если свойства
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(); }
Дополнительные вещи, которые я добавил ради забавы
- Я также пытался поработать над своими навыками 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 (надеюсь, у меня все получилось неплохо 😊).