<commit_before>import Promise from 'bluebird'
import {savePools, addPlayerIdsToPool} from 'src/server/db/pool'
import {flatten} from 'src/common/util'
import {shuffle, range} from 'src/server/util'
import getActivePlayersInChapter from 'src/server/actions/getActivePlayersInChapter'

const MAX_POOL_SIZE = 15
/* eslint-disable key-spacing */
const LEVELS = [
  {level: 0, xp:    0, elo:    0},
  {level: 1, xp:    0, elo:  850},
  {level: 2, xp:  150, elo: 1000},
  {level: 3, xp:  500, elo: 1050},
  {level: 4, xp:  750, elo: 1100},
  {level: 5, xp: 1000, elo: 1150},
]
/* eslint-enable key-spacing */
const LEVELS_DESC = LEVELS.slice().reverse()

const POOL_NAMES = [
  'Red',
  'Orange',
  'Yellow',
  'Green',
  'Blue',
  'Indigo',
  'Violet',
  'Black',
  'White',
  'Silver',
  'Gold',
]

export default async function createPoolsForCycle(cycle) {
  const players = await getActivePlayersInChapter(cycle.chapterId)
  const poolAssignments = _splitPlayersIntoPools(players)
  const poolCount = poolAssignments.length

  const poolIds = await _savePools(cycle, poolCount)

  await Promise.map(poolIds, (poolId, i) => {
    const playerIds = poolAssignments[i].map(_ => _.id)
    return addPlayerIdsToPool(poolId, playerIds)
  })
}

function _splitPlayersIntoPools(players) {
  const playerLevelById = players.reduce((result, player) => {
    result.set(player.id, _getLevel(player))
    return result
  }, new Map())

  const playersPerLevel = LEVELS.map(() => [])

  players.forEach(player => {
    const level = playerLevelById.get(player.id)
    playersPerLevel[level].push(player)
  })

  return playersPerLevel
    .filter(_ignoreEmptyLevels)
    .reduce(_splitLargeLevels, [])
}

function _ignoreEmptyLevels(players) {
  return players.length > 0
}

function _splitLargeLevels(result, playersForLevel) {
  const levelSize = playersForLevel.length

  if (levelSize <= MAX_POOL_SIZE) {
    result.push(playersForLevel)
    return result
  }

  const splitCount = Math.ceil(levelSize / MAX_POOL_SIZE)
  const playersPerSplit = Math.ceil(levelSize / splitCount)
  const players = shuffle(playersForLevel.slice())
  range(0, splitCount).forEach(() => {
    result.push(players.splice(0, playersPerSplit))
  })

  return result
}

function _getLevel(player) {
  const elo = _playerElo(player)
  const xp = _playerXp(player)

  for (const {level, xp: lvlXp, elo: lvlElo} of LEVELS_DESC) {
    if (xp >= lvlXp && elo >= lvlElo) {
      return level
    }
  }

  throw new Error(`Could not place this player in ANY level! ${player.id}`)
}

function _playerElo(player) {
  return parseInt(((player.stats || {}).elo || {}).rating, 10) || 0
}

function _playerXp(player) {
  return parseInt((player.stats || {}).xp, 10) || 0
}

async function _savePools(cycle, count) {
  const changes = await savePools(
    POOL_NAMES
      .slice(0, count)
      .map(name => ({cycleId: cycle.id, name}))
  , {returnChanges: true})
  const poolIds = flatten(changes.map(_ => _.generated_keys))
  return poolIds
}<commit_msg>save level along with pool information<commit_after>import Promise from 'bluebird'
import {savePools, addPlayerIdsToPool} from 'src/server/db/pool'
import {flatten} from 'src/common/util'
import {shuffle, range} from 'src/server/util'
import getActivePlayersInChapter from 'src/server/actions/getActivePlayersInChapter'

const MAX_POOL_SIZE = 15
/* eslint-disable key-spacing */
const LEVELS = [
  {level: 0, xp:    0, elo:    0},
  {level: 1, xp:    0, elo:  850},
  {level: 2, xp:  150, elo: 1000},
  {level: 3, xp:  500, elo: 1050},
  {level: 4, xp:  750, elo: 1100},
  {level: 5, xp: 1000, elo: 1150},
]
/* eslint-enable key-spacing */
const LEVELS_DESC = LEVELS.slice().reverse()

const POOL_NAMES = [
  'Red',
  'Orange',
  'Yellow',
  'Green',
  'Blue',
  'Indigo',
  'Violet',
  'Black',
  'White',
  'Silver',
  'Gold',
]

export default async function createPoolsForCycle(cycle) {
  const players = await getActivePlayersInChapter(cycle.chapterId)
  const poolAssignments = _splitPlayersIntoPools(players)
  await _savePoolAssignments(cycle, poolAssignments)
}

function _splitPlayersIntoPools(players) {
  const playerLevelById = players.reduce((result, player) => {
    result.set(player.id, _getLevel(player))
    return result
  }, new Map())

  const playersPerLevel = LEVELS.map(() => [])

  players.forEach(player => {
    const level = playerLevelById.get(player.id)
    playersPerLevel[level].push(player)
  })

  return playersPerLevel.reduce(_poolsForLevels, [])
}

function _poolsForLevels(result, playersForLevel, level) {
  const levelSize = playersForLevel.length

  if (levelSize === 0) {
    return result
  }

  if (levelSize <= MAX_POOL_SIZE) {
    result.push({level, players: playersForLevel})
    return result
  }

  // ensure no more than MAX_POOL_SIZE in any given pool
  const splitCount = Math.ceil(levelSize / MAX_POOL_SIZE)
  const playersPerSplit = Math.ceil(levelSize / splitCount)
  const players = shuffle(playersForLevel.slice())
  range(0, splitCount).forEach(() => {
    result.push({level, players: players.splice(0, playersPerSplit)})
  })

  return result
}

function _getLevel(player) {
  const elo = _playerElo(player)
  const xp = _playerXp(player)

  for (const {level, xp: lvlXp, elo: lvlElo} of LEVELS_DESC) {
    if (xp >= lvlXp && elo >= lvlElo) {
      return level
    }
  }

  throw new Error(`Could not place this player in ANY level! ${player.id}`)
}

function _playerElo(player) {
  return parseInt(((player.stats || {}).elo || {}).rating, 10) || 0
}

function _playerXp(player) {
  return parseInt((player.stats || {}).xp, 10) || 0
}

async function _savePoolAssignments(cycle, poolAssignments) {
  const poolInfos = poolAssignments.map(({level}, poolIdx) => ({
    level,
    name: POOL_NAMES[poolIdx],
    cycleId: cycle.id,
  }))
  const changes = await savePools(poolInfos, {returnChanges: true})
  const poolIds = flatten(changes.map(_ => _.generated_keys))

  await Promise.map(poolIds, (poolId, i) => {
    const playerIds = poolAssignments[i].players.map(_ => _.id)
    return addPlayerIdsToPool(poolId, playerIds)
  })
}
{'fraction_non_alphanumeric': 0.1376823471562039, 'fraction_numerical': 0.017210293394525487, 'mean_word_length': 4.153716216216216, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 24, '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': '6120950', 'n_tokens_mistral': 2275, 'n_tokens_neox': 2148, 'n_words': 502}