Compare commits
94 Commits
ba8e39f857
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59dd7c793a | ||
|
|
a9f733227c | ||
|
|
19feb9a55f | ||
|
|
df3c682ff6 | ||
|
|
aca9a4b597 | ||
|
|
e4055ad99c | ||
|
|
5dcb928c35 | ||
|
|
8c7995340e | ||
|
|
84025d6a8b | ||
|
|
84b158cf21 | ||
|
|
7531a69904 | ||
|
|
c402628161 | ||
|
|
e9df99bdcb | ||
|
|
a0f1914ce6 | ||
|
|
857f1816f0 | ||
|
|
5ac935cc95 | ||
|
|
af9f15d11d | ||
|
|
7747eabce2 | ||
|
|
990e18d3da | ||
|
|
234bbb2bbc | ||
|
|
f46d5f008b | ||
|
|
a86ef925ff | ||
|
|
ad26d2635c | ||
|
|
8d98581cd9 | ||
|
|
c2cdd55f2d | ||
|
|
34b3269d54 | ||
|
|
5aa5b1852a | ||
|
|
c3cadc3169 | ||
|
|
29cbd77e35 | ||
|
|
2310352ad6 | ||
|
|
5cc3a428f3 | ||
|
|
bbe573d8f3 | ||
|
|
3196877c37 | ||
|
|
ab25066936 | ||
|
|
f384d595e4 | ||
|
|
4ada601fb2 | ||
|
|
13e0bb9a8c | ||
|
|
055440418e | ||
|
|
352efb9147 | ||
|
|
1212261b21 | ||
|
|
a807cbc2aa | ||
|
|
f85cac7f2f | ||
|
|
402dbc3923 | ||
|
|
54968f3fb4 | ||
|
|
1228928592 | ||
|
|
6a35049609 | ||
|
|
a29eba70f0 | ||
|
|
064b4eb0d4 | ||
|
|
5e396a4abe | ||
|
|
453a3b1b42 | ||
|
|
a1a9d698e2 | ||
|
|
48cf433a5c | ||
|
|
61e935b925 | ||
|
|
9a7ef61d93 | ||
|
|
bad5dc52f7 | ||
|
|
d510caa123 | ||
|
|
b6cc3d716b | ||
|
|
6a36df3593 | ||
|
|
69bc1c9e6e | ||
|
|
aeb405967b | ||
|
|
73e60b81f1 | ||
|
|
c9b985f768 | ||
|
|
cb5de62e1c | ||
|
|
b3cab90810 | ||
|
|
47484ba7e2 | ||
|
|
4017f1cd07 | ||
|
|
03dfe8e437 | ||
|
|
1a992a6026 | ||
|
|
9f8f503f13 | ||
|
|
d460ba13c5 | ||
|
|
ed5337d828 | ||
|
|
59424af388 | ||
|
|
39a27f501e | ||
|
|
ffdfefa6d4 | ||
|
|
b3ab3bf1ba | ||
|
|
2e54d38383 | ||
|
|
98a1aa8fff | ||
|
|
6f2a5e03e9 | ||
|
|
9966c79a26 | ||
|
|
a46ffb431b | ||
|
|
b9be447dba | ||
|
|
a45d00c105 | ||
|
|
00b92f445e | ||
|
|
09e78a8bbb | ||
|
|
44d8304aa0 | ||
|
|
8e16f189f5 | ||
|
|
c85ec3440b | ||
|
|
650a8d2ec9 | ||
|
|
d263c42d38 | ||
|
|
fcd1597707 | ||
|
|
27a82b9cdd | ||
|
|
b791147ce0 | ||
|
|
29ce536ffd | ||
|
|
e584389453 |
@@ -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
2
.gitignore
vendored
@@ -4,5 +4,5 @@ config/*
|
||||
*.config.js
|
||||
!ecosystem.config.js
|
||||
*.sqlite
|
||||
points*.json
|
||||
points/*
|
||||
assets/mash-words.json
|
||||
|
||||
@@ -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] = {};
|
||||
|
||||
180143
assets/dictionary.json
Executable file → Normal file
180143
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
102260
assets/dictionary_old.json
Executable file
File diff suppressed because one or more lines are too long
31
assets/words-to-dictionary.js
Normal file
31
assets/words-to-dictionary.js
Normal 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
89686
assets/words.json
Normal file
File diff suppressed because it is too large
Load Diff
4
assets/words_blacklist.json
Normal file
4
assets/words_blacklist.json
Normal file
@@ -0,0 +1,4 @@
|
||||
[
|
||||
"ioctl",
|
||||
"xterm"
|
||||
]
|
||||
@@ -15,7 +15,7 @@ module.exports = {
|
||||
operators: ['admin'],
|
||||
server: 'irc.libera.chat',
|
||||
port: 6697,
|
||||
socket: 'ws://127.0.0.1:3000/socket',
|
||||
socket: 'ws://127.0.0.1:3000/socket/',
|
||||
api: 'http://127.0.0.1:3000/api',
|
||||
reconnectDelay: 10, // seconds
|
||||
prefix: '~',
|
||||
@@ -27,6 +27,7 @@ module.exports = {
|
||||
'chat',
|
||||
'mash',
|
||||
'trivia',
|
||||
'wordle',
|
||||
'letters',
|
||||
'numbers',
|
||||
'hunt',
|
||||
@@ -133,10 +134,16 @@ 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: 60,
|
||||
points: [10, 7, 7, 7, 7, 7, 5, 5, 5, 5, 5], // points by distance
|
||||
timeout: 90,
|
||||
points: [3, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1], // points by distance
|
||||
},
|
||||
riddle: {
|
||||
timeout: 30,
|
||||
|
||||
231
package-lock.json
generated
231
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "schat2-clive",
|
||||
"version": "1.26.2",
|
||||
"version": "1.30.8",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "schat2-clive",
|
||||
"version": "1.26.2",
|
||||
"version": "1.30.8",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^8.3.0",
|
||||
@@ -25,6 +25,7 @@
|
||||
"markov-strings": "^3.0.1",
|
||||
"mathjs": "^11.8.0",
|
||||
"simple-node-logger": "^21.8.12",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"string-similarity": "^4.0.4",
|
||||
"tensify": "^0.0.4",
|
||||
"vm2": "^3.9.11",
|
||||
@@ -566,6 +567,11 @@
|
||||
"node": ">=6.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||
},
|
||||
"node_modules/@tootallnate/once": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||
@@ -1337,6 +1343,47 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
|
||||
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.11.0",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/engine.io-client/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/engine.io-parser": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
|
||||
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/errors": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/errors/-/errors-0.2.0.tgz",
|
||||
@@ -3742,6 +3789,74 @@
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client": {
|
||||
"version": "4.7.2",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
|
||||
"integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-client/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"dependencies": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser/node_modules/debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"dependencies": {
|
||||
"ms": "2.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"supports-color": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/socket.io-parser/node_modules/ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
@@ -4432,9 +4547,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
||||
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -4464,6 +4579,14 @@
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
|
||||
},
|
||||
"node_modules/xmlhttprequest-ssl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/xtend": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz",
|
||||
@@ -4919,6 +5042,11 @@
|
||||
"to-fast-properties": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"@socket.io/component-emitter": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz",
|
||||
"integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg=="
|
||||
},
|
||||
"@tootallnate/once": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz",
|
||||
@@ -5504,6 +5632,38 @@
|
||||
"once": "^1.4.0"
|
||||
}
|
||||
},
|
||||
"engine.io-client": {
|
||||
"version": "6.5.3",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
|
||||
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
|
||||
"requires": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
"ws": "~8.11.0",
|
||||
"xmlhttprequest-ssl": "~2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"engine.io-parser": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
|
||||
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ=="
|
||||
},
|
||||
"errors": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/errors/-/errors-0.2.0.tgz",
|
||||
@@ -7278,6 +7438,56 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"socket.io-client": {
|
||||
"version": "4.7.2",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.2.tgz",
|
||||
"integrity": "sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==",
|
||||
"requires": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io-client": "~6.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"socket.io-parser": {
|
||||
"version": "4.2.4",
|
||||
"resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz",
|
||||
"integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==",
|
||||
"requires": {
|
||||
"@socket.io/component-emitter": "~3.1.0",
|
||||
"debug": "~4.3.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "4.3.4",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||
"integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
|
||||
"requires": {
|
||||
"ms": "2.1.2"
|
||||
}
|
||||
},
|
||||
"ms": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"source-map": {
|
||||
"version": "0.5.7",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
|
||||
@@ -7835,9 +8045,9 @@
|
||||
}
|
||||
},
|
||||
"ws": {
|
||||
"version": "8.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.2.3.tgz",
|
||||
"integrity": "sha512-wBuoj1BDpC6ZQ1B7DWQBYVLphPWkm8i9Y0/3YdHjHKHiohOJ1ws+3OccDWtH+PoC9DZD5WOTrJvNbWvjS6JWaA==",
|
||||
"version": "8.11.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
|
||||
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
|
||||
"requires": {}
|
||||
},
|
||||
"xml-name-validator": {
|
||||
@@ -7850,6 +8060,11 @@
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
"integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="
|
||||
},
|
||||
"xmlhttprequest-ssl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
||||
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A=="
|
||||
},
|
||||
"xtend": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xtend/-/xtend-3.0.0.tgz",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "schat2-clive",
|
||||
"version": "1.26.2",
|
||||
"version": "1.30.8",
|
||||
"description": "Game host for SChat 2-powered chat sites",
|
||||
"main": "src/app.js",
|
||||
"scripts": {
|
||||
@@ -34,6 +34,7 @@
|
||||
"markov-strings": "^3.0.1",
|
||||
"mathjs": "^11.8.0",
|
||||
"simple-node-logger": "^21.8.12",
|
||||
"socket.io-client": "^4.7.2",
|
||||
"string-similarity": "^4.0.4",
|
||||
"tensify": "^0.0.4",
|
||||
"vm2": "^3.9.11",
|
||||
|
||||
@@ -253,14 +253,15 @@ async function onCommand(args, context, isConversation) {
|
||||
});
|
||||
|
||||
const reply = res.body.choices?.[0].message;
|
||||
const target = prompt.match(/@\s*[\w-]+\s*$/)?.[0].slice(1).trim();
|
||||
const curatedContent = reply.content.replace(/\n+/g, '. ');
|
||||
|
||||
context.logger.info(`OpenAI ${config.chat.model} replied to ${context.user.username} (${res.body.usage.total_tokens} tokens, ${(usedTokens || 0) + res.body.usage.total_tokens}/${config.chat.userTokenLimit} used): ${curatedContent}`);
|
||||
context.logger.info(`OpenAI ${config.chat.model} replied to ${target || context.user.username} (${res.body.usage.total_tokens} tokens, ${(usedTokens || 0) + res.body.usage.total_tokens}/${config.chat.userTokenLimit} used): ${curatedContent}`);
|
||||
|
||||
if (isConversation) {
|
||||
context.sendMessage(`${curatedContent}`, context.room?.id, { label: false }, context.user.username);
|
||||
} else {
|
||||
context.sendMessage(`${context.user.prefixedUsername}: ${curatedContent}`, context.room.id, { label: false });
|
||||
context.sendMessage(`${target ? `${config.usernamePrefix}${target}` : context.user.prefixedUsername}: ${curatedContent}`, context.room.id, { label: false });
|
||||
}
|
||||
|
||||
await knex('chat_tokens').insert({
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const { intervalToDuration } = require('date-fns');
|
||||
|
||||
const style = require('../utils/style');
|
||||
const pickRandom = require('../utils/pick-random');
|
||||
@@ -56,11 +57,18 @@ function onCommand(args, context) {
|
||||
}
|
||||
|
||||
const hit = Math.random() > config.duck.missRatio;
|
||||
const time = ((new Date().getTime() - duck.getTime()) / 1000).toFixed(3);
|
||||
const time = new Date().getTime() - duck.getTime();
|
||||
|
||||
const distance = time < 60 * 1000 // show digits up to one minute
|
||||
? `${(time / 1000).toFixed(3)} seconds`
|
||||
: `${Object.entries(intervalToDuration({ start: duck, end: new Date() }))
|
||||
.filter(([, value]) => value > 0)
|
||||
.map(([key, value]) => `${value} ${key}`)
|
||||
.join(', ')} and ${time % 1000} milliseconds`;
|
||||
|
||||
if (['bang', 'shoot'].includes(context.command)) {
|
||||
if (hit) {
|
||||
context.sendMessage(`You shot a duck in ${style.bold(`${time} seconds`)}, ${context.user.prefixedUsername}`, context.room.id);
|
||||
context.sendMessage(`${context.user.prefixedUsername}: You shot a duck in ${style.bold(style.red(distance))}`, context.room.id);
|
||||
launchDuck(context);
|
||||
|
||||
context.setPoints(context.user, 1, { key: 'bang' });
|
||||
@@ -73,6 +81,7 @@ function onCommand(args, context) {
|
||||
`That's a miss! Better luck next time, ${context.user.prefixedUsername}.`,
|
||||
`The duck outsmarted you, ${context.user.prefixedUsername}`,
|
||||
`Channeling Gareth Southgate, ${context.user.prefixedUsername}? You missed!`,
|
||||
`Oh, and that's a bad miss, ${context.user.prefixedUsername}.`,
|
||||
]);
|
||||
|
||||
shots.set(context.user.id, new Date());
|
||||
@@ -83,7 +92,7 @@ function onCommand(args, context) {
|
||||
|
||||
if (['bef', 'befriend'].includes(context.command)) {
|
||||
if (hit) {
|
||||
context.sendMessage(`You befriended a duck in ${style.bold(style.green(`${time} seconds`))}, ${context.user.prefixedUsername}`, context.room.id);
|
||||
context.sendMessage(`${context.user.prefixedUsername}: You befriended a duck in ${style.bold(style.sky(distance))}`, context.room.id);
|
||||
launchDuck(context);
|
||||
|
||||
context.setPoints(context.user, 1, { key: 'befriend' });
|
||||
|
||||
@@ -2,16 +2,28 @@
|
||||
|
||||
const config = require('config');
|
||||
|
||||
const style = require('../utils/style');
|
||||
|
||||
function onCommand(args, context) {
|
||||
if (config.platform === 'schat') {
|
||||
context.sendMessage('Shutting down... :sleeping:', context.room?.id, { type: 'message', label: false }, context.message.user?.username);
|
||||
Object.keys(context.bot.rooms).forEach((roomId) => {
|
||||
context.sendMessage(`Kill command used by ${style.bold(context.user.username)}, shutting down... :sleeping:`, roomId, { type: 'message', label: false });
|
||||
});
|
||||
|
||||
if (context.message.user) {
|
||||
context.sendMessage('Shutting down... :sleeping:', context.room?.id, { type: 'message', label: false }, context.message.user.username);
|
||||
}
|
||||
}
|
||||
|
||||
if (config.platform === 'irc' && context.room.id === config.user.id) {
|
||||
// if the room ID is the bot's own nickname, it's a PM and we should reply to the sender
|
||||
context.sendMessage('Shutting down... 😴', context.user.id, { label: false });
|
||||
} else if (config.platform === 'irc') {
|
||||
context.sendMessage('Shutting down... 😴', context.room?.id, { type: 'message', label: false });
|
||||
if (config.platform === 'irc') {
|
||||
context.bot.rooms.forEach((room) => {
|
||||
context.sendMessage(`Kill command used by ${style.bold(context.user.username)}, shutting down... 😴`, room.id, { type: 'message', label: false });
|
||||
});
|
||||
|
||||
if (context.room.id === config.user.id) {
|
||||
// if the room ID is the bot's own nickname, it's a PM and we should reply to the sender
|
||||
context.sendMessage('Shutting down... 😴', context.user.id, { type: 'message', label: false }, context.message.user.username);
|
||||
}
|
||||
}
|
||||
|
||||
context.logger.info(`Kill command used by ${context.user.username}`);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
420
src/games/numbers-improved.js
Normal file
420
src/games/numbers-improved.js
Normal file
@@ -0,0 +1,420 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const crypto = require('crypto');
|
||||
const timers = require('timers/promises');
|
||||
|
||||
const math = require('mathjs');
|
||||
|
||||
const getLeaders = require('../utils/get-leaders');
|
||||
const pickRandom = require('../utils/pick-random');
|
||||
const style = require('../utils/style');
|
||||
const { solveAll } = require('../utils/numbers-solver');
|
||||
|
||||
const limitedMath = math.create({
|
||||
addDependencies: math.addDependencies,
|
||||
divideDependencies: math.divideDependencies,
|
||||
evaluateDependencies: math.evaluateDependencies,
|
||||
});
|
||||
|
||||
class RuleError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'RuleError';
|
||||
}
|
||||
}
|
||||
|
||||
function divide(a, b) {
|
||||
const result = math.divide(a, b);
|
||||
|
||||
if (result % 1) {
|
||||
throw new RuleError(`The division ${style.bold(style.code(`${a} / ${b}`))} results in a fraction, which is not allowed`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
limitedMath.import({
|
||||
divide,
|
||||
}, { override: true });
|
||||
|
||||
const settings = { ...config.numbers };
|
||||
const games = new Map();
|
||||
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
function padNumber(number) {
|
||||
if (!number) {
|
||||
return ' ';
|
||||
}
|
||||
|
||||
const string = String(number);
|
||||
|
||||
if (string.length === 3) {
|
||||
return string;
|
||||
}
|
||||
|
||||
if (string.length === 2) {
|
||||
return ` ${string}`;
|
||||
}
|
||||
|
||||
return ` ${string} `;
|
||||
}
|
||||
|
||||
function getTarget(game) {
|
||||
return `${style.grey(style.code('['))}${style.bold(style.code(game.target))}${style.grey(style.code(']'))}`;
|
||||
}
|
||||
|
||||
function getBoardNumbers(game) {
|
||||
// return `\`[\`${game.numbers.concat(Array.from({ length: config.numbers.length - game.numbers.length })).map((number) => style.bold(`\`${padNumber(number)}\``)).join('`|`')}\`]\``;
|
||||
return `${style.grey(style.code('['))}${game.numbers.concat(Array.from({ length: config.numbers.length - game.numbers.length })).map((number) => style.bold(style.code(padNumber(number)))).join(style.grey(style.code('|')))}${style.grey(style.code(']'))}`;
|
||||
}
|
||||
|
||||
function getBoard(context) {
|
||||
const game = games.get(context.room.id);
|
||||
|
||||
if (game.target) {
|
||||
return `${getBoardNumbers(game)} with target ${getTarget(game)}`;
|
||||
}
|
||||
|
||||
return getBoardNumbers(game);
|
||||
}
|
||||
|
||||
function countNumbers(numbers) {
|
||||
return numbers.reduce((acc, number) => ({ ...acc, [number]: (acc[number] || 0) + 1 }), {});
|
||||
}
|
||||
|
||||
function setTimeout(timeout, context) {
|
||||
if (!timeout) {
|
||||
context.sendMessage(`Timeout is set to ${style.bold(settings.timeout)}`, context.room.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedTimeout = Number(timeout);
|
||||
|
||||
if (Number.isNaN(parsedTimeout) || parsedTimeout < 10 || parsedTimeout > 300) {
|
||||
context.sendMessage('Timeout must be a number between 10 and 300', context.room.id);
|
||||
return;
|
||||
}
|
||||
|
||||
settings.timeout = parsedTimeout;
|
||||
|
||||
context.sendMessage(`Timeout set to ${style.bold(settings.timeout)} by ${context.user.prefixedUsername}`, context.room.id);
|
||||
}
|
||||
|
||||
function getWinnerText(game) {
|
||||
if (game.state === 'pick') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (game.winner) {
|
||||
return `The winner is ${style.bold(game.winner.prefixedUsername)}, who was ${game.distances[game.winner.username]} away for ${style.bold(game.points[game.distances[game.winner.username]])} points.`;
|
||||
}
|
||||
|
||||
return 'No one found a solution.';
|
||||
}
|
||||
|
||||
function getSolutionText(game) {
|
||||
if (game.distances[game.winner?.username] === 0 && game.solution?.answer === game.target) {
|
||||
return `The target ${getTarget(game)} was solved as follows: ${style.bold(style.code(game.winner.solution))}. My solution was ${style.bold(style.code(game.solution.solution))}.`;
|
||||
}
|
||||
|
||||
if (game.distances[game.winner?.username] === 0) {
|
||||
// the winner somehow found a solution the solver did not
|
||||
return `The target ${getTarget(game)} was solved as follows: ${style.bold(style.code(game.winner.solution))}. Even I couldn't get that, well done!`;
|
||||
}
|
||||
|
||||
if (game.solution?.answer === game.target) {
|
||||
return `The target ${getTarget(game)} could be solved as follows: ${style.bold(style.code(game.solution.solution))}.`;
|
||||
}
|
||||
|
||||
if (game.solution) {
|
||||
return `The target ${getTarget(game)} was impossible, the closest answer is ${style.bold(game.solution.answer)} as follows: ${style.bold(style.code(game.solution.solution))}.`;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function stop(context, aborted) {
|
||||
const game = games.get(context.room.id);
|
||||
|
||||
if (!game) {
|
||||
context.sendMessage(`There is no numbers game going on, ${context.user.prefixedUsername}. You can start one with ${config.prefix}numbers!`, context.room.id);
|
||||
return;
|
||||
}
|
||||
|
||||
game.ac.abort();
|
||||
games.delete(context.room.id);
|
||||
|
||||
if (game.winner) {
|
||||
context.setPoints(game.winner, config.numbers.points[game.distances[game.winner.username]]);
|
||||
}
|
||||
|
||||
const wrapText = aborted
|
||||
? `The game was stopped by ${style.cyan(`${config.usernamePrefix}${context.user.username}`)}.`
|
||||
: style.bold('Time\'s up!');
|
||||
|
||||
console.log('time up');
|
||||
|
||||
const winnerText = getWinnerText(game);
|
||||
|
||||
const leaders = Object.keys(game.distances).length > 1
|
||||
? `Honorary mentions: ${getLeaders(game.distances, context.user, { skip: [game.winner.username], pointsWord: 'away' })}.`
|
||||
: null;
|
||||
|
||||
const solutionText = getSolutionText(game);
|
||||
|
||||
context.sendMessage([wrapText, winnerText, leaders, solutionText].filter(Boolean).join(' '), context.room.id);
|
||||
}
|
||||
|
||||
async function play(context) {
|
||||
const game = games.get(context.room.id);
|
||||
|
||||
game.counts = countNumbers(game.numbers);
|
||||
game.target = crypto.randomInt(100, 999);
|
||||
|
||||
game.solutions = await solveAll(game.numbers, game.target, 3);
|
||||
game.solution = game.solutions.reduce((closest, solution) => (!closest || Math.abs(game.target - solution.answer) < Math.abs(game.target - closest.answer) ? solution : closest), null);
|
||||
|
||||
game.state = 'solutions';
|
||||
|
||||
context.sendMessage(`${getBoard(context)}. You have ${style.bold(style.green(settings.timeout))} seconds, let's start!`, context.room.id);
|
||||
context.logger.info(`Numbers game started by ${context.user.username}, numbers ${game.numbers.join('|')}, target ${game.target}, ${game.solution.answer === game.target ? 'exact' : 'closest'} solution for ${game.solution.answer} is: ${game.solution.solution}`);
|
||||
|
||||
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 / 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);
|
||||
|
||||
await timers.setTimeout((settings.timeout / 3) * 1000, null, { signal: game.ac.signal });
|
||||
|
||||
stop(context);
|
||||
} catch (error) {
|
||||
// abort expected, probably not an error
|
||||
}
|
||||
}
|
||||
|
||||
function pickLarge() {
|
||||
return pickRandom([100, 75, 50, 25]);
|
||||
}
|
||||
|
||||
function pickSmall() {
|
||||
return pickRandom([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
|
||||
}
|
||||
|
||||
function pickNumbers(type, context) {
|
||||
const game = games.get(context.room.id);
|
||||
|
||||
if (!game) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'large') {
|
||||
game.numbers = game.numbers.concat(pickLarge());
|
||||
}
|
||||
|
||||
if (type === 'small') {
|
||||
game.numbers = game.numbers.concat(pickSmall());
|
||||
}
|
||||
|
||||
if (type !== 'small' && type !== 'large') {
|
||||
type.toLowerCase().slice(0, config.numbers.length - game.numbers.length).split('').forEach((typeKey) => {
|
||||
game.numbers = game.numbers.concat(['b', 'l', 't'].includes(typeKey) ? pickLarge() : pickSmall());
|
||||
});
|
||||
}
|
||||
|
||||
if (game.numbers.length === config.numbers.length) {
|
||||
play(context);
|
||||
return;
|
||||
}
|
||||
|
||||
context.sendMessage(`${getBoard(context)} Would you like a large number or a small one?`, context.room.id);
|
||||
}
|
||||
|
||||
function playSolution(solution, context) {
|
||||
const game = games.get(context.room.id);
|
||||
|
||||
try {
|
||||
const parsed = limitedMath.parse(solution.replace(/`/g, '')); // backticks may be used to prevent the expression from being expanded by Markdown in SChat
|
||||
|
||||
const numbers = parsed.filter((node) => {
|
||||
if (node.type === 'FunctionNode' && !['add', 'subtract', 'multiply', 'divide'].includes(node.fn.name)) {
|
||||
throw new RuleError(`The ${style.bold(node.name)} function is not allowed`);
|
||||
}
|
||||
|
||||
if (node.op && !['add', 'subtract', 'multiply', 'divide'].includes(node.fn)) {
|
||||
throw new RuleError(`The ${style.bold(node.op)} operator is not allowed`);
|
||||
}
|
||||
|
||||
return node.type === 'ConstantNode';
|
||||
}).map((node) => node.value);
|
||||
|
||||
const counts = countNumbers(numbers);
|
||||
const imagined = Object.keys(counts).filter((number) => !game.counts[number]);
|
||||
const overused = Object.entries(counts).filter(([number, count]) => count > game.counts[number]).map(([number]) => number);
|
||||
|
||||
if (imagined.length > 0) {
|
||||
context.sendMessage(`${context.user.prefixedUsername}: ${imagined.map((number) => style.bold(number)).join(' and ')} is not on the board`, context.room.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (overused.length > 0) {
|
||||
context.sendMessage(`${context.user.prefixedUsername}: ${overused.map((number) => style.bold(number)).join(' and ')} is used too often`, context.room.id);
|
||||
return;
|
||||
}
|
||||
|
||||
const answer = parsed.evaluate();
|
||||
const distance = Math.abs(game.target - answer);
|
||||
const points = config.numbers.points[distance];
|
||||
const lastDistance = game.distances[context.user.username] || null;
|
||||
const closestDistance = Object.values(game.distances).reduce((acc, userDistance) => (userDistance < acc ? userDistance : acc), Infinity);
|
||||
|
||||
/*
|
||||
const summary = distance === 0
|
||||
? `Your solution ${style.bold(style.code(parsed))} results in ${style.bold(answer)}, which is ${style.bold('exactly on target')}`
|
||||
: `Your solution ${style.bold(style.code(parsed))} results in ${style.bold(answer)}, which is ${style.bold(distance)} away from target ${getTarget(game)}`;
|
||||
*/
|
||||
|
||||
const summary = distance === 0
|
||||
? `${style.bold(style.code(parsed))} results in ${style.bold(style.code(answer))}, that is ${style.bold('exactly on target')}`
|
||||
: `${style.bold(style.code(parsed))} results in ${style.bold(style.code(answer))}, that is ${style.bold(distance)} away from target ${getTarget(game)}`;
|
||||
|
||||
if (points && distance < lastDistance) {
|
||||
game.distances[context.user.username] = distance;
|
||||
}
|
||||
|
||||
console.log(distance, lastDistance, closestDistance, distance < closestDistance);
|
||||
console.log(game.distances);
|
||||
|
||||
if (points && distance < closestDistance) {
|
||||
game.winner = {
|
||||
...context.user,
|
||||
solution: parsed,
|
||||
};
|
||||
|
||||
context.sendMessage(`${context.user.prefixedUsername}: ${summary} for ${style.bold(points)} points, the current winner`, context.room.id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (points && distance < lastDistance) {
|
||||
context.sendMessage(`${context.user.prefixedUsername}: ${summary} for ${style.bold(points)} points, your personal best, but not closer than the winning ${style.bold(closestDistance)} away`, context.room.id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (points) {
|
||||
context.sendMessage(`${context.user.prefixedUsername}: ${summary} for ${style.bold(points)} points, but does not beat your personal closest of ${style.bold(lastDistance)} away`, context.room.id);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
context.sendMessage(`${summary} for ${style.bold('no')} points, ${context.user.prefixedUsername}`, context.room.id);
|
||||
} catch (error) {
|
||||
// invalid solution
|
||||
if (error.name === 'RuleError') {
|
||||
context.sendMessage(`${error.message}, ${context.user.prefixedUsername}`, context.room.id);
|
||||
return;
|
||||
}
|
||||
|
||||
context.logger.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
function start(context, numbers) {
|
||||
if (games.has(context.room.id)) {
|
||||
context.sendMessage(`${getBoard(context)} is the current board. Use ${config.prefix}numbers:stop to reset.`, context.room.id);
|
||||
return;
|
||||
}
|
||||
|
||||
games.set(context.room.id, {
|
||||
state: 'pick',
|
||||
numbers: [],
|
||||
target: null,
|
||||
distances: {},
|
||||
winner: null,
|
||||
ac: new AbortController(), // eslint-disable-line no-undef
|
||||
});
|
||||
|
||||
if (numbers) {
|
||||
pickNumbers(numbers, context);
|
||||
return;
|
||||
}
|
||||
|
||||
context.sendMessage('Let\'s play the numbers! Would you like a large number or a small one?', context.room.id);
|
||||
}
|
||||
|
||||
function solve(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();
|
||||
|
||||
context.sendMessage(`${style.bold(style.code(parsed))} = ${style.bold(answer)}`, context.room.id);
|
||||
} catch (error) {
|
||||
context.sendMessage(`Failed to solve ${style.bold(style.code(calculation))}: ${error.message}`, context.room.id);
|
||||
}
|
||||
}
|
||||
|
||||
function onCommand(args, context) {
|
||||
if (context.subcommand === 'stop') {
|
||||
stop(context, true);
|
||||
return;
|
||||
}
|
||||
|
||||
if (context.subcommand === 'timeout') {
|
||||
setTimeout(args[0], context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (['calculate', 'calc', 'solve'].includes(context.subcommand || context.command)) {
|
||||
solve(args.join(' '), context); // two from the top, four from the bottom, please Rachel
|
||||
return;
|
||||
}
|
||||
|
||||
if (['numsgo', 'numgo'].includes(context.command) || ['start', 'go', 'auto', 'random'].includes(context.subcommand)) {
|
||||
start(context, 'bbssss'); // two from the top, four from the bottom, please Rachel
|
||||
return;
|
||||
}
|
||||
|
||||
if (!args.subcommand) {
|
||||
start(context);
|
||||
}
|
||||
}
|
||||
|
||||
function onMessage(message, context) {
|
||||
const game = games.get(context.room?.id);
|
||||
const body = message.originalBody || message.body; // * gets resolved to <em>
|
||||
|
||||
if (game?.state === 'pick') {
|
||||
const multi = body.match(/\b[blts]{2,}$/i)?.[0];
|
||||
|
||||
if (multi) {
|
||||
pickNumbers(multi.toLowerCase(), context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/large|big|top/i.test(body)) {
|
||||
pickNumbers('large', context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/small|bottom/i.test(body)) {
|
||||
pickNumbers('small', context);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (game?.state === 'solutions' && body.match(/\d+/g)?.length > 1) {
|
||||
playSolution(body, context);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
onCommand,
|
||||
onMessage,
|
||||
commands: ['nums', 'numsgo', 'numgo', 'calculate', 'calc', 'solve'],
|
||||
help: `Reach the target number using only the ${config.numbers.length} numbers on the board. You can use each number only once, but do not have to use all of them. You may use addition, subtraction, multiplication and division, but each intermediate calculation must result in a whole number. You can score ${Array.from(new Set(config.numbers.points)).slice(0, -1).join(', ')} or ${config.numbers.points.at(-1)} points for solutions up to ${config.numbers.points.length - 1} away, but only the first best solution gets the points. Quick-start with ${config.prefix}numsgo, or use ${config.prefix}numbers to fill the board by manually selecting a random *large* or *big* number (100, 75, 50, 25), a random *small* number (1-10), or multiple at once like LLSSSS.`, // eslint-disable-line max-len
|
||||
};
|
||||
@@ -1,46 +1,42 @@
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
'use strict';
|
||||
|
||||
const config = require('config');
|
||||
const crypto = require('crypto');
|
||||
const timers = require('timers/promises');
|
||||
|
||||
const {
|
||||
create,
|
||||
parse,
|
||||
addDependencies,
|
||||
divideDependencies,
|
||||
evaluateDependencies,
|
||||
} = require('mathjs');
|
||||
const math = require('mathjs');
|
||||
|
||||
const getLeaders = require('../utils/get-leaders');
|
||||
const pickRandom = require('../utils/pick-random');
|
||||
const style = require('../utils/style');
|
||||
const { solveAll } = require('../utils/numbers-solver');
|
||||
|
||||
const { parse: limitedParse, divide: mathDivide, import: mathImport } = create({
|
||||
addDependencies,
|
||||
divideDependencies,
|
||||
evaluateDependencies,
|
||||
const limitedMath = math.create({
|
||||
addDependencies: math.addDependencies,
|
||||
divideDependencies: math.divideDependencies,
|
||||
evaluateDependencies: math.evaluateDependencies,
|
||||
});
|
||||
|
||||
class FractionError extends Error {
|
||||
class RuleError extends Error {
|
||||
constructor(message) {
|
||||
super(message);
|
||||
this.name = 'FractionError';
|
||||
this.name = 'RuleError';
|
||||
}
|
||||
}
|
||||
|
||||
function divide(a, b) {
|
||||
const result = mathDivide(a, b);
|
||||
const result = math.divide(a, b);
|
||||
|
||||
if (result % 1) {
|
||||
throw new FractionError(`${style.bold(style.code(`${a} / ${b}`))} results in a fraction`);
|
||||
throw new RuleError(`The division ${style.bold(style.code(`${a} / ${b}`))} results in a fraction, which is not allowed`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
mathImport({
|
||||
limitedMath.import({
|
||||
divide,
|
||||
}, { override: true });
|
||||
|
||||
@@ -120,11 +116,11 @@ function getWinnerText(game) {
|
||||
}
|
||||
|
||||
function getSolutionText(game) {
|
||||
if (game.points[game.winner?.username] === 10 && game.solution?.answer === game.target) {
|
||||
if (game.points[game.winner?.username] === config.numbers.points[0] && game.solution?.answer === game.target) {
|
||||
return `The target ${getTarget(game)} was solved as follows: ${style.bold(style.code(game.winner.solution))}. My solution was ${style.bold(style.code(game.solution.solution))}.`;
|
||||
}
|
||||
|
||||
if (game.points[game.winner?.username] === 10) {
|
||||
if (game.points[game.winner?.username] === config.numbers.points[0]) {
|
||||
// the winner somehow found a solution the solver did not
|
||||
return `The target ${getTarget(game)} was solved as follows: ${style.bold(style.code(game.winner.solution))}. Even I couldn't get that, well done!`;
|
||||
}
|
||||
@@ -224,7 +220,7 @@ function pickNumbers(type, context) {
|
||||
|
||||
if (type !== 'small' && type !== 'large') {
|
||||
type.toLowerCase().slice(0, config.numbers.length - game.numbers.length).split('').forEach((typeKey) => {
|
||||
game.numbers = game.numbers.concat(typeKey === 'b' || typeKey === 'l' ? pickLarge() : pickSmall());
|
||||
game.numbers = game.numbers.concat(['b', 'l', 't'].includes(typeKey) ? pickLarge() : pickSmall());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -240,8 +236,20 @@ function playSolution(solution, context) {
|
||||
const game = games.get(context.room.id);
|
||||
|
||||
try {
|
||||
const parsed = limitedParse(solution.replace(/`/g, '')); // backticks may be used to prevent the expression from being expanded by Markdown in SChat
|
||||
const numbers = parsed.filter((node) => node.type === 'ConstantNode').map((node) => node.value);
|
||||
const parsed = limitedMath.parse(solution.replace(/`/g, '')); // backticks may be used to prevent the expression from being expanded by Markdown in SChat
|
||||
|
||||
const numbers = parsed.filter((node) => {
|
||||
if (node.type === 'FunctionNode' && !['add', 'subtract', 'multiply', 'divide'].includes(node.fn.name)) {
|
||||
throw new RuleError(`The ${style.bold(node.name)} function is not allowed`);
|
||||
}
|
||||
|
||||
if (node.op && !['add', 'subtract', 'multiply', 'divide'].includes(node.fn)) {
|
||||
throw new RuleError(`The ${style.bold(node.op)} operator is not allowed`);
|
||||
}
|
||||
|
||||
return node.type === 'ConstantNode';
|
||||
}).map((node) => node.value);
|
||||
|
||||
const counts = countNumbers(numbers);
|
||||
const imagined = Object.keys(counts).filter((number) => !game.counts[number]);
|
||||
const overused = Object.entries(counts).filter(([number, count]) => count > game.counts[number]).map(([number]) => number);
|
||||
@@ -296,8 +304,8 @@ function playSolution(solution, context) {
|
||||
context.sendMessage(`${summary} for ${style.bold('no')} points, ${context.user.prefixedUsername}`, context.room.id);
|
||||
} catch (error) {
|
||||
// invalid solution
|
||||
if (error.name === 'FractionError') {
|
||||
context.sendMessage(`${error.message}, which is not allowed, ${context.user.prefixedUsername}`, context.room.id);
|
||||
if (error.name === 'RuleError') {
|
||||
context.sendMessage(`${error.message}, ${context.user.prefixedUsername}`, context.room.id);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -328,9 +336,9 @@ 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 = parse(calculation.replace(/`/g, '')); // backticks may be used to prevent the expression from being expanded by Markdown in SChat
|
||||
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();
|
||||
|
||||
context.sendMessage(`${style.bold(style.code(parsed))} = ${style.bold(answer)}`, context.room.id);
|
||||
@@ -339,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);
|
||||
@@ -350,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;
|
||||
}
|
||||
@@ -370,19 +428,19 @@ function onMessage(message, context) {
|
||||
const body = message.originalBody || message.body; // * gets resolved to <em>
|
||||
|
||||
if (game?.state === 'pick') {
|
||||
const multi = body.match(/\b[bls]{2,}$/i)?.[0];
|
||||
const multi = body.match(/\b[blts]{2,}$/i)?.[0];
|
||||
|
||||
if (multi) {
|
||||
pickNumbers(multi.toLowerCase(), context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/large|big/i.test(body)) {
|
||||
if (/large|big|top/i.test(body)) {
|
||||
pickNumbers('large', context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/small/i.test(body)) {
|
||||
if (/small|bottom/i.test(body)) {
|
||||
pickNumbers('small', context);
|
||||
return;
|
||||
}
|
||||
@@ -397,5 +455,5 @@ module.exports = {
|
||||
onCommand,
|
||||
onMessage,
|
||||
commands: ['nums', 'numsgo', 'numgo', 'calculate', 'calc', 'solve'],
|
||||
help: `Reach the target number using only the ${config.numbers.length} numbers on the board; you do not have to use all of them. You may use addition, subtraction, multiplication and divisions, but each calculation must result in a whole number. You can score points for solutions up to ${config.numbers.points.length - 1} away from the target, but only the first best solution gets the points. Quick-start with ${config.prefix}numsgo, or use ${config.prefix}numbers to fill the board by manually selecting a random *large* or *big* number (100, 75, 50, 25), a random *small* number (1-10), or multiple at once like LLSSSS.`, // eslint-disable-line max-len
|
||||
help: `Reach the target number using only the ${config.numbers.length} numbers on the board. You can use each number only once, but do not have to use all of them. You may use addition, subtraction, multiplication and division, but each intermediate calculation must result in a whole number. You can score ${Array.from(new Set(config.numbers.points)).slice(0, -1).join(', ')} or ${config.numbers.points.at(-1)} points for solutions up to ${config.numbers.points.length - 1} away, but only the first best solution gets the points. Quick-start with ${config.prefix}numsgo, or use ${config.prefix}numbers to fill the board by manually selecting a random *large* or *big* number (100, 75, 50, 25), a random *small* number (1-10), or multiple at once like LLSSSS.`, // eslint-disable-line max-len
|
||||
};
|
||||
|
||||
245
src/games/wordle.js
Normal file
245
src/games/wordle.js
Normal 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,
|
||||
};
|
||||
@@ -51,7 +51,7 @@ async function init() {
|
||||
client.addListener('message', (from, to, body) => onMessage({
|
||||
from,
|
||||
to,
|
||||
recipient: /^#/.test(to) ? to : null,
|
||||
recipient: /^#/.test(to) ? null : to,
|
||||
body,
|
||||
type: 'message',
|
||||
}, bot, games));
|
||||
|
||||
17
src/play.js
17
src/play.js
@@ -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,8 +121,13 @@ 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(roomId || recipient, curatedBody);
|
||||
bot.client.say(/^#/.test(roomId) ? roomId : recipient, curatedBody);
|
||||
}
|
||||
|
||||
if (config.platform === 'schat') {
|
||||
|
||||
72
src/schat.js
72
src/schat.js
@@ -17,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,
|
||||
}, {
|
||||
@@ -35,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()}`);
|
||||
@@ -60,8 +63,27 @@ async function onRooms({ rooms }, bot) {
|
||||
const usersRes = await bhttp.get(`${config.api}/room/${rooms.map((room) => room.id).join(',')}/users`);
|
||||
const users = usersRes.body;
|
||||
|
||||
const userIdsByRoom = Object.values(users).reduce((acc, user) => {
|
||||
user.sharedRooms.forEach((roomId) => {
|
||||
if (!acc[roomId]) {
|
||||
acc[roomId] = [];
|
||||
}
|
||||
|
||||
acc[roomId].push(user.id);
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
/* eslint-disable no-param-reassign */
|
||||
bot.rooms = rooms.reduce((acc, room) => ({ ...acc, [room.id]: room }), {});
|
||||
bot.rooms = rooms.reduce((acc, room) => ({
|
||||
...acc,
|
||||
[room.id]: {
|
||||
...room,
|
||||
userIds: userIdsByRoom[room.id],
|
||||
},
|
||||
}), {});
|
||||
|
||||
bot.users = { ...bot.users, ...users };
|
||||
/* eslint-enable no-param-reassign */
|
||||
|
||||
@@ -108,16 +130,20 @@ function handleError(error, socket, domain, data) {
|
||||
}
|
||||
}
|
||||
|
||||
const pongRegex = /^pong:\d+$/;
|
||||
|
||||
async function connect(bot, games) {
|
||||
const socket = { ws: { readyState: 0 } };
|
||||
const socket = {
|
||||
ws: { readyState: 0 },
|
||||
io: {},
|
||||
};
|
||||
|
||||
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}`);
|
||||
|
||||
@@ -127,7 +153,14 @@ async function connect(bot, games) {
|
||||
},
|
||||
});
|
||||
|
||||
socket.ws.on('message', async (msg) => {
|
||||
socket.ws.on('message', async (msgData) => {
|
||||
const msg = msgData.toString();
|
||||
|
||||
if (typeof msg === 'string' && pongRegex.test(msg)) {
|
||||
logger.debug(`Received pong ${msg.split(':')[1]}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const [domain, data] = JSON.parse(msg);
|
||||
|
||||
logger.debug(`Received ${domain}: ${JSON.stringify(data)}`);
|
||||
@@ -165,6 +198,21 @@ async function connect(bot, games) {
|
||||
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;
|
||||
|
||||
25
src/tools/merge-point-files.js
Normal file
25
src/tools/merge-point-files.js
Normal 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();
|
||||
35
src/tools/merge-point-users.js
Normal file
35
src/tools/merge-point-users.js
Normal 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();
|
||||
@@ -3,7 +3,12 @@
|
||||
const config = require('config');
|
||||
const style = require('./style');
|
||||
|
||||
function getLeaders(points, user, { ping = true, limit = 20, skip = [] } = {}) {
|
||||
function getLeaders(points, user, {
|
||||
ping = true,
|
||||
limit = 20,
|
||||
skip = [],
|
||||
pointsWord = 'points',
|
||||
} = {}) {
|
||||
return Object.entries(points)
|
||||
.filter(([userKey]) => !skip.includes(userKey))
|
||||
.sort(([, scoreA], [, scoreB]) => scoreB - scoreA)
|
||||
@@ -12,7 +17,7 @@ function getLeaders(points, user, { ping = true, limit = 20, skip = [] } = {}) {
|
||||
const username = userKey.split(':')[1] || userKey; // process the points file
|
||||
|
||||
if (index === 0) {
|
||||
return `${style.bold(style.yellow(`${ping || username === user.username ? config.usernamePrefix : ''}${username}`))} with ${style.bold(`${score}`)} points`;
|
||||
return `${style.bold(style.yellow(`${ping || username === user.username ? config.usernamePrefix : ''}${username}`))} with ${style.bold(`${score}`)} ${pointsWord}`;
|
||||
}
|
||||
|
||||
return `${style.bold(style.cyan(`${ping ? config.usernamePrefix : ''}${username}`))} with ${style.bold(`${score}`)} points`;
|
||||
|
||||
@@ -42,6 +42,7 @@ const styleMethods = (() => {
|
||||
return {
|
||||
...styles,
|
||||
forest: styles.green,
|
||||
orange: styles.yellow,
|
||||
code: bypass,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user