From 93aa16417b208f00c374a28e15512dcb776b607c Mon Sep 17 00:00:00 2001 From: w1dering Date: Fri, 27 Sep 2024 22:36:26 -0400 Subject: [PATCH 1/5] initial commit for counting game added COUNTING_CHANNEL_ID requirement to vars.json users must work together to count as high as possible coins will be awarded based on the highest number counted to --- src/events/messageCreate.ts | 87 +++++++++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 3 deletions(-) diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index e7156965..139f52c1 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -14,18 +14,28 @@ import { PDFDocument } from 'pdf-lib'; import { Logger } from 'winston'; import { applyBonusByUserId } from '../components/coin'; import { vars } from '../config'; -import { sendKickEmbed } from '../utils/embeds'; +import { sendKickEmbed, DEFAULT_EMBED_COLOUR } from '../utils/embeds'; import { convertPdfToPic } from '../utils/pdfToPic'; import { openDB } from '../components/db'; import { spawnSync } from 'child_process'; +import { User } from 'discord.js'; +import { getCoinEmoji } from '../components/emojis'; const ANNOUNCEMENTS_CHANNEL_ID: string = vars.ANNOUNCEMENTS_CHANNEL_ID; const RESUME_CHANNEL_ID: string = vars.RESUME_CHANNEL_ID; +const COUNTING_CHANNEL_ID: string = vars.COUNTING_CHANNEL_ID; const IRC_USER_ID: string = vars.IRC_USER_ID; const PDF_FILE_PATH = 'tmp/resume.pdf'; const HEIC_FILE_PATH = 'tmp/img.heic'; const CONVERTED_IMG_PATH = 'tmp/img.jpg'; +// Variables and constants associated with the counting game +const coinsPerMessage: number = 0.1; // Number of coins awarded = coinsPerMessage * highest counting number * messages sent by user +const countingAuthorDelay: number = 1; // The minimum number of users that must count for someone to go again +const previousCountingAuthors: Array = []; +const authorMessageCounts: Map = new Map(); +let currentCountingNumber: number = 1; + /* * If honeypot is to exist again, then add HONEYPOT_CHANNEL_ID to the config * and add a check for a message's channel ID being equal to HONEYPOT_CHANNEL_ID @@ -93,13 +103,12 @@ const convertResumePdfsIntoImages = async ( message: Message, ): Promise | undefined> => { const attachment = message.attachments.first(); - const hasAttachment = attachment; const isPDF = attachment && attachment.contentType === 'application/pdf'; const isImage = attachment && attachment.contentType && attachment.contentType.startsWith('image'); // If no resume pdf is provided, nuke message and DM user about why their message got nuked - if (!(hasAttachment && (isPDF || isImage))) { + if (!(attachment && (isPDF || isImage))) { const user = message.author.id; const channel = message.channelId; @@ -200,6 +209,74 @@ const convertResumePdfsIntoImages = async ( } }; +const countingGameLogic = async ( + client: Client, + message: Message, +): Promise | undefined> => { + + // Check to see if game should end + let reasonForFailure = ''; + if (isNaN(Number(message.content))) { // Message was not a number + reasonForFailure = `"${message.content}" is not a number!`; + } + else if (previousCountingAuthors.find((author) => author === message.author)) { // Author is still on cooldown + reasonForFailure = `<@${message.author.id}> counted too recently!`; + } + else if (Number(message.content) != currentCountingNumber) { // Wrong number was sent + reasonForFailure = `${message.content} is not the next number! The next number was ${currentCountingNumber}.`; + } + + if (reasonForFailure) { + return endCountingGame(client, message, reasonForFailure); + } + + // If checks passed, continue the game + currentCountingNumber++; + message.react('✅'); + previousCountingAuthors.unshift(message.author); // Add current author to list of authors on cooldown + while (previousCountingAuthors.length > countingAuthorDelay) { + previousCountingAuthors.pop(); // Remove last author from cooldown + } + const currentAuthorCount: number | undefined = authorMessageCounts.get(message.author); + authorMessageCounts.set(message.author, currentAuthorCount ? currentAuthorCount + 1 : 1); + + return; +} + +const endCountingGame = async ( + client: Client, + message: Message, + reasonForFailure: string +): Promise | undefined> => { + const sortedAuthorMessageCounts: Array<[User, number]> = Array.from(authorMessageCounts).sort((a, b) => b[1] - a[1]); // Turns map into descending sorted array + const coinsAwarded: Array = ['**Coins awarded:**']; + sortedAuthorMessageCounts.forEach((pair) => { + pair[1] *= coinsPerMessage * currentCountingNumber; // Changes number of messages sent to number of coins awarded + coinsAwarded.push(`<@${pair[0].id}> - ${pair[1]} ${getCoinEmoji()}`); + }); + + // ** REMEMBER TO ACTUALLY AWARD COINS + + const endGameEmbed = new EmbedBuilder() + .setColor(DEFAULT_EMBED_COLOUR) + .setTitle('Counting Game Over') + .setDescription(coinsAwarded.join('\n')); + endGameEmbed.addFields([ + { + name: 'Reason for Game Over', + value: reasonForFailure, + }, + ]); + + currentCountingNumber = 1; + message.react('❌'); + previousCountingAuthors.length = 0; + authorMessageCounts.clear(); + + return await message.channel?.send({embeds: [endGameEmbed]}); +}; + + export const initMessageCreate = async ( client: Client, logger: Logger, @@ -219,6 +296,10 @@ export const initMessageCreate = async ( await convertResumePdfsIntoImages(client, message); } + if (message.channelId === COUNTING_CHANNEL_ID) { + await countingGameLogic(client, message); + } + // Ignore DMs; include announcements, thread, and regular text channels if (message.channel.type !== ChannelType.DM) { await applyBonusByUserId(message.author.id); From aa8822a82f4f7f0d5990766bef55c9138478507a Mon Sep 17 00:00:00 2001 From: w1dering Date: Fri, 27 Sep 2024 22:59:07 -0400 Subject: [PATCH 2/5] added adjusting coin balance for game and minimum threshold a certain number must be reached in order for coins to be awarded coins will now be awarded to all participants KNOWN ERRORS: floating point issue for numbers when coins are being awarded --- src/commandDetails/coin/leaderboard.ts | 2 +- src/components/coin.ts | 1 + src/events/messageCreate.ts | 39 ++++++++++++++++---------- 3 files changed, 26 insertions(+), 16 deletions(-) diff --git a/src/commandDetails/coin/leaderboard.ts b/src/commandDetails/coin/leaderboard.ts index 25c24f31..2d0d0354 100644 --- a/src/commandDetails/coin/leaderboard.ts +++ b/src/commandDetails/coin/leaderboard.ts @@ -25,7 +25,7 @@ const getCoinLeaderboardEmbed = async ( // Initialize user's coin balance if they have not already const userBalance = await getCoinBalanceByUserId(userId); let previousBalance = -1; - let position = 0; + let position = 0; let rank = 0; let offset = 0; let i = 0; diff --git a/src/components/coin.ts b/src/components/coin.ts index 328443b4..18844359 100644 --- a/src/components/coin.ts +++ b/src/components/coin.ts @@ -28,6 +28,7 @@ export enum UserCoinEvent { BonusActivity, BonusInterviewerList, Blackjack, + Counting, RpsLoss, RpsDrawAgainstCodey, RpsWin, diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 139f52c1..2119f613 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -20,6 +20,7 @@ import { openDB } from '../components/db'; import { spawnSync } from 'child_process'; import { User } from 'discord.js'; import { getCoinEmoji } from '../components/emojis'; +import { adjustCoinBalanceByUserId, UserCoinEvent } from '../components/coin'; const ANNOUNCEMENTS_CHANNEL_ID: string = vars.ANNOUNCEMENTS_CHANNEL_ID; const RESUME_CHANNEL_ID: string = vars.RESUME_CHANNEL_ID; @@ -32,8 +33,9 @@ const CONVERTED_IMG_PATH = 'tmp/img.jpg'; // Variables and constants associated with the counting game const coinsPerMessage: number = 0.1; // Number of coins awarded = coinsPerMessage * highest counting number * messages sent by user const countingAuthorDelay: number = 1; // The minimum number of users that must count for someone to go again -const previousCountingAuthors: Array = []; -const authorMessageCounts: Map = new Map(); +const previousCountingAuthors: Array = []; // Stores the most recent counters +const authorMessageCounts: Map = new Map(); // Stores how many messages each user sent +const coinAwardNumberThreshold: number = 20; // The minimum number that must be reached for coins to be awarded let currentCountingNumber: number = 1; /* @@ -248,26 +250,33 @@ const endCountingGame = async ( message: Message, reasonForFailure: string ): Promise | undefined> => { - const sortedAuthorMessageCounts: Array<[User, number]> = Array.from(authorMessageCounts).sort((a, b) => b[1] - a[1]); // Turns map into descending sorted array - const coinsAwarded: Array = ['**Coins awarded:**']; - sortedAuthorMessageCounts.forEach((pair) => { - pair[1] *= coinsPerMessage * currentCountingNumber; // Changes number of messages sent to number of coins awarded - coinsAwarded.push(`<@${pair[0].id}> - ${pair[1]} ${getCoinEmoji()}`); - }); - - // ** REMEMBER TO ACTUALLY AWARD COINS - + // Builds game over embed const endGameEmbed = new EmbedBuilder() - .setColor(DEFAULT_EMBED_COLOUR) - .setTitle('Counting Game Over') - .setDescription(coinsAwarded.join('\n')); - endGameEmbed.addFields([ + .setColor(DEFAULT_EMBED_COLOUR) + .setTitle('Counting Game Over') + .addFields([ { name: 'Reason for Game Over', value: reasonForFailure, }, ]); + if (currentCountingNumber < coinAwardNumberThreshold) { + endGameEmbed.setDescription(`Coins will not be awarded because the threshold, ${coinAwardNumberThreshold}, was not reached.`); + } + else + { + const sortedAuthorMessageCounts: Array<[User, number]> = Array.from(authorMessageCounts).sort((a, b) => b[1] - a[1]); // Turns map into descending sorted array + const coinsAwarded: Array = ['**Coins awarded:**']; + for (let pair of sortedAuthorMessageCounts) { + pair[1] *= coinsPerMessage * currentCountingNumber; // Changes number of messages sent to number of coins awarded + coinsAwarded.push(`<@${pair[0].id}> - ${pair[1]} ${getCoinEmoji()}`); + await adjustCoinBalanceByUserId(message.author.id, pair[1], UserCoinEvent.Counting); + } + + endGameEmbed.setDescription(coinsAwarded.join('\n')); + } + currentCountingNumber = 1; message.react('❌'); previousCountingAuthors.length = 0; From a6dc11392522170cfcedf330120c79c7d59a5d13 Mon Sep 17 00:00:00 2001 From: w1dering Date: Sat, 28 Sep 2024 01:12:03 -0400 Subject: [PATCH 3/5] ran linter --- src/commandDetails/coin/leaderboard.ts | 2 +- src/events/messageCreate.ts | 57 +++++++++++++------------- 2 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/commandDetails/coin/leaderboard.ts b/src/commandDetails/coin/leaderboard.ts index 2d0d0354..25c24f31 100644 --- a/src/commandDetails/coin/leaderboard.ts +++ b/src/commandDetails/coin/leaderboard.ts @@ -25,7 +25,7 @@ const getCoinLeaderboardEmbed = async ( // Initialize user's coin balance if they have not already const userBalance = await getCoinBalanceByUserId(userId); let previousBalance = -1; - let position = 0; + let position = 0; let rank = 0; let offset = 0; let i = 0; diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 2119f613..33b74bf6 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -31,12 +31,12 @@ const HEIC_FILE_PATH = 'tmp/img.heic'; const CONVERTED_IMG_PATH = 'tmp/img.jpg'; // Variables and constants associated with the counting game -const coinsPerMessage: number = 0.1; // Number of coins awarded = coinsPerMessage * highest counting number * messages sent by user -const countingAuthorDelay: number = 1; // The minimum number of users that must count for someone to go again +const coinsPerMessage = 0.1; // Number of coins awarded = coinsPerMessage * highest counting number * messages sent by user +const countingAuthorDelay = 1; // The minimum number of users that must count for someone to go again const previousCountingAuthors: Array = []; // Stores the most recent counters const authorMessageCounts: Map = new Map(); // Stores how many messages each user sent -const coinAwardNumberThreshold: number = 20; // The minimum number that must be reached for coins to be awarded -let currentCountingNumber: number = 1; +const coinAwardNumberThreshold = 20; // The minimum number that must be reached for coins to be awarded +let currentCountingNumber = 1; /* * If honeypot is to exist again, then add HONEYPOT_CHANNEL_ID to the config @@ -215,16 +215,16 @@ const countingGameLogic = async ( client: Client, message: Message, ): Promise | undefined> => { - // Check to see if game should end let reasonForFailure = ''; - if (isNaN(Number(message.content))) { // Message was not a number + if (isNaN(Number(message.content))) { + // Message was not a number reasonForFailure = `"${message.content}" is not a number!`; - } - else if (previousCountingAuthors.find((author) => author === message.author)) { // Author is still on cooldown + } else if (previousCountingAuthors.find((author) => author === message.author)) { + // Author is still on cooldown reasonForFailure = `<@${message.author.id}> counted too recently!`; - } - else if (Number(message.content) != currentCountingNumber) { // Wrong number was sent + } else if (Number(message.content) != currentCountingNumber) { + // Wrong number was sent reasonForFailure = `${message.content} is not the next number! The next number was ${currentCountingNumber}.`; } @@ -243,37 +243,39 @@ const countingGameLogic = async ( authorMessageCounts.set(message.author, currentAuthorCount ? currentAuthorCount + 1 : 1); return; -} +}; const endCountingGame = async ( client: Client, message: Message, - reasonForFailure: string + reasonForFailure: string, ): Promise | undefined> => { // Builds game over embed const endGameEmbed = new EmbedBuilder() .setColor(DEFAULT_EMBED_COLOUR) .setTitle('Counting Game Over') .addFields([ - { - name: 'Reason for Game Over', - value: reasonForFailure, - }, - ]); + { + name: 'Reason for Game Over', + value: reasonForFailure, + }, + ]); if (currentCountingNumber < coinAwardNumberThreshold) { - endGameEmbed.setDescription(`Coins will not be awarded because the threshold, ${coinAwardNumberThreshold}, was not reached.`); - } - else - { - const sortedAuthorMessageCounts: Array<[User, number]> = Array.from(authorMessageCounts).sort((a, b) => b[1] - a[1]); // Turns map into descending sorted array - const coinsAwarded: Array = ['**Coins awarded:**']; - for (let pair of sortedAuthorMessageCounts) { + endGameEmbed.setDescription( + `Coins will not be awarded because the threshold, ${coinAwardNumberThreshold}, was not reached.`, + ); + } else { + const sortedAuthorMessageCounts: Array<[User, number]> = Array.from(authorMessageCounts).sort( + (a, b) => b[1] - a[1], + ); // Turns map into descending sorted array + const coinsAwarded: Array = ['**Coins awarded:**']; + for (const pair of sortedAuthorMessageCounts) { pair[1] *= coinsPerMessage * currentCountingNumber; // Changes number of messages sent to number of coins awarded coinsAwarded.push(`<@${pair[0].id}> - ${pair[1]} ${getCoinEmoji()}`); await adjustCoinBalanceByUserId(message.author.id, pair[1], UserCoinEvent.Counting); } - + endGameEmbed.setDescription(coinsAwarded.join('\n')); } @@ -281,10 +283,9 @@ const endCountingGame = async ( message.react('❌'); previousCountingAuthors.length = 0; authorMessageCounts.clear(); - - return await message.channel?.send({embeds: [endGameEmbed]}); -}; + return await message.channel?.send({ embeds: [endGameEmbed] }); +}; export const initMessageCreate = async ( client: Client, From 99cad69208f65c86bc40042798215d42609d6117 Mon Sep 17 00:00:00 2001 From: w1dering Date: Thu, 10 Oct 2024 14:38:44 -0400 Subject: [PATCH 4/5] completed suggestions, added cap to number of coins earned suggestions have been completed: constants have been refactored - counting channel id added to vars template - currentCountingNumber gets decremented on game end as well, now each individual user can only the MINIMUM of the following: - 2 coins per number counted - 20 coins per message sent - the normal calculation formula this should plateau the number of coins earned as the numbers increase this is also to prevent very active users from gaining a large amount of coins, and very inactive players sending a single message in a high-count game and earning hundreds of coins e.g., in a game where the count is 1000, - a user that sent 100 messages will earn 2000 coins, as opposed to normal 10000 - a user that sends one single message will earn 20 coins, as opposed to 100 it should be noted that, once the counting number is very high (500+), the standard formula will no longer be relevant, and each user will be capped from either of the two aforementioned methods (which may or may not be desirable) --- config/vars.template.json | 3 ++- src/events/messageCreate.ts | 22 +++++++++++++++------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/config/vars.template.json b/config/vars.template.json index d21050bc..8caa4706 100644 --- a/config/vars.template.json +++ b/config/vars.template.json @@ -10,5 +10,6 @@ "OFFICE_STATUS_CHANNEL_ID": "CHANNEL_ID", "RESUME_CHANNEL_ID": "CHANNEL_ID", "IRC_USER_ID": "USER_ID", - "MOD_USER_ID_FOR_BAN_APPEAL": "USER_ID" + "MOD_USER_ID_FOR_BAN_APPEAL": "USER_ID", + "COUNTING_CHANNEL_ID": "CHANNEL_ID" } diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 33b74bf6..0be7fe83 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -31,11 +31,13 @@ const HEIC_FILE_PATH = 'tmp/img.heic'; const CONVERTED_IMG_PATH = 'tmp/img.jpg'; // Variables and constants associated with the counting game -const coinsPerMessage = 0.1; // Number of coins awarded = coinsPerMessage * highest counting number * messages sent by user -const countingAuthorDelay = 1; // The minimum number of users that must count for someone to go again +const COINS_PER_MESSAGE = 0.1; // Number of coins awarded = COINS_PER_MESSAGE * highest counting number * number of messages sent by user +const COUNTING_AUTHOR_DELAY = 1; // The minimum number of users that must count for someone to go again const previousCountingAuthors: Array = []; // Stores the most recent counters const authorMessageCounts: Map = new Map(); // Stores how many messages each user sent -const coinAwardNumberThreshold = 20; // The minimum number that must be reached for coins to be awarded +const COIN_AWARD_NUMBER_THRESHOLD: number = 20; // The minimum number that must be reached for coins to be awarded +const MAX_COINS_PER_NUMBER_COUNTED: number = 2; // The maximum number of coins a user can receive every 100 numbers counted +const MAX_COINS_PER_MESSAGE_SENT: number = 20; let currentCountingNumber = 1; /* @@ -236,7 +238,7 @@ const countingGameLogic = async ( currentCountingNumber++; message.react('✅'); previousCountingAuthors.unshift(message.author); // Add current author to list of authors on cooldown - while (previousCountingAuthors.length > countingAuthorDelay) { + while (previousCountingAuthors.length > COUNTING_AUTHOR_DELAY) { previousCountingAuthors.pop(); // Remove last author from cooldown } const currentAuthorCount: number | undefined = authorMessageCounts.get(message.author); @@ -250,6 +252,7 @@ const endCountingGame = async ( message: Message, reasonForFailure: string, ): Promise | undefined> => { + currentCountingNumber--; // since the current counting number wasn't reached, decrement the value // Builds game over embed const endGameEmbed = new EmbedBuilder() .setColor(DEFAULT_EMBED_COLOUR) @@ -261,9 +264,9 @@ const endCountingGame = async ( }, ]); - if (currentCountingNumber < coinAwardNumberThreshold) { + if (currentCountingNumber < COIN_AWARD_NUMBER_THRESHOLD) { endGameEmbed.setDescription( - `Coins will not be awarded because the threshold, ${coinAwardNumberThreshold}, was not reached.`, + `Coins will not be awarded because the threshold, ${COIN_AWARD_NUMBER_THRESHOLD}, was not reached.`, ); } else { const sortedAuthorMessageCounts: Array<[User, number]> = Array.from(authorMessageCounts).sort( @@ -271,7 +274,12 @@ const endCountingGame = async ( ); // Turns map into descending sorted array const coinsAwarded: Array = ['**Coins awarded:**']; for (const pair of sortedAuthorMessageCounts) { - pair[1] *= coinsPerMessage * currentCountingNumber; // Changes number of messages sent to number of coins awarded + // Changes number of messages sent to number of coins awarded + // Multiplication and division of 100 should prevent floating point errors + pair[1] = Math.min(Math.round((pair[1] * Math.round(1000 * COINS_PER_MESSAGE) * currentCountingNumber) / 100) / 10, + MAX_COINS_PER_NUMBER_COUNTED * currentCountingNumber, + MAX_COINS_PER_MESSAGE_SENT * pair[1]); + coinsAwarded.push(`<@${pair[0].id}> - ${pair[1]} ${getCoinEmoji()}`); await adjustCoinBalanceByUserId(message.author.id, pair[1], UserCoinEvent.Counting); } From e64c9bbd074ffa4b94a3466a4704a4abac566cc6 Mon Sep 17 00:00:00 2001 From: w1dering Date: Thu, 10 Oct 2024 14:46:54 -0400 Subject: [PATCH 5/5] ran linter --- src/events/messageCreate.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/events/messageCreate.ts b/src/events/messageCreate.ts index 0be7fe83..041d1182 100644 --- a/src/events/messageCreate.ts +++ b/src/events/messageCreate.ts @@ -35,9 +35,9 @@ const COINS_PER_MESSAGE = 0.1; // Number of coins awarded = COINS_PER_MESSAGE * const COUNTING_AUTHOR_DELAY = 1; // The minimum number of users that must count for someone to go again const previousCountingAuthors: Array = []; // Stores the most recent counters const authorMessageCounts: Map = new Map(); // Stores how many messages each user sent -const COIN_AWARD_NUMBER_THRESHOLD: number = 20; // The minimum number that must be reached for coins to be awarded -const MAX_COINS_PER_NUMBER_COUNTED: number = 2; // The maximum number of coins a user can receive every 100 numbers counted -const MAX_COINS_PER_MESSAGE_SENT: number = 20; +const COIN_AWARD_NUMBER_THRESHOLD = 20; // The minimum number that must be reached for coins to be awarded +const MAX_COINS_PER_NUMBER_COUNTED = 2; // The maximum number of coins a user can receive every 100 numbers counted +const MAX_COINS_PER_MESSAGE_SENT = 20; let currentCountingNumber = 1; /* @@ -276,10 +276,13 @@ const endCountingGame = async ( for (const pair of sortedAuthorMessageCounts) { // Changes number of messages sent to number of coins awarded // Multiplication and division of 100 should prevent floating point errors - pair[1] = Math.min(Math.round((pair[1] * Math.round(1000 * COINS_PER_MESSAGE) * currentCountingNumber) / 100) / 10, - MAX_COINS_PER_NUMBER_COUNTED * currentCountingNumber, - MAX_COINS_PER_MESSAGE_SENT * pair[1]); - + pair[1] = Math.min( + Math.round((pair[1] * Math.round(1000 * COINS_PER_MESSAGE) * currentCountingNumber) / 100) / + 10, + MAX_COINS_PER_NUMBER_COUNTED * currentCountingNumber, + MAX_COINS_PER_MESSAGE_SENT * pair[1], + ); + coinsAwarded.push(`<@${pair[0].id}> - ${pair[1]} ${getCoinEmoji()}`); await adjustCoinBalanceByUserId(message.author.id, pair[1], UserCoinEvent.Counting); }