Showing posts with label Javascript. Show all posts
Showing posts with label Javascript. Show all posts

Tuesday, 25 August 2026

Web Tutorial: Pill Puzzle (Part 4/4)

We've developed the ability to place pills on either side of the scale, and return them to their original positions if required. Time to work on using the scale. For that, we ensure that the weigh() method is invoked when the button is clicked.
<div><button id="btnWeigh" onclick="pillgame.weigh()">WEIGH</button></div>


Then we work on this method. We begin with that return statement at the end. Then we exit early if playerStatus is not "ill".
weigh: function()
{
  if (this.playerStatus != "ill") return;

  return;

},


We also run setWeightTimes(), as we should.
weigh: function()
{
  if (this.playerStatus != "ill") return;
  this.setWeightTimes(this.weighTimes + 1);

  return;
},


Next, we define leftWeight. What we do is use the map() method to iterate through all the values of pills_left, which are indexes into the pills array, in effect making a new array based on all the weights of the pills that are in pills_left.
weigh: function()
{
  if (this.playerStatus != "ill") return;
  this.setWeightTimes(this.weighTimes + 1);

  const leftWeight = this.pills_left
  .map((x) => this.pills[x])


  return;
},


Then we use the reduce() method, totalling all the values.
weigh: function()
{
  if (this.playerStatus != "ill") return;
  this.setWeightTimes(this.weighTimes + 1);

  const leftWeight = this.pills_left
  .map((x) => this.pills[x])
  .reduce((sum, weight) => sum + weight, 0);

  return;
},


We do something similar for rightWeight. And after that, we have three If blocks to handle the various outcomes.
weigh: function()
{
  if (this.playerStatus != "ill") return;

  this.setWeightTimes(this.weighTimes + 1);
  const leftWeight = this.pills_left
  .map((x) => this.pills[x])
  .reduce((sum, weight) => sum + weight, 0);

  const rightWeight = this.pills_right
  .map((x) => this.pills[x])
  .reduce((sum, weight) => sum + weight, 0);

  if (leftWeight == rightWeight)
  {
    return;
  }

  if (leftWeight > rightWeight)
  {
    return;
  }

  if (leftWeight < rightWeight)
  {
    return;
  }


  return;
},


If leftWeight is heavier than rightWeight, pnlWeighPillsContainerLeft should go down and pnlWeighPillsContainerRight should go up, thus the justify-content properties of both need to be adjusted accordingly - pnlWeighPillsContainerLeft to flex-end (to sink to the "bottom") and pnlWeighPillsContainerRight to flex-start (to float to the "top"). The opposite is true for if rightWeight is heavier than leftWeight. And if both are equally heavy, both pnlWeighPillsContainerLeft and pnlWeighPillsContainerRight sink to the bottom.
weigh: function()
{
  if (this.playerStatus != "ill") return;
  this.setWeightTimes(this.weighTimes + 1);

  const leftWeight = this.pills_left
  .map((x) => this.pills[x])
  .reduce((sum, weight) => sum + weight, 0);

  const rightWeight = this.pills_right
  .map((x) => this.pills[x])
  .reduce((sum, weight) => sum + weight, 0);

  if (leftWeight == rightWeight)
  {
    $("#pnlWeighPillsContainerLeft").css("justifyContent", "flex-end");
    $("#pnlWeighPillsContainerRight").css("justifyContent", "flex-end");


    return;
  }

  if (leftWeight > rightWeight)
  {
    $("#pnlWeighPillsContainerLeft").css("justifyContent", "flex-end");
    $("#pnlWeighPillsContainerRight").css("justifyContent", "flex-start");


    return;
  }

  if (leftWeight < rightWeight)
  {
    $("#pnlWeighPillsContainerLeft").css("justifyContent", "flex-start");
    $("#pnlWeighPillsContainerRight").css("justifyContent", "flex-end");


    return;
  }

  return;
},


Let's try this! Place pill 1 on the left and pill 2 on the right.

Then click WEIGH button. See that it now indicates that the scale has been used once! In all likelihood, both pills are equal in weight, so both scale bowls should sink.

Refresh. Now place pill 1 on the left and pill 2 on the right... and then add pills 3 and 4 on the left.

Now weigh. The left side will sink and the right side will rise! And now the display says you have used the scale once.

Now place the rest of the pills on the right side...

...and weigh. The right side should sink and the left should float. The display should say you have used the scale twice, and the button should disappear.

Now for the final part!

Feeding any given pill to the mouth. For this, add a click action to the face. It should call placePill(), with "mouth" passed in as an argument.
<div id="playerFace" onclick="pillgame.placePill('mouth')"></div>


There are only two scenarios here - success or failure. That depends on the weight of the pill selected. We derive that by using the value of pill_selected as an index into pills. And this leads into the two If blocks.
if (slot == "mouth")
{
  if (this.pills[this.pill_selected] == 1)
  if (this.pills[this.pill_selected] == 10)


  return;
}


If the pill is light, we use setPlayerState() to set the player state to "well". If it's a heavy pill, then it's poison and you just killed the patient. Either way, we run gameOver() because the game is effectively over at this point.
if (slot == "mouth")
{
  if (this.pills[this.pill_selected] == 1) this.setPlayerState("well");
  if (this.pills[this.pill_selected] == 10) this.setPlayerState("dead");
  this.gameOver();


  return;
}


Now for the final method, gameOver(). We basically return stuff to factory settings, visually.
gameOver: function()
{
  $("#btnWeigh").hide();
  $(".pnlWeighPillsContainer").css("justifyContent", "center");
  this.pills_middle = [];
  this.pills_left = [];
  this.pills_right = [];
  this.renderPills();

  return;

}


Now remove the red lines and we're all set.
div {outline: 0px solid red;}


Final test!

It's easy to fail. Select any pill at random and click on the face. There's an 87.5% chance that you kill the patient.

But to cure the patient is the crux of how this puzzle is solved. Place pills 1, 2 and 3 on the left. Pills 4, 5 and 6 go on the right.

Weigh. You see that Pills 1, 2 and 3 are lighter, so the cure is definitely in that group.

Return pills 4, 5 and 6.

Return pills 2 and 3.

Place pill 2 on the right side of the scale, and weigh. Looks like pill 2 is still heavier.

Which makes pill 1 the cure. Feed it to the face, and you win!


We're done! Not too shabby, eh?

Fare "well",
T___T

Saturday, 22 August 2026

Web Tutorial: Pill Puzzle (Part 3/4)

All right! Now that we have a mechanism by which to display pills, we need to enable placing and returning them via the interface. For this, let us add a click action to the DOM. It will run placePill(), with an argument describing which location to place the pill in.

<div id="pnlWeighPillsContainerLeft" class="pnlWeighPillsContainer">
  <div class="pnlWeighPills" id="pnlWeighPills_left" onclick="pillgame.placePill('left')">
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div><div class="bowl"></div></div>
  </div>        
</div>

<div class="scaleContainer">
  <div class="pnlScale">⚖</div>
  <div>Times Used: <span id="pnlTimesUsed"></span></div>
  <div><button id="btnWeigh" onclick="pillgame.weigh()">WEIGH</button></div>
</div>

<div id="pnlWeighPillsContainerRight" class="pnlWeighPillsContainer">
  <div class="pnlWeighPills" id="pnlWeighPills_right" onclick="pillgame.placePill('right')">
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div><div class="bowl"></div></div>
  </div>        
</div>


We'll work on the placePill() method next. It accepts a parameter, slot. We have the default return statement, and a guard clause that says if playerStatus is anything but "ill", nothing happens. Which makes sense; in those cases the game is over and nothing needs to happen.
placePill: function(slot)
{
  if (this.playerStatus != "ill") return;

  return;

},


Now let's have a few cases. The first case is for if no pill is selected; that's when we return the pill rather than placing it. The second case is for if slot is "mouth", which means a pill has been selected and we're about to feed it to the face at the bottom. The last two are "left" and "right", and those don't have curly brackets because they're one-line operations.
placePill: function(slot)
{
  if (this.playerStatus != "ill") return;

  if (this.pill_selected == -1)
  {
    return;
  }

  if (slot == "mouth")
  {
    return;
  }

  if (slot == "left")
  if (slot == "right")


  return;
},


Let's handle the last two cases first. If pill_selected is not -1, that means there is a pill selected. If that's the case, we run the unshift() method on the appropriate array to place the selected pill at the beginning of the array.
placePill: function(slot)
{
  if (this.playerStatus != "ill") return;

  if (this.pill_selected == -1)
  {
    return;
  }

  if (slot == "mouth")
  {
    return;
  }

  if (slot == "left") this.pills_left.unshift(this.pill_selected);
  if (slot == "right") this.pills_right.unshift(this.pill_selected);

  return;
},


After that, declare index, and check if pills_middle still has the pill selected. If it does, remove it using the splice() method. Then set pill_selected to -1 and run renderPills().
placePill: function(slot)
{
  if (this.playerStatus != "ill") return;

  if (this.pill_selected == -1)
  {
    return;
  }

  if (slot == "mouth")
  {
    return;
  }

  if (slot == "left") this.pills_left.unshift(this.pill_selected);
  if (slot == "right") this.pills_right.unshift(this.pill_selected);

  const index = this.pills_middle.indexOf(this.pill_selected);

  if (index !== -1) this.pills_middle.splice(index, 1);

  this.pill_selected = -1;
  this.renderPills();


  return;
},


Let's test this! Click START. Click on pill 3 and then on the left side. Does pill 3 appear on the left?

Now click on pill 4 and then on the right side. Does pill 4 appear on the right?

What if you then clicked on pill 7 and then on the left side?

Go on, add more pills on the left and right side of the scale. Notice how the pills disappear from the middle when you place them.

Now let's handle replacing the pills. If no pills are selected (meaning pill_selected is -1), running placePill() should result in running returnPill(). But I'm getting ahead of myself; let's begin with the first If block in the placePill() method. We establish two more If blocks in those, and an early return statement after. These check for the value of slot, and ensure that there are actually elements in the relevant array before taking any action.
if (this.pill_selected == -1)
{
  if (slot == "left" && this.pills_left.length > 0)
  {

  }

  if (slot == "right" && this.pills_right.length > 0)
  {

  }

  return;

}


Here, you set pill_selected as the first element of pills_left. Then you remove it from pills_left using the shift() method.
if (this.pill_selected == -1)
{
  if (slot == "left" && this.pills_left.length > 0)
  {
    this.pill_selected = this.pills_left[0];
    this.pills_left.shift();

  }

  if (slot == "right" && this.pills_right.length > 0)
  {

  }

  return;
}


With that, you run the returnPill() method (we'll build that soon) and then renderPills() to update the display.
if (this.pill_selected == -1)
{
  if (slot == "left" && this.pills_left.length > 0)
  {
    this.pill_selected = this.pills_left[0];
    this.pills_left.shift();
    this.returnPill();
    this.renderPills();

  }

  if (slot == "right" && this.pills_right.length > 0)
  {

  }

  return;
}


We perform the analogous operations for the opposite side.
if (this.pill_selected == -1)
{
  if (slot == "left" && this.pills_left.length > 0)
  {
    this.pill_selected = this.pills_left[0];
    this.pills_left.shift();
    this.returnPill();
    this.renderPills();
  }

  if (slot == "right" && this.pills_right.length > 0)
  {
    this.pill_selected = this.pills_right[0];
    this.pills_right.shift();
    this.returnPill();
    this.renderPills();

  }

  return;
}


Now, what does returnPill() do?
returnPill()
{
  return;
},


It places the selected pill back in pills_middle via the push() method. Once that is done, pill_selected becomes -1 again.
returnPill()
{
  this.pills_middle.push(this.pill_selected);
  this.pill_selected = -1;


  return;
},


Go on, try it. Reproduce what we did in the previous screenshot, then click on the left side of the scale. Does pill 7 get sent back to the middle?

We've been using a fair amount of JavaScript array methods here. If you feel like reading up further, here are some references.
- shift()
- unshift()
- push()
- splice()

Next

Weighing the pills and delivering the cure.

Wednesday, 19 August 2026

Web Tutorial: Pill Puzzle (Part 2/4)

Now that the HTML is largely out of the way, it's time to lay out the JavaScript skeleton.

This is pillgame. It's the overall game object that contains the properties and methods we'll be using.
<script>
  let pillgame =
  {

  };

</script>


Let's have some properties. playerStatus is a string (default value "ill") which acts as an index into the object playerStates. pill_selected tracks which pill is currently in selection, and a negative number just means that no pill is selected. pills, pills_middle, pills_left and pills_right are arrays. weighTimes is an integer that defaults to 0, and tracks how many times the scale has been used.
<script>
  let pillgame =
  {
    playerStatus: "ill",
    playerStates:
    {
  
    },
    pill_selected: -1,
    pills: [],
    pills_middle: [],
    pills_left: [],
    pills_right: [],
    weighTimes: 0

  };
</script>


Now you see what I mean by playerStatus being an index into playerStates. playerStates is an object that has three different objects - ill, well and dead. Each one has face, which is a HTML symbol, color and message.
<script>
  let pillgame =
  {
    playerStatus: "ill",
    playerStates:
    {
      "ill":
      {
        "face": "&#9785;",
        "color": "#004400",
        "message": "Slowly dying..."
      },
      "well":
      {
        "face": "&#9786;",
        "color": "#FFFF00",
        "message": "Cured! Congratulations!"
      },
      "dead":
      {
        "face": "&#9760;",
        "color": "#404040",
        "message": "Dead. Too bad!"
      }

    },
    pill_selected: -1,
    pills: [],
    pills_middle: [],
    pills_left: [],
    pills_right: [],
    weighTimes: 0
  };
</script>


Now for the arrays. pills is the array of 8 pills. The figures show their weights. They're all a 10. 10 pounds, 10 grams - it doesn't matter. It's just a number, and the point is that right now they all have the same number. pills_middle has 8 elements. They are the index references to the elements in pills. When the game starts, by default, the pills are in this slot. pills_left and pills_right are also placement arrays, but at the start of the game, they are empty.
<script>
  let pillgame =
  {
    playerStatus: "ill",
    playerStates:
    {
      "ill":
      {
        "face": "☹",
        "color": "#004400",
        "message": "Slowly dying..."
      },
      "well":
      {
        "face": "☺",
        "color": "#FFFF00",
        "message": "Cured! Congratulations!"
      },
      "dead":
      {
        "face": "☠",
        "color": "#404040",
        "message": "Dead. Too bad!"
      },
    },
    pill_selected: -1,
    pills: [10, 10, 10, 10, 10, 10, 10, 10],
    pills_middle: [0, 1, 2, 3, 4, 5, 6, 7],
    pills_left: [],
    pills_right: [],
    weighTimes: 0
  };
</script>


And after this, we have 9 methods.
<script>
  let pillgame =
  {
    playerStatus: "ill",
    playerStates:
    {
      "ill":
      {
        "face": "☹",
        "color": "#004400",
        "message": "Slowly dying..."
      },
      "well":
      {
        "face": "☺",
        "color": "#FFFF00",
        "message": "Cured! Congratulations!"
      },
      "dead":
      {
        "face": "☠",
        "color": "#404040",
        "message": "Dead. Too bad!"
      },
    },
    pill_selected: -1,
    pills: [10, 10, 10, 10, 10, 10, 10, 10],
    pills_middle: [0, 1, 2, 3, 4, 5, 6, 7],
    pills_left: [],
    pills_right: [],
    weighTimes: 0,
    weigh: function()
    {

    },
    setWeighTimes: function(times)
    {

    },
    setPlayerState: function(state)
    {

    },
    start: function()
    {

    },
    renderPills: function()
    {

    },
    selectPill: function(index)
    {

    },
    placePill: function(slot)
    {

    },
    returnPill()
    {

    },
    gameOver: function()
    {

    }

  };
</script>


We are going to start building with the method start(). First, set this method to call when the button is clicked. Make sure you add the id, btnStart, as well.
<button id="btnStart" onclick="pillgame.start()">START</button>


And here's the styling for that. I just made it ultra-big.
.bowl
{
  width: 60px;
  height: 20px;
  background-color: rgb(200, 200, 200);
  border-top: none;
  border-radius: 0 0 10px 10px;
  margin: 0 auto 0 auto;
}

#btnStart
{
  width: 10em;
  height: 3em;
  border-radius: 3px;        
  font-size: 2em;
}


We begin with a return statement at the end, which is probably unnecessary but I like to form the habit of. Then we run the methods setPlayerState() with the argument "ill" and setWeighTimes() with the argument 0. Because these are the beginning states.
start: function()
{
  this.setPlayerState("ill");
  this.setWeighTimes(0);

  return;

},


setPlayerState() takes state and sets playerStatus to that value. Then it populates playerFace with the face property value of the element in playerStates pointed to by state, and sets the appropriate color. It also similarly sets the content of playerMessage.
setPlayerState: function(state)
{
  this.playerStatus = state;
  $("#playerFace").html(this.playerStates[state].face);
  $("#playerFace").css("color", this.playerStates[state].color);
  $("#playerMessage").text(this.playerStates[state].message);

  return;

},


As for setWeighTimes(), we take times and set weighTimes to that value. Then we ensure that the content of pnlTimesUsed is times. If times is equal to 2, which means the scale can no longer be used, we hide btnWeigh,
setWeighTimes: function(times)
{
  this.weighTimes = times;
  $("#pnlTimesUsed").text(times);
  if (times == 2) $("#btnWeigh").hide();

  return;

},


Now what are pnlTimesUsed and btnWeigh? Right here. For btnWeigh, we should even make sure it runs weigh() when clicked.
<div class="scaleContainer">
  <div class="pnlScale">⚖</div>
  <div>Times Used: <span id="pnlTimesUsed"></span></div>
  <div><button id="btnWeigh" onclick="pillgame.weigh()">WEIGH</button></div>
</div>


The style for btnWeigh is here. It's mostly cosmetic, except that it's set to be invisible by default using the display property.
.bowl
{
  width: 60px;
  height: 20px;
  background-color: rgb(200, 200, 200);
  border-top: none;
  border-radius: 0 0 10px 10px;
  margin: 0 auto 0 auto;
}

#btnWeigh
{
  width: 5em;
  height: 2em;
  border-radius: 3px;
  display: none;
}


#btnStart
{
  width: 10em;
  height: 3em;
  border-radius: 3px;        
  font-size: 2em;
}


See what happens here. The START button is huge. The WEIGH button is missing.

Click the START button, and the deep green face comes on because we ran setPlayerState().

We continue! We'll carry on by displaying the btnWeigh button, and changing the text on btnStart to read "RESTART". And we use the css() method to ensure that the divs styled by pnlWeighPillsContainer have the justify-content property reset to center.
start: function()
{
  this.setPlayerState("ill");
  this.setWeightTimes(0);
  $("#btnWeigh").show();
  $("#btnStart").html("RESTART");
  $(".pnlWeighPillsContainer").css("justifyContent", "center");


  return;
},


Here, we reset pills to its original state... and then we randomly select one of these to be 1 instead of 10, making it the lightest (and the cure!).
start: function()
{
  this.setPlayerState("ill");
  this.setWeightTimes(0);
  $("#btnWeigh").show();
  $("#btnStart").html("RESTART");
  $(".pnlWeighPillsContainer").css("justifyContent", "center");

  this.pills = [10, 10, 10, 10, 10, 10, 10, 10];
  this.pills[Math.floor(Math.random() * 8)] = 1;


  return;
},


Then we reset the values for pills_middle, pills_left and pills_right, and pill_selected. We also run the renderPills() method, which we will get to next.
start: function()
{
  this.setPlayerState("ill");
  this.setWeightTimes(0);
  return;
  $("#btnWeigh").show();
  $("#btnStart").html("RESTART");
  $(".pnlWeighPillsContainer").css("justifyContent", "center");

  this.pills = [10, 10, 10, 10, 10, 10, 10, 10];
  this.pills[Math.floor(Math.random() * 8)] = 1;

  this.pills_middle = [0, 1, 2, 3, 4, 5, 6, 7];
  this.pills_left = [];
  this.pills_right = [];
  this.renderPills();
  this.pill_selected = -1;


  return;
},


We start by making sure all div tags styled with the CSS class pillSlot, are cleared. And of course, a return statement.
renderPills: function()
{
  $(".pillSlot").html("");

  return;

},


And then we iterate through pills_middle using a forEach loop.
renderPills: function()
{
  $(".pillSlot").html("");

  this.pills_middle.forEach
  (

  );


  return;
},


The callback uses x, which is the element, and i, which is the index.
renderPills: function()
{
  $(".pillSlot").html("");

  this.pills_middle.forEach
  (
    (x, i) =>
    {

    }

  );

  return;
},


In here, we declare pill as a div element, and style it using the CSS classes pill and topView. We also ensure that the text within has a number. Since we want this to be user-friendly, we're going to avoid index 0 and just add 1 to everything.
renderPills: function()
{
  $(".pillSlot").html("");

  this.pills_middle.forEach
  (
    (x, i) =>
    {
      const pill = $("<div></div>");
      pill.addClass("pill topView");
      pill.text(x + 1);

    }
  );

  return;
},


Remember each div within pillsContainer was styled using pillSlot? Well, right now, these are the slots for the elements in pills_middle. We reference each one, declaring slot, then append the div element pill to it.
renderPills: function()
{
  $(".pillSlot").html("");

  this.pills_middle.forEach
  (
    (x, i) =>
    {
      const pill = $("<div></div>");
      pill.addClass("pill topView");
      pill.text(x + 1);

      const slot = $("#pillsContainer .pillSlot")[i];

      $(slot).append(pill);

    }
  );

  return;
},


And we also add a click action to pill, calling selectPill() and passing in x as an argument.
renderPills: function()
{
  $(".pillSlot").html("");

  this.pills_middle.forEach
  (
    (x, i) =>
    {
      const pill = $("<div></div>");
      pill.addClass("pill topView");
      pill.text(x + 1);
      pill.on("click", () => {
        this.selectPill(x);
      });

      const slot = $("#pillsContainer .pillSlot")[i];

      $(slot).append(pill);
    }
  );

  return;
},


Here's some CSS for pill. Generally, we're setting font size and color.
.pnlWeighPills
{
  display: grid;
  grid-template-rows: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
  gap: 1px;
}  

.pill
{
  font-family: sans-serif;
  font-size: 1em;
  font-weight: bold;
  color: rgba(0, 0, 0, 0.8);
}


.bowl
{
  width: 60px;
  height: 20px;
  background-color: rgb(200, 200, 200);
  border-top: none;
  border-radius: 0 0 10px 10px;
  margin: 0 auto 0 auto;
}


topView is the one with more meat. We want a circular div with a grey linear gradient background with a solid light grey outline. height is 15 pixels lower than width because we have a 15 pixel padding at the top.
.pnlWeighPills
{
  display: grid;
  grid-template-rows: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
  gap: 1px;
}  

.pill
{
  font-family: sans-serif;
  font-size: 1em;
  font-weight: bold;
  color: rgba(0, 0, 0, 0.8);
}

.topView
{
  width: 50px;
  height: 35px;
  outline: 2px solid rgb(200, 200, 200);
  border-radius: 50%;
  background: linear-gradient(45deg, rgb(100, 100, 100), rgb(200, 200, 200));
  text-align: center;
  margin: 0 auto 0 auto;
  padding-top: 15px;
}


.bowl
{
  width: 60px;
  height: 20px;
  background-color: rgb(200, 200, 200);
  border-top: none;
  border-radius: 0 0 10px 10px;
  margin: 0 auto 0 auto;
}


There you go, 8 pills! All numbered, too!

Now, let's set the numbers to highlight if moused over. And turn to red if selected.
#pillsContainer
{
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
  gap: 5px;
  height: 100px;
}

#pillsContainer div:hover
{
  color: rgb(255, 255, 255);
}

#pillsContainer div.selected
{
  color: rgb(255, 0, 0);
}


.pillSlot
{
  cursor: pointer;
}


And for that, we continue on with working on the selectPill() method. The method accepts a parameter, index. Start with a return statement, then use the removeClass() method to remove the CSS class selected from all elements styled using the pill CSS class.
selectPill: function(index)
{
  $(".pill").removeClass("selected");

  return;

},


Set pill_selected to index. Then declare searchVal and set it to the value of index, plus 1. This is the value to search for in the DOM.
selectPill: function(index)
{
  $(".pill").removeClass("selected");
  this.pill_selected = index;
  var searchVal = index + 1;


  return;
},


Now we go through all elements that are styled using the CSS class pill, and return the one where its value from the text() method is the same as searchVal.
selectPill: function(index)
{
  $(".pill").removeClass("selected");
  this.pill_selected = index;
  var searchVal = index + 1;

  $(".pill").filter
  (
    function ()
    {
      return $(this).text().trim() == searchVal;
    }
  )


  return;
},


And use addClass() to add the class "selected".
selectPill: function(index)
{
  $(".pill").removeClass("selected");
  this.pill_selected = index;
  var searchVal = index + 1;

  $(".pill").filter
  (
    function ()
    {
      return $(this).text().trim() == searchVal;
    }
  )
  .addClass("selected");

  return;
},


Try mousing over pill number 4.

Now click on it! It turns red!

We may need to place pills on the left and right sides of the scale as well. Declare starting_index.
renderPills: function()
{
  $(".pillSlot").html("");

  var starting_index = 0;

  this.pills_middle.forEach


Because of the law of gravity, pills will always be rendered from the bottom of the pile rather than at the top. Unfortunate, in the collection of divs styled using the CSS class pillSlot within pnlWeighPills_left (which also corresponds to the array pills_left), the number starts at the top. So what do we do? We set starting_index as 8 (which is the total number of pill slots) less the actual number of pills in pills_left. Then we iterate through pills_left the same way we iterated through pills_middle.
renderPills: function()
{
  $(".pillSlot").html("");

  var starting_index = 0;

  starting_index = (8 - this.pills_left.length);
  this.pills_left.forEach
  (
    (x, i) =>
    {

    }
  );


  this.pills_middle.forEach
  (
    (x, i) =>
    {
      const pill = $("<div></div>");
      pill.addClass("pill topView");
      pill.text(x + 1);
      pill.on("click", () => {
        this.selectPill(x);
      });
      const slot = $("#pillsContainer .pillSlot")[i];

      $(slot).append(pill);
    }
  );

  return;
},


We create the div and insert it into slot. Except that now slot is defined as that div styled using CSS class pillSlot within pnlWeighPills_left, and we start at starting_index, plus whatever value i is at the moment. i is, of course, the index of the current element of pills_left. So if you're processing the first element of pills_left and pills_left has 3 elements, it will render inside the fifth element of pnlWeighPills_left. The second element of pills_left will render inside the sixth element of pnlWeighPills_left. And so on.
renderPills: function()
{
  $(".pillSlot").html("");

  var starting_index = 0;

  starting_index = (8 - this.pills_left.length);
  this.pills_left.forEach
  (
    (x, i) =>
    {
      const pill = $("<div></div>");
      pill.addClass("pill sideView");
      pill.text(x + 1);
      const slot = $("#pnlWeighPills_left .pillSlot")[starting_index + i];

      $(slot).append(pill);

    }
  );

  this.pills_middle.forEach
  (
    (x, i) =>
    {
      const pill = $("<div></div>");
      pill.addClass("pill topView");
      pill.text(x + 1);
      pill.on("click", () => {
        this.selectPill(x);
      });
      const slot = $("#pillsContainer .pillSlot")[i];

      $(slot).append(pill);
    }
  );

  return;
},


We'll do pretty much the same for pills_right and pnlWeighPills_right. Notice that in both cases, we use CSS class sideView rather than topView.
renderPills: function()
{
  $(".pillSlot").html("");

  var starting_index = 0;

  starting_index = (8 - this.pills_left.length);
  this.pills_left.forEach
  (
    (x, i) =>
    {
      const pill = $("<div></div>");
      pill.addClass("pill sideView");
      pill.text(x + 1);
      const slot = $("#pnlWeighPills_left .pillSlot")[starting_index + i];

      $(slot).append(pill);
    }
  );

  starting_index = (8 - this.pills_right.length);
  this.pills_right.forEach
  (
    (x, i) =>
    {
      const pill = $("<div></div>");
      pill.addClass("pill sideView");
      pill.text(x + 1);
      const slot = $("#pnlWeighPills_right .pillSlot")[starting_index + i];

      $(slot).append(pill);
    }
  );


  this.pills_middle.forEach
  (
    (x, i) =>
    {
      const pill = $("<div></div>");
      pill.addClass("pill topView");
      pill.text(x + 1);
      pill.on("click", () => {
        this.selectPill(x);
      });
      const slot = $("#pillsContainer .pillSlot")[i];

      $(slot).append(pill);
    }
  );

  return;
},


If you're wondering where the heck pnlWeighPills_right and pnlWeighPills_left came from, they're the ids for the parents of those divs.
<div class="pnlWeighPillsContainer">
  <div class="pnlWeighPills" id="pnlWeighPills_left">
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div><div class="bowl"></div></div>
  </div>
</div>

<div class="scaleContainer">
  <div class="pnlScale">⚖</div>
  <div>Times Used: <span id="pnlTimesUsed"></span></div>
  <div><button id="btnWeigh" onclick="pillgame.weigh()">WEIGH</button></div>
</div>

<div class="pnlWeighPillsContainer">
  <div class="pnlWeighPills" id="pnlWeighPills_right">
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div class="pillSlot"></div>
    <div><div class="bowl"></div></div>
  </div>
</div>


This is the styling for sideView.
.topView
{
  width: 50px;
  height: 35px;
  outline: 2px solid rgb(200, 200, 200);
  border-radius: 50%;
  background: linear-gradient(45deg, rgb(100, 100, 100), rgb(200, 200, 200));
  text-align: center;
  margin: 0 auto 0 auto;
  padding-top: 15px;
}

.sideView
{
  width: 50px;
  height: 20px;
  outline: 2px solid rgb(200, 200, 200);
  background: linear-gradient(90deg, rgb(100, 100, 100), rgb(200, 200, 200), rgb(100, 100, 100));
  margin: 0 auto 0 auto;
}


.bowl
{
  width: 60px;
  height: 20px;
  background-color: rgb(200, 200, 200);
  border-top: none;
  border-radius: 0 0 10px 10px;
  margin: 0 auto 0 auto;
}


We can test this with some temporary code here...
{
  this.setPlayerState("ill");
  this.setWeightTimes(0);

  $("#btnWeigh").show();
  $("#btnStart").html("RESTART");
  $(".pnlWeighPillsContainer").css("justifyContent", "center");

  this.pills = [10, 10, 10, 10, 10, 10, 10, 10];
  this.pills[Math.floor(Math.random() * 8)] = 1;

  this.pills_middle = [0, 1, 2, 3, 4, 5, 6, 7];
  this.pills_left = [];
  this.pills_right = [];
  this.pills_left = [0, 1, 5];
  this.pills_right = [2, 3, 6, 7, 4];

  this.renderPills();
  this.pill_selected = -1;

  return;
},


See?

Comment out the code we just wrote, or erase it. We've done what we set out to do - display pills.

Next

Placing and returning pills.

Sunday, 16 August 2026

Web Tutorial: Pill Puzzle (Part 1/4)

During some audio Social Media dialogue on the Clubhouse app, I overheard someone describe a puzzle. Something along the lines of - you've been poisoned and about to die. There are eight pills, and any one could be the cure. One is deadly poison. It's the lightest pill, but only by a fraction. You have a weighing scale you can use twice.

I thought about it, and I wanted to make this game. Except that I would switch things around a bit. One of the pills is the cure and the rest are poison. Now that would make things more deadly. And exciting!

We will be using jQuery for this, because I've come a long way the last ten years and sometimes I can't be arsed to deal with vanilla JavaScript. This is one of those times.

We start off with this HTML, ensuring that the jQuery library is included, and setting font size and background color to black. div tags will be outlined in red while we built the interface.
<!DOCTYPE html>
<html>
  <head>
    <title>Pill Puzzle</title>

    <style>
      body
      {
        font-size: 12px;
        font-family: verdana;
        text-align: center;
        background-color: rgb(0, 0, 0);
        color: rgb(200, 200, 200);
      }

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

    <script src="https://code.jquery.com/jquery-3.7.1.min.js"></script>

    <script>

    </script>
  </head>

  <body>

  </body>
</html>


We'll divide the interface into three divs and style them accordingly.
<body>
    <div class="topContainer">

    </div>

    <div class="middleContainer">

    </div>

    <div class="bottomContainer">

    </div>

</body>


The top is 400 pixels wide, the middle 600, and the bottom 500. All of them use the margin property to center themselves.
<style>
body
{
    font-size: 12px;
    font-family: verdana;
    text-align: center;
    background-color: rgb(0, 0, 0);
    color: rgb(200, 200, 200);
}

div {outline: 0px solid red;}

.topContainer
{
    width: 400px;
    margin: 0 auto 0 auto;
}

.middleContainer
{
    width: 600px;
    margin: 0 auto 0 auto;
}      

.bottomContainer
{
    width: 500px;
    margin: 0 auto 0 auto;
}

</style>


Part of this exercise is my current flirtation with grid and flexbox, as you'll soon see. I want this div to contain three columns, the middle one widest.
<div class="topContainer">
    <div>

    </div>

    <div>

    </div>

    <div>

    </div>

</div>


And if we do this, we ensure the middle column is the biggest, with a 20 pixel space between all three.
.topContainer
{
    display: grid;
    grid-template-columns: 1fr 2fr 1fr;
    gap: 20px;

    width: 400px;
    margin: 0 auto 0 auto;
}


In the first div, we go to its center div and add three more divs inside. In the first one, we have the scale symbol. The second one is used to display how many times the scale has been used. And the last one contains a button used for weighing.
<div class="topContainer">
  <div>
  
  </div>

  <div>
    <div>&#x2696;</div>
    <div>Times Used: </div>
    <div><button>WEIGH</button></div>

  </div>

  <div>
  
  </div>
</div>


This is just the start.

Now for the sides. Each of these will contain another div, and in turn will contain 9 more divs! These are placeholders for the pills. There are going to be 8 pills, so theoretically, there's a maximum of 8 placeholders on each side. The ninth one is the scale bowl at the bottom!
<div class="topContainer">
  <div>
    <div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
    </div>

  </div>

  <div>
    <div>&#x2696;</div>
    <div>Times Used: </div>
    <div><button>WEIGH</button></div>
  </div>

  <div>
    <div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
    </div>

  </div>
</div>


To really hammer home the visuals, we're going to do some styling here. The left and right divs will be styled using pnlWeighPillsContainer. The first immediate div within will be styled using pnlWeighPills. The middle div is styled using scaleContainer, and that first div within is styled using pnlScale.
<div class="topContainer">
  <div class="pnlWeighPillsContainer">
    <div class="pnlWeighPills">
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
    </div>
  </div>

  <div class="scaleContainer">
    <div class="pnlScale">&#x2696;</div>
    <div>Times Used: </div>
    <div><button>WEIGH</button></div>
  </div>

  <div class="pnlWeighPillsContainer">
    <div class="pnlWeighPills">
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
    </div>
  </div>
</div>


Now for the CSS. scaleContainer is a grid container that divides its content into three rows, with the top row getting the most space. pnlScale neccessitates that because we've set the font size to a large one. pnlWeighPillsContainer is a flexbox container where the alignment is vertical and by default, content is set to the middle. However, its immediate child, pnlWeightPills, is a grid with nine rows, each having equal billing.
body
{
  font-size: 12px;
  font-family: verdana;
  text-align: center;
  background-color: rgb(0, 0, 0);
  color: rgb(200, 200, 200);
}

div {outline: 1px solid red;}

.topContainer
{
  display: grid;
  grid-template-columns: 1fr 2fr 1fr;
  gap: 20px;
  width: 400px;
  margin: 0 auto 0 auto;
}

.middleContainer
{
  width: 600px;
  margin: 0 auto 0 auto;
}      

.bottomContainer
{
  width: 500px;
  margin: 0 auto 0 auto;
}  

.scaleContainer
{
  display: grid;
  grid-template-rows: 8fr 1fr 1fr;
  gap: 0px;
}

.pnlScale
{
  font-size: 12em;
}

.pnlWeighPillsContainer
{
  display: flex;
  flex-direction: column;
  justify-content: center;
}

.pnlWeighPills
{
  display: grid;
  grid-template-rows: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
  gap: 1px;
}


Here you see the scale! The left and right divs have a mess of divs bunched right in the middle. We'll work on that next.

In the last div of the divs inside each div styled using pnlWeighPills, insert a div with the CSS class bowl.
<div class="topContainer">
  <div class="pnlWeighPillsContainer">
    <div class="pnlWeighPills">
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div><div class="bowl"></div></div>
    </div>
  </div>

  <div class="scaleContainer">
    <div class="pnlScale">&#x2696;</div>
    <div>Times Used: </div>
    <div><button>WEIGH</button></div>
  </div>

  <div class="pnlWeighPillsContainer">
    <div class="pnlWeighPills">
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div></div>
      <div><div class="bowl"></div></div>
    </div>
  </div>
</div>


This is bowl. It's a light grey div that has rounded corners at the bottom, with a fixed width and height.
.pnlWeighPills
{
  display: grid;
  grid-template-rows: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
  gap: 1px;
}  

.bowl
{
  width: 60px;
  height: 20px;
  background-color: rgb(200, 200, 200);
  border-top: none;
  border-radius: 0 0 10px 10px;
  margin: 0 auto 0 auto;
}


And now that one of the divs in the grid contains content that has fixed height and width, the rest of the divs follow suit!

We can leave the top portion alone for now. Let's work on the middle portion. We first have one div with the id pillsContainer. It in turn contains 8 div tags, one for each pill.
<div class="middleContainer">
  <div id="pillsContainer">
    <div></div>
    <div></div>
    <div></div>
    <div></div>
    <div></div>
    <div></div>
    <div></div>
    <div></div>
  </div>

</div>


After that, have two more divs on new lines - ids playerFace and playerMessage.
<div class="middleContainer">
  <div id="pillsContainer">
    <div></div>
    <div></div>
    <div></div>
    <div></div>
    <div></div>
    <div></div>
    <div></div>
    <div></div>
  </div>
  <br />
  <div id="playerFace"></div>
  <br />
  <div id="playerMessage"></div>

</div>


pillsContainer is another grid display, with 8 columns all with equal weight. For playerFace and playerMessage, I specified font size. For playerFace, the cursor property has been set to pointer because this is clickable.
.pnlScale
{
  font-size: 12em;
}

#pillsContainer
{
  display: grid;
  grid-template-columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
  gap: 5px;
  height: 100px;
}

#playerFace
{
  font-size: 7em;
  cursor: pointer;
}

#playerMessage
{
  font-size: 3em;
}


.pnlWeighPillsContainer
{
  display: flex;
  flex-direction: column;
  justify-content: center;
}


Speaking of clickable, let's also get this out of the way. All the div tags inside pillsContainer should be styled using the pillsSlot CSS class.
<div id="pillsContainer">
  <div class="pillSlot"></div>
  <div class="pillSlot"></div>
  <div class="pillSlot"></div>
  <div class="pillSlot"></div>
  <div class="pillSlot"></div>
  <div class="pillSlot"></div>
  <div class="pillSlot"></div>
  <div class="pillSlot"></div>
</div>


Same for these inside the divs styled using the pnlWeighPills CSS class. Just the first eight, not the last one.
<div class="pnlWeighPills">
  <div class="pillSlot"></div>
  <div class="pillSlot"></div>
  <div class="pillSlot"></div>
  <div class="pillSlot"></div>
  <div class="pillSlot"></div>
  <div class="pillSlot"></div>
  <div class="pillSlot"></div>
  <div class="pillSlot"></div>
  <div><div class="bowl"></div></div>
</div>


In the CSS, just set pillSlot to be clickable.
#pillsContainer div.selected
{
  color: rgb(255, 0, 0);
}

.pillSlot
{
  cursor: pointer;
}


#playerFace
{
  font-size: 7em;
  cursor: pointer;
}


In any case, your middle portion should be taking shape now.


Now for the bottom part! This part is the easiest. You add some instructions, and then add a button that the user clicks when they're ready to play.
<div class="bottomContainer">
  <p>You are slowly dying. There are eight pills. One of them is the cure. The rest are lethal poison and will kill you instantly. They are all identical in size and shape, and color. However, the cure is slightly lighter than the rest. You may use the scale exactly two times before it breaks.</p>

  <p>Once you have decided which pill to take, select the pill and click on the face at the bottom. Good luck!</p>

  <button>START</button>

</div>


And here we go, that's basically the UI. There's more to it, of course, but we have the skeleton upon which the rest can be filled in.


Next

Populating with pills.