|
|
|
@ -46,9 +46,52 @@ class Clue {
|
|
|
|
|
return (this.horizontal ? 'H' : 'V') + this.row + ',' + this.col
|
|
|
|
|
+ ',' + this.length + ',' + permutation;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get letterPositions() {
|
|
|
|
|
let res = [];
|
|
|
|
|
if (this.horizontal) {
|
|
|
|
|
for (let i = 0; i < this.length; i++) {
|
|
|
|
|
res.push({'letter': this.answer[i],
|
|
|
|
|
'row': this.row, 'col': this.col + i});
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for (let i = 0; i < this.length; i++) {
|
|
|
|
|
res.push({'letter': this.answer[i],
|
|
|
|
|
'row': this.row + i, 'col': this.col});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return res;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get beforePosition() {
|
|
|
|
|
if (this.horizontal) {
|
|
|
|
|
return {'row': this.row, 'col': this.col-1};
|
|
|
|
|
} else {
|
|
|
|
|
return {'row': this.row-1, 'col': this.col};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get afterPosition() {
|
|
|
|
|
return {'row': this.afterRow, 'col': this.afterCol};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
get adjacentPositions() {
|
|
|
|
|
if (this.horizontal) {
|
|
|
|
|
return this.letterPositions.map(pos => [
|
|
|
|
|
{'row': pos.row-1, 'col': pos.col},
|
|
|
|
|
{'row': pos.row+1, 'col': pos.col},
|
|
|
|
|
]);
|
|
|
|
|
} else {
|
|
|
|
|
return this.letterPositions.map(pos => [
|
|
|
|
|
{'row': pos.row, 'col': pos.col-1},
|
|
|
|
|
{'row': pos.row, 'col': pos.col+1},
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
class Crossword {
|
|
|
|
|
constructor(letters, clues) {
|
|
|
|
|
constructor(letters, clues, minGuessLength) {
|
|
|
|
|
this.minGuessLength = minGuessLength;
|
|
|
|
|
this.clues = clues;
|
|
|
|
|
this.unansweredClues = this.clues;
|
|
|
|
|
this.guesses = new Set();
|
|
|
|
@ -64,11 +107,11 @@ class Crossword {
|
|
|
|
|
try {
|
|
|
|
|
if (!fragment.includes('|')) return null;
|
|
|
|
|
|
|
|
|
|
let [sortedLetters, clueFragments] = fragment.split('|');
|
|
|
|
|
let [minGuessLength, sortedLetters, clueFragments] = fragment.split('|');
|
|
|
|
|
let clues = clueFragments
|
|
|
|
|
.split(';')
|
|
|
|
|
.map(f => Clue.fromFragment(f, sortedLetters));
|
|
|
|
|
return new Crossword(sortedLetters, clues);
|
|
|
|
|
return new Crossword(sortedLetters, clues, Number(minGuessLength));
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
@ -76,11 +119,22 @@ class Crossword {
|
|
|
|
|
|
|
|
|
|
getFragment() {
|
|
|
|
|
let sortedLetters = this.availableLetters.sort();
|
|
|
|
|
return sortedLetters.join('') + '|'
|
|
|
|
|
return this.minGuessLength + '|' + sortedLetters.join('') + '|'
|
|
|
|
|
+ this.clues.map(clue => clue.toFragment(sortedLetters)).join(';');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static generateRandom(settings) {
|
|
|
|
|
static isMatchingWord(word, sortedLetters) {
|
|
|
|
|
let sortedWord = word.split('').sort();
|
|
|
|
|
let i = 0;
|
|
|
|
|
for (let idx in sortedWord) {
|
|
|
|
|
while (sortedWord[idx] != sortedLetters[i++]) {
|
|
|
|
|
if (i >= sortedLetters.length) return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static generateDefault() {
|
|
|
|
|
return new Crossword('there', [
|
|
|
|
|
new Clue('there', 2, 0, true),
|
|
|
|
|
new Clue('the', 0, 2, false),
|
|
|
|
@ -88,8 +142,100 @@ class Crossword {
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static generateRandom(settings) {
|
|
|
|
|
if (settings === null) return null;
|
|
|
|
|
|
|
|
|
|
let maxIters = 100;
|
|
|
|
|
let iters = 0;
|
|
|
|
|
|
|
|
|
|
let regExp = new RegExp('^[a-z]{' + settings.min_word_length + ','
|
|
|
|
|
+ settings.max_word_length + '}$');
|
|
|
|
|
let word;
|
|
|
|
|
let sortedLetters;
|
|
|
|
|
let matchingTopWords;
|
|
|
|
|
do {
|
|
|
|
|
do {
|
|
|
|
|
word = topWords[1000+getRandomInt(30000)].word;
|
|
|
|
|
if (iters++ > maxIters) return null;
|
|
|
|
|
} while (!regExp.test(word) || !isValidWord(word));
|
|
|
|
|
sortedLetters = word.split('').sort();
|
|
|
|
|
let preciseRegExp = new RegExp('^['
|
|
|
|
|
+ [...new Set(sortedLetters)].join('')
|
|
|
|
|
+ ']{' + settings.min_guess_length + ',}$');
|
|
|
|
|
matchingTopWords =
|
|
|
|
|
topWords
|
|
|
|
|
.filter(topWord => preciseRegExp.test(topWord.word)
|
|
|
|
|
&& Crossword.isMatchingWord(topWord.word,
|
|
|
|
|
sortedLetters)
|
|
|
|
|
&& isValidWord(topWord.word));
|
|
|
|
|
} while(matchingTopWords.length < settings.min_matches
|
|
|
|
|
|| matchingTopWords.length > settings.max_matches);
|
|
|
|
|
|
|
|
|
|
matchingTopWords = shuffle(matchingTopWords);
|
|
|
|
|
let clues = [new Clue(word, 0, 0, getRandomBool())];
|
|
|
|
|
let letters = {};
|
|
|
|
|
clues[0].letterPositions.forEach(pos => {
|
|
|
|
|
letters[pos.row + ';' + pos.col] = pos.letter;
|
|
|
|
|
});
|
|
|
|
|
for (let i = 0; i < settings.num_clues; i++) {
|
|
|
|
|
let clueAdded = false;
|
|
|
|
|
do {
|
|
|
|
|
if (matchingTopWords.length == 0) return null;
|
|
|
|
|
let clueWord = matchingTopWords.pop().word;
|
|
|
|
|
let possibleCluesForWord = [];
|
|
|
|
|
for (let attachIdx = 0; attachIdx < clueWord.length; attachIdx++) {
|
|
|
|
|
let attachLetter = clueWord[attachIdx];
|
|
|
|
|
clues.forEach(clue => {
|
|
|
|
|
for (let clueIdx in clue.answer) {
|
|
|
|
|
clueIdx = Number(clueIdx);
|
|
|
|
|
if (clue.answer[clueIdx] != attachLetter) continue;
|
|
|
|
|
let newClue;
|
|
|
|
|
if (clue.horizontal) {
|
|
|
|
|
newClue = new Clue(clueWord,
|
|
|
|
|
clue.row-attachIdx,
|
|
|
|
|
clue.col+clueIdx,
|
|
|
|
|
false);
|
|
|
|
|
} else {
|
|
|
|
|
newClue = new Clue(clueWord,
|
|
|
|
|
clue.row+clueIdx,
|
|
|
|
|
clue.col-attachIdx,
|
|
|
|
|
true);
|
|
|
|
|
}
|
|
|
|
|
// TODO Allow two letter words?
|
|
|
|
|
if ([newClue.beforePosition, newClue.afterPosition].some(pos => (pos.row + ';' + pos.col) in letters)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
let adj = newClue.adjacentPositions;
|
|
|
|
|
adj.splice(attachIdx, 1);
|
|
|
|
|
if (adj.some(posList => posList.some(pos => (pos.row + ';' + pos.col) in letters))) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
possibleCluesForWord.push(newClue);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if (possibleCluesForWord.length > 0) {
|
|
|
|
|
let newClue = possibleCluesForWord[getRandomInt(possibleCluesForWord.length)];
|
|
|
|
|
clues.push(newClue);
|
|
|
|
|
newClue.letterPositions.forEach(pos => {
|
|
|
|
|
letters[pos.row + ';' + pos.col] = pos.letter;
|
|
|
|
|
});
|
|
|
|
|
clueAdded = true;
|
|
|
|
|
}
|
|
|
|
|
} while (!clueAdded);
|
|
|
|
|
}
|
|
|
|
|
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.minGuessLength);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
isValidGuess(guess) {
|
|
|
|
|
if (guess.length < 3) return false;
|
|
|
|
|
if (guess.length < this.minGuessLength) return false;
|
|
|
|
|
|
|
|
|
|
let availableLetters = this.availableLetters.slice();
|
|
|
|
|
for (let idx in guess) {
|
|
|
|
|