Friday 10 April 2020

Web Tutorial: Easter Memory Game (Part 3/3)

The logic we've coded so far is sound. But the presentation can be improved for usability purposes. It's not to make it look prettier but rather to ease gameplay.

For this, we'll need to delay the opening and closing of the cards. First, let's make some adjustments to the CSS. For card, we want it to rotate towards the user. So we define the transform-origin property to be right in the middle. The transition property we'll set to 200 milliseconds for all animatable transitions.
.card
{
    height: 100%;
    width: 100%;
    border-radius: 5px;   
    overflow: hidden;           
    cursor: pointer;
    -webkit-transform-origin: 50% 50%;
    transform-origin: 50% 50%;
    -webkit-transition: all 0.2s;
    transition: all 0.2s;
}


And create the new CSS class, flipping. This rotates the card horizontally around the middle of the card, as specified by the transform-origin property in card.
.card
{
    height: 100%;
    width: 100%;
    border-radius: 5px;   
    overflow: hidden;           
    cursor: pointer;
    -webkit-transform-origin: 50% 50%;
    transform-origin: 50% 50%;
    -webkit-transition: all 0.2s;
    transition: all 0.2s;
}

.flipping
{
    -webkit-transform: rotateY(90deg);
    transform: rotateY(90deg);
}

.closed:before
{
    display: block;
    content: "EASTER MEMORY GAME\A\2671";
    white-space: pre-wrap;
    text-align: center;
    height: 100%;   
    width: 100%;
    background-color: #FFCC00;
    color: #FFFFFF;
}


Add another property to card in the reset() method, isFlipping, with a default value of false.
for(var i = 0; i < totalCards; i++)
{
    var card =
    {
        id: i,
        template: undefined,
        isMatched: false,
        isOpened: false,
        isFlipping: false,
        flip: (e) =>
        {
            this.flipCard(e.currentTarget.id);
        }
    }

    cards.push(card);
}


In the render() method, make another change to the cardDisplay sub-component. This adds "flipping" to the classes that each card is already styled with, if their isFlipping property is true.
var cardDisplay = this.state.cards.map(
    (item, key) =>
    {
        var template = "template" + item.template;
        var style = "card " + (item.isOpened ? "opened " + template + (item.isMatched ? " matched" : "") : "closed");

        style = style + (item.isFlipping ? " flipping" : "");

        return (
            <div className="cardContainer" key={key}>
                <div className={style} id={item.id} onClick={item.flip}>                               
                </div>
            </div>
        );
    }
);


Got all that? Let's work some ReactJS magic!

In the flipCard() method, if the card is not opened, we want to open it. But let's close it with an animation. Instead of opening the card right away, enclose that code block in the setTimeout() function with an interval of 200 milliseconds.
if (cards[cardIndex].isOpened)
{

}
else
{
    setTimeout
    (
        () =>
        {
            cards[cardIndex].isOpened = true;
            this.setState({"cards": cards});                                   

            this.matchCards(cards);                           
        },
        200
    );                           
}


Be sure to set the isFlipping property to false, because...
if (cards[cardIndex].isOpened)
{

}
else
{
    cards[cardIndex].isFlipping = true;
    this.setState({"cards": cards});

    setTimeout
    (
        () =>
        {
            cards[cardIndex].isFlipping = false;
            cards[cardIndex].isOpened = true;
            this.setState({"cards": cards});                                   

            this.matchCards(cards);                           
        },
        200
    );                           
}


...before the setTimeout() function is run, you will set isFlipping to true and update state.
if (cards[cardIndex].isOpened)
{

}
else
{
    cards[cardIndex].isFlipping = true;
    this.setState({"cards": cards});

    setTimeout
    (
        () =>
        {
            cards[cardIndex].isFlipping = false;
            cards[cardIndex].isOpened = true;
            this.setState({"cards": cards});                                   

            this.matchCards(cards);                           
        },
        200
    );                           
}


See what happens when I click on one card? It rotates, then shows its image.




That's pretty enough when applied to closed cards. Let's apply this to open cards. We want to close the card if it's opened. But only if it's not also matched with another card. So create an If block to check if the card's isMatched property is false.
if (cards[cardIndex].isOpened)
{
    if (!cards[cardIndex].isMatched)
    {
                                   
    }
}
else
{   
    cards[cardIndex].isFlipping = true;
    this.setState({"cards": cards});

    setTimeout
    (
        () =>
        {
            cards[cardIndex].isFlipping = false;
            cards[cardIndex].isOpened = true;
            this.setState({"cards": cards});                                   

            this.matchCards(cards);                           
        },
        200
    );                           
}


Again, set isFlipping to true, update state, then run the setTimeout() function with an interval of 200 milliseconds. After 200 milliseconds has passed, isFlipping should be set back to false, isOpened to false, and state updated. ReactJS intelligently renders all this, within that 200 milliseconds!
if (!cards[cardIndex].isMatched)
{
    cards[cardIndex].isFlipping = true;
    this.setState({"cards": cards});

    setTimeout
    (
        () =>
        {
            cards[cardIndex].isFlipping = false;
            cards[cardIndex].isOpened = false;

            this.setState({"cards": cards});                                   
        },
        200
    );                                   
}


So now when you click on a closed card, it should flip open. If you click on an open card, it should flip closed unless it's matched with another open card, in which case it will do absolutely fuck-all. Sound about right?

Animation for matching

Now that we've implemented animation for opening and closing of cards, let's do it for matching. Remember if two cards don't match after being opened, they close immediately? Well, we need that shit to slow down.

First, encase the existing code block for not matching, in a setTimeout() function. Add lines to set the isFlipping property of both cards to false.
if (matching[0].template == matching[1].template)
{
    cards[matching[0].id].isMatched = true;
    cards[matching[1].id].isMatched = true;

    var notMatched = cards.filter((x) => {return !x.isMatched;});
    if (notMatched.length == 0) this.stopTimer();

    this.setState({"cards": cards});                                   
}
else
{   
    setTimeout
    (
        () =>
        {
            cards[matching[0].id].isFlipping = false;
            cards[matching[0].id].isOpened = false;

            cards[matching[1].id].isFlipping = false;
            cards[matching[1].id].isOpened = false;

            this.setState({"cards": cards});                                
        },
        200
    );                                         
}


Then encase that setTimeout() call in another outer setTimeout() call. Here, set isFlipping to true for both cards and update state.
setTimeout
(
    () =>
    {
        cards[matching[0].id].isFlipping = true;
        cards[matching[1].id].isFlipping = true;
        this.setState({"cards": cards});

        setTimeout
        (
            () =>
            {
                cards[matching[0].id].isFlipping = false;
                cards[matching[0].id].isOpened = false;

                cards[matching[1].id].isFlipping = false;
                cards[matching[1].id].isOpened = false;

                this.setState({"cards": cards});                                
            },
            200
        );                    
    },
    200
);   


See what happens when I click on two unmatched cards? They hang there for a split second, then flip back closed! Now you can actually see what didn't match!




Finishing up...

In the render() method, declare btnClick as a callback. It's the reset() method that is called.
var message;
var buttonText;

var btnClick = () => {this.reset();}

if (this.state.seconds == 0)
{
    message = "GAME OVER!";
    buttonText = "REPLAY";
}
else
{
    message = "TIME ELAPSED: ";
    buttonText = "RESET";       
}


When rendering the button, set the onClick attribute to btnClick. Now whenever you click the button, the entire grid should reset and the timer goes back to 100 seconds, and the cards are randomly assigned again!
<div id="buttonContainer">
    <button onClick={btnClick}>{buttonText}</button>
</div>


Also, declare notMatched. Use the filter() method on cards to get an array of all cards that have the isMatched property set to false. Assign the array to notMatched.
var btnClick = () => {this.reset();}

var notMatched = this.state.cards.filter((x) => {return !x.isMatched;});

if (this.state.seconds == 0)
{
    message = "GAME OVER!";
    buttonText = "REPLAY";
}
else
{
    message = "TIME ELAPSED: ";
    buttonText = "RESET";       
}


The code block following this should be enveloped in the Else portion of an If-Else block, checking if notMatched is an empty array.
var notMatched = this.state.cards.filter((x) => {return !x.isMatched;});

if (notMatched.length == 0)
{

}
else
{
    if (this.state.seconds == 0)
    {
        message = "GAME OVER!";
        buttonText = "REPLAY";
    }
    else
    {
        message = "TIME ELAPSED: ";
        buttonText = "RESET";       
    }
}


If notMatched is empty, that means the user has matched all card and congratulations are in order.
if (notMatched.length == 0)
{
    message = "CONGRATULATIONS! YOU HAVE COMPLETED THIS GAME WITH TIME REMAINING: ";
    buttonText = "REPLAY";
}
else
{
    if (this.state.seconds == 0)
    {
        message = "GAME OVER!";
        buttonText = "REPLAY";
    }
    else
    {
        message = "TIME ELAPSED: ";
        buttonText = "RESET";       
    }
}


Good shit, right?!


Sorry for swearing so much on an Easter-themed web tutorial. I'm just really excited. ReactJS can be a pain in the ass, but sometimes it's also really cool.

Flipping you off,
T___T

No comments:

Post a Comment