I created a card matching game with vanilla JS and encountered a bug that I'm struggling to figure out the cause of.
If you flip one card then click the reset icon, the flipped card is turned back over. But when you try flipping one card after having reset the game, a second card, which doesn't have an icon, is flipped by itself.
In my code, the class open changes the card color and triggers an animation, while the class show (when applied to a card--in other cases it's applied to a modal to set it to visible when the game has been won) displays the Font Awesome icon, so I know the class open is being added for the second card that is flipped by itself.
Here's the most relevant JS--I also provide the full JS below. For the CSS and HTML, please view my CodePen.
// Calls startGame() function with user clicks restart icon
restartButton.addEventListener('click', startGame);
function startGame() {
// Shuffles deck
cards = shuffle(cards);
// Removes any existing classes from each card
for (let i = 0; i < cards.length; i++) {
deck.innerHTML = '';
// Empty array literal is being used as a shortcut to expanded version, Array.prototype. getElementsByClassName method was used to create cards variable. Since getElementsByClassName returns an "array-like" like object rather than an array, Array.prototype/[] is needed it use array methods on element(s) selected with it.
[].forEach.call(cards, function(item) {
deck.appendChild(item);
});
// Class 'open' changes the card color and triggers an animation, while 'show' (when applied to a card; in other cases it is applied to the modal) displays the Font Awesome icon
cards[i].classList.remove('show', 'open', 'matching', 'disabled');
}
// Resets number of moves
moves = 0;
counter.innerHTML = moves;
// Resets star rating
for (let i = 0; i < stars.length; i++) {
stars[i].style.color = '#ffd700';
// When function moveCounter() is called, stars is set to display: none after a certain number of moves. (visibility: collapse was original method used to hide stars, but this prevented proper centering of stars in modal)
stars[i].style.display = 'inline';
}
// Resets timer
let second = 0;
let minute = 0;
let hour = 0;
let timer = document.querySelector('.timer');
timer.innerHTML = '0 mins 0 secs';
// Window method that stops setInterval() Window method from executing "myTimer" function every 1 second
clearInterval(interval);
}
Full JavaScript:
let card = document.getElementsByClassName('card');
// Spread operator (new in ES6) allows iterable to expand where 0+ arguments are expected
let cards = [...card];
console.log(cards);
// getElementsByClassName method returns HTMLCollection (or a NodeList for some older browsers https://www.w3schools.com/js/js_htmldom_nodelist.asp), an array-like object on which you can use Array.prototype methods. Added [0] to get the first element matched
let deck = document.getElementsByClassName('card-deck')[0];
let moves = 0;
let counter = document.querySelector('.moves');
// Const cannot be used here in order for star rating to be reset when startGame() is called
let stars = document.querySelectorAll('.fa-star');
let starsList = document.querySelectorAll('.stars li');
let matchingCard = document.getElementsByClassName('matching');
let closeIcon = document.querySelector('.close');
// Using getElementsByClassName instead of querySelector here (there's only one class to select) because querySelector is non-live, i.e., it doesn't reflect DOM manipulation. When the user wins the game, a class ("show") is added to the element with class modal, which is set to visible in CSS, so getElementsByClassName is needed (otherwise the modal remains hidden when the game has been won)
let modal = document.getElementsByClassName('modal')[0];
let openedCards = [];
let second = 0, minute = 0, hour = 0;
let timer = document.querySelector('.timer');
let interval;
const restartButton = document.querySelector('.restart');
const modalPlayAgainButton = document.querySelector('.play-again');
// Shuffle function from http://stackoverflow.com/a/2450976
function shuffle(array) {
let currentIndex = array.length, temporaryValue, randomIndex;
while (currentIndex !== 0) {
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1;
temporaryValue = array[currentIndex];
array[currentIndex] = array[randomIndex];
array[randomIndex] = temporaryValue;
}
return array;
}
// Shuffles cards upon page load
document.body.onload = startGame();
// Calls startGame() function with user clicks restart icon
restartButton.addEventListener('click', startGame);
// Calls reset() function (hides modal and restarts game) with user clicks "play again" button in modal
modalPlayAgainButton.addEventListener('click', reset);
function startGame() {
// Shuffles deck
cards = shuffle(cards);
// Removes any existing classes from each card
for (let i = 0; i < cards.length; i++) {
deck.innerHTML = '';
// Empty array literal is being used as a shortcut to expanded version, Array.prototype. getElementsByClassName method was used to create cards variable. Since getElementsByClassName returns an "array-like" like object rather than an array, Array.prototype/[] is needed it use array methods on element(s) selected with it.
[].forEach.call(cards, function(item) {
deck.appendChild(item);
});
// Class 'open' changes the card color and triggers an animation, while 'show' (when applied to a card; in other cases it is applied to the modal) displays the Font Awesome icon
cards[i].classList.remove('show', 'open', 'matching', 'disabled');
}
// Resets number of moves
moves = 0;
counter.innerHTML = moves;
// Resets star rating
for (let i = 0; i < stars.length; i++) {
stars[i].style.color = '#ffd700';
// When function moveCounter() is called, stars is set to display: none after a certain number of moves. (visibility: collapse was original method used to hide stars, but this prevented proper centering of stars in modal)
stars[i].style.display = 'inline';
}
// Resets timer
let second = 0;
let minute = 0;
let hour = 0;
let timer = document.querySelector('.timer');
timer.innerHTML = '0 mins 0 secs';
// Window method that stops setInterval() Window method from executing "myTimer" function every 1 second
clearInterval(interval);
}
// When called, function toggles open and show classes to display cards. Class 'open' changes the card color and triggers an animation, while 'show' (when applied to a card; in other cases it is applied to the modal) displays the Font Awesome icon.
let displayCard = function() {
this.classList.toggle('open');
this.classList.toggle('show');
this.classList.toggle('disabled');
};
// Adds flipped cards to openedCards array, calls the counter function if two have been flipped, and checks if cards are a match or not
function cardOpen() {
openedCards.push(this);
let len = openedCards.length;
if (len === 2) {
moveCounter();
if (openedCards[0].type === openedCards[1].type) {
matching();
} else {
notMatching();
}
}
}
// When cards match, adds/removes relevant classes and clears the two cards' arrays
function matching() {
openedCards[0].classList.add('matching', 'disabled');
openedCards[1].classList.add('matching', 'disabled');
openedCards[0].classList.remove('show', 'open');
openedCards[1].classList.remove('show', 'open');
openedCards = [];
}
// When cards don't match, adds class "not-matching" to both and calls disable() function (to disable flipping of other cards). After half a second, removes "not-matching" class, calls enable() function (to make flipping cards possible again), and clears the two cards' arrays
function notMatching() {
openedCards[0].classList.add('not-matching');
openedCards[1].classList.add('not-matching');
disable();
setTimeout(function() {
openedCards[0].classList.remove('show', 'open', 'not-matching');
openedCards[1].classList.remove('show', 'open', 'not-matching');
enable();
openedCards = [];
}, 500);
}
// Disables all cards temporarily (while two cards are flipped)
function disable() {
Array.prototype.filter.call(cards, function(card) {
card.classList.add('disabled');
});
}
// Enables flipping of cards, disables matching cards
function enable() {
Array.prototype.filter.call(cards, function(card) {
card.classList.remove('disabled');
for (let i = 0; i < matchingCard.length; i++) {
matchingCard[i].classList.add('disabled');
}
});
}
// Updates move counter
function moveCounter() {
// Increases "moves" by one
moves++;
counter.innerHTML = moves;
// Starts timer after first move (meaning two cards have been flipped)
// TODO: timer only starts after clicking second card; start after clicking first one
if (moves == 1) {
second = 0;
minute = 0;
hour = 0;
startTimer();
}
// Sets star rating based on number of moves. (Note: using display: none for removed stars instead of visibility: collapse, because with visibility: collapse, row is centered as if stars are still present)
if (moves > 8 && moves < 12) {
for (i = 0; i < 3; i++) {
if (i > 1) {
stars[i].style.display = 'none';
}
}
}
else if (moves > 13) {
for (i = 0; i < 3; i++) {
if (i > 0) {
stars[i].style.display = 'none';
}
}
}
}
// Game timer
function startTimer() {
interval = setInterval(function() {
timer.innerHTML = minute + ' mins ' + second + ' secs';
second++;
if (second == 60) {
minute++;
second = 0;
}
if (minute == 60) {
hour++;
minute = 0;
}
}, 1000);
}
// Congratulates player when all cards match and shows modal, moves, time and rating
function congratulations() {
if (matchingCard.length == 16) {
// Window method that stops setInterval() Window method from executing "myTimer" function every 1 second
clearInterval(interval);
let finalTime = timer.innerHTML;
// Shows congratulations modal
modal.classList.add('show');
let starRating = document.querySelector('.stars').innerHTML;
// Shows number of moves made, time, and rating on modal
document.getElementsByClassName('final-moves')[0].innerHTML = moves;
document.getElementsByClassName('star-rating')[0].innerHTML = starRating;
document.getElementsByClassName('total-time')[0].innerHTML = finalTime;
// Adds event listener for modal's close button
closeModal();
}
}
// Closes modal upon clicking its close icon
function closeModal() {
closeIcon.addEventListener('click', function(e) {
modal.classList.remove('show');
startGame();
});
}
// Called when user hits "play again" button
function reset() {
modal.classList.remove('show');
startGame();
}
// Adds event listeners to each card
for (let i = 0; i < cards.length; i++) {
card = cards[i];
card.addEventListener('click', displayCard);
card.addEventListener('click', cardOpen);
card.addEventListener('click', congratulations);
}
Related
new to JS here and was wondering if someone could guide me on how I can pause and delete when implementing a typewriter effect in JavaScript.
I've got a function that successfully types out each word in an array, however I would like it to pause after typing each word and backspace or delete the letters before typing the next one.
//counter
var i = 0;
var index = 0;
var texts = ['Salesforce Consultant', 'Developer', 'Writer'];
var speed = 110;
let letter = '';
let currentText = '';
let delay = 25;
function typeWriter() {
//If counter is less than the # of letters in txt, reset array of words
if (i === texts.length) {
i = 0;
}
//Use count to select text to display
currentText = texts[i];
letter = currentText.slice(0, ++index);
document.querySelector("#demo").textContent = letter;
//If letters displayed are the same number of letters in the current text
if (letter.length === currentText.length) {
//Pause before deleting
//Delete letters in word
//Word is done displaying, and reset letters on screen
i++;
index = 0;
}
setTimeout(typeWriter, speed);
}
typeWriter();
<div id="demo"></div>
HTML
<div class="centered">
<div class="intro">
<p>A </p>
<p class ="typing" id="demo"></p>
</div>
</div>
You can do this by introducing a variable that determines how the index will change (+1 or -1). The different delay is just a different argument to setTimeout.
I would also suggest converting some global variables into function parameters: that way they are better (more narrowly) scoped. The change that these variables get, can be managed by what you let setTimeout pass on to the next call.
Here is how that could work:
const texts = ['Salesforce Consultant', 'Developer', 'Writer'];
const speed = 110;
const pause = 800; // <--- the longer delay between text direction changes
function typeWriter(i=0, index=1, direction=1) {
let displayed = texts[i].slice(0, index);
document.querySelector("#demo").textContent = displayed;
if (displayed.length >= texts[i].length) { // start removing after pause
setTimeout(() => typeWriter(i, index-1, -1), pause);
} else if (displayed.length === 0) { // go to next text after pause
setTimeout(() => typeWriter((i+1) % texts.length), pause);
} else { // continue in the current direction
setTimeout(() => typeWriter(i, index+direction, direction), speed);
}
}
typeWriter();
<div id="demo"></div>
I am working on an application where I'd like to provide overlays of different animations onto a range of videos using p5js. I'm looking to organize my classes of animation types so that each animation has a similar structure to update and destroy objects during each loop. My plan is to have an array of animations that are currently "active" update them each iteration of the loop and then destroy them when they are completed. I built a class to fade text in this manner but I'm getting some weird flashy behavior that seems to occur every time a new animation is triggered in the middle of another animation. I've been trying to debug it but have been unsuccessful. Do you have any suggestions as to:
(1) if this is due to my code structure? (and maybe you have a suggestion of a better way),
or
(2) I'm doing something else incorrectly?
Here is the code:
// create an array of currently executing animations to update
// each animation class needs to have one function and one attribute:
// (1) update() -- function to move the objects where ever they need to be moved
// (2) done -- attribute to determine if they should be spliced out of the array
var animations = [];
//////////////////////////////////////////
// Global Variables for Animations //
//////////////////////////////////////////
let start = false;
let count = 0;
function setup(){
let canv = createCanvas(1920, 1080);
canv.id = "myP5canvas";
background(0);
}
function draw(){
background(0);
// Check things to see if we should be adding any animations to the picture
var drawText = random(100);
if (drawText > 98) {
//if (start == false) {
let r = 255;
let g = 204;
let b = 0;
let x = random(width-10);
let y = random(height-10);
animations.push(new TextFader("Wowwwzers!", 100, 'Georgia', r, g, b, x, y, count));
start = true;
count += 1;
}
// Update animations that exist!
for (var i=0; i < animations.length; i++) {
// update the position/attributes of the animation
animations[i].update();
// check if the animation is done and should be removed from the array
if (animations[i].done) {
console.log("SPLICE: " + animations[i].id);
animations.splice(i, 1);
}
}
}
// EXAMPLE ANIMATION
// TEXT FADE
let TextFader = function(words, size, font, red, green, blue, xloc, yloc, id) {
this.id = id;
console.log("create fader: " + this.id);
// translating inputs to variables
this.text = words;
this.size = size;
this.font = font;
// To Do: separating out each of the values until I figure out how to fade separately from the color constructor
this.red = red;
this.green = green;
this.blue = blue;
this.xloc = xloc;
this.yloc = yloc;
// Maybe add customization in the future for fading...
this.fade = 255;
this.fadeTime = 3; // in seconds
this.fadeIncrement = 5;
// Variables to use for destruction
this.createTime = millis();
this.done = false;
}
TextFader.prototype.update = function() {
// Update the fade
// If the fade is below zero don't update and set to be destroyed
this.fade -= this.fadeIncrement;
if (this.fade <= 0) {
this.done = true;
} else {
this.show();
}
}
TextFader.prototype.show = function() {
textFont(this.font);
textSize(this.size);
fill(this.red, this.green, this.blue, this.fade);
text(this.text, this.xloc, this.yloc);
console.log("Drawing: " + this.id + " fade: " + this.fade + " done: " + this.done);
}
Yay, I've got you an answer! It works like expected when you reverse the for loop that loops over the animations.
Because you splice elements of the same array inside the loop, some elements are skipped. For example; animations[0].done = true and gets removed. That means that animations[1] is now in the spot of animations[0] and animations[2] is now in the spot of animations[1].
The i variable is incremented to 1, so on the next loop, you update animations[1] (and skip the animation that is now in animation[0]).
When you reverse the loop, everything before the element you splice stays the same and nothing is skipped.
For example; animations[2].done = true and gets removed. That means that animations[1] is still in the spot of animations[1].
The i variable is decremented to 1, so on the next loop, you update animations[1] and don't skip any elements.
// Update animations that exist!
for (var i = animations.length - 1; i >= 0; i--) {
// update the position/attributes of the animation
animations[i].update();
// check if the animation is done and should be removed from the array
if (animations[i].done) {
//console.log("SPLICE: " + animations[i].id);
animations.splice(i, 1);
}
}
I'm sorry if this has been asked before,
I've searched through Stackoverflow but couldn't find anything that answered my problem.
I'm building a simple memory game, an online version of Simon, when you click the "Start" button it runs the code below to create a random array (of length 4) out of the four colour buttons.
But when you click "Start" again for the next round it doesn't clear the array, and instead creates a second one, and then checks your input against both telling you you're both right and wrong, or right and right (depending on the random array created out of the buttons).
I've tried buttonsToClick = [] in the else section, but it doesn't reset.
I don't know what I'm missing, I've only been learning JavaScript/jQuery for about a month but I wanted to test my knowledge.
The code snipped:
var score = 0;
$("#score").html(`${score}`);
$("#button5").on("click", function() {
var buttons = document.getElementsByClassName("js-button");
var buttonsToClick = chooseRandomButtons(buttons);
currentButtons = buttonsToClick;
flashButtons(buttonsToClick, 0);
var currentOrder = 0;
$(".js-button").on("click", function() {
var selectedButton = $(this)[0];
var button = currentButtons[0];
if (selectedButton === button) {
currentButtons.splice(button,1);
/*alert("Correct");*/
score += 1;
$("#score").html(`${score}`);
} else {
currentButtons = buttonsToClick;
alert("Wrong. Click 'Start' to try again");
score = 0;
$("#score").html(`${score}`);
}
});
})
function chooseRandomButtons(buttons) {
var buttonsToClick = [];
var maxRandomNumber = buttons.length - 1;
for (var i = 0; i < 4; i++) {
buttonsToClick.push(buttons[randomIntFromInterval(0, maxRandomNumber)]);
}
return buttonsToClick;
}
function randomIntFromInterval(min, max) { // min and max included
return Math.floor(Math.random() * (max - min + 1) + min);
}
function flashButtons(buttonsToClick, index) {
setTimeout(function() {
$(buttonsToClick[index]).fadeOut(500).fadeIn(500);
if (index === buttonsToClick.length - 1) {
return;
}
flashButtons(buttonsToClick, index = index + 1);
}, 1000);
}
welcome to SO.
In general you're doing anything correct with your arrays.
The issue is your event handler.
Every time you click the #button5, which I guess is the start button, you register on all your .js-button a new listener. Since you're not unbinding the old event listeners they're still active.
Since the old event listeners have a reference to your old array, you're basically checking the button against the old game and the new game.
Your solution would be to unregister the old one before registering the new one.
This could be done for example by the .off method.
Your code should then look like this:
var currentOrder = 0;
$(".js-button").off("click").on("click", function() {
var selectedButton = $(this)[0];
var button = currentButtons[0];
if (selectedButton === button) {
currentButtons.splice(button,1);
/*alert("Correct");*/
score += 1;
$("#score").html(`${score}`);
} else {
currentButtons = buttonsToClick;
alert("Wrong. Click 'Start' to try again");
score = 0;
$("#score").html(`${score}`);
}
});
Notice the .off() there.
The documentation about the method could be found here: https://api.jquery.com/off/
I have a function called pageReload which sets the a timer and variables back on that page to start, when the time is counting down, however when the timer reaches 0 it seems to disable the function even though when the function is called again the time should be set back to 18 as specified in the function.
When it's between 18 and 0 it trigger ok and sets the time back to 18, the other parts seems to work ok (number of tries and matches set back)
I've tried different variations without getting it to work so below if the function together with the other code in the app which might give a bit of context to what I'm doing
"use strict";
//select each card
const cards = document.querySelectorAll('.card');
let isFlipped = false;
let setBoard = false;
let first, second;
let counter = 1;
//add event listeners to each square
for(let i = 0; i < cards.length; i++) {
let element = cards[i];
element.addEventListener('click', flipSquare);
}
function checkForMatch() {
//check for 2 matching squares
let isMatch = first.classList.value === second.classList.value;
$('#counter').html(`The number of tries made is: ${counter++}`);
isMatch ? disable() : unflip();
//check to see if completed - if so, score will be displayed
completed();
}
function checkScore(){
//determing whether a score A, B or unsuccessful were acheived
if(counter <= 15) {
$('#score').html("You got an A");
}
else if(counter > 15 && counter <= 20){
$('#score').html("You got an B");
} else {
$('#score').html("You had too many attempts and were therefore unsuccessful");
}
}
function completed(){
//pop up if all have been disabled
if($('.card:not(.open)').length === 0){
//display modal
$("#myModal").modal('show');
clearInterval(timerId);
clearTimeout(myTimeout);
elemComplete.html(timeComplete + ' seconds comleted in');
}
//check score on completion and output the result
checkScore();
}
let timeLeft = 18;
let timeComplete;
let elem = $('#some_div');
let elemComplete = $('#new_div');
let timerId = setInterval(showClock, 1000);
function shuffleCards() {
//give square random positions
for(let i = 0; i < cards.length; i++) {
let ramdomPos = Math.ceil(Math.random() * 12);
cards[i].style.order = ramdomPos;
}
}
function pageReload(){
shuffleCards();
//loop through any open cards to and remove their open status and add back click function to unflipped card
for(let i = 0; i < cards.length; i++) {
$(".card").removeClass('open');
let element = cards[i];
element.addEventListener('click', flipSquare);
}
isFlipped = false;
setBoard = false;
timeLeft = 18;
counter = 0;
n = 0;
$('#counter').html(`The number of tries made is: ${counter}`);
$('#updated').html(`The number of matches made is: ${n}`);
counter++;
}
I'm not 100% sure as I don't think this is all of the code, but I have a feeling that you are stopping your timer in the completed() function using clearInterval() and never restarting it?
Presuming this is the cause, I would try resetting the timer in your page reload function.
function pageReload(){
shuffleCards();
//loop through any open cards to and remove their open status and add back click function to unflipped card
for(let i = 0; i < cards.length; i++) {
$(".card").removeClass('open');
let element = cards[i];
element.addEventListener('click', flipSquare);
}
isFlipped = false;
setBoard = false;
timeLeft = 18;
counter = 0;
n = 0;
timerId = setInterval(showClock, 1000);
$('#counter').html(`The number of tries made is: ${counter}`);
$('#updated').html(`The number of matches made is: ${n}`);
counter++;
}
This makes the timer code a little fragile, so you could refactor the timer logic out into its own functions and do something like this to make things a little clearer:
let timerId = undefined;
function startTimer() {
if (timerId != undefined) {
stopTimer();
}
timerId = setInterval(showClock, 1000);
}
function stopTimer() {
clearInterval(timerId);
timerId = undefined;
}
You would then remove all of you existing timer code and call startTimer() in pageReloaded() and stopTimer() in completed()
How do I change my code to start Blinking at the same time, and still be infinite?
function blink() {
if (!toggleSwitch) {
for (let i = 0; i < elements.length; i++) {
shapes.push(elements[i].className);
}
// Start off at the first element.
let i = 0;
let len = shapes.length;
// Do the next link
function doNext() {
let element = shapes[i];
i++;
eval(element).fillColor = eval(element).setColor;
document.getElementById(element).style.backgroundColor = (eval(element).fillColor === document.getElementById(element).style.backgroundColor) ? 'white' : eval(element).setColor;
if (i < len) {
// Don't do anything special
} else {
// Reset the counter
i = 0;
}
, myVar = setTimeout(doNext, 1000);
}
// And the code needs kicked off somewhere
doNext();
}
}
Also I can't toggle Classes because with a drag and drop I can change the background color any time during a running time so I have an object for each shape where I keep what color is the background. –
To have your page call your blink function after a certain pause first how about doing something like this?
<BODY onLoad="setTimeout('blink()', 5000);">