1
0
Fork 0

Cleanup/refactor crossword generation.

feature/save-game
Daniel Perelman 5 years ago
parent ff66d2cd8e
commit 18e72908c2

@ -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) {

Loading…
Cancel
Save