Have you ever played wordfind in any of those cheap books that you buy in the train station or bus? The idea of the soup of letters, is to find a list of words in a table (so-called soup). This time, you'll learn how to create a soup of letter game with Javascript and wordfind.js easily (with all posible use cases as solution, multiple orientation, word overlap and others).
Requirements
In order to create our soup of letters, we are going to work with wordfind.js, wordfind is a simple javascript library for generating word find (also known as word search) puzzles. You can get a copy of the script in the official Github repository here, however the repository doesn't provide a minified version of it, so you can use the following minified version in your project:
// wordfind.js
/**
* Wordfind.js
* (c) 2012 Bill, BunKat LLC.
* Wordfind is freely distributable under the MIT license.
* @documentation http://github.com/bunkat/wordfind
*/
(function(){"use strict";var n=function(){var n="abcdefghijklmnoprstuvwy",r=["horizontal","horizontalBack","vertical","verticalUp","diagonal","diagonalUp","diagonalBack","diagonalUpBack"],t={horizontal:function(n,r,t){return{x:n+t,y:r}},horizontalBack:function(n,r,t){return{x:n-t,y:r}},vertical:function(n,r,t){return{x:n,y:r+t}},verticalUp:function(n,r,t){return{x:n,y:r-t}},diagonal:function(n,r,t){return{x:n+t,y:r+t}},diagonalBack:function(n,r,t){return{x:n-t,y:r+t}},diagonalUp:function(n,r,t){return{x:n+t,y:r-t}},diagonalUpBack:function(n,r,t){return{x:n-t,y:r-t}}},o={horizontal:function(n,r,t,o,e){return o>=n+e},horizontalBack:function(n,r,t,o,e){return n+1>=e},vertical:function(n,r,t,o,e){return t>=r+e},verticalUp:function(n,r,t,o,e){return r+1>=e},diagonal:function(n,r,t,o,e){return o>=n+e&&t>=r+e},diagonalBack:function(n,r,t,o,e){return n+1>=e&&t>=r+e},diagonalUp:function(n,r,t,o,e){return o>=n+e&&r+1>=e},diagonalUpBack:function(n,r,t,o,e){return n+1>=e&&r+1>=e}},e={horizontal:function(n,r,t){return{x:0,y:r+1}},horizontalBack:function(n,r,t){return{x:t-1,y:r}},vertical:function(n,r,t){return{x:0,y:r+100}},verticalUp:function(n,r,t){return{x:0,y:t-1}},diagonal:function(n,r,t){return{x:0,y:r+1}},diagonalBack:function(n,r,t){return{x:t-1,y:n>=t-1?r+1:r}},diagonalUp:function(n,r,t){return{x:0,y:t-1>r?t-1:r+1}},diagonalUpBack:function(n,r,t){return{x:t-1,y:n>=t-1?r+1:r}}},i=function(n,r){var t,o,e,i=[];for(t=0;t<r.height;t++)for(i.push([]),o=0;o<r.width;o++)i[t].push("");for(t=0,e=n.length;e>t;t++)if(!a(i,r,n[t]))return null;return i},a=function(n,r,o){var e=l(n,r,o);if(0===e.length)return!1;var i=e[Math.floor(Math.random()*e.length)];return c(n,o,i.x,i.y,t[i.orientation]),!0},l=function(n,r,i){for(var a=[],l=r.height,c=r.width,h=i.length,g=0,v=0,p=r.orientations.length;p>v;v++)for(var d=r.orientations[v],s=o[d],x=t[d],y=e[d],k=0,B=0;l>B;)if(s(k,B,l,c,h)){var w=u(i,n,k,B,x);(w>=g||!r.preferOverlap&&w>-1)&&(g=w,a.push({x:k,y:B,orientation:d,overlap:w})),k++,k>=c&&(k=0,B++)}else{var U=y(k,B,h);k=U.x,B=U.y}return r.preferOverlap?f(a,g):a},u=function(n,r,t,o,e){for(var i=0,a=0,l=n.length;l>a;a++){var u=e(t,o,a),f=r[u.y][u.x];if(f===n[a])i++;else if(""!==f)return-1}return i},f=function(n,r){for(var t=[],o=0,e=n.length;e>o;o++)n[o].overlap>=r&&t.push(n[o]);return t},c=function(n,r,t,o,e){for(var i=0,a=r.length;a>i;i++){var l=e(t,o,i);n[l.y][l.x]=r[i]}};return{validOrientations:r,orientations:t,newPuzzle:function(n,t){var o,e,a=0,l=t||{};o=n.slice(0).sort(function(n,r){return n.length<r.length?1:0});for(var u={height:l.height||o[0].length,width:l.width||o[0].length,orientations:l.orientations||r,fillBlanks:void 0!==l.fillBlanks?l.fillBlanks:!0,maxAttempts:l.maxAttempts||3,preferOverlap:void 0!==l.preferOverlap?l.preferOverlap:!0};!e;){for(;!e&&a++<u.maxAttempts;)e=i(o,u);e||(u.height++,u.width++,a=0)}return u.fillBlanks&&this.fillBlanks(e,u),e},fillBlanks:function(r){for(var t=0,o=r.length;o>t;t++)for(var e=r[t],i=0,a=e.length;a>i;i++)if(!r[t][i]){var l=Math.floor(Math.random()*n.length);r[t][i]=n[l]}},solve:function(n,t){for(var o={height:n.length,width:n[0].length,orientations:r,preferOverlap:!0},e=[],i=[],a=0,u=t.length;u>a;a++){var f=t[a],c=l(n,o,f);c.length>0&&c[0].overlap===f.length?(c[0].word=f,e.push(c[0])):i.push(f)}return{found:e,notFound:i}},print:function(n){for(var r="",t=0,o=n.length;o>t;t++){for(var e=n[t],i=0,a=e.length;a>i;i++)r+=(""===e[i]?" ":e[i])+" ";r+="\n"}return console.log(r),r}}},r="undefined"!=typeof exports&&null!==exports?exports:window;r.wordfind=n()}).call(this);
Add the reference to your html document using a script tag and you're ready to go:
<script src="wordfind.js"></script>
Implementation with GUI
If you want a full implementation (and already built UI, and event handlers as user selection etc) you'll need to have jQuery as a dependency in your project. You can add a reference of jQuery to your project from the CDN:
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
Besides, you need to add another script that will be at charge of the creation of the GUI for the user (wordfindgame.js
):
/**
* wordfindgame.js
* Script to create the GUI
* Wordfindgame.js
* (c) 2012 Bill, BunKat LLC.
* Wordfind is freely distributable under the MIT license.
* For all details and documentation: http://github.com/bunkat/wordfind
*/
!function(e,t,n){"use strict";var r=function(){var r,o,a,l=function(e,n){for(var r="",o=0,a=n.length;a>o;o++){var l=n[o];r+="<div>";for(var u=0,s=l.length;s>u;u++)r+='<button class="puzzleSquare" x="'+u+'" y="'+o+'">',r+=l[u]||" ",r+="</button>";r+="</div>"}t(e).html(r)},u=function(e,n){for(var r="<ul>",o=0,a=n.length;a>o;o++){var l=n[o];r+='<li class="word '+l+'">'+l}r+="</ul>",t(e).html(r)},s=[],i="",d=function(){t(this).addClass("selected"),o=this,s.push(this),i=t(this).text()},c=function(e){if(o){var n=s[s.length-1];if(n!=e){for(var r,l=0,u=s.length;u>l;l++)if(s[l]==e){r=l+1;break}for(;r<s.length;)t(s[s.length-1]).removeClass("selected"),s.splice(r,1),i=i.substr(0,i.length-1);var d=p(t(o).attr("x")-0,t(o).attr("y")-0,t(e).attr("x")-0,t(e).attr("y")-0);d&&(s=[o],i=t(o).text(),n!==o&&(t(n).removeClass("selected"),n=o),a=d);var c=p(t(n).attr("x")-0,t(n).attr("y")-0,t(e).attr("x")-0,t(e).attr("y")-0);c&&(a&&a!==c||(a=c,h(e)))}}},f=function(t){var n=t.originalEvent.touches[0].pageX,r=t.originalEvent.touches[0].pageY,o=e.elementFromPoint(n,r);c(o)},v=function(){c(this)},h=function(e){for(var n=0,o=r.length;o>n;n++)if(0===r[n].indexOf(i+t(e).text())){t(e).addClass("selected"),s.push(e),i+=t(e).text();break}},z=function(){for(var e=0,n=r.length;n>e;e++)r[e]===i&&(t(".selected").addClass("found"),r.splice(e,1),t("."+i).addClass("wordFound")),0===r.length&&t(".puzzleSquare").addClass("complete");t(".selected").removeClass("selected"),o=null,s=[],i="",a=null},p=function(e,t,r,o){for(var a in n.orientations){var l=n.orientations[a],u=l(e,t,1);if(u.x===r&&u.y===o)return a}return null};return{create:function(e,o,a,s){r=e.slice(0).sort();var i=n.newPuzzle(e,s);return l(o,i),u(a,r),window.navigator.msPointerEnabled?(t(".puzzleSquare").on("MSPointerDown",d),t(".puzzleSquare").on("MSPointerOver",c),t(".puzzleSquare").on("MSPointerUp",z)):(t(".puzzleSquare").mousedown(d),t(".puzzleSquare").mouseenter(v),t(".puzzleSquare").mouseup(z),t(".puzzleSquare").on("touchstart",d),t(".puzzleSquare").on("touchmove",f),t(".puzzleSquare").on("touchend",z)),i},solve:function(e,r){for(var o=n.solve(e,r).found,a=0,l=o.length;l>a;a++){var u=o[a].word,s=o[a].orientation,i=o[a].x,d=o[a].y,c=n.orientations[s];if(!t("."+u).hasClass("wordFound")){for(var f=0,v=u.length;v>f;f++){var h=c(i,d,f);t('[x="'+h.x+'"][y="'+h.y+'"]').addClass("solved")}t("."+u).addClass("wordFound")}}}}};window.wordfindgame=r()}(document,jQuery,wordfind);
And add the reference in your document:
<script src="wordfindgame.js"></script>
The following example shows a full implementation (but without custom options at the initialization) using both scripts (wordfind.js
and wordfindgame.js
)
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Wordfind game Javascript</title>
</head>
<body>
<!-- Add required markup -->
<div id="puzzle-container"></div>
<div id="puzzle-words"></div>
<input type="button" id="solveBTN" value="Solve puzzle"/>
<!-- Add dependencies -->
<script src="https://code.jquery.com/jquery-2.2.4.min.js" integrity="sha256-BbhdlvQf/xTY9gja0Dq3HiwQF8LaCRTXxZKRutelT44=" crossorigin="anonymous"></script>
<script src="wordfind.js"></script>
<script src="wordfindgame.js"></script>
<!-- Add our logic -->
<script>
// An array with the words to show
var words = ['cows', 'tracks', 'arrived', 'located', 'sir', 'seat',
'division', 'effect', 'underline', 'view', 'annual',
'anniversary', 'centennial', 'millennium', 'perennial',
'artisan', 'apprentice', 'meteorologist', 'blizzard', 'tornado',
'intensify','speed','count','consonant','someone',
'sail','rolled','bear','wonder','smiled','angle', 'absent',
'decadent', 'excellent', 'frequent', 'impatient', 'cell',
'cytoplasm', 'organelle', 'diffusion', 'osmosis',
'respiration'
];
// Start a basic word game without customization !
var gamePuzzle = wordfindgame.create(words, '#puzzle-container', '#puzzle-words');
$("#solveBTN").click(function(){
// Solve the puzzle !
var result = wordfindgame.solve(gamePuzzle, words);
console.log(result);
});
</script>
</body>
</html>
And the result should be a soup of letter similar to:
Note: is up to you to apply styles to the container div of the puzzle. If you do not feel inspired today, you can use the provided styles in the repository (not well prefixed to prevent incompatibility with present libraries and styles) or you can use the following rules that will only apply to the container of the puzzle.
In this case, the style rules are based on the mentioned selectors in our code (#puzzle-container
and #puzzle-words
):
#puzzle-container {
border: 1px solid black;
padding: 20px;
float: left;
margin: 30px 20px;
}
#puzzle-container div {
width: 100%;
margin: 0 auto;
}
/* style for each square in the puzzle */
#puzzle-container .puzzleSquare {
height: 30px;
width: 30px;
text-transform: uppercase;
background-color: white;
border: 0;
font: 1em sans-serif;
}
#puzzle-container button::-moz-focus-inner {
border: 0;
}
/* indicates when a square has been selected */
#puzzle-container .selected {
background-color: orange;
}
/* indicates that the square is part of a word that has been found */
#puzzle-container .found {
background-color: blue;
color: white;
}
#puzzle-container .solved {
background-color: purple;
color: white;
}
/* indicates that all words have been found */
#puzzle-container .complete {
background-color: green;
}
/**
* Styles for the word list
*/
#puzzle-words {
padding-top: 20px;
-moz-column-count: 2;
-moz-column-gap: 20px;
-webkit-column-count: 2;
-webkit-column-gap: 20px;
column-count: 2;
column-gap: 20px;
width: 300px;
}
#puzzle-words ul {
list-style-type: none;
}
#puzzle-words li {
padding: 3px 0;
font: 1em sans-serif;
}
/* indicates that the word has been found */
#puzzle-words .wordFound {
text-decoration: line-through;
color: gray;
}
/**
* Styles for the button
*/
#solve {
margin: 0 30px;
}
And you're ready to go!
Implementation without GUI
In case you're looking only for the logic that generates the wordfind game, then today's your lucky day! Wordfind provides an easy API to retrieve the game structure within an array.
var words = ['cow'];
var puzzle = wordfind.newPuzzle(words, {
// Set dimensions of the puzzle
height: 3,
width: 3,
// or enable all with => orientations: wordfind.validOrientations,
orientations: ['horizontal', 'vertical'],
// Set a random character the empty spaces
fillBlanks: true,
preferOverlap: false
});
console.log(puzzle);
// Output array:
//[[A, X, C],
// [P, E, O],
// [J, I, W]]
The puzzle variable will be an array, on it every item is equivalent to a row on a table:
A | X | C |
P | E | O |
J | I | W |
This feature comes in handy if you want to implement by yourself the user interface and you need only the logic of generation of the soup of letters. There are a lot of useful options available in the library and you should read.
If you want, you can see an official working implementation here. Have fun !