|
|
|
@ -94,8 +94,50 @@ class Clue {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
class CrosswordPuzzle {
|
|
|
|
|
constructor(clues, letters) {
|
|
|
|
|
this.clues = clues === undefined ? [] : clues;
|
|
|
|
|
this.letters = clues === undefined ? new Map() : letters;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
withClue(clue) {
|
|
|
|
|
// TODO Check if valid to add clue?
|
|
|
|
|
let newClues = this.clues.slice();
|
|
|
|
|
newClues.push(clue);
|
|
|
|
|
|
|
|
|
|
let newLetters = new Map(this.letters);
|
|
|
|
|
clue.letterPositions.forEach(pos => {
|
|
|
|
|
newLetters.set(pos.row + ';' + pos.col, pos.letter);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return new CrosswordPuzzle(newClues, newLetters);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get normalizedClues() {
|
|
|
|
|
let rowOffset = -Math.min(...this.clues.map(clue => clue.row));
|
|
|
|
|
let colOffset = -Math.min(...this.clues.map(clue => clue.col));
|
|
|
|
|
|
|
|
|
|
let numRows = rowOffset+Math.max(...this.clues.map(clue => clue.afterRow));
|
|
|
|
|
let numCols = colOffset+Math.max(...this.clues.map(clue => clue.afterCol));
|
|
|
|
|
|
|
|
|
|
// If puzzle is too wide, flip it.
|
|
|
|
|
if (numRows < numCols && numCols > 15) {
|
|
|
|
|
return this.clues
|
|
|
|
|
.map(clue => new Clue(clue.answer,
|
|
|
|
|
clue.col+colOffset,
|
|
|
|
|
clue.row+rowOffset,
|
|
|
|
|
!clue.horizontal));
|
|
|
|
|
} else {
|
|
|
|
|
return this.clues
|
|
|
|
|
.map(clue => new Clue(clue.answer,
|
|
|
|
|
clue.row+rowOffset,
|
|
|
|
|
clue.col+colOffset,
|
|
|
|
|
clue.horizontal));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
class Crossword {
|
|
|
|
|
constructor(letters, clues, minGuessLength) {
|
|
|
|
|
constructor(letters, clues, minGuessLength, allTopWords) {
|
|
|
|
|
this.minGuessLength = minGuessLength;
|
|
|
|
|
this.clues = clues;
|
|
|
|
|
this.unansweredClues = this.clues;
|
|
|
|
@ -104,6 +146,10 @@ class Crossword {
|
|
|
|
|
this.numLetters = letters.length;
|
|
|
|
|
this.availableLetters = letters.split('').sort();
|
|
|
|
|
|
|
|
|
|
this.allTopWords = allTopWords !== undefined ? allTopWords :
|
|
|
|
|
topWords.map(topWord => topWord.word)
|
|
|
|
|
.filter(word => this.isValidGuess(word));
|
|
|
|
|
|
|
|
|
|
this.numRows = Math.max(...clues.map(clue => clue.afterRow));
|
|
|
|
|
this.numCols = Math.max(...clues.map(clue => clue.afterCol));
|
|
|
|
|
}
|
|
|
|
@ -158,6 +204,8 @@ class Crossword {
|
|
|
|
|
let word;
|
|
|
|
|
let sortedLetters;
|
|
|
|
|
let matchingTopWords;
|
|
|
|
|
let minMatches = Math.max(settings.min_matches, settings.num_clues);
|
|
|
|
|
if (settings.max_matches < minMatches) return null;
|
|
|
|
|
do {
|
|
|
|
|
do {
|
|
|
|
|
word = topWords[1000+getRandomInt(30000)].word;
|
|
|
|
@ -173,19 +221,22 @@ class Crossword {
|
|
|
|
|
&& Crossword.isMatchingWord(topWord.word,
|
|
|
|
|
sortedLetters)
|
|
|
|
|
&& isValidWord(topWord.word));
|
|
|
|
|
} while(matchingTopWords.length < settings.min_matches
|
|
|
|
|
} while(matchingTopWords.length < minMatches
|
|
|
|
|
|| matchingTopWords.length > settings.max_matches);
|
|
|
|
|
|
|
|
|
|
let clues = [new Clue(word, 0, 0, getRandomBool())];
|
|
|
|
|
let letters = {};
|
|
|
|
|
clues[0].letterPositions.forEach(pos => {
|
|
|
|
|
letters[pos.row + ';' + pos.col] = pos.letter;
|
|
|
|
|
});
|
|
|
|
|
while (clues.length < settings.num_clues) {
|
|
|
|
|
if (matchingTopWords.length == 0) return null;
|
|
|
|
|
let clueWord = matchingTopWords.shift().word;
|
|
|
|
|
if (clues.some(clue => clue.answer == clueWord)) continue;
|
|
|
|
|
let allMatchingWords = new Set(matchingTopWords.map(topWord => topWord.word));
|
|
|
|
|
// Sort topWords by length descending.
|
|
|
|
|
matchingTopWords = [...new Set(matchingTopWords.map(topWord => topWord.word.length))].sort((a,b) => b-a).flatMap(wordLen => matchingTopWords.filter(topWord => topWord.word.length == wordLen));
|
|
|
|
|
|
|
|
|
|
function possibleCluesForCrossword(crossword, clueWord) {
|
|
|
|
|
let clues = crossword.clues;
|
|
|
|
|
let letters = crossword.letters;
|
|
|
|
|
|
|
|
|
|
let possibleCluesForWord = [];
|
|
|
|
|
let maxIntersections = 0;
|
|
|
|
|
if (clues.some(clue => clue.answer == clueWord)) {
|
|
|
|
|
return {'maxIntersections': 0, 'options': []};
|
|
|
|
|
}
|
|
|
|
|
for (let attachIdx = 0; attachIdx < clueWord.length; attachIdx++) {
|
|
|
|
|
let attachLetter = clueWord[attachIdx];
|
|
|
|
|
clues.forEach(clue => {
|
|
|
|
@ -205,50 +256,63 @@ class Crossword {
|
|
|
|
|
true);
|
|
|
|
|
}
|
|
|
|
|
// TODO Allow two letter words?
|
|
|
|
|
if ([newClue.beforePosition, newClue.afterPosition].some(pos => (pos.row + ';' + pos.col) in letters)) {
|
|
|
|
|
if ([newClue.beforePosition, newClue.afterPosition].some(pos => letters.has(pos.row + ';' + pos.col))) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let adj = newClue.adjacentPositions;
|
|
|
|
|
adj.splice(attachIdx, 1);
|
|
|
|
|
if (adj.some(posList => posList.some(pos => (pos.row + ';' + pos.col) in letters))) {
|
|
|
|
|
if (adj.some(posList => posList.some(pos => letters.has(pos.row + ';' + pos.col)))) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let cluePos = newClue.letterPositions;
|
|
|
|
|
let lastHasLetter = false;
|
|
|
|
|
let invalid = false;
|
|
|
|
|
newClue.intersections = 0;
|
|
|
|
|
for (let i = 0; i < cluePos.length; i++) {
|
|
|
|
|
let pos = cluePos[i];
|
|
|
|
|
let key = pos.row + ';' + pos.col;
|
|
|
|
|
let thisHasLetter = key in letters;
|
|
|
|
|
let thisHasLetter = letters.has(key);
|
|
|
|
|
if (thisHasLetter) {
|
|
|
|
|
newClue.intersections++;
|
|
|
|
|
if (lastHasLetter
|
|
|
|
|
|| letters[key] != pos.letter) {
|
|
|
|
|
|| letters.get(key) != pos.letter) {
|
|
|
|
|
invalid = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
lastHasLetter = thisHasLetter;
|
|
|
|
|
}
|
|
|
|
|
if (!invalid) possibleCluesForWord.push(newClue);
|
|
|
|
|
if (!invalid) {
|
|
|
|
|
possibleCluesForWord.push(newClue);
|
|
|
|
|
maxIntersections = Math.max(maxIntersections,
|
|
|
|
|
newClue.intersections);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {'maxIntersections': maxIntersections,
|
|
|
|
|
'options': possibleCluesForWord};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let crossword = new CrosswordPuzzle()
|
|
|
|
|
.withClue(new Clue(word, 0, 0, getRandomBool()));
|
|
|
|
|
while (crossword.clues.length < settings.num_clues) {
|
|
|
|
|
if (matchingTopWords.length == 0) return null;
|
|
|
|
|
let clueWord = matchingTopWords.shift().word;
|
|
|
|
|
let possibleClues = possibleCluesForCrossword(crossword, clueWord);
|
|
|
|
|
let possibleCluesForWord = possibleClues.options;
|
|
|
|
|
if (possibleCluesForWord.length > 0) {
|
|
|
|
|
let maxIntersections = possibleClues.maxIntersections;
|
|
|
|
|
possibleCluesForWord = possibleCluesForWord
|
|
|
|
|
.filter(clue => clue.intersections == maxIntersections);
|
|
|
|
|
let newClue = possibleCluesForWord[getRandomInt(possibleCluesForWord.length)];
|
|
|
|
|
clues.push(newClue);
|
|
|
|
|
newClue.letterPositions.forEach(pos => {
|
|
|
|
|
letters[pos.row + ';' + pos.col] = pos.letter;
|
|
|
|
|
});
|
|
|
|
|
crossword = crossword.withClue(newClue);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let rowOffset = -Math.min(...clues.map(clue => clue.row));
|
|
|
|
|
let colOffset = -Math.min(...clues.map(clue => clue.col));
|
|
|
|
|
clues = clues
|
|
|
|
|
.map(clue => new Clue(clue.answer,
|
|
|
|
|
clue.row+rowOffset, clue.col+colOffset,
|
|
|
|
|
clue.horizontal));
|
|
|
|
|
return new Crossword(sortedLetters.join(''), clues,
|
|
|
|
|
settings.min_guess_length);
|
|
|
|
|
|
|
|
|
|
return new Crossword(sortedLetters.join(''), crossword.normalizedClues,
|
|
|
|
|
settings.min_guess_length, allMatchingWords);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isValidGuess(guess) {
|
|
|
|
|