discord-battleship
Version:
Create and handle advanced versions of the old classic game, BatlleShip!
353 lines (292 loc) • 24.6 kB
text/typescript
import { ColorResolvable, Message, GuildMember, Collection, MessageReaction, User, MessageEmbed, DMChannel, MessageCollector } from "discord.js";
import Board from "./data/interfaces/Board.interface";
import Boat from "./data/interfaces/Boat.interface";
import Cords from "./data/interfaces/Cords.interface";
import Game from "./data/interfaces/Game.interface";
export class DiscordBattleShip {
constructor(
public settings: {
embedColor: ColorResolvable,
prefix: string,
}
) { if (!this.settings.embedColor) this.settings.embedColor = "#6b8ba4" };
/**
* @param message The message object.
* To create, handle and finish a new battleship game, call the createGame() method. This method only accepts one parameter, which is the message object. This is also the only method in the package. This method will handle the creation of the game, DMing users, updating each board, attacking the opponent, win states and more. This is all you need to know to create a new game of battle ship!
*/
public async createGame (message: Message): Promise<Message> {
const challenger = <GuildMember>message.member; // Define the challenger
const opponent = <GuildMember>(<Collection<string, GuildMember>>message.mentions.members).first(); // Get and define the opponent
if (!opponent) return message.channel.send("Please mention a member to battle!"); // If there is no opponent, require them to define one
if (challenger.id === opponent.id) return message.channel.send("Please challenge someone other than yourself!"); // Check for prevention against challenging yourself
const accept = await message.channel.send(`Hey ${opponent}, ${challenger.user.tag} just challenged you to a game of Battle Ship. Do you accept?`); // Ask if the user would like to play
await Promise.all([accept.react("✅"), accept.react("❌")]);
const acceptFilter = (reaction: MessageReaction, user: User) => user.id === opponent.id && ["✅", "❌"].includes(reaction.emoji.name);
const acceptedData = await accept.awaitReactions(acceptFilter, { max: 1, time: 30000 });
if (acceptedData.size < 1) return accept.edit("They didn't react in time, looks like they declined.");
if ((<MessageReaction>acceptedData.first()).emoji.name === "❌") return accept.edit("Looks like they declined. \:(");
await accept.delete();
const trackingEmbed = new MessageEmbed()
.setTitle("Battle Ship Game")
.setFooter(`${challenger.user.tag} vs ${opponent.user.tag}`)
.setColor(this.settings.embedColor)
.setDescription("The game has begun! Check your DM's for instructions on how to proceed. This embed will update as the game continues.");
const trackMsg = await message.channel.send("", { embed: trackingEmbed });
const players: Game[] = [
{ collector: null, member: challenger, playerHitBoard: this.genBoard(10, 10), playerShipBoard: this.genBoard(10, 10), gameChannel: "", placedBoats: [], gameMessages: { start: "", hits: "", boats: "" }, ready: false },
{ collector: null, member: opponent , playerHitBoard: this.genBoard(10, 10), playerShipBoard: this.genBoard(10, 10), gameChannel: "", placedBoats: [], gameMessages: { start: "", hits: "", boats: "" }, ready: false },
];
let player = 0;
for (const play of players) {
const startMsg = await play.member.send(`Here is your starting Attack and Ship board! To add your boat pieces to the Ship board, please use the following command format. \`${this.settings.prefix}add <ship> <Board Cords> <direction>\`. An example of this would be, \`${this.settings.prefix}add destroyer D5 down\`\n\nAvailable Ships:\ncarrier (5)\nbattleship (4)\ndestroyer (3)\nsubmarine (3)\npatrolboat (2)`);
const hitBoard = await play.member.send(`Attack Board:\n${this.displayBoard(play.playerHitBoard, "hit")}`);
const dmBoard = await play.member.send(`Ship Board:\n${this.displayBoard(play.playerShipBoard, "ship")}`);
play.gameMessages.start = startMsg.id;
play.gameMessages.hits = hitBoard.id;
play.gameMessages.boats = dmBoard.id;
const filter = (msg: Message) => msg.author.id === play.member.id && [`${this.settings.prefix}add`, `${this.settings.prefix}attack`].includes(msg.content.split(" ")[0]);
const dmCollector = dmBoard.channel.createMessageCollector(filter);
play.collector = dmCollector;
play.gameChannel = dmBoard.channel.id;
const validBoats: Boat[] = [ { name: "carrier", length: 5, hits: 0, sunk: false }, { name: "battleship", length: 4,hits: 0, sunk: false }, { name: "destroyer", length: 3, hits: 0, sunk: false }, { name: "submarine", length: 3, hits: 0, sunk: false }, { name: "patrolboat", length: 2, hits: 0, sunk: false } ];
const validdirections = [ "up", "down", "right", "left" ];
dmCollector.on("collect", async (msg: Message) => {
const argument = msg.content.slice(this.settings.prefix.length).trim().split(/ +/g);
const cmd = argument.shift();
if (!(<Game>players.find(plr => plr.member.id === msg.author.id)).ready) {
if (cmd === "add") {
const currPlayer = players.find(plr => plr.member.id === msg.author.id);
const gameChannelObject = <DMChannel>message.client.channels.cache.get(currPlayer.gameChannel);
const boatType = argument[0];
if (!boatType) return msg.channel.send("Please provide a boat to place.").then(m => m.delete({ timeout: 15000 }));
if (!validBoats.some(value => value.name === boatType.toLowerCase())) return msg.channel.send("Please provide a valid boat type to place.").then(m => m.delete({ timeout: 15000 }));
if ((<Game>players.find(plyr => plyr.member.id === msg.author.id)).placedBoats.some(data => data.name === boatType.toLowerCase())) return msg.channel.send("You already placed that boat. Please try a different one.").then(m => m.delete({ timeout: 15000 }));
const cords = argument[1];
if (!cords) return msg.channel.send("Please enter cords for your ship. Ex: `D5`").then(m => m.delete({ timeout: 15000 }));
const directionRegex = /[a-z]([1-9]|10)/i;
if (!cords.match(directionRegex)) return msg.channel.send("Please enter valid cords for your ship. Ex: `D5`").then(m => m.delete({ timeout: 15000 }));
const direction = argument[2];
if (!direction) return msg.channel.send("Please provide a direction to position your boat!").then(m => m.delete({ timeout: 15000 }));
if (!validdirections.some(value => value === direction.toLowerCase())) return msg.channel.send(`Please provide a valid direction. Valid Choices: ${validdirections.join(", ")}`).then(m => m.delete({ timeout: 15000 }));
const checked = this.checkBoatPos(play.playerShipBoard, <Boat>validBoats.find(data => data.name === boatType.toLowerCase()), { letter: cords[0], number: parseInt(cords.slice(1)), cord: cords }, direction, "check");
if (!checked) return msg.channel.send(`You can't put the ${boatType} at ${cords} facing ${direction}`).then(m => m.delete({ timeout: 15000 }));
currPlayer.placedBoats.push(<Boat>validBoats.find(data => data.name === boatType.toLowerCase()));
const reRender = this.checkBoatPos((<Game>players.find(plyr => plyr.member.id === msg.author.id)).playerShipBoard, <Boat>validBoats.find(boat => boat.name === boatType.toLowerCase()), { letter: cords[0], number: parseInt(cords.slice(1)), cord: cords }, direction, "render");
currPlayer.playerShipBoard = reRender.board
gameChannelObject.messages.cache.get(currPlayer.gameMessages.boats).edit(`Ship Board:\n${this.displayBoard(reRender.board, "ship")}`);
let oldBoat = players.find(p => p.member.id === msg.author.id).placedBoats.find(b => b.name.toLowerCase() === reRender.boat.name.toLowerCase());
oldBoat = reRender.boat;
const editMe = gameChannelObject.messages.cache.get(currPlayer.gameMessages.start);
const regex = new RegExp(boatType.toLowerCase(), "ig");
editMe.edit(editMe.content.replace(regex, `~~${boatType.toLowerCase()}~~`));
if (currPlayer.placedBoats.length === 5) {
currPlayer.ready = true;
if (players[0].ready && players[1].ready) {
for (const playr of players) {
const perGame = <DMChannel>message.client.channels.cache.get(playr.gameChannel);
perGame.messages.cache.get(playr.gameMessages.start).edit(`You have both now finished the setup phase of the game! It is ${players[player].member.user.tag}'s turn to attack! Use \`${this.settings.prefix}attack <cords>\` to call an attack on that spot!\n\nLegend:\n- Attack Board:\n--- ◻️ = Empty Spot\n--- ⚪ = Missed Attack\n--- 🔴 = Hit Attack\n- Ship Board:\n--- 🔲 = Empty Spot\n--- 🟩 = Unhit Ship\n--- 🟥 = Hit Ship\n--- ⚪ = Missed Opponent Shot`);
playr.member.send(`${playr.member.user}`).then(m => m.delete());
}
trackingEmbed.setDescription("");
for (const p of players) {
trackingEmbed.addField(p.member.user.tag, `Has ${p.placedBoats.filter(b => !b.sunk).length} ships left!\n\n${p.placedBoats.map(b => b.sunk ? `❌ ${b.name}` : `✅ ${b.name}`).join("\n")}`);
}
trackMsg.edit(trackingEmbed);
} else return msg.channel.send("It looks like your opponent hasn't placed all of their ships yet! Please wait for them to finish. Once they finish you will get a DM.").then(m => m.delete({ timeout: 15000 }));
}
}
} else if (players[0].ready && players[1].ready) {
if (players[player].member.id === msg.author.id) {
if (cmd === "attack") {
const playerChannel = <DMChannel>message.client.channels.cache.get(players[player].gameChannel);
const opponentChannel = <DMChannel>message.client.channels.cache.get(players[(player + 1) % players.length].gameChannel);
const cords = argument[0];
if (!cords) return msg.channel.send("Please enter cords for your attack. Ex: `D5`").then(m => m.delete({ timeout: 15000 }));
const directionRegex = /[a-z]([1-9]|10)/i;
if (!cords.match(directionRegex)) return msg.channel.send("Please enter valid cords for your attack. Ex: `D5`").then(m => m.delete({ timeout: 15000 }));
const returnData = this.attack(players[player].playerHitBoard, players[(player + 1) % players.length].playerShipBoard, { letter: cords[0], number: parseInt(cords.slice(1)), cord: cords });
if (!returnData) return msg.channel.send("You can't attack there, please try somewhere else!").then(m => m.delete({ timeout: 15000 }));
playerChannel.messages.cache.get(players[player].gameMessages.hits).edit(`Attack Board:\n${this.displayBoard(returnData.attackBoard, "hit")}`);
players[player].playerHitBoard = returnData.attackBoard;
opponentChannel.messages.cache.get(players[(player + 1) % players.length].gameMessages.boats).edit(`Ship Board:\n${this.displayBoard(returnData.shipBoard, "ship")}`);
players[(player + 1) % 2].playerShipBoard = returnData.shipBoard;
const shipToHit = players[(player + 1) % players.length].placedBoats.find(s => s.name.toLowerCase() === returnData.shipName.toLowerCase());
if (shipToHit) {
shipToHit.hits++;
if (shipToHit.hits === shipToHit.length) {
shipToHit.sunk = true;
players[player].member.send(`You sunk ${players[(player + 1) % players.length].member.user.tag}'s ${shipToHit.name}!`);
players[(player + 1) % players.length].member.send(`Your ${returnData.shipName} was sunk!`);
const embed = new MessageEmbed()
.setTitle("Battle Ship Game")
.setFooter(`${challenger.user.tag} vs ${opponent.user.tag}`)
.setColor(this.settings.embedColor)
for (const p of players) {
embed.addField(p.member.user.tag, `Has ${p.placedBoats.filter(b => !b.sunk).length} ships left!\n\n${p.placedBoats.map(b => b.sunk ? `❌ ${b.name}` : `✅ ${b.name}`).join("\n")}`);
}
trackMsg.edit("", { embed });
}
}
if (this.winCondition(players[(player + 1) % players.length].placedBoats)) {
for (const p of players) {
p.collector.stop();
p.member.send(`${players[player].member.user} won the game!`);
}
const embed = new MessageEmbed()
.setTitle("Battle Ship Game")
.setFooter(`${challenger.user.tag} vs ${opponent.user.tag}`)
.setColor(this.settings.embedColor)
.setDescription(`${players[player].member.user} has won the game!`)
trackMsg.edit(`${players[0].member}, ${players[1].member}`, { embed });
}
playerChannel.messages.cache.get(players[player].gameMessages.start).edit(`It is now ${players[(player + 1) % players.length].member.user.tag}'s turn! Use \`${this.settings.prefix}attack <cords>\` to call an attack on that spot!\n\nLegend:\n- Attack Board:\n--- ◻️ = Empty Spot\n--- ⚪ = Missed Attack\n--- 🔴 = Hit Attack\n- Ship Board:\n--- 🔲 = Empty Spot\n--- 🟩 = Unhit Ship\n--- 🟥 = Hit Ship\n--- ⚪ = Missed Opponent Shot`);
opponentChannel.messages.cache.get(players[(player + 1) % players.length].gameMessages.start).edit(`It is now ${players[(player + 1) % players.length].member.user.tag}'s turn! Use \`${this.settings.prefix}attack <cords>\` to call an attack on that spot!\n\nLegend:\n- Attack Board:\n--- ◻️ = Empty Spot\n--- ⚪ = Missed Attack\n--- 🔴 = Hit Attack\n- Ship Board:\n--- 🔲 = Empty Spot\n--- 🟩 = Unhit Ship\n--- 🟥 = Hit Ship\n--- ⚪ = Missed Opponent Shot`);
player = (player + 1) % players.length;
players[player].member.send(`${players[player].member.user}`).then(m => m.delete());
}
} else return msg.channel.send("It isn't your turn yet. Please wait for the opponent to attack.").then(m => m.delete({ timeout: 10000 }));
} else return msg.channel.send("It looks like the opponent/you hasn't/haven't placed all their/your ships. Please either finish placing your ships or wait for your opponent to finish!").then(m => m.delete({ timeout: 10000 }));
});
}
return message.channel.send("In progress.");
}
private winCondition (boats: Boat[]) {
for (const playerBoat of boats) {
if (!playerBoat.sunk) return false;
}
return true;
}
private attack(attackBoard: Board[][], shipBoard: Board[][], spot: Cords) {
let shipName: string = "";
for (let i = 0; i < shipBoard.length; i++) {
const index = shipBoard[i].findIndex(data => data.cords.cord.toLowerCase() === spot.cord.toLowerCase());
if (shipBoard[i].find(data => data.cords.cord.toLowerCase() === spot.cord.toLowerCase())) {
// Missed attack
if (shipBoard[i][index].data === "0") {
shipBoard[i][index].data = "3";
attackBoard[i][index].data = "1";
// Successful attack
} else if (shipBoard[i][index].data === "1") {
shipBoard[i][index].data = "2";
attackBoard[i][index].data = "2";
shipName = shipBoard[i][index].ship;
} else return false;
}
}
return { shipBoard, attackBoard, shipName };
}
private checkBoatPos(board: Board[][], boat: Boat, cords: Cords, direction: string, type: "check" | "render") {
for (let i = 0; i < board.length; i++) {
if (board[i].find(data => data.cords.cord.toLowerCase() === cords.cord.toLowerCase())) {
switch (direction) {
case "up":
let countUp = 0;
let startPosUp = i;
do {
if (type === "check") {
if (board[startPosUp] === undefined) return;
if (board[startPosUp][cords.number - 1].data === "1") return;
countUp++;
startPosUp--;
} else {
board[startPosUp][cords.number - 1].data = "1";
board[startPosUp][cords.number - 1].ship = boat.name;
countUp++;
startPosUp--;
}
} while (countUp < boat.length);
break;
case "down":
let countDown = 0;
let startPosDown = i;
do {
if (type === "check") {
if (board[startPosDown] === undefined) return;
if (board[startPosDown][cords.number - 1].data === "1") return;
countDown++
startPosDown++;
} else {
board[startPosDown][cords.number - 1].data = "1";
board[startPosDown][cords.number - 1].ship = boat.name;
countDown++
startPosDown++;
}
} while (countDown < boat.length);
break;
case "left":
let countLeft = 0;
let currIndexLeft = board[i].findIndex(data => data.cords.cord.toLowerCase() === cords.cord.toLowerCase());
do {
if (type === "check") {
currIndexLeft--;
if (board[i][currIndexLeft] === undefined) return;
if (board[i][currIndexLeft].data === "1") return;
countLeft++;
} else {
board[i][currIndexLeft].data = "1";
board[i][currIndexLeft].ship = boat.name;
currIndexLeft--;
countLeft++;
}
} while (countLeft < boat.length);
break;
case "right":
let countRight = 0;
let currIndexRight = board[i].findIndex(data => data.cords.cord.toLowerCase() === cords.cord.toLowerCase());
do {
if (type === "check") {
currIndexRight++;
if (board[i][currIndexRight] === undefined) return;
if (board[i][currIndexRight].data === "1") return;
countRight++;
} else {
board[i][currIndexRight].data = "1";
board[i][currIndexRight].ship = boat.name;
currIndexRight++;
countRight++;
}
} while (countRight < boat.length);
break;
}
}
}
return { boat, board };
}
private genBoard(hor: number, ver: number) {
let whileCounter = 0;
const boardLetter = [ { i: 0, letter: "A" }, { i: 1, letter: "B" }, { i: 2, letter: "C" }, { i: 3, letter: "D" }, { i: 4, letter: "E" }, { i: 5, letter: "F" }, { i: 6, letter: "G" }, { i: 7, letter: "H" }, { i: 8, letter: "I" }, { i: 9, letter: "J" } ];
const doneData: { data: string, ship: string, cords: { letter: string, number: number, cord: string } }[][] = [];
do {
const temp: { data: string, ship: string, cords: { letter: string, number: number, cord: string } }[] = [];
for (let i = 0; i < ver; i++) {
const boardLttr = (<{ i: number; letter: string; }>boardLetter.find(data => data.i === whileCounter)).letter;
temp.push({ data: "0", ship: "", cords: { letter: boardLttr, number: i + 1, cord: boardLttr + (i + 1) } });
}
doneData.push(temp);
whileCounter++;
} while (whileCounter < hor);
return doneData;
}
private displayBoard(board: Board[][], type: "hit" | "ship") {
let returnData = "";
returnData = returnData.concat("⬛1️⃣2️⃣3️⃣4️⃣5️⃣6️⃣7️⃣8️⃣9️⃣🔟\n");
for (let i = 0; i < board.length; i++) {
let temp = "";
const leftEmoji = [ { i: 0, emoji: ":regional_indicator_a:" }, { i: 1, emoji: ":regional_indicator_b:" }, { i: 2, emoji: ":regional_indicator_c:" }, { i: 3, emoji: ":regional_indicator_d:" }, { i: 4, emoji: ":regional_indicator_e:" }, { i: 5, emoji: ":regional_indicator_f:" }, { i: 6, emoji: ":regional_indicator_g:" }, { i: 7, emoji: ":regional_indicator_h:" }, { i: 8, emoji: ":regional_indicator_i:" }, { i: 9, emoji: ":regional_indicator_j:" } ]
if (type === "hit") {
for (let j = 0; j < board[i].length; j++) {
// "0" is an empty space, "1" is a missed shot, "2" is a hit shot
temp += `${board[i][j].data === "0" ? "◻️" : board[i][j].data === "1" ? "⚪" : "🔴" }`;
}
} else {
for (let j = 0; j < board[i].length; j++) {
// "0" is an empty space, "1" is a unhit ship piece, "2" is a hit ship piece, "3" is a missed shot from opponent
temp += `${board[i][j].data === "0" ? "◻️" : board[i][j].data === "1" ? "🟩" : board[i][j].data === "2" ? "🟥" : "⚪" }`;
}
}
returnData += (<{ i: number, emoji: string }>leftEmoji.find(object => object.i === i)).emoji + temp + "\n"
}
return returnData;
}
}
module.exports.DiscordBattleShip = DiscordBattleShip;