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": "☹",
"color": "#004400",
"message": "Slowly dying..."
},
"well":
{
"face": "☺",
"color": "#FFFF00",
"message": "Cured! Congratulations!"
},
"dead":
{
"face": "☠",
"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.