This commit is contained in:
2025-05-03 11:46:44 +02:00
parent a6dd54edd9
commit d7c3ea5c7d
4576 changed files with 440347 additions and 0 deletions

17
src/boredBot/commands.ts Normal file
View File

@ -0,0 +1,17 @@
import { SlashCommandBuilder } from "discord.js";
import { Command } from "../discordBot/types";
export const boredBotCommands = [
new SlashCommandBuilder()
.setName(Command.Roll)
.setDescription("Roll dice (e.g. 'd20', '6d12-4', '2d8 + 1d6+4')")
.addStringOption((option) =>
option
.setName("input")
.setDescription("The dice you want to roll")
.setRequired(true),
),
new SlashCommandBuilder()
.setName(Command.Help)
.setDescription("Displays a list of available commands."),
];

123
src/boredBot/index.ts Normal file
View File

@ -0,0 +1,123 @@
import type { SlashCommandOptionsOnlyBuilder } from "discord.js";
import { DiceRoller } from "../diceRoller";
import type { DiceParseResult } from "../diceRoller/types";
import { parseDiceUserInput } from "../diceRoller/utils/parseDiceUserInput";
import { DiscordBot } from "../discordBot";
import { Command, MessageContent } from "../discordBot/types";
export class BoredBot extends DiscordBot {
private static instance: BoredBot;
private constructor(
token: string,
applicationId: string,
commands: SlashCommandOptionsOnlyBuilder[],
) {
super(token, applicationId, commands);
this.useCommand();
}
public static async getInstance(
token: string,
applicationId: string,
commands: SlashCommandOptionsOnlyBuilder[],
): Promise<BoredBot> {
if (!BoredBot.instance) {
BoredBot.instance = new BoredBot(token, applicationId, commands);
await BoredBot.instance.initialize("Bored Bot");
}
return BoredBot.instance;
}
private async useCommand() {
this.client.on("interactionCreate", async (interaction) => {
if (!interaction.isChatInputCommand()) return;
const command = interaction.commandName as Command;
const userName = interaction.user.displayName;
const userInput = interaction.options.getString(MessageContent.Input);
if (!userInput) throw new Error("Expected input to be defined");
console.log("got command");
switch (command) {
case Command.Roll: {
await interaction.reply(
await this.handleRollCommand(userInput, userName),
);
}
}
});
}
private async handleRollCommand(
inputStringFromUser: string,
username: string,
): Promise<string> {
let parsedInputResult: DiceParseResult = { dices: [], mod: 0 };
try {
parsedInputResult = parseDiceUserInput(inputStringFromUser);
} catch (error) {
if (error instanceof Error) return error.message;
}
const roller = new DiceRoller(username);
const resultsMessages: string[] = [];
if (parsedInputResult.dices.length === 1) {
const singleDie = parsedInputResult.dices[0];
const { rollResult, message } = await roller.roll(singleDie);
const mod = parsedInputResult.mod;
const finalResult = rollResult + mod;
let singleLine = `(${singleDie.toString()}) => ${rollResult}`;
if (mod !== 0) {
const sign = mod > 0 ? "+" : "-";
singleLine += ` ${sign}${Math.abs(mod)} = ${finalResult}`;
}
if (message) {
singleLine += `\n${message}`;
}
resultsMessages.push(singleLine);
} else {
resultsMessages.push(
`You rolled ${parsedInputResult.dices.length} dice!`,
);
let sum = 0;
let rollCount = 1;
for (const dieInput of parsedInputResult.dices) {
const { rollResult, message } = await roller.roll(dieInput);
sum += rollResult;
const prefix = `Roll #${rollCount}: `;
const suffix = message ? ` **${message}**` : "";
resultsMessages.push(
`${prefix}(d${dieInput.toString()}) => ${rollResult}${suffix}`,
);
rollCount++;
}
const mod = parsedInputResult.mod;
const finalResult = sum + mod;
if (mod !== 0) {
const sign = mod > 0 ? "+" : "-";
resultsMessages.push(
`Result: ${sum} ${sign} ${Math.abs(mod)} = ${finalResult}`,
);
} else {
resultsMessages.push(`**Final result: ${sum}**`);
}
}
return resultsMessages.join("\n");
}
}

53
src/diceRoller/index.ts Normal file
View File

@ -0,0 +1,53 @@
import { Critical, Dice } from "./types";
export class DiceRoller {
private username: string;
constructor(user: string) {
this.username = user;
}
public async roll(
dice: (typeof Dice)[keyof typeof Dice],
): Promise<{ rollResult: number; message?: string }> {
if (!Object.values(Dice).includes(dice))
throw new Error(`Invalid dice type: ${dice}`);
const { outcome: result, crit } = this.getSingleDiceRollOutcome(dice);
if (crit.failure || crit.success) {
return {
rollResult: result,
message: crit.success
? await this.handleCritical(Critical.Success)
: await this.handleCritical(Critical.Fail),
};
}
return { rollResult: result };
}
private getSingleDiceRollOutcome(dice: (typeof Dice)[keyof typeof Dice]): {
outcome: number;
crit: { failure: boolean; success: boolean };
} {
const roll = Math.floor(Math.random() * dice) + 1;
switch (dice) {
case Dice.Twenty:
return {
outcome: roll,
crit: { failure: roll === 1, success: roll === 20 },
};
default:
return { outcome: roll, crit: { failure: false, success: false } };
}
}
private async handleCritical(critical: Critical): Promise<string> {
switch (critical) {
case Critical.Fail:
return "Critical FAIL!";
case Critical.Success:
return "Critical SUCCESS!";
}
}
}

18
src/diceRoller/types.ts Normal file
View File

@ -0,0 +1,18 @@
export const Dice = {
Four: 4,
Six: 6,
Ten: 10,
Twelve: 12,
Twenty: 20,
Hundrer: 100,
};
export enum Critical {
Fail = "fail",
Success = "success",
}
export type DiceParseResult = {
dices: number[];
mod: number;
};

View File

@ -0,0 +1,71 @@
import { DiceRoller } from "../discord/dice";
import { parseDiceUserInput } from "./parseDiceUserInput";
import type { DiceParseResult } from "../types";
export async function handleRollCommand(
inputStringFromUser: string,
username: string,
): Promise<string> {
let parsedInputResult: DiceParseResult = { dices: [], mod: 0 };
try {
parsedInputResult = parseDiceUserInput(inputStringFromUser);
} catch (error) {
if (error instanceof Error) return error.message;
}
const roller = new DiceRoller(username);
const resultsMessages: string[] = [];
if (parsedInputResult.dices.length === 1) {
const singleDie = parsedInputResult.dices[0];
const { rollResult, message } = await roller.roll(singleDie);
const mod = parsedInputResult.mod;
const finalResult = rollResult + mod;
let singleLine = `(${singleDie.toString()}) => ${rollResult}`;
if (mod !== 0) {
const sign = mod > 0 ? "+" : "-";
singleLine += ` ${sign}${Math.abs(mod)} = ${finalResult}`;
}
if (message) {
singleLine += `\n${message}`;
}
resultsMessages.push(singleLine);
} else {
resultsMessages.push(`You rolled ${parsedInputResult.dices.length} dice!`);
let sum = 0;
let rollCount = 1;
for (const dieInput of parsedInputResult.dices) {
const { rollResult, message } = await roller.roll(dieInput);
sum += rollResult;
const prefix = `Roll #${rollCount}: `;
const suffix = message ? ` **${message}**` : "";
resultsMessages.push(
`${prefix}(d${dieInput.toString()}) => ${rollResult}${suffix}`,
);
rollCount++;
}
const mod = parsedInputResult.mod;
const finalResult = sum + mod;
if (mod !== 0) {
const sign = mod > 0 ? "+" : "-";
resultsMessages.push(
`Result: ${sum} ${sign} ${Math.abs(mod)} = ${finalResult}`,
);
} else {
resultsMessages.push(`**Final result: ${sum}**`);
}
}
return resultsMessages.join("\n");
}

View File

@ -0,0 +1,69 @@
import { Dice } from "../types";
const ALLOWED_DICE_SIDES = new Set(Object.values(Dice));
export function parseDiceUserInput(input: string): DiceParseResult {
if (typeof input !== "string" || !input.trim()) {
throw new Error("Input must be a non-empty string.");
}
let clean = input.toLowerCase().trim();
clean = clean.replace(/\b(roll|dice|die)\b/g, "");
clean = clean.replace(/\s+/g, "");
if (/^\d+$/.test(clean)) {
const sides = Number.parseInt(clean, 10);
if (!ALLOWED_DICE_SIDES.has(sides)) {
throw new Error(
`Allowed dice sides are ${[...ALLOWED_DICE_SIDES].join(", ")}. Received: ${sides}`,
);
}
return { dices: [sides], mod: 0 };
}
const tokenRegex = /(\d*d\d+|[+\-]\d+)/g;
const tokens = clean.match(tokenRegex);
if (!tokens) {
throw new Error(
"Unable to parse any dice or modifiers. Examples: '3d6+5', 'd20-2', '2d8+1d6+4'.",
);
}
const dices: number[] = [];
let mod = 0;
for (const token of tokens) {
if (token.includes("d")) {
const [countPart, sidesPart] = token.split("d");
let diceCount = countPart ? Number.parseInt(countPart, 10) : 1;
if (Number.isNaN(diceCount) || diceCount < 1) {
diceCount = 1;
}
const sides = Number.parseInt(sidesPart, 10);
if (Number.isNaN(sides) || sides <= 0) {
throw new Error(`Invalid number of sides: "${sidesPart}"`);
}
if (!ALLOWED_DICE_SIDES.has(sides)) {
throw new Error(
`Allowed dice sides are ${[...ALLOWED_DICE_SIDES].join(", ")}. Received: ${sides}`,
);
}
for (let i = 0; i < diceCount; i++) {
dices.push(sides);
}
} else {
const parsedMod = Number.parseInt(token, 10);
if (Number.isNaN(parsedMod)) {
throw new Error(`Invalid modifier: "${token}"`);
}
mod += parsedMod;
}
}
return { dices, mod };
}

65
src/discordBot/index.ts Normal file
View File

@ -0,0 +1,65 @@
import {
Client,
GatewayIntentBits,
REST,
Routes,
type SlashCommandOptionsOnlyBuilder,
} from "discord.js";
export abstract class DiscordBot {
private token: string;
private applicationId: string;
protected client: Client;
private commands: SlashCommandOptionsOnlyBuilder[];
protected constructor(
token: string,
applicationId: string,
commands: SlashCommandOptionsOnlyBuilder[],
) {
this.client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
],
});
this.token = token;
this.applicationId = applicationId;
this.commands = commands;
}
private async login(): Promise<void> {
if (!this.token) {
throw new Error("No token provided when attempting to log in bot");
}
try {
await this.client.login(this.token);
} catch (error) {
throw new Error(`Bot failed to log in: ${error}`);
}
}
private async registerCommands(): Promise<void> {
try {
await new REST({ version: "10" })
.setToken(this.token)
.put(Routes.applicationCommands(this.applicationId), {
body: this.commands,
});
} catch (error) {
throw new Error(`Failed to register slash commands: ${error}`);
}
}
protected async initialize(botName: string): Promise<void> {
console.log(`Initializing bot '${botName}'...`);
try {
await this.login();
await this.registerCommands();
console.log(`${botName} is ready!`);
} catch (error) {
console.log("Initialization error:", error);
}
}
}

8
src/discordBot/types.ts Normal file
View File

@ -0,0 +1,8 @@
export enum Command {
Roll = "roll",
Help = "help",
}
export enum MessageContent {
Input = "input",
}

10
src/index.ts Normal file
View File

@ -0,0 +1,10 @@
// import { Elysia } from "elysia";
import { BoredBot } from "./boredBot";
import { boredBotCommands } from "./boredBot/commands";
// new Elysia().get("/", () => "Hello Elysia").listen(3000);
if (!process.env.DISCORD_BOT_TOKEN || !process.env.APPLICATION_ID) {
throw new Error("DISCORD_BOT_TOKEN and APPLICATION_ID must be defined in the environment variables.");
}
await BoredBot.getInstance(process.env.DISCORD_BOT_TOKEN, process.env.APPLICATION_ID, boredBotCommands);