pokemon-showdown/tournaments/index.js
2014-08-29 17:23:56 -04:00

912 lines
31 KiB
JavaScript

require('es6-shim');
const BRACKET_MINIMUM_UPDATE_INTERVAL = 2 * 1000;
const AUTO_DISQUALIFY_WARNING_TIMEOUT = 30 * 1000;
var TournamentGenerators = {
roundrobin: require('./generator-round-robin.js').RoundRobin,
elimination: require('./generator-elimination.js').Elimination
};
var Tournament;
exports.tournaments = {};
function usersToNames(users) {
return users.map(function (user) { return user.name; });
}
function createTournamentGenerator(generator, args, output) {
var Generator = TournamentGenerators[toId(generator)];
if (!Generator) {
output.sendReply(generator + " is not a valid type.");
output.sendReply("Valid types: " + Object.keys(TournamentGenerators).join(", "));
return;
}
args.unshift(null);
return new (Generator.bind.apply(Generator, args))();
}
function createTournament(room, format, generator, isRated, args, output) {
if (room.type !== 'chat') {
output.sendReply("Tournaments can only be created in chat rooms.");
return;
}
if (exports.tournaments[room.id]) {
output.sendReply("A tournament is already running in the room.");
return;
}
if (Rooms.global.lockdown) {
output.sendReply("The server is restarting soon, so a tournament cannot be created.");
return;
}
format = Tools.getFormat(format);
if (format.effectType !== 'Format') {
output.sendReply(format.id + " is not a valid format.");
output.sendReply("Valid formats: " + Object.keys(Tools.data.Formats).filter(function (f) { return Tools.data.Formats[f].effectType === 'Format'; }).join(", "));
return;
}
if (!TournamentGenerators[toId(generator)]) {
output.sendReply(generator + " is not a valid type.");
output.sendReply("Valid types: " + Object.keys(TournamentGenerators).join(", "));
return;
}
return (exports.tournaments[room.id] = new Tournament(room, format, createTournamentGenerator(generator, args, output), isRated));
}
function deleteTournament(name, output) {
var id = toId(name);
var tournament = exports.tournaments[id];
if (!tournament) {
output.sendReply(name + " doesn't exist.");
return false;
}
tournament.forceEnd(output);
delete exports.tournaments[id];
return true;
}
function getTournament(name, output) {
var id = toId(name);
if (exports.tournaments[id]) {
return exports.tournaments[id];
}
}
Tournament = (function () {
function Tournament(room, format, generator, isRated) {
this.room = room;
this.format = toId(format);
this.generator = generator;
this.isRated = isRated;
this.isBracketInvalidated = true;
this.lastBracketUpdate = 0;
this.bracketUpdateTimer = null;
this.bracketCache = null;
this.isTournamentStarted = false;
this.availableMatches = null;
this.inProgressMatches = null;
this.isAvailableMatchesInvalidated = true;
this.availableMatchesCache = null;
this.pendingChallenges = null;
this.isEnded = false;
room.add('|tournament|create|' + this.format + '|' + generator.name);
room.send('|tournament|update|' + JSON.stringify({
format: this.format,
generator: generator.name,
isStarted: false,
isJoined: false
}));
this.update();
}
Tournament.prototype.setGenerator = function (generator, output) {
if (this.isTournamentStarted) {
output.sendReply('|tournament|error|BracketFrozen');
return;
}
var isErrored = false;
this.generator.getUsers().forEach(function (user) {
var error = generator.addUser(user);
if (typeof error === 'string') {
output.sendReply('|tournament|error|' + error);
isErrored = true;
}
});
if (isErrored) return;
this.generator = generator;
this.room.send('|tournament|update|' + JSON.stringify({generator: generator.name}));
this.isBracketInvalidated = true;
this.update();
};
Tournament.prototype.forceEnd = function () {
if (this.isTournamentStarted) {
this.inProgressMatches.forEach(function (match) {
if (match) delete match.room.win;
});
}
this.isEnded = true;
this.room.add('|tournament|forceend');
this.isEnded = true;
};
Tournament.prototype.updateFor = function (targetUser, connection) {
if (!connection) connection = targetUser;
if (this.isEnded) return;
if ((!this.bracketUpdateTimer && this.isBracketInvalidated) || (this.isTournamentStarted && this.isAvailableMatchesInvalidated)) {
this.room.add(
"Error: update() called with a target user when data invalidated: " +
(!this.bracketUpdateTimer && this.isBracketInvalidated) + ", " +
(this.isTournamentStarted && this.isAvailableMatchesInvalidated) +
"; Please report this to an admin."
);
return;
}
var isJoined = this.generator.getUsers().indexOf(targetUser) >= 0;
connection.sendTo(this.room, '|tournament|update|' + JSON.stringify({
format: this.format,
generator: this.generator.name,
isStarted: this.isTournamentStarted,
isJoined: isJoined,
bracketData: this.bracketCache
}));
if (this.isTournamentStarted && isJoined) {
connection.sendTo(this.room, '|tournament|update|' + JSON.stringify({
challenges: usersToNames(this.availableMatchesCache.challenges.get(targetUser)),
challengeBys: usersToNames(this.availableMatchesCache.challengeBys.get(targetUser))
}));
var pendingChallenge = this.pendingChallenges.get(targetUser);
if (pendingChallenge && pendingChallenge.to) {
connection.sendTo(this.room, '|tournament|update|' + JSON.stringify({challenging: pendingChallenge.to.name}));
} else if (pendingChallenge && pendingChallenge.from) {
connection.sendTo(this.room, '|tournament|update|' + JSON.stringify({challenged: pendingChallenge.from.name}));
}
}
connection.sendTo(this.room, '|tournament|updateEnd');
};
Tournament.prototype.update = function (targetUser) {
if (targetUser) throw new Error("Please use updateFor() to update the tournament for a specific user.");
if (this.isEnded) return;
if (this.isBracketInvalidated) {
if (Date.now() < this.lastBracketUpdate + BRACKET_MINIMUM_UPDATE_INTERVAL) {
if (this.bracketUpdateTimer) clearTimeout(this.bracketUpdateTimer);
this.bracketUpdateTimer = setTimeout(function () {
this.bracketUpdateTimer = null;
this.update();
}.bind(this), BRACKET_MINIMUM_UPDATE_INTERVAL);
} else {
this.lastBracketUpdate = Date.now();
this.bracketCache = this.getBracketData();
this.isBracketInvalidated = false;
this.room.send('|tournament|update|' + JSON.stringify({bracketData: this.bracketCache}));
}
}
if (this.isTournamentStarted && this.isAvailableMatchesInvalidated) {
this.availableMatchesCache = this.getAvailableMatches();
this.isAvailableMatchesInvalidated = false;
this.availableMatchesCache.challenges.forEach(function (opponents, user) {
user.sendTo(this.room, '|tournament|update|' + JSON.stringify({challenges: usersToNames(opponents)}));
}, this);
this.availableMatchesCache.challengeBys.forEach(function (opponents, user) {
user.sendTo(this.room, '|tournament|update|' + JSON.stringify({challengeBys: usersToNames(opponents)}));
}, this);
}
this.room.send('|tournament|updateEnd');
};
Tournament.prototype.purgeGhostUsers = function () {
// "Ghost" users sometimes end up in the tournament because they've merged with another user.
// This function is to remove those ghost users from the tournament.
this.generator.getUsers().forEach(function (user) {
var realUser = Users.getExact(user.userid);
if (!realUser || realUser !== user) {
// The two following functions are called without their second argument,
// but the second argument will not be used in this situation
if (this.isTournamentStarted) {
if (!this.disqualifiedUsers.get(user)) {
this.disqualifyUser(user);
}
} else {
this.removeUser(user);
}
}
}, this);
};
Tournament.prototype.addUser = function (user, isAllowAlts, output) {
if (!user.named) {
output.sendReply('|tournament|error|UserNotNamed');
return;
}
if (!isAllowAlts) {
var users = this.generator.getUsers();
for (var i = 0; i < users.length; i++) {
if (users[i].latestIp === user.latestIp) {
output.sendReply('|tournament|error|AltUserAlreadyAdded');
return;
}
}
}
var error = this.generator.addUser(user);
if (typeof error === 'string') {
output.sendReply('|tournament|error|' + error);
return;
}
this.room.add('|tournament|join|' + user.name);
user.sendTo(this.room, '|tournament|update|{"isJoined":true}');
this.isBracketInvalidated = true;
this.update();
};
Tournament.prototype.removeUser = function (user, output) {
var error = this.generator.removeUser(user);
if (typeof error === 'string') {
output.sendReply('|tournament|error|' + error);
return;
}
this.room.add('|tournament|leave|' + user.name);
user.sendTo(this.room, '|tournament|update|{"isJoined":false}');
this.isBracketInvalidated = true;
this.update();
};
Tournament.prototype.replaceUser = function (user, replacementUser, output) {
var error = this.generator.replaceUser(user, replacementUser);
if (typeof error === 'string') {
output.sendReply('|tournament|error|' + error);
return;
}
this.room.add('|tournament|replace|' + user.name + '|' + replacementUser.name);
user.sendTo(this.room, '|tournament|update|{"isJoined":false}');
replacementUser.sendTo(this.room, '|tournament|update|{"isJoined":true}');
this.isBracketInvalidated = true;
this.update();
};
Tournament.prototype.getBracketData = function () {
var data = this.generator.getBracketData();
if (data.type === 'tree' && data.rootNode) {
var queue = [data.rootNode];
while (queue.length > 0) {
var node = queue.shift();
if (node.state === 'available') {
var pendingChallenge = this.pendingChallenges.get(node.children[0].team);
if (pendingChallenge && node.children[1].team === pendingChallenge.to) {
node.state = 'challenging';
}
var inProgressMatch = this.inProgressMatches.get(node.children[0].team);
if (inProgressMatch && node.children[1].team === inProgressMatch.to) {
node.state = 'inprogress';
node.room = inProgressMatch.room.id;
}
}
if (node.team) node.team = node.team.name;
node.children.forEach(function (child) {
queue.push(child);
});
}
} else if (data.type === 'table') {
if (this.isTournamentStarted) {
data.tableContents.forEach(function (row, r) {
var pendingChallenge = this.pendingChallenges.get(data.tableHeaders.rows[r]);
var inProgressMatch = this.inProgressMatches.get(data.tableHeaders.rows[r]);
if (pendingChallenge || inProgressMatch) {
row.forEach(function (cell, c) {
if (!cell) return;
if (pendingChallenge && data.tableHeaders.cols[c] === pendingChallenge.to) {
cell.state = 'challenging';
}
if (inProgressMatch && data.tableHeaders.cols[c] === inProgressMatch.to) {
cell.state = 'inprogress';
cell.room = inProgressMatch.room.id;
}
});
}
}, this);
}
data.tableHeaders.cols = usersToNames(data.tableHeaders.cols);
data.tableHeaders.rows = usersToNames(data.tableHeaders.rows);
}
return data;
};
Tournament.prototype.startTournament = function (output) {
if (this.isTournamentStarted) {
output.sendReply('|tournament|error|AlreadyStarted');
return false;
}
this.purgeGhostUsers();
if (this.generator.getUsers().length < 2) {
output.sendReply('|tournament|error|NotEnoughUsers');
return false;
}
this.generator.freezeBracket();
this.availableMatches = new Map();
this.inProgressMatches = new Map();
this.pendingChallenges = new Map();
this.disqualifiedUsers = new Map();
this.isAutoDisqualifyWarned = new Map();
this.lastActionTimes = new Map();
var users = this.generator.getUsers();
users.forEach(function (user) {
var availableMatches = new Map();
users.forEach(function (user) {
availableMatches.set(user, false);
});
this.availableMatches.set(user, availableMatches);
this.inProgressMatches.set(user, null);
this.pendingChallenges.set(user, null);
this.disqualifiedUsers.set(user, false);
this.isAutoDisqualifyWarned.set(user, false);
this.lastActionTimes.set(user, Date.now());
}, this);
this.isTournamentStarted = true;
this.autoDisqualifyTimeout = Infinity;
this.isBracketInvalidated = true;
this.room.add('|tournament|start');
this.room.send('|tournament|update|{"isStarted":true}');
this.update();
return true;
};
Tournament.prototype.getAvailableMatches = function () {
var matches = this.generator.getAvailableMatches();
if (typeof matches === 'string') {
this.room.add("Unexpected error from getAvailableMatches(): " + matches + ". Please report this to an admin.");
return;
}
var users = this.generator.getUsers();
var challenges = new Map();
var challengeBys = new Map();
var oldAvailableMatchCounts = new Map();
users.forEach(function (user) {
challenges.set(user, []);
challengeBys.set(user, []);
var availableMatches = this.availableMatches.get(user);
var oldAvailableMatchCount = 0;
availableMatches.forEach(function (isAvailable, user) {
oldAvailableMatchCount += isAvailable;
availableMatches.set(user, false);
});
oldAvailableMatchCounts.set(user, oldAvailableMatchCount);
}, this);
matches.forEach(function (match) {
challenges.get(match[0]).push(match[1]);
challengeBys.get(match[1]).push(match[0]);
this.availableMatches.get(match[0]).set(match[1], true);
}, this);
this.availableMatches.forEach(function (availableMatches, user) {
if (oldAvailableMatchCounts.get(user) !== 0) return;
var availableMatchCount = 0;
availableMatches.forEach(function (isAvailable, user) {
availableMatchCount += isAvailable;
});
if (availableMatchCount > 0) this.lastActionTimes.set(user, Date.now());
}, this);
return {
challenges: challenges,
challengeBys: challengeBys
};
};
Tournament.prototype.disqualifyUser = function (user, output) {
var error = this.generator.disqualifyUser(user);
if (error) {
output.sendReply('|tournament|error|' + error);
return false;
}
if (this.disqualifiedUsers.get(user)) {
output.sendReply('|tournament|error|AlreadyDisqualified');
return false;
}
this.disqualifiedUsers.set(user, true);
this.generator.setUserBusy(user, false);
var challenge = this.pendingChallenges.get(user);
if (challenge) {
this.pendingChallenges.set(user, null);
if (challenge.to) {
this.generator.setUserBusy(challenge.to, false);
this.pendingChallenges.set(challenge.to, null);
challenge.to.sendTo(this.room, '|tournament|update|{"challenged":null}');
} else if (challenge.from) {
this.generator.setUserBusy(challenge.from, false);
this.pendingChallenges.set(challenge.from, null);
challenge.from.sendTo(this.room, '|tournament|update|{"challenging":null}');
}
}
var matchFrom = this.inProgressMatches.get(user);
if (matchFrom) {
this.generator.setUserBusy(matchFrom.to, false);
this.inProgressMatches.set(user, null);
delete matchFrom.room.win;
matchFrom.room.forfeit(user);
}
var matchTo = null;
this.inProgressMatches.forEach(function (match, userFrom) {
if (match && match.to === user) matchTo = userFrom;
});
if (matchTo) {
this.generator.setUserBusy(matchTo, false);
var matchRoom = this.inProgressMatches.get(matchTo).room;
delete matchRoom.win;
matchRoom.forfeit(user);
this.inProgressMatches.set(matchTo, null);
}
this.room.add('|tournament|disqualify|' + user.name);
user.sendTo(this.room, '|tournament|update|{"isJoined":false}');
this.isBracketInvalidated = true;
this.isAvailableMatchesInvalidated = true;
if (this.generator.isTournamentEnded()) {
this.onTournamentEnd();
} else {
this.update();
}
return true;
};
Tournament.prototype.setAutoDisqualifyTimeout = function (timeout, output) {
if (!this.isTournamentStarted) {
output.sendReply('|tournament|error|NotStarted');
return false;
}
if (timeout < AUTO_DISQUALIFY_WARNING_TIMEOUT || isNaN(timeout)) {
output.sendReply('|tournament|error|InvalidAutoDisqualifyTimeout');
return false;
}
this.autoDisqualifyTimeout = parseFloat(timeout);
if (this.autoDisqualifyTimeout === Infinity) {
this.room.add('|tournament|autodq|off');
} else {
this.room.add('|tournament|autodq|on|' + this.autoDisqualifyTimeout);
}
this.runAutoDisqualify();
return true;
};
Tournament.prototype.runAutoDisqualify = function (output) {
if (!this.isTournamentStarted) {
output.sendReply('|tournament|error|NotStarted');
return false;
}
this.lastActionTimes.forEach(function (time, user) {
var availableMatches = 0;
this.availableMatches.get(user).forEach(function (isAvailable) {
availableMatches += isAvailable;
});
var pendingChallenge = this.pendingChallenges.get(user);
if (availableMatches === 0 && !pendingChallenge) return;
if (pendingChallenge && pendingChallenge.to) return;
if (Date.now() > time + this.autoDisqualifyTimeout && this.isAutoDisqualifyWarned.get(user)) {
this.disqualifyUser(user);
} else if (Date.now() > time + this.autoDisqualifyTimeout - AUTO_DISQUALIFY_WARNING_TIMEOUT && !this.isAutoDisqualifyWarned.get(user)) {
var remainingTime = this.autoDisqualifyTimeout - Date.now() + time;
if (remainingTime <= 0) {
remainingTime = AUTO_DISQUALIFY_WARNING_TIMEOUT;
this.lastActionTimes.set(user, Date.now() - this.autoDisqualifyTimeout + AUTO_DISQUALIFY_WARNING_TIMEOUT);
}
this.isAutoDisqualifyWarned.set(user, true);
user.sendTo(this.room, '|tournament|autodq|target|' + remainingTime);
} else {
this.isAutoDisqualifyWarned.set(user, false);
}
}, this);
};
Tournament.prototype.challenge = function (from, to, output) {
if (!this.isTournamentStarted) {
output.sendReply('|tournament|error|NotStarted');
return;
}
if (!this.availableMatches.get(from) || !this.availableMatches.get(from).get(to)) {
output.sendReply('|tournament|error|InvalidMatch');
return;
}
if (this.generator.getUserBusy(from) || this.generator.getUserBusy(to)) {
this.room.add("Tournament backend breaks specifications. Please report this to an admin.");
return;
}
this.generator.setUserBusy(from, true);
this.generator.setUserBusy(to, true);
this.isAvailableMatchesInvalidated = true;
this.purgeGhostUsers();
this.update();
from.prepBattle(this.format, 'challenge', from, this.finishChallenge.bind(this, from, to, output));
};
Tournament.prototype.finishChallenge = function (from, to, output, result) {
if (!result) {
this.generator.setUserBusy(from, false);
this.generator.setUserBusy(to, false);
this.isAvailableMatchesInvalidated = true;
this.update();
return;
}
this.lastActionTimes.set(from, Date.now());
this.lastActionTimes.set(to, Date.now());
this.pendingChallenges.set(from, {to: to, team: from.team});
this.pendingChallenges.set(to, {from: from, team: from.team});
from.sendTo(this.room, '|tournament|update|' + JSON.stringify({challenging: to.name}));
to.sendTo(this.room, '|tournament|update|' + JSON.stringify({challenged: from.name}));
this.isBracketInvalidated = true;
this.update();
};
Tournament.prototype.cancelChallenge = function (user, output) {
if (!this.isTournamentStarted) {
output.sendReply('|tournament|error|NotStarted');
return;
}
var challenge = this.pendingChallenges.get(user);
if (!challenge || challenge.from) return;
this.generator.setUserBusy(user, false);
this.generator.setUserBusy(challenge.to, false);
this.pendingChallenges.set(user, null);
this.pendingChallenges.set(challenge.to, null);
user.sendTo(this.room, '|tournament|update|{"challenging":null}');
challenge.to.sendTo(this.room, '|tournament|update|{"challenged":null}');
this.isBracketInvalidated = true;
this.isAvailableMatchesInvalidated = true;
this.update();
};
Tournament.prototype.acceptChallenge = function (user, output) {
if (!this.isTournamentStarted) {
output.sendReply('|tournament|error|NotStarted');
return;
}
var challenge = this.pendingChallenges.get(user);
if (!challenge || !challenge.from) return;
user.prepBattle(this.format, 'challenge', user, this.finishAcceptChallenge.bind(this, user, challenge));
};
Tournament.prototype.finishAcceptChallenge = function (user, challenge, result) {
if (!result) return;
// Prevent double accepts and users that have been disqualified while between these two functions
if (!this.pendingChallenges.get(challenge.from)) return;
if (!this.pendingChallenges.get(user)) return;
var room = Rooms.global.startBattle(challenge.from, user, this.format, this.isRated, challenge.team, user.team);
if (!room) return;
this.pendingChallenges.set(challenge.from, null);
this.pendingChallenges.set(user, null);
challenge.from.sendTo(this.room, '|tournament|update|{"challenging":null}');
user.sendTo(this.room, '|tournament|update|{"challenged":null}');
this.inProgressMatches.set(challenge.from, {to: user, room: room});
this.room.add('|tournament|battlestart|' + challenge.from.name + '|' + user.name + '|' + room.id);
this.isBracketInvalidated = true;
this.runAutoDisqualify();
this.update();
var self = this;
room.win = function (winner) {
self.onBattleWin(this, Users.get(winner));
return Object.getPrototypeOf(this).win.call(this, winner);
};
};
Tournament.prototype.onBattleWin = function (room, winner) {
var from = Users.get(room.p1);
var to = Users.get(room.p2);
var result = 'draw';
if (from === winner) {
result = 'win';
} else if (to === winner) {
result = 'loss';
}
if (result === 'draw' && !this.generator.isDrawingSupported) {
this.room.add('|tournament|battleend|' + from.name + '|' + to.name + '|' + result + '|' + room.battle.score.join(',') + '|fail');
this.generator.setUserBusy(from, false);
this.generator.setUserBusy(to, false);
this.inProgressMatches.set(from, null);
this.isBracketInvalidated = true;
this.isAvailableMatchesInvalidated = true;
this.runAutoDisqualify();
this.update();
return;
}
var error = this.generator.setMatchResult([from, to], result, room.battle.score);
if (error) {
// Should never happen
this.room.add("Unexpected " + error + " from setMatchResult([" + from.userid + ", " + to.userid + "], " + result + ", " + room.battle.score + ") in onBattleWin(" + room.id + ", " + winner.userid + "). Please report this to an admin.");
return;
}
this.room.add('|tournament|battleend|' + from.name + '|' + to.name + '|' + result + '|' + room.battle.score.join(','));
this.generator.setUserBusy(from, false);
this.generator.setUserBusy(to, false);
this.inProgressMatches.set(from, null);
this.isBracketInvalidated = true;
this.isAvailableMatchesInvalidated = true;
if (this.generator.isTournamentEnded()) {
this.onTournamentEnd();
} else {
this.runAutoDisqualify();
this.update();
}
};
Tournament.prototype.onTournamentEnd = function () {
this.room.add('|tournament|end|' + JSON.stringify({
results: this.generator.getResults().map(usersToNames),
format: this.format,
generator: this.generator.name,
bracketData: this.getBracketData()
}));
this.isEnded = true;
delete exports.tournaments[toId(this.room.id)];
};
return Tournament;
})();
var commands = {
basic: {
j: 'join',
in: 'join',
join: function (tournament, user) {
tournament.addUser(user, false, this);
},
l: 'leave',
out: 'leave',
leave: function (tournament, user) {
if (tournament.isTournamentStarted) {
tournament.disqualifyUser(user, this);
} else {
tournament.removeUser(user, this);
}
},
getusers: function (tournament) {
if (!this.canBroadcast()) return;
var users = usersToNames(tournament.generator.getUsers().sort());
this.sendReplyBox("<strong>" + users.length + " users are in this tournament:</strong><br />" + users.join(", "));
},
getupdate: function (tournament, user) {
tournament.updateFor(user);
this.sendReply("Your tournament bracket has been updated.");
},
challenge: function (tournament, user, params, cmd) {
if (params.length < 1) {
return this.sendReply("Usage: " + cmd + " <user>");
}
var targetUser = Users.get(params[0]);
if (!targetUser) {
return this.sendReply("User " + params[0] + " not found.");
}
tournament.challenge(user, targetUser, this);
},
cancelchallenge: function (tournament, user) {
tournament.cancelChallenge(user, this);
},
acceptchallenge: function (tournament, user) {
tournament.acceptChallenge(user, this);
}
},
creation: {
settype: function (tournament, user, params, cmd) {
if (params.length < 1) {
return this.sendReply("Usage: " + cmd + " <type> [, <comma-separated arguments>]");
}
var generator = createTournamentGenerator(params.shift(), params, this);
if (generator) tournament.setGenerator(generator, this);
},
begin: 'start',
start: function (tournament, user) {
if (tournament.startTournament(this)) {
this.sendModCommand("(" + user.name + " started the tournament.)");
}
}
},
moderation: {
dq: 'disqualify',
disqualify: function (tournament, user, params, cmd) {
if (params.length < 1) {
return this.sendReply("Usage: " + cmd + " <user>");
}
var targetUser = Users.get(params[0]);
if (!targetUser) {
return this.sendReply("User " + params[0] + " not found.");
}
if (tournament.disqualifyUser(targetUser, this)) {
this.privateModCommand("(" + targetUser.name + " was disqualified from the tournament by " + user.name + ")");
}
},
autodq: 'setautodq',
setautodq: function (tournament, user, params, cmd) {
if (params.length < 1) {
return this.sendReply("Usage: " + cmd + " <minutes|off>");
}
var timeout = params[0].toLowerCase() === 'off' ? Infinity : params[0];
if (tournament.setAutoDisqualifyTimeout(timeout * 60 * 1000, this)) {
this.privateModCommand("(The tournament auto disqualify timeout was set to " + params[0] + " by " + user.name + ")");
}
},
runautodq: function (tournament) {
tournament.runAutoDisqualify(this);
},
end: 'delete',
stop: 'delete',
delete: function (tournament, user) {
if (deleteTournament(tournament.room.title, this)) {
this.privateModCommand("(" + user.name + " forcibly ended a tournament.)");
}
}
}
};
CommandParser.commands.tour = 'tournament';
CommandParser.commands.tours = 'tournament';
CommandParser.commands.tournaments = 'tournament';
CommandParser.commands.tournament = function (paramString, room, user) {
var cmdParts = paramString.split(' ');
var cmd = cmdParts.shift().trim().toLowerCase();
var params = cmdParts.join(' ').split(',').map(function (param) { return param.trim(); });
if (!params[0]) params = [];
if (cmd === '') {
if (!this.canBroadcast()) return;
this.sendReply('|tournaments|info|' + JSON.stringify(Object.keys(exports.tournaments).filter(function (tournament) {
tournament = exports.tournaments[tournament];
return !tournament.room.isPrivate && !tournament.room.staffRoom;
}).map(function (tournament) {
tournament = exports.tournaments[tournament];
return {room: tournament.room.title, format: tournament.format, generator: tournament.generator.name, isStarted: tournament.isTournamentStarted};
})));
} else if (cmd === 'help') {
if (!this.canBroadcast()) return;
return this.sendReplyBox(
"- create/new &lt;format>, &lt;type> [, &lt;comma-separated arguments>]: Creates a new tournament in the current room.<br />" +
"- settype &lt;type> [, &lt;comma-separated arguments>]: Modifies the type of tournament after it's been created, but before it has started.<br />" +
"- end/stop/delete: Forcibly ends the tournament in the current room.<br />" +
"- begin/start: Starts the tournament in the current room.<br />" +
"- dq/disqualify &lt;user>: Disqualifies a user.<br />" +
"- autodq/setautodq &lt;minutes|off>: Sets the automatic disqualification timeout.<br />" +
"- runautodq: Manually run the automatic disqualifier.<br />" +
"- getusers: Lists the users in the current tournament.<br />" +
"- on/off: Enables/disables allowing mods to start tournaments.<br />" +
"More detailed help can be found <a href=\"https://gist.github.com/kotarou3/7872574\">here</a>"
);
} else if (cmd === 'on' || cmd === 'enable') {
if (!this.can('tournamentsmanagement', null, room)) return;
if (room.toursEnabled) {
return this.sendReply("Tournaments are already enabled.");
}
room.toursEnabled = true;
if (room.chatRoomData) {
room.chatRoomData.toursEnabled = true;
Rooms.global.writeChatRoomData();
}
return this.sendReply("Tournaments enabled.");
} else if (cmd === 'off' || cmd === 'disable') {
if (!this.can('tournamentsmanagement', null, room)) return;
if (!room.toursEnabled) {
return this.sendReply("Tournaments are already disabled.");
}
delete room.toursEnabled;
if (room.chatRoomData) {
delete room.chatRoomData.toursEnabled;
Rooms.global.writeChatRoomData();
}
return this.sendReply("Tournaments disabled.");
} else if (cmd === 'create' || cmd === 'new') {
if (room.toursEnabled) {
if (!this.can('tournaments', null, room)) return;
} else {
if (!user.can('tournamentsmanagement', null, room)) {
return this.sendReply("Tournaments are disabled in this room ("+room.id+").");
}
}
if (params.length < 2) {
return this.sendReply("Usage: " + cmd + " <format>, <type> [, <comma-separated arguments>]");
}
var tour = createTournament(room, params.shift(), params.shift(), Config.istournamentsrated, params, this);
if (tour) this.privateModCommand("(" + user.name + " created a tournament in " + tour.format + " format.)");
} else {
var tournament = getTournament(room.title);
if (!tournament) {
return this.sendReply("There is currently no tournament running in this room.");
}
var commandHandler = null;
if (commands.basic[cmd]) {
commandHandler = typeof commands.basic[cmd] === 'string' ? commands.basic[commands.basic[cmd]] : commands.basic[cmd];
}
if (commands.creation[cmd]) {
if (room.toursEnabled) {
if (!this.can('tournaments', null, room)) return;
} else {
if (!user.can('tournamentsmanagement', null, room)) {
return this.sendReply("Tournaments are disabled in this room ("+room.id+").");
}
}
commandHandler = typeof commands.creation[cmd] === 'string' ? commands.creation[commands.creation[cmd]] : commands.creation[cmd];
}
if (commands.moderation[cmd]) {
if (!user.can('tournamentsmoderation', null, room)) {
return this.sendReply(cmd + " - Access denied.");
}
commandHandler = typeof commands.moderation[cmd] === 'string' ? commands.moderation[commands.moderation[cmd]] : commands.moderation[cmd];
}
if (!commandHandler) {
this.sendReply(cmd + " is not a tournament command.");
} else {
commandHandler.call(this, tournament, user, params, cmd);
}
}
};
exports.Tournament = Tournament;
exports.TournamentGenerators = TournamentGenerators;
exports.createTournament = createTournament;
exports.deleteTournament = deleteTournament;
exports.get = getTournament;
exports.commands = commands;