433 lines
17 KiB
HTML
433 lines
17 KiB
HTML
<!doctype html>
|
|
<html>
|
|
<head>
|
|
<meta content="text/html;charset=utf-8" http-equiv="Content-Type">
|
|
<meta content="utf-8" http-equiv="encoding">
|
|
<title>Anagram Crossword</title>
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
|
|
<link rel="stylesheet" type="text/css" href="main.css" />
|
|
<script src="../flatmap.js"></script>
|
|
<script src="../random.js"></script>
|
|
<script src="../scramblestring.js"></script>
|
|
<script type="text/javascript" src="../wordlist/typo.js"></script>
|
|
<script src="../wordlist/wordlist.js"></script>
|
|
<script src="./crossword.js"></script>
|
|
<script src="../entryui.js"></script>
|
|
<script >
|
|
class CrosswordUI extends AnagramEntryUI {
|
|
submitWord(word) {
|
|
let guess;
|
|
if (word) {
|
|
guess = word;
|
|
} else {
|
|
let seenSpace = false;
|
|
for (let idx in this.letters) {
|
|
if (this.letters[idx] == '') seenSpace = true;
|
|
else if (seenSpace) return;
|
|
}
|
|
guess = this.letters.join('');
|
|
}
|
|
let results = this.game.makeGuess(guess);
|
|
if (results === null) return;
|
|
this.saveGuess(guess);
|
|
|
|
this.updateCrossword(results);
|
|
|
|
let resultDisplay = document.createElement('tr');
|
|
resultDisplay.classList.add('guess');
|
|
|
|
for (let i = 0; i < this.game.numLetters; i++) {
|
|
let letter = document.createElement('td');
|
|
letter.innerText = guess[i] || '';
|
|
resultDisplay.appendChild(letter);
|
|
}
|
|
let link = document.createElement('a');
|
|
link.classList.add('dictionary_link');
|
|
link.href = 'https://en.wiktionary.org/wiki/' + guess;
|
|
link.target = '_blank';
|
|
link.innerText = '📖';
|
|
let linkCell = document.createElement('td');
|
|
linkCell.classList.add('dictionary_link');
|
|
linkCell.appendChild(link);
|
|
resultDisplay.appendChild(linkCell);
|
|
|
|
let guesses = document.getElementById('guesses');
|
|
if (results.length > 0) {
|
|
resultDisplay.classList.add('solved');
|
|
document.getElementById('clues_solved').innerText++;
|
|
} else if (this.game.allTopWords.has(guess)) {
|
|
resultDisplay.classList.add('top_word');
|
|
document.getElementById('top_words_found').innerText++;
|
|
} else {
|
|
resultDisplay.classList.add('bonus');
|
|
document.getElementById('bonus_words_found').innerText++;
|
|
}
|
|
guesses.prepend(resultDisplay);
|
|
|
|
this.clearUnlocked();
|
|
|
|
if (this.game.won) {
|
|
this.won = true;
|
|
document.getElementById('endgame').style.display = '';
|
|
}
|
|
}
|
|
|
|
updateCrossword(clues) {
|
|
clues.forEach(clue => {
|
|
if (!clue.solved) return;
|
|
|
|
let positions = clue.letterPositions;
|
|
for (let i in positions) {
|
|
let pos = positions[i];
|
|
this.cells[pos.row][pos.col].innerText = clue.answer[i];
|
|
this.cells[pos.row][pos.col].classList.add('solved');
|
|
}
|
|
});
|
|
}
|
|
|
|
recolorAvailableLetters() {
|
|
let idx = this.focusedTextbox;
|
|
let seen = new Array();
|
|
document.querySelectorAll('#available_letters button').forEach(b => {
|
|
// clear class list
|
|
while (b.classList.length > 0) {
|
|
b.classList.remove(b.classList.item(0));
|
|
}
|
|
|
|
let letter = b.innerText;
|
|
if (letter == '\xA0') {
|
|
b.classList.add('space');
|
|
return;
|
|
}
|
|
|
|
if (!this.mayEnterLetterAt(letter, idx, seen)) b.classList.add('not_here');
|
|
seen.push(letter);
|
|
});
|
|
}
|
|
|
|
mayEnterLetterAt(letter, idx, seen) {
|
|
let availableLetters = this.game.availableLetters.slice();
|
|
if (seen !== undefined) {
|
|
seen.forEach(letter => {
|
|
let toRemove = availableLetters.indexOf(letter);
|
|
if (toRemove > -1) availableLetters.splice(toRemove, 1);
|
|
});
|
|
}
|
|
for (let i = 0; i < this.game.numLetters; i++) {
|
|
if (i == idx) continue;
|
|
let toRemove = availableLetters.indexOf(this.letters[i]);
|
|
if (toRemove > -1) availableLetters.splice(toRemove, 1);
|
|
}
|
|
return availableLetters.includes(letter);
|
|
}
|
|
|
|
mayEnterLetter(letter) {
|
|
return this.mayEnterLetterAt(letter, this.focusedTextbox);
|
|
}
|
|
|
|
get defaultSettings() {
|
|
let default_settings = {
|
|
'min_word_length': 4,
|
|
'max_word_length': 7,
|
|
'min_matches': 1,
|
|
'max_matches': 50000,
|
|
'num_clues': 10,
|
|
'min_guess_length': 3,
|
|
};
|
|
return default_settings;
|
|
}
|
|
get settingsKey() {
|
|
return 'anagram-crossword-settings';
|
|
}
|
|
|
|
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() &&
|
|
Number(min_word_length.value) > Number(max_word_length.value)) {
|
|
max_word_length.value = min_word_length.value;
|
|
}
|
|
ui.saveSettings();
|
|
});
|
|
max_word_length.addEventListener('change', _ => {
|
|
if (max_word_length.checkValidity() &&
|
|
Number(min_word_length.value) > Number(max_word_length.value)) {
|
|
min_word_length.value = max_word_length.value;
|
|
}
|
|
ui.saveSettings();
|
|
});
|
|
min_matches.addEventListener('change', _ => {
|
|
if (min_matches.checkValidity() &&
|
|
Number(min_matches.value) > Number(max_matches.value)) {
|
|
max_matches.value = min_matches.value;
|
|
}
|
|
ui.saveSettings();
|
|
});
|
|
max_matches.addEventListener('change', _ => {
|
|
if (max_matches.checkValidity() &&
|
|
Number(min_matches.value) > Number(max_matches.value)) {
|
|
min_matches.value = max_matches.value;
|
|
}
|
|
ui.saveSettings();
|
|
});
|
|
num_clues.addEventListener('change', _ => {
|
|
ui.saveSettings();
|
|
});
|
|
min_guess_length.addEventListener('change', _ => {
|
|
ui.saveSettings();
|
|
});
|
|
|
|
this.nextGame(game);
|
|
}
|
|
|
|
get gameClass() {
|
|
return Crossword;
|
|
}
|
|
|
|
initialize(game) {
|
|
this.won = false;
|
|
this.game = game;
|
|
let ui = this;
|
|
|
|
scrambleString(game.fragment, ['', 'A', 'B']).then(fragment => {
|
|
document.getElementById('permalink').href = '#' + fragment;
|
|
document.getElementById('permalink_input').value
|
|
= window.location.href.split('#')[0] + '#' + fragment;
|
|
});
|
|
|
|
this.initializeInputs(game.numLetters);
|
|
|
|
clearElement(document.getElementById('guesses'));
|
|
|
|
let crossword = document.getElementById('crossword');
|
|
clearElement(crossword);
|
|
this.cells = new Array(game.numRows);
|
|
for (let row = 0; row < game.numRows; row++) {
|
|
let rowEl = document.createElement('tr');
|
|
this.cells[row] = new Array(game.numCols);
|
|
for (let col = 0; col < game.numCols; col++) {
|
|
let colEl = document.createElement('td');
|
|
colEl.classList.add('empty');
|
|
colEl.id = 'crossword-' + row + '-' + col;
|
|
colEl.addEventListener('click', e => {
|
|
let classList = e.target.classList;
|
|
if (classList.contains('empty')) return;
|
|
let clues = [...classList]
|
|
.filter(cls => cls.startsWith('clue'))
|
|
.map(cls => game.clues[Number(cls.substring(4))])
|
|
.filter(clue => !clue.solved);
|
|
if (clues.length == 0) return;
|
|
let clue = clues[0];
|
|
for (let i = clue.answer.length; i < game.numLetters; i++) {
|
|
ui.letters[i] = '';
|
|
ui.textboxes[i].innerText = '\xA0';
|
|
ui.locks[i].checked = true;
|
|
}
|
|
for (let i in clue.letterPositions) {
|
|
let pos = clue.letterPositions[i];
|
|
let letter = ui.cells[pos.row][pos.col].innerText;
|
|
if (letter == '') {
|
|
ui.letters[i] = '';
|
|
ui.textboxes[i].innerText = '\xA0';
|
|
ui.locks[i].checked = false;
|
|
} else {
|
|
ui.letters[i] = letter
|
|
ui.textboxes[i].innerText = letter;
|
|
ui.locks[i].checked = true;
|
|
}
|
|
ui.setFocusedTextboxIndex(0);
|
|
}
|
|
});
|
|
this.cells[row][col] = colEl;
|
|
rowEl.appendChild(colEl);
|
|
}
|
|
crossword.appendChild(rowEl);
|
|
}
|
|
|
|
let clueIdx = 0;
|
|
for (let clueIdx in game.clues) {
|
|
let clue = game.clues[clueIdx];
|
|
if (clue.horizontal) {
|
|
for (let col = clue.col; col < clue.afterCol; col++) {
|
|
this.cells[clue.row][col].classList.add('blank');
|
|
this.cells[clue.row][col].classList.add('clue' + clueIdx);
|
|
this.cells[clue.row][col].classList.remove('empty');
|
|
}
|
|
} else {
|
|
for (let row = clue.row; row < clue.afterRow; row++) {
|
|
this.cells[row][clue.col].classList.add('blank');
|
|
this.cells[row][clue.col].classList.add('clue' + clueIdx);
|
|
this.cells[row][clue.col].classList.remove('empty');
|
|
}
|
|
}
|
|
}
|
|
|
|
document.getElementById('clues_solved').innerText = 0;
|
|
document.getElementById('top_words_found').innerText = 0;
|
|
document.getElementById('bonus_words_found').innerText = 0;
|
|
document.getElementById('clues_total').innerText = game.clues.length;
|
|
document.getElementById('top_words_total').innerText = game.allTopWords.size - game.clues.length;
|
|
|
|
this.doPostInitialize();
|
|
}
|
|
}
|
|
|
|
function loaded() {
|
|
dictionaryPromise.then(_ =>
|
|
topWordsPromise
|
|
.then(_ => {
|
|
let fragment = window.location.hash.substring(1);
|
|
return unscrambleString(fragment).then(unscrambled => {
|
|
history.pushState("", document.title, window.location.pathname + window.location.search);
|
|
return Crossword.fromFragment(unscrambled);
|
|
});
|
|
})
|
|
.then(game => new CrosswordUI(game)));
|
|
}
|
|
</script>
|
|
<template id="letter_entry">
|
|
<td class="letter_entry">
|
|
<input type="checkbox" class="lock" />
|
|
<div class="lock">
|
|
<label class="lock_label" />
|
|
</div>
|
|
<div class="letter">
|
|
<span class="letter"> </span>
|
|
</div>
|
|
<div class="focus_indicator"></div>
|
|
</td>
|
|
</template>
|
|
</head>
|
|
<body onload="loaded();">
|
|
<label id="settings_toggle_label" for="settings_toggle">⚙</label>
|
|
<input type="checkbox" id="settings_toggle"></input>
|
|
<div id="settings">
|
|
<h1>Settings</h1>
|
|
<label>
|
|
<input type="checkbox" id="auto_submit" checked />
|
|
Auto-Submit (otherwise submit word only by clicking
|
|
the "Submit Word" button)
|
|
</label><br />
|
|
<a href="#" target="_blank" id="permalink">Permalink</a>
|
|
to current puzzle:
|
|
<input contenteditable id="permalink_input" />
|
|
<button id="copy_permalink">Copy</button><br />
|
|
<select id="available_games"></select>
|
|
<button id="switch_game">Switch Game</button>
|
|
<h2>Difficulty</h2>
|
|
<form id="difficulty" onsubmit="return false;">
|
|
Word length:
|
|
<input type="number" name="min_word_length" placeholder="min"
|
|
step="1" min="2" max="19" required
|
|
/>-<input type="number" name="max_word_length" placeholder="max"
|
|
step="1" min="2" max="19" required
|
|
/><br />
|
|
# of matches in top 50k words:
|
|
<input type="number" name="min_matches" placeholder="min"
|
|
step="1" min="1" max="50000" required
|
|
/>-<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>
|
|
<h2>Gameplay</h2>
|
|
<p>
|
|
Fill out the crossword puzzle using only the available letters
|
|
as clues.
|
|
</p>
|
|
<p>
|
|
The available letters are the anagram of a secret word which is
|
|
in the crossword. The other words in the crossword use some or
|
|
all of those letters. If a letter is only listed once, then you
|
|
can only use it once in your guesses.
|
|
</p>
|
|
<p>
|
|
You win by guessing all of the <span class="solved">clues</span>
|
|
in the crossword. If you guess a word that is not in the
|
|
crossword, it will be listed in your guesses and categorized
|
|
as either a <span class="top_word">common word</span> (in the
|
|
50,000 most common English words according to the word list) or
|
|
a <span class="bonus">bonus word</span> (any other correctly
|
|
spelled English word). After winning, you can continue trying
|
|
to guess more common/bonus words or click the "Next Game" button
|
|
to move onto the next crossword.
|
|
</p>
|
|
|
|
<h2>Interface</h2>
|
|
<p>
|
|
You can enter letters in any order; the current letter being edited
|
|
is highlighted with a yellow border. Clicking/tapping on the position
|
|
for another letter will select it. In order to aid in entering
|
|
letters out of order, letters may be frozen by clicking the
|
|
sun (🌞) above the entry box which will toggle it to a
|
|
snowflake (❄) to indicate that letter is frozen.
|
|
</p>
|
|
<p>
|
|
Words that do not use all of the words can be written simply by
|
|
leaving spaces blank. Note that empty spaces can be frozen to make
|
|
this easier.
|
|
</p>
|
|
<p>
|
|
All of your guesses are listed at the bottom, most recent at the top.
|
|
</p>
|
|
|
|
<h2>Credits</h2>
|
|
<ul>
|
|
<li>Game design cloned from iOS game <a href="https://itunes.apple.com/us/app/wordscapes/id1207472156">Wordscapes</a></li>
|
|
<li>Common words list from <a href="https://github.com/hermitdave/FrequencyWords">FrequencyWords</a> (see its credits and license at that link)</li>
|
|
<li>Valid word checks (spellchecking) using <a href="https://github.com/cfinke/Typo.js">Typo.js</a> (see its credits and license at that link)</li>
|
|
<li>Programming by <a href="https://aweirdimagination.net/~perelman/">Daniel Perelman</a>, licensed <a href="https://www.gnu.org/licenses/agpl-3.0.en.html">AGPLv3+</a></li>
|
|
<ul>
|
|
<li><a href="https://git.aweirdimagination.net/perelman/anagram-games/src/branch/master">Source code git repository</a></li>
|
|
<li>See also: <a href="https://aweirdimagination.net/tag/anagram-crossword/">blog posts about this game</a></li>
|
|
</ul>
|
|
</ul>
|
|
|
|
</div>
|
|
<table id="crossword">
|
|
</table>
|
|
<table id="guesses_and_entry">
|
|
<tr id="letters_entry"></tr>
|
|
</table>
|
|
<div id="submit_buttons">
|
|
<button id="clear">Clear All</button>
|
|
<button id="clear_nonlocked">Clear Non-Frozen</button>
|
|
<button id="submit">Submit Word</button>
|
|
</div>
|
|
<div id="endgame">
|
|
<button id="newgame">Next Game</button>
|
|
</div>
|
|
<div id="available_letters_display">
|
|
<button id="shuffle"></button>
|
|
<div id="available_letters"></div>
|
|
</div>
|
|
<div id="guess_list">
|
|
<div id="guess_counts">
|
|
<span class="solved">Clues: <span id="clues_solved">0</span>/<span id="clues_total">?</span></span>,
|
|
<span class="top_word">Other common words: <span id="top_words_found">0</span>/<span id="top_words_total">?</span></span>,
|
|
<span class="bonus">Bonus words: <span id="bonus_words_found">0</span></span>
|
|
</div>
|
|
<table id="guesses">
|
|
</table>
|
|
</div>
|
|
</body>
|
|
</html>
|