falgames
Version:
Falgames is a helpful package to enhance your discord bot with fun and interactive minigames
258 lines (230 loc) • 12.5 kB
JavaScript
import { EmbedBuilder, ActionRowBuilder } from "discord.js"
import { disableButtons, formatMessage, ButtonBuilder } from "../utils/utils.js"
import approve from "../utils/approve.js"
/**
* This class allows you to create and manage a Tic Tac Toe game in Discord, including handling user interactions and game logic.
* It extends the `approve` class to handle the approval process for the second player (opponent).
*
* @class TicTacToe
* @param {TicTacToeOptions} options - The options for the Tic Tac Toe game.
*
* @extends {events}
* @fires TicTacToe#gameOver
* @typedef {Object} TicTacToeOptions
*/
export class TicTacToe extends approve {
/**
* Represents a TicTacToe game.
* @constructor
* @param {Object} options - The options for the TicTacToe game.
* @param {boolean} [options.isSlashGame=false] - Whether the game is played using slash commands.
* @param {Object} options.message - The message object associated with the game.
* @param {Object} options.opponent - The opponent object for the game.
* @param {Object} [options.embed={}] - The embed options for the game.
* @param {string} [options.embed.title='Tic Tac Toe'] - The title of the embed.
* @param {string} [options.embed.statusTitle='Status'] - The status title of the embed.
* @param {string} [options.embed.overTitle='Game Over'] - The game over title of the embed.
* @param {string} [options.embed.color='#551476'] - The color of the embed.
* @param {Object} [options.emojis={}] - The emojis for the game.
* @param {string} [options.emojis.xButton='❌'] - The emoji for the X button.
* @param {string} [options.emojis.oButton='🔵'] - The emoji for the O button.
* @param {string} [options.emojis.blankButton='➖'] - The emoji for the blank button.
* @param {number} [options.timeoutTime=60000] - The timeout time for the game.
* @param {string} [options.xButtonStyle='DANGER'] - The style for the X button.
* @param {string} [options.oButtonStyle='PRIMARY'] - The style for the O button.
* @param {string} [options.turnMessage='{emoji} | Its turn of player **{player}**.'] - The turn message for the game.
* @param {string} [options.winMessage='{emoji} | **{player}** won the TicTacToe Game.'] - The win message for the game.
* @param {string} [options.tieMessage='The Game tied! No one won the Game!'] - The tie message for the game.
* @param {string} [options.timeoutMessage='The Game went unfinished! No one won the Game!'] - The timeout message for the game.
* @param {string} [options.requestMessage='{player} has invited you for a round of **Tic Tac Toe**.'] - The request message for the game.
* @param {string} [options.rejectMessage='The player denied your request for a round of **Tic Tac Toe**.'] - The reject message for the game.
* @param {string} [options.playerOnlyMessage='Only {player} and {opponent} can use these buttons.'] - The message to show when someone else tries to use the buttons.
*/
constructor(options = {}) {
if (!options.isSlashGame) options.isSlashGame = false
if (!options.message) throw new TypeError("NO_MESSAGE: No message option was provided.")
if (!options.opponent) throw new TypeError("NO_OPPONENT: No opponent option was provided.")
if (typeof options.message !== "object") throw new TypeError("INVALID_MESSAGE: message option must be an object.")
if (typeof options.isSlashGame !== "boolean")
throw new TypeError("INVALID_COMMAND_TYPE: isSlashGame option must be a boolean.")
if (typeof options.opponent !== "object")
throw new TypeError("INVALID_OPPONENT: opponent option must be an object.")
if (!options.embed) options.embed = {}
if (!options.embed.title) options.embed.title = "Tic Tac Toe"
if (!options.embed.statusTitle) options.embed.statusTitle = "Status"
if (!options.embed.overTitle) options.embed.overTitle = "Game Over"
if (!options.embed.color) options.embed.color = "#551476"
if (!options.emojis) options.emojis = {}
if (!options.emojis.xButton) options.emojis.xButton = "❌"
if (!options.emojis.oButton) options.emojis.oButton = "🔵"
if (!options.emojis.blankButton) options.emojis.blankButton = "➖"
if (!options.timeoutTime) options.timeoutTime = 60000
if (!options.xButtonStyle) options.xButtonStyle = "DANGER"
if (!options.oButtonStyle) options.oButtonStyle = "PRIMARY"
if (!options.turnMessage) options.turnMessage = "{emoji} | Its turn of player **{player}**."
if (!options.winMessage) options.winMessage = "{emoji} | **{player}** won the TicTacToe Game."
if (!options.tieMessage) options.tieMessage = "The Game tied! No one won the Game!"
if (!options.timeoutMessage) options.timeoutMessage = "The Game went unfinished! No one won the Game!"
if (!options.requestMessage) options.requestMessage = "{player} has invited you for a round of **Tic Tac Toe**."
if (!options.rejectMessage) options.rejectMessage = "The player denied your request for a round of **Tic Tac Toe**."
if (typeof options.embed !== "object") throw new TypeError("INVALID_EMBED: embed option must be an object.")
if (typeof options.embed.title !== "string") throw new TypeError("INVALID_EMBED: embed title must be a string.")
if (typeof options.embed.statusTitle !== "string")
throw new TypeError("INVALID_EMBED: status title must be a string.")
if (typeof options.embed.overTitle !== "string") throw new TypeError("INVALID_EMBED: over title must be a string.")
if (typeof options.emojis !== "object") throw new TypeError("INVALID_EMOJIS: emojis option must be an object.")
if (typeof options.emojis.xButton !== "string")
throw new TypeError("INVALID_EMOJIS: xButton emoji must be a string.")
if (typeof options.emojis.oButton !== "string")
throw new TypeError("INVALID_EMOJIS: oButton emoji must be a string.")
if (typeof options.emojis.blankButton !== "string")
throw new TypeError("INVALID_EMOJIS: blankButton emoji must be a string.")
if (typeof options.xButtonStyle !== "string")
throw new TypeError("INVALID_BUTTON_STYLE: xbutton style must be a string.")
if (typeof options.oButtonStyle !== "string")
throw new TypeError("INVALID_BUTTON_STYLE: obutton style must be a string.")
if (typeof options.turnMessage !== "string")
throw new TypeError("INVALID_MESSAGE: Turn message option must be a string.")
if (typeof options.winMessage !== "string")
throw new TypeError("INVALID_MESSAGE: Win message option must be a string.")
if (typeof options.tieMessage !== "string")
throw new TypeError("INVALID_MESSAGE: Tie message option must be a string.")
if (typeof options.timeoutMessage !== "string")
throw new TypeError("INVALID_MESSAGE: Win message option must be a string.")
if (typeof options.timeoutTime !== "number") throw new TypeError("INVALID_TIME: time option must be a number.")
if (options.playerOnlyMessage !== false) {
if (!options.playerOnlyMessage) options.playerOnlyMessage = "Only {player} and {opponent} can use these buttons."
if (typeof options.playerOnlyMessage !== "string")
throw new TypeError("INVALID_MESSAGE: playerOnly Message option must be a string.")
}
super(options)
this.options = options
this.message = options.message
this.opponent = options.opponent
this.gameBoard = [0, 0, 0, 0, 0, 0, 0, 0, 0]
this.player1Turn = true
}
async sendMessage(content) {
if (this.options.isSlashGame) return await this.message.editReply(content)
else return await this.message.channel.send(content)
}
async startGame() {
if (this.options.isSlashGame || !this.message.author) {
if (!this.message.deferred) await this.message.deferReply().catch((e) => {})
this.message.author = this.message.user
this.options.isSlashGame = true
}
const approve = await this.approve()
if (approve) this.TicTacToeGame(approve)
}
async TicTacToeGame(msg) {
const embed = new EmbedBuilder()
.setColor(this.options.embed.color)
.setTitle(this.options.embed.title)
.setFooter({ text: this.message.author.tag + " vs " + this.opponent.tag })
.addFields({ name: this.options.embed.statusTitle, value: this.getTurnMessage() })
await msg.edit({ content: null, embeds: [embed], components: this.getComponents() })
this.handleButtons(msg)
}
handleButtons(msg) {
const collector = msg.createMessageComponentCollector({ idle: this.options.timeoutTime })
collector.on("collect", async (btn) => {
await btn.deferUpdate().catch((e) => {})
if (btn.user.id !== this.message.author.id && btn.user.id !== this.opponent.id) {
if (this.options.playerOnlyMessage)
btn.followUp({ content: formatMessage(this.options, "playerOnlyMessage"), ephemeral: true })
return
}
if (btn.user.id !== (this.player1Turn ? this.message.author : this.opponent).id) return
this.gameBoard[btn.customId.split("_")[1]] = this.player1Turn ? 1 : 2
if (this.hasWonGame(1) || this.hasWonGame(2) || !this.gameBoard.includes(0)) collector.stop()
if (this.hasWonGame(1) || this.hasWonGame(2)) return this.gameOver(msg, "win")
if (!this.gameBoard.includes(0)) return this.gameOver(msg, "tie")
this.player1Turn = !this.player1Turn
const embed = new EmbedBuilder()
.setColor(this.options.embed.color)
.setTitle(this.options.embed.title)
.setFooter({ text: this.message.author.tag + " vs " + this.opponent.tag })
.addFields({ name: this.options.embed.statusTitle, value: this.getTurnMessage() })
return await msg.edit({ embeds: [embed], components: this.getComponents() })
})
collector.on("end", async (_, reason) => {
if (reason === "idle") return this.gameOver(msg, "timeout")
})
}
async gameOver(msg, result) {
const TicTacToeGame = { player: this.message.author, opponent: this.opponent, gameBoard: this.gameBoard }
if (result === "win") TicTacToeGame.winner = this.hasWonGame(1) ? this.message.author.id : this.opponent.id
this.emit("gameOver", { result: result, ...TicTacToeGame })
const embed = new EmbedBuilder()
.setColor(this.options.embed.color)
.setTitle(this.options.embed.title)
.setFooter({ text: this.message.author.tag + " vs " + this.opponent.tag })
.addFields({ name: this.options.embed.overTitle, value: this.getTurnMessage(result + "Message") })
return await msg.edit({ embeds: [embed], components: disableButtons(this.getComponents()) })
}
isGameOver() {
if (this.hasWonGame(1) || this.hasWonGame(2) || !this.gameBoard.includes(0)) return true
return false
}
hasWonGame(player) {
if (
this.gameBoard[0] === this.gameBoard[4] &&
this.gameBoard[0] === this.gameBoard[8] &&
this.gameBoard[0] === player
) {
return true
} else if (
this.gameBoard[6] === this.gameBoard[4] &&
this.gameBoard[6] === this.gameBoard[2] &&
this.gameBoard[6] === player
) {
return true
}
for (let i = 0; i < 3; ++i) {
if (
this.gameBoard[i * 3] === this.gameBoard[i * 3 + 1] &&
this.gameBoard[i * 3] === this.gameBoard[i * 3 + 2] &&
this.gameBoard[i * 3] === player
) {
return true
}
if (
this.gameBoard[i] === this.gameBoard[i + 3] &&
this.gameBoard[i] === this.gameBoard[i + 6] &&
this.gameBoard[i] === player
) {
return true
}
}
return false
}
getPlayerEmoji() {
return this.player1Turn ? this.options.emojis.xButton : this.options.emojis.oButton
}
getTurnMessage(msg) {
return this.formatTurnMessage(this.options, msg ?? "turnMessage").replace("{emoji}", this.getPlayerEmoji())
}
getButton(btn) {
if (btn === 1) return { emoji: this.options.emojis.xButton, style: this.options.xButtonStyle }
else if (btn === 2) return { emoji: this.options.emojis.oButton, style: this.options.oButtonStyle }
else return { emoji: this.options.emojis.blankButton, style: "SECONDARY" }
}
getComponents() {
const components = []
for (let x = 0; x < 3; x++) {
const row = new ActionRowBuilder()
for (let y = 0; y < 3; y++) {
const button = this.getButton(this.gameBoard[y * 3 + x])
const btn = new ButtonBuilder()
.setEmoji(button.emoji)
.setStyle(button.style)
.setCustomId("TicTacToe_" + (y * 3 + x))
if (this.gameBoard[y * 3 + x] !== 0) btn.setDisabled(true)
row.addComponents(btn)
}
components.push(row)
}
return components
}
}