Add minesweeper (Unfinished version)
This commit is contained in:
442
src/commands/slash/minesweeper.js
Normal file
442
src/commands/slash/minesweeper.js
Normal file
@@ -0,0 +1,442 @@
|
||||
const { SlashCommandBuilder, EmbedBuilder } = require('@discordjs/builders');
|
||||
const {
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
ModalBuilder,
|
||||
TextInputBuilder,
|
||||
TextInputStyle,
|
||||
} = require('discord.js');
|
||||
|
||||
const NUMBERS = [
|
||||
'zero',
|
||||
'one',
|
||||
'two',
|
||||
'three',
|
||||
'four',
|
||||
'five',
|
||||
'six',
|
||||
'seven',
|
||||
'eight',
|
||||
];
|
||||
|
||||
const moduleFuncs = {
|
||||
async run(context) {
|
||||
const { interaction, bot, uuid } = context;
|
||||
const size = interaction.options.getInteger('size') ?? 10;
|
||||
const mines =
|
||||
interaction.options.getInteger('mines') ?? Math.floor(size ** 2 / 5);
|
||||
const game = bot.minesweeper.createGame(size, mines, uuid);
|
||||
|
||||
const controlRow = this.getButtons(uuid, game.hasLost());
|
||||
const embed = this.getEmbed(game);
|
||||
|
||||
return interaction.reply({
|
||||
content: `This game will end <t:${Math.round(bot.minesweeper.getEndTime(uuid) / 1000)}:R>`,
|
||||
embeds: [embed],
|
||||
components: controlRow,
|
||||
});
|
||||
},
|
||||
|
||||
gridToString(board) {
|
||||
return board
|
||||
.reverse()
|
||||
.map((row) =>
|
||||
row
|
||||
.map((cell) =>
|
||||
cell.revealed
|
||||
? cell.mine
|
||||
? '💣'
|
||||
: `:${NUMBERS[cell.number]}:`
|
||||
: cell.flag
|
||||
? '🚩'
|
||||
: '⬛'
|
||||
)
|
||||
.join('')
|
||||
)
|
||||
.join('\n');
|
||||
},
|
||||
|
||||
getEmbed(game) {
|
||||
const grid = game.getBoard();
|
||||
const text = this.gridToString(grid);
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle('Minesweeper')
|
||||
.setDescription(game.hasLost() ? `Game Over!\n${text}` : text)
|
||||
.setColor(0x009bc2);
|
||||
|
||||
return embed;
|
||||
},
|
||||
|
||||
getButtons(uuid, lost) {
|
||||
return [
|
||||
new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`revealCells:${uuid}`)
|
||||
.setEmoji('🎯')
|
||||
.setLabel('Reveal cell')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(lost),
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`placeFlags:${uuid}`)
|
||||
.setEmoji('🚩')
|
||||
.setLabel('Place flag')
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setDisabled(lost)
|
||||
),
|
||||
new ActionRowBuilder().addComponents(
|
||||
new ButtonBuilder()
|
||||
.setCustomId(`exportGameDisabled:${uuid}`)
|
||||
.setEmoji('💾')
|
||||
.setLabel('Export game')
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setDisabled(lost)
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
async exportGame(context) {
|
||||
const { originalInteraction, bot, uuid } = context;
|
||||
const game = bot.minesweeper.getGame(uuid);
|
||||
const coords = game
|
||||
.getCoords()
|
||||
.map((coord) => coord.join(','))
|
||||
.join(' ');
|
||||
return originalInteraction.reply({
|
||||
content:
|
||||
'Paste this data in next time to load this game:```\n' + coords + '```',
|
||||
ephemeral: true,
|
||||
});
|
||||
},
|
||||
|
||||
async revealCellsInput(context) {
|
||||
const { interaction, originalInteraction, bot, uuid } = context;
|
||||
const game = bot.minesweeper.getGame(uuid);
|
||||
|
||||
const input = originalInteraction.fields
|
||||
.getTextInputValue('coordinates')
|
||||
.trim();
|
||||
|
||||
const isValid = /^(\d+,\d+\s*)+$/g.test(input);
|
||||
if (!isValid) {
|
||||
return originalInteraction.reply({
|
||||
content: 'Invalid format. Use: `row,col row,col ...`',
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
input.replaceAll('\n', ' ');
|
||||
const coords = input.split(' ');
|
||||
const size = game.getSize();
|
||||
coords.forEach((coord) => {
|
||||
const [x, y] = coord.split(',');
|
||||
if (x < size && y < size) game.revealCell(x, y);
|
||||
});
|
||||
|
||||
const controlRow = this.getButtons(uuid, game.hasLost());
|
||||
const embed = this.getEmbed(game);
|
||||
|
||||
await originalInteraction.deferUpdate();
|
||||
return interaction.editReply({
|
||||
content: `This game will end <t:${Math.round(bot.minesweeper.getEndTime(uuid) / 1000)}:R>`,
|
||||
embeds: [embed],
|
||||
components: controlRow,
|
||||
});
|
||||
},
|
||||
async revealCells(context) {
|
||||
const { originalInteraction, uuid } = context;
|
||||
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(`revealInput:${uuid}`)
|
||||
.setTitle('Toggle Cell Coordinates');
|
||||
|
||||
const input = new TextInputBuilder()
|
||||
.setCustomId('coordinates')
|
||||
.setLabel('Enter coordinates (e.g. 0,1 2,3)')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setRequired(true);
|
||||
|
||||
const action = new ActionRowBuilder().addComponents(input);
|
||||
|
||||
modal.addComponents(action);
|
||||
|
||||
await originalInteraction.showModal(modal);
|
||||
},
|
||||
|
||||
async placeFlagsInput(context) {
|
||||
const { interaction, originalInteraction, bot, uuid } = context;
|
||||
const game = bot.minesweeper.getGame(uuid);
|
||||
|
||||
const input = originalInteraction.fields
|
||||
.getTextInputValue('coordinates')
|
||||
.trim();
|
||||
|
||||
const isValid = /^(\d+,\d+\s*)+$/g.test(input);
|
||||
if (!isValid) {
|
||||
return originalInteraction.reply({
|
||||
content: 'Invalid format. Use: `row,col row,col ...`',
|
||||
ephemeral: true,
|
||||
});
|
||||
}
|
||||
|
||||
input.replaceAll('\n', ' ');
|
||||
const coords = input.split(' ');
|
||||
const size = game.getSize();
|
||||
coords.forEach((coord) => {
|
||||
const [x, y] = coord.split(',');
|
||||
if (x < size && y < size) game.placeFlag(x, y);
|
||||
});
|
||||
|
||||
const controlRow = this.getButtons(uuid, game.hasLost());
|
||||
const embed = this.getEmbed(game);
|
||||
|
||||
await originalInteraction.deferUpdate();
|
||||
return interaction.editReply({
|
||||
content: `This game will end <t:${Math.round(bot.minesweeper.getEndTime(uuid) / 1000)}:R>`,
|
||||
embeds: [embed],
|
||||
components: controlRow,
|
||||
});
|
||||
},
|
||||
|
||||
async placeFlags(context) {
|
||||
const { originalInteraction, uuid } = context;
|
||||
|
||||
const modal = new ModalBuilder()
|
||||
.setCustomId(`flagInput:${uuid}`)
|
||||
.setTitle('Place flag Coordinates');
|
||||
|
||||
const input = new TextInputBuilder()
|
||||
.setCustomId('coordinates')
|
||||
.setLabel('Enter coordinates (e.g. 0,1 2,3)')
|
||||
.setStyle(TextInputStyle.Paragraph)
|
||||
.setRequired(true);
|
||||
|
||||
const action = new ActionRowBuilder().addComponents(input);
|
||||
|
||||
modal.addComponents(action);
|
||||
|
||||
await originalInteraction.showModal(modal);
|
||||
},
|
||||
|
||||
init(context) {
|
||||
const { bot } = context;
|
||||
const { KeyValue } = bot;
|
||||
bot.minesweeper = {
|
||||
games: new KeyValue(),
|
||||
createGame(size, mines, id) {
|
||||
const game = new this.Minesweeper(size, mines);
|
||||
this.games.store(
|
||||
id,
|
||||
{
|
||||
game,
|
||||
endTime: new Date().getTime() + 15 * 60 * 1000,
|
||||
},
|
||||
15 * 60 * 1000
|
||||
);
|
||||
return game;
|
||||
},
|
||||
|
||||
getGame(id) {
|
||||
return this.games.get(id).game;
|
||||
},
|
||||
|
||||
getEndTime(id) {
|
||||
return this.games.get(id).endTime;
|
||||
},
|
||||
|
||||
Minesweeper: class {
|
||||
constructor(size, mines) {
|
||||
this.size = size;
|
||||
this.totalMines = mines;
|
||||
this.board = this.initBoard();
|
||||
this.minesPlaced = false;
|
||||
this.gameOver = false;
|
||||
this.won = false;
|
||||
this.lost = false;
|
||||
this.revealedCount = 0;
|
||||
}
|
||||
|
||||
initBoard() {
|
||||
return Array.from({ length: this.size }, () =>
|
||||
Array.from({ length: this.size }, () => ({
|
||||
revealed: false,
|
||||
mine: false,
|
||||
flag: false,
|
||||
number: 0,
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
placeMines(firstRow, firstCol) {
|
||||
let placed = 0;
|
||||
while (placed < this.totalMines) {
|
||||
const row = Math.floor(Math.random() * this.size);
|
||||
const col = Math.floor(Math.random() * this.size);
|
||||
|
||||
const isInSafeZone =
|
||||
Math.abs(row - firstRow) <= 1 && Math.abs(col - firstCol) <= 1;
|
||||
|
||||
if (!this.board[row][col].mine && !isInSafeZone) {
|
||||
this.board[row][col].mine = true;
|
||||
placed++;
|
||||
}
|
||||
}
|
||||
this.calculateNumber();
|
||||
this.minesPlaced = true;
|
||||
}
|
||||
|
||||
calculateNumber() {
|
||||
for (let row = 0; row < this.size; row++) {
|
||||
for (let col = 0; col < this.size; col++) {
|
||||
if (this.board[row][col].mine) {
|
||||
this.incrementNeighbors(row, col);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
incrementNeighbors(mineRow, mineCol) {
|
||||
for (let row = mineRow - 1; row <= mineRow + 1; row++) {
|
||||
for (let col = mineCol - 1; col <= mineCol + 1; col++) {
|
||||
if (
|
||||
row >= 0 &&
|
||||
row < this.size &&
|
||||
col >= 0 &&
|
||||
col < this.size &&
|
||||
!this.board[row][col].mine
|
||||
) {
|
||||
this.board[row][col].number++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleFlag(row, col) {
|
||||
const cell = this.board[row][col];
|
||||
if (!cell.revealed) {
|
||||
cell.flag = !cell.flag;
|
||||
}
|
||||
}
|
||||
|
||||
revealAll() {
|
||||
for (let row = 0; row < this.size; row++) {
|
||||
for (let col = 0; col < this.size; col++) {
|
||||
this.board[row][col].revealed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
revealCell(row, col) {
|
||||
if (this.gameOver) return;
|
||||
|
||||
if (!this.minesPlaced) {
|
||||
this.placeMines(row, col);
|
||||
}
|
||||
|
||||
const cell = this.board[row][col];
|
||||
if (cell.revealed || cell.flag) return;
|
||||
|
||||
cell.revealed = true;
|
||||
this.revealedCount++;
|
||||
|
||||
if (cell.mine) {
|
||||
this.gameOver = true;
|
||||
this.lost = true;
|
||||
this.revealAll();
|
||||
return;
|
||||
}
|
||||
|
||||
if (cell.number === 0) {
|
||||
this.revealAdjacentCells(row, col);
|
||||
}
|
||||
|
||||
this.checkWin();
|
||||
}
|
||||
|
||||
revealAdjacentCells(row, col) {
|
||||
for (let r = row - 1; r <= row + 1; r++) {
|
||||
for (let c = col - 1; c <= col + 1; c++) {
|
||||
if (
|
||||
r >= 0 &&
|
||||
r < this.size &&
|
||||
c >= 0 &&
|
||||
c < this.size &&
|
||||
!(r === row && c === col)
|
||||
) {
|
||||
const adjacent = this.board[r][c];
|
||||
if (!adjacent.revealed && !adjacent.flag) {
|
||||
adjacent.revealed = true;
|
||||
this.revealedCount++;
|
||||
if (adjacent.number === 0) {
|
||||
this.revealAdjacentCells(r, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkWin() {
|
||||
const totalCells = this.size * this.size;
|
||||
const nonMineCells = totalCells - this.totalMines;
|
||||
if (this.revealedCount === nonMineCells) {
|
||||
this.gameOver = true;
|
||||
this.won = true;
|
||||
}
|
||||
}
|
||||
|
||||
getSize() {
|
||||
return this.size;
|
||||
}
|
||||
|
||||
getBoard() {
|
||||
return [...this.board];
|
||||
}
|
||||
|
||||
isGameOver() {
|
||||
return this.gameOver;
|
||||
}
|
||||
|
||||
hasWon() {
|
||||
return this.won;
|
||||
}
|
||||
|
||||
hasLost() {
|
||||
return this.lost;
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
...moduleFuncs,
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('minesweeper')
|
||||
.setDescription('Allows you to play a game of minesweeper')
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('size')
|
||||
.setDescription('Size in format of x * x (Default 10)')
|
||||
.setMinValue(1)
|
||||
.setMaxValue(14)
|
||||
)
|
||||
.addIntegerOption((option) =>
|
||||
option
|
||||
.setName('mines')
|
||||
.setDescription('Amount of mines in the board')
|
||||
.setMinValue(1)
|
||||
.setMaxValue(14 * 14)
|
||||
),
|
||||
buttonIds: new Map([
|
||||
['revealCells', moduleFuncs.revealCells.bind(moduleFuncs)],
|
||||
['placeFlags', moduleFuncs.placeFlags.bind(moduleFuncs)],
|
||||
['exportGame', moduleFuncs.exportGame.bind(moduleFuncs)],
|
||||
]),
|
||||
modalIds: new Map([
|
||||
['revealInput', moduleFuncs.revealCellsInput.bind(moduleFuncs)],
|
||||
['flagInput', moduleFuncs.revealCellsInput.bind(moduleFuncs)],
|
||||
]),
|
||||
disabled: true,
|
||||
};
|
||||
Reference in New Issue
Block a user