Sunday 8 November 2020

Web Tutorial: Word Search Game (Part 1/4)

Today's web tutorial is going to be one of those long ones. It's a browser-based children's game... or at least it would have been in my time. I have no darn clue what kids play these days.

This will be a HTML/CSS/JavaScript project. In fact, I will be using jQuery for this because otherwise it's just too much effort. For this, we will display on screen a list of words to be discovered inside a grid of jumbled-up letters. There will be a time limit imposed, with the player expected to find all the words before the timer reaches zero.

Fun fact; I did something like this for a company I worked in. Only my efforts back then were a bit more rudimentary than this. Hopefully, I've improved as a programmer and the code will reflect this.

Let's have some boilerplate HTML.
<!DOCTYPE html>
<html>
    <head>
        <title>Word Search</title>

        <style>

        </style>

        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>

        <script>

        </script>
    </head>

    <body>

    </body>
</html>


Set all divs to have a red outline. Set the font as well.
<!DOCTYPE html>
<html>
    <head>
        <title>Word Search</title>

        <style>
            div {outline: 1px solid #FF0000;}

            body
            {
                font-size: 12px;
                font-family: verdana;
            }

        </style>

        <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.3/jquery.min.js"></script>
        
        <script>

        </script>
    </head>

    <body>

    </body>
</html>


The layout will be divided into four main areas - the dashboard, the word grid, the theme details and the score. Thus, we have a div with the CSS class container, and four divs within it, styled using dashboard, wordgrid, theme and score.
<body>
    <div class="container">
        <div class="dashboard">

        </div>

        <div class="wordgrid">

        </div>

        <div class="theme">

        </div>

        <div class="score">

        </div>

    </div>
</body>


Here's the styling for the four main areas. container has a width of 800 pixels, and will be set in the middle of the screen.
<style>
    div {outline: 1px solid #FF0000;}

    body
    {
        font-size: 12px;
        font-family: verdana;
    }

    .container
    {
        width: 800px;
        margin: 0 auto 0 auto;
    }

</style>


dashboard takes up 250 pixels width and 350 pixels height, and we float it left.
<style>
    div {outline: 1px solid #FF0000;}

    body
    {
        font-size: 12px;
        font-family: verdana;
    }

    .container
    {
        width: 800px;
        margin: 0 auto 0 auto;
    }

    .dashboard
    {
        width: 250px;
        height: 350px;
        float: left;
    }

</style>


wordgrid, on the other hand, is a 500 by 500 pixel square, floated right.
<style>
    div {outline: 1px solid #FF0000;}

    body
    {
        font-size: 12px;
        font-family: verdana;
    }

    .container
    {
        width: 800px;
        margin: 0 auto 0 auto;
    }

    .dashboard
    {
        width: 250px;
        height: 350px;
        float: left;
    }

    .wordgrid
    {
        width: 500px;
        height: 500px;
        float: right;
    }

</style>


Following so far?


Here, we set the width and height of theme, and float it left.
<style>
    div {outline: 1px solid #FF0000;}

    body
    {
        font-size: 12px;
        font-family: verdana;
    }

    .container
    {
        width: 800px;
        margin: 0 auto 0 auto;
    }

    .dashboard
    {
        width: 250px;
        height: 350px;
        float: left;
    }

    .wordgrid
    {
        width: 500px;
        height: 500px;
        float: right;
    }

    .theme
    {
        width: 250px;
        height: 100px;
        float: left;
        margin-top: 20px;
    }

</style>


Same for score.
<style>
    div {outline: 1px solid #FF0000;}

    body
    {
        font-size: 12px;
        font-family: verdana;
    }

    .container
    {
        width: 800px;
        margin: 0 auto 0 auto;
    }

    .dashboard
    {
        width: 250px;
        height: 350px;
        float: left;
    }

    .wordgrid
    {
        width: 500px;
        height: 500px;
        float: right;
    }

    .theme
    {
        width: 250px;
        height: 100px;
        float: left;
        margin-top: 20px;
    }

    .score
    {
        width: 250px;
        height: 100px;
        float: left;
    }

</style>


And there's our layout.



For the dashboard, we want a title and a timer. Add the divs to the HTML.
<div class="dashboard">
    <div class="title">
                
    </div>
        
    <div class="timer">

    </div>

</div>


Within the div styled using title, we add a h1 tag, a div for messages, and a button. The button should call the begin() method of the puzzle object. We will build that later.
<div class="dashboard">
    <div class="title">
        <h1>Word Search</h1>

        <div class="message">

        </div>    

        <button id="btnGo" onclick = "puzzle.begin();"></button>  
             
    </div>
        
    <div class="timer">

    </div>
</div>


Within the div styled using timer, we add a label and a placeholder div for the seconds remaining. Let's just put in a value of 0 for now.
<div class="dashboard">
    <div class="title">
        <h1>Word Search</h1>

        <div class="message">

        </div>    

        <button id="btnGo" onclick = "puzzle.begin();"></button>                
    </div>
        
    <div class="timer">
        <small>Time left</small>
        <div class="seconds">0</div>

    </div>
</div>


Time to style the dashboard. All this is a matter of aesthetics mostly, so do what makes sense to you.
.dashboard
{
    width: 250px;
    height: 350px;
    float: left;
}

.dashboard .title
{
    width: 100%;
    height: 150px;
    font-weight: bold;
    text-align: center;
}

.dashboard .timer
{
    width: 100%;
    height: 150px;
    font-weight: bold;
    text-align: center;
    margin-top: 20px;
}

.dashboard .seconds
{
    width: 100%;
    font-size: 4em;
}

button
{
    width: 1.5em;
    height: 1.5em;
    font-size: 3em;
    background-color: rgba(255, 200, 0, 1);
    color: rgba(255, 255, 255, 1);
    border-radius: 50%;
    border: 0px solid red;
    cursor: pointer;
    margin: 10 auto 10px auto;
    clear: both;
}

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

.dashboard .message
{
    width: 100%;
    height: 50px;
    float: left;
}


.wordgrid
{
    width: 500px;
    height: 500px;
    float: right;
}


Ta-daaaa....



Now let's provide some content for theme. Basically, we have three divs in there, styled using title, description and picture.
<div class="theme">
    <div class="title">

    </div>

    <div class="description">

    </div>

    <div class="picture">

    </div>

</div>

<div class="score">

</div>


And here's the styling. Again, it's mostly aesthetic choice here. But for picture, we'll want to set the width and height to 100 pixels, and the background properties to use a 100 by 100 pixel non-repeating image. We can deal with those images later.
.theme
{
    width: 250px;
    height: 100px;
    float: left;
    margin-top: 20px;
}

.theme .title
{
    width: 140px;
    float: right;
    font-size: 2em;    
    font-weight: bold;
}

.theme .description
{
    width: 140px;
    float: right;
}

.theme .picture
{
    width: 100px;
    height: 100px;
    float: left;
    margin-top: -40px;
    background-repeat: no-repeat;
    background-position: 50% 50%;
}


.score
{
    width: 250px;
    height: 100px;
    float: left;
}


score is a placeholder for the score meter, whose display changes depending on the completion percentage. To that end, we have a div styled using meter, and a div nested within that, styled using fill.
<div class="theme">
    <div class="title">

    </div>

    <div class="description">

    </div>

    <div class="picture">

    </div>
</div>

<div class="score">
    <div class="meter">
        <div class="fill"></div>
    </div>

</div>


Here's the styling. meter's background color is a dull orange. It has rounded corners and is long, thin and laid out horizontally. fill takes up all of its parent's height and width. There will be text in there, and it's white and aligned right to contrast nicely with the background.
.score
{
    width: 250px;
    height: 100px;
    float: left;
}

.score .meter
{
    width: 100%;
    height: 20px;
    border-radius: 5px;
    background-color: rgba(255, 200, 0, 0.3);
    overflow: hidden;
}

.score .meter .fill
{
    width: 100%;
    height: 100%;
    color: rgba(255, 255, 255, 1);
    text-align: right;
    font-weight: bold;
}


This is what you'll be seeing so far. That's the layout, mostly.



Time to prep the JavaScript...

You'll need two things - the themes array and the puzzle object. themes will hold data, while puzzle will hold logic.
<script>
    let themes =
    [

    ];

    let puzzle =
    {

    };
</script>


For themes, we need an array of objects. We will just put in one element for now, with title, picture and description as strings. I've already populated the values.
let themes =
[
    {
        title: "Sea Life",
        picture: "sealife",
        description: "Look for all life you can find in the oceans."
    }

];

let puzzle =
{

};


We'll also need words, an array of all the words to be found under that theme. Again, I've populated the array with samples. Each word should be five to twelve characters in length. Avoid plurals.
let themes =
[
    {
        title: "Sea Life",
        picture: "sealife",
        description: "Look for all life you can find in the oceans.",
        words: ["shark", "coral", "whale", "starfish", "jellyfish", "turtle", "anemone", "octopus", "swordfish", "squid", "lobster", "seahorse", "walrus", "lamprey", "prawn", "oyster", "barracuda", "mackerel", "clownfish", "plankton"]

    }
];

let puzzle =
{

};


For the logic in puzzle, we want some properties. timer is set to undefined and will control the timer. seconds is initialized to 0 and is an integer that represents the number of seconds left. maxSize is an integer that defines how many letters across and downwards the grid will have, at most. maxWords is the total number of words hidden within the grid at any one time. squares and words are arrays. squares keeps track of each item in the grid, while words is a list of the hidden words. Lastly, themeIndex is initialized to 0, and is the current pointer to the themes object.
let puzzle =
{
    timer: undefined,
    seconds: 0,
    maxSize: 20,
    maxWords: 10,
    squares: [],
    words: [],
    themeIndex: 0

};


Got all that? Great! Now let's define a few methods. begin() is what happens when you start a new game. In fact, after the puzzle object, I want to create a function call to activate begin() once the document has been loaded. Remember that this method is also called when the button is clicked?
let puzzle =
{
    timer: undefined,
    seconds: 0,
    maxSize: 20,
    maxWords: 10,
    squares: [],
    words: [],
    themeIndex: 0,
    begin: function ()
    {

    }

};

$(document).ready
(
    () =>
    {
        puzzle.begin();
    }
);


begin() should get a theme. For this, we use the method generateRandomNo(), passing in the total number of elements in themes, and set themeIndex to the returned value.
begin: function ()
{
    this.themeIndex = this.generateRandomNo(themes.length);
}


This is the generateRandomNo() method. It accepts a parameter, max, and returns a number from 0 to max.
begin: function ()
{
    this.themeIndex = this.generateRandomNo(themes.length);
},
generateRandomNo: function (max)
{
    return Math.floor((Math.random() * (max)));
},


This won't make a difference right now because we only have one element in themes, so no matter what happens, only that element will be returned. But it will be useful when we start adding elements later on. Once we have themeIndex, we can run the displayTheme() method.
begin: function ()
{
    this.themeIndex = this.generateRandomNo(themes.length);
    this.displayTheme();
},
generateRandomNo: function (max)
{
    return Math.floor((Math.random() * (max)));
},


This is the displayTheme() method. You'll populate the title, description and image.
generateRandomNo: function (max)
{
    return Math.floor((Math.random() * (max)));
},
displayTheme: function ()
{
    $(".theme .title").html(themes[this.themeIndex].title);
    $(".theme .description").html(themes[this.themeIndex].description);
    $(".theme .picture").attr("style", "background-image:url(" + themes[this.themeIndex].picture + ".png)");
}    


Oops, I almost forgot to mention - I've prepared some images for this, all PNG files. For this particular case, it's sealife.png. I can't be arsed to save them to a different directory, but do what you feel you need to do, and make the adjustments accordingly.
sealife.png


There you go; theme displayed!



begin() will also set the button text and message...
begin: function ()
{
    this.themeIndex = this.generateRandomNo(themes.length);
    this.displayTheme();
    $(".dashboard .message").html("Search for words both horizontal and vertical. Each word is five letters and longer.");
    $("#btnGo").html("&#9654;");

},


Nicely done!



begin() will also reset the seconds property and start decrementing. We will use 300 seconds as a benchmark. We should really put that figure as a configurable property, but it's only set here and I don't want to go to the trouble, so...
begin: function ()
{
    this.themeIndex = this.generateRandomNo(themes.length);
    this.displayTheme();
    this.seconds = 300;
    this.decrementCounter();

    $(".dashboard .message").html("Search for words both horizontal and vertical. Each word is five letters and longer.");
    $("#btnGo").html("&#9654;");
},


This is the decrementCounter() method. Here, we only take action if timer is undefined.
begin: function ()
{
    this.themeIndex = this.generateRandomNo(themes.length);
    this.displayTheme();
    this.seconds = 300;
    this.decrementCounter();
    $(".dashboard .message").html("Search for words both horizontal and vertical. Each word is five letters and longer.");
    $("#btnGo").html("&#9654;");
},
decrementCounter: function ()
{
    if (this.timer == undefined)
    {

    }
},


Then we use timer to call the setInterval() function, repeating the action every second.
decrementCounter: function ()
{
    if (this.timer == undefined)
    {
        this.timer = setInterval
        (
            () =>
            {
                        
            }
            ,1000
        )

    }
},


If seconds is more than 0, decrement seconds.
decrementCounter: function ()
{
    if (this.timer == undefined)
    {
        this.timer = setInterval
        (
            () =>
            {
                if (this.seconds > 0)
                {
                    this.seconds = this.seconds - 1;
                }
                         
            }
            ,1000
        )
    }
},


Otherwise, run the clearInterval() function, reset timer to undefined and set the message and button text.
decrementCounter: function ()
{
    if (this.timer == undefined)
    {
        this.timer = setInterval
        (
            () =>
            {
                if (this.seconds > 0)
                {
                    this.seconds = this.seconds - 1;
                }
                else
                {
                    clearInterval(this.timer);
                    this.timer = undefined;    
                    $(".dashboard .message").html("Time's up! Better luck next time!");
                    $("#btnGo").html("&#8634;");

                }
            }
            ,1000
        )
    }
},


Whatever happens, we want the timer on the dashboard to reflect the number of seconds left.
decrementCounter: function ()
{
    if (this.timer == undefined)
    {
        this.timer = setInterval
        (
            () =>
            {
                if (this.seconds > 0)
                {
                    this.seconds = this.seconds - 1;
                }
                else
                {
                    clearInterval(this.timer);
                    this.timer = undefined;    
                    $(".dashboard .message").html("Time's up! Better luck next time!");
                    $("#btnGo").html("&#8634;");
                }

                $(".dashboard .timer .seconds").html(this.seconds);                            
            }
            ,1000
        )
    }
},


The clock is ticking! And if you click on the orange button, the clock should restart.



And if you let it run down to 0, this should happen!



Next

We will start implementing the logic of hiding letters in the grid.


No comments:

Post a Comment