Tuesday, 13 May 2025

Web Tutorial: Paper Rock Scissors Game (Part 2/2)

The game is started via the start() method. So make sure that clicking the Start button runs this.
<button id="btnStart" class="topBtn" onclick="paprocksc.start()">Start</button>


In the start() method, we set the range and auto properties depending on the values of the ddlRange drop-down list and the cbAuto checkbox.
start: function()
{
  var ddlRange = document.getElementById("ddlRange");
  this.range = ddlRange.value;

  var cbAuto = document.getElementById("cbAuto");
  this.auto = cbAuto.checked;

},


Then we hide btnStart by adding the hidden CSS class to it, and show btnStop by removing the hidden CSS class.
start: function()
{
  var ddlRange = document.getElementById("ddlRange");
  this.range = ddlRange.value;

  var cbAuto = document.getElementById("cbAuto");
  this.auto = cbAuto.checked;

  var btnStart = document.getElementById("btnStart");
  var btnStop = document.getElementById("btnStop");

  btnStart.className = "topBtn hidden";
  btnStop.className = "topBtn";

},


And set the active property to true.
start: function()
{
  var ddlRange = document.getElementById("ddlRange");
  this.range = ddlRange.value;

  var cbAuto = document.getElementById("cbAuto");
  this.auto = cbAuto.checked;

  var btnStart = document.getElementById("btnStart");
  var btnStop = document.getElementById("btnStop");

  btnStart.className = "topBtn hidden";
  btnStop.className = "topBtn";
  this.active = true;
},


We then have an If block to handle the scenario of auto being true. For now, we will leave that alone, If auto is not true, however, we run showOptions().
start: function()
{
  var ddlRange = document.getElementById("ddlRange");
  this.range = ddlRange.value;

  var cbAuto = document.getElementById("cbAuto");
  this.auto = cbAuto.checked;

  var btnStart = document.getElementById("btnStart");
  var btnStop = document.getElementById("btnStop");

  btnStart.className = "topBtn hidden";
  btnStop.className = "topBtn";
  this.active = true;

  if (this.auto)
  {

  }
  else
  {
    this.showOptions();
  }

},


Now let's do stop(). This is run every time the Stop button is clicked, and also when the values in ddlRange are changed, and cbAuto is clicked.
<h1>Paper, Rock, Scissors</h1>
<select id="ddlRange" onchange="paprocksc.stop()">
  <option value="3">Classic</option>
  <option value="5">Extended</option>
</select>
<input type="checkbox" id="cbAuto" onclick="paprocksc.stop()">Auto

<button id="btnStart" class="topBtn" onclick="paprocksc.start()">Start</button>
<button id="btnStop" class="topBtn hidden" onclick="paprocksc.stop()">Stop</button>

<div class="gameContainer">


In here, we set range to 0 and run showOptions(). This effectively hides all the symbol buttons.
stop: function()
{
  this.range = 0;
  this.showOptions();

},


After that, we hide the Stop button and display the Start button.
stop: function()
{
  this.range = 0;
  this.showOptions();

  var btnStart = document.getElementById("btnStart");
  var btnStop = document.getElementById("btnStop");

  btnStart.className = "topBtn";
  btnStop.className = "topBtn hidden";

},


Then we change both user and computer symbols to show a question mark, by running showSymbol() twice, passing in an empty string for the second argument for both calls. We then set score to have all zeroes in the array, and run showScore() to show the zeroes.
stop: function()
{
  this.range = 0;
  this.showOptions();

  var btnStart = document.getElementById("btnStart");
  var btnStop = document.getElementById("btnStop");

  btnStart.className = "topBtn";
  btnStop.className = "topBtn hidden";

  this.showSymbol(0, "");
  this.showSymbol(1, "");

  this.score = [0, 0];
  this.showScore();

},


And if timer is not null, we run clearInterval() on it.
stop: function()
{
  this.range = 0;
  this.showOptions();

  var btnStart = document.getElementById("btnStart");
  var btnStop = document.getElementById("btnStop");

  btnStart.className = "topBtn";
  btnStop.className = "topBtn hidden";

  this.showSymbol(0, "");
  this.showSymbol(1, "");

  this.score = [0, 0];
  this.showScore();

  if (this.timer !== null) clearInterval(this.timer);
},


Now let's try this. Click the start button. Do the three buttons appear? Does the Start button disappear, only to be replaced by the Stop button?


Now select "Extended" in the drop-down list. Does the Stop button disappear, only to be replaced by the Start button? If you click Start, do all 5 buttons appear?


Now that we have a scenario where the symbol buttons appear, let's handle them being clicked.
<div class="dashboard">
  <button class="symbolBtn hidden" onclick="paprocksc.chooseSymbol('paper')"><img src="symbol_paper.jpg" width="80" /></button>
  <button class="symbolBtn hidden" onclick="paprocksc.chooseSymbol('rock')"><img src="symbol_rock.jpg" width="80" /></button>
  <button class="symbolBtn hidden" onclick="paprocksc.chooseSymbol('scissors')"><img src="symbol_scissors.jpg" width="80" /></button>
  <button class="symbolBtn hidden" onclick="paprocksc.chooseSymbol('spock')"><img src="symbol_spock.jpg" width="80" /></button>
  <button class="symbolBtn hidden" onclick="paprocksc.chooseSymbol('lizard')"><img src="symbol_lizard.jpg" width="80" /></button>
</div>


And we'll work on the chooseSymbol() method. It has a parameter, symbol. This is a string such as "rock, "paper", etc. The first thing we should do is check if active is false, and exit early if so. This means that when active is false, clicking on the buttons accomplishes nothing.
chooseSymbol: function(symbol)
{
  if (!this.active) return;
},  


There will be a scenario where symbol is an empty string. For now, create an If block for it, and cater to the Else condition. If symbol has a value, set the first element of the round array to that value. Then recursively call chooseSymbol(), this time with an empty string.
chooseSymbol: function(symbol)
{
  if (!this.active) return;

  if (symbol === "")
  {

  }
  else
  {
    this.round[0] = symbol;  

    this.chooseSymbol("");              
  }

},  


Thus, if chooseSymbol() is called with an empty string, we declare ptr and assign a random number to it, between 0 and range, excluding range. And then use it to point to an element in symbols, and assign that value to the second element in the round array. In effect, the computer makes a random choice! Use the return statement at this point, because then we're done for that scenario.
chooseSymbol: function(symbol)
{
  if (!this.active) return;

  if (symbol === "")
  {
    var ptr = Math.floor(Math.random() * this.range);
    this.round[1] = this.symbols[ptr].name;
    return;

  }
  else
  {
    this.round[0] = symbol;
    
    this.chooseSymbol("");              
  }
},  


Then we run the showSymbol() method twice, once for the user and once for the computer. And after that, we run whowins().
chooseSymbol: function(symbol)
{
  if (!this.active) return;

  if (symbol === "")
  {
    var ptr = Math.floor(Math.random() * this.range);
    this.round[1] = this.symbols[ptr].name;
    return;
  }
  else
  {
    this.round[0] = symbol;
    
    this.chooseSymbol("");              
  }

  this.showSymbol(0, this.round[0]);
  this.showSymbol(1, this.round[1]);
  this.whowins();

},  


We begin whowins() by setting the active property to false, to ensure that the symbol buttons can't be clicked during this time.
whowins: function()
{
  this.active = false;
},


Then we declare winner0 and winner1, both Booleans. winner1 is defined by passing the first and second elements of the round array into the xbeatsy() method. Thus winner0 is true if the user beats the computer, and false if not. The reverse is true for winner1. Note that the values are still false if it's a draw.
whowins: function()
{
  this.active = false;

  var winner0 = this.xbeatsy(this.round[0], this.round[1]);
  var winner1 = this.xbeatsy(this.round[1], this.round[0]);

},


We next have an If-else block. The first condition checks for a draw - meaning winner0 and winner1 are both false. In the Else block, we then have cases for if user beats computer or computer beats user.
whowins: function()
{
  this.active = false;

  var winner0 = this.xbeatsy(this.round[0], this.round[1]);
  var winner1 = this.xbeatsy(this.round[1], this.round[0]);

  if (!winner0 && !winner1)
  {

  }
  else
  {
    if (winner0)
    {

    }

    if (winner1)
    {

    }
  }

},


If it's a draw, we run showColor() twice, ensuring that both user and computer's circles have a grey outline.
whowins: function()
{
  this.active = false;

  var winner0 = this.xbeatsy(this.round[0], this.round[1]);
  var winner1 = this.xbeatsy(this.round[1], this.round[0]);

  if (!winner0 && !winner1)
  {
    this.showColor(0, "draw");
    this.showColor(1, "draw");

  }
  else
  {
    if (winner0)
    {

    }

    if (winner1)
    {

    }
  }
},


In the case of there being a winner, we display the correct colors too. And update the score array accordingly. At the end of it, because the score array was updated, we also run showScore().
whowins: function()
{
  this.active = false;

  var winner0 = this.xbeatsy(this.round[0], this.round[1]);
  var winner1 = this.xbeatsy(this.round[1], this.round[0]);

  if (!winner0 && !winner1)
  {
    this.showColor(0, "draw");
    this.showColor(1, "draw");
  }
  else
  {
    if (winner0)
    {
      this.score[0] ++;
      this.showColor(0, "win");
      this.showColor(1, "loss");

    }

    if (winner1)
    {
      this.score[1] ++;
      this.showColor(0, "loss");
      this.showColor(1, "win");

    }

    this.showScore();
  }
},


After that, we use the setTimeout() function, resetting the circle outlines to white and the symbols to question marks, and active to true, after 1 second.
whowins: function()
{
  this.active = false;

  var winner0 = this.xbeatsy(this.round[0], this.round[1]);
  var winner1 = this.xbeatsy(this.round[1], this.round[0]);

  if (!winner0 && !winner1)
  {
    this.showColor(0, "draw");
    this.showColor(1, "draw");
  }
  else
  {
    if (winner0)
    {
      this.score[0] ++;
      this.showColor(0, "win");
      this.showColor(1, "loss");
    }

    if (winner1)
    {
      this.score[1] ++;
      this.showColor(0, "loss");
      this.showColor(1, "win");
    }

    this.showScore();
  }

  setTimeout
  (
    ()=>
    {
      this.showSymbol(0, "");
      this.showSymbol(1, "");
      this.showColor(0, "end");
      this.showColor(1, "end");
      this.active = true;
    },
    1000
  );

},


Time to test this!

Click on Start. Select Scissors. The computer will randomly select its own symbol. In this case, it also selects Scissors, and it's a draw. See the grey outline?


Now select Rock. As you can see, the computer selects Paper and it's a loss. One point to computer.


Now select Paper. As you can see, the computer selects Rock and it's a win. One point to user.


Try selected Extended Mode. You'll see that the game stops immediately. And then select Lizard. Computer also selects Lizard, and it's a draw.


Now select Spock. Computer selects Paper. It's a loss! One point to Computer.


Auto Mode

We're going to write the code for Auto Mode now! This will be fun.

Remember in the start() method we checked if auto was true? Well, if so, we use setInterval() to run chooseSymbol() with "auto" passed in as an argument, every 1.5 seconds. To ensure that this can be disabled later with clearInterval(), we set this to the timer property.
start: function()
{
  var ddlRange = document.getElementById("ddlRange");
  this.range = ddlRange.value;

  var cbAuto = document.getElementById("cbAuto");
  this.auto = cbAuto.checked;

  var btnStart = document.getElementById("btnStart");
  var btnStop = document.getElementById("btnStop");

  btnStart.className = "topBtn hidden";
  btnStop.className = "topBtn";
  this.active = true;

  if (this.auto)
  {
    this.timer = setInterval
    (
      () =>
      {
        this.chooseSymbol("auto");
      },
      1500
    );

  }
  else
  {
    this.showOptions();
  }
},


In chooseSymbol(), we take that part where the first element of the round array is set to symbol, and encase it in the Else portion of an If-else block. The If block checks if symbol is "auto".
chooseSymbol: function(symbol)
{
  if (!this.active) return;

  if (symbol === "")
  {
    var ptr = Math.floor(Math.random() * this.range);
    this.round[1] = this.symbols[ptr].name;
    return;
  }
  else
  {
    if (symbol === "auto")
    {

    }
    else
    {

      this.round[0] = symbol;
    }
    
    this.chooseSymbol("");              
  }

  this.showSymbol(0, this.round[0]);
  this.showSymbol(1, this.round[1]);
  this.whowins();
},  


If it is, we write the code to randomly select a symbol for the user.
chooseSymbol: function(symbol)
{
  if (!this.active) return;

  if (symbol === "")
  {
    var ptr = Math.floor(Math.random() * this.range);
    this.round[1] = this.symbols[ptr].name;
    return;
  }
  else
  {
    if (symbol === "auto")
    {
      var ptr = Math.floor(Math.random() * this.range);
      this.round[0] = this.symbols[ptr].name;

    }
    else
    {
      this.round[0] = symbol;
    }
    
    this.chooseSymbol("");              
  }

  this.showSymbol(0, this.round[0]);
  this.showSymbol(1, this.round[1]);
  this.whowins();
},  


Now try checking Auto Mode and then clicking Start. You should see the game play by itself! The buttons aren't shown because you don't need them in Auto Mode.

That's about what I wrote for the coding test back then. Hopefully I've improved since!


Rock on!
T___T

Sunday, 11 May 2025

Web Tutorial: Paper Rock Scissors Game (Part 1/2)

During the early months of the COVID-19 outbreak, I applied for a job where the interviewers sent me a coding test. The objective? To write a Paper Rock Scissors Game in HTML, CSS and JavaScript.

Paper Rock Scissors

At the time, they requested that I keep the GitHub repository for the code private. It's been five years and I figure AI has rendered their fun little tests moot. In any case, I rewrote my code from scratch and hopefully my style now has improved.

The Stated Requirements

1. Implement a Paper Rock Scissors Game in HTML/CSS/JavaScript.
2. The game should be extensible to include Spock and Lizard.
3. The game should include an option for the computer to play against itself.

Let's begin!

Here's some starting HTML boilerplate. In the CSS, we set the general font face and font size. I've set divs to have a red outline. In the JavaScript, we want an object, paprocksc. I know, that's quite the name, eh?
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <title>Paper Rock Scisscors</title>

        <style>
            body
            {
                text-align: center;
                font-size: 14px;
                font-family: verdana;
            }

            div { outline: 1px solid red; }
        </style>

        <script>
            let paprocksc =
            {

            };
        </script>
    </head>
    
    <body>

    </body>
</html>


Let's get some HTML structure out of the way. We have a header. We also have the CSS class gameContainer styling the outermost div.
<body>
    <h1>Paper, Rock, Scissors</h1>

    <div class="gameContainer">

    </div>

</body>


Then two divs, each styled using player, and left and right respectively. They have different ids. And let's have a hr tag after all this.
<body>
    <h1>Paper, Rock, Scissors</h1>

    <div class="gameContainer">
        <div class="player left">

        </div>
        <div class="player right">

        </div>
        <hr />
    </div>
</body>


In the CSS, gameContainer occupies a certain width and height. player occupies roughly half the width. left makes its element float left, and right does the exact opposite.
<style>
    body
    {
        text-align: center;
        font-size: 14px;
        font-family: verdana;
    }

    div { outline: 1px solid red; }

    .gameContainer
    {
        width: 100%;
        height: 600px;
    }

    .player
    {
        width: 45%;
        height: 300px;
    }

    .left
    {
        float: left;
    }

    .right
    {
        float: right;
    }
</style>


And hr, that comes right after these two divs, has the clear property set to both so that we don't end up with collapsing issues.
<style>
body
{
text-align: center;
font-size: 14px;
font-family: verdana;
}

div { outline: 1px solid red; }

.gameContainer
{
width: 100%;
height: 600px;
}

.player
{
width: 45%;
height: 300px;
}

.left
{
float: left;
}

.right
{
float: right;
}

hr
{
clear: both;
}

</style>


This is what it looks like!


We're going to use this image next. I made this myself - just a white question mark over a black square.

symbol.jpg

In each div styled using the player CSS class, we have one div. They have different ids, and are styled using the symbol CSS class. Because we want them to be floated a certain direction, we style them using right and left, respectively. And we hard-code the background image to use symbol.jpg.
<div class="player left">
    <div id="player_0" class="symbol right" style="background-image: url(symbol.jpg);"></div>
</div>
<div class="player right">
    <div id="player_1" class="symbol left" style="background-image: url(symbol.jpg);"></div>
</div>


In the CSS, symbol is a circle, thus width and height properties are the same and border-radius is set to 50%. The background-related properties ensure that the background image used covers the entire element. outline is set to a thick white line. These are the defaults.
.right
{
    float: right;
}

.symbol
{
    width: 200px;
    height: 200px;
    border-radius: 50%;
    margin-top: 10px;
    background-size: cover;
    background-repeat: no-repeat;
    background-position: center center;
    outline: 15px solid rgb(255, 255, 255);
}


hr
{
    clear: both;
}


Take a look at how it's coming together. We're going to add some placeholders for scores.


Here we have a div. It's styled using score and right. Inside it is a small header.
<div class="player left">
    <div class="score right"><small>You</small><br /></div>
    <div id="player_0" class="symbol right" style="background-image: url(symbol.jpg);"></div>
</div>
<div class="player right">
    <div id="player_1" class="symbol left" style="background-image: url(symbol.jpg);"></div>
</div>


We'll also have a span tag, with an id. The default value is 0.
<div class="player left">
    <div class="score right"><small>You</small><br /><span id="score_0">0</span></div>
    <div id="player_0" class="symbol right" style="background-image: url(symbol.jpg);"></div>
</div>
<div class="player right">
    <div id="player_1" class="symbol left" style="background-image: url(symbol.jpg);"></div>
</div>


Repeat this for the other side...
<div class="player left">
    <div class="score right"><small>You</small><br /><span id="score_0">0</span></div>
    <div id="player_0" class="symbol right" style="background-image: url(symbol.jpg);"></div>
</div>
<div class="player right">
    <div class="score right"><small>You</small><br /><span id="score_0">0</span></div>
    <div id="player_1" class="symbol left" style="background-image: url(symbol.jpg);"></div>
</div>


... but make sure the div is styled using left, instead. The span tag id should be different, and the header is for the computer.
<div class="player left">
    <div class="score right"><small>You</small><br /><span id="score_0">0</span></div>
    <div id="player_0" class="symbol right" style="background-image: url(symbol.jpg);"></div>
</div>
<div class="player right">
    <div class="score left"><small>Computer</small><br /><span id="score_1">0</span></div>
    <div id="player_1" class="symbol left" style="background-image: url(symbol.jpg);"></div>
</div>


In the CSS, score has a defined width and height, and font. We align the text center. The small tag within score will be half the font size.
.symbol
{
    width: 200px;
    height: 200px;
    border-radius: 50%;
    margin-top: 10px;
    background-size: cover;
    background-repeat: no-repeat;
    background-position: center center;
    outline: 15px solid rgb(255, 255, 255);
}

.score
{
    width: 150px;
    height: 50px;
    font-weght: bold;
    font-size: 2em;
    text-align: center;
}

.score small
{
    font-size: 0.5em;
}


hr
{
    clear: both;
}


Looks good. Let's do a dashboard next.


After the hr tag, add a div. Style it using the CSS class dashboard.
<hr />
<div class="dashboard">

</div>


Inside it, add a button. Style it using the CSS classes symbolBtn and hidden.
<hr />
<div class="dashboard">
    <button class="symbolBtn hidden"></button>
</div>


Add five more buttons.
<hr />
<div class="dashboard">
    <button class="symbolBtn hidden"></button>
    <button class="symbolBtn hidden"></button>
    <button class="symbolBtn hidden"></button>
    <button class="symbolBtn hidden"></button>
    <button class="symbolBtn hidden"></button>
    <button class="symbolBtn hidden"></button>
</div>


Now, we're going to be using more images I generated using AI. They are crude black-and-white drawings of hand signs - Paper, Rock, Scissors, Lizard and Spock.

symbol_paper.jpg

symbol_rock.jpg

symbol_scissors.jpg

symbol_lizard.jpg

symbol_spock.jpg

Add these images as content in the buttons. We'll limit the width to 80 pixels.
<hr />
<div class="dashboard">
    <button class="symbolBtn hidden"><img src="symbol_paper.jpg" width="80" /></button>
    <button class="symbolBtn hidden"><img src="symbol_rock.jpg" width="80" /></button>
    <button class="symbolBtn hidden"><img src="symbol_scissors.jpg" width="80" /></button>
    <button class="symbolBtn hidden"><img src="symbol_spock.jpg" width="80" /></button>
    <button class="symbolBtn hidden"><img src="symbol_lizard.jpg" width="80" /></button>
</div>


In the CSS, dashboard has a defined with and height, and is positioned in the middle via the margin property.
.score small
{
    font-size: 0.5em;
}

.dashboard
{
    width: 90%;
    height: 200px;
    margin: 0 auto 0 auto;
}


hr
{
    clear: both;
}


We style symbolBtn by making it a 100 pixel square, with rounded corners and a bit of spacing on the right.
.score small
{
    font-size: 0.5em;
}

.dashboard
{
    width: 90%;
    height: 200px;
    margin: 0 auto 0 auto;
}

.symbolBtn
{
    width: 100px;
    height: 100px;
    border-radius: 5px;
    margin-right: 5px;
}


hr
{
    clear: both;
}


You may also want to generally style buttons to have a grey background when hovered over.
.gameContainer
{
    width: 100%;
    height: 600px;
}

button:hover
{
    background-color: rgb(100, 100, 100);
}


.player
{
    width: 45%;
    height: 300px;
}


Now you can see the buttons! And what happens when you mouse over one of them.


Now add the hidden CSS class, and the buttons should disappear.
.symbolBtn
{
    width: 100px;
    height: 100px;
    border-radius: 5px;
    margin-right: 5px;
}

.hidden
{
    display: none;
}


hr
{
    clear: both;
}


Near the title, let's add some controls. First, we want a drop-down list with the id ddlRange. It determines if the user is playing with just Paper, Rock and Scissors, or the full extended version. The values are 3 and 5, representing the number of hand symbols available.
<h1>Paper, Rock, Scissors</h1>
<select id="ddlRange">
    <option value="3">Classic</option>
    <option value="5">Extended</option>
</select>


<div class="gameContainer">


Then we have a checkbox with the id cbAuto. This controls whether or not the computer plays against itself or the user.
<h1>Paper, Rock, Scissors</h1>
<select id="ddlRange">
    <option value="3">Classic</option>
    <option value="5">Extended</option>
</select>
<input type="checkbox" id="cbAuto">Auto

<div class="gameContainer">


We then have two buttons, ids btnStart and btnStop. btnStop is hidden, but both buttons are styled using the CSS class topBtn.
<h1>Paper, Rock, Scissors</h1>
<select id="ddlRange">
    <option value="3">Classic</option>
    <option value="5">Extended</option>
</select>
<input type="checkbox" id="cbAuto">Auto

<button id="btnStart" class="topBtn">Start</button>
<button id="btnStop" class="topBtn hidden">Stop</button>


<div class="gameContainer">


Styling for topBtn is just a matter of visuals. I'm sticking to the black-white theme.
.gameContainer
{
    width: 100%;
    height: 600px;
}

.topBtn
{
    width: 5em;
    height: 2em;
    background-color: rgb(0, 0, 0);
    color: rgb(255, 255, 255);
    font-weight: bold;
    border-radius: 3px;
}


button:hover
{
    background-color: rgb(100, 100, 100);
}


There be your controls!


That's it for the UI! Turn the red outline for divs off.
div { outline: 0px solid red; }


The paprocksc object

We're going to define some properties. First, symbols. This is a collection of all the hand symbols used in the game.
let paprocksc =
{
    symbols:
    [
                                                            
    ]

};


Each one is an object with the name property with values such as "paper", "rock", etc. The beats property is an array, and it defines which symbol the current symbol, well, beats. For example, Paper beats Rock and Spock.
let paprocksc =
{
    symbols:
    [
        {
            name: "paper",
            beats: ["rock", "spock"]
        }  
                                                      
    ]
};


And that's how we populate the rest of symbols.
let paprocksc =
{
    symbols:
    [
        {
            name: "paper",
            beats: ["rock", "spock"]
        },
        {
            name: "rock",
            beats: ["lizard", "scissors"]
        },
        {
            name: "scissors",
            beats: ["paper", "lizard"]
        },
        {
            name: "spock",
            beats: ["scissors", "rock"]
        },
        {
            name: "lizard",
            beats: ["spock", "paper"]
        }     
                                                       
    ]
};


range defines how many elements of symbols to use. The default is 3, which means we use the first 3 elements - Paper, Rock and Scissors. auto defines whether or not the computer is playing against itself, and by default it's false.
let paprocksc =
{
    symbols:
    [
        {
            name: "paper",
            beats: ["rock", "spock"]
        },
        {
            name: "rock",
            beats: ["lizard", "scissors"]
        },
        {
            name: "scissors",
            beats: ["paper", "lizard"]
        },
        {
            name: "spock",
            beats: ["scissors", "rock"]
        },
        {
            name: "lizard",
            beats: ["spock", "paper"]
        }                                                           
    ],
    range: 3,
    auto: false

};


Next we have score and round, both of which are arrays with two elements. score is an array of integers which keeps track of rounds won. round keeps track of who's using what symbol. In both cases, the first element pertains to the user, and the second to the computer.
let paprocksc =
{
    symbols:
    [
        {
            name: "paper",
            beats: ["rock", "spock"]
        },
        {
            name: "rock",
            beats: ["lizard", "scissors"]
        },
        {
            name: "scissors",
            beats: ["paper", "lizard"]
        },
        {
            name: "spock",
            beats: ["scissors", "rock"]
        },
        {
            name: "lizard",
            beats: ["spock", "paper"]
        },                                                            
    ],
    range: 3,
    auto: false,
    score: [0, 0],
    round: ["", ""]

};


Lastly, we have active and timer. active just determines if the buttons are clickable, and timer is relevant only during Auto mode.
let paprocksc =
{
    symbols:
    [
        {
            name: "paper",
            beats: ["rock", "spock"]
        },
        {
            name: "rock",
            beats: ["lizard", "scissors"]
        },
        {
            name: "scissors",
            beats: ["paper", "lizard"]
        },
        {
            name: "spock",
            beats: ["scissors", "rock"]
        },
        {
            name: "lizard",
            beats: ["spock", "paper"]
        },                                                            
    ],
    range: 3,
    auto: false,
    score: [0, 0],
    round: ["", ""],
    active: false,
    timer: null

};


Now, on to methods! We first have start() and stop(), and I hope these are self-explanatory.
let paprocksc =
{
    symbols:
    [
        {
            name: "paper",
            beats: ["rock", "spock"]
        },
        {
            name: "rock",
            beats: ["lizard", "scissors"]
        },
        {
            name: "scissors",
            beats: ["paper", "lizard"]
        },
        {
            name: "spock",
            beats: ["scissors", "rock"]
        },
        {
            name: "lizard",
            beats: ["spock", "paper"]
        },                                                            
    ],
    range: 3,
    auto: false,
    score: [0, 0],
    round: ["", ""],
    active: false,
    timer: null,
    start: function()
    {

    },
    stop: function()
    {

    }

};


showOptions() shows and hides symbol buttons according to which should be available. And from there, we have chooseSymbol(), which takes the choice, represented by the parameter symbol, and processes it.
    start: function()
    {
    
    },
    stop: function()
    {
    
    },
    showOptions: function()
    {
    
    },
    chooseSymbol: function(symbol)
    {
    
    }

};


Then we have what I like to call the other "show" methods. showSymnbol() takes index to determine which placeholder to show symbol in. showScore() displays the current score. showColor() changes the outline of the placeholder indicated by index, to the color determined by colorCode.
    showOptions: function()
    {
    
    },
    chooseSymbol: function(symbol)
    {
    
    },    
    showSymbol: function(index, symbol)
    {
    
    },    
    showScore: function()
    {
    
    },
    showColor: function(index, colorCode)
    {
    
    }

};


whowins() is the parent method that calls xbeatsy() to determine if one symbol beats the other.
    
    showSymbol: function(index, symbol)
    {
    
    },    
    showScore: function()
    {
    
    },
    showColor: function(index, colorCode)
    {
    
    },
    whowins: function()
    {
    
    },        
    xbeatsy: function(x, y)
    {
    
    }

};


I'd like to begin with some of the simpler "show" methods. Like this one, showSymbol(). We declare imgUrl as the string "symbol.jpg". If the value for symbol is not an empty string, we add an underscore and symbol to the filename, e.g, "symbol_scissors.jpg".
showSymbol: function(index, symbol)
{
    var imgUrl = "symbol" + (symbol === "" ? "" : "_" + symbol) + ".jpg";
},


Then we have player. It either references the div player_0 or player_1.
showSymbol: function(index, symbol)
{
    var imgUrl = "symbol" + (symbol === "" ? "" : "_" + symbol) + ".jpg";
    var player = document.getElementById("player_" + index);
},


This last line populates the desired div with the correct image background.
showSymbol: function(index, symbol)
{
    var imgUrl = "symbol" + (symbol === "" ? "" : "_" + symbol) + ".jpg";
    var player = document.getElementById("player_" + index);

    player.style.backgroundImage = "url(" + imgUrl + ")";
},


Now for showScore(). This is straightforward. We declare score0 and score1, and set them to the elements score_0 and score_1, respectively.
showScore: function()
{
    var score0 = document.getElementById("score_0");
    var score1 = document.getElementById("score_1");

},


We then populate those elements with the values of the score array. As mentioned previously, the first element is for the user and the second is for the computer.
showScore: function()
{
    var score0 = document.getElementById("score_0");
    var score1 = document.getElementById("score_1");

    score0.innerHTML = this.score[0];
    score1.innerHTML = this.score[1];

},


And then we have showColor(). colorCode will be a RGB value, and this is how we define it. First, we have an object, codes. Then we have the win, loss, draw and end properties, each of which is a RGB value. A win is green, a loss is red, a draw is grey, and the neutral state, end, is white.
showColor: function(index, colorCode)
{
    var codes =
    {
        win: "rgb(0, 255, 0)",
        loss: "rgb(255, 0, 0)",
        draw: "rgb(200, 200, 200)",
        end: "rgb(255, 255, 255)",
    };

},


As with showSymbol(), index defines the div to work on.
showColor: function(index, colorCode)
{
    var codes =
    {
        win: "rgb(0, 255, 0)",
        loss: "rgb(255, 0, 0)",
        draw: "rgb(200, 200, 200)",
        end: "rgb(255, 255, 255)",
    };

    var symbol = document.getElementById("player_" + index);
},


We will give that div's outline a color based on the RGB value that colorCode points to in codes.
showColor: function(index, colorCode)
{
    var codes =
    {
        win: "rgb(0, 255, 0)",
        loss: "rgb(255, 0, 0)",
        draw: "rgb(200, 200, 200)",
        end: "rgb(255, 255, 255)",
    };

    var symbol = document.getElementById("player_" + index);
    symbol.style.outline = "15px solid " + codes[colorCode];
},


Now for showOptions()! In here, we decide which of the symbol buttons in the dashboard to show or hide. Create an array, btns, by running getElementsByClassName() and passing in the class name symbolBtn. Then use a For loop to iterate through them.
showOptions: function()
{
    var btns = document.getElementsByClassName("symbolBtn");

    for (var i = 0; i < btns.length; i ++)
    {

    }

},


In the loop, check if i is less than the range property.
showOptions: function()
{
    var btns = document.getElementsByClassName("symbolBtn");

    for (var i = 0; i < btns.length; i ++)
    {
        if (i < this.range)
        {

        }
        else
        {

        }

    }
},


If it is, we ensure that the button is displayed. If not, it is hidden. That means that in Classic Mode, where range is 3, only the first 3 buttons will be shown.
showOptions: function()
{
    var btns = document.getElementsByClassName("symbolBtn");

    for (var i = 0; i < btns.length; i ++)
    {
        if (i < this.range)
        {
            btns[i].className = "symbolBtn";
        }
        else
        {
            btns[i].className = "symbolBtn hidden";
        }
    }
},


One last method!

We'll do xbeatsy(). This method determines if symbol x beats symbol y. For this, we first need to find information about symbol x. Use the filter() method on symbols, returning the element where name matches x. Assign the resultant array to the variable symbol.
xbeatsy: function(x, y)
{
    var symbol = this.symbols.filter((s)=> { return s.name === x});
}


Here, the return statement's value is either true or false. We ask if the beats array of the first (and only) element of symbol, contains y. If it does, x beats y. If not, then it returns false.
xbeatsy: function(x, y)
{
    var symbol = this.symbols.filter((s)=> { return s.name === x});
    return (symbol[0].beats.indexOf(y) > -1);
}


These methods don't seem to do much, or have anything to do with each other. But fear not; all shall be revealed shortly. Join us for the next part of this web tutorial!

Next

Running the game.

Sunday, 4 May 2025

Full-time Pay, Part-time Job

Singapore has spoken. Yesterday on the 3rd of May, we spoke at the ballot box and made our respective decisions in our various constituencies. Some were won, some were lost, and Singapore moves on.

But in the neighbourhood of Mountbatten in the eastern part of the island, the spoils of victory go to Ms Gho Sze Kee of the People's Action Party. Her opponent? A certain independent candidate by the name of Jeremy Tan, a tech-savvy entrepreneur. And he's the subject of this blogpost today.

Tan (no relation here; us Tans are all over Southeast Asia) came to my attention on the 1st of May as he held a rally to present his case to Mountbatten residents. In the process, he won quite a few hearts, I suspect, including mine. Though I might be biased; I, too, am a software developer. There's footage of his presentation here, and honestly, I can't do it justice with a mere description; so do check it out below on YouTube.



Chill!

What I liked, first and foremost, was the chill, laid-back vibe. He was basically jiving with the crowd, getting them to repeat the night's catch-phrase - Full-time pay, part-time job, alluding to Gho Sze Kee's intention to only be a part-time MP - at pertinent parts of his speech. This was political showmanship without the fireworks. I know we love those from time to time, but this election served up a disproportionate dose of testosterone.





Thus, Jeremy Tan's cheeky quips and laid-back zingers were a refreshing change. It felt more like a stand-up comedy routine than a political rally as he roasted both his opponent Gho Sze Kee and the outgoing incumbent Lim Biow Chuan.

Relatable

One point Jeremy Tan made was in response to Lim Biow Chuan's assertion that "There's something about Jeremy Tan's language that I don't quite agree with.".

"You know what Mr Lim, I think most Singaporeans sound like myself than Miss Gho or yourself, you know. I think perhaps it's the fact that we speak different languages that you have been tone-deaf to your residents."

And this is a very valid point. You see, no one (OK, hardly any one) disputes the fact that the incumbent boasts some of the best and brightest minds of Singapore. The cream of the crop. The kind who would be able to make more than the monthly SGD 16,000 that is on offer for the average MP, quite easily. The kind, like Ms Gho Sze Kee, who saw the role of MP as only a "part-time job", or side gig.

And that's part of the problem. These people have higher-paying jobs that they don't have a chance of losing every 5 years or so. Therefore some of them sometimes give off the impression that they resent having to place themselves at the mercy of us unwashed schmucks every 5 years, and plead their case as to why they should be re-elected. After all they've done for the country! How dare these peasants presume to judge them?!

That ivory tower.

Therein lies the disconnect.

If you told the average man in the street, who earns maybe SGD 4,000 a month or less, and told him you'd pay him four times that but every 5 years he would have to go through a national review to keep his job, he would consider that an unbelievably good deal. That kind of pay and you don't need to worry about losing your job for at least 5 years?! Sign me up, bro.

Sometimes Singaporeans don't need the best. They need people who can adequately do the job and they can relate to. It's all very well to have the best and brightest doing the job until they piss you off every time they open their mouths. Remember, suitability for a role is not merely a matter of technical, but also social competence.

And finally... the tech stack

This was the part that sold me. I am a software developer, after all, even if not a particularly outstanding one.

Jeremy Tan commented on the state of the ERP and CRM Software used in the Town Council. As a software applications engineer with the relevant experience, he pledged to rebuild the entire tech stack.  It was his vision to create some kind of "digital town" for Mountbatten.

A "tech stack". Heh heh.

Now, was this all some tech gibberish designed to bamboozle the average non-techie? Maybe. It does sound like a tall order, especially when he started talking about AI. But it also does sound like an exciting project.

This also resonated because I have noticed similar things about other Government-linked systems I have used recently and in the past. While some of the deficiencies were tech-based, some of it was due to tech being layered on top of outdated manual processes. Granted, there have been marked improvements as of late, but it's also a little concerning that these improvements have only come recently.

I could go on, but this is supposed to be about my guy Jeremy Tan, so...

In conclusion?

Jeremy Tan's efforts brought me back to a period in time where Big Tech founders conducted bold experiments in their family garages. That kind of enterprise is perhaps what we sorely need as a nation.

He may not last in the public eye beyond this election. But he'll be remembered fondly. By me, at least. And if he ever wants to run again, I'll gladly distribute his flyers.

Give this man a full-time job!
T___T

Tuesday, 29 April 2025

Sidestepping the year-end bonus trap

By the second year in my current job, I had gone for years without a year-end bonus due to a combination of factors such as constant job-hopping and working for cheap financially-prudent employers. But I made do. Not only that, I thrived.

Thus it was with some puzzlement when I received an email from HR informing me that I was due for a year-end bonus. An entire month's worth, no strings attached.

Extra money out of nowhere.

Wow. Imagine. My. Shock. Having spent so many years learning to live without a year-end windfall, now I faced an entirely different problem: what in the blue hell was I supposed to do with that chunk of change that had just appeared out of nowhere and didn't fit in my budget?

As a married man, the solution was easy. I just handed it over to my wife.

When the Missus received that fat stack of cash, the first question she asked, with a hint of suspicion, was why I was suddenly giving her money. I put on my most macho-bullshit act and declared, "Woman, I told you when you married me, whatever money I make from here onwards, you will make." Famous last words. Because every subsequent year, right at the tail end, she would inquire about my year-end bonus and now I was expected to give it to her every time.

Moral of the story: Be very careful what you tell your woman. She's going to hold you to it. Believe that!

The real reason

When I told my friends what I'd been doing with my year-end bonus for the past few years, what I got was the mind-numbing chorus of "wow, that's true love" and "what a great husband".

Have people always been this simple, or is this just happening now?

This is about freedom, not love. I was once trapped in a job I hated due to having developed a dependency on their year-end bonus. And whenever I tried to leave, I always found myself waiting to get the bonus before making plans to leave. Plenty of people find themselves in the same predicament now. It's that Singaporean mentality of not wanting to lose out.

Yes, I know that this isn't necessarily some cynical move by the company to hold their employees ransom. A year-end bonus is a pretty common practice and nobody was holding a gun to my head in order to force me to stay at the company. It was purely my reluctance to forfeit that lump sum of cash by leaving at year end, that kept me there. However, even taking full responsibility for it, does not make the dependency on the year-end bonus any less of a trap. It merely turns this into a self-imposed trap.

No! It's a trap!

Now having tailored my cash flow around not having a year-end bonus after so many years of going without, I have effectively weaned myself off this dependency. Why would it be a good idea to voluntarily redevelop the same crippling dependency now? Why would I give up the freedom to leave whenever I damn well feel like it, even if there are no plans to leave at the moment?

Not only do I give my wife my yearly bonus, I've also developed the habit of giving Mom my salary increases. Why? Same reasoning. It's a bit of extra money that I don't care about, it keeps 'em happy, and that's a great ROI. Most importantly, it keeps me lean. And hungry. It was that same hunger that had me going back to school. Learning new tech tools. Picking up skillsets from related disciplines such as DevOps, Data Analytics and Mobile Development. It was that very hunger that had me build this blog and keep honing my craft.

It's the principle of the thing. Financially, I'm in a good place and don't need the money. I have worked long and hard, over the course of the last decade, to have the luxury of being able to easily ignore the lure of immediate money, in favor of things of greater value. And approaching the age of 50, autonomy in my career is arguably more important than ever. Definitely more important than an extra few thousand bucks.

Conclusion

Losing my job isn't high on the list of things to worry about. Come to think of it, it might not even be on the list. No matter what you tell yourself so you can get out of bed in the morning, a job is ultimately just a job. Your ability to land a job is far more important than the job itself.

Gotta maintain that edge.

It's more my edge I'm afraid of losing. Not being some special tech talent that can walk into any company and demand the sky, I only got this far due to enthusiasm and motivation. Not complacency. Due to careful financial management, I already have way more money than I realistically need.

I am in charge of my own career, and will give that up when hell freezes over.

Be$t wi$he$,
T___T

Wednesday, 23 April 2025

The case of the ill-considered feature quotation

When you're in-house tech personnel dealing with external tech vendors, sometimes experience and common sense can be useful.

Also, the willingness to prioritize professional duty over being liked. I mention this because that's not me - I actively try not to be the guy who ruins everyone's workday by nitpicking on small details in the name of being "thorough". Work does suck for a lot of people, and there's just no point in making it suck more than it already does, without good reason.

That's far enough.

Except, sometimes, there is a good reason. Sometimes, there are multiple good reasons to dig your heels in and say "that's far enough, buddy".

This is such a story I'm going to tell today.

What happened

My company at the time had contracted a vendor to store our data, which would be sent to them by means of an API endpoint they provided us. Admittedly, they weren't the solution provider I would have gone with, but in the interest of saving time (and also because I didn't have a better alternative), I played nice. After ascertaining that the solution worked - i.e, our system would send data though that API endpoint call and the data would be saved in their system, it was time to talk about security.

Just a key.

Our proposed solution was to have an extra property in the JSON object that we were sending them, with a password that they would provide. Like an API key. Any calls made using that key would be validated against their records, so that they could at least be confident that the origin of the data would be correct.

The solution was simple enough, and their Sales Representative told us it could be done. But then he made the mistake of telling us that we would be quoted an extra charge for it. And that was when I drew a line in the sand, and told them, no, this was absolutely not going to happen.

In retrospect, the fact that I was the one who had to broach the subject of security was a red flag. If I hadn't, would they just have carried on? Alarming if one considered that we weren't their only clients.

Why I put my foot down

The extra feature we requested was for security. Security should always be considered a basic, rather than extra, feature. Especially when the service providers in question are holding on to client data. If these vendors had our data in their storage, why should we have to pay extra for a basic security feature?

Also, in the event of a data breach, could these vendors really afford not to be able to show subsequent audit that they, at the very least, had previously done their due diligence?

Security should be a
basic feature.

From my experience with small vendors like these, they usually aren't using separate database servers for different clients. More often than not, it's some sort of shared virtual hosting. Which meant that any data breach on our part could affect their other clients, and vice versa.

All in all, the vendors had significantly more to lose than we did, from a security breach. From that viewpoint, it was patently ridiculous for them to want to charge us for security. That would be like me demanding payment from the locksmith to install a lock on my own door.

There is the possibility that their Sales Representative was not really thinking clearly, and that he was asking for extra payment out of sheer habit. Because that was the way he had been trained. Honestly, I don't think this made it better. Definitely didn't make my company's data feel more secure in their hands.

Conclusion

Going with the flow is easy. Standing firm on principle is harder. In fact, I would argue that standing firm when the situation is as egregious as this, is easy. The hard part is really identifying such situations in the first place. I don't think there's exactly a playbook for things like that.

Thanks for reading, that's far enough!
T___T