Alternative to Pointer Events - Safari and IE10 Support

Hi there,

I’ve got a simple Memory Card Game that I’ve created using pure JavaScript (i.e. no jQuery). The problem I’ve got is that the cards are clickable even while they’re being checked which causes problems when you select another card - it counts a click on one of the “checking in progress” cards as a click, and tries to match it with the “third” card you click. This results in a reset and a bit of confusing game mechanic.

Therefore, while the cards are being compared, I have attempted to set Pointer Events to none on the selected cards. (and reset the Pointer Events back to “visible” once the comparison is complete). This works perfectly in all browsers except Internet Explorer 10 (and below), and Safari (both desktop and iOS versions).

Is there an alternative I can use, or is there a better method to what I’m trying to do?

Below in the We Transfer are all of the files (HTML, CSS, JS and all the images used for the tiles). Included below is the raw JS in code tags.

cardMatchGame();

function cardMatchGame() {
    this.gameboard = document.getElementById("ws-game-board");

    this.gameWidth = 4;
    this.gameHeight = 4;
    this.firstCard = null;
    this.secondCard = null;

    this.turn = 0;
    this.matches = 0;
    this.checkTimeout = null;

    this.sec = 0;

    function pad(val) {
        return val > 9 ? val : "0" + val;
    }

    this.timer = setInterval(function () {

        document.getElementById("minutes").innerText  = pad(parseInt(sec / 60, 10));
        document.getElementById("seconds").innerText = pad(++sec % 60);
        
        if (document.getElementById("minutes").innerText  >= "60") {
            document.getElementById("seconds").innerText = "00";
            gameEnd();
        }

    }, 1000);

    this.turnUpdate = function () {
        turnDisplay = document.getElementsByClassName("ws-turn-display");  // Find the elements
        for (var i = 0; i < turnDisplay.length; i++) {
            turnDisplay[i].innerText = turn;    // Change the content
        }
    }

    this.createGrid = function (h, v) {
        var a = [];

        // create squares within the grid and push the values into an array
        for (var i = 0; i < gameWidth * gameHeight / 2; i++) {
            a.push(i);
            a.push(i);
        }

        // create a randomised array of numbers using the contents of the "a" array created above
        // remove this value from the "a" array ready for the next number
        var s = [];
        while (a.length > 0) {
            var r = Math.floor(Math.random() * a.length);
            s.push(a[r]);
            a.splice(r, 1);
        }

        // create the grid using the "s" array
        for (var x = 0; x < h; x++) {
            for (var y = 0; y < v; y++) {
                createCard(s.pop(), x, y);
            }
        }
    }

    this.createCard = function (cardNum, posX, posY) {
        var card = document.createElement("img");
        card.num = cardNum;
        card.src = "images/xmas18_memorygame_tileback.jpg";
        card.classList.add("ws-card");
        card.onclick = clickCard;
        gameboard.appendChild(card);
    }

    this.clickCard = function (e) {
        var card = e.target;
        // set card image for each card
        card.src = "images/xmas18_memorygame_tile" + card.num + ".jpg";
        

        // if another element is clicked while other cards are being checked 
        // then clear the timeout and run checkCards again
        if (checkTimeout != null) {
            clearTimeout(checkTimeout);
            checkCards();
        }

        // check if firstCard or secondCard are set to null
        if (firstCard == null) {
            firstCard = card;

        } else if (firstCard == card) {
            firstCard.src = "images/xmas18_memorygame_tileback.jpg";
            firstCard = null;
        } else if (secondCard == null) {
            secondCard = card;
            firstCard.style.pointerEvents = "none";
            secondCard.style.pointerEvents = "none";
            checkTimeout = setTimeout(checkCards, 1000);
        }
    }

    this.checkCards = function () {
        // if the num of firstCard and secondCard match then remove from board
        if (firstCard.num == secondCard.num) {
            firstCard.style.visibility = "hidden";
            secondCard.style.visibility = "hidden";
            matches++;

            // if game complete
            if (matches >= gameWidth * gameHeight / 2) {
                gameEnd();
            }

        } else {
            // if the cards don't match then set their src back to the original
            firstCard.src = "images/xmas18_memorygame_tileback.jpg";
            secondCard.src = "images/xmas18_memorygame_tileback.jpg";
        }

        // add 1 to number of turns
        turn++;
        turnUpdate();

        // reset pointer events back to visible so the cards selected are clickable again
        firstCard.style.pointerEvents = "visible";
        secondCard.style.pointerEvents = "visible";
        // reset variables back to null so the next turn can be taken
        firstCard = null;
        secondCard = null;
        checkTimeout = null;
    }

    this.gameEnd = function () {
        document.getElementById("ws-game-end").style.visibility = "visible";
        document.getElementById("ws-game-end").style.display = "flex";

        turnUpdate();
        clearInterval(timer);

        upperPrizeMsg = "Wow, you completed it in " + document.getElementById("minutes").innerText  + " minutes " + document.getElementById("seconds").innerText + " seconds! Here's your prize: \n WHITESTUFF15";
        lowerPrizeMsg = "Hmm, seems like you need some more practice. \n Here's a small prize to keep you going \n WHITESTUFF5";
        outOfTimeMsg = "Oh dear, you ran out of time!";
        sorryMsg = "Sorry, you didn't win a discount code this time. Try again!";

        if (turn >= 0 && turn < 32 && document.getElementById("minutes").innerText  < "05") {
            document.getElementById("ws-game-end-message").innerText = upperPrizeMsg;
        } else if (turn >= 32) {
            document.getElementById("ws-game-end-message").innerText = lowerPrizeMsg;
        } else if (document.getElementById("minutes").innerText  >= "60") {
            document.getElementById("ws-game-end-message").innerText = outOfTimeMsg
        } else {
            document.getElementById("ws-game-end-message").innerText = sorryMsg;
        }
    }

    turnUpdate();
    createGrid(gameWidth, gameHeight);
}

Any thoughts please?

Cheers,

Shoxt3r

Hi @Shoxt3r, you might add a guard to the top of the clickCard() function like so:

var clickCard = function (e) {
  if (secondCard) return

  var card = e.target // etc...
}

This way the event will still get triggered, but the handler function exits immediately if the 2nd card is set.

BTW, if you’re assigning the functions to this you will have to call it as a constructor, i.e.

function CardMatchGame () {
  // ...
}

var game = new CardMatchGame()

Otherwise this will refer to the window, which happens to work in this case but will fail with multiple game instances; also polluting the global scope should generally be avoided. But unless you want to expose those functions as a public API, you actually don’t need to assign them to this at all – just declare them as regular functions inside cardMatchGame() (no new required then either).

1 Like

Thanks m3g4p0p! A simple solution to the problem that works really well.
I guess the only issue with this is that it slows the user down slightly as they can’t select another card without waiting on the check, but adjusting the timeout to around 400ms ensures that they’re not waiting too long (10000 was set in the script above for testing purposes only).

I see… but you could always do the timeout check first, and only then return from the clickCard() function early if a previously selected card was clicked:

var clieckCard = function (e) {
    var card = e.target;
    var wasSelected = card === firstCard || card === secondCard

    if (checkTimeout != null) {
        clearTimeout(checkTimeout);
        checkCards();

        if (wasSelected) return
    }

    card.src = "images/xmas18_memorygame_tile" + card.num + ".jpg";
    // etc...
}

Thanks for the tip, makes perfect sense!

However, I don’t believe there is a need to check for wasSelected for example, as I have a check running if checkTimeout doesn’t equal to null (i.e. if another card is clicked in the meantime), then the checkTimeout is cleared and checkCards is run again (if by some chance the new selections match).

By all means let me know your thoughts though?

Cheers,

Andrew

If you omit that early return, then clicking an already selected card will make that card immediately selected in the next turn too. If this is what you want, you’ll have to check if the previous cards did match though; otherwise the clicked card will remain visible even though it should actually be removed:

var clickCard = function (e) {
  var card = e.target
  var wasMatch = firstCard === secondCard

  if (checkTimeout != null) {
    clearTimeout(checkTimeout)
    checkCards()

    if (wasMatch) return
  }
  
  // etc...
}

FWIW I’d prefer the additional-click version though, so that clicking a card consistently flips that card in all possible cases. Also the user has to confirm they meant to select that card again.

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.