Wednesday, 11 November 2020

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

In the previous part, we did quite a fair bit. For today's part, it's a smaller but just as vital piece of the puzzle. We will be hiding the words within the grid. There's quite a bit of logic that goes into this, so bear with me!

We'll start off with something simple. Just before displaying the theme, call the assignWords() method.
begin: function ()
{
    this.themeIndex = this.generateRandomNo(themes.length);
    this.assignWords();
    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("↺");
},


Create the method. Grab themeIndex, and set words to an empty array. Declare variable possibleWordList and set it to the words array of the element in themes pointed to by themeIndex. There's only one possible value right now, so it should always return the list of sea creature names.
begin: function ()
{
    this.themeIndex = this.generateRandomNo(themes.length);
    this.assignWords();
    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("↺");
},
assignWords: function()
{
    themeIndex = this.themeIndex;
    this.words = [];

    var possibleWordList = themes[themeIndex].words;
},    
            
decrementCounter: function ()
{


And while words has less elements than maxWords...
assignWords: function()
{
    themeIndex = this.themeIndex;
    this.words = [];

    var possibleWordList = themes[themeIndex].words;

    while (this.words.length < this.maxWords)
    {

    }

},


Grab a random element from possibleWordList.
assignWords: function()
{
    themeIndex = this.themeIndex;
    this.words = [];

    var possibleWordList = themes[themeIndex].words;

    while (this.words.length < this.maxWords)
    {
        var assignedWord = possibleWordList[this.generateRandomNo(possibleWordList.length)];
    }
},


Push that word into words, encapsulating it into an object, along with a property, assigned, which is false. Then remove that word from possibleWordList using the filter() method.

What we've basically done is - create a list of all possible words, grab words randomly from the list to populate the words array with, deleting that word from the list of possible words as we do. So the list gets shorter each time and we don't get repeats.
assignWords: function()
{
    themeIndex = this.themeIndex;
    this.words = [];

    var possibleWordList = themes[themeIndex].words;

    while (this.words.length < this.maxWords)
    {
        var assignedWord = possibleWordList[this.generateRandomNo(possibleWordList.length)];

        this.words.push({word: assignedWord, assigned: false});
        possibleWordList = possibleWordList.filter((x) => { return x != assignedWord; });

    }
},


Now call the populateGrid() method.
begin: function ()
{
    this.themeIndex = this.generateRandomNo(themes.length);
    this.assignWords();
    this.displayTheme();
    this.populateGrid();
    this.seconds = 300;
    this.decrementCounter();
    $(".dashboard .message").html("Search for words both horizontal and vertical. Each word is five letters and longer.");
    $("#btnGo").html("&#8634;");
},


populateGrid() is a method that takes words from the words array (which we just populated) and places them randomly in the grid. Right now the div styled by wordgrid is totally empty. We should make sure of that by wiping it clean like this.
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("&#9654;");
                }

                $(".dashboard .timer .seconds").html(this.seconds);                            
            }
            ,1000
        )
    }
},
populateGrid: function ()
{
    $(".wordgrid").html("");
},

generateRandomNo: function (max)
{
    return Math.floor((Math.random() * (max)));
},


And then set squares to an empty array.
populateGrid: function ()
{
    $(".wordgrid").html("");
    this.squares = [];
},


We want to make a grid of squares, specifically, maxSize squares by maxSize squares. So what we do is iterate from 0 to maxSize, and make maxSize empty elements in squares.
populateGrid: function ()
{
    $(".wordgrid").html("");
    this.squares = [];

    for (var x = 0; x < this.maxSize; x++)
    {
        this.squares[x] = [];    
    }
},


Then we again iterate from 0 to maxSize, and, using the push() method, set each array in squares to be an array of objects. Each object will have these properties as shown. I'll explain what they're for later on.
populateGrid: function ()
{
    $(".wordgrid").html("");
    this.squares = [];

    for (var x = 0; x < this.maxSize; x++)
    {
        this.squares[x] = [];

        for (var y = 0; y < this.maxSize; y++)
        {
            this.squares[x].push
            (
                {
                    letter: "",
                    selected: false,
                    correct: false,
                    dir: undefined,
                    intersected: false
                }
            );
        } 
   
    }
},


And for each array object, we create a div, and append some useful data in it. We also style it using the CSS class letter.
populateGrid: function ()
{
    $(".wordgrid").html("");
    this.squares = [];

    for (var x = 0; x < this.maxSize; x++)
    {
        this.squares[x] = [];

        for (var y = 0; y < this.maxSize; y++)
        {
            this.squares[x].push
            (
                {
                    letter: "",
                    selected: false,
                    correct: false,
                    dir: undefined,
                    intersected: false
                }
            );

            var square = $("<div><div>");
            square.attr("data-x", x);
            square.attr("data-y", y);
            square.addClass("letter");

        }    
    }
},


And then we set a click event, which, for now, will do absolutely nothing.
populateGrid: function ()
{
    $(".wordgrid").html("");
    this.squares = [];

    for (var x = 0; x < this.maxSize; x++)
    {
        this.squares[x] = [];

        for (var y = 0; y < this.maxSize; y++)
        {
            this.squares[x].push
            (
                {
                    letter: "",
                    selected: false,
                    correct: false,
                    dir: undefined,
                    intersected: false
                }
            );

            var square = $("<div><div>");
            square.attr("data-x", x);
            square.attr("data-y", y);
            square.addClass("letter");
            square.click
            (
                (e) =>
                {

                }
            );

        }    
    }
},


After all that, we then append the element to the div.
populateGrid: function ()
{
    $(".wordgrid").html("");
    this.squares = [];

    for (var x = 0; x < this.maxSize; x++)
    {
        this.squares[x] = [];

        for (var y = 0; y < this.maxSize; y++)
        {
            this.squares[x].push
            (
                {
                    letter: "",
                    selected: false,
                    correct: false,
                    dir: undefined,
                    intersected: false
                }
            );

            var square = $("<div><div>");
            square.attr("data-x", x);
            square.attr("data-y", y);
            square.addClass("letter");
            square.click
            (
                (e) =>
                {

                }
            );

            $(".wordgrid").append(square);
        }    
    }
},


This is the letter CSS class. Width and height have been set to 4%, with some margins. We will float this left. These are the only things necessary for the layout to work; the rest of the properties I'll leave up to your personal taste.
.score .meter .fill
{
    width: 100%;
    height: 100%;
    color: rgba(255, 255, 255, 1);
    text-align: right;
    font-weight: bold;
}

.letter
{
    width: 4%;
    height: 4%;
    margin: 1% 1% 0 0;
    float: left;
    color: rgba(255, 200, 0, 1);
    cursor: pointer;
    text-align: center;
    font-size: 1.2em;
    font-weight: bold;
    text-transform: uppercase;
    border-radius: 50%;
}


We have a grid!



Now let's start placing the letters. Declare unassigned, an empty array, and i.
populateGrid: function ()
{
    $(".wordgrid").html("");
    this.squares = [];

    for (var x = 0; x < this.maxSize; x++)
    {
        this.squares[x] = [];

        for (var y = 0; y < this.maxSize; y++)
        {
            this.squares[x].push
            (
                {
                    letter: "",
                    selected: false,
                    correct: false,
                    dir: undefined,
                    intersected: false
                }
            );

            var square = $("<div><div>");
            square.attr("data-x", x);
            square.attr("data-y", y);
            square.addClass("letter");
            square.click
            (
                (e) =>
                {

                }
            );

            $(".wordgrid").append(square);
        }    
    }

    var unassigned = [];
    var i;    

},


The next operation will be carried out, and repeated as long as there are elements in unassigned.
var unassigned = [];
var i;
do
{

}
while (unassigned.length > 0);
    


The method populateGridWithWords() will be called, and after that, we set unassigned to the filtered words array, getting all the elements where assigned is false. Remember the assigned property in words we defaulted to false earlier?

This ensures that all the words in words are assigned before the script can proceed.
var unassigned = [];
var i;
do
{
    this.populateGridWithWords();

    unassigned = this.words.filter((x) => {return !x.assigned;});

}
while (unassigned.length > 0);


And of course, we'll want to create populateGridWithWords(). We start off the method by running a nested For loop to "clean" the data. Basically reset both the squares array and the grid to an initial state. This is important because if this method is being run, not all words have been assigned and we want to start the operation over.
populateGrid: function ()
{
    $(".wordgrid").html("");
    this.squares = [];

    for (var x = 0; x < this.maxSize; x++)
    {
        this.squares[x] = [];

        for (var y = 0; y < this.maxSize; y++)
        {
            this.squares[x].push
            (
                {
                    letter: "",
                    selected: false,
                    correct: false,
                    dir: undefined,
                    intersected: false
                }
            );

            var square = $("<div><div>");
            square.attr("data-x", x);
            square.attr("data-y", y);
            square.addClass("letter");
            square.click
            (
                (e) =>
                {

                }
            );

            $(".wordgrid").append(square);
        }    
    }

    var unassigned = [];
    var i;
    do
    {
        this.populateGridWithWords();

        unassigned = this.words.filter((x) => {return !x.assigned;});
    }
    while (unassigned.length > 0);    
},
populateGridWithWords: function()
{
    for (var x = 0; x < this.maxSize; x++)
    {
        for (var y = 0; y < this.maxSize; y++)
        {
            this.squares[x][y].letter = "";
            this.squares[x][y].correct = false;
            this.squares[x][y].dir = undefined;
            this.squares[x][y].intersected = false;
            $(".letter[data-x=" + x + "][data-y=" + y + "]").html("");
        }    
    }
},

generateRandomNo: function (max)
{
    return Math.floor((Math.random() * (max)));
},


Now iterate through the words array and set all assigned properties to false. Declare variable word, and set it to the current element's word property. Also declare possibleSlots, an empty array.
populateGridWithWords: function()
{
    for (var x = 0; x < this.maxSize; x++)
    {
        for (var y = 0; y < this.maxSize; y++)
        {
            this.squares[x][y].letter = "";
            this.squares[x][y].correct = false;
            this.squares[x][y].dir = undefined;
            this.squares[x][y].intersected = false;
            $(".letter[data-x=" + x + "][data-y=" + y + "]").html("");
        }    
    }

    for (var i = 0; i < this.words.length; i++)
    {
        this.words[i].assigned = false;

        var word = this.words[i].word;
        var possibleSlots = [];
    }

},


Use a nested For loop to go through the entire grid.
for (var i = 0; i < this.words.length; i++)
{
    this.words[i].assigned = false;

    var word = this.words[i].word;
    var possibleSlots = [];

    for (var x = 0; x < this.maxSize; x++)
    {
        for (var y = 0; y < this.maxSize; y++)
        {
                        
        }    
    }  
                     
}


Here, we use the isWordSpaceAvailable() method, which we will create after this. The x and y coordinates (supplied by the nested For loop), the direction variable dir (0 for horizontal and 1 for vertical) and word are passed in as arguments. If the script finds either a horizontal or vertical space, it pushes an object into possible slots. If there are no elements in possibleSlots after this, the entire operation is a failure and we use the return statement to abort it, and retry.
for (var i = 0; i < this.words.length; i++)
{
    this.words[i].assigned = false;

    var word = this.words[i].word;
    var possibleSlots = [];

    for (var x = 0; x < this.maxSize; x++)
    {
        for (var y = 0; y < this.maxSize; y++)
        {
            if (this.isWordSpaceAvailable(x, y, 0, word))
            {
                possibleSlots.push({x: x, y: y, dir: 0});
            }

            if (this.isWordSpaceAvailable(x, y, 1, word))
            {
                possibleSlots.push({x: x, y: y, dir: 1});
            }    

            if (possibleSlots.length == 0) return;  
                         
        }    
    }                        
}


Next, we declare variables slot and slotIndex. slotIndex is initialized as 0. Then possibleSlots_intersected is declared. It's an array of all elements in squares that aren't empty strings, as pointed to by the x and y properties in possibleSlots. possibleSlots here means places in the grid which the word can begin in - the x and y coordinates, and direction.
for (var i = 0; i < this.words.length; i++)
{
    this.words[i].assigned = false;

    var word = this.words[i].word;
    var possibleSlots = [];

    for (var x = 0; x < this.maxSize; x++)
    {
        for (var y = 0; y < this.maxSize; y++)
        {
            if (this.isWordSpaceAvailable(x, y, 0, word))
            {
                possibleSlots.push({x: x, y: y, dir: 0});
            }

            if (this.isWordSpaceAvailable(x, y, 1, word))
            {
                possibleSlots.push({x: x, y: y, dir: 1});
            }    

            if (possibleSlots.length == 0) return;                            
        }    
    }    

    var slotIndex = 0;
    var slot;

    var possibleSlots_intersected = possibleSlots.filter((s) => {return this.squares[s.x][s.y].letter != "";});    
            
}


This is because we want the words to possibly cross each other, like so. possibleSlots_intersected will provide a list of possible slots that are open to two words sharing a letter space.



So here, a conditional checks if possibleSlots_intersected is empty. If it is not, then that means there are possible places for intersection. We'll use possibleSlots_intersected and the generateRandomNo() method to derive slot.
for (var i = 0; i < this.words.length; i++)
{
    this.words[i].assigned = false;

    var word = this.words[i].word;
    var possibleSlots = [];

    for (var x = 0; x < this.maxSize; x++)
    {
        for (var y = 0; y < this.maxSize; y++)
        {
            if (this.isWordSpaceAvailable(x, y, 0, word))
            {
                possibleSlots.push({x: x, y: y, dir: 0});
            }

            if (this.isWordSpaceAvailable(x, y, 1, word))
            {
                possibleSlots.push({x: x, y: y, dir: 1});
            }    

            if (possibleSlots.length == 0) return;                            
        }    
    }    

    var slotIndex = 0;
    var slot;

    var possibleSlots_intersected = possibleSlots.filter((s) => {return this.squares[s.x][s.y].letter != "";});                

    if (possibleSlots_intersected.length == 0)
    {

    }
    else
    {
        slotIndex = this.generateRandomNo(possibleSlots_intersected.length);
        slot = possibleSlots_intersected[slotIndex];
    }    

}


If there are no possible starting slots for intersection, we use possibleSlots instead to derive slot. And then we set that square's intersected property to true. Why, you might ask? It's a wee bit tricky, but trust me, it's necessary. I'll explain soon enough!
for (var i = 0; i < this.words.length; i++)
{
    this.words[i].assigned = false;

    var word = this.words[i].word;
    var possibleSlots = [];

    for (var x = 0; x < this.maxSize; x++)
    {
        for (var y = 0; y < this.maxSize; y++)
        {
            if (this.isWordSpaceAvailable(x, y, 0, word))
            {
                possibleSlots.push({x: x, y: y, dir: 0});
            }

            if (this.isWordSpaceAvailable(x, y, 1, word))
            {
                possibleSlots.push({x: x, y: y, dir: 1});
            }    

            if (possibleSlots.length == 0) return;                            
        }    
    }    

    var slotIndex = 0;
    var slot;

    var possibleSlots_intersected = possibleSlots.filter((s) => {return this.squares[s.x][s.y].letter != "";});                

    if (possibleSlots_intersected.length == 0)
    {
        slotIndex = this.generateRandomNo(possibleSlots.length);
        slot = possibleSlots[slotIndex];
        this.squares[slot.x][slot.y].intersected = true;

    }
    else
    {
        slotIndex = this.generateRandomNo(possibleSlots_intersected.length);
        slot = possibleSlots_intersected[slotIndex];
    }
}


Now, use a For loop to iterate through every letter in the word. Declare letters as an array of all the elements in the word, using the split() method. Set a conditional for the dir property, and then set the assigned property to true.
for (var i = 0; i < this.words.length; i++)
{
    this.words[i].assigned = false;

    var word = this.words[i].word;
    var possibleSlots = [];

    for (var x = 0; x < this.maxSize; x++)
    {
        for (var y = 0; y < this.maxSize; y++)
        {
            if (this.isWordSpaceAvailable(x, y, 0, word))
            {
                possibleSlots.push({x: x, y: y, dir: 0});
            }

            if (this.isWordSpaceAvailable(x, y, 1, word))
            {
                possibleSlots.push({x: x, y: y, dir: 1});
            }    

            if (possibleSlots.length == 0) return;                            
        }    
    }    

    var slotIndex = 0;
    var slot;

    var possibleSlots_intersected = possibleSlots.filter((s) => {return this.squares[s.x][s.y].letter != "";});                

    if (possibleSlots_intersected.length == 0)
    {
        slotIndex = this.generateRandomNo(possibleSlots.length);
        slot = possibleSlots[slotIndex];
        this.squares[slot.x][slot.y].intersected = true;
    }
    else
    {
        slotIndex = this.generateRandomNo(possibleSlots_intersected.length);
        slot = possibleSlots_intersected[slotIndex];
    }

    for (var j = 0; j < word.length; j++)
    {
        var letters = word.split("");

        if (slot.dir == 0)
        {

        }

        if (slot.dir == 1)
        {

        }

        this.words[i].assigned = true;
    }    

}


What we're doing here is populating the squares array with the letters from the words. dir is important here; again, I'll explain why later. At the end of it, we'll also populate the grid, using jQuery, with the letter and the x and y coordinates.
for (var i = 0; i < this.words.length; i++)
{
    this.words[i].assigned = false;

    var word = this.words[i].word;
    var possibleSlots = [];

    for (var x = 0; x < this.maxSize; x++)
    {
        for (var y = 0; y < this.maxSize; y++)
        {
            if (this.isWordSpaceAvailable(x, y, 0, word))
            {
                possibleSlots.push({x: x, y: y, dir: 0});
            }

            if (this.isWordSpaceAvailable(x, y, 1, word))
            {
                possibleSlots.push({x: x, y: y, dir: 1});
            }    

            if (possibleSlots.length == 0) return;                            
        }    
    }    

    var slotIndex = 0;
    var slot;

    var possibleSlots_intersected = possibleSlots.filter((s) => {return this.squares[s.x][s.y].letter != "";});                

    if (possibleSlots_intersected.length == 0)
    {
        slotIndex = this.generateRandomNo(possibleSlots.length);
        slot = possibleSlots[slotIndex];
        this.squares[slot.x][slot.y].intersected = true;
    }
    else
    {
        slotIndex = this.generateRandomNo(possibleSlots_intersected.length);
        slot = possibleSlots_intersected[slotIndex];
    }

    for (var j = 0; j < word.length; j++)
    {
        var letters = word.split("");

        if (slot.dir == 0)
        {
            this.squares[slot.x + j][slot.y].letter = letters[j];
            this.squares[slot.x + j][slot.y].correct = true;
            this.squares[slot.x + j][slot.y].dir = slot.dir;
            $(".letter[data-x=" + (slot.x + j) + "][data-y=" + (slot.y) + "]").html(letters[j]);

        }

        if (slot.dir == 1)
        {
            this.squares[slot.x][slot.y + j].letter = letters[j];
            this.squares[slot.x][slot.y + j].correct = true;
            this.squares[slot.x][slot.y + j].dir = slot.dir;
            $(".letter[data-x=" + (slot.x) + "][data-y=" + (slot.y + j) + "]").html(letters[j]);

        }

        this.words[i].assigned = true;
    }    
}


And finally, we create the isWordSpaceAvailable() method to help determine if a given square is a suitable place to begin a word. Obviously, there are certain conditions to look out for. If dir is horizontal and the word is so long that starting the word near the end of the grid would cause the word to overrun the set boundaries, then obviously we don't want that. Ditto for vertical.

We begin by declaring letters as an array of all the letters in the parameter word.
populateGridWithWords: function()
{
    for (var x = 0; x < this.maxSize; x++)
    {
        for (var y = 0; y < this.maxSize; y++)
        {
            this.squares[x][y].letter = "";
            this.squares[x][y].correct = false;
            this.squares[x][y].dir = undefined;
            this.squares[x][y].intersected = false;
            $(".letter[data-x=" + x + "][data-y=" + y + "]").html("");
        }    
    }

    for (var i = 0; i < this.words.length; i++)
    {
        this.words[i].assigned = false;

        var word = this.words[i].word;
        var possibleSlots = [];

        for (var x = 0; x < this.maxSize; x++)
        {
            for (var y = 0; y < this.maxSize; y++)
            {
                if (this.isWordSpaceAvailable(x, y, 0, word))
                {
                    possibleSlots.push({x: x, y: y, dir: 0});
                }

                if (this.isWordSpaceAvailable(x, y, 1, word))
                {
                    possibleSlots.push({x: x, y: y, dir: 1});
                }    

                if (possibleSlots.length == 0) return;                            
            }    
        }    

        var slotIndex = 0;
        var slot;

        var possibleSlots_intersected = possibleSlots.filter((s) => {return this.squares[s.x][s.y].letter != "";});

        if (possibleSlots_intersected.length == 0)
        {
            slotIndex = this.generateRandomNo(possibleSlots.length);
            slot = possibleSlots[slotIndex];
            this.squares[slot.x][slot.y].intersected = true;
        }
        else
        {
            slotIndex = this.generateRandomNo(possibleSlots_intersected.length);
            slot = possibleSlots_intersected[slotIndex];
        }

        for (var j = 0; j < word.length; j++)
        {
            var letters = word.split("");

            if (slot.dir == 0)
            {
                this.squares[slot.x + j][slot.y].letter = letters[j];
                this.squares[slot.x + j][slot.y].correct = true;
                this.squares[slot.x + j][slot.y].dir = slot.dir;
                $(".letter[data-x=" + (slot.x + j) + "][data-y=" + (slot.y) + "]").html(letters[j]);
            }

            if (slot.dir == 1)
            {
                this.squares[slot.x][slot.y + j].letter = letters[j];
                this.squares[slot.x][slot.y + j].correct = true;
                this.squares[slot.x][slot.y + j].dir = slot.dir;
                $(".letter[data-x=" + (slot.x) + "][data-y=" + (slot.y + j) + "]").html(letters[j]);
            }

            this.words[i].assigned = true;
        }                    
    }
},
isWordSpaceAvailable: function (x, y, dir, word)
{
    var letters = word.split("");
},

generateRandomNo: function (max)
{
    return Math.floor((Math.random() * (max)));
},


We prepare conditional blocks for dir. In both cases, we return true at the end of the block.
isWordSpaceAvailable: function (x, y, dir, word)
{
    var letters = word.split("");

    if (dir == 0)
    {    
        return true;
    }

    if (dir == 1)
    {
        return true;
    }  
 
},


For a horizontal case, if the length of the word plus the x coordinate would overrun maxSize, we return false.
isWordSpaceAvailable: function (x, y, dir, word)
{
    var letters = word.split("");

    if (dir == 0)
    {    
        if ((x + letters.length) >= this.maxSize) return false;

        return true;
    }

    if (dir == 1)
    {
        return true;
    }    
},


Then we go through all the letters. If the direction has already been set and is the same as the current direction, we return false because we don't want the words to overlap by more than one character - unlikely, but entirely possible depending on the word selection.
isWordSpaceAvailable: function (x, y, dir, word)
{
    var letters = word.split("");

    if (dir == 0)
    {    
        if ((x + letters.length) >= this.maxSize) return false;

        for (var i = 0; i < letters.length; i++)
        {
            if (this.squares[x + i][y].dir == dir) return false;
        }


        return true;
    }

    if (dir == 1)
    {
        return true;
    }    
},


Like in this case...



If the intersected property is true, we also return false. This is so that the word does not intersect an already intersected letter.
isWordSpaceAvailable: function (x, y, dir, word)
{
    var letters = word.split("");

    if (dir == 0)
    {    
        if ((x + letters.length) >= this.maxSize) return false;

        for (var i = 0; i < letters.length; i++)
        {
            if (this.squares[x + i][y].dir == dir) return false;
            if (this.squares[x + i][y].intersected) return false;
        }

        return true;
    }

    if (dir == 1)
    {
        return true;
    }    
},


Basically, I'm trying to prevent a case like this...



And if the space is already occupied by a letter and that letter is not the same as the current letter, we also return false.
isWordSpaceAvailable: function (x, y, dir, word)
{
    var letters = word.split("");

    if (dir == 0)
    {    
        if ((x + letters.length) >= this.maxSize) return false;

        for (var i = 0; i < letters.length; i++)
        {
            if (this.squares[x + i][y].dir == dir) return false;
            if (this.squares[x + i][y].intersected) return false;

            if (this.squares[x + i][y].letter != "" && this.squares[x + i][y].letter != letters[i]) return false;
        }

        return true;
    }

    if (dir == 1)
    {
        return true;
    }    
},


Now repeat this for a vertical direction.
isWordSpaceAvailable: function (x, y, dir, word)
{
    var letters = word.split("");

    if (dir == 0)
    {    
        if ((x + letters.length) >= this.maxSize) return false;

        for (var i = 0; i < letters.length; i++)
        {
            if (this.squares[x + i][y].dir == dir) return false;
            if (this.squares[x + i][y].intersected) return false;

            if (this.squares[x + i][y].letter != "" && this.squares[x + i][y].letter != letters[i]) return false;
        }

        return true;
    }

    if (dir == 1)
    {
        if ((y + letters.length) >= this.maxSize) return false;

        for (var i = 0; i < letters.length; i++)
        {
            if (this.squares[x][y + i].dir == dir) return false;
            if (this.squares[x][y + i].intersected) return false;

            if (this.squares[x][y + i].letter != "" && this.squares[x][y + i].letter != letters[i]) return false;
        }

        return true;

    }    
},


There you go.



Phew! Now that was a lot to unpack. But we're far from done, folks...

Next

Handling clicks, correct and incorrect input.

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.


Monday, 2 November 2020

Film Review: Silicon Valley Season 6

Silicon Valley Season 6 has come and gone, and here I'm looking back at the conclusion of the series. Yes, this Season was the final one. What a ride it's been, eh?

 

The characters that we've come to know, have matured as the years go by - some more quickly than others, though they've not grown so rapidly as to be totally unrecognizable. Previously, they've always been the underdog, scrambling to survive and always on the backfoot. The team's only saving grace was that they had genuinely talented people who cared about their mission.Now we're seeing them in a habitat of success, how far can they take their skills?

Warning - crude tech humor ahead

Silicon Valley is as loud and rude as ever, but it's (spoiler alert!) also pretty sad at the end.

The Premise

On the back of their success, all eyes are now on Pied Piper as they aim for the next rung in that ladder. After the usual suspects throwing roadblocks into the works, the team meets with unprecedented success - but the cost of that success is terrible to bear. The team has to make a heart-breaking decision right there, and there ends the struggle of six years of work.

The Characters

Thomas Middleditch is Pied Piper CEO Richard Hendricks. We've seen Richard at his most awkward and bumbling, but we've also seen him at his most brilliant. This Season, we get to see it all over again. The earnest tech wizard, who, at his very core, sincerely does wants to make the world a better place. The guy who tells nerdy jokes that nobody gets, and even those who do get it, don't find funny. The guy who's prone to the odd body spasm as his emotions get ahead of his brain. Middleditch delivers, and we really feel him.

How Richard discovers the growing danger that is the Pied Piper creation, is nothing short of brilliant. It really showcases Richard's pedant and attention to detail.

Zach Woods as Jared Dunn. Is it my imagination or has Woods gotten a little pudgy? Woods is equal parts nurturing and neurotic as Pied Piper's resident walking conscience. Jared starts off lost as Pied Piper has grown to the point where he doesn't think he's needed anymore. We're treated to a little subplot where he finally meets his birth parents and finds out just how unwanted he really is by his actual family. It's all really sad.

Woods puts up a great performance as always. While Jared gets many moments, he feels severely underused this Season, which is a great pity.

Kumail Nanjiani as Dinesh Chugtai. Dinesh has become increasingly unlikeable with each Season due to his pettiness, vanity and vindictiveness - traits he shares with Gavin Belson. And the thing is, he knows it. Dinesh shows remarkable self-awareness in the finale, and it's largely down to him that the day is saved.

Nanjiani carries this role with amazing skill. Ever since the departure of Erlich, it's hard to find a character we hate and love in almost equal measure... unless we count Jian Yang.

Martin Starr plays the anti-social tech whiz Bertram Gilfoyle. This Season sees him create an AI, battle with HR, and almost make a new friend. All with the same trademark unflappability. Starr is superb, and the platonic chemistry he shares with Amanda Crew's Monica Hall has to be seen to be believed.

Amanda Crew
as Monica Hall. Monica has been increasingly fun to watch the past Season, and she continues this pattern here with very well-timed facial expressions and sage business advice. And when she's really stressed...

Whether she's giving Richard a healthy dose of reality or scheming her way into prominence or defending Pied Piper's good name from Jian Yang, Monica lights up my screen every time I see her. She was always a watchable character to begin with, but shes really come into her own since that first Season. Last time I saw her was in the low-budget slasher flick Tone Deaf, and she was great there as well.

Matt Ross
as Gavin Belson, the fallen CEO of Hooli. Gavin is on his way down, and his ill-fated schemes to regain his power and prestige are at least partially what drives the show.


Gavin isn't the main villain this time; arguably he has very little to do with the trouble the Pied Piper team find themselves in at the end of the Season. By the end, he's become little more than a slapstick figure who's all but thrown his toys out of the pram.

Comedian Jimmy O. Yang as Jian Yang. This guy is incredibly talented. Jian Yang can go from ingratiating to coldly dangerous in a heartbeat. That baleful glare we've come to know takes on new levels of menace this Season as he plots to further his place in this world. Unfortunately, most of his schemes, while dastardly, are impractical in nature and all too easy to foil. He's come a long way since that stuttering Chinese stereotype in earlier Seasons.

Helen Hong plays Tracy Robertson, Pied Piper's HR. Her interplay with Gilfoyle is amazing. That upbeat chippy psychoanalyzation of Gilfoyle against his cold deadpan snarkiness, is a joy to watch.

Craig Frank as Michael Eisenberg. He's Richard's liaison at AT&T. He's only in a few scenes, but I like that character. His candor is refreshing in Silicon Valley.

Tyler Jeffrey Smith as Nathan, a tech in Pied Piper. He's there in one scene, but damn if he isn't Gilfoyle without the beard.

Aristotle Athiras as Gabe. Awkward and nerdy, and really good at (unintentionally) getting under Dinesh's skin, with his wearable chair.

Nandini Bapat as Gwart. Doesn't say a word the entire Season, and her expression is mostly bland.

George Basil as Ethan. He comes across as helpful and eager to be useful, at first, then quickly becomes insufferable as he displays an appalling lack of respect for Richard who is now his CEO. Gets put in his place quite soon, though. By Holden, of all people.

Neil Casey is Colin, K-Hole boss. He's an asshole, but boy his level of douchebaggery really lampshades that of Facebook and Google.

"But going forward, maybe stop saying, Pied Piper won't mine user data... or do say it. I don’t care. Maybe it'll be like Google saying, Don't be evil, or Facebook saying, I'm sorry. We'll do better."


Ben Feldman plays smarmy lawyer Ron LaFlamme. What can I say? This guy is good, and Feldman's standards haven't flagged since the first Season. The easygoing yet savvy lawyer gimmick just never gets old.

Josh Brener as Nelson "Big Head" Bighetti. I've become increasingly irritated with this character. I swear, this guy seems to become dumber with each Season. I have nothing against Josh Brener, but what value does his character bring to the show now?

Suzanne Cryer
plays Laurie Bream with the usual robotic mannerisms. Ruthless, pragmatic and almost totally expressionless, Cryer pulls it off as well as ever. Even when she's in flirt mode.

Chris Diamantopoulos as Russ Hannemann. A-ha! One of my favorite characters is back, and while he gets more to do than in previous Seasons, here he's a full bag of insecurities. But the stuff that made me love watching him - the crudeness, the crassness, the sheer materialism - are still present in spades.

Aaron Sanders as Holden. It turns out that Holden learned more than a little from Jared, because mid-way though the Season, the way he handled Ethan was nothing short of classic Jared understated menace. Too bad he leaves the Season halfway without anything more than a footnote. Sanders deserves a lot of credit here.

Bernard White
is Denpok, Gavin's spiritual advisor. It occurs to me he's Jared's counterpart. Richard relies on Jared's moral compass to guide him and talk him out of unethical decisions. Gavin relies on Denpok to steer him in a direction he secretly wants to go, but needs a good excuse to.

Chris Williams as Hoover, Gavin's fanboyish Security Manager. He's almost in attack dog mode here, but this Season, he and Denpok work together more than against each other.

Arturo Castro  as Maximo Reyes, the gangster. Shit, this is the guy who plays David Rodriguez, the psycho from Narcos. He is great at this!

Henry Philips as John the server guy. His name is John Stafford. In this Season, his deadpan demeanor turns out to be a perfect foil to Gilfoyle's, and to Gilfoyle's chagrin, he even turns out to be better at chess than him.

Chris Aquino as Danny, formerly from Sliceline. Now in a relationship with Becky.

Rachel Rosenbloom as Becky, formerly from Optimoji. In a bickering relationship with Danny, in what appears to be a running gag.

Krishna Smitha as Priyanka Singh. There's nothing very interesting about this character. They basically recycled her in a gag that turned her from Monica's scapegoat to her downfall (in a very minor way). She's basically just a plot device prop here.

The Mood

It begins in a courtroom where Richard delivers an inspiring speech, followed by a celebration - seems to be a lot of that going around - back at Pied Piper's new office. Before long, politics rears its ugly head and the team finds itself in the thick of it once again. What happens next is a frantic race against time as they struggle again and again to keep disaster from striking. It always feels as though they're barely staying afloat, forced into one bad decision after another.

Finally, they triumph, but at a sad and terrible cost.

What I liked

Holy shit. The Season really starts off strong with Richard being both awkward and inspiring (in that order) on National Television. It's that speech he gives that turns disdain into admiration. It's amazing what Richard can do when he truly hits his stride.

"We are in the seat of the US government, a government that was founded by people who were, at one point, ruled by kings they couldn't overthrow. So, what did they do, right? They started over. They came here, to the New World. World 2.0, version 1776. And the way we win is by creating a new, democratic, decentralized Internet, one where the behavior of companies like this will be impossible, forever. One where it is the users, not the kings, who have sovereign control over their data. This, I promise to you: I will help you end this tyranny by building an Internet that is of the people, by the people, and for the people, so help me God."


Gilfoyle's AI is awesome. And it's hilarious how Dinesh manages to screw it up.

Colin's game engine eavesdrops on users and uses the data to run ads in the game itself. Hate to say this, but that's really creative, in a sick sort of way. Just wait till you see how Dinesh inadvertedly triggers a porn overload.


OMG, just look at Gavin Belson's presentation! HooliCares! HOOLISHEETS.

 The part where Hoover is conspiring with Denpok. First off, it's great to see how these two ultimately self-serving guys come together in the face of a common threat. Secondly, I love how Denpok always automatically picks the answer that makes him look good, then after a pause, gives the real answer.

The entire subplot with Pied Piper buying over Hooli is a gem. For one, we have basically the entire team working together like a well-oiled machine, which is what I watch Silicon Valley for. And for another, there are so many references to past Seasons. And thirdly, yes, this is yet another classic example of Gavin Belson fucking himself over due to pettiness.

Gilfoyle declares "I would rather do my own mother than Russfest". Which is funny if you recall that Gilfoyle despises his Mom.

Gilfoyle and John get along so well that John actually sends him a friend request through HooliSocial. And for a moment, Gilfoyle almost accepts. This is significant because it shows that Gilfoyle is not all impenetrable shell. Also, who's the "DC" he keeps slagging in the background? Dinesh Chugtai?

Monica and Gilfoyle plotting together is adorable. I also like Gilfoyle's mug. Totally in-character.

That visual gag of Gwart disappearing. It's kind of mean, but still funny.


While I do think the ending was a bit rushed, it was a decent ending to a wonderful series. It brought the entire arc to a close, and even perennial moral fuck-up Dinesh gets to play a part.

And all those celebrity cameos! Can't begin to name them all, but Bill Gates is a standout!

What I didn't

Richard could have kept his big mouth shut and just let Gavin hang himself using the rope produced by his own plagiarism. But damn, he has to have that satisfaction of exposition. And Gavin, being the evil genius he is, thinks on his feet and outsmarts Richard. Though it's really a Pyrrhic victory for Gavin because all he manages to do is escape. This whole scene was both funny and annoying at the same time.

True to form, this is how Richard appears on Jian Yang's mobile. Dude doesn't like anybody. But Monica's mobile, too? Ouch. I mean, it's funny and all, but it just doesn't make sense for Monica.


There's an awful lot of deadpan going around. From previous Seasons, it was mostly Gilfoyle and Laurie Bream. Now Gabe and John are getting into the act, as well as a few other minor characters. It's starting to become repetitive.

The ending feels a bit abrupt. It all screeches to a halt and we're treated to a time skip of ten years. It's all kind of jarring.

And damn it, where's Drew Daly in this one?!

Conclusion

Despite some flaws, Silicon Valley's final Season caps off six years of work in style. It's a fitting sendoff to everything that's come before. The characterization and events are largely consistent with how they've been portrayed in the past Seasons, and every main character gets a significant amount of air-time.

Silicon Valley would never have lasted forever, and it's great that it decided to quit while it was ahead.

My Rating


9.5 / 10

Time to pay the Piper!
T___T

Monday, 5 October 2020

TeochewThunder: Year Six (Part 2/2)

As with all other blogging years, there were some standouts. Some were outright astonishing.

The undisputed winner was my accounting of the GovTech recruitment process. While it wasn't exactly a literary masterpiece, it got shared, and re-shared among actual GovTech employees, which contributed to a high view rate. Unfortunately, it was the only blogpost to hit those heights.

Another often-viewed post was a piece I wrote during the Singapore General Elections. It was meant to remind voters to keep an open mind, and not be slave to their biases. And true to form, I used JavaScript to illustrate my point... though I don't think I did it extremely well. In fact, I happen to think this piece I wrote about silly rumor-mongering during the Singapore General Elections was more worthy of attention; alas, it garnered a miserable amount of views.

A sharp spike.


Coming a close third was my rumination on whether or not the Singapore General Elections should continue to be held during a COVID-19 outbreak. This was a topic on which few citizens can truly remain neutral, and it was reflected in a spike in viewership. I blogged about this because the situation strongly reminded me of the perennial tech headache of having to strike a balance between system security and user convenience.

These topics were close to my heart, and the language I used might have been a little strong. I don't regret that; not in the least. Sometimes language needs to be strong.

Finally, there was a short piece on how I mounted the learning curve on automated DevOps. I didn't think it was all that interesting, but it appears the statistics disagree with me.

Slipping back into bad habits, the profanity I have used in the past year, especially in the tail end, has been somewhat gratuitous. As was the case for Five Stupid Things Tech Bosses Say, which seems popular only because it's so expletive-ridden. Also probably somewhat relatable, considering the subject matter.

In conclusion

Blogging remains one of my life priorities. Not because my blog is so damn popular, because it isn't. But because this is my industry and as such there is absolutely no good excuse not to have a blog, or at least a demo site. I try to keep content varied, and for the most part, that endeavor is manageable.

Any chump can cobble together a few words and put them online. I'm not even that great a developer. If I can't do even that, then what good am I?

A calming effect.

Blogging helps to center me. It's a side-project where I can focus my thoughts and condense them into a sharp blade. Six years from the day I committed to this endeavor, I'm still at it. Have I mentioned that it's a labor of love? Well, it is. Very much so.

And for all those who've stuck with me thus far, thanks so much.

Thundering on ahead,
T___T

Saturday, 3 October 2020

TeochewThunder: Year Six (Part 1/2)

2020 has been a turbulent year, not just for me, but for just about everyone on the planet. It will forever be known as the year COVID-19 ravaged the world, and the scary thing is, it's far from over. At least not if USA and India have anything to say about it. As befits such a world-shattering event, many of my blogposts this year revolved around COVID-19 as a subject. Not only did this unprecedented crisis give rise to much technical innovation, it also forced changes in the way many people do things, thus forcing even more innovation. The effects were far-reaching and felt even to this day.

Somewhat closer to home, the Singapore General Elections were held around this time. This also gave rise to a series of writings that sometimes coincided with views on COVID-19. This wasn't done on purpose, I swear. COVID-19 just got its hooks into everything.

As it stands, I've just lost my job last month. Long story. As of the first day of this month, I landed another job (though goodness knows how long that will last) and I'm scheduled for yet another stint in school starting this month. The yearly blogging break could not have come at a better time.

Here are some notes about content on this blog...

Web Tutorials

The web tutorials have mostly focused around a whole lot of JavaScript. What can I say; I'm a severely limited programmer and JavaScript happens to be an old friend. There's been new stuff, obviously - D3.js, ReactJS... yes, it's all still JavaScript, but it's different ways of applying the JavaScript ecosystem. You can be a good or bad programmer, but above all, have fun.

My love for learning new stuff has never been put to the test quite as severely as the past year. Everything I learned challenged my assumptions and showed me I know a lot less than I thought I did, and that I have a long way to go before considering myself even mildly accomplished. Which is fine by me; ego is overrated.

A long, long climb.

Despite writing about Ruby just two months ago, I've not actually had a chance to touch it much. It's the same with QBasic. This year has been almost exclusively JavaScript and PHP. All in all, my grasp on tech has evolved. Though, it has to be said, I'm still trying to keep the web tutorials at levels that are accessible to beginners such as myself back then. In many ways, I'm still that wide-eyed kid playing with stuff.

Reviews

Thankfully, unlike last year, I managed to tone down on the overwhelming number of reviews, and actually varied the kind of reviews on offer. There's a recent Reference Review I did on The Mythical Man-month: Essays on Software Engineering, for instance.

No time to play.


I've not had much time or inclination to play or review mobile games, which is a real pity. The mobile game makers seem to be getting more innovative by the day. I guess they have to, with all the competition that's out there, and the notoriously short attention span of their target audience.

Rants

As mentioned in the very first paragraph, there's been quite a few rants on this blog involving either the Singapore General Elections or COVID-19, or both. Regrettably, I've used more vulgarities than was absolutely necessary, and while I don't apologize for it, I acknowledge that things could have been worded a lot less offensively. Could.


Having things to say.


All in all, though, I've been way less of a windbag this year than I've been prone to being the past few years, even though COVID-19 has given me plenty to rant about. Maybe it's because I got most of it out of my system. No point repeating myself.

Or possibly, I'm just getting old.

Others

As mentioned, there's been considerably more diversity in feature blogposts - a good balance of reviews, listicles and what-not. I will strive to maintain that balance.

That tends to be hard because daily, there is tech-related breaking news and I need to decide if I care about it enough to include it in TeochewThunder. And if I do, I need to make hard decisions about what to post now, and what to post later.

Next

Examining what resonated with readers, and what didn't.