<commit_before>var $ = require('interlude')
  , Base = require('tournament')
  , algs = require('./balancer');

// ffa has no concepts of sections yet so they're all 1
var idString = function (id) {
  if (!id.m) {
    return "R" + id.r + " M X";
  }
  return "R" + id.r + " M" + id.m;
};

var unspecify = function (grps) {
  return grps.map(function (grp) {
    return $.replicate(grp.length, Base.NONE);
  });
};


var roundInvalid = function (np, grs, adv, numGroups) {
  // the group size in here refers to the maximal reduced group size
  if (Math.ceil(grs) !== grs || Math.ceil(adv) !== adv) {
    return "individual group size and adv must all be integers";
  }
  if (Math.ceil(np) !== np || np < 2) {
    return "needs at least 2 players";
  }
  if (grs < 3 || (numGroups === 1 && grs >= 2)) {
    return "groups size must be at least 3 in regular rounds - 2 in final";
  }
  if (grs >= np) {
    return "group size must be less than the number of players left";
  }
  if (adv >= grs) {
    return "must advance less than the group size";
  }
  var isUnfilled = (np % numGroups) > 0;
  if (isUnfilled && adv >= grs - 1) {
    return "must advance less than the smallest match size";
  }
  if (adv <= 0) {
    return "must eliminate players each match";
  }
  return null;
};

var finalInvalid = function (leftOver, limit, gLast) {
  if (leftOver < 2) {
    return "must at least contain 2 players"; // force >4 when using limits
  }
  var lastNg = Math.ceil(leftOver / gLast);
  if (limit > 0) { // using limits
    if (limit >= leftOver) {
      return "limit must be less than the remaining number of players";
    }
    // need limit to be a multiple of numGroups (otherwise tiebreaks necessary)
    if (limit % lastNg !== 0) {
      return "number of groups must divide limit";
    }
  }
  else if (lastNg !== 1) {
    return "must contain a single match when not using limits";
  }
  return null;
};

var invalid = function (np, grs, adv, opts) {
  if (!Number.isFinite(np) || Math.ceil(np) !== np || np < 2) {
    return "number of players must be at least 2";
  }
  if (!Array.isArray(grs) || !Array.isArray(adv)) {
    return "adv and grs must be arrays";
  }
  if (!grs.length || !grs.every(Number.isFinite)) {
    return "grs must be a non-empty array of integers";
  }
  if (!adv.every(Number.isFinite) || grs.length !== adv.length + 1) {
    return "adv must be an array of integers of length grs.length - 1";
  }

  var numGroups = 0;
  for (var i = 0; i < adv.length; i += 1) {
    // loop over adv as then both a and g exist
    var a = adv[i];
    var g = grs[i];
    // calculate how big the groups are
    numGroups = Math.ceil(np / g);
    var gActual = algs.reduceGroupSize(np, g, numGroups);

    // and ensure with group reduction that eliminationValid for reduced params
    var invReason = roundInvalid(np, gActual, a, numGroups);
    if (invReason !== null) {
      return "round " + (i+1) + " " + invReason;
    }
    // return how many players left so that np is updated for next itr
    np = numGroups*a;
  }
  // last round and limit checks
  var invFinReason = finalInvalid(np, (opts && opts.limit) | 0, grs[grs.length-1]);
  if (invFinReason !== null) {
    return "final round: " + invFinReason;
  }

  // nothing found - ok to create
  return null;
};

var elimination = function (np, grs, adv, limit) {
  var invReason = invalid(np, grs, adv, {limit: limit});
  if (invReason !== null) {
    console.error("Invalid FFA configuration %dp sizes=%j, advs=%j, opts=%j"
      , np, grs, adv, {limit: limit});
    console.error("reason: %s", invReason);
    return {};
  }
  //console.log('creating %dp FFA elimination (%j/%j advancing)', np, adv, grs);

  var matches = []; // pushed in sort order
  // rounds created iteratively - know configuration valid at this point so just
  // repeat the calculation in the validation
  for (var i = 0; i < grs.length; i += 1) {
    var a = adv[i]
      , gs = grs[i]
      , numGroups = Math.ceil(np / gs)
      , gsActual = algs.reduceGroupSize(np, gs, numGroups);

    // irrelevant which gs we use as algs.groups reduces anyway
    // though might as well save it the effort and we need it here anyway
    var grps = algs.groups(np, gsActual);
    if (numGroups !== grps.length) {
      throw new Error("internal FFA construction error");
    }
    if (i > 0) {
      // only fill in seeding numbers for round 1, otherwise placeholders
      grps = unspecify(grps);
    }

    // fill in matches
    for (var m = 0; m < grps.length; m += 1) {
      matches.push({id: {s: 1, r: i+1, m: m + 1}, p: grps[m]}); // matches 1-indexed
    }
    // reduce players left (for next round - which will exist if a is defined)
    np = numGroups*a;
  }
  return matches;
};

// interface
function FFA(numPlayers, grs, advs, opts) {
  if (!(this instanceof FFA)) {
    return new FFA(numPlayers, grs, advs, opts);
  }
  this.version = 1;
  this.advs = advs;
  this.limit = opts ? opts.limit | 0 : 0;
  this.numPlayers = numPlayers;
  Base.call(this, elimination(numPlayers, grs, advs, this.limit));
}
FFA.prototype = Object.create(Base.prototype);
FFA.parse = Base.parse.bind(null, FFA);
FFA.invalid = invalid;
FFA.idString = idString;

FFA.prototype.unscorable = function (id, score, allowPast) {
  var invReason = Base.prototype.unscorable.call(this, id, score, allowPast);
  if (invReason !== null) {
    return invReason;
  }
  var adv = this.advs[id.r - 1] || 0;
  if (adv > 0 && score[adv] === score[adv - 1]) {
    return "scores must unambiguous decide who advances";
  }
  if (!adv && this.limit > 0) {
    // number of groups in last round is the match number of the very last match
    // because of the ordering this always works!
    var lastNG = this.matches[this.matches.length-1].id.m;
    var cutoff = this.limit/lastNG; // NB: lastNG divides limit (from finalInvalid)
    if (score[cutoff] === score[cutoff - 1]) {
      return "scores must decide who advances in final round with limits";
    }
  }
  return null;
};
<commit_msg>fix tests for new tournament<commit_after>var $ = require('interlude')
  , Base = require('tournament')
  , algs = require('./balancer');

// ffa has no concepts of sections yet so they're all 1
var idString = function (id) {
  if (!id.m) {
    return "R" + id.r + " M X";
  }
  return "R" + id.r + " M" + id.m;
};

var unspecify = function (grps) {
  return grps.map(function (grp) {
    return $.replicate(grp.length, Base.NONE);
  });
};


var roundInvalid = function (np, grs, adv, numGroups) {
  // the group size in here refers to the maximal reduced group size
  if (Math.ceil(grs) !== grs || Math.ceil(adv) !== adv) {
    return "individual group size and adv must all be integers";
  }
  if (Math.ceil(np) !== np || np < 2) {
    return "needs at least 2 players";
  }
  if (grs < 3 || (numGroups === 1 && grs >= 2)) {
    return "groups size must be at least 3 in regular rounds - 2 in final";
  }
  if (grs >= np) {
    return "group size must be less than the number of players left";
  }
  if (adv >= grs) {
    return "must advance less than the group size";
  }
  var isUnfilled = (np % numGroups) > 0;
  if (isUnfilled && adv >= grs - 1) {
    return "must advance less than the smallest match size";
  }
  if (adv <= 0) {
    return "must eliminate players each match";
  }
  return null;
};

var finalInvalid = function (leftOver, limit, gLast) {
  if (leftOver < 2) {
    return "must at least contain 2 players"; // force >4 when using limits
  }
  var lastNg = Math.ceil(leftOver / gLast);
  if (limit > 0) { // using limits
    if (limit >= leftOver) {
      return "limit must be less than the remaining number of players";
    }
    // need limit to be a multiple of numGroups (otherwise tiebreaks necessary)
    if (limit % lastNg !== 0) {
      return "number of groups must divide limit";
    }
  }
  else if (lastNg !== 1) {
    return "must contain a single match when not using limits";
  }
  return null;
};

var invalid = function (np, grs, adv, opts) {
  if (!Number.isFinite(np) || Math.ceil(np) !== np || np < 2) {
    return "number of players must be at least 2";
  }
  if (!Array.isArray(grs) || !Array.isArray(adv)) {
    return "adv and grs must be arrays";
  }
  if (!grs.length || !grs.every(Number.isFinite)) {
    return "grs must be a non-empty array of integers";
  }
  if (!adv.every(Number.isFinite) || grs.length !== adv.length + 1) {
    return "adv must be an array of integers of length grs.length - 1";
  }

  var numGroups = 0;
  for (var i = 0; i < adv.length; i += 1) {
    // loop over adv as then both a and g exist
    var a = adv[i];
    var g = grs[i];
    // calculate how big the groups are
    numGroups = Math.ceil(np / g);
    var gActual = algs.reduceGroupSize(np, g, numGroups);

    // and ensure with group reduction that eliminationValid for reduced params
    var invReason = roundInvalid(np, gActual, a, numGroups);
    if (invReason !== null) {
      return "round " + (i+1) + " " + invReason;
    }
    // return how many players left so that np is updated for next itr
    np = numGroups*a;
  }
  // last round and limit checks
  var invFinReason = finalInvalid(np, (opts && opts.limit) | 0, grs[grs.length-1]);
  if (invFinReason !== null) {
    return "final round: " + invFinReason;
  }

  // nothing found - ok to create
  return null;
};

var elimination = function (np, grs, adv, limit) {
  var invReason = invalid(np, grs, adv, {limit: limit});
  if (invReason !== null) {
    console.error("Invalid FFA configuration %dp sizes=%j, advs=%j, opts=%j"
      , np, grs, adv, {limit: limit});
    console.error("reason: %s", invReason);
    return {};
  }
  //console.log('creating %dp FFA elimination (%j/%j advancing)', np, adv, grs);

  var matches = []; // pushed in sort order
  // rounds created iteratively - know configuration valid at this point so just
  // repeat the calculation in the validation
  for (var i = 0; i < grs.length; i += 1) {
    var a = adv[i]
      , gs = grs[i]
      , numGroups = Math.ceil(np / gs)
      , gsActual = algs.reduceGroupSize(np, gs, numGroups);

    // irrelevant which gs we use as algs.groups reduces anyway
    // though might as well save it the effort and we need it here anyway
    var grps = algs.groups(np, gsActual);
    if (numGroups !== grps.length) {
      throw new Error("internal FFA construction error");
    }
    if (i > 0) {
      // only fill in seeding numbers for round 1, otherwise placeholders
      grps = unspecify(grps);
    }

    // fill in matches
    for (var m = 0; m < grps.length; m += 1) {
      matches.push({id: {s: 1, r: i+1, m: m + 1}, p: grps[m]}); // matches 1-indexed
    }
    // reduce players left (for next round - which will exist if a is defined)
    np = numGroups*a;
  }
  return matches;
};

// interface
function FFA(numPlayers, grs, advs, opts) {
  if (!(this instanceof FFA)) {
    return new FFA(numPlayers, grs, advs, opts);
  }
  this.version = 1;
  this.advs = advs;
  this.limit = opts ? opts.limit | 0 : 0;
  this.numPlayers = numPlayers;
  Base.call(this, elimination(numPlayers, grs, advs, this.limit));
}
FFA.prototype = Object.create(Base.prototype);
FFA.parse = Base.parse.bind(null, FFA);
FFA.invalid = invalid;
FFA.idString = idString;
FFA.prototype.rep = idString; // for now

FFA.prototype.unscorable = function (id, score, allowPast) {
  var invReason = Base.prototype.unscorable.call(this, id, score, allowPast);
  if (invReason !== null) {
    return invReason;
  }
  var adv = this.advs[id.r - 1] || 0;
  if (adv > 0 && score[adv] === score[adv - 1]) {
    return "scores must unambiguous decide who advances";
  }
  if (!adv && this.limit > 0) {
    // number of groups in last round is the match number of the very last match
    // because of the ordering this always works!
    var lastNG = this.matches[this.matches.length-1].id.m;
    var cutoff = this.limit/lastNG; // NB: lastNG divides limit (from finalInvalid)
    if (score[cutoff] === score[cutoff - 1]) {
      return "scores must decide who advances in final round with limits";
    }
  }
  return null;
};

{'fraction_non_alphanumeric': 0.13722893560668764, 'fraction_numerical': 0.007780168846217513, 'mean_word_length': 2.8370911400444587, 'pattern_counts': {'":': 0, '<': 19, '<?xml version=': 0, '>': 25, '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': '2151056', 'n_tokens_mistral': 4163, 'n_tokens_neox': 4016, 'n_words': 1446}