Compare commits
37 Commits
42c850ac90
...
socketio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
59424af388 | ||
|
|
39a27f501e | ||
|
|
ffdfefa6d4 | ||
|
|
b3ab3bf1ba | ||
|
|
2e54d38383 | ||
|
|
98a1aa8fff | ||
|
|
6f2a5e03e9 | ||
|
|
9966c79a26 | ||
|
|
a46ffb431b | ||
|
|
b9be447dba | ||
|
|
a45d00c105 | ||
|
|
00b92f445e | ||
|
|
09e78a8bbb | ||
|
|
44d8304aa0 | ||
|
|
8e16f189f5 | ||
|
|
c85ec3440b | ||
|
|
650a8d2ec9 | ||
|
|
d263c42d38 | ||
|
|
fcd1597707 | ||
|
|
27a82b9cdd | ||
|
|
b791147ce0 | ||
|
|
29ce536ffd | ||
|
|
e584389453 | ||
|
|
ba8e39f857 | ||
|
|
44386cf096 | ||
|
|
21feb37d21 | ||
|
|
85be251b3f | ||
|
|
73117e7614 | ||
|
|
750997f3cc | ||
|
|
da9c85d90c | ||
|
|
ee50935339 | ||
|
|
2860630921 | ||
|
|
6c5fd6ed0a | ||
|
|
c43be9df13 | ||
|
|
8aef44b2d9 | ||
|
|
77b6ebfb5c | ||
|
|
6817f51b83 |
@@ -34,7 +34,7 @@ async function init() {
|
||||
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) => definition.split('.')[0].toLowerCase());
|
||||
|
||||
if (!acc[anagram.length]) {
|
||||
acc[anagram.length] = {};
|
||||
|
||||
@@ -1,4 +1,45 @@
|
||||
{
|
||||
"anthropomorphologically": "With regard to anthropomorphology.",
|
||||
"blepharosphincterectomy": "Excision of part of the orbicularis palpebrarum to relieve pressure of the eyelid on the cornea.",
|
||||
"carboxymethylcelluloses": "An acid ether derivative of cellulose that in the form of its sodium salt is used as a thickening, emulsifying, and stabilizing agent and as a bulk laxative in medicine.",
|
||||
"chlorotrifluoroethylene": "Flammable, colourless gas that belongs to the family of organic halogen compounds, used in the manufacture of a series of synthetic oils, greases, waxes, elastomers, and plastics that are unusually resistant to attack by chemicals and heat.",
|
||||
"deinstitutionalization": "Discharge (a long-term inmate) from an institution such as a psychiatric hospital or prison.",
|
||||
"desoxyribonucleoprotein": null,
|
||||
"dichlorodifluoromethane": null,
|
||||
"dihdroxycholecalciferol": null,
|
||||
"disestablismentarianism": null,
|
||||
"electroencephalographer": null,
|
||||
"electroencephalographic": null,
|
||||
"electrophotomicrography": null,
|
||||
"epididymodeferentectomy": null,
|
||||
"formaldehydesulphoxylic": null,
|
||||
"gastroenteroanastomosis": null,
|
||||
"hematospectrophotometer": null,
|
||||
"hexamethylenetetramine": null,
|
||||
"hydrochlorofluorocarbon": null,
|
||||
"hypobetalipoproteinemia": null,
|
||||
"indistinguishableness": null,
|
||||
"intersubstitutability": null,
|
||||
"macracanthrorhynchiasis": null,
|
||||
"magnetohydrodynamically": null,
|
||||
"microspectrophotometer": null,
|
||||
"microspectrophotometric": null,
|
||||
"nonrepresentationalism": null,
|
||||
"overindividualistically": null,
|
||||
"overintellectualization": null,
|
||||
"pancreaticoduodenostomy": null,
|
||||
"pathologicohistological": null,
|
||||
"pericardiomediastinitis": null,
|
||||
"phenolsulphonephthalein": null,
|
||||
"philosophicotheological": null,
|
||||
"polytetrafluoroethylene": null,
|
||||
"pseudolamellibranchiata": null,
|
||||
"pseudolamellibranchiate": null,
|
||||
"pseudophilanthropically": null,
|
||||
"reinstitutionalization": "The act or process of institutionalizing someone or something again.",
|
||||
"scientificogeographical": null,
|
||||
"thymolsulphonephthalein": null,
|
||||
"transubstantiationalist": null,
|
||||
"anopheles" : "A genus of mosquitoes which are secondary hosts of the malaria parasites, and whose bite is the usual, if not the only, means of infecting human beings with malaria. Several species are found in the United States. They may be distinguished from the ordinary mosquitoes of the genus Culex by the long slender palpi, nearly equaling the beak in length, while those of the female Culex are very short. They also assume different positions when resting, Culex usually holding the body parallel to the surface on which it rests and keeping the head and beak bent at an angle, while Anopheles holds the body at an angle with the surface and the head and beak in line with it. Unless they become themselves infected by previously biting a subject affected with malaria, the insects cannot transmit the disease.",
|
||||
"uniclinal" : "See Nonoclinal.",
|
||||
"sarong" : "A sort of petticoat worn by both sexes in Java and the Malay Archipelago. Balfour (Cyc. of India)",
|
||||
@@ -102216,4 +102257,4 @@
|
||||
"prudential" : "1. Proceeding from, or dictated or characterized by, prudence; prudent; discreet; sometimes, selfish or pecuniary as distinguished from higher motives or influences; as, prudential motives. \" A prudential line of conduct.\" Sir W. Scott. 2. Exercising prudence; discretionary; advisory; superintending or executive; as, a prudential committee.\n\nThat which relates to or demands the exercise of, discretion or prudence; -- usually in the pl. Many stanzas, in poetic measures, contain rules relating to common prudentials as well as to religion. I. Watts.",
|
||||
"obviation" : "The act of obviating, or the state of being obviated.",
|
||||
"silkness" : "Silkiness. [Obs.] B. Jonson."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: '~',
|
||||
@@ -100,11 +100,43 @@ module.exports = {
|
||||
letters: {
|
||||
length: 9,
|
||||
timeout: 60,
|
||||
consonantBias: 0.56, // for auto fill
|
||||
// http://www.thecountdownpage.com/letters.htm
|
||||
vowels: {
|
||||
a: 15,
|
||||
e: 21,
|
||||
i: 13,
|
||||
o: 13,
|
||||
u: 5,
|
||||
},
|
||||
consonants: {
|
||||
b: 2,
|
||||
c: 3,
|
||||
d: 6,
|
||||
f: 2,
|
||||
g: 3,
|
||||
h: 2,
|
||||
j: 1,
|
||||
k: 1,
|
||||
l: 5,
|
||||
m: 4,
|
||||
n: 8,
|
||||
p: 4,
|
||||
q: 1,
|
||||
r: 9,
|
||||
s: 9,
|
||||
t: 9,
|
||||
v: 1,
|
||||
w: 1,
|
||||
x: 1,
|
||||
y: 1,
|
||||
z: 1,
|
||||
},
|
||||
},
|
||||
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.25.2",
|
||||
"version": "1.26.12",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "schat2-clive",
|
||||
"version": "1.25.2",
|
||||
"version": "1.26.12",
|
||||
"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.25.2",
|
||||
"version": "1.26.12",
|
||||
"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",
|
||||
|
||||
@@ -7,9 +7,11 @@ const style = require('../utils/style');
|
||||
const knex = require('../knex');
|
||||
|
||||
const promptRegex = new RegExp(`^${config.usernamePrefix ? `${config.usernamePrefix}?` : ''}${config.user.username}[:,\\s]`, 'ui');
|
||||
const history = new Map();
|
||||
|
||||
const settings = config.chat;
|
||||
const settings = { ...config.chat };
|
||||
|
||||
const history = new Map();
|
||||
const processing = new Set();
|
||||
|
||||
async function onStart(context) {
|
||||
const totalExists = await knex.schema.hasTable('chat_tokens');
|
||||
@@ -40,7 +42,7 @@ async function onStart(context) {
|
||||
|
||||
function setHistory(value, context) {
|
||||
if (!value) {
|
||||
context.sendMessage(`Chat history is set to ${style.bold(settings.history)}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Chat history is set to ${style.bold(settings.history)}`, context.room?.id, { label: false }, context.user.username);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -50,17 +52,17 @@ function setHistory(value, context) {
|
||||
settings.history = newHistory;
|
||||
|
||||
context.logger.info(`Chat history set to ${newHistory} by ${context.user.username}`);
|
||||
context.sendMessage(`Chat history set to ${style.bold(newHistory)} by ${context.user.prefixedUsername}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Chat history set to ${style.bold(newHistory)} by ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
context.sendMessage(`Chat history must be a valid number between 0 and 10, ${context.user.prefixedUsername}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Chat history must be a valid number between 0 and 10, ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
|
||||
}
|
||||
|
||||
function setReplyWordLimit(value, context) {
|
||||
if (!value) {
|
||||
context.sendMessage(`Chat reply word limit is set to ${style.bold(settings.replyWordLimit)}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Chat reply word limit is set to ${style.bold(settings.replyWordLimit)}`, context.room?.id, { label: false }, context.user.username);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -70,17 +72,17 @@ function setReplyWordLimit(value, context) {
|
||||
settings.replyWordLimit = newReplyWordLimit;
|
||||
|
||||
context.logger.info(`Chat reply word limit set to ${newReplyWordLimit} by ${context.user.username}`);
|
||||
context.sendMessage(`Chat reply word limit set to ${style.bold(newReplyWordLimit)} by ${context.user.prefixedUsername}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Chat reply word limit set to ${style.bold(newReplyWordLimit)} by ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
context.sendMessage(`Chat reply word limit must be a valid number between 3 and 200, ${context.user.prefixedUsername}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Chat reply word limit must be a valid number between 3 and 200, ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
|
||||
}
|
||||
|
||||
function setTemperature(value, context) {
|
||||
if (!value) {
|
||||
context.sendMessage(`Chat temperature is set to ${style.bold(settings.temperature)}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Chat temperature is set to ${style.bold(settings.temperature)}`, context.room?.id, { label: false }, context.user.username);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -90,17 +92,17 @@ function setTemperature(value, context) {
|
||||
settings.temperature = newTemperature;
|
||||
|
||||
context.logger.info(`Chat temperature set to ${newTemperature} by ${context.user.username}`);
|
||||
context.sendMessage(`Chat temperature set to ${style.bold(newTemperature)} by ${context.user.prefixedUsername}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Chat temperature set to ${style.bold(newTemperature)} by ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
context.sendMessage(`Chat temperature must be a valid number between 0 and 2, ${context.user.prefixedUsername}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Chat temperature must be a valid number between 0 and 2, ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
|
||||
}
|
||||
|
||||
function setModel(model, context) {
|
||||
if (!model) {
|
||||
context.sendMessage(`Chat model is set to ${style.bold(settings.model)}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Chat model is set to ${style.bold(settings.model)}`, context.room?.id, { label: false }, context.user.username);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -108,17 +110,17 @@ function setModel(model, context) {
|
||||
settings.model = model;
|
||||
|
||||
context.logger.info(`Chat model set to '${model}' by ${context.user.username}`);
|
||||
context.sendMessage(`Chat model set to '${style.bold(model)}' by ${context.user.prefixedUsername}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Chat model set to '${style.bold(model)}' by ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
context.sendMessage(`Model '${model}' is not supported right now, ${context.user.prefixedUsername}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Model '${model}' is not supported right now, ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
|
||||
}
|
||||
|
||||
function setRule(rule, context) {
|
||||
if (!rule) {
|
||||
context.sendMessage(`Chat is ${style.bold(settings.rule)}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Chat is ${style.bold(settings.rule)}`, context.room?.id, { label: false }, context.user.username);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -126,7 +128,7 @@ function setRule(rule, context) {
|
||||
settings.rule = config.chat.rule;
|
||||
|
||||
context.logger.info(`Chat reset by ${context.user.username} to be ${config.chat.rule}`);
|
||||
context.sendMessage(`Chat reset by ${context.user.prefixedUsername} to be ${style.bold(config.chat.rule)}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Chat reset by ${context.user.prefixedUsername} to be ${style.bold(config.chat.rule)}`, context.room?.id, { label: false }, context.user.username);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -135,12 +137,12 @@ function setRule(rule, context) {
|
||||
settings.rule = `${rule.replace(/\.+$/, '')}`;
|
||||
|
||||
context.logger.info(`Chat set by ${context.user.username} to be ${rule}`);
|
||||
context.sendMessage(`Chat set by ${context.user.prefixedUsername} to be ${style.bold(rule)}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Chat set by ${context.user.prefixedUsername} to be ${style.bold(rule)}`, context.room?.id, { label: false }, context.user.username);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
context.sendMessage(`Chat rule must be at least 3 characters long, ${context.user.prefixedUsername}`, context.room.id, { label: false });
|
||||
context.sendMessage(`Chat rule must be at least 3 characters long, ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username);
|
||||
}
|
||||
|
||||
async function getTokens(username) {
|
||||
@@ -163,7 +165,7 @@ async function resetTokens(username) {
|
||||
.delete();
|
||||
}
|
||||
|
||||
async function onCommand(args, context) {
|
||||
async function onCommand(args, context, isConversation) {
|
||||
if (context.subcommand === 'history' && config.operators.includes(context.user.username)) {
|
||||
setHistory(args[0], context);
|
||||
return;
|
||||
@@ -193,7 +195,7 @@ async function onCommand(args, context) {
|
||||
const username = args[0] ? args[0].replace(new RegExp(`^${config.usernamePrefix}`), '') : context.user.username;
|
||||
const tokens = await getTokens(username);
|
||||
|
||||
context.sendMessage(`${args[0] ? `${style.bold(username)} has` : 'You have'} ${style.bold(config.chat.userTokenLimit - tokens)} chat tokens remaining. They will be returned gradually over ${config.chat.userTokenPeriod} hours.`, context.room.id, { label: false });
|
||||
context.sendMessage(`${args[0] ? `${style.bold(username)} has` : 'You have'} ${style.bold(config.chat.userTokenLimit - tokens)} chat tokens remaining. They will be returned gradually over ${config.chat.userTokenPeriod} hours.`, context.room?.id, { label: false }, context.user.username);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -203,19 +205,26 @@ async function onCommand(args, context) {
|
||||
|
||||
await resetTokens(username);
|
||||
|
||||
context.sendMessage(args[0] ? `Chat tokens reset for ${style.bold(username)}` : 'Chat tokens reset', context.room.id, { label: false });
|
||||
context.sendMessage(args[0] ? `Chat tokens reset for ${style.bold(username)}` : 'Chat tokens reset', context.room?.id, { label: false }, context.user.username);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (processing.has(context.user.username)) {
|
||||
context.logger.info(`Skipped prompt from ${context.user.username} due processing lock`);
|
||||
return;
|
||||
}
|
||||
|
||||
const prompt = args.join(' ');
|
||||
|
||||
processing.add(context.user.username);
|
||||
|
||||
try {
|
||||
const usedTokens = await getTokens(context.user.username);
|
||||
|
||||
if (usedTokens >= config.chat.userTokenLimit) {
|
||||
context.logger.info(`${context.user.username} was rate limited at ${usedTokens}: ${prompt}`);
|
||||
context.sendMessage(`Sorry, I love talking with you ${context.user.prefixedUsername}, but I need to take a break :( Check ${config.prefix}chat:tokens for more information.`, context.room.id, { label: false });
|
||||
context.sendMessage(`Sorry, I love talking with you ${context.user.prefixedUsername}, but I need to take a break :( Check ${config.prefix}chat:tokens for more information.`, context.room?.id, { label: false }, context.user.username);
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -244,10 +253,16 @@ async function onCommand(args, context) {
|
||||
});
|
||||
|
||||
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.sendMessage(`${context.user.prefixedUsername}: ${curatedContent}`, context.room.id, { label: false });
|
||||
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(`${target ? `${config.usernamePrefix}${target}` : context.user.prefixedUsername}: ${curatedContent}`, context.room.id, { label: false });
|
||||
}
|
||||
|
||||
await knex('chat_tokens').insert({
|
||||
user_id: context.user.username,
|
||||
@@ -260,13 +275,17 @@ async function onCommand(args, context) {
|
||||
}
|
||||
} catch (error) {
|
||||
context.logger.error(error.response ? `${error.response.status}: ${JSON.stringify(error.response.data)}` : error.message);
|
||||
context.sendMessage('Sorry, I can\'t think right now', context.room.id, { label: false });
|
||||
context.sendMessage('Sorry, I can\'t think right now', context.room?.id, { label: false }, context.user.username);
|
||||
}
|
||||
|
||||
processing.delete(context.user.username);
|
||||
}
|
||||
|
||||
function onMessage(message, context) {
|
||||
if (promptRegex.test(message.body)) {
|
||||
onCommand([message.body.replace(promptRegex, '').trim()], context);
|
||||
const isConversation = message.recipient === config.user.username && message.type !== 'whisper' && !context.containsCommand;
|
||||
|
||||
if (promptRegex.test(message.body) || isConversation) {
|
||||
onCommand([message.body.replace(promptRegex, '').trim()], context, isConversation);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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' });
|
||||
|
||||
@@ -97,7 +97,7 @@ async function onCommand(args, context) {
|
||||
|
||||
async function onMessage(message, context) {
|
||||
['countries', 'states', 'flags'].forEach(async (type) => {
|
||||
const game = games[type].get(context.room.id);
|
||||
const game = games[type].get(context.room?.id);
|
||||
|
||||
if (!game) {
|
||||
return;
|
||||
|
||||
@@ -145,7 +145,7 @@ function onCommand(args, context) {
|
||||
}
|
||||
|
||||
function onMessage(message, context) {
|
||||
if (games.has(context.room.id)) {
|
||||
if (games.has(context.room?.id)) {
|
||||
const letter = message.body.match(/^\s*\w\s*$/)?.[0];
|
||||
const word = message.body.match(/^\s*\w{2,}\s*$/)?.[0];
|
||||
|
||||
|
||||
@@ -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}`);
|
||||
|
||||
@@ -11,36 +11,8 @@ const words = require('../../assets/mash-words.json');
|
||||
|
||||
// http://www.thecountdownpage.com/letters.htm
|
||||
const availableLetters = {
|
||||
vowel: [
|
||||
...Array.from({ length: 15 }, () => 'a'),
|
||||
...Array.from({ length: 21 }, () => 'e'),
|
||||
...Array.from({ length: 13 }, () => 'i'),
|
||||
...Array.from({ length: 13 }, () => 'o'),
|
||||
...Array.from({ length: 5 }, () => 'u'),
|
||||
],
|
||||
consonant: [
|
||||
...Array.from({ length: 2 }, () => 'b'),
|
||||
...Array.from({ length: 3 }, () => 'c'),
|
||||
...Array.from({ length: 6 }, () => 'd'),
|
||||
...Array.from({ length: 2 }, () => 'f'),
|
||||
...Array.from({ length: 3 }, () => 'g'),
|
||||
...Array.from({ length: 2 }, () => 'h'),
|
||||
...Array.from({ length: 1 }, () => 'j'),
|
||||
...Array.from({ length: 1 }, () => 'k'),
|
||||
...Array.from({ length: 5 }, () => 'l'),
|
||||
...Array.from({ length: 4 }, () => 'm'),
|
||||
...Array.from({ length: 8 }, () => 'n'),
|
||||
...Array.from({ length: 4 }, () => 'p'),
|
||||
...Array.from({ length: 1 }, () => 'q'),
|
||||
...Array.from({ length: 9 }, () => 'r'),
|
||||
...Array.from({ length: 9 }, () => 's'),
|
||||
...Array.from({ length: 9 }, () => 't'),
|
||||
...Array.from({ length: 1 }, () => 'v'),
|
||||
...Array.from({ length: 1 }, () => 'w'),
|
||||
...Array.from({ length: 1 }, () => 'x'),
|
||||
...Array.from({ length: 1 }, () => 'y'), // Countdown regards Y as a consonant
|
||||
...Array.from({ length: 1 }, () => 'z'),
|
||||
],
|
||||
vowel: Object.entries(config.letters.vowels).flatMap(([vowel, repeat]) => Array.from({ length: repeat }, () => vowel)),
|
||||
consonant: Object.entries(config.letters.consonants).flatMap(([consonant, repeat]) => Array.from({ length: repeat }, () => consonant)),
|
||||
};
|
||||
|
||||
const types = { v: 'vowel', c: 'consonant' };
|
||||
@@ -116,7 +88,7 @@ async function play(context) {
|
||||
game.state = 'words';
|
||||
game.counts = countLetters(game.word);
|
||||
|
||||
context.sendMessage(`${getBoard(context)} You have ${style.bold(style.green(settings.timeout))}, let's start!`, context.room.id);
|
||||
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 });
|
||||
@@ -185,7 +157,7 @@ function onCommand(args, context) {
|
||||
}
|
||||
|
||||
if (context.command === 'letsgo' || ['start', 'go', 'auto', 'random'].includes(context.subcommand)) {
|
||||
start(context, Array.from({ length: config.letters.length }, () => (Math.random() < 0.55 ? 'c' : 'v')).join('')); // a slight bias towards consonants seems to give better boards
|
||||
start(context, Array.from({ length: config.letters.length }, () => (Math.random() < config.letters.consonantBias ? 'c' : 'v')).join('')); // a slight bias towards consonants seems to give better boards
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -202,7 +174,7 @@ function onMessage(message, context) {
|
||||
}
|
||||
|
||||
if (game?.state === 'letters') {
|
||||
const multi = message.body.match(/\b[vc]{2,}\b/i)?.[0];
|
||||
const multi = message.body.match(/\b[vc]{2,}$/i)?.[0];
|
||||
|
||||
if (multi) {
|
||||
pickLetters(multi.toLowerCase(), context);
|
||||
|
||||
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,50 +1,46 @@
|
||||
/* 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 });
|
||||
|
||||
const settings = config.numbers;
|
||||
const settings = { ...config.numbers };
|
||||
const games = new Map();
|
||||
|
||||
/* eslint-disable no-irregular-whitespace */
|
||||
@@ -89,6 +85,24 @@ 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;
|
||||
@@ -102,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!`;
|
||||
}
|
||||
@@ -181,7 +195,7 @@ async function play(context) {
|
||||
}
|
||||
}
|
||||
|
||||
function pickBig() {
|
||||
function pickLarge() {
|
||||
return pickRandom([100, 75, 50, 25]);
|
||||
}
|
||||
|
||||
@@ -196,17 +210,17 @@ function pickNumbers(type, context) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'big') {
|
||||
game.numbers = game.numbers.concat(pickBig());
|
||||
if (type === 'large') {
|
||||
game.numbers = game.numbers.concat(pickLarge());
|
||||
}
|
||||
|
||||
if (type === 'small') {
|
||||
game.numbers = game.numbers.concat(pickSmall());
|
||||
}
|
||||
|
||||
if (type !== 'small' && type !== 'big') {
|
||||
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' ? pickBig() : pickSmall());
|
||||
game.numbers = game.numbers.concat(['b', 'l', 't'].includes(typeKey) ? pickLarge() : pickSmall());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -215,15 +229,27 @@ function pickNumbers(type, context) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.sendMessage(`${getBoard(context)} Would you like a big number or a small one?`, context.room.id);
|
||||
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 = 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);
|
||||
@@ -278,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;
|
||||
}
|
||||
|
||||
@@ -307,12 +333,12 @@ function start(context, numbers) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.sendMessage('Let\'s play the numbers! Would you like a big number or a small one?', context.room.id);
|
||||
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 = 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);
|
||||
@@ -327,6 +353,11 @@ function onCommand(args, context) {
|
||||
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;
|
||||
@@ -343,23 +374,23 @@ function onCommand(args, context) {
|
||||
}
|
||||
|
||||
function onMessage(message, context) {
|
||||
const game = games.get(context.room.id);
|
||||
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[bls]{2,}\b/i)?.[0];
|
||||
const multi = body.match(/\b[blts]{2,}$/i)?.[0];
|
||||
|
||||
if (multi) {
|
||||
pickNumbers(multi.toLowerCase(), context);
|
||||
return;
|
||||
}
|
||||
|
||||
if (/big/i.test(body)) {
|
||||
pickNumbers('big', context);
|
||||
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;
|
||||
}
|
||||
@@ -373,6 +404,6 @@ function onMessage(message, context) {
|
||||
module.exports = {
|
||||
onCommand,
|
||||
onMessage,
|
||||
commands: ['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} 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
|
||||
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
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ async function init() {
|
||||
client.addListener('message', (from, to, body) => onMessage({
|
||||
from,
|
||||
to,
|
||||
recipient: /^#/.test(to) ? null : to,
|
||||
body,
|
||||
type: 'message',
|
||||
}, bot, games));
|
||||
|
||||
@@ -118,14 +118,14 @@ async function getGames(bot, identifier) {
|
||||
const curatedBody = curateMessageBody(body, game, key, options);
|
||||
|
||||
if (config.platform === 'irc') {
|
||||
bot.client.say(roomId || recipient, curatedBody);
|
||||
bot.client.say(/^#/.test(roomId) ? roomId : recipient, curatedBody);
|
||||
}
|
||||
|
||||
if (config.platform === 'schat') {
|
||||
bot.socket.transmit('message', {
|
||||
roomId,
|
||||
recipient,
|
||||
type: recipient && options?.type !== 'message' ? 'whisper' : 'message',
|
||||
recipient: options?.type === 'whisper' || !roomId ? recipient : null,
|
||||
type: options?.type || 'message',
|
||||
body: curatedBody,
|
||||
style: { ...config.style, ...options?.style },
|
||||
});
|
||||
@@ -248,6 +248,8 @@ function onMessage(message, bot, games) {
|
||||
...game,
|
||||
bot,
|
||||
message,
|
||||
containsCommand: command,
|
||||
containsSubcommand: subcommand,
|
||||
user: user && {
|
||||
...user,
|
||||
points: points[game.key]?.[`${user.id}:${user.username}`] || 0,
|
||||
|
||||
61
src/schat.js
61
src/schat.js
@@ -3,7 +3,8 @@
|
||||
const config = require('config');
|
||||
const { setTimeout: delay } = require('timers/promises');
|
||||
const bhttp = require('bhttp');
|
||||
const WebSocket = require('ws');
|
||||
// const WebSocket = require('ws');
|
||||
const io = require('socket.io-client');
|
||||
const logger = require('simple-node-logger').createSimpleLogger();
|
||||
const { argv } = require('yargs');
|
||||
|
||||
@@ -60,8 +61,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 */
|
||||
|
||||
@@ -109,7 +129,10 @@ function handleError(error, socket, domain, data) {
|
||||
}
|
||||
|
||||
async function connect(bot, games) {
|
||||
const socket = { ws: { readyState: 0 } };
|
||||
const socket = {
|
||||
ws: { readyState: 0 },
|
||||
io: {},
|
||||
};
|
||||
|
||||
socket.connect = async () => {
|
||||
try {
|
||||
@@ -121,13 +144,29 @@ async function connect(bot, games) {
|
||||
|
||||
logger.info(`Attempting to connect to ${config.socket}`);
|
||||
|
||||
socket.ws = new WebSocket(`${config.socket}?${new URLSearchParams({ v: wsCreds.wsId, t: wsCreds.timestamp }).toString()}`, [], {
|
||||
headers: {
|
||||
const { origin, pathname } = new URL(config.socket);
|
||||
|
||||
socket.io = io(origin, {
|
||||
transports: ['websocket'],
|
||||
path: pathname,
|
||||
query: {
|
||||
v: wsCreds.wsId,
|
||||
t: wsCreds.timestamp,
|
||||
},
|
||||
extraHeaders: {
|
||||
cookie: sessionCookie,
|
||||
},
|
||||
});
|
||||
|
||||
socket.ws.on('message', async (msg) => {
|
||||
socket.io.on('connect', () => {
|
||||
logger.info(`Connected to ${config.socket}`);
|
||||
});
|
||||
|
||||
socket.io.on('connect_error', (error) => {
|
||||
logger.info(`Failed to connect to ${config.socket}: ${error}`);
|
||||
});
|
||||
|
||||
socket.io.on('_', async (msg) => {
|
||||
const [domain, data] = JSON.parse(msg);
|
||||
|
||||
logger.debug(`Received ${domain}: ${JSON.stringify(data)}`);
|
||||
@@ -141,18 +180,12 @@ async function connect(bot, games) {
|
||||
}
|
||||
});
|
||||
|
||||
socket.ws.on('close', async (info) => {
|
||||
socket.io.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}`);
|
||||
|
||||
@@ -162,7 +195,7 @@ async function connect(bot, games) {
|
||||
};
|
||||
|
||||
socket.transmit = (domain, data) => {
|
||||
socket.ws.send(JSON.stringify([domain, data]));
|
||||
socket.io.emit('_', JSON.stringify([domain, data]));
|
||||
};
|
||||
|
||||
socket.connect();
|
||||
|
||||
@@ -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`;
|
||||
|
||||
Reference in New Issue
Block a user