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

fixed LMS simulation command #5985

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
190 changes: 190 additions & 0 deletions src/lib/minions/functions/lmsSimCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import type { Channel, Message } from 'discord.js';
import { TextChannel } from 'discord.js';
import { chunk, sleep } from 'e';

import type LastManStandingUsage from '../../structures/LastManStandingUsage';
import { LMS_FINAL, LMS_PREP, LMS_ROUND } from '../../structures/LastManStandingUsage';
import { channelIsSendable } from '../../util';

const playing = new Set<string>();

function calculateMaxDeaths(game: LastManStandingGame) {
return game.prep // have 0 deaths during the preparation phase
? 0
: // For 16 people, 5 die, 36 -> 7, and so on keeps the game interesting.
game.contestants.size >= 16
? Math.ceil(Math.sqrt(game.contestants.size) + 1)
: // If there are more than 7 contestants, proceed to kill them in 4s.
game.contestants.size > 7
? 4
: // If there are more than 3 contestants, eliminate 2, else 1 (3 -> 2, 2 -> 1)
game.contestants.size > 3
? 2
: 1;
}

function shuffle(contestants: string[]) {
let m = contestants.length;
while (m) {
const i = Math.floor(Math.random() * m--);
[contestants[m], contestants[i]] = [contestants[i], contestants[m]];
}
return new Set(contestants);
}

function makeResultEvents(game: LastManStandingGame, events: readonly LastManStandingUsage[]) {
const results = [] as string[];
const deaths = [] as string[];
let maxDeaths = calculateMaxDeaths(game);

const round = new Set([...game.contestants]);
for (const contestant of game.contestants) {
// If the player already had its round, skip
if (!round.has(contestant)) continue;

// Pick a valid event
const event = pick(events, round.size, maxDeaths);

// Pick the contestants
const pickedcontestants = pickcontestants(contestant, round, event.contestants);

// Delete all the picked contestants from this round
for (const picked of pickedcontestants) {
round.delete(picked);
}

// Kill all the unfortunate contestants
for (const death of event.deaths) {
game.contestants.delete(pickedcontestants[death]);
deaths.push(pickedcontestants[death]);
maxDeaths--;
}

// Push the result of this match
results.push(event.display(...pickedcontestants));
}

return { results, deaths };
}

function buildTexts(game: LastManStandingGame, results: string[], deaths: string[]) {
const header = game.prep ? 'Preparation' : game.final ? `Finals, Round: ${game.round}` : `Round: ${game.round}`;
const death =
deaths.length > 0
? `${`**${deaths.length} new gravestone${
deaths.length === 1 ? ' litters' : 's litter'
} the battlefield.**`}\n\n${deaths.map(d => `- ${d}`).join('\n')}`
: '';
const panels = chunk(results, 5);

const texts = panels.map(
panel => `**Last Man Standing ${header}:**\n\n${panel.map(text => `- ${text}`).join('\n')}`
);
if (deaths.length > 0) texts.push(`${death}`);
return texts;
}

function pick(events: readonly LastManStandingUsage[], contestants: number, maxDeaths: number) {
events = events.filter(event => event.contestants <= contestants && event.deaths.size <= maxDeaths);
return events[Math.floor(Math.random() * events.length)];
}

function pickcontestants(contestant: string, round: Set<string>, amount: number) {
if (amount === 0) return [];
if (amount === 1) return [contestant];
const array = [...round];
array.splice(array.indexOf(contestant), 1);

let m = array.length;
while (m) {
const i = Math.floor(Math.random() * m--);
[array[m], array[i]] = [array[i], array[m]];
}
array.unshift(contestant);
return array.slice(0, amount);
}

export async function lmsSimCommand(channel: Channel | undefined, names?: string) {
if (!channel) return;
if (!(channel instanceof TextChannel)) return;
let filtered = new Set<string>();
const splitContestants = names ? names.split(',') : [];
// Autofill using authors from the last 100 messages, if none are given to the command
if (names === 'auto' || !names || splitContestants.length === 0) {
const messages = await channel.messages.fetch({ limit: 100 });

for (const { author } of messages.values()) {
const name = author.username;
if (!filtered.has(name)) filtered.add(name);
}
} else {
filtered = new Set(splitContestants);
if (filtered.size !== splitContestants.length) {
return channel.send('I am sorry, but a user cannot play twice.');
}

if (filtered.size < 4) {
return channel.send(
'Please specify atleast 4 players for Last Man Standing, like so: `+lms Alex, Kyra, Magna, Rick`, or type `+lms auto` to automatically pick people from the chat.'
);
}

if (filtered.size > 48) {
return channel.send('I am sorry but the amount of players can be no greater than 48.');
}
}

if (playing.has(channel.guildId)) {
return channel.send('There is a game in progress in this server already, try again when it finishes.');
}

playing.add(channel.guildId);

let gameMessage: Message | null = null;
const game: LastManStandingGame = Object.seal({
prep: true,
final: false,
contestants: shuffle([...filtered]),
round: 0
});

while (game.contestants.size > 1) {
// If it's not prep, increase the round
if (!game.prep) ++game.round;
const events = game.prep ? LMS_PREP : game.final ? LMS_FINAL : LMS_ROUND;

// Main logic of the game
const { results, deaths } = makeResultEvents(game, events);
const texts = buildTexts(game, results, deaths);

// Ask for the user to proceed:
for (const text of texts) {
// If the channel is not postable, break:
if (!channelIsSendable(channel)) return;

gameMessage = await channel.send({ content: text });
await sleep(Math.max(gameMessage?.content.length / 20, 7) * 700);

// Delete the previous message, and if stopped, send stop.
gameMessage?.delete();
}

if (game.prep) game.prep = false;
else if (game.contestants.size < 4) game.final = true;
}

// The match finished with one remaining player
const winner = game.contestants.values().next().value;
playing.delete(channel.guildId);
return channel.send({
content: `And the Last Man Standing is... **${winner}**!`,
allowedMentions: { parse: [], users: [] }
});
}

interface LastManStandingGame {
prep: boolean;
final: boolean;
contestants: Set<string>;
round: number;
}
9 changes: 9 additions & 0 deletions src/mahoji/lib/abstracted_commands/lmsCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Time } from 'e';
import { Bank } from 'oldschooljs';

import { LMSBuyables } from '../../../lib/data/CollectionsExport';
import { lmsSimCommand } from '../../../lib/minions/functions/lmsSimCommand';
import type { MinigameActivityTaskOptionsWithNoChanges } from '../../../lib/types/minions';
import { formatDuration, randomVariation, stringMatches } from '../../../lib/util';
import addSubTaskToActivityTask from '../../../lib/util/addSubTaskToActivityTask';
Expand All @@ -15,6 +16,7 @@ export async function lmsCommand(
stats?: {};
start?: {};
buy?: { name?: string; quantity?: number };
simulate?: { names?: string };
},
user: MUser,
channelID: string,
Expand All @@ -33,6 +35,13 @@ export async function lmsCommand(
**Total Matches:** ${stats.totalGames}`;
}

if (options.simulate) {
lmsSimCommand(globalClient.channels.cache.get(channelID.toString()), options.simulate.names);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding error handling for when globalClient.channels.cache.get(channelID.toString()) returns undefined. This will improve user experience by providing feedback when the channel is not found or the simulation cannot be started.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The asynchronous function lmsSimCommand should be awaited to ensure all its operations complete before proceeding.

Suggested change
lmsSimCommand(globalClient.channels.cache.get(channelID.toString()), options.simulate.names);
await lmsSimCommand(globalClient.channels.cache.get(channelID.toString()), options.simulate.names);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gc @themrrobert why is the bot requesting to add this?

return {
content: 'Starting simulation...'
};
}

if (options.buy) {
const itemToBuy = LMSBuyables.find(i => stringMatches(i.item.name, options.buy?.name ?? ''));
if (!itemToBuy) {
Expand Down
Loading
Loading