pokemon-showdown/chat-plugins/scavenger-games.js
2017-06-18 22:37:43 -07:00

513 lines
16 KiB
JavaScript

/**
* Scavengers Games Plugin
* Pokemon Showdown - http://pokemonshowdown.com/
*
* This plugin stores the different possible game modes and twists that take place in scavengers room
*
* @license MIT license
*/
'use strict';
class Leaderboard {
constructor(game) {
this.game = game;
this.data = {};
}
addPoints(name, aspect, points, noUpdate) {
let userid = toId(name);
if (!userid || userid === 'constructor' || !points) return this;
if (!this.data[userid]) this.data[userid] = {name: name};
if (!this.data[userid][aspect]) this.data[userid][aspect] = 0;
this.data[userid][aspect] += points;
if (!noUpdate) this.data[userid].name = name; // always keep the last used name
return this; // allow chaining
}
visualize(sortBy, userid) {
// return a promise for async sorting - make this less exploitable
return new Promise((resolve, reject) => {
let lowestScore = Infinity;
let lastPlacement = 1;
let ladder = Object.keys(this.data)
.filter(k => sortBy in this.data[k])
.sort((a, b) => this.data[b][sortBy] - this.data[a][sortBy])
.map((u, i) => {
u = this.data[u];
if (u[sortBy] !== lowestScore) {
lowestScore = u[sortBy];
lastPlacement = i + 1;
}
return Object.assign(
{rank: lastPlacement},
u
);
}); // identify ties
if (userid) {
let rank = ladder.find(entry => toId(entry.name) === userid);
resolve(rank);
} else {
resolve(ladder);
}
});
}
htmlLadder() {
return new Promise((resolve, reject) => {
this.visualize('points').then(data => {
let display = `<div class="ladder" style="overflow-y: scroll; max-height: 170px;"><table style="width: 100%">` +
`<tr><th>Rank</th><th>Name</th><th>Points</th></tr>` +
data.map(line => `<tr><td>${line.rank}</td><td>${line.name}</td><td>${line.points}</td></tr>`).join('') +
`</table></div>`;
resolve(display);
});
});
}
}
class ScavGame extends Rooms.RoomGame {
constructor(room, gameType = "Scavenger Game") {
super(room);
this.title = gameType;
this.gameid = toId(gameType);
this.childGame = null;
this.playerCap = Infinity;
this.leaderboard = null;
this.round = 0;
// identify this as a scav game.
this.scavParentGame = true;
this.scavGame = true;
}
canJoinGame(user) {
/**
* Placeholder function that checks whether or not a player can join the current round of a scavenger game
* Used in elimination based games, where players may not be able to join in subsequent rounds.
*/
return true;
}
joinGame(user) {
if (!this.childGame) return user.sendTo(this.room, "There is no hunt to join yet.");
if (!this.canJoinGame(user)) return user.sendTo(this.room, "You are not allowed to join this hunt.");
if ((user.userid in this.players) || this._joinGame(user)) { // if user is already in this parent game, or if the user is able to join this parent game
if (this.childGame && this.childGame.joinGame) return this.childGame.joinGame(user);
}
}
// joining the parent game
_joinGame(user) {
let success = this.addPlayer(user);
if (success) user.sendTo(this.room, `You have joined the Scavenger Game - ${this.title}.`);
return success;
}
// renaming in the parent game
onRename(user, oldUserid, isJoining, isForceRenamed) {
if (!this.allowRenames || (!user.named && !isForceRenamed)) {
if (!(user.userid in this.players)) {
user.games.delete(this.id);
user.updateSearch();
}
return;
}
if (!(oldUserid in this.players)) return;
this.renamePlayer(user, oldUserid);
if (this.childGame && this.childGame.onRename) this.childGame.onRename(user, oldUserid, isJoining, isForceRenamed);
}
// get rid of childgame
destroy() {
if (this.childGame && this.childGame.destroy) this.childGame.destroy();
if (this.timer) {
clearTimeout(this.timer);
this.timer = null;
}
this.room.game = null;
this.room = null;
for (let i in this.players) {
this.players[i].destroy();
}
}
/**
* Carry forward roomgame properties of scavenger hunts
*/
onSubmit(...args) {
if (this.childGame && this.childGame.onSubmit) this.childGame.onSubmit(...args);
}
onConnect(...args) {
if (this.childGame && this.childGame.onConnect) this.childGame.onConnect(...args);
}
leaveGame(...args) {
if (this.childGame && this.childGame.leaveGame) this.childGame.leaveGame(...args);
}
setTimer(...args) {
if (this.childGame && this.childGame.setTimer) this.childGame.setTimer(...args);
}
onSendQuestion(...args) {
if (this.childGame && this.childGame.onSendQuestion) return this.childGame.onSendQuestion(...args);
}
onEnd(...args) {
if (this.childGame && this.childGame.onEnd) this.childGame.onEnd(...args);
}
onChatMessage(msg) {
if (this.childGame && this.childGame.onChatMessage) return this.childGame.onChatMessage(msg);
}
onUpdateConnection() {}
/**
* Functions for the child hunt to call once a certain condition is met
* in the child roomgame
*/
onStartEvent() {
this.round++;
}
onCompleteEvent(player) {}
onEndEvent() {}
onDestroyEvent() {
this.childGame = null;
}
/**
* General Game functions.
*/
createHunt(room, staffHost, host, gameType, questions) {
if (this.childGame) return staffHost.sendTo(this.room, "There is already a scavenger hunt in progress.");
this.onStartEvent();
this.childGame = new Rooms.ScavengerHunt(room, staffHost, host, gameType, questions, this);
return true;
}
announce(msg) {
this.room.add(`|raw|<div class="broadcast-green"><strong>${msg}</strong></div>`).update();
}
eliminate(userid) {
if (!(userid in this.players)) return false;
let name = this.players[userid].name;
this.players[userid].destroy();
if (this.childGame.eliminate) this.childGame.eliminate(userid);
delete this.players[userid];
this.playerCount--;
if (this.leaderboard) {
delete this.leaderboard.data[userid]; // remove their points in the leaderboard
}
return name;
}
}
/**
* Knockout games - slowest user, or non finishers will be eliminated.
*/
class KOGame extends ScavGame {
constructor(room) {
super(room, "Knockout Games");
}
canJoinGame(user) {
return this.round === 1 || (user.userid in this.players);
}
onStartEvent() {
this.round++;
if (this.round === 1) {
this.announce(`Knockout Games - Round 1. Everyone is welcome to join!`);
} else {
let participants = Object.keys(this.players).map(p => this.players[p].name);
this.announce(`Knockout Games - Round ${this.round}. Only the following ${participants.length} players are allowed to join: ${participants.join(', ')}.`);
}
}
onEndEvent() {
let completed = this.childGame.completed.map(u => toId(u.name)); // list of userids
if (!completed.length) {
this.announce(`No one has completed the hunt! This round has been void!`);
this.round--;
return;
}
if (this.round === 1) {
// prune the players that havent finished
for (let i in this.players) {
if (!(i in this.childGame.players) || !this.childGame.players[i].completed) this.eliminate(i); // user hasnt finished.
}
this.announce(`Congratulations to ${Chat.toListString(Object.keys(this.players).map(i => this.players[i].name))}! They have completed the first round, and have moved on to the next round!`);
return;
}
let unfinished = Object.keys(this.players).filter(id => !completed.includes(id));
if (!unfinished.length) {
unfinished = completed.slice(-1);
}
let eliminated = unfinished.map(id => this.eliminate(id)).filter(n => n); // this.eliminate() returns the players name.
if (Object.keys(this.players).length <= 1) {
// game over
let winner = this.players[Object.keys(this.players)[0]];
if (winner) {
this.announce(`Congratulations to ${winner.name} for winning the Knockout Games!`);
} else {
this.announce(`Sorry, no winners this time!`); // a catch - this should not realistically happen.
}
setImmediate(() => this.destroy()); // destroy the parent game after the child game finishes running their destroy functions.
return;
}
this.announce(`${Chat.toListString(eliminated.map(n => `<em>${n}</em>`))} ${(eliminated.length > 1 ? 'have' : 'has')} been eliminated! ${Chat.toListString(Object.keys(this.players).map(p => `<em>${this.players[p].name}</em>`))} have successfully completed the last hunt and have moved on to the next round!`);
}
}
class ScavengerGames extends ScavGame {
constructor(room) {
super(room, "Scavenger Games");
}
canJoinGame(user) {
return this.round === 1 || (user.userid in this.players);
}
onStartEvent() {
this.round++;
if (this.round === 1) {
this.announce(`Scavenger Games - Round 1. Everyone is welcome to join!`);
} else {
let participants = Object.keys(this.players).map(p => this.players[p].name);
this.announce(`Scavenger Games - Round ${this.round}. Only the following ${participants.length} players are allowed to join: ${participants.join(', ')}. You have one minute to complete the hunt!`);
setImmediate(() => this.childGame.setTimer(1));
}
}
onEndEvent() {
let completed = this.childGame.completed.map(u => toId(u.name)); // list of userids
if (!completed.length) {
this.announce(`No one has completed the hunt! This round has been voided!`);
this.round--;
return;
}
if (this.round === 1) {
// prune the players that havent finished
for (let i in this.players) {
if (!(i in this.childGame.players) || !this.childGame.players[i].completed) this.eliminate(i); // user hasnt finished.
}
this.announce(`Congratulations to ${Chat.toListString(Object.keys(this.players).map(i => this.players[i].name))}! They have completed the first round, and have moved on to the next round!`);
return;
}
let unfinished = Object.keys(this.players).filter(id => !completed.includes(id));
let eliminated = unfinished.map(id => this.eliminate(id)).filter(n => n); // this.eliminate() returns the players name.
if (Object.keys(this.players).length <= 1) {
// game over
let winner = this.players[Object.keys(this.players)[0]];
if (winner) {
this.announce(`Congratulations to ${winner.name} for winning the Knockout Games!`);
} else {
this.announce(`Sorry, no winners this time!`); // a catch - this should not realistically happen.
}
setImmediate(() => this.destroy()); // destroy the parent game after the child game finishes running their destroy functions.
return;
}
this.announce(`${Chat.toListString(eliminated.map(n => `<em>${n}</em>`))} ${(eliminated.length > 1 ? 'have' : 'has')} been eliminated! ${Chat.toListString(Object.keys(this.players).map(p => `<em>${this.players[p].name}</em>`))} have successfully completed the last hunt and have moved on to the next round!`);
}
}
class JumpStart extends ScavGame {
constructor(room) {
super(room, "Jump Start");
// the place to store both hunts
this.hunts = [];
this.completed = [];
}
// alter the create hunt function so that both hunts are entered before the actual hunt starts
createHunt(room, staffHost, host, gameType, questions) {
if (this.hunts.length === 0) {
this.hunts.push([room, staffHost, host, gameType, questions]);
staffHost.sendTo(this.room, "The first hunt has been set. Please use /starthunt again to add the second hunt.");
} else if (this.hunts.length === 1) {
this.hunts.push([room, staffHost, host, gameType, questions]);
staffHost.sendTo(this.room, "The second hunt has been set. Please use /scav game jumpstart set [seconds], [seconds]... to set how many seconds early the hints should be automatically given out.");
} else {
staffHost.sendTo(this.room, "There are already 2 hunts set for this scavenger game.");
}
return false;
}
setJumpStart(timesArray) {
if (this.jumpStartTimes) return "The times for the jump start has already been set.";
timesArray = timesArray.map(t => Number(t));
this.jumpStartTimes = [];
const MIN_WAIT_TIME = 60; // seconds
for (let i = 0; i < timesArray.length; i++) {
let diff = timesArray[i];
if (!diff || diff < 0) return "The times must be numbers greater than 0 in seconds.";
if (i) {
let prevDiff = timesArray[i - 1];
this.jumpStartTimes.push(prevDiff - diff); // make the timer call itself as one runs out
if (i === timesArray.length - 1) {
this.huntWait = diff; // the last wait tme
}
} else {
this.jumpStartTimes.push(MIN_WAIT_TIME); // the first one is always 0 + the minimum wait
}
}
if (this.jumpStartTimes.some(t => t < 0)) {
this.jumpStartTimes = null;
return "Invalid ordering of times.";
}
this.earlyTimes = timesArray;
// start the hunt
this.onStartEvent();
this.childGame = new Rooms.ScavengerHunt(...this.hunts[0], this);
}
runJumpStartTimer() {
if (this.jumpStartTimes.length) {
let targetUserId = this.completed.shift();
// if there are no more to distribute - set one timer with all of the remaining times together
if (!targetUserId) {
this.timer = setTimeout(() => {
this.onStartEvent();
this.childGame = new Rooms.ScavengerHunt(...this.hunts[1], this); // start it after the last hunt object has been destroyed
}, this.huntWait + (this.jumpStartTimes.reduce((a, b) => a + b) * 1000));
return;
}
// set the (recursive) timer to give out hints.
let duration = this.jumpStartTimes.shift() * 1000;
this.timer = setTimeout(() => {
let targetUser = Users(targetUserId);
if (targetUser) {
targetUser.sendTo(this.room, `|raw|<strong>The first hint to the next hunt is:</strong> ${Chat.parseText(this.hunts[1][4][0])}`);
targetUser.sendTo(this.room, `|notify|Early Hint|The first hint to the next hunt is: ${this.hunts[1][4][0]}`);
}
this.runJumpStartTimer();
}, duration);
} else {
// there are no more slots for early delivery - start the new hunt
this.timer = setTimeout(() => {
this.onStartEvent();
this.childGame = new Rooms.ScavengerHunt(...this.hunts[1], this);
this.room.add(`|c|~|[ScavengerManager] A scavenger hunt by ${Chat.toListString(this.childGame.hosts.map(h => h.name))} has been automatically started.`).update(); // highlight the users with "hunt by"
}, this.huntWait);
}
}
onCompleteEvent(player) {
if (this.round === 1 && this.completed.length < this.jumpStartTimes.length) {
player.sendRoom(`You will receive your hint ${this.earlyTimes.shift()} seconds ahead of time!`);
this.completed.push(player.userid);
}
}
onEndEvent() {
let completed = this.childGame.completed.map(u => toId(u.name)); // list of userids
if (!completed.length) {
this.announce(`No one has completed the hunt! Better luck next time!`);
setImmediate(() => this.destroy());
return;
}
if (this.round === 1) {
// prune the players that havent finished
for (let i in this.players) {
if (!(i in this.childGame.players) || !this.childGame.players[i].completed) this.eliminate(i); // user hasnt finished.
}
this.announce(`The early distribution of hints will start in one minute!`);
if (this.hunts.length === 2) {
this.runJumpStartTimer();
} else {
// technically should never happen
this.announce("ERROR: The scavenger game has been abruptly ended due to the lack of a second game.");
setImmediate(() => this.destroy());
}
} else {
this.announce(`Congratulations to the winners of the Jump Start game!`);
setImmediate(() => this.destroy());
}
}
}
class PointRally extends ScavGame {
constructor(room) {
super(room, "Point Rally");
this.pointDistribution = [50, 40, 32, 25, 20, 15, 10];
this.leaderboard = new Leaderboard(this);
}
getPoints(place) {
return this.pointDistribution[place] || this.pointDistribution[this.pointDistribution.length - 1];
}
onStartEvent() {
this.round++;
this.announce(`Hunt #${this.round}`);
}
onEndEvent() {
// give points
let completed = this.childGame.completed.map(e => e.name);
let length = completed.length;
for (let i = 0; i < length; i++) {
this.leaderboard.addPoints(completed[i], 'points', this.getPoints(i));
}
// post leaderboard
this.leaderboard.htmlLadder().then(html => {
this.room.add(`|raw|${html}`).update();
});
}
destroy() {
if (this.childGame && this.childGame.destroy) this.childGame.destroy();
this.room.game = null;
this.room = null;
for (let i in this.players) {
this.players[i].destroy();
}
}
}
module.exports = {
KOGame: KOGame,
JumpStart: JumpStart,
PointRally: PointRally,
ScavengerGames: ScavengerGames,
};