Compare commits

...

40 Commits

Author SHA1 Message Date
ThePendulum
59dd7c793a 1.30.8 2025-02-18 22:13:26 +01:00
ThePendulum
a9f733227c Added weekly points back-up, writing points to dedicated directory. 2025-02-18 22:13:24 +01:00
ThePendulum
19feb9a55f 1.30.7 2024-09-04 22:28:02 +02:00
ThePendulum
df3c682ff6 Separated wordle and hardle score. 2024-09-04 22:28:00 +02:00
ThePendulum
aca9a4b597 1.30.6 2024-09-04 22:26:16 +02:00
ThePendulum
e4055ad99c Another fix for missing definition breaking mash. 2024-09-04 22:26:14 +02:00
ThePendulum
5dcb928c35 1.30.5 2024-08-24 18:01:10 +02:00
ThePendulum
8c7995340e Fixed missing definition breaking mash. 2024-08-24 18:01:08 +02:00
ThePendulum
84025d6a8b 1.30.4 2024-08-15 22:32:02 +02:00
ThePendulum
84b158cf21 Fixed crash when target is missing from message. 2024-08-15 22:32:00 +02:00
ThePendulum
7531a69904 1.30.3 2024-08-14 01:37:10 +02:00
ThePendulum
c402628161 Disable wordle bonus points for small dictionaries. 2024-08-14 01:37:08 +02:00
ThePendulum
e9df99bdcb 1.30.2 2024-07-05 00:40:55 +02:00
ThePendulum
a0f1914ce6 Amended wordle help. 2024-07-05 00:40:52 +02:00
ThePendulum
857f1816f0 1.30.1 2024-07-05 00:36:43 +02:00
ThePendulum
5ac935cc95 Fixed wordle hard hint letter background color. 2024-07-05 00:36:41 +02:00
ThePendulum
af9f15d11d 1.30.0 2024-07-05 00:30:27 +02:00
ThePendulum
7747eabce2 Regenerated dictionary. 2024-07-05 00:30:10 +02:00
ThePendulum
990e18d3da Added hard mode to wordle. 2024-07-05 00:29:40 +02:00
ThePendulum
234bbb2bbc Added word blacklist for lowercase names. Improved definition curation. 2024-07-04 22:55:54 +02:00
ThePendulum
f46d5f008b 1.29.9 2024-06-19 00:23:24 +02:00
ThePendulum
a86ef925ff Re-added morse and a few other 'name' words. Removed raw word list. 2024-06-19 00:23:21 +02:00
ThePendulum
ad26d2635c 1.29.8 2024-06-18 23:34:30 +02:00
ThePendulum
8d98581cd9 Fixed any mention of sponges being interpreted as a pong. 2024-06-18 23:34:27 +02:00
ThePendulum
c2cdd55f2d 1.29.7 2024-06-18 21:40:41 +02:00
ThePendulum
34b3269d54 Removed names from dictionary. 2024-06-18 21:40:38 +02:00
ThePendulum
5aa5b1852a 1.29.6 2024-06-10 01:24:26 +02:00
ThePendulum
c3cadc3169 Clarify when word is in dictionary without a definition. 2024-06-10 01:24:24 +02:00
ThePendulum
29cbd77e35 1.29.5 2024-06-09 23:37:19 +02:00
ThePendulum
2310352ad6 Improved dictionary processing. 2024-06-09 23:37:17 +02:00
ThePendulum
5cc3a428f3 1.29.4 2024-06-09 18:26:20 +02:00
ThePendulum
bbe573d8f3 Fixed missing word definition breaking Clive. Using em space in wordle letterboard for SChat. 2024-06-09 18:26:17 +02:00
ThePendulum
3196877c37 1.29.3 2024-06-09 18:07:55 +02:00
ThePendulum
ab25066936 Improved dictionary. 2024-06-09 18:07:52 +02:00
ThePendulum
f384d595e4 Using regular space to join wordle letterboard. 2024-06-05 22:45:04 +02:00
ThePendulum
4ada601fb2 1.29.2 2024-06-04 23:56:52 +02:00
ThePendulum
13e0bb9a8c Improved repeat letter marking in wordle. 2024-06-04 23:56:51 +02:00
ThePendulum
055440418e Added colorless hints to IRC wordle instructions. 2024-06-04 17:31:33 +02:00
ThePendulum
352efb9147 Using correct format in IRC wordle instructions. 2024-06-04 17:25:43 +02:00
ThePendulum
1212261b21 Changed symbol for 6> faced dice. 2024-06-04 01:50:51 +02:00
16 changed files with 270036 additions and 102299 deletions

View File

@@ -15,6 +15,6 @@
"no-console": 0,
"indent": ["error", "tab"],
"no-tabs": 0,
"max-len": [2, {"code": 400, "tabWidth": 4, "ignoreUrls": true}]
"max-len": 0
}
}

2
.gitignore vendored
View File

@@ -4,5 +4,5 @@ config/*
*.config.js
!ecosystem.config.js
*.sqlite
points*.json
points/*
assets/mash-words.json

View File

@@ -1,11 +1,11 @@
'use strict';
const fs = require('fs').promises;
/*
const inflect = require('inflect');
const tensify = require('tensify');
const dictionary = require('./dictionary.json');
function getTenses(word) {
try {
const { past, past_participle: participle } = tensify(word);
@@ -15,26 +15,40 @@ function getTenses(word) {
return {};
}
}
*/
const dictionary = require('./dictionary.json');
async function init() {
/*
const formsDictionary = Object.fromEntries(Object.entries(dictionary).flatMap(([word, definition]) => {
const plural = inflect.pluralize(word);
const { past, participle } = getTenses(word);
return [
[word, definition],
...(plural && !dictionary[plural] ? [[plural, `Plural of ${word}`]] : []),
...(past && !dictionary[past] ? [[past, `Past tense of ${word}`]] : []),
...(participle && !dictionary[participle] ? [[past, `Past participle of ${word}`]] : []),
...(plural && !dictionary[plural] ? [[plural, definition]] : []),
...(past && !dictionary[past] ? [[past, definition]] : []),
...(participle && !dictionary[participle] ? [[past, definition]] : []),
];
}));
*/
const validWords = Object.entries(formsDictionary).filter(([word]) => /^[a-zA-Z]+$/.test(word));
const validWords = Object.entries(dictionary).filter(([word]) => /^[a-zA-Z]+$/.test(word));
const sortedWords = validWords.reduce((acc, [rawWord, fullDefinition]) => {
const word = rawWord.toLowerCase();
const anagram = word.split('').sort().join('');
const definitions = fullDefinition?.split(/\d+\.\s+/).filter(Boolean).map((definition) => definition.split('.')[0].toLowerCase());
const definitions = fullDefinition
?.split(/\d+\.\s+/).filter(Boolean).map((definition) => {
const splitIndex = definition.indexOf('.', 16); // split after n characters to avoid splitting on e.g. abbreviated categories at the start of the definition: (Anat.)
if (splitIndex > -1) {
return definition.slice(0, splitIndex).trim().toLowerCase();
}
return definition.toLowerCase();
}) || [];
if (!acc[anagram.length]) {
acc[anagram.length] = {};

180184
assets/dictionary.json Executable file → Normal file

File diff suppressed because one or more lines are too long

102260
assets/dictionary_old.json Executable file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,31 @@
'use strict';
const fs = require('fs').promises;
const words = require('./words.json');
const wordsBlacklist = require('./words_blacklist.json');
const dictionary = require('./dictionary.json');
// mainly lowercase names
const blacklist = new Set(wordsBlacklist);
async function init() {
const notNameWords = words.filter((word) => word.charAt(0) === word.charAt(0).toLowerCase() && !blacklist.has(word)); // don't include names for places, products, people, etc.
const definitions = Object.fromEntries(notNameWords.map((word) => {
const normalizedWord = word.normalize('NFD').replace(/\p{Diacritic}/ug, '').toLowerCase().trim();
const definition = dictionary[normalizedWord];
const singular = normalizedWord.replace(/s$/, '');
const singularDefinition = dictionary[singular];
return [normalizedWord, definition || singularDefinition || null];
}));
const string = JSON.stringify(definitions, null, 4);
await fs.writeFile('./dictionary.json', string);
console.log(`Wrote ${Object.keys(definitions).length} words to ./dictionary.json`);
}
init();

89686
assets/words.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,4 @@
[
"ioctl",
"xterm"
]

View File

@@ -136,8 +136,9 @@ module.exports = {
},
wordle: {
minLength: 3,
defaultLength: 5,
highlightRepeat: false, // in wordle, if the I in the guess HINDI is in the wrong place, only the first I is orange
length: 5,
bonusDictionaryThreshold: 1000, // minimum dictionary size to assign bonus points, prevent people from scoring full bonus points from 1-word dictionaries
mode: 'easy',
},
numbers: {
length: 6,

4
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "schat2-clive",
"version": "1.29.1",
"version": "1.30.8",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "schat2-clive",
"version": "1.29.1",
"version": "1.30.8",
"license": "ISC",
"dependencies": {
"better-sqlite3": "^8.3.0",

View File

@@ -1,6 +1,6 @@
{
"name": "schat2-clive",
"version": "1.29.1",
"version": "1.30.8",
"description": "Game host for SChat 2-powered chat sites",
"main": "src/app.js",
"scripts": {

View File

@@ -37,7 +37,7 @@ function onCommand(args, context) {
return `${style.yellow(`(${coinFaces[result - 1]})`)} ${style.bold(result === 1 ? 'heads' : 'tails')}`; // eslint-disable-line no-irregular-whitespace
}
return `${style.grey(dieFaces[result - 1] || '')} ${style.bold(result)}`; // eslint-disable-line no-irregular-whitespace
return `${style.grey(dieFaces[result - 1] || '')} ${style.bold(result)}`; // eslint-disable-line no-irregular-whitespace
});
context.sendMessage(results.join(style.grey(' | ')), context.room.id, { label: type });

View File

@@ -35,7 +35,7 @@ function start(length, context, attempt = 0) {
if (answers.some((answer) => answer.word === anagram)) {
if (attempt >= 10) {
context.sendMessage(`Sorry, I did not find a mashable ${length}-letter word`);
context.sendMessage(`Sorry, I did not find a mashable ${length}-letter word`, context.room.id);
return;
}
@@ -81,7 +81,9 @@ function play(rawWord, context, shouted) {
}
if (answer) {
const definition = answer.definitions[0] ? `: ${style.italic(`${answer.definitions[0].slice(0, 100)}${mash.answers[0].definitions[0].length > 100 ? '...' : ''}`)}` : '';
const definition = answer.definitions[0]
? `: ${style.italic(`${answer.definitions[0].slice(0, 100)}${answer.definitions[0].length > 100 ? '...' : ''}`)}`
: '';
context.sendMessage(mash.answers.length === 1
? `${style.bold(style.yellow(word))} is the right answer${definition}, ${style.bold(style.cyan(`${config.usernamePrefix}${context.user.username}`))} now has ${style.bold(`${context.user.points + 1} ${context.user.points === 0 ? 'point' : 'points'}`)}! There were no other options for ${style.bold(mash.anagram)}.`
@@ -144,10 +146,19 @@ function define(word, context) {
return;
}
context.sendMessage(`No definition available for ${style.bold(word)}, ${config.usernamePrefix}${context.user.username}`, context.room.id);
if (answer) {
context.sendMessage(`${style.bold(word)} is in my dictionary, but I cannot provide a definition, ${config.usernamePrefix}${context.user.username}`, context.room.id);
return;
}
context.sendMessage(`${style.bold(word)} is not in my dictionary, ${config.usernamePrefix}${context.user.username}`, context.room.id);
}
function sanitizeDefinition(definition, answers) {
if (!definition) {
return 'No definition';
}
return definition.replaceAll(/\w+/g, (word) => {
if (answers.some((answer) => answer.word.includes(word))) {
return '*'.repeat(word.length);

View File

@@ -58,15 +58,15 @@ function getBoard(letters, showLetters, context) {
return config.platform === 'irc'
? letter
: style.grey(letter);
}).join('');
}).join(config.platform === 'irc' ? ' ' : ''); // regular space vs em space
return `${prefix}${middle}${suffix}Letters: ${letterBoard}`; // eslint-disable-line no-irregular-whitespace
return `${prefix}${middle}${suffix}${wordle.mode === 'hard' ? ' (Hard)' : ''}Letters: ${letterBoard}`; // eslint-disable-line no-irregular-whitespace
}
return `${prefix}${middle}${suffix}`;
}
function start(length = settings.defaultLength, context) {
function start(length = settings.length, mode = settings.mode, context) {
const wordPool = words[length];
if (length < settings.minLength) {
@@ -85,12 +85,17 @@ function start(length = settings.defaultLength, context) {
wordles.set(context.room.id, {
word: word.word.toUpperCase(),
wordList,
mode,
definitions: word.definitions,
letters: new Map(alphabet.map((letter) => [letter, null])),
guesses: [],
});
context.sendMessage(`${getBoard(Array.from({ length }, () => null), false, context)} guess the word with ${config.prefix}w [word]! ${style.grey(style.bold('X'))} not in word; ${style.orange(style.bold('X'))} wrong position; ${style.green(style.bold('X'))} correct position.`, context.room.id);
if (config.platform === 'irc') {
context.sendMessage(`${getBoard(Array.from({ length }, () => null), false, context)}${mode === 'hard' ? ' (Hard)' : ''} guess the word with ${config.prefix}w [word]! ${style.bgsilver('X')} not in word; ${style.bgyellow(`X${style.yellow('?')}`)} wrong position; ${style.bggreen(`X${style.green('*')}`)} correct position.`, context.room.id); // eslint-disable-line no-irregular-whitespace
} else {
context.sendMessage(`${getBoard(Array.from({ length }, () => null), false, context)}${mode === 'hard' ? ' (Hard)' : ''} guess the word with ${config.prefix}w [word]! ${style.grey(style.bold('X'))} not in word; ${style.orange(style.bold('X'))} wrong position; ${style.green(style.bold('X'))} correct position.`, context.room.id);
}
context.logger.info(`Wordle started: ${word.word.toUpperCase()}`);
}
@@ -114,39 +119,82 @@ function play(guess, context) {
}
const upperGuess = guess.toUpperCase();
const processed = new Set();
const occurrences = wordle.word.split('').reduce((acc, letter) => acc.set(letter, (acc.get(letter) || 0) + 1), new Map());
const check = upperGuess.split('').map((letter, index) => {
const alreadySeen = settings.highlightRepeat ? false : processed.has(letter);
const guessLetters = upperGuess.split('');
const check = guessLetters.map((letter) => [letter, null]);
const prevGuess = wordle.guesses.at(-1)?.[1];
processed.add(letter);
if (wordle.mode === 'hard' && prevGuess) {
const prevLetters = prevGuess.split('');
const valid = prevLetters.every((letter, index) => {
if (wordle.letters.get(letter) === false && !guessLetters.includes(letter)) {
context.sendMessage(`(Hard) Your guess must include the letter ${config.platform === 'irc'
? style.bgyellow(` ${letter} `)
: `${style.grey(style.code('['))}${style.orange(style.bold(style.code(letter)))}${style.grey(style.code(']'))}`
}.`, context.room.id);
return false;
}
if (wordle.letters.get(letter) === true && letter === wordle.word[index] && letter !== guessLetters[index]) {
context.sendMessage(`(Hard) Your guess must include the letter ${config.platform === 'irc'
? style.bggreen(` ${letter} `)
: `${style.grey(style.code('['))}${style.green(style.bold(style.code(letter)))}${style.grey(style.code(']'))}`
} in the correct position.`, context.room.id);
return false;
}
return true;
});
if (!valid) {
return;
}
}
// correct
guessLetters.forEach((letter, index) => {
if (wordle.word[index] === letter) {
wordle.letters.set(letter, true);
return [letter, true];
check[index] = [letter, true];
}
});
// wrong place
guessLetters.forEach((letter, index) => {
if (wordle.word.includes(letter)) {
if (wordle.letters.get(letter) !== true) {
wordle.letters.set(letter, false);
}
return [letter, alreadySeen ? null : false]; // repeating letter in the wrong place is not highlighted
const marks = check.filter(([checkLetter, status]) => checkLetter === letter && status !== null).length;
if (check[index][1] !== true && marks < occurrences.get(letter)) {
check[index] = [letter, false];
}
return;
}
wordle.letters.delete(letter);
return [letter, null];
});
wordle.guesses = wordle.guesses.concat([[context.user.username, upperGuess]]);
if (upperGuess === wordle.word) {
const points = Math.max((wordle.word.length + 1) - wordle.guesses.length, 1);
const assignBonusPoints = wordle.wordList.length > config.wordle.bonusDictionaryThreshold;
const points = assignBonusPoints
? Math.max((wordle.word.length + 1) - wordle.guesses.length, 1)
: 1;
const definition = wordle.definitions[0] ? `: ${style.italic(`${wordle.definitions[0].slice(0, 100)}${wordle.definitions[0].length > 100 ? '...' : ''}`)}` : '';
context.setPoints(context.user, points);
context.sendMessage(`${getBoard(check, false, context)} is correct in ${wordle.guesses.length} guesses! ${style.bold(style.cyan(`${config.usernamePrefix}${context.user.username}`))} gets ${points} ${points > 1 ? 'points' : 'point'}. ${style.bold(wordle.word)}${definition}`, context.room.id);
context.setPoints(context.user, points, { key: wordle.mode === 'hard' ? 'hardle' : 'wordle' });
context.sendMessage(`${getBoard(check, false, context)} is correct in ${wordle.guesses.length} guesses! ${style.bold(style.cyan(`${config.usernamePrefix}${context.user.username}`))} gets ${points} ${points > 1 ? 'points' : 'point'}${assignBonusPoints ? '. ' : ` (${wordle.word.length}-letter dictionary too small for bonus points). `}${style.bold(wordle.word)}${definition}`, context.room.id);
wordles.delete(context.room.id);
@@ -158,7 +206,10 @@ function play(guess, context) {
function onCommand(args, context) {
const wordle = wordles.get(context.room.id);
const length = Number(args[0]) || settings.defaultLength;
const length = args.map((arg) => Number(arg)).find(Boolean) || settings.length;
const mode = args.find((arg) => arg === 'easy' || arg === 'hard')
|| (context.command === 'hardle' && 'hard')
|| settings.mode;
if (['guess', 'w'].includes(context.command)) {
play(args[0], context);
@@ -178,7 +229,7 @@ function onCommand(args, context) {
}
if (!wordle && context.room.id) {
start(length, context);
start(length, mode, context);
return;
}
@@ -187,8 +238,8 @@ function onCommand(args, context) {
module.exports = {
name: 'Wordle',
commands: ['wordle', 'guess', 'w'],
help: `Guess the ${settings.defaultLength}-letter word on the board. Submit a guess with ${config.prefix}w [word]. If the letter is green, it is in the correct place. If it is yellow, it is part of the word, but not in the correct place. The number of letters in the word is the highest score you can get. You lose 1 point per guess, down to 1 point.`,
commands: ['wordle', 'hardle', 'lingo', 'guess', 'w'],
help: `Guess the ${settings.length}-letter word on the board. Submit a guess with ${config.prefix}w [word]. If the letter is green, it is in the correct place. If it is yellow, it is part of the word, but not in the correct place. The number of letters in the word is the highest score you can get. You lose 1 point per guess, down to 1 point. Change the numbers of letters or switch to hard mode with ~wordle [length] [hard] or ~hardle [length].`,
onCommand,
// onMessage,
};

View File

@@ -3,6 +3,7 @@
const config = require('config');
const fs = require('fs').promises;
const logger = require('simple-node-logger').createSimpleLogger();
const { getWeek } = require('date-fns');
const { argv } = require('yargs');
// const timers = require('timers/promises');
@@ -15,14 +16,16 @@ const points = {};
async function initPoints(identifier) {
try {
const pointsFile = await fs.readFile(`./points-${identifier}.json`, 'utf-8');
const pointsFile = await fs.readFile(`./points/points-${identifier}.json`, 'utf-8');
Object.assign(points, JSON.parse(pointsFile));
} catch (error) {
if (error.code === 'ENOENT') {
logger.info('Creating new points file');
await fs.writeFile(`./points-${identifier}.json`, '{}');
await fs.mkdir('./points', { recursive: true });
await fs.writeFile(`./points/points-${identifier}.json`, '{}');
await initPoints(identifier);
}
}
@@ -50,7 +53,8 @@ async function setPoints(identifier, defaultKey, user, value, { mode = 'add', ke
points[gameKey][userKey] = value;
}
await fs.writeFile(`./points-${identifier}.json`, JSON.stringify(points, null, 4));
await fs.writeFile(`./points/points-${identifier}_backup${getWeek(new Date())}.json`, JSON.stringify(points, null, 4)); // weekly back-up
await fs.writeFile(`./points/points-${identifier}.json`, JSON.stringify(points, null, 4));
}
function getPoints(game, rawUsername, { user, room, command }) {
@@ -117,6 +121,11 @@ async function getGames(bot, identifier) {
const sendMessage = (body, roomId, options, recipient) => {
const curatedBody = curateMessageBody(body, game, key, options);
if (!roomId && !recipient) {
logger.error(`Missing room ID or recipient for message: ${body}`);
return;
}
if (config.platform === 'irc') {
bot.client.say(/^#/.test(roomId) ? roomId : recipient, curatedBody);
}

View File

@@ -130,6 +130,8 @@ function handleError(error, socket, domain, data) {
}
}
const pongRegex = /^pong:\d+$/;
async function connect(bot, games) {
const socket = {
ws: { readyState: 0 },
@@ -154,7 +156,7 @@ async function connect(bot, games) {
socket.ws.on('message', async (msgData) => {
const msg = msgData.toString();
if (typeof msg === 'string' && msg.includes('pong')) {
if (typeof msg === 'string' && pongRegex.test(msg)) {
logger.debug(`Received pong ${msg.split(':')[1]}`);
return;
}