two sentence horror game

This commit is contained in:
Jill 2023-10-29 12:22:23 +03:00
parent 4d181c8ad8
commit 125e001e4a
Signed by: oat
GPG Key ID: 33489AA58A955108
6 changed files with 246 additions and 133 deletions

View File

@ -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",

View File

@ -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'}

View File

@ -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);
});
}
};

View File

@ -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);
});
}
};

View File

@ -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);
}
});

153
src/lib/game.ts Normal file
View File

@ -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']
}
});
});
}