UNPKG

la-cosa-nostra

Version:

A Mafia bot designed to run in Discord - beware the traitors and the lies!

2,032 lines (1,334 loc) 46.7 kB
/* By the time this instance is initialised, the roles should already be defined */ var logger = process.logger; var executable = require("../executable.js"); var alphabets = require("../alpha_table.js"); var Actions = require("./Actions.js"); var Player = require("./Player.js"); var expansions = require("../expansions.js"); var auxils = require("../auxils.js"); var flavours = require("../flavours.js"); var lcn = require("../../lcn.js"); module.exports = class { constructor (client, config) { this.client = client; this.config = config; this.temp = new Object(); } init (players) { this.players = players; this.init_time = new Date(); this.actions = new Actions().init(this); this.trial_vote_operations = new Array(); this.players_tracked = players.length; this.fast_forward_votes = new Array(); this.channels = new Object(); this.period_log = new Object(); this.intro_messages = new Array(); this.period = this.config["game"]["day-zero"] ? 0 : 1; this.steps = 0; this.state = "pre-game"; this.flavour_identifier = this.config["playing"]["flavour"]; this.voting_halted = false; this.game_config_override = new Object(); // Timezone is GMT relative this.timezone = this.config.time.timezone; for (var i = expansions.length - 1; i >= 0; i--) { var game_init = expansions[i].scripts.game_init; if (!game_init) { continue; }; game_init(this); }; this.primeDesignatedTime(); for (var i = 0; i < this.players.length; i++) { this.players[i].setGame(this); this.players[i].postGameInit(); }; return this; } primeDesignatedTime () { // Always start day zero at closest 12pm/am var current = new Date(); var utc_hour = current.getUTCHours(); var now = new Date(); current.setUTCHours(-this.timezone, 0, 0, 0); while (current.getTime() - now.getTime() < 0) { current.setUTCHours(current.getUTCHours() + 24); }; this.start_time = current; this.current_time = current; this.next_action = new Date(current); } setChannel (name, channel) { // This.channels stores SPECIAL channels, // not the private ones // not the logging ones either this.channels[name] = {id: channel.id, name: channel.name, created_at: channel.createdAt}; }; getChannel (name) { var guild = this.getGuild(); if (!this.channels[name]) { return undefined; }; return guild.channels.get(this.channels[name].id); } getChannelById (id) { var guild = this.getGuild(); return guild.channels.get(id); } getPlayerById (id) { for (var i = 0; i < this.players.length; i++) { if (this.players[i].id === id) { return this.players[i]; }; }; return null; } getPlayerByIdentifier (identifier) { for (var i = 0; i < this.players.length; i++) { if (this.players[i].identifier === identifier) { return this.players[i]; }; }; return null; } getPlayer (argument) { // Flexible if (argument instanceof Player) { return argument; }; var id = this.getPlayerById(argument); var identifier = this.getPlayerByIdentifier(argument); return id || identifier; } getPlayerByAlphabet (alphabet) { for (var i = 0; i < this.players.length; i++) { if (this.players[i].alphabet === alphabet.toUpperCase()) { return this.players[i]; }; }; return null; } getAlive () { // Count number alive var count = new Number(); for (var i = 0; i < this.players.length; i++) { count += this.players[i].status.alive ? 1 : 0; }; return count; } getAlivePlayers () { var alive = new Array(); for (var i = 0; i < this.players.length; i++) { if (this.players[i].status.alive) { alive.push(this.players[i]); }; }; return alive; } async createTrialVote (load_preemptives=true) { var messages = await executable.misc.createTrialVote(this); var period_log = this.getPeriodLog(); period_log.trial_vote = {messages: new Array(), channel: messages[0].channel.id}; for (var i = 0; i < messages.length; i++) { period_log.trial_vote.messages.push(messages[i].id); }; this.save(); this.instantiateTrialVoteCollector(); if (load_preemptives) { this.loadPreemptiveVotes(); }; } async instantiateTrialVoteCollector () { var period_log = this.getPeriodLog(); if (period_log === undefined || period_log.trial_vote === null) { return null; }; var channel = this.client.channels.get(period_log.trial_vote.channel); this.temp.trial_collectors = new Array(); for (var i = 0; i < period_log.trial_vote.messages.length; i++) { var message = await channel.fetchMessage(period_log.trial_vote.messages[i]); // Casting instance to bigger scope var run_as = this; this.temp.trial_collectors.push(message.createReactionCollector(function (reaction, user) { run_as.__receivedTrialVote(reaction, user); })); }; } clearTrialVoteCollectors () { if (!this.temp.trial_collectors) { return null; }; // Remove promises to free up memory for (var i = 0; i < this.temp.trial_collectors.length; i++) { this.temp.trial_collectors[i].stop("Autocleared"); }; } clearTrialVoteReactions (remove_extra=true) { var period_log = this.getPeriodLog(); if (period_log.trial_vote === null) { return null; }; var channel_id = period_log.trial_vote.channel; var messages_id = period_log.trial_vote.messages; for (var i = 0; i < messages_id.length; i++) { if (i < 1 || !remove_extra) { executable.misc.clearReactions(this, channel_id, messages_id[i]); }; if (i > 0 && remove_extra) { executable.misc.deleteMessage(this, channel_id, messages_id[i]); }; }; this.clearTrialVoteCollectors(); } async __receivedTrialVote (reaction, user) { if (user.bot) { return null; }; reaction.remove(user); if (!this.isAlive(user.id)) { logger.log(3, user.id + " tried to vote on the trial although they are either dead or not in the game!"); await user.send(":x: You are not alive and in the game, please do not vote in the trials! If you try that again, I will have you kicked."); return null; }; var reversed = auxils.flipObject(alphabets); var emote = reaction.emoji; var alphabet = reversed[emote]; // Count the vote var voter = this.getPlayerById(user.id); // Cycle through special vote types var special_vote_types = this.getPeriodLog().special_vote_types; for (var i = 0; i < special_vote_types.length; i++) { var vote_type = special_vote_types[i]; if (emote.name === vote_type.emote) { this.toggleVote(voter, vote_type.identifier, true); return null; }; }; if (alphabet === undefined) { return null; }; if (alphabet === "nl") { if (!this.config["game"]["lynch"]["no-lynch-option"]) { logger.log(3, user.id + " tried voting no-lynch using the reaction poll but no-lynches are disabled!"); await user.send(":x: The no-lynch vote is disabled."); return null; }; var voted_against = "nl"; } else { var voted_against = this.getPlayerByAlphabet(alphabet); if (voted_against === null) { return null; }; // Bug check if (!voted_against.status.alive) { logger.log(3, "Dead player voted on!"); await user.send(":x: You voted on a dead player! Sorry man, but the dude is already dead!"); return null; }; }; this.toggleVote(voter, voted_against); } toggleVote (voter, voted_against, special_vote=false) { // Post corresponding messages if (this.voting_halted) { return null; }; if (voter.getStatus("vote-blocked")) { return null; }; var no_lynch_vote = voted_against === "nl" && !special_vote; var voted_no_lynch = this.isVotingNoLynch(voter.identifier); // Check for (a) singular (b) total lynch counts var special_vote_types = this.getPeriodLog().special_vote_types; var special_votes_from = special_vote_types.filter(x => x.voters.some(y => y.identifier === voter.identifier)); var voted_singular = special_votes_from.some(x => x.singular); var magnitude = voter.getVoteMagnitude(); if (no_lynch_vote) { if (voted_singular) { return false; }; // NL vote is independent var has_voted = (this.votesFrom(voter.identifier).length + special_votes_from.length) > 0; if (has_voted) { return false; }; var before_votes = this.getNoLynchVoteCount(); // Count NL vote if (voted_no_lynch) { // Remove no-lynch vote this.clearNoLynchVotesBy(voter.identifier); executable.misc.removedNolynch(this, voter); } else { this.addNoLynchVote(voter.identifier, magnitude); executable.misc.addedNolynch(this, voter); }; var after_votes = this.getNoLynchVoteCount(); this.__checkLynchAnnouncement("nl", before_votes, after_votes); } else if (!special_vote) { if (voted_no_lynch) { return false; }; if (voted_singular) { return false; }; var already_voting = voted_against.isVotedAgainstBy(voter.identifier); if (!already_voting && (this.votesFrom(voter.identifier).length + special_votes_from.length) >= this.getLynchesAvailable()) { // New vote, check if exceeds limit return false; }; var before_votes = voted_against.countVotes(); var toggle_on = voted_against.toggleVotes(voter.identifier, magnitude); var after_votes = voted_against.countVotes(); if (toggle_on) { // New vote // OLD SYSTEM: uses IDs directly executable.misc.addedLynch(this, voter, voted_against); } else { executable.misc.removedLynch(this, voter, voted_against); }; this.__checkLynchAnnouncement(voted_against.identifier, before_votes, after_votes); } else { // Special vote special_vote = special_vote_types.find(x => x.identifier === voted_against); if (voted_no_lynch) { return false; }; // Check already voting var already_voting = special_vote.voters.some(x => x.identifier === voter.identifier); if (!already_voting && ((this.votesFrom(voter.identifier).length + special_votes_from.length) >= this.getLynchesAvailable() || voted_singular)) { // New vote, check if exceeds limit return false; }; // Toggle votes if (already_voting) { // Remove special vote special_vote.voters = special_vote.voters.filter(x => x.identifier !== voter.identifier); this.execute("unvote", {target: "s/" + voted_against, voter: voter.identifier}); } else { special_vote.voters.push({identifier: voter.identifier, magnitude: magnitude}); this.execute("vote", {target: "s/" + voted_against, voter: voter.identifier}); }; }; this.__reloadTrialVoteMessage(); // Save file this.save(); if (this.hammerActive() && !this.voting_halted) { this.__checkLynchHammer(); }; return true; } addSpecialVoteType (identifier, name, emote, singular) { // {reaction, name, singular} var period_log = this.getPeriodLog(); period_log.special_vote_types.push({ identifier: identifier, name: name, emote: emote, singular: singular, voters: new Array() }); } getVotesBy (identifier) { var ret = new Array(); for (var i = 0; i < this.players.length; i++) { if (this.players[i].isVotedAgainstBy(identifier)) { ret.push(this.players[i]); }; }; return ret; } votesFrom (identifier) { // Get everyone someone is voting against var roles = new Array(); for (var i = 0; i < this.players.length; i++) { if (this.players[i].isVotedAgainstBy(identifier)) { roles.push(this.players[i]); }; }; return roles; } __checkLynchAnnouncement (identifier, before, after) { var role = this.getPlayerByIdentifier(identifier); if (identifier === "nl") { var required = this.getNoLynchVotesRequired(); } else { var required = this.getVotesRequired() - role.getVoteOffset(); }; // !this.config["game"]["lynch"]["top-voted-lynch"] && !this.hammerActive() if (!this.hammerActive() && !this.config["game"]["lynch"]["top-voted-lynch"]) { if (before < required && after >= required) { // New lynch identifier === "nl" ? executable.misc.nolynchReached(this) : executable.misc.lynchReached(this, role); } else if (before >= required && after < required) { identifier === "nl" ? executable.misc.nolynchOff(this) : executable.misc.lynchOff(this, role); }; }; } __checkLynchHammer () { var no_lynch_votes = this.getNoLynchVoteCount(); if (no_lynch_votes >= this.getNoLynchVotesRequired()) { this.fastforward(); return true; }; // Checks for all potential hammers for (var i = 0; i < this.players.length; i++) { var votes = this.players[i].countVotes(); var required = this.getVotesRequired() - this.players[i].getVoteOffset(); if (votes >= required) { // Execute the player //var success = this.lynch(this.players[i]); // Fastforward cycle this.fastforward(); return true; }; }; return false; } __reloadTrialVoteMessage () { executable.misc.editTrialVote(this); } clearAllVotesBy (identifier) { // Clears all the votes on other people // by id specified var cleared = false; for (var i = 0; i < this.players.length; i++) { cleared = cleared || this.players[i].clearVotesBy(identifier); }; return cleared; } clearVotes (edit_trial=false) { // Clear ALL votes for (var i = 0; i < this.players.length; i++) { this.players[i].clearVotes(); }; if (edit_trial) { this.__reloadTrialVoteMessage(); }; } isAlive (id) { var alive = this.getAlivePlayers(); for (var i = 0; i < alive.length; i++) { if (alive[i].id === id) { return true; }; }; return false; } async step (adjust_to_current_time=false) { // Synced with Timer class // Should return next date // this.config.time.day var addition = this.state === "pre-game" ? 0 : 1; if (adjust_to_current_time) { this.current_time = new Date(); var time = new Date(); time.setUTCHours(time.getUTCHours() + 1, 0, 0, 0); this.next_action = calculateNextAction(time, this.period + addition, this.config); } else { this.current_time = new Date(this.next_action); this.next_action = calculateNextAction(this.next_action, this.period + addition, this.config); }; if (this.state === "pre-game") { this.__routines(); this.cycle(); await this.__start(); // Periodic updates are handled in roles/postRoleIntroduction // because of async issues // Player routines in start //this.__playerRoutines(); this.execute("postcycle", {period: this.period}); } else if (this.state === "playing") { this.voting_halted = true; // Print period in private channel await this.messagePeriodicUpdate(1); // Handles actions, // closes trial votes, etc. // i.e. dawn/dusk time await this.precycle(); this.steps += 1; this.period += 1; // Create period log this.__routines(); // Broadcast var broadcast = this.getBroadcast(-1, true); await executable.misc.postNewPeriod(this, broadcast); // Win check this.checkWin(); if (this.state === "ended") { this.save(); return null; }; // Open Mafia chat, create votes, routine stuff this.cycle(); // Player routines - configurable await this.__playerRoutines(); this.execute("postcycle", {period: this.period}); } else { return null; }; // Save this.voting_halted = false; this.save(); return this.next_action; function calculateNextAction (time, period, config) { var divided = period % 2; // Clone time obj time = new Date(time); if (divided === 0) { // Daytime time.setUTCHours(time.getUTCHours() + config["time"]["day"]); } else { time.setUTCHours(time.getUTCHours() + config["time"]["night"]); }; return time; }; } async fastforward () { this.voting_halted = true; return await this.timer.fastforward(); } async precycle () { this.clearPeriodPins(); if (this.period % 2 === 0) { await executable.misc.editTrialVote(this, true); this.clearTrialVoteReactions(); // Dusk this.checkLynches(); this.clearVotes(); }; this.execute("cycle", {period: this.period}); this.enterDeathMessages(); this.sendMessages(); } async messagePeriodicUpdate (offset=0) { await this.messageAll("~~ ~~ **" + this.getFormattedDay(offset) + "**", "permanent"); } async messageAll (message, pin=false) { for (var i = 0; i < this.players.length; i++) { await new Promise(function(resolve, reject) { setTimeout(function () { resolve(); }, 100) }); if (this.players[i].isAlive()) { var channel = this.players[i].getPrivateChannel(); if (pin === "period") { this.sendPeriodPin(channel, message); } else if (pin === "permanent") { this.sendPin(channel, message); } else { this.players[i].getPrivateChannel().send(message); }; }; }; } cycle () { for (var i = expansions.length - 1; i >= 0; i--) { var cycle = expansions[i].scripts.cycle; if (!cycle) { continue; }; cycle(this); }; if (this.period % 2 === 0) { this.day(); } else { this.night(); }; } day () { // Executed at the start of daytime this.createTrialVote(); if (this.config["game"]["mafia"]["night-only"]) { executable.misc.lockMafiaChat(this); } else { executable.misc.openMafiaChat(this); executable.misc.postMafiaPeriodicMessage(this); }; executable.misc.openMainChats(this); } night () { // Executed at the start of nighttime // Lynch players executable.misc.openMafiaChat(this); executable.misc.postMafiaPeriodicMessage(this); if (!this.config["game"]["town"]["night-chat"]) { executable.misc.lockMainChats(this); } else { executable.misc.openMainChats(this); }; } createPeriodPin (message) { var log = this.getPeriodLog(); var result = executable.misc.pinMessage(message); if (result) { var jx = { "message": message.id, "channel": message.channel.id, "pin_time": new Date() }; log.pins.push(jx); }; return result; } createPin (message) { var result = executable.misc.pinMessage(message); return result; } async sendPeriodPin (channel, message) { var out = await channel.send(message); this.createPeriodPin(out); } async sendPin (channel, message) { var out = await channel.send(message); this.createPin(out); } checkLynches () { // Find players who will be lynched var lynchable = new Array(); for (var i = 0; i < this.players.length; i++) { if (!this.players[i].isAlive()) { continue; }; var votes = this.players[i].countVotes(); var required = this.getVotesRequired() - this.players[i].getVoteOffset(); var top_voted_lynch = this.config["game"]["lynch"]["top-voted-lynch"] && votes >= this.config["game"]["lynch"]["top-voted-lynch-minimum-votes"]; if (votes >= required || top_voted_lynch) { // Execute the player //var success = this.lynch(this.players[i]); lynchable.push({player: this.players[i], score: votes - required, votes: votes}); }; }; var lynches_available = this.getLynchesAvailable(); lynchable = auxils.cryptographicShuffle(lynchable); lynchable.sort(function (a, b) { return b.score - a.score; }); var lynched = new Array(); var no_lynch_votes = this.getNoLynchVoteCount(); var top_voted_lynch = this.config["game"]["lynch"]["top-voted-lynch"]; // Check no-lynch if (no_lynch_votes < this.getNoLynchVotesRequired() || top_voted_lynch) { while (lynchable.length > 0 && lynches_available > lynched.length) { var score = lynchable[0].score; var votes = lynchable[0].votes; var target = lynchable[0].player; // Checks popularity of no lynch votes if (votes <= no_lynch_votes) { break; }; // Encased in loop in event of > 2 lynches available and second-ups are tied if (lynchable.filter(x => x.score === score).length > (lynches_available - lynched.length) && !this.config["game"]["lynch"]["tied-random"]) { // Stop further lynch break; }; var success = this.lynch(target); if (success) { lynched.push(target); }; lynchable.splice(0, 1); }; }; // Successful lynches go into lynched // Broadcast the lynches in the main channel executable.misc.broadcastMainLynch(this, lynched); } lynch (role) { var success = executable.misc.lynch(this, role); // Add lynch summary if (success) { this.silentKill(role, "__lynched__"); }; return success; } kill (role, reason, secondary_reason, broadcast_position_offset=0, circumstances=new Object()) { // Secondary reason is what the player sees // Can be used to mask death but show true // reason of death to the player killed this.silentKill(...arguments); if (this.getPeriodLog() && this.getPeriodLog().trial_vote) { this.clearAllVotesFromAndTo(role.identifier); this.__reloadTrialVoteMessage(); this.__checkLynchHammer(); }; } silentKill (role, reason, secondary_reason, broadcast_position_offset=0, circumstances=new Object()) { // Work in progress, should remove emote /* if (this.getPeriodLog() && this.getPeriodLog().trial_vote) { executable.misc.removePlayerEmote(this, role.identifier); }; */ // Secondary reason is what the player sees // Can be used to mask death but show true // reason of death to the player killed this.execute("killed", {target: role.identifier, circumstances: circumstances}); executable.misc.kill(this, role); this.primeDeathMessages(role, reason, secondary_reason, broadcast_position_offset); } modkill (id) { var role = this.getPlayerById(id); if (role === null) { return false; }; role.getPrivateChannel().send(":exclamation: You have been removed from the game by a moderator."); executable.admin.modkill(this, role); return true; } primeDeathMessages (role, reason, secondary, broadcast_position_offset=0) { this.addDeathBroadcast(role, reason, broadcast_position_offset); if (secondary) { this.addDeathMessage(role, secondary); } else { this.addDeathMessage(role, reason); }; } enterDeathMessages (offset=0) { var log = this.getPeriodLog(offset); // {role, reason} var registers = Array.from(log.death_messages); var messages = new Object(); for (var i = 0; i < registers.length; i++) { var identifier = registers[i].role; if (!messages[identifier]) { messages[identifier] = new Array(); }; messages[identifier].push(registers[i].reason); }; var keys = Object.keys(messages); for (var i = 0; i < keys.length; i++) { var identifier = keys[i]; var role = this.getPlayerByIdentifier(identifier); var reason = auxils.pettyFormat(messages[keys[i]]); var message = executable.misc.getDeathMessage(this, role, reason); this.addMessage(role, message); }; } enterDeathBroadcasts (offset=0) { // Enters in from log.death_broadcasts var log = this.getPeriodLog(offset); var registers = Array.from(log.death_broadcasts); registers.sort((a, b) => a.position_offset - b.position_offset); var unique = new Array(); for (var i = 0; i < registers.length; i++) { if (!unique.includes(registers[i].role)) { unique.push(registers[i].role); }; }; var cause_of_death_config = this.config["game"]["cause-of-death"]; var exceptions = cause_of_death_config["exceptions"]; // Inverted var hide_day = (cause_of_death_config["hide-day"] && !this.isDay()); var hide_night = (cause_of_death_config["hide-night"] && this.isDay()); for (var i = 0; i < unique.length; i++) { var role = this.getPlayerByIdentifier(unique[i]); var reasons = new Array(); for (var j = 0; j < registers.length; j++) { if (registers[j].role === unique[i]) { var exempt = false; // TODO: fix for (var k = 0; k < exceptions.length; k++) { if (registers[j].reason.includes(exceptions[k])) { exempt = true; break; }; }; if (!(hide_day || hide_night) || exempt) { reasons.push(registers[j].reason); }; }; }; if (reasons.length < 1) { reasons.push("found dead"); }; var reason = auxils.pettyFormat(reasons); var message = executable.misc.getDeathBroadcast(this, role, reason); this.addBroadcastSummary(message, offset); }; this.uploadPublicRoleInformation(unique); } uploadPublicRoleInformation (role_identifiers, ignore_cleaned=true) { var display = new Array(); if (!this.previously_uploaded_role_info) { this.previously_uploaded_role_info = new Array(); }; for (var i = 0; i < role_identifiers.length; i++) { var player = this.getPlayerByIdentifier(role_identifiers[i]); var flavour = this.getGameFlavour(); var exception = this.previously_uploaded_role_info.includes(player.role_identifier) && flavour && flavour.info["do-not-repost-duplicate-cards"] === true; if (!player.misc.role_cleaned && !exception) { display.push(player); }; if (!this.previously_uploaded_role_info.includes(player.role_identifier)) { this.previously_uploaded_role_info.push(player.role_identifier); }; }; executable.roles.uploadPublicRoleInformation(this, display); } addDeathBroadcast (role, reason, position_offset=0) { var log = this.getPeriodLog(); log.death_broadcasts.push({role: role.identifier, reason: reason, position_offset: position_offset}); } addBroadcastSummary (message, offset=0) { var log = this.getPeriodLog(offset); log.summary.push({message: message, time: new Date()}); } addDeathMessage (role, reason) { var log = this.getPeriodLog(); log.death_messages.push({role: role.identifier, reason: reason}); } addMessage (role, message) { var log = this.getPeriodLog(); log.messages.push({message: message, recipient: role.identifier, time: new Date()}); } addMessages (roles, message) { for (var i = 0; i < roles.length; i++) { this.addMessage(roles[i], message); }; } getBroadcast (offset=0, enter=false) { if (enter) { this.enterDeathBroadcasts(offset); }; // Get the summary broadcast var log = this.getPeriodLog(offset); var broadcasts = log.summary; if (broadcasts.length < 1) { return undefined; } else { var concat = new Array(); for (var i = 0; i < broadcasts.length; i++) { concat.push(broadcasts[i].message); }; return concat.join("\n\n"); }; } async sendMessages (offset=0) { // Actually sends messages var log = this.getPeriodLog(offset); var messages = log.messages; for (var i = 0; i < messages.length; i++) { var message = messages[i].message; executable.misc.sendIndivMessage(this, messages[i].recipient, message); await new Promise(function(resolve, reject) { setTimeout(function () { resolve(); }, 80); }); }; } clearPeriodPins (offset=0) { // Clears the pinned messages in the period log var log = this.getPeriodLog(); var pins = log.pins; for (var i = 0; i < pins.length; i++) { var pin = pins[i]; executable.misc.unpinMessage(this, pin.channel, pin.message); }; } getGuildMember (id) { var guild = this.client.guilds.get(this.config["server-id"]); var member = guild.members.get(id); return member; } async __start () { this.state = "playing"; for (var i = expansions.length - 1; i >= 0; i--) { var game_start = expansions[i].scripts.game_start; if (!game_start) { continue; }; await game_start(this); }; var cache = new Array(); for (var i = 0; i < this.players.length; i++) { cache.push(this.players[i].start()); }; executable.misc.postGameStart(this); await Promise.all(cache); if (!this.isDay() && !this.config["game"]["town"]["night-chat"]) { executable.misc.lockMainChats(this); } else { executable.misc.openMainChats(this); }; for (var i = expansions.length - 1; i >= 0; i--) { var game_secondary_start = expansions[i].scripts.game_secondary_start; if (!game_secondary_start) { continue; }; await game_secondary_start(this); }; } __routines () { // Check day night cycles, also used on referesh // Should not put post functions in here, // only administrative junk var trials = Math.max(this.config["game"]["minimum-trials"], Math.ceil(this.config["game"]["lynch-ratio-floored"] * this.getAlive())); // Clear fast forward votes this.fast_forward_votes = new Array(); for (var i = 0; i < this.trial_vote_operations.length; i++) { var operation = this.trial_vote_operations[i].operation; trials = auxils.operations[operation](trials, this.trial_vote_operations[i].amount); }; // Clear TV operations this.trial_vote_operations = new Array(); this.period_log[this.period.toString()] = { "trials": trials, "summary": new Array(), "death_broadcasts": new Array(), "death_messages": new Array(), "messages": new Array(), "trial_vote": null, "no_lynch_vote": new Array(), "period": this.period, "pins": new Array(), "special_vote_types": new Array() }; } addTrialVoteOperation (operation, amount) { var allowed = ["addition", "subtraction", "multiplication", "division", "modulo", "max", "min"]; if (!allowed.includes(operation)) { var err = new Error("Operation " + operation + " is not allowed!"); throw err; }; this.trial_vote_operations.push({operation: operation, amount: amount}) } async __playerRoutines () { for (var i = 0; i < this.players.length; i++) { await new Promise(function(resolve, reject) { setTimeout(function () { resolve(); }, 100); }); this.players[i].__routines(); }; } getLynchesAvailable (offset=0) { var log = this.getPeriodLog(offset); return log.trials; } getPeriodLog (offset=0) { return this.period_log[(this.period + offset).toString()]; } getVotesRequired () { var alive = this.getAlive(); // Floored of alive //return 1; return Math.max(this.config["game"]["minimum-lynch-votes"], Math.floor(alive / 2) + 1); } getNoLynchVotesRequired () { var alive = this.getAlive(); // Ceiled of alive return Math.max(this.config["game"]["minimum-nolynch-votes"], Math.ceil(alive / 2)); } getDiscordUser (alphabet) { var id = this.getPlayerByAlphabet(alphabet); return this.client.users.get(id); } setPresence (presence) { executable.misc.updatePresence(this, presence); } getFormattedDay (offset=0) { var period = this.period + offset; var numeral = Math.ceil(0.5 * period); var flavour = this.getGameFlavour(); if (flavour && flavour.info["step-names"]) { var step_names = flavour.info["step-names"]; var step = this.getStep() + offset; var index = step % step_names.length; return step_names[index] + " " + numeral; }; if (period % 2 === 0) { return "Day " + numeral; } else { return "Night " + numeral; }; } save () { this.timer.save(...arguments); } tentativeSave () { this.timer.tentativeSave(...arguments); } async saveAsynchronously () { this.save(); } format (string) { return executable.misc.__formatter(string); } reinstantiate (timer, players) { this.timer = timer; this.players = players; if (this.game_config_override) { this.config.game = auxils.objectOverride(this.config.game, this.game_config_override); } else { this.game_config_override = new Object(); }; // Check role/attribute incompatibility var incompatible = new Array(); for (var i = 0; i < players.length; i++) { incompatible = incompatible.concat(players[i].verifyProperties()); }; if (this.flavour_identifier && !flavours[this.flavour_identifier]) { incompatible = incompatible.concat({type: "flavour", identifier: this.flavour_identifier}); }; if (incompatible.length > 0) { var errors = [{type: "role", items: auxils.getUniqueArray(incompatible.filter(x => x.type === "role").map(x => x.identifier))}, {type: "attribute", items: auxils.getUniqueArray(incompatible.filter(x => x.type === "attribute").map(x => x.identifier))}, {type: "flavour", items: auxils.getUniqueArray(incompatible.filter(x => x.type === "flavour").map(x => x.identifier))}]; for (var i = 0; i < errors.length; i++) { if (errors[i].items.length > 0) { logger.log(3, "\nError loading type " + errors[i].type + ":"); console.table(errors[i].items); }; }; logger.log(3, "\nStopped save reload due to role/attribute incompatibilities. Make sure expansions required for this save can be loaded (you can also check config/playing.json's \"expansions\" field). Restart the bot when ready. Key \"uninstantiate\" to delete saves.\n"); return false; }; for (var i = 0; i < players.length; i++) { players[i].reinstantiate(this); }; if (this.players_tracked !== players.length) { logger.log(4, "The players' save files have been removed/deleted!"); }; this.players_tracked = players.length; this.actions.reinstantiate(this); this.instantiateTrialVoteCollector(); return true; } addAction () { // Inherits return this.actions.add(...arguments); } execute () { this.actions.execute(...arguments); } getPlayerMatch (name) { // Check if name is alphabet var player = this.getPlayerByAlphabet(name); if (player === null) { var guild = this.client.guilds.get(this.config["server-id"]); var distances = new Array(); for (var i = 0; i < this.players.length; i++) { var member = guild.members.get(this.players[i].id); if (member === undefined) { distances.push(-1); continue; }; var nickname = member.displayName; var username = member.user.username; // Calculate Levenshtein Distance // Ratio'd var s_username = auxils.hybridisedStringComparison(name.toLowerCase(), username.toLowerCase()); var s_nickname = auxils.hybridisedStringComparison(name.toLowerCase(), nickname.toLowerCase()); var distance = Math.max(s_username, s_nickname); distances.push(distance); }; // Compare distances var best_match_index = distances.indexOf(Math.max(...distances)); var score = distances[best_match_index]; player = this.players[best_match_index]; } else { var score = Infinity; }; return {"score": score, "player": player}; } find (key, value) { for (var i = 0; i < this.players.length; i++) { if (typeof key === "function") { var condition = key(this.players[i]); if (condition) { return this.players[i]; }; } else { if (this.players[i][key] === value) { return this.players[i]; }; }; }; return undefined; } findAll (key, value) { var ret = new Array(); for (var i = 0; i < this.players.length; i++) { if (typeof key === "function") { var condition = key(this.players[i]); if (condition) { ret.push(this.players[i]); }; } else if (this.players[i][key] === value) { ret.push(this.players[i]); }; }; return ret; } exists (key, value) { return this.find(key, value) !== undefined; } checkWin () { executable.wins.checkWin(this); } endGame () { logger.log(2, "Game ended!"); executable.conclusion.endGame(this); this.getMainChannel().send(this.config["messages"]["game-over"]); this.clearTrialVoteReactions(); // End the game this.state = "ended"; this.timer.updatePresence(); } getGuild () { return this.client.guilds.get(this.config["server-id"]); } postWinLog() { if (this.win_log) { executable.misc.postWinLog(this, this.win_log.faction, this.win_log.caption); } else { logger.log(3, "The win log has not been primed!"); }; } primeWinLog (faction, caption) { this.win_log = {faction: faction, caption: caption}; } getRolesChannel () { return this.getGuild().channels.find(x => x.name === this.config["channels"]["roles"]); } getLogChannel () { return this.getGuild().channels.find(x => x.name === this.config["channels"]["log"]); } getMainChannel () { return this.getGuild().channels.find(x => x.name === this.config["channels"]["main"]); } getWhisperLogChannel () { return this.getGuild().channels.find(x => x.name === this.config["channels"]["whisper-log"]); } getPeriod () { return this.period; } getStep () { return this.steps; } isDay () { return this.getPeriod() % 2 === 0; } setWin (role) { executable.misc.postWinMessage(role); role.setWin(); } setWins (roles) { for (var i = 0; i < roles.length; i++) { this.setWin(roles[i]); }; } async createPrivateChannel () { return await executable.misc.createPrivateChannel(this, ...arguments); } postPrimeLog () { executable.misc.postPrimeMessage(this); } postDelayNotice () { executable.misc.postDelayNotice(this); } substitute (id1, id2, detailed_substitution) { return executable.admin.substitute(this, id1, id2, detailed_substitution); }; clearPreemptiveVotes () { for (var i = 0; i < this.players.length; i++) { this.players[i].clearPreemptiveVotes(); }; } loadPreemptiveVotes (clear_cache=true) { var lynches = this.getLynchesAvailable(); for (var i = 0; i < this.players.length; i++) { var player = this.players[i]; var votes = player.getPreemtiveVotes(); if (!player.isAlive()) { continue; }; var amount = Math.min(lynches - this.votesFrom(player.identifier).length, votes.length); var successes = new Array(); for (var j = 0; j < votes.length; j++) { // Check if player is votable var current = this.getPlayerByIdentifier(votes[i]); var already_voted = current.isVotedAgainstBy(player.identifier); var alive = current.isAlive(); if (alive && !already_voted) { this.toggleVote(player, current); successes.push(current); if (successes.length >= amount) { break; }; }; }; if (successes.length > 0) { executable.misc.sendPreemptMessage(player, successes);; }; }; if (clear_cache) { this.clearPreemptiveVotes(); }; } getGameFlavour () { var config = this.config; var flavour_identifier = this.flavour_identifier; if (!flavour_identifier) { // No flavour return null; }; var flavour = flavours[flavour_identifier]; if (!flavour) { logger.log(3, "Invalid flavour " + flavour_identifier + "! Defaulting to no flavour."); return null; }; return flavour; } addFastForwardVote (identifier) { if (this.votedFastForward(identifier)) { return null; }; this.fast_forward_votes.push(identifier); } removeFastForwardVote (identifier) { if (!this.votedFastForward(identifier)) { return null; }; this.fast_forward_votes = this.fast_forward_votes.filter(x => x !== identifier); } votedFastForward (identifier) { return this.fast_forward_votes.includes(identifier); } checkFastForward () { // Wrt to the configuration var alive_count = this.getAlive(); var minimum = Math.ceil(alive_count * this.config["game"]["fast-forwarding"]["ratio"]); var ff_votes = this.fast_forward_votes; // Confirm that all players are alive ff_votes = ff_votes.filter(x => this.getPlayerByIdentifier(x).isAlive()); var ratio = ff_votes.length/alive_count; var percentage = Math.round(ratio * 1000)/10; if (ff_votes.length >= minimum) { // Fast forward the game this.addBroadcastSummary("The game has been **fastforwarded** with __" + percentage + "%__ of alive players voting for such last cycle."); this.fastforward(); }; } checkRole (condition) { if (typeof condition === "function") { // Check return this.exists(condition); } else if (typeof condition === "string") { condition = condition.toLowerCase(); // Check separately var cond1 = this.exists(x => x.isAlive() && x.role_identifier === condition); var cond2 = this.exists(x => x.isAlive() && x.role.alignment === condition); var cond3 = this.exists(x => x.isAlive() && x.role.class === condition); var cond4 = false; if (condition.includes("-")) { condition = condition.split("-"); var cond4 = this.exists(x => x.isAlive() && x.role.alignment === condition[0] && x.role.class === condition[1]); }; return cond1 || cond2 || cond3 || cond4; } else { return null; }; } checkRoles (conditions) { var ret = false; for (var i = 0; i < conditions.length; i++) { ret = ret || this.checkRole(conditions[i]); }; return ret; } getNoLynchVoters () { var ret = new Array(); var no_lynch_vote = this.getPeriodLog()["no_lynch_vote"]; // {identifier, magnitude} for (var i = 0; i < no_lynch_vote.length; i++) { ret.push(no_lynch_vote[i].identifier); }; return ret; } getNoLynchVoteCount () { var count = new Number(); var no_lynch_vote = this.getPeriodLog()["no_lynch_vote"]; for (var i = 0; i < no_lynch_vote.length; i++) { count += no_lynch_vote[i].magnitude; }; return count; } getSpecialVoteCount (identifier) { var special_vote = this.getPeriodLog().special_vote_types.find(x => x.identifier === identifier); if (!special_vote) { return null; }; var count = new Number(); for (var i = 0; i < special_vote.voters.length; i++) { count += special_vote.voters[i].magnitude; }; return count; } addNoLynchVote (identifier, magnitude) { var no_lynch_vote = this.getPeriodLog()["no_lynch_vote"]; no_lynch_vote.push({identifier: identifier, magnitude: magnitude}); this.execute("vote", {target: "nl", voter: identifier}); } clearNoLynchVotesBy (identifier) { var no_lynch_vote = this.getPeriodLog()["no_lynch_vote"]; var cleared = false; for (var i = no_lynch_vote.length - 1; i >= 0; i--) { if (no_lynch_vote[i].identifier === identifier) { no_lynch_vote.splice(i, 1); cleared = true; }; }; if (cleared) { this.execute("unvote", {target: "nl", voter: identifier}); }; return cleared; } clearNoLynchVotes () { this.getPeriodLog()["no_lynch_vote"] = new Array(); } isVotingNoLynch (identifier) { return this.getNoLynchVoters().includes(identifier); } hammerActive () { var trials_available = this.getTrialsAvailable(); return this.config["game"]["lynch"]["allow-hammer"] && (trials_available < 2); } getTrialsAvailable () { var period_log = this.getPeriodLog(); return period_log ? period_log["trials"] : Math.max(this.config["game"]["minimum-trials"], Math.ceil(this.config["game"]["lynch-ratio-floored"] * this.getAlive())); } clearAllVotesOn (identifier) { var player = this.getPlayerByIdentifier(identifier); player.clearVotes(); } clearAllVotesFromAndTo (identifier) { // Stops votes to and from player this.clearNoLynchVotesBy(identifier); this.clearAllVotesBy(identifier); this.clearAllVotesOn(identifier); } addIntroMessage (channel_id, message) { this.intro_messages.push({channel_id: channel_id, message: message}); } async postIntroMessages () { for (var i = 0; i < this.intro_messages.length; i++) { var channel = this.getChannelById(this.intro_messages[i].channel_id); await channel.send(this.intro_messages[i].message); }; } setGameConfigOverride (game_config, update_immediately=true) { this.game_config_override = auxils.objectOverride(this.game_config_override, game_config); if (update_immediately) { this.config.game = auxils.objectOverride(this.config.game, this.game_config_override); }; } getAPI () { return lcn; } };