RosettaCodeData/Task/Wordiff/JavaScript/wordiff.js

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();