201 lines
5.9 KiB
JavaScript
201 lines
5.9 KiB
JavaScript
const readline = require("readline");
|
|
const rl = readline.createInterface({
|
|
input: process.stdin,
|
|
output: process.stdout
|
|
});
|
|
const crypto = require("crypto");
|
|
|
|
const dictionaryUrl = "http://wiki.puzzlers.org/pub/wordlists/unixdict.txt";
|
|
const wordRegexPattern = /^[a-z]{3,}$/m;
|
|
let dictionary = undefined;
|
|
|
|
function askQuestion(query) {
|
|
return new Promise(resolve => rl.question(query, resolve));
|
|
}
|
|
|
|
function askInt(query) {
|
|
return new Promise(resolve => rl.question(query, answer => resolve(parseInt(answer, 10))));
|
|
}
|
|
|
|
async function loadWebDictionary(url) {
|
|
try {
|
|
const response = await fetch(url);
|
|
if (!response.ok)
|
|
throw new Error(`HTTP ERROR -- ${response.status}`);
|
|
|
|
const rawText = await response.text();
|
|
const wordList = rawText.split("\n")
|
|
.map(str => {
|
|
const match = str.match(wordRegexPattern);
|
|
return match ? match[0] : null;
|
|
})
|
|
.filter(word => word !== null);
|
|
return wordList;
|
|
} catch (err) {
|
|
console.error("Failed to load dictionary", err.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function zip(arr0, arr1) {
|
|
return arr0.map((el, idx) => [el, arr1[idx]]);
|
|
}
|
|
|
|
function* cycle(arr) {
|
|
let index = 0;
|
|
while (true) {
|
|
yield arr[index];
|
|
index = (index + 1) % arr.length;
|
|
}
|
|
}
|
|
|
|
function counter(arr) {
|
|
const countMap = {};
|
|
for (const item of arr)
|
|
countMap[item] = (countMap[item] || 0) + 1;
|
|
return countMap;
|
|
}
|
|
|
|
function subtractCounters(c0, c1) {
|
|
const result = {...c0};
|
|
|
|
for (const [key, count] of Object.entries(c1))
|
|
if (result[key]) {
|
|
result[key] -= count;
|
|
if (result[key] <= 0) {
|
|
delete result[key];
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
function subtractSets(s0, s1) {
|
|
const result = new Set();
|
|
for (const item of s0)
|
|
if (!s1.has(item))
|
|
result.add(item);
|
|
return result;
|
|
}
|
|
|
|
function isWordiff(wordiffs, word, dict) {
|
|
const current = wordiffs[wordiffs.length-1];
|
|
|
|
if (!dict.includes(word))
|
|
return false;
|
|
if (wordiffs.includes(word))
|
|
return false;
|
|
|
|
if (word.length < current.length)
|
|
return isWordiffRemoval(word, current);
|
|
else if (word.length > current.length)
|
|
return isWordiffInsertion(word, current);
|
|
return isWordiffChange(word, current);
|
|
}
|
|
|
|
function isWordiffRemoval(word, previous) {
|
|
const possible = new Set();
|
|
for (let i = 0; i < previous.length; i++)
|
|
possible.add(previous.slice(0, i)+previous.slice(i+1));
|
|
return possible.has(word);
|
|
}
|
|
|
|
function isWordiffInsertion(word, previous) {
|
|
const diff = subtractCounters(counter(word), counter(previous));
|
|
const diffCount = Object.values(diff).reduce((a, v) => a + v, 0);
|
|
if (diffCount !== 1)
|
|
return false;
|
|
|
|
const insert = Object.keys(diff)[0];
|
|
const possible = new Set();
|
|
for (let i = 0; i < previous.length + 1; i++)
|
|
possible.add(previous.slice(0, i)+insert+previous.slice(i));
|
|
return possible.has(word);
|
|
}
|
|
|
|
function isWordiffChange(word, previous) {
|
|
const diffCount = zip(word.split(""), previous.split(""))
|
|
.map(([a, b]) => a !== b ? 1 : 0)
|
|
.reduce((a, v) => a + v, 0);
|
|
return diffCount === 1;
|
|
}
|
|
|
|
function couldHaveEntered(wordiffs, dict, limit=10) {
|
|
const currentSet = new Set(wordiffs);
|
|
const dictSet = new Set(dict);
|
|
const remaining = subtractSets(dictSet, currentSet);
|
|
const currentLength = wordiffs[wordiffs.length-1].length;
|
|
const possibleLengths = [currentLength-1, currentLength, currentLength+1];
|
|
const filteredRemaining = Array.from(remaining).filter(word => possibleLengths.includes(word.length));
|
|
const suggestions = [];
|
|
for (const candidate of filteredRemaining)
|
|
if (isWordiff(wordiffs, candidate, dict)) {
|
|
suggestions.push(candidate);
|
|
if (suggestions.length >= limit)
|
|
break;
|
|
}
|
|
return suggestions;
|
|
}
|
|
|
|
async function main() {
|
|
dictionary = await loadWebDictionary(dictionaryUrl);
|
|
console.log(`Loaded ${dictionary.length} words into dictionary.`);
|
|
if (dictionary.length === 0) {
|
|
rl.close();
|
|
return;
|
|
}
|
|
|
|
const dict34 = dictionary.filter(word => word.length === 3 || word.length === 4);
|
|
if (dict34.length === 0) {
|
|
console.error("ERROR -- No valid 3 or 4 letter words found in the dictionary.");
|
|
rl.close();
|
|
return;
|
|
}
|
|
|
|
const startIndex = crypto.randomInt(dict34.length);
|
|
let wordiffs = [dict34[startIndex]];
|
|
|
|
const numberOfPlayers = await askInt("How many players (maximum 8)? ");
|
|
if (numberOfPlayers > 8 || numberOfPlayers < 1) {
|
|
console.error(`ERROR -- Must have between 1 and 8 players. Got ${numberOfPlayers}.`);
|
|
rl.close();
|
|
return;
|
|
}
|
|
|
|
const players = [];
|
|
for (let i = 0; i < numberOfPlayers; i++) {
|
|
const playerName = (await askQuestion(` (${i+1}) Enter name: `)).trim();
|
|
players.push(playerName || `Player${i+1}`);
|
|
}
|
|
|
|
const playersCycle = cycle(players);
|
|
|
|
while (true) {
|
|
let currentPlayer = playersCycle.next().value;
|
|
|
|
console.log(`=== ${currentPlayer}'s turn! ===`);
|
|
const currentWord = wordiffs[wordiffs.length-1];
|
|
|
|
const word = (await askQuestion(
|
|
`Current word is "${wordiffs.at(-1)}"\n`+
|
|
"Enter your word: "
|
|
)).trim().toLowerCase();
|
|
|
|
if (isWordiff(wordiffs, word, dictionary)) {
|
|
wordiffs.push(word);
|
|
console.log(`Accepted! New chain: ${wordiffs.join(" -> ")}`);
|
|
} else {
|
|
const alternatives = couldHaveEntered(wordiffs, dictionary);
|
|
console.log(
|
|
`Game over, ${currentPlayer}!\n`+
|
|
`You could have entered one of these words (first ${alternatives.length} suggestions): ${alternatives.join(", ")}`
|
|
);
|
|
break;
|
|
}
|
|
}
|
|
|
|
rl.close();
|
|
}
|
|
|
|
main();
|