Browse Source

Support generating random crossword puzzles.

feature/save-game
Daniel Perelman 3 years ago
parent
commit
3701512640
3 changed files with 173 additions and 6 deletions
  1. +152
    -6
      www/crossword/crossword.js
  2. +18
    -0
      www/crossword/index.html
  3. +3
    -0
      www/random.js

+ 152
- 6
www/crossword/crossword.js View File

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


+ 18
- 0
www/crossword/index.html View File

@@ -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 @@
/>-<input type="number" name="max_matches" placeholder="max"
step="1" min="1" max="50000" required
/><br />
# of clues:
<input type="number" name="num_clues"
step="1" min="1" max="99" required /><br />
Minimum guess length:
<input type="number" name="min_guess_length"
step="1" min="3" max="24" required /><br />
</form>
<button id="newgame_button">Generate new game</button>
<h1>How to Play</h1>


+ 3
- 0
www/random.js View File

@@ -6,6 +6,9 @@
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
function getRandomBool() {
return getRandomInt(2) == 1;
}
function getRandomLetter() {
return (10 + getRandomInt(26)).toString(36);
}


Loading…
Cancel
Save