two sentence horror game
This commit is contained in:
parent
4d181c8ad8
commit
125e001e4a
|
@ -12,12 +12,14 @@
|
|||
"author": "oatmealine",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"d3-array": "^3.2.4",
|
||||
"discord.js": "^14.11.0",
|
||||
"got": "^11.8.3",
|
||||
"parse-color": "^1.0.0",
|
||||
"random-seed": "^0.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/d3-array": "^3.0.9",
|
||||
"@types/parse-color": "^1.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.27.1",
|
||||
"@typescript-eslint/parser": "^5.27.1",
|
||||
|
|
|
@ -1,6 +1,13 @@
|
|||
lockfileVersion: '6.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
dependencies:
|
||||
d3-array:
|
||||
specifier: ^3.2.4
|
||||
version: 3.2.4
|
||||
discord.js:
|
||||
specifier: ^14.11.0
|
||||
version: 14.11.0
|
||||
|
@ -15,6 +22,9 @@ dependencies:
|
|||
version: 0.3.0
|
||||
|
||||
devDependencies:
|
||||
'@types/d3-array':
|
||||
specifier: ^3.0.9
|
||||
version: 3.0.9
|
||||
'@types/parse-color':
|
||||
specifier: ^1.0.1
|
||||
version: 1.0.1
|
||||
|
@ -194,6 +204,10 @@ packages:
|
|||
'@types/responselike': 1.0.0
|
||||
dev: false
|
||||
|
||||
/@types/d3-array@3.0.9:
|
||||
resolution: {integrity: sha512-mZowFN3p64ajCJJ4riVYlOjNlBJv3hctgAY01pjw3qTnJePD8s9DZmYDzhHKvzfCYvdjwylkU38+Vdt7Cu2FDA==}
|
||||
dev: true
|
||||
|
||||
/@types/http-cache-semantics@4.0.1:
|
||||
resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==}
|
||||
dev: false
|
||||
|
@ -497,6 +511,13 @@ packages:
|
|||
which: 2.0.2
|
||||
dev: true
|
||||
|
||||
/d3-array@3.2.4:
|
||||
resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
|
||||
engines: {node: '>=12'}
|
||||
dependencies:
|
||||
internmap: 2.0.3
|
||||
dev: false
|
||||
|
||||
/debug@4.3.4:
|
||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
|
@ -887,6 +908,11 @@ packages:
|
|||
/inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
/internmap@2.0.3:
|
||||
resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
|
||||
engines: {node: '>=12'}
|
||||
dev: false
|
||||
|
||||
/is-extglob@2.1.1:
|
||||
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
|
|
@ -1,9 +1,5 @@
|
|||
import { GuildMember, Message, User, SlashCommandBuilder, Interaction } from 'discord.js';
|
||||
|
||||
const DEFAULT_EMOJI = '🪙';
|
||||
const STOP_EMOJI = '⏹️';
|
||||
const DONE_EMOJI = '👍';
|
||||
const BAD_EMOJI = '👎';
|
||||
import { GuildMember, SlashCommandBuilder, Interaction } from 'discord.js';
|
||||
import { getTextResponsePrettyPlease, randomWord, sendSegments, startGame } from '../lib/game';
|
||||
|
||||
const END_TEMPLATES = [
|
||||
'Alright! Here\'s the messages you all conjured:',
|
||||
|
@ -12,35 +8,6 @@ const END_TEMPLATES = [
|
|||
'That does it! Here\'s what you\'ve all cooked up together:'
|
||||
];
|
||||
|
||||
const RANDOM_WORDS = [
|
||||
'tarsorado', 'aboba', 'robtop', 'viprin', 'milk', 'milking station', 'radiation', 'extreme', 'glogging', 'glogged',
|
||||
'penis', 'deadlocked', 'cream', 'dragon cream', 'urine', 'communal', 'piss', 'matpat', 'big and round', 'easy',
|
||||
'cum', 'glue', 'tampon', 'contaminated water', 'centrifuge', 'inflation', 'plutonium', 'uranium', 'thorium',
|
||||
'imposter', 'sounding', '💥', '🥵', '🎊', '!!!', '...', '???', '?..', '?!', '!', '?', 'balls itch', 'robert',
|
||||
'gas leak', 'among us', 'stick a finger', 'overclock', 'breed', 'gay sex', 'breedable', 'cock vore', 'appendix',
|
||||
'mukbang', 'edging', 'onlyfans', 'productive', 'mandelbrot', 'novosibirsk', 'oops!', 'farting', 'memory leak',
|
||||
'pepsi can'
|
||||
];
|
||||
|
||||
function formatMessage(users: User[], time: number) {
|
||||
return `Starting a Markov chain game (${users.length} player${users.length !== 1 ? 's' : ''})\n`
|
||||
+ users.map(user => `- ${user.toString()}`).join('\n') + '\n'
|
||||
+ (time <= 0 ?
|
||||
'**Already started**' :
|
||||
`Starting in **${Math.ceil(time / 1000)}s** - react ${STOP_EMOJI} to begin now`
|
||||
);
|
||||
}
|
||||
|
||||
function randomWord() {
|
||||
return RANDOM_WORDS[Math.floor(Math.random() * RANDOM_WORDS.length)];
|
||||
}
|
||||
|
||||
function* chunks(arr: unknown[], n: number) {
|
||||
for (let i = 0; i < arr.length; i += n) {
|
||||
yield arr.slice(i, i + n);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('markov')
|
||||
|
@ -64,58 +31,10 @@ module.exports = {
|
|||
execute: async (interaction: Interaction, member: GuildMember) => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
let participants: User[] = [member.user];
|
||||
const context = interaction.options.getInteger('context') || 3;
|
||||
const maxIterations = interaction.options.getInteger('iterations') || 10;
|
||||
|
||||
const duration = 25_000;
|
||||
|
||||
const m = await interaction.reply({
|
||||
fetchReply: true,
|
||||
content: formatMessage(participants, duration)
|
||||
});
|
||||
|
||||
if (!(m instanceof Message)) return;
|
||||
|
||||
const emoji = m.guild?.emojis.cache.random();
|
||||
await m.react(emoji || DEFAULT_EMOJI);
|
||||
await m.react(STOP_EMOJI);
|
||||
|
||||
const started = Date.now();
|
||||
const collector = m.createReactionCollector({
|
||||
filter: (reaction, user) =>
|
||||
!user.bot && (
|
||||
(
|
||||
(emoji ? reaction.emoji.id === emoji.id : reaction.emoji.name === DEFAULT_EMOJI) &&
|
||||
!participants.find(u => user.id === u.id)
|
||||
) || (reaction.emoji.name === STOP_EMOJI && user.id === member.id)
|
||||
),
|
||||
time: duration,
|
||||
dispose: true
|
||||
});
|
||||
|
||||
const updateInterval = setInterval(() => {
|
||||
m.edit(formatMessage(participants, duration - (Date.now() - started)));
|
||||
}, 3_000);
|
||||
|
||||
collector.on('collect', (reaction, user) => {
|
||||
if (reaction.emoji.name === STOP_EMOJI) {
|
||||
collector.stop();
|
||||
} else {
|
||||
participants.push(user);
|
||||
m.edit(formatMessage(participants, duration - (Date.now() - started)));
|
||||
}
|
||||
});
|
||||
|
||||
collector.on('remove', (_, user) => {
|
||||
participants = participants.filter(u => u.id !== user.id);
|
||||
m.edit(formatMessage(participants, duration - (Date.now() - started)));
|
||||
});
|
||||
|
||||
collector.on('end', async () => {
|
||||
clearInterval(updateInterval);
|
||||
m.edit(formatMessage(participants, 0));
|
||||
|
||||
startGame(interaction, member.user, 'Markov game', async (participants, channel) => {
|
||||
let sentences: string[][] = Array(participants.length).fill(0);
|
||||
sentences = sentences.map(() => []);
|
||||
let iterations = 0;
|
||||
|
@ -125,25 +44,10 @@ module.exports = {
|
|||
await Promise.all(
|
||||
participants.map(async (p, i) => {
|
||||
const sentence = sentences[(i + iterations) % sentences.length];
|
||||
const msg = await p.send((`Continue the following sentence: [${iterations}/${maxIterations}]\n\n> _${context < sentence.length ? '…' : ''}${sentence.length > 0 ? sentence.slice(-context).join(' ') : `start a sentence... (try working with: “${randomWord()}”)`}_` + (iterations === 0 ? '\n\n**Send a message to continue**' : '')).slice(0, 2000));
|
||||
try {
|
||||
const collected = await msg.channel.awaitMessages({
|
||||
max: 1,
|
||||
time: 45_000,
|
||||
errors: ['time'],
|
||||
filter: (msg) => {
|
||||
const valid = msg.content !== '' && msg.content.length <= 2000;
|
||||
if (!valid) msg.react(BAD_EMOJI);
|
||||
return valid;
|
||||
}
|
||||
});
|
||||
const message = collected.first() as Message;
|
||||
sentence.push(...message.content.split(' '));
|
||||
await message.react(DONE_EMOJI);
|
||||
} catch (err) {
|
||||
await p.send('Took too long... Surprise... Added...... :)');
|
||||
sentence.push(...randomWord().split(' '));
|
||||
}
|
||||
const prompt = (`Continue the following sentence: [${iterations}/${maxIterations}]\n\n> _${context < sentence.length ? '…' : ''}${sentence.length > 0 ? sentence.slice(-context).join(' ') : `start a sentence... (try working with: “${randomWord()}”)`}_` + (iterations === 0 ? '\n\n**Send a message to continue**' : '')).slice(0, 2000);
|
||||
|
||||
const resp = await getTextResponsePrettyPlease(p, prompt);
|
||||
sentence.push(...resp.split(' '));
|
||||
})
|
||||
);
|
||||
|
||||
|
@ -162,35 +66,7 @@ module.exports = {
|
|||
'\n\nThank you for participating :)'
|
||||
];
|
||||
|
||||
const content = [];
|
||||
let contentBuffer = '';
|
||||
|
||||
while (segments.length > 0) {
|
||||
const segment = segments.splice(0, 1)[0];
|
||||
const newMsg = contentBuffer + '\n' + segment;
|
||||
if (newMsg.length > 2000) {
|
||||
content.push(contentBuffer);
|
||||
contentBuffer = '';
|
||||
|
||||
if (segment.length > 2000) {
|
||||
content.push(...([...(chunks(segment.split(''), 2000))].map(s => s.join(''))));
|
||||
} else {
|
||||
contentBuffer = segment;
|
||||
}
|
||||
} else {
|
||||
contentBuffer = newMsg;
|
||||
}
|
||||
}
|
||||
if (contentBuffer !== '') content.push(contentBuffer);
|
||||
|
||||
content.forEach(async content => {
|
||||
await m.channel.send({
|
||||
content: content,
|
||||
allowedMentions: {
|
||||
parse: ['users']
|
||||
}
|
||||
});
|
||||
});
|
||||
await sendSegments(segments, channel);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -0,0 +1,55 @@
|
|||
import { GuildMember, SlashCommandBuilder, Interaction } from 'discord.js';
|
||||
import { getTextResponsePrettyPlease, randomWord, sendSegments, startGame } from '../lib/game';
|
||||
import { shuffle } from 'd3-array';
|
||||
import { knownServers } from '../lib/knownServers';
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('twosentencehorror')
|
||||
.setDescription('Communally create the worst horror stories known to man'),
|
||||
|
||||
serverWhitelist: [...knownServers.fbi],
|
||||
|
||||
execute: async (interaction: Interaction, member: GuildMember) => {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
startGame(interaction, member.user, 'Markov game', async (players, channel) => {
|
||||
const firstPlayers = shuffle(players);
|
||||
const secondPlayers = [...firstPlayers.slice(1), firstPlayers[0]]; // shift by 1
|
||||
|
||||
const firstHalves = await Promise.all(firstPlayers.map(async (firstPlayer) => {
|
||||
let firstHalf = await getTextResponsePrettyPlease(
|
||||
firstPlayer,
|
||||
`Start a horror story... (1 sentence max!) (try working with: “${randomWord()}”)\n\n**Send a message to continue**`,
|
||||
(msg) => !(msg.includes('. ') || msg.includes('! ') || msg.includes('? '))
|
||||
);
|
||||
|
||||
if (!(firstHalf.endsWith('.') || firstHalf.endsWith('!') || firstHalf.endsWith('?'))) firstHalf = firstHalf + '.';
|
||||
|
||||
return firstHalf;
|
||||
}));
|
||||
|
||||
const secondHalves = await Promise.all(secondPlayers.map(async (secondPlayer, i) => {
|
||||
const firstHalf = firstHalves[i];
|
||||
|
||||
let secondHalf = await getTextResponsePrettyPlease(
|
||||
secondPlayer,
|
||||
`> _${firstHalf}_\nContinue this sentence with a twist!`,
|
||||
(msg) => !(msg.includes('. ') || msg.includes('! ') || msg.includes('? '))
|
||||
);
|
||||
|
||||
if (!(secondHalf.endsWith('.') || secondHalf.endsWith('!') || secondHalf.endsWith('?'))) secondHalf = secondHalf + '.';
|
||||
|
||||
return secondHalf;
|
||||
}));
|
||||
|
||||
const segments = [
|
||||
'Here\'s the bone-chilling stories you\'ve all created:\n\n',
|
||||
...firstHalves.map((firstHalf, i) => firstHalf + ' ' + secondHalves[i]),
|
||||
'\n\nThank you for participating :)'
|
||||
];
|
||||
|
||||
await sendSegments(segments, channel);
|
||||
});
|
||||
}
|
||||
};
|
|
@ -48,7 +48,8 @@ bot.on(Events.InteractionCreate, async (interaction) => {
|
|||
try {
|
||||
await command.execute(interaction, interaction.member);
|
||||
} catch (error) {
|
||||
interaction.reply({ content: '`ERROR`', ephemeral: true });
|
||||
if (interaction.isRepliable() && !interaction.replied && !interaction.deferred) interaction.reply({ content: '`ERROR`', ephemeral: true });
|
||||
if (interaction.deferred) interaction.followUp('`ERROR`');
|
||||
console.error(error);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
import { Interaction, Message, TextBasedChannel, User } from 'discord.js';
|
||||
|
||||
export const RANDOM_WORDS = [
|
||||
'tarsorado', 'aboba', 'robtop', 'viprin', 'milk', 'milking station', 'radiation', 'extreme', 'glogging', 'glogged',
|
||||
'penis', 'deadlocked', 'cream', 'dragon cream', 'urine', 'communal', 'piss', 'matpat', 'big and round', 'easy',
|
||||
'cum', 'glue', 'tampon', 'contaminated water', 'centrifuge', 'inflation', 'plutonium', 'uranium', 'thorium',
|
||||
'imposter', 'sounding', '💥', '🥵', '🎊', '!!!', '...', '???', '?..', '?!', '!', '?', 'balls itch', 'robert',
|
||||
'gas leak', 'among us', 'stick a finger', 'overclock', 'breed', 'gay sex', 'breedable', 'cock vore', 'appendix',
|
||||
'mukbang', 'edging', 'onlyfans', 'productive', 'mandelbrot', 'novosibirsk', 'oops!', 'farting', 'memory leak',
|
||||
'pepsi can'
|
||||
];
|
||||
|
||||
export function randomWord() {
|
||||
return RANDOM_WORDS[Math.floor(Math.random() * RANDOM_WORDS.length)];
|
||||
}
|
||||
|
||||
const DONE_EMOJI = '👍';
|
||||
const BAD_EMOJI = '👎';
|
||||
|
||||
const DEFAULT_EMOJI = '🪙';
|
||||
const STOP_EMOJI = '⏹️';
|
||||
|
||||
function formatMessage(users: User[], time: number, name: string, ended = false) {
|
||||
return `Starting a **${name}** game (${users.length} player${users.length !== 1 ? 's' : ''})\n`
|
||||
+ users.map(user => `- ${user.toString()}`).join('\n') + '\n'
|
||||
+ (time <= 0 ?
|
||||
(ended ? '**Already ended!**' : '**Already started**') :
|
||||
`Starting in **${Math.ceil(time / 1000)}s** - react ${STOP_EMOJI} to begin now`
|
||||
);
|
||||
}
|
||||
|
||||
export async function startGame(interaction: Interaction, startingUser: User, name: string, callback: (players: User[], channel: TextBasedChannel) => Promise<void>) {
|
||||
if (!interaction.isChatInputCommand()) return;
|
||||
|
||||
let participants: User[] = [startingUser];
|
||||
const duration = 25_000;
|
||||
|
||||
const m = await interaction.reply({
|
||||
fetchReply: true,
|
||||
content: formatMessage(participants, duration, name)
|
||||
});
|
||||
|
||||
if (!(m instanceof Message)) return;
|
||||
|
||||
const emoji = m.guild?.emojis.cache.random();
|
||||
await m.react(emoji || DEFAULT_EMOJI);
|
||||
await m.react(STOP_EMOJI);
|
||||
|
||||
const started = Date.now();
|
||||
const collector = m.createReactionCollector({
|
||||
filter: (reaction, user) =>
|
||||
!user.bot && (
|
||||
(
|
||||
(emoji ? reaction.emoji.id === emoji.id : reaction.emoji.name === DEFAULT_EMOJI) &&
|
||||
!participants.find(u => user.id === u.id)
|
||||
) || (reaction.emoji.name === STOP_EMOJI && user.id === startingUser.id)
|
||||
),
|
||||
time: duration,
|
||||
dispose: true
|
||||
});
|
||||
|
||||
const updateInterval = setInterval(() => {
|
||||
m.edit(formatMessage(participants, duration - (Date.now() - started), name));
|
||||
}, 3_000);
|
||||
|
||||
collector.on('collect', (reaction, user) => {
|
||||
if (reaction.emoji.name === STOP_EMOJI) {
|
||||
collector.stop();
|
||||
} else {
|
||||
participants.push(user);
|
||||
m.edit(formatMessage(participants, duration - (Date.now() - started), name));
|
||||
}
|
||||
});
|
||||
|
||||
collector.on('remove', (_, user) => {
|
||||
participants = participants.filter(u => u.id !== user.id);
|
||||
m.edit(formatMessage(participants, duration - (Date.now() - started), name));
|
||||
});
|
||||
|
||||
collector.on('end', async () => {
|
||||
clearInterval(updateInterval);
|
||||
m.edit(formatMessage(participants, 0, name));
|
||||
|
||||
await callback(participants, m.channel);
|
||||
|
||||
m.edit(formatMessage(participants, 0, name, true));
|
||||
});
|
||||
}
|
||||
|
||||
export async function getTextResponse(user: User, prompt: string, filter: (content: string) => boolean = () => true): Promise<string | null> {
|
||||
const msg = await user.send(prompt);
|
||||
try {
|
||||
const collected = await msg.channel.awaitMessages({
|
||||
max: 1,
|
||||
time: 45_000,
|
||||
errors: ['time'],
|
||||
filter: (msg) => {
|
||||
const valid = msg.content !== '' && msg.content.length <= 2000 && filter(msg.content);
|
||||
if (!valid) msg.react(BAD_EMOJI);
|
||||
return valid;
|
||||
}
|
||||
});
|
||||
const message = collected.first() as Message;
|
||||
await message.react(DONE_EMOJI);
|
||||
return message.content;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
export async function getTextResponsePrettyPlease(user: User, prompt: string, filter: (content: string) => boolean = () => true): Promise<string> {
|
||||
const resp = await getTextResponse(user, prompt, filter);
|
||||
if (resp) return resp;
|
||||
user.send('Took too long... Surprise... Added...... :)');
|
||||
return randomWord();
|
||||
}
|
||||
|
||||
function* chunks(arr: unknown[], n: number) {
|
||||
for (let i = 0; i < arr.length; i += n) {
|
||||
yield arr.slice(i, i + n);
|
||||
}
|
||||
}
|
||||
|
||||
export async function sendSegments(segments: string[], channel: TextBasedChannel) {
|
||||
const content = [];
|
||||
let contentBuffer = '';
|
||||
|
||||
while (segments.length > 0) {
|
||||
const segment = segments.splice(0, 1)[0];
|
||||
const newMsg = contentBuffer + '\n' + segment;
|
||||
if (newMsg.length > 2000) {
|
||||
content.push(contentBuffer);
|
||||
contentBuffer = '';
|
||||
|
||||
if (segment.length > 2000) {
|
||||
content.push(...([...(chunks(segment.split(''), 2000))].map(s => s.join(''))));
|
||||
} else {
|
||||
contentBuffer = segment;
|
||||
}
|
||||
} else {
|
||||
contentBuffer = newMsg;
|
||||
}
|
||||
}
|
||||
if (contentBuffer !== '') content.push(contentBuffer);
|
||||
|
||||
content.forEach(async content => {
|
||||
await channel.send({
|
||||
content: content,
|
||||
allowedMentions: {
|
||||
parse: ['users']
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue