-
Notifications
You must be signed in to change notification settings - Fork 133
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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; | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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'; | ||||||
|
@@ -15,6 +16,7 @@ export async function lmsCommand( | |||||
stats?: {}; | ||||||
start?: {}; | ||||||
buy?: { name?: string; quantity?: number }; | ||||||
simulate?: { names?: string }; | ||||||
}, | ||||||
user: MUser, | ||||||
channelID: string, | ||||||
|
@@ -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); | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The asynchronous function
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||||||
|
There was a problem hiding this comment.
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())
returnsundefined
. This will improve user experience by providing feedback when the channel is not found or the simulation cannot be started.