Implement leveling and economy features with associated models and commands
- Added new models for leveling and economy functionalities in the Prisma schema, including LevelingConfig, MemberLevel, LevelReward, EconomyConfig, MemberEconomy, ShopItem, and InventoryItem. - Introduced commands for managing XP, economy transactions, and shop interactions, enhancing user engagement. - Updated job handling to include reminders and poll closing functionalities. - Enhanced localization support for new commands and features in both German and English. - Improved the bot's job processing capabilities with a dedicated reminder worker for scheduled tasks.
This commit is contained in:
574
apps/bot/src/modules/fun/games.ts
Normal file
574
apps/bot/src/modules/fun/games.ts
Normal file
@@ -0,0 +1,574 @@
|
||||
import {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
type APIEmbed
|
||||
} from 'discord.js';
|
||||
import { HANGMAN_WORDS, randomInt, TRIVIA_QUESTIONS, type Locale, tf } from '@nexumi/shared';
|
||||
import type { BotContext } from '../../types.js';
|
||||
|
||||
export const FUN_BUTTON_PREFIX = 'fun:';
|
||||
export const TRIVIA_TTL_SECONDS = 120;
|
||||
export const GAME_TTL_SECONDS = 3600;
|
||||
|
||||
const TRIVIA_KEY_PREFIX = 'fun:trivia:';
|
||||
const TTT_KEY_PREFIX = 'fun:ttt:';
|
||||
const C4_KEY_PREFIX = 'fun:c4:';
|
||||
const HM_KEY_PREFIX = 'fun:hm:';
|
||||
|
||||
export type TicTacToeMark = 'X' | 'O';
|
||||
export type TicTacToeCell = TicTacToeMark | null;
|
||||
|
||||
export type TicTacToeState = {
|
||||
gameId: string;
|
||||
guildId: string;
|
||||
channelId: string;
|
||||
playerX: string;
|
||||
playerO: string;
|
||||
board: TicTacToeCell[];
|
||||
current: TicTacToeMark;
|
||||
status: 'active' | 'draw' | 'won';
|
||||
winner?: TicTacToeMark;
|
||||
};
|
||||
|
||||
export type Connect4Mark = 'R' | 'Y';
|
||||
export type Connect4Cell = Connect4Mark | null;
|
||||
|
||||
export type Connect4State = {
|
||||
gameId: string;
|
||||
guildId: string;
|
||||
channelId: string;
|
||||
playerRed: string;
|
||||
playerYellow: string;
|
||||
board: Connect4Cell[][];
|
||||
current: Connect4Mark;
|
||||
status: 'active' | 'draw' | 'won';
|
||||
winner?: Connect4Mark;
|
||||
};
|
||||
|
||||
export type HangmanState = {
|
||||
gameId: string;
|
||||
guildId: string;
|
||||
userId: string;
|
||||
word: string;
|
||||
guessed: string[];
|
||||
wrongGuesses: number;
|
||||
maxWrong: number;
|
||||
status: 'active' | 'won' | 'lost';
|
||||
letterPage: number;
|
||||
};
|
||||
|
||||
export type TriviaSession = {
|
||||
userId: string;
|
||||
guildId: string;
|
||||
questionIndex: number;
|
||||
correctIndex: number;
|
||||
};
|
||||
|
||||
const TTT_LINES = [
|
||||
[0, 1, 2],
|
||||
[3, 4, 5],
|
||||
[6, 7, 8],
|
||||
[0, 3, 6],
|
||||
[1, 4, 7],
|
||||
[2, 5, 8],
|
||||
[0, 4, 8],
|
||||
[2, 4, 6]
|
||||
] as const;
|
||||
|
||||
const ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.split('');
|
||||
|
||||
export function createGameId(): string {
|
||||
return crypto.randomUUID().replace(/-/g, '').slice(0, 12);
|
||||
}
|
||||
|
||||
export function checkTicTacToeWinner(board: TicTacToeCell[]): TicTacToeMark | null {
|
||||
for (const [a, b, c] of TTT_LINES) {
|
||||
const mark = board[a];
|
||||
if (mark && mark === board[b] && mark === board[c]) {
|
||||
return mark;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function currentTicTacToePlayerId(state: TicTacToeState): string {
|
||||
return state.current === 'X' ? state.playerX : state.playerO;
|
||||
}
|
||||
|
||||
export function applyTicTacToeMove(state: TicTacToeState, cell: number): TicTacToeState {
|
||||
if (state.status !== 'active' || state.board[cell] !== null) {
|
||||
return state;
|
||||
}
|
||||
const board = [...state.board];
|
||||
board[cell] = state.current;
|
||||
const winner = checkTicTacToeWinner(board);
|
||||
if (winner) {
|
||||
return { ...state, board, status: 'won', winner };
|
||||
}
|
||||
if (board.every((value) => value !== null)) {
|
||||
return { ...state, board, status: 'draw' };
|
||||
}
|
||||
return { ...state, board, current: state.current === 'X' ? 'O' : 'X' };
|
||||
}
|
||||
|
||||
export function createConnect4Board(): Connect4Cell[][] {
|
||||
return Array.from({ length: 6 }, () => Array.from({ length: 7 }, () => null));
|
||||
}
|
||||
|
||||
function connect4DropRow(board: Connect4Cell[][], col: number): number {
|
||||
for (let row = board.length - 1; row >= 0; row -= 1) {
|
||||
if (board[row]![col] === null) {
|
||||
return row;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function connect4ColumnFull(board: Connect4Cell[][], col: number): boolean {
|
||||
return board[0]![col] !== null;
|
||||
}
|
||||
|
||||
export function applyConnect4Move(state: Connect4State, col: number): Connect4State {
|
||||
if (state.status !== 'active' || col < 0 || col >= 7 || connect4ColumnFull(state.board, col)) {
|
||||
return state;
|
||||
}
|
||||
const row = connect4DropRow(state.board, col);
|
||||
if (row < 0) {
|
||||
return state;
|
||||
}
|
||||
const board = state.board.map((line) => [...line]);
|
||||
board[row]![col] = state.current;
|
||||
const winner = checkConnect4Winner(board);
|
||||
if (winner) {
|
||||
return { ...state, board, status: 'won', winner };
|
||||
}
|
||||
if (board.every((line) => line.every((cell) => cell !== null))) {
|
||||
return { ...state, board, status: 'draw' };
|
||||
}
|
||||
return { ...state, board, current: state.current === 'R' ? 'Y' : 'R' };
|
||||
}
|
||||
|
||||
export function checkConnect4Winner(board: Connect4Cell[][]): Connect4Mark | null {
|
||||
const directions = [
|
||||
[0, 1],
|
||||
[1, 0],
|
||||
[1, 1],
|
||||
[1, -1]
|
||||
] as const;
|
||||
|
||||
for (let row = 0; row < 6; row += 1) {
|
||||
for (let col = 0; col < 7; col += 1) {
|
||||
const start = board[row]![col];
|
||||
if (!start) {
|
||||
continue;
|
||||
}
|
||||
for (const [dr, dc] of directions) {
|
||||
let matches = 1;
|
||||
for (let step = 1; step < 4; step += 1) {
|
||||
const next = board[row + dr * step]?.[col + dc * step];
|
||||
if (next !== start) {
|
||||
break;
|
||||
}
|
||||
matches += 1;
|
||||
}
|
||||
if (matches >= 4) {
|
||||
return start;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function currentConnect4PlayerId(state: Connect4State): string {
|
||||
return state.current === 'R' ? state.playerRed : state.playerYellow;
|
||||
}
|
||||
|
||||
export function pickHangmanWord(locale: Locale): string {
|
||||
const words = HANGMAN_WORDS[locale];
|
||||
return words[randomInt(0, words.length - 1)]!;
|
||||
}
|
||||
|
||||
export function renderHangmanWord(word: string, guessed: string[]): string {
|
||||
return word
|
||||
.split('')
|
||||
.map((letter) => (guessed.includes(letter) ? letter : '⬜'))
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
export function applyHangmanGuess(state: HangmanState, letter: string): HangmanState {
|
||||
const normalized = letter.toUpperCase();
|
||||
if (
|
||||
state.status !== 'active' ||
|
||||
normalized.length !== 1 ||
|
||||
!/^[A-Z]$/.test(normalized) ||
|
||||
state.guessed.includes(normalized)
|
||||
) {
|
||||
return state;
|
||||
}
|
||||
|
||||
const guessed = [...state.guessed, normalized];
|
||||
const inWord = state.word.includes(normalized);
|
||||
const wrongGuesses = inWord ? state.wrongGuesses : state.wrongGuesses + 1;
|
||||
const revealed = state.word.split('').every((char) => guessed.includes(char));
|
||||
if (revealed) {
|
||||
return { ...state, guessed, wrongGuesses, status: 'won' };
|
||||
}
|
||||
if (wrongGuesses >= state.maxWrong) {
|
||||
return { ...state, guessed, wrongGuesses, status: 'lost' };
|
||||
}
|
||||
return { ...state, guessed, wrongGuesses };
|
||||
}
|
||||
|
||||
async function saveJson(redis: BotContext['redis'], key: string, value: unknown, ttl: number): Promise<void> {
|
||||
await redis.set(key, JSON.stringify(value), 'EX', ttl);
|
||||
}
|
||||
|
||||
async function loadJson<T>(redis: BotContext['redis'], key: string): Promise<T | null> {
|
||||
const raw = await redis.get(key);
|
||||
if (!raw) {
|
||||
return null;
|
||||
}
|
||||
return JSON.parse(raw) as T;
|
||||
}
|
||||
|
||||
export async function saveTriviaSession(context: BotContext, session: TriviaSession): Promise<void> {
|
||||
await saveJson(context.redis, `${TRIVIA_KEY_PREFIX}${session.userId}`, session, TRIVIA_TTL_SECONDS);
|
||||
}
|
||||
|
||||
export async function loadTriviaSession(context: BotContext, userId: string): Promise<TriviaSession | null> {
|
||||
return loadJson<TriviaSession>(context.redis, `${TRIVIA_KEY_PREFIX}${userId}`);
|
||||
}
|
||||
|
||||
export async function deleteTriviaSession(context: BotContext, userId: string): Promise<void> {
|
||||
await context.redis.del(`${TRIVIA_KEY_PREFIX}${userId}`);
|
||||
}
|
||||
|
||||
export async function saveTicTacToeState(context: BotContext, state: TicTacToeState): Promise<void> {
|
||||
await saveJson(context.redis, `${TTT_KEY_PREFIX}${state.gameId}`, state, GAME_TTL_SECONDS);
|
||||
}
|
||||
|
||||
export async function loadTicTacToeState(context: BotContext, gameId: string): Promise<TicTacToeState | null> {
|
||||
return loadJson<TicTacToeState>(context.redis, `${TTT_KEY_PREFIX}${gameId}`);
|
||||
}
|
||||
|
||||
export async function deleteTicTacToeState(context: BotContext, gameId: string): Promise<void> {
|
||||
await context.redis.del(`${TTT_KEY_PREFIX}${gameId}`);
|
||||
}
|
||||
|
||||
export async function saveConnect4State(context: BotContext, state: Connect4State): Promise<void> {
|
||||
await saveJson(context.redis, `${C4_KEY_PREFIX}${state.gameId}`, state, GAME_TTL_SECONDS);
|
||||
}
|
||||
|
||||
export async function loadConnect4State(context: BotContext, gameId: string): Promise<Connect4State | null> {
|
||||
return loadJson<Connect4State>(context.redis, `${C4_KEY_PREFIX}${gameId}`);
|
||||
}
|
||||
|
||||
export async function deleteConnect4State(context: BotContext, gameId: string): Promise<void> {
|
||||
await context.redis.del(`${C4_KEY_PREFIX}${gameId}`);
|
||||
}
|
||||
|
||||
export async function saveHangmanState(context: BotContext, state: HangmanState): Promise<void> {
|
||||
await saveJson(context.redis, `${HM_KEY_PREFIX}${state.gameId}`, state, GAME_TTL_SECONDS);
|
||||
}
|
||||
|
||||
export async function loadHangmanState(context: BotContext, gameId: string): Promise<HangmanState | null> {
|
||||
return loadJson<HangmanState>(context.redis, `${HM_KEY_PREFIX}${gameId}`);
|
||||
}
|
||||
|
||||
export async function deleteHangmanState(context: BotContext, gameId: string): Promise<void> {
|
||||
await context.redis.del(`${HM_KEY_PREFIX}${gameId}`);
|
||||
}
|
||||
|
||||
function tttCellLabel(cell: TicTacToeCell): string {
|
||||
if (cell === 'X') return '❌';
|
||||
if (cell === 'O') return '⭕';
|
||||
return '⬜';
|
||||
}
|
||||
|
||||
export function buildTicTacToeEmbed(locale: Locale, state: TicTacToeState): APIEmbed {
|
||||
const boardLines = [0, 1, 2]
|
||||
.map((row) => state.board.slice(row * 3, row * 3 + 3).map(tttCellLabel).join(' '))
|
||||
.join('\n');
|
||||
|
||||
let description = tf(locale, 'fun.tictactoe.board', {
|
||||
x: `<@${state.playerX}>`,
|
||||
o: `<@${state.playerO}>`,
|
||||
board: boardLines
|
||||
});
|
||||
|
||||
if (state.status === 'won' && state.winner) {
|
||||
const winnerId = state.winner === 'X' ? state.playerX : state.playerO;
|
||||
description += `\n\n${tf(locale, 'fun.tictactoe.won', { user: `<@${winnerId}>` })}`;
|
||||
} else if (state.status === 'draw') {
|
||||
description += `\n\n${tf(locale, 'fun.tictactoe.draw', {})}`;
|
||||
} else {
|
||||
description += `\n\n${tf(locale, 'fun.tictactoe.turn', {
|
||||
user: `<@${currentTicTacToePlayerId(state)}>`
|
||||
})}`;
|
||||
}
|
||||
|
||||
return { title: tf(locale, 'fun.tictactoe.title', {}), description, color: 0x6366f1 };
|
||||
}
|
||||
|
||||
export function buildTicTacToeComponents(state: TicTacToeState): ActionRowBuilder<ButtonBuilder>[] {
|
||||
if (state.status !== 'active') {
|
||||
return [];
|
||||
}
|
||||
const rows: ActionRowBuilder<ButtonBuilder>[] = [];
|
||||
for (let row = 0; row < 3; row += 1) {
|
||||
const builder = new ActionRowBuilder<ButtonBuilder>();
|
||||
for (let col = 0; col < 3; col += 1) {
|
||||
const cell = row * 3 + col;
|
||||
const mark = state.board[cell];
|
||||
builder.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`${FUN_BUTTON_PREFIX}ttt:${state.gameId}:${cell}`)
|
||||
.setLabel(mark ?? String(cell + 1))
|
||||
.setStyle(mark ? ButtonStyle.Secondary : ButtonStyle.Primary)
|
||||
.setDisabled(mark !== null)
|
||||
);
|
||||
}
|
||||
rows.push(builder);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
function c4CellLabel(cell: Connect4Cell): string {
|
||||
if (cell === 'R') return '🔴';
|
||||
if (cell === 'Y') return '🟡';
|
||||
return '⚪';
|
||||
}
|
||||
|
||||
export function buildConnect4Embed(locale: Locale, state: Connect4State): APIEmbed {
|
||||
const boardText = [...state.board]
|
||||
.reverse()
|
||||
.map((row) => row.map(c4CellLabel).join(''))
|
||||
.join('\n');
|
||||
|
||||
let description = tf(locale, 'fun.connect4.board', {
|
||||
red: `<@${state.playerRed}>`,
|
||||
yellow: `<@${state.playerYellow}>`,
|
||||
board: boardText
|
||||
});
|
||||
|
||||
if (state.status === 'won' && state.winner) {
|
||||
const winnerId = state.winner === 'R' ? state.playerRed : state.playerYellow;
|
||||
description += `\n\n${tf(locale, 'fun.connect4.won', { user: `<@${winnerId}>` })}`;
|
||||
} else if (state.status === 'draw') {
|
||||
description += `\n\n${tf(locale, 'fun.connect4.draw', {})}`;
|
||||
} else {
|
||||
description += `\n\n${tf(locale, 'fun.connect4.turn', {
|
||||
user: `<@${currentConnect4PlayerId(state)}>`
|
||||
})}`;
|
||||
}
|
||||
|
||||
return { title: tf(locale, 'fun.connect4.title', {}), description, color: 0x6366f1 };
|
||||
}
|
||||
|
||||
export function buildConnect4Components(state: Connect4State): ActionRowBuilder<ButtonBuilder>[] {
|
||||
if (state.status !== 'active') {
|
||||
return [];
|
||||
}
|
||||
const row = new ActionRowBuilder<ButtonBuilder>();
|
||||
for (let col = 0; col < 7; col += 1) {
|
||||
row.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`${FUN_BUTTON_PREFIX}c4:${state.gameId}:${col}`)
|
||||
.setLabel(String(col + 1))
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(connect4ColumnFull(state.board, col))
|
||||
);
|
||||
}
|
||||
return [row];
|
||||
}
|
||||
|
||||
export function buildHangmanEmbed(locale: Locale, state: HangmanState): APIEmbed {
|
||||
let description = tf(locale, 'fun.hangman.progress', {
|
||||
word: renderHangmanWord(state.word, state.guessed),
|
||||
wrong: state.wrongGuesses,
|
||||
max: state.maxWrong,
|
||||
guessed: state.guessed.length > 0 ? state.guessed.join(', ') : '—'
|
||||
});
|
||||
|
||||
if (state.status === 'won') {
|
||||
description += `\n\n${tf(locale, 'fun.hangman.won', { word: state.word })}`;
|
||||
} else if (state.status === 'lost') {
|
||||
description += `\n\n${tf(locale, 'fun.hangman.lost', { word: state.word })}`;
|
||||
}
|
||||
|
||||
return { title: tf(locale, 'fun.hangman.title', {}), description, color: 0x6366f1 };
|
||||
}
|
||||
|
||||
function hangmanLettersForPage(state: HangmanState): { letters: string[]; page: number; totalPages: number } {
|
||||
const remaining = ALPHABET.filter((letter) => !state.guessed.includes(letter));
|
||||
const lettersPerPage = 24;
|
||||
const totalPages = Math.max(1, Math.ceil(remaining.length / lettersPerPage));
|
||||
const page = Math.min(state.letterPage, totalPages - 1);
|
||||
const start = page * lettersPerPage;
|
||||
return {
|
||||
letters: remaining.slice(start, start + lettersPerPage),
|
||||
page,
|
||||
totalPages
|
||||
};
|
||||
}
|
||||
|
||||
export function buildHangmanComponents(state: HangmanState): ActionRowBuilder<ButtonBuilder>[] {
|
||||
if (state.status !== 'active') {
|
||||
return [];
|
||||
}
|
||||
|
||||
const { letters, page, totalPages } = hangmanLettersForPage(state);
|
||||
const rows: ActionRowBuilder<ButtonBuilder>[] = [];
|
||||
|
||||
for (let i = 0; i < letters.length; i += 5) {
|
||||
const chunk = letters.slice(i, i + 5);
|
||||
const row = new ActionRowBuilder<ButtonBuilder>();
|
||||
for (const letter of chunk) {
|
||||
row.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`${FUN_BUTTON_PREFIX}hm:${state.gameId}:${letter}`)
|
||||
.setLabel(letter)
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
);
|
||||
}
|
||||
rows.push(row);
|
||||
if (rows.length >= 4) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const navRow = new ActionRowBuilder<ButtonBuilder>();
|
||||
if (totalPages > 1) {
|
||||
navRow.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`${FUN_BUTTON_PREFIX}hm:${state.gameId}:page:${Math.max(0, page - 1)}`)
|
||||
.setLabel('◀')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setDisabled(page <= 0),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`${FUN_BUTTON_PREFIX}hm:${state.gameId}:page:${Math.min(totalPages - 1, page + 1)}`)
|
||||
.setLabel('▶')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setDisabled(page >= totalPages - 1)
|
||||
);
|
||||
}
|
||||
if (navRow.components.length > 0) {
|
||||
rows.push(navRow);
|
||||
}
|
||||
|
||||
return rows.slice(0, 5);
|
||||
}
|
||||
|
||||
export function pickTriviaQuestion(): (typeof TRIVIA_QUESTIONS)[number] {
|
||||
return TRIVIA_QUESTIONS[randomInt(0, TRIVIA_QUESTIONS.length - 1)]!;
|
||||
}
|
||||
|
||||
export function buildTriviaEmbed(
|
||||
locale: Locale,
|
||||
question: (typeof TRIVIA_QUESTIONS)[number],
|
||||
userId: string
|
||||
): APIEmbed {
|
||||
const options = question.options[locale]
|
||||
.map((option, index) => `${String.fromCharCode(65 + index)}. ${option}`)
|
||||
.join('\n');
|
||||
|
||||
return {
|
||||
title: tf(locale, 'fun.trivia.title', {}),
|
||||
description: tf(locale, 'fun.trivia.question', {
|
||||
question: question.q[locale],
|
||||
options
|
||||
}),
|
||||
footer: { text: tf(locale, 'fun.trivia.footer', { user: `<@${userId}>` }) },
|
||||
color: 0x6366f1
|
||||
};
|
||||
}
|
||||
|
||||
export function buildTriviaComponents(
|
||||
userId: string,
|
||||
correctIndex: number,
|
||||
locale: Locale,
|
||||
question: (typeof TRIVIA_QUESTIONS)[number]
|
||||
): ActionRowBuilder<ButtonBuilder>[] {
|
||||
const labels = ['A', 'B', 'C', 'D'];
|
||||
const row = new ActionRowBuilder<ButtonBuilder>();
|
||||
for (let index = 0; index < question.options[locale].length; index += 1) {
|
||||
row.addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`${FUN_BUTTON_PREFIX}trivia:${userId}:${index}:${correctIndex}`)
|
||||
.setLabel(`${labels[index]!}. ${question.options[locale][index]!.slice(0, 20)}`)
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
);
|
||||
}
|
||||
return [row];
|
||||
}
|
||||
|
||||
export function parseTriviaButton(customId: string): {
|
||||
userId: string;
|
||||
answerIndex: number;
|
||||
correctIndex: number;
|
||||
} | null {
|
||||
const parts = customId.split(':');
|
||||
if (parts.length !== 5 || parts[0] !== 'fun' || parts[1] !== 'trivia') {
|
||||
return null;
|
||||
}
|
||||
const userId = parts[2];
|
||||
const answerIndex = Number(parts[3]);
|
||||
const correctIndex = Number(parts[4]);
|
||||
if (!userId || Number.isNaN(answerIndex) || Number.isNaN(correctIndex)) {
|
||||
return null;
|
||||
}
|
||||
return { userId, answerIndex, correctIndex };
|
||||
}
|
||||
|
||||
export function parseTicTacToeButton(customId: string): { gameId: string; cell: number } | null {
|
||||
const parts = customId.split(':');
|
||||
if (parts.length !== 4 || parts[0] !== 'fun' || parts[1] !== 'ttt') {
|
||||
return null;
|
||||
}
|
||||
const gameId = parts[2];
|
||||
const cell = Number(parts[3]);
|
||||
if (!gameId || Number.isNaN(cell) || cell < 0 || cell > 8) {
|
||||
return null;
|
||||
}
|
||||
return { gameId, cell };
|
||||
}
|
||||
|
||||
export function parseConnect4Button(customId: string): { gameId: string; col: number } | null {
|
||||
const parts = customId.split(':');
|
||||
if (parts.length !== 4 || parts[0] !== 'fun' || parts[1] !== 'c4') {
|
||||
return null;
|
||||
}
|
||||
const gameId = parts[2];
|
||||
const col = Number(parts[3]);
|
||||
if (!gameId || Number.isNaN(col) || col < 0 || col > 6) {
|
||||
return null;
|
||||
}
|
||||
return { gameId, col };
|
||||
}
|
||||
|
||||
export function parseHangmanButton(
|
||||
customId: string
|
||||
): { gameId: string; letter?: string; page?: number } | null {
|
||||
const parts = customId.split(':');
|
||||
if (parts.length < 4 || parts[0] !== 'fun' || parts[1] !== 'hm') {
|
||||
return null;
|
||||
}
|
||||
const gameId = parts[2];
|
||||
if (!gameId) {
|
||||
return null;
|
||||
}
|
||||
if (parts[3] === 'page') {
|
||||
const page = Number(parts[4]);
|
||||
if (Number.isNaN(page) || page < 0) {
|
||||
return null;
|
||||
}
|
||||
return { gameId, page };
|
||||
}
|
||||
const letter = parts[3]?.toUpperCase();
|
||||
if (!letter || !/^[A-Z]$/.test(letter)) {
|
||||
return null;
|
||||
}
|
||||
return { gameId, letter };
|
||||
}
|
||||
Reference in New Issue
Block a user