Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Blackjack Leaderboards Functionality #531

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
83 changes: 13 additions & 70 deletions src/commandDetails/coin/leaderboard.ts
Original file line number Diff line number Diff line change
@@ -1,87 +1,30 @@
import { container, SapphireClient } from '@sapphire/framework';
import { EmbedBuilder } from 'discord.js';
import { container } from '@sapphire/framework';
import {
CodeyCommandDetails,
getUserFromMessage,
SapphireMessageExecuteType,
SapphireMessageResponse,
getUserFromMessage,
} from '../../codeyCommand';
import { getCoinBalanceByUserId, getCoinLeaderboard } from '../../components/coin';
import { getCoinEmoji } from '../../components/emojis';
import { DEFAULT_EMBED_COLOUR } from '../../utils/embeds';

// Number of users to display on leaderboard
const LEADERBOARD_LIMIT_DISPLAY = 10;
// Number of users to fetch for leaderboard
const LEADERBOARD_LIMIT_FETCH = LEADERBOARD_LIMIT_DISPLAY * 2;

const getCoinLeaderboardEmbed = async (
client: SapphireClient<boolean>,
userId: string,
): Promise<EmbedBuilder> => {
// Get extra users to filter bots later
let leaderboard = await getCoinLeaderboard(LEADERBOARD_LIMIT_FETCH);
const leaderboardArray: string[] = [];
// Initialize user's coin balance if they have not already
const userBalance = await getCoinBalanceByUserId(userId);
let previousBalance = -1;
let position = 0;
let rank = 0;
let offset = 0;
let i = 0;
let absoluteCount = 0;
while (leaderboardArray.length < LEADERBOARD_LIMIT_DISPLAY || position === 0) {
if (i === LEADERBOARD_LIMIT_FETCH) {
offset += LEADERBOARD_LIMIT_FETCH;
leaderboard = await getCoinLeaderboard(LEADERBOARD_LIMIT_FETCH, offset);
i = 0;
}
if (i >= leaderboard.length) {
break;
}
const userCoinEntry = leaderboard[i++];
const user = await client.users.fetch(userCoinEntry.user_id).catch(() => null);
if (user?.bot) continue;
if (previousBalance === userCoinEntry.balance) {
previousBalance = userCoinEntry.balance;
// rank does not change
} else {
previousBalance = userCoinEntry.balance;
rank = absoluteCount + 1;
}
// count how many total users have been processed:
absoluteCount++;
if (userCoinEntry.user_id === userId) {
position = rank;
}
if (leaderboardArray.length < LEADERBOARD_LIMIT_DISPLAY) {
const userCoinEntryText = `${rank}\\. <@${userCoinEntry.user_id}> - ${
userCoinEntry.balance
} ${getCoinEmoji()}`;
leaderboardArray.push(userCoinEntryText);
}
}
const leaderboardText = leaderboardArray.join('\n');
const leaderboardEmbed = new EmbedBuilder()
.setColor(DEFAULT_EMBED_COLOUR)
.setTitle('Codey Coin Leaderboard')
.setDescription(leaderboardText);
leaderboardEmbed.addFields([
{
name: 'Your Position',
value: `You are currently **#${position}** in the leaderboard with ${userBalance} ${getCoinEmoji()}.`,
},
]);
return leaderboardEmbed;
};
import { getLeaderboardEmbed } from '../../utils/leaderboards';

const coinLeaderboardExecuteCommand: SapphireMessageExecuteType = async (
client,
messageFromUser,
_args,
): Promise<SapphireMessageResponse> => {
const userId = getUserFromMessage(messageFromUser).id;
return { embeds: [await getCoinLeaderboardEmbed(client, userId)] };
const leaderboardEmbed = await getLeaderboardEmbed(
client,
userId,
getCoinLeaderboard,
(entry, rank) => `${rank}\\. <@${entry.user_id}> - ${entry.balance} coins`,
getCoinBalanceByUserId,
'Coin Leaderboard',
getCoinEmoji(),
);
return { embeds: [leaderboardEmbed] };
};

export const coinLeaderboardCommandDetails: CodeyCommandDetails = {
Expand Down
24 changes: 16 additions & 8 deletions src/commandDetails/games/blackjack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
startGame,
} from '../../components/games/blackjack';
import { pluralize } from '../../utils/pluralize';
import { adjustBlackjackGameResult } from '../../components/games/blackjackLeaderboards';

// CodeyCoin constants
const DEFAULT_BET = 10;
Expand Down Expand Up @@ -121,6 +122,7 @@ const getBalanceChange = (game: GameState): number => {
const getEmbedColourFromGame = (game: GameState): keyof typeof Colors => {
if (game.stage === BlackjackStage.DONE) {
const balance = getBalanceChange(game);

// Player lost coins
if (balance < 0) {
return 'Red';
Expand All @@ -138,7 +140,7 @@ const getEmbedColourFromGame = (game: GameState): keyof typeof Colors => {
};

// Retrieve game status at different states
const getDescriptionFromGame = (game: GameState): string => {
const getDescriptionFromGame = async (game: GameState): Promise<string> => {
const amountDiff = Math.abs(getBalanceChange(game));
if (game.stage === BlackjackStage.DONE) {
// Player surrendered
Expand Down Expand Up @@ -170,14 +172,15 @@ const getDescriptionFromGame = (game: GameState): string => {
};

// Display embed to play game
const getEmbedFromGame = (game: GameState): EmbedBuilder => {
const getEmbedFromGame = async (game: GameState): Promise<EmbedBuilder> => {
const embed = new EmbedBuilder().setTitle('Blackjack');

embed.setColor(getEmbedColourFromGame(game));

const description = await getDescriptionFromGame(game);
embed.addFields([
// Show bet amount and game description
{ name: `Bet: ${game.bet} ${getCoinEmoji()}`, value: getDescriptionFromGame(game) },
{ name: `Bet: ${game.bet} ${getCoinEmoji()}`, value: description },
// Show player and dealer value and hands
{
name: `Player: ${game.playerValue.join(' or ')}`,
Expand All @@ -193,7 +196,9 @@ const getEmbedFromGame = (game: GameState): EmbedBuilder => {
};

// End the game
const closeGame = (playerId: string, balanceChange = 0) => {
const closeGame = async (playerId: string, game: GameState) => {
const balanceChange = getBalanceChange(game);
adjustBlackjackGameResult(playerId, balanceChange);
endGame(playerId);
adjustCoinBalanceByUserId(playerId, balanceChange, UserCoinEvent.Blackjack);
};
Expand Down Expand Up @@ -239,9 +244,10 @@ const blackjackExecuteCommand: SapphireMessageExecuteType = async (
return 'Please finish your current game before starting another one!';
}

const embed = await getEmbedFromGame(game);
// Show game initial state and setup reactions
const msg = await message.reply({
embeds: [getEmbedFromGame(game)],
embeds: [embed],
components: game?.stage != BlackjackStage.DONE ? [optionRow] : [],
fetchReply: true,
});
Expand All @@ -261,9 +267,10 @@ const blackjackExecuteCommand: SapphireMessageExecuteType = async (

// Wait for user action
game = await performActionFromReaction(reactCollector, author);
const updatedEmbed = await getEmbedFromGame(game!);

// Return next game state
await msg.edit({ embeds: [getEmbedFromGame(game!)] });
await msg.edit({ embeds: [updatedEmbed] });
await reactCollector.update({ components: [optionRow] });
} catch {
// If player has not acted within time limit, consider it as quitting the game
Expand All @@ -278,9 +285,10 @@ const blackjackExecuteCommand: SapphireMessageExecuteType = async (
}
if (game) {
// Update game embed
await msg.edit({ embeds: [getEmbedFromGame(game)], components: [] });
const finalEmbed = await getEmbedFromGame(game);
await msg.edit({ embeds: [finalEmbed], components: [] });
// End the game
closeGame(author, getBalanceChange(game));
closeGame(author, game);
}
};

Expand Down
53 changes: 53 additions & 0 deletions src/commandDetails/games/blackjackLeaderboards/total.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { container } from '@sapphire/framework';
import {
CodeyCommandDetails,
SapphireMessageExecuteType,
SapphireMessageResponse,
getUserFromMessage,
} from '../../../codeyCommand';
import { getCoinEmoji } from '../../../components/emojis';
import { getLeaderboardEmbed } from '../../../utils/leaderboards';
import {
getNetTotalBlackjackBalanceByUserId,
getBlackjackNetTotalLeaderboard,
} from '../../../components/games/blackjackLeaderboards';

const blackjackNetTotalLeaderboardExecuteCommand: SapphireMessageExecuteType = async (
client,
messageFromUser,
_args,
): Promise<SapphireMessageResponse> => {
const userId = getUserFromMessage(messageFromUser).id;
const leaderboardEmbed = await getLeaderboardEmbed(
client,
userId,
getBlackjackNetTotalLeaderboard,
(entry, rank) => {
const netGainLoss = entry.net_gain_loss ?? 0;
const formattedNetGainLoss = netGainLoss < 0 ? `(${netGainLoss})` : netGainLoss.toString();
return `${rank}\\. <@${entry.user_id}> - ${formattedNetGainLoss} coins`;
},
async (id) => {
const netGainLoss = await getNetTotalBlackjackBalanceByUserId(id);
return netGainLoss < 0 ? `(${netGainLoss})` : netGainLoss.toString();
},
'Blackjack Net Total Leaderboard',
getCoinEmoji(),
);
return { embeds: [leaderboardEmbed] };
};

export const blackjackTotalLeaderboardCommandDetails: CodeyCommandDetails = {
name: 'total',
aliases: ['t'],
description: 'Get the current blackjack net gain/loss leaderboard.',
detailedDescription: `**Examples:**
\`${container.botPrefix}blackjackleaderboards total\`
\`${container.botPrefix}blackjackleaderboards t\``,

isCommandResponseEphemeral: false,
messageWhenExecutingCommand: 'Getting the current blackjack net gain/loss leaderboard...',
executeCommand: blackjackNetTotalLeaderboardExecuteCommand,
options: [],
subcommandDetails: {},
};
51 changes: 51 additions & 0 deletions src/commandDetails/games/blackjackLeaderboards/winrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { container } from '@sapphire/framework';
import {
CodeyCommandDetails,
SapphireMessageExecuteType,
SapphireMessageResponse,
getUserFromMessage,
} from '../../../codeyCommand';
import { getLeaderboardEmbed } from '../../../utils/leaderboards';
import {
getWinrateBlackjackByUserId,
getBlackjackWinrateLeaderboard,
} from '../../../components/games/blackjackLeaderboards';

const blackjackWinrateLeaderboardExecuteCommand: SapphireMessageExecuteType = async (
client,
messageFromUser,
_args,
): Promise<SapphireMessageResponse> => {
const userId = getUserFromMessage(messageFromUser).id;
const leaderboardEmbed = await getLeaderboardEmbed(
client,
userId,
getBlackjackWinrateLeaderboard,
(entry, rank) => {
const formattedWinrate = entry.winrate ? (entry.winrate * 100).toFixed(2) + ' %' : 'N/A';
return `${rank}\\. <@${entry.user_id}> - ${formattedWinrate}`;
},
async (id) => {
const winrate = await getWinrateBlackjackByUserId(id);
return winrate ? (winrate * 100).toFixed(2) + ' %' : 'N/A';
},
'Blackjack Winrate Leaderboard',
'',
);
return { embeds: [leaderboardEmbed] };
};

export const blackjackWinrateLeaderboardCommandDetails: CodeyCommandDetails = {
name: 'winrate',
aliases: ['wr'],
description: 'Get the current blackjack winrate leaderboard.',
detailedDescription: `**Examples:**
\`${container.botPrefix}blackjackleaderboards winrate\`
\`${container.botPrefix}blackjackleaderboards wr\``,

isCommandResponseEphemeral: false,
messageWhenExecutingCommand: 'Getting the current blackjack winrate leaderboard...',
executeCommand: blackjackWinrateLeaderboardExecuteCommand,
options: [],
subcommandDetails: {},
};
32 changes: 32 additions & 0 deletions src/commands/games/blackjackLeaderboards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Command, container } from '@sapphire/framework';
import { CodeyCommand, CodeyCommandDetails } from '../../codeyCommand';
import { blackjackWinrateLeaderboardCommandDetails } from '../../commandDetails/games/blackjackLeaderboards/winrate';
import { blackjackTotalLeaderboardCommandDetails } from '../../commandDetails/games/blackjackLeaderboards/total';

const blackjackLeaderboardsCommandDetails: CodeyCommandDetails = {
name: 'blackjackleaderboards',
aliases: ['blackjacklb', 'bjlb'],
description: 'Handle blackjack leaderboard functions.',
detailedDescription: `**Examples:**
\`${container.botPrefix}blackjackleaderboards winrate @Codey\`
\`${container.botPrefix}blackjackleaderboards total @Codey\``,
options: [],
subcommandDetails: {
winrate: blackjackWinrateLeaderboardCommandDetails,
total: blackjackTotalLeaderboardCommandDetails,
},
defaultSubcommandDetails: blackjackWinrateLeaderboardCommandDetails,
};

export class GamesBlackjackLeaderboardsCommand extends CodeyCommand {
details = blackjackLeaderboardsCommandDetails;

public constructor(context: Command.Context, options: Command.Options) {
super(context, {
...options,
aliases: blackjackLeaderboardsCommandDetails.aliases,
description: blackjackLeaderboardsCommandDetails.description,
detailedDescription: blackjackLeaderboardsCommandDetails.detailedDescription,
});
}
}
20 changes: 20 additions & 0 deletions src/components/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,25 @@ const initUserCoinTable = async (db: Database): Promise<void> => {
);
};

const initBlackjackPlayerStats = async (db: Database): Promise<void> => {
await db.run(
`
CREATE TABLE IF NOT EXISTS blackjack_player_stats (
user_id VARCHAR(255) PRIMARY KEY NOT NULL,
games_played INTEGER NOT NULL DEFAULT 0,
games_won INTEGER NOT NULL DEFAULT 0,
games_lost INTEGER NOT NULL DEFAULT 0,
net_gain_loss INTEGER NOT NULL DEFAULT 0,
winrate REAL NOT NULL DEFAULT 0.0
)
`,
);
await db.run(
`CREATE INDEX IF NOT EXISTS idx_net_gain_loss ON blackjack_player_stats (net_gain_loss)`,
);
await db.run(`CREATE INDEX IF NOT EXISTS idx_winrate ON blackjack_player_stats (winrate)`);
};

const initUserProfileTable = async (db: Database): Promise<void> => {
await db.run(
`
Expand Down Expand Up @@ -230,6 +249,7 @@ const initTables = async (db: Database): Promise<void> => {
await initUserCoinBonusTable(db);
await initUserCoinLedgerTable(db);
await initUserCoinTable(db);
await initBlackjackPlayerStats(db);
await initUserProfileTable(db);
await initRpsGameInfo(db);
await initConnectFourGameInfo(db);
Expand Down
Loading
Loading