crossword-layout-generator
Version:
Generates a crossword layout from any list of answers given in a JSON format.
485 lines (431 loc) • 12.2 kB
JavaScript
// Author: Michael Wehar
// Additional credits: Itay Livni, Michael Blättler
// MIT License
// Math functions
function distance(x1, y1, x2, y2){
return Math.abs(x1 - x2) + Math.abs(y1 - y2);
}
function weightedAverage(weights, values){
var temp = 0;
for(let k = 0; k < weights.length; k++){
temp += weights[k] * values[k];
}
if(temp < 0 || temp > 1){
console.log("Error: " + values);
}
return temp;
}
// Component scores
// 1. Number of connections
function computeScore1(connections, word){
return (connections / (word.length / 2));
}
// 2. Distance from center
function computeScore2(rows, cols, i, j){
return 1 - (distance(rows / 2, cols / 2, i, j) / ((rows / 2) + (cols / 2)));
}
// 3. Vertical versus horizontal orientation
function computeScore3(a, b, verticalCount, totalCount){
if(verticalCount > totalCount / 2){
return a;
}
else if(verticalCount < totalCount / 2){
return b;
}
else{
return 0.5;
}
}
// 4. Word length
function computeScore4(val, word){
return word.length / val;
}
// Word functions
function addWord(best, words, table){
var bestScore = best[0];
var word = best[1];
var index = best[2];
var bestI = best[3];
var bestJ = best[4];
var bestO = best[5];
words[index].startx = bestJ + 1;
words[index].starty = bestI + 1;
if(bestO == 0){
for(let k = 0; k < word.length; k++){
table[bestI][bestJ + k] = word.charAt(k);
}
words[index].orientation = "across";
}
else{
for(let k = 0; k < word.length; k++){
table[bestI + k][bestJ] = word.charAt(k);
}
words[index].orientation = "down";
}
console.log(word + ", " + bestScore);
}
function assignPositions(words){
var positions = {};
for(let index in words){
var word = words[index];
if(word.orientation != "none"){
var tempStr = word.starty + "," + word.startx;
if(tempStr in positions){
word.position = positions[tempStr];
}
else{
// Object.keys is supported in ES5-compatible environments
positions[tempStr] = Object.keys(positions).length + 1;
word.position = positions[tempStr];
}
}
}
}
function computeDimension(words, factor){
var temp = 0;
for(let i = 0; i < words.length; i++){
if(temp < words[i].answer.length){
temp = words[i].answer.length;
}
}
return temp * factor;
}
// Table functions
function initTable(rows, cols){
var table = [];
for(let i = 0; i < rows; i++){
for(let j = 0; j < cols; j++){
if(j == 0){
table[i] = ["-"];
}
else{
table[i][j] = "-";
}
}
}
return table;
}
function isConflict(table, isVertical, character, i, j){
if(character != table[i][j] && table[i][j] != "-"){
return true;
}
else if(table[i][j] == "-" && !isVertical && (i + 1) in table && table[i + 1][j] != "-"){
return true;
}
else if(table[i][j] == "-" && !isVertical && (i - 1) in table && table[i - 1][j] != "-"){
return true;
}
else if(table[i][j] == "-" && isVertical && (j + 1) in table[i] && table[i][j + 1] != "-"){
return true;
}
else if(table[i][j] == "-" && isVertical && (j - 1) in table[i] && table[i][j - 1] != "-"){
return true
}
else{
return false;
}
}
function attemptToInsert(rows, cols, table, weights, verticalCount, totalCount, word, index){
var bestI = 0;
var bestJ = 0;
var bestO = 0;
var bestScore = -1;
// Horizontal
for(let i = 0; i < rows; i++){
for(let j = 0; j < cols - word.length + 1; j++){
var isValid = true;
var atleastOne = false;
var connections = 0;
var prevFlag = false;
for(let k = 0; k < word.length; k++){
if(isConflict(table, false, word.charAt(k), i, j + k)){
isValid = false;
break;
}
else if(table[i][j + k] == "-"){
prevFlag = false;
atleastOne = true;
}
else{
if(prevFlag){
isValid = false;
break;
}
else{
prevFlag = true;
connections += 1;
}
}
}
if((j - 1) in table[i] && table[i][j - 1] != "-"){
isValid = false;
}
else if((j + word.length) in table[i] && table[i][j + word.length] != "-"){
isValid = false;
}
if(isValid && atleastOne && word.length > 1){
var tempScore1 = computeScore1(connections, word);
var tempScore2 = computeScore2(rows, cols, i, j + (word.length / 2), word);
var tempScore3 = computeScore3(1, 0, verticalCount, totalCount);
var tempScore4 = computeScore4(rows, word);
var tempScore = weightedAverage(weights, [tempScore1, tempScore2, tempScore3, tempScore4]);
if(tempScore > bestScore){
bestScore = tempScore;
bestI = i;
bestJ = j;
bestO = 0;
}
}
}
}
// Vertical
for(let i = 0; i < rows - word.length + 1; i++){
for(let j = 0; j < cols; j++){
var isValid = true;
var atleastOne = false;
var connections = 0;
var prevFlag = false;
for(let k = 0; k < word.length; k++){
if(isConflict(table, true, word.charAt(k), i + k, j)){
isValid = false;
break;
}
else if(table[i + k][j] == "-"){
prevFlag = false;
atleastOne = true;
}
else{
if(prevFlag){
isValid = false;
break;
}
else{
prevFlag = true;
connections += 1;
}
}
}
if((i - 1) in table && table[i - 1][j] != "-"){
isValid = false;
}
else if((i + word.length) in table && table[i + word.length][j] != "-"){
isValid = false;
}
if(isValid && atleastOne && word.length > 1){
var tempScore1 = computeScore1(connections, word);
var tempScore2 = computeScore2(rows, cols, i + (word.length / 2), j, word);
var tempScore3 = computeScore3(0, 1, verticalCount, totalCount);
var tempScore4 = computeScore4(rows, word);
var tempScore = weightedAverage(weights, [tempScore1, tempScore2, tempScore3, tempScore4]);
if(tempScore > bestScore){
bestScore = tempScore;
bestI = i;
bestJ = j;
bestO = 1;
}
}
}
}
if(bestScore > -1){
return [bestScore, word, index, bestI, bestJ, bestO];
}
else{
return [-1];
}
}
function generateTable(table, rows, cols, words, weights){
var verticalCount = 0;
var totalCount = 0;
for(let outerIndex in words){
var best = [-1];
for(let innerIndex in words){
if("answer" in words[innerIndex] && !("startx" in words[innerIndex])){
var temp = attemptToInsert(rows, cols, table, weights, verticalCount, totalCount, words[innerIndex].answer, innerIndex);
if(temp[0] > best[0]){
best = temp;
}
}
}
if(best[0] == -1){
break;
}
else{
addWord(best, words, table);
if(best[5] == 1){
verticalCount += 1;
}
totalCount += 1;
}
}
for(let index in words){
if(!("startx" in words[index])){
words[index].orientation = "none";
}
}
return {"table": table, "result": words};
}
function removeIsolatedWords(data){
var oldTable = data.table;
var words = data.result;
var rows = oldTable.length;
var cols = oldTable[0].length;
var newTable = initTable(rows, cols);
// Draw intersections as "X"'s
for(let wordIndex in words){
var word = words[wordIndex];
if(word.orientation == "across"){
var i = word.starty - 1;
var j = word.startx - 1;
for(let k = 0; k < word.answer.length; k++){
if(newTable[i][j + k] == "-"){
newTable[i][j + k] = "O";
}
else if(newTable[i][j + k] == "O"){
newTable[i][j + k] = "X";
}
}
}
else if(word.orientation == "down"){
var i = word.starty - 1;
var j = word.startx - 1;
for(let k = 0; k < word.answer.length; k++){
if(newTable[i + k][j] == "-"){
newTable[i + k][j] = "O";
}
else if(newTable[i + k][j] == "O"){
newTable[i + k][j] = "X";
}
}
}
}
// Set orientations to "none" if they have no intersections
for(let wordIndex in words){
var word = words[wordIndex];
var isIsolated = true;
if(word.orientation == "across"){
var i = word.starty - 1;
var j = word.startx - 1;
for(let k = 0; k < word.answer.length; k++){
if(newTable[i][j + k] == "X"){
isIsolated = false;
break;
}
}
}
else if(word.orientation == "down"){
var i = word.starty - 1;
var j = word.startx - 1;
for(let k = 0; k < word.answer.length; k++){
if(newTable[i + k][j] == "X"){
isIsolated = false;
break;
}
}
}
if(word.orientation != "none" && isIsolated){
delete words[wordIndex].startx;
delete words[wordIndex].starty;
delete words[wordIndex].position;
words[wordIndex].orientation = "none";
}
}
// Draw new table
newTable = initTable(rows, cols);
for(let wordIndex in words){
var word = words[wordIndex];
if(word.orientation == "across"){
var i = word.starty - 1;
var j = word.startx - 1;
for(let k = 0; k < word.answer.length; k++){
newTable[i][j + k] = word.answer.charAt(k);
}
}
else if(word.orientation == "down"){
var i = word.starty - 1;
var j = word.startx - 1;
for(let k = 0; k < word.answer.length; k++){
newTable[i + k][j] = word.answer.charAt(k);
}
}
}
return {"table": newTable, "result": words};
}
function trimTable(data){
var table = data.table;
var rows = table.length;
var cols = table[0].length;
var leftMost = cols;
var topMost = rows;
var rightMost = -1;
var bottomMost = -1;
for(let i = 0; i < rows; i++){
for(let j = 0; j < cols; j++){
if(table[i][j] != "-"){
var x = j;
var y = i;
if(x < leftMost){
leftMost = x;
}
if(x > rightMost){
rightMost = x;
}
if(y < topMost){
topMost = y;
}
if(y > bottomMost){
bottomMost = y;
}
}
}
}
var trimmedTable = initTable(bottomMost - topMost + 1, rightMost - leftMost + 1);
for(let i = topMost; i < bottomMost + 1; i++){
for(let j = leftMost; j < rightMost + 1; j++){
trimmedTable[i - topMost][j - leftMost] = table[i][j];
}
}
var words = data.result;
for(let entry in words){
if("startx" in words[entry]) {
words[entry].startx -= leftMost;
words[entry].starty -= topMost;
}
}
return {"table": trimmedTable, "result": words, "rows": Math.max(bottomMost - topMost + 1, 0), "cols": Math.max(rightMost - leftMost + 1, 0)};
}
function tableToString(table, delim){
var rows = table.length;
if(rows >= 1){
var cols = table[0].length;
var output = "";
for(let i = 0; i < rows; i++){
for(let j = 0; j < cols; j++){
output += table[i][j];
}
output += delim;
}
return output;
}
else{
return "";
}
}
function generateSimpleTable(words){
var rows = computeDimension(words, 3);
var cols = rows;
var blankTable = initTable(rows, cols);
var table = generateTable(blankTable, rows, cols, words, [0.7, 0.15, 0.1, 0.05]);
var newTable = removeIsolatedWords(table);
var finalTable = trimTable(newTable);
assignPositions(finalTable.result);
return finalTable;
}
function generateLayout(words_json){
var layout = generateSimpleTable(words_json);
layout.table_string = tableToString(layout.table, "<br>");
return layout;
}
// The following was added to support Node.js
if(typeof module !== 'undefined'){
module.exports = { generateLayout };
}