Compare commits

..

69 Commits

Author SHA1 Message Date
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
ThePendulum a807cbc2aa 1.29.1 2024-06-03 22:46:19 +02:00
ThePendulum f85cac7f2f Improved wordle readability for IRC. 2024-06-03 22:46:17 +02:00
ThePendulum 402dbc3923 Mapped IRC orange to yellow instead of brown. 2024-06-03 02:17:41 +02:00
ThePendulum 54968f3fb4 Mapped IRC orange to brown. 2024-06-03 02:16:36 +02:00
ThePendulum 1228928592 Added guess count to wordle finish. 2024-06-03 02:05:02 +02:00
ThePendulum 6a35049609 Fixed orange highlight overwriting green highlight in wordle game. 2024-06-03 01:57:44 +02:00
ThePendulum a29eba70f0 Added instructions to wordle start message. 2024-06-03 01:10:07 +02:00
ThePendulum 064b4eb0d4 1.29.0 2024-06-03 01:02:33 +02:00
ThePendulum 5e396a4abe Added/implemented wordle game. 2024-06-03 01:02:30 +02:00
ThePendulum 453a3b1b42 Fixed letters game board timeout. 2024-04-02 01:09:31 +02:00
ThePendulum a1a9d698e2 Showing board 5 instead of 4 times in letters game. 2024-04-02 01:04:58 +02:00
ThePendulum 48cf433a5c 1.28.5 2024-04-02 00:56:21 +02:00
ThePendulum 61e935b925 Showing board 4 instead of 3 times in letters game. 2024-04-02 00:56:19 +02:00
ThePendulum 9a7ef61d93 1.28.4 2024-03-08 00:49:44 +01:00
ThePendulum bad5dc52f7 Added user point merging utility. 2024-03-08 00:49:37 +01:00
ThePendulum d510caa123 1.28.3 2024-02-11 17:33:21 +01:00
ThePendulum b6cc3d716b Using cookie instead of http session so we can strip Domain attribute. 2024-02-11 17:33:19 +01:00
ThePendulum 6a36df3593 1.28.2 2024-01-13 03:55:55 +01:00
ThePendulum 69bc1c9e6e Added points merge utility. 2024-01-13 03:55:46 +01:00
ThePendulum aeb405967b 1.28.1 2024-01-13 02:26:05 +01:00
ThePendulum 73e60b81f1 Added ping. 2024-01-13 02:26:03 +01:00
ThePendulum c9b985f768 1.28.0 2024-01-13 01:29:14 +01:00
ThePendulum cb5de62e1c Reverted SocketIO to ws. 2024-01-13 01:29:10 +01:00
ThePendulum b3cab90810 1.27.3 2024-01-02 16:15:39 +01:00
ThePendulum 47484ba7e2 Preventing ~solve when numbers game is in progress. 2024-01-02 16:15:37 +01:00
ThePendulum 4017f1cd07 1.27.2 2024-01-02 02:13:14 +01:00
ThePendulum 03dfe8e437 Exposing numbers game solver through command. 2024-01-02 02:13:11 +01:00
ThePendulum 1a992a6026 1.27.1 2023-12-07 22:05:53 +01:00
ThePendulum 9f8f503f13 Removed reconnect logic, handled by SocketIO. 2023-12-07 22:05:51 +01:00
ThePendulum d460ba13c5 1.27.0 2023-12-05 20:55:47 +01:00
ThePendulum ed5337d828 Fixed socket close event listener. 2023-12-05 20:55:39 +01:00
20 changed files with 270374 additions and 102310 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
}
}

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] = {};

180182
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

@ -27,6 +27,7 @@ module.exports = {
'chat',
'mash',
'trivia',
'wordle',
'letters',
'numbers',
'hunt',
@ -133,6 +134,12 @@ module.exports = {
z: 1,
},
},
wordle: {
minLength: 3,
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,
timeout: 90,

4
package-lock.json generated
View File

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

View File

@ -1,6 +1,6 @@
{
"name": "schat2-clive",
"version": "1.26.12",
"version": "1.30.7",
"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

@ -91,13 +91,20 @@ async function play(context) {
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(settings.timeout))} seconds, let's start!`, context.room.id);
try {
await timers.setTimeout((settings.timeout / 3) * 1000, null, { signal: game.ac.signal });
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round((settings.timeout / 3) * 2)} seconds`))} left`, context.room.id);
await timers.setTimeout((settings.timeout / 5) * 1000, null, { signal: game.ac.signal });
await timers.setTimeout((settings.timeout / 3) * 1000, null, { signal: game.ac.signal });
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round(settings.timeout / 3)} seconds`))} left`, context.room.id);
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round((settings.timeout / 5) * 4)} seconds`))} left`, context.room.id);
await timers.setTimeout((settings.timeout / 5) * 1000, null, { signal: game.ac.signal });
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round((settings.timeout / 5) * 3)} seconds`))} left`, context.room.id);
await timers.setTimeout((settings.timeout / 5) * 1000, null, { signal: game.ac.signal });
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round((settings.timeout / 5) * 2)} seconds`))} left`, context.room.id);
await timers.setTimeout((settings.timeout / 5) * 1000, null, { signal: game.ac.signal });
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(`${Math.round(settings.timeout / 5)} seconds`))} left`, context.room.id);
await timers.setTimeout((settings.timeout / 5) * 1000, null, { signal: game.ac.signal });
await timers.setTimeout((settings.timeout / 3) * 1000, null, { signal: game.ac.signal });
stop(context);
} catch (error) {
// abort expected, probably not an error

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

@ -336,7 +336,7 @@ function start(context, numbers) {
context.sendMessage('Let\'s play the numbers! Would you like a large number or a small one?', context.room.id);
}
function solve(calculation, context) {
function calc(calculation, context) {
try {
const parsed = math.parse(calculation.replace(/`/g, '')); // backticks may be used to prevent the expression from being expanded by Markdown in SChat
const answer = parsed.evaluate();
@ -347,6 +347,51 @@ function solve(calculation, context) {
}
}
async function solve(equation, context) {
const [numberString, targetString] = equation.split('=');
const numbers = numberString?.split(' ').map((string) => Number(string)).filter(Boolean);
const target = Number(targetString);
if (!numberString || !target) {
context.sendMessage('The input should be in the form 1 2 3 4 5 6 7 = 10', context.room.id);
return;
}
if (numbers.length < 2 || numbers.length > 7) {
context.sendMessage('The selection should contain at least 2 and at most 7 numbers', context.room.id);
return;
}
if (target < 100 || target > 999) {
context.sendMessage('The target must be a number between 100 and 999', context.room.id);
return;
}
if (Array.from(games.entries()).some(([roomId, game]) => roomId === context.room.id || game.target === target)) {
context.sendMessage('Nice try! Please wait for this numbers round to end :)', context.room.id);
return;
}
try {
const solutions = await solveAll(numbers, target, 3);
const bestSolution = solutions.reduce((closest, solution) => (!closest || Math.abs(target - solution.answer) < Math.abs(target - closest.answer) ? solution : closest), null);
if (!bestSolution) {
context.sendMessage(`I could not find a solution for ${numbers.join(' ')} = ${target} :(`, context.room.id);
return;
}
if (bestSolution.answer === target) {
context.sendMessage(`My best solution for ${numbers.join(' ')} = ${target} is: ${style.bold(bestSolution.solution)}`, context.room.id);
return;
}
context.sendMessage(`I could not find an exact solution for ${numbers.join(' ')} = ${target}. My closest solution is: ${style.bold(bestSolution.solution)} = ${style.italic(bestSolution.answer)}`, context.room.id);
} catch (error) {
console.log(error);
}
}
function onCommand(args, context) {
if (context.subcommand === 'stop') {
stop(context, true);
@ -358,7 +403,12 @@ function onCommand(args, context) {
return;
}
if (['calculate', 'calc', 'solve'].includes(context.subcommand || context.command)) {
if (['calculate', 'calc'].includes(context.subcommand || context.command)) {
calc(args.join(' '), context); // two from the top, four from the bottom, please Rachel
return;
}
if (['solve'].includes(context.subcommand || context.command)) {
solve(args.join(' '), context); // two from the top, four from the bottom, please Rachel
return;
}

245
src/games/wordle.js Normal file
View File

@ -0,0 +1,245 @@
'use strict';
const config = require('config');
const crypto = require('crypto');
const style = require('../utils/style');
const words = require('../../assets/mash-words.json');
const settings = { ...config.wordle };
const wordles = new Map();
const alphabet = Array.from({ length: 26 }, (value, index) => String.fromCharCode(65 + index));
function getBoard(letters, showLetters, context) {
const wordle = wordles.get(context.room.id);
const prefix = config.platform === 'irc' ? '' : style.grey(style.code('['));
const middle = letters.map((letter) => {
if (letter === null) {
return config.platform === 'irc'
? style.bgsilver('?')
: style.code(''); // em space, U+2003, charcode 8195
}
if (letter[1] === true) {
return config.platform === 'irc'
? style.bggreen(` ${letter[0].toUpperCase()}${style.green('*')}`)
: style.green(style.bold(style.code(letter[0].toUpperCase())));
}
if (letter[1] === false) {
return config.platform === 'irc'
? style.bgyellow(` ${letter[0].toUpperCase()}${style.yellow('?')}`)
: style.orange(style.bold(style.code(letter[0].toUpperCase())));
}
return config.platform === 'irc'
? style.bgsilver(` ${letter[0].toUpperCase()} `)
: style.grey(style.bold(style.code(letter[0].toUpperCase())));
}).join(config.platform === 'irc' ? '' : style.grey(style.code('|')));
const suffix = config.platform === 'irc' ? '' : `${style.grey(style.code(']'))}`;
if (showLetters) {
const letterBoard = Array.from(wordle.letters).map(([letter, state]) => {
if (state === true) {
return config.platform === 'irc'
? style.bggreen(`${letter}${style.green('*')}`)
: style.green(style.bold(letter));
}
if (state === false) {
return config.platform === 'irc'
? style.bgyellow(`${letter}${style.yellow('?')}`)
: style.orange(style.bold(letter));
}
return config.platform === 'irc'
? letter
: style.grey(letter);
}).join(config.platform === 'irc' ? ' ' : ''); // regular space vs em space
return `${prefix}${middle}${suffix}${wordle.mode === 'hard' ? ' (Hard)' : ''}Letters: ${letterBoard}`; // eslint-disable-line no-irregular-whitespace
}
return `${prefix}${middle}${suffix}`;
}
function start(length = settings.length, mode = settings.mode, context) {
const wordPool = words[length];
if (length < settings.minLength) {
context.sendMessage(`Wordle must be at least ${settings.minLength} letters long.`, context.room.id);
return;
}
if (!wordPool) {
context.sendMessage(`No words with ${length} letters available.`, context.room.id);
return;
}
const wordList = Object.values(wordPool).flat();
const word = wordList[crypto.randomInt(wordList.length)];
wordles.set(context.room.id, {
word: word.word.toUpperCase(),
wordList,
mode,
definitions: word.definitions,
letters: new Map(alphabet.map((letter) => [letter, null])),
guesses: [],
});
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()}`);
}
function play(guess, context) {
const wordle = wordles.get(context.room.id);
if (!wordle) {
context.sendMessage(`There's no wordle going on. Start one with ${config.prefix}wordle [length]!`, context.room.id);
return;
}
if (!guess || guess.length !== wordle.word.length) {
context.sendMessage(`Your guess needs to be ${wordle.word.length} letters.`, context.room.id);
return;
}
if (!wordle.wordList.some((definition) => definition.word.toLowerCase() === guess.toLowerCase())) {
context.sendMessage(`The word '${guess}' is not in my dictionary.`, context.room.id);
return;
}
const upperGuess = guess.toUpperCase();
const occurrences = wordle.word.split('').reduce((acc, letter) => acc.set(letter, (acc.get(letter) || 0) + 1), new Map());
const guessLetters = upperGuess.split('');
const check = guessLetters.map((letter) => [letter, null]);
const prevGuess = wordle.guesses.at(-1)?.[1];
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);
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);
}
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);
});
wordle.guesses = wordle.guesses.concat([[context.user.username, upperGuess]]);
if (upperGuess === wordle.word) {
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, { 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);
return;
}
context.sendMessage(`${getBoard(check, true, context)}`, context.room.id);
}
function onCommand(args, context) {
const wordle = wordles.get(context.room.id);
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);
return;
}
if (context.subcommand === 'stop') {
if (wordle) {
context.sendMessage(`The game was stopped by ${config.usernamePrefix}${context.user.username}. The word was ${style.bold(wordle.word)}.`, context.room.id);
wordles.delete(context.room.id);
return;
}
context.sendMessage(`The there is no wordle going on, ${config.usernamePrefix}${context.user.username}, start one with ${config.prefix}wordle [length]!`, context.room.id);
return;
}
if (!wordle && context.room.id) {
start(length, mode, context);
return;
}
context.sendMessage(`There is already a ${wordle.word.length}-letter wordle going on, guess with ${config.prefix}w [word]!`, context.room.id);
}
module.exports = {
name: 'Wordle',
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

@ -117,6 +117,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

@ -3,8 +3,7 @@
const config = require('config');
const { setTimeout: delay } = require('timers/promises');
const bhttp = require('bhttp');
// const WebSocket = require('ws');
const io = require('socket.io-client');
const WebSocket = require('ws');
const logger = require('simple-node-logger').createSimpleLogger();
const { argv } = require('yargs');
@ -18,10 +17,9 @@ const instance = process.env.NODE_APP_INSTANCE || 'main';
logger.setLevel(argv.level || 'info');
async function auth() {
const httpSession = bhttp.session();
const username = config.uniqueUsername ? `${config.user.username}-${new Date().getTime().toString().slice(-5)}` : config.user.username;
const res = await httpSession.post(`${config.api}/session`, {
const res = await bhttp.post(`${config.api}/session`, {
...config.user,
username,
}, {
@ -36,13 +34,17 @@ async function auth() {
return {
user: res.body,
httpSession,
sessionCookie: res.headers['set-cookie'][0],
// auth may return an explicit auth cookie domain, but we connect through the VPN domain that would break the cookie, so don't use a bhttp session and strip the domain
sessionCookie: res.headers['set-cookie'][0].replace(/Domain=.*;/, ''),
};
}
async function getWsId(httpSession) {
const res = await httpSession.get(`${config.api}/socket`);
async function getWsId(sessionCookie) {
const res = await bhttp.get(`${config.api}/socket`, {
headers: {
cookie: sessionCookie,
},
});
if (res.statusCode !== 200) {
throw new Error(`Failed to retrieve WebSocket ID: ${res.body.toString()}`);
@ -128,6 +130,8 @@ function handleError(error, socket, domain, data) {
}
}
const pongRegex = /^pong:\d+$/;
async function connect(bot, games) {
const socket = {
ws: { readyState: 0 },
@ -136,37 +140,27 @@ async function connect(bot, games) {
socket.connect = async () => {
try {
const { user, httpSession, sessionCookie } = await auth();
const wsCreds = await getWsId(httpSession);
const { user, sessionCookie } = await auth();
const wsCreds = await getWsId(sessionCookie);
bot.user = user;
bot.httpSession = httpSession;
logger.info(`Attempting to connect to ${config.socket}`);
const { origin, pathname } = new URL(config.socket);
socket.io = io(origin, {
transports: ['websocket'],
path: pathname,
query: {
v: wsCreds.wsId,
t: wsCreds.timestamp,
},
extraHeaders: {
socket.ws = new WebSocket(`${config.socket}?${new URLSearchParams({ v: wsCreds.wsId, t: wsCreds.timestamp }).toString()}`, [], {
headers: {
cookie: sessionCookie,
},
});
socket.io.on('connect', () => {
logger.info(`Connected to ${config.socket}`);
});
socket.ws.on('message', async (msgData) => {
const msg = msgData.toString();
socket.io.on('connect_error', (error) => {
logger.info(`Failed to connect to ${config.socket}: ${error}`);
});
if (typeof msg === 'string' && pongRegex.test(msg)) {
logger.debug(`Received pong ${msg.split(':')[1]}`);
return;
}
socket.io.on('_', async (msg) => {
const [domain, data] = JSON.parse(msg);
logger.debug(`Received ${domain}: ${JSON.stringify(data)}`);
@ -180,12 +174,18 @@ async function connect(bot, games) {
}
});
socket.io.on('close', async (info) => {
socket.ws.on('close', async (info) => {
logger.error(`WebSocket closed, reconnecting in ${config.reconnectDelay} seconds: ${info}`);
await delay(config.reconnectDelay * 1000);
socket.connect();
});
socket.ws.on('error', async (error) => {
logger.error(`WebSocket error: ${error.message}`);
});
logger.info(`Connected to ${config.socket}`);
} catch (error) {
logger.error(`Failed to connect, retrying in ${config.reconnectDelay} seconds: ${error.message}`);
@ -195,9 +195,24 @@ async function connect(bot, games) {
};
socket.transmit = (domain, data) => {
socket.io.emit('_', JSON.stringify([domain, data]));
socket.ws.send(JSON.stringify([domain, data]));
};
function ping() {
setTimeout(() => {
if (socket.ws && socket.ws?.readyState === socket.ws?.OPEN) {
const now = Date.now();
socket.ws.send(`ping:${now}`);
logger.debug(`Sent ping ${now}`);
}
ping();
}, 10000);
}
ping();
socket.connect();
return socket;

View File

@ -0,0 +1,25 @@
const path = require('path');
const args = require('yargs').argv;
function init() {
const scoresA = require(path.resolve(__dirname, args.a));
const scoresB = require(path.resolve(__dirname, args.b));
const sum = {};
[scoresA, scoresB].forEach((record) => {
Object.entries(record).forEach(([game, scores]) => {
if (!sum[game]) {
sum[game] = {};
}
Object.entries(scores).forEach(([id, score]) => {
sum[game][id] = (sum[game][id] || 0) + score;
});
});
});
console.log(JSON.stringify(sum, null, 4));
}
init();

View File

@ -0,0 +1,35 @@
const path = require('path');
const args = require('yargs').argv;
const fs = require('fs').promises;
async function init() {
await fs.copyFile(args.file, `${path.basename(args.file, path.extname(args.file))}_backup_${Date.now()}.json`);
const filepath = path.join(process.cwd(), args.file);
const points = require(filepath); // eslint-disable-line
Object.entries(points).forEach(([game, scores]) => {
const originKey = Object.keys(scores).find((userKey) => userKey.includes(`:${args.origin}`));
const originScore = scores[originKey];
const targetKey = Object.keys(scores).find((userKey) => userKey.includes(`:${args.target}`));
const targetScore = scores[targetKey];
if (typeof originScore === 'undefined' || typeof targetScore === 'undefined') {
return;
}
const totalScore = targetScore + originScore;
points[game][targetKey] = targetScore + originScore;
delete points[game][originKey];
console.log(`${game} ${targetScore} (${args.target}) + ${originScore} (${args.origin}) = ${totalScore}`);
});
await fs.writeFile(filepath, JSON.stringify(points, null, 4));
console.log(`Saved ${filepath}`);
}
init();

View File

@ -42,6 +42,7 @@ const styleMethods = (() => {
return {
...styles,
forest: styles.green,
orange: styles.yellow,
code: bypass,
};
}