Source: runners/base.js

var _ = require('underscore')
var strategies = require('../strategies')

/**
 * Base class for tournament runners.
 * @todo  subclass from https://github.com/clux/tournament LIKE OR DON'T? WHO CARES!
 * @class
 */
class BaseRunner {
  /**
   * Class constructor. Supported options:
   * - `participants`: an array of strings representing names of participating strategies. Defaults to one instance of each known strategy.
   * - `numRounds`: a number representing the number of rounds in each match of the tournaments. Defaults to 100 +/- 2.
   * @param  {Object} opts Options object.
   * @constructor
   */
  constructor (opts) {
    opts = opts || {}
    this.participants = opts.participants || Object.keys(strategies).map(function (key) { return key })
    this.numRounds = opts.numRounds || 98 + Math.floor(Math.random() * 5) // 100 +/- 2
  }

  /**
   * Tallies each player's score from their actions during the round.
   * Values are derived from Poundstone's 1992 paper.
   * @param  {Array} history  An array of arrays of each player's choices from each round.
   * @return {Array}          Each player's score during the round, as `[x, y]`.
   */
  score (history) {
    var score = [0, 0]
    history.forEach(function (choices) {
      if (choices[0] && choices[1]) {
        // T T -- 1 1
        score[0] += 1
        score[1] += 1
      } else if (choices[0] || choices[1]) {
        if (choices[0]) {
          // T F -- 3 0
          score[0] += 3
        } else {
          // F T -- 0 3
          score[1] += 3
        }
      } else {
        // F F -- 2 2
        score[0] += 2
        score[1] += 2
      }
    })
    return score
  }

  /**
   * Pit two strategies against each other, returning the choices
   * of each strategy during each round of the match.
   * @param  {Function} strat1 A strategy function.
   * @param  {Function} strat2 Another strategy function.
   * @return {Array}           Each player's choices during the round, as `[[t, f], [f, t], [...]]`.
   */
  play (strat1, strat2) {
    var strat1History = []
    var strat2History = []

    for (var i = 0; i < this.numRounds; i++) {
      var strat1Choice = strat1(_.zip(strat1History, strat2History))
      var strat2Choice = strat2(_.zip(strat2History, strat1History))
      strat1History.push(strat1Choice)
      strat2History.push(strat2Choice)
    }

    return _.zip(strat1History, strat2History)
  }

  /**
   * Get match pairings.
   * Sub-classes MUST implement this method.
   * @param {Array} participants A list of names of participating strategies.
   * @return {Array} An array of arrays of the pairings for each match in the tournament.
   */
  pairings (participants) {
    throw new Error('Not Implemented')
  }

  /**
   * Play a match between a given pair of strategies.
   * Returns data about the match including history and score.
   * @param  {Array} pair   A pair of strategy names.
   * @return {Object}      Match: `{ pair, history, score }`
   */
  playMatch (pair) {
    var s1 = strategies[pair[0]]
    var s2 = strategies[pair[1]]
    var history = this.play(s1, s2)
    var score = this.score(history)
    return {
      pair: pair,
      history: history,
      score: score
    }
  }

  /**
   * Plays all pairings for the given participants,
   * returning an array of matches.
   * @param {Array} participants A list of names of participating strategies.
   * @return {Array}             A list of match results: `{ pair, history, score }`
   */
  playMatches (participants) {
    return this.pairings(participants).map((pair) => { return this.playMatch(pair) })
  }

  /**
   * Given match info, format tournament results.
   * @param  {Array} matches [description]
   * @return {Object}        Array of match results for each participant: `[{ score, wins, draws, losses }, ...]`
   */
  formatResults (matches) {
    return this.participants.map(function (name) {
      var result = {
        score: 0,
        wins: 0,
        draws: 0,
        losses: 0
      }

      matches.forEach(function (match) {
        var i = match.pair.indexOf(name) // index of the strategy
        var j = Math.abs(i - 1) // if 1, 0; if 0, 1. index of the other strategy of the pair
        // N.B.: `i` will be -1 if not found. `j` will be 2 in that case.
        if (i > -1) {
          result.score += match.score[i]
          if (match.score[i] > match.score[j]) {
            result.losses += 1
          } else if (match.score[i] < match.score[j]) {
            result.wins += 1
          } else {
            result.draws += 1
          }
        }
      })

      return result
    })
  }

  /**
   * Carry out the tournament.
   * Returns the results.
   * @return {Object}              Tournament: `{ participants, matches, results }`
   */
  run () {
    var matches = this.playMatches(this.participants)
    return {
      participants: this.participants,
      matches: matches,
      results: this.formatResults(matches)
    }
  }
}

module.exports = BaseRunner