Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions Readme.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
_Wordfind_ is a simple `javascript` library for generating (hopefully fun) word find (also known as word search) puzzles. Just give it a set of words and a few milliseconds later it will spit out a puzzle containing those words.
_Wordfind_ is a simple `javascript` library for generating (hopefully fun) word find (also known as word search) puzzles.
Just give it a set of words and a few milliseconds later it will spit out a puzzle containing those words.

The core `wordfind.js` library contains no dependencies and will work both in the browser and in node.js. The repository also includes a fully functional word find game (aptly called `wordfindgame.js`) as an example. The game has a dependency on `jQuery`.
The core `wordfind.js` library contains no dependencies and will work both in the browser and in node.js.
The repository also includes a fully functional word find game (aptly called `wordfindgame.js`) as an example.
The game has a dependency on `jQuery`.

This is a fork of https://github.com/bunkat/wordfind allowing to specify the filling letters you want.
This is a fork of https://github.com/bunkat/wordfind allowing to specify the filling letters you want,
letting some words be ommited and making the generator to retry with a bigger grid if none was found initially.

Check out the sample game at http://Lucas-C.github.com/wordfind/.
Check out the sample game at http://Lucas-C.github.io/wordfind/.
49 changes: 29 additions & 20 deletions index.html
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,27 @@
<h1>WordFind.js by BunKat &amp; Lucas-C</h1>
<div id="main" role="main">
<div id="puzzle"></div>
<ul id="words">
<li><button id="add-word">Add word</button></li>
</ul>
<div id="words-editor">
<ul id="words"></ul>
<button id="add-word">Add word</button>
<button id="remove-last-word">Remove last word</button>
</div>
<fieldset id="controls">
<label for="allowed-missing-words">Allowed missing words :
<input id="allowed-missing-words" type="number" min="0" max="5" step="1" value="2">
<input id="allowed-missing-words" type="number" min="0" max="10" step="1" value="2">
</label>
<label for="max-grid-growth">Max grid growth :
<input id="max-grid-growth" type="number" min="0" max="5" step="1" value="0">
</label>
<label for="extra-letters">Extra letters :
<select id="extra-letters">
<option value="secret-word" selected>form a secret word</option>
<label for="max-attemps-per-grid-and-wordset">Max attemps per grid &amp; wordset :
<input id="max-attemps-per-grid-and-wordset" type="number" min="1" max="500" step="1" value="100">
</label>
<label for="prefer-letter-overlaps">Prefer letter overlap :
<input id="prefer-letter-overlaps" type="checkbox" checked>
</label>
<label for="filling-letters">Filling letters :
<select id="filling-letters" selected>
<option value="secret-word">form a secret word</option>
<option value="none">none, allow blanks</option>
<option value="secret-word-plus-blanks">form a secret word but allow for extra blanks</option>
<option value="random">random</option>
Expand Down Expand Up @@ -65,43 +73,44 @@ <h1>WordFind.js by BunKat &amp; Lucas-C</h1>
'prudente',
'sexy',
'tendre',
].map(word => WordFindGame.insertWordBefore($('#add-word').parent(), word));
].map(word => WordFindGame.insertWord($('#words'), word));
$('#secret-word').val('LAETITIA');

/* Init */
function recreate() {
$('#puzzle').children().remove();
$('#result-message').removeClass();
var fillBlanks, game;
if ($('#extra-letters').val() === 'none') {
if ($('#filling-letters').val() === 'none') {
fillBlanks = false;
} else if ($('#extra-letters').val().startsWith('secret-word')) {
} else if ($('#filling-letters').val().startsWith('secret-word')) {
fillBlanks = $('#secret-word').val();
}
try {
game = new WordFindGame('#puzzle', {
allowedMissingWords: +$('#allowed-missing-words').val(),
maxGridGrowth: +$('#max-grid-growth').val(),
fillBlanks: fillBlanks,
allowExtraBlanks: ['none', 'secret-word-plus-blanks'].includes($('#extra-letters').val()),
maxAttempts: 100,
allowExtraBlanks: ['none', 'secret-word-plus-blanks'].includes($('#filling-letters').val()),
maxAttempts: +$('#max-attemps-per-grid-and-wordset').val(),
preferOverlap: $('#prefer-letter-overlaps').val() == 'on'
});
} catch (error) {
$('#result-message').text(`😞 ${error}, try to specify less ones`).css({color: 'red'});
$('#result-message').text(`😞 ${error} → Retry, specify less words or allow more missing words / grid growth`).css({color: 'red'});
return;
}
wordfind.print(game);
if (window.game) {
var emptySquaresCount = WordFindGame.emptySquaresCount();
$('#result-message').text(`😃 ${emptySquaresCount ? 'but there are empty squares' : ''}`).css({color: ''});
}
wordfind.print(game.puzzle);
var emptySquaresCount = WordFindGame.emptySquaresCount();
$('#result-message').text(`😃 ${emptySquaresCount ? 'but there are empty squares' : ''}`).css({color: ''});
window.game = game;
}
recreate();

/* Event listeners */
$('#extra-letters').change((evt) => $('#secret-word').prop('disabled', !evt.target.value.startsWith('secret-word')));
$('#filling-letters').change((evt) => $('#secret-word').prop('disabled', !evt.target.value.startsWith('secret-word')));

$('#add-word').click( () => WordFindGame.insertWordBefore($('#add-word').parent()));
$('#remove-last-word').click( () => $('#words').children().last().remove());
$('#add-word').click( () => WordFindGame.insertWord($('#words')));

$('#create-grid').click(recreate);

Expand Down
33 changes: 16 additions & 17 deletions wordfind.css
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,10 @@ h1 {
}

#main {
margin: 0 auto;
max-width: 80rem;
text-align: center; /* to center #puzzle on small devices */
}
@media only screen and (min-width: 600px) {
#main {
text-align: left;
}
display: flex;
flex-flow: row wrap;
justify-content: center;
align-items: center;
}

/**
Expand All @@ -36,7 +32,7 @@ h1 {
#puzzle {
display: inline-block;
border: 1px solid black;
padding: 3vw;
padding: 0;
}

#puzzle > div { /* rows */
Expand All @@ -50,14 +46,10 @@ h1 {
width: 7vw;
text-transform: uppercase;
background-color: white;
border: 0;
border: 1px solid black;
font: 5vw sans-serif;
}
@media only screen and (min-width: 600px) {
#puzzle {
float: left;
padding: 1rem;
}
.puzzleSquare {
height: 3.5rem;
width: 3.5rem;
Expand Down Expand Up @@ -93,8 +85,13 @@ button::-moz-focus-inner {
/**
* Styles for the word list
*/
#words-editor {
display: flex;
flex-flow: column;
justify-content: center;
align-items: center;
}
#words {
display: inline-block;
max-width: 30rem;
padding: 1em;
list-style-type: none;
Expand Down Expand Up @@ -124,7 +121,6 @@ button::-moz-focus-inner {
* Styles for the controls
*/
#controls {
display: inline-block;
max-width: 30rem;
padding: 1em;
border: none;
Expand All @@ -140,7 +136,10 @@ input {
font-size: 1em;
}
input[type="number"] {
width: 2rem;
width: 3rem;
}
select {
max-width: 10rem;
}
select {
max-width: 10rem;
Expand Down
120 changes: 59 additions & 61 deletions wordfind.js
100755 → 100644
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@
* WordFind has no dependencies.
*/

// FROM: https://stackoverflow.com/a/6274381/636849
function shuffle(a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}

/**
* Initializes the WordFind object.
*
Expand Down Expand Up @@ -91,7 +100,7 @@
* @param {[String]} words: The list of words to fit into the puzzle
* @param {[Options]} options: The options to use when filling the puzzle
*/
var fillPuzzle = function (words, options) {
var fillPuzzle = function (words, options, wordsNotIncluded) {

var puzzle = [], i, j, len;

Expand All @@ -104,10 +113,15 @@
}

// add each word into the puzzle one at a time
shuffle(words);
for (i = 0, len = words.length; i < len; i++) {
if (!placeWordInPuzzle(puzzle, options, words[i])) {
// if a word didn't fit in the puzzle, give up
return null;
if (options.allowedMissingWords && wordsNotIncluded.length < options.allowedMissingWords) {
wordsNotIncluded.push(words[i]);
} else {
// if a word didn't fit in the puzzle, give up
return null;
}
}
}

Expand Down Expand Up @@ -327,27 +341,57 @@

// copy and sort the words by length, inserting words into the puzzle
// from longest to shortest works out the best
wordList = words.slice(0).sort();
wordList = words.slice(0).sort((a, b) => b.length - a.length);

// initialize the options
var maxWordLength = wordList[0].length;
var options = {
height: opts.height || maxWordLength,
width: opts.width || maxWordLength,
orientations: opts.orientations || allOrientations,
fillBlanks: opts.fillBlanks !== undefined ? opts.fillBlanks : true,
allowExtraBlanks: opts.allowExtraBlanks !== undefined ? opts.allowExtraBlanks : true,
maxAttempts: opts.maxAttempts || 3,
maxGridGrowth: opts.maxGridGrowth !== undefined ? opts.maxGridGrowth : 10,
preferOverlap: opts.preferOverlap !== undefined ? opts.preferOverlap : true
height: opts.height || maxWordLength,
width: opts.width || maxWordLength,
orientations: opts.orientations || allOrientations,
fillBlanks: opts.fillBlanks !== undefined ? opts.fillBlanks : true,
allowExtraBlanks: opts.allowExtraBlanks !== undefined ? opts.allowExtraBlanks : true,
maxAttempts: opts.maxAttempts || 3,
maxGridGrowth: opts.maxGridGrowth !== undefined ? opts.maxGridGrowth : 10,
preferOverlap: opts.preferOverlap !== undefined ? opts.preferOverlap : true,
allowedMissingWords: opts.allowedMissingWords,
};

// add the words to the puzzle
// since puzzles are random, attempt to create a valid one up to
// maxAttempts and then increase the puzzle size and try again
while (!puzzle) {
while (!puzzle && attempts++ < options.maxAttempts) {
puzzle = fillPuzzle(wordList, options);
try {
var wordsNotIncluded = [];
puzzle = fillPuzzle(wordList, options, wordsNotIncluded);
// fill in empty spaces with random letters
if (options.fillBlanks) {
var lettersToAdd, fillingBlanksCount = 0, extraLetterGenerator;
if (typeof options.fillBlanks === 'function') {
extraLetterGenerator = options.fillBlanks;
} else if (typeof options.fillBlanks === 'string') {
lettersToAdd = options.fillBlanks.toLowerCase().split('');
extraLetterGenerator = () => lettersToAdd.pop() || (fillingBlanksCount++ && '');
} else {
extraLetterGenerator = () => LETTERS[Math.floor(Math.random() * LETTERS.length)];
}
var extraLettersCount = this.fillBlanks({puzzle, extraLetterGenerator: extraLetterGenerator});
if (lettersToAdd && lettersToAdd.length) {
throw new Error(`Some extra letters provided were not used: ${lettersToAdd}`);
}
if (lettersToAdd && fillingBlanksCount && !options.allowExtraBlanks) {
throw new Error(`${fillingBlanksCount} extra letters were missing to fill the grid`);
}
var gridFillPercent = 100 * (1 - extraLettersCount / (options.width * options.height));
console.log(`Final grid filled at ${gridFillPercent.toFixed(0)}% - Blanks filled with ${extraLettersCount} letters`);
}
} catch (e) {
if (attempts > options.maxAttempts) {
throw e;
}
puzzle = null; // force retry
}
}

if (!puzzle) {
Expand All @@ -362,59 +406,13 @@
}
}

// fill in empty spaces with random letters
if (options.fillBlanks) {
var lettersToAdd, fillingBlanksCount = 0, extraLetterGenerator;
if (typeof options.fillBlanks === 'function') {
extraLetterGenerator = options.fillBlanks;
} else if (typeof options.fillBlanks === 'string') {
lettersToAdd = options.fillBlanks.toLowerCase().split('');
extraLetterGenerator = () => lettersToAdd.pop() || (fillingBlanksCount++ && '');
} else {
extraLetterGenerator = () => LETTERS[Math.floor(Math.random() * LETTERS.length)];
}
var extraLettersCount = this.fillBlanks({puzzle, extraLetterGenerator: extraLetterGenerator});
if (lettersToAdd && lettersToAdd.length) {
throw new Error(`Some extra letters provided were not used: ${lettersToAdd}`);
}
if (lettersToAdd && fillingBlanksCount && !options.allowExtraBlanks) {
throw new Error(`${fillingBlanksCount} extra letters were missing to fill the grid`);
}
var gridFillPercent = 100 * (1 - extraLettersCount / (options.width * options.height));
console.log(`Blanks filled with ${extraLettersCount} random letters - Final grid is filled at ${gridFillPercent.toFixed(0)}%`);
if (wordsNotIncluded) {
console.log('Words not included:', wordsNotIncluded.join(', '));
}

return puzzle;
},

/**
* Wrapper around `newPuzzle` allowing to find a solution without some words.
*
* @param {options} settings: The options to use for this puzzle.
* Same as `newPuzzle` + allowedMissingWords
*/
newPuzzleLax: function(words, opts) {
try {
return this.newPuzzle(words, opts);
} catch (e) {
if (!opts.allowedMissingWords) {
throw e;
}
var opts = Object.assign({}, opts); // shallow copy
opts.allowedMissingWords--;
for (var i = 0; i < words.length; i++) {
var wordList = words.slice(0);
wordList.splice(i, 1);
try {
var puzzle = this.newPuzzleLax(wordList, opts);
console.log(`Solution found without word "${words[i]}"`);
return puzzle;
} catch (e) {} // continue if error
}
throw e;
}
},

/**
* Fills in any empty spaces in the puzzle with random letters.
*
Expand Down
Loading