From fe788434772350f30b1c8e92bc4c8ce5db5d6b79 Mon Sep 17 00:00:00 2001 From: Bacon-Fixation Date: Sun, 3 Sep 2023 10:07:54 -0500 Subject: [PATCH] Games for turbo rewrite --- apps/bot/src/commands/other/games.ts | 148 ++++++++ apps/bot/src/lib/games/connect-4.ts | 478 ++++++++++++++++++++++++++ apps/bot/src/lib/games/inviteEmbed.ts | 81 +++++ apps/bot/src/lib/games/tic-tac-toe.ts | 439 +++++++++++++++++++++++ 4 files changed, 1146 insertions(+) create mode 100644 apps/bot/src/commands/other/games.ts create mode 100644 apps/bot/src/lib/games/connect-4.ts create mode 100644 apps/bot/src/lib/games/inviteEmbed.ts create mode 100644 apps/bot/src/lib/games/tic-tac-toe.ts diff --git a/apps/bot/src/commands/other/games.ts b/apps/bot/src/commands/other/games.ts new file mode 100644 index 00000000..1803684f --- /dev/null +++ b/apps/bot/src/commands/other/games.ts @@ -0,0 +1,148 @@ +import { TicTacToeGame } from '../../lib/games/tic-tac-toe'; +import { Connect4Game } from '../../lib/games/connect-4'; +import { GameInvite } from '../../lib/games/inviteEmbed'; +import { ApplyOptions } from '@sapphire/decorators'; +import { Command, CommandOptions } from '@sapphire/framework'; +import type { User } from 'discord.js'; + +export const playersInGame: Map = new Map(); +@ApplyOptions({ + name: 'games', + description: 'Play games like Connect 4 and Tic Tac Toe with another person', + preconditions: ['isCommandDisabled', 'GuildOnly'] +}) +export class GamesCommand extends Command { + public override async chatInputRun( + interaction: Command.ChatInputCommandInteraction + ) { + let maxPlayers = 2; + const playerMap = new Map(); + const player1 = interaction.user; + + const subCommand = interaction.options.getSubcommand(true); + let gameTitle: string; + if (playersInGame.has(player1.id)) { + await interaction.reply({ + content: ":x: You can't play more than 1 game at a time", + ephemeral: true + }); + return; + } + playerMap.set(player1.id, player1); + if (subCommand == 'connect-4') { + gameTitle = 'Connect 4'; + } else { + gameTitle = 'Tic-Tac-Toe'; + } + + const invite = new GameInvite(gameTitle!, [player1], interaction); + + await interaction + .reply({ + embeds: [invite.gameInviteEmbed()], + components: [invite.gameInviteButtons()] + }) + .then(async i => { + const inviteCollector = + interaction.channel?.createMessageComponentCollector({ + time: 60 * 1000 + }); + inviteCollector?.on('collect', async response => { + if (response.customId === `${interaction.id}${player1.id}-No`) { + if (response.user.id !== player1.id) { + playerMap.delete(response.user.id); + } else { + await interaction.followUp({ + content: ':x: You started the invite.', + ephemeral: true + }); + } + } + + if (response.customId === `${interaction.id}${player1.id}-Yes`) { + if (playersInGame.has(response.user.id)) { + await interaction.followUp({ + content: `:x: You are already playing a game.`, + ephemeral: true + }); + } + + if (!playerMap.has(response.user.id)) { + playerMap.set(response.user.id, response.user); + } + if (playerMap.size == maxPlayers) + return inviteCollector.stop('start-game'); + } + const accepted: User[] = []; + playerMap.forEach(player => accepted.push(player)); + const invite = new GameInvite(gameTitle, accepted, interaction); + await response.update({ + embeds: [invite.gameInviteEmbed()] + }); + if (response.customId === `${interaction.id}${player1.id}-Start`) { + if (playerMap.has(response.user.id)) { + if (accepted.length > 1) { + playerMap.forEach((player: User) => + playersInGame.set(player.id, player) + ); + return inviteCollector.stop('start-game'); + } + } + } + }); + inviteCollector?.on('end', async (collected, reason) => { + await interaction.deleteReply()!; + if (playerMap.size === 1 || reason === 'declined') { + playerMap.forEach(player => playersInGame.delete(player.id)); + } + if (reason === 'time') { + await interaction.followUp({ + content: `:x: No one responded to your invitation.`, + ephemeral: true, + target: player1 + }); + if (playerMap.size > 1) { + playerMap.forEach((player: User) => + playersInGame.set(player.id, player) + ); + return startGame(subCommand); + } + } + if (reason === 'start-game') { + return startGame(subCommand); + } + }); + function startGame(subCommand: string) { + switch (subCommand) { + case 'connect-4': + new Connect4Game().connect4(interaction, playerMap); + break; + + case 'tic-tac-toe': + new TicTacToeGame().ticTacToe(interaction, playerMap); + break; + } + } + }); + } + + public override registerApplicationCommands( + registry: Command.Registry + ): void { + registry.registerChatInputCommand(builder => + builder + .setName(this.name) + .setDescription(this.description) + .addSubcommand(subCommand => + subCommand + .setName('connect-4') + .setDescription('Play a game of Connect-4 with another Person.') + ) + .addSubcommand(subCommand => + subCommand + .setName('tic-tac-toe') + .setDescription('Play a game of Tic-Tac-Toe with another Person.') + ) + ); + } +} diff --git a/apps/bot/src/lib/games/connect-4.ts b/apps/bot/src/lib/games/connect-4.ts new file mode 100644 index 00000000..fc584e3e --- /dev/null +++ b/apps/bot/src/lib/games/connect-4.ts @@ -0,0 +1,478 @@ +import { createCanvas, Image } from '@napi-rs/canvas'; +import axios from 'axios'; +import { + AttachmentBuilder, + EmbedBuilder, + MessageReaction, + ChatInputCommandInteraction, + User, + Message, + Colors +} from 'discord.js'; +import { playersInGame } from '../../commands/other/games'; +import Logger from '../logger'; + +export class Connect4Game { + public async connect4( + interaction: ChatInputCommandInteraction, + playerMap: Map + ) { + const player1 = interaction.user; + let player2: User; + playerMap.forEach(player => { + if (player.id !== player1.id) player2 = player; + }); + + const player1Avatar = player1.displayAvatarURL({ + extension: 'jpg' + }); + const player1Image = await axios.request({ + responseType: 'arraybuffer', + url: player1Avatar + }); + + const player1Piece = new Image(); + player1Piece.src = Buffer.from(await player1Image.data); + + const player2Avatar = player2!.displayAvatarURL({ + extension: 'jpg' + }); + + const player2Image = await axios.request({ + responseType: 'arraybuffer', + url: player2Avatar + }); + const player2Piece = new Image(); + player2Piece.src = Buffer.from(await player2Image.data); + await game(player1, player2!); + + async function game(player1: User, player2: User) { + let gameBoard: number[][] = [ + [0, 0, 0, 0, 0, 0, 0], // row 6 + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0] // row 1 + // column -> + ]; + + const row: { [key: number]: number[] } = { + 0: [1, 2, 3, 4, 5, 6], // column 1 + 1: [1, 2, 3, 4, 5, 6], + 2: [1, 2, 3, 4, 5, 6], + 3: [1, 2, 3, 4, 5, 6], + 4: [1, 2, 3, 4, 5, 6], + 5: [1, 2, 3, 4, 5, 6], + 6: [1, 2, 3, 4, 5, 6] // column 7 + // row -> + }; + + let currentPlayer = player1.id; + let boardImageURL: string | null = null; + + await createBoard(); + + const Embed = new EmbedBuilder() + .setThumbnail(player1Avatar) + .setColor(Colors.Red) + .setTitle(`Connect 4 - Player 1's Turn`) + .setDescription( + `Incase of invisible board click 🔄. + Use 1️⃣, 2️⃣, 3️⃣, etc... to place your colored disc in that column. + Thumbnail and Title indicate current players turn. + You have 1 minute per turn or it's an automatic forfeit.` + ) + .setImage(boardImageURL!) + .setFooter({ text: 'Incase of invisible board click 🔄' }) + .setTimestamp(); + + await interaction.channel + ?.send({ embeds: [Embed] }) + .then(async (message: Message) => { + const embed = new EmbedBuilder(message.embeds[0].data); + + try { + await message.react('1️⃣'); + await message.react('2️⃣'); + await message.react('3️⃣'); + await message.react('4️⃣'); + await message.react('5️⃣'); + await message.react('6️⃣'); + await message.react('7️⃣'); + await message.react('🔄'); + } catch (error) { + Logger.error('Connect 4 - ' + error); + } + + const filter = (reaction: MessageReaction) => { + return ( + reaction.emoji.name === '1️⃣' || + reaction.emoji.name === '2️⃣' || + reaction.emoji.name === '3️⃣' || + reaction.emoji.name === '4️⃣' || + reaction.emoji.name === '5️⃣' || + reaction.emoji.name === '6️⃣' || + reaction.emoji.name === '7️⃣' || + reaction.emoji.name === '🔄' + ); + }; + + const gameCollector = message.createReactionCollector({ + filter: filter, + idle: 60 * 1000 + }); + + gameCollector.on( + 'collect', + async function (reaction: MessageReaction, user: User) { + if (user.id !== interaction.applicationId) + await reaction.users.remove(user).catch(error => { + Logger.error(`Connect 4 - ` + error); + }); + + // Refresh Image + if ( + reaction.emoji.name === '🔄' && + (user.id === player1.id || user.id === player2.id) + ) { + embed.setImage(boardImageURL!); + await message.edit({ + embeds: [embed] + }); + } + + if (user.id !== currentPlayer) { + return; + } + + // Column 1 + if (reaction.emoji.name === '1️⃣') + await playerMove(0, user, embed); + + // Column 2 + if (reaction.emoji.name === '2️⃣') + await playerMove(1, user, embed); + + // Column 3 + if (reaction.emoji.name === '3️⃣') + await playerMove(2, user, embed); + + // Column 4 + if (reaction.emoji.name === '4️⃣') + await playerMove(3, user, embed); + + // Column 5 + if (reaction.emoji.name === '5️⃣') + await playerMove(4, user, embed); + + // Column 6 + if (reaction.emoji.name === '6️⃣') + await playerMove(5, user, embed); + + // Column 7 + if (reaction.emoji.name === '7️⃣') + await playerMove(6, user, embed); + + await message.edit({ embeds: [embed] }); + } + ); + + gameCollector.on('end', async () => { + playerMap.forEach(player => playersInGame.delete(player.id)); + return await message.reactions + .removeAll() + .catch((error: string) => Logger.error('Connect 4 - ' + error)); + }); + }); + + async function createBoard() { + // Set asset sizes + const boardHeight = 600; + const boardWidth = 700; + const pieceSize = 75 / 2; + const offset = 25 / 2; + + // Set Image size + const canvas = createCanvas(boardWidth, boardHeight); + const ctx = canvas.getContext('2d'); + + // Get Center to Center measurements for grid spacing + const positionX = boardWidth / 7; + const positionY = boardHeight / 6; + + // Connect 4 Board + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, boardWidth, boardHeight); + + // Build the Game Board + for (let columnIndex = 0; columnIndex < 7; ++columnIndex) { + for (let rowIndex = 0; rowIndex < 6; ++rowIndex) { + // Empty Spaces + if (gameBoard[rowIndex][columnIndex] === 0) { + ctx.beginPath(); + ctx.shadowColor = 'white'; + ctx.shadowBlur = 7; + ctx.shadowOffsetX = 2; + ctx.shadowOffsetY = 2; + ctx.arc( + offset + (pieceSize + positionX * columnIndex), + offset + (pieceSize + positionY * rowIndex), + pieceSize, + 0, + Math.PI * 2, + true + ); + ctx.fillStyle = 'grey'; + ctx.fill(); + } + // Player 1 Pieces + if (gameBoard[rowIndex][columnIndex] === 1) { + ctx.beginPath(); + ctx.shadowColor = 'grey'; + ctx.shadowBlur = 7; + ctx.shadowOffsetX = 2; + ctx.shadowOffsetY = 2; + if (player1Piece) { + ctx.save(); + ctx.arc( + offset + (pieceSize + positionX * columnIndex), + offset + (pieceSize + positionY * rowIndex), + pieceSize, + 0, + Math.PI * 2, + true + ); + ctx.fillStyle = 'grey'; + ctx.fill(); + ctx.clip(); + ctx.drawImage( + player1Piece, + offset + positionX * columnIndex, + offset + positionY * rowIndex, + pieceSize * 2, + pieceSize * 2 + ); + ctx.restore(); + } else { + ctx.arc( + offset + (pieceSize + positionX * columnIndex), + offset + (pieceSize + positionY * rowIndex), + pieceSize, + 0, + Math.PI * 2, + true + ); + ctx.fillStyle = 'red'; + ctx.fill(); + } + } + // Player 2 Pieces + if (gameBoard[rowIndex][columnIndex] === 2) { + ctx.beginPath(); + ctx.shadowColor = 'grey'; + ctx.shadowBlur = 7; + ctx.shadowOffsetX = 2; + ctx.shadowOffsetY = 2; + if (player2Piece) { + ctx.save(); + ctx.arc( + offset + (pieceSize + positionX * columnIndex), + offset + (pieceSize + positionY * rowIndex), + pieceSize, + 0, + Math.PI * 2, + true + ); + ctx.fillStyle = 'grey'; + ctx.fill(); + ctx.clip(); + ctx.drawImage( + player2Piece, + offset + positionX * columnIndex, + offset + positionY * rowIndex, + pieceSize * 2, + pieceSize * 2 + ); + ctx.restore(); + } else { + ctx.arc( + offset + (pieceSize + positionX * columnIndex), + offset + (pieceSize + positionY * rowIndex), + pieceSize, + 0, + Math.PI * 2, + true + ); + ctx.fillStyle = 'blue'; + ctx.fill(); + } + } + } + } + + return await interaction.channel + ?.send({ + files: [ + new AttachmentBuilder(canvas.toBuffer('image/png'), { + name: 'Connect4.png' + }) + ] + }) + .then(async (result: Message) => { + boardImageURL = await result.attachments.entries().next().value[1] + .url; + + result.delete(); + }) + .catch((error: string) => { + Logger.error( + 'Connect 4 - Failed to Delete previous Image\n', + error + ); + }); + } + + async function playerMove( + index: number, + user: User, + instance: EmbedBuilder + ) { + if (currentPlayer === 'Game Over' || row[index].length === 0) { + return; + } + + if (currentPlayer === user.id) { + row[index].pop(); + if (currentPlayer === player1.id) { + currentPlayer = player2.id; + gameBoard[row[index].length][index] = 1; + instance + .setThumbnail(player2Avatar!) + .setTitle(`Connect 4 - Player 2's Turn`) + .setColor(Colors.Blue) + .setTimestamp(); + } else { + gameBoard[row[index].length][index] = 2; + currentPlayer = player1.id; + instance + .setThumbnail(player1Avatar) + .setTitle(`Connect 4 - Player 1's Turn`) + .setColor(Colors.Red) + .setTimestamp(); + } + await createBoard(); + } + + if (checkWinner(gameBoard) === 0) { + // No More Possible Moves + if (!emptySpaces(gameBoard)) { + instance + .setTitle(`Connect 4 - Game Over`) + .setColor(Colors.Grey) + .setThumbnail(''); + + currentPlayer = 'Game Over'; + playerMap.forEach(player => playersInGame.delete(player.id)); + } + return instance.setImage(boardImageURL!).setTimestamp(); + } else { + instance + .setImage(boardImageURL!) + .setTitle( + `Connect 4 - 👑 Player ${checkWinner(gameBoard)} Wins! 👑` + ) + .setTimestamp(); + + if (currentPlayer === player1.id) { + instance.setThumbnail(player2Avatar!).setColor(Colors.Blue); + } else { + instance.setThumbnail(player1Avatar).setColor(Colors.Red); + } + currentPlayer = 'Game Over'; + playerMap.forEach(player => playersInGame.delete(player.id)); + return; + } + } + + // Check for available spaces + function emptySpaces(board: number[][]) { + let result = false; + for (let columnIndex = 0; columnIndex < 7; ++columnIndex) { + for (let rowIndex = 0; rowIndex < 6; ++rowIndex) { + if (board[rowIndex][columnIndex] === 0) { + result = true; + } + } + } + return result; + } + + // Reference https://stackoverflow.com/questions/15457796/four-in-a-row-logic/15457826#15457826 + + // Check for Win Conditions + function checkLine(a: number, b: number, c: number, d: number) { + // Check first cell non-zero and all cells match + return a != 0 && a == b && a == c && a == d; + } + + function checkWinner(board: number[][]) { + // Check down + for (let r = 0; r < 3; r++) + for (let c = 0; c < 7; c++) + if ( + checkLine( + board[r][c], + board[r + 1][c], + board[r + 2][c], + board[r + 3][c] + ) + ) + return board[r][c]; + + // Check right + for (let r = 0; r < 6; r++) + for (let c = 0; c < 4; c++) + if ( + checkLine( + board[r][c], + board[r][c + 1], + board[r][c + 2], + board[r][c + 3] + ) + ) + return board[r][c]; + + // Check down-right + for (let r = 0; r < 3; r++) + for (let c = 0; c < 4; c++) + if ( + checkLine( + board[r][c], + board[r + 1][c + 1], + board[r + 2][c + 2], + board[r + 3][c + 3] + ) + ) + return board[r][c]; + + // Check down-left + for (let r = 3; r < 6; r++) + for (let c = 0; c < 4; c++) + if ( + checkLine( + board[r][c], + board[r - 1][c + 1], + board[r - 2][c + 2], + board[r - 3][c + 3] + ) + ) + return board[r][c]; + + return 0; + } + } + } + // }); + // } +} diff --git a/apps/bot/src/lib/games/inviteEmbed.ts b/apps/bot/src/lib/games/inviteEmbed.ts new file mode 100644 index 00000000..230e7eaf --- /dev/null +++ b/apps/bot/src/lib/games/inviteEmbed.ts @@ -0,0 +1,81 @@ +import { + EmbedBuilder, + User, + ActionRowBuilder, + ButtonBuilder, + ChatInputCommandInteraction, + Colors, + ButtonStyle +} from 'discord.js'; + +export class GameInvite { + title: string; + players: User[]; + interaction: ChatInputCommandInteraction; + + public constructor( + title: string, + players: User[], + interaction: ChatInputCommandInteraction + ) { + this.title = title; + this.players = players; + this.interaction = interaction; + } + + public gameInviteEmbed(): EmbedBuilder { + let thumbnail: string = ''; + switch (this.title) { + case 'Connect 4': + thumbnail = 'https://i.imgur.com/cUpy82Q.png'; + break; + case 'Tic-Tac-Toe': + thumbnail = 'https://i.imgur.com/lbPsXXN.png'; + break; + + default: + thumbnail = this.interaction.user.displayAvatarURL(); + break; + } + + const gameInvite = new EmbedBuilder() + .setAuthor({ + name: this.interaction.user.displayName, + iconURL: this.interaction.user.avatar + ? this.interaction.user.displayAvatarURL() + : this.interaction.user.defaultAvatarURL + }) + .setTitle(`${this.title} - Game Invitation`) + .setColor(Colors.Yellow) + .setThumbnail(thumbnail) + .setDescription( + `${this.interaction.user} would like to play a game of ${this.title}. Click Yes or No. if you want to join in` + ) + .addFields({ + name: 'Players', + value: `${this.players.length > 0 ? this.players : 'None'}`, + inline: true + }) + .setFooter({ text: 'Invite will expire in 60 seconds' }) + .setTimestamp(); + return gameInvite; + } + public gameInviteButtons(): ActionRowBuilder { + const gameInviteButtons = + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`${this.interaction.id}${this.players.at(0)?.id}-Yes`) + .setLabel('Yes') + .setStyle(ButtonStyle.Success), + new ButtonBuilder() + .setCustomId(`${this.interaction.id}${this.players.at(0)?.id}-No`) + .setLabel('No') + .setStyle(ButtonStyle.Danger), + new ButtonBuilder() + .setCustomId(`${this.interaction.id}${this.players.at(0)?.id}-Start`) + .setLabel('Start') + .setStyle(ButtonStyle.Primary) + ); + return gameInviteButtons; + } +} diff --git a/apps/bot/src/lib/games/tic-tac-toe.ts b/apps/bot/src/lib/games/tic-tac-toe.ts new file mode 100644 index 00000000..ebd43f4e --- /dev/null +++ b/apps/bot/src/lib/games/tic-tac-toe.ts @@ -0,0 +1,439 @@ +import { createCanvas, Image } from '@napi-rs/canvas'; +import axios from 'axios'; +import { + AttachmentBuilder, + EmbedBuilder, + MessageReaction, + User, + Message, + ChatInputCommandInteraction, + Colors +} from 'discord.js'; +import { playersInGame } from '../../commands/other/games'; +import Logger from '../logger'; + +export class TicTacToeGame { + public async ticTacToe( + interaction: ChatInputCommandInteraction, + playerMap: Map + ) { + const player1 = interaction.user; + let player2: User; + playerMap.forEach(player => { + if (player.id !== player1.id) player2 = player; + }); + + const player1Avatar = player1.displayAvatarURL({ + extension: 'jpg' + }); + const player1Image = await axios.request({ + responseType: 'arraybuffer', + url: player1Avatar + }); + + const player1Piece = new Image(); + player1Piece.src = Buffer.from(await player1Image.data); + + const player2Avatar = player2!.displayAvatarURL({ + extension: 'jpg' + }); + + const player2Image = await axios.request({ + responseType: 'arraybuffer', + url: player2Avatar + }); + const player2Piece = new Image(); + player2Piece.src = Buffer.from(await player2Image.data); + await game(player1, player2!); + async function game(player1: User, player2: User) { + let gameBoard: number[][] = [ + [0, 0, 0], //row 1 + [0, 0, 0], + [0, 0, 0] + // column -> + ]; + + let rowChoice: number | null = null; + let columnChoice: number | null = null; + + let currentPlayer = player1.id; + let boardImageURL: string | null = null; + + let currentTurn = 0; + await createBoard(); + ++currentTurn; + + const Embed = new EmbedBuilder() + .setThumbnail(player1Avatar) + .setColor(Colors.Red) + .setTitle(`Tic Tac Toe - Player 1's Turn`) + .setDescription( + `Use the emojis 1️⃣, 2️⃣, 3️⃣ for columns and 🇦, 🇧, 🇨 for rows.\n + You must click both a **Number** and a **Letter** to place your colored square in that space.\n + You have 1 minute per turn or it's an automatic forfeit. + Incase of invisible board click 🔄.` + ) + .addFields( + { name: 'Column', value: 'None', inline: true }, + { name: 'Row', value: 'None', inline: true } + ) + .setImage(boardImageURL!) + .setFooter({ text: 'Incase of invisible board click 🔄' }) + .setTimestamp(); + + await interaction.channel + ?.send({ embeds: [Embed] }) + + .then(async message => { + const embed = new EmbedBuilder(message.embeds[0].data); + try { + await message.react('1️⃣'); + await message.react('2️⃣'); + await message.react('3️⃣'); + await message.react('🇦'); + await message.react('🇧'); + await message.react('🇨'); + await message.react('🔄'); + } catch (error) { + Logger.error(`Tic-Tac-Toe - ` + error); + } + + const filter = (reaction: MessageReaction) => { + return ( + reaction.emoji.name === '1️⃣' || + reaction.emoji.name === '2️⃣' || + reaction.emoji.name === '3️⃣' || + reaction.emoji.name === '🇦' || + reaction.emoji.name === '🇧' || + reaction.emoji.name === '🇨' || + reaction.emoji.name === '🔄' + ); + }; + + const gameCollector = message.createReactionCollector({ + filter: filter, + idle: 60 * 1000 + }); + + gameCollector.on( + 'collect', + async function (reaction: MessageReaction, user: User) { + // Reset the Reactions + if (user.id !== interaction.applicationId) + await reaction.users.remove(user).catch(error => { + Logger.error(`Tic-Tac-Toe - ` + error); + }); + + // Refresh Image + if ( + reaction.emoji.name === '🔄' && + (user.id === player1.id || user.id === player2.id) + ) { + embed.setImage(boardImageURL!); + await message.edit({ + embeds: [embed] + }); + } + + if (user.id !== currentPlayer) { + return; + } + // Column 1 + if (reaction.emoji.name === '1️⃣') { + columnChoice = 0; + await playerMove(rowChoice!, columnChoice, user, embed); + } + // Column 2 + if (reaction.emoji.name === '2️⃣') { + columnChoice = 1; + await playerMove(rowChoice!, columnChoice, user, embed); + } + // Column 3 + if (reaction.emoji.name === '3️⃣') { + columnChoice = 2; + await playerMove(rowChoice!, columnChoice, user, embed); + } + // Row A + if (reaction.emoji.name === '🇦') { + rowChoice = 0; + await playerMove(rowChoice, columnChoice!, user, embed); + } + // Row B + if (reaction.emoji.name === '🇧') { + rowChoice = 1; + await playerMove(rowChoice, columnChoice!, user, embed); + } + // Row C + if (reaction.emoji.name === '🇨') { + rowChoice = 2; + await playerMove(rowChoice, columnChoice!, user, embed); + } + + await message.edit({ + embeds: [embed] + }); + } + ); + + gameCollector.on('end', async () => { + playerMap.forEach(player => playersInGame.delete(player.id)); + return await message.reactions + .removeAll() + .catch((error: string) => Logger.error(`Tic-Tac-Toe - ` + error)); + }); + }); + + async function createBoard() { + // Set asset sizes + const boardHeight = 700; + const boardWidth = 700; + const pieceSize = 150; + + // Set Image size + const canvas = createCanvas(boardWidth, boardHeight); + const ctx = canvas.getContext('2d'); + + // Get Center to Center measurements for grid spacing + const positionX = 200; + const positionY = 200; + + // Tic-Tac-Toe Board + ctx.fillStyle = 'black'; + ctx.fillRect(0, 0, boardWidth, boardHeight); + + ctx.font = '100px Arial'; + ctx.fillStyle = 'grey'; + // Add Shadows to indicators and empty spaces + ctx.shadowColor = 'white'; + ctx.shadowBlur = 5; + ctx.shadowOffsetX = 4; + ctx.shadowOffsetY = 2; + // Column Numbers + ctx.fillText('1', 40, 650); + ctx.fillText('2', 250, 650); + ctx.fillText('3', 450, 650); + // Row Letters + ctx.fillText('A', 575, 110); + ctx.fillText('B', 575, 310); + ctx.fillText('C', 575, 510); + + // Build the Game Board + for (let columnIndex = 0; columnIndex < 3; ++columnIndex) { + for (let rowIndex = 0; rowIndex < 3; ++rowIndex) { + ctx.beginPath(); + + // Empty Spaces + if (gameBoard[rowIndex][columnIndex] === 0) { + ctx.fillStyle = 'grey'; + ctx.fillRect( + positionX * columnIndex, + positionY * rowIndex, + pieceSize, + pieceSize + ); + } + + // Player 1 Pieces + if (gameBoard[rowIndex][columnIndex] === 1) { + if (player1Piece) { + ctx.drawImage( + player1Piece, + positionX * columnIndex, + positionY * rowIndex, + pieceSize, + pieceSize + ); + } else { + ctx.fillStyle = 'red'; + ctx.shadowColor = 'grey'; + ctx.shadowBlur = 5; + ctx.shadowOffsetX = 4; + ctx.shadowOffsetY = 2; + ctx.fillRect( + positionX * columnIndex, + positionY * rowIndex, + pieceSize, + pieceSize + ); + } + } + // Player 2 Pieces + if (gameBoard[rowIndex][columnIndex] === 2) { + if (player2Piece) { + ctx.drawImage( + player2Piece, + positionX * columnIndex, + positionY * rowIndex, + pieceSize, + pieceSize + ); + } else { + ctx.fillStyle = 'blue'; + ctx.shadowColor = 'grey'; + ctx.shadowBlur = 5; + ctx.shadowOffsetX = 4; + ctx.shadowOffsetY = 2; + ctx.fillRect( + positionX * columnIndex, + positionY * rowIndex, + pieceSize, + pieceSize + ); + } + } + } + } + + return await interaction.channel + ?.send({ + files: [ + new AttachmentBuilder(canvas.toBuffer('image/png'), { + name: `TicTacToe-${player1.id}-${player2.id}${currentTurn}.png` + }) + ] + }) + + .then(async (result: Message) => { + boardImageURL = await result.attachments.entries().next().value[1] + .url; + + await result.delete(); + }) + .catch((error: string) => { + Logger.error(`Tic-Tac-Toe - ` + error); + }); + } + async function playerMove( + row: number, + column: number, + user: User, + instance: EmbedBuilder + ) { + const rowsLetters = ['A', 'B', 'C']; + + if (currentPlayer === user.id) { + instance.setFields( + { + name: 'Column', + value: `${column !== null ? `${column + 1}` : 'None'}`, + inline: true + }, + { + name: 'Row', + value: `${row !== null ? rowsLetters[row] : 'None'}`, + inline: true + } + ); + } + // Wait for both + if (row === null || column === null) { + return; + } + + // Reset 'Column' & 'Row' for next turn + instance.setFields( + { name: 'Column', value: 'None', inline: true }, + { name: 'Row', value: 'None', inline: true } + ); + columnChoice = null; + rowChoice = null; + + if (currentPlayer === 'Game Over' || gameBoard[row][column] !== 0) + return; + + if (currentPlayer === user.id) { + if (currentPlayer === player1.id) { + gameBoard[row][column] = 1; + currentPlayer = player2.id; + instance + .setThumbnail(player2Avatar!) + .setTitle(`Tic Tac Toe - Player 2's Turn`) + .setColor(Colors.Blue) + .setTimestamp(); + } else { + gameBoard[row][column] = 2; + currentPlayer = player1.id; + instance + .setThumbnail(player1Avatar) + .setTitle(`Tic Tac Toe - Player 1's Turn`) + .setColor(Colors.Red) + .setTimestamp(); + } + await createBoard(); + ++currentTurn; + } + + if (checkWinner(gameBoard) === 0) { + // No More Possible Moves + if (!emptySpaces(gameBoard)) { + instance + .setTitle(`Tic Tac Toe - Game Over`) + .setColor(Colors.Grey) + .setThumbnail(''); + currentPlayer = 'Game Over'; + playerMap.forEach(player => playersInGame.delete(player.id)); + } + instance.setImage(boardImageURL!).setTimestamp(); + return; + } else { + instance + .setImage(boardImageURL!) + .setTitle( + `Tic Tac Toe - 👑 Player ${checkWinner(gameBoard)} Wins! 👑` + ) + .setTimestamp(); + if (currentPlayer === player1.id) { + instance.setThumbnail(player2Avatar!).setColor(Colors.Blue); + } else { + instance.setThumbnail(player1Avatar).setColor(Colors.Red); + } + currentPlayer = 'Game Over'; + playerMap.forEach(player => playersInGame.delete(player.id)); + return; + } + } + + // Check for available spaces + function emptySpaces(board: number[][]) { + let result = false; + for (let columnIndex = 0; columnIndex < 3; ++columnIndex) { + for (let rowIndex = 0; rowIndex < 3; ++rowIndex) { + if (board[columnIndex][rowIndex] === 0) { + result = true; + } + } + } + return result; + } + + // Check for Win Conditions + function checkLine(a: number, b: number, c: number) { + // Check first cell non-zero and all cells match + return a != 0 && a == b && a == c; + } + + function checkWinner(board: number[][]) { + // Check down + for (let c = 0; c < 3; c++) + if (checkLine(board[0][c], board[1][c], board[2][c])) + return board[0][c]; + + // Check right + for (let r = 0; r < 3; r++) + if (checkLine(board[r][0], board[r][1], board[r][2])) + return board[r][0]; + + // Check down-right + if (checkLine(board[0][0], board[1][1], board[2][2])) + return board[0][0]; + + // Check down-left + if (checkLine(board[0][2], board[1][1], board[2][0])) + return board[0][2]; + + return 0; + } + } + // }); + } +}