diff --git a/www/crossword/crossword.js b/www/crossword/crossword.js
index 6a6fd60..55109c0 100644
--- a/www/crossword/crossword.js
+++ b/www/crossword/crossword.js
@@ -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) {
diff --git a/www/crossword/index.html b/www/crossword/index.html
index b0c1110..cfa2d6e 100644
--- a/www/crossword/index.html
+++ b/www/crossword/index.html
@@ -88,6 +88,8 @@
'max_word_length': 7,
'min_matches': 1,
'max_matches': 50000,
+ 'num_clues': 10,
+ 'min_guess_length': 3,
};
return default_settings;
}
@@ -98,11 +100,15 @@
constructor(game) {
super();
+ let ui = this;
+
let difficulty = document.forms['difficulty'];
let min_word_length = difficulty.elements['min_word_length'];
let max_word_length = difficulty.elements['max_word_length'];
let min_matches = difficulty.elements['min_matches'];
let max_matches = difficulty.elements['max_matches'];
+ let num_clues = difficulty.elements['num_clues'];
+ let min_guess_length = difficulty.elements['min_guess_length'];
min_word_length.addEventListener('change', _ => {
if (min_word_length.checkValidity() &&
@@ -132,6 +138,12 @@
}
ui.saveSettings();
});
+ num_clues.addEventListener('change', _ => {
+ ui.saveSettings();
+ });
+ min_guess_length.addEventListener('change', _ => {
+ ui.saveSettings();
+ });
if (game === null) {
this.newGame();
@@ -234,6 +246,12 @@
/>-
+ # of clues:
+
+ Minimum guess length:
+