├── LICENSE ├── README.md ├── example_images ├── crossword1_blank.png ├── crossword1_filled.png └── crossword2_plain_text_layout.png ├── index.html └── layout_generator.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Michael Wehar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Crossword Layout Generator - Open Source 2 | ## Introduction 3 | A crossword consists of clues, answers, and a layout: 4 | - The answers are the hidden words that the player is trying to guess. 5 | - Each answer has a clue which is a sentence or phrase that helps the player to guess the associated answer. 6 | - The **crossword layout** describes where the answers are located in a two-dimensional grid. 7 | 8 | This crossword layout generator takes in a list of answers and outputs a crossword layout. Our program **does not** generate the answers or the clues. 9 | 10 | ## Input and Output Format 11 | 12 | An input is a list of answers in a JSON format. The clues can optionally be included with the input. 13 | 14 | Here is an example input: 15 | 16 | `[{"clue":"that which is established as a rule or model by authority, custom, or general consent","answer":"standard"},{"clue":"a machine that computes","answer":"computer"},{"clue":"the collective designation of items for a particular purpose","answer":"equipment"},{"clue":"an opening or entrance to an inclosed place","answer":"port"},{"clue":"a point where two things can connect and interact","answer":"interface"}]` 17 | 18 | The output is a crossword layout. That is, we associate a position, startx, starty, and orientation with each answer. 19 | 20 | Here is an example output: 21 | 22 | `[{"clue":"the collective designation of items for a particular purpose","answer":"equipment","startx":1,"starty":4,"position":1,"orientation":"across"},{"clue":"an opening or entrance to an inclosed place","answer":"port","startx":5,"starty":4,"position":2,"orientation":"down"},{"clue":"that which is established as a rule or model by authority, custom, or general consent","answer":"standard","startx":8,"starty":1,"position":3,"orientation":"down"},{"clue":"a machine that computes","answer":"computer","startx":3,"starty":2,"position":4,"orientation":"across"},{"clue":"a point where two things can connect and interact","answer":"interface","startx":1,"starty":1,"position":5,"orientation":"down"}]` 23 | 24 | One can visualize the output as follows: 25 | 26 | ![Example Output](https://github.com/MichaelWehar/Crossword-Layout-Generator/blob/master/example_images/crossword1_filled.png) 27 | 28 | ## Getting Started 29 | 30 | **Step 1:** Add the following line to the head of your html document: 31 | 32 | `` 33 | 34 | **Step 2:** In the body of your html document, you can add the following JavaScript: 35 | 36 | ``` 37 | 47 | ``` 48 | 49 | **Update:** Our crossword layout generator is now available as a package for Node.js applications. For more information, see the Node.js version of our README here: https://github.com/MichaelWehar/Crossword-Layout-Generator/blob/npm/README.md 50 | 51 | Also, see our package's npm listing here: https://www.npmjs.com/package/crossword-layout-generator 52 | 53 | ## Demo Website 54 | 55 | The demo website's source code can be found in `index.html`. 56 | 57 | The demo website shows: 58 | 59 | - how to generate the crossword layout in a JSON format 60 | 61 | - how to generate the crossword layout in a plain text grid format (using HTML line breaks). 62 | 63 | - how to turn your crossword layout into a **word search puzzle** with horizontal and vertical answers. 64 | 65 | **Demo:** http://michaelwehar.com/crosswords 66 | 67 | **Short Article:** https://makeprojects.com/project/crossword-layout-generator---open-source 68 | 69 | ## Information for Advanced Users 70 | 71 | - The generated layouts don't always contain all of the input words. If a word does not appear in the layout, then its orientation attribute will be set to "none". 72 | 73 | - The generated crossword layouts are not always connected. Occasionally, there will be islands of disconnected words. 74 | 75 | - The program is efficient on small word lists, but it runs noticably slower when the list contains more than 100 words. 76 | 77 | - We are still exploring potential ways to evaluate the quality of the generated crossword layouts. See [Issue #2](https://github.com/MichaelWehar/Crossword-Layout-Generator/issues/2). 78 | 79 | ## License 80 | - MIT 81 | 82 | ## Credits 83 | - Michael Wehar 84 | - Itay Livni 85 | - Michael Blättler 86 | 87 | ## Forked Repositories with Additional Features 88 | 89 | During Fall 2024, two teams of students in the Open Source Software Development course at Swarthmore College, taught by Chris Murphy, worked to improve this open source project by adding additional features. 90 | 91 | **Added features for AI integration and dark mode** 92 | 93 | Contributors: Abe P., Mina M., and Nick F. 94 | 95 | Link: https://github.com/nicholasyfu1/Crossword-Layout-Generator 96 | 97 | **Added features for generating PDF's and using synonyms** 98 | 99 | Contributors: Jayson B., Marcus W., and Sharvari T. 100 | 101 | Link: https://github.com/MarcusW03/Crossword-Layout-Generator 102 | 103 | ## Updates (December 2024) 104 | 105 | I also co-implemented an algorithm for automatic crossword puzzle filling. Take a look at the new repository here @ [Automatic Crossword Puzzle Filling](https://github.com/MichaelWehar/Automatic-Crossword-Puzzle-Filling) 106 | 107 | ## External Projects That Use Our Library 108 | 109 | - [WoordSchaap](https://github.com/erasche/woordschaap) 110 | 111 | - [Collaboration with TapNotion at PyCon 2018](https://pycon-archive.python.org/2018/schedule/presentation/179/) 112 | -------------------------------------------------------------------------------- /example_images/crossword1_blank.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelWehar/Crossword-Layout-Generator/ff5ff36b223da58ca0d039c06ab29a8b91b32a94/example_images/crossword1_blank.png -------------------------------------------------------------------------------- /example_images/crossword1_filled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelWehar/Crossword-Layout-Generator/ff5ff36b223da58ca0d039c06ab29a8b91b32a94/example_images/crossword1_filled.png -------------------------------------------------------------------------------- /example_images/crossword2_plain_text_layout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MichaelWehar/Crossword-Layout-Generator/ff5ff36b223da58ca0d039c06ab29a8b91b32a94/example_images/crossword2_plain_text_layout.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Crossword Layout Generator 4 | 5 | 6 | 7 | 8 | 16 | 17 | 18 | 19 | 20 |
21 |

22 |

Crossword Layout Generator

23 |

Enter a list of words below



24 | 25 |



26 |


27 | 28 |

29 | 30 |

View Source Code (MIT License)


31 | Short Article @ ProjectBoard



32 | 33 | 42 |
43 | 44 | 90 | 91 |

92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /layout_generator.js: -------------------------------------------------------------------------------- 1 | // Author: Michael Wehar 2 | // Additional credits: Itay Livni, Michael Blättler 3 | // MIT License 4 | 5 | // Math functions 6 | function distance(x1, y1, x2, y2){ 7 | return Math.abs(x1 - x2) + Math.abs(y1 - y2); 8 | } 9 | 10 | function weightedAverage(weights, values){ 11 | var temp = 0; 12 | for(let k = 0; k < weights.length; k++){ 13 | temp += weights[k] * values[k]; 14 | } 15 | 16 | if(temp < 0 || temp > 1){ 17 | console.log("Error: " + values); 18 | } 19 | 20 | return temp; 21 | } 22 | 23 | 24 | // Component scores 25 | // 1. Number of connections 26 | function computeScore1(connections, word){ 27 | return (connections / (word.length / 2)); 28 | } 29 | 30 | // 2. Distance from center 31 | function computeScore2(rows, cols, i, j){ 32 | return 1 - (distance(rows / 2, cols / 2, i, j) / ((rows / 2) + (cols / 2))); 33 | } 34 | 35 | // 3. Vertical versus horizontal orientation 36 | function computeScore3(a, b, verticalCount, totalCount){ 37 | if(verticalCount > totalCount / 2){ 38 | return a; 39 | } 40 | else if(verticalCount < totalCount / 2){ 41 | return b; 42 | } 43 | else{ 44 | return 0.5; 45 | } 46 | } 47 | 48 | // 4. Word length 49 | function computeScore4(val, word){ 50 | return word.length / val; 51 | } 52 | 53 | 54 | // Word functions 55 | function addWord(best, words, table){ 56 | var bestScore = best[0]; 57 | var word = best[1]; 58 | var index = best[2]; 59 | var bestI = best[3]; 60 | var bestJ = best[4]; 61 | var bestO = best[5]; 62 | 63 | words[index].startx = bestJ + 1; 64 | words[index].starty = bestI + 1; 65 | 66 | if(bestO == 0){ 67 | for(let k = 0; k < word.length; k++){ 68 | table[bestI][bestJ + k] = word.charAt(k); 69 | } 70 | words[index].orientation = "across"; 71 | } 72 | else{ 73 | for(let k = 0; k < word.length; k++){ 74 | table[bestI + k][bestJ] = word.charAt(k); 75 | } 76 | words[index].orientation = "down"; 77 | } 78 | console.log(word + ", " + bestScore); 79 | } 80 | 81 | function assignPositions(words){ 82 | var positions = {}; 83 | for(let index in words){ 84 | var word = words[index]; 85 | if(word.orientation != "none"){ 86 | var tempStr = word.starty + "," + word.startx; 87 | if(tempStr in positions){ 88 | word.position = positions[tempStr]; 89 | } 90 | else{ 91 | // Object.keys is supported in ES5-compatible environments 92 | positions[tempStr] = Object.keys(positions).length + 1; 93 | word.position = positions[tempStr]; 94 | } 95 | } 96 | } 97 | } 98 | 99 | function computeDimension(words, factor){ 100 | var temp = 0; 101 | for(let i = 0; i < words.length; i++){ 102 | if(temp < words[i].answer.length){ 103 | temp = words[i].answer.length; 104 | } 105 | } 106 | 107 | return temp * factor; 108 | } 109 | 110 | 111 | // Table functions 112 | function initTable(rows, cols){ 113 | var table = []; 114 | for(let i = 0; i < rows; i++){ 115 | for(let j = 0; j < cols; j++){ 116 | if(j == 0){ 117 | table[i] = ["-"]; 118 | } 119 | else{ 120 | table[i][j] = "-"; 121 | } 122 | } 123 | } 124 | 125 | return table; 126 | } 127 | 128 | function isConflict(table, isVertical, character, i, j){ 129 | if(character != table[i][j] && table[i][j] != "-"){ 130 | return true; 131 | } 132 | else if(table[i][j] == "-" && !isVertical && (i + 1) in table && table[i + 1][j] != "-"){ 133 | return true; 134 | } 135 | else if(table[i][j] == "-" && !isVertical && (i - 1) in table && table[i - 1][j] != "-"){ 136 | return true; 137 | } 138 | else if(table[i][j] == "-" && isVertical && (j + 1) in table[i] && table[i][j + 1] != "-"){ 139 | return true; 140 | } 141 | else if(table[i][j] == "-" && isVertical && (j - 1) in table[i] && table[i][j - 1] != "-"){ 142 | return true 143 | } 144 | else{ 145 | return false; 146 | } 147 | } 148 | 149 | function attemptToInsert(rows, cols, table, weights, verticalCount, totalCount, word, index){ 150 | var bestI = 0; 151 | var bestJ = 0; 152 | var bestO = 0; 153 | var bestScore = -1; 154 | 155 | // Horizontal 156 | for(let i = 0; i < rows; i++){ 157 | for(let j = 0; j < cols - word.length + 1; j++){ 158 | var isValid = true; 159 | var atleastOne = false; 160 | var connections = 0; 161 | var prevFlag = false; 162 | 163 | for(let k = 0; k < word.length; k++){ 164 | if(isConflict(table, false, word.charAt(k), i, j + k)){ 165 | isValid = false; 166 | break; 167 | } 168 | else if(table[i][j + k] == "-"){ 169 | prevFlag = false; 170 | atleastOne = true; 171 | } 172 | else{ 173 | if(prevFlag){ 174 | isValid = false; 175 | break; 176 | } 177 | else{ 178 | prevFlag = true; 179 | connections += 1; 180 | } 181 | } 182 | } 183 | 184 | if((j - 1) in table[i] && table[i][j - 1] != "-"){ 185 | isValid = false; 186 | } 187 | else if((j + word.length) in table[i] && table[i][j + word.length] != "-"){ 188 | isValid = false; 189 | } 190 | 191 | if(isValid && atleastOne && word.length > 1){ 192 | var tempScore1 = computeScore1(connections, word); 193 | var tempScore2 = computeScore2(rows, cols, i, j + (word.length / 2), word); 194 | var tempScore3 = computeScore3(1, 0, verticalCount, totalCount); 195 | var tempScore4 = computeScore4(rows, word); 196 | var tempScore = weightedAverage(weights, [tempScore1, tempScore2, tempScore3, tempScore4]); 197 | 198 | if(tempScore > bestScore){ 199 | bestScore = tempScore; 200 | bestI = i; 201 | bestJ = j; 202 | bestO = 0; 203 | } 204 | } 205 | } 206 | } 207 | 208 | // Vertical 209 | for(let i = 0; i < rows - word.length + 1; i++){ 210 | for(let j = 0; j < cols; j++){ 211 | var isValid = true; 212 | var atleastOne = false; 213 | var connections = 0; 214 | var prevFlag = false; 215 | 216 | for(let k = 0; k < word.length; k++){ 217 | if(isConflict(table, true, word.charAt(k), i + k, j)){ 218 | isValid = false; 219 | break; 220 | } 221 | else if(table[i + k][j] == "-"){ 222 | prevFlag = false; 223 | atleastOne = true; 224 | } 225 | else{ 226 | if(prevFlag){ 227 | isValid = false; 228 | break; 229 | } 230 | else{ 231 | prevFlag = true; 232 | connections += 1; 233 | } 234 | } 235 | } 236 | 237 | if((i - 1) in table && table[i - 1][j] != "-"){ 238 | isValid = false; 239 | } 240 | else if((i + word.length) in table && table[i + word.length][j] != "-"){ 241 | isValid = false; 242 | } 243 | 244 | if(isValid && atleastOne && word.length > 1){ 245 | var tempScore1 = computeScore1(connections, word); 246 | var tempScore2 = computeScore2(rows, cols, i + (word.length / 2), j, word); 247 | var tempScore3 = computeScore3(0, 1, verticalCount, totalCount); 248 | var tempScore4 = computeScore4(rows, word); 249 | var tempScore = weightedAverage(weights, [tempScore1, tempScore2, tempScore3, tempScore4]); 250 | 251 | if(tempScore > bestScore){ 252 | bestScore = tempScore; 253 | bestI = i; 254 | bestJ = j; 255 | bestO = 1; 256 | } 257 | } 258 | } 259 | } 260 | 261 | if(bestScore > -1){ 262 | return [bestScore, word, index, bestI, bestJ, bestO]; 263 | } 264 | else{ 265 | return [-1]; 266 | } 267 | } 268 | 269 | function generateTable(table, rows, cols, words, weights){ 270 | var verticalCount = 0; 271 | var totalCount = 0; 272 | 273 | for(let outerIndex in words){ 274 | var best = [-1]; 275 | for(let innerIndex in words){ 276 | if("answer" in words[innerIndex] && !("startx" in words[innerIndex])){ 277 | var temp = attemptToInsert(rows, cols, table, weights, verticalCount, totalCount, words[innerIndex].answer, innerIndex); 278 | if(temp[0] > best[0]){ 279 | best = temp; 280 | } 281 | } 282 | } 283 | 284 | if(best[0] == -1){ 285 | break; 286 | } 287 | else{ 288 | addWord(best, words, table); 289 | if(best[5] == 1){ 290 | verticalCount += 1; 291 | } 292 | totalCount += 1; 293 | } 294 | } 295 | 296 | for(let index in words){ 297 | if(!("startx" in words[index])){ 298 | words[index].orientation = "none"; 299 | } 300 | } 301 | 302 | return {"table": table, "result": words}; 303 | } 304 | 305 | function removeIsolatedWords(data){ 306 | var oldTable = data.table; 307 | var words = data.result; 308 | var rows = oldTable.length; 309 | var cols = oldTable[0].length; 310 | var newTable = initTable(rows, cols); 311 | 312 | // Draw intersections as "X"'s 313 | for(let wordIndex in words){ 314 | var word = words[wordIndex]; 315 | if(word.orientation == "across"){ 316 | var i = word.starty - 1; 317 | var j = word.startx - 1; 318 | for(let k = 0; k < word.answer.length; k++){ 319 | if(newTable[i][j + k] == "-"){ 320 | newTable[i][j + k] = "O"; 321 | } 322 | else if(newTable[i][j + k] == "O"){ 323 | newTable[i][j + k] = "X"; 324 | } 325 | } 326 | } 327 | else if(word.orientation == "down"){ 328 | var i = word.starty - 1; 329 | var j = word.startx - 1; 330 | for(let k = 0; k < word.answer.length; k++){ 331 | if(newTable[i + k][j] == "-"){ 332 | newTable[i + k][j] = "O"; 333 | } 334 | else if(newTable[i + k][j] == "O"){ 335 | newTable[i + k][j] = "X"; 336 | } 337 | } 338 | } 339 | } 340 | 341 | // Set orientations to "none" if they have no intersections 342 | for(let wordIndex in words){ 343 | var word = words[wordIndex]; 344 | var isIsolated = true; 345 | if(word.orientation == "across"){ 346 | var i = word.starty - 1; 347 | var j = word.startx - 1; 348 | for(let k = 0; k < word.answer.length; k++){ 349 | if(newTable[i][j + k] == "X"){ 350 | isIsolated = false; 351 | break; 352 | } 353 | } 354 | } 355 | else if(word.orientation == "down"){ 356 | var i = word.starty - 1; 357 | var j = word.startx - 1; 358 | for(let k = 0; k < word.answer.length; k++){ 359 | if(newTable[i + k][j] == "X"){ 360 | isIsolated = false; 361 | break; 362 | } 363 | } 364 | } 365 | if(word.orientation != "none" && isIsolated){ 366 | delete words[wordIndex].startx; 367 | delete words[wordIndex].starty; 368 | delete words[wordIndex].position; 369 | words[wordIndex].orientation = "none"; 370 | } 371 | } 372 | 373 | // Draw new table 374 | newTable = initTable(rows, cols); 375 | for(let wordIndex in words){ 376 | var word = words[wordIndex]; 377 | if(word.orientation == "across"){ 378 | var i = word.starty - 1; 379 | var j = word.startx - 1; 380 | for(let k = 0; k < word.answer.length; k++){ 381 | newTable[i][j + k] = word.answer.charAt(k); 382 | } 383 | } 384 | else if(word.orientation == "down"){ 385 | var i = word.starty - 1; 386 | var j = word.startx - 1; 387 | for(let k = 0; k < word.answer.length; k++){ 388 | newTable[i + k][j] = word.answer.charAt(k); 389 | } 390 | } 391 | } 392 | 393 | return {"table": newTable, "result": words}; 394 | } 395 | 396 | function trimTable(data){ 397 | var table = data.table; 398 | var rows = table.length; 399 | var cols = table[0].length; 400 | 401 | var leftMost = cols; 402 | var topMost = rows; 403 | var rightMost = -1; 404 | var bottomMost = -1; 405 | 406 | for(let i = 0; i < rows; i++){ 407 | for(let j = 0; j < cols; j++){ 408 | if(table[i][j] != "-"){ 409 | var x = j; 410 | var y = i; 411 | 412 | if(x < leftMost){ 413 | leftMost = x; 414 | } 415 | if(x > rightMost){ 416 | rightMost = x; 417 | } 418 | if(y < topMost){ 419 | topMost = y; 420 | } 421 | if(y > bottomMost){ 422 | bottomMost = y; 423 | } 424 | } 425 | } 426 | } 427 | 428 | var trimmedTable = initTable(bottomMost - topMost + 1, rightMost - leftMost + 1); 429 | for(let i = topMost; i < bottomMost + 1; i++){ 430 | for(let j = leftMost; j < rightMost + 1; j++){ 431 | trimmedTable[i - topMost][j - leftMost] = table[i][j]; 432 | } 433 | } 434 | 435 | var words = data.result; 436 | for(let entry in words){ 437 | if("startx" in words[entry]) { 438 | words[entry].startx -= leftMost; 439 | words[entry].starty -= topMost; 440 | } 441 | } 442 | 443 | return {"table": trimmedTable, "result": words, "rows": Math.max(bottomMost - topMost + 1, 0), "cols": Math.max(rightMost - leftMost + 1, 0)}; 444 | } 445 | 446 | function tableToString(table, delim){ 447 | var rows = table.length; 448 | if(rows >= 1){ 449 | var cols = table[0].length; 450 | var output = ""; 451 | for(let i = 0; i < rows; i++){ 452 | for(let j = 0; j < cols; j++){ 453 | output += table[i][j]; 454 | } 455 | output += delim; 456 | } 457 | return output; 458 | } 459 | else{ 460 | return ""; 461 | } 462 | } 463 | 464 | function generateSimpleTable(words){ 465 | var rows = computeDimension(words, 3); 466 | var cols = rows; 467 | var blankTable = initTable(rows, cols); 468 | var table = generateTable(blankTable, rows, cols, words, [0.7, 0.15, 0.1, 0.05]); 469 | var newTable = removeIsolatedWords(table); 470 | var finalTable = trimTable(newTable); 471 | assignPositions(finalTable.result); 472 | return finalTable; 473 | } 474 | 475 | function generateLayout(words_json){ 476 | var layout = generateSimpleTable(words_json); 477 | layout.table_string = tableToString(layout.table, "
"); 478 | return layout; 479 | } 480 | 481 | // The following was added to support Node.js 482 | if(typeof module !== 'undefined'){ 483 | module.exports = { generateLayout }; 484 | } 485 | --------------------------------------------------------------------------------