// Copyright 2018-2020 Campbell Crowley. All rights reserved. // Author: Campbell Crowley (dev@campbellcrowley.com) const {workerData, parentPort} = require('worker_threads'); const HungryGames = require('../HungryGames.js'); /** * @description Asyncronous worker that does the actual simulating. * @memberof HungryGames~Simulator * @inner */ class Worker { /** * @description Create and start simulating. * @param {{ * game: object, * messages: object.<string>, * events: HungryGames~EventContainer * }} sim Simulation data. * @param {number} [retries=2] Number of remaining times to retry in the event * of an error. */ constructor(sim, retries = 2) { sim.game.currentGame.prevDay = sim.game.currentGame.day; sim.game.currentGame.day = HungryGames.Day.from(sim.game.currentGame.nextDay); sim.game.currentGame.day.state = 1; if (!sim.game.customEventStore.serializable) { sim.game.customEventStore = new HungryGames.EventContainer(sim.game.customEventStore); } if (!sim.events.serializable) { const battle = sim.events.battles; sim.events = new HungryGames.EventContainer(sim.events); sim.events.battles = battle; } sim.messages = { _messages: sim.messages, /** * Get a random message of a given type. * * @public * @param {string} type The message type to get. * @returns {string} A random message of the given type. */ get(type) { const list = this._messages[type]; if (!list) return 'badtype'; const length = list.length; if (length == 0) return 'nomessage'; return list[Math.floor(Math.random() * length)]; }, }; // Wait for all custom events to be fetched. sim.game.customEventStore.waitForReady(() => { sim.events.waitForReady(() => { this._simulate(sim, retries); }); }); } /** * @description Run the simulation. * @private * @param {{game: objcet, messages: object.<string>}} sim Simulation data. * @param {number} retries Number of remaining times to retry in the event of * an error. */ _simulate(sim, retries) { const id = sim.game.id; let startingAlive = 0; let numAlive = 0; const userPool = sim.game.currentGame.includedUsers.filter((player) => { if (player.living) startingAlive++; if (player.state === 'zombie') player.state = 'normal'; player.simWeapons = Object.assign({}, player.weapons); let isV = false; const evt = sim.game.currentGame.day.events.find((el) => { const icon = el.icons.find((icon) => icon.id === player.id); if (icon) isV = icon.settings.victim; return icon; }); if (player.living) { if (!evt) { numAlive++; } else if (isV && evt.victim.outcome !== 'dies') { numAlive++; } else if (!isV && evt.attacker.outcome !== 'dies') { numAlive++; } } else if (evt) { if (isV && evt.victim.outcome === 'revived') { numAlive++; } else if (!isV && evt.attacker.outcome === 'revived') { numAlive++; } } return player.living && !evt; }); const deadPool = sim.game.currentGame.includedUsers.filter( (obj) => !userPool.find((el) => el.id === obj.id)); // Shuffle user order because games may have been rigged :thonk:. for (let i = 0; i < userPool.length; i++) { const index = Math.floor(Math.random() * (userPool.length - i)) + i; const tmp = userPool[i]; userPool[i] = userPool[index]; userPool[index] = tmp; } const teams = sim.game.currentGame.teams; // Shuffle team order because games may have been rigged :hyperthonk:. for (let i = 0; i < teams.length; i++) { const index = Math.floor(Math.random() * (teams.length - i)) + i; const tmp = teams[i]; teams[i] = teams[index]; teams[index] = tmp; } let userEventPool; let doArenaEvent = false; let arenaEvent; if (sim.game.currentGame.day.num === 0) { userEventPool = sim.events.getArray('bloodbath') .concat(sim.game.customEventStore.getArray('bloodbath')); userEventPool = userEventPool.filter( (el) => !sim.game.disabledEventIds.bloodbath.includes(el.id)); if (userEventPool.length == 0) { this.cb({ reply: 'All bloodbath events have been disabled! Please enable ' + 'events so that something can happen in the games!', endGame: true, reason: 'No Bloodbath Events', }); return; } } else { doArenaEvent = startingAlive > 2 && sim.game.options.arenaEvents && Math.random() < sim.game.options.probabilityOfArenaEvent; if (doArenaEvent) { const arenaEventPool = sim.events.getArray('arena') .concat(sim.game.customEventStore.getArray('arena')) .filter( (evt) => !sim.game.disabledEventIds.arena.includes(evt.id)); while (arenaEventPool.length > 0) { let total = arenaEventPool.length; if (sim.game.options.customEventWeight != 1) { arenaEventPool.forEach((el) => { if (el.custom) { total += sim.game.options.customEventWeight - 1; } }); } const pick = Math.random() * total; const index = arenaEventPool.findIndex((el) => { total -= el.custom ? sim.game.options.customEventWeight : 1; if (total < pick) return true; return false; }); arenaEvent = arenaEventPool[index]; userEventPool = arenaEvent.outcomes.filter( (el) => !sim.game.disabledEventIds.arena.includes( `${arenaEvent.id}/${el.id}`)); if (userEventPool.length == 0) { arenaEventPool.splice(index, 1); } else { sim.game.currentGame.day.events.push( HungryGames.Event.finalizeSimple( sim.messages.get('eventStart'), sim.game)); sim.game.currentGame.day.events.push( HungryGames.Event.finalizeSimple( `**___${arenaEvent.message}___**`, sim.game)); break; } } if (arenaEventPool.length == 0) doArenaEvent = false; } if (!doArenaEvent) { userEventPool = sim.events.getArray('player').concat( sim.game.customEventStore.getArray('player')); userEventPool = userEventPool.filter( (el) => !sim.game.disabledEventIds.player.includes(el.id)); if (userEventPool.length == 0) { this.cb({ reply: 'All player events have been disabled! Please enable events' + ' so that something can happen in the games!', endGame: true, reason: 'No Player Events', }); return; } } } const weapons = Object.assign( Object.assign({}, sim.events.get('weapon')), sim.game.customEventStore.get('weapon')); const disabledList = sim.game.disabledEventIds.weapon; const weaponList = Object.values(weapons); weaponList.forEach((evt) => { evt.outcomes = evt.outcomes.filter( (el) => !disabledList.includes(`${evt.id}/${el.id}`)); if (evt.outcomes.length === 0 || disabledList.includes(evt.id)) { delete weapons[evt.id]; } }); const battles = sim.events.battles; /* sim.events.getArray('battles').concat( sim.game.customEventStore.get('battles')) */ const probOpts = sim.game.currentGame.day.num === 0 ? sim.game.options.bloodbathOutcomeProbs : (doArenaEvent ? (arenaEvent.outcomeProbs || sim.game.options.arenaOutcomeProbs) : sim.game.options.playerOutcomeProbs); const nameFormat = sim.game.options.useNicknames ? 'nickname' : 'username'; while (userPool.length > 0) { let eventTry; let affectedUsers; let userWithWeapon = null; if (!doArenaEvent) { const usersWithWeapon = []; for (let i = 0; i < userPool.length; i++) { if (userPool[i].weapons && Object.keys(userPool[i].weapons).length > 0) { usersWithWeapon.push(userPool[i]); } } if (usersWithWeapon.length > 0) { userWithWeapon = usersWithWeapon[Math.floor( Math.random() * usersWithWeapon.length)]; } } let useWeapon = userWithWeapon && Math.random() < sim.game.options.probabilityOfUseWeapon; if (useWeapon) { const userWeapons = Object.keys(userWithWeapon.weapons); const chosenWeapon = userWeapons[Math.floor(Math.random() * userWeapons.length)]; if (!weapons[chosenWeapon]) { useWeapon = false; // console.log('No event pool with weapon', chosenWeapon); } else { eventTry = HungryGames.Simulator._pickEvent( userPool, weapons[chosenWeapon].outcomes, sim.game.options, numAlive, sim.game.currentGame.includedUsers.length, teams, probOpts, userWithWeapon, chosenWeapon); if (!eventTry) { useWeapon = false; /* self.error( 'No event with weapon "' + chosenWeapon + '" for available players ' + id); */ } else { affectedUsers = HungryGames.Simulator._pickAffectedPlayers( eventTry, sim.game.options, userPool, deadPool, teams, userWithWeapon, chosenWeapon); const count = HungryGames.Simulator._parseConsumeCount( eventTry.consumes, eventTry.victim.count, eventTry.attacker.count); eventTry.consumer = userWithWeapon.id; eventTry.consumes = [{id: chosenWeapon, count: count}]; const firstAttacker = affectedUsers[eventTry.victim.count] && affectedUsers[eventTry.victim.count].id == userWithWeapon.id; eventTry.subMessage = HungryGames.Simulator.formatWeaponEvent( eventTry, userWithWeapon, HungryGames.Grammar.formatMultiNames( [userWithWeapon], nameFormat), firstAttacker, chosenWeapon, weapons); if (userWithWeapon.weapons[chosenWeapon]) { userWithWeapon.weapons[chosenWeapon] -= count; if (userWithWeapon.weapons[chosenWeapon] <= 0) { delete userWithWeapon.weapons[chosenWeapon]; } } } } } const doBattle = ((!useWeapon && !doArenaEvent) || !eventTry) && userPool.length > 1 && (Math.random() < sim.game.options.probabilityOfBattle || (startingAlive == 2 && numAlive == 2)) && !HungryGames.Simulator._validateEventRequirements( 1, 1, userPool, numAlive, teams, sim.game.options, true, false); if (doBattle) { let numVictim; let numAttacker; do { numAttacker = HungryGames.Simulator.weightedUserRand(); numVictim = HungryGames.Simulator.weightedUserRand(); } while (HungryGames.Simulator._validateEventRequirements( numVictim, numAttacker, userPool, numAlive, teams, sim.game.options, true, false)); affectedUsers = HungryGames.Simulator._pickAffectedPlayers( new HungryGames.NormalEvent( '', numVictim, numAttacker, 'dies', 'nothing'), sim.game.options, userPool, deadPool, teams, null); eventTry = HungryGames.Battle.finalize( affectedUsers, numVictim, numAttacker, sim.game.options.mentionAll, sim.game, battles); } else if (!useWeapon || !eventTry) { eventTry = HungryGames.Simulator._pickEvent( userPool, userEventPool, sim.game.options, numAlive, sim.game.currentGame.includedUsers.length, teams, probOpts); if (!eventTry) { console.error( 'No event for ' + userPool.length + ' from ' + userEventPool.length + ' events. No weapon, Arena Event: ' + (doArenaEvent ? arenaEvent.message : 'No') + ', Day: ' + sim.game.currentGame.day.num + ' Guild: ' + id + ' Retrying: ' + retries > 0); sim.game.currentGame.day.state = 0; if (retries > 0) { // new Worker(sim, retries - 1); this._simulate(sim, retries - 1); return; } this.cb({ reply: 'Oops! I wasn\'t able to find a valid event for the ' + 'remaining players.\nThis is usually because too many ' + 'events are disabled.\nIf you think this is a bug, ' + 'please report this to my Discord server.', reply2: 'Try again with `{prefix}next`.\n(Failed to find valid ' + 'event for \'' + (doArenaEvent ? arenaEvent.message : 'player events') + '\' suitable for ' + userPool.length + ' remaining players)', reason: 'Bad Configuration', game: sim.game, }); return; } affectedUsers = HungryGames.Simulator._pickAffectedPlayers( eventTry, sim.game.options, userPool, deadPool, teams, null); } if (!doBattle) { eventTry = eventTry.finalize(sim.game, affectedUsers); } if (eventTry.victim.outcome === 'dies') { [].push.apply(deadPool, affectedUsers.slice(0, eventTry.victim.count)); } if (eventTry.attacker.outcome === 'dies') { [].push.apply(deadPool, affectedUsers.slice(eventTry.victim.count)); } sim.game.currentGame.day.events.push(eventTry); const numKilled = (eventTry.attacker.outcome === 'dies' ? eventTry.attacker.count : 0) + (eventTry.victim.outcome === 'dies' ? eventTry.victim.count : 0); numAlive -= numKilled; if (numKilled > 4) { sim.game.currentGame.day.events.push( HungryGames.Event.finalizeSimple( sim.messages.get('slaughter'), sim.game)); } const numRevived = (eventTry.attacker.outcome === 'revived' ? eventTry.attacker.count : 0) + (eventTry.victim.outcome === 'revived' ? eventTry.victim.count : 0); numAlive += numRevived; } if (doArenaEvent) { sim.game.currentGame.day.events.push( HungryGames.Event.finalizeSimple( sim.messages.get('eventEnd'), sim.game)); } sim.game.currentGame.includedUsers.forEach((player) => { if (player.simWeapons) { player.weapons = player.simWeapons; delete player.simWeapons; } }); // Apply outcomes. sim.game.currentGame.day.events.forEach((evt) => { const affected = []; evt.icons.forEach((icon) => { if (!icon.id) return; const player = sim.game.currentGame.includedUsers.find((p) => p.id === icon.id); if (!player) return; affected.push(player); const isV = icon.settings.victim; const isA = icon.settings.attacker; const group = (isA && evt.attacker) || (isV && evt.victim); const other = (isA && evt.victim) || (isV && evt.attacker); HungryGames.Simulator._applyOutcome( sim.game, player, group.killer ? other.count : 0, null, group.outcome); if (group.weapons && group.weapons.length > 0) { for (const w of group.weapons) { if (player.weapons[w.id]) { player.weapons[w.id] = player.weapons[w.id] * 1 + w.count * 1; if (player.weapons[w.id] <= 0) delete player.weapons[w.id]; } else if (w.count > 0) { player.weapons[w.id] = w.count * 1; } } } }); if (evt.consumes && evt.consumes.length > 0) { const consumer = sim.game.currentGame.includedUsers.find( (p) => p.id === evt.consumer); if (consumer) { for (const consumed of evt.consumes) { if (consumer.weapons[consumed.id]) { const count = consumed.count; consumer.weapons[consumed.id] -= count; if (consumer.weapons[consumed.id] <= 0) { delete consumer.weapons[consumed.id]; } } } } } evt.subMessage += HungryGames.Simulator.formatWeaponCounts( evt, affected, weapons, nameFormat); }); // Bleeding sim.game.currentGame.includedUsers.forEach((player) => { if (player.state == 'wounded') { player.bleeding++; } else { player.bleeding = 0; } }); // Finish bleeding const usersBleeding = []; const usersRecovered = []; sim.game.currentGame.includedUsers.forEach((obj) => { if (obj.bleeding > 0 && obj.bleeding > sim.game.options.bleedDays && obj.living) { if (Math.random() < sim.game.options.probabilityOfBleedToDeath && (sim.game.options.allowNoVictors || numAlive > 1)) { usersBleeding.push(obj); } else { usersRecovered.push(obj); } } }); if (usersRecovered.length > 0) { sim.game.currentGame.day.events.push( HungryGames.NormalEvent.finalize( sim.messages.get('patchWounds'), usersRecovered, usersRecovered.length, 0, 'thrives', 'nothing', sim.game)); usersRecovered.forEach((player) => { HungryGames.Simulator._applyOutcome( sim.game, player, 0, null, 'thrives'); }); } if (usersBleeding.length > 0) { numAlive -= usersBleeding.length; sim.game.currentGame.day.events.push( HungryGames.NormalEvent.finalize( sim.messages.get('bleedOut'), usersBleeding, usersBleeding.length, 0, 'dies', 'nothing', sim.game)); usersBleeding.forEach((player) => { HungryGames.Simulator._applyOutcome(sim.game, player, 0, null, 'dies'); }); } // Additional messages const deathPercentage = 1 - (numAlive / startingAlive); if (deathPercentage > HungryGames.Simulator._lotsOfDeathRate) { sim.game.currentGame.day.events.splice( 0, 0, HungryGames.Event.finalizeSimple( sim.messages.get('lotsOfDeath'), sim.game)); } else if (deathPercentage === 0) { sim.game.currentGame.day.events.push( HungryGames.Event.finalizeSimple( sim.messages.get('noDeath'), sim.game)); } else if (deathPercentage < HungryGames.Simulator._littleDeathRate) { sim.game.currentGame.day.events.splice( 0, 0, HungryGames.Event.finalizeSimple( sim.messages.get('littleDeath'), sim.game)); } sim.game.currentGame.day.state = 2; sim.game.currentGame.nextDay = HungryGames.Day.from({num: sim.game.currentGame.day.num + 1}); this.cb({game: sim.game}); } /** * @description Pass a message back to the parent. * @public * @param {*} [data] Data to send to the parent. */ cb(data) { parentPort.postMessage(data); } } module.exports = new Worker(workerData);