Source: hg/simulator/worker.js

// 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);