Add minesweeper (Unfinished version)

This commit is contained in:
Frostbide
2025-07-14 06:03:03 -07:00
parent c03d16bcc9
commit 1989728980

View 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,
};