<commit_before>/**
 * Public API of Battle Prediction module.
 * Must be loaded first, since it defines the module's structure.
 *
 * DO NOT modify single file module of `src/library/module/BattlePrediction.js` directly,
 * To generate the single file module, run `grunt concat:battlePredictionDev`.
 */
(function () {
  const BP = {
    fleets: {},
    battle: {
      // Phase parsers - convert phase JSON to Attacks
      phases: {
        kouku: {}, // aerial phase (航空)
        hougeki: {}, // shelling (砲撃)
        raigeki: {}, // torpedoes (雷撃)
        support: {}, // support expedition
        yasen: {}, // night battle (夜戦)
      },
      // Specify phase ordering for each battle type
      engagement: {},
    },
    // Rank prediction
    rank: {
      airRaid: {},
      battle: {},
    },
    // MVP prediction
    mvp: {},
  };

  // --------------------------------------------------------------------------
  // PUBLIC API
  // --------------------------------------------------------------------------

  BP.analyzeBattle = (battleData, playerDamecons, battleType) => {
    const { fleets, battle, formatResult } = KC3BattlePrediction;

    try {
      const initialFleets = fleets.getInitialState(battleData, playerDamecons);
      const resultFleets = battle.simulateBattle(battleData, initialFleets, battleType);

      return formatResult(initialFleets, resultFleets);
    } catch (error) {
      // Pass context explicitly, so it is recorded
      KC3Log.error(error, error.data, { battleType, battleData, playerDamecons });
      throw error;
    }
  };

  BP.predictRank = (apiName, battleData, battleResult) => {
    const { parseStartJson, normalizeFleets, getRankPredictor } = KC3BattlePrediction.rank;

    return getRankPredictor(apiName).predict(
      normalizeFleets(parseStartJson(battleData), battleData),
      normalizeFleets(battleResult, battleData)
    );
  };

  BP.predictMvp = (dayResult, nightResult) => {
    const { combineResults, getHighestDamage } = KC3BattlePrediction.mvp;

    const { playerMain, playerEscort } = combineResults(dayResult, nightResult);
    return {
      playerMain: getHighestDamage(playerMain),
      playerEscort: getHighestDamage(playerEscort),
    };
  };

  // --------------------------------------------------------------------------
  // ENUMS
  // --------------------------------------------------------------------------

  // PUBLIC ENUMS
  // -------------

  BP.Player = Object.freeze({ SINGLE: 'single', STF: 'stf', CTF: 'ctf', TCF: 'ctf' });

  BP.Enemy = Object.freeze({ SINGLE: 'single', COMBINED: 'combined' });

  BP.Time = Object.freeze({ DAY: 'day', NIGHT: 'night', NIGHT_TO_DAY: 'night-to-day' });

  // INTERNAL ENUMS
  // ---------------

  // Player or enemy ship
  BP.Side = Object.freeze({ PLAYER: 'player', ENEMY: 'enemy' });

  // Ship in main or escort fleet
  BP.Role = Object.freeze({ MAIN_FLEET: 'main', ESCORT_FLEET: 'escort' });

  // Damecon API codes
  BP.Damecon = Object.freeze({ NONE: 0, TEAM: 1, GODDESS: 2 });

  // --------------------------------------------------------------------------
  // INTERNAL METHODS
  // --------------------------------------------------------------------------

  BP.EMPTY_SLOT = -1;

  BP.extendError = (error, data) =>
    (error.data ? Object.assign(error.data, data) : Object.assign(error, { data }));

  BP.normalizeArrayIndexing = array => (array[0] === -1 ? array.slice(1) : array);

  BP.bind = (target, ...args) => target.bind(null, ...args);

  BP.validateEnum = (enumObj, value) => {
    // Ideally this should use Object.values(), but it was only introduced in Chrome 54
    return Object.keys(enumObj).some(key => enumObj[key] === value);
  };

  /* ---------------------[ ZIP WITH ]--------------------- */

  const parseArgs = (args) => {
    const iteratee = args.slice(-1)[0];

    return typeof iteratee === 'function'
      ? { arrays: args.slice(0, -1), iteratee }
      : { arrays: args, iteratee: (...elements) => elements };
  };
  const getElements = (arrays, index) => arrays.map(array => array[index]);
  const getLongestArray = arrays =>
    arrays.reduce((result, array) => (array.length > result ? array.length : result), 0);
  // See: http://devdocs.io/lodash~4/index#zipWith
  BP.zipWith = (...args) => {
    const { arrays, iteratee } = parseArgs(args);

    const length = getLongestArray(arrays);
    const result = [];
    for (let i = 0; i < length; i += 1) {
      const elements = getElements(arrays, i);
      result.push(iteratee(...elements));
    }

    return result;
  };

  window.KC3BattlePrediction = BP;
}());

(function () {
  const battle = {};
  /*--------------------------------------------------------*/
  /* ----------------------[ PUBLIC ]---------------------- */
  /*--------------------------------------------------------*/

  battle.simulateBattle = (battleData, initalFleets, battleType) => {
    const { battle: { parseBattle }, fleets: { simulateAttack } } = KC3BattlePrediction;

    return parseBattle(battleType, battleData).reduce(simulateAttack, initalFleets);
  };

  /*--------------------------------------------------------*/
  /* ---------------------[ INTERNAL ]--------------------- */
  /*--------------------------------------------------------*/

  /* -----------------[ PARSE BATTLE DATA ]---------------- */

  battle.parseBattle = (battleType, battleData) => {
    const { accumulateAttacks, parsePhases } = KC3BattlePrediction.battle;

    return accumulateAttacks(parsePhases(battleType, battleData));
  };

  battle.parsePhases = (battleType, battleData) => {
    const { getBattlePhases } = KC3BattlePrediction.battle.engagement;

    return getBattlePhases(battleType).map(parsePhase => parsePhase(battleData));
  };

  battle.accumulateAttacks = phases =>
    phases.reduce((attacks, phase) => attacks.concat(phase), []);

  /*--------------------------------------------------------*/
  /* ---------------------[ EXPORTS ]---------------------- */
  /*--------------------------------------------------------*/

  Object.assign(window.KC3BattlePrediction.battle, battle);
}());

(function () {
  /*--------------------------------------------------------*/
  /* --------------------[ PUBLIC API ]-------------------- */
  /*--------------------------------------------------------*/

  // Create a Fleets object with the state of the player and enemy fleets at battle start
  const getInitialState = ({ api_f_nowhps, api_f_maxhps, api_e_nowhps, api_e_maxhps,
      api_f_nowhps_combined, api_f_maxhps_combined, api_e_nowhps_combined, api_e_maxhps_combined },
      playerDamecons) => {
    const { Role } = KC3BattlePrediction;
    const { getFleetShips, addDamecons, createFleets } = KC3BattlePrediction.fleets;

    const mainFleets = getFleetShips(api_f_nowhps, api_f_maxhps, api_e_nowhps, api_e_maxhps);
    const escortFleets = getFleetShips(api_f_nowhps_combined, api_f_maxhps_combined, api_e_nowhps_combined, api_e_maxhps_combined);

    return createFleets(
      addDamecons(Role.MAIN_FLEET, playerDamecons, mainFleets.player),
      mainFleets.enemy,
      addDamecons(Role.ESCORT_FLEET, playerDamecons, escortFleets.player),
      escortFleets.enemy
    );
  };

  /*--------------------------------------------------------*/
  /* --------------------[ INTERNALS ]--------------------- */
  /*--------------------------------------------------------*/

  /* ----------------------[ SHIPS ]----------------------- */

  const getFleetShips = (nowhpsPlayer, maxhpsPlayer, nowhpsEnemy, maxhpsEnemy) => {
    const { normalizeHps, convertToShips } = KC3BattlePrediction.fleets;

    // short-circuit if neither side has a fleet
    if (!nowhpsPlayer && !maxhpsPlayer && !nowhpsEnemy && !maxhpsEnemy) { return { player: [], enemy: [] }; }

    return {
      player: convertToShips(normalizeHps(nowhpsPlayer), normalizeHps(maxhpsPlayer)),
      enemy: convertToShips(normalizeHps(nowhpsEnemy), normalizeHps(maxhpsEnemy)),
    };
  };

  const normalizeHps = (hps = []) => {
    const { EMPTY_SLOT } = KC3BattlePrediction;

    if (hps.length < 6) {
      const emptySlotCount = 6 - (hps.length % 6);
      return hps.concat(new Array(emptySlotCount).fill(EMPTY_SLOT));
    }
    return hps;
  };

  const convertToShips = (nowHps, maxHps) => {
    const { createShip } = KC3BattlePrediction.fleets;

    if (nowHps.length !== maxHps.length) {
      throw new Error(`Length of nowhps (${nowHps.length}) and maxhps (${maxHps.length}) do not match`);
    }

    return nowHps.map((currentHp, index) => createShip(currentHp, maxHps[index]));
  };

  const isFleetNotEmpty = ships => ships.some(ship => ship !== KC3BattlePrediction.EMPTY_SLOT);
  const splitSides = (ships) => {
    if (ships.length !== 6 && ships.length !== 12) {
      throw new Error(`Expected 6 or 12 ships, but got ${ships.length}`);
    }

    const player = ships.slice(0, 6);
    const enemy = ships.slice(6, 12);

    return {
      player: isFleetNotEmpty(player) ? player : [],
      enemy,
    };
  };

  /* ---------------------[ DAMECONS ]--------------------- */

  const addDamecons = (role, damecons, ships) => {
    const { getDamecons, installDamecon } = KC3BattlePrediction.fleets;

    const dameconCodes = getDamecons(role, damecons);

    return ships.map((ship, index) => installDamecon(dameconCodes[index], ship));
  };

  const getDamecons = (role, damecons) => {
    const { Role } = KC3BattlePrediction;

    switch (role) {
      case Role.MAIN_FLEET:
        return damecons.slice(0, 6);
      case Role.ESCORT_FLEET:
        return damecons.slice(6, 12);
      default:
        throw new Error(`Bad role: ${role}`);
    }
  };

  /*--------------------------------------------------------*/
  /* ---------------------[ EXPORTS ]---------------------- */
  /*--------------------------------------------------------*/

  Object.assign(window.KC3BattlePrediction.fleets, {
    // Public
    getInitialState,
    // Internal
    getFleetShips,
    normalizeHps,
    convertToShips,
    splitSides,

    addDamecons,
    getDamecons,
  });
}());

(function () {
  /*--------------------------------------------------------*/
  /* --------------------[ PUBLIC API ]-------------------- */
  /*--------------------------------------------------------*/

  const combineResults = (dayFleets, nightFleets) => {
    const { combineFleetResults } = KC3BattlePrediction.mvp;

    if (!nightFleets) { return dayFleets; }
    if (!dayFleets) { return nightFleets; }
    return {
      playerMain: combineFleetResults(dayFleets.playerMain, nightFleets.playerMain),
      playerEscort: combineFleetResults(dayFleets.playerEscort, nightFleets.playerEscort),
    };
  };

  const getHighestDamage = (fleet) => {
    const { index } = fleet.reduce((result, { damageDealt }, i) => {
      return damageDealt > result.damageDealt
        ? { damageDealt, index: i }
        : result;
    }, { damageDealt: -1 });
    // MVP index 1-based
    return index !== undefined ? index + 1 : undefined;
  };

  /*--------------------------------------------------------*/
  /* --------------------[ INTERNALS ]--------------------- */
  /*--------------------------------------------------------*/

  const combineFleetResults = (day, night) => {
    const { zipWith } = KC3BattlePrediction;

    return zipWith(day, night, (dayResult, nightResult) => ({
      damageDealt: dayResult.damageDealt + nightResult.damageDealt,
    }));
  };

  /*--------------------------------------------------------*/
  /* ---------------------[ EXPORTS ]---------------------- */
  /*--------------------------------------------------------*/

  Object.assign(KC3BattlePrediction.mvp, {
    // Public
    combineResults,
    getHighestDamage,
    // Internal
    combineFleetResults,
  });
}());

(function () {
  /*--------------------------------------------------------*/
  /* --------------------[ PUBLIC API ]-------------------- */
  /*--------------------------------------------------------*/

  const formatResult = (initialFleets, resultFleets) => {
    const { isPlayerNoDamage, fleets: { formatFleets } } = KC3BattlePrediction;

    const initial = formatFleets(initialFleets);
    const result = formatFleets(resultFleets);

    return {
      fleets: result,
      isPlayerNoDamage: isPlayerNoDamage(initial, result),
    };
  };

  /*--------------------------------------------------------*/
  /* --------------------[ INTERNALS ]--------------------- */
  /*--------------------------------------------------------*/

  /* --------------------[ NO DAMAGE ]--------------------- */

  const isPlayerNoDamage = (initialFleets, resultFleets) => {
    const { extendError, isNotDamaged } = KC3BattlePrediction;

    const initialShips = initialFleets.playerMain.concat(initialFleets.playerEscort);
    const resultShips = resultFleets.playerMain.concat(resultFleets.playerEscort);

    if (initialShips.length !== resultShips.length) {
      throw extendError(new Error('Mismatched initial and result fleets'), { initialFleets, resultFleets });
    }

    return resultShips.every((resultShip, index) => isNotDamaged(initialShips[index], resultShip));
  };

  // If damecon increases HP beyond initial value, it counts as no damage
  const isNotDamaged = (initial, result) => initial.hp <= result.hp;


  /*--------------------------------------------------------*/
  /* ---------------------[ EXPORTS ]---------------------- */
  /*--------------------------------------------------------*/

  Object.assign(KC3BattlePrediction, {
    // Public
    formatResult,
    // Internals
    isPlayerNoDamage,
    isNotDamaged,
  });
}());

(function () {
  /*--------------------------------------------------------*/
  /* ----------------------[ PUBLIC ]---------------------- */
  /*--------------------------------------------------------*/

  const getBattlePhases = (battleType) => {
    const { getEngagementType, getPhaseParsers } = KC3BattlePrediction.battle.engagement;

    return getPhaseParsers(getEngagementType(battleType));
  };

  /*--------------------------------------------------------*/
  /* ---------------------[ INTERNAL ]--------------------- */
  /*--------------------------------------------------------*/

  const getPhaseParsers = (engagementInstance) => {
    // getOwnPropertyNames should guarantee (string) keys are returned in declaration order
    return Object.getOwnPropertyNames(engagementInstance).map(phase => engagementInstance[phase]);
  };

  /*--------------------------------------------------------*/
  /* ---------------------[ EXPORTS ]---------------------- */
  /*--------------------------------------------------------*/

  Object.assign(KC3BattlePrediction.battle.engagement, {
    // Public
    getBattlePhases,
    // Internal
    getPhaseParsers,
  });
}());

(function () {
  const toKey = (player, enemy, time) => `${player}-${enemy}-${time}`;

  /*--------------------------------------------------------*/
  /* ------------[ ENGAGEMENT TYPE FACTORIES ]------------- */
  /*--------------------------------------------------------*/
  const { Player, Enemy, Time } = KC3BattlePrediction;
  const types = {
    [toKey(Player.SINGLE, Enemy.SINGLE, Time.DAY)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        kouku: { parseKouku },
        support: { parseSupport },
        hougeki: { parseHougeki },
        raigeki: { parseRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        airBaseInjection: create('airBaseInjection', parseKouku),
        injectionKouku: create('injectionKouku', parseKouku),
        airBaseAttack: create('airBaseAttack', parseKouku),
        kouku: create('kouku', parseKouku),
        kouku2: create('kouku2', parseKouku),
        support: create('support', parseSupport),
        openingTaisen: create('openingTaisen', bind(parseHougeki, battleType)),
        openingAtack: create('openingAtack', bind(parseRaigeki, battleType)),
        hougeki1: create('hougeki1', bind(parseHougeki, battleType)),
        hougeki2: create('hougeki2', bind(parseHougeki, battleType)),
        hougeki3: create('hougeki3', bind(parseHougeki, battleType)),
        raigeki: create('raigeki', bind(parseRaigeki, battleType)),
      };
    },
    [toKey(Player.CTF, Enemy.SINGLE, Time.DAY)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        kouku: { parseKouku },
        support: { parseSupport },
        hougeki: { parseHougeki },
        raigeki: { parseRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        airBaseInjection: create('airBaseInjection', parseKouku),
        injectionKouku: create('injectionKouku', parseKouku),
        airBaseAttack: create('airBaseAttack', parseKouku),
        kouku: create('kouku', parseKouku),
        kouku2: create('kouku2', parseKouku),
        support: create('support', parseSupport),
        openingTaisen: create('openingTaisen', bind(parseHougeki, battleType)),
        openingAtack: create('openingAtack', bind(parseRaigeki, battleType)),
        hougeki1: create('hougeki1', bind(parseHougeki, battleType)),
        raigeki: create('raigeki', bind(parseRaigeki, battleType)),
        hougeki2: create('hougeki2', bind(parseHougeki, battleType)),
        hougeki3: create('hougeki3', bind(parseHougeki, battleType)),
      };
    },
    [toKey(Player.STF, Enemy.SINGLE, Time.DAY)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        kouku: { parseKouku },
        support: { parseSupport },
        hougeki: { parseHougeki },
        raigeki: { parseRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        airBaseInjection: create('airBaseInjection', parseKouku),
        injectionKouku: create('injectionKouku', parseKouku),
        airBaseAttack: create('airBaseAttack', parseKouku),
        kouku: create('kouku', parseKouku),
        kouku2: create('kouku2', parseKouku),
        support: create('support', parseSupport),
        openingTaisen: create('openingTaisen', bind(parseHougeki, battleType)),
        openingAtack: create('openingAtack', bind(parseRaigeki, battleType)),
        hougeki1: create('hougeki1', bind(parseHougeki, battleType)),
        hougeki2: create('hougeki2', bind(parseHougeki, battleType)),
        hougeki3: create('hougeki3', bind(parseHougeki, battleType)),
        raigeki: create('raigeki', bind(parseRaigeki, battleType)),
      };
    },
    [toKey(Player.SINGLE, Enemy.COMBINED, Time.DAY)]() {
      const {
        kouku: { parseKouku },
        support: { parseCombinedSupport },
        hougeki: { parseCombinedHougeki },
        raigeki: { parseCombinedRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        airBaseInjection: create('airBaseInjection', parseKouku),
        injectionKouku: create('injectionKouku', parseKouku),
        airBaseAttack: create('airBaseAttack', parseKouku),
        kouku: create('kouku', parseKouku),
        kouku2: create('kouku2', parseKouku),
        support: create('support', parseCombinedSupport),
        openingTaisen: create('openingTaisen', parseCombinedHougeki),
        openingAtack: create('openingAtack', parseCombinedRaigeki),
        hougeki1: create('hougeki1', parseCombinedHougeki),
        raigeki: create('raigeki', parseCombinedRaigeki),
        hougeki2: create('hougeki2', parseCombinedHougeki),
        hougeki3: create('hougeki3', parseCombinedHougeki),
      };
    },
    [toKey(Player.CTF, Enemy.COMBINED, Time.DAY)]() {
      const {
        kouku: { parseKouku },
        support: { parseCombinedSupport },
        hougeki: { parseCombinedHougeki },
        raigeki: { parseCombinedRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        airBaseInjection: create('airBaseInjection', parseKouku),
        injectionKouku: create('injectionKouku', parseKouku),
        airBaseAttack: create('airBaseAttack', parseKouku),
        kouku: create('kouku', parseKouku),
        kouku2: create('kouku2', parseKouku),
        support: create('support', parseCombinedSupport),
        openingTaisen: create('openingTaisen', parseCombinedHougeki),
        openingAtack: create('openingAtack', parseCombinedRaigeki),
        hougeki1: create('hougeki1', parseCombinedHougeki),
        hougeki2: create('hougeki2', parseCombinedHougeki),
        raigeki: create('raigeki', parseCombinedRaigeki),
        hougeki3: create('hougeki3', parseCombinedHougeki),
      };
    },
    [toKey(Player.STF, Enemy.COMBINED, Time.DAY)]() {
      const {
        kouku: { parseKouku },
        support: { parseCombinedSupport },
        hougeki: { parseCombinedHougeki },
        raigeki: { parseCombinedRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        airBaseInjection: create('airBaseInjection', parseKouku),
        injectionKouku: create('injectionKouku', parseKouku),
        airBaseAttack: create('airBaseAttack', parseKouku),
        kouku: create('kouku', parseKouku),
        kouku2: create('kouku2', parseKouku),
        support: create('support', parseCombinedSupport),
        openingTaisen: create('openingTaisen', parseCombinedHougeki),
        openingAtack: create('openingAtack', parseCombinedRaigeki),
        hougeki1: create('hougeki1', parseCombinedHougeki),
        hougeki2: create('hougeki2', parseCombinedHougeki),
        hougeki3: create('hougeki3', parseCombinedHougeki),
        raigeki: create('raigeki', parseCombinedRaigeki),
      };
    },
    /* -------------------[ NIGHT TO DAY ]------------------- */
    [toKey(Player.SINGLE, Enemy.COMBINED, Time.NIGHT_TO_DAY)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        kouku: { parseKouku },
        support: { parseCombinedSupport },
        hougeki: { parseHougeki },
        raigeki: { parseRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        kouku: create('kouku', parseKouku),
        openingTaisen: create('openingTaisen', bind(parseHougeki, battleType)),
        openingAtack: create('openingAtack', bind(parseRaigeki, battleType)),
        support: create('support', parseCombinedSupport),
        nSupport: create('nSupport', parseCombinedSupport),
        nHougeki1: create('nHougeki1', bind(parseHougeki, battleType)),
        nHougeki2: create('nHougeki2', bind(parseHougeki, battleType)),
        hougeki1: create('hougeki1', bind(parseHougeki, battleType)),
      };
    },
    [toKey(Player.SINGLE, Enemy.SINGLE, Time.NIGHT_TO_DAY)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        kouku: { parseKouku },
        support: { parseSupport },
        hougeki: { parseHougeki },
        raigeki: { parseRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        kouku: create('kouku', parseKouku),
        openingTaisen: create('openingTaisen', bind(parseHougeki, battleType)),
        openingAtack: create('openingAtack', bind(parseRaigeki, battleType)),
        support: create('support', parseSupport),
        nSupport: create('nSupport', parseSupport),
        nHougeki1: create('nHougeki1', bind(parseHougeki, battleType)),
        nHougeki2: create('nHougeki2', bind(parseHougeki, battleType)),
        hougeki1: create('hougeki1', bind(parseHougeki, battleType)),
      };
    },

    /* -------------------[ NIGHT BATTLE ]------------------- */
    [toKey(Player.SINGLE, Enemy.SINGLE, Time.NIGHT)](battleType) {
      const { bind } = KC3BattlePrediction;
      const { parseYasen } = KC3BattlePrediction.battle.phases.yasen;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        hougeki: create('midnight', bind(parseYasen, battleType)),
      };
    },
    [toKey(Player.CTF, Enemy.SINGLE, Time.NIGHT)](battleType) {
      const { bind } = KC3BattlePrediction;
      const { parseYasen } = KC3BattlePrediction.battle.phases.yasen;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        hougeki: create('midnight', bind(parseYasen, battleType)),
      };
    },
    [toKey(Player.STF, Enemy.SINGLE, Time.NIGHT)](battleType) {
      const { bind } = KC3BattlePrediction;
      const { parseYasen } = KC3BattlePrediction.battle.phases.yasen;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        hougeki: create('midnight', bind(parseYasen, battleType)),
      };
    },
    [toKey(Player.SINGLE, Enemy.COMBINED, Time.NIGHT)](battleType) {
      const { bind } = KC3BattlePrediction;
      const { parseYasen } = KC3BattlePrediction.battle.phases.yasen;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        hougeki: create('midnight', bind(parseYasen, battleType)),
      };
    },
    [toKey(Player.CTF, Enemy.COMBINED, Time.NIGHT)](battleType) {
      const { bind } = KC3BattlePrediction;
      const { parseYasen } = KC3BattlePrediction.battle.phases.yasen;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        hougeki: create('midnight', bind(parseYasen, battleType)),
      };
    },
    [toKey(Player.STF, Enemy.COMBINED, Time.NIGHT)](battleType) {
      const { bind } = KC3BattlePrediction;
      const { parseYasen } = KC3BattlePrediction.battle.phases.yasen;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        hougeki: create('midnight', bind(parseYasen, battleType)),
      };
    },
  };
  /*--------------------------------------------------------*/
  /* ---------------------[ ACCESSOR ]--------------------- */
  /*--------------------------------------------------------*/

  // The engagement types are defined as factory functions to avoid file load order dependencies
  // NB: It may be worth caching instances if performance proves to be an issue
  const getEngagementType = (battleType = {}) => {
    const { types } = KC3BattlePrediction.battle.engagement;

    const key = toKey(battleType.player, battleType.enemy, battleType.time);
    if (!types[key]) {
      throw new Error(`Bad battle type: ${JSON.stringify(battleType)}`);
    }
    return types[key](battleType);
  };<commit_msg>Update compiled BattlePrediction.js<commit_after>/**
 * Public API of Battle Prediction module.
 * Must be loaded first, since it defines the module's structure.
 *
 * DO NOT modify single file module of `src/library/module/BattlePrediction.js` directly,
 * To generate the single file module, run `grunt concat:battlePredictionDev`.
 */
(function () {
  const BP = {
    fleets: {},
    battle: {
      // Phase parsers - convert phase JSON to Attacks
      phases: {
        kouku: {}, // aerial phase (航空)
        hougeki: {}, // shelling (砲撃)
        raigeki: {}, // torpedoes (雷撃)
        support: {}, // support expedition
        yasen: {}, // night battle (夜戦)
      },
      // Specify phase ordering for each battle type
      engagement: {},
    },
    // Rank prediction
    rank: {
      airRaid: {},
      battle: {},
    },
    // MVP prediction
    mvp: {},
  };

  // --------------------------------------------------------------------------
  // PUBLIC API
  // --------------------------------------------------------------------------

  BP.analyzeBattle = (battleData, playerDamecons, battleType) => {
    const { fleets, battle, formatResult } = KC3BattlePrediction;

    try {
      const initialFleets = fleets.getInitialState(battleData, playerDamecons);
      const resultFleets = battle.simulateBattle(battleData, initialFleets, battleType);

      return formatResult(initialFleets, resultFleets);
    } catch (error) {
      // Pass context explicitly, so it is recorded
      KC3Log.error(error, error.data, { battleType, battleData, playerDamecons });
      throw error;
    }
  };

  BP.predictRank = (apiName, battleData, battleResult) => {
    const { parseStartJson, normalizeFleets, getRankPredictor } = KC3BattlePrediction.rank;

    return getRankPredictor(apiName).predict(
      normalizeFleets(parseStartJson(battleData), battleData),
      normalizeFleets(battleResult, battleData)
    );
  };

  BP.predictMvp = (dayResult, nightResult) => {
    const { combineResults, getHighestDamage } = KC3BattlePrediction.mvp;

    const { playerMain, playerEscort } = combineResults(dayResult, nightResult);
    return {
      playerMain: getHighestDamage(playerMain),
      playerEscort: getHighestDamage(playerEscort),
    };
  };

  // --------------------------------------------------------------------------
  // ENUMS
  // --------------------------------------------------------------------------

  // PUBLIC ENUMS
  // -------------

  BP.Player = Object.freeze({ SINGLE: 'single', STF: 'stf', CTF: 'ctf', TCF: 'ctf' });

  BP.Enemy = Object.freeze({ SINGLE: 'single', COMBINED: 'combined' });

  BP.Time = Object.freeze({ DAY: 'day', NIGHT: 'night', NIGHT_TO_DAY: 'night-to-day' });

  // INTERNAL ENUMS
  // ---------------

  // Player or enemy ship
  BP.Side = Object.freeze({ PLAYER: 'player', ENEMY: 'enemy' });

  // Ship in main or escort fleet
  BP.Role = Object.freeze({ MAIN_FLEET: 'main', ESCORT_FLEET: 'escort' });

  // Damecon API codes
  BP.Damecon = Object.freeze({ NONE: 0, TEAM: 1, GODDESS: 2 });

  // --------------------------------------------------------------------------
  // INTERNAL METHODS
  // --------------------------------------------------------------------------

  BP.EMPTY_SLOT = -1;

  BP.extendError = (error, data) =>
    (error.data ? Object.assign(error.data, data) : Object.assign(error, { data }));

  BP.normalizeArrayIndexing = array => (array[0] === -1 ? array.slice(1) : array);

  BP.bind = (target, ...args) => target.bind(null, ...args);

  BP.validateEnum = (enumObj, value) => {
    // Ideally this should use Object.values(), but it was only introduced in Chrome 54
    return Object.keys(enumObj).some(key => enumObj[key] === value);
  };

  /* ---------------------[ ZIP WITH ]--------------------- */

  const parseArgs = (args) => {
    const iteratee = args.slice(-1)[0];

    return typeof iteratee === 'function'
      ? { arrays: args.slice(0, -1), iteratee }
      : { arrays: args, iteratee: (...elements) => elements };
  };
  const getElements = (arrays, index) => arrays.map(array => array[index]);
  const getLongestArray = arrays =>
    arrays.reduce((result, array) => (array.length > result ? array.length : result), 0);
  // See: http://devdocs.io/lodash~4/index#zipWith
  BP.zipWith = (...args) => {
    const { arrays, iteratee } = parseArgs(args);

    const length = getLongestArray(arrays);
    const result = [];
    for (let i = 0; i < length; i += 1) {
      const elements = getElements(arrays, i);
      result.push(iteratee(...elements));
    }

    return result;
  };

  window.KC3BattlePrediction = BP;
}());

(function () {
  const battle = {};
  /*--------------------------------------------------------*/
  /* ----------------------[ PUBLIC ]---------------------- */
  /*--------------------------------------------------------*/

  battle.simulateBattle = (battleData, initalFleets, battleType) => {
    const { battle: { parseBattle }, fleets: { simulateAttack } } = KC3BattlePrediction;

    return parseBattle(battleType, battleData).reduce(simulateAttack, initalFleets);
  };

  /*--------------------------------------------------------*/
  /* ---------------------[ INTERNAL ]--------------------- */
  /*--------------------------------------------------------*/

  /* -----------------[ PARSE BATTLE DATA ]---------------- */

  battle.parseBattle = (battleType, battleData) => {
    const { accumulateAttacks, parsePhases } = KC3BattlePrediction.battle;

    return accumulateAttacks(parsePhases(battleType, battleData));
  };

  battle.parsePhases = (battleType, battleData) => {
    const { getBattlePhases } = KC3BattlePrediction.battle.engagement;

    return getBattlePhases(battleType).map(parsePhase => parsePhase(battleData));
  };

  battle.accumulateAttacks = phases =>
    phases.reduce((attacks, phase) => attacks.concat(phase), []);

  /*--------------------------------------------------------*/
  /* ---------------------[ EXPORTS ]---------------------- */
  /*--------------------------------------------------------*/

  Object.assign(window.KC3BattlePrediction.battle, battle);
}());

(function () {
  /*--------------------------------------------------------*/
  /* --------------------[ PUBLIC API ]-------------------- */
  /*--------------------------------------------------------*/

  // Create a Fleets object with the state of the player and enemy fleets at battle start
  const getInitialState = ({ api_f_nowhps, api_f_maxhps, api_e_nowhps, api_e_maxhps,
      api_f_nowhps_combined, api_f_maxhps_combined, api_e_nowhps_combined, api_e_maxhps_combined },
      playerDamecons) => {
    const { Role } = KC3BattlePrediction;
    const { getFleetShips, addDamecons, createFleets } = KC3BattlePrediction.fleets;

    const mainFleets = getFleetShips(api_f_nowhps, api_f_maxhps, api_e_nowhps, api_e_maxhps);
    const escortFleets = getFleetShips(api_f_nowhps_combined, api_f_maxhps_combined, api_e_nowhps_combined, api_e_maxhps_combined);

    return createFleets(
      addDamecons(Role.MAIN_FLEET, playerDamecons, mainFleets.player),
      mainFleets.enemy,
      addDamecons(Role.ESCORT_FLEET, playerDamecons, escortFleets.player),
      escortFleets.enemy
    );
  };

  /*--------------------------------------------------------*/
  /* --------------------[ INTERNALS ]--------------------- */
  /*--------------------------------------------------------*/

  /* ----------------------[ SHIPS ]----------------------- */

  const getFleetShips = (nowhpsPlayer, maxhpsPlayer, nowhpsEnemy, maxhpsEnemy) => {
    const { normalizeHps, convertToShips } = KC3BattlePrediction.fleets;

    // short-circuit if neither side has a fleet
    if (!nowhpsPlayer && !maxhpsPlayer && !nowhpsEnemy && !maxhpsEnemy) { return { player: [], enemy: [] }; }

    return {
      player: convertToShips(normalizeHps(nowhpsPlayer), normalizeHps(maxhpsPlayer)),
      enemy: convertToShips(normalizeHps(nowhpsEnemy), normalizeHps(maxhpsEnemy)),
    };
  };

  const normalizeHps = (hps = []) => {
    const { EMPTY_SLOT } = KC3BattlePrediction;

    if (hps.length < 6) {
      const emptySlotCount = 6 - (hps.length % 6);
      return hps.concat(new Array(emptySlotCount).fill(EMPTY_SLOT));
    }
    return hps;
  };

  const convertToShips = (nowHps, maxHps) => {
    const { createShip } = KC3BattlePrediction.fleets;

    if (nowHps.length !== maxHps.length) {
      throw new Error(`Length of nowhps (${nowHps.length}) and maxhps (${maxHps.length}) do not match`);
    }

    return nowHps.map((currentHp, index) => createShip(currentHp, maxHps[index]));
  };

  const isFleetNotEmpty = ships => ships.some(ship => ship !== KC3BattlePrediction.EMPTY_SLOT);
  const splitSides = (ships) => {
    if (ships.length !== 6 && ships.length !== 12) {
      throw new Error(`Expected 6 or 12 ships, but got ${ships.length}`);
    }

    const player = ships.slice(0, 6);
    const enemy = ships.slice(6, 12);

    return {
      player: isFleetNotEmpty(player) ? player : [],
      enemy,
    };
  };

  /* ---------------------[ DAMECONS ]--------------------- */

  const addDamecons = (role, damecons, ships) => {
    const { getDamecons, installDamecon } = KC3BattlePrediction.fleets;

    const dameconCodes = getDamecons(role, damecons);

    return ships.map((ship, index) => installDamecon(dameconCodes[index], ship));
  };

  const getDamecons = (role, damecons) => {
    const { Role } = KC3BattlePrediction;

    switch (role) {
      case Role.MAIN_FLEET:
        return damecons;
      case Role.ESCORT_FLEET:
        return damecons.slice(6, 12);
      default:
        throw new Error(`Bad role: ${role}`);
    }
  };

  /*--------------------------------------------------------*/
  /* ---------------------[ EXPORTS ]---------------------- */
  /*--------------------------------------------------------*/

  Object.assign(window.KC3BattlePrediction.fleets, {
    // Public
    getInitialState,
    // Internal
    getFleetShips,
    normalizeHps,
    convertToShips,
    splitSides,

    addDamecons,
    getDamecons,
  });
}());

(function () {
  /*--------------------------------------------------------*/
  /* --------------------[ PUBLIC API ]-------------------- */
  /*--------------------------------------------------------*/

  const combineResults = (dayFleets, nightFleets) => {
    const { combineFleetResults } = KC3BattlePrediction.mvp;

    if (!nightFleets) { return dayFleets; }
    if (!dayFleets) { return nightFleets; }
    return {
      playerMain: combineFleetResults(dayFleets.playerMain, nightFleets.playerMain),
      playerEscort: combineFleetResults(dayFleets.playerEscort, nightFleets.playerEscort),
    };
  };

  const getHighestDamage = (fleet) => {
    const { index } = fleet.reduce((result, { damageDealt }, i) => {
      return damageDealt > result.damageDealt
        ? { damageDealt, index: i }
        : result;
    }, { damageDealt: -1 });
    // MVP index 1-based
    return index !== undefined ? index + 1 : undefined;
  };

  /*--------------------------------------------------------*/
  /* --------------------[ INTERNALS ]--------------------- */
  /*--------------------------------------------------------*/

  const combineFleetResults = (day, night) => {
    const { zipWith } = KC3BattlePrediction;

    return zipWith(day, night, (dayResult, nightResult) => ({
      damageDealt: dayResult.damageDealt + nightResult.damageDealt,
    }));
  };

  /*--------------------------------------------------------*/
  /* ---------------------[ EXPORTS ]---------------------- */
  /*--------------------------------------------------------*/

  Object.assign(KC3BattlePrediction.mvp, {
    // Public
    combineResults,
    getHighestDamage,
    // Internal
    combineFleetResults,
  });
}());

(function () {
  /*--------------------------------------------------------*/
  /* --------------------[ PUBLIC API ]-------------------- */
  /*--------------------------------------------------------*/

  const formatResult = (initialFleets, resultFleets) => {
    const { isPlayerNoDamage, fleets: { formatFleets } } = KC3BattlePrediction;

    const initial = formatFleets(initialFleets);
    const result = formatFleets(resultFleets);

    return {
      fleets: result,
      isPlayerNoDamage: isPlayerNoDamage(initial, result),
    };
  };

  /*--------------------------------------------------------*/
  /* --------------------[ INTERNALS ]--------------------- */
  /*--------------------------------------------------------*/

  /* --------------------[ NO DAMAGE ]--------------------- */

  const isPlayerNoDamage = (initialFleets, resultFleets) => {
    const { extendError, isNotDamaged } = KC3BattlePrediction;

    const initialShips = initialFleets.playerMain.concat(initialFleets.playerEscort);
    const resultShips = resultFleets.playerMain.concat(resultFleets.playerEscort);

    if (initialShips.length !== resultShips.length) {
      throw extendError(new Error('Mismatched initial and result fleets'), { initialFleets, resultFleets });
    }

    return resultShips.every((resultShip, index) => isNotDamaged(initialShips[index], resultShip));
  };

  // If damecon increases HP beyond initial value, it counts as no damage
  const isNotDamaged = (initial, result) => initial.hp <= result.hp;


  /*--------------------------------------------------------*/
  /* ---------------------[ EXPORTS ]---------------------- */
  /*--------------------------------------------------------*/

  Object.assign(KC3BattlePrediction, {
    // Public
    formatResult,
    // Internals
    isPlayerNoDamage,
    isNotDamaged,
  });
}());

(function () {
  /*--------------------------------------------------------*/
  /* ----------------------[ PUBLIC ]---------------------- */
  /*--------------------------------------------------------*/

  const getBattlePhases = (battleType) => {
    const { getEngagementType, getPhaseParsers } = KC3BattlePrediction.battle.engagement;

    return getPhaseParsers(getEngagementType(battleType));
  };

  /*--------------------------------------------------------*/
  /* ---------------------[ INTERNAL ]--------------------- */
  /*--------------------------------------------------------*/

  const getPhaseParsers = (engagementInstance) => {
    // getOwnPropertyNames should guarantee (string) keys are returned in declaration order
    return Object.getOwnPropertyNames(engagementInstance).map(phase => engagementInstance[phase]);
  };

  /*--------------------------------------------------------*/
  /* ---------------------[ EXPORTS ]---------------------- */
  /*--------------------------------------------------------*/

  Object.assign(KC3BattlePrediction.battle.engagement, {
    // Public
    getBattlePhases,
    // Internal
    getPhaseParsers,
  });
}());

(function () {
  const toKey = (player, enemy, time) => `${player}-${enemy}-${time}`;

  /*--------------------------------------------------------*/
  /* ------------[ ENGAGEMENT TYPE FACTORIES ]------------- */
  /*--------------------------------------------------------*/
  const { Player, Enemy, Time } = KC3BattlePrediction;
  const types = {
    [toKey(Player.SINGLE, Enemy.SINGLE, Time.DAY)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        kouku: { parseKouku },
        support: { parseSupport },
        hougeki: { parseHougeki },
        raigeki: { parseRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        airBaseInjection: create('airBaseInjection', parseKouku),
        injectionKouku: create('injectionKouku', parseKouku),
        airBaseAttack: create('airBaseAttack', parseKouku),
        kouku: create('kouku', parseKouku),
        kouku2: create('kouku2', parseKouku),
        support: create('support', parseSupport),
        openingTaisen: create('openingTaisen', bind(parseHougeki, battleType)),
        openingAtack: create('openingAtack', bind(parseRaigeki, battleType)),
        hougeki1: create('hougeki1', bind(parseHougeki, battleType)),
        hougeki2: create('hougeki2', bind(parseHougeki, battleType)),
        hougeki3: create('hougeki3', bind(parseHougeki, battleType)),
        raigeki: create('raigeki', bind(parseRaigeki, battleType)),
      };
    },
    [toKey(Player.CTF, Enemy.SINGLE, Time.DAY)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        kouku: { parseKouku },
        support: { parseSupport },
        hougeki: { parseHougeki },
        raigeki: { parseRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        airBaseInjection: create('airBaseInjection', parseKouku),
        injectionKouku: create('injectionKouku', parseKouku),
        airBaseAttack: create('airBaseAttack', parseKouku),
        kouku: create('kouku', parseKouku),
        kouku2: create('kouku2', parseKouku),
        support: create('support', parseSupport),
        openingTaisen: create('openingTaisen', bind(parseHougeki, battleType)),
        openingAtack: create('openingAtack', bind(parseRaigeki, battleType)),
        hougeki1: create('hougeki1', bind(parseHougeki, battleType)),
        raigeki: create('raigeki', bind(parseRaigeki, battleType)),
        hougeki2: create('hougeki2', bind(parseHougeki, battleType)),
        hougeki3: create('hougeki3', bind(parseHougeki, battleType)),
      };
    },
    [toKey(Player.STF, Enemy.SINGLE, Time.DAY)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        kouku: { parseKouku },
        support: { parseSupport },
        hougeki: { parseHougeki },
        raigeki: { parseRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        airBaseInjection: create('airBaseInjection', parseKouku),
        injectionKouku: create('injectionKouku', parseKouku),
        airBaseAttack: create('airBaseAttack', parseKouku),
        kouku: create('kouku', parseKouku),
        kouku2: create('kouku2', parseKouku),
        support: create('support', parseSupport),
        openingTaisen: create('openingTaisen', bind(parseHougeki, battleType)),
        openingAtack: create('openingAtack', bind(parseRaigeki, battleType)),
        hougeki1: create('hougeki1', bind(parseHougeki, battleType)),
        hougeki2: create('hougeki2', bind(parseHougeki, battleType)),
        hougeki3: create('hougeki3', bind(parseHougeki, battleType)),
        raigeki: create('raigeki', bind(parseRaigeki, battleType)),
      };
    },
    [toKey(Player.SINGLE, Enemy.COMBINED, Time.DAY)]() {
      const {
        kouku: { parseKouku },
        support: { parseCombinedSupport },
        hougeki: { parseCombinedHougeki },
        raigeki: { parseCombinedRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        airBaseInjection: create('airBaseInjection', parseKouku),
        injectionKouku: create('injectionKouku', parseKouku),
        airBaseAttack: create('airBaseAttack', parseKouku),
        kouku: create('kouku', parseKouku),
        kouku2: create('kouku2', parseKouku),
        support: create('support', parseCombinedSupport),
        openingTaisen: create('openingTaisen', parseCombinedHougeki),
        openingAtack: create('openingAtack', parseCombinedRaigeki),
        hougeki1: create('hougeki1', parseCombinedHougeki),
        raigeki: create('raigeki', parseCombinedRaigeki),
        hougeki2: create('hougeki2', parseCombinedHougeki),
        hougeki3: create('hougeki3', parseCombinedHougeki),
      };
    },
    [toKey(Player.CTF, Enemy.COMBINED, Time.DAY)]() {
      const {
        kouku: { parseKouku },
        support: { parseCombinedSupport },
        hougeki: { parseCombinedHougeki },
        raigeki: { parseCombinedRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        airBaseInjection: create('airBaseInjection', parseKouku),
        injectionKouku: create('injectionKouku', parseKouku),
        airBaseAttack: create('airBaseAttack', parseKouku),
        kouku: create('kouku', parseKouku),
        kouku2: create('kouku2', parseKouku),
        support: create('support', parseCombinedSupport),
        openingTaisen: create('openingTaisen', parseCombinedHougeki),
        openingAtack: create('openingAtack', parseCombinedRaigeki),
        hougeki1: create('hougeki1', parseCombinedHougeki),
        hougeki2: create('hougeki2', parseCombinedHougeki),
        raigeki: create('raigeki', parseCombinedRaigeki),
        hougeki3: create('hougeki3', parseCombinedHougeki),
      };
    },
    [toKey(Player.STF, Enemy.COMBINED, Time.DAY)]() {
      const {
        kouku: { parseKouku },
        support: { parseCombinedSupport },
        hougeki: { parseCombinedHougeki },
        raigeki: { parseCombinedRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        airBaseInjection: create('airBaseInjection', parseKouku),
        injectionKouku: create('injectionKouku', parseKouku),
        airBaseAttack: create('airBaseAttack', parseKouku),
        kouku: create('kouku', parseKouku),
        kouku2: create('kouku2', parseKouku),
        support: create('support', parseCombinedSupport),
        openingTaisen: create('openingTaisen', parseCombinedHougeki),
        openingAtack: create('openingAtack', parseCombinedRaigeki),
        hougeki1: create('hougeki1', parseCombinedHougeki),
        hougeki2: create('hougeki2', parseCombinedHougeki),
        hougeki3: create('hougeki3', parseCombinedHougeki),
        raigeki: create('raigeki', parseCombinedRaigeki),
      };
    },
    /* -------------------[ NIGHT TO DAY ]------------------- */
    [toKey(Player.SINGLE, Enemy.COMBINED, Time.NIGHT_TO_DAY)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        kouku: { parseKouku },
        support: { parseCombinedSupport },
        hougeki: { parseHougeki },
        raigeki: { parseRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        nSupport: create('nSupport', parseCombinedSupport),
        nHougeki1: create('nHougeki1', bind(parseHougeki, battleType)),
        nHougeki2: create('nHougeki2', bind(parseHougeki, battleType)),
        airBaseInjection: create('airBaseInjection', parseKouku),
        injectionKouku: create('injectionKouku', parseKouku),
        airBaseAttack: create('airBaseAttack', parseKouku),
        kouku: create('kouku', parseKouku),
        support: create('support', parseCombinedSupport),
        openingTaisen: create('openingTaisen', bind(parseHougeki, battleType)),
        openingAtack: create('openingAtack', bind(parseRaigeki, battleType)),
        hougeki1: create('hougeki1', bind(parseHougeki, battleType)),
        hougeki2: create('hougeki2', bind(parseHougeki, battleType)),
        raigeki: create('raigeki', bind(parseRaigeki, battleType)),
      };
    },
    [toKey(Player.SINGLE, Enemy.SINGLE, Time.NIGHT_TO_DAY)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        kouku: { parseKouku },
        support: { parseSupport },
        hougeki: { parseHougeki },
        raigeki: { parseRaigeki },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        nSupport: create('nSupport', parseSupport),
        nHougeki1: create('nHougeki1', bind(parseHougeki, battleType)),
        nHougeki2: create('nHougeki2', bind(parseHougeki, battleType)),
        airBaseInjection: create('airBaseInjection', parseKouku),
        injectionKouku: create('injectionKouku', parseKouku),
        airBaseAttack: create('airBaseAttack', parseKouku),
        kouku: create('kouku', parseKouku),
        support: create('support', parseSupport),
        openingTaisen: create('openingTaisen', bind(parseHougeki, battleType)),
        openingAtack: create('openingAtack', bind(parseRaigeki, battleType)),
        hougeki1: create('hougeki1', bind(parseHougeki, battleType)),
        hougeki2: create('hougeki2', bind(parseHougeki, battleType)),
        raigeki: create('raigeki', bind(parseRaigeki, battleType)),
      };
    },

    /* -------------------[ NIGHT BATTLE ]------------------- */
    [toKey(Player.SINGLE, Enemy.SINGLE, Time.NIGHT)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        support: { parseSupport },
        yasen: { parseYasen },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        nSupport: create('nSupport', parseSupport),
        hougeki: create('midnight', bind(parseYasen, battleType)),
      };
    },
    [toKey(Player.CTF, Enemy.SINGLE, Time.NIGHT)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        support: { parseSupport },
        yasen: { parseYasen },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        nSupport: create('nSupport', parseSupport),
        hougeki: create('midnight', bind(parseYasen, battleType)),
      };
    },
    [toKey(Player.STF, Enemy.SINGLE, Time.NIGHT)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        support: { parseSupport },
        yasen: { parseYasen },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        nSupport: create('nSupport', parseSupport),
        hougeki: create('midnight', bind(parseYasen, battleType)),
      };
    },
    [toKey(Player.SINGLE, Enemy.COMBINED, Time.NIGHT)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        support: { parseCombinedSupport },
        yasen: { parseYasen },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        nSupport: create('nSupport', parseCombinedSupport),
        hougeki: create('midnight', bind(parseYasen, battleType)),
      };
    },
    [toKey(Player.CTF, Enemy.COMBINED, Time.NIGHT)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        support: { parseCombinedSupport },
        yasen: { parseYasen },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        nSupport: create('nSupport', parseCombinedSupport),
        hougeki: create('midnight', bind(parseYasen, battleType)),
      };
    },
    [toKey(Player.STF, Enemy.COMBINED, Time.NIGHT)](battleType) {
      const { bind } = KC3BattlePrediction;
      const {
        support: { parseCombinedSupport },
        yasen: { parseYasen },
      } = KC3BattlePrediction.battle.phases;
      const { create } = KC3BattlePrediction.battle.engagement.parserFactory;

      return {
        nSupport: create('nSupport', parseCombinedSupport),
        hougeki: create('midnight', bind(parseYasen, battleType)),
      };
    },
  };
  /*--------------------------------------------------------*/
  /* ---------------------[ ACCESSOR ]--------------------- */
  /*--------------------------------------------------------*/

  // The engagement types are defined as factory functions to avoid file load order dependencies
  // NB: It may be worth caching instances if performance proves to be an issue
  const getEngagementType = (battleType = {}) => {
    const { types } = KC3BattlePrediction.battle.engagement;

    const key = toKey(battleType.player, battleType.enemy, battleType.time);
    if (!types[key]) {
      throw new Error(`Bad battle type: ${JSON.stringify(battleType)}`);
    }
    return types[key](battleType);
  };
{'fraction_non_alphanumeric': 0.24102573409313638, 'fraction_numerical': 0.00606148597147109, 'mean_word_length': 2.022986614000439, 'pattern_counts': {'":': 0, '<': 9, '<?xml version=': 0, '>': 99, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3505917', 'n_tokens_mistral': 17084, 'n_tokens_neox': 16540, 'n_words': 2899}