In the previous part, we did quite a fair bit. For today's part, it's a smaller but just as vital piece of the puzzle. We will be hiding the words within the grid. There's quite a bit of logic that goes into this, so bear with me!
We'll start off with something simple. Just before displaying the theme, call the
assignWords() method.
begin: function ()
{
this.themeIndex = this.generateRandomNo(themes.length);
this.assignWords();
this.displayTheme();
this.seconds = 300;
this.decrementCounter();
$(".dashboard .message").html("Search for words both horizontal and vertical. Each word is five letters and longer.");
$("#btnGo").html("↺");
},
Create the method. Grab
themeIndex, and set
words to an empty array. Declare variable
possibleWordList and set it to the
words array of the element in
themes pointed to by
themeIndex. There's only one possible value right now, so it should always return the list of sea creature names.
begin: function ()
{
this.themeIndex = this.generateRandomNo(themes.length);
this.assignWords();
this.displayTheme();
this.seconds = 300;
this.decrementCounter();
$(".dashboard .message").html("Search for words both horizontal and vertical. Each word is five letters and longer.");
$("#btnGo").html("↺");
},
assignWords: function()
{
themeIndex = this.themeIndex;
this.words = [];
var possibleWordList = themes[themeIndex].words;
},
decrementCounter: function ()
{
And while
words has less elements than
maxWords...
assignWords: function()
{
themeIndex = this.themeIndex;
this.words = [];
var possibleWordList = themes[themeIndex].words;
while (this.words.length < this.maxWords)
{
}
},
Grab a random element from
possibleWordList.
assignWords: function()
{
themeIndex = this.themeIndex;
this.words = [];
var possibleWordList = themes[themeIndex].words;
while (this.words.length < this.maxWords)
{
var assignedWord = possibleWordList[this.generateRandomNo(possibleWordList.length)];
}
},
Push that word into
words, encapsulating it into an object, along with a property,
assigned, which is
false. Then remove that word from
possibleWordList using the
filter() method.
What we've basically done is - create a list of all possible words, grab words randomly from the list to populate the
words array with, deleting that word from the list of possible words as we do. So the list gets shorter each time and we don't get repeats.
assignWords: function()
{
themeIndex = this.themeIndex;
this.words = [];
var possibleWordList = themes[themeIndex].words;
while (this.words.length < this.maxWords)
{
var assignedWord = possibleWordList[this.generateRandomNo(possibleWordList.length)];
this.words.push({word: assignedWord, assigned: false});
possibleWordList = possibleWordList.filter((x) => { return x != assignedWord; });
}
},
Now call the
populateGrid() method.
begin: function ()
{
this.themeIndex = this.generateRandomNo(themes.length);
this.assignWords();
this.displayTheme();
this.populateGrid();
this.seconds = 300;
this.decrementCounter();
$(".dashboard .message").html("Search for words both horizontal and vertical. Each word is five letters and longer.");
$("#btnGo").html("↺");
},
populateGrid() is a method that takes words from the
words array (which we just populated) and places them randomly in the grid. Right now the div styled by
wordgrid is totally empty. We should make sure of that by wiping it clean like this.
decrementCounter: function ()
{
if (this.timer == undefined)
{
this.timer = setInterval
(
() =>
{
if (this.seconds > 0)
{
this.seconds = this.seconds - 1;
}
else
{
clearInterval(this.timer);
this.timer = undefined;
$(".dashboard .message").html("Time's up! Better luck next time!");
$("#btnGo").html("▶");
}
$(".dashboard .timer .seconds").html(this.seconds);
}
,1000
)
}
},
populateGrid: function ()
{
$(".wordgrid").html("");
},
generateRandomNo: function (max)
{
return Math.floor((Math.random() * (max)));
},
And then set
squares to an empty array.
populateGrid: function ()
{
$(".wordgrid").html("");
this.squares = [];
},
We want to make a grid of squares, specifically,
maxSize squares by
maxSize squares. So what we do is iterate from 0 to
maxSize, and make
maxSize empty elements in squares.
populateGrid: function ()
{
$(".wordgrid").html("");
this.squares = [];
for (var x = 0; x < this.maxSize; x++)
{
this.squares[x] = [];
}
},
Then we again iterate from 0 to
maxSize, and, using the
push() method, set each array in
squares to be an array of
objects. Each object will have these properties as shown. I'll explain what they're for later on.
populateGrid: function ()
{
$(".wordgrid").html("");
this.squares = [];
for (var x = 0; x < this.maxSize; x++)
{
this.squares[x] = [];
for (var y = 0; y < this.maxSize; y++)
{
this.squares[x].push
(
{
letter: "",
selected: false,
correct: false,
dir: undefined,
intersected: false
}
);
}
}
},
And for each array object, we create a div, and append some useful data in it. We also style it using the CSS class
letter.
populateGrid: function ()
{
$(".wordgrid").html("");
this.squares = [];
for (var x = 0; x < this.maxSize; x++)
{
this.squares[x] = [];
for (var y = 0; y < this.maxSize; y++)
{
this.squares[x].push
(
{
letter: "",
selected: false,
correct: false,
dir: undefined,
intersected: false
}
);
var square = $("<div><div>");
square.attr("data-x", x);
square.attr("data-y", y);
square.addClass("letter");
}
}
},
And then we set a click event, which, for now, will do absolutely nothing.
populateGrid: function ()
{
$(".wordgrid").html("");
this.squares = [];
for (var x = 0; x < this.maxSize; x++)
{
this.squares[x] = [];
for (var y = 0; y < this.maxSize; y++)
{
this.squares[x].push
(
{
letter: "",
selected: false,
correct: false,
dir: undefined,
intersected: false
}
);
var square = $("<div><div>");
square.attr("data-x", x);
square.attr("data-y", y);
square.addClass("letter");
square.click
(
(e) =>
{
}
);
}
}
},
After all that, we then append the element to the div.
populateGrid: function ()
{
$(".wordgrid").html("");
this.squares = [];
for (var x = 0; x < this.maxSize; x++)
{
this.squares[x] = [];
for (var y = 0; y < this.maxSize; y++)
{
this.squares[x].push
(
{
letter: "",
selected: false,
correct: false,
dir: undefined,
intersected: false
}
);
var square = $("<div><div>");
square.attr("data-x", x);
square.attr("data-y", y);
square.addClass("letter");
square.click
(
(e) =>
{
}
);
$(".wordgrid").append(square);
}
}
},
This is the
letter CSS class. Width and height have been set to 4%, with some margins. We will float this left. These are the only things necessary for the layout to work; the rest of the properties I'll leave up to your personal taste.
.score .meter .fill
{
width: 100%;
height: 100%;
color: rgba(255, 255, 255, 1);
text-align: right;
font-weight: bold;
}
.letter
{
width: 4%;
height: 4%;
margin: 1% 1% 0 0;
float: left;
color: rgba(255, 200, 0, 1);
cursor: pointer;
text-align: center;
font-size: 1.2em;
font-weight: bold;
text-transform: uppercase;
border-radius: 50%;
}
We have a grid!
Now let's start placing the letters. Declare
unassigned, an empty array, and
i.
populateGrid: function ()
{
$(".wordgrid").html("");
this.squares = [];
for (var x = 0; x < this.maxSize; x++)
{
this.squares[x] = [];
for (var y = 0; y < this.maxSize; y++)
{
this.squares[x].push
(
{
letter: "",
selected: false,
correct: false,
dir: undefined,
intersected: false
}
);
var square = $("<div><div>");
square.attr("data-x", x);
square.attr("data-y", y);
square.addClass("letter");
square.click
(
(e) =>
{
}
);
$(".wordgrid").append(square);
}
}
var unassigned = [];
var i;
},
The next operation will be carried out, and repeated as long as there are elements in
unassigned.
var unassigned = [];
var i;
do
{
}
while (unassigned.length > 0);
The method
populateGridWithWords() will be called, and after that, we set
unassigned to the filtered
words array, getting all the elements where
assigned is
false. Remember the
assigned property in
words we defaulted to
false earlier?
This ensures that all the words in
words are assigned before the script can proceed.
var unassigned = [];
var i;
do
{
this.populateGridWithWords();
unassigned = this.words.filter((x) => {return !x.assigned;});
}
while (unassigned.length > 0);
And of course, we'll want to create
populateGridWithWords(). We start off the method by running a nested
For loop to "clean" the data. Basically reset both the squares array and the grid to an initial state. This is important because if this method is being run, not all words have been assigned and we want to start the operation over.
populateGrid: function ()
{
$(".wordgrid").html("");
this.squares = [];
for (var x = 0; x < this.maxSize; x++)
{
this.squares[x] = [];
for (var y = 0; y < this.maxSize; y++)
{
this.squares[x].push
(
{
letter: "",
selected: false,
correct: false,
dir: undefined,
intersected: false
}
);
var square = $("<div><div>");
square.attr("data-x", x);
square.attr("data-y", y);
square.addClass("letter");
square.click
(
(e) =>
{
}
);
$(".wordgrid").append(square);
}
}
var unassigned = [];
var i;
do
{
this.populateGridWithWords();
unassigned = this.words.filter((x) => {return !x.assigned;});
}
while (unassigned.length > 0);
},
populateGridWithWords: function()
{
for (var x = 0; x < this.maxSize; x++)
{
for (var y = 0; y < this.maxSize; y++)
{
this.squares[x][y].letter = "";
this.squares[x][y].correct = false;
this.squares[x][y].dir = undefined;
this.squares[x][y].intersected = false;
$(".letter[data-x=" + x + "][data-y=" + y + "]").html("");
}
}
},
generateRandomNo: function (max)
{
return Math.floor((Math.random() * (max)));
},
Now iterate through the
words array and set all assigned properties to
false. Declare variable
word, and set it to the current element's
word property. Also declare
possibleSlots, an empty array.
populateGridWithWords: function()
{
for (var x = 0; x < this.maxSize; x++)
{
for (var y = 0; y < this.maxSize; y++)
{
this.squares[x][y].letter = "";
this.squares[x][y].correct = false;
this.squares[x][y].dir = undefined;
this.squares[x][y].intersected = false;
$(".letter[data-x=" + x + "][data-y=" + y + "]").html("");
}
}
for (var i = 0; i < this.words.length; i++)
{
this.words[i].assigned = false;
var word = this.words[i].word;
var possibleSlots = [];
}
},
Use a nested
For loop to go through the entire grid.
for (var i = 0; i < this.words.length; i++)
{
this.words[i].assigned = false;
var word = this.words[i].word;
var possibleSlots = [];
for (var x = 0; x < this.maxSize; x++)
{
for (var y = 0; y < this.maxSize; y++)
{
}
}
}
Here, we use the
isWordSpaceAvailable() method, which we will create after this. The
x and
y coordinates (supplied by the nested
For loop), the direction variable
dir (0 for horizontal and 1 for vertical) and
word are passed in as arguments. If the script finds either a horizontal or vertical space, it pushes an object into possible slots. If there are no elements in
possibleSlots after this, the entire operation is a failure and we use the
return statement to abort it, and retry.
for (var i = 0; i < this.words.length; i++)
{
this.words[i].assigned = false;
var word = this.words[i].word;
var possibleSlots = [];
for (var x = 0; x < this.maxSize; x++)
{
for (var y = 0; y < this.maxSize; y++)
{
if (this.isWordSpaceAvailable(x, y, 0, word))
{
possibleSlots.push({x: x, y: y, dir: 0});
}
if (this.isWordSpaceAvailable(x, y, 1, word))
{
possibleSlots.push({x: x, y: y, dir: 1});
}
if (possibleSlots.length == 0) return;
}
}
}
Next, we declare variables
slot and
slotIndex.
slotIndex is initialized as 0. Then
possibleSlots_intersected is declared. It's an array of all elements in squares that aren't empty strings, as pointed to by the
x and
y properties in
possibleSlots.
possibleSlots here means places in the grid which the word can begin in - the
x and
y coordinates, and direction.
for (var i = 0; i < this.words.length; i++)
{
this.words[i].assigned = false;
var word = this.words[i].word;
var possibleSlots = [];
for (var x = 0; x < this.maxSize; x++)
{
for (var y = 0; y < this.maxSize; y++)
{
if (this.isWordSpaceAvailable(x, y, 0, word))
{
possibleSlots.push({x: x, y: y, dir: 0});
}
if (this.isWordSpaceAvailable(x, y, 1, word))
{
possibleSlots.push({x: x, y: y, dir: 1});
}
if (possibleSlots.length == 0) return;
}
}
var slotIndex = 0;
var slot;
var possibleSlots_intersected = possibleSlots.filter((s) => {return this.squares[s.x][s.y].letter != "";});
}
This is because we want the words to possibly cross each other, like so.
possibleSlots_intersected will provide a list of possible slots that are open to two words sharing a letter space.
So here, a conditional checks if
possibleSlots_intersected is empty. If it is not, then that means there are possible places for intersection. We'll use
possibleSlots_intersected and the
generateRandomNo() method to derive
slot.
for (var i = 0; i < this.words.length; i++)
{
this.words[i].assigned = false;
var word = this.words[i].word;
var possibleSlots = [];
for (var x = 0; x < this.maxSize; x++)
{
for (var y = 0; y < this.maxSize; y++)
{
if (this.isWordSpaceAvailable(x, y, 0, word))
{
possibleSlots.push({x: x, y: y, dir: 0});
}
if (this.isWordSpaceAvailable(x, y, 1, word))
{
possibleSlots.push({x: x, y: y, dir: 1});
}
if (possibleSlots.length == 0) return;
}
}
var slotIndex = 0;
var slot;
var possibleSlots_intersected = possibleSlots.filter((s) => {return this.squares[s.x][s.y].letter != "";});
if (possibleSlots_intersected.length == 0)
{
}
else
{
slotIndex = this.generateRandomNo(possibleSlots_intersected.length);
slot = possibleSlots_intersected[slotIndex];
}
}
If there are no possible starting slots for intersection, we use
possibleSlots instead to derive
slot. And then we set that square's
intersected property to
true. Why, you might ask? It's a wee bit tricky, but trust me, it's necessary. I'll explain soon enough!
for (var i = 0; i < this.words.length; i++)
{
this.words[i].assigned = false;
var word = this.words[i].word;
var possibleSlots = [];
for (var x = 0; x < this.maxSize; x++)
{
for (var y = 0; y < this.maxSize; y++)
{
if (this.isWordSpaceAvailable(x, y, 0, word))
{
possibleSlots.push({x: x, y: y, dir: 0});
}
if (this.isWordSpaceAvailable(x, y, 1, word))
{
possibleSlots.push({x: x, y: y, dir: 1});
}
if (possibleSlots.length == 0) return;
}
}
var slotIndex = 0;
var slot;
var possibleSlots_intersected = possibleSlots.filter((s) => {return this.squares[s.x][s.y].letter != "";});
if (possibleSlots_intersected.length == 0)
{
slotIndex = this.generateRandomNo(possibleSlots.length);
slot = possibleSlots[slotIndex];
this.squares[slot.x][slot.y].intersected = true;
}
else
{
slotIndex = this.generateRandomNo(possibleSlots_intersected.length);
slot = possibleSlots_intersected[slotIndex];
}
}
Now, use a
For loop to iterate through every letter in the word. Declare
letters as an array of all the elements in the word, using the
split() method. Set a conditional for the
dir property, and then set the
assigned property to
true.
for (var i = 0; i < this.words.length; i++)
{
this.words[i].assigned = false;
var word = this.words[i].word;
var possibleSlots = [];
for (var x = 0; x < this.maxSize; x++)
{
for (var y = 0; y < this.maxSize; y++)
{
if (this.isWordSpaceAvailable(x, y, 0, word))
{
possibleSlots.push({x: x, y: y, dir: 0});
}
if (this.isWordSpaceAvailable(x, y, 1, word))
{
possibleSlots.push({x: x, y: y, dir: 1});
}
if (possibleSlots.length == 0) return;
}
}
var slotIndex = 0;
var slot;
var possibleSlots_intersected = possibleSlots.filter((s) => {return this.squares[s.x][s.y].letter != "";});
if (possibleSlots_intersected.length == 0)
{
slotIndex = this.generateRandomNo(possibleSlots.length);
slot = possibleSlots[slotIndex];
this.squares[slot.x][slot.y].intersected = true;
}
else
{
slotIndex = this.generateRandomNo(possibleSlots_intersected.length);
slot = possibleSlots_intersected[slotIndex];
}
for (var j = 0; j < word.length; j++)
{
var letters = word.split("");
if (slot.dir == 0)
{
}
if (slot.dir == 1)
{
}
this.words[i].assigned = true;
}
}
What we're doing here is populating the
squares array with the letters from the words.
dir is important here; again, I'll explain why later. At the end of it, we'll also populate the grid, using jQuery, with the letter and the
x and
y coordinates.
for (var i = 0; i < this.words.length; i++)
{
this.words[i].assigned = false;
var word = this.words[i].word;
var possibleSlots = [];
for (var x = 0; x < this.maxSize; x++)
{
for (var y = 0; y < this.maxSize; y++)
{
if (this.isWordSpaceAvailable(x, y, 0, word))
{
possibleSlots.push({x: x, y: y, dir: 0});
}
if (this.isWordSpaceAvailable(x, y, 1, word))
{
possibleSlots.push({x: x, y: y, dir: 1});
}
if (possibleSlots.length == 0) return;
}
}
var slotIndex = 0;
var slot;
var possibleSlots_intersected = possibleSlots.filter((s) => {return this.squares[s.x][s.y].letter != "";});
if (possibleSlots_intersected.length == 0)
{
slotIndex = this.generateRandomNo(possibleSlots.length);
slot = possibleSlots[slotIndex];
this.squares[slot.x][slot.y].intersected = true;
}
else
{
slotIndex = this.generateRandomNo(possibleSlots_intersected.length);
slot = possibleSlots_intersected[slotIndex];
}
for (var j = 0; j < word.length; j++)
{
var letters = word.split("");
if (slot.dir == 0)
{
this.squares[slot.x + j][slot.y].letter = letters[j];
this.squares[slot.x + j][slot.y].correct = true;
this.squares[slot.x + j][slot.y].dir = slot.dir;
$(".letter[data-x=" + (slot.x + j) + "][data-y=" + (slot.y) + "]").html(letters[j]);
}
if (slot.dir == 1)
{
this.squares[slot.x][slot.y + j].letter = letters[j];
this.squares[slot.x][slot.y + j].correct = true;
this.squares[slot.x][slot.y + j].dir = slot.dir;
$(".letter[data-x=" + (slot.x) + "][data-y=" + (slot.y + j) + "]").html(letters[j]);
}
this.words[i].assigned = true;
}
}
And finally, we create the
isWordSpaceAvailable() method to help determine if a given square is a suitable place to begin a word. Obviously, there are certain conditions to look out for. If
dir is horizontal and the word is so long that starting the word near the end of the grid would cause the word to overrun the set boundaries, then obviously we don't want that. Ditto for vertical.
We begin by declaring letters as an array of all the letters in the parameter
word.
populateGridWithWords: function()
{
for (var x = 0; x < this.maxSize; x++)
{
for (var y = 0; y < this.maxSize; y++)
{
this.squares[x][y].letter = "";
this.squares[x][y].correct = false;
this.squares[x][y].dir = undefined;
this.squares[x][y].intersected = false;
$(".letter[data-x=" + x + "][data-y=" + y + "]").html("");
}
}
for (var i = 0; i < this.words.length; i++)
{
this.words[i].assigned = false;
var word = this.words[i].word;
var possibleSlots = [];
for (var x = 0; x < this.maxSize; x++)
{
for (var y = 0; y < this.maxSize; y++)
{
if (this.isWordSpaceAvailable(x, y, 0, word))
{
possibleSlots.push({x: x, y: y, dir: 0});
}
if (this.isWordSpaceAvailable(x, y, 1, word))
{
possibleSlots.push({x: x, y: y, dir: 1});
}
if (possibleSlots.length == 0) return;
}
}
var slotIndex = 0;
var slot;
var possibleSlots_intersected = possibleSlots.filter((s) => {return this.squares[s.x][s.y].letter != "";});
if (possibleSlots_intersected.length == 0)
{
slotIndex = this.generateRandomNo(possibleSlots.length);
slot = possibleSlots[slotIndex];
this.squares[slot.x][slot.y].intersected = true;
}
else
{
slotIndex = this.generateRandomNo(possibleSlots_intersected.length);
slot = possibleSlots_intersected[slotIndex];
}
for (var j = 0; j < word.length; j++)
{
var letters = word.split("");
if (slot.dir == 0)
{
this.squares[slot.x + j][slot.y].letter = letters[j];
this.squares[slot.x + j][slot.y].correct = true;
this.squares[slot.x + j][slot.y].dir = slot.dir;
$(".letter[data-x=" + (slot.x + j) + "][data-y=" + (slot.y) + "]").html(letters[j]);
}
if (slot.dir == 1)
{
this.squares[slot.x][slot.y + j].letter = letters[j];
this.squares[slot.x][slot.y + j].correct = true;
this.squares[slot.x][slot.y + j].dir = slot.dir;
$(".letter[data-x=" + (slot.x) + "][data-y=" + (slot.y + j) + "]").html(letters[j]);
}
this.words[i].assigned = true;
}
}
},
isWordSpaceAvailable: function (x, y, dir, word)
{
var letters = word.split("");
},
generateRandomNo: function (max)
{
return Math.floor((Math.random() * (max)));
},
We prepare conditional blocks for
dir. In both cases, we return
true at the end of the block.
isWordSpaceAvailable: function (x, y, dir, word)
{
var letters = word.split("");
if (dir == 0)
{
return true;
}
if (dir == 1)
{
return true;
}
},
For a horizontal case, if the length of the word plus the
x coordinate would overrun
maxSize, we return false.
isWordSpaceAvailable: function (x, y, dir, word)
{
var letters = word.split("");
if (dir == 0)
{
if ((x + letters.length) >= this.maxSize) return false;
return true;
}
if (dir == 1)
{
return true;
}
},
Then we go through all the letters. If the direction has already been set and is the same as the current direction, we return
false because we don't want the words to overlap by more than one character - unlikely, but entirely possible depending on the word selection.
isWordSpaceAvailable: function (x, y, dir, word)
{
var letters = word.split("");
if (dir == 0)
{
if ((x + letters.length) >= this.maxSize) return false;
for (var i = 0; i < letters.length; i++)
{
if (this.squares[x + i][y].dir == dir) return false;
}
return true;
}
if (dir == 1)
{
return true;
}
},
Like in this case...
If the
intersected property is
true, we also return
false. This is so that the word does not intersect an already intersected letter.
isWordSpaceAvailable: function (x, y, dir, word)
{
var letters = word.split("");
if (dir == 0)
{
if ((x + letters.length) >= this.maxSize) return false;
for (var i = 0; i < letters.length; i++)
{
if (this.squares[x + i][y].dir == dir) return false;
if (this.squares[x + i][y].intersected) return false;
}
return true;
}
if (dir == 1)
{
return true;
}
},
Basically, I'm trying to prevent a case like this...
And if the space is already occupied by a letter and that letter is not the same as the current letter, we also return
false.
isWordSpaceAvailable: function (x, y, dir, word)
{
var letters = word.split("");
if (dir == 0)
{
if ((x + letters.length) >= this.maxSize) return false;
for (var i = 0; i < letters.length; i++)
{
if (this.squares[x + i][y].dir == dir) return false;
if (this.squares[x + i][y].intersected) return false;
if (this.squares[x + i][y].letter != "" && this.squares[x + i][y].letter != letters[i]) return false;
}
return true;
}
if (dir == 1)
{
return true;
}
},
Now repeat this for a vertical direction.
isWordSpaceAvailable: function (x, y, dir, word)
{
var letters = word.split("");
if (dir == 0)
{
if ((x + letters.length) >= this.maxSize) return false;
for (var i = 0; i < letters.length; i++)
{
if (this.squares[x + i][y].dir == dir) return false;
if (this.squares[x + i][y].intersected) return false;
if (this.squares[x + i][y].letter != "" && this.squares[x + i][y].letter != letters[i]) return false;
}
return true;
}
if (dir == 1)
{
if ((y + letters.length) >= this.maxSize) return false;
for (var i = 0; i < letters.length; i++)
{
if (this.squares[x][y + i].dir == dir) return false;
if (this.squares[x][y + i].intersected) return false;
if (this.squares[x][y + i].letter != "" && this.squares[x][y + i].letter != letters[i]) return false;
}
return true;
}
},
There you go.
Phew! Now that was a lot to unpack. But we're far from done, folks...
Next
Handling clicks, correct and incorrect input.