Wednesday, 29 April 2020

Social Justice League: A TeochewThunder Project

During the Circuit-breaker period introduced by the Singapore Government to combat the COVID-19 outbreak, many Singaporeans found themselves at a loss as they adjusted to a life without socializing, dine-in options and various other services. I had my own problems, which I won't go into here, but cabin fever wasn't one of them.

You see, I'm a programmer. And one thing about the introvert stereotype that's often applied to those of my ilk - there's a discomfiting amount of truth in it. I'm comfortable enough around people, but it's when I'm imposing my will upon a hapless machine to do my bidding that I'm truly at home. Suffice to say, being made to stay home didn't bother me nearly as much as most others.

The time spent staying home resulted in me catching up on an awful lot of code. And I completed projects, some of which I had started years prior to this.

Social Justice League

One of them was a browser-based game I had written in using jQuery - Social Justice League.

And yes, that name is a ripoff of the Justice League. But it's nothing like the comic or the movie. In fact, it plays more like a desktop version of Magic: The Gathering.

How did I get that idea?

Back in 2018, I was playing this really awesome card game. It's a mobile app, Stormbound. And that's where I derived most of my inspiration for gameplay. Each card needs a certain amount of points to play, and what card you play against is also a factor. Certain cards have certain attributes that can be incredibly powerful in conjunction with other cards, or against other cards.

Stormbound

Sometime around then, the SJW phenomenon had exploded all over USA and Canada. People were yammering on about Safe Spaces, intersectionality and micro-aggressions, and seemingly engaged in some obnoxious competition to see who was more "woke". Every day, on my news feed, all sorts of really bizarre shit just popped up. Some terms kept coming up, such as "mainsplaining", "Participation Trophy" and "Virtue-signalling".

That's when I started thinking: what if each of these terms were their own card, and I could play them against each other? What if I could literally play the Race Card, Victim Card and Gender Card? What would I call the points used to play those cards? Woke points.

When those cards got attacked and lost, I would call it being Triggered.

When they were returned to the deck like Magic: The Gathering, instead of calling it The Graveyard, I would call it The Safe Space.

Heck, this game just about wrote itself as far as concept was concerned. The terms I kept reading were now being put to good use. Feminazi, Snowflake, Keyboard Warrior, Tone Police, Whitewashing... all these were terms I now turned into cards.

Plenty of my friends were pretty enthusiastic about coming up with suggestions once I pitched the idea.

Coding begins...

After the conceptualization had started to take shape, it was time to stop thinking, stop talking, and just do it.

The Interface. First of all, I needed a visual layout in which to place the cards. This took the better part of two weekends as I hashed out what worked and what didn't. HTML and CSS is easy in principle, depending on what you want it to do. But manage I did.

Gameplay. And then came animation and Human-computer Interaction. What did I want users to click on? What would happen when they did? And how would they understand when they were to click, and on what? How could I design the flow in a way that made sense?

Cards

Cards. The visual aspect of the cards was simple enough. Since I wasn't planning to profit off ths game, I was quite comfortable using free stock photos from Pixabay. The various stats had to be arranged nicely, and there had to be three different sizes - the large one for more information, the medium one for representation in the player's hand, and the tiny one for the deck. But no, it was the game mechanics that I had to be very careful with here. Nothing game-breaking or totally useless.

Early on, I figured out that attempting to code animations by hand for such a big project was just asking for a lot of grief. So I learned jQuery UI. Best investment I ever made. After all, I was already using jQuery.

After I had coded the layout and the visuals and the main animations, all that was left was providing special abilities for each card... all forty plus of them.

Distractions, and eventual completion

While two years passed in the meantime, it's not like I had an entire two years to do this. Between work and learning new stuff, it was a struggle to find time to work on this. Sometimes I would achieve a lot in one spurt of enthusiasm, and other times I was just too tired. There were shiny new things to learn too - Ruby, ReactJS, D3, to name a few.

And then I got married, and things took a back seat for a while. I'd work on Social Justice League maybe once every few months. The inevitable happened; the project began to gather dust.

In the second week of the Circuit-breaker period, I sat down seriously and took stock of where I had left off with Social Justice League . And found, to my astonishment, that there wasn't that much left to do. Some cards needed to be coded, and others were ideas I discarded. All I needed was a little time.

And time was something I had an abundance of now.

It was done sooner than I expected. Sure, I'm a better coder now than I was in 2018, but even back then, I had the sense to write my code in such a way that I could recycle whatever I needed, and produce new functionality with a bit of copy-paste-editing. Over the course of a few hours, the last remaining cards were coded into the system.

The final piece.

I was done. Social Justice League was no longer "in progress". It was ready to play.

Aftermath

Words can't adequately express the emotional release that came from finally completing something that I've incubated in my mind for so long. Sure, Social Justice League is nothing ground-breaking. There's tons of room for improvement. It's nothing more than an amalgamation of cultural references packaged in snarky homage to self-righteous SJWs everywhere. But I made it. It's mine. Even if no more than ten people ever play it, I've completed a sizeable (by my standards) project.

At the risk of sounding dramatic, it's like this great weight has been lifted off my shoulders. Could I have done anything different? Of course. But that's no mean feat considering just how crap I was two years ago.

The sweetest thing about this whole deal was, I learned. My command of JavaScript probably increased exponentially. And being forced to use jQuery UI to handle the animations, added a new tool to my ever-growing arsenal. I made mistakes along the way, and learned from them.

No matter how Social Justice League is received by the world, it's been great.

Play your cards right!
T___T

Saturday, 25 April 2020

One Missing File

When undergoing the tech job interview process, sometimes it's the little things that trip you up. This is an amusing (and hopefully educational!) tale of one such incident.

I received a phone call from a tech company (which shall remain unnamed) and what would you know, I forgot that why I had even applied for the job and when. Somehow I recovered from that boo-boo and managed to have a coherent conversation with my phone interviewer about all things tech. The interview concluded with the interviewer telling me that she would get back to me after processing our conversation with her team.

A few days later, I received an email which contained a PDF - specifications for the code test I needed to pass in order to progress in the job application. After taking a gander, I was blown away. Now this was a fun one. Instead of asking me to answer a series of technical questions that made me feel like I was studying for an exam, this tech company wanted me to create a simple web application using no frameworks and libraries. For me, this meant HTML, CSS and vanilla JavaScript.

So the weekend arrived, and I spent most of it coding. I gave it my best. Proper indentation, proper spacing, clean code, code test coverage. Made sure the solution fit the specifications as stated. Tested the solution on multiple browsers and screen sizes. At the end of it, I even wrote a ReadMe file.

No zip files allowed!


GMail flatly refused to allow me to attach a zip file, thus I uploaded everything to a GitHub repository and sent them the link. They replied by politely asking me to set the repository to Private after downloading my code, and I acquiesced.

One week later...

I received an email from those guys informing me that "we have decided not to move forward with your application", which in layman's speak, simply meant that I had failed this round.

No matter. I asked them; since my code was presumably not up to their standards anyway, would they mind if I continued my work on the web app and reset the repository to Public? Their response was to ask me to continue setting the repository to Private.

Now this befuddled me. Ostensibly, they had originally asked me to set the repository to Private so that other candidates they might be evaluating did not copy off my work. But now that I'd failed, why did it matter if anyone copied my work? They would be copying something that wasn't up to the mark, after all.

I obsessed over this, going through my code and trying to figure out where it was lacking. I even added my ex-boss as a collaborator and asked him to help review it. And he pointed out something that caused me to facepalm.

Oh, FFS.

I was missing one file. Not just any file. I was missing index.html! I'd checked my solution before uploading but somehow neglected to re-check after uploading.

Oh, FSS. Of all the stupid mistakes I could have made...

As errors go, this was really elementary and I was horribly embarrassed. I probably will never speak of this again, but at least it makes for a good story.

The takeaway from all this

Don't get carried away by enthusiasm. Maintain a healthy amount of detachment, because if you're too close to the issue, chances are you're going to miss something that will be glaringly obvious in hindsight.

Stay hungry, stay foolish. But not too foolish!
T___T

Sunday, 19 April 2020

Why some things should not be automated

We live in a world that is increasingly being automated. Mundane tasks such as ordering food and buying groceries is now done via means of Food Delivery apps. Trips to the bank have long been supplanted by online banking services. No longer do you need to stand in line to purchase a movie ticket - mobile apps make this a huge convenience.

In the software industry, where software developers are responsible for much of this automation, it comes as no surprise that many work-related tasks are now subject to this very same automation. Automated DevOps is a thing. Manual testing still has its place, but test-driven development via automated testing is the de facto standard. Linters now beautify and neaten huge chunks of code at a click of a mouse button, a task that used to be manually carried out by the more obsessive of us.

Yet I say, some things in life should not be automated. Yes, they can be automated and indeed many people are already doing so, but my contention is that they shouldn't. Because the very act of manually doing them is what gives those acts meaning, and builds character.

Developing good habits

To give this some context, I'd like to relate an anecdote from last year.

My first-ever wedding anniversary was coming up, and I was stressing out over what would qualify as an acceptable anniversary gift. Like the average unimaginative guy, I was leaning towards jewelry, but my taste in aesthetics has always been horrible. And then my wife called me while I was at work one day and told me she had decided to buy another phone for some reason or other. She'd narrowed it down to two models, but couldn't decide and wanted an opinion. The first model cost 700 SGD and had a lot of features. The second model cost half that, had all the features she needed, but she was afraid it would be obsolete in a year and didn't want to risk wasting her money.

700 SGD? Shit, I could handle that.

I told her to head for the top drawer of the cupboard in our bedroom, look in the bottom left corner of that drawer. In it was a white envelope containing exactly 720 SGD. I told her to use that to buy her phone. I also informed her, quite gleefully, that this was her anniversary gift.

In one move, I had achieved a few objectives. I had gotten my wife something she actually wanted for an anniversary gift, and the beauty of it was, I hadn't needed to choose it, or even buy it myself. And my wife now thinks I'm some kind of miracle worker who can produce extra cash out of nowhere.

But that's enough bragging about how clever I am. The real question is - how did I happen to have that envelope of cash lying around just for this occasion?

Squirreling money away

You see, I have this habit of squirreling money away. 2 SGD a day amounts to 730 SGD a year. Over time, I'd simplified the formula to 10 SGD every five days, or just 60 SGD a month. The 720 SGD in the envelope was cash I'd squirreled away for the year of 2017, and forgotten about until that point!

It's not like I even needed the money. But years of being financially strapped and slaving away for cheap-bastard sweatshop bosses had ingrained this little habit in me. Even now, drawing more than twice what I used to earn as a web developer, I've refused to let go of this tendency to put cash aside.

Every month, my tenants pay me more than that 720 SGD in cash. I could quite easily put one month's rental in the envelope and forget about saving petty cash for the rest of the year. It would be convenient, and quite doable. And perhaps utterly disastrous in the long run. The only reason why I find it so easy to save money when other people have all sorts of problems, is because I've made a habit of it. It's something I do without thinking because I do it regularly and constantly. Were I to ease up just for the sake of making it even more convenient, sooner or later, I would lose that habit simply because I allowed it to atrophy.

Drawing a bigger paycheck can properly screw up your ability to live with less money. Or, if you retain the habits you developed when you were a lot poorer, make you a heck of a lot more resilient.

So yeah... I didn't automate that task. Or perhaps I did, but in a very manual way. Does that make sense?

Giving acts meaning

Since I began life as a working professional roughly two decades ago, I've been paying the Womb Rental. And by that, I mean that amount of money I give my mother every month. Some like to call it Parental Protection Money or Uterus Tax, but to each his own.

It wasn't always this way. When I was younger, I found this a drag and kept thinking of ways to smoothen the process. Maybe give her access to my bank account so she could draw the money herself? Maybe set up some kind of automated monthly payment to her bank account from mine? Or give her a whole lot of money at one shot so I could go for months without going through all that hassle?

Mom vetoed all of that shit. She wanted me to personally hand her cash, every month. It didn't matter how much, as long as I could afford it.

Funds transfer

At that time, I didn't understand. Wasn't my way more convenient for everyone involved? Why the insistence on the manual method? But after so many years of doing this, it's starting to make sense. As outdated as I think my mother's worldview is, and as desperate as I am not to turn into my parents, I'm beginning to think Mom may have had a point there.

You see, it wasn't about the money. It was never just about the money.

What my mother wanted was for me to get used to having to consider her needs at least once monthly. To get used to contributing to her finances ever so often. To prepare me for the day I would have to truly step up. My software developer mindset would have gotten in the way of all that.

And it's worked. Well, kind of. I take it as a challenge to remember to pay the Womb Rental on time. Every time my mother has to call me to remind me because I missed the date, I consider it a personal failure, and resolve to do better.

To Conclude...

Efficiency may be one of the ultimate goals of programming, but it isn't everything. It makes life easier immediately, but sometimes we need to look at long-term effects.

I really should automate this signoff,
T___T

Tuesday, 14 April 2020

Google and Apple's Proposed Collaboration on Contact Tracing

Whassup!

Remember TraceTogether, the app for contact tracing that's supposed to help Singapore in these trying times?

Collaboration!

Well, apparently Google and Apple are belatedly getting into the act. They just announced that they will be partnering to produce such an app that will work on both their platforms. I assume that means sharing resources and producing some cool shit, such as Bluetooth-based technology for aid in contact tracing.

Singapore's equivalent

And I hate to sound like a nationalistic jackass here, but we got there first. That's right - I didn't have a hand in the creation of TraceTogether, but it is my country and my industry that produced it and I'm feeling unreasonably proud at this moment.

TraceTogether

Other countries have asked if Singapore would license the code to them, and our Minister of Foreign Affairs, Vivian Balakrishnan, had this to say.

"We believe that making our code available to the world will enhance trust and collaboration in dealing with a global threat that does not respect boundaries, political systems or economies."


I don't like the guy (can't even remember why right now) but that is one awesome response. We're making this beauty open-source, thus making Singapore's work even more relevant to the world at large.

A possible problem with Google and Apple's proposed collaboration

Privacy has been kind of a hot-button issue ever since Google started using location tracking. Sure, we could always turn it off, but the fact remains is that Google collects a lot of data about its users. And users have absolutely no reason to believe that this won't be abused in the future. I mean, look at the state of the world as it was even before the Coronavirus outbreak started.

How do we balance a user's right to privacy with the immeasurable potential to save lives? There's no clear, correct answer. Even Singapore doesn't have the answer, though I think TraceTogether is a sensible compromise many of us can accept. However, I'm not going to offer the typical American response and declare that Singapore's solution works for every country simply because it works for Singapore. This is something every country needs to decide for themselves.

Eye in the sky.

Even in Singapore, there are those who refuse to install the app because they don't trust the Government not to abuse the privilege. They're concerned about their privacy and possible intrusions into it despite the fact that the COVID-19 situation in Singapore is getting worse, and everyone's lives, not just their privacy, is being threatened.

To these people, I say: It's not about you. It's not about what kinky porn you've been viewing on your phones or what embarrassing private business you've been up to. This is about all of us. There's a bigger picture out there, and you're not seeing it. It's all well and good to be prepared to die for your privacy. Are you prepared to let others die, as well?

Think about it. Stop thinking about yourselves for maybe one goddamn second.

Remember, stay safe out there!

Break out of the outbreak!
T___T

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

Tuesday, 7 April 2020

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

This game is all about pairing up. Remember in the reset() method we had 6 templates? Well, for each template there will be 3 pairs. That makes (3 x 2 = 6) cards per template, and since we have 6 templates, that makes (6 x 6 = 36) cards!

After having derived the cards array, create a For loop to iterate through totalTemplates. Also, since we're talking about templates, add the template property to the card object, with a default value of undefined.

And just in case you didn't set the isOpened property to true in the previous part of this tutorial, you should totally do it now.
reset()
{
    this.stopTimer();

    var cards = [];
    var totalTemplates = 6;
    var totalCards = totalTemplates * totalTemplates;

    for(var i = 0; i < totalCards; i++)
    {
        var card =
        {
            id: i,
            template: undefined,
            isOpened: true,
        }

        cards.push(card);
    }

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

    this.startTimer();
}

for (var i = 0; i < totalTemplates; i++)
{

}

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


Within the loop, declare assigned and set it to 0. Then create a While loop that runs as long as assigned is less than totalTemplates.
for (var i = 0; i < totalTemplates; i++)
{
    var assigned = 0;

    while (assigned < totalTemplates)
    {

    }
}


Then declare unassigned. It's an array, returned by running the cards array through the filter() method to only get the elements whose template property is undefined. Here's some info about the filter() method.
for (var i = 0; i < totalTemplates; i++)
{
    var assigned = 0;

    while (assigned < totalTemplates)
    {
        var unassigned = cards.filter((x) => {return x.template == undefined;});
    }
}


Next, declare randomIndex and use a random number function to get any number from 0 to the length of unassigned, minus 1.
for (var i = 0; i < totalTemplates; i++)
{
    var assigned = 0;

    while (assigned < totalTemplates)
    {
        var unassigned = cards.filter((x) => {return x.template == undefined;});
        var randomIndex = Math.floor((Math.random() * (unassigned.length)));
    }
}


And once that's done, we use the id property of the element of unassigned pointed to by randomIndex, to point to the actual element in cards, and set the template property to the current template number! Then increment assigned.

The idea here is to go through every template, numbered 0 to 5, progressively setting the template property of "unassigned" cards until the current template fills up 6 cards. Once each of the 6 templates is assigned to 6 cards, the script exits the For loop. This will not be an infinite loop because the length of unassigned grows shorter by one every time the contents of the While loop is run.
for (var i = 0; i < totalTemplates; i++)
{
    var assigned = 0;

    while (assigned < totalTemplates)
    {
        var unassigned = cards.filter((x) => {return x.template == undefined;});
        var randomIndex = Math.floor((Math.random() * (unassigned.length)));

        cards[unassigned[randomIndex].id].template = i;
        assigned++;
    }
}


Now in the render() method, remember we created cardDisplay? Here, declare template and set it to "template", concatenating the string with the template property. Then redefine style so that it adds template to the class as well as "opened" if isOpen is true. That's what the space was for!
var cardDisplay = this.state.cards.map(
    (item, key) =>
    {
        var template = "template" + item.template;
        var style = "card " + (item.isOpened ? "opened " + template: "closed");

        return (
            <div className="cardContainer" key={key}>
                <div className={style}>                               
                </div>
            </div>
        );
    }
);


And after this, the CSS. Set background properties for opened. And then set the background-image property for CSS classes template0 to template5. We'll use the images shown in the first part of this tutorial.
.opened:before
{
    display: block;
    content: "";
    height: 100%;   
    width: 100%;
    background-size: cover;
    background-position: center center;
    background-repeat: no-repeat;
}

.template0:before
{
    background-image: url(easter0.jpg);
}

.template1:before
{
    background-image: url(easter1.jpg);
}   

.template2:before
{
    background-image: url(easter2.jpg);
}   

.template3:before
{
    background-image: url(easter3.jpg);
}   

.template4:before
{
    background-image: url(easter4.jpg);
}

.template5:before
{
    background-image: url(easter5.jpg);
}


There you go. Each individual template should have 3 pairs, randomly placed all throughout the grid. Remember we set isOpened to true? That's so you can see what's going on.


Clicking the cards!

In the reset() method, set isOpened to false again, and add the flip() method to card. We pass in the event, e, as an argument. In the method, we call the flipCard() method (which we'll write soon), and pass in the id of the card. Remember each card has an id? Well, it's about to be useful.
for(var i = 0; i < totalCards; i++)
{
    var card =
    {
        id: i,
        template: undefined,
        isOpened: false,
        flip: (e) =>
        {
            this.flipCard(e.currentTarget.id);
        }
    }

    cards.push(card);
}


In the render() method, ensure that the onClick attribute is set with the flip() method.
return (
    <div className="cardContainer" key={key}>
        <div className={style} id={item.id} onClick={item.flip}>                               
        </div>
    </div>
);


Now, we're going to define the flipCard() method. It accepts a parameter - an integer which is the id of the card clicked.
constructor(props)
{
    super(props);
    this.state =
    {
        "seconds": 100,
        "cards": []
    };
}

flipCard(cardIndex)
{

}

stopTimer()
{
    clearInterval(this.interval);
    this.interval = undefined;
}


flipCard() does nothing if seconds is 0, because then the game would be over.
flipCard(cardIndex)
{
    if (this.state.seconds > 0)
    {       

    }
}


Then declare cards, and assign the value of the cards array in state, to it. Think of cards in this case as a temporary storage variable. Then define an If block, using the element in the cards array pointed to by cardIndex, and checking if it's opened.
flipCard(cardIndex)
{
    if (this.state.seconds > 0)
    {       
        var cards = this.state.cards;

        if (cards[cardIndex].isOpened)
        {

        }
        else
        {

        }
    }
}


Don't do anything for now if the card is opened. But if it isn't, set the isOpened property to true, then replace the cards array in the state, with this altered cards array. We need to do this because arrays in state aren't mutable the normal way.
flipCard(cardIndex)
{
    if (this.state.seconds > 0)
    {       
        var cards = this.state.cards;

        if (cards[cardIndex].isOpened)
        {

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


Now try clicking on each card. Those that are closed, will be opened to reveal their picture. And those already opened, won't react!


Matching Logic

Time for the next part - determining if the cards are matched. Add the isMatched property to card in the reset() method. The default value is false.
for(var i = 0; i < totalCards; i++)
{
    var card =
    {
        id: i,
        template: undefined,
        isMatched: false,
        isOpened: false,
        flip: (e) =>
        {
            this.flipCard(e.currentTarget.id);
        }
    }

    cards.push(card);
}


Then in the flipCard() method, after setting the isOpened property to true and setting the state, run the matchCards() method, passing in cards as an argument.
flipCard(cardIndex)
{
    if (this.state.seconds > 0)
    {       
        var cards = this.state.cards;

        if (cards[cardIndex].isOpened)
        {

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

            this.matchCards(cards);                                   
        }
    }
}


Here, define the matchCards() method. It will accept the array arr as a parameter.
constructor(props)
{
    super(props);
    this.state =
    {
        "seconds": 100,
        "cards": []
    };
}

matchCards(arr)
{
   
}

flipCard(cardIndex)
{
    if (this.state.seconds > 0)
    {       
        var cards = this.state.cards;

        if (cards[cardIndex].isOpened)
        {

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

            this.matchCards(cards);                                   
        }
    }
}


Declare cards and assign arr as its value. Then define the variable matching. It is an array that will be produced when we run cards through the filter() method, only returning cards that are opened and not matched.

Then, in an If block, check if the there are two elements in the matching array. This would mean that there are two cards that are opened and not matched, and it is these two cards that need to be compared.
matchCards(arr)
{
    var cards = arr;
    var matching = cards.filter((x) => {return x.isOpened && !x.isMatched;});

    if (matching.length == 2)
    {

    }   
}


Compare the template properties of these cards in a nested If block.
matchCards(arr)
{
    var cards = arr;
    var matching = cards.filter((x) => {return x.isOpened && !x.isMatched;});

    if (matching.length == 2)
    {
        if (matching[0].template == matching[1].template)
        {
                                   
        }
        else
        {
                                
        }
    }   
}


If they match, set the isMatched property of these cards to true. Ensure that you use the id property of the matching elements to reference the index of the cards array when you do, because it's the cards in cards that you need to change, not those in matching. Then update cards in state with the newly updated array.
matchCards(arr)
{
    var cards = arr;
    var matching = cards.filter((x) => {return x.isOpened && !x.isMatched;});

    if (matching.length == 2)
    {
        if (matching[0].template == matching[1].template)
        {
            cards[matching[0].id].isMatched = true;
            cards[matching[1].id].isMatched = true;

            this.setState({"cards": cards});                                   
        }
        else
        {
                            
        }
    }   
}


Add an extra condition meanwhile. using the filter() function on cards again, check if any cards are not matched and return the resulting array to the variable notMatched. If there are no cards that aren't matched, stop the timer because the game is over.
matchCards(arr)
{
    var cards = arr;
    var matching = cards.filter((x) => {return x.isOpened && !x.isMatched;});

    if (matching.length == 2)
    {
        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
        {
                            
        }
    }   
}


Naturally, if the two cards do not match, close them by setting isOpened to false, and update state.
matchCards(arr)
{
    var cards = arr;
    var matching = cards.filter((x) => {return x.isOpened && !x.isMatched;});

    if (matching.length == 2)
    {
        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
        {
            cards[matching[0].id].isOpened = false;
            cards[matching[1].id].isOpened = false;
            this.setState({"cards": cards});                                
        }
    }   
}


Next, in the render() method, the cardDisplay component should add an extra CSS class if isOpened and isMatched are true. The CSS class is matched, and that's what we're going to create next.
var cardDisplay = this.state.cards.map(
    (item, key) =>
    {
        var template = "template" + item.template;
        var style = "card " + (item.isOpened ? "opened " + template + (item.isMatched ? " matched" : "") : "closed");

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


This one is simple. We'll just set brightness lower if the cards are matched, to visually differentiate them.
.template5:before
{
    background-image: url(easter5.jpg);
}

.matched:before
{
    filter: brightness(30%);
}   


Oh wow, did you see that? The matched cards are darker.


You may notice that it is devilishly hard to get a matched pair because when you open a second card and it doesn't match, both cards close straight away. This behavior is correct, but it needs to be improved via animation.

Next

Making the interface friendlier via animation, and some cleaning up.

Saturday, 4 April 2020

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

It's time for the annual Easter-themed web tutorial. And I've got a good one here for you today. It's an Easter-themed memory game, the kind you might have played as a kid. For this, we'll be using a crowd-pleaser... ReactJS!

This, of course, will require us to start with some boilerplate code.
<!DOCTYPE html>
<html>
    <head>
        <title>Easter Memory Game</title>
        <style>

        </style>

        <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
        <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
        <script src="https://unpkg.com/babel-standalone@6.26.0/babel.js"></script>
    </head>

    <body>
        <script type="text/babel">

        </script>
    </body>
</html>


Add this div, and style it using the CSS class preload. In it, we have image tags for all the six images we'll be using.
<!DOCTYPE html>
<html>
    <head>
        <title>Easter Memory Game</title>
        <style>

        </style>

        <script src="https://unpkg.com/react@16/umd/react.development.js"></script>
        <script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
        <script src="https://unpkg.com/babel-standalone@6.26.0/babel.js"></script>
    </head>

    <body>
        <div class="preload">
            <img src="easter0.jpg" />
            <img src="easter1.jpg" />
            <img src="easter2.jpg" />
            <img src="easter3.jpg" />
            <img src="easter4.jpg" />
            <img src="easter5.jpg" />
        </div>

        <script type="text/babel">

        </script>
    </body>
</html>


In the stylesheet, give all divs a red outline. Set the default font to Verdana and font size to 12 pixels. And hide the preload CSS class.
<style>
    div {outline: 1px solid #FF0000;}
  
    body
    {
        font-size: 12px;
        font-family: verdana;
    }

    .preload
    {
        display:none;
    }
</style>


The images in the div will not be visible. That's the entire point. We put them there so that the images will load normally when they're being used. It's a cheap low-tech preloading technique, but it works! These are the images in question.

easter0.jpg

easter1.jpg

easter2.jpg

easter3.jpg

easter4.jpg

easter5.jpg

Now let's add one more div in our app. It has an id of appContainer.
<div class="preload">
    <img src="easter0.jpg" />
    <img src="easter1.jpg" />
    <img src="easter2.jpg" />
    <img src="easter3.jpg" />
    <img src="easter4.jpg" />
    <img src="easter5.jpg" />
</div>

<div id="appContainer">

</div>

<script type="text/babel">

</script>


Here's the styling for it. It will be 500 by 700 pixels, and centered.
.preload
{
    display:none;
}

#appContainer
{  
    height: 700px;          
    width: 500px;
    margin: 5px auto 0 auto;
}


This is how we begin - with a tall box.


For the script, our component will be EasterMemoryGame. In it, I've added a constructor with a blank state, a componentDidMount() method for stuff we want to run upon loading, and the render() method. Pretty standard stuff.
<script type="text/babel">
    class EasterMemoryGame extends React.Component
    {
        constructor(props)
        {
            super(props);
            this.state =
            {

            };
        }

        componentDidMount()
        {

        }

        render()
        {
      
        }
    }

    ReactDOM.render(<EasterMemoryGame />, document.getElementById("appContainer"));
</script>


For the state, we need the property seconds, which will default to a value of 100. The second value, cards, is an array.
constructor(props)
{
    super(props);
    this.state =
    {
        "seconds": 100,
        "cards": []
    };
}


In the render() method, we will return this component within appContainer. There is a div with an id of easterMemoryGameContainer. In turn, it contains an id with a div of timeContainer, a div with an id of buttonContainer containing a button, and a div with an id of deckContainer.
render()
{
    return (
        <div id="easterMemoryGameContainer">
            <div id="timeContainer">
              
            </div>

            <div id="buttonContainer">
                <button></button>
            </div>

            <div id="deckContainer">
              
            </div>  
        </div>
    );
}


This won't look like much right now. We need to style it.


I won't spend a whole lot of time explaining the layout because it's really simple. easterMemoryGameContainer will fit the whole of appContainer. Wthin it, the three divs are meant to occupy a certain space in that one-column layout, so all have widths set to 100%.
<style>
    #easterMemoryGameContainer
    {  
        height: 100%;  
        width: 100%;
    }

    #timeContainer
    {          
        width: 100%;
    }

    #buttonContainer
    {              
        width: 100%;
    }

    #deckContainer
    {              
        width: 100%;
    }
</style>


These are the heights of each of the divs.
#easterMemoryGameContainer
{  
    height: 100%;  
    width: 100%;
}

#timeContainer
{  
    height: 150px;          
    width: 100%;
}

#buttonContainer
{  
    height: 50px;          
    width: 100%;
}

#deckContainer
{  
    height: 500px;          
    width: 100%;
}


Also, timeContainer and buttonContainer have text color set to black and aligned center.
#easterMemoryGameContainer
{  
    height: 100%;  
    width: 100%;
}

#timeContainer
{  
    height: 150px;          
    width: 100%;
    text-align: center;
    color: #000000;
}

#buttonContainer
{  
    height: 50px;          
    width: 100%;
    text-align: center;
    color: #000000;
}

#deckContainer
{  
    height: 500px;          
    width: 100%;
}


Now we're talking. Let's style the button next.


This is not really all that essential. It's mostly up to personal preferences. I've generally made the button orange with white text, with some changes upon a mouseover.
#buttonContainer
{  
    height: 50px;          
    width: 100%;
    text-align: center;
    color: #000000;
}

#buttonContainer button
{
    margin: 0.25em;
    width: 5em;
    height: 2.5em;
    text-align: center;
    border-radius: 5px;
    border: 0px solid #000000;
    background-color: rgba(255, 200, 0, 1);
    color: rgba(255, 255, 255, 1);
    font-weight: bold;
}

#buttonContainer button:hover
{
    background-color: rgba(255, 200, 0, 0.5);
    color: rgba(0, 0, 0, 1);
}

#deckContainer
{  
    height: 500px;          
    width: 100%;
}


Oh, there's no text in the button? That shouldn't surprise you at all. We never added any, after all. This is going to be the domain of the render() method.


In the render() method, the returned method will include the seconds property of this application's state, within another div.
render()
{
    return (
        <div id="easterMemoryGameContainer">
            <div id="timeContainer">
                <div>
                    <h1>{this.state.seconds}</h1>
                    seconds
                </div>
            </div>

            <div id="buttonContainer">
                <button></button>
            </div>

            <div id="deckContainer">
              
            </div>  
        </div>
    );
}


And since that value is set to 100, this is what you should see.


Declare two variables - message and buttonText.
render()
{
    var message;
    var buttonText;

    return (
        <div id="easterMemoryGameContainer">
            <div id="timeContainer">
                <div>
                    <h1>{this.state.seconds}</h1>
                    seconds
                </div>
            </div>

            <div id="buttonContainer">
                <button></button>
            </div>

            <div id="deckContainer">
              
            </div>  
        </div>
    );
}


Next, create an If block for the condition that seconds is 0. Add an Else block.
render()
{
    var message;
    var buttonText;
  
    if (this.state.seconds == 0)
    {

    }
    else
    {

    }

    return (
        <div id="easterMemoryGameContainer">
            <div id="timeContainer">
                <div>
                    <h1>{this.state.seconds}</h1>
                    seconds
                </div>
            </div>

            <div id="buttonContainer">
                <button></button>
            </div>

            <div id="deckContainer">
              
            </div>  
        </div>
    );
}


Set message and buttonText accordingly. Here's what we're trying to accomplish - the user is given 100 seconds to finish the game. As long as seconds is not zero, message should show "TIME ELAPSED: " and the button acts as a Reset button. Once seconds reaches 0, message should tell the user that the game is over, and the button acts as a Replay button. In reality, the button's functionality doesn't change, but it makes for a more friendly UI.
render()
{
    var message;
    var buttonText;
  
    if (this.state.seconds == 0)
    {
        message = "GAME OVER!";
        buttonText = "REPLAY";
    }
    else
    {
        message = "TIME ELAPSED: ";
        buttonText = "RESET";      
    }

    return (
        <div id="easterMemoryGameContainer">
            <div id="timeContainer">
                <div>
                    <h1>{this.state.seconds}</h1>
                    seconds
                </div>
            </div>

            <div id="buttonContainer">
                <button></button>
            </div>

            <div id="deckContainer">
              
            </div>  
        </div>
    );
}


Add the message and buttonText template strings.
return (
    <div id="easterMemoryGameContainer">
        <div id="timeContainer">
            <div>
                <br />{message}
                <h1>{this.state.seconds}</h1>
                seconds
            </div>
        </div>

        <div id="buttonContainer">
            <button>{buttonText}</button>
        </div>

        <div id="deckContainer">

        </div>  
    </div>
);


You should see that your button now has text!

Timer Functions

It's time to move on to actually making the timer move. In the componentDidMount() method, add a call to the reset() method. This means that reset() will be called as soon as the component loads.
componentDidMount()
{
    this.reset();
}


Create the reset() method and add calls to the stopTimer() method and the startTimer() method. Create those methods.
stopTimer()
{

}

startTimer()
{

}

reset()
{
    this.stopTimer();

    this.startTimer();
}

componentDidMount()
{
    this.reset();
}


In the stopTimer() method, run the clearInterval() function with the app's built-in interval property as an argument. Then set interval to undefined.
stopTimer()
{
    clearInterval(this.interval);
    this.interval = undefined;
}


First, check if interval is undefined. It should be (either reset in stopTimer() or as a default value, but no harm being careful.
startTimer()
{
    if (this.interval == undefined)
    {
                      
    }
}


Then use interval to run the setInterval() method. Here, we're using the ECMAScript's Fat Arrow Notation convention. The interval is one second.
startTimer()
{
    if (this.interval == undefined)
    {
        this.interval = setInterval
        (
            () =>
            {

            },
            1000
        );                      
    }
}


Decrement the seconds property of the application state. If this means that seconds is now 0, run the stopTimer() method.
this.interval = setInterval
(
    () =>
    {
        this.setState({"seconds": this.state.seconds - 1});

        if (this.state.seconds == 0)
        {
            this.stopTimer();
        }
    },
    1000
);                      


Now when you refresh, you can see the number running down.


And when it reaches 0, you can see that the message and button text have changed.


The Cards

Now we get to the mest of the application - the cards. Here's some styling for cardContainer. Our aim is to have a 6 by 6 grid of cards fitting into the deckContainer div. So if deckContainer is 500 pixels width (because it takes up 100% of appContainer's 500 pixel width), divide that by 6 and you get 83 with some left over. So 72 pixels height and width, with a 10 pixel margin to the top and left, sounds reasonable. Float everything left.
#deckContainer
{  
    height: 500px;          
    width: 100%;
}  

.cardContainer
{  
    height: 72px;
    width: 72px;
    margin-left: 10px;
    margin-top: 10px;
    float: left;
}


card is another CSS class which will be nested within cardContainer. It will take up all of its parent's height and width, with rounded corners. Because it is clickable, the cursor property has been set to pointer. The overflow property is set to hidden because we're going to have stuff nested that may exceed the boundaries of this CSS class.
.cardContainer
{  
    height: 72px;
    width: 72px;
    margin-left: 10px;
    margin-top: 10px;
    float: left;
}

.card
{
    height: 100%;
    width: 100%;
    border-radius: 5px;  
    overflow: hidden;          
    cursor: pointer;
}


Our next piece of work will be done at the reset() method. First, declare an empty array, cards. Then set the seconds property of the state to 100, and the cards property to the array you just declared. This method is meant to reset, and that means these are the values at the beginning of every game.
reset()
{
    this.stopTimer();

    var cards = [];

    this.setState({"seconds": 100, "cards": cards});
  
    this.startTimer();
}


Our intention, remember, is to have a 6 by 6 grid of cards. For that, we'll have 6 templates. Declare the variable totalTemplates and set that to 6. Then declare totalCards and set the value to that of totalTemplates squared.
reset()
{
    this.stopTimer();

    var cards = [];
    var totalTemplates = 6;
    var totalCards = totalTemplates * totalTemplates;

    this.setState({"seconds": 100, "cards": cards});
  
    this.startTimer();
}


Now implement a For loop to iterate through totalCards.
reset()
{
    this.stopTimer();

    var cards = [];
    var totalTemplates = 6;
    var totalCards = totalTemplates * totalTemplates;

    for(var i = 0; i < totalCards; i++)
    {

    }

    this.setState({"seconds": 100, "cards": cards});
  
    this.startTimer();
}


Within that loop, declare card as an object that has the properties id (not strictly necessary, but good to have) and isOpened, which is a boolean value telling us if the card is opened (true) or closed (false). By default, it's false. Then add card into the cards array with the push() method.
reset()
{
    this.stopTimer();

    var cards = [];
    var totalTemplates = 6;
    var totalCards = totalTemplates * totalTemplates;

    for(var i = 0; i < totalCards; i++)
    {
        var card =
        {
            id: i,
            isOpened: false,
        }

        cards.push(card);
    }

    this.setState({"seconds": 100, "cards": cards});
  
    this.startTimer();
}


Now that's done, let's move on to the render() method. Before declaring message and buttonText, declare cardDisplay. It will be a HTML component generated using the map() method applied to the cards array in state which we've just updated with a 36-element grid!
render()
{
    var cardDisplay = this.state.cards.map(
        (item, key) =>
        {

        }
    );

    var message;
    var buttonText;


Declare style. It's "card " with a space at the end, and either "opened " (again with a space) or "closed" depending on the value of the isOpened property.
var cardDisplay = this.state.cards.map(
    (item, key) =>
    {
        var style = "card " + (item.isOpened ? "opened " : "closed");
    }
);


Then return a div styled using cardContainer. We add a key so as to prevent any complaints from the transpiler. Nested within is another div, this one styled using style.
var cardDisplay = this.state.cards.map(
    (item, key) =>
    {
        var style = "card " + (item.isOpened ? "opened " : "closed");

        return (
            <div className="cardContainer" key={key}>
                <div className={style}>                              
                </div>
            </div>
        );
    }
);


Then in the return statement of the render() method, add cardDisplay to the template returned.
return (
    <div id="easterMemoryGameContainer">
        <div id="timeContainer">
            <div>
                <br />{message}
                <h1>{this.state.seconds}</h1>
                seconds
            </div>
        </div>

        <div id="buttonContainer">
            <button>{buttonText}</button>
        </div>

        <div id="deckContainer">
            {cardDisplay}
        </div>  
    </div>
);


Here you can see the grid you just made! The squares are blank, and that's because we have not yet created the CSS classes for opened or closed.


Let's start by specifying the CSS class closed. We'll use the before pseudoselector for this because we want to make use of the content property. Text is set to white and background is orange. Get creative!

The display property is block, of cours, and we'll make it take up full height and width. Since the parent, card, has overflow set to hidden, its round corners will be aptly shown.

Finally, for content, we have a line of text followed by a break and the HTML symbol of a cross. The white-space property is set to pre-wrap because we want the browser to preserve white spaces for this CSS class and break on line breaks.
.card
{
    height: 100%;
    width: 100%;
    border-radius: 5px;  
    overflow: hidden;          
    cursor: pointer;
}

.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;
}


For opened, it's pretty much the same, only there's no content.
.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;
}

.opened:before
{
    display: block;
    content: "";
    height: 100%;  
    width: 100%;
}


Also, at this point, you can get rid of the red lines. They're no longer needed.
div {outline: 0px solid #FF0000;}


Now refresh your browser... and see what you've just made! It's a little tacky, sure, what what the hell, right?


If you want to see what opened looks like, just do this, and refresh. You should see the entire grid disappear because no content has been rendered yet for opened, plus we got rid of the red lines.
for(var i = 0; i < totalCards; i++)
{
    var card =
    {
        id: i,
        isOpened: true,
    }

    cards.push(card);
}


Next

No sweat, we're just getting started. Next up, we will focus on game logic and handle opening and closing of cards.