251 lines
8.6 KiB
JavaScript
251 lines
8.6 KiB
JavaScript
const Revolt = require('revolt.js');
|
|
const { client, config } = require('..');
|
|
const getUser = require('../util/get_user');
|
|
|
|
/**
|
|
* @type {Map<String, { createdBy: String, opponent: String, messageID: String }>}
|
|
*/
|
|
let sessions = new Map();
|
|
const gameEndEmitter = new (require('events'));
|
|
let playerSymbol = 'ඞ';
|
|
let invisColor = '242424';
|
|
// Note to myself: https://katex.org/docs/supported.html
|
|
|
|
module.exports.meta = {
|
|
name: 'connect4',
|
|
aliases: [ 'connect-4' ],
|
|
description: 'Play connect 4.'
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param { Revolt.Message } message
|
|
* @param { string[] } args
|
|
*/
|
|
module.exports.run = async (message, args) => new Promise(async (resolve, reject) => {
|
|
if ((await client.channels.fetch(message.channel)).channel_type !== 'Group')
|
|
return client.channels.sendMessage(message.channel, ':x: You can\'t play here.');
|
|
|
|
if (args[0]?.toLowerCase() === 'stop' || args[0]?.toLowerCase() === 'end') {
|
|
if (!sessions.get(message.channel)?.messageID)
|
|
return client.channels.sendMessage(message.channel, `:x: No ongoing match found.`);
|
|
|
|
sessions.delete(message.channel);
|
|
gameEndEmitter.emit(message.channel);
|
|
await client.channels.sendMessage(message.channel, `:white_check_mark: Game stopped`);
|
|
return;
|
|
}
|
|
|
|
if (sessions.get(message.channel))
|
|
return client.channels.sendMessage(message.channel, `:x: There's an ongoing match ` +
|
|
`[here](https://app.revolt.chat/channel/${message.channel}/${sessions.get(message.channel).messageID}), ` +
|
|
`please wait for it to finish or use \`${config.prefix}connect4 stop\`.`);
|
|
|
|
if (!args[0])
|
|
return client.channels.sendMessage(message.channel, ':x: Please specify the ID or username of your opponent.');
|
|
|
|
let opponent = await getUser(args.join(' '));
|
|
if (!opponent)
|
|
return client.channels.sendMessage(message.channel, 'I can\'t find that user.');
|
|
if (opponent._id === client.user._id)
|
|
return client.channels.sendMessage(message.channel, ':x: You can\'t play against me. (yet)');
|
|
if (opponent._id === message.author)
|
|
return client.channels.sendMessage(message.channel, ':x: You can\'t play against yourself, dumbus');
|
|
if (client.channels.get(message.channel).recipients?.indexOf(opponent._id) === -1)
|
|
return client.channels.sendMessage(message.channel, ':x: That user is not in this group.');
|
|
|
|
let field = [
|
|
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
|
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
|
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
|
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
|
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
|
[' ', ' ', ' ', ' ', ' ', ' ', ' '],
|
|
];
|
|
|
|
let red = await client.users.fetch(message.author);
|
|
let blue = opponent;
|
|
let currentPlayer = 'R';
|
|
let winner = null;
|
|
if (Math.random() > 0.5) [red, blue] = [blue, red];
|
|
if (Math.random() > 0.5) currentPlayer = 'B';
|
|
|
|
let text;
|
|
let msg;
|
|
let resendTip = true;
|
|
const updateMsg = async () => {
|
|
text = `Blue: <@${blue._id}> \u200b Red: <@${red._id}>\n`
|
|
+ `${winner === null ? `**${currentPlayer === 'R' ? 'Red' : 'Blue'}'s turn**` : ''}\n\u200b\n`
|
|
+ '| 1 | 2 | 3 | 4 | 5 | 6 | 7 |\n'
|
|
+ '|:-:|:-:|:-:|:-:|:-:|:-:|:-:|\n';
|
|
|
|
field.forEach(row => {
|
|
row.forEach(p => {
|
|
let color
|
|
if (p !== ' ')
|
|
color = p === 'B' ? 'blue' : 'red';
|
|
else
|
|
color = invisColor;
|
|
|
|
text += `| $\\color{${color}}\\huge\\text{\u200b ${playerSymbol} \u200b}$`;
|
|
});
|
|
text += '|\n';
|
|
});
|
|
|
|
if (winner === false) {
|
|
text += '\n\u200b\n# No winner'
|
|
} else if (winner !== null) {
|
|
if (Math.random() > 0.8) {
|
|
text += `\n\u200b\n# $\\color{${winner === 'B' ? 'red' : 'blue'}}\\huge\\text{𓀐}$ ` +
|
|
`$\\color{${winner === 'B' ? 'blue' : 'red'}}\\huge\\text{𓂺ඞ}$\n` +
|
|
`# <@${(winner === 'R' ? blue : red)._id}> got fucked by <@${(winner === 'R' ? red : blue)._id}>!`;
|
|
} else {
|
|
text += `\n\u200b\n# <@${(winner === 'R' ? red : blue)._id}> wins`;
|
|
}
|
|
} else if (resendTip) {
|
|
text += `\n\u200b\n##### Tip: Use \`${config.prefix}resend\` to move this message to the bottom`;
|
|
}
|
|
|
|
|
|
if (!msg) {
|
|
msg = await client.channels.sendMessage(message.channel, text);
|
|
sessions.set(message.channel, { messageID: msg._id, createdBy: message.author, opponent: opponent._id });
|
|
} else
|
|
await client.channels.editMessage(message.channel, msg._id, { content: text });
|
|
}
|
|
|
|
await updateMsg();
|
|
|
|
const end = awaitMessages(message.channel, mesg => {
|
|
if (mesg.content?.toLowerCase() === `${config.prefix}resend` && (mesg.author === red._id || mesg.author === blue._id)) {
|
|
let oldID = msg?._id;
|
|
msg = null;
|
|
resendTip = false;
|
|
updateMsg();
|
|
client.channels.deleteMessage(message.channel, oldID)
|
|
.catch(console.warn);
|
|
return;
|
|
}
|
|
|
|
let [num, bruh] = mesg.content.split(' ');
|
|
num = Number(num) - 1;
|
|
if (bruh || isNaN(num) || num > 6 || num < 0) return;
|
|
|
|
if (mesg.author !== red._id && mesg.author !== blue._id)
|
|
return client.channels.sendMessage(message.channel, `You are not part of this game, <@${mesg.author}>`);
|
|
|
|
if ((currentPlayer === 'R' ? red : blue)._id !== mesg.author)
|
|
return client.channels.sendMessage(message.channel, `It's not your turn, <@${mesg.author}>`);
|
|
|
|
if (field[0][num] !== ' ')
|
|
return client.channels.sendMessage(message.channel, 'That row is occupied.');
|
|
|
|
let i;
|
|
for (i = 5; field[i][num] !== ' '; i--) {}
|
|
|
|
field[i][num] = currentPlayer;
|
|
currentPlayer = currentPlayer === 'R' ? 'B' : 'R';
|
|
|
|
let w = getWinner(field);
|
|
if (w === false) {
|
|
winner = false;
|
|
client.channels.sendMessage(message.channel, 'Draw: Nobody wins');
|
|
end();
|
|
}
|
|
if (w) {
|
|
winner = w;
|
|
client.channels.sendMessage(message.channel, `<@${w === 'R' ? red._id : blue._id}> wins`);
|
|
end();
|
|
}
|
|
|
|
updateMsg();
|
|
});
|
|
});
|
|
|
|
/**
|
|
*
|
|
* @param {String} channel
|
|
* @param {function} callback
|
|
*/
|
|
|
|
const emitter = new (require('events'));
|
|
client.on('message', message => {
|
|
emitter.emit('message', message);
|
|
});
|
|
|
|
function awaitMessages(channel, callback) {
|
|
let ended = false;
|
|
const listener = emitter.on('message', message => {
|
|
if (message.channel === channel && !ended) {
|
|
callback(message);
|
|
}
|
|
});
|
|
|
|
gameEndEmitter.on(channel, () => ended = true);
|
|
return () => {
|
|
// somehow remove the listener idk how
|
|
gameEndEmitter.emit(channel);
|
|
sessions.delete(channel);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {('R'|'B'|' ')[][]} field
|
|
* @returns {null|'R'|'B'|boolean}
|
|
*/
|
|
const getWinner = field => {
|
|
// Horizontal
|
|
for (let i = 0; i < 6; i++) {
|
|
for (let j = 0; j < 4; j++) {
|
|
if (field[i][j] !== ' ' &&
|
|
field[i][j + 1] === field[i][j] &&
|
|
field[i][j + 2] === field[i][j] &&
|
|
field[i][j + 3] === field[i][j]
|
|
) return field[i][j];
|
|
}
|
|
}
|
|
|
|
// Vertical
|
|
for (let i = 0; i < 3; i++) {
|
|
for (let j = 0; j < 7; j++) {
|
|
if (field[i][j] !== ' ' &&
|
|
field[i + 1][j] === field[i][j] &&
|
|
field[i + 2][j] === field[i][j] &&
|
|
field[i + 3][j] === field[i][j]
|
|
) return field[i][j];
|
|
}
|
|
}
|
|
|
|
// Diagonal (Top left - bottom right)
|
|
for (let i = 0; i < 6; i++) {
|
|
for (let j = 0; j < 7; j++) {
|
|
if (field[i][j] !== ' ' &&
|
|
field[i + 1]?.[j + 1] === field[i][j] &&
|
|
field[i + 2]?.[j + 2] === field[i][j] &&
|
|
field[i + 3]?.[j + 3] === field[i][j]
|
|
) return field[i][j];
|
|
}
|
|
}
|
|
|
|
// Diagonal (Top right - bottom left)
|
|
for (let i = 0; i < 6; i++) {
|
|
for (let j = 0; j < 7; j++) {
|
|
if (field[i][j] !== ' ' &&
|
|
field[i - 1]?.[j + 1] === field[i][j] &&
|
|
field[i - 2]?.[j + 2] === field[i][j] &&
|
|
field[i - 3]?.[j + 3] === field[i][j]
|
|
) return field[i][j];
|
|
}
|
|
}
|
|
|
|
// Draw
|
|
let draw = true;
|
|
for (let i = 0; i < 6; i++) {
|
|
for (let j = 0; j < 7; j++) {
|
|
if (field[i][j] === ' ') draw = false;
|
|
}
|
|
}
|
|
if (draw) return false;
|
|
return null;
|
|
}
|