UNPKG

wordsearch-generator

Version:

Module for creating wordsearch puzzles

220 lines (202 loc) 6.33 kB
/* * File: index.js * Project: word search generator * Date: September 2018 * * Author: Todd Hibbs * * Description: Generates a word search puzzle given a grid size and a list of words * The words can appear forward or backward in either a horizontal, vertical, or diagonal * direction. */ import _ from 'lodash' const LANGUAGES = { 'en': 'abcdefghijklmnopqrstuvwxyz'.split('').map(letter => letter.toUpperCase()), 'es': 'abcdefghijklmnñopqrstuvwxyz'.split('').map(letter => letter.toUpperCase()) } const printGrid = (grid, space) => { let lines = [] _.each(grid, row => { let r = row.join(' ') if (space) { r = r.replace(/0/gi, ' ') } lines.push(r) }) return lines } const initializeGrid = (width, height) => { let grid = [] for(let i = 0; i < height; i++) { grid.push(_.fill(Array(width), 0)) } return grid } const cloneGrid = (grid) => { let _grid = [] grid.forEach(row => _grid.push(row.slice(0))) return _grid } const hideWords = (grid, languageCode) => { let alphabet = LANGUAGES[languageCode] let _grid = cloneGrid(grid) for(let i = 0; i < _grid.length; i++) { for (let j = 0; j < _grid[i].length; j++) { if (_grid[i][j] == 0) { let alphabetIndex = getRandomIntInclusive(0, alphabet.length - 1) let letter = alphabet[alphabetIndex] // console.log('alphabetIndex', alphabetIndex, 'letter', letter) _grid[i][j] = letter } } } return _grid } const reverseWord = (word) => _.reverse(word.split('')).join('') const cleanWord = (word) => word.split(' ').join('') const cleanWordsList = (words) => words.map(cleanWord).map(w => w.toUpperCase()) const sortWordsByLength = (words) => { let _words = words return _words.sort((w1, w2) => w2.length - w1.length ) } const minColumnIndex = () => 0 const maxColumnIndex = (grid, word) => { // maximum starting column index const wordLength = word.length const gridWidth = grid[0].length if (wordLength > gridWidth) { throw "Grid Width is too narrow for the given word: " + word } return gridWidth - wordLength } const minRowIndex = () => 0 const maxRowIndex = (grid, word) => { // maximum starting row index const wordLength = word.length const gridHeight = grid.length if (wordLength > gridHeight) { throw "Grid Height is too short for the given word: " + word } return gridHeight - wordLength } const minimumGridSize = (wordList) => cleanWordsList(wordList).map(w => w.length).reduce((a, c) => Math.max(a, c)) const getRandomInt = (max) => Math.floor(Math.random() * Math.floor(max)) const getRandomIntInclusive = (min, max) => { const _min = Math.ceil(min) const _max = Math.floor(max) return Math.floor(Math.random() * (_max - _min + 1)) + _min } const getRandomBool = () => !!getRandomInt(2) const getRandomDirection = () => { const directions = [ 'horizontal', 'vertical', 'diagonal' ] return directions[getRandomIntInclusive(0, 2)] } const canPlaceWord = (grid, word, row, column, direction) => { let _grid = cloneGrid(grid) for (let i = 0; i < word.length; i++) { let r = row let c = column if (direction == 'vertical' || direction == 'diagonal') { r += i } if (direction == 'horizontal' || direction == 'diagonal') { c += i } let cell = _grid[r][c] if (cell != 0 && word[i] != cell) { //collision detected return false } // _grid[r][c] = word[i] } //made it to the end with no collisions return true } const placeWord = (grid, word, row, column, direction) => { //returns a new grid with the word placed in the specified location let _grid = grid for (let i = 0; i < word.length; i++) { let r = row let c = column if (direction == 'vertical' || direction == 'diagonal') { r += i } if (direction == 'horizontal' || direction == 'diagonal') { c += i } _grid[r][c] = word[i] } return _grid } /* This function is potentially called recursively until a valid location for the word * within the grid is located. To prevent stack overflow situations, arbitrary limites are set * on the number of times it can be called */ const placeWordRandom = (grid, word, counter) => { // console.log('placeWordRandom', 'word', word, 'counter', counter) if (counter > 1000) { throw 'Unable to find any location for this word. Try increasing grid size' } const isBackward = getRandomBool() const rowIndex = getRandomIntInclusive(minRowIndex(), maxRowIndex(grid, word)) const columnIndex = getRandomIntInclusive(minColumnIndex(), maxColumnIndex(grid, word)) const direction = getRandomDirection() let _grid = cloneGrid(grid) let _word = isBackward ? reverseWord(word) : word // NOTE: it might be smarter to try to find overlaps. this implementation simply brute forces a place // to put the word by checking many possible locations. The smaller the grid, the more difficult this is let wordPlaced = false let tryCount = 0 while (!wordPlaced && tryCount < 1000) { if (canPlaceWord(_grid, _word, rowIndex, columnIndex, direction)) { _grid = placeWord(_grid, _word, rowIndex, columnIndex, direction) wordPlaced = true } tryCount++ } if (!wordPlaced) { // console.log('trying in different random position:', word) _grid = placeWordRandom(_grid, word, ++counter) } return _grid } const createPuzzle = (width, height, languageCode, words) => { let _wordList = sortWordsByLength(cleanWordsList(words)) let minimumGrid = minimumGridSize(_wordList) //expand grid if needed // let gridWidth = minimumGrid > gridWidth ? minimumGrid * 2 : gridWidth // let gridHeight = minimumGrid > gridHeight ? minimumGrid * 2 : gridHeight let grid = initializeGrid(width, height) _wordList.forEach(w => { grid = placeWordRandom(grid, w, 0) }) return grid } module.exports = { createPuzzle, printGrid, initializeGrid, cloneGrid, hideWords, reverseWord, cleanWord, cleanWordsList, sortWordsByLength, minColumnIndex, maxColumnIndex, minRowIndex, maxRowIndex, minimumGridSize, getRandomInt, getRandomIntInclusive, getRandomBool, getRandomDirection, canPlaceWord, placeWord, placeWordRandom, LANGUAGES }