d-bot
Version:
A quirky Discord bot made for single, small, private servers
414 lines (401 loc) • 18.8 kB
JavaScript
// A graphical turn-based game where players try to eliminate each other with lasers
var util = require('./../util.js');
var map = require('./map.js');
var powerups = require('./powerups.js');
var fs = require('fs');
var directions = { u: { x: 0, y: -1 }, d: { x: 0, y: 1 }, l: { x: -1, y: 0 }, r: { x: 1, y: 0 } };
var sendMsg, bot;
var game = false;
var joinGame = function(user,userID) {
if(!game || game.state != 'starting') return;
function randomColor() {
var hex = '#';
for(var i = 0; i < 6; i++) { hex += util.pickInArray(util.hex); }
return hex;
}
function initPlayer() {
var userColor = randomColor(), roles = bot.servers[game.server].members[userID].roles;
for(var i = 0; i < roles.length; i++) {
var role = bot.servers[game.server].roles[roles[i]];
if(role.color) userColor = '#'+role.color.toString(16);
}
game.players[user] = { id: userID, color: userColor, channel: bot.directMessages[userID].id };
}
if(!bot.directMessages[userID]) { // Greet player if never PMed before
bot.sendMessage({
to: userID, message: 'Hi there!'
}, function(){ initPlayer(); });
} else {
initPlayer();
}
};
var newTurn = function() {
game.turn++;
var turnMessage = game.turn == 1 ? 'You can choose up to **2** commands per turn.'
+ 'If you shoot or reflect, it must be the last action.\n' +
' **move** <direction>\n' + ' **shoot** <direction>\n' + ' **reflect** <direction>\n' +
'_Example moves:_ "move right move up", "move down shoot left", "move left reflect up", "shoot down"\n' : '';
turnMessage += '__Turn '+game.turn+'__';
setTimeout(function(){ // TODO: Move this into image send callback
for(var pKey in game.players) { if(!game.players.hasOwnProperty(pKey)) continue;
if(game.players[pKey].dead) continue;
var powerNotice = game.players[pKey].powerup ? '\n You have a ' + game.players[pKey].powerup.name +
' powerup. To use it, include "powerup" in your command.' : '';
bot.sendMessage({to:game.players[pKey].id,message:turnMessage+powerNotice});
}
},3000);
};
var simulateTurn = function() {
var newPlayerPositions = {};
if(game.laserMap) game.laserMapHistory.push(game.laserMap);
game.laserMap = {};
game.movement = false;
game.reflection = false;
//game.newPowerups = false;
// First process everyone's commands
for(var pKey in game.players) { if(!game.players.hasOwnProperty(pKey)) continue;
if(!game.players[pKey].command) continue;
var player = game.players[pKey];
delete player.newX; delete player.newY;
var moveTo = { x: player.x, y: player.y };
if(moveTo.x+player.command.move.x >= 0 && moveTo.y+player.command.move.y >= 0
&& moveTo.x+player.command.move.x < map.width && moveTo.y+player.command.move.y < map.height
&& game.map[(moveTo.x+player.command.move.x)+':'+(moveTo.y+player.command.move.y)] != 'block') {
moveTo.x += player.command.move.x;
moveTo.y += player.command.move.y;
if(newPlayerPositions[moveTo.x+':'+moveTo.y]) { // If another player is moving here
var existingPlayer = newPlayerPositions[moveTo.x+':'+moveTo.y];
existingPlayer.newX = existingPlayer.x; // Cancel his move
existingPlayer.newY = existingPlayer.y;
}
}
player.reflecting = player.command.reflect;
if(player.reflecting) game.reflection = true;
player.newX = moveTo.x;
player.newY = moveTo.y;
newPlayerPositions[moveTo.x+':'+moveTo.y] = player;
}
// Simulate player movement
for(pKey in game.players) { if(!game.players.hasOwnProperty(pKey)) continue;
player = game.players[pKey];
delete game.playerMap[player.x+':'+player.y];
player.prevX = player.x;
player.prevY = player.y;
if(player.newX != player.x || player.newY != player.y) game.movement = true;
player.x = player.newX;
player.y = player.newY;
game.playerMap[player.x+':'+player.y] = player;
if(game.powerups[player.x+':'+player.y]) {
player.powerup = game.powerups[player.x+':'+player.y];
player.powerup.turn = game.turn;
delete game.powerups[player.x+':'+player.y];
}
}
game.longestLaser = 0;
for(pKey in game.players) { if(!game.players.hasOwnProperty(pKey)) continue;
player = game.players[pKey];
if(player.command.shoot) {
var addLaser = function(grid,laser) {
if(game.laserMap[grid]) {
game.laserMap[grid].push(laser);
for(var l = 0; l < game.laserMap[grid].length; l++) {
var checkLaser = game.laserMap[grid][l];
if(checkLaser != laser
&& checkLaser.to.x == laser.to.x
&& checkLaser.to.y == laser.to.y
&& checkLaser.from.x == laser.from.x
&& checkLaser.from.y == laser.from.y
&& checkLaser.player.id == laser.player.id) {
return true; // Laser is repeating itself
}
}
} else {
game.laserMap[grid] = [laser];
}
return false;
};
var lasers = [player.command.shoot];
if(player.command.powerup && player.command.powerup.name == 'Omni Power') {
lasers = [{x:0,y:1},{x:0,y:-1},{x:1,y:0},{x:-1,y:0}];
}
for(var l = 0; l < lasers.length; l++) {
var laser = {
player: player, from: {x:0,y:0}, to: lasers[l], segment: 0, x: player.x, y: player.y
};
var laserLength = 1;
var laserPoint = { x: player.x, y: player.y };
addLaser(laserPoint.x+':'+laserPoint.y,laser);
var reflected = false;
do {
laserPoint.x += laser.to.x;
laserPoint.y += laser.to.y;
var from = { x: laser.to.x * -1, y: laser.to.y * -1 };
var to = { x: laser.to.x, y: laser.to.y };
var mapGrid = game.map[laserPoint.x+':'+laserPoint.y];
var angled = false, kill = false;
if(mapGrid) {
if(mapGrid == 'block') {
break; // Stop laser at block
} else { // Angle
if(!player.command.powerup || player.command.powerup.name != 'Clipper Beam') {
var flip = mapGrid.type == 'forSlash' ? -1 : 1;
if(to.x != 0) {
to.y = to.x * flip;
to.x = 0;
} else {
to.x = to.y * flip;
to.y = 0;
}
}
if(!mapGrid.hit || mapGrid.hit > laserLength) mapGrid.hit = laserLength;
angled = mapGrid;
}
}
var playerGrid = game.playerMap[laserPoint.x+':'+laserPoint.y];
if(playerGrid) {
if(playerGrid.reflecting
&& (playerGrid.reflecting.x == from.x && playerGrid.reflecting.y == from.y
|| (playerGrid.command.powerup && playerGrid.command.powerup.name == 'Omni Power'))) {
// If player is reflecting in proper direction, or omni reflecting
to.x = from.x;
to.y = from.y;
reflected = true;
} else {
playerGrid.dead = game.turn;
kill = true;
}
}
laser = {
player: player, from: from, to: to, x: laserPoint.x, y: laserPoint.y,
segment: laserLength, reflected: reflected, angled: angled, kill: kill
};
if(addLaser(laserPoint.x+':'+laserPoint.y,laser)) {
break; // Break if laser is repeating itself
}
laserLength++;
} while(true);
game.longestLaser = Math.max(laserLength,game.longestLaser);
}
}
}
for(var p = 0; p < Object.keys(game.players).length - Object.keys(game.powerups).length; p++) {
//game.newPowerups = true;
spawnPowerup(); // Spawn powerups to match player count
}
//require('fs').writeFileSync('./rayfight/game.json', JSON.stringify(game, null, '\t'));
map.drawMap(game, function() {
for(var pKey in game.players) { if(!game.players.hasOwnProperty(pKey)) continue;
bot.uploadFile({
channel: game.players[pKey].channel,
file: fs.createReadStream(__dirname + '/turn.png')
});
}
game.turnAngles = false;
for(var mKey in game.map) { if(!game.map.hasOwnProperty(mKey)) continue;
if(game.map[mKey].type) game.map[mKey].turned = false;
if(game.map[mKey] != 'block' && game.map[mKey].hit) {
if(Math.random() > 0.5) {
game.map[mKey].type = game.map[mKey].type == 'backSlash' ? 'forSlash' : 'backSlash';
game.map[mKey].turned = true;
game.turnAngles = true;
}
game.map[mKey].hit = false;
}
}
var deadCount = 0, playerCount = Object.keys(game.players).length, winner;
for(pKey in game.players) { if(!game.players.hasOwnProperty(pKey)) continue;
game.players[pKey].command = false;
if(game.players[pKey].dead) {
deadCount++;
} else {
winner = pKey;
}
}
if(deadCount == playerCount - 1) {
gameOver(winner);
return;
} else if(deadCount == playerCount) {
gameOver(false);
return;
}
newTurn();
});
};
var spawnPowerup = function() {
do {
var px = util.randomInt(1,map.width-2),
py = util.randomInt(1,map.height-2);
} while(game.map[px+':'+py] || game.playerMap[px+':'+py]);
game.powerups[px+':'+py] = powerups.spawn(game.turn,px,py);
};
var checkCommands = function() { // Check if all players have sent a command for this turn
var missingCommands = [];
for(var pKey in game.players) { if(!game.players.hasOwnProperty(pKey)) continue;
// At least one living player still hasn't sent a command
if(!game.players[pKey].dead && !game.players[pKey].command) {
missingCommands.push(game.players[pKey]);
}
}
return missingCommands;
};
var gameOver = function(winner) {
game.state = 'ended';
console.log('game over, drawing final');
setTimeout(function(){
map.drawFinal(game, function() {
var message = winner ? '**'+winner+'** is the last man standing!':
'Game over, everybody died!';
bot.uploadFile({
channel: game.channel,
file: fs.createReadStream(__dirname + '/final.png')
});
setTimeout(function(){
sendMsg(game.channel,[message]);
for(var pKey in game.players) { if(!game.players.hasOwnProperty(pKey)) continue;
bot.sendMessage({to:game.players[pKey].id,message:message});
}
game = false;
},4000);
});
},2000);
};
module.exports = {
init: function(sm,b) { sendMsg = sm; bot = b; },
createGame: function(user,channel,server,userID) {
if(game) {
sendMsg(channel,['Sorry '+user+', a game is already in progress.']);
return;
}
sendMsg(channel,['**'+user+'** wants to play Ray Fight, type **!rf join** to play!']);
game = {
host: user, state: 'starting', server: server, channel: channel,
players: {}, turn: 0, laserMapHistory: [], frames: []
};
joinGame(user,userID);
},
joinGame: function(user,channel, userID) {
if(!game || game.state != 'starting' || game.channel != channel || game.players[user]) {
if(!game) {
sendMsg(channel,['You must create a game first with **!rf create**']);
} else if(game.state == 'playing') {
sendMsg(channel,['The game has already in progress!']);
} else if(game.players[user]) {
sendMsg(channel,[user+', you already joined!']);
}
return;
}
joinGame(user,userID);
sendMsg(channel,[''+user+' has joined the game!']);
},
startGame: function(user,channel) {
if(!game || game.state != 'starting' || game.host != user || game.channel != channel) {
if(!game) {
sendMsg(channel,['You must create a game first with **!rf create**']);
} else if(game.state != 'starting') {
sendMsg(channel,['The game is already in progress!']);
} else if(game.host != user) {
sendMsg(channel,['Sorry, only '+game.host+' may start the game.']);
}
return;
}
map.createMap(game);
for(var p = 0; p < Object.keys(game.players).length; p++) {
spawnPowerup(); // Spawn a powerup for every player
}
game.state = 'playing';
sendMsg(channel,['**The game has begun!** Check your PMs!']);
map.drawMap(game, function() {
for(var pKey in game.players) { if(!game.players.hasOwnProperty(pKey)) continue;
bot.uploadFile({
channel: game.players[pKey].channel,
file: fs.createReadStream(__dirname + '/turn.png')
});
}
newTurn();
});
},
stopGame: function(user,channel) {
if(!game || game.host != user || game.channel != channel) {
if(game.host != user) {
sendMsg(channel,['Sorry, only '+game.host+' may stop the game.']);
}
return;
}
sendMsg(channel,['**Game has been stopped.**']);
game = false;
},
kickPlayer: function(user,channel,player) {
if(!game || game.host != user || !game.players[player]) {
if(game.host != user) {
sendMsg(channel,['Sorry, only '+game.host+' may kick players.']);
} else if(!game.players[player]) {
sendMsg(channel,['Can\'t find that player in the game.']);
}
return;
}
game.players[player].dead = true;
if(checkCommands().length == 0) simulateTurn(); // Simulate turn if all players have sent commands
},
command: function(user,message) {
if(!game || game.state != 'playing' || !game.players[user]) return;
if(game.players[user].command || game.players[user].dead) return; // Already sent command, or dead
message = message.toLowerCase();
var commands = message.split(' ');
var command = { move: { x: 0, y: 0 } };
for(var c = 0; c < Math.min(commands.length,5); c++) {
commands[c] = commands[c][0];
if(commands[c] == 'p') {
command.powerup = game.players[user].powerup;
game.players[user].powerup = false;
continue;
}
if(['m','s','r'].indexOf(commands[c]) < 0) {
bot.sendMessage({to:game.players[user].id,message:'Invalid command!'});
return;
}
if(!commands[c+1] || ['u','l','r','d'].indexOf(commands[c+1][0]) < 0) {
bot.sendMessage({to:game.players[user].id,
message:'The **'+commands[c]+'** command requires a **direction**! ' +
'Up, down, left, or right.'});
return;
}
if(commands[c] == 'm') { // Add movement deltas
command.move.x += directions[commands[c+1][0]].x;
command.move.y += directions[commands[c+1][0]].y;
} else if(commands[c] == 's') {
command.shoot = { // Shoot
x: directions[commands[c+1][0]].x,
y: directions[commands[c+1][0]].y
};
} else {
command.reflect = { // Reflect
x: directions[commands[c+1][0]].x,
y: directions[commands[c+1][0]].y
};
}
c++;
}
if(command.shoot) command.reflect = false;
if(command.powerup && command.powerup.name == 'Clipper Beam' && !command.shoot) { // Can't clipper reflect
command.powerup = false;
}
bot.sendMessage({to:game.players[user].id,message:'Command received.'});
game.players[user].command = command;
setTimeout(function(){
if(checkCommands().length == 0) { // All players have sent commands
for(var pKey in game.players) { if(!game.players.hasOwnProperty(pKey)) continue;
bot.sendMessage({to:game.players[pKey].id,message:'Processing turn ' + game.turn + '...'});
}
setTimeout(simulateTurn,300);
}
},300);
}
};
// Commands
switch(params[0]) {
case 'create': rayFight.createGame(user,channelID,serverID,userID); break;
case 'start': rayFight.startGame(user,channelID); break;
case 'join': rayFight.joinGame(user,channelID,userID); break;
case 'stop': rayFight.stopGame(user,channelID); break;
case 'kick': rayFight.kickPlayer(user,channelID,params[1]); break;
}